@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 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
- plugins: [uniflowed({ root, config, target: routeTarget })],
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 { TransformService, isFlowModule, uniflowedPackages } from "@uniflowed/host/transform";
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 ??= new TransformService({ command, root });
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; a build is
748
- // done with it here.
749
- if (server == null) {
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
- const acted = await entry.callAction(asRequest);
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
- const fromHeader = request.headers[INTERCEPTED_FROM_HEADER];
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: Array.isArray(fromHeader) ? fromHeader[0] : fromHeader,
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.1.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.1.0",
39
- "@uniflowed/router": "0.1.0",
40
- "@uniflowed/server": "0.1.0",
41
- "@uniflowed/validator": "0.1.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",