@uniflowed/vite 0.0.0-alpha.14 → 0.0.0-alpha.16
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 +232 -27
- package/index.js +143 -12
- package/internal/a11y-runtime.js +234 -0
- package/internal/a11y.js +100 -0
- package/internal/assets.js +266 -18
- package/internal/compile-assets.js +67 -0
- package/internal/devtools.js +117 -0
- package/internal/diagnostics.js +5 -2
- package/internal/routes.js +18 -2
- package/internal/serve.js +30 -0
- package/package.json +10 -4
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,11 +40,12 @@
|
|
|
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";
|
|
45
47
|
|
|
48
|
+
import { COMPILE_ASSETS_ID, compileAssetsPlugin } from "./internal/compile-assets.js";
|
|
46
49
|
import { emit, errorEvent, eventLogger } from "./internal/events.js";
|
|
47
50
|
import { loadUfConfig, projectConfig } from "./internal/config.js";
|
|
48
51
|
import { send, toRequest } from "./internal/http.js";
|
|
@@ -50,6 +53,7 @@ import { withProjectConfig } from "./merge.js";
|
|
|
50
53
|
import { VIRTUAL, scanRoutes } from "./internal/routes.js";
|
|
51
54
|
import {
|
|
52
55
|
assetsFromManifest,
|
|
56
|
+
createPrerenderGate,
|
|
53
57
|
createServeHandler,
|
|
54
58
|
loadBuild,
|
|
55
59
|
nodeListener,
|
|
@@ -97,7 +101,7 @@ process.stdin.on("end", () => process.exit(0));
|
|
|
97
101
|
process.stdin.on("error", () => process.exit(0));
|
|
98
102
|
process.stdin.resume();
|
|
99
103
|
|
|
100
|
-
const commands = { dev, build, compile, deploy, preview, start, config: printConfig };
|
|
104
|
+
const commands = { dev, build, library, compile, deploy, preview, start, config: printConfig };
|
|
101
105
|
const run = commands[command];
|
|
102
106
|
if (run == null) {
|
|
103
107
|
emit("error", { message: `unknown driver command ${JSON.stringify(command)}` });
|
|
@@ -341,6 +345,16 @@ function watchEnvFiles(server) {
|
|
|
341
345
|
* The static middleware still runs first, and that is deliberate rather than
|
|
342
346
|
* incidental; see `internal/serve.js` for why `uf start` orders itself the
|
|
343
347
|
* same way.
|
|
348
|
+
*
|
|
349
|
+
* One request is the exception, and it is the only thing uf mounts in *front*
|
|
350
|
+
* of Vite here: a request carrying the draft cookie, which no front door may
|
|
351
|
+
* answer from a prerendered document. `createStaticHandler` applies that rule
|
|
352
|
+
* and cannot reach a request Vite's file middleware answered first, so under
|
|
353
|
+
* `uf preview` draft mode appeared to be off while it worked under `uf dev`
|
|
354
|
+
* and `uf start` — ubugeeei-prod/uf#620. `configurePreviewServer` is where a
|
|
355
|
+
* middleware goes ahead of Vite's own; the hook's *body* runs before they are
|
|
356
|
+
* installed and a function it returns runs after, which is why this is a body
|
|
357
|
+
* and the handler below is a `use` on the started server.
|
|
344
358
|
*/
|
|
345
359
|
async function preview() {
|
|
346
360
|
const { preview: startPreview } = await import("vite");
|
|
@@ -361,25 +375,53 @@ async function preview() {
|
|
|
361
375
|
serverDir: path.join(".uf", "build", "server"),
|
|
362
376
|
});
|
|
363
377
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
}
|
|
382
|
-
})
|
|
378
|
+
// One handler for both positions in the chain. A draft request meets it in
|
|
379
|
+
// front of Vite's file middleware and every other request meets it behind,
|
|
380
|
+
// and because it is the same handler the two give the same answer — which is
|
|
381
|
+
// the whole reason `uf preview` exists.
|
|
382
|
+
const handle =
|
|
383
|
+
build == null ? null : createServeHandler({ ...build, cache: config.app?.rendering?.cache });
|
|
384
|
+
const answer = (previewServer) => async (request, response, next) => {
|
|
385
|
+
try {
|
|
386
|
+
const asRequest = await toRequest(request, previewServer.config);
|
|
387
|
+
// The same lifecycle `uf start` gets from `nodeListener`, spelled out
|
|
388
|
+
// because this door is Vite's connect chain rather than a bare
|
|
389
|
+
// `node:http` server: the whole request runs inside it, and it settles
|
|
390
|
+
// once `send` has returned. A preview whose `after()` fired at a
|
|
391
|
+
// different moment from the production server's would be a preview that
|
|
392
|
+
// is checked and believed and wrong.
|
|
393
|
+
await withRequest(build.entry, asRequest, async () => {
|
|
394
|
+
await send(response, await handle(asRequest));
|
|
395
|
+
});
|
|
396
|
+
} catch (error) {
|
|
397
|
+
next(error);
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const mayAnswerFromPrerender = createPrerenderGate();
|
|
402
|
+
const draftFirst = {
|
|
403
|
+
name: "uf:draft-before-files",
|
|
404
|
+
configurePreviewServer(previewServer) {
|
|
405
|
+
if (handle == null) return;
|
|
406
|
+
const run = answer(previewServer);
|
|
407
|
+
previewServer.middlewares.use((request, response, next) => {
|
|
408
|
+
// Not `await`ed by connect, which takes no promise: the gate is
|
|
409
|
+
// resolved inside and `next()` is called from there. A rejection is a
|
|
410
|
+
// `next(error)` for the same reason.
|
|
411
|
+
mayAnswerFromPrerender(request.headers.cookie ?? null)
|
|
412
|
+
.then((mayAnswer) => (mayAnswer ? next() : run(request, response, next)))
|
|
413
|
+
.catch(next);
|
|
414
|
+
});
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
const server = await startPreview({
|
|
419
|
+
...inline,
|
|
420
|
+
appType: "custom",
|
|
421
|
+
plugins: [...(inline.plugins ?? []), draftFirst],
|
|
422
|
+
});
|
|
423
|
+
if (handle != null) {
|
|
424
|
+
server.middlewares.use(answer(server));
|
|
383
425
|
}
|
|
384
426
|
|
|
385
427
|
const urls = server.resolvedUrls ?? { local: [], network: [] };
|
|
@@ -680,6 +722,147 @@ async function build() {
|
|
|
680
722
|
process.exit(0);
|
|
681
723
|
}
|
|
682
724
|
|
|
725
|
+
/**
|
|
726
|
+
* The library build, for a project whose `app.router.enabled` is false.
|
|
727
|
+
*
|
|
728
|
+
* `build` above is an application build and has no other mode: it links
|
|
729
|
+
* `virtual:uf/client`, which imports the router and the project's `app.js`.
|
|
730
|
+
* A library has neither, so `uf build` in a project `uf create lib`
|
|
731
|
+
* scaffolded failed at the first pass with `Could not resolve '<root>/app.js'`
|
|
732
|
+
* — a file a library does not have and never had. See ubugeeei-prod/uf#268.
|
|
733
|
+
*
|
|
734
|
+
* This is the fourth thing the driver does, beside `dev`, `build` and
|
|
735
|
+
* `compile`, and it is one pass per format over one input list. Which of the
|
|
736
|
+
* two builds runs is **not decided here**: `uf` resolves it from the config
|
|
737
|
+
* (`uf_config`'s `LibraryPlan`) and spawns this subcommand, the same way
|
|
738
|
+
* `--prerender` arrives as one word rather than as two settings for this file
|
|
739
|
+
* to read together.
|
|
740
|
+
*
|
|
741
|
+
* # The three ways it differs from the application build
|
|
742
|
+
*
|
|
743
|
+
* * **Every dependency stays an import.** `--external` names them, and
|
|
744
|
+
* `uf` computes the list from the project's own manifest —
|
|
745
|
+
* `dependencies`, `peerDependencies`, `optionalDependencies` — so a
|
|
746
|
+
* library ships its own modules and nobody else's. That is the opposite
|
|
747
|
+
* of the application build, which inlines what it can because an
|
|
748
|
+
* application is the end of the line and a library is not: a bundled copy
|
|
749
|
+
* of React inside a library is a second React in every application that
|
|
750
|
+
* installs it.
|
|
751
|
+
* * **One output per entry, named after the entry.** `index.js` becomes
|
|
752
|
+
* `dist/index.js`; `internal/parse.js` becomes `dist/internal/parse.js`.
|
|
753
|
+
* The path rather than the basename, so two entries cannot collide at the
|
|
754
|
+
* moment one would overwrite the other.
|
|
755
|
+
* * **No manifest, no prerender, no server bundle.** There is no document to
|
|
756
|
+
* write and no route table to write it from.
|
|
757
|
+
*
|
|
758
|
+
* Vite's own `build.lib` does the work. uf owns *that* a library is a
|
|
759
|
+
* different build and what goes into it; how this builder performs one is the
|
|
760
|
+
* builder's, which is the same line `build` draws around `rollupOptions`.
|
|
761
|
+
*/
|
|
762
|
+
async function library() {
|
|
763
|
+
const vite = await import("vite");
|
|
764
|
+
const config = await loadConfig();
|
|
765
|
+
const inline = await viteConfig(config, argument("--mode") ?? "production");
|
|
766
|
+
const outDir = path.resolve(root, inline.build.outDir);
|
|
767
|
+
const entries = argumentAll("--entry");
|
|
768
|
+
const formats = argumentAll("--format");
|
|
769
|
+
const external = argumentAll("--external");
|
|
770
|
+
if (entries.length === 0) {
|
|
771
|
+
throw new Error("uf: `driver.js library` needs at least one --entry");
|
|
772
|
+
}
|
|
773
|
+
if (formats.length === 0) {
|
|
774
|
+
throw new Error("uf: `driver.js library` needs at least one --format");
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Keyed by the entry's path without its extension, which is what Vite's lib
|
|
778
|
+
// mode turns into the output file name.
|
|
779
|
+
const input = {};
|
|
780
|
+
for (const entry of entries) {
|
|
781
|
+
input[entryName(entry)] = path.resolve(root, entry);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const isExternal = externalTest(external);
|
|
785
|
+
// One pass per format rather than one build with several outputs: Vite's
|
|
786
|
+
// lib mode writes a whole `outDir` per format, and the second pass must not
|
|
787
|
+
// empty what the first wrote. So `emptyOutDir` is true exactly once, on the
|
|
788
|
+
// first, which is also what makes a build that dropped an entry leave no
|
|
789
|
+
// stale copy of it behind.
|
|
790
|
+
let first = true;
|
|
791
|
+
for (const format of formats) {
|
|
792
|
+
emit("phase", { name: `library (${format})` });
|
|
793
|
+
await vite.build({
|
|
794
|
+
...inline,
|
|
795
|
+
build: {
|
|
796
|
+
...inline.build,
|
|
797
|
+
// Vite's `manifest` maps source modules to hashed browser assets. A
|
|
798
|
+
// library has neither — its file names are its API — and writing one
|
|
799
|
+
// would put a `.vite/` directory into a published tarball.
|
|
800
|
+
manifest: false,
|
|
801
|
+
outDir,
|
|
802
|
+
emptyOutDir: first,
|
|
803
|
+
lib: {
|
|
804
|
+
entry: input,
|
|
805
|
+
formats: [format],
|
|
806
|
+
fileName: (_format, name) => `${name}.${format === "cjs" ? "cjs" : "js"}`,
|
|
807
|
+
},
|
|
808
|
+
rollupOptions: { external: isExternal },
|
|
809
|
+
},
|
|
810
|
+
});
|
|
811
|
+
first = false;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
emit("done", { outDir: path.relative(root, outDir), pages: 0 });
|
|
815
|
+
process.exit(0);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* The output name for one entry: its path, without the extension.
|
|
820
|
+
*
|
|
821
|
+
* Not the basename. `index.js` and `internal/index.js` are two entries a
|
|
822
|
+
* library can reasonably have, and under a basename they are one file written
|
|
823
|
+
* twice — the second silently winning, which is a published package whose
|
|
824
|
+
* subpath export is somebody else's module.
|
|
825
|
+
*/
|
|
826
|
+
function entryName(entry) {
|
|
827
|
+
const normalised = entry.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
828
|
+
const dot = normalised.lastIndexOf(".");
|
|
829
|
+
const slash = normalised.lastIndexOf("/");
|
|
830
|
+
return dot > slash ? normalised.slice(0, dot) : normalised;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Whether an import is somebody else's module.
|
|
835
|
+
*
|
|
836
|
+
* Three checks, and only the one over `names` is a policy uf decided. `names`
|
|
837
|
+
* is what `uf` read out of the project's manifest and passed as `--external`,
|
|
838
|
+
* and a subpath of one of those names — `@scope/pkg/deep` for `@scope/pkg` —
|
|
839
|
+
* is the same package. The other two are the host's built-in modules, and they
|
|
840
|
+
* are a fact rather than a decision: `node:fs` has no bytes to inline.
|
|
841
|
+
*
|
|
842
|
+
* A bare relative or absolute id is never external, which is the rule that
|
|
843
|
+
* makes this a library build at all: what the author wrote is bundled, and
|
|
844
|
+
* what they installed is imported.
|
|
845
|
+
*/
|
|
846
|
+
function externalTest(names) {
|
|
847
|
+
const declared = new Set(names);
|
|
848
|
+
// The host's built-in module names, unprefixed. `node:`-prefixed ids are
|
|
849
|
+
// caught by the first check whatever the host is; this set is for the bare
|
|
850
|
+
// spellings — `fs`, `path`, `stream` — which a dependency written before the
|
|
851
|
+
// prefix existed still uses. Read from the running host rather than written
|
|
852
|
+
// down, because the list grows and a stale copy of it here would be a
|
|
853
|
+
// bundled `node:worker_threads` that cannot be bundled.
|
|
854
|
+
const builtins = new Set(builtinModules ?? []);
|
|
855
|
+
return (id) => {
|
|
856
|
+
if (id.startsWith("node:")) return true;
|
|
857
|
+
if (builtins.has(id)) return true;
|
|
858
|
+
if (declared.has(id)) return true;
|
|
859
|
+
for (const name of declared) {
|
|
860
|
+
if (id.startsWith(`${name}/`)) return true;
|
|
861
|
+
}
|
|
862
|
+
return false;
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
|
|
683
866
|
/**
|
|
684
867
|
* Link the whole application into one JavaScript file, for `uf build --compile`.
|
|
685
868
|
*
|
|
@@ -741,17 +924,19 @@ async function compile() {
|
|
|
741
924
|
emit("phase", { name: "standalone" });
|
|
742
925
|
|
|
743
926
|
// The entry is written to disk rather than served as another virtual module:
|
|
744
|
-
// it is generated per build (it
|
|
927
|
+
// it is generated per build (it bakes in this build's document), and a real
|
|
745
928
|
// file is the version a person can open when a compiled binary misbehaves.
|
|
746
929
|
const entry = path.join(bundleDir, "entry.js");
|
|
747
930
|
mkdirSync(bundleDir, { recursive: true });
|
|
748
|
-
|
|
749
|
-
|
|
931
|
+
writeFileSync(
|
|
932
|
+
entry,
|
|
933
|
+
entrySource(path.relative(root, assets), assetsFromManifest(readManifest(outDir))),
|
|
934
|
+
);
|
|
750
935
|
|
|
751
936
|
await vite.build({
|
|
752
937
|
...inline,
|
|
753
938
|
customLogger: eventLogger("warn"),
|
|
754
|
-
plugins: [...inline.plugins, nativeAddonGuard()],
|
|
939
|
+
plugins: [...inline.plugins, nativeAddonGuard(), compileAssetsPlugin(assets)],
|
|
755
940
|
ssr: { ...(inline.ssr ?? {}), noExternal: true },
|
|
756
941
|
build: {
|
|
757
942
|
...inline.build,
|
|
@@ -887,6 +1072,16 @@ const SERVERLESS_CAPABILITIES = { module: "@uniflowed/server/lambda", name: "lam
|
|
|
887
1072
|
* copying every file in it is bulk work over the whole build, which belongs in
|
|
888
1073
|
* Rust rather than in the host process — the same division `--compile` makes
|
|
889
1074
|
* with its embedded assets.
|
|
1075
|
+
*
|
|
1076
|
+
* # `--adapter static` never reaches this function
|
|
1077
|
+
*
|
|
1078
|
+
* It is the one implemented target with no application to link: a static host
|
|
1079
|
+
* returns files, and `uf build` has already written them. So `uf` copies the
|
|
1080
|
+
* output directory itself and never spawns this driver for it, which is why
|
|
1081
|
+
* [`ADAPTERS`] has four rows and not five. What that target does instead of
|
|
1082
|
+
* linking is refuse a project whose route handlers, middleware, unprerendered
|
|
1083
|
+
* routes or server actions a static host cannot answer — in Rust, because the
|
|
1084
|
+
* facts it needs are the route table and what the prerender reported.
|
|
890
1085
|
*/
|
|
891
1086
|
async function deploy() {
|
|
892
1087
|
const vite = await import("vite");
|
|
@@ -1162,8 +1357,14 @@ export const handler = createLambdaHandler({ handle, beginRequest, staticDir });
|
|
|
1162
1357
|
* bytes of `dist/`. The document's script and stylesheet URLs are baked in
|
|
1163
1358
|
* here because they come from the client manifest, which exists at this moment
|
|
1164
1359
|
* and not inside the binary.
|
|
1360
|
+
*
|
|
1361
|
+
* `assetsFile` is the generated payload's path, and it appears in a comment
|
|
1362
|
+
* rather than in the import: the module enters the graph under a virtual id so
|
|
1363
|
+
* that uf's Flow transform never meets eight megabytes of base64. The reason
|
|
1364
|
+
* that matters is `./internal/compile-assets.js`; naming the file here is what
|
|
1365
|
+
* keeps a reader of the generated entry able to find the bytes it carries.
|
|
1165
1366
|
*/
|
|
1166
|
-
function entrySource(
|
|
1367
|
+
function entrySource(assetsFile, document) {
|
|
1167
1368
|
// Not `await serve(...)` at the top level. uf parses that now
|
|
1168
1369
|
// (ubugeeei-prod/uf#204) and this entry is a module, so it would work;
|
|
1169
1370
|
// `.catch` is the better spelling regardless: a binary that cannot take its
|
|
@@ -1171,7 +1372,11 @@ function entrySource(assetsSpecifier, document) {
|
|
|
1171
1372
|
// unhandled rejection.
|
|
1172
1373
|
return `// Generated by \`uf build --compile\`. Not checked in, not edited.
|
|
1173
1374
|
import { serve } from "@uniflowed/server/standalone";
|
|
1174
|
-
|
|
1375
|
+
// Every file \`uf build\` wrote, base64 in one string. It is on disk at
|
|
1376
|
+
// ${assetsFile}, and it is imported under a virtual id so that uf's
|
|
1377
|
+
// Flow transform is never asked to parse it — see \`@uniflowed/vite\`'s
|
|
1378
|
+
// \`internal/compile-assets.js\` for why that matters.
|
|
1379
|
+
import { assets } from ${JSON.stringify(COMPILE_ASSETS_ID)};
|
|
1175
1380
|
import * as app from ${JSON.stringify(VIRTUAL.server)};
|
|
1176
1381
|
|
|
1177
1382
|
serve({ app, assets, document: ${JSON.stringify(document)} }).catch((error) => {
|
package/index.js
CHANGED
|
@@ -45,6 +45,13 @@ import path from "node:path";
|
|
|
45
45
|
import mdx from "@mdx-js/rollup";
|
|
46
46
|
import rehypeSlug from "rehype-slug";
|
|
47
47
|
|
|
48
|
+
import {
|
|
49
|
+
AUDIT_PUBLIC_PATH,
|
|
50
|
+
AUDIT_RESOLVED_ID,
|
|
51
|
+
auditAvailable,
|
|
52
|
+
auditRuntimeSource,
|
|
53
|
+
auditTag,
|
|
54
|
+
} from "./internal/a11y.js";
|
|
48
55
|
import { assetPlugin } from "./internal/assets.js";
|
|
49
56
|
import { emit, reportRenderError } from "./internal/events.js";
|
|
50
57
|
import { highlightPlugin } from "./internal/highlight.js";
|
|
@@ -79,6 +86,7 @@ import {
|
|
|
79
86
|
} from "./internal/routes.js";
|
|
80
87
|
import { TransformService, isFlowModule } from "@uniflowed/host/transform";
|
|
81
88
|
import { createChannelMiddleware } from "./internal/diagnostics.js";
|
|
89
|
+
import { devtoolsPreamble } from "./internal/devtools.js";
|
|
82
90
|
import { send, toRequest } from "./internal/http.js";
|
|
83
91
|
import { beginRequest } from "./internal/serve.js";
|
|
84
92
|
|
|
@@ -122,21 +130,45 @@ export default function uniflowed(options = {}) {
|
|
|
122
130
|
const appEntry = app.router?.entry ?? ufConfig.build?.entries?.[0] ?? "app.js";
|
|
123
131
|
const markdown = app.builtins?.markdown ?? {};
|
|
124
132
|
const builtins = app.builtins ?? {};
|
|
133
|
+
// On unless the project says otherwise, and read as `!== false` rather than
|
|
134
|
+
// `=== true` because that is what "on by default" means for a field almost
|
|
135
|
+
// no `uf.config.js` will mention. It only ever reaches the *development*
|
|
136
|
+
// client entry; see `flowPlugin`'s `load`. ubugeeei-prod/uf#516.
|
|
137
|
+
const strictMode = app.react?.strictMode !== false;
|
|
138
|
+
|
|
139
|
+
const accessibility = ufConfig.accessibility ?? {};
|
|
125
140
|
|
|
126
141
|
return [
|
|
127
|
-
flowPlugin({
|
|
142
|
+
flowPlugin({
|
|
143
|
+
routerRoot,
|
|
144
|
+
appEntry,
|
|
145
|
+
strictMode,
|
|
146
|
+
command: options.command,
|
|
147
|
+
accessibility,
|
|
148
|
+
}),
|
|
128
149
|
mdxPlugin(markdown),
|
|
129
150
|
assetPlugin({
|
|
130
151
|
images: builtins.images ?? {},
|
|
131
152
|
fonts: builtins.fonts ?? {},
|
|
153
|
+
icons: builtins.icons ?? {},
|
|
154
|
+
og: builtins.og ?? {},
|
|
132
155
|
command: options.command,
|
|
133
156
|
}),
|
|
134
157
|
];
|
|
135
158
|
}
|
|
136
159
|
|
|
137
|
-
function flowPlugin({ routerRoot, appEntry, command }) {
|
|
160
|
+
function flowPlugin({ routerRoot, appEntry, strictMode, command, accessibility }) {
|
|
138
161
|
let root = process.cwd();
|
|
139
162
|
let isProduction = false;
|
|
163
|
+
/**
|
|
164
|
+
* Whether this project has axe-core, so the audit has an engine.
|
|
165
|
+
*
|
|
166
|
+
* Answered once in `configResolved` rather than per document: it is a
|
|
167
|
+
* question about `node_modules`, the answer cannot change while the server
|
|
168
|
+
* runs without a restart anyway, and asking it per render would put a module
|
|
169
|
+
* resolution on the path of every page.
|
|
170
|
+
*/
|
|
171
|
+
let auditsPage = false;
|
|
140
172
|
let base = "/";
|
|
141
173
|
let appRoot = "";
|
|
142
174
|
let entryPath = "";
|
|
@@ -246,6 +278,10 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
246
278
|
config(userConfig, env) {
|
|
247
279
|
const projectRoot = path.resolve(userConfig.root ?? process.cwd());
|
|
248
280
|
isProduction = env.mode === "production" || env.command === "build";
|
|
281
|
+
// Decided here rather than in `configResolved`, because the answer has to
|
|
282
|
+
// reach `optimizeDeps.include` below and that is written in this hook.
|
|
283
|
+
auditsPage =
|
|
284
|
+
!isProduction && (accessibility?.devAudit ?? true) && auditAvailable(projectRoot);
|
|
249
285
|
return {
|
|
250
286
|
// uf serves HTML itself; there is no index.html to fall back to.
|
|
251
287
|
appType: "custom",
|
|
@@ -260,6 +296,16 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
260
296
|
"react/compiler-runtime",
|
|
261
297
|
"react-dom",
|
|
262
298
|
"react-dom/client",
|
|
299
|
+
// The accessibility audit's engine, when this project has one.
|
|
300
|
+
//
|
|
301
|
+
// Named up front rather than left to be discovered. axe-core is
|
|
302
|
+
// CommonJS, and the only thing that imports it is a *virtual*
|
|
303
|
+
// module reached from the document — so Vite would meet it for the
|
|
304
|
+
// first time after a page had already loaded, optimise it then, and
|
|
305
|
+
// reload the page it had just served. A full reload in the middle
|
|
306
|
+
// of the first render of every `uf dev` session is a high price for
|
|
307
|
+
// a feature whose whole manner is to be quiet.
|
|
308
|
+
...(auditsPage ? ["axe-core"] : []),
|
|
263
309
|
],
|
|
264
310
|
// uf's packages ship Flow. The dependency optimiser pre-bundles
|
|
265
311
|
// with a JavaScript parser and would reject every one of them.
|
|
@@ -287,6 +333,7 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
287
333
|
|
|
288
334
|
resolveId(id) {
|
|
289
335
|
if (id === RUNTIME_PUBLIC_PATH) return RUNTIME_RESOLVED_ID;
|
|
336
|
+
if (id === AUDIT_PUBLIC_PATH) return AUDIT_RESOLVED_ID;
|
|
290
337
|
if (VIRTUAL_IDS.has(id)) return resolved(id);
|
|
291
338
|
// A module's own stylesheet, which `transform` below asked for by
|
|
292
339
|
// importing this id. Returning it unchanged marks it resolved without
|
|
@@ -297,6 +344,7 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
297
344
|
|
|
298
345
|
load(id, loadOptions) {
|
|
299
346
|
if (id === RUNTIME_RESOLVED_ID) return refreshRuntimeSource();
|
|
347
|
+
if (id === AUDIT_RESOLVED_ID) return auditRuntimeSource(accessibility?.axe);
|
|
300
348
|
if (id === resolved(VIRTUAL.routes)) {
|
|
301
349
|
const table = scanRoutes(appRoot);
|
|
302
350
|
// The server renders every route, so the server's table is the whole
|
|
@@ -305,7 +353,12 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
305
353
|
if (isSsr(this, loadOptions)) return routesModuleSource(table);
|
|
306
354
|
return clientRoutesModule(table);
|
|
307
355
|
}
|
|
308
|
-
|
|
356
|
+
// Strict Mode belongs to the client entry and to development only: a
|
|
357
|
+
// build passes `false`, so the generated module is the one that existed
|
|
358
|
+
// before #516 and a visitor's browser renders once.
|
|
359
|
+
if (id === resolved(VIRTUAL.client)) {
|
|
360
|
+
return clientModuleSource(entryPath, { strictMode: strictMode && !isProduction });
|
|
361
|
+
}
|
|
309
362
|
if (id === resolved(VIRTUAL.server)) return serverModuleSource(entryPath);
|
|
310
363
|
// Only `virtual:uf/server` imports this, so it is only ever asked for in
|
|
311
364
|
// the server environment — but the table it carries is every callable
|
|
@@ -404,9 +457,26 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
404
457
|
}
|
|
405
458
|
},
|
|
406
459
|
|
|
460
|
+
// The two scripts a development document loads before its own, and
|
|
461
|
+
// nothing at all in a build — which is the whole of "the hook is out of a
|
|
462
|
+
// production build" (ubugeeei-prod/uf#503) and of "production does not run
|
|
463
|
+
// under Strict Mode" (#516, whose flag is generated into
|
|
464
|
+
// `virtual:uf/client` rather than injected here).
|
|
465
|
+
//
|
|
466
|
+
// DevTools first, and as a *classic* script rather than a module: React
|
|
467
|
+
// registers itself with `__REACT_DEVTOOLS_GLOBAL_HOOK__` while `react-dom`
|
|
468
|
+
// is evaluated and never again, so the hook has to exist before any module
|
|
469
|
+
// runs. A classic inline script runs while the parser is on it; a module
|
|
470
|
+
// waits for the document. `internal/devtools.js` has the rest of the
|
|
471
|
+
// argument, and the three conditions DevTools needs.
|
|
407
472
|
transformIndexHtml() {
|
|
408
473
|
if (isProduction) return [];
|
|
409
|
-
|
|
474
|
+
const tags = [
|
|
475
|
+
{
|
|
476
|
+
tag: "script",
|
|
477
|
+
children: devtoolsPreamble(),
|
|
478
|
+
injectTo: "head-prepend",
|
|
479
|
+
},
|
|
410
480
|
{
|
|
411
481
|
tag: "script",
|
|
412
482
|
attrs: { type: "module" },
|
|
@@ -414,6 +484,13 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
414
484
|
injectTo: "head-prepend",
|
|
415
485
|
},
|
|
416
486
|
];
|
|
487
|
+
// Only when the project has the engine. A tag pointing at a module that
|
|
488
|
+
// cannot resolve `axe-core` would be a red console on every page of a
|
|
489
|
+
// project that never asked for an audit, which is a worse default than
|
|
490
|
+
// no audit.
|
|
491
|
+
const audit = auditTag(base, auditsPage);
|
|
492
|
+
if (audit != null) tags.push(audit);
|
|
493
|
+
return tags;
|
|
417
494
|
},
|
|
418
495
|
|
|
419
496
|
configureServer(devServer) {
|
|
@@ -590,7 +667,7 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
590
667
|
// `POST`, and letting one try would turn a missing handler into
|
|
591
668
|
// a rendered page with a 200 rather than a 404.
|
|
592
669
|
//
|
|
593
|
-
// `
|
|
670
|
+
// `notADocumentBecause` is stricter than the production handler, which
|
|
594
671
|
// renders anything a static file did not answer, and the
|
|
595
672
|
// difference is Vite's chain: `/@id/…`, `/node_modules/…` and
|
|
596
673
|
// any path with an extension belong to the module server, and a
|
|
@@ -600,7 +677,15 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
600
677
|
// and the project's own not-found *page* under `uf preview` and
|
|
601
678
|
// `uf start` — a difference in the body of a 404 for a path that
|
|
602
679
|
// is an asset request in the first place.
|
|
603
|
-
|
|
680
|
+
const notDocument = notADocumentBecause(request);
|
|
681
|
+
if (notDocument === "accept") {
|
|
682
|
+
// Everything about this is a navigation except the header, and
|
|
683
|
+
// the path is one uf renders. Say so, rather than letting the
|
|
684
|
+
// chain below answer `Cannot GET /` in somebody else's words.
|
|
685
|
+
documentNeedsHtml(request, response);
|
|
686
|
+
return true;
|
|
687
|
+
}
|
|
688
|
+
if (notDocument != null) return false;
|
|
604
689
|
|
|
605
690
|
const result = await entry.render(
|
|
606
691
|
url,
|
|
@@ -700,16 +785,62 @@ async function importServerEntry(devServer) {
|
|
|
700
785
|
return devServer.ssrLoadModule(VIRTUAL.server);
|
|
701
786
|
}
|
|
702
787
|
|
|
703
|
-
|
|
704
|
-
|
|
788
|
+
/**
|
|
789
|
+
* Why `request` is not a document request, or `null` when it is one.
|
|
790
|
+
*
|
|
791
|
+
* A reason rather than a boolean because one of the four is worth saying out
|
|
792
|
+
* loud. Three of them mean the request belongs to somebody else — Vite's module
|
|
793
|
+
* server, a static file, or a method a page cannot answer — and handing it back
|
|
794
|
+
* is the whole point. The fourth means the client asked for a page uf would
|
|
795
|
+
* have rendered and did not say it accepts HTML, and `curl` is the client that
|
|
796
|
+
* does that. See ubugeeei-prod/uf#675.
|
|
797
|
+
*
|
|
798
|
+
* The extension test moved above the `accept` test so the two cannot be
|
|
799
|
+
* confused: `/favicon.svg` with `Accept: *\/*` is an asset, not a navigation
|
|
800
|
+
* with the wrong header, and still goes back to Vite's chain untouched.
|
|
801
|
+
*/
|
|
802
|
+
function notADocumentBecause(request) {
|
|
803
|
+
if (request.method !== "GET" && request.method !== "HEAD") return "method";
|
|
705
804
|
const url = request.url ?? "/";
|
|
706
|
-
if (url.startsWith("/@") || url.startsWith("/node_modules/")) return
|
|
707
|
-
const accept = request.headers.accept ?? "";
|
|
708
|
-
if (!accept.includes("text/html")) return false;
|
|
805
|
+
if (url.startsWith("/@") || url.startsWith("/node_modules/")) return "module-server";
|
|
709
806
|
const pathname = url.split("?")[0];
|
|
710
807
|
// A request for a file — `/favicon.svg`, `/assets/x.js` — that no static
|
|
711
808
|
// middleware answered is a 404, not a page.
|
|
712
|
-
|
|
809
|
+
if (/\.[a-z0-9]+$/i.test(pathname)) return "asset";
|
|
810
|
+
const accept = request.headers.accept ?? "";
|
|
811
|
+
if (!accept.includes("text/html")) return "accept";
|
|
812
|
+
return null;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* The 404 for a navigation that did not ask for HTML.
|
|
817
|
+
*
|
|
818
|
+
* uf's own words rather than the framework underneath saying `Cannot GET /` in
|
|
819
|
+
* its: the path is one uf would have rendered, and the only thing that stopped
|
|
820
|
+
* it is a header the reader cannot see from the terminal. Development only —
|
|
821
|
+
* this middleware is the dev server — and `uf preview` and `uf start` answer a
|
|
822
|
+
* 404 with nothing in it, as `docs/security.md` requires.
|
|
823
|
+
*
|
|
824
|
+
* The client's `Accept` is quoted back, so it is treated the way every other
|
|
825
|
+
* piece of foreign text here is treated: control characters removed and the
|
|
826
|
+
* length capped. A header is not a thing a terminal should be asked to run.
|
|
827
|
+
* See ubugeeei-prod/uf#649.
|
|
828
|
+
*/
|
|
829
|
+
function documentNeedsHtml(request, response) {
|
|
830
|
+
const accept = String(request.headers.accept ?? "")
|
|
831
|
+
// eslint-disable-next-line no-control-regex
|
|
832
|
+
.replace(/[\u0000-\u001f\u007f]/g, " ")
|
|
833
|
+
.slice(0, 80);
|
|
834
|
+
const path = String(request.url ?? "/").slice(0, 200);
|
|
835
|
+
const body =
|
|
836
|
+
`404 ${request.method} ${path}\n\n` +
|
|
837
|
+
"This path is a page, and a page is rendered for a request that accepts\n" +
|
|
838
|
+
`HTML. This request sent \`Accept: ${accept || "(none)"}\`.\n\n` +
|
|
839
|
+
` curl -H 'Accept: text/html' http://${request.headers.host ?? "localhost"}${path}\n\n` +
|
|
840
|
+
"A browser sends it; `curl` does not. Nothing is wrong with the route.\n";
|
|
841
|
+
response.statusCode = 404;
|
|
842
|
+
response.setHeader("content-type", "text/plain; charset=utf-8");
|
|
843
|
+
response.end(body);
|
|
713
844
|
}
|
|
714
845
|
|
|
715
846
|
/** The name of the environment variable that turns every finding back on. */
|