ttsc 0.19.2 → 0.20.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 (178) hide show
  1. package/README.md +7 -5
  2. package/cmd/graphdump/main.go +2 -4
  3. package/cmd/ttsc/filter.go +12 -4
  4. package/cmd/ttsc/filter_resolves_flag_names_case_insensitively_test.go +53 -0
  5. package/cmd/ttsc/flags_gen.go +3 -3
  6. package/cmd/ttscgraph/dump.go +5 -7
  7. package/cmd/ttscgraph/run_dump_emits_only_checker_verified_member_relations_test.go +118 -0
  8. package/cmd/ttscgraph/serve.go +58 -308
  9. package/cmd/ttscgraph/serve_session_keeps_base_url_fallback_unchanged_test.go +49 -0
  10. package/cmd/ttscgraph/serve_session_keeps_esm_directory_candidate_unchanged_test.go +61 -0
  11. package/cmd/ttscgraph/serve_session_keeps_exports_blocked_file_candidates_unchanged_test.go +64 -0
  12. package/cmd/ttscgraph/serve_session_keeps_inactive_conditional_export_unchanged_test.go +66 -0
  13. package/cmd/ttscgraph/serve_session_keeps_lower_priority_module_candidate_unchanged_test.go +67 -0
  14. package/cmd/ttscgraph/serve_session_keeps_lower_priority_paths_pattern_unchanged_test.go +58 -0
  15. package/cmd/ttscgraph/serve_session_keeps_nonlongest_root_dirs_candidate_unchanged_test.go +54 -0
  16. package/cmd/ttscgraph/serve_session_reloads_commonjs_package_entry_for_esm_test.go +50 -0
  17. package/cmd/ttscgraph/serve_session_reloads_module_suffix_candidate_test.go +41 -0
  18. package/cmd/ttscgraph/serve_session_reloads_superseding_module_candidates_test.go +216 -0
  19. package/cmd/ttscgraph/serve_session_retries_failed_dump_test.go +103 -0
  20. package/driver/graph.go +12 -6
  21. package/driver/lsp.go +30 -0
  22. package/driver/program.go +37 -1
  23. package/driver/resolution_candidates.go +819 -0
  24. package/driver/windowsjunction/windowsjunction.go +40 -0
  25. package/go.mod +2 -0
  26. package/internal/graph/build.go +45 -11
  27. package/internal/graph/dump.go +312 -108
  28. package/internal/graph/dump_checkout_paths_are_stable_test.go +210 -0
  29. package/internal/graph/dump_diagnostics_ride_the_generation_that_built_it_test.go +9 -3
  30. package/internal/graph/dump_paths.go +172 -0
  31. package/internal/graph/dump_paths_are_portable_and_injective_test.go +93 -0
  32. package/internal/graph/dump_schema_version_matches_the_typescript_client_test.go +1 -1
  33. package/internal/graph/dump_uses_ecma_line_terminators_test.go +87 -0
  34. package/internal/graph/duplicate_variable_object_members_keep_first_declaration_test.go +61 -0
  35. package/internal/graph/edges.go +220 -5
  36. package/internal/graph/enum_members_carry_their_names_on_the_enum_node_test.go +131 -0
  37. package/internal/graph/graph.go +177 -5
  38. package/internal/graph/line_model.go +122 -0
  39. package/internal/graph/literals.go +175 -0
  40. package/internal/graph/literals_are_absent_when_the_type_admits_more_than_it_can_name_test.go +74 -0
  41. package/internal/graph/literals_cover_enum_members_the_graph_has_no_nodes_for_test.go +79 -0
  42. package/internal/graph/literals_do_not_depend_on_declaration_line_wrapping_test.go +84 -0
  43. package/internal/graph/literals_render_every_value_in_typescript_source_form_test.go +77 -0
  44. package/internal/graph/literals_resolve_through_alias_indirection_test.go +64 -0
  45. package/internal/graph/member_relation_edges_follow_checker_validity_test.go +85 -0
  46. package/internal/graph/member_relation_edges_preserve_checker_boundaries_test.go +191 -0
  47. package/internal/graph/member_relation_edges_reject_checker_invalid_pairs_test.go +119 -0
  48. package/internal/graph/node_ids_preserve_hash_bearing_components_test.go +41 -0
  49. package/internal/graph/node_modifiers_emit_union_strings_test.go +4 -1
  50. package/internal/graph/object_literal_members_ride_on_the_snapshot_test.go +192 -0
  51. package/internal/graph/object_member_signatures_preserve_literal_whitespace_test.go +67 -0
  52. package/internal/graph/object_members.go +229 -0
  53. package/internal/graph/provenance.go +88 -30
  54. package/internal/graph/source_texts_cover_every_resident_program_source_test.go +73 -0
  55. package/internal/graphsymbols/position_helpers_follow_ecma_line_terminators_test.go +66 -0
  56. package/internal/graphsymbols/provider.go +26 -91
  57. package/internal/lspserver/lsp_clean_didopen_refreshes_compiler_state_test.go +144 -0
  58. package/internal/lspserver/lsp_completion.go +109 -0
  59. package/internal/lspserver/lsp_completion_gate_never_refuses_a_matching_line_test.go +50 -0
  60. package/internal/lspserver/lsp_completion_hints_longest_trigger_wins_test.go +149 -0
  61. package/internal/lspserver/lsp_completion_hints_nearest_trigger_wins_test.go +37 -0
  62. package/internal/lspserver/lsp_completion_merges_into_upstream_response_test.go +149 -0
  63. package/internal/lspserver/lsp_completion_nearest_hint_replaces_nearest_filter_test.go +43 -0
  64. package/internal/lspserver/lsp_completion_preserves_completion_list_extensions_test.go +99 -0
  65. package/internal/lspserver/lsp_completion_refuses_hints_inside_a_string_literal_test.go +69 -0
  66. package/internal/lspserver/lsp_completion_replaces_full_filter_in_rank_order_test.go +85 -0
  67. package/internal/lspserver/lsp_completion_request.go +455 -0
  68. package/internal/lspserver/lsp_completion_scope.go +530 -0
  69. package/internal/lspserver/lsp_completion_scope_bench_test.go +41 -0
  70. package/internal/lspserver/lsp_completion_scope_classifies_regex_statement_and_operator_boundaries_test.go +39 -0
  71. package/internal/lspserver/lsp_completion_scope_ignores_jsdoc_bytes_in_literals_test.go +108 -0
  72. package/internal/lspserver/lsp_completion_scope_refuses_regex_hints_test.go +52 -0
  73. package/internal/lspserver/lsp_completion_trigger_uses_last_rune_test.go +24 -0
  74. package/internal/lspserver/lsp_deleted_watched_file_withdraws_plugin_diagnostics_test.go +110 -0
  75. package/internal/lspserver/lsp_didsave_keeps_the_incremental_changed_uri_path_test.go +78 -0
  76. package/internal/lspserver/lsp_hints_absence_is_not_an_error_test.go +46 -0
  77. package/internal/lspserver/lsp_hints_refresh_coalesces_concurrent_requests_test.go +82 -0
  78. package/internal/lspserver/lsp_hints_refresh_ignores_a_superseded_generation_test.go +40 -0
  79. package/internal/lspserver/lsp_hints_refresh_keeps_each_producer_isolated_test.go +64 -0
  80. package/internal/lspserver/lsp_hints_refresh_replaces_the_session_snapshot_test.go +66 -0
  81. package/internal/lspserver/lsp_hints_refresh_scheduled_by_editor_events_test.go +90 -0
  82. package/internal/lspserver/lsp_hints_refresh_serves_completion_while_running_test.go +91 -0
  83. package/internal/lspserver/lsp_index_byte_from_bounds_its_search_start_test.go +38 -0
  84. package/internal/lspserver/lsp_initialize_constrains_position_encoding_to_utf16_test.go +205 -0
  85. package/internal/lspserver/lsp_late_completion_trigger_is_reported_once_test.go +105 -0
  86. package/internal/lspserver/lsp_native_completion_hints_decode_flat_lint_wire_test.go +66 -0
  87. package/internal/lspserver/lsp_native_completion_hints_grouped_compatibility_boundary_test.go +60 -0
  88. package/internal/lspserver/lsp_native_plugin_source.go +315 -2
  89. package/internal/lspserver/lsp_native_resident.go +304 -0
  90. package/internal/lspserver/lsp_plugin.go +85 -8
  91. package/internal/lspserver/lsp_plugin_completion_resolve_stays_local_test.go +82 -0
  92. package/internal/lspserver/lsp_position_conversions_span_astral_and_combining_runes_test.go +119 -0
  93. package/internal/lspserver/lsp_proxy.go +601 -9
  94. package/internal/lspserver/lsp_refresh.go +70 -0
  95. package/internal/lspserver/lsp_resident_falls_back_when_serve_unsupported_test.go +43 -0
  96. package/internal/lspserver/lsp_resident_maps_verb_args_test.go +34 -0
  97. package/internal/lspserver/lsp_server.go +13 -1
  98. package/internal/lspserver/lsp_watched_file_changes_reach_compiler_caches_test.go +172 -0
  99. package/lib/TtscCompiler.d.ts +3 -1
  100. package/lib/TtscCompiler.js +5 -1
  101. package/lib/TtscCompiler.js.map +1 -1
  102. package/lib/TtscService.d.ts +13 -3
  103. package/lib/TtscService.js +6 -6
  104. package/lib/TtscService.js.map +1 -1
  105. package/lib/compiler/internal/project/readConfigJson.d.ts +30 -0
  106. package/lib/compiler/internal/project/readConfigJson.js +208 -0
  107. package/lib/compiler/internal/project/readConfigJson.js.map +1 -0
  108. package/lib/compiler/internal/project/readProjectConfig.js +53 -149
  109. package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
  110. package/lib/compiler/internal/residentTransformProcess.d.ts +15 -2
  111. package/lib/compiler/internal/residentTransformProcess.js +159 -39
  112. package/lib/compiler/internal/residentTransformProcess.js.map +1 -1
  113. package/lib/compiler/internal/runBuild.d.ts +6 -0
  114. package/lib/compiler/internal/runBuild.js +143 -44
  115. package/lib/compiler/internal/runBuild.js.map +1 -1
  116. package/lib/compiler/internal/startResidentTransform.d.ts +5 -1
  117. package/lib/compiler/internal/startResidentTransform.js +3 -1
  118. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  119. package/lib/compiler/internal/transformProjectInMemory.js +12 -2
  120. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  121. package/lib/flags/parser.d.ts +19 -0
  122. package/lib/flags/parser.js +78 -36
  123. package/lib/flags/parser.js.map +1 -1
  124. package/lib/flags/schema.d.ts +49 -11
  125. package/lib/flags/schema.js +94 -22
  126. package/lib/flags/schema.js.map +1 -1
  127. package/lib/internal/assertSafeExplicitCacheDirectory.d.ts +5 -0
  128. package/lib/internal/assertSafeExplicitCacheDirectory.js +26 -0
  129. package/lib/internal/assertSafeExplicitCacheDirectory.js.map +1 -0
  130. package/lib/launcher/internal/runTtsc.js +107 -69
  131. package/lib/launcher/internal/runTtsc.js.map +1 -1
  132. package/lib/launcher/internal/runTtsx.js +28 -41
  133. package/lib/launcher/internal/runTtsx.js.map +1 -1
  134. package/lib/launcher/internal/runtimeHooks.d.ts +15 -0
  135. package/lib/launcher/internal/runtimeHooks.js +18 -1
  136. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  137. package/lib/launcher/internal/watchTopology.d.ts +43 -0
  138. package/lib/launcher/internal/watchTopology.js +360 -0
  139. package/lib/launcher/internal/watchTopology.js.map +1 -0
  140. package/lib/plugin/internal/buildSourcePlugin.d.ts +14 -3
  141. package/lib/plugin/internal/buildSourcePlugin.js +247 -61
  142. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  143. package/lib/plugin/internal/loadProjectPlugins.js +63 -2
  144. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  145. package/lib/structures/ITtscCompilerTransformation.d.ts +16 -0
  146. package/lib/structures/internal/ITtscParsedProjectConfig.d.ts +2 -0
  147. package/package.json +8 -8
  148. package/shim/ast/shim.go +48 -3
  149. package/shim/astnav/go.mod +14 -0
  150. package/shim/astnav/go.sum +20 -0
  151. package/shim/astnav/shim.go +15 -0
  152. package/shim/checker/shim.go +113 -0
  153. package/shim/core/shim.go +47 -0
  154. package/shim/diagnosticwriter/lint.go +50 -0
  155. package/src/TtscCompiler.ts +5 -1
  156. package/src/TtscService.ts +37 -10
  157. package/src/compiler/internal/project/readConfigJson.ts +209 -0
  158. package/src/compiler/internal/project/readProjectConfig.ts +62 -158
  159. package/src/compiler/internal/residentTransformProcess.ts +197 -39
  160. package/src/compiler/internal/runBuild.ts +169 -47
  161. package/src/compiler/internal/startResidentTransform.ts +12 -1
  162. package/src/compiler/internal/transformProjectInMemory.ts +12 -1
  163. package/src/flags/parser.ts +113 -42
  164. package/src/flags/schema.ts +113 -31
  165. package/src/internal/assertSafeExplicitCacheDirectory.ts +29 -0
  166. package/src/launcher/internal/runTtsc.ts +117 -68
  167. package/src/launcher/internal/runTtsx.ts +29 -44
  168. package/src/launcher/internal/runtimeHooks.ts +30 -2
  169. package/src/launcher/internal/watchTopology.ts +466 -0
  170. package/src/plugin/internal/buildSourcePlugin.ts +326 -64
  171. package/src/plugin/internal/loadProjectPlugins.ts +80 -2
  172. package/src/structures/ITtscCompilerTransformation.ts +17 -0
  173. package/src/structures/internal/ITtscParsedProjectConfig.ts +2 -0
  174. package/tools/shim_audit/baseline.json +0 -3
  175. package/tools/shim_audit/main.go +42 -14
  176. package/tools/shim_audit/shim_dir_coverage_test.go +25 -0
  177. package/utility/flags_gen.go +3 -3
  178. package/utility/host.go +9 -3
@@ -8,6 +8,7 @@ import fs from "node:fs";
8
8
  import { createRequire } from "node:module";
9
9
  import os from "node:os";
10
10
  import path from "node:path";
11
+ import { fileURLToPath } from "node:url";
11
12
 
12
13
  import { findNearestGoMod } from "../../compiler/internal/paths";
13
14
 
@@ -193,7 +194,7 @@ export function buildSourcePlugin(opts: {
193
194
  const { dir, entry, source } = resolveSourceBuildTarget(opts);
194
195
  const overlayDirs = [...(opts.overlayDirs ?? findTtscOverlayDirs())].sort();
195
196
  const contributors = opts.contributors ?? [];
196
- const goBinary = resolveGoCompiler(env);
197
+ const goBinary = resolveGoToolForBuild(resolveGoCompiler(env), env, dir);
197
198
  ensureExecutableGoToolchain(goBinary);
198
199
  const key = computeCacheKey({
199
200
  contributors,
@@ -267,7 +268,8 @@ function compileSourcePlugin(opts: {
267
268
  .join(", ")}`;
268
269
  process.stderr.write(
269
270
  `ttsc: building ${opts.label} "${opts.pluginName}" from ${opts.source}${extra} ` +
270
- `(this runs once per cache key and can take several minutes on a cold Go cache)\n`,
271
+ `(this runs once per cache key and can take several minutes on a cold Go cache) ` +
272
+ `See https://ttsc.dev/docs/ttsc/compile#plugin-cache to persist it across builds.\n`,
271
273
  );
272
274
  }
273
275
 
@@ -1462,6 +1464,13 @@ function sourceBuildWorkspaceReplacements(
1462
1464
  * `/` (the workspace convention) and then delegate to
1463
1465
  * {@link autoQuoteGoModToken}, which mirrors `modfile.AutoQuote`.
1464
1466
  *
1467
+ * Separator normalization is itself a quoting trigger. A Windows UNC
1468
+ * (`\\server\share\...`) or extended-length (`\\?\C:\...`) path normalizes into
1469
+ * a token that starts with `//`, and the modfile lexer reads `//` as a line
1470
+ * comment wherever it appears. Emitted bare, such a token turns its whole
1471
+ * `use`/`replace` line into a comment: `go` exits 0, reports nothing, and the
1472
+ * overlay module simply disappears from the workspace.
1473
+ *
1465
1474
  * Exported for unit tests.
1466
1475
  */
1467
1476
  export function formatGoWorkPath(p: string): string {
@@ -1472,9 +1481,9 @@ export function formatGoWorkPath(p: string): string {
1472
1481
  * Quote `token` for a `go.mod`/`go.work` line exactly as
1473
1482
  * `golang.org/x/mod/modfile`'s `AutoQuote` does: return it unchanged when it is
1474
1483
  * already a clean bare token, otherwise return its Go double-quoted form so the
1475
- * value round-trips through the modfile lexer. A space-free path is therefore
1476
- * emitted byte-for-byte as before; only tokens that would otherwise
1477
- * mis-tokenize are quoted.
1484
+ * value round-trips through the modfile lexer. A clean bare token is therefore
1485
+ * emitted byte-for-byte as before; only tokens that would otherwise be split or
1486
+ * interpreted as comments are quoted.
1478
1487
  *
1479
1488
  * Exported for unit tests.
1480
1489
  */
@@ -1505,11 +1514,11 @@ function mustQuoteGoModToken(s: string): boolean {
1505
1514
  }
1506
1515
  continue;
1507
1516
  }
1508
- if (!isGoGraphic(ch)) {
1517
+ if (!isGoPrintable(ch)) {
1509
1518
  return true;
1510
1519
  }
1511
1520
  }
1512
- return s === "" || s === "//" || s === "/*";
1521
+ return s === "" || s.includes("//") || s.includes("/*");
1513
1522
  }
1514
1523
 
1515
1524
  // Mirror `strconv.Quote`: wrap in double quotes, backslash-escape `"` and `\`,
@@ -1562,13 +1571,6 @@ function escapeGoRune(ch: string): string {
1562
1571
  }
1563
1572
  }
1564
1573
 
1565
- // `unicode.IsGraphic`: categories L, M, N, P, S, Zs (all spacing).
1566
- const GO_GRAPHIC_RE = /^[\p{L}\p{M}\p{N}\p{P}\p{S}\p{Zs}]$/u;
1567
-
1568
- function isGoGraphic(ch: string): boolean {
1569
- return GO_GRAPHIC_RE.test(ch);
1570
- }
1571
-
1572
1574
  // `strconv.IsPrint`: the graphic categories except that the ONLY spacing
1573
1575
  // character is the ASCII space (U+0020); other Unicode spaces are escaped.
1574
1576
  const GO_PRINTABLE_RE = /^[\p{L}\p{M}\p{N}\p{P}\p{S}]$/u;
@@ -1761,24 +1763,85 @@ function goToolchainNotFoundMessage(pluginName: string): string {
1761
1763
  );
1762
1764
  }
1763
1765
 
1764
- function spawnGoTool(
1766
+ export function spawnGoTool(
1765
1767
  goBinary: string,
1766
1768
  args: readonly string[],
1767
1769
  options: SpawnSyncOptionsWithStringEncoding,
1768
1770
  ): SpawnSyncReturns<string> {
1769
- const shell = shouldSpawnGoToolThroughShell(goBinary);
1770
- // Under shell:true (a Windows .cmd/.bat wrapper) Node does not quote the
1771
- // command, so a wrapper at a path containing a space would be split by cmd.
1772
- // Quote it here; the go arguments ttsc passes never contain spaces, and
1773
- // quoting a space-free path is harmless.
1774
- return spawnSync(shell ? `"${goBinary}"` : goBinary, [...args], {
1775
- ...options,
1776
- shell,
1777
- });
1771
+ if (process.platform !== "win32") {
1772
+ return spawnSync(goBinary, [...args], options);
1773
+ }
1774
+ const inheritedEnv = options.env ?? process.env;
1775
+ const resolved = resolveWindowsGoTool(
1776
+ goBinary,
1777
+ inheritedEnv,
1778
+ spawnWorkingDirectory(options.cwd),
1779
+ );
1780
+ if (!resolved.wrapper) {
1781
+ return spawnSync(goBinary, [...args], options);
1782
+ }
1783
+ // Preserve the native spawn ENOENT contract before cmd.exe becomes the
1784
+ // actual child process. The callers use that code for the install guidance.
1785
+ if (resolved.location === null) {
1786
+ return spawnSync(goBinary, [...args], options);
1787
+ }
1788
+ const shim = createWindowsGoCommandShim([resolved.location, ...args]);
1789
+ return spawnSync(
1790
+ readWindowsEnvironmentValue(inheritedEnv, "COMSPEC") ??
1791
+ readWindowsEnvironmentValue(process.env, "COMSPEC") ??
1792
+ "cmd.exe",
1793
+ windowsGoCommandArgs(shim.payload),
1794
+ {
1795
+ ...options,
1796
+ env: { ...inheritedEnv, ...shim.environment },
1797
+ shell: false,
1798
+ // The /c payload is already one fully quoted Windows command line.
1799
+ windowsVerbatimArguments: true,
1800
+ },
1801
+ );
1778
1802
  }
1779
1803
 
1780
- function shouldSpawnGoToolThroughShell(goBinary: string): boolean {
1781
- return process.platform === "win32" && /\.(?:bat|cmd)$/i.test(goBinary);
1804
+ /** Build the fixed cmd.exe switch sequence for one already quoted payload. */
1805
+ export function windowsGoCommandArgs(payload: string): string[] {
1806
+ return ["/d", "/v:off", "/s", "/c", payload];
1807
+ }
1808
+
1809
+ function spawnWorkingDirectory(
1810
+ cwd: SpawnSyncOptionsWithStringEncoding["cwd"],
1811
+ ): string {
1812
+ if (cwd === undefined) return process.cwd();
1813
+ return path.resolve(typeof cwd === "string" ? cwd : fileURLToPath(cwd));
1814
+ }
1815
+
1816
+ function isWindowsCommandWrapper(location: string): boolean {
1817
+ return process.platform === "win32" && /\.(?:bat|cmd)$/i.test(location);
1818
+ }
1819
+
1820
+ /** Pass volatile cmd wrapper arguments through one-pass environment expansion. */
1821
+ function createWindowsGoCommandShim(args: readonly string[]): {
1822
+ environment: NodeJS.ProcessEnv;
1823
+ payload: string;
1824
+ } {
1825
+ const prefix = `TTSC_GO_COMMAND_SHIM_${crypto
1826
+ .randomBytes(8)
1827
+ .toString("hex")
1828
+ .toUpperCase()}_ARG_`;
1829
+ const environment = Object.fromEntries(
1830
+ args.map((arg, index) => [prefix + index, quoteWindowsCommandArg(arg)]),
1831
+ );
1832
+ return {
1833
+ environment,
1834
+ // cmd expands each placeholder exactly once. Percent-shaped text inside an
1835
+ // expanded value is not scanned as a second environment reference.
1836
+ payload: `"${args.map((_, index) => `%${prefix}${index}%`).join(" ")}"`,
1837
+ };
1838
+ }
1839
+
1840
+ /** Quote one argv value for the Windows command-line parser used by cmd. */
1841
+ function quoteWindowsCommandArg(arg: string): string {
1842
+ return `"${String(arg)
1843
+ .replace(/(\\*)"/g, '$1$1\\"')
1844
+ .replace(/(\\*)$/, "$1$1")}"`;
1782
1845
  }
1783
1846
 
1784
1847
  function goBuildEnv(
@@ -2228,15 +2291,19 @@ export function computeCacheKey(inputs: {
2228
2291
  tsgoVersion: string;
2229
2292
  }): string {
2230
2293
  const env = inputs.env ?? process.env;
2294
+ const goBinary =
2295
+ inputs.goBinary === undefined
2296
+ ? undefined
2297
+ : resolveGoToolForBuild(inputs.goBinary, env, inputs.dir);
2231
2298
  const hash = crypto.createHash("sha256");
2232
2299
  hash.update(`ttsc=${inputs.ttscVersion}\n`);
2233
2300
  hash.update(`tsgo=${inputs.tsgoVersion}\n`);
2234
2301
  hash.update(`platform=${process.platform}/${process.arch}\n`);
2235
2302
  hash.update(`entry=${inputs.entry}\n`);
2236
- if (inputs.goBinary !== undefined) {
2237
- hash.update(`go=${resolveGoCompilerIdentity(inputs.goBinary, env)}\n`);
2303
+ if (goBinary !== undefined) {
2304
+ hash.update(`go=${resolveGoCompilerIdentity(goBinary, env, inputs.dir)}\n`);
2238
2305
  }
2239
- hashGoBuildEnvironment(hash, inputs.goBinary, inputs.dir, env);
2306
+ hashGoBuildEnvironment(hash, goBinary, inputs.dir, env);
2240
2307
  hashExternalGoBuildEnvironment(hash, env);
2241
2308
  hashSourceDirectory(hash, "plugin", inputs.dir);
2242
2309
  for (const [index, dir] of [...(inputs.overlayDirs ?? [])].sort().entries()) {
@@ -2328,23 +2395,42 @@ function isHashableFile(name: string): boolean {
2328
2395
  // content signature (byte size + nanosecond mtime). That signature changes if
2329
2396
  // a long-lived host rewrites the binary in place between calls, so the memo
2330
2397
  // re-derives the identity exactly as the un-memoized code would and the
2331
- // cache-key bytes stay byte-for-byte identical to today. `go version` reads no
2332
- // cwd/custom env (the spawn passes neither), so cwd is not part of the key.
2398
+ // cache-key bytes stay byte-for-byte identical to today. The selected compiler
2399
+ // path is shared by every build subprocess on Windows, while `go version` uses
2400
+ // the same effective cwd and environment as the cache-key `go env` query. The
2401
+ // memo key includes that context so an environment-sensitive wrapper cannot
2402
+ // lend its version result to another compiler invocation.
2333
2403
  const goCompilerIdentityCache = new Map<string, string>();
2334
2404
 
2335
2405
  function resolveGoCompilerIdentity(
2336
2406
  goBinary: string,
2337
2407
  env: NodeJS.ProcessEnv = process.env,
2408
+ cwd: string = process.cwd(),
2338
2409
  ): string {
2339
- const resolved = resolveExecutableIdentityPath(goBinary, env);
2340
- const memoKey = goCompilerIdentityMemoKey(goBinary, resolved);
2410
+ const selected = resolveGoToolForBuild(goBinary, env, cwd);
2411
+ const resolved =
2412
+ process.platform === "win32"
2413
+ ? resolveRealPath(selected)
2414
+ : resolveExecutableIdentityPath(selected, env, cwd);
2415
+ const compilerEnv = goBuildEnv(selected, undefined, env);
2416
+ const memoKey = goCompilerIdentityMemoKey(
2417
+ goBinary,
2418
+ resolved,
2419
+ compilerEnv,
2420
+ cwd,
2421
+ );
2341
2422
  if (memoKey !== null) {
2342
2423
  const cached = goCompilerIdentityCache.get(memoKey);
2343
2424
  if (cached !== undefined) {
2344
2425
  return cached;
2345
2426
  }
2346
2427
  }
2347
- const identity = computeGoCompilerIdentity(goBinary, resolved);
2428
+ const identity = computeGoCompilerIdentity(
2429
+ selected,
2430
+ resolved,
2431
+ compilerEnv,
2432
+ cwd,
2433
+ );
2348
2434
  if (memoKey !== null) {
2349
2435
  goCompilerIdentityCache.set(memoKey, identity);
2350
2436
  }
@@ -2357,21 +2443,38 @@ function resolveGoCompilerIdentity(
2357
2443
  function goCompilerIdentityMemoKey(
2358
2444
  goBinary: string,
2359
2445
  resolved: string,
2446
+ env: NodeJS.ProcessEnv,
2447
+ cwd: string,
2360
2448
  ): string | null {
2361
2449
  try {
2362
2450
  const stat = fs.statSync(resolved);
2363
- return `${goBinary}\0${resolved}\0${stat.size}\0${stat.mtimeMs}`;
2451
+ const context = crypto.createHash("sha256");
2452
+ context.update(cwd);
2453
+ for (const [key, value] of Object.entries(env).sort(([left], [right]) =>
2454
+ left === right ? 0 : left < right ? -1 : 1,
2455
+ )) {
2456
+ if (value === undefined) continue;
2457
+ context.update(`\0${key.length}:${key}${value.length}:${value}`);
2458
+ }
2459
+ return `${goBinary}\0${resolved}\0${stat.size}\0${stat.mtimeMs}\0${context.digest("hex")}`;
2364
2460
  } catch {
2365
2461
  return null;
2366
2462
  }
2367
2463
  }
2368
2464
 
2369
- function computeGoCompilerIdentity(goBinary: string, resolved: string): string {
2465
+ function computeGoCompilerIdentity(
2466
+ goBinary: string,
2467
+ resolved: string,
2468
+ env: NodeJS.ProcessEnv,
2469
+ cwd: string,
2470
+ ): string {
2370
2471
  if (!fs.existsSync(resolved)) {
2371
2472
  return "missing";
2372
2473
  }
2373
2474
  const version = spawnGoTool(goBinary, ["version"], {
2475
+ cwd,
2374
2476
  encoding: "utf8",
2477
+ env,
2375
2478
  windowsHide: true,
2376
2479
  });
2377
2480
  const versionText =
@@ -2385,48 +2488,207 @@ function computeGoCompilerIdentity(goBinary: string, resolved: string): string {
2385
2488
  function resolveExecutableIdentityPath(
2386
2489
  binary: string,
2387
2490
  env: NodeJS.ProcessEnv = process.env,
2491
+ cwd: string = process.cwd(),
2388
2492
  ): string {
2389
- if (path.isAbsolute(binary)) {
2390
- return resolveRealPath(binary);
2391
- }
2392
- if (binary.includes(path.sep)) {
2393
- return resolveRealPath(path.resolve(binary));
2394
- }
2395
- for (const dir of readPathEnvironment(env).split(path.delimiter)) {
2396
- if (dir.length === 0) continue;
2397
- const candidate = path.join(dir, binary);
2398
- if (fs.existsSync(candidate)) {
2399
- return resolveRealPath(candidate);
2400
- }
2401
- if (process.platform === "win32") {
2402
- // Probe every PATHEXT extension, not just `.exe`, so a compiler backed by
2403
- // a `.cmd`/`.bat` wrapper resolves to its real file and is hashed into the
2404
- // cache key. Otherwise the wrapper reads as "missing" and a change to it
2405
- // would not invalidate the cached plugin binary.
2406
- for (const ext of windowsExecutableExtensions(env)) {
2407
- const executable = `${candidate}${ext}`;
2408
- if (fs.existsSync(executable)) {
2409
- return resolveRealPath(executable);
2410
- }
2493
+ const resolved = findExecutablePath(binary, env, cwd);
2494
+ return resolved === null ? binary : resolveRealPath(resolved);
2495
+ }
2496
+
2497
+ /** Pin Windows PATH and command-wrapper lookup to one build-wide target. */
2498
+ function resolveGoToolForBuild(
2499
+ binary: string,
2500
+ env: NodeJS.ProcessEnv,
2501
+ cwd: string,
2502
+ ): string {
2503
+ if (process.platform !== "win32") return binary;
2504
+ return resolveWindowsGoTool(binary, env, cwd).location ?? binary;
2505
+ }
2506
+
2507
+ function findExecutablePath(
2508
+ binary: string,
2509
+ env: NodeJS.ProcessEnv,
2510
+ cwd: string,
2511
+ ): string | null {
2512
+ for (const candidate of executableSearchBases(binary, env, cwd)) {
2513
+ const resolved = findExecutableCandidate(candidate, env);
2514
+ if (resolved !== null) return resolved;
2515
+ }
2516
+ return null;
2517
+ }
2518
+
2519
+ interface IWindowsGoToolResolution {
2520
+ location: string | null;
2521
+ wrapper: boolean;
2522
+ }
2523
+
2524
+ /** Preserve libuv's native target before falling back to cmd/bat wrappers. */
2525
+ function resolveWindowsGoTool(
2526
+ binary: string,
2527
+ env: NodeJS.ProcessEnv,
2528
+ cwd: string,
2529
+ ): IWindowsGoToolResolution {
2530
+ const candidates = executableSearchBases(binary, env, cwd);
2531
+ if (isWindowsCommandWrapper(binary)) {
2532
+ return {
2533
+ location: candidates.find(isExecutableFile) ?? null,
2534
+ wrapper: true,
2535
+ };
2536
+ }
2537
+
2538
+ const hasExtension = windowsFileNameHasExtension(binary);
2539
+ for (const candidate of candidates) {
2540
+ if (hasExtension && isExecutableFile(candidate)) {
2541
+ return { location: candidate, wrapper: false };
2542
+ }
2543
+ for (const extension of [".com", ".exe"]) {
2544
+ const executable = candidate + extension;
2545
+ if (isExecutableFile(executable)) {
2546
+ return { location: executable, wrapper: false };
2547
+ }
2548
+ }
2549
+ }
2550
+
2551
+ const wrapperExtensions = windowsExecutableExtensions(env).filter((ext) =>
2552
+ /\.(?:bat|cmd)$/i.test(ext),
2553
+ );
2554
+ for (const candidate of candidates) {
2555
+ for (const extension of wrapperExtensions) {
2556
+ const executable = candidate + extension;
2557
+ if (isExecutableFile(executable)) {
2558
+ return { location: executable, wrapper: true };
2411
2559
  }
2412
2560
  }
2413
2561
  }
2414
- return binary;
2562
+ return { location: null, wrapper: false };
2563
+ }
2564
+
2565
+ function executableSearchBases(
2566
+ binary: string,
2567
+ env: NodeJS.ProcessEnv,
2568
+ cwd: string,
2569
+ ): string[] {
2570
+ if (path.isAbsolute(binary)) return [binary];
2571
+ if (hasPathQualifier(binary)) return [path.resolve(cwd, binary)];
2572
+
2573
+ const directories =
2574
+ process.platform === "win32"
2575
+ ? splitWindowsSearchPath(readPathEnvironment(env))
2576
+ // libuv skips only genuinely empty slices. A quoted-empty slice
2577
+ // survives that check, unquotes to "", and therefore names cwd.
2578
+ .filter((entry) => entry.length > 0)
2579
+ .map(unquoteWindowsSearchEntry)
2580
+ : readPathEnvironment(env).split(path.delimiter);
2581
+ const noDefaultCurrentDirectory =
2582
+ process.platform === "win32"
2583
+ ? readWindowsEnvironmentValue(
2584
+ process.env,
2585
+ "NoDefaultCurrentDirectoryInExePath",
2586
+ )
2587
+ : undefined;
2588
+ if (process.platform === "win32" && noDefaultCurrentDirectory === undefined) {
2589
+ directories.unshift(cwd);
2590
+ }
2591
+ return directories.map((dir) => path.resolve(cwd, dir, binary));
2592
+ }
2593
+
2594
+ function findExecutableCandidate(
2595
+ candidate: string,
2596
+ env: NodeJS.ProcessEnv,
2597
+ ): string | null {
2598
+ if (isExecutableFile(candidate)) return candidate;
2599
+ if (process.platform !== "win32") return null;
2600
+ // Probe every PATHEXT extension, not just `.exe`, so a compiler backed by a
2601
+ // `.cmd`/`.bat` wrapper is both launched correctly and hashed into the cache
2602
+ // key. Otherwise the wrapper reads as missing and changes do not invalidate
2603
+ // the cached plugin binary.
2604
+ for (const ext of windowsExecutableExtensions(env)) {
2605
+ const executable = `${candidate}${ext}`;
2606
+ if (isExecutableFile(executable)) return executable;
2607
+ }
2608
+ return null;
2609
+ }
2610
+
2611
+ function isExecutableFile(location: string): boolean {
2612
+ try {
2613
+ return fs.statSync(location).isFile();
2614
+ } catch {
2615
+ return false;
2616
+ }
2617
+ }
2618
+
2619
+ function hasPathQualifier(location: string): boolean {
2620
+ return (
2621
+ location.includes(path.sep) ||
2622
+ (process.platform === "win32" &&
2623
+ (location.includes("/") || /^[a-zA-Z]:/.test(location)))
2624
+ );
2625
+ }
2626
+
2627
+ function windowsFileNameHasExtension(location: string): boolean {
2628
+ const name = path.basename(location);
2629
+ const dot = name.indexOf(".");
2630
+ return dot >= 0 && dot < name.length - 1;
2631
+ }
2632
+
2633
+ function splitWindowsSearchPath(value: string): string[] {
2634
+ const entries: string[] = [];
2635
+ let start = 0;
2636
+ let quote = "";
2637
+ for (let index = 0; index < value.length; index++) {
2638
+ const character = value[index]!;
2639
+ if (quote !== "") {
2640
+ if (character === quote) quote = "";
2641
+ } else if ((character === '"' || character === "'") && index === start) {
2642
+ quote = character;
2643
+ } else if (character === ";") {
2644
+ entries.push(value.slice(start, index));
2645
+ start = index + 1;
2646
+ }
2647
+ }
2648
+ entries.push(value.slice(start));
2649
+ return entries;
2650
+ }
2651
+
2652
+ function unquoteWindowsSearchEntry(location: string): string {
2653
+ const first = location[0];
2654
+ const withoutFirst =
2655
+ first === '"' || first === "'" ? location.slice(1) : location;
2656
+ const last = withoutFirst[withoutFirst.length - 1];
2657
+ return last === '"' || last === "'"
2658
+ ? withoutFirst.slice(0, -1)
2659
+ : withoutFirst;
2415
2660
  }
2416
2661
 
2417
2662
  function windowsExecutableExtensions(
2418
2663
  env: NodeJS.ProcessEnv = process.env,
2419
2664
  ): readonly string[] {
2420
- const pathext = env.PATHEXT;
2665
+ const pathext = readWindowsEnvironmentValue(env, "PATHEXT");
2421
2666
  const raw = pathext && pathext.length > 0 ? pathext : ".COM;.EXE;.BAT;.CMD";
2422
- return raw
2423
- .split(";")
2424
- .map((ext) => ext.trim().toLowerCase())
2667
+ return splitWindowsSearchPath(raw)
2668
+ .map(unquoteWindowsSearchEntry)
2669
+ .map((ext) => ext.toLowerCase())
2425
2670
  .filter((ext) => ext.length > 0);
2426
2671
  }
2427
2672
 
2428
2673
  function readPathEnvironment(env: NodeJS.ProcessEnv = process.env): string {
2429
- return env.PATH ?? env.Path ?? "";
2674
+ return process.platform === "win32"
2675
+ ? (readWindowsEnvironmentValue(env, "PATH") ??
2676
+ readWindowsEnvironmentValue(process.env, "PATH") ??
2677
+ "")
2678
+ : (env.PATH ?? "/usr/bin:/bin");
2679
+ }
2680
+
2681
+ function readWindowsEnvironmentValue(
2682
+ env: NodeJS.ProcessEnv,
2683
+ name: string,
2684
+ ): string | undefined {
2685
+ const exact = env[name];
2686
+ if (exact !== undefined) return exact;
2687
+ const normalized = name.toLowerCase();
2688
+ const key = Object.keys(env)
2689
+ .filter((candidate) => candidate.toLowerCase() === normalized)
2690
+ .sort()[0];
2691
+ return key === undefined ? undefined : env[key];
2430
2692
  }
2431
2693
 
2432
2694
  function resolveRealPath(location: string): string {
@@ -5,6 +5,7 @@ import os from "node:os";
5
5
  import path from "node:path";
6
6
 
7
7
  import { findNearestGoMod } from "../../compiler/internal/paths";
8
+ import { readJsonFile } from "../../compiler/internal/project/readConfigJson";
8
9
  import { readProjectConfig } from "../../compiler/internal/project/readProjectConfig";
9
10
  import type { ITtscPlugin } from "../../structures/ITtscPlugin";
10
11
  import type { ITtscPluginContributor } from "../../structures/ITtscPluginContributor";
@@ -541,11 +542,17 @@ function findNearestPackageJson(location: string): string | undefined {
541
542
  }
542
543
  }
543
544
 
545
+ /**
546
+ * Read a package manifest, or `undefined` when the file is absent or is not a
547
+ * JSON object. A malformed manifest throws naming the file: these are usually
548
+ * files the user did not author, which makes an unattributed `JSON.parse`
549
+ * message worse here than anywhere else.
550
+ */
544
551
  function readPackageManifest(file: string): PackageManifest | undefined {
545
552
  if (!fs.existsSync(file)) {
546
553
  return undefined;
547
554
  }
548
- const parsed = JSON.parse(fs.readFileSync(file, "utf8")) as unknown;
555
+ const parsed = readJsonFile(file);
549
556
  return isRecord(parsed) ? (parsed as PackageManifest) : undefined;
550
557
  }
551
558
 
@@ -1157,7 +1164,78 @@ function selectExportTarget(exportsField: unknown, subpath: string): unknown {
1157
1164
  // Conditions object: the whole value is the `.` target.
1158
1165
  return subpath === "." ? exportsField : undefined;
1159
1166
  }
1160
- return subpath in record ? record[subpath] : undefined;
1167
+ if (
1168
+ Object.prototype.hasOwnProperty.call(record, subpath) &&
1169
+ !subpath.includes("*") &&
1170
+ !subpath.endsWith("/")
1171
+ ) {
1172
+ return record[subpath];
1173
+ }
1174
+ const patterns = Object.keys(record)
1175
+ .filter((key) => exportPatternReplacement(key, subpath) !== undefined)
1176
+ .sort(compareExportPatternKeys);
1177
+ if (patterns.length === 0) {
1178
+ return undefined;
1179
+ }
1180
+ const pattern = patterns[0]!;
1181
+ return substituteExportTarget(
1182
+ record[pattern],
1183
+ exportPatternReplacement(pattern, subpath)!,
1184
+ );
1185
+ }
1186
+
1187
+ /** Capture the middle of one valid single-star exports key. */
1188
+ function exportPatternReplacement(
1189
+ pattern: string,
1190
+ subpath: string,
1191
+ ): string | undefined {
1192
+ const star = pattern.indexOf("*");
1193
+ if (
1194
+ !pattern.startsWith("./") ||
1195
+ star === -1 ||
1196
+ pattern.indexOf("*", star + 1) !== -1
1197
+ ) {
1198
+ return undefined;
1199
+ }
1200
+ const prefix = pattern.slice(0, star);
1201
+ const suffix = pattern.slice(star + 1);
1202
+ if (
1203
+ subpath.length < pattern.length ||
1204
+ !subpath.startsWith(prefix) ||
1205
+ !subpath.endsWith(suffix)
1206
+ ) {
1207
+ return undefined;
1208
+ }
1209
+ return subpath.slice(prefix.length, subpath.length - suffix.length);
1210
+ }
1211
+
1212
+ /** Node exports patterns rank longer prefixes, then longer full keys, first. */
1213
+ function compareExportPatternKeys(left: string, right: string): number {
1214
+ const leftPrefix = left.indexOf("*");
1215
+ const rightPrefix = right.indexOf("*");
1216
+ if (leftPrefix !== rightPrefix) {
1217
+ return rightPrefix - leftPrefix;
1218
+ }
1219
+ return right.length - left.length;
1220
+ }
1221
+
1222
+ /** Substitute the selected pattern capture into every string target branch. */
1223
+ function substituteExportTarget(target: unknown, replacement: string): unknown {
1224
+ if (typeof target === "string") {
1225
+ return target.split("*").join(replacement);
1226
+ }
1227
+ if (Array.isArray(target)) {
1228
+ return target.map((entry) => substituteExportTarget(entry, replacement));
1229
+ }
1230
+ if (typeof target !== "object" || target === null) {
1231
+ return target;
1232
+ }
1233
+ return Object.fromEntries(
1234
+ Object.entries(target).map(([condition, value]) => [
1235
+ condition,
1236
+ substituteExportTarget(value, replacement),
1237
+ ]),
1238
+ );
1161
1239
  }
1162
1240
 
1163
1241
  /** True when condition key `condition` appears anywhere in a (nested) target. */
@@ -48,6 +48,23 @@ export namespace ITtscCompilerTransformation {
48
48
 
49
49
  /** The project tsconfig followed by its `extends` ancestry. */
50
50
  configs: string[];
51
+
52
+ /**
53
+ * Resolution candidates that would outrank the selected target, keyed by
54
+ * importing file. The compiler already resolved the import to a graph edge;
55
+ * a listed path can be absent now or can be an existing unsuccessful probe,
56
+ * and its appearance or change can alter that edge without editing the
57
+ * importer or a recorded file.
58
+ *
59
+ * Hosts watch and hash these paths in addition to realized graph members. A
60
+ * candidate strictly below the selected target is deliberately absent: its
61
+ * creation cannot change resolution and must not invalidate a cache.
62
+ *
63
+ * A host with nothing to report leaves the whole property out rather than
64
+ * sending an empty map, so `undefined` and "no superseding candidate" are
65
+ * the same observation on the wire and after decoding.
66
+ */
67
+ candidates?: Record<string, string[]>;
51
68
  }
52
69
 
53
70
  /** Successful source-to-source transformation result. */
@@ -3,6 +3,8 @@ import type { ITtscProjectIdentity } from "./ITtscProjectIdentity";
3
3
 
4
4
  /** Resolved project config subset used inside the ttsc host. */
5
5
  export interface ITtscParsedProjectConfig {
6
+ /** Every resolved tsconfig/jsconfig in the inherited `extends` chain. */
7
+ configPaths: readonly string[];
6
8
  /** Compiler options after extends inheritance has been applied. */
7
9
  compilerOptions: {
8
10
  /** Absolute output directory when configured. */