Skip to content

Commit 9fb5c1c

Browse files
arcanismerceyz
authored andcommittedNov 1, 2023
feat: add Yarn PnP support
1 parent 88f80c7 commit 9fb5c1c

14 files changed

+477
-44
lines changed
 

‎.github/workflows/ci.yml

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ on:
55
branches:
66
- main
77
- release-*
8+
- '*/pnp-*'
89
pull_request:
910
branches:
1011
- main

‎src/compiler/moduleNameResolver.ts

+81-2
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ import {
110110
versionMajorMinor,
111111
VersionRange,
112112
} from "./_namespaces/ts";
113+
import {
114+
getPnpApi,
115+
getPnpTypeRoots,
116+
} from "./pnp";
113117

114118
/** @internal */
115119
export function trace(host: ModuleResolutionHost, message: DiagnosticMessage, ...args: any[]): void {
@@ -490,7 +494,7 @@ export function getEffectiveTypeRoots(options: CompilerOptions, host: GetEffecti
490494
* Returns the path to every node_modules/@types directory from some ancestor directory.
491495
* Returns undefined if there are none.
492496
*/
493-
function getDefaultTypeRoots(currentDirectory: string): string[] | undefined {
497+
function getNodeModulesTypeRoots(currentDirectory: string) {
494498
let typeRoots: string[] | undefined;
495499
forEachAncestorDirectory(normalizePath(currentDirectory), directory => {
496500
const atTypes = combinePaths(directory, nodeModulesAtTypes);
@@ -505,6 +509,18 @@ function arePathsEqual(path1: string, path2: string, host: ModuleResolutionHost)
505509
return comparePaths(path1, path2, !useCaseSensitiveFileNames) === Comparison.EqualTo;
506510
}
507511

512+
function getDefaultTypeRoots(currentDirectory: string): string[] | undefined {
513+
const nmTypes = getNodeModulesTypeRoots(currentDirectory);
514+
const pnpTypes = getPnpTypeRoots(currentDirectory);
515+
516+
if (nmTypes?.length) {
517+
return [...nmTypes, ...pnpTypes];
518+
}
519+
else if (pnpTypes.length) {
520+
return pnpTypes;
521+
}
522+
}
523+
508524
function getOriginalAndResolvedFileName(fileName: string, host: ModuleResolutionHost, traceEnabled: boolean) {
509525
const resolvedFileName = realPath(fileName, host, traceEnabled);
510526
const pathsAreEqual = arePathsEqual(fileName, resolvedFileName, host);
@@ -784,6 +800,18 @@ export function resolvePackageNameToPackageJson(
784800
): PackageJsonInfo | undefined {
785801
const moduleResolutionState = getTemporaryModuleResolutionState(cache?.getPackageJsonInfoCache(), host, options);
786802

803+
const pnpapi = getPnpApi(containingDirectory);
804+
if (pnpapi) {
805+
try {
806+
const resolution = pnpapi.resolveToUnqualified(packageName, `${containingDirectory}/`, { considerBuiltins: false });
807+
const candidate = normalizeSlashes(resolution).replace(/\/$/, "");
808+
return getPackageJsonInfo(candidate, /*onlyRecordFailures*/ false, moduleResolutionState);
809+
}
810+
catch {
811+
return;
812+
}
813+
}
814+
787815
return forEachAncestorDirectory(containingDirectory, ancestorDirectory => {
788816
if (getBaseFileName(ancestorDirectory) !== "node_modules") {
789817
const nodeModulesFolder = combinePaths(ancestorDirectory, "node_modules");
@@ -2963,7 +2991,16 @@ function loadModuleFromNearestNodeModulesDirectoryWorker(extensions: Extensions,
29632991
}
29642992

29652993
function lookup(extensions: Extensions) {
2966-
return forEachAncestorDirectory(normalizeSlashes(directory), ancestorDirectory => {
2994+
const issuer = normalizeSlashes(directory);
2995+
if (getPnpApi(issuer)) {
2996+
const resolutionFromCache = tryFindNonRelativeModuleNameInCache(cache, moduleName, mode, issuer, redirectedReference, state);
2997+
if (resolutionFromCache) {
2998+
return resolutionFromCache;
2999+
}
3000+
return toSearchResult(loadModuleFromImmediateNodeModulesDirectoryPnP(extensions, moduleName, issuer, state, typesScopeOnly, cache, redirectedReference));
3001+
}
3002+
3003+
return forEachAncestorDirectory(issuer, ancestorDirectory => {
29673004
if (getBaseFileName(ancestorDirectory) !== "node_modules") {
29683005
const resolutionFromCache = tryFindNonRelativeModuleNameInCache(cache, moduleName, mode, ancestorDirectory, redirectedReference, state);
29693006
if (resolutionFromCache) {
@@ -3002,11 +3039,34 @@ function loadModuleFromImmediateNodeModulesDirectory(extensions: Extensions, mod
30023039
}
30033040
}
30043041

3042+
function loadModuleFromImmediateNodeModulesDirectoryPnP(extensions: Extensions, moduleName: string, directory: string, state: ModuleResolutionState, typesScopeOnly: boolean, cache: ModuleResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined): Resolved | undefined {
3043+
const issuer = normalizeSlashes(directory);
3044+
3045+
if (!typesScopeOnly) {
3046+
const packageResult = tryLoadModuleUsingPnpResolution(extensions, moduleName, issuer, state, cache, redirectedReference);
3047+
if (packageResult) {
3048+
return packageResult;
3049+
}
3050+
}
3051+
3052+
if (extensions & Extensions.Declaration) {
3053+
return tryLoadModuleUsingPnpResolution(Extensions.Declaration, `@types/${mangleScopedPackageNameWithTrace(moduleName, state)}`, issuer, state, cache, redirectedReference);
3054+
}
3055+
}
3056+
30053057
function loadModuleFromSpecificNodeModulesDirectory(extensions: Extensions, moduleName: string, nodeModulesDirectory: string, nodeModulesDirectoryExists: boolean, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined): Resolved | undefined {
30063058
const candidate = normalizePath(combinePaths(nodeModulesDirectory, moduleName));
30073059
const { packageName, rest } = parsePackageName(moduleName);
30083060
const packageDirectory = combinePaths(nodeModulesDirectory, packageName);
3061+
return loadModuleFromSpecificNodeModulesDirectoryImpl(extensions, nodeModulesDirectoryExists, state, cache, redirectedReference, candidate, rest, packageDirectory);
3062+
}
30093063

3064+
function loadModuleFromPnpResolution(extensions: Extensions, packageDirectory: string, rest: string, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined): Resolved | undefined {
3065+
const candidate = normalizePath(combinePaths(packageDirectory, rest));
3066+
return loadModuleFromSpecificNodeModulesDirectoryImpl(extensions, /*nodeModulesDirectoryExists*/ true, state, cache, redirectedReference, candidate, rest, packageDirectory);
3067+
}
3068+
3069+
function loadModuleFromSpecificNodeModulesDirectoryImpl(extensions: Extensions, nodeModulesDirectoryExists: boolean, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined, candidate: string, rest: string, packageDirectory: string): Resolved | undefined {
30103070
let rootPackageInfo: PackageJsonInfo | undefined;
30113071
// First look for a nested package.json, as in `node_modules/foo/bar/package.json`.
30123072
let packageInfo = getPackageJsonInfo(candidate, !nodeModulesDirectoryExists, state);
@@ -3333,3 +3393,22 @@ function useCaseSensitiveFileNames(state: ModuleResolutionState) {
33333393
typeof state.host.useCaseSensitiveFileNames === "boolean" ? state.host.useCaseSensitiveFileNames :
33343394
state.host.useCaseSensitiveFileNames();
33353395
}
3396+
3397+
function loadPnpPackageResolution(packageName: string, containingDirectory: string) {
3398+
try {
3399+
const resolution = getPnpApi(containingDirectory).resolveToUnqualified(packageName, `${containingDirectory}/`, { considerBuiltins: false });
3400+
return normalizeSlashes(resolution).replace(/\/$/, "");
3401+
}
3402+
catch {
3403+
// Nothing to do
3404+
}
3405+
}
3406+
3407+
function tryLoadModuleUsingPnpResolution(extensions: Extensions, moduleName: string, containingDirectory: string, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined) {
3408+
const { packageName, rest } = parsePackageName(moduleName);
3409+
3410+
const packageResolution = loadPnpPackageResolution(packageName, containingDirectory);
3411+
return packageResolution
3412+
? loadModuleFromPnpResolution(extensions, packageResolution, rest, state, cache, redirectedReference)
3413+
: undefined;
3414+
}

‎src/compiler/moduleSpecifiers.ts

+79-14
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ import {
8484
NodeFlags,
8585
NodeModulePathParts,
8686
normalizePath,
87+
PackagePathParts,
8788
Path,
8889
pathContainsNodeModules,
8990
pathIsBareSpecifier,
@@ -110,6 +111,9 @@ import {
110111
TypeChecker,
111112
UserPreferences,
112113
} from "./_namespaces/ts";
114+
import {
115+
getPnpApi,
116+
} from "./pnp";
113117

114118
// Used by importFixes, getEditsForFileRename, and declaration emit to synthesize import module specifiers.
115119

@@ -652,7 +656,17 @@ function getAllModulePathsWorker(importingFileName: Path, importedFileName: stri
652656
host,
653657
/*preferSymlinks*/ true,
654658
(path, isRedirect) => {
655-
const isInNodeModules = pathContainsNodeModules(path);
659+
let isInNodeModules = pathContainsNodeModules(path);
660+
661+
const pnpapi = getPnpApi(path);
662+
if (!isInNodeModules && pnpapi) {
663+
const fromLocator = pnpapi.findPackageLocator(importingFileName);
664+
const toLocator = pnpapi.findPackageLocator(path);
665+
if (fromLocator && toLocator && fromLocator !== toLocator) {
666+
isInNodeModules = true;
667+
}
668+
}
669+
656670
allFileNames.set(path, { path: getCanonicalFileName(path), isRedirect, isInNodeModules });
657671
importedFileFromNodeModules = importedFileFromNodeModules || isInNodeModules;
658672
// don't return value, so we collect everything
@@ -918,7 +932,51 @@ function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCan
918932
if (!host.fileExists || !host.readFile) {
919933
return undefined;
920934
}
921-
const parts: NodeModulePathParts = getNodeModulePathParts(path)!;
935+
let parts: NodeModulePathParts | PackagePathParts | undefined = getNodeModulePathParts(path);
936+
937+
let pnpPackageName: string | undefined;
938+
939+
const pnpApi = getPnpApi(path);
940+
if (pnpApi) {
941+
const fromLocator = pnpApi.findPackageLocator(importingSourceFile.fileName);
942+
const toLocator = pnpApi.findPackageLocator(path);
943+
944+
// Don't use the package name when the imported file is inside
945+
// the source directory (prefer a relative path instead)
946+
if (fromLocator === toLocator) {
947+
return undefined;
948+
}
949+
950+
if (fromLocator && toLocator) {
951+
const fromInfo = pnpApi.getPackageInformation(fromLocator);
952+
if (toLocator.reference === fromInfo.packageDependencies.get(toLocator.name)) {
953+
pnpPackageName = toLocator.name;
954+
}
955+
else {
956+
// Aliased dependencies
957+
for (const [name, reference] of fromInfo.packageDependencies) {
958+
if (Array.isArray(reference)) {
959+
if (reference[0] === toLocator.name && reference[1] === toLocator.reference) {
960+
pnpPackageName = name;
961+
break;
962+
}
963+
}
964+
}
965+
}
966+
967+
if (!parts) {
968+
const toInfo = pnpApi.getPackageInformation(toLocator);
969+
parts = {
970+
topLevelNodeModulesIndex: undefined,
971+
topLevelPackageNameIndex: undefined,
972+
// The last character from packageLocation is the trailing "/", we want to point to it
973+
packageRootIndex: toInfo.packageLocation.length - 1,
974+
fileNameIndex: path.lastIndexOf(`/`),
975+
};
976+
}
977+
}
978+
}
979+
922980
if (!parts) {
923981
return undefined;
924982
}
@@ -963,19 +1021,26 @@ function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCan
9631021
return undefined;
9641022
}
9651023

966-
const globalTypingsCacheLocation = host.getGlobalTypingsCacheLocation && host.getGlobalTypingsCacheLocation();
967-
// Get a path that's relative to node_modules or the importing file's path
968-
// if node_modules folder is in this folder or any of its parent folders, no need to keep it.
969-
const pathToTopLevelNodeModules = getCanonicalFileName(moduleSpecifier.substring(0, parts.topLevelNodeModulesIndex));
970-
if (!(startsWith(sourceDirectory, pathToTopLevelNodeModules) || globalTypingsCacheLocation && startsWith(getCanonicalFileName(globalTypingsCacheLocation), pathToTopLevelNodeModules))) {
971-
return undefined;
1024+
// If PnP is enabled the node_modules entries we'll get will always be relevant even if they
1025+
// are located in a weird path apparently outside of the source directory
1026+
if (typeof process.versions.pnp === "undefined") {
1027+
const globalTypingsCacheLocation = host.getGlobalTypingsCacheLocation && host.getGlobalTypingsCacheLocation();
1028+
// Get a path that's relative to node_modules or the importing file's path
1029+
// if node_modules folder is in this folder or any of its parent folders, no need to keep it.
1030+
const pathToTopLevelNodeModules = getCanonicalFileName(moduleSpecifier.substring(0, parts.topLevelNodeModulesIndex));
1031+
if (!(startsWith(sourceDirectory, pathToTopLevelNodeModules) || globalTypingsCacheLocation && startsWith(getCanonicalFileName(globalTypingsCacheLocation), pathToTopLevelNodeModules))) {
1032+
return undefined;
1033+
}
9721034
}
9731035

9741036
// If the module was found in @types, get the actual Node package name
975-
const nodeModulesDirectoryName = moduleSpecifier.substring(parts.topLevelPackageNameIndex + 1);
976-
const packageName = getPackageNameFromTypesPackageName(nodeModulesDirectoryName);
1037+
const nodeModulesDirectoryName = typeof pnpPackageName !== "undefined"
1038+
? pnpPackageName + moduleSpecifier.substring(parts.packageRootIndex)
1039+
: moduleSpecifier.substring(parts.topLevelPackageNameIndex! + 1);
1040+
1041+
const packageNameFromPath = getPackageNameFromTypesPackageName(nodeModulesDirectoryName);
9771042
// For classic resolution, only allow importing from node_modules/@types, not other node_modules
978-
return getEmitModuleResolutionKind(options) === ModuleResolutionKind.Classic && packageName === nodeModulesDirectoryName ? undefined : packageName;
1043+
return getEmitModuleResolutionKind(options) === ModuleResolutionKind.Classic && packageNameFromPath === nodeModulesDirectoryName ? undefined : packageNameFromPath;
9791044

9801045
function tryDirectoryWithPackageJson(packageRootIndex: number): { moduleFileToTry: string; packageRootPath?: string; blockedByExports?: true; verbatimFromExports?: true; } {
9811046
const packageRootPath = path.substring(0, packageRootIndex);
@@ -990,8 +1055,8 @@ function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCan
9901055
// The package name that we found in node_modules could be different from the package
9911056
// name in the package.json content via url/filepath dependency specifiers. We need to
9921057
// use the actual directory name, so don't look at `packageJsonContent.name` here.
993-
const nodeModulesDirectoryName = packageRootPath.substring(parts.topLevelPackageNameIndex + 1);
994-
const packageName = getPackageNameFromTypesPackageName(nodeModulesDirectoryName);
1058+
const nodeModulesDirectoryName = packageRootPath.substring(parts!.topLevelPackageNameIndex! + 1);
1059+
const packageName = getPackageNameFromTypesPackageName(pnpPackageName ? pnpPackageName : nodeModulesDirectoryName);
9951060
const conditions = getConditions(options, importMode);
9961061
const fromExports = packageJsonContent.exports
9971062
? tryGetModuleNameFromExports(options, path, packageRootPath, packageName, packageJsonContent.exports, conditions)
@@ -1057,7 +1122,7 @@ function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCan
10571122
}
10581123
else {
10591124
// No package.json exists; an index.js will still resolve as the package name
1060-
const fileName = getCanonicalFileName(moduleFileToTry.substring(parts.packageRootIndex + 1));
1125+
const fileName = getCanonicalFileName(moduleFileToTry.substring(parts!.packageRootIndex + 1));
10611126
if (fileName === "index.d.ts" || fileName === "index.js" || fileName === "index.ts" || fileName === "index.tsx") {
10621127
return { moduleFileToTry, packageRootPath };
10631128
}

‎src/compiler/pnp.ts

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import {
2+
getDirectoryPath,
3+
resolvePath,
4+
} from "./path";
5+
6+
export function getPnpApi(path: string) {
7+
if (typeof process.versions.pnp === "undefined") {
8+
return;
9+
}
10+
11+
const { findPnpApi } = require("module");
12+
if (findPnpApi) {
13+
return findPnpApi(`${path}/`);
14+
}
15+
}
16+
17+
export function getPnpApiPath(path: string): string | undefined {
18+
// eslint-disable-next-line no-null/no-null
19+
return getPnpApi(path)?.resolveRequest("pnpapi", /*issuer*/ null);
20+
}
21+
22+
export function getPnpTypeRoots(currentDirectory: string) {
23+
const pnpApi = getPnpApi(currentDirectory);
24+
if (!pnpApi) {
25+
return [];
26+
}
27+
28+
// Some TS consumers pass relative paths that aren't normalized
29+
currentDirectory = resolvePath(currentDirectory);
30+
31+
const currentPackage = pnpApi.findPackageLocator(`${currentDirectory}/`);
32+
if (!currentPackage) {
33+
return [];
34+
}
35+
36+
const { packageDependencies } = pnpApi.getPackageInformation(currentPackage);
37+
38+
const typeRoots: string[] = [];
39+
for (const [name, referencish] of Array.from<any>(packageDependencies.entries())) {
40+
// eslint-disable-next-line no-null/no-null
41+
if (name.startsWith(`@types/`) && referencish !== null) {
42+
const dependencyLocator = pnpApi.getLocator(name, referencish);
43+
const { packageLocation } = pnpApi.getPackageInformation(dependencyLocator);
44+
45+
typeRoots.push(getDirectoryPath(packageLocation));
46+
}
47+
}
48+
49+
return typeRoots;
50+
}
51+
52+
export function isImportablePathPnp(fromPath: string, toPath: string): boolean {
53+
const pnpApi = getPnpApi(fromPath);
54+
55+
const fromLocator = pnpApi.findPackageLocator(fromPath);
56+
const toLocator = pnpApi.findPackageLocator(toPath);
57+
58+
// eslint-disable-next-line no-null/no-null
59+
if (toLocator === null) {
60+
return false;
61+
}
62+
63+
const fromInfo = pnpApi.getPackageInformation(fromLocator);
64+
const toReference = fromInfo.packageDependencies.get(toLocator.name);
65+
66+
if (toReference) {
67+
return toReference === toLocator.reference;
68+
}
69+
70+
// Aliased dependencies
71+
for (const reference of fromInfo.packageDependencies.values()) {
72+
if (Array.isArray(reference)) {
73+
if (reference[0] === toLocator.name && reference[1] === toLocator.reference) {
74+
return true;
75+
}
76+
}
77+
}
78+
79+
return false;
80+
}

‎src/compiler/sys.ts

+4
Original file line numberDiff line numberDiff line change
@@ -1728,6 +1728,10 @@ export let sys: System = (() => {
17281728
}
17291729

17301730
function isFileSystemCaseSensitive(): boolean {
1731+
// The PnP runtime is always case-sensitive
1732+
if (typeof process.versions.pnp !== `undefined`) {
1733+
return true;
1734+
}
17311735
// win32\win64 are case insensitive platforms
17321736
if (platform === "win32" || platform === "win64") {
17331737
return false;

‎src/compiler/utilities.ts

+9
Original file line numberDiff line numberDiff line change
@@ -10249,6 +10249,15 @@ export interface NodeModulePathParts {
1024910249
readonly packageRootIndex: number;
1025010250
readonly fileNameIndex: number;
1025110251
}
10252+
10253+
/** @internal */
10254+
export interface PackagePathParts {
10255+
readonly topLevelNodeModulesIndex: undefined;
10256+
readonly topLevelPackageNameIndex: undefined;
10257+
readonly packageRootIndex: number;
10258+
readonly fileNameIndex: number;
10259+
}
10260+
1025210261
/** @internal */
1025310262
export function getNodeModulePathParts(fullPath: string): NodeModulePathParts | undefined {
1025410263
// If fullPath can't be valid module file within node_modules, returns undefined.

0 commit comments

Comments
 (0)