@wordpress/build 0.2.0 → 0.2.1-next.2f1c7c01b.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,10 +10,15 @@
10
10
  */
11
11
  import toposort from 'toposort';
12
12
 
13
+ /**
14
+ * Node dependencies
15
+ */
16
+ import path from 'path';
17
+
13
18
  /**
14
19
  * Internal dependencies
15
20
  */
16
- import { getPackageInfo } from './package-utils.mjs';
21
+ import { getPackageInfo, getPackageInfoFromFile } from './package-utils.mjs';
17
22
 
18
23
  /**
19
24
  * Check if a package is a script or script module.
@@ -31,12 +36,12 @@ function isScriptOrModule( packageName ) {
31
36
  }
32
37
 
33
38
  /**
34
- * Get WordPress package dependencies from a package.json file.
39
+ * Get package dependencies from a package.json file.
35
40
  *
36
41
  * @param {string} packageName The full package name (e.g., '@wordpress/blocks').
37
- * @return {string[]} Array of WordPress package names this package depends on.
42
+ * @return {string[]} Array of package names this package depends on.
38
43
  */
39
- function getWordPressDependencies( packageName ) {
44
+ function getDependencies( packageName ) {
40
45
  const packageJson = getPackageInfo( packageName );
41
46
  if ( ! packageJson ) {
42
47
  return [];
@@ -44,10 +49,7 @@ function getWordPressDependencies( packageName ) {
44
49
 
45
50
  const deps = packageJson.dependencies || {};
46
51
 
47
- // Extract @wordpress/* package names (keep full names)
48
- return Object.keys( deps ).filter( ( dep ) =>
49
- dep.startsWith( '@wordpress/' )
50
- );
52
+ return Object.keys( deps );
51
53
  }
52
54
 
53
55
  /**
@@ -62,7 +64,7 @@ function buildDependencyGraph( packages ) {
62
64
  const packagesSet = new Set( packages );
63
65
 
64
66
  for ( const packageName of packages ) {
65
- const deps = getWordPressDependencies( packageName );
67
+ const deps = getDependencies( packageName );
66
68
 
67
69
  // Only include edges where both packages are in our list
68
70
  for ( const dep of deps ) {
@@ -143,7 +145,7 @@ function groupByDepth( packages ) {
143
145
 
144
146
  visited.add( packageName );
145
147
 
146
- const deps = getWordPressDependencies( packageName );
148
+ const deps = getDependencies( packageName );
147
149
  const relevantDeps = deps.filter( ( dep ) => packagesSet.has( dep ) );
148
150
 
149
151
  if ( relevantDeps.length === 0 ) {
@@ -192,7 +194,7 @@ function getReverseDependencies( packageName, allPackages ) {
192
194
  const dependents = [];
193
195
 
194
196
  for ( const pkg of allPackages ) {
195
- const deps = getWordPressDependencies( pkg );
197
+ const deps = getDependencies( pkg );
196
198
  if ( deps.includes( packageName ) ) {
197
199
  dependents.push( pkg );
198
200
  }
@@ -260,12 +262,108 @@ function findScriptsToRebundle( changedPackage, allPackages ) {
260
262
  return Array.from( scriptsToRebundle );
261
263
  }
262
264
 
265
+ /**
266
+ * Get package dependencies from a route's package.json file.
267
+ *
268
+ * @param {string} rootDir Root directory path.
269
+ * @param {string} routeName The route name (e.g., 'home').
270
+ * @return {string[]} Array of package names this route depends on.
271
+ */
272
+ function getRouteDependencies( rootDir, routeName ) {
273
+ const packageJson = getPackageInfoFromFile(
274
+ path.join( rootDir, 'routes', routeName, 'package.json' )
275
+ );
276
+ if ( ! packageJson ) {
277
+ return [];
278
+ }
279
+
280
+ const deps = packageJson.dependencies || {};
281
+
282
+ return Object.keys( deps );
283
+ }
284
+
285
+ /**
286
+ * Find routes that need to be rebuilt when a bundled package changes.
287
+ * Uses BFS to traverse reverse dependencies, stopping at script/module boundaries.
288
+ *
289
+ * When a bundled package changes, we need to rebuild any routes that depend on it
290
+ * through a chain of bundled packages. Routes are leaf nodes like scripts/modules.
291
+ *
292
+ * Example:
293
+ * - A (bundled) changes
294
+ * - B (bundled) depends on A
295
+ * - C (script) depends on B
296
+ * - home (route) depends on B
297
+ * Result: Both C and home need rebuilding
298
+ *
299
+ * @param {string} changedPackage The full package name that changed.
300
+ * @param {string[]} allPackages Array of all full package names.
301
+ * @param {string} rootDir Root directory path.
302
+ * @param {string[]} allRoutes Array of all route names.
303
+ * @return {string[]} Array of route names to rebuild.
304
+ */
305
+ function findRoutesToRebuild(
306
+ changedPackage,
307
+ allPackages,
308
+ rootDir,
309
+ allRoutes
310
+ ) {
311
+ // If the changed package itself is a script/module, routes won't be affected
312
+ // (routes depend on bundled packages, not scripts/modules)
313
+ if ( isScriptOrModule( changedPackage ) ) {
314
+ return [];
315
+ }
316
+
317
+ const routesToRebuild = new Set();
318
+ const visited = new Set();
319
+ const queue = [ changedPackage ];
320
+
321
+ while ( queue.length > 0 ) {
322
+ const currentPackage = queue.shift();
323
+
324
+ if ( ! currentPackage || visited.has( currentPackage ) ) {
325
+ continue;
326
+ }
327
+ visited.add( currentPackage );
328
+
329
+ // Check if any routes depend on the current package
330
+ for ( const route of allRoutes ) {
331
+ const deps = getRouteDependencies( rootDir, route );
332
+ if ( deps.includes( currentPackage ) ) {
333
+ routesToRebuild.add( route );
334
+ }
335
+ }
336
+
337
+ // Get all packages that depend on the current package
338
+ const dependents = getReverseDependencies(
339
+ currentPackage,
340
+ allPackages
341
+ );
342
+
343
+ for ( const dependent of dependents ) {
344
+ // If this dependent is a script/module, don't traverse further
345
+ // (stop at script boundaries)
346
+ if (
347
+ ! isScriptOrModule( dependent ) &&
348
+ ! visited.has( dependent )
349
+ ) {
350
+ // If it's a bundled package, continue traversing
351
+ queue.push( dependent );
352
+ }
353
+ }
354
+ }
355
+
356
+ return Array.from( routesToRebuild );
357
+ }
358
+
263
359
  export {
264
- getWordPressDependencies,
360
+ getDependencies,
265
361
  buildDependencyGraph,
266
362
  topologicalSort,
267
363
  groupByDepth,
268
364
  isScriptOrModule,
269
365
  getReverseDependencies,
270
366
  findScriptsToRebundle,
367
+ getRouteDependencies,
368
+ findRoutesToRebuild,
271
369
  };
@@ -35,6 +35,11 @@ const packagePathCache = new Map();
35
35
  * @property {string[]} [keywords] Package keywords.
36
36
  */
37
37
 
38
+ // Create a new type that extends PackageJson with an optional "route" property
39
+ /**
40
+ * @typedef {PackageJson & { route: { path: string } }} RoutePackageJson
41
+ */
42
+
38
43
  /**
39
44
  * Get package.json info using Node's module resolution.
40
45
  * Resolves the package using import.meta.resolve and reads its package.json.
@@ -9,6 +9,7 @@ import { fileURLToPath } from 'url';
9
9
  * Internal dependencies
10
10
  */
11
11
  import { getPackageInfoFromFile } from './package-utils.mjs';
12
+ import { getAllRoutes, getRouteFiles } from './route-utils.mjs';
12
13
 
13
14
  const __dirname = path.dirname( fileURLToPath( import.meta.url ) );
14
15
 
@@ -70,3 +71,103 @@ export async function generatePhpFromTemplate(
70
71
  await mkdir( path.dirname( outputPath ), { recursive: true } );
71
72
  await writeFile( outputPath, content );
72
73
  }
74
+
75
+ /**
76
+ * Generate routes/index.php file with route registry data.
77
+ *
78
+ * @param {string} rootDir Root directory path.
79
+ * @param {string} buildDir Build directory path.
80
+ * @param {string} prefix Package prefix.
81
+ */
82
+ export async function generateRoutesRegistry( rootDir, buildDir, prefix ) {
83
+ const routeNames = getAllRoutes( rootDir );
84
+
85
+ if ( routeNames.length === 0 ) {
86
+ // No routes to register, skip generating routes registry
87
+ return;
88
+ }
89
+
90
+ // Build routes array
91
+ const routes = routeNames.map( ( routeName ) => {
92
+ // Read package.json to get route path
93
+ const routePackageJson =
94
+ /** @type {import('./package-utils.mjs').RoutePackageJson} */ (
95
+ getPackageInfoFromFile(
96
+ path.join( rootDir, 'routes', routeName, 'package.json' )
97
+ )
98
+ );
99
+ const routePath = routePackageJson.route.path;
100
+
101
+ // Check if route.js exists
102
+ const routeFiles = getRouteFiles(
103
+ path.join( rootDir, 'routes', routeName )
104
+ );
105
+
106
+ return {
107
+ name: routeName,
108
+ path: routePath,
109
+ has_route: routeFiles.hasRoute,
110
+ };
111
+ } );
112
+
113
+ // Generate PHP array entries
114
+ const routeEntries = routes
115
+ .map( ( route ) => {
116
+ const hasRouteStr = route.has_route ? 'true' : 'false';
117
+ return `\tarray(
118
+ 'name' => '${ route.name }',
119
+ 'path' => '${ route.path }',
120
+ 'has_route' => ${ hasRouteStr },
121
+ )`;
122
+ } )
123
+ .join( ',\n' );
124
+
125
+ // Generate routes/index.php
126
+ const replacements = {
127
+ '{{PREFIX}}': prefix,
128
+ '{{ROUTES}}': routeEntries,
129
+ };
130
+
131
+ await generatePhpFromTemplate(
132
+ 'route-registry.php.template',
133
+ path.join( buildDir, 'routes', 'index.php' ),
134
+ replacements
135
+ );
136
+ }
137
+
138
+ /**
139
+ * Generate routes.php file with route registration logic.
140
+ *
141
+ * @param {string} rootDir Root directory path.
142
+ * @param {string} buildDir Build directory path.
143
+ * @param {string} handlePrefix Handle prefix for script modules.
144
+ * @param {string} prefix Package prefix.
145
+ */
146
+ export async function generateRoutesPhp(
147
+ rootDir,
148
+ buildDir,
149
+ handlePrefix,
150
+ prefix
151
+ ) {
152
+ const routeNames = getAllRoutes( rootDir );
153
+
154
+ if ( routeNames.length === 0 ) {
155
+ // No routes to register, skip generating routes.php
156
+ return;
157
+ }
158
+
159
+ const namespace = handlePrefix.replace( /-/g, '_' );
160
+
161
+ // Generate routes.php
162
+ const replacements = {
163
+ '{{PREFIX}}': prefix,
164
+ '{{NAMESPACE}}': namespace,
165
+ '{{HANDLE_PREFIX}}': handlePrefix,
166
+ };
167
+
168
+ await generatePhpFromTemplate(
169
+ 'routes.php.template',
170
+ path.join( buildDir, 'routes.php' ),
171
+ replacements
172
+ );
173
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { readdirSync } from 'fs';
5
+ import path from 'path';
6
+
7
+ /**
8
+ * Get all route names from the routes directory.
9
+ *
10
+ * @param {string} rootDir Root directory of the project.
11
+ * @return {string[]} Array of route names.
12
+ */
13
+ export function getAllRoutes( rootDir ) {
14
+ const routesPath = path.join( rootDir, 'routes' );
15
+
16
+ try {
17
+ return readdirSync( routesPath, { withFileTypes: true } )
18
+ .filter( ( dirent ) => dirent.isDirectory() )
19
+ .map( ( dirent ) => dirent.name );
20
+ } catch ( error ) {
21
+ // Routes directory doesn't exist, return empty array
22
+ return [];
23
+ }
24
+ }
25
+
26
+ /**
27
+ * @typedef {Object} RouteFiles
28
+ * @property {boolean} hasRoute Whether route file exists.
29
+ * @property {boolean} hasStage Whether stage file exists.
30
+ * @property {boolean} hasInspector Whether inspector file exists.
31
+ * @property {boolean} hasStyle Whether style file exists.
32
+ */
33
+
34
+ /**
35
+ * Check if a route has specific files.
36
+ *
37
+ * @param {string} routeDirectory Route directory path.
38
+ * @return {RouteFiles} Object with boolean flags for route files.
39
+ */
40
+ export function getRouteFiles( routeDirectory ) {
41
+ const extensions = [ 'tsx', 'ts', 'jsx', 'js' ];
42
+ const files = {
43
+ hasRoute: false,
44
+ hasStage: false,
45
+ hasInspector: false,
46
+ hasStyle: false,
47
+ };
48
+
49
+ const entries = readdirSync( routeDirectory );
50
+
51
+ for ( const ext of extensions ) {
52
+ if ( entries.includes( `route.${ ext }` ) ) {
53
+ files.hasRoute = true;
54
+ }
55
+ if ( entries.includes( `stage.${ ext }` ) ) {
56
+ files.hasStage = true;
57
+ }
58
+ if ( entries.includes( `inspector.${ ext }` ) ) {
59
+ files.hasInspector = true;
60
+ }
61
+ }
62
+
63
+ if ( entries.includes( 'route.scss' ) ) {
64
+ files.hasStyle = true;
65
+ }
66
+
67
+ return files;
68
+ }
69
+
70
+ /**
71
+ * Generate a synthetic content entry point for a route.
72
+ * This creates a module that imports and re-exports stage and inspector components.
73
+ *
74
+ * @param {RouteFiles} files Route files information.
75
+ * @return {string} Generated entry point code.
76
+ */
77
+ export function generateContentEntryPoint( files ) {
78
+ const lines = [];
79
+
80
+ if ( files.hasStage ) {
81
+ lines.push( "export { stage } from './stage';" );
82
+ }
83
+
84
+ if ( files.hasInspector ) {
85
+ lines.push( "export { inspector } from './inspector';" );
86
+ }
87
+
88
+ // If neither stage nor inspector exists, export empty object
89
+ if ( lines.length === 0 ) {
90
+ lines.push( 'export {};' );
91
+ }
92
+
93
+ return lines.join( '\n' );
94
+ }
@@ -15,10 +15,10 @@ import { getPackageInfo } from './package-utils.mjs';
15
15
  * This plugin handles WordPress package externals and vendor libraries,
16
16
  * treating them as external dependencies available via global variables.
17
17
  *
18
- * @param {string} packageNamespace Custom package namespace (e.g., 'wordpress', 'my-plugin').
19
- * @param {string|false} scriptGlobal Global variable name (e.g., 'wp', 'myPlugin') or false to disable globals.
20
- * @param {Object} externalNamespaces Additional namespaces to externalize (e.g., { 'woo': { global: 'woo', handlePrefix: 'woocommerce' } }).
21
- * @param {string} handlePrefix Handle prefix for main package (e.g., 'wp', 'mp'). Defaults to packageNamespace.
18
+ * @param {string} packageNamespace Custom package namespace (e.g., 'wordpress', 'my-plugin').
19
+ * @param {string|false} scriptGlobal Global variable name (e.g., 'wp', 'myPlugin') or false to disable globals.
20
+ * @param {Object} externalNamespaces Additional namespaces to externalize (e.g., { 'woo': { global: 'woo', handlePrefix: 'woocommerce' } }).
21
+ * @param {string} handlePrefix Handle prefix for main package (e.g., 'wp', 'mp'). Defaults to packageNamespace.
22
22
  * @return {Function} Function that creates the esbuild plugin instance.
23
23
  */
24
24
  export function createWordpressExternalsPlugin(
@@ -117,7 +117,11 @@ export function createWordpressExternalsPlugin(
117
117
  ];
118
118
 
119
119
  // Add custom namespace if different from wordpress and scriptGlobal is not false
120
- if ( packageNamespace && packageNamespace !== 'wordpress' && scriptGlobal !== false ) {
120
+ if (
121
+ packageNamespace &&
122
+ packageNamespace !== 'wordpress' &&
123
+ scriptGlobal !== false
124
+ ) {
121
125
  packageExternals.push( {
122
126
  namespace: packageNamespace,
123
127
  pattern: new RegExp( `^@${ packageNamespace }/` ),
@@ -127,7 +131,9 @@ export function createWordpressExternalsPlugin(
127
131
  }
128
132
 
129
133
  // Add additional external namespaces from configuration
130
- for ( const [ namespace, config ] of Object.entries( externalNamespaces ) ) {
134
+ for ( const [ namespace, config ] of Object.entries(
135
+ externalNamespaces
136
+ ) ) {
131
137
  packageExternals.push( {
132
138
  namespace,
133
139
  pattern: new RegExp( `^@${ namespace }/` ),
@@ -198,11 +204,17 @@ export function createWordpressExternalsPlugin(
198
204
 
199
205
  if ( isScriptModule ) {
200
206
  if ( kind === 'static' ) {
201
- moduleDependencies.set( args.path, 'static' );
207
+ moduleDependencies.set(
208
+ args.path,
209
+ 'static'
210
+ );
202
211
  } else if (
203
212
  ! moduleDependencies.has( args.path )
204
213
  ) {
205
- moduleDependencies.set( args.path, 'dynamic' );
214
+ moduleDependencies.set(
215
+ args.path,
216
+ 'dynamic'
217
+ );
206
218
  }
207
219
 
208
220
  return {
@@ -217,7 +229,9 @@ export function createWordpressExternalsPlugin(
217
229
  return {
218
230
  path: args.path,
219
231
  namespace: 'package-external',
220
- pluginData: { globalName: externalConfig.globalName },
232
+ pluginData: {
233
+ globalName: externalConfig.globalName,
234
+ },
221
235
  };
222
236
  }
223
237
 
@@ -246,7 +260,10 @@ export function createWordpressExternalsPlugin(
246
260
  const globalName = args.pluginData.globalName;
247
261
  // Extract package name after '@namespace/' prefix
248
262
  // e.g., '@wordpress/blocks' or '@my-plugin/data'
249
- const packagePath = args.path.split( '/' ).slice( 1 ).join( '/' );
263
+ const packagePath = args.path
264
+ .split( '/' )
265
+ .slice( 1 )
266
+ .join( '/' );
250
267
  const camelCasedName = camelCase( packagePath );
251
268
 
252
269
  return {
@@ -29,3 +29,9 @@ $styles_file = __DIR__ . '/styles.php';
29
29
  if ( file_exists( $styles_file ) ) {
30
30
  require_once $styles_file;
31
31
  }
32
+
33
+ // Load routes registration.
34
+ $routes_file = __DIR__ . '/routes.php';
35
+ if ( file_exists( $routes_file ) ) {
36
+ require_once $routes_file;
37
+ }
@@ -0,0 +1,11 @@
1
+ <?php
2
+ /**
3
+ * Route registry - Auto-generated by build process.
4
+ * Do not edit this file manually.
5
+ *
6
+ * @package {{PREFIX}}
7
+ */
8
+
9
+ return array(
10
+ {{ROUTES}}
11
+ );
@@ -0,0 +1,66 @@
1
+ <?php
2
+ /**
3
+ * Routes registration - Auto-generated by build process.
4
+ * Do not edit this file manually.
5
+ *
6
+ * @package {{PREFIX}}
7
+ */
8
+
9
+ if ( ! function_exists( '{{NAMESPACE}}_register_routes' ) ) {
10
+ /**
11
+ * Register all routes with WordPress.
12
+ */
13
+ function {{NAMESPACE}}_register_routes() {
14
+ $routes_dir = __DIR__ . '/routes';
15
+ $routes_file = $routes_dir . '/index.php';
16
+
17
+ if ( ! file_exists( $routes_file ) ) {
18
+ return;
19
+ }
20
+
21
+ $routes = require $routes_file;
22
+ $base_url = plugins_url( 'build/routes/', dirname( __FILE__ ) );
23
+ $extension = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '.js' : '.min.js';
24
+
25
+ foreach ( $routes as $route ) {
26
+ // Register content module
27
+ $content_asset_path = $routes_dir . '/' . $route['name'] . '/content.min.asset.php';
28
+ $content_asset = file_exists( $content_asset_path ) ? require $content_asset_path : array();
29
+ $content_handle = '{{HANDLE_PREFIX}}/routes/' . $route['name'] . '/content';
30
+
31
+ wp_register_script_module(
32
+ $content_handle,
33
+ $base_url . $route['name'] . '/content' . $extension,
34
+ $content_asset['module_dependencies'] ?? array(),
35
+ $content_asset['version'] ?? false
36
+ );
37
+
38
+ $route_handle = null;
39
+
40
+ // Register route module if it exists
41
+ if ( isset( $route['has_route'] ) && $route['has_route'] ) {
42
+ $route_asset_path = $routes_dir . '/' . $route['name'] . '/route.min.asset.php';
43
+ $route_asset = file_exists( $route_asset_path ) ? require $route_asset_path : array();
44
+ $route_handle = '{{HANDLE_PREFIX}}/routes/' . $route['name'] . '/route';
45
+
46
+ wp_register_script_module(
47
+ $route_handle,
48
+ $base_url . $route['name'] . '/route' . $extension,
49
+ $route_asset['module_dependencies'] ?? array(),
50
+ $route_asset['version'] ?? false
51
+ );
52
+ }
53
+
54
+ // Register the route with boot
55
+ if ( function_exists( 'gutenberg_register_boot_route' ) ) {
56
+ gutenberg_register_boot_route(
57
+ $route['path'],
58
+ $content_handle,
59
+ $route_handle
60
+ );
61
+ }
62
+ }
63
+ }
64
+
65
+ add_action( 'gutenberg_boot_init', '{{NAMESPACE}}_register_routes', 10 );
66
+ }