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