ttsc 0.31.4 → 0.32.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 (140) hide show
  1. package/cmd/ttsc/compiler_argument_cwd_test.go +38 -38
  2. package/cmd/ttscserver/main.go +2 -12
  3. package/cmd/ttscserver/resolve_launch_uses_native_flag_authority_test.go +7 -7
  4. package/driver/lsp.go +4 -43
  5. package/driver/rewrite.go +1 -1
  6. package/internal/e2etrace/completion_hints.go +3 -3
  7. package/internal/e2etrace/completion_hints_test.go +26 -12
  8. package/internal/graph/build.go +5 -6
  9. package/internal/graph/graph.go +3 -3
  10. package/internal/graph/line_model.go +2 -35
  11. package/internal/lspserver/lsp_clean_didopen_refreshes_compiler_state_test.go +13 -36
  12. package/internal/lspserver/lsp_hints_refresh_ignores_a_superseded_generation_test.go +9 -3
  13. package/internal/lspserver/lsp_plugin.go +19 -0
  14. package/internal/lspserver/lsp_proxy.go +9 -129
  15. package/internal/lspserver/lsp_server.go +0 -8
  16. package/internal/lspserver/lsp_watched_file_changes_reach_compiler_caches_test.go +6 -36
  17. package/internal/sourceprocess/windows_target_owns_suspended_admission_test.go +5 -5
  18. package/lib/compiler/internal/build/BuildExecution.js +11 -3
  19. package/lib/compiler/internal/build/BuildExecution.js.map +1 -1
  20. package/lib/compiler/internal/build/TsgoArguments.js.map +1 -1
  21. package/lib/compiler/internal/project/CompilerProjectSelection.d.ts +9 -8
  22. package/lib/compiler/internal/project/CompilerProjectSelection.js +11 -9
  23. package/lib/compiler/internal/project/CompilerProjectSelection.js.map +1 -1
  24. package/lib/compiler/internal/project/resolveOwningProjectConfig.d.ts +20 -8
  25. package/lib/compiler/internal/project/resolveOwningProjectConfig.js +21 -12
  26. package/lib/compiler/internal/project/resolveOwningProjectConfig.js.map +1 -1
  27. package/lib/compiler/internal/runSingleFileEmit.d.ts +5 -4
  28. package/lib/compiler/internal/runSingleFileEmit.js +5 -4
  29. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  30. package/lib/flags/ParseOptions.d.ts +2 -2
  31. package/lib/internal/E2ETrace.d.ts +1 -0
  32. package/lib/internal/E2ETrace.js.map +1 -1
  33. package/lib/internal/OwnedNativeProcess.d.ts +2 -2
  34. package/lib/internal/OwnedNativeProcess.js +2 -2
  35. package/lib/internal/SourceNativeRetirement.d.ts +8 -7
  36. package/lib/internal/SourceNativeRetirement.js +19 -10
  37. package/lib/internal/SourceNativeRetirement.js.map +1 -1
  38. package/lib/internal/TtsxNodeOptions.d.ts +79 -0
  39. package/lib/internal/TtsxNodeOptions.js +121 -0
  40. package/lib/internal/TtsxNodeOptions.js.map +1 -0
  41. package/lib/internal/javascriptRuntimeCapabilities.js +4 -1
  42. package/lib/internal/javascriptRuntimeCapabilities.js.map +1 -1
  43. package/lib/internal/parseWindowsDirectoryCaseSensitivity.d.ts +5 -5
  44. package/lib/internal/parseWindowsDirectoryCaseSensitivity.js +5 -5
  45. package/lib/internal/pathIdentity/createCaseSensitivityObservations.d.ts +35 -0
  46. package/lib/internal/pathIdentity/createCaseSensitivityObservations.js +209 -0
  47. package/lib/internal/pathIdentity/createCaseSensitivityObservations.js.map +1 -0
  48. package/lib/internal/pathIdentity/createFilesystemPathIdentityContext.js +2 -143
  49. package/lib/internal/pathIdentity/createFilesystemPathIdentityContext.js.map +1 -1
  50. package/lib/internal/probeLocalProcess.d.ts +5 -1
  51. package/lib/internal/probeLocalProcess.js +9 -7
  52. package/lib/internal/probeLocalProcess.js.map +1 -1
  53. package/lib/internal/projectInputPathIdentity.d.ts +1 -0
  54. package/lib/internal/projectInputPathIdentity.js +1 -0
  55. package/lib/internal/projectInputPathIdentity.js.map +1 -1
  56. package/lib/launcher/internal/parseTtsxCLI.d.ts +5 -4
  57. package/lib/launcher/internal/parseTtsxCLI.js +5 -4
  58. package/lib/launcher/internal/parseTtsxCLI.js.map +1 -1
  59. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  60. package/lib/launcher/internal/resolveSingleFileOutput.d.ts +8 -9
  61. package/lib/launcher/internal/resolveSingleFileOutput.js +8 -9
  62. package/lib/launcher/internal/resolveSingleFileOutput.js.map +1 -1
  63. package/lib/launcher/internal/runTtsc.js +5 -5
  64. package/lib/launcher/internal/runTtsx.js +5 -2
  65. package/lib/launcher/internal/runTtsx.js.map +1 -1
  66. package/lib/launcher/internal/runtime/ProcessOwnedDirectory.d.ts +2 -0
  67. package/lib/launcher/internal/runtime/ProcessOwnedDirectory.js +2 -1
  68. package/lib/launcher/internal/runtime/ProcessOwnedDirectory.js.map +1 -1
  69. package/lib/launcher/internal/runtime/RuntimeEntrySources.d.ts +40 -0
  70. package/lib/launcher/internal/runtime/RuntimeEntrySources.js +57 -0
  71. package/lib/launcher/internal/runtime/RuntimeEntrySources.js.map +1 -0
  72. package/lib/launcher/internal/runtime/RuntimeProjectOwnership.d.ts +5 -4
  73. package/lib/launcher/internal/runtime/RuntimeProjectOwnership.js +8 -6
  74. package/lib/launcher/internal/runtime/RuntimeProjectOwnership.js.map +1 -1
  75. package/lib/launcher/internal/runtime/commonJsExportNames.d.ts +2 -1
  76. package/lib/launcher/internal/runtime/commonJsExportNames.js +2 -1
  77. package/lib/launcher/internal/runtime/commonJsExportNames.js.map +1 -1
  78. package/lib/launcher/internal/runtime/installRuntimeHooks.js +35 -9
  79. package/lib/launcher/internal/runtime/installRuntimeHooks.js.map +1 -1
  80. package/lib/launcher/internal/runtime/isLocalProcessGone.d.ts +1 -1
  81. package/lib/launcher/internal/runtime/isLocalProcessGone.js +4 -4
  82. package/lib/launcher/internal/runtime/isLocalProcessGone.js.map +1 -1
  83. package/lib/launcher/internal/runtime/resolveRuntimeCleanTargets.js +3 -1
  84. package/lib/launcher/internal/runtime/resolveRuntimeCleanTargets.js.map +1 -1
  85. package/lib/launcher/internal/watch/FseventsStreams.d.ts +2 -0
  86. package/lib/launcher/internal/watch/FseventsStreams.js +19 -0
  87. package/lib/launcher/internal/watch/FseventsStreams.js.map +1 -1
  88. package/lib/launcher/internal/watch/WatchTopology.d.ts +74 -12
  89. package/lib/launcher/internal/watch/WatchTopology.js +301 -92
  90. package/lib/launcher/internal/watch/WatchTopology.js.map +1 -1
  91. package/lib/launcher/internal/watch/projectInputReloadEventShouldNotify.d.ts +4 -1
  92. package/lib/launcher/internal/watch/projectInputReloadEventShouldNotify.js +3 -2
  93. package/lib/launcher/internal/watch/projectInputReloadEventShouldNotify.js.map +1 -1
  94. package/lib/plugin/internal/load/loadProjectPlugins.js +17 -9
  95. package/lib/plugin/internal/load/loadProjectPlugins.js.map +1 -1
  96. package/lib/plugin/internal/load/resolveNativeSource.js +2 -3
  97. package/lib/plugin/internal/load/resolveNativeSource.js.map +1 -1
  98. package/lib/plugin/internal/source/pluginModuleReplaceDirectories.d.ts +4 -1
  99. package/lib/plugin/internal/source/pluginModuleReplaceDirectories.js +9 -2
  100. package/lib/plugin/internal/source/pluginModuleReplaceDirectories.js.map +1 -1
  101. package/lib/structures/internal/TtscCommonOptions.d.ts +2 -2
  102. package/package.json +9 -9
  103. package/src/compiler/internal/build/BuildExecution.ts +15 -6
  104. package/src/compiler/internal/build/TsgoArguments.ts +4 -1
  105. package/src/compiler/internal/project/CompilerProjectSelection.ts +14 -14
  106. package/src/compiler/internal/project/resolveOwningProjectConfig.ts +29 -14
  107. package/src/compiler/internal/runSingleFileEmit.ts +11 -7
  108. package/src/flags/ParseOptions.ts +2 -2
  109. package/src/internal/E2ETrace.ts +5 -1
  110. package/src/internal/OwnedNativeProcess.ts +2 -2
  111. package/src/internal/SourceNativeRetirement.ts +42 -19
  112. package/src/internal/TtsxNodeOptions.ts +124 -0
  113. package/src/internal/javascriptRuntimeCapabilities.ts +6 -1
  114. package/src/internal/parseWindowsDirectoryCaseSensitivity.ts +5 -5
  115. package/src/internal/pathIdentity/createCaseSensitivityObservations.ts +242 -0
  116. package/src/internal/pathIdentity/createFilesystemPathIdentityContext.ts +2 -169
  117. package/src/internal/probeLocalProcess.ts +13 -8
  118. package/src/internal/projectInputPathIdentity.ts +1 -0
  119. package/src/launcher/internal/parseTtsxCLI.ts +5 -4
  120. package/src/launcher/internal/prepareExecution.ts +4 -1
  121. package/src/launcher/internal/resolveSingleFileOutput.ts +8 -9
  122. package/src/launcher/internal/runTtsc.ts +5 -5
  123. package/src/launcher/internal/runTtsx.ts +17 -8
  124. package/src/launcher/internal/runtime/ProcessOwnedDirectory.ts +5 -1
  125. package/src/launcher/internal/runtime/RuntimeEntrySources.ts +57 -0
  126. package/src/launcher/internal/runtime/RuntimeProjectOwnership.ts +23 -10
  127. package/src/launcher/internal/runtime/commonJsExportNames.ts +11 -4
  128. package/src/launcher/internal/runtime/installRuntimeHooks.ts +55 -13
  129. package/src/launcher/internal/runtime/isLocalProcessGone.ts +5 -3
  130. package/src/launcher/internal/runtime/resolveRuntimeCleanTargets.ts +17 -5
  131. package/src/launcher/internal/watch/FseventsStreams.ts +22 -0
  132. package/src/launcher/internal/watch/WatchTopology.ts +387 -99
  133. package/src/launcher/internal/watch/projectInputReloadEventShouldNotify.ts +14 -9
  134. package/src/plugin/internal/load/loadProjectPlugins.ts +17 -3
  135. package/src/plugin/internal/load/resolveNativeSource.ts +10 -4
  136. package/src/plugin/internal/source/pluginModuleReplaceDirectories.ts +10 -1
  137. package/src/structures/internal/TtscCommonOptions.ts +2 -2
  138. package/internal/graphsymbols/position_helpers_follow_ecma_line_terminators_test.go +0 -78
  139. package/internal/graphsymbols/provider.go +0 -650
  140. package/internal/lspserver/lsp_symbols.go +0 -212
@@ -1,13 +1,10 @@
1
- import childProcess from "node:child_process";
2
1
  import fs from "node:fs";
3
2
  import path from "node:path";
4
3
 
5
- import { E2ETrace } from "../E2ETrace";
6
- import { OwnedSynchronousProcess } from "../OwnedSynchronousProcess";
7
- import { parseWindowsDirectoryCaseSensitivity } from "../parseWindowsDirectoryCaseSensitivity";
8
4
  import type { FilesystemPathIdentity } from "./FilesystemPathIdentity";
9
5
  import type { FilesystemPathIdentityContext } from "./FilesystemPathIdentityContext";
10
6
  import type { FilesystemPathIdentityOperations } from "./FilesystemPathIdentityOperations";
7
+ import { createCaseSensitivityObservations } from "./createCaseSensitivityObservations";
11
8
  import { isFilesystemPathIdentityWithin } from "./isFilesystemPathIdentityWithin";
12
9
  import { resolveFilesystemPath } from "./resolveFilesystemPath";
13
10
 
@@ -47,19 +44,7 @@ export function createFilesystemPathIdentityContext(
47
44
  const readdir = operations.readdir ?? fs.readdirSync;
48
45
  const caseSensitive =
49
46
  operations.caseSensitive ??
50
- ((directory: string) => {
51
- try {
52
- return filesystemDirectoryIsCaseSensitive(directory, platform, {
53
- lstat,
54
- readdir,
55
- realpath,
56
- });
57
- } catch {
58
- // A failed capability observation is unknown. This does not suppress
59
- // strict physical identity errors in the separate realpath resolver.
60
- return undefined;
61
- }
62
- });
47
+ createCaseSensitivityObservations({ lstat, platform, readdir, realpath });
63
48
 
64
49
  const resolve = (location: string): FilesystemPathIdentity => {
65
50
  const normalized = resolveFilesystemPath(location, platform);
@@ -262,150 +247,6 @@ function physicalRealpath(location: string): string {
262
247
  return fs.realpathSync.native?.(location) ?? fs.realpathSync(location);
263
248
  }
264
249
 
265
- function filesystemDirectoryIsCaseSensitive(
266
- directory: string,
267
- platform: NodeJS.Platform,
268
- operations: {
269
- lstat(location: string): fs.Stats | fs.BigIntStats;
270
- readdir(directory: string): string[];
271
- realpath(location: string): string;
272
- },
273
- ): boolean | undefined {
274
- const pathApi = platform === "win32" ? path.win32 : path.posix;
275
- let entries: string[];
276
- try {
277
- entries = operations.readdir(directory);
278
- } catch {
279
- return undefined;
280
- }
281
- const foldedNames = new Map<string, string>();
282
- for (const name of entries) {
283
- const folded = foldAsciiCase(name);
284
- const previous = foldedNames.get(folded);
285
- if (previous !== undefined && previous !== name) return true;
286
- foldedNames.set(folded, name);
287
- }
288
- for (const name of entries) {
289
- const alternate = alternateCase(name);
290
- if (alternate === name) continue;
291
- try {
292
- operations.lstat(pathApi.join(directory, alternate));
293
- } catch (error) {
294
- if (isMissingFilesystemEntry(error)) {
295
- // Enumeration is not an atomic snapshot. An absent alternate proves
296
- // sensitivity only if the original spelling still names an entry.
297
- try {
298
- operations.lstat(pathApi.join(directory, name));
299
- return true;
300
- } catch (originalError) {
301
- if (isMissingFilesystemEntry(originalError) === false)
302
- throw originalError;
303
- }
304
- continue;
305
- }
306
- throw error;
307
- }
308
- try {
309
- const actual = resolveFilesystemPath(
310
- operations.realpath(pathApi.join(directory, name)),
311
- platform,
312
- );
313
- const alias = resolveFilesystemPath(
314
- operations.realpath(pathApi.join(directory, alternate)),
315
- platform,
316
- );
317
- return actual !== alias;
318
- } catch (error) {
319
- if (isMissingFilesystemEntry(error) === false) throw error;
320
- }
321
- }
322
- if (platform === "darwin") {
323
- // APFS/HFS case semantics are volume-wide. An empty directory has no child
324
- // name to probe, so ask the same read-only question of its existing name in
325
- // the parent and walk upward until a name with ASCII case is available.
326
- // This avoids a write probe while still distinguishing default APFS from a
327
- // case-sensitive volume.
328
- let current = resolveFilesystemPath(directory, platform);
329
- while (true) {
330
- const parent = pathApi.dirname(current);
331
- if (parent === current) break;
332
- // Names are interpreted by their parent; another mount is not evidence
333
- // of the directory's own volume case policy.
334
- if (operations.lstat(current).dev !== operations.lstat(parent).dev)
335
- return undefined;
336
- const name = pathApi.basename(current);
337
- const alternate = alternateCase(name);
338
- if (alternate !== name) {
339
- try {
340
- const actual = resolveFilesystemPath(
341
- operations.realpath(current),
342
- platform,
343
- );
344
- const alias = resolveFilesystemPath(
345
- operations.realpath(pathApi.join(parent, alternate)),
346
- platform,
347
- );
348
- return actual !== alias;
349
- } catch (error) {
350
- if (isMissingFilesystemEntry(error) === false) throw error;
351
- }
352
- }
353
- current = parent;
354
- }
355
- return undefined;
356
- }
357
- if (platform !== "win32") return undefined;
358
- // Node does not expose the Windows per-directory flag. Prefer fsutil's
359
- // read-only answer; unavailable capability remains unknown.
360
- const queried = queryWindowsDirectoryCaseSensitivity(directory);
361
- if (queried !== undefined) return queried;
362
- return undefined;
363
- }
364
-
365
- /**
366
- * Read the per-directory flag without depending on fsutil's display language.
367
- *
368
- * English output is cheap to recognize directly. Other Windows locales write
369
- * console-code-page bytes that Node cannot reliably decode, so their raw
370
- * message is interpreted against the volume-root query.
371
- */
372
- function queryWindowsDirectoryCaseSensitivity(
373
- directory: string,
374
- ): boolean | undefined {
375
- const result = queryWindowsDirectoryCaseSensitivityBytes(directory);
376
- if (result === undefined) return undefined;
377
- const volumeRoot = path.win32.parse(directory).root;
378
- const direct = parseWindowsDirectoryCaseSensitivity(
379
- result,
380
- undefined,
381
- volumeRoot,
382
- );
383
- if (direct !== undefined) return direct;
384
- const volume = queryWindowsDirectoryCaseSensitivityBytes(volumeRoot);
385
- return parseWindowsDirectoryCaseSensitivity(result, volume, volumeRoot);
386
- }
387
-
388
- function queryWindowsDirectoryCaseSensitivityBytes(
389
- directory: string,
390
- ): Buffer | undefined {
391
- const args = ["file", "queryCaseSensitiveInfo", directory];
392
- const result = E2ETrace.synchronous(
393
- "fsutil.exe",
394
- args,
395
- {},
396
- "path-case-probe",
397
- () =>
398
- OwnedSynchronousProcess.launch("fsutil.exe", args, {
399
- windowsHide: true,
400
- }) ?? childProcess.spawnSync("fsutil.exe", args, { windowsHide: true }),
401
- );
402
- return result.error === undefined &&
403
- result.status === 0 &&
404
- Buffer.isBuffer(result.stdout)
405
- ? result.stdout
406
- : undefined;
407
- }
408
-
409
250
  function filesystemPathIdentityKey(
410
251
  location: string,
411
252
  platform: NodeJS.Platform,
@@ -415,14 +256,6 @@ function filesystemPathIdentityKey(
415
256
  return `${root.toLowerCase()}${location.slice(root.length)}`;
416
257
  }
417
258
 
418
- function alternateCase(value: string): string {
419
- return value.replace(/[A-Za-z]/g, (character) =>
420
- character === character.toLowerCase()
421
- ? character.toUpperCase()
422
- : character.toLowerCase(),
423
- );
424
- }
425
-
426
259
  /** ASCII folding is the case equivalence observed by the read-only probes. */
427
260
  function foldAsciiCase(value: string): string {
428
261
  return value.replace(/[A-Z]/g, (character) => character.toLowerCase());
@@ -12,7 +12,9 @@ import fs from "node:fs";
12
12
  * indefinitely, so its `/proc` status reports it absent too. A leader whose own
13
13
  * thread exited also reads as a zombie while its other threads run, so the
14
14
  * zombie must be the process's last thread. Its id cannot be reused while the
15
- * zombie holds it, so this never names a live process absent.
15
+ * zombie holds it, so this never names a live process absent. An absent answer
16
+ * carries its evidence: ESRCH, the /proc read's code when collection raced it,
17
+ * or the zombie's state, so a diagnostic reader can check which one held.
16
18
  *
17
19
  * @evidence contracts/common.md#principled-implementation ESRCH, or a Linux /proc status of zombie or dead with no other thread, proves the process exited; success with any other status is presence, and every other failure stays unknown.
18
20
  * @evidence contracts/common.md#clear-and-simple-design One probe gives the ttsx run, dependency-lock and plugin-lock owners the same exit decision.
@@ -26,6 +28,9 @@ import fs from "node:fs";
26
28
  export function probeLocalProcess(pid: number): {
27
29
  result: "present" | "absent" | "unknown";
28
30
  errorCode?: string;
31
+
32
+ /** The Linux /proc state that proved an exited process absent. */
33
+ state?: string;
29
34
  } {
30
35
  try {
31
36
  process.kill(pid, 0);
@@ -40,17 +45,17 @@ export function probeLocalProcess(pid: number): {
40
45
  } catch (error) {
41
46
  const code = (error as NodeJS.ErrnoException | null)?.code;
42
47
  // The process was collected between the probe and the read.
43
- if (code === "ENOENT" || code === "ESRCH") return { result: "absent" };
48
+ if (code === "ENOENT" || code === "ESRCH")
49
+ return { result: "absent", errorCode: code };
44
50
  return { result: "unknown", errorCode: code };
45
51
  }
46
52
  // A leader whose own thread exited reads as a zombie while its other threads
47
53
  // still run; only a zombie that is the process's last thread has exited.
48
54
  const state = /^State:\s*(\S)/m.exec(status)?.[1];
49
55
  const threads = /^Threads:\s*(\d+)/m.exec(status)?.[1];
50
- return {
51
- result:
52
- (state === "Z" || state === "X") && threads === "1"
53
- ? "absent"
54
- : "present",
55
- };
56
+ // The state travels with an absent answer: signal zero succeeded, so it is the
57
+ // only evidence of the exit a diagnostic reader can check.
58
+ return (state === "Z" || state === "X") && threads === "1"
59
+ ? { result: "absent", state }
60
+ : { result: "present" };
56
61
  }
@@ -16,6 +16,7 @@
16
16
  export * from "./pathIdentity/FilesystemPathIdentity";
17
17
  export * from "./pathIdentity/FilesystemPathIdentityOperations";
18
18
  export * from "./pathIdentity/FilesystemPathIdentityContext";
19
+ export * from "./pathIdentity/createCaseSensitivityObservations";
19
20
  export * from "./pathIdentity/createFilesystemPathIdentityContext";
20
21
  export * from "./pathIdentity/isFilesystemPathIdentityWithin";
21
22
  export * from "./pathIdentity/resolveFilesystemPath";
@@ -14,10 +14,11 @@ import { assertTtsxNoWatch } from "./assertTtsxNoWatch";
14
14
  * Compiler flags before the first entry, repeated preloads and launcher-owned
15
15
  * settings are separated once through the shared flag schema. Terminal requests
16
16
  * are recognized only before the entry; unsupported watch/build requests fail
17
- * before a check, build or program is started. These decisions inspect the supplied
18
- * argv frame; this parser does not read response-file contents or certify their
19
- * eventual native effects. Preparation inspects response frames at the selected
20
- * project's actual compiler directory before requesting its build.
17
+ * before a check, build or program is started. These decisions inspect the
18
+ * supplied argv frame; this parser does not read response-file contents or
19
+ * certify their eventual native effects. Preparation inspects response frames
20
+ * at the selected project's actual compiler directory before requesting its
21
+ * build.
21
22
  *
22
23
  * @param argv Command tokens as received from the launcher.
23
24
  * @returns A terminal request or the launcher's typed option record.
@@ -325,7 +325,10 @@ function createProjectContext(
325
325
  options.passthrough,
326
326
  options.binary,
327
327
  );
328
- const privateEmitRootDir = resolveRuntimeSourceRoot(project, effectiveOptions);
328
+ const privateEmitRootDir = resolveRuntimeSourceRoot(
329
+ project,
330
+ effectiveOptions,
331
+ );
329
332
  // Resolved once: resolution costs a realpath (and, for a missing directory on
330
333
  // Windows, a case-sensitivity probe).
331
334
  const runtimeRootDir = createFilesystemPathIdentityContext({
@@ -14,15 +14,14 @@ import type { ITtscParsedProjectConfig } from "../../structures/internal/ITtscPa
14
14
  * then copies one transformed JavaScript file to this path, so project-mode
15
15
  * declaration, map, build-info, outFile, and broad outDir products are not
16
16
  * positional outputs. Explicit output mirrors cwd for contained sources and
17
- * uses the source basename for outside-layout or different-volume sources.
18
- * A supplied invocation project owns placement without
19
- * resolving the original locator again. Forwarded JSX uses the supplied native
20
- * argv frames and does not expand response-file requests here. CLI enum
21
- * normalization; an explicit reset selects the default suffix instead of
22
- * reviving the configured JSX. Placement-setting read failures use the
23
- * no-project fallback; that fallback does not certify a successful build or
24
- * valid configuration. Physical spelling is a current native observation, not a
25
- * pinned filesystem object.
17
+ * uses the source basename for outside-layout or different-volume sources. A
18
+ * supplied invocation project owns placement without resolving the original
19
+ * locator again. Forwarded JSX uses the supplied native argv frames and does
20
+ * not expand response-file requests here. CLI enum normalization; an explicit
21
+ * reset selects the default suffix instead of reviving the configured JSX.
22
+ * Placement-setting read failures use the no-project fallback; that fallback
23
+ * does not certify a successful build or valid configuration. Physical spelling
24
+ * is a current native observation, not a pinned filesystem object.
26
25
  *
27
26
  * @evidence contracts/common.md#principled-implementation CLI output wins over the selected project output; a contained cwd-relative source mirrors its layout while an outside-layout or cross-volume source uses its basename inside that explicit root. Supported extensions choose the emitted suffix, and physical root/file relation preserves configured project layout through links.
28
27
  * @evidence contracts/common.md#clear-and-simple-design Output placement delegates project settings and isolates containment, extension and forwarded-option readers; it does not materialize compiler side products.
@@ -578,11 +578,11 @@ function printCacheHelp(): void {
578
578
 
579
579
  /**
580
580
  * Adapt one normalized CLI request to its selected private producer and public
581
- * copy. The existing selection owns config and original argument anchoring;
582
- * the full request retains cache, reporting and watch callbacks. Only the
583
- * private helper writes the selected public file. Success presentation follows
584
- * that completed copy and describes public emission, without private products
585
- * or a fabricated transform-site count. Watch supplies quiet true itself.
581
+ * copy. The existing selection owns config and original argument anchoring; the
582
+ * full request retains cache, reporting and watch callbacks. Only the private
583
+ * helper writes the selected public file. Success presentation follows that
584
+ * completed copy and describes public emission, without private products or a
585
+ * fabricated transform-site count. Watch supplies quiet true itself.
586
586
  */
587
587
  function runSingleFile(
588
588
  options: ReturnType<typeof parseTtscBuildArgs> &
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
 
5
5
  import { resolveTsgo } from "../../compiler/internal/resolveTsgo";
6
6
  import { E2ETrace } from "../../internal/E2ETrace";
7
+ import { TtsxNodeOptions } from "../../internal/TtsxNodeOptions";
7
8
  import { TtsxEntryOptions } from "./TtsxEntryOptions";
8
9
  import { getCompilerVersionText } from "./getCompilerVersionText";
9
10
  import { parseTtsxCLI } from "./parseTtsxCLI";
@@ -243,9 +244,13 @@ async function runJavaScriptEntry(
243
244
  ];
244
245
  const env: NodeJS.ProcessEnv = {
245
246
  ...process.env,
246
- NODE_OPTIONS: appendNodeOption(
247
- process.env.NODE_OPTIONS,
248
- `--require ${JSON.stringify(path.join(__dirname, "..", "..", "register.js"))}`,
247
+ // ttsc's own helpers run with what the user passed (#1740).
248
+ ...TtsxNodeOptions.give(
249
+ process.env,
250
+ appendNodeOption(
251
+ process.env.NODE_OPTIONS,
252
+ `--require ${JSON.stringify(path.join(__dirname, "..", "..", "register.js"))}`,
253
+ ),
249
254
  ),
250
255
  ...(parsed.binary !== undefined
251
256
  ? { TTSC_TSGO_BINARY: path.resolve(cwd, parsed.binary) }
@@ -324,12 +329,16 @@ async function runPreparedEntry(
324
329
  ];
325
330
  const runtimeEnv: NodeJS.ProcessEnv = {
326
331
  ...process.env,
327
- NODE_OPTIONS: appendNodeOption(
328
- prependNodeOption(
329
- process.env.NODE_OPTIONS,
330
- `--require ${JSON.stringify(path.join(__dirname, "runtimeOwnerPreload.js"))}`,
332
+ // ttsc's own helpers run with what the user passed (#1740).
333
+ ...TtsxNodeOptions.give(
334
+ process.env,
335
+ appendNodeOption(
336
+ prependNodeOption(
337
+ process.env.NODE_OPTIONS,
338
+ `--require ${JSON.stringify(path.join(__dirname, "runtimeOwnerPreload.js"))}`,
339
+ ),
340
+ `--require ${JSON.stringify(path.join(__dirname, "runtimeHookPreload.js"))}`,
331
341
  ),
332
- `--require ${JSON.stringify(path.join(__dirname, "runtimeHookPreload.js"))}`,
333
342
  ),
334
343
  // The compiler this run resolved, which already honours an inherited
335
344
  // `TTSC_TSGO_BINARY` when no `--binary` was given.
@@ -53,6 +53,9 @@ export namespace ProcessOwnedDirectory {
53
53
 
54
54
  /** Native code already caught by the operation, if it supplied one. */
55
55
  errorCode?: string;
56
+
57
+ /** The Linux /proc state that proved an exited owner absent. */
58
+ state?: string;
56
59
  }
57
60
 
58
61
  /**
@@ -192,12 +195,13 @@ export namespace ProcessOwnedDirectory {
192
195
  owner,
193
196
  observe === undefined
194
197
  ? undefined
195
- : (result, errorCode) =>
198
+ : (result, errorCode, state) =>
196
199
  report(observe, {
197
200
  record: name,
198
201
  owner,
199
202
  result,
200
203
  ...(errorCode === undefined ? {} : { errorCode }),
204
+ ...(state === undefined ? {} : { state }),
201
205
  }),
202
206
  )
203
207
  )
@@ -0,0 +1,57 @@
1
+ import type { RuntimeManifest } from "./RuntimeManifest";
2
+
3
+ /**
4
+ * The served source of each entry module a checked entry build emitted,
5
+ * prepared once.
6
+ *
7
+ * A checked entry build's emit is immutable for the run: ttsx prepares it
8
+ * before the program starts, and a root `ttsc/register` prepares stays
9
+ * registered (`RuntimeManifestRegistry`). Node 22.15 nevertheless calls the
10
+ * `load` hook twice for the process entry, and each call read and source-mapped
11
+ * the same emitted file again (samchon/ttsc#1749). A repeated entry load of one
12
+ * build's emitted file is answered from its first preparation. A module served
13
+ * by any other lane, a project build or an isolated emit of a file the program
14
+ * may rewrite, is prepared on every load as before, and so is a non-entry
15
+ * module, whose text Node already holds.
16
+ *
17
+ * @evidence contracts/common.md#principled-implementation Only an entry module's emitted file of a registered checked build, the registry's immutable premise, is remembered, keyed by that build, so a reused answer is the answer a repeated read would give.
18
+ * @evidence contracts/common.md#clear-and-simple-design One map per build and one accessor; the caller supplies the preparation it already performs.
19
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts Nothing about Node's loader is patched; the hook still answers every call Node makes, and lanes whose source can change are never remembered.
20
+ * @evidence contracts/common.md#meaningful-documentation Native prose names the repeated call, the immutable entry emit and the lanes left unaffected.
21
+ * @evidence contracts/portability.md#os-neutral-implementation Files are keyed by the emitted path the build's ownership index returned, under the build object that owns it.
22
+ * @evidence contracts/performance.md#efficient-algorithms One WeakMap and one map lookup per serve.
23
+ * @evidence contracts/performance.md#reuse-equivalent-work A repeated entry load of an entry build's emitted file reuses the first preparation.
24
+ * @evidence contracts/performance.md#bound-retention-and-release-resources One served source per entry module, held while the registry keeps its build; a run has one entry and a handful of JavaScript-to-TypeScript boundaries.
25
+ */
26
+ export namespace RuntimeEntrySources {
27
+ /**
28
+ * The served source of entry module `emittedFile` from checked entry build
29
+ * `manifest`, prepared by `prepare` on its first serve only.
30
+ *
31
+ * @evidence contracts/common.md#principled-implementation Only an entry load of a registered checked build's emitted file asks; that emit is immutable for the run, so the first preparation is the answer a repeated one would give.
32
+ * @evidence contracts/common.md#clear-and-simple-design One lookup or one preparation and insertion.
33
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts The caller's own preparation runs unchanged on the first serve.
34
+ * @evidence contracts/common.md#meaningful-documentation Native prose states which serve prepares.
35
+ * @evidence contracts/portability.md#os-neutral-implementation Files are keyed by the emitted path the ownership index returned.
36
+ * @evidence contracts/performance.md#efficient-algorithms One WeakMap and one map lookup, and on the first serve one insertion.
37
+ * @evidence contracts/performance.md#reuse-equivalent-work A repeated serve returns the first preparation.
38
+ * @evidence contracts/performance.md#bound-retention-and-release-resources One source per entry module is retained while its build is.
39
+ */
40
+ export function serve<Source>(
41
+ manifest: RuntimeManifest,
42
+ emittedFile: string,
43
+ prepare: () => Source,
44
+ ): Source {
45
+ let files = served.get(manifest);
46
+ if (files === undefined) {
47
+ files = new Map();
48
+ served.set(manifest, files);
49
+ }
50
+ if (files.has(emittedFile)) return files.get(emittedFile) as Source;
51
+ const source = prepare();
52
+ files.set(emittedFile, source);
53
+ return source;
54
+ }
55
+ }
56
+
57
+ const served = new WeakMap<RuntimeManifest, Map<string, unknown>>();
@@ -24,8 +24,8 @@ export namespace RuntimeProjectOwnership {
24
24
  const cache = new Map<string, Lookup>();
25
25
 
26
26
  /**
27
- * Whether a physical source path passes through an installed directory.
28
- * A linked workspace physically outside that directory remains user source.
27
+ * Whether a physical source path passes through an installed directory. A
28
+ * linked workspace physically outside that directory remains user source.
29
29
  * Alternate spellings count only when the native lowercase installed spelling
30
30
  * resolves to the same physical directory; failed identity reads prove none.
31
31
  *
@@ -39,7 +39,7 @@ export namespace RuntimeProjectOwnership {
39
39
  * @evidence contracts/performance.md#bound-retention-and-release-resources The synchronous walk retains no handles or historical identities.
40
40
  */
41
41
  export function isInstalledPackageSource(real: string): boolean {
42
- for (let directory = path.dirname(real);;) {
42
+ for (let directory = path.dirname(real); ; ) {
43
43
  if (isInstalledDirectory(directory)) return true;
44
44
  const parent = path.dirname(directory);
45
45
  if (parent === directory) return false;
@@ -54,8 +54,9 @@ export namespace RuntimeProjectOwnership {
54
54
  * Cached walks recheck both candidate files and the native boundary topology.
55
55
  * Creating/deleting a config or adding/removing an installed alias therefore
56
56
  * cannot retain an earlier consumer or isolated selection. The observer
57
- * brackets config candidates and alternate directory identities for descriptor
58
- * dependency recording; it does not pin the filesystem during compilation.
57
+ * brackets config candidates and alternate directory identities for
58
+ * descriptor dependency recording; it does not pin the filesystem during
59
+ * compilation.
59
60
  *
60
61
  * @evidence contracts/common.md#principled-implementation Upward traversal selects the first existing config before the installed boundary. Cache entries retain ordered candidates and their stopping boundary; current candidate and native topology checks establish whether the old selection still applies.
61
62
  * @evidence contracts/common.md#clear-and-simple-design The same private predicate serves fresh lookup, cached topology validation and installed-source classification. One directory-keyed cache shares the walked suffix without a second casing policy.
@@ -79,18 +80,28 @@ export namespace RuntimeProjectOwnership {
79
80
  if (cached !== undefined) {
80
81
  observe(cached.candidates);
81
82
  const topologyCurrent =
82
- (cached.boundary === undefined || isInstalledDirectory(cached.boundary, observe)) &&
83
- cached.candidates.every((candidate) => !isInstalledDirectory(path.dirname(candidate), observe));
83
+ (cached.boundary === undefined ||
84
+ isInstalledDirectory(cached.boundary, observe)) &&
85
+ cached.candidates.every(
86
+ (candidate) =>
87
+ !isInstalledDirectory(path.dirname(candidate), observe),
88
+ );
84
89
  const current = nearestExisting(cached.candidates);
85
90
  observe(cached.candidates);
86
91
  if (topologyCurrent && current === cached.result)
87
- return remember(chain, cached.result, cached.candidates, cached.boundary);
92
+ return remember(
93
+ chain,
94
+ cached.result,
95
+ cached.candidates,
96
+ cached.boundary,
97
+ );
88
98
  cache.delete(directory);
89
99
  }
90
100
  chain.push(directory);
91
101
  const candidate = path.join(directory, "tsconfig.json");
92
102
  observe([candidate]);
93
- if (RuntimeFilesystem.isFile(candidate)) return remember(chain, candidate);
103
+ if (RuntimeFilesystem.isFile(candidate))
104
+ return remember(chain, candidate);
94
105
  const parent = path.dirname(directory);
95
106
  if (parent === directory) return remember(chain, null);
96
107
  directory = parent;
@@ -108,7 +119,9 @@ export namespace RuntimeProjectOwnership {
108
119
  const inputs = [directory, lowercase];
109
120
  observe(inputs);
110
121
  try {
111
- return fs.realpathSync.native(directory) === fs.realpathSync.native(lowercase);
122
+ return (
123
+ fs.realpathSync.native(directory) === fs.realpathSync.native(lowercase)
124
+ );
112
125
  } catch {
113
126
  return false;
114
127
  } finally {
@@ -13,7 +13,8 @@ import { realPath } from "./realPath";
13
13
  * become resolution origins. Targets are not evaluated. Failed resolution adds
14
14
  * no names and leaves the eventual native load to report its error, whereas a
15
15
  * reader's ownership error propagates. Root default metadata is retained for
16
- * the facade's native own-property ordering; nested default is not re-exported.
16
+ * the facade's native own-property ordering; nested default is not
17
+ * re-exported.
17
18
  *
18
19
  * @evidence contracts/common.md#principled-implementation Node createRequire resolves each edge from the physical source module using require conditions; the runtime supplies its existing source-rescue resolver on supported hook variants. Owned compiler bytes retain TypeScript elision and lowering. Static parsing discovers possible names, while the original loaded helper and facade own values and property semantics.
19
20
  * @evidence contracts/common.md#clear-and-simple-design One traversal serves both JavaScript and TypeScript facades. A call-scoped physical seen set prevents mixed-source cycles and duplicate reads without separate emitted/source resolution policies.
@@ -28,8 +29,10 @@ export function commonJsExportNames(
28
29
  source: string,
29
30
  filename: string,
30
31
  readSource: (file: string) => string | null,
31
- resolve: (parent: string, specifier: string) => string = (parent, specifier) =>
32
- createRequire(parent).resolve(specifier),
32
+ resolve: (parent: string, specifier: string) => string = (
33
+ parent,
34
+ specifier,
35
+ ) => createRequire(parent).resolve(specifier),
33
36
  ): string[] {
34
37
  const names = new Set<string>();
35
38
  const seen = new Set<string>();
@@ -42,7 +45,11 @@ export function commonJsExportNames(
42
45
  if (root || name !== "default") names.add(name);
43
46
  for (const specifier of parsed.reexports) {
44
47
  let target: string;
45
- try { target = resolve(real, specifier); } catch { continue; }
48
+ try {
49
+ target = resolve(real, specifier);
50
+ } catch {
51
+ continue;
52
+ }
46
53
  if (!path.isAbsolute(target)) continue;
47
54
  if ([".json", ".node", ".mjs"].includes(path.extname(target))) continue;
48
55
  const physical = realPath(target);