ttsc 0.14.1 → 0.15.0

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 (45) hide show
  1. package/driver/emit_plugin.go +131 -0
  2. package/driver/plugins.go +37 -0
  3. package/go.mod +0 -3
  4. package/go.sum +0 -4
  5. package/lib/compiler/internal/resolveEmittedJavaScript.js +25 -4
  6. package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
  7. package/lib/launcher/internal/prepareExecution.d.ts +4 -1
  8. package/lib/launcher/internal/prepareExecution.js +13 -17
  9. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  10. package/lib/launcher/internal/registerRuntimeHooks.d.ts +1 -0
  11. package/lib/launcher/internal/registerRuntimeHooks.js +54 -0
  12. package/lib/launcher/internal/registerRuntimeHooks.js.map +1 -0
  13. package/lib/launcher/internal/runTtsx.js +49 -370
  14. package/lib/launcher/internal/runTtsx.js.map +1 -1
  15. package/lib/launcher/internal/runtimeHookPreload.d.ts +1 -0
  16. package/lib/launcher/internal/runtimeHookPreload.js +16 -0
  17. package/lib/launcher/internal/runtimeHookPreload.js.map +1 -0
  18. package/lib/launcher/internal/runtimeHooks.d.ts +21 -0
  19. package/lib/launcher/internal/runtimeHooks.js +810 -0
  20. package/lib/launcher/internal/runtimeHooks.js.map +1 -0
  21. package/lib/plugin/internal/buildSourcePlugin.js +120 -16
  22. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  23. package/lib/plugin/internal/loadProjectPlugins.js +15 -4
  24. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  25. package/package.json +9 -9
  26. package/shim/ast/parent.go +31 -0
  27. package/shim/compiler/emitassemble.go +30 -0
  28. package/shim/compiler/emithook.go +22 -0
  29. package/shim/compiler/go.mod +9 -0
  30. package/shim/compiler/go.sum +22 -0
  31. package/shim/core/modulekind.go +10 -0
  32. package/shim/printer/emithost.go +24 -0
  33. package/shim/printer/go.mod +9 -0
  34. package/shim/printer/go.sum +22 -0
  35. package/shim/transformers/go.mod +14 -0
  36. package/shim/transformers/go.sum +20 -0
  37. package/shim/transformers/shim.go +15 -0
  38. package/src/compiler/internal/resolveEmittedJavaScript.ts +35 -4
  39. package/src/launcher/internal/prepareExecution.ts +18 -21
  40. package/src/launcher/internal/registerRuntimeHooks.ts +59 -0
  41. package/src/launcher/internal/runTtsx.ts +65 -437
  42. package/src/launcher/internal/runtimeHookPreload.ts +14 -0
  43. package/src/launcher/internal/runtimeHooks.ts +976 -0
  44. package/src/plugin/internal/buildSourcePlugin.ts +147 -16
  45. package/src/plugin/internal/loadProjectPlugins.ts +16 -4
@@ -0,0 +1,810 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.installRuntimeHooks = installRuntimeHooks;
7
+ exports.entryModuleFormat = entryModuleFormat;
8
+ const node_crypto_1 = __importDefault(require("node:crypto"));
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ const node_module_1 = require("node:module");
11
+ const node_os_1 = __importDefault(require("node:os"));
12
+ const node_path_1 = __importDefault(require("node:path"));
13
+ const node_url_1 = require("node:url");
14
+ const readProjectConfig_1 = require("../../compiler/internal/project/readProjectConfig");
15
+ const resolveEmittedJavaScript_1 = require("../../compiler/internal/resolveEmittedJavaScript");
16
+ const resolveTsgo_1 = require("../../compiler/internal/resolveTsgo");
17
+ const runBuild_1 = require("../../compiler/internal/runBuild");
18
+ const spawnNative_1 = require("../../compiler/internal/spawnNative");
19
+ /**
20
+ * Synchronous Node module hooks installed (via `module.registerHooks`) in the
21
+ * child process `ttsx` spawns to run a TypeScript entry _from source_.
22
+ *
23
+ * They give the runner ts-node-style whole-graph reach without weakening the
24
+ * compile gate. The owning entry project is type-checked and built up front (by
25
+ * `prepareExecution`, with its transform plugins such as typia); these hooks
26
+ * serve that build under the source URLs so `__dirname`/`import.meta.url` keep
27
+ * pointing at the source tree. Three load paths:
28
+ *
29
+ * 1. A `.ts` belonging to the entry project → serve the pre-built emitted JS
30
+ * (transform plugins already applied), mapped by the project's `rootDir`.
31
+ * 2. Any other raw `.ts` dependency (a published or workspace package that ships
32
+ * source) → build its own owning `tsconfig.json` once via `runBuild` and
33
+ * serve the emit. A real build (not a type-strip) is required because Node's
34
+ * type-stripping cannot do cross-file type-only elision — e.g. a
35
+ * value-shaped import of a type+namespace merge survives stripping and
36
+ * dangles at runtime.
37
+ * 3. No owning tsconfig → transform the lone file by the format it resolves to: a
38
+ * CommonJS-classified file (`.cts`, or a `.ts` in a package without `type:
39
+ * "module"`) is lowered to CommonJS through a tsgo single-file emit so its
40
+ * `export` syntax becomes `module.exports`; any other (ESM) file keeps the
41
+ * fast in-process `mode: "transform"` type-strip.
42
+ *
43
+ * The hooks are synchronous and run on the main thread (not a loader worker):
44
+ * that is what lets a CommonJS `require("./x")` chain reach them and what makes
45
+ * `require.resolve(..., { paths })` inside `runBuild`'s plugin loader behave.
46
+ */
47
+ /** Source/JS extensions probed when an extensionless relative import fails. */
48
+ const RESOLVABLE_EXTENSIONS = [
49
+ ".ts",
50
+ ".tsx",
51
+ ".mts",
52
+ ".cts",
53
+ ".js",
54
+ ".mjs",
55
+ ".cjs",
56
+ ];
57
+ /** TypeScript source extensions these hooks compile. */
58
+ const TYPESCRIPT_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts"];
59
+ let manifestCache;
60
+ function manifest() {
61
+ if (manifestCache !== undefined) {
62
+ return manifestCache;
63
+ }
64
+ const file = process.env.TTSX_RUNTIME_MANIFEST;
65
+ if (file === undefined || file.length === 0) {
66
+ manifestCache = null;
67
+ return manifestCache;
68
+ }
69
+ try {
70
+ manifestCache = JSON.parse(node_fs_1.default.readFileSync(file, "utf8"));
71
+ }
72
+ catch {
73
+ manifestCache = null;
74
+ }
75
+ return manifestCache;
76
+ }
77
+ let installed = false;
78
+ /**
79
+ * Install the source-loading hooks on the current (main) thread. Idempotent:
80
+ * the bootstrap installs them for the entry process, and `NODE_OPTIONS`
81
+ * re-imports the installer in every child process the program spawns — both may
82
+ * run in the same process.
83
+ *
84
+ * Two hooks are needed, because `module.registerHooks` does not intercept a
85
+ * `require()` made from inside a CommonJS module that was itself reached
86
+ * through an ESM `import` (the interop translator loads it on the raw CJS
87
+ * path). The ESM graph goes through `registerHooks`; the CommonJS `require`
88
+ * graph goes through `Module._extensions` — the canonical loader extension
89
+ * point `ts-node`/`tsx` use for the same reason.
90
+ */
91
+ function installRuntimeHooks() {
92
+ if (installed) {
93
+ return;
94
+ }
95
+ installed = true;
96
+ (0, node_module_1.registerHooks)({ load, resolve });
97
+ installCommonJsHook();
98
+ }
99
+ /**
100
+ * Register a CommonJS `require` handler for each TypeScript source extension so
101
+ * a `require("./x")` chain compiles `.ts` the same way the ESM `load` hook
102
+ * does.
103
+ */
104
+ function installCommonJsHook() {
105
+ const extensions = node_module_1.Module._extensions;
106
+ const compile = (module, filename) => {
107
+ module._compile(resolveServedSource(filename).source, filename);
108
+ };
109
+ for (const extension of [".ts", ".tsx", ".cts"]) {
110
+ extensions[extension] = compile;
111
+ }
112
+ }
113
+ /**
114
+ * The module format of the entry source file, derived from the entry project's
115
+ * `module` option (via the runtime manifest) the same way the served files are
116
+ * classified. The bootstrap uses it to load the entry through a CommonJS
117
+ * `require` or an ESM `import`.
118
+ */
119
+ function entryModuleFormat(entryFile) {
120
+ return moduleFormat(entryFile, manifest()?.moduleOption) === "module"
121
+ ? "module"
122
+ : "commonjs";
123
+ }
124
+ /**
125
+ * Rescue an extensionless or directory relative specifier that Node's resolver
126
+ * rejected. Only runs after `nextResolve` throws, so a successful resolution is
127
+ * never perturbed; a genuinely missing module finds no candidate and the
128
+ * original error is rethrown, preserving `ERR_MODULE_NOT_FOUND`.
129
+ */
130
+ function resolve(specifier, context, nextResolve) {
131
+ try {
132
+ return nextResolve(specifier, context);
133
+ }
134
+ catch (error) {
135
+ const rescued = probeRescuableSpecifier(specifier, context.parentURL);
136
+ if (rescued === null) {
137
+ throw error;
138
+ }
139
+ return { shortCircuit: true, url: rescued };
140
+ }
141
+ }
142
+ const builtProjects = new Map();
143
+ function load(url, context, nextLoad) {
144
+ if (!url.startsWith("file:")) {
145
+ return nextLoad(url, context);
146
+ }
147
+ const filename = (0, node_url_1.fileURLToPath)(url);
148
+ if (!isTypeScriptSource(filename)) {
149
+ return nextLoad(url, context);
150
+ }
151
+ const { source, moduleOption } = resolveServedSource(filename, url);
152
+ return {
153
+ format: moduleFormat(filename, moduleOption),
154
+ shortCircuit: true,
155
+ source,
156
+ };
157
+ }
158
+ /**
159
+ * Resolve the JavaScript to run for a TypeScript source file, in priority
160
+ * order: the entry project's pre-built emit (transform plugins applied), a
161
+ * built raw `.ts` dependency, or — when no tsconfig owns it — a `mode:
162
+ * "transform"` type-strip. Shared by the ESM `load` hook and the CommonJS
163
+ * `require` handler.
164
+ */
165
+ function resolveServedSource(filename, url = (0, node_url_1.pathToFileURL)(filename).href) {
166
+ const real = realPath(filename);
167
+ const served = serveEntryEmit(real);
168
+ if (served !== null) {
169
+ return { moduleOption: manifest()?.moduleOption, source: served };
170
+ }
171
+ const built = serveDependencyEmit(real);
172
+ if (built !== null) {
173
+ return built;
174
+ }
175
+ return {
176
+ moduleOption: undefined,
177
+ source: transformOrphanSource(filename, url),
178
+ };
179
+ }
180
+ /**
181
+ * Transform a TypeScript source file that no tsconfig owns (a published or
182
+ * vendored package that ships raw `.ts`/`.cts`/`.mts` straight under
183
+ * `node_modules`), choosing the lowering by the format the file resolves to.
184
+ *
185
+ * Node's in-process `stripTypeScriptTypes` only erases type syntax; it never
186
+ * rewrites ECMAScript `import`/`export` into CommonJS. That is correct for a
187
+ * file Node will load as ESM, but wrong for one classified CommonJS — a `.cts`,
188
+ * or a `.ts` in a package without `type: "module"` — when the author wrote it
189
+ * with module syntax (`export const`, `export namespace`, `export function`).
190
+ * Stripping leaves the `export` in place and Node's CommonJS loader dies with
191
+ * `SyntaxError: Unexpected token 'export'`. So a CommonJS-format orphan is
192
+ * lowered through a real tsgo `--module commonjs` single-file emit (which also
193
+ * handles `export =`), exactly the format decision tsgo would have made for an
194
+ * owning project; an ESM-format orphan keeps the fast in-process strip.
195
+ */
196
+ function transformOrphanSource(filename, url) {
197
+ if (moduleFormat(filename, undefined) === "commonjs") {
198
+ const lowered = emitOrphanAsCommonJs(filename);
199
+ if (lowered !== null) {
200
+ return lowered;
201
+ }
202
+ }
203
+ return (0, node_module_1.stripTypeScriptTypes)(node_fs_1.default.readFileSync(filename, "utf8"), {
204
+ mode: "transform",
205
+ sourceUrl: url,
206
+ });
207
+ }
208
+ /**
209
+ * Lower a single CommonJS-format source file to CommonJS JavaScript by running
210
+ * tsgo on the lone file with `--module commonjs`. Emit-only, no diagnostic gate
211
+ * (the entry project's up-front check is the type gate), matching
212
+ * `buildDependency`. Returns `null` when tsgo is unavailable or produced no
213
+ * output, so the caller can fall back to the in-process strip.
214
+ */
215
+ function emitOrphanAsCommonJs(filename) {
216
+ let tsgo;
217
+ try {
218
+ tsgo = (0, resolveTsgo_1.resolveTsgo)({ cwd: node_path_1.default.dirname(filename) }).binary;
219
+ }
220
+ catch {
221
+ return null;
222
+ }
223
+ // Content-hash cache: a CJS-format orphan ('s tsgo single-file emit) is lowered
224
+ // once and reused by every other process in the run, and across runs. Without
225
+ // it a program that fans out into many processes (the automated test corpus
226
+ // imports the same vendored `.ts` deps from thousands of generated files) would
227
+ // re-spawn tsgo per file per process and crawl.
228
+ const cacheFile = orphanCacheFile(filename, tsgo);
229
+ if (cacheFile !== null) {
230
+ const hit = readFileOrNull(cacheFile);
231
+ if (hit !== null) {
232
+ return hit;
233
+ }
234
+ }
235
+ const outDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "ttsx-orphan-"));
236
+ try {
237
+ const res = (0, spawnNative_1.spawnNative)(tsgo, [
238
+ filename,
239
+ // The file is named on the command line, so any tsconfig tsgo would
240
+ // discover by walking up (the consumer's own) must be ignored — both
241
+ // because it is not this file's project and because tsgo errors out
242
+ // ("tsconfig.json is present but will not be loaded") otherwise.
243
+ "--ignoreConfig",
244
+ "--module",
245
+ "commonjs",
246
+ "--target",
247
+ "es2022",
248
+ // This is an emit-only lowering: the entry project's up-front build is
249
+ // the type gate, so the single-file pass does not need to type-check.
250
+ // Skipping the check (and the lib check it implies) cuts the per-file
251
+ // cost several-fold, which matters when a program generates and imports
252
+ // thousands of raw `.ts` files at runtime (a fanned-out test corpus) and
253
+ // each one would otherwise pay a full single-file check.
254
+ "--noCheck",
255
+ "--skipLibCheck",
256
+ "--outDir",
257
+ outDir,
258
+ "--listEmittedFiles",
259
+ ], { cwd: node_path_1.default.dirname(filename), encoding: "utf8" });
260
+ const emitted = parseFirstEmittedFile((0, spawnNative_1.outputText)(res.stdout));
261
+ const lowered = emitted === null ? null : readFileOrNull(emitted);
262
+ if (lowered !== null && cacheFile !== null) {
263
+ writeOrphanCache(cacheFile, lowered);
264
+ }
265
+ return lowered;
266
+ }
267
+ catch {
268
+ return null;
269
+ }
270
+ finally {
271
+ node_fs_1.default.rmSync(outDir, { force: true, recursive: true });
272
+ }
273
+ }
274
+ /** Cache root for lowered orphan sources, shared per run (and across runs when
275
+ * `TTSC_CACHE_DIR` points at a persisted directory). */
276
+ function orphanCacheRoot() {
277
+ const base = process.env.TTSC_CACHE_DIR && process.env.TTSC_CACHE_DIR.length !== 0
278
+ ? process.env.TTSC_CACHE_DIR
279
+ : node_path_1.default.join(node_os_1.default.tmpdir(), "ttsc-orphan");
280
+ return node_path_1.default.join(base, "ttsx-orphan-cjs");
281
+ }
282
+ /** Content-addressed cache path for one orphan file's CommonJS lowering, keyed
283
+ * by source bytes and the tsgo binary so a tsgo bump invalidates it. `null` when
284
+ * the source cannot be read. */
285
+ function orphanCacheFile(filename, tsgo) {
286
+ let source;
287
+ try {
288
+ source = node_fs_1.default.readFileSync(filename);
289
+ }
290
+ catch {
291
+ return null;
292
+ }
293
+ const key = node_crypto_1.default
294
+ .createHash("sha256")
295
+ .update(tsgo)
296
+ .update("\0")
297
+ .update(source)
298
+ .digest("hex")
299
+ .slice(0, 32);
300
+ return node_path_1.default.join(orphanCacheRoot(), `${key}.js`);
301
+ }
302
+ /** Write the lowered source to its cache path atomically (temp + rename), so a
303
+ * concurrent reader never sees a half-written file. Best-effort: a failure just
304
+ * means the next process re-lowers. */
305
+ function writeOrphanCache(cacheFile, lowered) {
306
+ try {
307
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(cacheFile), { recursive: true });
308
+ const tmp = `${cacheFile}.${process.pid}.${Date.now()}.tmp`;
309
+ node_fs_1.default.writeFileSync(tmp, lowered);
310
+ node_fs_1.default.renameSync(tmp, cacheFile);
311
+ }
312
+ catch {
313
+ // ignore — caching is an optimization, correctness does not depend on it
314
+ }
315
+ }
316
+ /** First `TSFILE:` path tsgo printed under `--listEmittedFiles`, or `null`. */
317
+ function parseFirstEmittedFile(stdout) {
318
+ for (const line of stdout.split(/\r?\n/)) {
319
+ const match = line.match(/^TSFILE:\s*(.+)$/);
320
+ if (match?.[1]) {
321
+ return match[1].trim();
322
+ }
323
+ }
324
+ return null;
325
+ }
326
+ /**
327
+ * Serve the entry project's pre-built JavaScript for a source file the build
328
+ * emitted, or `null` when the file is outside the build or its emit is
329
+ * missing.
330
+ *
331
+ * The bound is the project's `rootDir` (the source root the emit mirrors), not
332
+ * its tsconfig directory: a project can pull in a file from elsewhere via
333
+ * `files` with a wider `rootDir` (e.g. the lint config loader compiles a
334
+ * `*.config.ts` from any directory under `rootDir: "/"`). Anything outside
335
+ * `rootDir` cannot have a mirrored emit, so it falls through to the dependency
336
+ * paths.
337
+ */
338
+ function serveEntryEmit(real) {
339
+ const m = manifest();
340
+ if (m === null) {
341
+ return null;
342
+ }
343
+ if (!isWithin(real, m.rootDir)) {
344
+ return null;
345
+ }
346
+ const emitted = (0, resolveEmittedJavaScript_1.resolveEmittedJavaScript)({
347
+ emittedFiles: m.emittedFiles,
348
+ outDir: m.emitDir,
349
+ projectRoot: m.rootDir,
350
+ sourceFile: real,
351
+ });
352
+ return readFileOrNull(emitted);
353
+ }
354
+ /**
355
+ * True when `real` is `directory` itself or sits beneath it. Handles a root
356
+ * `directory` (`/`, `C:\`): naively appending a separator would yield `//`,
357
+ * which no path starts with, so a `rootDir: "/"` project would serve nothing.
358
+ */
359
+ function isWithin(real, directory) {
360
+ if (real === directory) {
361
+ return true;
362
+ }
363
+ const prefix = directory.endsWith(node_path_1.default.sep)
364
+ ? directory
365
+ : directory + node_path_1.default.sep;
366
+ return real.startsWith(prefix);
367
+ }
368
+ /**
369
+ * Build the project that owns `real` (nearest `tsconfig.json` above its real
370
+ * path) and return its emitted JavaScript, or `null` when no tsconfig owns it
371
+ * or the project does not emit it. The build honours the dependency's own
372
+ * tsconfig (transform plugins included), so a source-shipping package that
373
+ * needs a transform behaves correctly at runtime.
374
+ */
375
+ function serveDependencyEmit(real) {
376
+ const tsconfig = nearestTsconfig(real);
377
+ if (tsconfig === null) {
378
+ return null;
379
+ }
380
+ let built;
381
+ try {
382
+ built = ensureProjectBuilt(tsconfig);
383
+ }
384
+ catch {
385
+ // The owning project produced no emit at all; fall back to type-stripping
386
+ // this single file rather than failing the whole run.
387
+ return null;
388
+ }
389
+ const emitted = (0, resolveEmittedJavaScript_1.resolveEmittedJavaScript)({
390
+ emittedFiles: built.emittedFiles,
391
+ outDir: built.emitDir,
392
+ projectRoot: built.rootDir,
393
+ sourceFile: real,
394
+ });
395
+ const source = readFileOrNull(emitted);
396
+ return source === null ? null : { moduleOption: built.moduleOption, source };
397
+ }
398
+ /**
399
+ * Build the project that owns a dependency once per run and share the result
400
+ * across every process the program spawns.
401
+ *
402
+ * A program (a benchmark, a worker pool) can fan out into many child processes,
403
+ * each of which inherits the runtime manifest and would otherwise rebuild every
404
+ * dependency from scratch — and worse, several at once into the same directory,
405
+ * corrupting each other. So the build output is content-keyed under the shared
406
+ * per-run cache: a finished build leaves a meta marker that any later process
407
+ * (or a second import in this one) reuses, and concurrent first-builders are
408
+ * serialised by an atomic lock directory.
409
+ */
410
+ function ensureProjectBuilt(tsconfig) {
411
+ const cached = builtProjects.get(tsconfig);
412
+ if (cached !== undefined) {
413
+ return cached;
414
+ }
415
+ const key = node_crypto_1.default
416
+ .createHash("sha256")
417
+ .update(tsconfig)
418
+ .digest("hex")
419
+ .slice(0, 16);
420
+ const root = dependencyCacheRoot();
421
+ const emitDir = node_path_1.default.join(root, key);
422
+ const metaPath = node_path_1.default.join(root, `${key}.json`);
423
+ const lockDir = node_path_1.default.join(root, `${key}.lock`);
424
+ const reuse = readDependencyCache(emitDir, metaPath);
425
+ if (reuse !== null) {
426
+ builtProjects.set(tsconfig, reuse);
427
+ return reuse;
428
+ }
429
+ node_fs_1.default.mkdirSync(root, { recursive: true });
430
+ const built = withBuildLock(lockDir, metaPath, emitDir, () => buildDependency(tsconfig, emitDir, metaPath));
431
+ builtProjects.set(tsconfig, built);
432
+ return built;
433
+ }
434
+ /** Reuse a dependency another process (or an earlier import) already built. */
435
+ function readDependencyCache(emitDir, metaPath) {
436
+ let meta;
437
+ try {
438
+ meta = JSON.parse(node_fs_1.default.readFileSync(metaPath, "utf8"));
439
+ }
440
+ catch {
441
+ return null;
442
+ }
443
+ if (!emittedAnything(emitDir)) {
444
+ return null;
445
+ }
446
+ return {
447
+ emitDir,
448
+ emittedFiles: undefined,
449
+ moduleOption: meta.moduleOption,
450
+ rootDir: meta.rootDir,
451
+ };
452
+ }
453
+ /**
454
+ * Run `build` while holding an exclusive lock for this dependency, re-checking
455
+ * the cache once the lock is held (a concurrent builder may have just
456
+ * finished). A waiter polls for the winner's meta marker, and steals an
457
+ * abandoned lock (a builder that crashed) after a generous timeout so the run
458
+ * never wedges.
459
+ */
460
+ function withBuildLock(lockDir, metaPath, emitDir, build) {
461
+ const stealAfterMs = 600_000;
462
+ for (;;) {
463
+ try {
464
+ node_fs_1.default.mkdirSync(lockDir);
465
+ }
466
+ catch {
467
+ const waited = waitForDependencyCache(emitDir, metaPath, stealAfterMs);
468
+ if (waited !== null) {
469
+ return waited;
470
+ }
471
+ node_fs_1.default.rmSync(lockDir, { force: true, recursive: true });
472
+ continue;
473
+ }
474
+ try {
475
+ const reuse = readDependencyCache(emitDir, metaPath);
476
+ return reuse ?? build();
477
+ }
478
+ finally {
479
+ node_fs_1.default.rmSync(lockDir, { force: true, recursive: true });
480
+ }
481
+ }
482
+ }
483
+ /** Poll for a concurrent builder's completion, up to `timeoutMs`. */
484
+ function waitForDependencyCache(emitDir, metaPath, timeoutMs) {
485
+ const startedAt = Date.now();
486
+ for (;;) {
487
+ const reuse = readDependencyCache(emitDir, metaPath);
488
+ if (reuse !== null) {
489
+ return reuse;
490
+ }
491
+ if (Date.now() - startedAt > timeoutMs) {
492
+ return null;
493
+ }
494
+ sleepSync(50);
495
+ }
496
+ }
497
+ /** Block the current (synchronous) thread for `ms` without busy-spinning. */
498
+ function sleepSync(ms) {
499
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
500
+ }
501
+ /** Compile a dependency project to `emitDir` and write its completion marker. */
502
+ function buildDependency(tsconfig, emitDir, metaPath) {
503
+ const project = (0, readProjectConfig_1.readProjectConfig)({ cwd: node_path_1.default.dirname(tsconfig), tsconfig });
504
+ node_fs_1.default.rmSync(emitDir, { force: true, recursive: true });
505
+ const result = (0, runBuild_1.runBuild)({
506
+ cwd: project.root,
507
+ emit: true,
508
+ forceListEmittedFiles: true,
509
+ outDir: emitDir,
510
+ // Honour the dependency's own transform plugins: a source-shipping package
511
+ // can itself depend on a transform (e.g. a fixture whose values are built
512
+ // with `typia.createRandom`), and its runtime behaviour is wrong without it.
513
+ // `runBuild` runs on this main thread, so its plugin resolution works the
514
+ // same as the entry build's.
515
+ quiet: true,
516
+ // Emit only: the entry project's up-front check is the type gate. A
517
+ // dependency build pulls its own transitive sources into the program and
518
+ // would otherwise fail on type diagnostics that belong to those packages
519
+ // under their own (laxer) config — e.g. unused-type-parameter warnings in a
520
+ // transitively imported library. We still want the type-aware emit (for
521
+ // type-only elision), just not the error gate.
522
+ skipDiagnosticsCheck: true,
523
+ tsconfig,
524
+ });
525
+ // Success is "the project wrote JavaScript", not "the build reported a file
526
+ // list": a native transform host (typia, @ttsc/banner, …) emits without
527
+ // printing the `--listEmittedFiles` lines, so `result.emittedFiles` is empty
528
+ // even on a clean build. A genuinely empty output directory is the real
529
+ // failure; the caller then falls back to type-stripping the one file.
530
+ if (!emittedAnything(emitDir)) {
531
+ throw new Error([
532
+ `ttsx: dependency build produced no output for ${tsconfig}`,
533
+ result.stderr || result.stdout,
534
+ ]
535
+ .filter((line) => line.trim().length !== 0)
536
+ .join("\n"));
537
+ }
538
+ const rootDir = typeof project.compilerOptions.rootDir === "string"
539
+ ? project.compilerOptions.rootDir
540
+ : project.root;
541
+ const moduleOption = typeof project.compilerOptions.module === "string"
542
+ ? project.compilerOptions.module
543
+ : undefined;
544
+ node_fs_1.default.writeFileSync(metaPath, JSON.stringify({ moduleOption, rootDir }), "utf8");
545
+ return { emitDir, emittedFiles: undefined, moduleOption, rootDir };
546
+ }
547
+ function dependencyCacheRoot() {
548
+ const m = manifest();
549
+ return m !== null && m.depCacheDir.length !== 0
550
+ ? m.depCacheDir
551
+ : node_path_1.default.join(node_os_1.default.tmpdir(), "ttsx-dep");
552
+ }
553
+ /** Owning-tsconfig cache keyed by directory, mirroring `packageTypeCache`. */
554
+ const tsconfigCache = new Map();
555
+ /**
556
+ * The nearest `tsconfig.json` at or above `file`'s directory, or `null`. The
557
+ * walk stops at a `node_modules` boundary: a tsconfig above `node_modules`
558
+ * belongs to the consumer, not to the published dependency inside it, so a
559
+ * dependency that ships no tsconfig of its own has no owning project and is
560
+ * type-stripped instead. A pnpm-symlinked workspace package is unaffected
561
+ * because `file` is already its real path (outside `node_modules`).
562
+ *
563
+ * The walk is memoised per directory (the whole walked chain shares one
564
+ * answer), so the thousands of files a fanned-out test corpus imports from the
565
+ * same handful of projects do not each re-stat the same parent directories.
566
+ */
567
+ function nearestTsconfig(file) {
568
+ let directory = node_path_1.default.dirname(file);
569
+ const chain = [];
570
+ for (;;) {
571
+ const cached = tsconfigCache.get(directory);
572
+ if (cached !== undefined) {
573
+ return rememberTsconfig(chain, cached);
574
+ }
575
+ if (node_path_1.default.basename(directory) === "node_modules") {
576
+ return rememberTsconfig(chain, null);
577
+ }
578
+ chain.push(directory);
579
+ const candidate = node_path_1.default.join(directory, "tsconfig.json");
580
+ if (isFile(candidate)) {
581
+ return rememberTsconfig(chain, candidate);
582
+ }
583
+ const parent = node_path_1.default.dirname(directory);
584
+ if (parent === directory) {
585
+ return rememberTsconfig(chain, null);
586
+ }
587
+ directory = parent;
588
+ }
589
+ }
590
+ function rememberTsconfig(directories, result) {
591
+ for (const directory of directories) {
592
+ tsconfigCache.set(directory, result);
593
+ }
594
+ return result;
595
+ }
596
+ function readFileOrNull(file) {
597
+ if (file === null) {
598
+ return null;
599
+ }
600
+ try {
601
+ return node_fs_1.default.readFileSync(file, "utf8");
602
+ }
603
+ catch {
604
+ return null;
605
+ }
606
+ }
607
+ function realPath(target) {
608
+ try {
609
+ return node_fs_1.default.realpathSync(target);
610
+ }
611
+ catch {
612
+ return target;
613
+ }
614
+ }
615
+ /**
616
+ * Map the JavaScript extension a relative `specifier` carries to the TypeScript
617
+ * source extensions tsgo would have emitted it from. Running from source, a
618
+ * `"./x.js"` import (whether authored or rewritten from `"./x.ts"` by
619
+ * `--rewriteRelativeImportExtensions`) has no `.js` on disk — only `./x.ts`.
620
+ */
621
+ const JS_TO_TS_EXTENSIONS = new Map([
622
+ [".js", [".ts", ".tsx"]],
623
+ [".jsx", [".tsx"]],
624
+ [".mjs", [".mts"]],
625
+ [".cjs", [".cts"]],
626
+ ]);
627
+ /**
628
+ * Rescue a `specifier` that Node's resolver rejected: map a JavaScript
629
+ * extension back to its TypeScript source, or probe candidate extensions /
630
+ * directory indexes for an extensionless form. Returns a `file:` URL for the
631
+ * first match, or `null` when nothing matches.
632
+ *
633
+ * Handles two shapes:
634
+ *
635
+ * - A relative specifier (`./x`) resolved against a `file:` parent — a normal
636
+ * `import`/`require` inside a served module;
637
+ * - An already-absolute specifier with no parent — the main entry of a
638
+ * `child_process.fork(__dirname + "/servant.js")`. fork's main module reaches
639
+ * the resolve hook as an absolute `.js` path with `parentURL` undefined, and
640
+ * run-from-source ships only the `.ts`, so without this the child dies with
641
+ * `Cannot find module servant.js` and a tgrid master waits on it forever.
642
+ */
643
+ function probeRescuableSpecifier(specifier, parentURL) {
644
+ // A `?query` / `#hash` suffix is part of module identity, not the path; strip
645
+ // it before resolving and re-attach it to the resolved URL so a loader keying
646
+ // on the suffix (and `import.meta.url`) sees it preserved.
647
+ const suffixStart = specifier.search(/[?#]/);
648
+ const suffix = suffixStart === -1 ? "" : specifier.slice(suffixStart);
649
+ const pathname = suffixStart === -1 ? specifier : specifier.slice(0, suffixStart);
650
+ let base;
651
+ if (isRelativeSpecifier(specifier)) {
652
+ if (parentURL === undefined || !parentURL.startsWith("file:")) {
653
+ return null;
654
+ }
655
+ const parentDir = node_path_1.default.dirname((0, node_url_1.fileURLToPath)(parentURL));
656
+ base = node_path_1.default.resolve(parentDir, pathname);
657
+ }
658
+ else if (node_path_1.default.isAbsolute(pathname)) {
659
+ base = pathname;
660
+ }
661
+ else {
662
+ return null;
663
+ }
664
+ const withSuffix = (candidate) => (0, node_url_1.pathToFileURL)(candidate).href + suffix;
665
+ const jsExtension = node_path_1.default.extname(base).toLowerCase();
666
+ const tsExtensions = JS_TO_TS_EXTENSIONS.get(jsExtension);
667
+ if (tsExtensions !== undefined) {
668
+ const stem = base.slice(0, base.length - jsExtension.length);
669
+ for (const extension of tsExtensions) {
670
+ const candidate = stem + extension;
671
+ if (isFile(candidate)) {
672
+ return withSuffix(candidate);
673
+ }
674
+ }
675
+ return null;
676
+ }
677
+ if (hasConcreteExtension(pathname)) {
678
+ return null;
679
+ }
680
+ for (const extension of RESOLVABLE_EXTENSIONS) {
681
+ const candidate = base + extension;
682
+ if (isFile(candidate)) {
683
+ return withSuffix(candidate);
684
+ }
685
+ }
686
+ for (const extension of RESOLVABLE_EXTENSIONS) {
687
+ const candidate = node_path_1.default.join(base, `index${extension}`);
688
+ if (isFile(candidate)) {
689
+ return withSuffix(candidate);
690
+ }
691
+ }
692
+ return null;
693
+ }
694
+ /**
695
+ * Decide the module format the way Node and tsgo do — from configuration, never
696
+ * by sniffing the emitted text. The file extension is authoritative
697
+ * (`.mts`/`.mjs` → module, `.cts`/`.cjs` → commonjs); otherwise the owning
698
+ * tsconfig's `module` option decides: a fixed CommonJS / ES target maps
699
+ * directly, while the `node*`/`preserve` family (and a file with no owning
700
+ * tsconfig) defers to the nearest `package.json` `type`, exactly as tsgo chose
701
+ * when it emitted.
702
+ */
703
+ function moduleFormat(filename, moduleOption) {
704
+ if (filename.endsWith(".mts") || filename.endsWith(".mjs")) {
705
+ return "module";
706
+ }
707
+ if (filename.endsWith(".cts") || filename.endsWith(".cjs")) {
708
+ return "commonjs";
709
+ }
710
+ const option = (moduleOption ?? "").toLowerCase();
711
+ if (option === "commonjs" || option === "node10" || option === "none") {
712
+ return "commonjs";
713
+ }
714
+ if (option === "" ||
715
+ option === "node16" ||
716
+ option === "node18" ||
717
+ option === "nodenext" ||
718
+ option === "preserve") {
719
+ return nearestPackageType(filename);
720
+ }
721
+ // Every remaining `module` target (es2015 … esnext, system, amd, umd) emits
722
+ // ECMAScript modules.
723
+ return "module";
724
+ }
725
+ /** Package-type cache keyed by directory, mirroring Node's own lookup walk. */
726
+ const packageTypeCache = new Map();
727
+ function nearestPackageType(filename) {
728
+ let directory = node_path_1.default.dirname(filename);
729
+ const chain = [];
730
+ while (true) {
731
+ const cached = packageTypeCache.get(directory);
732
+ if (cached !== undefined) {
733
+ return rememberPackageType(chain, cached);
734
+ }
735
+ chain.push(directory);
736
+ const type = readPackageType(directory);
737
+ if (type !== null) {
738
+ return rememberPackageType(chain, type);
739
+ }
740
+ const parent = node_path_1.default.dirname(directory);
741
+ if (parent === directory) {
742
+ return rememberPackageType(chain, "commonjs");
743
+ }
744
+ directory = parent;
745
+ }
746
+ }
747
+ function rememberPackageType(directories, type) {
748
+ for (const directory of directories) {
749
+ packageTypeCache.set(directory, type);
750
+ }
751
+ return type;
752
+ }
753
+ /** Read a directory's `package.json` `type`, or `null` when absent/invalid. */
754
+ function readPackageType(directory) {
755
+ const manifestPath = node_path_1.default.join(directory, "package.json");
756
+ if (!isFile(manifestPath)) {
757
+ return null;
758
+ }
759
+ try {
760
+ const parsed = JSON.parse(node_fs_1.default.readFileSync(manifestPath, "utf8"));
761
+ return parsed.type === "module" ? "module" : "commonjs";
762
+ }
763
+ catch {
764
+ return "commonjs";
765
+ }
766
+ }
767
+ function isRelativeSpecifier(specifier) {
768
+ return (specifier === "." ||
769
+ specifier === ".." ||
770
+ specifier.startsWith("./") ||
771
+ specifier.startsWith("../"));
772
+ }
773
+ /** True when `specifier` already carries an extension Node can load directly. */
774
+ function hasConcreteExtension(specifier) {
775
+ return /\.(?:[cm]?jsx?|json|node|[cm]?tsx?)$/i.test(specifier);
776
+ }
777
+ function isTypeScriptSource(filename) {
778
+ return TYPESCRIPT_EXTENSIONS.some((extension) => filename.endsWith(extension));
779
+ }
780
+ function isFile(candidate) {
781
+ try {
782
+ return node_fs_1.default.statSync(candidate).isFile();
783
+ }
784
+ catch {
785
+ return false;
786
+ }
787
+ }
788
+ /** True when `directory` holds at least one emitted JavaScript file (any depth). */
789
+ function emittedAnything(directory) {
790
+ let entries;
791
+ try {
792
+ entries = node_fs_1.default.readdirSync(directory, { withFileTypes: true });
793
+ }
794
+ catch {
795
+ return false;
796
+ }
797
+ for (const entry of entries) {
798
+ const full = node_path_1.default.join(directory, entry.name);
799
+ if (entry.isDirectory()) {
800
+ if (emittedAnything(full)) {
801
+ return true;
802
+ }
803
+ }
804
+ else if (entry.isFile() && /\.(?:[cm]?js)$/i.test(entry.name)) {
805
+ return true;
806
+ }
807
+ }
808
+ return false;
809
+ }
810
+ //# sourceMappingURL=runtimeHooks.js.map