Skip to content

Commit

Permalink
feat(resolve)!: support "exports" field
Browse files Browse the repository at this point in the history
  • Loading branch information
AviVahl committed Jul 30, 2023
1 parent 047bc6c commit a4edfd8
Show file tree
Hide file tree
Showing 4 changed files with 392 additions and 80 deletions.
6 changes: 3 additions & 3 deletions packages/commonjs/test/npm-packages.nodespec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe('commonjs module system - integration with existing npm packages', func
it('evaluates postcss successfully', () => {
const { requireFrom, requireCache } = createCjsModuleSystem({
fs,
resolver: createRequestResolver({ fs, target: 'node' }),
resolver: createRequestResolver({ fs, conditions: ['node', 'require'] }),
});
requireCache.set('path', { filename: 'path', id: 'path', exports: path, children: [] });
requireCache.set('url', { filename: 'url', id: 'url', exports: url, children: [] });
Expand All @@ -60,7 +60,7 @@ describe('commonjs module system - integration with existing npm packages', func
it('evaluates mocha successfully', () => {
const { requireFrom, requireCache } = createCjsModuleSystem({
fs,
resolver: createRequestResolver({ fs, target: 'node' }),
resolver: createRequestResolver({ fs, conditions: ['node', 'require'] }),
});
requireCache.set('path', { filename: 'path', id: 'path', exports: path, children: [] });
requireCache.set('stream', { filename: 'stream', id: 'stream', exports: stream, children: [] });
Expand All @@ -77,7 +77,7 @@ describe('commonjs module system - integration with existing npm packages', func
it('evaluates sass successfully', () => {
const { requireFrom, requireCache } = createCjsModuleSystem({
fs,
resolver: createRequestResolver({ fs, target: 'node' }),
resolver: createRequestResolver({ fs, conditions: ['node', 'require'] }),
});
requireCache.set('fs', { filename: 'fs', id: 'fs', exports: fs, children: [] });
requireCache.set('path', { filename: 'path', id: 'path', exports: path, children: [] });
Expand Down
82 changes: 73 additions & 9 deletions packages/resolve/src/request-resolver.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { PackageJson } from 'type-fest';
import type { RequestResolver, IRequestResolverOptions, IResolvedPackageJson, IResolutionOutput } from './types.js';
import type { IRequestResolverOptions, IResolutionOutput, IResolvedPackageJson, RequestResolver } from './types.js';

const defaultTarget = 'browser';
const defaultPackageRoots = ['node_modules'];
const defaultExtensions = ['.js', '.json'];
const defaultConditions = ['browser', 'import', 'require'];
const isRelative = (request: string) =>
request === '.' || request === '..' || request.startsWith('./') || request.startsWith('../');
const PACKAGE_JSON = 'package.json';
Expand All @@ -14,13 +14,16 @@ export function createRequestResolver(options: IRequestResolverOptions): Request
fs: { statSync, readFileSync, realpathSync, dirname, join, resolve, isAbsolute },
packageRoots = defaultPackageRoots,
extensions = defaultExtensions,
target = defaultTarget,
moduleField = target === 'browser',
conditions = defaultConditions,
resolvedPacakgesCache = new Map<string, IResolvedPackageJson | undefined>(),
alias = {},
fallback = {},
} = options;

const exportConditions = new Set(conditions);
const targetsBrowser = exportConditions.has('browser');
const targetsEsm = exportConditions.has('import');

const loadPackageJsonFromCached = wrapWithCache(loadPackageJsonFrom, resolvedPacakgesCache);
const remapUsingAlias = createRequestRemapper(alias);
const remapUsingFallback = createRequestRemapper(fallback);
Expand All @@ -39,7 +42,7 @@ export function createRequestResolver(options: IRequestResolverOptions): Request
if (!statSyncSafe(resolvedFilePath)?.isFile()) {
continue;
}
if (target === 'browser') {
if (targetsBrowser) {
const toPackageJson = findUpPackageJson(dirname(resolvedFilePath));
if (toPackageJson) {
visitedPaths.add(toPackageJson.filePath);
Expand Down Expand Up @@ -73,7 +76,7 @@ export function createRequestResolver(options: IRequestResolverOptions): Request
yield requestAlias;
}

if (!emittedCandidate && target === 'browser') {
if (!emittedCandidate && targetsBrowser) {
const fromPackageJson = findUpPackageJson(contextPath);
if (fromPackageJson) {
visitedPaths.add(fromPackageJson.filePath);
Expand Down Expand Up @@ -133,6 +136,7 @@ export function createRequestResolver(options: IRequestResolverOptions): Request
if (resolvedPackageJson !== undefined) {
visitedPaths.add(resolvedPackageJson.filePath);
}

const mainPath = resolvedPackageJson?.mainPath;

if (mainPath !== undefined) {
Expand All @@ -147,6 +151,19 @@ export function createRequestResolver(options: IRequestResolverOptions): Request
if (!statSyncSafe(packagesPath)?.isDirectory()) {
continue;
}
const [packageName, innerPath] = parsePackageSpecifier(request);
if (!packageName.length || (packageName.startsWith('@') && !packageName.includes('/'))) {
return;
}
const packageDirectoryPath = join(packagesPath, packageName);
const resolvedPackageJson = loadPackageJsonFromCached(packageDirectoryPath);
if (resolvedPackageJson !== undefined) {
visitedPaths.add(resolvedPackageJson.filePath);
}
if (resolvedPackageJson?.exports !== undefined && resolvedPackageJson.exports !== null) {
yield* exportsRequestPaths(packageDirectoryPath, resolvedPackageJson.exports, innerPath);
return;
}
const requestInPackages = join(packagesPath, request);
yield* fileRequestPaths(requestInPackages);
yield* directoryRequestPaths(requestInPackages, visitedPaths);
Expand Down Expand Up @@ -181,7 +198,7 @@ export function createRequestResolver(options: IRequestResolverOptions): Request

const { browser } = packageJson;
let browserMappings: Record<string, string | false> | undefined = undefined;
if (target === 'browser' && typeof browser === 'object' && browser !== null) {
if (targetsBrowser && typeof browser === 'object' && browser !== null) {
browserMappings = Object.create(null) as Record<string, string | false>;
for (const [from, to] of Object.entries(browser)) {
const resolvedFrom = isRelative(from) ? resolveRelative(join(directoryPath, from)) : from;
Expand All @@ -199,9 +216,34 @@ export function createRequestResolver(options: IRequestResolverOptions): Request
directoryPath,
mainPath,
browserMappings,
exports: packageJson.exports,
};
}

function* exportsRequestPaths(
directoryPath: string,
exports: PackageJson.Exports,
innerPath: string
): Generator<string> {
if (exports === null) {
return;
} else if (typeof exports === 'string') {
if (innerPath === '.') {
yield join(directoryPath, exports);
}
} else if (typeof exports === 'object' && !Array.isArray(exports)) {
for (const [key, value] of Object.entries(exports)) {
if (key === innerPath || (key === 'default' && innerPath === '.') || exportConditions.has(key)) {
if (typeof value === 'string') {
yield join(directoryPath, value);
} else if (value !== null && typeof value === 'object') {
yield* exportsRequestPaths(directoryPath, value, innerPath);
}
}
}
}
}

function resolveRemappedRequest(directoryPath: string, to: string | false): string | false | undefined {
if (to === false) {
return to;
Expand Down Expand Up @@ -249,9 +291,9 @@ export function createRequestResolver(options: IRequestResolverOptions): Request
}

function packageJsonTarget({ main, browser, module: moduleFieldValue }: PackageJson): string | undefined {
if (target === 'browser' && typeof browser === 'string') {
if (targetsBrowser && typeof browser === 'string') {
return browser;
} else if (moduleField && typeof moduleFieldValue === 'string') {
} else if (targetsEsm && typeof moduleFieldValue === 'string') {
return moduleFieldValue;
}
return typeof main === 'string' ? main : undefined;
Expand Down Expand Up @@ -345,3 +387,25 @@ interface TracedErrorConstructor extends ErrorConstructor {
stackTraceLimit?: number;
}
declare let Error: TracedErrorConstructor;

/**
* @example parsePackageSpecifier('react-dom') === ['react-dom', "."]
* @example parsePackageSpecifier('react-dom/client') === ['react-dom', './client']
* @example parsePackageSpecifier('@stylable/core') === ['@stylable/core', "./core"]
* @example parsePackageSpecifier('@stylable/core/dist/some-file') === ['@stylable/core', './dist/some-file']
*/
function parsePackageSpecifier(specifier: string): readonly [packageName: string, pathInPackage: string] {
const firstSlashIdx = specifier.indexOf('/');
if (firstSlashIdx === -1) {
return [specifier, '.'];
}
const isScopedPackage = specifier.startsWith('@');
if (isScopedPackage) {
const secondSlashIdx = specifier.indexOf('/', firstSlashIdx + 1);
return secondSlashIdx === -1
? [specifier, '.']
: [specifier.slice(0, secondSlashIdx), '.' + specifier.slice(secondSlashIdx)];
} else {
return [specifier.slice(0, firstSlashIdx), '.' + specifier.slice(firstSlashIdx)];
}
}
18 changes: 8 additions & 10 deletions packages/resolve/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { PackageJson } from 'type-fest';

export interface IRequestResolverOptions {
/**
* File system to use when resolving requests.
Expand All @@ -19,10 +21,12 @@ export interface IRequestResolverOptions {
extensions?: string[];

/**
* Whether to prefer the 'browser' field or 'main' field
* in `package.json`.
* Package export conditions to try resolving the request with.
*
* @default ['browser', 'import', 'require']
* @see https://nodejs.org/api/packages.html#conditional-exports
*/
target?: 'node' | 'browser';
conditions?: string[];

/**
* Cache for resolved packages. Map keys are directoryPaths.
Expand All @@ -43,13 +47,6 @@ export interface IRequestResolverOptions {
* Original request is attempted before fallback.
*/
fallback?: Record<string, string | false>;

/**
* Support the "module" field. Picked up over "main".
*
* @default true when "target" is set to "browser"
*/
moduleField?: boolean;
}

export interface IResolutionOutput {
Expand Down Expand Up @@ -105,4 +102,5 @@ export interface IResolvedPackageJson {
browserMappings?: {
[from: string]: string | false;
};
exports?: PackageJson['exports'];
}
Loading

0 comments on commit a4edfd8

Please sign in to comment.