@wordpress/build 0.5.1-next.ba3aee3a2.0 → 0.6.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.6.0 (2026-01-16)
6
+
7
+ ### Breaking Changes
8
+
9
+ - Renamed generated PHP files to avoid `index.php` naming conflicts:
10
+ - `build/index.php` → `build/build.php`
11
+ - `build/modules/index.php` → `build/modules/registry.php`
12
+ - `build/scripts/index.php` → `build/scripts/registry.php`
13
+ - `build/styles/index.php` → `build/styles/registry.php`
14
+ - `build/routes/index.php` → `build/routes/registry.php`
15
+ - All generated page functions now include the `{{PREFIX}}` (from `wpPlugin.name`) at the beginning:
16
+ - `register_my_page_route()` → `my_plugin_register_my_page_route()`
17
+ - `my_page_render_page()` → `my_plugin_my_page_render_page()`
18
+ - And similarly for all other page functions
19
+ - Route registration now uses named functions instead of anonymous closures, allowing third-party developers to unhook them
20
+
5
21
  ## 0.4.0 (2025-11-26)
6
22
 
7
23
  ## 0.3.0 (2025-11-12)
package/README.md CHANGED
@@ -248,15 +248,17 @@ WP-Admin mode integrates within the standard WordPress admin interface (keeping
248
248
 
249
249
  ```php
250
250
  add_submenu_page(
251
- 'themes.php', // Parent menu
252
- __( 'My Page', 'my-plugin' ), // Page title
253
- __( 'My Page', 'my-plugin' ), // Menu title
254
- 'edit_theme_options', // Capability
255
- 'my-admin-page-wp-admin', // Menu slug (simple)
256
- 'my_admin_page_wp_admin_render_page' // Callback from generated PHP
251
+ 'themes.php', // Parent menu
252
+ __( 'My Page', 'my-plugin' ), // Page title
253
+ __( 'My Page', 'my-plugin' ), // Menu title
254
+ 'edit_theme_options', // Capability
255
+ 'my-admin-page-wp-admin', // Menu slug (simple)
256
+ 'my_plugin_my_admin_page_wp_admin_render_page' // Callback from generated PHP (prefixed)
257
257
  );
258
258
  ```
259
259
 
260
+ Note: The callback function name is prefixed with your plugin name (from `wpPlugin.name` in root `package.json`). For example, if your plugin name is `my-plugin`, the function will be `my_plugin_my_admin_page_wp_admin_render_page`.
261
+
260
262
  The page slug is `my-admin-page-wp-admin` (your page ID + `-wp-admin`). WordPress routes all requests to this callback, and the JavaScript router handles internal navigation.
261
263
 
262
264
  **Deep linking with the `p` query parameter:**
@@ -274,7 +276,7 @@ When the page loads, the JavaScript boot system reads the `p` parameter and navi
274
276
  Full-page mode takes over the entire admin screen with a custom sidebar:
275
277
 
276
278
  ```php
277
- add_menu_page( 'Title', 'Menu', 'capability', 'my-admin-page', 'my_admin_page_render_page', 'icon', 20 );
279
+ add_menu_page( 'Title', 'Menu', 'capability', 'my-admin-page', 'my_plugin_my_admin_page_render_page', 'icon', 20 );
278
280
  ```
279
281
 
280
282
  **Init Modules:**
@@ -364,7 +366,7 @@ The built tool generates several files in the `build/` directory, but the primar
364
366
  Make sure to include the generated PHP file in your plugin file.
365
367
 
366
368
  ```php
367
- require_once plugin_dir_path( __FILE__ ) . 'build/index.php';
369
+ require_once plugin_dir_path( __FILE__ ) . 'build/build.php';
368
370
  ```
369
371
 
370
372
  ## Routes (Experimental)
@@ -470,7 +472,7 @@ The `canvas()` function controls which canvas is rendered:
470
472
  The build system generates:
471
473
  - `build/routes/{route-name}/content.js` - Bundled stage/inspector/canvas components
472
474
  - `build/routes/{route-name}/route.js` - Bundled lifecycle hooks (if present)
473
- - `build/routes/index.php` - Route registry data
475
+ - `build/routes/registry.php` - Route registry data
474
476
  - `build/routes.php` - Route registration logic
475
477
 
476
478
  The boot package in Gutenberg will automatically use these routes and make them available.
package/lib/build.mjs CHANGED
@@ -17,6 +17,7 @@ import rtlcss from 'rtlcss';
17
17
  import cssnano from 'cssnano';
18
18
  import babel from 'esbuild-plugin-babel';
19
19
  import { camelCase } from 'change-case';
20
+ import { NodePackageImporter } from 'sass-embedded';
20
21
 
21
22
  /**
22
23
  * Internal dependencies
@@ -29,6 +30,7 @@ import {
29
30
  import {
30
31
  generatePhpFromTemplate,
31
32
  getPhpReplacements,
33
+ renderTemplateToString,
32
34
  } from './php-generator.mjs';
33
35
  import { getPackageInfo, getPackageInfoFromFile } from './package-utils.mjs';
34
36
  import { createWordpressExternalsPlugin } from './wordpress-externals-plugin.mjs';
@@ -107,26 +109,59 @@ const wordpressExternalsPlugin = createWordpressExternalsPlugin(
107
109
  HANDLE_PREFIX
108
110
  );
109
111
 
110
- const styleBundlingPlugins = [
111
- // Handle CSS modules (.module.css and .module.scss)
112
- sassPlugin( {
113
- embedded: true,
114
- filter: /\.module\.(css|scss)$/,
115
- transform: postcssModules( {
116
- generateScopedName: '[name]__[local]__[hash:base64:5]',
112
+ /**
113
+ * Get SASS options for the given working directory.
114
+ *
115
+ * Uses NodePackageImporter from sass-embedded for resolving package imports
116
+ * (like @wordpress/base-styles) which works with any package manager (npm, pnpm, yarn).
117
+ *
118
+ * @param {string} workingDir - The directory where we're working (for NodePackageImporter).
119
+ * @return {Object} SASS options object with importers and loadPaths.
120
+ */
121
+ function getSassOptions( workingDir ) {
122
+ return {
123
+ importers: [ new NodePackageImporter( workingDir ) ],
124
+ // loadPaths for resolving @wordpress/base-styles imports and local base-styles imports
125
+ loadPaths: [
126
+ // Package's own node_modules (for pnpm isolated deps)
127
+ path.join( workingDir, 'node_modules' ),
128
+ // Root node_modules (for npm hoisted deps)
129
+ path.join( ROOT_DIR, 'node_modules' ),
130
+ // For local imports like @use "mixins"
131
+ path.join( PACKAGES_DIR, 'base-styles' ),
132
+ ],
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Create style bundling plugins with the given working directory.
138
+ *
139
+ * @param {string} workingDir - The directory where we're working (for NodePackageImporter).
140
+ * @return {object[]} Array of esbuild plugins for handling CSS/SCSS.
141
+ */
142
+ function createStyleBundlingPlugins( workingDir ) {
143
+ const sassOptions = getSassOptions( workingDir );
144
+ return [
145
+ // Handle CSS modules (.module.css and .module.scss)
146
+ sassPlugin( {
147
+ embedded: true,
148
+ filter: /\.module\.(css|scss)$/,
149
+ transform: postcssModules( {
150
+ generateScopedName: '[name]__[local]__[hash:base64:5]',
151
+ } ),
152
+ type: 'style',
153
+ ...sassOptions,
117
154
  } ),
118
- type: 'style',
119
- loadPaths: [ 'node_modules', path.join( PACKAGES_DIR, 'base-styles' ) ],
120
- } ),
121
- // Handle regular CSS/SCSS files
122
- // Note: .module.css and .module.scss already handled by plugin above
123
- sassPlugin( {
124
- embedded: true,
125
- filter: /\.(css|scss)$/,
126
- type: 'style',
127
- loadPaths: [ 'node_modules', path.join( PACKAGES_DIR, 'base-styles' ) ],
128
- } ),
129
- ];
155
+ // Handle regular CSS/SCSS files
156
+ // Note: .module.css and .module.scss already handled by plugin above
157
+ sassPlugin( {
158
+ embedded: true,
159
+ filter: /\.(css|scss)$/,
160
+ type: 'style',
161
+ ...sassOptions,
162
+ } ),
163
+ ];
164
+ }
130
165
 
131
166
  /**
132
167
  * Normalize path separators for cross-platform compatibility.
@@ -193,56 +228,36 @@ function transformPhpContent( content, transforms ) {
193
228
  function momentTimezoneAliasPlugin() {
194
229
  return {
195
230
  name: 'moment-timezone-alias',
196
- async setup( build ) {
197
- const { createRequire } = await import( 'module' );
198
- const require = createRequire( import.meta.url );
199
-
200
- // Cached paths - resolved lazily on first use
201
- let preBuiltBundlePath;
202
- let momentTimezoneUtilsPath;
203
- const resolvePaths = () => {
204
- if ( preBuiltBundlePath ) {
205
- return;
206
- }
207
- preBuiltBundlePath = require.resolve(
208
- 'moment-timezone/builds/moment-timezone-with-data-1970-2030'
209
- );
210
- momentTimezoneUtilsPath = require.resolve(
211
- 'moment-timezone/moment-timezone-utils.js'
212
- );
213
- };
231
+ setup( build ) {
232
+ // Alias path that esbuild will resolve
233
+ const aliasPath =
234
+ 'moment-timezone/builds/moment-timezone-with-data-1970-2030.js';
214
235
 
215
236
  // Redirect main moment-timezone files to pre-built bundle
216
237
  build.onResolve(
217
- { filter: /^moment-timezone\/moment-timezone$/ },
218
- () => {
219
- resolvePaths();
220
- return { path: preBuiltBundlePath };
221
- }
222
- );
223
-
224
- // For utils, we need to load it but ensure it works with the pre-built bundle.
225
- // The utils file tries to require('./') which would load index.js.
226
- // We need to make sure it gets the pre-built bundle instead.
227
- build.onResolve(
228
- { filter: /^moment-timezone\/moment-timezone-utils$/ },
229
- () => {
230
- resolvePaths();
231
- return { path: momentTimezoneUtilsPath };
232
- }
238
+ { filter: /^moment-timezone\/moment-timezone\.js$/ },
239
+ ( { importer, resolveDir, kind } ) =>
240
+ build.resolve( aliasPath, {
241
+ importer,
242
+ resolveDir,
243
+ kind,
244
+ } )
233
245
  );
234
246
 
235
247
  // Intercept the require('./') call inside moment-timezone-utils
236
248
  // and redirect it to the pre-built bundle.
237
- build.onResolve( { filter: /^\.\/$/ }, ( args ) => {
238
- if (
239
- args.importer &&
240
- args.importer.includes( 'moment-timezone-utils' )
241
- ) {
242
- resolvePaths();
243
- return { path: preBuiltBundlePath };
249
+ build.onResolve(
250
+ { filter: /^\.\/$/ },
251
+ ( { importer, resolveDir, kind } ) => {
252
+ if ( importer.includes( 'moment-timezone-utils' ) ) {
253
+ return build.resolve( aliasPath, {
254
+ importer,
255
+ resolveDir,
256
+ kind,
257
+ } );
258
+ }
244
259
  }
245
- } );
260
+ );
246
261
  },
247
262
  };
248
263
  }
@@ -729,7 +744,7 @@ async function generateModuleRegistrationPhp( modules, replacements ) {
729
744
  await Promise.all( [
730
745
  generatePhpFromTemplate(
731
746
  'module-registry.php.template',
732
- path.join( BUILD_DIR, 'modules', 'index.php' ),
747
+ path.join( BUILD_DIR, 'modules', 'registry.php' ),
733
748
  { ...replacements, '{{MODULES}}': modulesArray }
734
749
  ),
735
750
  generatePhpFromTemplate(
@@ -762,7 +777,7 @@ async function generateScriptRegistrationPhp( scripts, replacements ) {
762
777
  await Promise.all( [
763
778
  generatePhpFromTemplate(
764
779
  'script-registry.php.template',
765
- path.join( BUILD_DIR, 'scripts', 'index.php' ),
780
+ path.join( BUILD_DIR, 'scripts', 'registry.php' ),
766
781
  { ...replacements, '{{SCRIPTS}}': scriptsArray }
767
782
  ),
768
783
  generatePhpFromTemplate(
@@ -810,7 +825,7 @@ async function generateStyleRegistrationPhp( styles, replacements ) {
810
825
  await Promise.all( [
811
826
  generatePhpFromTemplate(
812
827
  'style-registry.php.template',
813
- path.join( BUILD_DIR, 'styles', 'index.php' ),
828
+ path.join( BUILD_DIR, 'styles', 'registry.php' ),
814
829
  { ...replacements, '{{STYLES}}': stylesArray }
815
830
  ),
816
831
  generatePhpFromTemplate(
@@ -822,14 +837,14 @@ async function generateStyleRegistrationPhp( styles, replacements ) {
822
837
  }
823
838
 
824
839
  /**
825
- * Generate main index.php that loads both modules and scripts.
840
+ * Generate main build.php that loads both modules and scripts.
826
841
  *
827
842
  * @param {Record<string, string>} replacements PHP template replacements.
828
843
  */
829
- async function generateMainIndexPhp( replacements ) {
844
+ async function generateMainBuildPhp( replacements ) {
830
845
  await generatePhpFromTemplate(
831
- 'index.php.template',
832
- path.join( BUILD_DIR, 'index.php' ),
846
+ 'build.php.template',
847
+ path.join( BUILD_DIR, 'build.php' ),
833
848
  replacements
834
849
  );
835
850
  }
@@ -862,10 +877,10 @@ async function generateRoutesRegistry( routes, replacements ) {
862
877
  } )
863
878
  .join( ',\n' );
864
879
 
865
- // Generate single global registry at build/routes/index.php
880
+ // Generate single global registry at build/routes/registry.php
866
881
  await generatePhpFromTemplate(
867
882
  'route-registry.php.template',
868
- path.join( BUILD_DIR, 'routes', 'index.php' ),
883
+ path.join( BUILD_DIR, 'routes', 'registry.php' ),
869
884
  { ...replacements, '{{ROUTES}}': routeEntries }
870
885
  );
871
886
  }
@@ -883,11 +898,35 @@ async function generateRoutesPhp( routes, replacements ) {
883
898
  return;
884
899
  }
885
900
 
886
- // Generate routes.php from template
901
+ // Get unique pages from routes
902
+ const pages = [ ...new Set( routes.map( ( route ) => route.page ) ) ];
903
+
904
+ // Generate page-specific functions for each page
905
+ const pageFunctionsPromises = pages.map( async ( pageSlug ) => {
906
+ const pageSlugUnderscore = pageSlug.replace( /-/g, '_' );
907
+ const pageReplacements = {
908
+ ...replacements,
909
+ '{{PAGE_SLUG}}': pageSlug,
910
+ '{{PAGE_SLUG_UNDERSCORE}}': pageSlugUnderscore,
911
+ };
912
+ return renderTemplateToString(
913
+ 'routes-page-functions.php.template',
914
+ pageReplacements
915
+ );
916
+ } );
917
+
918
+ const pageFunctions = await Promise.all( pageFunctionsPromises );
919
+ const pageRouteFunctionsCode = pageFunctions.join( '\n' );
920
+
921
+ // Generate routes.php from template with injected page functions
887
922
  await generatePhpFromTemplate(
888
923
  'routes-registration.php.template',
889
924
  path.join( BUILD_DIR, 'routes.php' ),
890
- { ...replacements, '{{HANDLE_PREFIX}}': HANDLE_PREFIX }
925
+ {
926
+ ...replacements,
927
+ '{{HANDLE_PREFIX}}': HANDLE_PREFIX,
928
+ '{{PAGE_ROUTE_FUNCTIONS}}': pageRouteFunctionsCode,
929
+ }
891
930
  );
892
931
  }
893
932
 
@@ -1092,7 +1131,7 @@ async function transpilePackage( packageName ) {
1092
1131
  const plugins = [
1093
1132
  needsEmotionPlugin && emotionPlugin,
1094
1133
  externalizeAllExceptCssPlugin,
1095
- ...styleBundlingPlugins,
1134
+ ...createStyleBundlingPlugins( packageDir ),
1096
1135
  ].filter( Boolean );
1097
1136
 
1098
1137
  if ( packageJson.main ) {
@@ -1226,10 +1265,7 @@ async function compileStyles( packageName ) {
1226
1265
  plugins: [
1227
1266
  sassPlugin( {
1228
1267
  embedded: true,
1229
- loadPaths: [
1230
- 'node_modules',
1231
- path.join( PACKAGES_DIR, 'base-styles' ),
1232
- ],
1268
+ ...getSassOptions( packageDir ),
1233
1269
  async transform( source ) {
1234
1270
  // Process with autoprefixer for LTR version
1235
1271
  const ltrResult = await postcss( [
@@ -1332,12 +1368,6 @@ async function buildRoute( routeName ) {
1332
1368
  // Ensure output directory exists
1333
1369
  await mkdir( outputDir, { recursive: true } );
1334
1370
 
1335
- // Copy package.json
1336
- await copyFile(
1337
- path.join( routeDir, 'package.json' ),
1338
- path.join( outputDir, 'package.json' )
1339
- );
1340
-
1341
1371
  const files = getRouteFiles( routeDir );
1342
1372
 
1343
1373
  // Build route.js if it exists
@@ -1414,7 +1444,7 @@ async function buildRoute( routeName ) {
1414
1444
  [],
1415
1445
  true // Generate asset file for minified build
1416
1446
  ),
1417
- ...styleBundlingPlugins,
1447
+ ...createStyleBundlingPlugins( routeDir ),
1418
1448
  ],
1419
1449
  } ),
1420
1450
  esbuild.build( {
@@ -1432,7 +1462,7 @@ async function buildRoute( routeName ) {
1432
1462
  [],
1433
1463
  false // Skip asset file for non-minified build
1434
1464
  ),
1435
- ...styleBundlingPlugins,
1465
+ ...createStyleBundlingPlugins( routeDir ),
1436
1466
  ],
1437
1467
  } ),
1438
1468
  ] );
@@ -1635,7 +1665,7 @@ async function buildAll( baseUrlExpression ) {
1635
1665
  baseUrlExpression
1636
1666
  );
1637
1667
  await Promise.all( [
1638
- generateMainIndexPhp( phpReplacements ),
1668
+ generateMainBuildPhp( phpReplacements ),
1639
1669
  generateModuleRegistrationPhp( modules, phpReplacements ),
1640
1670
  generateScriptRegistrationPhp( scripts, phpReplacements ),
1641
1671
  generateStyleRegistrationPhp( styles, phpReplacements ),
@@ -1644,13 +1674,14 @@ async function buildAll( baseUrlExpression ) {
1644
1674
  generateRoutesPhp( routes, phpReplacements ),
1645
1675
  generatePagesPhp( pageData, phpReplacements ),
1646
1676
  ] );
1677
+ console.log( ' ✔ Generated build/build.php' );
1647
1678
  console.log( ' ✔ Generated build/modules.php' );
1648
- console.log( ' ✔ Generated build/modules/index.php' );
1679
+ console.log( ' ✔ Generated build/modules/registry.php' );
1649
1680
  console.log( ' ✔ Generated build/scripts.php' );
1650
- console.log( ' ✔ Generated build/scripts/index.php' );
1681
+ console.log( ' ✔ Generated build/scripts/registry.php' );
1651
1682
  console.log( ' ✔ Generated build/styles.php' );
1652
- console.log( ' ✔ Generated build/styles/index.php' );
1653
- console.log( ' ✔ Generated build/version.php' );
1683
+ console.log( ' ✔ Generated build/styles/registry.php' );
1684
+ console.log( ' ✔ Generated build/constants.php' );
1654
1685
  console.log( ' ✔ Generated build/routes.php' );
1655
1686
  if ( pageData.length > 0 ) {
1656
1687
  console.log( ' ✔ Generated build/pages.php' );
@@ -1663,7 +1694,6 @@ async function buildAll( baseUrlExpression ) {
1663
1694
  );
1664
1695
  }
1665
1696
  }
1666
- console.log( ' ✔ Generated build/index.php' );
1667
1697
 
1668
1698
  const totalTime = Date.now() - startTime;
1669
1699
  console.log(
@@ -1933,6 +1963,7 @@ async function main() {
1933
1963
  default: 'plugin_dir_url( __FILE__ )',
1934
1964
  },
1935
1965
  },
1966
+ strict: false,
1936
1967
  } );
1937
1968
 
1938
1969
  const baseUrlExpression = values[ 'base-url' ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wordpress/build",
3
- "version": "0.5.1-next.ba3aee3a2.0",
3
+ "version": "0.6.0",
4
4
  "description": "Build tool for WordPress plugins.",
5
5
  "author": "The WordPress Contributors",
6
6
  "license": "GPL-2.0-or-later",
@@ -48,7 +48,8 @@
48
48
  "moment-timezone": "^0.5.40",
49
49
  "postcss": "8.4.38",
50
50
  "postcss-modules": "6.0.1",
51
- "rtlcss": "4.3.0"
51
+ "rtlcss": "4.3.0",
52
+ "sass-embedded": "^1.97.2"
52
53
  },
53
54
  "devDependencies": {
54
55
  "@types/node": "^20.17.10"
@@ -76,5 +77,5 @@
76
77
  "publishConfig": {
77
78
  "access": "public"
78
79
  },
79
- "gitHead": "67d2e486fcd40c753591cf911ca0659132f519ca"
80
+ "gitHead": "50c4c0f51e4797c217946ce42adfaa5eb026f33f"
80
81
  }
@@ -13,8 +13,8 @@ if ( ! function_exists( '{{PREFIX}}_register_script_modules' ) ) {
13
13
  function {{PREFIX}}_register_script_modules() {
14
14
  // Load build constants
15
15
  $build_constants = require __DIR__ . '/constants.php';
16
- $modules_dir = __DIR__ . '/modules';
17
- $modules_file = $modules_dir . '/index.php';
16
+ $modules_dir = __DIR__ . '/modules';
17
+ $modules_file = $modules_dir . '/registry.php';
18
18
 
19
19
  if ( ! file_exists( $modules_file ) ) {
20
20
  return;
@@ -12,10 +12,10 @@
12
12
 
13
13
  // Global storage for {{PAGE_SLUG}} routes and menu items
14
14
  global ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_routes, ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_menu_items;
15
- ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_routes = array();
15
+ ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_routes = array();
16
16
  ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_menu_items = array();
17
17
 
18
- if ( ! function_exists( 'register_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_route' ) ) {
18
+ if ( ! function_exists( '{{PREFIX}}_register_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_route' ) ) {
19
19
  /**
20
20
  * Register a route for the {{PAGE_SLUG}}-wp-admin page.
21
21
  *
@@ -23,7 +23,7 @@ if ( ! function_exists( 'register_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_route' ) ) {
23
23
  * @param string|null $content_module Script module ID for content (stage/inspector).
24
24
  * @param string|null $route_module Script module ID for route lifecycle hooks.
25
25
  */
26
- function register_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_route( $path, $content_module = null, $route_module = null ) {
26
+ function {{PREFIX}}_register_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_route( $path, $content_module = null, $route_module = null ) {
27
27
  global ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_routes;
28
28
 
29
29
  $route = array( 'path' => $path );
@@ -38,7 +38,7 @@ if ( ! function_exists( 'register_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_route' ) ) {
38
38
  }
39
39
  }
40
40
 
41
- if ( ! function_exists( 'register_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_menu_item' ) ) {
41
+ if ( ! function_exists( '{{PREFIX}}_register_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_menu_item' ) ) {
42
42
  /**
43
43
  * Register a menu item for the {{PAGE_SLUG}}-wp-admin page.
44
44
  * Note: Menu items are registered but not displayed in single-page mode.
@@ -48,7 +48,7 @@ if ( ! function_exists( 'register_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_menu_item' )
48
48
  * @param string $to Route path to navigate to.
49
49
  * @param string $parent_id Optional. Parent menu item ID.
50
50
  */
51
- function register_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_menu_item( $id, $label, $to, $parent_id = '' ) {
51
+ function {{PREFIX}}_register_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_menu_item( $id, $label, $to, $parent_id = '' ) {
52
52
  global ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_menu_items;
53
53
 
54
54
  $menu_item = array(
@@ -65,36 +65,36 @@ if ( ! function_exists( 'register_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_menu_item' )
65
65
  }
66
66
  }
67
67
 
68
- if ( ! function_exists( 'get_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_routes' ) ) {
68
+ if ( ! function_exists( '{{PREFIX}}_get_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_routes' ) ) {
69
69
  /**
70
70
  * Get all registered routes for the {{PAGE_SLUG}}-wp-admin page.
71
71
  *
72
72
  * @return array Array of route objects.
73
73
  */
74
- function get_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_routes() {
74
+ function {{PREFIX}}_get_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_routes() {
75
75
  global ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_routes;
76
76
  return ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_routes ?? array();
77
77
  }
78
78
  }
79
79
 
80
- if ( ! function_exists( 'get_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_menu_items' ) ) {
80
+ if ( ! function_exists( '{{PREFIX}}_get_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_menu_items' ) ) {
81
81
  /**
82
82
  * Get all registered menu items for the {{PAGE_SLUG}}-wp-admin page.
83
83
  *
84
84
  * @return array Array of menu item objects.
85
85
  */
86
- function get_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_menu_items() {
86
+ function {{PREFIX}}_get_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_menu_items() {
87
87
  global ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_menu_items;
88
88
  return ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_menu_items ?? array();
89
89
  }
90
90
  }
91
91
 
92
- if ( ! function_exists( '{{PAGE_SLUG_UNDERSCORE}}_wp_admin_preload_data' ) ) {
92
+ if ( ! function_exists( '{{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_preload_data' ) ) {
93
93
  /**
94
94
  * Preload REST API data for the {{PAGE_SLUG}}-wp-admin page.
95
95
  * Automatically called during page rendering.
96
96
  */
97
- function {{PAGE_SLUG_UNDERSCORE}}_wp_admin_preload_data() {
97
+ function {{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_preload_data() {
98
98
  // Define paths to preload - same for all pages
99
99
  $preload_paths = array(
100
100
  '/?_fields=description,gmt_offset,home,name,site_icon,site_icon_url,site_logo,timezone_string,url,page_for_posts,page_on_front,show_on_front',
@@ -120,14 +120,14 @@ if ( ! function_exists( '{{PAGE_SLUG_UNDERSCORE}}_wp_admin_preload_data' ) ) {
120
120
  }
121
121
  }
122
122
 
123
- if ( ! function_exists( '{{PAGE_SLUG_UNDERSCORE}}_wp_admin_enqueue_scripts' ) ) {
123
+ if ( ! function_exists( '{{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_enqueue_scripts' ) ) {
124
124
  /**
125
125
  * Enqueue scripts and styles for the {{PAGE_SLUG}}-wp-admin page.
126
126
  * Hooked to admin_enqueue_scripts.
127
127
  *
128
128
  * @param string $hook_suffix The current admin page.
129
129
  */
130
- function {{PAGE_SLUG_UNDERSCORE}}_wp_admin_enqueue_scripts( $hook_suffix ) {
130
+ function {{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_enqueue_scripts( $hook_suffix ) {
131
131
  // Check all possible ways this page can be accessed:
132
132
  // 1. Menu page via admin.php?page={{PAGE_SLUG}}-wp-admin (plugin)
133
133
  // 2. Direct file via {{PAGE_SLUG}}.php (Core) - screen ID will be '{{PAGE_SLUG}}'
@@ -148,10 +148,10 @@ if ( ! function_exists( '{{PAGE_SLUG_UNDERSCORE}}_wp_admin_enqueue_scripts' ) )
148
148
  do_action( '{{PAGE_SLUG}}-wp-admin_init' );
149
149
 
150
150
  // Preload REST API data
151
- {{PAGE_SLUG_UNDERSCORE}}_wp_admin_preload_data();
151
+ {{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_preload_data();
152
152
 
153
153
  // Get all registered routes
154
- $routes = get_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_routes();
154
+ $routes = {{PREFIX}}_get_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_routes();
155
155
 
156
156
  // Get boot module asset file for dependencies
157
157
  $asset_file = __DIR__ . '/../../modules/boot/index.min.asset.php';
@@ -221,13 +221,13 @@ if ( ! function_exists( '{{PAGE_SLUG_UNDERSCORE}}_wp_admin_enqueue_scripts' ) )
221
221
  }
222
222
  }
223
223
 
224
- if ( ! function_exists( '{{PAGE_SLUG_UNDERSCORE}}_wp_admin_render_page' ) ) {
224
+ if ( ! function_exists( '{{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_render_page' ) ) {
225
225
  /**
226
226
  * Render the {{PAGE_SLUG}}-wp-admin page.
227
227
  * Call this function from add_menu_page or add_submenu_page.
228
228
  * This renders within the normal WordPress admin interface.
229
229
  */
230
- function {{PAGE_SLUG_UNDERSCORE}}_wp_admin_render_page() {
230
+ function {{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_render_page() {
231
231
  ?>
232
232
  <style>
233
233
  /* Critical styles to prevent layout shifts - inlined for immediate application */
@@ -288,5 +288,5 @@ if ( ! function_exists( '{{PAGE_SLUG_UNDERSCORE}}_wp_admin_render_page' ) ) {
288
288
  }
289
289
 
290
290
  // Hook the enqueue function to admin_enqueue_scripts
291
- add_action( 'admin_enqueue_scripts', '{{PAGE_SLUG_UNDERSCORE}}_wp_admin_enqueue_scripts' );
291
+ add_action( 'admin_enqueue_scripts', '{{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_enqueue_scripts' );
292
292
 
@@ -9,10 +9,10 @@
9
9
 
10
10
  // Global storage for {{PAGE_SLUG}} routes and menu items
11
11
  global ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_routes, ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_menu_items;
12
- ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_routes = array();
12
+ ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_routes = array();
13
13
  ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_menu_items = array();
14
14
 
15
- if ( ! function_exists( 'register_{{PAGE_SLUG_UNDERSCORE}}_route' ) ) {
15
+ if ( ! function_exists( '{{PREFIX}}_register_{{PAGE_SLUG_UNDERSCORE}}_route' ) ) {
16
16
  /**
17
17
  * Register a route for the {{PAGE_SLUG}} page.
18
18
  *
@@ -20,7 +20,7 @@ if ( ! function_exists( 'register_{{PAGE_SLUG_UNDERSCORE}}_route' ) ) {
20
20
  * @param string|null $content_module Script module ID for content (stage/inspector).
21
21
  * @param string|null $route_module Script module ID for route lifecycle hooks.
22
22
  */
23
- function register_{{PAGE_SLUG_UNDERSCORE}}_route( $path, $content_module = null, $route_module = null ) {
23
+ function {{PREFIX}}_register_{{PAGE_SLUG_UNDERSCORE}}_route( $path, $content_module = null, $route_module = null ) {
24
24
  global ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_routes;
25
25
 
26
26
  $route = array( 'path' => $path );
@@ -35,7 +35,7 @@ if ( ! function_exists( 'register_{{PAGE_SLUG_UNDERSCORE}}_route' ) ) {
35
35
  }
36
36
  }
37
37
 
38
- if ( ! function_exists( 'register_{{PAGE_SLUG_UNDERSCORE}}_menu_item' ) ) {
38
+ if ( ! function_exists( '{{PREFIX}}_register_{{PAGE_SLUG_UNDERSCORE}}_menu_item' ) ) {
39
39
  /**
40
40
  * Register a menu item for the {{PAGE_SLUG}} page.
41
41
  *
@@ -45,7 +45,7 @@ if ( ! function_exists( 'register_{{PAGE_SLUG_UNDERSCORE}}_menu_item' ) ) {
45
45
  * @param string $parent_id Optional. Parent menu item ID.
46
46
  * @param string $parent_type Optional. Parent type: 'drilldown' or 'dropdown'.
47
47
  */
48
- function register_{{PAGE_SLUG_UNDERSCORE}}_menu_item( $id, $label, $to, $parent_id = '', $parent_type = '' ) {
48
+ function {{PREFIX}}_register_{{PAGE_SLUG_UNDERSCORE}}_menu_item( $id, $label, $to, $parent_id = '', $parent_type = '' ) {
49
49
  global ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_menu_items;
50
50
 
51
51
  $menu_item = array(
@@ -66,36 +66,36 @@ if ( ! function_exists( 'register_{{PAGE_SLUG_UNDERSCORE}}_menu_item' ) ) {
66
66
  }
67
67
  }
68
68
 
69
- if ( ! function_exists( 'get_{{PAGE_SLUG_UNDERSCORE}}_routes' ) ) {
69
+ if ( ! function_exists( '{{PREFIX}}_get_{{PAGE_SLUG_UNDERSCORE}}_routes' ) ) {
70
70
  /**
71
71
  * Get all registered routes for the {{PAGE_SLUG}} page.
72
72
  *
73
73
  * @return array Array of route objects.
74
74
  */
75
- function get_{{PAGE_SLUG_UNDERSCORE}}_routes() {
75
+ function {{PREFIX}}_get_{{PAGE_SLUG_UNDERSCORE}}_routes() {
76
76
  global ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_routes;
77
77
  return ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_routes ?? array();
78
78
  }
79
79
  }
80
80
 
81
- if ( ! function_exists( 'get_{{PAGE_SLUG_UNDERSCORE}}_menu_items' ) ) {
81
+ if ( ! function_exists( '{{PREFIX}}_get_{{PAGE_SLUG_UNDERSCORE}}_menu_items' ) ) {
82
82
  /**
83
83
  * Get all registered menu items for the {{PAGE_SLUG}} page.
84
84
  *
85
85
  * @return array Array of menu item objects.
86
86
  */
87
- function get_{{PAGE_SLUG_UNDERSCORE}}_menu_items() {
87
+ function {{PREFIX}}_get_{{PAGE_SLUG_UNDERSCORE}}_menu_items() {
88
88
  global ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_menu_items;
89
89
  return ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_menu_items ?? array();
90
90
  }
91
91
  }
92
92
 
93
- if ( ! function_exists( '{{PAGE_SLUG_UNDERSCORE}}_preload_data' ) ) {
93
+ if ( ! function_exists( '{{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_preload_data' ) ) {
94
94
  /**
95
95
  * Preload REST API data for the {{PAGE_SLUG}} page.
96
96
  * Automatically called during page rendering.
97
97
  */
98
- function {{PAGE_SLUG_UNDERSCORE}}_preload_data() {
98
+ function {{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_preload_data() {
99
99
  // Define paths to preload - same for all pages
100
100
  $preload_paths = array(
101
101
  '/?_fields=description,gmt_offset,home,name,site_icon,site_icon_url,site_logo,timezone_string,url,page_for_posts,page_on_front,show_on_front',
@@ -121,12 +121,12 @@ if ( ! function_exists( '{{PAGE_SLUG_UNDERSCORE}}_preload_data' ) ) {
121
121
  }
122
122
  }
123
123
 
124
- if ( ! function_exists( '{{PAGE_SLUG_UNDERSCORE}}_render_page' ) ) {
124
+ if ( ! function_exists( '{{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_render_page' ) ) {
125
125
  /**
126
126
  * Render the {{PAGE_SLUG}} page.
127
127
  * Call this function from add_menu_page or add_submenu_page.
128
128
  */
129
- function {{PAGE_SLUG_UNDERSCORE}}_render_page() {
129
+ function {{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_render_page() {
130
130
  // Load build constants
131
131
  $build_constants = require __DIR__ . '/../../constants.php';
132
132
 
@@ -153,11 +153,11 @@ if ( ! function_exists( '{{PAGE_SLUG_UNDERSCORE}}_render_page' ) ) {
153
153
  }
154
154
 
155
155
  // Preload REST API data
156
- {{PAGE_SLUG_UNDERSCORE}}_preload_data();
156
+ {{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_preload_data();
157
157
 
158
158
  // Get all registered routes and menu items
159
- $menu_items = get_{{PAGE_SLUG_UNDERSCORE}}_menu_items();
160
- $routes = get_{{PAGE_SLUG_UNDERSCORE}}_routes();
159
+ $menu_items = {{PREFIX}}_get_{{PAGE_SLUG_UNDERSCORE}}_menu_items();
160
+ $routes = {{PREFIX}}_get_{{PAGE_SLUG_UNDERSCORE}}_routes();
161
161
 
162
162
  // Get boot module asset file for dependencies
163
163
  $asset_file = __DIR__ . '/../../modules/boot/index.min.asset.php';
@@ -312,19 +312,19 @@ if ( ! function_exists( '{{PAGE_SLUG_UNDERSCORE}}_render_page' ) ) {
312
312
  }
313
313
  }
314
314
 
315
- if ( ! function_exists( '{{PAGE_SLUG_UNDERSCORE}}_intercept_render' ) ) {
315
+ if ( ! function_exists( '{{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_intercept_render' ) ) {
316
316
  /**
317
317
  * Intercept admin_init to render the page early.
318
318
  * This bypasses the default WordPress admin template.
319
319
  */
320
- function {{PAGE_SLUG_UNDERSCORE}}_intercept_render() {
320
+ function {{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_intercept_render() {
321
321
  // phpcs:ignore WordPress.Security.NonceVerification.Recommended
322
322
  if ( isset( $_GET['page'] ) && '{{PAGE_SLUG}}' === $_GET['page'] ) {
323
- {{PAGE_SLUG_UNDERSCORE}}_render_page();
323
+ {{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_render_page();
324
324
  exit;
325
325
  }
326
326
  }
327
327
  }
328
328
 
329
329
  // Hook the interceptor to admin_init
330
- add_action( 'admin_init', '{{PAGE_SLUG_UNDERSCORE}}_intercept_render' );
330
+ add_action( 'admin_init', '{{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_intercept_render' );
@@ -0,0 +1,22 @@
1
+ // Page-specific route registration functions for {{PAGE_SLUG}}
2
+ if ( ! function_exists( '{{PREFIX}}_register_{{PAGE_SLUG_UNDERSCORE}}_page_routes' ) ) {
3
+ /**
4
+ * Register routes for {{PAGE_SLUG}} page (full-page mode).
5
+ */
6
+ function {{PREFIX}}_register_{{PAGE_SLUG_UNDERSCORE}}_page_routes() {
7
+ global ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_routes_data;
8
+ {{PREFIX}}_register_page_routes( ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_routes_data, '{{PREFIX}}_register_{{PAGE_SLUG_UNDERSCORE}}_route' );
9
+ }
10
+ }
11
+ add_action( '{{PAGE_SLUG}}_init', '{{PREFIX}}_register_{{PAGE_SLUG_UNDERSCORE}}_page_routes' );
12
+
13
+ if ( ! function_exists( '{{PREFIX}}_register_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_page_routes' ) ) {
14
+ /**
15
+ * Register routes for {{PAGE_SLUG}} page (wp-admin mode).
16
+ */
17
+ function {{PREFIX}}_register_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_page_routes() {
18
+ global ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_routes_data;
19
+ {{PREFIX}}_register_page_routes( ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_routes_data, '{{PREFIX}}_register_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_route' );
20
+ }
21
+ }
22
+ add_action( '{{PAGE_SLUG}}-wp-admin_init', '{{PREFIX}}_register_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_page_routes' );
@@ -8,14 +8,14 @@
8
8
  */
9
9
 
10
10
  // Load routes registry
11
- $routes_file = __DIR__ . '/routes/index.php';
11
+ $routes_file = __DIR__ . '/routes/registry.php';
12
12
  if ( ! file_exists( $routes_file ) ) {
13
13
  return;
14
14
  }
15
15
 
16
16
  $routes = require $routes_file;
17
17
 
18
- // Group routes by page
18
+ // Group routes by page and store in globals for page-specific functions
19
19
  $routes_by_page = array();
20
20
  foreach ( $routes as $route ) {
21
21
  $page_slug = $route['page'];
@@ -25,11 +25,24 @@ foreach ( $routes as $route ) {
25
25
  $routes_by_page[ $page_slug ][] = $route;
26
26
  }
27
27
 
28
- // Helper function to register routes for a page
29
- $register_routes_callback = function ( $page_routes, $page_slug_underscore, $register_function_name ) {
30
- return function () use ( $page_routes, $page_slug_underscore, $register_function_name ) {
28
+ // Store routes data in globals for each page
29
+ foreach ( $routes_by_page as $page_slug => $page_routes ) {
30
+ $page_slug_underscore = str_replace( '-', '_', $page_slug );
31
+ $global_name = '{{PREFIX}}_' . $page_slug_underscore . '_routes_data';
32
+ $GLOBALS[ $global_name ] = $page_routes;
33
+ }
34
+
35
+ if ( ! function_exists( '{{PREFIX}}_register_page_routes' ) ) {
36
+ /**
37
+ * Generic helper function to register routes for a page.
38
+ *
39
+ * @param array $page_routes Array of route data for the page.
40
+ * @param string $register_function_name Name of the function to call for registering each route.
41
+ */
42
+ function {{PREFIX}}_register_page_routes( $page_routes, $register_function_name ) {
31
43
  // Load build constants
32
44
  $build_constants = require __DIR__ . '/constants.php';
45
+
33
46
  foreach ( $page_routes as $route ) {
34
47
  $content_handle = null;
35
48
  $route_handle = null;
@@ -71,22 +84,8 @@ $register_routes_callback = function ( $page_routes, $page_slug_underscore, $reg
71
84
  call_user_func( $register_function_name, $route['path'], $content_handle, $route_handle );
72
85
  }
73
86
  }
74
- };
75
- };
76
-
77
- // Register hooks for both full-page and wp-admin modes
78
- foreach ( $routes_by_page as $page_slug => $page_routes ) {
79
- $page_slug_underscore = str_replace( '-', '_', $page_slug );
80
-
81
- // Register all routes for full-page mode (page.php)
82
- add_action(
83
- "{$page_slug}_init",
84
- $register_routes_callback( $page_routes, $page_slug_underscore, "register_{$page_slug_underscore}_route" )
85
- );
86
-
87
- // Register all routes for wp-admin mode (page-wp-admin.php)
88
- add_action(
89
- "{$page_slug}-wp-admin_init",
90
- $register_routes_callback( $page_routes, $page_slug_underscore, "register_{$page_slug_underscore}_wp_admin_route" )
91
- );
87
+ }
92
88
  }
89
+
90
+ // Page-specific route registration functions
91
+ {{PAGE_ROUTE_FUNCTIONS}}
@@ -59,7 +59,7 @@ if ( ! function_exists( '{{PREFIX}}_register_package_scripts' ) ) {
59
59
  $extension = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '.js' : '.min.js';
60
60
 
61
61
  $scripts_dir = __DIR__ . '/scripts';
62
- $scripts_file = $scripts_dir . '/index.php';
62
+ $scripts_file = $scripts_dir . '/registry.php';
63
63
 
64
64
  if ( ! file_exists( $scripts_file ) ) {
65
65
  return;
@@ -49,7 +49,7 @@ if ( ! function_exists( '{{PREFIX}}_register_package_styles' ) ) {
49
49
  $suffix = SCRIPT_DEBUG ? '' : '.min';
50
50
 
51
51
  $styles_dir = __DIR__ . '/styles';
52
- $styles_file = $styles_dir . '/index.php';
52
+ $styles_file = $styles_dir . '/registry.php';
53
53
 
54
54
  if ( ! file_exists( $styles_file ) ) {
55
55
  return;