@uniflowed/vite 0.0.0-alpha.34 → 0.0.0-alpha.35

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/internal/serve.js CHANGED
@@ -213,7 +213,46 @@ export async function loadBuild({ root, outDir, serverDir }) {
213
213
  const entry = await import(pathToFileURL(entryFile).href);
214
214
  await deployment();
215
215
  const build = await buildIdentity(root, serverDir);
216
- return { entry, assets: assetsFromManifest(manifest), distDir, root, build };
216
+ return {
217
+ entry,
218
+ assets: await documentAssetsFor(path.resolve(root, serverDir), manifest),
219
+ distDir,
220
+ root,
221
+ build,
222
+ };
223
+ }
224
+
225
+ /** What `uf build` records a document's tags in, beside the server bundle. */
226
+ export const DOCUMENT_ASSETS_FILE = "uf-document-assets.json";
227
+
228
+ /**
229
+ * The tags a served document needs: the ones `uf build` recorded, or — for a
230
+ * build from before it recorded them — the client manifest's.
231
+ *
232
+ * Recorded rather than recomputed, because the client manifest is no longer the
233
+ * whole answer. An application React Server Components render links the
234
+ * stylesheets its rsc graph emitted and the ones each client module's chunk
235
+ * carries, and neither is reachable from the client entry the manifest is walked
236
+ * from — so a server that recomputed the tags rendered every page without the
237
+ * stylesheets its prerendered pages had. `uf start`, `uf preview`, `--adapter`
238
+ * and `--compile` all read them from here.
239
+ *
240
+ * @param {string} serverDir absolute path of the server bundle's directory
241
+ * @param {object} manifest the client build's Vite manifest
242
+ */
243
+ export async function documentAssetsFor(serverDir, manifest) {
244
+ const file = path.join(serverDir, DOCUMENT_ASSETS_FILE);
245
+ let recorded;
246
+ try {
247
+ recorded = await readFile(file, "utf8");
248
+ } catch {
249
+ return assetsFromManifest(manifest);
250
+ }
251
+ try {
252
+ return JSON.parse(recorded);
253
+ } catch {
254
+ throw new Error(`uf: ${file} is not the JSON \`uf build\` writes; run \`uf build\` again`);
255
+ }
217
256
  }
218
257
 
219
258
  /**
@@ -304,7 +343,14 @@ async function readable(file, message) {
304
343
  * at build time, and bakes the answer into what it emits.
305
344
  */
306
345
  export function assetsFromManifest(manifest) {
307
- const entry = Object.values(manifest).find((chunk) => chunk.isEntry);
346
+ // `client` by name first. An application React Server Components render
347
+ // gives the client build one entry per client module as well, and the
348
+ // document's script is the application's entry, not whichever of those the
349
+ // manifest happens to list first.
350
+ const chunks = Object.values(manifest);
351
+ const entry =
352
+ chunks.find((chunk) => chunk.isEntry && chunk.name === "client") ??
353
+ chunks.find((chunk) => chunk.isEntry);
308
354
  if (entry == null) throw new Error("uf: the client manifest has no entry chunk");
309
355
 
310
356
  const styles = new Set(entry.css ?? []);
@@ -0,0 +1,170 @@
1
+ // @noflow
2
+ //
3
+ // Plain JavaScript: `../driver.js` imports this before any transform exists.
4
+ //
5
+ // The Node built-ins a Cloudflare Worker does not provide at the compatibility
6
+ // date `uf build --adapter edge` writes, and what the build says about a module
7
+ // that reaches one.
8
+ //
9
+ // # Where the list comes from
10
+ //
11
+ // A measurement rather than a summary of the documentation. Each module below
12
+ // was imported statically into a Worker — so Wrangler applied the polyfills it
13
+ // applies to uf's own bundle — served by `wrangler dev --local` at compatibility
14
+ // date 2024-09-23 with `nodejs_compat`, and the function beside it was called.
15
+ // Every one threw `[unenv] <function> is not implemented yet!`.
16
+ // `tools/ci/edge-worker-smoke.sh` builds that probe from this table on every run
17
+ // and fails when a module here stops throwing or stops importing, so the list
18
+ // cannot go on claiming something workerd no longer does.
19
+ //
20
+ // Whole modules only. `net` answers `connect` and refuses `createServer`, and
21
+ // `dgram`, `inspector` and `worker_threads` construct objects without throwing;
22
+ // a module that partly works is not one a build can name as absent. A call that
23
+ // fails in one of those fails at request time, where `@uniflowed/server/edge`
24
+ // names the function in the log line.
25
+ //
26
+ // # Reported, not refused
27
+ //
28
+ // The build goes on. The date is the one uf writes into `wrangler.json`, and a
29
+ // project that deploys with a `wrangler.json` of its own at a newer date may
30
+ // have the module — `node:fs` gained a virtual filesystem at a later one. A
31
+ // refusal here would stop a deployment uf cannot see from being built at all,
32
+ // so what uf owes is the sentence: which module, which file reached it, and what
33
+ // a request that reaches it answers on uf's date.
34
+
35
+ /** The compatibility date the table was measured at; `wrangler.json` pins it. */
36
+ export const WORKERS_COMPATIBILITY_DATE = "2024-09-23";
37
+
38
+ /**
39
+ * Every module a Worker provides only as a stub at that date, with the call
40
+ * that shows it.
41
+ *
42
+ * `construct` is for the one entry whose representative member is a class.
43
+ * The smoke builds `new wasi.WASI(...)` from it rather than a call.
44
+ */
45
+ export const UNAVAILABLE_ON_WORKERS = Object.freeze([
46
+ Object.freeze({ module: "child_process", member: "execFileSync", args: ["true"] }),
47
+ Object.freeze({ module: "cluster", member: "fork", args: [] }),
48
+ Object.freeze({ module: "fs", member: "readFileSync", args: ["/tmp/uf-probe"] }),
49
+ Object.freeze({ module: "http", member: "createServer", args: [] }),
50
+ Object.freeze({ module: "http2", member: "createServer", args: [] }),
51
+ Object.freeze({ module: "https", member: "createServer", args: [] }),
52
+ Object.freeze({ module: "repl", member: "start", args: [] }),
53
+ Object.freeze({
54
+ module: "wasi",
55
+ member: "WASI",
56
+ construct: true,
57
+ args: [{ version: "preview1" }],
58
+ }),
59
+ ]);
60
+
61
+ /** How many reached modules one build names before it counts the rest. */
62
+ export const MAX_NAMED = 8;
63
+
64
+ /**
65
+ * The table entry `specifier` reaches, or `null` when it reaches none.
66
+ *
67
+ * `node:fs`, `fs` and `node:fs/promises` are all `fs`: the prefix is optional
68
+ * in an import and a subpath is the same module's functions under another name.
69
+ */
70
+ export function unavailableOnWorkers(specifier) {
71
+ if (typeof specifier !== "string") return null;
72
+ const bare = specifier.startsWith("node:") ? specifier.slice("node:".length) : specifier;
73
+ const name = bare.split("/")[0];
74
+ return UNAVAILABLE_ON_WORKERS.find((entry) => entry.module === name) ?? null;
75
+ }
76
+
77
+ /**
78
+ * The reached imports that are still in the output once the bundler has shaken
79
+ * the graph.
80
+ *
81
+ * Resolution sees every import of every module the graph names, including
82
+ * modules that contribute nothing to the bundle. Measured on the
83
+ * `rsc-split-app` fixture: a client component's top-level
84
+ * `import { expect, it } from "@uniflowed/test"`, kept for an in-source test
85
+ * that compiles to `void 0` in a build, reached `node:fs` and
86
+ * `node:child_process` through `@uniflowed/test` and `@uniflowed/host` — and not
87
+ * one byte of either was in the Worker. A warning about a module the Worker does
88
+ * not contain is a warning every edge project would learn to ignore.
89
+ *
90
+ * So an import counts only when a chunk of the output still imports that
91
+ * module *and* the importing module rendered code into that chunk. `chunks` is
92
+ * the bundle's chunk entries, as `generateBundle` receives them: `imports` is
93
+ * the chunk's external imports, `modules` its modules with `renderedLength`.
94
+ */
95
+ export function survivingImports(reached, chunks) {
96
+ return reached.filter(({ specifier, importer }) => {
97
+ const entry = unavailableOnWorkers(specifier);
98
+ if (entry == null) return false;
99
+ return chunks.some(
100
+ (chunk) =>
101
+ (chunk.imports ?? []).some((name) => unavailableOnWorkers(name)?.module === entry.module) &&
102
+ (chunk.modules?.[importer]?.renderedLength ?? 0) > 0,
103
+ );
104
+ });
105
+ }
106
+
107
+ /**
108
+ * The warnings one edge build prints, one line per module and importer.
109
+ *
110
+ * `reached` is `{ specifier, importer }` in the order the bundler met them,
111
+ * `root` the project; an importer inside the project and outside
112
+ * `node_modules` is the project's own code and is named before any
113
+ * dependency, because it is the one a reader can change. Bounded by
114
+ * [`MAX_NAMED`], with the rest counted, so a dependency that reaches a stub
115
+ * from a hundred files cannot print a hundred lines.
116
+ */
117
+ export function workerBuiltinWarnings(reached, root) {
118
+ const seen = new Set();
119
+ const lines = [];
120
+ for (const { specifier, importer } of reached) {
121
+ const entry = unavailableOnWorkers(specifier);
122
+ if (entry == null || importer == null) continue;
123
+ const key = `${entry.module}\0${importer}`;
124
+ if (seen.has(key)) continue;
125
+ seen.add(key);
126
+ const inside = isProjectModule(importer, root);
127
+ const from = inside ? relative(importer, root) : dependencyName(importer);
128
+ lines.push({
129
+ inside,
130
+ text:
131
+ `${from} imports node:${entry.module}, which a Cloudflare Worker at compatibility date ` +
132
+ `${WORKERS_COMPATIBILITY_DATE} provides only as a stub: every function of it throws. ` +
133
+ (inside
134
+ ? "A request that reaches it will answer 500 on this target."
135
+ : "The dependency may never call it here; if a request does, it answers 500."),
136
+ });
137
+ }
138
+ lines.sort((a, b) => Number(b.inside) - Number(a.inside));
139
+ const named = lines.slice(0, MAX_NAMED).map((line) => `--adapter edge: ${line.text}`);
140
+ if (lines.length > MAX_NAMED) {
141
+ named.push(
142
+ `--adapter edge: and ${lines.length - MAX_NAMED} more importers of the same modules`,
143
+ );
144
+ }
145
+ return named;
146
+ }
147
+
148
+ function isProjectModule(importer, root) {
149
+ const normalised = importer.split("\\").join("/");
150
+ const base = root.split("\\").join("/").replace(/\/$/, "");
151
+ return normalised.startsWith(`${base}/`) && !normalised.includes("/node_modules/");
152
+ }
153
+
154
+ function relative(importer, root) {
155
+ const base = root.split("\\").join("/").replace(/\/$/, "");
156
+ return importer
157
+ .split("\\")
158
+ .join("/")
159
+ .slice(base.length + 1);
160
+ }
161
+
162
+ /** `a dependency (name)`, from the last `node_modules/` segment of a path. */
163
+ function dependencyName(importer) {
164
+ const normalised = importer.split("\\").join("/");
165
+ const at = normalised.lastIndexOf("/node_modules/");
166
+ if (at === -1) return `a module outside the project (${normalised})`;
167
+ const rest = normalised.slice(at + "/node_modules/".length).split("/");
168
+ const name = rest[0]?.startsWith("@") ? `${rest[0]}/${rest[1]}` : rest[0];
169
+ return `the dependency ${name}`;
170
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniflowed/vite",
3
- "version": "0.0.0-alpha.34",
3
+ "version": "0.0.0-alpha.35",
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,16 +34,18 @@
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.34",
38
- "@uniflowed/router": "0.0.0-alpha.34",
39
- "@uniflowed/server": "0.0.0-alpha.34",
40
- "@uniflowed/validator": "0.0.0-alpha.34",
37
+ "@uniflowed/host": "0.0.0-alpha.35",
38
+ "@uniflowed/router": "0.0.0-alpha.35",
39
+ "@uniflowed/server": "0.0.0-alpha.35",
40
+ "@uniflowed/validator": "0.0.0-alpha.35",
41
+ "estree-util-value-to-estree": "^3.5.0",
41
42
  "rehype-slug": "^6.0.0",
42
43
  "remark-frontmatter": "^5.0.0",
43
44
  "remark-gfm": "^4.0.1",
44
- "remark-mdx-frontmatter": "^5.2.0",
45
45
  "shiki": "^3.23.0",
46
- "vite": "^8.2.2"
46
+ "unist-util-mdx-define": "^1.1.2",
47
+ "vite": "^8.2.2",
48
+ "yaml": "^2.9.1"
47
49
  },
48
50
  "peerDependencies": {
49
51
  "react": ">=19",