@rohal12/twee-ts 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/bin/twee-ts.js +47 -32
  2. package/dist/bin/twee-ts.js.map +1 -1
  3. package/dist/{diagnostic-text-BRddVulf.cjs → diagnostic-text-Bj-UQ0Jl.cjs} +4283 -3739
  4. package/dist/diagnostic-text-Bj-UQ0Jl.cjs.map +1 -0
  5. package/dist/{diagnostic-text-CebYCTPQ.js → diagnostic-text-DMVZfBh9.js} +4260 -3770
  6. package/dist/diagnostic-text-DMVZfBh9.js.map +1 -0
  7. package/dist/index.cjs +4 -2
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.cts +16 -0
  10. package/dist/index.d.ts +16 -0
  11. package/dist/index.js +3 -3
  12. package/dist/{lint-VayZI_jp.js → lint-BkpfLGuP.js} +3 -3
  13. package/dist/{lint-VayZI_jp.js.map → lint-BkpfLGuP.js.map} +1 -1
  14. package/dist/plugins/rollup.cjs +9 -4
  15. package/dist/plugins/rollup.cjs.map +1 -1
  16. package/dist/plugins/rollup.d.cts +6 -0
  17. package/dist/plugins/rollup.d.ts +6 -0
  18. package/dist/plugins/rollup.js +9 -4
  19. package/dist/plugins/rollup.js.map +1 -1
  20. package/dist/plugins/vite.cjs +453 -154
  21. package/dist/plugins/vite.cjs.map +1 -1
  22. package/dist/plugins/vite.js +453 -154
  23. package/dist/plugins/vite.js.map +1 -1
  24. package/dist/{watch-targets-C0Anuw17.cjs → watch-targets-BrEYmUwU.cjs} +84 -16
  25. package/dist/watch-targets-BrEYmUwU.cjs.map +1 -0
  26. package/dist/{watch-targets-BI7jYrwa.js → watch-targets-DzvPyWDo.js} +79 -17
  27. package/dist/watch-targets-DzvPyWDo.js.map +1 -0
  28. package/package.json +2 -2
  29. package/dist/diagnostic-text-BRddVulf.cjs.map +0 -1
  30. package/dist/diagnostic-text-CebYCTPQ.js.map +0 -1
  31. package/dist/watch-targets-BI7jYrwa.js.map +0 -1
  32. package/dist/watch-targets-C0Anuw17.cjs.map +0 -1
@@ -1,9 +1,11 @@
1
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";
2
+ import { At as lineColumnFinder, Ct as viteWaitingPage, Dt as AcornParser, Et as javaScriptContexts, G as recordOwnOutput, Mt as isRecord, Ot as javaScriptNodes, St as insertViteClient, Tt as cssContexts, U as sha256Hex, at as outputPaths, dt as isSameOrInside, ft as mediaTypeFromFilename, jt as types$1, kt as trySyntax, lt as identify, nt as getFilenames, pt as normalizedFileExt } from "../diagnostic-text-DMVZfBh9.js";
3
+ import { a as checkViteVersion, c as createOutputRecord, d as isViteConfigTemp, f as keyWithin, g as fatalError, h as compileStory, i as watchTargets, l as emittedFilePath, m as toPosix, n as missingImportFolders, o as resolvePluginOptions, p as outputLocations, s as canonicalPath, t as importWatchTargets, u as fileKey } from "../watch-targets-DzvPyWDo.js";
4
+ import { findPackageJSON } from "node:module";
4
5
  import { basename, dirname, isAbsolute, join, posix, relative, resolve, win32 } from "node:path";
5
6
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
6
7
  import { stripVTControlCharacters } from "node:util";
8
+ import { pathToFileURL } from "node:url";
7
9
  import { build, createIdResolver, loadConfigFromFile, mergeConfig, version } from "vite";
8
10
  //#region src/plugins/vite-glob.ts
9
11
  /**
@@ -14,8 +16,8 @@ import { build, createIdResolver, loadConfigFromFile, mergeConfig, version } fro
14
16
  *
15
17
  * A scope is a superset of what the glob matches: a file added to it or removed from it may not match (a negated
16
18
  * pattern, a name the wildcards leave out), which only bundles again for nothing. The calls are read with acorn as Vite reads them: patterns are
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.
19
+ * string literals, template literals without expressions, or arrays of those. Options are static literal data,
20
+ * including computed keys and repeated members; expressions outside that subset are diagnosed, never evaluated.
19
21
  *
20
22
  * Code that is a module is read whole, so only real calls count. Code that is not (yet) one, as a plugin may hand
21
23
  * on a component file or TypeScript it transforms later, is read the way Vite's import-glob reads every module:
@@ -32,18 +34,6 @@ const MODULE_OPTIONS = {
32
34
  function mayHoldGlob(code) {
33
35
  return code.includes("import.meta.glob");
34
36
  }
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
37
  /** Whether `node` is the callee `import.meta.glob`. */
48
38
  function isGlobCallee(node) {
49
39
  return node.type === "MemberExpression" && !node.computed && node.property.type === "Identifier" && node.property.name === "glob" && node.object.type === "MetaProperty" && node.object.meta.name === "import" && node.object.property.name === "meta";
@@ -54,24 +44,62 @@ function stringOf(node) {
54
44
  if (node.type === "Literal") return typeof node.value === "string" ? node.value : void 0;
55
45
  if (node.type === "TemplateLiteral" && node.expressions.length === 0) return node.quasis[0]?.value.raw;
56
46
  }
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;
47
+ /** An expression outside the static data subset: never run it or guess the folder it selects. */
48
+ const UNREADABLE = Symbol("unreadable");
49
+ /** Static option data, with JavaScript's cooked strings, computed keys and last-member precedence. */
50
+ function staticValue(node, depth = 0) {
51
+ if (depth > 100) return UNREADABLE;
52
+ switch (node.type) {
53
+ case "Literal": return node.regex === void 0 && node.bigint === void 0 ? node.value : UNREADABLE;
54
+ case "TemplateLiteral": return node.expressions.length === 0 ? node.quasis[0]?.value.cooked ?? UNREADABLE : UNREADABLE;
55
+ case "UnaryExpression": {
56
+ const value = staticValue(node.argument, depth + 1);
57
+ if (value === UNREADABLE) return UNREADABLE;
58
+ if (node.operator === "!") return !value;
59
+ if (typeof value === "number") {
60
+ if (node.operator === "-") return -value;
61
+ if (node.operator === "+") return value;
62
+ }
63
+ return UNREADABLE;
64
+ }
65
+ case "ArrayExpression": {
66
+ const values = [];
67
+ for (const element of node.elements) {
68
+ if (element === null || element.type === "SpreadElement") return UNREADABLE;
69
+ const value = staticValue(element, depth + 1);
70
+ if (value === UNREADABLE) return UNREADABLE;
71
+ values.push(value);
72
+ }
73
+ return values;
74
+ }
75
+ case "ObjectExpression": {
76
+ const entries = /* @__PURE__ */ new Map();
77
+ for (const property of node.properties) {
78
+ if (property.type === "SpreadElement") return UNREADABLE;
79
+ if (property.kind !== "init" || property.method || property.shorthand) return UNREADABLE;
80
+ if (property.computed && property.key.type !== "Literal") return UNREADABLE;
81
+ const key = !property.computed && property.key.type === "Identifier" ? property.key.name : staticValue(property.key, depth + 1);
82
+ const value = staticValue(property.value, depth + 1);
83
+ if (depth === 0 && key === "exhaustive" && property.value.type !== "Literal") return UNREADABLE;
84
+ if (key !== null && typeof key !== "string" && typeof key !== "number" && typeof key !== "boolean" || value === UNREADABLE || !property.computed && key === "__proto__") return UNREADABLE;
85
+ entries.set(String(key), value);
86
+ }
87
+ return Object.fromEntries(entries);
88
+ }
89
+ default: return UNREADABLE;
63
90
  }
64
91
  }
65
92
  /** The call a glob call's arguments make. */
66
93
  function callOf(args) {
67
94
  const [first, second] = args;
68
95
  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");
96
+ const options = second === void 0 ? {} : second.type === "SpreadElement" ? UNREADABLE : staticValue(second);
97
+ if (options === UNREADABLE) return void 0;
98
+ const base = isRecord(options) ? options["base"] : void 0;
71
99
  return {
72
100
  patterns,
73
101
  base: typeof base === "string" ? base : void 0,
74
- exhaustive: optionOf(options, "exhaustive") === true
102
+ exhaustive: isRecord(options) && options["exhaustive"] === true
75
103
  };
76
104
  }
77
105
  /** Where Vite's import-glob finds a call in a module's text: the callee, any TypeScript type arguments, `(`. */
@@ -81,13 +109,26 @@ const GLOB_CALL_START = /\bimport\.meta\.glob(?:<\w+>)?\s*\(/g;
81
109
  * there: past strings, template literals, regular expressions and comments that hold parentheses. Undefined when the
82
110
  * list does not close, or the text cannot be tokenized.
83
111
  */
84
- function argumentsEnd(code, paren) {
112
+ function argumentsEnd(code, paren, budget) {
113
+ const tokenizer = Object.assign(AcornParser.tokenizer("", MODULE_OPTIONS), {
114
+ input: code,
115
+ pos: paren
116
+ });
85
117
  const read = trySyntax(() => {
86
118
  let depth = 0;
87
- for (const token of AcornParser.tokenizer(code.slice(paren), MODULE_OPTIONS)) {
119
+ for (;;) {
120
+ const from = tokenizer.pos;
121
+ let token;
122
+ try {
123
+ token = tokenizer.getToken();
124
+ } finally {
125
+ const searched = token === void 0 || token.type === types$1.invalidTemplate ? code.length : tokenizer.pos;
126
+ budget.remaining -= Math.max(1, searched - from);
127
+ }
128
+ if (budget.remaining < 0 || token.type === types$1.eof) return void 0;
88
129
  if (token.type === types$1.parenL) depth += 1;
89
130
  else if (token.type === types$1.parenR) depth -= 1;
90
- if (depth === 0) return paren + token.end;
131
+ if (depth === 0) return token.end;
91
132
  }
92
133
  });
93
134
  return read.ok ? read.value : void 0;
@@ -102,9 +143,11 @@ function wholeExpression(text, options) {
102
143
  * of its argument list: as a call expression, or, with type arguments between the two (which JavaScript reads as
103
144
  * comparisons), as its argument list in parentheses. Undefined when acorn reads neither.
104
145
  */
105
- function callAt(code, start, paren) {
106
- const end = argumentsEnd(code, paren);
146
+ function callAt(code, start, paren, budget) {
147
+ const end = argumentsEnd(code, paren, budget);
107
148
  if (end === void 0) return void 0;
149
+ budget.remaining -= end - start + end - paren;
150
+ if (budget.remaining < 0) return void 0;
108
151
  const call = wholeExpression(code.slice(start, end), MODULE_OPTIONS);
109
152
  if (call?.type === "CallExpression" && isGlobCallee(call.callee)) return callOf(call.arguments);
110
153
  const list = wholeExpression(code.slice(paren, end), {
@@ -118,31 +161,46 @@ function callAt(code, start, paren) {
118
161
  /** The glob calls of a module's code: read whole when it is a module, else each call on its own. */
119
162
  function globCalls(code) {
120
163
  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
- };
164
+ if (parsed.ok) {
165
+ const calls = [];
166
+ const unreadable = [];
167
+ for (const node of javaScriptNodes(parsed.value)) {
168
+ if (node.type !== "CallExpression" || !isGlobCallee(node.callee)) continue;
169
+ const call = callOf(node.arguments);
170
+ if (call === void 0) unreadable.push(node.start);
171
+ else calls.push(call);
172
+ }
173
+ return {
174
+ calls,
175
+ unreadable
176
+ };
177
+ }
125
178
  const calls = [];
126
179
  const unreadable = [];
180
+ const budget = { remaining: code.length * 16 };
127
181
  for (const match of code.matchAll(GLOB_CALL_START)) {
128
- const call = callAt(code, match.index, match.index + match[0].length - 1);
182
+ const call = callAt(code, match.index, match.index + match[0].length - 1, budget);
129
183
  if (call === void 0) unreadable.push(match.index);
130
184
  else calls.push(call);
185
+ if (budget.remaining < 0) return {
186
+ calls,
187
+ unreadable,
188
+ recoveryLimited: true
189
+ };
131
190
  }
132
191
  return {
133
192
  calls,
134
193
  unreadable
135
194
  };
136
195
  }
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)}`;
141
- }
142
196
  /** Characters that make a segment of a pattern match more than its own name (picomatch's, and its escape). */
143
197
  const MAGIC = /[*?[\]{}()!+@\\]/;
144
- /** The characters at the end of a segment after its last MAGIC one. */
145
- const LITERAL_END = /[^*?[\]{}()!+@\\]*$/;
198
+ /** The characters after the last MAGIC one, found without retrying each literal prefix as a regex start. */
199
+ function literalEnd(segment) {
200
+ let start = segment.length;
201
+ while (start > 0 && !MAGIC.test(segment.charAt(start - 1))) start -= 1;
202
+ return segment.slice(start).toLowerCase();
203
+ }
146
204
  /** Whether a pattern's segments may match a name starting with a dot, which a wildcard alone never does. */
147
205
  function namesDot(segments) {
148
206
  return segments.some((segment) => segment !== ".." && /(?:^|[{,])\./.test(segment));
@@ -162,7 +220,7 @@ function scopeOf(dir, rest, exhaustive) {
162
220
  deep: below.length > 1 || below.some((segment) => segment.includes("**")),
163
221
  dot: exhaustive || namesDot(below),
164
222
  exhaustive,
165
- suffix: (LITERAL_END.exec(below.at(-1) ?? "")?.[0] ?? "").toLowerCase()
223
+ suffix: literalEnd(below.at(-1) ?? "")
166
224
  };
167
225
  }
168
226
  /**
@@ -285,7 +343,7 @@ function scopeWatchTargets(scopes) {
285
343
  async function resolveGlob(context, pattern, importer) {
286
344
  const isSubImportsPattern = pattern.startsWith("#") && pattern.includes("*");
287
345
  try {
288
- return (await context.resolve(pattern, importer, { custom: { "vite:import-glob": { isSubImportsPattern } } }))?.id.replace(/[?#].*$/s, "");
346
+ return (await context.resolve(pattern, importer, { custom: { "vite:import-glob": { isSubImportsPattern } } }))?.id;
289
347
  } catch {
290
348
  return;
291
349
  }
@@ -297,10 +355,14 @@ async function resolveGlob(context, pattern, importer) {
297
355
  */
298
356
  async function moduleGlobScopes(context, code, id, root) {
299
357
  if (!mayHoldGlob(code)) return [];
300
- const { calls, unreadable } = globCalls(code);
358
+ const { calls, unreadable, recoveryLimited } = globCalls(code);
301
359
  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.`);
360
+ const locate = lineColumnFinder(code);
361
+ const where = unreadable.map((offset) => {
362
+ const { line, column } = locate(offset);
363
+ return `${String(line)}:${String(column)}`;
364
+ }).join(", ");
365
+ context.warn(`twee-ts could not read the import.meta.glob() call at ${where} of ${id}; ` + (recoveryLimited === true ? "recovery work limit reached; remaining calls were not read; " : "") + "files added to its folders are bundled the next time something else changes.");
304
366
  }
305
367
  const file = id.replace(/[?#].*$/s, "");
306
368
  const importer = isAbsolute(file) && !id.startsWith("\0") ? file : void 0;
@@ -422,8 +484,24 @@ function entryInputSettings(inputName, entryPath, userOutput, outputFilename) {
422
484
  };
423
485
  }
424
486
  /** 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*$/, "");
487
+ function dropFileSourceMapComment(code, language) {
488
+ const end = code.trimEnd().length;
489
+ const start = Math.max(language === "js" ? code.lastIndexOf("//# sourceMappingURL=", end) : -1, code.lastIndexOf("/*# sourceMappingURL=", end));
490
+ if (start === -1) return code;
491
+ const candidate = code.slice(start, end);
492
+ if (!/^(?:\/\/# sourceMappingURL=(?!data:)\S*|\/\*# sourceMappingURL=(?!data:)[^*]*\*\/)$/u.test(candidate)) return code;
493
+ const ranges = [{
494
+ start,
495
+ end
496
+ }, {
497
+ start: Math.max(0, start - 1),
498
+ end
499
+ }];
500
+ const [at, before] = language === "js" ? javaScriptContexts(code, ranges) : cssContexts(code, ranges);
501
+ const kind = at?.context.kind;
502
+ if (kind !== "line-comment" && kind !== "block-comment" && kind !== "comment") return code;
503
+ if (start > 0 && before?.context.kind === kind) return code;
504
+ return code.slice(0, code[start - 1] === "\n" ? start - 1 : start);
427
505
  }
428
506
  /** A module id or watched file as a forward-slash file path, without a query or hash (like Vite's cleanUrl). */
429
507
  function fileOfId(id) {
@@ -458,14 +536,14 @@ function takeEntryFromBundle(bundle, inputName) {
458
536
  const styles = [];
459
537
  for (const [fileName, item] of Object.entries(bundle)) if (item.type === "chunk") {
460
538
  if (!item.isEntry || inputName !== void 0 && item.name !== inputName) continue;
461
- script = dropFileSourceMapComment(item.code);
539
+ script = dropFileSourceMapComment(item.code, "js");
462
540
  for (const id of item.moduleIds) if (!id.startsWith("\0")) files.add(fileOfId(id));
463
541
  removeFromBundle(bundle, fileName);
464
542
  } else if (fileName.endsWith(".css") && !isImportedFile(item)) {
465
543
  const css = typeof item.source === "string" ? item.source : new TextDecoder().decode(item.source);
466
- styles.push(dropFileSourceMapComment(css));
544
+ styles.push(dropFileSourceMapComment(css, "css"));
467
545
  removeFromBundle(bundle, fileName);
468
- } else if (fileName.endsWith(".map")) removeFromBundle(bundle, fileName);
546
+ } else if (fileName.endsWith(".map") && (item.originalFileNames?.length ?? 0) === 0) removeFromBundle(bundle, fileName);
469
547
  else assets.set(fileName, item.source);
470
548
  return {
471
549
  script,
@@ -567,31 +645,69 @@ function appliesTo(plugin, command, config, env) {
567
645
  /**
568
646
  * `plugin` as the entry build runs it: without `apply` (already decided by
569
647
  * 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.
648
+ * and `configResolved` hooks, environment selection and hook contexts seeing
649
+ * the dev command. Vite's own build plugins still see build. The plugin object itself is left untouched.
572
650
  */
573
- function asEntryBuildPlugin(plugin, command) {
651
+ function asEntryBuildPlugin(plugin, command, views) {
574
652
  const overrides = /* @__PURE__ */ new Map([["apply", void 0]]);
575
653
  if (command === "serve") {
576
654
  const devEnv = (env) => Object.assign({}, env, {
577
655
  command,
578
656
  isSsrBuild: false
579
657
  });
580
- const wrap = (name, patch) => {
581
- const hook = plugin[name];
658
+ const devConfig = (config) => {
659
+ if (!isRecord(config)) return config;
660
+ let view = views.configs.get(config);
661
+ if (view === void 0) {
662
+ view = withOverrides(config, /* @__PURE__ */ new Map([["command", command]]));
663
+ views.configs.set(config, view);
664
+ }
665
+ return view;
666
+ };
667
+ const devEnvironment = (environment) => {
668
+ if (!isRecord(environment)) return environment;
669
+ const known = views.environments.get(environment);
670
+ if (known !== void 0) return known;
671
+ const overrides = /* @__PURE__ */ new Map([["config", devConfig(environment["config"])]]);
672
+ const topLevel = environment["getTopLevelConfig"];
673
+ if (typeof topLevel === "function") overrides.set("getTopLevelConfig", () => devConfig(Reflect.apply(topLevel, environment, [])));
674
+ const view = withOverrides(environment, overrides);
675
+ views.environments.set(environment, view);
676
+ return view;
677
+ };
678
+ const devContext = (context) => {
679
+ if (!isRecord(context) || context["environment"] === void 0) return context;
680
+ const environment = devEnvironment(context["environment"]);
681
+ return new Proxy(context, { get(target, key) {
682
+ if (key === "environment") return environment;
683
+ const value = Reflect.get(target, key, target);
684
+ return typeof value === "function" ? value.bind(target) : value;
685
+ } });
686
+ };
687
+ const wrap = (name, patch = (args) => args) => {
688
+ const hook = Reflect.get(plugin, name);
582
689
  const handler = hookHandler(hook);
583
690
  if (typeof handler !== "function") return;
584
691
  overrides.set(name, withHandler(hook, function(...args) {
585
- return Reflect.apply(handler, this, patch(args));
692
+ return Reflect.apply(handler, devContext(this), patch(args));
586
693
  }));
587
694
  };
695
+ const keys = /* @__PURE__ */ new Set();
696
+ for (let object = plugin; object !== null && object !== Object.prototype; object = Reflect.getPrototypeOf(object)) for (const key of Reflect.ownKeys(object)) keys.add(key);
697
+ for (const key of keys) if (key !== "constructor" && key !== "apply" && key !== "applyToEnvironment") wrap(key);
588
698
  wrap("config", ([config, env]) => [config, devEnv(env)]);
589
699
  wrap("configEnvironment", ([name, config, env]) => [
590
700
  name,
591
701
  config,
592
702
  devEnv(env)
593
703
  ]);
594
- wrap("configResolved", ([config]) => [isRecord(config) ? withOverrides(config, /* @__PURE__ */ new Map([["command", command]])) : config]);
704
+ wrap("configResolved", ([config]) => [devConfig(config)]);
705
+ const select = plugin.applyToEnvironment;
706
+ if (select !== void 0) overrides.set("applyToEnvironment", async function(environment) {
707
+ const result = await Reflect.apply(select, this, [devEnvironment(environment)]);
708
+ if (result === true || !result) return result;
709
+ return (await flattenPlugins(result)).filter(isPlugin).map((selected) => asEntryBuildPlugin(selected, command, views));
710
+ });
595
711
  }
596
712
  return pluginWith(plugin, overrides);
597
713
  }
@@ -696,7 +812,7 @@ function entryBuildEnforcer(entryPath, command, outputFilename) {
696
812
  }
697
813
  };
698
814
  }
699
- /** Build-phase plugin hooks that may call `this.addWatchFile`. */
815
+ /** Plugin hooks that may read an entry input and register it with `this.addWatchFile` before the story is emitted. */
700
816
  const WATCH_FILE_HOOKS = [
701
817
  "buildStart",
702
818
  "resolveId",
@@ -704,7 +820,11 @@ const WATCH_FILE_HOOKS = [
704
820
  "load",
705
821
  "transform",
706
822
  "moduleParsed",
707
- "buildEnd"
823
+ "buildEnd",
824
+ "renderStart",
825
+ "renderChunk",
826
+ "augmentChunkHash",
827
+ "generateBundle"
708
828
  ];
709
829
  /** A plugin context whose `addWatchFile` also passes the file to `record`. */
710
830
  function recordingContext(context, record) {
@@ -713,7 +833,7 @@ function recordingContext(context, record) {
713
833
  if (typeof value !== "function") return value;
714
834
  if (key !== "addWatchFile") return value.bind(target);
715
835
  return (id) => {
716
- if (typeof id === "string" && !id.startsWith("\0")) record(fileOfId(resolve(id)));
836
+ if (typeof id === "string" && !id.startsWith("\0")) record(resolve(id));
717
837
  return Reflect.apply(value, target, [id]);
718
838
  };
719
839
  } });
@@ -724,13 +844,14 @@ function recordingContext(context, record) {
724
844
  * never collects wrappers. A plugin without such hooks (a built-in one, for
725
845
  * instance) is returned as it is.
726
846
  */
727
- function recordingPlugin(plugin, record) {
847
+ function recordingPlugin(plugin, recordFor) {
728
848
  const overrides = /* @__PURE__ */ new Map();
729
849
  for (const name of WATCH_FILE_HOOKS) {
730
850
  const hook = plugin[name];
731
851
  const handler = hookHandler(hook);
732
852
  if (typeof handler !== "function") continue;
733
853
  overrides.set(name, withHandler(hook, function(...args) {
854
+ const record = recordFor(name, args);
734
855
  const context = typeof this === "object" && this !== null ? recordingContext(this, record) : this;
735
856
  return Reflect.apply(handler, context, args);
736
857
  }));
@@ -784,6 +905,45 @@ function writtenPaths(source, importer, root) {
784
905
  return from !== void 0 && isAbsolute(from) && /^\.\.?(?:[/\\]|$)/.test(source) ? [fileOfId(resolve(dirname(from), source))] : [];
785
906
  }
786
907
  /**
908
+ * Package locations Node's upward node_modules search can use for an unresolved bare import. These are recovery
909
+ * inputs only: Vite still decides aliases, exports and the actual module. Watching the package itself sees a
910
+ * restored manifest, export target or nested subpath without watching every installed package recursively.
911
+ */
912
+ function missingPackagePaths(source, importer) {
913
+ if (importer === void 0 || !isAbsolute(fileOfId(importer)) || source === "" || /^[./\\#\0]/.test(source) || source.includes(":")) return [];
914
+ const name = source.split("/").slice(0, source.startsWith("@") ? 2 : 1).join("/");
915
+ const paths = [];
916
+ for (let folder = dirname(fileOfId(importer));; folder = dirname(folder)) {
917
+ if (toPosix(folder).split("/").at(-1) !== "node_modules") paths.push(toPosix(join(folder, "node_modules", name)));
918
+ if (dirname(folder) === folder) break;
919
+ }
920
+ return paths;
921
+ }
922
+ /**
923
+ * A package can import itself through its exports without living in node_modules. Node can locate its manifest
924
+ * even when the export target is missing. Track the package's own file listing as a recovery scope, excluding
925
+ * node_modules; recording the whole folder as a watch file would scan all of its installed dependencies too.
926
+ * The node_modules candidates above remain necessary for packages that have no manifest yet.
927
+ */
928
+ function missingPackageScope(source, importer, candidates) {
929
+ if (importer === void 0 || candidates.length === 0) return void 0;
930
+ let manifest;
931
+ try {
932
+ manifest = findPackageJSON(source, pathToFileURL(fileOfId(importer)));
933
+ } catch {
934
+ return;
935
+ }
936
+ if (manifest === void 0) return void 0;
937
+ const dir = toPosix(dirname(manifest));
938
+ return candidates.includes(dir) ? void 0 : {
939
+ dir,
940
+ deep: true,
941
+ dot: true,
942
+ exhaustive: false,
943
+ suffix: ""
944
+ };
945
+ }
946
+ /**
787
947
  * Every path an import of `source` by `importer` is spelled as (see writtenPaths), and the path Vite's resolver
788
948
  * reaches the file by, links kept (`kept`, see keptPath). A caller then sees a link on the way that is retargeted,
789
949
  * which the file's own path never shows. Nothing for an import from a virtual module, or of one.
@@ -809,55 +969,100 @@ async function importSpellings(context, source, importer, root) {
809
969
  * which sees the final plugin list, including the plugins Vite resolves per
810
970
  * environment (`applyToEnvironment`) after `configResolved`.
811
971
  */
812
- function recordWatchFiles(files, root, observer) {
972
+ function recordWatchFiles(files, root, observer = {}) {
813
973
  const { onLoad, onResolve, onGlob, onWatchFile } = observer;
974
+ const watches = /* @__PURE__ */ new Map();
975
+ const touched = /* @__PURE__ */ new Set();
976
+ let modules = /* @__PURE__ */ new Set();
977
+ let started = false;
814
978
  const record = (file) => {
815
- files.add(file);
979
+ files?.add(file);
816
980
  onWatchFile?.(file);
817
981
  };
982
+ const recordFor = (hook, args) => {
983
+ if (hook === "buildStart" && !started) {
984
+ started = true;
985
+ touched.clear();
986
+ watches.delete(void 0);
987
+ }
988
+ const owner = hook === "transform" || hook === "resolveId" || hook === "resolveDynamicImport" ? args[1] : hook === "load" ? args[0] : hook === "moduleParsed" && isRecord(args[0]) ? args[0]["id"] : void 0;
989
+ const id = typeof owner === "string" ? owner : void 0;
990
+ if (id !== void 0 && (hook === "load" || hook === "transform") && !touched.has(id)) {
991
+ touched.add(id);
992
+ watches.delete(id);
993
+ }
994
+ return (file) => {
995
+ record(file);
996
+ const owned = watches.get(id) ?? /* @__PURE__ */ new Set();
997
+ watches.set(id, owned.add(file));
998
+ };
999
+ };
818
1000
  return {
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);
1001
+ plugin: {
1002
+ name: `${PLUGIN_NAME}:record-watch-files`,
1003
+ buildStart() {
1004
+ recordFor("buildStart", []);
1005
+ },
1006
+ load: {
1007
+ order: "pre",
1008
+ handler(id) {
1009
+ const file = fileOfId(id);
1010
+ if (!isAbsolute(file) || id.startsWith("\0")) return null;
1011
+ onLoad?.(file);
1012
+ onResolve?.(file);
1013
+ return null;
1014
+ }
1015
+ },
1016
+ resolveId: {
1017
+ order: "pre",
1018
+ async handler(source, importer) {
1019
+ if (onResolve === void 0) return null;
1020
+ const { spellings, kept } = await importSpellings(this, source, importer, root);
1021
+ for (const spelled of spellings) onResolve(spelled);
1022
+ if (kept === void 0 && spellings.length === 0) {
1023
+ const candidates = missingPackagePaths(source, importer);
1024
+ for (const path of candidates) record(path);
1025
+ const scope = missingPackageScope(source, importer, candidates);
1026
+ if (scope !== void 0) {
1027
+ record(join(scope.dir, "package.json"));
1028
+ onGlob?.(scope);
1029
+ }
1030
+ }
1031
+ return null;
1032
+ }
1033
+ },
1034
+ transform: {
1035
+ filter: { code: "import.meta.glob" },
1036
+ async handler(code, id) {
1037
+ if (onGlob === void 0) return null;
1038
+ for (const scope of await moduleGlobScopes(this, code, id, root)) onGlob(scope);
1039
+ return null;
1040
+ }
1041
+ },
1042
+ buildEnd() {
1043
+ modules = new Set(this.getModuleIds());
1044
+ started = false;
1045
+ for (const owner of watches.keys()) if (owner !== void 0 && !modules.has(owner)) watches.delete(owner);
1046
+ for (const id of modules) {
1047
+ const file = fileOfId(id);
1048
+ if (isAbsolute(file)) files?.add(file);
1049
+ }
1050
+ },
1051
+ options: {
1052
+ order: "post",
1053
+ async handler(inputOptions) {
1054
+ const plugins = (await flattenPlugins(inputOptions.plugins)).filter(isPlugin).map((p) => recordingPlugin(p, recordFor));
1055
+ return {
1056
+ ...inputOptions,
1057
+ plugins
1058
+ };
1059
+ }
850
1060
  }
851
1061
  },
852
- options: {
853
- order: "post",
854
- async handler(inputOptions) {
855
- const plugins = (await flattenPlugins(inputOptions.plugins)).filter(isPlugin).map((p) => recordingPlugin(p, record));
856
- return {
857
- ...inputOptions,
858
- plugins
859
- };
860
- }
1062
+ inputs: () => {
1063
+ const current = new Set([...modules].filter((id) => !id.startsWith("\0")).map(fileOfId).filter(isAbsolute));
1064
+ for (const [owner, watched] of watches) if (owner === void 0 || modules.has(owner)) for (const file of watched) current.add(file);
1065
+ return current;
861
1066
  }
862
1067
  };
863
1068
  }
@@ -902,7 +1107,13 @@ async function bundleEntry(config, entryPath, command, outputFilename, observer
902
1107
  isPreview: false
903
1108
  };
904
1109
  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));
1110
+ const views = {
1111
+ configs: /* @__PURE__ */ new WeakMap(),
1112
+ environments: /* @__PURE__ */ new WeakMap()
1113
+ };
1114
+ const entryPlugins = async (option) => (await flattenPlugins(option)).filter(isPlugin).filter((plugin) => plugin.name !== "twee-ts" && appliesTo(plugin, command, user, env)).map((plugin) => asEntryBuildPlugin(plugin, command, views));
1115
+ const plugins = await entryPlugins(user.plugins);
1116
+ const workerPlugins = user.worker?.plugins;
906
1117
  const inline = {
907
1118
  ...withoutExcludedKeys(user),
908
1119
  configFile: false,
@@ -915,9 +1126,12 @@ async function bundleEntry(config, entryPath, command, outputFilename, observer
915
1126
  plugins: [
916
1127
  ...plugins,
917
1128
  entryBuildEnforcer(entryPath, command, outputFilename),
918
- recordWatchFiles(watchFiles, config.root, observer)
1129
+ recordWatchFiles(watchFiles, config.root, observer).plugin
919
1130
  ],
920
- worker: withWorkerPlugin(user.worker, () => recordWatchFiles(watchFiles, config.root, observer))
1131
+ worker: withWorkerPlugin({
1132
+ ...user.worker,
1133
+ plugins: () => [entryPlugins(typeof workerPlugins === "function" ? workerPlugins() : workerPlugins)]
1134
+ }, () => recordWatchFiles(watchFiles, config.root, observer).plugin)
921
1135
  };
922
1136
  const [result] = [await build(inline)].flat();
923
1137
  if (result === void 0 || !("output" in result)) throw new Error("twee-ts: the entry build returned no bundle.");
@@ -1058,9 +1272,9 @@ function fileState(file) {
1058
1272
  * `folder` is not a folder.
1059
1273
  */
1060
1274
  function folderState(folder) {
1061
- if (statSync(folder, { throwIfNoEntry: false })?.isDirectory() !== true) return void 0;
1062
1275
  let names;
1063
1276
  try {
1277
+ if (statSync(folder, { throwIfNoEntry: false })?.isDirectory() !== true) return void 0;
1064
1278
  names = readdirSync(folder, {
1065
1279
  recursive: true,
1066
1280
  encoding: "utf8"
@@ -1087,6 +1301,15 @@ function fileRead(file) {
1087
1301
  function tracked(paths) {
1088
1302
  return new Map([...paths].map((path) => [fileKey(path), canonicalPath(path)]));
1089
1303
  }
1304
+ /** A native identity key and every parent above it, without identifying any path again. */
1305
+ function* keyParents(key) {
1306
+ for (let current = key;;) {
1307
+ yield current;
1308
+ const parent = dirname(current);
1309
+ if (parent === current) return;
1310
+ current = parent;
1311
+ }
1312
+ }
1090
1313
  /**
1091
1314
  * The key of the file `authored` reaches, when it reaches it through a link (somewhere along the path, or
1092
1315
  * at the end of it): the one case where the file's own path says nothing of a change of target.
@@ -1155,8 +1378,8 @@ function installStoryRoute(middlewares, route) {
1155
1378
  handle: route
1156
1379
  });
1157
1380
  }
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();
1381
+ /** The instances and entry watches of each server; the watcher is shared by every story (#517). */
1382
+ const devServers = /* @__PURE__ */ new WeakMap();
1160
1383
  /** The error for a story page whose URL another story or an entry asset of the same dev server also claims. */
1161
1384
  function storyCollisionMessage(path) {
1162
1385
  return `twee-ts: more than one generated file is served at ${path || "/"} (another story or an entry asset); set a distinct outputFilename.`;
@@ -1188,11 +1411,20 @@ async function setUpDevStory(server, dev) {
1188
1411
  let lastError;
1189
1412
  let entry;
1190
1413
  let servedAssets;
1191
- const instances = devInstances.get(server) ?? /* @__PURE__ */ new Set();
1192
- devInstances.set(server, instances);
1414
+ const state = devServers.get(server) ?? {
1415
+ instances: /* @__PURE__ */ new Set(),
1416
+ watched: /* @__PURE__ */ new Map(),
1417
+ released: /* @__PURE__ */ new Map()
1418
+ };
1419
+ devServers.set(server, state);
1420
+ const { instances, watched, released } = state;
1421
+ const watchedForEntry = /* @__PURE__ */ new Map();
1193
1422
  const self = {
1194
1423
  paths,
1195
- assets: () => servedAssets
1424
+ assets: () => servedAssets,
1425
+ catchUp: () => catchUp(),
1426
+ inputKeys,
1427
+ entryWatches: watchedForEntry
1196
1428
  };
1197
1429
  instances.add(self);
1198
1430
  const fail = (res, message) => {
@@ -1211,12 +1443,12 @@ async function setUpDevStory(server, dev) {
1211
1443
  let entryFolders = /* @__PURE__ */ new Map();
1212
1444
  let entryReads = /* @__PURE__ */ new Map();
1213
1445
  const readsModules = config.server.watch !== null;
1214
- const watchedForEntry = /* @__PURE__ */ new Map();
1215
1446
  let queue = Promise.resolve();
1216
1447
  let pending = /* @__PURE__ */ new Set();
1217
1448
  let timer;
1218
1449
  let closed = false;
1219
1450
  let compiledInputs = /* @__PURE__ */ new Map();
1451
+ let compiledInputNames;
1220
1452
  let catchingUp;
1221
1453
  const closeWatcher = server.watcher.close.bind(server.watcher);
1222
1454
  server.watcher.close = async () => {
@@ -1226,8 +1458,13 @@ async function setUpDevStory(server, dev) {
1226
1458
  return closeWatcher();
1227
1459
  };
1228
1460
  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";
1461
+ let kind;
1462
+ try {
1463
+ const stat = statSync(input, { throwIfNoEntry: false });
1464
+ kind = stat === void 0 ? "none" : stat.isFile() ? "file" : stat.isDirectory() ? "folder" : "other";
1465
+ } catch (error) {
1466
+ kind = `failed:${String(error)}`;
1467
+ }
1231
1468
  return `${fileKey(input)}\0${kind}`;
1232
1469
  }).join("\0");
1233
1470
  let inputWalk;
@@ -1236,7 +1473,7 @@ async function setUpDevStory(server, dev) {
1236
1473
  const folders = /* @__PURE__ */ new Map();
1237
1474
  const files = getFilenames(inputs, outputs, [], "source", (folder) => folders.set(folder, fileState(folder))).files.filter((file) => !excludedGlob(file.path)).map((file) => ({
1238
1475
  key: file.key,
1239
- path: resolve(file.path),
1476
+ path: identify(file.path).absolute,
1240
1477
  link: file.link === true
1241
1478
  }));
1242
1479
  return {
@@ -1270,14 +1507,28 @@ async function setUpDevStory(server, dev) {
1270
1507
  const entryWatchTargets = () => new Map([...entryFiles, ...tracked(scopeWatchTargets(entryGlobs.values()))]);
1271
1508
  const watchEntryFiles = (files) => {
1272
1509
  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);
1510
+ for (const key of watchedForEntry.keys()) if (!files.has(key)) watchedForEntry.delete(key);
1511
+ for (const [key, path] of added) {
1512
+ watchedForEntry.set(key, path);
1513
+ watched.set(key, path);
1514
+ }
1515
+ const owners = /* @__PURE__ */ new Set([root, ...[...instances].flatMap((instance) => [...instance.inputKeys(), ...instance.entryWatches.keys()])]);
1516
+ const aboveOwners = new Set([...owners].flatMap((key) => [...keyParents(key)]));
1517
+ const needed = (key) => aboveOwners.has(key) || [...keyParents(key)].some((parent) => owners.has(parent));
1518
+ const dropped = [...watched].filter(([key]) => !needed(key));
1519
+ for (const [key, path] of dropped) {
1520
+ watched.delete(key);
1521
+ released.set(key, path);
1522
+ }
1523
+ const restored = [...released].filter(([key]) => needed(key));
1524
+ for (const [key, path] of restored) {
1525
+ released.delete(key);
1526
+ watched.set(key, path);
1280
1527
  }
1528
+ if (dropped.length > 0) server.watcher.unwatch(dropped.map(([, path]) => path));
1529
+ const registrations = new Map([...restored, ...added]);
1530
+ if (registrations.size > 0) server.watcher.add([...registrations.values()]);
1531
+ if (added.length > 0) recheckOnceWatched(new Map(added)).catch(keepQueueAlive);
1281
1532
  };
1282
1533
  const isListed = (path) => {
1283
1534
  return server.watcher.getWatched()[dirname(path)]?.includes(basename(path)) ?? false;
@@ -1382,7 +1633,6 @@ async function setUpDevStory(server, dev) {
1382
1633
  globStates = globsRead;
1383
1634
  entryFolders = folders;
1384
1635
  entryReads = reads;
1385
- watchEntryFiles(entryWatchTargets());
1386
1636
  };
1387
1637
  const alreadyRead = (key) => {
1388
1638
  const read = entryReads.get(key);
@@ -1402,8 +1652,10 @@ async function setUpDevStory(server, dev) {
1402
1652
  outputs = dev.outputs(config);
1403
1653
  output = outputPaths(outputs);
1404
1654
  compiledInputs = storyInputFiles(true);
1655
+ compiledInputNames = inputWalk?.named;
1405
1656
  for (const key of [...cache.keys()]) if (changed.has(fileKey(key))) cache.delete(key);
1406
1657
  if (entryStale || regrouped || unread.some(touchesEntry)) await bundle();
1658
+ watchEntryFiles(entryWatchTargets());
1407
1659
  const story = await compileStory(options.compile(entrySources(entry)), outputs, cache);
1408
1660
  for (const warning of story.warnings) config.logger.warn(`[twee-ts] ${warning}`);
1409
1661
  html = outputMode === "html" ? dev.injectClient(story.output, base) : story.output;
@@ -1451,6 +1703,7 @@ async function setUpDevStory(server, dev) {
1451
1703
  if (queue === waited) break;
1452
1704
  }
1453
1705
  const changed = filesChanged(compiledInputs, storyInputFiles(false));
1706
+ if (compiledInputNames !== inputWalk?.named) for (const input of inputs) changed.add(fileKey(input));
1454
1707
  for (const file of filesChanged(entryStates, fileStates(entryFiles))) changed.add(file);
1455
1708
  if (changed.size === 0 && pending.size === 0 && !linkMoved() && !globsChanged() && !foldersChanged()) return;
1456
1709
  for (const file of pending) changed.add(file);
@@ -1493,33 +1746,63 @@ async function setUpDevStory(server, dev) {
1493
1746
  next();
1494
1747
  return;
1495
1748
  }
1496
- const others = [...instances].filter((other) => other !== self);
1497
1749
  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));
1750
+ if (!mine && servedAssets?.has(path) !== true) {
1751
+ next();
1500
1752
  return;
1501
1753
  }
1502
- if (paths.includes(path)) {
1503
- catchUp().then(() => {
1754
+ ([...instances].some((instance) => instance.paths.includes(path)) ? Promise.all([...instances].map((instance) => instance.catchUp())) : Promise.resolve()).then(() => {
1755
+ const others = [...instances].filter((other) => other !== self);
1756
+ if ((mine || servedAssets?.has(path) === true) && others.some((other) => other.paths.includes(path) || mine && paths.some((storyPath) => other.assets()?.has(storyPath) === true))) {
1757
+ fail(res, storyCollisionMessage(path));
1758
+ return;
1759
+ }
1760
+ if (mine) {
1761
+ if (paths.some((storyPath) => servedAssets?.has(storyPath) === true)) {
1762
+ fail(res, storyCollisionMessage(path));
1763
+ return;
1764
+ }
1504
1765
  if (html) send(req, res, OUTPUT_MEDIA_TYPES[outputMode], html);
1505
1766
  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
1767
  return;
1519
1768
  }
1769
+ const asset = servedAssets?.get(path);
1770
+ if (asset === void 0) {
1771
+ next();
1772
+ return;
1773
+ }
1774
+ for (const other of others) {
1775
+ const theirs = other.assets()?.get(path);
1776
+ if (theirs !== void 0 && !sameBytes(theirs, asset)) {
1777
+ fail(res, entryCollisionMessage(path));
1778
+ return;
1779
+ }
1780
+ }
1781
+ send(req, res, assetMediaType(path), asset);
1782
+ }, next);
1783
+ };
1784
+ }
1785
+ //#endregion
1786
+ //#region src/plugins/vite-public.ts
1787
+ /** Inventory public copies by their destination names, following directory links as Vite's copyDir does. */
1788
+ function publicFiles(publicDir) {
1789
+ if (!existsSync(publicDir)) return [];
1790
+ const files = [];
1791
+ const ancestors = /* @__PURE__ */ new Set();
1792
+ const walk = (folder) => {
1793
+ const key = fileKey(folder);
1794
+ if (ancestors.has(key)) return;
1795
+ ancestors.add(key);
1796
+ for (const name of readdirSync(folder)) {
1797
+ const path = resolve(folder, name);
1798
+ if (!existsSync(path)) continue;
1799
+ if (statSync(path).isDirectory()) walk(path);
1800
+ else files.push(relative(publicDir, path));
1520
1801
  }
1521
- send(req, res, assetMediaType(path), asset);
1802
+ ancestors.delete(key);
1522
1803
  };
1804
+ walk(publicDir);
1805
+ return files;
1523
1806
  }
1524
1807
  //#endregion
1525
1808
  //#region src/plugins/vite.ts
@@ -1588,14 +1871,14 @@ function configOutputLocations(config) {
1588
1871
  * Every path a build with this config writes, as far as the config tells: each
1589
1872
  * output's folder (see configOutputLocations), the story in it, and the copies
1590
1873
  * of the public files Vite puts there. The plugin adds the files each bundle
1591
- * writes as it builds (see createOutputRecord).
1874
+ * writes as it builds (see createOutputRecord). A build passes its persistent
1875
+ * record so public copies remain excluded after a file is removed or a link is retargeted.
1592
1876
  */
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))) : [];
1877
+ function configOutputs(config, record) {
1878
+ const copiedFiles = config.build.copyPublicDir && config.publicDir ? publicFiles(config.publicDir) : [];
1596
1879
  for (const location of configOutputLocations(config)) {
1597
1880
  record.addLocation(location);
1598
- record.addFiles(location, publicFiles);
1881
+ record.addFiles(location, copiedFiles);
1599
1882
  }
1600
1883
  return record.outputs();
1601
1884
  }
@@ -1659,8 +1942,9 @@ function tweeTsPlugin(options) {
1659
1942
  const entryInput = `${ENTRY_INPUT_NAME}-${instances}`;
1660
1943
  const cache = /* @__PURE__ */ new Map();
1661
1944
  const ownBuilds = /* @__PURE__ */ new WeakMap();
1945
+ const entryInputs = /* @__PURE__ */ new WeakMap();
1662
1946
  const record = createOutputRecord(outputFilename);
1663
- const allOutputs = (config) => mergeOutputs(configOutputs(config, outputFilename), record.outputs());
1947
+ const allOutputs = (config) => mergeOutputs(configOutputs(config, createOutputRecord(outputFilename)), record.outputs());
1664
1948
  /**
1665
1949
  * The build settings this instance adds to a client build: none when the
1666
1950
  * user's config names an input (the entry is then bundled by a build of its
@@ -1784,6 +2068,11 @@ function tweeTsPlugin(options) {
1784
2068
  },
1785
2069
  configResolved(config) {
1786
2070
  if (entryPath === void 0) return;
2071
+ if (config.command === "build" && (bundlesEntryInside(config) || bundlesEntryInside({ build: config.environments[CLIENT]?.build }))) {
2072
+ const recorder = recordWatchFiles(void 0, config.root);
2073
+ entryInputs.set(config, recorder.inputs);
2074
+ Reflect.set(config.plugins, config.plugins.length, pluginWith(recorder.plugin, /* @__PURE__ */ new Map([["applyToEnvironment", (environment) => environment.name === CLIENT]])));
2075
+ }
1787
2076
  if (config.command === "build" && config.build.watch && bundlesEntryInside(config)) config.plugins.forEach((plugin, at) => {
1788
2077
  if (!isOwnPlugin(plugin)) Reflect.set(config.plugins, at, claimingPlugin(plugin, (key) => claimed.add(key)));
1789
2078
  });
@@ -1795,7 +2084,7 @@ function tweeTsPlugin(options) {
1795
2084
  watchOutputs = /* @__PURE__ */ new WeakMap();
1796
2085
  forgetKeptResolver(config);
1797
2086
  if (this.meta.watchMode) {
1798
- const outputs = outputPaths(allOutputs(config));
2087
+ const outputs = outputPaths(allOutputs(config), "snapshot");
1799
2088
  for (const target of watchTargets(resolved.inputs, resolved.excluded, outputs, "real")) this.addWatchFile(target);
1800
2089
  }
1801
2090
  if (entryPath === void 0 || bundlesEntryInside(config)) return;
@@ -1805,7 +2094,7 @@ function tweeTsPlugin(options) {
1805
2094
  const registerEntryInputs = () => {
1806
2095
  if (!this.meta.watchMode) return;
1807
2096
  for (const file of watched) this.addWatchFile(canonicalPath(file));
1808
- const outputs = outputPaths(allOutputs(config));
2097
+ const outputs = outputPaths(allOutputs(config), "snapshot");
1809
2098
  for (const target of importWatchTargets(authored, outputs)) this.addWatchFile(target);
1810
2099
  for (const target of globWatchTargets(globs, outputs)) this.addWatchFile(target);
1811
2100
  };
@@ -1882,7 +2171,7 @@ function tweeTsPlugin(options) {
1882
2171
  const items = bundle;
1883
2172
  entry = bundlesEntryInside(config) ? takeEntryFromBundle(items, entryInput) : ownBuilds.get(this.environment);
1884
2173
  if (!bundlesEntryInside(config)) for (const [fileName, source] of entry?.assets ?? []) {
1885
- if (Object.hasOwn(bundle, fileName)) return this.error(entryCollisionMessage(fileName));
2174
+ if (fileName === outputFilename || Object.hasOwn(bundle, fileName)) return this.error(entryCollisionMessage(fileName));
1886
2175
  this.emitFile({
1887
2176
  type: "asset",
1888
2177
  fileName,
@@ -1894,7 +2183,12 @@ function tweeTsPlugin(options) {
1894
2183
  record.addFiles(outputOptions, [...Object.keys(bundle), ...entry?.assets.keys() ?? []]);
1895
2184
  let story;
1896
2185
  try {
1897
- story = await compileStory(resolved.compile(entrySources(entry)), allOutputs(config), cache, true);
2186
+ const entryFiles = entryPath === void 0 ? [] : [
2187
+ entryPath,
2188
+ ...entry?.files ?? [],
2189
+ ...entryInputs.get(this.environment.getTopLevelConfig())?.() ?? []
2190
+ ].filter((id) => !id.startsWith("\0"));
2191
+ story = await compileStory(resolved.compile(entrySources(entry)), configOutputs(config, record), cache, true, entryFiles);
1898
2192
  } catch (e) {
1899
2193
  return this.error(fatalError(e));
1900
2194
  }
@@ -1906,6 +2200,11 @@ function tweeTsPlugin(options) {
1906
2200
  });
1907
2201
  }
1908
2202
  },
2203
+ writeBundle(outputOptions, bundle) {
2204
+ const story = bundle[outputFilename];
2205
+ const path = emittedFilePath(outputOptions, outputFilename);
2206
+ if (story?.type === "asset" && path !== void 0) recordOwnOutput(path, story.source);
2207
+ },
1909
2208
  async configureServer(server) {
1910
2209
  const route = await setUpDevStory(server, {
1911
2210
  options: resolved,