ttsc 0.10.0 → 0.10.2
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/internal/cwd/cwd.go +16 -0
- package/lib/compiler/internal/compileProjectInMemory.js +1 -1
- package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
- package/lib/compiler/internal/runBuild.js +3 -47
- package/lib/compiler/internal/runBuild.js.map +1 -1
- package/lib/compiler/internal/sharedHostHelpers.d.ts +47 -0
- package/lib/compiler/internal/sharedHostHelpers.js +101 -0
- package/lib/compiler/internal/sharedHostHelpers.js.map +1 -0
- package/lib/compiler/internal/transformProjectInMemory.js +3 -47
- package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
- package/lib/plugin/internal/loadProjectPlugins.js +70 -1
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/lib/structures/ITtscPlugin.d.ts +10 -0
- package/lib/structures/ITtscPlugin.js.map +1 -1
- package/package.json +9 -8
- package/shim/ast/lint.go +2 -5
- package/shim/ast/shim.go +8 -0
- package/shim/ast/surface.go +685 -0
- package/shim/checker/shim.go +8 -0
- package/src/compiler/internal/compileProjectInMemory.ts +1 -1
- package/src/compiler/internal/runBuild.ts +3 -57
- package/src/compiler/internal/sharedHostHelpers.ts +105 -0
- package/src/compiler/internal/transformProjectInMemory.ts +3 -57
- package/src/plugin/internal/loadProjectPlugins.ts +104 -5
- package/src/structures/ITtscPlugin.ts +11 -0
- package/tools/gen_shims/main.go +29 -9
package/shim/checker/shim.go
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
// gen_shims:hand-maintained
|
|
2
|
+
//
|
|
3
|
+
// This shim file mixes generated re-exports with hand-written `go:linkname`
|
|
4
|
+
// declarations targeting unexported `*Checker` methods that the @ttsc/lint
|
|
5
|
+
// engine relies on. gen_shims detects the marker on the first line and skips
|
|
6
|
+
// this file. Remove the marker only if you are intentionally regenerating and
|
|
7
|
+
// willing to re-add the hand-maintained content.
|
|
8
|
+
|
|
1
9
|
package checker
|
|
2
10
|
|
|
3
11
|
import (
|
|
@@ -29,7 +29,7 @@ export function compileProjectInMemory(options: ITtscCompilerContext): {
|
|
|
29
29
|
const tsconfig = project.path;
|
|
30
30
|
const binary = buildNativeCompiler({
|
|
31
31
|
cacheBaseDir: project.root,
|
|
32
|
-
cacheDir: options.cacheDir,
|
|
32
|
+
cacheDir: options.cacheDir ?? options.env?.TTSC_CACHE_DIR,
|
|
33
33
|
packageRoot: packageRootDir(),
|
|
34
34
|
});
|
|
35
35
|
const res = spawnSync(
|
|
@@ -12,6 +12,7 @@ import type { TtscCommonOptions } from "../../structures/internal/TtscCommonOpti
|
|
|
12
12
|
import { resolveProjectConfig } from "./project/resolveProjectConfig";
|
|
13
13
|
import { resolveBinary } from "./resolveBinary";
|
|
14
14
|
import { resolveTsgo } from "./resolveTsgo";
|
|
15
|
+
import { assertSharedHostCompatibility } from "./sharedHostHelpers";
|
|
15
16
|
|
|
16
17
|
/** Merge spawn env without clobbering unrelated vars. */
|
|
17
18
|
function mergeEnv(extra?: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
@@ -106,7 +107,7 @@ export function runBuild(
|
|
|
106
107
|
|
|
107
108
|
if (options.emit === false) {
|
|
108
109
|
if (compilers.length !== 0) {
|
|
109
|
-
|
|
110
|
+
assertSharedHostCompatibility(compilers, "emit");
|
|
110
111
|
return appendBuildOutput(
|
|
111
112
|
checked,
|
|
112
113
|
buildWithNativeCompilerPlugins(options, execution, compilers),
|
|
@@ -120,7 +121,7 @@ export function runBuild(
|
|
|
120
121
|
|
|
121
122
|
let result: TtscBuildResult;
|
|
122
123
|
if (compilers.length !== 0) {
|
|
123
|
-
|
|
124
|
+
assertSharedHostCompatibility(compilers, "emit");
|
|
124
125
|
result = appendBuildOutput(
|
|
125
126
|
checked,
|
|
126
127
|
buildWithNativeCompilerPlugins(options, execution, compilers),
|
|
@@ -394,61 +395,6 @@ export function appendBuildOutput(
|
|
|
394
395
|
});
|
|
395
396
|
}
|
|
396
397
|
|
|
397
|
-
function assertCompilerHostCompatibility(
|
|
398
|
-
plugins: readonly ITtscLoadedNativePlugin[],
|
|
399
|
-
): void {
|
|
400
|
-
const binaries = [...new Set(plugins.map((plugin) => plugin.binary))];
|
|
401
|
-
if (binaries.length <= 1) {
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
if (plugins.every(isFirstPartyUtilityTransformPlugin)) {
|
|
405
|
-
return;
|
|
406
|
-
}
|
|
407
|
-
throw new Error(
|
|
408
|
-
"ttsc: multiple compiler native backends cannot share one emit pass; " +
|
|
409
|
-
"compose transform libraries through one aggregate native host",
|
|
410
|
-
);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
function isFirstPartyUtilityTransformPlugin(
|
|
414
|
-
plugin: ITtscLoadedNativePlugin,
|
|
415
|
-
): boolean {
|
|
416
|
-
if (plugin.stage !== "transform") return false;
|
|
417
|
-
if (!firstPartyUtilityPluginNames.has(plugin.name)) return false;
|
|
418
|
-
const manifest = readNearestPackageManifest(plugin.source);
|
|
419
|
-
return manifest?.name === plugin.name;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
const firstPartyUtilityPluginNames = new Set([
|
|
423
|
-
"@ttsc/banner",
|
|
424
|
-
"@ttsc/paths",
|
|
425
|
-
"@ttsc/strip",
|
|
426
|
-
]);
|
|
427
|
-
|
|
428
|
-
function readNearestPackageManifest(
|
|
429
|
-
source: string,
|
|
430
|
-
): { name?: unknown } | undefined {
|
|
431
|
-
try {
|
|
432
|
-
let current = fs.statSync(source).isDirectory()
|
|
433
|
-
? source
|
|
434
|
-
: path.dirname(source);
|
|
435
|
-
for (let i = 0; i < 4; i += 1) {
|
|
436
|
-
const manifest = path.join(current, "package.json");
|
|
437
|
-
if (fs.existsSync(manifest)) {
|
|
438
|
-
return JSON.parse(fs.readFileSync(manifest, "utf8")) as {
|
|
439
|
-
name?: unknown;
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
const parent = path.dirname(current);
|
|
443
|
-
if (parent === current) break;
|
|
444
|
-
current = parent;
|
|
445
|
-
}
|
|
446
|
-
} catch {
|
|
447
|
-
return undefined;
|
|
448
|
-
}
|
|
449
|
-
return undefined;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
398
|
function resolveExecutionContext(
|
|
453
399
|
options: TtscCommonOptions & { tsconfig?: string },
|
|
454
400
|
) {
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { ITtscLoadedNativePlugin } from "../../structures/internal/ITtscLoadedNativePlugin";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Names of the first-party utility transform plugins that are allowed to share
|
|
8
|
+
* one compiler emit / one source-to-source pass even when their resolved
|
|
9
|
+
* binaries differ. Each entry must be both name-pinned and manifest-pinned: the
|
|
10
|
+
* plugin must declare itself with one of these names AND its source path must
|
|
11
|
+
* resolve to a `package.json` whose `name` field matches.
|
|
12
|
+
*
|
|
13
|
+
* This whitelist exists because `@ttsc/banner`, `@ttsc/paths`, and `@ttsc/strip`
|
|
14
|
+
* are AST-only mutators that operate on the same `*shimast.SourceFile` graph
|
|
15
|
+
* and do not own emit. They are composed through `packages/ttsc/utility/host.go`,
|
|
16
|
+
* so any one of their binaries can host the shared run. Third-party plugins
|
|
17
|
+
* cannot participate in this fast path; they must opt into composition through
|
|
18
|
+
* the explicit `ITtscPlugin.composes` field.
|
|
19
|
+
*/
|
|
20
|
+
export const FIRST_PARTY_UTILITY_PLUGIN_NAMES: ReadonlySet<string> = new Set([
|
|
21
|
+
"@ttsc/banner",
|
|
22
|
+
"@ttsc/paths",
|
|
23
|
+
"@ttsc/strip",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Reports whether the given plugin is a first-party utility transform plugin
|
|
28
|
+
* that may participate in the shared compiler host alongside other first-party
|
|
29
|
+
* utility transforms. Returns `false` for `check`-stage plugins, for unknown
|
|
30
|
+
* names, and for plugins whose `source` does not resolve to a matching
|
|
31
|
+
* `package.json`.
|
|
32
|
+
*/
|
|
33
|
+
export function isFirstPartyUtilityTransformPlugin(
|
|
34
|
+
plugin: ITtscLoadedNativePlugin,
|
|
35
|
+
): boolean {
|
|
36
|
+
if (plugin.stage !== "transform") return false;
|
|
37
|
+
if (!FIRST_PARTY_UTILITY_PLUGIN_NAMES.has(plugin.name)) return false;
|
|
38
|
+
const manifest = readNearestPackageManifest(plugin.source);
|
|
39
|
+
return manifest?.name === plugin.name;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Walks upward from `source` (a directory or file path) up to four parent
|
|
44
|
+
* levels searching for the nearest `package.json`. Returns the parsed manifest
|
|
45
|
+
* object on success, or `undefined` when no manifest is found or parsing
|
|
46
|
+
* fails. The depth bound mirrors the pre-existing inline implementations in
|
|
47
|
+
* `runBuild.ts` and `transformProjectInMemory.ts`.
|
|
48
|
+
*/
|
|
49
|
+
export function readNearestPackageManifest(
|
|
50
|
+
source: string,
|
|
51
|
+
): { name?: unknown } | undefined {
|
|
52
|
+
try {
|
|
53
|
+
let current = fs.statSync(source).isDirectory()
|
|
54
|
+
? source
|
|
55
|
+
: path.dirname(source);
|
|
56
|
+
for (let i = 0; i < 4; i += 1) {
|
|
57
|
+
const manifest = path.join(current, "package.json");
|
|
58
|
+
if (fs.existsSync(manifest)) {
|
|
59
|
+
return JSON.parse(fs.readFileSync(manifest, "utf8")) as {
|
|
60
|
+
name?: unknown;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const parent = path.dirname(current);
|
|
64
|
+
if (parent === current) break;
|
|
65
|
+
current = parent;
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Verifies that all transform plugins in `plugins` either resolve to the same
|
|
75
|
+
* native binary (the common case) or are all first-party utility plugins (the
|
|
76
|
+
* shared compiler-host fast path).
|
|
77
|
+
*
|
|
78
|
+
* Two callers exist with subtly different error wording: the build path
|
|
79
|
+
* (`runBuild.ts`) reports "multiple compiler native backends cannot share one
|
|
80
|
+
* emit pass" while the source-to-source path
|
|
81
|
+
* (`transformProjectInMemory.ts`) reports "cannot share one source-to-source
|
|
82
|
+
* pass". The `pass` argument selects the appropriate phrase so the error
|
|
83
|
+
* message remains diagnostic-grade instead of generic.
|
|
84
|
+
*/
|
|
85
|
+
export function assertSharedHostCompatibility(
|
|
86
|
+
plugins: readonly ITtscLoadedNativePlugin[],
|
|
87
|
+
pass: "emit" | "source-to-source",
|
|
88
|
+
): void {
|
|
89
|
+
const binaries = [...new Set(plugins.map((plugin) => plugin.binary))];
|
|
90
|
+
if (binaries.length <= 1) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (plugins.every(isFirstPartyUtilityTransformPlugin)) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const phrase =
|
|
97
|
+
pass === "emit"
|
|
98
|
+
? "multiple compiler native backends cannot share one emit pass"
|
|
99
|
+
: "multiple transform native backends cannot share one source-to-source pass";
|
|
100
|
+
throw new Error(
|
|
101
|
+
"ttsc: " +
|
|
102
|
+
phrase +
|
|
103
|
+
"; compose transform libraries through one aggregate native host",
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -16,6 +16,7 @@ import { readProjectConfig } from "./project/readProjectConfig";
|
|
|
16
16
|
import { resolveBinary } from "./resolveBinary";
|
|
17
17
|
import { resolveTsgo } from "./resolveTsgo";
|
|
18
18
|
import { appendBuildOutput, normalizeBuildOutput } from "./runBuild";
|
|
19
|
+
import { assertSharedHostCompatibility } from "./sharedHostHelpers";
|
|
19
20
|
|
|
20
21
|
/** Transform a project and capture TypeScript source output in memory. */
|
|
21
22
|
export function transformProjectInMemory(options: ITtscCompilerContext): {
|
|
@@ -50,7 +51,7 @@ function transformProjectWithNativeHost(
|
|
|
50
51
|
} {
|
|
51
52
|
const binary = buildNativeCompiler({
|
|
52
53
|
cacheBaseDir: project.root,
|
|
53
|
-
cacheDir: options.cacheDir,
|
|
54
|
+
cacheDir: options.cacheDir ?? options.env?.TTSC_CACHE_DIR,
|
|
54
55
|
packageRoot: packageRootDir(),
|
|
55
56
|
});
|
|
56
57
|
const res = spawnNative(
|
|
@@ -123,7 +124,7 @@ function transformProjectWithPlugins(
|
|
|
123
124
|
typescript: transformed.typescript,
|
|
124
125
|
};
|
|
125
126
|
}
|
|
126
|
-
|
|
127
|
+
assertSharedHostCompatibility(transformers, "source-to-source");
|
|
127
128
|
|
|
128
129
|
const plugin = transformers[0]!;
|
|
129
130
|
const res = spawnNative(
|
|
@@ -235,61 +236,6 @@ function serializeNativePlugins(
|
|
|
235
236
|
);
|
|
236
237
|
}
|
|
237
238
|
|
|
238
|
-
function assertTransformHostCompatibility(
|
|
239
|
-
plugins: readonly ITtscLoadedNativePlugin[],
|
|
240
|
-
): void {
|
|
241
|
-
const binaries = [...new Set(plugins.map((plugin) => plugin.binary))];
|
|
242
|
-
if (binaries.length <= 1) {
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
if (plugins.every(isFirstPartyUtilityTransformPlugin)) {
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
throw new Error(
|
|
249
|
-
"ttsc: multiple transform native backends cannot share one source-to-source pass; " +
|
|
250
|
-
"compose transform libraries through one aggregate native host",
|
|
251
|
-
);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function isFirstPartyUtilityTransformPlugin(
|
|
255
|
-
plugin: ITtscLoadedNativePlugin,
|
|
256
|
-
): boolean {
|
|
257
|
-
if (plugin.stage !== "transform") return false;
|
|
258
|
-
if (!firstPartyUtilityPluginNames.has(plugin.name)) return false;
|
|
259
|
-
const manifest = readNearestPackageManifest(plugin.source);
|
|
260
|
-
return manifest?.name === plugin.name;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const firstPartyUtilityPluginNames = new Set([
|
|
264
|
-
"@ttsc/banner",
|
|
265
|
-
"@ttsc/paths",
|
|
266
|
-
"@ttsc/strip",
|
|
267
|
-
]);
|
|
268
|
-
|
|
269
|
-
function readNearestPackageManifest(
|
|
270
|
-
source: string,
|
|
271
|
-
): { name?: unknown } | undefined {
|
|
272
|
-
try {
|
|
273
|
-
let current = fs.statSync(source).isDirectory()
|
|
274
|
-
? source
|
|
275
|
-
: path.dirname(source);
|
|
276
|
-
for (let i = 0; i < 4; i += 1) {
|
|
277
|
-
const manifest = path.join(current, "package.json");
|
|
278
|
-
if (fs.existsSync(manifest)) {
|
|
279
|
-
return JSON.parse(fs.readFileSync(manifest, "utf8")) as {
|
|
280
|
-
name?: unknown;
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
const parent = path.dirname(current);
|
|
284
|
-
if (parent === current) break;
|
|
285
|
-
current = parent;
|
|
286
|
-
}
|
|
287
|
-
} catch {
|
|
288
|
-
return undefined;
|
|
289
|
-
}
|
|
290
|
-
return undefined;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
239
|
function nativePluginEnv(
|
|
294
240
|
options: ITtscCompilerContext,
|
|
295
241
|
projectRoot: string,
|
|
@@ -10,6 +10,7 @@ import type { TtscPluginStage } from "../../structures/TtscPluginStage";
|
|
|
10
10
|
import type { ITtscLoadedNativePlugin } from "../../structures/internal/ITtscLoadedNativePlugin";
|
|
11
11
|
import type { ITtscParsedProjectConfig } from "../../structures/internal/ITtscParsedProjectConfig";
|
|
12
12
|
import { buildSourcePlugin } from "./buildSourcePlugin";
|
|
13
|
+
import { FIRST_PARTY_UTILITY_PLUGIN_NAMES } from "../../compiler/internal/sharedHostHelpers";
|
|
13
14
|
|
|
14
15
|
type TtscPluginFactory<T = ITtscProjectPluginConfig> = (
|
|
15
16
|
context: ITtscPluginFactoryContext<T>,
|
|
@@ -64,11 +65,14 @@ export function loadProjectPlugins(options: {
|
|
|
64
65
|
projectRoot: project.root,
|
|
65
66
|
tsconfig: project.path,
|
|
66
67
|
};
|
|
67
|
-
const plugins =
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
const plugins = composePluginSources(
|
|
69
|
+
entries,
|
|
70
|
+
entries.map((entry) =>
|
|
71
|
+
loadPluginEntry(
|
|
72
|
+
entry.config,
|
|
73
|
+
{ ...context, plugin: entry.config },
|
|
74
|
+
entry.baseDir,
|
|
75
|
+
),
|
|
72
76
|
),
|
|
73
77
|
);
|
|
74
78
|
|
|
@@ -100,6 +104,101 @@ export function loadProjectPlugins(options: {
|
|
|
100
104
|
};
|
|
101
105
|
}
|
|
102
106
|
|
|
107
|
+
function composePluginSources(
|
|
108
|
+
entries: readonly ProjectPluginEntry[],
|
|
109
|
+
plugins: readonly ITtscPlugin[],
|
|
110
|
+
): ITtscPlugin[] {
|
|
111
|
+
const aggregates = plugins
|
|
112
|
+
.map((plugin, index) => ({ index, plugin }))
|
|
113
|
+
.filter(({ plugin }) => Array.isArray(plugin.composes));
|
|
114
|
+
if (aggregates.length === 0) {
|
|
115
|
+
return [...plugins];
|
|
116
|
+
}
|
|
117
|
+
for (const { plugin } of aggregates) {
|
|
118
|
+
for (const target of plugin.composes!) {
|
|
119
|
+
if (typeof target !== "string" || target.trim() === "") {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`ttsc: plugin "${plugin.name}" has an invalid "composes" target; ` +
|
|
122
|
+
`targets must be non-empty plugin names or transform specifiers`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// First-party utility plugin names are never legal composes targets for
|
|
128
|
+
// user / third-party plugins. The first-party shared compiler host has its
|
|
129
|
+
// own opt-in path through the manifest-pinned whitelist in
|
|
130
|
+
// `sharedHostHelpers.ts::isFirstPartyUtilityTransformPlugin`. Letting any
|
|
131
|
+
// descriptor borrow the binary of `@ttsc/banner` etc. would bypass that
|
|
132
|
+
// pin and turn `composes` into a supply-chain redirect vector.
|
|
133
|
+
for (const { plugin } of aggregates) {
|
|
134
|
+
for (const target of plugin.composes!) {
|
|
135
|
+
if (FIRST_PARTY_UTILITY_PLUGIN_NAMES.has(target)) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`ttsc: plugin "${plugin.name}" cannot compose first-party utility "${target}"; ` +
|
|
138
|
+
`first-party utility plugins are composed automatically through their shared compiler host`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Composition is intentionally one hop only: A.composes=[B] sends B to A's
|
|
144
|
+
// binary, but if B.composes=[C] then C uses B's original source and does NOT
|
|
145
|
+
// cascade to A. Detect cycles (A.composes=[B] && B.composes=[A]) and throw,
|
|
146
|
+
// otherwise the silent reswap below would mis-route both plugins.
|
|
147
|
+
for (const { index: i, plugin: a } of aggregates) {
|
|
148
|
+
for (const { index: j, plugin: b } of aggregates) {
|
|
149
|
+
if (i === j) continue;
|
|
150
|
+
const aTransform = entries[i]?.config.transform;
|
|
151
|
+
const bTransform = entries[j]?.config.transform;
|
|
152
|
+
const aComposesB = a.composes!.some((alias) =>
|
|
153
|
+
matchesPluginAlias(alias, b, bTransform),
|
|
154
|
+
);
|
|
155
|
+
const bComposesA = b.composes!.some((alias) =>
|
|
156
|
+
matchesPluginAlias(alias, a, aTransform),
|
|
157
|
+
);
|
|
158
|
+
if (aComposesB && bComposesA) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`ttsc: plugin composes cycle detected between "${a.name}" and "${b.name}"; ` +
|
|
161
|
+
`each plugin lists the other in its "composes" array — composition is one hop only, not transitive`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return plugins.map((plugin, index) => {
|
|
167
|
+
const transform = entries[index]?.config.transform;
|
|
168
|
+
const matchingAggregates = aggregates.filter(
|
|
169
|
+
({ index: aggregateIndex, plugin: aggregatePlugin }) =>
|
|
170
|
+
aggregateIndex !== index &&
|
|
171
|
+
aggregatePlugin.composes!.some((alias) =>
|
|
172
|
+
matchesPluginAlias(alias, plugin, transform),
|
|
173
|
+
),
|
|
174
|
+
);
|
|
175
|
+
if (matchingAggregates.length > 1) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`ttsc: plugin "${plugin.name}" is composed by multiple aggregate plugins; ` +
|
|
178
|
+
`each plugin entry can be redirected to only one aggregate native host`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
const aggregate = matchingAggregates[0];
|
|
182
|
+
return aggregate === undefined
|
|
183
|
+
? plugin
|
|
184
|
+
: {
|
|
185
|
+
...plugin,
|
|
186
|
+
source: aggregate.plugin.source,
|
|
187
|
+
};
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function matchesPluginAlias(
|
|
192
|
+
alias: string,
|
|
193
|
+
plugin: ITtscPlugin,
|
|
194
|
+
transform: ITtscProjectPluginConfig["transform"],
|
|
195
|
+
): boolean {
|
|
196
|
+
return (
|
|
197
|
+
alias === plugin.name ||
|
|
198
|
+
(typeof transform === "string" && alias === transform)
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
103
202
|
export function hasProjectPluginEntries(
|
|
104
203
|
project: ITtscParsedProjectConfig,
|
|
105
204
|
entries?: readonly ITtscProjectPluginConfig[] | false,
|
|
@@ -45,6 +45,17 @@ export interface ITtscPlugin {
|
|
|
45
45
|
*/
|
|
46
46
|
source: string;
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Other transform plugin names or transform specifiers that this native
|
|
50
|
+
* sidecar can execute in the same compiler pass.
|
|
51
|
+
*
|
|
52
|
+
* Package auto-discovery may find multiple transform packages that must share
|
|
53
|
+
* one emit host. When one descriptor lists another entry here, ttsc keeps the
|
|
54
|
+
* original plugin config in `--plugins-json` but points the composed entry at
|
|
55
|
+
* this descriptor's native source so both entries resolve to one binary.
|
|
56
|
+
*/
|
|
57
|
+
composes?: string[];
|
|
58
|
+
|
|
48
59
|
/**
|
|
49
60
|
* Pipeline stage implemented by the sidecar.
|
|
50
61
|
*
|
package/tools/gen_shims/main.go
CHANGED
|
@@ -429,18 +429,38 @@ func main() {
|
|
|
429
429
|
shimHeaderBuilder.WriteString("\n")
|
|
430
430
|
|
|
431
431
|
shimGoPath := path.Join(shimDirPath, "shim.go")
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
432
|
+
if isHandMaintained(shimGoPath) {
|
|
433
|
+
fmt.Fprintf(os.Stderr, "gen_shims: skipping %s (hand-maintained marker present; remove '// gen_shims:hand-maintained' to regenerate)\n", shimGoPath)
|
|
434
|
+
} else {
|
|
435
|
+
file, err := os.Create(shimGoPath)
|
|
436
|
+
if err != nil {
|
|
437
|
+
log.Fatalf("gen_shims: opening %v for write: %v", shimGoPath, err)
|
|
438
|
+
}
|
|
439
|
+
file.WriteString(shimHeaderBuilder.String())
|
|
440
|
+
file.WriteString(shimBuilder.String())
|
|
441
|
+
if err := file.Close(); err != nil {
|
|
442
|
+
log.Fatalf("gen_shims: closing %v: %v", shimGoPath, err)
|
|
443
|
+
}
|
|
444
|
+
fmt.Fprintf(os.Stdout, "gen_shims: wrote %s (%d bytes)\n", shimGoPath, len(shimBuilder.String()))
|
|
440
445
|
}
|
|
441
|
-
fmt.Fprintf(os.Stdout, "gen_shims: wrote %s (%d bytes)\n", shimGoPath, len(shimBuilder.String()))
|
|
442
446
|
|
|
443
447
|
shimHeaderBuilder.Reset()
|
|
444
448
|
shimBuilder.Reset()
|
|
445
449
|
}
|
|
446
450
|
}
|
|
451
|
+
|
|
452
|
+
// isHandMaintained reports whether the given shim file opts out of regeneration
|
|
453
|
+
// by carrying the magic marker `// gen_shims:hand-maintained` within its first
|
|
454
|
+
// 200 bytes. Hand-maintained shim files combine generated re-exports with
|
|
455
|
+
// `go:linkname` declarations or other content that gen_shims cannot reproduce;
|
|
456
|
+
// silently overwriting them would break consumers (e.g. @ttsc/lint).
|
|
457
|
+
func isHandMaintained(shimGoPath string) bool {
|
|
458
|
+
file, err := os.Open(shimGoPath)
|
|
459
|
+
if err != nil {
|
|
460
|
+
return false
|
|
461
|
+
}
|
|
462
|
+
defer file.Close()
|
|
463
|
+
buf := make([]byte, 200)
|
|
464
|
+
n, _ := file.Read(buf)
|
|
465
|
+
return strings.Contains(string(buf[:n]), "gen_shims:hand-maintained")
|
|
466
|
+
}
|