@ttsc/unplugin 0.30.2 → 0.30.3

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/src/core/index.ts CHANGED
@@ -1,8 +1,7 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
1
  import type { UnpluginFactory, UnpluginInstance } from "unplugin";
4
2
  import { createUnplugin } from "unplugin";
5
3
 
4
+ import { createEsbuildOptions } from "./esbuild";
6
5
  import type { TtscUnpluginOptions } from "./options";
7
6
  import { resolveOptions } from "./options";
8
7
  import { typescriptTransformSourcePattern } from "./sourceExtensions";
@@ -22,7 +21,7 @@ import {
22
21
  transformTtsc,
23
22
  watchInputEvidenceMatchesBaseline,
24
23
  } from "./transform";
25
- import { createViteServeMissingInputWatch } from "./viteServe";
24
+ import { createViteServeInputWatch } from "./viteServe";
26
25
 
27
26
  const name = "ttsc-unplugin";
28
27
  /**
@@ -60,10 +59,13 @@ const virtualModulePattern = /\0/;
60
59
  const unpluginFactory: UnpluginFactory<
61
60
  TtscUnpluginOptions | undefined,
62
61
  false
63
- > = (rawOptions = {}) => {
62
+ > = (rawOptions = {}, meta) => {
64
63
  const options = resolveOptions(rawOptions);
64
+ if (meta.framework === "esbuild") {
65
+ return createEsbuildOptions(options, isTransformTarget);
66
+ }
65
67
  const transformCache = createTtscTransformCache();
66
- const missingInputs = createViteServeMissingInputWatch();
68
+ const serveInputs = createViteServeInputWatch();
67
69
  let aliases: unknown;
68
70
  let viteCommand: string | undefined;
69
71
  let viteWatching = true;
@@ -78,13 +80,6 @@ const unpluginFactory: UnpluginFactory<
78
80
  // old containers cannot dispose a replacement's freshly initialized cache.
79
81
  let viteBuildOwners = new WeakSet<object>();
80
82
  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;
88
83
 
89
84
  return {
90
85
  name,
@@ -116,14 +111,11 @@ const unpluginFactory: UnpluginFactory<
116
111
  viteBuildWatching =
117
112
  (config as { build?: { watch?: unknown } }).build?.watch != null;
118
113
  },
119
- // Vite serve funnels every transform-context `addWatchFile()` into the
120
- // module's added-import graph (`_addedImports`), which import-analysis
121
- // resolves like real imports. Capture the dev server so the transform
122
- // hook can route watch inputs that do not exist yet — superseding
123
- // resolution candidates above all — around that graph and still
124
- // invalidate their importers when the path is created.
114
+ // Compiler dependencies belong to the filesystem watch graph. Vite's
115
+ // transform-context addWatchFile also inserts runtime imports, so none
116
+ // of those dependencies may use that channel during serve (#1368).
125
117
  configureServer(server) {
126
- missingInputs.attach(server);
118
+ serveInputs.attach(server);
127
119
  },
128
120
  // Vite calls buildEnd when the dev server closes, and Rollup calls it at
129
121
  // the end of every build phase; drop every poller and, once the last
@@ -140,15 +132,15 @@ const unpluginFactory: UnpluginFactory<
140
132
  // fixing one of the two sites alone left this host recompiling the whole
141
133
  // project per edit (samchon/ttsc#1301). The watching build hands its
142
134
  // teardown to `closeWatcher` below instead.
143
- buildEnd() {
135
+ async buildEnd() {
144
136
  if (viteBuildOwners.delete(this)) {
145
137
  viteBuildLifecycles -= 1;
146
138
  }
147
139
  if (viteBuildLifecycles === 0) {
148
- missingInputs.dispose();
149
140
  if (viteCommand === "serve" || !viteBuildWatching) {
150
141
  resetTtscTransformCache(transformCache);
151
142
  }
143
+ await serveInputs.dispose();
152
144
  }
153
145
  },
154
146
  // The watching build's real teardown, and the only hook in a
@@ -165,11 +157,11 @@ const unpluginFactory: UnpluginFactory<
165
157
  // `buildEnd` would then decrement a counter that is already zero and
166
158
  // strand it below zero, after which the disposal above could never fire
167
159
  // again for this plugin instance.
168
- closeWatcher() {
160
+ async closeWatcher() {
169
161
  viteBuildOwners = new WeakSet<object>();
170
162
  viteBuildLifecycles = 0;
171
- missingInputs.dispose();
172
163
  resetTtscTransformCache(transformCache);
164
+ await serveInputs.dispose();
173
165
  },
174
166
  },
175
167
 
@@ -221,26 +213,16 @@ const unpluginFactory: UnpluginFactory<
221
213
  resetTtscTransformCache(transformCache);
222
214
  });
223
215
  },
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
- });
216
+ farm: {
217
+ // Farm calls buildStart only for the initial compilation. Every update
218
+ // opens a new pass so a failed verdict can recover, while an unchanged
219
+ // successful generation remains reusable across its module deliveries.
220
+ updateModules: {
221
+ executor() {
222
+ beginTtscTransformBuild(transformCache);
223
+ },
241
224
  },
242
225
  },
243
-
244
226
  buildStart() {
245
227
  if (viteCommand !== undefined && !viteBuildOwners.has(this as object)) {
246
228
  viteBuildOwners.add(this as object);
@@ -283,65 +265,34 @@ const unpluginFactory: UnpluginFactory<
283
265
  return undefined;
284
266
  }
285
267
  return transformTtsc(file, source, options, aliases, transformCache, {
286
- // Register the derived watch inputs (plugin-reported `dependencies`
287
- // unioned with the host-owned reference graph) so type-only inputs
288
- // invalidate this module in watch mode and persistent caches;
289
- // bundlers erase type-only imports from their own module graph and
290
- // would otherwise serve stale generated code. Under Vite serve a
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.
296
- addWatchFile: (watched, evidence) => {
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
- }
313
- // Trust the generation's recorded existence when it supplied one:
314
- // every cache hit revalidates it, and probing each input again
315
- // costs one `existsSync` per input per delivered module.
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) {
326
- missingInputs.watch(
327
- watched,
328
- path.resolve(file),
329
- unavailable === "not-file" ? "file" : "exists",
330
- );
331
- return;
332
- }
333
- }
334
- // A dev server configured without a watcher can never deliver a
335
- // change event, so a registration here buys nothing, and it is not
336
- // free: Vite's import analysis resolves every registered path like a
337
- // real import of the transformed module, once per module. The
338
- // adapter's own missing-input poll above stays active either way,
339
- // because it never depended on Vite's watcher.
340
- if (viteCommand === "serve" && !viteWatching) {
341
- return;
342
- }
343
- this.addWatchFile(watched);
344
- },
268
+ // A watcherless server has no invalidation channel and needs no
269
+ // watch-input derivation. Every other host keeps its native contract.
270
+ addWatchFiles:
271
+ viteCommand === "serve" && !viteWatching
272
+ ? undefined
273
+ : (inputs, failed) => {
274
+ if (viteCommand === "serve") {
275
+ serveInputs.replace(file, inputs, failed);
276
+ } else {
277
+ const native = this.getNativeBuildContext?.();
278
+ for (const input of inputs) {
279
+ if (
280
+ native?.framework === "rspack" &&
281
+ native.loaderContext !== undefined
282
+ ) {
283
+ // Compilation-level dependencies schedule a pass but do
284
+ // not invalidate Rspack's cached transformed modules.
285
+ if (input.evidence?.missing === true)
286
+ native.loaderContext.addMissingDependency(input.file);
287
+ else native.loaderContext.addDependency(input.file);
288
+ } else if (native?.framework === "farm") {
289
+ native.context.addWatchFile(file, input.file);
290
+ } else {
291
+ this.addWatchFile(input.file);
292
+ }
293
+ }
294
+ }
295
+ },
345
296
  // A module the plugin declared volatile depends on non-file inputs,
346
297
  // which no file-dependency snapshot can represent; mark it
347
298
  // uncacheable where the bundler exposes that control.
@@ -239,14 +239,13 @@ class TtscUnstableGenerationError extends TtscTerminalGenerationError {
239
239
  /**
240
240
  * A compile this pass already attempted, whose envelope failed outright.
241
241
  *
242
- * The envelope cannot say whether the host reported diagnostics about the
243
- * project or failed to run at all: an ordinary type error arrives as an
244
- * `"exception"` carrying the compiler's own diagnostic text, exactly as a
245
- * crashed host would. Sniffing that message to tell the two apart would be a
246
- * guess, so the adapter uses the one boundary it genuinely owns. Inside a pass
247
- * the answer is already settled, so every later module replays it instead of
248
- * repeating a whole-project transform to reach the same verdict, which is what
249
- * made a single broken save cost one compile per delivered module
242
+ * Native project diagnostics carry a structured failure envelope; setup and
243
+ * host failures can still arrive as opaque exceptions. Both settle the current
244
+ * attempt, so the adapter uses the delivery boundary it owns rather than
245
+ * guessing retryability from a diagnostic message. Inside a pass the answer is
246
+ * already settled, so every later module replays it instead of repeating a
247
+ * whole-project transform to reach the same verdict, which is what made a
248
+ * single broken save cost one compile per delivered module
250
249
  * (samchon/ttsc#1303).
251
250
  *
252
251
  * The scope is exactly the pass. A host whose `buildStart` repeats drops the
@@ -811,10 +810,12 @@ export interface TtscTransformHooks {
811
810
  addWatchFile?: (file: string, evidence?: TtscWatchInputEvidence) => void;
812
811
  /**
813
812
  * Batched form of {@link addWatchFile}. When supplied, the transform calls it
814
- * once per delivered module and does not call `addWatchFile` for that
815
- * module.
813
+ * once per delivered module and does not call `addWatchFile` for that module.
814
+ * `failed` marks a recovery batch: a failed compiler can omit inputs from its
815
+ * previous successful result, so replacing hosts should retain those
816
+ * spellings until the next successful delivery.
816
817
  */
817
- addWatchFiles?: (inputs: readonly TtscWatchInput[]) => void;
818
+ addWatchFiles?: (inputs: readonly TtscWatchInput[], failed?: boolean) => void;
818
819
  /**
819
820
  * Invoked when the plugin declared the transformed file volatile (the
820
821
  * envelope's `volatile` list): its output depends on non-file inputs that no
@@ -1947,11 +1948,11 @@ function collectDeclaredIdentities(
1947
1948
  * invalidated, and the error stays on screen (samchon/ttsc#1312).
1948
1949
  *
1949
1950
  * A failure envelope can retain exact external input spellings from its graph
1950
- * and host metadata. A pre-transform typecheck failure may have no graph yet;
1951
- * its structured diagnostics, or the host's standard diagnostic lines when it
1952
- * could return only an exception, still name the external files that need a
1953
- * repair. The cost is paid only on a failure, and only until the next compile
1954
- * succeeds and narrows the set back to the derived inputs.
1951
+ * and host metadata, including missing resolution candidates on native
1952
+ * typecheck failures. For hosts that return no graph, structured diagnostics or
1953
+ * standard diagnostic lines provide only the paths they actually name. The cost
1954
+ * is paid only on a failure, and only until the next compile succeeds and
1955
+ * narrows the set back to the derived inputs.
1955
1956
  */
1956
1957
  function notifyFailedGenerationInputs(
1957
1958
  hooks: TtscTransformHooks | undefined,
@@ -2002,7 +2003,7 @@ function notifyFailedGenerationInputs(
2002
2003
  }
2003
2004
  }
2004
2005
  if (addWatchFiles !== undefined) {
2005
- addWatchFiles(inputs);
2006
+ addWatchFiles(inputs, true);
2006
2007
  return;
2007
2008
  }
2008
2009
  for (const input of inputs) {
@@ -5006,7 +5007,9 @@ async function registerWindowsProjectMutationTracker(
5006
5007
  });
5007
5008
  broker.pendingRegistrations += 1;
5008
5009
  broker.child.ref();
5009
- broker.child.channel?.ref();
5010
+ // Bun's IPC channel omits Node's Control.ref/unref methods. The child itself
5011
+ // still owns the outstanding acknowledgement on that runtime.
5012
+ broker.child.channel?.ref?.();
5010
5013
  const id = broker.nextId++;
5011
5014
  let resolveReady!: () => void;
5012
5015
  const ready = new Promise<void>((resolve) => {
@@ -5071,7 +5074,7 @@ async function registerWindowsProjectMutationTracker(
5071
5074
  // exit mid-build.
5072
5075
  if (broker.pendingRegistrations === 0 && broker.pendingDrains === 0) {
5073
5076
  broker.child.unref();
5074
- broker.child.channel?.unref();
5077
+ broker.child.channel?.unref?.();
5075
5078
  }
5076
5079
  }
5077
5080
  }
@@ -5199,7 +5202,7 @@ function startWindowsProjectMutationDrain(
5199
5202
  broker.pendingDrains -= 1;
5200
5203
  if (broker.pendingDrains === 0 && broker.pendingRegistrations === 0) {
5201
5204
  broker.child.unref();
5202
- broker.child.channel?.unref();
5205
+ broker.child.channel?.unref?.();
5203
5206
  }
5204
5207
  resolve();
5205
5208
  };
@@ -5210,7 +5213,7 @@ function startWindowsProjectMutationDrain(
5210
5213
  // process exits mid-build with nothing to report.
5211
5214
  broker.pendingDrains += 1;
5212
5215
  broker.child.ref();
5213
- broker.child.channel?.ref();
5216
+ broker.child.channel?.ref?.();
5214
5217
  const timer = setTimeout(release, WINDOWS_MUTATION_DRAIN_FALLBACK_MS);
5215
5218
  broker.drains.set(id, release);
5216
5219
  if (broker.child.send?.({ id, op: "drain" }) !== true) {
@@ -7858,13 +7861,12 @@ function formatUnknownError(error: unknown): string {
7858
7861
  /**
7859
7862
  * Remove terminal colour and cursor sequences from text the adapter surfaces.
7860
7863
  *
7861
- * An ordinary type error reaches the adapter as an `"exception"` envelope whose
7862
- * `error` is the host's own rendered output, colour and all, and the envelope
7863
- * carries no structured diagnostics to format instead. What the adapter hands
7864
- * back is not going to a terminal: it becomes the `Error` a bundler reports, so
7865
- * it lands in a Vite overlay, a webpack error report or a CI annotation, where
7866
- * the escapes render as literal noise around the file and line the reader needs
7867
- * (samchon/ttsc#1312).
7864
+ * An opaque host exception can contain the host's own rendered output, colour
7865
+ * and all, with no structured diagnostics to format instead. What the adapter
7866
+ * hands back is not going to a terminal: it becomes the `Error` a bundler
7867
+ * reports, so it lands in a Vite overlay, a webpack error report or a CI
7868
+ * annotation, where the escapes render as literal noise around the file and
7869
+ * line the reader needs (samchon/ttsc#1312).
7868
7870
  *
7869
7871
  * The colour originates in the host's rendering rather than in anything this
7870
7872
  * adapter configures, so this is the adapter-side repair, applied to every