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.
Files changed (81) hide show
  1. package/README.md +96 -25
  2. package/cmd/graphbench/main.go +135 -0
  3. package/cmd/graphdump/main.go +60 -0
  4. package/cmd/ttscgraph/dump.go +69 -0
  5. package/cmd/ttscgraph/main.go +91 -0
  6. package/cmd/ttscgraph/run_dump_prints_the_graph_as_json_test.go +80 -0
  7. package/cmd/ttscgraph/run_help_version_and_bad_flags_exit_codes_test.go +75 -0
  8. package/cmd/utility-host/main.go +4 -1
  9. package/driver/overlay.go +64 -0
  10. package/driver/program.go +8 -1
  11. package/driver/session.go +85 -0
  12. package/internal/graph/build.go +330 -0
  13. package/internal/graph/build_records_a_node_per_top_level_declaration_test.go +96 -0
  14. package/internal/graph/decorators.go +122 -0
  15. package/internal/graph/decorators_capture_name_and_literal_arguments_test.go +98 -0
  16. package/internal/graph/diagnostics.go +23 -0
  17. package/internal/graph/dump.go +390 -0
  18. package/internal/graph/dump_evidence_skips_leading_trivia_test.go +92 -0
  19. package/internal/graph/dump_serializes_the_full_graph_test.go +146 -0
  20. package/internal/graph/edge_spans_point_to_resolved_usage_test.go +90 -0
  21. package/internal/graph/edges.go +538 -0
  22. package/internal/graph/edges_cover_class_level_references_test.go +93 -0
  23. package/internal/graph/enum_initializer_calls_are_edges_test.go +63 -0
  24. package/internal/graph/exports.go +60 -0
  25. package/internal/graph/file_diagnostics_match_tsgo_code_and_location_test.go +59 -0
  26. package/internal/graph/file_diagnostics_report_code_column_and_message_test.go +69 -0
  27. package/internal/graph/function_locals_do_not_collide_test.go +66 -0
  28. package/internal/graph/gitignore.go +51 -0
  29. package/internal/graph/graph.go +119 -0
  30. package/internal/graph/heritage_edges_cover_implements_and_interface_extends_test.go +76 -0
  31. package/internal/graph/heritage_edges_keep_extends_and_implements_to_same_base_test.go +61 -0
  32. package/internal/graph/heritage_edges_record_extends_versus_implements_origin_test.go +54 -0
  33. package/internal/graph/heritage_edges_traverse_barrels_and_stop_at_external_leaves_test.go +112 -0
  34. package/internal/graph/namespace_dotted_form_qualifies_deeply_test.go +60 -0
  35. package/internal/graph/namespace_members_become_qualified_nodes_and_edges_test.go +100 -0
  36. package/internal/graph/node_identity_is_position_invariant_test.go +84 -0
  37. package/internal/graph/node_modifiers_emit_union_strings_test.go +118 -0
  38. package/internal/graph/nodes_are_marked_exported_through_the_export_table_test.go +61 -0
  39. package/internal/graph/overload_implementation_replaces_signature_test.go +54 -0
  40. package/internal/graph/property_member_edges_remain_owner_visible_test.go +83 -0
  41. package/internal/graph/resolve.go +102 -0
  42. package/internal/graph/resolve_prefers_implementation_declaration_test.go +50 -0
  43. package/internal/graph/resolve_unwraps_barrel_reexport_to_real_declaration_test.go +230 -0
  44. package/internal/graph/type_ref_edges_cover_typeof_as_satisfies_test.go +79 -0
  45. package/internal/graph/type_ref_edges_resolve_named_types_across_files_test.go +52 -0
  46. package/internal/graph/value_access_edges_cover_properties_test.go +96 -0
  47. package/internal/graph/value_call_edges_cover_jsx_components_test.go +82 -0
  48. package/internal/graph/value_call_edges_cover_methods_and_constructors_test.go +70 -0
  49. package/internal/graph/value_call_edges_cover_variable_bound_callables_test.go +46 -0
  50. package/internal/graph/value_call_edges_do_not_double_record_invoked_member_access_test.go +65 -0
  51. package/internal/graph/value_call_edges_record_construct_versus_call_origin_test.go +75 -0
  52. package/internal/graph/value_call_edges_resolve_across_files_and_dedupe_test.go +55 -0
  53. package/internal/graph/value_call_edges_skip_self_and_unresolved_test.go +74 -0
  54. package/lib/TtscService.d.ts +55 -0
  55. package/lib/TtscService.js +93 -0
  56. package/lib/TtscService.js.map +1 -0
  57. package/lib/compiler/internal/residentTransformProcess.d.ts +47 -0
  58. package/lib/compiler/internal/residentTransformProcess.js +187 -0
  59. package/lib/compiler/internal/residentTransformProcess.js.map +1 -0
  60. package/lib/compiler/internal/startResidentTransform.d.ts +26 -0
  61. package/lib/compiler/internal/startResidentTransform.js +98 -0
  62. package/lib/compiler/internal/startResidentTransform.js.map +1 -0
  63. package/lib/index.d.ts +1 -0
  64. package/lib/index.js +1 -0
  65. package/lib/index.js.map +1 -1
  66. package/lib/plugin/internal/loadProjectPlugins.js +17 -3
  67. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  68. package/lib/structures/ITtscPlugin.d.ts +7 -5
  69. package/lib/structures/ITtscPluginFactoryContext.d.ts +24 -1
  70. package/package.json +8 -8
  71. package/shim/ast/shim.go +6 -0
  72. package/shim/checker/shim.go +22 -12
  73. package/src/TtscService.ts +94 -0
  74. package/src/compiler/internal/residentTransformProcess.ts +216 -0
  75. package/src/compiler/internal/startResidentTransform.ts +126 -0
  76. package/src/index.ts +1 -0
  77. package/src/plugin/internal/loadProjectPlugins.ts +17 -3
  78. package/src/structures/ITtscPlugin.ts +7 -5
  79. package/src/structures/ITtscPluginFactoryContext.ts +26 -1
  80. package/utility/host.go +8 -2
  81. 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 {
@@ -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 → 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 — which yields the
219
- // constructor (static) type of a class symbol — the result IS a
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
- // (`()` — minimum 0) from a single-optional-parameter one (`(x?)` — also
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 — a FIRST parameter must exist
281
- // and every later parameter must be optional or rest — as
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 — emission requires the
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[])` — whose
312
- // seed is the ELEMENT S — from a genuine array-typed parameter `(seed: 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 — read it
321
- // from Signature_parameters(sig)[0], NOT the rest element — matching
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
@@ -8,4 +8,5 @@
8
8
  */
9
9
 
10
10
  export * from "./TtscCompiler";
11
+ export * from "./TtscService";
11
12
  export * from "./structures/index";
@@ -570,7 +570,7 @@ function orderNativePlugins(
570
570
 
571
571
  function loadPluginEntry(
572
572
  entry: ITtscProjectPluginConfig,
573
- context: ITtscPluginFactoryContext,
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, resolve it from context.projectRoot ` +
857
- `instead, e.g. createRequire(path.join(context.projectRoot, "package.json"))` +
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(__dirname, "src")` when a package descriptor keeps
45
- * its Go command in `src`.
46
- * - `source: path.resolve(__dirname, "plugin")` when a published package has a
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 project root.
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
  *