220 lines
5.8 KiB
JavaScript
220 lines
5.8 KiB
JavaScript
|
/**
|
||
|
* Add an asset file for each entry point that contains the current version calculated for the current source code.
|
||
|
*
|
||
|
* This is modified from WP dependency-extraction-webpack-plugin plugin:
|
||
|
* https://github.com/WordPress/gutenberg/tree/a04a8e94e8b93ba60441c6534e21f4c3c26ff1bc/packages/dependency-extraction-webpack-plugin
|
||
|
*
|
||
|
* We can contribute this back to the original plugin in the future and remove this file.
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* External dependencies
|
||
|
*/
|
||
|
const path = require( 'path' );
|
||
|
const webpack = require( 'webpack' );
|
||
|
const json2php = require( 'json2php' );
|
||
|
const { createHash } = webpack.util;
|
||
|
|
||
|
const { RawSource } = webpack.sources;
|
||
|
const { AsyncDependenciesBlock } = webpack;
|
||
|
|
||
|
class AssetDataPlugin {
|
||
|
constructor( options ) {
|
||
|
this.options = Object.assign(
|
||
|
{
|
||
|
combineAssets: false,
|
||
|
combinedOutputFile: null,
|
||
|
outputFormat: 'php',
|
||
|
outputFilename: null,
|
||
|
},
|
||
|
options
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {any} asset Asset Data
|
||
|
* @return {string} Stringified asset data suitable for output
|
||
|
*/
|
||
|
stringify( asset ) {
|
||
|
if ( this.options.outputFormat === 'php' ) {
|
||
|
return `<?php return ${ json2php(
|
||
|
JSON.parse( JSON.stringify( asset ) )
|
||
|
) };\n`;
|
||
|
}
|
||
|
|
||
|
return JSON.stringify( asset );
|
||
|
}
|
||
|
|
||
|
apply( compiler ) {
|
||
|
compiler.hooks.thisCompilation.tap(
|
||
|
this.constructor.name,
|
||
|
( compilation ) => {
|
||
|
compilation.hooks.processAssets.tap(
|
||
|
{
|
||
|
name: this.constructor.name,
|
||
|
stage: compiler.webpack.Compilation
|
||
|
.PROCESS_ASSETS_STAGE_ANALYSE,
|
||
|
},
|
||
|
() => this.addAssets( compilation )
|
||
|
);
|
||
|
}
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/** @param {webpack.Compilation} compilation */
|
||
|
addAssets( compilation ) {
|
||
|
const {
|
||
|
combineAssets,
|
||
|
combinedOutputFile,
|
||
|
outputFormat,
|
||
|
outputFilename,
|
||
|
} = this.options;
|
||
|
|
||
|
const combinedAssetsData = {};
|
||
|
|
||
|
// Accumulate all entrypoint chunks, some of them shared
|
||
|
const entrypointChunks = new Set();
|
||
|
for ( const entrypoint of compilation.entrypoints.values() ) {
|
||
|
for ( const chunk of entrypoint.chunks ) {
|
||
|
entrypointChunks.add( chunk );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Process each entrypoint chunk independently
|
||
|
for ( const chunk of entrypointChunks ) {
|
||
|
const chunkFiles = Array.from( chunk.files );
|
||
|
|
||
|
const styleExtensionRegExp = /\.s?css$/i;
|
||
|
|
||
|
const chunkStyleFile = chunkFiles.find( ( f ) =>
|
||
|
styleExtensionRegExp.test( f )
|
||
|
);
|
||
|
if ( ! chunkStyleFile ) {
|
||
|
// No style file, skip
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Go through the assets and hash the sources. We can't just use
|
||
|
// `chunk.contentHash` because that's not updated when
|
||
|
// assets are minified. In practice the hash is updated by
|
||
|
// `RealContentHashPlugin` after minification, but it only modifies
|
||
|
// already-produced asset filenames and the updated hash is not
|
||
|
// available to plugins.
|
||
|
const { hashFunction, hashDigest, hashDigestLength } =
|
||
|
compilation.outputOptions;
|
||
|
|
||
|
const contentHash = chunkFiles
|
||
|
.sort()
|
||
|
.reduce( ( hash, filename ) => {
|
||
|
const asset = compilation.getAsset( filename );
|
||
|
return hash.update( asset.source.buffer() );
|
||
|
}, createHash( hashFunction ) )
|
||
|
.digest( hashDigest )
|
||
|
.slice( 0, hashDigestLength );
|
||
|
|
||
|
const assetData = {
|
||
|
version: contentHash,
|
||
|
};
|
||
|
|
||
|
if ( combineAssets ) {
|
||
|
combinedAssetsData[ chunkStyleFile ] = assetData;
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
let assetFilename;
|
||
|
if ( outputFilename ) {
|
||
|
assetFilename = compilation.getPath( outputFilename, {
|
||
|
chunk,
|
||
|
filename: chunkStyleFile,
|
||
|
contentHash,
|
||
|
} );
|
||
|
} else {
|
||
|
const suffix =
|
||
|
'.asset.' + ( outputFormat === 'php' ? 'php' : 'json' );
|
||
|
assetFilename = compilation
|
||
|
.getPath( '[file]', { filename: chunkStyleFile } )
|
||
|
.replace( styleExtensionRegExp, suffix );
|
||
|
}
|
||
|
|
||
|
// Add source and file into compilation for webpack to output.
|
||
|
compilation.assets[ assetFilename ] = new RawSource(
|
||
|
this.stringify( assetData )
|
||
|
);
|
||
|
chunk.files.add( assetFilename );
|
||
|
}
|
||
|
|
||
|
if ( combineAssets ) {
|
||
|
const outputFolder = compilation.outputOptions.path;
|
||
|
|
||
|
const assetsFilePath = path.resolve(
|
||
|
outputFolder,
|
||
|
combinedOutputFile ||
|
||
|
'assets.' + ( outputFormat === 'php' ? 'php' : 'json' )
|
||
|
);
|
||
|
const assetsFilename = path.relative(
|
||
|
outputFolder,
|
||
|
assetsFilePath
|
||
|
);
|
||
|
|
||
|
// Add source into compilation for webpack to output.
|
||
|
compilation.assets[ assetsFilename ] = new RawSource(
|
||
|
this.stringify( combinedAssetsData )
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Can we trace a line of static dependencies from an entry to a module
|
||
|
*
|
||
|
* @param {webpack.Compilation} compilation
|
||
|
* @param {webpack.DependenciesBlock} block
|
||
|
*
|
||
|
* @return {boolean} True if there is a static import path to the root
|
||
|
*/
|
||
|
static hasStaticDependencyPathToRoot( compilation, block ) {
|
||
|
const incomingConnections = [
|
||
|
...compilation.moduleGraph.getIncomingConnections( block ),
|
||
|
].filter(
|
||
|
( connection ) =>
|
||
|
// Library connections don't have a dependency, this is a root
|
||
|
connection.dependency &&
|
||
|
// Entry dependencies are another root
|
||
|
connection.dependency.constructor.name !== 'EntryDependency'
|
||
|
);
|
||
|
|
||
|
// If we don't have non-entry, non-library incoming connections,
|
||
|
// we've reached a root of
|
||
|
if ( ! incomingConnections.length ) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
const staticDependentModules = incomingConnections.flatMap(
|
||
|
( connection ) => {
|
||
|
const { dependency } = connection;
|
||
|
const parentBlock =
|
||
|
compilation.moduleGraph.getParentBlock( dependency );
|
||
|
|
||
|
return parentBlock.constructor.name !==
|
||
|
AsyncDependenciesBlock.name
|
||
|
? [ compilation.moduleGraph.getParentModule( dependency ) ]
|
||
|
: [];
|
||
|
}
|
||
|
);
|
||
|
|
||
|
// All the dependencies were Async, the module was reached via a dynamic import
|
||
|
if ( ! staticDependentModules.length ) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// Continue to explore any static dependencies
|
||
|
return staticDependentModules.some( ( parentStaticDependentModule ) =>
|
||
|
AssetDataPlugin.hasStaticDependencyPathToRoot(
|
||
|
compilation,
|
||
|
parentStaticDependentModule
|
||
|
)
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
module.exports = AssetDataPlugin;
|