Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 1d266cf

Browse files
committedAug 19, 2024··
feat: add Yarn PnP support
1 parent e6914a5 commit 1d266cf

16 files changed

+474
-46
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

+79-2
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ import {
108108
versionMajorMinor,
109109
VersionRange,
110110
} from "./_namespaces/ts.js";
111+
import { getPnpTypeRoots } from "./pnp.js";
112+
import { getPnpApi } from "./pnpapi.js";
111113

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

513+
function getDefaultTypeRoots(currentDirectory: string): string[] | undefined {
514+
const nmTypes = getNodeModulesTypeRoots(currentDirectory);
515+
const pnpTypes = getPnpTypeRoots(currentDirectory);
516+
517+
if (nmTypes?.length) {
518+
return [...nmTypes, ...pnpTypes];
519+
}
520+
else if (pnpTypes.length) {
521+
return pnpTypes;
522+
}
523+
}
524+
511525
function getOriginalAndResolvedFileName(fileName: string, host: ModuleResolutionHost, traceEnabled: boolean) {
512526
const resolvedFileName = realPath(fileName, host, traceEnabled);
513527
const pathsAreEqual = arePathsEqual(fileName, resolvedFileName, host);
@@ -788,6 +802,18 @@ export function resolvePackageNameToPackageJson(
788802
): PackageJsonInfo | undefined {
789803
const moduleResolutionState = getTemporaryModuleResolutionState(cache?.getPackageJsonInfoCache(), host, options);
790804

805+
const pnpapi = getPnpApi(containingDirectory);
806+
if (pnpapi) {
807+
try {
808+
const resolution = pnpapi.resolveToUnqualified(packageName, `${containingDirectory}/`, { considerBuiltins: false });
809+
const candidate = normalizeSlashes(resolution).replace(/\/$/, "");
810+
return getPackageJsonInfo(candidate, /*onlyRecordFailures*/ false, moduleResolutionState);
811+
}
812+
catch {
813+
return;
814+
}
815+
}
816+
791817
return forEachAncestorDirectory(containingDirectory, ancestorDirectory => {
792818
if (getBaseFileName(ancestorDirectory) !== "node_modules") {
793819
const nodeModulesFolder = combinePaths(ancestorDirectory, "node_modules");
@@ -3002,7 +3028,16 @@ function loadModuleFromNearestNodeModulesDirectoryWorker(extensions: Extensions,
30023028
}
30033029

30043030
function lookup(extensions: Extensions) {
3005-
return forEachAncestorDirectory(normalizeSlashes(directory), ancestorDirectory => {
3031+
const issuer = normalizeSlashes(directory);
3032+
if (getPnpApi(issuer)) {
3033+
const resolutionFromCache = tryFindNonRelativeModuleNameInCache(cache, moduleName, mode, issuer, redirectedReference, state);
3034+
if (resolutionFromCache) {
3035+
return resolutionFromCache;
3036+
}
3037+
return toSearchResult(loadModuleFromImmediateNodeModulesDirectoryPnP(extensions, moduleName, issuer, state, typesScopeOnly, cache, redirectedReference));
3038+
}
3039+
3040+
return forEachAncestorDirectory(issuer, ancestorDirectory => {
30063041
if (getBaseFileName(ancestorDirectory) !== "node_modules") {
30073042
const resolutionFromCache = tryFindNonRelativeModuleNameInCache(cache, moduleName, mode, ancestorDirectory, redirectedReference, state);
30083043
if (resolutionFromCache) {
@@ -3041,11 +3076,34 @@ function loadModuleFromImmediateNodeModulesDirectory(extensions: Extensions, mod
30413076
}
30423077
}
30433078

3079+
function loadModuleFromImmediateNodeModulesDirectoryPnP(extensions: Extensions, moduleName: string, directory: string, state: ModuleResolutionState, typesScopeOnly: boolean, cache: ModuleResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined): Resolved | undefined {
3080+
const issuer = normalizeSlashes(directory);
3081+
3082+
if (!typesScopeOnly) {
3083+
const packageResult = tryLoadModuleUsingPnpResolution(extensions, moduleName, issuer, state, cache, redirectedReference);
3084+
if (packageResult) {
3085+
return packageResult;
3086+
}
3087+
}
3088+
3089+
if (extensions & Extensions.Declaration) {
3090+
return tryLoadModuleUsingPnpResolution(Extensions.Declaration, `@types/${mangleScopedPackageNameWithTrace(moduleName, state)}`, issuer, state, cache, redirectedReference);
3091+
}
3092+
}
3093+
30443094
function loadModuleFromSpecificNodeModulesDirectory(extensions: Extensions, moduleName: string, nodeModulesDirectory: string, nodeModulesDirectoryExists: boolean, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined): Resolved | undefined {
30453095
const candidate = normalizePath(combinePaths(nodeModulesDirectory, moduleName));
30463096
const { packageName, rest } = parsePackageName(moduleName);
30473097
const packageDirectory = combinePaths(nodeModulesDirectory, packageName);
3098+
return loadModuleFromSpecificNodeModulesDirectoryImpl(extensions, nodeModulesDirectoryExists, state, cache, redirectedReference, candidate, rest, packageDirectory);
3099+
}
3100+
3101+
function loadModuleFromPnpResolution(extensions: Extensions, packageDirectory: string, rest: string, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined): Resolved | undefined {
3102+
const candidate = normalizePath(combinePaths(packageDirectory, rest));
3103+
return loadModuleFromSpecificNodeModulesDirectoryImpl(extensions, /*nodeModulesDirectoryExists*/ true, state, cache, redirectedReference, candidate, rest, packageDirectory);
3104+
}
30483105

3106+
function loadModuleFromSpecificNodeModulesDirectoryImpl(extensions: Extensions, nodeModulesDirectoryExists: boolean, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined, candidate: string, rest: string, packageDirectory: string): Resolved | undefined {
30493107
let rootPackageInfo: PackageJsonInfo | undefined;
30503108
// First look for a nested package.json, as in `node_modules/foo/bar/package.json`.
30513109
let packageInfo = getPackageJsonInfo(candidate, !nodeModulesDirectoryExists, state);
@@ -3377,3 +3435,22 @@ function useCaseSensitiveFileNames(state: ModuleResolutionState) {
33773435
typeof state.host.useCaseSensitiveFileNames === "boolean" ? state.host.useCaseSensitiveFileNames :
33783436
state.host.useCaseSensitiveFileNames();
33793437
}
3438+
3439+
function loadPnpPackageResolution(packageName: string, containingDirectory: string) {
3440+
try {
3441+
const resolution = getPnpApi(containingDirectory).resolveToUnqualified(packageName, `${containingDirectory}/`, { considerBuiltins: false });
3442+
return normalizeSlashes(resolution).replace(/\/$/, "");
3443+
}
3444+
catch {
3445+
// Nothing to do
3446+
}
3447+
}
3448+
3449+
function tryLoadModuleUsingPnpResolution(extensions: Extensions, moduleName: string, containingDirectory: string, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined) {
3450+
const { packageName, rest } = parsePackageName(moduleName);
3451+
3452+
const packageResolution = loadPnpPackageResolution(packageName, containingDirectory);
3453+
return packageResolution
3454+
? loadModuleFromPnpResolution(extensions, packageResolution, rest, state, cache, redirectedReference)
3455+
: undefined;
3456+
}

‎src/compiler/moduleSpecifiers.ts

+77-14
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 {
@@ -825,7 +827,17 @@ function getAllModulePathsWorker(info: Info, importedFileName: string, host: Mod
825827
host,
826828
/*preferSymlinks*/ true,
827829
(path, isRedirect) => {
828-
const isInNodeModules = pathContainsNodeModules(path);
830+
let isInNodeModules = pathContainsNodeModules(path);
831+
832+
const pnpapi = getPnpApi(path);
833+
if (!isInNodeModules && pnpapi) {
834+
const fromLocator = pnpapi.findPackageLocator(info.importingSourceFileName);
835+
const toLocator = pnpapi.findPackageLocator(path);
836+
if (fromLocator && toLocator && fromLocator !== toLocator) {
837+
isInNodeModules = true;
838+
}
839+
}
840+
829841
allFileNames.set(path, { path: info.getCanonicalFileName(path), isRedirect, isInNodeModules });
830842
importedFileFromNodeModules = importedFileFromNodeModules || isInNodeModules;
831843
// don't return value, so we collect everything
@@ -1178,7 +1190,51 @@ function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCan
11781190
if (!host.fileExists || !host.readFile) {
11791191
return undefined;
11801192
}
1181-
const parts: NodeModulePathParts = getNodeModulePathParts(path)!;
1193+
let parts: NodeModulePathParts | PackagePathParts | undefined = getNodeModulePathParts(path);
1194+
1195+
let pnpPackageName: string | undefined;
1196+
1197+
const pnpApi = getPnpApi(path);
1198+
if (pnpApi) {
1199+
const fromLocator = pnpApi.findPackageLocator(importingSourceFile.fileName);
1200+
const toLocator = pnpApi.findPackageLocator(path);
1201+
1202+
// Don't use the package name when the imported file is inside
1203+
// the source directory (prefer a relative path instead)
1204+
if (fromLocator === toLocator) {
1205+
return undefined;
1206+
}
1207+
1208+
if (fromLocator && toLocator) {
1209+
const fromInfo = pnpApi.getPackageInformation(fromLocator);
1210+
if (toLocator.reference === fromInfo.packageDependencies.get(toLocator.name)) {
1211+
pnpPackageName = toLocator.name;
1212+
}
1213+
else {
1214+
// Aliased dependencies
1215+
for (const [name, reference] of fromInfo.packageDependencies) {
1216+
if (Array.isArray(reference)) {
1217+
if (reference[0] === toLocator.name && reference[1] === toLocator.reference) {
1218+
pnpPackageName = name;
1219+
break;
1220+
}
1221+
}
1222+
}
1223+
}
1224+
1225+
if (!parts) {
1226+
const toInfo = pnpApi.getPackageInformation(toLocator);
1227+
parts = {
1228+
topLevelNodeModulesIndex: undefined,
1229+
topLevelPackageNameIndex: undefined,
1230+
// The last character from packageLocation is the trailing "/", we want to point to it
1231+
packageRootIndex: toInfo.packageLocation.length - 1,
1232+
fileNameIndex: path.lastIndexOf(`/`),
1233+
};
1234+
}
1235+
}
1236+
}
1237+
11821238
if (!parts) {
11831239
return undefined;
11841240
}
@@ -1223,19 +1279,26 @@ function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCan
12231279
return undefined;
12241280
}
12251281

1226-
const globalTypingsCacheLocation = host.getGlobalTypingsCacheLocation && host.getGlobalTypingsCacheLocation();
1227-
// Get a path that's relative to node_modules or the importing file's path
1228-
// if node_modules folder is in this folder or any of its parent folders, no need to keep it.
1229-
const pathToTopLevelNodeModules = getCanonicalFileName(moduleSpecifier.substring(0, parts.topLevelNodeModulesIndex));
1230-
if (!(startsWith(canonicalSourceDirectory, pathToTopLevelNodeModules) || globalTypingsCacheLocation && startsWith(getCanonicalFileName(globalTypingsCacheLocation), pathToTopLevelNodeModules))) {
1231-
return undefined;
1282+
// If PnP is enabled the node_modules entries we'll get will always be relevant even if they
1283+
// are located in a weird path apparently outside of the source directory
1284+
if (typeof process.versions.pnp === "undefined") {
1285+
const globalTypingsCacheLocation = host.getGlobalTypingsCacheLocation && host.getGlobalTypingsCacheLocation();
1286+
// Get a path that's relative to node_modules or the importing file's path
1287+
// if node_modules folder is in this folder or any of its parent folders, no need to keep it.
1288+
const pathToTopLevelNodeModules = getCanonicalFileName(moduleSpecifier.substring(0, parts.topLevelNodeModulesIndex));
1289+
if (!(startsWith(canonicalSourceDirectory, pathToTopLevelNodeModules) || globalTypingsCacheLocation && startsWith(getCanonicalFileName(globalTypingsCacheLocation), pathToTopLevelNodeModules))) {
1290+
return undefined;
1291+
}
12321292
}
12331293

12341294
// If the module was found in @types, get the actual Node package name
1235-
const nodeModulesDirectoryName = moduleSpecifier.substring(parts.topLevelPackageNameIndex + 1);
1236-
const packageName = getPackageNameFromTypesPackageName(nodeModulesDirectoryName);
1295+
const nodeModulesDirectoryName = typeof pnpPackageName !== "undefined"
1296+
? pnpPackageName + moduleSpecifier.substring(parts.packageRootIndex)
1297+
: moduleSpecifier.substring(parts.topLevelPackageNameIndex! + 1);
1298+
1299+
const packageNameFromPath = getPackageNameFromTypesPackageName(nodeModulesDirectoryName);
12371300
// For classic resolution, only allow importing from node_modules/@types, not other node_modules
1238-
return getEmitModuleResolutionKind(options) === ModuleResolutionKind.Classic && packageName === nodeModulesDirectoryName ? undefined : packageName;
1301+
return getEmitModuleResolutionKind(options) === ModuleResolutionKind.Classic && packageNameFromPath === nodeModulesDirectoryName ? undefined : packageNameFromPath;
12391302

12401303
function tryDirectoryWithPackageJson(packageRootIndex: number): { moduleFileToTry: string; packageRootPath?: string; blockedByExports?: true; verbatimFromExports?: true; } {
12411304
const packageRootPath = path.substring(0, packageRootIndex);
@@ -1250,8 +1313,8 @@ function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCan
12501313
// The package name that we found in node_modules could be different from the package
12511314
// name in the package.json content via url/filepath dependency specifiers. We need to
12521315
// use the actual directory name, so don't look at `packageJsonContent.name` here.
1253-
const nodeModulesDirectoryName = packageRootPath.substring(parts.topLevelPackageNameIndex + 1);
1254-
const packageName = getPackageNameFromTypesPackageName(nodeModulesDirectoryName);
1316+
const nodeModulesDirectoryName = packageRootPath.substring(parts!.topLevelPackageNameIndex! + 1);
1317+
const packageName = getPackageNameFromTypesPackageName(pnpPackageName ? pnpPackageName : nodeModulesDirectoryName);
12551318
const conditions = getConditions(options, importMode);
12561319
const fromExports = packageJsonContent?.exports
12571320
? tryGetModuleNameFromExports(
@@ -1322,7 +1385,7 @@ function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCan
13221385
}
13231386
else {
13241387
// No package.json exists; an index.js will still resolve as the package name
1325-
const fileName = getCanonicalFileName(moduleFileToTry.substring(parts.packageRootIndex + 1));
1388+
const fileName = getCanonicalFileName(moduleFileToTry.substring(parts!.packageRootIndex + 1));
13261389
if (fileName === "index.d.ts" || fileName === "index.js" || fileName === "index.ts" || fileName === "index.tsx") {
13271390
return { moduleFileToTry, packageRootPath };
13281391
}

‎src/compiler/pnp.ts

+65
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) {
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

+18
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) {
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

+4
Original file line numberDiff line numberDiff line change
@@ -1723,6 +1723,10 @@ export let sys: System = (() => {
17231723
}
17241724

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

‎src/compiler/utilities.ts

+9
Original file line numberDiff line numberDiff line change
@@ -10672,6 +10672,15 @@ export interface NodeModulePathParts {
1067210672
readonly packageRootIndex: number;
1067310673
readonly fileNameIndex: number;
1067410674
}
10675+
10676+
/** @internal */
10677+
export interface PackagePathParts {
10678+
readonly topLevelNodeModulesIndex: undefined;
10679+
readonly topLevelPackageNameIndex: undefined;
10680+
readonly packageRootIndex: number;
10681+
readonly fileNameIndex: number;
10682+
}
10683+
1067510684
/** @internal */
1067610685
export function getNodeModulePathParts(fullPath: string): NodeModulePathParts | undefined {
1067710686
// If fullPath can't be valid module file within node_modules, returns undefined.

0 commit comments

Comments
 (0)
Please sign in to comment.