@uniflowed/vite 0.0.0-alpha.9 → 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.
@@ -0,0 +1,109 @@
1
+ // @noflow
2
+ //
3
+ // Plain JavaScript: executed by the host that runs Vite, before any transform.
4
+ //
5
+ // What a project needs before its routes can render as React Server Components.
6
+ //
7
+ // `@uniflowed/router` installs beside React 19.2.3, the React that Expo SDK 57
8
+ // and React Native 0.87 ship, and lists `react-server-dom-parcel` as an optional
9
+ // peer (ubugeeei-prod/uf#992). An application rendered from its modules
10
+ // (`app.rsc: false`) needs nothing more. One whose routes render as Server
11
+ // Components, which is the default, needs that package and the React it was
12
+ // released with. Without the package a build fails on an unresolved import
13
+ // somewhere inside the router, and on too old a React it fails later, inside a
14
+ // render, in terms of React's internals. So `uf:flow` asks here once, while Vite
15
+ // reads its configuration, and stops with one sentence that says what the
16
+ // project has and what to change.
17
+ //
18
+ // The router's own Server Components entries refuse the same React at run time
19
+ // (`packages/router/internal/react-version.js`). This is the earlier half of the
20
+ // same rule, for a project that builds with uf.
21
+
22
+ import fs from "node:fs";
23
+ import { createRequire } from "node:module";
24
+ import path from "node:path";
25
+ import { fileURLToPath } from "node:url";
26
+
27
+ /** The oldest React, and `react-server-dom-parcel`, that Server Components render on. */
28
+ export const SERVER_COMPONENTS_REACT = "19.3.0";
29
+
30
+ /**
31
+ * What stops the project at `root` from rendering React Server Components, as a
32
+ * sentence, or `null` when nothing does.
33
+ *
34
+ * Both packages are resolved the way the router's own imports resolve them: from
35
+ * the `@uniflowed/router` the project resolves, or from the one this package
36
+ * depends on when the project names none. A prerelease tag is ignored, so a 19.3
37
+ * canary counts as 19.3.
38
+ *
39
+ * @param {string} root
40
+ * @returns {string | null}
41
+ */
42
+ export function serverComponentsProblem(root) {
43
+ const router =
44
+ resolveFrom(path.join(root, "package.json"), "@uniflowed/router/package.json") ??
45
+ resolveFrom(fileURLToPath(import.meta.url), "@uniflowed/router/package.json") ??
46
+ path.join(root, "package.json");
47
+ const react = versionOf(resolveFrom(router, "react/package.json"));
48
+ const flight = versionOf(resolveFrom(router, "react-server-dom-parcel/package.json"));
49
+
50
+ const found = [];
51
+ if (react == null) {
52
+ found.push("no react");
53
+ } else if (!isRecentEnough(react)) {
54
+ found.push(`React ${react}`);
55
+ }
56
+ if (flight == null) {
57
+ found.push("no react-server-dom-parcel");
58
+ } else if (!isRecentEnough(flight)) {
59
+ found.push(`react-server-dom-parcel ${flight}`);
60
+ }
61
+ if (found.length === 0) {
62
+ return null;
63
+ }
64
+ return (
65
+ "uf: routes render as React Server Components unless `app.rsc` is false, and that needs " +
66
+ `react-server-dom-parcel and React ${SERVER_COMPONENTS_REACT} or newer. This project has ` +
67
+ `${found.join(" and ")}. Install react, react-dom and react-server-dom-parcel at ^19.3.0, ` +
68
+ "or set `app.rsc: false` in uf.config.js to render routes from their modules, which the " +
69
+ "router supports from React 19.2.3."
70
+ );
71
+ }
72
+
73
+ /** Where `specifier` resolves from the file at `from`, or `null`. */
74
+ function resolveFrom(from, specifier) {
75
+ try {
76
+ return createRequire(from).resolve(specifier);
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+
82
+ /** The `version` in the manifest at `file`, or `null`. */
83
+ function versionOf(file) {
84
+ if (file == null) {
85
+ return null;
86
+ }
87
+ try {
88
+ const { version } = JSON.parse(fs.readFileSync(file, "utf8"));
89
+ return typeof version === "string" ? version : null;
90
+ } catch {
91
+ return null;
92
+ }
93
+ }
94
+
95
+ /** Whether `version` is at least `SERVER_COMPONENTS_REACT`, ignoring a prerelease tag. */
96
+ function isRecentEnough(version) {
97
+ const found = /^(\d+)\.(\d+)\.(\d+)/.exec(version);
98
+ if (found == null) {
99
+ return false;
100
+ }
101
+ const needed = SERVER_COMPONENTS_REACT.split(".").map(Number);
102
+ for (let index = 0; index < 3; index += 1) {
103
+ const part = Number(found[index + 1]);
104
+ if (part !== needed[index]) {
105
+ return part > needed[index];
106
+ }
107
+ }
108
+ return true;
109
+ }
@@ -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.9",
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",
@@ -10,6 +10,14 @@
10
10
  "url": "git+https://github.com/ubugeeei-prod/uf.git",
11
11
  "directory": "packages/vite"
12
12
  },
13
+ "uf": {
14
+ "builder": {
15
+ "driver": "./driver.js",
16
+ "preload": {
17
+ "bun": "@uniflowed/host/bun-preload"
18
+ }
19
+ }
20
+ },
13
21
  "exports": {
14
22
  ".": "./index.js",
15
23
  "./driver": "./driver.js",
@@ -20,22 +28,36 @@
20
28
  "driver.js",
21
29
  "index.js",
22
30
  "internal",
23
- "merge.js"
31
+ "merge.js",
32
+ "!*.test.js"
24
33
  ],
25
34
  "dependencies": {
35
+ "@babel/core": "^7.29.7",
26
36
  "@mdx-js/rollup": "^3.1.1",
27
37
  "@shikijs/rehype": "^3.23.0",
28
- "@uniflowed/host": "0.0.0-alpha.9",
29
- "@uniflowed/server": "0.0.0-alpha.9",
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
+ "babel-plugin-relay": "^21.0.1",
43
+ "estree-util-value-to-estree": "^3.5.0",
30
44
  "rehype-slug": "^6.0.0",
31
45
  "remark-frontmatter": "^5.0.0",
32
46
  "remark-gfm": "^4.0.1",
33
- "remark-mdx-frontmatter": "^5.2.0",
34
47
  "shiki": "^3.23.0",
35
- "vite": "^8.2.2"
48
+ "unist-util-mdx-define": "^1.1.2",
49
+ "vite": "^8.2.2",
50
+ "yaml": "^2.9.1",
51
+ "rolldown": "^1.2.7"
36
52
  },
37
53
  "peerDependencies": {
54
+ "axe-core": ">=4",
38
55
  "react": ">=19",
39
56
  "react-dom": ">=19"
57
+ },
58
+ "peerDependenciesMeta": {
59
+ "axe-core": {
60
+ "optional": true
61
+ }
40
62
  }
41
63
  }