@ttsc/unplugin 0.28.5 → 0.29.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 (86) hide show
  1. package/README.md +15 -11
  2. package/lib/api.js +10 -0
  3. package/lib/api.js.map +1 -1
  4. package/lib/api.mjs +3 -2
  5. package/lib/api.mjs.map +1 -1
  6. package/lib/bun-register.d.cts +10 -7
  7. package/lib/bun-register.d.mts +10 -7
  8. package/lib/bun-register.d.ts +10 -7
  9. package/lib/bun-register.js +90 -24
  10. package/lib/bun-register.js.map +1 -1
  11. package/lib/bun-register.mjs +90 -24
  12. package/lib/bun-register.mjs.map +1 -1
  13. package/lib/bun.d.cts +10 -8
  14. package/lib/bun.d.mts +10 -8
  15. package/lib/bun.d.ts +10 -8
  16. package/lib/bun.js +19 -17
  17. package/lib/bun.js.map +1 -1
  18. package/lib/bun.mjs +20 -18
  19. package/lib/bun.mjs.map +1 -1
  20. package/lib/core/index.d.cts +10 -8
  21. package/lib/core/index.d.mts +10 -8
  22. package/lib/core/index.d.ts +10 -8
  23. package/lib/core/index.js +88 -16
  24. package/lib/core/index.js.map +1 -1
  25. package/lib/core/index.mjs +81 -18
  26. package/lib/core/index.mjs.map +1 -1
  27. package/lib/core/projectDiscovery.d.cts +55 -0
  28. package/lib/core/projectDiscovery.d.mts +55 -0
  29. package/lib/core/projectDiscovery.d.ts +55 -0
  30. package/lib/core/projectDiscovery.js +187 -0
  31. package/lib/core/projectDiscovery.js.map +1 -0
  32. package/lib/core/projectDiscovery.mjs +182 -0
  33. package/lib/core/projectDiscovery.mjs.map +1 -0
  34. package/lib/core/sourceExtensions.d.cts +10 -0
  35. package/lib/core/sourceExtensions.d.mts +10 -0
  36. package/lib/core/sourceExtensions.d.ts +10 -0
  37. package/lib/core/sourceExtensions.js +38 -0
  38. package/lib/core/sourceExtensions.js.map +1 -0
  39. package/lib/core/sourceExtensions.mjs +32 -0
  40. package/lib/core/sourceExtensions.mjs.map +1 -0
  41. package/lib/core/transform.d.cts +96 -16
  42. package/lib/core/transform.d.mts +96 -16
  43. package/lib/core/transform.d.ts +96 -16
  44. package/lib/core/transform.js +1295 -309
  45. package/lib/core/transform.js.map +1 -1
  46. package/lib/core/transform.mjs +1290 -310
  47. package/lib/core/transform.mjs.map +1 -1
  48. package/lib/core/tsconfigPaths.d.cts +55 -3
  49. package/lib/core/tsconfigPaths.d.mts +55 -3
  50. package/lib/core/tsconfigPaths.d.ts +55 -3
  51. package/lib/core/tsconfigPaths.js +260 -46
  52. package/lib/core/tsconfigPaths.js.map +1 -1
  53. package/lib/core/tsconfigPaths.mjs +255 -47
  54. package/lib/core/tsconfigPaths.mjs.map +1 -1
  55. package/lib/core/viteServe.d.cts +18 -21
  56. package/lib/core/viteServe.d.mts +18 -21
  57. package/lib/core/viteServe.d.ts +18 -21
  58. package/lib/core/viteServe.js +88 -47
  59. package/lib/core/viteServe.js.map +1 -1
  60. package/lib/core/viteServe.mjs +89 -49
  61. package/lib/core/viteServe.mjs.map +1 -1
  62. package/lib/next.d.cts +7 -0
  63. package/lib/next.d.mts +7 -0
  64. package/lib/next.d.ts +7 -0
  65. package/lib/next.js +186 -54
  66. package/lib/next.js.map +1 -1
  67. package/lib/next.mjs +186 -55
  68. package/lib/next.mjs.map +1 -1
  69. package/lib/turbopack.d.cts +6 -4
  70. package/lib/turbopack.d.mts +6 -4
  71. package/lib/turbopack.d.ts +6 -4
  72. package/lib/turbopack.js +11 -10
  73. package/lib/turbopack.js.map +1 -1
  74. package/lib/turbopack.mjs +11 -10
  75. package/lib/turbopack.mjs.map +1 -1
  76. package/package.json +7 -4
  77. package/src/bun-register.ts +112 -25
  78. package/src/bun.ts +60 -50
  79. package/src/core/index.ts +114 -19
  80. package/src/core/projectDiscovery.ts +256 -0
  81. package/src/core/sourceExtensions.ts +47 -0
  82. package/src/core/transform.ts +1671 -361
  83. package/src/core/tsconfigPaths.ts +406 -46
  84. package/src/core/viteServe.ts +125 -74
  85. package/src/next.ts +230 -54
  86. package/src/turbopack.ts +11 -10
@@ -1,3 +1,5 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+
1
3
  import bun, { type BunLikePlugin } from "./bun";
2
4
  import type { TtscUnpluginOptions } from "./core/options";
3
5
 
@@ -12,20 +14,29 @@ interface BunRuntimeGlobal {
12
14
  plugin(plugin: BunLikePlugin): void;
13
15
  }
14
16
 
17
+ interface BunRegistrationState {
18
+ activeOptions: TtscUnpluginOptions | undefined;
19
+ lockedOptions: TtscUnpluginOptions | undefined;
20
+ optionsLocked: boolean;
21
+ registered: boolean;
22
+ }
23
+
15
24
  /**
16
- * Effective options for the single runtime loader.
25
+ * Pending and locked options for the single runtime loader.
17
26
  *
18
27
  * Bun uses the first matching `onLoad` hook and does not fall through to a
19
28
  * later overlapping plugin (oven-sh/bun#20583). Registering twice — once
20
29
  * implicitly on import, once explicitly — would let the default loader shadow
21
- * the configured one. Instead exactly one Bun plugin is registered; it resolves
22
- * these options lazily on first load (see {@link bun}'s provider form), so an
23
- * explicit `register(options)` made right after importing this module overrides
24
- * the preload defaults without a second shadowing registration.
30
+ * the configured one. Instead exactly one Bun plugin is registered. Calls made
31
+ * before its first load replace this detached snapshot; entering the first
32
+ * transformable load locks that value synchronously for the process's immutable
33
+ * module-loading session. State is keyed by the Bun runtime in a global weak
34
+ * map so loading both emitted module conditions cannot install two overlapping
35
+ * loaders.
25
36
  */
26
- let activeOptions: TtscUnpluginOptions | undefined;
27
- /** Whether the single runtime loader has already been registered with Bun. */
28
- let registered = false;
37
+ const BUN_REGISTRATION_STATES = Symbol.for(
38
+ "@ttsc/unplugin/bun-register/states/v1",
39
+ );
29
40
 
30
41
  /**
31
42
  * Register the ttsc transform as a Bun **runtime** plugin.
@@ -39,14 +50,17 @@ let registered = false;
39
50
  * "@ttsc/unplugin/bun-register"`. Options are read from the nearest
40
51
  * `tsconfig.json`, identical to the bundler adapters.
41
52
  *
42
- * Registration is idempotent: the first call (implicit on import, or explicit)
43
- * registers the one loader with Bun; later calls only update the effective
44
- * options, so accessing the explicit API cannot install a second default loader
45
- * that shadows the caller's configuration. Repeated explicit calls are
46
- * last-write-wins for the effective options.
53
+ * The first call registers one loader. Calls before its first transformable
54
+ * TypeScript load use last-call-wins and capture options by value, so an
55
+ * explicit call right after importing this module replaces the preload defaults
56
+ * without installing a shadowing loader. Entering the first such load locks
57
+ * that snapshot synchronously. Later calls with a structurally identical value
58
+ * are idempotent; a different value throws rather than pretending a resolved
59
+ * loader changed configuration.
47
60
  *
48
- * @throws When called explicitly off the Bun runtime (`globalThis.Bun.plugin`
49
- * is unavailable). The auto-registration below stays silent off Bun so the
61
+ * @throws When called explicitly off the Bun runtime, when options are not
62
+ * structured-cloneable, or when a different option value is supplied after
63
+ * the first load. The auto-registration below stays silent off Bun so the
50
64
  * module is harmless to import from Node (tests, tooling).
51
65
  */
52
66
  export function register(options?: TtscUnpluginOptions): void {
@@ -58,15 +72,61 @@ export function register(options?: TtscUnpluginOptions): void {
58
72
  "@ttsc/unplugin/vite for non-Bun toolchains.",
59
73
  );
60
74
  }
61
- activeOptions = options;
62
- if (registered) {
63
- return;
75
+ const state = registrationState(runtime);
76
+ const snapshot = snapshotOptions(options);
77
+ if (state.optionsLocked) {
78
+ if (isDeepStrictEqual(snapshot, state.lockedOptions)) {
79
+ return;
80
+ }
81
+ throw new Error(
82
+ "@ttsc/unplugin/bun-register options are locked because the runtime " +
83
+ "loader has started handling a TypeScript module. Restart the Bun " +
84
+ "process to use different compiler or plugin options.",
85
+ );
86
+ }
87
+ state.activeOptions = snapshot;
88
+ ensureRegistered(runtime, state);
89
+ }
90
+
91
+ /** Install the runtime's one loader without changing its pending options. */
92
+ function ensureRegistered(
93
+ runtime: BunRuntimeGlobal,
94
+ state: BunRegistrationState,
95
+ ): void {
96
+ if (state.registered) return;
97
+ state.registered = true;
98
+ try {
99
+ runtime.plugin(bun(() => lockOptions(state)));
100
+ } catch (error) {
101
+ state.registered = false;
102
+ throw error;
103
+ }
104
+ }
105
+
106
+ /** Lock and return the detached option snapshot at first load-handler entry. */
107
+ function lockOptions(
108
+ state: BunRegistrationState,
109
+ ): TtscUnpluginOptions | undefined {
110
+ if (!state.optionsLocked) {
111
+ state.lockedOptions = state.activeOptions;
112
+ state.optionsLocked = true;
113
+ }
114
+ return state.lockedOptions;
115
+ }
116
+
117
+ /** Detach JSON-shaped options from mutations made after `register` returns. */
118
+ function snapshotOptions(
119
+ options: TtscUnpluginOptions | undefined,
120
+ ): TtscUnpluginOptions | undefined {
121
+ if (options === undefined) return undefined;
122
+ try {
123
+ return structuredClone(options);
124
+ } catch (cause) {
125
+ throw new TypeError(
126
+ "@ttsc/unplugin/bun-register options must contain structured-cloneable values.",
127
+ { cause },
128
+ );
64
129
  }
65
- registered = true;
66
- // Register a single loader whose options are read from `activeOptions` on
67
- // first load, so an explicit call made after the import-time auto-register
68
- // still wins.
69
- runtime.plugin(bun(() => activeOptions));
70
130
  }
71
131
 
72
132
  function bunRuntime(): BunRuntimeGlobal | undefined {
@@ -76,11 +136,38 @@ function bunRuntime(): BunRuntimeGlobal | undefined {
76
136
  : undefined;
77
137
  }
78
138
 
139
+ /** Resolve the shared state owned by one concrete Bun runtime object. */
140
+ function registrationState(runtime: BunRuntimeGlobal): BunRegistrationState {
141
+ const holder = globalThis as unknown as Record<PropertyKey, unknown>;
142
+ let states = holder[BUN_REGISTRATION_STATES];
143
+ if (!(states instanceof WeakMap)) {
144
+ states = new WeakMap<object, BunRegistrationState>();
145
+ Object.defineProperty(holder, BUN_REGISTRATION_STATES, {
146
+ configurable: false,
147
+ enumerable: false,
148
+ value: states,
149
+ writable: false,
150
+ });
151
+ }
152
+ const registrations = states as WeakMap<object, BunRegistrationState>;
153
+ const existing = registrations.get(runtime);
154
+ if (existing !== undefined) return existing;
155
+ const created: BunRegistrationState = {
156
+ activeOptions: undefined,
157
+ lockedOptions: undefined,
158
+ optionsLocked: false,
159
+ registered: false,
160
+ };
161
+ registrations.set(runtime, created);
162
+ return created;
163
+ }
164
+
79
165
  // Auto-register on import so a `bunfig.toml` `preload` entry — which only
80
166
  // imports the module — takes effect. Guarded so importing from Node (a stray
81
167
  // import, or a unit test) is a harmless no-op rather than a throw.
82
- if (bunRuntime() !== undefined) {
83
- register();
168
+ const runtime = bunRuntime();
169
+ if (runtime !== undefined) {
170
+ ensureRegistered(runtime, registrationState(runtime));
84
171
  }
85
172
 
86
173
  export default register;
package/src/bun.ts CHANGED
@@ -4,18 +4,15 @@ import {
4
4
  beginTtscTransformBuild,
5
5
  createTtscTransformCache,
6
6
  isTransformTarget,
7
+ resetTtscTransformCache,
7
8
  resolveOptions,
8
9
  transformTtsc,
9
10
  } from "./core/index";
10
11
  import type { TtscUnpluginOptions } from "./core/options";
11
-
12
- /**
13
- * Bun normally reports filesystem source paths, while plugin-created virtual
14
- * ids may contain a NUL sentinel. A virtual id must stay with the plugin that
15
- * created it: claiming it here and trying to read it from disk prevents that
16
- * plugin's later loader from running.
17
- */
18
- const bunSourceFilePattern = /^[^\x00]*\.[cm]?tsx?$/;
12
+ import {
13
+ bunTypeScriptTransformSourcePattern,
14
+ typescriptTransformBunLoader,
15
+ } from "./core/sourceExtensions";
19
16
 
20
17
  /**
21
18
  * Minimal subset of the Bun plugin API consumed by this adapter.
@@ -40,10 +37,10 @@ export type BunLoader = "ts" | "tsx";
40
37
  *
41
38
  * The provider form exists for the runtime registration path (`bun-register`),
42
39
  * where a single Bun plugin is registered on import but its effective options
43
- * may be overridden by an explicit `register(options)` call made in the same
44
- * synchronous tick. Resolving through the provider on first load, rather than
45
- * at registration, lets that later call win without Bun ever seeing a second
46
- * shadowing loader.
40
+ * may be overridden by explicit `register(options)` calls made before the first
41
+ * transformable TypeScript load. Resolving through the provider on that first
42
+ * load, rather than at registration, lets the last pending call win without Bun
43
+ * ever seeing a second shadowing loader.
47
44
  */
48
45
  export type TtscBunOptions =
49
46
  | TtscUnpluginOptions
@@ -75,10 +72,10 @@ function resolveBunOptions(
75
72
  /**
76
73
  * Minimal subset of the Bun `BuildConfig` plugin build object.
77
74
  *
78
- * `onLoad` drives the source transform. Bun's bundler also exposes `onStart`,
79
- * which is used when available to forward the shared plugin's build lifecycle
80
- * and clear its per-build cache. The runtime plugin API omits that hook, so
81
- * plugin setup itself starts its one process-scoped module-loading session.
75
+ * `onLoad` drives the source transform. Bun's bundler also exposes `onStart`
76
+ * and `onEnd`, which bracket the shared plugin's build lifecycle. The runtime
77
+ * plugin API omits those hooks, so plugin setup itself starts its one
78
+ * process-scoped module-loading session.
82
79
  */
83
80
  export interface BunLikeBuild {
84
81
  /**
@@ -97,6 +94,8 @@ export interface BunLikeBuild {
97
94
  * Optional because `Bun.plugin()` runtime builders do not expose this hook.
98
95
  */
99
96
  onStart?(callback: () => void | Promise<void>): void;
97
+ /** Register a callback for deterministic bundler-session teardown. */
98
+ onEnd?(callback: () => void | Promise<void>): void;
100
99
  /**
101
100
  * Register a loader callback for files matching `filter`.
102
101
  *
@@ -141,10 +140,10 @@ export default function bun(options?: TtscBunOptions): BunLikePlugin {
141
140
  return {
142
141
  name: "ttsc-unplugin",
143
142
  setup(build) {
144
- // Resolve options lazily on first load. Runtime registration may call
145
- // register(options) immediately after the import-time default
146
- // registration; the provider form must observe that last synchronous
147
- // update without installing a second shadowing loader.
143
+ // Resolve options lazily on the first transformable TypeScript load.
144
+ // Runtime registration may replace its pending configuration at any time
145
+ // before then; the provider form must observe that last update without
146
+ // installing a second shadowing loader.
148
147
  let resolved: ReturnType<typeof resolveOptions> | undefined;
149
148
  const getOptions = () =>
150
149
  (resolved ??= resolveOptions(resolveBunOptions(options)));
@@ -159,43 +158,54 @@ export default function bun(options?: TtscBunOptions): BunLikePlugin {
159
158
  // repeats it for subsequent builds.
160
159
  beginTtscTransformBuild(cache);
161
160
  build.onStart?.(() => beginTtscTransformBuild(cache));
162
- build.onLoad({ filter: bunSourceFilePattern }, async (args) => {
163
- if (!runtime && ownsInMemoryFile(args.path)) {
164
- return undefined;
165
- }
166
- if (!isTransformTarget(args.path)) {
167
- if (!runtime) return undefined;
168
- return {
169
- contents: await fs.readFile(args.path, "utf8"),
170
- loader: bunLoaderFor(args.path),
171
- };
172
- }
173
- const loader = bunLoaderFor(args.path);
174
- const source = await fs.readFile(args.path, "utf8");
175
- const result = await transformTtsc(
176
- args.path,
177
- source,
178
- getOptions(),
179
- undefined,
180
- cache,
181
- bunTransformHooks,
182
- );
183
- if (result !== undefined) {
184
- return { contents: result.code, loader };
185
- }
186
- return runtime ? { contents: source, loader } : undefined;
187
- });
161
+ if (!runtime) {
162
+ build.onEnd?.(() => resetTtscTransformCache(cache));
163
+ }
164
+ build.onLoad(
165
+ { filter: bunTypeScriptTransformSourcePattern },
166
+ async (args) => {
167
+ if (!runtime && ownsInMemoryFile(args.path)) {
168
+ return undefined;
169
+ }
170
+ if (!isTransformTarget(args.path)) {
171
+ if (!runtime) return undefined;
172
+ return {
173
+ contents: await fs.readFile(args.path, "utf8"),
174
+ loader: bunLoaderFor(args.path),
175
+ };
176
+ }
177
+ const loader = bunLoaderFor(args.path);
178
+ const transformOptions = getOptions();
179
+ const source = await fs.readFile(args.path, "utf8");
180
+ const result = await transformTtsc(
181
+ args.path,
182
+ source,
183
+ transformOptions,
184
+ undefined,
185
+ cache,
186
+ bunTransformHooks,
187
+ );
188
+ if (result !== undefined) {
189
+ return { contents: result.code, loader };
190
+ }
191
+ return runtime ? { contents: source, loader } : undefined;
192
+ },
193
+ );
188
194
  },
189
195
  };
190
196
  }
191
197
 
192
198
  /**
193
- * Pick the Bun loader for a matched file. `bunSourceFilePattern` is
194
- * `/\.[cm]?tsx?$/`, so a trailing `x` (`.tsx`/`.ctsx`/`.mtsx`) is JSX-flavored
195
- * TypeScript and everything else (`.ts`/`.cts`/`.mts`) is plain TypeScript.
199
+ * Pick the Bun loader recorded beside the matched extension in the shared
200
+ * source table. Reaching this function without a table entry would mean Bun
201
+ * invoked a callback whose registration filter did not match.
196
202
  */
197
203
  function bunLoaderFor(filePath: string): BunLoader {
198
- return /x$/i.test(filePath) ? "tsx" : "ts";
204
+ const loader = typescriptTransformBunLoader(filePath);
205
+ if (loader === undefined) {
206
+ throw new Error(`Bun delivered an unsupported source path: ${filePath}`);
207
+ }
208
+ return loader;
199
209
  }
200
210
 
201
211
  /**
package/src/core/index.ts CHANGED
@@ -5,30 +5,36 @@ import { createUnplugin } from "unplugin";
5
5
 
6
6
  import type { TtscUnpluginOptions } from "./options";
7
7
  import { resolveOptions } from "./options";
8
+ import { typescriptTransformSourcePattern } from "./sourceExtensions";
8
9
  import {
9
10
  beginTtscTransformBuild,
11
+ captureWatchInputBaseline,
12
+ captureWatchInputFileBaseline,
10
13
  collectExternalInputHashes,
14
+ collectProjectInputHashSnapshot,
11
15
  collectProjectInputHashes,
12
16
  createTtscTransformCache,
13
17
  isDeclarationFile,
14
18
  isProjectWalkPath,
19
+ isWatchInputKeyBaseline,
15
20
  resetTtscTransformCache,
16
21
  stripQuery,
17
22
  transformTtsc,
23
+ watchInputEvidenceMatchesBaseline,
18
24
  } from "./transform";
19
25
  import { createViteServeMissingInputWatch } from "./viteServe";
20
26
 
21
27
  const name = "ttsc-unplugin";
22
28
  /**
23
- * Matches the TypeScript source extensions the ttsc transform handles: `.ts`,
24
- * `.tsx`, `.mts`, `.cts` and their `x` forms. JavaScript is deliberately not
25
- * among them, so a `.js` module reaches no adapter's transform.
29
+ * Matches the exact TypeScript source extensions the ttsc transform handles:
30
+ * `.ts`, `.tsx`, `.mts`, and `.cts`. JavaScript and invented extension forms
31
+ * such as `.mtsx` are deliberately excluded.
26
32
  *
27
33
  * Shared with the Bun adapter (`bun.ts`) and the standalone Turbopack loader
28
34
  * (`turbopack.ts`) through {@link isTransformTarget}, so the filter is defined
29
35
  * once and every adapter answers the same way.
30
36
  */
31
- export const sourceFilePattern = /\.[cm]?tsx?$/;
37
+ export const sourceFilePattern = typescriptTransformSourcePattern;
32
38
  /** Matches any path segment that is a `node_modules` directory (cross-platform). */
33
39
  const nodeModulesPattern = /(?:^|[/\\])node_modules(?:[/\\]|$)/;
34
40
  /**
@@ -72,6 +78,13 @@ const unpluginFactory: UnpluginFactory<
72
78
  // old containers cannot dispose a replacement's freshly initialized cache.
73
79
  let viteBuildOwners = new WeakSet<object>();
74
80
  let viteBuildLifecycles = 0;
81
+ // esbuild schedules one-shot onDispose callbacks after it settles the build
82
+ // Promise. Acquire ownership only at onStart: plugin setup runs before build
83
+ // option validation, and a validation failure has no onDispose callback with
84
+ // which to release a setup-time owner. Once a build has actually started, the
85
+ // count keeps an older delayed callback from disposing its active generation.
86
+ const esbuildOwners = new WeakSet<object>();
87
+ let esbuildLifecycles = 0;
75
88
 
76
89
  return {
77
90
  name,
@@ -128,15 +141,14 @@ const unpluginFactory: UnpluginFactory<
128
141
  // project per edit (samchon/ttsc#1301). The watching build hands its
129
142
  // teardown to `closeWatcher` below instead.
130
143
  buildEnd() {
131
- missingInputs.dispose();
132
144
  if (viteBuildOwners.delete(this)) {
133
145
  viteBuildLifecycles -= 1;
134
146
  }
135
- if (
136
- viteBuildLifecycles === 0 &&
137
- (viteCommand === "serve" || !viteBuildWatching)
138
- ) {
139
- resetTtscTransformCache(transformCache);
147
+ if (viteBuildLifecycles === 0) {
148
+ missingInputs.dispose();
149
+ if (viteCommand === "serve" || !viteBuildWatching) {
150
+ resetTtscTransformCache(transformCache);
151
+ }
140
152
  }
141
153
  },
142
154
  // The watching build's real teardown, and the only hook in a
@@ -156,6 +168,7 @@ const unpluginFactory: UnpluginFactory<
156
168
  closeWatcher() {
157
169
  viteBuildOwners = new WeakSet<object>();
158
170
  viteBuildLifecycles = 0;
171
+ missingInputs.dispose();
159
172
  resetTtscTransformCache(transformCache);
160
173
  },
161
174
  },
@@ -195,6 +208,39 @@ const unpluginFactory: UnpluginFactory<
195
208
  },
196
209
  },
197
210
 
211
+ // These hosts map a top-level buildEnd to a per-compilation hook, so use
212
+ // their true compiler or context teardown instead. The custom callbacks
213
+ // are installed by unplugin alongside its ordinary transform wiring.
214
+ webpack(compiler) {
215
+ compiler.hooks.shutdown.tap(name, () => {
216
+ resetTtscTransformCache(transformCache);
217
+ });
218
+ },
219
+ rspack(compiler) {
220
+ compiler.hooks.shutdown.tap(name, () => {
221
+ resetTtscTransformCache(transformCache);
222
+ });
223
+ },
224
+ esbuild: {
225
+ setup(build) {
226
+ build.onStart(() => {
227
+ if (!esbuildOwners.has(build)) {
228
+ esbuildOwners.add(build);
229
+ esbuildLifecycles += 1;
230
+ }
231
+ });
232
+ build.onDispose(() => {
233
+ if (!esbuildOwners.delete(build)) {
234
+ return;
235
+ }
236
+ esbuildLifecycles -= 1;
237
+ if (esbuildLifecycles === 0) {
238
+ resetTtscTransformCache(transformCache);
239
+ }
240
+ });
241
+ },
242
+ },
243
+
198
244
  buildStart() {
199
245
  if (viteCommand !== undefined && !viteBuildOwners.has(this as object)) {
200
246
  viteBuildOwners.add(this as object);
@@ -242,22 +288,45 @@ const unpluginFactory: UnpluginFactory<
242
288
  // invalidate this module in watch mode and persistent caches;
243
289
  // bundlers erase type-only imports from their own module graph and
244
290
  // would otherwise serve stale generated code. Under Vite serve a
245
- // missing input must not enter `addWatchFile`: import-analysis
246
- // resolves added imports and 500s on a path that is absent by design
247
- // (a superseding resolution candidate, a not-yet-generated
248
- // dependency), so those are watched on the filesystem instead and
249
- // invalidate this module when created.
291
+ // resolver input that is not proven to be a file must not enter
292
+ // `addWatchFile`: import-analysis resolves added imports and 500s on
293
+ // missing paths and directories, so those are watched against their
294
+ // compiler predicates instead and invalidate this module when the
295
+ // observation changes.
250
296
  addWatchFile: (watched, evidence) => {
251
297
  if (viteCommand === "serve" && missingInputs.serving()) {
298
+ const observation =
299
+ evidence?.state?.codec === "predicates"
300
+ ? evidence.state.observation
301
+ : undefined;
302
+ const unsafePredicate =
303
+ observation !== undefined &&
304
+ observation.fileExists !== true &&
305
+ observation.stat !== "file" &&
306
+ observation.readFile?.ok !== true
307
+ ? observation
308
+ : undefined;
309
+ if (unsafePredicate !== undefined) {
310
+ missingInputs.watch(watched, path.resolve(file), unsafePredicate);
311
+ return;
312
+ }
252
313
  // Trust the generation's recorded existence when it supplied one:
253
314
  // every cache hit revalidates it, and probing each input again
254
315
  // costs one `existsSync` per input per delivered module.
255
- const missing = evidence?.missing ?? !fs.existsSync(watched);
256
- if (missing) {
316
+ const unavailable =
317
+ evidence?.unavailable ??
318
+ (evidence === undefined
319
+ ? !fs.existsSync(watched)
320
+ ? "missing"
321
+ : undefined
322
+ : evidence.missing
323
+ ? "missing"
324
+ : undefined);
325
+ if (unavailable !== undefined) {
257
326
  missingInputs.watch(
258
327
  watched,
259
328
  path.resolve(file),
260
- evidence?.identity,
329
+ unavailable === "not-file" ? "file" : "exists",
261
330
  );
262
331
  return;
263
332
  }
@@ -298,26 +367,52 @@ export type {
298
367
  TtscUnpluginOptions,
299
368
  } from "./options";
300
369
  export type {
370
+ TtscProjectDiscoveryFilesystem,
371
+ TtscProjectTreeDiscoveryFilesystem,
372
+ TtscProjectTsconfigCandidate,
373
+ TtscProjectTsconfigDiscovery,
374
+ } from "./projectDiscovery";
375
+ export type {
376
+ TtscProjectInputHashSnapshot,
301
377
  TtscTransformFilesystemOperations,
302
378
  TtscTransformHooks,
379
+ TtscWatchInput,
380
+ TtscWatchInputBaseline,
303
381
  TtscWatchInputEvidence,
382
+ TtscWatchInputFileBaseline,
383
+ TtscWatchInputKeyBaseline,
384
+ TtscWatchInputState,
304
385
  } from "./transform";
305
- export type { ITtscProjectMembershipPolicy } from "./tsconfigPaths";
386
+ export type {
387
+ ITsconfigSourceSnapshotEntry,
388
+ ITtscProjectMembershipPolicy,
389
+ } from "./tsconfigPaths";
306
390
  export {
307
391
  mergeMembershipPolicyOverlay,
308
392
  readProjectMembershipPolicy,
393
+ readTsconfigSourceSnapshot,
309
394
  } from "./tsconfigPaths";
310
395
  export {
311
396
  beginTtscTransformBuild,
397
+ captureWatchInputBaseline,
398
+ captureWatchInputFileBaseline,
312
399
  collectExternalInputHashes,
400
+ collectProjectInputHashSnapshot,
313
401
  collectProjectInputHashes,
314
402
  createTtscTransformCache,
315
403
  isProjectWalkPath,
404
+ isWatchInputKeyBaseline,
316
405
  resetTtscTransformCache,
317
406
  resolveOptions,
318
407
  transformTtsc,
319
408
  unplugin,
409
+ watchInputEvidenceMatchesBaseline,
320
410
  };
411
+ export {
412
+ discoverNearestProjectTsconfig,
413
+ findNearestProjectTsconfig,
414
+ findProjectTsconfigs,
415
+ } from "./projectDiscovery";
321
416
 
322
417
  export default unplugin;
323
418