Skip to content

Commit 793bfe3

Browse files
arcanismerceyz
authored andcommitted
feat: add Yarn PnP support
1 parent a10409c commit 793bfe3

10 files changed

+347
-36
lines changed

src/compiler/moduleNameResolver.ts

+127-9
Original file line numberDiff line numberDiff line change
@@ -260,26 +260,66 @@ namespace ts {
260260

261261
/**
262262
* Returns the path to every node_modules/@types directory from some ancestor directory.
263-
* Returns undefined if there are none.
264263
*/
265-
function getDefaultTypeRoots(currentDirectory: string, host: { directoryExists?: (directoryName: string) => boolean }): string[] | undefined {
264+
function getNodeModulesTypeRoots(currentDirectory: string, host: { directoryExists?: (directoryName: string) => boolean }) {
266265
if (!host.directoryExists) {
267266
return [combinePaths(currentDirectory, nodeModulesAtTypes)];
268267
// And if it doesn't exist, tough.
269268
}
270269

271-
let typeRoots: string[] | undefined;
270+
const typeRoots: string[] = [];
272271
forEachAncestorDirectory(normalizePath(currentDirectory), directory => {
273272
const atTypes = combinePaths(directory, nodeModulesAtTypes);
274273
if (host.directoryExists!(atTypes)) {
275-
(typeRoots || (typeRoots = [])).push(atTypes);
274+
typeRoots.push(atTypes);
276275
}
277276
return undefined;
278277
});
278+
279279
return typeRoots;
280280
}
281281
const nodeModulesAtTypes = combinePaths("node_modules", "@types");
282282

283+
export function getPnpTypeRoots(currentDirectory: string) {
284+
const pnpapi = getPnpApi(currentDirectory);
285+
if (!pnpapi) {
286+
return [];
287+
}
288+
289+
// Some TS consumers pass relative paths that aren't normalized
290+
currentDirectory = sys.resolvePath(currentDirectory);
291+
292+
const currentPackage = pnpapi.findPackageLocator(`${currentDirectory}/`);
293+
if (!currentPackage) {
294+
return [];
295+
}
296+
297+
const {packageDependencies} = pnpapi.getPackageInformation(currentPackage);
298+
299+
const typeRoots: string[] = [];
300+
for (const [name, referencish] of Array.from<any>(packageDependencies.entries())) {
301+
// eslint-disable-next-line no-null/no-null
302+
if (name.startsWith(typesPackagePrefix) && referencish !== null) {
303+
const dependencyLocator = pnpapi.getLocator(name, referencish);
304+
const {packageLocation} = pnpapi.getPackageInformation(dependencyLocator);
305+
306+
typeRoots.push(getDirectoryPath(packageLocation));
307+
}
308+
}
309+
310+
return typeRoots;
311+
}
312+
const typesPackagePrefix = "@types/";
313+
314+
function getDefaultTypeRoots(currentDirectory: string, host: { directoryExists?: (directoryName: string) => boolean }): string[] | undefined {
315+
const nmTypes = getNodeModulesTypeRoots(currentDirectory, host);
316+
const pnpTypes = getPnpTypeRoots(currentDirectory);
317+
318+
if (nmTypes.length > 0 || pnpTypes.length > 0) {
319+
return [...nmTypes, ...pnpTypes];
320+
}
321+
}
322+
283323
/**
284324
* @param {string | undefined} containingFile - file that contains type reference directive, can be undefined if containing file is unknown.
285325
* This is possible in case if resolution is performed for directives specified via 'types' parameter. In this case initial path for secondary lookups
@@ -400,7 +440,10 @@ namespace ts {
400440
}
401441
let result: Resolved | undefined;
402442
if (!isExternalModuleNameRelative(typeReferenceDirectiveName)) {
403-
const searchResult = loadModuleFromNearestNodeModulesDirectory(Extensions.DtsOnly, typeReferenceDirectiveName, initialLocationForSecondaryLookup, moduleResolutionState, /*cache*/ undefined, /*redirectedReference*/ undefined);
443+
const searchResult = getPnpApi(initialLocationForSecondaryLookup)
444+
? tryLoadModuleUsingPnpResolution(Extensions.DtsOnly, typeReferenceDirectiveName, initialLocationForSecondaryLookup, moduleResolutionState)
445+
: loadModuleFromNearestNodeModulesDirectory(Extensions.DtsOnly, typeReferenceDirectiveName, initialLocationForSecondaryLookup, moduleResolutionState, /*cache*/ undefined, /*redirectedReference*/ undefined);
446+
404447
result = searchResult && searchResult.value;
405448
}
406449
else {
@@ -1117,8 +1160,14 @@ namespace ts {
11171160
if (traceEnabled) {
11181161
trace(host, Diagnostics.Loading_module_0_from_node_modules_folder_target_file_type_1, moduleName, Extensions[extensions]);
11191162
}
1120-
const resolved = loadModuleFromNearestNodeModulesDirectory(extensions, moduleName, containingDirectory, state, cache, redirectedReference);
1121-
if (!resolved) return undefined;
1163+
1164+
const resolved = getPnpApi(containingDirectory)
1165+
? tryLoadModuleUsingPnpResolution(extensions, moduleName, containingDirectory, state)
1166+
: loadModuleFromNearestNodeModulesDirectory(extensions, moduleName, containingDirectory, state, cache, redirectedReference);
1167+
1168+
if (!resolved) {
1169+
return undefined;
1170+
}
11221171

11231172
let resolvedValue = resolved.value;
11241173
if (!compilerOptions.preserveSymlinks && resolvedValue && !resolvedValue.originalPath) {
@@ -1497,7 +1546,15 @@ namespace ts {
14971546

14981547
function loadModuleFromSpecificNodeModulesDirectory(extensions: Extensions, moduleName: string, nodeModulesDirectory: string, nodeModulesDirectoryExists: boolean, state: ModuleResolutionState): Resolved | undefined {
14991548
const candidate = normalizePath(combinePaths(nodeModulesDirectory, moduleName));
1549+
return loadModuleFromSpecificNodeModulesDirectoryImpl(extensions, moduleName, nodeModulesDirectory, nodeModulesDirectoryExists, state, candidate, undefined, undefined);
1550+
}
15001551

1552+
function loadModuleFromPnpResolution(extensions: Extensions, packageDirectory: string, rest: string, state: ModuleResolutionState): Resolved | undefined {
1553+
const candidate = normalizePath(combinePaths(packageDirectory, rest));
1554+
return loadModuleFromSpecificNodeModulesDirectoryImpl(extensions, undefined, undefined, true, state, candidate, rest, packageDirectory);
1555+
}
1556+
1557+
function loadModuleFromSpecificNodeModulesDirectoryImpl(extensions: Extensions, moduleName: string | undefined, nodeModulesDirectory: string | undefined, nodeModulesDirectoryExists: boolean, state: ModuleResolutionState, candidate: string, rest: string | undefined, packageDirectory: string | undefined): Resolved | undefined {
15011558
// First look for a nested package.json, as in `node_modules/foo/bar/package.json`.
15021559
let packageInfo = getPackageJsonInfo(candidate, !nodeModulesDirectoryExists, state);
15031560
if (packageInfo) {
@@ -1531,9 +1588,10 @@ namespace ts {
15311588
return withPackageId(packageInfo, pathAndExtension);
15321589
};
15331590

1534-
const { packageName, rest } = parsePackageName(moduleName);
1591+
let packageName: string;
1592+
if (rest === undefined) ({ packageName, rest } = parsePackageName(moduleName!));
15351593
if (rest !== "") { // If "rest" is empty, we just did this search above.
1536-
const packageDirectory = combinePaths(nodeModulesDirectory, packageName);
1594+
if (packageDirectory === undefined) packageDirectory = combinePaths(nodeModulesDirectory!, packageName!);
15371595

15381596
// Don't use a "types" or "main" from here because we're not loading the root, but a subdirectory -- just here for the packageId and path mappings.
15391597
packageInfo = getPackageJsonInfo(packageDirectory, !nodeModulesDirectoryExists, state);
@@ -1713,4 +1771,64 @@ namespace ts {
17131771
function toSearchResult<T>(value: T | undefined): SearchResult<T> {
17141772
return value !== undefined ? { value } : undefined;
17151773
}
1774+
1775+
/**
1776+
* We only allow PnP to be used as a resolution strategy if TypeScript
1777+
* itself is executed under a PnP runtime (and we only allow it to access
1778+
* the current PnP runtime, not any on the disk). This ensures that we
1779+
* don't execute potentially malicious code that didn't already have a
1780+
* chance to be executed (if we're running within the runtime, it means
1781+
* that the runtime has already been executed).
1782+
* @internal
1783+
*/
1784+
function getPnpApi(path: string) {
1785+
const {findPnpApi} = require("module");
1786+
if (findPnpApi === undefined) {
1787+
return undefined;
1788+
}
1789+
return findPnpApi(`${path}/`);
1790+
}
1791+
1792+
function loadPnpPackageResolution(packageName: string, containingDirectory: string) {
1793+
try {
1794+
const resolution = getPnpApi(containingDirectory).resolveToUnqualified(packageName, `${containingDirectory}/`, { considerBuiltins: false });
1795+
return normalizeSlashes(resolution);
1796+
}
1797+
catch {
1798+
// Nothing to do
1799+
}
1800+
}
1801+
1802+
function loadPnpTypePackageResolution(packageName: string, containingDirectory: string) {
1803+
return loadPnpPackageResolution(getTypesPackageName(packageName), containingDirectory);
1804+
}
1805+
1806+
/* @internal */
1807+
function tryLoadModuleUsingPnpResolution(extensions: Extensions, moduleName: string, containingDirectory: string, state: ModuleResolutionState) {
1808+
const {packageName, rest} = parsePackageName(moduleName);
1809+
1810+
const packageResolution = loadPnpPackageResolution(packageName, containingDirectory);
1811+
const packageFullResolution = packageResolution
1812+
? loadModuleFromPnpResolution(extensions, packageResolution, rest, state)
1813+
: undefined;
1814+
1815+
let resolved;
1816+
if (packageFullResolution) {
1817+
resolved = packageFullResolution;
1818+
}
1819+
else if (extensions === Extensions.TypeScript || extensions === Extensions.DtsOnly) {
1820+
const typePackageResolution = loadPnpTypePackageResolution(packageName, containingDirectory);
1821+
const typePackageFullResolution = typePackageResolution
1822+
? loadModuleFromPnpResolution(Extensions.DtsOnly, typePackageResolution, rest, state)
1823+
: undefined;
1824+
1825+
if (typePackageFullResolution) {
1826+
resolved = typePackageFullResolution;
1827+
}
1828+
}
1829+
1830+
if (resolved) {
1831+
return toSearchResult(resolved);
1832+
}
1833+
}
17161834
}

src/compiler/moduleSpecifiers.ts

+53-12
Original file line numberDiff line numberDiff line change
@@ -553,7 +553,33 @@ namespace ts.moduleSpecifiers {
553553
if (!host.fileExists || !host.readFile) {
554554
return undefined;
555555
}
556-
const parts: NodeModulePathParts = getNodeModulePathParts(path)!;
556+
let parts: NodeModulePathParts | PackagePathParts | undefined
557+
= getNodeModulePathParts(path);
558+
559+
let packageName: string | undefined;
560+
if (!parts && typeof process.versions.pnp !== "undefined") {
561+
const pnpApi = require("pnpapi");
562+
const locator = pnpApi.findPackageLocator(path);
563+
// eslint-disable-next-line no-null/no-null
564+
if (locator !== null) {
565+
const sourceLocator = pnpApi.findPackageLocator(`${sourceDirectory}/`);
566+
// Don't use the package name when the imported file is inside
567+
// the source directory (prefer a relative path instead)
568+
if (locator === sourceLocator) {
569+
return undefined;
570+
}
571+
const information = pnpApi.getPackageInformation(locator);
572+
packageName = locator.name;
573+
parts = {
574+
topLevelNodeModulesIndex: undefined,
575+
topLevelPackageNameIndex: undefined,
576+
// The last character from packageLocation is the trailing "/", we want to point to it
577+
packageRootIndex: information.packageLocation.length - 1,
578+
fileNameIndex: path.lastIndexOf(`/`),
579+
};
580+
}
581+
}
582+
557583
if (!parts) {
558584
return undefined;
559585
}
@@ -588,19 +614,26 @@ namespace ts.moduleSpecifiers {
588614
return undefined;
589615
}
590616

591-
const globalTypingsCacheLocation = host.getGlobalTypingsCacheLocation && host.getGlobalTypingsCacheLocation();
592-
// Get a path that's relative to node_modules or the importing file's path
593-
// if node_modules folder is in this folder or any of its parent folders, no need to keep it.
594-
const pathToTopLevelNodeModules = getCanonicalFileName(moduleSpecifier.substring(0, parts.topLevelNodeModulesIndex));
595-
if (!(startsWith(sourceDirectory, pathToTopLevelNodeModules) || globalTypingsCacheLocation && startsWith(getCanonicalFileName(globalTypingsCacheLocation), pathToTopLevelNodeModules))) {
596-
return undefined;
617+
// If PnP is enabled the node_modules entries we'll get will always be relevant even if they
618+
// are located in a weird path apparently outside of the source directory
619+
if (typeof process.versions.pnp === "undefined") {
620+
const globalTypingsCacheLocation = host.getGlobalTypingsCacheLocation && host.getGlobalTypingsCacheLocation();
621+
// Get a path that's relative to node_modules or the importing file's path
622+
// if node_modules folder is in this folder or any of its parent folders, no need to keep it.
623+
const pathToTopLevelNodeModules = getCanonicalFileName(moduleSpecifier.substring(0, parts.topLevelNodeModulesIndex));
624+
if (!(startsWith(sourceDirectory, pathToTopLevelNodeModules) || globalTypingsCacheLocation && startsWith(getCanonicalFileName(globalTypingsCacheLocation), pathToTopLevelNodeModules))) {
625+
return undefined;
626+
}
597627
}
598628

599629
// If the module was found in @types, get the actual Node package name
600-
const nodeModulesDirectoryName = moduleSpecifier.substring(parts.topLevelPackageNameIndex + 1);
601-
const packageName = getPackageNameFromTypesPackageName(nodeModulesDirectoryName);
630+
const nodeModulesDirectoryName = typeof packageName !== "undefined"
631+
? packageName + moduleSpecifier.substring(parts.packageRootIndex)
632+
: moduleSpecifier.substring(parts.topLevelPackageNameIndex! + 1);
633+
634+
const packageNameFromPath = getPackageNameFromTypesPackageName(nodeModulesDirectoryName);
602635
// For classic resolution, only allow importing from node_modules/@types, not other node_modules
603-
return getEmitModuleResolutionKind(options) !== ModuleResolutionKind.NodeJs && packageName === nodeModulesDirectoryName ? undefined : packageName;
636+
return getEmitModuleResolutionKind(options) !== ModuleResolutionKind.NodeJs && packageNameFromPath === nodeModulesDirectoryName ? undefined : packageNameFromPath;
604637

605638
function tryDirectoryWithPackageJson(packageRootIndex: number) {
606639
const packageRootPath = path.substring(0, packageRootIndex);
@@ -641,8 +674,8 @@ namespace ts.moduleSpecifiers {
641674

642675
// If the file is /index, it can be imported by its directory name
643676
// IFF there is not _also_ a file by the same name
644-
if (getCanonicalFileName(fullModulePathWithoutExtension.substring(parts.fileNameIndex)) === "/index" && !tryGetAnyFileFromPath(host, fullModulePathWithoutExtension.substring(0, parts.fileNameIndex))) {
645-
return fullModulePathWithoutExtension.substring(0, parts.fileNameIndex);
677+
if (getCanonicalFileName(fullModulePathWithoutExtension.substring(parts!.fileNameIndex)) === "/index" && !tryGetAnyFileFromPath(host, fullModulePathWithoutExtension.substring(0, parts!.fileNameIndex))) {
678+
return fullModulePathWithoutExtension.substring(0, parts!.fileNameIndex);
646679
}
647680

648681
return fullModulePathWithoutExtension;
@@ -667,6 +700,14 @@ namespace ts.moduleSpecifiers {
667700
readonly packageRootIndex: number;
668701
readonly fileNameIndex: number;
669702
}
703+
704+
interface PackagePathParts {
705+
readonly topLevelNodeModulesIndex: undefined;
706+
readonly topLevelPackageNameIndex: undefined;
707+
readonly packageRootIndex: number;
708+
readonly fileNameIndex: number;
709+
}
710+
670711
function getNodeModulePathParts(fullPath: string): NodeModulePathParts | undefined {
671712
// If fullPath can't be valid module file within node_modules, returns undefined.
672713
// Example of expected pattern: /base/path/node_modules/[@scope/otherpackage/@otherscope/node_modules/]package/[subdirectory/]file.js

src/compiler/sys.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1513,6 +1513,11 @@ namespace ts {
15131513
}
15141514

15151515
function isFileSystemCaseSensitive(): boolean {
1516+
// The PnP runtime is always case-sensitive
1517+
// @ts-ignore
1518+
if (process.versions.pnp) {
1519+
return true;
1520+
}
15161521
// win32\win64 are case insensitive platforms
15171522
if (platform === "win32" || platform === "win64") {
15181523
return false;

0 commit comments

Comments
 (0)