ttsc 0.26.2 → 0.27.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 (111) hide show
  1. package/README.md +9 -0
  2. package/cmd/ttsc/api_transform.go +17 -3
  3. package/cmd/ttscgraph/auxiliary_identity_state_tracks_retargets_not_source_contents_test.go +85 -0
  4. package/cmd/ttscgraph/disk_realpath_other.go +16 -0
  5. package/cmd/ttscgraph/disk_realpath_windows.go +56 -0
  6. package/cmd/ttscgraph/serve.go +104 -35
  7. package/driver/emit_plugin.go +20 -3
  8. package/driver/graph.go +64 -11
  9. package/driver/input_observation.go +273 -0
  10. package/driver/input_observation_test.go +149 -0
  11. package/driver/plugins.go +384 -4
  12. package/driver/program.go +54 -1
  13. package/driver/resolution_candidates.go +13 -5
  14. package/driver/rewrite.go +45 -2
  15. package/lib/TtscCompiler.d.ts +12 -11
  16. package/lib/TtscCompiler.js +44 -29
  17. package/lib/TtscCompiler.js.map +1 -1
  18. package/lib/compiler/internal/captureProcessOutput.js +2 -2
  19. package/lib/compiler/internal/captureProcessOutput.js.map +1 -1
  20. package/lib/compiler/internal/compileProjectInMemory.js +4 -3
  21. package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
  22. package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +2 -0
  23. package/lib/compiler/internal/resolveEmittedJavaScript.js +3 -0
  24. package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
  25. package/lib/compiler/internal/runBuild.d.ts +1 -1
  26. package/lib/compiler/internal/runBuild.js +80 -34
  27. package/lib/compiler/internal/runBuild.js.map +1 -1
  28. package/lib/compiler/internal/runSingleFileEmit.js +2 -2
  29. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  30. package/lib/compiler/internal/sharedHostHelpers.d.ts +28 -0
  31. package/lib/compiler/internal/sharedHostHelpers.js +39 -0
  32. package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
  33. package/lib/compiler/internal/startResidentTransform.js +12 -4
  34. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  35. package/lib/compiler/internal/transformProjectInMemory.d.ts +3 -0
  36. package/lib/compiler/internal/transformProjectInMemory.js +239 -35
  37. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  38. package/lib/flags/schema.js +12 -5
  39. package/lib/flags/schema.js.map +1 -1
  40. package/lib/internal/createCanonicalTempDirectory.d.ts +17 -0
  41. package/lib/internal/createCanonicalTempDirectory.js +42 -0
  42. package/lib/internal/createCanonicalTempDirectory.js.map +1 -0
  43. package/lib/internal/projectInputPathIdentity.d.ts +3 -0
  44. package/lib/internal/projectInputPathIdentity.js +11 -5
  45. package/lib/internal/projectInputPathIdentity.js.map +1 -1
  46. package/lib/internal/resolveNodeBinary.d.ts +14 -0
  47. package/lib/internal/resolveNodeBinary.js +150 -0
  48. package/lib/internal/resolveNodeBinary.js.map +1 -0
  49. package/lib/internal/resolveSafeCacheCleanupTargets.d.ts +15 -0
  50. package/lib/internal/resolveSafeCacheCleanupTargets.js +61 -0
  51. package/lib/internal/resolveSafeCacheCleanupTargets.js.map +1 -0
  52. package/lib/launcher/internal/prepareExecution.d.ts +3 -0
  53. package/lib/launcher/internal/prepareExecution.js +42 -14
  54. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  55. package/lib/launcher/internal/runTtsc.js +25 -13
  56. package/lib/launcher/internal/runTtsc.js.map +1 -1
  57. package/lib/launcher/internal/runTtscserver.js +18 -3
  58. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  59. package/lib/launcher/internal/runTtsx.js +2 -0
  60. package/lib/launcher/internal/runTtsx.js.map +1 -1
  61. package/lib/launcher/internal/runtimeHooks.d.ts +37 -1
  62. package/lib/launcher/internal/runtimeHooks.js +653 -62
  63. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  64. package/lib/plugin/internal/buildSourcePlugin.d.ts +62 -8
  65. package/lib/plugin/internal/buildSourcePlugin.js +754 -72
  66. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  67. package/lib/plugin/internal/loadProjectPlugins.d.ts +17 -5
  68. package/lib/plugin/internal/loadProjectPlugins.js +1339 -77
  69. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  70. package/lib/register.d.ts +1 -0
  71. package/lib/register.js +46 -0
  72. package/lib/register.js.map +1 -0
  73. package/lib/structures/ITtscCompilerContext.d.ts +6 -3
  74. package/lib/structures/ITtscCompilerTransformation.d.ts +31 -0
  75. package/lib/structures/ITtscPlugin.d.ts +33 -0
  76. package/package.json +16 -9
  77. package/shim/checker/go.mod +9 -0
  78. package/shim/checker/go.sum +22 -0
  79. package/shim/checker/shim.go +75 -0
  80. package/shim/compiler/incremental.go +45 -8
  81. package/src/TtscCompiler.ts +55 -33
  82. package/src/compiler/internal/captureProcessOutput.ts +3 -2
  83. package/src/compiler/internal/compileProjectInMemory.ts +4 -3
  84. package/src/compiler/internal/resolveEmittedJavaScript.ts +6 -0
  85. package/src/compiler/internal/runBuild.ts +102 -39
  86. package/src/compiler/internal/runSingleFileEmit.ts +2 -2
  87. package/src/compiler/internal/sharedHostHelpers.ts +44 -0
  88. package/src/compiler/internal/startResidentTransform.ts +12 -3
  89. package/src/compiler/internal/transformProjectInMemory.ts +410 -39
  90. package/src/flags/schema.ts +12 -5
  91. package/src/internal/createCanonicalTempDirectory.ts +53 -0
  92. package/src/internal/projectInputPathIdentity.ts +17 -4
  93. package/src/internal/resolveNodeBinary.ts +173 -0
  94. package/src/internal/resolveSafeCacheCleanupTargets.ts +85 -0
  95. package/src/launcher/internal/prepareExecution.ts +50 -14
  96. package/src/launcher/internal/runTtsc.ts +24 -13
  97. package/src/launcher/internal/runTtscserver.ts +18 -3
  98. package/src/launcher/internal/runTtsx.ts +2 -0
  99. package/src/launcher/internal/runtimeHooks.ts +823 -66
  100. package/src/plugin/internal/buildSourcePlugin.ts +984 -79
  101. package/src/plugin/internal/loadProjectPlugins.ts +1718 -104
  102. package/src/register.ts +47 -0
  103. package/src/structures/ITtscCompilerContext.ts +6 -3
  104. package/src/structures/ITtscCompilerTransformation.ts +39 -0
  105. package/src/structures/ITtscPlugin.ts +36 -0
  106. package/tools/shim_audit/baseline.json +0 -1
  107. package/utility/host.go +9 -3
  108. package/lib/internal/assertSafeExplicitCacheDirectory.d.ts +0 -5
  109. package/lib/internal/assertSafeExplicitCacheDirectory.js +0 -26
  110. package/lib/internal/assertSafeExplicitCacheDirectory.js.map +0 -1
  111. package/src/internal/assertSafeExplicitCacheDirectory.ts +0 -29
@@ -1,13 +1,20 @@
1
1
  // gen_shims:hand-maintained
2
2
  //
3
- // Reference-graph parts of tsgo's own incremental engine (`tsc --incremental`
4
- // semantics), linked from internal/execute/incremental so ttsc's transform
5
- // envelopes can carry the same language-semantic input bound the compiler
6
- // itself uses for invalidation: per-file direct resolved references and the
7
- // files that contribute to the global scope.
3
+ // Parts of tsgo's own incremental engine (`tsc --incremental` semantics),
4
+ // exposed from internal/execute/incremental for the two things a ttsc host
5
+ // needs from it:
6
+ //
7
+ // - the reference graph, so transform envelopes can carry the same
8
+ // language-semantic input bound the compiler itself uses for invalidation:
9
+ // per-file direct resolved references and the files that contribute to the
10
+ // global scope;
11
+ // - the build-information emit, so a host that constructs its Program
12
+ // in-process still writes the `.tsbuildinfo` an `incremental` or
13
+ // `composite` project asked for.
8
14
  package compiler
9
15
 
10
16
  import (
17
+ "context"
11
18
  _ "unsafe"
12
19
 
13
20
  innerast "github.com/microsoft/typescript-go/internal/ast"
@@ -15,11 +22,41 @@ import (
15
22
  innercompiler "github.com/microsoft/typescript-go/internal/compiler"
16
23
  "github.com/microsoft/typescript-go/internal/tspath"
17
24
 
18
- // The linknamed symbols below live in the incremental package; the blank
19
- // import compiles it into every shim consumer so the references resolve.
20
- _ "github.com/microsoft/typescript-go/internal/execute/incremental"
25
+ // Imported for EmitFreshWithBuildInfo below, and for the linknamed symbols
26
+ // further down: compiling the package into every shim consumer is what makes
27
+ // those references resolve.
28
+ "github.com/microsoft/typescript-go/internal/execute/incremental"
21
29
  )
22
30
 
31
+ // EmitFreshWithBuildInfo runs a full emit through tsgo's own incremental
32
+ // program, so an `incremental` or `composite` project gets the `.tsbuildinfo`
33
+ // tsgo would have written, in the exact format and location
34
+ // `outputpaths.GetBuildInfoFileName` resolves from the compiler options.
35
+ //
36
+ // This is the emit half of `tsc.go::performIncrementalCompilation`, which the
37
+ // tsgo CLI takes whenever `CompilerOptions.IsIncremental()`. A host that builds
38
+ // its Program in-process (ttsc's driver, and therefore every plugin sidecar
39
+ // emitting through it) never reaches that CLI path, so without this the build
40
+ // information is silently dropped even though the options parsed cleanly.
41
+ //
42
+ // "Fresh" is the load-bearing word: no previous snapshot is supplied, so
43
+ // `programToSnapshot` marks every file changed and this emits the whole program
44
+ // exactly as `Program.Emit` does, then writes the build info. A ttsc plugin's
45
+ // output is not a pure function of the source text a build info records — it
46
+ // also depends on the plugin binary, its config file, and its contributors —
47
+ // so reusing a previous snapshot to skip a file would serve stale transformed
48
+ // output. Producing the record is sound; consuming it needs plugin identity in
49
+ // the invalidation key first.
50
+ func EmitFreshWithBuildInfo(ctx context.Context, program *Program, options EmitOptions) *EmitResult {
51
+ incrementalProgram := incremental.NewProgram(
52
+ program,
53
+ nil,
54
+ incremental.CreateHost(program.Host()),
55
+ false,
56
+ )
57
+ return incrementalProgram.Emit(ctx, options)
58
+ }
59
+
23
60
  //go:linkname incrementalGetReferencedFiles github.com/microsoft/typescript-go/internal/execute/incremental.getReferencedFiles
24
61
  func incrementalGetReferencedFiles(program *innercompiler.Program, file *innerast.SourceFile) *collections.Set[tspath.Path]
25
62
 
@@ -6,7 +6,10 @@ import { resolveProjectConfig } from "./compiler/internal/project/resolveProject
6
6
  import { resolveBinary } from "./compiler/internal/resolveBinary";
7
7
  import { createProcessDiagnostic } from "./compiler/internal/runBuild";
8
8
  import { transformProjectInMemory } from "./compiler/internal/transformProjectInMemory";
9
- import { assertSafeExplicitCacheDirectory } from "./internal/assertSafeExplicitCacheDirectory";
9
+ import {
10
+ type SafeCacheCleanupTarget,
11
+ resolveSafeCacheCleanupTargets,
12
+ } from "./internal/resolveSafeCacheCleanupTargets";
10
13
  import { resolveCleanTargets } from "./plugin/internal/buildSourcePlugin";
11
14
  import { loadProjectPlugins } from "./plugin/internal/loadProjectPlugins";
12
15
  import type { ITtscCompilerContext } from "./structures/ITtscCompilerContext";
@@ -86,15 +89,16 @@ export class TtscCompiler {
86
89
  /**
87
90
  * Remove compiled cache artifacts for this compiler instance.
88
91
  *
89
- * Removes the resolved cache root (which holds both the plugin binaries and,
90
- * when ttsc-owned, the Go build cache), a ttsc-owned Go build cache that
91
- * lives outside that root (`TTSC_GO_CACHE_DIR`), and the two legacy
92
- * project-local caches. A user-provided `GOCACHE` is never removed. The cache
92
+ * Removes an explicit `cacheDir` wholesale. Otherwise removes the plugin
93
+ * binary subdirectory, a safely identified ttsc-owned Go build cache
94
+ * (including an external `TTSC_GO_CACHE_DIR`), and the two legacy
95
+ * project-local caches. A user-provided `GOCACHE` is never removed. Every
96
+ * resolved deletion target is validated before the first removal. The cache
93
97
  * location comes from this instance's `cacheDir` and environment
94
98
  * (`TTSC_CACHE_DIR` / `TTSC_GO_CACHE_DIR`), defaulting to
95
- * `<workspaceRoot>/node_modules/.cache/ttsc`. An explicit cache directory
96
- * that equals or contains the project, or names a filesystem root, is
97
- * rejected before any directory is removed.
99
+ * `<workspaceRoot>/node_modules/.cache/ttsc`. Any resolved cache target that
100
+ * equals or contains the project, or names a filesystem root, is rejected
101
+ * before any directory is removed.
98
102
  *
99
103
  * @returns Cache directories that were removed.
100
104
  */
@@ -105,31 +109,35 @@ export class TtscCompiler {
105
109
  path.join(projectRoot, ".ttsc"),
106
110
  ];
107
111
  const explicitCacheDir = this.resolveCacheDir();
112
+ let targets: string[];
108
113
  if (explicitCacheDir !== undefined) {
109
- assertSafeExplicitCacheDirectory(projectRoot, explicitCacheDir);
110
114
  // An explicit constructor `cacheDir` names the cache directory for this
111
115
  // instance — exactly like `ttsc clean --cache-dir X` on the CLI — so
112
116
  // remove it wholesale plus the legacy project-local caches. This keeps
113
117
  // the programmatic and CLI clean contracts identical for an explicit
114
118
  // cache dir.
115
- return removeExistingDirectories([explicitCacheDir, ...legacyTargets]);
116
- }
117
- // Default / `context.env.TTSC_CACHE_DIR`: resolve the cache root and the Go
118
- // build cache (`TTSC_GO_CACHE_DIR`) from this instance's effective
119
- // environment — the same `{ ...process.env, ...context.env }` that
120
- // prepare()/compile()/transform() build with — then remove only the
121
- // ttsc-owned subdirectories, so a possibly-shared root is never deleted and
122
- // a user-provided `GOCACHE` is never touched. Using the effective env (not
123
- // ambient `process.env`) makes clean() remove exactly the artifacts this
124
- // instance owns, including a `TTSC_GO_CACHE_DIR` supplied only in
125
- // `context.env`.
126
- return removeExistingDirectories(
127
- resolveCleanTargets(
119
+ targets = [explicitCacheDir, ...legacyTargets];
120
+ } else {
121
+ // Default / `context.env.TTSC_CACHE_DIR`: resolve the cache root and the Go
122
+ // build cache (`TTSC_GO_CACHE_DIR`) from this instance's effective
123
+ // environment — the same `{ ...process.env, ...context.env }` that
124
+ // prepare()/compile()/transform() build with — then remove only the
125
+ // ttsc-owned subdirectories, so a possibly-shared root is never deleted and
126
+ // a user-provided `GOCACHE` is never touched. Using the effective env (not
127
+ // ambient `process.env`) makes clean() remove exactly the artifacts this
128
+ // instance owns, including a `TTSC_GO_CACHE_DIR` supplied only in
129
+ // `context.env`.
130
+ targets = resolveCleanTargets(
128
131
  projectRoot,
129
132
  this.resolvePluginCacheDir(),
130
133
  this.resolveEffectiveEnv(),
131
- ),
132
- );
134
+ );
135
+ }
136
+ // Validate the complete deletion set before removing the first directory.
137
+ // This includes an environment-selected TTSC_GO_CACHE_DIR and project-local
138
+ // legacy locations, not only an explicit constructor cacheDir.
139
+ const safeTargets = resolveSafeCacheCleanupTargets(projectRoot, targets);
140
+ return removeExistingDirectories(safeTargets);
133
141
  }
134
142
 
135
143
  /**
@@ -234,24 +242,29 @@ export class TtscCompiler {
234
242
  * The effective environment for this instance's source-plugin builds and
235
243
  * clean targets: `context.env` merged over `process.env`, matching the
236
244
  * documented {@link ITtscCompilerContext.env} contract that child compiler,
237
- * native-plugin, and native-host processes already receive. Returned as a
238
- * fresh object so callers never mutate the shared `process.env`; when no
239
- * `context.env` was supplied this is a plain copy of `process.env`, so CLI /
240
- * default behavior is unchanged.
245
+ * native-plugin, native-host, and isolated descriptor processes already
246
+ * receive. Returned as a fresh object so callers never mutate the shared
247
+ * `process.env`; when no `context.env` was supplied this is a plain copy of
248
+ * `process.env`, so CLI / default behavior is unchanged.
241
249
  */
242
250
  private resolveEffectiveEnv(): NodeJS.ProcessEnv {
243
251
  return { ...process.env, ...this.context.env };
244
252
  }
245
253
  }
246
254
 
247
- function removeExistingDirectories(directories: readonly string[]): string[] {
255
+ function removeExistingDirectories(
256
+ directories: readonly SafeCacheCleanupTarget[],
257
+ ): string[] {
248
258
  const removed: string[] = [];
249
- for (const directory of [...new Set(directories)]) {
250
- if (!fs.existsSync(directory)) {
259
+ const visited = new Set<string>();
260
+ for (const directory of directories) {
261
+ if (visited.has(directory.path)) continue;
262
+ visited.add(directory.path);
263
+ if (!directory.exists || !fs.existsSync(directory.path)) {
251
264
  continue;
252
265
  }
253
- fs.rmSync(directory, { recursive: true, force: true });
254
- removed.push(directory);
266
+ fs.rmSync(directory.path, { recursive: true, force: true });
267
+ removed.push(directory.requestedPath);
255
268
  }
256
269
  return removed;
257
270
  }
@@ -265,6 +278,9 @@ interface ProjectTransformation {
265
278
  dependencies?: Record<string, string[]>;
266
279
  dependenciesComplete?: string[];
267
280
  graph?: ITtscCompilerTransformation.IReferenceGraph;
281
+ hostInputHashes?: Record<string, string | null>;
282
+ hostInputRealpaths?: Record<string, string | null>;
283
+ hostInputs?: string[];
268
284
  result: TtscBuildResult;
269
285
  typescript: Record<string, string>;
270
286
  volatile?: string[];
@@ -382,6 +398,9 @@ function toCompilerTransformation(
382
398
  dependencies,
383
399
  dependenciesComplete,
384
400
  graph,
401
+ hostInputHashes,
402
+ hostInputRealpaths,
403
+ hostInputs,
385
404
  result,
386
405
  typescript,
387
406
  volatile,
@@ -390,6 +409,9 @@ function toCompilerTransformation(
390
409
  ...(dependencies === undefined ? {} : { dependencies }),
391
410
  ...(dependenciesComplete === undefined ? {} : { dependenciesComplete }),
392
411
  ...(graph === undefined ? {} : { graph }),
412
+ ...(hostInputHashes === undefined ? {} : { hostInputHashes }),
413
+ ...(hostInputRealpaths === undefined ? {} : { hostInputRealpaths }),
414
+ ...(hostInputs === undefined ? {} : { hostInputs }),
393
415
  ...(volatile === undefined ? {} : { volatile }),
394
416
  };
395
417
  if (result.status === 0 && !hasErrorDiagnostics(result.diagnostics)) {
@@ -1,7 +1,8 @@
1
1
  import fs from "node:fs";
2
- import os from "node:os";
3
2
  import path from "node:path";
4
3
 
4
+ import { createCanonicalTempDirectory } from "../../internal/createCanonicalTempDirectory";
5
+
5
6
  export interface CapturedProcessOutput {
6
7
  /** Close the descriptors and remove the backing files. */
7
8
  dispose(): void;
@@ -32,7 +33,7 @@ export interface CapturedProcessOutput {
32
33
  * bytes.
33
34
  */
34
35
  export function captureProcessOutput(): CapturedProcessOutput {
35
- const directory = fs.mkdtempSync(path.join(os.tmpdir(), "ttsc-spawn-"));
36
+ const directory = createCanonicalTempDirectory("ttsc-spawn-");
36
37
  const stdoutPath = path.join(directory, "stdout");
37
38
  const stderrPath = path.join(directory, "stderr");
38
39
  const stdoutFd = fs.openSync(stdoutPath, "w+");
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
- import os from "node:os";
3
2
  import path from "node:path";
4
3
 
4
+ import { createCanonicalTempDirectory } from "../../internal/createCanonicalTempDirectory";
5
5
  import { hasProjectPluginEntries } from "../../plugin/internal/loadProjectPlugins";
6
6
  import type { ITtscCompilerContext } from "../../structures/ITtscCompilerContext";
7
7
  import type { ITtscCompilerDiagnostic } from "../../structures/ITtscCompilerDiagnostic";
@@ -11,6 +11,7 @@ import { buildNativeCompiler } from "./buildNativeCompiler";
11
11
  import { isOutsideRelativePath, packageRootDir } from "./paths";
12
12
  import { readProjectConfig } from "./project/readProjectConfig";
13
13
  import { runBuild } from "./runBuild";
14
+ import { inheritedSidecarEnv } from "./sharedHostHelpers";
14
15
  import { outputText, spawnNative } from "./spawnNative";
15
16
 
16
17
  /**
@@ -50,7 +51,7 @@ export function compileProjectInMemory(options: ITtscCompilerContext): {
50
51
  ["api-compile", "--cwd", project.root, "--tsconfig", tsconfig],
51
52
  {
52
53
  cwd: project.root,
53
- env: { ...process.env, ...options.env },
54
+ env: inheritedSidecarEnv(options.env),
54
55
  },
55
56
  );
56
57
  if (res.error) {
@@ -110,7 +111,7 @@ function compileProjectWithPlugins(
110
111
  output: Record<string, string>;
111
112
  result: TtscBuildResult;
112
113
  } {
113
- const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ttsc-api-output-"));
114
+ const tempRoot = createCanonicalTempDirectory("ttsc-api-output-");
114
115
  const tempOutDir = path.join(tempRoot, "out");
115
116
  try {
116
117
  const result = runBuild({
@@ -20,6 +20,8 @@ import { isOutsideRelativePath } from "./paths";
20
20
  * Returns `null` when no matching output file is found on disk.
21
21
  */
22
22
  export function resolveEmittedJavaScript(options: {
23
+ /** Skip trailing-stem recovery when exact source ownership is required. */
24
+ allowStemFallback?: boolean;
23
25
  /** Pre-computed list of emitted paths; when absent `outDir` is scanned. */
24
26
  emittedFiles?: readonly string[];
25
27
  outDir: string;
@@ -45,6 +47,10 @@ export function resolveEmittedJavaScript(options: {
45
47
  }
46
48
  }
47
49
 
50
+ if (options.allowStemFallback === false) {
51
+ return null;
52
+ }
53
+
48
54
  // Score the pre-computed emit list first (cheap). When it yields nothing —
49
55
  // because the list is incomplete (a native transform host such as typia emits
50
56
  // without printing the `--listEmittedFiles` lines) or because the emit landed
@@ -6,6 +6,7 @@ import {
6
6
  type ProjectInputPathIdentityContext,
7
7
  createProjectInputPathIdentityContext,
8
8
  } from "../../internal/projectInputPathIdentity";
9
+ import { resolveNodeBinary } from "../../internal/resolveNodeBinary";
9
10
  import {
10
11
  hasProjectPluginEntries,
11
12
  loadProjectPlugins,
@@ -26,7 +27,10 @@ import {
26
27
  import { resolveBinary } from "./resolveBinary";
27
28
  import { resolveTsgo } from "./resolveTsgo";
28
29
  import {
30
+ TSGO_ARGS_ENV,
29
31
  assertSharedHostCompatibility,
32
+ clearInheritedTsgoArgs,
33
+ inheritedSidecarEnv,
30
34
  linkedTransformPlugins,
31
35
  resolvePluginConfigDir,
32
36
  selectSharedHostPlugin,
@@ -101,13 +105,18 @@ type BuildTiming = {
101
105
  * `TTSC_NODE_BINARY` so child processes can re-invoke the same Node.js binary
102
106
  * without searching `PATH`.
103
107
  */
104
- function mergeEnv(extra?: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
105
- const base = {
108
+ function mergeEnv(
109
+ extra?: NodeJS.ProcessEnv,
110
+ cwd: string = process.cwd(),
111
+ ): NodeJS.ProcessEnv {
112
+ const env: NodeJS.ProcessEnv = {
106
113
  ...process.env,
107
- TTSC_NODE_BINARY: process.env.TTSC_NODE_BINARY ?? process.execPath,
114
+ ...extra,
108
115
  };
109
- if (!extra) return base;
110
- return { ...base, ...extra };
116
+ const node = resolveNodeBinary(env, cwd);
117
+ if (node === undefined) delete env.TTSC_NODE_BINARY;
118
+ else env.TTSC_NODE_BINARY = node;
119
+ return env;
111
120
  }
112
121
 
113
122
  function createBuildTiming(options: TtscCommonOptions): BuildTiming {
@@ -198,17 +207,29 @@ function nativePluginEnv(
198
207
  extra: NodeJS.ProcessEnv | undefined,
199
208
  execution: ReturnType<typeof resolveExecutionContext>,
200
209
  plugin?: ITtscLoadedNativePlugin,
210
+ tsgoArgs?: string,
201
211
  ): NodeJS.ProcessEnv {
202
- const env = mergeEnv({
203
- ...(execution.pluginConfigDir === undefined
204
- ? {}
205
- : { TTSC_PLUGIN_CONFIG_DIR: execution.pluginConfigDir }),
206
- TTSC_TSGO_BINARY: process.env.TTSC_TSGO_BINARY ?? execution.tsgo.binary,
207
- TTSC_TTSX_BINARY:
208
- process.env.TTSC_TTSX_BINARY ??
209
- path.join(__dirname, "..", "..", "launcher", "ttsx.js"),
210
- ...extra,
211
- });
212
+ const env = mergeEnv(
213
+ {
214
+ ...(execution.pluginConfigDir === undefined
215
+ ? {}
216
+ : { TTSC_PLUGIN_CONFIG_DIR: execution.pluginConfigDir }),
217
+ TTSC_TSGO_BINARY: process.env.TTSC_TSGO_BINARY ?? execution.tsgo.binary,
218
+ TTSC_TTSX_BINARY:
219
+ process.env.TTSC_TTSX_BINARY ??
220
+ path.join(__dirname, "..", "..", "launcher", "ttsx.js"),
221
+ ...extra,
222
+ },
223
+ execution.projectRoot,
224
+ );
225
+ // Forwarded tsgo argv is per-invocation state this host owns, exactly like
226
+ // the config anchor below: publish this run's payload, or drop whatever an
227
+ // ancestor ttsc process left behind when this lane forwards nothing.
228
+ if (tsgoArgs !== undefined) {
229
+ env[TSGO_ARGS_ENV] = tsgoArgs;
230
+ } else {
231
+ clearInheritedTsgoArgs(env, extra);
232
+ }
212
233
  // The anchor is per-invocation state owned by this host: when this run
213
234
  // declared none (and the caller's env does not name one), drop any value
214
235
  // inherited from an ancestor ttsc process so a nested build never
@@ -391,8 +412,11 @@ export class ResidentCheckWatchSession {
391
412
  stderr: "",
392
413
  stdout: "",
393
414
  };
394
- const checks = planResidentCheckEntries(execution.nativePlugins, (plugin) =>
395
- createNativeCheckArgs(execution, options, plugin),
415
+ const tsgoArgs = createNativeTsgoArgs(options);
416
+ const checks = planResidentCheckEntries(
417
+ execution.nativePlugins,
418
+ (plugin) => createNativeCheckArgs(execution, options, plugin),
419
+ tsgoArgs,
396
420
  );
397
421
  const request = residentCheckRequest(change, execution.projectRoot);
398
422
  // Buffer the cycle for every resident plugin before running any of them.
@@ -411,6 +435,7 @@ export class ResidentCheckWatchSession {
411
435
  "ttsc.check",
412
436
  timing,
413
437
  `ttsc check plugin ${plugin.name} time`,
438
+ tsgoArgs,
414
439
  );
415
440
  } else {
416
441
  const startedAt = process.hrtime.bigint();
@@ -420,7 +445,7 @@ export class ResidentCheckWatchSession {
420
445
  args: ["check-serve", ...args.slice(1)],
421
446
  binary: plugin.binary,
422
447
  cwd: execution.projectRoot,
423
- env: nativePluginEnv(options.env, execution, plugin),
448
+ env: nativePluginEnv(options.env, execution, plugin, tsgoArgs),
424
449
  });
425
450
  this.processes.set(key, resident);
426
451
  }
@@ -506,8 +531,9 @@ function residentCheckExecutionIsCompatible(
506
531
  function residentCheckProcessKey(
507
532
  plugin: ITtscLoadedNativePlugin,
508
533
  args: readonly string[],
534
+ tsgoArgs?: string,
509
535
  ): string {
510
- return `${plugin.binary}\0${plugin.name}\0${JSON.stringify(args)}`;
536
+ return `${plugin.binary}\0${plugin.name}\0${JSON.stringify(args)}\0${tsgoArgs ?? ""}`;
511
537
  }
512
538
 
513
539
  export type ResidentCheckEntryPlan = {
@@ -524,6 +550,11 @@ export type ResidentCheckEntryPlan = {
524
550
  export function planResidentCheckEntries(
525
551
  plugins: readonly ITtscLoadedNativePlugin[],
526
552
  createArgs: (plugin: ITtscLoadedNativePlugin) => string[],
553
+ // Part of the process identity even though it never appears in argv: the
554
+ // forwarded tsgo payload rides the environment, and a resident process holds
555
+ // the compiler options it was started with. Two cycles that forward different
556
+ // flags must not share one warm Program.
557
+ tsgoArgs?: string,
527
558
  ): ResidentCheckEntryPlan[] {
528
559
  return plugins
529
560
  .filter((candidate) => candidate.stage === "check")
@@ -534,7 +565,7 @@ export function planResidentCheckEntries(
534
565
  entryIndex,
535
566
  key:
536
567
  plugin.capabilities?.residentCheck === true
537
- ? residentCheckProcessKey(plugin, args)
568
+ ? residentCheckProcessKey(plugin, args, tsgoArgs)
538
569
  : undefined,
539
570
  plugin,
540
571
  };
@@ -830,7 +861,7 @@ function runProjectFreeTerminalFlag(
830
861
  const tsgo = resolveTsgo({ ...options, cwd });
831
862
  const res = spawnNative(tsgo.binary, [...(options.passthrough ?? [])], {
832
863
  cwd,
833
- env: mergeEnv(options.env),
864
+ env: mergeEnv(options.env, cwd),
834
865
  encoding: "utf8",
835
866
  });
836
867
  if (res.error) {
@@ -1126,6 +1157,7 @@ function buildWithNativeCompilerPlugins(
1126
1157
  "ttsc.build",
1127
1158
  timing,
1128
1159
  transformHostTimingLabel(plugins),
1160
+ createNativeBuildTsgoArgs(execution, options),
1129
1161
  );
1130
1162
  }
1131
1163
 
@@ -1150,7 +1182,7 @@ function runTsgo(
1150
1182
  ],
1151
1183
  {
1152
1184
  cwd: execution.projectRoot,
1153
- env: mergeEnv(options.env),
1185
+ env: mergeEnv(options.env, execution.projectRoot),
1154
1186
  encoding: "utf8",
1155
1187
  },
1156
1188
  );
@@ -1184,7 +1216,7 @@ function runTsgoBuild(
1184
1216
  ): TtscBuildResult {
1185
1217
  const res = spawnNative(execution.tsgo.binary, args, {
1186
1218
  cwd: execution.projectRoot,
1187
- env: mergeEnv(options.env),
1219
+ env: mergeEnv(options.env, execution.projectRoot),
1188
1220
  encoding: "utf8",
1189
1221
  });
1190
1222
  if (res.error) {
@@ -1401,15 +1433,23 @@ function createNativeBuildArgs(
1401
1433
  args.push("--quiet");
1402
1434
  }
1403
1435
  }
1404
- // The sidecar builds its Program in-process, so the pinned `rootDir` has to
1405
- // travel the same channel every other tsgo option takes to it — the host flag
1406
- // set does not declare `--rootDir`, and `filterHostArgs` would strip it.
1407
- args.push(
1408
- ...createNativeTsgoArgs(options, pinnedRootDirArgs(execution, options)),
1409
- );
1410
1436
  return args;
1411
1437
  }
1412
1438
 
1439
+ /**
1440
+ * The forwarded-tsgo payload for a native `build`/`check` emit invocation.
1441
+ *
1442
+ * The sidecar builds its Program in-process, so the pinned `rootDir` has to
1443
+ * travel the same channel every other tsgo option takes to it — the host flag
1444
+ * set does not declare `--rootDir`, and `filterHostArgs` would strip it.
1445
+ */
1446
+ function createNativeBuildTsgoArgs(
1447
+ execution: ReturnType<typeof resolveExecutionContext>,
1448
+ options: RunBuildOptions,
1449
+ ): string | undefined {
1450
+ return createNativeTsgoArgs(options, pinnedRootDirArgs(execution, options));
1451
+ }
1452
+
1413
1453
  /** Build the argument list for a native plugin check/fix/format invocation. */
1414
1454
  function createNativeCheckArgs(
1415
1455
  execution: ReturnType<typeof resolveExecutionContext>,
@@ -1440,7 +1480,6 @@ function createNativeCheckArgs(
1440
1480
  }
1441
1481
  args.push(...createNativeCheckThreadingArgs(options, plugin));
1442
1482
  args.push(...createNativeCheckDiagnosticsArgs(options, plugin));
1443
- args.push(...createNativeTsgoArgs(options));
1444
1483
  return args;
1445
1484
  }
1446
1485
 
@@ -1550,15 +1589,37 @@ function transformHostTimingLabel(
1550
1589
 
1551
1590
  /**
1552
1591
  * Forward the tsgo flags ttsc did not recognize to a native sidecar as one
1553
- * JSON-encoded `--tsgo-args` flag. The sidecar replays them through tsgo's own
1554
- * option parser onto `CompilerOptions`, so a flag like `ttsc --strict` reaches
1555
- * a plugin build the same way it reaches the plain tsgo lane. Encoded as a
1556
- * single token so the sidecars' unknown-flag filters keep it intact.
1592
+ * JSON-encoded payload. The sidecar replays them through tsgo's own option
1593
+ * parser onto `CompilerOptions`, so a flag like `ttsc --strict` reaches a
1594
+ * plugin build the same way it reaches the plain tsgo lane.
1595
+ *
1596
+ * The payload travels in the `TTSC_TSGO_ARGS` environment variable, not on the
1597
+ * sidecar's command line. #113 shipped it as a `--tsgo-args` flag, which is an
1598
+ * addition to a plugin protocol third-party hosts had already frozen: a Go
1599
+ * `flag.FlagSet` created with `flag.ContinueOnError` treats an undeclared flag
1600
+ * as fatal, so every pre-#113 sidecar answered `flag provided but not defined:
1601
+ * -tsgo-args` and exited 2. That took down `ttsc --strict`, `ttsc
1602
+ * --declaration`, `ttsx --strict` and — because {@link isolatedTsgoOutputArgs}
1603
+ * makes this payload non-empty on its own — plain `ttsc <file.ts>`, on every
1604
+ * project carrying a typia/nestia-era transform host (issue #1188).
1605
+ *
1606
+ * The environment is the channel ttsc already uses for host-owned payloads that
1607
+ * must not collide with a third-party flag set (`TTSC_LINKED_PLUGINS_JSON`,
1608
+ * `TTSC_PLUGIN_CONFIG_DIR`). It reaches those hosts without any change on their
1609
+ * side, because `buildSourcePlugin.ts::sourceBuildWorkspaceReplacements` builds
1610
+ * every source plugin against the installed ttsc's own driver, and
1611
+ * `driver.LoadProgram` reads the variable whenever the caller supplied no
1612
+ * explicit argv. It is strictly better than the capability gate `ad3443a` used
1613
+ * for `--singleThreaded` / `--checkers`: that one drops the flag for hosts that
1614
+ * cannot take it, which is acceptable for a threading knob and not for
1615
+ * `--strict`.
1616
+ *
1617
+ * Returns the JSON payload, or `undefined` when this lane forwards nothing.
1557
1618
  */
1558
1619
  function createNativeTsgoArgs(
1559
1620
  options: TtscCommonOptions,
1560
1621
  leading: readonly string[] = [],
1561
- ): string[] {
1622
+ ): string | undefined {
1562
1623
  const passthrough = [
1563
1624
  // Ahead of the user's own flags, so the same precedence holds here as on
1564
1625
  // the direct tsgo lane: whatever the user forwarded wins.
@@ -1567,9 +1628,9 @@ function createNativeTsgoArgs(
1567
1628
  ...isolatedTsgoOutputArgs(options),
1568
1629
  ];
1569
1630
  if (passthrough.length === 0) {
1570
- return [];
1631
+ return undefined;
1571
1632
  }
1572
- return ["--tsgo-args=" + JSON.stringify(passthrough)];
1633
+ return JSON.stringify(passthrough);
1573
1634
  }
1574
1635
 
1575
1636
  function isolatedTsgoOutputArgs(options: TtscCommonOptions): string[] {
@@ -1678,6 +1739,7 @@ function runNativeCheckPlugins(
1678
1739
  "ttsc.check",
1679
1740
  timing,
1680
1741
  `ttsc check plugin ${plugin.name} time`,
1742
+ createNativeTsgoArgs(options),
1681
1743
  );
1682
1744
  out = appendBuildOutput(out, result);
1683
1745
  if (result.status !== 0) {
@@ -1949,11 +2011,12 @@ function runNativePluginCommand(
1949
2011
  label: string,
1950
2012
  timing: BuildTiming,
1951
2013
  timingLabel: string,
2014
+ tsgoArgs?: string,
1952
2015
  ): TtscBuildResult {
1953
2016
  const startedAt = process.hrtime.bigint();
1954
2017
  const res = spawnNative(plugin.binary, args, {
1955
2018
  cwd: execution.projectRoot,
1956
- env: nativePluginEnv(options.env, execution, plugin),
2019
+ env: nativePluginEnv(options.env, execution, plugin, tsgoArgs),
1957
2020
  encoding: "utf8",
1958
2021
  });
1959
2022
  recordTiming(timing, timingLabel, startedAt);
@@ -2047,7 +2110,7 @@ function resolveExecutionContext(
2047
2110
  cacheDir: options.cacheDir ?? options.env?.TTSC_CACHE_DIR,
2048
2111
  cwd,
2049
2112
  entries: options.plugins,
2050
- env: { ...process.env, ...options.env },
2113
+ env: inheritedSidecarEnv(options.env),
2051
2114
  onWatchInputs: options.onWatchInputs,
2052
2115
  pluginConfigDir: options.pluginConfigDir,
2053
2116
  projectRoot,
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
- import os from "node:os";
3
2
  import path from "node:path";
4
3
 
4
+ import { createCanonicalTempDirectory } from "../../internal/createCanonicalTempDirectory";
5
5
  import type { TtscSingleFileEmitOptions } from "../../structures/internal/TtscSingleFileEmitOptions";
6
6
  import { readProjectConfig } from "./project/readProjectConfig";
7
7
  import { resolveEmittedJavaScript } from "./resolveEmittedJavaScript";
@@ -32,7 +32,7 @@ export function runSingleFileEmit(options: TtscSingleFileEmitOptions): string {
32
32
  });
33
33
  const tsconfig = project.path;
34
34
  const projectRoot = project.root;
35
- const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "ttsc-single-file-"));
35
+ const outDir = createCanonicalTempDirectory("ttsc-single-file-");
36
36
  try {
37
37
  const result = runBuild({
38
38
  ...options,