@uniflowed/vite 0.1.0 → 0.2.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 +2 -1
- package/index.js +87 -8
- package/internal/rsc.js +59 -0
- package/package.json +5 -5
package/driver.js
CHANGED
|
@@ -256,7 +256,8 @@ async function viteConfig(config, mode) {
|
|
|
256
256
|
base: basePathOf(config) === "" ? "/" : `${basePathOf(config)}/`,
|
|
257
257
|
clearScreen: false,
|
|
258
258
|
customLogger: eventLogger(argument("--log-level") ?? "info"),
|
|
259
|
-
|
|
259
|
+
// One `uf transform` for every pass this process runs; see the option.
|
|
260
|
+
plugins: [uniflowed({ root, config, target: routeTarget, shareTransformAcrossBuilds: true })],
|
|
260
261
|
server: {
|
|
261
262
|
host,
|
|
262
263
|
port,
|
package/index.js
CHANGED
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
|
|
47
47
|
import { instrumentationFile } from "./internal/instrumentation.js";
|
|
48
48
|
|
|
49
|
+
import { readFileSync } from "node:fs";
|
|
49
50
|
import path from "node:path";
|
|
50
51
|
import { nativeWebPlugin } from "./internal/native-web.js";
|
|
51
52
|
|
|
@@ -79,6 +80,8 @@ import {
|
|
|
79
80
|
import {
|
|
80
81
|
RSC_MANIFEST_ENV,
|
|
81
82
|
actionReferenceSource,
|
|
83
|
+
declaresDefaultExport,
|
|
84
|
+
serverActionSource,
|
|
82
85
|
actionsModuleSource,
|
|
83
86
|
clientRouteFilter,
|
|
84
87
|
readRscManifest,
|
|
@@ -96,7 +99,12 @@ import {
|
|
|
96
99
|
scanRoutes,
|
|
97
100
|
serverModuleSource,
|
|
98
101
|
} from "./internal/routes.js";
|
|
99
|
-
import {
|
|
102
|
+
import {
|
|
103
|
+
TransformService,
|
|
104
|
+
isCompiledOutput,
|
|
105
|
+
isFlowModule,
|
|
106
|
+
uniflowedPackages,
|
|
107
|
+
} from "@uniflowed/host/transform";
|
|
100
108
|
import {
|
|
101
109
|
DEV_RSC_HOOK,
|
|
102
110
|
FLIGHT_BROWSER_DEPENDENCIES,
|
|
@@ -180,6 +188,12 @@ export function devUrlFor(id) {
|
|
|
180
188
|
* @property {object} [config] the loaded `uf.config.js` object
|
|
181
189
|
* @property {"web" | "native" | "ios" | "android"} [target] app target
|
|
182
190
|
* @property {string} [command] the `uf` binary to transform through
|
|
191
|
+
* @property {boolean} [shareTransformAcrossBuilds] keep one `uf transform` for
|
|
192
|
+
* every build these plugins run, rather than closing it when each ends. For a
|
|
193
|
+
* caller that runs several builds with the same plugins and owns the process
|
|
194
|
+
* they run in — `driver.js` — which is what makes an idle service left open
|
|
195
|
+
* at the end nobody's problem. Off by default: a process that builds many
|
|
196
|
+
* projects in turn would otherwise keep one idle `uf` per project.
|
|
183
197
|
*/
|
|
184
198
|
|
|
185
199
|
/**
|
|
@@ -252,6 +266,7 @@ export default function uniflowed(options = {}) {
|
|
|
252
266
|
flightState,
|
|
253
267
|
routing,
|
|
254
268
|
command: options.command,
|
|
269
|
+
shareTransformAcrossBuilds: options.shareTransformAcrossBuilds === true,
|
|
255
270
|
accessibility,
|
|
256
271
|
relayEnabled: builtins.relay !== false,
|
|
257
272
|
}),
|
|
@@ -282,6 +297,7 @@ function flowPlugin({
|
|
|
282
297
|
flightState,
|
|
283
298
|
routing,
|
|
284
299
|
command,
|
|
300
|
+
shareTransformAcrossBuilds,
|
|
285
301
|
accessibility,
|
|
286
302
|
relayEnabled,
|
|
287
303
|
}) {
|
|
@@ -331,8 +347,11 @@ function flowPlugin({
|
|
|
331
347
|
/** Findings held back as a dependency's, waiting to be counted out loud. */
|
|
332
348
|
let suppressed = [];
|
|
333
349
|
|
|
350
|
+
// A service that has lost its process is replaced rather than kept: under
|
|
351
|
+
// `shareTransformAcrossBuilds` one outlives a build, and a `uf transform`
|
|
352
|
+
// that died in one pass must not fail every pass after it.
|
|
334
353
|
const ensureService = () => {
|
|
335
|
-
service
|
|
354
|
+
if (service == null || !service.alive) service = new TransformService({ command, root });
|
|
336
355
|
return service;
|
|
337
356
|
};
|
|
338
357
|
|
|
@@ -664,6 +683,23 @@ function flowPlugin({
|
|
|
664
683
|
if (!isSsr(this, loadOptions) && this.environment?.name !== RSC_ENVIRONMENT) {
|
|
665
684
|
const references = actionTables().modules.get(cleanId(id));
|
|
666
685
|
if (references != null) return actionReferenceSource(references);
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
// And in the server graphs, the file itself behind a module that gives
|
|
689
|
+
// each callable export what React reads to write a form that posts before
|
|
690
|
+
// hydration. The file is imported under a query, so it is still one
|
|
691
|
+
// module; only the plain id is the wrapper. See `serverActionSource`.
|
|
692
|
+
if (!id.includes("?")) {
|
|
693
|
+
const exported = actionTables().modules.get(id);
|
|
694
|
+
if (exported != null) {
|
|
695
|
+
let source = "";
|
|
696
|
+
try {
|
|
697
|
+
source = readFileSync(id, "utf8");
|
|
698
|
+
} catch {
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
return serverActionSource(id, exported, declaresDefaultExport(source));
|
|
702
|
+
}
|
|
667
703
|
}
|
|
668
704
|
return null;
|
|
669
705
|
},
|
|
@@ -672,6 +708,10 @@ function flowPlugin({
|
|
|
672
708
|
// A view of a barrel's namespace has the barrel's path and none of its
|
|
673
709
|
// source: `uf:barrel-imports` generates it as JavaScript.
|
|
674
710
|
if (!isFlowModule(id) || namespaceViewOf(id) != null) return null;
|
|
711
|
+
// What an earlier pass of this build wrote under `.uf/build/` — the ssr
|
|
712
|
+
// pass imports the rsc graph's bundle — is already this transform's
|
|
713
|
+
// output. See `isCompiledOutput`.
|
|
714
|
+
if (isCompiledOutput(id)) return null;
|
|
675
715
|
// Both server graphs: neither gets a refresh wrapper, and the rsc graph's
|
|
676
716
|
// findings are reported as that graph's.
|
|
677
717
|
const rsc = this.environment?.name === RSC_ENVIRONMENT;
|
|
@@ -744,9 +784,15 @@ function flowPlugin({
|
|
|
744
784
|
buildEnd() {
|
|
745
785
|
summariseSuppressed(this, suppressed);
|
|
746
786
|
suppressed = [];
|
|
747
|
-
// A dev server keeps its service for the whole session
|
|
748
|
-
//
|
|
749
|
-
|
|
787
|
+
// A dev server keeps its service for the whole session. A build is done
|
|
788
|
+
// with it here, unless the caller runs more builds with these plugins:
|
|
789
|
+
// `uf build`'s passes — the rsc graph, the client, the server — go over
|
|
790
|
+
// largely the same modules with the same options, and `uf transform`
|
|
791
|
+
// answers a request it has already answered from memory. A service per
|
|
792
|
+
// pass compiled `@uniflowed/router` and every shared module three times.
|
|
793
|
+
// An idle service does not hold the host open (see `TransformService`),
|
|
794
|
+
// and its child sees stdin close when the host exits.
|
|
795
|
+
if (server == null && !shareTransformAcrossBuilds) {
|
|
750
796
|
service?.close();
|
|
751
797
|
service = null;
|
|
752
798
|
}
|
|
@@ -1029,7 +1075,34 @@ function flowPlugin({
|
|
|
1029
1075
|
// to sit at the URL it was posted to. The same two lines are in
|
|
1030
1076
|
// `@uniflowed/server`'s `fetch.js`, which is what every
|
|
1031
1077
|
// deployment runs.
|
|
1032
|
-
|
|
1078
|
+
//
|
|
1079
|
+
// A form posted before its page hydrated is answered with the
|
|
1080
|
+
// page, rendered with the action's result as the submitting
|
|
1081
|
+
// `useActionState`'s state — the render below, given `formState`.
|
|
1082
|
+
const acted = await entry.callAction(asRequest, {
|
|
1083
|
+
postback: async (formState) => {
|
|
1084
|
+
const result = await entry.render(
|
|
1085
|
+
url,
|
|
1086
|
+
{ scripts: [devUrlFor(VIRTUAL.client)], styles: [], preloads: [] },
|
|
1087
|
+
{
|
|
1088
|
+
onError: (error) => reportRenderError(devServer, url, error),
|
|
1089
|
+
transformHead: (head) =>
|
|
1090
|
+
devServer.transformIndexHtml(
|
|
1091
|
+
url,
|
|
1092
|
+
flightState == null
|
|
1093
|
+
? head
|
|
1094
|
+
: linkStylesheets(head, devStylesheets(devServer)),
|
|
1095
|
+
),
|
|
1096
|
+
formState,
|
|
1097
|
+
},
|
|
1098
|
+
);
|
|
1099
|
+
if (result.error != null) reportRenderError(devServer, url, result.error);
|
|
1100
|
+
return new Response(result.stream(), {
|
|
1101
|
+
status: result.status ?? 200,
|
|
1102
|
+
headers: { ...result.headers, "content-type": "text/html; charset=utf-8" },
|
|
1103
|
+
});
|
|
1104
|
+
},
|
|
1105
|
+
});
|
|
1033
1106
|
if (acted != null) {
|
|
1034
1107
|
await send(response, acted);
|
|
1035
1108
|
return true;
|
|
@@ -1049,10 +1122,16 @@ function flowPlugin({
|
|
|
1049
1122
|
if (payloadFor != null && (request.method === "GET" || request.method === "HEAD")) {
|
|
1050
1123
|
const query = url.includes("?") ? url.slice(url.indexOf("?")) : "";
|
|
1051
1124
|
const target = payloadFor + query;
|
|
1052
|
-
|
|
1125
|
+
// Read off the request the guard handed back, never off the
|
|
1126
|
+
// one Node received: the middleware runner takes this header
|
|
1127
|
+
// off when the page it names is one the guards of that page
|
|
1128
|
+
// would not serve, and reading the original would render it
|
|
1129
|
+
// anyway. See "A payload that renders over another page" in
|
|
1130
|
+
// `@uniflowed/router/middleware`.
|
|
1131
|
+
const interceptedFrom = asRequest.headers.get(INTERCEPTED_FROM_HEADER) ?? undefined;
|
|
1053
1132
|
const answered = await entry.flight(target, {
|
|
1054
1133
|
onError: (error) => reportRenderError(devServer, target, error),
|
|
1055
|
-
interceptedFrom
|
|
1134
|
+
interceptedFrom,
|
|
1056
1135
|
});
|
|
1057
1136
|
if (answered.error != null) reportRenderError(devServer, target, answered.error);
|
|
1058
1137
|
if (request.method === "HEAD") await answered.stream?.cancel();
|
package/internal/rsc.js
CHANGED
|
@@ -407,6 +407,65 @@ export function actionReferenceSource(actions) {
|
|
|
407
407
|
return `${lines.join("\n")}\n`;
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
+
/**
|
|
411
|
+
* The query that names a `"use server"` module's own source in a server graph.
|
|
412
|
+
*
|
|
413
|
+
* In a server graph the plain id of such a module answers with
|
|
414
|
+
* {@link serverActionSource}, which imports the file under this query, so the
|
|
415
|
+
* file itself is still one module and the wrapper is another.
|
|
416
|
+
*/
|
|
417
|
+
export const SERVER_ACTION_IMPL_QUERY = "?uf-server-impl";
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Whether a module's source declares a default export.
|
|
421
|
+
*
|
|
422
|
+
* Read off the text because the wrapper below has to decide whether to
|
|
423
|
+
* re-export `default` before anything has parsed the file — and Flow's
|
|
424
|
+
* `component` syntax is not something Rollup's parser reads. A default export
|
|
425
|
+
* is `export default …` or a name exported `as default`; a false negative
|
|
426
|
+
* leaves a non-callable default export unreachable from the server graph, and
|
|
427
|
+
* the RSC graph already refuses a `"use server"` export that is not an async
|
|
428
|
+
* function, so what is at stake is small and written down.
|
|
429
|
+
*
|
|
430
|
+
* @param {string} source
|
|
431
|
+
*/
|
|
432
|
+
export function declaresDefaultExport(source) {
|
|
433
|
+
return (
|
|
434
|
+
/(^|[\n;])\s*export\s+default\b/.test(source) ||
|
|
435
|
+
/export\s*\{[^}]*\b(as\s+default|default)\b[^}]*\}/.test(source)
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* A server graph's stand-in for one `"use server"` module.
|
|
441
|
+
*
|
|
442
|
+
* Re-exports the file's own module — every binding, the same functions — and
|
|
443
|
+
* on the way past gives each callable export the property React reads to write
|
|
444
|
+
* a form that posts before hydration (`registerServerAction` in
|
|
445
|
+
* `@uniflowed/router/action`). Nothing about calling the functions changes;
|
|
446
|
+
* the endpoint, a route handler and a server component call exactly what they
|
|
447
|
+
* called before. See ubugeeei-prod/uf#1358.
|
|
448
|
+
*
|
|
449
|
+
* @param {string} file absolute path of the module
|
|
450
|
+
* @param {Array<{id: string, module: string, export: string}>} actions its callable exports
|
|
451
|
+
* @param {boolean} hasDefault whether the file declares a default export
|
|
452
|
+
*/
|
|
453
|
+
export function serverActionSource(file, actions, hasDefault) {
|
|
454
|
+
const impl = JSON.stringify(`${file}${SERVER_ACTION_IMPL_QUERY}`);
|
|
455
|
+
const lines = [
|
|
456
|
+
'import { registerServerAction } from "@uniflowed/router/action";',
|
|
457
|
+
`import * as impl from ${impl};`,
|
|
458
|
+
`export * from ${impl};`,
|
|
459
|
+
];
|
|
460
|
+
if (hasDefault) lines.push(`export { default } from ${impl};`);
|
|
461
|
+
for (const action of actions) {
|
|
462
|
+
lines.push(
|
|
463
|
+
`registerServerAction(impl[${JSON.stringify(action.export)}], ${JSON.stringify(action.id)});`,
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
return `${lines.join("\n")}\n`;
|
|
467
|
+
}
|
|
468
|
+
|
|
410
469
|
/**
|
|
411
470
|
* The source of `virtual:uf/actions`: the table the endpoint dials into.
|
|
412
471
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniflowed/vite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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.2.0",
|
|
39
|
+
"@uniflowed/router": "0.2.0",
|
|
40
|
+
"@uniflowed/server": "0.2.0",
|
|
41
|
+
"@uniflowed/validator": "0.2.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",
|