@10up/wp-vite-plugins 1.0.0-alpha.3 → 1.0.0-alpha.4
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/LICENSE +338 -0
- package/README.md +84 -58
- package/dist/index.d.ts +400 -366
- package/dist/index.js +4290 -4020
- package/dist/php/wp-boot/Page.php +803 -0
- package/dist/php/wp-boot/Pages.php +133 -0
- package/dist/php/wp-vite-dev-server/Bridge.php +507 -0
- package/dist/php/wp-vite-dev-server/HotFile.php +95 -0
- package/package.json +19 -51
- package/dist/build.d.ts +0 -13
- package/dist/build.js +0 -413
- package/dist/build.js.map +0 -1
- package/dist/cli.js +0 -628
- package/dist/cli.js.map +0 -1
- package/dist/dev.d.ts +0 -35
- package/dist/dev.js +0 -384
- package/dist/dev.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/runner-jUVlAHlZ.d.ts +0 -183
- package/dist/watch.d.ts +0 -12
- package/dist/watch.js +0 -398
- package/dist/watch.js.map +0 -1
|
@@ -0,0 +1,803 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
/**
|
|
3
|
+
* Page class.
|
|
4
|
+
*
|
|
5
|
+
* @package TenUp\WpBoot
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
declare( strict_types = 1 );
|
|
9
|
+
|
|
10
|
+
namespace TenUp\WpBoot;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* One `@wordpress/boot` admin page in one mode.
|
|
14
|
+
*
|
|
15
|
+
* - 'full-page' takes over the screen: it intercepts `admin_init` for
|
|
16
|
+
* `?page=<slug>`, prints its own HTML document and boots with `init()`.
|
|
17
|
+
* - 'wp-admin' renders inside the regular admin chrome on
|
|
18
|
+
* `?page=<slug>-wp-admin` and boots with `initSinglePage()`.
|
|
19
|
+
*
|
|
20
|
+
* Both fire `<handle>_init` (register routes and menu items there) and filter
|
|
21
|
+
* `<handle>_boot_dependencies`, the names `@wordpress/build` uses.
|
|
22
|
+
*
|
|
23
|
+
* Ported from `@wordpress/build`'s page.php.template (full-page) and
|
|
24
|
+
* page-wp-admin.php.template (wp-admin), the original implementation; each
|
|
25
|
+
* method notes the part it comes from. See the README's "Origin".
|
|
26
|
+
*
|
|
27
|
+
* @link https://github.com/WordPress/gutenberg/blob/trunk/packages/wp-build/templates/page.php.template
|
|
28
|
+
* @link https://github.com/WordPress/gutenberg/blob/trunk/packages/wp-build/templates/page-wp-admin.php.template
|
|
29
|
+
*
|
|
30
|
+
* @phpstan-import-type PageConfig from Pages
|
|
31
|
+
*/
|
|
32
|
+
final class Page {
|
|
33
|
+
|
|
34
|
+
/** Full-page takeover mode. */
|
|
35
|
+
public const FULL_PAGE = 'full-page';
|
|
36
|
+
|
|
37
|
+
/** wp-admin integrated mode. */
|
|
38
|
+
public const WP_ADMIN = 'wp-admin';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The site-entity request core-data makes on load, by the first WordPress
|
|
42
|
+
* version that asks for it. Its `_fields` must match the running
|
|
43
|
+
* core-data's exactly, same fields in the same order, or the preloaded
|
|
44
|
+
* response is never used.
|
|
45
|
+
*
|
|
46
|
+
* @var array<string, string>
|
|
47
|
+
*/
|
|
48
|
+
private const SITE_PRELOAD_PATHS = array(
|
|
49
|
+
'7.2-alpha' => '/?_fields=description,generate_animated_image_subsizes,gmt_offset,home,image_max_bit_depth,image_sizes,image_size_threshold,image_strip_meta,name,site_icon,site_icon_url,site_logo,timezone_string,url,page_for_posts,page_on_front,show_on_front',
|
|
50
|
+
'7.1-alpha' => '/?_fields=description,gmt_offset,home,image_max_bit_depth,image_sizes,image_size_threshold,image_strip_meta,name,site_icon,site_icon_url,site_logo,timezone_string,url,page_for_posts,page_on_front,show_on_front',
|
|
51
|
+
'7.0-alpha' => '/?_fields=description,gmt_offset,home,image_sizes,image_size_threshold,image_output_formats,jpeg_interlaced,png_interlaced,gif_interlaced,name,site_icon,site_icon_url,site_logo,timezone_string,url,page_for_posts,page_on_front,show_on_front',
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Script handle, Script Module id and hook prefix: `<slug>` or `<slug>-wp-admin`.
|
|
56
|
+
*
|
|
57
|
+
* @var string
|
|
58
|
+
*/
|
|
59
|
+
public readonly string $handle;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Registered routes, handed to `@wordpress/boot`.
|
|
63
|
+
*
|
|
64
|
+
* @var list<array{path: string, content_module?: string, route_module?: string}>
|
|
65
|
+
*/
|
|
66
|
+
private array $routes = array();
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Registered menu items, handed to `@wordpress/boot` in full-page mode.
|
|
70
|
+
*
|
|
71
|
+
* @var list<array{id: string, label: string, to: string, parent?: string, parent_type?: string}>
|
|
72
|
+
*/
|
|
73
|
+
private array $menu_items = array();
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Create a page.
|
|
77
|
+
*
|
|
78
|
+
* @param Pages $build The build the page belongs to.
|
|
79
|
+
* @param array $config The page, as the build wrote it.
|
|
80
|
+
* @param string $mode Page::FULL_PAGE or Page::WP_ADMIN.
|
|
81
|
+
*
|
|
82
|
+
* @phpstan-param PageConfig $config
|
|
83
|
+
*/
|
|
84
|
+
public function __construct(
|
|
85
|
+
private readonly Pages $build,
|
|
86
|
+
private readonly array $config,
|
|
87
|
+
private readonly string $mode,
|
|
88
|
+
) {
|
|
89
|
+
$this->handle = self::WP_ADMIN === $mode ? $config['slug'] . '-wp-admin' : $config['slug'];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Hook the page into WordPress.
|
|
94
|
+
*
|
|
95
|
+
* @param bool $owns_menu Whether this page registers the admin menu entry.
|
|
96
|
+
* @return void
|
|
97
|
+
*/
|
|
98
|
+
public function register_hooks( bool $owns_menu ): void {
|
|
99
|
+
add_action( $this->handle . '_init', array( $this, 'register_route_modules' ) );
|
|
100
|
+
|
|
101
|
+
if ( self::WP_ADMIN === $this->mode ) {
|
|
102
|
+
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
|
|
103
|
+
} else {
|
|
104
|
+
add_action( 'admin_init', array( $this, 'intercept_render' ) );
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if ( $owns_menu ) {
|
|
108
|
+
add_action( 'admin_menu', array( $this, 'register_menu' ) );
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Register a route.
|
|
114
|
+
*
|
|
115
|
+
* @param string $path Route path, e.g. '/types/$type/edit/$id'.
|
|
116
|
+
* @param string|null $content_module Script Module id of the route's content (stage/inspector).
|
|
117
|
+
* @param string|null $route_module Script Module id of the route's lifecycle hooks.
|
|
118
|
+
* @return void
|
|
119
|
+
*/
|
|
120
|
+
public function register_route( string $path, ?string $content_module = null, ?string $route_module = null ): void {
|
|
121
|
+
$route = array( 'path' => $path );
|
|
122
|
+
|
|
123
|
+
if ( ! empty( $content_module ) ) {
|
|
124
|
+
$route['content_module'] = $content_module;
|
|
125
|
+
}
|
|
126
|
+
if ( ! empty( $route_module ) ) {
|
|
127
|
+
$route['route_module'] = $route_module;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
$this->routes[] = $route;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Register a menu item.
|
|
135
|
+
*
|
|
136
|
+
* @param string $id Menu item id.
|
|
137
|
+
* @param string $label Display label.
|
|
138
|
+
* @param string $to Route path to navigate to.
|
|
139
|
+
* @param string $parent_id Parent menu item id.
|
|
140
|
+
* @param string $parent_type 'drilldown' or 'dropdown'. Full-page mode only.
|
|
141
|
+
* @return void
|
|
142
|
+
*/
|
|
143
|
+
public function register_menu_item( string $id, string $label, string $to, string $parent_id = '', string $parent_type = '' ): void {
|
|
144
|
+
$menu_item = array(
|
|
145
|
+
'id' => $id,
|
|
146
|
+
'label' => $label,
|
|
147
|
+
'to' => $to,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
if ( ! empty( $parent_id ) ) {
|
|
151
|
+
$menu_item['parent'] = $parent_id;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if ( self::FULL_PAGE === $this->mode && in_array( $parent_type, array( 'drilldown', 'dropdown' ), true ) ) {
|
|
155
|
+
$menu_item['parent_type'] = $parent_type;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
$this->menu_items[] = $menu_item;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The registered routes.
|
|
163
|
+
*
|
|
164
|
+
* @return list<array{path: string, content_module?: string, route_module?: string}>
|
|
165
|
+
*/
|
|
166
|
+
public function routes(): array {
|
|
167
|
+
return $this->routes;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The registered menu items.
|
|
172
|
+
*
|
|
173
|
+
* @return list<array{id: string, label: string, to: string, parent?: string, parent_type?: string}>
|
|
174
|
+
*/
|
|
175
|
+
public function menu_items(): array {
|
|
176
|
+
return $this->menu_items;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Register the Script Modules (and stylesheet) of every route the build
|
|
181
|
+
* produced, then register the routes with the page. Runs on
|
|
182
|
+
* `<handle>_init`.
|
|
183
|
+
*
|
|
184
|
+
* From routes-registration.php.template's `<prefix>_register_page_routes()`; the stylesheet is ours.
|
|
185
|
+
*
|
|
186
|
+
* Route modules are `<handle_prefix>/routes/<slug>/<route>/{content,route}`.
|
|
187
|
+
* Each is deregistered first, so a plugin can override a module core
|
|
188
|
+
* registered under the same id.
|
|
189
|
+
*
|
|
190
|
+
* @return void
|
|
191
|
+
*/
|
|
192
|
+
public function register_route_modules(): void {
|
|
193
|
+
foreach ( $this->config['routes'] as $route ) {
|
|
194
|
+
$content_module = null;
|
|
195
|
+
$route_module = null;
|
|
196
|
+
$module_prefix = $this->build->handle_prefix . '/routes/' . $this->config['slug'] . '/' . $route['name'];
|
|
197
|
+
|
|
198
|
+
if ( $route['has_content'] ) {
|
|
199
|
+
$asset = $this->asset( $route['dir'] . '/content.asset.php' );
|
|
200
|
+
|
|
201
|
+
if ( null !== $asset ) {
|
|
202
|
+
$content_module = $module_prefix . '/content';
|
|
203
|
+
$this->register_module( $content_module, $route['dir'] . '/content.js', $asset );
|
|
204
|
+
|
|
205
|
+
// The page stylesheet, when the entry imported any CSS.
|
|
206
|
+
if ( file_exists( $this->build->path( $route['dir'] . '/content.css' ) ) ) {
|
|
207
|
+
$style_handle = str_replace( '/', '-', $content_module );
|
|
208
|
+
wp_register_style( $style_handle, $this->build->url( $route['dir'] . '/content.css' ), array(), $asset['version'] ?? false );
|
|
209
|
+
wp_enqueue_style( $style_handle );
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if ( $route['has_route'] ) {
|
|
215
|
+
$asset = $this->asset( $route['dir'] . '/route.asset.php' );
|
|
216
|
+
|
|
217
|
+
if ( null !== $asset ) {
|
|
218
|
+
$route_module = $module_prefix . '/route';
|
|
219
|
+
$this->register_module( $route_module, $route['dir'] . '/route.js', $asset );
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
$this->register_route( $route['path'], $content_module, $route_module );
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Preload the page's REST paths into `wp.apiFetch`: the page's
|
|
229
|
+
* `restPreload`, else what core's own pages preload.
|
|
230
|
+
*
|
|
231
|
+
* From both templates' `<prefix>_<slug>_preload_data()`; the filter is page-wp-admin.php.template's, here in both modes.
|
|
232
|
+
*
|
|
233
|
+
* @return void
|
|
234
|
+
*/
|
|
235
|
+
public function preload_data(): void {
|
|
236
|
+
$paths = $this->config['rest_preload'] ?? array( self::site_preload_path(), array( '/wp/v2/settings', 'OPTIONS' ) );
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Filters the REST API paths preloaded for this page, the name
|
|
240
|
+
* `@wordpress/build` uses: `<prefix>_<slug>[_wp_admin]_preload_paths`.
|
|
241
|
+
*
|
|
242
|
+
* Each entry is a path (GET) or a `[ path, method ]` pair.
|
|
243
|
+
*
|
|
244
|
+
* @param array $paths Paths to preload.
|
|
245
|
+
*/
|
|
246
|
+
$paths = apply_filters( str_replace( '-', '_', $this->build->prefix . '_' . $this->handle ) . '_preload_paths', $paths );
|
|
247
|
+
|
|
248
|
+
$preload_data = array();
|
|
249
|
+
foreach ( $paths as $path ) {
|
|
250
|
+
// Core also takes array( $path, $method ), though its docblock says string.
|
|
251
|
+
$preload_data = rest_preload_api_request( $preload_data, $path );
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
wp_add_inline_script(
|
|
255
|
+
'wp-api-fetch',
|
|
256
|
+
sprintf(
|
|
257
|
+
'wp.apiFetch.use( wp.apiFetch.createPreloadingMiddleware( %s ) );',
|
|
258
|
+
wp_json_encode( $preload_data )
|
|
259
|
+
),
|
|
260
|
+
'after'
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* The site-entity preload path for the running WordPress version.
|
|
266
|
+
*
|
|
267
|
+
* From the `_fields` path both templates hardcode, one per WordPress version.
|
|
268
|
+
*
|
|
269
|
+
* With the Gutenberg plugin active, core-data comes from the plugin and
|
|
270
|
+
* may ask for other fields; pages can set `restPreload` for that.
|
|
271
|
+
*
|
|
272
|
+
* @return string
|
|
273
|
+
*/
|
|
274
|
+
public static function site_preload_path(): string {
|
|
275
|
+
$version = get_bloginfo( 'version' );
|
|
276
|
+
|
|
277
|
+
foreach ( self::SITE_PRELOAD_PATHS as $since => $path ) {
|
|
278
|
+
if ( version_compare( $version, $since, '>=' ) ) {
|
|
279
|
+
return $path;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return self::SITE_PRELOAD_PATHS['7.0-alpha'];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Boot a wp-admin page on its own screen. Runs on `admin_enqueue_scripts`.
|
|
288
|
+
*
|
|
289
|
+
* From page-wp-admin.php.template's `<prefix>_<slug>_enqueue_scripts()`.
|
|
290
|
+
*
|
|
291
|
+
* @return void
|
|
292
|
+
*/
|
|
293
|
+
public function enqueue_scripts(): void {
|
|
294
|
+
$screen = get_current_screen();
|
|
295
|
+
|
|
296
|
+
$is_our_page = (
|
|
297
|
+
( isset( $_GET['page'] ) && $this->handle === $_GET['page'] ) || // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
|
298
|
+
( $screen && $this->config['slug'] === $screen->id )
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
if ( $is_our_page ) {
|
|
302
|
+
$this->enqueue_boot();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Render a full-page page early, before WordPress prints its own admin
|
|
308
|
+
* template. Runs on `admin_init`.
|
|
309
|
+
*
|
|
310
|
+
* From page.php.template's `<prefix>_<slug>_intercept_render()`.
|
|
311
|
+
*
|
|
312
|
+
* This bypasses the menu callback's own checks, and `admin_init` also
|
|
313
|
+
* fires for logged-out requests (admin-post.php), so it checks login and
|
|
314
|
+
* the page's capability itself.
|
|
315
|
+
*
|
|
316
|
+
* @return void
|
|
317
|
+
*/
|
|
318
|
+
public function intercept_render(): void {
|
|
319
|
+
if ( ! isset( $_GET['page'] ) || $this->handle !== $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if ( ! is_user_logged_in() ) {
|
|
324
|
+
auth_redirect();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if ( ! current_user_can( $this->config['capability'] ) ) {
|
|
328
|
+
// Core's own string, so core's translation applies.
|
|
329
|
+
wp_die( esc_html__( 'Sorry, you are not allowed to access this page.' ), 403 ); // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
$this->render_page();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* The page's render callback: what the admin menu entry points at.
|
|
337
|
+
*
|
|
338
|
+
* Full-page mode prints a whole HTML document and exits.
|
|
339
|
+
*
|
|
340
|
+
* @return void
|
|
341
|
+
*/
|
|
342
|
+
public function render_page(): void {
|
|
343
|
+
if ( self::WP_ADMIN === $this->mode ) {
|
|
344
|
+
$this->render_wp_admin();
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
$this->render_document();
|
|
349
|
+
exit;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Register the page's admin menu entry. Runs on `admin_menu`.
|
|
354
|
+
*
|
|
355
|
+
* No `parent` (or 'options-general.php') adds it under Settings, `false`
|
|
356
|
+
* or 'toplevel' adds a top-level entry, anything else is a submenu.
|
|
357
|
+
*
|
|
358
|
+
* @return void
|
|
359
|
+
*/
|
|
360
|
+
public function register_menu(): void {
|
|
361
|
+
if ( ! isset( $this->config['menu'] ) ) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
$menu = $this->config['menu'];
|
|
366
|
+
$title = ( $menu['title'] )();
|
|
367
|
+
$capability = $menu['capability'] ?? $this->config['capability'];
|
|
368
|
+
$parent = $menu['parent'] ?? 'options-general.php';
|
|
369
|
+
$callback = array( $this, 'render_page' );
|
|
370
|
+
|
|
371
|
+
if ( false === $parent || 'toplevel' === $parent ) {
|
|
372
|
+
add_menu_page( $title, $title, $capability, $this->handle, $callback, $menu['icon'] ?? 'dashicons-admin-generic', $menu['position'] ?? null );
|
|
373
|
+
} elseif ( 'options-general.php' === $parent ) {
|
|
374
|
+
add_options_page( $title, $title, $capability, $this->handle, $callback );
|
|
375
|
+
} else {
|
|
376
|
+
add_submenu_page( $parent, $title, $title, $capability, $this->handle, $callback );
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Print the full-page document: WordPress's admin head and footer
|
|
382
|
+
* printing around an empty mount point.
|
|
383
|
+
*
|
|
384
|
+
* From page.php.template's `<prefix>_<slug>_render_page()`.
|
|
385
|
+
*
|
|
386
|
+
* @return void
|
|
387
|
+
*/
|
|
388
|
+
public function render_document(): void {
|
|
389
|
+
set_current_screen();
|
|
390
|
+
|
|
391
|
+
remove_action( 'admin_head', 'wp_admin_bar_header' );
|
|
392
|
+
|
|
393
|
+
// Drop whatever `admin_init` enqueued.
|
|
394
|
+
foreach ( wp_scripts()->queue as $script ) {
|
|
395
|
+
wp_dequeue_script( $script );
|
|
396
|
+
}
|
|
397
|
+
foreach ( wp_styles()->queue as $style ) {
|
|
398
|
+
wp_dequeue_style( $style );
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
$this->enqueue_boot();
|
|
402
|
+
|
|
403
|
+
?>
|
|
404
|
+
<!DOCTYPE html>
|
|
405
|
+
<html <?php language_attributes(); ?>>
|
|
406
|
+
<head>
|
|
407
|
+
<meta charset="<?php bloginfo( 'charset' ); ?>">
|
|
408
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
409
|
+
<title><?php echo esc_html( get_admin_page_title() ); ?></title>
|
|
410
|
+
<style>
|
|
411
|
+
html {
|
|
412
|
+
background: #f1f1f1;
|
|
413
|
+
color: #444;
|
|
414
|
+
font-family: -apple-system, system-ui, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
|
415
|
+
font-size: 13px;
|
|
416
|
+
line-height: 1.4em;
|
|
417
|
+
}
|
|
418
|
+
body {
|
|
419
|
+
margin: 0;
|
|
420
|
+
}
|
|
421
|
+
#wpadminbar { display: none; }
|
|
422
|
+
/* wp-admin's common.css, which this document doesn't load, has this rule. */
|
|
423
|
+
.js .hide-if-js { display: none; }
|
|
424
|
+
</style>
|
|
425
|
+
<?php
|
|
426
|
+
global $hook_suffix;
|
|
427
|
+
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
|
|
428
|
+
$hook_suffix = $this->handle;
|
|
429
|
+
|
|
430
|
+
// BEGIN see wp-admin/admin-header.php.
|
|
431
|
+
print_admin_styles();
|
|
432
|
+
print_head_scripts();
|
|
433
|
+
|
|
434
|
+
/** This action is documented in wp-admin/admin-header.php */
|
|
435
|
+
do_action( "admin_head-{$hook_suffix}" ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
|
|
436
|
+
|
|
437
|
+
/** This action is documented in wp-admin/admin-header.php */
|
|
438
|
+
do_action( 'admin_head' );
|
|
439
|
+
// END see wp-admin/admin-header.php.
|
|
440
|
+
?>
|
|
441
|
+
</head>
|
|
442
|
+
<body class="<?php echo esc_attr( $this->handle ); ?> no-js">
|
|
443
|
+
<?php // See wp-admin/admin-header.php. ?>
|
|
444
|
+
<script type="text/javascript">
|
|
445
|
+
document.body.className = document.body.className.replace( 'no-js', 'js' );
|
|
446
|
+
</script>
|
|
447
|
+
<?php $this->render_no_js_notice( 'margin: 20px;' ); ?>
|
|
448
|
+
<div id="<?php echo esc_attr( $this->mount_id() ); ?>" style="height: 100vh; box-sizing: border-box;"></div>
|
|
449
|
+
<?php
|
|
450
|
+
// BEGIN see wp-admin/admin-footer.php.
|
|
451
|
+
|
|
452
|
+
/** This action is documented in wp-admin/admin-footer.php */
|
|
453
|
+
do_action( 'admin_footer', $hook_suffix );
|
|
454
|
+
|
|
455
|
+
// Print the import map first so it's available to inline scripts.
|
|
456
|
+
wp_script_modules()->print_import_map();
|
|
457
|
+
print_footer_scripts();
|
|
458
|
+
wp_script_modules()->print_enqueued_script_modules();
|
|
459
|
+
wp_script_modules()->print_script_module_preloads();
|
|
460
|
+
wp_script_modules()->print_script_module_data();
|
|
461
|
+
|
|
462
|
+
/** This action is documented in wp-admin/admin-footer.php */
|
|
463
|
+
do_action( "admin_footer-{$hook_suffix}" ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
|
|
464
|
+
// END see wp-admin/admin-footer.php.
|
|
465
|
+
?>
|
|
466
|
+
</body>
|
|
467
|
+
</html>
|
|
468
|
+
<?php
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Print the wp-admin mount point, with the critical styles that keep the
|
|
473
|
+
* layout from shifting.
|
|
474
|
+
*
|
|
475
|
+
* From page-wp-admin.php.template's `<prefix>_<slug>_render_page()`.
|
|
476
|
+
*
|
|
477
|
+
* @return void
|
|
478
|
+
*/
|
|
479
|
+
private function render_wp_admin(): void {
|
|
480
|
+
?>
|
|
481
|
+
<style>
|
|
482
|
+
/* Critical styles to prevent layout shifts - inlined for immediate application */
|
|
483
|
+
|
|
484
|
+
#wpwrap {
|
|
485
|
+
overflow-y: auto;
|
|
486
|
+
}
|
|
487
|
+
body.js {
|
|
488
|
+
background: #fff;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/* Reset wp-admin padding */
|
|
492
|
+
body.js #wpcontent {
|
|
493
|
+
padding-inline-start: 0;
|
|
494
|
+
}
|
|
495
|
+
body.js #wpbody-content {
|
|
496
|
+
padding-bottom: 0;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/* Hide legacy admin elements */
|
|
500
|
+
body.js #wpbody-content > div:not(#<?php echo esc_attr( $this->mount_id() ); ?>):not(#screen-meta) {
|
|
501
|
+
display: none;
|
|
502
|
+
}
|
|
503
|
+
body.js #wpfooter {
|
|
504
|
+
display: none;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/* Accessibility regions */
|
|
508
|
+
.a11y-speak-region {
|
|
509
|
+
inset-inline-start: -1px;
|
|
510
|
+
top: -1px;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/* Admin menu indicators */
|
|
514
|
+
ul#adminmenu a.wp-has-current-submenu::after,
|
|
515
|
+
ul#adminmenu > li.current > a.current::after {
|
|
516
|
+
border-inline-end-color: #fff;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/* Media frame fix */
|
|
520
|
+
.media-frame select.attachment-filters:last-of-type {
|
|
521
|
+
width: auto;
|
|
522
|
+
max-width: 100%;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/* Responsive overflow fix for #wpwrap */
|
|
526
|
+
@media (min-width: 782px) {
|
|
527
|
+
#wpwrap {
|
|
528
|
+
overflow-y: initial;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
</style>
|
|
532
|
+
<?php
|
|
533
|
+
$this->render_no_js_notice();
|
|
534
|
+
// Boot's layout before its CSS Modules (Gutenberg #81756) sizes the
|
|
535
|
+
// viewport by this class; drop it once WordPress 7.2 is the minimum.
|
|
536
|
+
?>
|
|
537
|
+
<div id="<?php echo esc_attr( $this->mount_id() ); ?>" class="boot-layout-container"></div>
|
|
538
|
+
<?php
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Print what the page shows without JavaScript: its title and an error
|
|
543
|
+
* notice. `hide-if-js` hides it once the page boots.
|
|
544
|
+
*
|
|
545
|
+
* From the `wrap hide-if-js` block of both templates.
|
|
546
|
+
*
|
|
547
|
+
* @param string $style Inline style for the wrapper.
|
|
548
|
+
* @return void
|
|
549
|
+
*/
|
|
550
|
+
private function render_no_js_notice( string $style = '' ): void {
|
|
551
|
+
?>
|
|
552
|
+
<div class="wrap hide-if-js"<?php echo $style ? ' style="' . esc_attr( $style ) . '"' : ''; ?>>
|
|
553
|
+
<h1 class="wp-heading-inline"><?php echo esc_html( get_admin_page_title() ); ?></h1>
|
|
554
|
+
<?php
|
|
555
|
+
wp_admin_notice(
|
|
556
|
+
// Core's own string, so core's translation applies.
|
|
557
|
+
esc_html__( 'This screen requires JavaScript. Enable JavaScript in your browser settings and reload the page.' ), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
|
|
558
|
+
array( 'type' => 'error' )
|
|
559
|
+
);
|
|
560
|
+
?>
|
|
561
|
+
</div>
|
|
562
|
+
<?php
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Fire `<handle>_init`, then enqueue everything that boots the page.
|
|
567
|
+
*
|
|
568
|
+
* From the enqueue half of both templates' render/enqueue functions.
|
|
569
|
+
*
|
|
570
|
+
* The prerequisites carrier is an empty-src classic script depending on
|
|
571
|
+
* every classic handle the page's modules read off `window.*` (from the
|
|
572
|
+
* routes' `.asset.php` sidecars and core's boot module), so those exist by
|
|
573
|
+
* the time the modules run. It also hosts the inline bootstrap. The boot
|
|
574
|
+
* graph hangs off the `loader.js` anchor module. Nothing is enqueued when
|
|
575
|
+
* the build left no sidecars.
|
|
576
|
+
*
|
|
577
|
+
* @return void
|
|
578
|
+
*/
|
|
579
|
+
private function enqueue_boot(): void {
|
|
580
|
+
/**
|
|
581
|
+
* Fires when the page initializes, so extensions can register routes
|
|
582
|
+
* and menu items.
|
|
583
|
+
*/
|
|
584
|
+
do_action( $this->handle . '_init' );
|
|
585
|
+
|
|
586
|
+
if ( self::FULL_PAGE === $this->mode && function_exists( 'wp_enqueue_command_palette_assets' ) ) {
|
|
587
|
+
wp_enqueue_command_palette_assets();
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
$this->preload_data();
|
|
591
|
+
|
|
592
|
+
$script_dependencies = array();
|
|
593
|
+
$version = false;
|
|
594
|
+
$has_assets = false;
|
|
595
|
+
|
|
596
|
+
foreach ( $this->asset_files() as $asset_file ) {
|
|
597
|
+
$asset = $this->asset( $asset_file );
|
|
598
|
+
|
|
599
|
+
if ( null === $asset ) {
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
$has_assets = true;
|
|
604
|
+
$script_dependencies = array_merge( $script_dependencies, $asset['dependencies'] ?? array() );
|
|
605
|
+
|
|
606
|
+
if ( false === $version && ! empty( $asset['version'] ) ) {
|
|
607
|
+
$version = $asset['version'];
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if ( ! $has_assets ) {
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Core's boot module carries classic dependencies of its own.
|
|
616
|
+
$boot_asset_file = ABSPATH . WPINC . '/js/dist/script-modules/boot/index.min.asset.php';
|
|
617
|
+
if ( file_exists( $boot_asset_file ) ) {
|
|
618
|
+
$boot_asset = require $boot_asset_file;
|
|
619
|
+
$script_dependencies = array_merge( $script_dependencies, $boot_asset['dependencies'] ?? array() );
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
$script_dependencies = array_values( array_unique( $script_dependencies ) );
|
|
623
|
+
$carrier = $this->handle . '-prerequisites';
|
|
624
|
+
|
|
625
|
+
// A `false` src, not '': an empty-src carrier script.
|
|
626
|
+
wp_register_script( $carrier, false, $script_dependencies, $version, true );
|
|
627
|
+
wp_add_inline_script( $carrier, $this->bootstrap_script() );
|
|
628
|
+
|
|
629
|
+
wp_register_style(
|
|
630
|
+
$carrier,
|
|
631
|
+
false,
|
|
632
|
+
array_values( array_filter( $script_dependencies, static fn ( string $handle ): bool => wp_style_is( $handle, 'registered' ) ) ),
|
|
633
|
+
$version
|
|
634
|
+
);
|
|
635
|
+
|
|
636
|
+
// Dummy Script Module whose dependencies are the page's boot graph.
|
|
637
|
+
wp_register_script_module( $this->handle, $this->build->url( 'modules/' . $this->config['slug'] . '/loader.js' ), $this->boot_dependencies() );
|
|
638
|
+
|
|
639
|
+
wp_enqueue_script( $carrier );
|
|
640
|
+
wp_enqueue_script_module( $this->handle );
|
|
641
|
+
wp_enqueue_style( $carrier );
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* The inline script that boots the app.
|
|
646
|
+
*
|
|
647
|
+
* From the `wp_add_inline_script()` calls of both templates.
|
|
648
|
+
*
|
|
649
|
+
* Full-page mode calls `init()` right away. wp-admin mode defers the
|
|
650
|
+
* `initSinglePage()` import to DOMContentLoaded, so every classic
|
|
651
|
+
* dependency of `@wordpress/boot` has run first; a modulepreloaded boot
|
|
652
|
+
* module can otherwise evaluate before `wp.theme.privateApis` exists and
|
|
653
|
+
* throw "Cannot unlock an undefined object" (core #65103).
|
|
654
|
+
*
|
|
655
|
+
* @return string
|
|
656
|
+
*/
|
|
657
|
+
private function bootstrap_script(): string {
|
|
658
|
+
$json_flags = JSON_HEX_TAG | JSON_UNESCAPED_SLASHES;
|
|
659
|
+
|
|
660
|
+
if ( self::FULL_PAGE === $this->mode ) {
|
|
661
|
+
return sprintf(
|
|
662
|
+
'import("@wordpress/boot").then(mod => mod.init({mountId: "%s", menuItems: %s, routes: %s, initModules: %s, dashboardLink: "%s"}));',
|
|
663
|
+
$this->mount_id(),
|
|
664
|
+
wp_json_encode( $this->menu_items, $json_flags ),
|
|
665
|
+
wp_json_encode( $this->routes, $json_flags ),
|
|
666
|
+
wp_json_encode( $this->config['init_modules'], $json_flags ),
|
|
667
|
+
esc_url( admin_url( '/' ) )
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
$init = <<<'JS'
|
|
672
|
+
( mountId, routes, initModules ) => {
|
|
673
|
+
const run = async () => {
|
|
674
|
+
const mod = await import( "@wordpress/boot" );
|
|
675
|
+
mod.initSinglePage( { mountId, routes, initModules } );
|
|
676
|
+
};
|
|
677
|
+
if ( document.readyState === "loading" ) {
|
|
678
|
+
document.addEventListener( "DOMContentLoaded", run );
|
|
679
|
+
} else {
|
|
680
|
+
run();
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
JS;
|
|
684
|
+
|
|
685
|
+
return sprintf(
|
|
686
|
+
'( %s )( %s, %s, %s );',
|
|
687
|
+
$init,
|
|
688
|
+
wp_json_encode( $this->mount_id(), $json_flags ),
|
|
689
|
+
wp_json_encode( $this->routes, $json_flags ),
|
|
690
|
+
wp_json_encode( $this->config['init_modules'], $json_flags )
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* The page's Script Module graph: `@wordpress/boot` and the init modules
|
|
696
|
+
* statically, each route's `route` module statically and its `content`
|
|
697
|
+
* module dynamically.
|
|
698
|
+
*
|
|
699
|
+
* From both templates' `$boot_dependencies`.
|
|
700
|
+
*
|
|
701
|
+
* @return array<int, array{import: string, id: string}>
|
|
702
|
+
*/
|
|
703
|
+
private function boot_dependencies(): array {
|
|
704
|
+
$dependencies = array(
|
|
705
|
+
array(
|
|
706
|
+
'import' => 'static',
|
|
707
|
+
'id' => '@wordpress/boot',
|
|
708
|
+
),
|
|
709
|
+
);
|
|
710
|
+
|
|
711
|
+
foreach ( $this->config['init_modules'] as $module ) {
|
|
712
|
+
$dependencies[] = array(
|
|
713
|
+
'import' => 'static',
|
|
714
|
+
'id' => $module,
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
foreach ( $this->routes as $route ) {
|
|
719
|
+
if ( isset( $route['route_module'] ) ) {
|
|
720
|
+
$dependencies[] = array(
|
|
721
|
+
'import' => 'static',
|
|
722
|
+
'id' => $route['route_module'],
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
if ( isset( $route['content_module'] ) ) {
|
|
726
|
+
$dependencies[] = array(
|
|
727
|
+
'import' => 'dynamic',
|
|
728
|
+
'id' => $route['content_module'],
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Filters the page's boot Script Module dependencies.
|
|
735
|
+
*
|
|
736
|
+
* Append entries with 'import' ('static' or 'dynamic') and 'id' (Script
|
|
737
|
+
* Module id) keys to load more modules with the page.
|
|
738
|
+
*
|
|
739
|
+
* @param array $dependencies Boot dependencies for the page.
|
|
740
|
+
*/
|
|
741
|
+
return apply_filters( $this->handle . '_boot_dependencies', $dependencies );
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Build-relative paths of every route's `.asset.php` sidecars.
|
|
746
|
+
*
|
|
747
|
+
* @return list<string>
|
|
748
|
+
*/
|
|
749
|
+
private function asset_files(): array {
|
|
750
|
+
$files = array();
|
|
751
|
+
|
|
752
|
+
foreach ( $this->config['routes'] as $route ) {
|
|
753
|
+
if ( $route['has_content'] ) {
|
|
754
|
+
$files[] = $route['dir'] . '/content.asset.php';
|
|
755
|
+
}
|
|
756
|
+
if ( $route['has_route'] ) {
|
|
757
|
+
$files[] = $route['dir'] . '/route.asset.php';
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
return $files;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Read a `.asset.php` sidecar.
|
|
766
|
+
*
|
|
767
|
+
* @param string $file Build-relative path.
|
|
768
|
+
* @return array<string, mixed>|null Null when the build didn't write it.
|
|
769
|
+
*/
|
|
770
|
+
private function asset( string $file ): ?array {
|
|
771
|
+
$path = $this->build->path( $file );
|
|
772
|
+
|
|
773
|
+
if ( ! file_exists( $path ) ) {
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
$asset = require $path;
|
|
778
|
+
|
|
779
|
+
return is_array( $asset ) ? $asset : null;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Register one route Script Module from its sidecar.
|
|
784
|
+
*
|
|
785
|
+
* @param string $id Script Module id.
|
|
786
|
+
* @param string $file Build-relative module path.
|
|
787
|
+
* @param array<string, mixed> $asset The module's `.asset.php` sidecar.
|
|
788
|
+
* @return void
|
|
789
|
+
*/
|
|
790
|
+
private function register_module( string $id, string $file, array $asset ): void {
|
|
791
|
+
wp_deregister_script_module( $id );
|
|
792
|
+
wp_register_script_module( $id, $this->build->url( $file ), $asset['module_dependencies'] ?? array(), $asset['version'] ?? false );
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* The id of the element the app mounts into.
|
|
797
|
+
*
|
|
798
|
+
* @return string
|
|
799
|
+
*/
|
|
800
|
+
private function mount_id(): string {
|
|
801
|
+
return $this->handle . '-app';
|
|
802
|
+
}
|
|
803
|
+
}
|