@wordpress/scripts
Version:
Collection of reusable scripts for WordPress development.
460 lines (407 loc) • 13.5 kB
JavaScript
/**
* External dependencies
*/
const { readFileSync } = require( 'fs' );
const { basename, dirname, extname, join, sep } = require( 'path' );
const { sync: glob } = require( 'fast-glob' );
/**
* Internal dependencies
*/
const {
getArgFromCLI,
getArgsFromCLI,
getFileArgsFromCLI,
hasArgInCLI,
hasFileArgInCLI,
} = require( './cli' );
const { fromConfigRoot, fromProjectRoot, hasProjectFile } = require( './file' );
const { hasPackageProp } = require( './package' );
const {
getBlockJsonModuleFields,
getBlockJsonScriptFields,
} = require( './block-json' );
const { warn } = console;
// See https://babeljs.io/docs/en/config-files#configuration-file-types.
const hasBabelConfig = () =>
hasProjectFile( '.babelrc.js' ) ||
hasProjectFile( '.babelrc.json' ) ||
hasProjectFile( 'babel.config.js' ) ||
hasProjectFile( 'babel.config.json' ) ||
hasProjectFile( '.babelrc' ) ||
hasPackageProp( 'babel' );
// See https://cssnano.co/docs/config-file.
const hasCssnanoConfig = () =>
hasProjectFile( '.cssnanorc' ) ||
hasProjectFile( '.cssnanorc.js' ) ||
hasProjectFile( '.cssnanorc.json' ) ||
hasProjectFile( '.cssnanorc.yaml' ) ||
hasProjectFile( '.cssnanorc.yml' ) ||
hasProjectFile( 'cssnano.config.js' ) ||
hasPackageProp( 'cssnano' );
/**
* Returns path to a Jest configuration which should be provided as the explicit
* configuration when there is none available for discovery by Jest in the
* project environment. Returns undefined if Jest should be allowed to discover
* an available configuration.
*
* This can be used in cases where multiple possible configurations are
* supported. Since Jest will only discover `jest.config.js`, or `jest` package
* directive, such custom configurations must be specified explicitly.
*
* @param {"e2e"|"unit"} suffix Suffix of configuration file to accept.
*
* @return {string= | undefined} Override or fallback configuration file path.
*/
function getJestOverrideConfigFile( suffix ) {
if ( hasArgInCLI( '-c' ) || hasArgInCLI( '--config' ) ) {
return;
}
if ( hasProjectFile( `jest-${ suffix }.config.js` ) ) {
return fromProjectRoot( `jest-${ suffix }.config.js` );
}
if ( ! hasJestConfig() ) {
return fromConfigRoot( `jest-${ suffix }.config.js` );
}
}
// See https://jestjs.io/docs/configuration.
const hasJestConfig = () =>
hasProjectFile( 'jest.config.js' ) ||
hasProjectFile( 'jest.config.json' ) ||
hasProjectFile( 'jest.config.ts' ) ||
hasPackageProp( 'jest' );
// See https://prettier.io/docs/en/configuration.html.
const hasPrettierConfig = () =>
hasProjectFile( '.prettierrc.js' ) ||
hasProjectFile( '.prettierrc.json' ) ||
hasProjectFile( '.prettierrc.toml' ) ||
hasProjectFile( '.prettierrc.yaml' ) ||
hasProjectFile( '.prettierrc.yml' ) ||
hasProjectFile( 'prettier.config.js' ) ||
hasProjectFile( '.prettierrc' ) ||
hasPackageProp( 'prettier' );
const hasWebpackConfig = () =>
hasArgInCLI( '--config' ) ||
hasProjectFile( 'webpack.config.js' ) ||
hasProjectFile( 'webpack.config.babel.js' );
// See https://github.com/michael-ciniawsky/postcss-load-config#usage (used by postcss-loader).
const hasPostCSSConfig = () =>
hasProjectFile( 'postcss.config.js' ) ||
hasProjectFile( '.postcssrc' ) ||
hasProjectFile( '.postcssrc.json' ) ||
hasProjectFile( '.postcssrc.yaml' ) ||
hasProjectFile( '.postcssrc.yml' ) ||
hasProjectFile( '.postcssrc.js' ) ||
hasPackageProp( 'postcss' );
/**
* Converts CLI arguments to the format which webpack understands.
*
* @see https://webpack.js.org/api/cli/#usage-with-config-file
*
* @return {Array} The list of CLI arguments to pass to webpack CLI.
*/
const getWebpackArgs = () => {
// Gets all args from CLI without those prefixed with `--webpack`.
let webpackArgs = getArgsFromCLI( [
'--blocks-manifest',
'--experimental-modules',
'--source-path',
'--webpack',
] );
if ( hasArgInCLI( '--experimental-modules' ) ) {
process.env.WP_EXPERIMENTAL_MODULES = true;
}
if ( hasArgInCLI( '--source-path' ) ) {
process.env.WP_SOURCE_PATH = getArgFromCLI( '--source-path' );
} else if ( hasArgInCLI( '--webpack-src-dir' ) ) {
// Backwards compatibility.
process.env.WP_SOURCE_PATH = getArgFromCLI( '--webpack-src-dir' );
}
if ( hasArgInCLI( '--webpack-bundle-analyzer' ) ) {
process.env.WP_BUNDLE_ANALYZER = true;
}
if ( hasArgInCLI( '--webpack-copy-php' ) ) {
process.env.WP_COPY_PHP_FILES_TO_DIST = true;
}
if ( hasArgInCLI( '--webpack--devtool' ) ) {
process.env.WP_DEVTOOL = getArgFromCLI( '--webpack--devtool' );
}
if ( hasArgInCLI( '--webpack-no-externals' ) ) {
process.env.WP_NO_EXTERNALS = true;
}
if ( hasArgInCLI( '--blocks-manifest' ) ) {
process.env.WP_BLOCKS_MANIFEST = true;
}
const hasWebpackOutputOption =
hasArgInCLI( '-o' ) || hasArgInCLI( '--output' );
if (
! hasWebpackOutputOption &&
! hasArgInCLI( '--entry' ) &&
hasFileArgInCLI()
) {
/**
* Converts a legacy path to the entry pair supported by webpack, e.g.:
* `./entry-one.js` -> `[ 'entry-one', './entry-one.js] ]`
* `entry-two.js` -> `[ 'entry-two', './entry-two.js' ]`
*
* @param {string} path The path provided.
*
* @return {string[]} The entry pair of its name and the file path.
*/
const pathToEntry = ( path ) => {
const entryName = basename( path, '.js' );
return [ entryName, path ];
};
const fileArgs = getFileArgsFromCLI();
if ( fileArgs.length > 0 ) {
// Filters out all CLI arguments that are recognized as file paths.
const fileArgsToRemove = new Set( fileArgs );
webpackArgs = webpackArgs.filter( ( cliArg ) => {
if ( fileArgsToRemove.has( cliArg ) ) {
fileArgsToRemove.delete( cliArg );
return false;
}
return true;
} );
// Converts all CLI arguments that are file paths to the `entry` format supported by webpack.
// It is going to be consumed in the config through the WP_ENTRY global variable.
const entry = {};
fileArgs.forEach( ( fileArg ) => {
const [ entryName, path ] = fileArg.includes( '=' )
? fileArg.split( '=' )
: pathToEntry( fileArg );
entry[ entryName ] = fromProjectRoot(
process.env.WP_SOURCE_PATH
? join( process.env.WP_SOURCE_PATH, path )
: path
);
} );
process.env.WP_ENTRY = JSON.stringify( entry );
}
}
if ( ! hasWebpackConfig() ) {
webpackArgs.push( '--config', fromConfigRoot( 'webpack.config.js' ) );
}
return webpackArgs;
};
/**
* Returns the project source path. It defaults to 'src' if the
* `process.env.WP_SOURCE_PATH` variable is not set.
*
* @return {string} The WordPress source directory.
*/
function getProjectSourcePath() {
return process.env.WP_SOURCE_PATH || 'src';
}
/**
* Detects the list of entry points to use with webpack. There are three alternative ways to do this:
* 1. Use the recommended command format that lists the paths to JavaScript files.
* 2. Scan `block.json` files to detect referenced JavaScript and PHP files automatically.
* 3. Fallback to the `src/index.*` file.
*
* @see https://webpack.js.org/concepts/entry-points/
*
* @param {'script' | 'module'} buildType
*/
function getWebpackEntryPoints( buildType ) {
/**
* @return {Object<string,string>} The list of entry points.
*/
return () => {
// 1. Uses the recommended command format that lists entry points as paths to JavaScript files.
// Example: `wp-scripts build one.js two.js`.
if ( process.env.WP_ENTRY ) {
return buildType === 'script'
? JSON.parse( process.env.WP_ENTRY )
: {};
}
// Continues only if the source directory exists. Defaults to "src" if not explicitly set in the command.
if ( ! hasProjectFile( getProjectSourcePath() ) ) {
warn(
`Source directory "${ getProjectSourcePath() }" was not found. Please confirm there is a "src" directory in the root or the value passed with "--output-path" is correct.`
);
return {};
}
// 2. Checks whether any block metadata files can be detected in the defined source directory.
// It scans all discovered files, looks for JavaScript assets, and converts them to entry points.
const blockMetadataFiles = glob( '**/block.json', {
absolute: true,
cwd: fromProjectRoot( getProjectSourcePath() ),
} );
if ( blockMetadataFiles.length > 0 ) {
const srcDirectory = fromProjectRoot(
getProjectSourcePath() + sep
);
const entryPoints = {};
for ( const blockMetadataFile of blockMetadataFiles ) {
const fileContents = readFileSync( blockMetadataFile );
let parsedBlockJson;
// wrapping in try/catch in case the file is malformed
// this happens especially when new block.json files are added
// at which point they are completely empty and therefore not valid JSON
try {
parsedBlockJson = JSON.parse( fileContents );
} catch ( error ) {
warn(
`Not scanning "${ blockMetadataFile.replace(
fromProjectRoot( sep ),
''
) }" due to collect entry points due to malformed JSON.`
);
continue;
}
const fields =
buildType === 'script'
? getBlockJsonScriptFields( parsedBlockJson )
: getBlockJsonModuleFields( parsedBlockJson );
if ( ! fields ) {
continue;
}
for ( const value of Object.values( fields ).flat() ) {
if ( ! value.startsWith( 'file:' ) ) {
continue;
}
// Removes the `file:` prefix.
const filepath = join(
dirname( blockMetadataFile ),
value.replace( 'file:', '' )
);
// Takes the path without the file extension, and relative to the defined source directory.
if ( ! filepath.startsWith( srcDirectory ) ) {
warn(
`Skipping "${ value.replace(
'file:',
''
) }" listed in "${ blockMetadataFile.replace(
fromProjectRoot( sep ),
''
) }". File is located outside of the "${ getProjectSourcePath() }" directory.`
);
continue;
}
const entryName = filepath
.replace( extname( filepath ), '' )
.replace( srcDirectory, '' )
.replace( /\\/g, '/' );
// Detects the proper file extension used in the defined source directory.
const [ entryFilepath ] = glob(
`${ entryName }.?(m)[jt]s?(x)`,
{
absolute: true,
cwd: fromProjectRoot( getProjectSourcePath() ),
}
);
if ( ! entryFilepath ) {
warn(
`Skipping "${ value.replace(
'file:',
''
) }" listed in "${ blockMetadataFile.replace(
fromProjectRoot( sep ),
''
) }". File does not exist in the "${ getProjectSourcePath() }" directory.`
);
continue;
}
entryPoints[ entryName ] = entryFilepath;
}
}
if ( Object.keys( entryPoints ).length > 0 ) {
return entryPoints;
}
}
// Don't do any further processing if this is a module build.
// This only respects *module block.json fields.
if ( buildType === 'module' ) {
return {};
}
// 3. Checks whether a standard file name can be detected in the defined source directory,
// and converts the discovered file to entry point.
const [ entryFile ] = glob( 'index.[jt]s?(x)', {
absolute: true,
cwd: fromProjectRoot( getProjectSourcePath() ),
} );
if ( ! entryFile ) {
warn(
`No entry file discovered in the "${ getProjectSourcePath() }" directory.`
);
return {};
}
return {
index: entryFile,
};
};
}
/**
* Returns the list of PHP file paths found in `block.json` files for the given props.
*
* @param {string} context The path to search for `block.json` files.
* @param {string[]} props The props to search for in the `block.json` files.
* @return {string[]} The list of PHP file paths.
*/
function getPhpFilePaths( context, props ) {
// Continue only if the source directory exists.
if ( ! hasProjectFile( context ) ) {
return [];
}
// Checks whether any block metadata files can be detected in the defined source directory.
const blockMetadataFiles = glob( '**/block.json', {
absolute: true,
cwd: fromProjectRoot( context ),
} );
const srcDirectory = fromProjectRoot( context + sep );
return blockMetadataFiles.flatMap( ( blockMetadataFile ) => {
const paths = [];
let parsedBlockJson;
try {
parsedBlockJson = JSON.parse( readFileSync( blockMetadataFile ) );
} catch ( error ) {
warn(
`Not scanning "${ blockMetadataFile.replace(
fromProjectRoot( sep ),
''
) }" due to detect render files due to malformed JSON.`
);
return paths;
}
for ( const prop of props ) {
if (
typeof parsedBlockJson?.[ prop ] !== 'string' ||
! parsedBlockJson[ prop ]?.startsWith( 'file:' )
) {
continue;
}
// Removes the `file:` prefix.
const filepath = join(
dirname( blockMetadataFile ),
parsedBlockJson[ prop ].replace( 'file:', '' )
);
// Takes the path without the file extension, and relative to the defined source directory.
if ( ! filepath.startsWith( srcDirectory ) ) {
warn(
`Skipping "${ parsedBlockJson[ prop ].replace(
'file:',
''
) }" listed in "${ blockMetadataFile.replace(
fromProjectRoot( sep ),
''
) }". File is located outside of the "${ context }" directory.`
);
continue;
}
paths.push( filepath.replace( /\\/g, '/' ) );
}
return paths;
} );
}
module.exports = {
getJestOverrideConfigFile,
getPhpFilePaths,
getProjectSourcePath,
getWebpackArgs,
getWebpackEntryPoints,
hasBabelConfig,
hasCssnanoConfig,
hasJestConfig,
hasPostCSSConfig,
hasPrettierConfig,
};