ttsc 0.19.3 → 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 (170) 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 +276 -100
  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_uses_ecma_line_terminators_test.go +87 -0
  33. package/internal/graph/duplicate_variable_object_members_keep_first_declaration_test.go +61 -0
  34. package/internal/graph/edges.go +208 -0
  35. package/internal/graph/graph.go +143 -5
  36. package/internal/graph/line_model.go +122 -0
  37. package/internal/graph/member_relation_edges_follow_checker_validity_test.go +85 -0
  38. package/internal/graph/member_relation_edges_preserve_checker_boundaries_test.go +191 -0
  39. package/internal/graph/member_relation_edges_reject_checker_invalid_pairs_test.go +119 -0
  40. package/internal/graph/node_ids_preserve_hash_bearing_components_test.go +41 -0
  41. package/internal/graph/node_modifiers_emit_union_strings_test.go +4 -1
  42. package/internal/graph/object_literal_members_ride_on_the_snapshot_test.go +192 -0
  43. package/internal/graph/object_member_signatures_preserve_literal_whitespace_test.go +67 -0
  44. package/internal/graph/object_members.go +229 -0
  45. package/internal/graph/provenance.go +88 -30
  46. package/internal/graph/source_texts_cover_every_resident_program_source_test.go +73 -0
  47. package/internal/graphsymbols/position_helpers_follow_ecma_line_terminators_test.go +66 -0
  48. package/internal/graphsymbols/provider.go +26 -91
  49. package/internal/lspserver/lsp_clean_didopen_refreshes_compiler_state_test.go +144 -0
  50. package/internal/lspserver/lsp_completion.go +109 -0
  51. package/internal/lspserver/lsp_completion_gate_never_refuses_a_matching_line_test.go +50 -0
  52. package/internal/lspserver/lsp_completion_hints_longest_trigger_wins_test.go +149 -0
  53. package/internal/lspserver/lsp_completion_hints_nearest_trigger_wins_test.go +37 -0
  54. package/internal/lspserver/lsp_completion_merges_into_upstream_response_test.go +149 -0
  55. package/internal/lspserver/lsp_completion_nearest_hint_replaces_nearest_filter_test.go +43 -0
  56. package/internal/lspserver/lsp_completion_preserves_completion_list_extensions_test.go +99 -0
  57. package/internal/lspserver/lsp_completion_refuses_hints_inside_a_string_literal_test.go +69 -0
  58. package/internal/lspserver/lsp_completion_replaces_full_filter_in_rank_order_test.go +85 -0
  59. package/internal/lspserver/lsp_completion_request.go +455 -0
  60. package/internal/lspserver/lsp_completion_scope.go +530 -0
  61. package/internal/lspserver/lsp_completion_scope_bench_test.go +41 -0
  62. package/internal/lspserver/lsp_completion_scope_classifies_regex_statement_and_operator_boundaries_test.go +39 -0
  63. package/internal/lspserver/lsp_completion_scope_ignores_jsdoc_bytes_in_literals_test.go +108 -0
  64. package/internal/lspserver/lsp_completion_scope_refuses_regex_hints_test.go +52 -0
  65. package/internal/lspserver/lsp_completion_trigger_uses_last_rune_test.go +24 -0
  66. package/internal/lspserver/lsp_deleted_watched_file_withdraws_plugin_diagnostics_test.go +110 -0
  67. package/internal/lspserver/lsp_didsave_keeps_the_incremental_changed_uri_path_test.go +78 -0
  68. package/internal/lspserver/lsp_hints_absence_is_not_an_error_test.go +46 -0
  69. package/internal/lspserver/lsp_hints_refresh_coalesces_concurrent_requests_test.go +82 -0
  70. package/internal/lspserver/lsp_hints_refresh_ignores_a_superseded_generation_test.go +40 -0
  71. package/internal/lspserver/lsp_hints_refresh_keeps_each_producer_isolated_test.go +64 -0
  72. package/internal/lspserver/lsp_hints_refresh_replaces_the_session_snapshot_test.go +66 -0
  73. package/internal/lspserver/lsp_hints_refresh_scheduled_by_editor_events_test.go +90 -0
  74. package/internal/lspserver/lsp_hints_refresh_serves_completion_while_running_test.go +91 -0
  75. package/internal/lspserver/lsp_index_byte_from_bounds_its_search_start_test.go +38 -0
  76. package/internal/lspserver/lsp_initialize_constrains_position_encoding_to_utf16_test.go +205 -0
  77. package/internal/lspserver/lsp_late_completion_trigger_is_reported_once_test.go +105 -0
  78. package/internal/lspserver/lsp_native_completion_hints_decode_flat_lint_wire_test.go +66 -0
  79. package/internal/lspserver/lsp_native_completion_hints_grouped_compatibility_boundary_test.go +60 -0
  80. package/internal/lspserver/lsp_native_plugin_source.go +315 -2
  81. package/internal/lspserver/lsp_native_resident.go +304 -0
  82. package/internal/lspserver/lsp_plugin.go +85 -8
  83. package/internal/lspserver/lsp_plugin_completion_resolve_stays_local_test.go +82 -0
  84. package/internal/lspserver/lsp_position_conversions_span_astral_and_combining_runes_test.go +119 -0
  85. package/internal/lspserver/lsp_proxy.go +601 -9
  86. package/internal/lspserver/lsp_refresh.go +70 -0
  87. package/internal/lspserver/lsp_resident_falls_back_when_serve_unsupported_test.go +43 -0
  88. package/internal/lspserver/lsp_resident_maps_verb_args_test.go +34 -0
  89. package/internal/lspserver/lsp_server.go +13 -1
  90. package/internal/lspserver/lsp_watched_file_changes_reach_compiler_caches_test.go +172 -0
  91. package/lib/TtscCompiler.d.ts +3 -1
  92. package/lib/TtscCompiler.js +5 -1
  93. package/lib/TtscCompiler.js.map +1 -1
  94. package/lib/TtscService.d.ts +13 -3
  95. package/lib/TtscService.js +6 -6
  96. package/lib/TtscService.js.map +1 -1
  97. package/lib/compiler/internal/project/readConfigJson.d.ts +30 -0
  98. package/lib/compiler/internal/project/readConfigJson.js +208 -0
  99. package/lib/compiler/internal/project/readConfigJson.js.map +1 -0
  100. package/lib/compiler/internal/project/readProjectConfig.js +53 -149
  101. package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
  102. package/lib/compiler/internal/residentTransformProcess.d.ts +15 -2
  103. package/lib/compiler/internal/residentTransformProcess.js +159 -39
  104. package/lib/compiler/internal/residentTransformProcess.js.map +1 -1
  105. package/lib/compiler/internal/runBuild.d.ts +6 -0
  106. package/lib/compiler/internal/runBuild.js +143 -44
  107. package/lib/compiler/internal/runBuild.js.map +1 -1
  108. package/lib/compiler/internal/startResidentTransform.d.ts +5 -1
  109. package/lib/compiler/internal/startResidentTransform.js +3 -1
  110. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  111. package/lib/compiler/internal/transformProjectInMemory.js +12 -2
  112. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  113. package/lib/flags/parser.d.ts +19 -0
  114. package/lib/flags/parser.js +78 -36
  115. package/lib/flags/parser.js.map +1 -1
  116. package/lib/flags/schema.d.ts +49 -11
  117. package/lib/flags/schema.js +94 -22
  118. package/lib/flags/schema.js.map +1 -1
  119. package/lib/internal/assertSafeExplicitCacheDirectory.d.ts +5 -0
  120. package/lib/internal/assertSafeExplicitCacheDirectory.js +26 -0
  121. package/lib/internal/assertSafeExplicitCacheDirectory.js.map +1 -0
  122. package/lib/launcher/internal/runTtsc.js +107 -69
  123. package/lib/launcher/internal/runTtsc.js.map +1 -1
  124. package/lib/launcher/internal/runTtsx.js +28 -41
  125. package/lib/launcher/internal/runTtsx.js.map +1 -1
  126. package/lib/launcher/internal/runtimeHooks.d.ts +15 -0
  127. package/lib/launcher/internal/runtimeHooks.js +18 -1
  128. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  129. package/lib/launcher/internal/watchTopology.d.ts +43 -0
  130. package/lib/launcher/internal/watchTopology.js +360 -0
  131. package/lib/launcher/internal/watchTopology.js.map +1 -0
  132. package/lib/plugin/internal/buildSourcePlugin.d.ts +14 -3
  133. package/lib/plugin/internal/buildSourcePlugin.js +247 -61
  134. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  135. package/lib/plugin/internal/loadProjectPlugins.js +63 -2
  136. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  137. package/lib/structures/ITtscCompilerTransformation.d.ts +16 -0
  138. package/lib/structures/internal/ITtscParsedProjectConfig.d.ts +2 -0
  139. package/package.json +8 -8
  140. package/shim/ast/shim.go +48 -3
  141. package/shim/astnav/go.mod +14 -0
  142. package/shim/astnav/go.sum +20 -0
  143. package/shim/astnav/shim.go +15 -0
  144. package/shim/checker/shim.go +100 -0
  145. package/shim/core/shim.go +47 -0
  146. package/shim/diagnosticwriter/lint.go +50 -0
  147. package/src/TtscCompiler.ts +5 -1
  148. package/src/TtscService.ts +37 -10
  149. package/src/compiler/internal/project/readConfigJson.ts +209 -0
  150. package/src/compiler/internal/project/readProjectConfig.ts +62 -158
  151. package/src/compiler/internal/residentTransformProcess.ts +197 -39
  152. package/src/compiler/internal/runBuild.ts +169 -47
  153. package/src/compiler/internal/startResidentTransform.ts +12 -1
  154. package/src/compiler/internal/transformProjectInMemory.ts +12 -1
  155. package/src/flags/parser.ts +113 -42
  156. package/src/flags/schema.ts +113 -31
  157. package/src/internal/assertSafeExplicitCacheDirectory.ts +29 -0
  158. package/src/launcher/internal/runTtsc.ts +117 -68
  159. package/src/launcher/internal/runTtsx.ts +29 -44
  160. package/src/launcher/internal/runtimeHooks.ts +30 -2
  161. package/src/launcher/internal/watchTopology.ts +466 -0
  162. package/src/plugin/internal/buildSourcePlugin.ts +326 -64
  163. package/src/plugin/internal/loadProjectPlugins.ts +80 -2
  164. package/src/structures/ITtscCompilerTransformation.ts +17 -0
  165. package/src/structures/internal/ITtscParsedProjectConfig.ts +2 -0
  166. package/tools/shim_audit/baseline.json +0 -3
  167. package/tools/shim_audit/main.go +42 -14
  168. package/tools/shim_audit/shim_dir_coverage_test.go +25 -0
  169. package/utility/flags_gen.go +3 -3
  170. package/utility/host.go +9 -3
@@ -0,0 +1,209 @@
1
+ import fs from "node:fs";
2
+
3
+ /**
4
+ * Attributed JSON / JSONC readers for every configuration file ttsc owns.
5
+ *
6
+ * A bare `JSON.parse` failure reports a byte offset in an unnamed file, which
7
+ * is unusable exactly where attribution matters most: an `extends` chain, where
8
+ * any ancestor could be the source, and a watch session, which reprints the
9
+ * same line on every save. Every ttsc-owned configuration read goes through
10
+ * these helpers so the failure names its file and position in ttsc's own
11
+ * diagnostic voice, like the neighbouring `ttsc: extended tsconfig not found:
12
+ * …`.
13
+ *
14
+ * Comment, trailing-comma, and BOM removal is length-preserving (each removed
15
+ * character becomes a space, each removed newline stays a newline), so the line
16
+ * and column the JSON parser reports are the position in the file the user
17
+ * actually edited rather than in a shortened intermediate string.
18
+ */
19
+
20
+ /**
21
+ * Read and parse a strict-JSON configuration file (`package.json`), naming it
22
+ * on failure.
23
+ *
24
+ * A leading UTF-8 BOM is accepted, matching the tsconfig reader that closed
25
+ * issue #216: the two readers are consulted for the same project and
26
+ * disagreeing about a byte order mark would only surprise the user who hit it.
27
+ */
28
+ export function readJsonFile(file: string): unknown {
29
+ const text = stripLeadingBom(fs.readFileSync(file, "utf8"));
30
+ try {
31
+ return JSON.parse(text);
32
+ } catch (error) {
33
+ throw new Error(`ttsc: failed to parse ${file}: ${describe(error)}`);
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Read and parse a JSONC (JSON with comments and trailing commas) configuration
39
+ * file — tsconfig.json, jsconfig.json — naming it on failure.
40
+ */
41
+ export function readJsoncFile(file: string): unknown {
42
+ const text = fs.readFileSync(file, "utf8");
43
+ try {
44
+ return parseJsonc(text);
45
+ } catch (error) {
46
+ throw new Error(`ttsc: failed to parse ${file}: ${describe(error)}`);
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Parse a JSONC string by blanking comments and trailing commas before handing
52
+ * off to `JSON.parse`. Callers hold a path, so they go through
53
+ * {@link readJsoncFile} and their failures are attributed.
54
+ */
55
+ function parseJsonc(input: string): unknown {
56
+ return JSON.parse(stripTrailingCommas(stripComments(stripLeadingBom(input))));
57
+ }
58
+
59
+ /** Render a parse failure's message without the `Error:` noise around it. */
60
+ function describe(error: unknown): string {
61
+ return error instanceof Error ? error.message : String(error);
62
+ }
63
+
64
+ /**
65
+ * Replace a leading UTF-8 BOM with a space. JSON ignores leading whitespace, so
66
+ * blanking rather than removing keeps every later offset equal to the offset in
67
+ * the original file. A BOM anywhere else is left in place and still rejected,
68
+ * which is the behaviour issue #216 pinned.
69
+ */
70
+ function stripLeadingBom(input: string): string {
71
+ return input.charCodeAt(0) === 0xfeff ? ` ${input.slice(1)}` : input;
72
+ }
73
+
74
+ /**
75
+ * Blank `//` line comments and `/* block comments *\/` in a JSONC string,
76
+ * writing a space for every removed character and preserving newlines.
77
+ * Correctly handles strings that contain comment-like character sequences by
78
+ * tracking string boundaries and escape characters.
79
+ */
80
+ function stripComments(input: string): string {
81
+ let output = "";
82
+ let inBlockComment = false;
83
+ let inLineComment = false;
84
+ let inString = false;
85
+ let quote = "";
86
+ let escape = false;
87
+
88
+ for (let i = 0; i < input.length; i += 1) {
89
+ const current = input[i]!;
90
+ const next = input[i + 1];
91
+
92
+ if (inBlockComment) {
93
+ if (current === "*" && next === "/") {
94
+ inBlockComment = false;
95
+ output += " ";
96
+ i += 1;
97
+ continue;
98
+ }
99
+ output += blank(current);
100
+ continue;
101
+ }
102
+ if (inLineComment) {
103
+ if (current === "\n") {
104
+ inLineComment = false;
105
+ output += current;
106
+ continue;
107
+ }
108
+ output += blank(current);
109
+ continue;
110
+ }
111
+ if (inString) {
112
+ output += current;
113
+ if (escape) {
114
+ escape = false;
115
+ } else if (current === "\\") {
116
+ escape = true;
117
+ } else if (current === quote) {
118
+ inString = false;
119
+ quote = "";
120
+ }
121
+ continue;
122
+ }
123
+
124
+ if (current === '"' || current === "'") {
125
+ inString = true;
126
+ quote = current;
127
+ output += current;
128
+ continue;
129
+ }
130
+ if (current === "/" && next === "/") {
131
+ inLineComment = true;
132
+ output += " ";
133
+ i += 1;
134
+ continue;
135
+ }
136
+ if (current === "/" && next === "*") {
137
+ inBlockComment = true;
138
+ output += " ";
139
+ i += 1;
140
+ continue;
141
+ }
142
+ output += current;
143
+ }
144
+ return output;
145
+ }
146
+
147
+ /** Keep a newline as a newline; every other removed character becomes a space. */
148
+ function blank(current: string): string {
149
+ return current === "\n" ? "\n" : " ";
150
+ }
151
+
152
+ /**
153
+ * Blank trailing commas before `}` or `]` in a JSON string (after comments have
154
+ * already been blanked). Handles string boundaries and escape characters to
155
+ * avoid touching commas inside string values.
156
+ */
157
+ function stripTrailingCommas(input: string): string {
158
+ let output = "";
159
+ let inString = false;
160
+ let quote = "";
161
+ let escape = false;
162
+
163
+ for (let i = 0; i < input.length; i += 1) {
164
+ const current = input[i]!;
165
+ if (inString) {
166
+ output += current;
167
+ if (escape) {
168
+ escape = false;
169
+ } else if (current === "\\") {
170
+ escape = true;
171
+ } else if (current === quote) {
172
+ inString = false;
173
+ quote = "";
174
+ }
175
+ continue;
176
+ }
177
+
178
+ if (current === '"' || current === "'") {
179
+ inString = true;
180
+ quote = current;
181
+ output += current;
182
+ continue;
183
+ }
184
+ if (current === ",") {
185
+ const next = nextNonWhitespace(input, i + 1);
186
+ if (next === "}" || next === "]") {
187
+ output += " ";
188
+ continue;
189
+ }
190
+ }
191
+ output += current;
192
+ }
193
+ return output;
194
+ }
195
+
196
+ /**
197
+ * Return the first non-whitespace character at or after position `from` in
198
+ * `input`, or `undefined` when only whitespace remains. Used by
199
+ * `stripTrailingCommas` to detect whether a comma is trailing.
200
+ */
201
+ function nextNonWhitespace(input: string, from: number): string | undefined {
202
+ for (let i = from; i < input.length; i += 1) {
203
+ const current = input[i]!;
204
+ if (/\s/.test(current) === false) {
205
+ return current;
206
+ }
207
+ }
208
+ return undefined;
209
+ }
@@ -5,6 +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 { readJsonFile, readJsoncFile } from "./readConfigJson";
8
9
  import { resolveProjectIdentity } from "./resolveProjectConfig";
9
10
 
10
11
  /**
@@ -18,6 +19,7 @@ const PATH_OPTIONS = new Set(["baseUrl", "declarationDir", "rootDir"]);
18
19
  * projected into `ITtscParsedProjectConfig`.
19
20
  */
20
21
  type ResolvedCompilerOptions = {
22
+ configPaths: string[];
21
23
  options: Record<string, unknown>;
22
24
  /** Directory of the tsconfig that last declared each option key. */
23
25
  optionBaseDirs: Record<string, string>;
@@ -45,6 +47,7 @@ export function readProjectConfig(
45
47
  const root = identity.physicalProjectRoot;
46
48
  const compilerOptions = readResolvedCompilerOptions(tsconfig);
47
49
  return {
50
+ configPaths: compilerOptions.configPaths,
48
51
  compilerOptions: {
49
52
  ...compilerOptions.options,
50
53
  outDir: compilerOptions.outDir,
@@ -101,7 +104,7 @@ function readResolvedCompilerOptions(
101
104
  }
102
105
  seen.add(canonical);
103
106
  try {
104
- const parsed = parseJsonc(fs.readFileSync(canonical, "utf8")) as {
107
+ const parsed = readJsoncFile(canonical) as {
105
108
  extends?: unknown;
106
109
  compilerOptions?: Record<string, unknown> & {
107
110
  outDir?: unknown;
@@ -134,6 +137,7 @@ function readResolvedCompilerOptions(
134
137
  ? ownPlugins.filter(isProjectPluginConfig)
135
138
  : base.plugins;
136
139
  return {
140
+ configPaths: uniquePaths([...base.configPaths, canonical]),
137
141
  optionBaseDirs,
138
142
  options,
139
143
  outDir:
@@ -174,6 +178,7 @@ function resolveBaseCompilerOptions(
174
178
  }
175
179
  if (!Array.isArray(extended)) {
176
180
  return {
181
+ configPaths: [],
177
182
  optionBaseDirs: {},
178
183
  options: {},
179
184
  pluginBaseDirs: [],
@@ -182,6 +187,7 @@ function resolveBaseCompilerOptions(
182
187
  };
183
188
  }
184
189
  let merged: ResolvedCompilerOptions = {
190
+ configPaths: [],
185
191
  optionBaseDirs: {},
186
192
  options: {},
187
193
  pluginBaseDirs: [],
@@ -197,6 +203,7 @@ function resolveBaseCompilerOptions(
197
203
  seen,
198
204
  );
199
205
  merged = {
206
+ configPaths: uniquePaths([...merged.configPaths, ...current.configPaths]),
200
207
  optionBaseDirs: {
201
208
  ...merged.optionBaseDirs,
202
209
  ...current.optionBaseDirs,
@@ -216,6 +223,10 @@ function resolveBaseCompilerOptions(
216
223
  return merged;
217
224
  }
218
225
 
226
+ function uniquePaths(paths: readonly string[]): string[] {
227
+ return [...new Set(paths)];
228
+ }
229
+
219
230
  /**
220
231
  * Resolve path-typed compiler options to absolute paths using the directory of
221
232
  * the tsconfig that declared each option (`baseDirs`).
@@ -272,9 +283,10 @@ function resolveExtendsConfig(tsconfig: string, specifier: string): string {
272
283
  /**
273
284
  * When `specifier` names a bare package root, resolve the config file its
274
285
  * `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.
286
+ * Returns `undefined` when the specifier is a subpath, the manifest cannot be
287
+ * resolved at all, or it declares no `tsconfig` field, so the caller falls back
288
+ * to Node entrypoint resolution. A manifest that resolves but does not parse
289
+ * throws, naming the file.
278
290
  */
279
291
  function resolvePackageManifestTsconfig(
280
292
  resolver: NodeRequire,
@@ -286,19 +298,38 @@ function resolvePackageManifestTsconfig(
286
298
  let manifestPath: string;
287
299
  try {
288
300
  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 {
301
+ } catch (error) {
302
+ // Node parses a package's manifest while resolving into it, so a malformed
303
+ // preset manifest fails here rather than at the read below. Swallowing it
304
+ // is what turned a broken manifest into the confusing downstream
305
+ // "Cannot find module 'example-preset.json'" from the `extends` fallback,
306
+ // which names a file that was never the problem. Node's own message names
307
+ // the real one, so report it in ttsc's voice instead of continuing.
308
+ if (
309
+ (error as NodeJS.ErrnoException | undefined)?.code ===
310
+ "ERR_INVALID_PACKAGE_CONFIG"
311
+ ) {
312
+ throw new Error(
313
+ `ttsc: failed to parse the package manifest of ${JSON.stringify(specifier)}: ${
314
+ error instanceof Error ? error.message : String(error)
315
+ }`,
316
+ );
317
+ }
300
318
  return undefined;
301
319
  }
320
+ // A manifest that exists but does not parse is a real configuration error,
321
+ // and it used to be swallowed here: the read returned `undefined`, the caller
322
+ // fell back to Node entrypoint resolution, and the user learned nothing about
323
+ // the file that actually broke. Report it instead.
324
+ //
325
+ // A manifest that parses to something other than an object carries no
326
+ // `tsconfig` field to read, so it falls back rather than throwing on a
327
+ // property access.
328
+ const manifest = readJsonFile(manifestPath);
329
+ const field =
330
+ typeof manifest === "object" && manifest !== null
331
+ ? (manifest as { tsconfig?: unknown }).tsconfig
332
+ : undefined;
302
333
  if (typeof field !== "string" || field.length === 0) {
303
334
  return undefined;
304
335
  }
@@ -318,23 +349,31 @@ function isBarePackageRoot(specifier: string): boolean {
318
349
  }
319
350
 
320
351
  /**
321
- * Given an on-disk location, try the path as-is, with `.json` appended, and as
322
- * a directory containing `tsconfig.json`. Returns the first match or throws.
352
+ * Given an on-disk location, try the path as-is and, unless it already ends in
353
+ * `.json`, with that extension appended. Only regular files are valid `extends`
354
+ * targets; TypeScript neither expands a directory into `tsconfig.json` nor
355
+ * probes a double `.json` suffix.
323
356
  */
324
357
  function resolveExistingExtendsPath(location: string): string {
325
- const candidates = new Set<string>([
326
- location,
327
- `${location}.json`,
328
- path.join(location, "tsconfig.json"),
329
- ]);
358
+ const candidates = location.endsWith(".json")
359
+ ? [location]
360
+ : [location, `${location}.json`];
330
361
  for (const candidate of candidates) {
331
- if (fs.existsSync(candidate)) {
362
+ if (isFile(candidate)) {
332
363
  return resolveRealPath(candidate);
333
364
  }
334
365
  }
335
366
  throw new Error(`ttsc: extended tsconfig not found: ${location}`);
336
367
  }
337
368
 
369
+ function isFile(location: string): boolean {
370
+ try {
371
+ return fs.statSync(location).isFile();
372
+ } catch {
373
+ return false;
374
+ }
375
+ }
376
+
338
377
  /**
339
378
  * Return true when `specifier` is a relative path reference: `.`, `..`, or a
340
379
  * string starting with `./`, `../`, `.\\`, or `..\\`.
@@ -349,138 +388,3 @@ function isRelativeSpecifier(specifier: string): boolean {
349
388
  specifier.startsWith("..\\")
350
389
  );
351
390
  }
352
-
353
- /**
354
- * Parse a JSONC (JSON with Comments) string by stripping comments and trailing
355
- * commas before handing off to `JSON.parse`.
356
- */
357
- function parseJsonc(input: string): unknown {
358
- return JSON.parse(stripTrailingCommas(stripComments(stripLeadingBom(input))));
359
- }
360
-
361
- function stripLeadingBom(input: string): string {
362
- return input.charCodeAt(0) === 0xfeff ? input.slice(1) : input;
363
- }
364
-
365
- /**
366
- * Remove `//` line comments and `/* block comments *\/` from a JSONC string.
367
- * Correctly handles strings that contain comment-like character sequences by
368
- * tracking string boundaries and escape characters.
369
- */
370
- function stripComments(input: string): string {
371
- let output = "";
372
- let inBlockComment = false;
373
- let inLineComment = false;
374
- let inString = false;
375
- let quote = "";
376
- let escape = false;
377
-
378
- for (let i = 0; i < input.length; i += 1) {
379
- const current = input[i]!;
380
- const next = input[i + 1];
381
-
382
- if (inBlockComment) {
383
- if (current === "*" && next === "/") {
384
- inBlockComment = false;
385
- i += 1;
386
- }
387
- continue;
388
- }
389
- if (inLineComment) {
390
- if (current === "\n") {
391
- inLineComment = false;
392
- output += current;
393
- }
394
- continue;
395
- }
396
- if (inString) {
397
- output += current;
398
- if (escape) {
399
- escape = false;
400
- } else if (current === "\\") {
401
- escape = true;
402
- } else if (current === quote) {
403
- inString = false;
404
- quote = "";
405
- }
406
- continue;
407
- }
408
-
409
- if (current === '"' || current === "'") {
410
- inString = true;
411
- quote = current;
412
- output += current;
413
- continue;
414
- }
415
- if (current === "/" && next === "/") {
416
- inLineComment = true;
417
- i += 1;
418
- continue;
419
- }
420
- if (current === "/" && next === "*") {
421
- inBlockComment = true;
422
- i += 1;
423
- continue;
424
- }
425
- output += current;
426
- }
427
- return output;
428
- }
429
-
430
- /**
431
- * Remove trailing commas before `}` or `]` from a JSON string (after comments
432
- * have already been stripped). Handles string boundaries and escape characters
433
- * to avoid removing commas inside string values.
434
- */
435
- function stripTrailingCommas(input: string): string {
436
- let output = "";
437
- let inString = false;
438
- let quote = "";
439
- let escape = false;
440
-
441
- for (let i = 0; i < input.length; i += 1) {
442
- const current = input[i]!;
443
- if (inString) {
444
- output += current;
445
- if (escape) {
446
- escape = false;
447
- } else if (current === "\\") {
448
- escape = true;
449
- } else if (current === quote) {
450
- inString = false;
451
- quote = "";
452
- }
453
- continue;
454
- }
455
-
456
- if (current === '"' || current === "'") {
457
- inString = true;
458
- quote = current;
459
- output += current;
460
- continue;
461
- }
462
- if (current === ",") {
463
- const next = nextNonWhitespace(input, i + 1);
464
- if (next === "}" || next === "]") {
465
- continue;
466
- }
467
- }
468
- output += current;
469
- }
470
- return output;
471
- }
472
-
473
- /**
474
- * Return the first non-whitespace character at or after position `from` in
475
- * `input`, or `undefined` when only whitespace remains. Used by
476
- * `stripTrailingCommas` to detect whether a comma is trailing.
477
- */
478
- function nextNonWhitespace(input: string, from: number): string | undefined {
479
- for (let i = from; i < input.length; i += 1) {
480
- const current = input[i]!;
481
- if (/\s/.test(current) === false) {
482
- return current;
483
- }
484
- }
485
- return undefined;
486
- }