diff --git a/README.md b/README.md index dd2c214..3c39b17 100644 --- a/README.md +++ b/README.md @@ -146,7 +146,7 @@ The public API consists of these functions: export interface ExplicitParams { baseUrl: string; paths: { [key: string]: Array }; - mainFields?: Array; + mainFields?: (string | string[])[]; addMatchAll?: boolean; cwd?: string; } @@ -202,14 +202,14 @@ export interface MatchPath { * Creates a function that can resolve paths according to tsconfig paths property. * @param absoluteBaseUrl Absolute version of baseUrl as specified in tsconfig. * @param paths The paths as specified in tsconfig. - * @param mainFields A list of package.json field names to try when resolving module files. + * @param mainFields A list of package.json field names to try when resolving module files. Select a nested field using an array of field names. * @param addMatchAll Add a match-all "*" rule if none is present * @returns a function that can resolve paths. */ export function createMatchPath( absoluteBaseUrl: string, paths: { [key: string]: Array }, - mainFields: string[] = ["main"], + mainFields: (string | string[])[] = ["main"], addMatchAll: boolean = true ): MatchPath { ``` @@ -226,7 +226,7 @@ The `createMatchPath` function will create a function that can match paths. It a * @param readJson Function that can read json from a path (useful for testing). * @param fileExists Function that checks for existence of a file at a path (useful for testing). * @param extensions File extensions to probe for (useful for testing). - * @param mainFields A list of package.json field names to try when resolving module files. + * @param mainFields A list of package.json field names to try when resolving module files. Select a nested field using an array of field names. * @returns the found path, or undefined if no path was found. */ export function matchFromAbsolutePaths( @@ -235,7 +235,7 @@ export function matchFromAbsolutePaths( readJson: Filesystem.ReadJsonSync = Filesystem.readJsonFromDiskSync, fileExists: Filesystem.FileExistsSync = Filesystem.fileExistsSync, extensions: Array = Object.keys(require.extensions), - mainFields: string[] = ["main"] + mainFields: (string | string[])[] = ["main"] ): string | undefined { ``` diff --git a/src/__tests__/data/match-path-data.ts b/src/__tests__/data/match-path-data.ts index e9c07c2..75b6288 100644 --- a/src/__tests__/data/match-path-data.ts +++ b/src/__tests__/data/match-path-data.ts @@ -7,7 +7,7 @@ export interface OneTest { readonly skip?: boolean; readonly absoluteBaseUrl: string; readonly paths: { [key: string]: Array }; - readonly mainFields?: string[]; + readonly mainFields?: (string | string[])[]; readonly addMatchAll?: boolean; readonly existingFiles: ReadonlyArray; readonly requestedModule: string; @@ -149,6 +149,17 @@ export const tests: ReadonlyArray = [ extensions: [".ts", ".js"], expectedPath: join("/root", "location", "mylibjs", "kalle.js"), }, + { + name: "should resolve nested main fields", + absoluteBaseUrl: "/root/", + paths: { "lib/*": ["location/*"] }, + mainFields: [["esnext", "main"]], + packageJson: { esnext: { main: "./main.js" } }, + existingFiles: [join("/root", "location", "mylibjs", "main.js")], + extensions: [".ts", ".js"], + requestedModule: "lib/mylibjs", + expectedPath: join("/root", "location", "mylibjs", "main.js"), + }, { name: "should ignore advanced field mappings in package.json", absoluteBaseUrl: "/root/", diff --git a/src/config-loader.ts b/src/config-loader.ts index 796a518..1ea0c0c 100644 --- a/src/config-loader.ts +++ b/src/config-loader.ts @@ -4,7 +4,7 @@ import * as path from "path"; export interface ExplicitParams { baseUrl: string; paths: { [key: string]: Array }; - mainFields?: Array; + mainFields?: (string | string[])[]; addMatchAll?: boolean; } @@ -24,7 +24,7 @@ export interface ConfigLoaderSuccessResult { baseUrl?: string; absoluteBaseUrl: string; paths: { [key: string]: Array }; - mainFields?: Array; + mainFields?: (string | string[])[]; addMatchAll?: boolean; } diff --git a/src/filesystem.ts b/src/filesystem.ts index e77a083..bdac28b 100644 --- a/src/filesystem.ts +++ b/src/filesystem.ts @@ -4,7 +4,7 @@ import * as fs from "fs"; * Typing for the fields of package.json we care about */ export interface PackageJson { - [key: string]: string; + [key: string]: string | PackageJson; } /** diff --git a/src/match-path-async.ts b/src/match-path-async.ts index 9405a82..1fc7fea 100644 --- a/src/match-path-async.ts +++ b/src/match-path-async.ts @@ -27,7 +27,7 @@ export interface MatchPathAsyncCallback { export function createMatchPathAsync( absoluteBaseUrl: string, paths: { [key: string]: Array }, - mainFields: string[] = ["main"], + mainFields: (string | string[])[] = ["main"], addMatchAll: boolean = true ): MatchPathAsync { const absolutePaths = MappingEntry.getAbsoluteMappingEntries( @@ -64,7 +64,7 @@ export function matchFromAbsolutePathsAsync( fileExists: Filesystem.FileExistsAsync = Filesystem.fileExistsAsync, extensions: ReadonlyArray = Object.keys(require.extensions), callback: MatchPathAsyncCallback, - mainFields: string[] = ["main"] + mainFields: (string | string[])[] = ["main"] ): void { const tryPaths = TryPath.getPathsToTry( extensions, @@ -88,7 +88,7 @@ export function matchFromAbsolutePathsAsync( function findFirstExistingMainFieldMappedFile( packageJson: Filesystem.PackageJson, - mainFields: string[], + mainFields: (string | string[])[], packageJsonPath: string, fileExistsAsync: Filesystem.FileExistsAsync, doneCallback: (err?: Error, filepath?: string) => void, @@ -108,7 +108,11 @@ function findFirstExistingMainFieldMappedFile( index + 1 ); - const mainFieldMapping = packageJson[mainFields[index]]; + const mainFieldSelector = mainFields[index]; + const mainFieldMapping = + typeof mainFieldSelector === "string" + ? packageJson[mainFieldSelector] + : mainFieldSelector.reduce((obj, key) => obj[key], packageJson); if (typeof mainFieldMapping !== "string") { // Skip mappings that are not pointers to replacement files return tryNext(); @@ -136,7 +140,7 @@ function findFirstExistingPath( fileExists: Filesystem.FileExistsAsync, doneCallback: MatchPathAsyncCallback, index: number = 0, - mainFields: string[] = ["main"] + mainFields: (string | string[])[] = ["main"] ): void { const tryPath = tryPaths[index]; if ( diff --git a/src/match-path-sync.ts b/src/match-path-sync.ts index 9acb10b..65c38be 100644 --- a/src/match-path-sync.ts +++ b/src/match-path-sync.ts @@ -20,14 +20,14 @@ export interface MatchPath { * * @param absoluteBaseUrl Absolute version of baseUrl as specified in tsconfig. * @param paths The paths as specified in tsconfig. - * @param mainFields A list of package.json field names to try when resolving module files. + * @param mainFields A list of package.json field names to try when resolving module files. Select a nested field using an array of field names. * @param addMatchAll Add a match-all "*" rule if none is present * @returns a function that can resolve paths. */ export function createMatchPath( absoluteBaseUrl: string, paths: { [key: string]: Array }, - mainFields: string[] = ["main"], + mainFields: (string | string[])[] = ["main"], addMatchAll: boolean = true ): MatchPath { const absolutePaths = MappingEntry.getAbsoluteMappingEntries( @@ -60,7 +60,7 @@ export function createMatchPath( * @param readJson Function that can read json from a path (useful for testing). * @param fileExists Function that checks for existence of a file at a path (useful for testing). * @param extensions File extensions to probe for (useful for testing). - * @param mainFields A list of package.json field names to try when resolving module files. + * @param mainFields A list of package.json field names to try when resolving module files. Select a nested field using an array of field names. * @returns the found path, or undefined if no path was found. */ export function matchFromAbsolutePaths( @@ -69,7 +69,7 @@ export function matchFromAbsolutePaths( readJson: Filesystem.ReadJsonSync = Filesystem.readJsonFromDiskSync, fileExists: Filesystem.FileExistsSync = Filesystem.fileExistsSync, extensions: Array = Object.keys(require.extensions), - mainFields: string[] = ["main"] + mainFields: (string | string[])[] = ["main"] ): string | undefined { const tryPaths = TryPath.getPathsToTry( extensions, @@ -86,13 +86,16 @@ export function matchFromAbsolutePaths( function findFirstExistingMainFieldMappedFile( packageJson: Filesystem.PackageJson, - mainFields: string[], + mainFields: (string | string[])[], packageJsonPath: string, fileExists: Filesystem.FileExistsSync ): string | undefined { for (let index = 0; index < mainFields.length; index++) { - const mainFieldName = mainFields[index]; - const candidateMapping = packageJson[mainFieldName]; + const mainFieldSelector = mainFields[index]; + const candidateMapping = + typeof mainFieldSelector === "string" + ? packageJson[mainFieldSelector] + : mainFieldSelector.reduce((obj, key) => obj[key], packageJson); if (candidateMapping && typeof candidateMapping === "string") { const candidateFilePath = path.join( path.dirname(packageJsonPath), @@ -111,7 +114,7 @@ function findFirstExistingPath( tryPaths: ReadonlyArray, readJson: Filesystem.ReadJsonSync = Filesystem.readJsonFromDiskSync, fileExists: Filesystem.FileExistsSync, - mainFields: string[] = ["main"] + mainFields: (string | string[])[] = ["main"] ): string | undefined { for (const tryPath of tryPaths) { if (