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