@uniflowed/vite 0.0.0-alpha.4 → 0.0.0-alpha.41
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 +2269 -188
- package/index.js +1180 -80
- package/internal/a11y-runtime.js +234 -0
- package/internal/a11y.js +102 -0
- package/internal/assets.js +644 -0
- package/internal/barrel-imports.js +459 -0
- package/internal/compile-assets.js +67 -0
- package/internal/config.js +39 -6
- package/internal/dev-state.js +161 -0
- package/internal/devtools.js +117 -0
- package/internal/diagnostics.js +369 -0
- package/internal/events.js +23 -0
- package/internal/flight.js +777 -0
- package/internal/flow-keywords.js +1 -1
- package/internal/frontmatter.js +33 -0
- package/internal/http.js +98 -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 +1581 -72
- package/internal/rsc.js +475 -0
- package/internal/serve.js +741 -0
- package/internal/server-components.js +109 -0
- package/internal/worker-builtins.js +170 -0
- package/package.json +27 -5
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
// @noflow
|
|
2
|
+
//
|
|
3
|
+
// Plain JavaScript: executed by the host that runs Vite, before any transform.
|
|
4
|
+
//
|
|
5
|
+
// `import { Switch } from "@uniflowed/ui"`, as the bundler has to see it: an
|
|
6
|
+
// import from `switch.js` (ubugeeei-prod/uf#1118).
|
|
7
|
+
//
|
|
8
|
+
// # Why a barrel import is rewritten at all
|
|
9
|
+
//
|
|
10
|
+
// `@uniflowed/ui` is imported through its barrel, which re-exports every one of
|
|
11
|
+
// its modules, and `sideEffects: false` lets a bundler drop the modules a page
|
|
12
|
+
// does not use. But that happens when a build tree-shakes, and two things
|
|
13
|
+
// happen before it:
|
|
14
|
+
//
|
|
15
|
+
// * **The rsc pass records client modules as it transforms them.**
|
|
16
|
+
// `clientReferencePlugin` in `./flight.js` makes every `"use client"` module
|
|
17
|
+
// the rsc graph *loads* an entry of the client build. A page that imported
|
|
18
|
+
// `{ Switch }` loaded the barrel, the barrel loaded all forty-one modules,
|
|
19
|
+
// and the client build of one switch was 57 files and 146 KB gzipped where
|
|
20
|
+
// an import of `switch.js` alone is 6 files and 88 KB.
|
|
21
|
+
// * **A development server evaluates what is imported.** Nothing is
|
|
22
|
+
// tree-shaken under `uf dev`, so a page using one part ran all of them.
|
|
23
|
+
//
|
|
24
|
+
// So a named import from the barrel becomes an import from the file that
|
|
25
|
+
// defines the name, before either happens: the idea behind Next.js's
|
|
26
|
+
// `optimizePackageImports`. `uf_rsc` draws the client boundary of such an
|
|
27
|
+
// import at that module too (`uf_lib::client_modules_exporting`), so the
|
|
28
|
+
// analysis and the bundle agree about what a name reaches.
|
|
29
|
+
//
|
|
30
|
+
// # Read off the barrel the project resolves
|
|
31
|
+
//
|
|
32
|
+
// Which file defines a name is read from the barrel itself: its static
|
|
33
|
+
// `import { A } from "./a.js"` and `export { A } from "./a.js"` statements, and
|
|
34
|
+
// the object literals it builds its namespaces from. Not a table kept here,
|
|
35
|
+
// because the barrel a project installed is the one whose names count.
|
|
36
|
+
//
|
|
37
|
+
// A name that reading cannot place stays an import from the barrel, and so does
|
|
38
|
+
// every form that binds no name — `import * as ui`, `export * from` and
|
|
39
|
+
// `import()` — which is the rule `uf_rsc` applies to the same forms. A barrel
|
|
40
|
+
// import left alone costs size, never correctness, for the next reason.
|
|
41
|
+
//
|
|
42
|
+
// # One module, however it is reached
|
|
43
|
+
//
|
|
44
|
+
// A target is the file's absolute path, not a package subpath, so the package
|
|
45
|
+
// can export `.` alone. It is the path the barrel's own `./switch.js` resolves
|
|
46
|
+
// to, and Vite gives an absolute, a relative and a bare import of one file the
|
|
47
|
+
// same id in every environment — the `?v=` a development server adds to a file
|
|
48
|
+
// in `node_modules` included. So a module still loading the barrel and a module
|
|
49
|
+
// rewritten past it share one `switch.js`, and one React context between their
|
|
50
|
+
// parts.
|
|
51
|
+
//
|
|
52
|
+
// # Namespaces
|
|
53
|
+
//
|
|
54
|
+
// `Dialog` is not an export of `dialog.js`. It is an object the barrel builds
|
|
55
|
+
// from `dialog.js`'s parts, and `ContextMenu`'s is built from two modules. An
|
|
56
|
+
// import of one is served from a view of the barrel, `index.js?uf-namespace=Dialog`:
|
|
57
|
+
// a module generated here that imports those parts and builds the same object.
|
|
58
|
+
// It is the barrel's own path with a query, so its relative imports resolve the
|
|
59
|
+
// way the barrel's do.
|
|
60
|
+
|
|
61
|
+
import { readFileSync, statSync } from "node:fs";
|
|
62
|
+
import path from "node:path";
|
|
63
|
+
|
|
64
|
+
import { normalizePath, parseAst } from "vite";
|
|
65
|
+
|
|
66
|
+
/** The packages whose barrel imports are rewritten. */
|
|
67
|
+
export const BARREL_PACKAGES = Object.freeze(["@uniflowed/ui"]);
|
|
68
|
+
|
|
69
|
+
/** The query that makes a barrel's path a view of one of its namespaces. */
|
|
70
|
+
export const NAMESPACE_QUERY = "uf-namespace";
|
|
71
|
+
|
|
72
|
+
/** The module kinds whose imports are read, once earlier plugins made them JavaScript. */
|
|
73
|
+
const SCRIPT = /\.(?:[cm]?[jt]sx?|mdx)$/;
|
|
74
|
+
|
|
75
|
+
/** A name the barrel exports that no rewrite can place. */
|
|
76
|
+
const OPAQUE = Object.freeze({ kind: "opaque" });
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The plugin that rewrites named imports from a barrel to the files defining
|
|
80
|
+
* them, in every environment.
|
|
81
|
+
*
|
|
82
|
+
* A normal plugin, so it runs after `uf:flow` and `uf:mdx` and after Vite's own
|
|
83
|
+
* transforms: whatever the module was written in, it is JavaScript by the time
|
|
84
|
+
* its imports are read.
|
|
85
|
+
*/
|
|
86
|
+
export function barrelImportsPlugin() {
|
|
87
|
+
/** Each barrel's reading, keyed by its file and kept while the file is unchanged. */
|
|
88
|
+
const readings = new Map();
|
|
89
|
+
/** The barrels already reported as unreadable, so each is reported once. */
|
|
90
|
+
const reported = new Set();
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
name: "uf:barrel-imports",
|
|
94
|
+
|
|
95
|
+
async transform(code, id) {
|
|
96
|
+
if (id.startsWith("\0") || namespaceViewOf(id) != null) return null;
|
|
97
|
+
if (!BARREL_PACKAGES.some((name) => code.includes(name))) return null;
|
|
98
|
+
if (!SCRIPT.test(cleanId(id))) return null;
|
|
99
|
+
let program;
|
|
100
|
+
try {
|
|
101
|
+
program = parseAst(code);
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const statements = program.body.filter(importsFromBarrel);
|
|
106
|
+
if (statements.length === 0) return null;
|
|
107
|
+
|
|
108
|
+
const edits = [];
|
|
109
|
+
for (const name of BARREL_PACKAGES) {
|
|
110
|
+
const own = statements.filter((node) => node.source.value === name);
|
|
111
|
+
if (own.length === 0) continue;
|
|
112
|
+
const resolved = await this.resolve(name, id, { skipSelf: true });
|
|
113
|
+
// Left to whatever loads an external module, which reads the barrel
|
|
114
|
+
// as a whole; nothing here can change what that loads.
|
|
115
|
+
if (resolved == null || resolved.external) continue;
|
|
116
|
+
const file = cleanId(resolved.id);
|
|
117
|
+
const barrel = readBarrel(readings, file);
|
|
118
|
+
if (barrel.problem != null) {
|
|
119
|
+
if (!reported.has(file)) {
|
|
120
|
+
reported.add(file);
|
|
121
|
+
this.warn(
|
|
122
|
+
`uf: could not read which file defines each export of ${name} (${barrel.problem}), ` +
|
|
123
|
+
"so a module importing from it loads every module it re-exports",
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
this.addWatchFile?.(file);
|
|
129
|
+
for (const node of own) {
|
|
130
|
+
const replacement = rewriteStatement(node, barrel.exports, file);
|
|
131
|
+
if (replacement != null) edits.push({ node, replacement });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (edits.length === 0) return null;
|
|
135
|
+
return { code: applyEdits(code, edits), map: null };
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
load(id) {
|
|
139
|
+
const view = namespaceViewOf(id);
|
|
140
|
+
if (view == null) return null;
|
|
141
|
+
return namespaceViewSource(readBarrel(readings, view.file).exports, view.file, view.name);
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The barrel and the namespace a view module stands for, or `null` for any
|
|
148
|
+
* other id.
|
|
149
|
+
*
|
|
150
|
+
* `uf:flow` asks too: a view has the barrel's path and extension, and is not
|
|
151
|
+
* the barrel's Flow source.
|
|
152
|
+
*/
|
|
153
|
+
export function namespaceViewOf(id) {
|
|
154
|
+
const at = id.indexOf("?");
|
|
155
|
+
if (at === -1 || id.startsWith("\0")) return null;
|
|
156
|
+
const name = new URLSearchParams(id.slice(at + 1)).get(NAMESPACE_QUERY);
|
|
157
|
+
return name == null || name === "" ? null : { file: id.slice(0, at), name };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Where each name a barrel exports is defined, read from its source.
|
|
162
|
+
*
|
|
163
|
+
* The barrel is Flow, read with the TypeScript grammar: the only type syntax a
|
|
164
|
+
* barrel has is `export type { A } from` and `import { type A }`, which the two
|
|
165
|
+
* grammars spell alike. A barrel this cannot parse throws, and its importers
|
|
166
|
+
* are left as they were.
|
|
167
|
+
*
|
|
168
|
+
* Each name maps to one of three things:
|
|
169
|
+
*
|
|
170
|
+
* * `{ kind: "binding", file, name }` — an export of another module, passed
|
|
171
|
+
* through under this name;
|
|
172
|
+
* * `{ kind: "namespace", parts }` — an object literal whose every property
|
|
173
|
+
* is such a binding, each part `{ key, file, name }`;
|
|
174
|
+
* * `OPAQUE` — anything else, which stays an import from the barrel.
|
|
175
|
+
*
|
|
176
|
+
* @param {string} source
|
|
177
|
+
* @param {string} file the barrel's absolute path
|
|
178
|
+
* @returns {Map<string, object>}
|
|
179
|
+
*/
|
|
180
|
+
export function barrelExports(source, file) {
|
|
181
|
+
const program = parseAst(source, { lang: "ts" });
|
|
182
|
+
const directory = path.dirname(file);
|
|
183
|
+
const fileOf = (node) =>
|
|
184
|
+
typeof node?.value === "string" && /^\.\.?\//.test(node.value)
|
|
185
|
+
? normalizePath(path.join(directory, node.value))
|
|
186
|
+
: null;
|
|
187
|
+
|
|
188
|
+
// Every binding an import made, and every `const` object, before any export
|
|
189
|
+
// is read: `export { Dialog }` may come before the `const` it names.
|
|
190
|
+
const bindings = new Map();
|
|
191
|
+
const objects = new Map();
|
|
192
|
+
for (const node of program.body) {
|
|
193
|
+
if (node.type === "ImportDeclaration" && node.importKind !== "type") {
|
|
194
|
+
const from = fileOf(node.source);
|
|
195
|
+
for (const specifier of node.specifiers) {
|
|
196
|
+
if (specifier.importKind === "type") continue;
|
|
197
|
+
const imported =
|
|
198
|
+
specifier.type === "ImportSpecifier"
|
|
199
|
+
? nameOf(specifier.imported)
|
|
200
|
+
: specifier.type === "ImportDefaultSpecifier"
|
|
201
|
+
? "default"
|
|
202
|
+
: null;
|
|
203
|
+
bindings.set(
|
|
204
|
+
specifier.local.name,
|
|
205
|
+
from == null || imported == null ? null : { file: from, name: imported },
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const declaration = node.type === "ExportNamedDeclaration" ? node.declaration : node;
|
|
210
|
+
if (declaration?.type === "VariableDeclaration" && declaration.kind === "const") {
|
|
211
|
+
for (const declarator of declaration.declarations) {
|
|
212
|
+
if (declarator.id.type === "Identifier" && declarator.init?.type === "ObjectExpression") {
|
|
213
|
+
objects.set(declarator.id.name, declarator.init);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const local = (name) => {
|
|
220
|
+
const binding = bindings.get(name);
|
|
221
|
+
if (binding != null) return { kind: "binding", ...binding };
|
|
222
|
+
const object = objects.get(name);
|
|
223
|
+
return object == null ? OPAQUE : namespaceOf(object, bindings);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const exports = new Map();
|
|
227
|
+
for (const node of program.body) {
|
|
228
|
+
if (node.type === "ExportDefaultDeclaration") {
|
|
229
|
+
exports.set("default", OPAQUE);
|
|
230
|
+
}
|
|
231
|
+
if (node.type !== "ExportNamedDeclaration" || node.exportKind === "type") continue;
|
|
232
|
+
const { declaration } = node;
|
|
233
|
+
if (declaration != null) {
|
|
234
|
+
if (declaration.type === "VariableDeclaration") {
|
|
235
|
+
for (const declarator of declaration.declarations) {
|
|
236
|
+
if (declarator.id.type === "Identifier") {
|
|
237
|
+
exports.set(declarator.id.name, local(declarator.id.name));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
} else if (declaration.id?.type === "Identifier" && !declaration.type.startsWith("TS")) {
|
|
241
|
+
exports.set(declaration.id.name, OPAQUE);
|
|
242
|
+
}
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const from = node.source == null ? null : fileOf(node.source);
|
|
246
|
+
for (const specifier of node.specifiers) {
|
|
247
|
+
if (specifier.exportKind === "type") continue;
|
|
248
|
+
const name = nameOf(specifier.local);
|
|
249
|
+
exports.set(
|
|
250
|
+
nameOf(specifier.exported),
|
|
251
|
+
node.source == null
|
|
252
|
+
? local(name)
|
|
253
|
+
: from == null
|
|
254
|
+
? OPAQUE
|
|
255
|
+
: { kind: "binding", file: from, name },
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return exports;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* The module a view of a barrel's namespace is: the parts, imported from their
|
|
264
|
+
* files, and the object the barrel builds from them.
|
|
265
|
+
*
|
|
266
|
+
* A name that is not a namespace the barrel builds — the barrel changed under a
|
|
267
|
+
* development server after an importer was rewritten — is re-exported from the
|
|
268
|
+
* barrel itself, which answers correctly, if slowly, or with the bundler's own
|
|
269
|
+
* error for a name that is gone.
|
|
270
|
+
*
|
|
271
|
+
* @param {Map<string, object>} exports what `barrelExports` read
|
|
272
|
+
* @param {string} barrel the barrel's absolute path
|
|
273
|
+
* @param {string} name the namespace
|
|
274
|
+
*/
|
|
275
|
+
export function namespaceViewSource(exports, barrel, name) {
|
|
276
|
+
const target = exports.get(name);
|
|
277
|
+
const directory = path.dirname(barrel);
|
|
278
|
+
const specifierOf = (file) => {
|
|
279
|
+
const relative = normalizePath(path.relative(directory, file));
|
|
280
|
+
return JSON.stringify(relative.startsWith("../") ? relative : `./${relative}`);
|
|
281
|
+
};
|
|
282
|
+
if (target?.kind !== "namespace") {
|
|
283
|
+
return `export { ${printName(name)} } from ${specifierOf(barrel)};\n`;
|
|
284
|
+
}
|
|
285
|
+
const locals = new Map();
|
|
286
|
+
const taken = new Set();
|
|
287
|
+
const imports = new Map();
|
|
288
|
+
for (const part of target.parts) {
|
|
289
|
+
const key = `${part.file}\0${part.name}`;
|
|
290
|
+
if (locals.has(key)) continue;
|
|
291
|
+
let alias = IDENTIFIER.test(part.name) ? part.name : "part";
|
|
292
|
+
while (taken.has(alias)) alias = `${alias}$`;
|
|
293
|
+
taken.add(alias);
|
|
294
|
+
locals.set(key, alias);
|
|
295
|
+
const specifiers = imports.get(part.file) ?? [];
|
|
296
|
+
specifiers.push(alias === part.name ? alias : `${printName(part.name)} as ${alias}`);
|
|
297
|
+
imports.set(part.file, specifiers);
|
|
298
|
+
}
|
|
299
|
+
const lines = [...imports].map(
|
|
300
|
+
([file, specifiers]) => `import { ${specifiers.join(", ")} } from ${specifierOf(file)};`,
|
|
301
|
+
);
|
|
302
|
+
const properties = target.parts.map(
|
|
303
|
+
(part) => ` ${printName(part.key)}: ${locals.get(`${part.file}\0${part.name}`)},`,
|
|
304
|
+
);
|
|
305
|
+
lines.push(`export const ${name} = {`, ...properties, "};");
|
|
306
|
+
return `${lines.join("\n")}\n`;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** An import or re-export whose source is one of `BARREL_PACKAGES`. */
|
|
310
|
+
function importsFromBarrel(node) {
|
|
311
|
+
if (node.type === "ImportDeclaration") {
|
|
312
|
+
return node.importKind !== "type" && BARREL_PACKAGES.includes(node.source.value);
|
|
313
|
+
}
|
|
314
|
+
return (
|
|
315
|
+
node.type === "ExportNamedDeclaration" &&
|
|
316
|
+
node.source != null &&
|
|
317
|
+
node.exportKind !== "type" &&
|
|
318
|
+
BARREL_PACKAGES.includes(node.source.value)
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* The statements one import or re-export from a barrel becomes, as source, or
|
|
324
|
+
* `null` to leave it as it was.
|
|
325
|
+
*
|
|
326
|
+
* @param {object} node the statement
|
|
327
|
+
* @param {Map<string, object>} exports the barrel's reading
|
|
328
|
+
* @param {string} barrel the barrel's absolute path
|
|
329
|
+
*/
|
|
330
|
+
function rewriteStatement(node, exports, barrel) {
|
|
331
|
+
const isImport = node.type === "ImportDeclaration";
|
|
332
|
+
const specifiers = node.specifiers;
|
|
333
|
+
// A bare import binds nothing, and the barrel declares it has no side
|
|
334
|
+
// effects: what it loads is every module, for nothing.
|
|
335
|
+
if (isImport && specifiers.length === 0) return "";
|
|
336
|
+
// `import * as ui` names nothing that can be placed; see the header.
|
|
337
|
+
if (specifiers.some((specifier) => specifier.type === "ImportNamespaceSpecifier")) return null;
|
|
338
|
+
|
|
339
|
+
const kept = [];
|
|
340
|
+
let keptDefault = null;
|
|
341
|
+
const moved = new Map();
|
|
342
|
+
for (const specifier of specifiers) {
|
|
343
|
+
if (specifier.type === "ImportDefaultSpecifier") {
|
|
344
|
+
keptDefault = specifier.local.name;
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
const exported = nameOf(isImport ? specifier.imported : specifier.local);
|
|
348
|
+
const binding = isImport ? specifier.local.name : nameOf(specifier.exported);
|
|
349
|
+
const printed = (name) =>
|
|
350
|
+
name === binding ? printName(name) : `${printName(name)} as ${printName(binding)}`;
|
|
351
|
+
const target = specifier.importKind === "type" ? OPAQUE : (exports.get(exported) ?? OPAQUE);
|
|
352
|
+
if (target.kind === "opaque") {
|
|
353
|
+
kept.push(printed(exported));
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
const [source, name] =
|
|
357
|
+
target.kind === "binding"
|
|
358
|
+
? [target.file, target.name]
|
|
359
|
+
: [`${normalizePath(barrel)}?${NAMESPACE_QUERY}=${encodeURIComponent(exported)}`, exported];
|
|
360
|
+
const list = moved.get(source) ?? [];
|
|
361
|
+
list.push(printed(name));
|
|
362
|
+
moved.set(source, list);
|
|
363
|
+
}
|
|
364
|
+
if (moved.size === 0) return null;
|
|
365
|
+
|
|
366
|
+
const keyword = isImport ? "import" : "export";
|
|
367
|
+
const statements = [];
|
|
368
|
+
if (kept.length > 0 || keptDefault != null) {
|
|
369
|
+
const clause = [keptDefault, kept.length > 0 ? `{ ${kept.join(", ")} }` : null]
|
|
370
|
+
.filter(Boolean)
|
|
371
|
+
.join(", ");
|
|
372
|
+
statements.push(`${keyword} ${clause} from ${JSON.stringify(node.source.value)};`);
|
|
373
|
+
}
|
|
374
|
+
for (const [source, list] of moved) {
|
|
375
|
+
statements.push(`${keyword} { ${list.join(", ")} } from ${JSON.stringify(source)};`);
|
|
376
|
+
}
|
|
377
|
+
return statements.join("\n");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* `code` with each statement replaced, and every line after it where it was.
|
|
382
|
+
*
|
|
383
|
+
* No source map: the only lines that change are the imports themselves, and
|
|
384
|
+
* each replacement is padded to the line count it replaced, so a position
|
|
385
|
+
* anywhere after them maps as it did — the reason `clientReferencePlugin`
|
|
386
|
+
* returns `map: null` too.
|
|
387
|
+
*/
|
|
388
|
+
function applyEdits(code, edits) {
|
|
389
|
+
let out = "";
|
|
390
|
+
let at = 0;
|
|
391
|
+
for (const { node, replacement } of [...edits].sort((a, b) => a.node.start - b.node.start)) {
|
|
392
|
+
const original = code.slice(node.start, node.end);
|
|
393
|
+
const lines = original.split("\n").length - 1;
|
|
394
|
+
const written = replacement.split("\n").length - 1;
|
|
395
|
+
const text =
|
|
396
|
+
written <= lines
|
|
397
|
+
? `${replacement}${"\n".repeat(lines - written)}`
|
|
398
|
+
: `${replacement.split("\n").join(" ")}${"\n".repeat(lines)}`;
|
|
399
|
+
out += code.slice(at, node.start) + text;
|
|
400
|
+
at = node.end;
|
|
401
|
+
}
|
|
402
|
+
return out + code.slice(at);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** An object literal of imported bindings, as a namespace, or `OPAQUE`. */
|
|
406
|
+
function namespaceOf(object, bindings) {
|
|
407
|
+
const parts = [];
|
|
408
|
+
for (const property of object.properties) {
|
|
409
|
+
if (property.type !== "Property" || property.kind !== "init" || property.computed) {
|
|
410
|
+
return OPAQUE;
|
|
411
|
+
}
|
|
412
|
+
if (property.method || property.value.type !== "Identifier") return OPAQUE;
|
|
413
|
+
const key =
|
|
414
|
+
property.key.type === "Identifier"
|
|
415
|
+
? property.key.name
|
|
416
|
+
: typeof property.key.value === "string"
|
|
417
|
+
? property.key.value
|
|
418
|
+
: null;
|
|
419
|
+
const binding = bindings.get(property.value.name);
|
|
420
|
+
if (key == null || binding == null) return OPAQUE;
|
|
421
|
+
parts.push({ key, ...binding });
|
|
422
|
+
}
|
|
423
|
+
return { kind: "namespace", parts };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** A barrel's reading, re-read only when its size or modification time changes. */
|
|
427
|
+
function readBarrel(readings, file) {
|
|
428
|
+
let stats;
|
|
429
|
+
try {
|
|
430
|
+
stats = statSync(file);
|
|
431
|
+
} catch (error) {
|
|
432
|
+
return { exports: new Map(), problem: error.message };
|
|
433
|
+
}
|
|
434
|
+
const known = readings.get(file);
|
|
435
|
+
if (known != null && known.mtimeMs === stats.mtimeMs && known.size === stats.size) return known;
|
|
436
|
+
const reading = { mtimeMs: stats.mtimeMs, size: stats.size, exports: new Map(), problem: null };
|
|
437
|
+
try {
|
|
438
|
+
reading.exports = barrelExports(readFileSync(file, "utf8"), file);
|
|
439
|
+
} catch (error) {
|
|
440
|
+
reading.problem = error.message;
|
|
441
|
+
}
|
|
442
|
+
readings.set(file, reading);
|
|
443
|
+
return reading;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
447
|
+
|
|
448
|
+
function nameOf(node) {
|
|
449
|
+
return node.type === "Identifier" ? node.name : String(node.value);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function printName(name) {
|
|
453
|
+
return IDENTIFIER.test(name) ? name : JSON.stringify(name);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function cleanId(id) {
|
|
457
|
+
const at = id.indexOf("?");
|
|
458
|
+
return at === -1 ? id : id.slice(0, at);
|
|
459
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// @noflow
|
|
2
|
+
//
|
|
3
|
+
// Plain JavaScript: executed by the host that runs Vite, before any transform.
|
|
4
|
+
//
|
|
5
|
+
// The embedded copy of `dist/`, handed to the bundler as a virtual module.
|
|
6
|
+
//
|
|
7
|
+
// `uf build --compile` puts everything `uf build` wrote inside the binary, and
|
|
8
|
+
// the way it travels there is one generated module: `uf_bundle::embed` writes
|
|
9
|
+
// `export const assets = JSON.parse("…")` into `.uf/build/compile/assets.js`,
|
|
10
|
+
// where the string is base64 of every file in the output directory. For the
|
|
11
|
+
// docs site that is eight megabytes; for a project with images it is tens.
|
|
12
|
+
//
|
|
13
|
+
// # Why it does not enter the graph as the file it is
|
|
14
|
+
//
|
|
15
|
+
// Because it is uf's own data, and the module graph is where uf's *source*
|
|
16
|
+
// transform is. `crates/uf_transform` refuses a source over
|
|
17
|
+
// `MAX_SOURCE_BYTES` — 8 MiB, a ceiling that bounds the parser against a file
|
|
18
|
+
// nobody meant to compile — and `.uf/build/compile/assets.js` is a file
|
|
19
|
+
// nobody wrote at all: its size is the size of the *build output*, which has
|
|
20
|
+
// no relationship to the size of any source file and grows every time a page
|
|
21
|
+
// is added. A site that crossed the line failed with
|
|
22
|
+
//
|
|
23
|
+
// TransformError: source is 8442536 bytes, over the 8388608 byte ceiling
|
|
24
|
+
// [plugin uf:flow] docs/.uf/build/compile/assets.js
|
|
25
|
+
//
|
|
26
|
+
// which names a file the project does not own, for a limit it did not break,
|
|
27
|
+
// at the end of a build that had already succeeded.
|
|
28
|
+
//
|
|
29
|
+
// So the payload enters the graph the way a build tool's own modules do,
|
|
30
|
+
// under a NUL-prefixed id. `@uniflowed/host/transform`'s `isFlowModule`
|
|
31
|
+
// declines those by name — "a build tool synthesises modules of its own", the
|
|
32
|
+
// first sentence of its contract — so the bytes go from disk to the bundler
|
|
33
|
+
// without being parsed as Flow on the way. Raising the ceiling instead would
|
|
34
|
+
// have weakened the one thing it is for, on every real source file, to make
|
|
35
|
+
// room for a file that is not source.
|
|
36
|
+
//
|
|
37
|
+
// The file on disk is still written and still what is served, so a person
|
|
38
|
+
// debugging a compiled binary can open the thing that went into it. What
|
|
39
|
+
// changed is only which name the bundler reaches it by.
|
|
40
|
+
|
|
41
|
+
import { readFileSync } from "node:fs";
|
|
42
|
+
|
|
43
|
+
/** What the generated entry imports. */
|
|
44
|
+
export const COMPILE_ASSETS_ID = "virtual:uf/compile-assets";
|
|
45
|
+
|
|
46
|
+
/** The resolved id Vite hands back for it. */
|
|
47
|
+
export const COMPILE_ASSETS_RESOLVED_ID = `\0${COMPILE_ASSETS_ID}`;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Serve `file` — `uf_bundle::embed`'s output — as [`COMPILE_ASSETS_ID`].
|
|
51
|
+
*
|
|
52
|
+
* Read at `load` rather than when the plugin is made, so a build that fails
|
|
53
|
+
* before it reaches the entry never pays for eight megabytes of string, and
|
|
54
|
+
* so the bytes are the ones on disk at the moment the bundler asked for them
|
|
55
|
+
* rather than at the moment the plugin list was assembled.
|
|
56
|
+
*/
|
|
57
|
+
export function compileAssetsPlugin(file) {
|
|
58
|
+
return {
|
|
59
|
+
name: "uf:compile-assets",
|
|
60
|
+
resolveId(id) {
|
|
61
|
+
return id === COMPILE_ASSETS_ID ? COMPILE_ASSETS_RESOLVED_ID : null;
|
|
62
|
+
},
|
|
63
|
+
load(id) {
|
|
64
|
+
return id === COMPILE_ASSETS_RESOLVED_ID ? readFileSync(file, "utf8") : null;
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
package/internal/config.js
CHANGED
|
@@ -17,11 +17,12 @@
|
|
|
17
17
|
// data and cannot use the functions.
|
|
18
18
|
|
|
19
19
|
import { createHash } from "node:crypto";
|
|
20
|
-
import {
|
|
20
|
+
import { readFileSync } from "node:fs";
|
|
21
21
|
import path from "node:path";
|
|
22
22
|
import { pathToFileURL } from "node:url";
|
|
23
23
|
|
|
24
24
|
import { transformFlow } from "@uniflowed/host/transform";
|
|
25
|
+
import { writeAtomically } from "@uniflowed/host/write-atomically";
|
|
25
26
|
|
|
26
27
|
/** The one config file name uf reads. */
|
|
27
28
|
export const CONFIG_FILES = ["uf.config.js"];
|
|
@@ -71,7 +72,7 @@ export async function loadUfConfig(root) {
|
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
const compiled = await compileConfig(source, file, root);
|
|
74
|
-
const module = await
|
|
75
|
+
const module = await importConfigModule(compiled);
|
|
75
76
|
const config = module.default;
|
|
76
77
|
if (config == null || typeof config !== "object") {
|
|
77
78
|
throw new Error(
|
|
@@ -81,6 +82,20 @@ export async function loadUfConfig(root) {
|
|
|
81
82
|
return { config, file };
|
|
82
83
|
}
|
|
83
84
|
|
|
85
|
+
async function importConfigModule(compiled) {
|
|
86
|
+
const previous = process.env.UF_TRANSFORM_BOOTSTRAP_CONFIG;
|
|
87
|
+
process.env.UF_TRANSFORM_BOOTSTRAP_CONFIG = "1";
|
|
88
|
+
try {
|
|
89
|
+
return await import(pathToFileURL(compiled).href);
|
|
90
|
+
} finally {
|
|
91
|
+
if (previous == null) {
|
|
92
|
+
delete process.env.UF_TRANSFORM_BOOTSTRAP_CONFIG;
|
|
93
|
+
} else {
|
|
94
|
+
process.env.UF_TRANSFORM_BOOTSTRAP_CONFIG = previous;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
84
99
|
/**
|
|
85
100
|
* Transform the config to JavaScript and write it where it can be imported.
|
|
86
101
|
*
|
|
@@ -92,10 +107,21 @@ async function compileConfig(source, file, root) {
|
|
|
92
107
|
const directory = path.join(root, COMPILED_DIR);
|
|
93
108
|
const target = path.join(directory, `uf.config.${hash}.mjs`);
|
|
94
109
|
|
|
95
|
-
const out = await transformFlow(source, file, {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
110
|
+
const out = await transformFlow(source, file, {
|
|
111
|
+
root,
|
|
112
|
+
sourceMap: false,
|
|
113
|
+
configBootstrap: true,
|
|
114
|
+
});
|
|
115
|
+
const code = rewriteConfigImports(
|
|
116
|
+
rewriteRelativeImports(out?.code ?? source, path.dirname(file)),
|
|
117
|
+
);
|
|
118
|
+
// Atomically, because two `uf` commands in one project write this same path
|
|
119
|
+
// at the same time — the hash is of the source, so they agree on the name —
|
|
120
|
+
// and `writeFileSync` truncates before it writes. A reader that caught it
|
|
121
|
+
// mid-write imported a module with no exports and reported it as
|
|
122
|
+
// `uf.config.js must export default defineConfig({ ... })`, which is a
|
|
123
|
+
// sentence about a file that is perfectly correct. See ubugeeei-prod/uf#240.
|
|
124
|
+
writeAtomically(target, `// Compiled from ${file}. Do not edit; edit the source.\n${code}`);
|
|
99
125
|
return target;
|
|
100
126
|
}
|
|
101
127
|
|
|
@@ -119,6 +145,13 @@ export function rewriteRelativeImports(code, baseDirectory) {
|
|
|
119
145
|
);
|
|
120
146
|
}
|
|
121
147
|
|
|
148
|
+
function rewriteConfigImports(code) {
|
|
149
|
+
return code.replace(
|
|
150
|
+
/^\s*import\s*\{\s*defineConfig\s*\}\s*from\s*["']@uniflowed\/config["'];?\n?/gm,
|
|
151
|
+
"function defineConfig(config) {\n return config;\n}\n",
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
122
155
|
/**
|
|
123
156
|
* The JSON projection of a config object.
|
|
124
157
|
*
|