Follow and respect export maps when generating module specifiers (#46159)

* Follow and respect export maps when generating module specifiers

* Type baseline updates from master merge
This commit is contained in:
Wesley Wigham
2021-10-01 13:54:07 -07:00
committed by GitHub
parent 9ed49b6ba7
commit a02a7ab8e9
40 changed files with 1695 additions and 11 deletions

View File

@@ -1708,7 +1708,8 @@ namespace ts {
return idx === -1 ? { packageName: moduleName, rest: "" } : { packageName: moduleName.slice(0, idx), rest: moduleName.slice(idx + 1) };
}
function allKeysStartWithDot(obj: MapLike<unknown>) {
/* @internal */
export function allKeysStartWithDot(obj: MapLike<unknown>) {
return every(getOwnKeys(obj), k => startsWith(k, "."));
}
@@ -1922,14 +1923,15 @@ namespace ts {
}
return toSearchResult(/*value*/ undefined);
}
}
function isApplicableVersionedTypesKey(conditions: string[], key: string) {
if (conditions.indexOf("types") === -1) return false; // only apply versioned types conditions if the types condition is applied
if (!startsWith(key, "types@")) return false;
const range = VersionRange.tryParse(key.substring("types@".length));
if (!range) return false;
return range.test(version);
}
/* @internal */
export function isApplicableVersionedTypesKey(conditions: string[], key: string) {
if (conditions.indexOf("types") === -1) return false; // only apply versioned types conditions if the types condition is applied
if (!startsWith(key, "types@")) return false;
const range = VersionRange.tryParse(key.substring("types@".length));
if (!range) return false;
return range.test(version);
}
function loadModuleFromNearestNodeModulesDirectory(extensions: Extensions, moduleName: string, directory: string, state: ModuleResolutionState, cache: ModuleResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined): SearchResult<Resolved> {

View File

@@ -555,6 +555,77 @@ namespace ts.moduleSpecifiers {
}
}
const enum MatchingMode {
Exact,
Directory,
Pattern
}
function tryGetModuleNameFromExports(options: CompilerOptions, targetFilePath: string, packageDirectory: string, packageName: string, exports: unknown, conditions: string[], mode = MatchingMode.Exact): { moduleFileToTry: string } | undefined {
if (typeof exports === "string") {
const pathOrPattern = getNormalizedAbsolutePath(combinePaths(packageDirectory, exports), /*currentDirectory*/ undefined);
const extensionSwappedTarget = hasTSFileExtension(targetFilePath) ? removeFileExtension(targetFilePath) + tryGetJSExtensionForFile(targetFilePath, options) : undefined;
switch (mode) {
case MatchingMode.Exact:
if (comparePaths(targetFilePath, pathOrPattern) === Comparison.EqualTo || (extensionSwappedTarget && comparePaths(extensionSwappedTarget, pathOrPattern) === Comparison.EqualTo)) {
return { moduleFileToTry: packageName };
}
break;
case MatchingMode.Directory:
if (containsPath(pathOrPattern, targetFilePath)) {
const fragment = getRelativePathFromDirectory(pathOrPattern, targetFilePath, /*ignoreCase*/ false);
return { moduleFileToTry: getNormalizedAbsolutePath(combinePaths(combinePaths(packageName, exports), fragment), /*currentDirectory*/ undefined) };
}
break;
case MatchingMode.Pattern:
const starPos = pathOrPattern.indexOf("*");
const leadingSlice = pathOrPattern.slice(0, starPos);
const trailingSlice = pathOrPattern.slice(starPos + 1);
if (startsWith(targetFilePath, leadingSlice) && endsWith(targetFilePath, trailingSlice)) {
const starReplacement = targetFilePath.slice(leadingSlice.length, targetFilePath.length - trailingSlice.length);
return { moduleFileToTry: packageName.replace("*", starReplacement) };
}
if (extensionSwappedTarget && startsWith(extensionSwappedTarget, leadingSlice) && endsWith(extensionSwappedTarget, trailingSlice)) {
const starReplacement = extensionSwappedTarget.slice(leadingSlice.length, extensionSwappedTarget.length - trailingSlice.length);
return { moduleFileToTry: packageName.replace("*", starReplacement) };
}
break;
}
}
else if (Array.isArray(exports)) {
return forEach(exports, e => tryGetModuleNameFromExports(options, targetFilePath, packageDirectory, packageName, e, conditions));
}
else if (typeof exports === "object" && exports !== null) { // eslint-disable-line no-null/no-null
if (allKeysStartWithDot(exports as MapLike<unknown>)) {
// sub-mappings
// 3 cases:
// * directory mappings (legacyish, key ends with / (technically allows index/extension resolution under cjs mode))
// * pattern mappings (contains a *)
// * exact mappings (no *, does not end with /)
return forEach(getOwnKeys(exports as MapLike<unknown>), k => {
const subPackageName = getNormalizedAbsolutePath(combinePaths(packageName, k), /*currentDirectory*/ undefined);
const mode = endsWith(k, "/") ? MatchingMode.Directory
: stringContains(k, "*") ? MatchingMode.Pattern
: MatchingMode.Exact;
return tryGetModuleNameFromExports(options, targetFilePath, packageDirectory, subPackageName, (exports as MapLike<unknown>)[k], conditions, mode);
});
}
else {
// conditional mapping
for (const key of getOwnKeys(exports as MapLike<unknown>)) {
if (key === "default" || conditions.indexOf(key) >= 0 || isApplicableVersionedTypesKey(conditions, key)) {
const subTarget = (exports as MapLike<unknown>)[key];
const result = tryGetModuleNameFromExports(options, targetFilePath, packageDirectory, packageName, subTarget, conditions);
if (result) {
return result;
}
}
}
}
}
return undefined;
}
function tryGetModuleNameFromRootDirs(rootDirs: readonly string[], moduleFileName: string, sourceDirectory: string, getCanonicalFileName: (file: string) => string, ending: Ending, compilerOptions: CompilerOptions): string | undefined {
const normalizedTargetPath = getPathRelativeToRootDirs(moduleFileName, rootDirs, getCanonicalFileName);
if (normalizedTargetPath === undefined) {
@@ -586,7 +657,15 @@ namespace ts.moduleSpecifiers {
let moduleFileNameForExtensionless: string | undefined;
while (true) {
// If the module could be imported by a directory name, use that directory's name
const { moduleFileToTry, packageRootPath } = tryDirectoryWithPackageJson(packageRootIndex);
const { moduleFileToTry, packageRootPath, blockedByExports, verbatimFromExports } = tryDirectoryWithPackageJson(packageRootIndex);
if (getEmitModuleResolutionKind(options) !== ModuleResolutionKind.Classic) {
if (blockedByExports) {
return undefined; // File is under this package.json, but is not publicly exported - there's no way to name it via `node_modules` resolution
}
if (verbatimFromExports) {
return moduleFileToTry;
}
}
if (packageRootPath) {
moduleSpecifier = packageRootPath;
isPackageRootPath = true;
@@ -621,12 +700,21 @@ namespace ts.moduleSpecifiers {
// For classic resolution, only allow importing from node_modules/@types, not other node_modules
return getEmitModuleResolutionKind(options) === ModuleResolutionKind.Classic && packageName === nodeModulesDirectoryName ? undefined : packageName;
function tryDirectoryWithPackageJson(packageRootIndex: number) {
function tryDirectoryWithPackageJson(packageRootIndex: number): { moduleFileToTry: string, packageRootPath?: string, blockedByExports?: true, verbatimFromExports?: true } {
const packageRootPath = path.substring(0, packageRootIndex);
const packageJsonPath = combinePaths(packageRootPath, "package.json");
let moduleFileToTry = path;
if (host.fileExists(packageJsonPath)) {
const packageJsonContent = JSON.parse(host.readFile!(packageJsonPath)!);
// TODO: Inject `require` or `import` condition based on the intended import mode
const fromExports = packageJsonContent.exports && typeof packageJsonContent.name === "string" ? tryGetModuleNameFromExports(options, path, packageRootPath, packageJsonContent.name, packageJsonContent.exports, ["node", "types"]) : undefined;
if (fromExports) {
const withJsExtension = !hasTSFileExtension(fromExports.moduleFileToTry) ? fromExports : { moduleFileToTry: removeFileExtension(fromExports.moduleFileToTry) + tryGetJSExtensionForFile(fromExports.moduleFileToTry, options) };
return { ...withJsExtension, verbatimFromExports: true };
}
if (packageJsonContent.exports) {
return { moduleFileToTry: path, blockedByExports: true };
}
const versionPaths = packageJsonContent.typesVersions
? getPackageJsonTypesVersionsPaths(packageJsonContent.typesVersions)
: undefined;
@@ -641,7 +729,6 @@ namespace ts.moduleSpecifiers {
moduleFileToTry = combinePaths(packageRootPath, fromPaths);
}
}
// If the file is the main module, it can be imported by the package name
const mainFileRelative = packageJsonContent.typings || packageJsonContent.types || packageJsonContent.main;
if (isString(mainFileRelative)) {

View File

@@ -88,4 +88,101 @@ namespace ts {
commandLineArgs: ["-b", "/src", "--verbose"]
});
});
// https://github.com/microsoft/TypeScript/issues/44434 but with `module: node12`, some `exports` maps blocking direct access, and no `baseUrl`
describe("unittests:: tsbuild:: moduleSpecifiers:: synthesized module specifiers across referenced projects resolve correctly", () => {
verifyTsc({
scenario: "moduleSpecifiers",
subScenario: `synthesized module specifiers across projects resolve correctly`,
fs: () => loadProjectFromFiles({
"/src/src-types/index.ts": Utils.dedent`
export * from './dogconfig.js';`,
"/src/src-types/dogconfig.ts": Utils.dedent`
export interface DogConfig {
name: string;
}`,
"/src/src-dogs/index.ts": Utils.dedent`
export * from 'src-types';
export * from './lassie/lassiedog.js';
`,
"/src/src-dogs/dogconfig.ts": Utils.dedent`
import { DogConfig } from 'src-types';
export const DOG_CONFIG: DogConfig = {
name: 'Default dog',
};
`,
"/src/src-dogs/dog.ts": Utils.dedent`
import { DogConfig } from 'src-types';
import { DOG_CONFIG } from './dogconfig.js';
export abstract class Dog {
public static getCapabilities(): DogConfig {
return DOG_CONFIG;
}
}
`,
"/src/src-dogs/lassie/lassiedog.ts": Utils.dedent`
import { Dog } from '../dog.js';
import { LASSIE_CONFIG } from './lassieconfig.js';
export class LassieDog extends Dog {
protected static getDogConfig = () => LASSIE_CONFIG;
}
`,
"/src/src-dogs/lassie/lassieconfig.ts": Utils.dedent`
import { DogConfig } from 'src-types';
export const LASSIE_CONFIG: DogConfig = { name: 'Lassie' };
`,
"/src/tsconfig-base.json": Utils.dedent`
{
"compilerOptions": {
"declaration": true,
"module": "node12"
}
}`,
"/src/src-types/package.json": Utils.dedent`
{
"type": "module",
"exports": "./index.js"
}`,
"/src/src-dogs/package.json": Utils.dedent`
{
"type": "module",
"exports": "./index.js"
}`,
"/src/src-types/tsconfig.json": Utils.dedent`
{
"extends": "../tsconfig-base.json",
"compilerOptions": {
"composite": true
},
"include": [
"**/*"
]
}`,
"/src/src-dogs/tsconfig.json": Utils.dedent`
{
"extends": "../tsconfig-base.json",
"compilerOptions": {
"composite": true
},
"references": [
{ "path": "../src-types" }
],
"include": [
"**/*"
]
}`,
}, ""),
modifyFs: fs => {
fs.writeFileSync("/lib/lib.es2020.full.d.ts", tscWatch.libFile.content);
fs.symlinkSync("/src", "/src/src-types/node_modules");
fs.symlinkSync("/src", "/src/src-dogs/node_modules");
},
commandLineArgs: ["-b", "src/src-types", "src/src-dogs", "--verbose"]
});
});
}