You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

@wordpress/build

Package Overview
Dependencies
Maintainers
23
Versions
29
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@wordpress/build - npm Package Compare versions

Comparing version
0.2.1-next.2f1c7c01b.0
to
0.3.0
templates/page-wp-admin.php.template

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

+2
-0

@@ -5,2 +5,4 @@ <!-- Learn how to maintain this file at https://github.com/WordPress/gutenberg/tree/HEAD/packages#maintaining-changelogs. -->

## 0.3.0 (2025-11-12)
## 0.2.0 (2025-10-29)

@@ -7,0 +9,0 @@

+2
-2
{
"name": "@wordpress/build",
"version": "0.2.1-next.2f1c7c01b.0",
"version": "0.3.0",
"description": "Build tool for WordPress plugins.",

@@ -57,3 +57,3 @@ "author": "The WordPress Contributors",

},
"gitHead": "c6ddcdf455bc02567a2c9e03de6862a2061b85e8"
"gitHead": "77aa1f194edceafe8ac2a1b9438bf84b557e76e3"
}

@@ -204,2 +204,33 @@ # @wordpress/build

### `wpPlugin.pages` (Experimental)
Define admin pages that support routes. Each page gets generated PHP functions for route registration and can be extended by other plugins:
```json
{
"wpPlugin": {
"pages": ["my-admin-page"]
}
}
```
This generates two page modes:
- `build/pages/my-admin-page/page.php` - Full-page mode (takes over entire admin screen with custom sidebar)
- `build/pages/my-admin-page/page-wp-admin.php` - WP-Admin mode (integrates within standard wp-admin interface)
- `build/pages.php` - Loader for all pages
Each mode provides route/menu registration functions and a render callback. Routes are automatically registered for both modes.
**Registering a menu item for WP-Admin mode:**
```php
// Build URL with initial route via 'p' query parameter
$url = admin_url( 'admin.php?page=my-admin-page-wp-admin&p=' . urlencode( '/my/route' ) );
add_menu_page( 'Title', 'Menu', 'capability', $url, '', 'icon', 20 );
```
**Registering a menu item for full-page mode:**
```php
add_menu_page( 'Title', 'Menu', 'capability', 'my-admin-page', 'my_admin_page_render_page', 'icon', 20 );
```
### Example: WordPress Core (Gutenberg)

@@ -258,3 +289,3 @@

Routes provide a file-based routing system for WordPress admin pages. Create a `routes/` directory at your repository root with subdirectories for each route.
Routes provide a file-based routing system for WordPress admin pages. Each route must be associated with a page defined in `wpPlugin.pages` (see above). Create a `routes/` directory at your repository root with subdirectories for each route.

@@ -279,3 +310,4 @@ ### Structure

"route": {
"path": "/"
"path": "/",
"page": "my-admin-page"
}

@@ -285,2 +317,4 @@ }

The `page` field must match one of the pages defined in `wpPlugin.pages` in your root `package.json`. This tells the build system which page this route belongs to. It can also map to an existing page registered by another plugin.
### Components

@@ -287,0 +321,0 @@

@@ -32,4 +32,2 @@ #!/usr/bin/env node

getPhpReplacements,
generateRoutesPhp,
generateRoutesRegistry,
} from './php-generator.mjs';

@@ -41,2 +39,3 @@ import { getPackageInfo, getPackageInfoFromFile } from './package-utils.mjs';

getRouteFiles,
getRouteMetadata,
generateContentEntryPoint,

@@ -87,2 +86,3 @@ } from './route-utils.mjs';

const EXTERNAL_NAMESPACES = WP_PLUGIN_CONFIG.externalNamespaces || {};
const PAGES = WP_PLUGIN_CONFIG.pages || [];

@@ -790,2 +790,128 @@ const baseDefine = {

/**
* Generate global route registry file.
* Creates a single registry with all routes including page metadata.
*
* @param {Array} routes Array of route objects.
* @param {Record<string, string>} replacements PHP template replacements.
*/
async function generateRoutesRegistry( routes, replacements ) {
if ( routes.length === 0 ) {
// No routes to register, skip generating routes registry
return;
}
// Generate PHP array entries with page metadata
const routeEntries = routes
.map( ( route ) => {
const hasRouteStr = route.hasRoute ? 'true' : 'false';
const hasContentStr = route.hasContent ? 'true' : 'false';
return `\tarray(
'name' => '${ route.name }',
'path' => '${ route.path }',
'page' => '${ route.page }',
'has_route' => ${ hasRouteStr },
'has_content' => ${ hasContentStr },
)`;
} )
.join( ',\n' );
// Generate single global registry at build/routes/index.php
await generatePhpFromTemplate(
'route-registry.php.template',
path.join( BUILD_DIR, 'routes', 'index.php' ),
{ ...replacements, '{{ROUTES}}': routeEntries }
);
}
/**
* Generate routes.php file with route registration logic.
* Uses registry pattern with loop-based registration on page init hooks.
*
* @param {Array} routes Array of route objects.
* @param {Record<string, string>} replacements PHP template replacements.
*/
async function generateRoutesPhp( routes, replacements ) {
if ( routes.length === 0 ) {
// No routes to register, skip generating routes.php
return;
}
// Generate routes.php from template
await generatePhpFromTemplate(
'routes-registration.php.template',
path.join( BUILD_DIR, 'routes.php' ),
{ ...replacements, '{{HANDLE_PREFIX}}': HANDLE_PREFIX }
);
}
/**
* Generate page-specific PHP files for all pages.
*
* @param {Array} pageData Array of page objects with routes.
* @param {Record<string, string>} replacements PHP template replacements.
*/
async function generatePagesPhp( pageData, replacements ) {
if ( pageData.length === 0 ) {
// No pages to generate
return;
}
// Generate files for each page
const pageGenerationPromises = pageData.map( async ( page ) => {
// Skip pages with no routes
if ( page.routes.length === 0 ) {
return;
}
const pageSlugUnderscore = page.slug.replace( /-/g, '_' );
const prefixUnderscore = replacements[ '{{PREFIX}}' ].replace(
/-/g,
'_'
);
const templateReplacements = {
...replacements,
'{{PAGE_SLUG}}': page.slug,
'{{PAGE_SLUG_UNDERSCORE}}': pageSlugUnderscore,
'{{PREFIX}}': prefixUnderscore,
};
// Generate both page.php and page-wp-admin.php
await Promise.all( [
generatePhpFromTemplate(
'page.php.template',
path.join( BUILD_DIR, 'pages', page.slug, 'page.php' ),
templateReplacements
),
generatePhpFromTemplate(
'page-wp-admin.php.template',
path.join( BUILD_DIR, 'pages', page.slug, 'page-wp-admin.php' ),
templateReplacements
),
] );
// Generate empty loader.js (dummy module for dependencies)
await writeFile(
path.join( BUILD_DIR, 'pages', page.slug, 'loader.js' ),
'// Empty module loader for page dependencies\n'
);
} );
await Promise.all( pageGenerationPromises );
// Generate pages.php loader
const pageIncludes = pageData
.map( ( page ) => {
return `require_once __DIR__ . '/pages/${ page.slug }/page.php';\nrequire_once __DIR__ . '/pages/${ page.slug }/page-wp-admin.php';`;
} )
.join( '\n' );
await generatePhpFromTemplate(
'pages.php.template',
path.join( BUILD_DIR, 'pages.php' ),
{ ...replacements, '{{PAGE_INCLUDES}}': pageIncludes }
);
}
/**
* Transpile a single package's source files and copy JSON files.

@@ -1288,2 +1414,25 @@ *

// Collect route and page data for PHP generation
const routes = getAllRoutes( ROOT_DIR ).map( ( routeName ) => {
const metadata = getRouteMetadata( ROOT_DIR, routeName );
const routeFiles = getRouteFiles(
path.join( ROOT_DIR, 'routes', routeName )
);
return {
name: routeName,
path: metadata?.path,
page: metadata?.page,
hasRoute: routeFiles.hasRoute,
hasContent: routeFiles.hasStage || routeFiles.hasInspector,
};
} );
const pageData = PAGES.map( ( pageSlug ) => {
const pageRoutes = routes.filter( ( r ) => r.page === pageSlug );
return {
slug: pageSlug,
routes: pageRoutes,
};
} );
console.log( '\n📄 Generating PHP registration files...\n' );

@@ -1297,13 +1446,5 @@ const phpReplacements = await getPhpReplacements( ROOT_DIR );

generateVersionPhp( phpReplacements ),
generateRoutesRegistry(
ROOT_DIR,
BUILD_DIR,
phpReplacements[ '{{PREFIX}}' ]
),
generateRoutesPhp(
ROOT_DIR,
BUILD_DIR,
HANDLE_PREFIX,
phpReplacements[ '{{PREFIX}}' ]
),
generateRoutesRegistry( routes, phpReplacements ),
generateRoutesPhp( routes, phpReplacements ),
generatePagesPhp( pageData, phpReplacements ),
] );

@@ -1318,2 +1459,11 @@ console.log( ' ✔ Generated build/modules.php' );

console.log( ' ✔ Generated build/routes.php' );
if ( PAGES.length > 0 ) {
console.log( ' ✔ Generated build/pages.php' );
for ( const page of PAGES ) {
console.log( ` ✔ Generated build/pages/${ page }/page.php` );
console.log(
` ✔ Generated build/pages/${ page }/page-wp-admin.php`
);
}
}
console.log( ' ✔ Generated build/index.php' );

@@ -1519,2 +1669,14 @@

ignoreInitial: true,
ignored: ( filepath ) => {
const basename = path.basename( filepath );
// Ignore .content-entry.js temporary build files
if ( basename === '.content-entry.js' ) {
return true;
}
// Ignore node_modules directories and contents
if ( filepath.includes( 'node_modules' ) ) {
return true;
}
return false;
},
useFsEvents: true,

@@ -1521,0 +1683,0 @@ depth: 10,

@@ -40,3 +40,3 @@ /**

/**
* @typedef {PackageJson & { route: { path: string } }} RoutePackageJson
* @typedef {PackageJson & { route: { path: string; page?: string } }} RoutePackageJson
*/

@@ -43,0 +43,0 @@

@@ -12,3 +12,2 @@ /**

import { getPackageInfoFromFile } from './package-utils.mjs';
import { getAllRoutes, getRouteFiles } from './route-utils.mjs';

@@ -45,23 +44,9 @@ const __dirname = path.dirname( fileURLToPath( import.meta.url ) );

/**
* Generate a PHP file from a template with replacements.
* Apply template replacements to a template string.
*
* @param {string} templateName Template file name.
* @param {string} outputPath Full output path.
* @param {string} template Template string with placeholders.
* @param {Record<string, string>} replacements Replacements object (e.g. {'{{PREFIX}}': 'gutenberg'}).
* @return {string} Template with replacements applied.
*/
export async function generatePhpFromTemplate(
templateName,
outputPath,
replacements
) {
// Templates directory
const templatesDir = path.join( __dirname, '..', 'templates' );
// Read template
const template = await readFile(
path.join( templatesDir, templateName ),
'utf8'
);
// Apply all replacements
export function applyTemplateReplacements( template, replacements ) {
let content = template;

@@ -71,106 +56,44 @@ for ( const [ placeholder, value ] of Object.entries( replacements ) ) {

}
// Write output file
await mkdir( path.dirname( outputPath ), { recursive: true } );
await writeFile( outputPath, content );
return content;
}
/**
* Generate routes/index.php file with route registry data.
* Render a template to a string with replacements.
*
* @param {string} rootDir Root directory path.
* @param {string} buildDir Build directory path.
* @param {string} prefix Package prefix.
* @param {string} templateName Template file name.
* @param {Record<string, string>} replacements Replacements object (e.g. {'{{PREFIX}}': 'gutenberg'}).
* @return {Promise<string>} Rendered template string.
*/
export async function generateRoutesRegistry( rootDir, buildDir, prefix ) {
const routeNames = getAllRoutes( rootDir );
export async function renderTemplateToString( templateName, replacements ) {
// Templates directory
const templatesDir = path.join( __dirname, '..', 'templates' );
if ( routeNames.length === 0 ) {
// No routes to register, skip generating routes registry
return;
}
// Read template
const template = await readFile(
path.join( templatesDir, templateName ),
'utf8'
);
// Build routes array
const routes = routeNames.map( ( routeName ) => {
// Read package.json to get route path
const routePackageJson =
/** @type {import('./package-utils.mjs').RoutePackageJson} */ (
getPackageInfoFromFile(
path.join( rootDir, 'routes', routeName, 'package.json' )
)
);
const routePath = routePackageJson.route.path;
// Check if route.js exists
const routeFiles = getRouteFiles(
path.join( rootDir, 'routes', routeName )
);
return {
name: routeName,
path: routePath,
has_route: routeFiles.hasRoute,
};
} );
// Generate PHP array entries
const routeEntries = routes
.map( ( route ) => {
const hasRouteStr = route.has_route ? 'true' : 'false';
return `\tarray(
'name' => '${ route.name }',
'path' => '${ route.path }',
'has_route' => ${ hasRouteStr },
)`;
} )
.join( ',\n' );
// Generate routes/index.php
const replacements = {
'{{PREFIX}}': prefix,
'{{ROUTES}}': routeEntries,
};
await generatePhpFromTemplate(
'route-registry.php.template',
path.join( buildDir, 'routes', 'index.php' ),
replacements
);
// Apply replacements
return applyTemplateReplacements( template, replacements );
}
/**
* Generate routes.php file with route registration logic.
* Generate a PHP file from a template with replacements.
*
* @param {string} rootDir Root directory path.
* @param {string} buildDir Build directory path.
* @param {string} handlePrefix Handle prefix for script modules.
* @param {string} prefix Package prefix.
* @param {string} templateName Template file name.
* @param {string} outputPath Full output path.
* @param {Record<string, string>} replacements Replacements object (e.g. {'{{PREFIX}}': 'gutenberg'}).
*/
export async function generateRoutesPhp(
rootDir,
buildDir,
handlePrefix,
prefix
export async function generatePhpFromTemplate(
templateName,
outputPath,
replacements
) {
const routeNames = getAllRoutes( rootDir );
// Render template to string
const content = await renderTemplateToString( templateName, replacements );
if ( routeNames.length === 0 ) {
// No routes to register, skip generating routes.php
return;
}
const namespace = handlePrefix.replace( /-/g, '_' );
// Generate routes.php
const replacements = {
'{{PREFIX}}': prefix,
'{{NAMESPACE}}': namespace,
'{{HANDLE_PREFIX}}': handlePrefix,
};
await generatePhpFromTemplate(
'routes.php.template',
path.join( buildDir, 'routes.php' ),
replacements
);
// Write output file
await mkdir( path.dirname( outputPath ), { recursive: true } );
await writeFile( outputPath, content );
}

@@ -8,2 +8,7 @@ /**

/**
* Internal dependencies
*/
import { getPackageInfoFromFile } from './package-utils.mjs';
/**
* Get all route names from the routes directory.

@@ -28,2 +33,35 @@ *

/**
* @typedef {Object} RouteMetadata
* @property {string} name Route name.
* @property {string} path Route path.
* @property {string|null} page Page slug this route belongs to.
*/
/**
* Get route metadata from package.json.
*
* @param {string} rootDir Root directory of the project.
* @param {string} routeName Route name.
* @return {RouteMetadata|null} Route metadata object or null if not found.
*/
export function getRouteMetadata( rootDir, routeName ) {
const routePackageJson =
/** @type {import('./package-utils.mjs').RoutePackageJson|null} */ (
getPackageInfoFromFile(
path.join( rootDir, 'routes', routeName, 'package.json' )
)
);
if ( ! routePackageJson || ! routePackageJson.route ) {
return null;
}
return {
name: routeName,
path: routePackageJson.route.path,
page: routePackageJson.route.page || null,
};
}
/**
* @typedef {Object} RouteFiles

@@ -30,0 +68,0 @@ * @property {boolean} hasRoute Whether route file exists.

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet