Skip to content

Commit

Permalink
feat(node-resolve): support package entry points
Browse files Browse the repository at this point in the history
  • Loading branch information
LarsDenBakker committed Sep 12, 2020
1 parent 6cd15b9 commit 784ebc7
Show file tree
Hide file tree
Showing 67 changed files with 493 additions and 0 deletions.
130 changes: 130 additions & 0 deletions packages/node-resolve/src/resolveId.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import fs from 'fs';
import path from 'path';
import { promisify } from 'util';

import resolve from 'resolve';

import { getPackageName } from './util';

const resolveImportPath = promisify(resolve);
const readFile = promisify(fs.readFile);

const pathNotFoundError = (subPath, pkgPath) =>
new Error(`Package subpath '${subPath}' is not defined by "exports" in ${pkgPath}`);

function findExportKeyMatch(keys, subPath) {
return keys.find((key) => (key.endsWith('/') ? subPath.startsWith(key) : key === subPath));
}

function mapSubPath(pkgJsonPath, subPath, key, value) {
if (typeof value === 'string') {
if (value.endsWith('/')) {
return `${value}${subPath.substring(key.length)}`;
}
// mapping is a string, for example { "./foo": "./dist/foo.js" }
return value;
}

if (Array.isArray(value)) {
// mapping is an array with fallbacks, for example { "./foo": ["foo:bar", "./dist/foo.js"] }
return value.find((v) => v.startsWith('./'));
}

throw pathNotFoundError(subPath, pkgJsonPath);
}

function findEntrypoint(pkgJsonPath, subPath, exportMap, conditions, key) {
if (typeof exportMap !== 'object') {
return mapSubPath(pkgJsonPath, subPath, key, exportMap);
}

// iterate conditions recursively, find the first that matches all conditions
for (const [condition, subExportMap] of Object.entries(exportMap)) {
if (conditions.includes(condition)) {
const mappedSubPath = findEntrypoint(pkgJsonPath, subPath, subExportMap, conditions, key);
if (mappedSubPath) {
return mappedSubPath;
}
}
}
throw pathNotFoundError(subPath, pkgJsonPath);
}

export function findEntrypointTopLevel(pkgJsonPath, subPath, exportMap, conditions) {
if (typeof exportMap !== 'object') {
// the export map shorthand, for example { exports: "./index.js" }
if (subPath !== '.') {
// shorthand only supports a main entrypoint
throw pathNotFoundError(subPath, pkgJsonPath);
}
return mapSubPath(pkgJsonPath, subPath, null, exportMap);
}

// export map is an object, the top level can be either conditions or sub path mappings
const keys = Object.keys(exportMap);
const isConditions = keys.every((k) => !k.startsWith('.'));
const isMappings = keys.every((k) => k.startsWith('.'));

if (!isConditions && !isMappings) {
throw new Error(
`Invalid package config ${pkgJsonPath}, "exports" cannot contain some keys starting with '.'` +
' and some not. The exports object must either be an object of package subpath keys or an object of main entry' +
' condition name keys only.'
);
}

let key = null;
let exportMapForSubPath;

if (isConditions) {
// top level is conditions, for example { "import": ..., "require": ..., "module": ... }
if (subPath !== '.') {
// package with top level conditions means it only supports a main entrypoint
throw pathNotFoundError(subPath, pkgJsonPath);
}
exportMapForSubPath = exportMap;
} else {
// top level is sub path mappings, for example { ".": ..., "./foo": ..., "./bar": ... }
key = findExportKeyMatch(keys, subPath);
if (!key) {
throw pathNotFoundError(subPath, pkgJsonPath);
}
exportMapForSubPath = exportMap[key];
}

return findEntrypoint(pkgJsonPath, subPath, exportMapForSubPath, conditions, key);
}

export default async function resolveId(importPath, options, exportConditions, warn) {
const pkgName = getPackageName(importPath);
if (pkgName) {
let pkgJsonPath;
let pkgJson;
try {
pkgJsonPath = await resolveImportPath(`${pkgName}/package.json`, options);
pkgJson = JSON.parse(await readFile(pkgJsonPath, 'utf-8'));
} catch (_) {
// if there is no package.json we defer to regular resolve behavior
}

if (pkgJsonPath && pkgJson && pkgJson.exports) {
try {
const packageSubPath =
pkgName === importPath ? '.' : `.${importPath.substring(pkgName.length)}`;
const mappedSubPath = findEntrypointTopLevel(
pkgJsonPath,
packageSubPath,
pkgJson.exports,
exportConditions
);
const pkgDir = path.dirname(pkgJsonPath);
return path.join(pkgDir, mappedSubPath);
} catch (error) {
warn(error);
return null;
}
}
}

return resolveImportPath(importPath, options);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import main from 'exports-conditions-fallback';

export default main;
5 changes: 5 additions & 0 deletions packages/node-resolve/test/fixtures/exports-directory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import a from 'exports-directory/foo/a.js';
import b from 'exports-directory/foo/b.js';
import c from 'exports-directory/foo/nested/c.js';

export default { a, b, c };
5 changes: 5 additions & 0 deletions packages/node-resolve/test/fixtures/exports-main-directory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import a from 'exports-main-directory/a.js';
import b from 'exports-main-directory/foo/b.js';
import c from 'exports-main-directory/foo/nested/c.js';

export default { a, b, c };
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import main from 'exports-mappings-and-conditions';
import foo from 'exports-mappings-and-conditions/foo';
import bar from 'exports-mappings-and-conditions/bar';

export default { main, foo, bar };
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import main from 'exports-nested-conditions';

export default main;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import bar from 'exports-non-existing-subpath/bar';

export default bar;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import bar from 'exports-top-level-mappings/bar';

export default bar;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import exportsMapEntry from 'exports-shorthand';

export default exportsMapEntry;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import exportsMapEntry from 'exports-shorthand/foo';

export default exportsMapEntry;
3 changes: 3 additions & 0 deletions packages/node-resolve/test/fixtures/exports-shorthand.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import exportsMapEntry from 'exports-shorthand';

export default exportsMapEntry;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import main from 'exports-top-level-conditions';

export default main;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import main from 'exports-top-level-mappings';
import foo from 'exports-top-level-mappings/foo';

export default { main, foo };

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 784ebc7

Please sign in to comment.