ttsc 0.3.1 → 0.4.0-dev.20260425

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.
Files changed (121) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +93 -362
  3. package/cmd/ttsc/build.go +128 -0
  4. package/cmd/ttsc/main.go +222 -0
  5. package/cmd/ttsc/main_test.go +288 -0
  6. package/cmd/ttsc/transform.go +166 -0
  7. package/driver/host.go +31 -0
  8. package/driver/program.go +298 -0
  9. package/driver/rewrite.go +315 -0
  10. package/go.mod +48 -0
  11. package/go.sum +22 -0
  12. package/lib/api.d.ts +91 -0
  13. package/lib/api.js +325 -0
  14. package/lib/api.js.map +1 -0
  15. package/lib/cli.d.ts +1 -0
  16. package/lib/cli.js +481 -0
  17. package/lib/cli.js.map +1 -0
  18. package/lib/index.d.ts +18 -0
  19. package/lib/index.js +35 -0
  20. package/lib/index.js.map +1 -0
  21. package/lib/native.d.ts +15 -0
  22. package/lib/native.js +34 -0
  23. package/lib/native.js.map +1 -0
  24. package/lib/platform.d.ts +66 -0
  25. package/lib/platform.js +170 -0
  26. package/lib/platform.js.map +1 -0
  27. package/lib/plugin.d.ts +47 -0
  28. package/lib/plugin.js +150 -0
  29. package/lib/plugin.js.map +1 -0
  30. package/lib/project.d.ts +32 -0
  31. package/lib/project.js +303 -0
  32. package/lib/project.js.map +1 -0
  33. package/lib/runner/cli.d.ts +1 -0
  34. package/lib/runner/cli.js +284 -0
  35. package/lib/runner/cli.js.map +1 -0
  36. package/lib/runner/register.d.ts +13 -0
  37. package/lib/runner/register.js +424 -0
  38. package/lib/runner/register.js.map +1 -0
  39. package/package.json +72 -63
  40. package/shim/ast/extra-shim.json +4 -0
  41. package/shim/ast/go.mod +5 -0
  42. package/shim/ast/shim.go +117 -0
  43. package/shim/bundled/extra-shim.json +1 -0
  44. package/shim/bundled/go.mod +5 -0
  45. package/shim/bundled/shim.go +19 -0
  46. package/shim/checker/extra-shim.json +31 -0
  47. package/shim/checker/go.mod +5 -0
  48. package/shim/checker/shim.go +146 -0
  49. package/shim/compiler/extra-shim.json +4 -0
  50. package/shim/compiler/go.mod +5 -0
  51. package/shim/compiler/shim.go +50 -0
  52. package/shim/core/extra-shim.json +4 -0
  53. package/shim/core/go.mod +5 -0
  54. package/shim/core/shim.go +11 -0
  55. package/shim/diagnosticwriter/go.mod +5 -0
  56. package/shim/diagnosticwriter/shim.go +29 -0
  57. package/shim/parser/extra-shim.json +4 -0
  58. package/shim/parser/go.mod +5 -0
  59. package/shim/parser/shim.go +55 -0
  60. package/shim/scanner/extra-shim.json +4 -0
  61. package/shim/scanner/go.mod +5 -0
  62. package/shim/scanner/shim.go +96 -0
  63. package/shim/tsoptions/extra-shim.json +4 -0
  64. package/shim/tsoptions/go.mod +5 -0
  65. package/shim/tsoptions/shim.go +123 -0
  66. package/shim/tspath/extra-shim.json +4 -0
  67. package/shim/tspath/go.mod +5 -0
  68. package/shim/tspath/shim.go +165 -0
  69. package/shim/vfs/cachedvfs/extra-shim.json +4 -0
  70. package/shim/vfs/cachedvfs/go.mod +5 -0
  71. package/shim/vfs/cachedvfs/shim.go +12 -0
  72. package/shim/vfs/extra-shim.json +4 -0
  73. package/shim/vfs/go.mod +5 -0
  74. package/shim/vfs/osvfs/extra-shim.json +4 -0
  75. package/shim/vfs/osvfs/go.mod +5 -0
  76. package/shim/vfs/osvfs/shim.go +13 -0
  77. package/shim/vfs/shim.go +19 -0
  78. package/src/api.ts +416 -0
  79. package/src/cli.ts +492 -0
  80. package/src/index.ts +19 -0
  81. package/src/launcher/ttsc-dev.js +42 -0
  82. package/src/launcher/ttsc.js +19 -0
  83. package/src/launcher/ttsx.js +19 -0
  84. package/src/native.ts +60 -0
  85. package/src/platform.ts +163 -0
  86. package/src/plugin.ts +241 -0
  87. package/src/project.ts +330 -0
  88. package/src/runner/cli.ts +286 -0
  89. package/src/runner/register.ts +487 -0
  90. package/test/native.test.ts +60 -0
  91. package/test/package.test.ts +131 -0
  92. package/test/platform.test.ts +58 -0
  93. package/test/project.test.ts +195 -0
  94. package/tools/gen_shims/go.mod +14 -0
  95. package/tools/gen_shims/go.sum +12 -0
  96. package/tools/gen_shims/main.go +446 -0
  97. package/CHANGELOG.md +0 -2
  98. package/bin/tsc +0 -2
  99. package/bin/tsserver +0 -2
  100. package/lib/PluginCreator.d.ts +0 -67
  101. package/lib/PluginCreator.js +0 -190
  102. package/lib/cancelationToken.d.ts +0 -0
  103. package/lib/cancelationToken.js +0 -2
  104. package/lib/loadTypescript.d.ts +0 -7
  105. package/lib/loadTypescript.js +0 -56
  106. package/lib/patchCreateProgram.d.ts +0 -21
  107. package/lib/patchCreateProgram.js +0 -190
  108. package/lib/tsc.d.ts +0 -1
  109. package/lib/tsc.js +0 -24
  110. package/lib/tsserver.d.ts +0 -0
  111. package/lib/tsserver.js +0 -2
  112. package/lib/tsserverlibrary.d.ts +0 -3
  113. package/lib/tsserverlibrary.js +0 -4
  114. package/lib/typescript.d.ts +0 -3
  115. package/lib/typescript.js +0 -4
  116. package/lib/typescriptServices.d.ts +0 -3
  117. package/lib/typescriptServices.js +0 -4
  118. package/lib/typingsInstaller.d.ts +0 -0
  119. package/lib/typingsInstaller.js +0 -2
  120. package/lib/watchGuard.d.ts +0 -0
  121. package/lib/watchGuard.js +0 -2
@@ -0,0 +1,487 @@
1
+ import { createHash } from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+
5
+ import {
6
+ build,
7
+ check,
8
+ type CommonOptions,
9
+ transform,
10
+ } from "../api";
11
+ import {
12
+ defaultCacheDirectory,
13
+ resolveProjectConfig,
14
+ resolveProjectRoot,
15
+ } from "../project";
16
+ import {
17
+ resolveBinary,
18
+ } from "../platform";
19
+
20
+ export interface RegisterOptions extends CommonOptions {
21
+ cacheDir?: string;
22
+ project?: string;
23
+ extensions?: readonly string[];
24
+ }
25
+
26
+ export interface PreparedExecution {
27
+ emitDir: string;
28
+ entryFile: string;
29
+ moduleKind: "cjs" | "esm";
30
+ }
31
+
32
+ type RequireExtension = (module: NodeJS.Module, filename: string) => void;
33
+ type CompilableModule = NodeJS.Module & {
34
+ _compile(code: string, filename: string): void;
35
+ };
36
+
37
+ interface ProjectContext {
38
+ cacheDir: string;
39
+ cacheSalt: string;
40
+ compiledText: Map<string, string>;
41
+ emitDir: string;
42
+ emittedFiles: string[] | null;
43
+ diagnosticsChecked: boolean;
44
+ entryMap: Map<string, string>;
45
+ root: string;
46
+ tsconfig: string;
47
+ }
48
+
49
+ const DEFAULT_EXTENSIONS: readonly string[] = Object.freeze([
50
+ ".ts",
51
+ ".tsx",
52
+ ".cts",
53
+ ".mts",
54
+ ]);
55
+
56
+ const PROCESS_CACHE_KEY = String(process.pid);
57
+ const SINGLE_FILE_CACHE_VERSION = "v4";
58
+
59
+ export function register(options: RegisterOptions = {}): () => void {
60
+ const cwd = path.resolve(options.cwd ?? process.cwd());
61
+ const extensions = [...(options.extensions ?? DEFAULT_EXTENSIONS)];
62
+ const contextCache = new Map<string, ProjectContext>();
63
+ const previous = new Map<string, RequireExtension | undefined>();
64
+
65
+ const getContext = (filename: string): ProjectContext => {
66
+ if (options.project) {
67
+ const key = path.resolve(cwd, options.project);
68
+ const cached = contextCache.get(key);
69
+ if (cached) return cached;
70
+ const tsconfig = resolveProjectConfig({ cwd, tsconfig: key });
71
+ const root = resolveProjectRoot({ cwd, tsconfig });
72
+ const cacheDir = options.cacheDir ?? defaultCacheDirectory(root, "ttsx");
73
+ const created = {
74
+ tsconfig,
75
+ root,
76
+ cacheDir,
77
+ cacheSalt: createCompilerCacheSalt(options),
78
+ compiledText: new Map<string, string>(),
79
+ diagnosticsChecked: false,
80
+ emitDir: path.join(cacheDir, "project", PROCESS_CACHE_KEY),
81
+ emittedFiles: null,
82
+ entryMap: new Map<string, string>(),
83
+ };
84
+ contextCache.set(key, created);
85
+ return created;
86
+ }
87
+
88
+ const tsconfig = resolveProjectConfig({ cwd, file: filename });
89
+ const cached = contextCache.get(tsconfig);
90
+ if (cached) return cached;
91
+ const root = resolveProjectRoot({ cwd, tsconfig });
92
+ const cacheDir = options.cacheDir ?? defaultCacheDirectory(root, "ttsx");
93
+ const created = {
94
+ tsconfig,
95
+ root,
96
+ cacheDir,
97
+ cacheSalt: createCompilerCacheSalt(options),
98
+ compiledText: new Map<string, string>(),
99
+ diagnosticsChecked: false,
100
+ emitDir: path.join(cacheDir, "project", PROCESS_CACHE_KEY),
101
+ emittedFiles: null,
102
+ entryMap: new Map<string, string>(),
103
+ };
104
+ contextCache.set(tsconfig, created);
105
+ return created;
106
+ };
107
+
108
+ const compile = (filename: string): string => {
109
+ const context = getContext(filename);
110
+ ensureProjectDiagnostics(context, options);
111
+ const output = readCompiledOutput(context, filename, options);
112
+ if (looksLikeESM(output)) {
113
+ throw new Error(
114
+ `ttsx: ESM output is not yet supported by the in-process CJS runner (${filename}).`,
115
+ );
116
+ }
117
+ return output;
118
+ };
119
+
120
+ for (const extension of extensions) {
121
+ previous.set(extension, require.extensions[extension]);
122
+ require.extensions[extension] = (module: NodeJS.Module, filename: string) => {
123
+ const compiled = compile(filename);
124
+ (module as CompilableModule)._compile(compiled, filename);
125
+ };
126
+ }
127
+
128
+ return () => {
129
+ for (const extension of extensions) {
130
+ const handler = previous.get(extension);
131
+ if (handler) {
132
+ require.extensions[extension] = handler;
133
+ } else {
134
+ delete require.extensions[extension];
135
+ }
136
+ }
137
+ };
138
+ }
139
+
140
+ export function prepareExecution(
141
+ entryFile: string,
142
+ options: RegisterOptions = {},
143
+ ): PreparedExecution {
144
+ const cwd = path.resolve(options.cwd ?? process.cwd());
145
+ const context = resolveProjectContext(cwd, entryFile, options);
146
+ ensureProjectDiagnostics(context, options);
147
+ const entryOutput = transformSingleFile(context, entryFile, options);
148
+ if (!looksLikeESM(entryOutput)) {
149
+ return {
150
+ emitDir: context.emitDir,
151
+ entryFile,
152
+ moduleKind: "cjs",
153
+ };
154
+ }
155
+ ensureProjectBuild(context, options);
156
+ const resolvedEntry = resolveEmittedFile(context, entryFile);
157
+ if (resolvedEntry === null) {
158
+ throw new Error(`ttsx: emitted entry not found for ${entryFile}`);
159
+ }
160
+ const output = fs.readFileSync(resolvedEntry, "utf8");
161
+ return {
162
+ emitDir: context.emitDir,
163
+ entryFile: resolvedEntry,
164
+ moduleKind: looksLikeESM(output) ? "esm" : "cjs",
165
+ };
166
+ }
167
+
168
+ function ensureProjectBuild(context: ProjectContext, options: RegisterOptions): void {
169
+ if (context.emittedFiles !== null) return;
170
+
171
+ fs.mkdirSync(context.cacheDir, { recursive: true });
172
+ fs.rmSync(context.emitDir, { recursive: true, force: true });
173
+ const result = build({
174
+ binary: options.binary,
175
+ cwd: context.root,
176
+ env: options.env,
177
+ emit: true,
178
+ outDir: context.emitDir,
179
+ quiet: true,
180
+ tsconfig: context.tsconfig,
181
+ });
182
+ if (result.status === 0) {
183
+ context.emittedFiles = listEmittedFiles(context.emitDir);
184
+ return;
185
+ }
186
+
187
+ fs.rmSync(context.emitDir, { recursive: true, force: true });
188
+ const detail = [
189
+ `ttsx: native project build failed for ${context.tsconfig}`,
190
+ result.stderr || result.stdout,
191
+ ]
192
+ .filter((line) => line.trim().length !== 0)
193
+ .join("\n");
194
+ throw new Error(detail);
195
+ }
196
+
197
+ function readCompiledOutput(
198
+ context: ProjectContext,
199
+ filename: string,
200
+ options: RegisterOptions,
201
+ ): string {
202
+ return transformSingleFile(context, filename, options);
203
+ }
204
+
205
+ function resolveProjectContext(
206
+ cwd: string,
207
+ filename: string,
208
+ options: RegisterOptions,
209
+ ): ProjectContext {
210
+ if (options.project) {
211
+ const tsconfig = resolveProjectConfig({
212
+ cwd,
213
+ tsconfig: path.resolve(cwd, options.project),
214
+ });
215
+ return createProjectContext(cwd, tsconfig, options);
216
+ }
217
+ const tsconfig = resolveProjectConfig({ cwd, file: filename });
218
+ return createProjectContext(cwd, tsconfig, options);
219
+ }
220
+
221
+ function createProjectContext(
222
+ cwd: string,
223
+ tsconfig: string,
224
+ options: RegisterOptions,
225
+ ): ProjectContext {
226
+ const root = resolveProjectRoot({ cwd, tsconfig });
227
+ const cacheDir = options.cacheDir ?? defaultCacheDirectory(root, "ttsx");
228
+ return {
229
+ tsconfig,
230
+ root,
231
+ cacheDir,
232
+ cacheSalt: createCompilerCacheSalt(options),
233
+ compiledText: new Map<string, string>(),
234
+ diagnosticsChecked: false,
235
+ emitDir: path.join(cacheDir, "project", PROCESS_CACHE_KEY),
236
+ emittedFiles: null,
237
+ entryMap: new Map<string, string>(),
238
+ };
239
+ }
240
+
241
+ function ensureProjectDiagnostics(
242
+ context: ProjectContext,
243
+ options: RegisterOptions,
244
+ ): void {
245
+ if (context.diagnosticsChecked) {
246
+ return;
247
+ }
248
+ const result = check({
249
+ binary: options.binary,
250
+ cwd: context.root,
251
+ env: options.env,
252
+ plugins: options.plugins,
253
+ quiet: true,
254
+ rewriteMode: options.rewriteMode,
255
+ tsconfig: context.tsconfig,
256
+ });
257
+ if (result.status !== 0) {
258
+ const detail = [result.stderr.trim(), result.stdout.trim()]
259
+ .filter(Boolean)
260
+ .join("\n");
261
+ throw new Error(
262
+ `ttsx: project check failed for ${context.tsconfig}` +
263
+ (detail ? `\n${detail}` : ""),
264
+ );
265
+ }
266
+ context.diagnosticsChecked = true;
267
+ }
268
+
269
+ function transformSingleFile(
270
+ context: ProjectContext,
271
+ filename: string,
272
+ options: RegisterOptions,
273
+ ): string {
274
+ const normalized = path.resolve(filename);
275
+ const cached = context.compiledText.get(normalized);
276
+ if (cached !== undefined) return cached;
277
+
278
+ const cacheFile = singleFileCachePath(context, normalized);
279
+ const sourceStat = safeStat(normalized);
280
+ const cacheStat = safeStat(cacheFile);
281
+ if (
282
+ sourceStat &&
283
+ cacheStat &&
284
+ cacheStat.isFile() &&
285
+ cacheStat.mtimeMs >= sourceStat.mtimeMs
286
+ ) {
287
+ const output = fs.readFileSync(cacheFile, "utf8");
288
+ context.compiledText.set(normalized, output);
289
+ return output;
290
+ }
291
+
292
+ const output = transform({
293
+ binary: options.binary,
294
+ cwd: context.root,
295
+ env: options.env,
296
+ file: normalized,
297
+ rewriteMode: options.rewriteMode,
298
+ tsconfig: context.tsconfig,
299
+ });
300
+ fs.mkdirSync(path.dirname(cacheFile), { recursive: true });
301
+ fs.writeFileSync(cacheFile, output, "utf8");
302
+ context.compiledText.set(normalized, output);
303
+ return output;
304
+ }
305
+
306
+ function singleFileCachePath(context: ProjectContext, filename: string): string {
307
+ const digest = createHash("sha1")
308
+ .update(context.cacheSalt)
309
+ .update("\0")
310
+ .update(filename)
311
+ .digest("hex");
312
+ return path.join(
313
+ context.cacheDir,
314
+ "single",
315
+ SINGLE_FILE_CACHE_VERSION,
316
+ `${digest}.js`,
317
+ );
318
+ }
319
+
320
+ function createCompilerCacheSalt(options: RegisterOptions): string {
321
+ const parts = [SINGLE_FILE_CACHE_VERSION];
322
+ const binary = resolveBinary(options);
323
+ if (binary) {
324
+ parts.push(fileSignature(binary));
325
+ for (const location of workspaceCompilerLocations(binary)) {
326
+ const signature = treeSignature(location);
327
+ if (signature) {
328
+ parts.push(signature);
329
+ }
330
+ }
331
+ }
332
+ return parts.join("|");
333
+ }
334
+
335
+ function workspaceCompilerLocations(binary: string): string[] {
336
+ const resolved = path.resolve(binary);
337
+ const packageRoot = path.resolve(path.dirname(resolved), "..");
338
+ const candidates = [
339
+ path.resolve(packageRoot, "cmd"),
340
+ path.resolve(packageRoot, "src"),
341
+ ];
342
+ return candidates.filter((location) => fs.existsSync(location));
343
+ }
344
+
345
+ function fileSignature(file: string): string {
346
+ const stat = safeStat(file);
347
+ if (!stat) return `missing:${path.resolve(file)}`;
348
+ return `file:${path.resolve(file)}:${stat.size}:${stat.mtimeMs}`;
349
+ }
350
+
351
+ function treeSignature(root: string): string | null {
352
+ if (!fs.existsSync(root)) return null;
353
+ let latest = 0;
354
+ let count = 0;
355
+ const stack = [root];
356
+ while (stack.length !== 0) {
357
+ const current = stack.pop()!;
358
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
359
+ const next = path.join(current, entry.name);
360
+ if (entry.isDirectory()) {
361
+ stack.push(next);
362
+ continue;
363
+ }
364
+ if (!entry.isFile()) continue;
365
+ if (!/\.(?:go|[cm]?js|ts)$/.test(entry.name)) continue;
366
+ const stat = safeStat(next);
367
+ if (!stat) continue;
368
+ latest = Math.max(latest, stat.mtimeMs);
369
+ count += 1;
370
+ }
371
+ }
372
+ return `tree:${path.resolve(root)}:${count}:${latest}`;
373
+ }
374
+
375
+ function safeStat(file: string): fs.Stats | null {
376
+ try {
377
+ return fs.statSync(file);
378
+ } catch {
379
+ return null;
380
+ }
381
+ }
382
+
383
+ function resolveEmittedFile(context: ProjectContext, filename: string): string | null {
384
+ const normalized = path.resolve(filename);
385
+ const cached = context.entryMap.get(normalized);
386
+ if (cached && fs.existsSync(cached)) return cached;
387
+
388
+ const exact = resolveExactEmittedFile(context, normalized);
389
+ if (exact && fs.existsSync(exact)) {
390
+ context.entryMap.set(normalized, exact);
391
+ return exact;
392
+ }
393
+
394
+ const files = context.emittedFiles ?? [];
395
+ let best: string | null = null;
396
+ let bestScore = 0;
397
+ for (const file of files) {
398
+ const score = sharedSourceStemSegments(file, normalized);
399
+ if (score > bestScore) {
400
+ best = file;
401
+ bestScore = score;
402
+ }
403
+ }
404
+ if (!best || !fs.existsSync(best)) {
405
+ return null;
406
+ }
407
+ context.entryMap.set(normalized, best);
408
+ return best;
409
+ }
410
+
411
+ function resolveExactEmittedFile(
412
+ context: ProjectContext,
413
+ filename: string,
414
+ ): string | null {
415
+ const relative = path.relative(context.root, filename);
416
+ if (relative === "" || relative.startsWith("..") || path.isAbsolute(relative)) {
417
+ return null;
418
+ }
419
+ return path.resolve(
420
+ context.emitDir,
421
+ relative.slice(0, relative.length - path.extname(relative).length) +
422
+ emittedJavaScriptExtension(filename),
423
+ );
424
+ }
425
+
426
+ function listEmittedFiles(root: string): string[] {
427
+ if (!fs.existsSync(root)) return [];
428
+ const output: string[] = [];
429
+ const stack = [root];
430
+ while (stack.length !== 0) {
431
+ const current = stack.pop()!;
432
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
433
+ const next = path.join(current, entry.name);
434
+ if (entry.isDirectory()) {
435
+ stack.push(next);
436
+ } else if (entry.isFile() && isJavaScriptOutput(next)) {
437
+ output.push(path.resolve(next));
438
+ }
439
+ }
440
+ }
441
+ return output;
442
+ }
443
+
444
+ function sharedSourceStemSegments(outPath: string, srcPath: string): number {
445
+ const trim = (location: string): string[] => {
446
+ const normalized = location.split(path.sep).join("/");
447
+ return normalized
448
+ .slice(0, normalized.length - path.extname(normalized).length)
449
+ .split("/");
450
+ };
451
+ const a = trim(outPath);
452
+ const b = trim(srcPath);
453
+ const count = Math.min(a.length, b.length);
454
+ let shared = 0;
455
+ for (let i = 1; i <= count; i += 1) {
456
+ if (a[a.length - i] !== b[b.length - i]) break;
457
+ shared += 1;
458
+ }
459
+ return shared;
460
+ }
461
+
462
+ function looksLikeESM(output: string): boolean {
463
+ if (
464
+ /\bObject\.defineProperty\(exports\b/.test(output) ||
465
+ /\bmodule\.exports\b/.test(output) ||
466
+ /\brequire\(/.test(output) ||
467
+ /\bexports\./.test(output)
468
+ ) {
469
+ return false;
470
+ }
471
+ return /^\s*(import|export)\s/m.test(output);
472
+ }
473
+
474
+ function emittedJavaScriptExtension(filename: string): string {
475
+ switch (path.extname(filename).toLowerCase()) {
476
+ case ".mts":
477
+ return ".mjs";
478
+ case ".cts":
479
+ return ".cjs";
480
+ default:
481
+ return ".js";
482
+ }
483
+ }
484
+
485
+ function isJavaScriptOutput(filename: string): boolean {
486
+ return /\.(?:[cm]?js)$/i.test(filename);
487
+ }
@@ -0,0 +1,60 @@
1
+ const assert = require("node:assert/strict");
2
+ const test = require("node:test");
3
+
4
+ const { resolveNativeBackend } = require("../src/native.ts");
5
+
6
+ test("resolveNativeBackend accepts versioned native backend descriptors", () => {
7
+ const backend = resolveNativeBackend({
8
+ name: "native-test",
9
+ native: {
10
+ mode: "native-test",
11
+ binary: "/tmp/ttsc-native-test",
12
+ contractVersion: 1,
13
+ capabilities: ["rewrite", "diagnostics"],
14
+ },
15
+ });
16
+
17
+ assert.deepEqual(backend, {
18
+ mode: "native-test",
19
+ binary: "/tmp/ttsc-native-test",
20
+ contractVersion: 1,
21
+ capabilities: ["rewrite", "diagnostics"],
22
+ });
23
+ });
24
+
25
+ test("resolveNativeBackend keeps nativeMode/nativeBinary as a legacy alias", () => {
26
+ const backend = resolveNativeBackend({
27
+ name: "legacy-native-test",
28
+ nativeMode: "legacy-native-test",
29
+ nativeBinary: "/tmp/ttsc-legacy-native-test",
30
+ });
31
+
32
+ assert.deepEqual(backend, {
33
+ mode: "legacy-native-test",
34
+ binary: "/tmp/ttsc-legacy-native-test",
35
+ contractVersion: 1,
36
+ });
37
+ });
38
+
39
+ test("resolveNativeBackend rejects ambiguous native declarations", () => {
40
+ assert.throws(
41
+ () =>
42
+ resolveNativeBackend({
43
+ name: "bad-native-test",
44
+ native: { mode: "native-test" },
45
+ nativeMode: "native-test",
46
+ }),
47
+ /must use either native or nativeMode\/nativeBinary/,
48
+ );
49
+ });
50
+
51
+ test("resolveNativeBackend rejects unsupported native contract versions", () => {
52
+ assert.throws(
53
+ () =>
54
+ resolveNativeBackend({
55
+ name: "future-native-test",
56
+ native: { mode: "native-test", contractVersion: 2 },
57
+ }),
58
+ /unsupported native contract version 2/,
59
+ );
60
+ });
@@ -0,0 +1,131 @@
1
+ const assert = require("node:assert/strict");
2
+ const fs = require("node:fs");
3
+ const path = require("node:path");
4
+ const test = require("node:test");
5
+
6
+ const workspaceRoot = path.resolve(__dirname, "../../..");
7
+
8
+ test("package metadata separates monorepo source exports from publish exports", () => {
9
+ const packageJson = readPackageJson("ttsc");
10
+ assert.match(packageJson.main, /^src\/.+\.ts$/);
11
+ assert.match(packageJson.types, /^src\/.+\.ts$/);
12
+ assert.equal(packageJson.exports["."].types, "./src/index.ts");
13
+ assert.equal(packageJson.exports["."].import, "./src/index.ts");
14
+ assert.equal(packageJson.exports["."].require, "./lib/index.js");
15
+ assert.equal(packageJson.exports["."].default, "./src/index.ts");
16
+ assert.equal(packageJson.publishConfig.main, "lib/index.js");
17
+ assert.equal(packageJson.publishConfig.types, "lib/index.d.ts");
18
+ assert.equal(
19
+ packageJson.publishConfig.exports["."].types,
20
+ "./lib/index.d.ts",
21
+ );
22
+ assert.equal(
23
+ packageJson.publishConfig.exports["."].default,
24
+ "./lib/index.js",
25
+ );
26
+ });
27
+
28
+ test("ttsc package owns both compiler and runtime commands", () => {
29
+ const packageJson = readPackageJson("ttsc");
30
+ assert.deepEqual(packageJson.bin, {
31
+ ttsc: "src/launcher/ttsc.js",
32
+ ttsx: "src/launcher/ttsx.js",
33
+ });
34
+ assert.deepEqual(packageJson.publishConfig.bin, packageJson.bin);
35
+ });
36
+
37
+ test("ttsc exposes plugin helpers through the root package only", () => {
38
+ const packageJson = readPackageJson("ttsc");
39
+ assert.equal(packageJson.exports["./plugin"], undefined);
40
+ assert.equal(packageJson.publishConfig.exports["./plugin"], undefined);
41
+ assert.equal(
42
+ fs.existsSync(path.join(workspaceRoot, "packages", "ttsc", "src", "plugin.ts")),
43
+ true,
44
+ );
45
+ assert.equal(
46
+ fs.existsSync(path.join(workspaceRoot, "packages", "ttsc", "src", "index.ts")),
47
+ true,
48
+ );
49
+ });
50
+
51
+ test("ttsc declares platform packages as optional dependencies", () => {
52
+ const packageJson = readPackageJson("ttsc");
53
+ const expected = [
54
+ "@ttsc/linux-x64",
55
+ "@ttsc/linux-arm64",
56
+ "@ttsc/darwin-x64",
57
+ "@ttsc/darwin-arm64",
58
+ "@ttsc/win32-x64",
59
+ "@ttsc/win32-arm64",
60
+ ];
61
+
62
+ assert.deepEqual(
63
+ Object.keys(packageJson.optionalDependencies).sort(),
64
+ expected.toSorted(),
65
+ );
66
+ for (const name of expected) {
67
+ assert.equal(packageJson.optionalDependencies[name], "workspace:*");
68
+ }
69
+ assert.equal(packageJson.files.includes("native"), false);
70
+ });
71
+
72
+ test("published package file lists keep TypeScript and Go sources", () => {
73
+ const ttsc = readPackageJson("ttsc");
74
+ for (const entry of [
75
+ "cmd",
76
+ "driver",
77
+ "shim",
78
+ "src",
79
+ "test",
80
+ "tools",
81
+ "go.mod",
82
+ "go.sum",
83
+ ]) {
84
+ assert.equal(
85
+ ttsc.files.includes(entry),
86
+ true,
87
+ `ttsc files must include ${entry}`,
88
+ );
89
+ }
90
+ for (const entry of ["native", "node_modules", "THIRD-PARTY-LICENSES.md"]) {
91
+ assert.equal(
92
+ ttsc.files.includes(entry),
93
+ false,
94
+ `ttsc files must not include ${entry}`,
95
+ );
96
+ }
97
+ assert.equal(ttsc.files.includes("tsconfig.json"), false);
98
+
99
+ assert.equal(fs.existsSync(path.join(workspaceRoot, "packages", "ttsx")), false);
100
+ });
101
+
102
+ test("platform packages expose os cpu constrained native binaries", () => {
103
+ const matrix = {
104
+ "ttsc-linux-x64": ["@ttsc/linux-x64", "linux", "x64", "Linux x64"],
105
+ "ttsc-linux-arm64": ["@ttsc/linux-arm64", "linux", "arm64", "Linux arm64"],
106
+ "ttsc-darwin-x64": ["@ttsc/darwin-x64", "darwin", "x64", "macOS x64"],
107
+ "ttsc-darwin-arm64": ["@ttsc/darwin-arm64", "darwin", "arm64", "macOS arm64"],
108
+ "ttsc-win32-x64": ["@ttsc/win32-x64", "win32", "x64", "Windows x64"],
109
+ "ttsc-win32-arm64": ["@ttsc/win32-arm64", "win32", "arm64", "Windows arm64"],
110
+ };
111
+
112
+ for (const [dir, [name, os, cpu, label]] of Object.entries(matrix)) {
113
+ const packageJson = readPackageJson(dir);
114
+ assert.equal(packageJson.name, name);
115
+ assert.deepEqual(packageJson.os, [os]);
116
+ assert.deepEqual(packageJson.cpu, [cpu]);
117
+ assert.deepEqual(packageJson.files, ["bin", "package.json", "README.md"]);
118
+ assert.equal(packageJson.scripts.build, "node ../../scripts/build-platform-package.cjs");
119
+ assert.equal(packageJson.scripts.prepack, "pnpm run build");
120
+ assert.equal(packageJson.description.includes(label), true);
121
+ }
122
+ });
123
+
124
+ function readPackageJson(packageName) {
125
+ return JSON.parse(
126
+ fs.readFileSync(
127
+ path.join(workspaceRoot, "packages", packageName, "package.json"),
128
+ "utf8",
129
+ ),
130
+ );
131
+ }