@uniflowed/vite 0.0.0-alpha.15 → 0.0.0-alpha.17
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 +77 -25
- package/index.js +112 -11
- package/internal/a11y-runtime.js +234 -0
- package/internal/a11y.js +100 -0
- package/internal/compile-assets.js +67 -0
- package/internal/serve.js +30 -0
- package/package.json +10 -4
package/driver.js
CHANGED
|
@@ -45,6 +45,7 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node
|
|
|
45
45
|
import path from "node:path";
|
|
46
46
|
import { pathToFileURL } from "node:url";
|
|
47
47
|
|
|
48
|
+
import { COMPILE_ASSETS_ID, compileAssetsPlugin } from "./internal/compile-assets.js";
|
|
48
49
|
import { emit, errorEvent, eventLogger } from "./internal/events.js";
|
|
49
50
|
import { loadUfConfig, projectConfig } from "./internal/config.js";
|
|
50
51
|
import { send, toRequest } from "./internal/http.js";
|
|
@@ -52,6 +53,7 @@ import { withProjectConfig } from "./merge.js";
|
|
|
52
53
|
import { VIRTUAL, scanRoutes } from "./internal/routes.js";
|
|
53
54
|
import {
|
|
54
55
|
assetsFromManifest,
|
|
56
|
+
createPrerenderGate,
|
|
55
57
|
createServeHandler,
|
|
56
58
|
loadBuild,
|
|
57
59
|
nodeListener,
|
|
@@ -343,6 +345,16 @@ function watchEnvFiles(server) {
|
|
|
343
345
|
* The static middleware still runs first, and that is deliberate rather than
|
|
344
346
|
* incidental; see `internal/serve.js` for why `uf start` orders itself the
|
|
345
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.
|
|
346
358
|
*/
|
|
347
359
|
async function preview() {
|
|
348
360
|
const { preview: startPreview } = await import("vite");
|
|
@@ -363,25 +375,53 @@ async function preview() {
|
|
|
363
375
|
serverDir: path.join(".uf", "build", "server"),
|
|
364
376
|
});
|
|
365
377
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
}
|
|
384
|
-
})
|
|
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));
|
|
385
425
|
}
|
|
386
426
|
|
|
387
427
|
const urls = server.resolvedUrls ?? { local: [], network: [] };
|
|
@@ -884,17 +924,19 @@ async function compile() {
|
|
|
884
924
|
emit("phase", { name: "standalone" });
|
|
885
925
|
|
|
886
926
|
// The entry is written to disk rather than served as another virtual module:
|
|
887
|
-
// it is generated per build (it
|
|
927
|
+
// it is generated per build (it bakes in this build's document), and a real
|
|
888
928
|
// file is the version a person can open when a compiled binary misbehaves.
|
|
889
929
|
const entry = path.join(bundleDir, "entry.js");
|
|
890
930
|
mkdirSync(bundleDir, { recursive: true });
|
|
891
|
-
|
|
892
|
-
|
|
931
|
+
writeFileSync(
|
|
932
|
+
entry,
|
|
933
|
+
entrySource(path.relative(root, assets), assetsFromManifest(readManifest(outDir))),
|
|
934
|
+
);
|
|
893
935
|
|
|
894
936
|
await vite.build({
|
|
895
937
|
...inline,
|
|
896
938
|
customLogger: eventLogger("warn"),
|
|
897
|
-
plugins: [...inline.plugins, nativeAddonGuard()],
|
|
939
|
+
plugins: [...inline.plugins, nativeAddonGuard(), compileAssetsPlugin(assets)],
|
|
898
940
|
ssr: { ...(inline.ssr ?? {}), noExternal: true },
|
|
899
941
|
build: {
|
|
900
942
|
...inline.build,
|
|
@@ -1315,8 +1357,14 @@ export const handler = createLambdaHandler({ handle, beginRequest, staticDir });
|
|
|
1315
1357
|
* bytes of `dist/`. The document's script and stylesheet URLs are baked in
|
|
1316
1358
|
* here because they come from the client manifest, which exists at this moment
|
|
1317
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.
|
|
1318
1366
|
*/
|
|
1319
|
-
function entrySource(
|
|
1367
|
+
function entrySource(assetsFile, document) {
|
|
1320
1368
|
// Not `await serve(...)` at the top level. uf parses that now
|
|
1321
1369
|
// (ubugeeei-prod/uf#204) and this entry is a module, so it would work;
|
|
1322
1370
|
// `.catch` is the better spelling regardless: a binary that cannot take its
|
|
@@ -1324,7 +1372,11 @@ function entrySource(assetsSpecifier, document) {
|
|
|
1324
1372
|
// unhandled rejection.
|
|
1325
1373
|
return `// Generated by \`uf build --compile\`. Not checked in, not edited.
|
|
1326
1374
|
import { serve } from "@uniflowed/server/standalone";
|
|
1327
|
-
|
|
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)};
|
|
1328
1380
|
import * as app from ${JSON.stringify(VIRTUAL.server)};
|
|
1329
1381
|
|
|
1330
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";
|
|
@@ -129,8 +136,16 @@ export default function uniflowed(options = {}) {
|
|
|
129
136
|
// client entry; see `flowPlugin`'s `load`. ubugeeei-prod/uf#516.
|
|
130
137
|
const strictMode = app.react?.strictMode !== false;
|
|
131
138
|
|
|
139
|
+
const accessibility = ufConfig.accessibility ?? {};
|
|
140
|
+
|
|
132
141
|
return [
|
|
133
|
-
flowPlugin({
|
|
142
|
+
flowPlugin({
|
|
143
|
+
routerRoot,
|
|
144
|
+
appEntry,
|
|
145
|
+
strictMode,
|
|
146
|
+
command: options.command,
|
|
147
|
+
accessibility,
|
|
148
|
+
}),
|
|
134
149
|
mdxPlugin(markdown),
|
|
135
150
|
assetPlugin({
|
|
136
151
|
images: builtins.images ?? {},
|
|
@@ -142,9 +157,18 @@ export default function uniflowed(options = {}) {
|
|
|
142
157
|
];
|
|
143
158
|
}
|
|
144
159
|
|
|
145
|
-
function flowPlugin({ routerRoot, appEntry, strictMode, command }) {
|
|
160
|
+
function flowPlugin({ routerRoot, appEntry, strictMode, command, accessibility }) {
|
|
146
161
|
let root = process.cwd();
|
|
147
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;
|
|
148
172
|
let base = "/";
|
|
149
173
|
let appRoot = "";
|
|
150
174
|
let entryPath = "";
|
|
@@ -254,6 +278,10 @@ function flowPlugin({ routerRoot, appEntry, strictMode, command }) {
|
|
|
254
278
|
config(userConfig, env) {
|
|
255
279
|
const projectRoot = path.resolve(userConfig.root ?? process.cwd());
|
|
256
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);
|
|
257
285
|
return {
|
|
258
286
|
// uf serves HTML itself; there is no index.html to fall back to.
|
|
259
287
|
appType: "custom",
|
|
@@ -268,6 +296,16 @@ function flowPlugin({ routerRoot, appEntry, strictMode, command }) {
|
|
|
268
296
|
"react/compiler-runtime",
|
|
269
297
|
"react-dom",
|
|
270
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"] : []),
|
|
271
309
|
],
|
|
272
310
|
// uf's packages ship Flow. The dependency optimiser pre-bundles
|
|
273
311
|
// with a JavaScript parser and would reject every one of them.
|
|
@@ -295,6 +333,7 @@ function flowPlugin({ routerRoot, appEntry, strictMode, command }) {
|
|
|
295
333
|
|
|
296
334
|
resolveId(id) {
|
|
297
335
|
if (id === RUNTIME_PUBLIC_PATH) return RUNTIME_RESOLVED_ID;
|
|
336
|
+
if (id === AUDIT_PUBLIC_PATH) return AUDIT_RESOLVED_ID;
|
|
298
337
|
if (VIRTUAL_IDS.has(id)) return resolved(id);
|
|
299
338
|
// A module's own stylesheet, which `transform` below asked for by
|
|
300
339
|
// importing this id. Returning it unchanged marks it resolved without
|
|
@@ -305,6 +344,7 @@ function flowPlugin({ routerRoot, appEntry, strictMode, command }) {
|
|
|
305
344
|
|
|
306
345
|
load(id, loadOptions) {
|
|
307
346
|
if (id === RUNTIME_RESOLVED_ID) return refreshRuntimeSource();
|
|
347
|
+
if (id === AUDIT_RESOLVED_ID) return auditRuntimeSource(accessibility?.axe);
|
|
308
348
|
if (id === resolved(VIRTUAL.routes)) {
|
|
309
349
|
const table = scanRoutes(appRoot);
|
|
310
350
|
// The server renders every route, so the server's table is the whole
|
|
@@ -431,7 +471,7 @@ function flowPlugin({ routerRoot, appEntry, strictMode, command }) {
|
|
|
431
471
|
// argument, and the three conditions DevTools needs.
|
|
432
472
|
transformIndexHtml() {
|
|
433
473
|
if (isProduction) return [];
|
|
434
|
-
|
|
474
|
+
const tags = [
|
|
435
475
|
{
|
|
436
476
|
tag: "script",
|
|
437
477
|
children: devtoolsPreamble(),
|
|
@@ -444,6 +484,13 @@ function flowPlugin({ routerRoot, appEntry, strictMode, command }) {
|
|
|
444
484
|
injectTo: "head-prepend",
|
|
445
485
|
},
|
|
446
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;
|
|
447
494
|
},
|
|
448
495
|
|
|
449
496
|
configureServer(devServer) {
|
|
@@ -620,7 +667,7 @@ function flowPlugin({ routerRoot, appEntry, strictMode, command }) {
|
|
|
620
667
|
// `POST`, and letting one try would turn a missing handler into
|
|
621
668
|
// a rendered page with a 200 rather than a 404.
|
|
622
669
|
//
|
|
623
|
-
// `
|
|
670
|
+
// `notADocumentBecause` is stricter than the production handler, which
|
|
624
671
|
// renders anything a static file did not answer, and the
|
|
625
672
|
// difference is Vite's chain: `/@id/…`, `/node_modules/…` and
|
|
626
673
|
// any path with an extension belong to the module server, and a
|
|
@@ -630,7 +677,15 @@ function flowPlugin({ routerRoot, appEntry, strictMode, command }) {
|
|
|
630
677
|
// and the project's own not-found *page* under `uf preview` and
|
|
631
678
|
// `uf start` — a difference in the body of a 404 for a path that
|
|
632
679
|
// is an asset request in the first place.
|
|
633
|
-
|
|
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;
|
|
634
689
|
|
|
635
690
|
const result = await entry.render(
|
|
636
691
|
url,
|
|
@@ -730,16 +785,62 @@ async function importServerEntry(devServer) {
|
|
|
730
785
|
return devServer.ssrLoadModule(VIRTUAL.server);
|
|
731
786
|
}
|
|
732
787
|
|
|
733
|
-
|
|
734
|
-
|
|
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";
|
|
735
804
|
const url = request.url ?? "/";
|
|
736
|
-
if (url.startsWith("/@") || url.startsWith("/node_modules/")) return
|
|
737
|
-
const accept = request.headers.accept ?? "";
|
|
738
|
-
if (!accept.includes("text/html")) return false;
|
|
805
|
+
if (url.startsWith("/@") || url.startsWith("/node_modules/")) return "module-server";
|
|
739
806
|
const pathname = url.split("?")[0];
|
|
740
807
|
// A request for a file — `/favicon.svg`, `/assets/x.js` — that no static
|
|
741
808
|
// middleware answered is a 404, not a page.
|
|
742
|
-
|
|
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);
|
|
743
844
|
}
|
|
744
845
|
|
|
745
846
|
/** The name of the environment variable that turns every finding back on. */
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// @noflow
|
|
2
|
+
//
|
|
3
|
+
// Plain JavaScript: this module is served to the browser as text, by
|
|
4
|
+
// `./a11y.js`'s virtual module, before any transform has run on it.
|
|
5
|
+
//
|
|
6
|
+
// The accessibility audit `uf dev` runs against the page on screen.
|
|
7
|
+
//
|
|
8
|
+
// The second half of ubugeeei-prod/uf#511, and the half that is worth more: a
|
|
9
|
+
// violation found while the component is being written is a violation somebody
|
|
10
|
+
// can fix in the same minute, and one found in CI is a pull request that goes
|
|
11
|
+
// red an hour later. Same engine and same rule set as
|
|
12
|
+
// `expect(el).toHaveNoAxeViolations()` — `uf.config.js`'s `accessibility.axe`
|
|
13
|
+
// is read once and given to both — so the two cannot disagree about what
|
|
14
|
+
// "accessible" means.
|
|
15
|
+
//
|
|
16
|
+
// # It reports to the terminal rather than drawing an overlay
|
|
17
|
+
//
|
|
18
|
+
// `internal/diagnostics.js` records the decision and the reason, from #583: an
|
|
19
|
+
// overlay is a rectangle drawn over the page, and a rectangle drawn over the
|
|
20
|
+
// page is the last thing an *accessibility* report should be. It would cover
|
|
21
|
+
// the markup it is complaining about, it would change the layout the audit
|
|
22
|
+
// just measured, and it would need to be accessible itself. So a finding goes
|
|
23
|
+
// down the channel that already exists, `POST /__uf/diagnostic`, and comes out
|
|
24
|
+
// in the terminal beside every other thing `uf dev` has to say.
|
|
25
|
+
//
|
|
26
|
+
// # When it runs
|
|
27
|
+
//
|
|
28
|
+
// Once when the page has settled, and again after the DOM stops changing.
|
|
29
|
+
// Not on every mutation: a React render is hundreds of them, and axe walks the
|
|
30
|
+
// whole subtree each time. The debounce is what makes it a background job
|
|
31
|
+
// rather than a stutter, and the audit is skipped outright while the tab is
|
|
32
|
+
// hidden — a page nobody is looking at has nothing to report about.
|
|
33
|
+
//
|
|
34
|
+
// Development only. `./a11y.js` injects this module from `transformIndexHtml`,
|
|
35
|
+
// which is guarded on `isProduction`, and the module is not reachable from any
|
|
36
|
+
// entry a build follows.
|
|
37
|
+
|
|
38
|
+
import axe from "axe-core";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Whether the engine is mid-run, for every audit in the process.
|
|
42
|
+
*
|
|
43
|
+
* Module scope because `axe` is a singleton and holds a single-run lock of its
|
|
44
|
+
* own: asking it for a second run while one is going answers "Axe is already
|
|
45
|
+
* running" rather than a result, and a per-page flag would not see the other
|
|
46
|
+
* page's run. One engine, one lock.
|
|
47
|
+
*/
|
|
48
|
+
let engineBusy = false;
|
|
49
|
+
|
|
50
|
+
/** Most violations one report names; the rest are counted. */
|
|
51
|
+
const MAX_VIOLATIONS_REPORTED = 8;
|
|
52
|
+
|
|
53
|
+
/** Longest excerpt of an element's markup a line quotes. */
|
|
54
|
+
const MAX_NODE_CHARS = 100;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Start auditing.
|
|
58
|
+
*
|
|
59
|
+
* `options` is generated by `./a11y.js` and carries the project's rule set,
|
|
60
|
+
* the endpoint to report to, and nothing a page could have chosen: everything
|
|
61
|
+
* here is decided by `uf.config.js` on the server before the module exists.
|
|
62
|
+
*/
|
|
63
|
+
export function start(options) {
|
|
64
|
+
const endpoint = options.endpoint;
|
|
65
|
+
// How long the DOM has to stop moving before an audit runs. Decided by
|
|
66
|
+
// `./a11y.js` and written into this module's last line rather than kept here
|
|
67
|
+
// as a constant, so there is one number rather than two — and so a test can
|
|
68
|
+
// drive the loop without waiting three quarters of a second per assertion.
|
|
69
|
+
const settleMs = options.settleMs;
|
|
70
|
+
const runOptions = axeOptions(options.axe ?? {});
|
|
71
|
+
const floor = options.axe?.minImpact ?? null;
|
|
72
|
+
let timer = null;
|
|
73
|
+
// Whether `stop` has been called. A run already in flight cannot be
|
|
74
|
+
// cancelled, but nothing after it should act as though the page is still
|
|
75
|
+
// being watched.
|
|
76
|
+
let stopped = false;
|
|
77
|
+
// What the last report said, so a re-render that changes nothing the audit
|
|
78
|
+
// cares about does not print the same block again. A page under active
|
|
79
|
+
// editing re-renders constantly; a terminal that repeats itself every time
|
|
80
|
+
// is a terminal nobody reads.
|
|
81
|
+
let reported = "";
|
|
82
|
+
|
|
83
|
+
const audit = async () => {
|
|
84
|
+
if (stopped || document.hidden) return;
|
|
85
|
+
if (engineBusy) {
|
|
86
|
+
// Not a run to skip — a run to come back for. The timer that brought us
|
|
87
|
+
// here cleared itself before calling, so returning without scheduling
|
|
88
|
+
// again loses the mutation for good: nothing is left pending, and the
|
|
89
|
+
// DOM it settled into never gets audited. Rescheduling rather than
|
|
90
|
+
// queueing a flag also means the wait is another settle, which is right
|
|
91
|
+
// — the tree may still be moving — and it is the same answer whether the
|
|
92
|
+
// engine is busy for this page or for another one.
|
|
93
|
+
schedule();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
engineBusy = true;
|
|
97
|
+
try {
|
|
98
|
+
const results = await axe.run(document, runOptions);
|
|
99
|
+
// The engine takes as long as it takes, and `stop` can land in the
|
|
100
|
+
// middle of it. A caller that has said it is done is not expecting one
|
|
101
|
+
// more report a second later.
|
|
102
|
+
if (stopped) return;
|
|
103
|
+
const violations = (results?.violations ?? []).filter((violation) =>
|
|
104
|
+
atOrAbove(violation.impact, floor),
|
|
105
|
+
);
|
|
106
|
+
const summary = violations.map((violation) => violation.id).join(",");
|
|
107
|
+
if (summary === reported) return;
|
|
108
|
+
reported = summary;
|
|
109
|
+
if (violations.length > 0) report(endpoint, violations);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
// An audit that throws is a bug in this file or in the engine, and it
|
|
112
|
+
// must not take the page with it: `uf dev` is for developing the
|
|
113
|
+
// application, not this. Reported once, at `info`, and then the run is
|
|
114
|
+
// over — `reported` is left as it was so the next settle tries again.
|
|
115
|
+
report(endpoint, null, error);
|
|
116
|
+
} finally {
|
|
117
|
+
engineBusy = false;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const schedule = () => {
|
|
122
|
+
if (timer != null) clearTimeout(timer);
|
|
123
|
+
timer = setTimeout(() => {
|
|
124
|
+
timer = null;
|
|
125
|
+
void audit();
|
|
126
|
+
}, settleMs);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const observer = new MutationObserver(schedule);
|
|
130
|
+
observer.observe(document.documentElement, {
|
|
131
|
+
subtree: true,
|
|
132
|
+
childList: true,
|
|
133
|
+
attributes: true,
|
|
134
|
+
});
|
|
135
|
+
document.addEventListener("visibilitychange", () => {
|
|
136
|
+
if (!document.hidden) schedule();
|
|
137
|
+
});
|
|
138
|
+
schedule();
|
|
139
|
+
|
|
140
|
+
// Returned so a caller that owns the page's lifetime can end the loop. The
|
|
141
|
+
// generated call in `./a11y.js` ignores it: a dev server's page is over when
|
|
142
|
+
// the document is, and nothing outlives that.
|
|
143
|
+
return () => {
|
|
144
|
+
stopped = true;
|
|
145
|
+
if (timer != null) clearTimeout(timer);
|
|
146
|
+
observer.disconnect();
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The project's rule set in axe's own vocabulary. */
|
|
151
|
+
function axeOptions(settings) {
|
|
152
|
+
const options = {};
|
|
153
|
+
const tags = settings.tags ?? [];
|
|
154
|
+
if (tags.length > 0) options.runOnly = { type: "tag", values: [...tags] };
|
|
155
|
+
const disabled = settings.disabledRules ?? [];
|
|
156
|
+
if (disabled.length > 0) {
|
|
157
|
+
const rules = {};
|
|
158
|
+
for (const rule of disabled) rules[rule] = { enabled: false };
|
|
159
|
+
options.rules = rules;
|
|
160
|
+
}
|
|
161
|
+
return options;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Impacts weakest first, so a floor is a comparison rather than a match. */
|
|
165
|
+
const IMPACTS = ["minor", "moderate", "serious", "critical"];
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Whether a violation clears the configured floor.
|
|
169
|
+
*
|
|
170
|
+
* A violation axe could not rate clears every floor: "we do not know how bad
|
|
171
|
+
* this is" is not a reason to hide it.
|
|
172
|
+
*/
|
|
173
|
+
function atOrAbove(impact, floor) {
|
|
174
|
+
if (floor == null || impact == null) return true;
|
|
175
|
+
const at = IMPACTS.indexOf(impact);
|
|
176
|
+
return at === -1 || at >= IMPACTS.indexOf(floor);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** One element, short enough to be one line of a terminal. */
|
|
180
|
+
function excerpt(html) {
|
|
181
|
+
const line = String(html ?? "")
|
|
182
|
+
.split("\n")[0]
|
|
183
|
+
.trim();
|
|
184
|
+
return line.length > MAX_NODE_CHARS ? `${line.slice(0, MAX_NODE_CHARS)}…` : line;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Send one report down the diagnostic channel.
|
|
189
|
+
*
|
|
190
|
+
* `keepalive` and a swallowed failure, because this is telemetry about the
|
|
191
|
+
* page rather than part of it: a dev server that has gone away must not turn
|
|
192
|
+
* into an unhandled rejection in the application being developed.
|
|
193
|
+
*/
|
|
194
|
+
function report(endpoint, violations, error) {
|
|
195
|
+
const body =
|
|
196
|
+
violations == null
|
|
197
|
+
? {
|
|
198
|
+
severity: "info",
|
|
199
|
+
message: `the accessibility audit could not run: ${String(error)}`,
|
|
200
|
+
}
|
|
201
|
+
: {
|
|
202
|
+
severity: "warn",
|
|
203
|
+
message: `${violations.length} accessibility ${
|
|
204
|
+
violations.length === 1 ? "violation" : "violations"
|
|
205
|
+
} on this page`,
|
|
206
|
+
detail: detailLines(violations),
|
|
207
|
+
};
|
|
208
|
+
void fetch(endpoint, {
|
|
209
|
+
method: "POST",
|
|
210
|
+
keepalive: true,
|
|
211
|
+
headers: { "content-type": "application/json" },
|
|
212
|
+
body: JSON.stringify({ ...body, url: window.location.href }),
|
|
213
|
+
}).then(
|
|
214
|
+
() => {},
|
|
215
|
+
() => {},
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** One line per rule, then the elements that broke it. */
|
|
220
|
+
function detailLines(violations) {
|
|
221
|
+
const lines = [];
|
|
222
|
+
for (const violation of violations.slice(0, MAX_VIOLATIONS_REPORTED)) {
|
|
223
|
+
const impact = violation.impact == null ? "" : ` (${violation.impact})`;
|
|
224
|
+
lines.push(`${violation.id}${impact} — ${violation.help}`);
|
|
225
|
+
for (const node of (violation.nodes ?? []).slice(0, 2)) {
|
|
226
|
+
lines.push(` ${excerpt(node.html)}`);
|
|
227
|
+
}
|
|
228
|
+
if (violation.helpUrl) lines.push(` ${violation.helpUrl}`);
|
|
229
|
+
}
|
|
230
|
+
if (violations.length > MAX_VIOLATIONS_REPORTED) {
|
|
231
|
+
lines.push(`…and ${violations.length - MAX_VIOLATIONS_REPORTED} more rules`);
|
|
232
|
+
}
|
|
233
|
+
return lines;
|
|
234
|
+
}
|
package/internal/a11y.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// @noflow
|
|
2
|
+
//
|
|
3
|
+
// Plain JavaScript: executed by the host that runs Vite, before any transform.
|
|
4
|
+
//
|
|
5
|
+
// Serving `./a11y-runtime.js` to the page `uf dev` renders.
|
|
6
|
+
//
|
|
7
|
+
// The same shape as `./refresh.js`, and deliberately so: a virtual module
|
|
8
|
+
// whose source is a file read from disk, and a tag added to every development
|
|
9
|
+
// document that imports it. Two mechanisms for "a module uf injects into the
|
|
10
|
+
// page" would be two things to keep working.
|
|
11
|
+
//
|
|
12
|
+
// What is different is the gate. Fast Refresh is always there; this is not.
|
|
13
|
+
// axe-core is an *optional* dependency — uf does not make a project that never
|
|
14
|
+
// audits carry a megabyte of rule definitions — so the injection is decided by
|
|
15
|
+
// asking the project's own `node_modules` whether the engine is installed. The
|
|
16
|
+
// question is answered on the server, once, before anything is injected, which
|
|
17
|
+
// is why a project without the engine gets no script, no failed import and no
|
|
18
|
+
// error in a console it did not open.
|
|
19
|
+
|
|
20
|
+
import { createRequire } from "node:module";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
import { readFileSync } from "node:fs";
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
24
|
+
|
|
25
|
+
import { DIAGNOSTIC_ENDPOINT } from "./diagnostics.js";
|
|
26
|
+
|
|
27
|
+
/** Public URL the audit runtime is served from. */
|
|
28
|
+
export const AUDIT_PUBLIC_PATH = "/@uf-a11y";
|
|
29
|
+
|
|
30
|
+
/** The resolved id Vite hands back for it. */
|
|
31
|
+
export const AUDIT_RESOLVED_ID = "\0uf:a11y-audit";
|
|
32
|
+
|
|
33
|
+
const RUNTIME_SOURCE_PATH = fileURLToPath(new URL("./a11y-runtime.js", import.meta.url));
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* How long the DOM has to stop moving before an audit runs, in milliseconds.
|
|
37
|
+
*
|
|
38
|
+
* A React render is hundreds of mutations and axe walks the whole subtree, so
|
|
39
|
+
* without a window this would be a stutter rather than a background job. Long
|
|
40
|
+
* enough to sit out a render and short enough that somebody who has just saved
|
|
41
|
+
* a file hears about it while they are still looking at the page.
|
|
42
|
+
*/
|
|
43
|
+
export const SETTLE_MS = 750;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Whether the project has axe-core.
|
|
47
|
+
*
|
|
48
|
+
* Resolved from the project root rather than from this package, because the
|
|
49
|
+
* dependency is the *project's* — `@uniflowed/vite` declares it as an optional
|
|
50
|
+
* peer, so it is installed beside the application and not beside the plugin.
|
|
51
|
+
*
|
|
52
|
+
* A failure to resolve is the ordinary answer, not an error: "no engine" is
|
|
53
|
+
* what most projects will say, and it is the reason this function exists.
|
|
54
|
+
*/
|
|
55
|
+
export function auditAvailable(root) {
|
|
56
|
+
try {
|
|
57
|
+
createRequire(path.join(root, "package.json")).resolve("axe-core");
|
|
58
|
+
return true;
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The audit module's source, with the project's settings written into its
|
|
66
|
+
* last line.
|
|
67
|
+
*
|
|
68
|
+
* Generated rather than passed at runtime because the module is a *module*:
|
|
69
|
+
* there is nowhere for a caller to hand it arguments, and a global would be a
|
|
70
|
+
* second name to agree on. `JSON.stringify` of a value uf built from its own
|
|
71
|
+
* config — never from anything a page said — is what goes in.
|
|
72
|
+
*/
|
|
73
|
+
export function auditRuntimeSource(settings) {
|
|
74
|
+
const options = {
|
|
75
|
+
endpoint: DIAGNOSTIC_ENDPOINT,
|
|
76
|
+
settleMs: SETTLE_MS,
|
|
77
|
+
axe: {
|
|
78
|
+
tags: settings?.tags ?? [],
|
|
79
|
+
disabledRules: settings?.disabledRules ?? [],
|
|
80
|
+
minImpact: settings?.minImpact ?? null,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
return `${readFileSync(RUNTIME_SOURCE_PATH, "utf8")}\nstart(${JSON.stringify(options)});\n`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The tag that loads it, or `null` when this project has no engine.
|
|
88
|
+
*
|
|
89
|
+
* `injectTo: "body"` rather than the head: the audit reads the rendered tree,
|
|
90
|
+
* so there is nothing for it to do until there is one, and a script in the
|
|
91
|
+
* head would only sit through the same wait with the parser stopped behind it.
|
|
92
|
+
*/
|
|
93
|
+
export function auditTag(base, available) {
|
|
94
|
+
if (!available) return null;
|
|
95
|
+
return {
|
|
96
|
+
tag: "script",
|
|
97
|
+
attrs: { type: "module", src: `${base}${AUDIT_PUBLIC_PATH.slice(1)}` },
|
|
98
|
+
injectTo: "body",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// @noflow
|
|
2
|
+
//
|
|
3
|
+
// Plain JavaScript: executed by the host that runs Vite, before any transform.
|
|
4
|
+
//
|
|
5
|
+
// The embedded copy of `dist/`, handed to the bundler as a virtual module.
|
|
6
|
+
//
|
|
7
|
+
// `uf build --compile` puts everything `uf build` wrote inside the binary, and
|
|
8
|
+
// the way it travels there is one generated module: `uf_bundle::embed` writes
|
|
9
|
+
// `export const assets = JSON.parse("…")` into `.uf/build/compile/assets.js`,
|
|
10
|
+
// where the string is base64 of every file in the output directory. For the
|
|
11
|
+
// docs site that is eight megabytes; for a project with images it is tens.
|
|
12
|
+
//
|
|
13
|
+
// # Why it does not enter the graph as the file it is
|
|
14
|
+
//
|
|
15
|
+
// Because it is uf's own data, and the module graph is where uf's *source*
|
|
16
|
+
// transform is. `crates/uf_transform` refuses a source over
|
|
17
|
+
// `MAX_SOURCE_BYTES` — 8 MiB, a ceiling that bounds the parser against a file
|
|
18
|
+
// nobody meant to compile — and `.uf/build/compile/assets.js` is a file
|
|
19
|
+
// nobody wrote at all: its size is the size of the *build output*, which has
|
|
20
|
+
// no relationship to the size of any source file and grows every time a page
|
|
21
|
+
// is added. A site that crossed the line failed with
|
|
22
|
+
//
|
|
23
|
+
// TransformError: source is 8442536 bytes, over the 8388608 byte ceiling
|
|
24
|
+
// [plugin uf:flow] docs/.uf/build/compile/assets.js
|
|
25
|
+
//
|
|
26
|
+
// which names a file the project does not own, for a limit it did not break,
|
|
27
|
+
// at the end of a build that had already succeeded.
|
|
28
|
+
//
|
|
29
|
+
// So the payload enters the graph the way a build tool's own modules do,
|
|
30
|
+
// under a NUL-prefixed id. `@uniflowed/host/transform`'s `isFlowModule`
|
|
31
|
+
// declines those by name — "a build tool synthesises modules of its own", the
|
|
32
|
+
// first sentence of its contract — so the bytes go from disk to the bundler
|
|
33
|
+
// without being parsed as Flow on the way. Raising the ceiling instead would
|
|
34
|
+
// have weakened the one thing it is for, on every real source file, to make
|
|
35
|
+
// room for a file that is not source.
|
|
36
|
+
//
|
|
37
|
+
// The file on disk is still written and still what is served, so a person
|
|
38
|
+
// debugging a compiled binary can open the thing that went into it. What
|
|
39
|
+
// changed is only which name the bundler reaches it by.
|
|
40
|
+
|
|
41
|
+
import { readFileSync } from "node:fs";
|
|
42
|
+
|
|
43
|
+
/** What the generated entry imports. */
|
|
44
|
+
export const COMPILE_ASSETS_ID = "virtual:uf/compile-assets";
|
|
45
|
+
|
|
46
|
+
/** The resolved id Vite hands back for it. */
|
|
47
|
+
export const COMPILE_ASSETS_RESOLVED_ID = `\0${COMPILE_ASSETS_ID}`;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Serve `file` — `uf_bundle::embed`'s output — as [`COMPILE_ASSETS_ID`].
|
|
51
|
+
*
|
|
52
|
+
* Read at `load` rather than when the plugin is made, so a build that fails
|
|
53
|
+
* before it reaches the entry never pays for eight megabytes of string, and
|
|
54
|
+
* so the bytes are the ones on disk at the moment the bundler asked for them
|
|
55
|
+
* rather than at the moment the plugin list was assembled.
|
|
56
|
+
*/
|
|
57
|
+
export function compileAssetsPlugin(file) {
|
|
58
|
+
return {
|
|
59
|
+
name: "uf:compile-assets",
|
|
60
|
+
resolveId(id) {
|
|
61
|
+
return id === COMPILE_ASSETS_ID ? COMPILE_ASSETS_RESOLVED_ID : null;
|
|
62
|
+
},
|
|
63
|
+
load(id) {
|
|
64
|
+
return id === COMPILE_ASSETS_RESOLVED_ID ? readFileSync(file, "utf8") : null;
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
package/internal/serve.js
CHANGED
|
@@ -362,6 +362,36 @@ export function createServeHandler({ entry, assets, distDir, cache }) {
|
|
|
362
362
|
};
|
|
363
363
|
}
|
|
364
364
|
|
|
365
|
+
/**
|
|
366
|
+
* Whether a file server may answer this request, or uf has to go first.
|
|
367
|
+
*
|
|
368
|
+
* `@uniflowed/server`'s `prerenderedMayAnswer`, reached the same way
|
|
369
|
+
* `createStaticHandler` is. It exists out here because of the one request
|
|
370
|
+
* `uf preview` may not leave to Vite: `createServeHandler` above is mounted
|
|
371
|
+
* *behind* Vite's static middleware, which is fine for everything except a
|
|
372
|
+
* request carrying the draft cookie. `createStaticHandler` declines a
|
|
373
|
+
* prerendered document for such a request and, under `uf preview`, never sees
|
|
374
|
+
* it — so draft mode appeared to be off there while it worked under `uf dev`
|
|
375
|
+
* and `uf start`. That is ubugeeei-prod/uf#620.
|
|
376
|
+
*
|
|
377
|
+
* `driver.js` asks this in a middleware mounted in *front* of Vite's, and
|
|
378
|
+
* mounts `createServeHandler` behind it for the answer, so a draft request
|
|
379
|
+
* goes through the same handler `uf start` uses and gets the same answer —
|
|
380
|
+
* including its stylesheets and chunks, which that handler still serves off
|
|
381
|
+
* disk. Every other request is untouched and Vite's file middleware runs as
|
|
382
|
+
* before.
|
|
383
|
+
*
|
|
384
|
+
* A gate rather than a second handler, because the caller has a request
|
|
385
|
+
* lifecycle to open and must not open one for a request it is about to hand
|
|
386
|
+
* on.
|
|
387
|
+
*/
|
|
388
|
+
export function createPrerenderGate() {
|
|
389
|
+
const ready = deployment().then(({ prerenderedMayAnswer }) => prerenderedMayAnswer);
|
|
390
|
+
return async function prerenderedMayAnswer(cookieHeader) {
|
|
391
|
+
return (await ready)(cookieHeader ?? null);
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
365
395
|
/**
|
|
366
396
|
* A `Request`/`Response` handler as a Node request listener.
|
|
367
397
|
*
|
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.17",
|
|
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.17",
|
|
37
|
+
"@uniflowed/server": "0.0.0-alpha.17",
|
|
38
38
|
"rehype-slug": "^6.0.0",
|
|
39
39
|
"remark-frontmatter": "^5.0.0",
|
|
40
40
|
"remark-gfm": "^4.0.1",
|
|
@@ -44,6 +44,12 @@
|
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"react": ">=19",
|
|
47
|
-
"react-dom": ">=19"
|
|
47
|
+
"react-dom": ">=19",
|
|
48
|
+
"axe-core": ">=4"
|
|
49
|
+
},
|
|
50
|
+
"peerDependenciesMeta": {
|
|
51
|
+
"axe-core": {
|
|
52
|
+
"optional": true
|
|
53
|
+
}
|
|
48
54
|
}
|
|
49
55
|
}
|