@rohal12/twee-ts 1.18.2 → 2.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.
Files changed (42) hide show
  1. package/README.md +23 -3
  2. package/THIRD_PARTY_NOTICES +111 -0
  3. package/UNLICENSE +24 -0
  4. package/dist/bin/twee-ts.js +705 -9447
  5. package/dist/bin/twee-ts.js.map +1 -1
  6. package/dist/diagnostic-text-Bj-UQ0Jl.cjs +27007 -0
  7. package/dist/diagnostic-text-Bj-UQ0Jl.cjs.map +1 -0
  8. package/dist/diagnostic-text-DMVZfBh9.js +26530 -0
  9. package/dist/diagnostic-text-DMVZfBh9.js.map +1 -0
  10. package/dist/index.cjs +91 -9140
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +208 -416
  13. package/dist/index.d.ts +208 -416
  14. package/dist/index.js +4 -9317
  15. package/dist/lint-BkpfLGuP.js +288 -0
  16. package/dist/lint-BkpfLGuP.js.map +1 -0
  17. package/dist/options-DuBkFyqi.d.cts +28 -0
  18. package/dist/options-nc5NpnSc.d.ts +28 -0
  19. package/dist/plugins/rollup.cjs +177 -0
  20. package/dist/plugins/rollup.cjs.map +1 -0
  21. package/dist/plugins/rollup.d.cts +52 -0
  22. package/dist/plugins/rollup.d.ts +13 -12
  23. package/dist/plugins/rollup.js +136 -66
  24. package/dist/plugins/rollup.js.map +1 -1
  25. package/dist/plugins/vite.cjs +2225 -0
  26. package/dist/plugins/vite.cjs.map +1 -0
  27. package/dist/plugins/vite.d.cts +15 -0
  28. package/dist/plugins/vite.d.ts +5 -12
  29. package/dist/plugins/vite.js +2120 -516
  30. package/dist/plugins/vite.js.map +1 -1
  31. package/dist/types-BvI6pg2y.d.cts +389 -0
  32. package/dist/types-BvI6pg2y.d.ts +389 -0
  33. package/dist/watch-targets-BrEYmUwU.cjs +643 -0
  34. package/dist/watch-targets-BrEYmUwU.cjs.map +1 -0
  35. package/dist/watch-targets-DzvPyWDo.js +548 -0
  36. package/dist/watch-targets-DzvPyWDo.js.map +1 -0
  37. package/package.json +80 -27
  38. package/schemas/twee-ts.config.schema.json +55 -17
  39. package/dist/index.js.map +0 -1
  40. package/dist/paths-BuTAg2av.js +0 -7044
  41. package/dist/paths-BuTAg2av.js.map +0 -1
  42. package/dist/types-D8Xfe0l8.d.ts +0 -71
@@ -1,8 +1,1810 @@
1
- import { a as toPosix, c as splitDiagnostics, d as getFilenames, f as isExcluded, g as mediaTypeFromFilename, h as walkedEntry, i as outputLocations, l as compileForOutputFile, m as realPathOf, n as isInside, o as fatalError, p as outputPaths, r as isViteConfigTemp, s as formatDiagnostic, t as createOutputRecord, u as findHeadStartEnd } from "../paths-BuTAg2av.js";
2
- import { readdirSync, statSync } from "node:fs";
3
- import { join, relative, resolve } from "node:path";
1
+ /*! @rohal12/twee-ts (Unlicense). Bundles third-party code under its own licence: see THIRD_PARTY_NOTICES. */
2
+ import { At as lineColumnFinder, Ct as viteWaitingPage, Dt as AcornParser, Et as javaScriptContexts, G as recordOwnOutput, Mt as isRecord, Ot as javaScriptNodes, St as insertViteClient, Tt as cssContexts, U as sha256Hex, at as outputPaths, dt as isSameOrInside, ft as mediaTypeFromFilename, jt as types$1, kt as trySyntax, lt as identify, nt as getFilenames, pt as normalizedFileExt } from "../diagnostic-text-DMVZfBh9.js";
3
+ import { a as checkViteVersion, c as createOutputRecord, d as isViteConfigTemp, f as keyWithin, g as fatalError, h as compileStory, i as watchTargets, l as emittedFilePath, m as toPosix, n as missingImportFolders, o as resolvePluginOptions, p as outputLocations, s as canonicalPath, t as importWatchTargets, u as fileKey } from "../watch-targets-DzvPyWDo.js";
4
+ import { findPackageJSON } from "node:module";
5
+ import { basename, dirname, isAbsolute, join, posix, relative, resolve, win32 } from "node:path";
6
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
4
7
  import { stripVTControlCharacters } from "node:util";
5
- import { build, version } from "vite";
8
+ import { pathToFileURL } from "node:url";
9
+ import { build, createIdResolver, loadConfigFromFile, mergeConfig, version } from "vite";
10
+ //#region src/plugins/vite-glob.ts
11
+ /**
12
+ * The folders an entry's `import.meta.glob()` calls read. Vite replaces each call with the files that match it
13
+ * when it bundles, so the bundle names those files and nothing else: a file added later that matches is in no
14
+ * bundle's module list. To bundle again when the files a glob selects change, the plugin reads each call itself,
15
+ * just before Vite's own import-glob transform does, and notes the folder below which it can match (its scope).
16
+ *
17
+ * A scope is a superset of what the glob matches: a file added to it or removed from it may not match (a negated
18
+ * pattern, a name the wildcards leave out), which only bundles again for nothing. The calls are read with acorn as Vite reads them: patterns are
19
+ * string literals, template literals without expressions, or arrays of those. Options are static literal data,
20
+ * including computed keys and repeated members; expressions outside that subset are diagnosed, never evaluated.
21
+ *
22
+ * Code that is a module is read whole, so only real calls count. Code that is not (yet) one, as a plugin may hand
23
+ * on a component file or TypeScript it transforms later, is read the way Vite's import-glob reads every module:
24
+ * each `import.meta.glob(` in the text, with TypeScript type arguments allowed, is read on its own as a call.
25
+ * Text in a comment or a string that reads as a call then adds a scope too, which only costs a bundle for nothing.
26
+ */
27
+ /** A module, as the bundler hands it to a plugin's transform hook (the entry build's code is a module). */
28
+ const MODULE_OPTIONS = {
29
+ ecmaVersion: "latest",
30
+ sourceType: "module",
31
+ allowHashBang: true
32
+ };
33
+ /** Whether `code` may hold a glob call, as Vite's import-glob filters the modules it reads. */
34
+ function mayHoldGlob(code) {
35
+ return code.includes("import.meta.glob");
36
+ }
37
+ /** Whether `node` is the callee `import.meta.glob`. */
38
+ function isGlobCallee(node) {
39
+ return node.type === "MemberExpression" && !node.computed && node.property.type === "Identifier" && node.property.name === "glob" && node.object.type === "MetaProperty" && node.object.meta.name === "import" && node.object.property.name === "meta";
40
+ }
41
+ /** The string a pattern argument spells: a string literal, or a template literal without expressions. */
42
+ function stringOf(node) {
43
+ if (node === null) return void 0;
44
+ if (node.type === "Literal") return typeof node.value === "string" ? node.value : void 0;
45
+ if (node.type === "TemplateLiteral" && node.expressions.length === 0) return node.quasis[0]?.value.raw;
46
+ }
47
+ /** An expression outside the static data subset: never run it or guess the folder it selects. */
48
+ const UNREADABLE = Symbol("unreadable");
49
+ /** Static option data, with JavaScript's cooked strings, computed keys and last-member precedence. */
50
+ function staticValue(node, depth = 0) {
51
+ if (depth > 100) return UNREADABLE;
52
+ switch (node.type) {
53
+ case "Literal": return node.regex === void 0 && node.bigint === void 0 ? node.value : UNREADABLE;
54
+ case "TemplateLiteral": return node.expressions.length === 0 ? node.quasis[0]?.value.cooked ?? UNREADABLE : UNREADABLE;
55
+ case "UnaryExpression": {
56
+ const value = staticValue(node.argument, depth + 1);
57
+ if (value === UNREADABLE) return UNREADABLE;
58
+ if (node.operator === "!") return !value;
59
+ if (typeof value === "number") {
60
+ if (node.operator === "-") return -value;
61
+ if (node.operator === "+") return value;
62
+ }
63
+ return UNREADABLE;
64
+ }
65
+ case "ArrayExpression": {
66
+ const values = [];
67
+ for (const element of node.elements) {
68
+ if (element === null || element.type === "SpreadElement") return UNREADABLE;
69
+ const value = staticValue(element, depth + 1);
70
+ if (value === UNREADABLE) return UNREADABLE;
71
+ values.push(value);
72
+ }
73
+ return values;
74
+ }
75
+ case "ObjectExpression": {
76
+ const entries = /* @__PURE__ */ new Map();
77
+ for (const property of node.properties) {
78
+ if (property.type === "SpreadElement") return UNREADABLE;
79
+ if (property.kind !== "init" || property.method || property.shorthand) return UNREADABLE;
80
+ if (property.computed && property.key.type !== "Literal") return UNREADABLE;
81
+ const key = !property.computed && property.key.type === "Identifier" ? property.key.name : staticValue(property.key, depth + 1);
82
+ const value = staticValue(property.value, depth + 1);
83
+ if (depth === 0 && key === "exhaustive" && property.value.type !== "Literal") return UNREADABLE;
84
+ if (key !== null && typeof key !== "string" && typeof key !== "number" && typeof key !== "boolean" || value === UNREADABLE || !property.computed && key === "__proto__") return UNREADABLE;
85
+ entries.set(String(key), value);
86
+ }
87
+ return Object.fromEntries(entries);
88
+ }
89
+ default: return UNREADABLE;
90
+ }
91
+ }
92
+ /** The call a glob call's arguments make. */
93
+ function callOf(args) {
94
+ const [first, second] = args;
95
+ const patterns = (first === void 0 ? [] : first.type === "ArrayExpression" ? first.elements : [first]).map(stringOf).filter((pattern) => pattern !== void 0);
96
+ const options = second === void 0 ? {} : second.type === "SpreadElement" ? UNREADABLE : staticValue(second);
97
+ if (options === UNREADABLE) return void 0;
98
+ const base = isRecord(options) ? options["base"] : void 0;
99
+ return {
100
+ patterns,
101
+ base: typeof base === "string" ? base : void 0,
102
+ exhaustive: isRecord(options) && options["exhaustive"] === true
103
+ };
104
+ }
105
+ /** Where Vite's import-glob finds a call in a module's text: the callee, any TypeScript type arguments, `(`. */
106
+ const GLOB_CALL_START = /\bimport\.meta\.glob(?:<\w+>)?\s*\(/g;
107
+ /**
108
+ * The end (offset just past it) of the argument list that opens at `paren`, as acorn's tokenizer reads the text from
109
+ * there: past strings, template literals, regular expressions and comments that hold parentheses. Undefined when the
110
+ * list does not close, or the text cannot be tokenized.
111
+ */
112
+ function argumentsEnd(code, paren, budget) {
113
+ const tokenizer = Object.assign(AcornParser.tokenizer("", MODULE_OPTIONS), {
114
+ input: code,
115
+ pos: paren
116
+ });
117
+ const read = trySyntax(() => {
118
+ let depth = 0;
119
+ for (;;) {
120
+ const from = tokenizer.pos;
121
+ let token;
122
+ try {
123
+ token = tokenizer.getToken();
124
+ } finally {
125
+ const searched = token === void 0 || token.type === types$1.invalidTemplate ? code.length : tokenizer.pos;
126
+ budget.remaining -= Math.max(1, searched - from);
127
+ }
128
+ if (budget.remaining < 0 || token.type === types$1.eof) return void 0;
129
+ if (token.type === types$1.parenL) depth += 1;
130
+ else if (token.type === types$1.parenR) depth -= 1;
131
+ if (depth === 0) return token.end;
132
+ }
133
+ });
134
+ return read.ok ? read.value : void 0;
135
+ }
136
+ /** `text` read as one expression, which must take all of it; undefined when acorn reads it otherwise. */
137
+ function wholeExpression(text, options) {
138
+ const read = trySyntax(() => AcornParser.parseExpressionAt(text, 0, options));
139
+ return read.ok && read.value.end === text.length ? read.value : void 0;
140
+ }
141
+ /**
142
+ * The call whose callee starts at `start` and whose argument list opens at `paren`, read on its own, up to the end
143
+ * of its argument list: as a call expression, or, with type arguments between the two (which JavaScript reads as
144
+ * comparisons), as its argument list in parentheses. Undefined when acorn reads neither.
145
+ */
146
+ function callAt(code, start, paren, budget) {
147
+ const end = argumentsEnd(code, paren, budget);
148
+ if (end === void 0) return void 0;
149
+ budget.remaining -= end - start + end - paren;
150
+ if (budget.remaining < 0) return void 0;
151
+ const call = wholeExpression(code.slice(start, end), MODULE_OPTIONS);
152
+ if (call?.type === "CallExpression" && isGlobCallee(call.callee)) return callOf(call.arguments);
153
+ const list = wholeExpression(code.slice(paren, end), {
154
+ ...MODULE_OPTIONS,
155
+ preserveParens: true
156
+ });
157
+ if (list?.type !== "ParenthesizedExpression") return void 0;
158
+ const { expression } = list;
159
+ return callOf(expression.type === "SequenceExpression" ? expression.expressions : [expression]);
160
+ }
161
+ /** The glob calls of a module's code: read whole when it is a module, else each call on its own. */
162
+ function globCalls(code) {
163
+ const parsed = trySyntax(() => AcornParser.parse(code, MODULE_OPTIONS));
164
+ if (parsed.ok) {
165
+ const calls = [];
166
+ const unreadable = [];
167
+ for (const node of javaScriptNodes(parsed.value)) {
168
+ if (node.type !== "CallExpression" || !isGlobCallee(node.callee)) continue;
169
+ const call = callOf(node.arguments);
170
+ if (call === void 0) unreadable.push(node.start);
171
+ else calls.push(call);
172
+ }
173
+ return {
174
+ calls,
175
+ unreadable
176
+ };
177
+ }
178
+ const calls = [];
179
+ const unreadable = [];
180
+ const budget = { remaining: code.length * 16 };
181
+ for (const match of code.matchAll(GLOB_CALL_START)) {
182
+ const call = callAt(code, match.index, match.index + match[0].length - 1, budget);
183
+ if (call === void 0) unreadable.push(match.index);
184
+ else calls.push(call);
185
+ if (budget.remaining < 0) return {
186
+ calls,
187
+ unreadable,
188
+ recoveryLimited: true
189
+ };
190
+ }
191
+ return {
192
+ calls,
193
+ unreadable
194
+ };
195
+ }
196
+ /** Characters that make a segment of a pattern match more than its own name (picomatch's, and its escape). */
197
+ const MAGIC = /[*?[\]{}()!+@\\]/;
198
+ /** The characters after the last MAGIC one, found without retrying each literal prefix as a regex start. */
199
+ function literalEnd(segment) {
200
+ let start = segment.length;
201
+ while (start > 0 && !MAGIC.test(segment.charAt(start - 1))) start -= 1;
202
+ return segment.slice(start).toLowerCase();
203
+ }
204
+ /** Whether a pattern's segments may match a name starting with a dot, which a wildcard alone never does. */
205
+ function namesDot(segments) {
206
+ return segments.some((segment) => segment !== ".." && /(?:^|[{,])\./.test(segment));
207
+ }
208
+ /**
209
+ * The scope of the pattern `rest` below an absolute folder `dir` (as the file system spells it, so nothing in it
210
+ * is taken for a wildcard): the folder its leading plain segments name (`..` included), whether the segments after
211
+ * those reach below it, and the literal end of its last segment.
212
+ */
213
+ function scopeOf(dir, rest, exhaustive) {
214
+ const segments = posix.normalize(rest).split("/");
215
+ const magic = segments.findIndex((segment) => MAGIC.test(segment));
216
+ const plain = magic === -1 ? segments.slice(0, -1) : segments.slice(0, magic);
217
+ const below = segments.slice(plain.length);
218
+ return {
219
+ dir: toPosix(plain.reduce((folder, segment) => resolve(folder, segment), dir)),
220
+ deep: below.length > 1 || below.some((segment) => segment.includes("**")),
221
+ dot: exhaustive || namesDot(below),
222
+ exhaustive,
223
+ suffix: literalEnd(below.at(-1) ?? "")
224
+ };
225
+ }
226
+ /**
227
+ * The scopes of the patterns of `call`, made in the module `importer` (a file path, or undefined for a virtual
228
+ * module) under `root`, as Vite's import-glob places them: `/` from the root, `./` and `../` from the importer's
229
+ * folder or the `base` option, `**` from the root, and anything else (an alias, a `#` import) as the resolver
230
+ * resolves it. A negated pattern only takes files away, and has none.
231
+ */
232
+ async function scopesOf(call, importer, root, resolver) {
233
+ const folder = importer === void 0 ? root : dirname(importer);
234
+ const from = call.base === void 0 ? folder : call.base.startsWith("/") ? join(root, call.base) : resolve(folder, call.base);
235
+ const scopes = [];
236
+ for (const pattern of call.patterns) {
237
+ if (pattern.startsWith("!")) continue;
238
+ if (pattern.startsWith("/")) scopes.push(scopeOf(root, pattern.slice(1), call.exhaustive));
239
+ else if (pattern.startsWith("./") || pattern.startsWith("../")) scopes.push(scopeOf(from, pattern, call.exhaustive));
240
+ else if (pattern.startsWith("**")) scopes.push(scopeOf(root, pattern, call.exhaustive));
241
+ else {
242
+ const resolved = await resolver(pattern);
243
+ if (resolved === void 0 || !isAbsolute(resolved)) continue;
244
+ const spelled = toPosix(resolved);
245
+ let shared = 0;
246
+ while (shared < Math.min(spelled.length, pattern.length) && spelled.at(-1 - shared) === pattern.at(-1 - shared)) shared += 1;
247
+ const cut = spelled.lastIndexOf("/", spelled.length - shared);
248
+ scopes.push(scopeOf(spelled.slice(0, Math.max(cut, 0)) || "/", spelled.slice(cut + 1), call.exhaustive));
249
+ }
250
+ }
251
+ return scopes;
252
+ }
253
+ /** A key that is the same for two equal scopes. */
254
+ function scopeId(scope) {
255
+ return JSON.stringify([
256
+ scope.dir,
257
+ scope.deep,
258
+ scope.dot,
259
+ scope.exhaustive,
260
+ scope.suffix
261
+ ]);
262
+ }
263
+ /**
264
+ * Whether an entry named `name` of a folder in the scope may be matched, or, for a folder, lead to a match. The
265
+ * temporary copies Vite makes of a config file never are: they come and go with every entry build.
266
+ */
267
+ function admits(scope, name, isDir) {
268
+ if (name.startsWith(".") && !scope.dot || isViteConfigTemp(name)) return false;
269
+ if (isDir) return scope.deep && (scope.exhaustive || name !== "node_modules");
270
+ return name.toLowerCase().endsWith(scope.suffix);
271
+ }
272
+ /** The real path (forward slashes) of the folder the link at `path` leads to; undefined for a link to anything else. */
273
+ function linkedFolder(path) {
274
+ try {
275
+ return statSync(path).isDirectory() ? canonicalPath(path) : void 0;
276
+ } catch {
277
+ return;
278
+ }
279
+ }
280
+ /**
281
+ * What is in a scope: the files of its folder that `admits` lets through, and for a deep scope the folders below
282
+ * it and theirs. A link to a folder is followed, as Vite's import-glob follows it, and listed with its target, so
283
+ * retargeting it shows; a folder already listed (a link back to a folder above, or a second link to one) is not
284
+ * listed again, as Vite's crawler does not crawl it again. A link to a file is listed as a file. A scope whose folder
285
+ * does not exist (yet) lists as missing.
286
+ */
287
+ function readScope(scope) {
288
+ const entries = [];
289
+ const linked = /* @__PURE__ */ new Set();
290
+ const visited = /* @__PURE__ */ new Set();
291
+ const list = (dir, prefix) => {
292
+ let found;
293
+ try {
294
+ found = readdirSync(dir, { withFileTypes: true });
295
+ } catch {
296
+ return false;
297
+ }
298
+ visited.add(fileKey(dir));
299
+ for (const entry of found.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
300
+ const path = `${prefix}${entry.name}`;
301
+ const target = entry.isSymbolicLink() ? linkedFolder(join(dir, entry.name)) : void 0;
302
+ const isDir = entry.isDirectory() || target !== void 0;
303
+ if (!admits(scope, entry.name, isDir)) continue;
304
+ entries.push(target === void 0 ? `${isDir ? "d" : "f"}:${path}` : `l:${path}>${target}`);
305
+ if (target !== void 0) {
306
+ if (visited.has(fileKey(target))) continue;
307
+ linked.add(target);
308
+ }
309
+ if (isDir) list(join(dir, entry.name), `${path}/`);
310
+ }
311
+ return true;
312
+ };
313
+ return {
314
+ listing: list(scope.dir, "") ? entries.join("\n") : "\0missing",
315
+ linked: [...linked]
316
+ };
317
+ }
318
+ /**
319
+ * Whether adding or removing the file (or folder, `isDir`) at `path` may change what the glob of `scope` matches:
320
+ * it is in the scope, or in one of the folders it reaches through links (`linked`, see readScope), and `admits`
321
+ * lets it through, or it is one of those folders itself. Folders are compared by identity key, so one is
322
+ * recognised however the path spells it, through a link or not.
323
+ */
324
+ function scopeAdmits(scope, path, isDir, linked) {
325
+ const folders = [scope.dir, ...scope.deep ? linked : []].map(fileKey);
326
+ if (folders.includes(fileKey(path))) return true;
327
+ const parent = fileKey(dirname(path));
328
+ return (scope.deep ? keyWithin(parent, folders) : parent === folders[0]) && admits(scope, basename(path), isDir);
329
+ }
330
+ /**
331
+ * The folders a watcher watches for `scopes`: each scope's folder, or, for one that does not exist, the nearest
332
+ * folder above it that does, where creating it shows; and the folders a deep scope reaches through links, which a
333
+ * watcher of the scope's folder may not follow.
334
+ */
335
+ function scopeWatchTargets(scopes) {
336
+ return [...new Set([...scopes].flatMap((scope) => [toPosix(missingImportFolders([scope.dir])[0] ?? scope.dir), ...readScope(scope).linked]))];
337
+ }
338
+ /**
339
+ * The file `pattern` (an alias or a `#` import, not a path) resolves to from `importer`, asked as Vite's
340
+ * import-glob asks; undefined when it resolves to none, or the resolver fails (Vite's import-glob then reports
341
+ * the pattern itself).
342
+ */
343
+ async function resolveGlob(context, pattern, importer) {
344
+ const isSubImportsPattern = pattern.startsWith("#") && pattern.includes("*");
345
+ try {
346
+ return (await context.resolve(pattern, importer, { custom: { "vite:import-glob": { isSubImportsPattern } } }))?.id;
347
+ } catch {
348
+ return;
349
+ }
350
+ }
351
+ /**
352
+ * The scopes of the glob calls in a module's code, read in a plugin's transform hook, whose `context` resolves an
353
+ * alias or `#` pattern as Vite's import-glob would; a call acorn cannot read is reported with a warning and has
354
+ * none.
355
+ */
356
+ async function moduleGlobScopes(context, code, id, root) {
357
+ if (!mayHoldGlob(code)) return [];
358
+ const { calls, unreadable, recoveryLimited } = globCalls(code);
359
+ if (unreadable.length > 0) {
360
+ const locate = lineColumnFinder(code);
361
+ const where = unreadable.map((offset) => {
362
+ const { line, column } = locate(offset);
363
+ return `${String(line)}:${String(column)}`;
364
+ }).join(", ");
365
+ context.warn(`twee-ts could not read the import.meta.glob() call at ${where} of ${id}; ` + (recoveryLimited === true ? "recovery work limit reached; remaining calls were not read; " : "") + "files added to its folders are bundled the next time something else changes.");
366
+ }
367
+ const file = id.replace(/[?#].*$/s, "");
368
+ const importer = isAbsolute(file) && !id.startsWith("\0") ? file : void 0;
369
+ const resolver = (pattern) => resolveGlob(context, pattern, id);
370
+ return (await Promise.all(calls.map((call) => scopesOf(call, importer, root, resolver)))).flat();
371
+ }
372
+ //#endregion
373
+ //#region src/plugins/vite-entry.ts
374
+ /**
375
+ * Bundling the Vite plugin's `entry` into one script and one stylesheet for the
376
+ * story. In a build, the plugin bundles the entry inside the user's build when
377
+ * it can (see entryInputSettings), and otherwise, and always in dev, with a
378
+ * build of its own: bundleEntry() below, which replays the user's configuration.
379
+ */
380
+ /** The plugin's name, which is also how the entry build tells it from the user's other plugins. */
381
+ const PLUGIN_NAME = "twee-ts";
382
+ /** Passage names the bundled entry takes inside the story. */
383
+ const ENTRY_SCRIPT_NAME = "twee-ts-entry.js";
384
+ const ENTRY_STYLE_NAME = "twee-ts-entry.css";
385
+ /** How the name of the entry's input starts when the plugin bundles it inside the user's build. */
386
+ const ENTRY_INPUT_NAME = "twee-ts-entry";
387
+ /** The error for a file the entry emits under a name the build (or another story's entry) already writes. */
388
+ function entryCollisionMessage(fileName) {
389
+ return `twee-ts: the entry emits ${fileName}, a file the build already writes; rename one of them.`;
390
+ }
391
+ /** The entry's script and stylesheet as inline sources of the story (none without an entry). */
392
+ function entrySources(entry) {
393
+ if (entry === void 0) return [];
394
+ return [{
395
+ filename: ENTRY_SCRIPT_NAME,
396
+ content: `;${entry.script}`
397
+ }, ...entry.style === "" ? [] : [{
398
+ filename: ENTRY_STYLE_NAME,
399
+ content: entry.style
400
+ }]];
401
+ }
402
+ /**
403
+ * The output options of the entry's bundle: one IIFE script, and assets under
404
+ * their own names, so a build writes them where the dev server serves them.
405
+ */
406
+ const ENTRY_OUTPUT = {
407
+ format: "iife",
408
+ entryFileNames: ENTRY_SCRIPT_NAME,
409
+ assetFileNames: "[name][extname]"
410
+ };
411
+ /**
412
+ * The output options of the entry's bundle given the user's `output` option:
413
+ * the user's own when it is one object, with the entry's format and file names
414
+ * on top. An array of outputs describes the user's own files; none of them
415
+ * applies to the entry.
416
+ */
417
+ function entryOutput(userOutput) {
418
+ return userOutput === void 0 || Array.isArray(userOutput) ? { ...ENTRY_OUTPUT } : {
419
+ ...userOutput,
420
+ ...ENTRY_OUTPUT
421
+ };
422
+ }
423
+ /**
424
+ * Settings that make every font and image the entry uses inline as a data URL,
425
+ * and its CSS one stylesheet, so the story stays one file; only an import marked
426
+ * `?no-inline` is still emitted separately.
427
+ */
428
+ const ONE_FILE = {
429
+ cssCodeSplit: false,
430
+ assetsInlineLimit: () => true
431
+ };
432
+ /**
433
+ * What `import.meta.url` stands for in the entry's script, given the story's output file name. The script runs
434
+ * as a classic script inside the story page, where `import.meta` does not exist (the bundler leaves `{}.url`, so
435
+ * `new URL(asset, import.meta.url)` and Vite's worker URLs throw `Invalid URL`). The files the build writes
436
+ * besides the story, such as an asset marked `?no-inline`, are found from the output folder the story is below,
437
+ * so the base is the story page's URL, taken up one level for each folder of a nested `outputFilename`.
438
+ */
439
+ function entryDefine(outputFilename) {
440
+ const depth = folderDepth(outputFilename);
441
+ return { "import.meta.url": depth === 0 ? "document.baseURI" : `new URL(${JSON.stringify("../".repeat(depth))}, document.baseURI).href` };
442
+ }
443
+ /** How many folders deep `outputFilename` puts the story below the output folder. */
444
+ function folderDepth(outputFilename) {
445
+ return outputFilename.split("/").length - 1;
446
+ }
447
+ /**
448
+ * The `experimental.renderBuiltUrl` of a build that bundles the entry, given the build's `base`, the story's
449
+ * `outputFilename` and the configuration's own `renderBuiltUrl` (`user`, which decides first), or `user` itself
450
+ * when nothing changes. With a relative `base` (`''` or `'./'`), Vite makes the URL of a file the build writes
451
+ * besides the story (an asset marked `?no-inline`, a worker) relative to the script or stylesheet that names it.
452
+ * The story page holds both inline, so such a URL resolves against the page, which a nested `outputFilename` puts
453
+ * below the output folder the files are written to. For the entry's script, the URL is then resolved at run time
454
+ * against the output folder, as `import.meta.url` is (see `entryDefine`); in its stylesheet, it is a path up from
455
+ * the page. Every stylesheet of such a build is the entry's: the build bundles nothing else.
456
+ */
457
+ function entryRenderBuiltUrl(base, outputFilename, user) {
458
+ const depth = folderDepth(outputFilename);
459
+ if (base !== "" && base !== "./" || depth === 0) return user;
460
+ const outputFolder = entryDefine(outputFilename)["import.meta.url"];
461
+ return (filename, type) => {
462
+ const own = user?.(filename, type);
463
+ if (own !== void 0) return own;
464
+ const path = filename.replaceAll("%", "%25");
465
+ if (type.hostType === "js" && type.hostId === ENTRY_SCRIPT_NAME) return { runtime: `new URL(${JSON.stringify(path)}, ${outputFolder}).href` };
466
+ return type.hostType === "css" ? `${"../".repeat(depth)}${path}` : void 0;
467
+ };
468
+ }
469
+ /**
470
+ * Build settings that bundle the entry inside the user's build, as its only
471
+ * input, under `inputName`, which tells the plugin instance it is its own, and
472
+ * what the bundled script needs to run in the story (`define`).
473
+ */
474
+ function entryInputSettings(inputName, entryPath, userOutput, outputFilename) {
475
+ return {
476
+ build: {
477
+ ...ONE_FILE,
478
+ rolldownOptions: {
479
+ input: { [inputName]: entryPath },
480
+ output: entryOutput(userOutput)
481
+ }
482
+ },
483
+ define: entryDefine(outputFilename)
484
+ };
485
+ }
486
+ /** Removes a trailing source-map comment that points at a file (a data: URL map stays). */
487
+ function dropFileSourceMapComment(code, language) {
488
+ const end = code.trimEnd().length;
489
+ const start = Math.max(language === "js" ? code.lastIndexOf("//# sourceMappingURL=", end) : -1, code.lastIndexOf("/*# sourceMappingURL=", end));
490
+ if (start === -1) return code;
491
+ const candidate = code.slice(start, end);
492
+ if (!/^(?:\/\/# sourceMappingURL=(?!data:)\S*|\/\*# sourceMappingURL=(?!data:)[^*]*\*\/)$/u.test(candidate)) return code;
493
+ const ranges = [{
494
+ start,
495
+ end
496
+ }, {
497
+ start: Math.max(0, start - 1),
498
+ end
499
+ }];
500
+ const [at, before] = language === "js" ? javaScriptContexts(code, ranges) : cssContexts(code, ranges);
501
+ const kind = at?.context.kind;
502
+ if (kind !== "line-comment" && kind !== "block-comment" && kind !== "comment") return code;
503
+ if (start > 0 && before?.context.kind === kind) return code;
504
+ return code.slice(0, code[start - 1] === "\n" ? start - 1 : start);
505
+ }
506
+ /** A module id or watched file as a forward-slash file path, without a query or hash (like Vite's cleanUrl). */
507
+ function fileOfId(id) {
508
+ return toPosix(id.replace(/[?#].*$/s, ""));
509
+ }
510
+ /** Removes a file from a bundle, which bundlers hand plugins as a plain object keyed by file name. */
511
+ function removeFromBundle(bundle, fileName) {
512
+ delete bundle[fileName];
513
+ }
514
+ /**
515
+ * Whether an asset is a file the entry imported as a URL (`theme.css?url`), not the stylesheet the bundler made
516
+ * of the entry's CSS. The bundler names the files an asset came from; for its own stylesheet that is a bare
517
+ * output name (`style.css`), for an imported file the file's path.
518
+ */
519
+ function isImportedFile(item) {
520
+ return (item.originalFileNames ?? []).some((name) => isAbsolute(name) || win32.isAbsolute(name));
521
+ }
522
+ /**
523
+ * Takes the entry out of a bundle: its entry chunk (the one named `inputName`,
524
+ * when the entry is one input of the user's build; the only one, from the
525
+ * entry's own build), every stylesheet it made of the entry's CSS (a CSS file imported as a URL stays an asset), and the map files, which the story
526
+ * cannot ship. The chunk is found by its input's name, not by its module's
527
+ * path, which the bundler may spell another way (a root reached through a
528
+ * link is read by its real path). Anything else the bundler emitted for it
529
+ * stays in the bundle, so a build writes it next to the HTML, and is also
530
+ * returned, so the dev server can serve it.
531
+ */
532
+ function takeEntryFromBundle(bundle, inputName) {
533
+ let script = "";
534
+ const files = /* @__PURE__ */ new Set();
535
+ const assets = /* @__PURE__ */ new Map();
536
+ const styles = [];
537
+ for (const [fileName, item] of Object.entries(bundle)) if (item.type === "chunk") {
538
+ if (!item.isEntry || inputName !== void 0 && item.name !== inputName) continue;
539
+ script = dropFileSourceMapComment(item.code, "js");
540
+ for (const id of item.moduleIds) if (!id.startsWith("\0")) files.add(fileOfId(id));
541
+ removeFromBundle(bundle, fileName);
542
+ } else if (fileName.endsWith(".css") && !isImportedFile(item)) {
543
+ const css = typeof item.source === "string" ? item.source : new TextDecoder().decode(item.source);
544
+ styles.push(dropFileSourceMapComment(css, "css"));
545
+ removeFromBundle(bundle, fileName);
546
+ } else if (fileName.endsWith(".map") && (item.originalFileNames?.length ?? 0) === 0) removeFromBundle(bundle, fileName);
547
+ else assets.set(fileName, item.source);
548
+ return {
549
+ script,
550
+ style: styles.join("\n"),
551
+ files,
552
+ assets
553
+ };
554
+ }
555
+ /**
556
+ * The entry build's logger: its warnings go to the outer logger; its progress
557
+ * lines and its own "build failed" line are dropped, because the plugin
558
+ * reports a failed bundle itself. (The progress lines Vite 8 prints itself are
559
+ * kept quiet by the entry build's log level; see bundleEntry.)
560
+ */
561
+ function entryBuildLogger(outer) {
562
+ return {
563
+ info: () => {},
564
+ warn: (message, options) => {
565
+ outer.warn(message, options);
566
+ },
567
+ warnOnce: (message, options) => {
568
+ outer.warnOnce(message, options);
569
+ },
570
+ error: () => {},
571
+ clearScreen: () => {},
572
+ hasErrorLogged: () => false,
573
+ get hasWarned() {
574
+ return outer.hasWarned;
575
+ }
576
+ };
577
+ }
578
+ /**
579
+ * The entry build's log level: no progress lines (Vite 8 prints those itself
580
+ * at `info`), and no more than the outer build or server shows.
581
+ */
582
+ function entryBuildLogLevel(outer) {
583
+ return outer === void 0 || outer === "info" ? "warn" : outer;
584
+ }
585
+ /**
586
+ * Top-level keys of the user's configuration the entry build doesn't take, and
587
+ * why. Everything else (define, resolve, css, json, esbuild/oxc, env options,
588
+ * base, assetsInclude, worker, build options, environments, experimental and
589
+ * the rest) comes across as the user gave it, so the entry bundles the same in
590
+ * dev as in a build.
591
+ */
592
+ const ENTRY_BUILD_EXCLUDED_KEYS = {
593
+ configFile: "the entry build is given the loaded config file itself",
594
+ plugins: "replaced by the same plugins without twee-ts, plus the entry build’s own",
595
+ root: "set to the resolved root",
596
+ mode: "set to the resolved mode",
597
+ logLevel: "set so the entry build prints no progress lines",
598
+ customLogger: "the entry build reports through the outer logger",
599
+ clearScreen: "the entry build never clears the screen",
600
+ publicDir: "set to the outer build’s resolved public directory, which the entry build resolves from but never copies",
601
+ server: "configures the dev server, not a build",
602
+ preview: "configures the preview server, not a build",
603
+ builder: "would build every environment instead of the entry",
604
+ devtools: "starts Vite’s devtools integration, which belongs to the outer server or build"
605
+ };
606
+ /** The user's configuration without the keys the entry build doesn't take. */
607
+ function withoutExcludedKeys(config) {
608
+ const copy = { ...config };
609
+ for (const key of Object.keys(ENTRY_BUILD_EXCLUDED_KEYS)) Reflect.deleteProperty(copy, key);
610
+ return copy;
611
+ }
612
+ /** A plugin option (nested arrays, promises, falsy entries) as one flat list. */
613
+ async function flattenPlugins(option) {
614
+ const value = await option;
615
+ if (Array.isArray(value)) return (await Promise.all(value.map(flattenPlugins))).flat();
616
+ return value ? [value] : [];
617
+ }
618
+ function isPlugin(value) {
619
+ return isRecord(value) && typeof value["name"] === "string";
620
+ }
621
+ /** The handler of a plugin hook in function or object form. */
622
+ function hookHandler(hook) {
623
+ return isRecord(hook) ? hook["handler"] : hook;
624
+ }
625
+ /** A hook with its handler replaced, keeping the object form's order and other fields. */
626
+ function withHandler(hook, handler) {
627
+ return isRecord(hook) ? {
628
+ ...hook,
629
+ handler
630
+ } : handler;
631
+ }
632
+ /**
633
+ * Whether Vite applies `plugin` for `command`: as Vite reads `apply`, with
634
+ * the configuration and environment the plugin would see.
635
+ */
636
+ function appliesTo(plugin, command, config, env) {
637
+ const { apply } = plugin;
638
+ if (apply === void 0) return true;
639
+ if (typeof apply === "function") return apply({
640
+ ...config,
641
+ mode: env.mode
642
+ }, env);
643
+ return apply === command;
644
+ }
645
+ /**
646
+ * `plugin` as the entry build runs it: without `apply` (already decided by
647
+ * appliesTo), and, for the dev command, with its `config`, `configEnvironment`
648
+ * and `configResolved` hooks, environment selection and hook contexts seeing
649
+ * the dev command. Vite's own build plugins still see build. The plugin object itself is left untouched.
650
+ */
651
+ function asEntryBuildPlugin(plugin, command, views) {
652
+ const overrides = /* @__PURE__ */ new Map([["apply", void 0]]);
653
+ if (command === "serve") {
654
+ const devEnv = (env) => Object.assign({}, env, {
655
+ command,
656
+ isSsrBuild: false
657
+ });
658
+ const devConfig = (config) => {
659
+ if (!isRecord(config)) return config;
660
+ let view = views.configs.get(config);
661
+ if (view === void 0) {
662
+ view = withOverrides(config, /* @__PURE__ */ new Map([["command", command]]));
663
+ views.configs.set(config, view);
664
+ }
665
+ return view;
666
+ };
667
+ const devEnvironment = (environment) => {
668
+ if (!isRecord(environment)) return environment;
669
+ const known = views.environments.get(environment);
670
+ if (known !== void 0) return known;
671
+ const overrides = /* @__PURE__ */ new Map([["config", devConfig(environment["config"])]]);
672
+ const topLevel = environment["getTopLevelConfig"];
673
+ if (typeof topLevel === "function") overrides.set("getTopLevelConfig", () => devConfig(Reflect.apply(topLevel, environment, [])));
674
+ const view = withOverrides(environment, overrides);
675
+ views.environments.set(environment, view);
676
+ return view;
677
+ };
678
+ const devContext = (context) => {
679
+ if (!isRecord(context) || context["environment"] === void 0) return context;
680
+ const environment = devEnvironment(context["environment"]);
681
+ return new Proxy(context, { get(target, key) {
682
+ if (key === "environment") return environment;
683
+ const value = Reflect.get(target, key, target);
684
+ return typeof value === "function" ? value.bind(target) : value;
685
+ } });
686
+ };
687
+ const wrap = (name, patch = (args) => args) => {
688
+ const hook = Reflect.get(plugin, name);
689
+ const handler = hookHandler(hook);
690
+ if (typeof handler !== "function") return;
691
+ overrides.set(name, withHandler(hook, function(...args) {
692
+ return Reflect.apply(handler, devContext(this), patch(args));
693
+ }));
694
+ };
695
+ const keys = /* @__PURE__ */ new Set();
696
+ for (let object = plugin; object !== null && object !== Object.prototype; object = Reflect.getPrototypeOf(object)) for (const key of Reflect.ownKeys(object)) keys.add(key);
697
+ for (const key of keys) if (key !== "constructor" && key !== "apply" && key !== "applyToEnvironment") wrap(key);
698
+ wrap("config", ([config, env]) => [config, devEnv(env)]);
699
+ wrap("configEnvironment", ([name, config, env]) => [
700
+ name,
701
+ config,
702
+ devEnv(env)
703
+ ]);
704
+ wrap("configResolved", ([config]) => [devConfig(config)]);
705
+ const select = plugin.applyToEnvironment;
706
+ if (select !== void 0) overrides.set("applyToEnvironment", async function(environment) {
707
+ const result = await Reflect.apply(select, this, [devEnvironment(environment)]);
708
+ if (result === true || !result) return result;
709
+ return (await flattenPlugins(result)).filter(isPlugin).map((selected) => asEntryBuildPlugin(selected, command, views));
710
+ });
711
+ }
712
+ return pluginWith(plugin, overrides);
713
+ }
714
+ /**
715
+ * A copy of `plugin` with some properties of its own reading differently; the plugin itself is left untouched. The
716
+ * copy has the plugin's prototype (a class's methods and getters) and each of its own properties as they are
717
+ * defined (a getter stays a getter), so a frozen plugin, or one whose hooks are getters, is copied as well: a Proxy
718
+ * may not answer differently for a property a frozen object defines. The copy stands in for the plugin wherever
719
+ * the plugin list has it.
720
+ */
721
+ function pluginWith(plugin, overrides) {
722
+ const copy = { ...plugin };
723
+ const layer = {};
724
+ Object.setPrototypeOf(layer, Reflect.getPrototypeOf(plugin));
725
+ for (const [key, value] of overrides) Object.defineProperty(layer, key, {
726
+ value,
727
+ writable: true,
728
+ enumerable: false,
729
+ configurable: true
730
+ });
731
+ Object.setPrototypeOf(copy, layer);
732
+ for (const key of Reflect.ownKeys(plugin)) {
733
+ const descriptor = Object.getOwnPropertyDescriptor(plugin, key);
734
+ if (descriptor !== void 0 && !overrides.has(key)) Object.defineProperty(copy, key, descriptor);
735
+ }
736
+ for (const [key, value] of overrides) Object.defineProperty(copy, key, {
737
+ value,
738
+ writable: true,
739
+ enumerable: true,
740
+ configurable: true
741
+ });
742
+ return copy;
743
+ }
744
+ /** `target` with some properties reading differently; the object itself is left untouched. */
745
+ function withOverrides(target, overrides) {
746
+ return new Proxy(target, { get(object, key, receiver) {
747
+ return overrides.has(key) ? overrides.get(key) : Reflect.get(object, key, receiver);
748
+ } });
749
+ }
750
+ /**
751
+ * Build settings that make the entry the build's one input and its bundle one
752
+ * script and one stylesheet held in memory, whatever the user's build settings
753
+ * say; for the dev command also unminified, with an inline source map. Settings
754
+ * that would build something else (`lib`, `ssr`) or write files are dropped.
755
+ */
756
+ function entryBuildSettings(userBuild, entryPath, command) {
757
+ const { lib: _lib, ssr: _ssr, rollupOptions, rolldownOptions, ...rest } = { ...userBuild };
758
+ const { input: _input, output, ...bundler } = rolldownOptions ?? rollupOptions ?? {};
759
+ return {
760
+ ...rest,
761
+ ...ONE_FILE,
762
+ write: false,
763
+ watch: null,
764
+ emptyOutDir: false,
765
+ copyPublicDir: false,
766
+ ...command === "serve" ? {
767
+ minify: false,
768
+ sourcemap: "inline"
769
+ } : {},
770
+ rolldownOptions: {
771
+ ...bundler,
772
+ input: entryPath,
773
+ output: entryOutput(output)
774
+ }
775
+ };
776
+ }
777
+ /**
778
+ * Applies entryBuildSettings last, after every config hook, in the
779
+ * configuration and in the client environment's own options (which the build
780
+ * reads), so no setting from the user or a plugin makes the entry build build
781
+ * or write anything else.
782
+ */
783
+ function entryBuildEnforcer(entryPath, command, outputFilename) {
784
+ return {
785
+ name: `${PLUGIN_NAME}:entry-build`,
786
+ enforce: "post",
787
+ config: {
788
+ order: "post",
789
+ handler(config) {
790
+ config.build = entryBuildSettings(config.build, entryPath, command);
791
+ config.define = {
792
+ ...config.define,
793
+ ...entryDefine(outputFilename)
794
+ };
795
+ const renderBuiltUrl = entryRenderBuiltUrl(config.base, outputFilename, config.experimental?.renderBuiltUrl);
796
+ if (renderBuiltUrl !== void 0) config.experimental = {
797
+ ...config.experimental,
798
+ renderBuiltUrl
799
+ };
800
+ }
801
+ },
802
+ configEnvironment: {
803
+ order: "post",
804
+ handler(name, config) {
805
+ if (name !== "client") return;
806
+ config.build = entryBuildSettings(config.build, entryPath, command);
807
+ config.define = {
808
+ ...config.define,
809
+ ...entryDefine(outputFilename)
810
+ };
811
+ }
812
+ }
813
+ };
814
+ }
815
+ /** Plugin hooks that may read an entry input and register it with `this.addWatchFile` before the story is emitted. */
816
+ const WATCH_FILE_HOOKS = [
817
+ "buildStart",
818
+ "resolveId",
819
+ "resolveDynamicImport",
820
+ "load",
821
+ "transform",
822
+ "moduleParsed",
823
+ "buildEnd",
824
+ "renderStart",
825
+ "renderChunk",
826
+ "augmentChunkHash",
827
+ "generateBundle"
828
+ ];
829
+ /** A plugin context whose `addWatchFile` also passes the file to `record`. */
830
+ function recordingContext(context, record) {
831
+ return new Proxy(context, { get(target, key) {
832
+ const value = Reflect.get(target, key, target);
833
+ if (typeof value !== "function") return value;
834
+ if (key !== "addWatchFile") return value.bind(target);
835
+ return (id) => {
836
+ if (typeof id === "string" && !id.startsWith("\0")) record(resolve(id));
837
+ return Reflect.apply(value, target, [id]);
838
+ };
839
+ } });
840
+ }
841
+ /**
842
+ * `plugin` with hooks that pass the file of each of its `addWatchFile` calls to `record`. The
843
+ * original stays untouched, so a plugin object shared across entry builds
844
+ * never collects wrappers. A plugin without such hooks (a built-in one, for
845
+ * instance) is returned as it is.
846
+ */
847
+ function recordingPlugin(plugin, recordFor) {
848
+ const overrides = /* @__PURE__ */ new Map();
849
+ for (const name of WATCH_FILE_HOOKS) {
850
+ const hook = plugin[name];
851
+ const handler = hookHandler(hook);
852
+ if (typeof handler !== "function") continue;
853
+ overrides.set(name, withHandler(hook, function(...args) {
854
+ const record = recordFor(name, args);
855
+ const context = typeof this === "object" && this !== null ? recordingContext(this, record) : this;
856
+ return Reflect.apply(handler, context, args);
857
+ }));
858
+ }
859
+ return overrides.size === 0 ? plugin : pluginWith(plugin, overrides);
860
+ }
861
+ const isFile = (path) => statSync(path, { throwIfNoEntry: false })?.isFile() === true;
862
+ /** How an import is resolved with its links kept (see keptPath), one resolver per configuration. */
863
+ const keptResolvers = /* @__PURE__ */ new WeakMap();
864
+ /**
865
+ * Makes keptPath resolve with a new resolver for `config`: Vite's resolver remembers what it found, which a build of
866
+ * `vite build --watch` must not take from the one before.
867
+ */
868
+ function forgetKeptResolver(config) {
869
+ keptResolvers.delete(config);
870
+ }
871
+ /**
872
+ * The file `source`, imported by `importer`, reaches as Vite's resolver finds it, with the links on the way kept
873
+ * (`resolve.preserveSymlinks`): the path through a linked file, folder or package folder, which the module's own
874
+ * (real) path never shows. It follows every spelling Vite's resolver reads: relative and absolute paths, the root's
875
+ * `/`, missing extensions, a folder's index, `resolve.alias`, bare package names, a package's subpaths and exports,
876
+ * `#` subpath imports and `resolve.tsconfigPaths`. Undefined for an import it resolves to no file (a virtual module,
877
+ * an external one, one it cannot resolve), or when resolving fails, which the bundle itself then reports.
878
+ */
879
+ async function keptPath(context, source, importer) {
880
+ const { environment } = context;
881
+ let resolver = keptResolvers.get(environment.config);
882
+ if (resolver === void 0) {
883
+ resolver = createIdResolver(environment.config, { preserveSymlinks: true });
884
+ keptResolvers.set(environment.config, resolver);
885
+ }
886
+ try {
887
+ const id = await resolver(environment, source, importer);
888
+ return id === void 0 || id.startsWith("\0") ? void 0 : fileOfId(id);
889
+ } catch {
890
+ return;
891
+ }
892
+ }
893
+ /**
894
+ * The paths an import of `source` by `importer` is written as, absolute and with forward slashes, when it is written
895
+ * as a path: relative to the importer, or absolute; also when that reaches nothing yet, so creating it shows. None for
896
+ * a bare name or an alias. An absolute path that starts with `/` is read as Vite reads it: from the root first, unless
897
+ * it already names a path inside the root, and as a path of the file system when that is where it reaches a file.
898
+ */
899
+ function writtenPaths(source, importer, root) {
900
+ const from = importer === void 0 ? void 0 : fileOfId(importer);
901
+ if (isAbsolute(source)) {
902
+ const file = fileOfId(source);
903
+ return [.../^[/\\]/.test(source) && !file.startsWith(`${toPosix(root).replace(/\/$/, "")}/`) && !existsSync(file) ? [fileOfId(join(root, source))] : [], file];
904
+ }
905
+ return from !== void 0 && isAbsolute(from) && /^\.\.?(?:[/\\]|$)/.test(source) ? [fileOfId(resolve(dirname(from), source))] : [];
906
+ }
907
+ /**
908
+ * Package locations Node's upward node_modules search can use for an unresolved bare import. These are recovery
909
+ * inputs only: Vite still decides aliases, exports and the actual module. Watching the package itself sees a
910
+ * restored manifest, export target or nested subpath without watching every installed package recursively.
911
+ */
912
+ function missingPackagePaths(source, importer) {
913
+ if (importer === void 0 || !isAbsolute(fileOfId(importer)) || source === "" || /^[./\\#\0]/.test(source) || source.includes(":")) return [];
914
+ const name = source.split("/").slice(0, source.startsWith("@") ? 2 : 1).join("/");
915
+ const paths = [];
916
+ for (let folder = dirname(fileOfId(importer));; folder = dirname(folder)) {
917
+ if (toPosix(folder).split("/").at(-1) !== "node_modules") paths.push(toPosix(join(folder, "node_modules", name)));
918
+ if (dirname(folder) === folder) break;
919
+ }
920
+ return paths;
921
+ }
922
+ /**
923
+ * A package can import itself through its exports without living in node_modules. Node can locate its manifest
924
+ * even when the export target is missing. Track the package's own file listing as a recovery scope, excluding
925
+ * node_modules; recording the whole folder as a watch file would scan all of its installed dependencies too.
926
+ * The node_modules candidates above remain necessary for packages that have no manifest yet.
927
+ */
928
+ function missingPackageScope(source, importer, candidates) {
929
+ if (importer === void 0 || candidates.length === 0) return void 0;
930
+ let manifest;
931
+ try {
932
+ manifest = findPackageJSON(source, pathToFileURL(fileOfId(importer)));
933
+ } catch {
934
+ return;
935
+ }
936
+ if (manifest === void 0) return void 0;
937
+ const dir = toPosix(dirname(manifest));
938
+ return candidates.includes(dir) ? void 0 : {
939
+ dir,
940
+ deep: true,
941
+ dot: true,
942
+ exhaustive: false,
943
+ suffix: ""
944
+ };
945
+ }
946
+ /**
947
+ * Every path an import of `source` by `importer` is spelled as (see writtenPaths), and the path Vite's resolver
948
+ * reaches the file by, links kept (`kept`, see keptPath). A caller then sees a link on the way that is retargeted,
949
+ * which the file's own path never shows. Nothing for an import from a virtual module, or of one.
950
+ */
951
+ async function importSpellings(context, source, importer, root) {
952
+ if (source.startsWith("\0") || (importer?.startsWith("\0") ?? false)) return {
953
+ spellings: [],
954
+ kept: void 0
955
+ };
956
+ const written = writtenPaths(source, importer, root);
957
+ const resolved = written.find(isFile) ?? await keptPath(context, source, importer);
958
+ const kept = resolved !== void 0 && isAbsolute(resolved) ? resolved : void 0;
959
+ return {
960
+ spellings: [.../* @__PURE__ */ new Set([...written, ...kept === void 0 ? [] : [kept]])],
961
+ kept
962
+ };
963
+ }
964
+ /**
965
+ * Collects the files the entry build's plugins add with `addWatchFile`. These
966
+ * are not modules of the bundle: Vite's CSS plugin adds the stylesheets pulled
967
+ * in by `@import` and the files `url()` points at this way. The build runs on
968
+ * recording stand-ins of its plugins, made in the bundler's `options` hook,
969
+ * which sees the final plugin list, including the plugins Vite resolves per
970
+ * environment (`applyToEnvironment`) after `configResolved`.
971
+ */
972
+ function recordWatchFiles(files, root, observer = {}) {
973
+ const { onLoad, onResolve, onGlob, onWatchFile } = observer;
974
+ const watches = /* @__PURE__ */ new Map();
975
+ const touched = /* @__PURE__ */ new Set();
976
+ let modules = /* @__PURE__ */ new Set();
977
+ let started = false;
978
+ const record = (file) => {
979
+ files?.add(file);
980
+ onWatchFile?.(file);
981
+ };
982
+ const recordFor = (hook, args) => {
983
+ if (hook === "buildStart" && !started) {
984
+ started = true;
985
+ touched.clear();
986
+ watches.delete(void 0);
987
+ }
988
+ const owner = hook === "transform" || hook === "resolveId" || hook === "resolveDynamicImport" ? args[1] : hook === "load" ? args[0] : hook === "moduleParsed" && isRecord(args[0]) ? args[0]["id"] : void 0;
989
+ const id = typeof owner === "string" ? owner : void 0;
990
+ if (id !== void 0 && (hook === "load" || hook === "transform") && !touched.has(id)) {
991
+ touched.add(id);
992
+ watches.delete(id);
993
+ }
994
+ return (file) => {
995
+ record(file);
996
+ const owned = watches.get(id) ?? /* @__PURE__ */ new Set();
997
+ watches.set(id, owned.add(file));
998
+ };
999
+ };
1000
+ return {
1001
+ plugin: {
1002
+ name: `${PLUGIN_NAME}:record-watch-files`,
1003
+ buildStart() {
1004
+ recordFor("buildStart", []);
1005
+ },
1006
+ load: {
1007
+ order: "pre",
1008
+ handler(id) {
1009
+ const file = fileOfId(id);
1010
+ if (!isAbsolute(file) || id.startsWith("\0")) return null;
1011
+ onLoad?.(file);
1012
+ onResolve?.(file);
1013
+ return null;
1014
+ }
1015
+ },
1016
+ resolveId: {
1017
+ order: "pre",
1018
+ async handler(source, importer) {
1019
+ if (onResolve === void 0) return null;
1020
+ const { spellings, kept } = await importSpellings(this, source, importer, root);
1021
+ for (const spelled of spellings) onResolve(spelled);
1022
+ if (kept === void 0 && spellings.length === 0) {
1023
+ const candidates = missingPackagePaths(source, importer);
1024
+ for (const path of candidates) record(path);
1025
+ const scope = missingPackageScope(source, importer, candidates);
1026
+ if (scope !== void 0) {
1027
+ record(join(scope.dir, "package.json"));
1028
+ onGlob?.(scope);
1029
+ }
1030
+ }
1031
+ return null;
1032
+ }
1033
+ },
1034
+ transform: {
1035
+ filter: { code: "import.meta.glob" },
1036
+ async handler(code, id) {
1037
+ if (onGlob === void 0) return null;
1038
+ for (const scope of await moduleGlobScopes(this, code, id, root)) onGlob(scope);
1039
+ return null;
1040
+ }
1041
+ },
1042
+ buildEnd() {
1043
+ modules = new Set(this.getModuleIds());
1044
+ started = false;
1045
+ for (const owner of watches.keys()) if (owner !== void 0 && !modules.has(owner)) watches.delete(owner);
1046
+ for (const id of modules) {
1047
+ const file = fileOfId(id);
1048
+ if (isAbsolute(file)) files?.add(file);
1049
+ }
1050
+ },
1051
+ options: {
1052
+ order: "post",
1053
+ async handler(inputOptions) {
1054
+ const plugins = (await flattenPlugins(inputOptions.plugins)).filter(isPlugin).map((p) => recordingPlugin(p, recordFor));
1055
+ return {
1056
+ ...inputOptions,
1057
+ plugins
1058
+ };
1059
+ }
1060
+ }
1061
+ },
1062
+ inputs: () => {
1063
+ const current = new Set([...modules].filter((id) => !id.startsWith("\0")).map(fileOfId).filter(isAbsolute));
1064
+ for (const [owner, watched] of watches) if (owner === void 0 || modules.has(owner)) for (const file of watched) current.add(file);
1065
+ return current;
1066
+ }
1067
+ };
1068
+ }
1069
+ /**
1070
+ * `worker` with `plugin()` after its own `worker.plugins`, which Vite calls for each worker build (nested ones
1071
+ * too); the user's plugins come first, in their order, whether given as a function or as the list older Vite
1072
+ * versions took.
1073
+ */
1074
+ function withWorkerPlugin(worker, plugin) {
1075
+ const own = worker?.plugins;
1076
+ return {
1077
+ ...worker,
1078
+ plugins: () => [typeof own === "function" ? own() : own, plugin()]
1079
+ };
1080
+ }
1081
+ /**
1082
+ * The user's own configuration as Vite evaluated it for the outer server or
1083
+ * build, but for `command`: the config file (when there is one) evaluated
1084
+ * again for that command and mode, with what was passed to `createServer()` or
1085
+ * `build()` (or the CLI flags) on top, as Vite merges them.
1086
+ */
1087
+ async function userConfigFor(config, env) {
1088
+ const inline = config.inlineConfig;
1089
+ if (!config.configFile) return inline;
1090
+ const loaded = await loadConfigFromFile(env, config.configFile, config.root, entryBuildLogLevel(config.logLevel), entryBuildLogger(config.logger), inline.configLoader);
1091
+ return loaded === null ? inline : mergeConfig(loaded.config, inline);
1092
+ }
1093
+ /**
1094
+ * Bundles the entry with a Vite build of its own, from the user's own
1095
+ * configuration, evaluated for `command`: the config file's function and each
1096
+ * plugin's `apply` see that command, and so do the `config`, `configEnvironment`
1097
+ * and `configResolved` hooks of the user's plugins. Every setting comes across
1098
+ * but those ENTRY_BUILD_EXCLUDED_KEYS lists and the build settings
1099
+ * entryBuildSettings decides. twee-ts itself (every instance) is left out.
1100
+ */
1101
+ async function bundleEntry(config, entryPath, command, outputFilename, observer = {}) {
1102
+ const watchFiles = observer.watchFiles ?? /* @__PURE__ */ new Set();
1103
+ const env = {
1104
+ command,
1105
+ mode: config.mode,
1106
+ isSsrBuild: false,
1107
+ isPreview: false
1108
+ };
1109
+ const user = await userConfigFor(config, env);
1110
+ const views = {
1111
+ configs: /* @__PURE__ */ new WeakMap(),
1112
+ environments: /* @__PURE__ */ new WeakMap()
1113
+ };
1114
+ const entryPlugins = async (option) => (await flattenPlugins(option)).filter(isPlugin).filter((plugin) => plugin.name !== "twee-ts" && appliesTo(plugin, command, user, env)).map((plugin) => asEntryBuildPlugin(plugin, command, views));
1115
+ const plugins = await entryPlugins(user.plugins);
1116
+ const workerPlugins = user.worker?.plugins;
1117
+ const inline = {
1118
+ ...withoutExcludedKeys(user),
1119
+ configFile: false,
1120
+ root: config.root,
1121
+ mode: config.mode,
1122
+ logLevel: entryBuildLogLevel(config.logLevel),
1123
+ customLogger: entryBuildLogger(config.logger),
1124
+ clearScreen: false,
1125
+ publicDir: config.publicDir || false,
1126
+ plugins: [
1127
+ ...plugins,
1128
+ entryBuildEnforcer(entryPath, command, outputFilename),
1129
+ recordWatchFiles(watchFiles, config.root, observer).plugin
1130
+ ],
1131
+ worker: withWorkerPlugin({
1132
+ ...user.worker,
1133
+ plugins: () => [entryPlugins(typeof workerPlugins === "function" ? workerPlugins() : workerPlugins)]
1134
+ }, () => recordWatchFiles(watchFiles, config.root, observer).plugin)
1135
+ };
1136
+ const [result] = [await build(inline)].flat();
1137
+ if (result === void 0 || !("output" in result)) throw new Error("twee-ts: the entry build returned no bundle.");
1138
+ const entry = takeEntryFromBundle(Object.fromEntries(result.output.map((item) => [item.fileName, item])));
1139
+ return {
1140
+ ...entry,
1141
+ files: /* @__PURE__ */ new Set([...entry.files, ...watchFiles])
1142
+ };
1143
+ }
1144
+ //#endregion
1145
+ //#region src/plugins/vite-dev.ts
1146
+ /** How long after a change the story is compiled, so saves landing together compile once. */
1147
+ const DEBOUNCE_MS = 50;
1148
+ /**
1149
+ * How long a file the watcher was asked to add may take to be listed as watched, and how long after that its
1150
+ * watch may still miss an edit (macOS FSEvents starts reporting a moment after the watch is set up); see
1151
+ * `recheckOnceWatched` in setUpDevStory.
1152
+ */
1153
+ const WATCH_LISTED_TIMEOUT_MS = 1e4;
1154
+ const WATCH_START_MS = 500;
1155
+ /** Resolves after `ms`, without keeping the process alive. */
1156
+ function delay(ms) {
1157
+ return new Promise((done) => setTimeout(done, ms).unref());
1158
+ }
1159
+ /**
1160
+ * The paths under the base URL the story is served at: the output file name,
1161
+ * and, as a static host serves it, the folder of an `index.html`.
1162
+ */
1163
+ function storyPaths(outputFilename) {
1164
+ const index = "index.html";
1165
+ return outputFilename === index || outputFilename.endsWith(`/${index}`) ? [outputFilename, outputFilename.slice(0, -10)] : [outputFilename];
1166
+ }
1167
+ /**
1168
+ * `path` (a decoded URL path below the base, with no leading slash) as one spelling: a backslash is a slash, as in
1169
+ * an http URL, a `.` segment is dropped and a `..` segment drops the one before it, as the URL standard resolves
1170
+ * them (a last `.` or `..` leaves a trailing slash), and empty segments (`a//b`) are dropped, as Vite's file
1171
+ * middlewares and static hosts drop them. A `..` never leaves the base.
1172
+ */
1173
+ function oneSpelling(path) {
1174
+ const segments = path.replaceAll("\\", "/").split("/");
1175
+ const kept = [];
1176
+ segments.forEach((segment, at) => {
1177
+ const last = at === segments.length - 1;
1178
+ if (segment === "..") kept.pop();
1179
+ if (segment !== "" && segment !== "." && segment !== "..") kept.push(segment);
1180
+ else if (last) kept.push("");
1181
+ });
1182
+ return kept.join("/");
1183
+ }
1184
+ /**
1185
+ * A request URL's path below `base`, decoded, without query or hash, and with its dot segments, empty segments and
1186
+ * backslashes resolved (see oneSpelling), so that every spelling of the story's path is the story (#401); undefined
1187
+ * when the path is outside the base or is not a valid percent-encoding.
1188
+ */
1189
+ function pathBelowBase(url, base) {
1190
+ const raw = url.replace(/[?#].*$/s, "");
1191
+ let path;
1192
+ let decodedBase;
1193
+ try {
1194
+ path = decodeURIComponent(raw);
1195
+ decodedBase = decodeURIComponent(base);
1196
+ } catch {
1197
+ return;
1198
+ }
1199
+ return path.startsWith(decodedBase) ? oneSpelling(path.slice(decodedBase.length)) : void 0;
1200
+ }
1201
+ /** The reload message for a rebuilt story: limited to the story's pages unless Vite's client could not match them. */
1202
+ function reloadPayload(base, outputFilename) {
1203
+ let decoded;
1204
+ try {
1205
+ decoded = decodeURI(base);
1206
+ } catch {
1207
+ return { type: "full-reload" };
1208
+ }
1209
+ return decoded === base ? {
1210
+ type: "full-reload",
1211
+ path: `/${outputFilename}`
1212
+ } : { type: "full-reload" };
1213
+ }
1214
+ /** The media type of the story as each output mode writes it. */
1215
+ const OUTPUT_MEDIA_TYPES = {
1216
+ html: "text/html; charset=utf-8",
1217
+ "twine2-archive": "text/html; charset=utf-8",
1218
+ "twine1-archive": "text/html; charset=utf-8",
1219
+ twee3: "text/plain; charset=utf-8",
1220
+ twee1: "text/plain; charset=utf-8",
1221
+ json: "application/json; charset=utf-8"
1222
+ };
1223
+ /** Served until the first successful compile, so the overlay has a page to appear on. */
1224
+ function waitingPage(base) {
1225
+ return viteWaitingPage(`${base}@vite/client`);
1226
+ }
1227
+ function toOverlayError(e) {
1228
+ const error = fatalError(e);
1229
+ return {
1230
+ message: stripVTControlCharacters(error.message),
1231
+ stack: "",
1232
+ plugin: PLUGIN_NAME,
1233
+ ...error.id ? { id: error.id } : {},
1234
+ ...error.loc ? { loc: error.loc } : {}
1235
+ };
1236
+ }
1237
+ /**
1238
+ * Media types of the files a bundle emits that Tweego's table of story media does not have: a browser
1239
+ * refuses a worker or stylesheet served as `application/octet-stream`.
1240
+ */
1241
+ const BUNDLE_MEDIA_TYPES = /* @__PURE__ */ new Map([
1242
+ ["js", "text/javascript"],
1243
+ ["mjs", "text/javascript"],
1244
+ ["css", "text/css"],
1245
+ ["json", "application/json"],
1246
+ ["map", "application/json"],
1247
+ ["wasm", "application/wasm"],
1248
+ ["html", "text/html; charset=utf-8"],
1249
+ ["txt", "text/plain; charset=utf-8"]
1250
+ ]);
1251
+ /** The media type the dev server gives an asset of the entry's bundle. */
1252
+ function assetMediaType(path) {
1253
+ return BUNDLE_MEDIA_TYPES.get(normalizedFileExt(path)) ?? mediaTypeFromFilename(path);
1254
+ }
1255
+ /**
1256
+ * What a change to a file alters: modification time, change time (which an in-place edit that restores the
1257
+ * modification time still moves, as the loader's file signature relies on), size and inode (a file replaced by a
1258
+ * new one).
1259
+ */
1260
+ function fileState(file) {
1261
+ try {
1262
+ const stat = statSync(file);
1263
+ return `${stat.mtimeMs}:${stat.ctimeMs}:${stat.size}:${stat.ino}`;
1264
+ } catch {
1265
+ return;
1266
+ }
1267
+ }
1268
+ /**
1269
+ * What is in the folder `folder`, at any depth: the name and state (see fileState) of each file and folder in it,
1270
+ * links to folders not followed. A file added, removed, renamed or changed anywhere in it changes this, as it
1271
+ * makes the bundler's own watcher (`vite build --watch`) build again for a folder a plugin watches. undefined when
1272
+ * `folder` is not a folder.
1273
+ */
1274
+ function folderState(folder) {
1275
+ let names;
1276
+ try {
1277
+ if (statSync(folder, { throwIfNoEntry: false })?.isDirectory() !== true) return void 0;
1278
+ names = readdirSync(folder, {
1279
+ recursive: true,
1280
+ encoding: "utf8"
1281
+ });
1282
+ } catch {
1283
+ return;
1284
+ }
1285
+ return names.sort().map((name) => `${name}\0${fileState(join(folder, name)) ?? ""}`).join("\0");
1286
+ }
1287
+ /** `file` as it is now, as a FileRead; undefined when it can't be read. */
1288
+ function fileRead(file) {
1289
+ const state = fileState(file);
1290
+ if (state === void 0) return void 0;
1291
+ try {
1292
+ return {
1293
+ state,
1294
+ hash: sha256Hex(readFileSync(file))
1295
+ };
1296
+ } catch {
1297
+ return;
1298
+ }
1299
+ }
1300
+ /** `paths` as TrackedFiles. */
1301
+ function tracked(paths) {
1302
+ return new Map([...paths].map((path) => [fileKey(path), canonicalPath(path)]));
1303
+ }
1304
+ /** A native identity key and every parent above it, without identifying any path again. */
1305
+ function* keyParents(key) {
1306
+ for (let current = key;;) {
1307
+ yield current;
1308
+ const parent = dirname(current);
1309
+ if (parent === current) return;
1310
+ current = parent;
1311
+ }
1312
+ }
1313
+ /**
1314
+ * The key of the file `authored` reaches, when it reaches it through a link (somewhere along the path, or
1315
+ * at the end of it): the one case where the file's own path says nothing of a change of target.
1316
+ * undefined for a path with no link in it, and for one that reaches nothing.
1317
+ */
1318
+ function linkKey(authored) {
1319
+ if (!existsSync(authored)) return void 0;
1320
+ return canonicalPath(authored) === toPosix(resolve(authored)) ? void 0 : fileKey(authored);
1321
+ }
1322
+ /** The state of each of `files`, by key, for telling later which changed (see filesChanged). */
1323
+ function fileStates(files) {
1324
+ const states = /* @__PURE__ */ new Map();
1325
+ for (const [key, path] of files) {
1326
+ const state = fileState(path);
1327
+ if (state !== void 0) states.set(key, state);
1328
+ }
1329
+ return states;
1330
+ }
1331
+ /**
1332
+ * The states to compare later changes with, for the files a bundle was made from: each file as it was just
1333
+ * before the bundle read it (`observed`, noted as each module loads), else as it was before the bundle began
1334
+ * (`before`, for a file known from the last bundle), else as it is now. An edit while the bundle was made
1335
+ * then differs from the recorded state afterwards, and the next request makes the bundle again.
1336
+ */
1337
+ function settledStates(files, observed, before) {
1338
+ const states = /* @__PURE__ */ new Map();
1339
+ for (const [key, path] of files) {
1340
+ const state = observed.get(key) ?? before.get(key) ?? fileState(path);
1341
+ if (state !== void 0) states.set(key, state);
1342
+ }
1343
+ return states;
1344
+ }
1345
+ /** The keys of the files added, removed or changed between two `fileStates` results. */
1346
+ function filesChanged(before, after) {
1347
+ const changed = /* @__PURE__ */ new Set();
1348
+ for (const [file, state] of after) if (before.get(file) !== state) changed.add(file);
1349
+ for (const file of before.keys()) if (!after.has(file)) changed.add(file);
1350
+ return changed;
1351
+ }
1352
+ /**
1353
+ * The names of Vite's middlewares that answer a request with a file: from the public folder, the module
1354
+ * transform, the files of the root and the file system, and the HTML fallback.
1355
+ */
1356
+ const VITE_FILE_MIDDLEWARES = /* @__PURE__ */ new Set([
1357
+ "viteServePublicMiddleware",
1358
+ "viteTriggerLazyBundlingMiddleware",
1359
+ "viteMemoryFilesMiddleware",
1360
+ "viteTransformMiddleware",
1361
+ "viteServeRawFsMiddleware",
1362
+ "viteServeStaticMiddleware",
1363
+ "viteHtmlFallbackMiddleware",
1364
+ "viteIndexHtmlMiddleware"
1365
+ ]);
1366
+ /**
1367
+ * Installs the story's route (see setUpDevStory) among `middlewares`, as the plugin's post hook does: after Vite's
1368
+ * checks of a request (host, CORS, proxy, base), so they apply to the story as to any page, and before the first
1369
+ * of its middlewares that serve files. A build writes the story and the entry's assets over a public file of the
1370
+ * same name, and a static host serves nothing else at their paths, so the dev server must not answer them with a
1371
+ * file from the public folder or the root (#339). Appended when none of those middlewares is there.
1372
+ */
1373
+ function installStoryRoute(middlewares, route) {
1374
+ const at = middlewares.stack.findIndex(({ handle }) => typeof handle === "function" && VITE_FILE_MIDDLEWARES.has(handle.name));
1375
+ if (at === -1) middlewares.use(route);
1376
+ else middlewares.stack.splice(at, 0, {
1377
+ route: "",
1378
+ handle: route
1379
+ });
1380
+ }
1381
+ /** The instances and entry watches of each server; the watcher is shared by every story (#517). */
1382
+ const devServers = /* @__PURE__ */ new WeakMap();
1383
+ /** The error for a story page whose URL another story or an entry asset of the same dev server also claims. */
1384
+ function storyCollisionMessage(path) {
1385
+ return `twee-ts: more than one generated file is served at ${path || "/"} (another story or an entry asset); set a distinct outputFilename.`;
1386
+ }
1387
+ function sameBytes(a, b) {
1388
+ return Buffer.from(a).equals(Buffer.from(b));
1389
+ }
1390
+ /**
1391
+ * Sets up the story in `server`: the first compile, the watcher, and a
1392
+ * middleware that notes each request's URL before Vite's own middlewares
1393
+ * rewrite it. Resolves, after the first compile, to the middleware that serves
1394
+ * the story and the entry's assets, for installStoryRoute.
1395
+ */
1396
+ async function setUpDevStory(server, dev) {
1397
+ const { options, cache } = dev;
1398
+ const config = server.config;
1399
+ const { base } = config;
1400
+ const paths = storyPaths(options.outputFilename);
1401
+ const { inputs, excluded: excludedGlob } = options;
1402
+ const entryPath = options.entry;
1403
+ const root = fileKey(config.root);
1404
+ const inputKeys = () => inputs.map(fileKey);
1405
+ let outputs = dev.outputs(config);
1406
+ let output = outputPaths(outputs);
1407
+ const excluded = (file) => excludedGlob(file) || output.isOutput(file, inputs);
1408
+ server.watcher.add([...inputs]);
1409
+ const outputMode = options.compile().outputMode ?? "html";
1410
+ let html = "";
1411
+ let lastError;
1412
+ let entry;
1413
+ let servedAssets;
1414
+ const state = devServers.get(server) ?? {
1415
+ instances: /* @__PURE__ */ new Set(),
1416
+ watched: /* @__PURE__ */ new Map(),
1417
+ released: /* @__PURE__ */ new Map()
1418
+ };
1419
+ devServers.set(server, state);
1420
+ const { instances, watched, released } = state;
1421
+ const watchedForEntry = /* @__PURE__ */ new Map();
1422
+ const self = {
1423
+ paths,
1424
+ assets: () => servedAssets,
1425
+ catchUp: () => catchUp(),
1426
+ inputKeys,
1427
+ entryWatches: watchedForEntry
1428
+ };
1429
+ instances.add(self);
1430
+ const fail = (res, message) => {
1431
+ config.logger.error(`[twee-ts] ${message}`);
1432
+ res.statusCode = 500;
1433
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
1434
+ res.end(message);
1435
+ };
1436
+ let entryStale = true;
1437
+ let entryFiles = /* @__PURE__ */ new Map();
1438
+ let entryStates = /* @__PURE__ */ new Map();
1439
+ let entryLinks = /* @__PURE__ */ new Map();
1440
+ let entryMissingFolders = [];
1441
+ let entryGlobs = /* @__PURE__ */ new Map();
1442
+ let globStates = /* @__PURE__ */ new Map();
1443
+ let entryFolders = /* @__PURE__ */ new Map();
1444
+ let entryReads = /* @__PURE__ */ new Map();
1445
+ const readsModules = config.server.watch !== null;
1446
+ let queue = Promise.resolve();
1447
+ let pending = /* @__PURE__ */ new Set();
1448
+ let timer;
1449
+ let closed = false;
1450
+ let compiledInputs = /* @__PURE__ */ new Map();
1451
+ let compiledInputNames;
1452
+ let catchingUp;
1453
+ const closeWatcher = server.watcher.close.bind(server.watcher);
1454
+ server.watcher.close = async () => {
1455
+ closed = true;
1456
+ clearTimeout(timer);
1457
+ pending.clear();
1458
+ return closeWatcher();
1459
+ };
1460
+ const namedInputs = () => inputs.map((input) => {
1461
+ let kind;
1462
+ try {
1463
+ const stat = statSync(input, { throwIfNoEntry: false });
1464
+ kind = stat === void 0 ? "none" : stat.isFile() ? "file" : stat.isDirectory() ? "folder" : "other";
1465
+ } catch (error) {
1466
+ kind = `failed:${String(error)}`;
1467
+ }
1468
+ return `${fileKey(input)}\0${kind}`;
1469
+ }).join("\0");
1470
+ let inputWalk;
1471
+ const walkInputs = () => {
1472
+ const named = namedInputs();
1473
+ const folders = /* @__PURE__ */ new Map();
1474
+ const files = getFilenames(inputs, outputs, [], "source", (folder) => folders.set(folder, fileState(folder))).files.filter((file) => !excludedGlob(file.path)).map((file) => ({
1475
+ key: file.key,
1476
+ path: identify(file.path).absolute,
1477
+ link: file.link === true
1478
+ }));
1479
+ return {
1480
+ outputs,
1481
+ named,
1482
+ folders,
1483
+ files
1484
+ };
1485
+ };
1486
+ const storyInputFiles = (walk) => {
1487
+ const last = inputWalk;
1488
+ const used = last !== void 0 && !walk && last.outputs === outputs && last.named === namedInputs() && [...last.folders].every(([folder, state]) => fileState(folder) === state) ? last : walkInputs();
1489
+ inputWalk = used;
1490
+ const states = /* @__PURE__ */ new Map();
1491
+ for (const { key, path, link } of used.files) {
1492
+ const state = fileState(path);
1493
+ if (state !== void 0) states.set(link ? fileKey(path) : key, state);
1494
+ }
1495
+ return states;
1496
+ };
1497
+ const touchesEntry = (key) => entryPath !== void 0 && (entryFiles.has(key) || entryStale && keyWithin(key, [root, ...entryMissingFolders]));
1498
+ const linkMoved = () => [...entryLinks].some(([spelled, key]) => linkKey(spelled) !== key);
1499
+ const globsChanged = () => [...entryGlobs].some(([id, scope]) => readScope(scope).listing !== globStates.get(id)?.listing);
1500
+ const foldersChanged = () => [...entryFolders.values()].some(({ path, state }) => folderState(path) !== state);
1501
+ const inWatchedFolder = (path) => {
1502
+ if (entryPath === void 0 || entryFolders.size === 0) return false;
1503
+ const folders = [...entryFolders.keys()];
1504
+ return keyWithin(fileKey(path), folders) || keyWithin(fileKey(dirname(path)), folders);
1505
+ };
1506
+ const inGlobScope = (path, isDir) => [...entryGlobs].some(([id, scope]) => scopeAdmits(scope, path, isDir, globStates.get(id)?.linked ?? []));
1507
+ const entryWatchTargets = () => new Map([...entryFiles, ...tracked(scopeWatchTargets(entryGlobs.values()))]);
1508
+ const watchEntryFiles = (files) => {
1509
+ const added = [...files].filter(([key]) => !keyWithin(key, [root]) && !keyWithin(key, inputKeys()) && !watchedForEntry.has(key));
1510
+ for (const key of watchedForEntry.keys()) if (!files.has(key)) watchedForEntry.delete(key);
1511
+ for (const [key, path] of added) {
1512
+ watchedForEntry.set(key, path);
1513
+ watched.set(key, path);
1514
+ }
1515
+ const owners = /* @__PURE__ */ new Set([root, ...[...instances].flatMap((instance) => [...instance.inputKeys(), ...instance.entryWatches.keys()])]);
1516
+ const aboveOwners = new Set([...owners].flatMap((key) => [...keyParents(key)]));
1517
+ const needed = (key) => aboveOwners.has(key) || [...keyParents(key)].some((parent) => owners.has(parent));
1518
+ const dropped = [...watched].filter(([key]) => !needed(key));
1519
+ for (const [key, path] of dropped) {
1520
+ watched.delete(key);
1521
+ released.set(key, path);
1522
+ }
1523
+ const restored = [...released].filter(([key]) => needed(key));
1524
+ for (const [key, path] of restored) {
1525
+ released.delete(key);
1526
+ watched.set(key, path);
1527
+ }
1528
+ if (dropped.length > 0) server.watcher.unwatch(dropped.map(([, path]) => path));
1529
+ const registrations = new Map([...restored, ...added]);
1530
+ if (registrations.size > 0) server.watcher.add([...registrations.values()]);
1531
+ if (added.length > 0) recheckOnceWatched(new Map(added)).catch(keepQueueAlive);
1532
+ };
1533
+ const isListed = (path) => {
1534
+ return server.watcher.getWatched()[dirname(path)]?.includes(basename(path)) ?? false;
1535
+ };
1536
+ const recheckOnceWatched = async (files) => {
1537
+ if (config.server.watch === null) return;
1538
+ const deadline = Date.now() + WATCH_LISTED_TIMEOUT_MS;
1539
+ while (!closed && Date.now() < deadline && ![...files.values()].every(isListed)) await delay(DEBOUNCE_MS);
1540
+ await delay(WATCH_START_MS);
1541
+ if (closed) return;
1542
+ queue = queue.then(() => {
1543
+ const own = new Map([...files].filter(([key]) => entryFiles.has(key)));
1544
+ const now = fileStates(own);
1545
+ const then = new Map([...entryStates].filter(([key]) => own.has(key)));
1546
+ for (const key of filesChanged(then, now)) if (watchedForEntry.has(key)) noteChange(key);
1547
+ const [folder] = [...files.keys()].filter((key) => !own.has(key) && watchedForEntry.has(key));
1548
+ if (folder !== void 0 && globsChanged()) noteChange(folder);
1549
+ const [watched] = [...files.keys()].filter((key) => entryFolders.has(key) && watchedForEntry.has(key));
1550
+ if (watched !== void 0 && foldersChanged()) noteChange(watched);
1551
+ }).catch(keepQueueAlive);
1552
+ await queue;
1553
+ };
1554
+ const bundle = async () => {
1555
+ if (entryPath === void 0) return;
1556
+ entryStale = true;
1557
+ const spelled = /* @__PURE__ */ new Map();
1558
+ const authoredPaths = /* @__PURE__ */ new Set();
1559
+ const watchFiles = /* @__PURE__ */ new Set();
1560
+ const noteSpelling = (authored) => {
1561
+ authoredPaths.add(authored);
1562
+ const key = linkKey(authored);
1563
+ if (key !== void 0 && !spelled.has(authored)) spelled.set(authored, key);
1564
+ };
1565
+ noteSpelling(entryPath);
1566
+ if (entry === void 0) {
1567
+ entryFiles = tracked([entryPath]);
1568
+ entryStates = fileStates(entryFiles);
1569
+ watchEntryFiles(entryWatchTargets());
1570
+ }
1571
+ const globs = /* @__PURE__ */ new Map();
1572
+ const globsRead = /* @__PURE__ */ new Map();
1573
+ const globsBefore = new Map([...entryGlobs].map(([id, scope]) => [id, readScope(scope)]));
1574
+ const folders = /* @__PURE__ */ new Map();
1575
+ const before = fileStates(entryFiles);
1576
+ const observed = /* @__PURE__ */ new Map();
1577
+ const loaded = /* @__PURE__ */ new Map();
1578
+ const reads = /* @__PURE__ */ new Map();
1579
+ let next;
1580
+ try {
1581
+ next = await bundleEntry(config, entryPath, "serve", options.outputFilename, {
1582
+ watchFiles,
1583
+ onWatchFile: (file) => {
1584
+ const key = fileKey(file);
1585
+ if (folders.has(key)) return;
1586
+ const path = canonicalPath(file);
1587
+ const state = folderState(path);
1588
+ if (state !== void 0) folders.set(key, {
1589
+ path,
1590
+ state
1591
+ });
1592
+ },
1593
+ onLoad: (file) => {
1594
+ const key = fileKey(file);
1595
+ const read = readsModules ? fileRead(canonicalPath(file)) : void 0;
1596
+ const state = read?.state ?? fileState(canonicalPath(file));
1597
+ if (state !== void 0 && !observed.has(key)) observed.set(key, state);
1598
+ if (read !== void 0 && !reads.has(key)) reads.set(key, read);
1599
+ if (!loaded.has(key)) loaded.set(key, canonicalPath(file));
1600
+ },
1601
+ onResolve: noteSpelling,
1602
+ onGlob: (scope) => {
1603
+ const id = scopeId(scope);
1604
+ if (globs.has(id)) return;
1605
+ globs.set(id, scope);
1606
+ globsRead.set(id, readScope(scope));
1607
+ }
1608
+ });
1609
+ } catch (error) {
1610
+ const missing = missingImportFolders(authoredPaths);
1611
+ entryMissingFolders = missing.map(fileKey);
1612
+ entryFiles = new Map([
1613
+ ...entryFiles,
1614
+ ...loaded,
1615
+ ...tracked(watchFiles),
1616
+ ...tracked(missing)
1617
+ ]);
1618
+ entryStates = settledStates(entryFiles, observed, before);
1619
+ entryLinks = new Map([...entryLinks, ...spelled]);
1620
+ entryGlobs = new Map([...entryGlobs, ...globs]);
1621
+ globStates = new Map([...globsBefore, ...globsRead]);
1622
+ entryFolders = new Map([...entryFolders, ...folders]);
1623
+ watchEntryFiles(entryWatchTargets());
1624
+ throw error;
1625
+ }
1626
+ entry = next;
1627
+ entryStale = false;
1628
+ entryFiles = tracked(next.files);
1629
+ entryStates = settledStates(entryFiles, observed, before);
1630
+ entryLinks = spelled;
1631
+ entryMissingFolders = [];
1632
+ entryGlobs = globs;
1633
+ globStates = globsRead;
1634
+ entryFolders = folders;
1635
+ entryReads = reads;
1636
+ };
1637
+ const alreadyRead = (key) => {
1638
+ const read = entryReads.get(key);
1639
+ const path = entryFiles.get(key);
1640
+ if (entryStale || keyWithin(key, inputKeys())) return false;
1641
+ if (read === void 0 && !entryFiles.has(key) && keyWithin(key, [...entryFolders.keys()])) return !foldersChanged();
1642
+ if (read === void 0 || path === void 0) return false;
1643
+ const now = fileRead(path);
1644
+ return now?.state === read.state && now.hash === read.hash;
1645
+ };
1646
+ const rebuild = async (changed, initial = false) => {
1647
+ if (closed) return;
1648
+ try {
1649
+ const unread = [...changed].filter((key) => !alreadyRead(key));
1650
+ const regrouped = linkMoved() || globsChanged() || foldersChanged();
1651
+ if (changed.size > 0 && unread.length === 0 && !regrouped) return;
1652
+ outputs = dev.outputs(config);
1653
+ output = outputPaths(outputs);
1654
+ compiledInputs = storyInputFiles(true);
1655
+ compiledInputNames = inputWalk?.named;
1656
+ for (const key of [...cache.keys()]) if (changed.has(fileKey(key))) cache.delete(key);
1657
+ if (entryStale || regrouped || unread.some(touchesEntry)) await bundle();
1658
+ watchEntryFiles(entryWatchTargets());
1659
+ const story = await compileStory(options.compile(entrySources(entry)), outputs, cache);
1660
+ for (const warning of story.warnings) config.logger.warn(`[twee-ts] ${warning}`);
1661
+ html = outputMode === "html" ? dev.injectClient(story.output, base) : story.output;
1662
+ servedAssets = entry?.assets;
1663
+ lastError = void 0;
1664
+ if (!initial) server.ws.send(reloadPayload(base, options.outputFilename));
1665
+ } catch (e) {
1666
+ lastError = toOverlayError(e);
1667
+ config.logger.error(`[twee-ts] ${lastError.message}`);
1668
+ server.ws.send({
1669
+ type: "error",
1670
+ err: lastError
1671
+ });
1672
+ }
1673
+ };
1674
+ const keepQueueAlive = (e) => {
1675
+ try {
1676
+ config.logger.error(`[twee-ts] ${fatalError(e).message}`);
1677
+ } catch {}
1678
+ };
1679
+ const noteChange = (changed) => {
1680
+ if (closed) return;
1681
+ pending.add(changed);
1682
+ clearTimeout(timer);
1683
+ timer = setTimeout(() => {
1684
+ const files = pending;
1685
+ pending = /* @__PURE__ */ new Set();
1686
+ queue = queue.then(() => rebuild(files)).catch(keepQueueAlive);
1687
+ }, DEBOUNCE_MS);
1688
+ };
1689
+ await rebuild(/* @__PURE__ */ new Set(), true);
1690
+ server.watcher.on("all", (event, path) => {
1691
+ if (isViteConfigTemp(path)) return;
1692
+ const changed = fileKey(path);
1693
+ const isDir = event === "addDir" || event === "unlinkDir";
1694
+ const regrouped = event !== "change" && inGlobScope(path, isDir) || inWatchedFolder(path) || isDir && entryPath !== void 0 && entryFiles.has(changed);
1695
+ if (event !== "add" && event !== "change" && event !== "unlink" && !regrouped) return;
1696
+ if (!regrouped && (!keyWithin(changed, inputKeys()) || excluded(path)) && !touchesEntry(changed) && !linkMoved()) return;
1697
+ noteChange(changed);
1698
+ });
1699
+ const catchUp = () => {
1700
+ catchingUp ??= (async () => {
1701
+ for (let waited = queue;; waited = queue) {
1702
+ await waited;
1703
+ if (queue === waited) break;
1704
+ }
1705
+ const changed = filesChanged(compiledInputs, storyInputFiles(false));
1706
+ if (compiledInputNames !== inputWalk?.named) for (const input of inputs) changed.add(fileKey(input));
1707
+ for (const file of filesChanged(entryStates, fileStates(entryFiles))) changed.add(file);
1708
+ if (changed.size === 0 && pending.size === 0 && !linkMoved() && !globsChanged() && !foldersChanged()) return;
1709
+ for (const file of pending) changed.add(file);
1710
+ pending = /* @__PURE__ */ new Set();
1711
+ clearTimeout(timer);
1712
+ queue = queue.then(() => rebuild(changed)).catch(keepQueueAlive);
1713
+ await queue;
1714
+ })().finally(() => {
1715
+ catchingUp = void 0;
1716
+ });
1717
+ return catchingUp;
1718
+ };
1719
+ server.ws.on("connection", () => {
1720
+ if (lastError) server.ws.send({
1721
+ type: "error",
1722
+ err: lastError
1723
+ });
1724
+ });
1725
+ const requestUrls = /* @__PURE__ */ new WeakMap();
1726
+ server.middlewares.use((req, _res, next) => {
1727
+ requestUrls.set(req, String(req.url));
1728
+ next();
1729
+ });
1730
+ const send = (req, res, type, body) => {
1731
+ for (const [name, value] of Object.entries({ ...config.server.headers })) if (typeof value === "string" || typeof value === "number") res.setHeader(name, value);
1732
+ else if (Array.isArray(value)) res.setHeader(name, value.map(String));
1733
+ res.statusCode = 200;
1734
+ res.setHeader("Content-Type", type);
1735
+ res.setHeader("Content-Length", typeof body === "string" ? Buffer.byteLength(body) : body.byteLength);
1736
+ res.setHeader("Cache-Control", "no-cache");
1737
+ res.end(req.method === "HEAD" ? void 0 : body);
1738
+ };
1739
+ return (req, res, next) => {
1740
+ if (req.method !== "GET" && req.method !== "HEAD") {
1741
+ next();
1742
+ return;
1743
+ }
1744
+ const path = pathBelowBase(String(requestUrls.get(req)), base);
1745
+ if (path === void 0) {
1746
+ next();
1747
+ return;
1748
+ }
1749
+ const mine = paths.includes(path);
1750
+ if (!mine && servedAssets?.has(path) !== true) {
1751
+ next();
1752
+ return;
1753
+ }
1754
+ ([...instances].some((instance) => instance.paths.includes(path)) ? Promise.all([...instances].map((instance) => instance.catchUp())) : Promise.resolve()).then(() => {
1755
+ const others = [...instances].filter((other) => other !== self);
1756
+ if ((mine || servedAssets?.has(path) === true) && others.some((other) => other.paths.includes(path) || mine && paths.some((storyPath) => other.assets()?.has(storyPath) === true))) {
1757
+ fail(res, storyCollisionMessage(path));
1758
+ return;
1759
+ }
1760
+ if (mine) {
1761
+ if (paths.some((storyPath) => servedAssets?.has(storyPath) === true)) {
1762
+ fail(res, storyCollisionMessage(path));
1763
+ return;
1764
+ }
1765
+ if (html) send(req, res, OUTPUT_MEDIA_TYPES[outputMode], html);
1766
+ else send(req, res, "text/html; charset=utf-8", waitingPage(base));
1767
+ return;
1768
+ }
1769
+ const asset = servedAssets?.get(path);
1770
+ if (asset === void 0) {
1771
+ next();
1772
+ return;
1773
+ }
1774
+ for (const other of others) {
1775
+ const theirs = other.assets()?.get(path);
1776
+ if (theirs !== void 0 && !sameBytes(theirs, asset)) {
1777
+ fail(res, entryCollisionMessage(path));
1778
+ return;
1779
+ }
1780
+ }
1781
+ send(req, res, assetMediaType(path), asset);
1782
+ }, next);
1783
+ };
1784
+ }
1785
+ //#endregion
1786
+ //#region src/plugins/vite-public.ts
1787
+ /** Inventory public copies by their destination names, following directory links as Vite's copyDir does. */
1788
+ function publicFiles(publicDir) {
1789
+ if (!existsSync(publicDir)) return [];
1790
+ const files = [];
1791
+ const ancestors = /* @__PURE__ */ new Set();
1792
+ const walk = (folder) => {
1793
+ const key = fileKey(folder);
1794
+ if (ancestors.has(key)) return;
1795
+ ancestors.add(key);
1796
+ for (const name of readdirSync(folder)) {
1797
+ const path = resolve(folder, name);
1798
+ if (!existsSync(path)) continue;
1799
+ if (statSync(path).isDirectory()) walk(path);
1800
+ else files.push(relative(publicDir, path));
1801
+ }
1802
+ ancestors.delete(key);
1803
+ };
1804
+ walk(publicDir);
1805
+ return files;
1806
+ }
1807
+ //#endregion
6
1808
  //#region src/plugins/vite.ts
7
1809
  /**
8
1810
  * Ready-made Vite plugin for twee-ts.
@@ -12,122 +1814,41 @@ import { build, version } from "vite";
12
1814
  * Story Stylesheet.
13
1815
  *
14
1816
  * In dev the compiled HTML is served at the base URL with Vite's client added.
15
- * Every change to a source, the head file, a module or a file the entry imports
16
- * recompiles it and reloads the page, and errors appear in Vite's overlay. A
17
- * request for the story also recompiles it first if a source, the head file or
18
- * a module changed without the watcher reporting it.
19
- */
20
- /** Passage names the bundled entry takes inside the story. */
21
- const ENTRY_SCRIPT_NAME = "twee-ts-entry.js";
22
- const ENTRY_STYLE_NAME = "twee-ts-entry.css";
23
- /**
24
- * Set on the inline config of the build this plugin starts itself to bundle the
25
- * entry in dev. The plugin instance that build loads from the user's config file
26
- * sees it and stands aside.
1817
+ * Every change to a source, the head file, a module or a file the entry was
1818
+ * bundled from recompiles it and reloads the page, and errors appear in Vite's
1819
+ * overlay (see vite-dev.ts). A build emits the story with the configured file
1820
+ * name, in the client environment only.
27
1821
  */
28
- /** The plugin's name, which is also how the entry build tells it from the user's other plugins. */
29
- const PLUGIN_NAME = "twee-ts";
30
- const INNER_BUILD_FLAG = "__tweeTsEntryBuild";
31
1822
  /**
32
- * A build input that stands in when there is no entry, so Vite does not look
33
- * for an index.html (whose page would replace the story). Its empty chunk is
34
- * dropped from the output.
1823
+ * A build input that stands in when the user's config names none, so Vite does
1824
+ * not look for an index.html (whose page would replace the story). Its empty
1825
+ * chunk is dropped from the output.
35
1826
  */
36
1827
  const EMPTY_INPUT = "virtual:twee-ts-empty-input";
37
1828
  const RESOLVED_EMPTY_INPUT = "\0" + EMPTY_INPUT;
38
- const viteMajor = Number.parseInt(version, 10);
39
- function buildCompileOptions(options, entry) {
40
- const inline = [];
41
- if (entry) {
42
- inline.push({
43
- filename: ENTRY_SCRIPT_NAME,
44
- content: entry.script
45
- });
46
- if (entry.style !== "") inline.push({
47
- filename: ENTRY_STYLE_NAME,
48
- content: entry.style
49
- });
50
- }
51
- return {
52
- ...options.compileOptions,
53
- sources: [...options.sources, ...inline],
54
- formatId: options.format ?? options.compileOptions?.formatId
55
- };
56
- }
57
- /** Whether the user's config names build inputs of its own. */
58
- function hasUserInput(userConfig) {
59
- const build = userConfig.build;
60
- return build?.["rolldownOptions"]?.input !== void 0 || build?.["rollupOptions"]?.input !== void 0;
61
- }
62
- /** Build settings naming one input, under the option name this Vite version reads. */
63
- function inputOnly(input) {
64
- return viteMajor >= 8 ? { rolldownOptions: { input } } : { rollupOptions: { input } };
65
- }
66
- /**
67
- * Build settings that turn the entry into one IIFE script and one stylesheet.
68
- * Fonts and images the entry uses are inlined as data URLs, so the story stays
69
- * one file; only an import marked `?no-inline` is still emitted separately.
70
- */
71
- function entryBuildOptions(entryPath) {
72
- return {
73
- cssCodeSplit: false,
74
- assetsInlineLimit: () => true,
75
- rolldownOptions: {
76
- input: entryPath,
77
- output: {
78
- format: "iife",
79
- entryFileNames: ENTRY_SCRIPT_NAME,
80
- assetFileNames: "[name][extname]"
81
- }
82
- }
83
- };
84
- }
85
- /** Removes a trailing source-map comment that points at a file (a data: URL map stays). */
86
- function dropFileSourceMapComment(code) {
87
- return code.replace(/\n?\/\/# sourceMappingURL=(?!data:)\S*\s*$/, "").replace(/\n?\/\*# sourceMappingURL=(?!data:)[^*]*\*\/\s*$/, "");
1829
+ /** The environment the story belongs to: the browser's. */
1830
+ const CLIENT = "client";
1831
+ /** Vite's plugin that bundles workers, and keeps their bundles between the builds of `vite build --watch`. */
1832
+ const VITE_WORKER_PLUGIN = "vite:worker";
1833
+ /** The plugin instances created so far, which numbers each instance's entry input. */
1834
+ let instances = 0;
1835
+ /** The bundler options of build settings: `rolldownOptions`, or the older name Vite 8 still reads. */
1836
+ function bundlerOptionsOf(build) {
1837
+ if (!isRecord(build)) return void 0;
1838
+ const options = build["rolldownOptions"] ?? build["rollupOptions"];
1839
+ return isRecord(options) ? options : void 0;
88
1840
  }
89
1841
  /**
90
- * Takes the entry's script and stylesheet out of the bundle, together with
91
- * their map files, which the story cannot ship. Anything else the bundler
92
- * emitted stays in the bundle, so a build writes it next to the HTML, and is
93
- * also returned, so the dev server can serve it.
1842
+ * Whether a config (or an environment's options) names what the build builds:
1843
+ * a bundler input, Vite 8's top-level `input`, a library entry or an SSR entry.
94
1844
  */
95
- function takeEntryFromBundle(bundle) {
96
- const entry = {
97
- script: "",
98
- style: "",
99
- files: /* @__PURE__ */ new Set(),
100
- assets: /* @__PURE__ */ new Map()
101
- };
102
- const styles = [];
103
- for (const [fileName, item] of Object.entries(bundle)) if (item.type === "chunk") {
104
- if (item.isEntry) {
105
- entry.script = dropFileSourceMapComment(item.code);
106
- for (const id of item.moduleIds) if (!id.startsWith("\0")) entry.files.add(fileOfId(id));
107
- }
108
- delete bundle[fileName];
109
- } else if (fileName.endsWith(".css")) {
110
- const css = typeof item.source === "string" ? item.source : new TextDecoder().decode(item.source);
111
- styles.push(dropFileSourceMapComment(css));
112
- delete bundle[fileName];
113
- } else if (fileName.endsWith(".map")) delete bundle[fileName];
114
- else entry.assets.set(fileName, item.source);
115
- entry.style = styles.join("\n");
116
- return entry;
117
- }
118
- /** Absolute forward-slash paths of the head file and the modules, which `exclude` doesn't apply to. */
119
- function headInputs(options) {
120
- const extra = options.compileOptions;
121
- return [...extra?.headFile ? [extra.headFile] : [], ...extra?.modules ?? []].map((p) => toPosix(resolve(p)));
1845
+ function namesInput(config) {
1846
+ const build = Reflect.get(config, "build");
1847
+ return Reflect.get(config, "input") !== void 0 || bundlerOptionsOf(build)?.["input"] !== void 0 || isRecord(build) && (Boolean(build["lib"]) || Boolean(build["ssr"]));
122
1848
  }
123
- /**
124
- * Whether a file (forward-slash path) is a source `exclude` matches. `exclude`
125
- * never applies to the head file and modules.
126
- */
127
- function excludedByGlob(options) {
128
- const exclude = options.compileOptions?.exclude ?? [];
129
- const notExcludable = headInputs(options);
130
- return (file) => isExcluded(file, exclude) && !isInside(file, notExcludable);
1849
+ /** Build settings naming one input. */
1850
+ function inputOnly(input) {
1851
+ return { rolldownOptions: { input } };
131
1852
  }
132
1853
  /**
133
1854
  * Where a build with this config writes: each output's folder, the bundler's
@@ -138,7 +1859,7 @@ function excludedByGlob(options) {
138
1859
  */
139
1860
  function configOutputLocations(config) {
140
1861
  const outDir = resolve(config.root, config.build.outDir);
141
- const option = config.build[viteMajor >= 8 ? "rolldownOptions" : "rollupOptions"]?.output;
1862
+ const option = bundlerOptionsOf(config.build)?.["output"];
142
1863
  return (option === void 0 ? [{}] : Array.isArray(option) ? option : [option]).flatMap((output) => {
143
1864
  const [location] = outputLocations(output);
144
1865
  if (location === void 0) return [{ dir: outDir }];
@@ -150,14 +1871,14 @@ function configOutputLocations(config) {
150
1871
  * Every path a build with this config writes, as far as the config tells: each
151
1872
  * output's folder (see configOutputLocations), the story in it, and the copies
152
1873
  * of the public files Vite puts there. The plugin adds the files each bundle
153
- * writes as it builds (see createOutputRecord).
1874
+ * writes as it builds (see createOutputRecord). A build passes its persistent
1875
+ * record so public copies remain excluded after a file is removed or a link is retargeted.
154
1876
  */
155
- function configOutputs(config, outputFilename) {
156
- const record = createOutputRecord(outputFilename);
157
- const publicFiles = config.build.copyPublicDir && config.publicDir ? getFilenames([config.publicDir]).filenames.map((file) => relative(config.publicDir, resolve(file))) : [];
1877
+ function configOutputs(config, record) {
1878
+ const copiedFiles = config.build.copyPublicDir && config.publicDir ? publicFiles(config.publicDir) : [];
158
1879
  for (const location of configOutputLocations(config)) {
159
1880
  record.addLocation(location);
160
- record.addFiles(location, publicFiles);
1881
+ record.addFiles(location, copiedFiles);
161
1882
  }
162
1883
  return record.outputs();
163
1884
  }
@@ -165,301 +1886,275 @@ function configOutputs(config, outputFilename) {
165
1886
  function mergeOutputs(a, b) {
166
1887
  return {
167
1888
  files: [...a.files, ...b.files],
1889
+ stories: [...a.stories ?? [], ...b.stories ?? []],
168
1890
  dirs: [...a.dirs, ...b.dirs]
169
1891
  };
170
1892
  }
171
- /** Absolute forward-slash paths whose changes recompile the story: sources, head file, modules. */
172
- function watchedInputs(options) {
173
- return [...options.sources.map((p) => toPosix(resolve(p))), ...headInputs(options)];
174
- }
175
- /**
176
- * What `vite build --watch` watches for the story: every file under `inputs`
177
- * (forward-slash paths) that source discovery reads, but those `skip` returns
178
- * true for, and every folder under them. The bundler watches each path on its
179
- * own, not recursively, so a folder is listed for the files added to it or
180
- * deleted from it. As in source discovery, a link to a folder is not followed.
181
- *
182
- * A watched folder may still report changes in its subfolders (Rolldown's
183
- * watcher on Linux does), so no path the build writes (`outputs`) is listed, nor
184
- * a folder that holds one: writing the outputs would start the next build. A file
185
- * added straight to such a folder is found when another change starts a build.
186
- */
187
- function buildWatchTargets(inputs, skip, outputs) {
188
- const targets = [];
189
- const walk = (path, real, isRoot) => {
190
- if (outputs.isFile(real) || !isRoot && outputs.isDir(real)) return;
191
- let entry;
192
- let names;
193
- try {
194
- entry = isRoot ? {
195
- stat: statSync(path),
196
- real
197
- } : walkedEntry(path, real);
198
- if (entry === void 0 || outputs.isFile(entry.real)) return;
199
- if (!entry.stat.isDirectory()) {
200
- if (!skip(path)) targets.push(path);
201
- return;
202
- }
203
- names = readdirSync(path);
204
- } catch {
205
- return;
206
- }
207
- if (!outputs.holds(entry.real)) targets.push(path);
208
- for (const name of names) walk(`${path}/${name}`, join(entry.real, name), false);
209
- };
210
- for (const input of inputs) walk(input, realPathOf(input), true);
211
- return targets;
1893
+ /** Adds Vite's client to the page, first in its head, so reloads and the error overlay reach it. */
1894
+ function injectViteClient(html, base) {
1895
+ return insertViteClient(html, `${base}@vite/client`);
212
1896
  }
213
1897
  /**
214
- * Every file under `inputs` (forward-slash paths) that source discovery reads,
215
- * leaving out `outputs`, but those `skip` returns true for, with what a change to
216
- * it alters: modification time, size and inode (a file replaced by a new one).
1898
+ * What `vite build --watch` is told to watch for the entry's globs: each glob's folder (or the nearest one above it
1899
+ * that exists), by real path, as the bundler's watcher reports it. A folder that holds a build output is left out,
1900
+ * since the watcher would report what the build writes there and rebuild for ever: a file added to it is bundled
1901
+ * with the next build something else starts.
217
1902
  */
218
- function inputFiles(inputs, skip, outputs) {
219
- const files = /* @__PURE__ */ new Map();
220
- for (const filename of getFilenames(inputs, outputs).filenames) {
221
- const file = toPosix(resolve(filename));
222
- if (skip(file)) continue;
223
- try {
224
- const stat = statSync(filename);
225
- files.set(file, `${stat.mtimeMs}:${stat.size}:${stat.ino}`);
226
- } catch {}
227
- }
228
- return files;
229
- }
230
- /** The files added, removed or changed between two `inputFiles` results. */
231
- function filesChanged(before, after) {
232
- const changed = /* @__PURE__ */ new Set();
233
- for (const [file, state] of after) if (before.get(file) !== state) changed.add(file);
234
- for (const file of before.keys()) if (!after.has(file)) changed.add(file);
235
- return changed;
1903
+ function globWatchTargets(scopes, outputs) {
1904
+ return scopeWatchTargets(scopes).map(canonicalPath).filter((folder) => !outputs.holds(folder));
236
1905
  }
237
- /** Adds Vite's client to the page so reloads and the error overlay reach it. */
238
- function injectViteClient(html, base) {
239
- const tag = `<script type="module" src="${base}@vite/client"><\/script>`;
240
- const at = findHeadStartEnd(html);
241
- if (at === void 0) return tag + html;
242
- return html.slice(0, at) + tag + html.slice(at);
1906
+ /** Whether a resolved config is a build of the client, the only build that carries the story. */
1907
+ function isStoryBuild(config) {
1908
+ return config.command === "build" && !config.build.ssr;
243
1909
  }
244
- /** Served until the first successful compile, so the overlay has a page to appear on. */
245
- function waitingPage(base) {
246
- return `<!doctype html><html><head><meta charset="utf-8"><script type="module" src="${base}@vite/client"><\/script><title>twee-ts</title></head><body><p>The story has not compiled yet.</p></body></html>`;
1910
+ /** An import as written: its importer and its source. */
1911
+ function importKey(importer, source) {
1912
+ return `${importer ?? ""}\0${source}`;
247
1913
  }
248
- function toOverlayError(e) {
249
- const error = fatalError(e);
250
- return {
251
- message: stripVTControlCharacters(error.message),
252
- stack: "",
253
- plugin: "twee-ts",
254
- ...error.id ? { id: error.id } : {},
255
- ...error.loc ? { loc: error.loc } : {}
256
- };
1914
+ /** Whether a plugin is Vite's own (its resolver, alias and the rest) or one of twee-ts's. */
1915
+ function isOwnPlugin(plugin) {
1916
+ return plugin.name === "alias" || plugin.name.startsWith("vite:") || plugin.name.startsWith("builtin:") || plugin.name === "twee-ts" || plugin.name.startsWith(`twee-ts:`);
257
1917
  }
258
- /** Bundles the entry for dev with the user's own Vite config, unminified, with an inline source map. */
259
1918
  /**
260
- * The entry build's logger: its warnings go to the dev server's logger; its
261
- * progress lines and its own "build failed" line are dropped, because the plugin
262
- * reports a failed bundle itself.
1919
+ * A copy of `plugin` (see pluginWith) with a `resolveId` hook that passes each import it resolves (see importKey) to
1920
+ * `claim`. A plugin without a `resolveId` hook in JavaScript is returned as it is.
263
1921
  */
264
- function entryBuildLogger(outer) {
265
- return {
266
- info: () => {},
267
- warn: (message, options) => outer.warn(message, options),
268
- warnOnce: (message, options) => outer.warnOnce(message, options),
269
- error: () => {},
270
- clearScreen: () => {},
271
- hasErrorLogged: () => false,
272
- get hasWarned() {
273
- return outer.hasWarned;
274
- }
1922
+ function claimingPlugin(plugin, claim) {
1923
+ const hook = plugin.resolveId;
1924
+ const handler = typeof hook === "function" ? hook : hook?.handler;
1925
+ if (typeof handler !== "function") return plugin;
1926
+ const claiming = async function(...args) {
1927
+ const result = await Reflect.apply(handler, this, args);
1928
+ const [source, importer] = args;
1929
+ if (result !== null && result !== void 0 && typeof source === "string") claim(importKey(typeof importer === "string" ? importer : void 0, source));
1930
+ return result;
275
1931
  };
1932
+ return pluginWith(plugin, /* @__PURE__ */ new Map([["resolveId", typeof hook === "function" ? claiming : {
1933
+ ...hook,
1934
+ handler: claiming
1935
+ }]]));
276
1936
  }
277
- /**
278
- * Keeps the entry build a one-off even when the user's config sets build.watch:
279
- * Vite's config merge skips a null, so the inline `watch: null` alone can't.
280
- */
281
- const oneOffEntryBuild = {
282
- name: "twee-ts:one-off-entry-build",
283
- enforce: "post",
284
- config(userConfig) {
285
- if (userConfig.build) userConfig.build.watch = null;
286
- }
287
- };
288
- /** Build-phase plugin hooks that may call `this.addWatchFile`. */
289
- const WATCH_FILE_HOOKS = [
290
- "buildStart",
291
- "resolveId",
292
- "resolveDynamicImport",
293
- "load",
294
- "transform",
295
- "moduleParsed",
296
- "buildEnd"
297
- ];
298
- /** A module id or watched file as a forward-slash file path, without a query or hash (like Vite's cleanUrl). */
299
- function fileOfId(id) {
300
- return toPosix(id.replace(/[?#].*$/s, ""));
301
- }
302
- /** A plugin context whose `addWatchFile` also records the file in `files`. */
303
- function recordingContext(context, files) {
304
- return new Proxy(context, { get(target, key) {
305
- const value = Reflect.get(target, key, target);
306
- if (typeof value !== "function") return value;
307
- if (key !== "addWatchFile") return value.bind(target);
308
- return (id) => {
309
- if (!id.startsWith("\0")) files.add(fileOfId(resolve(id)));
310
- return value.call(target, id);
311
- };
312
- } });
313
- }
314
- /**
315
- * A copy of `plugin` whose hooks record `addWatchFile` calls in `files`. The
316
- * original stays untouched, so a plugin object shared across entry builds never
317
- * collects wrappers. The copy keeps the prototype, so class-based plugins keep
318
- * their methods, and object-form hooks keep `order`, `filter` and the rest. A
319
- * plugin with no such hooks (a built-in one, for instance) is returned as is.
320
- */
321
- function recordingPlugin(plugin, files) {
322
- if (typeof plugin !== "object" || plugin === null) return plugin;
323
- const hooks = plugin;
324
- const hookHandler = (hook) => typeof hook === "object" && hook !== null ? hook.handler : hook;
325
- if (!WATCH_FILE_HOOKS.some((name) => typeof hookHandler(hooks[name]) === "function")) return plugin;
326
- const copy = Object.create(Object.getPrototypeOf(plugin), Object.getOwnPropertyDescriptors(plugin));
327
- for (const name of WATCH_FILE_HOOKS) {
328
- const hook = hooks[name];
329
- const handler = hookHandler(hook);
330
- if (typeof handler !== "function") continue;
331
- const wrapped = function(...args) {
332
- return handler.apply(recordingContext(this, files), args);
333
- };
334
- copy[name] = typeof hook === "function" ? wrapped : {
335
- ...hook,
336
- handler: wrapped
337
- };
338
- }
339
- return copy;
340
- }
341
- /**
342
- * Collects the files the entry build's plugins add with `addWatchFile`. These
343
- * are not modules of the bundle: Vite's CSS plugin adds the stylesheets pulled
344
- * in by `@import` and the files `url()` points at this way. The build runs on
345
- * recording copies of its plugins. They are made in the bundler's `options`
346
- * hook, which sees the final plugin list, including the plugins Vite resolves
347
- * per environment (`applyToEnvironment`) after `configResolved`.
348
- */
349
- function recordWatchFiles(files) {
350
- return {
351
- name: "twee-ts:record-watch-files",
352
- options: {
353
- order: "post",
354
- async handler(inputOptions) {
355
- const plugins = (await flattenPlugins(inputOptions.plugins)).map((p) => recordingPlugin(p, files));
356
- return {
357
- ...inputOptions,
358
- plugins
359
- };
1937
+ function tweeTsPlugin(options) {
1938
+ checkViteVersion(version);
1939
+ const resolved = resolvePluginOptions("vite", options);
1940
+ const { outputFilename, entry: entryPath } = resolved;
1941
+ instances += 1;
1942
+ const entryInput = `${ENTRY_INPUT_NAME}-${instances}`;
1943
+ const cache = /* @__PURE__ */ new Map();
1944
+ const ownBuilds = /* @__PURE__ */ new WeakMap();
1945
+ const entryInputs = /* @__PURE__ */ new WeakMap();
1946
+ const record = createOutputRecord(outputFilename);
1947
+ const allOutputs = (config) => mergeOutputs(configOutputs(config, createOutputRecord(outputFilename)), record.outputs());
1948
+ /**
1949
+ * The build settings this instance adds to a client build: none when the
1950
+ * user's config names an input (the entry is then bundled by a build of its
1951
+ * own); else, with an entry, the entry as the only input (unless the user's
1952
+ * output is an array, which the entry's IIFE can't use); else the stand-in
1953
+ * input.
1954
+ */
1955
+ const inputSettings = (config) => {
1956
+ if (namesInput(config)) return void 0;
1957
+ if (entryPath === void 0) return { build: inputOnly(EMPTY_INPUT) };
1958
+ const output = config.build?.rolldownOptions?.output;
1959
+ if (Array.isArray(output)) return { build: inputOnly(EMPTY_INPUT) };
1960
+ return entryInputSettings(entryInput, entryPath, output, outputFilename);
1961
+ };
1962
+ /** Whether this instance's entry is an input of the build with this config. */
1963
+ const bundlesEntryInside = (config) => {
1964
+ const input = bundlerOptionsOf(config.build)?.["input"];
1965
+ return isRecord(input) && Object.hasOwn(input, entryInput);
1966
+ };
1967
+ /** The resolved config of the build a hook runs in: its environment's, so builds sharing the instance keep apart. */
1968
+ const configOf = (context) => context.environment.config;
1969
+ let watchOutputs = /* @__PURE__ */ new WeakMap();
1970
+ const keptTargets = /* @__PURE__ */ new Map();
1971
+ const claimed = /* @__PURE__ */ new Set();
1972
+ const outputsOf = (config) => {
1973
+ const known = watchOutputs.get(config);
1974
+ if (known !== void 0) return known;
1975
+ const outputs = outputPaths(allOutputs(config));
1976
+ watchOutputs.set(config, outputs);
1977
+ return outputs;
1978
+ };
1979
+ /**
1980
+ * `vite build --watch` with the entry bundled inside the build: the bundler watches each module by its real path,
1981
+ * so the links an import of the entry's graph goes through (or the folder where an import that reaches nothing
1982
+ * would be created) are registered here, as the import is resolved (see importWatchTargets).
1983
+ */
1984
+ const registerImport = async (context, source, importer) => {
1985
+ const config = configOf(context);
1986
+ const { spellings, kept } = await importSpellings(context, source, importer, config.root);
1987
+ for (const target of importWatchTargets(spellings, outputsOf(config))) context.addWatchFile(target);
1988
+ return kept;
1989
+ };
1990
+ /** Likewise, each glob's folder: Vite's import-glob reads the files a glob matches, and registers no folder. */
1991
+ const registerGlobs = async (context, code, id) => {
1992
+ const config = configOf(context);
1993
+ const scopes = await moduleGlobScopes(context, code, id, config.root);
1994
+ for (const target of globWatchTargets(scopes, outputsOf(config))) context.addWatchFile(target);
1995
+ return scopes;
1996
+ };
1997
+ const workerGlobs = /* @__PURE__ */ new Map();
1998
+ let warnedWorkerCache = false;
1999
+ /**
2000
+ * `vite build --watch` keeps each worker's bundle until a file it read changes, and a file added to the folder of
2001
+ * one of its globs is none of those. When `file` is added to (or removed from) the scope of a worker's glob, the
2002
+ * bundles of the workers whose modules call the glob are let go, through Vite's own worker plugin, as an edit of
2003
+ * those modules would; the build that follows bundles them again (#424).
2004
+ */
2005
+ const releaseWorkerGlobs = async (context, file) => {
2006
+ const importers = [...workerGlobs.values()].filter(({ scope }) => {
2007
+ const { linked } = readScope(scope);
2008
+ return scopeAdmits(scope, file, false, linked) || scopeAdmits(scope, file, true, linked);
2009
+ }).flatMap(({ importers: modules }) => [...modules]);
2010
+ if (importers.length === 0) return;
2011
+ const hook = configOf(context).plugins.find((plugin) => plugin.name === VITE_WORKER_PLUGIN)?.watchChange;
2012
+ const handler = typeof hook === "function" ? hook : hook?.handler;
2013
+ if (typeof handler !== "function") {
2014
+ if (!warnedWorkerCache) {
2015
+ warnedWorkerCache = true;
2016
+ configOf(context).logger.warn(`[twee-ts] This Vite version has no ${VITE_WORKER_PLUGIN} plugin with a watchChange hook, through which twee-ts has Vite bundle a worker again: a file added to the folder of a worker's import.meta.glob() is bundled when one of the worker's modules changes.`);
360
2017
  }
2018
+ return;
361
2019
  }
2020
+ for (const module of importers) await Reflect.apply(handler, context, [module, { event: "update" }]);
362
2021
  };
363
- }
364
- /** A plugin option (nested arrays, promises, falsy entries) as one flat list. */
365
- async function flattenPlugins(option) {
366
- const value = await option;
367
- if (Array.isArray(value)) return (await Promise.all(value.map(flattenPlugins))).flat();
368
- return value ? [value] : [];
369
- }
370
- /**
371
- * The plugins passed to `createServer()` itself, flattened, without twee-ts: the entry build must not run this
372
- * plugin, whose state belongs to the server, and a config file brings its own plugins when it is read again.
373
- * Vite's resolved list is not used, as it holds the built-ins every build adds for itself.
374
- */
375
- async function inlinePlugins(config) {
376
- return (await flattenPlugins(config.inlineConfig.plugins)).filter((plugin) => plugin.name !== PLUGIN_NAME);
377
- }
378
- /** Bundles the entry for dev with the user's own Vite config, unminified, with an inline source map. */
379
- async function bundleEntryForDev(config, entryPath) {
380
- const watchFiles = /* @__PURE__ */ new Set();
381
- const { define, resolve: inlineResolve } = config.inlineConfig;
382
- const settings = config.configFile ? {
383
- define,
384
- ...inlineResolve?.alias ? { resolve: { alias: inlineResolve.alias } } : {}
385
- } : {
386
- define: config.define,
387
- resolve: { alias: config.resolve.alias }
388
- };
389
- const inline = {
390
- configFile: config.configFile ?? false,
391
- root: config.root,
392
- mode: config.mode,
393
- customLogger: entryBuildLogger(config.logger),
394
- publicDir: false,
395
- ...settings,
396
- build: {
397
- ...entryBuildOptions(entryPath),
398
- write: false,
399
- minify: false,
400
- sourcemap: "inline",
401
- emptyOutDir: false,
402
- copyPublicDir: false,
403
- watch: null
2022
+ /**
2023
+ * The same registrations in each build of a worker the entry starts, which Vite bundles nested in the entry's
2024
+ * build with the plugins of `worker.plugins` (#424). What a worker build registers is watched by the outer build.
2025
+ */
2026
+ const workerWatcher = () => ({
2027
+ name: `${PLUGIN_NAME}:worker-watch`,
2028
+ resolveId: {
2029
+ order: "pre",
2030
+ async handler(source, importer) {
2031
+ await registerImport(this, source, importer);
2032
+ }
404
2033
  },
405
- plugins: [
406
- ...await inlinePlugins(config),
407
- oneOffEntryBuild,
408
- recordWatchFiles(watchFiles)
409
- ],
410
- [INNER_BUILD_FLAG]: true
411
- };
412
- const out = await build(inline);
413
- const bundle = {};
414
- for (const result of Array.isArray(out) ? out : [out]) {
415
- if (!("output" in result)) {
416
- await result.close();
417
- throw new Error("twee-ts: the entry build returned a watcher instead of a bundle.");
2034
+ transform: {
2035
+ filter: { code: "import.meta.glob" },
2036
+ async handler(code, id) {
2037
+ for (const scope of await registerGlobs(this, code, id)) {
2038
+ const key = scopeId(scope);
2039
+ const known = workerGlobs.get(key) ?? {
2040
+ scope,
2041
+ importers: /* @__PURE__ */ new Set()
2042
+ };
2043
+ known.importers.add(fileOfId(id));
2044
+ workerGlobs.set(key, known);
2045
+ }
2046
+ }
418
2047
  }
419
- for (const item of result.output) bundle[item.fileName] = item;
420
- }
421
- const entry = takeEntryFromBundle(bundle);
422
- for (const file of watchFiles) entry.files.add(file);
423
- return entry;
424
- }
425
- function tweeTsPlugin(options) {
426
- const outputFilename = options.outputFilename ?? "index.html";
427
- const cache = /* @__PURE__ */ new Map();
428
- let innerBuild = false;
429
- let building = false;
430
- let buildConfig;
431
- const record = createOutputRecord(outputFilename);
432
- const allOutputs = (config) => mergeOutputs(configOutputs(config, outputFilename), record.outputs());
433
- if (options.entry && viteMajor < 8) throw new Error(`twee-ts: the entry option needs Vite 8 or newer (found ${version}).`);
2048
+ });
434
2049
  return {
435
2050
  name: PLUGIN_NAME,
436
- config(userConfig, env) {
437
- innerBuild = userConfig[INNER_BUILD_FLAG] === true;
438
- building = env.command === "build";
439
- if (innerBuild || !building) return void 0;
440
- if (options.entry) return { build: entryBuildOptions(resolve(options.entry)) };
441
- if (hasUserInput(userConfig)) return void 0;
442
- return { build: inputOnly(EMPTY_INPUT) };
2051
+ applyToEnvironment: (environment) => environment.name === CLIENT,
2052
+ config: {
2053
+ order: "post",
2054
+ handler(config, env) {
2055
+ const client = config.environments?.[CLIENT] ?? {};
2056
+ if (env.command !== "build" || entryPath === void 0 || namesInput(config) || namesInput(client)) return;
2057
+ const renderBuiltUrl = entryRenderBuiltUrl(config.base, outputFilename, config.experimental?.renderBuiltUrl);
2058
+ if (renderBuiltUrl !== void 0) config.experimental = {
2059
+ ...config.experimental,
2060
+ renderBuiltUrl
2061
+ };
2062
+ if (config.build?.watch) config.worker = withWorkerPlugin(config.worker, workerWatcher);
2063
+ }
2064
+ },
2065
+ configEnvironment(name, environmentConfig, env) {
2066
+ if (name !== CLIENT || env.command !== "build") return void 0;
2067
+ return inputSettings(environmentConfig);
443
2068
  },
444
2069
  configResolved(config) {
445
- if (innerBuild) return;
446
- if (building) buildConfig = config;
447
- if (!options.entry) return;
448
- const sources = options.sources.map((p) => toPosix(resolve(p)));
449
- if (isInside(toPosix(resolve(options.entry)), sources)) config.logger.warn(`[twee-ts] The entry ${options.entry} is inside the story sources (${options.sources.join(", ")}). twee-ts also loads the .js and .css files it finds in source folders, unbundled, as Story JavaScript and Story Stylesheet; keep the entry's folder out of \`sources\`.`);
2070
+ if (entryPath === void 0) return;
2071
+ if (config.command === "build" && (bundlesEntryInside(config) || bundlesEntryInside({ build: config.environments[CLIENT]?.build }))) {
2072
+ const recorder = recordWatchFiles(void 0, config.root);
2073
+ entryInputs.set(config, recorder.inputs);
2074
+ Reflect.set(config.plugins, config.plugins.length, pluginWith(recorder.plugin, /* @__PURE__ */ new Map([["applyToEnvironment", (environment) => environment.name === CLIENT]])));
2075
+ }
2076
+ if (config.command === "build" && config.build.watch && bundlesEntryInside(config)) config.plugins.forEach((plugin, at) => {
2077
+ if (!isOwnPlugin(plugin)) Reflect.set(config.plugins, at, claimingPlugin(plugin, (key) => claimed.add(key)));
2078
+ });
2079
+ if (resolved.inputs.some((input) => isSameOrInside(entryPath, input))) config.logger.warn(`[twee-ts] The entry ${toPosix(entryPath)} is inside the story sources (${resolved.sources.join(", ")}). twee-ts also loads the .js and .css files it finds in source folders, unbundled, as Story JavaScript and Story Stylesheet; keep the entry's folder out of \`sources\`.`);
2080
+ },
2081
+ async buildStart() {
2082
+ const config = configOf(this);
2083
+ if (!isStoryBuild(config)) return;
2084
+ watchOutputs = /* @__PURE__ */ new WeakMap();
2085
+ forgetKeptResolver(config);
2086
+ if (this.meta.watchMode) {
2087
+ const outputs = outputPaths(allOutputs(config), "snapshot");
2088
+ for (const target of watchTargets(resolved.inputs, resolved.excluded, outputs, "real")) this.addWatchFile(target);
2089
+ }
2090
+ if (entryPath === void 0 || bundlesEntryInside(config)) return;
2091
+ const watched = /* @__PURE__ */ new Set();
2092
+ const authored = /* @__PURE__ */ new Set([entryPath]);
2093
+ const globs = [];
2094
+ const registerEntryInputs = () => {
2095
+ if (!this.meta.watchMode) return;
2096
+ for (const file of watched) this.addWatchFile(canonicalPath(file));
2097
+ const outputs = outputPaths(allOutputs(config), "snapshot");
2098
+ for (const target of importWatchTargets(authored, outputs)) this.addWatchFile(target);
2099
+ for (const target of globWatchTargets(globs, outputs)) this.addWatchFile(target);
2100
+ };
2101
+ watched.add(entryPath);
2102
+ let bundle;
2103
+ try {
2104
+ bundle = await bundleEntry(config, entryPath, "build", outputFilename, {
2105
+ watchFiles: watched,
2106
+ onLoad: (file) => watched.add(file),
2107
+ onResolve: (spelling) => authored.add(spelling),
2108
+ onGlob: (scope) => globs.push(scope)
2109
+ });
2110
+ } catch (e) {
2111
+ registerEntryInputs();
2112
+ return this.error(fatalError(e));
2113
+ }
2114
+ ownBuilds.set(this.environment, bundle);
2115
+ for (const file of bundle.files) watched.add(file);
2116
+ registerEntryInputs();
450
2117
  },
451
- buildStart() {
452
- if (innerBuild || !building || !this.meta.watchMode || buildConfig === void 0) return;
453
- const outputs = outputPaths(allOutputs(buildConfig));
454
- for (const target of buildWatchTargets(watchedInputs(options), excludedByGlob(options), outputs)) this.addWatchFile(target);
2118
+ async watchChange(id, change) {
2119
+ const changed = fileKey(id);
2120
+ for (const key of [...cache.keys()]) if (fileKey(key) === changed) cache.delete(key);
2121
+ if (change.event !== "update") await releaseWorkerGlobs(this, id);
455
2122
  },
456
- watchChange(id) {
457
- if (innerBuild) return;
458
- const changed = toPosix(resolve(id));
459
- for (const key of [...cache.keys()]) if (toPosix(resolve(key)) === changed) cache.delete(key);
2123
+ transform: {
2124
+ filter: { code: "import.meta.glob" },
2125
+ async handler(code, id) {
2126
+ const config = configOf(this);
2127
+ if (!this.meta.watchMode || !isStoryBuild(config) || !bundlesEntryInside(config)) return;
2128
+ await registerGlobs(this, code, id);
2129
+ }
460
2130
  },
461
- resolveId(id) {
462
- return id === EMPTY_INPUT ? RESOLVED_EMPTY_INPUT : void 0;
2131
+ resolveId: {
2132
+ order: "pre",
2133
+ async handler(id, importer, options) {
2134
+ if (id === EMPTY_INPUT) return RESOLVED_EMPTY_INPUT;
2135
+ const config = configOf(this);
2136
+ if (!this.meta.watchMode || !isStoryBuild(config) || !bundlesEntryInside(config)) return void 0;
2137
+ const kept = await registerImport(this, id, importer);
2138
+ if (kept === void 0) return void 0;
2139
+ const now = canonicalPath(kept);
2140
+ const written = importKey(importer, id);
2141
+ const targets = keptTargets.get(written) ?? /* @__PURE__ */ new Set();
2142
+ keptTargets.set(written, targets.add(now));
2143
+ if (targets.size < 2) return void 0;
2144
+ claimed.delete(written);
2145
+ const resolved = await this.resolve(id, importer, {
2146
+ ...options,
2147
+ skipSelf: true
2148
+ });
2149
+ if (claimed.has(written)) return void 0;
2150
+ if (resolved === null || resolved.external === true || resolved.external === "absolute") return void 0;
2151
+ const file = canonicalPath(fileOfId(resolved.id));
2152
+ if (file === now || !targets.has(file)) return void 0;
2153
+ return {
2154
+ ...resolved,
2155
+ id: `${now}${/[?#].*$/s.exec(resolved.id)?.[0] ?? ""}`
2156
+ };
2157
+ }
463
2158
  },
464
2159
  load(id) {
465
2160
  return id === RESOLVED_EMPTY_INPUT ? "globalThis.tweeTsEmptyInput = true;" : void 0;
@@ -467,150 +2162,59 @@ function tweeTsPlugin(options) {
467
2162
  generateBundle: {
468
2163
  order: "post",
469
2164
  async handler(outputOptions, bundle) {
470
- if (innerBuild) return;
471
- for (const [fileName, item] of Object.entries(bundle)) if (item.type === "chunk" && item.facadeModuleId === RESOLVED_EMPTY_INPUT) delete bundle[fileName];
2165
+ const config = configOf(this);
2166
+ if (!isStoryBuild(config)) return;
2167
+ for (const [fileName, item] of Object.entries(bundle)) if (item.type === "chunk" && item.facadeModuleId === RESOLVED_EMPTY_INPUT) removeFromBundle(bundle, fileName);
2168
+ if (Object.hasOwn(bundle, outputFilename)) return this.error(`twee-ts: the build already writes a file named ${outputFilename} (another input or plugin emits it); set the plugin's outputFilename to another name.`);
2169
+ let entry;
2170
+ if (entryPath !== void 0) {
2171
+ const items = bundle;
2172
+ entry = bundlesEntryInside(config) ? takeEntryFromBundle(items, entryInput) : ownBuilds.get(this.environment);
2173
+ if (!bundlesEntryInside(config)) for (const [fileName, source] of entry?.assets ?? []) {
2174
+ if (fileName === outputFilename || Object.hasOwn(bundle, fileName)) return this.error(entryCollisionMessage(fileName));
2175
+ this.emitFile({
2176
+ type: "asset",
2177
+ fileName,
2178
+ source
2179
+ });
2180
+ }
2181
+ }
472
2182
  record.addLocation(outputOptions);
473
- record.addFiles(outputOptions, Object.keys(bundle));
474
- const outputs = buildConfig === void 0 ? record.outputs() : allOutputs(buildConfig);
475
- const entry = options.entry ? takeEntryFromBundle(bundle) : void 0;
476
- let result;
2183
+ record.addFiles(outputOptions, [...Object.keys(bundle), ...entry?.assets.keys() ?? []]);
2184
+ let story;
477
2185
  try {
478
- result = await compileForOutputFile(buildCompileOptions(options, entry), outputs, cache);
2186
+ const entryFiles = entryPath === void 0 ? [] : [
2187
+ entryPath,
2188
+ ...entry?.files ?? [],
2189
+ ...entryInputs.get(this.environment.getTopLevelConfig())?.() ?? []
2190
+ ].filter((id) => !id.startsWith("\0"));
2191
+ story = await compileStory(resolved.compile(entrySources(entry)), configOutputs(config, record), cache, true, entryFiles);
479
2192
  } catch (e) {
480
2193
  return this.error(fatalError(e));
481
2194
  }
482
- let warnings;
483
- try {
484
- warnings = splitDiagnostics(result);
485
- } catch (e) {
486
- return this.error(e);
487
- }
488
- for (const w of warnings) this.warn(formatDiagnostic(w));
2195
+ for (const warning of story.warnings) this.warn(warning);
489
2196
  this.emitFile({
490
2197
  type: "asset",
491
2198
  fileName: outputFilename,
492
- source: result.output
2199
+ source: story.output
493
2200
  });
494
2201
  }
495
2202
  },
2203
+ writeBundle(outputOptions, bundle) {
2204
+ const story = bundle[outputFilename];
2205
+ const path = emittedFilePath(outputOptions, outputFilename);
2206
+ if (story?.type === "asset" && path !== void 0) recordOwnOutput(path, story.source);
2207
+ },
496
2208
  async configureServer(server) {
497
- if (innerBuild) return;
498
- const base = server.config.base;
499
- const servePaths = outputFilename === "index.html" ? [base, `${base}index.html`] : [`${base}${outputFilename}`];
500
- const inputs = watchedInputs(options);
501
- let outputs = allOutputs(server.config);
502
- let output = outputPaths(outputs);
503
- const refreshOutputs = () => {
504
- outputs = allOutputs(server.config);
505
- output = outputPaths(outputs);
506
- };
507
- const excludedGlob = excludedByGlob(options);
508
- const excluded = (file) => excludedGlob(file) || output.isOutput(file, inputs);
509
- const entryPath = options.entry ? resolve(options.entry) : void 0;
510
- const root = toPosix(server.config.root);
511
- server.watcher.add(inputs);
512
- let html = "";
513
- let lastError;
514
- let entry;
515
- let entryStale = true;
516
- let queue = Promise.resolve();
517
- let pending = /* @__PURE__ */ new Set();
518
- let timer;
519
- let closed = false;
520
- let compiledInputs = /* @__PURE__ */ new Map();
521
- let catchingUp;
522
- const closeWatcher = server.watcher.close.bind(server.watcher);
523
- server.watcher.close = async () => {
524
- closed = true;
525
- clearTimeout(timer);
526
- pending.clear();
527
- return closeWatcher();
528
- };
529
- const touchesEntry = (file) => entryPath !== void 0 && (entryStale ? isInside(file, [root]) : entry?.files.has(file) ?? false);
530
- const rebuild = async (changed, initial = false) => {
531
- if (closed) return;
532
- refreshOutputs();
533
- compiledInputs = inputFiles(inputs, excludedGlob, outputs);
534
- for (const key of [...cache.keys()]) if (changed.has(toPosix(resolve(key)))) cache.delete(key);
535
- try {
536
- if (entryPath && (entryStale || [...changed].some(touchesEntry))) {
537
- entryStale = true;
538
- entry = await bundleEntryForDev(server.config, entryPath);
539
- entryStale = false;
540
- }
541
- const result = await compileForOutputFile(buildCompileOptions(options, entry), outputs, cache);
542
- for (const w of splitDiagnostics(result)) server.config.logger.warn(`[twee-ts] ${formatDiagnostic(w)}`);
543
- html = injectViteClient(result.output, base);
544
- lastError = void 0;
545
- if (!initial) server.ws.send({ type: "full-reload" });
546
- } catch (e) {
547
- lastError = toOverlayError(e);
548
- server.config.logger.error(`[twee-ts] ${lastError.message}`);
549
- server.ws.send({
550
- type: "error",
551
- err: lastError
552
- });
553
- }
554
- };
555
- const keepQueueAlive = (e) => {
556
- try {
557
- server.config.logger.error(`[twee-ts] ${fatalError(e).message}`);
558
- } catch {}
559
- };
560
- await rebuild(/* @__PURE__ */ new Set(), true);
561
- server.watcher.on("all", (event, file) => {
562
- if (event !== "add" && event !== "change" && event !== "unlink") return;
563
- const changed = toPosix(resolve(file));
564
- if (isViteConfigTemp(changed)) return;
565
- if ((!isInside(changed, inputs) || excluded(changed)) && !touchesEntry(changed)) return;
566
- pending.add(changed);
567
- clearTimeout(timer);
568
- timer = setTimeout(() => {
569
- const files = pending;
570
- pending = /* @__PURE__ */ new Set();
571
- queue = queue.then(() => rebuild(files)).catch(keepQueueAlive);
572
- }, 50);
2209
+ const route = await setUpDevStory(server, {
2210
+ options: resolved,
2211
+ cache,
2212
+ outputs: allOutputs,
2213
+ injectClient: injectViteClient
573
2214
  });
574
- const catchUp = () => {
575
- catchingUp ??= (async () => {
576
- await queue;
577
- const changed = filesChanged(compiledInputs, inputFiles(inputs, excludedGlob, outputs));
578
- if (changed.size === 0) return;
579
- for (const file of pending) changed.add(file);
580
- pending = /* @__PURE__ */ new Set();
581
- clearTimeout(timer);
582
- queue = queue.then(() => rebuild(changed)).catch(keepQueueAlive);
583
- await queue;
584
- })().finally(() => {
585
- catchingUp = void 0;
586
- });
587
- return catchingUp;
2215
+ return () => {
2216
+ installStoryRoute(server.middlewares, route);
588
2217
  };
589
- server.ws.on("connection", () => {
590
- if (lastError) server.ws.send({
591
- type: "error",
592
- err: lastError
593
- });
594
- });
595
- server.middlewares.use((req, res, next) => {
596
- const path = (req.url ?? "").split("?")[0] ?? "";
597
- if (servePaths.includes(path)) {
598
- catchUp().then(() => {
599
- res.statusCode = 200;
600
- res.setHeader("Content-Type", "text/html; charset=utf-8");
601
- res.end(html || waitingPage(base));
602
- }, next);
603
- return;
604
- }
605
- const asset = path.startsWith(base) ? entry?.assets.get(path.slice(base.length)) : void 0;
606
- if (asset !== void 0) {
607
- res.statusCode = 200;
608
- res.setHeader("Content-Type", mediaTypeFromFilename(path));
609
- res.end(asset);
610
- return;
611
- }
612
- next();
613
- });
614
2218
  }
615
2219
  };
616
2220
  }