diff --git a/packages/sandpack-core/src/resolver/resolver.ts b/packages/sandpack-core/src/resolver/resolver.ts index a738b802304..6a3e99969f8 100644 --- a/packages/sandpack-core/src/resolver/resolver.ts +++ b/packages/sandpack-core/src/resolver/resolver.ts @@ -12,6 +12,7 @@ import { processTSConfig, getPotentialPathsFromTSConfig, } from './utils/tsconfig'; +import { replaceGlob } from './utils/glob'; export type ResolverCache = Map; @@ -219,17 +220,20 @@ function* resolveNodeModule( : yield* loadNearestPackageJSON(pkgFilePath, opts, rootDir); if (pkgJson) { try { - return yield* resolve(pkgFilePath, { + return yield* internalResolve(pkgFilePath, { ...opts, filename: pkgJson.filepath, pkgJson, }); } catch (err) { if (!pkgSpecifierParts.filepath) { - return yield* resolve(pathUtils.join(pkgFilePath, 'index'), { - ...opts, - filename: pkgJson.filepath, - }); + return yield* internalResolve( + pathUtils.join(pkgFilePath, 'index'), + { + ...opts, + filename: pkgJson.filepath, + } + ); } throw err; @@ -264,6 +268,7 @@ function* findPackageJSON( content: { aliases: {}, hasExports: false, + imports: {}, }, }; } @@ -343,14 +348,54 @@ function* getTSConfig( return config; } -function* resolve( +function resolvePkgImport( + specifier: string, + pkgJson: IFoundPackageJSON +): string { + const pkgImports = pkgJson.content.imports; + if (!pkgImports) return specifier; + + if (pkgImports[specifier]) { + return pkgImports[specifier] as string; + } + + for (const [importKey, importValue] of Object.entries(pkgImports)) { + if (!importKey.includes('*')) { + continue; + } + + const match = replaceGlob(importKey, importValue, specifier); + if (match) { + return match; + } + } + + return specifier; +} + +function* resolvePkgImports( + specifier: string, + opts: IResolveOptions +): Generator { + // Imports always have the `#` prefix + if (specifier[0] !== '#') { + return specifier; + } + + const pkgJson = yield* findPackageJSON(opts.filename, opts); + const resolved = resolvePkgImport(specifier, pkgJson); + if (resolved !== specifier) { + opts.filename = pkgJson.filepath; + } + return resolved; +} + +function* internalResolve( moduleSpecifier: string, - inputOpts: IResolveOptionsInput, + opts: IResolveOptions, skipIndexExpansion: boolean = false ): Generator { const normalizedSpecifier = normalizeModuleSpecifier(moduleSpecifier); - const opts = normalizeResolverOptions(inputOpts); - const modulePath = yield* resolveModule(normalizedSpecifier, opts); if (modulePath[0] !== '/') { @@ -364,7 +409,7 @@ function* resolve( ); for (const potentialPath of potentialPaths) { try { - return yield* resolve(potentialPath, opts); + return yield* internalResolve(potentialPath, opts); } catch { // do nothing, it's probably a node_module in this case } @@ -390,7 +435,11 @@ function* resolve( try { const parts = moduleSpecifier.split('/'); if (!parts.length || !parts[parts.length - 1].startsWith('index')) { - foundFile = yield* resolve(moduleSpecifier + '/index', opts, true); + foundFile = yield* internalResolve( + moduleSpecifier + '/index', + opts, + true + ); } } catch (err) { // should throw ModuleNotFound for original specifier, not new one @@ -405,6 +454,16 @@ function* resolve( return foundFile; } +function* resolve( + moduleSpecifier: string, + inputOpts: IResolveOptionsInput, + skipIndexExpansion: boolean = false +): Generator { + const opts = normalizeResolverOptions(inputOpts); + const specifier = yield* resolvePkgImports(moduleSpecifier, opts); + return yield* internalResolve(specifier, opts, skipIndexExpansion); +} + export const resolver = gensync< ( moduleSpecifier: string, diff --git a/packages/sandpack-core/src/resolver/utils/__snapshots__/pkg-json.test.ts.snap b/packages/sandpack-core/src/resolver/utils/__snapshots__/pkg-json.test.ts.snap index 7b4367c2343..69d9b165830 100644 --- a/packages/sandpack-core/src/resolver/utils/__snapshots__/pkg-json.test.ts.snap +++ b/packages/sandpack-core/src/resolver/utils/__snapshots__/pkg-json.test.ts.snap @@ -19,6 +19,7 @@ exports[`process package.json Should correctly handle nested pkg#exports fields "/node_modules/solid-js/web/dist/*": "/node_modules/solid-js/web/dist/$1", }, "hasExports": true, + "imports": {}, } `; @@ -41,6 +42,7 @@ exports[`process package.json Should correctly handle root pkg.json 1`] = ` "something/*": "/nested/test.js/$1", }, "hasExports": false, + "imports": {}, } `; @@ -230,5 +232,6 @@ exports[`process package.json Should correctly process pkg.exports from @babel/r "/node_modules/@babel/runtime/regenerator/*.js": "/node_modules/@babel/runtime/regenerator/$1.js", }, "hasExports": true, + "imports": {}, } `; diff --git a/packages/sandpack-core/src/resolver/utils/exports.ts b/packages/sandpack-core/src/resolver/utils/exports.ts index 999994d0e11..7948f4b4c7d 100644 --- a/packages/sandpack-core/src/resolver/utils/exports.ts +++ b/packages/sandpack-core/src/resolver/utils/exports.ts @@ -3,16 +3,16 @@ import { normalizeAliasFilePath } from './alias'; // exports keys, sorted from high to low priority const EXPORTS_KEYS = ['browser', 'development', 'default', 'require', 'import']; -type PackageExportType = +export type PackageExportType = | string | null | false | PackageExportObj | PackageExportArr; -type PackageExportArr = Array; +export type PackageExportArr = Array; -type PackageExportObj = { +export type PackageExportObj = { [key: string]: string | null | false | PackageExportType; }; diff --git a/packages/sandpack-core/src/resolver/utils/glob.test.ts b/packages/sandpack-core/src/resolver/utils/glob.test.ts new file mode 100644 index 00000000000..6b16d7550f3 --- /dev/null +++ b/packages/sandpack-core/src/resolver/utils/glob.test.ts @@ -0,0 +1,24 @@ +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('replaces glob and target at the end', () => { + const input = replaceGlob( + '/@test/foo/*', + '/@test/foo/dist/*', + '/@test/foo/dist/index' + ); + expect(input).toBe('/@test/foo/dist/index'); + }); + + it('replace glob in the middle', () => { + expect(replaceGlob('#test/*.js', './test/*.js', '#test/hello.js')).toBe( + './test/hello.js' + ); + }); +}); diff --git a/packages/sandpack-core/src/resolver/utils/glob.ts b/packages/sandpack-core/src/resolver/utils/glob.ts new file mode 100644 index 00000000000..21c89e6f7c6 --- /dev/null +++ b/packages/sandpack-core/src/resolver/utils/glob.ts @@ -0,0 +1,43 @@ +export function replaceGlob( + source: string, + target: string, + specifier: string +): false | string { + const starIndex = source.indexOf('*'); + if (starIndex < 0) { + return false; + } + + const prefix = source.substring(0, starIndex); + const suffix = source.substring(starIndex + 1); + if ( + !specifier.startsWith(prefix) || + (suffix && !specifier.endsWith(suffix)) + ) { + return false; + } + + const targetStarLocation = target.indexOf('*'); + const targetBeforeStar = target.substring(0, targetStarLocation); + + if (specifier.indexOf(targetBeforeStar) > -1) { + return ( + targetBeforeStar + + specifier.substring(targetBeforeStar.length, specifier.length) + ); + } + + if (targetStarLocation < 0) { + return target; + } + + const globPart = specifier.substring( + prefix.length, + specifier.length - suffix.length + ); + return ( + target.substring(0, targetStarLocation) + + globPart + + target.substring(targetStarLocation + 1) + ); +} diff --git a/packages/sandpack-core/src/resolver/utils/imports.ts b/packages/sandpack-core/src/resolver/utils/imports.ts new file mode 100644 index 00000000000..ab1b7a04a60 --- /dev/null +++ b/packages/sandpack-core/src/resolver/utils/imports.ts @@ -0,0 +1,49 @@ +type PackageImportArr = Array; +type PackageImportType = + | string + | null + | false + | PackageImportObj + | PackageImportArr; +type PackageImportObj = { + [key: string]: string | null | false | PackageImportType; +}; + +export function extractSpecifierFromImport( + importValue: PackageImportType, + pkgRoot: string, + importKeys: string[] +): string | false { + if (!importValue) { + return false; + } + + if (typeof importValue === 'string') { + return importValue; + } + + if (Array.isArray(importValue)) { + const foundPaths = importValue + .map(v => extractSpecifierFromImport(v, pkgRoot, importKeys)) + .filter(Boolean); + if (!foundPaths.length) { + return false; + } + return foundPaths[0]; + } + + if (typeof importValue === 'object') { + for (const key of importKeys) { + const importFilename = importValue[key]; + if (importFilename !== undefined) { + if (typeof importFilename === 'string') { + return importFilename; + } + return extractSpecifierFromImport(importFilename, pkgRoot, importKeys); + } + } + return false; + } + + throw new Error(`Unsupported imports type ${typeof importValue}`); +} diff --git a/packages/sandpack-core/src/resolver/utils/pkg-json.ts b/packages/sandpack-core/src/resolver/utils/pkg-json.ts index 5fd3490031f..4c0f1ffa539 100644 --- a/packages/sandpack-core/src/resolver/utils/pkg-json.ts +++ b/packages/sandpack-core/src/resolver/utils/pkg-json.ts @@ -1,16 +1,23 @@ import { normalizeAliasFilePath } from './alias'; import { extractPathFromExport } from './exports'; import { EMPTY_SHIM } from './constants'; +import { extractSpecifierFromImport } from './imports'; // 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 IMPORTS_KEYS = ['browser', 'development', 'default', 'require', 'import']; + +function sortDescending(a: string, b: string) { + return b.length - a.length; +} type AliasesDict = { [key: string]: string }; export interface ProcessedPackageJSON { aliases: AliasesDict; hasExports: boolean; + imports: AliasesDict; } // See https://webpack.js.org/guides/package-exports/ for a good reference on how this should work @@ -20,7 +27,7 @@ export function processPackageJSON( pkgRoot: string ): ProcessedPackageJSON { if (!content || typeof content !== 'object') { - return { aliases: {}, hasExports: false }; + return { aliases: {}, hasExports: false, imports: {} }; } const aliases: AliasesDict = {}; @@ -83,5 +90,26 @@ export function processPackageJSON( } } - return { aliases, hasExports }; + // load imports + const imports: AliasesDict = {}; + if (content.imports) { + if (typeof content.imports === 'object') { + for (const importKey of Object.keys(content.imports).sort( + sortDescending + )) { + const value = extractSpecifierFromImport( + content.imports[importKey], + pkgRoot, + IMPORTS_KEYS + ); + imports[importKey] = value || EMPTY_SHIM; + } + } + } + + return { + aliases, + hasExports, + imports, + }; }