@ttsc/lint 0.33.0-next.1 → 0.33.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.
@@ -2013,9 +2013,11 @@ function evaluateTtsxConfigPlugins(
2013
2013
  ): ConfigPluginEvaluation {
2014
2014
  const existing = evaluated.get(configPath);
2015
2015
  if (existing !== undefined) return existing;
2016
- const tempDir = createCanonicalTempDirectory(
2017
- "ttsc-lint-cfg-",
2018
- loaderTempBase(configPath),
2016
+ const tempDir = createConfigLoaderTempDirectory(
2017
+ configPath,
2018
+ os.tmpdir(),
2019
+ process.env.TTSC_PLUGIN_RECORD_ROOT,
2020
+ context.projectRoot,
2019
2021
  );
2020
2022
  try {
2021
2023
  linkNearestNodeModules(tempDir, path.dirname(configPath));
@@ -2737,29 +2739,204 @@ function linkNearestNodeModules(tempDir: string, sourceDir: string): void {
2737
2739
  }
2738
2740
 
2739
2741
  /**
2740
- * Picks the parent directory for the ephemeral config-loader tree. The system
2741
- * temp dir is the default, but when it sits on a different volume than the
2742
- * config file (Windows: TEMP on `C:`, project on `D:`) the loader cannot work
2743
- * from there — no single tsconfig `rootDir` spans two volumes and
2744
- * `path.relative` cannot produce a relative import across drives (#305) — so
2745
- * the tree is created under the config's nearest `node_modules/.cache` instead,
2746
- * falling back to the config's own directory when no `node_modules` exists (or
2747
- * its `.cache` cannot be created): any location on the config's volume beats
2748
- * the system temp dir, which is guaranteed to fail. Keeps the system temp dir
2749
- * when the volumes already match.
2742
+ * Creates the typed config evaluator below a stable same-volume cache directory
2743
+ * when a descriptor load has already opened one. The identity directory exists
2744
+ * before descriptor imports are observed, so creating and removing an evaluator
2745
+ * below it leaves the project directory's missing-path witness unchanged. A
2746
+ * moved, aliased, unwritable, or differently mounted identity directory uses
2747
+ * the established loader placement instead. TypeScript-Go needs the generated
2748
+ * loader and the lexical config path on one volume (#305), so a TEMP junction
2749
+ * cannot override the volume of the canonical loader path. Without a suitable
2750
+ * cache, an existing user temp directory outside the config tree is preferred
2751
+ * to writing beside the config. A TEMP path in or above the declared project or
2752
+ * config directory yields to an existing identity store so observed ancestor
2753
+ * metadata remains stable. An independent config evaluation without descriptor
2754
+ * input capture may retain a strict external ancestor as its historical system
2755
+ * temp. No safe same-volume directory is an error.
2756
+ *
2757
+ * @evidence contracts/common.md#principled-implementation Native realpath and the returned canonical child must share the config's lexical volume; a rejected empty child is removed by its physical path. A system temp overlapping a physical project or config ancestor yields to an authoritative identity store, and descriptor-load children retain conservative ancestor handling. Descriptor identities need physical child and config-ancestor checks, and alternate user temp candidates must already exist outside the config tree.
2758
+ * @evidence contracts/common.md#clear-and-simple-design This function owns placement, while the existing canonical creator and nearest-cache selector retain their respective creation and discovery responsibilities.
2759
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts Selection uses the descriptor's actual cache root and native filesystem identity, and every accepted fallback satisfies the compiler's lexical-volume requirement without returning a mutable junction spelling.
2760
+ * @evidence contracts/common.md#meaningful-documentation The comment explains the preexisting cache preference, TEMP junction and ancestor handling, and explicit failure when no usable same-volume directory exists.
2750
2761
  */
2751
- function loaderTempBase(configPath: string): string {
2752
- const systemTemp = os.tmpdir();
2753
- const systemRoot = path.parse(systemTemp).root;
2754
- const configRoot = path.parse(configPath).root;
2755
- // A relative config path has no root; "" must not be read as "a volume
2756
- // other than the system temp's" — it keeps the historical default.
2762
+ export function createConfigLoaderTempDirectory(
2763
+ configPath: string,
2764
+ systemTemp: string = os.tmpdir(),
2765
+ recordRoot: string | undefined = process.env.TTSC_PLUGIN_RECORD_ROOT,
2766
+ projectRoot?: string,
2767
+ ): string {
2768
+ const prefix = "ttsc-lint-cfg-";
2769
+ const configVolume = path.parse(configPath).root;
2770
+ if (configVolume === "")
2771
+ return createCanonicalTempDirectory(prefix, systemTemp);
2772
+
2773
+ const configDir = path.dirname(configPath);
2774
+ const physicalConfigDir = fs.realpathSync.native(configDir);
2775
+ let physicalProjectRoot: string | undefined = physicalConfigDir;
2776
+ if (projectRoot?.trim()) {
2777
+ try {
2778
+ physicalProjectRoot = fs.realpathSync.native(path.resolve(projectRoot));
2779
+ } catch {
2780
+ physicalProjectRoot = undefined;
2781
+ }
2782
+ }
2783
+ const contains = (ancestor: string, other: string): boolean => {
2784
+ const relative = path.relative(ancestor, other);
2785
+ return (
2786
+ relative === "" ||
2787
+ (relative !== ".." &&
2788
+ !relative.startsWith(`..${path.sep}`) &&
2789
+ !path.isAbsolute(relative))
2790
+ );
2791
+ };
2792
+ const unavailable = (error: unknown): boolean =>
2793
+ ["EACCES", "EPERM", "ENOENT", "ENOTDIR", "EROFS"].includes(
2794
+ (error as NodeJS.ErrnoException).code ?? "",
2795
+ );
2796
+ let physicalSystemTemp: string | undefined;
2797
+ try {
2798
+ physicalSystemTemp = fs.realpathSync.native(systemTemp);
2799
+ } catch {
2800
+ // An unavailable system temp can be replaced by an existing safe owner.
2801
+ }
2802
+ const createOnConfigVolume = (parent: string): string | undefined => {
2803
+ let directory: string;
2804
+ try {
2805
+ directory = createCanonicalTempDirectory(prefix, parent);
2806
+ } catch (error) {
2807
+ if (unavailable(error)) return undefined;
2808
+ throw error;
2809
+ }
2810
+ if (
2811
+ path.parse(directory).root.toLowerCase() === configVolume.toLowerCase()
2812
+ ) {
2813
+ return directory;
2814
+ }
2815
+ fs.rmdirSync(directory);
2816
+ return undefined;
2817
+ };
2818
+ const systemTempOnConfigVolume =
2819
+ physicalSystemTemp !== undefined &&
2820
+ path.parse(physicalSystemTemp).root.toLowerCase() ===
2821
+ configVolume.toLowerCase();
2822
+ const systemTempInsideProjectOrConfig =
2823
+ physicalSystemTemp !== undefined &&
2824
+ (physicalProjectRoot === undefined ||
2825
+ contains(physicalProjectRoot, physicalSystemTemp) ||
2826
+ contains(physicalConfigDir, physicalSystemTemp));
2827
+ const systemTempExternalAncestor =
2828
+ physicalSystemTemp !== undefined &&
2829
+ !systemTempInsideProjectOrConfig &&
2830
+ ((physicalProjectRoot !== undefined &&
2831
+ contains(physicalSystemTemp, physicalProjectRoot)) ||
2832
+ contains(physicalSystemTemp, physicalConfigDir));
2833
+ const independentEvaluation = process.env.TTSC_PLUGIN_DESCRIPTOR_LOAD !== "1";
2834
+ if (
2835
+ systemTempOnConfigVolume &&
2836
+ !systemTempInsideProjectOrConfig &&
2837
+ !systemTempExternalAncestor
2838
+ ) {
2839
+ const directory = createOnConfigVolume(systemTemp);
2840
+ if (directory !== undefined) return directory;
2841
+ }
2842
+
2843
+ let preferred: string | undefined;
2844
+ if (recordRoot && path.isAbsolute(recordRoot)) {
2845
+ try {
2846
+ const physicalRoot = fs.realpathSync.native(recordRoot);
2847
+ const identityPath = path.join(recordRoot, "identities");
2848
+ const identityEntry = fs.lstatSync(identityPath);
2849
+ const physicalIdentity = fs.realpathSync.native(identityPath);
2850
+ if (
2851
+ identityEntry.isDirectory() &&
2852
+ !identityEntry.isSymbolicLink() &&
2853
+ path.dirname(physicalIdentity) === physicalRoot &&
2854
+ path.parse(physicalIdentity).root.toLowerCase() ===
2855
+ configVolume.toLowerCase() &&
2856
+ !contains(physicalIdentity, physicalConfigDir)
2857
+ ) {
2858
+ preferred = physicalIdentity;
2859
+ }
2860
+ } catch {
2861
+ // The optional cache placement is unavailable; keep the established one.
2862
+ }
2863
+ }
2864
+ if (preferred !== undefined) {
2865
+ const directory = createOnConfigVolume(preferred);
2866
+ if (directory !== undefined) return directory;
2867
+ }
2757
2868
  if (
2758
- configRoot === "" ||
2759
- systemRoot.toLowerCase() === configRoot.toLowerCase()
2869
+ independentEvaluation &&
2870
+ systemTempOnConfigVolume &&
2871
+ systemTempExternalAncestor &&
2872
+ !recordRoot
2760
2873
  ) {
2761
- return systemTemp;
2874
+ const directory = createOnConfigVolume(systemTemp);
2875
+ if (directory !== undefined) return directory;
2876
+ }
2877
+
2878
+ const nearest = loaderTempBase(configPath);
2879
+ const ambient = [
2880
+ process.env.TMPDIR,
2881
+ process.env.TMP,
2882
+ process.env.TEMP,
2883
+ process.env.LOCALAPPDATA
2884
+ ? path.join(process.env.LOCALAPPDATA, "Temp")
2885
+ : undefined,
2886
+ ];
2887
+ const candidates = nearest === configDir ? ambient : [nearest, ...ambient];
2888
+ const seen = new Set<string>();
2889
+ for (const candidate of candidates) {
2890
+ if (!candidate || !path.isAbsolute(candidate)) continue;
2891
+ let physical: string;
2892
+ try {
2893
+ physical = fs.realpathSync.native(candidate);
2894
+ if (!fs.lstatSync(physical).isDirectory()) continue;
2895
+ } catch {
2896
+ continue;
2897
+ }
2898
+ if (seen.has(physical)) continue;
2899
+ seen.add(physical);
2900
+ if (path.parse(physical).root.toLowerCase() !== configVolume.toLowerCase())
2901
+ continue;
2902
+ if (
2903
+ (nearest === configDir || candidate !== nearest) &&
2904
+ (contains(physical, physicalConfigDir) ||
2905
+ contains(physicalConfigDir, physical))
2906
+ ) {
2907
+ continue;
2908
+ }
2909
+ const directory = createOnConfigVolume(physical);
2910
+ if (directory !== undefined) return directory;
2911
+ }
2912
+ if (
2913
+ independentEvaluation &&
2914
+ systemTempOnConfigVolume &&
2915
+ systemTempExternalAncestor &&
2916
+ recordRoot
2917
+ ) {
2918
+ const directory = createOnConfigVolume(systemTemp);
2919
+ if (directory !== undefined) return directory;
2762
2920
  }
2921
+ if (
2922
+ path.parse(physicalConfigDir).root.toLowerCase() ===
2923
+ configVolume.toLowerCase()
2924
+ ) {
2925
+ const directory = createOnConfigVolume(configDir);
2926
+ if (directory !== undefined) return directory;
2927
+ }
2928
+ throw new Error(
2929
+ `@ttsc/lint: no writable temporary directory on ${configVolume} for config ${configPath}; set TEMP, TMP, or TMPDIR to an existing directory on that drive or enable a same-drive ttsc cache`,
2930
+ );
2931
+ }
2932
+
2933
+ /**
2934
+ * Finds the nearest node_modules cache, falling back to the config directory.
2935
+ * The caller checks the canonical drive before using either path because a
2936
+ * config junction can place that directory on another drive (#305).
2937
+ */
2938
+ function loaderTempBase(configPath: string): string {
2939
+ const configRoot = path.parse(configPath).root;
2763
2940
  const nodeModules = findNearestNodeModules(path.dirname(configPath));
2764
2941
  if (!nodeModules) return path.dirname(configPath);
2765
2942
  const base = path.join(nodeModules, ".cache");