ttsc 0.18.4 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -6
- package/cmd/graphbench/main.go +1 -1
- package/cmd/platform/main.go +1 -48
- package/cmd/ttsc/build_branches_test.go +5 -1
- package/cmd/ttsc/main.go +0 -50
- package/cmd/ttsc/main_alias_branches_test.go +6 -12
- package/cmd/ttscgraph/dump.go +13 -3
- package/cmd/ttscserver/main.go +5 -3
- package/driver/lsp.go +13 -1
- package/driver/plugins.go +35 -0
- package/driver/program.go +7 -8
- package/driver/rewrite.go +381 -21
- package/go.mod +1 -0
- package/internal/graph/build.go +297 -3
- package/internal/graph/build_records_a_node_per_top_level_declaration_test.go +16 -2
- package/internal/graph/diagnostics.go +5 -4
- package/internal/graph/dump.go +60 -4
- package/internal/graph/dump_is_a_function_of_the_source_test.go +71 -0
- package/internal/graph/dump_serializes_the_full_graph_test.go +16 -4
- package/internal/graph/edges.go +122 -11
- package/internal/graph/exports.go +74 -17
- package/internal/graph/graph.go +33 -1
- package/internal/graph/node_identity_is_position_invariant_test.go +4 -2
- package/internal/graph/resolve.go +24 -0
- package/internal/graphsymbols/provider.go +50 -14
- package/internal/lspserver/lsp_native_plugin_source.go +77 -23
- package/internal/lspserver/lsp_plugin.go +19 -2
- package/internal/lspserver/lsp_proxy.go +244 -52
- package/internal/lspserver/lsp_server.go +35 -27
- package/internal/lspserver/lsp_symbols.go +25 -16
- package/lib/TtscCompiler.d.ts +10 -0
- package/lib/TtscCompiler.js +24 -6
- package/lib/TtscCompiler.js.map +1 -1
- package/lib/TtscService.js +9 -7
- package/lib/TtscService.js.map +1 -1
- package/lib/compiler/internal/compileProjectInMemory.js +1 -0
- package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
- package/lib/compiler/internal/project/createNativeProjectContextArgs.d.ts +3 -0
- package/lib/compiler/internal/project/createNativeProjectContextArgs.js +14 -0
- package/lib/compiler/internal/project/createNativeProjectContextArgs.js.map +1 -0
- package/lib/compiler/internal/project/readProjectConfig.js +53 -4
- package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
- package/lib/compiler/internal/project/resolveProjectConfig.d.ts +9 -2
- package/lib/compiler/internal/project/resolveProjectConfig.js +45 -16
- package/lib/compiler/internal/project/resolveProjectConfig.js.map +1 -1
- package/lib/compiler/internal/residentTransformProcess.d.ts +16 -5
- package/lib/compiler/internal/residentTransformProcess.js +68 -20
- package/lib/compiler/internal/residentTransformProcess.js.map +1 -1
- package/lib/compiler/internal/runBuild.d.ts +11 -0
- package/lib/compiler/internal/runBuild.js +44 -8
- package/lib/compiler/internal/runBuild.js.map +1 -1
- package/lib/compiler/internal/runSingleFileEmit.js +7 -4
- package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
- package/lib/compiler/internal/sharedHostHelpers.d.ts +16 -0
- package/lib/compiler/internal/sharedHostHelpers.js +23 -0
- package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
- package/lib/compiler/internal/startResidentTransform.js +18 -1
- package/lib/compiler/internal/startResidentTransform.js.map +1 -1
- package/lib/compiler/internal/transformProjectInMemory.js +34 -8
- package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
- package/lib/flags/parser.d.ts +17 -0
- package/lib/flags/parser.js +11 -1
- package/lib/flags/parser.js.map +1 -1
- package/lib/flags/schema.js +7 -0
- package/lib/flags/schema.js.map +1 -1
- package/lib/launcher/internal/prepareExecution.js +21 -2
- package/lib/launcher/internal/prepareExecution.js.map +1 -1
- package/lib/launcher/internal/runTtsc.js +13 -53
- package/lib/launcher/internal/runTtsc.js.map +1 -1
- package/lib/launcher/internal/runTtscserver.js +11 -0
- package/lib/launcher/internal/runTtscserver.js.map +1 -1
- package/lib/launcher/internal/runTtsx.js +27 -11
- package/lib/launcher/internal/runTtsx.js.map +1 -1
- package/lib/launcher/internal/runtimeHooks.d.ts +81 -0
- package/lib/launcher/internal/runtimeHooks.js +632 -43
- package/lib/launcher/internal/runtimeHooks.js.map +1 -1
- package/lib/launcher/internal/servedSourceMap.d.ts +25 -0
- package/lib/launcher/internal/servedSourceMap.js +164 -0
- package/lib/launcher/internal/servedSourceMap.js.map +1 -0
- package/lib/plugin/internal/buildSourcePlugin.d.ts +146 -2
- package/lib/plugin/internal/buildSourcePlugin.js +651 -113
- package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
- package/lib/plugin/internal/loadProjectPlugins.d.ts +7 -0
- package/lib/plugin/internal/loadProjectPlugins.js +33 -10
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/lib/structures/ITtscCompilerContext.d.ts +16 -2
- package/lib/structures/ITtscPluginCapabilities.d.ts +8 -0
- package/lib/structures/ITtscPluginFactoryContext.d.ts +14 -0
- package/lib/structures/internal/ITtscParsedProjectConfig.d.ts +3 -0
- package/lib/structures/internal/ITtscProjectIdentity.d.ts +17 -0
- package/lib/structures/internal/ITtscProjectIdentity.js +3 -0
- package/lib/structures/internal/ITtscProjectIdentity.js.map +1 -0
- package/lib/structures/internal/TtscCommonOptions.d.ts +8 -0
- package/package.json +11 -8
- package/shim/ast/lint.go +10 -0
- package/shim/ast/shim.go +44 -0
- package/shim/checker/enums_gen.go +19 -0
- package/shim/checker/shim.go +130 -0
- package/shim/diagnosticwriter/lint.go +30 -3
- package/shim/scanner/regexp.go +27 -0
- package/src/TtscCompiler.ts +25 -6
- package/src/TtscService.ts +15 -7
- package/src/compiler/internal/compileProjectInMemory.ts +1 -0
- package/src/compiler/internal/project/createNativeProjectContextArgs.ts +15 -0
- package/src/compiler/internal/project/readProjectConfig.ts +61 -5
- package/src/compiler/internal/project/resolveProjectConfig.ts +51 -21
- package/src/compiler/internal/residentTransformProcess.ts +95 -21
- package/src/compiler/internal/runBuild.ts +76 -9
- package/src/compiler/internal/runSingleFileEmit.ts +7 -4
- package/src/compiler/internal/sharedHostHelpers.ts +24 -0
- package/src/compiler/internal/startResidentTransform.ts +21 -1
- package/src/compiler/internal/transformProjectInMemory.ts +49 -6
- package/src/flags/parser.ts +28 -1
- package/src/flags/schema.ts +8 -0
- package/src/launcher/internal/prepareExecution.ts +22 -2
- package/src/launcher/internal/runTtsc.ts +13 -61
- package/src/launcher/internal/runTtscserver.ts +14 -0
- package/src/launcher/internal/runTtsx.ts +28 -13
- package/src/launcher/internal/runtimeHooks.ts +771 -53
- package/src/launcher/internal/servedSourceMap.ts +183 -0
- package/src/plugin/internal/buildSourcePlugin.ts +859 -122
- package/src/plugin/internal/loadProjectPlugins.ts +36 -6
- package/src/structures/ITtscCompilerContext.ts +17 -2
- package/src/structures/ITtscPluginCapabilities.ts +9 -0
- package/src/structures/ITtscPluginFactoryContext.ts +15 -0
- package/src/structures/internal/ITtscParsedProjectConfig.ts +3 -0
- package/src/structures/internal/ITtscProjectIdentity.ts +17 -0
- package/src/structures/internal/TtscCommonOptions.ts +8 -0
- package/tools/shim_audit/baseline.json +16 -7
- package/tools/shim_audit/main.go +777 -71
- package/tools/shim_audit/main_test.go +28 -1
- package/tools/shim_audit/producer_exemptions_require_rationale_test.go +42 -0
- package/tools/shim_audit/producer_surface_includes_exposed_methods_test.go +154 -0
- package/tools/shim_audit/producer_surface_rejects_unproducible_reference_test.go +142 -0
- package/utility/host.go +46 -20
- package/utility/serve.go +1 -1
- package/shim/checker/signature_api_completeness.go +0 -58
|
@@ -3,9 +3,16 @@ 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.TTSX_MINIMUM_NODE_VERSION = void 0;
|
|
7
|
+
exports.checkNodeRuntimeSupport = checkNodeRuntimeSupport;
|
|
6
8
|
exports.installRuntimeHooks = installRuntimeHooks;
|
|
7
9
|
exports.entryModuleFormat = entryModuleFormat;
|
|
8
10
|
exports.isWithin = isWithin;
|
|
11
|
+
exports.readDependencyCache = readDependencyCache;
|
|
12
|
+
exports.acquireDependencyBuildLock = acquireDependencyBuildLock;
|
|
13
|
+
exports.releaseDependencyBuildLock = releaseDependencyBuildLock;
|
|
14
|
+
exports.reclaimDependencyBuildLock = reclaimDependencyBuildLock;
|
|
15
|
+
exports.inspectDependencyBuildLock = inspectDependencyBuildLock;
|
|
9
16
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
10
17
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
11
18
|
const node_module_1 = require("node:module");
|
|
@@ -17,6 +24,7 @@ const resolveEmittedJavaScript_1 = require("../../compiler/internal/resolveEmitt
|
|
|
17
24
|
const resolveTsgo_1 = require("../../compiler/internal/resolveTsgo");
|
|
18
25
|
const runBuild_1 = require("../../compiler/internal/runBuild");
|
|
19
26
|
const spawnNative_1 = require("../../compiler/internal/spawnNative");
|
|
27
|
+
const servedSourceMap_1 = require("./servedSourceMap");
|
|
20
28
|
/**
|
|
21
29
|
* Synchronous Node module hooks installed (via `module.registerHooks`) in the
|
|
22
30
|
* child process `ttsx` spawns to run a TypeScript entry _from source_.
|
|
@@ -75,6 +83,72 @@ function manifest() {
|
|
|
75
83
|
}
|
|
76
84
|
return manifestCache;
|
|
77
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Lowest Node.js the ttsx source runtime supports. The synchronous
|
|
88
|
+
* `module.registerHooks` (Node 22.15.0) is the highest floor among the runtime
|
|
89
|
+
* APIs these hooks depend on — `stripTypeScriptTypes` (22.13.0) and the child's
|
|
90
|
+
* `--disable-warning` flag (20.11.0) are both lower — so it sets the effective
|
|
91
|
+
* minimum. Kept in sync with `packages/ttsc/package.json#engines.node` and the
|
|
92
|
+
* documented requirement in `website/src/content/docs/development/index.mdx`.
|
|
93
|
+
*/
|
|
94
|
+
exports.TTSX_MINIMUM_NODE_VERSION = "22.15.0";
|
|
95
|
+
const TTSX_MINIMUM_NODE_PARTS = [22, 15, 0];
|
|
96
|
+
/**
|
|
97
|
+
* Report why the running (or a candidate) Node.js version cannot execute the
|
|
98
|
+
* ttsx source runtime, or `null` when it can. Returning an actionable message —
|
|
99
|
+
* rather than letting the child die with an internal `TypeError` on the missing
|
|
100
|
+
* `registerHooks`, or Node 18 rejecting `--disable-warning` with exit 9 — is
|
|
101
|
+
* what turns an opaque internal failure into a clear version diagnostic.
|
|
102
|
+
*
|
|
103
|
+
* Exported for direct exercise by the ttsx e2e suite: the built launcher can
|
|
104
|
+
* only be spawned under the Node version running the tests, so the boundary
|
|
105
|
+
* around the floor cannot otherwise be pinned on CI.
|
|
106
|
+
*/
|
|
107
|
+
function checkNodeRuntimeSupport(version) {
|
|
108
|
+
const parts = parseNodeVersion(version);
|
|
109
|
+
if (parts === null) {
|
|
110
|
+
// An unrecognizable version string is not proof of an unsupported runtime;
|
|
111
|
+
// let execution proceed rather than block on a parsing quirk.
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
if (compareVersionParts(parts, TTSX_MINIMUM_NODE_PARTS) >= 0) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
return (`ttsx requires Node.js ${exports.TTSX_MINIMUM_NODE_VERSION} or later, but this ` +
|
|
118
|
+
`process is Node.js ${version}. The source runtime installs synchronous ` +
|
|
119
|
+
`module hooks (module.registerHooks, Node 22.15.0) and strips types with ` +
|
|
120
|
+
`module.stripTypeScriptTypes (Node 22.13.0), neither of which exists on ` +
|
|
121
|
+
`earlier releases. Upgrade Node.js to 22.15.0+ (or the current LTS), or ` +
|
|
122
|
+
`compile the project with \`ttsc\` and run the emitted JavaScript directly.`);
|
|
123
|
+
}
|
|
124
|
+
function parseNodeVersion(version) {
|
|
125
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(version.trim());
|
|
126
|
+
if (match === null) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
130
|
+
}
|
|
131
|
+
function compareVersionParts(a, b) {
|
|
132
|
+
for (let index = 0; index < 3; index += 1) {
|
|
133
|
+
if (a[index] !== b[index]) {
|
|
134
|
+
return a[index] < b[index] ? -1 : 1;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Throw an actionable version error when the current Node.js cannot run the
|
|
141
|
+
* ttsx source runtime. Guards the hook-installation boundary directly (a child
|
|
142
|
+
* or grandchild that inherits the runtime preload under an unsupported Node) so
|
|
143
|
+
* the failure is diagnosed here instead of surfacing as a bare `TypeError:
|
|
144
|
+
* registerHooks is not a function`.
|
|
145
|
+
*/
|
|
146
|
+
function assertNodeRuntimeSupport() {
|
|
147
|
+
const message = checkNodeRuntimeSupport(process.versions.node);
|
|
148
|
+
if (message !== null) {
|
|
149
|
+
throw new Error(message);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
78
152
|
let installed = false;
|
|
79
153
|
/**
|
|
80
154
|
* Install the source-loading hooks on the current (main) thread. Idempotent:
|
|
@@ -93,7 +167,15 @@ function installRuntimeHooks() {
|
|
|
93
167
|
if (installed) {
|
|
94
168
|
return;
|
|
95
169
|
}
|
|
170
|
+
assertNodeRuntimeSupport();
|
|
96
171
|
installed = true;
|
|
172
|
+
// Map error stacks through the source maps the serve path now inlines, so a
|
|
173
|
+
// thrown frame reports the true `.ts` line:col out of the box (no
|
|
174
|
+
// `--enable-source-maps` needed). Applied before the entry loads; user code
|
|
175
|
+
// that later toggles it wins, since this is a plain runtime switch.
|
|
176
|
+
if (typeof process.setSourceMapsEnabled === "function") {
|
|
177
|
+
process.setSourceMapsEnabled(true);
|
|
178
|
+
}
|
|
97
179
|
(0, node_module_1.registerHooks)({ load, resolve });
|
|
98
180
|
installCommonJsHook();
|
|
99
181
|
}
|
|
@@ -233,11 +315,14 @@ function resolveServedSource(filename, url = (0, node_url_1.pathToFileURL)(filen
|
|
|
233
315
|
const real = realPath(filename);
|
|
234
316
|
const served = serveEntryEmit(real);
|
|
235
317
|
if (served !== null) {
|
|
236
|
-
return {
|
|
318
|
+
return withInlineSourceMap({
|
|
319
|
+
moduleOption: manifest()?.moduleOption,
|
|
320
|
+
...served,
|
|
321
|
+
});
|
|
237
322
|
}
|
|
238
323
|
const built = serveDependencyEmit(real);
|
|
239
324
|
if (built !== null) {
|
|
240
|
-
return built;
|
|
325
|
+
return withInlineSourceMap(built);
|
|
241
326
|
}
|
|
242
327
|
return {
|
|
243
328
|
moduleOption: undefined,
|
|
@@ -245,6 +330,17 @@ function resolveServedSource(filename, url = (0, node_url_1.pathToFileURL)(filen
|
|
|
245
330
|
source: transformOrphanSource(filename, url),
|
|
246
331
|
};
|
|
247
332
|
}
|
|
333
|
+
/**
|
|
334
|
+
* Inline a served emit's external source map into its text and absolutize the
|
|
335
|
+
* map's `sources`, so the JavaScript executed under the `.ts` source URL stays
|
|
336
|
+
* self-describing after the per-run emit directory is deleted. Applied to both
|
|
337
|
+
* the entry lane (`serveEntryEmit`) and the dependency lane
|
|
338
|
+
* (`serveBuiltDependency`); the orphan type-strip lane carries no emitted map.
|
|
339
|
+
*/
|
|
340
|
+
function withInlineSourceMap(served) {
|
|
341
|
+
const source = (0, servedSourceMap_1.inlineServedSourceMap)(served.source, served.emittedFile, served.sourceFile);
|
|
342
|
+
return source === served.source ? served : { ...served, source };
|
|
343
|
+
}
|
|
248
344
|
/**
|
|
249
345
|
* Transform a TypeScript source file that no tsconfig owns (a published or
|
|
250
346
|
* vendored package that ships raw `.ts`/`.cts`/`.mts` straight under
|
|
@@ -551,14 +647,152 @@ function collectCommonJsExportNames(emittedFile, seen) {
|
|
|
551
647
|
return names;
|
|
552
648
|
}
|
|
553
649
|
function collectStaticCommonJsExportNames(source) {
|
|
650
|
+
// Scan executable syntax only. Text that merely resembles an assignment —
|
|
651
|
+
// `exports.x =` inside a comment, string, or template-literal text — must not
|
|
652
|
+
// become an ESM-visible export name, or a named import of it would link to
|
|
653
|
+
// `undefined` for a property the CommonJS module never defines. Masking the
|
|
654
|
+
// inert lexical spans before matching keeps genuine top-level assignments
|
|
655
|
+
// (and executable `${ ... }` template substitutions) while dropping the
|
|
656
|
+
// decoys.
|
|
657
|
+
const scannable = maskCommentsAndStrings(source);
|
|
554
658
|
const names = new Set();
|
|
555
659
|
const pattern = /(?:^|[^\w$])(?:exports|module\.exports)\.([A-Za-z_$][\w$]*)\s*=/g;
|
|
556
660
|
let match;
|
|
557
|
-
while ((match = pattern.exec(
|
|
661
|
+
while ((match = pattern.exec(scannable)) !== null) {
|
|
558
662
|
names.add(match[1]);
|
|
559
663
|
}
|
|
560
664
|
return names;
|
|
561
665
|
}
|
|
666
|
+
/**
|
|
667
|
+
* Blank out the interior of line comments, block comments, string literals, and
|
|
668
|
+
* template-literal text in emitted JavaScript, replacing each masked character
|
|
669
|
+
* with a space while preserving newlines, code, and template `${ ... }`
|
|
670
|
+
* substitutions verbatim. Positions and length are preserved so an offset in
|
|
671
|
+
* the masked text maps back to the same offset in the source.
|
|
672
|
+
*
|
|
673
|
+
* The input is tsgo's CommonJS emit (well-formed JavaScript), so a character
|
|
674
|
+
* scanner that tracks the standard comment/string/template states is sufficient
|
|
675
|
+
* to separate executable tokens from inert text. Regular-expression literals
|
|
676
|
+
* are intentionally not masked: distinguishing `/`-division from a regex
|
|
677
|
+
* literal needs full tokenization, and tsgo's CommonJS emit never wraps an
|
|
678
|
+
* `exports.<name> =` assignment inside a regex literal.
|
|
679
|
+
*/
|
|
680
|
+
function maskCommentsAndStrings(source) {
|
|
681
|
+
const out = source.split("");
|
|
682
|
+
const n = out.length;
|
|
683
|
+
const blank = (index) => {
|
|
684
|
+
const ch = out[index];
|
|
685
|
+
if (ch !== "\n" && ch !== "\r") {
|
|
686
|
+
out[index] = " ";
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
const stack = [{ kind: "code", braceDepth: 0 }];
|
|
690
|
+
let i = 0;
|
|
691
|
+
while (i < n) {
|
|
692
|
+
const top = stack[stack.length - 1];
|
|
693
|
+
const ch = out[i];
|
|
694
|
+
if (top.kind === "template") {
|
|
695
|
+
if (ch === "\\") {
|
|
696
|
+
blank(i);
|
|
697
|
+
blank(i + 1);
|
|
698
|
+
i += 2;
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
if (ch === "`") {
|
|
702
|
+
blank(i);
|
|
703
|
+
stack.pop();
|
|
704
|
+
i += 1;
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
if (ch === "$" && out[i + 1] === "{") {
|
|
708
|
+
// Enter a code substitution: `${` and its contents stay executable.
|
|
709
|
+
stack.push({ kind: "code", braceDepth: 0 });
|
|
710
|
+
i += 2;
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
blank(i);
|
|
714
|
+
i += 1;
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
// Code context.
|
|
718
|
+
if (ch === "/" && out[i + 1] === "/") {
|
|
719
|
+
blank(i);
|
|
720
|
+
blank(i + 1);
|
|
721
|
+
i += 2;
|
|
722
|
+
while (i < n && out[i] !== "\n") {
|
|
723
|
+
blank(i);
|
|
724
|
+
i += 1;
|
|
725
|
+
}
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
if (ch === "/" && out[i + 1] === "*") {
|
|
729
|
+
blank(i);
|
|
730
|
+
blank(i + 1);
|
|
731
|
+
i += 2;
|
|
732
|
+
while (i < n && !(out[i] === "*" && out[i + 1] === "/")) {
|
|
733
|
+
blank(i);
|
|
734
|
+
i += 1;
|
|
735
|
+
}
|
|
736
|
+
if (i < n) {
|
|
737
|
+
blank(i);
|
|
738
|
+
blank(i + 1);
|
|
739
|
+
i += 2;
|
|
740
|
+
}
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
if (ch === '"' || ch === "'") {
|
|
744
|
+
blank(i);
|
|
745
|
+
i += 1;
|
|
746
|
+
while (i < n && out[i] !== ch) {
|
|
747
|
+
if (out[i] === "\\") {
|
|
748
|
+
blank(i);
|
|
749
|
+
blank(i + 1);
|
|
750
|
+
i += 2;
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
// A bare newline ends an unterminated string; stop masking so the rest
|
|
754
|
+
// of the line is still scanned as code (defensive — tsgo never emits
|
|
755
|
+
// one).
|
|
756
|
+
if (out[i] === "\n") {
|
|
757
|
+
break;
|
|
758
|
+
}
|
|
759
|
+
blank(i);
|
|
760
|
+
i += 1;
|
|
761
|
+
}
|
|
762
|
+
if (i < n && out[i] === ch) {
|
|
763
|
+
blank(i);
|
|
764
|
+
i += 1;
|
|
765
|
+
}
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
if (ch === "`") {
|
|
769
|
+
blank(i);
|
|
770
|
+
stack.push({ kind: "template", braceDepth: 0 });
|
|
771
|
+
i += 1;
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
if (ch === "{") {
|
|
775
|
+
top.braceDepth += 1;
|
|
776
|
+
i += 1;
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
if (ch === "}") {
|
|
780
|
+
if (top.braceDepth === 0 && stack.length > 1) {
|
|
781
|
+
// Close the enclosing template `${ ... }` and resume template text.
|
|
782
|
+
stack.pop();
|
|
783
|
+
i += 1;
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
if (top.braceDepth > 0) {
|
|
787
|
+
top.braceDepth -= 1;
|
|
788
|
+
}
|
|
789
|
+
i += 1;
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
i += 1;
|
|
793
|
+
}
|
|
794
|
+
return out.join("");
|
|
795
|
+
}
|
|
562
796
|
function collectSourceCommonJsExportNames(sourceFile, seen) {
|
|
563
797
|
const real = realPath(sourceFile);
|
|
564
798
|
if (seen.has(real)) {
|
|
@@ -761,14 +995,14 @@ function ensureProjectBuilt(tsconfig) {
|
|
|
761
995
|
if (cached !== undefined) {
|
|
762
996
|
return cached;
|
|
763
997
|
}
|
|
764
|
-
const {
|
|
765
|
-
const reuse = readDependencyCache(
|
|
998
|
+
const { cacheDir, lockDir, metaPath, root } = dependencyCachePaths(tsconfig);
|
|
999
|
+
const reuse = readDependencyCache(cacheDir, metaPath);
|
|
766
1000
|
if (reuse !== null) {
|
|
767
1001
|
builtProjects.set(tsconfig, reuse);
|
|
768
1002
|
return reuse;
|
|
769
1003
|
}
|
|
770
1004
|
node_fs_1.default.mkdirSync(root, { recursive: true });
|
|
771
|
-
const built = withBuildLock(
|
|
1005
|
+
const built = withBuildLock(cacheDir, metaPath, lockDir, () => buildDependency(tsconfig, cacheDir, metaPath));
|
|
772
1006
|
builtProjects.set(tsconfig, built);
|
|
773
1007
|
return built;
|
|
774
1008
|
}
|
|
@@ -780,14 +1014,38 @@ function dependencyCachePaths(tsconfig) {
|
|
|
780
1014
|
.slice(0, 16);
|
|
781
1015
|
const root = dependencyCacheRoot();
|
|
782
1016
|
return {
|
|
783
|
-
|
|
1017
|
+
cacheDir: node_path_1.default.join(root, key),
|
|
784
1018
|
lockDir: node_path_1.default.join(root, `${key}.lock`),
|
|
785
1019
|
metaPath: node_path_1.default.join(root, `${key}.json`),
|
|
786
1020
|
root,
|
|
787
1021
|
};
|
|
788
1022
|
}
|
|
789
|
-
/**
|
|
790
|
-
function
|
|
1023
|
+
/** The immutable emit directory of one build generation under `cacheDir`. */
|
|
1024
|
+
function dependencyGenerationDir(cacheDir, generation) {
|
|
1025
|
+
return node_path_1.default.join(cacheDir, `gen-${generation}`);
|
|
1026
|
+
}
|
|
1027
|
+
/** A fresh 128-bit build-generation identifier. */
|
|
1028
|
+
function newDependencyGeneration() {
|
|
1029
|
+
return node_crypto_1.default.randomBytes(16).toString("hex");
|
|
1030
|
+
}
|
|
1031
|
+
/** True for a well-formed 128-bit hex build generation. */
|
|
1032
|
+
function isDependencyGeneration(value) {
|
|
1033
|
+
return typeof value === "string" && /^[0-9a-f]{32}$/.test(value);
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Reuse a dependency another process (or an earlier import) already built.
|
|
1037
|
+
*
|
|
1038
|
+
* The completion marker names the exact emit generation, so this reads metadata
|
|
1039
|
+
* and emit as one unit: it returns a hit only when the marker parses to a valid
|
|
1040
|
+
* generation AND that generation's directory holds emitted JavaScript. A reader
|
|
1041
|
+
* that runs while a replacement build is populating a DIFFERENT generation
|
|
1042
|
+
* directory keeps returning the previous complete generation until the atomic
|
|
1043
|
+
* marker swap points at the new one — never a mix of old metadata and a partial
|
|
1044
|
+
* new emit.
|
|
1045
|
+
*
|
|
1046
|
+
* Exported for the ttsx dependency-cache regressions.
|
|
1047
|
+
*/
|
|
1048
|
+
function readDependencyCache(cacheDir, metaPath) {
|
|
791
1049
|
let meta;
|
|
792
1050
|
try {
|
|
793
1051
|
meta = JSON.parse(node_fs_1.default.readFileSync(metaPath, "utf8"));
|
|
@@ -795,73 +1053,102 @@ function readDependencyCache(emitDir, metaPath) {
|
|
|
795
1053
|
catch {
|
|
796
1054
|
return null;
|
|
797
1055
|
}
|
|
1056
|
+
if (!isDependencyGeneration(meta.generation) ||
|
|
1057
|
+
typeof meta.rootDir !== "string") {
|
|
1058
|
+
return null;
|
|
1059
|
+
}
|
|
1060
|
+
const emitDir = dependencyGenerationDir(cacheDir, meta.generation);
|
|
798
1061
|
if (!emittedAnything(emitDir)) {
|
|
799
1062
|
return null;
|
|
800
1063
|
}
|
|
801
1064
|
return {
|
|
802
1065
|
emitDir,
|
|
803
1066
|
emittedFiles: undefined,
|
|
804
|
-
moduleOption: meta.moduleOption,
|
|
1067
|
+
moduleOption: typeof meta.moduleOption === "string" ? meta.moduleOption : undefined,
|
|
805
1068
|
rootDir: meta.rootDir,
|
|
806
1069
|
};
|
|
807
1070
|
}
|
|
808
1071
|
/**
|
|
809
|
-
* Run `build` while holding
|
|
1072
|
+
* Run `build` while holding the fenced lock for this dependency, re-checking
|
|
810
1073
|
* the cache once the lock is held (a concurrent builder may have just
|
|
811
|
-
* finished). A
|
|
812
|
-
*
|
|
813
|
-
* never
|
|
1074
|
+
* finished). A loser polls for the winner's completion marker and, only when
|
|
1075
|
+
* the holding generation is provably abandoned (dead owner or the steal budget
|
|
1076
|
+
* elapsed), retires precisely that generation before retrying — never a
|
|
1077
|
+
* successor's.
|
|
814
1078
|
*/
|
|
815
|
-
function withBuildLock(
|
|
816
|
-
const stealAfterMs = 600_000;
|
|
1079
|
+
function withBuildLock(cacheDir, metaPath, lockDir, build) {
|
|
817
1080
|
for (;;) {
|
|
1081
|
+
const reuse = readDependencyCache(cacheDir, metaPath);
|
|
1082
|
+
if (reuse !== null) {
|
|
1083
|
+
return reuse;
|
|
1084
|
+
}
|
|
1085
|
+
let lease;
|
|
818
1086
|
try {
|
|
819
|
-
|
|
1087
|
+
lease = acquireDependencyBuildLock(lockDir);
|
|
820
1088
|
}
|
|
821
1089
|
catch {
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
1090
|
+
// An unusable coordination directory must not silently skip the build.
|
|
1091
|
+
// Generation-stamped emit and the atomic marker swap still keep every
|
|
1092
|
+
// reader's view of publication consistent without the lock.
|
|
1093
|
+
return build();
|
|
1094
|
+
}
|
|
1095
|
+
if (lease === null) {
|
|
1096
|
+
const waited = waitForDependencyBuild(cacheDir, metaPath, lockDir, DEP_BUILD_LOCK_STEAL_MS);
|
|
1097
|
+
if (waited.outcome === "built") {
|
|
1098
|
+
return waited.built;
|
|
1099
|
+
}
|
|
1100
|
+
if (waited.outcome === "abandoned") {
|
|
1101
|
+
// Retire only the generation this observation named. Losing the rename
|
|
1102
|
+
// race means the holder's own release (or another waiter) already made
|
|
1103
|
+
// progress, so a stale result never removes a live successor.
|
|
1104
|
+
reclaimDependencyBuildLock(lockDir, waited.fence);
|
|
825
1105
|
}
|
|
826
|
-
|
|
1106
|
+
// "released" needs no repair: the holder freed the lock normally, so
|
|
1107
|
+
// retry the ordinary acquisition.
|
|
827
1108
|
continue;
|
|
828
1109
|
}
|
|
829
1110
|
try {
|
|
830
|
-
const
|
|
831
|
-
return
|
|
1111
|
+
const reuseUnderLock = readDependencyCache(cacheDir, metaPath);
|
|
1112
|
+
return reuseUnderLock ?? build();
|
|
832
1113
|
}
|
|
833
1114
|
finally {
|
|
834
|
-
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
/** Poll for a concurrent builder's completion, up to `timeoutMs`. */
|
|
839
|
-
function waitForDependencyCache(emitDir, metaPath, timeoutMs) {
|
|
840
|
-
const startedAt = Date.now();
|
|
841
|
-
for (;;) {
|
|
842
|
-
const reuse = readDependencyCache(emitDir, metaPath);
|
|
843
|
-
if (reuse !== null) {
|
|
844
|
-
return reuse;
|
|
845
|
-
}
|
|
846
|
-
if (Date.now() - startedAt > timeoutMs) {
|
|
847
|
-
return null;
|
|
1115
|
+
releaseDependencyBuildLock(lockDir, lease);
|
|
848
1116
|
}
|
|
849
|
-
sleepSync(50);
|
|
850
1117
|
}
|
|
851
1118
|
}
|
|
852
1119
|
/** Block the current (synchronous) thread for `ms` without busy-spinning. */
|
|
853
1120
|
function sleepSync(ms) {
|
|
854
1121
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
855
1122
|
}
|
|
856
|
-
/**
|
|
857
|
-
|
|
1123
|
+
/**
|
|
1124
|
+
* Compile a dependency project into a fresh generation directory and publish
|
|
1125
|
+
* its completion marker atomically.
|
|
1126
|
+
*
|
|
1127
|
+
* The emit lands in `<cacheDir>/gen-<generation>`, a directory no other
|
|
1128
|
+
* generation or process shares, so it is never mutated in place while a reader
|
|
1129
|
+
* looks at it. Only after the emit is proven non-empty is the marker written by
|
|
1130
|
+
* temp-and-rename, binding metadata to that exact generation. A build that
|
|
1131
|
+
* produced no output drops its partial directory so a failed generation can
|
|
1132
|
+
* never be reused.
|
|
1133
|
+
*/
|
|
1134
|
+
function buildDependency(tsconfig, cacheDir, metaPath) {
|
|
858
1135
|
const project = (0, readProjectConfig_1.readProjectConfig)({ cwd: node_path_1.default.dirname(tsconfig), tsconfig });
|
|
1136
|
+
const generation = newDependencyGeneration();
|
|
1137
|
+
const emitDir = dependencyGenerationDir(cacheDir, generation);
|
|
859
1138
|
node_fs_1.default.rmSync(emitDir, { force: true, recursive: true });
|
|
1139
|
+
node_fs_1.default.mkdirSync(emitDir, { recursive: true });
|
|
860
1140
|
const result = (0, runBuild_1.runBuild)({
|
|
861
1141
|
cwd: project.root,
|
|
862
1142
|
emit: true,
|
|
863
1143
|
forceListEmittedFiles: true,
|
|
864
1144
|
outDir: emitDir,
|
|
1145
|
+
// Emit a source map on the transient dependency emit (it never reaches the
|
|
1146
|
+
// dependency's published `lib/`) so the serve path can inline it under the
|
|
1147
|
+
// source URL, but only when the dependency configures none itself. Routed
|
|
1148
|
+
// as a dedicated build option, not a forwarded tsgo flag, so it never
|
|
1149
|
+
// reaches a native plugin host's argument parser (issue #353).
|
|
1150
|
+
forceRuntimeSourceMap: project.compilerOptions.sourceMap !== true &&
|
|
1151
|
+
project.compilerOptions.inlineSourceMap !== true,
|
|
865
1152
|
// Honour the dependency's own transform plugins: a source-shipping package
|
|
866
1153
|
// can itself depend on a transform (e.g. a fixture whose values are built
|
|
867
1154
|
// with `typia.createRandom`), and its runtime behaviour is wrong without it.
|
|
@@ -872,6 +1159,7 @@ function buildDependency(tsconfig, emitDir, metaPath) {
|
|
|
872
1159
|
// deadlocks, so every dependency in that graph builds with plugins off.
|
|
873
1160
|
plugins: process.env.TTSC_PLUGIN_DESCRIPTOR_LOAD === "1" ? false : undefined,
|
|
874
1161
|
quiet: true,
|
|
1162
|
+
resolvedProject: project,
|
|
875
1163
|
// Emit only: the entry project's up-front check is the type gate. A
|
|
876
1164
|
// dependency build pulls its own transitive sources into the program and
|
|
877
1165
|
// would otherwise fail on type diagnostics that belong to those packages
|
|
@@ -887,6 +1175,9 @@ function buildDependency(tsconfig, emitDir, metaPath) {
|
|
|
887
1175
|
// even on a clean build. A genuinely empty output directory is the real
|
|
888
1176
|
// failure; the caller then falls back to type-stripping the one file.
|
|
889
1177
|
if (!emittedAnything(emitDir)) {
|
|
1178
|
+
// Drop the failed generation so its partial directory can never be mistaken
|
|
1179
|
+
// for a reusable build.
|
|
1180
|
+
node_fs_1.default.rmSync(emitDir, { force: true, recursive: true });
|
|
890
1181
|
throw new Error([
|
|
891
1182
|
`ttsx: dependency build produced no output for ${tsconfig}`,
|
|
892
1183
|
result.stderr || result.stdout,
|
|
@@ -900,11 +1191,30 @@ function buildDependency(tsconfig, emitDir, metaPath) {
|
|
|
900
1191
|
const moduleOption = typeof project.compilerOptions.module === "string"
|
|
901
1192
|
? project.compilerOptions.module
|
|
902
1193
|
: undefined;
|
|
903
|
-
|
|
1194
|
+
publishDependencyMeta(metaPath, { generation, moduleOption, rootDir });
|
|
904
1195
|
return { emitDir, emittedFiles: undefined, moduleOption, rootDir };
|
|
905
1196
|
}
|
|
906
|
-
|
|
907
|
-
|
|
1197
|
+
/**
|
|
1198
|
+
* Publish the completion marker atomically: write it to a private temp name in
|
|
1199
|
+
* the same directory, then rename onto `metaPath`. Node's rename replaces an
|
|
1200
|
+
* existing file atomically on POSIX and Windows alike, so a concurrent reader
|
|
1201
|
+
* sees either the whole previous marker or the whole new one, never a
|
|
1202
|
+
* half-written file. The marker is the LAST artifact a build writes, after its
|
|
1203
|
+
* generation's emit is complete, so observing the new marker guarantees the new
|
|
1204
|
+
* generation is complete.
|
|
1205
|
+
*/
|
|
1206
|
+
function publishDependencyMeta(metaPath, meta) {
|
|
1207
|
+
const tmp = `${metaPath}.${process.pid}.${Date.now()}.${node_crypto_1.default
|
|
1208
|
+
.randomBytes(6)
|
|
1209
|
+
.toString("hex")}.tmp`;
|
|
1210
|
+
node_fs_1.default.writeFileSync(tmp, JSON.stringify(meta), "utf8");
|
|
1211
|
+
try {
|
|
1212
|
+
node_fs_1.default.renameSync(tmp, metaPath);
|
|
1213
|
+
}
|
|
1214
|
+
catch (error) {
|
|
1215
|
+
node_fs_1.default.rmSync(tmp, { force: true });
|
|
1216
|
+
throw error;
|
|
1217
|
+
}
|
|
908
1218
|
}
|
|
909
1219
|
function dependencyCacheRoot() {
|
|
910
1220
|
const m = manifest();
|
|
@@ -912,6 +1222,285 @@ function dependencyCacheRoot() {
|
|
|
912
1222
|
? m.depCacheDir
|
|
913
1223
|
: node_path_1.default.join(node_os_1.default.tmpdir(), "ttsx-dep");
|
|
914
1224
|
}
|
|
1225
|
+
// -----------------------------------------------------------------------------
|
|
1226
|
+
// Fenced dependency-build lock.
|
|
1227
|
+
//
|
|
1228
|
+
// The lock serialises concurrent first-builders of one dependency so the
|
|
1229
|
+
// expensive `runBuild` runs once per key while the rest wait and reuse the
|
|
1230
|
+
// published generation. It is generation-fenced so a stale observer or a former
|
|
1231
|
+
// owner can never release a successor's lock:
|
|
1232
|
+
//
|
|
1233
|
+
// * `<lockDir>/current` is the held generation — a directory carrying a
|
|
1234
|
+
// `generation` id and an `owner.json` (pid + hostname). A contender writes a
|
|
1235
|
+
// private candidate and atomically renames it onto `current`; a directory
|
|
1236
|
+
// rename cannot replace a non-empty `current`, so exactly one contender wins
|
|
1237
|
+
// with no empty-owner publication window.
|
|
1238
|
+
// * Release and reclaim both retire a generation by renaming `current` to its
|
|
1239
|
+
// deterministic tombstone `<lockDir>/retired/<generation>`. The only way to
|
|
1240
|
+
// free `current` is to create that tombstone, so a successor can acquire only
|
|
1241
|
+
// after its predecessor's tombstone exists. A late or duplicate retire of an
|
|
1242
|
+
// already-retired generation therefore finds the tombstone occupied and
|
|
1243
|
+
// fails atomically, and a reclaim that named an old generation can never move
|
|
1244
|
+
// a different successor into that old tombstone.
|
|
1245
|
+
//
|
|
1246
|
+
// This mirrors the source-plugin v2 protocol (`buildSourcePlugin.ts`) proven by
|
|
1247
|
+
// issue #452 / PR #460, minus the legacy-compatibility layer: the ttsx
|
|
1248
|
+
// dependency cache lives under a per-run directory with no shipped on-disk
|
|
1249
|
+
// format to stay compatible with.
|
|
1250
|
+
// -----------------------------------------------------------------------------
|
|
1251
|
+
const DEP_BUILD_LOCK_STEAL_MS = 600_000;
|
|
1252
|
+
const DEP_BUILD_LOCK_POLL_MS = 50;
|
|
1253
|
+
const DEP_BUILD_LOCK_STALE_MS = 30_000;
|
|
1254
|
+
const DEP_BUILD_LOCK_CURRENT_DIR = "current";
|
|
1255
|
+
const DEP_BUILD_LOCK_RETIRED_DIR = "retired";
|
|
1256
|
+
const DEP_BUILD_LOCK_GENERATION_FILE = "generation";
|
|
1257
|
+
const DEP_BUILD_LOCK_OWNER_FILE = "owner.json";
|
|
1258
|
+
/**
|
|
1259
|
+
* Atomically acquire the current generation of a dependency build lock, or
|
|
1260
|
+
* `null` when another process already holds it. Exported for the deterministic
|
|
1261
|
+
* multi-process cache regressions.
|
|
1262
|
+
*/
|
|
1263
|
+
function acquireDependencyBuildLock(lockDir) {
|
|
1264
|
+
ensureDependencyLockRoot(lockDir);
|
|
1265
|
+
const generation = newDependencyGeneration();
|
|
1266
|
+
const candidateDir = node_path_1.default.join(lockDir, `candidate-${generation}`);
|
|
1267
|
+
node_fs_1.default.mkdirSync(candidateDir);
|
|
1268
|
+
try {
|
|
1269
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(candidateDir, DEP_BUILD_LOCK_GENERATION_FILE), `${generation}\n`, { encoding: "utf8", flag: "wx" });
|
|
1270
|
+
writeDependencyLockOwner(candidateDir, generation);
|
|
1271
|
+
const currentDir = node_path_1.default.join(lockDir, DEP_BUILD_LOCK_CURRENT_DIR);
|
|
1272
|
+
try {
|
|
1273
|
+
node_fs_1.default.renameSync(candidateDir, currentDir);
|
|
1274
|
+
}
|
|
1275
|
+
catch (error) {
|
|
1276
|
+
if (isMissingPathError(error) ||
|
|
1277
|
+
isRenameDestinationOccupied(error, currentDir)) {
|
|
1278
|
+
return null;
|
|
1279
|
+
}
|
|
1280
|
+
throw error;
|
|
1281
|
+
}
|
|
1282
|
+
return { generation };
|
|
1283
|
+
}
|
|
1284
|
+
finally {
|
|
1285
|
+
// The candidate name carries this process's random generation and can never
|
|
1286
|
+
// alias `current` or another contender's candidate.
|
|
1287
|
+
node_fs_1.default.rmSync(candidateDir, { force: true, recursive: true });
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
/** Retire a held generation during the holder's `finally`. */
|
|
1291
|
+
function releaseDependencyBuildLock(lockDir, lease) {
|
|
1292
|
+
return retireDependencyBuildLock(lockDir, lease.generation);
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Retire exactly the generation carried by an abandoned observation. Exported
|
|
1296
|
+
* for the deterministic multi-process cache regressions.
|
|
1297
|
+
*/
|
|
1298
|
+
function reclaimDependencyBuildLock(lockDir, fence) {
|
|
1299
|
+
return retireDependencyBuildLock(lockDir, fence.generation);
|
|
1300
|
+
}
|
|
1301
|
+
function retireDependencyBuildLock(lockDir, generation) {
|
|
1302
|
+
if (!isDependencyGeneration(generation)) {
|
|
1303
|
+
return false;
|
|
1304
|
+
}
|
|
1305
|
+
const retiredDir = node_path_1.default.join(lockDir, DEP_BUILD_LOCK_RETIRED_DIR);
|
|
1306
|
+
try {
|
|
1307
|
+
node_fs_1.default.mkdirSync(retiredDir);
|
|
1308
|
+
}
|
|
1309
|
+
catch (error) {
|
|
1310
|
+
if (error.code !== "EEXIST") {
|
|
1311
|
+
if (isMissingPathError(error)) {
|
|
1312
|
+
return false;
|
|
1313
|
+
}
|
|
1314
|
+
throw error;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
const destination = node_path_1.default.join(retiredDir, generation);
|
|
1318
|
+
try {
|
|
1319
|
+
node_fs_1.default.renameSync(node_path_1.default.join(lockDir, DEP_BUILD_LOCK_CURRENT_DIR), destination);
|
|
1320
|
+
return true;
|
|
1321
|
+
}
|
|
1322
|
+
catch (error) {
|
|
1323
|
+
if (isMissingPathError(error) ||
|
|
1324
|
+
isRenameDestinationOccupied(error, destination)) {
|
|
1325
|
+
return false;
|
|
1326
|
+
}
|
|
1327
|
+
throw error;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Classify the current state of a dependency build lock directory. Exported for
|
|
1332
|
+
* the deterministic multi-process cache regressions.
|
|
1333
|
+
*/
|
|
1334
|
+
function inspectDependencyBuildLock(lockDir, now) {
|
|
1335
|
+
const currentDir = node_path_1.default.join(lockDir, DEP_BUILD_LOCK_CURRENT_DIR);
|
|
1336
|
+
const generation = readDependencyLockGeneration(currentDir);
|
|
1337
|
+
if (generation === null) {
|
|
1338
|
+
if (dependencyLockAgeMs(currentDir, now) === null) {
|
|
1339
|
+
return { state: "released" };
|
|
1340
|
+
}
|
|
1341
|
+
// `current` exists without a readable generation id. Acquisition writes the
|
|
1342
|
+
// id into the candidate BEFORE the atomic rename, so this is a transient or
|
|
1343
|
+
// corrupt state; keep waiting rather than steal on ambiguous evidence.
|
|
1344
|
+
return {
|
|
1345
|
+
state: "active",
|
|
1346
|
+
owner: "lock generation without a readable id",
|
|
1347
|
+
fence: { generation: "" },
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
const fence = { generation };
|
|
1351
|
+
const owner = readDependencyLockOwner(currentDir);
|
|
1352
|
+
if (owner !== null) {
|
|
1353
|
+
const label = describeDependencyLockOwner(owner);
|
|
1354
|
+
if (isLocalHostName(owner.hostname) && !isProcessAlive(owner.pid)) {
|
|
1355
|
+
return {
|
|
1356
|
+
state: "abandoned",
|
|
1357
|
+
reason: `${label} is no longer running`,
|
|
1358
|
+
fence,
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
return { state: "active", owner: label, fence };
|
|
1362
|
+
}
|
|
1363
|
+
const ageMs = dependencyLockAgeMs(currentDir, now);
|
|
1364
|
+
if (ageMs === null) {
|
|
1365
|
+
return { state: "released" };
|
|
1366
|
+
}
|
|
1367
|
+
if (ageMs > DEP_BUILD_LOCK_STALE_MS) {
|
|
1368
|
+
return {
|
|
1369
|
+
state: "abandoned",
|
|
1370
|
+
reason: `lock generation has no ${DEP_BUILD_LOCK_OWNER_FILE} and is ${formatDuration(ageMs)} old`,
|
|
1371
|
+
fence,
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
return {
|
|
1375
|
+
state: "active",
|
|
1376
|
+
owner: `lock generation with no ${DEP_BUILD_LOCK_OWNER_FILE}`,
|
|
1377
|
+
fence,
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
/** Poll for the locked builder to publish, up to `timeoutMs`. */
|
|
1381
|
+
function waitForDependencyBuild(cacheDir, metaPath, lockDir, timeoutMs) {
|
|
1382
|
+
const startedAt = Date.now();
|
|
1383
|
+
for (;;) {
|
|
1384
|
+
const reuse = readDependencyCache(cacheDir, metaPath);
|
|
1385
|
+
if (reuse !== null) {
|
|
1386
|
+
return { outcome: "built", built: reuse };
|
|
1387
|
+
}
|
|
1388
|
+
const now = Date.now();
|
|
1389
|
+
const lock = inspectDependencyBuildLock(lockDir, now);
|
|
1390
|
+
if (lock.state === "released") {
|
|
1391
|
+
// The holder retired its generation between the cache check above and this
|
|
1392
|
+
// observation: prefer the marker if it landed in that window, otherwise
|
|
1393
|
+
// hand the free lock back to the caller to re-acquire.
|
|
1394
|
+
const built = readDependencyCache(cacheDir, metaPath);
|
|
1395
|
+
return built !== null
|
|
1396
|
+
? { outcome: "built", built }
|
|
1397
|
+
: { outcome: "released" };
|
|
1398
|
+
}
|
|
1399
|
+
if (lock.state === "abandoned") {
|
|
1400
|
+
return { outcome: "abandoned", reason: lock.reason, fence: lock.fence };
|
|
1401
|
+
}
|
|
1402
|
+
if (now - startedAt > timeoutMs) {
|
|
1403
|
+
return {
|
|
1404
|
+
outcome: "abandoned",
|
|
1405
|
+
reason: `timed out after ${formatDuration(now - startedAt)}`,
|
|
1406
|
+
fence: lock.fence,
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
sleepSync(DEP_BUILD_LOCK_POLL_MS);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
function ensureDependencyLockRoot(lockDir) {
|
|
1413
|
+
node_fs_1.default.mkdirSync(node_path_1.default.join(lockDir, DEP_BUILD_LOCK_RETIRED_DIR), {
|
|
1414
|
+
recursive: true,
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
function writeDependencyLockOwner(generationDir, generation) {
|
|
1418
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(generationDir, DEP_BUILD_LOCK_OWNER_FILE), `${JSON.stringify({
|
|
1419
|
+
generation,
|
|
1420
|
+
hostname: node_os_1.default.hostname(),
|
|
1421
|
+
pid: process.pid,
|
|
1422
|
+
startedAt: new Date().toISOString(),
|
|
1423
|
+
})}\n`, "utf8");
|
|
1424
|
+
}
|
|
1425
|
+
function readDependencyLockOwner(generationDir) {
|
|
1426
|
+
try {
|
|
1427
|
+
const parsed = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(generationDir, DEP_BUILD_LOCK_OWNER_FILE), "utf8"));
|
|
1428
|
+
if (typeof parsed.hostname !== "string" ||
|
|
1429
|
+
typeof parsed.pid !== "number" ||
|
|
1430
|
+
!Number.isInteger(parsed.pid) ||
|
|
1431
|
+
parsed.pid <= 0) {
|
|
1432
|
+
return null;
|
|
1433
|
+
}
|
|
1434
|
+
return {
|
|
1435
|
+
hostname: parsed.hostname,
|
|
1436
|
+
pid: parsed.pid,
|
|
1437
|
+
startedAt: typeof parsed.startedAt === "string" ? parsed.startedAt : undefined,
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
catch {
|
|
1441
|
+
return null;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
function readDependencyLockGeneration(generationDir) {
|
|
1445
|
+
try {
|
|
1446
|
+
const generation = node_fs_1.default
|
|
1447
|
+
.readFileSync(node_path_1.default.join(generationDir, DEP_BUILD_LOCK_GENERATION_FILE), "utf8")
|
|
1448
|
+
.trim();
|
|
1449
|
+
return isDependencyGeneration(generation) ? generation : null;
|
|
1450
|
+
}
|
|
1451
|
+
catch {
|
|
1452
|
+
return null;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
/** Age of an observed lock directory, or `null` when it no longer exists. */
|
|
1456
|
+
function dependencyLockAgeMs(generationDir, now) {
|
|
1457
|
+
try {
|
|
1458
|
+
return Math.max(0, now - node_fs_1.default.statSync(generationDir).mtimeMs);
|
|
1459
|
+
}
|
|
1460
|
+
catch (error) {
|
|
1461
|
+
return isMissingPathError(error) ? null : 0;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
function describeDependencyLockOwner(owner) {
|
|
1465
|
+
const started = owner.startedAt === undefined ? "" : ` started at ${owner.startedAt}`;
|
|
1466
|
+
return `pid ${owner.pid} on ${owner.hostname}${started}`;
|
|
1467
|
+
}
|
|
1468
|
+
function isLocalHostName(hostname) {
|
|
1469
|
+
return hostname.toLowerCase() === node_os_1.default.hostname().toLowerCase();
|
|
1470
|
+
}
|
|
1471
|
+
function isProcessAlive(pid) {
|
|
1472
|
+
try {
|
|
1473
|
+
process.kill(pid, 0);
|
|
1474
|
+
return true;
|
|
1475
|
+
}
|
|
1476
|
+
catch (error) {
|
|
1477
|
+
return error.code === "EPERM";
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
function isMissingPathError(error) {
|
|
1481
|
+
const code = error.code;
|
|
1482
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
1483
|
+
}
|
|
1484
|
+
function isRenameDestinationOccupied(error, destination) {
|
|
1485
|
+
const code = error.code;
|
|
1486
|
+
if (code === "EEXIST" || code === "ENOTEMPTY") {
|
|
1487
|
+
return true;
|
|
1488
|
+
}
|
|
1489
|
+
return (code === "EACCES" || code === "EPERM") && node_fs_1.default.existsSync(destination);
|
|
1490
|
+
}
|
|
1491
|
+
/** Render a millisecond duration for lock diagnostics (`137ms`, `42s`, `9m 3s`). */
|
|
1492
|
+
function formatDuration(ms) {
|
|
1493
|
+
if (!Number.isFinite(ms)) {
|
|
1494
|
+
return "an unknown time";
|
|
1495
|
+
}
|
|
1496
|
+
if (ms < 1_000) {
|
|
1497
|
+
return `${Math.max(0, Math.round(ms))}ms`;
|
|
1498
|
+
}
|
|
1499
|
+
const seconds = Math.floor(ms / 1_000);
|
|
1500
|
+
const minutes = Math.floor(seconds / 60);
|
|
1501
|
+
const remainder = seconds % 60;
|
|
1502
|
+
return minutes === 0 ? `${seconds}s` : `${minutes}m ${remainder}s`;
|
|
1503
|
+
}
|
|
915
1504
|
/** Owning-tsconfig cache keyed by directory, mirroring `packageTypeCache`. */
|
|
916
1505
|
const tsconfigCache = new Map();
|
|
917
1506
|
const moduleOptionCache = new Map();
|