Skip to content

Commit

Permalink
feat: add Yarn PnP support
Browse files Browse the repository at this point in the history
  • Loading branch information
arcanis authored and merceyz committed May 17, 2023
1 parent dddd066 commit 5f12e35
Show file tree
Hide file tree
Showing 13 changed files with 454 additions and 45 deletions.
80 changes: 78 additions & 2 deletions src/compiler/moduleNameResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ import {
versionMajorMinor,
VersionRange,
} from "./_namespaces/ts";
import { getPnpApi, getPnpTypeRoots } from "./pnp";

/** @internal */
export function trace(host: ModuleResolutionHost, message: DiagnosticMessage, ...args: any[]): void;
Expand Down Expand Up @@ -459,7 +460,7 @@ export function getEffectiveTypeRoots(options: CompilerOptions, host: GetEffecti
* Returns the path to every node_modules/@types directory from some ancestor directory.
* Returns undefined if there are none.
*/
function getDefaultTypeRoots(currentDirectory: string): string[] | undefined {
function getNodeModulesTypeRoots(currentDirectory: string) {
let typeRoots: string[] | undefined;
forEachAncestorDirectory(normalizePath(currentDirectory), directory => {
const atTypes = combinePaths(directory, nodeModulesAtTypes);
Expand All @@ -474,6 +475,18 @@ function arePathsEqual(path1: string, path2: string, host: ModuleResolutionHost)
return comparePaths(path1, path2, !useCaseSensitiveFileNames) === Comparison.EqualTo;
}

function getDefaultTypeRoots(currentDirectory: string): string[] | undefined {
const nmTypes = getNodeModulesTypeRoots(currentDirectory);
const pnpTypes = getPnpTypeRoots(currentDirectory);

if (nmTypes?.length) {
return [...nmTypes, ...pnpTypes];
}
else if (pnpTypes.length) {
return pnpTypes;
}
}

function getOriginalAndResolvedFileName(fileName: string, host: ModuleResolutionHost, traceEnabled: boolean) {
const resolvedFileName = realPath(fileName, host, traceEnabled);
const pathsAreEqual = arePathsEqual(fileName, resolvedFileName, host);
Expand Down Expand Up @@ -740,6 +753,18 @@ export function resolvePackageNameToPackageJson(
): PackageJsonInfo | undefined {
const moduleResolutionState = getTemporaryModuleResolutionState(cache?.getPackageJsonInfoCache(), host, options);

const pnpapi = getPnpApi(containingDirectory);
if (pnpapi) {
try {
const resolution = pnpapi.resolveToUnqualified(packageName, `${containingDirectory}/`, { considerBuiltins: false });
const candidate = normalizeSlashes(resolution).replace(/\/$/, "");
return getPackageJsonInfo(candidate, /*onlyRecordFailures*/ false, moduleResolutionState);
}
catch {
return;
}
}

return forEachAncestorDirectory(containingDirectory, ancestorDirectory => {
if (getBaseFileName(ancestorDirectory) !== "node_modules") {
const nodeModulesFolder = combinePaths(ancestorDirectory, "node_modules");
Expand Down Expand Up @@ -2830,7 +2855,16 @@ function loadModuleFromNearestNodeModulesDirectoryWorker(extensions: Extensions,
}

function lookup(extensions: Extensions) {
return forEachAncestorDirectory(normalizeSlashes(directory), ancestorDirectory => {
const issuer = normalizeSlashes(directory);
if (getPnpApi(issuer)) {
const resolutionFromCache = tryFindNonRelativeModuleNameInCache(cache, moduleName, mode, issuer, redirectedReference, state);
if (resolutionFromCache) {
return resolutionFromCache;
}
return toSearchResult(loadModuleFromImmediateNodeModulesDirectoryPnP(extensions, moduleName, issuer, state, typesScopeOnly, cache, redirectedReference));
}

return forEachAncestorDirectory(issuer, ancestorDirectory => {
if (getBaseFileName(ancestorDirectory) !== "node_modules") {
const resolutionFromCache = tryFindNonRelativeModuleNameInCache(cache, moduleName, mode, ancestorDirectory, redirectedReference, state);
if (resolutionFromCache) {
Expand Down Expand Up @@ -2869,11 +2903,34 @@ function loadModuleFromImmediateNodeModulesDirectory(extensions: Extensions, mod
}
}

function loadModuleFromImmediateNodeModulesDirectoryPnP(extensions: Extensions, moduleName: string, directory: string, state: ModuleResolutionState, typesScopeOnly: boolean, cache: ModuleResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined): Resolved | undefined {
const issuer = normalizeSlashes(directory);

if (!typesScopeOnly) {
const packageResult = tryLoadModuleUsingPnpResolution(extensions, moduleName, issuer, state, cache, redirectedReference);
if (packageResult) {
return packageResult;
}
}

if (extensions & Extensions.Declaration) {
return tryLoadModuleUsingPnpResolution(Extensions.Declaration, `@types/${mangleScopedPackageNameWithTrace(moduleName, state)}`, issuer, state, cache, redirectedReference);
}
}

function loadModuleFromSpecificNodeModulesDirectory(extensions: Extensions, moduleName: string, nodeModulesDirectory: string, nodeModulesDirectoryExists: boolean, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined): Resolved | undefined {
const candidate = normalizePath(combinePaths(nodeModulesDirectory, moduleName));
const { packageName, rest } = parsePackageName(moduleName);
const packageDirectory = combinePaths(nodeModulesDirectory, packageName);
return loadModuleFromSpecificNodeModulesDirectoryImpl(extensions, nodeModulesDirectoryExists, state, cache, redirectedReference, candidate, rest, packageDirectory);
}

function loadModuleFromPnpResolution(extensions: Extensions, packageDirectory: string, rest: string, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined): Resolved | undefined {
const candidate = normalizePath(combinePaths(packageDirectory, rest));
return loadModuleFromSpecificNodeModulesDirectoryImpl(extensions, /*nodeModulesDirectoryExists*/ true, state, cache, redirectedReference, candidate, rest, packageDirectory);
}

function loadModuleFromSpecificNodeModulesDirectoryImpl(extensions: Extensions, nodeModulesDirectoryExists: boolean, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined, candidate: string, rest: string, packageDirectory: string): Resolved | undefined {
let rootPackageInfo: PackageJsonInfo | undefined;
// First look for a nested package.json, as in `node_modules/foo/bar/package.json`.
let packageInfo = getPackageJsonInfo(candidate, !nodeModulesDirectoryExists, state);
Expand Down Expand Up @@ -3191,3 +3248,22 @@ function traceIfEnabled(state: ModuleResolutionState, diagnostic: DiagnosticMess
trace(state.host, diagnostic, ...args);
}
}

function loadPnpPackageResolution(packageName: string, containingDirectory: string) {
try {
const resolution = getPnpApi(containingDirectory).resolveToUnqualified(packageName, `${containingDirectory}/`, { considerBuiltins: false });
return normalizeSlashes(resolution).replace(/\/$/, "");
}
catch {
// Nothing to do
}
}

function tryLoadModuleUsingPnpResolution(extensions: Extensions, moduleName: string, containingDirectory: string, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined) {
const {packageName, rest} = parsePackageName(moduleName);

const packageResolution = loadPnpPackageResolution(packageName, containingDirectory);
return packageResolution
? loadModuleFromPnpResolution(extensions, packageResolution, rest, state, cache, redirectedReference)
: undefined;
}
91 changes: 77 additions & 14 deletions src/compiler/moduleSpecifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ import {
NodeFlags,
NodeModulePathParts,
normalizePath,
PackagePathParts,
Path,
pathContainsNodeModules,
pathIsBareSpecifier,
Expand Down Expand Up @@ -111,6 +112,7 @@ import {
TypeChecker,
UserPreferences,
} from "./_namespaces/ts";
import { getPnpApi } from "./pnp";

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

Expand Down Expand Up @@ -631,7 +633,17 @@ function getAllModulePathsWorker(importingFileName: Path, importedFileName: stri
host,
/*preferSymlinks*/ true,
(path, isRedirect) => {
const isInNodeModules = pathContainsNodeModules(path);
let isInNodeModules = pathContainsNodeModules(path);

const pnpapi = getPnpApi(path);
if (!isInNodeModules && pnpapi) {
const fromLocator = pnpapi.findPackageLocator(importingFileName);
const toLocator = pnpapi.findPackageLocator(path);
if (fromLocator && toLocator && fromLocator !== toLocator) {
isInNodeModules = true;
}
}

allFileNames.set(path, { path: getCanonicalFileName(path), isRedirect, isInNodeModules });
importedFileFromNodeModules = importedFileFromNodeModules || isInNodeModules;
// don't return value, so we collect everything
Expand Down Expand Up @@ -892,7 +904,51 @@ function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCan
if (!host.fileExists || !host.readFile) {
return undefined;
}
const parts: NodeModulePathParts = getNodeModulePathParts(path)!;
let parts: NodeModulePathParts | PackagePathParts | undefined = getNodeModulePathParts(path);

let pnpPackageName: string | undefined;

const pnpApi = getPnpApi(path);
if (pnpApi) {
const fromLocator = pnpApi.findPackageLocator(importingSourceFile.fileName);
const toLocator = pnpApi.findPackageLocator(path);

// Don't use the package name when the imported file is inside
// the source directory (prefer a relative path instead)
if (fromLocator === toLocator) {
return undefined;
}

if (fromLocator && toLocator) {
const fromInfo = pnpApi.getPackageInformation(fromLocator);
if (toLocator.reference === fromInfo.packageDependencies.get(toLocator.name)) {
pnpPackageName = toLocator.name;
}
else {
// Aliased dependencies
for (const [name, reference] of fromInfo.packageDependencies) {
if (Array.isArray(reference)) {
if (reference[0] === toLocator.name && reference[1] === toLocator.reference) {
pnpPackageName = name;
break;
}
}
}
}

if (!parts) {
const toInfo = pnpApi.getPackageInformation(toLocator);
parts = {
topLevelNodeModulesIndex: undefined,
topLevelPackageNameIndex: undefined,
// The last character from packageLocation is the trailing "/", we want to point to it
packageRootIndex: toInfo.packageLocation.length - 1,
fileNameIndex: path.lastIndexOf(`/`),
};
}
}
}

if (!parts) {
return undefined;
}
Expand Down Expand Up @@ -937,19 +993,26 @@ function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCan
return undefined;
}

const globalTypingsCacheLocation = host.getGlobalTypingsCacheLocation && host.getGlobalTypingsCacheLocation();
// Get a path that's relative to node_modules or the importing file's path
// if node_modules folder is in this folder or any of its parent folders, no need to keep it.
const pathToTopLevelNodeModules = getCanonicalFileName(moduleSpecifier.substring(0, parts.topLevelNodeModulesIndex));
if (!(startsWith(sourceDirectory, pathToTopLevelNodeModules) || globalTypingsCacheLocation && startsWith(getCanonicalFileName(globalTypingsCacheLocation), pathToTopLevelNodeModules))) {
return undefined;
// If PnP is enabled the node_modules entries we'll get will always be relevant even if they
// are located in a weird path apparently outside of the source directory
if (typeof process.versions.pnp === "undefined") {
const globalTypingsCacheLocation = host.getGlobalTypingsCacheLocation && host.getGlobalTypingsCacheLocation();
// Get a path that's relative to node_modules or the importing file's path
// if node_modules folder is in this folder or any of its parent folders, no need to keep it.
const pathToTopLevelNodeModules = getCanonicalFileName(moduleSpecifier.substring(0, parts.topLevelNodeModulesIndex));
if (!(startsWith(sourceDirectory, pathToTopLevelNodeModules) || globalTypingsCacheLocation && startsWith(getCanonicalFileName(globalTypingsCacheLocation), pathToTopLevelNodeModules))) {
return undefined;
}
}

// If the module was found in @types, get the actual Node package name
const nodeModulesDirectoryName = moduleSpecifier.substring(parts.topLevelPackageNameIndex + 1);
const packageName = getPackageNameFromTypesPackageName(nodeModulesDirectoryName);
const nodeModulesDirectoryName = typeof pnpPackageName !== "undefined"
? pnpPackageName + moduleSpecifier.substring(parts.packageRootIndex)
: moduleSpecifier.substring(parts.topLevelPackageNameIndex! + 1);

const packageNameFromPath = getPackageNameFromTypesPackageName(nodeModulesDirectoryName);
// For classic resolution, only allow importing from node_modules/@types, not other node_modules
return getEmitModuleResolutionKind(options) === ModuleResolutionKind.Classic && packageName === nodeModulesDirectoryName ? undefined : packageName;
return getEmitModuleResolutionKind(options) === ModuleResolutionKind.Classic && packageNameFromPath === nodeModulesDirectoryName ? undefined : packageNameFromPath;

function tryDirectoryWithPackageJson(packageRootIndex: number): { moduleFileToTry: string, packageRootPath?: string, blockedByExports?: true, verbatimFromExports?: true } {
const packageRootPath = path.substring(0, packageRootIndex);
Expand All @@ -964,8 +1027,8 @@ function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCan
// The package name that we found in node_modules could be different from the package
// name in the package.json content via url/filepath dependency specifiers. We need to
// use the actual directory name, so don't look at `packageJsonContent.name` here.
const nodeModulesDirectoryName = packageRootPath.substring(parts.topLevelPackageNameIndex + 1);
const packageName = getPackageNameFromTypesPackageName(nodeModulesDirectoryName);
const nodeModulesDirectoryName = packageRootPath.substring(parts!.topLevelPackageNameIndex! + 1);
const packageName = getPackageNameFromTypesPackageName(pnpPackageName ? pnpPackageName : nodeModulesDirectoryName);
const conditions = getConditions(options, importMode === ModuleKind.ESNext);
const fromExports = packageJsonContent.exports
? tryGetModuleNameFromExports(options, path, packageRootPath, packageName, packageJsonContent.exports, conditions)
Expand Down Expand Up @@ -1031,7 +1094,7 @@ function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCan
}
else {
// No package.json exists; an index.js will still resolve as the package name
const fileName = getCanonicalFileName(moduleFileToTry.substring(parts.packageRootIndex + 1));
const fileName = getCanonicalFileName(moduleFileToTry.substring(parts!.packageRootIndex + 1));
if (fileName === "index.d.ts" || fileName === "index.js" || fileName === "index.ts" || fileName === "index.tsx") {
return { moduleFileToTry, packageRootPath };
}
Expand Down
77 changes: 77 additions & 0 deletions src/compiler/pnp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { getDirectoryPath, resolvePath } from "./path";

export function getPnpApi(path: string) {
if (typeof process.versions.pnp === "undefined") {
return;
}

const {findPnpApi} = require("module");
if (findPnpApi) {
return findPnpApi(`${path}/`);
}
}

export function getPnpApiPath(path: string): string | undefined {
// eslint-disable-next-line no-null/no-null
return getPnpApi(path)?.resolveRequest("pnpapi", /*issuer*/ null);
}

export function getPnpTypeRoots(currentDirectory: string) {
const pnpApi = getPnpApi(currentDirectory);
if (!pnpApi) {
return [];
}

// Some TS consumers pass relative paths that aren't normalized
currentDirectory = resolvePath(currentDirectory);

const currentPackage = pnpApi.findPackageLocator(`${currentDirectory}/`);
if (!currentPackage) {
return [];
}

const {packageDependencies} = pnpApi.getPackageInformation(currentPackage);

const typeRoots: string[] = [];
for (const [name, referencish] of Array.from<any>(packageDependencies.entries())) {
// eslint-disable-next-line no-null/no-null
if (name.startsWith(`@types/`) && referencish !== null) {
const dependencyLocator = pnpApi.getLocator(name, referencish);
const {packageLocation} = pnpApi.getPackageInformation(dependencyLocator);

typeRoots.push(getDirectoryPath(packageLocation));
}
}

return typeRoots;
}

export function isImportablePathPnp(fromPath: string, toPath: string): boolean {
const pnpApi = getPnpApi(fromPath);

const fromLocator = pnpApi.findPackageLocator(fromPath);
const toLocator = pnpApi.findPackageLocator(toPath);

// eslint-disable-next-line no-null/no-null
if (toLocator === null) {
return false;
}

const fromInfo = pnpApi.getPackageInformation(fromLocator);
const toReference = fromInfo.packageDependencies.get(toLocator.name);

if (toReference) {
return toReference === toLocator.reference;
}

// Aliased dependencies
for (const reference of fromInfo.packageDependencies.values()) {
if (Array.isArray(reference)) {
if (reference[0] === toLocator.name && reference[1] === toLocator.reference) {
return true;
}
}
}

return false;
}
4 changes: 4 additions & 0 deletions src/compiler/sys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1717,6 +1717,10 @@ export let sys: System = (() => {
}

function isFileSystemCaseSensitive(): boolean {
// The PnP runtime is always case-sensitive
if (typeof process.versions.pnp !== `undefined`) {
return true;
}
// win32\win64 are case insensitive platforms
if (platform === "win32" || platform === "win64") {
return false;
Expand Down
9 changes: 9 additions & 0 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10073,6 +10073,15 @@ export interface NodeModulePathParts {
readonly packageRootIndex: number;
readonly fileNameIndex: number;
}

/** @internal */
export interface PackagePathParts {
readonly topLevelNodeModulesIndex: undefined;
readonly topLevelPackageNameIndex: undefined;
readonly packageRootIndex: number;
readonly fileNameIndex: number;
}

/** @internal */
export function getNodeModulePathParts(fullPath: string): NodeModulePathParts | undefined {
// If fullPath can't be valid module file within node_modules, returns undefined.
Expand Down
Loading

0 comments on commit 5f12e35

Please sign in to comment.