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