@uniflowed/vite 0.0.0-alpha.20 → 0.0.0-alpha.21
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 +74 -13
- package/index.js +15 -5
- package/internal/config.js +30 -3
- package/internal/openapi.js +218 -0
- package/internal/routes.js +81 -25
- package/internal/rsc.js +5 -6
- package/package.json +5 -3
package/driver.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
//
|
|
8
8
|
// <host> driver.js dev --root <dir> [--mode <m>] [--host <h>] [--port <n>] [--strict-port]
|
|
9
9
|
// [--uf-env-file <file>]...
|
|
10
|
-
// <host> driver.js build --root <dir> [--mode <m>] [--out-dir <dir>]
|
|
10
|
+
// <host> driver.js build --root <dir> [--mode <m>] [--out-dir <dir>] [--target <target>]
|
|
11
11
|
// [--prerender everything|possible|nothing]
|
|
12
12
|
// [--static-build] [--because <sentence>]
|
|
13
13
|
// <host> driver.js library --root <dir> [--mode <m>] [--out-dir <dir>]
|
|
@@ -50,8 +50,9 @@ import { COMPILE_ASSETS_ID, compileAssetsPlugin } from "./internal/compile-asset
|
|
|
50
50
|
import { emit, errorEvent, eventLogger } from "./internal/events.js";
|
|
51
51
|
import { loadUfConfig, projectConfig } from "./internal/config.js";
|
|
52
52
|
import { send, toRequest } from "./internal/http.js";
|
|
53
|
+
import { createOpenApiDocument } from "./internal/openapi.js";
|
|
53
54
|
import { withProjectConfig } from "./merge.js";
|
|
54
|
-
import { VIRTUAL, scanRoutes } from "./internal/routes.js";
|
|
55
|
+
import { VIRTUAL, resolveRouteTarget, scanRoutes } from "./internal/routes.js";
|
|
55
56
|
import {
|
|
56
57
|
BUILD_ID_FILE,
|
|
57
58
|
assetsFromManifest,
|
|
@@ -161,6 +162,7 @@ async function viteConfig(config, mode) {
|
|
|
161
162
|
const port = Number(argument("--port") ?? dev.port ?? 5173);
|
|
162
163
|
const allowedHosts =
|
|
163
164
|
Array.isArray(dev.allowedHosts) && dev.allowedHosts.length > 0 ? dev.allowedHosts : undefined;
|
|
165
|
+
const routeTarget = resolveRouteTarget(config, argument("--target"));
|
|
164
166
|
|
|
165
167
|
// What uf generates from the semantics it owns: where the project is, which
|
|
166
168
|
// plugins make Flow compile, and the few settings uf enforces rather than
|
|
@@ -191,7 +193,7 @@ async function viteConfig(config, mode) {
|
|
|
191
193
|
mode,
|
|
192
194
|
clearScreen: false,
|
|
193
195
|
customLogger: eventLogger(argument("--log-level") ?? "info"),
|
|
194
|
-
plugins: [uniflowed({ root, config })],
|
|
196
|
+
plugins: [uniflowed({ root, config, target: routeTarget })],
|
|
195
197
|
server: {
|
|
196
198
|
host,
|
|
197
199
|
port,
|
|
@@ -255,6 +257,7 @@ async function viteConfig(config, mode) {
|
|
|
255
257
|
async function dev() {
|
|
256
258
|
const { createServer } = await import("vite");
|
|
257
259
|
const config = await loadConfig();
|
|
260
|
+
const routeTarget = resolveRouteTarget(config, argument("--target"));
|
|
258
261
|
// The mode is uf's to decide, not this file's: `uf dev` resolves `--mode`,
|
|
259
262
|
// the profile `uf env use` wrote and `env.active` before it starts anything,
|
|
260
263
|
// and always passes the answer. The fallback is for a driver started by hand.
|
|
@@ -266,9 +269,9 @@ async function dev() {
|
|
|
266
269
|
emit("listening", {
|
|
267
270
|
local: urls.local,
|
|
268
271
|
network: urls.network,
|
|
269
|
-
routes: scanRoutes(path.resolve(root, config.app?.router?.root ?? "app")
|
|
270
|
-
|
|
271
|
-
),
|
|
272
|
+
routes: scanRoutes(path.resolve(root, config.app?.router?.root ?? "app"), {
|
|
273
|
+
target: routeTarget,
|
|
274
|
+
}).routes.map((route) => route.path),
|
|
272
275
|
});
|
|
273
276
|
watchSources(server);
|
|
274
277
|
watchEnvFiles(server);
|
|
@@ -390,6 +393,7 @@ function watchEnvFiles(server) {
|
|
|
390
393
|
async function preview() {
|
|
391
394
|
const { preview: startPreview } = await import("vite");
|
|
392
395
|
const config = await loadConfig();
|
|
396
|
+
const routeTarget = resolveRouteTarget(config, argument("--target"));
|
|
393
397
|
const inline = await viteConfig(config, argument("--mode") ?? "production");
|
|
394
398
|
// A build that declared it emits no server has none to mount. `uf` refuses
|
|
395
399
|
// `uf start` for such a project and lets this one through, because a preview
|
|
@@ -489,9 +493,9 @@ async function preview() {
|
|
|
489
493
|
// be the report being wrong about the thing it exists to report.
|
|
490
494
|
routes:
|
|
491
495
|
build == null
|
|
492
|
-
? scanRoutes(path.resolve(root, config.app?.router?.root ?? "app")
|
|
493
|
-
|
|
494
|
-
)
|
|
496
|
+
? scanRoutes(path.resolve(root, config.app?.router?.root ?? "app"), {
|
|
497
|
+
target: routeTarget,
|
|
498
|
+
}).routes.map((route) => route.path)
|
|
495
499
|
: build.entry.routes.map((route) => route.path),
|
|
496
500
|
handlers: build == null ? [] : build.entry.handlers.map((handler) => handler.path),
|
|
497
501
|
});
|
|
@@ -632,6 +636,10 @@ async function build() {
|
|
|
632
636
|
emit("phase", { name: "prerender" });
|
|
633
637
|
const server = await import(pathToFileURL(path.join(serverDir, "server.js")).href);
|
|
634
638
|
const assets = assetsFromManifest(manifest);
|
|
639
|
+
const openapi = await createOpenApiDocument(server.handlers);
|
|
640
|
+
const openapiFile = path.join(root, ".uf", "build", "meta", "openapi.json");
|
|
641
|
+
mkdirSync(path.dirname(openapiFile), { recursive: true });
|
|
642
|
+
writeFileSync(openapiFile, `${JSON.stringify(openapi, null, 2)}\n`);
|
|
635
643
|
const plan = await renderingPlan(server, prerender);
|
|
636
644
|
emit("rendering", {
|
|
637
645
|
prerender,
|
|
@@ -1070,9 +1078,9 @@ async function compile() {
|
|
|
1070
1078
|
* of what an adapter is, and keeping the differences in one object is what
|
|
1071
1079
|
* stops a second one from quietly becoming a second application.
|
|
1072
1080
|
*
|
|
1073
|
-
* `
|
|
1081
|
+
* `static` is deliberately absent; `uf_config`'s
|
|
1074
1082
|
* `DeployAdapter::is_implemented` is the other half of that fact and
|
|
1075
|
-
* `docs/app/reference/cli/$page.mdx` says why
|
|
1083
|
+
* `docs/app/reference/cli/$page.mdx` says why.
|
|
1076
1084
|
*/
|
|
1077
1085
|
const ADAPTERS = {
|
|
1078
1086
|
node: {
|
|
@@ -1101,6 +1109,12 @@ const ADAPTERS = {
|
|
|
1101
1109
|
server: bunEntrySource("./handler.js", schedules),
|
|
1102
1110
|
}),
|
|
1103
1111
|
},
|
|
1112
|
+
deno: {
|
|
1113
|
+
entries: (document, cache, build, schedules) => ({
|
|
1114
|
+
handler: handlerEntrySource(document, cache, DENO_CAPABILITIES, build),
|
|
1115
|
+
server: denoEntrySource("./handler.js", schedules),
|
|
1116
|
+
}),
|
|
1117
|
+
},
|
|
1104
1118
|
// The same two files. What `--adapter container` adds is a `Dockerfile` and
|
|
1105
1119
|
// a `.dockerignore`, and both are plain text that `uf` writes beside this
|
|
1106
1120
|
// output rather than anything the bundler produces — see `uf_cli`'s
|
|
@@ -1155,6 +1169,7 @@ const ADAPTERS = {
|
|
|
1155
1169
|
*/
|
|
1156
1170
|
const NODE_CAPABILITIES = { module: "@uniflowed/server/node", name: "nodeCapabilities" };
|
|
1157
1171
|
const BUN_CAPABILITIES = { module: "@uniflowed/server/bun", name: "bunCapabilities" };
|
|
1172
|
+
const DENO_CAPABILITIES = { module: "@uniflowed/server/deno", name: "denoCapabilities" };
|
|
1158
1173
|
const EDGE_CAPABILITIES = { module: "@uniflowed/server/edge", name: "edgeCapabilities" };
|
|
1159
1174
|
const SERVERLESS_CAPABILITIES = { module: "@uniflowed/server/lambda", name: "lambdaCapabilities" };
|
|
1160
1175
|
|
|
@@ -1206,7 +1221,7 @@ const SERVERLESS_CAPABILITIES = { module: "@uniflowed/server/lambda", name: "lam
|
|
|
1206
1221
|
* It is the one implemented target with no application to link: a static host
|
|
1207
1222
|
* returns files, and `uf build` has already written them. So `uf` copies the
|
|
1208
1223
|
* output directory itself and never spawns this driver for it, which is why
|
|
1209
|
-
* [`ADAPTERS`] has
|
|
1224
|
+
* [`ADAPTERS`] has six rows and not seven. What that target does instead of
|
|
1210
1225
|
* linking is refuse a project whose route handlers, middleware, unprerendered
|
|
1211
1226
|
* routes or server actions a static host cannot answer — in Rust, because the
|
|
1212
1227
|
* facts it needs are the route table and what the prerender reported.
|
|
@@ -1415,7 +1430,7 @@ const cache = { store: createCacheStore(${
|
|
|
1415
1430
|
|
|
1416
1431
|
`
|
|
1417
1432
|
: ""
|
|
1418
|
-
}// What this target can do, and it is not the same for all
|
|
1433
|
+
}// What this target can do, and it is not the same for all six: whether a
|
|
1419
1434
|
// response body reaches the client as it is produced, and whether the process
|
|
1420
1435
|
// is still there once it has. A route handler that streams events or takes a
|
|
1421
1436
|
// socket asks through this rather than finding out in production. Nothing is
|
|
@@ -1590,6 +1605,52 @@ serve({ handle: fetch, staticDir, beginRequest${cron.option} }).catch((error) =>
|
|
|
1590
1605
|
`;
|
|
1591
1606
|
}
|
|
1592
1607
|
|
|
1608
|
+
/**
|
|
1609
|
+
* The source of `server.js`: the Deno socket around that handler.
|
|
1610
|
+
*
|
|
1611
|
+
* The same generated contract as Node and Bun, with `@uniflowed/server/deno`
|
|
1612
|
+
* owning the host-specific pieces. The static directory is resolved with
|
|
1613
|
+
* `import.meta.url` alone so the entry carries no `node:` imports.
|
|
1614
|
+
*/
|
|
1615
|
+
function denoEntrySource(handlerSpecifier, schedules) {
|
|
1616
|
+
const cron = scheduleLines("@uniflowed/server/schedule", schedules);
|
|
1617
|
+
return `// Generated by \`uf build --adapter deno\`. Not checked in, not edited.
|
|
1618
|
+
import { serve } from "@uniflowed/server/deno";
|
|
1619
|
+
${cron.imports}
|
|
1620
|
+
// \`beginRequest\` comes from the handler beside this file rather than from
|
|
1621
|
+
// \`@uniflowed/server/deno\` above, because the request has to be established in
|
|
1622
|
+
// the storage the *application* reads, which is the copy bundled into
|
|
1623
|
+
// \`handler.js\`. See ubugeeei-prod/uf#389.
|
|
1624
|
+
//
|
|
1625
|
+
// Deno has no Node globals, but dependency bundles may still carry a CommonJS
|
|
1626
|
+
// production branch that expects a few of them. Establish the small environment
|
|
1627
|
+
// shape before \`handler.js\` is evaluated, which requires a dynamic import here
|
|
1628
|
+
// rather than a static one.
|
|
1629
|
+
globalThis.process ??= { env: {} };
|
|
1630
|
+
globalThis.process.env ??= {};
|
|
1631
|
+
globalThis.process.env.NODE_ENV ??= "production";
|
|
1632
|
+
globalThis.Buffer ??= {
|
|
1633
|
+
byteLength(value) {
|
|
1634
|
+
return new TextEncoder().encode(String(value)).byteLength;
|
|
1635
|
+
},
|
|
1636
|
+
};
|
|
1637
|
+
globalThis.setImmediate ??= (callback, ...args) => setTimeout(callback, 0, ...args);
|
|
1638
|
+
globalThis.clearImmediate ??= (handle) => clearTimeout(handle);
|
|
1639
|
+
const { beginRequest, fetch } = await import(${JSON.stringify(handlerSpecifier)});
|
|
1640
|
+
|
|
1641
|
+
// Resolved from this file and not from the working directory: a process
|
|
1642
|
+
// manager and a person in a shell each start a server from wherever they
|
|
1643
|
+
// happen to be, and a directory that only served its own assets when it was
|
|
1644
|
+
// started from inside itself would be a deployment with a trap in it.
|
|
1645
|
+
const staticDir = decodeURIComponent(new URL("./static", import.meta.url).pathname);
|
|
1646
|
+
${cron.declarations}
|
|
1647
|
+
serve({ handle: fetch, staticDir, beginRequest${cron.option} }).catch((error) => {
|
|
1648
|
+
console.error(\`uf: \${error?.message ?? String(error)}\`);
|
|
1649
|
+
Deno.exit(1);
|
|
1650
|
+
});
|
|
1651
|
+
`;
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1593
1654
|
/**
|
|
1594
1655
|
* The source of `worker.js`: the Cloudflare Workers entry around that handler.
|
|
1595
1656
|
*
|
package/index.js
CHANGED
|
@@ -80,6 +80,7 @@ import {
|
|
|
80
80
|
RESERVED,
|
|
81
81
|
VIRTUAL,
|
|
82
82
|
clientModuleSource,
|
|
83
|
+
resolveRouteTarget,
|
|
83
84
|
routesModuleSource,
|
|
84
85
|
scanRoutes,
|
|
85
86
|
serverModuleSource,
|
|
@@ -115,6 +116,7 @@ export function devUrlFor(id) {
|
|
|
115
116
|
* @typedef {object} UniflowedOptions
|
|
116
117
|
* @property {string} [root] absolute project root; Vite's root by default
|
|
117
118
|
* @property {object} [config] the loaded `uf.config.js` object
|
|
119
|
+
* @property {"web" | "native" | "ios" | "android"} [target] app target
|
|
118
120
|
* @property {string} [command] the `uf` binary to transform through
|
|
119
121
|
*/
|
|
120
122
|
|
|
@@ -126,6 +128,7 @@ export function devUrlFor(id) {
|
|
|
126
128
|
export default function uniflowed(options = {}) {
|
|
127
129
|
const ufConfig = options.config ?? {};
|
|
128
130
|
const app = ufConfig.app ?? {};
|
|
131
|
+
const routeTarget = resolveRouteTarget(ufConfig, options.target);
|
|
129
132
|
const routerRoot = app.router?.root ?? "app";
|
|
130
133
|
const appEntry = app.router?.entry ?? ufConfig.build?.entries?.[0] ?? "app.js";
|
|
131
134
|
const markdown = app.builtins?.markdown ?? {};
|
|
@@ -156,6 +159,7 @@ export default function uniflowed(options = {}) {
|
|
|
156
159
|
flowPlugin({
|
|
157
160
|
routerRoot,
|
|
158
161
|
appEntry,
|
|
162
|
+
routeTarget,
|
|
159
163
|
strictMode,
|
|
160
164
|
navigation,
|
|
161
165
|
mount,
|
|
@@ -176,6 +180,7 @@ export default function uniflowed(options = {}) {
|
|
|
176
180
|
function flowPlugin({
|
|
177
181
|
routerRoot,
|
|
178
182
|
appEntry,
|
|
183
|
+
routeTarget,
|
|
179
184
|
strictMode,
|
|
180
185
|
navigation,
|
|
181
186
|
mount,
|
|
@@ -260,10 +265,15 @@ function flowPlugin({
|
|
|
260
265
|
// survives the build — so the browser's table was shipping the absolute
|
|
261
266
|
// path of every page on the machine that built the site, to every visitor.
|
|
262
267
|
// The server's table is read where those files are and keeps them.
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
268
|
+
// HTTP handlers are exclusively server capabilities. Even an unused
|
|
269
|
+
// dynamic import makes Vite traverse the handler's database/auth imports.
|
|
270
|
+
return routesModuleSource(
|
|
271
|
+
{ ...table, handlers: [] },
|
|
272
|
+
{
|
|
273
|
+
shipsPage: (route) => kept.has(route),
|
|
274
|
+
relativeTo: root,
|
|
275
|
+
},
|
|
276
|
+
);
|
|
267
277
|
};
|
|
268
278
|
|
|
269
279
|
/**
|
|
@@ -373,7 +383,7 @@ function flowPlugin({
|
|
|
373
383
|
if (id === RUNTIME_RESOLVED_ID) return refreshRuntimeSource();
|
|
374
384
|
if (id === AUDIT_RESOLVED_ID) return auditRuntimeSource(accessibility?.axe);
|
|
375
385
|
if (id === resolved(VIRTUAL.routes)) {
|
|
376
|
-
const table = scanRoutes(appRoot);
|
|
386
|
+
const table = scanRoutes(appRoot, { target: routeTarget });
|
|
377
387
|
// The server renders every route, so the server's table is the whole
|
|
378
388
|
// one and is generated with no filter at all. Only the browser's copy
|
|
379
389
|
// is split.
|
package/internal/config.js
CHANGED
|
@@ -72,7 +72,7 @@ export async function loadUfConfig(root) {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
const compiled = await compileConfig(source, file, root);
|
|
75
|
-
const module = await
|
|
75
|
+
const module = await importConfigModule(compiled);
|
|
76
76
|
const config = module.default;
|
|
77
77
|
if (config == null || typeof config !== "object") {
|
|
78
78
|
throw new Error(
|
|
@@ -82,6 +82,20 @@ export async function loadUfConfig(root) {
|
|
|
82
82
|
return { config, file };
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
async function importConfigModule(compiled) {
|
|
86
|
+
const previous = process.env.UF_TRANSFORM_BOOTSTRAP_CONFIG;
|
|
87
|
+
process.env.UF_TRANSFORM_BOOTSTRAP_CONFIG = "1";
|
|
88
|
+
try {
|
|
89
|
+
return await import(pathToFileURL(compiled).href);
|
|
90
|
+
} finally {
|
|
91
|
+
if (previous == null) {
|
|
92
|
+
delete process.env.UF_TRANSFORM_BOOTSTRAP_CONFIG;
|
|
93
|
+
} else {
|
|
94
|
+
process.env.UF_TRANSFORM_BOOTSTRAP_CONFIG = previous;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
85
99
|
/**
|
|
86
100
|
* Transform the config to JavaScript and write it where it can be imported.
|
|
87
101
|
*
|
|
@@ -93,8 +107,14 @@ async function compileConfig(source, file, root) {
|
|
|
93
107
|
const directory = path.join(root, COMPILED_DIR);
|
|
94
108
|
const target = path.join(directory, `uf.config.${hash}.mjs`);
|
|
95
109
|
|
|
96
|
-
const out = await transformFlow(source, file, {
|
|
97
|
-
|
|
110
|
+
const out = await transformFlow(source, file, {
|
|
111
|
+
root,
|
|
112
|
+
sourceMap: false,
|
|
113
|
+
configBootstrap: true,
|
|
114
|
+
});
|
|
115
|
+
const code = rewriteConfigImports(
|
|
116
|
+
rewriteRelativeImports(out?.code ?? source, path.dirname(file)),
|
|
117
|
+
);
|
|
98
118
|
// Atomically, because two `uf` commands in one project write this same path
|
|
99
119
|
// at the same time — the hash is of the source, so they agree on the name —
|
|
100
120
|
// and `writeFileSync` truncates before it writes. A reader that caught it
|
|
@@ -125,6 +145,13 @@ export function rewriteRelativeImports(code, baseDirectory) {
|
|
|
125
145
|
);
|
|
126
146
|
}
|
|
127
147
|
|
|
148
|
+
function rewriteConfigImports(code) {
|
|
149
|
+
return code.replace(
|
|
150
|
+
/^\s*import\s*\{\s*defineConfig\s*\}\s*from\s*["']@uniflowed\/config["'];?\n?/gm,
|
|
151
|
+
"function defineConfig(config) {\n return config;\n}\n",
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
128
155
|
/**
|
|
129
156
|
* The JSON projection of a config object.
|
|
130
157
|
*
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// @noflow
|
|
2
|
+
//
|
|
3
|
+
// Route-handler OpenAPI output.
|
|
4
|
+
//
|
|
5
|
+
// The build driver imports this module before it registers the Node Flow
|
|
6
|
+
// hooks, so it must stay plain JavaScript and must not statically import
|
|
7
|
+
// Flow-authored packages. The schema and method vocabulary are loaded inside
|
|
8
|
+
// `createOpenApiDocument`, after the driver has installed the hooks.
|
|
9
|
+
|
|
10
|
+
const JSON = "application/json";
|
|
11
|
+
const QUERY_EXTENSION = "x-uf-query";
|
|
12
|
+
const STANDARD_METHODS = new Set(["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]);
|
|
13
|
+
|
|
14
|
+
export async function createOpenApiDocument(handlers, options = {}) {
|
|
15
|
+
const [{ HANDLER_METHODS }, { toJsonSchema }] = await Promise.all([
|
|
16
|
+
import("@uniflowed/router/handler"),
|
|
17
|
+
import("@uniflowed/validator/json-schema"),
|
|
18
|
+
]);
|
|
19
|
+
const paths = {};
|
|
20
|
+
for (const record of handlers ?? []) {
|
|
21
|
+
const routePath = openApiPath(record.path);
|
|
22
|
+
const pathItem = paths[routePath] ?? {};
|
|
23
|
+
paths[routePath] = pathItem;
|
|
24
|
+
const module = await loadHandlerModule(record, pathItem);
|
|
25
|
+
if (module == null) continue;
|
|
26
|
+
for (const method of implementedMethods(module, HANDLER_METHODS)) {
|
|
27
|
+
const operation = createOperation(record, method, schemaFor(module, method), toJsonSchema);
|
|
28
|
+
if (STANDARD_METHODS.has(method)) {
|
|
29
|
+
pathItem[method.toLowerCase()] = operation;
|
|
30
|
+
} else if (method === "QUERY") {
|
|
31
|
+
pathItem[QUERY_EXTENSION] = operation;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
openapi: "3.1.0",
|
|
37
|
+
info: {
|
|
38
|
+
title: options.title ?? "uf application",
|
|
39
|
+
version: options.version ?? "0.0.0",
|
|
40
|
+
},
|
|
41
|
+
paths,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function loadHandlerModule(record, pathItem) {
|
|
46
|
+
try {
|
|
47
|
+
return await record.load();
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (record.file != null) pathItem["x-uf-source"] = record.file;
|
|
50
|
+
pathItem["x-uf-schema-unavailable"] = errorMessage(error);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function implementedMethods(module, methods) {
|
|
56
|
+
return methods.filter(
|
|
57
|
+
(method) =>
|
|
58
|
+
typeof module[method] === "function" ||
|
|
59
|
+
(method === "HEAD" && typeof module.GET === "function"),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function schemaFor(module, method) {
|
|
64
|
+
const table = module.schemas;
|
|
65
|
+
if (!isRecord(table)) return null;
|
|
66
|
+
const schema = table[method];
|
|
67
|
+
if (schema == null) return null;
|
|
68
|
+
if (!isRecord(schema)) {
|
|
69
|
+
throw new Error(`route handler schemas.${method} must be an object`);
|
|
70
|
+
}
|
|
71
|
+
return schema;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function createOperation(record, method, schema, toJsonSchema) {
|
|
75
|
+
const parameters = pathParameters(record);
|
|
76
|
+
const operation = {
|
|
77
|
+
operationId: operationId(record, method),
|
|
78
|
+
responses: untypedResponses(method),
|
|
79
|
+
};
|
|
80
|
+
if (record.file != null) operation["x-uf-source"] = record.file;
|
|
81
|
+
if (parameters.length > 0) operation.parameters = parameters;
|
|
82
|
+
if (schema == null) {
|
|
83
|
+
operation["x-uf-untyped"] = true;
|
|
84
|
+
return operation;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const unrepresentable = [];
|
|
88
|
+
if (schema.query != null) {
|
|
89
|
+
addQuerySchema(operation, exportSchema(toJsonSchema, schema.query, "query", unrepresentable));
|
|
90
|
+
}
|
|
91
|
+
if (schema.body != null) {
|
|
92
|
+
operation.requestBody = {
|
|
93
|
+
required: true,
|
|
94
|
+
content: {
|
|
95
|
+
[JSON]: { schema: exportSchema(toJsonSchema, schema.body, "body", unrepresentable) },
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (schema.response != null) {
|
|
100
|
+
operation.responses = {
|
|
101
|
+
"200": {
|
|
102
|
+
description: method === "HEAD" ? "Typed response headers" : "Typed response",
|
|
103
|
+
content:
|
|
104
|
+
method === "HEAD"
|
|
105
|
+
? undefined
|
|
106
|
+
: {
|
|
107
|
+
[JSON]: {
|
|
108
|
+
schema: exportSchema(toJsonSchema, schema.response, "response", unrepresentable),
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
if (method === "HEAD") delete operation.responses["200"].content;
|
|
114
|
+
}
|
|
115
|
+
if (unrepresentable.length > 0) {
|
|
116
|
+
operation["x-uf-unrepresentable"] = unrepresentable;
|
|
117
|
+
}
|
|
118
|
+
return operation;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function untypedResponses(method) {
|
|
122
|
+
return {
|
|
123
|
+
"200": {
|
|
124
|
+
description: method === "HEAD" ? "Untyped response headers" : "Untyped response",
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function exportSchema(toJsonSchema, schema, part, unrepresentable) {
|
|
130
|
+
const exported = toJsonSchema(schema);
|
|
131
|
+
for (const item of exported.unrepresentable) {
|
|
132
|
+
unrepresentable.push({ part, path: item.path, kind: item.kind });
|
|
133
|
+
}
|
|
134
|
+
return stripDialect(exported.schema);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function addQuerySchema(operation, schema) {
|
|
138
|
+
if (schema.type !== "object" || !isRecord(schema.properties) || hasOwn(schema, "$defs")) {
|
|
139
|
+
operation["x-uf-query-schema"] = schema;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
143
|
+
const parameters = Object.keys(schema.properties).map((name) => ({
|
|
144
|
+
name,
|
|
145
|
+
in: "query",
|
|
146
|
+
required: required.has(name),
|
|
147
|
+
schema: schema.properties[name],
|
|
148
|
+
}));
|
|
149
|
+
operation.parameters = [...(operation.parameters ?? []), ...parameters];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function stripDialect(value) {
|
|
153
|
+
if (Array.isArray(value)) return value.map(stripDialect);
|
|
154
|
+
if (!isRecord(value)) return value;
|
|
155
|
+
const out = {};
|
|
156
|
+
for (const key of Object.keys(value)) {
|
|
157
|
+
if (key === "$schema") continue;
|
|
158
|
+
out[key] = stripDialect(value[key]);
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function openApiPath(routePath) {
|
|
164
|
+
return routePath
|
|
165
|
+
.split("/")
|
|
166
|
+
.map((segment) => {
|
|
167
|
+
if (!segment.startsWith(":")) return segment;
|
|
168
|
+
return `{${segment.endsWith("*") ? segment.slice(1, -1) : segment.slice(1)}}`;
|
|
169
|
+
})
|
|
170
|
+
.join("/");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function pathParameters(record) {
|
|
174
|
+
const params = record.params ?? paramsFromPath(record.path);
|
|
175
|
+
return params.map((param) => {
|
|
176
|
+
const parameter = {
|
|
177
|
+
name: param.name,
|
|
178
|
+
in: "path",
|
|
179
|
+
required: true,
|
|
180
|
+
schema: { type: "string" },
|
|
181
|
+
};
|
|
182
|
+
if (param.catchAll) {
|
|
183
|
+
parameter.description = "Catch-all route segment, slash-separated in the URL.";
|
|
184
|
+
}
|
|
185
|
+
return parameter;
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function paramsFromPath(routePath) {
|
|
190
|
+
return routePath
|
|
191
|
+
.split("/")
|
|
192
|
+
.filter((segment) => segment.startsWith(":"))
|
|
193
|
+
.map((segment) => ({
|
|
194
|
+
name: segment.endsWith("*") ? segment.slice(1, -1) : segment.slice(1),
|
|
195
|
+
catchAll: segment.endsWith("*"),
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function operationId(record, method) {
|
|
200
|
+
const name = `${method.toLowerCase()} ${record.path}`;
|
|
201
|
+
return name
|
|
202
|
+
.replaceAll(/[^a-zA-Z0-9]+/g, " ")
|
|
203
|
+
.trim()
|
|
204
|
+
.replaceAll(/ ([a-zA-Z0-9])/g, (_, char) => char.toUpperCase());
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function isRecord(value) {
|
|
208
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function hasOwn(value, key) {
|
|
212
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function errorMessage(error) {
|
|
216
|
+
if (error instanceof Error) return error.message;
|
|
217
|
+
return String(error);
|
|
218
|
+
}
|
package/internal/routes.js
CHANGED
|
@@ -78,6 +78,52 @@ export const LAYOUT_PROP_NAMES = Object.freeze(["children", "params"]);
|
|
|
78
78
|
const PAGE_EXTENSIONS = [".js", ".jsx", ".mdx"];
|
|
79
79
|
const MODULE_EXTENSIONS = [".js", ".jsx"];
|
|
80
80
|
|
|
81
|
+
/** Application targets the route scanner knows how to select files for. */
|
|
82
|
+
export const ROUTE_TARGETS = Object.freeze(["web", "native", "ios", "android"]);
|
|
83
|
+
|
|
84
|
+
const TARGET_VARIANTS = Object.freeze({
|
|
85
|
+
web: ["web", null],
|
|
86
|
+
native: ["native", null],
|
|
87
|
+
ios: ["ios", "native", null],
|
|
88
|
+
android: ["android", "native", null],
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The route target a loaded `uf.config.js` and an optional CLI flag describe.
|
|
93
|
+
*
|
|
94
|
+
* `react-native` is accepted as the config-shaped spelling of the same target
|
|
95
|
+
* `uf build --target native` selects. The default follows the framework
|
|
96
|
+
* preset rather than the target list: uf's default list names both web and
|
|
97
|
+
* React Native, so the list is a promise the project should keep satisfying,
|
|
98
|
+
* not the one build to run when none was requested.
|
|
99
|
+
*/
|
|
100
|
+
export function resolveRouteTarget(config = {}, requested = null) {
|
|
101
|
+
const app = config.app ?? {};
|
|
102
|
+
const named =
|
|
103
|
+
requested == null || requested === ""
|
|
104
|
+
? app.framework === "react-native"
|
|
105
|
+
? "native"
|
|
106
|
+
: "web"
|
|
107
|
+
: requested === "react-native"
|
|
108
|
+
? "native"
|
|
109
|
+
: requested;
|
|
110
|
+
if (!ROUTE_TARGETS.includes(named)) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`uf: ${JSON.stringify(named)} is not an application target; choose web, native, ios or android`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
const declared = app.targets;
|
|
116
|
+
if (Array.isArray(declared)) {
|
|
117
|
+
const needs = named === "web" ? "web" : "react-native";
|
|
118
|
+
if (!declared.includes(needs)) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`uf: --target ${named} needs app.targets to include ${JSON.stringify(needs)}`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return named;
|
|
125
|
+
}
|
|
126
|
+
|
|
81
127
|
/** Deepest directory nesting the scan will follow. */
|
|
82
128
|
const MAX_DEPTH = 32;
|
|
83
129
|
|
|
@@ -254,6 +300,7 @@ const MAX_DEPTH = 32;
|
|
|
254
300
|
* about.
|
|
255
301
|
*
|
|
256
302
|
* @param {string} appRoot absolute path of the router root (`app/`)
|
|
303
|
+
* @param {{target?: "web" | "native" | "ios" | "android"}} [options]
|
|
257
304
|
* @returns {{
|
|
258
305
|
* routes: Route[],
|
|
259
306
|
* handlers: Handler[],
|
|
@@ -262,7 +309,8 @@ const MAX_DEPTH = 32;
|
|
|
262
309
|
* errors: ErrorBoundary[],
|
|
263
310
|
* }}
|
|
264
311
|
*/
|
|
265
|
-
export function scanRoutes(appRoot) {
|
|
312
|
+
export function scanRoutes(appRoot, options = {}) {
|
|
313
|
+
const target = resolveRouteTarget({}, options.target ?? "web");
|
|
266
314
|
const routes = [];
|
|
267
315
|
const handlers = [];
|
|
268
316
|
const middleware = [];
|
|
@@ -283,7 +331,7 @@ export function scanRoutes(appRoot) {
|
|
|
283
331
|
// A `$default.js` answers one question — what a slot renders when the
|
|
284
332
|
// URL says nothing about it — and this walk is everywhere a slot is not,
|
|
285
333
|
// so one found here is a file nothing would ever open.
|
|
286
|
-
const strayDefault = findModule(directory, RESERVED.default, PAGE_EXTENSIONS);
|
|
334
|
+
const strayDefault = findModule(directory, RESERVED.default, PAGE_EXTENSIONS, target);
|
|
287
335
|
if (strayDefault != null) {
|
|
288
336
|
throw new Error(
|
|
289
337
|
`${strayDefault}: \`$default.js\` is what a \`@slot\` renders when the URL says ` +
|
|
@@ -293,7 +341,7 @@ export function scanRoutes(appRoot) {
|
|
|
293
341
|
);
|
|
294
342
|
}
|
|
295
343
|
|
|
296
|
-
const ownLayout = findModule(directory, RESERVED.layout, MODULE_EXTENSIONS);
|
|
344
|
+
const ownLayout = findModule(directory, RESERVED.layout, MODULE_EXTENSIONS, target);
|
|
297
345
|
const nextLayouts = ownLayout ? [...layouts, ownLayout] : layouts;
|
|
298
346
|
if (depth === 0) {
|
|
299
347
|
rootLayouts = nextLayouts;
|
|
@@ -305,7 +353,7 @@ export function scanRoutes(appRoot) {
|
|
|
305
353
|
// `nextLayouts.length` is therefore the count taken after the own layout is
|
|
306
354
|
// added, not before. A segment with a loading file and no layout of its own
|
|
307
355
|
// still gets a boundary — it just shares its parent's frame.
|
|
308
|
-
const ownLoading = findModule(directory, RESERVED.loading, MODULE_EXTENSIONS);
|
|
356
|
+
const ownLoading = findModule(directory, RESERVED.loading, MODULE_EXTENSIONS, target);
|
|
309
357
|
const nextLoading = ownLoading
|
|
310
358
|
? [...loading, { above: nextLayouts.length, module: ownLoading }]
|
|
311
359
|
: loading;
|
|
@@ -316,7 +364,7 @@ export function scanRoutes(appRoot) {
|
|
|
316
364
|
// Every template above a route is on that route, one inside the next, for
|
|
317
365
|
// the reason every layout is — the difference between the two is a `key`,
|
|
318
366
|
// not a shape.
|
|
319
|
-
const ownTemplate = findModule(directory, RESERVED.template, MODULE_EXTENSIONS);
|
|
367
|
+
const ownTemplate = findModule(directory, RESERVED.template, MODULE_EXTENSIONS, target);
|
|
320
368
|
const nextTemplates = ownTemplate
|
|
321
369
|
? [...templates, { above: nextLayouts.length, module: ownTemplate }]
|
|
322
370
|
: templates;
|
|
@@ -339,6 +387,7 @@ export function scanRoutes(appRoot) {
|
|
|
339
387
|
segments,
|
|
340
388
|
ownLayout,
|
|
341
389
|
nextLayouts.length,
|
|
390
|
+
target,
|
|
342
391
|
depth,
|
|
343
392
|
),
|
|
344
393
|
];
|
|
@@ -347,12 +396,12 @@ export function scanRoutes(appRoot) {
|
|
|
347
396
|
// A middleware guards this directory and everything below it, whether or
|
|
348
397
|
// not this directory is itself a route: `app/dashboard/$middleware.js`
|
|
349
398
|
// with no `$page.js` beside it still guards `/dashboard/settings`.
|
|
350
|
-
const ownMiddleware = findModule(directory, RESERVED.middleware, MODULE_EXTENSIONS);
|
|
399
|
+
const ownMiddleware = findModule(directory, RESERVED.middleware, MODULE_EXTENSIONS, target);
|
|
351
400
|
if (ownMiddleware) {
|
|
352
401
|
middleware.push({ path: routeFromSegments(segments).path, module: ownMiddleware });
|
|
353
402
|
}
|
|
354
403
|
|
|
355
|
-
const page = findModule(directory, RESERVED.page, PAGE_EXTENSIONS);
|
|
404
|
+
const page = findModule(directory, RESERVED.page, PAGE_EXTENSIONS, target);
|
|
356
405
|
if (page) {
|
|
357
406
|
const { path: routePath, pattern, params } = routeFromSegments(segments);
|
|
358
407
|
routes.push({
|
|
@@ -370,7 +419,7 @@ export function scanRoutes(appRoot) {
|
|
|
370
419
|
// A handler answers the request itself, so it takes no layouts and is not
|
|
371
420
|
// MDX. It may sit beside a page: `/feed` can render for a browser and
|
|
372
421
|
// `/feed.xml` answer for a reader, and both are the same directory tree.
|
|
373
|
-
const handler = findModule(directory, RESERVED.route, MODULE_EXTENSIONS);
|
|
422
|
+
const handler = findModule(directory, RESERVED.route, MODULE_EXTENSIONS, target);
|
|
374
423
|
if (handler) {
|
|
375
424
|
const { path: routePath, pattern, params } = routeFromSegments(segments);
|
|
376
425
|
handlers.push({ path: routePath, pattern, params, module: handler });
|
|
@@ -380,7 +429,7 @@ export function scanRoutes(appRoot) {
|
|
|
380
429
|
// `app/guide/$not-found.js` was never looked for and a reader who
|
|
381
430
|
// followed a stale link into the manual was answered by the site's root
|
|
382
431
|
// 404, outside the manual's own layout. See ubugeeei-prod/uf#263.
|
|
383
|
-
const ownNotFound = findModule(directory, RESERVED.notFound, PAGE_EXTENSIONS);
|
|
432
|
+
const ownNotFound = findModule(directory, RESERVED.notFound, PAGE_EXTENSIONS, target);
|
|
384
433
|
if (ownNotFound) {
|
|
385
434
|
notFound.push({
|
|
386
435
|
path: routeFromSegments(segments).path,
|
|
@@ -392,7 +441,7 @@ export function scanRoutes(appRoot) {
|
|
|
392
441
|
|
|
393
442
|
// `errors` is the boundaries a project declares, not failures that
|
|
394
443
|
// happened: one entry per directory holding an `$error.js`.
|
|
395
|
-
const ownError = findModule(directory, RESERVED.error, MODULE_EXTENSIONS);
|
|
444
|
+
const ownError = findModule(directory, RESERVED.error, MODULE_EXTENSIONS, target);
|
|
396
445
|
if (ownError) {
|
|
397
446
|
errors.push({
|
|
398
447
|
path: routeFromSegments(segments).path,
|
|
@@ -495,10 +544,11 @@ export function scanRoutes(appRoot) {
|
|
|
495
544
|
* @param {ReadonlyArray<string>} segments the declaring segments, for the URL
|
|
496
545
|
* @param {?string} ownLayout the declaring segment's own layout, or `null`
|
|
497
546
|
* @param {number} above how many layouts are outside the slot
|
|
547
|
+
* @param {"web" | "native" | "ios" | "android"} target application target
|
|
498
548
|
* @param {number} depth nesting depth, against `MAX_DEPTH`
|
|
499
549
|
* @returns {Slot}
|
|
500
550
|
*/
|
|
501
|
-
function scanSlot(parent, directoryName, name, segments, ownLayout, above, depth) {
|
|
551
|
+
function scanSlot(parent, directoryName, name, segments, ownLayout, above, target, depth) {
|
|
502
552
|
const directory = path.join(parent, directoryName);
|
|
503
553
|
if (LAYOUT_PROP_NAMES.includes(name)) {
|
|
504
554
|
throw new Error(
|
|
@@ -525,7 +575,7 @@ function scanSlot(parent, directoryName, name, segments, ownLayout, above, depth
|
|
|
525
575
|
}
|
|
526
576
|
|
|
527
577
|
const routes = [];
|
|
528
|
-
const defaultPage = findModule(directory, RESERVED.default, PAGE_EXTENSIONS);
|
|
578
|
+
const defaultPage = findModule(directory, RESERVED.default, PAGE_EXTENSIONS, target);
|
|
529
579
|
|
|
530
580
|
const walkSlot = (current, currentSegments, layouts, atSlotRoot, currentDepth) => {
|
|
531
581
|
if (currentDepth > MAX_DEPTH) return;
|
|
@@ -534,7 +584,8 @@ function scanSlot(parent, directoryName, name, segments, ownLayout, above, depth
|
|
|
534
584
|
// is left of parallel routes; see the issue.
|
|
535
585
|
for (const role of [RESERVED.notFound, RESERVED.error, RESERVED.loading, RESERVED.template]) {
|
|
536
586
|
const found =
|
|
537
|
-
findModule(current, role, MODULE_EXTENSIONS) ??
|
|
587
|
+
findModule(current, role, MODULE_EXTENSIONS, target) ??
|
|
588
|
+
findModule(current, role, PAGE_EXTENSIONS, target);
|
|
538
589
|
if (found != null) {
|
|
539
590
|
throw new Error(
|
|
540
591
|
`${found}: a \`@slot\` renders a page and the layouts inside the slot, and has no ` +
|
|
@@ -546,7 +597,7 @@ function scanSlot(parent, directoryName, name, segments, ownLayout, above, depth
|
|
|
546
597
|
}
|
|
547
598
|
}
|
|
548
599
|
for (const role of [RESERVED.route, RESERVED.middleware]) {
|
|
549
|
-
const found = findModule(current, role, MODULE_EXTENSIONS);
|
|
600
|
+
const found = findModule(current, role, MODULE_EXTENSIONS, target);
|
|
550
601
|
if (found != null) {
|
|
551
602
|
throw new Error(
|
|
552
603
|
`${found}: a \`@slot\` renders inside the page at a URL and answers no request of its ` +
|
|
@@ -559,15 +610,16 @@ function scanSlot(parent, directoryName, name, segments, ownLayout, above, depth
|
|
|
559
610
|
// One default per slot, at the slot. A deeper one would be a second answer
|
|
560
611
|
// to a question that is asked once — the URL either addressed this slot or
|
|
561
612
|
// it did not.
|
|
562
|
-
|
|
613
|
+
const nestedDefault = findModule(current, RESERVED.default, PAGE_EXTENSIONS, target);
|
|
614
|
+
if (!atSlotRoot && nestedDefault != null) {
|
|
563
615
|
throw new Error(
|
|
564
|
-
`${
|
|
616
|
+
`${nestedDefault}: a \`@slot\` has one ` +
|
|
565
617
|
`\`$default.js\`, directly inside \`${directoryName}\`, and this one is deeper, so ` +
|
|
566
618
|
"nothing would ever render it.",
|
|
567
619
|
);
|
|
568
620
|
}
|
|
569
621
|
|
|
570
|
-
const layoutHere = findModule(current, RESERVED.layout, MODULE_EXTENSIONS);
|
|
622
|
+
const layoutHere = findModule(current, RESERVED.layout, MODULE_EXTENSIONS, target);
|
|
571
623
|
const nextLayouts = layoutHere ? [...layouts, layoutHere] : layouts;
|
|
572
624
|
|
|
573
625
|
const entries = readdirSync(current, { withFileTypes: true }).sort((a, b) =>
|
|
@@ -589,12 +641,13 @@ function scanSlot(parent, directoryName, name, segments, ownLayout, above, depth
|
|
|
589
641
|
currentSegments,
|
|
590
642
|
layoutHere,
|
|
591
643
|
nextLayouts.length,
|
|
644
|
+
target,
|
|
592
645
|
currentDepth,
|
|
593
646
|
),
|
|
594
647
|
];
|
|
595
648
|
}
|
|
596
649
|
|
|
597
|
-
const page = findModule(current, RESERVED.page, PAGE_EXTENSIONS);
|
|
650
|
+
const page = findModule(current, RESERVED.page, PAGE_EXTENSIONS, target);
|
|
598
651
|
if (page) {
|
|
599
652
|
const { path: routePath, params } = routeFromSegments(currentSegments);
|
|
600
653
|
routes.push({
|
|
@@ -647,13 +700,16 @@ function isDirectory(candidate) {
|
|
|
647
700
|
}
|
|
648
701
|
}
|
|
649
702
|
|
|
650
|
-
function findModule(directory, stem, extensions) {
|
|
651
|
-
for (const
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
703
|
+
function findModule(directory, stem, extensions, target = "web") {
|
|
704
|
+
for (const variant of TARGET_VARIANTS[target] ?? TARGET_VARIANTS.web) {
|
|
705
|
+
for (const extension of extensions) {
|
|
706
|
+
const fileName = variant == null ? `${stem}${extension}` : `${stem}.${variant}${extension}`;
|
|
707
|
+
const candidate = path.join(directory, fileName);
|
|
708
|
+
try {
|
|
709
|
+
if (statSync(candidate).isFile()) return candidate;
|
|
710
|
+
} catch {
|
|
711
|
+
// keep looking
|
|
712
|
+
}
|
|
657
713
|
}
|
|
658
714
|
}
|
|
659
715
|
return null;
|
package/internal/rsc.js
CHANGED
|
@@ -47,13 +47,12 @@ export const RSC_MANIFEST_ENV = "UF_RSC_MANIFEST";
|
|
|
47
47
|
/**
|
|
48
48
|
* The manifest schema this understands.
|
|
49
49
|
*
|
|
50
|
-
* Version
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* `"reaches-boundary"`, and quietly drop every route from the client bundle.
|
|
50
|
+
* Version 3 lets client boundaries name package specifiers as well as project
|
|
51
|
+
* paths. An older manifest is therefore refused rather than read
|
|
52
|
+
* optimistically: it cannot know a route imports a package client module, and
|
|
53
|
+
* quietly dropping that route would be the worst possible answer.
|
|
55
54
|
*/
|
|
56
|
-
const SUPPORTED_VERSION =
|
|
55
|
+
const SUPPORTED_VERSION = 3;
|
|
57
56
|
|
|
58
57
|
/**
|
|
59
58
|
* Read the RSC manifest, or `null` when there is nothing usable to read.
|
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.21",
|
|
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",
|
|
@@ -34,8 +34,10 @@
|
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@mdx-js/rollup": "^3.1.1",
|
|
36
36
|
"@shikijs/rehype": "^3.23.0",
|
|
37
|
-
"@uniflowed/host": "0.0.0-alpha.
|
|
38
|
-
"@uniflowed/
|
|
37
|
+
"@uniflowed/host": "0.0.0-alpha.21",
|
|
38
|
+
"@uniflowed/router": "0.0.0-alpha.21",
|
|
39
|
+
"@uniflowed/server": "0.0.0-alpha.21",
|
|
40
|
+
"@uniflowed/validator": "0.0.0-alpha.21",
|
|
39
41
|
"rehype-slug": "^6.0.0",
|
|
40
42
|
"remark-frontmatter": "^5.0.0",
|
|
41
43
|
"remark-gfm": "^4.0.1",
|