@uniflowed/vite 0.0.0-alpha.14 → 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 +155 -2
- package/index.js +33 -3
- package/internal/assets.js +266 -18
- package/internal/devtools.js +117 -0
- package/internal/diagnostics.js +5 -2
- package/internal/routes.js +18 -2
- package/package.json +3 -3
package/driver.js
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
// <host> driver.js build --root <dir> [--mode <m>] [--out-dir <dir>]
|
|
11
11
|
// [--prerender everything|possible|nothing]
|
|
12
12
|
// [--static-build] [--because <sentence>]
|
|
13
|
+
// <host> driver.js library --root <dir> [--mode <m>] [--out-dir <dir>]
|
|
14
|
+
// --entry <file>... --format <es|cjs>... [--external <name>]...
|
|
13
15
|
// <host> driver.js compile --root <dir> [--mode <m>] [--out-dir <dir>] --assets <file> --bundle <dir>
|
|
14
16
|
// <host> driver.js deploy --root <dir> [--mode <m>] [--out-dir <dir>] --adapter <name> --work <dir> --output <dir>
|
|
15
17
|
// <host> driver.js preview --root <dir> [--mode <m>] [--out-dir <dir>] [--host <h>] [--port <n>]
|
|
@@ -38,7 +40,7 @@
|
|
|
38
40
|
// one host that can evaluate the file evaluates it.
|
|
39
41
|
|
|
40
42
|
import { createServer as createHttpServer } from "node:http";
|
|
41
|
-
import { register } from "node:module";
|
|
43
|
+
import { builtinModules, register } from "node:module";
|
|
42
44
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
43
45
|
import path from "node:path";
|
|
44
46
|
import { pathToFileURL } from "node:url";
|
|
@@ -97,7 +99,7 @@ process.stdin.on("end", () => process.exit(0));
|
|
|
97
99
|
process.stdin.on("error", () => process.exit(0));
|
|
98
100
|
process.stdin.resume();
|
|
99
101
|
|
|
100
|
-
const commands = { dev, build, compile, deploy, preview, start, config: printConfig };
|
|
102
|
+
const commands = { dev, build, library, compile, deploy, preview, start, config: printConfig };
|
|
101
103
|
const run = commands[command];
|
|
102
104
|
if (run == null) {
|
|
103
105
|
emit("error", { message: `unknown driver command ${JSON.stringify(command)}` });
|
|
@@ -680,6 +682,147 @@ async function build() {
|
|
|
680
682
|
process.exit(0);
|
|
681
683
|
}
|
|
682
684
|
|
|
685
|
+
/**
|
|
686
|
+
* The library build, for a project whose `app.router.enabled` is false.
|
|
687
|
+
*
|
|
688
|
+
* `build` above is an application build and has no other mode: it links
|
|
689
|
+
* `virtual:uf/client`, which imports the router and the project's `app.js`.
|
|
690
|
+
* A library has neither, so `uf build` in a project `uf create lib`
|
|
691
|
+
* scaffolded failed at the first pass with `Could not resolve '<root>/app.js'`
|
|
692
|
+
* — a file a library does not have and never had. See ubugeeei-prod/uf#268.
|
|
693
|
+
*
|
|
694
|
+
* This is the fourth thing the driver does, beside `dev`, `build` and
|
|
695
|
+
* `compile`, and it is one pass per format over one input list. Which of the
|
|
696
|
+
* two builds runs is **not decided here**: `uf` resolves it from the config
|
|
697
|
+
* (`uf_config`'s `LibraryPlan`) and spawns this subcommand, the same way
|
|
698
|
+
* `--prerender` arrives as one word rather than as two settings for this file
|
|
699
|
+
* to read together.
|
|
700
|
+
*
|
|
701
|
+
* # The three ways it differs from the application build
|
|
702
|
+
*
|
|
703
|
+
* * **Every dependency stays an import.** `--external` names them, and
|
|
704
|
+
* `uf` computes the list from the project's own manifest —
|
|
705
|
+
* `dependencies`, `peerDependencies`, `optionalDependencies` — so a
|
|
706
|
+
* library ships its own modules and nobody else's. That is the opposite
|
|
707
|
+
* of the application build, which inlines what it can because an
|
|
708
|
+
* application is the end of the line and a library is not: a bundled copy
|
|
709
|
+
* of React inside a library is a second React in every application that
|
|
710
|
+
* installs it.
|
|
711
|
+
* * **One output per entry, named after the entry.** `index.js` becomes
|
|
712
|
+
* `dist/index.js`; `internal/parse.js` becomes `dist/internal/parse.js`.
|
|
713
|
+
* The path rather than the basename, so two entries cannot collide at the
|
|
714
|
+
* moment one would overwrite the other.
|
|
715
|
+
* * **No manifest, no prerender, no server bundle.** There is no document to
|
|
716
|
+
* write and no route table to write it from.
|
|
717
|
+
*
|
|
718
|
+
* Vite's own `build.lib` does the work. uf owns *that* a library is a
|
|
719
|
+
* different build and what goes into it; how this builder performs one is the
|
|
720
|
+
* builder's, which is the same line `build` draws around `rollupOptions`.
|
|
721
|
+
*/
|
|
722
|
+
async function library() {
|
|
723
|
+
const vite = await import("vite");
|
|
724
|
+
const config = await loadConfig();
|
|
725
|
+
const inline = await viteConfig(config, argument("--mode") ?? "production");
|
|
726
|
+
const outDir = path.resolve(root, inline.build.outDir);
|
|
727
|
+
const entries = argumentAll("--entry");
|
|
728
|
+
const formats = argumentAll("--format");
|
|
729
|
+
const external = argumentAll("--external");
|
|
730
|
+
if (entries.length === 0) {
|
|
731
|
+
throw new Error("uf: `driver.js library` needs at least one --entry");
|
|
732
|
+
}
|
|
733
|
+
if (formats.length === 0) {
|
|
734
|
+
throw new Error("uf: `driver.js library` needs at least one --format");
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Keyed by the entry's path without its extension, which is what Vite's lib
|
|
738
|
+
// mode turns into the output file name.
|
|
739
|
+
const input = {};
|
|
740
|
+
for (const entry of entries) {
|
|
741
|
+
input[entryName(entry)] = path.resolve(root, entry);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const isExternal = externalTest(external);
|
|
745
|
+
// One pass per format rather than one build with several outputs: Vite's
|
|
746
|
+
// lib mode writes a whole `outDir` per format, and the second pass must not
|
|
747
|
+
// empty what the first wrote. So `emptyOutDir` is true exactly once, on the
|
|
748
|
+
// first, which is also what makes a build that dropped an entry leave no
|
|
749
|
+
// stale copy of it behind.
|
|
750
|
+
let first = true;
|
|
751
|
+
for (const format of formats) {
|
|
752
|
+
emit("phase", { name: `library (${format})` });
|
|
753
|
+
await vite.build({
|
|
754
|
+
...inline,
|
|
755
|
+
build: {
|
|
756
|
+
...inline.build,
|
|
757
|
+
// Vite's `manifest` maps source modules to hashed browser assets. A
|
|
758
|
+
// library has neither — its file names are its API — and writing one
|
|
759
|
+
// would put a `.vite/` directory into a published tarball.
|
|
760
|
+
manifest: false,
|
|
761
|
+
outDir,
|
|
762
|
+
emptyOutDir: first,
|
|
763
|
+
lib: {
|
|
764
|
+
entry: input,
|
|
765
|
+
formats: [format],
|
|
766
|
+
fileName: (_format, name) => `${name}.${format === "cjs" ? "cjs" : "js"}`,
|
|
767
|
+
},
|
|
768
|
+
rollupOptions: { external: isExternal },
|
|
769
|
+
},
|
|
770
|
+
});
|
|
771
|
+
first = false;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
emit("done", { outDir: path.relative(root, outDir), pages: 0 });
|
|
775
|
+
process.exit(0);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* The output name for one entry: its path, without the extension.
|
|
780
|
+
*
|
|
781
|
+
* Not the basename. `index.js` and `internal/index.js` are two entries a
|
|
782
|
+
* library can reasonably have, and under a basename they are one file written
|
|
783
|
+
* twice — the second silently winning, which is a published package whose
|
|
784
|
+
* subpath export is somebody else's module.
|
|
785
|
+
*/
|
|
786
|
+
function entryName(entry) {
|
|
787
|
+
const normalised = entry.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
788
|
+
const dot = normalised.lastIndexOf(".");
|
|
789
|
+
const slash = normalised.lastIndexOf("/");
|
|
790
|
+
return dot > slash ? normalised.slice(0, dot) : normalised;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Whether an import is somebody else's module.
|
|
795
|
+
*
|
|
796
|
+
* Three checks, and only the one over `names` is a policy uf decided. `names`
|
|
797
|
+
* is what `uf` read out of the project's manifest and passed as `--external`,
|
|
798
|
+
* and a subpath of one of those names — `@scope/pkg/deep` for `@scope/pkg` —
|
|
799
|
+
* is the same package. The other two are the host's built-in modules, and they
|
|
800
|
+
* are a fact rather than a decision: `node:fs` has no bytes to inline.
|
|
801
|
+
*
|
|
802
|
+
* A bare relative or absolute id is never external, which is the rule that
|
|
803
|
+
* makes this a library build at all: what the author wrote is bundled, and
|
|
804
|
+
* what they installed is imported.
|
|
805
|
+
*/
|
|
806
|
+
function externalTest(names) {
|
|
807
|
+
const declared = new Set(names);
|
|
808
|
+
// The host's built-in module names, unprefixed. `node:`-prefixed ids are
|
|
809
|
+
// caught by the first check whatever the host is; this set is for the bare
|
|
810
|
+
// spellings — `fs`, `path`, `stream` — which a dependency written before the
|
|
811
|
+
// prefix existed still uses. Read from the running host rather than written
|
|
812
|
+
// down, because the list grows and a stale copy of it here would be a
|
|
813
|
+
// bundled `node:worker_threads` that cannot be bundled.
|
|
814
|
+
const builtins = new Set(builtinModules ?? []);
|
|
815
|
+
return (id) => {
|
|
816
|
+
if (id.startsWith("node:")) return true;
|
|
817
|
+
if (builtins.has(id)) return true;
|
|
818
|
+
if (declared.has(id)) return true;
|
|
819
|
+
for (const name of declared) {
|
|
820
|
+
if (id.startsWith(`${name}/`)) return true;
|
|
821
|
+
}
|
|
822
|
+
return false;
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
|
|
683
826
|
/**
|
|
684
827
|
* Link the whole application into one JavaScript file, for `uf build --compile`.
|
|
685
828
|
*
|
|
@@ -887,6 +1030,16 @@ const SERVERLESS_CAPABILITIES = { module: "@uniflowed/server/lambda", name: "lam
|
|
|
887
1030
|
* copying every file in it is bulk work over the whole build, which belongs in
|
|
888
1031
|
* Rust rather than in the host process — the same division `--compile` makes
|
|
889
1032
|
* with its embedded assets.
|
|
1033
|
+
*
|
|
1034
|
+
* # `--adapter static` never reaches this function
|
|
1035
|
+
*
|
|
1036
|
+
* It is the one implemented target with no application to link: a static host
|
|
1037
|
+
* returns files, and `uf build` has already written them. So `uf` copies the
|
|
1038
|
+
* output directory itself and never spawns this driver for it, which is why
|
|
1039
|
+
* [`ADAPTERS`] has four rows and not five. What that target does instead of
|
|
1040
|
+
* linking is refuse a project whose route handlers, middleware, unprerendered
|
|
1041
|
+
* routes or server actions a static host cannot answer — in Rust, because the
|
|
1042
|
+
* facts it needs are the route table and what the prerender reported.
|
|
890
1043
|
*/
|
|
891
1044
|
async function deploy() {
|
|
892
1045
|
const vite = await import("vite");
|
package/index.js
CHANGED
|
@@ -79,6 +79,7 @@ import {
|
|
|
79
79
|
} from "./internal/routes.js";
|
|
80
80
|
import { TransformService, isFlowModule } from "@uniflowed/host/transform";
|
|
81
81
|
import { createChannelMiddleware } from "./internal/diagnostics.js";
|
|
82
|
+
import { devtoolsPreamble } from "./internal/devtools.js";
|
|
82
83
|
import { send, toRequest } from "./internal/http.js";
|
|
83
84
|
import { beginRequest } from "./internal/serve.js";
|
|
84
85
|
|
|
@@ -122,19 +123,26 @@ export default function uniflowed(options = {}) {
|
|
|
122
123
|
const appEntry = app.router?.entry ?? ufConfig.build?.entries?.[0] ?? "app.js";
|
|
123
124
|
const markdown = app.builtins?.markdown ?? {};
|
|
124
125
|
const builtins = app.builtins ?? {};
|
|
126
|
+
// On unless the project says otherwise, and read as `!== false` rather than
|
|
127
|
+
// `=== true` because that is what "on by default" means for a field almost
|
|
128
|
+
// no `uf.config.js` will mention. It only ever reaches the *development*
|
|
129
|
+
// client entry; see `flowPlugin`'s `load`. ubugeeei-prod/uf#516.
|
|
130
|
+
const strictMode = app.react?.strictMode !== false;
|
|
125
131
|
|
|
126
132
|
return [
|
|
127
|
-
flowPlugin({ routerRoot, appEntry, command: options.command }),
|
|
133
|
+
flowPlugin({ routerRoot, appEntry, strictMode, command: options.command }),
|
|
128
134
|
mdxPlugin(markdown),
|
|
129
135
|
assetPlugin({
|
|
130
136
|
images: builtins.images ?? {},
|
|
131
137
|
fonts: builtins.fonts ?? {},
|
|
138
|
+
icons: builtins.icons ?? {},
|
|
139
|
+
og: builtins.og ?? {},
|
|
132
140
|
command: options.command,
|
|
133
141
|
}),
|
|
134
142
|
];
|
|
135
143
|
}
|
|
136
144
|
|
|
137
|
-
function flowPlugin({ routerRoot, appEntry, command }) {
|
|
145
|
+
function flowPlugin({ routerRoot, appEntry, strictMode, command }) {
|
|
138
146
|
let root = process.cwd();
|
|
139
147
|
let isProduction = false;
|
|
140
148
|
let base = "/";
|
|
@@ -305,7 +313,12 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
305
313
|
if (isSsr(this, loadOptions)) return routesModuleSource(table);
|
|
306
314
|
return clientRoutesModule(table);
|
|
307
315
|
}
|
|
308
|
-
|
|
316
|
+
// Strict Mode belongs to the client entry and to development only: a
|
|
317
|
+
// build passes `false`, so the generated module is the one that existed
|
|
318
|
+
// before #516 and a visitor's browser renders once.
|
|
319
|
+
if (id === resolved(VIRTUAL.client)) {
|
|
320
|
+
return clientModuleSource(entryPath, { strictMode: strictMode && !isProduction });
|
|
321
|
+
}
|
|
309
322
|
if (id === resolved(VIRTUAL.server)) return serverModuleSource(entryPath);
|
|
310
323
|
// Only `virtual:uf/server` imports this, so it is only ever asked for in
|
|
311
324
|
// the server environment — but the table it carries is every callable
|
|
@@ -404,9 +417,26 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
404
417
|
}
|
|
405
418
|
},
|
|
406
419
|
|
|
420
|
+
// The two scripts a development document loads before its own, and
|
|
421
|
+
// nothing at all in a build — which is the whole of "the hook is out of a
|
|
422
|
+
// production build" (ubugeeei-prod/uf#503) and of "production does not run
|
|
423
|
+
// under Strict Mode" (#516, whose flag is generated into
|
|
424
|
+
// `virtual:uf/client` rather than injected here).
|
|
425
|
+
//
|
|
426
|
+
// DevTools first, and as a *classic* script rather than a module: React
|
|
427
|
+
// registers itself with `__REACT_DEVTOOLS_GLOBAL_HOOK__` while `react-dom`
|
|
428
|
+
// is evaluated and never again, so the hook has to exist before any module
|
|
429
|
+
// runs. A classic inline script runs while the parser is on it; a module
|
|
430
|
+
// waits for the document. `internal/devtools.js` has the rest of the
|
|
431
|
+
// argument, and the three conditions DevTools needs.
|
|
407
432
|
transformIndexHtml() {
|
|
408
433
|
if (isProduction) return [];
|
|
409
434
|
return [
|
|
435
|
+
{
|
|
436
|
+
tag: "script",
|
|
437
|
+
children: devtoolsPreamble(),
|
|
438
|
+
injectTo: "head-prepend",
|
|
439
|
+
},
|
|
410
440
|
{
|
|
411
441
|
tag: "script",
|
|
412
442
|
attrs: { type: "module" },
|
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
|
+
}
|
package/internal/diagnostics.js
CHANGED
|
@@ -16,8 +16,11 @@
|
|
|
16
16
|
//
|
|
17
17
|
// * `POST /__uf/diagnostic` — a diagnostic a browser-side runtime produced
|
|
18
18
|
// and wants a person to read. `@uniflowed/router`'s `internal/diagnostics.js`
|
|
19
|
-
// is the client half
|
|
20
|
-
//
|
|
19
|
+
// is the client half; the hydration-mismatch report beside it is what calls
|
|
20
|
+
// it, and so is `internal/devtools.js`, which reads back after hydration
|
|
21
|
+
// whether React DevTools can attach to this page at all
|
|
22
|
+
// (ubugeeei-prod/uf#503). One channel and not one per feature is the whole
|
|
23
|
+
// point: a second endpoint would be a second thing to notice.
|
|
21
24
|
// * `POST /__uf/vitals` — the five numbers `@uniflowed/web/vitals`
|
|
22
25
|
// measures, posted by `vitalsBeacon()`. In production a project points the
|
|
23
26
|
// beacon at an endpoint of its own; in development there was nothing at
|
package/internal/routes.js
CHANGED
|
@@ -790,12 +790,28 @@ export default routes;
|
|
|
790
790
|
* The current route's modules are loaded *before* hydration so the first
|
|
791
791
|
* render is synchronous and matches the server's HTML; a lazy import during
|
|
792
792
|
* hydration would suspend and React would fall back to a client render.
|
|
793
|
+
*
|
|
794
|
+
* # Strict Mode is a generated constant, not a runtime check
|
|
795
|
+
*
|
|
796
|
+
* `strictMode` is written into this module as a literal, so a production build
|
|
797
|
+
* gets `hydrate({ … })` with the argument absent and Rollup has nothing to
|
|
798
|
+
* decide. It would have been shorter to have `hydrate` read `import.meta.hot`
|
|
799
|
+
* — the way `client.js` gates the hydration reporter — and that would have been
|
|
800
|
+
* one signal answering two questions: `uf.config.js` can turn Strict Mode off
|
|
801
|
+
* (ubugeeei-prod/uf#516) and `import.meta.hot` cannot be told about it. A
|
|
802
|
+
* project that sets `app.react.strictMode: false` gets a dev server that
|
|
803
|
+
* hydrates the way its deployment does, which is the whole of the escape
|
|
804
|
+
* hatch.
|
|
805
|
+
*
|
|
806
|
+
* @param {string} appEntry the project's `app.js`, as an import specifier
|
|
807
|
+
* @param {{ strictMode?: boolean }} [options]
|
|
793
808
|
*/
|
|
794
|
-
export function clientModuleSource(appEntry) {
|
|
809
|
+
export function clientModuleSource(appEntry, options = {}) {
|
|
810
|
+
const strictMode = options.strictMode === true ? ", strictMode: true" : "";
|
|
795
811
|
return `import { hydrate } from "@uniflowed/router/client";
|
|
796
812
|
import { routes, notFound, errors } from ${JSON.stringify(VIRTUAL.routes)};
|
|
797
813
|
import App from ${JSON.stringify(appEntry)};
|
|
798
|
-
hydrate({ App, routes, notFound, errors });
|
|
814
|
+
hydrate({ App, routes, notFound, errors${strictMode} });
|
|
799
815
|
`;
|
|
800
816
|
}
|
|
801
817
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniflowed/vite",
|
|
3
|
-
"version": "0.0.0-alpha.
|
|
3
|
+
"version": "0.0.0-alpha.15",
|
|
4
4
|
"description": "Vite, driven by uf.config.js: every Flow module through `uf transform`, MDX, the file-system router and static rendering as Vite plugins.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@mdx-js/rollup": "^3.1.1",
|
|
35
35
|
"@shikijs/rehype": "^3.23.0",
|
|
36
|
-
"@uniflowed/host": "0.0.0-alpha.
|
|
37
|
-
"@uniflowed/server": "0.0.0-alpha.
|
|
36
|
+
"@uniflowed/host": "0.0.0-alpha.15",
|
|
37
|
+
"@uniflowed/server": "0.0.0-alpha.15",
|
|
38
38
|
"rehype-slug": "^6.0.0",
|
|
39
39
|
"remark-frontmatter": "^5.0.0",
|
|
40
40
|
"remark-gfm": "^4.0.1",
|