-
-
Notifications
You must be signed in to change notification settings - Fork 591
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(node-resolve): support pkg imports and export array
- Loading branch information
1 parent
2283377
commit ba0262b
Showing
50 changed files
with
626 additions
and
186 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
48 changes: 48 additions & 0 deletions
48
packages/node-resolve/src/package/resolvePackageExports.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; |
61 changes: 61 additions & 0 deletions
61
packages/node-resolve/src/package/resolvePackageImports.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import path from 'path'; | ||
import fs from 'fs'; | ||
import { pathToFileURL } from 'url'; | ||
|
||
import { createBaseErrorMsg, InvalidModuleSpecifierError } from './utils'; | ||
import resolvePackageImportsExports from './resolvePackageImportsExports'; | ||
|
||
function isModuleDir(current, moduleDirs) { | ||
return moduleDirs.some((dir) => current.endsWith(dir)); | ||
} | ||
|
||
function findPackageJson(base, moduleDirs) { | ||
const { root } = path.parse(base); | ||
let current = base; | ||
|
||
while (current !== root && !isModuleDir(current, moduleDirs)) { | ||
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, moduleDirs, conditions, resolveId }) { | ||
const result = findPackageJson(importer, moduleDirs); | ||
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, | ||
moduleDirs, | ||
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; |
44 changes: 44 additions & 0 deletions
44
packages/node-resolve/src/package/resolvePackageImportsExports.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; |
111 changes: 111 additions & 0 deletions
111
packages/node-resolve/src/package/resolvePackageTarget.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
/* eslint-disable no-await-in-loop */ | ||
import { pathToFileURL } from 'url'; | ||
|
||
import { isUrl, InvalidModuleSpecifierError, InvalidPackageTargetError } from './utils'; | ||
|
||
function includesInvalidSegments(pathSegments, moduleDirs) { | ||
return pathSegments | ||
.split('/') | ||
.slice(1) | ||
.some((t) => ['.', '..', ...moduleDirs].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, context.moduleDirs)) { | ||
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, context.moduleDirs)) { | ||
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)); | ||
} | ||
} |
Oops, something went wrong.