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
|
@@ -89,6 +89,17 @@ const EXTERNAL_GO_BUILD_ENV_KEYS: readonly string[] = [
|
|
|
89
89
|
];
|
|
90
90
|
const CONTRIBUTIONS_FILE_NAME = "ttsc_contributions.go";
|
|
91
91
|
const CONTRIB_DIRNAME = "contrib";
|
|
92
|
+
// A cold source-plugin build is a multi-second-to-minutes `go build`. When a
|
|
93
|
+
// program fans out into many processes (a `pnpm -r` running several suites in
|
|
94
|
+
// parallel, a benchmark, a worker pool), each inherits the same cold cache and
|
|
95
|
+
// would otherwise launch its own full build of the SAME cache key at the same
|
|
96
|
+
// instant. The atomic lock below lets one process build while the rest poll for
|
|
97
|
+
// its published binary, so the toolchain runs once per cache key instead of N
|
|
98
|
+
// times. A waiter steals an abandoned lock (builder crashed) after this timeout
|
|
99
|
+
// so a fan-out never wedges; it matches the dependency-build lock in
|
|
100
|
+
// runtimeHooks.ts.
|
|
101
|
+
const PLUGIN_BUILD_LOCK_STEAL_MS = 600_000;
|
|
102
|
+
const PLUGIN_BUILD_LOCK_POLL_MS = 50;
|
|
92
103
|
const GLOBAL_CACHE_DIRNAME = "ttsc";
|
|
93
104
|
const PLUGIN_CACHE_DIRNAME = "plugins";
|
|
94
105
|
const CACHE_LAST_USED_FILE = ".last-used";
|
|
@@ -143,47 +154,167 @@ export function buildSourcePlugin(opts: {
|
|
|
143
154
|
return binaryPath;
|
|
144
155
|
}
|
|
145
156
|
fs.mkdirSync(cacheDir, { recursive: true });
|
|
146
|
-
|
|
147
|
-
|
|
157
|
+
return buildUnderPluginLock(cacheDir, binaryPath, () =>
|
|
158
|
+
compileSourcePlugin({
|
|
159
|
+
binaryPath,
|
|
160
|
+
cacheDir,
|
|
161
|
+
contributors,
|
|
162
|
+
dir,
|
|
163
|
+
entry,
|
|
164
|
+
goBinary,
|
|
165
|
+
key,
|
|
166
|
+
label: opts.label ?? "source plugin",
|
|
167
|
+
overlayDirs,
|
|
168
|
+
pluginName: opts.pluginName,
|
|
169
|
+
quiet: opts.quiet === true,
|
|
170
|
+
source,
|
|
171
|
+
}),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Run the actual `go build` and publish the binary; assumes the lock is held. */
|
|
176
|
+
function compileSourcePlugin(opts: {
|
|
177
|
+
binaryPath: string;
|
|
178
|
+
cacheDir: string;
|
|
179
|
+
contributors: readonly ITtscBuildContributor[];
|
|
180
|
+
dir: string;
|
|
181
|
+
entry: string;
|
|
182
|
+
goBinary: string;
|
|
183
|
+
key: string;
|
|
184
|
+
label: string;
|
|
185
|
+
overlayDirs: readonly string[];
|
|
186
|
+
pluginName: string;
|
|
187
|
+
quiet: boolean;
|
|
188
|
+
source: string;
|
|
189
|
+
}): string {
|
|
190
|
+
if (!opts.quiet) {
|
|
148
191
|
const extra =
|
|
149
|
-
contributors.length === 0
|
|
192
|
+
opts.contributors.length === 0
|
|
150
193
|
? ""
|
|
151
|
-
: ` + ${contributors.length} contributor(s): ${contributors
|
|
194
|
+
: ` + ${opts.contributors.length} contributor(s): ${opts.contributors
|
|
152
195
|
.map((c) => c.name)
|
|
153
196
|
.join(", ")}`;
|
|
154
197
|
process.stderr.write(
|
|
155
|
-
`ttsc: building ${label} "${opts.pluginName}" from ${source}${extra} (this runs once per cache key)\n`,
|
|
198
|
+
`ttsc: building ${opts.label} "${opts.pluginName}" from ${opts.source}${extra} (this runs once per cache key)\n`,
|
|
156
199
|
);
|
|
157
200
|
}
|
|
158
201
|
|
|
159
202
|
const scratchDir = fs.mkdtempSync(
|
|
160
|
-
path.join(os.tmpdir(), `ttsc-plugin-${key}-`),
|
|
203
|
+
path.join(os.tmpdir(), `ttsc-plugin-${opts.key}-`),
|
|
161
204
|
);
|
|
162
205
|
try {
|
|
163
|
-
materializeScratchDir(dir, scratchDir);
|
|
164
|
-
const goModReader = createGoModReader(goBinary, opts.pluginName);
|
|
165
|
-
if (contributors.length > 0) {
|
|
206
|
+
materializeScratchDir(opts.dir, scratchDir);
|
|
207
|
+
const goModReader = createGoModReader(opts.goBinary, opts.pluginName);
|
|
208
|
+
if (opts.contributors.length > 0) {
|
|
166
209
|
mergeContributors({
|
|
167
|
-
contributors,
|
|
168
|
-
entry,
|
|
210
|
+
contributors: opts.contributors,
|
|
211
|
+
entry: opts.entry,
|
|
169
212
|
goModReader,
|
|
170
213
|
pluginName: opts.pluginName,
|
|
171
214
|
scratchDir,
|
|
172
215
|
});
|
|
173
216
|
}
|
|
174
|
-
writeGoWork(scratchDir, overlayDirs, goBinary, opts.pluginName);
|
|
217
|
+
writeGoWork(scratchDir, opts.overlayDirs, opts.goBinary, opts.pluginName);
|
|
175
218
|
const scratchBinaryName =
|
|
176
219
|
process.platform === "win32" ? ".ttsc-plugin.exe" : ".ttsc-plugin";
|
|
177
|
-
runGoBuild(
|
|
220
|
+
runGoBuild(
|
|
221
|
+
scratchDir,
|
|
222
|
+
opts.entry,
|
|
223
|
+
scratchBinaryName,
|
|
224
|
+
opts.pluginName,
|
|
225
|
+
opts.goBinary,
|
|
226
|
+
);
|
|
178
227
|
const builtBinary = path.join(scratchDir, scratchBinaryName);
|
|
179
|
-
publishBuiltBinary(builtBinary, binaryPath);
|
|
180
|
-
touchCacheEntry(cacheDir);
|
|
181
|
-
return binaryPath;
|
|
228
|
+
publishBuiltBinary(builtBinary, opts.binaryPath);
|
|
229
|
+
touchCacheEntry(opts.cacheDir);
|
|
230
|
+
return opts.binaryPath;
|
|
182
231
|
} finally {
|
|
183
232
|
fs.rmSync(scratchDir, { recursive: true, force: true });
|
|
184
233
|
}
|
|
185
234
|
}
|
|
186
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Build a source plugin while holding an exclusive cross-process lock for its
|
|
238
|
+
* cache key, so concurrent fan-out (parallel suites, a benchmark, a worker
|
|
239
|
+
* pool) runs the `go build` once instead of once per process.
|
|
240
|
+
*
|
|
241
|
+
* The lock is an atomic `mkdir` on `<cacheDir>.lock`. The winner builds and
|
|
242
|
+
* publishes the binary; every loser polls for that binary to appear and reuses
|
|
243
|
+
* it. A loser that waits past `PLUGIN_BUILD_LOCK_STEAL_MS` (the builder
|
|
244
|
+
* crashed without publishing) steals the abandoned lock and retries. This is
|
|
245
|
+
* the same shape as `withBuildLock` in the runtime hooks, keyed on the plugin
|
|
246
|
+
* binary's existence instead of a JSON meta marker.
|
|
247
|
+
*
|
|
248
|
+
* Correctness still rests on `publishBuiltBinary`'s atomic rename: the lock is
|
|
249
|
+
* an optimisation to avoid duplicate work, not the only guard against a
|
|
250
|
+
* corrupt binary, so a stolen lock that races a slow builder cannot ship a
|
|
251
|
+
* half-written file.
|
|
252
|
+
*/
|
|
253
|
+
function buildUnderPluginLock(
|
|
254
|
+
cacheDir: string,
|
|
255
|
+
binaryPath: string,
|
|
256
|
+
build: () => string,
|
|
257
|
+
): string {
|
|
258
|
+
const lockDir = `${cacheDir}.lock`;
|
|
259
|
+
for (;;) {
|
|
260
|
+
if (fs.existsSync(binaryPath)) {
|
|
261
|
+
touchCacheEntry(cacheDir);
|
|
262
|
+
return binaryPath;
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
// Plain mkdir (no `recursive`): recursive mode is idempotent and would
|
|
266
|
+
// not throw EEXIST, defeating the lock. The parent `cacheDir` already
|
|
267
|
+
// exists, so a single-level mkdir is all that is needed and its EEXIST
|
|
268
|
+
// is exactly the "someone else holds the lock" signal.
|
|
269
|
+
fs.mkdirSync(lockDir);
|
|
270
|
+
} catch (error) {
|
|
271
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") {
|
|
272
|
+
// A lock dir we cannot create for an unexpected reason (e.g. a
|
|
273
|
+
// read-only cache) must not silently skip the build. Fall back to an
|
|
274
|
+
// unlocked build — correctness is preserved by the atomic publish.
|
|
275
|
+
return build();
|
|
276
|
+
}
|
|
277
|
+
const waited = waitForPluginBinary(binaryPath, PLUGIN_BUILD_LOCK_STEAL_MS);
|
|
278
|
+
if (waited) {
|
|
279
|
+
touchCacheEntry(cacheDir);
|
|
280
|
+
return binaryPath;
|
|
281
|
+
}
|
|
282
|
+
// Builder appears to have crashed: steal the abandoned lock and retry.
|
|
283
|
+
fs.rmSync(lockDir, { force: true, recursive: true });
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
// Re-check under the lock: a previous holder may have just published.
|
|
288
|
+
if (fs.existsSync(binaryPath)) {
|
|
289
|
+
touchCacheEntry(cacheDir);
|
|
290
|
+
return binaryPath;
|
|
291
|
+
}
|
|
292
|
+
return build();
|
|
293
|
+
} finally {
|
|
294
|
+
fs.rmSync(lockDir, { force: true, recursive: true });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Poll for the locked builder to publish its binary, up to `timeoutMs`. */
|
|
300
|
+
function waitForPluginBinary(binaryPath: string, timeoutMs: number): boolean {
|
|
301
|
+
const startedAt = Date.now();
|
|
302
|
+
for (;;) {
|
|
303
|
+
if (fs.existsSync(binaryPath)) {
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
if (Date.now() - startedAt > timeoutMs) {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
sleepSync(PLUGIN_BUILD_LOCK_POLL_MS);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Block the current (synchronous) thread for `ms` without busy-spinning. */
|
|
314
|
+
function sleepSync(ms: number): void {
|
|
315
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
316
|
+
}
|
|
317
|
+
|
|
187
318
|
/**
|
|
188
319
|
* Copy every contributor's Go source into a sub-package of the host module and
|
|
189
320
|
* synthesize a blank-import file alongside the host's entry package so each
|
|
@@ -346,10 +346,22 @@ function resolvePluginEntries(
|
|
|
346
346
|
config,
|
|
347
347
|
}));
|
|
348
348
|
}
|
|
349
|
-
const configured = project.compilerOptions.plugins.map((config, index) =>
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
349
|
+
const configured = project.compilerOptions.plugins.map((config, index) => {
|
|
350
|
+
// A bare/package plugin specifier (e.g. "typia/lib/transform") must resolve
|
|
351
|
+
// from the project's own node_modules, not from the tsconfig that declared
|
|
352
|
+
// it: an `extends`ed base config (a shared `tests/config/tsconfig.json`)
|
|
353
|
+
// declares the plugin, but the package is installed under the consuming
|
|
354
|
+
// project. Only a relative specifier ("./plugin") is meaningful relative to
|
|
355
|
+
// the declaring config's directory. Mirrors discoverPackagePluginEntries.
|
|
356
|
+
const declaringDir = project.pluginBaseDirs[index];
|
|
357
|
+
const baseDir =
|
|
358
|
+
typeof config.transform === "string" &&
|
|
359
|
+
isRelativePluginSpecifier(config.transform) &&
|
|
360
|
+
declaringDir !== undefined
|
|
361
|
+
? declaringDir
|
|
362
|
+
: project.root;
|
|
363
|
+
return { baseDir, config };
|
|
364
|
+
});
|
|
353
365
|
return [...configured, ...discoverPackagePluginEntries(project, configured)];
|
|
354
366
|
}
|
|
355
367
|
|