@wordpress/build 0.2.1-next.2f1c7c01b.0 → 0.3.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.
@@ -9,7 +9,6 @@ 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';
13
12
 
14
13
  const __dirname = path.dirname( fileURLToPath( import.meta.url ) );
15
14
 
@@ -41,133 +40,57 @@ export async function getPhpReplacements( rootDir ) {
41
40
  }
42
41
 
43
42
  /**
44
- * Generate a PHP file from a template with replacements.
43
+ * Apply template replacements to a template string.
45
44
  *
46
- * @param {string} templateName Template file name.
47
- * @param {string} outputPath Full output path.
45
+ * @param {string} template Template string with placeholders.
48
46
  * @param {Record<string, string>} replacements Replacements object (e.g. {'{{PREFIX}}': 'gutenberg'}).
47
+ * @return {string} Template with replacements applied.
49
48
  */
50
- export async function generatePhpFromTemplate(
51
- templateName,
52
- outputPath,
53
- replacements
54
- ) {
55
- // Templates directory
56
- const templatesDir = path.join( __dirname, '..', 'templates' );
57
-
58
- // Read template
59
- const template = await readFile(
60
- path.join( templatesDir, templateName ),
61
- 'utf8'
62
- );
63
-
64
- // Apply all replacements
49
+ export function applyTemplateReplacements( template, replacements ) {
65
50
  let content = template;
66
51
  for ( const [ placeholder, value ] of Object.entries( replacements ) ) {
67
52
  content = content.replaceAll( placeholder, value );
68
53
  }
69
-
70
- // Write output file
71
- await mkdir( path.dirname( outputPath ), { recursive: true } );
72
- await writeFile( outputPath, content );
54
+ return content;
73
55
  }
74
56
 
75
57
  /**
76
- * Generate routes/index.php file with route registry data.
58
+ * Render a template to a string with replacements.
77
59
  *
78
- * @param {string} rootDir Root directory path.
79
- * @param {string} buildDir Build directory path.
80
- * @param {string} prefix Package prefix.
60
+ * @param {string} templateName Template file name.
61
+ * @param {Record<string, string>} replacements Replacements object (e.g. {'{{PREFIX}}': 'gutenberg'}).
62
+ * @return {Promise<string>} Rendered template string.
81
63
  */
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
- };
64
+ export async function renderTemplateToString( templateName, replacements ) {
65
+ // Templates directory
66
+ const templatesDir = path.join( __dirname, '..', 'templates' );
130
67
 
131
- await generatePhpFromTemplate(
132
- 'route-registry.php.template',
133
- path.join( buildDir, 'routes', 'index.php' ),
134
- replacements
68
+ // Read template
69
+ const template = await readFile(
70
+ path.join( templatesDir, templateName ),
71
+ 'utf8'
135
72
  );
73
+
74
+ // Apply replacements
75
+ return applyTemplateReplacements( template, replacements );
136
76
  }
137
77
 
138
78
  /**
139
- * Generate routes.php file with route registration logic.
79
+ * Generate a PHP file from a template with replacements.
140
80
  *
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.
81
+ * @param {string} templateName Template file name.
82
+ * @param {string} outputPath Full output path.
83
+ * @param {Record<string, string>} replacements Replacements object (e.g. {'{{PREFIX}}': 'gutenberg'}).
145
84
  */
146
- export async function generateRoutesPhp(
147
- rootDir,
148
- buildDir,
149
- handlePrefix,
150
- prefix
85
+ export async function generatePhpFromTemplate(
86
+ templateName,
87
+ outputPath,
88
+ replacements
151
89
  ) {
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, '_' );
90
+ // Render template to string
91
+ const content = await renderTemplateToString( templateName, replacements );
160
92
 
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
- );
93
+ // Write output file
94
+ await mkdir( path.dirname( outputPath ), { recursive: true } );
95
+ await writeFile( outputPath, content );
173
96
  }
@@ -4,6 +4,11 @@
4
4
  import { readdirSync } from 'fs';
5
5
  import path from 'path';
6
6
 
7
+ /**
8
+ * Internal dependencies
9
+ */
10
+ import { getPackageInfoFromFile } from './package-utils.mjs';
11
+
7
12
  /**
8
13
  * Get all route names from the routes directory.
9
14
  *
@@ -23,6 +28,39 @@ export function getAllRoutes( rootDir ) {
23
28
  }
24
29
  }
25
30
 
31
+ /**
32
+ * @typedef {Object} RouteMetadata
33
+ * @property {string} name Route name.
34
+ * @property {string} path Route path.
35
+ * @property {string|null} page Page slug this route belongs to.
36
+ */
37
+
38
+ /**
39
+ * Get route metadata from package.json.
40
+ *
41
+ * @param {string} rootDir Root directory of the project.
42
+ * @param {string} routeName Route name.
43
+ * @return {RouteMetadata|null} Route metadata object or null if not found.
44
+ */
45
+ export function getRouteMetadata( rootDir, routeName ) {
46
+ const routePackageJson =
47
+ /** @type {import('./package-utils.mjs').RoutePackageJson|null} */ (
48
+ getPackageInfoFromFile(
49
+ path.join( rootDir, 'routes', routeName, 'package.json' )
50
+ )
51
+ );
52
+
53
+ if ( ! routePackageJson || ! routePackageJson.route ) {
54
+ return null;
55
+ }
56
+
57
+ return {
58
+ name: routeName,
59
+ path: routePackageJson.route.path,
60
+ page: routePackageJson.route.page || null,
61
+ };
62
+ }
63
+
26
64
  /**
27
65
  * @typedef {Object} RouteFiles
28
66
  * @property {boolean} hasRoute Whether route file exists.
@@ -35,3 +35,9 @@ $routes_file = __DIR__ . '/routes.php';
35
35
  if ( file_exists( $routes_file ) ) {
36
36
  require_once $routes_file;
37
37
  }
38
+
39
+ // Load pages registration.
40
+ $pages_file = __DIR__ . '/pages.php';
41
+ if ( file_exists( $pages_file ) ) {
42
+ require_once $pages_file;
43
+ }
@@ -0,0 +1,252 @@
1
+ <?php
2
+ /**
3
+ * Page: {{PAGE_SLUG}} (wp-admin integrated)
4
+ * Auto-generated by build process.
5
+ * Do not edit this file manually.
6
+ *
7
+ * This version integrates with the standard WordPress admin interface,
8
+ * keeping the wp-admin sidebar and scripts/styles intact.
9
+ *
10
+ * @package {{PREFIX}}
11
+ */
12
+
13
+ // Global storage for {{PAGE_SLUG}} routes and menu items
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();
16
+ ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_menu_items = array();
17
+
18
+ if ( ! function_exists( 'register_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_route' ) ) {
19
+ /**
20
+ * Register a route for the {{PAGE_SLUG}}-wp-admin page.
21
+ *
22
+ * @param string $path Route path (e.g., '/types/$type/edit/$id').
23
+ * @param string|null $content_module Script module ID for content (stage/inspector).
24
+ * @param string|null $route_module Script module ID for route lifecycle hooks.
25
+ */
26
+ function register_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_route( $path, $content_module = null, $route_module = null ) {
27
+ global ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_routes;
28
+
29
+ $route = array( 'path' => $path );
30
+ if ( ! empty( $content_module ) ) {
31
+ $route['content_module'] = $content_module;
32
+ }
33
+ if ( ! empty( $route_module ) ) {
34
+ $route['route_module'] = $route_module;
35
+ }
36
+
37
+ ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_routes[] = $route;
38
+ }
39
+ }
40
+
41
+ if ( ! function_exists( 'register_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_menu_item' ) ) {
42
+ /**
43
+ * Register a menu item for the {{PAGE_SLUG}}-wp-admin page.
44
+ * Note: Menu items are registered but not displayed in single-page mode.
45
+ *
46
+ * @param string $id Menu item ID.
47
+ * @param string $label Display label.
48
+ * @param string $to Route path to navigate to.
49
+ * @param string $parent_id Optional. Parent menu item ID.
50
+ */
51
+ function register_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_menu_item( $id, $label, $to, $parent_id = '' ) {
52
+ global ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_menu_items;
53
+
54
+ $menu_item = array(
55
+ 'id' => $id,
56
+ 'label' => $label,
57
+ 'to' => $to,
58
+ );
59
+
60
+ if ( ! empty( $parent_id ) ) {
61
+ $menu_item['parent'] = $parent_id;
62
+ }
63
+
64
+ ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_menu_items[] = $menu_item;
65
+ }
66
+ }
67
+
68
+ if ( ! function_exists( 'get_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_routes' ) ) {
69
+ /**
70
+ * Get all registered routes for the {{PAGE_SLUG}}-wp-admin page.
71
+ *
72
+ * @return array Array of route objects.
73
+ */
74
+ function get_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_routes() {
75
+ global ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_routes;
76
+ return ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_routes ?? array();
77
+ }
78
+ }
79
+
80
+ if ( ! function_exists( 'get_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_menu_items' ) ) {
81
+ /**
82
+ * Get all registered menu items for the {{PAGE_SLUG}}-wp-admin page.
83
+ *
84
+ * @return array Array of menu item objects.
85
+ */
86
+ function get_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_menu_items() {
87
+ global ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_menu_items;
88
+ return ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_menu_items ?? array();
89
+ }
90
+ }
91
+
92
+ if ( ! function_exists( '{{PAGE_SLUG_UNDERSCORE}}_wp_admin_preload_data' ) ) {
93
+ /**
94
+ * Preload REST API data for the {{PAGE_SLUG}}-wp-admin page.
95
+ * Automatically called during page rendering.
96
+ */
97
+ function {{PAGE_SLUG_UNDERSCORE}}_wp_admin_preload_data() {
98
+ // Define paths to preload - same for all pages
99
+ $preload_paths = array(
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',
101
+ array( '/wp/v2/settings', 'OPTIONS' ),
102
+ );
103
+
104
+ // Use rest_preload_api_request to gather the preloaded data
105
+ $preload_data = array_reduce(
106
+ $preload_paths,
107
+ 'rest_preload_api_request',
108
+ array()
109
+ );
110
+
111
+ // Register the preloading middleware with wp-api-fetch
112
+ wp_add_inline_script(
113
+ 'wp-api-fetch',
114
+ sprintf(
115
+ 'wp.apiFetch.use( wp.apiFetch.createPreloadingMiddleware( %s ) );',
116
+ wp_json_encode( $preload_data )
117
+ ),
118
+ 'after'
119
+ );
120
+ }
121
+ }
122
+
123
+ if ( ! function_exists( '{{PAGE_SLUG_UNDERSCORE}}_wp_admin_enqueue_scripts' ) ) {
124
+ /**
125
+ * Enqueue scripts and styles for the {{PAGE_SLUG}}-wp-admin page.
126
+ * Hooked to admin_enqueue_scripts.
127
+ *
128
+ * @param string $hook_suffix The current admin page.
129
+ */
130
+ function {{PAGE_SLUG_UNDERSCORE}}_wp_admin_enqueue_scripts( $hook_suffix ) {
131
+ // Only enqueue on our page
132
+ if ( ! isset( $_GET['page'] ) || '{{PAGE_SLUG}}-wp-admin' !== $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
133
+ return;
134
+ }
135
+
136
+ // Fire init action for extensions to register routes and menu items
137
+ do_action( '{{PAGE_SLUG}}-wp-admin_init' );
138
+
139
+ // Preload REST API data
140
+ {{PAGE_SLUG_UNDERSCORE}}_wp_admin_preload_data();
141
+
142
+ // Get all registered routes
143
+ $routes = get_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_routes();
144
+
145
+ // Get boot module asset file for dependencies
146
+ $asset_file = plugin_dir_path( __FILE__ ) . '../../modules/boot/index.min.asset.php';
147
+ if ( file_exists( $asset_file ) ) {
148
+ $asset = require $asset_file;
149
+
150
+ // This script serves two purposes:
151
+ // 1. It ensures all the globals that are made available to the modules are loaded.
152
+ // 2. It initializes the boot module as an inline script.
153
+ wp_register_script( '{{PAGE_SLUG}}-wp-admin-prerequisites', '', $asset['dependencies'], $asset['version'], true );
154
+
155
+ // Add inline script to initialize the app using initSinglePage (no menuItems)
156
+ wp_add_inline_script(
157
+ '{{PAGE_SLUG}}-wp-admin-prerequisites',
158
+ sprintf(
159
+ 'import("@wordpress/boot").then(mod => mod.initSinglePage({mountId: "%s", routes: %s}));',
160
+ '{{PAGE_SLUG}}-wp-admin-app',
161
+ wp_json_encode( $routes, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES )
162
+ )
163
+ );
164
+
165
+ // Register prerequisites style by filtering script dependencies to find registered styles
166
+ $style_dependencies = array_filter(
167
+ $asset['dependencies'],
168
+ function ( $handle ) {
169
+ return wp_style_is( $handle, 'registered' );
170
+ }
171
+ );
172
+ wp_register_style( '{{PAGE_SLUG}}-wp-admin-prerequisites', false, $style_dependencies, $asset['version'] );
173
+
174
+ // Build dependencies for {{PAGE_SLUG}}-wp-admin module
175
+ $boot_dependencies = array(
176
+ array(
177
+ 'import' => 'static',
178
+ 'id' => '@wordpress/boot',
179
+ ),
180
+ );
181
+
182
+ // Add all registered routes as dependencies
183
+ foreach ( $routes as $route ) {
184
+ if ( isset( $route['route_module'] ) ) {
185
+ $boot_dependencies[] = array(
186
+ 'import' => 'static',
187
+ 'id' => $route['route_module'],
188
+ );
189
+ }
190
+ if ( isset( $route['content_module'] ) ) {
191
+ $boot_dependencies[] = array(
192
+ 'import' => 'dynamic',
193
+ 'id' => $route['content_module'],
194
+ );
195
+ }
196
+ }
197
+
198
+ // Dummy script module to ensure dependencies are loaded
199
+ wp_register_script_module(
200
+ '{{PAGE_SLUG}}-wp-admin',
201
+ plugin_dir_url( __FILE__ ) . 'loader.js',
202
+ $boot_dependencies
203
+ );
204
+
205
+ // Enqueue the boot scripts and styles
206
+ wp_enqueue_script( '{{PAGE_SLUG}}-wp-admin-prerequisites' );
207
+ wp_enqueue_script_module( '{{PAGE_SLUG}}-wp-admin' );
208
+ wp_enqueue_style( '{{PAGE_SLUG}}-wp-admin-prerequisites' );
209
+ }
210
+ }
211
+ }
212
+
213
+ if ( ! function_exists( '{{PAGE_SLUG_UNDERSCORE}}_wp_admin_render_page' ) ) {
214
+ /**
215
+ * Render the {{PAGE_SLUG}}-wp-admin page.
216
+ * Call this function from add_menu_page or add_submenu_page.
217
+ * This renders within the normal WordPress admin interface.
218
+ */
219
+ function {{PAGE_SLUG_UNDERSCORE}}_wp_admin_render_page() {
220
+ ?>
221
+ <div id="{{PAGE_SLUG}}-wp-admin-app" class="boot-layout-container"></div>
222
+ <?php
223
+ }
224
+ }
225
+
226
+ // Hook the enqueue function to admin_enqueue_scripts
227
+ add_action( 'admin_enqueue_scripts', '{{PAGE_SLUG_UNDERSCORE}}_wp_admin_enqueue_scripts' );
228
+
229
+ if ( ! function_exists( '{{PAGE_SLUG_UNDERSCORE}}_wp_admin_register_page' ) ) {
230
+ /**
231
+ * Register the {{PAGE_SLUG}}-wp-admin base page handler.
232
+ * This is hidden from the menu but provides the page callback for URL routing.
233
+ * Visible menu items should use the full URL pattern with query parameters.
234
+ *
235
+ * Example:
236
+ * $url = admin_url( 'admin.php?page={{PAGE_SLUG}}-wp-admin&p=' . urlencode( '/your/route' ) );
237
+ * add_menu_page( 'Title', 'Menu', 'capability', $url, '', 'icon', 10 );
238
+ */
239
+ function {{PAGE_SLUG_UNDERSCORE}}_wp_admin_register_page() {
240
+ add_submenu_page(
241
+ 'nothing-{{PAGE_SLUG}}', // Hidden page (no parent)
242
+ __( '{{PAGE_SLUG}} (WP Admin)', '{{PREFIX}}' ), // Page title
243
+ __( '{{PAGE_SLUG}} (WP Admin)', '{{PREFIX}}' ), // Menu title (not visible)
244
+ 'read', // Minimum capability
245
+ '{{PAGE_SLUG}}-wp-admin', // Menu slug for URL routing
246
+ '{{PAGE_SLUG_UNDERSCORE}}_wp_admin_render_page' // Callback function
247
+ );
248
+ }
249
+ }
250
+
251
+ // Hook the registration to admin_menu
252
+ add_action( 'admin_menu', '{{PAGE_SLUG_UNDERSCORE}}_wp_admin_register_page' );