ttsc 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -66
- package/cmd/ttsc/api_compile.go +5 -1
- package/driver/program.go +23 -1
- package/driver/rewrite.go +126 -9
- package/driver/rewrite_test.go +52 -0
- package/lib/TtscCompiler.js +28 -46
- package/lib/TtscCompiler.js.map +1 -1
- package/lib/compiler/internal/buildNativeCompiler.js +8 -38
- package/lib/compiler/internal/buildNativeCompiler.js.map +1 -1
- package/lib/compiler/internal/compileProjectInMemory.js +36 -63
- package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
- package/lib/compiler/internal/project/readProjectConfig.js +108 -60
- package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
- package/lib/compiler/internal/project/resolveProjectConfig.js +15 -45
- package/lib/compiler/internal/project/resolveProjectConfig.js.map +1 -1
- package/lib/compiler/internal/resolveBinary.js +10 -40
- package/lib/compiler/internal/resolveBinary.js.map +1 -1
- package/lib/compiler/internal/resolveEmittedJavaScript.js +21 -48
- package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
- package/lib/compiler/internal/resolveTsgo.js +14 -44
- package/lib/compiler/internal/resolveTsgo.js.map +1 -1
- package/lib/compiler/internal/runBuild.d.ts +7 -0
- package/lib/compiler/internal/runBuild.js +47 -66
- package/lib/compiler/internal/runBuild.js.map +1 -1
- package/lib/compiler/internal/runSingleFileEmit.js +18 -48
- package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
- package/lib/compiler/internal/transformProjectInMemory.js +41 -75
- package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
- package/lib/launcher/internal/getCompilerVersionText.js +7 -37
- package/lib/launcher/internal/getCompilerVersionText.js.map +1 -1
- package/lib/launcher/internal/prepareExecution.js +48 -69
- package/lib/launcher/internal/prepareExecution.js.map +1 -1
- package/lib/launcher/internal/runTtsc.js +53 -64
- package/lib/launcher/internal/runTtsc.js.map +1 -1
- package/lib/launcher/internal/runTtsx.js +328 -57
- package/lib/launcher/internal/runTtsx.js.map +1 -1
- package/lib/plugin/internal/buildSourcePlugin.d.ts +8 -0
- package/lib/plugin/internal/buildSourcePlugin.js +179 -125
- package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
- package/lib/plugin/internal/loadProjectPlugins.d.ts +2 -0
- package/lib/plugin/internal/loadProjectPlugins.js +197 -9
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/lib/structures/ITtscCompilerContext.d.ts +16 -3
- package/lib/structures/ITtscCompilerResult.d.ts +4 -0
- package/lib/structures/ITtscCompilerTransformation.d.ts +2 -0
- package/lib/structures/ITtscPluginFactoryContext.d.ts +6 -3
- package/lib/structures/ITtscProjectPluginConfig.d.ts +8 -7
- package/lib/structures/internal/ITtscParsedProjectConfig.d.ts +2 -0
- package/lib/structures/internal/ITtscProjectLocatorOptions.d.ts +2 -0
- package/lib/structures/internal/TtscCommonOptions.d.ts +2 -0
- package/package.json +9 -14
- package/shim/diagnosticwriter/lint.go +8 -0
- package/src/TtscCompiler.ts +20 -5
- package/src/compiler/internal/buildNativeCompiler.ts +2 -2
- package/src/compiler/internal/compileProjectInMemory.ts +21 -16
- package/src/compiler/internal/project/readProjectConfig.ts +134 -35
- package/src/compiler/internal/project/resolveProjectConfig.ts +2 -2
- package/src/compiler/internal/resolveBinary.ts +2 -2
- package/src/compiler/internal/resolveEmittedJavaScript.ts +11 -7
- package/src/compiler/internal/resolveTsgo.ts +2 -2
- package/src/compiler/internal/runBuild.ts +34 -21
- package/src/compiler/internal/runSingleFileEmit.ts +3 -3
- package/src/compiler/internal/transformProjectInMemory.ts +37 -37
- package/src/launcher/internal/getCompilerVersionText.ts +2 -2
- package/src/launcher/internal/prepareExecution.ts +14 -4
- package/src/launcher/internal/runTtsc.ts +30 -4
- package/src/launcher/internal/runTtsx.ts +377 -26
- package/src/plugin/internal/buildSourcePlugin.ts +127 -32
- package/src/plugin/internal/loadProjectPlugins.ts +266 -10
- package/src/structures/ITtscCompilerContext.ts +17 -3
- package/src/structures/ITtscCompilerResult.ts +5 -0
- package/src/structures/ITtscCompilerTransformation.ts +3 -0
- package/src/structures/ITtscPluginFactoryContext.ts +6 -3
- package/src/structures/ITtscProjectPluginConfig.ts +8 -7
- package/src/structures/internal/ITtscParsedProjectConfig.ts +2 -0
- package/src/structures/internal/ITtscProjectLocatorOptions.ts +2 -0
- package/src/structures/internal/TtscCommonOptions.ts +2 -0
- package/utility/host.go +610 -18
- package/utility/host_test.go +162 -0
- package/test/compiler.test.ts +0 -422
- package/test/platform.test.ts +0 -12
- package/test/project.test.ts +0 -197
- package/test/source-build.test.ts +0 -45
- package/test/tsgo.test.ts +0 -69
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
|
-
import
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
6
7
|
|
|
7
8
|
const GO_MOD_SEARCH_MAX_DEPTH = 3;
|
|
8
9
|
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
".git",
|
|
12
|
-
"dist",
|
|
13
|
-
"build",
|
|
14
|
-
"vendor",
|
|
15
|
-
"lib",
|
|
16
|
-
]);
|
|
10
|
+
const PRUNE_DIRS = new Set(["node_modules", ".git", ".ttsc"]);
|
|
11
|
+
const GENERATED_WORKSPACE_FILES = new Set(["go.work", "go.work.sum"]);
|
|
17
12
|
|
|
18
13
|
/** Build one Go source plugin into a cached executable. */
|
|
19
14
|
export function buildSourcePlugin(opts: {
|
|
@@ -28,9 +23,13 @@ export function buildSourcePlugin(opts: {
|
|
|
28
23
|
tsgoVersion: string;
|
|
29
24
|
}): string {
|
|
30
25
|
const { dir, entry, source } = resolveSourceBuildTarget(opts);
|
|
26
|
+
const overlayDirs = opts.overlayDirs ?? findTtscOverlayDirs();
|
|
27
|
+
const goBinary = resolveGoCompiler();
|
|
31
28
|
const key = computeCacheKey({
|
|
32
29
|
dir,
|
|
33
30
|
entry,
|
|
31
|
+
goBinary,
|
|
32
|
+
overlayDirs,
|
|
34
33
|
ttscVersion: opts.ttscVersion,
|
|
35
34
|
tsgoVersion: opts.tsgoVersion,
|
|
36
35
|
});
|
|
@@ -49,27 +48,46 @@ export function buildSourcePlugin(opts: {
|
|
|
49
48
|
);
|
|
50
49
|
}
|
|
51
50
|
|
|
52
|
-
const scratchDir =
|
|
53
|
-
|
|
54
|
-
`scratch-${key}-${process.pid}-${Date.now()}`,
|
|
51
|
+
const scratchDir = fs.mkdtempSync(
|
|
52
|
+
path.join(os.tmpdir(), `ttsc-plugin-${key}-`),
|
|
55
53
|
);
|
|
56
54
|
try {
|
|
57
55
|
materializeScratchDir(dir, scratchDir);
|
|
58
|
-
writeGoWork(scratchDir,
|
|
56
|
+
writeGoWork(scratchDir, overlayDirs);
|
|
59
57
|
const scratchBinaryName =
|
|
60
58
|
process.platform === "win32" ? ".ttsc-plugin.exe" : ".ttsc-plugin";
|
|
61
|
-
runGoBuild(scratchDir, entry, scratchBinaryName, opts.pluginName);
|
|
59
|
+
runGoBuild(scratchDir, entry, scratchBinaryName, opts.pluginName, goBinary);
|
|
62
60
|
const builtBinary = path.join(scratchDir, scratchBinaryName);
|
|
63
|
-
|
|
64
|
-
if (process.platform !== "win32") {
|
|
65
|
-
fs.chmodSync(binaryPath, 0o755);
|
|
66
|
-
}
|
|
61
|
+
publishBuiltBinary(builtBinary, binaryPath);
|
|
67
62
|
return binaryPath;
|
|
68
63
|
} finally {
|
|
69
64
|
fs.rmSync(scratchDir, { recursive: true, force: true });
|
|
70
65
|
}
|
|
71
66
|
}
|
|
72
67
|
|
|
68
|
+
function publishBuiltBinary(builtBinary: string, binaryPath: string): void {
|
|
69
|
+
const pending = `${binaryPath}.${process.pid}.${Date.now()}-${Math.random()
|
|
70
|
+
.toString(16)
|
|
71
|
+
.slice(2)}.tmp`;
|
|
72
|
+
fs.copyFileSync(builtBinary, pending);
|
|
73
|
+
if (process.platform !== "win32") {
|
|
74
|
+
fs.chmodSync(pending, 0o755);
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
fs.renameSync(pending, binaryPath);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
fs.rmSync(pending, { force: true });
|
|
80
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
81
|
+
if (
|
|
82
|
+
(code === "EEXIST" || code === "EPERM" || code === "EACCES") &&
|
|
83
|
+
fs.existsSync(binaryPath)
|
|
84
|
+
) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
73
91
|
function resolveSourceBuildTarget(opts: {
|
|
74
92
|
source: string;
|
|
75
93
|
pluginName: string;
|
|
@@ -136,8 +154,8 @@ function materializeScratchDir(source: string, scratch: string): void {
|
|
|
136
154
|
recursive: true,
|
|
137
155
|
filter: (src) => {
|
|
138
156
|
const base = path.basename(src);
|
|
139
|
-
if (
|
|
140
|
-
if (base
|
|
157
|
+
if (shouldPruneDirectory(base)) return false;
|
|
158
|
+
if (shouldOmitSourceFile(base)) return false;
|
|
141
159
|
return true;
|
|
142
160
|
},
|
|
143
161
|
});
|
|
@@ -189,8 +207,8 @@ function runGoBuild(
|
|
|
189
207
|
entry: string,
|
|
190
208
|
binaryName: string,
|
|
191
209
|
pluginName: string,
|
|
210
|
+
goBinary: string,
|
|
192
211
|
): void {
|
|
193
|
-
const goBinary = resolveGoCompiler();
|
|
194
212
|
ensureExecutableGoToolchain(goBinary);
|
|
195
213
|
const result = spawnSync(goBinary, ["build", "-o", binaryName, entry], {
|
|
196
214
|
cwd,
|
|
@@ -305,7 +323,7 @@ function walkForGoMod(dir: string, out: string[]): void {
|
|
|
305
323
|
}
|
|
306
324
|
for (const entry of entries) {
|
|
307
325
|
if (!entry.isDirectory()) continue;
|
|
308
|
-
if (
|
|
326
|
+
if (shouldPruneDirectory(entry.name)) continue;
|
|
309
327
|
walkForGoMod(path.join(dir, entry.name), out);
|
|
310
328
|
}
|
|
311
329
|
}
|
|
@@ -315,7 +333,7 @@ function resolvePluginCacheRoot(
|
|
|
315
333
|
cacheDir?: string,
|
|
316
334
|
): string {
|
|
317
335
|
if (cacheDir) {
|
|
318
|
-
return path.resolve(cacheDir, "plugins");
|
|
336
|
+
return path.resolve(projectRoot, cacheDir, "plugins");
|
|
319
337
|
}
|
|
320
338
|
if (process.env.TTSC_CACHE_DIR) {
|
|
321
339
|
return path.resolve(process.env.TTSC_CACHE_DIR, "plugins");
|
|
@@ -387,9 +405,11 @@ function resolveGoCompiler(): string {
|
|
|
387
405
|
return "go";
|
|
388
406
|
}
|
|
389
407
|
|
|
390
|
-
function computeCacheKey(inputs: {
|
|
408
|
+
export function computeCacheKey(inputs: {
|
|
391
409
|
dir: string;
|
|
392
410
|
entry: string;
|
|
411
|
+
goBinary?: string;
|
|
412
|
+
overlayDirs?: readonly string[];
|
|
393
413
|
ttscVersion: string;
|
|
394
414
|
tsgoVersion: string;
|
|
395
415
|
}): string {
|
|
@@ -398,12 +418,28 @@ function computeCacheKey(inputs: {
|
|
|
398
418
|
hash.update(`tsgo=${inputs.tsgoVersion}\n`);
|
|
399
419
|
hash.update(`platform=${process.platform}/${process.arch}\n`);
|
|
400
420
|
hash.update(`entry=${inputs.entry}\n`);
|
|
401
|
-
|
|
402
|
-
|
|
421
|
+
if (inputs.goBinary !== undefined) {
|
|
422
|
+
hash.update(`go=${resolveGoCompilerIdentity(inputs.goBinary)}\n`);
|
|
423
|
+
}
|
|
424
|
+
hashSourceDirectory(hash, "plugin", inputs.dir);
|
|
425
|
+
for (const [index, dir] of [...(inputs.overlayDirs ?? [])].sort().entries()) {
|
|
426
|
+
hashSourceDirectory(hash, `overlay:${index}`, dir);
|
|
427
|
+
}
|
|
428
|
+
return hash.digest("hex").slice(0, 32);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function hashSourceDirectory(
|
|
432
|
+
hash: crypto.Hash,
|
|
433
|
+
label: string,
|
|
434
|
+
root: string,
|
|
435
|
+
): void {
|
|
436
|
+
hash.update(`dir=${label}\n`);
|
|
437
|
+
for (const file of collectSourceFiles(root)) {
|
|
438
|
+
const rel = path.relative(root, file).replace(/\\/g, "/");
|
|
403
439
|
hash.update(`f=${rel}\n`);
|
|
404
440
|
hash.update(fs.readFileSync(file));
|
|
441
|
+
hash.update("\n");
|
|
405
442
|
}
|
|
406
|
-
return hash.digest("hex").slice(0, 32);
|
|
407
443
|
}
|
|
408
444
|
|
|
409
445
|
function collectSourceFiles(root: string): string[] {
|
|
@@ -421,19 +457,78 @@ function walk(dir: string, out: string[]): void {
|
|
|
421
457
|
return;
|
|
422
458
|
}
|
|
423
459
|
for (const entry of entries) {
|
|
424
|
-
if (SKIP_DIRS.has(entry.name)) continue;
|
|
425
460
|
const full = path.join(dir, entry.name);
|
|
426
461
|
if (entry.isDirectory()) {
|
|
462
|
+
if (shouldPruneDirectory(entry.name)) continue;
|
|
427
463
|
walk(full, out);
|
|
428
464
|
continue;
|
|
429
465
|
}
|
|
430
466
|
if (!entry.isFile()) continue;
|
|
467
|
+
if (shouldOmitSourceFile(entry.name)) continue;
|
|
431
468
|
if (!isHashableFile(entry.name)) continue;
|
|
432
469
|
out.push(full);
|
|
433
470
|
}
|
|
434
471
|
}
|
|
435
472
|
|
|
473
|
+
function shouldPruneDirectory(name: string): boolean {
|
|
474
|
+
return PRUNE_DIRS.has(name);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function shouldOmitSourceFile(name: string): boolean {
|
|
478
|
+
return GENERATED_WORKSPACE_FILES.has(name);
|
|
479
|
+
}
|
|
480
|
+
|
|
436
481
|
function isHashableFile(name: string): boolean {
|
|
437
|
-
|
|
438
|
-
|
|
482
|
+
return !name.endsWith("~");
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function resolveGoCompilerIdentity(goBinary: string): string {
|
|
486
|
+
const resolved = resolveExecutableIdentityPath(goBinary);
|
|
487
|
+
const version = spawnSync(goBinary, ["version"], {
|
|
488
|
+
encoding: "utf8",
|
|
489
|
+
windowsHide: true,
|
|
490
|
+
});
|
|
491
|
+
const versionText =
|
|
492
|
+
version.error !== undefined
|
|
493
|
+
? version.error.message
|
|
494
|
+
: `${version.status ?? 0}:${version.stdout}${version.stderr}`;
|
|
495
|
+
let statText = "";
|
|
496
|
+
try {
|
|
497
|
+
const stat = fs.statSync(resolved);
|
|
498
|
+
statText = `${stat.size}:${stat.mtimeMs}`;
|
|
499
|
+
} catch {
|
|
500
|
+
statText = "missing";
|
|
501
|
+
}
|
|
502
|
+
return `${goBinary}:${resolved}:${statText}:${versionText}`;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function resolveExecutableIdentityPath(binary: string): string {
|
|
506
|
+
if (path.isAbsolute(binary)) {
|
|
507
|
+
return resolveRealPath(binary);
|
|
508
|
+
}
|
|
509
|
+
if (binary.includes(path.sep)) {
|
|
510
|
+
return resolveRealPath(path.resolve(binary));
|
|
511
|
+
}
|
|
512
|
+
for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
513
|
+
if (dir.length === 0) continue;
|
|
514
|
+
const candidate = path.join(dir, binary);
|
|
515
|
+
if (fs.existsSync(candidate)) {
|
|
516
|
+
return resolveRealPath(candidate);
|
|
517
|
+
}
|
|
518
|
+
if (process.platform === "win32") {
|
|
519
|
+
const executable = `${candidate}.exe`;
|
|
520
|
+
if (fs.existsSync(executable)) {
|
|
521
|
+
return resolveRealPath(executable);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return binary;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function resolveRealPath(location: string): string {
|
|
529
|
+
try {
|
|
530
|
+
return fs.realpathSync(location);
|
|
531
|
+
} catch {
|
|
532
|
+
return location;
|
|
533
|
+
}
|
|
439
534
|
}
|
|
@@ -15,12 +15,25 @@ type TtscPluginFactory<T = ITtscProjectPluginConfig> = (
|
|
|
15
15
|
context: ITtscPluginFactoryContext<T>,
|
|
16
16
|
) => ITtscPlugin;
|
|
17
17
|
|
|
18
|
+
type ProjectPluginEntry = {
|
|
19
|
+
baseDir: string;
|
|
20
|
+
config: ITtscProjectPluginConfig;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type PackageManifest = {
|
|
24
|
+
dependencies?: Record<string, unknown>;
|
|
25
|
+
devDependencies?: Record<string, unknown>;
|
|
26
|
+
name?: unknown;
|
|
27
|
+
ttsc?: unknown;
|
|
28
|
+
};
|
|
29
|
+
|
|
18
30
|
export function loadProjectPlugins(options: {
|
|
19
31
|
binary: string;
|
|
20
32
|
cacheDir?: string;
|
|
21
33
|
cwd?: string;
|
|
22
34
|
entries?: readonly ITtscProjectPluginConfig[] | false;
|
|
23
35
|
file?: string;
|
|
36
|
+
projectRoot?: string;
|
|
24
37
|
tsconfig?: string;
|
|
25
38
|
}): {
|
|
26
39
|
nativePlugins: ITtscLoadedNativePlugin[];
|
|
@@ -29,13 +42,14 @@ export function loadProjectPlugins(options: {
|
|
|
29
42
|
const project = readProjectConfig({
|
|
30
43
|
cwd: options.cwd,
|
|
31
44
|
file: options.file,
|
|
45
|
+
projectRoot: options.projectRoot,
|
|
32
46
|
tsconfig: options.tsconfig,
|
|
33
47
|
});
|
|
34
|
-
const entries =
|
|
48
|
+
const entries: ProjectPluginEntry[] =
|
|
35
49
|
options.entries === false
|
|
36
50
|
? []
|
|
37
|
-
:
|
|
38
|
-
(entry) => entry.enabled !== false,
|
|
51
|
+
: resolvePluginEntries(project, options.entries).filter(
|
|
52
|
+
(entry) => entry.config.enabled !== false,
|
|
39
53
|
);
|
|
40
54
|
if (entries.length === 0) {
|
|
41
55
|
return {
|
|
@@ -51,7 +65,11 @@ export function loadProjectPlugins(options: {
|
|
|
51
65
|
tsconfig: project.path,
|
|
52
66
|
};
|
|
53
67
|
const plugins = entries.map((entry) =>
|
|
54
|
-
loadPluginEntry(
|
|
68
|
+
loadPluginEntry(
|
|
69
|
+
entry.config,
|
|
70
|
+
{ ...context, plugin: entry.config },
|
|
71
|
+
entry.baseDir,
|
|
72
|
+
),
|
|
55
73
|
);
|
|
56
74
|
|
|
57
75
|
const nativePlugins: ITtscLoadedNativePlugin[] = [];
|
|
@@ -70,28 +88,258 @@ export function loadProjectPlugins(options: {
|
|
|
70
88
|
});
|
|
71
89
|
nativePlugins.push({
|
|
72
90
|
binary,
|
|
73
|
-
config: entries[index]
|
|
91
|
+
config: entries[index]!.config,
|
|
74
92
|
name: plugin.name,
|
|
75
93
|
source: plugin.source,
|
|
76
94
|
stage,
|
|
77
95
|
});
|
|
78
96
|
});
|
|
79
97
|
return {
|
|
80
|
-
nativePlugins,
|
|
98
|
+
nativePlugins: orderNativePlugins(nativePlugins),
|
|
81
99
|
project,
|
|
82
100
|
};
|
|
83
101
|
}
|
|
84
102
|
|
|
103
|
+
export function hasProjectPluginEntries(
|
|
104
|
+
project: ITtscParsedProjectConfig,
|
|
105
|
+
entries?: readonly ITtscProjectPluginConfig[] | false,
|
|
106
|
+
): boolean {
|
|
107
|
+
if (entries === false) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
return resolvePluginEntries(project, entries).some(
|
|
111
|
+
(entry) => entry.config.enabled !== false,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function resolvePluginEntries(
|
|
116
|
+
project: ITtscParsedProjectConfig,
|
|
117
|
+
entries?: readonly ITtscProjectPluginConfig[],
|
|
118
|
+
): ProjectPluginEntry[] {
|
|
119
|
+
if (entries !== undefined) {
|
|
120
|
+
return entries.map((config) => ({
|
|
121
|
+
baseDir: project.root,
|
|
122
|
+
config,
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
const configured = project.compilerOptions.plugins.map((config, index) => ({
|
|
126
|
+
baseDir: project.pluginBaseDirs[index] ?? project.root,
|
|
127
|
+
config,
|
|
128
|
+
}));
|
|
129
|
+
return [...configured, ...discoverPackagePluginEntries(project, configured)];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function discoverPackagePluginEntries(
|
|
133
|
+
project: ITtscParsedProjectConfig,
|
|
134
|
+
configured: readonly ProjectPluginEntry[],
|
|
135
|
+
): ProjectPluginEntry[] {
|
|
136
|
+
const projectPackageJson = findNearestPackageJson(project.root);
|
|
137
|
+
if (projectPackageJson === undefined) {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
const projectPackageRoot = path.dirname(projectPackageJson);
|
|
141
|
+
const projectManifest = readPackageManifest(projectPackageJson);
|
|
142
|
+
if (projectManifest === undefined) {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const configuredTransforms = createConfiguredTransformSet(configured);
|
|
147
|
+
const out: ProjectPluginEntry[] = [];
|
|
148
|
+
for (const name of directDependencyNames(projectManifest)) {
|
|
149
|
+
const packageJson = resolveDependencyPackageJson(name, projectPackageRoot);
|
|
150
|
+
if (packageJson === undefined) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const manifest = readPackageManifest(packageJson);
|
|
154
|
+
const config = readPackagePluginConfig(name, manifest);
|
|
155
|
+
if (config === undefined || config.enabled === false) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const packageRoot = path.dirname(packageJson);
|
|
159
|
+
const transform = config.transform;
|
|
160
|
+
if (typeof transform !== "string") {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const baseDir = isRelativePluginSpecifier(transform)
|
|
164
|
+
? packageRoot
|
|
165
|
+
: projectPackageRoot;
|
|
166
|
+
const resolved = resolvePluginRequest(transform, baseDir);
|
|
167
|
+
if (hasConfiguredTransform(configuredTransforms, transform, resolved)) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
out.push({
|
|
171
|
+
baseDir,
|
|
172
|
+
config,
|
|
173
|
+
});
|
|
174
|
+
addConfiguredTransform(configuredTransforms, transform, resolved);
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
type ConfiguredTransformSet = {
|
|
180
|
+
raw: Set<string>;
|
|
181
|
+
resolved: Set<string>;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
function createConfiguredTransformSet(
|
|
185
|
+
entries: readonly ProjectPluginEntry[],
|
|
186
|
+
): ConfiguredTransformSet {
|
|
187
|
+
const raw = new Set<string>();
|
|
188
|
+
const resolved = new Set<string>();
|
|
189
|
+
for (const entry of entries) {
|
|
190
|
+
const transform = entry.config.transform;
|
|
191
|
+
if (typeof transform !== "string" || transform.length === 0) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (!isRelativePluginSpecifier(transform)) {
|
|
195
|
+
raw.add(transform);
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
resolved.add(resolvePluginRequest(transform, entry.baseDir));
|
|
199
|
+
} catch {
|
|
200
|
+
// Keep the normal plugin loading error path for invalid explicit entries.
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return { raw, resolved };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function hasConfiguredTransform(
|
|
207
|
+
configuredTransforms: ConfiguredTransformSet,
|
|
208
|
+
transform: string,
|
|
209
|
+
resolved: string,
|
|
210
|
+
): boolean {
|
|
211
|
+
return (
|
|
212
|
+
configuredTransforms.resolved.has(resolved) ||
|
|
213
|
+
(!isRelativePluginSpecifier(transform) &&
|
|
214
|
+
configuredTransforms.raw.has(transform))
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function addConfiguredTransform(
|
|
219
|
+
configuredTransforms: ConfiguredTransformSet,
|
|
220
|
+
transform: string,
|
|
221
|
+
resolved: string,
|
|
222
|
+
): void {
|
|
223
|
+
if (!isRelativePluginSpecifier(transform)) {
|
|
224
|
+
configuredTransforms.raw.add(transform);
|
|
225
|
+
}
|
|
226
|
+
configuredTransforms.resolved.add(resolved);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function directDependencyNames(manifest: PackageManifest): string[] {
|
|
230
|
+
const seen = new Set<string>();
|
|
231
|
+
const out: string[] = [];
|
|
232
|
+
for (const dependencies of [
|
|
233
|
+
manifest.dependencies,
|
|
234
|
+
manifest.devDependencies,
|
|
235
|
+
]) {
|
|
236
|
+
if (!isRecord(dependencies)) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
for (const name of Object.keys(dependencies)) {
|
|
240
|
+
if (seen.has(name)) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
seen.add(name);
|
|
244
|
+
out.push(name);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function resolveDependencyPackageJson(
|
|
251
|
+
name: string,
|
|
252
|
+
projectRoot: string,
|
|
253
|
+
): string | undefined {
|
|
254
|
+
const direct = path.join(projectRoot, "node_modules", ...name.split("/"));
|
|
255
|
+
const directManifest = path.join(direct, "package.json");
|
|
256
|
+
if (fs.existsSync(directManifest)) {
|
|
257
|
+
return resolveRealPath(directManifest);
|
|
258
|
+
}
|
|
259
|
+
const projectPackage = path.join(projectRoot, "package.json");
|
|
260
|
+
const projectRequire = createRequire(projectPackage);
|
|
261
|
+
try {
|
|
262
|
+
return resolveRealPath(projectRequire.resolve(`${name}/package.json`));
|
|
263
|
+
} catch {
|
|
264
|
+
try {
|
|
265
|
+
return findNearestPackageJson(projectRequire.resolve(name));
|
|
266
|
+
} catch {
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function findNearestPackageJson(location: string): string | undefined {
|
|
273
|
+
let current = fs.statSync(location).isDirectory()
|
|
274
|
+
? location
|
|
275
|
+
: path.dirname(location);
|
|
276
|
+
while (true) {
|
|
277
|
+
const manifest = path.join(current, "package.json");
|
|
278
|
+
if (fs.existsSync(manifest)) {
|
|
279
|
+
return resolveRealPath(manifest);
|
|
280
|
+
}
|
|
281
|
+
const parent = path.dirname(current);
|
|
282
|
+
if (parent === current) {
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
current = parent;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function readPackageManifest(file: string): PackageManifest | undefined {
|
|
290
|
+
if (!fs.existsSync(file)) {
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf8")) as unknown;
|
|
294
|
+
return isRecord(parsed) ? (parsed as PackageManifest) : undefined;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function readPackagePluginConfig(
|
|
298
|
+
packageName: string,
|
|
299
|
+
manifest: PackageManifest | undefined,
|
|
300
|
+
): ITtscProjectPluginConfig | undefined {
|
|
301
|
+
const ttsc = manifest?.ttsc;
|
|
302
|
+
if (!isRecord(ttsc) || !("plugin" in ttsc)) {
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
const plugin = ttsc.plugin;
|
|
306
|
+
if (!isRecord(plugin) || Array.isArray(plugin)) {
|
|
307
|
+
throw new Error(
|
|
308
|
+
`ttsc: package ${JSON.stringify(packageName)} declares invalid "ttsc.plugin"; expected an object`,
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
if (typeof plugin.transform !== "string" || plugin.transform.length === 0) {
|
|
312
|
+
throw new Error(
|
|
313
|
+
`ttsc: package ${JSON.stringify(packageName)} declares invalid "ttsc.plugin.transform"; expected a non-empty string`,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
return { ...plugin } as ITtscProjectPluginConfig;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
320
|
+
return typeof value === "object" && value !== null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function orderNativePlugins(
|
|
324
|
+
plugins: readonly ITtscLoadedNativePlugin[],
|
|
325
|
+
): ITtscLoadedNativePlugin[] {
|
|
326
|
+
return [
|
|
327
|
+
...plugins.filter((plugin) => plugin.stage === "check"),
|
|
328
|
+
...plugins.filter((plugin) => plugin.stage === "transform"),
|
|
329
|
+
];
|
|
330
|
+
}
|
|
331
|
+
|
|
85
332
|
function loadPluginEntry(
|
|
86
333
|
entry: ITtscProjectPluginConfig,
|
|
87
334
|
context: ITtscPluginFactoryContext,
|
|
335
|
+
baseDir: string,
|
|
88
336
|
): ITtscPlugin {
|
|
89
337
|
const specifier = entry.transform;
|
|
90
338
|
if (typeof specifier !== "string" || specifier.length === 0) {
|
|
91
339
|
throw new Error(`ttsc: plugin entry is missing a string "transform" field`);
|
|
92
340
|
}
|
|
93
341
|
|
|
94
|
-
const request = resolvePluginRequest(specifier,
|
|
342
|
+
const request = resolvePluginRequest(specifier, baseDir);
|
|
95
343
|
const mod = require(request) as {
|
|
96
344
|
createTtscPlugin?: TtscPluginFactory;
|
|
97
345
|
default?: ITtscPlugin | TtscPluginFactory;
|
|
@@ -170,12 +418,20 @@ function isPluginStage(value: string): value is TtscPluginStage {
|
|
|
170
418
|
|
|
171
419
|
function resolvePluginRequest(specifier: string, projectRoot: string): string {
|
|
172
420
|
if (path.isAbsolute(specifier)) {
|
|
173
|
-
return specifier;
|
|
421
|
+
return resolveRealPath(specifier);
|
|
174
422
|
}
|
|
175
423
|
if (isRelativePluginSpecifier(specifier)) {
|
|
176
|
-
return path.resolve(projectRoot, specifier);
|
|
424
|
+
return resolveRealPath(path.resolve(projectRoot, specifier));
|
|
425
|
+
}
|
|
426
|
+
return resolveRealPath(require.resolve(specifier, { paths: [projectRoot] }));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function resolveRealPath(location: string): string {
|
|
430
|
+
try {
|
|
431
|
+
return fs.realpathSync(location);
|
|
432
|
+
} catch {
|
|
433
|
+
return location;
|
|
177
434
|
}
|
|
178
|
-
return require.resolve(specifier, { paths: [projectRoot] });
|
|
179
435
|
}
|
|
180
436
|
|
|
181
437
|
function isRelativePluginSpecifier(specifier: string): boolean {
|
|
@@ -35,6 +35,17 @@ export interface ITtscCompilerContext {
|
|
|
35
35
|
*/
|
|
36
36
|
tsconfig?: string;
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Project root override for generated config wrappers.
|
|
40
|
+
*
|
|
41
|
+
* Most callers should leave this unset so the project root is the directory
|
|
42
|
+
* containing {@link ITtscCompilerContext.tsconfig}. Embedders that synthesize
|
|
43
|
+
* a temporary config extending a real project config can set this to keep
|
|
44
|
+
* plugin resolution, cache paths, and native sidecar `--cwd` anchored to the
|
|
45
|
+
* real project.
|
|
46
|
+
*/
|
|
47
|
+
projectRoot?: string;
|
|
48
|
+
|
|
38
49
|
/**
|
|
39
50
|
* Explicit TypeScript-Go executable for controlled embedding.
|
|
40
51
|
*
|
|
@@ -73,9 +84,12 @@ export interface ITtscCompilerContext {
|
|
|
73
84
|
/**
|
|
74
85
|
* Plugin entries for this compiler instance.
|
|
75
86
|
*
|
|
76
|
-
* - `undefined`: read `compilerOptions.plugins`
|
|
77
|
-
*
|
|
78
|
-
* -
|
|
87
|
+
* - `undefined`: read project plugins from `compilerOptions.plugins` and
|
|
88
|
+
* directly installed package markers.
|
|
89
|
+
* - `false`: ignore project config plugins and package markers for this
|
|
90
|
+
* compiler instance.
|
|
91
|
+
* - array: use these plugin entries instead of project config entries and
|
|
92
|
+
* package markers.
|
|
79
93
|
*
|
|
80
94
|
* Plugin entries are resolved once per operation from this instance context.
|
|
81
95
|
* Per-call plugin overrides are intentionally not part of the public API.
|
|
@@ -19,6 +19,9 @@ export namespace ITtscCompilerTransformation {
|
|
|
19
19
|
/** Indicates that transformation completed without diagnostics. */
|
|
20
20
|
type: "success";
|
|
21
21
|
|
|
22
|
+
/** Non-fatal diagnostics reported during transformation. */
|
|
23
|
+
diagnostics?: ITtscCompilerDiagnostic[];
|
|
24
|
+
|
|
22
25
|
/** Transformed TypeScript source text keyed by project file path. */
|
|
23
26
|
typescript: Record<string, string>;
|
|
24
27
|
}
|
|
@@ -43,10 +43,13 @@ export interface ITtscPluginFactoryContext<T = ITtscProjectPluginConfig> {
|
|
|
43
43
|
plugin: T;
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
*
|
|
46
|
+
* Project root used as the native plugin working directory and package
|
|
47
|
+
* discovery base.
|
|
47
48
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
49
|
+
* By default this is the directory containing the resolved tsconfig/jsconfig.
|
|
50
|
+
* Callers may override it when a wrapper config lives outside the project.
|
|
51
|
+
* Relative `source` paths returned by a plugin descriptor resolve from this
|
|
52
|
+
* directory.
|
|
50
53
|
*/
|
|
51
54
|
projectRoot: string;
|
|
52
55
|
|