Skip to content

Commit 18776a7

Browse files
committed
feat: add Yarn PnP support
1 parent 69fb689 commit 18776a7

File tree

16 files changed

+473
-45
lines changed

16 files changed

+473
-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: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ import {
110110
versionMajorMinor,
111111
VersionRange,
112112
} from "./_namespaces/ts.js";
113+
import { getPnpTypeRoots } from "./pnp.js";
114+
import { getPnpApi } from "./pnpapi.js";
113115

114116
/** @internal */
115117
export function trace(host: ModuleResolutionHost, message: DiagnosticMessage, ...args: any[]): void {
@@ -495,7 +497,7 @@ export function getEffectiveTypeRoots(options: CompilerOptions, host: GetEffecti
495497
* Returns the path to every node_modules/@types directory from some ancestor directory.
496498
* Returns undefined if there are none.
497499
*/
498-
function getDefaultTypeRoots(currentDirectory: string): string[] | undefined {
500+
function getNodeModulesTypeRoots(currentDirectory: string) {
499501
let typeRoots: string[] | undefined;
500502
forEachAncestorDirectory(normalizePath(currentDirectory), directory => {
501503
const atTypes = combinePaths(directory, nodeModulesAtTypes);
@@ -510,6 +512,18 @@ function arePathsEqual(path1: string, path2: string, host: ModuleResolutionHost)
510512
return comparePaths(path1, path2, !useCaseSensitiveFileNames) === Comparison.EqualTo;
511513
}
512514

515+
function getDefaultTypeRoots(currentDirectory: string): string[] | undefined {
516+
const nmTypes = getNodeModulesTypeRoots(currentDirectory);
517+
const pnpTypes = getPnpTypeRoots(currentDirectory);
518+
519+
if (nmTypes?.length) {
520+
return [...nmTypes, ...pnpTypes];
521+
}
522+
else if (pnpTypes.length) {
523+
return pnpTypes;
524+
}
525+
}
526+
513527
function getOriginalAndResolvedFileName(fileName: string, host: ModuleResolutionHost, traceEnabled: boolean) {
514528
const resolvedFileName = realPath(fileName, host, traceEnabled);
515529
const pathsAreEqual = arePathsEqual(fileName, resolvedFileName, host);
@@ -790,6 +804,18 @@ export function resolvePackageNameToPackageJson(
790804
): PackageJsonInfo | undefined {
791805
const moduleResolutionState = getTemporaryModuleResolutionState(cache?.getPackageJsonInfoCache(), host, options);
792806

807+
const pnpapi = getPnpApi(containingDirectory);
808+
if (pnpapi) {
809+
try {
810+
const resolution = pnpapi.resolveToUnqualified(packageName, `${containingDirectory}/`, { considerBuiltins: false });
811+
const candidate = normalizeSlashes(resolution).replace(/\/$/, "");
812+
return getPackageJsonInfo(candidate, /*onlyRecordFailures*/ false, moduleResolutionState);
813+
}
814+
catch {
815+
return;
816+
}
817+
}
818+
793819
return forEachAncestorDirectoryStoppingAtGlobalCache(host, containingDirectory, ancestorDirectory => {
794820
if (getBaseFileName(ancestorDirectory) !== "node_modules") {
795821
const nodeModulesFolder = combinePaths(ancestorDirectory, "node_modules");
@@ -3007,6 +3033,15 @@ function loadModuleFromNearestNodeModulesDirectoryWorker(extensions: Extensions,
30073033
}
30083034

30093035
function lookup(extensions: Extensions) {
3036+
const issuer = normalizeSlashes(directory);
3037+
if (getPnpApi(issuer)) {
3038+
const resolutionFromCache = tryFindNonRelativeModuleNameInCache(cache, moduleName, mode, issuer, redirectedReference, state);
3039+
if (resolutionFromCache) {
3040+
return resolutionFromCache;
3041+
}
3042+
return toSearchResult(loadModuleFromImmediateNodeModulesDirectoryPnP(extensions, moduleName, issuer, state, typesScopeOnly, cache, redirectedReference));
3043+
}
3044+
30103045
return forEachAncestorDirectoryStoppingAtGlobalCache(
30113046
state.host,
30123047
normalizeSlashes(directory),
@@ -3069,11 +3104,34 @@ function loadModuleFromImmediateNodeModulesDirectory(extensions: Extensions, mod
30693104
}
30703105
}
30713106

3107+
function loadModuleFromImmediateNodeModulesDirectoryPnP(extensions: Extensions, moduleName: string, directory: string, state: ModuleResolutionState, typesScopeOnly: boolean, cache: ModuleResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined): Resolved | undefined {
3108+
const issuer = normalizeSlashes(directory);
3109+
3110+
if (!typesScopeOnly) {
3111+
const packageResult = tryLoadModuleUsingPnpResolution(extensions, moduleName, issuer, state, cache, redirectedReference);
3112+
if (packageResult) {
3113+
return packageResult;
3114+
}
3115+
}
3116+
3117+
if (extensions & Extensions.Declaration) {
3118+
return tryLoadModuleUsingPnpResolution(Extensions.Declaration, `@types/${mangleScopedPackageNameWithTrace(moduleName, state)}`, issuer, state, cache, redirectedReference);
3119+
}
3120+
}
3121+
30723122
function loadModuleFromSpecificNodeModulesDirectory(extensions: Extensions, moduleName: string, nodeModulesDirectory: string, nodeModulesDirectoryExists: boolean, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined): Resolved | undefined {
30733123
const candidate = normalizePath(combinePaths(nodeModulesDirectory, moduleName));
30743124
const { packageName, rest } = parsePackageName(moduleName);
30753125
const packageDirectory = combinePaths(nodeModulesDirectory, packageName);
3126+
return loadModuleFromSpecificNodeModulesDirectoryImpl(extensions, nodeModulesDirectoryExists, state, cache, redirectedReference, candidate, rest, packageDirectory);
3127+
}
3128+
3129+
function loadModuleFromPnpResolution(extensions: Extensions, packageDirectory: string, rest: string, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined): Resolved | undefined {
3130+
const candidate = normalizePath(combinePaths(packageDirectory, rest));
3131+
return loadModuleFromSpecificNodeModulesDirectoryImpl(extensions, /*nodeModulesDirectoryExists*/ true, state, cache, redirectedReference, candidate, rest, packageDirectory);
3132+
}
30763133

3134+
function loadModuleFromSpecificNodeModulesDirectoryImpl(extensions: Extensions, nodeModulesDirectoryExists: boolean, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined, candidate: string, rest: string, packageDirectory: string): Resolved | undefined {
30773135
let rootPackageInfo: PackageJsonInfo | undefined;
30783136
// First look for a nested package.json, as in `node_modules/foo/bar/package.json`.
30793137
let packageInfo = getPackageJsonInfo(candidate, !nodeModulesDirectoryExists, state);
@@ -3409,3 +3467,22 @@ function useCaseSensitiveFileNames(state: ModuleResolutionState) {
34093467
typeof state.host.useCaseSensitiveFileNames === "boolean" ? state.host.useCaseSensitiveFileNames :
34103468
state.host.useCaseSensitiveFileNames();
34113469
}
3470+
3471+
function loadPnpPackageResolution(packageName: string, containingDirectory: string) {
3472+
try {
3473+
const resolution = getPnpApi(containingDirectory).resolveToUnqualified(packageName, `${containingDirectory}/`, { considerBuiltins: false });
3474+
return normalizeSlashes(resolution).replace(/\/$/, "");
3475+
}
3476+
catch {
3477+
// Nothing to do
3478+
}
3479+
}
3480+
3481+
function tryLoadModuleUsingPnpResolution(extensions: Extensions, moduleName: string, containingDirectory: string, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined) {
3482+
const { packageName, rest } = parsePackageName(moduleName);
3483+
3484+
const packageResolution = loadPnpPackageResolution(packageName, containingDirectory);
3485+
return packageResolution
3486+
? loadModuleFromPnpResolution(extensions, packageResolution, rest, state, cache, redirectedReference)
3487+
: undefined;
3488+
}

src/compiler/moduleSpecifiers.ts

Lines changed: 77 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ import {
100100
NodeModulePathParts,
101101
normalizePath,
102102
PackageJsonPathFields,
103+
PackagePathParts,
103104
pathContainsNodeModules,
104105
pathIsBareSpecifier,
105106
pathIsRelative,
@@ -129,6 +130,7 @@ import {
129130
TypeChecker,
130131
UserPreferences,
131132
} from "./_namespaces/ts.js";
133+
import { getPnpApi } from "./pnpapi.js";
132134

133135
const stringToRegex = memoizeOne((pattern: string) => {
134136
try {
@@ -835,7 +837,17 @@ function getAllModulePathsWorker(info: Info, importedFileName: string, host: Mod
835837
host,
836838
/*preferSymlinks*/ true,
837839
(path, isRedirect) => {
838-
const isInNodeModules = pathContainsNodeModules(path);
840+
let isInNodeModules = pathContainsNodeModules(path);
841+
842+
const pnpapi = getPnpApi(path);
843+
if (!isInNodeModules && pnpapi) {
844+
const fromLocator = pnpapi.findPackageLocator(info.importingSourceFileName);
845+
const toLocator = pnpapi.findPackageLocator(path);
846+
if (fromLocator && toLocator && fromLocator !== toLocator) {
847+
isInNodeModules = true;
848+
}
849+
}
850+
839851
allFileNames.set(path, { path: info.getCanonicalFileName(path), isRedirect, isInNodeModules });
840852
importedFileFromNodeModules = importedFileFromNodeModules || isInNodeModules;
841853
// don't return value, so we collect everything
@@ -1188,7 +1200,51 @@ function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCan
11881200
if (!host.fileExists || !host.readFile) {
11891201
return undefined;
11901202
}
1191-
const parts: NodeModulePathParts = getNodeModulePathParts(path)!;
1203+
let parts: NodeModulePathParts | PackagePathParts | undefined = getNodeModulePathParts(path);
1204+
1205+
let pnpPackageName: string | undefined;
1206+
1207+
const pnpApi = getPnpApi(path);
1208+
if (pnpApi) {
1209+
const fromLocator = pnpApi.findPackageLocator(importingSourceFile.fileName);
1210+
const toLocator = pnpApi.findPackageLocator(path);
1211+
1212+
// Don't use the package name when the imported file is inside
1213+
// the source directory (prefer a relative path instead)
1214+
if (fromLocator === toLocator) {
1215+
return undefined;
1216+
}
1217+
1218+
if (fromLocator && toLocator) {
1219+
const fromInfo = pnpApi.getPackageInformation(fromLocator);
1220+
if (toLocator.reference === fromInfo.packageDependencies.get(toLocator.name)) {
1221+
pnpPackageName = toLocator.name;
1222+
}
1223+
else {
1224+
// Aliased dependencies
1225+
for (const [name, reference] of fromInfo.packageDependencies) {
1226+
if (Array.isArray(reference)) {
1227+
if (reference[0] === toLocator.name && reference[1] === toLocator.reference) {
1228+
pnpPackageName = name;
1229+
break;
1230+
}
1231+
}
1232+
}
1233+
}
1234+
1235+
if (!parts) {
1236+
const toInfo = pnpApi.getPackageInformation(toLocator);
1237+
parts = {
1238+
topLevelNodeModulesIndex: undefined,
1239+
topLevelPackageNameIndex: undefined,
1240+
// The last character from packageLocation is the trailing "/", we want to point to it
1241+
packageRootIndex: toInfo.packageLocation.length - 1,
1242+
fileNameIndex: path.lastIndexOf(`/`),
1243+
};
1244+
}
1245+
}
1246+
}
1247+
11921248
if (!parts) {
11931249
return undefined;
11941250
}
@@ -1233,19 +1289,26 @@ function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCan
12331289
return undefined;
12341290
}
12351291

1236-
const globalTypingsCacheLocation = host.getGlobalTypingsCacheLocation && host.getGlobalTypingsCacheLocation();
1237-
// Get a path that's relative to node_modules or the importing file's path
1238-
// if node_modules folder is in this folder or any of its parent folders, no need to keep it.
1239-
const pathToTopLevelNodeModules = getCanonicalFileName(moduleSpecifier.substring(0, parts.topLevelNodeModulesIndex));
1240-
if (!(startsWith(canonicalSourceDirectory, pathToTopLevelNodeModules) || globalTypingsCacheLocation && startsWith(getCanonicalFileName(globalTypingsCacheLocation), pathToTopLevelNodeModules))) {
1241-
return undefined;
1292+
// If PnP is enabled the node_modules entries we'll get will always be relevant even if they
1293+
// are located in a weird path apparently outside of the source directory
1294+
if (typeof process.versions.pnp === "undefined") {
1295+
const globalTypingsCacheLocation = host.getGlobalTypingsCacheLocation && host.getGlobalTypingsCacheLocation();
1296+
// Get a path that's relative to node_modules or the importing file's path
1297+
// if node_modules folder is in this folder or any of its parent folders, no need to keep it.
1298+
const pathToTopLevelNodeModules = getCanonicalFileName(moduleSpecifier.substring(0, parts.topLevelNodeModulesIndex));
1299+
if (!(startsWith(canonicalSourceDirectory, pathToTopLevelNodeModules) || globalTypingsCacheLocation && startsWith(getCanonicalFileName(globalTypingsCacheLocation), pathToTopLevelNodeModules))) {
1300+
return undefined;
1301+
}
12421302
}
12431303

12441304
// If the module was found in @types, get the actual Node package name
1245-
const nodeModulesDirectoryName = moduleSpecifier.substring(parts.topLevelPackageNameIndex + 1);
1246-
const packageName = getPackageNameFromTypesPackageName(nodeModulesDirectoryName);
1305+
const nodeModulesDirectoryName = typeof pnpPackageName !== "undefined"
1306+
? pnpPackageName + moduleSpecifier.substring(parts.packageRootIndex)
1307+
: moduleSpecifier.substring(parts.topLevelPackageNameIndex! + 1);
1308+
1309+
const packageNameFromPath = getPackageNameFromTypesPackageName(nodeModulesDirectoryName);
12471310
// For classic resolution, only allow importing from node_modules/@types, not other node_modules
1248-
return getEmitModuleResolutionKind(options) === ModuleResolutionKind.Classic && packageName === nodeModulesDirectoryName ? undefined : packageName;
1311+
return getEmitModuleResolutionKind(options) === ModuleResolutionKind.Classic && packageNameFromPath === nodeModulesDirectoryName ? undefined : packageNameFromPath;
12491312

12501313
function tryDirectoryWithPackageJson(packageRootIndex: number): { moduleFileToTry: string; packageRootPath?: string; blockedByExports?: true; verbatimFromExports?: true; } {
12511314
const packageRootPath = path.substring(0, packageRootIndex);
@@ -1260,8 +1323,8 @@ function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCan
12601323
// The package name that we found in node_modules could be different from the package
12611324
// name in the package.json content via url/filepath dependency specifiers. We need to
12621325
// use the actual directory name, so don't look at `packageJsonContent.name` here.
1263-
const nodeModulesDirectoryName = packageRootPath.substring(parts.topLevelPackageNameIndex + 1);
1264-
const packageName = getPackageNameFromTypesPackageName(nodeModulesDirectoryName);
1326+
const nodeModulesDirectoryName = packageRootPath.substring(parts!.topLevelPackageNameIndex! + 1);
1327+
const packageName = getPackageNameFromTypesPackageName(pnpPackageName ? pnpPackageName : nodeModulesDirectoryName);
12651328
const conditions = getConditions(options, importMode);
12661329
const fromExports = packageJsonContent?.exports
12671330
? tryGetModuleNameFromExports(
@@ -1332,7 +1395,7 @@ function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCan
13321395
}
13331396
else {
13341397
// No package.json exists; an index.js will still resolve as the package name
1335-
const fileName = getCanonicalFileName(moduleFileToTry.substring(parts.packageRootIndex + 1));
1398+
const fileName = getCanonicalFileName(moduleFileToTry.substring(parts!.packageRootIndex + 1));
13361399
if (fileName === "index.d.ts" || fileName === "index.js" || fileName === "index.ts" || fileName === "index.tsx") {
13371400
return { moduleFileToTry, packageRootPath };
13381401
}

src/compiler/pnp.ts

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

src/compiler/pnpapi.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// To preserve the effects of https://github.com/microsoft/TypeScript/pull/55326
2+
// this file needs to avoid importing large graphs.
3+
4+
export function getPnpApi(path: string): any {
5+
if (typeof process.versions.pnp === "undefined") {
6+
return;
7+
}
8+
9+
const { findPnpApi } = require("module");
10+
if (findPnpApi) {
11+
return findPnpApi(`${path}/`);
12+
}
13+
}
14+
15+
export function getPnpApiPath(path: string): string | undefined {
16+
// eslint-disable-next-line no-restricted-syntax
17+
return getPnpApi(path)?.resolveRequest("pnpapi", /*issuer*/ null);
18+
}

src/compiler/sys.ts

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

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

src/compiler/utilities.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10824,6 +10824,15 @@ export interface NodeModulePathParts {
1082410824
readonly packageRootIndex: number;
1082510825
readonly fileNameIndex: number;
1082610826
}
10827+
10828+
/** @internal */
10829+
export interface PackagePathParts {
10830+
readonly topLevelNodeModulesIndex: undefined;
10831+
readonly topLevelPackageNameIndex: undefined;
10832+
readonly packageRootIndex: number;
10833+
readonly fileNameIndex: number;
10834+
}
10835+
1082710836
/** @internal */
1082810837
export function getNodeModulePathParts(fullPath: string): NodeModulePathParts | undefined {
1082910838
// If fullPath can't be valid module file within node_modules, returns undefined.

0 commit comments

Comments
 (0)