@ttsc/unplugin 0.32.0 → 0.32.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.
Files changed (93) hide show
  1. package/lib/core/options/ResolvedTtscUnpluginOptions.d.cts +2 -0
  2. package/lib/core/options/ResolvedTtscUnpluginOptions.d.mts +2 -0
  3. package/lib/core/options/ResolvedTtscUnpluginOptions.d.ts +2 -0
  4. package/lib/core/options/TtscUnpluginOptions.d.cts +12 -2
  5. package/lib/core/options/TtscUnpluginOptions.d.mts +12 -2
  6. package/lib/core/options/TtscUnpluginOptions.d.ts +12 -2
  7. package/lib/core/options/resolveOptions.d.cts +4 -3
  8. package/lib/core/options/resolveOptions.d.mts +4 -3
  9. package/lib/core/options/resolveOptions.d.ts +4 -3
  10. package/lib/core/options/resolveOptions.js +5 -3
  11. package/lib/core/options/resolveOptions.js.map +1 -1
  12. package/lib/core/options/resolveOptions.mjs +5 -3
  13. package/lib/core/options/resolveOptions.mjs.map +1 -1
  14. package/lib/core/transform/TtscPrecompilation.d.cts +52 -0
  15. package/lib/core/transform/TtscPrecompilation.d.mts +52 -0
  16. package/lib/core/transform/TtscPrecompilation.d.ts +52 -0
  17. package/lib/core/transform/TtscPrecompilation.js +101 -0
  18. package/lib/core/transform/TtscPrecompilation.js.map +1 -0
  19. package/lib/core/transform/TtscPrecompilation.mjs +101 -0
  20. package/lib/core/transform/TtscPrecompilation.mjs.map +1 -0
  21. package/lib/core/transform/cache/selectCachedGenerationAction.d.cts +2 -0
  22. package/lib/core/transform/cache/selectCachedGenerationAction.d.mts +2 -0
  23. package/lib/core/transform/cache/selectCachedGenerationAction.d.ts +2 -0
  24. package/lib/core/transform/cache/selectCachedGenerationAction.js +1 -1
  25. package/lib/core/transform/cache/selectCachedGenerationAction.js.map +1 -1
  26. package/lib/core/transform/cache/selectCachedGenerationAction.mjs +1 -1
  27. package/lib/core/transform/cache/selectCachedGenerationAction.mjs.map +1 -1
  28. package/lib/core/transform/cache/withdrawTtscTransformGenerations.js +2 -0
  29. package/lib/core/transform/cache/withdrawTtscTransformGenerations.js.map +1 -1
  30. package/lib/core/transform/cache/withdrawTtscTransformGenerations.mjs +2 -0
  31. package/lib/core/transform/cache/withdrawTtscTransformGenerations.mjs.map +1 -1
  32. package/lib/core/transform/generation/captureTransformGeneration.d.cts +2 -0
  33. package/lib/core/transform/generation/captureTransformGeneration.d.mts +2 -0
  34. package/lib/core/transform/generation/captureTransformGeneration.d.ts +2 -0
  35. package/lib/core/transform/generation/captureTransformGeneration.js +7 -2
  36. package/lib/core/transform/generation/captureTransformGeneration.js.map +1 -1
  37. package/lib/core/transform/generation/captureTransformGeneration.mjs +7 -2
  38. package/lib/core/transform/generation/captureTransformGeneration.mjs.map +1 -1
  39. package/lib/core/transform/generation/transformProject.d.cts +2 -0
  40. package/lib/core/transform/generation/transformProject.d.mts +2 -0
  41. package/lib/core/transform/generation/transformProject.d.ts +2 -0
  42. package/lib/core/transform/generation/transformProject.js.map +1 -1
  43. package/lib/core/transform/generation/transformProject.mjs.map +1 -1
  44. package/lib/core/transform/transformTtsc.d.cts +8 -24
  45. package/lib/core/transform/transformTtsc.d.mts +8 -24
  46. package/lib/core/transform/transformTtsc.d.ts +8 -24
  47. package/lib/core/transform/transformTtsc.js +51 -31
  48. package/lib/core/transform/transformTtsc.js.map +1 -1
  49. package/lib/core/transform/transformTtsc.mjs +51 -31
  50. package/lib/core/transform/transformTtsc.mjs.map +1 -1
  51. package/lib/core/transform/validation/captureUniversalHostInputValidation.d.cts +1 -1
  52. package/lib/core/transform/validation/captureUniversalHostInputValidation.d.mts +1 -1
  53. package/lib/core/transform/validation/captureUniversalHostInputValidation.d.ts +1 -1
  54. package/lib/core/transform/validation/captureUniversalHostInputValidation.js +12 -4
  55. package/lib/core/transform/validation/captureUniversalHostInputValidation.js.map +1 -1
  56. package/lib/core/transform/validation/captureUniversalHostInputValidation.mjs +12 -4
  57. package/lib/core/transform/validation/captureUniversalHostInputValidation.mjs.map +1 -1
  58. package/lib/core/transform/validation/matchesCachedSource.d.cts +9 -2
  59. package/lib/core/transform/validation/matchesCachedSource.d.mts +9 -2
  60. package/lib/core/transform/validation/matchesCachedSource.d.ts +9 -2
  61. package/lib/core/transform/validation/matchesCachedSource.js +11 -3
  62. package/lib/core/transform/validation/matchesCachedSource.js.map +1 -1
  63. package/lib/core/transform/validation/matchesCachedSource.mjs +11 -3
  64. package/lib/core/transform/validation/matchesCachedSource.mjs.map +1 -1
  65. package/lib/core/transform/watch/TtscTransformHooks.d.cts +9 -0
  66. package/lib/core/transform/watch/TtscTransformHooks.d.mts +9 -0
  67. package/lib/core/transform/watch/TtscTransformHooks.d.ts +9 -0
  68. package/lib/core/unplugin.js +14 -0
  69. package/lib/core/unplugin.js.map +1 -1
  70. package/lib/core/unplugin.mjs +14 -0
  71. package/lib/core/unplugin.mjs.map +1 -1
  72. package/lib/core/vite/installViteDependencyScanPlugins.d.cts +46 -0
  73. package/lib/core/vite/installViteDependencyScanPlugins.d.mts +46 -0
  74. package/lib/core/vite/installViteDependencyScanPlugins.d.ts +46 -0
  75. package/lib/core/vite/installViteDependencyScanPlugins.js +186 -0
  76. package/lib/core/vite/installViteDependencyScanPlugins.js.map +1 -0
  77. package/lib/core/vite/installViteDependencyScanPlugins.mjs +184 -0
  78. package/lib/core/vite/installViteDependencyScanPlugins.mjs.map +1 -0
  79. package/package.json +5 -4
  80. package/src/core/options/ResolvedTtscUnpluginOptions.ts +3 -0
  81. package/src/core/options/TtscUnpluginOptions.ts +13 -2
  82. package/src/core/options/resolveOptions.ts +6 -3
  83. package/src/core/transform/TtscPrecompilation.ts +103 -0
  84. package/src/core/transform/cache/selectCachedGenerationAction.ts +3 -0
  85. package/src/core/transform/cache/withdrawTtscTransformGenerations.ts +2 -0
  86. package/src/core/transform/generation/captureTransformGeneration.ts +12 -2
  87. package/src/core/transform/generation/transformProject.ts +3 -0
  88. package/src/core/transform/transformTtsc.ts +81 -36
  89. package/src/core/transform/validation/captureUniversalHostInputValidation.ts +12 -4
  90. package/src/core/transform/validation/matchesCachedSource.ts +9 -2
  91. package/src/core/transform/watch/TtscTransformHooks.ts +10 -0
  92. package/src/core/unplugin.ts +32 -0
  93. package/src/core/vite/installViteDependencyScanPlugins.ts +219 -0
@@ -0,0 +1,103 @@
1
+ import path from "node:path";
2
+
3
+ import { isTransformTarget } from "../isTransformTarget";
4
+ import type { ResolvedTtscUnpluginOptions } from "../options/ResolvedTtscUnpluginOptions";
5
+ import type { TtscTransformCache } from "./cache/TtscTransformCache";
6
+ import { transformCacheEpoch } from "./cache/transformCacheEpoch";
7
+ import { transformFilesystem } from "./cache/transformFilesystem";
8
+ import { pluginsAreDisabled } from "./utils/pluginsAreDisabled";
9
+
10
+ /**
11
+ * Schedule explicitly selected source entries before the host reaches their
12
+ * imports. Compilation and proof stay with the ordinary transform core.
13
+ *
14
+ * A host often discovers its next workspace project only after the preceding
15
+ * project's transform finishes (#1764). Starting known entries together avoids
16
+ * that dependency on host traversal order. Walking unrelated projects would
17
+ * execute plugins the user never selected, so this owner uses only the supplied
18
+ * entries. Preparation leaves module delivery and host registration to the
19
+ * actual request.
20
+ *
21
+ * @evidence contracts/common.md#principled-implementation Explicit entries authorize scheduling; the supplied core capture retains project selection, configuration identity and all generation proofs. The context contains the current working directory, options, aliases and delivery epoch, not new cache authority.
22
+ * @evidence contracts/common.md#clear-and-simple-design One scheduler owns per-cache startup deduplication and reset; the transform core owns compiled generations and their disposal.
23
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts No directory-wide discovery, plugin substitution or skipped proof is introduced; preparation never impersonates a host module delivery.
24
+ * @evidence contracts/common.md#meaningful-documentation Native prose explains host traversal order, explicit effectful selection and separation from delivery; members state context lifetime and failure ownership.
25
+ * @evidence contracts/portability.md#os-neutral-implementation Entries resolve through native path semantics from the current cwd and read through the cache's filesystem boundary; source eligibility uses the adapter's existing shared predicate.
26
+ * @evidence contracts/performance.md#efficient-algorithms Each new context serializes cwd, options and aliases, then resolves and deduplicates E entries and reads their source bytes once. Capture starts without waiting for other entries; native capture and proof retain their own costs. Repeated requests serialize the context but do not reread the entries.
27
+ * @evidence contracts/performance.md#reuse-equivalent-work One latest context per cache and epoch starts each entry once; ordinary project keys deduplicate entries in the same project. Context equality controls scheduling only, and retained results still need normal admission.
28
+ * @evidence contracts/performance.md#bound-retention-and-release-resources A WeakMap retains one context string per live cache. Startup rejection handlers consume each detached promise; running captures and native resources remain cache-owned and withdrawal disposes their fulfilled generations. No cancellation deadline or entry-count cap is added.
29
+ */
30
+ export namespace TtscPrecompilation {
31
+ const contexts = new WeakMap<TtscTransformCache, string>();
32
+
33
+ /**
34
+ * Start every eligible explicit entry without waiting for its siblings.
35
+ * Failed speculative preparation is reported when its module is requested, so
36
+ * an unrelated delivered project's load can proceed.
37
+ *
38
+ * @evidence contracts/common.md#principled-implementation The context is installed before captures start, so recursive and concurrent deliveries cannot restart the same batch; only supported source entries reach normal core capture.
39
+ * @evidence contracts/common.md#clear-and-simple-design One synchronous loop separates startup ownership from asynchronous capture settlement.
40
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts Failed entry reads are not fabricated as empty source, and rejected captures do not become successful output. Actual delivery remains the error boundary.
41
+ * @evidence contracts/common.md#meaningful-documentation Prose states parallel startup and deferred error reporting; callback and context ownership remain explicit.
42
+ * @evidence contracts/portability.md#os-neutral-implementation Native absolute entry paths are passed as exact paths to capture and source reads use the same cache-owned filesystem as generation validation.
43
+ * @evidence contracts/performance.md#efficient-algorithms Context serialization precedes one E-entry deduplicating loop and actual source-byte reads; captures overlap their asynchronous work rather than being awaited sequentially.
44
+ * @evidence contracts/performance.md#reuse-equivalent-work Identical latest cwd, options, aliases and epoch skip scheduling; project Promise identity and normal input proof decide compile reuse, independently of this scheduling record.
45
+ * @evidence contracts/performance.md#bound-retention-and-release-resources Context state follows its weak cache key. Every started capture receives a rejection handler immediately; its own core/cache owners release native resources, including withdrawal during capture.
46
+ */
47
+ export function start(
48
+ cache: TtscTransformCache,
49
+ options: ResolvedTtscUnpluginOptions,
50
+ aliasPaths: Record<string, string[]> | undefined,
51
+ capture: (file: string, source: string) => Promise<unknown>,
52
+ ): void {
53
+ if (
54
+ options.precompile?.length === 0 ||
55
+ options.precompile === undefined ||
56
+ pluginsAreDisabled(options.plugins)
57
+ )
58
+ return;
59
+ // Entry, project and projectRoot paths all use cwd. The same relative
60
+ // options under another directory select different work, even if the
61
+ // host retains its cache; capture coordinates must belong to this call.
62
+ const cwd = process.cwd();
63
+ const context = JSON.stringify([
64
+ cwd,
65
+ options,
66
+ aliasPaths,
67
+ transformCacheEpoch(cache),
68
+ ]);
69
+ if (contexts.get(cache) === context) return;
70
+ contexts.set(cache, context);
71
+ const entries = new Set<string>();
72
+ const filesystem = transformFilesystem(cache);
73
+ for (const entry of options.precompile) {
74
+ if (!isTransformTarget(entry)) continue;
75
+ const file = path.resolve(cwd, entry);
76
+ if (entries.has(file)) continue;
77
+ entries.add(file);
78
+ try {
79
+ const source = filesystem.readFile(file).toString("utf8");
80
+ void capture(file, source).catch(() => undefined);
81
+ } catch {
82
+ // A missing entry must not poison another project's load. Its actual
83
+ // host delivery still owns the native read error and recovery.
84
+ }
85
+ }
86
+ }
87
+
88
+ /**
89
+ * End startup ownership when the cache withdraws its generations.
90
+ *
91
+ * @evidence contracts/common.md#principled-implementation Withdrawal removes scheduling state with generation ownership so a later lifecycle can prepare the same entries again.
92
+ * @evidence contracts/common.md#clear-and-simple-design One weak-key deletion shares the cache's existing reset boundary.
93
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts Clearing scheduling permission does not admit or cancel any compiled result.
94
+ * @evidence contracts/common.md#meaningful-documentation The headline identifies the same generation withdrawal boundary used by callers.
95
+ * @evidenceExclude contracts/portability.md#os-neutral-implementation WeakMap deletion performs no native operation.
96
+ * @evidenceExclude contracts/performance.md#efficient-algorithms One identity deletion chooses no traversal or processing strategy.
97
+ * @evidenceExclude contracts/performance.md#reuse-equivalent-work This operation withdraws the previous scheduling record rather than reusing work.
98
+ * @evidenceExclude contracts/performance.md#bound-retention-and-release-resources Deletion releases the single scheduling record; native resources remain owned by generation withdrawal.
99
+ */
100
+ export function clear(cache: TtscTransformCache): void {
101
+ contexts.delete(cache);
102
+ }
103
+ }
@@ -43,6 +43,8 @@ export function selectCachedGenerationAction(props: {
43
43
  generation: Promise<TtscCachedProjectTransform>;
44
44
  key: string;
45
45
  source: string;
46
+ /** False only for speculative source reads before an actual module delivery. */
47
+ reportDivergence?: boolean;
46
48
  proof?: TtscGenerationProof.Transaction;
47
49
  }): "serve" | "retry" | "capture" {
48
50
  if (
@@ -57,6 +59,7 @@ export function selectCachedGenerationAction(props: {
57
59
  props.source,
58
60
  props.epoch,
59
61
  props.proof,
62
+ props.reportDivergence,
60
63
  )
61
64
  ) {
62
65
  return "serve";
@@ -1,3 +1,4 @@
1
+ import { TtscPrecompilation } from "../TtscPrecompilation";
1
2
  import type { TtscTransformCache } from "./TtscTransformCache";
2
3
  import { disposeCachedTransform } from "./disposeCachedTransform";
3
4
 
@@ -35,6 +36,7 @@ export function withdrawTtscTransformGenerations(
35
36
  cache: TtscTransformCache,
36
37
  ): void {
37
38
  const generations = [...cache.values()];
39
+ TtscPrecompilation.clear(cache);
38
40
  cache.clear();
39
41
  for (const generation of generations) {
40
42
  void generation.then(disposeCachedTransform, () => undefined);
@@ -127,6 +127,9 @@ export async function captureTransformGeneration(props: {
127
127
  /** Host-delivered text compared with the compiler's on-disk source baseline. */
128
128
  currentSource: string;
129
129
 
130
+ /** False for preparation, whose source read is not a host module delivery. */
131
+ reportDivergence?: boolean;
132
+
130
133
  /** Optional delivery epoch to which this captured observation belongs. */
131
134
  deliveryEpoch?: number;
132
135
 
@@ -638,7 +641,7 @@ export async function captureTransformGeneration(props: {
638
641
  // this generation. A delivered text that differs, because a plugin ordered
639
642
  // before ttsc rewrote the module or the file changed after the host read
640
643
  // it, must not replace them: every sibling delivery compares the disk and
641
- // would recompile the project (samchon/ttsc#1394). It is reported once
644
+ // would recompile the project (samchon/ttsc#1394). An actual delivery is reported once
642
645
  // instead, and the file's own compile is served.
643
646
  const currentFileKey = toProjectKey(
644
647
  projectRoot,
@@ -691,7 +694,14 @@ export async function captureTransformGeneration(props: {
691
694
  props.currentFile,
692
695
  diskHash ?? deliveredHash,
693
696
  );
694
- if (diskHash !== undefined && diskHash !== deliveredHash) {
697
+ // Preparation reads native source only to seed the ordinary capture. A
698
+ // later edit is handled by generation proof, not a warning about a host
699
+ // rewrite: no host delivered that speculative module (#1764).
700
+ if (
701
+ props.reportDivergence !== false &&
702
+ diskHash !== undefined &&
703
+ diskHash !== deliveredHash
704
+ ) {
695
705
  reportDivergentDelivery(cached, props.currentFile);
696
706
  }
697
707
  // An adopted compile was certified by its publisher, and is matched below
@@ -97,6 +97,9 @@ export async function transformProject(props: {
97
97
  /** Delivered module text compared against the compiler's disk observation. */
98
98
  currentSource: string;
99
99
 
100
+ /** False for preparation, whose source read is not a host module delivery. */
101
+ reportDivergence?: boolean;
102
+
100
103
  /**
101
104
  * Delivery pass this compile was started for; see
102
105
  * {@link TtscCachedProjectTransform.deliveryEpoch}.
@@ -1,6 +1,7 @@
1
1
  import path from "node:path";
2
2
 
3
3
  import type { ResolvedTtscUnpluginOptions } from "../options/ResolvedTtscUnpluginOptions";
4
+ import { TtscPrecompilation } from "./TtscPrecompilation";
4
5
  import type { TtscTransformResult } from "./TtscTransformResult";
5
6
  import { createAliasPaths } from "./alias/createAliasPaths";
6
7
  import { TERMINAL_TRANSFORM_GENERATIONS } from "./cache/TERMINAL_TRANSFORM_GENERATIONS";
@@ -68,38 +69,40 @@ import { recordedDependencyWitnesses } from "./watch/recordedDependencyWitnesses
68
69
  * @param hooks - Optional adapter callbacks; see {@link TtscTransformHooks}.
69
70
  * Dependency notifications fire on cache hits too; watch registrations are
70
71
  * per build, not per compilation.
71
- * @evidence contracts/common.md#principled-implementation Project selection and generation-qualified cache admission preserve compiler disk authority; fresh-only success additionally needs an explicit nonwatching lifecycle, coherent project declaration and actual host-cache withdrawal, because incomplete observation cannot support watch invalidation.
72
- * @evidence contracts/common.md#clear-and-simple-design One delivery coordinator composes project selection, cache admission, compilation, output selection and host notifications; dedicated owners handle proof and lifetime internals.
73
- * @evidence contracts/common.md#prohibited-implementation-shortcuts Wrapper modules cannot poison source baselines; incomplete local results can share only explicitly nonwatching first deliveries in their exact pass. Unknown/watching lifecycles or unsupported withdrawal reject and evict them rather than invent persistent records or dependency closure.
74
- * @evidence contracts/common.md#meaningful-documentation Native paragraphs and argument tags explain project scope, no-transform outcomes, cache epochs and per-build notification responsibilities with links to maintained reference context.
75
- * @evidence contracts/performance.md#efficient-algorithms
76
- * Selection/key construction pays ancestor/config/alias/option/path text work.
77
- * Each iteration may prepare toolchain state, settle native notifications and
78
- * replay source/project/external/universal proofs before selecting output.
79
- * In-flight/valid completed reuse avoids compilation, not those checks or
80
- * output/map/notification/record work. Capture misses pay whole native compile
81
- * and input populations. The common proof owner skips redundant environment
82
- * preparation for proven first deliveries and shares synchronous native proof
83
- * queries; persistent/repeated delivery establishes fresh authority. Concurrent supersession can repeat the outer loop
84
- * without a delivery-level retry/time bound here.
85
- * @evidence contracts/performance.md#reuse-equivalent-work Cache identity covers config/options/plugins/aliases; current generations, pass-qualified terminal verdicts and in-flight Promises are shared only while their source and dependency proof remains valid.
86
- * @evidence contracts/performance.md#bound-retention-and-release-resources
87
- * Optional cache state retains current generation Promises/results and
88
- * per-key dependency/case facts without a byte/key cap here. Native tasks
89
- * belong to compiler/capture owners; uncached captures retain no notification
90
- * or persistent clock probe. Fresh-only delivery releases capture resources,
91
- * retaining only an explicitly admitted local pass result; rejected or failed
92
- * incomplete handoff detaches its generation. Eviction/disposal attempt independent resource release
93
- * and native failures need not close every handle or remove every probe.
94
- * @evidence contracts/portability.md#os-neutral-implementation Native module paths and project coordinates use supported path/filesystem abstractions, while actual case policy and watcher capability come from generation proof rather than OS-name assumptions.
72
+ * @evidence contracts/common.md#principled-implementation Project selection and generation-qualified admission preserve compiler disk authority; preparation shares selection, keys, capture and input proofs with delivery, while host effects and first-delivery permission remain exclusive to actual delivery. Fresh-only success still requires an explicit nonwatching lifecycle, coherent project declaration and supported host-cache withdrawal because incomplete observation cannot establish watch invalidation.
73
+ * @evidence contracts/common.md#clear-and-simple-design One coordinator mode shares project selection, admission, compilation and output selection between preparation and delivery; dedicated owners retain proof and lifetime internals, and delivery alone performs host notification.
74
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts Wrapper modules cannot poison source baselines. Preparation skips handoff rather than postcompile proof, and superseded preparation stops without reviving withdrawn generations. Incomplete local results share only explicitly nonwatching first deliveries in their exact pass; unknown/watching lifecycles or unsupported withdrawal reject and evict without invented persistent records or dependency closure.
75
+ * @evidence contracts/common.md#meaningful-documentation Native paragraphs and argument tags explain project scope, no-transform outcomes, cache epochs and per-build notifications; coordinator prose identifies delivery-only effects and postpreparation admission, with maintained invalidation reference context.
76
+ * @evidence contracts/performance.md#efficient-algorithms Selection and keys pay ancestor/config/alias/option/path-text work. Each iteration may prepare toolchains, settle notifications and replay source/project/external/universal proofs; reuse avoids compilation rather than those checks or output/map/notification/record work. Misses pay native compilation and input populations. Proven first deliveries skip redundant environment preparation and share synchronous proof queries; repeated delivery establishes fresh authority. Startup schedules explicit entries concurrently and skips delivery output/map/registration work. Supersession can repeat the outer loop without a retry or time bound.
77
+ * @evidence contracts/performance.md#reuse-equivalent-work Config/options/plugins/aliases key the ordinary project Promise shared by preparation, simultaneous loads and sibling entries; completed generations, pass-qualified terminal verdicts and in-flight work remain reusable only under current source and dependency proof.
78
+ * @evidence contracts/performance.md#bound-retention-and-release-resources Optional cache state retains generation Promises/results and per-key dependency/case facts without a byte/key cap. Native tasks belong to compiler/capture owners; uncached captures retain no notification or persistent clock probe. Fresh-only delivery releases capture resources while retaining an admitted local pass result; failed or rejected incomplete handoff detaches its generation. Superseded preparation preserves the new owner, and withdrawal schedules disposal of its pending fulfilled result. Eviction/disposal attempt independent release; native failure need not close every handle or remove every probe.
79
+ * @evidence contracts/portability.md#os-neutral-implementation Preparation and delivery use the same native project selection, path identity and cache-owned filesystem boundaries, with exact startup entry paths. Generation proof supplies case and watcher policy rather than OS-name assumptions.
95
80
  */
96
- export async function transformTtsc(
81
+ export function transformTtsc(
97
82
  id: string,
98
83
  source: string,
99
84
  options: ResolvedTtscUnpluginOptions,
100
85
  aliases?: unknown,
101
86
  cache?: TtscTransformCache,
102
87
  hooks?: TtscTransformHooks,
88
+ ): Promise<TtscTransformResult | undefined> {
89
+ return runTransformTtsc(id, source, options, aliases, cache, hooks, false);
90
+ }
91
+
92
+ /**
93
+ * Coordinate capture and actual delivery through the same project cache.
94
+ * Preparation stops before handoff: it cannot mark a module served, publish a
95
+ * host record, acknowledge volatility or emit module diagnostics. A prepared
96
+ * generation must still pass ordinary admission when the host requests it.
97
+ */
98
+ async function runTransformTtsc(
99
+ id: string,
100
+ source: string,
101
+ options: ResolvedTtscUnpluginOptions,
102
+ aliases: unknown,
103
+ cache: TtscTransformCache | undefined,
104
+ hooks: TtscTransformHooks | undefined,
105
+ preparing: boolean,
103
106
  ): Promise<TtscTransformResult | undefined> {
104
107
  const filesystem = transformFilesystem(cache);
105
108
  const exact = hooks?.exactPath === true;
@@ -126,7 +129,10 @@ export async function transformTtsc(
126
129
  // Writing the delivered record must not create a new member of that proof.
127
130
  prepareProjectRecordDirectories(hooks?.project);
128
131
 
129
- const selection = resolveProjectSelection(file, options.project, filesystem);
132
+ const selection =
133
+ hooks?.projectSelection === undefined
134
+ ? resolveProjectSelection(file, options.project, filesystem)
135
+ : { tsconfig: path.resolve(hooks.projectSelection), consulted: [] };
130
136
  const tsconfig = selection.tsconfig;
131
137
  const selectedProjectRoot = path.resolve(
132
138
  options.projectRoot ?? path.dirname(tsconfig),
@@ -143,6 +149,19 @@ export async function transformTtsc(
143
149
  tsconfig,
144
150
  };
145
151
  const aliasPaths = createAliasPaths(aliases);
152
+ if (!preparing && cache !== undefined) {
153
+ TtscPrecompilation.start(cache, options, aliasPaths, (entry, text) =>
154
+ runTransformTtsc(
155
+ entry,
156
+ text,
157
+ options,
158
+ aliases,
159
+ cache,
160
+ { ...hooks, exactPath: true, projectSelection: undefined },
161
+ true,
162
+ ),
163
+ );
164
+ }
146
165
  const key = createTransformCacheKey({
147
166
  aliasPaths,
148
167
  compilerOptions: options.compilerOptions,
@@ -169,7 +188,10 @@ export async function transformTtsc(
169
188
  undefined,
170
189
  terminal.validation.cached.projectRoot,
171
190
  );
172
- if (cache?.get(key) !== transformed) continue;
191
+ if (cache?.get(key) !== transformed) {
192
+ if (preparing) return undefined;
193
+ continue;
194
+ }
173
195
  }
174
196
  // A terminal verdict is an answer about one observed environment, not an
175
197
  // invitation for every later module to repeat the whole compile.
@@ -180,7 +202,13 @@ export async function transformTtsc(
180
202
  filesystem,
181
203
  })
182
204
  ) {
183
- notifyRejectedGenerationInputs(hooks, terminal, file, watchSelection);
205
+ if (!preparing)
206
+ notifyRejectedGenerationInputs(
207
+ hooks,
208
+ terminal,
209
+ file,
210
+ watchSelection,
211
+ );
184
212
  throw terminal;
185
213
  }
186
214
  evictGeneration(cache, key, transformed);
@@ -193,12 +221,13 @@ export async function transformTtsc(
193
221
  if (transformed !== undefined) {
194
222
  const cached = await awaitOrEvict(cache, key, transformed).catch(
195
223
  (rejection: unknown) => {
196
- notifyRejectedGenerationInputs(
197
- hooks,
198
- rejection,
199
- file,
200
- watchSelection,
201
- );
224
+ if (!preparing)
225
+ notifyRejectedGenerationInputs(
226
+ hooks,
227
+ rejection,
228
+ file,
229
+ watchSelection,
230
+ );
202
231
  throw rejection;
203
232
  },
204
233
  );
@@ -206,6 +235,7 @@ export async function transformTtsc(
206
235
  // While this caller awaited the old Promise, another caller may have
207
236
  // invalidated it and installed a newer authoritative generation.
208
237
  if (cache?.get(key) !== transformed) {
238
+ if (preparing) return undefined;
209
239
  continue;
210
240
  }
211
241
  // A generation captured while native notifications were trusted keeps
@@ -218,11 +248,15 @@ export async function transformTtsc(
218
248
  if (epoch === undefined) {
219
249
  await settleProjectMutationEvents(cached);
220
250
  if (cache?.get(key) !== transformed) {
251
+ if (preparing) return undefined;
221
252
  continue;
222
253
  }
223
254
  }
224
255
  await TtscGenerationProof.prepare(cached, file, epoch);
225
- if (cache?.get(key) !== transformed) continue;
256
+ if (cache?.get(key) !== transformed) {
257
+ if (preparing) return undefined;
258
+ continue;
259
+ }
226
260
  // No await between this observation and synchronous admission.
227
261
  const proof = TtscGenerationProof.create(cached, file, epoch);
228
262
  const action = selectCachedGenerationAction({
@@ -234,11 +268,13 @@ export async function transformTtsc(
234
268
  generation: transformed,
235
269
  key,
236
270
  source,
271
+ reportDivergence: !preparing,
237
272
  });
238
273
  if (action === "retry") {
239
274
  continue;
240
275
  }
241
276
  if (action === "serve") {
277
+ if (preparing) return undefined;
242
278
  return deliverCachedTransform({
243
279
  cache,
244
280
  cached,
@@ -261,6 +297,7 @@ export async function transformTtsc(
261
297
  projectRoot: selectedProjectRoot,
262
298
  currentFile: file,
263
299
  currentSource: source,
300
+ reportDivergence: !preparing,
264
301
  // Stamp the pass this compile was started for, not the one it happens
265
302
  // to finish in: a boundary crossed mid-compile leaves the generation
266
303
  // belonging to the earlier pass, so the next pass re-proves it.
@@ -296,11 +333,18 @@ export async function transformTtsc(
296
333
  const generation = transformed;
297
334
  const cached = await awaitOrEvict(cache, key, generation).catch(
298
335
  (rejection: unknown) => {
299
- notifyRejectedGenerationInputs(hooks, rejection, file, watchSelection);
336
+ if (!preparing)
337
+ notifyRejectedGenerationInputs(
338
+ hooks,
339
+ rejection,
340
+ file,
341
+ watchSelection,
342
+ );
300
343
  throw rejection;
301
344
  },
302
345
  );
303
346
  if (cache !== undefined && cache.get(key) !== generation) {
347
+ if (preparing) return undefined;
304
348
  continue;
305
349
  }
306
350
  if (cache !== undefined) {
@@ -320,6 +364,7 @@ export async function transformTtsc(
320
364
  policies.set(key, reported);
321
365
  }
322
366
  }
367
+ if (preparing) return undefined;
323
368
  return deliverCachedTransform({
324
369
  cache,
325
370
  cached,
@@ -42,7 +42,7 @@ import { matchesRecordedInput } from "./matchesRecordedInput";
42
42
  * @evidence contracts/common.md#meaningful-documentation Native prose explains failed admission and successful generation attachment; inline comments justify readable-state, blocker and tree distinctions.
43
43
  * @evidence contracts/portability.md#os-neutral-implementation The generation's filesystem view qualifies metadata and exact native absence. Grouped candidates retain original spelling because directory case policy alone does not define Unicode or short-name equivalence; their joined path follows that view's platform dialect. Physical targets and aliases remain distinct from content identity, and native plugin environment authority is not established by arbitrary filesystem injection.
44
44
  * @evidence contracts/performance.md#bound-retention-and-release-resources The generation retains manifest entries, lexical coverage, raw missing candidates and tree environments proportional to admitted inputs and their path/state text, without a total byte cap or native handle here. Failed attempt records are call-local and diagnostic witness counts are bounded by their owner; replacing or releasing the generation ends manifest ownership.
45
- * @evidence contracts/performance.md#efficient-algorithms Input selection materializes the union of producer lists/proof keys and scans U universal inputs and T plugin source trees. Comparisons include content bytes, physical identity/ancestor/case queries, paired metadata and exact candidate stat; source proof includes tree population/bytes and environment validation. These delegated costs and temporary input/manifest populations are not constant-time map insertion work.
45
+ * @evidence contracts/performance.md#efficient-algorithms Input selection materializes the union of producer lists/proof keys and scans U universal inputs and T plugin source trees. Async-owned environments share one synchronous batch, qualifying common SDK paths once across plugin directories. Comparisons include content bytes, physical identity/ancestor/case queries, paired metadata and exact candidate stat; source proof includes tree population/bytes and environment validation. These delegated costs and temporary input/manifest populations are not constant-time map insertion work.
46
46
  * @evidence contracts/performance.md#reuse-equivalent-work This shared generation manifest records exactly qualified lexical spellings, separable signatures and tree environments for later validators; changed proof requires new admission.
47
47
  */
48
48
  export function captureUniversalHostInputValidation(
@@ -295,13 +295,21 @@ export function captureUniversalHostInputValidation(
295
295
  // A plugin binary keyed on a source other than the disk's now, whether it
296
296
  // was built here or adopted from another worker, is output for a state
297
297
  // already gone (samchon/ttsc#1487).
298
- for (const [directory, digest] of selectPluginSourceInputs(cached.result)) {
298
+ const sources = selectPluginSourceInputs(cached.result);
299
+ const prepared = usesPreparedPluginBuildEnvironments(cached.result);
300
+ // Plugin directories commonly share the thousands-file Go SDK. Qualify it
301
+ // once for this synchronous proof, not once per directory (#1764). The map
302
+ // must stay inside this call: an await or later delivery needs fresh native
303
+ // authority, even when source watchers heard no change (#1712, #1516).
304
+ const environments = prepared
305
+ ? PluginBuildEnvironmentReadings.cachedAll(sources.keys())
306
+ : undefined;
307
+ for (const [directory, digest] of sources) {
299
308
  // The environment is recorded as read before the proof, so it is never a
300
309
  // reading the proof did not see; one that moved meanwhile only makes the
301
310
  // next delivery prove the tree again.
302
- const prepared = usesPreparedPluginBuildEnvironments(cached.result);
303
311
  const environment = prepared
304
- ? PluginBuildEnvironmentReadings.cached(directory)
312
+ ? environments!.get(directory)
305
313
  : processPluginBuildEnvironment(directory, false, cached.projectRoot);
306
314
  if (
307
315
  environment === undefined ||
@@ -37,7 +37,9 @@ import { notificationsProveProgramUnchanged } from "./notificationsProveProgramU
37
37
  * rejects this generation for the delivery; its caller chooses replacement or
38
38
  * capture. Delivered text that differs while the disk still holds the bytes the
39
39
  * generation compiled is not one: it is reported and served, since the compile
40
- * read the disk (samchon/ttsc#1394).
40
+ * read the disk (samchon/ttsc#1394). Preparation performs the same comparison
41
+ * without reporting a host rewrite, because its native source read is not a
42
+ * delivered module.
41
43
  *
42
44
  * Its place in the adapter's invalidation model, and the units beside it, are
43
45
  * mapped in the maintainer page
@@ -73,6 +75,11 @@ export function matchesCachedSource(
73
75
  /** Current host pass, or undefined for persistent input validation. */
74
76
  epoch: number | undefined,
75
77
  proof?: TtscGenerationProof.Transaction,
78
+ /**
79
+ * Preparation compares the same native bytes without reporting a host
80
+ * rewrite.
81
+ */
82
+ reportDivergence = true,
76
83
  ): boolean {
77
84
  if (!nativeInputPredicatesHold(cached, proof)) return false;
78
85
  const pass = TtscGenerationProof.sharesPass(cached, file, epoch);
@@ -133,7 +140,7 @@ export function matchesCachedSource(
133
140
  ) {
134
141
  return false;
135
142
  }
136
- reportDivergentDelivery(cached, file);
143
+ if (reportDivergence) reportDivergentDelivery(cached, file);
137
144
  }
138
145
  if (pass) return true;
139
146
  if (epoch !== undefined && cached.projectSnapshotComplete === true) {
@@ -30,6 +30,16 @@ import type { TtscWatchInputEvidence } from "./TtscWatchInputEvidence";
30
30
  * state at runtime.
31
31
  */
32
32
  export interface TtscTransformHooks {
33
+ /**
34
+ * Native config already selected by the host for this delivered module.
35
+ *
36
+ * A host that freezes routing and its discovery inputs in one transaction
37
+ * supplies that config here and retains those inputs in its watch channel.
38
+ * Startup entries select their own projects from the user's
39
+ * `options.project`, independently of another module's discovered route.
40
+ */
41
+ projectSelection?: string;
42
+
33
43
  /**
34
44
  * Invoked once per absolute watch-input path derived for the transformed file
35
45
  * `F`: the plugin-reported `dependencies[F]` list unioned with the host-owned
@@ -42,6 +42,7 @@ import type { TtscProjectRegistration } from "./transform/watch/TtscProjectRegis
42
42
  import { createViteBuildLifecycle } from "./vite/createViteBuildLifecycle";
43
43
  import { createViteServeInputWatch } from "./vite/createViteServeInputWatch";
44
44
  import { createViteServeWatchHooks } from "./vite/createViteServeWatchHooks";
45
+ import { installViteDependencyScanPlugins } from "./vite/installViteDependencyScanPlugins";
45
46
  import { TTSC_SOURCE_MAP_STASH } from "./webpack/TTSC_SOURCE_MAP_STASH";
46
47
  import { registerTtscSourceMapLoader } from "./webpack/registerTtscSourceMapLoader";
47
48
  import { reportCompiledProjectRecords } from "./webpack/reportCompiledProjectRecords";
@@ -253,6 +254,37 @@ const unpluginFactory: UnpluginFactory<
253
254
  transformCache,
254
255
  hostDeclaresPolling(process.env, viteLifecycle.usePolling),
255
256
  );
257
+ installViteDependencyScanPlugins(
258
+ config,
259
+ isTransformTarget,
260
+ (source, file) => {
261
+ const startedAt = viteLifecycle.watching
262
+ ? serveInputs.begin()
263
+ : undefined;
264
+ return transformTtsc(
265
+ file,
266
+ source,
267
+ options,
268
+ aliases,
269
+ transformCache,
270
+ {
271
+ exactPath: true,
272
+ watching: viteLifecycle.watching,
273
+ ...createViteServeWatchHooks(
274
+ "serve",
275
+ viteLifecycle.watching,
276
+ serveInputs,
277
+ file,
278
+ startedAt,
279
+ ),
280
+ },
281
+ );
282
+ },
283
+ (this?.meta as { rolldownVersion?: string } | undefined)
284
+ ?.rolldownVersion === undefined
285
+ ? "esbuild"
286
+ : "rolldown",
287
+ );
256
288
  },
257
289
  // Compiler dependencies belong to the filesystem watch graph. Vite's
258
290
  // transform-context addWatchFile also inserts runtime imports, so none