ttsc 0.16.3 → 0.16.6
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 +120 -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 +374 -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 +581 -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 +122 -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_attribute_function_assignments_to_members_test.go +59 -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/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/utility/host.go +8 -2
- package/utility/serve.go +158 -0
|
@@ -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
package/utility/host.go
CHANGED
|
@@ -10,12 +10,13 @@ import (
|
|
|
10
10
|
|
|
11
11
|
shimcompiler "github.com/microsoft/typescript-go/shim/compiler"
|
|
12
12
|
shimprinter "github.com/microsoft/typescript-go/shim/printer"
|
|
13
|
+
"github.com/microsoft/typescript-go/shim/vfs"
|
|
13
14
|
|
|
14
15
|
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
15
16
|
)
|
|
16
17
|
|
|
17
|
-
// hostOptions is the parsed form of the flags accepted by
|
|
18
|
-
//
|
|
18
|
+
// hostOptions is the parsed form of the flags accepted by the subcommands
|
|
19
|
+
// (check, build, transform, serve).
|
|
19
20
|
type hostOptions struct {
|
|
20
21
|
cwd string
|
|
21
22
|
emit bool
|
|
@@ -28,6 +29,10 @@ type hostOptions struct {
|
|
|
28
29
|
singleThreaded bool
|
|
29
30
|
checkers int
|
|
30
31
|
tsgoArgs []string
|
|
32
|
+
// fs overrides the filesystem the program loads from. Only the resident serve
|
|
33
|
+
// host sets it (to an OverlayFS), so build/check/transform leave it nil and
|
|
34
|
+
// LoadProgram falls back to the default filesystem.
|
|
35
|
+
fs vfs.FS
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
// transformResult is the JSON envelope written to stdout by RunTransform.
|
|
@@ -251,6 +256,7 @@ func loadUtilityProgram(opts hostOptions) (*driver.Program, []driver.PluginEntry
|
|
|
251
256
|
SingleThreaded: opts.singleThreaded,
|
|
252
257
|
Checkers: opts.checkers,
|
|
253
258
|
TsgoArgs: opts.tsgoArgs,
|
|
259
|
+
FS: opts.fs,
|
|
254
260
|
})
|
|
255
261
|
if err != nil {
|
|
256
262
|
fmt.Fprintf(os.Stderr, "ttsc utility: %v\n", err)
|
package/utility/serve.go
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
package utility
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bufio"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"fmt"
|
|
7
|
+
"io"
|
|
8
|
+
"os"
|
|
9
|
+
"path/filepath"
|
|
10
|
+
"strings"
|
|
11
|
+
|
|
12
|
+
shimprinter "github.com/microsoft/typescript-go/shim/printer"
|
|
13
|
+
|
|
14
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
// serveRequest is one newline-delimited request the resident host reads from its
|
|
18
|
+
// input stream. It is either a transform request (File set) for the transformed
|
|
19
|
+
// TypeScript of a file, or an update request (Update set) that applies new
|
|
20
|
+
// in-memory content for a file and re-transforms the project.
|
|
21
|
+
type serveRequest struct {
|
|
22
|
+
Content string `json:"content"`
|
|
23
|
+
File string `json:"file"`
|
|
24
|
+
Update string `json:"update"`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// serveResponse is the reply to a transform request: the transformed TypeScript
|
|
28
|
+
// for the requested file and whether the resident program had it.
|
|
29
|
+
type serveResponse struct {
|
|
30
|
+
TypeScript string `json:"typescript"`
|
|
31
|
+
Found bool `json:"found"`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// serveUpdateResponse is the reply to an update request: whether re-transforming
|
|
35
|
+
// the project with the new content succeeded. A false reply leaves the previous
|
|
36
|
+
// transform in place (for example the edit introduced a type error); the host's
|
|
37
|
+
// diagnostics are written to stderr.
|
|
38
|
+
type serveUpdateResponse struct {
|
|
39
|
+
Updated bool `json:"updated"`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// RunServe is the resident transform host. It transforms the whole project once
|
|
43
|
+
// (the expensive compile plus linked-plugin pass) over an in-memory overlay,
|
|
44
|
+
// caches every file's transformed text, then answers newline-delimited requests
|
|
45
|
+
// read from in by writing one JSON reply per line to out, until in reaches EOF:
|
|
46
|
+
//
|
|
47
|
+
// - {"file":"<path>"} returns that file's transformed TypeScript.
|
|
48
|
+
// - {"update":"<path>","content":"<text>"} applies new content for the file
|
|
49
|
+
// and re-transforms, so subsequent transform requests reflect the edit.
|
|
50
|
+
//
|
|
51
|
+
// One resident process answers a request stream without recompiling the project
|
|
52
|
+
// per call and reflects edits without respawning the host. Sharing one host
|
|
53
|
+
// across separate worker processes (a Metro worker pool) is tracked in
|
|
54
|
+
// samchon/ttsc#255.
|
|
55
|
+
//
|
|
56
|
+
// in and out are explicit so the request loop is testable; the utility-host
|
|
57
|
+
// command wires them to os.Stdin and os.Stdout.
|
|
58
|
+
func RunServe(in io.Reader, out io.Writer, args []string) int {
|
|
59
|
+
opts, ok := parseHostOptions("serve", args)
|
|
60
|
+
if !ok {
|
|
61
|
+
return 2
|
|
62
|
+
}
|
|
63
|
+
overlay := driver.NewOverlayFS(driver.DefaultFS())
|
|
64
|
+
opts.fs = overlay
|
|
65
|
+
cache, ok := buildServeCache(opts)
|
|
66
|
+
if !ok {
|
|
67
|
+
return 2
|
|
68
|
+
}
|
|
69
|
+
encoder := json.NewEncoder(out)
|
|
70
|
+
// ReadString imposes no line-length limit. An update request carries a whole
|
|
71
|
+
// file's content on one line, so the line is as large as that content; a
|
|
72
|
+
// bufio.Scanner cap would be an arbitrary ceiling at which the resident host
|
|
73
|
+
// dies, and there is no such natural ceiling on source size.
|
|
74
|
+
reader := bufio.NewReader(in)
|
|
75
|
+
for {
|
|
76
|
+
raw, err := reader.ReadString('\n')
|
|
77
|
+
if line := strings.TrimSpace(raw); line != "" {
|
|
78
|
+
cache = handleServeLine(line, opts, overlay, cache, encoder)
|
|
79
|
+
}
|
|
80
|
+
if err != nil {
|
|
81
|
+
if err != io.EOF {
|
|
82
|
+
fmt.Fprintf(os.Stderr, "ttsc utility serve: read error: %v\n", err)
|
|
83
|
+
return 2
|
|
84
|
+
}
|
|
85
|
+
return 0
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// handleServeLine answers one request line and returns the cache to use for the
|
|
91
|
+
// next request: the rebuilt cache after a successful update, the unchanged cache
|
|
92
|
+
// otherwise.
|
|
93
|
+
func handleServeLine(
|
|
94
|
+
line string,
|
|
95
|
+
opts hostOptions,
|
|
96
|
+
overlay *driver.OverlayFS,
|
|
97
|
+
cache map[string]string,
|
|
98
|
+
encoder *json.Encoder,
|
|
99
|
+
) map[string]string {
|
|
100
|
+
var req serveRequest
|
|
101
|
+
if err := json.Unmarshal([]byte(line), &req); err != nil {
|
|
102
|
+
_ = encoder.Encode(serveResponse{})
|
|
103
|
+
return cache
|
|
104
|
+
}
|
|
105
|
+
if req.Update != "" {
|
|
106
|
+
abs := resolveServePath(opts.cwd, req.Update)
|
|
107
|
+
prev, had := overlay.Get(abs)
|
|
108
|
+
overlay.Set(abs, req.Content)
|
|
109
|
+
if rebuilt, ok := buildServeCache(opts); ok {
|
|
110
|
+
_ = encoder.Encode(serveUpdateResponse{Updated: true})
|
|
111
|
+
return rebuilt
|
|
112
|
+
}
|
|
113
|
+
// Roll the failed edit back so a file that does not compile does not poison
|
|
114
|
+
// every later rebuild; the previous transform stays in effect.
|
|
115
|
+
if had {
|
|
116
|
+
overlay.Set(abs, prev)
|
|
117
|
+
} else {
|
|
118
|
+
overlay.Unset(abs)
|
|
119
|
+
}
|
|
120
|
+
_ = encoder.Encode(serveUpdateResponse{Updated: false})
|
|
121
|
+
return cache
|
|
122
|
+
}
|
|
123
|
+
key := apiOutputKey(opts.cwd, resolveServePath(opts.cwd, req.File))
|
|
124
|
+
text, found := cache[key]
|
|
125
|
+
_ = encoder.Encode(serveResponse{TypeScript: text, Found: found})
|
|
126
|
+
return cache
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// buildServeCache runs the whole-project transform once over the current overlay
|
|
130
|
+
// state and returns the transformed text keyed exactly like the transform
|
|
131
|
+
// subcommand's JSON envelope.
|
|
132
|
+
func buildServeCache(opts hostOptions) (map[string]string, bool) {
|
|
133
|
+
prog, _, ok := loadUtilityProgram(opts)
|
|
134
|
+
if !ok {
|
|
135
|
+
return nil, false
|
|
136
|
+
}
|
|
137
|
+
defer prog.Close()
|
|
138
|
+
if err := prog.ApplyLinkedPlugins(); err != nil {
|
|
139
|
+
fmt.Fprintln(os.Stderr, err)
|
|
140
|
+
return nil, false
|
|
141
|
+
}
|
|
142
|
+
printer := shimprinter.NewPrinter(shimprinter.PrinterOptions{}, shimprinter.PrintHandlers{}, nil)
|
|
143
|
+
cache := map[string]string{}
|
|
144
|
+
for _, file := range prog.SourceFiles() {
|
|
145
|
+
cache[apiOutputKey(opts.cwd, file.FileName())] = shimprinter.EmitSourceFile(printer, file)
|
|
146
|
+
}
|
|
147
|
+
return cache, true
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// resolveServePath turns a request's file into an absolute path so apiOutputKey
|
|
151
|
+
// computes the same key buildServeCache stored, and so overlay overrides are
|
|
152
|
+
// keyed the way the program asks for them.
|
|
153
|
+
func resolveServePath(cwd, file string) string {
|
|
154
|
+
if filepath.IsAbs(file) {
|
|
155
|
+
return file
|
|
156
|
+
}
|
|
157
|
+
return filepath.Join(cwd, file)
|
|
158
|
+
}
|