@uniflowed/vite 0.0.0-alpha.9 → 0.1.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 +1854 -190
- package/index.js +931 -121
- package/internal/a11y-runtime.js +234 -0
- package/internal/a11y.js +102 -0
- package/internal/assets.js +366 -18
- package/internal/barrel-imports.js +459 -0
- package/internal/compile-assets.js +67 -0
- package/internal/config.js +30 -3
- package/internal/dev-state.js +161 -0
- package/internal/devtools.js +117 -0
- package/internal/diagnostics.js +369 -0
- package/internal/events.js +8 -5
- package/internal/flight.js +803 -0
- package/internal/flow-keywords.js +1 -1
- package/internal/frontmatter.js +33 -0
- package/internal/http.js +21 -2
- package/internal/image-endpoint.js +118 -0
- package/internal/instrumentation.js +23 -0
- package/internal/module-graph.js +134 -0
- package/internal/native-web.js +26 -0
- package/internal/openapi.js +218 -0
- package/internal/relay.js +74 -0
- package/internal/routes.js +1294 -81
- package/internal/rsc.js +335 -11
- package/internal/serve.js +583 -29
- package/internal/server-components.js +109 -0
- package/internal/worker-builtins.js +170 -0
- package/package.json +28 -6
|
@@ -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.
|
|
3
|
+
"version": "0.1.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.
|
|
29
|
-
"@uniflowed/
|
|
38
|
+
"@uniflowed/host": "0.1.0",
|
|
39
|
+
"@uniflowed/router": "0.1.0",
|
|
40
|
+
"@uniflowed/server": "0.1.0",
|
|
41
|
+
"@uniflowed/validator": "0.1.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
|
-
"
|
|
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
|
}
|