ttsc 0.18.3 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -6
- package/cmd/graphbench/main.go +1 -1
- package/cmd/platform/main.go +1 -48
- package/cmd/ttsc/build_branches_test.go +5 -1
- package/cmd/ttsc/main.go +0 -50
- package/cmd/ttsc/main_alias_branches_test.go +6 -12
- package/cmd/ttscgraph/dump.go +13 -3
- package/cmd/ttscgraph/main.go +18 -12
- package/cmd/ttscgraph/run_help_version_and_bad_flags_exit_codes_test.go +23 -5
- package/cmd/ttscgraph/serve.go +863 -0
- package/cmd/ttscgraph/serve_protocol_rejects_invalid_request_line_test.go +43 -0
- package/cmd/ttscgraph/serve_protocol_retries_session_creation_test.go +83 -0
- package/cmd/ttscgraph/serve_protocol_reuses_session_test.go +35 -0
- package/cmd/ttscgraph/serve_session_applies_source_edit_incrementally_test.go +55 -0
- package/cmd/ttscgraph/serve_session_fails_closed_on_deleted_config_test.go +68 -0
- package/cmd/ttscgraph/serve_session_fails_closed_on_invalid_config_test.go +50 -0
- package/cmd/ttscgraph/serve_session_keeps_missing_literal_root_unchanged_test.go +46 -0
- package/cmd/ttscgraph/serve_session_rebuilds_import_graph_change_test.go +60 -0
- package/cmd/ttscgraph/serve_session_reloads_created_literal_root_test.go +44 -0
- package/cmd/ttscgraph/serve_session_reloads_extended_config_change_test.go +45 -0
- package/cmd/ttscgraph/serve_session_reloads_newly_resolved_conditional_export_test.go +60 -0
- package/cmd/ttscgraph/serve_session_reloads_newly_resolved_config_types_test.go +44 -0
- package/cmd/ttscgraph/serve_session_reloads_newly_resolved_excluded_import_test.go +62 -0
- package/cmd/ttscgraph/serve_session_reloads_newly_resolved_package_export_test.go +46 -0
- package/cmd/ttscgraph/serve_session_reloads_newly_resolved_package_import_test.go +46 -0
- package/cmd/ttscgraph/serve_session_reloads_newly_resolved_path_alias_test.go +47 -0
- package/cmd/ttscgraph/serve_session_reloads_newly_resolved_reference_path_test.go +42 -0
- package/cmd/ttscgraph/serve_session_reloads_newly_resolved_root_dirs_target_test.go +46 -0
- package/cmd/ttscgraph/serve_session_reloads_newly_resolved_type_reference_test.go +42 -0
- package/cmd/ttscgraph/serve_session_reloads_root_file_set_test.go +52 -0
- package/cmd/ttscgraph/serve_session_reloads_source_deleted_during_load_test.go +60 -0
- package/cmd/ttscgraph/serve_session_reloads_transitive_project_reference_test.go +55 -0
- package/cmd/ttscgraph/serve_session_reloads_when_program_plugin_linked_test.go +53 -0
- package/cmd/ttscgraph/serve_session_reuses_unchanged_preamble_source_test.go +45 -0
- package/cmd/ttscgraph/serve_session_reuses_unchanged_snapshot_test.go +52 -0
- package/cmd/ttscgraph/serve_session_revisits_source_edited_during_load_test.go +61 -0
- package/cmd/ttscgraph/serve_session_tolerates_os_invalid_resolution_candidate_test.go +56 -0
- package/cmd/ttscgraph/serve_session_tracks_all_module_specifier_forms_test.go +41 -0
- package/cmd/ttscgraph/serve_session_tracks_diamond_project_references_once_test.go +61 -0
- package/cmd/ttscserver/main.go +5 -3
- package/driver/lsp.go +13 -1
- package/driver/plugins.go +56 -5
- package/driver/program.go +25 -10
- package/driver/rewrite.go +381 -21
- package/driver/session.go +11 -3
- package/go.mod +1 -0
- package/internal/graph/build.go +297 -3
- package/internal/graph/build_records_a_node_per_top_level_declaration_test.go +16 -2
- package/internal/graph/diagnostics.go +5 -4
- package/internal/graph/dump.go +60 -4
- package/internal/graph/dump_is_a_function_of_the_source_test.go +71 -0
- package/internal/graph/dump_serializes_the_full_graph_test.go +16 -4
- package/internal/graph/edges.go +122 -11
- package/internal/graph/exports.go +74 -17
- package/internal/graph/graph.go +33 -1
- package/internal/graph/node_identity_is_position_invariant_test.go +4 -2
- package/internal/graph/resolve.go +24 -0
- package/internal/graphsymbols/provider.go +50 -14
- package/internal/lspserver/lsp_native_plugin_source.go +77 -23
- package/internal/lspserver/lsp_plugin.go +19 -2
- package/internal/lspserver/lsp_proxy.go +244 -52
- package/internal/lspserver/lsp_server.go +35 -27
- package/internal/lspserver/lsp_symbols.go +25 -16
- package/lib/TtscCompiler.d.ts +13 -2
- package/lib/TtscCompiler.js +30 -20
- package/lib/TtscCompiler.js.map +1 -1
- package/lib/TtscService.js +9 -7
- package/lib/TtscService.js.map +1 -1
- package/lib/compiler/internal/compileProjectInMemory.js +15 -1
- package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
- package/lib/compiler/internal/project/createNativeProjectContextArgs.d.ts +3 -0
- package/lib/compiler/internal/project/createNativeProjectContextArgs.js +14 -0
- package/lib/compiler/internal/project/createNativeProjectContextArgs.js.map +1 -0
- package/lib/compiler/internal/project/readProjectConfig.js +53 -4
- package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
- package/lib/compiler/internal/project/resolveProjectConfig.d.ts +9 -2
- package/lib/compiler/internal/project/resolveProjectConfig.js +45 -16
- package/lib/compiler/internal/project/resolveProjectConfig.js.map +1 -1
- package/lib/compiler/internal/residentTransformProcess.d.ts +16 -5
- package/lib/compiler/internal/residentTransformProcess.js +68 -20
- package/lib/compiler/internal/residentTransformProcess.js.map +1 -1
- package/lib/compiler/internal/runBuild.d.ts +18 -0
- package/lib/compiler/internal/runBuild.js +220 -24
- package/lib/compiler/internal/runBuild.js.map +1 -1
- package/lib/compiler/internal/runSingleFileEmit.js +7 -4
- package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
- package/lib/compiler/internal/sharedHostHelpers.d.ts +16 -0
- package/lib/compiler/internal/sharedHostHelpers.js +23 -0
- package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
- package/lib/compiler/internal/startResidentTransform.js +18 -1
- package/lib/compiler/internal/startResidentTransform.js.map +1 -1
- package/lib/compiler/internal/transformProjectInMemory.js +34 -8
- package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
- package/lib/flags/parser.d.ts +17 -0
- package/lib/flags/parser.js +11 -1
- package/lib/flags/parser.js.map +1 -1
- package/lib/flags/schema.js +7 -0
- package/lib/flags/schema.js.map +1 -1
- package/lib/launcher/internal/prepareExecution.js +21 -2
- package/lib/launcher/internal/prepareExecution.js.map +1 -1
- package/lib/launcher/internal/runTtsc.js +13 -53
- package/lib/launcher/internal/runTtsc.js.map +1 -1
- package/lib/launcher/internal/runTtscserver.js +11 -0
- package/lib/launcher/internal/runTtscserver.js.map +1 -1
- package/lib/launcher/internal/runTtsx.js +27 -11
- package/lib/launcher/internal/runTtsx.js.map +1 -1
- package/lib/launcher/internal/runtimeHooks.d.ts +81 -0
- package/lib/launcher/internal/runtimeHooks.js +632 -43
- package/lib/launcher/internal/runtimeHooks.js.map +1 -1
- package/lib/launcher/internal/servedSourceMap.d.ts +25 -0
- package/lib/launcher/internal/servedSourceMap.js +164 -0
- package/lib/launcher/internal/servedSourceMap.js.map +1 -0
- package/lib/plugin/internal/buildSourcePlugin.d.ts +146 -2
- package/lib/plugin/internal/buildSourcePlugin.js +651 -113
- package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
- package/lib/plugin/internal/loadProjectPlugins.d.ts +7 -0
- package/lib/plugin/internal/loadProjectPlugins.js +33 -10
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/lib/structures/ITtscCompilerContext.d.ts +16 -2
- package/lib/structures/ITtscCompilerResult.d.ts +8 -5
- package/lib/structures/ITtscPluginCapabilities.d.ts +8 -0
- package/lib/structures/ITtscPluginFactoryContext.d.ts +14 -0
- package/lib/structures/internal/ITtscParsedProjectConfig.d.ts +3 -0
- package/lib/structures/internal/ITtscProjectIdentity.d.ts +17 -0
- package/lib/structures/internal/ITtscProjectIdentity.js +3 -0
- package/lib/structures/internal/ITtscProjectIdentity.js.map +1 -0
- package/lib/structures/internal/TtscCommonOptions.d.ts +8 -0
- package/package.json +11 -8
- package/shim/ast/lint.go +10 -0
- package/shim/ast/shim.go +44 -0
- package/shim/checker/enums_gen.go +19 -0
- package/shim/checker/shim.go +130 -0
- package/shim/diagnosticwriter/lint.go +30 -3
- package/shim/printer/shim.go +22 -4
- package/shim/scanner/regexp.go +27 -0
- package/src/TtscCompiler.ts +29 -22
- package/src/TtscService.ts +15 -7
- package/src/compiler/internal/compileProjectInMemory.ts +18 -1
- package/src/compiler/internal/project/createNativeProjectContextArgs.ts +15 -0
- package/src/compiler/internal/project/readProjectConfig.ts +61 -5
- package/src/compiler/internal/project/resolveProjectConfig.ts +51 -21
- package/src/compiler/internal/residentTransformProcess.ts +95 -21
- package/src/compiler/internal/runBuild.ts +313 -32
- package/src/compiler/internal/runSingleFileEmit.ts +7 -4
- package/src/compiler/internal/sharedHostHelpers.ts +24 -0
- package/src/compiler/internal/startResidentTransform.ts +21 -1
- package/src/compiler/internal/transformProjectInMemory.ts +49 -6
- package/src/flags/parser.ts +28 -1
- package/src/flags/schema.ts +8 -0
- package/src/launcher/internal/prepareExecution.ts +22 -2
- package/src/launcher/internal/runTtsc.ts +13 -61
- package/src/launcher/internal/runTtscserver.ts +14 -0
- package/src/launcher/internal/runTtsx.ts +28 -13
- package/src/launcher/internal/runtimeHooks.ts +771 -53
- package/src/launcher/internal/servedSourceMap.ts +183 -0
- package/src/plugin/internal/buildSourcePlugin.ts +859 -122
- package/src/plugin/internal/loadProjectPlugins.ts +36 -6
- package/src/structures/ITtscCompilerContext.ts +17 -2
- package/src/structures/ITtscCompilerResult.ts +8 -5
- package/src/structures/ITtscPluginCapabilities.ts +9 -0
- package/src/structures/ITtscPluginFactoryContext.ts +15 -0
- package/src/structures/internal/ITtscParsedProjectConfig.ts +3 -0
- package/src/structures/internal/ITtscProjectIdentity.ts +17 -0
- package/src/structures/internal/TtscCommonOptions.ts +8 -0
- package/tools/shim_audit/baseline.json +16 -7
- package/tools/shim_audit/main.go +777 -71
- package/tools/shim_audit/main_test.go +28 -1
- package/tools/shim_audit/producer_exemptions_require_rationale_test.go +42 -0
- package/tools/shim_audit/producer_surface_includes_exposed_methods_test.go +154 -0
- package/tools/shim_audit/producer_surface_rejects_unproducible_reference_test.go +142 -0
- package/utility/host.go +46 -20
- package/utility/serve.go +1 -1
- package/shim/checker/signature_api_completeness.go +0 -58
package/src/TtscService.ts
CHANGED
|
@@ -58,9 +58,14 @@ export class TtscService {
|
|
|
58
58
|
* carries the host's diagnostics so callers can surface a real build error.
|
|
59
59
|
*/
|
|
60
60
|
public async transformFile(fileName: string): Promise<string | undefined> {
|
|
61
|
-
const reply = await this.resident.request(
|
|
62
|
-
file: this.absolutePath(fileName),
|
|
63
|
-
|
|
61
|
+
const reply = await this.resident.request(
|
|
62
|
+
{ file: this.absolutePath(fileName) },
|
|
63
|
+
"transform",
|
|
64
|
+
);
|
|
65
|
+
// The resident client already validated the reply shape (boolean `found`,
|
|
66
|
+
// string `typescript` when found), so a malformed or wrong-shape reply
|
|
67
|
+
// rejected instead of reaching here. `found: false` is the only path to
|
|
68
|
+
// `undefined`; a protocol failure never masquerades as an absent file.
|
|
64
69
|
return reply.found === true && typeof reply.typescript === "string"
|
|
65
70
|
? reply.typescript
|
|
66
71
|
: undefined;
|
|
@@ -74,10 +79,13 @@ export class TtscService {
|
|
|
74
79
|
* `fileName` is resolved against the project root.
|
|
75
80
|
*/
|
|
76
81
|
public async updateFile(fileName: string, content: string): Promise<boolean> {
|
|
77
|
-
const reply = await this.resident.request(
|
|
78
|
-
content,
|
|
79
|
-
update
|
|
80
|
-
|
|
82
|
+
const reply = await this.resident.request(
|
|
83
|
+
{ content, update: this.absolutePath(fileName) },
|
|
84
|
+
"update",
|
|
85
|
+
);
|
|
86
|
+
// The resident client validated the reply carries a boolean `updated`, so a
|
|
87
|
+
// malformed or wrong-shape reply rejected instead of collapsing to `false`.
|
|
88
|
+
// `false` here means only that the edit did not compile.
|
|
81
89
|
return reply.updated === true;
|
|
82
90
|
}
|
|
83
91
|
|
|
@@ -36,7 +36,7 @@ export function compileProjectInMemory(options: ITtscCompilerContext): {
|
|
|
36
36
|
projectRoot: options.projectRoot,
|
|
37
37
|
tsconfig: options.tsconfig,
|
|
38
38
|
});
|
|
39
|
-
if (
|
|
39
|
+
if (shouldUsePluginBuild(options, project)) {
|
|
40
40
|
return compileProjectWithPlugins(options, cwd, project);
|
|
41
41
|
}
|
|
42
42
|
const tsconfig = project.path;
|
|
@@ -82,6 +82,22 @@ function hasConfiguredPlugins(
|
|
|
82
82
|
return hasProjectPluginEntries(project, options.plugins);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Route plugin discovery failures through runBuild's recoverable setup path.
|
|
87
|
+
* The fast native-host lane cannot surface the plugin error or run the
|
|
88
|
+
* post-failure TypeScript check, while the plugin-backed lane can do both.
|
|
89
|
+
*/
|
|
90
|
+
function shouldUsePluginBuild(
|
|
91
|
+
options: ITtscCompilerContext,
|
|
92
|
+
project: ITtscParsedProjectConfig,
|
|
93
|
+
): boolean {
|
|
94
|
+
try {
|
|
95
|
+
return hasConfiguredPlugins(options, project);
|
|
96
|
+
} catch {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
85
101
|
/**
|
|
86
102
|
* Plugin-backed compilation: emit into a temp directory via `runBuild`, then
|
|
87
103
|
* read back every file the build wrote so they can be returned as strings.
|
|
@@ -104,6 +120,7 @@ function compileProjectWithPlugins(
|
|
|
104
120
|
forceListEmittedFiles: true,
|
|
105
121
|
outDir: tempOutDir,
|
|
106
122
|
quiet: true,
|
|
123
|
+
resolvedProject: project,
|
|
107
124
|
structuredDiagnostics: true,
|
|
108
125
|
tsconfig: project.path,
|
|
109
126
|
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ITtscParsedProjectConfig } from "../../../structures/internal/ITtscParsedProjectConfig";
|
|
2
|
+
|
|
3
|
+
/** Serialize the retained project identity for a native sidecar invocation. */
|
|
4
|
+
export function createNativeProjectContextArgs(
|
|
5
|
+
project: ITtscParsedProjectConfig,
|
|
6
|
+
pluginConfigOrigin?: string,
|
|
7
|
+
): string[] {
|
|
8
|
+
return [
|
|
9
|
+
"--project-context-json=" +
|
|
10
|
+
JSON.stringify({
|
|
11
|
+
...project.identity,
|
|
12
|
+
...(pluginConfigOrigin === undefined ? {} : { pluginConfigOrigin }),
|
|
13
|
+
}),
|
|
14
|
+
];
|
|
15
|
+
}
|
|
@@ -5,7 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import type { ITtscProjectPluginConfig } from "../../../structures/ITtscProjectPluginConfig";
|
|
6
6
|
import type { ITtscParsedProjectConfig } from "../../../structures/internal/ITtscParsedProjectConfig";
|
|
7
7
|
import type { ITtscProjectLocatorOptions } from "../../../structures/internal/ITtscProjectLocatorOptions";
|
|
8
|
-
import {
|
|
8
|
+
import { resolveProjectIdentity } from "./resolveProjectConfig";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Compiler option keys whose values are file-system paths that must be resolved
|
|
@@ -40,10 +40,9 @@ type ResolvedCompilerOptions = {
|
|
|
40
40
|
export function readProjectConfig(
|
|
41
41
|
opts: ITtscProjectLocatorOptions = {},
|
|
42
42
|
): ITtscParsedProjectConfig {
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
: path.dirname(tsconfig);
|
|
43
|
+
const identity = resolveProjectIdentity(opts);
|
|
44
|
+
const tsconfig = identity.physicalConfigPath;
|
|
45
|
+
const root = identity.physicalProjectRoot;
|
|
47
46
|
const compilerOptions = readResolvedCompilerOptions(tsconfig);
|
|
48
47
|
return {
|
|
49
48
|
compilerOptions: {
|
|
@@ -51,6 +50,7 @@ export function readProjectConfig(
|
|
|
51
50
|
outDir: compilerOptions.outDir,
|
|
52
51
|
plugins: compilerOptions.plugins,
|
|
53
52
|
},
|
|
53
|
+
identity,
|
|
54
54
|
path: tsconfig,
|
|
55
55
|
pluginBaseDirs: compilerOptions.pluginBaseDirs,
|
|
56
56
|
root,
|
|
@@ -254,6 +254,14 @@ function resolveExtendsConfig(tsconfig: string, specifier: string): string {
|
|
|
254
254
|
return resolveExistingExtendsPath(path.resolve(baseDir, specifier));
|
|
255
255
|
}
|
|
256
256
|
const resolver = createRequire(tsconfig);
|
|
257
|
+
// A bare package root selects its preset through `package.json#tsconfig`,
|
|
258
|
+
// matching TypeScript's config resolution. Presets shipped this way often
|
|
259
|
+
// have no JavaScript/JSON entrypoint at all, so Node's entrypoint resolver
|
|
260
|
+
// and the `<specifier>.json` fallback below both miss them.
|
|
261
|
+
const viaManifest = resolvePackageManifestTsconfig(resolver, specifier);
|
|
262
|
+
if (viaManifest !== undefined) {
|
|
263
|
+
return resolveExistingExtendsPath(viaManifest);
|
|
264
|
+
}
|
|
257
265
|
try {
|
|
258
266
|
return resolveRealPath(resolver.resolve(specifier));
|
|
259
267
|
} catch {
|
|
@@ -261,6 +269,54 @@ function resolveExtendsConfig(tsconfig: string, specifier: string): string {
|
|
|
261
269
|
}
|
|
262
270
|
}
|
|
263
271
|
|
|
272
|
+
/**
|
|
273
|
+
* When `specifier` names a bare package root, resolve the config file its
|
|
274
|
+
* `package.json#tsconfig` field selects (anchored at the package directory).
|
|
275
|
+
* Returns `undefined` when the specifier is a subpath, the manifest is
|
|
276
|
+
* unresolvable/unparsable, or it declares no `tsconfig` field, so the caller
|
|
277
|
+
* falls back to Node entrypoint resolution.
|
|
278
|
+
*/
|
|
279
|
+
function resolvePackageManifestTsconfig(
|
|
280
|
+
resolver: NodeRequire,
|
|
281
|
+
specifier: string,
|
|
282
|
+
): string | undefined {
|
|
283
|
+
if (!isBarePackageRoot(specifier)) {
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
let manifestPath: string;
|
|
287
|
+
try {
|
|
288
|
+
manifestPath = resolver.resolve(`${specifier}/package.json`);
|
|
289
|
+
} catch {
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
let field: unknown;
|
|
293
|
+
try {
|
|
294
|
+
field = (
|
|
295
|
+
JSON.parse(stripLeadingBom(fs.readFileSync(manifestPath, "utf8"))) as {
|
|
296
|
+
tsconfig?: unknown;
|
|
297
|
+
}
|
|
298
|
+
).tsconfig;
|
|
299
|
+
} catch {
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
if (typeof field !== "string" || field.length === 0) {
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
return path.resolve(path.dirname(manifestPath), field);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Return true when `specifier` is a bare package root (no subpath): a plain
|
|
310
|
+
* package name (`preset`) or a scoped name (`@scope/preset`). Subpaths such as
|
|
311
|
+
* `@scope/preset/base.json` resolve directly and keep their current meaning.
|
|
312
|
+
*/
|
|
313
|
+
function isBarePackageRoot(specifier: string): boolean {
|
|
314
|
+
if (specifier.startsWith("@")) {
|
|
315
|
+
return specifier.split("/").length === 2;
|
|
316
|
+
}
|
|
317
|
+
return !specifier.includes("/");
|
|
318
|
+
}
|
|
319
|
+
|
|
264
320
|
/**
|
|
265
321
|
* Given an on-disk location, try the path as-is, with `.json` appended, and as
|
|
266
322
|
* a directory containing `tsconfig.json`. Returns the first match or throws.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
+
import type { ITtscProjectIdentity } from "../../../structures/internal/ITtscProjectIdentity";
|
|
4
5
|
import type { ITtscProjectLocatorOptions } from "../../../structures/internal/ITtscProjectLocatorOptions";
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -13,13 +14,29 @@ import type { ITtscProjectLocatorOptions } from "../../../structures/internal/IT
|
|
|
13
14
|
* walking up from the file's directory.
|
|
14
15
|
* 3. `opts.cwd` — the nearest ancestor config walking up from cwd.
|
|
15
16
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
17
|
+
* Returns the real (symlink-resolved) absolute config path. Use
|
|
18
|
+
* {@link resolveProjectIdentity} when both the caller-selected spelling and the
|
|
19
|
+
* Program identity are required.
|
|
18
20
|
*/
|
|
19
21
|
export function resolveProjectConfig(
|
|
20
22
|
opts: ITtscProjectLocatorOptions = {},
|
|
21
23
|
): string {
|
|
24
|
+
return resolveProjectIdentity(opts).physicalConfigPath;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Resolve the selected config while retaining its lexical spelling separately
|
|
29
|
+
* from the physical paths used by the TypeScript Program.
|
|
30
|
+
*/
|
|
31
|
+
export function resolveProjectIdentity(
|
|
32
|
+
opts: ITtscProjectLocatorOptions = {},
|
|
33
|
+
): Omit<ITtscProjectIdentity, "pluginConfigOrigin"> {
|
|
22
34
|
const cwd = path.resolve(opts.cwd ?? process.cwd());
|
|
35
|
+
const explicitProjectRoot =
|
|
36
|
+
opts.projectRoot === undefined || opts.projectRoot === ""
|
|
37
|
+
? undefined
|
|
38
|
+
: resolveAbsolutePath(cwd, opts.projectRoot);
|
|
39
|
+
let logicalConfigPath: string;
|
|
23
40
|
if (opts.tsconfig) {
|
|
24
41
|
const resolved = resolveAbsolutePath(cwd, opts.tsconfig);
|
|
25
42
|
if (!fs.existsSync(resolved)) {
|
|
@@ -35,30 +52,43 @@ export function resolveProjectConfig(
|
|
|
35
52
|
if (isDirectory(resolved)) {
|
|
36
53
|
const tsconfigInDir = path.join(resolved, "tsconfig.json");
|
|
37
54
|
if (fs.existsSync(tsconfigInDir)) {
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
55
|
+
logicalConfigPath = tsconfigInDir;
|
|
56
|
+
} else {
|
|
57
|
+
const jsconfigInDir = path.join(resolved, "jsconfig.json");
|
|
58
|
+
if (fs.existsSync(jsconfigInDir)) {
|
|
59
|
+
logicalConfigPath = jsconfigInDir;
|
|
60
|
+
} else {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`ttsc: directory has no tsconfig.json / jsconfig.json: ${resolved}`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
43
65
|
}
|
|
66
|
+
} else {
|
|
67
|
+
logicalConfigPath = resolved;
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
const start = opts.file ? resolveAbsolutePath(cwd, opts.file) : cwd;
|
|
71
|
+
const from = isDirectory(start) ? start : path.dirname(start);
|
|
72
|
+
const found = findUp(from, ["tsconfig.json", "jsconfig.json"]);
|
|
73
|
+
if (!found) {
|
|
44
74
|
throw new Error(
|
|
45
|
-
`ttsc:
|
|
75
|
+
`ttsc: could not find tsconfig.json or jsconfig.json starting from ${from}`,
|
|
46
76
|
);
|
|
47
77
|
}
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const start = opts.file
|
|
52
|
-
? resolveRealPath(resolveAbsolutePath(cwd, opts.file))
|
|
53
|
-
: cwd;
|
|
54
|
-
const from = isDirectory(start) ? start : path.dirname(start);
|
|
55
|
-
const found = findUp(from, ["tsconfig.json", "jsconfig.json"]);
|
|
56
|
-
if (!found) {
|
|
57
|
-
throw new Error(
|
|
58
|
-
`ttsc: could not find tsconfig.json or jsconfig.json starting from ${from}`,
|
|
59
|
-
);
|
|
78
|
+
logicalConfigPath = found;
|
|
60
79
|
}
|
|
61
|
-
|
|
80
|
+
const physicalConfigPath = resolveRealPath(logicalConfigPath);
|
|
81
|
+
const physicalProjectRoot = resolveRealPath(
|
|
82
|
+
explicitProjectRoot ?? path.dirname(physicalConfigPath),
|
|
83
|
+
);
|
|
84
|
+
return {
|
|
85
|
+
...(explicitProjectRoot === undefined ? {} : { explicitProjectRoot }),
|
|
86
|
+
invocationCwd: cwd,
|
|
87
|
+
logicalConfigPath,
|
|
88
|
+
logicalProjectRoot: path.dirname(logicalConfigPath),
|
|
89
|
+
physicalConfigPath,
|
|
90
|
+
physicalProjectRoot,
|
|
91
|
+
};
|
|
62
92
|
}
|
|
63
93
|
|
|
64
94
|
/** Resolve `target` against `cwd` when it is not already absolute. */
|
|
@@ -4,6 +4,19 @@ import { type Interface, createInterface } from "node:readline";
|
|
|
4
4
|
/** Cap on retained stderr so a long-lived host cannot grow it without bound. */
|
|
5
5
|
const STDERR_TAIL_LIMIT = 64 * 1024;
|
|
6
6
|
|
|
7
|
+
/** Cap on how much of an offending line an error message echoes back. */
|
|
8
|
+
const REPLY_ECHO_LIMIT = 200;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The operation a request expects a reply for. The host answers a transform
|
|
12
|
+
* request (`{"file":...}`) with `{"typescript":...,"found":...}` and an update
|
|
13
|
+
* request (`{"update":...,"content":...}`) with `{"updated":...}`. The client
|
|
14
|
+
* knows which it sent, so it validates the reply's shape against the operation
|
|
15
|
+
* before handing it back — a well-formed JSON object of the wrong operation
|
|
16
|
+
* shape is a protocol error, not a valid negative result.
|
|
17
|
+
*/
|
|
18
|
+
export type ResidentReplyKind = "transform" | "update";
|
|
19
|
+
|
|
7
20
|
/** Options for spawning the resident transform host. */
|
|
8
21
|
export interface ResidentTransformProcessOptions {
|
|
9
22
|
args: readonly string[];
|
|
@@ -13,6 +26,7 @@ export interface ResidentTransformProcessOptions {
|
|
|
13
26
|
}
|
|
14
27
|
|
|
15
28
|
interface PendingRequest {
|
|
29
|
+
kind: ResidentReplyKind;
|
|
16
30
|
reject: (reason: Error) => void;
|
|
17
31
|
resolve: (reply: Record<string, unknown>) => void;
|
|
18
32
|
}
|
|
@@ -78,13 +92,16 @@ export class ResidentTransformProcess {
|
|
|
78
92
|
}
|
|
79
93
|
|
|
80
94
|
/**
|
|
81
|
-
* Send one request to the host and resolve with its
|
|
82
|
-
* host answers in FIFO order
|
|
83
|
-
* payload
|
|
84
|
-
*
|
|
95
|
+
* Send one request to the host and resolve with its validated JSON reply. The
|
|
96
|
+
* host answers in FIFO order; `kind` tells the client which reply shape the
|
|
97
|
+
* payload asks for, so the reply is validated as a well-formed `kind` reply
|
|
98
|
+
* before it resolves. Rejects when the reply is not a valid `kind` reply,
|
|
99
|
+
* when the host has already failed or exited, or if writing the request
|
|
100
|
+
* fails.
|
|
85
101
|
*/
|
|
86
102
|
public request(
|
|
87
103
|
payload: Record<string, unknown>,
|
|
104
|
+
kind: ResidentReplyKind,
|
|
88
105
|
): Promise<Record<string, unknown>> {
|
|
89
106
|
if (this.failure !== undefined) {
|
|
90
107
|
return Promise.reject(this.failure);
|
|
@@ -96,7 +113,7 @@ export class ResidentTransformProcess {
|
|
|
96
113
|
);
|
|
97
114
|
}
|
|
98
115
|
return new Promise<Record<string, unknown>>((resolve, reject) => {
|
|
99
|
-
const pending: PendingRequest = { reject, resolve };
|
|
116
|
+
const pending: PendingRequest = { kind, reject, resolve };
|
|
100
117
|
this.pending.push(pending);
|
|
101
118
|
stdin.write(`${JSON.stringify(payload)}\n`, (error) => {
|
|
102
119
|
if (error) {
|
|
@@ -149,7 +166,39 @@ export class ResidentTransformProcess {
|
|
|
149
166
|
}
|
|
150
167
|
return;
|
|
151
168
|
}
|
|
152
|
-
|
|
169
|
+
const reply = parseReplyObject(trimmed);
|
|
170
|
+
if (reply === undefined) {
|
|
171
|
+
// Framing violation: the line is not a JSON object, so it cannot
|
|
172
|
+
// represent any reply. This request already left the FIFO, so reject it,
|
|
173
|
+
// then fail the whole process: a bad line may be corruption that shifted
|
|
174
|
+
// the stream, and the host's real reply that follows must not be paired
|
|
175
|
+
// with a later request. `fail` marks the failure so that trailing line is
|
|
176
|
+
// treated as benign instead of unsolicited.
|
|
177
|
+
const error = new Error(
|
|
178
|
+
`ttsc: resident transform host sent a malformed reply: ${echoLine(
|
|
179
|
+
trimmed,
|
|
180
|
+
)}`,
|
|
181
|
+
);
|
|
182
|
+
request.reject(error);
|
|
183
|
+
this.fail(error);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (!isValidReply(reply, request.kind)) {
|
|
187
|
+
// Operation-shape violation: a well-formed JSON object that is not a valid
|
|
188
|
+
// reply for the operation this request sent. FIFO framing is intact — one
|
|
189
|
+
// line consumed exactly one slot — so only this request is corrupt; later
|
|
190
|
+
// replies still pair correctly. Reject just this request instead of
|
|
191
|
+
// failing the whole process.
|
|
192
|
+
request.reject(
|
|
193
|
+
new Error(
|
|
194
|
+
`ttsc: resident transform host sent an invalid ${request.kind} reply: ${echoLine(
|
|
195
|
+
trimmed,
|
|
196
|
+
)}`,
|
|
197
|
+
),
|
|
198
|
+
);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
request.resolve(reply);
|
|
153
202
|
}
|
|
154
203
|
|
|
155
204
|
private onReaderClose(): void {
|
|
@@ -194,23 +243,48 @@ export class ResidentTransformProcess {
|
|
|
194
243
|
}
|
|
195
244
|
|
|
196
245
|
/**
|
|
197
|
-
* Parse one reply line into a plain object
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
246
|
+
* Parse one reply line into a plain object, or `undefined` when the line is not
|
|
247
|
+
* a JSON object. Invalid JSON, arrays, primitives, and `null` all yield
|
|
248
|
+
* `undefined`: none of them can carry a reply's fields, so the caller treats
|
|
249
|
+
* them as a framing failure rather than an empty reply. Returning `{}` here
|
|
250
|
+
* would let a corrupt line masquerade as a valid negative result.
|
|
201
251
|
*/
|
|
202
|
-
function
|
|
252
|
+
function parseReplyObject(line: string): Record<string, unknown> | undefined {
|
|
253
|
+
let parsed: unknown;
|
|
203
254
|
try {
|
|
204
|
-
|
|
205
|
-
if (
|
|
206
|
-
typeof parsed === "object" &&
|
|
207
|
-
parsed !== null &&
|
|
208
|
-
!Array.isArray(parsed)
|
|
209
|
-
) {
|
|
210
|
-
return parsed as Record<string, unknown>;
|
|
211
|
-
}
|
|
255
|
+
parsed = JSON.parse(line);
|
|
212
256
|
} catch {
|
|
213
|
-
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
260
|
+
return parsed as Record<string, unknown>;
|
|
261
|
+
}
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Whether a parsed reply object is a well-formed reply for the given operation.
|
|
267
|
+
* A transform reply must carry a boolean `found`, and when `found` is `true` a
|
|
268
|
+
* string `typescript` (a found file always carries its transformed text); when
|
|
269
|
+
* `found` is `false` the text is irrelevant. An update reply must carry a
|
|
270
|
+
* boolean `updated`. Every other object shape is a protocol error.
|
|
271
|
+
*/
|
|
272
|
+
function isValidReply(
|
|
273
|
+
reply: Record<string, unknown>,
|
|
274
|
+
kind: ResidentReplyKind,
|
|
275
|
+
): boolean {
|
|
276
|
+
if (kind === "transform") {
|
|
277
|
+
if (typeof reply.found !== "boolean") {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
return reply.found ? typeof reply.typescript === "string" : true;
|
|
214
281
|
}
|
|
215
|
-
return
|
|
282
|
+
return typeof reply.updated === "boolean";
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Truncate an offending reply line so error messages stay bounded. */
|
|
286
|
+
function echoLine(line: string): string {
|
|
287
|
+
return line.length > REPLY_ECHO_LIMIT
|
|
288
|
+
? `${line.slice(0, REPLY_ECHO_LIMIT)}…`
|
|
289
|
+
: line;
|
|
216
290
|
}
|