ttsc 0.14.2 → 0.15.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.
Files changed (44) hide show
  1. package/driver/emit_plugin.go +165 -0
  2. package/driver/plugins.go +37 -0
  3. package/go.mod +0 -3
  4. package/go.sum +0 -4
  5. package/lib/compiler/internal/resolveEmittedJavaScript.js +25 -4
  6. package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
  7. package/lib/launcher/internal/prepareExecution.d.ts +4 -1
  8. package/lib/launcher/internal/prepareExecution.js +13 -17
  9. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  10. package/lib/launcher/internal/registerRuntimeHooks.js +43 -8
  11. package/lib/launcher/internal/registerRuntimeHooks.js.map +1 -1
  12. package/lib/launcher/internal/runTtsx.js +46 -386
  13. package/lib/launcher/internal/runTtsx.js.map +1 -1
  14. package/lib/launcher/internal/runtimeHookPreload.d.ts +1 -0
  15. package/lib/launcher/internal/runtimeHookPreload.js +16 -0
  16. package/lib/launcher/internal/runtimeHookPreload.js.map +1 -0
  17. package/lib/launcher/internal/runtimeHooks.d.ts +16 -38
  18. package/lib/launcher/internal/runtimeHooks.js +672 -119
  19. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  20. package/lib/plugin/internal/buildSourcePlugin.js +120 -16
  21. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  22. package/lib/plugin/internal/loadProjectPlugins.js +15 -4
  23. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  24. package/package.json +8 -8
  25. package/shim/ast/parent.go +31 -0
  26. package/shim/compiler/emitassemble.go +30 -0
  27. package/shim/compiler/emithook.go +22 -0
  28. package/shim/compiler/go.mod +9 -0
  29. package/shim/compiler/go.sum +22 -0
  30. package/shim/core/modulekind.go +10 -0
  31. package/shim/printer/emithost.go +24 -0
  32. package/shim/printer/go.mod +9 -0
  33. package/shim/printer/go.sum +22 -0
  34. package/shim/transformers/go.mod +14 -0
  35. package/shim/transformers/go.sum +20 -0
  36. package/shim/transformers/shim.go +15 -0
  37. package/src/compiler/internal/resolveEmittedJavaScript.ts +35 -4
  38. package/src/launcher/internal/prepareExecution.ts +18 -21
  39. package/src/launcher/internal/registerRuntimeHooks.ts +53 -8
  40. package/src/launcher/internal/runTtsx.ts +61 -453
  41. package/src/launcher/internal/runtimeHookPreload.ts +14 -0
  42. package/src/launcher/internal/runtimeHooks.ts +794 -136
  43. package/src/plugin/internal/buildSourcePlugin.ts +147 -16
  44. package/src/plugin/internal/loadProjectPlugins.ts +16 -4
@@ -1,8 +1,8 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
- import { pathToFileURL } from "node:url";
5
4
 
5
+ import { resolveTsgo } from "../../compiler/internal/resolveTsgo";
6
6
  import {
7
7
  getBoolean,
8
8
  getNumber,
@@ -70,7 +70,7 @@ function run(argv: readonly string[]): number {
70
70
  project: parsed.project,
71
71
  singleThreaded: parsed.singleThreaded,
72
72
  });
73
- return runPreparedEntry(parsed, prepared, cwd);
73
+ return runPreparedEntry(parsed, prepared, cwd, entry);
74
74
  }
75
75
 
76
76
  function formatError(error: unknown): string {
@@ -215,21 +215,17 @@ function printHelp(): void {
215
215
  }
216
216
 
217
217
  /**
218
- * Node flags that install ttsx's runtime module hooks in the child process.
219
- * `--import` loads the registrar before the compiled entry, giving the
220
- * `resolve`/`load` hooks whole-graph reach (extensionless relative imports and
221
- * raw `.ts` dependencies under `node_modules`) without weakening the up-front
222
- * compile gate. `--disable-warning` silences the `ExperimentalWarning` that the
223
- * `load` hook's internal `stripTypeScriptTypes` call would otherwise print, the
224
- * same flag this repository's own TypeScript runner uses.
218
+ * Append a Node flag to an existing `NODE_OPTIONS` value (or start one). Used
219
+ * to propagate the runtime-hook installer into every child process the program
220
+ * spawns, so workers launched as `node worker.ts` inherit the source loader.
225
221
  */
226
- function runtimeHookArgs(): string[] {
227
- const registrar = path.join(__dirname, "registerRuntimeHooks.js");
228
- return [
229
- "--disable-warning=ExperimentalWarning",
230
- "--import",
231
- pathToFileURL(registrar).href,
232
- ];
222
+ function appendNodeOption(
223
+ existing: string | undefined,
224
+ option: string,
225
+ ): string {
226
+ return existing && existing.trim().length !== 0
227
+ ? `${existing} ${option}`
228
+ : option;
233
229
  }
234
230
 
235
231
  function resolvePreload(cwd: string, preload: string): string {
@@ -250,34 +246,71 @@ function isRelativeSpecifier(specifier: string): boolean {
250
246
  );
251
247
  }
252
248
 
249
+ /**
250
+ * Run the TypeScript entry from source in a child Node process whose runtime
251
+ * module hooks serve the already-built entry project and build raw `.ts`
252
+ * dependencies on demand.
253
+ *
254
+ * The child is `node [-r preload...] registerRuntimeHooks.js <source-entry>
255
+ * <argv...>` (the bootstrap, run as the main module — not `--import`, so a
256
+ * CommonJS `require` chain reaches the hooks). A runtime manifest pins the
257
+ * entry project's emit for the hooks; `TTSC_TSGO_BINARY` lets dependency builds
258
+ * find tsgo without re-resolving it from inside the hook.
259
+ */
253
260
  function runPreparedEntry(
254
261
  parsed: Exclude<ReturnType<typeof parseCLI>, "help" | "version">,
255
262
  execution: ReturnType<typeof prepareExecution>,
256
263
  cwd: string,
264
+ sourceEntry: string,
257
265
  ): number {
258
266
  try {
259
- fs.mkdirSync(execution.emitDir, { recursive: true });
260
- if (execution.moduleKind === "esm") {
261
- rewriteEsmSpecifiers(execution.emitDir);
262
- fs.writeFileSync(
263
- path.join(execution.emitDir, "package.json"),
264
- JSON.stringify({ type: "module" }),
265
- "utf8",
266
- );
267
- }
267
+ const depCacheDir = path.join(execution.cleanupDir, "deps");
268
+ const manifestPath = path.join(
269
+ execution.cleanupDir,
270
+ "runtime-manifest.json",
271
+ );
272
+ fs.mkdirSync(execution.cleanupDir, { recursive: true });
273
+ fs.writeFileSync(
274
+ manifestPath,
275
+ JSON.stringify({
276
+ depCacheDir,
277
+ emitDir: execution.emitDir,
278
+ emittedFiles: execution.emittedFiles,
279
+ moduleOption: execution.moduleOption,
280
+ projectRoot: execution.projectRoot,
281
+ rootDir: execution.rootDir,
282
+ }),
283
+ "utf8",
284
+ );
285
+
286
+ const tsgo = resolveTsgo({
287
+ binary: parsed.binary,
288
+ cwd: execution.projectRoot,
289
+ }).binary;
290
+
291
+ const bootstrap = path.join(__dirname, "registerRuntimeHooks.js");
268
292
  const args = [
269
- ...runtimeHookArgs(),
293
+ "--disable-warning=ExperimentalWarning",
270
294
  ...parsed.preload.flatMap((preload) => [
271
295
  "-r",
272
296
  resolvePreload(cwd, preload),
273
297
  ]),
274
- execution.entryFile,
298
+ bootstrap,
299
+ sourceEntry,
275
300
  ...parsed.passthrough,
276
301
  ];
277
302
  const result = spawnSync(process.execPath, args, {
278
303
  cwd,
304
+ env: {
305
+ ...process.env,
306
+ NODE_OPTIONS: appendNodeOption(
307
+ process.env.NODE_OPTIONS,
308
+ `--require ${JSON.stringify(path.join(__dirname, "runtimeHookPreload.js"))}`,
309
+ ),
310
+ TTSC_TSGO_BINARY: process.env.TTSC_TSGO_BINARY ?? tsgo,
311
+ TTSX_RUNTIME_MANIFEST: manifestPath,
312
+ },
279
313
  stdio: "inherit",
280
- env: process.env,
281
314
  windowsHide: true,
282
315
  });
283
316
  if (result.error) {
@@ -297,428 +330,3 @@ function removeRuntimeOutput(directory: string): void {
297
330
  // Best effort: cleanup must not replace the child process exit status.
298
331
  }
299
332
  }
300
-
301
- /**
302
- * Walk every `.js`/`.mjs`/`.cjs` file in `root` and rewrite bare relative
303
- * specifiers (e.g. `"./foo"`) to include the resolved file extension so Node
304
- * can load them with `--input-type=module`. Files that did not change are not
305
- * written back.
306
- */
307
- function rewriteEsmSpecifiers(root: string): void {
308
- const stack = [root];
309
- while (stack.length !== 0) {
310
- const current = stack.pop()!;
311
- for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
312
- const next = path.join(current, entry.name);
313
- if (entry.isDirectory()) {
314
- stack.push(next);
315
- continue;
316
- }
317
- if (!entry.isFile() || !isJavaScriptOutput(next)) {
318
- continue;
319
- }
320
- const before = fs.readFileSync(next, "utf8");
321
- const after = rewriteEsmSpecifiersInText(next, before);
322
- if (after !== before) {
323
- fs.writeFileSync(next, after, "utf8");
324
- }
325
- }
326
- }
327
- }
328
-
329
- function rewriteEsmSpecifiersInText(fromFile: string, input: string): string {
330
- const replacements: Array<{ start: number; end: number; text: string }> = [];
331
- scanEsmSpecifiers(fromFile, input, 0, input.length, replacements);
332
- if (replacements.length === 0) {
333
- return input;
334
- }
335
- let output = "";
336
- let last = 0;
337
- for (const replacement of replacements) {
338
- output += input.slice(last, replacement.start);
339
- output += replacement.text;
340
- last = replacement.end;
341
- }
342
- return output + input.slice(last);
343
- }
344
-
345
- function scanEsmSpecifiers(
346
- fromFile: string,
347
- input: string,
348
- start: number,
349
- end: number,
350
- replacements: Array<{ start: number; end: number; text: string }>,
351
- ): void {
352
- let i = start;
353
- while (i < end) {
354
- if (input[i] === "`") {
355
- i = scanTemplateExpressions(fromFile, input, i, replacements);
356
- continue;
357
- }
358
- const skipped = skipNonCode(input, i);
359
- if (skipped !== i) {
360
- i = skipped;
361
- continue;
362
- }
363
- if (isKeywordAt(input, i, "import")) {
364
- const next = skipWhitespace(input, i + "import".length);
365
- if (input[next] === "(") {
366
- rewriteDynamicImportSpecifier(fromFile, input, next, replacements);
367
- } else if (isQuote(input[next])) {
368
- rewriteStringSpecifier(fromFile, input, next, replacements);
369
- } else {
370
- rewriteFromSpecifier(fromFile, input, next, replacements);
371
- }
372
- i += "import".length;
373
- continue;
374
- }
375
- if (isKeywordAt(input, i, "export")) {
376
- rewriteFromSpecifier(fromFile, input, i + "export".length, replacements);
377
- i += "export".length;
378
- continue;
379
- }
380
- i += 1;
381
- }
382
- }
383
-
384
- function rewriteDynamicImportSpecifier(
385
- fromFile: string,
386
- input: string,
387
- openParen: number,
388
- replacements: Array<{ start: number; end: number; text: string }>,
389
- ): void {
390
- const specifier = skipWhitespace(input, openParen + 1);
391
- if (isQuote(input[specifier])) {
392
- rewriteStringSpecifier(fromFile, input, specifier, replacements);
393
- }
394
- }
395
-
396
- function rewriteFromSpecifier(
397
- fromFile: string,
398
- input: string,
399
- start: number,
400
- replacements: Array<{ start: number; end: number; text: string }>,
401
- ): void {
402
- let i = start;
403
- let depth = 0;
404
- while (i < input.length) {
405
- const skipped = skipNonCode(input, i);
406
- if (skipped !== i) {
407
- i = skipped;
408
- continue;
409
- }
410
- const current = input[i]!;
411
- if (depth === 0 && current === ";") {
412
- return;
413
- }
414
- if (current === "{" || current === "[" || current === "(") {
415
- depth += 1;
416
- i += 1;
417
- continue;
418
- }
419
- if (current === "}" || current === "]" || current === ")") {
420
- depth = Math.max(0, depth - 1);
421
- i += 1;
422
- continue;
423
- }
424
- if (depth === 0 && isKeywordAt(input, i, "from")) {
425
- const specifier = skipWhitespace(input, i + "from".length);
426
- if (isQuote(input[specifier])) {
427
- rewriteStringSpecifier(fromFile, input, specifier, replacements);
428
- }
429
- return;
430
- }
431
- i += 1;
432
- }
433
- }
434
-
435
- function rewriteStringSpecifier(
436
- fromFile: string,
437
- input: string,
438
- quoteIndex: number,
439
- replacements: Array<{ start: number; end: number; text: string }>,
440
- ): void {
441
- const literal = readSimpleStringLiteral(input, quoteIndex);
442
- if (literal === null) {
443
- return;
444
- }
445
- const next = withResolvableExtension(fromFile, literal.value);
446
- if (next === literal.value) {
447
- return;
448
- }
449
- replacements.push({
450
- start: literal.start,
451
- end: literal.end,
452
- text: next,
453
- });
454
- }
455
-
456
- function scanTemplateExpressions(
457
- fromFile: string,
458
- input: string,
459
- start: number,
460
- replacements: Array<{ start: number; end: number; text: string }>,
461
- ): number {
462
- for (let i = start + 1; i < input.length; i += 1) {
463
- const current = input[i]!;
464
- if (current === "\\") {
465
- i += 1;
466
- continue;
467
- }
468
- if (current === "`") {
469
- return i + 1;
470
- }
471
- if (current === "$" && input[i + 1] === "{") {
472
- const expressionStart = i + 2;
473
- const expressionEnd = findTemplateExpressionEnd(input, expressionStart);
474
- if (expressionEnd === null) {
475
- return input.length;
476
- }
477
- scanEsmSpecifiers(
478
- fromFile,
479
- input,
480
- expressionStart,
481
- expressionEnd,
482
- replacements,
483
- );
484
- i = expressionEnd;
485
- }
486
- }
487
- return input.length;
488
- }
489
-
490
- function findTemplateExpressionEnd(
491
- input: string,
492
- expressionStart: number,
493
- ): number | null {
494
- let depth = 1;
495
- for (let i = expressionStart; i < input.length; i += 1) {
496
- const skipped = skipNonCode(input, i);
497
- if (skipped !== i) {
498
- i = skipped - 1;
499
- continue;
500
- }
501
- const current = input[i]!;
502
- if (current === "{") {
503
- depth += 1;
504
- continue;
505
- }
506
- if (current === "}") {
507
- depth -= 1;
508
- if (depth === 0) {
509
- return i;
510
- }
511
- }
512
- }
513
- return null;
514
- }
515
-
516
- /**
517
- * Append or fix a file extension on a relative specifier so that Node's ESM
518
- * loader can resolve it. The resolution order mirrors Node's own algorithm:
519
- * exact file match → directory index → `.js` fallback. Non-relative specifiers
520
- * and already-extensioned paths are returned unchanged.
521
- */
522
- function withResolvableExtension(fromFile: string, specifier: string): string {
523
- if (!specifier.startsWith(".")) {
524
- // Bare specifiers (packages, builtins) need no rewriting.
525
- return specifier;
526
- }
527
- const [pathname, suffix = ""] = splitSpecifierSuffix(specifier);
528
- if (/\.(?:[cm]?js|json|node)$/i.test(pathname)) {
529
- // Already has a concrete extension the loader understands.
530
- return specifier;
531
- }
532
- const fromDir = path.dirname(fromFile);
533
- // 1. Try the specifier as a file path with each JS extension.
534
- for (const extension of [".js", ".mjs", ".cjs"]) {
535
- if (fs.existsSync(path.resolve(fromDir, pathname + extension))) {
536
- return pathname + extension + suffix;
537
- }
538
- }
539
- // 2. Try interpreting it as a directory with an index file.
540
- const directory = pathname.replace(/\/+$/, "");
541
- for (const extension of [".js", ".mjs", ".cjs"]) {
542
- if (fs.existsSync(path.resolve(fromDir, directory, "index" + extension))) {
543
- return `${directory}/index${extension}${suffix}`;
544
- }
545
- }
546
- // 3. Last resort: append `.js` and let Node surface the error if it's wrong.
547
- return `${pathname}.js${suffix}`;
548
- }
549
-
550
- function splitSpecifierSuffix(specifier: string): [string, string?] {
551
- const index = specifier.search(/[?#]/);
552
- if (index === -1) {
553
- return [specifier];
554
- }
555
- return [specifier.slice(0, index), specifier.slice(index)];
556
- }
557
-
558
- function isJavaScriptOutput(filename: string): boolean {
559
- return /\.(?:[cm]?js)$/i.test(filename);
560
- }
561
-
562
- function skipNonCode(input: string, start: number): number {
563
- const current = input[start];
564
- const next = input[start + 1];
565
- if (current === '"' || current === "'") {
566
- return skipString(input, start);
567
- }
568
- if (current === "`") {
569
- return skipTemplate(input, start);
570
- }
571
- if (current === "/" && next === "/") {
572
- return skipLineComment(input, start);
573
- }
574
- if (current === "/" && next === "*") {
575
- return skipBlockComment(input, start);
576
- }
577
- if (current === "/" && looksLikeRegexStart(input, start)) {
578
- return skipRegex(input, start);
579
- }
580
- return start;
581
- }
582
-
583
- function skipString(input: string, start: number): number {
584
- const quote = input[start]!;
585
- for (let i = start + 1; i < input.length; i += 1) {
586
- const current = input[i]!;
587
- if (current === "\\") {
588
- i += 1;
589
- continue;
590
- }
591
- if (current === quote) {
592
- return i + 1;
593
- }
594
- }
595
- return input.length;
596
- }
597
-
598
- function skipTemplate(input: string, start: number): number {
599
- for (let i = start + 1; i < input.length; i += 1) {
600
- const current = input[i]!;
601
- if (current === "\\") {
602
- i += 1;
603
- continue;
604
- }
605
- if (current === "`") {
606
- return i + 1;
607
- }
608
- }
609
- return input.length;
610
- }
611
-
612
- function skipLineComment(input: string, start: number): number {
613
- const end = input.indexOf("\n", start + 2);
614
- return end === -1 ? input.length : end + 1;
615
- }
616
-
617
- function skipBlockComment(input: string, start: number): number {
618
- const end = input.indexOf("*/", start + 2);
619
- return end === -1 ? input.length : end + 2;
620
- }
621
-
622
- function skipRegex(input: string, start: number): number {
623
- let inClass = false;
624
- for (let i = start + 1; i < input.length; i += 1) {
625
- const current = input[i]!;
626
- if (current === "\\") {
627
- i += 1;
628
- continue;
629
- }
630
- if (current === "[") {
631
- inClass = true;
632
- continue;
633
- }
634
- if (current === "]") {
635
- inClass = false;
636
- continue;
637
- }
638
- if (current === "/" && !inClass) {
639
- i += 1;
640
- while (/[A-Za-z]/.test(input[i] ?? "")) {
641
- i += 1;
642
- }
643
- return i;
644
- }
645
- }
646
- return input.length;
647
- }
648
-
649
- /**
650
- * Determine whether the `/` at `start` opens a regex literal rather than a
651
- * division operator. This is a conservative approximation: check the preceding
652
- * non-whitespace token — if it is an operator character or a keyword that must
653
- * be followed by an expression, we assume regex.
654
- */
655
- function looksLikeRegexStart(input: string, start: number): boolean {
656
- let i = start - 1;
657
- while (i >= 0 && /\s/.test(input[i]!)) {
658
- i -= 1;
659
- }
660
- if (i < 0) {
661
- return true;
662
- }
663
- const previous = input[i]!;
664
- if ("([{=,:;!?&|+-*~^<>".includes(previous)) {
665
- return true;
666
- }
667
- const word = readPreviousWord(input, i);
668
- return /^(?:return|throw|case|delete|void|typeof|instanceof|in|of|yield|await)$/.test(
669
- word,
670
- );
671
- }
672
-
673
- function readPreviousWord(input: string, end: number): string {
674
- let start = end;
675
- while (start >= 0 && isIdentifierPart(input[start]!)) {
676
- start -= 1;
677
- }
678
- return input.slice(start + 1, end + 1);
679
- }
680
-
681
- function readSimpleStringLiteral(
682
- input: string,
683
- quoteIndex: number,
684
- ): { start: number; end: number; value: string } | null {
685
- const quote = input[quoteIndex]!;
686
- for (let i = quoteIndex + 1; i < input.length; i += 1) {
687
- const current = input[i]!;
688
- if (current === "\\") {
689
- return null;
690
- }
691
- if (current === quote) {
692
- return {
693
- start: quoteIndex + 1,
694
- end: i,
695
- value: input.slice(quoteIndex + 1, i),
696
- };
697
- }
698
- }
699
- return null;
700
- }
701
-
702
- function skipWhitespace(input: string, start: number): number {
703
- let i = start;
704
- while (i < input.length && /\s/.test(input[i]!)) {
705
- i += 1;
706
- }
707
- return i;
708
- }
709
-
710
- function isKeywordAt(input: string, start: number, keyword: string): boolean {
711
- return (
712
- input.startsWith(keyword, start) &&
713
- !isIdentifierPart(input[start - 1] ?? "") &&
714
- !isIdentifierPart(input[start + keyword.length] ?? "")
715
- );
716
- }
717
-
718
- function isIdentifierPart(value: string): boolean {
719
- return /[$\p{ID_Continue}]/u.test(value);
720
- }
721
-
722
- function isQuote(value: string | undefined): value is '"' | "'" {
723
- return value === '"' || value === "'";
724
- }
@@ -0,0 +1,14 @@
1
+ import { installRuntimeHooks } from "./runtimeHooks";
2
+
3
+ /**
4
+ * `--require` target that only installs the runtime module hooks. `ttsx` puts
5
+ * it on `NODE_OPTIONS` for the child process so every descendant the program
6
+ * spawns (e.g. a worker started with `node worker.ts`) inherits the same
7
+ * source-loading hooks, the way `ts-node` propagates through `--require
8
+ * ts-node/register`. The preload itself is CommonJS and only registers the
9
+ * hooks; ESM support comes from the `registerHooks` they install, not from how
10
+ * the preload is loaded. The entry process additionally goes through the
11
+ * bootstrap, which loads the entry itself; here we only register
12
+ * (idempotently).
13
+ */
14
+ installRuntimeHooks();