mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-06-11 10:46:28 -05:00
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:
@@ -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> {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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"]
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user