@uniflowed/vite 0.0.0-alpha.13 → 0.0.0-alpha.15
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/driver.js +436 -149
- package/index.js +217 -93
- package/internal/assets.js +266 -18
- package/internal/devtools.js +117 -0
- package/internal/diagnostics.js +369 -0
- package/internal/events.js +5 -5
- package/internal/routes.js +62 -8
- package/internal/serve.js +39 -15
- package/package.json +11 -3
package/internal/assets.js
CHANGED
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
// imports", `resolveId` + `load` + `generateBundle` + `writeBundle` +
|
|
10
10
|
// `transformIndexHtml` — since before there was anything behind it, and
|
|
11
11
|
// `uf inspect` has been listing it in the resolved pipeline. This is the
|
|
12
|
-
// implementation of a plugin uf was already claiming to run
|
|
12
|
+
// implementation of a plugin uf was already claiming to run — and `resolveId`
|
|
13
|
+
// and `generateBundle`, declared there from the start and unimplemented until
|
|
14
|
+
// icons arrived, are now real: the first resolves `uf:icon/…`, the second
|
|
15
|
+
// assembles the sprite once the graph is complete.
|
|
13
16
|
//
|
|
14
17
|
// # What an import becomes
|
|
15
18
|
//
|
|
@@ -45,6 +48,19 @@
|
|
|
45
48
|
// two must agree": there is one pipeline and one set of bytes, and the only
|
|
46
49
|
// thing that differs between them is the URL prefix they are served under.
|
|
47
50
|
//
|
|
51
|
+
// # Icons, and why they are not `.svg`
|
|
52
|
+
//
|
|
53
|
+
// `import Star from "uf:icon/star"` resolves against the directory
|
|
54
|
+
// `app.builtins.icons.dir` names, and `import sprite from "uf:icon-sprite"` is
|
|
55
|
+
// the sprite built from every icon the build reached. Neither claims an
|
|
56
|
+
// extension, which is the point: `.svg` stays Vite's, so `vite-plugin-svgr`
|
|
57
|
+
// and everything like it keep working, and uf adds a capability in its own
|
|
58
|
+
// namespace instead of taking one away.
|
|
59
|
+
//
|
|
60
|
+
// The sprite is emitted in `generateBundle`, which is the first hook that runs
|
|
61
|
+
// after every module has been loaded — and therefore the first moment the set
|
|
62
|
+
// of icons a build reached is the whole set.
|
|
63
|
+
//
|
|
48
64
|
// # What this plugin deliberately does not claim
|
|
49
65
|
//
|
|
50
66
|
// An import with a query — `./hero.png?url`, `?raw`, `?inline` — is left to
|
|
@@ -57,7 +73,13 @@
|
|
|
57
73
|
import { existsSync, readFileSync } from "node:fs";
|
|
58
74
|
import path from "node:path";
|
|
59
75
|
|
|
60
|
-
import {
|
|
76
|
+
import {
|
|
77
|
+
AssetService,
|
|
78
|
+
ICON_PREFIX,
|
|
79
|
+
ICON_SPRITE,
|
|
80
|
+
OG_EXTENSION,
|
|
81
|
+
assetKind,
|
|
82
|
+
} from "@uniflowed/host/assets";
|
|
61
83
|
|
|
62
84
|
/** Where transformed assets are kept, relative to the project root. */
|
|
63
85
|
export const CACHE_DIR = ".uf/cache/assets";
|
|
@@ -71,6 +93,36 @@ export const CACHE_DIR = ".uf/cache/assets";
|
|
|
71
93
|
*/
|
|
72
94
|
export const DEV_PREFIX = "@uf-asset/";
|
|
73
95
|
|
|
96
|
+
/**
|
|
97
|
+
* What stands in for the sprite until `generateBundle` knows what is in it.
|
|
98
|
+
*
|
|
99
|
+
* A string no source file would contain, replaced in the generated chunk once
|
|
100
|
+
* every module has been loaded. The module graph needs *something* at `load`
|
|
101
|
+
* time and the sprite is not knowable then; see the `sprite` branch of `load`.
|
|
102
|
+
*/
|
|
103
|
+
const SPRITE_PLACEHOLDER = "__UF_ICON_SPRITE__";
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* What a `*.og.json` import resolves to.
|
|
107
|
+
*
|
|
108
|
+
* A card cannot keep its own id, and the reason is worth writing down because
|
|
109
|
+
* nothing about `enforce: "pre"` prevents it. Vite's JSON handling is a
|
|
110
|
+
* *native* rolldown plugin, `builtin:vite-json`, and it selects modules by
|
|
111
|
+
* **id**: anything still ending in `.json` when its `transform` runs is put
|
|
112
|
+
* through a JSON parser, whatever an earlier `load` returned. Ordering the
|
|
113
|
+
* hooks does not help, because the module this plugin loads is JavaScript
|
|
114
|
+
* under a name that says JSON — and the parser's answer is
|
|
115
|
+
* `expected value at line 1 column 1`.
|
|
116
|
+
*
|
|
117
|
+
* So the id changes rather than the order. The card resolves to
|
|
118
|
+
* `\0uf-og:<absolute path>.js`: `\0` is the convention for a module that is
|
|
119
|
+
* not a file, and the trailing extension is what takes it out of every
|
|
120
|
+
* `.json` filter in the pipeline.
|
|
121
|
+
*/
|
|
122
|
+
const OG_PREFIX = "\0uf-og:";
|
|
123
|
+
/** Appended to that id so it does not end in `.json`. See [`OG_PREFIX`]. */
|
|
124
|
+
const OG_SUFFIX = ".js";
|
|
125
|
+
|
|
74
126
|
/**
|
|
75
127
|
* The module source for one transformed asset.
|
|
76
128
|
*
|
|
@@ -146,15 +198,20 @@ export function withUrls(image, baseUrl) {
|
|
|
146
198
|
* @param {object} options
|
|
147
199
|
* @param {object} [options.images] `app.builtins.images`
|
|
148
200
|
* @param {object} [options.fonts] `app.builtins.fonts`
|
|
201
|
+
* @param {object} [options.icons] `app.builtins.icons`
|
|
202
|
+
* @param {object} [options.og] `app.builtins.og`
|
|
149
203
|
* @param {string} [options.command] the `uf` binary to transform through
|
|
150
204
|
*/
|
|
151
|
-
export function assetPlugin({ images = {}, fonts = {}, command } = {}) {
|
|
205
|
+
export function assetPlugin({ images = {}, fonts = {}, icons = {}, og = {}, command } = {}) {
|
|
152
206
|
// Both halves can be turned off independently, and a plugin that is off is
|
|
153
207
|
// still in the array: `uf inspect` lists the resolved pipeline, and a
|
|
154
208
|
// pipeline that changes shape when a feature is disabled is a pipeline whose
|
|
155
209
|
// listing cannot be compared between two projects.
|
|
156
210
|
const imagesOn = images.enabled !== false;
|
|
157
211
|
const fontsOn = fonts.enabled !== false;
|
|
212
|
+
const iconsOn = icons.enabled !== false;
|
|
213
|
+
const ogOn = og.enabled !== false;
|
|
214
|
+
const iconDir = icons.dir ?? "icons";
|
|
158
215
|
|
|
159
216
|
let root = process.cwd();
|
|
160
217
|
let base = "/";
|
|
@@ -174,6 +231,18 @@ export function assetPlugin({ images = {}, fonts = {}, command } = {}) {
|
|
|
174
231
|
* redo every image, which is the whole cost this map exists to avoid.
|
|
175
232
|
*/
|
|
176
233
|
const transformed = new Map();
|
|
234
|
+
/** Whether anything imported the sprite at all. */
|
|
235
|
+
let spriteRequested = false;
|
|
236
|
+
/**
|
|
237
|
+
* Every icon this build has reached, by symbol id.
|
|
238
|
+
*
|
|
239
|
+
* Here and not in the `uf assets` process, because this closure outlives it:
|
|
240
|
+
* a build closes the service in `buildEnd`, which runs *before*
|
|
241
|
+
* `generateBundle`, and a build with a client environment and a server one
|
|
242
|
+
* has two bundles and one set of icons between them. The plugin is what
|
|
243
|
+
* spans a build, so the plugin is what remembers.
|
|
244
|
+
*/
|
|
245
|
+
const reachedIcons = new Map();
|
|
177
246
|
|
|
178
247
|
const cacheDir = () => path.resolve(root, CACHE_DIR);
|
|
179
248
|
/**
|
|
@@ -206,9 +275,32 @@ export function assetPlugin({ images = {}, fonts = {}, command } = {}) {
|
|
|
206
275
|
const kind = assetKind(id);
|
|
207
276
|
if (kind === "image" && !imagesOn) return null;
|
|
208
277
|
if (kind === "font" && !fontsOn) return null;
|
|
278
|
+
if (kind === "og" && !ogOn) return null;
|
|
279
|
+
if ((kind === "icon" || kind === "sprite") && !iconsOn) return null;
|
|
209
280
|
return kind;
|
|
210
281
|
};
|
|
211
282
|
|
|
283
|
+
/**
|
|
284
|
+
* The file one `uf:icon/<name>` names.
|
|
285
|
+
*
|
|
286
|
+
* The name is a path segment and nothing else. `..` in it would reach out of
|
|
287
|
+
* the icon directory and turn an import into a way to read the repository,
|
|
288
|
+
* so it is rejected rather than resolved — the same rule the dev middleware
|
|
289
|
+
* applies to an emitted file name.
|
|
290
|
+
*/
|
|
291
|
+
const iconFile = (id) => {
|
|
292
|
+
const name = id.slice(ICON_PREFIX.length);
|
|
293
|
+
if (name === "" || name.includes("..") || path.isAbsolute(name)) return null;
|
|
294
|
+
return { name, file: path.resolve(root, iconDir, `${name}.svg`) };
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
/** Tell a dev server the sprite it already sent is missing an icon. */
|
|
298
|
+
const invalidateSprite = () => {
|
|
299
|
+
if (server == null || !spriteRequested) return;
|
|
300
|
+
const module = server.moduleGraph.getModuleById(ICON_SPRITE);
|
|
301
|
+
if (module != null) server.moduleGraph.invalidateModule(module);
|
|
302
|
+
};
|
|
303
|
+
|
|
212
304
|
return {
|
|
213
305
|
name: "uf:asset",
|
|
214
306
|
// Before Vite's own asset handling, which would otherwise claim the same
|
|
@@ -222,9 +314,93 @@ export function assetPlugin({ images = {}, fonts = {}, command } = {}) {
|
|
|
222
314
|
isBuild = config.command === "build";
|
|
223
315
|
},
|
|
224
316
|
|
|
317
|
+
// `uf:icon/…` and `uf:icon-sprite` are uf's own ids and resolve to
|
|
318
|
+
// themselves. Returning the id unchanged rather than a `\0`-prefixed one
|
|
319
|
+
// keeps it readable in a stack trace and in `vite --debug`, and nothing
|
|
320
|
+
// else in the pipeline claims the `uf:` scheme.
|
|
321
|
+
//
|
|
322
|
+
// A card is the opposite case: it has to *lose* its name, because the name
|
|
323
|
+
// is what the native JSON plugin claims it by. See `OG_PREFIX`.
|
|
324
|
+
async resolveId(id, importer, options) {
|
|
325
|
+
if (iconsOn && (id === ICON_SPRITE || id.startsWith(ICON_PREFIX))) return id;
|
|
326
|
+
if (!ogOn || !id.toLowerCase().endsWith(OG_EXTENSION)) return null;
|
|
327
|
+
// Compared against the id as written, so a query is never claimed:
|
|
328
|
+
// `./card.og.json?raw` and `?url` still reach the file, because a query
|
|
329
|
+
// is Vite's.
|
|
330
|
+
const resolved = await this.resolve(id, importer, { ...options, skipSelf: true });
|
|
331
|
+
if (resolved == null || resolved.external || !existsSync(resolved.id)) return resolved;
|
|
332
|
+
return `${OG_PREFIX}${resolved.id}${OG_SUFFIX}`;
|
|
333
|
+
},
|
|
334
|
+
|
|
225
335
|
async load(id) {
|
|
336
|
+
if (id.startsWith(OG_PREFIX)) {
|
|
337
|
+
const file = id.slice(OG_PREFIX.length, -OG_SUFFIX.length);
|
|
338
|
+
// Watched by hand, because the module id is no longer the file's path
|
|
339
|
+
// and nothing else would associate the two. Without this a dev server
|
|
340
|
+
// never redraws a card whose template was edited.
|
|
341
|
+
this.addWatchFile(file);
|
|
342
|
+
return loadAsset.call(this, {
|
|
343
|
+
kind: "og",
|
|
344
|
+
file,
|
|
345
|
+
transformed,
|
|
346
|
+
service: ensureService(),
|
|
347
|
+
cacheDir: cacheDir(),
|
|
348
|
+
baseUrl: baseUrl(),
|
|
349
|
+
assetsDir,
|
|
350
|
+
isBuild,
|
|
351
|
+
images,
|
|
352
|
+
fonts,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
226
356
|
const kind = claims(id);
|
|
227
357
|
if (kind == null) return null;
|
|
358
|
+
|
|
359
|
+
if (kind === "sprite") {
|
|
360
|
+
spriteRequested = true;
|
|
361
|
+
// A build cannot know the sprite here: `load` runs while the graph is
|
|
362
|
+
// still being walked, so the set of icons reached so far is not the
|
|
363
|
+
// set. It emits a placeholder that `generateBundle` — the first hook
|
|
364
|
+
// after every module has been loaded — replaces with the real markup.
|
|
365
|
+
//
|
|
366
|
+
// A dev server has no `generateBundle`, so it assembles from what has
|
|
367
|
+
// been reached and invalidates this module whenever a new icon turns
|
|
368
|
+
// up. That costs one extra reload the first time a page introduces an
|
|
369
|
+
// icon and is exactly right afterwards, which is the trade a dev
|
|
370
|
+
// server makes everywhere else too.
|
|
371
|
+
if (isBuild) return assetModuleSource({ markup: SPRITE_PLACEHOLDER });
|
|
372
|
+
const sprite = await ensureService().sprite({
|
|
373
|
+
outDir: cacheDir(),
|
|
374
|
+
icons: [...reachedIcons.values()],
|
|
375
|
+
});
|
|
376
|
+
return assetModuleSource({ markup: sprite.markup });
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (kind === "icon") {
|
|
380
|
+
const resolved = iconFile(id);
|
|
381
|
+
if (resolved == null) return null;
|
|
382
|
+
if (!existsSync(resolved.file)) {
|
|
383
|
+
this.error(
|
|
384
|
+
`${id} does not exist: uf looked for ${path.relative(root, resolved.file)}. ` +
|
|
385
|
+
"`app.builtins.icons.dir` is where `uf:icon/…` resolves against.",
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
const asset = await ensureService().icon(resolved.file, {
|
|
389
|
+
outDir: cacheDir(),
|
|
390
|
+
name: resolved.name,
|
|
391
|
+
});
|
|
392
|
+
this.addWatchFile(resolved.file);
|
|
393
|
+
reachedIcons.set(asset.id, asset);
|
|
394
|
+
invalidateSprite();
|
|
395
|
+
return assetModuleSource({
|
|
396
|
+
id: asset.id,
|
|
397
|
+
href: `#${asset.id}`,
|
|
398
|
+
viewBox: asset.viewBox,
|
|
399
|
+
width: asset.width,
|
|
400
|
+
height: asset.height,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
228
404
|
const file = path.resolve(id);
|
|
229
405
|
// Not this plugin's to fail on: an id with one of these extensions that
|
|
230
406
|
// is not a file on disk is a virtual module somebody else owns.
|
|
@@ -244,6 +420,38 @@ export function assetPlugin({ images = {}, fonts = {}, command } = {}) {
|
|
|
244
420
|
});
|
|
245
421
|
},
|
|
246
422
|
|
|
423
|
+
// After every module has been loaded, which is the first moment the set of
|
|
424
|
+
// icons this build reached is the whole set. A sprite assembled in `load`
|
|
425
|
+
// would hold the icons reached *so far*, which is a different sprite on
|
|
426
|
+
// every run depending on module order.
|
|
427
|
+
async generateBundle(_options, bundle) {
|
|
428
|
+
if (!iconsOn || !spriteRequested) return;
|
|
429
|
+
const sprite = await ensureService().sprite({
|
|
430
|
+
outDir: cacheDir(),
|
|
431
|
+
icons: [...reachedIcons.values()],
|
|
432
|
+
});
|
|
433
|
+
// Inlined into the chunk and *not* emitted as a file of its own. An
|
|
434
|
+
// external sprite would be the better answer if it worked — one file
|
|
435
|
+
// cached across every page — but `<use href="sprite.svg#id">` does not
|
|
436
|
+
// resolve across documents in any version of Safari and is blocked
|
|
437
|
+
// cross-origin in Chrome, so the file would be dead weight in `dist/`
|
|
438
|
+
// and in `uf_bundle`'s size report. `uf assets` still writes it into the
|
|
439
|
+
// cache directory, which is where the dev server reads it from.
|
|
440
|
+
//
|
|
441
|
+
// The placeholder is replaced rather than the module re-run: by
|
|
442
|
+
// `generateBundle` the chunk is already generated, and the sprite is one
|
|
443
|
+
// string literal in it.
|
|
444
|
+
//
|
|
445
|
+
// A function replacement, because a plain string one would interpret
|
|
446
|
+
// `$&` and `$'` — and an icon is somebody else's markup.
|
|
447
|
+
const escaped = JSON.stringify(sprite.markup).slice(1, -1);
|
|
448
|
+
for (const chunk of Object.values(bundle)) {
|
|
449
|
+
if (chunk.type === "chunk" && chunk.code.includes(SPRITE_PLACEHOLDER)) {
|
|
450
|
+
chunk.code = chunk.code.replaceAll(SPRITE_PLACEHOLDER, () => escaped);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
|
|
247
455
|
configureServer(devServer) {
|
|
248
456
|
server = devServer;
|
|
249
457
|
devServer.httpServer?.once("close", () => {
|
|
@@ -288,6 +496,7 @@ export function assetPlugin({ images = {}, fonts = {}, command } = {}) {
|
|
|
288
496
|
// files and leaves the old ones for anything still holding a URL.
|
|
289
497
|
transformed.delete(`image:${path.resolve(id)}`);
|
|
290
498
|
transformed.delete(`font:${path.resolve(id)}`);
|
|
499
|
+
transformed.delete(`og:${path.resolve(id)}`);
|
|
291
500
|
},
|
|
292
501
|
|
|
293
502
|
buildEnd() {
|
|
@@ -319,24 +528,38 @@ async function loadAsset(context) {
|
|
|
319
528
|
const key = `${kind}:${file}`;
|
|
320
529
|
let manifest = transformed.get(key);
|
|
321
530
|
if (manifest == null) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
531
|
+
if (kind === "image") {
|
|
532
|
+
manifest = await service.image(file, {
|
|
533
|
+
outDir: cacheDir,
|
|
534
|
+
widths: images.widths,
|
|
535
|
+
quality: images.quality,
|
|
536
|
+
blur: images.placeholder,
|
|
537
|
+
});
|
|
538
|
+
} else if (kind === "og") {
|
|
539
|
+
manifest = await service.og(file, { outDir: cacheDir });
|
|
540
|
+
} else {
|
|
541
|
+
manifest = await service.font(file, {
|
|
542
|
+
outDir: cacheDir,
|
|
543
|
+
family: fonts.family,
|
|
544
|
+
display: fonts.display,
|
|
545
|
+
subset: fonts.subset,
|
|
546
|
+
preload: fonts.preload,
|
|
547
|
+
baseUrl,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
336
550
|
transformed.set(key, manifest);
|
|
337
551
|
}
|
|
338
552
|
|
|
339
|
-
const files =
|
|
553
|
+
const files =
|
|
554
|
+
kind === "image"
|
|
555
|
+
? manifest.variants.map((v) => v.file)
|
|
556
|
+
: kind === "og"
|
|
557
|
+
? [manifest.file]
|
|
558
|
+
: // Every bucket, not just the primary: a `unicode-range` split emits
|
|
559
|
+
// one file per script and the browser fetches whichever the page
|
|
560
|
+
// needs. Emitting only the one the manifest calls primary would
|
|
561
|
+
// leave the others named in the stylesheet and absent from `dist/`.
|
|
562
|
+
manifest.faces.map((face) => face.file);
|
|
340
563
|
if (isBuild) {
|
|
341
564
|
// Handed to Rollup rather than copied by hand, so the bundler owns what
|
|
342
565
|
// lands in the output directory and `uf_bundle`'s size report — which
|
|
@@ -356,6 +579,18 @@ async function loadAsset(context) {
|
|
|
356
579
|
if (kind === "image") {
|
|
357
580
|
return assetModuleSource(withUrls(manifest, baseUrl));
|
|
358
581
|
}
|
|
582
|
+
if (kind === "og") {
|
|
583
|
+
return assetModuleSource({
|
|
584
|
+
// The shape `Metadata.openGraph.images` and `OgImage` both read. A URL,
|
|
585
|
+
// a size and the alt text, which is all a card ever is to a page.
|
|
586
|
+
url: `${baseUrl}${manifest.file}`,
|
|
587
|
+
width: manifest.width,
|
|
588
|
+
height: manifest.height,
|
|
589
|
+
type: manifest.mime,
|
|
590
|
+
alt: manifest.alt,
|
|
591
|
+
bytes: manifest.bytes,
|
|
592
|
+
});
|
|
593
|
+
}
|
|
359
594
|
return assetModuleSource({
|
|
360
595
|
src: `${baseUrl}${manifest.file}`,
|
|
361
596
|
family: manifest.family,
|
|
@@ -373,6 +608,19 @@ async function loadAsset(context) {
|
|
|
373
608
|
metrics: manifest.metrics,
|
|
374
609
|
fallback: manifest.fallback,
|
|
375
610
|
fallbackDeclined: manifest.fallbackDeclined,
|
|
611
|
+
// Every emitted file with its URL, so `Font` can preload exactly the one
|
|
612
|
+
// marked rather than all of them — which is the one thing that would undo
|
|
613
|
+
// a `unicode-range` split.
|
|
614
|
+
faces: (manifest.faces ?? []).map((face) => ({
|
|
615
|
+
...face,
|
|
616
|
+
url: `${baseUrl}${face.file}`,
|
|
617
|
+
})),
|
|
618
|
+
subset: manifest.subset ?? null,
|
|
619
|
+
// Carried through for the same reason `declined` is on an image: a project
|
|
620
|
+
// that asked for a subset and got the whole font is entitled to the
|
|
621
|
+
// sentence saying why, without reading this plugin.
|
|
622
|
+
subsetDeclined: manifest.subsetDeclined ?? null,
|
|
623
|
+
sourceBytes: manifest.sourceBytes ?? null,
|
|
376
624
|
});
|
|
377
625
|
}
|
|
378
626
|
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// @noflow
|
|
2
|
+
//
|
|
3
|
+
// Plain JavaScript: executed by the host that runs Vite, before any transform.
|
|
4
|
+
//
|
|
5
|
+
// React DevTools, in `uf dev`, on purpose.
|
|
6
|
+
//
|
|
7
|
+
// DevTools does not attach to React. React attaches to *DevTools*: while
|
|
8
|
+
// `react-dom` is being evaluated it looks for `__REACT_DEVTOOLS_GLOBAL_HOOK__`
|
|
9
|
+
// on the global object and registers itself with whatever it finds, once. A
|
|
10
|
+
// hook that arrives after that line has run is a hook no renderer ever sees, so
|
|
11
|
+
// everything below is about one ordering — the hook exists first — and about
|
|
12
|
+
// saying so in a file whose name a person can grep for.
|
|
13
|
+
//
|
|
14
|
+
// # Why this file exists at all, when it worked before it
|
|
15
|
+
//
|
|
16
|
+
// It did work, and by accident. The Fast Refresh preamble calls
|
|
17
|
+
// `injectIntoGlobalHook` (`./refresh-runtime.js`, Meta's runtime as vendored by
|
|
18
|
+
// `@vitejs/plugin-react`), and that function installs a hook when it finds
|
|
19
|
+
// none, because Fast Refresh needs one to decorate. So `uf dev` had a DevTools
|
|
20
|
+
// hook as a side effect of a function whose subject is hot reloading, in a file
|
|
21
|
+
// uf does not own, with nothing anywhere naming DevTools and no test that would
|
|
22
|
+
// notice its absence. The next upgrade of that vendored runtime, or a change to
|
|
23
|
+
// how the preamble is injected, could have taken it away in a diff nobody would
|
|
24
|
+
// read as being about DevTools. See ubugeeei-prod/uf#503.
|
|
25
|
+
//
|
|
26
|
+
// So the hook is installed here, first, deliberately, and `tests/library/
|
|
27
|
+
// devtools.test.js` runs this script's own text against a fake window.
|
|
28
|
+
//
|
|
29
|
+
// # Three things DevTools needs, and what carries each
|
|
30
|
+
//
|
|
31
|
+
// Naming them together, because each is provided somewhere else and each is one
|
|
32
|
+
// edit away from being lost:
|
|
33
|
+
//
|
|
34
|
+
// 1. **The hook, before the renderer.** This module, injected by
|
|
35
|
+
// `packages/vite/index.js`'s `transformIndexHtml` as a *classic* script at
|
|
36
|
+
// the top of the head — see [`devtoolsPreamble`] for why classic.
|
|
37
|
+
// 2. **One copy of the renderer.** `resolve.dedupe: ["react", "react-dom"]`
|
|
38
|
+
// in that same file. Two copies of `react-dom` register two renderers, and
|
|
39
|
+
// DevTools shows the tree of whichever one it heard from — which is the
|
|
40
|
+
// shape of the "multiple renderers concurrently rendering" report.
|
|
41
|
+
// 3. **The development build of it.** `mode` is `development`, so Vite
|
|
42
|
+
// resolves React's development export condition, and `uf transform` is
|
|
43
|
+
// called with `development: true` — which is what emits `jsxDEV` and the
|
|
44
|
+
// `_jsxFileName` beside every element. Against a production build DevTools
|
|
45
|
+
// says so and shows a tree with no props, no hooks and no source.
|
|
46
|
+
//
|
|
47
|
+
// # And out of a production build
|
|
48
|
+
//
|
|
49
|
+
// A production build injects none of this: `transformIndexHtml` returns an
|
|
50
|
+
// empty list unless the plugin is serving. That is the half a person can check
|
|
51
|
+
// on the artefact rather than by reading, and
|
|
52
|
+
// `crates/uf_cli/tests/vite.rs`'s `a_build_ships_no_devtools_hook` does.
|
|
53
|
+
//
|
|
54
|
+
// What it checks for is the *assignment* below rather than the name, and the
|
|
55
|
+
// distinction is worth stating here because the obvious test is wrong: React's
|
|
56
|
+
// own production build mentions `__REACT_DEVTOOLS_GLOBAL_HOOK__` twice, because
|
|
57
|
+
// reading that global is how a deployed React application is attachable at all.
|
|
58
|
+
// React reads it; only an installer writes it. So `window.<hook> =` is what
|
|
59
|
+
// must be absent, and it is absent because this function is never called
|
|
60
|
+
// outside a dev server.
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The global React registers itself with.
|
|
64
|
+
*
|
|
65
|
+
* Written once, here, so that every other mention of it in uf — the injected
|
|
66
|
+
* script below, the assertion that a build has none — is this constant rather
|
|
67
|
+
* than a fourth spelling of a name whose whole value is that it matches
|
|
68
|
+
* React's exactly.
|
|
69
|
+
*/
|
|
70
|
+
export const DEVTOOLS_HOOK = "__REACT_DEVTOOLS_GLOBAL_HOOK__";
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The script every document loads before anything else in development.
|
|
74
|
+
*
|
|
75
|
+
* # A classic script, not a module
|
|
76
|
+
*
|
|
77
|
+
* Everything else uf injects is `type="module"`, and a module script is
|
|
78
|
+
* deferred: it runs after the document has been parsed, in document order with
|
|
79
|
+
* the other modules. That would still be early enough today, because the client
|
|
80
|
+
* entry is also a module and comes later — but "early enough as long as nobody
|
|
81
|
+
* adds a script above it" is exactly the accident this file exists to end. A
|
|
82
|
+
* classic inline script runs while the parser is on it, so no module, no
|
|
83
|
+
* import, and no `<script src>` a project's own Vite plugin injects can get
|
|
84
|
+
* between this and the renderer.
|
|
85
|
+
*
|
|
86
|
+
* # It never replaces a hook that is already there
|
|
87
|
+
*
|
|
88
|
+
* The DevTools extension installs its hook at `document_start`, which is before
|
|
89
|
+
* any script in the document, so on a machine that has DevTools the branch
|
|
90
|
+
* below is not taken and the extension's hook is what React registers with.
|
|
91
|
+
* Overwriting it would be the one way this file could break the thing it exists
|
|
92
|
+
* to support: the extension holds the connection to the panel, and a stub in
|
|
93
|
+
* its place is a page DevTools can see and never hear from.
|
|
94
|
+
*
|
|
95
|
+
* What is installed when there is nothing to leave alone is the minimum a
|
|
96
|
+
* renderer will register with — `renderers`, `supportsFiber`, `inject` and the
|
|
97
|
+
* three commit callbacks. It reports nothing to anybody: there is no panel, and
|
|
98
|
+
* the point of installing it is that `react-dom` takes the branch where a hook
|
|
99
|
+
* exists, so Fast Refresh has one to decorate and DevTools opened *later* in
|
|
100
|
+
* the same page finds a renderer already registered rather than a page that has
|
|
101
|
+
* to be reloaded. It is the same shape `injectIntoGlobalHook` installs, because
|
|
102
|
+
* it is the same contract; the difference is that this is uf saying so.
|
|
103
|
+
*/
|
|
104
|
+
export function devtoolsPreamble() {
|
|
105
|
+
return `(function () {
|
|
106
|
+
if (window.${DEVTOOLS_HOOK} != null) return;
|
|
107
|
+
var nextID = 0;
|
|
108
|
+
window.${DEVTOOLS_HOOK} = {
|
|
109
|
+
renderers: new Map(),
|
|
110
|
+
supportsFiber: true,
|
|
111
|
+
inject: function () { return nextID++; },
|
|
112
|
+
onScheduleFiberRoot: function () {},
|
|
113
|
+
onCommitFiberRoot: function () {},
|
|
114
|
+
onCommitFiberUnmount: function () {},
|
|
115
|
+
};
|
|
116
|
+
})();`;
|
|
117
|
+
}
|