@uniflowed/vite 0.0.0-alpha.1 → 0.0.0-alpha.11
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 +932 -93
- package/index.js +384 -26
- package/internal/assets.js +396 -0
- package/internal/config.js +22 -7
- package/internal/events.js +22 -0
- package/internal/flow-grammar-shim.js +241 -0
- package/internal/flow-keywords.js +505 -0
- package/internal/highlight.js +181 -0
- package/internal/http.js +79 -0
- package/internal/refresh-runtime.js +221 -246
- package/internal/refresh.js +2 -0
- package/internal/routes.js +391 -34
- package/internal/rsc.js +151 -0
- package/internal/serve.js +345 -0
- package/merge.js +87 -0
- package/package.json +10 -10
- package/bun-preload.js +0 -22
- package/internal/node-hooks.js +0 -111
- package/register.js +0 -11
- package/transform.js +0 -187
package/internal/node-hooks.js
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
// Plain JavaScript: this *is* the loader, so it cannot be Flow.
|
|
2
|
-
//
|
|
3
|
-
// Node.js module customization hooks that transform Flow on import.
|
|
4
|
-
//
|
|
5
|
-
// Registered by `@uniflowed/vite/register` (through `node:module`'s
|
|
6
|
-
// `register()`), which makes `node --import @uniflowed/vite/register app.js`
|
|
7
|
-
// run a Flow project directly: every `.js` module uf is responsible for is
|
|
8
|
-
// transformed as it is loaded through `uf transform`, and everything else is
|
|
9
|
-
// left to Node.
|
|
10
|
-
//
|
|
11
|
-
// Transforms are cached on disk under `.uf/cache/transform/` keyed by a hash
|
|
12
|
-
// of the source, so a second run of the same file is a read rather than a
|
|
13
|
-
// round trip. The cache is content-addressed: an edited file hashes
|
|
14
|
-
// differently, so there is no invalidation to get wrong.
|
|
15
|
-
|
|
16
|
-
import { createHash } from "node:crypto";
|
|
17
|
-
import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
18
|
-
import path from "node:path";
|
|
19
|
-
import { fileURLToPath } from "node:url";
|
|
20
|
-
|
|
21
|
-
import { isFlowModule, transformFlow } from "../transform.js";
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Write `contents` to `target` so a concurrent reader never sees half of it.
|
|
25
|
-
*
|
|
26
|
-
* `uf test` runs one of these processes per core and they all import the same
|
|
27
|
-
* few modules at once, so two writers and a reader meet on the same cache
|
|
28
|
-
* entry constantly. `writeFileSync` is not atomic — a reader can observe a
|
|
29
|
-
* truncated file and report a module that "does not provide an export" — so
|
|
30
|
-
* the content goes to a private temporary name first and is then renamed,
|
|
31
|
-
* which is atomic within a filesystem.
|
|
32
|
-
*
|
|
33
|
-
* A failure here is not a failure: a read-only checkout still runs, just
|
|
34
|
-
* without the cache.
|
|
35
|
-
*/
|
|
36
|
-
function writeAtomically(target, contents) {
|
|
37
|
-
const temporary = `${target}.${process.pid}.${Math.random().toString(36).slice(2)}`;
|
|
38
|
-
try {
|
|
39
|
-
mkdirSync(cacheDirectory, { recursive: true });
|
|
40
|
-
writeFileSync(temporary, contents);
|
|
41
|
-
renameSync(temporary, target);
|
|
42
|
-
} catch {
|
|
43
|
-
try {
|
|
44
|
-
unlinkSync(temporary);
|
|
45
|
-
} catch {
|
|
46
|
-
// Nothing to clean up.
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Bumped whenever the transform's output shape changes, to retire old entries. */
|
|
52
|
-
const CACHE_VERSION = "2";
|
|
53
|
-
|
|
54
|
-
let cacheDirectory = null;
|
|
55
|
-
let root = null;
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Called once by `register()` with `{ root }`; the cache lives under it and
|
|
59
|
-
* the transform service is started there so it reads the right config.
|
|
60
|
-
*/
|
|
61
|
-
export async function initialize(data) {
|
|
62
|
-
root = data?.root ?? process.cwd();
|
|
63
|
-
cacheDirectory = path.join(root, ".uf", "cache", "transform");
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* The `load` hook: transform Flow modules, defer everything else.
|
|
68
|
-
*/
|
|
69
|
-
export async function load(url, context, nextLoad) {
|
|
70
|
-
if (!url.startsWith("file:")) return nextLoad(url, context);
|
|
71
|
-
const filename = fileURLToPath(url);
|
|
72
|
-
if (!isFlowModule(filename)) return nextLoad(url, context);
|
|
73
|
-
|
|
74
|
-
const source = readFileSync(filename, "utf8");
|
|
75
|
-
const code = await cachedTransform(source, filename);
|
|
76
|
-
if (code == null) return nextLoad(url, context);
|
|
77
|
-
// uf projects are ES modules. Forcing the format here means a project whose
|
|
78
|
-
// package.json forgot `"type": "module"` still runs, rather than failing on
|
|
79
|
-
// an `import` in what Node would have guessed was CommonJS.
|
|
80
|
-
return { format: "module", source: code, shortCircuit: true };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async function cachedTransform(source, filename) {
|
|
84
|
-
const key = createHash("sha256")
|
|
85
|
-
.update(CACHE_VERSION)
|
|
86
|
-
.update("\0")
|
|
87
|
-
.update(filename)
|
|
88
|
-
.update("\0")
|
|
89
|
-
.update(source)
|
|
90
|
-
.digest("hex");
|
|
91
|
-
const entry = cacheDirectory ? path.join(cacheDirectory, `${key}.mjs`) : null;
|
|
92
|
-
|
|
93
|
-
if (entry) {
|
|
94
|
-
try {
|
|
95
|
-
return readFileSync(entry, "utf8");
|
|
96
|
-
} catch {
|
|
97
|
-
// not cached yet
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const out = await transformFlow(source, filename, { root, development: true, sourceMap: true });
|
|
102
|
-
if (out == null) return null;
|
|
103
|
-
const output = out.map
|
|
104
|
-
? `${out.code}\n//# sourceMappingURL=data:application/json;base64,${Buffer.from(out.map).toString("base64")}\n`
|
|
105
|
-
: out.code;
|
|
106
|
-
|
|
107
|
-
if (entry) {
|
|
108
|
-
writeAtomically(entry, output);
|
|
109
|
-
}
|
|
110
|
-
return output;
|
|
111
|
-
}
|
package/register.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
// Plain JavaScript: this file registers the loader, so it cannot need one.
|
|
2
|
-
//
|
|
3
|
-
// `node --import @uniflowed/vite/register app.js` runs a Flow project on
|
|
4
|
-
// Node.js without a build step. Importing this module installs the hooks in
|
|
5
|
-
// `./internal/node-hooks.js` for the rest of the process.
|
|
6
|
-
|
|
7
|
-
import { register } from "node:module";
|
|
8
|
-
|
|
9
|
-
register("./internal/node-hooks.js", import.meta.url, {
|
|
10
|
-
data: { root: process.env.UF_PROJECT_ROOT ?? process.cwd() },
|
|
11
|
-
});
|
package/transform.js
DELETED
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
// Plain JavaScript: executed by the host that runs Vite, before any transform
|
|
2
|
-
// exists — this module is how the transform is reached, so it cannot be Flow.
|
|
3
|
-
//
|
|
4
|
-
// The Flow → JavaScript transform lives in `uf` itself (`crates/uf_transform`:
|
|
5
|
-
// the official Flow parser, Flow's own lowering rules, the official React
|
|
6
|
-
// Compiler, oxc for JSX and code generation). This module is the JavaScript
|
|
7
|
-
// side of the `uf transform` service: one long-lived `uf` process per host
|
|
8
|
-
// process, newline-delimited JSON in, replies in request order out.
|
|
9
|
-
//
|
|
10
|
-
// Every host that runs Flow — the Vite plugin, the Node loader hook, the Bun
|
|
11
|
-
// preload, the config loader — goes through here, which is what makes them
|
|
12
|
-
// all produce the same module from the same source.
|
|
13
|
-
|
|
14
|
-
import { spawn } from "node:child_process";
|
|
15
|
-
import { createInterface } from "node:readline";
|
|
16
|
-
|
|
17
|
-
/** File extensions uf treats as Flow source. */
|
|
18
|
-
export const FLOW_EXTENSIONS = [".js", ".jsx", ".mjs", ".cjs"];
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Whether uf is responsible for transforming this module.
|
|
22
|
-
*
|
|
23
|
-
* Mirrors `uf_transform::is_flow_module`, and must keep mirroring it: a `uf
|
|
24
|
-
* dev` session and a `uf test` run that disagree about which files are Flow
|
|
25
|
-
* disagree about what the code is.
|
|
26
|
-
*
|
|
27
|
-
* A build driver synthesises modules of its own (`\0vite/client`, Rolldown's
|
|
28
|
-
* shims), and a third-party dependency ships JavaScript that is already
|
|
29
|
-
* JavaScript; neither is Flow. `@uniflowed/*` under `node_modules` is the
|
|
30
|
-
* deliberate exception: those packages ship Flow source, because that is what
|
|
31
|
-
* uf tells everyone to write.
|
|
32
|
-
*/
|
|
33
|
-
export function isFlowModule(id) {
|
|
34
|
-
if (id.startsWith("\0")) return false;
|
|
35
|
-
const clean = stripQuery(id);
|
|
36
|
-
if (!FLOW_EXTENSIONS.some((extension) => clean.endsWith(extension))) return false;
|
|
37
|
-
const at = clean.lastIndexOf("/node_modules/");
|
|
38
|
-
return at === -1 || clean.slice(at).startsWith("/node_modules/@uniflowed/");
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function stripQuery(id) {
|
|
42
|
-
const at = id.indexOf("?");
|
|
43
|
-
return at === -1 ? id : id.slice(0, at);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* The `uf` binary to talk to.
|
|
48
|
-
*
|
|
49
|
-
* `uf dev`, `uf build` and `uf test` set `UF_BINARY` to themselves when they
|
|
50
|
-
* start a host, so the host reaches exactly the binary that started it. A host
|
|
51
|
-
* started by hand finds `uf` on PATH, which is what the installer arranges.
|
|
52
|
-
*/
|
|
53
|
-
export function ufBinary() {
|
|
54
|
-
return process.env.UF_BINARY ?? "uf";
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* An error the transform reported for one module, with its position when
|
|
59
|
-
* the parser or the lowering rules gave one.
|
|
60
|
-
*/
|
|
61
|
-
export class TransformError extends Error {
|
|
62
|
-
constructor(id, message, line, column) {
|
|
63
|
-
super(message);
|
|
64
|
-
this.name = "TransformError";
|
|
65
|
-
this.id = id;
|
|
66
|
-
this.loc = line != null ? { file: id, line, column: column ?? 0 } : undefined;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* One `uf transform` process, with requests answered in the order they were
|
|
72
|
-
* sent.
|
|
73
|
-
*
|
|
74
|
-
* `uf transform` replies once per request, in order, so a plain queue of
|
|
75
|
-
* resolvers pairs a reply with its caller — no correlation ids and no map to
|
|
76
|
-
* leak. Any exit is final: a request made after the process has gone is
|
|
77
|
-
* rejected at once rather than queued against something that will never
|
|
78
|
-
* answer.
|
|
79
|
-
*/
|
|
80
|
-
export class TransformService {
|
|
81
|
-
#child;
|
|
82
|
-
#pending = [];
|
|
83
|
-
#failure = null;
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* @param {object} [options]
|
|
87
|
-
* @param {string} [options.command] the `uf` binary; `ufBinary()` by default
|
|
88
|
-
* @param {string} [options.root] project root, so `uf.config.js` is found
|
|
89
|
-
*/
|
|
90
|
-
constructor(options = {}) {
|
|
91
|
-
const command = options.command ?? ufBinary();
|
|
92
|
-
const root = options.root ?? process.cwd();
|
|
93
|
-
this.#child = spawn(command, ["--cwd", root, "transform"], {
|
|
94
|
-
stdio: ["pipe", "pipe", "inherit"],
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
createInterface({ input: this.#child.stdout }).on("line", (line) => {
|
|
98
|
-
const waiting = this.#pending.shift();
|
|
99
|
-
if (!waiting) return;
|
|
100
|
-
let reply;
|
|
101
|
-
try {
|
|
102
|
-
reply = JSON.parse(line);
|
|
103
|
-
} catch {
|
|
104
|
-
waiting.reject(new Error(`uf transform sent a malformed reply: ${line}`));
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
if (reply.error != null) {
|
|
108
|
-
waiting.reject(new TransformError(waiting.id, reply.error, reply.line, reply.column));
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
waiting.resolve(reply);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
this.#child.on("error", (error) => {
|
|
115
|
-
this.#settleAll(new Error(`could not run \`${command} transform\`: ${error.message}`));
|
|
116
|
-
});
|
|
117
|
-
this.#child.on("close", (code) => {
|
|
118
|
-
this.#settleAll(new Error(`uf transform exited (${code})`));
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
#settleAll(error) {
|
|
123
|
-
this.#failure = error;
|
|
124
|
-
while (this.#pending.length > 0) this.#pending.shift().reject(error);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Transform one module.
|
|
129
|
-
*
|
|
130
|
-
* Resolves to `{ code, map, diagnostics }`, or to `null` when the module is
|
|
131
|
-
* not uf's to transform (see `isFlowModule`). Rejects with a
|
|
132
|
-
* `TransformError` carrying the position when the source is not valid Flow.
|
|
133
|
-
*
|
|
134
|
-
* @param {string} id absolute path, used for the map and for errors
|
|
135
|
-
* @param {string} code the Flow source
|
|
136
|
-
* @param {object} [options]
|
|
137
|
-
* @param {boolean} [options.development] readable output, `jsxDEV`
|
|
138
|
-
* @param {boolean} [options.refresh] Fast Refresh registrations (development only)
|
|
139
|
-
* @param {boolean} [options.sourceMap] produce a source map; on by default
|
|
140
|
-
*/
|
|
141
|
-
transform(id, code, options = {}) {
|
|
142
|
-
if (this.#failure) return Promise.reject(this.#failure);
|
|
143
|
-
return new Promise((resolve, reject) => {
|
|
144
|
-
this.#pending.push({
|
|
145
|
-
id,
|
|
146
|
-
reject,
|
|
147
|
-
resolve: (reply) => {
|
|
148
|
-
if (reply.code == null) {
|
|
149
|
-
resolve(null);
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
resolve({ code: reply.code, map: reply.map ?? null, diagnostics: reply.diagnostics ?? [] });
|
|
153
|
-
},
|
|
154
|
-
});
|
|
155
|
-
this.#child.stdin.write(`${JSON.stringify({ id, code, options })}\n`);
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/** Stop the process. Outstanding requests are rejected. */
|
|
160
|
-
close() {
|
|
161
|
-
this.#child.stdin.end();
|
|
162
|
-
this.#child.kill();
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
let shared = null;
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* The process-wide service, started on first use.
|
|
170
|
-
*
|
|
171
|
-
* The loader hooks and the config loader share one process per host rather
|
|
172
|
-
* than one per module; it lives as long as the host does.
|
|
173
|
-
*/
|
|
174
|
-
export function sharedService(root) {
|
|
175
|
-
shared ??= new TransformService({ root: root ?? process.env.UF_PROJECT_ROOT ?? process.cwd() });
|
|
176
|
-
return shared;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Transform one Flow module through the shared service.
|
|
181
|
-
*
|
|
182
|
-
* Returns `{ code, map, diagnostics }`; a module that is not uf's to transform
|
|
183
|
-
* comes back as `null`.
|
|
184
|
-
*/
|
|
185
|
-
export function transformFlow(code, filename, options = {}) {
|
|
186
|
-
return sharedService(options.root).transform(filename, code, options);
|
|
187
|
-
}
|