ttsc 0.16.2 → 0.16.5
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 +96 -25
- package/cmd/graphbench/main.go +135 -0
- package/cmd/graphdump/main.go +60 -0
- package/cmd/ttscgraph/dump.go +69 -0
- package/cmd/ttscgraph/main.go +91 -0
- package/cmd/ttscgraph/run_dump_prints_the_graph_as_json_test.go +80 -0
- package/cmd/ttscgraph/run_help_version_and_bad_flags_exit_codes_test.go +75 -0
- package/cmd/utility-host/main.go +4 -1
- package/driver/overlay.go +64 -0
- package/driver/program.go +8 -1
- package/driver/session.go +85 -0
- package/internal/graph/build.go +330 -0
- package/internal/graph/build_records_a_node_per_top_level_declaration_test.go +96 -0
- package/internal/graph/decorators.go +122 -0
- package/internal/graph/decorators_capture_name_and_literal_arguments_test.go +98 -0
- package/internal/graph/diagnostics.go +23 -0
- package/internal/graph/dump.go +390 -0
- package/internal/graph/dump_evidence_skips_leading_trivia_test.go +92 -0
- package/internal/graph/dump_serializes_the_full_graph_test.go +146 -0
- package/internal/graph/edge_spans_point_to_resolved_usage_test.go +90 -0
- package/internal/graph/edges.go +538 -0
- package/internal/graph/edges_cover_class_level_references_test.go +93 -0
- package/internal/graph/enum_initializer_calls_are_edges_test.go +63 -0
- package/internal/graph/exports.go +60 -0
- package/internal/graph/file_diagnostics_match_tsgo_code_and_location_test.go +59 -0
- package/internal/graph/file_diagnostics_report_code_column_and_message_test.go +69 -0
- package/internal/graph/function_locals_do_not_collide_test.go +66 -0
- package/internal/graph/gitignore.go +51 -0
- package/internal/graph/graph.go +119 -0
- package/internal/graph/heritage_edges_cover_implements_and_interface_extends_test.go +76 -0
- package/internal/graph/heritage_edges_keep_extends_and_implements_to_same_base_test.go +61 -0
- package/internal/graph/heritage_edges_record_extends_versus_implements_origin_test.go +54 -0
- package/internal/graph/heritage_edges_traverse_barrels_and_stop_at_external_leaves_test.go +112 -0
- package/internal/graph/namespace_dotted_form_qualifies_deeply_test.go +60 -0
- package/internal/graph/namespace_members_become_qualified_nodes_and_edges_test.go +100 -0
- package/internal/graph/node_identity_is_position_invariant_test.go +84 -0
- package/internal/graph/node_modifiers_emit_union_strings_test.go +118 -0
- package/internal/graph/nodes_are_marked_exported_through_the_export_table_test.go +61 -0
- package/internal/graph/overload_implementation_replaces_signature_test.go +54 -0
- package/internal/graph/property_member_edges_remain_owner_visible_test.go +83 -0
- package/internal/graph/resolve.go +102 -0
- package/internal/graph/resolve_prefers_implementation_declaration_test.go +50 -0
- package/internal/graph/resolve_unwraps_barrel_reexport_to_real_declaration_test.go +230 -0
- package/internal/graph/type_ref_edges_cover_typeof_as_satisfies_test.go +79 -0
- package/internal/graph/type_ref_edges_resolve_named_types_across_files_test.go +52 -0
- package/internal/graph/value_access_edges_cover_properties_test.go +96 -0
- package/internal/graph/value_call_edges_cover_jsx_components_test.go +82 -0
- package/internal/graph/value_call_edges_cover_methods_and_constructors_test.go +70 -0
- package/internal/graph/value_call_edges_cover_variable_bound_callables_test.go +46 -0
- package/internal/graph/value_call_edges_do_not_double_record_invoked_member_access_test.go +65 -0
- package/internal/graph/value_call_edges_record_construct_versus_call_origin_test.go +75 -0
- package/internal/graph/value_call_edges_resolve_across_files_and_dedupe_test.go +55 -0
- package/internal/graph/value_call_edges_skip_self_and_unresolved_test.go +74 -0
- package/lib/TtscService.d.ts +55 -0
- package/lib/TtscService.js +93 -0
- package/lib/TtscService.js.map +1 -0
- package/lib/compiler/internal/residentTransformProcess.d.ts +47 -0
- package/lib/compiler/internal/residentTransformProcess.js +187 -0
- package/lib/compiler/internal/residentTransformProcess.js.map +1 -0
- package/lib/compiler/internal/startResidentTransform.d.ts +26 -0
- package/lib/compiler/internal/startResidentTransform.js +98 -0
- package/lib/compiler/internal/startResidentTransform.js.map +1 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/plugin/internal/loadProjectPlugins.js +17 -3
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/lib/structures/ITtscPlugin.d.ts +7 -5
- package/lib/structures/ITtscPluginFactoryContext.d.ts +24 -1
- package/package.json +8 -8
- package/shim/ast/shim.go +6 -0
- package/shim/checker/shim.go +22 -12
- package/src/TtscService.ts +94 -0
- package/src/compiler/internal/residentTransformProcess.ts +216 -0
- package/src/compiler/internal/startResidentTransform.ts +126 -0
- package/src/index.ts +1 -0
- package/src/plugin/internal/loadProjectPlugins.ts +17 -3
- package/src/structures/ITtscPlugin.ts +7 -5
- package/src/structures/ITtscPluginFactoryContext.ts +26 -1
- package/utility/host.go +8 -2
- package/utility/serve.go +158 -0
package/shim/ast/shim.go
CHANGED
|
@@ -189,6 +189,12 @@ func NewNodeVisitor(visit func(node *Node) *Node, factory *NodeFactory, options
|
|
|
189
189
|
return innerast.NewNodeVisitor(visit, factory, options)
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
// GetCombinedModifierFlags returns syntactic modifiers combined across wrappers
|
|
193
|
+
// such as variable statements and their declaration lists.
|
|
194
|
+
func GetCombinedModifierFlags(node *Node) ModifierFlags {
|
|
195
|
+
return innerast.GetCombinedModifierFlags(node)
|
|
196
|
+
}
|
|
197
|
+
|
|
192
198
|
// IsFunctionLike reports whether node is any function-like construct
|
|
193
199
|
// (function declaration, arrow function, method, constructor, accessor, etc.).
|
|
194
200
|
func IsFunctionLike(node *Node) bool {
|
package/shim/checker/shim.go
CHANGED
|
@@ -81,7 +81,7 @@ func Checker_getPropertiesOfType(recv *innerchecker.Checker, t *innerchecker.Typ
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
// Checker_getApparentProperties returns the properties visible on t after
|
|
84
|
-
// resolving primitive wrapper types (e.g. string
|
|
84
|
+
// resolving primitive wrapper types (e.g. string to String).
|
|
85
85
|
func Checker_getApparentProperties(recv *innerchecker.Checker, t *innerchecker.Type) []*innerast.Symbol {
|
|
86
86
|
return recv.GetApparentProperties(t)
|
|
87
87
|
}
|
|
@@ -149,6 +149,16 @@ func Checker_getAliasedSymbol(recv *innerchecker.Checker, symbol *innerast.Symbo
|
|
|
149
149
|
return recv.GetAliasedSymbol(symbol)
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
// Checker_getExportsOfModule returns the exported symbols of a source-file or
|
|
153
|
+
// namespace module symbol, resolving export-star aggregation the same way the
|
|
154
|
+
// checker does for emit and services.
|
|
155
|
+
func Checker_getExportsOfModule(recv *innerchecker.Checker, symbol *innerast.Symbol) []*innerast.Symbol {
|
|
156
|
+
if recv == nil || symbol == nil {
|
|
157
|
+
return nil
|
|
158
|
+
}
|
|
159
|
+
return recv.GetExportsOfModule(symbol)
|
|
160
|
+
}
|
|
161
|
+
|
|
152
162
|
//go:linkname checkerResolveEntityName github.com/microsoft/typescript-go/internal/checker.(*Checker).resolveEntityName
|
|
153
163
|
func checkerResolveEntityName(
|
|
154
164
|
recv *innerchecker.Checker,
|
|
@@ -215,8 +225,8 @@ func Checker_getBaseTypes(recv *innerchecker.Checker, t *innerchecker.Type) []*i
|
|
|
215
225
|
func checkerGetDeclaredTypeOfSymbol(recv *innerchecker.Checker, symbol *innerast.Symbol) *innerchecker.Type
|
|
216
226
|
|
|
217
227
|
// Checker_getDeclaredTypeOfSymbol returns the declared (instance) type of a
|
|
218
|
-
// class or interface symbol. Unlike Checker_getTypeOfSymbol
|
|
219
|
-
// constructor (static) type of a class symbol
|
|
228
|
+
// class or interface symbol. Unlike Checker_getTypeOfSymbol, which yields the
|
|
229
|
+
// constructor (static) type of a class symbol, the result IS a
|
|
220
230
|
// ClassOrInterface type and is therefore safe to feed back into
|
|
221
231
|
// Checker_getBaseTypes. This lets a consumer resolve a generic base's symbol to
|
|
222
232
|
// its declared type and keep walking the base chain past the generic boundary
|
|
@@ -275,10 +285,10 @@ func Checker_getReturnTypeOfSignature(recv *innerchecker.Checker, signature *inn
|
|
|
275
285
|
// parameter is excluded (it is held separately from the value parameters).
|
|
276
286
|
//
|
|
277
287
|
// Checker_getMinArgumentCount alone cannot tell a zero-parameter signature
|
|
278
|
-
// (`()`
|
|
288
|
+
// (`()` minimum 0) from a single-optional-parameter one (`(x?)` also
|
|
279
289
|
// minimum 0). A type-transform plugin needs that distinction to replicate the
|
|
280
|
-
// type-level "single meaningful argument" rule
|
|
281
|
-
// and every later parameter must be optional or rest
|
|
290
|
+
// type-level "single meaningful argument" rule: a FIRST parameter must exist
|
|
291
|
+
// and every later parameter must be optional or rest, as
|
|
282
292
|
// `Signature_parameterCount(sig) >= 1 && Checker_getMinArgumentCount(c, sig) <= 1`.
|
|
283
293
|
// Without it the `new C(x)` / `C.from(x)` strategies silently fall back to field
|
|
284
294
|
// copy for every optional-first constructor or factory. Returns 0 if signature
|
|
@@ -297,7 +307,7 @@ func Signature_parameterCount(signature *innerchecker.Signature) int {
|
|
|
297
307
|
// yields the seed TYPE the plugin must decode before constructing the instance.
|
|
298
308
|
//
|
|
299
309
|
// Signature_parameterCount is len() of this slice; the slice itself is needed
|
|
300
|
-
// because detection (count + min-args) is not enough
|
|
310
|
+
// because detection (count + min-args) is not enough; emission requires the
|
|
301
311
|
// seed parameter's type. Returns nil if signature is nil.
|
|
302
312
|
func Signature_parameters(signature *innerchecker.Signature) []*innerast.Symbol {
|
|
303
313
|
if signature == nil {
|
|
@@ -308,8 +318,8 @@ func Signature_parameters(signature *innerchecker.Signature) []*innerast.Symbol
|
|
|
308
318
|
|
|
309
319
|
// Signature_hasRestParameter reports whether the signature's last value
|
|
310
320
|
// parameter is a rest parameter (`...xs: S[]`). It is the signal a from/new
|
|
311
|
-
// transform needs to tell a rest-only single-seed call `(...xs: S[])
|
|
312
|
-
// seed is the ELEMENT S
|
|
321
|
+
// transform needs to tell a rest-only single-seed call `(...xs: S[])`, whose
|
|
322
|
+
// seed is the ELEMENT S, from a genuine array-typed parameter `(seed: S[])`,
|
|
313
323
|
// whose seed is the array S[]: getTypeOfSymbol yields `S[]` for BOTH, so without
|
|
314
324
|
// this flag they are indistinguishable and the rest case decodes the wrong
|
|
315
325
|
// shape.
|
|
@@ -317,8 +327,8 @@ func Signature_parameters(signature *innerchecker.Signature) []*innerast.Symbol
|
|
|
317
327
|
// The rest ELEMENT is the seed ONLY when the rest parameter is the sole value
|
|
318
328
|
// parameter, i.e. `Signature_hasRestParameter(sig) && Signature_parameterCount(sig) == 1`.
|
|
319
329
|
// A leading-required + rest-tail signature `(s: S, ...r: R[])` also has a rest
|
|
320
|
-
// parameter (this returns true), but its seed is the FIRST parameter S
|
|
321
|
-
// from Signature_parameters(sig)[0], NOT the rest element
|
|
330
|
+
// parameter (this returns true), but its seed is the FIRST parameter S. Read it
|
|
331
|
+
// from Signature_parameters(sig)[0], NOT the rest element, matching
|
|
322
332
|
// ClassifiableSeed, whose `[infer P, ...Rest]` arm picks P=S there. Returns
|
|
323
333
|
// false if signature is nil.
|
|
324
334
|
func Signature_hasRestParameter(signature *innerchecker.Signature) bool {
|
|
@@ -330,7 +340,7 @@ func Signature_hasRestParameter(signature *innerchecker.Signature) bool {
|
|
|
330
340
|
|
|
331
341
|
// Checker_getRestTypeOfSignature returns the ELEMENT type of the signature's
|
|
332
342
|
// rest parameter (`...xs: S[]` -> S; a tuple rest unwraps to its element too),
|
|
333
|
-
// which is the seed type for a rest-ONLY single-argument constructor/factory
|
|
343
|
+
// which is the seed type for a rest-ONLY single-argument constructor/factory,
|
|
334
344
|
// matching ClassifiableSeed, which unwraps the rest to its element. When the
|
|
335
345
|
// signature has NO rest parameter it falls back to `any` upstream; and a
|
|
336
346
|
// leading-required + rest-tail `(s: S, ...r: R[])` has a rest parameter yet its
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import type { ResidentTransformProcess } from "./compiler/internal/residentTransformProcess";
|
|
4
|
+
import { startResidentTransform } from "./compiler/internal/startResidentTransform";
|
|
5
|
+
import type { ITtscCompilerContext } from "./structures/ITtscCompilerContext";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Resident, incremental transform service for the `ttsc` TypeScript-Go
|
|
9
|
+
* pipeline.
|
|
10
|
+
*
|
|
11
|
+
* Where {@link TtscCompiler.transform} spawns a fresh process and recompiles the
|
|
12
|
+
* whole project on every call, `TtscService` keeps one long-lived host warm: it
|
|
13
|
+
* compiles the project once and then answers per-file transform requests from
|
|
14
|
+
* that warm program. A single service instance transforms many files (a watch
|
|
15
|
+
* server, an editor session, a codegen tool) while paying the project compile
|
|
16
|
+
* once instead of once per file. Sharing one host across separate worker
|
|
17
|
+
* processes (a Metro worker pool) is tracked in samchon/ttsc#255.
|
|
18
|
+
*
|
|
19
|
+
* The shape mirrors a legacy TypeScript `LanguageService`: construct it against
|
|
20
|
+
* a project context, ask it to transform individual files, and dispose it when
|
|
21
|
+
* done. Construction is synchronous (it launches the host); the host compiles
|
|
22
|
+
* in the background, so the first {@link transformFile} resolves once that
|
|
23
|
+
* compile lands.
|
|
24
|
+
*
|
|
25
|
+
* Resident mode runs through the linked-plugin shared host, so the project must
|
|
26
|
+
* declare at least one transform-stage plugin; the constructor throws
|
|
27
|
+
* otherwise. It does not run check-stage plugins (unlike
|
|
28
|
+
* {@link TtscCompiler.transform}); only the program's own type-checking gates an
|
|
29
|
+
* {@link updateFile}.
|
|
30
|
+
*/
|
|
31
|
+
export class TtscService {
|
|
32
|
+
private readonly resident: ResidentTransformProcess;
|
|
33
|
+
private readonly projectRoot: string;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create a service bound to the given project context and launch its resident
|
|
37
|
+
* host. The context is the same shape {@link TtscCompiler} accepts; it is not
|
|
38
|
+
* replaceable per call.
|
|
39
|
+
*/
|
|
40
|
+
public constructor(context: ITtscCompilerContext = {}) {
|
|
41
|
+
const started = startResidentTransform({
|
|
42
|
+
...context,
|
|
43
|
+
env: context.env ? { ...context.env } : undefined,
|
|
44
|
+
plugins: Array.isArray(context.plugins)
|
|
45
|
+
? [...context.plugins]
|
|
46
|
+
: context.plugins,
|
|
47
|
+
});
|
|
48
|
+
this.resident = started.process;
|
|
49
|
+
this.projectRoot = started.projectRoot;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Return the transformed TypeScript source for one file, or `undefined` when
|
|
54
|
+
* the resident program does not contain it (for example a file excluded from
|
|
55
|
+
* the tsconfig). A relative `fileName` is resolved against the project root.
|
|
56
|
+
*
|
|
57
|
+
* Rejects when the resident host failed to compile the project; the rejection
|
|
58
|
+
* carries the host's diagnostics so callers can surface a real build error.
|
|
59
|
+
*/
|
|
60
|
+
public async transformFile(fileName: string): Promise<string | undefined> {
|
|
61
|
+
const reply = await this.resident.request({
|
|
62
|
+
file: this.absolutePath(fileName),
|
|
63
|
+
});
|
|
64
|
+
return reply.found === true && typeof reply.typescript === "string"
|
|
65
|
+
? reply.typescript
|
|
66
|
+
: undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Apply new in-memory content for one file and re-transform the project, so a
|
|
71
|
+
* subsequent {@link transformFile} reflects the edit without restarting the
|
|
72
|
+
* host. Returns whether the re-transform succeeded; `false` means the edit
|
|
73
|
+
* did not compile and the previous transform is still in effect. A relative
|
|
74
|
+
* `fileName` is resolved against the project root.
|
|
75
|
+
*/
|
|
76
|
+
public async updateFile(fileName: string, content: string): Promise<boolean> {
|
|
77
|
+
const reply = await this.resident.request({
|
|
78
|
+
content,
|
|
79
|
+
update: this.absolutePath(fileName),
|
|
80
|
+
});
|
|
81
|
+
return reply.updated === true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Terminate the resident host and reject any in-flight requests. */
|
|
85
|
+
public dispose(): void {
|
|
86
|
+
this.resident.dispose();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private absolutePath(fileName: string): string {
|
|
90
|
+
return path.isAbsolute(fileName)
|
|
91
|
+
? fileName
|
|
92
|
+
: path.resolve(this.projectRoot, fileName);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { type ChildProcess, spawn } from "node:child_process";
|
|
2
|
+
import { type Interface, createInterface } from "node:readline";
|
|
3
|
+
|
|
4
|
+
/** Cap on retained stderr so a long-lived host cannot grow it without bound. */
|
|
5
|
+
const STDERR_TAIL_LIMIT = 64 * 1024;
|
|
6
|
+
|
|
7
|
+
/** Options for spawning the resident transform host. */
|
|
8
|
+
export interface ResidentTransformProcessOptions {
|
|
9
|
+
args: readonly string[];
|
|
10
|
+
binary: string;
|
|
11
|
+
cwd?: string;
|
|
12
|
+
env?: NodeJS.ProcessEnv;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface PendingRequest {
|
|
16
|
+
reject: (reason: Error) => void;
|
|
17
|
+
resolve: (reply: Record<string, unknown>) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Async client for the long-lived `utility-host serve` process.
|
|
22
|
+
*
|
|
23
|
+
* The host transforms the whole project once at startup, caches every file's
|
|
24
|
+
* transformed TypeScript, then answers newline-delimited requests. This class
|
|
25
|
+
* speaks that protocol: each {@link request} writes one JSON line and the host
|
|
26
|
+
* replies with one line, matched FIFO. A transform request (`{"file":...}`) is
|
|
27
|
+
* answered with `{"typescript":...,"found":...}`, and an update request
|
|
28
|
+
* (`{"update":...,"content":...}`) with `{"updated":...}`.
|
|
29
|
+
*
|
|
30
|
+
* One resident process answers every request from one service instead of
|
|
31
|
+
* spawning a fresh `transform` subprocess per call, so a single process pays
|
|
32
|
+
* the project compile once (samchon/ttsc#255).
|
|
33
|
+
*/
|
|
34
|
+
export class ResidentTransformProcess {
|
|
35
|
+
private readonly child: ChildProcess;
|
|
36
|
+
private readonly reader: Interface;
|
|
37
|
+
private readonly pending: PendingRequest[] = [];
|
|
38
|
+
private stderr = "";
|
|
39
|
+
private failure: Error | undefined;
|
|
40
|
+
|
|
41
|
+
public constructor(options: ResidentTransformProcessOptions) {
|
|
42
|
+
// Default stdio is "pipe" for stdin/stdout/stderr, which is exactly what the
|
|
43
|
+
// line protocol needs; spelling it out as a string[] would not narrow to
|
|
44
|
+
// StdioOptions, so it is left implicit.
|
|
45
|
+
this.child = spawn(options.binary, [...options.args], {
|
|
46
|
+
cwd: options.cwd,
|
|
47
|
+
env: options.env,
|
|
48
|
+
windowsHide: true,
|
|
49
|
+
});
|
|
50
|
+
const stdout = this.child.stdout;
|
|
51
|
+
const stdin = this.child.stdin;
|
|
52
|
+
if (stdout === null || stdin === null) {
|
|
53
|
+
this.child.kill();
|
|
54
|
+
throw new Error("ttsc: resident transform host has no stdio pipes");
|
|
55
|
+
}
|
|
56
|
+
this.reader = createInterface({ input: stdout });
|
|
57
|
+
this.reader.on("line", (line) => this.onLine(line));
|
|
58
|
+
// Drive completion off stdout's end, not the process "exit" event: every
|
|
59
|
+
// buffered "line" is delivered before "close", so a reply that arrived just
|
|
60
|
+
// before the host exited still resolves its request instead of racing the
|
|
61
|
+
// exit-time rejection.
|
|
62
|
+
this.reader.on("close", () => this.onReaderClose());
|
|
63
|
+
this.child.stderr?.on("data", (chunk: Buffer | string) => {
|
|
64
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
65
|
+
// Keep only the tail: stderr is consulted for the crash message, and a
|
|
66
|
+
// long resident session can write diagnostics on every failed update.
|
|
67
|
+
this.stderr = (this.stderr + text).slice(-STDERR_TAIL_LIMIT);
|
|
68
|
+
});
|
|
69
|
+
this.child.on("error", (error) => this.fail(error));
|
|
70
|
+
// A dead host's pipes emit "error" (EPIPE on POSIX, often ECONNRESET on
|
|
71
|
+
// Windows). Without these listeners Node turns a pipe error into an uncaught
|
|
72
|
+
// exception that crashes the whole consumer process; route them through
|
|
73
|
+
// fail() (stderr is non-critical) so a pipe death fails in-flight requests
|
|
74
|
+
// instead, and also closes the window where stdin.destroyed has not flipped.
|
|
75
|
+
stdin.on("error", (error) => this.fail(error));
|
|
76
|
+
stdout.on("error", (error) => this.fail(error));
|
|
77
|
+
this.child.stderr?.on("error", () => {});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Send one request to the host and resolve with its parsed JSON reply. The
|
|
82
|
+
* host answers in FIFO order, so the caller interprets the reply shape by the
|
|
83
|
+
* payload it sent (a transform reply vs an update reply). Rejects when the
|
|
84
|
+
* host has already failed or exited, or if writing the request fails.
|
|
85
|
+
*/
|
|
86
|
+
public request(
|
|
87
|
+
payload: Record<string, unknown>,
|
|
88
|
+
): Promise<Record<string, unknown>> {
|
|
89
|
+
if (this.failure !== undefined) {
|
|
90
|
+
return Promise.reject(this.failure);
|
|
91
|
+
}
|
|
92
|
+
const stdin = this.child.stdin;
|
|
93
|
+
if (stdin === null || stdin.destroyed) {
|
|
94
|
+
return Promise.reject(
|
|
95
|
+
new Error("ttsc: resident transform host stdin is closed"),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return new Promise<Record<string, unknown>>((resolve, reject) => {
|
|
99
|
+
const pending: PendingRequest = { reject, resolve };
|
|
100
|
+
this.pending.push(pending);
|
|
101
|
+
stdin.write(`${JSON.stringify(payload)}\n`, (error) => {
|
|
102
|
+
if (error) {
|
|
103
|
+
this.removePending(pending);
|
|
104
|
+
reject(error);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Terminate the resident process and reject any in-flight requests. Safe to
|
|
112
|
+
* call more than once.
|
|
113
|
+
*/
|
|
114
|
+
public dispose(): void {
|
|
115
|
+
if (this.failure === undefined) {
|
|
116
|
+
// If the host already died, reject with its real exit error (stderr +
|
|
117
|
+
// exit code) rather than a bland "disposed" message.
|
|
118
|
+
this.failure =
|
|
119
|
+
this.child.exitCode !== null || this.child.signalCode !== null
|
|
120
|
+
? this.exitError()
|
|
121
|
+
: new Error("ttsc: resident transform host disposed");
|
|
122
|
+
}
|
|
123
|
+
const stdin = this.child.stdin;
|
|
124
|
+
if (stdin !== null && !stdin.destroyed) {
|
|
125
|
+
stdin.end();
|
|
126
|
+
}
|
|
127
|
+
this.reader.close();
|
|
128
|
+
this.rejectAll(this.failure);
|
|
129
|
+
if (this.child.exitCode === null && this.child.signalCode === null) {
|
|
130
|
+
this.child.kill();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private onLine(line: string): void {
|
|
135
|
+
const trimmed = line.trim();
|
|
136
|
+
if (trimmed.length === 0) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const request = this.pending.shift();
|
|
140
|
+
if (request === undefined) {
|
|
141
|
+
// The host must emit exactly one reply per request, so an unmatched line
|
|
142
|
+
// is a protocol violation that would desync every later reply into the
|
|
143
|
+
// wrong request. Fail fast rather than silently returning wrong output.
|
|
144
|
+
// A line arriving after teardown (failure already set) is benign.
|
|
145
|
+
if (this.failure === undefined) {
|
|
146
|
+
this.fail(
|
|
147
|
+
new Error("ttsc: resident transform host sent an unsolicited reply"),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
request.resolve(parseReply(trimmed));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private onReaderClose(): void {
|
|
156
|
+
if (this.pending.length === 0) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
this.rejectAll(this.failure ?? this.exitError());
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private fail(error: Error): void {
|
|
163
|
+
this.failure ??= error;
|
|
164
|
+
this.rejectAll(this.failure);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private rejectAll(error: Error): void {
|
|
168
|
+
while (this.pending.length !== 0) {
|
|
169
|
+
this.pending.shift()!.reject(error);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private removePending(target: PendingRequest): void {
|
|
174
|
+
const index = this.pending.indexOf(target);
|
|
175
|
+
if (index !== -1) {
|
|
176
|
+
this.pending.splice(index, 1);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private exitError(): Error {
|
|
181
|
+
const detail = this.stderr.trim();
|
|
182
|
+
if (detail.length !== 0) {
|
|
183
|
+
return new Error(detail);
|
|
184
|
+
}
|
|
185
|
+
// Read the child's own exit fields (set synchronously by Node) so the
|
|
186
|
+
// message is accurate even before this instance's "exit" handler would run.
|
|
187
|
+
const code = this.child.exitCode;
|
|
188
|
+
const signalCode = this.child.signalCode;
|
|
189
|
+
const signal = signalCode === null ? "" : `, signal ${signalCode}`;
|
|
190
|
+
return new Error(
|
|
191
|
+
`ttsc: resident transform host exited (code ${code ?? "null"}${signal})`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Parse one reply line into a plain object. A malformed line degrades to an
|
|
198
|
+
* empty object rather than throwing, so one bad line never rejects a request
|
|
199
|
+
* the FIFO has already advanced past; the caller reads missing fields as
|
|
200
|
+
* absent.
|
|
201
|
+
*/
|
|
202
|
+
function parseReply(line: string): Record<string, unknown> {
|
|
203
|
+
try {
|
|
204
|
+
const parsed = JSON.parse(line) as unknown;
|
|
205
|
+
if (
|
|
206
|
+
typeof parsed === "object" &&
|
|
207
|
+
parsed !== null &&
|
|
208
|
+
!Array.isArray(parsed)
|
|
209
|
+
) {
|
|
210
|
+
return parsed as Record<string, unknown>;
|
|
211
|
+
}
|
|
212
|
+
} catch {
|
|
213
|
+
// A non-object or unparseable line is treated as an empty reply.
|
|
214
|
+
}
|
|
215
|
+
return {};
|
|
216
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { loadProjectPlugins } from "../../plugin/internal/loadProjectPlugins";
|
|
4
|
+
import type { ITtscCompilerContext } from "../../structures/ITtscCompilerContext";
|
|
5
|
+
import type { ITtscLoadedNativePlugin } from "../../structures/internal/ITtscLoadedNativePlugin";
|
|
6
|
+
import { readProjectConfig } from "./project/readProjectConfig";
|
|
7
|
+
import { ResidentTransformProcess } from "./residentTransformProcess";
|
|
8
|
+
import { resolveBinary } from "./resolveBinary";
|
|
9
|
+
import { resolveTsgo } from "./resolveTsgo";
|
|
10
|
+
import {
|
|
11
|
+
assertSharedHostCompatibility,
|
|
12
|
+
linkedTransformPlugins,
|
|
13
|
+
selectSharedHostPlugin,
|
|
14
|
+
} from "./sharedHostHelpers";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A started resident transform host plus the project root its keys are relative
|
|
18
|
+
* to.
|
|
19
|
+
*/
|
|
20
|
+
export interface StartedResidentTransform {
|
|
21
|
+
process: ResidentTransformProcess;
|
|
22
|
+
projectRoot: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Start a resident `serve` host for the configured project.
|
|
27
|
+
*
|
|
28
|
+
* Mirrors the plugin spawn of `transformProjectInMemory`, but launches the
|
|
29
|
+
* shared host's `serve` subcommand as one long-lived process instead of a
|
|
30
|
+
* per-call `transform` subprocess. The host compiles the whole project once at
|
|
31
|
+
* startup and then answers per-file requests, so one caller pays the project
|
|
32
|
+
* compile once and reuses it across its own per-file requests.
|
|
33
|
+
*
|
|
34
|
+
* Resident mode runs through the linked-plugin shared host
|
|
35
|
+
* (`cmd/utility-host`), which is the only binary that exposes `serve`. It
|
|
36
|
+
* therefore requires at least one transform-stage plugin; executable transform
|
|
37
|
+
* hosts that own their own process are not served and must use the per-call
|
|
38
|
+
* transform path. Check-stage plugins are not run by the resident host.
|
|
39
|
+
*/
|
|
40
|
+
export function startResidentTransform(
|
|
41
|
+
context: ITtscCompilerContext,
|
|
42
|
+
): StartedResidentTransform {
|
|
43
|
+
const cwd = path.resolve(context.cwd ?? process.cwd());
|
|
44
|
+
const project = readProjectConfig({
|
|
45
|
+
cwd,
|
|
46
|
+
projectRoot: context.projectRoot,
|
|
47
|
+
tsconfig: context.tsconfig,
|
|
48
|
+
});
|
|
49
|
+
const loaded = loadProjectPlugins({
|
|
50
|
+
binary: resolveBinary(context) ?? "",
|
|
51
|
+
cacheDir: context.cacheDir ?? context.env?.TTSC_CACHE_DIR,
|
|
52
|
+
cwd,
|
|
53
|
+
entries: context.plugins,
|
|
54
|
+
projectRoot: context.projectRoot,
|
|
55
|
+
tsconfig: project.path,
|
|
56
|
+
});
|
|
57
|
+
const transformers = loaded.nativePlugins.filter(
|
|
58
|
+
(plugin) => plugin.stage === "transform",
|
|
59
|
+
);
|
|
60
|
+
if (transformers.length === 0) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
"ttsc: TtscService resident mode requires at least one transform-stage plugin; " +
|
|
63
|
+
"use TtscCompiler.transform for projects with only check-stage plugins or none",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
assertSharedHostCompatibility(transformers, "source-to-source");
|
|
67
|
+
|
|
68
|
+
const host = selectSharedHostPlugin(transformers);
|
|
69
|
+
const tsgoBinary = resolveTsgo({ ...context, cwd: project.root }).binary;
|
|
70
|
+
const resident = new ResidentTransformProcess({
|
|
71
|
+
args: [
|
|
72
|
+
"serve",
|
|
73
|
+
`--tsconfig=${project.path}`,
|
|
74
|
+
`--plugins-json=${serializeNativePlugins(transformers)}`,
|
|
75
|
+
`--cwd=${project.root}`,
|
|
76
|
+
],
|
|
77
|
+
binary: host.binary,
|
|
78
|
+
cwd: project.root,
|
|
79
|
+
env: residentEnv(context, tsgoBinary, loaded.nativePlugins),
|
|
80
|
+
});
|
|
81
|
+
return { process: resident, projectRoot: project.root };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Build the environment for the resident host spawn. Matches the per-call
|
|
86
|
+
* transform spawn: injects the Node, tsgo, and ttsx binaries so the host never
|
|
87
|
+
* searches PATH, and forwards linked transform plugins via
|
|
88
|
+
* `TTSC_LINKED_PLUGINS_JSON`.
|
|
89
|
+
*/
|
|
90
|
+
function residentEnv(
|
|
91
|
+
context: ITtscCompilerContext,
|
|
92
|
+
tsgoBinary: string,
|
|
93
|
+
nativePlugins: readonly ITtscLoadedNativePlugin[],
|
|
94
|
+
): NodeJS.ProcessEnv {
|
|
95
|
+
const env: NodeJS.ProcessEnv = {
|
|
96
|
+
...process.env,
|
|
97
|
+
TTSC_NODE_BINARY: process.env.TTSC_NODE_BINARY ?? process.execPath,
|
|
98
|
+
TTSC_TSGO_BINARY: process.env.TTSC_TSGO_BINARY ?? tsgoBinary,
|
|
99
|
+
TTSC_TTSX_BINARY:
|
|
100
|
+
process.env.TTSC_TTSX_BINARY ??
|
|
101
|
+
path.join(__dirname, "..", "..", "launcher", "ttsx.js"),
|
|
102
|
+
...context.env,
|
|
103
|
+
};
|
|
104
|
+
const linked = linkedTransformPlugins(nativePlugins);
|
|
105
|
+
if (linked.length !== 0) {
|
|
106
|
+
env.TTSC_LINKED_PLUGINS_JSON = serializeNativePlugins(linked);
|
|
107
|
+
}
|
|
108
|
+
return env;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Serialize the plugin list to the `--plugins-json` /
|
|
113
|
+
* `TTSC_LINKED_PLUGINS_JSON` shape the native host reads: only the fields it
|
|
114
|
+
* needs, to keep the arg short.
|
|
115
|
+
*/
|
|
116
|
+
function serializeNativePlugins(
|
|
117
|
+
plugins: readonly ITtscLoadedNativePlugin[],
|
|
118
|
+
): string {
|
|
119
|
+
return JSON.stringify(
|
|
120
|
+
plugins.map((plugin) => ({
|
|
121
|
+
config: plugin.config,
|
|
122
|
+
name: plugin.name,
|
|
123
|
+
stage: plugin.stage,
|
|
124
|
+
})),
|
|
125
|
+
);
|
|
126
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -570,7 +570,7 @@ function orderNativePlugins(
|
|
|
570
570
|
|
|
571
571
|
function loadPluginEntry(
|
|
572
572
|
entry: ITtscProjectPluginConfig,
|
|
573
|
-
|
|
573
|
+
base: Omit<ITtscPluginFactoryContext, "dirname" | "filename">,
|
|
574
574
|
baseDir: string,
|
|
575
575
|
): ITtscPlugin {
|
|
576
576
|
return withPluginLoaderEnv(() => {
|
|
@@ -582,6 +582,16 @@ function loadPluginEntry(
|
|
|
582
582
|
}
|
|
583
583
|
|
|
584
584
|
const request = resolvePluginRequest(specifier, baseDir);
|
|
585
|
+
// `dirname`/`filename` are per-entry: each plugin entry resolves to its own
|
|
586
|
+
// descriptor module, so they are derived here from the resolved `request`
|
|
587
|
+
// rather than carried on the shared base context. They give factories a
|
|
588
|
+
// load-mode-independent stand-in for `__dirname`/`__filename`, which are
|
|
589
|
+
// undefined when a descriptor loads through ttsx or as ESM.
|
|
590
|
+
const context: ITtscPluginFactoryContext = {
|
|
591
|
+
...base,
|
|
592
|
+
dirname: path.dirname(request),
|
|
593
|
+
filename: request,
|
|
594
|
+
};
|
|
585
595
|
const mod = requirePluginEntry(request, context) as {
|
|
586
596
|
createTtscPlugin?: TtscPluginFactory;
|
|
587
597
|
default?: ITtscPlugin | TtscPluginFactory;
|
|
@@ -702,6 +712,8 @@ function loadDescriptorViaTtsx(
|
|
|
702
712
|
TTSC_PLUGIN_CONTEXT: JSON.stringify({
|
|
703
713
|
binary: context.binary,
|
|
704
714
|
cwd: context.cwd,
|
|
715
|
+
dirname: context.dirname,
|
|
716
|
+
filename: context.filename,
|
|
705
717
|
plugin: context.plugin,
|
|
706
718
|
projectRoot: context.projectRoot,
|
|
707
719
|
tsconfig: context.tsconfig,
|
|
@@ -853,8 +865,10 @@ function resolveGoPackageDir(source: string, label: string): string {
|
|
|
853
865
|
`ttsc: plugin "${label}" source does not exist: ${source}\n` +
|
|
854
866
|
` Plugin descriptors run without CommonJS globals: __dirname, __filename, ` +
|
|
855
867
|
`and require are undefined when ttsc loads a descriptor through ttsx or as ESM. ` +
|
|
856
|
-
`If this path was derived from one of them,
|
|
857
|
-
`
|
|
868
|
+
`If this path was derived from one of them, use context.dirname / ` +
|
|
869
|
+
`context.filename (the descriptor's own directory and file, populated in ` +
|
|
870
|
+
`every load mode), or resolve it from context.projectRoot, e.g. ` +
|
|
871
|
+
`createRequire(path.join(context.projectRoot, "package.json"))` +
|
|
858
872
|
`.resolve("<your-package>/package.json").`,
|
|
859
873
|
);
|
|
860
874
|
}
|
|
@@ -37,14 +37,16 @@ export interface ITtscPlugin {
|
|
|
37
37
|
*
|
|
38
38
|
* Relative paths are resolved from the consumer project root. Package
|
|
39
39
|
* descriptors published in npm packages should normally return absolute paths
|
|
40
|
-
* based on their own descriptor directory
|
|
40
|
+
* based on their own descriptor directory — derive them from a factory's
|
|
41
|
+
* `context.dirname` (the load-mode-independent replacement for `__dirname`,
|
|
42
|
+
* which is undefined when ttsc loads the descriptor through ttsx or as ESM).
|
|
41
43
|
*
|
|
42
44
|
* Common layouts:
|
|
43
45
|
*
|
|
44
|
-
* - `source: path.resolve(
|
|
45
|
-
* its Go command in `src`.
|
|
46
|
-
* - `source: path.resolve(
|
|
47
|
-
* dedicated Go plugin folder.
|
|
46
|
+
* - `source: path.resolve(context.dirname, "src")` when a package descriptor
|
|
47
|
+
* keeps its Go command in `src`.
|
|
48
|
+
* - `source: path.resolve(context.dirname, "plugin")` when a published package
|
|
49
|
+
* has a dedicated Go plugin folder.
|
|
48
50
|
* - `source: "plugin"` only for project-local descriptors where the consumer
|
|
49
51
|
* project root owns the `plugin` directory.
|
|
50
52
|
* - `source: "go.mod"` when the consumer project root itself is the command
|
|
@@ -6,7 +6,7 @@ import type { ITtscProjectPluginConfig } from "./ITtscProjectPluginConfig";
|
|
|
6
6
|
* Plugin packages may export a static {@link ITtscPlugin} descriptor when the
|
|
7
7
|
* descriptor never depends on the consuming project. Export a factory when the
|
|
8
8
|
* descriptor needs to inspect the original plugin config, the resolved tsconfig
|
|
9
|
-
* path, or the
|
|
9
|
+
* path, the project root, or the descriptor's own location on disk.
|
|
10
10
|
*
|
|
11
11
|
* The factory runs in Node.js while ttsc is loading `compilerOptions.plugins`.
|
|
12
12
|
* It should only create the descriptor. Heavy validation and TypeScript-Go work
|
|
@@ -32,6 +32,31 @@ export interface ITtscPluginFactoryContext<T = ITtscProjectPluginConfig> {
|
|
|
32
32
|
*/
|
|
33
33
|
cwd: string;
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Absolute path to the directory holding the resolved plugin descriptor entry
|
|
37
|
+
* — the directory of {@link ITtscPluginFactoryContext.filename}.
|
|
38
|
+
*
|
|
39
|
+
* This is the load-mode-independent replacement for the CommonJS `__dirname`.
|
|
40
|
+
* A descriptor compiled to CommonJS and loaded through `require` keeps
|
|
41
|
+
* `__dirname`, but a `.ts`-source or ESM descriptor (loaded through ttsx or
|
|
42
|
+
* as a native module) runs without it, so a `source` derived from `__dirname`
|
|
43
|
+
* silently mis-resolves. Resolve package-relative paths from `dirname`
|
|
44
|
+
* instead: it is always the descriptor file's own directory, regardless of
|
|
45
|
+
* how ttsc loaded it.
|
|
46
|
+
*/
|
|
47
|
+
dirname: string;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Absolute path to the resolved plugin descriptor entry module itself — the
|
|
51
|
+
* file ttsc loaded for this entry's `transform` specifier.
|
|
52
|
+
*
|
|
53
|
+
* This is the load-mode-independent replacement for the CommonJS
|
|
54
|
+
* `__filename`, available even when the descriptor runs as ESM or `.ts`
|
|
55
|
+
* source where the `__filename` global is undefined.
|
|
56
|
+
* {@link ITtscPluginFactoryContext.dirname} is its containing directory.
|
|
57
|
+
*/
|
|
58
|
+
filename: string;
|
|
59
|
+
|
|
35
60
|
/**
|
|
36
61
|
* Original `compilerOptions.plugins[]` entry that loaded this plugin.
|
|
37
62
|
*
|