Skip to content

Commit

Permalink
Merge pull request #5 from masonitedoors/remote-cra-support
Browse files Browse the repository at this point in the history
Remote cra support
  • Loading branch information
josephfusco authored Jan 13, 2021
2 parents 78fa9dd + 5b60e71 commit 5b1c8b1
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 26 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.4.0] - 2021-01-13

### Added

- Added support for loading a remote react app.

## [1.3.0] - 2020-03-10

### Added
Expand Down
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ The `register` method has 4 required parameters and should be called within the
| :---------------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| \$slug | string | The page slug where the React app will live on the site. The loader will also reserve this slug with WordPress, preventing any new posts from being made at the same URL. If any existing posts share the defined slug, they will not be able to be accessed on the front-end of the site once rewrite rules are flushed. |
| \$root_id | string | The id of the root element that the React app should mount to. By default, Create React App has this as `'root'`. |
| \$plugin_dir_path | string | The absolute path to the plugin directory that contains the react app. In most situations, this should be `plugin_dir_path( __FILE__ )`. |
| \$cra_directory | string | The absolute path to the plugin directory that contains the react app. In most situations, this should be `plugin_dir_path( __FILE__ )`. This can also be a URL to the base directory of a React app on a different website. |
| \$role | string | The WordPress user role required to view the page. If a user tries to access the page without this role, they will be redirected to the site's [home_url()](https://developer.wordpress.org/reference/functions/home_url/). If no authentication is needed, this should be set as `'nopriv'`. |
| \$callback | callable | Optional callback function. This is only fired on the registered page before the React app assets are enqueued. |
| \$wp_permalinks | array | Optional array of subdirectories off of the defined slug that we DO WANT WordPress to handle. |
Expand All @@ -83,7 +83,6 @@ add_action( 'plugins_loaded', function() {
);
});
```

### URL Structure

When a React plugin is registered with this loader plugin, a [virtual page](https://metabox.io/how-to-create-a-virtual-page-in-wordpress/) is created within WordPress. As a result, this new page will not show up within the regular pages/posts list in wp-admin. Because of the nature of creating a virtual page by adding new rewrite rules to WordPress, the rewrite rules will need to be flushed before the new page will be accessible.
Expand All @@ -106,6 +105,24 @@ wp rewrite flush

Trailing slash has been removed for registerd React app pages. This was done in an effort to create consistency in behavior with create-react-app's node-server structure (the environment that fires up when you run `npm start`).

#### Remote React App Support

Support for loading React apps hosted on other websites is available as of version __1.4.0__.

This can be achived by using a URL to the root of the React app For the 3rd argument in the register method.
If your React app's asset-manifest.json is https://example.org/my-react-app/asset-manifest.json, use the first part of the URL (omit asset-manifest.json).

```php
add_action( 'plugins_loaded', function() {
\ReactAppLoader\API::register(
'my-react-app-slug',
'root',
'https://example.org/my-react-app/',
'nopriv'
);
});
```

## Recommendations

### Using Images Within Your React App
Expand Down
8 changes: 4 additions & 4 deletions lib/API.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,20 @@ class API {
*
* @param string $slug The slug to tell WordPress to stop handling so react can handle routing.
* @param string $root_id The id of the root element we will be mounting our react app to.
* @param string $plugin_dir_path The absolute path to the plugin directory that contains the react app.
* @param string $cra_directory The absolute path to the plugin directory that has the CRA based react app. Can also be a URL to a remote CRA base react app.
* @param string $role The role required to view the page.
* @param callable $callback The callback function that fires before assets are enqueued to the page.
* @param array $wp_permalinks An array of subdirectories off of the defined slug that we DO WANT WordPress to handle.
*/
public static function register( $slug, $root_id, $plugin_dir_path, $role, $callback = false, $wp_permalinks = [] ) : void {
public static function register( $slug, $root_id, $cra_directory, $role, $callback = false, $wp_permalinks = [] ): void {
add_action(
'init',
function() use ( $slug, $root_id, $plugin_dir_path, $role, $callback, $wp_permalinks ) {
function() use ( $slug, $root_id, $cra_directory, $role, $callback, $wp_permalinks ) {
$virtual_page = new Virtual_Page();
$virtual_page->create(
$slug,
$root_id,
$plugin_dir_path,
$cra_directory,
$role,
$callback,
$wp_permalinks
Expand Down
64 changes: 61 additions & 3 deletions lib/Assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class Assets {
* @type array $styles Style dependencies.
* }
*/
public function enqueue( $directory, $opts = [] ) : void {
public function enqueue( $directory, $opts = [] ): void {
$defaults = [
'base_url' => '',
'handle' => basename( $directory ),
Expand Down Expand Up @@ -92,11 +92,69 @@ public function enqueue( $directory, $opts = [] ) : void {
}
}

/**
* Enqueue our remote react app assets.
*
* @param string $base_url Base URL to our remote react app.
*/
public function enqueue_remote( $base_url ): void {
$assets = self::fetch_remote_assets( $base_url );

foreach ( $assets as $asset_url ) {
$is_js = preg_match( '/\.js$/', $asset_url );
$is_css = preg_match( '/\.css$/', $asset_url );
$handle = sanitize_key( basename( $asset_url ) );

if ( $is_js ) {
wp_enqueue_script( $handle, $asset_url, [], null, true );
} elseif ( $is_css ) {
wp_enqueue_style( $handle, $asset_url, [] );
}
}
}

/**
* Makes a GET request to the remote react app to fetch the asset-manifest.json.
* Returns an array of entrypoints but with absolute URLs.
*
* @param string $base_url The base URL for the asset-manifest.json.
* @return array
*/
private static function fetch_remote_assets( $base_url ) {
$url = trailingslashit( $base_url ) . 'asset-manifest.json';
$response = wp_remote_get( $url );

if ( is_wp_error( $response ) ) {
return [];
}

$body = wp_remote_retrieve_body( $response );
$assets = json_decode( $body, true );

if ( empty( $assets ) ) {
return [];
}

if ( ! array_key_exists( 'entrypoints', $assets ) ) {
trigger_error( 'React App Loader: Entrypoints key was not found within your react app\'s asset-manifest.json. This may indicate that you are using an unsupported version of react-scripts. Your react app should be using react-scripts@3.2.0 or later.', E_USER_WARNING ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
return [];
}

$filtered_assets = array_map(
function( $asset_path ) use ( $base_url ) {
return "$base_url/$asset_path";
},
array_values( $assets['entrypoints'] )
);

return $filtered_assets;
}

/**
* Attempt to load a file at the specified path and parse its contents as JSON.
*
* @param string $path The path to the JSON file to load.
* @return array|null;
* @return array|null
*/
public static function load_asset_file( $path ) {
if ( ! file_exists( $path ) ) {
Expand All @@ -117,7 +175,7 @@ public static function load_asset_file( $path ) {
* decode and return the asset list JSON if found.
*
* @param string $directory Root directory containing `src` and `build` directory.
* @return array|null;
* @return array|null
*/
public static function get_assets_list( string $directory ) {
$directory = trailingslashit( $directory );
Expand Down
56 changes: 39 additions & 17 deletions lib/Virtual_Page.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ class Virtual_Page {
private $root_id;

/**
* The absolute path to the plugin directory that contains the react app.
* The absolute path to the plugin directory that has the CRA based react app. Can also be a URL to a remote CRA base react app.
*
* @var string
*/
private $plugin_dir_path;
private $cra_directory;

/**
* The user role required to view to view this page.
Expand Down Expand Up @@ -65,19 +65,19 @@ class Virtual_Page {
*
* @param string $slug The slug to tell WordPress to stop handling so react can handle routing.
* @param string $root_id The id of the root element we will be mounting our react app to.
* @param string $plugin_dir_path The absolute path to the plugin directory that contains the react app.
* @param string $cra_directory The absolute path to the plugin directory that has the CRA based react app. Can also be a URL to a remote CRA base react app.
* @param string $role The role required to view the page.
* @param callable $callback The callback function that fires before assets are enqueued to the page.
* @param array $wp_permalinks An array of subdirectories off of the defined slug that we DO WANT WordPress to handle.
*/
public function create( $slug, $root_id, $plugin_dir_path, $role, $callback, $wp_permalinks ) {
$this->slug = $slug;
$this->root_id = $root_id;
$this->plugin_dir_path = $plugin_dir_path;
$this->role = $role;
$this->key = basename( $plugin_dir_path );
$this->callback = $callback;
$this->wp_permalinks = $wp_permalinks;
public function create( $slug, $root_id, $cra_directory, $role, $callback, $wp_permalinks ) {
$this->slug = $slug;
$this->root_id = $root_id;
$this->cra_directory = $cra_directory;
$this->role = $role;
$this->key = basename( $cra_directory );
$this->callback = $callback;
$this->wp_permalinks = $wp_permalinks;

$this->generate_page();
$this->disable_wp_rewrite();
Expand All @@ -88,7 +88,7 @@ public function create( $slug, $root_id, $plugin_dir_path, $role, $callback, $wp
/**
* Create the virtual page the react app with live within.
*/
public function generate_page() : void {
public function generate_page(): void {
add_filter(
'query_vars',
function( $query_vars ) {
Expand Down Expand Up @@ -127,7 +127,7 @@ function() {
/**
* Prevent WordPress from thinking that react app routes are separate WordPress pages.
*/
public function disable_wp_rewrite() : void {
public function disable_wp_rewrite(): void {
$regex_pattern = '^' . $this->slug . '/(.*)$';

if ( ! empty( $this->wp_permalinks ) ) {
Expand All @@ -145,7 +145,7 @@ public function disable_wp_rewrite() : void {
/**
* Handles various aspects of updating requests to our virtual pages.
*/
public function handle_request() : void {
public function handle_request(): void {
add_filter(
'request',
function( $request ) {
Expand Down Expand Up @@ -189,7 +189,7 @@ function( $request ) {
* In the case of a bad (i.e conflicting slug), WordPress appends a "-2" to
* the permalink.
*/
public function reserve_slug() : void {
public function reserve_slug(): void {
add_filter( 'wp_unique_post_slug_is_bad_hierarchical_slug', [ $this, 'fe_prevent_slug_conflict' ], 10, 4 );
add_filter( 'wp_unique_post_slug_is_bad_flat_slug', [ $this, 'fe_prevent_slug_conflict' ], 10, 3 );
}
Expand Down Expand Up @@ -238,6 +238,22 @@ function( $classes ) {
);
}

/**
* Check if the string is a valid URL.
*
* @param string $possible_url String to check if a URL.
* @return boolean
*/
private static function is_url( string $possible_url ) {
$parts = wp_parse_url( $possible_url );

if ( isset( $parts['host'] ) ) {
return true;
}

return false;
}

/**
* Removes the trailing slash from current request URL.
*/
Expand All @@ -254,7 +270,7 @@ function( $string ) {
/**
* Handles the displaying of our virtual page content.
*/
public function display_page_content() : void {
public function display_page_content(): void {
// Redirect user to homepage if they do not have permissions.
$user = wp_get_current_user();

Expand All @@ -265,7 +281,13 @@ public function display_page_content() : void {
}

$assets = new Assets();
$assets->enqueue( $this->plugin_dir_path );

// Handle the loading of assets from a remote URL or a local plugin.
if ( self::is_url( $this->cra_directory ) ) {
$assets->enqueue_remote( $this->cra_directory );
} else {
$assets->enqueue( $this->cra_directory );
}

// Fire our callback if one is defined.
if ( false !== $this->callback ) {
Expand Down

0 comments on commit 5b1c8b1

Please sign in to comment.