ttsc 0.10.2 → 0.11.0-dev.20260517
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 +8 -0
- package/cmd/ttsc/api_compile.go +106 -106
- package/cmd/ttsc/api_transform.go +54 -54
- package/cmd/ttsc/main.go +103 -103
- package/cmd/ttscserver/helpers_test.go +27 -0
- package/cmd/ttscserver/main.go +154 -0
- package/cmd/ttscserver/run_lsp_reports_getwd_error_test.go +34 -0
- package/cmd/ttscserver/run_lsp_reports_server_error_test.go +42 -0
- package/driver/lsp_envelope.go +136 -0
- package/driver/lsp_frame.go +121 -0
- package/driver/lsp_plugin.go +150 -0
- package/driver/lsp_proxy.go +394 -0
- package/driver/lsp_server.go +197 -0
- package/go.mod +4 -0
- package/go.sum +4 -0
- package/internal/cwd/cwd.go +8 -8
- package/lib/TtscCompiler.d.ts +15 -16
- package/lib/TtscCompiler.js +15 -16
- package/lib/TtscCompiler.js.map +1 -1
- package/lib/compiler/internal/compileProjectInMemory.d.ts +4 -1
- package/lib/compiler/internal/compileProjectInMemory.js +4 -1
- package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
- package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
- package/lib/compiler/internal/runBuild.js +43 -1
- package/lib/compiler/internal/runBuild.js.map +1 -1
- package/lib/compiler/internal/sharedHostHelpers.d.ts +12 -12
- package/lib/compiler/internal/sharedHostHelpers.js +12 -12
- package/lib/defineTtscPlugin.d.ts +18 -0
- package/lib/defineTtscPlugin.js +7 -0
- package/lib/defineTtscPlugin.js.map +1 -0
- package/lib/index.d.ts +4 -3
- package/lib/index.js +4 -3
- package/lib/index.js.map +1 -1
- package/lib/launcher/internal/prepareExecution.js +1 -1
- package/lib/launcher/internal/prepareExecution.js.map +1 -1
- package/lib/launcher/internal/resolveTtscserverBinary.d.ts +12 -0
- package/lib/launcher/internal/resolveTtscserverBinary.js +43 -0
- package/lib/launcher/internal/resolveTtscserverBinary.js.map +1 -0
- package/lib/launcher/internal/runTtsc.js +47 -8
- package/lib/launcher/internal/runTtsc.js.map +1 -1
- package/lib/launcher/internal/runTtscserver.d.ts +20 -0
- package/lib/launcher/internal/runTtscserver.js +125 -0
- package/lib/launcher/internal/runTtscserver.js.map +1 -0
- package/lib/launcher/internal/runTtsx.js +1 -1
- package/lib/launcher/internal/runTtsx.js.map +1 -1
- package/lib/launcher/ttscserver.d.ts +2 -0
- package/lib/launcher/ttscserver.js +9 -0
- package/lib/launcher/ttscserver.js.map +1 -0
- package/lib/plugin/internal/buildSourcePlugin.d.ts +9 -0
- package/lib/plugin/internal/buildSourcePlugin.js +197 -2
- package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
- package/lib/plugin/internal/loadProjectPlugins.js +86 -8
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/lib/structures/ITtscCompilerContext.d.ts +1 -1
- package/lib/structures/ITtscCompilerDiagnostic.d.ts +3 -3
- package/lib/structures/ITtscCompilerResult.d.ts +10 -22
- package/lib/structures/ITtscCompilerTransformation.d.ts +2 -2
- package/lib/structures/ITtscPlugin.d.ts +37 -4
- package/lib/structures/ITtscPlugin.js +0 -4
- package/lib/structures/ITtscPlugin.js.map +1 -1
- package/lib/structures/ITtscPluginContributor.d.ts +26 -0
- package/lib/structures/ITtscPluginContributor.js +3 -0
- package/lib/structures/ITtscPluginContributor.js.map +1 -0
- package/lib/structures/ITtscPluginFactoryContext.d.ts +5 -5
- package/lib/structures/ITtscProjectPluginConfig.d.ts +2 -2
- package/lib/structures/TtscPluginStage.d.ts +4 -3
- package/lib/structures/index.d.ts +1 -0
- package/lib/structures/index.js +1 -0
- package/lib/structures/index.js.map +1 -1
- package/lib/structures/internal/ITtscLoadedNativePlugin.d.ts +3 -0
- package/lib/structures/internal/TtscBuildOptions.d.ts +13 -0
- package/lib/structures/internal/TtscCommonOptions.d.ts +5 -2
- package/package.json +10 -9
- package/shim/lsp/extra-shim.json +4 -0
- package/shim/lsp/go.mod +16 -0
- package/shim/lsp/go.sum +26 -0
- package/shim/lsp/shim.go +42 -0
- package/shim/printer/shim.go +5 -5
- package/src/TtscCompiler.ts +15 -16
- package/src/compiler/internal/compileProjectInMemory.ts +4 -1
- package/src/compiler/internal/project/readProjectConfig.ts +0 -1
- package/src/compiler/internal/runBuild.ts +43 -1
- package/src/compiler/internal/sharedHostHelpers.ts +12 -12
- package/src/defineTtscPlugin.ts +26 -0
- package/src/index.ts +4 -3
- package/src/launcher/internal/prepareExecution.ts +1 -1
- package/src/launcher/internal/resolveTtscserverBinary.ts +51 -0
- package/src/launcher/internal/runTtsc.ts +53 -11
- package/src/launcher/internal/runTtscserver.ts +92 -0
- package/src/launcher/internal/runTtsx.ts +1 -1
- package/src/launcher/ttscserver.ts +8 -0
- package/src/plugin/internal/buildSourcePlugin.ts +231 -12
- package/src/plugin/internal/loadProjectPlugins.ts +108 -8
- package/src/structures/ITtscCompilerContext.ts +1 -1
- package/src/structures/ITtscCompilerDiagnostic.ts +3 -3
- package/src/structures/ITtscCompilerResult.ts +10 -22
- package/src/structures/ITtscCompilerTransformation.ts +2 -2
- package/src/structures/ITtscPlugin.ts +37 -11
- package/src/structures/ITtscPluginContributor.ts +27 -0
- package/src/structures/ITtscPluginFactoryContext.ts +5 -5
- package/src/structures/ITtscProjectPluginConfig.ts +2 -2
- package/src/structures/TtscPluginStage.ts +4 -3
- package/src/structures/index.ts +1 -0
- package/src/structures/internal/ITtscLoadedNativePlugin.ts +3 -0
- package/src/structures/internal/TtscBuildOptions.ts +13 -0
- package/src/structures/internal/TtscCommonOptions.ts +5 -2
|
@@ -12,12 +12,43 @@ const TSGO_GO_MODULE_PATH = "github.com/microsoft/typescript-go";
|
|
|
12
12
|
const PRUNE_DIRS = new Set(["node_modules", ".git", ".ttsc"]);
|
|
13
13
|
const GENERATED_WORKSPACE_FILES = new Set(["go.work", "go.work.sum"]);
|
|
14
14
|
|
|
15
|
+
// Go env vars that change the produced binary. Hashed into the plugin
|
|
16
|
+
// build cache key so cross-compile target / build tags / cgo toggle
|
|
17
|
+
// don't collide with the native-platform key.
|
|
18
|
+
const GO_BUILD_ENV_KEYS: readonly string[] = [
|
|
19
|
+
"GOOS",
|
|
20
|
+
"GOARCH",
|
|
21
|
+
"GOAMD64",
|
|
22
|
+
"GOARM",
|
|
23
|
+
"GO386",
|
|
24
|
+
"GOMIPS",
|
|
25
|
+
"GOMIPS64",
|
|
26
|
+
"GOPPC64",
|
|
27
|
+
"GORISCV64",
|
|
28
|
+
"GOWASM",
|
|
29
|
+
"GOFLAGS",
|
|
30
|
+
"GOEXPERIMENT",
|
|
31
|
+
"CGO_ENABLED",
|
|
32
|
+
"GOTOOLCHAIN",
|
|
33
|
+
];
|
|
34
|
+
const CONTRIBUTIONS_FILE_NAME = "ttsc_contributions.go";
|
|
35
|
+
const CONTRIB_DIRNAME = "contrib";
|
|
36
|
+
|
|
37
|
+
/** One contributor's resolved Go source plus its target sub-package name. */
|
|
38
|
+
export interface ITtscBuildContributor {
|
|
39
|
+
/** Sub-package suffix: scratch lands at `<host>/contrib/<name>/`. */
|
|
40
|
+
name: string;
|
|
41
|
+
/** Absolute path to the contributor's source directory. */
|
|
42
|
+
source: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
15
45
|
/** Build one Go source plugin into a cached executable. */
|
|
16
46
|
export function buildSourcePlugin(opts: {
|
|
17
47
|
source: string;
|
|
18
48
|
pluginName: string;
|
|
19
49
|
baseDir: string;
|
|
20
50
|
cacheDir?: string;
|
|
51
|
+
contributors?: readonly ITtscBuildContributor[];
|
|
21
52
|
label?: string;
|
|
22
53
|
overlayDirs?: readonly string[];
|
|
23
54
|
quiet?: boolean;
|
|
@@ -26,9 +57,11 @@ export function buildSourcePlugin(opts: {
|
|
|
26
57
|
}): string {
|
|
27
58
|
const { dir, entry, source } = resolveSourceBuildTarget(opts);
|
|
28
59
|
const overlayDirs = opts.overlayDirs ?? findTtscOverlayDirs();
|
|
60
|
+
const contributors = opts.contributors ?? [];
|
|
29
61
|
const goBinary = resolveGoCompiler();
|
|
30
62
|
ensureExecutableGoToolchain(goBinary);
|
|
31
63
|
const key = computeCacheKey({
|
|
64
|
+
contributors,
|
|
32
65
|
dir,
|
|
33
66
|
entry,
|
|
34
67
|
goBinary,
|
|
@@ -46,8 +79,14 @@ export function buildSourcePlugin(opts: {
|
|
|
46
79
|
fs.mkdirSync(cacheDir, { recursive: true });
|
|
47
80
|
const label = opts.label ?? "source plugin";
|
|
48
81
|
if (opts.quiet !== true) {
|
|
82
|
+
const extra =
|
|
83
|
+
contributors.length === 0
|
|
84
|
+
? ""
|
|
85
|
+
: ` + ${contributors.length} contributor(s): ${contributors
|
|
86
|
+
.map((c) => c.name)
|
|
87
|
+
.join(", ")}`;
|
|
49
88
|
process.stderr.write(
|
|
50
|
-
`ttsc: building ${label} "${opts.pluginName}" from ${source} (this runs once per cache key)\n`,
|
|
89
|
+
`ttsc: building ${label} "${opts.pluginName}" from ${source}${extra} (this runs once per cache key)\n`,
|
|
51
90
|
);
|
|
52
91
|
}
|
|
53
92
|
|
|
@@ -56,6 +95,16 @@ export function buildSourcePlugin(opts: {
|
|
|
56
95
|
);
|
|
57
96
|
try {
|
|
58
97
|
materializeScratchDir(dir, scratchDir);
|
|
98
|
+
const goModReader = createGoModReader(goBinary, opts.pluginName);
|
|
99
|
+
if (contributors.length > 0) {
|
|
100
|
+
mergeContributors({
|
|
101
|
+
contributors,
|
|
102
|
+
entry,
|
|
103
|
+
goModReader,
|
|
104
|
+
pluginName: opts.pluginName,
|
|
105
|
+
scratchDir,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
59
108
|
writeGoWork(scratchDir, overlayDirs, goBinary, opts.pluginName);
|
|
60
109
|
const scratchBinaryName =
|
|
61
110
|
process.platform === "win32" ? ".ttsc-plugin.exe" : ".ttsc-plugin";
|
|
@@ -68,6 +117,128 @@ export function buildSourcePlugin(opts: {
|
|
|
68
117
|
}
|
|
69
118
|
}
|
|
70
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Copy every contributor's Go source into a sub-package of the host module and
|
|
122
|
+
* synthesize a blank-import file alongside the host's entry package so each
|
|
123
|
+
* contributor's `init()` runs before `main`.
|
|
124
|
+
*
|
|
125
|
+
* - Sources land at `<scratch>/<CONTRIB_DIRNAME>/<name>/` (recursive copy with
|
|
126
|
+
* the same pruning rules used for the host source).
|
|
127
|
+
* - The entry directory receives `<CONTRIBUTIONS_FILE_NAME>` containing one
|
|
128
|
+
* blank-import per contributor. The host's module path is read from the
|
|
129
|
+
* materialized go.mod, so the import path is always correct for the host
|
|
130
|
+
* plugin's actual module declaration.
|
|
131
|
+
* - Contributors that ship their own `go.mod` are rejected — the design relies on
|
|
132
|
+
* the contributor living inside the host's module so that workspace overlay
|
|
133
|
+
* rules and the host's `go.sum` cover transitive dependencies. This also
|
|
134
|
+
* closes the supply-chain hole where a contributor could otherwise pull in
|
|
135
|
+
* arbitrary Go modules.
|
|
136
|
+
*/
|
|
137
|
+
function mergeContributors(opts: {
|
|
138
|
+
contributors: readonly ITtscBuildContributor[];
|
|
139
|
+
entry: string;
|
|
140
|
+
goModReader: GoModReader;
|
|
141
|
+
pluginName: string;
|
|
142
|
+
scratchDir: string;
|
|
143
|
+
}): void {
|
|
144
|
+
const hostModulePath = opts.goModReader.read(opts.scratchDir).modulePath;
|
|
145
|
+
if (hostModulePath === null || hostModulePath === "") {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`ttsc: plugin "${opts.pluginName}" cannot accept contributors because its module ` +
|
|
148
|
+
`root has no resolvable go.mod module path`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
const contribRoot = path.join(opts.scratchDir, CONTRIB_DIRNAME);
|
|
152
|
+
// Refuse to merge when the host plugin's own source already owns a
|
|
153
|
+
// `contrib/` directory. We'd otherwise silently merge contributor
|
|
154
|
+
// files into a pre-populated host package and ship a hybrid binary
|
|
155
|
+
// whose contents nobody declared. Loud failure is the only safe
|
|
156
|
+
// option — the host plugin must rename its directory or the
|
|
157
|
+
// contributor system must use a different sub-package root.
|
|
158
|
+
if (fs.existsSync(contribRoot)) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`ttsc: plugin "${opts.pluginName}" already ships a ${CONTRIB_DIRNAME}/ directory in its source; ` +
|
|
161
|
+
`contributor merge would silently overwrite. Rename the host plugin's directory to a different name.`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
fs.mkdirSync(contribRoot, { recursive: true });
|
|
165
|
+
// Sort contributors by name so the synthesized `ttsc_contributions.go`
|
|
166
|
+
// emits blank imports in a deterministic order independent of
|
|
167
|
+
// declaration order. The cache key is already sort-stable
|
|
168
|
+
// (`computeCacheKey` sorts contributors by name), so without this
|
|
169
|
+
// matching sort the SAME cache key could correspond to two distinct
|
|
170
|
+
// binaries whose `init()` sequence across contributors differs by
|
|
171
|
+
// import order.
|
|
172
|
+
const sortedContributors = [...opts.contributors].sort((a, b) =>
|
|
173
|
+
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
|
|
174
|
+
);
|
|
175
|
+
const imports: string[] = [];
|
|
176
|
+
for (const contributor of sortedContributors) {
|
|
177
|
+
if (fs.existsSync(path.join(contributor.source, "go.mod"))) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`ttsc: plugin "${opts.pluginName}" contributor "${contributor.name}" must ship Go ` +
|
|
180
|
+
`source as a package, not a module (go.mod found at ${contributor.source}/go.mod). ` +
|
|
181
|
+
`Remove go.mod so the contributor compiles inside the host module's dependency graph.`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
const target = path.join(contribRoot, contributor.name);
|
|
185
|
+
if (fs.existsSync(target)) {
|
|
186
|
+
// Defensive: validatePluginContributors already rejects duplicate
|
|
187
|
+
// names, and the contribRoot-existence guard above blocks the
|
|
188
|
+
// host plugin from pre-shipping a `contrib/` directory. Reaching
|
|
189
|
+
// this branch implies an upstream contract break. Fail loud
|
|
190
|
+
// rather than overwrite.
|
|
191
|
+
throw new Error(
|
|
192
|
+
`ttsc: plugin "${opts.pluginName}" contributor "${contributor.name}" target ${target} already exists; ` +
|
|
193
|
+
`contributor names must be unique within one plugin build`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
fs.cpSync(contributor.source, target, {
|
|
197
|
+
recursive: true,
|
|
198
|
+
filter: (src) => {
|
|
199
|
+
const base = path.basename(src);
|
|
200
|
+
if (shouldPruneDirectory(base)) return false;
|
|
201
|
+
if (shouldOmitSourceFile(base)) return false;
|
|
202
|
+
return true;
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
imports.push(`${hostModulePath}/${CONTRIB_DIRNAME}/${contributor.name}`);
|
|
206
|
+
}
|
|
207
|
+
const entryDir = path.resolve(opts.scratchDir, opts.entry);
|
|
208
|
+
fs.mkdirSync(entryDir, { recursive: true });
|
|
209
|
+
const contributionsPath = path.join(entryDir, CONTRIBUTIONS_FILE_NAME);
|
|
210
|
+
// Same reasoning as the contribRoot guard: when entry resolves to the
|
|
211
|
+
// module root (`entry === "."`), entryDir == scratchDir and a
|
|
212
|
+
// pre-existing `ttsc_contributions.go` from the host plugin's own
|
|
213
|
+
// source would be silently overwritten by the generator below.
|
|
214
|
+
if (fs.existsSync(contributionsPath)) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
`ttsc: plugin "${opts.pluginName}" already ships ${CONTRIBUTIONS_FILE_NAME} in its entry package; ` +
|
|
217
|
+
`that filename is reserved for the contributor blank-import generator. Rename the host's file.`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
writeContributionsFile(contributionsPath, imports);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function writeContributionsFile(filePath: string, imports: string[]): void {
|
|
224
|
+
const importLines = imports
|
|
225
|
+
.map((spec) => `\t_ ${JSON.stringify(spec)}`)
|
|
226
|
+
.join("\n");
|
|
227
|
+
const body = `// Code generated by ttsc — DO NOT EDIT.
|
|
228
|
+
//
|
|
229
|
+
// This file is synthesized by ttsc's plugin builder when the host plugin
|
|
230
|
+
// descriptor declares "contributors". The blank imports below pull each
|
|
231
|
+
// contributor sub-package into the build so its init() runs before main.
|
|
232
|
+
|
|
233
|
+
package main
|
|
234
|
+
|
|
235
|
+
import (
|
|
236
|
+
${importLines}
|
|
237
|
+
)
|
|
238
|
+
`;
|
|
239
|
+
fs.writeFileSync(filePath, body, "utf8");
|
|
240
|
+
}
|
|
241
|
+
|
|
71
242
|
function publishBuiltBinary(builtBinary: string, binaryPath: string): void {
|
|
72
243
|
const pending = `${binaryPath}.${process.pid}.${Date.now()}-${Math.random()
|
|
73
244
|
.toString(16)
|
|
@@ -88,6 +259,30 @@ function publishBuiltBinary(builtBinary: string, binaryPath: string): void {
|
|
|
88
259
|
return;
|
|
89
260
|
}
|
|
90
261
|
throw error;
|
|
262
|
+
} finally {
|
|
263
|
+
// Best-effort sweep of any leftover `.tmp` siblings from a prior
|
|
264
|
+
// crash between copyFileSync and renameSync. Same-directory pending
|
|
265
|
+
// names guarantee the rename stays a same-filesystem atomic op, so
|
|
266
|
+
// we accept the GC cost rather than move pending files to os.tmpdir.
|
|
267
|
+
pruneOrphanPendingBinaries(binaryPath);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function pruneOrphanPendingBinaries(binaryPath: string): void {
|
|
272
|
+
// Only sweep pending files owned by THIS process. Concurrent ttsc
|
|
273
|
+
// invocations (two `ttsc --watch` shells against the same project)
|
|
274
|
+
// may have their own `<binary>.<their-pid>.*.tmp` mid-flight, and
|
|
275
|
+
// deleting them would race their renameSync into ENOENT.
|
|
276
|
+
try {
|
|
277
|
+
const dir = path.dirname(binaryPath);
|
|
278
|
+
const prefix = `${path.basename(binaryPath)}.${process.pid}.`;
|
|
279
|
+
for (const name of fs.readdirSync(dir)) {
|
|
280
|
+
if (name.startsWith(prefix) && name.endsWith(".tmp")) {
|
|
281
|
+
fs.rmSync(path.join(dir, name), { force: true });
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
} catch {
|
|
285
|
+
// Best-effort; never mask the underlying publish outcome.
|
|
91
286
|
}
|
|
92
287
|
}
|
|
93
288
|
|
|
@@ -171,20 +366,12 @@ function writeGoWork(
|
|
|
171
366
|
pluginName: string,
|
|
172
367
|
): void {
|
|
173
368
|
const goModReader = createGoModReader(goBinary, pluginName);
|
|
174
|
-
validateSourceReplacements(
|
|
175
|
-
scratchDir,
|
|
176
|
-
useDirs,
|
|
177
|
-
goModReader,
|
|
178
|
-
pluginName,
|
|
179
|
-
);
|
|
369
|
+
validateSourceReplacements(scratchDir, useDirs, goModReader, pluginName);
|
|
180
370
|
const useLines = ["\t."];
|
|
181
371
|
for (const dir of useDirs) {
|
|
182
372
|
useLines.push(`\t${dir.replace(/\\/g, "/")}`);
|
|
183
373
|
}
|
|
184
|
-
const replaceLines = sourceBuildWorkspaceReplacements(
|
|
185
|
-
useDirs,
|
|
186
|
-
goModReader,
|
|
187
|
-
);
|
|
374
|
+
const replaceLines = sourceBuildWorkspaceReplacements(useDirs, goModReader);
|
|
188
375
|
const replaceBlock =
|
|
189
376
|
replaceLines.length === 0 ? "" : `\n\n${replaceLines.join("\n")}\n`;
|
|
190
377
|
const goWork = `go 1.26\n\nuse (\n${useLines.join("\n")}\n)${replaceBlock}`;
|
|
@@ -582,6 +769,7 @@ function resolveGoCompiler(): string {
|
|
|
582
769
|
}
|
|
583
770
|
|
|
584
771
|
export function computeCacheKey(inputs: {
|
|
772
|
+
contributors?: readonly ITtscBuildContributor[];
|
|
585
773
|
dir: string;
|
|
586
774
|
entry: string;
|
|
587
775
|
goBinary?: string;
|
|
@@ -597,10 +785,34 @@ export function computeCacheKey(inputs: {
|
|
|
597
785
|
if (inputs.goBinary !== undefined) {
|
|
598
786
|
hash.update(`go=${resolveGoCompilerIdentity(inputs.goBinary)}\n`);
|
|
599
787
|
}
|
|
788
|
+
// Hash the Go env vars that change `go build`'s output (cross-compile
|
|
789
|
+
// target, build tags, cgo toggle, etc.). Without this, a user setting
|
|
790
|
+
// `GOOS=linux GOARCH=arm64` on a darwin host would write a Linux-arm64
|
|
791
|
+
// binary into the same cache slot as the native build, and the next
|
|
792
|
+
// native invocation would spawn an unrunnable artifact.
|
|
793
|
+
for (const key of GO_BUILD_ENV_KEYS) {
|
|
794
|
+
const value = process.env[key];
|
|
795
|
+
if (value !== undefined && value !== "") {
|
|
796
|
+
hash.update(`${key}=${value}\n`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
600
799
|
hashSourceDirectory(hash, "plugin", inputs.dir);
|
|
601
800
|
for (const [index, dir] of [...(inputs.overlayDirs ?? [])].sort().entries()) {
|
|
602
801
|
hashSourceDirectory(hash, `overlay:${index}`, dir);
|
|
603
802
|
}
|
|
803
|
+
// Hash contributors in sorted-by-name order so two consumers with the
|
|
804
|
+
// same logical set produce the same key regardless of declaration order
|
|
805
|
+
// in the host's plugin descriptor.
|
|
806
|
+
const sortedContributors = [...(inputs.contributors ?? [])].sort((a, b) =>
|
|
807
|
+
a.name === b.name ? 0 : a.name < b.name ? -1 : 1,
|
|
808
|
+
);
|
|
809
|
+
for (const contributor of sortedContributors) {
|
|
810
|
+
hashSourceDirectory(
|
|
811
|
+
hash,
|
|
812
|
+
`contributor:${contributor.name}`,
|
|
813
|
+
contributor.source,
|
|
814
|
+
);
|
|
815
|
+
}
|
|
604
816
|
return hash.digest("hex").slice(0, 32);
|
|
605
817
|
}
|
|
606
818
|
|
|
@@ -651,7 +863,14 @@ function shouldPruneDirectory(name: string): boolean {
|
|
|
651
863
|
}
|
|
652
864
|
|
|
653
865
|
function shouldOmitSourceFile(name: string): boolean {
|
|
654
|
-
|
|
866
|
+
if (GENERATED_WORKSPACE_FILES.has(name)) return true;
|
|
867
|
+
// npm-pack tarballs and macOS/Windows editor sidecars are local
|
|
868
|
+
// build artifacts that drift independently of the Go source. They
|
|
869
|
+
// would otherwise enter the cache key and bust the cached binary on
|
|
870
|
+
// every unrelated `npm pack` or editor save.
|
|
871
|
+
if (name.endsWith(".tgz") || name.endsWith(".tar.gz")) return true;
|
|
872
|
+
if (name === ".DS_Store" || name === "Thumbs.db") return true;
|
|
873
|
+
return false;
|
|
655
874
|
}
|
|
656
875
|
|
|
657
876
|
function isHashableFile(name: string): boolean {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { createRequire } from "node:module";
|
|
2
1
|
import fs from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
5
|
import { readProjectConfig } from "../../compiler/internal/project/readProjectConfig";
|
|
6
|
+
import { FIRST_PARTY_UTILITY_PLUGIN_NAMES } from "../../compiler/internal/sharedHostHelpers";
|
|
6
7
|
import type { ITtscPlugin } from "../../structures/ITtscPlugin";
|
|
7
8
|
import type { ITtscPluginFactoryContext } from "../../structures/ITtscPluginFactoryContext";
|
|
8
9
|
import type { ITtscProjectPluginConfig } from "../../structures/ITtscProjectPluginConfig";
|
|
@@ -10,7 +11,6 @@ import type { TtscPluginStage } from "../../structures/TtscPluginStage";
|
|
|
10
11
|
import type { ITtscLoadedNativePlugin } from "../../structures/internal/ITtscLoadedNativePlugin";
|
|
11
12
|
import type { ITtscParsedProjectConfig } from "../../structures/internal/ITtscParsedProjectConfig";
|
|
12
13
|
import { buildSourcePlugin } from "./buildSourcePlugin";
|
|
13
|
-
import { FIRST_PARTY_UTILITY_PLUGIN_NAMES } from "../../compiler/internal/sharedHostHelpers";
|
|
14
14
|
|
|
15
15
|
type TtscPluginFactory<T = ITtscProjectPluginConfig> = (
|
|
16
16
|
context: ITtscPluginFactoryContext<T>,
|
|
@@ -82,9 +82,11 @@ export function loadProjectPlugins(options: {
|
|
|
82
82
|
plugins.forEach((plugin, index) => {
|
|
83
83
|
const stage = resolvePluginStage(plugin);
|
|
84
84
|
validatePluginSource(plugin);
|
|
85
|
+
const contributors = validatePluginContributors(plugin);
|
|
85
86
|
const binary = buildSourcePlugin({
|
|
86
87
|
baseDir: context.projectRoot,
|
|
87
88
|
cacheDir: options.cacheDir,
|
|
89
|
+
contributors,
|
|
88
90
|
pluginName: plugin.name,
|
|
89
91
|
source: plugin.source,
|
|
90
92
|
ttscVersion,
|
|
@@ -93,6 +95,7 @@ export function loadProjectPlugins(options: {
|
|
|
93
95
|
nativePlugins.push({
|
|
94
96
|
binary,
|
|
95
97
|
config: entries[index]!.config,
|
|
98
|
+
contributors,
|
|
96
99
|
name: plugin.name,
|
|
97
100
|
source: plugin.source,
|
|
98
101
|
stage,
|
|
@@ -179,12 +182,25 @@ function composePluginSources(
|
|
|
179
182
|
);
|
|
180
183
|
}
|
|
181
184
|
const aggregate = matchingAggregates[0];
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
185
|
+
if (aggregate === undefined) {
|
|
186
|
+
return plugin;
|
|
187
|
+
}
|
|
188
|
+
// A composed plugin's source is rerouted to the aggregate's binary,
|
|
189
|
+
// so its own `contributors` would link into a different host than
|
|
190
|
+
// it was authored against. The "one binary" guarantee in the
|
|
191
|
+
// protocol doc holds only when the composed plugin defers entirely
|
|
192
|
+
// to the aggregate; reject early instead of silently producing two
|
|
193
|
+
// diverging binaries.
|
|
194
|
+
if (plugin.contributors && plugin.contributors.length > 0) {
|
|
195
|
+
throw new Error(
|
|
196
|
+
`ttsc: plugin "${plugin.name}" is composed by "${aggregate.plugin.name}" but declares its own "contributors"; ` +
|
|
197
|
+
`move the contributors onto the aggregate plugin or drop the composes redirect`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
...plugin,
|
|
202
|
+
source: aggregate.plugin.source,
|
|
203
|
+
};
|
|
188
204
|
});
|
|
189
205
|
}
|
|
190
206
|
|
|
@@ -511,10 +527,94 @@ function validatePluginSource(plugin: ITtscPlugin): void {
|
|
|
511
527
|
}
|
|
512
528
|
}
|
|
513
529
|
|
|
530
|
+
const CONTRIBUTOR_NAME_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
531
|
+
|
|
532
|
+
function validatePluginContributors(
|
|
533
|
+
plugin: ITtscPlugin,
|
|
534
|
+
): readonly { name: string; source: string }[] | undefined {
|
|
535
|
+
const contributors = plugin.contributors;
|
|
536
|
+
if (contributors === undefined) return undefined;
|
|
537
|
+
if (!Array.isArray(contributors)) {
|
|
538
|
+
throw new Error(
|
|
539
|
+
`ttsc: plugin "${plugin.name}" "contributors" must be an array of { name, source } entries`,
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
if (contributors.length === 0) return undefined;
|
|
543
|
+
const seen = new Set<string>();
|
|
544
|
+
const out: { name: string; source: string }[] = [];
|
|
545
|
+
for (const [index, entry] of contributors.entries()) {
|
|
546
|
+
if (typeof entry !== "object" || entry === null) {
|
|
547
|
+
throw new Error(
|
|
548
|
+
`ttsc: plugin "${plugin.name}" contributors[${index}] must be an object`,
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
const { name, source } = entry as { name?: unknown; source?: unknown };
|
|
552
|
+
if (typeof name !== "string" || !CONTRIBUTOR_NAME_PATTERN.test(name)) {
|
|
553
|
+
throw new Error(
|
|
554
|
+
`ttsc: plugin "${plugin.name}" contributors[${index}].name must match /^[a-z][a-z0-9_]*$/; ` +
|
|
555
|
+
`got ${JSON.stringify(name)}`,
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
if (seen.has(name)) {
|
|
559
|
+
throw new Error(
|
|
560
|
+
`ttsc: plugin "${plugin.name}" contributors[${index}] duplicate name ${JSON.stringify(name)}`,
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
seen.add(name);
|
|
564
|
+
if (typeof source !== "string" || source.length === 0) {
|
|
565
|
+
throw new Error(
|
|
566
|
+
`ttsc: plugin "${plugin.name}" contributors[${index}].source must be a non-empty string`,
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
if (!path.isAbsolute(source)) {
|
|
570
|
+
throw new Error(
|
|
571
|
+
`ttsc: plugin "${plugin.name}" contributors[${index}].source must be an absolute path; ` +
|
|
572
|
+
`got ${JSON.stringify(source)}`,
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
if (!fs.existsSync(source) || !fs.statSync(source).isDirectory()) {
|
|
576
|
+
throw new Error(
|
|
577
|
+
`ttsc: plugin "${plugin.name}" contributors[${index}].source must be an existing directory: ${source}`,
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
// Pre-flight check that the directory actually carries a buildable
|
|
581
|
+
// contributor package. Without this, an accidentally-empty directory
|
|
582
|
+
// (or a directory containing only `_test.go` files, which `go build`
|
|
583
|
+
// silently skips) reaches the synthesized blank-import step and Go's
|
|
584
|
+
// compile error surfaces with a scratch-tempdir path that doesn't
|
|
585
|
+
// name the contributor entry. Catching it here lets us name the
|
|
586
|
+
// entry the user actually authored.
|
|
587
|
+
if (!hasBuildableGoSource(source)) {
|
|
588
|
+
throw new Error(
|
|
589
|
+
`ttsc: plugin "${plugin.name}" contributors[${index}].source must contain at least one non-test ".go" file: ${source}`,
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
out.push({ name, source: resolveRealPath(source) });
|
|
593
|
+
}
|
|
594
|
+
return out;
|
|
595
|
+
}
|
|
596
|
+
|
|
514
597
|
function isPluginStage(value: string): value is TtscPluginStage {
|
|
515
598
|
return value === "transform" || value === "check";
|
|
516
599
|
}
|
|
517
600
|
|
|
601
|
+
function hasBuildableGoSource(dir: string): boolean {
|
|
602
|
+
// `go build` consumes `.go` files but silently ignores `_test.go`. A
|
|
603
|
+
// contributor whose source dir holds only test files would compile to
|
|
604
|
+
// an empty package and surface as an opaque scratch-tempdir error;
|
|
605
|
+
// require at least one production `.go` file so the validator can
|
|
606
|
+
// name the contributor entry instead.
|
|
607
|
+
let entries: string[];
|
|
608
|
+
try {
|
|
609
|
+
entries = fs.readdirSync(dir);
|
|
610
|
+
} catch {
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
613
|
+
return entries.some(
|
|
614
|
+
(name) => name.endsWith(".go") && !name.endsWith("_test.go"),
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
|
|
518
618
|
function resolvePluginRequest(specifier: string, projectRoot: string): string {
|
|
519
619
|
if (path.isAbsolute(specifier)) {
|
|
520
620
|
return resolveRealPath(specifier);
|
|
@@ -88,7 +88,7 @@ export interface ITtscCompilerContext {
|
|
|
88
88
|
* directly installed package markers.
|
|
89
89
|
* - `false`: ignore project config plugins and package markers for this
|
|
90
90
|
* compiler instance.
|
|
91
|
-
* -
|
|
91
|
+
* - Array: use these plugin entries instead of project config entries and
|
|
92
92
|
* package markers.
|
|
93
93
|
*
|
|
94
94
|
* Plugin entries are resolved once per operation from this instance context.
|
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
*/
|
|
12
12
|
export interface ITtscCompilerDiagnostic {
|
|
13
13
|
/**
|
|
14
|
-
* The filename where the diagnostic originated, or null if not
|
|
15
|
-
* file-specific.
|
|
14
|
+
* The filename where the diagnostic originated, or null if not file-specific.
|
|
16
15
|
*
|
|
17
16
|
* Global diagnostics such as invalid compiler options or host-level failures
|
|
18
|
-
* are reported with `null` because they are not tied to a single source
|
|
17
|
+
* are reported with `null` because they are not tied to a single source
|
|
18
|
+
* file.
|
|
19
19
|
*/
|
|
20
20
|
file: string | null;
|
|
21
21
|
|
|
@@ -3,8 +3,8 @@ import type { ITtscCompilerDiagnostic } from "./ITtscCompilerDiagnostic";
|
|
|
3
3
|
/**
|
|
4
4
|
* Result of a ttsc TypeScript-Go compilation operation.
|
|
5
5
|
*
|
|
6
|
-
* Represents the possible outcomes of {@link TtscCompiler.compile}, which can
|
|
7
|
-
*
|
|
6
|
+
* Represents the possible outcomes of {@link TtscCompiler.compile}, which can be
|
|
7
|
+
* either a successful compilation, a compilation that completed with
|
|
8
8
|
* diagnostics, or an unexpected host error during the compilation process.
|
|
9
9
|
*
|
|
10
10
|
* This type follows the legacy `embed-typescript` result model, but stores all
|
|
@@ -25,18 +25,14 @@ export namespace ITtscCompilerResult {
|
|
|
25
25
|
* Represents a successful ttsc compilation result.
|
|
26
26
|
*
|
|
27
27
|
* This interface is returned when TypeScript-Go completes without
|
|
28
|
-
* diagnostics, containing every text output captured from the native
|
|
29
|
-
*
|
|
28
|
+
* diagnostics, containing every text output captured from the native compiler
|
|
29
|
+
* host's `WriteFile` callback.
|
|
30
30
|
*/
|
|
31
31
|
export interface ISuccess {
|
|
32
|
-
/**
|
|
33
|
-
* Indicates that the compilation was successful.
|
|
34
|
-
*/
|
|
32
|
+
/** Indicates that the compilation was successful. */
|
|
35
33
|
type: "success";
|
|
36
34
|
|
|
37
|
-
/**
|
|
38
|
-
* Non-fatal diagnostics reported during compilation.
|
|
39
|
-
*/
|
|
35
|
+
/** Non-fatal diagnostics reported during compilation. */
|
|
40
36
|
diagnostics?: ITtscCompilerDiagnostic[];
|
|
41
37
|
|
|
42
38
|
/**
|
|
@@ -59,14 +55,10 @@ export namespace ITtscCompilerResult {
|
|
|
59
55
|
* diagnostics.
|
|
60
56
|
*/
|
|
61
57
|
export interface IFailure {
|
|
62
|
-
/**
|
|
63
|
-
* Indicates that compilation completed with diagnostics.
|
|
64
|
-
*/
|
|
58
|
+
/** Indicates that compilation completed with diagnostics. */
|
|
65
59
|
type: "failure";
|
|
66
60
|
|
|
67
|
-
/**
|
|
68
|
-
* Array of diagnostic messages describing the compilation issues.
|
|
69
|
-
*/
|
|
61
|
+
/** Array of diagnostic messages describing the compilation issues. */
|
|
70
62
|
diagnostics: ITtscCompilerDiagnostic[];
|
|
71
63
|
|
|
72
64
|
/**
|
|
@@ -86,14 +78,10 @@ export namespace ITtscCompilerResult {
|
|
|
86
78
|
* {@link IFailure}; this variant is reserved for host-level exceptions.
|
|
87
79
|
*/
|
|
88
80
|
export interface IException {
|
|
89
|
-
/**
|
|
90
|
-
* Indicates that an unexpected host error occurred.
|
|
91
|
-
*/
|
|
81
|
+
/** Indicates that an unexpected host error occurred. */
|
|
92
82
|
type: "exception";
|
|
93
83
|
|
|
94
|
-
/**
|
|
95
|
-
* The error that was thrown while preparing or running the compiler host.
|
|
96
|
-
*/
|
|
84
|
+
/** The error that was thrown while preparing or running the compiler host. */
|
|
97
85
|
error: unknown;
|
|
98
86
|
}
|
|
99
87
|
}
|
|
@@ -4,8 +4,8 @@ import type { ITtscCompilerDiagnostic } from "./ITtscCompilerDiagnostic";
|
|
|
4
4
|
* Result of a TypeScript source-to-source transformation operation.
|
|
5
5
|
*
|
|
6
6
|
* This mirrors `embed-typescript`'s `IEmbedTypeScriptTransformation` model.
|
|
7
|
-
* Unlike {@link ITtscCompilerResult}, this contract is not an emit contract:
|
|
8
|
-
*
|
|
7
|
+
* Unlike {@link ITtscCompilerResult}, this contract is not an emit contract: the
|
|
8
|
+
* `typescript` map must contain TypeScript source text, not generated
|
|
9
9
|
* JavaScript, declaration files, or source maps.
|
|
10
10
|
*/
|
|
11
11
|
export type ITtscCompilerTransformation =
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ITtscPluginContributor } from "./ITtscPluginContributor";
|
|
1
2
|
import type { TtscPluginStage } from "./TtscPluginStage";
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -28,7 +29,7 @@ export interface ITtscPlugin {
|
|
|
28
29
|
/**
|
|
29
30
|
* Go command package directory, or a `go.mod` file, that ttsc lazily builds.
|
|
30
31
|
*
|
|
31
|
-
*
|
|
32
|
+
* Ttsc accepts source only. It does not accept a prebuilt binary path: the
|
|
32
33
|
* package-local Go compiler builds this source into the ttsc plugin cache on
|
|
33
34
|
* demand.
|
|
34
35
|
*
|
|
@@ -60,19 +61,44 @@ export interface ITtscPlugin {
|
|
|
60
61
|
* Pipeline stage implemented by the sidecar.
|
|
61
62
|
*
|
|
62
63
|
* Omit this field for normal compiler-transform plugins. The only explicit
|
|
63
|
-
* non-transform stage is `"check"`.
|
|
64
|
+
* non-transform stage is `"check"`. Check-stage plugins receive `check`
|
|
65
|
+
* during normal builds and may implement `fix` for `ttsc fix` and `format`
|
|
66
|
+
* for `ttsc format`.
|
|
64
67
|
*
|
|
65
68
|
* @default "transform"
|
|
66
69
|
*/
|
|
67
70
|
stage?: TtscPluginStage;
|
|
68
|
-
}
|
|
69
71
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Additional Go source packages to statically link into this plugin's binary
|
|
74
|
+
* at build time ("plugin-within-plugin" composition).
|
|
75
|
+
*
|
|
76
|
+
* Each contributor's Go source directory is copied into the scratch build
|
|
77
|
+
* tree as a sub-package of this plugin's module and reached by a synthesized
|
|
78
|
+
* blank import. The contributor's `init()` runs before the host binary's
|
|
79
|
+
* `main`, registering whatever state the host expects to find at startup
|
|
80
|
+
* (e.g. lint rules through `github.com/samchon/ttsc/packages/lint/rule`).
|
|
81
|
+
*
|
|
82
|
+
* Differs from `composes`:
|
|
83
|
+
*
|
|
84
|
+
* - `composes` is horizontal — many plugin entries dispatch to one binary by
|
|
85
|
+
* name. Each entry is still a top-level `compilerOptions.plugins[]` citizen
|
|
86
|
+
* with its own lifecycle slot.
|
|
87
|
+
* - `contributors` is vertical — one binary statically links additional Go
|
|
88
|
+
* sources that never appear as top-level plugin entries. The contributing
|
|
89
|
+
* npm packages are discovered through the host plugin's own config file
|
|
90
|
+
* (e.g. `lint.config.ts` for `@ttsc/lint`).
|
|
91
|
+
*
|
|
92
|
+
* Constraints:
|
|
93
|
+
*
|
|
94
|
+
* - Contributors ship Go source as a package (no `go.mod`); the host plugin's
|
|
95
|
+
* module supplies every transitive Go dependency. This is also a
|
|
96
|
+
* supply-chain feature — contributors cannot pull in arbitrary Go modules
|
|
97
|
+
* at build time.
|
|
98
|
+
* - Contributor source paths must be absolute (the host plugin's JS factory
|
|
99
|
+
* typically resolves them through `require.resolve`).
|
|
100
|
+
* - Contributor names are used as the sub-package import suffix and must be
|
|
101
|
+
* unique within a single plugin build.
|
|
102
|
+
*/
|
|
103
|
+
contributors?: ITtscPluginContributor[];
|
|
78
104
|
}
|