@uniflowed/vite 0.2.0 → 0.3.0
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 +52 -0
- package/index.js +29 -22
- package/internal/barrel-imports.js +50 -125
- package/internal/error-boundaries.js +157 -0
- package/internal/flight.js +35 -11
- package/internal/http.js +60 -4
- package/package.json +5 -5
package/driver.js
CHANGED
|
@@ -535,6 +535,8 @@ async function preview() {
|
|
|
535
535
|
previewServer.middlewares.use((request, response, next) => {
|
|
536
536
|
if ((request.url ?? "").split("?")[0].endsWith("/__uf.flight")) {
|
|
537
537
|
response.setHeader("content-type", "text/x-component");
|
|
538
|
+
// As every other door answers a payload: never sniffed into a document.
|
|
539
|
+
response.setHeader("x-content-type-options", "nosniff");
|
|
538
540
|
}
|
|
539
541
|
if ((request.url ?? "").split("?")[0] === "/.well-known/apple-app-site-association") {
|
|
540
542
|
response.setHeader("content-type", "application/json; charset=utf-8");
|
|
@@ -1512,6 +1514,28 @@ const ADAPTERS = {
|
|
|
1512
1514
|
// partially is refused by name; see [`deploy`].
|
|
1513
1515
|
streams: false,
|
|
1514
1516
|
},
|
|
1517
|
+
// Vercel's Build Output API. `uf` writes the directory layout and the two
|
|
1518
|
+
// JSON files (`config.json`, `.vc-config.json`); this links the function's
|
|
1519
|
+
// `index.js`, a Node.js function over the same `handler.js`. Node's export
|
|
1520
|
+
// conditions, like `node`: it is Node that runs it.
|
|
1521
|
+
vercel: {
|
|
1522
|
+
entries: (document, cache, build, _schedules, regeneration, images, partial) => ({
|
|
1523
|
+
handler: handlerEntrySource(
|
|
1524
|
+
document,
|
|
1525
|
+
cache,
|
|
1526
|
+
VERCEL_CAPABILITIES,
|
|
1527
|
+
build,
|
|
1528
|
+
regeneration,
|
|
1529
|
+
images,
|
|
1530
|
+
partial,
|
|
1531
|
+
),
|
|
1532
|
+
index: vercelEntrySource("./handler.js"),
|
|
1533
|
+
}),
|
|
1534
|
+
// A function instance's memory and `/tmp` go with the instance, as a
|
|
1535
|
+
// Lambda's do, so a build that regenerates pages names a provider module
|
|
1536
|
+
// in `rendering.cache.store` or is refused by name; see [`deploy`].
|
|
1537
|
+
regenerationStore: null,
|
|
1538
|
+
},
|
|
1515
1539
|
};
|
|
1516
1540
|
|
|
1517
1541
|
/**
|
|
@@ -1536,6 +1560,7 @@ const BUN_CAPABILITIES = { module: "@uniflowed/server/bun", name: "bunCapabiliti
|
|
|
1536
1560
|
const DENO_CAPABILITIES = { module: "@uniflowed/server/deno", name: "denoCapabilities" };
|
|
1537
1561
|
const EDGE_CAPABILITIES = { module: "@uniflowed/server/edge", name: "edgeCapabilities" };
|
|
1538
1562
|
const SERVERLESS_CAPABILITIES = { module: "@uniflowed/server/lambda", name: "lambdaCapabilities" };
|
|
1563
|
+
const VERCEL_CAPABILITIES = { module: "@uniflowed/server/vercel", name: "vercelCapabilities" };
|
|
1539
1564
|
|
|
1540
1565
|
/**
|
|
1541
1566
|
* Link the application into a directory that can be copied, for
|
|
@@ -2348,6 +2373,33 @@ export default {
|
|
|
2348
2373
|
`;
|
|
2349
2374
|
}
|
|
2350
2375
|
|
|
2376
|
+
/**
|
|
2377
|
+
* The source of `index.js`: the Vercel Node.js function around that handler.
|
|
2378
|
+
*
|
|
2379
|
+
* Its default export is what Vercel's Node.js launcher calls with Node's
|
|
2380
|
+
* request and response; `@uniflowed/server/vercel` says what it does with
|
|
2381
|
+
* them. The static directory is resolved from this file, for the reason
|
|
2382
|
+
* [`nodeEntrySource`] gives.
|
|
2383
|
+
*/
|
|
2384
|
+
function vercelEntrySource(handlerSpecifier) {
|
|
2385
|
+
return `// Generated by \`uf build --adapter vercel\`. Not checked in, not edited.
|
|
2386
|
+
import path from "node:path";
|
|
2387
|
+
import { fileURLToPath } from "node:url";
|
|
2388
|
+
|
|
2389
|
+
import { createVercelHandler } from "@uniflowed/server/vercel";
|
|
2390
|
+
|
|
2391
|
+
// \`beginRequest\` comes from the handler beside this file rather than from
|
|
2392
|
+
// \`@uniflowed/server/vercel\` above, because the request has to be established
|
|
2393
|
+
// in the storage the *application* reads, which is the copy bundled into
|
|
2394
|
+
// \`handler.js\`. See ubugeeei-prod/uf#389.
|
|
2395
|
+
import { beginRequest, fetch as handle, routing } from ${JSON.stringify(handlerSpecifier)};
|
|
2396
|
+
|
|
2397
|
+
const staticDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "static");
|
|
2398
|
+
|
|
2399
|
+
export default createVercelHandler({ handle, beginRequest, staticDir, routing });
|
|
2400
|
+
`;
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2351
2403
|
/**
|
|
2352
2404
|
* The source of `lambda.js`: the AWS Lambda entry around that handler.
|
|
2353
2405
|
*
|
package/index.js
CHANGED
|
@@ -62,7 +62,8 @@ import {
|
|
|
62
62
|
auditTag,
|
|
63
63
|
} from "./internal/a11y.js";
|
|
64
64
|
import { assetPlugin } from "./internal/assets.js";
|
|
65
|
-
import { barrelImportsPlugin
|
|
65
|
+
import { barrelImportsPlugin } from "./internal/barrel-imports.js";
|
|
66
|
+
import { refuseServerErrorBoundaries } from "./internal/error-boundaries.js";
|
|
66
67
|
import { emit, reportRenderError, errorEvent } from "./internal/events.js";
|
|
67
68
|
import remarkFrontmatterExport from "./internal/frontmatter.js";
|
|
68
69
|
import { highlightPlugin } from "./internal/highlight.js";
|
|
@@ -579,6 +580,11 @@ function flowPlugin({
|
|
|
579
580
|
// `react-server` for nothing. The ssr graph gets exactly those two,
|
|
580
581
|
// because every route it renders reaches it as a payload.
|
|
581
582
|
if (flightState != null && this.environment?.name === RSC_ENVIRONMENT) {
|
|
583
|
+
// The graph that renders routes is where a boundary has to be a
|
|
584
|
+
// client reference, so this is where one that is not fails — the
|
|
585
|
+
// build, and `uf dev`'s table — rather than the first page that
|
|
586
|
+
// throws in production. See `./internal/error-boundaries.js`.
|
|
587
|
+
refuseServerErrorBoundaries(table, root);
|
|
582
588
|
return routesModuleSource({ ...table, handlers: [], middleware: [] });
|
|
583
589
|
}
|
|
584
590
|
if (flightState != null && isSsr(this, loadOptions)) {
|
|
@@ -620,17 +626,11 @@ function flowPlugin({
|
|
|
620
626
|
if (id === resolved(VIRTUAL.server)) {
|
|
621
627
|
return flightState == null
|
|
622
628
|
? serverModuleSource(entryPath, routing, instrumentationFile(appRoot))
|
|
623
|
-
: flightServerSource(
|
|
624
|
-
entryPath,
|
|
625
|
-
VIRTUAL.routes,
|
|
626
|
-
VIRTUAL.actions,
|
|
627
|
-
routing,
|
|
628
|
-
instrumentationFile(appRoot),
|
|
629
|
-
);
|
|
629
|
+
: flightServerSource(entryPath, VIRTUAL.routes, routing, instrumentationFile(appRoot));
|
|
630
630
|
}
|
|
631
631
|
if (flightState != null) {
|
|
632
632
|
if (id === resolved(FLIGHT_VIRTUAL.entry))
|
|
633
|
-
return rscEntrySource(VIRTUAL.routes, routing, flightState.deployment);
|
|
633
|
+
return rscEntrySource(VIRTUAL.routes, routing, flightState.deployment, VIRTUAL.actions);
|
|
634
634
|
if (id === resolved(FLIGHT_VIRTUAL.compilerRuntime)) return compilerRuntimeSource();
|
|
635
635
|
if (id === resolved(FLIGHT_VIRTUAL.bridge)) {
|
|
636
636
|
if (server != null) return devBridgeSource();
|
|
@@ -651,10 +651,11 @@ function flowPlugin({
|
|
|
651
651
|
: builtReferencesSource(flightState.chunkUrls);
|
|
652
652
|
}
|
|
653
653
|
}
|
|
654
|
-
// Only `virtual:uf/
|
|
655
|
-
//
|
|
656
|
-
//
|
|
657
|
-
// it
|
|
654
|
+
// Only the server entries import this — `virtual:uf/rsc` under React
|
|
655
|
+
// Server Components, `virtual:uf/server` otherwise — so it is only ever
|
|
656
|
+
// asked for in a server environment. But the table it carries is every
|
|
657
|
+
// callable endpoint of the build, so it is worth saying that a browser
|
|
658
|
+
// asking for it gets nothing rather than getting the list.
|
|
658
659
|
if (id === resolved(VIRTUAL.actions)) {
|
|
659
660
|
if (!isSsr(this, loadOptions))
|
|
660
661
|
return "export const actions = [];\nexport default actions;\n";
|
|
@@ -705,9 +706,7 @@ function flowPlugin({
|
|
|
705
706
|
},
|
|
706
707
|
|
|
707
708
|
async transform(code, id, transformOptions) {
|
|
708
|
-
|
|
709
|
-
// source: `uf:barrel-imports` generates it as JavaScript.
|
|
710
|
-
if (!isFlowModule(id) || namespaceViewOf(id) != null) return null;
|
|
709
|
+
if (!isFlowModule(id)) return null;
|
|
711
710
|
// What an earlier pass of this build wrote under `.uf/build/` — the ssr
|
|
712
711
|
// pass imports the rsc graph's bundle — is already this transform's
|
|
713
712
|
// output. See `isCompiledOutput`.
|
|
@@ -950,12 +949,20 @@ function flowPlugin({
|
|
|
950
949
|
// depend on a clock.
|
|
951
950
|
forgetActions();
|
|
952
951
|
const nextActions = actionTables().modules;
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
const
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
952
|
+
// The action table and its modules live in the rsc graph too, where
|
|
953
|
+
// the endpoint is built (#1469), so that graph forgets them as well.
|
|
954
|
+
const graphs = [
|
|
955
|
+
devServer.moduleGraph,
|
|
956
|
+
devServer.environments?.[RSC_ENVIRONMENT]?.moduleGraph,
|
|
957
|
+
].filter((graph) => graph != null);
|
|
958
|
+
for (const graph of graphs) {
|
|
959
|
+
invalidateActionModules(graph, previousActions);
|
|
960
|
+
invalidateActionModules(graph, nextActions);
|
|
961
|
+
const routes = graph.getModuleById(resolved(VIRTUAL.routes));
|
|
962
|
+
if (routes) graph.invalidateModule(routes);
|
|
963
|
+
const actions = graph.getModuleById(resolved(VIRTUAL.actions));
|
|
964
|
+
if (actions) graph.invalidateModule(actions);
|
|
965
|
+
}
|
|
959
966
|
devServer.ws.send({ type: "full-reload", path: "*" });
|
|
960
967
|
};
|
|
961
968
|
devServer.watcher.on("add", onManifest);
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
// # Read off the barrel the project resolves
|
|
31
31
|
//
|
|
32
32
|
// Which file defines a name is read from the barrel itself: its static
|
|
33
|
-
// `import { A } from "./a.js"
|
|
34
|
-
//
|
|
35
|
-
//
|
|
33
|
+
// `import { A } from "./a.js"`, `export { A } from "./a.js"` and
|
|
34
|
+
// `export * as A from "./a.js"` statements. Not a table kept here, because the
|
|
35
|
+
// barrel a project installed is the one whose names count.
|
|
36
36
|
//
|
|
37
37
|
// A name that reading cannot place stays an import from the barrel, and so does
|
|
38
38
|
// every form that binds no name — `import * as ui`, `export * from` and
|
|
@@ -51,12 +51,20 @@
|
|
|
51
51
|
//
|
|
52
52
|
// # Namespaces
|
|
53
53
|
//
|
|
54
|
-
// `Dialog` is
|
|
55
|
-
//
|
|
56
|
-
// import
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
54
|
+
// `Dialog` is `dialog.js` itself: the barrel says `export * as Dialog from
|
|
55
|
+
// "./dialog.js"` (ubugeeei-prod/uf#1453), so `import { Dialog } from
|
|
56
|
+
// "@uniflowed/ui"` becomes `import * as Dialog from ".../dialog.js"` and
|
|
57
|
+
// `export { Dialog as Modal } from "@uniflowed/ui"` becomes
|
|
58
|
+
// `export * as Modal from ".../dialog.js"`. A module namespace is the same object
|
|
59
|
+
// however many importers ask for it, so nothing is generated to stand in for
|
|
60
|
+
// it. `ContextMenu` spans two modules and is still one: `context-menu.js`
|
|
61
|
+
// re-exports `Menu`'s parts, and importing it reaches `menu.js` through that.
|
|
62
|
+
//
|
|
63
|
+
// Until #1453 the barrel built each namespace as an object literal, and a
|
|
64
|
+
// generated `index.js?uf-namespace=Dialog` module rebuilt the object for an
|
|
65
|
+
// importer. That form is no longer read: `@uniflowed/vite` and `@uniflowed/ui`
|
|
66
|
+
// are released together, and an object literal the reading meets now is
|
|
67
|
+
// `OPAQUE` — left on the barrel, which costs size and never correctness.
|
|
60
68
|
|
|
61
69
|
import { readFileSync, statSync } from "node:fs";
|
|
62
70
|
import path from "node:path";
|
|
@@ -66,9 +74,6 @@ import { normalizePath, parseAst } from "vite";
|
|
|
66
74
|
/** The packages whose barrel imports are rewritten. */
|
|
67
75
|
export const BARREL_PACKAGES = Object.freeze(["@uniflowed/ui"]);
|
|
68
76
|
|
|
69
|
-
/** The query that makes a barrel's path a view of one of its namespaces. */
|
|
70
|
-
export const NAMESPACE_QUERY = "uf-namespace";
|
|
71
|
-
|
|
72
77
|
/** The module kinds whose imports are read, once earlier plugins made them JavaScript. */
|
|
73
78
|
const SCRIPT = /\.(?:[cm]?[jt]sx?|mdx)$/;
|
|
74
79
|
|
|
@@ -93,7 +98,7 @@ export function barrelImportsPlugin() {
|
|
|
93
98
|
name: "uf:barrel-imports",
|
|
94
99
|
|
|
95
100
|
async transform(code, id) {
|
|
96
|
-
if (id.startsWith("\0")
|
|
101
|
+
if (id.startsWith("\0")) return null;
|
|
97
102
|
if (!BARREL_PACKAGES.some((name) => code.includes(name))) return null;
|
|
98
103
|
if (!SCRIPT.test(cleanId(id))) return null;
|
|
99
104
|
let program;
|
|
@@ -134,29 +139,9 @@ export function barrelImportsPlugin() {
|
|
|
134
139
|
if (edits.length === 0) return null;
|
|
135
140
|
return { code: applyEdits(code, edits), map: null };
|
|
136
141
|
},
|
|
137
|
-
|
|
138
|
-
load(id) {
|
|
139
|
-
const view = namespaceViewOf(id);
|
|
140
|
-
if (view == null) return null;
|
|
141
|
-
return namespaceViewSource(readBarrel(readings, view.file).exports, view.file, view.name);
|
|
142
|
-
},
|
|
143
142
|
};
|
|
144
143
|
}
|
|
145
144
|
|
|
146
|
-
/**
|
|
147
|
-
* The barrel and the namespace a view module stands for, or `null` for any
|
|
148
|
-
* other id.
|
|
149
|
-
*
|
|
150
|
-
* `uf:flow` asks too: a view has the barrel's path and extension, and is not
|
|
151
|
-
* the barrel's Flow source.
|
|
152
|
-
*/
|
|
153
|
-
export function namespaceViewOf(id) {
|
|
154
|
-
const at = id.indexOf("?");
|
|
155
|
-
if (at === -1 || id.startsWith("\0")) return null;
|
|
156
|
-
const name = new URLSearchParams(id.slice(at + 1)).get(NAMESPACE_QUERY);
|
|
157
|
-
return name == null || name === "" ? null : { file: id.slice(0, at), name };
|
|
158
|
-
}
|
|
159
|
-
|
|
160
145
|
/**
|
|
161
146
|
* Where each name a barrel exports is defined, read from its source.
|
|
162
147
|
*
|
|
@@ -169,8 +154,8 @@ export function namespaceViewOf(id) {
|
|
|
169
154
|
*
|
|
170
155
|
* * `{ kind: "binding", file, name }` — an export of another module, passed
|
|
171
156
|
* through under this name;
|
|
172
|
-
* * `{ kind: "
|
|
173
|
-
*
|
|
157
|
+
* * `{ kind: "module", file }` — another module whole, re-exported as a
|
|
158
|
+
* namespace with `export * as Name from "./file.js"`;
|
|
174
159
|
* * `OPAQUE` — anything else, which stays an import from the barrel.
|
|
175
160
|
*
|
|
176
161
|
* @param {string} source
|
|
@@ -185,10 +170,9 @@ export function barrelExports(source, file) {
|
|
|
185
170
|
? normalizePath(path.join(directory, node.value))
|
|
186
171
|
: null;
|
|
187
172
|
|
|
188
|
-
// Every binding an import made,
|
|
189
|
-
//
|
|
173
|
+
// Every binding an import made, before any export is read: `export { A }`
|
|
174
|
+
// may come before the `import` it names.
|
|
190
175
|
const bindings = new Map();
|
|
191
|
-
const objects = new Map();
|
|
192
176
|
for (const node of program.body) {
|
|
193
177
|
if (node.type === "ImportDeclaration" && node.importKind !== "type") {
|
|
194
178
|
const from = fileOf(node.source);
|
|
@@ -206,21 +190,11 @@ export function barrelExports(source, file) {
|
|
|
206
190
|
);
|
|
207
191
|
}
|
|
208
192
|
}
|
|
209
|
-
const declaration = node.type === "ExportNamedDeclaration" ? node.declaration : node;
|
|
210
|
-
if (declaration?.type === "VariableDeclaration" && declaration.kind === "const") {
|
|
211
|
-
for (const declarator of declaration.declarations) {
|
|
212
|
-
if (declarator.id.type === "Identifier" && declarator.init?.type === "ObjectExpression") {
|
|
213
|
-
objects.set(declarator.id.name, declarator.init);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
193
|
}
|
|
218
194
|
|
|
219
195
|
const local = (name) => {
|
|
220
196
|
const binding = bindings.get(name);
|
|
221
|
-
|
|
222
|
-
const object = objects.get(name);
|
|
223
|
-
return object == null ? OPAQUE : namespaceOf(object, bindings);
|
|
197
|
+
return binding == null ? OPAQUE : { kind: "binding", ...binding };
|
|
224
198
|
};
|
|
225
199
|
|
|
226
200
|
const exports = new Map();
|
|
@@ -228,6 +202,14 @@ export function barrelExports(source, file) {
|
|
|
228
202
|
if (node.type === "ExportDefaultDeclaration") {
|
|
229
203
|
exports.set("default", OPAQUE);
|
|
230
204
|
}
|
|
205
|
+
// `export * as Dialog from "./dialog.js"`: the namespace is the module. A
|
|
206
|
+
// bare `export * from` names nothing, so it places nothing.
|
|
207
|
+
if (node.type === "ExportAllDeclaration" && node.exported != null) {
|
|
208
|
+
if (node.exportKind === "type") continue;
|
|
209
|
+
const from = fileOf(node.source);
|
|
210
|
+
exports.set(nameOf(node.exported), from == null ? OPAQUE : { kind: "module", file: from });
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
231
213
|
if (node.type !== "ExportNamedDeclaration" || node.exportKind === "type") continue;
|
|
232
214
|
const { declaration } = node;
|
|
233
215
|
if (declaration != null) {
|
|
@@ -259,53 +241,6 @@ export function barrelExports(source, file) {
|
|
|
259
241
|
return exports;
|
|
260
242
|
}
|
|
261
243
|
|
|
262
|
-
/**
|
|
263
|
-
* The module a view of a barrel's namespace is: the parts, imported from their
|
|
264
|
-
* files, and the object the barrel builds from them.
|
|
265
|
-
*
|
|
266
|
-
* A name that is not a namespace the barrel builds — the barrel changed under a
|
|
267
|
-
* development server after an importer was rewritten — is re-exported from the
|
|
268
|
-
* barrel itself, which answers correctly, if slowly, or with the bundler's own
|
|
269
|
-
* error for a name that is gone.
|
|
270
|
-
*
|
|
271
|
-
* @param {Map<string, object>} exports what `barrelExports` read
|
|
272
|
-
* @param {string} barrel the barrel's absolute path
|
|
273
|
-
* @param {string} name the namespace
|
|
274
|
-
*/
|
|
275
|
-
export function namespaceViewSource(exports, barrel, name) {
|
|
276
|
-
const target = exports.get(name);
|
|
277
|
-
const directory = path.dirname(barrel);
|
|
278
|
-
const specifierOf = (file) => {
|
|
279
|
-
const relative = normalizePath(path.relative(directory, file));
|
|
280
|
-
return JSON.stringify(relative.startsWith("../") ? relative : `./${relative}`);
|
|
281
|
-
};
|
|
282
|
-
if (target?.kind !== "namespace") {
|
|
283
|
-
return `export { ${printName(name)} } from ${specifierOf(barrel)};\n`;
|
|
284
|
-
}
|
|
285
|
-
const locals = new Map();
|
|
286
|
-
const taken = new Set();
|
|
287
|
-
const imports = new Map();
|
|
288
|
-
for (const part of target.parts) {
|
|
289
|
-
const key = `${part.file}\0${part.name}`;
|
|
290
|
-
if (locals.has(key)) continue;
|
|
291
|
-
let alias = IDENTIFIER.test(part.name) ? part.name : "part";
|
|
292
|
-
while (taken.has(alias)) alias = `${alias}$`;
|
|
293
|
-
taken.add(alias);
|
|
294
|
-
locals.set(key, alias);
|
|
295
|
-
const specifiers = imports.get(part.file) ?? [];
|
|
296
|
-
specifiers.push(alias === part.name ? alias : `${printName(part.name)} as ${alias}`);
|
|
297
|
-
imports.set(part.file, specifiers);
|
|
298
|
-
}
|
|
299
|
-
const lines = [...imports].map(
|
|
300
|
-
([file, specifiers]) => `import { ${specifiers.join(", ")} } from ${specifierOf(file)};`,
|
|
301
|
-
);
|
|
302
|
-
const properties = target.parts.map(
|
|
303
|
-
(part) => ` ${printName(part.key)}: ${locals.get(`${part.file}\0${part.name}`)},`,
|
|
304
|
-
);
|
|
305
|
-
lines.push(`export const ${name} = {`, ...properties, "};");
|
|
306
|
-
return `${lines.join("\n")}\n`;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
244
|
/** An import or re-export whose source is one of `BARREL_PACKAGES`. */
|
|
310
245
|
function importsFromBarrel(node) {
|
|
311
246
|
if (node.type === "ImportDeclaration") {
|
|
@@ -339,6 +274,8 @@ function rewriteStatement(node, exports, barrel) {
|
|
|
339
274
|
const kept = [];
|
|
340
275
|
let keptDefault = null;
|
|
341
276
|
const moved = new Map();
|
|
277
|
+
// One statement each: `import * as Dialog` binds a single name.
|
|
278
|
+
const namespaces = [];
|
|
342
279
|
for (const specifier of specifiers) {
|
|
343
280
|
if (specifier.type === "ImportDefaultSpecifier") {
|
|
344
281
|
keptDefault = specifier.local.name;
|
|
@@ -353,15 +290,21 @@ function rewriteStatement(node, exports, barrel) {
|
|
|
353
290
|
kept.push(printed(exported));
|
|
354
291
|
continue;
|
|
355
292
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
293
|
+
if (target.kind === "module") {
|
|
294
|
+
// A namespace binding has to be an identifier, which an import's local
|
|
295
|
+
// name always is and an export's exported name may not be.
|
|
296
|
+
if (!IDENTIFIER.test(binding)) {
|
|
297
|
+
kept.push(printed(exported));
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
namespaces.push({ file: target.file, binding });
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
const list = moved.get(target.file) ?? [];
|
|
304
|
+
list.push(printed(target.name));
|
|
305
|
+
moved.set(target.file, list);
|
|
363
306
|
}
|
|
364
|
-
if (moved.size === 0) return null;
|
|
307
|
+
if (moved.size === 0 && namespaces.length === 0) return null;
|
|
365
308
|
|
|
366
309
|
const keyword = isImport ? "import" : "export";
|
|
367
310
|
const statements = [];
|
|
@@ -374,6 +317,9 @@ function rewriteStatement(node, exports, barrel) {
|
|
|
374
317
|
for (const [source, list] of moved) {
|
|
375
318
|
statements.push(`${keyword} { ${list.join(", ")} } from ${JSON.stringify(source)};`);
|
|
376
319
|
}
|
|
320
|
+
for (const { file, binding } of namespaces) {
|
|
321
|
+
statements.push(`${keyword} * as ${binding} from ${JSON.stringify(file)};`);
|
|
322
|
+
}
|
|
377
323
|
return statements.join("\n");
|
|
378
324
|
}
|
|
379
325
|
|
|
@@ -402,27 +348,6 @@ function applyEdits(code, edits) {
|
|
|
402
348
|
return out + code.slice(at);
|
|
403
349
|
}
|
|
404
350
|
|
|
405
|
-
/** An object literal of imported bindings, as a namespace, or `OPAQUE`. */
|
|
406
|
-
function namespaceOf(object, bindings) {
|
|
407
|
-
const parts = [];
|
|
408
|
-
for (const property of object.properties) {
|
|
409
|
-
if (property.type !== "Property" || property.kind !== "init" || property.computed) {
|
|
410
|
-
return OPAQUE;
|
|
411
|
-
}
|
|
412
|
-
if (property.method || property.value.type !== "Identifier") return OPAQUE;
|
|
413
|
-
const key =
|
|
414
|
-
property.key.type === "Identifier"
|
|
415
|
-
? property.key.name
|
|
416
|
-
: typeof property.key.value === "string"
|
|
417
|
-
? property.key.value
|
|
418
|
-
: null;
|
|
419
|
-
const binding = bindings.get(property.value.name);
|
|
420
|
-
if (key == null || binding == null) return OPAQUE;
|
|
421
|
-
parts.push({ key, ...binding });
|
|
422
|
-
}
|
|
423
|
-
return { kind: "namespace", parts };
|
|
424
|
-
}
|
|
425
|
-
|
|
426
351
|
/** A barrel's reading, re-read only when its size or modification time changes. */
|
|
427
352
|
function readBarrel(readings, file) {
|
|
428
353
|
let stats;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// @noflow
|
|
2
|
+
//
|
|
3
|
+
// Plain JavaScript: executed by the host that runs Vite, before any transform.
|
|
4
|
+
//
|
|
5
|
+
// Every `$error.js` has to be a client module, said while the route table is
|
|
6
|
+
// generated rather than the first time a page throws.
|
|
7
|
+
//
|
|
8
|
+
// Under React Server Components an error boundary catches a throw while the
|
|
9
|
+
// *browser* renders, so its component runs in the browser and the module has
|
|
10
|
+
// to open with `"use client"`. `@uniflowed/router`'s `rsc.js` has always
|
|
11
|
+
// refused a boundary that is not a client reference — but only when it is
|
|
12
|
+
// needed, which is when a page throws. A project whose `$error.js` lacked the
|
|
13
|
+
// directive built, deployed, and answered the first exception in production
|
|
14
|
+
// with a bare `500` and the reason in the server log. The routing guide's own
|
|
15
|
+
// example had no directive, so copying the documentation produced exactly
|
|
16
|
+
// that.
|
|
17
|
+
//
|
|
18
|
+
// This is the same rule, asked of the files: the RSC graph loads the route
|
|
19
|
+
// table, the table names every boundary module, and a boundary whose source
|
|
20
|
+
// does not start with the directive fails the build — or the dev server's
|
|
21
|
+
// route table, which is the same moment for `uf dev` — naming every such file
|
|
22
|
+
// at once.
|
|
23
|
+
//
|
|
24
|
+
// What it is not: a parser. The directive prologue is the only part of a
|
|
25
|
+
// module this reads (comments, then string-literal statements), which is all
|
|
26
|
+
// the directive rule concerns; `crates/uf_rsc`'s scan decides the same thing
|
|
27
|
+
// from a real parse for the client/server split, and a module that satisfies
|
|
28
|
+
// this check but not that one is refused by the router at request time, as
|
|
29
|
+
// before.
|
|
30
|
+
|
|
31
|
+
import { readFileSync } from "node:fs";
|
|
32
|
+
import path from "node:path";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Whether `source` opens with a `"use client"` directive.
|
|
36
|
+
*
|
|
37
|
+
* Reads the directive prologue: leading whitespace, `//` and `/* *\/`
|
|
38
|
+
* comments (a `// @flow` header among them), then string-literal statements.
|
|
39
|
+
* `"use client"` anywhere in the prologue counts, as it does for React's own
|
|
40
|
+
* rule; the first thing that is not a comment or a string literal ends it.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} source
|
|
43
|
+
* @returns {boolean}
|
|
44
|
+
*/
|
|
45
|
+
export function hasUseClientDirective(source) {
|
|
46
|
+
let at = source.charCodeAt(0) === 0xfeff ? 1 : 0;
|
|
47
|
+
const length = source.length;
|
|
48
|
+
for (;;) {
|
|
49
|
+
// Whitespace and comments between statements.
|
|
50
|
+
for (;;) {
|
|
51
|
+
while (at < length && /\s/.test(source[at])) at++;
|
|
52
|
+
if (source.startsWith("//", at)) {
|
|
53
|
+
const end = source.indexOf("\n", at);
|
|
54
|
+
at = end === -1 ? length : end + 1;
|
|
55
|
+
} else if (source.startsWith("/*", at)) {
|
|
56
|
+
const end = source.indexOf("*/", at + 2);
|
|
57
|
+
if (end === -1) return false;
|
|
58
|
+
at = end + 2;
|
|
59
|
+
} else {
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const quote = source[at];
|
|
64
|
+
if (quote !== '"' && quote !== "'") return false;
|
|
65
|
+
const close = source.indexOf(quote, at + 1);
|
|
66
|
+
if (close === -1) return false;
|
|
67
|
+
const value = source.slice(at + 1, close);
|
|
68
|
+
if (value.includes("\n")) return false;
|
|
69
|
+
at = close + 1;
|
|
70
|
+
// A directive is a whole statement: `;`, a line end, a comment, or the
|
|
71
|
+
// end. Anything else — `"use client".length` — is an expression.
|
|
72
|
+
while (at < length && (source[at] === " " || source[at] === "\t")) at++;
|
|
73
|
+
if (source[at] === ";") {
|
|
74
|
+
at++;
|
|
75
|
+
} else if (
|
|
76
|
+
at < length &&
|
|
77
|
+
source[at] !== "\n" &&
|
|
78
|
+
source[at] !== "\r" &&
|
|
79
|
+
!source.startsWith("//", at) &&
|
|
80
|
+
!source.startsWith("/*", at)
|
|
81
|
+
) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
if (value === "use client") return true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Every error-boundary module a scanned route table names, once each.
|
|
90
|
+
*
|
|
91
|
+
* `table.errors` is one entry per directory holding an `$error.js`; a slot's
|
|
92
|
+
* boundaries are only on the routes that render the slot, as an
|
|
93
|
+
* `errorBoundary`. Both are collected by walking the table rather than by
|
|
94
|
+
* knowing where each kind lives, so a boundary a later change puts somewhere
|
|
95
|
+
* new is still asked about.
|
|
96
|
+
*
|
|
97
|
+
* @param {object} table what `scanRoutes` answered
|
|
98
|
+
* @returns {string[]} absolute paths, sorted
|
|
99
|
+
*/
|
|
100
|
+
export function errorBoundaryModules(table) {
|
|
101
|
+
const found = new Set();
|
|
102
|
+
for (const entry of table.errors ?? []) {
|
|
103
|
+
if (typeof entry?.module === "string") found.add(entry.module);
|
|
104
|
+
}
|
|
105
|
+
const seen = new Set();
|
|
106
|
+
const walk = (value) => {
|
|
107
|
+
if (value == null || typeof value !== "object" || seen.has(value)) return;
|
|
108
|
+
seen.add(value);
|
|
109
|
+
if (Array.isArray(value)) {
|
|
110
|
+
for (const item of value) walk(item);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
for (const [key, child] of Object.entries(value)) {
|
|
114
|
+
if (key === "errorBoundary" && typeof child?.module === "string") found.add(child.module);
|
|
115
|
+
walk(child);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
walk(table.routes ?? []);
|
|
119
|
+
return [...found].sort();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Fail when an error boundary in `table` is not a client module.
|
|
124
|
+
*
|
|
125
|
+
* Throws one error naming every such file, relative to `root`, with the fix;
|
|
126
|
+
* answers nothing otherwise. `read` is the file reader, for a test.
|
|
127
|
+
*
|
|
128
|
+
* @param {object} table what `scanRoutes` answered
|
|
129
|
+
* @param {string} root the project root, for the names in the message
|
|
130
|
+
* @param {(file: string) => string} [read]
|
|
131
|
+
*/
|
|
132
|
+
export function refuseServerErrorBoundaries(
|
|
133
|
+
table,
|
|
134
|
+
root,
|
|
135
|
+
read = (file) => readFileSync(file, "utf8"),
|
|
136
|
+
) {
|
|
137
|
+
const missing = errorBoundaryModules(table).filter((file) => {
|
|
138
|
+
let source;
|
|
139
|
+
try {
|
|
140
|
+
source = read(file);
|
|
141
|
+
} catch {
|
|
142
|
+
// A boundary the scan found and nobody can read is the bundler's to
|
|
143
|
+
// report, with the error it gets opening it.
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
return !hasUseClientDirective(source);
|
|
147
|
+
});
|
|
148
|
+
if (missing.length === 0) return;
|
|
149
|
+
const names = missing.map((file) => path.relative(root, file).split(path.sep).join("/"));
|
|
150
|
+
throw new Error(
|
|
151
|
+
`uf: ${names.length === 1 ? "an error boundary is" : `${names.length} error boundaries are`} not ` +
|
|
152
|
+
`a client module: ${names.join(", ")}. An \`$error.js\` catches a throw while the browser ` +
|
|
153
|
+
'renders, so its component runs in the browser, and the module has to open with "use client" ' +
|
|
154
|
+
"as its first statement. Without it the page that throws is answered with a bare 500. Add " +
|
|
155
|
+
'"use client"; at the top of each. See docs/app/guide/routing.',
|
|
156
|
+
);
|
|
157
|
+
}
|
package/internal/flight.js
CHANGED
|
@@ -18,9 +18,14 @@
|
|
|
18
18
|
// client directive is not evaluated here: it is replaced by a client
|
|
19
19
|
// reference per export, naming the chunk the browser loads it from.
|
|
20
20
|
// * **`ssr`**, Vite's own server environment. It holds the HTML renderer, the
|
|
21
|
-
// route handlers, the middleware
|
|
22
|
-
//
|
|
23
|
-
//
|
|
21
|
+
// route handlers, the middleware — and the *server copy* of every client
|
|
22
|
+
// module, which is what renders a client component into HTML. It reaches
|
|
23
|
+
// the rsc graph through one module, the bridge, for the payload and for
|
|
24
|
+
// the server-action endpoint: the action table is the rsc graph's, so an
|
|
25
|
+
// action shares every module instance with the pages that show what it
|
|
26
|
+
// wrote (ubugeeei-prod/uf#1469). A route handler and a middleware do not
|
|
27
|
+
// yet: a module one of them imports and a page imports is evaluated once in
|
|
28
|
+
// each graph.
|
|
24
29
|
// * **`client`**, the browser's. Its entry hydrates from the payload the
|
|
25
30
|
// document carries, and it holds no page, layout or loader — only the
|
|
26
31
|
// client modules, each an entry of its own, loaded when a payload names it.
|
|
@@ -509,18 +514,30 @@ export function fsFileOf(pathname) {
|
|
|
509
514
|
* build rendered it — a prerendered payload is a file, and nothing that serves
|
|
510
515
|
* a file can say so for it. `null` under `uf dev`.
|
|
511
516
|
*/
|
|
512
|
-
export function rscEntrySource(routesId, routing = {}, deployment = null) {
|
|
517
|
+
export function rscEntrySource(routesId, routing = {}, deployment = null, actionsId = null) {
|
|
513
518
|
const settings = {
|
|
514
519
|
basePath: routing.basePath ?? "",
|
|
515
520
|
trailingSlash: routing.trailingSlash ?? "ignore",
|
|
516
521
|
};
|
|
517
|
-
|
|
522
|
+
// The action table is loaded here, in the graph the pages render in, and
|
|
523
|
+
// not in the ssr graph that answers the request. A module an action and a
|
|
524
|
+
// page both import is then one instance: a write the action makes is what
|
|
525
|
+
// the next render — the postback of a form posted before hydration
|
|
526
|
+
// included — reads. In the ssr graph it was a second copy, and the page
|
|
527
|
+
// never saw the write (ubugeeei-prod/uf#1469).
|
|
528
|
+
const actions =
|
|
529
|
+
actionsId == null
|
|
530
|
+
? "export const callAction = createActionDispatcher({ actions: [] });"
|
|
531
|
+
: `import { actions } from ${JSON.stringify(actionsId)};
|
|
532
|
+
export const callAction = createActionDispatcher({ actions });`;
|
|
533
|
+
return `import { createActionDispatcher, createFlightRenderer, installRouting } from "@uniflowed/router/rsc";
|
|
518
534
|
import { routes, notFound, errors } from ${JSON.stringify(routesId)};
|
|
519
535
|
installRouting(${JSON.stringify(settings)});
|
|
520
536
|
export { routes, notFound, errors };
|
|
521
537
|
export const renderFlight = createFlightRenderer({ routes, notFound, errors, deployment: ${JSON.stringify(
|
|
522
538
|
deployment ?? null,
|
|
523
539
|
)} });
|
|
540
|
+
${actions}
|
|
524
541
|
`;
|
|
525
542
|
}
|
|
526
543
|
|
|
@@ -543,13 +560,16 @@ if (typeof load !== "function") {
|
|
|
543
560
|
export async function renderFlight(url, options) {
|
|
544
561
|
return (await load()).renderFlight(url, options);
|
|
545
562
|
}
|
|
563
|
+
export async function callAction(request, settings) {
|
|
564
|
+
return (await load()).callAction(request, settings);
|
|
565
|
+
}
|
|
546
566
|
export const { routes, notFound, errors } = await load();
|
|
547
567
|
`;
|
|
548
568
|
}
|
|
549
569
|
|
|
550
570
|
/** `virtual:uf/rsc-bridge` in a build: the rsc build's output, bundled in. */
|
|
551
571
|
export function builtBridgeSource(rscOutput) {
|
|
552
|
-
return `export { renderFlight, routes, notFound, errors } from ${JSON.stringify(rscOutput)};\n`;
|
|
572
|
+
return `export { renderFlight, callAction, routes, notFound, errors } from ${JSON.stringify(rscOutput)};\n`;
|
|
553
573
|
}
|
|
554
574
|
|
|
555
575
|
/**
|
|
@@ -697,12 +717,10 @@ ${clientInstrumentationSource(options.instrumentation)}hydrateFlight({ App${stri
|
|
|
697
717
|
export function flightServerSource(
|
|
698
718
|
appEntry,
|
|
699
719
|
routesId,
|
|
700
|
-
actionsId,
|
|
701
720
|
routing = { redirects: [], rewrites: [], headers: [], basePath: "", trailingSlash: "ignore" },
|
|
702
721
|
instrumentation = null,
|
|
703
722
|
) {
|
|
704
723
|
return `import {
|
|
705
|
-
createActionDispatcher,
|
|
706
724
|
createInstrumentation,
|
|
707
725
|
instrumentRender,
|
|
708
726
|
traceRequestPhase,
|
|
@@ -712,8 +730,13 @@ export function flightServerSource(
|
|
|
712
730
|
} from "@uniflowed/router/server";
|
|
713
731
|
import { createDocumentRenderer } from "@uniflowed/router/rsc/ssr";
|
|
714
732
|
import { handlers, middleware } from ${JSON.stringify(routesId)};
|
|
715
|
-
import {
|
|
716
|
-
|
|
733
|
+
import {
|
|
734
|
+
renderFlight,
|
|
735
|
+
callAction as callActionInRsc,
|
|
736
|
+
routes,
|
|
737
|
+
notFound,
|
|
738
|
+
errors,
|
|
739
|
+
} from ${JSON.stringify(FLIGHT_VIRTUAL.bridge)};
|
|
717
740
|
import { loadClientModule } from ${JSON.stringify(FLIGHT_VIRTUAL.references)};
|
|
718
741
|
import App from ${JSON.stringify(appEntry)};
|
|
719
742
|
export const routing = ${JSON.stringify(routing)};
|
|
@@ -735,7 +758,8 @@ export const flight = (url, options = {}) => instrumentRender(
|
|
|
735
758
|
export { shellDocument } from "@uniflowed/router/server";
|
|
736
759
|
const dispatchRoute = createDispatcher({ handlers });
|
|
737
760
|
export const dispatch = (request) => traceRequestPhase("route", () => dispatchRoute(request));
|
|
738
|
-
|
|
761
|
+
// Built in the rsc graph and reached through the bridge; see \`rscEntrySource\`.
|
|
762
|
+
export const callAction = callActionInRsc;
|
|
739
763
|
const guard = createMiddlewareRunner({ middleware });
|
|
740
764
|
export const runMiddleware = (request) => traceRequestPhase("middleware", () => guard(request));
|
|
741
765
|
`;
|
package/internal/http.js
CHANGED
|
@@ -34,9 +34,12 @@
|
|
|
34
34
|
* @param {string} [path]
|
|
35
35
|
*/
|
|
36
36
|
export async function toRequest(incoming, config, path) {
|
|
37
|
-
const host = incoming.headers.host ?? "localhost";
|
|
38
37
|
const protocol = config?.server?.https == null ? "http" : "https";
|
|
39
|
-
const url =
|
|
38
|
+
const url = requestUrl(
|
|
39
|
+
protocol,
|
|
40
|
+
incoming.headers.host,
|
|
41
|
+
path ?? incoming.originalUrl ?? incoming.url,
|
|
42
|
+
);
|
|
40
43
|
|
|
41
44
|
const headers = new Headers();
|
|
42
45
|
for (const [name, value] of Object.entries(incoming.headers)) {
|
|
@@ -67,8 +70,61 @@ export async function toRequest(incoming, config, path) {
|
|
|
67
70
|
* @param {import("node:http").IncomingMessage} incoming
|
|
68
71
|
*/
|
|
69
72
|
export function toAddressRequest(incoming) {
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
return new Request(
|
|
74
|
+
requestUrl("http", incoming.headers.host, incoming.originalUrl ?? incoming.url),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The URL a Node request was for: `Host`'s authority and the request-target's
|
|
80
|
+
* path and query, joined as text so a target of `//evil.example/x` (or
|
|
81
|
+
* `/\evil.example/x`) cannot become the request's host, and an absolute-form
|
|
82
|
+
* target keeps only its path. A `Host` holding a path, a user name, a query or
|
|
83
|
+
* a fragment is not believed.
|
|
84
|
+
*
|
|
85
|
+
* The same function as `requestUrl` in `@uniflowed/server/node`, which has the
|
|
86
|
+
* argument for why; spelled twice because this file runs before any Flow
|
|
87
|
+
* transform and cannot import that one. `packages/server/serve.test.js` holds
|
|
88
|
+
* both to one answer.
|
|
89
|
+
*
|
|
90
|
+
* @param {string} protocol
|
|
91
|
+
* @param {unknown} host the `Host` header as Node parsed it
|
|
92
|
+
* @param {string | undefined} target the request-target
|
|
93
|
+
* @returns {URL}
|
|
94
|
+
*/
|
|
95
|
+
function requestUrl(protocol, host, target) {
|
|
96
|
+
const authority =
|
|
97
|
+
typeof host === "string" && host !== "" && isAuthority(host) ? host : "localhost";
|
|
98
|
+
let path = target ?? "/";
|
|
99
|
+
if (!path.startsWith("/")) {
|
|
100
|
+
try {
|
|
101
|
+
const absolute = new URL(path);
|
|
102
|
+
path = absolute.pathname + absolute.search;
|
|
103
|
+
} catch {
|
|
104
|
+
path = "/";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
return new URL(`${protocol}://${authority}${path}`);
|
|
109
|
+
} catch {
|
|
110
|
+
return new URL(`${protocol}://localhost/`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Whether `value` has none of the characters that end a URL's authority.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} value
|
|
118
|
+
*/
|
|
119
|
+
function isAuthority(value) {
|
|
120
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
121
|
+
const code = value.charCodeAt(index);
|
|
122
|
+
// `/`, `\`, `?`, `#`, `@`, and every space and control character.
|
|
123
|
+
if (code === 47 || code === 92 || code === 63 || code === 35 || code === 64 || code <= 32) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return true;
|
|
72
128
|
}
|
|
73
129
|
|
|
74
130
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniflowed/vite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|
|
@@ -35,10 +35,10 @@
|
|
|
35
35
|
"@babel/core": "^7.29.7",
|
|
36
36
|
"@mdx-js/rollup": "^3.1.1",
|
|
37
37
|
"@shikijs/rehype": "^3.23.0",
|
|
38
|
-
"@uniflowed/host": "0.
|
|
39
|
-
"@uniflowed/router": "0.
|
|
40
|
-
"@uniflowed/server": "0.
|
|
41
|
-
"@uniflowed/validator": "0.
|
|
38
|
+
"@uniflowed/host": "0.3.0",
|
|
39
|
+
"@uniflowed/router": "0.3.0",
|
|
40
|
+
"@uniflowed/server": "0.3.0",
|
|
41
|
+
"@uniflowed/validator": "0.3.0",
|
|
42
42
|
"babel-plugin-relay": "^21.0.1",
|
|
43
43
|
"estree-util-value-to-estree": "^3.5.0",
|
|
44
44
|
"rehype-slug": "^6.0.0",
|