ttsc 0.18.4 → 0.19.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 (137) hide show
  1. package/README.md +5 -6
  2. package/cmd/graphbench/main.go +1 -1
  3. package/cmd/platform/main.go +1 -48
  4. package/cmd/ttsc/build_branches_test.go +5 -1
  5. package/cmd/ttsc/main.go +0 -50
  6. package/cmd/ttsc/main_alias_branches_test.go +6 -12
  7. package/cmd/ttscgraph/dump.go +13 -3
  8. package/cmd/ttscserver/main.go +5 -3
  9. package/driver/lsp.go +13 -1
  10. package/driver/plugins.go +35 -0
  11. package/driver/program.go +7 -8
  12. package/driver/rewrite.go +381 -21
  13. package/go.mod +1 -0
  14. package/internal/graph/build.go +297 -3
  15. package/internal/graph/build_records_a_node_per_top_level_declaration_test.go +16 -2
  16. package/internal/graph/diagnostics.go +5 -4
  17. package/internal/graph/dump.go +60 -4
  18. package/internal/graph/dump_is_a_function_of_the_source_test.go +71 -0
  19. package/internal/graph/dump_serializes_the_full_graph_test.go +16 -4
  20. package/internal/graph/edges.go +122 -11
  21. package/internal/graph/exports.go +74 -17
  22. package/internal/graph/graph.go +33 -1
  23. package/internal/graph/node_identity_is_position_invariant_test.go +4 -2
  24. package/internal/graph/resolve.go +24 -0
  25. package/internal/graphsymbols/provider.go +50 -14
  26. package/internal/lspserver/lsp_native_plugin_source.go +77 -23
  27. package/internal/lspserver/lsp_plugin.go +19 -2
  28. package/internal/lspserver/lsp_proxy.go +244 -52
  29. package/internal/lspserver/lsp_server.go +35 -27
  30. package/internal/lspserver/lsp_symbols.go +25 -16
  31. package/lib/TtscCompiler.d.ts +10 -0
  32. package/lib/TtscCompiler.js +24 -6
  33. package/lib/TtscCompiler.js.map +1 -1
  34. package/lib/TtscService.js +9 -7
  35. package/lib/TtscService.js.map +1 -1
  36. package/lib/compiler/internal/compileProjectInMemory.js +1 -0
  37. package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
  38. package/lib/compiler/internal/project/createNativeProjectContextArgs.d.ts +3 -0
  39. package/lib/compiler/internal/project/createNativeProjectContextArgs.js +14 -0
  40. package/lib/compiler/internal/project/createNativeProjectContextArgs.js.map +1 -0
  41. package/lib/compiler/internal/project/readProjectConfig.js +53 -4
  42. package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
  43. package/lib/compiler/internal/project/resolveProjectConfig.d.ts +9 -2
  44. package/lib/compiler/internal/project/resolveProjectConfig.js +45 -16
  45. package/lib/compiler/internal/project/resolveProjectConfig.js.map +1 -1
  46. package/lib/compiler/internal/residentTransformProcess.d.ts +16 -5
  47. package/lib/compiler/internal/residentTransformProcess.js +68 -20
  48. package/lib/compiler/internal/residentTransformProcess.js.map +1 -1
  49. package/lib/compiler/internal/runBuild.d.ts +11 -0
  50. package/lib/compiler/internal/runBuild.js +44 -8
  51. package/lib/compiler/internal/runBuild.js.map +1 -1
  52. package/lib/compiler/internal/runSingleFileEmit.js +7 -4
  53. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  54. package/lib/compiler/internal/sharedHostHelpers.d.ts +16 -0
  55. package/lib/compiler/internal/sharedHostHelpers.js +23 -0
  56. package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
  57. package/lib/compiler/internal/startResidentTransform.js +18 -1
  58. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  59. package/lib/compiler/internal/transformProjectInMemory.js +34 -8
  60. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  61. package/lib/flags/parser.d.ts +17 -0
  62. package/lib/flags/parser.js +11 -1
  63. package/lib/flags/parser.js.map +1 -1
  64. package/lib/flags/schema.js +7 -0
  65. package/lib/flags/schema.js.map +1 -1
  66. package/lib/launcher/internal/prepareExecution.js +21 -2
  67. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  68. package/lib/launcher/internal/runTtsc.js +13 -53
  69. package/lib/launcher/internal/runTtsc.js.map +1 -1
  70. package/lib/launcher/internal/runTtscserver.js +11 -0
  71. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  72. package/lib/launcher/internal/runTtsx.js +27 -11
  73. package/lib/launcher/internal/runTtsx.js.map +1 -1
  74. package/lib/launcher/internal/runtimeHooks.d.ts +81 -0
  75. package/lib/launcher/internal/runtimeHooks.js +632 -43
  76. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  77. package/lib/launcher/internal/servedSourceMap.d.ts +25 -0
  78. package/lib/launcher/internal/servedSourceMap.js +164 -0
  79. package/lib/launcher/internal/servedSourceMap.js.map +1 -0
  80. package/lib/plugin/internal/buildSourcePlugin.d.ts +146 -2
  81. package/lib/plugin/internal/buildSourcePlugin.js +651 -113
  82. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  83. package/lib/plugin/internal/loadProjectPlugins.d.ts +7 -0
  84. package/lib/plugin/internal/loadProjectPlugins.js +33 -10
  85. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  86. package/lib/structures/ITtscCompilerContext.d.ts +16 -2
  87. package/lib/structures/ITtscPluginCapabilities.d.ts +8 -0
  88. package/lib/structures/ITtscPluginFactoryContext.d.ts +14 -0
  89. package/lib/structures/internal/ITtscParsedProjectConfig.d.ts +3 -0
  90. package/lib/structures/internal/ITtscProjectIdentity.d.ts +17 -0
  91. package/lib/structures/internal/ITtscProjectIdentity.js +3 -0
  92. package/lib/structures/internal/ITtscProjectIdentity.js.map +1 -0
  93. package/lib/structures/internal/TtscCommonOptions.d.ts +8 -0
  94. package/package.json +11 -8
  95. package/shim/ast/lint.go +10 -0
  96. package/shim/ast/shim.go +44 -0
  97. package/shim/checker/enums_gen.go +19 -0
  98. package/shim/checker/shim.go +130 -0
  99. package/shim/diagnosticwriter/lint.go +30 -3
  100. package/shim/scanner/regexp.go +27 -0
  101. package/src/TtscCompiler.ts +25 -6
  102. package/src/TtscService.ts +15 -7
  103. package/src/compiler/internal/compileProjectInMemory.ts +1 -0
  104. package/src/compiler/internal/project/createNativeProjectContextArgs.ts +15 -0
  105. package/src/compiler/internal/project/readProjectConfig.ts +61 -5
  106. package/src/compiler/internal/project/resolveProjectConfig.ts +51 -21
  107. package/src/compiler/internal/residentTransformProcess.ts +95 -21
  108. package/src/compiler/internal/runBuild.ts +76 -9
  109. package/src/compiler/internal/runSingleFileEmit.ts +7 -4
  110. package/src/compiler/internal/sharedHostHelpers.ts +24 -0
  111. package/src/compiler/internal/startResidentTransform.ts +21 -1
  112. package/src/compiler/internal/transformProjectInMemory.ts +49 -6
  113. package/src/flags/parser.ts +28 -1
  114. package/src/flags/schema.ts +8 -0
  115. package/src/launcher/internal/prepareExecution.ts +22 -2
  116. package/src/launcher/internal/runTtsc.ts +13 -61
  117. package/src/launcher/internal/runTtscserver.ts +14 -0
  118. package/src/launcher/internal/runTtsx.ts +28 -13
  119. package/src/launcher/internal/runtimeHooks.ts +771 -53
  120. package/src/launcher/internal/servedSourceMap.ts +183 -0
  121. package/src/plugin/internal/buildSourcePlugin.ts +859 -122
  122. package/src/plugin/internal/loadProjectPlugins.ts +36 -6
  123. package/src/structures/ITtscCompilerContext.ts +17 -2
  124. package/src/structures/ITtscPluginCapabilities.ts +9 -0
  125. package/src/structures/ITtscPluginFactoryContext.ts +15 -0
  126. package/src/structures/internal/ITtscParsedProjectConfig.ts +3 -0
  127. package/src/structures/internal/ITtscProjectIdentity.ts +17 -0
  128. package/src/structures/internal/TtscCommonOptions.ts +8 -0
  129. package/tools/shim_audit/baseline.json +16 -7
  130. package/tools/shim_audit/main.go +777 -71
  131. package/tools/shim_audit/main_test.go +28 -1
  132. package/tools/shim_audit/producer_exemptions_require_rationale_test.go +42 -0
  133. package/tools/shim_audit/producer_surface_includes_exposed_methods_test.go +154 -0
  134. package/tools/shim_audit/producer_surface_rejects_unproducible_reference_test.go +142 -0
  135. package/utility/host.go +46 -20
  136. package/utility/serve.go +1 -1
  137. package/shim/checker/signature_api_completeness.go +0 -58
@@ -5,7 +5,7 @@ import path from "node:path";
5
5
  import type { ITtscProjectPluginConfig } from "../../../structures/ITtscProjectPluginConfig";
6
6
  import type { ITtscParsedProjectConfig } from "../../../structures/internal/ITtscParsedProjectConfig";
7
7
  import type { ITtscProjectLocatorOptions } from "../../../structures/internal/ITtscProjectLocatorOptions";
8
- import { resolveProjectConfig } from "./resolveProjectConfig";
8
+ import { resolveProjectIdentity } from "./resolveProjectConfig";
9
9
 
10
10
  /**
11
11
  * Compiler option keys whose values are file-system paths that must be resolved
@@ -40,10 +40,9 @@ type ResolvedCompilerOptions = {
40
40
  export function readProjectConfig(
41
41
  opts: ITtscProjectLocatorOptions = {},
42
42
  ): ITtscParsedProjectConfig {
43
- const tsconfig = resolveProjectConfig(opts);
44
- const root = opts.projectRoot
45
- ? path.resolve(opts.cwd ?? process.cwd(), opts.projectRoot)
46
- : path.dirname(tsconfig);
43
+ const identity = resolveProjectIdentity(opts);
44
+ const tsconfig = identity.physicalConfigPath;
45
+ const root = identity.physicalProjectRoot;
47
46
  const compilerOptions = readResolvedCompilerOptions(tsconfig);
48
47
  return {
49
48
  compilerOptions: {
@@ -51,6 +50,7 @@ export function readProjectConfig(
51
50
  outDir: compilerOptions.outDir,
52
51
  plugins: compilerOptions.plugins,
53
52
  },
53
+ identity,
54
54
  path: tsconfig,
55
55
  pluginBaseDirs: compilerOptions.pluginBaseDirs,
56
56
  root,
@@ -254,6 +254,14 @@ function resolveExtendsConfig(tsconfig: string, specifier: string): string {
254
254
  return resolveExistingExtendsPath(path.resolve(baseDir, specifier));
255
255
  }
256
256
  const resolver = createRequire(tsconfig);
257
+ // A bare package root selects its preset through `package.json#tsconfig`,
258
+ // matching TypeScript's config resolution. Presets shipped this way often
259
+ // have no JavaScript/JSON entrypoint at all, so Node's entrypoint resolver
260
+ // and the `<specifier>.json` fallback below both miss them.
261
+ const viaManifest = resolvePackageManifestTsconfig(resolver, specifier);
262
+ if (viaManifest !== undefined) {
263
+ return resolveExistingExtendsPath(viaManifest);
264
+ }
257
265
  try {
258
266
  return resolveRealPath(resolver.resolve(specifier));
259
267
  } catch {
@@ -261,6 +269,54 @@ function resolveExtendsConfig(tsconfig: string, specifier: string): string {
261
269
  }
262
270
  }
263
271
 
272
+ /**
273
+ * When `specifier` names a bare package root, resolve the config file its
274
+ * `package.json#tsconfig` field selects (anchored at the package directory).
275
+ * Returns `undefined` when the specifier is a subpath, the manifest is
276
+ * unresolvable/unparsable, or it declares no `tsconfig` field, so the caller
277
+ * falls back to Node entrypoint resolution.
278
+ */
279
+ function resolvePackageManifestTsconfig(
280
+ resolver: NodeRequire,
281
+ specifier: string,
282
+ ): string | undefined {
283
+ if (!isBarePackageRoot(specifier)) {
284
+ return undefined;
285
+ }
286
+ let manifestPath: string;
287
+ try {
288
+ manifestPath = resolver.resolve(`${specifier}/package.json`);
289
+ } catch {
290
+ return undefined;
291
+ }
292
+ let field: unknown;
293
+ try {
294
+ field = (
295
+ JSON.parse(stripLeadingBom(fs.readFileSync(manifestPath, "utf8"))) as {
296
+ tsconfig?: unknown;
297
+ }
298
+ ).tsconfig;
299
+ } catch {
300
+ return undefined;
301
+ }
302
+ if (typeof field !== "string" || field.length === 0) {
303
+ return undefined;
304
+ }
305
+ return path.resolve(path.dirname(manifestPath), field);
306
+ }
307
+
308
+ /**
309
+ * Return true when `specifier` is a bare package root (no subpath): a plain
310
+ * package name (`preset`) or a scoped name (`@scope/preset`). Subpaths such as
311
+ * `@scope/preset/base.json` resolve directly and keep their current meaning.
312
+ */
313
+ function isBarePackageRoot(specifier: string): boolean {
314
+ if (specifier.startsWith("@")) {
315
+ return specifier.split("/").length === 2;
316
+ }
317
+ return !specifier.includes("/");
318
+ }
319
+
264
320
  /**
265
321
  * Given an on-disk location, try the path as-is, with `.json` appended, and as
266
322
  * a directory containing `tsconfig.json`. Returns the first match or throws.
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
 
4
+ import type { ITtscProjectIdentity } from "../../../structures/internal/ITtscProjectIdentity";
4
5
  import type { ITtscProjectLocatorOptions } from "../../../structures/internal/ITtscProjectLocatorOptions";
5
6
 
6
7
  /**
@@ -13,13 +14,29 @@ import type { ITtscProjectLocatorOptions } from "../../../structures/internal/IT
13
14
  * walking up from the file's directory.
14
15
  * 3. `opts.cwd` — the nearest ancestor config walking up from cwd.
15
16
  *
16
- * Always returns a real (symlink-resolved) absolute path. Throws when no config
17
- * is found or the explicitly supplied path does not exist.
17
+ * Returns the real (symlink-resolved) absolute config path. Use
18
+ * {@link resolveProjectIdentity} when both the caller-selected spelling and the
19
+ * Program identity are required.
18
20
  */
19
21
  export function resolveProjectConfig(
20
22
  opts: ITtscProjectLocatorOptions = {},
21
23
  ): string {
24
+ return resolveProjectIdentity(opts).physicalConfigPath;
25
+ }
26
+
27
+ /**
28
+ * Resolve the selected config while retaining its lexical spelling separately
29
+ * from the physical paths used by the TypeScript Program.
30
+ */
31
+ export function resolveProjectIdentity(
32
+ opts: ITtscProjectLocatorOptions = {},
33
+ ): Omit<ITtscProjectIdentity, "pluginConfigOrigin"> {
22
34
  const cwd = path.resolve(opts.cwd ?? process.cwd());
35
+ const explicitProjectRoot =
36
+ opts.projectRoot === undefined || opts.projectRoot === ""
37
+ ? undefined
38
+ : resolveAbsolutePath(cwd, opts.projectRoot);
39
+ let logicalConfigPath: string;
23
40
  if (opts.tsconfig) {
24
41
  const resolved = resolveAbsolutePath(cwd, opts.tsconfig);
25
42
  if (!fs.existsSync(resolved)) {
@@ -35,30 +52,43 @@ export function resolveProjectConfig(
35
52
  if (isDirectory(resolved)) {
36
53
  const tsconfigInDir = path.join(resolved, "tsconfig.json");
37
54
  if (fs.existsSync(tsconfigInDir)) {
38
- return resolveRealPath(tsconfigInDir);
39
- }
40
- const jsconfigInDir = path.join(resolved, "jsconfig.json");
41
- if (fs.existsSync(jsconfigInDir)) {
42
- return resolveRealPath(jsconfigInDir);
55
+ logicalConfigPath = tsconfigInDir;
56
+ } else {
57
+ const jsconfigInDir = path.join(resolved, "jsconfig.json");
58
+ if (fs.existsSync(jsconfigInDir)) {
59
+ logicalConfigPath = jsconfigInDir;
60
+ } else {
61
+ throw new Error(
62
+ `ttsc: directory has no tsconfig.json / jsconfig.json: ${resolved}`,
63
+ );
64
+ }
43
65
  }
66
+ } else {
67
+ logicalConfigPath = resolved;
68
+ }
69
+ } else {
70
+ const start = opts.file ? resolveAbsolutePath(cwd, opts.file) : cwd;
71
+ const from = isDirectory(start) ? start : path.dirname(start);
72
+ const found = findUp(from, ["tsconfig.json", "jsconfig.json"]);
73
+ if (!found) {
44
74
  throw new Error(
45
- `ttsc: directory has no tsconfig.json / jsconfig.json: ${resolved}`,
75
+ `ttsc: could not find tsconfig.json or jsconfig.json starting from ${from}`,
46
76
  );
47
77
  }
48
- return resolveRealPath(resolved);
49
- }
50
-
51
- const start = opts.file
52
- ? resolveRealPath(resolveAbsolutePath(cwd, opts.file))
53
- : cwd;
54
- const from = isDirectory(start) ? start : path.dirname(start);
55
- const found = findUp(from, ["tsconfig.json", "jsconfig.json"]);
56
- if (!found) {
57
- throw new Error(
58
- `ttsc: could not find tsconfig.json or jsconfig.json starting from ${from}`,
59
- );
78
+ logicalConfigPath = found;
60
79
  }
61
- return resolveRealPath(found);
80
+ const physicalConfigPath = resolveRealPath(logicalConfigPath);
81
+ const physicalProjectRoot = resolveRealPath(
82
+ explicitProjectRoot ?? path.dirname(physicalConfigPath),
83
+ );
84
+ return {
85
+ ...(explicitProjectRoot === undefined ? {} : { explicitProjectRoot }),
86
+ invocationCwd: cwd,
87
+ logicalConfigPath,
88
+ logicalProjectRoot: path.dirname(logicalConfigPath),
89
+ physicalConfigPath,
90
+ physicalProjectRoot,
91
+ };
62
92
  }
63
93
 
64
94
  /** Resolve `target` against `cwd` when it is not already absolute. */
@@ -4,6 +4,19 @@ import { type Interface, createInterface } from "node:readline";
4
4
  /** Cap on retained stderr so a long-lived host cannot grow it without bound. */
5
5
  const STDERR_TAIL_LIMIT = 64 * 1024;
6
6
 
7
+ /** Cap on how much of an offending line an error message echoes back. */
8
+ const REPLY_ECHO_LIMIT = 200;
9
+
10
+ /**
11
+ * The operation a request expects a reply for. The host answers a transform
12
+ * request (`{"file":...}`) with `{"typescript":...,"found":...}` and an update
13
+ * request (`{"update":...,"content":...}`) with `{"updated":...}`. The client
14
+ * knows which it sent, so it validates the reply's shape against the operation
15
+ * before handing it back — a well-formed JSON object of the wrong operation
16
+ * shape is a protocol error, not a valid negative result.
17
+ */
18
+ export type ResidentReplyKind = "transform" | "update";
19
+
7
20
  /** Options for spawning the resident transform host. */
8
21
  export interface ResidentTransformProcessOptions {
9
22
  args: readonly string[];
@@ -13,6 +26,7 @@ export interface ResidentTransformProcessOptions {
13
26
  }
14
27
 
15
28
  interface PendingRequest {
29
+ kind: ResidentReplyKind;
16
30
  reject: (reason: Error) => void;
17
31
  resolve: (reply: Record<string, unknown>) => void;
18
32
  }
@@ -78,13 +92,16 @@ export class ResidentTransformProcess {
78
92
  }
79
93
 
80
94
  /**
81
- * Send one request to the host and resolve with its parsed JSON reply. The
82
- * host answers in FIFO order, so the caller interprets the reply shape by the
83
- * payload it sent (a transform reply vs an update reply). Rejects when the
84
- * host has already failed or exited, or if writing the request fails.
95
+ * Send one request to the host and resolve with its validated JSON reply. The
96
+ * host answers in FIFO order; `kind` tells the client which reply shape the
97
+ * payload asks for, so the reply is validated as a well-formed `kind` reply
98
+ * before it resolves. Rejects when the reply is not a valid `kind` reply,
99
+ * when the host has already failed or exited, or if writing the request
100
+ * fails.
85
101
  */
86
102
  public request(
87
103
  payload: Record<string, unknown>,
104
+ kind: ResidentReplyKind,
88
105
  ): Promise<Record<string, unknown>> {
89
106
  if (this.failure !== undefined) {
90
107
  return Promise.reject(this.failure);
@@ -96,7 +113,7 @@ export class ResidentTransformProcess {
96
113
  );
97
114
  }
98
115
  return new Promise<Record<string, unknown>>((resolve, reject) => {
99
- const pending: PendingRequest = { reject, resolve };
116
+ const pending: PendingRequest = { kind, reject, resolve };
100
117
  this.pending.push(pending);
101
118
  stdin.write(`${JSON.stringify(payload)}\n`, (error) => {
102
119
  if (error) {
@@ -149,7 +166,39 @@ export class ResidentTransformProcess {
149
166
  }
150
167
  return;
151
168
  }
152
- request.resolve(parseReply(trimmed));
169
+ const reply = parseReplyObject(trimmed);
170
+ if (reply === undefined) {
171
+ // Framing violation: the line is not a JSON object, so it cannot
172
+ // represent any reply. This request already left the FIFO, so reject it,
173
+ // then fail the whole process: a bad line may be corruption that shifted
174
+ // the stream, and the host's real reply that follows must not be paired
175
+ // with a later request. `fail` marks the failure so that trailing line is
176
+ // treated as benign instead of unsolicited.
177
+ const error = new Error(
178
+ `ttsc: resident transform host sent a malformed reply: ${echoLine(
179
+ trimmed,
180
+ )}`,
181
+ );
182
+ request.reject(error);
183
+ this.fail(error);
184
+ return;
185
+ }
186
+ if (!isValidReply(reply, request.kind)) {
187
+ // Operation-shape violation: a well-formed JSON object that is not a valid
188
+ // reply for the operation this request sent. FIFO framing is intact — one
189
+ // line consumed exactly one slot — so only this request is corrupt; later
190
+ // replies still pair correctly. Reject just this request instead of
191
+ // failing the whole process.
192
+ request.reject(
193
+ new Error(
194
+ `ttsc: resident transform host sent an invalid ${request.kind} reply: ${echoLine(
195
+ trimmed,
196
+ )}`,
197
+ ),
198
+ );
199
+ return;
200
+ }
201
+ request.resolve(reply);
153
202
  }
154
203
 
155
204
  private onReaderClose(): void {
@@ -194,23 +243,48 @@ export class ResidentTransformProcess {
194
243
  }
195
244
 
196
245
  /**
197
- * Parse one reply line into a plain object. A malformed line degrades to an
198
- * empty object rather than throwing, so one bad line never rejects a request
199
- * the FIFO has already advanced past; the caller reads missing fields as
200
- * absent.
246
+ * Parse one reply line into a plain object, or `undefined` when the line is not
247
+ * a JSON object. Invalid JSON, arrays, primitives, and `null` all yield
248
+ * `undefined`: none of them can carry a reply's fields, so the caller treats
249
+ * them as a framing failure rather than an empty reply. Returning `{}` here
250
+ * would let a corrupt line masquerade as a valid negative result.
201
251
  */
202
- function parseReply(line: string): Record<string, unknown> {
252
+ function parseReplyObject(line: string): Record<string, unknown> | undefined {
253
+ let parsed: unknown;
203
254
  try {
204
- const parsed = JSON.parse(line) as unknown;
205
- if (
206
- typeof parsed === "object" &&
207
- parsed !== null &&
208
- !Array.isArray(parsed)
209
- ) {
210
- return parsed as Record<string, unknown>;
211
- }
255
+ parsed = JSON.parse(line);
212
256
  } catch {
213
- // A non-object or unparseable line is treated as an empty reply.
257
+ return undefined;
258
+ }
259
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
260
+ return parsed as Record<string, unknown>;
261
+ }
262
+ return undefined;
263
+ }
264
+
265
+ /**
266
+ * Whether a parsed reply object is a well-formed reply for the given operation.
267
+ * A transform reply must carry a boolean `found`, and when `found` is `true` a
268
+ * string `typescript` (a found file always carries its transformed text); when
269
+ * `found` is `false` the text is irrelevant. An update reply must carry a
270
+ * boolean `updated`. Every other object shape is a protocol error.
271
+ */
272
+ function isValidReply(
273
+ reply: Record<string, unknown>,
274
+ kind: ResidentReplyKind,
275
+ ): boolean {
276
+ if (kind === "transform") {
277
+ if (typeof reply.found !== "boolean") {
278
+ return false;
279
+ }
280
+ return reply.found ? typeof reply.typescript === "string" : true;
214
281
  }
215
- return {};
282
+ return typeof reply.updated === "boolean";
283
+ }
284
+
285
+ /** Truncate an offending reply line so error messages stay bounded. */
286
+ function echoLine(line: string): string {
287
+ return line.length > REPLY_ECHO_LIMIT
288
+ ? `${line.slice(0, REPLY_ECHO_LIMIT)}…`
289
+ : line;
216
290
  }
@@ -7,15 +7,18 @@ import {
7
7
  } from "../../plugin/internal/loadProjectPlugins";
8
8
  import type { ITtscCompilerDiagnostic } from "../../structures/ITtscCompilerDiagnostic";
9
9
  import type { ITtscLoadedNativePlugin } from "../../structures/internal/ITtscLoadedNativePlugin";
10
+ import type { ITtscParsedProjectConfig } from "../../structures/internal/ITtscParsedProjectConfig";
10
11
  import type { TtscBuildOptions } from "../../structures/internal/TtscBuildOptions";
11
12
  import type { TtscBuildResult } from "../../structures/internal/TtscBuildResult";
12
13
  import type { TtscCommonOptions } from "../../structures/internal/TtscCommonOptions";
14
+ import { createNativeProjectContextArgs } from "./project/createNativeProjectContextArgs";
13
15
  import { readProjectConfig } from "./project/readProjectConfig";
14
16
  import { resolveBinary } from "./resolveBinary";
15
17
  import { resolveTsgo } from "./resolveTsgo";
16
18
  import {
17
19
  assertSharedHostCompatibility,
18
20
  linkedTransformPlugins,
21
+ resolvePluginConfigDir,
19
22
  selectSharedHostPlugin,
20
23
  } from "./sharedHostHelpers";
21
24
  import { outputText, spawnNative } from "./spawnNative";
@@ -23,6 +26,16 @@ import { outputText, spawnNative } from "./spawnNative";
23
26
  type RunBuildOptions = TtscBuildOptions & {
24
27
  skipDiagnosticsCheck?: boolean;
25
28
  forceListEmittedFiles?: boolean;
29
+ /**
30
+ * Emit an external source map from the direct tsgo build lane even when the
31
+ * project configures none. Set by the ttsx runtime builds so a served emit
32
+ * carries a map to inline under the source URL (issue #353). Applied only to
33
+ * the plain tsgo emit — never forwarded to a native plugin host, whose own
34
+ * emit honours the project's `sourceMap` setting.
35
+ */
36
+ forceRuntimeSourceMap?: boolean;
37
+ /** Retain an already selected project's lexical identity across API lanes. */
38
+ resolvedProject?: ITtscParsedProjectConfig;
26
39
  };
27
40
 
28
41
  type BuildTiming = {
@@ -119,9 +132,13 @@ function formatTimingSeconds(ms: number): string {
119
132
 
120
133
  /**
121
134
  * Build the environment for a native plugin spawn. Injects `TTSC_TSGO_BINARY`
122
- * and `TTSC_TTSX_BINARY` alongside the base env from `mergeEnv`. For
123
- * transform-stage plugins, also passes `TTSC_LINKED_PLUGINS_JSON` containing
124
- * any linked sources so they run inside the same process as the host plugin.
135
+ * and `TTSC_TTSX_BINARY` alongside the base env from `mergeEnv`, plus
136
+ * `TTSC_PLUGIN_CONFIG_DIR` when the caller declared a plugin config anchor (an
137
+ * embedder compiling through a generated wrapper tsconfig) so config-file
138
+ * discovery walks the real project instead of the wrapper's temp-dir ancestry.
139
+ * For transform-stage plugins, also passes `TTSC_LINKED_PLUGINS_JSON`
140
+ * containing any linked sources so they run inside the same process as the host
141
+ * plugin.
125
142
  */
126
143
  function nativePluginEnv(
127
144
  extra: NodeJS.ProcessEnv | undefined,
@@ -129,12 +146,25 @@ function nativePluginEnv(
129
146
  plugin?: ITtscLoadedNativePlugin,
130
147
  ): NodeJS.ProcessEnv {
131
148
  const env = mergeEnv({
149
+ ...(execution.pluginConfigDir === undefined
150
+ ? {}
151
+ : { TTSC_PLUGIN_CONFIG_DIR: execution.pluginConfigDir }),
132
152
  TTSC_TSGO_BINARY: process.env.TTSC_TSGO_BINARY ?? execution.tsgo.binary,
133
153
  TTSC_TTSX_BINARY:
134
154
  process.env.TTSC_TTSX_BINARY ??
135
155
  path.join(__dirname, "..", "..", "launcher", "ttsx.js"),
136
156
  ...extra,
137
157
  });
158
+ // The anchor is per-invocation state owned by this host: when this run
159
+ // declared none (and the caller's env does not name one), drop any value
160
+ // inherited from an ancestor ttsc process so a nested build never
161
+ // mis-anchors its plugins at the outer project.
162
+ if (
163
+ execution.pluginConfigDir === undefined &&
164
+ extra?.TTSC_PLUGIN_CONFIG_DIR === undefined
165
+ ) {
166
+ delete env.TTSC_PLUGIN_CONFIG_DIR;
167
+ }
138
168
  if (plugin?.stage === "transform") {
139
169
  const linked = linkedTransformPlugins(execution.nativePlugins);
140
170
  if (linked.length !== 0) {
@@ -664,6 +694,12 @@ function createTsgoBuildArgs(
664
694
  if (execution.rewriteRelativeImportExtensionsForEmit) {
665
695
  args.push("--rewriteRelativeImportExtensions");
666
696
  }
697
+ // The ttsx runtime build asks for an external map when the project emits
698
+ // none, so its served emit carries a map to inline under the source URL.
699
+ // Pushed before passthrough so an explicit user `--sourceMap` still wins.
700
+ if (options.forceRuntimeSourceMap === true) {
701
+ args.push("--sourceMap", "true");
702
+ }
667
703
  } else if (options.emit === false) {
668
704
  args.push("--noEmit");
669
705
  }
@@ -731,6 +767,16 @@ function createNativeBuildArgs(
731
767
  "--plugins-json=" + serializeNativePlugins(plugins),
732
768
  "--cwd=" + execution.projectRoot,
733
769
  ];
770
+ if (
771
+ selectSharedHostPlugin(plugins).capabilities?.projectContextArgs === true
772
+ ) {
773
+ args.push(
774
+ ...createNativeProjectContextArgs(
775
+ execution.project,
776
+ execution.pluginConfigDir,
777
+ ),
778
+ );
779
+ }
734
780
  if (options.emit === true) {
735
781
  args.push("--emit");
736
782
  }
@@ -763,6 +809,14 @@ function createNativeCheckArgs(
763
809
  "--plugins-json=" + serializeNativePlugins(execution.nativePlugins),
764
810
  "--cwd=" + execution.projectRoot,
765
811
  ];
812
+ if (plugin.capabilities?.projectContextArgs === true) {
813
+ args.push(
814
+ ...createNativeProjectContextArgs(
815
+ execution.project,
816
+ execution.pluginConfigDir,
817
+ ),
818
+ );
819
+ }
766
820
  if (options.outDir) {
767
821
  args.push("--outDir=" + path.resolve(execution.cwd, options.outDir));
768
822
  }
@@ -1059,14 +1113,20 @@ export function createProcessDiagnostic(
1059
1113
  * here so every code path in `runBuild` shares the same resolution logic.
1060
1114
  */
1061
1115
  function resolveExecutionContext(
1062
- options: TtscCommonOptions & { emit?: boolean; tsconfig?: string },
1116
+ options: TtscCommonOptions & {
1117
+ emit?: boolean;
1118
+ resolvedProject?: ITtscParsedProjectConfig;
1119
+ tsconfig?: string;
1120
+ },
1063
1121
  ) {
1064
1122
  const cwd = path.resolve(options.cwd ?? process.cwd());
1065
- const project = readProjectConfig({
1066
- cwd,
1067
- projectRoot: options.projectRoot,
1068
- tsconfig: options.tsconfig,
1069
- });
1123
+ const project =
1124
+ options.resolvedProject ??
1125
+ readProjectConfig({
1126
+ cwd,
1127
+ projectRoot: options.projectRoot,
1128
+ tsconfig: options.tsconfig,
1129
+ });
1070
1130
  const tsconfig = project.path;
1071
1131
  const projectRoot = project.root;
1072
1132
  const tsgo = resolveTsgo({ ...options, cwd: projectRoot });
@@ -1079,6 +1139,8 @@ function resolveExecutionContext(
1079
1139
  cacheDir: options.cacheDir ?? options.env?.TTSC_CACHE_DIR,
1080
1140
  cwd,
1081
1141
  entries: options.plugins,
1142
+ env: { ...process.env, ...options.env },
1143
+ pluginConfigDir: options.pluginConfigDir,
1082
1144
  projectRoot,
1083
1145
  tsconfig,
1084
1146
  }).nativePlugins;
@@ -1094,7 +1156,12 @@ function resolveExecutionContext(
1094
1156
  return {
1095
1157
  cwd,
1096
1158
  nativePlugins,
1159
+ pluginConfigDir: resolvePluginConfigDir({
1160
+ cwd,
1161
+ pluginConfigDir: options.pluginConfigDir,
1162
+ }),
1097
1163
  pluginSetupFailure,
1164
+ project,
1098
1165
  projectNoEmit: project.compilerOptions.noEmit === true,
1099
1166
  projectRoot,
1100
1167
  rewriteRelativeImportExtensionsForEmit:
@@ -3,7 +3,7 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
 
5
5
  import type { TtscSingleFileEmitOptions } from "../../structures/internal/TtscSingleFileEmitOptions";
6
- import { resolveProjectConfig } from "./project/resolveProjectConfig";
6
+ import { readProjectConfig } from "./project/readProjectConfig";
7
7
  import { resolveEmittedJavaScript } from "./resolveEmittedJavaScript";
8
8
  import { runBuild } from "./runBuild";
9
9
 
@@ -24,12 +24,14 @@ export function runSingleFileEmit(options: TtscSingleFileEmitOptions): string {
24
24
  ? options.file
25
25
  : path.resolve(cwd, options.file),
26
26
  );
27
- const tsconfig = resolveProjectConfig({
27
+ const project = readProjectConfig({
28
28
  cwd,
29
- file: sourceFile,
29
+ file: options.file,
30
+ projectRoot: options.projectRoot,
30
31
  tsconfig: options.tsconfig,
31
32
  });
32
- const projectRoot = path.dirname(tsconfig);
33
+ const tsconfig = project.path;
34
+ const projectRoot = project.root;
33
35
  const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "ttsc-single-file-"));
34
36
  try {
35
37
  const result = runBuild({
@@ -38,6 +40,7 @@ export function runSingleFileEmit(options: TtscSingleFileEmitOptions): string {
38
40
  emit: true,
39
41
  forceListEmittedFiles: true,
40
42
  outDir,
43
+ resolvedProject: project,
41
44
  tsconfig,
42
45
  });
43
46
  if (result.status !== 0) {
@@ -1,5 +1,29 @@
1
+ import path from "node:path";
2
+
1
3
  import type { ITtscLoadedNativePlugin } from "../../structures/internal/ITtscLoadedNativePlugin";
2
4
 
5
+ /**
6
+ * Resolve the caller-declared plugin config anchor to an absolute path, or
7
+ * `undefined` when the caller did not declare one.
8
+ *
9
+ * The anchor exists for embedders that compile through a generated tsconfig
10
+ * outside the project (the bundler adapters' alias overlay): they set
11
+ * `pluginConfigDir` to the real project directory and every native plugin spawn
12
+ * forwards it as `TTSC_PLUGIN_CONFIG_DIR`, so config-file discovery walks the
13
+ * project instead of the wrapper's temp-dir ancestry. Callers that point at a
14
+ * user-authored tsconfig (even a wrapper outside the project) leave it unset,
15
+ * keeping discovery anchored at the tsconfig's own directory.
16
+ */
17
+ export function resolvePluginConfigDir(options: {
18
+ cwd?: string;
19
+ pluginConfigDir?: string;
20
+ }): string | undefined {
21
+ if (options.pluginConfigDir === undefined || options.pluginConfigDir === "") {
22
+ return undefined;
23
+ }
24
+ return path.resolve(options.cwd ?? process.cwd(), options.pluginConfigDir);
25
+ }
26
+
3
27
  /**
4
28
  * Reports whether the given transform source is linked into another compiler
5
29
  * host instead of owning the process itself.
@@ -10,6 +10,7 @@ import { resolveTsgo } from "./resolveTsgo";
10
10
  import {
11
11
  assertSharedHostCompatibility,
12
12
  linkedTransformPlugins,
13
+ resolvePluginConfigDir,
13
14
  selectSharedHostPlugin,
14
15
  } from "./sharedHostHelpers";
15
16
 
@@ -51,6 +52,8 @@ export function startResidentTransform(
51
52
  cacheDir: context.cacheDir ?? context.env?.TTSC_CACHE_DIR,
52
53
  cwd,
53
54
  entries: context.plugins,
55
+ env: { ...process.env, ...context.env },
56
+ pluginConfigDir: context.pluginConfigDir,
54
57
  projectRoot: context.projectRoot,
55
58
  tsconfig: project.path,
56
59
  });
@@ -84,7 +87,10 @@ export function startResidentTransform(
84
87
  /**
85
88
  * Build the environment for the resident host spawn. Matches the per-call
86
89
  * transform spawn: injects the Node, tsgo, and ttsx binaries so the host never
87
- * searches PATH, and forwards linked transform plugins via
90
+ * searches PATH, sets `TTSC_PLUGIN_CONFIG_DIR` when the caller declared a
91
+ * plugin config anchor (an embedder compiling through a generated wrapper
92
+ * tsconfig) so config-file discovery walks the real project instead of the
93
+ * wrapper's temp-dir ancestry, and forwards linked transform plugins via
88
94
  * `TTSC_LINKED_PLUGINS_JSON`.
89
95
  */
90
96
  function residentEnv(
@@ -92,15 +98,29 @@ function residentEnv(
92
98
  tsgoBinary: string,
93
99
  nativePlugins: readonly ITtscLoadedNativePlugin[],
94
100
  ): NodeJS.ProcessEnv {
101
+ const pluginConfigDir = resolvePluginConfigDir(context);
95
102
  const env: NodeJS.ProcessEnv = {
96
103
  ...process.env,
97
104
  TTSC_NODE_BINARY: process.env.TTSC_NODE_BINARY ?? process.execPath,
105
+ ...(pluginConfigDir === undefined
106
+ ? {}
107
+ : { TTSC_PLUGIN_CONFIG_DIR: pluginConfigDir }),
98
108
  TTSC_TSGO_BINARY: process.env.TTSC_TSGO_BINARY ?? tsgoBinary,
99
109
  TTSC_TTSX_BINARY:
100
110
  process.env.TTSC_TTSX_BINARY ??
101
111
  path.join(__dirname, "..", "..", "launcher", "ttsx.js"),
102
112
  ...context.env,
103
113
  };
114
+ // The anchor is per-invocation state owned by this host: when this run
115
+ // declared none (and the caller's env does not name one), drop any value
116
+ // inherited from an ancestor ttsc process so a nested build never
117
+ // mis-anchors its plugins at the outer project.
118
+ if (
119
+ pluginConfigDir === undefined &&
120
+ context.env?.TTSC_PLUGIN_CONFIG_DIR === undefined
121
+ ) {
122
+ delete env.TTSC_PLUGIN_CONFIG_DIR;
123
+ }
104
124
  const linked = linkedTransformPlugins(nativePlugins);
105
125
  if (linked.length !== 0) {
106
126
  env.TTSC_LINKED_PLUGINS_JSON = serializeNativePlugins(linked);