ttsc 0.12.3 → 0.12.4
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/cmd/platform/main.go +9 -0
- package/cmd/ttsc/api_compile.go +109 -101
- package/cmd/ttsc/api_compile_branches_test.go +55 -57
- package/cmd/ttsc/api_transform.go +54 -49
- package/cmd/ttsc/api_transform_branches_test.go +30 -32
- package/cmd/ttsc/build.go +111 -108
- package/cmd/ttsc/build_branches_test.go +95 -97
- package/cmd/ttsc/helpers_test.go +39 -28
- package/cmd/ttsc/main.go +111 -104
- package/cmd/ttsc/main_alias_branches_test.go +21 -23
- package/cmd/ttscserver/main.go +96 -90
- package/cmd/ttscserver/run_lsp_prefers_tsgo_flag_test.go +22 -22
- package/cmd/ttscserver/run_lsp_reports_server_error_test.go +22 -22
- package/cmd/ttscserver/run_lsp_uses_tsgo_environment_test.go +23 -23
- package/cmd/utility-host/main.go +30 -23
- package/driver/lsp.go +53 -7
- package/driver/plugins.go +88 -72
- package/driver/program.go +362 -339
- package/driver/rewrite.go +442 -380
- package/internal/lspserver/lsp_envelope.go +64 -64
- package/internal/lspserver/lsp_frame.go +60 -60
- package/internal/lspserver/lsp_plugin.go +57 -57
- package/internal/lspserver/lsp_proxy.go +265 -251
- package/internal/lspserver/lsp_server.go +155 -144
- package/lib/TtscCompiler.d.ts +7 -0
- package/lib/TtscCompiler.js +7 -0
- package/lib/TtscCompiler.js.map +1 -1
- package/lib/compiler/internal/buildNativeCompiler.d.ts +11 -1
- package/lib/compiler/internal/buildNativeCompiler.js +23 -3
- package/lib/compiler/internal/buildNativeCompiler.js.map +1 -1
- package/lib/compiler/internal/compileProjectInMemory.d.ts +9 -0
- package/lib/compiler/internal/compileProjectInMemory.js +36 -21
- package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
- package/lib/compiler/internal/paths.d.ts +27 -0
- package/lib/compiler/internal/paths.js +70 -0
- package/lib/compiler/internal/paths.js.map +1 -0
- package/lib/compiler/internal/project/readProjectConfig.d.ts +9 -1
- package/lib/compiler/internal/project/readProjectConfig.js +80 -1
- package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
- package/lib/compiler/internal/project/resolveProjectConfig.d.ts +13 -1
- package/lib/compiler/internal/project/resolveProjectConfig.js +24 -1
- package/lib/compiler/internal/project/resolveProjectConfig.js.map +1 -1
- package/lib/compiler/internal/resolveBinary.d.ts +9 -1
- package/lib/compiler/internal/resolveBinary.js +17 -1
- package/lib/compiler/internal/resolveBinary.js.map +1 -1
- package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +16 -1
- package/lib/compiler/internal/resolveEmittedJavaScript.js +42 -10
- package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
- package/lib/compiler/internal/resolveTsgo.d.ts +21 -1
- package/lib/compiler/internal/resolveTsgo.js +23 -1
- package/lib/compiler/internal/resolveTsgo.js.map +1 -1
- package/lib/compiler/internal/runBuild.d.ts +19 -0
- package/lib/compiler/internal/runBuild.js +112 -46
- package/lib/compiler/internal/runBuild.js.map +1 -1
- package/lib/compiler/internal/runSingleFileEmit.d.ts +10 -1
- package/lib/compiler/internal/runSingleFileEmit.js +14 -1
- package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
- package/lib/compiler/internal/sharedHostHelpers.d.ts +5 -0
- package/lib/compiler/internal/sharedHostHelpers.js +6 -2
- package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
- package/lib/compiler/internal/spawnNative.d.ts +21 -0
- package/lib/compiler/internal/spawnNative.js +59 -0
- package/lib/compiler/internal/spawnNative.js.map +1 -0
- package/lib/compiler/internal/transformProjectInMemory.d.ts +15 -1
- package/lib/compiler/internal/transformProjectInMemory.js +57 -61
- package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
- package/lib/launcher/internal/getCompilerVersionText.js +7 -0
- package/lib/launcher/internal/getCompilerVersionText.js.map +1 -1
- package/lib/launcher/internal/prepareExecution.js +32 -0
- package/lib/launcher/internal/prepareExecution.js.map +1 -1
- package/lib/launcher/internal/resolveCacheDir.d.ts +9 -0
- package/lib/launcher/internal/resolveCacheDir.js +22 -0
- package/lib/launcher/internal/resolveCacheDir.js.map +1 -0
- package/lib/launcher/internal/resolveTtscserverBinary.js +2 -0
- package/lib/launcher/internal/resolveTtscserverBinary.js.map +1 -1
- package/lib/launcher/internal/runTtsc.d.ts +10 -0
- package/lib/launcher/internal/runTtsc.js +22 -9
- package/lib/launcher/internal/runTtsc.js.map +1 -1
- package/lib/launcher/internal/runTtscserver.js +10 -35
- package/lib/launcher/internal/runTtscserver.js.map +1 -1
- package/lib/launcher/internal/runTtsx.d.ts +8 -0
- package/lib/launcher/internal/runTtsx.js +33 -7
- package/lib/launcher/internal/runTtsx.js.map +1 -1
- package/lib/plugin/internal/buildSourcePlugin.d.ts +38 -0
- package/lib/plugin/internal/buildSourcePlugin.js +40 -17
- package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
- package/lib/plugin/internal/loadProjectPlugins.d.ts +26 -0
- package/lib/plugin/internal/loadProjectPlugins.js +32 -18
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/lib/structures/ITtscCompilerTransformation.d.ts +15 -2
- package/lib/structures/internal/TtscSingleFileEmitOptions.d.ts +0 -2
- package/package.json +9 -9
- package/shim/ast/shim.go +24 -0
- package/shim/checker/shim.go +35 -0
- package/shim/core/shim.go +17 -0
- package/shim/diagnosticwriter/shim.go +4 -0
- package/shim/lsp/shim.go +3 -3
- package/shim/printer/shim.go +20 -0
- package/src/TtscCompiler.ts +7 -0
- package/src/compiler/internal/buildNativeCompiler.ts +23 -3
- package/src/compiler/internal/compileProjectInMemory.ts +34 -25
- package/src/compiler/internal/paths.ts +68 -0
- package/src/compiler/internal/project/readProjectConfig.ts +86 -1
- package/src/compiler/internal/project/resolveProjectConfig.ts +24 -1
- package/src/compiler/internal/resolveBinary.ts +17 -1
- package/src/compiler/internal/resolveEmittedJavaScript.ts +43 -12
- package/src/compiler/internal/resolveTsgo.ts +29 -1
- package/src/compiler/internal/runBuild.ts +110 -55
- package/src/compiler/internal/runSingleFileEmit.ts +14 -1
- package/src/compiler/internal/sharedHostHelpers.ts +7 -7
- package/src/compiler/internal/spawnNative.ts +64 -0
- package/src/compiler/internal/transformProjectInMemory.ts +47 -67
- package/src/launcher/internal/getCompilerVersionText.ts +7 -0
- package/src/launcher/internal/prepareExecution.ts +32 -0
- package/src/launcher/internal/resolveCacheDir.ts +19 -0
- package/src/launcher/internal/resolveTtscserverBinary.ts +2 -0
- package/src/launcher/internal/runTtsc.ts +22 -10
- package/src/launcher/internal/runTtscserver.ts +9 -1
- package/src/launcher/internal/runTtsx.ts +33 -8
- package/src/plugin/internal/buildSourcePlugin.ts +40 -14
- package/src/plugin/internal/loadProjectPlugins.ts +42 -18
- package/src/structures/ITtscCompilerTransformation.ts +15 -2
- package/src/structures/ITtscPlugin.ts +0 -1
- package/src/structures/internal/TtscSingleFileEmitOptions.ts +0 -2
- package/tools/gen_shims/main.go +43 -8
- package/utility/host.go +294 -245
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Spawn a native binary (or a Node.js script when the path has a JS/TS
|
|
6
|
+
* extension) with the given args and return the `spawnSync` result. Sets a
|
|
7
|
+
* generous 256 MiB output buffer so large projects do not truncate.
|
|
8
|
+
*
|
|
9
|
+
* When `options.encoding` is omitted it defaults to `"utf8"`. Pass an explicit
|
|
10
|
+
* value when the caller needs a different encoding or raw `Buffer` output.
|
|
11
|
+
*/
|
|
12
|
+
export function spawnNative(
|
|
13
|
+
binary: string,
|
|
14
|
+
args: readonly string[],
|
|
15
|
+
options: {
|
|
16
|
+
cwd?: string;
|
|
17
|
+
env?: NodeJS.ProcessEnv;
|
|
18
|
+
encoding?: BufferEncoding;
|
|
19
|
+
},
|
|
20
|
+
) {
|
|
21
|
+
const viaNode = /\.(?:[cm]?js|ts)$/i.test(binary);
|
|
22
|
+
if (!viaNode) {
|
|
23
|
+
ensureExecutable(binary);
|
|
24
|
+
}
|
|
25
|
+
return spawnSync(
|
|
26
|
+
viaNode ? process.execPath : binary,
|
|
27
|
+
viaNode ? [binary, ...args] : [...args],
|
|
28
|
+
{
|
|
29
|
+
cwd: options.cwd,
|
|
30
|
+
encoding: options.encoding ?? "utf8",
|
|
31
|
+
env: options.env,
|
|
32
|
+
maxBuffer: 1024 * 1024 * 256,
|
|
33
|
+
windowsHide: true,
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Ensure the binary has the executable bit set on POSIX systems. Silently skips
|
|
40
|
+
* on Windows and swallows `chmod` errors to let the original spawn error
|
|
41
|
+
* surface instead of masking it with a permission error.
|
|
42
|
+
*/
|
|
43
|
+
export function ensureExecutable(binary: string): void {
|
|
44
|
+
if (process.platform === "win32") {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const mode = fs.statSync(binary).mode & 0o777;
|
|
49
|
+
if ((mode & 0o111) !== 0) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
fs.chmodSync(binary, mode | 0o755);
|
|
53
|
+
} catch {
|
|
54
|
+
/* keep the original spawn error path */
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Coerce a `spawnSync` output value to a plain string, defaulting to `""`. */
|
|
59
|
+
export function outputText(value: string | Buffer | null | undefined): string {
|
|
60
|
+
if (value == null) {
|
|
61
|
+
return "";
|
|
62
|
+
}
|
|
63
|
+
return typeof value === "string" ? value : value.toString("utf8");
|
|
64
|
+
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
|
-
import fs from "node:fs";
|
|
3
1
|
import path from "node:path";
|
|
4
2
|
|
|
5
3
|
import {
|
|
@@ -12,6 +10,7 @@ import type { ITtscLoadedNativePlugin } from "../../structures/internal/ITtscLoa
|
|
|
12
10
|
import type { ITtscParsedProjectConfig } from "../../structures/internal/ITtscParsedProjectConfig";
|
|
13
11
|
import type { TtscBuildResult } from "../../structures/internal/TtscBuildResult";
|
|
14
12
|
import { buildNativeCompiler } from "./buildNativeCompiler";
|
|
13
|
+
import { packageRootDir } from "./paths";
|
|
15
14
|
import { readProjectConfig } from "./project/readProjectConfig";
|
|
16
15
|
import { resolveBinary } from "./resolveBinary";
|
|
17
16
|
import { resolveTsgo } from "./resolveTsgo";
|
|
@@ -21,8 +20,23 @@ import {
|
|
|
21
20
|
linkedTransformPlugins,
|
|
22
21
|
selectSharedHostPlugin,
|
|
23
22
|
} from "./sharedHostHelpers";
|
|
23
|
+
import { outputText, spawnNative } from "./spawnNative";
|
|
24
24
|
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Transform a project and capture TypeScript source output in memory.
|
|
27
|
+
*
|
|
28
|
+
* When no plugins are configured the fast path spawns the native ttsc compiler
|
|
29
|
+
* host (`cmd/ttsc api-transform`) which returns a JSON map of transformed
|
|
30
|
+
* TypeScript sources. When plugins are present:
|
|
31
|
+
*
|
|
32
|
+
* 1. Check-stage plugins run first and abort on failure.
|
|
33
|
+
* 2. If there are no transform-stage plugins the host is used as the transformer.
|
|
34
|
+
* 3. If transform plugins exist they are dispatched through the shared-host binary
|
|
35
|
+
* with linked plugins passed via `TTSC_LINKED_PLUGINS_JSON`.
|
|
36
|
+
*
|
|
37
|
+
* @returns A `{ result, typescript }` pair where `typescript` maps output paths
|
|
38
|
+
* to their transformed TypeScript source text.
|
|
39
|
+
*/
|
|
26
40
|
export function transformProjectInMemory(options: ITtscCompilerContext): {
|
|
27
41
|
result: TtscBuildResult;
|
|
28
42
|
typescript: Record<string, string>;
|
|
@@ -39,6 +53,7 @@ export function transformProjectInMemory(options: ITtscCompilerContext): {
|
|
|
39
53
|
return transformProjectWithNativeHost(options, project);
|
|
40
54
|
}
|
|
41
55
|
|
|
56
|
+
/** Return true when the project or the call-level options declare any plugins. */
|
|
42
57
|
function hasConfiguredPlugins(
|
|
43
58
|
options: ITtscCompilerContext,
|
|
44
59
|
project: ITtscParsedProjectConfig,
|
|
@@ -46,6 +61,11 @@ function hasConfiguredPlugins(
|
|
|
46
61
|
return hasProjectPluginEntries(project, options.plugins);
|
|
47
62
|
}
|
|
48
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Transform via the built-in native compiler host (`cmd/ttsc api-transform`).
|
|
66
|
+
* Used when no user plugins are configured, or as the fallback transformer when
|
|
67
|
+
* check-stage plugins pass and no transform-stage plugins are declared.
|
|
68
|
+
*/
|
|
49
69
|
function transformProjectWithNativeHost(
|
|
50
70
|
options: ITtscCompilerContext,
|
|
51
71
|
project: ITtscParsedProjectConfig,
|
|
@@ -160,6 +180,10 @@ function transformProjectWithPlugins(
|
|
|
160
180
|
};
|
|
161
181
|
}
|
|
162
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Run every check-stage plugin in sequence, short-circuiting on the first
|
|
185
|
+
* failure. Returns the aggregated `TtscBuildResult` (status 0 when all pass).
|
|
186
|
+
*/
|
|
163
187
|
function runNativeChecks(
|
|
164
188
|
options: ITtscCompilerContext,
|
|
165
189
|
project: ITtscParsedProjectConfig,
|
|
@@ -204,6 +228,7 @@ function runNativeChecks(
|
|
|
204
228
|
return result;
|
|
205
229
|
}
|
|
206
230
|
|
|
231
|
+
/** Build the CLI argument list for the `transform` subcommand. */
|
|
207
232
|
function createNativeTransformArgs(
|
|
208
233
|
project: ITtscParsedProjectConfig,
|
|
209
234
|
plugins: readonly ITtscLoadedNativePlugin[],
|
|
@@ -216,6 +241,7 @@ function createNativeTransformArgs(
|
|
|
216
241
|
];
|
|
217
242
|
}
|
|
218
243
|
|
|
244
|
+
/** Build the CLI argument list for the `check` subcommand. */
|
|
219
245
|
function createNativeCheckArgs(
|
|
220
246
|
project: ITtscParsedProjectConfig,
|
|
221
247
|
plugins: readonly ITtscLoadedNativePlugin[],
|
|
@@ -228,6 +254,10 @@ function createNativeCheckArgs(
|
|
|
228
254
|
];
|
|
229
255
|
}
|
|
230
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Serialize the plugin list to a JSON string for `--plugins-json=`. Only the
|
|
259
|
+
* fields the native binary needs are included to keep the arg short.
|
|
260
|
+
*/
|
|
231
261
|
function serializeNativePlugins(
|
|
232
262
|
plugins: readonly ITtscLoadedNativePlugin[],
|
|
233
263
|
): string {
|
|
@@ -240,6 +270,12 @@ function serializeNativePlugins(
|
|
|
240
270
|
);
|
|
241
271
|
}
|
|
242
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Build the environment for a native plugin spawn. Injects `TTSC_NODE_BINARY`,
|
|
275
|
+
* `TTSC_TSGO_BINARY`, and `TTSC_TTSX_BINARY` so the sidecar can re-invoke
|
|
276
|
+
* Node.js or tsgo without searching PATH. For transform plugins, also passes
|
|
277
|
+
* `TTSC_LINKED_PLUGINS_JSON` when linked sources are present.
|
|
278
|
+
*/
|
|
243
279
|
function nativePluginEnv(
|
|
244
280
|
options: ITtscCompilerContext,
|
|
245
281
|
projectRoot: string,
|
|
@@ -265,53 +301,13 @@ function nativePluginEnv(
|
|
|
265
301
|
return env;
|
|
266
302
|
}
|
|
267
303
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
) {
|
|
276
|
-
const viaNode = /\.(?:[cm]?js|ts)$/i.test(binary);
|
|
277
|
-
if (!viaNode) {
|
|
278
|
-
ensureExecutable(binary);
|
|
279
|
-
}
|
|
280
|
-
return spawnSync(
|
|
281
|
-
viaNode ? process.execPath : binary,
|
|
282
|
-
viaNode ? [binary, ...args] : [...args],
|
|
283
|
-
{
|
|
284
|
-
cwd: options.cwd,
|
|
285
|
-
encoding: "utf8",
|
|
286
|
-
env: options.env,
|
|
287
|
-
maxBuffer: 1024 * 1024 * 256,
|
|
288
|
-
windowsHide: true,
|
|
289
|
-
},
|
|
290
|
-
);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function ensureExecutable(binary: string): void {
|
|
294
|
-
if (process.platform === "win32") {
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
try {
|
|
298
|
-
const mode = fs.statSync(binary).mode & 0o777;
|
|
299
|
-
if ((mode & 0o111) !== 0) {
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
fs.chmodSync(binary, mode | 0o755);
|
|
303
|
-
} catch {
|
|
304
|
-
/* keep the original spawn error path */
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function outputText(value: string | Buffer | null | undefined): string {
|
|
309
|
-
if (value == null) {
|
|
310
|
-
return "";
|
|
311
|
-
}
|
|
312
|
-
return typeof value === "string" ? value : value.toString("utf8");
|
|
313
|
-
}
|
|
314
|
-
|
|
304
|
+
/**
|
|
305
|
+
* Parse the JSON envelope written by the native transform host to stdout.
|
|
306
|
+
*
|
|
307
|
+
* The `typescript` field must be a `Record<string, string>`. Any other shape is
|
|
308
|
+
* treated as a protocol error and throws with the stderr/stdout context. JSON
|
|
309
|
+
* parse errors are also wrapped with the same context message.
|
|
310
|
+
*/
|
|
315
311
|
function parseNativeTransformOutput(
|
|
316
312
|
stdout: string,
|
|
317
313
|
stderr: string,
|
|
@@ -344,6 +340,7 @@ function parseNativeTransformOutput(
|
|
|
344
340
|
}
|
|
345
341
|
}
|
|
346
342
|
|
|
343
|
+
/** Type guard: true when `value` is a non-null, non-array object of strings. */
|
|
347
344
|
function isTextRecord(value: unknown): value is Record<string, string> {
|
|
348
345
|
return (
|
|
349
346
|
typeof value === "object" &&
|
|
@@ -352,20 +349,3 @@ function isTextRecord(value: unknown): value is Record<string, string> {
|
|
|
352
349
|
Object.values(value).every((entry) => typeof entry === "string")
|
|
353
350
|
);
|
|
354
351
|
}
|
|
355
|
-
|
|
356
|
-
function packageRootDir(): string {
|
|
357
|
-
let current = path.resolve(__dirname);
|
|
358
|
-
while (true) {
|
|
359
|
-
if (
|
|
360
|
-
fs.existsSync(path.join(current, "package.json")) &&
|
|
361
|
-
fs.existsSync(path.join(current, "go.mod"))
|
|
362
|
-
) {
|
|
363
|
-
return fs.realpathSync.native?.(current) ?? fs.realpathSync(current);
|
|
364
|
-
}
|
|
365
|
-
const parent = path.dirname(current);
|
|
366
|
-
if (parent === current) {
|
|
367
|
-
throw new Error("ttsc: package root not found for native compiler build");
|
|
368
|
-
}
|
|
369
|
-
current = parent;
|
|
370
|
-
}
|
|
371
|
-
}
|
|
@@ -23,6 +23,7 @@ export function getCompilerVersionText(
|
|
|
23
23
|
return `ttsc ${readOwnPackageVersion()} (${outputText(res.stdout).trim()})`;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/** Coerce a spawnSync stdio field (string | Buffer | null) to a plain string. */
|
|
26
27
|
function outputText(value: string | Buffer | null | undefined): string {
|
|
27
28
|
if (value == null) {
|
|
28
29
|
return "";
|
|
@@ -30,6 +31,12 @@ function outputText(value: string | Buffer | null | undefined): string {
|
|
|
30
31
|
return typeof value === "string" ? value : value.toString("utf8");
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Read the `version` field from the `@ttsc/ttsc` package.json that sits three
|
|
36
|
+
* directories above the compiled launcher output (`lib/launcher/internal/`).
|
|
37
|
+
* Returns `"0.0.0"` on any I/O or parse failure so the banner is always safe to
|
|
38
|
+
* display.
|
|
39
|
+
*/
|
|
33
40
|
function readOwnPackageVersion(): string {
|
|
34
41
|
try {
|
|
35
42
|
const file = path.resolve(__dirname, "..", "..", "..", "package.json");
|
|
@@ -7,7 +7,14 @@ import { resolveEmittedJavaScript } from "../../compiler/internal/resolveEmitted
|
|
|
7
7
|
import { runBuild } from "../../compiler/internal/runBuild";
|
|
8
8
|
import type { TtscCommonOptions } from "../../structures/internal/TtscCommonOptions";
|
|
9
9
|
|
|
10
|
+
/** Subdirectory name that isolates concurrent ttsx processes by PID. */
|
|
10
11
|
const PROCESS_CACHE_KEY = String(process.pid);
|
|
12
|
+
/**
|
|
13
|
+
* Maximum number of ancestor directories above the project root that the
|
|
14
|
+
* virtual filesystem overlay mirrors. Three levels covers the common monorepo
|
|
15
|
+
* layout (workspace-root → packages → package-root) so `node_modules` symlinks
|
|
16
|
+
* resolve correctly without reaching an unsafe boundary.
|
|
17
|
+
*/
|
|
11
18
|
const MAX_VIRTUAL_PARENT_DEPTH = 3;
|
|
12
19
|
|
|
13
20
|
/** Build the owning project and locate the emitted JavaScript entry for `ttsx`. */
|
|
@@ -162,6 +169,7 @@ function linkVirtualEntry(
|
|
|
162
169
|
entry: fs.Dirent,
|
|
163
170
|
): void {
|
|
164
171
|
if (entry.isDirectory()) {
|
|
172
|
+
// Use junction points on Windows; plain symlinks elsewhere.
|
|
165
173
|
fs.symlinkSync(
|
|
166
174
|
realEntry,
|
|
167
175
|
virtualEntry,
|
|
@@ -171,15 +179,25 @@ function linkVirtualEntry(
|
|
|
171
179
|
}
|
|
172
180
|
if (entry.isFile()) {
|
|
173
181
|
try {
|
|
182
|
+
// Hard-link first: cheap, preserves inode, no extra disk usage.
|
|
174
183
|
fs.linkSync(realEntry, virtualEntry);
|
|
175
184
|
} catch {
|
|
185
|
+
// Cross-device or unsupported filesystem: fall back to a full copy.
|
|
176
186
|
fs.copyFileSync(realEntry, virtualEntry);
|
|
177
187
|
}
|
|
178
188
|
return;
|
|
179
189
|
}
|
|
190
|
+
// Symlinks (and other special entries) are re-symlinked as-is.
|
|
180
191
|
fs.symlinkSync(realEntry, virtualEntry);
|
|
181
192
|
}
|
|
182
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Walk from `projectRoot` upward (up to `MAX_VIRTUAL_PARENT_DEPTH` steps),
|
|
196
|
+
* stopping early at a workspace root (`pnpm-workspace.yaml` or `.git`). The
|
|
197
|
+
* collected directories are reversed so callers can iterate outermost-first,
|
|
198
|
+
* which lets inner symlinks override outer ones without conflicting mkdir
|
|
199
|
+
* calls.
|
|
200
|
+
*/
|
|
183
201
|
function collectLinkDirectories(projectRoot: string): string[] {
|
|
184
202
|
const out: string[] = [];
|
|
185
203
|
let current = projectRoot;
|
|
@@ -207,6 +225,14 @@ function isUnsafeVirtualParent(directory: string): boolean {
|
|
|
207
225
|
return resolved === root || resolved === path.resolve(os.tmpdir());
|
|
208
226
|
}
|
|
209
227
|
|
|
228
|
+
/**
|
|
229
|
+
* Map an absolute path into a stable, filesystem-safe subtree under `root`.
|
|
230
|
+
*
|
|
231
|
+
* On POSIX the root is always `/`, so every path shares the same prefix —
|
|
232
|
+
* represented here as `"posix"`. On Windows, drive letters and UNC roots each
|
|
233
|
+
* get a sanitized label (e.g. `"C_"` for `C:\`), preventing collisions between
|
|
234
|
+
* paths from different drives inside the same virtual root.
|
|
235
|
+
*/
|
|
210
236
|
function virtualPath(root: string, absolute: string): string {
|
|
211
237
|
const parsed = path.parse(path.resolve(absolute));
|
|
212
238
|
const label =
|
|
@@ -218,6 +244,12 @@ function virtualPath(root: string, absolute: string): string {
|
|
|
218
244
|
return path.join(root, label, relative);
|
|
219
245
|
}
|
|
220
246
|
|
|
247
|
+
/**
|
|
248
|
+
* Heuristic: classify emitted JS as ESM when it contains top-level `import` or
|
|
249
|
+
* `export` statements but none of the well-known CJS patterns. The CJS checks
|
|
250
|
+
* run first so that re-exported CJS bundles with both `require` calls and an
|
|
251
|
+
* `export` declaration are conservatively treated as CJS.
|
|
252
|
+
*/
|
|
221
253
|
function looksLikeESM(output: string): boolean {
|
|
222
254
|
if (
|
|
223
255
|
/\bObject\.defineProperty\(exports\b/.test(output) ||
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve a user-supplied `--cache-dir` value into an absolute path.
|
|
5
|
+
*
|
|
6
|
+
* When `cacheDir` is absent or empty, returns `undefined` so callers fall
|
|
7
|
+
* through to the environment variable and global-cache defaults handled inside
|
|
8
|
+
* `buildSourcePlugin`. When `cacheDir` is already absolute it is returned
|
|
9
|
+
* unchanged; otherwise it is resolved relative to `cwd`.
|
|
10
|
+
*/
|
|
11
|
+
export function resolveCacheDir(
|
|
12
|
+
cwd: string,
|
|
13
|
+
cacheDir?: string,
|
|
14
|
+
): string | undefined {
|
|
15
|
+
if (!cacheDir) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
return path.isAbsolute(cacheDir) ? cacheDir : path.resolve(cwd, cacheDir);
|
|
19
|
+
}
|
|
@@ -47,5 +47,7 @@ function defaultLocalBinaryPath(): string | null {
|
|
|
47
47
|
|
|
48
48
|
function packageRootDir(): string {
|
|
49
49
|
const moduleDir = path.resolve(__dirname, "..", "..");
|
|
50
|
+
// Prefer the faster native variant; fall back to the JS implementation on
|
|
51
|
+
// platforms (or environments) where `realpathSync.native` is unavailable.
|
|
50
52
|
return fs.realpathSync.native?.(moduleDir) ?? fs.realpathSync(moduleDir);
|
|
51
53
|
}
|
|
@@ -11,7 +11,18 @@ import { runSingleFileEmit } from "../../compiler/internal/runSingleFileEmit";
|
|
|
11
11
|
import { defaultPluginCacheCleanTargets } from "../../plugin/internal/buildSourcePlugin";
|
|
12
12
|
import type { TtscBuildOptions } from "../../structures/internal/TtscBuildOptions";
|
|
13
13
|
import { getCompilerVersionText } from "./getCompilerVersionText";
|
|
14
|
-
|
|
14
|
+
import { resolveCacheDir } from "./resolveCacheDir";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* CLI entry point for `ttsc`. Dispatches argv to the appropriate build lane
|
|
18
|
+
* (build, check, fix, format, prepare, clean, or native-delegate) and returns
|
|
19
|
+
* an exit code. Errors thrown by any lane are caught here and written to stderr
|
|
20
|
+
* so the process can exit cleanly.
|
|
21
|
+
*
|
|
22
|
+
* @param argv - Command-line arguments (defaults to `process.argv.slice(2)`).
|
|
23
|
+
* @returns The exit code: `0` on success, `1` on binary-not-found, `2` on user
|
|
24
|
+
* error or build failure.
|
|
25
|
+
*/
|
|
15
26
|
export function runTtsc(
|
|
16
27
|
argv: readonly string[] = process.argv.slice(2),
|
|
17
28
|
): number {
|
|
@@ -67,6 +78,12 @@ export function runTtsc(
|
|
|
67
78
|
}
|
|
68
79
|
}
|
|
69
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Return `true` when `command` looks like a build argument rather than a
|
|
83
|
+
* subcommand name — a flag (`-p`, `--watch`, …) or a TypeScript/config file
|
|
84
|
+
* path. These are forwarded to the build lane unchanged so users can write
|
|
85
|
+
* `ttsc -p tsconfig.json` without an explicit `build` subcommand.
|
|
86
|
+
*/
|
|
70
87
|
function isBuildAlias(command: string): boolean {
|
|
71
88
|
if (command.startsWith("-")) return true;
|
|
72
89
|
return [".json", ".ts", ".tsx", ".mts", ".cts"].some((ext) =>
|
|
@@ -362,6 +379,8 @@ function parseBuildArgs(argv: readonly string[], checkOnly: boolean) {
|
|
|
362
379
|
} else if (current.startsWith("--outDir=")) {
|
|
363
380
|
outDir = current.slice("--outDir=".length);
|
|
364
381
|
} else if (current === "-w") {
|
|
382
|
+
// Unreachable: `-w` is handled by the switch case above. Kept here
|
|
383
|
+
// as a guard in case the switch is refactored in the future.
|
|
365
384
|
watch = true;
|
|
366
385
|
} else if (current.startsWith("--tsconfig=")) {
|
|
367
386
|
tsconfig = current.slice("--tsconfig=".length);
|
|
@@ -375,6 +394,7 @@ function parseBuildArgs(argv: readonly string[], checkOnly: boolean) {
|
|
|
375
394
|
} else if (current.startsWith("--cache-dir=")) {
|
|
376
395
|
cacheDir = current.slice("--cache-dir=".length);
|
|
377
396
|
} else if (current === "--verbose") {
|
|
397
|
+
// Unreachable: `--verbose` is handled by the switch case above.
|
|
378
398
|
quiet = false;
|
|
379
399
|
} else if (current.startsWith("-")) {
|
|
380
400
|
throw new Error(`ttsc: unknown option ${current}`);
|
|
@@ -588,8 +608,7 @@ function runWatch(
|
|
|
588
608
|
tsconfig: options.tsconfig,
|
|
589
609
|
}),
|
|
590
610
|
);
|
|
591
|
-
const
|
|
592
|
-
const directories = collectWatchDirectories(watchRoot);
|
|
611
|
+
const directories = collectWatchDirectories(root);
|
|
593
612
|
const watchers = directories.map((dir) =>
|
|
594
613
|
fs.watch(dir, { persistent: true }, () => trigger()),
|
|
595
614
|
);
|
|
@@ -652,13 +671,6 @@ function runWatch(
|
|
|
652
671
|
return 0;
|
|
653
672
|
}
|
|
654
673
|
|
|
655
|
-
function resolveCacheDir(cwd: string, cacheDir?: string): string | undefined {
|
|
656
|
-
if (!cacheDir) {
|
|
657
|
-
return undefined;
|
|
658
|
-
}
|
|
659
|
-
return path.isAbsolute(cacheDir) ? cacheDir : path.resolve(cwd, cacheDir);
|
|
660
|
-
}
|
|
661
|
-
|
|
662
674
|
function collectWatchDirectories(root: string): string[] {
|
|
663
675
|
const out: string[] = [];
|
|
664
676
|
const stack: string[] = [root];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
-
import
|
|
3
|
+
import os from "node:os";
|
|
4
4
|
|
|
5
5
|
import { resolveTsgo } from "../../compiler/internal/resolveTsgo";
|
|
6
6
|
import { resolveTtscserverBinary } from "./resolveTtscserverBinary";
|
|
@@ -66,8 +66,16 @@ export function runTtscserver(
|
|
|
66
66
|
return result.status ?? 1;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Build the environment for the native binary. When running in `--stdio` (LSP)
|
|
71
|
+
* mode the Go binary needs to know which tsgo binary to wrap; inject
|
|
72
|
+
* `TTSC_TSGO_BINARY` so the native host does not have to re-resolve it from
|
|
73
|
+
* inside a potentially different working directory. Skip injection when the
|
|
74
|
+
* caller already provided the variable or passed an explicit `--tsgo` option.
|
|
75
|
+
*/
|
|
69
76
|
function resolveTtscserverEnv(argv: readonly string[]): NodeJS.ProcessEnv {
|
|
70
77
|
if (!argv.includes("--stdio")) {
|
|
78
|
+
// Non-LSP invocations (--version, --help) do not shell out to tsgo.
|
|
71
79
|
return process.env;
|
|
72
80
|
}
|
|
73
81
|
if (process.env.TTSC_TSGO_BINARY || hasTsgoOption(argv)) {
|
|
@@ -4,7 +4,16 @@ import path from "node:path";
|
|
|
4
4
|
|
|
5
5
|
import { getCompilerVersionText } from "./getCompilerVersionText";
|
|
6
6
|
import { prepareExecution } from "./prepareExecution";
|
|
7
|
-
|
|
7
|
+
import { resolveCacheDir } from "./resolveCacheDir";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* CLI entry point for `ttsx`. Type-checks the owning project via tsgo, emits
|
|
11
|
+
* JavaScript to a PID-isolated temp directory, rewrites ESM specifiers when
|
|
12
|
+
* needed, and executes the compiled entry with the current Node.js runtime.
|
|
13
|
+
*
|
|
14
|
+
* @param argv - Command-line arguments (defaults to `process.argv.slice(2)`).
|
|
15
|
+
* @returns The child-process exit code, or `2` on a ttsx-level error.
|
|
16
|
+
*/
|
|
8
17
|
export function runTtsx(
|
|
9
18
|
argv: readonly string[] = process.argv.slice(2),
|
|
10
19
|
): number {
|
|
@@ -172,13 +181,6 @@ function isRelativeSpecifier(specifier: string): boolean {
|
|
|
172
181
|
);
|
|
173
182
|
}
|
|
174
183
|
|
|
175
|
-
function resolveCacheDir(cwd: string, cacheDir?: string): string | undefined {
|
|
176
|
-
if (!cacheDir) {
|
|
177
|
-
return undefined;
|
|
178
|
-
}
|
|
179
|
-
return path.isAbsolute(cacheDir) ? cacheDir : path.resolve(cwd, cacheDir);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
184
|
function takeValue(flag: string, rest: string[]): string {
|
|
183
185
|
const value = rest.shift();
|
|
184
186
|
if (!value) {
|
|
@@ -222,6 +224,12 @@ function runPreparedEntry(
|
|
|
222
224
|
return result.status ?? 1;
|
|
223
225
|
}
|
|
224
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Walk every `.js`/`.mjs`/`.cjs` file in `root` and rewrite bare relative
|
|
229
|
+
* specifiers (e.g. `"./foo"`) to include the resolved file extension so Node
|
|
230
|
+
* can load them with `--input-type=module`. Files that did not change are not
|
|
231
|
+
* written back.
|
|
232
|
+
*/
|
|
225
233
|
function rewriteEsmSpecifiers(root: string): void {
|
|
226
234
|
const stack = [root];
|
|
227
235
|
while (stack.length !== 0) {
|
|
@@ -431,26 +439,37 @@ function findTemplateExpressionEnd(
|
|
|
431
439
|
return null;
|
|
432
440
|
}
|
|
433
441
|
|
|
442
|
+
/**
|
|
443
|
+
* Append or fix a file extension on a relative specifier so that Node's ESM
|
|
444
|
+
* loader can resolve it. The resolution order mirrors Node's own algorithm:
|
|
445
|
+
* exact file match → directory index → `.js` fallback. Non-relative specifiers
|
|
446
|
+
* and already-extensioned paths are returned unchanged.
|
|
447
|
+
*/
|
|
434
448
|
function withResolvableExtension(fromFile: string, specifier: string): string {
|
|
435
449
|
if (!specifier.startsWith(".")) {
|
|
450
|
+
// Bare specifiers (packages, builtins) need no rewriting.
|
|
436
451
|
return specifier;
|
|
437
452
|
}
|
|
438
453
|
if (/\.(?:[cm]?js|json|node)$/i.test(specifier)) {
|
|
454
|
+
// Already has a concrete extension the loader understands.
|
|
439
455
|
return specifier;
|
|
440
456
|
}
|
|
441
457
|
const [pathname, suffix = ""] = splitSpecifierSuffix(specifier);
|
|
442
458
|
const fromDir = path.dirname(fromFile);
|
|
459
|
+
// 1. Try the specifier as a file path with each JS extension.
|
|
443
460
|
for (const extension of [".js", ".mjs", ".cjs"]) {
|
|
444
461
|
if (fs.existsSync(path.resolve(fromDir, pathname + extension))) {
|
|
445
462
|
return pathname + extension + suffix;
|
|
446
463
|
}
|
|
447
464
|
}
|
|
465
|
+
// 2. Try interpreting it as a directory with an index file.
|
|
448
466
|
const directory = pathname.replace(/\/+$/, "");
|
|
449
467
|
for (const extension of [".js", ".mjs", ".cjs"]) {
|
|
450
468
|
if (fs.existsSync(path.resolve(fromDir, directory, "index" + extension))) {
|
|
451
469
|
return `${directory}/index${extension}${suffix}`;
|
|
452
470
|
}
|
|
453
471
|
}
|
|
472
|
+
// 3. Last resort: append `.js` and let Node surface the error if it's wrong.
|
|
454
473
|
return `${specifier}.js`;
|
|
455
474
|
}
|
|
456
475
|
|
|
@@ -553,6 +572,12 @@ function skipRegex(input: string, start: number): number {
|
|
|
553
572
|
return input.length;
|
|
554
573
|
}
|
|
555
574
|
|
|
575
|
+
/**
|
|
576
|
+
* Determine whether the `/` at `start` opens a regex literal rather than a
|
|
577
|
+
* division operator. This is a conservative approximation: check the preceding
|
|
578
|
+
* non-whitespace token — if it is an operator character or a keyword that must
|
|
579
|
+
* be followed by an expression, we assume regex.
|
|
580
|
+
*/
|
|
556
581
|
function looksLikeRegexStart(input: string, start: number): boolean {
|
|
557
582
|
let i = start - 1;
|
|
558
583
|
while (i >= 0 && /\s/.test(input[i]!)) {
|
|
@@ -5,6 +5,8 @@ import { createRequire } from "node:module";
|
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
|
|
8
|
+
import { findNearestGoMod } from "../../compiler/internal/paths";
|
|
9
|
+
|
|
8
10
|
const GO_MOD_SEARCH_MAX_DEPTH = 3;
|
|
9
11
|
const TTSC_GO_MODULE_PATH = "github.com/samchon/ttsc/packages/ttsc";
|
|
10
12
|
const TSGO_GO_MODULE_PATH = "github.com/microsoft/typescript-go";
|
|
@@ -397,20 +399,6 @@ function resolveSourceBuildTarget(opts: {
|
|
|
397
399
|
};
|
|
398
400
|
}
|
|
399
401
|
|
|
400
|
-
function findNearestGoMod(from: string, maxDepth: number): string | null {
|
|
401
|
-
let current = path.resolve(from);
|
|
402
|
-
let depth = 0;
|
|
403
|
-
while (true) {
|
|
404
|
-
const candidate = path.join(current, "go.mod");
|
|
405
|
-
if (fs.existsSync(candidate)) return candidate;
|
|
406
|
-
if (depth >= maxDepth) return null;
|
|
407
|
-
const parent = path.dirname(current);
|
|
408
|
-
if (parent === current) return null;
|
|
409
|
-
current = parent;
|
|
410
|
-
depth += 1;
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
402
|
function materializeScratchDir(source: string, scratch: string): void {
|
|
415
403
|
fs.mkdirSync(scratch, { recursive: true });
|
|
416
404
|
fs.cpSync(source, scratch, {
|
|
@@ -768,6 +756,19 @@ function walkForGoMod(dir: string, out: string[]): void {
|
|
|
768
756
|
}
|
|
769
757
|
}
|
|
770
758
|
|
|
759
|
+
/**
|
|
760
|
+
* Resolve the directory where compiled plugin binaries are cached.
|
|
761
|
+
*
|
|
762
|
+
* Priority order:
|
|
763
|
+
*
|
|
764
|
+
* 1. `cacheDir` option (resolved relative to `projectRoot`).
|
|
765
|
+
* 2. `TTSC_CACHE_DIR` environment variable.
|
|
766
|
+
* 3. Platform-specific global user cache (XDG / AppData / Library/Caches).
|
|
767
|
+
*
|
|
768
|
+
* When falling back to the global cache, a GC pass is triggered
|
|
769
|
+
* opportunistically to evict old or oversized entries before the cache is
|
|
770
|
+
* used.
|
|
771
|
+
*/
|
|
771
772
|
export function resolvePluginCacheRoot(
|
|
772
773
|
projectRoot: string,
|
|
773
774
|
cacheDir?: string,
|
|
@@ -783,6 +784,13 @@ export function resolvePluginCacheRoot(
|
|
|
783
784
|
return root;
|
|
784
785
|
}
|
|
785
786
|
|
|
787
|
+
/**
|
|
788
|
+
* Return the absolute path to the global plugin binary cache root.
|
|
789
|
+
*
|
|
790
|
+
* The path follows platform conventions (XDG on Linux, AppData on Windows,
|
|
791
|
+
* Library/Caches on macOS) and does not depend on project-local config. Used by
|
|
792
|
+
* `ttsc cache clean` to enumerate cache locations.
|
|
793
|
+
*/
|
|
786
794
|
export function resolveGlobalPluginCacheRoot(): string {
|
|
787
795
|
return path.join(
|
|
788
796
|
resolveUserCacheRoot(),
|
|
@@ -791,6 +799,13 @@ export function resolveGlobalPluginCacheRoot(): string {
|
|
|
791
799
|
);
|
|
792
800
|
}
|
|
793
801
|
|
|
802
|
+
/**
|
|
803
|
+
* Return all directories that `ttsc cache clean` should wipe for a project.
|
|
804
|
+
*
|
|
805
|
+
* Covers three locations where plugin binaries may accumulate: the global user
|
|
806
|
+
* cache, the project-local `node_modules/.ttsc` cache (legacy), and the
|
|
807
|
+
* project-local `.ttsc` cache.
|
|
808
|
+
*/
|
|
794
809
|
export function defaultPluginCacheCleanTargets(projectRoot: string): string[] {
|
|
795
810
|
return [
|
|
796
811
|
resolveGlobalPluginCacheRoot(),
|
|
@@ -878,6 +893,17 @@ function resolveGoCompiler(): string {
|
|
|
878
893
|
return "go";
|
|
879
894
|
}
|
|
880
895
|
|
|
896
|
+
/**
|
|
897
|
+
* Compute a deterministic SHA-256 cache key for a plugin build.
|
|
898
|
+
*
|
|
899
|
+
* The key covers every input that can produce a different binary: ttsc/tsgo
|
|
900
|
+
* versions, platform, entry package, Go compiler identity, Go build environment
|
|
901
|
+
* variables, overlay module sources, plugin source files, and contributor
|
|
902
|
+
* source files. Contributors are sorted by name so declaration order does not
|
|
903
|
+
* affect the key.
|
|
904
|
+
*
|
|
905
|
+
* Exposed for testing and for the `ttsc cache` CLI command.
|
|
906
|
+
*/
|
|
881
907
|
export function computeCacheKey(inputs: {
|
|
882
908
|
contributors?: readonly ITtscBuildContributor[];
|
|
883
909
|
dir: string;
|