Skip to content

Commit

Permalink
Add support for providing a list of field names to try instead of jus…
Browse files Browse the repository at this point in the history
…t using "main" (#45)

* Allow passing a prioritized list of field names to use from package.json

* Add behavior for sync version of createMatchPath

* Add docstring for mainFields params

And reflect changes in README.md
  • Loading branch information
christoffer-dropbox authored and jonaskello committed Jun 12, 2018
1 parent ee472ad commit 4e402ae
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 22 deletions.
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,14 @@ export interface MatchPath {
* @param tsConfigPath The paths where tsconfig.json is located.
* @param baseUrl The baseUrl specified in tsconfig.
* @param paths The paths specified in tsconfig.
* @param mainFields A list of package.json field names to try when resolving module files.
*/
export function createMatchPath(
absoluteBaseUrl: string,
paths: { [key: string]: Array<string> }
): MatchPath
paths: { [key: string]: Array<string> },
mainFields: string[] = ["main"]
): MatchPath {

```
The `createMatchPath` function will create a function that can match paths. It accepts `baseUrl` and `paths` directly as they are specified in tsconfig and will handle resolving paths to absolute form. The created function has the signare specified by the type `MatchPath` above.
Expand All @@ -144,15 +147,17 @@ 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 existance 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.
* @returns the found path, or undefined if no path was found.
*/
export function matchFromAbsolutePaths(
absolutePathMappings: ReadonlyArray<MappingEntry.MappingEntry>,
requestedModule: string,
readJson: Filesystem.ReadJsonSync = Filesystem.readJsonFromDiskSync,
fileExists: Filesystem.FileExistsSync = Filesystem.fileExistsSync,
extensions: Array<string> = Object.keys(require.extensions)
): string | undefined
extensions: Array<string> = Object.keys(require.extensions),
mainFields: string[] = ["main"]
): string | undefined {
```
This function is lower level and requries that the paths as already been resolved to absolute form and sorted in correct order into an array.
Expand Down
2 changes: 1 addition & 1 deletion src/filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as fs from "fs";
* Typing for the fields of package.json we care about
*/
export interface PackageJson {
readonly main?: string;
[key: string]: string;
}

/**
Expand Down
42 changes: 32 additions & 10 deletions src/match-path-async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as path from "path";
import * as TryPath from "./try-path";
import * as MappingEntry from "./mapping-entry";
import * as Filesystem from "./filesystem";
import { getPrioritizedMainFieldName } from "./match-path-sync";

/**
* Function that can match a path async
Expand All @@ -25,7 +26,8 @@ export interface MatchPathAsyncCallback {
*/
export function createMatchPathAsync(
absoluteBaseUrl: string,
paths: { [key: string]: Array<string> }
paths: { [key: string]: Array<string> },
mainFields: string[] = ["main"]
): MatchPathAsync {
const absolutePaths = MappingEntry.getAbsoluteMappingEntries(
absoluteBaseUrl,
Expand All @@ -45,7 +47,8 @@ export function createMatchPathAsync(
readJson,
fileExists,
extensions,
callback
callback,
mainFields
);
}

Expand All @@ -58,7 +61,8 @@ export function matchFromAbsolutePathsAsync(
readJson: Filesystem.ReadJsonAsync = Filesystem.readJsonFromDiskAsync,
fileExists: Filesystem.FileExistsAsync = Filesystem.fileExistsAsync,
extensions: ReadonlyArray<string> = Object.keys(require.extensions),
callback: MatchPathAsyncCallback
callback: MatchPathAsyncCallback,
mainFields: string[] = ["main"]
): void {
const tryPaths = TryPath.getPathsToTry(
extensions,
Expand All @@ -70,7 +74,14 @@ export function matchFromAbsolutePathsAsync(
return callback();
}

findFirstExistingPath(tryPaths, readJson, fileExists, callback);
findFirstExistingPath(
tryPaths,
readJson,
fileExists,
callback,
0,
mainFields
);
}

// Recursive loop to probe for physical files
Expand All @@ -79,7 +90,8 @@ function findFirstExistingPath(
readJson: Filesystem.ReadJsonAsync,
fileExists: Filesystem.FileExistsAsync,
doneCallback: MatchPathAsyncCallback,
index: number = 0
index: number = 0,
mainFields: string[] = ["main"]
): void {
const tryPath = tryPaths[index];
if (
Expand All @@ -104,16 +116,24 @@ function findFirstExistingPath(
readJson,
fileExists,
doneCallback,
index + 1
index + 1,
mainFields
);
});
} else if (tryPath.type === "package") {
readJson(tryPath.path, (err, packageJson) => {
if (err) {
return doneCallback(err);
}
if (packageJson && packageJson.main) {
const file = path.join(path.dirname(tryPath.path), packageJson.main);
const mainFieldName = getPrioritizedMainFieldName(
packageJson,
mainFields
);
if (mainFieldName) {
const file = path.join(
path.dirname(tryPath.path),
packageJson[mainFieldName]
);
fileExists(file, (err2, exists) => {
if (err2) {
return doneCallback(err2);
Expand All @@ -128,7 +148,8 @@ function findFirstExistingPath(
readJson,
fileExists,
doneCallback,
index + 1
index + 1,
mainFields
);
});
} else {
Expand All @@ -142,7 +163,8 @@ function findFirstExistingPath(
readJson,
fileExists,
doneCallback,
index + 1
index + 1,
mainFields
);
}
});
Expand Down
51 changes: 44 additions & 7 deletions src/match-path-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ export interface MatchPath {
* @param tsConfigPath The paths where tsconfig.json is located.
* @param baseUrl The baseUrl specified in tsconfig.
* @param paths The paths specified in tsconfig.
* @param mainFields A list of package.json field names to try when resolving module files.
*/
export function createMatchPath(
absoluteBaseUrl: string,
paths: { [key: string]: Array<string> }
paths: { [key: string]: Array<string> },
mainFields: string[] = ["main"]
): MatchPath {
const absolutePaths = MappingEntry.getAbsoluteMappingEntries(
absoluteBaseUrl,
Expand All @@ -41,7 +43,8 @@ export function createMatchPath(
requestedModule,
readJson,
fileExists,
extensions
extensions,
mainFields
);
}

Expand All @@ -52,14 +55,16 @@ export function createMatchPath(
* @param readJson Function that can read json from a path (useful for testing).
* @param fileExists Function that checks for existance 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.
* @returns the found path, or undefined if no path was found.
*/
export function matchFromAbsolutePaths(
absolutePathMappings: ReadonlyArray<MappingEntry.MappingEntry>,
requestedModule: string,
readJson: Filesystem.ReadJsonSync = Filesystem.readJsonFromDiskSync,
fileExists: Filesystem.FileExistsSync = Filesystem.fileExistsSync,
extensions: Array<string> = Object.keys(require.extensions)
extensions: Array<string> = Object.keys(require.extensions),
mainFields: string[] = ["main"]
): string | undefined {
const tryPaths = TryPath.getPathsToTry(
extensions,
Expand All @@ -71,13 +76,38 @@ export function matchFromAbsolutePaths(
return undefined;
}

return findFirstExistingPath(tryPaths, readJson, fileExists);
return findFirstExistingPath(tryPaths, readJson, fileExists, mainFields);
}

/**
* Given a parsed package.json object, get the first field name that is defined
* in a list of prioritized field names to try.
*
* @param packageJson Parsed JSON object from package.json. May be undefined.
* @param mainFields A list of field names to try (in order)
* @returns The first matched field name in packageJson, or undefined.
*/
export function getPrioritizedMainFieldName(
packageJson: Filesystem.PackageJson | undefined,
mainFields: string[]
): string | undefined {
if (packageJson) {
for (let index = 0; index < mainFields.length; index++) {
const mainFieldName = mainFields[index];
if (packageJson[mainFieldName]) {
return mainFieldName;
}
}
}

return undefined;
}

function findFirstExistingPath(
tryPaths: ReadonlyArray<TryPath.TryPath>,
readJson: Filesystem.ReadJsonSync = Filesystem.readJsonFromDiskSync,
fileExists: Filesystem.FileExistsSync
fileExists: Filesystem.FileExistsSync,
mainFields: string[] = ["main"]
): string | undefined {
for (const tryPath of tryPaths) {
if (
Expand All @@ -91,8 +121,15 @@ function findFirstExistingPath(
}
} else if (tryPath.type === "package") {
const packageJson: Filesystem.PackageJson = readJson(tryPath.path);
if (packageJson && packageJson.main) {
const file = path.join(path.dirname(tryPath.path), packageJson.main);
const mainFieldName = getPrioritizedMainFieldName(
packageJson,
mainFields
);
if (mainFieldName) {
const file = path.join(
path.dirname(tryPath.path),
packageJson[mainFieldName]
);
if (fileExists(file)) {
// Not sure why we don't just return the full path? Why strip it?
return Filesystem.removeExtension(file);
Expand Down
25 changes: 25 additions & 0 deletions test/match-path-async-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,31 @@ describe("match-path-async", () => {
);
});

it("should resolve from list of fields by priority in package.json", done => {
const matchPath = createMatchPathAsync(
"/root/",
{
"lib/*": ["location/*"]
},
["missing", "browser", "main"]
);
const existingPath = join("/root", "location", "mylib", "christoffer.ts");
matchPath(
"lib/mylib",
(_path, callback) =>
callback(undefined, {
main: "./kalle.ts",
browser: "./christoffer.ts"
}),
(path, callback) => callback(undefined, path === existingPath),
undefined,
(_err, result) => {
assert.equal(result, removeExtension(existingPath), result);
done();
}
);
});

it("should resolve to with the help of baseUrl when not explicitly set", done => {
const matchPath = createMatchPathAsync("/root/", {});
const existingPath = join("/root", "mylib", "index.ts");
Expand Down
18 changes: 18 additions & 0 deletions test/match-path-sync-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,24 @@ describe("match-path-sync", () => {
assert.equal(result, existingPath);
});

it("should resolve from list of fields by priority in package.json", () => {
const matchPath = createMatchPath("/root/", { "lib/*": ["location/*"] }, [
"missing",
"browser",
"main"
]);
const existingPath = join("/root", "location", "mylibjs", "christofferjs");
// Make sure we escape the "."
const result = matchPath(
"lib/mylibjs",
(_: string) => ({ main: "./kallejs", browser: "./christofferjs" }),
(name: string) => name === existingPath,
[".ts", ".js"]
);

assert.equal(result, existingPath);
});

it("should resolve to with the help of baseUrl when not explicitly set", () => {
const matchPath = createMatchPath("/root/", {});
const existingPath = join("/root", "mylib", "index.ts");
Expand Down

0 comments on commit 4e402ae

Please sign in to comment.