Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions bin/build-block-manifest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/usr/bin/env node

/**
* Build blocks manifest script.
*
* Runs wp-scripts build-blocks-manifest only when compiled block directories
* exist inside assets/build/blocks/. Exits silently with success if no blocks
* are found, preventing build:prod from failing on a fresh clone.
*/

/**
* External dependencies
*/
const fs = require( 'fs' );
const path = require( 'path' );
const { spawnSync } = require( 'child_process' );

const blocksBuildDir = path.join( process.cwd(), 'assets', 'build', 'blocks' );

/**
* Check if the build blocks directory contains at least one compiled block.json.
*
* @return {boolean}
*/
const hasCompiledBlocks = () => {
if ( ! fs.existsSync( blocksBuildDir ) ) {
return false;
}

const entries = fs.readdirSync( blocksBuildDir, { withFileTypes: true } );

return entries
.filter( ( entry ) => entry.isDirectory() )
.some( ( dir ) =>
fs.existsSync( path.join( blocksBuildDir, dir.name, 'block.json' ) )
);
};

if ( ! hasCompiledBlocks() ) {
console.log( 'No compiled blocks found. Skipping manifest generation.' );
process.exit( 0 );
}

const result = spawnSync(
'npx',
[
'wp-scripts',
'build-blocks-manifest',
`--input=${ blocksBuildDir }`,
`--output=${ path.join( blocksBuildDir, 'blocks-manifest.php' ) }`,
],
{ stdio: 'inherit', cwd: process.cwd() }
);

if ( result.error ) {
throw result.error;
}

process.exit( result.status ?? 0 );
60 changes: 60 additions & 0 deletions bin/build-blocks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/env node

/**
* Build blocks script.
*
* Runs wp-scripts build for blocks only when block source directories exist
* inside assets/src/blocks/. Exits silently with success if no blocks are
* found, preventing an empty assets/build/blocks/ directory from being created.
*/

/**
* External dependencies
*/
const fs = require( 'fs' );
const path = require( 'path' );
const { spawnSync } = require( 'child_process' );

const blocksSourceDir = path.join( process.cwd(), 'assets', 'src', 'blocks' );

/**
* Check if the source blocks directory contains at least one buildable block.
* Directories prefixed with '_' are intentionally excluded and do not count.
*
* @return {boolean}
*/
const hasSourceBlocks = () => {
if ( ! fs.existsSync( blocksSourceDir ) ) {
return false;
}

const entries = fs.readdirSync( blocksSourceDir, { withFileTypes: true } );

return entries
.filter( ( entry ) => entry.isDirectory() )
.some( ( dir ) => ! dir.name.startsWith( '_' ) );
};

if ( ! hasSourceBlocks() ) {
console.log( 'No block sources found. Skipping block build.' );
process.exit( 0 );
}

const result = spawnSync(
'npx',
[
'wp-scripts',
'build',
'--config',
'./node_modules/@wordpress/scripts/config/webpack.config.js',
'--webpack-src-dir=./assets/src/blocks/',
'--output-path=./assets/build/blocks/',
],
{ stdio: 'inherit', cwd: process.cwd() }
);

if ( result.error ) {
throw result.error;
}

process.exit( result.status ?? 0 );
174 changes: 174 additions & 0 deletions bin/create-block.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
#!/usr/bin/env node

/**
* External dependencies
*/
const fs = require( 'fs' );
const path = require( 'path' );
const { spawnSync } = require( 'child_process' );

/**
* Default arguments passed to @wordpress/create-block.
* Stored as a Map to make insertion order and intent explicit.
*/
const DEFAULT_ARGS = new Map( [
[ '--variant', 'static' ],
[ '--namespace', 'elementary-theme' ],
] );

/**
* Validate block slug.
* Must start and end with a lowercase letter or number.
* May contain hyphens in between, but not at the start or end.
*
* @param {string} blockName Block slug.
* @return {boolean}
*/
const isValidBlockName = ( blockName ) => {
return /^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test( blockName );
};

/**
* Print usage instructions to stderr.
*
* @return {void}
*/
const printUsage = () => {
console.error( 'Usage: node create-block.js <block-name> [options]' );
console.error( 'Example: node create-block.js my-block --variant=dynamic' );
};

/**
* Parse CLI arguments, merging user-provided args with defaults.
* User-provided values always take precedence over defaults.
*
* Handles both flag-style args (--no-plugin) and key=value args (--variant=static).
*
* @param {Array} args CLI args passed by the user.
* @return {Array} Merged array of arguments.
*/
const parseArgs = ( args ) => {
const parsedArgs = [];
const seenKeys = new Set();

// Add user-provided args, tracking which keys have been supplied.
args.forEach( ( arg ) => {
// Extract the key portion regardless of whether arg has a value or not.
const key = arg.startsWith( '--' ) ? arg.split( '=' )[ 0 ] : arg;
seenKeys.add( key );
parsedArgs.push( arg );
} );

// Add defaults only for keys the user did not supply.
DEFAULT_ARGS.forEach( ( value, key ) => {
if ( ! seenKeys.has( key ) ) {
parsedArgs.push( `${ key }=${ value }` );
}
} );

return parsedArgs;
};

/**
* Run @wordpress/create-block CLI and verify the output directory was created.
*
* --target-dir only accepts a path relative to cwd, not an absolute path.
* We set cwd to the project root and pass a relative target-dir so
* create-block scaffolds files in the correct location.
*
* @param {string} blockName Block slug.
* @param {Array} args Merged CLI args.
* @return {void}
* @throws {Error} If the block directory already exists, the command fails,
* or the block directory is not created after the run.
*/
const createBlock = ( blockName, args ) => {
const projectRoot = process.cwd();

// Relative path from project root — required by --target-dir.
const relativeBlockDir = path.join( 'assets', 'src', 'blocks', blockName );

// Absolute path used for pre-run existence check and post-run verification.
const absoluteBlockDir = path.join( projectRoot, relativeBlockDir );

// Guard against silently overwriting an existing block.
if ( fs.existsSync( absoluteBlockDir ) ) {
throw new Error(
`Block "${ blockName }" already exists at: ${ absoluteBlockDir }`
);
}

const cliArgs = [
'@wordpress/create-block',
blockName,
`--target-dir=${ relativeBlockDir }`,
'--no-plugin',
...args,
];

console.log( `Creating block "${ blockName }"...` );

// spawnSync passes args as an array — no shell interpolation, no injection risk.
// cwd ensures create-block resolves --target-dir relative to the project root.
const result = spawnSync( 'npx', cliArgs, {
stdio: 'inherit',
cwd: projectRoot,
} );

if ( result.error ) {
throw result.error;
}

if ( result.status !== 0 ) {
// result.status is null when the process was killed by a signal.
const exitInfo =
result.status !== null
? `status ${ result.status }`
: `signal ${ result.signal }`;
throw new Error( `create-block exited with ${ exitInfo }.` );
}

if ( ! fs.existsSync( absoluteBlockDir ) ) {
throw new Error(
`Block directory was not created at: ${ absoluteBlockDir }`
);
}

console.log( `Block created successfully at ${ absoluteBlockDir }` );
};

/**
* Entry point.
*
* @return {void}
*/
const init = () => {
try {
const args = process.argv.slice( 2 );
const blockName = args.shift();

if ( ! blockName ) {
console.error( 'Error: Block name is required.' );
printUsage();
process.exit( 1 );
}

if ( ! isValidBlockName( blockName ) ) {
console.error(
'Error: Block name must start and end with a lowercase letter or number, ' +
'and may only contain lowercase letters, numbers, and hyphens.'
);
printUsage();
process.exit( 1 );
}

const parsedArgs = parseArgs( args );

createBlock( blockName, parsedArgs );
} catch ( error ) {
console.error( 'Error creating block:', error.message );
process.exit( 1 );
}
};

init();
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
"php": ">=8.2"
},
"require-dev": {
"wp-coding-standards/wpcs": "^2.3",
"wp-coding-standards/wpcs": "^3.0",
"phpcompatibility/phpcompatibility-wp": "^2.1",
"automattic/vipwpcs": "^2.3",
"automattic/vipwpcs": "^3.0",
"dealerdirect/phpcodesniffer-composer-installer": "1.2.0",
"sirbrillig/phpcs-variable-analysis": "^2.11.3",
"phpunit/phpunit": "^9.5",
Expand Down
Loading
Loading