@ttsc/unplugin 0.20.0 → 0.20.1

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.
package/lib/core/index.js CHANGED
@@ -2,9 +2,12 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
+ var fs = require('node:fs');
6
+ var path = require('node:path');
5
7
  var unplugin$1 = require('unplugin');
6
8
  var options = require('./options.js');
7
9
  var transform = require('./transform.js');
10
+ var viteServe = require('./viteServe.js');
8
11
 
9
12
  const name = "ttsc-unplugin";
10
13
  /**
@@ -33,13 +36,34 @@ const virtualModulePattern = /\0/;
33
36
  const unpluginFactory = (rawOptions = {}) => {
34
37
  const options$1 = options.resolveOptions(rawOptions);
35
38
  const transformCache = transform.createTtscTransformCache();
39
+ const missingInputs = viteServe.createViteServeMissingInputWatch();
36
40
  let aliases;
41
+ let viteCommand;
37
42
  return {
38
43
  name,
39
44
  enforce: "pre",
40
45
  vite: {
41
46
  configResolved(config) {
42
47
  aliases = config.resolve.alias;
48
+ // Re-read per config resolution: a plugin instance reused across a
49
+ // serve and a later build must stop routing missing inputs to the
50
+ // serve-time poll, even though the closed server stays attached
51
+ // (see the dispose note in viteServe.ts).
52
+ viteCommand = config.command;
53
+ },
54
+ // Vite serve funnels every transform-context `addWatchFile()` into the
55
+ // module's added-import graph (`_addedImports`), which import-analysis
56
+ // resolves like real imports. Capture the dev server so the transform
57
+ // hook can route watch inputs that do not exist yet — superseding
58
+ // resolution candidates above all — around that graph and still
59
+ // invalidate their importers when the path is created.
60
+ configureServer(server) {
61
+ missingInputs.attach(server);
62
+ },
63
+ // Vite calls buildEnd when the dev server (or build) closes; drop every
64
+ // poller so a stopped server leaks no watch state.
65
+ buildEnd() {
66
+ missingInputs.dispose();
43
67
  },
44
68
  },
45
69
  buildStart() {
@@ -59,8 +83,21 @@ const unpluginFactory = (rawOptions = {}) => {
59
83
  // unioned with the host-owned reference graph) so type-only inputs
60
84
  // invalidate this module in watch mode and persistent caches;
61
85
  // bundlers erase type-only imports from their own module graph and
62
- // would otherwise serve stale generated code.
63
- addWatchFile: (watched) => this.addWatchFile(watched),
86
+ // would otherwise serve stale generated code. Under Vite serve a
87
+ // missing input must not enter `addWatchFile`: import-analysis
88
+ // resolves added imports and 500s on a path that is absent by design
89
+ // (a superseding resolution candidate, a not-yet-generated
90
+ // dependency), so those are watched on the filesystem instead and
91
+ // invalidate this module when created.
92
+ addWatchFile: (watched) => {
93
+ if (viteCommand === "serve" &&
94
+ missingInputs.serving() &&
95
+ !fs.existsSync(watched)) {
96
+ missingInputs.watch(watched, path.resolve(file));
97
+ return;
98
+ }
99
+ this.addWatchFile(watched);
100
+ },
64
101
  // A module the plugin declared volatile depends on non-file inputs,
65
102
  // which no file-dependency snapshot can represent; mark it
66
103
  // uncacheable where the bundler exposes that control.
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../src/core/index.ts"],"sourcesContent":[null],"names":["options","resolveOptions","createTtscTransformCache","stripQuery","transformTtsc","createUnplugin","isDeclarationFile"],"mappings":";;;;;;;;AAeA,MAAM,IAAI,GAAG,eAAe;AAC5B;;;;AAIG;AACI,MAAM,iBAAiB,GAAG;AACjC;AACA,MAAM,kBAAkB,GAAG,oCAAoC;AAC/D;;;AAGG;AACH,MAAM,oBAAoB,GAAG,IAAI;AAEjC;;;;;;;;;AASG;AACH,MAAM,eAAe,GAGjB,CAAC,UAAU,GAAG,EAAE,KAAI;AACtB,IAAA,MAAMA,SAAO,GAAGC,sBAAc,CAAC,UAAU,CAAC;AAC1C,IAAA,MAAM,cAAc,GAAGC,kCAAwB,EAAE;AACjD,IAAA,IAAI,OAAgB;IAEpB,OAAO;QACL,IAAI;AACJ,QAAA,OAAO,EAAE,KAAK;AAEd,QAAA,IAAI,EAAE;AACJ,YAAA,cAAc,CAAC,MAAM,EAAA;AACnB,gBAAA,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK;YAChC,CAAC;AACF,SAAA;QAED,UAAU,GAAA;YACR,cAAc,CAAC,KAAK,EAAE;QACxB,CAAC;AAED,QAAA,gBAAgB,CAAC,EAAE,EAAA;AACjB,YAAA,MAAM,IAAI,GAAGC,oBAAU,CAAC,EAAE,CAAC;AAC3B,YAAA,OAAO,iBAAiB,CAAC,IAAI,CAAC;QAChC,CAAC;AAED,QAAA,MAAM,SAAS,CAAC,MAAM,EAAE,EAAE,EAAA;AACxB,YAAA,MAAM,IAAI,GAAGA,oBAAU,CAAC,EAAE,CAAC;AAC3B,YAAA,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,EAAE;AAC5B,gBAAA,OAAO,SAAS;YAClB;YACA,OAAOC,uBAAa,CAAC,IAAI,EAAE,MAAM,EAAEJ,SAAO,EAAE,OAAO,EAAE,cAAc,EAAE;;;;;;gBAMnE,YAAY,EAAE,CAAC,OAAO,KAAK,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC;;;;gBAIrD,YAAY,EAAE,MAAK;AACjB,oBAAA,MAAM,MAAM,GAAG,IAAI,CAAC,qBAAqB,IAAI;AAC7C,oBAAA,IACE,MAAM,EAAE,SAAS,KAAK,SAAS;AAC/B,wBAAA,MAAM,EAAE,SAAS,KAAK,QAAQ,EAC9B;wBACA,MAAM,CAAC,aAAa,EAAE,SAAS,GAAG,KAAK,CAAC;oBAC1C;gBACF,CAAC;AACF,aAAA,CAAC;QACJ,CAAC;KACF;AACH,CAAC;AAED,MAAM,QAAQ,GACZK,yBAAc,CAAC,eAAe;AAmBhC;;;;;;AAMG;AACH,SAAS,iBAAiB,CAAC,EAAU,EAAA;AACnC,IAAA,QACE,iBAAiB,CAAC,IAAI,CAAC,EAAE,CAAC;AAC1B,QAAA,CAAC,oBAAoB,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9B,CAACC,2BAAiB,CAAC,EAAE,CAAC;AACtB,QAAA,CAAC,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC;AAEhC;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":["../../src/core/index.ts"],"sourcesContent":[null],"names":["options","resolveOptions","createTtscTransformCache","createViteServeMissingInputWatch","stripQuery","transformTtsc","createUnplugin","isDeclarationFile"],"mappings":";;;;;;;;;;;AAkBA,MAAM,IAAI,GAAG,eAAe;AAC5B;;;;AAIG;AACI,MAAM,iBAAiB,GAAG;AACjC;AACA,MAAM,kBAAkB,GAAG,oCAAoC;AAC/D;;;AAGG;AACH,MAAM,oBAAoB,GAAG,IAAI;AAEjC;;;;;;;;;AASG;AACH,MAAM,eAAe,GAGjB,CAAC,UAAU,GAAG,EAAE,KAAI;AACtB,IAAA,MAAMA,SAAO,GAAGC,sBAAc,CAAC,UAAU,CAAC;AAC1C,IAAA,MAAM,cAAc,GAAGC,kCAAwB,EAAE;AACjD,IAAA,MAAM,aAAa,GAAGC,0CAAgC,EAAE;AACxD,IAAA,IAAI,OAAgB;AACpB,IAAA,IAAI,WAA+B;IAEnC,OAAO;QACL,IAAI;AACJ,QAAA,OAAO,EAAE,KAAK;AAEd,QAAA,IAAI,EAAE;AACJ,YAAA,cAAc,CAAC,MAAM,EAAA;AACnB,gBAAA,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK;;;;;AAK9B,gBAAA,WAAW,GAAG,MAAM,CAAC,OAAO;YAC9B,CAAC;;;;;;;AAOD,YAAA,eAAe,CAAC,MAAM,EAAA;AACpB,gBAAA,aAAa,CAAC,MAAM,CAAC,MAAM,CAAC;YAC9B,CAAC;;;YAGD,QAAQ,GAAA;gBACN,aAAa,CAAC,OAAO,EAAE;YACzB,CAAC;AACF,SAAA;QAED,UAAU,GAAA;YACR,cAAc,CAAC,KAAK,EAAE;QACxB,CAAC;AAED,QAAA,gBAAgB,CAAC,EAAE,EAAA;AACjB,YAAA,MAAM,IAAI,GAAGC,oBAAU,CAAC,EAAE,CAAC;AAC3B,YAAA,OAAO,iBAAiB,CAAC,IAAI,CAAC;QAChC,CAAC;AAED,QAAA,MAAM,SAAS,CAAC,MAAM,EAAE,EAAE,EAAA;AACxB,YAAA,MAAM,IAAI,GAAGA,oBAAU,CAAC,EAAE,CAAC;AAC3B,YAAA,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,EAAE;AAC5B,gBAAA,OAAO,SAAS;YAClB;YACA,OAAOC,uBAAa,CAAC,IAAI,EAAE,MAAM,EAAEL,SAAO,EAAE,OAAO,EAAE,cAAc,EAAE;;;;;;;;;;;AAWnE,gBAAA,YAAY,EAAE,CAAC,OAAO,KAAI;oBACxB,IACE,WAAW,KAAK,OAAO;wBACvB,aAAa,CAAC,OAAO,EAAE;AACvB,wBAAA,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,EACvB;AACA,wBAAA,aAAa,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;wBAChD;oBACF;AACA,oBAAA,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC;gBAC5B,CAAC;;;;gBAID,YAAY,EAAE,MAAK;AACjB,oBAAA,MAAM,MAAM,GAAG,IAAI,CAAC,qBAAqB,IAAI;AAC7C,oBAAA,IACE,MAAM,EAAE,SAAS,KAAK,SAAS;AAC/B,wBAAA,MAAM,EAAE,SAAS,KAAK,QAAQ,EAC9B;wBACA,MAAM,CAAC,aAAa,EAAE,SAAS,GAAG,KAAK,CAAC;oBAC1C;gBACF,CAAC;AACF,aAAA,CAAC;QACJ,CAAC;KACF;AACH,CAAC;AAED,MAAM,QAAQ,GACZM,yBAAc,CAAC,eAAe;AAmBhC;;;;;;AAMG;AACH,SAAS,iBAAiB,CAAC,EAAU,EAAA;AACnC,IAAA,QACE,iBAAiB,CAAC,IAAI,CAAC,EAAE,CAAC;AAC1B,QAAA,CAAC,oBAAoB,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9B,CAACC,2BAAiB,CAAC,EAAE,CAAC;AACtB,QAAA,CAAC,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC;AAEhC;;;;;;;;;;;;"}
@@ -1,7 +1,10 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
1
3
  import { createUnplugin } from 'unplugin';
2
4
  import { resolveOptions } from './options.mjs';
3
5
  import { stripQuery, transformTtsc, isDeclarationFile, createTtscTransformCache } from './transform.mjs';
4
6
  export { collectExternalInputHashes, collectProjectInputHashes, isProjectWalkPath } from './transform.mjs';
7
+ import { createViteServeMissingInputWatch } from './viteServe.mjs';
5
8
 
6
9
  const name = "ttsc-unplugin";
7
10
  /**
@@ -30,13 +33,34 @@ const virtualModulePattern = /\0/;
30
33
  const unpluginFactory = (rawOptions = {}) => {
31
34
  const options = resolveOptions(rawOptions);
32
35
  const transformCache = createTtscTransformCache();
36
+ const missingInputs = createViteServeMissingInputWatch();
33
37
  let aliases;
38
+ let viteCommand;
34
39
  return {
35
40
  name,
36
41
  enforce: "pre",
37
42
  vite: {
38
43
  configResolved(config) {
39
44
  aliases = config.resolve.alias;
45
+ // Re-read per config resolution: a plugin instance reused across a
46
+ // serve and a later build must stop routing missing inputs to the
47
+ // serve-time poll, even though the closed server stays attached
48
+ // (see the dispose note in viteServe.ts).
49
+ viteCommand = config.command;
50
+ },
51
+ // Vite serve funnels every transform-context `addWatchFile()` into the
52
+ // module's added-import graph (`_addedImports`), which import-analysis
53
+ // resolves like real imports. Capture the dev server so the transform
54
+ // hook can route watch inputs that do not exist yet — superseding
55
+ // resolution candidates above all — around that graph and still
56
+ // invalidate their importers when the path is created.
57
+ configureServer(server) {
58
+ missingInputs.attach(server);
59
+ },
60
+ // Vite calls buildEnd when the dev server (or build) closes; drop every
61
+ // poller so a stopped server leaks no watch state.
62
+ buildEnd() {
63
+ missingInputs.dispose();
40
64
  },
41
65
  },
42
66
  buildStart() {
@@ -56,8 +80,21 @@ const unpluginFactory = (rawOptions = {}) => {
56
80
  // unioned with the host-owned reference graph) so type-only inputs
57
81
  // invalidate this module in watch mode and persistent caches;
58
82
  // bundlers erase type-only imports from their own module graph and
59
- // would otherwise serve stale generated code.
60
- addWatchFile: (watched) => this.addWatchFile(watched),
83
+ // would otherwise serve stale generated code. Under Vite serve a
84
+ // missing input must not enter `addWatchFile`: import-analysis
85
+ // resolves added imports and 500s on a path that is absent by design
86
+ // (a superseding resolution candidate, a not-yet-generated
87
+ // dependency), so those are watched on the filesystem instead and
88
+ // invalidate this module when created.
89
+ addWatchFile: (watched) => {
90
+ if (viteCommand === "serve" &&
91
+ missingInputs.serving() &&
92
+ !fs.existsSync(watched)) {
93
+ missingInputs.watch(watched, path.resolve(file));
94
+ return;
95
+ }
96
+ this.addWatchFile(watched);
97
+ },
61
98
  // A module the plugin declared volatile depends on non-file inputs,
62
99
  // which no file-dependency snapshot can represent; mark it
63
100
  // uncacheable where the bundler exposes that control.
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","sources":["../../src/core/index.ts"],"sourcesContent":[null],"names":[],"mappings":";;;;;AAeA,MAAM,IAAI,GAAG,eAAe;AAC5B;;;;AAIG;AACI,MAAM,iBAAiB,GAAG;AACjC;AACA,MAAM,kBAAkB,GAAG,oCAAoC;AAC/D;;;AAGG;AACH,MAAM,oBAAoB,GAAG,IAAI;AAEjC;;;;;;;;;AASG;AACH,MAAM,eAAe,GAGjB,CAAC,UAAU,GAAG,EAAE,KAAI;AACtB,IAAA,MAAM,OAAO,GAAG,cAAc,CAAC,UAAU,CAAC;AAC1C,IAAA,MAAM,cAAc,GAAG,wBAAwB,EAAE;AACjD,IAAA,IAAI,OAAgB;IAEpB,OAAO;QACL,IAAI;AACJ,QAAA,OAAO,EAAE,KAAK;AAEd,QAAA,IAAI,EAAE;AACJ,YAAA,cAAc,CAAC,MAAM,EAAA;AACnB,gBAAA,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK;YAChC,CAAC;AACF,SAAA;QAED,UAAU,GAAA;YACR,cAAc,CAAC,KAAK,EAAE;QACxB,CAAC;AAED,QAAA,gBAAgB,CAAC,EAAE,EAAA;AACjB,YAAA,MAAM,IAAI,GAAG,UAAU,CAAC,EAAE,CAAC;AAC3B,YAAA,OAAO,iBAAiB,CAAC,IAAI,CAAC;QAChC,CAAC;AAED,QAAA,MAAM,SAAS,CAAC,MAAM,EAAE,EAAE,EAAA;AACxB,YAAA,MAAM,IAAI,GAAG,UAAU,CAAC,EAAE,CAAC;AAC3B,YAAA,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,EAAE;AAC5B,gBAAA,OAAO,SAAS;YAClB;YACA,OAAO,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE;;;;;;gBAMnE,YAAY,EAAE,CAAC,OAAO,KAAK,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC;;;;gBAIrD,YAAY,EAAE,MAAK;AACjB,oBAAA,MAAM,MAAM,GAAG,IAAI,CAAC,qBAAqB,IAAI;AAC7C,oBAAA,IACE,MAAM,EAAE,SAAS,KAAK,SAAS;AAC/B,wBAAA,MAAM,EAAE,SAAS,KAAK,QAAQ,EAC9B;wBACA,MAAM,CAAC,aAAa,EAAE,SAAS,GAAG,KAAK,CAAC;oBAC1C;gBACF,CAAC;AACF,aAAA,CAAC;QACJ,CAAC;KACF;AACH,CAAC;AAED,MAAM,QAAQ,GACZ,cAAc,CAAC,eAAe;AAmBhC;;;;;;AAMG;AACH,SAAS,iBAAiB,CAAC,EAAU,EAAA;AACnC,IAAA,QACE,iBAAiB,CAAC,IAAI,CAAC,EAAE,CAAC;AAC1B,QAAA,CAAC,oBAAoB,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9B,CAAC,iBAAiB,CAAC,EAAE,CAAC;AACtB,QAAA,CAAC,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC;AAEhC;;;;"}
1
+ {"version":3,"file":"index.mjs","sources":["../../src/core/index.ts"],"sourcesContent":[null],"names":[],"mappings":";;;;;;;;AAkBA,MAAM,IAAI,GAAG,eAAe;AAC5B;;;;AAIG;AACI,MAAM,iBAAiB,GAAG;AACjC;AACA,MAAM,kBAAkB,GAAG,oCAAoC;AAC/D;;;AAGG;AACH,MAAM,oBAAoB,GAAG,IAAI;AAEjC;;;;;;;;;AASG;AACH,MAAM,eAAe,GAGjB,CAAC,UAAU,GAAG,EAAE,KAAI;AACtB,IAAA,MAAM,OAAO,GAAG,cAAc,CAAC,UAAU,CAAC;AAC1C,IAAA,MAAM,cAAc,GAAG,wBAAwB,EAAE;AACjD,IAAA,MAAM,aAAa,GAAG,gCAAgC,EAAE;AACxD,IAAA,IAAI,OAAgB;AACpB,IAAA,IAAI,WAA+B;IAEnC,OAAO;QACL,IAAI;AACJ,QAAA,OAAO,EAAE,KAAK;AAEd,QAAA,IAAI,EAAE;AACJ,YAAA,cAAc,CAAC,MAAM,EAAA;AACnB,gBAAA,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK;;;;;AAK9B,gBAAA,WAAW,GAAG,MAAM,CAAC,OAAO;YAC9B,CAAC;;;;;;;AAOD,YAAA,eAAe,CAAC,MAAM,EAAA;AACpB,gBAAA,aAAa,CAAC,MAAM,CAAC,MAAM,CAAC;YAC9B,CAAC;;;YAGD,QAAQ,GAAA;gBACN,aAAa,CAAC,OAAO,EAAE;YACzB,CAAC;AACF,SAAA;QAED,UAAU,GAAA;YACR,cAAc,CAAC,KAAK,EAAE;QACxB,CAAC;AAED,QAAA,gBAAgB,CAAC,EAAE,EAAA;AACjB,YAAA,MAAM,IAAI,GAAG,UAAU,CAAC,EAAE,CAAC;AAC3B,YAAA,OAAO,iBAAiB,CAAC,IAAI,CAAC;QAChC,CAAC;AAED,QAAA,MAAM,SAAS,CAAC,MAAM,EAAE,EAAE,EAAA;AACxB,YAAA,MAAM,IAAI,GAAG,UAAU,CAAC,EAAE,CAAC;AAC3B,YAAA,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,EAAE;AAC5B,gBAAA,OAAO,SAAS;YAClB;YACA,OAAO,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE;;;;;;;;;;;AAWnE,gBAAA,YAAY,EAAE,CAAC,OAAO,KAAI;oBACxB,IACE,WAAW,KAAK,OAAO;wBACvB,aAAa,CAAC,OAAO,EAAE;AACvB,wBAAA,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,EACvB;AACA,wBAAA,aAAa,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;wBAChD;oBACF;AACA,oBAAA,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC;gBAC5B,CAAC;;;;gBAID,YAAY,EAAE,MAAK;AACjB,oBAAA,MAAM,MAAM,GAAG,IAAI,CAAC,qBAAqB,IAAI;AAC7C,oBAAA,IACE,MAAM,EAAE,SAAS,KAAK,SAAS;AAC/B,wBAAA,MAAM,EAAE,SAAS,KAAK,QAAQ,EAC9B;wBACA,MAAM,CAAC,aAAa,EAAE,SAAS,GAAG,KAAK,CAAC;oBAC1C;gBACF,CAAC;AACF,aAAA,CAAC;QACJ,CAAC;KACF;AACH,CAAC;AAED,MAAM,QAAQ,GACZ,cAAc,CAAC,eAAe;AAmBhC;;;;;;AAMG;AACH,SAAS,iBAAiB,CAAC,EAAU,EAAA;AACnC,IAAA,QACE,iBAAiB,CAAC,IAAI,CAAC,EAAE,CAAC;AAC1B,QAAA,CAAC,oBAAoB,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9B,CAAC,iBAAiB,CAAC,EAAE,CAAC;AACtB,QAAA,CAAC,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC;AAEhC;;;;"}
@@ -0,0 +1,74 @@
1
+ /** One module node inside a Vite module graph; opaque to this module. */
2
+ type ViteModuleNodeLike = object;
3
+ /**
4
+ * The module-graph surface this module touches, shared by Vite's mixed module
5
+ * graph and the per-environment graphs of the environment API.
6
+ */
7
+ interface ViteModuleGraphLike {
8
+ fileToModulesMap?: Map<string, Set<ViteModuleNodeLike>>;
9
+ getModulesByFile?(file: string): Set<ViteModuleNodeLike> | undefined;
10
+ invalidateModule?(node: ViteModuleNodeLike): void;
11
+ }
12
+ /** A channel that can deliver a full-reload event to connected clients. */
13
+ interface ViteHotChannelLike {
14
+ send?(payload: {
15
+ path?: string;
16
+ type: "full-reload";
17
+ }): void;
18
+ }
19
+ /** One dev-server environment (client, ssr, or a custom one). */
20
+ interface ViteEnvironmentLike {
21
+ hot?: ViteHotChannelLike;
22
+ moduleGraph?: ViteModuleGraphLike;
23
+ }
24
+ /**
25
+ * Minimal structural view of the Vite dev server. Declared locally instead of
26
+ * importing `vite` so the published type declarations never require Vite to be
27
+ * installed, and so one shape spans the mixed module graph (Vite 5), the
28
+ * environment API (Vite 6+), and whichever of `ws`/`hot` a major still
29
+ * carries.
30
+ */
31
+ export interface ViteDevServerLike {
32
+ environments?: Record<string, ViteEnvironmentLike>;
33
+ hot?: ViteHotChannelLike;
34
+ moduleGraph?: ViteModuleGraphLike;
35
+ ws?: ViteHotChannelLike;
36
+ }
37
+ /**
38
+ * Filesystem watch for derived watch inputs that do not exist while a Vite dev
39
+ * server is running.
40
+ *
41
+ * Vite serve treats every transform-context `addWatchFile()` registration as an
42
+ * added import: `TransformPluginContext.addWatchFile` stores the path in
43
+ * `_addedImports`, and `vite:import-analysis` resolves each entry like a real
44
+ * import of the transformed module. A missing path — a superseding resolution
45
+ * candidate or a plugin-reported dependency that is not generated yet — then
46
+ * fails that resolve and turns the importer's first request into a 500, even
47
+ * though the transform itself succeeded.
48
+ *
49
+ * This registry is the serve-only replacement for those registrations. Each
50
+ * missing path is stat-polled; when it is created, every importer that
51
+ * registered it is invalidated in the server's module graphs and one
52
+ * full-reload is sent, so the next request retransforms the importer against
53
+ * the new resolution winner. The project transform cache re-validates through
54
+ * its external-input hashes (a recorded `missing` marker differs from a content
55
+ * hash), so the retransform recompiles instead of replaying.
56
+ */
57
+ export interface ViteServeMissingInputWatch {
58
+ /** Adopt the dev server whose module graphs creation events invalidate. */
59
+ attach(server: ViteDevServerLike): void;
60
+ /** Stop every poll; safe to call repeatedly. */
61
+ dispose(): void;
62
+ /**
63
+ * Report whether a dev server has ever been attached. This is not a liveness
64
+ * predicate — the reference intentionally survives the server's close (see
65
+ * {@link dispose}) — so route decisions must also gate on the resolved
66
+ * config's `command`, as the adapter does.
67
+ */
68
+ serving(): boolean;
69
+ /** Register one missing watch input derived for `importer`. */
70
+ watch(input: string, importer: string): void;
71
+ }
72
+ /** Create an empty missing-input watch for one plugin instance. */
73
+ export declare function createViteServeMissingInputWatch(): ViteServeMissingInputWatch;
74
+ export {};
@@ -0,0 +1,180 @@
1
+ 'use strict';
2
+
3
+ var fs = require('node:fs');
4
+ var path = require('node:path');
5
+ var transform = require('./transform.js');
6
+
7
+ /**
8
+ * How often each registered missing watch input is stat-polled, in
9
+ * milliseconds.
10
+ *
11
+ * Polling is the only watch primitive that covers the whole class: the dev
12
+ * server's chokidar watcher ignores every `node_modules` directory, which is
13
+ * exactly where superseding resolution candidates usually live, and `fs.watch`
14
+ * cannot observe a path whose parent directories do not exist yet. One `stat`
15
+ * every half second per missing path is negligible against a dev server's
16
+ * baseline.
17
+ */
18
+ const MISSING_INPUT_POLL_INTERVAL = 500;
19
+ /** Create an empty missing-input watch for one plugin instance. */
20
+ function createViteServeMissingInputWatch() {
21
+ const entries = new Map();
22
+ let server;
23
+ const unwatch = (identity, entry) => {
24
+ fs.unwatchFile(entry.spelling, entry.listener);
25
+ entries.delete(identity);
26
+ };
27
+ return {
28
+ attach(next) {
29
+ server = next;
30
+ },
31
+ dispose() {
32
+ for (const [identity, entry] of entries) {
33
+ unwatch(identity, entry);
34
+ }
35
+ // The server reference deliberately survives: `vite.restartServer`
36
+ // configures the replacement server (attach) before it closes the old
37
+ // one (whose buildEnd runs this dispose), so unsetting it here would
38
+ // detach the freshly attached replacement and revive the 500 this
39
+ // module exists to prevent. A same-instance `vite build` after a serve
40
+ // is instead excluded by the adapter's `config.command` gate.
41
+ },
42
+ serving() {
43
+ return server !== undefined;
44
+ },
45
+ watch(input, importer) {
46
+ const spelling = path.resolve(input);
47
+ const identity = transform.pathIdentityKey(spelling);
48
+ const existing = entries.get(identity);
49
+ if (existing !== undefined) {
50
+ existing.importers.add(path.resolve(importer));
51
+ return;
52
+ }
53
+ const entry = {
54
+ importers: new Set([path.resolve(importer)]),
55
+ listener: (current) => {
56
+ // `fs.watchFile` reports a missing path as zeroed stats (and fires
57
+ // once with them right after registration); only a poll that
58
+ // observes a real file is a creation event.
59
+ if (current.mtimeMs === 0 && !fs.existsSync(entry.spelling)) {
60
+ return;
61
+ }
62
+ unwatch(identity, entry);
63
+ if (server === undefined) {
64
+ return;
65
+ }
66
+ invalidateImporters(server, entry.importers);
67
+ sendFullReload(server);
68
+ },
69
+ spelling,
70
+ };
71
+ entries.set(identity, entry);
72
+ const watcher = fs.watchFile(spelling, { interval: MISSING_INPUT_POLL_INTERVAL }, entry.listener);
73
+ // A poller must never keep the dev-server process alive on its own.
74
+ watcher.unref?.();
75
+ // `fs.watchFile` snapshots the path's stats at registration and fires
76
+ // only on a subsequent change, so a file created between the adapter's
77
+ // existence check and this registration would count as "unchanged" and
78
+ // never fire. One deferred recheck closes that window; routing through
79
+ // the listener keeps a single finalization path.
80
+ const recheck = setTimeout(() => {
81
+ if (entries.get(identity) !== entry) {
82
+ return;
83
+ }
84
+ try {
85
+ entry.listener(fs.statSync(entry.spelling));
86
+ }
87
+ catch {
88
+ // Still missing (or deleted again): the ordinary poll stays armed.
89
+ }
90
+ }, MISSING_INPUT_POLL_INTERVAL);
91
+ recheck.unref?.();
92
+ },
93
+ };
94
+ }
95
+ /**
96
+ * Invalidate every module-graph node of the registered importers so the next
97
+ * request retransforms them. Importers keep their original absolute spelling so
98
+ * the module graph's exact-key lookup can hit; graph lookups still go through
99
+ * {@link selectModulesByFile} because module-graph file keys are
100
+ * slash-normalized and, on case-insensitive filesystems, may not match the
101
+ * compiler's spelling byte for byte.
102
+ */
103
+ function invalidateImporters(server, importers) {
104
+ for (const graph of selectModuleGraphs(server)) {
105
+ for (const importer of importers) {
106
+ for (const node of selectModulesByFile(graph, importer)) {
107
+ try {
108
+ graph.invalidateModule?.(node);
109
+ }
110
+ catch {
111
+ // A graph shape this structural view mispredicts must not crash the
112
+ // poll; the full-reload below still forces a refetch, and the
113
+ // transform cache's external-input hashes force the recompile.
114
+ }
115
+ }
116
+ }
117
+ }
118
+ }
119
+ /**
120
+ * Enumerate the server's module graphs: one per environment under the
121
+ * environment API (Vite 6+), otherwise the mixed module graph (Vite 5).
122
+ */
123
+ function selectModuleGraphs(server) {
124
+ const graphs = [];
125
+ for (const environment of Object.values(server.environments ?? {})) {
126
+ if (environment?.moduleGraph !== undefined) {
127
+ graphs.push(environment.moduleGraph);
128
+ }
129
+ }
130
+ if (graphs.length === 0 && server.moduleGraph !== undefined) {
131
+ graphs.push(server.moduleGraph);
132
+ }
133
+ return graphs;
134
+ }
135
+ /**
136
+ * Look up the module nodes registered for one importer spelling: the fast
137
+ * slash-normalized `getModulesByFile` lookup first, then an identity scan of
138
+ * `fileToModulesMap` for spellings that differ only by separator or case.
139
+ */
140
+ function selectModulesByFile(graph, importer) {
141
+ const direct = graph.getModulesByFile?.(importer.replace(/\\/g, "/"));
142
+ if (direct !== undefined && direct.size !== 0) {
143
+ return [...direct];
144
+ }
145
+ const identity = transform.pathIdentityKey(importer);
146
+ const output = [];
147
+ for (const [file, nodes] of graph.fileToModulesMap ?? []) {
148
+ if (typeof file === "string" && transform.pathIdentityKey(file) === identity) {
149
+ output.push(...nodes);
150
+ }
151
+ }
152
+ return output;
153
+ }
154
+ /**
155
+ * Deliver one full-reload so connected clients refetch the invalidated
156
+ * importers. The channels differ across Vite majors (`ws`, deprecated `hot`,
157
+ * per-environment `hot`); the first one that accepts the payload wins.
158
+ */
159
+ function sendFullReload(server) {
160
+ for (const channel of [
161
+ server.ws,
162
+ server.hot,
163
+ server.environments?.client?.hot,
164
+ ]) {
165
+ if (channel?.send === undefined) {
166
+ continue;
167
+ }
168
+ try {
169
+ channel.send({ path: "*", type: "full-reload" });
170
+ return;
171
+ }
172
+ catch {
173
+ // Try the next channel; an unsupported payload on one major must not
174
+ // suppress delivery through another.
175
+ }
176
+ }
177
+ }
178
+
179
+ exports.createViteServeMissingInputWatch = createViteServeMissingInputWatch;
180
+ //# sourceMappingURL=viteServe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viteServe.js","sources":["../../src/core/viteServe.ts"],"sourcesContent":[null],"names":["pathIdentityKey"],"mappings":"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"}
@@ -0,0 +1,178 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { pathIdentityKey } from './transform.mjs';
4
+
5
+ /**
6
+ * How often each registered missing watch input is stat-polled, in
7
+ * milliseconds.
8
+ *
9
+ * Polling is the only watch primitive that covers the whole class: the dev
10
+ * server's chokidar watcher ignores every `node_modules` directory, which is
11
+ * exactly where superseding resolution candidates usually live, and `fs.watch`
12
+ * cannot observe a path whose parent directories do not exist yet. One `stat`
13
+ * every half second per missing path is negligible against a dev server's
14
+ * baseline.
15
+ */
16
+ const MISSING_INPUT_POLL_INTERVAL = 500;
17
+ /** Create an empty missing-input watch for one plugin instance. */
18
+ function createViteServeMissingInputWatch() {
19
+ const entries = new Map();
20
+ let server;
21
+ const unwatch = (identity, entry) => {
22
+ fs.unwatchFile(entry.spelling, entry.listener);
23
+ entries.delete(identity);
24
+ };
25
+ return {
26
+ attach(next) {
27
+ server = next;
28
+ },
29
+ dispose() {
30
+ for (const [identity, entry] of entries) {
31
+ unwatch(identity, entry);
32
+ }
33
+ // The server reference deliberately survives: `vite.restartServer`
34
+ // configures the replacement server (attach) before it closes the old
35
+ // one (whose buildEnd runs this dispose), so unsetting it here would
36
+ // detach the freshly attached replacement and revive the 500 this
37
+ // module exists to prevent. A same-instance `vite build` after a serve
38
+ // is instead excluded by the adapter's `config.command` gate.
39
+ },
40
+ serving() {
41
+ return server !== undefined;
42
+ },
43
+ watch(input, importer) {
44
+ const spelling = path.resolve(input);
45
+ const identity = pathIdentityKey(spelling);
46
+ const existing = entries.get(identity);
47
+ if (existing !== undefined) {
48
+ existing.importers.add(path.resolve(importer));
49
+ return;
50
+ }
51
+ const entry = {
52
+ importers: new Set([path.resolve(importer)]),
53
+ listener: (current) => {
54
+ // `fs.watchFile` reports a missing path as zeroed stats (and fires
55
+ // once with them right after registration); only a poll that
56
+ // observes a real file is a creation event.
57
+ if (current.mtimeMs === 0 && !fs.existsSync(entry.spelling)) {
58
+ return;
59
+ }
60
+ unwatch(identity, entry);
61
+ if (server === undefined) {
62
+ return;
63
+ }
64
+ invalidateImporters(server, entry.importers);
65
+ sendFullReload(server);
66
+ },
67
+ spelling,
68
+ };
69
+ entries.set(identity, entry);
70
+ const watcher = fs.watchFile(spelling, { interval: MISSING_INPUT_POLL_INTERVAL }, entry.listener);
71
+ // A poller must never keep the dev-server process alive on its own.
72
+ watcher.unref?.();
73
+ // `fs.watchFile` snapshots the path's stats at registration and fires
74
+ // only on a subsequent change, so a file created between the adapter's
75
+ // existence check and this registration would count as "unchanged" and
76
+ // never fire. One deferred recheck closes that window; routing through
77
+ // the listener keeps a single finalization path.
78
+ const recheck = setTimeout(() => {
79
+ if (entries.get(identity) !== entry) {
80
+ return;
81
+ }
82
+ try {
83
+ entry.listener(fs.statSync(entry.spelling));
84
+ }
85
+ catch {
86
+ // Still missing (or deleted again): the ordinary poll stays armed.
87
+ }
88
+ }, MISSING_INPUT_POLL_INTERVAL);
89
+ recheck.unref?.();
90
+ },
91
+ };
92
+ }
93
+ /**
94
+ * Invalidate every module-graph node of the registered importers so the next
95
+ * request retransforms them. Importers keep their original absolute spelling so
96
+ * the module graph's exact-key lookup can hit; graph lookups still go through
97
+ * {@link selectModulesByFile} because module-graph file keys are
98
+ * slash-normalized and, on case-insensitive filesystems, may not match the
99
+ * compiler's spelling byte for byte.
100
+ */
101
+ function invalidateImporters(server, importers) {
102
+ for (const graph of selectModuleGraphs(server)) {
103
+ for (const importer of importers) {
104
+ for (const node of selectModulesByFile(graph, importer)) {
105
+ try {
106
+ graph.invalidateModule?.(node);
107
+ }
108
+ catch {
109
+ // A graph shape this structural view mispredicts must not crash the
110
+ // poll; the full-reload below still forces a refetch, and the
111
+ // transform cache's external-input hashes force the recompile.
112
+ }
113
+ }
114
+ }
115
+ }
116
+ }
117
+ /**
118
+ * Enumerate the server's module graphs: one per environment under the
119
+ * environment API (Vite 6+), otherwise the mixed module graph (Vite 5).
120
+ */
121
+ function selectModuleGraphs(server) {
122
+ const graphs = [];
123
+ for (const environment of Object.values(server.environments ?? {})) {
124
+ if (environment?.moduleGraph !== undefined) {
125
+ graphs.push(environment.moduleGraph);
126
+ }
127
+ }
128
+ if (graphs.length === 0 && server.moduleGraph !== undefined) {
129
+ graphs.push(server.moduleGraph);
130
+ }
131
+ return graphs;
132
+ }
133
+ /**
134
+ * Look up the module nodes registered for one importer spelling: the fast
135
+ * slash-normalized `getModulesByFile` lookup first, then an identity scan of
136
+ * `fileToModulesMap` for spellings that differ only by separator or case.
137
+ */
138
+ function selectModulesByFile(graph, importer) {
139
+ const direct = graph.getModulesByFile?.(importer.replace(/\\/g, "/"));
140
+ if (direct !== undefined && direct.size !== 0) {
141
+ return [...direct];
142
+ }
143
+ const identity = pathIdentityKey(importer);
144
+ const output = [];
145
+ for (const [file, nodes] of graph.fileToModulesMap ?? []) {
146
+ if (typeof file === "string" && pathIdentityKey(file) === identity) {
147
+ output.push(...nodes);
148
+ }
149
+ }
150
+ return output;
151
+ }
152
+ /**
153
+ * Deliver one full-reload so connected clients refetch the invalidated
154
+ * importers. The channels differ across Vite majors (`ws`, deprecated `hot`,
155
+ * per-environment `hot`); the first one that accepts the payload wins.
156
+ */
157
+ function sendFullReload(server) {
158
+ for (const channel of [
159
+ server.ws,
160
+ server.hot,
161
+ server.environments?.client?.hot,
162
+ ]) {
163
+ if (channel?.send === undefined) {
164
+ continue;
165
+ }
166
+ try {
167
+ channel.send({ path: "*", type: "full-reload" });
168
+ return;
169
+ }
170
+ catch {
171
+ // Try the next channel; an unsupported payload on one major must not
172
+ // suppress delivery through another.
173
+ }
174
+ }
175
+ }
176
+
177
+ export { createViteServeMissingInputWatch };
178
+ //# sourceMappingURL=viteServe.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viteServe.mjs","sources":["../../src/core/viteServe.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttsc/unplugin",
3
- "version": "0.20.0",
3
+ "version": "0.20.1",
4
4
  "description": "Bundler adapters for ttsc plugins.",
5
5
  "main": "lib/index.js",
6
6
  "module": "lib/index.mjs",
@@ -95,7 +95,7 @@
95
95
  "unplugin": "^2.3.11"
96
96
  },
97
97
  "peerDependencies": {
98
- "ttsc": "^0.20.0"
98
+ "ttsc": "^0.20.1"
99
99
  },
100
100
  "devDependencies": {
101
101
  "@rollup/plugin-commonjs": "^29.0.2",
@@ -108,7 +108,7 @@
108
108
  "tslib": "^2.8.1",
109
109
  "typescript": "^7.0.2",
110
110
  "vite": "^7.1.12",
111
- "ttsc": "0.20.0"
111
+ "ttsc": "0.20.1"
112
112
  },
113
113
  "repository": {
114
114
  "type": "git",
package/src/core/index.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
1
3
  import type { UnpluginFactory, UnpluginInstance } from "unplugin";
2
4
  import { createUnplugin } from "unplugin";
3
5
 
@@ -12,6 +14,7 @@ import {
12
14
  stripQuery,
13
15
  transformTtsc,
14
16
  } from "./transform";
17
+ import { createViteServeMissingInputWatch } from "./viteServe";
15
18
 
16
19
  const name = "ttsc-unplugin";
17
20
  /**
@@ -44,7 +47,9 @@ const unpluginFactory: UnpluginFactory<
44
47
  > = (rawOptions = {}) => {
45
48
  const options = resolveOptions(rawOptions);
46
49
  const transformCache = createTtscTransformCache();
50
+ const missingInputs = createViteServeMissingInputWatch();
47
51
  let aliases: unknown;
52
+ let viteCommand: string | undefined;
48
53
 
49
54
  return {
50
55
  name,
@@ -53,6 +58,25 @@ const unpluginFactory: UnpluginFactory<
53
58
  vite: {
54
59
  configResolved(config) {
55
60
  aliases = config.resolve.alias;
61
+ // Re-read per config resolution: a plugin instance reused across a
62
+ // serve and a later build must stop routing missing inputs to the
63
+ // serve-time poll, even though the closed server stays attached
64
+ // (see the dispose note in viteServe.ts).
65
+ viteCommand = config.command;
66
+ },
67
+ // Vite serve funnels every transform-context `addWatchFile()` into the
68
+ // module's added-import graph (`_addedImports`), which import-analysis
69
+ // resolves like real imports. Capture the dev server so the transform
70
+ // hook can route watch inputs that do not exist yet — superseding
71
+ // resolution candidates above all — around that graph and still
72
+ // invalidate their importers when the path is created.
73
+ configureServer(server) {
74
+ missingInputs.attach(server);
75
+ },
76
+ // Vite calls buildEnd when the dev server (or build) closes; drop every
77
+ // poller so a stopped server leaks no watch state.
78
+ buildEnd() {
79
+ missingInputs.dispose();
56
80
  },
57
81
  },
58
82
 
@@ -75,8 +99,23 @@ const unpluginFactory: UnpluginFactory<
75
99
  // unioned with the host-owned reference graph) so type-only inputs
76
100
  // invalidate this module in watch mode and persistent caches;
77
101
  // bundlers erase type-only imports from their own module graph and
78
- // would otherwise serve stale generated code.
79
- addWatchFile: (watched) => this.addWatchFile(watched),
102
+ // would otherwise serve stale generated code. Under Vite serve a
103
+ // missing input must not enter `addWatchFile`: import-analysis
104
+ // resolves added imports and 500s on a path that is absent by design
105
+ // (a superseding resolution candidate, a not-yet-generated
106
+ // dependency), so those are watched on the filesystem instead and
107
+ // invalidate this module when created.
108
+ addWatchFile: (watched) => {
109
+ if (
110
+ viteCommand === "serve" &&
111
+ missingInputs.serving() &&
112
+ !fs.existsSync(watched)
113
+ ) {
114
+ missingInputs.watch(watched, path.resolve(file));
115
+ return;
116
+ }
117
+ this.addWatchFile(watched);
118
+ },
80
119
  // A module the plugin declared volatile depends on non-file inputs,
81
120
  // which no file-dependency snapshot can represent; mark it
82
121
  // uncacheable where the bundler exposes that control.
@@ -0,0 +1,271 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { pathIdentityKey } from "./transform";
5
+
6
+ /**
7
+ * How often each registered missing watch input is stat-polled, in
8
+ * milliseconds.
9
+ *
10
+ * Polling is the only watch primitive that covers the whole class: the dev
11
+ * server's chokidar watcher ignores every `node_modules` directory, which is
12
+ * exactly where superseding resolution candidates usually live, and `fs.watch`
13
+ * cannot observe a path whose parent directories do not exist yet. One `stat`
14
+ * every half second per missing path is negligible against a dev server's
15
+ * baseline.
16
+ */
17
+ const MISSING_INPUT_POLL_INTERVAL = 500;
18
+
19
+ /** One module node inside a Vite module graph; opaque to this module. */
20
+ type ViteModuleNodeLike = object;
21
+
22
+ /**
23
+ * The module-graph surface this module touches, shared by Vite's mixed module
24
+ * graph and the per-environment graphs of the environment API.
25
+ */
26
+ interface ViteModuleGraphLike {
27
+ fileToModulesMap?: Map<string, Set<ViteModuleNodeLike>>;
28
+ getModulesByFile?(file: string): Set<ViteModuleNodeLike> | undefined;
29
+ invalidateModule?(node: ViteModuleNodeLike): void;
30
+ }
31
+
32
+ /** A channel that can deliver a full-reload event to connected clients. */
33
+ interface ViteHotChannelLike {
34
+ send?(payload: { path?: string; type: "full-reload" }): void;
35
+ }
36
+
37
+ /** One dev-server environment (client, ssr, or a custom one). */
38
+ interface ViteEnvironmentLike {
39
+ hot?: ViteHotChannelLike;
40
+ moduleGraph?: ViteModuleGraphLike;
41
+ }
42
+
43
+ /**
44
+ * Minimal structural view of the Vite dev server. Declared locally instead of
45
+ * importing `vite` so the published type declarations never require Vite to be
46
+ * installed, and so one shape spans the mixed module graph (Vite 5), the
47
+ * environment API (Vite 6+), and whichever of `ws`/`hot` a major still
48
+ * carries.
49
+ */
50
+ export interface ViteDevServerLike {
51
+ environments?: Record<string, ViteEnvironmentLike>;
52
+ hot?: ViteHotChannelLike;
53
+ moduleGraph?: ViteModuleGraphLike;
54
+ ws?: ViteHotChannelLike;
55
+ }
56
+
57
+ /**
58
+ * Filesystem watch for derived watch inputs that do not exist while a Vite dev
59
+ * server is running.
60
+ *
61
+ * Vite serve treats every transform-context `addWatchFile()` registration as an
62
+ * added import: `TransformPluginContext.addWatchFile` stores the path in
63
+ * `_addedImports`, and `vite:import-analysis` resolves each entry like a real
64
+ * import of the transformed module. A missing path — a superseding resolution
65
+ * candidate or a plugin-reported dependency that is not generated yet — then
66
+ * fails that resolve and turns the importer's first request into a 500, even
67
+ * though the transform itself succeeded.
68
+ *
69
+ * This registry is the serve-only replacement for those registrations. Each
70
+ * missing path is stat-polled; when it is created, every importer that
71
+ * registered it is invalidated in the server's module graphs and one
72
+ * full-reload is sent, so the next request retransforms the importer against
73
+ * the new resolution winner. The project transform cache re-validates through
74
+ * its external-input hashes (a recorded `missing` marker differs from a content
75
+ * hash), so the retransform recompiles instead of replaying.
76
+ */
77
+ export interface ViteServeMissingInputWatch {
78
+ /** Adopt the dev server whose module graphs creation events invalidate. */
79
+ attach(server: ViteDevServerLike): void;
80
+ /** Stop every poll; safe to call repeatedly. */
81
+ dispose(): void;
82
+ /**
83
+ * Report whether a dev server has ever been attached. This is not a liveness
84
+ * predicate — the reference intentionally survives the server's close (see
85
+ * {@link dispose}) — so route decisions must also gate on the resolved
86
+ * config's `command`, as the adapter does.
87
+ */
88
+ serving(): boolean;
89
+ /** Register one missing watch input derived for `importer`. */
90
+ watch(input: string, importer: string): void;
91
+ }
92
+
93
+ /** Poll bookkeeping for one registered missing path. */
94
+ interface IMissingInputEntry {
95
+ importers: Set<string>;
96
+ listener: (current: fs.Stats) => void;
97
+ spelling: string;
98
+ }
99
+
100
+ /** Create an empty missing-input watch for one plugin instance. */
101
+ export function createViteServeMissingInputWatch(): ViteServeMissingInputWatch {
102
+ const entries = new Map<string, IMissingInputEntry>();
103
+ let server: ViteDevServerLike | undefined;
104
+
105
+ const unwatch = (identity: string, entry: IMissingInputEntry): void => {
106
+ fs.unwatchFile(entry.spelling, entry.listener);
107
+ entries.delete(identity);
108
+ };
109
+
110
+ return {
111
+ attach(next) {
112
+ server = next;
113
+ },
114
+ dispose() {
115
+ for (const [identity, entry] of entries) {
116
+ unwatch(identity, entry);
117
+ }
118
+ // The server reference deliberately survives: `vite.restartServer`
119
+ // configures the replacement server (attach) before it closes the old
120
+ // one (whose buildEnd runs this dispose), so unsetting it here would
121
+ // detach the freshly attached replacement and revive the 500 this
122
+ // module exists to prevent. A same-instance `vite build` after a serve
123
+ // is instead excluded by the adapter's `config.command` gate.
124
+ },
125
+ serving() {
126
+ return server !== undefined;
127
+ },
128
+ watch(input, importer) {
129
+ const spelling = path.resolve(input);
130
+ const identity = pathIdentityKey(spelling);
131
+ const existing = entries.get(identity);
132
+ if (existing !== undefined) {
133
+ existing.importers.add(path.resolve(importer));
134
+ return;
135
+ }
136
+ const entry: IMissingInputEntry = {
137
+ importers: new Set([path.resolve(importer)]),
138
+ listener: (current) => {
139
+ // `fs.watchFile` reports a missing path as zeroed stats (and fires
140
+ // once with them right after registration); only a poll that
141
+ // observes a real file is a creation event.
142
+ if (current.mtimeMs === 0 && !fs.existsSync(entry.spelling)) {
143
+ return;
144
+ }
145
+ unwatch(identity, entry);
146
+ if (server === undefined) {
147
+ return;
148
+ }
149
+ invalidateImporters(server, entry.importers);
150
+ sendFullReload(server);
151
+ },
152
+ spelling,
153
+ };
154
+ entries.set(identity, entry);
155
+ const watcher = fs.watchFile(
156
+ spelling,
157
+ { interval: MISSING_INPUT_POLL_INTERVAL },
158
+ entry.listener,
159
+ );
160
+ // A poller must never keep the dev-server process alive on its own.
161
+ watcher.unref?.();
162
+ // `fs.watchFile` snapshots the path's stats at registration and fires
163
+ // only on a subsequent change, so a file created between the adapter's
164
+ // existence check and this registration would count as "unchanged" and
165
+ // never fire. One deferred recheck closes that window; routing through
166
+ // the listener keeps a single finalization path.
167
+ const recheck = setTimeout(() => {
168
+ if (entries.get(identity) !== entry) {
169
+ return;
170
+ }
171
+ try {
172
+ entry.listener(fs.statSync(entry.spelling));
173
+ } catch {
174
+ // Still missing (or deleted again): the ordinary poll stays armed.
175
+ }
176
+ }, MISSING_INPUT_POLL_INTERVAL);
177
+ recheck.unref?.();
178
+ },
179
+ };
180
+ }
181
+
182
+ /**
183
+ * Invalidate every module-graph node of the registered importers so the next
184
+ * request retransforms them. Importers keep their original absolute spelling so
185
+ * the module graph's exact-key lookup can hit; graph lookups still go through
186
+ * {@link selectModulesByFile} because module-graph file keys are
187
+ * slash-normalized and, on case-insensitive filesystems, may not match the
188
+ * compiler's spelling byte for byte.
189
+ */
190
+ function invalidateImporters(
191
+ server: ViteDevServerLike,
192
+ importers: ReadonlySet<string>,
193
+ ): void {
194
+ for (const graph of selectModuleGraphs(server)) {
195
+ for (const importer of importers) {
196
+ for (const node of selectModulesByFile(graph, importer)) {
197
+ try {
198
+ graph.invalidateModule?.(node);
199
+ } catch {
200
+ // A graph shape this structural view mispredicts must not crash the
201
+ // poll; the full-reload below still forces a refetch, and the
202
+ // transform cache's external-input hashes force the recompile.
203
+ }
204
+ }
205
+ }
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Enumerate the server's module graphs: one per environment under the
211
+ * environment API (Vite 6+), otherwise the mixed module graph (Vite 5).
212
+ */
213
+ function selectModuleGraphs(server: ViteDevServerLike): ViteModuleGraphLike[] {
214
+ const graphs: ViteModuleGraphLike[] = [];
215
+ for (const environment of Object.values(server.environments ?? {})) {
216
+ if (environment?.moduleGraph !== undefined) {
217
+ graphs.push(environment.moduleGraph);
218
+ }
219
+ }
220
+ if (graphs.length === 0 && server.moduleGraph !== undefined) {
221
+ graphs.push(server.moduleGraph);
222
+ }
223
+ return graphs;
224
+ }
225
+
226
+ /**
227
+ * Look up the module nodes registered for one importer spelling: the fast
228
+ * slash-normalized `getModulesByFile` lookup first, then an identity scan of
229
+ * `fileToModulesMap` for spellings that differ only by separator or case.
230
+ */
231
+ function selectModulesByFile(
232
+ graph: ViteModuleGraphLike,
233
+ importer: string,
234
+ ): ViteModuleNodeLike[] {
235
+ const direct = graph.getModulesByFile?.(importer.replace(/\\/g, "/"));
236
+ if (direct !== undefined && direct.size !== 0) {
237
+ return [...direct];
238
+ }
239
+ const identity = pathIdentityKey(importer);
240
+ const output: ViteModuleNodeLike[] = [];
241
+ for (const [file, nodes] of graph.fileToModulesMap ?? []) {
242
+ if (typeof file === "string" && pathIdentityKey(file) === identity) {
243
+ output.push(...nodes);
244
+ }
245
+ }
246
+ return output;
247
+ }
248
+
249
+ /**
250
+ * Deliver one full-reload so connected clients refetch the invalidated
251
+ * importers. The channels differ across Vite majors (`ws`, deprecated `hot`,
252
+ * per-environment `hot`); the first one that accepts the payload wins.
253
+ */
254
+ function sendFullReload(server: ViteDevServerLike): void {
255
+ for (const channel of [
256
+ server.ws,
257
+ server.hot,
258
+ server.environments?.client?.hot,
259
+ ]) {
260
+ if (channel?.send === undefined) {
261
+ continue;
262
+ }
263
+ try {
264
+ channel.send({ path: "*", type: "full-reload" });
265
+ return;
266
+ } catch {
267
+ // Try the next channel; an unsupported payload on one major must not
268
+ // suppress delivery through another.
269
+ }
270
+ }
271
+ }