diff --git a/packages/node-resolve/README.md b/packages/node-resolve/README.md index 010926007..4104edfcc 100755 --- a/packages/node-resolve/README.md +++ b/packages/node-resolve/README.md @@ -34,14 +34,18 @@ export default { input: 'src/index.js', output: { dir: 'output', - format: 'cjs', + format: 'cjs' }, - plugins: [nodeResolve()], + plugins: [nodeResolve()] }; ``` Then call `rollup` either via the [CLI](https://www.rollupjs.org/guide/en/#command-line-reference) or the [API](https://www.rollupjs.org/guide/en/#javascript-api). +## Package entrypoints + +This plugin supports the package entrypoints feature from node js, specified in the `exports` or `imports` field of a package. Check the [official documentation](https://nodejs.org/api/packages.html#packages_package_entry_points) for more information on how this works. + ## Options ### `exportConditions` @@ -62,6 +66,8 @@ Default: `false` If `true`, instructs the plugin to use the `"browser"` property in `package.json` files to specify alternative files to load for bundling. This is useful when bundling for a browser environment. Alternatively, a value of `'browser'` can be added to the `mainFields` option. If `false`, any `"browser"` properties in package files will be ignored. This option takes precedence over `mainFields`. +> This option does not work when a package is using [package entrypoints](https://nodejs.org/api/packages.html#packages_package_entry_points) + ### `moduleDirectories` Type: `Array[...String]`
@@ -169,9 +175,9 @@ export default { output: { file: 'bundle.js', format: 'iife', - name: 'MyModule', + name: 'MyModule' }, - plugins: [nodeResolve(), commonjs()], + plugins: [nodeResolve(), commonjs()] }; ``` @@ -203,7 +209,7 @@ The node resolve plugin uses `import` by default, you can opt into using the `re ```js this.resolve(importee, importer, { skipSelf: true, - custom: { 'node-resolve': { isRequire: true } }, + custom: { 'node-resolve': { isRequire: true } } }); ``` diff --git a/packages/node-resolve/src/index.js b/packages/node-resolve/src/index.js index 7896396f4..481fa8a60 100644 --- a/packages/node-resolve/src/index.js +++ b/packages/node-resolve/src/index.js @@ -7,7 +7,7 @@ import isModule from 'is-module'; import { isDirCached, isFileCached, readCachedFile } from './cache'; import { exists, readFile, realpath } from './fs'; -import { resolveImportSpecifiers } from './resolveImportSpecifiers'; +import resolveImportSpecifiers from './resolveImportSpecifiers'; import { getMainFields, getPackageName, normalizeInput } from './util'; import handleDeprecatedOptions from './deprecated-options'; diff --git a/packages/node-resolve/src/package/resolvePackageExports.js b/packages/node-resolve/src/package/resolvePackageExports.js new file mode 100644 index 000000000..0c233f00b --- /dev/null +++ b/packages/node-resolve/src/package/resolvePackageExports.js @@ -0,0 +1,48 @@ +import { + InvalidModuleSpecifierError, + InvalidConfigurationError, + isMappings, + isConditions, + isMixedExports +} from './utils'; +import resolvePackageTarget from './resolvePackageTarget'; +import resolvePackageImportsExports from './resolvePackageImportsExports'; + +async function resolvePackageExports(context, subpath, exports) { + if (isMixedExports(exports)) { + throw new InvalidConfigurationError( + context, + 'All keys must either start with ./, or without one.' + ); + } + + if (subpath === '.') { + let mainExport; + // If exports is a String or Array, or an Object containing no keys starting with ".", then + if (typeof exports === 'string' || Array.isArray(exports) || isConditions(exports)) { + mainExport = exports; + } else if (isMappings(exports)) { + mainExport = exports['.']; + } + + if (mainExport) { + const resolved = await resolvePackageTarget(context, { target: mainExport, subpath: '' }); + if (resolved) { + return resolved; + } + } + } else if (isMappings(exports)) { + const resolvedMatch = await resolvePackageImportsExports(context, { + matchKey: subpath, + matchObj: exports + }); + + if (resolvedMatch) { + return resolvedMatch; + } + } + + throw new InvalidModuleSpecifierError(context); +} + +export default resolvePackageExports; diff --git a/packages/node-resolve/src/package/resolvePackageImports.js b/packages/node-resolve/src/package/resolvePackageImports.js new file mode 100644 index 000000000..369c6cfd8 --- /dev/null +++ b/packages/node-resolve/src/package/resolvePackageImports.js @@ -0,0 +1,49 @@ +import path from 'path'; +import fs from 'fs'; +import { pathToFileURL } from 'url'; + +import { createBaseErrorMsg, InvalidModuleSpecifierError } from './utils'; +import resolvePackageImportsExports from './resolvePackageImportsExports'; + +function findPackageJson(base) { + const { root } = path.parse(base); + let current = base; + + while (current !== root && !current.endsWith('node_modules')) { + const pkgJsonPath = path.join(current, 'package.json'); + if (fs.existsSync(pkgJsonPath)) { + const pkgJsonString = fs.readFileSync(pkgJsonPath, 'utf-8'); + return { pkgJson: JSON.parse(pkgJsonString), pkgPath: current, pkgJsonPath }; + } + current = path.resolve(current, '..'); + } + return null; +} + +function resolvePackageImports({ importSpecifier, importer, conditions, resolveId }) { + const result = findPackageJson(importer); + if (!result) { + throw new Error(createBaseErrorMsg('. Could not find a parent package.json.')); + } + + const { pkgPath, pkgJsonPath, pkgJson } = result; + const pkgURL = pathToFileURL(`${pkgPath}/`); + const context = { importer, importSpecifier, pkgURL, pkgJsonPath, conditions, resolveId }; + + const { imports } = pkgJson; + if (!imports) { + throw new InvalidModuleSpecifierError(context, true); + } + + if (importSpecifier === '#' || importSpecifier.startsWith('#/')) { + throw new InvalidModuleSpecifierError(context, 'Invalid import specifier.'); + } + + return resolvePackageImportsExports(context, { + matchKey: importSpecifier, + matchObj: imports, + internal: true + }); +} + +export default resolvePackageImports; diff --git a/packages/node-resolve/src/package/resolvePackageImportsExports.js b/packages/node-resolve/src/package/resolvePackageImportsExports.js new file mode 100644 index 000000000..029937298 --- /dev/null +++ b/packages/node-resolve/src/package/resolvePackageImportsExports.js @@ -0,0 +1,44 @@ +/* eslint-disable no-await-in-loop */ +import resolvePackageTarget from './resolvePackageTarget'; + +import { InvalidModuleSpecifierError } from './utils'; + +async function resolvePackageImportsExports(context, { matchKey, matchObj, internal }) { + if (!matchKey.endsWith('*') && matchKey in matchObj) { + const target = matchObj[matchKey]; + const resolved = await resolvePackageTarget(context, { target, subpath: '', internal }); + return resolved; + } + + const expansionKeys = Object.keys(matchObj) + .filter((k) => k.endsWith('/') || k.endsWith('*')) + .sort((a, b) => b.length - a.length); + + for (const expansionKey of expansionKeys) { + const prefix = expansionKey.substring(0, expansionKey.length - 1); + + if (expansionKey.endsWith('*') && matchKey.startsWith(prefix)) { + const target = matchObj[expansionKey]; + const subpath = matchKey.substring(expansionKey.length - 1); + const resolved = await resolvePackageTarget(context, { + target, + subpath, + pattern: true, + internal + }); + return resolved; + } + + if (matchKey.startsWith(expansionKey)) { + const target = matchObj[expansionKey]; + const subpath = matchKey.substring(expansionKey.length); + + const resolved = await resolvePackageTarget(context, { target, subpath, internal }); + return resolved; + } + } + + throw new InvalidModuleSpecifierError(context, internal); +} + +export default resolvePackageImportsExports; diff --git a/packages/node-resolve/src/package/resolvePackageTarget.js b/packages/node-resolve/src/package/resolvePackageTarget.js new file mode 100644 index 000000000..4cb36230c --- /dev/null +++ b/packages/node-resolve/src/package/resolvePackageTarget.js @@ -0,0 +1,111 @@ +/* eslint-disable no-await-in-loop */ +import { pathToFileURL } from 'url'; + +import { isUrl, InvalidModuleSpecifierError, InvalidPackageTargetError } from './utils'; + +function includesInvalidSegments(pathSegments) { + return pathSegments + .split('/') + .slice(1) + .some((t) => ['.', '..', 'node_modules'].includes(t)); +} + +async function resolvePackageTarget(context, { target, subpath, pattern, internal }) { + if (target == null) { + return null; + } + + if (typeof target === 'string') { + if (!pattern && subpath.length > 0 && !target.endsWith('/')) { + throw new InvalidModuleSpecifierError(context); + } + + if (!target.startsWith('./')) { + if (internal && !['/', '../'].some((p) => target.startsWith(p)) && !isUrl(target)) { + // this is a bare package import, remap it and resolve it using regular node resolve + if (pattern) { + const result = await context.resolveId( + target.replace(/\*/g, subpath), + context.pkgURL.href + ); + return result ? pathToFileURL(result.location) : null; + } + + const result = await context.resolveId(`${target}${subpath}`, context.pkgURL.href); + return result ? pathToFileURL(result.location) : null; + } + throw new InvalidPackageTargetError(context, `Invalid mapping: "${target}".`); + } + + if (includesInvalidSegments(target)) { + throw new InvalidPackageTargetError(context, `Invalid mapping: "${target}".`); + } + + const resolvedTarget = new URL(target, context.pkgURL); + if (!resolvedTarget.href.startsWith(context.pkgURL.href)) { + throw new InvalidPackageTargetError( + context, + `Resolved to ${resolvedTarget.href} which is outside package ${context.pkgURL.href}` + ); + } + + if (includesInvalidSegments(subpath)) { + throw new InvalidModuleSpecifierError(context); + } + + if (pattern) { + return resolvedTarget.href.replace(/\*/g, subpath); + } + return new URL(subpath, resolvedTarget).href; + } + + if (Array.isArray(target)) { + let lastError; + for (const item of target) { + try { + const resolved = await resolvePackageTarget(context, { + target: item, + subpath, + pattern, + internal + }); + + if (resolved) { + return resolved; + } + } catch (error) { + if (!(error instanceof InvalidPackageTargetError)) { + throw error; + } else { + lastError = error; + } + } + } + + if (lastError) { + throw lastError; + } + return null; + } + + if (typeof target === 'object') { + for (const [key, value] of Object.entries(target)) { + if (key === 'default' || context.conditions.includes(key)) { + const resolved = await resolvePackageTarget(context, { + target: value, + subpath, + pattern, + internal + }); + + if (resolved) { + return resolved; + } + } + } + } + + throw new InvalidPackageTargetError(context, `Invalid exports field.`); +} + +export default resolvePackageTarget; diff --git a/packages/node-resolve/src/package/utils.js b/packages/node-resolve/src/package/utils.js new file mode 100644 index 000000000..07edb8683 --- /dev/null +++ b/packages/node-resolve/src/package/utils.js @@ -0,0 +1,51 @@ +export function isUrl(str) { + try { + return !!new URL(str); + } catch (_) { + return false; + } +} + +export function isConditions(exports) { + return typeof exports === 'object' && Object.keys(exports).every((k) => !k.startsWith('.')); +} + +export function isMappings(exports) { + return typeof exports === 'object' && !isConditions(exports); +} + +export function isMixedExports(exports) { + const keys = Object.keys(exports); + return keys.some((k) => k.startsWith('.')) && keys.some((k) => !k.startsWith('.')); +} + +export function createBaseErrorMsg(importSpecifier, importer) { + return `Could not resolve import "${importSpecifier}" in ${importer}`; +} + +export function createErrorMsg(context, reason, internal) { + const { importSpecifier, importer, pkgJsonPath } = context; + const base = createBaseErrorMsg(importSpecifier, importer); + const field = internal ? 'imports' : 'exports'; + return `${base} using ${field} defined in ${pkgJsonPath}.${reason ? ` ${reason}` : ''}`; +} + +export class ResolveError extends Error {} + +export class InvalidConfigurationError extends ResolveError { + constructor(context, reason) { + super(createErrorMsg(context, `Invalid "exports" field. ${reason}`)); + } +} + +export class InvalidModuleSpecifierError extends ResolveError { + constructor(context, internal) { + super(createErrorMsg(context, internal)); + } +} + +export class InvalidPackageTargetError extends ResolveError { + constructor(context, reason) { + super(createErrorMsg(context, reason)); + } +} diff --git a/packages/node-resolve/src/resolveImportSpecifiers.js b/packages/node-resolve/src/resolveImportSpecifiers.js index 8e224920f..d6cbe4027 100644 --- a/packages/node-resolve/src/resolveImportSpecifiers.js +++ b/packages/node-resolve/src/resolveImportSpecifiers.js @@ -1,175 +1,22 @@ import fs from 'fs'; -import path from 'path'; import { promisify } from 'util'; +import { fileURLToPath, pathToFileURL } from 'url'; import resolve from 'resolve'; import { getPackageInfo, getPackageName } from './util'; import { exists, realpath } from './fs'; import { isDirCached, isFileCached, readCachedFile } from './cache'; +import resolvePackageExports from './package/resolvePackageExports'; +import resolvePackageImports from './package/resolvePackageImports'; +import { ResolveError } from './package/utils'; const resolveImportPath = promisify(resolve); const readFile = promisify(fs.readFile); -const pathNotFoundError = (importPath, importer, subPath, pkgPath) => - new Error( - `Could not resolve import "${importPath}" in "${importer}".` + - ` Package subpath "${subPath}" is not defined by "exports" in ${pkgPath}` - ); - -function findExportKeyMatch(exportMap, subPath) { - if (subPath in exportMap) { - return subPath; - } - - const matchKeys = Object.keys(exportMap) - .filter((key) => key.endsWith('/') || key.endsWith('*')) - .sort((a, b) => b.length - a.length); - - for (const key of matchKeys) { - if (key.endsWith('*')) { - // star match: "./foo/*": "./foo/*.js" - const keyWithoutStar = key.substring(0, key.length - 1); - if (subPath.startsWith(keyWithoutStar)) { - return key; - } - } - - if (key.endsWith('/') && subPath.startsWith(key)) { - // directory match (deprecated by node): "./foo/": "./foo/.js" - return key; - } - - if (key === subPath) { - // literal match - return key; - } - } - return null; -} - -function mapSubPath({ importPath, importer, pkgJsonPath, subPath, key, value }) { - if (typeof value === 'string') { - if (typeof key === 'string' && key.endsWith('*')) { - // star match: "./foo/*": "./foo/*.js" - const keyWithoutStar = key.substring(0, key.length - 1); - const subPathAfterKey = subPath.substring(keyWithoutStar.length); - return value.replace(/\*/g, subPathAfterKey); - } - - if (value.endsWith('/')) { - // directory match (deprecated by node): "./foo/": "./foo/.js" - 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(importPath, importer, subPath, pkgJsonPath); -} - -function findEntrypoint({ - importPath, - importer, - pkgJsonPath, - subPath, - exportMap, - conditions, - key -}) { - if (typeof exportMap !== 'object') { - return mapSubPath({ importPath, importer, pkgJsonPath, subPath, key, value: 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({ - importPath, - importer, - pkgJsonPath, - subPath, - exportMap: subExportMap, - conditions, - key - }); - if (mappedSubPath) { - return mappedSubPath; - } - } - } - throw pathNotFoundError(importer, subPath, pkgJsonPath); -} - -export function findEntrypointTopLevel({ - importPath, - importer, - 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(importPath, importer, subPath, pkgJsonPath); - } - return mapSubPath({ importPath, importer, pkgJsonPath, subPath, key: null, value: 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(importPath, importer, subPath, pkgJsonPath); - } - exportMapForSubPath = exportMap; - } else { - // top level is sub path mappings, for example { ".": ..., "./foo": ..., "./bar": ... } - key = findExportKeyMatch(exportMap, subPath); - if (!key) { - throw pathNotFoundError(importPath, importer, subPath, pkgJsonPath); - } - exportMapForSubPath = exportMap[key]; - } - - return findEntrypoint({ - importPath, - importer, - pkgJsonPath, - subPath, - exportMap: exportMapForSubPath, - conditions, - key - }); -} - async function resolveId({ importer, - importPath, + importSpecifier, exportConditions, warn, packageInfoCache, @@ -215,8 +62,32 @@ async function resolveId({ let location; - const pkgName = getPackageName(importPath); - if (pkgName) { + const pkgName = getPackageName(importSpecifier); + if (importSpecifier.startsWith('#')) { + // this is a package internal import, resolve using package imports field + const resolveResult = await resolvePackageImports({ + importSpecifier, + importer, + conditions: exportConditions, + resolveId(id, parent) { + return resolveId({ + importSpecifier: id, + importer: parent, + exportConditions, + warn, + packageInfoCache, + extensions, + mainFields, + preserveSymlinks, + useBrowserOverrides, + baseDir, + moduleDirectories + }); + } + }); + location = fileURLToPath(resolveResult); + } else if (pkgName) { + // this is a bare import, resolve using package exports if possible let pkgJsonPath; let pkgJson; try { @@ -228,19 +99,20 @@ async function resolveId({ if (pkgJsonPath && pkgJson && pkgJson.exports) { try { - const packageSubPath = - pkgName === importPath ? '.' : `.${importPath.substring(pkgName.length)}`; - const mappedSubPath = findEntrypointTopLevel({ - importer, - importPath, - pkgJsonPath, - subPath: packageSubPath, - exportMap: pkgJson.exports, - conditions: exportConditions - }); - const pkgDir = path.dirname(pkgJsonPath); - location = path.join(pkgDir, mappedSubPath); + const subpath = + pkgName === importSpecifier ? '.' : `.${importSpecifier.substring(pkgName.length)}`; + const pkgDr = pkgJsonPath.replace('package.json', ''); + const pkgURL = pathToFileURL(pkgDr); + const resolvedPackageExport = await resolvePackageExports( + { importer, importSpecifier, pkgURL, pkgJsonPath, conditions: exportConditions }, + subpath, + pkgJson.exports + ); + location = fileURLToPath(resolvedPackageExport); } catch (error) { + if (!(error instanceof ResolveError)) { + throw error; + } warn(error); return null; } @@ -248,8 +120,9 @@ async function resolveId({ } if (!location) { + // package has no imports or exports, use classic node resolve try { - location = await resolveImportPath(importPath, resolveOptions); + location = await resolveImportPath(importSpecifier, resolveOptions); } catch (error) { if (error.code !== 'MODULE_NOT_FOUND') { throw error; @@ -275,7 +148,7 @@ async function resolveId({ // Resolve module specifiers in order. Promise resolves to the first module that resolves // successfully, or the error that resulted from the last attempted module resolution. -export async function resolveImportSpecifiers({ +export default async function resolveImportSpecifiers({ importer, importSpecifierList, exportConditions, @@ -292,7 +165,7 @@ export async function resolveImportSpecifiers({ // eslint-disable-next-line no-await-in-loop const resolved = await resolveId({ importer, - importPath: importSpecifierList[i], + importSpecifier: importSpecifierList[i], exportConditions, warn, packageInfoCache, diff --git a/packages/node-resolve/test/fixtures/exports-shorthand-fallback-conditions.js b/packages/node-resolve/test/fixtures/exports-shorthand-fallback-conditions.js new file mode 100644 index 000000000..b3420537b --- /dev/null +++ b/packages/node-resolve/test/fixtures/exports-shorthand-fallback-conditions.js @@ -0,0 +1,3 @@ +import exportsMapEntry from 'exports-shorthand-fallback-conditions/foo.js'; + +export default exportsMapEntry; diff --git a/packages/node-resolve/test/fixtures/exports-shorthand-fallback-error.js b/packages/node-resolve/test/fixtures/exports-shorthand-fallback-error.js new file mode 100644 index 000000000..fb0a69481 --- /dev/null +++ b/packages/node-resolve/test/fixtures/exports-shorthand-fallback-error.js @@ -0,0 +1,3 @@ +import exportsMapEntry from 'exports-shorthand-fallback-error'; + +export default exportsMapEntry; diff --git a/packages/node-resolve/test/fixtures/exports-star-specificity.js b/packages/node-resolve/test/fixtures/exports-star-specificity.js new file mode 100644 index 000000000..8a237eef0 --- /dev/null +++ b/packages/node-resolve/test/fixtures/exports-star-specificity.js @@ -0,0 +1,5 @@ +import a1 from 'exports-star-specificity/one/a'; +import a2 from 'exports-star-specificity/one/two/a'; +import a3 from 'exports-star-specificity/one/two/three/a'; + +export default { a1, a2, a3 }; diff --git a/packages/node-resolve/test/fixtures/imports-bare-dependency-exports.js b/packages/node-resolve/test/fixtures/imports-bare-dependency-exports.js new file mode 100644 index 000000000..cc165779f --- /dev/null +++ b/packages/node-resolve/test/fixtures/imports-bare-dependency-exports.js @@ -0,0 +1,3 @@ +import dependencyExports from 'imports-bare-dependency-exports'; + +export default dependencyExports; diff --git a/packages/node-resolve/test/fixtures/imports-bare.js b/packages/node-resolve/test/fixtures/imports-bare.js new file mode 100644 index 000000000..279fa081b --- /dev/null +++ b/packages/node-resolve/test/fixtures/imports-bare.js @@ -0,0 +1,3 @@ +import importBare from 'imports-bare'; + +export default importBare; diff --git a/packages/node-resolve/test/fixtures/imports-conditions.js b/packages/node-resolve/test/fixtures/imports-conditions.js new file mode 100644 index 000000000..efc9686c6 --- /dev/null +++ b/packages/node-resolve/test/fixtures/imports-conditions.js @@ -0,0 +1,3 @@ +import importsConditions from 'imports-conditions'; + +export default importsConditions; diff --git a/packages/node-resolve/test/fixtures/imports-pattern.js b/packages/node-resolve/test/fixtures/imports-pattern.js new file mode 100644 index 000000000..921516849 --- /dev/null +++ b/packages/node-resolve/test/fixtures/imports-pattern.js @@ -0,0 +1,3 @@ +import importsPattern from 'imports-pattern'; + +export default importsPattern; diff --git a/packages/node-resolve/test/fixtures/imports-relative.js b/packages/node-resolve/test/fixtures/imports-relative.js new file mode 100644 index 000000000..6a54dbe90 --- /dev/null +++ b/packages/node-resolve/test/fixtures/imports-relative.js @@ -0,0 +1,3 @@ +import importsRelative from 'imports-relative'; + +export default importsRelative; diff --git a/packages/node-resolve/test/fixtures/node_modules/exports-shorthand-fallback-conditions/index-mapped.js b/packages/node-resolve/test/fixtures/node_modules/exports-shorthand-fallback-conditions/index-mapped.js new file mode 100644 index 000000000..dc5589590 --- /dev/null +++ b/packages/node-resolve/test/fixtures/node_modules/exports-shorthand-fallback-conditions/index-mapped.js @@ -0,0 +1 @@ +export default 'MAIN MAPPED'; diff --git a/packages/node-resolve/test/fixtures/node_modules/exports-shorthand-fallback-conditions/index.js b/packages/node-resolve/test/fixtures/node_modules/exports-shorthand-fallback-conditions/index.js new file mode 100644 index 000000000..aaf1b3fe1 --- /dev/null +++ b/packages/node-resolve/test/fixtures/node_modules/exports-shorthand-fallback-conditions/index.js @@ -0,0 +1 @@ +export default 'MAIN'; diff --git a/packages/node-resolve/test/fixtures/node_modules/exports-shorthand-fallback-conditions/package.json b/packages/node-resolve/test/fixtures/node_modules/exports-shorthand-fallback-conditions/package.json new file mode 100644 index 000000000..6e6b43a9c --- /dev/null +++ b/packages/node-resolve/test/fixtures/node_modules/exports-shorthand-fallback-conditions/package.json @@ -0,0 +1,10 @@ +{ + "name": "exports-shorthand-fallback-conditions", + "main": "index.js", + "exports": { + "./foo.js": [ + { "require": "./not-index-mapped.js" }, + { "import": "./index-mapped.js" } + ] + } +} diff --git a/packages/node-resolve/test/fixtures/node_modules/exports-shorthand-fallback-error/index-mapped.js b/packages/node-resolve/test/fixtures/node_modules/exports-shorthand-fallback-error/index-mapped.js new file mode 100644 index 000000000..dc5589590 --- /dev/null +++ b/packages/node-resolve/test/fixtures/node_modules/exports-shorthand-fallback-error/index-mapped.js @@ -0,0 +1 @@ +export default 'MAIN MAPPED'; diff --git a/packages/node-resolve/test/fixtures/node_modules/exports-shorthand-fallback-error/index.js b/packages/node-resolve/test/fixtures/node_modules/exports-shorthand-fallback-error/index.js new file mode 100644 index 000000000..aaf1b3fe1 --- /dev/null +++ b/packages/node-resolve/test/fixtures/node_modules/exports-shorthand-fallback-error/index.js @@ -0,0 +1 @@ +export default 'MAIN'; diff --git a/packages/node-resolve/test/fixtures/node_modules/exports-shorthand-fallback-error/package.json b/packages/node-resolve/test/fixtures/node_modules/exports-shorthand-fallback-error/package.json new file mode 100644 index 000000000..1b9936932 --- /dev/null +++ b/packages/node-resolve/test/fixtures/node_modules/exports-shorthand-fallback-error/package.json @@ -0,0 +1,8 @@ +{ + "name": "exports-shorthand-fallback-error", + "main": "index.js", + "exports": [ + "./node_modules/not-index-mapped.js", + "./index-mapped.js" + ] +} diff --git a/packages/node-resolve/test/fixtures/node_modules/exports-star-specificity/foo-one/a.js b/packages/node-resolve/test/fixtures/node_modules/exports-star-specificity/foo-one/a.js new file mode 100644 index 000000000..bfbbe0e79 --- /dev/null +++ b/packages/node-resolve/test/fixtures/node_modules/exports-star-specificity/foo-one/a.js @@ -0,0 +1 @@ +export default 'foo-one a'; diff --git a/packages/node-resolve/test/fixtures/node_modules/exports-star-specificity/foo-three/a.js b/packages/node-resolve/test/fixtures/node_modules/exports-star-specificity/foo-three/a.js new file mode 100644 index 000000000..9f316b175 --- /dev/null +++ b/packages/node-resolve/test/fixtures/node_modules/exports-star-specificity/foo-three/a.js @@ -0,0 +1 @@ +export default 'foo-three a'; diff --git a/packages/node-resolve/test/fixtures/node_modules/exports-star-specificity/foo-two/a.js b/packages/node-resolve/test/fixtures/node_modules/exports-star-specificity/foo-two/a.js new file mode 100644 index 000000000..5ff612a02 --- /dev/null +++ b/packages/node-resolve/test/fixtures/node_modules/exports-star-specificity/foo-two/a.js @@ -0,0 +1 @@ +export default 'foo-two a'; diff --git a/packages/node-resolve/test/fixtures/node_modules/exports-star-specificity/package.json b/packages/node-resolve/test/fixtures/node_modules/exports-star-specificity/package.json new file mode 100644 index 000000000..b0640d2f5 --- /dev/null +++ b/packages/node-resolve/test/fixtures/node_modules/exports-star-specificity/package.json @@ -0,0 +1,9 @@ +{ + "name": "exports-star-specificity", + "main": "index.js", + "exports": { + "./one/*": "./foo-one/*.js", + "./one/two/*": "./foo-two/*.js", + "./one/two/three/*": "./foo-three/*.js" + } +} diff --git a/packages/node-resolve/test/fixtures/node_modules/imports-bare-dependency-exports/index.js b/packages/node-resolve/test/fixtures/node_modules/imports-bare-dependency-exports/index.js new file mode 100644 index 000000000..e5a2e82bc --- /dev/null +++ b/packages/node-resolve/test/fixtures/node_modules/imports-bare-dependency-exports/index.js @@ -0,0 +1 @@ +export default 'imports-bare-dependency-exports'; diff --git a/packages/node-resolve/test/fixtures/node_modules/imports-bare-dependency-exports/mapped-index.js b/packages/node-resolve/test/fixtures/node_modules/imports-bare-dependency-exports/mapped-index.js new file mode 100644 index 000000000..05ec10354 --- /dev/null +++ b/packages/node-resolve/test/fixtures/node_modules/imports-bare-dependency-exports/mapped-index.js @@ -0,0 +1 @@ +export default 'imports-bare-dependency-exports mapped'; diff --git a/packages/node-resolve/test/fixtures/node_modules/imports-bare-dependency-exports/package.json b/packages/node-resolve/test/fixtures/node_modules/imports-bare-dependency-exports/package.json new file mode 100644 index 000000000..d8b4375cf --- /dev/null +++ b/packages/node-resolve/test/fixtures/node_modules/imports-bare-dependency-exports/package.json @@ -0,0 +1,5 @@ +{ + "name": "imports-bare-dependency-exports", + "main": "index.js", + "exports": "./mapped-index.js" +} diff --git a/packages/node-resolve/test/fixtures/node_modules/imports-bare-dependency/index.js b/packages/node-resolve/test/fixtures/node_modules/imports-bare-dependency/index.js new file mode 100644 index 000000000..28cb8c99d --- /dev/null +++ b/packages/node-resolve/test/fixtures/node_modules/imports-bare-dependency/index.js @@ -0,0 +1 @@ +export default 'imports-bare-dependency'; diff --git a/packages/node-resolve/test/fixtures/node_modules/imports-bare-dependency/package.json b/packages/node-resolve/test/fixtures/node_modules/imports-bare-dependency/package.json new file mode 100644 index 000000000..cbfe2b85e --- /dev/null +++ b/packages/node-resolve/test/fixtures/node_modules/imports-bare-dependency/package.json @@ -0,0 +1,4 @@ +{ + "name": "imports-bare-dependency", + "main": "index.js" +} diff --git a/packages/node-resolve/test/fixtures/node_modules/imports-bare-exports/index.js b/packages/node-resolve/test/fixtures/node_modules/imports-bare-exports/index.js new file mode 100644 index 000000000..10f9b7b9f --- /dev/null +++ b/packages/node-resolve/test/fixtures/node_modules/imports-bare-exports/index.js @@ -0,0 +1,3 @@ +import foo from '#foo'; + +export default `imports-remap-exports imported ${foo}`; diff --git a/packages/node-resolve/test/fixtures/node_modules/imports-bare-exports/package.json b/packages/node-resolve/test/fixtures/node_modules/imports-bare-exports/package.json new file mode 100644 index 000000000..2c86cbf5b --- /dev/null +++ b/packages/node-resolve/test/fixtures/node_modules/imports-bare-exports/package.json @@ -0,0 +1,10 @@ +{ + "name": "imports-remap-exports", + "main": "index.js", + "imports": { + "#foo": "./src/foo.js" + }, + "exports": { + "./src/foo.js": "./src/foo.js" + } +} diff --git a/packages/node-resolve/test/fixtures/node_modules/imports-bare-exports/src/foo.js b/packages/node-resolve/test/fixtures/node_modules/imports-bare-exports/src/foo.js new file mode 100644 index 000000000..7dfbc7584 --- /dev/null +++ b/packages/node-resolve/test/fixtures/node_modules/imports-bare-exports/src/foo.js @@ -0,0 +1 @@ +export default './src/foo'; \ No newline at end of file diff --git a/packages/node-resolve/test/fixtures/node_modules/imports-bare/index.js b/packages/node-resolve/test/fixtures/node_modules/imports-bare/index.js new file mode 100644 index 000000000..e57ebace3 --- /dev/null +++ b/packages/node-resolve/test/fixtures/node_modules/imports-bare/index.js @@ -0,0 +1,3 @@ +import foo from '#foo'; + +export default `imports-bare imported ${foo}`; diff --git a/packages/node-resolve/test/fixtures/node_modules/imports-bare/package.json b/packages/node-resolve/test/fixtures/node_modules/imports-bare/package.json new file mode 100644 index 000000000..7855b68e2 --- /dev/null +++ b/packages/node-resolve/test/fixtures/node_modules/imports-bare/package.json @@ -0,0 +1,7 @@ +{ + "name": "imports-bare", + "main": "index.js", + "imports": { + "#foo": "imports-bare-dependency" + } +} diff --git a/packages/node-resolve/test/fixtures/node_modules/imports-conditions/index.js b/packages/node-resolve/test/fixtures/node_modules/imports-conditions/index.js new file mode 100644 index 000000000..91eb98077 --- /dev/null +++ b/packages/node-resolve/test/fixtures/node_modules/imports-conditions/index.js @@ -0,0 +1,3 @@ +import foo from '#foo'; + +export default `imports-conditions imported ${foo}`; diff --git a/packages/node-resolve/test/fixtures/node_modules/imports-conditions/package.json b/packages/node-resolve/test/fixtures/node_modules/imports-conditions/package.json new file mode 100644 index 000000000..b26b124ef --- /dev/null +++ b/packages/node-resolve/test/fixtures/node_modules/imports-conditions/package.json @@ -0,0 +1,10 @@ +{ + "name": "imports-conditions", + "main": "index.js", + "imports": { + "#foo": { + "import": "./src/foo.mjs", + "require": "./src/foo.cjs" + } + } +} diff --git a/packages/node-resolve/test/fixtures/node_modules/imports-conditions/src/foo.cjs b/packages/node-resolve/test/fixtures/node_modules/imports-conditions/src/foo.cjs new file mode 100644 index 000000000..c3a6a88d4 --- /dev/null +++ b/packages/node-resolve/test/fixtures/node_modules/imports-conditions/src/foo.cjs @@ -0,0 +1 @@ +export default './src/foo.cjs'; \ No newline at end of file diff --git a/packages/node-resolve/test/fixtures/node_modules/imports-conditions/src/foo.mjs b/packages/node-resolve/test/fixtures/node_modules/imports-conditions/src/foo.mjs new file mode 100644 index 000000000..d5fefb6c9 --- /dev/null +++ b/packages/node-resolve/test/fixtures/node_modules/imports-conditions/src/foo.mjs @@ -0,0 +1 @@ +export default './src/foo.mjs'; \ No newline at end of file diff --git a/packages/node-resolve/test/fixtures/node_modules/imports-pattern/foo/x-a.js b/packages/node-resolve/test/fixtures/node_modules/imports-pattern/foo/x-a.js new file mode 100644 index 000000000..900e061e6 --- /dev/null +++ b/packages/node-resolve/test/fixtures/node_modules/imports-pattern/foo/x-a.js @@ -0,0 +1 @@ +export default './foo/x-a.js'; \ No newline at end of file diff --git a/packages/node-resolve/test/fixtures/node_modules/imports-pattern/foo/x-b.js b/packages/node-resolve/test/fixtures/node_modules/imports-pattern/foo/x-b.js new file mode 100644 index 000000000..5aad5177b --- /dev/null +++ b/packages/node-resolve/test/fixtures/node_modules/imports-pattern/foo/x-b.js @@ -0,0 +1 @@ +export default './foo/x-b.js'; \ No newline at end of file diff --git a/packages/node-resolve/test/fixtures/node_modules/imports-pattern/index.js b/packages/node-resolve/test/fixtures/node_modules/imports-pattern/index.js new file mode 100644 index 000000000..60e7c2e99 --- /dev/null +++ b/packages/node-resolve/test/fixtures/node_modules/imports-pattern/index.js @@ -0,0 +1,6 @@ +import a from '#a'; +import b from '#b'; +import fooA from '#foo/a'; +import fooB from '#foo/b'; + +export default { a, b, fooA, fooB }; diff --git a/packages/node-resolve/test/fixtures/node_modules/imports-pattern/package.json b/packages/node-resolve/test/fixtures/node_modules/imports-pattern/package.json new file mode 100644 index 000000000..d68ed7f6d --- /dev/null +++ b/packages/node-resolve/test/fixtures/node_modules/imports-pattern/package.json @@ -0,0 +1,8 @@ +{ + "name": "imports-pattern", + "main": "index.js", + "imports": { + "#*": "./src/*.js", + "#foo/*": "./foo/x-*.js" + } +} diff --git a/packages/node-resolve/test/fixtures/node_modules/imports-pattern/src/a.js b/packages/node-resolve/test/fixtures/node_modules/imports-pattern/src/a.js new file mode 100644 index 000000000..e255acbcb --- /dev/null +++ b/packages/node-resolve/test/fixtures/node_modules/imports-pattern/src/a.js @@ -0,0 +1 @@ +export default './src/a.js'; \ No newline at end of file diff --git a/packages/node-resolve/test/fixtures/node_modules/imports-pattern/src/b.js b/packages/node-resolve/test/fixtures/node_modules/imports-pattern/src/b.js new file mode 100644 index 000000000..c5329cb02 --- /dev/null +++ b/packages/node-resolve/test/fixtures/node_modules/imports-pattern/src/b.js @@ -0,0 +1 @@ +export default './src/b.js'; \ No newline at end of file diff --git a/packages/node-resolve/test/fixtures/node_modules/imports-relative/index.js b/packages/node-resolve/test/fixtures/node_modules/imports-relative/index.js new file mode 100644 index 000000000..9e25a5280 --- /dev/null +++ b/packages/node-resolve/test/fixtures/node_modules/imports-relative/index.js @@ -0,0 +1,3 @@ +import foo from '#foo'; + +export default `imports-relative imported ${foo}`; diff --git a/packages/node-resolve/test/fixtures/node_modules/imports-relative/package.json b/packages/node-resolve/test/fixtures/node_modules/imports-relative/package.json new file mode 100644 index 000000000..0b9d86aca --- /dev/null +++ b/packages/node-resolve/test/fixtures/node_modules/imports-relative/package.json @@ -0,0 +1,7 @@ +{ + "name": "imports-relative", + "main": "index.js", + "imports": { + "#foo": "./src/foo.js" + } +} diff --git a/packages/node-resolve/test/fixtures/node_modules/imports-relative/src/foo.js b/packages/node-resolve/test/fixtures/node_modules/imports-relative/src/foo.js new file mode 100644 index 000000000..7dfbc7584 --- /dev/null +++ b/packages/node-resolve/test/fixtures/node_modules/imports-relative/src/foo.js @@ -0,0 +1 @@ +export default './src/foo'; \ No newline at end of file diff --git a/packages/node-resolve/test/package-entry-points.js b/packages/node-resolve/test/package-entry-points.js index 336760697..91c2e2c4a 100644 --- a/packages/node-resolve/test/package-entry-points.js +++ b/packages/node-resolve/test/package-entry-points.js @@ -135,7 +135,7 @@ test('handles main directory exports', async (t) => { }); test('logs a warning when using shorthand and importing a subpath', async (t) => { - t.plan(2); + t.plan(1); const errors = []; await rollup({ input: 'exports-shorthand-subpath.js', @@ -146,11 +146,10 @@ test('logs a warning when using shorthand and importing a subpath', async (t) => }); t.true(errors[0].message.includes('Could not resolve import "exports-shorthand/foo" in ')); - t.true(errors[0].message.includes('Package subpath "./foo" is not defined by "exports" in')); }); test('logs a warning when a subpath cannot be found', async (t) => { - t.plan(2); + t.plan(1); const errors = []; await rollup({ input: 'exports-non-existing-subpath.js', @@ -163,11 +162,10 @@ test('logs a warning when a subpath cannot be found', async (t) => { t.true( errors[0].message.includes('Could not resolve import "exports-non-existing-subpath/bar" in ') ); - t.true(errors[0].message.includes('Package subpath "./bar" is not defined by "exports" in')); }); test('prevents importing files not specified in exports map', async (t) => { - t.plan(2); + t.plan(1); const errors = []; await rollup({ input: 'exports-prevent-unspecified-subpath.js', @@ -180,7 +178,6 @@ test('prevents importing files not specified in exports map', async (t) => { t.true( errors[0].message.includes('Could not resolve import "exports-top-level-mappings/bar" in ') ); - t.true(errors[0].message.includes('Package subpath "./bar" is not defined by "exports" in')); }); test('uses "require" condition when a module is referenced with require', async (t) => { @@ -209,6 +206,23 @@ test('can use star pattern in exports field', async (t) => { t.deepEqual(module.exports, { a: 'A', b: 'B', c: 'C' }); }); +test('the most specific star pattern matches', async (t) => { + const bundle = await rollup({ + input: 'exports-star-specificity.js', + onwarn: () => { + t.fail('No warnings were expected'); + }, + plugins: [nodeResolve()] + }); + const { module } = await testBundle(t, bundle); + + t.deepEqual(module.exports, { + a1: 'foo-one a', + a2: 'foo-two a', + a3: 'foo-three a' + }); +}); + test('a literal match takes presedence', async (t) => { const bundle = await rollup({ input: 'exports-literal-specificity.js', @@ -222,7 +236,7 @@ test('a literal match takes presedence', async (t) => { t.deepEqual(module.exports, { a: 'foo a' }); }); -test('longest matching directory takes priority', async (t) => { +test('the most specific directory mapping pattern matches', async (t) => { const bundle = await rollup({ input: 'exports-directory-specificity.js', onwarn: () => { @@ -238,3 +252,86 @@ test('longest matching directory takes priority', async (t) => { a3: 'foo-three a' }); }); + +test('can resolve fallback with conditions', async (t) => { + const bundle = await rollup({ + input: 'exports-shorthand-fallback-conditions.js', + onwarn: () => { + t.fail('No warnings were expected'); + }, + plugins: [nodeResolve()] + }); + const { module } = await testBundle(t, bundle); + + t.deepEqual(module.exports, 'MAIN MAPPED'); +}); + +test('can resolve fallback with errors', async (t) => { + const bundle = await rollup({ + input: 'exports-shorthand-fallback-error.js', + onwarn: () => { + t.fail('No warnings were expected'); + }, + plugins: [nodeResolve()] + }); + const { module } = await testBundle(t, bundle); + + t.deepEqual(module.exports, 'MAIN MAPPED'); +}); + +test('can resolve a package import to a relative file', async (t) => { + const bundle = await rollup({ + input: 'imports-relative.js', + onwarn: () => { + t.fail('No warnings were expected'); + }, + plugins: [nodeResolve()] + }); + const { module } = await testBundle(t, bundle); + + t.deepEqual(module.exports, 'imports-relative imported ./src/foo'); +}); + +test('can resolve a package import to a bare import', async (t) => { + const bundle = await rollup({ + input: 'imports-bare.js', + onwarn: () => { + t.fail('No warnings were expected'); + }, + plugins: [nodeResolve()] + }); + const { module } = await testBundle(t, bundle); + + t.deepEqual(module.exports, 'imports-bare imported imports-bare-dependency'); +}); + +test('can resolve a package import with conditions', async (t) => { + const bundle = await rollup({ + input: 'imports-conditions.js', + onwarn: () => { + t.fail('No warnings were expected'); + }, + plugins: [nodeResolve()] + }); + const { module } = await testBundle(t, bundle); + + t.deepEqual(module.exports, 'imports-conditions imported ./src/foo.mjs'); +}); + +test('can resolve a package import with a pattern', async (t) => { + const bundle = await rollup({ + input: 'imports-pattern.js', + onwarn: () => { + t.fail('No warnings were expected'); + }, + plugins: [nodeResolve()] + }); + const { module } = await testBundle(t, bundle); + + t.deepEqual(module.exports, { + a: './src/a.js', + b: './src/b.js', + fooA: './foo/x-a.js', + fooB: './foo/x-b.js' + }); +});