@uniflowed/vite 0.0.0-alpha.1 → 0.0.0-alpha.10
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
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
// @noflow
|
|
2
|
+
//
|
|
3
|
+
// Plain JavaScript: Vite imports this module directly, before any transform.
|
|
4
|
+
//
|
|
5
|
+
// `uf:asset` — what an imported image or font becomes.
|
|
6
|
+
//
|
|
7
|
+
// The name and the hook set are not new. `crates/uf_plugin/src/builtin.rs` has
|
|
8
|
+
// declared `uf:asset` — "resolves, fingerprints, and emits non-JavaScript
|
|
9
|
+
// imports", `resolveId` + `load` + `generateBundle` + `writeBundle` +
|
|
10
|
+
// `transformIndexHtml` — since before there was anything behind it, and
|
|
11
|
+
// `uf inspect` has been listing it in the resolved pipeline. This is the
|
|
12
|
+
// implementation of a plugin uf was already claiming to run.
|
|
13
|
+
//
|
|
14
|
+
// # What an import becomes
|
|
15
|
+
//
|
|
16
|
+
// ```js
|
|
17
|
+
// import hero from "./hero.jpg";
|
|
18
|
+
// <Image src={hero} alt="…" sizes="(max-width: 640px) 100vw, 640px" />
|
|
19
|
+
// ```
|
|
20
|
+
//
|
|
21
|
+
// `hero` is not a URL string. It is the manifest `crates/uf_assets` produced —
|
|
22
|
+
// the intrinsic width and height, every emitted variant with its own width, the
|
|
23
|
+
// blur placeholder — because a `srcSet` can only be written by something that
|
|
24
|
+
// knows which other sizes exist, and a URL string does not.
|
|
25
|
+
//
|
|
26
|
+
// A font import is the same shape: the self-hosted file, the `@font-face` rules
|
|
27
|
+
// that declare it, and the metric-matched fallback.
|
|
28
|
+
//
|
|
29
|
+
// # Where the work happens, and when
|
|
30
|
+
//
|
|
31
|
+
// In `uf`, over the `uf assets` protocol — one native process for the whole
|
|
32
|
+
// build rather than an image codec in the dependency tree. Both schedules go
|
|
33
|
+
// through the same process with the same parameters, and both write to the
|
|
34
|
+
// same cache directory:
|
|
35
|
+
//
|
|
36
|
+
// * **`uf build`** reads each emitted variant out of the cache and hands it to
|
|
37
|
+
// Rollup with `emitFile`, so the bundler owns what lands in `dist/` and the
|
|
38
|
+
// size report counts it.
|
|
39
|
+
// * **`uf dev`** serves the same files out of the same cache directory over a
|
|
40
|
+
// middleware, transformed on the first import and reused after.
|
|
41
|
+
//
|
|
42
|
+
// The files are named by a content hash of the source and the parameters, so
|
|
43
|
+
// the second build of an unchanged image does no work in either mode and a dev
|
|
44
|
+
// session warms the cache a build then reuses. That is also the whole of "the
|
|
45
|
+
// two must agree": there is one pipeline and one set of bytes, and the only
|
|
46
|
+
// thing that differs between them is the URL prefix they are served under.
|
|
47
|
+
//
|
|
48
|
+
// # What this plugin deliberately does not claim
|
|
49
|
+
//
|
|
50
|
+
// An import with a query — `./hero.png?url`, `?raw`, `?inline` — is left to
|
|
51
|
+
// Vite. Those are Vite's own asset conventions and a project reaching for one
|
|
52
|
+
// is reaching past uf on purpose; claiming them here would make a documented
|
|
53
|
+
// Vite feature unreachable from a uf project, which is red line 8 in
|
|
54
|
+
// `docs/red-lines.md`. `import hero from "./hero.png"` is uf's; everything
|
|
55
|
+
// with a `?` after it is Vite's.
|
|
56
|
+
|
|
57
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
58
|
+
import path from "node:path";
|
|
59
|
+
|
|
60
|
+
import { AssetService, assetKind } from "@uniflowed/host/assets";
|
|
61
|
+
|
|
62
|
+
/** Where transformed assets are kept, relative to the project root. */
|
|
63
|
+
export const CACHE_DIR = ".uf/cache/assets";
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The URL prefix a dev server answers transformed assets on.
|
|
67
|
+
*
|
|
68
|
+
* `@` first, following the convention Vite uses for everything that is not a
|
|
69
|
+
* file in the project: it cannot collide with a real path, and Vite's own
|
|
70
|
+
* middlewares leave it alone.
|
|
71
|
+
*/
|
|
72
|
+
export const DEV_PREFIX = "@uf-asset/";
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The module source for one transformed asset.
|
|
76
|
+
*
|
|
77
|
+
* A frozen object literal rather than a JSON blob assigned to a variable: this
|
|
78
|
+
* is what the component destructures, it is small, and a build that inlines it
|
|
79
|
+
* into the one component that used it is the right outcome.
|
|
80
|
+
*/
|
|
81
|
+
export function assetModuleSource(manifest) {
|
|
82
|
+
return `export default Object.freeze(${JSON.stringify(manifest)});\n`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The `srcSet` for one format, and the URLs that go in it.
|
|
87
|
+
*
|
|
88
|
+
* Written here rather than in the component so that a build and a dev server
|
|
89
|
+
* cannot produce different strings from the same manifest: the only input that
|
|
90
|
+
* differs between them is `baseUrl`, and it is an argument.
|
|
91
|
+
*/
|
|
92
|
+
export function withUrls(image, baseUrl) {
|
|
93
|
+
const variants = image.variants.map((variant) => ({
|
|
94
|
+
...variant,
|
|
95
|
+
url: `${baseUrl}${variant.file}`,
|
|
96
|
+
}));
|
|
97
|
+
// Widest last within a format, which is the order a `srcset` reads best in
|
|
98
|
+
// and the order `sizes` is evaluated against.
|
|
99
|
+
variants.sort((left, right) => left.width - right.width);
|
|
100
|
+
|
|
101
|
+
const formats = [];
|
|
102
|
+
for (const variant of variants) {
|
|
103
|
+
if (variant.format === image.format) continue;
|
|
104
|
+
if (!formats.includes(variant.format)) formats.push(variant.format);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const srcSetFor = (format) =>
|
|
108
|
+
variants
|
|
109
|
+
.filter((variant) => variant.format === format)
|
|
110
|
+
// `640w` and not `2x`: a density descriptor describes one layout width,
|
|
111
|
+
// and the whole point of the ladder is that the layout width is not
|
|
112
|
+
// known here. With `w`, the browser combines it with `sizes` and picks.
|
|
113
|
+
.map((variant) => `${variant.url} ${variant.width}w`)
|
|
114
|
+
.join(", ");
|
|
115
|
+
|
|
116
|
+
const fallbacks = variants.filter((variant) => variant.format === image.format);
|
|
117
|
+
const widest = fallbacks[fallbacks.length - 1] ?? variants[variants.length - 1];
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
src: widest?.url ?? null,
|
|
121
|
+
width: image.width,
|
|
122
|
+
height: image.height,
|
|
123
|
+
srcSet: srcSetFor(image.format),
|
|
124
|
+
// Alternatives first: a browser takes the first `<source>` it understands,
|
|
125
|
+
// so the format every browser understands must not be offered before the
|
|
126
|
+
// ones that are smaller.
|
|
127
|
+
sources: formats.map((format) => ({
|
|
128
|
+
type: variants.find((variant) => variant.format === format).mime,
|
|
129
|
+
srcSet: srcSetFor(format),
|
|
130
|
+
})),
|
|
131
|
+
blurDataURL: image.blur,
|
|
132
|
+
// Carried through so a project can see what the pipeline decided and why,
|
|
133
|
+
// rather than having to infer it from what is missing: `hero.declined` is
|
|
134
|
+
// the widths where the alternative format was encoded and came out larger,
|
|
135
|
+
// with both byte counts. Nothing prints them — a line on every build about
|
|
136
|
+
// a format that was correctly not emitted is noise — and `uf explain build`
|
|
137
|
+
// is where the limit itself is stated.
|
|
138
|
+
declined: image.declined,
|
|
139
|
+
note: image.note,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* uf's asset pipeline, as a Vite plugin.
|
|
145
|
+
*
|
|
146
|
+
* @param {object} options
|
|
147
|
+
* @param {object} [options.images] `app.builtins.images`
|
|
148
|
+
* @param {object} [options.fonts] `app.builtins.fonts`
|
|
149
|
+
* @param {string} [options.command] the `uf` binary to transform through
|
|
150
|
+
*/
|
|
151
|
+
export function assetPlugin({ images = {}, fonts = {}, command } = {}) {
|
|
152
|
+
// Both halves can be turned off independently, and a plugin that is off is
|
|
153
|
+
// still in the array: `uf inspect` lists the resolved pipeline, and a
|
|
154
|
+
// pipeline that changes shape when a feature is disabled is a pipeline whose
|
|
155
|
+
// listing cannot be compared between two projects.
|
|
156
|
+
const imagesOn = images.enabled !== false;
|
|
157
|
+
const fontsOn = fonts.enabled !== false;
|
|
158
|
+
|
|
159
|
+
let root = process.cwd();
|
|
160
|
+
let base = "/";
|
|
161
|
+
let assetsDir = "assets";
|
|
162
|
+
let isBuild = false;
|
|
163
|
+
/** @type {import("vite").ViteDevServer | null} */
|
|
164
|
+
let server = null;
|
|
165
|
+
/** @type {AssetService | null} */
|
|
166
|
+
let service = null;
|
|
167
|
+
/**
|
|
168
|
+
* The manifest for each source path, so one image is transformed once.
|
|
169
|
+
*
|
|
170
|
+
* `uf build` runs Vite twice over the same modules — once for the browser
|
|
171
|
+
* bundle and once for the server one — from a single plugin array, so both
|
|
172
|
+
* passes share this map and the second decodes nothing. It survives
|
|
173
|
+
* `buildEnd` deliberately: clearing it there is what made the server pass
|
|
174
|
+
* redo every image, which is the whole cost this map exists to avoid.
|
|
175
|
+
*/
|
|
176
|
+
const transformed = new Map();
|
|
177
|
+
|
|
178
|
+
const cacheDir = () => path.resolve(root, CACHE_DIR);
|
|
179
|
+
/**
|
|
180
|
+
* The `uf assets` process, started on the first asset and not before.
|
|
181
|
+
*
|
|
182
|
+
* Lazily rather than in `buildStart`, which is where `uf:flow` starts its
|
|
183
|
+
* transform service: that one is going to be asked about every module in the
|
|
184
|
+
* project, and this one is asked about nothing at all in a project that
|
|
185
|
+
* imports no images or fonts. A build that has no use for an image codec
|
|
186
|
+
* should not spawn one.
|
|
187
|
+
*/
|
|
188
|
+
const ensureService = () => {
|
|
189
|
+
service ??= new AssetService({ command, root });
|
|
190
|
+
return service;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Where a transformed file is served from.
|
|
195
|
+
*
|
|
196
|
+
* A build's URL is the bundler's output directory; a dev server's is this
|
|
197
|
+
* plugin's own middleware. This is the *only* thing that differs between the
|
|
198
|
+
* two schedules, and it is one string.
|
|
199
|
+
*/
|
|
200
|
+
const baseUrl = () => (isBuild ? `${base}${assetsDir}/` : `${base}${DEV_PREFIX}`);
|
|
201
|
+
|
|
202
|
+
const claims = (id) => {
|
|
203
|
+
// A query is Vite's, not uf's. See the header.
|
|
204
|
+
if (id.includes("?")) return null;
|
|
205
|
+
if (id.startsWith("\0")) return null;
|
|
206
|
+
const kind = assetKind(id);
|
|
207
|
+
if (kind === "image" && !imagesOn) return null;
|
|
208
|
+
if (kind === "font" && !fontsOn) return null;
|
|
209
|
+
return kind;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
name: "uf:asset",
|
|
214
|
+
// Before Vite's own asset handling, which would otherwise claim the same
|
|
215
|
+
// extensions and return a URL string.
|
|
216
|
+
enforce: "pre",
|
|
217
|
+
|
|
218
|
+
configResolved(config) {
|
|
219
|
+
root = config.root;
|
|
220
|
+
base = config.base;
|
|
221
|
+
assetsDir = config.build?.assetsDir ?? "assets";
|
|
222
|
+
isBuild = config.command === "build";
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
async load(id) {
|
|
226
|
+
const kind = claims(id);
|
|
227
|
+
if (kind == null) return null;
|
|
228
|
+
const file = path.resolve(id);
|
|
229
|
+
// Not this plugin's to fail on: an id with one of these extensions that
|
|
230
|
+
// is not a file on disk is a virtual module somebody else owns.
|
|
231
|
+
if (!existsSync(file)) return null;
|
|
232
|
+
|
|
233
|
+
return loadAsset.call(this, {
|
|
234
|
+
kind,
|
|
235
|
+
file,
|
|
236
|
+
transformed,
|
|
237
|
+
service: ensureService(),
|
|
238
|
+
cacheDir: cacheDir(),
|
|
239
|
+
baseUrl: baseUrl(),
|
|
240
|
+
assetsDir,
|
|
241
|
+
isBuild,
|
|
242
|
+
images,
|
|
243
|
+
fonts,
|
|
244
|
+
});
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
configureServer(devServer) {
|
|
248
|
+
server = devServer;
|
|
249
|
+
devServer.httpServer?.once("close", () => {
|
|
250
|
+
service?.close();
|
|
251
|
+
service = null;
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Before Vite's own middlewares: nothing else knows this prefix, and the
|
|
255
|
+
// files are outside the module graph, so there is nothing to wait for.
|
|
256
|
+
const directory = cacheDir();
|
|
257
|
+
devServer.middlewares.use((request, response, next) => {
|
|
258
|
+
const url = request.url ?? "";
|
|
259
|
+
const at = url.indexOf(DEV_PREFIX);
|
|
260
|
+
if (at === -1) return next();
|
|
261
|
+
const name = decodeURIComponent(url.slice(at + DEV_PREFIX.length).split("?")[0]);
|
|
262
|
+
// The name is a file name and nothing else. Every emitted name is one
|
|
263
|
+
// path segment by construction, so a request carrying a separator is
|
|
264
|
+
// not a name this plugin ever minted — refusing it rather than
|
|
265
|
+
// resolving it is what keeps the cache directory from being a way to
|
|
266
|
+
// read the rest of the disk.
|
|
267
|
+
if (name === "" || name.includes("/") || name.includes("\\") || name.includes("..")) {
|
|
268
|
+
response.statusCode = 400;
|
|
269
|
+
response.end("bad asset name");
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const target = path.join(directory, name);
|
|
273
|
+
if (!existsSync(target)) return next();
|
|
274
|
+
response.setHeader("Content-Type", contentTypeOf(name));
|
|
275
|
+
// The name is a content hash, so the bytes under it never change.
|
|
276
|
+
response.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
|
277
|
+
response.end(readFileSync(target));
|
|
278
|
+
});
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
watchChange(id) {
|
|
282
|
+
// The memo below is what stops `uf build` decoding every image twice,
|
|
283
|
+
// once per bundle. In a dev server it would also stop uf ever noticing
|
|
284
|
+
// that an image was edited: Vite invalidates the module and calls `load`
|
|
285
|
+
// again, and `load` would hand back the manifest it made before the
|
|
286
|
+
// change. Dropping both keys is cheap and the next `load` redoes the
|
|
287
|
+
// work — which, because the emitted names are content hashes, writes new
|
|
288
|
+
// files and leaves the old ones for anything still holding a URL.
|
|
289
|
+
transformed.delete(`image:${path.resolve(id)}`);
|
|
290
|
+
transformed.delete(`font:${path.resolve(id)}`);
|
|
291
|
+
},
|
|
292
|
+
|
|
293
|
+
buildEnd() {
|
|
294
|
+
// A dev server keeps its service for the whole session; a build is done
|
|
295
|
+
// with it here. The same rule `uf:flow` follows next door.
|
|
296
|
+
//
|
|
297
|
+
// `transformed` is *not* cleared. A build's second pass over the same
|
|
298
|
+
// modules then needs no process at all — every answer is already in the
|
|
299
|
+
// map, and `ensureService` is never reached.
|
|
300
|
+
if (server == null) {
|
|
301
|
+
service?.close();
|
|
302
|
+
service = null;
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Transform one asset and return the module that stands for it.
|
|
310
|
+
*
|
|
311
|
+
* Split out of the hook so the hook stays readable and so the memoisation is
|
|
312
|
+
* visible: `uf build` runs Vite twice over the same modules, once for the
|
|
313
|
+
* browser bundle and once for the server one, and an image transformed on both
|
|
314
|
+
* passes would be decoded twice for one build.
|
|
315
|
+
*/
|
|
316
|
+
async function loadAsset(context) {
|
|
317
|
+
const { kind, file, transformed, service, cacheDir, baseUrl, assetsDir, isBuild, images, fonts } =
|
|
318
|
+
context;
|
|
319
|
+
const key = `${kind}:${file}`;
|
|
320
|
+
let manifest = transformed.get(key);
|
|
321
|
+
if (manifest == null) {
|
|
322
|
+
manifest =
|
|
323
|
+
kind === "image"
|
|
324
|
+
? await service.image(file, {
|
|
325
|
+
outDir: cacheDir,
|
|
326
|
+
widths: images.widths,
|
|
327
|
+
quality: images.quality,
|
|
328
|
+
blur: images.placeholder,
|
|
329
|
+
})
|
|
330
|
+
: await service.font(file, {
|
|
331
|
+
outDir: cacheDir,
|
|
332
|
+
family: fonts.family,
|
|
333
|
+
display: fonts.display,
|
|
334
|
+
baseUrl,
|
|
335
|
+
});
|
|
336
|
+
transformed.set(key, manifest);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const files = kind === "image" ? manifest.variants.map((v) => v.file) : [manifest.file];
|
|
340
|
+
if (isBuild) {
|
|
341
|
+
// Handed to Rollup rather than copied by hand, so the bundler owns what
|
|
342
|
+
// lands in the output directory and `uf_bundle`'s size report — which
|
|
343
|
+
// walks that directory — counts every one of them.
|
|
344
|
+
for (const name of files) {
|
|
345
|
+
this.emitFile({
|
|
346
|
+
type: "asset",
|
|
347
|
+
// `fileName` rather than `name`: the name is already a content hash of
|
|
348
|
+
// the source and the parameters, and letting Rollup hash it again
|
|
349
|
+
// would move it on every encoder change while saying nothing new.
|
|
350
|
+
fileName: `${assetsDir}/${name}`,
|
|
351
|
+
source: readFileSync(path.join(cacheDir, name)),
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (kind === "image") {
|
|
357
|
+
return assetModuleSource(withUrls(manifest, baseUrl));
|
|
358
|
+
}
|
|
359
|
+
return assetModuleSource({
|
|
360
|
+
src: `${baseUrl}${manifest.file}`,
|
|
361
|
+
family: manifest.family,
|
|
362
|
+
fallbackFamily: manifest.fallbackFamily,
|
|
363
|
+
// The stack a page should set `font-family` to: the real face, then the
|
|
364
|
+
// metric-matched fallback, then the local face it was scaled from. Written
|
|
365
|
+
// here so no page has to remember that the fallback only ever applies when
|
|
366
|
+
// it is named after the real face.
|
|
367
|
+
fontFamily: [manifest.family, manifest.fallbackFamily, manifest.fallback?.local]
|
|
368
|
+
.filter((name) => name != null)
|
|
369
|
+
.map((name) => JSON.stringify(name))
|
|
370
|
+
.join(", "),
|
|
371
|
+
type: manifest.mime,
|
|
372
|
+
css: manifest.css,
|
|
373
|
+
metrics: manifest.metrics,
|
|
374
|
+
fallback: manifest.fallback,
|
|
375
|
+
fallbackDeclined: manifest.fallbackDeclined,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** The media type for one emitted file name. */
|
|
380
|
+
function contentTypeOf(name) {
|
|
381
|
+
const extension = name.slice(name.lastIndexOf(".") + 1).toLowerCase();
|
|
382
|
+
const types = {
|
|
383
|
+
avif: "image/avif",
|
|
384
|
+
gif: "image/gif",
|
|
385
|
+
jpg: "image/jpeg",
|
|
386
|
+
jpeg: "image/jpeg",
|
|
387
|
+
otf: "font/otf",
|
|
388
|
+
png: "image/png",
|
|
389
|
+
svg: "image/svg+xml",
|
|
390
|
+
ttf: "font/ttf",
|
|
391
|
+
webp: "image/webp",
|
|
392
|
+
woff: "font/woff",
|
|
393
|
+
woff2: "font/woff2",
|
|
394
|
+
};
|
|
395
|
+
return types[extension] ?? "application/octet-stream";
|
|
396
|
+
}
|
package/internal/config.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// @noflow
|
|
2
|
+
//
|
|
1
3
|
// Plain JavaScript: executed by the host that runs Vite, before any transform.
|
|
2
4
|
//
|
|
3
5
|
// Loading `uf.config.js`.
|
|
@@ -15,11 +17,12 @@
|
|
|
15
17
|
// data and cannot use the functions.
|
|
16
18
|
|
|
17
19
|
import { createHash } from "node:crypto";
|
|
18
|
-
import {
|
|
20
|
+
import { readFileSync } from "node:fs";
|
|
19
21
|
import path from "node:path";
|
|
20
22
|
import { pathToFileURL } from "node:url";
|
|
21
23
|
|
|
22
|
-
import { transformFlow } from "
|
|
24
|
+
import { transformFlow } from "@uniflowed/host/transform";
|
|
25
|
+
import { writeAtomically } from "@uniflowed/host/write-atomically";
|
|
23
26
|
|
|
24
27
|
/** The one config file name uf reads. */
|
|
25
28
|
export const CONFIG_FILES = ["uf.config.js"];
|
|
@@ -63,14 +66,18 @@ export async function loadUfConfig(root) {
|
|
|
63
66
|
|
|
64
67
|
const source = readFileSync(file, "utf8");
|
|
65
68
|
if (source.length > MAX_CONFIG_BYTES) {
|
|
66
|
-
throw new Error(
|
|
69
|
+
throw new Error(
|
|
70
|
+
`uf: ${file} is ${source.length} bytes, over the ${MAX_CONFIG_BYTES} byte ceiling`,
|
|
71
|
+
);
|
|
67
72
|
}
|
|
68
73
|
|
|
69
74
|
const compiled = await compileConfig(source, file, root);
|
|
70
75
|
const module = await import(pathToFileURL(compiled).href);
|
|
71
76
|
const config = module.default;
|
|
72
77
|
if (config == null || typeof config !== "object") {
|
|
73
|
-
throw new Error(
|
|
78
|
+
throw new Error(
|
|
79
|
+
`uf: ${path.relative(root, file)} must \`export default defineConfig({ ... })\``,
|
|
80
|
+
);
|
|
74
81
|
}
|
|
75
82
|
return { config, file };
|
|
76
83
|
}
|
|
@@ -88,8 +95,13 @@ async function compileConfig(source, file, root) {
|
|
|
88
95
|
|
|
89
96
|
const out = await transformFlow(source, file, { root, sourceMap: false });
|
|
90
97
|
const code = rewriteRelativeImports(out?.code ?? source, path.dirname(file));
|
|
91
|
-
|
|
92
|
-
|
|
98
|
+
// Atomically, because two `uf` commands in one project write this same path
|
|
99
|
+
// at the same time — the hash is of the source, so they agree on the name —
|
|
100
|
+
// and `writeFileSync` truncates before it writes. A reader that caught it
|
|
101
|
+
// mid-write imported a module with no exports and reported it as
|
|
102
|
+
// `uf.config.js must export default defineConfig({ ... })`, which is a
|
|
103
|
+
// sentence about a file that is perfectly correct. See ubugeeei-prod/uf#240.
|
|
104
|
+
writeAtomically(target, `// Compiled from ${file}. Do not edit; edit the source.\n${code}`);
|
|
93
105
|
return target;
|
|
94
106
|
}
|
|
95
107
|
|
|
@@ -126,7 +138,10 @@ export function projectConfig(config) {
|
|
|
126
138
|
JSON.stringify(config, (key, value) => {
|
|
127
139
|
if (typeof value === "function") return undefined;
|
|
128
140
|
if (key === "plugins" && Array.isArray(value)) {
|
|
129
|
-
return value
|
|
141
|
+
return value
|
|
142
|
+
.flat(Infinity)
|
|
143
|
+
.map(pluginName)
|
|
144
|
+
.filter((name) => name != null);
|
|
130
145
|
}
|
|
131
146
|
return value;
|
|
132
147
|
}),
|
package/internal/events.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// @noflow
|
|
2
|
+
//
|
|
1
3
|
// Plain JavaScript: executed by the host that runs Vite, before any transform.
|
|
2
4
|
//
|
|
3
5
|
// The driver's control channel.
|
|
@@ -49,6 +51,26 @@ export function stripAnsi(text) {
|
|
|
49
51
|
return text.replace(ANSI, "");
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Report a page that rendered its error boundary instead of itself.
|
|
56
|
+
*
|
|
57
|
+
* `uf dev` has two renderers — the plugin's middleware and the driver's — and
|
|
58
|
+
* this is the one place either of them says so, because a message written
|
|
59
|
+
* twice is a message that ends up saying two things. The document the browser
|
|
60
|
+
* gets is the application's error page, which is what a visitor would see;
|
|
61
|
+
* the exception belongs in the terminal, which is uf's.
|
|
62
|
+
*
|
|
63
|
+
* The stack is mapped back onto the Flow source first, so the frames name the
|
|
64
|
+
* file that was written rather than the one that was compiled.
|
|
65
|
+
*/
|
|
66
|
+
export function reportRenderError(server, url, error) {
|
|
67
|
+
if (error instanceof Error) {
|
|
68
|
+
server.ssrFixStacktrace(error);
|
|
69
|
+
}
|
|
70
|
+
const detail = error instanceof Error ? (error.stack ?? error.message) : String(error);
|
|
71
|
+
server.config.logger.error(`${url} rendered its error boundary\n${stripAnsi(detail)}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
52
74
|
/**
|
|
53
75
|
* Describe an error for the channel: message, and a location when Babel or
|
|
54
76
|
* Rolldown attached one.
|