ttsc 0.4.2 → 0.4.3-dev.20260426-2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,3 @@
1
- import { createHash } from "node:crypto";
2
1
  import * as fs from "node:fs";
3
2
  import * as path from "node:path";
4
3
 
@@ -6,16 +5,12 @@ import {
6
5
  build,
7
6
  check,
8
7
  type CommonOptions,
9
- transform,
10
8
  } from "../api";
11
9
  import {
12
10
  defaultCacheDirectory,
13
11
  resolveProjectConfig,
14
12
  resolveProjectRoot,
15
13
  } from "../project";
16
- import {
17
- resolveBinary,
18
- } from "../platform";
19
14
 
20
15
  export interface RegisterOptions extends CommonOptions {
21
16
  cacheDir?: string;
@@ -36,8 +31,6 @@ type CompilableModule = NodeJS.Module & {
36
31
 
37
32
  interface ProjectContext {
38
33
  cacheDir: string;
39
- cacheSalt: string;
40
- compiledText: Map<string, string>;
41
34
  emitDir: string;
42
35
  emittedFiles: string[] | null;
43
36
  diagnosticsChecked: boolean;
@@ -54,7 +47,7 @@ const DEFAULT_EXTENSIONS: readonly string[] = Object.freeze([
54
47
  ]);
55
48
 
56
49
  const PROCESS_CACHE_KEY = String(process.pid);
57
- const SINGLE_FILE_CACHE_VERSION = "v4";
50
+ const preparedContexts = new Map<string, ProjectContext>();
58
51
 
59
52
  export function register(options: RegisterOptions = {}): () => void {
60
53
  const cwd = path.resolve(options.cwd ?? process.cwd());
@@ -70,12 +63,15 @@ export function register(options: RegisterOptions = {}): () => void {
70
63
  const tsconfig = resolveProjectConfig({ cwd, tsconfig: key });
71
64
  const root = resolveProjectRoot({ cwd, tsconfig });
72
65
  const cacheDir = options.cacheDir ?? defaultCacheDirectory(root, "ttsx");
66
+ const prepared = takePreparedContext(tsconfig, cacheDir, options);
67
+ if (prepared) {
68
+ contextCache.set(key, prepared);
69
+ return prepared;
70
+ }
73
71
  const created = {
74
72
  tsconfig,
75
73
  root,
76
74
  cacheDir,
77
- cacheSalt: createCompilerCacheSalt(options),
78
- compiledText: new Map<string, string>(),
79
75
  diagnosticsChecked: false,
80
76
  emitDir: path.join(cacheDir, "project", PROCESS_CACHE_KEY),
81
77
  emittedFiles: null,
@@ -90,12 +86,15 @@ export function register(options: RegisterOptions = {}): () => void {
90
86
  if (cached) return cached;
91
87
  const root = resolveProjectRoot({ cwd, tsconfig });
92
88
  const cacheDir = options.cacheDir ?? defaultCacheDirectory(root, "ttsx");
89
+ const prepared = takePreparedContext(tsconfig, cacheDir, options);
90
+ if (prepared) {
91
+ contextCache.set(tsconfig, prepared);
92
+ return prepared;
93
+ }
93
94
  const created = {
94
95
  tsconfig,
95
96
  root,
96
97
  cacheDir,
97
- cacheSalt: createCompilerCacheSalt(options),
98
- compiledText: new Map<string, string>(),
99
98
  diagnosticsChecked: false,
100
99
  emitDir: path.join(cacheDir, "project", PROCESS_CACHE_KEY),
101
100
  emittedFiles: null,
@@ -144,8 +143,9 @@ export function prepareExecution(
144
143
  const cwd = path.resolve(options.cwd ?? process.cwd());
145
144
  const context = resolveProjectContext(cwd, entryFile, options);
146
145
  ensureProjectDiagnostics(context, options);
147
- const entryOutput = transformSingleFile(context, entryFile, options);
146
+ const entryOutput = readCompiledOutput(context, entryFile, options);
148
147
  if (!looksLikeESM(entryOutput)) {
148
+ storePreparedContext(context, options);
149
149
  return {
150
150
  emitDir: context.emitDir,
151
151
  entryFile,
@@ -165,6 +165,44 @@ export function prepareExecution(
165
165
  };
166
166
  }
167
167
 
168
+ function storePreparedContext(
169
+ context: ProjectContext,
170
+ options: RegisterOptions,
171
+ ): void {
172
+ preparedContexts.set(preparedContextKey(context.tsconfig, context.cacheDir, options), context);
173
+ }
174
+
175
+ function takePreparedContext(
176
+ tsconfig: string,
177
+ cacheDir: string,
178
+ options: RegisterOptions,
179
+ ): ProjectContext | null {
180
+ const key = preparedContextKey(tsconfig, cacheDir, options);
181
+ const context = preparedContexts.get(key);
182
+ if (!context) {
183
+ return null;
184
+ }
185
+ preparedContexts.delete(key);
186
+ return context;
187
+ }
188
+
189
+ function preparedContextKey(
190
+ tsconfig: string,
191
+ cacheDir: string,
192
+ options: RegisterOptions,
193
+ ): string {
194
+ return JSON.stringify([
195
+ PROCESS_CACHE_KEY,
196
+ tsconfig,
197
+ cacheDir,
198
+ options.binary ?? "",
199
+ options.rewriteMode ?? "",
200
+ options.env?.TTSC_TSGO_BINARY ?? "",
201
+ options.env?.TTSC_BINARY ?? "",
202
+ options.plugins === false ? false : options.plugins ?? null,
203
+ ]);
204
+ }
205
+
168
206
  function ensureProjectBuild(context: ProjectContext, options: RegisterOptions): void {
169
207
  if (context.emittedFiles !== null) return;
170
208
 
@@ -176,7 +214,10 @@ function ensureProjectBuild(context: ProjectContext, options: RegisterOptions):
176
214
  env: options.env,
177
215
  emit: true,
178
216
  outDir: context.emitDir,
217
+ plugins: options.plugins,
179
218
  quiet: true,
219
+ rewriteMode: options.rewriteMode,
220
+ skipDiagnosticsCheck: context.diagnosticsChecked,
180
221
  tsconfig: context.tsconfig,
181
222
  });
182
223
  if (result.status === 0) {
@@ -186,7 +227,7 @@ function ensureProjectBuild(context: ProjectContext, options: RegisterOptions):
186
227
 
187
228
  fs.rmSync(context.emitDir, { recursive: true, force: true });
188
229
  const detail = [
189
- `ttsx: native project build failed for ${context.tsconfig}`,
230
+ `ttsx: project build failed for ${context.tsconfig}`,
190
231
  result.stderr || result.stdout,
191
232
  ]
192
233
  .filter((line) => line.trim().length !== 0)
@@ -199,7 +240,12 @@ function readCompiledOutput(
199
240
  filename: string,
200
241
  options: RegisterOptions,
201
242
  ): string {
202
- return transformSingleFile(context, filename, options);
243
+ ensureProjectBuild(context, options);
244
+ const emitted = resolveEmittedFile(context, filename);
245
+ if (!emitted) {
246
+ throw new Error(`ttsx: emitted file not found for ${filename}`);
247
+ }
248
+ return fs.readFileSync(emitted, "utf8");
203
249
  }
204
250
 
205
251
  function resolveProjectContext(
@@ -229,8 +275,6 @@ function createProjectContext(
229
275
  tsconfig,
230
276
  root,
231
277
  cacheDir,
232
- cacheSalt: createCompilerCacheSalt(options),
233
- compiledText: new Map<string, string>(),
234
278
  diagnosticsChecked: false,
235
279
  emitDir: path.join(cacheDir, "project", PROCESS_CACHE_KEY),
236
280
  emittedFiles: null,
@@ -266,120 +310,6 @@ function ensureProjectDiagnostics(
266
310
  context.diagnosticsChecked = true;
267
311
  }
268
312
 
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
313
  function resolveEmittedFile(context: ProjectContext, filename: string): string | null {
384
314
  const normalized = path.resolve(filename);
385
315
  const cached = context.entryMap.get(normalized);
package/src/tsgo.ts ADDED
@@ -0,0 +1,108 @@
1
+ import * as fs from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import * as path from "node:path";
4
+
5
+ export interface TsgoResolveOptions {
6
+ /** Absolute path to an explicit tsgo-compatible binary. */
7
+ binary?: string;
8
+ /** Project directory used for node_modules resolution. */
9
+ cwd?: string;
10
+ /** Environment variables used for explicit binary overrides. */
11
+ env?: NodeJS.ProcessEnv;
12
+ /** Override process.platform for tests. */
13
+ platform?: NodeJS.Platform;
14
+ /** Override process.arch for tests. */
15
+ arch?: string;
16
+ /** Override package resolution for tests. */
17
+ resolver?: (request: string) => string;
18
+ }
19
+
20
+ export interface ResolvedTsgo {
21
+ binary: string;
22
+ gitHead?: string;
23
+ packageJson: string;
24
+ packageRoot: string;
25
+ platformPackageJson?: string;
26
+ version: string;
27
+ }
28
+
29
+ export function nativePreviewPlatformPackage(
30
+ opts: TsgoResolveOptions = {},
31
+ ): string {
32
+ return `@typescript/native-preview-${opts.platform ?? process.platform}-${
33
+ opts.arch ?? process.arch
34
+ }`;
35
+ }
36
+
37
+ export function resolveTsgo(opts: TsgoResolveOptions = {}): ResolvedTsgo {
38
+ const env = opts.env ?? process.env;
39
+ const explicit = opts.binary ?? env.TTSC_TSGO_BINARY;
40
+ if (explicit) {
41
+ if (!path.isAbsolute(explicit) || !fs.existsSync(explicit)) {
42
+ throw new Error(
43
+ `ttsc: explicit tsgo binary must be an existing absolute path: ${explicit}`,
44
+ );
45
+ }
46
+ return {
47
+ binary: explicit,
48
+ packageJson: "",
49
+ packageRoot: path.dirname(explicit),
50
+ version: "custom",
51
+ };
52
+ }
53
+
54
+ const cwd = path.resolve(opts.cwd ?? process.cwd());
55
+ const resolver =
56
+ opts.resolver ??
57
+ ((request: string) => createRequire(path.join(cwd, "package.json")).resolve(request));
58
+ let packageJson: string;
59
+ try {
60
+ packageJson = resolver("@typescript/native-preview/package.json");
61
+ } catch {
62
+ throw new Error(
63
+ [
64
+ "ttsc: @typescript/native-preview is required.",
65
+ "Install the TypeScript-Go preview in the consuming project:",
66
+ " npm i -D @typescript/native-preview",
67
+ ].join("\n"),
68
+ );
69
+ }
70
+
71
+ const manifest = readPackageJson(packageJson);
72
+ const packageRoot = path.dirname(packageJson);
73
+ const platformPackage = nativePreviewPlatformPackage(opts);
74
+ const platformResolver = createRequire(packageJson);
75
+ let platformPackageJson: string;
76
+ try {
77
+ platformPackageJson = platformResolver.resolve(`${platformPackage}/package.json`);
78
+ } catch {
79
+ throw new Error(
80
+ [
81
+ `ttsc: platform-specific TypeScript-Go binary not found (${platformPackage}).`,
82
+ "Reinstall @typescript/native-preview with optional dependencies enabled.",
83
+ ].join("\n"),
84
+ );
85
+ }
86
+
87
+ const platformRoot = path.dirname(platformPackageJson);
88
+ const binary = path.join(
89
+ platformRoot,
90
+ "lib",
91
+ (opts.platform ?? process.platform) === "win32" ? "tsgo.exe" : "tsgo",
92
+ );
93
+ if (!fs.existsSync(binary)) {
94
+ throw new Error(`ttsc: TypeScript-Go executable not found: ${binary}`);
95
+ }
96
+ return {
97
+ binary,
98
+ gitHead: typeof manifest.gitHead === "string" ? manifest.gitHead : undefined,
99
+ packageJson,
100
+ packageRoot,
101
+ platformPackageJson,
102
+ version: typeof manifest.version === "string" ? manifest.version : "unknown",
103
+ };
104
+ }
105
+
106
+ function readPackageJson(file: string): Record<string, unknown> {
107
+ return JSON.parse(fs.readFileSync(file, "utf8")) as Record<string, unknown>;
108
+ }
@@ -0,0 +1,68 @@
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
+ const {
7
+ nativePreviewPlatformPackage,
8
+ resolveTsgo,
9
+ } = require("../src/tsgo.ts");
10
+
11
+ test("resolveTsgo accepts TTSC_TSGO_BINARY as an explicit compiler", () => {
12
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "ttsc-tsgo-test-"));
13
+ const binary = path.join(root, "tsgo");
14
+ fs.writeFileSync(binary, "", "utf8");
15
+
16
+ const resolved = resolveTsgo({
17
+ env: { TTSC_TSGO_BINARY: binary },
18
+ });
19
+
20
+ assert.equal(resolved.binary, binary);
21
+ assert.equal(resolved.version, "custom");
22
+ });
23
+
24
+ test("resolveTsgo resolves the consumer native-preview platform package", () => {
25
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "ttsc-tsgo-test-"));
26
+ const nativeRoot = path.join(root, "node_modules", "@typescript", "native-preview");
27
+ const platformRoot = path.join(
28
+ root,
29
+ "node_modules",
30
+ "@typescript",
31
+ "native-preview-linux-x64",
32
+ );
33
+ fs.mkdirSync(nativeRoot, { recursive: true });
34
+ fs.mkdirSync(path.join(platformRoot, "lib"), { recursive: true });
35
+ fs.writeFileSync(
36
+ path.join(nativeRoot, "package.json"),
37
+ JSON.stringify({
38
+ name: "@typescript/native-preview",
39
+ version: "7.0.0-dev.consumer",
40
+ gitHead: "abc123",
41
+ }),
42
+ "utf8",
43
+ );
44
+ fs.writeFileSync(
45
+ path.join(platformRoot, "package.json"),
46
+ JSON.stringify({
47
+ name: "@typescript/native-preview-linux-x64",
48
+ version: "7.0.0-dev.consumer",
49
+ }),
50
+ "utf8",
51
+ );
52
+ fs.writeFileSync(path.join(platformRoot, "lib", "tsgo"), "", "utf8");
53
+
54
+ const resolved = resolveTsgo({
55
+ arch: "x64",
56
+ cwd: root,
57
+ env: {},
58
+ platform: "linux",
59
+ });
60
+
61
+ assert.equal(resolved.version, "7.0.0-dev.consumer");
62
+ assert.equal(resolved.gitHead, "abc123");
63
+ assert.equal(resolved.binary, path.join(platformRoot, "lib", "tsgo"));
64
+ assert.equal(
65
+ nativePreviewPlatformPackage({ platform: "linux", arch: "x64" }),
66
+ "@typescript/native-preview-linux-x64",
67
+ );
68
+ });