@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.
- package/lib/createTtscPlugin.d.ts +22 -0
- package/lib/createTtscPlugin.js +179 -19
- package/lib/createTtscPlugin.js.map +1 -1
- package/linthost/config.go +79 -1
- package/linthost/loader_canonical_directory_other.go +34 -0
- package/linthost/loader_canonical_directory_windows.go +60 -0
- package/linthost/loader_identity_temp_base_posix_test.go +117 -0
- package/linthost/windows_loader_identity_temp_base_preserves_config_directory_test.go +212 -0
- package/package.json +2 -2
- package/src/createTtscPlugin.ts +199 -22
package/src/createTtscPlugin.ts
CHANGED
|
@@ -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 =
|
|
2017
|
-
|
|
2018
|
-
|
|
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
|
-
*
|
|
2741
|
-
*
|
|
2742
|
-
*
|
|
2743
|
-
*
|
|
2744
|
-
*
|
|
2745
|
-
* the
|
|
2746
|
-
*
|
|
2747
|
-
*
|
|
2748
|
-
*
|
|
2749
|
-
*
|
|
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
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
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
|
-
|
|
2759
|
-
|
|
2869
|
+
independentEvaluation &&
|
|
2870
|
+
systemTempOnConfigVolume &&
|
|
2871
|
+
systemTempExternalAncestor &&
|
|
2872
|
+
!recordRoot
|
|
2760
2873
|
) {
|
|
2761
|
-
|
|
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");
|