@ttsc/unplugin 0.28.2 → 0.28.4

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 (46) hide show
  1. package/README.md +71 -7
  2. package/lib/api.js +3 -0
  3. package/lib/api.js.map +1 -1
  4. package/lib/api.mjs +1 -0
  5. package/lib/api.mjs.map +1 -1
  6. package/lib/core/index.d.cts +17 -6
  7. package/lib/core/index.d.mts +17 -6
  8. package/lib/core/index.d.ts +17 -6
  9. package/lib/core/index.js +131 -18
  10. package/lib/core/index.js.map +1 -1
  11. package/lib/core/index.mjs +130 -19
  12. package/lib/core/index.mjs.map +1 -1
  13. package/lib/core/transform.d.cts +116 -29
  14. package/lib/core/transform.d.mts +116 -29
  15. package/lib/core/transform.d.ts +116 -29
  16. package/lib/core/transform.js +1502 -222
  17. package/lib/core/transform.js.map +1 -1
  18. package/lib/core/transform.mjs +1503 -223
  19. package/lib/core/transform.mjs.map +1 -1
  20. package/lib/core/tsconfigPaths.d.cts +61 -0
  21. package/lib/core/tsconfigPaths.d.mts +61 -0
  22. package/lib/core/tsconfigPaths.d.ts +61 -0
  23. package/lib/core/tsconfigPaths.js +190 -7
  24. package/lib/core/tsconfigPaths.js.map +1 -1
  25. package/lib/core/tsconfigPaths.mjs +188 -8
  26. package/lib/core/tsconfigPaths.mjs.map +1 -1
  27. package/lib/next.d.cts +27 -10
  28. package/lib/next.d.mts +27 -10
  29. package/lib/next.d.ts +27 -10
  30. package/lib/next.js +229 -8
  31. package/lib/next.js.map +1 -1
  32. package/lib/next.mjs +229 -8
  33. package/lib/next.mjs.map +1 -1
  34. package/lib/turbopack.d.cts +5 -4
  35. package/lib/turbopack.d.mts +5 -4
  36. package/lib/turbopack.d.ts +5 -4
  37. package/lib/turbopack.js +13 -7
  38. package/lib/turbopack.js.map +1 -1
  39. package/lib/turbopack.mjs +14 -8
  40. package/lib/turbopack.mjs.map +1 -1
  41. package/package.json +3 -3
  42. package/src/core/index.ts +136 -18
  43. package/src/core/transform.ts +2070 -254
  44. package/src/core/tsconfigPaths.ts +254 -8
  45. package/src/next.ts +262 -10
  46. package/src/turbopack.ts +13 -9
package/src/core/index.ts CHANGED
@@ -20,9 +20,13 @@ import { createViteServeMissingInputWatch } from "./viteServe";
20
20
 
21
21
  const name = "ttsc-unplugin";
22
22
  /**
23
- * Matches any TypeScript or JavaScript source extension (.ts, .tsx, .mts, .cts,
24
- * etc.). Shared with the Bun adapter (`bun.ts`) so the filter is defined once
25
- * and both adapters stay in sync.
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.
26
+ *
27
+ * Shared with the Bun adapter (`bun.ts`) and the standalone Turbopack loader
28
+ * (`turbopack.ts`) through {@link isTransformTarget}, so the filter is defined
29
+ * once and every adapter answers the same way.
26
30
  */
27
31
  export const sourceFilePattern = /\.[cm]?tsx?$/;
28
32
  /** Matches any path segment that is a `node_modules` directory (cross-platform). */
@@ -37,13 +41,15 @@ const virtualModulePattern = /\0/;
37
41
  * Unplugin factory that wires the ttsc transform pipeline into any supported
38
42
  * bundler (Vite, Rollup, Rolldown, webpack, Rspack, esbuild, Farm).
39
43
  *
40
- * The factory resolves raw options once, creates a per-build transform cache,
41
- * and captures Vite alias configuration via the `vite.configResolved` hook so
42
- * that path aliases are forwarded to the generated tsconfig overlay. Real build
43
- * lifecycles use a per-build cache; a watching Vite development server keeps
44
- * persistent validation because its one `buildStart` spans later HMR edits,
45
- * while a dev server configured without a watcher takes the build-scoped path
46
- * with them, having declared it will observe no edit at all.
44
+ * The factory resolves raw options once, creates one transform cache for the
45
+ * whole plugin instance, and captures Vite alias configuration via the
46
+ * `vite.configResolved` hook so that path aliases are forwarded to the
47
+ * generated tsconfig overlay. A host with a real `buildStart` opens a delivery
48
+ * pass there and keeps its generation across passes; a watching Vite
49
+ * development server keeps persistent validation instead, because its one
50
+ * `buildStart` spans later HMR edits and so cannot mark a pass, while a dev
51
+ * server configured without a watcher takes the pass lifecycle with them,
52
+ * having declared it will observe no edit at all.
47
53
  */
48
54
  const unpluginFactory: UnpluginFactory<
49
55
  TtscUnpluginOptions | undefined,
@@ -55,6 +61,17 @@ const unpluginFactory: UnpluginFactory<
55
61
  let aliases: unknown;
56
62
  let viteCommand: string | undefined;
57
63
  let viteWatching = true;
64
+ // Whether a build-mode session is driven by Rollup's watcher. `build.watch`
65
+ // is `null` for an ordinary build and an object under `--watch`, which is the
66
+ // axis the disposal boundary actually turns on: only a watching build repeats
67
+ // its build phase, and only a watching build ends at `closeWatcher`.
68
+ let viteBuildWatching = false;
69
+ // A restart can start the replacement plugin container before closing the
70
+ // old one, and Vite calls buildEnd even for a container that never started.
71
+ // Track the stable per-container PluginContext identity so that unstarted
72
+ // old containers cannot dispose a replacement's freshly initialized cache.
73
+ let viteBuildOwners = new WeakSet<object>();
74
+ let viteBuildLifecycles = 0;
58
75
 
59
76
  return {
60
77
  name,
@@ -78,6 +95,13 @@ const unpluginFactory: UnpluginFactory<
78
95
  // (samchon/ttsc#1246).
79
96
  viteWatching =
80
97
  (config as { server?: { watch?: unknown } }).server?.watch !== null;
98
+ // Read on the same principle as the line above, from the half of the
99
+ // config that governs a build rather than a server. The comparison is
100
+ // loose where the server's is strict because the two defaults differ:
101
+ // `server.watch` is an object unless explicitly `null`, while
102
+ // `build.watch` is absent or `null` unless `--watch` supplies one.
103
+ viteBuildWatching =
104
+ (config as { build?: { watch?: unknown } }).build?.watch != null;
81
105
  },
82
106
  // Vite serve funnels every transform-context `addWatchFile()` into the
83
107
  // module's added-import graph (`_addedImports`), which import-analysis
@@ -88,14 +112,94 @@ const unpluginFactory: UnpluginFactory<
88
112
  configureServer(server) {
89
113
  missingInputs.attach(server);
90
114
  },
91
- // Vite calls buildEnd when the dev server (or build) closes; drop every
92
- // poller so a stopped server leaks no watch state.
115
+ // Vite calls buildEnd when the dev server closes, and Rollup calls it at
116
+ // the end of every build phase; drop every poller and, once the last
117
+ // overlapping container has closed, every generation-owned filesystem
118
+ // tracker as well.
119
+ //
120
+ // Disposing here is right wherever the end of a build phase is also the
121
+ // end of the session: a dev server, and an ordinary one-shot build. It is
122
+ // wrong for a watching build, whose watcher repeats build phases, so it
123
+ // means "this pass ended" there — measured as
124
+ // `buildStart -> buildEnd -> ... -> buildStart -> buildEnd` across
125
+ // `vite build --watch` rebuilds. Disposing on that repeat discarded the
126
+ // generation once per rebuild independently of the `buildStart` clear, so
127
+ // fixing one of the two sites alone left this host recompiling the whole
128
+ // project per edit (samchon/ttsc#1301). The watching build hands its
129
+ // teardown to `closeWatcher` below instead.
93
130
  buildEnd() {
94
131
  missingInputs.dispose();
132
+ if (viteBuildOwners.delete(this)) {
133
+ viteBuildLifecycles -= 1;
134
+ }
135
+ if (
136
+ viteBuildLifecycles === 0 &&
137
+ (viteCommand === "serve" || !viteBuildWatching)
138
+ ) {
139
+ resetTtscTransformCache(transformCache);
140
+ }
141
+ },
142
+ // The watching build's real teardown, and the only hook in a
143
+ // `vite build --watch` trace that fires exactly once: buildEnd,
144
+ // writeBundle and closeBundle all repeat per rebuild there. A generation
145
+ // retained across passes owns directory watchers, so this is where they
146
+ // are released. Vite's dev server drives no Rollup watcher and an
147
+ // ordinary build closes its bundle instead, so neither reaches here;
148
+ // a host that fired both would simply reset twice, which is idempotent.
149
+ //
150
+ // The container bookkeeping is cleared with the cache, and the owner set
151
+ // is replaced rather than merely zeroed alongside it. A watcher closed
152
+ // mid-rebuild leaves a container still registered, and its later
153
+ // `buildEnd` would then decrement a counter that is already zero and
154
+ // strand it below zero, after which the disposal above could never fire
155
+ // again for this plugin instance.
156
+ closeWatcher() {
157
+ viteBuildOwners = new WeakSet<object>();
158
+ viteBuildLifecycles = 0;
159
+ resetTtscTransformCache(transformCache);
160
+ },
161
+ },
162
+
163
+ // Rollup and Rolldown carry none of the Vite block's hooks, so before this
164
+ // they had no disposal site at all. They get both halves of the same
165
+ // boundary: a watching session ends at `closeWatcher`, and a one-shot build
166
+ // ends when its build phase does. `this.meta.watchMode` separates the two
167
+ // there, the way `build.watch` does for Vite, so a one-shot build is not
168
+ // left without a site the way `vite build` was (samchon/ttsc#1301).
169
+ // unplugin merges each of these blocks only into its own adapter, so the
170
+ // Vite adapter never receives them.
171
+ //
172
+ // A `buildEnd` at the top level instead of inside a block would be a
173
+ // regression rather than a shorthand: unplugin forwards a top-level one to
174
+ // esbuild's `onEnd` and to webpack's and Rspack's `hooks.emit`, each of
175
+ // which repeats per rebuild, so those hosts would start discarding a valid
176
+ // generation on every edit, which is samchon/ttsc#1300 again.
177
+ rollup: {
178
+ buildEnd(this: { meta?: { watchMode?: boolean } }) {
179
+ if (this.meta?.watchMode !== true) {
180
+ resetTtscTransformCache(transformCache);
181
+ }
182
+ },
183
+ closeWatcher() {
184
+ resetTtscTransformCache(transformCache);
185
+ },
186
+ },
187
+ rolldown: {
188
+ buildEnd(this: { meta?: { watchMode?: boolean } }) {
189
+ if (this.meta?.watchMode !== true) {
190
+ resetTtscTransformCache(transformCache);
191
+ }
192
+ },
193
+ closeWatcher() {
194
+ resetTtscTransformCache(transformCache);
95
195
  },
96
196
  },
97
197
 
98
198
  buildStart() {
199
+ if (viteCommand !== undefined && !viteBuildOwners.has(this as object)) {
200
+ viteBuildOwners.add(this as object);
201
+ viteBuildLifecycles += 1;
202
+ }
99
203
  // Persistent validation exists for a session that spans edits it can
100
204
  // observe, and a dev server told to open no watcher is not one:
101
205
  // `server.watch: null` leaves Vite with no change channel at all, so no
@@ -103,14 +207,18 @@ const unpluginFactory: UnpluginFactory<
103
207
  // no client is hot-updated. Validating each delivery there does not buy
104
208
  // freshness, it buys incoherence — modules delivered before an edit and
105
209
  // after it would come from two different compilations of one program —
106
- // while costing a full derived-input proof per delivered module. The
107
- // build-scoped lifecycle settles each module's first delivery against the
108
- // generation the session started from, exactly as a build does, and still
210
+ // while costing a full derived-input proof per delivered module. The pass
211
+ // lifecycle settles each module's first delivery against the generation
212
+ // the session started from, exactly as a build does, and still
109
213
  // revalidates a module this session already delivered. A one-shot suite
110
214
  // configures precisely this server (`vitest --run` sets `server.watch =
111
215
  // null`) and is the workload behind samchon/ttsc#970
112
216
  // (samchon/ttsc#1260). The neighbouring watch-registration decision reads
113
217
  // the same two properties for the same reason.
218
+ //
219
+ // Opening a pass no longer discards the generation, so the `else` branch
220
+ // is what every host with a repeating `buildStart` takes without paying a
221
+ // whole-project transform per rebuild (samchon/ttsc#1300).
114
222
  if (viteCommand === "serve" && viteWatching) {
115
223
  resetTtscTransformCache(transformCache);
116
224
  } else {
@@ -194,6 +302,11 @@ export type {
194
302
  TtscTransformHooks,
195
303
  TtscWatchInputEvidence,
196
304
  } from "./transform";
305
+ export type { ITtscProjectMembershipPolicy } from "./tsconfigPaths";
306
+ export {
307
+ mergeMembershipPolicyOverlay,
308
+ readProjectMembershipPolicy,
309
+ } from "./tsconfigPaths";
197
310
  export {
198
311
  beginTtscTransformBuild,
199
312
  collectExternalInputHashes,
@@ -209,10 +322,15 @@ export {
209
322
  export default unplugin;
210
323
 
211
324
  /**
212
- * Returns `true` when the module id refers to a real TypeScript/JavaScript
213
- * source file that should be processed by the ttsc transform.
325
+ * Returns `true` when the module id refers to a real TypeScript source file
326
+ * that should be processed by the ttsc transform.
327
+ *
328
+ * TypeScript only. {@link sourceFilePattern} deliberately excludes JavaScript,
329
+ * so a `.js` module reaches no adapter's transform, and this docstring used to
330
+ * say otherwise while the pattern it is built from said the truth
331
+ * (samchon/ttsc#1309).
214
332
  *
215
- * Excluded ids: virtual modules (NUL prefix), `.d.ts` declaration files, and
333
+ * Also excluded: virtual modules (NUL prefix), `.d.ts` declaration files, and
216
334
  * anything inside `node_modules`.
217
335
  */
218
336
  export function isTransformTarget(id: string): boolean {