ttsc 0.15.2 → 0.15.4

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.
@@ -220,10 +220,26 @@ function linkVirtualEntry(
220
220
  }
221
221
  return;
222
222
  }
223
+ if (
224
+ process.platform === "win32" &&
225
+ entry.isSymbolicLink() &&
226
+ isDirectorySymlinkTarget(realEntry)
227
+ ) {
228
+ fs.symlinkSync(realEntry, virtualEntry, "junction");
229
+ return;
230
+ }
223
231
  // Symlinks (and other special entries) are re-symlinked as-is.
224
232
  fs.symlinkSync(realEntry, virtualEntry);
225
233
  }
226
234
 
235
+ function isDirectorySymlinkTarget(realEntry: string): boolean {
236
+ try {
237
+ return fs.statSync(realEntry).isDirectory();
238
+ } catch {
239
+ return false;
240
+ }
241
+ }
242
+
227
243
  /**
228
244
  * Walk from `projectRoot` upward (up to `MAX_VIRTUAL_PARENT_DEPTH` steps),
229
245
  * stopping early at a workspace root (`pnpm-workspace.yaml` or `.git`). The
@@ -78,6 +78,13 @@ interface LoadResult {
78
78
  shortCircuit?: boolean;
79
79
  }
80
80
 
81
+ interface ServedSource {
82
+ source: string;
83
+ moduleOption?: string;
84
+ emittedFile?: string;
85
+ sourceFile?: string;
86
+ }
87
+
81
88
  type NextResolve = (
82
89
  specifier: string,
83
90
  context: ResolveContext,
@@ -200,13 +207,19 @@ function resolve(
200
207
  nextResolve: NextResolve,
201
208
  ): ResolveResult {
202
209
  try {
203
- return nextResolve(specifier, context);
210
+ return rememberCommonJsNamedInterop(
211
+ nextResolve(specifier, context),
212
+ context,
213
+ );
204
214
  } catch (error) {
205
215
  const rescued = probeRescuableSpecifier(specifier, context.parentURL);
206
216
  if (rescued === null) {
207
217
  throw error;
208
218
  }
209
- return { shortCircuit: true, url: rescued };
219
+ return rememberCommonJsNamedInterop(
220
+ { shortCircuit: true, url: rescued },
221
+ context,
222
+ );
210
223
  }
211
224
  }
212
225
 
@@ -219,6 +232,10 @@ interface BuiltProject {
219
232
  }
220
233
  const builtProjects = new Map<string, BuiltProject>();
221
234
 
235
+ /** File URLs whose CommonJS source was reached from an ESM parent import. */
236
+ const commonJsNamedInteropUrls = new Set<string>();
237
+ const commonJsNameScanSources = new Map<string, string | null>();
238
+
222
239
  function load(
223
240
  url: string,
224
241
  context: LoadContext,
@@ -231,14 +248,95 @@ function load(
231
248
  if (!isTypeScriptSource(filename)) {
232
249
  return nextLoad(url, context);
233
250
  }
234
- const { source, moduleOption } = resolveServedSource(filename, url);
251
+ const { format, source } = resolveRuntimeSource(filename, url);
235
252
  return {
236
- format: moduleFormat(filename, moduleOption),
253
+ format,
237
254
  shortCircuit: true,
238
255
  source,
239
256
  };
240
257
  }
241
258
 
259
+ function resolveRuntimeSource(
260
+ filename: string,
261
+ url: string = pathToFileURL(filename).href,
262
+ ): { format: string; source: string } {
263
+ const served = resolveServedSource(filename, url);
264
+ const format = moduleFormat(filename, served.moduleOption);
265
+ return {
266
+ format,
267
+ source:
268
+ format === "commonjs" && commonJsNamedInteropUrls.has(url)
269
+ ? exposeCommonJsStarExports(
270
+ served.source,
271
+ served.emittedFile,
272
+ served.sourceFile,
273
+ )
274
+ : served.source,
275
+ };
276
+ }
277
+
278
+ function rememberCommonJsNamedInterop(
279
+ result: ResolveResult,
280
+ context: ResolveContext,
281
+ ): ResolveResult {
282
+ if (shouldExposeCommonJsNamedExports(result.url, context.parentURL)) {
283
+ commonJsNamedInteropUrls.add(result.url);
284
+ }
285
+ return result;
286
+ }
287
+
288
+ function shouldExposeCommonJsNamedExports(
289
+ url: string,
290
+ parentURL: string | undefined,
291
+ ): boolean {
292
+ if (
293
+ parentURL === undefined ||
294
+ !url.startsWith("file:") ||
295
+ !parentURL.startsWith("file:")
296
+ ) {
297
+ return false;
298
+ }
299
+ const parentFile = fileURLToPath(parentURL);
300
+ if (moduleFormat(parentFile, moduleOptionForSource(parentFile)) !== "module") {
301
+ return false;
302
+ }
303
+ const filename = fileURLToPath(url);
304
+ return (
305
+ isTypeScriptSource(filename) &&
306
+ moduleFormat(filename, moduleOptionForSource(filename)) === "commonjs"
307
+ );
308
+ }
309
+
310
+ function moduleOptionForSource(filename: string): string | undefined {
311
+ if (!isTypeScriptSource(filename)) {
312
+ return undefined;
313
+ }
314
+ const real = realPath(filename);
315
+ const m = manifest();
316
+ if (m !== null && isWithin(real, m.rootDir)) {
317
+ return m.moduleOption;
318
+ }
319
+ const tsconfig = nearestTsconfig(real);
320
+ if (tsconfig === null) {
321
+ return undefined;
322
+ }
323
+ if (moduleOptionCache.has(tsconfig)) {
324
+ return moduleOptionCache.get(tsconfig);
325
+ }
326
+ let moduleOption: string | undefined;
327
+ try {
328
+ const project = readProjectConfig({ cwd: path.dirname(tsconfig), tsconfig });
329
+ moduleOption =
330
+ typeof project.compilerOptions.module === "string"
331
+ ? project.compilerOptions.module
332
+ : undefined;
333
+ } catch {
334
+ moduleOption = undefined;
335
+ }
336
+ moduleOptionCache.set(tsconfig, moduleOption);
337
+ return moduleOption;
338
+ }
339
+
242
340
  /**
243
341
  * Resolve the JavaScript to run for a TypeScript source file, in priority
244
342
  * order: the entry project's pre-built emit (transform plugins applied), a
@@ -249,11 +347,11 @@ function load(
249
347
  function resolveServedSource(
250
348
  filename: string,
251
349
  url: string = pathToFileURL(filename).href,
252
- ): { source: string; moduleOption?: string } {
350
+ ): ServedSource {
253
351
  const real = realPath(filename);
254
352
  const served = serveEntryEmit(real);
255
353
  if (served !== null) {
256
- return { moduleOption: manifest()?.moduleOption, source: served };
354
+ return { moduleOption: manifest()?.moduleOption, ...served };
257
355
  }
258
356
  const built = serveDependencyEmit(real);
259
357
  if (built !== null) {
@@ -261,6 +359,7 @@ function resolveServedSource(
261
359
  }
262
360
  return {
263
361
  moduleOption: undefined,
362
+ sourceFile: filename,
264
363
  source: transformOrphanSource(filename, url),
265
364
  };
266
365
  }
@@ -362,6 +461,61 @@ function emitOrphanAsCommonJs(filename: string): string | null {
362
461
  }
363
462
  }
364
463
 
464
+ /**
465
+ * Emit a source file only for CommonJS export-name discovery.
466
+ *
467
+ * This intentionally does not read or write the runtime orphan cache. Name
468
+ * discovery may inspect a source dependency without executing it, so sharing
469
+ * that output with the runtime fallback would let a speculative scan affect a
470
+ * later load path.
471
+ */
472
+ function emitCommonJsForNameScan(filename: string): string | null {
473
+ const real = realPath(filename);
474
+ const cached = commonJsNameScanSources.get(real);
475
+ if (cached !== undefined) {
476
+ return cached;
477
+ }
478
+ let tsgo: string;
479
+ try {
480
+ tsgo = resolveTsgo({ cwd: path.dirname(real) }).binary;
481
+ } catch {
482
+ commonJsNameScanSources.set(real, null);
483
+ return null;
484
+ }
485
+ const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "ttsx-export-scan-"));
486
+ try {
487
+ const res = spawnNative(
488
+ tsgo,
489
+ [
490
+ real,
491
+ "--ignoreConfig",
492
+ "--module",
493
+ "commonjs",
494
+ "--target",
495
+ "es2022",
496
+ "--noCheck",
497
+ "--skipLibCheck",
498
+ "--outDir",
499
+ outDir,
500
+ "--listEmittedFiles",
501
+ ],
502
+ { cwd: path.dirname(real), encoding: "utf8" },
503
+ );
504
+ const emitted = pickEmittedJavaScript(
505
+ real,
506
+ parseEmittedFiles(outputText(res.stdout)),
507
+ );
508
+ const lowered = emitted === null ? null : readFileOrNull(emitted);
509
+ commonJsNameScanSources.set(real, lowered);
510
+ return lowered;
511
+ } catch {
512
+ commonJsNameScanSources.set(real, null);
513
+ return null;
514
+ } finally {
515
+ fs.rmSync(outDir, { force: true, recursive: true });
516
+ }
517
+ }
518
+
365
519
  /**
366
520
  * Cache root for lowered orphan sources, shared per run (and across runs when
367
521
  * `TTSC_CACHE_DIR` points at a persisted directory).
@@ -423,6 +577,245 @@ function parseFirstEmittedFile(stdout: string): string | null {
423
577
  return null;
424
578
  }
425
579
 
580
+ /** `TSFILE:` paths tsgo printed under `--listEmittedFiles`. */
581
+ function parseEmittedFiles(stdout: string): string[] {
582
+ const files: string[] = [];
583
+ for (const line of stdout.split(/\r?\n/)) {
584
+ const match = line.match(/^TSFILE:\s*(.+)$/);
585
+ if (match?.[1]) {
586
+ files.push(match[1].trim());
587
+ }
588
+ }
589
+ return files;
590
+ }
591
+
592
+ /** Pick the emitted JavaScript corresponding to the source file requested. */
593
+ function pickEmittedJavaScript(
594
+ filename: string,
595
+ emittedFiles: readonly string[],
596
+ ): string | null {
597
+ const stem = path
598
+ .basename(filename)
599
+ .replace(/\.[cm]?tsx?$/i, "")
600
+ .toLowerCase();
601
+ const candidates = emittedFiles.filter((file) => {
602
+ const parsed = path.parse(file);
603
+ return (
604
+ parsed.name.toLowerCase() === stem && /\.(?:[cm]?js)$/i.test(parsed.base)
605
+ );
606
+ });
607
+ if (candidates.length === 1) {
608
+ return candidates[0]!;
609
+ }
610
+ return emittedFiles.find((file) => /\.(?:[cm]?js)$/i.test(file)) ?? null;
611
+ }
612
+
613
+ /**
614
+ * Make TypeScript-Go's CommonJS `export *` output visible to Node's
615
+ * ESM-from-CJS named export scanner.
616
+ *
617
+ * Tsgo lowers star re-exports to `__exportStar(require("./x"), exports)`.
618
+ * Runtime CommonJS consumers see the getters that helper installs, but Node's
619
+ * ESM linker only exposes named imports it can statically identify from
620
+ * `exports.name = ...` assignments. For relative star re-exports whose emitted
621
+ * target is available, replace the helper call with explicit configurable
622
+ * export placeholders followed by the same `__createBinding` getter install.
623
+ */
624
+ function exposeCommonJsStarExports(
625
+ source: string,
626
+ emittedFile: string | undefined,
627
+ sourceFile: string | undefined,
628
+ ): string {
629
+ if (!source.includes("__exportStar(")) {
630
+ return source;
631
+ }
632
+ const reserved = collectStaticCommonJsExportNames(source);
633
+ let index = 0;
634
+ return source.replace(
635
+ /^(\s*)__exportStar\(\s*require\((["'])([^"']+)\2\)\s*,\s*exports\s*\);/gm,
636
+ (statement: string, indent: string, _quote: string, specifier: string) => {
637
+ const names = [
638
+ ...collectStarExportNames(emittedFile, sourceFile, specifier),
639
+ ].filter(
640
+ (name) =>
641
+ name !== "default" &&
642
+ name !== "__esModule" &&
643
+ isIdentifierName(name) &&
644
+ !reserved.has(name),
645
+ );
646
+ if (names.length === 0) {
647
+ return statement;
648
+ }
649
+ for (const name of names) {
650
+ reserved.add(name);
651
+ }
652
+ const receiver = `__ttsx_export_star_${index++}`;
653
+ return [
654
+ ...names.map((name) => `${indent}exports.${name} = void 0;`),
655
+ `${indent}var ${receiver} = require(${JSON.stringify(specifier)});`,
656
+ ...names.map(
657
+ (name) =>
658
+ `${indent}__createBinding(exports, ${receiver}, ${JSON.stringify(name)});`,
659
+ ),
660
+ ].join("\n");
661
+ },
662
+ );
663
+ }
664
+
665
+ function collectStarExportNames(
666
+ emittedFile: string | undefined,
667
+ sourceFile: string | undefined,
668
+ specifier: string,
669
+ ): Set<string> {
670
+ if (emittedFile !== undefined) {
671
+ const emittedTarget = resolveEmittedRequire(emittedFile, specifier);
672
+ if (emittedTarget !== null) {
673
+ return collectCommonJsExportNames(emittedTarget, new Set());
674
+ }
675
+ }
676
+ if (sourceFile !== undefined) {
677
+ const sourceTarget = resolveSourceSpecifier(sourceFile, specifier);
678
+ if (sourceTarget !== null) {
679
+ return collectSourceCommonJsExportNames(sourceTarget, new Set());
680
+ }
681
+ }
682
+ return new Set();
683
+ }
684
+
685
+ function collectCommonJsExportNames(
686
+ emittedFile: string,
687
+ seen: Set<string>,
688
+ ): Set<string> {
689
+ const real = realPath(emittedFile);
690
+ if (seen.has(real)) {
691
+ return new Set();
692
+ }
693
+ seen.add(real);
694
+ const source = readFileOrNull(real);
695
+ if (source === null) {
696
+ return new Set();
697
+ }
698
+ const names = collectStaticCommonJsExportNames(source);
699
+ for (const specifier of collectExportStarSpecifiers(source)) {
700
+ const target = resolveEmittedRequire(real, specifier);
701
+ if (target === null) {
702
+ continue;
703
+ }
704
+ for (const name of collectCommonJsExportNames(target, seen)) {
705
+ if (name !== "default" && name !== "__esModule" && !names.has(name)) {
706
+ names.add(name);
707
+ }
708
+ }
709
+ }
710
+ return names;
711
+ }
712
+
713
+ function collectStaticCommonJsExportNames(source: string): Set<string> {
714
+ const names = new Set<string>();
715
+ const pattern =
716
+ /(?:^|[^\w$])(?:exports|module\.exports)\.([A-Za-z_$][\w$]*)\s*=/g;
717
+ let match: RegExpExecArray | null;
718
+ while ((match = pattern.exec(source)) !== null) {
719
+ names.add(match[1]!);
720
+ }
721
+ return names;
722
+ }
723
+
724
+ function collectSourceCommonJsExportNames(
725
+ sourceFile: string,
726
+ seen: Set<string>,
727
+ ): Set<string> {
728
+ const real = realPath(sourceFile);
729
+ if (seen.has(real)) {
730
+ return new Set();
731
+ }
732
+ seen.add(real);
733
+ const source = emitCommonJsForNameScan(real);
734
+ if (source === null) {
735
+ return new Set();
736
+ }
737
+ const names = collectStaticCommonJsExportNames(source);
738
+ for (const specifier of collectExportStarSpecifiers(source)) {
739
+ const target = resolveSourceSpecifier(real, specifier);
740
+ if (target === null) {
741
+ continue;
742
+ }
743
+ for (const name of collectSourceCommonJsExportNames(target, seen)) {
744
+ if (name !== "default" && name !== "__esModule" && !names.has(name)) {
745
+ names.add(name);
746
+ }
747
+ }
748
+ }
749
+ return names;
750
+ }
751
+
752
+ function collectExportStarSpecifiers(source: string): string[] {
753
+ const specifiers: string[] = [];
754
+ const pattern =
755
+ /^(\s*)__exportStar\(\s*require\((["'])([^"']+)\2\)\s*,\s*exports\s*\);/gm;
756
+ let match: RegExpExecArray | null;
757
+ while ((match = pattern.exec(source)) !== null) {
758
+ specifiers.push(match[3]!);
759
+ }
760
+ return specifiers;
761
+ }
762
+
763
+ function resolveEmittedRequire(
764
+ emittedFile: string,
765
+ specifier: string,
766
+ ): string | null {
767
+ if (!isRelativeSpecifier(specifier)) {
768
+ return null;
769
+ }
770
+ const base = path.resolve(path.dirname(emittedFile), specifier);
771
+ if (path.extname(base).length !== 0) {
772
+ return isFile(base) ? base : null;
773
+ }
774
+ for (const extension of [".js", ".cjs", ".mjs"] as const) {
775
+ const candidate = base + extension;
776
+ if (isFile(candidate)) {
777
+ return candidate;
778
+ }
779
+ }
780
+ for (const extension of [".js", ".cjs", ".mjs"] as const) {
781
+ const candidate = path.join(base, `index${extension}`);
782
+ if (isFile(candidate)) {
783
+ return candidate;
784
+ }
785
+ }
786
+ return null;
787
+ }
788
+
789
+ function resolveSourceSpecifier(
790
+ sourceFile: string,
791
+ specifier: string,
792
+ ): string | null {
793
+ if (!isRelativeSpecifier(specifier)) {
794
+ return null;
795
+ }
796
+ const base = path.resolve(path.dirname(sourceFile), specifier);
797
+ if (path.extname(base).length !== 0) {
798
+ return isFile(base) ? base : null;
799
+ }
800
+ for (const extension of TYPESCRIPT_EXTENSIONS) {
801
+ const candidate = base + extension;
802
+ if (isFile(candidate)) {
803
+ return candidate;
804
+ }
805
+ }
806
+ for (const extension of TYPESCRIPT_EXTENSIONS) {
807
+ const candidate = path.join(base, `index${extension}`);
808
+ if (isFile(candidate)) {
809
+ return candidate;
810
+ }
811
+ }
812
+ return null;
813
+ }
814
+
815
+ function isIdentifierName(name: string): boolean {
816
+ return /^[A-Za-z_$][\w$]*$/.test(name);
817
+ }
818
+
426
819
  /**
427
820
  * Serve the entry project's pre-built JavaScript for a source file the build
428
821
  * emitted, or `null` when the file is outside the build or its emit is
@@ -435,7 +828,7 @@ function parseFirstEmittedFile(stdout: string): string | null {
435
828
  * `rootDir` cannot have a mirrored emit, so it falls through to the dependency
436
829
  * paths.
437
830
  */
438
- function serveEntryEmit(real: string): string | null {
831
+ function serveEntryEmit(real: string): ServedSource | null {
439
832
  const m = manifest();
440
833
  if (m === null) {
441
834
  return null;
@@ -449,7 +842,13 @@ function serveEntryEmit(real: string): string | null {
449
842
  projectRoot: m.rootDir,
450
843
  sourceFile: real,
451
844
  });
452
- return readFileOrNull(emitted);
845
+ if (emitted === null) {
846
+ return null;
847
+ }
848
+ const source = readFileOrNull(emitted);
849
+ return source === null
850
+ ? null
851
+ : { emittedFile: emitted, source, sourceFile: real };
453
852
  }
454
853
 
455
854
  /**
@@ -474,9 +873,7 @@ function isWithin(real: string, directory: string): boolean {
474
873
  * tsconfig (transform plugins included), so a source-shipping package that
475
874
  * needs a transform behaves correctly at runtime.
476
875
  */
477
- function serveDependencyEmit(
478
- real: string,
479
- ): { source: string; moduleOption?: string } | null {
876
+ function serveDependencyEmit(real: string): ServedSource | null {
480
877
  const tsconfig = nearestTsconfig(real);
481
878
  if (tsconfig === null) {
482
879
  return null;
@@ -489,14 +886,35 @@ function serveDependencyEmit(
489
886
  // this single file rather than failing the whole run.
490
887
  return null;
491
888
  }
889
+ const served = serveBuiltDependency(built, real);
890
+ if (served !== null) {
891
+ return served;
892
+ }
893
+ return null;
894
+ }
895
+
896
+ function serveBuiltDependency(
897
+ built: BuiltProject,
898
+ real: string,
899
+ ): ServedSource | null {
492
900
  const emitted = resolveEmittedJavaScript({
493
901
  emittedFiles: built.emittedFiles,
494
902
  outDir: built.emitDir,
495
903
  projectRoot: built.rootDir,
496
904
  sourceFile: real,
497
905
  });
906
+ if (emitted === null) {
907
+ return null;
908
+ }
498
909
  const source = readFileOrNull(emitted);
499
- return source === null ? null : { moduleOption: built.moduleOption, source };
910
+ return source === null
911
+ ? null
912
+ : {
913
+ emittedFile: emitted,
914
+ moduleOption: built.moduleOption,
915
+ source,
916
+ sourceFile: real,
917
+ };
500
918
  }
501
919
 
502
920
  /** On-disk completion marker for a built dependency, shared across processes. */
@@ -522,15 +940,7 @@ function ensureProjectBuilt(tsconfig: string): BuiltProject {
522
940
  if (cached !== undefined) {
523
941
  return cached;
524
942
  }
525
- const key = crypto
526
- .createHash("sha256")
527
- .update(tsconfig)
528
- .digest("hex")
529
- .slice(0, 16);
530
- const root = dependencyCacheRoot();
531
- const emitDir = path.join(root, key);
532
- const metaPath = path.join(root, `${key}.json`);
533
- const lockDir = path.join(root, `${key}.lock`);
943
+ const { emitDir, lockDir, metaPath, root } = dependencyCachePaths(tsconfig);
534
944
 
535
945
  const reuse = readDependencyCache(emitDir, metaPath);
536
946
  if (reuse !== null) {
@@ -546,6 +956,28 @@ function ensureProjectBuilt(tsconfig: string): BuiltProject {
546
956
  return built;
547
957
  }
548
958
 
959
+ interface DependencyCachePaths {
960
+ emitDir: string;
961
+ lockDir: string;
962
+ metaPath: string;
963
+ root: string;
964
+ }
965
+
966
+ function dependencyCachePaths(tsconfig: string): DependencyCachePaths {
967
+ const key = crypto
968
+ .createHash("sha256")
969
+ .update(tsconfig)
970
+ .digest("hex")
971
+ .slice(0, 16);
972
+ const root = dependencyCacheRoot();
973
+ return {
974
+ emitDir: path.join(root, key),
975
+ lockDir: path.join(root, `${key}.lock`),
976
+ metaPath: path.join(root, `${key}.json`),
977
+ root,
978
+ };
979
+ }
980
+
549
981
  /** Reuse a dependency another process (or an earlier import) already built. */
550
982
  function readDependencyCache(
551
983
  emitDir: string,
@@ -643,7 +1075,12 @@ function buildDependency(
643
1075
  // can itself depend on a transform (e.g. a fixture whose values are built
644
1076
  // with `typia.createRandom`), and its runtime behaviour is wrong without it.
645
1077
  // `runBuild` runs on this main thread, so its plugin resolution works the
646
- // same as the entry build's.
1078
+ // same as the entry build's. The exception is loading a plugin descriptor
1079
+ // (`TTSC_PLUGIN_DESCRIPTOR_LOAD`): there the descriptor's own — possibly
1080
+ // self-hosting — transform must NOT run, or it re-enters plugin loading and
1081
+ // deadlocks, so every dependency in that graph builds with plugins off.
1082
+ plugins:
1083
+ process.env.TTSC_PLUGIN_DESCRIPTOR_LOAD === "1" ? false : undefined,
647
1084
  quiet: true,
648
1085
  // Emit only: the entry project's up-front check is the type gate. A
649
1086
  // dependency build pulls its own transitive sources into the program and
@@ -677,14 +1114,17 @@ function buildDependency(
677
1114
  typeof project.compilerOptions.module === "string"
678
1115
  ? project.compilerOptions.module
679
1116
  : undefined;
680
- fs.writeFileSync(
681
- metaPath,
682
- JSON.stringify({ moduleOption, rootDir } satisfies DependencyCacheMeta),
683
- "utf8",
684
- );
1117
+ writeDependencyMeta(metaPath, { moduleOption, rootDir });
685
1118
  return { emitDir, emittedFiles: undefined, moduleOption, rootDir };
686
1119
  }
687
1120
 
1121
+ function writeDependencyMeta(
1122
+ metaPath: string,
1123
+ meta: DependencyCacheMeta,
1124
+ ): void {
1125
+ fs.writeFileSync(metaPath, JSON.stringify(meta), "utf8");
1126
+ }
1127
+
688
1128
  function dependencyCacheRoot(): string {
689
1129
  const m = manifest();
690
1130
  return m !== null && m.depCacheDir.length !== 0
@@ -694,6 +1134,7 @@ function dependencyCacheRoot(): string {
694
1134
 
695
1135
  /** Owning-tsconfig cache keyed by directory, mirroring `packageTypeCache`. */
696
1136
  const tsconfigCache = new Map<string, string | null>();
1137
+ const moduleOptionCache = new Map<string, string | undefined>();
697
1138
 
698
1139
  /**
699
1140
  * The nearest `tsconfig.json` at or above `file`'s directory, or `null`. The