@uniflowed/vite 0.0.0-alpha.1
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/bun-preload.js +22 -0
- package/driver.js +360 -0
- package/index.js +316 -0
- package/internal/config.js +141 -0
- package/internal/events.js +68 -0
- package/internal/node-hooks.js +111 -0
- package/internal/refresh-runtime.js +670 -0
- package/internal/refresh.js +110 -0
- package/internal/routes.js +243 -0
- package/package.json +41 -0
- package/register.js +11 -0
- package/transform.js +187 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Plain JavaScript: executed by the host that runs Vite, before any transform.
|
|
2
|
+
//
|
|
3
|
+
// Loading `uf.config.js`.
|
|
4
|
+
//
|
|
5
|
+
// The config is a Flow module — `import { defineConfig } from
|
|
6
|
+
// "@uniflowed/config"` and the `// @flow` docblock are the documented shape —
|
|
7
|
+
// so no host can import it as written. It is transformed through
|
|
8
|
+
// `uf transform` like every other module, written under `.uf/config/`, and
|
|
9
|
+
// imported from there. Relative imports inside the config are rewritten to
|
|
10
|
+
// absolute URLs first, because the compiled copy does not live where the
|
|
11
|
+
// author's file does.
|
|
12
|
+
//
|
|
13
|
+
// The result is the config *object*, functions and plugin instances included.
|
|
14
|
+
// `projectConfig` is its JSON projection for the Rust side, which needs the
|
|
15
|
+
// data and cannot use the functions.
|
|
16
|
+
|
|
17
|
+
import { createHash } from "node:crypto";
|
|
18
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { pathToFileURL } from "node:url";
|
|
21
|
+
|
|
22
|
+
import { transformFlow } from "../transform.js";
|
|
23
|
+
|
|
24
|
+
/** The one config file name uf reads. */
|
|
25
|
+
export const CONFIG_FILES = ["uf.config.js"];
|
|
26
|
+
|
|
27
|
+
/** Where compiled config modules are written, relative to the project root. */
|
|
28
|
+
const COMPILED_DIR = path.join(".uf", "config");
|
|
29
|
+
|
|
30
|
+
/** Longest config file the loader accepts. */
|
|
31
|
+
const MAX_CONFIG_BYTES = 1024 * 1024;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Find `uf.config.js` at `root`, or `null`.
|
|
35
|
+
*/
|
|
36
|
+
export function findConfigFile(root) {
|
|
37
|
+
for (const name of CONFIG_FILES) {
|
|
38
|
+
const candidate = path.join(root, name);
|
|
39
|
+
try {
|
|
40
|
+
readFileSync(candidate);
|
|
41
|
+
return candidate;
|
|
42
|
+
} catch {
|
|
43
|
+
// keep looking
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Load the project's config object.
|
|
51
|
+
*
|
|
52
|
+
* A project without a config file gets `{}`, which every consumer treats as
|
|
53
|
+
* "the defaults". A config whose default export is not an object is an error
|
|
54
|
+
* at load time, named after the file, rather than a cascade of `undefined`
|
|
55
|
+
* later.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} root absolute project root
|
|
58
|
+
* @returns {Promise<{config: object, file: string | null}>}
|
|
59
|
+
*/
|
|
60
|
+
export async function loadUfConfig(root) {
|
|
61
|
+
const file = findConfigFile(root);
|
|
62
|
+
if (file == null) return { config: {}, file: null };
|
|
63
|
+
|
|
64
|
+
const source = readFileSync(file, "utf8");
|
|
65
|
+
if (source.length > MAX_CONFIG_BYTES) {
|
|
66
|
+
throw new Error(`uf: ${file} is ${source.length} bytes, over the ${MAX_CONFIG_BYTES} byte ceiling`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const compiled = await compileConfig(source, file, root);
|
|
70
|
+
const module = await import(pathToFileURL(compiled).href);
|
|
71
|
+
const config = module.default;
|
|
72
|
+
if (config == null || typeof config !== "object") {
|
|
73
|
+
throw new Error(`uf: ${path.relative(root, file)} must \`export default defineConfig({ ... })\``);
|
|
74
|
+
}
|
|
75
|
+
return { config, file };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Transform the config to JavaScript and write it where it can be imported.
|
|
80
|
+
*
|
|
81
|
+
* The file name carries a hash of the source, so an edited config is a new
|
|
82
|
+
* module and never a stale cached one — `import()` caches by URL.
|
|
83
|
+
*/
|
|
84
|
+
async function compileConfig(source, file, root) {
|
|
85
|
+
const hash = createHash("sha256").update(source).digest("hex").slice(0, 16);
|
|
86
|
+
const directory = path.join(root, COMPILED_DIR);
|
|
87
|
+
const target = path.join(directory, `uf.config.${hash}.mjs`);
|
|
88
|
+
|
|
89
|
+
const out = await transformFlow(source, file, { root, sourceMap: false });
|
|
90
|
+
const code = rewriteRelativeImports(out?.code ?? source, path.dirname(file));
|
|
91
|
+
mkdirSync(directory, { recursive: true });
|
|
92
|
+
writeFileSync(target, `// Compiled from ${file}. Do not edit; edit the source.\n${code}`);
|
|
93
|
+
return target;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Make every relative import specifier in `code` absolute.
|
|
98
|
+
*
|
|
99
|
+
* The compiled config lives under `.uf/config/`, so `./plugins/x.js` would
|
|
100
|
+
* otherwise resolve to a file that is not there. Bare specifiers are left
|
|
101
|
+
* alone: they resolve through `node_modules` from the compiled file exactly as
|
|
102
|
+
* they would from the source.
|
|
103
|
+
*
|
|
104
|
+
* Specifiers are string literals in `import`/`export … from` statements and
|
|
105
|
+
* `import()` calls; matching them textually is enough because the transform
|
|
106
|
+
* has already produced plain JavaScript with one statement per line.
|
|
107
|
+
*/
|
|
108
|
+
export function rewriteRelativeImports(code, baseDirectory) {
|
|
109
|
+
const absolute = (specifier) => pathToFileURL(path.resolve(baseDirectory, specifier)).href;
|
|
110
|
+
return code.replace(
|
|
111
|
+
/((?:\bfrom\s*|\bimport\s*\(?\s*)["'])(\.\.?\/[^"']*)(["'])/g,
|
|
112
|
+
(_, head, specifier, tail) => `${head}${absolute(specifier)}${tail}`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The JSON projection of a config object.
|
|
118
|
+
*
|
|
119
|
+
* Functions are dropped, and a Vite plugin object is reduced to its name so
|
|
120
|
+
* `uf inspect` can still say which plugins a project declares. Everything the
|
|
121
|
+
* Rust side reads — lint rules, formatter settings, tasks, hosts — is plain
|
|
122
|
+
* data and survives unchanged.
|
|
123
|
+
*/
|
|
124
|
+
export function projectConfig(config) {
|
|
125
|
+
return JSON.parse(
|
|
126
|
+
JSON.stringify(config, (key, value) => {
|
|
127
|
+
if (typeof value === "function") return undefined;
|
|
128
|
+
if (key === "plugins" && Array.isArray(value)) {
|
|
129
|
+
return value.flat(Infinity).map(pluginName).filter((name) => name != null);
|
|
130
|
+
}
|
|
131
|
+
return value;
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function pluginName(plugin) {
|
|
137
|
+
if (plugin == null || plugin === false) return null;
|
|
138
|
+
if (typeof plugin === "string") return plugin;
|
|
139
|
+
if (typeof plugin === "object" && typeof plugin.name === "string") return plugin.name;
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Plain JavaScript: executed by the host that runs Vite, before any transform.
|
|
2
|
+
//
|
|
3
|
+
// The driver's control channel.
|
|
4
|
+
//
|
|
5
|
+
// `uf dev` and `uf build` in Rust own the terminal: banners, phase timings,
|
|
6
|
+
// code frames, the summary. The driver therefore never prints for a person. It
|
|
7
|
+
// writes one JSON object per line to stdout — `{"event": "...", ...}` — and the
|
|
8
|
+
// Rust side renders them. Anything a person would read on stderr is Vite's own
|
|
9
|
+
// logger, which is redirected here as well so nothing bypasses the channel.
|
|
10
|
+
|
|
11
|
+
import { createLogger } from "vite";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Emit one event.
|
|
15
|
+
*
|
|
16
|
+
* Writes are synchronous so an event precedes a crash that follows it, and so
|
|
17
|
+
* `done` is on the pipe before the process exits.
|
|
18
|
+
*/
|
|
19
|
+
export function emit(event, fields = {}) {
|
|
20
|
+
process.stdout.write(`${JSON.stringify({ event, ...fields })}\n`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A Vite logger whose every message becomes a `log` event.
|
|
25
|
+
*
|
|
26
|
+
* `clearScreen` is a no-op: the Rust side decides what the terminal shows.
|
|
27
|
+
*/
|
|
28
|
+
export function eventLogger(level = "info") {
|
|
29
|
+
const base = createLogger(level, { allowClearScreen: false });
|
|
30
|
+
const forward = (kind) => (message, options) => {
|
|
31
|
+
if (options?.timestamp === false && message.trim() === "") return;
|
|
32
|
+
emit("log", { level: kind, message: stripAnsi(String(message)) });
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
...base,
|
|
36
|
+
info: forward("info"),
|
|
37
|
+
warn: forward("warn"),
|
|
38
|
+
warnOnce: forward("warn"),
|
|
39
|
+
error: forward("error"),
|
|
40
|
+
clearScreen() {},
|
|
41
|
+
hasErrorLogged: base.hasErrorLogged,
|
|
42
|
+
hasWarned: base.hasWarned,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const ANSI = /\[[0-9;]*m/g;
|
|
47
|
+
|
|
48
|
+
export function stripAnsi(text) {
|
|
49
|
+
return text.replace(ANSI, "");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Describe an error for the channel: message, and a location when Babel or
|
|
54
|
+
* Rolldown attached one.
|
|
55
|
+
*/
|
|
56
|
+
export function errorEvent(error) {
|
|
57
|
+
const fields = { message: stripAnsi(error?.message ?? String(error)) };
|
|
58
|
+
if (error?.loc && typeof error.loc === "object") {
|
|
59
|
+
fields.file = error.loc.file ?? error.id ?? null;
|
|
60
|
+
fields.line = error.loc.line ?? null;
|
|
61
|
+
fields.column = error.loc.column ?? null;
|
|
62
|
+
} else if (error?.id) {
|
|
63
|
+
fields.file = error.id;
|
|
64
|
+
}
|
|
65
|
+
if (typeof error?.frame === "string") fields.frame = stripAnsi(error.frame);
|
|
66
|
+
if (typeof error?.stack === "string") fields.stack = stripAnsi(error.stack);
|
|
67
|
+
return fields;
|
|
68
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
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
|
+
}
|