Skip to content

Commit

Permalink
Merge pull request #1 from codesandbox/pkg-imports
Browse files Browse the repository at this point in the history
feat: Support package.json#imports
  • Loading branch information
DeMoorJasper authored Nov 15, 2022
2 parents 58c5226 + dfdc5ac commit 00f0628
Show file tree
Hide file tree
Showing 23 changed files with 228 additions and 66 deletions.
2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,8 @@
"@swc/core": "^1.3.14",
"@types/jest": "^29.2.2",
"@types/node": "^18.11.9",
"@types/picomatch": "^2.3.0",
"esbuild": "^0.15.13",
"jest": "^29.3.1",
"picomatch": "npm:browser-picomatch@^2.3.1",
"typescript": "^4.8.4",
"prettier": "^2.7.1"
}
Expand Down
Empty file.
17 changes: 17 additions & 0 deletions src/fixture/node_modules/chalk/package.json

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

Empty file.
Empty file.
Empty file.
Empty file.
16 changes: 16 additions & 0 deletions src/fixture/node_modules/imports-glob/package.json

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

Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
4 changes: 3 additions & 1 deletion src/fixture/node_modules/rollup/dist/es/package.json

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

3 changes: 1 addition & 2 deletions src/fixture/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
"something": "./nested/test.js",
"aliasedfolder": "./nested",
"aliasedabsolute": "/nested",
"foo/bar": "./bar.js",
"glob/*/*": "./nested/$2"
"foo/bar": "./bar.js"
},
"exports": {
"a-custom-export": "./nested"
Expand Down
44 changes: 35 additions & 9 deletions src/resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import { ModuleNotFoundError } from './errors/ModuleNotFound';
const FIXTURE_PATH = path.join(__dirname, 'fixture');

// alias/exports/main keys, sorted from high to low priority
const MAIN_PKG_FIELDS = ['module', 'browser', 'main', 'jsnext:main'];
const PKG_ALIAS_FIELDS = ['browser', 'alias'];
const EXPORTS_KEYS = ['browser', 'development', 'default', 'require', 'import'];
const MAIN_KEYS = ['module', 'browser', 'main', 'jsnext:main'];
const ALIAS_KEYS = ['browser', 'alias'];
const ENV_KEYS = ['browser', 'development', 'default', 'require', 'import'];

const readFiles = (dir: string, rootPath: string, files: Map<string, string>) => {
const entries = fs.readdirSync(dir);
Expand Down Expand Up @@ -44,9 +44,9 @@ describe('resolve', () => {
isFileSync,
readFile,
readFileSync,
mainFields: MAIN_PKG_FIELDS,
aliasFields: PKG_ALIAS_FIELDS,
exportKeys: EXPORTS_KEYS,
mainFields: MAIN_KEYS,
aliasFields: ALIAS_KEYS,
environmentKeys: ENV_KEYS,
};

describe('file paths', () => {
Expand Down Expand Up @@ -364,7 +364,7 @@ describe('resolve', () => {
});
});

describe('package#exports', () => {
describe.only('package#exports', () => {
it('should alias package.exports root export', () => {
const resolved = resolver.resolveSync('package-exports', {
...baseConfig,
Expand Down Expand Up @@ -393,7 +393,7 @@ describe('resolve', () => {
expect(resolved).toBe('/node_modules/package-exports/src/components/a.js');
});

it('should alias package.exports subdirectory globs', () => {
it.only('should alias package.exports subdirectory globs', () => {
const resolved = resolver.resolveSync('@zendesk/laika/esm/laika', {
...baseConfig,
filename: '/index.tsx',
Expand Down Expand Up @@ -453,14 +453,40 @@ describe('resolve', () => {
...baseConfig,
filename: '/node_modules/rollup/dist/es/rollup.js',
extensions: ['.ts', '.tsx', '.js', '.jsx'],
exportKeys: ['node', 'import', 'require', 'default'],
environmentKeys: ['node', 'import', 'require', 'default'],
mainFields: ['module', 'main'],
aliasFields: [],
});
expect(resolved).toBe('/node_modules/rollup/dist/es/rollup.js');
});
});

describe('package#imports', () => {
it('chalk', () => {
const resolved = resolver.resolveSync('#ansi-styles', {
...baseConfig,
filename: '/node_modules/chalk/index.js',
extensions: ['.ts', '.tsx', '.js', '.jsx'],
environmentKeys: ['node', 'import', 'require', 'default'],
mainFields: ['module', 'main'],
aliasFields: [],
});
expect(resolved).toBe('/node_modules/chalk/source/vendor/ansi-styles/index.js');
});

it('imports glob', () => {
const resolved = resolver.resolveSync('#test/a', {
...baseConfig,
filename: '/node_modules/imports-glob/index.js',
extensions: ['.ts', '.tsx', '.js', '.jsx'],
environmentKeys: ['node', 'import', 'require', 'default'],
mainFields: ['module', 'main'],
aliasFields: [],
});
expect(resolved).toBe('/node_modules/imports-glob/source/vendor/test/a.js');
});
});

describe('normalize module specifier', () => {
it('normalize module specifier', () => {
expect(normalizeModuleSpecifier('/test//fluent-d')).toBe('/test/fluent-d');
Expand Down
60 changes: 48 additions & 12 deletions src/resolver.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
/* eslint-disable no-else-return */
/* eslint-disable no-continue */
import picomatch from 'picomatch';

import * as pathUtils from './utils/path';
import { ModuleNotFoundError } from './errors/ModuleNotFound';
import { EMPTY_SHIM } from './utils/constants';
import { replaceGlob } from './utils/glob';

import { ProcessedPackageJSON, processPackageJSON } from './utils/pkg-json';
import { FnIsFile, FnIsFileSync, FnReadFile, FnReadFileSync, getParentDirectories } from './utils/fs';
Expand Down Expand Up @@ -33,8 +32,11 @@ export interface IResolveOptionsInput {
* sorted from high to low priority
* */
aliasFields: string[];
/** Export keys from high to low priority */
exportKeys: string[];
/**
* Environment keys from high to low priority
* Used for exports and imports field in pkg.json
*/
environmentKeys: string[];
}

interface IResolveOptions extends IResolveOptionsInput {
Expand All @@ -60,7 +62,7 @@ function normalizeResolverOptions(opts: IResolveOptionsInput): IResolveOptions {
resolverCache: opts.resolverCache || new Map(),
mainFields: opts.mainFields,
aliasFields: opts.aliasFields,
exportKeys: opts.exportKeys,
environmentKeys: opts.environmentKeys,
skipTsConfig: !!opts.skipTsConfig,
};
}
Expand All @@ -82,6 +84,29 @@ function resolveFile(filepath: string, dir: string): string {
}
}

function resolvePkgImport(specifier: string, pkgJson: IFoundPackageJSON): string {
const pkgImports = pkgJson.content.imports;
if (!pkgImports) return specifier;

if (pkgImports[specifier]) {
return pkgImports[specifier];
}

for (const [importKey, importValue] of Object.entries(pkgImports)) {
if (!importKey.includes('*')) {
continue;
}

const match = replaceGlob(importKey, importValue, specifier);
if (match) {
return match;
}
}

return specifier;
}

// This might be interesting for improving exports support: https://github.com/lukeed/resolve.exports
function resolveAlias(pkgJson: IFoundPackageJSON, filename: string): string {
const aliases = pkgJson.content.aliases;

Expand All @@ -103,15 +128,14 @@ function resolveAlias(pkgJson: IFoundPackageJSON, filename: string): string {
continue;
}

for (const aliasKey of Object.keys(aliases)) {
for (const [aliasKey, aliasValue] of Object.entries(aliases)) {
if (!aliasKey.includes('*')) {
continue;
}

const re = picomatch.makeRe(aliasKey, { capture: true });
if (re.test(relativeFilepath)) {
const val = aliases[aliasKey];
aliasedPath = relativeFilepath.replace(re, val);
const match = replaceGlob(aliasKey, aliasValue, relativeFilepath);
if (match) {
aliasedPath = match;
if (aliasedPath.startsWith(relativeFilepath)) {
const newAddition = aliasedPath.substr(relativeFilepath.length);
if (!newAddition.includes('/') && relativeFilepath.endsWith(newAddition)) {
Expand Down Expand Up @@ -256,7 +280,7 @@ class Resolver {
pathUtils.dirname(packageFilePath),
opts.mainFields,
opts.aliasFields,
opts.exportKeys
opts.environmentKeys
);
opts.resolverCache.set(packageFilePath, packageContent);
} catch (err) {
Expand Down Expand Up @@ -374,6 +398,17 @@ class Resolver {
return foundFile;
}

// $MakeMeSync
async resolvePkgImports(specifier: string, opts: IResolveOptions): Promise<string> {
// Imports always have the `#` prefix
if (specifier[0] !== '#') {
return specifier;
}

const pkgJson = await this.findPackageJSON(opts.filename, opts);
return resolvePkgImport(specifier, pkgJson);
}

// $RemoveMe
resolveSync(moduleSpecifier: string, inputOpts: IResolveOptionsInput): string {
throw new Error('Not compiled');
Expand All @@ -382,7 +417,8 @@ class Resolver {
// $MakeMeSync
async resolve(moduleSpecifier: string, inputOpts: IResolveOptionsInput): Promise<string> {
const opts = normalizeResolverOptions(inputOpts);
return this.internalResolve(moduleSpecifier, opts); // $MakeMeSync
const specifier = await this.resolvePkgImports(moduleSpecifier, opts);
return this.internalResolve(specifier, opts); // $MakeMeSync
}
}

Expand Down
28 changes: 13 additions & 15 deletions src/utils/__snapshots__/pkg-json.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,19 @@ exports[`process package.json Should correctly handle nested pkg#exports fields
{
"aliases": {
"/node_modules/solid-js": "/node_modules/solid-js/dist/solid.cjs",
"/node_modules/solid-js/dist/*": "/node_modules/solid-js/dist/$1",
"/node_modules/solid-js/dist/*": "/node_modules/solid-js/dist/*",
"/node_modules/solid-js/dist/server.cjs": "/node_modules/solid-js/dist/solid.cjs",
"/node_modules/solid-js/dist/server.js": "/node_modules/solid-js/dist/solid.js",
"/node_modules/solid-js/h": "/node_modules/solid-js/h/dist/h.cjs",
"/node_modules/solid-js/h/dist/*": "/node_modules/solid-js/h/dist/$1",
"/node_modules/solid-js/h/dist/*": "/node_modules/solid-js/h/dist/*",
"/node_modules/solid-js/html": "/node_modules/solid-js/html/dist/html.cjs",
"/node_modules/solid-js/html/dist/*": "/node_modules/solid-js/html/dist/$1",
"/node_modules/solid-js/html/dist/*": "/node_modules/solid-js/html/dist/*",
"/node_modules/solid-js/store": "/node_modules/solid-js/store/dist/store.cjs",
"/node_modules/solid-js/store/dist/*": "/node_modules/solid-js/store/dist/$1",
"/node_modules/solid-js/store/dist/*": "/node_modules/solid-js/store/dist/*",
"/node_modules/solid-js/universal": "/node_modules/solid-js/universal/dist/dev.cjs",
"/node_modules/solid-js/universal/dist/*": "/node_modules/solid-js/universal/dist/$1",
"/node_modules/solid-js/universal/dist/*": "/node_modules/solid-js/universal/dist/*",
"/node_modules/solid-js/web": "/node_modules/solid-js/web/dist/web.cjs",
"/node_modules/solid-js/web/dist/*": "/node_modules/solid-js/web/dist/$1",
"/node_modules/solid-js/web/dist/*": "/node_modules/solid-js/web/dist/*",
},
"imports": {},
}
Expand All @@ -27,18 +27,16 @@ exports[`process package.json Should correctly handle root pkg.json 1`] = `
"aliases": {
"aliased": "foo",
"aliased-file": "/bar.js",
"aliased-file/*": "/bar.js/$1",
"aliased/*": "foo/$1",
"aliased-file/*": "/bar.js/*",
"aliased/*": "foo/*",
"aliasedabsolute": "/nested",
"aliasedabsolute/*": "/nested/$1",
"aliasedabsolute/*": "/nested/*",
"aliasedfolder": "/nested",
"aliasedfolder/*": "/nested/$1",
"aliasedfolder/*": "/nested/*",
"foo/bar": "/bar.js",
"foo/bar/*": "/bar.js/$1",
"glob/*/*": "/nested/$2",
"glob/*/*/*": "/nested/$2/$1",
"foo/bar/*": "/bar.js/*",
"something": "/nested/test.js",
"something/*": "/nested/test.js/$1",
"something/*": "/nested/test.js/*",
},
"imports": {},
}
Expand Down Expand Up @@ -227,7 +225,7 @@ exports[`process package.json Should correctly process pkg.exports from @babel/r
"/node_modules/@babel/runtime/package.json": "/node_modules/@babel/runtime/package.json",
"/node_modules/@babel/runtime/regenerator": "/node_modules/@babel/runtime/regenerator/index.js",
"/node_modules/@babel/runtime/regenerator/": "/node_modules/@babel/runtime/regenerator/",
"/node_modules/@babel/runtime/regenerator/*.js": "/node_modules/@babel/runtime/regenerator/$1.js",
"/node_modules/@babel/runtime/regenerator/*.js": "/node_modules/@babel/runtime/regenerator/*.js",
},
"imports": {},
}
Expand Down
19 changes: 7 additions & 12 deletions src/utils/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,44 +8,39 @@ type PackageExportObj = {
[key: string]: string | null | false | PackageExportType;
};

export function normalizePackageExport(filepath: string, pkgRoot: string): string {
return normalizeAliasFilePath(filepath.replace(/\*/g, '$1'), pkgRoot);
}

export function extractPathFromExport(
exportValue: PackageExportType,
pkgRoot: string,
exportKeys: string[],
isExport: boolean,
environmentKeys: string[]
): string | false {
if (!exportValue) {
return false;
}

if (typeof exportValue === 'string') {
return normalizePackageExport(exportValue, pkgRoot);
return normalizeAliasFilePath(exportValue, pkgRoot);
}

if (Array.isArray(exportValue)) {
const foundPaths = exportValue.map((v) => extractPathFromExport(v, pkgRoot, exportKeys, isExport)).filter(Boolean);
const foundPaths = exportValue.map((v) => extractPathFromExport(v, pkgRoot, environmentKeys)).filter(Boolean);
if (!foundPaths.length) {
return false;
}
return foundPaths[0];
}

if (typeof exportValue === 'object') {
for (const key of exportKeys) {
for (const key of environmentKeys) {
const exportFilename = exportValue[key];
if (exportFilename !== undefined) {
if (typeof exportFilename === 'string') {
return normalizePackageExport(exportFilename, pkgRoot);
return normalizeAliasFilePath(exportFilename, pkgRoot);
}
return extractPathFromExport(exportFilename, pkgRoot, exportKeys, isExport);
return extractPathFromExport(exportFilename, pkgRoot, environmentKeys);
}
}
return false;
}

throw new Error(`Unsupported ${isExport ? 'exports' : 'imports'} type ${typeof exportValue}`);
throw new Error(`Unsupported exports type ${typeof exportValue}`);
}
11 changes: 11 additions & 0 deletions src/utils/glob.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { replaceGlob } from './glob';

describe('glob utils', () => {
it('replace glob at the end', () => {
expect(replaceGlob('#test/*', './something/*/index.js', '#test/hello')).toBe('./something/hello/index.js');
});

it('replace glob in the middle', () => {
expect(replaceGlob('#test/*.js', './test/*.js', '#test/hello.js')).toBe('./test/hello.js');
});
});
Loading

0 comments on commit 00f0628

Please sign in to comment.