ttsc 0.14.2 → 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.
- package/driver/emit_plugin.go +131 -0
- package/driver/plugins.go +37 -0
- package/go.mod +0 -3
- package/go.sum +0 -4
- package/lib/compiler/internal/resolveEmittedJavaScript.js +25 -4
- package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
- package/lib/launcher/internal/prepareExecution.d.ts +4 -1
- package/lib/launcher/internal/prepareExecution.js +13 -17
- package/lib/launcher/internal/prepareExecution.js.map +1 -1
- package/lib/launcher/internal/registerRuntimeHooks.js +43 -8
- package/lib/launcher/internal/registerRuntimeHooks.js.map +1 -1
- package/lib/launcher/internal/runTtsx.js +46 -386
- package/lib/launcher/internal/runTtsx.js.map +1 -1
- package/lib/launcher/internal/runtimeHookPreload.d.ts +1 -0
- package/lib/launcher/internal/runtimeHookPreload.js +16 -0
- package/lib/launcher/internal/runtimeHookPreload.js.map +1 -0
- package/lib/launcher/internal/runtimeHooks.d.ts +16 -38
- package/lib/launcher/internal/runtimeHooks.js +672 -119
- package/lib/launcher/internal/runtimeHooks.js.map +1 -1
- package/lib/plugin/internal/buildSourcePlugin.js +120 -16
- package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
- package/lib/plugin/internal/loadProjectPlugins.js +15 -4
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/package.json +8 -8
- package/shim/ast/parent.go +31 -0
- package/shim/compiler/emitassemble.go +30 -0
- package/shim/compiler/emithook.go +22 -0
- package/shim/compiler/go.mod +9 -0
- package/shim/compiler/go.sum +22 -0
- package/shim/core/modulekind.go +10 -0
- package/shim/printer/emithost.go +24 -0
- package/shim/printer/go.mod +9 -0
- package/shim/printer/go.sum +22 -0
- package/shim/transformers/go.mod +14 -0
- package/shim/transformers/go.sum +20 -0
- package/shim/transformers/shim.go +15 -0
- package/src/compiler/internal/resolveEmittedJavaScript.ts +35 -4
- package/src/launcher/internal/prepareExecution.ts +18 -21
- package/src/launcher/internal/registerRuntimeHooks.ts +53 -8
- package/src/launcher/internal/runTtsx.ts +61 -453
- package/src/launcher/internal/runtimeHookPreload.ts +14 -0
- package/src/launcher/internal/runtimeHooks.ts +794 -136
- package/src/plugin/internal/buildSourcePlugin.ts +147 -16
- package/src/plugin/internal/loadProjectPlugins.ts +16 -4
|
@@ -3,27 +3,46 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
7
|
-
exports.
|
|
6
|
+
exports.installRuntimeHooks = installRuntimeHooks;
|
|
7
|
+
exports.entryModuleFormat = entryModuleFormat;
|
|
8
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
8
9
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
10
|
const node_module_1 = require("node:module");
|
|
11
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
10
12
|
const node_path_1 = __importDefault(require("node:path"));
|
|
11
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");
|
|
12
19
|
/**
|
|
13
|
-
* Node
|
|
14
|
-
* `ttsx` spawns
|
|
15
|
-
* without weakening the compile gate: type-checking still happens up front in
|
|
16
|
-
* `prepareExecution`'s tsgo build, and these hooks only affect how dependencies
|
|
17
|
-
* are _loaded_ at runtime.
|
|
20
|
+
* Synchronous Node module hooks installed (via `module.registerHooks`) in the
|
|
21
|
+
* child process `ttsx` spawns to run a TypeScript entry _from source_.
|
|
18
22
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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:
|
|
24
28
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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.
|
|
27
46
|
*/
|
|
28
47
|
/** Source/JS extensions probed when an extensionless relative import fails. */
|
|
29
48
|
const RESOLVABLE_EXTENSIONS = [
|
|
@@ -35,141 +54,673 @@ const RESOLVABLE_EXTENSIONS = [
|
|
|
35
54
|
".mjs",
|
|
36
55
|
".cjs",
|
|
37
56
|
];
|
|
38
|
-
/** TypeScript source extensions
|
|
57
|
+
/** TypeScript source extensions these hooks compile. */
|
|
39
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;
|
|
40
78
|
/**
|
|
41
|
-
*
|
|
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.
|
|
42
83
|
*
|
|
43
|
-
*
|
|
44
|
-
* `
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
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`.
|
|
49
129
|
*/
|
|
50
|
-
|
|
130
|
+
function resolve(specifier, context, nextResolve) {
|
|
51
131
|
try {
|
|
52
|
-
return
|
|
132
|
+
return nextResolve(specifier, context);
|
|
53
133
|
}
|
|
54
134
|
catch (error) {
|
|
55
|
-
const rescued =
|
|
135
|
+
const rescued = probeRescuableSpecifier(specifier, context.parentURL);
|
|
56
136
|
if (rescued === null) {
|
|
57
137
|
throw error;
|
|
58
138
|
}
|
|
59
139
|
return { shortCircuit: true, url: rescued };
|
|
60
140
|
}
|
|
61
141
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
* Node strips types for first-party `.ts` automatically but refuses to do so
|
|
66
|
-
* under `node_modules` (`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`). This
|
|
67
|
-
* hook performs the same type-strip transform for those files, scoped strictly
|
|
68
|
-
* to `node_modules` so workspace neighbours keep Node's native path.
|
|
69
|
-
*/
|
|
70
|
-
async function load(url, context, nextLoad) {
|
|
71
|
-
const filename = transpilableNodeModulesPath(url);
|
|
72
|
-
if (filename === null) {
|
|
142
|
+
const builtProjects = new Map();
|
|
143
|
+
function load(url, context, nextLoad) {
|
|
144
|
+
if (!url.startsWith("file:")) {
|
|
73
145
|
return nextLoad(url, context);
|
|
74
146
|
}
|
|
75
|
-
const
|
|
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);
|
|
76
152
|
return {
|
|
77
|
-
format: moduleFormat(filename,
|
|
153
|
+
format: moduleFormat(filename, moduleOption),
|
|
78
154
|
shortCircuit: true,
|
|
79
155
|
source,
|
|
80
156
|
};
|
|
81
157
|
}
|
|
82
158
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
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.
|
|
86
164
|
*/
|
|
87
|
-
function
|
|
88
|
-
|
|
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 {
|
|
89
221
|
return null;
|
|
90
222
|
}
|
|
91
|
-
|
|
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 {
|
|
92
268
|
return null;
|
|
93
269
|
}
|
|
94
|
-
|
|
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 {
|
|
95
291
|
return null;
|
|
96
292
|
}
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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);
|
|
104
311
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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();
|
|
109
322
|
}
|
|
110
323
|
}
|
|
111
324
|
return null;
|
|
112
325
|
}
|
|
113
326
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
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.
|
|
118
337
|
*/
|
|
119
|
-
function
|
|
120
|
-
|
|
338
|
+
function serveEntryEmit(real) {
|
|
339
|
+
const m = manifest();
|
|
340
|
+
if (m === null) {
|
|
121
341
|
return null;
|
|
122
342
|
}
|
|
123
|
-
|
|
124
|
-
if (!isTypeScriptSource(filename) || !isUnderNodeModules(filename)) {
|
|
343
|
+
if (!isWithin(real, m.rootDir)) {
|
|
125
344
|
return null;
|
|
126
345
|
}
|
|
127
|
-
|
|
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);
|
|
128
353
|
}
|
|
129
354
|
/**
|
|
130
|
-
*
|
|
131
|
-
* (
|
|
132
|
-
*
|
|
133
|
-
* URL, so each dependency file reaches this hook at most once per run.
|
|
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.
|
|
134
358
|
*/
|
|
135
|
-
function
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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,
|
|
139
394
|
});
|
|
395
|
+
const source = readFileOrNull(emitted);
|
|
396
|
+
return source === null ? null : { moduleOption: built.moduleOption, source };
|
|
140
397
|
}
|
|
141
398
|
/**
|
|
142
|
-
*
|
|
399
|
+
* Build the project that owns a dependency once per run and share the result
|
|
400
|
+
* across every process the program spawns.
|
|
143
401
|
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
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.
|
|
150
409
|
*/
|
|
151
|
-
function
|
|
152
|
-
|
|
153
|
-
|
|
410
|
+
function ensureProjectBuilt(tsconfig) {
|
|
411
|
+
const cached = builtProjects.get(tsconfig);
|
|
412
|
+
if (cached !== undefined) {
|
|
413
|
+
return cached;
|
|
154
414
|
}
|
|
155
|
-
|
|
156
|
-
|
|
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;
|
|
157
428
|
}
|
|
158
|
-
|
|
159
|
-
|
|
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;
|
|
160
442
|
}
|
|
161
|
-
|
|
443
|
+
if (!emittedAnything(emitDir)) {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
return {
|
|
447
|
+
emitDir,
|
|
448
|
+
emittedFiles: undefined,
|
|
449
|
+
moduleOption: meta.moduleOption,
|
|
450
|
+
rootDir: meta.rootDir,
|
|
451
|
+
};
|
|
162
452
|
}
|
|
163
453
|
/**
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
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.
|
|
169
459
|
*/
|
|
170
|
-
function
|
|
171
|
-
|
|
172
|
-
|
|
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";
|
|
173
724
|
}
|
|
174
725
|
/** Package-type cache keyed by directory, mirroring Node's own lookup walk. */
|
|
175
726
|
const packageTypeCache = new Map();
|
|
@@ -201,12 +752,12 @@ function rememberPackageType(directories, type) {
|
|
|
201
752
|
}
|
|
202
753
|
/** Read a directory's `package.json` `type`, or `null` when absent/invalid. */
|
|
203
754
|
function readPackageType(directory) {
|
|
204
|
-
const
|
|
205
|
-
if (!isFile(
|
|
755
|
+
const manifestPath = node_path_1.default.join(directory, "package.json");
|
|
756
|
+
if (!isFile(manifestPath)) {
|
|
206
757
|
return null;
|
|
207
758
|
}
|
|
208
759
|
try {
|
|
209
|
-
const parsed = JSON.parse(node_fs_1.default.readFileSync(
|
|
760
|
+
const parsed = JSON.parse(node_fs_1.default.readFileSync(manifestPath, "utf8"));
|
|
210
761
|
return parsed.type === "module" ? "module" : "commonjs";
|
|
211
762
|
}
|
|
212
763
|
catch {
|
|
@@ -226,26 +777,6 @@ function hasConcreteExtension(specifier) {
|
|
|
226
777
|
function isTypeScriptSource(filename) {
|
|
227
778
|
return TYPESCRIPT_EXTENSIONS.some((extension) => filename.endsWith(extension));
|
|
228
779
|
}
|
|
229
|
-
/**
|
|
230
|
-
* True when `filename` lives inside a real `node_modules` package. A
|
|
231
|
-
* `node_modules/.cache` segment does not count: `ttsx` mirrors the project into
|
|
232
|
-
* `<root>/node_modules/.cache/ttsc/ttsx/.../fs/...`, so the user's own sources
|
|
233
|
-
* appear under that cache prefix at runtime. Treating them as dependencies
|
|
234
|
-
* would type-strip a project file (which `stripTypeScriptTypes` cannot do as
|
|
235
|
-
* faithfully as the compile gate, e.g. it cannot elide type-only imports) and
|
|
236
|
-
* steal it from a host loader. A genuine dependency still has a deeper
|
|
237
|
-
* `node_modules/<pkg>` segment, so it is detected; `.cache` (never a package
|
|
238
|
-
* name) is the only thing skipped.
|
|
239
|
-
*/
|
|
240
|
-
function isUnderNodeModules(filename) {
|
|
241
|
-
const segments = filename.split(node_path_1.default.sep);
|
|
242
|
-
for (let i = 0; i < segments.length - 1; i += 1) {
|
|
243
|
-
if (segments[i] === "node_modules" && segments[i + 1] !== ".cache") {
|
|
244
|
-
return true;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
return false;
|
|
248
|
-
}
|
|
249
780
|
function isFile(candidate) {
|
|
250
781
|
try {
|
|
251
782
|
return node_fs_1.default.statSync(candidate).isFile();
|
|
@@ -254,4 +785,26 @@ function isFile(candidate) {
|
|
|
254
785
|
return false;
|
|
255
786
|
}
|
|
256
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
|
+
}
|
|
257
810
|
//# sourceMappingURL=runtimeHooks.js.map
|