Skip to content

Commit d871830

Browse files
committed
feat: add Yarn PnP support
1 parent f0e9921 commit d871830

File tree

14 files changed

+466
-45
lines changed

14 files changed

+466
-45
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
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

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ import {
109109
versionMajorMinor,
110110
VersionRange,
111111
} from "./_namespaces/ts.js";
112+
import {
113+
getPnpApi,
114+
getPnpTypeRoots,
115+
} from "./pnp.js";
112116

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

516+
function getDefaultTypeRoots(currentDirectory: string): string[] | undefined {
517+
const nmTypes = getNodeModulesTypeRoots(currentDirectory);
518+
const pnpTypes = getPnpTypeRoots(currentDirectory);
519+
520+
if (nmTypes?.length) {
521+
return [...nmTypes, ...pnpTypes];
522+
}
523+
else if (pnpTypes.length) {
524+
return pnpTypes;
525+
}
526+
}
527+
512528
function getOriginalAndResolvedFileName(fileName: string, host: ModuleResolutionHost, traceEnabled: boolean) {
513529
const resolvedFileName = realPath(fileName, host, traceEnabled);
514530
const pathsAreEqual = arePathsEqual(fileName, resolvedFileName, host);
@@ -789,6 +805,18 @@ export function resolvePackageNameToPackageJson(
789805
): PackageJsonInfo | undefined {
790806
const moduleResolutionState = getTemporaryModuleResolutionState(cache?.getPackageJsonInfoCache(), host, options);
791807

808+
const pnpapi = getPnpApi(containingDirectory);
809+
if (pnpapi) {
810+
try {
811+
const resolution = pnpapi.resolveToUnqualified(packageName, `${containingDirectory}/`, { considerBuiltins: false });
812+
const candidate = normalizeSlashes(resolution).replace(/\/$/, "");
813+
return getPackageJsonInfo(candidate, /*onlyRecordFailures*/ false, moduleResolutionState);
814+
}
815+
catch {
816+
return;
817+
}
818+
}
819+
792820
return forEachAncestorDirectory(containingDirectory, ancestorDirectory => {
793821
if (getBaseFileName(ancestorDirectory) !== "node_modules") {
794822
const nodeModulesFolder = combinePaths(ancestorDirectory, "node_modules");
@@ -3012,7 +3040,16 @@ function loadModuleFromNearestNodeModulesDirectoryWorker(extensions: Extensions,
30123040
}
30133041

30143042
function lookup(extensions: Extensions) {
3015-
return forEachAncestorDirectory(normalizeSlashes(directory), ancestorDirectory => {
3043+
const issuer = normalizeSlashes(directory);
3044+
if (getPnpApi(issuer)) {
3045+
const resolutionFromCache = tryFindNonRelativeModuleNameInCache(cache, moduleName, mode, issuer, redirectedReference, state);
3046+
if (resolutionFromCache) {
3047+
return resolutionFromCache;
3048+
}
3049+
return toSearchResult(loadModuleFromImmediateNodeModulesDirectoryPnP(extensions, moduleName, issuer, state, typesScopeOnly, cache, redirectedReference));
3050+
}
3051+
3052+
return forEachAncestorDirectory(issuer, ancestorDirectory => {
30163053
if (getBaseFileName(ancestorDirectory) !== "node_modules") {
30173054
const resolutionFromCache = tryFindNonRelativeModuleNameInCache(cache, moduleName, mode, ancestorDirectory, redirectedReference, state);
30183055
if (resolutionFromCache) {
@@ -3051,11 +3088,34 @@ function loadModuleFromImmediateNodeModulesDirectory(extensions: Extensions, mod
30513088
}
30523089
}
30533090

3091+
function loadModuleFromImmediateNodeModulesDirectoryPnP(extensions: Extensions, moduleName: string, directory: string, state: ModuleResolutionState, typesScopeOnly: boolean, cache: ModuleResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined): Resolved | undefined {
3092+
const issuer = normalizeSlashes(directory);
3093+
3094+
if (!typesScopeOnly) {
3095+
const packageResult = tryLoadModuleUsingPnpResolution(extensions, moduleName, issuer, state, cache, redirectedReference);
3096+
if (packageResult) {
3097+
return packageResult;
3098+
}
3099+
}
3100+
3101+
if (extensions & Extensions.Declaration) {
3102+
return tryLoadModuleUsingPnpResolution(Extensions.Declaration, `@types/${mangleScopedPackageNameWithTrace(moduleName, state)}`, issuer, state, cache, redirectedReference);
3103+
}
3104+
}
3105+
30543106
function loadModuleFromSpecificNodeModulesDirectory(extensions: Extensions, moduleName: string, nodeModulesDirectory: string, nodeModulesDirectoryExists: boolean, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined): Resolved | undefined {
30553107
const candidate = normalizePath(combinePaths(nodeModulesDirectory, moduleName));
30563108
const { packageName, rest } = parsePackageName(moduleName);
30573109
const packageDirectory = combinePaths(nodeModulesDirectory, packageName);
3110+
return loadModuleFromSpecificNodeModulesDirectoryImpl(extensions, nodeModulesDirectoryExists, state, cache, redirectedReference, candidate, rest, packageDirectory);
3111+
}
30583112

3113+
function loadModuleFromPnpResolution(extensions: Extensions, packageDirectory: string, rest: string, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined): Resolved | undefined {
3114+
const candidate = normalizePath(combinePaths(packageDirectory, rest));
3115+
return loadModuleFromSpecificNodeModulesDirectoryImpl(extensions, /*nodeModulesDirectoryExists*/ true, state, cache, redirectedReference, candidate, rest, packageDirectory);
3116+
}
3117+
3118+
function loadModuleFromSpecificNodeModulesDirectoryImpl(extensions: Extensions, nodeModulesDirectoryExists: boolean, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined, candidate: string, rest: string, packageDirectory: string): Resolved | undefined {
30593119
let rootPackageInfo: PackageJsonInfo | undefined;
30603120
// First look for a nested package.json, as in `node_modules/foo/bar/package.json`.
30613121
let packageInfo = getPackageJsonInfo(candidate, !nodeModulesDirectoryExists, state);
@@ -3387,3 +3447,22 @@ function useCaseSensitiveFileNames(state: ModuleResolutionState) {
33873447
typeof state.host.useCaseSensitiveFileNames === "boolean" ? state.host.useCaseSensitiveFileNames :
33883448
state.host.useCaseSensitiveFileNames();
33893449
}
3450+
3451+
function loadPnpPackageResolution(packageName: string, containingDirectory: string) {
3452+
try {
3453+
const resolution = getPnpApi(containingDirectory).resolveToUnqualified(packageName, `${containingDirectory}/`, { considerBuiltins: false });
3454+
return normalizeSlashes(resolution).replace(/\/$/, "");
3455+
}
3456+
catch {
3457+
// Nothing to do
3458+
}
3459+
}
3460+
3461+
function tryLoadModuleUsingPnpResolution(extensions: Extensions, moduleName: string, containingDirectory: string, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined) {
3462+
const { packageName, rest } = parsePackageName(moduleName);
3463+
3464+
const packageResolution = loadPnpPackageResolution(packageName, containingDirectory);
3465+
return packageResolution
3466+
? loadModuleFromPnpResolution(extensions, packageResolution, rest, state, cache, redirectedReference)
3467+
: undefined;
3468+
}

src/compiler/moduleSpecifiers.ts

Lines changed: 77 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ import {
9797
NodeModulePathParts,
9898
normalizePath,
9999
PackageJsonPathFields,
100+
PackagePathParts,
100101
pathContainsNodeModules,
101102
pathIsBareSpecifier,
102103
pathIsRelative,
@@ -126,6 +127,7 @@ import {
126127
TypeChecker,
127128
UserPreferences,
128129
} from "./_namespaces/ts.js";
130+
import { getPnpApi } from "./pnp.js";
129131

130132
// Used by importFixes, getEditsForFileRename, and declaration emit to synthesize import module specifiers.
131133

@@ -755,7 +757,17 @@ function getAllModulePathsWorker(info: Info, importedFileName: string, host: Mod
755757
host,
756758
/*preferSymlinks*/ true,
757759
(path, isRedirect) => {
758-
const isInNodeModules = pathContainsNodeModules(path);
760+
let isInNodeModules = pathContainsNodeModules(path);
761+
762+
const pnpapi = getPnpApi(path);
763+
if (!isInNodeModules && pnpapi) {
764+
const fromLocator = pnpapi.findPackageLocator(info.importingSourceFileName);
765+
const toLocator = pnpapi.findPackageLocator(path);
766+
if (fromLocator && toLocator && fromLocator !== toLocator) {
767+
isInNodeModules = true;
768+
}
769+
}
770+
759771
allFileNames.set(path, { path: info.getCanonicalFileName(path), isRedirect, isInNodeModules });
760772
importedFileFromNodeModules = importedFileFromNodeModules || isInNodeModules;
761773
// don't return value, so we collect everything
@@ -1086,7 +1098,51 @@ function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCan
10861098
if (!host.fileExists || !host.readFile) {
10871099
return undefined;
10881100
}
1089-
const parts: NodeModulePathParts = getNodeModulePathParts(path)!;
1101+
let parts: NodeModulePathParts | PackagePathParts | undefined = getNodeModulePathParts(path);
1102+
1103+
let pnpPackageName: string | undefined;
1104+
1105+
const pnpApi = getPnpApi(path);
1106+
if (pnpApi) {
1107+
const fromLocator = pnpApi.findPackageLocator(importingSourceFile.fileName);
1108+
const toLocator = pnpApi.findPackageLocator(path);
1109+
1110+
// Don't use the package name when the imported file is inside
1111+
// the source directory (prefer a relative path instead)
1112+
if (fromLocator === toLocator) {
1113+
return undefined;
1114+
}
1115+
1116+
if (fromLocator && toLocator) {
1117+
const fromInfo = pnpApi.getPackageInformation(fromLocator);
1118+
if (toLocator.reference === fromInfo.packageDependencies.get(toLocator.name)) {
1119+
pnpPackageName = toLocator.name;
1120+
}
1121+
else {
1122+
// Aliased dependencies
1123+
for (const [name, reference] of fromInfo.packageDependencies) {
1124+
if (Array.isArray(reference)) {
1125+
if (reference[0] === toLocator.name && reference[1] === toLocator.reference) {
1126+
pnpPackageName = name;
1127+
break;
1128+
}
1129+
}
1130+
}
1131+
}
1132+
1133+
if (!parts) {
1134+
const toInfo = pnpApi.getPackageInformation(toLocator);
1135+
parts = {
1136+
topLevelNodeModulesIndex: undefined,
1137+
topLevelPackageNameIndex: undefined,
1138+
// The last character from packageLocation is the trailing "/", we want to point to it
1139+
packageRootIndex: toInfo.packageLocation.length - 1,
1140+
fileNameIndex: path.lastIndexOf(`/`),
1141+
};
1142+
}
1143+
}
1144+
}
1145+
10901146
if (!parts) {
10911147
return undefined;
10921148
}
@@ -1131,19 +1187,26 @@ function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCan
11311187
return undefined;
11321188
}
11331189

1134-
const globalTypingsCacheLocation = host.getGlobalTypingsCacheLocation && host.getGlobalTypingsCacheLocation();
1135-
// Get a path that's relative to node_modules or the importing file's path
1136-
// if node_modules folder is in this folder or any of its parent folders, no need to keep it.
1137-
const pathToTopLevelNodeModules = getCanonicalFileName(moduleSpecifier.substring(0, parts.topLevelNodeModulesIndex));
1138-
if (!(startsWith(canonicalSourceDirectory, pathToTopLevelNodeModules) || globalTypingsCacheLocation && startsWith(getCanonicalFileName(globalTypingsCacheLocation), pathToTopLevelNodeModules))) {
1139-
return undefined;
1190+
// If PnP is enabled the node_modules entries we'll get will always be relevant even if they
1191+
// are located in a weird path apparently outside of the source directory
1192+
if (typeof process.versions.pnp === "undefined") {
1193+
const globalTypingsCacheLocation = host.getGlobalTypingsCacheLocation && host.getGlobalTypingsCacheLocation();
1194+
// Get a path that's relative to node_modules or the importing file's path
1195+
// if node_modules folder is in this folder or any of its parent folders, no need to keep it.
1196+
const pathToTopLevelNodeModules = getCanonicalFileName(moduleSpecifier.substring(0, parts.topLevelNodeModulesIndex));
1197+
if (!(startsWith(canonicalSourceDirectory, pathToTopLevelNodeModules) || globalTypingsCacheLocation && startsWith(getCanonicalFileName(globalTypingsCacheLocation), pathToTopLevelNodeModules))) {
1198+
return undefined;
1199+
}
11401200
}
11411201

11421202
// If the module was found in @types, get the actual Node package name
1143-
const nodeModulesDirectoryName = moduleSpecifier.substring(parts.topLevelPackageNameIndex + 1);
1144-
const packageName = getPackageNameFromTypesPackageName(nodeModulesDirectoryName);
1203+
const nodeModulesDirectoryName = typeof pnpPackageName !== "undefined"
1204+
? pnpPackageName + moduleSpecifier.substring(parts.packageRootIndex)
1205+
: moduleSpecifier.substring(parts.topLevelPackageNameIndex! + 1);
1206+
1207+
const packageNameFromPath = getPackageNameFromTypesPackageName(nodeModulesDirectoryName);
11451208
// For classic resolution, only allow importing from node_modules/@types, not other node_modules
1146-
return getEmitModuleResolutionKind(options) === ModuleResolutionKind.Classic && packageName === nodeModulesDirectoryName ? undefined : packageName;
1209+
return getEmitModuleResolutionKind(options) === ModuleResolutionKind.Classic && packageNameFromPath === nodeModulesDirectoryName ? undefined : packageNameFromPath;
11471210

11481211
function tryDirectoryWithPackageJson(packageRootIndex: number): { moduleFileToTry: string; packageRootPath?: string; blockedByExports?: true; verbatimFromExports?: true; } {
11491212
const packageRootPath = path.substring(0, packageRootIndex);
@@ -1158,8 +1221,8 @@ function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCan
11581221
// The package name that we found in node_modules could be different from the package
11591222
// name in the package.json content via url/filepath dependency specifiers. We need to
11601223
// use the actual directory name, so don't look at `packageJsonContent.name` here.
1161-
const nodeModulesDirectoryName = packageRootPath.substring(parts.topLevelPackageNameIndex + 1);
1162-
const packageName = getPackageNameFromTypesPackageName(nodeModulesDirectoryName);
1224+
const nodeModulesDirectoryName = packageRootPath.substring(parts!.topLevelPackageNameIndex! + 1);
1225+
const packageName = getPackageNameFromTypesPackageName(pnpPackageName ? pnpPackageName : nodeModulesDirectoryName);
11631226
const conditions = getConditions(options, importMode);
11641227
const fromExports = packageJsonContent?.exports
11651228
? tryGetModuleNameFromExports(options, host, path, packageRootPath, packageName, packageJsonContent.exports, conditions)
@@ -1222,7 +1285,7 @@ function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCan
12221285
}
12231286
else {
12241287
// No package.json exists; an index.js will still resolve as the package name
1225-
const fileName = getCanonicalFileName(moduleFileToTry.substring(parts.packageRootIndex + 1));
1288+
const fileName = getCanonicalFileName(moduleFileToTry.substring(parts!.packageRootIndex + 1));
12261289
if (fileName === "index.d.ts" || fileName === "index.js" || fileName === "index.ts" || fileName === "index.tsx") {
12271290
return { moduleFileToTry, packageRootPath };
12281291
}

src/compiler/pnp.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import {
2+
getDirectoryPath,
3+
resolvePath,
4+
} from "./path.js";
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-restricted-syntax
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-restricted-syntax
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-restricted-syntax
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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1722,6 +1722,10 @@ export let sys: System = (() => {
17221722
}
17231723

17241724
function isFileSystemCaseSensitive(): boolean {
1725+
// The PnP runtime is always case-sensitive
1726+
if (typeof process.versions.pnp !== `undefined`) {
1727+
return true;
1728+
}
17251729
// win32\win64 are case insensitive platforms
17261730
if (platform === "win32" || platform === "win64") {
17271731
return false;

src/compiler/utilities.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10625,6 +10625,15 @@ export interface NodeModulePathParts {
1062510625
readonly packageRootIndex: number;
1062610626
readonly fileNameIndex: number;
1062710627
}
10628+
10629+
/** @internal */
10630+
export interface PackagePathParts {
10631+
readonly topLevelNodeModulesIndex: undefined;
10632+
readonly topLevelPackageNameIndex: undefined;
10633+
readonly packageRootIndex: number;
10634+
readonly fileNameIndex: number;
10635+
}
10636+
1062810637
/** @internal */
1062910638
export function getNodeModulePathParts(fullPath: string): NodeModulePathParts | undefined {
1063010639
// If fullPath can't be valid module file within node_modules, returns undefined.

0 commit comments

Comments
 (0)