-
-
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 cf92a06
Showing
54 changed files
with
665 additions
and
185 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; |
71 changes: 71 additions & 0 deletions
71
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,71 @@ | ||
/* eslint-disable no-await-in-loop */ | ||
import path from 'path'; | ||
import fs from 'fs'; | ||
import { pathToFileURL } from 'url'; | ||
import { promisify } from 'util'; | ||
|
||
import { createBaseErrorMsg, InvalidModuleSpecifierError } from './utils'; | ||
import resolvePackageImportsExports from './resolvePackageImportsExports'; | ||
|
||
const fileExists = promisify(fs.exists); | ||
|
||
function isModuleDir(current, moduleDirs) { | ||
return moduleDirs.some((dir) => current.endsWith(dir)); | ||
} | ||
|
||
async 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 (await fileExists(pkgJsonPath)) { | ||
const pkgJsonString = fs.readFileSync(pkgJsonPath, 'utf-8'); | ||
return { pkgJson: JSON.parse(pkgJsonString), pkgPath: current, pkgJsonPath }; | ||
} | ||
current = path.resolve(current, '..'); | ||
} | ||
return null; | ||
} | ||
|
||
async function resolvePackageImports({ | ||
importSpecifier, | ||
importer, | ||
moduleDirs, | ||
conditions, | ||
resolveId | ||
}) { | ||
const result = await 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; |
114 changes: 114 additions & 0 deletions
114
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,114 @@ | ||
/* 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 (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 | ||
}); | ||
|
||
// return if defined or null, but not undefined | ||
if (resolved !== undefined) { | ||
return resolved; | ||
} | ||
} catch (error) { | ||
if (!(error instanceof InvalidPackageTargetError)) { | ||
throw error; | ||
} else { | ||
lastError = error; | ||
} | ||
} | ||
} | ||
|
||
if (lastError) { | ||
throw lastError; | ||
} | ||
return null; | ||
} | ||
|
||
if (target && 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 | ||
}); | ||
|
||
// return if defined or null, but not undefined | ||
if (resolved !== undefined) { | ||
return resolved; | ||
} | ||
} | ||
} | ||
return undefined; | ||
} | ||
|
||
if (target === null) { | ||
return null; | ||
} | ||
|
||
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.