ttsc 0.4.4-dev.20260426 → 0.5.0-dev.20260429
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 +35 -3
- package/cmd/ttsc/main.go +2 -2
- package/driver/program.go +120 -15
- package/go.mod +3 -3
- package/go.sum +6 -6
- package/lib/api.d.ts +3 -3
- package/lib/api.js +275 -624
- package/lib/api.js.map +1 -1
- package/lib/cli.js +78 -2
- package/lib/cli.js.map +1 -1
- package/lib/index.d.ts +3 -3
- package/lib/index.js +3 -3
- package/lib/native.d.ts +5 -0
- package/lib/native.js +8 -0
- package/lib/native.js.map +1 -1
- package/lib/plugin.d.ts +8 -48
- package/lib/plugin.js +79 -132
- package/lib/plugin.js.map +1 -1
- package/lib/runner/cli.js +1 -0
- package/lib/runner/cli.js.map +1 -1
- package/lib/source-build.d.ts +30 -0
- package/lib/source-build.js +365 -0
- package/lib/source-build.js.map +1 -0
- package/package.json +9 -9
- package/shim/ast/go.mod +1 -1
- package/shim/ast/lint.go +328 -0
- package/shim/bundled/go.mod +1 -1
- package/shim/checker/go.mod +1 -1
- package/shim/compiler/go.mod +1 -1
- package/shim/core/go.mod +1 -1
- package/shim/core/shim.go +15 -0
- package/shim/diagnosticwriter/go.mod +1 -1
- package/shim/diagnosticwriter/lint.go +137 -0
- package/shim/parser/go.mod +1 -1
- package/shim/scanner/go.mod +1 -1
- package/shim/tsoptions/go.mod +1 -1
- package/shim/tspath/go.mod +1 -1
- package/shim/vfs/cachedvfs/go.mod +1 -1
- package/shim/vfs/go.mod +1 -1
- package/shim/vfs/osvfs/go.mod +1 -1
- package/src/api.ts +413 -771
- package/src/cli.ts +82 -2
- package/src/index.ts +3 -3
- package/src/native.ts +18 -0
- package/src/plugin.ts +107 -213
- package/src/runner/cli.ts +5 -0
- package/src/source-build.ts +383 -0
- package/test/native.test.ts +43 -0
- package/test/source-build.test.ts +219 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import * as crypto from "node:crypto";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
|
|
7
|
+
import type { TtscNativeSource } from "./native";
|
|
8
|
+
|
|
9
|
+
export interface BuildSourcePluginOptions {
|
|
10
|
+
source: TtscNativeSource;
|
|
11
|
+
pluginName: string;
|
|
12
|
+
baseDir: string;
|
|
13
|
+
ttscVersion: string;
|
|
14
|
+
tsgoVersion: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function buildSourcePlugin(opts: BuildSourcePluginOptions): string {
|
|
18
|
+
const dir = path.isAbsolute(opts.source.dir)
|
|
19
|
+
? opts.source.dir
|
|
20
|
+
: path.resolve(opts.baseDir, opts.source.dir);
|
|
21
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`ttsc: plugin "${opts.pluginName}" native.source.dir does not exist: ${dir}`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
const entry = opts.source.entry ?? ".";
|
|
27
|
+
const key = computeCacheKey({
|
|
28
|
+
dir,
|
|
29
|
+
entry,
|
|
30
|
+
ttscVersion: opts.ttscVersion,
|
|
31
|
+
tsgoVersion: opts.tsgoVersion,
|
|
32
|
+
});
|
|
33
|
+
const root = resolvePluginCacheRoot(opts.baseDir);
|
|
34
|
+
const cacheDir = path.join(root, key);
|
|
35
|
+
const binaryName = process.platform === "win32" ? "plugin.exe" : "plugin";
|
|
36
|
+
const binaryPath = path.join(cacheDir, binaryName);
|
|
37
|
+
if (fs.existsSync(binaryPath)) {
|
|
38
|
+
return binaryPath;
|
|
39
|
+
}
|
|
40
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
41
|
+
process.stderr.write(
|
|
42
|
+
`ttsc: building source plugin "${opts.pluginName}" from ${dir} (this runs once per cache key)\n`,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const scratchDir = path.join(
|
|
46
|
+
root,
|
|
47
|
+
`scratch-${key}-${process.pid}-${Date.now()}`,
|
|
48
|
+
);
|
|
49
|
+
try {
|
|
50
|
+
materializeScratchDir(dir, scratchDir);
|
|
51
|
+
writeGoWork(scratchDir, findTtscOverlayDirs());
|
|
52
|
+
runGoBuild(scratchDir, entry, binaryName, opts.pluginName);
|
|
53
|
+
const builtBinary = path.join(scratchDir, binaryName);
|
|
54
|
+
fs.renameSync(builtBinary, binaryPath);
|
|
55
|
+
if (process.platform !== "win32") {
|
|
56
|
+
fs.chmodSync(binaryPath, 0o755);
|
|
57
|
+
}
|
|
58
|
+
return binaryPath;
|
|
59
|
+
} finally {
|
|
60
|
+
fs.rmSync(scratchDir, { recursive: true, force: true });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function materializeScratchDir(source: string, scratch: string): void {
|
|
65
|
+
fs.mkdirSync(scratch, { recursive: true });
|
|
66
|
+
fs.cpSync(source, scratch, {
|
|
67
|
+
recursive: true,
|
|
68
|
+
filter: (src) => {
|
|
69
|
+
const base = path.basename(src);
|
|
70
|
+
if (SKIP_DIRS.has(base)) return false;
|
|
71
|
+
if (base === "go.work" || base === "go.work.sum") return false;
|
|
72
|
+
return true;
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function writeGoWork(scratchDir: string, useDirs: readonly string[]): void {
|
|
78
|
+
const useLines = ["\t."];
|
|
79
|
+
for (const dir of useDirs) {
|
|
80
|
+
useLines.push(`\t${dir.replace(/\\/g, "/")}`);
|
|
81
|
+
}
|
|
82
|
+
const goWork = `go 1.26\n\nuse (\n${useLines.join("\n")}\n)\n`;
|
|
83
|
+
fs.writeFileSync(path.join(scratchDir, "go.work"), goWork, "utf8");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function runGoBuild(
|
|
87
|
+
cwd: string,
|
|
88
|
+
entry: string,
|
|
89
|
+
binaryName: string,
|
|
90
|
+
pluginName: string,
|
|
91
|
+
): void {
|
|
92
|
+
const goBinary = resolveGoBinary();
|
|
93
|
+
ensureExecutableGoToolchain(goBinary);
|
|
94
|
+
const result = spawnSync(
|
|
95
|
+
goBinary,
|
|
96
|
+
["build", "-o", binaryName, entry],
|
|
97
|
+
{
|
|
98
|
+
cwd,
|
|
99
|
+
encoding: "utf8",
|
|
100
|
+
env: goBuildEnv(goBinary),
|
|
101
|
+
maxBuffer: 1024 * 1024 * 64,
|
|
102
|
+
windowsHide: true,
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
if (result.error) {
|
|
106
|
+
if ((result.error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`ttsc: building plugin "${pluginName}" failed because the Go toolchain was not found. ` +
|
|
109
|
+
`Reinstall ttsc with optional dependencies so the bundled Go compiler is present, ` +
|
|
110
|
+
`or set TTSC_GO_BINARY to an absolute path.`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
throw new Error(
|
|
114
|
+
`ttsc: building plugin "${pluginName}" failed to spawn ${goBinary}: ${result.error.message}`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
if (result.status !== 0) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`ttsc: building plugin "${pluginName}" via "go build" failed:\n${result.stderr || result.stdout}`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function goBuildEnv(goBinary: string): NodeJS.ProcessEnv {
|
|
125
|
+
const env = { ...process.env };
|
|
126
|
+
const goRoot = inferGoRoot(goBinary);
|
|
127
|
+
if (goRoot && !env.GOROOT) {
|
|
128
|
+
env.GOROOT = goRoot;
|
|
129
|
+
}
|
|
130
|
+
return env;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function inferGoRoot(goBinary: string): string | null {
|
|
134
|
+
if (!path.isAbsolute(goBinary)) return null;
|
|
135
|
+
const binDir = path.dirname(goBinary);
|
|
136
|
+
if (path.basename(binDir) !== "bin") return null;
|
|
137
|
+
const goRoot = path.dirname(binDir);
|
|
138
|
+
return fs.existsSync(path.join(goRoot, "src", "runtime")) ? goRoot : null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function ensureExecutableGoToolchain(goBinary: string): void {
|
|
142
|
+
if (process.platform === "win32") return;
|
|
143
|
+
if (!path.isAbsolute(goBinary) || !fs.existsSync(goBinary)) return;
|
|
144
|
+
try {
|
|
145
|
+
fs.chmodSync(goBinary, 0o755);
|
|
146
|
+
const goRoot = inferGoRoot(goBinary);
|
|
147
|
+
if (!goRoot) return;
|
|
148
|
+
const gofmt = path.join(path.dirname(goBinary), "gofmt");
|
|
149
|
+
if (fs.existsSync(gofmt)) fs.chmodSync(gofmt, 0o755);
|
|
150
|
+
const toolDir = path.join(goRoot, "pkg", "tool");
|
|
151
|
+
if (!fs.existsSync(toolDir)) return;
|
|
152
|
+
for (const file of walkToolFiles(toolDir)) {
|
|
153
|
+
fs.chmodSync(file, 0o755);
|
|
154
|
+
}
|
|
155
|
+
} catch {
|
|
156
|
+
// Let the subsequent go build spawn fail with the real OS error.
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function walkToolFiles(dir: string): string[] {
|
|
161
|
+
const out: string[] = [];
|
|
162
|
+
let entries: fs.Dirent[];
|
|
163
|
+
try {
|
|
164
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
165
|
+
} catch {
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
for (const entry of entries) {
|
|
169
|
+
const file = path.join(dir, entry.name);
|
|
170
|
+
if (entry.isDirectory()) {
|
|
171
|
+
out.push(...walkToolFiles(file));
|
|
172
|
+
} else if (entry.isFile()) {
|
|
173
|
+
out.push(file);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface ResolveGoBinaryOptions {
|
|
180
|
+
arch?: string;
|
|
181
|
+
env?: NodeJS.ProcessEnv;
|
|
182
|
+
localGoLookup?: () => string | null;
|
|
183
|
+
platform?: NodeJS.Platform;
|
|
184
|
+
resolver?: (request: string) => string;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function goBinaryName(opts: ResolveGoBinaryOptions = {}): string {
|
|
188
|
+
return (opts.platform ?? process.platform) === "win32" ? "go.exe" : "go";
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function bundledGoPackageRequest(
|
|
192
|
+
opts: ResolveGoBinaryOptions = {},
|
|
193
|
+
): string {
|
|
194
|
+
return `@ttsc/${goPlatformKey(opts)}/bin/go/bin/${goBinaryName(opts)}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let cachedOverlayDirs: readonly string[] | null = null;
|
|
198
|
+
|
|
199
|
+
function findTtscOverlayDirs(): readonly string[] {
|
|
200
|
+
if (cachedOverlayDirs !== null) {
|
|
201
|
+
return cachedOverlayDirs;
|
|
202
|
+
}
|
|
203
|
+
const ttscRoot = path.resolve(__dirname, "..");
|
|
204
|
+
const dirs: string[] = [];
|
|
205
|
+
if (fs.existsSync(path.join(ttscRoot, "go.mod"))) {
|
|
206
|
+
dirs.push(ttscRoot);
|
|
207
|
+
}
|
|
208
|
+
const shimRoot = path.join(ttscRoot, "shim");
|
|
209
|
+
if (fs.existsSync(shimRoot)) {
|
|
210
|
+
walkForGoMod(shimRoot, dirs);
|
|
211
|
+
}
|
|
212
|
+
dirs.sort();
|
|
213
|
+
cachedOverlayDirs = dirs;
|
|
214
|
+
return dirs;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function walkForGoMod(dir: string, out: string[]): void {
|
|
218
|
+
let entries: fs.Dirent[];
|
|
219
|
+
try {
|
|
220
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
221
|
+
} catch {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
let hasGoMod = false;
|
|
225
|
+
for (const entry of entries) {
|
|
226
|
+
if (entry.isFile() && entry.name === "go.mod") {
|
|
227
|
+
hasGoMod = true;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (hasGoMod) {
|
|
231
|
+
out.push(dir);
|
|
232
|
+
}
|
|
233
|
+
for (const entry of entries) {
|
|
234
|
+
if (!entry.isDirectory()) continue;
|
|
235
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
236
|
+
walkForGoMod(path.join(dir, entry.name), out);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function resolvePluginCacheRoot(projectRoot: string): string {
|
|
241
|
+
if (process.env.TTSC_CACHE_DIR) {
|
|
242
|
+
return path.resolve(process.env.TTSC_CACHE_DIR, "plugins");
|
|
243
|
+
}
|
|
244
|
+
return resolveDefaultPluginCacheRoot(projectRoot);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function resolveDefaultPluginCacheRoot(projectRoot: string): string {
|
|
248
|
+
const root = path.resolve(projectRoot);
|
|
249
|
+
const nodeModules = path.join(root, "node_modules");
|
|
250
|
+
if (isDirectory(nodeModules)) {
|
|
251
|
+
return path.join(nodeModules, ".ttsc", "plugins");
|
|
252
|
+
}
|
|
253
|
+
return path.join(root, ".ttsc", "plugins");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function pluginCacheCleanupTargets(projectRoot: string): string[] {
|
|
257
|
+
const root = path.resolve(projectRoot);
|
|
258
|
+
const targets = [
|
|
259
|
+
path.join(root, "node_modules", ".ttsc"),
|
|
260
|
+
path.join(root, ".ttsc"),
|
|
261
|
+
];
|
|
262
|
+
if (process.env.TTSC_CACHE_DIR) {
|
|
263
|
+
targets.push(path.resolve(process.env.TTSC_CACHE_DIR, "plugins"));
|
|
264
|
+
}
|
|
265
|
+
return [...new Set(targets)];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function isDirectory(candidate: string): boolean {
|
|
269
|
+
try {
|
|
270
|
+
return fs.statSync(candidate).isDirectory();
|
|
271
|
+
} catch {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function resolveGoBinary(): string {
|
|
277
|
+
return resolveGoCompiler();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function resolveGoCompiler(
|
|
281
|
+
opts: ResolveGoBinaryOptions = {},
|
|
282
|
+
): string {
|
|
283
|
+
const env = opts.env ?? process.env;
|
|
284
|
+
const explicit = env.TTSC_GO_BINARY;
|
|
285
|
+
if (explicit && explicit.length > 0) return explicit;
|
|
286
|
+
|
|
287
|
+
const resolver =
|
|
288
|
+
opts.resolver ??
|
|
289
|
+
((request: string) => createRequire(__filename).resolve(request));
|
|
290
|
+
try {
|
|
291
|
+
return resolver(bundledGoPackageRequest(opts));
|
|
292
|
+
} catch {
|
|
293
|
+
/* fall through */
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (opts.localGoLookup) {
|
|
297
|
+
const local = opts.localGoLookup();
|
|
298
|
+
if (local) return local;
|
|
299
|
+
} else {
|
|
300
|
+
const local = defaultLocalGoBinaryPath(opts);
|
|
301
|
+
if (local) return local;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return "go";
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function defaultLocalGoBinaryPath(opts: ResolveGoBinaryOptions): string | null {
|
|
308
|
+
const candidate = path.resolve(
|
|
309
|
+
__dirname,
|
|
310
|
+
"..",
|
|
311
|
+
"native",
|
|
312
|
+
"go",
|
|
313
|
+
"bin",
|
|
314
|
+
goBinaryName(opts),
|
|
315
|
+
);
|
|
316
|
+
return fs.existsSync(candidate) ? candidate : null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function goPlatformKey(opts: ResolveGoBinaryOptions = {}): string {
|
|
320
|
+
return `${opts.platform ?? process.platform}-${opts.arch ?? process.arch}`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
interface KeyInputs {
|
|
324
|
+
dir: string;
|
|
325
|
+
entry: string;
|
|
326
|
+
ttscVersion: string;
|
|
327
|
+
tsgoVersion: string;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function computeCacheKey(inputs: KeyInputs): string {
|
|
331
|
+
const hash = crypto.createHash("sha256");
|
|
332
|
+
hash.update(`ttsc=${inputs.ttscVersion}\n`);
|
|
333
|
+
hash.update(`tsgo=${inputs.tsgoVersion}\n`);
|
|
334
|
+
hash.update(`platform=${process.platform}/${process.arch}\n`);
|
|
335
|
+
hash.update(`entry=${inputs.entry}\n`);
|
|
336
|
+
for (const file of collectSourceFiles(inputs.dir)) {
|
|
337
|
+
const rel = path.relative(inputs.dir, file).replace(/\\/g, "/");
|
|
338
|
+
hash.update(`f=${rel}\n`);
|
|
339
|
+
hash.update(fs.readFileSync(file));
|
|
340
|
+
}
|
|
341
|
+
return hash.digest("hex").slice(0, 32);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const SKIP_DIRS = new Set([
|
|
345
|
+
"node_modules",
|
|
346
|
+
".git",
|
|
347
|
+
"dist",
|
|
348
|
+
"build",
|
|
349
|
+
"vendor",
|
|
350
|
+
"lib",
|
|
351
|
+
]);
|
|
352
|
+
|
|
353
|
+
function collectSourceFiles(root: string): string[] {
|
|
354
|
+
const out: string[] = [];
|
|
355
|
+
walk(root, root, out);
|
|
356
|
+
out.sort();
|
|
357
|
+
return out;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function walk(root: string, dir: string, out: string[]): void {
|
|
361
|
+
let entries: fs.Dirent[];
|
|
362
|
+
try {
|
|
363
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
364
|
+
} catch {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
for (const entry of entries) {
|
|
368
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
369
|
+
const full = path.join(dir, entry.name);
|
|
370
|
+
if (entry.isDirectory()) {
|
|
371
|
+
walk(root, full, out);
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
if (!entry.isFile()) continue;
|
|
375
|
+
if (!isHashableFile(entry.name)) continue;
|
|
376
|
+
out.push(full);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function isHashableFile(name: string): boolean {
|
|
381
|
+
if (name === "go.mod" || name === "go.sum" || name === "go.work") return true;
|
|
382
|
+
return /\.(?:go|s|c|h|cpp|hpp)$/i.test(name);
|
|
383
|
+
}
|
package/test/native.test.ts
CHANGED
|
@@ -58,3 +58,46 @@ test("resolveNativeBackend rejects unsupported native contract versions", () =>
|
|
|
58
58
|
/unsupported native contract version 2/,
|
|
59
59
|
);
|
|
60
60
|
});
|
|
61
|
+
|
|
62
|
+
test("resolveNativeBackend accepts native.source descriptors", () => {
|
|
63
|
+
const backend = resolveNativeBackend({
|
|
64
|
+
name: "source-test",
|
|
65
|
+
native: {
|
|
66
|
+
mode: "native-test",
|
|
67
|
+
source: { dir: "/abs/path/to/plugin", entry: "./cmd/host" },
|
|
68
|
+
contractVersion: 1,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
assert.deepEqual(backend, {
|
|
73
|
+
mode: "native-test",
|
|
74
|
+
source: { dir: "/abs/path/to/plugin", entry: "./cmd/host" },
|
|
75
|
+
contractVersion: 1,
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("resolveNativeBackend rejects native.source + native.binary together", () => {
|
|
80
|
+
assert.throws(
|
|
81
|
+
() =>
|
|
82
|
+
resolveNativeBackend({
|
|
83
|
+
name: "conflict-source-test",
|
|
84
|
+
native: {
|
|
85
|
+
mode: "native-test",
|
|
86
|
+
binary: "/tmp/binary",
|
|
87
|
+
source: { dir: "/tmp/source" },
|
|
88
|
+
},
|
|
89
|
+
}),
|
|
90
|
+
/must use either native\.binary or native\.source, not both/,
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("resolveNativeBackend rejects empty native.source.dir", () => {
|
|
95
|
+
assert.throws(
|
|
96
|
+
() =>
|
|
97
|
+
resolveNativeBackend({
|
|
98
|
+
name: "empty-source-test",
|
|
99
|
+
native: { mode: "native-test", source: { dir: "" } },
|
|
100
|
+
}),
|
|
101
|
+
/native\.source\.dir must be a non-empty string/,
|
|
102
|
+
);
|
|
103
|
+
});
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
const assert = require("node:assert/strict");
|
|
2
|
+
const fs = require("node:fs");
|
|
3
|
+
const os = require("node:os");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const test = require("node:test");
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
bundledGoPackageRequest,
|
|
9
|
+
computeCacheKey,
|
|
10
|
+
goBinaryName,
|
|
11
|
+
pluginCacheCleanupTargets,
|
|
12
|
+
resolveDefaultPluginCacheRoot,
|
|
13
|
+
resolveGoCompiler,
|
|
14
|
+
resolvePluginCacheRoot,
|
|
15
|
+
} = require("../src/source-build.ts");
|
|
16
|
+
|
|
17
|
+
function withTempSourceDir(files: Record<string, string>): string {
|
|
18
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "ttsc-cache-key-"));
|
|
19
|
+
for (const [name, contents] of Object.entries(files)) {
|
|
20
|
+
const file = path.join(root, name);
|
|
21
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
22
|
+
fs.writeFileSync(file, contents, "utf8");
|
|
23
|
+
}
|
|
24
|
+
return root;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test("computeCacheKey is stable across calls for identical inputs", () => {
|
|
28
|
+
const dir = withTempSourceDir({
|
|
29
|
+
"go.mod": "module example\n\ngo 1.26\n",
|
|
30
|
+
"main.go": "package main\nfunc main() {}\n",
|
|
31
|
+
});
|
|
32
|
+
const inputs = { dir, entry: ".", ttscVersion: "1.0.0", tsgoVersion: "7.0.0-dev" };
|
|
33
|
+
assert.equal(computeCacheKey(inputs), computeCacheKey(inputs));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("computeCacheKey changes when ttscVersion changes", () => {
|
|
37
|
+
const dir = withTempSourceDir({
|
|
38
|
+
"go.mod": "module example\n\ngo 1.26\n",
|
|
39
|
+
"main.go": "package main\nfunc main() {}\n",
|
|
40
|
+
});
|
|
41
|
+
const a = computeCacheKey({ dir, entry: ".", ttscVersion: "1.0.0", tsgoVersion: "7.0.0-dev" });
|
|
42
|
+
const b = computeCacheKey({ dir, entry: ".", ttscVersion: "1.0.1", tsgoVersion: "7.0.0-dev" });
|
|
43
|
+
assert.notEqual(a, b);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("computeCacheKey changes when tsgoVersion changes", () => {
|
|
47
|
+
const dir = withTempSourceDir({
|
|
48
|
+
"go.mod": "module example\n\ngo 1.26\n",
|
|
49
|
+
"main.go": "package main\nfunc main() {}\n",
|
|
50
|
+
});
|
|
51
|
+
const a = computeCacheKey({ dir, entry: ".", ttscVersion: "1.0.0", tsgoVersion: "7.0.0-dev.A" });
|
|
52
|
+
const b = computeCacheKey({ dir, entry: ".", ttscVersion: "1.0.0", tsgoVersion: "7.0.0-dev.B" });
|
|
53
|
+
assert.notEqual(a, b);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("computeCacheKey changes when entry changes", () => {
|
|
57
|
+
const dir = withTempSourceDir({
|
|
58
|
+
"go.mod": "module example\n\ngo 1.26\n",
|
|
59
|
+
"main.go": "package main\nfunc main() {}\n",
|
|
60
|
+
"cmd/foo/main.go": "package main\nfunc main() {}\n",
|
|
61
|
+
});
|
|
62
|
+
const a = computeCacheKey({ dir, entry: ".", ttscVersion: "1.0.0", tsgoVersion: "x" });
|
|
63
|
+
const b = computeCacheKey({ dir, entry: "./cmd/foo", ttscVersion: "1.0.0", tsgoVersion: "x" });
|
|
64
|
+
assert.notEqual(a, b);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("computeCacheKey changes when source content changes", () => {
|
|
68
|
+
const dir = withTempSourceDir({
|
|
69
|
+
"go.mod": "module example\n\ngo 1.26\n",
|
|
70
|
+
"main.go": "package main\nfunc main() {}\n",
|
|
71
|
+
});
|
|
72
|
+
const before = computeCacheKey({ dir, entry: ".", ttscVersion: "1.0.0", tsgoVersion: "x" });
|
|
73
|
+
fs.writeFileSync(
|
|
74
|
+
path.join(dir, "main.go"),
|
|
75
|
+
"package main\nfunc main() { _ = 1 }\n",
|
|
76
|
+
);
|
|
77
|
+
const after = computeCacheKey({ dir, entry: ".", ttscVersion: "1.0.0", tsgoVersion: "x" });
|
|
78
|
+
assert.notEqual(before, after);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("computeCacheKey ignores non-Go files (e.g. README.md)", () => {
|
|
82
|
+
const dir = withTempSourceDir({
|
|
83
|
+
"go.mod": "module example\n\ngo 1.26\n",
|
|
84
|
+
"main.go": "package main\nfunc main() {}\n",
|
|
85
|
+
});
|
|
86
|
+
const before = computeCacheKey({ dir, entry: ".", ttscVersion: "1.0.0", tsgoVersion: "x" });
|
|
87
|
+
fs.writeFileSync(path.join(dir, "README.md"), "documentation\n");
|
|
88
|
+
const after = computeCacheKey({ dir, entry: ".", ttscVersion: "1.0.0", tsgoVersion: "x" });
|
|
89
|
+
assert.equal(before, after);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("resolveGoCompiler prefers TTSC_GO_BINARY override", () => {
|
|
93
|
+
const resolved = resolveGoCompiler({
|
|
94
|
+
env: { TTSC_GO_BINARY: "/tmp/custom-go" },
|
|
95
|
+
localGoLookup: () => "/tmp/local-go",
|
|
96
|
+
resolver: () => {
|
|
97
|
+
throw new Error("resolver should not be called when TTSC_GO_BINARY is set");
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
assert.equal(resolved, "/tmp/custom-go");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("resolveGoCompiler uses bundled platform package before PATH fallback", () => {
|
|
104
|
+
const resolved = resolveGoCompiler({
|
|
105
|
+
arch: "x64",
|
|
106
|
+
env: {},
|
|
107
|
+
localGoLookup: () => null,
|
|
108
|
+
platform: "linux",
|
|
109
|
+
resolver: (request) => {
|
|
110
|
+
assert.equal(request, "@ttsc/linux-x64/bin/go/bin/go");
|
|
111
|
+
return "/node_modules/@ttsc/linux-x64/bin/go/bin/go";
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
assert.equal(resolved, "/node_modules/@ttsc/linux-x64/bin/go/bin/go");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("resolveGoCompiler falls back to package-local bundled Go before PATH", () => {
|
|
118
|
+
const resolved = resolveGoCompiler({
|
|
119
|
+
env: {},
|
|
120
|
+
localGoLookup: () => "/workspace/packages/ttsc/native/go/bin/go",
|
|
121
|
+
resolver: () => {
|
|
122
|
+
throw new Error("platform package missing");
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
assert.equal(resolved, "/workspace/packages/ttsc/native/go/bin/go");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("resolveGoCompiler keeps go on PATH as the last development fallback", () => {
|
|
129
|
+
const resolved = resolveGoCompiler({
|
|
130
|
+
env: {},
|
|
131
|
+
localGoLookup: () => null,
|
|
132
|
+
resolver: () => {
|
|
133
|
+
throw new Error("platform package missing");
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
assert.equal(resolved, "go");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("bundled Go package request follows platform package layout", () => {
|
|
140
|
+
assert.equal(goBinaryName({ platform: "win32" }), "go.exe");
|
|
141
|
+
assert.equal(goBinaryName({ platform: "linux" }), "go");
|
|
142
|
+
assert.equal(
|
|
143
|
+
bundledGoPackageRequest({ platform: "darwin", arch: "arm64" }),
|
|
144
|
+
"@ttsc/darwin-arm64/bin/go/bin/go",
|
|
145
|
+
);
|
|
146
|
+
assert.equal(
|
|
147
|
+
bundledGoPackageRequest({ platform: "win32", arch: "x64" }),
|
|
148
|
+
"@ttsc/win32-x64/bin/go/bin/go.exe",
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("resolvePluginCacheRoot prefers project node_modules cache", () => {
|
|
153
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "ttsc-local-cache-"));
|
|
154
|
+
fs.mkdirSync(path.join(root, "node_modules"));
|
|
155
|
+
assert.equal(
|
|
156
|
+
resolvePluginCacheRoot(root),
|
|
157
|
+
path.join(root, "node_modules", ".ttsc", "plugins"),
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("resolvePluginCacheRoot falls back to project .ttsc without node_modules", () => {
|
|
162
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "ttsc-local-cache-"));
|
|
163
|
+
assert.equal(resolvePluginCacheRoot(root), path.join(root, ".ttsc", "plugins"));
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("resolveDefaultPluginCacheRoot ignores TTSC_CACHE_DIR for local clean paths", () => {
|
|
167
|
+
const previous = process.env.TTSC_CACHE_DIR;
|
|
168
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "ttsc-local-cache-"));
|
|
169
|
+
fs.mkdirSync(path.join(root, "node_modules"));
|
|
170
|
+
try {
|
|
171
|
+
process.env.TTSC_CACHE_DIR = path.join(root, "override");
|
|
172
|
+
assert.equal(
|
|
173
|
+
resolveDefaultPluginCacheRoot(root),
|
|
174
|
+
path.join(root, "node_modules", ".ttsc", "plugins"),
|
|
175
|
+
);
|
|
176
|
+
} finally {
|
|
177
|
+
if (previous === undefined) {
|
|
178
|
+
delete process.env.TTSC_CACHE_DIR;
|
|
179
|
+
} else {
|
|
180
|
+
process.env.TTSC_CACHE_DIR = previous;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("resolvePluginCacheRoot honors TTSC_CACHE_DIR as an explicit test override", () => {
|
|
186
|
+
const previous = process.env.TTSC_CACHE_DIR;
|
|
187
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "ttsc-local-cache-"));
|
|
188
|
+
const override = path.join(root, "override");
|
|
189
|
+
try {
|
|
190
|
+
process.env.TTSC_CACHE_DIR = override;
|
|
191
|
+
assert.equal(resolvePluginCacheRoot(root), path.join(override, "plugins"));
|
|
192
|
+
} finally {
|
|
193
|
+
if (previous === undefined) {
|
|
194
|
+
delete process.env.TTSC_CACHE_DIR;
|
|
195
|
+
} else {
|
|
196
|
+
process.env.TTSC_CACHE_DIR = previous;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("pluginCacheCleanupTargets deletes both local roots and explicit override", () => {
|
|
202
|
+
const previous = process.env.TTSC_CACHE_DIR;
|
|
203
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "ttsc-local-cache-"));
|
|
204
|
+
const override = path.join(root, "override");
|
|
205
|
+
try {
|
|
206
|
+
process.env.TTSC_CACHE_DIR = override;
|
|
207
|
+
assert.deepEqual(pluginCacheCleanupTargets(root), [
|
|
208
|
+
path.join(root, "node_modules", ".ttsc"),
|
|
209
|
+
path.join(root, ".ttsc"),
|
|
210
|
+
path.join(override, "plugins"),
|
|
211
|
+
]);
|
|
212
|
+
} finally {
|
|
213
|
+
if (previous === undefined) {
|
|
214
|
+
delete process.env.TTSC_CACHE_DIR;
|
|
215
|
+
} else {
|
|
216
|
+
process.env.TTSC_CACHE_DIR = previous;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
});
|