Giter Site home page Giter Site logo

humanmade / asset-loader Goto Github PK

View Code? Open in Web Editor NEW
25.0 18.0 4.0 208 KB

PHP utilities for WordPress to aid including dynamic Webpack-generated assets in themes or plugins.

Home Page: https://humanmade.github.io/asset-loader/

License: Other

PHP 100.00%

asset-loader's Introduction

Asset Loader

This plugin exposes functions which may be used within other WordPress themes or plugins to aid in detecting and loading assets generated by Webpack, including those served from local webpack-dev-server instances.

Build Status

Usage

This library is designed to work in conjunction with a Webpack configuration (such as those created with the presets in @humanmade/webpack-helpers) which generate an asset manifest file. This manifest associates asset bundle names with either URIs pointing to asset bundles on a running DevServer instance, or else local file paths on disk.

Asset_Loader provides a set of methods for reading in this manifest file and registering a specific resource within it to load within your WordPress website. The primary public interface provided by this plugin is a pair of methods, Asset_Loader\register_asset() and Asset_Loader\enqueue_asset(). To register a manifest asset call one of these methods inside actions like wp_enqueue_scripts or enqueue_block_editor_assets, in the same manner you would have called the standard WordPress wp_register_script or wp_enqueue_style functions.

<?php
namespace My_Theme\Scripts;

use Asset_Loader;

add_action( 'enqueue_block_editor_assets', __NAMESPACE__ . '\\enqueue_block_editor_assets' );

/**
 * Enqueue the JS and CSS for blocks in the editor.
 *
 * @return void
 */
function enqueue_block_editor_assets() {
  Asset_Loader\enqueue_asset(
    // In a plugin, this would be `plugin_dir_path( __FILE__ )` or similar.
    get_stylesheet_directory() . '/build/asset-manifest.json',
    // The handle of a resource within the manifest. For static file fallbacks,
    // this should also match the filename on disk of a build production asset.
    'editor.js',
    [
      'handle'       => 'optional-custom-script-handle',
      'dependencies' => [ 'wp-element', 'wp-editor' ],
    ]
  );

  Asset_Loader\enqueue_asset(
    // In a plugin, this would be `plugin_dir_path( __FILE__ )` or similar.
    get_stylesheet_directory() . '/build/asset-manifest.json',
    // Enqueue CSS for the editor.
    'editor.css',
    [
      'handle'       => 'custom-style-handle',
      'dependencies' => [ 'some-style-dependency' ],
    ]
  );
}

Documentation

For complete documentation, including contributing process, visit the docs site.

License

This plugin is free software. You can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.

asset-loader's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

asset-loader's Issues

Support plugins outside of `wp-content/plugins`.

Asset_Loader\Paths\plugin_or_theme_file_uri() currently only works for plugins inside the wp-content/plugins/ folder. This is a limitation of plugin_dir_url() which is used.

On a VIP Go project, it's not uncommon for plugins to be located in wp-content/client-mu-plugins/, and this doesn't work with plugin_dir_url().

Facilitate hashed file names from webpack

Webpack allows you to configure the output.filename property of an asset and include a hash in it, such as [name].[hash].js.

Using this naming convention means you can skip using the $ver parameter when enqueueing the asset in WordPress. It would be great if Asset Loader either transparently or optionally allowed for this, allowing the md5_file() version usage to be skipped.

Support loading scripts in the head

We make an assumption in this project that all scripts should be loaded in the footer. This may not always be the case; custom logic impacting ad tech, for example, should be output onto the page as soon as possible. The registration functions should be able to support marking a script for inclusion in the head, not the footer.

Support hashed production filenames

Currently the way we match files on disk is by direct filename match, e.g. theme.js. This breaks down if the Webpack config specifies a hashed value in the output filename. We should support loading hashed files in production mode, or clarify explicitly that it is prohibited if it becomes too complex.

Do not fatal when trying to enqueue asset from missing manifest location

This plugin will currently throw a hard error when we try to load the page if we try to enqueue an asset from a manifest that doesn't exist. We should adjust this behavior so that it logs a warning, but does not actually stop loading the page, because having it completely error prevents quick bisecting between branches that need builds (among other things).

The error we see sometimes:

 Fatal error: Uncaught Error: Asset_Loader\enqueue_asset(): Argument #1 ($manifest_path) must be of type string, null given, called in /wp/wp-content/themes/themename/functions.php on line 139
in /wp/wp-content/plugins/asset-loader/inc/namespace.php on line 170

This is a consequence of themes using this pattern:

/**
 * Check through the available manifests to find the first which includes the
 * target asset. This allows some assets to be loaded from a running DevServer
 * while others load from production files on disk.
 *
 * @param string $target_asset Desired asset within the manifest.
 * @return string|null
 */
function myproject_custom_get_manifest_path( $target_asset ) {
	$manifests = [
		get_template_directory() . '/assets/dist/development-asset-manifest.json',
		get_template_directory() . '/assets/dist/production-asset-manifest.json',
	];
	foreach ( $manifests as $manifest_path ) {
		$asset_uri = \Asset_Loader\Manifest\get_manifest_resource( $manifest_path, $target_asset );
		if ( ! empty( $asset_uri ) ) {
			return $manifest_path;
		}
	}
}


function mytheme_enqueue_scripts() {
	Asset_Loader\enqueue_asset(
		myproject_custom_get_manifest_path( 'my-script-bundle.js' ),
		'my-script-bundle.js',
		[
			'dependencies' => [],
			'handle' => 'my-script-bundle',
		]
	);
}

This code should be improved -- The function which looks up a resource in a series of manifests should not return null if no matching asset is found. But that theme code quality concern still shouldn't cause a hard error because of this plugin.

Proposed change

Within Asset Loader, we should alter the enqueue_asset and register_asset functions to allow them to receive a ?string instead of a string, and add an if ( empty( $manifest_path ) ) { at the start of register_asset so that it will detect missing manifests before we try to register the asset. If the manifest is missing, we should log an informational error to the logs, and return an empty array from register_asset(). (Returning an empty array from that method will cause enqueue_asset to take no further action.)

Steps to reproduce

  • In a vip dev-env environment that is using a theme using this library, (contact me for an example, if HM-internal),
  • ensure the built files are not yet present (e.g. delete build directory or check out non-release branch)
  • try to load a page on the site
  • observe that the error shown above is thrown after no manifest can be found

Version 1.0.0 - what's blocking this?

At time of writing, the current release is v0.6.0.

This is causing a few headaches with updates, as Composer (understandably) treats anything before 1.0 as pre-release.

i.e. while a constraint of ^1.0.0 will happily update to 1.1.0, ^0.5.0 will not update to 0.6.0.

Given that this library is already in use by a lot of production projects and is pretty stable, I'd argue it's no longer something I'd call pre-release. What would we still like to achieve before declaring it stable, and updating to v1.0.0?

  • Documentation?
  • Additional filters and actions?

composer.json is not valid

This package is not installable with Composer because its composer.json is invalid. It needs at least the name and type field - not sure if others, we should able to figure it out.

Introduce an add_editor_style function

add_editor_style takes a theme-relative path to a stylesheet, and enqueues that stylesheet as an editor asset. Given that hashed filenames can make it challenging to use editor stylesheets, we should consider introducing a helper method which can take the name of a bundle in the manifest and ensure it gets ingested as an editor stylesheet.

Pseudo-code from #35 which we'd want to wrap in an Asset_Loader\add_editor_style() top-level function:

	$editor_stylesheet = Asset_Loader\Manifest\get_manifest_resource(
		get_theme_file_path( 'dist/production-asset-manifest.json' ),
		'tetramino.css'
	);

	add_editor_style( "dist/$editor_stylesheet" );

Please provide Gutenberg-ready example in docs

Hi @kadamwhite

I've been using asset-loader on the last few projects I've worked on, and alongside the webpack helpers package, really enjoy the developer experience when it all works, so kudos to you for putting that together.

An area where I consistently struggle with, and waste a surprising amount of time on, is the new project initial set-up, where I might set up all the build tools etc so it's ready to go for the rest of my dev teams.

It'd be really valuable if, at least, there were docs that explained how to make the most of asset-loader for a typical plugin that adds a Gutenberg block(s). I'm sure you'd agree there's some community best practise for the file layout of a Gutenberg block, such as potentially different CSS for the front-end vs the Gutenberg editor, and I always struggle getting that kind of structure to work with asset-loader.

I think some sample code that I could copy/paste in to a project as a starting point would shave down dramatically some of the time.

I get there in the end, but I'd estimate it takes me 1 to 1.5 days to get asset-loader set-up on a new project (for blocks, and for eslint etc), and I'd love to see that come down. Much of the time is spent debugging the PHP and understanding why it's not working.
(For example, I know the second parameter of autoregister() etc needs to match the "entry" object name in the Webpack config, but I seem to have to re-discover that myself for each new project.)

Bundle naming conflicts with parent / child themes

The entry bundle keys for environments must be unique, specifying the same key for a parent theme and a child theme means the parent bundle is not loaded.

Instead of prefixing entry bundle keys with parent_ this scenario would be avoided if the environment name would be prepended to the final bunlde name, resulting in $name-$bundle.

Example of a problematic configuration (both the parent and child theme create a styles bundle):

module.exports = choosePort( 8080 ).then( port => [
	presets.development( {
		name: 'parent-theme',
		...shared,
		devServer : {
			...devServer,
			port,
		},
		entry: {
			styles: filePath( `${paths.theme_parent}/style.css` ),
		},
		output: {
			path: filePath( `${ paths.theme_parent }/build` ),
			publicPath: `https://${ host }:${ port }/parent-theme/`,
		},
	} ),
	presets.development( {
		name: 'child-theme',
		...shared,
		devServer : {
			...devServer,
			port,
		},
		entry: {
			styles: filePath( `${paths.theme_child}/style.css` ),
		},
		output: {
			path: filePath( `${ paths.theme_child }/build` ),
			publicPath: `https://${ host }:${ port }/child-theme/`,
		},
	} ),

Publish releases on GitHub, or remove existing ones

When you view the repo front page it looks like the latest version is 0.3.4 because that was the latest tag that has a published release for it. There are subsequent tags for 0.4.0, 0.4.1, and 0.5.0.

Options:

  1. Publish a release for the new tags and keep doing so
  2. Delete the existing releases and stick to just tagging

Cannot enqueue CSS and JS asset with the same handle

In the WordPress world it's possible for a CSS dependency and a JS dependency to share the same handle. When using Asset Loader, this works fine for a production build but not when using webpack-dev-server during dev.

It appears that the latterly registered dependency gets skipped completely. In the example below, the main.js file gets enqueued but the style.css file does not.

\Asset_Loader\enqueue_asset(
	plugin_dir_path( __DIR__ ) . 'build/asset-manifest.json',
	'main.js',
	[
		'handle' => 'hello-world',
	]
);
\Asset_Loader\enqueue_asset(
	plugin_dir_path( __DIR__ ) . 'build/asset-manifest.json',
	'style.css',
	[
		'handle' => 'hello-world',
	]
);

May be related to #27

Support script attributes like "async", "defer", "preload"

We should have a way of registering that a script should be rendered with an async attribute, or included in a preload list.

In another project using this asset loader, a team recently wrote a custom wrapper which used this logic to set the "defaults" for scripts:

  • If async is not explicitly set, and the script has no dependencies, then default to async loading.
  • If defer is not explicitly set, but the script has dependencies, then default to defer loading.

Then when registering the script the wrapper would add the relevant parameters:

		foreach ( [ 'async', 'defer' ] as $attr ) {
			if ( ! empty( $args[ $attr ] ) ) {
				wp_script_add_data( $args['handle'], $attr, true );
				break;
			}
		}

Then, filter the script based on that data:

function filter_script_loader_tag( string $tag, string $handle ) : string {
	foreach ( [ 'async', 'defer' ] as $attr ) {
		if ( ! wp_scripts()->get_data( $handle, $attr ) ) {
			continue;
		}

		// Prevent adding attribute when already added in #12009.
		if ( ! preg_match( ":\s$attr(=|>|\s):", $tag ) ) {
			$tag = preg_replace( ':(?=></script>):', " $attr", $tag, 1 );
		}

		// Only allow async or defer, not both.
		break;
	}

	return $tag;
}

The note about #12009 refers to this ticket to add async and defer handling to core wp_enqueue_script itself.

Cache manifests in a static var to reduce FS interaction on repeat access

On a current client project we ended up re-writing the loader to something more like this:

/**
 * 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;
 */
function loadAssetManifest($path) : ?array
{
    // Avoid repeatedly opening & decoding the same file.
    static $manifests = [];

    if (isset($manifests[$path])) {
        return $manifests[$path];
    }

    if (! file_exists($path)) {
        return null;
    }

    $contents = file_get_contents($path);

    if (empty($contents)) {
        return null;
    }

    $manifests[$path] = json_decode($contents, true);

    return $manifests[$path];
}

This seems to work nicely to avoid repeatedly accessing the same file on disk, a change which enabled us to alter the external API of the module from the somewhat overly-magic enqueue methods, to a more specific set of registerAsset() and enqueueAsset() methods

See the rest of that implementation here
/**
 * Attempt to extract a specific value from an asset manifest file.
 *
 * @param string $manifestPath File system path for an asset manifest JSON file.
 * @param string $asset        Asset to retrieve within the specified manifest.
 * @return string|null;
 */
function getManifestResource(string $manifestPath, string $asset) : ?string
{
    $devAssets = loadAssetManifest($manifestPath);

    if (! isset($devAssets[$asset])) {
        return null;
    }

    return $devAssets[$asset];
}

/**
 * Helper function to naively check whether or not a given URI is a CSS resource.
 *
 * @param string $uri A URI to test for CSS-ness.
 * @return boolean Whether that URI points to a CSS file.
 */
function isCSS(string $uri) : bool
{
    return preg_match('/\.css(\?.*)?$/', $uri) === 1;
}

/**
 * Attempt to register a particular script bundle from a manifest.
 *
 * @param string $manifestPath File system path for an asset manifest JSON file.
 * @param string $targetAsset  Asset to retrieve within the specified manifest.
 * @param array  $options {
 *     @type string $handle          Handle to use when enqueuing the style/script bundle.
 *                                   Required.
 *     @type string $transformDevURI Search-replace string replacement to apply to URIs in
 *                                   development environment. Optional.
 *     @type array  $scripts         Script dependencies. Optional.
 *     @type array  $styles          Style dependencies. Optional.
 * }
 * @return void;
 */
function registerAsset(string $manifestPath, string $targetAsset, array $options = []) : void
{
    $defaults = [
        'transformDevURI' => [],
        'scripts' => [],
        'styles' => [],
    ];
    $options = wp_parse_args($options, $defaults);

    $assetURI = getManifestResource($manifestPath, $targetAsset);

    if (empty($assetURI)) {
        // @TODO: Signal or log that enqueue has failed, if not isCSS($assetURI).
        // Failure is allowed for CSS files as they are not exported in dev builds.
        return;
    }

    $isDevelopment = getenv('APPLICATION_ENV') === 'development';

    if ($isDevelopment && count($options['transformDevURI']) === 2) {
        list($from, $to) = $options['transformDevURI'];
        $assetURI = str_replace($from, $to, $assetURI);
    }

    if (isCSS($assetURI)) {
        wp_register_style(
            $options['handle'],
            $assetURI,
            $options['styles']
        );
    } else {
        wp_register_script(
            $options['handle'],
            $assetURI,
            $options['scripts'],
            false,
            true
        );
    }
}

/**
 * Attempt to register and then enqueue a particular script bundle from a manifest.
 *
 * @param string $manifestPath File system path for an asset manifest JSON file.
 * @param string $targetAsset  Asset to retrieve within the specified manifest.
 * @param array  $options {
 *     @type string $handle          Handle to use when enqueuing the style/script bundle.
 *                                   Required.
 *     @type string $transformDevURI Search-replace string replacement to apply to URIs in
 *                                   development environment. Optional.
 *     @type array  $scripts         Script dependencies. Optional.
 *     @type array  $styles          Style dependencies. Optional.
 * }
 * @return void;
 */
function enqueueAsset(string $manifestPath, string $targetAsset, array $options = []) : void
{
    registerAsset($manifestPath, $targetAsset, $options);

    // $targetAsset will share a filename extension with the enqueued asset.
    if (isCSS($targetAsset)) {
        wp_enqueue_style($options['handle']);
    } else {
        wp_enqueue_script($options['handle']);
    }
}

(the isDevelopment check would need to change, but directionally I'm actually a lot more fond of this approach than our current set of public methods)

Support CSS-only entrypoints.

Currently the way to support a CSS-only entrypoint is to pretend it is JS:

	Asset_Loader\autoenqueue(
		get_stylesheet_directory() . '/build/asset-manifest.json',
		'style.js', // Note: no JS file will exist.
		[
			'scripts' => [],
			'handle'  => 'mytheme-base-style',
		]
	);

This is confusing and should not be necessary.

Functions with signatures more similar to those in WordPress

When switching from using wp_enqueue_script() and co to using this package's autoenqueue(), the difference in the function interfaces is a little jarring. I'd like to recommend the addition of two helper functions which:

  • Use signatures a little more familiar to WordPress developers in order to make the transition easier
  • Remove the need to specify an asset manifest path which is IMO an implementation detail
  • Get around #1 by silently rewriting a .css path to .js (which is working for theme stylesheets on the SW project)
function register_asset( string $handle, string $file, array $options = [] ) :? array {
	$file = str_replace( '.css', '.js', $file );
	$manifest = dirname( $file ) . '/build/asset-manifest.json';
	$options['handle'] = $handle;
	return autoregister( $manifest, basename( $file ), $options );
}

function enqueue_asset( string $handle, string $file, array $options = [] ) : void {
	$file = str_replace( '.css', '.js', $file );
	$manifest = dirname( $file ) . '/build/asset-manifest.json';
	$options['handle'] = $handle;
	autoenqueue( $manifest, basename( $file ), $options );
}

Usage looks like:

enqueue_asset(
	'theme-style',
	get_stylesheet_directory(). '/style.css',
	[
		'styles' => [ 'dashicons' ],
	]
);

Thoughts? I can work on a PR if we agree these would be useful additions.

Version inference logic doesn't work in some builds

Reading the md5 of the asset manifest is an invalid way to infer the version of a build, since with a file like,

{
    "editor": "editor.js"
}

the contents of the file will never change and all builds will use the same version number. This can cause stale assets due to various layers of caching.

Permit styles to be registered, not fully enqueued

If you've split your application up into separate bundles, you might want to enqueue one only on archive pages for a specific type of record, etc. While it's possible to do this by checking context within the enqueue styles method, it would be cleaner to provide a flag to the autoenqueue function (or an alternate version, perhaps autoregister?) which will not complete the registration, leaving the theme or plugin developer to manually trigger that enqueue later.

Incorrect load order for inline CSS when hot reloading

Given an enqueued stylesheet that also includes some inline CSS, the main stylesheet incorrectly takes precedence over the inline CSS when using dev mode (eg. with Webpack dev server from Webpack Helpers).

Example

Given a theme's style.css that contains the following (make paragraphs red):

p { color: red; }

Use the following to enqueue the style and then add an inline style that uses the same specificity selector (make paragraphs green):

add_action( 'wp_enqueue_scripts', function() {

    \Asset_Loader\autoenqueue(
        get_template_directory() . '/build/asset-manifest.json',
        // style.css - https://github.com/humanmade/asset-loader/issues/1
        'style.js',
        [
            'handle' => 'my-style',
        ]
    );

    wp_add_inline_style( 'my-style', 'p { color: green; }' );
} );

When loading the site with the Webpack dev server running, the inline style will not take precedence as it should (your paragraphs will unexpectedly be red). This appears to be due to the way that Webpack dev server loads the CSS is as JavaScript.

I've not looked into the root cause but Ryan suggested this might be due to Asset Loader not telling style-loader about which <link> element it should be replacing. Needs confirming.

Do not fatal if manifest is missing (type hinting)

(extracted from a comment on #40)

Probably the biggest risk / gotcha I run into regularly with Asset Loader right now is that if you try to enqueue a file and there is no manifest on disk, get_asset_manifest() returns null which is not a string which causes a 500.

Argument 1 passed to Asset_Loader\enqueue_asset() must be of the type string, null given

This is a rough edge we should probably smooth off, by one or more of

  • removing the type constraint
  • making the manifest function return an empty string instead of null
  • and possibly log a warning within (enqueue|register)_asset if the manifest is missing

I'm personally most in favor of removing the constraint and possibly adding an error, but curious for thoughts. @Sephsekla et al

Paths\get_file_uri() doesn't work with symlinks

Initially bug reported in humanmade/altis-core#505.

When wp-content is a symlink, Paths\get_file_uri() will fail to generate the correct URI. Looking at the code at https://github.com/humanmade/asset-loader/blob/main/inc/paths.php#L69:

For example, given:

ABSPATH=/var/www/html/wordpress
WP_CONTENT_DIR=/var/www/html/wordpress/wp-content

/var/www/html/wordpress/wp-content -> symlinked -> /www/html/wp-content
$path = "/www/html/wp-content/plugins/altis/vendor/altis/aws-analytics/build/accelerate.js";
return content_url( str_replace( WP_CONTENT_DIR, '', $path ) );

WP_CONTENT_DIR is not present in $path so this will then fail.

Reconsider `filemtime()` for version reference.

On multi-system environments, filemtime() can differ on each system.

Changing the value to a hash of the file contents or another property consistent across properties will prevent the same version of the file having different cache busting strings depending on the server used.

Support editor styles

It's a bit of a dance to get editor styles enqueued via Asset_Loader -- should be a single top-level command, ideally.

	$editor_stylesheet = Asset_Loader\Manifest\get_manifest_resource(
		get_theme_file_path( 'dist/production-asset-manifest.json' ),
		'tetramino.css'
	);

	add_editor_style( "dist/$editor_stylesheet" );

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.