@10up/wp-vite-plugins 1.0.0-alpha.2 → 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 +129 -53
- package/dist/index.d.ts +429 -244
- package/dist/index.js +4334 -2623
- 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 -46
- package/dist/build.d.ts +0 -13
- package/dist/build.js +0 -396
- package/dist/build.js.map +0 -1
- package/dist/cli.js +0 -550
- package/dist/cli.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/runner-WUYCS-nx.d.ts +0 -178
- package/dist/watch.d.ts +0 -12
- package/dist/watch.js +0 -381
- package/dist/watch.js.map +0 -1
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
/**
|
|
3
|
+
* Pages class.
|
|
4
|
+
*
|
|
5
|
+
* @package TenUp\WpBoot
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
declare( strict_types = 1 );
|
|
9
|
+
|
|
10
|
+
namespace TenUp\WpBoot;
|
|
11
|
+
|
|
12
|
+
use Closure;
|
|
13
|
+
use InvalidArgumentException;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The `@wordpress/boot` admin pages one build produced, and where its
|
|
17
|
+
* output lives.
|
|
18
|
+
*
|
|
19
|
+
* A port of the PHP `@wordpress/build` generates for its pages, the original
|
|
20
|
+
* implementation (Gutenberg's packages/wp-build/templates; see the README's
|
|
21
|
+
* "Origin"). Where wp-build writes one file per page and keeps routes in
|
|
22
|
+
* globals, the build here emits a `build.php` that calls register() with the
|
|
23
|
+
* pages as data; each page gets a Page per mode it was built for
|
|
24
|
+
* ('full-page' and/or 'wp-admin').
|
|
25
|
+
*
|
|
26
|
+
* @link https://github.com/WordPress/gutenberg/tree/trunk/packages/wp-build
|
|
27
|
+
*
|
|
28
|
+
* @phpstan-type Route array{name: string, path: string, dir: string, has_content: bool, has_route: bool}
|
|
29
|
+
* @phpstan-type Menu array{title: Closure(): string, parent?: string|false, capability?: string, position?: int, icon?: string}
|
|
30
|
+
* @phpstan-type PageConfig array{slug: string, capability: string, modes: list<string>, init_modules: list<string>, rest_preload?: list<string|array{string, string}>, menu?: Menu, routes: list<Route>}
|
|
31
|
+
*/
|
|
32
|
+
final class Pages {
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Every registered page, by handle (`<slug>` or `<slug>-wp-admin`).
|
|
36
|
+
*
|
|
37
|
+
* @var array<string, Page>
|
|
38
|
+
*/
|
|
39
|
+
private static array $pages = array();
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Memoized result of `$build_url`.
|
|
43
|
+
*
|
|
44
|
+
* @var string|null
|
|
45
|
+
*/
|
|
46
|
+
private ?string $url = null;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Describe one build's output.
|
|
50
|
+
*
|
|
51
|
+
* @param string $build_dir Absolute path of the build directory.
|
|
52
|
+
* @param Closure $build_url Returns the build directory's URL. Called lazily, so
|
|
53
|
+
* filters added after load still apply.
|
|
54
|
+
* @param string $prefix Project prefix, for hook names.
|
|
55
|
+
* @param string $handle_prefix Script Module id prefix for route modules.
|
|
56
|
+
*
|
|
57
|
+
* @phpstan-param Closure(): string $build_url
|
|
58
|
+
*/
|
|
59
|
+
public function __construct(
|
|
60
|
+
private readonly string $build_dir,
|
|
61
|
+
private readonly Closure $build_url,
|
|
62
|
+
public readonly string $prefix,
|
|
63
|
+
public readonly string $handle_prefix,
|
|
64
|
+
) {}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Register a build's pages and hook them into WordPress.
|
|
68
|
+
*
|
|
69
|
+
* When a page is built for both modes the wp-admin one owns the
|
|
70
|
+
* generated menu entry.
|
|
71
|
+
*
|
|
72
|
+
* @param string $build_dir Absolute path of the build directory.
|
|
73
|
+
* @param Closure $build_url Returns the build directory's URL.
|
|
74
|
+
* @param string $prefix Project prefix, for hook names.
|
|
75
|
+
* @param string $handle_prefix Script Module id prefix for route modules.
|
|
76
|
+
* @param array $pages The pages, as the build wrote them.
|
|
77
|
+
* @return void
|
|
78
|
+
*
|
|
79
|
+
* @phpstan-param Closure(): string $build_url
|
|
80
|
+
* @phpstan-param list<PageConfig> $pages
|
|
81
|
+
*/
|
|
82
|
+
public static function register( string $build_dir, Closure $build_url, string $prefix, string $handle_prefix, array $pages ): void {
|
|
83
|
+
$build = new self( $build_dir, $build_url, $prefix, $handle_prefix );
|
|
84
|
+
|
|
85
|
+
foreach ( $pages as $config ) {
|
|
86
|
+
foreach ( $config['modes'] as $mode ) {
|
|
87
|
+
$page = new Page( $build, $config, $mode );
|
|
88
|
+
$owns_menu = isset( $config['menu'] ) && ( Page::WP_ADMIN === $mode || ! in_array( Page::WP_ADMIN, $config['modes'], true ) );
|
|
89
|
+
|
|
90
|
+
$page->register_hooks( $owns_menu );
|
|
91
|
+
self::$pages[ $page->handle ] = $page;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* A registered page, for the generated `<prefix>_register_<slug>_route()`
|
|
98
|
+
* style functions.
|
|
99
|
+
*
|
|
100
|
+
* @param string $handle Page handle: `<slug>` or `<slug>-wp-admin`.
|
|
101
|
+
* @return Page
|
|
102
|
+
* @throws InvalidArgumentException When no page has that handle.
|
|
103
|
+
*/
|
|
104
|
+
public static function get( string $handle ): Page {
|
|
105
|
+
if ( ! isset( self::$pages[ $handle ] ) ) {
|
|
106
|
+
throw new InvalidArgumentException( esc_html( "No admin page is registered as \"{$handle}\"." ) );
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return self::$pages[ $handle ];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Absolute path of a file in the build directory.
|
|
114
|
+
*
|
|
115
|
+
* @param string $path Build-relative path.
|
|
116
|
+
* @return string
|
|
117
|
+
*/
|
|
118
|
+
public function path( string $path ): string {
|
|
119
|
+
return $this->build_dir . '/' . $path;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* URL of a file in the build directory.
|
|
124
|
+
*
|
|
125
|
+
* @param string $path Build-relative path.
|
|
126
|
+
* @return string
|
|
127
|
+
*/
|
|
128
|
+
public function url( string $path ): string {
|
|
129
|
+
$this->url ??= ( $this->build_url )();
|
|
130
|
+
|
|
131
|
+
return $this->url . $path;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
/**
|
|
3
|
+
* Bridge class.
|
|
4
|
+
*
|
|
5
|
+
* @package TenUp\WpViteDevServer
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
declare( strict_types = 1 );
|
|
9
|
+
|
|
10
|
+
namespace TenUp\WpViteDevServer;
|
|
11
|
+
|
|
12
|
+
use WP_HTML_Tag_Processor;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Repoints WordPress's enqueues at a running Vite dev server.
|
|
16
|
+
*
|
|
17
|
+
* WordPress prints the URLs a script, stylesheet or Script Module was
|
|
18
|
+
* registered with. While a dev server is up, every one of those that points
|
|
19
|
+
* into this build's dist directory is mapped back to its source file through
|
|
20
|
+
* the `source` key of the `.asset.php` sidecar next to it, and served by Vite
|
|
21
|
+
* instead:
|
|
22
|
+
*
|
|
23
|
+
* - Classic scripts: `script_loader_src` for the URL, `script_loader_tag`
|
|
24
|
+
* to print them `type="module"`. Filters rather than a registry walk, so a
|
|
25
|
+
* script registered while printing is caught too.
|
|
26
|
+
* - Script Modules: `script_module_loader_src` (WP 6.5+), the single choke
|
|
27
|
+
* point for the module tag, its modulepreload link and the import map.
|
|
28
|
+
* - Stylesheets: dequeued and replaced with a module script, because Vite
|
|
29
|
+
* serves CSS through JavaScript in dev (that is how it hot-swaps them).
|
|
30
|
+
* `style_loader_src` rewrites any it missed to Vite's `?direct` CSS.
|
|
31
|
+
*
|
|
32
|
+
* It also enqueues `@vite/client` ahead of everything it repoints, and the
|
|
33
|
+
* React Fast Refresh preamble when the hot file lists `vite:react-refresh`.
|
|
34
|
+
*/
|
|
35
|
+
final class Bridge {
|
|
36
|
+
|
|
37
|
+
/** The dev-server plugin whose presence turns on the React refresh preamble. */
|
|
38
|
+
private const REACT_REFRESH = 'vite:react-refresh';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Script handle of Vite's HMR client: `<prefix>-vite-client`.
|
|
42
|
+
*
|
|
43
|
+
* @var string
|
|
44
|
+
*/
|
|
45
|
+
private readonly string $client;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* URL of the dist directory, or '' when WordPress can't build one. Null until computed.
|
|
49
|
+
*
|
|
50
|
+
* @var string|null
|
|
51
|
+
*/
|
|
52
|
+
private ?string $dist_url = null;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Source path per asset URL already looked up (null: not ours).
|
|
56
|
+
*
|
|
57
|
+
* @var array<string, string|null>
|
|
58
|
+
*/
|
|
59
|
+
private array $sources = array();
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Script handles that have to print as `type="module"`.
|
|
63
|
+
*
|
|
64
|
+
* @var array<string, true>
|
|
65
|
+
*/
|
|
66
|
+
private array $modules = array();
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Whether the React refresh preamble has been attached.
|
|
70
|
+
*
|
|
71
|
+
* @var bool
|
|
72
|
+
*/
|
|
73
|
+
private bool $preamble_attached = false;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Create a bridge for one dist directory.
|
|
77
|
+
*
|
|
78
|
+
* @param string $dist_dir Absolute path of the dist directory.
|
|
79
|
+
* @param HotFile $hot The running dev server.
|
|
80
|
+
* @param string $prefix Project prefix; namespaces the client handle, constant and filter.
|
|
81
|
+
*/
|
|
82
|
+
public function __construct(
|
|
83
|
+
private readonly string $dist_dir,
|
|
84
|
+
private readonly HotFile $hot,
|
|
85
|
+
private readonly string $prefix,
|
|
86
|
+
) {
|
|
87
|
+
$this->client = $prefix . '-vite-client';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Start bridging a dist directory when a dev server is running for it.
|
|
92
|
+
*
|
|
93
|
+
* With no hot file this is a single `file_exists()` and no hooks.
|
|
94
|
+
*
|
|
95
|
+
* @param string $dist_dir Absolute path of the dist directory.
|
|
96
|
+
* @param string $hot_file Hot file name, relative to `$dist_dir`.
|
|
97
|
+
* @param string $prefix Project prefix.
|
|
98
|
+
* @return self|null The registered bridge, or null when no dev server is running.
|
|
99
|
+
*/
|
|
100
|
+
public static function boot( string $dist_dir, string $hot_file, string $prefix ): ?self {
|
|
101
|
+
$hot = HotFile::read( $dist_dir . '/' . $hot_file );
|
|
102
|
+
|
|
103
|
+
if ( null === $hot ) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
$bridge = new self( $dist_dir, $hot, $prefix );
|
|
108
|
+
|
|
109
|
+
if ( $bridge->should_verify_dev_server() && ! $bridge->dev_server_responds() ) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
$bridge->register_hooks();
|
|
114
|
+
|
|
115
|
+
return $bridge;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Hook the bridge into WordPress's enqueue pipeline.
|
|
120
|
+
*
|
|
121
|
+
* @return void
|
|
122
|
+
*/
|
|
123
|
+
private function register_hooks(): void {
|
|
124
|
+
foreach ( array( 'wp_enqueue_scripts', 'admin_enqueue_scripts' ) as $hook ) {
|
|
125
|
+
add_action( $hook, array( $this, 'enqueue_client' ), -1000 );
|
|
126
|
+
add_action( $hook, array( $this, 'prepare' ), PHP_INT_MAX );
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
add_filter( 'script_loader_src', array( $this, 'script_src' ), 10, 2 );
|
|
130
|
+
add_filter( 'script_loader_tag', array( $this, 'script_tag' ), 10, 2 );
|
|
131
|
+
add_filter( 'script_module_loader_src', array( $this, 'script_module_src' ) );
|
|
132
|
+
add_filter( 'style_loader_src', array( $this, 'style_src' ) );
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Whether to probe the dev server before trusting the hot file.
|
|
137
|
+
*
|
|
138
|
+
* Off by default. A `kill -9`'d dev server leaves its hot file behind,
|
|
139
|
+
* pointing at a socket nothing answers on; catching that costs an HTTP
|
|
140
|
+
* request on every request, so it is opt-in through the
|
|
141
|
+
* `<PREFIX>_WP_VITE_CHECK_DEV_SERVER` constant or the
|
|
142
|
+
* `<prefix>_wp_vite_check_dev_server` filter.
|
|
143
|
+
*
|
|
144
|
+
* @return bool
|
|
145
|
+
*/
|
|
146
|
+
private function should_verify_dev_server(): bool {
|
|
147
|
+
$prefix = str_replace( '-', '_', $this->prefix );
|
|
148
|
+
$constant = strtoupper( $prefix ) . '_WP_VITE_CHECK_DEV_SERVER';
|
|
149
|
+
$default = defined( $constant ) && (bool) constant( $constant );
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Filters whether the dev-server bridge checks the server is reachable
|
|
153
|
+
* before trusting the hot file.
|
|
154
|
+
*
|
|
155
|
+
* @param bool $verify Default from the `<PREFIX>_WP_VITE_CHECK_DEV_SERVER` constant, else false.
|
|
156
|
+
*/
|
|
157
|
+
return (bool) apply_filters( $prefix . '_wp_vite_check_dev_server', $default );
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Whether `@vite/client` answers within 200ms.
|
|
162
|
+
*
|
|
163
|
+
* @return bool
|
|
164
|
+
*/
|
|
165
|
+
private function dev_server_responds(): bool {
|
|
166
|
+
$response = wp_remote_head(
|
|
167
|
+
$this->hot->url( '@vite/client' ),
|
|
168
|
+
array( 'timeout' => 0.2 )
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
$status = is_wp_error( $response ) ? 0 : (int) wp_remote_retrieve_response_code( $response );
|
|
172
|
+
|
|
173
|
+
return $status >= 200 && $status < 400;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Register and enqueue Vite's HMR client before anything else.
|
|
178
|
+
*
|
|
179
|
+
* It opens the websocket, applies hot updates and renders the error
|
|
180
|
+
* overlay.
|
|
181
|
+
*
|
|
182
|
+
* @return void
|
|
183
|
+
*/
|
|
184
|
+
public function enqueue_client(): void {
|
|
185
|
+
wp_register_script( $this->client, $this->hot->url( '@vite/client' ), array(), null, false ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
|
|
186
|
+
wp_enqueue_script( $this->client );
|
|
187
|
+
$this->modules[ $this->client ] = true;
|
|
188
|
+
|
|
189
|
+
$this->enqueue_react_refresh_preamble();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Attach the React Fast Refresh preamble to the client handle, when the
|
|
194
|
+
* dev server runs `@vitejs/plugin-react`.
|
|
195
|
+
*
|
|
196
|
+
* The preamble must run before any component module evaluates. Attaching
|
|
197
|
+
* it to the client, which every repointed classic script depends on and
|
|
198
|
+
* which prints before any Script Module, gets that ordering for free.
|
|
199
|
+
*
|
|
200
|
+
* @return void
|
|
201
|
+
*/
|
|
202
|
+
private function enqueue_react_refresh_preamble(): void {
|
|
203
|
+
if ( $this->preamble_attached || ! $this->hot->has_plugin( self::REACT_REFRESH ) ) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
$this->preamble_attached = true;
|
|
208
|
+
|
|
209
|
+
$preamble = sprintf(
|
|
210
|
+
'import RefreshRuntime from "%1$s"
|
|
211
|
+
RefreshRuntime.injectIntoGlobalHook(window)
|
|
212
|
+
window.$RefreshReg$ = () => {}
|
|
213
|
+
window.$RefreshSig$ = () => (type) => type
|
|
214
|
+
window.__vite_plugin_react_preamble_installed__ = true',
|
|
215
|
+
esc_js( $this->hot->url( '@react-refresh' ) )
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
wp_add_inline_script( $this->client, $preamble, 'after' );
|
|
219
|
+
add_filter( 'wp_inline_script_attributes', array( $this, 'react_refresh_inline_attributes' ) );
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Print the preamble `type="module"`: it is a top-level `import`.
|
|
224
|
+
*
|
|
225
|
+
* Matches the inline block's own `<handle>-js-after` id, so other inline
|
|
226
|
+
* scripts on the client handle stay classic.
|
|
227
|
+
*
|
|
228
|
+
* @param mixed $attributes Attributes of the inline script about to print.
|
|
229
|
+
* @return mixed
|
|
230
|
+
*/
|
|
231
|
+
public function react_refresh_inline_attributes( mixed $attributes ): mixed {
|
|
232
|
+
if ( is_array( $attributes ) && isset( $attributes['id'] ) && $this->client . '-js-after' === $attributes['id'] ) {
|
|
233
|
+
$attributes['type'] = 'module';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return $attributes;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The pass that needs the queues still mutable: make the client a
|
|
241
|
+
* dependency of every script about to be repointed, and swap this
|
|
242
|
+
* directory's queued stylesheets for the JS modules Vite serves them as.
|
|
243
|
+
*
|
|
244
|
+
* Runs last on the enqueue hooks.
|
|
245
|
+
*
|
|
246
|
+
* @return void
|
|
247
|
+
*/
|
|
248
|
+
public function prepare(): void {
|
|
249
|
+
$claimed = array();
|
|
250
|
+
$has_client = wp_script_is( $this->client, 'registered' );
|
|
251
|
+
|
|
252
|
+
foreach ( wp_scripts()->registered as $handle => $script ) {
|
|
253
|
+
if ( $this->client === $handle || ! is_string( $script->src ) || '' === $script->src ) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
$source = $this->source( $script->src );
|
|
258
|
+
|
|
259
|
+
if ( null === $source ) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
$claimed[ $source ] = true;
|
|
264
|
+
|
|
265
|
+
// A missing dependency makes WordPress drop the script, so only
|
|
266
|
+
// depend on a client that is there.
|
|
267
|
+
if ( $has_client && ! in_array( $this->client, $script->deps, true ) ) {
|
|
268
|
+
$script->deps[] = $this->client;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
$styles = wp_styles();
|
|
273
|
+
|
|
274
|
+
foreach ( $styles->queue as $handle ) {
|
|
275
|
+
$src = isset( $styles->registered[ $handle ] ) ? $styles->registered[ $handle ]->src : null;
|
|
276
|
+
|
|
277
|
+
if ( ! is_string( $src ) || '' === $src ) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
$source = $this->source( $src );
|
|
282
|
+
|
|
283
|
+
if ( null === $source ) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
wp_dequeue_style( $handle );
|
|
288
|
+
|
|
289
|
+
// An entry that imports its own CSS already carries it in dev.
|
|
290
|
+
if ( isset( $claimed[ $source ] ) ) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
$claimed[ $source ] = true;
|
|
295
|
+
$shim = $handle . '-vite-dev';
|
|
296
|
+
|
|
297
|
+
wp_register_script( $shim, $this->hot->url( $source ), $has_client ? array( $this->client ) : array(), null, false ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
|
|
298
|
+
wp_enqueue_script( $shim );
|
|
299
|
+
$this->modules[ $shim ] = true;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Repoint a classic script.
|
|
305
|
+
*
|
|
306
|
+
* @param mixed $src Script URL.
|
|
307
|
+
* @param string $handle Script handle.
|
|
308
|
+
* @return mixed The dev-server URL, or `$src` unchanged.
|
|
309
|
+
*/
|
|
310
|
+
public function script_src( mixed $src, string $handle ): mixed {
|
|
311
|
+
$source = $this->source( $src );
|
|
312
|
+
|
|
313
|
+
if ( null === $source ) {
|
|
314
|
+
return $src;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// script_loader_src always runs before script_loader_tag for the same
|
|
318
|
+
// handle, so marking it here is enough.
|
|
319
|
+
$this->modules[ $handle ] = true;
|
|
320
|
+
|
|
321
|
+
return $this->hot->url( $source );
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Print the scripts this bridge repointed as `type="module"`.
|
|
326
|
+
*
|
|
327
|
+
* @param mixed $tag Script tag HTML.
|
|
328
|
+
* @param string $handle Script handle.
|
|
329
|
+
* @return mixed
|
|
330
|
+
*/
|
|
331
|
+
public function script_tag( mixed $tag, string $handle ): mixed {
|
|
332
|
+
if ( ! is_string( $tag ) || ! isset( $this->modules[ $handle ] ) ) {
|
|
333
|
+
return $tag;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
$processor = new WP_HTML_Tag_Processor( $tag );
|
|
337
|
+
|
|
338
|
+
while ( $processor->next_tag( array( 'tag_name' => 'script' ) ) ) {
|
|
339
|
+
// Inline blocks attached to the same handle stay classic.
|
|
340
|
+
if ( null !== $processor->get_attribute( 'src' ) ) {
|
|
341
|
+
$processor->set_attribute( 'type', 'module' );
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return $processor->get_updated_html();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Repoint a Script Module.
|
|
350
|
+
*
|
|
351
|
+
* @param mixed $src Script Module URL.
|
|
352
|
+
* @return mixed
|
|
353
|
+
*/
|
|
354
|
+
public function script_module_src( mixed $src ): mixed {
|
|
355
|
+
$source = $this->source( $src );
|
|
356
|
+
|
|
357
|
+
return null === $source ? $src : $this->hot->url( $source );
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Repoint a stylesheet prepare() never saw (registered later, or pulled
|
|
362
|
+
* in as another stylesheet's dependency) at Vite's plain-CSS `?direct`
|
|
363
|
+
* URL: correct styles, no hot reload.
|
|
364
|
+
*
|
|
365
|
+
* @param mixed $src Stylesheet URL.
|
|
366
|
+
* @return mixed
|
|
367
|
+
*/
|
|
368
|
+
public function style_src( mixed $src ): mixed {
|
|
369
|
+
$source = $this->source( $src );
|
|
370
|
+
|
|
371
|
+
return null === $source ? $src : $this->hot->url( $source ) . '?direct';
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Map an asset URL in the dist directory to the source file Vite serves.
|
|
376
|
+
*
|
|
377
|
+
* Strips the query and the dist URL, swaps the extension for
|
|
378
|
+
* `.asset.php` and reads that sidecar's 'source' key.
|
|
379
|
+
*
|
|
380
|
+
* @param mixed $url Asset URL as WordPress is about to print it.
|
|
381
|
+
* @return string|null Project-relative source path, or null when the URL is
|
|
382
|
+
* not in the dist directory or has no sidecar.
|
|
383
|
+
*/
|
|
384
|
+
private function source( mixed $url ): ?string {
|
|
385
|
+
if ( ! is_string( $url ) || '' === $url ) {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
$dist_url = $this->dist_url();
|
|
390
|
+
|
|
391
|
+
if ( '' === $dist_url ) {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// The sidecar lives at the unversioned path.
|
|
396
|
+
$query = strpos( $url, '?' );
|
|
397
|
+
if ( false !== $query ) {
|
|
398
|
+
$url = substr( $url, 0, $query );
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if ( ! array_key_exists( $url, $this->sources ) ) {
|
|
402
|
+
$this->sources[ $url ] = $this->lookup_source( $url, $dist_url );
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return $this->sources[ $url ];
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* The uncached half of source().
|
|
410
|
+
*
|
|
411
|
+
* @param string $url Asset URL without its query.
|
|
412
|
+
* @param string $dist_url URL of the dist directory.
|
|
413
|
+
* @return string|null
|
|
414
|
+
*/
|
|
415
|
+
private function lookup_source( string $url, string $dist_url ): ?string {
|
|
416
|
+
$relative = null;
|
|
417
|
+
|
|
418
|
+
// Compare without the scheme (an http/https mismatch is routine), then
|
|
419
|
+
// by path alone, for root-relative and CDN-fronted srcs.
|
|
420
|
+
$candidates = array(
|
|
421
|
+
array( self::strip_scheme( $dist_url ), self::strip_scheme( $url ) ),
|
|
422
|
+
array( self::strip_host( $dist_url ), self::strip_host( $url ) ),
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
foreach ( $candidates as list( $prefix, $candidate ) ) {
|
|
426
|
+
if ( '' !== $prefix && str_starts_with( $candidate, $prefix ) ) {
|
|
427
|
+
$relative = substr( $candidate, strlen( $prefix ) );
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// The path came out of a URL and reaches a require(): only the shape a
|
|
433
|
+
// build emits gets through.
|
|
434
|
+
if ( null === $relative || ! preg_match( '#^[A-Za-z0-9_][A-Za-z0-9_./-]*$#', $relative ) || str_contains( $relative, '..' ) ) {
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
$asset_file = $this->dist_dir . '/' . preg_replace( '/\.(js|mjs|css)$/', '', $relative ) . '.asset.php';
|
|
439
|
+
|
|
440
|
+
if ( ! file_exists( $asset_file ) ) {
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
$asset = require $asset_file;
|
|
445
|
+
|
|
446
|
+
return is_array( $asset ) && ! empty( $asset['source'] ) && is_string( $asset['source'] ) ? $asset['source'] : null;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* A URL without its scheme: `//host/path`.
|
|
451
|
+
*
|
|
452
|
+
* @param string $url URL.
|
|
453
|
+
* @return string
|
|
454
|
+
*/
|
|
455
|
+
private static function strip_scheme( string $url ): string {
|
|
456
|
+
return (string) preg_replace( '#^[a-z][a-z0-9+.-]*:#i', '', $url );
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* A URL without its scheme and host: `/path`.
|
|
461
|
+
*
|
|
462
|
+
* @param string $url URL.
|
|
463
|
+
* @return string
|
|
464
|
+
*/
|
|
465
|
+
private static function strip_host( string $url ): string {
|
|
466
|
+
return (string) preg_replace( '#^([a-z][a-z0-9+.-]*:)?//[^/]*#i', '', $url );
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* URL of the dist directory, derived from its path.
|
|
471
|
+
*
|
|
472
|
+
* Tried most-specific first: a theme under a custom theme root, or a
|
|
473
|
+
* directory reached through a symlink, is only spelled right by its own
|
|
474
|
+
* helper.
|
|
475
|
+
*
|
|
476
|
+
* @return string Trailing-slashed URL, or '' when the directory is outside
|
|
477
|
+
* every location WordPress can build a URL for.
|
|
478
|
+
*/
|
|
479
|
+
private function dist_url(): string {
|
|
480
|
+
if ( null !== $this->dist_url ) {
|
|
481
|
+
return $this->dist_url;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
$this->dist_url = '';
|
|
485
|
+
$dir = rtrim( str_replace( '\\', '/', $this->dist_dir ), '/' );
|
|
486
|
+
$roots = array(
|
|
487
|
+
array( get_stylesheet_directory(), get_stylesheet_directory_uri() ),
|
|
488
|
+
array( get_template_directory(), get_template_directory_uri() ),
|
|
489
|
+
array( WPMU_PLUGIN_DIR, WPMU_PLUGIN_URL ),
|
|
490
|
+
array( WP_PLUGIN_DIR, WP_PLUGIN_URL ),
|
|
491
|
+
array( WP_CONTENT_DIR, content_url() ),
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
foreach ( $roots as list( $root_dir, $root_url ) ) {
|
|
495
|
+
$root_dir = rtrim( str_replace( '\\', '/', $root_dir ), '/' );
|
|
496
|
+
|
|
497
|
+
if ( '' === $root_dir || '' === $root_url || ! str_starts_with( $dir . '/', $root_dir . '/' ) ) {
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
$this->dist_url = rtrim( $root_url, '/' ) . substr( $dir, strlen( $root_dir ) ) . '/';
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return $this->dist_url;
|
|
506
|
+
}
|
|
507
|
+
}
|