import { dirname, resolve, extname, normalize, sep } from 'path'; import builtinList from 'builtin-modules'; import deepMerge from 'deepmerge'; import isModule from 'is-module'; import fs, { realpathSync } from 'fs'; import { promisify } from 'util'; import { createFilter } from '@rollup/pluginutils'; import resolveModule from 'resolve'; const exists = promisify(fs.exists); const readFile = promisify(fs.readFile); const realpath = promisify(fs.realpath); const stat = promisify(fs.stat); const onError = (error) => { if (error.code === 'ENOENT') { return false; } throw error; }; const makeCache = (fn) => { const cache = new Map(); const wrapped = async (param, done) => { if (cache.has(param) === false) { cache.set( param, fn(param).catch((err) => { cache.delete(param); throw err; }) ); } try { const result = cache.get(param); const value = await result; return done(null, value); } catch (error) { return done(error); } }; wrapped.clear = () => cache.clear(); return wrapped; }; const isDirCached = makeCache(async (file) => { try { const stats = await stat(file); return stats.isDirectory(); } catch (error) { return onError(error); } }); const isFileCached = makeCache(async (file) => { try { const stats = await stat(file); return stats.isFile(); } catch (error) { return onError(error); } }); const readCachedFile = makeCache(readFile); const resolveId = promisify(resolveModule); // returns the imported package name for bare module imports function getPackageName(id) { if (id.startsWith('.') || id.startsWith('/')) { return null; } const split = id.split('/'); // @my-scope/my-package/foo.js -> @my-scope/my-package // @my-scope/my-package -> @my-scope/my-package if (split[0][0] === '@') { return `${split[0]}/${split[1]}`; } // my-package/foo.js -> my-package // my-package -> my-package return split[0]; } function getMainFields(options) { let mainFields; if (options.mainFields) { ({ mainFields } = options); } else { mainFields = ['module', 'main']; } if (options.browser && mainFields.indexOf('browser') === -1) { return ['browser'].concat(mainFields); } if (!mainFields.length) { throw new Error('Please ensure at least one `mainFields` value is specified'); } return mainFields; } function getPackageInfo(options) { const { cache, extensions, pkg, mainFields, preserveSymlinks, useBrowserOverrides } = options; let { pkgPath } = options; if (cache.has(pkgPath)) { return cache.get(pkgPath); } // browserify/resolve doesn't realpath paths returned in its packageFilter callback if (!preserveSymlinks) { pkgPath = realpathSync(pkgPath); } const pkgRoot = dirname(pkgPath); const packageInfo = { // copy as we are about to munge the `main` field of `pkg`. packageJson: Object.assign({}, pkg), // path to package.json file packageJsonPath: pkgPath, // directory containing the package.json root: pkgRoot, // which main field was used during resolution of this module (main, module, or browser) resolvedMainField: 'main', // whether the browser map was used to resolve the entry point to this module browserMappedMain: false, // the entry point of the module with respect to the selected main field and any // relevant browser mappings. resolvedEntryPoint: '' }; let overriddenMain = false; for (let i = 0; i < mainFields.length; i++) { const field = mainFields[i]; if (typeof pkg[field] === 'string') { pkg.main = pkg[field]; packageInfo.resolvedMainField = field; overriddenMain = true; break; } } const internalPackageInfo = { cachedPkg: pkg, hasModuleSideEffects: () => null, hasPackageEntry: overriddenMain !== false || mainFields.indexOf('main') !== -1, packageBrowserField: useBrowserOverrides && typeof pkg.browser === 'object' && Object.keys(pkg.browser).reduce((browser, key) => { let resolved = pkg.browser[key]; if (resolved && resolved[0] === '.') { resolved = resolve(pkgRoot, resolved); } /* eslint-disable no-param-reassign */ browser[key] = resolved; if (key[0] === '.') { const absoluteKey = resolve(pkgRoot, key); browser[absoluteKey] = resolved; if (!extname(key)) { extensions.reduce((subBrowser, ext) => { subBrowser[absoluteKey + ext] = subBrowser[key]; return subBrowser; }, browser); } } return browser; }, {}), packageInfo }; const browserMap = internalPackageInfo.packageBrowserField; if ( useBrowserOverrides && typeof pkg.browser === 'object' && // eslint-disable-next-line no-prototype-builtins browserMap.hasOwnProperty(pkg.main) ) { packageInfo.resolvedEntryPoint = browserMap[pkg.main]; packageInfo.browserMappedMain = true; } else { // index.node is technically a valid default entrypoint as well... packageInfo.resolvedEntryPoint = resolve(pkgRoot, pkg.main || 'index.js'); packageInfo.browserMappedMain = false; } const packageSideEffects = pkg.sideEffects; if (typeof packageSideEffects === 'boolean') { internalPackageInfo.hasModuleSideEffects = () => packageSideEffects; } else if (Array.isArray(packageSideEffects)) { internalPackageInfo.hasModuleSideEffects = createFilter(packageSideEffects, null, { resolve: pkgRoot }); } cache.set(pkgPath, internalPackageInfo); return internalPackageInfo; } function normalizeInput(input) { if (Array.isArray(input)) { return input; } else if (typeof input === 'object') { return Object.values(input); } // otherwise it's a string return [input]; } // Resolve module specifiers in order. Promise resolves to the first module that resolves // successfully, or the error that resulted from the last attempted module resolution. function resolveImportSpecifiers(importSpecifierList, resolveOptions) { let promise = Promise.resolve(); for (let i = 0; i < importSpecifierList.length; i++) { promise = promise.then((value) => { // if we've already resolved to something, just return it. if (value) { return value; } return resolveId(importSpecifierList[i], resolveOptions).then((result) => { if (!resolveOptions.preserveSymlinks) { result = realpathSync(result); } return result; }); }); if (i < importSpecifierList.length - 1) { // swallow MODULE_NOT_FOUND errors from all but the last resolution promise = promise.catch((error) => { if (error.code !== 'MODULE_NOT_FOUND') { throw error; } }); } } return promise; } /* eslint-disable no-param-reassign, no-shadow, no-undefined */ const builtins = new Set(builtinList); const ES6_BROWSER_EMPTY = '\0node-resolve:empty.js'; const nullFn = () => null; const deepFreeze = (object) => { Object.freeze(object); for (const value of Object.values(object)) { if (typeof value === 'object' && !Object.isFrozen(value)) { deepFreeze(value); } } return object; }; const defaults = { customResolveOptions: {}, dedupe: [], // It's important that .mjs is listed before .js so that Rollup will interpret npm modules // which deploy both ESM .mjs and CommonJS .js files as ESM. extensions: ['.mjs', '.js', '.json', '.node'], resolveOnly: [] }; const DEFAULTS = deepFreeze(deepMerge({}, defaults)); function nodeResolve(opts = {}) { const options = Object.assign({}, defaults, opts); const { customResolveOptions, extensions, jail } = options; const warnings = []; const packageInfoCache = new Map(); const idToPackageInfo = new Map(); const mainFields = getMainFields(options); const useBrowserOverrides = mainFields.indexOf('browser') !== -1; const isPreferBuiltinsSet = options.preferBuiltins === true || options.preferBuiltins === false; const preferBuiltins = isPreferBuiltinsSet ? options.preferBuiltins : true; const rootDir = options.rootDir || process.cwd(); let { dedupe } = options; let rollupOptions; if (options.only) { warnings.push('node-resolve: The `only` options is deprecated, please use `resolveOnly`'); options.resolveOnly = options.only; } if (typeof dedupe !== 'function') { dedupe = (importee) => options.dedupe.includes(importee) || options.dedupe.includes(getPackageName(importee)); } const resolveOnly = options.resolveOnly.map((pattern) => { if (pattern instanceof RegExp) { return pattern; } const normalized = pattern.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&'); return new RegExp(`^${normalized}$`); }); const browserMapCache = new Map(); let preserveSymlinks; return { name: 'node-resolve', buildStart(options) { rollupOptions = options; for (const warning of warnings) { this.warn(warning); } ({ preserveSymlinks } = options); }, generateBundle() { readCachedFile.clear(); isFileCached.clear(); isDirCached.clear(); }, async resolveId(importee, importer) { if (importee === ES6_BROWSER_EMPTY) { return importee; } // ignore IDs with null character, these belong to other plugins if (/\0/.test(importee)) return null; // strip hash and query params from import const [withoutHash, hash] = importee.split('#'); const [importPath, params] = withoutHash.split('?'); const importSuffix = `${params ? `?${params}` : ''}${hash ? `#${hash}` : ''}`; importee = importPath; const basedir = !importer || dedupe(importee) ? rootDir : dirname(importer); // https://github.com/defunctzombie/package-browser-field-spec const browser = browserMapCache.get(importer); if (useBrowserOverrides && browser) { const resolvedImportee = resolve(basedir, importee); if (browser[importee] === false || browser[resolvedImportee] === false) { return ES6_BROWSER_EMPTY; } const browserImportee = browser[importee] || browser[resolvedImportee] || browser[`${resolvedImportee}.js`] || browser[`${resolvedImportee}.json`]; if (browserImportee) { importee = browserImportee; } } const parts = importee.split(/[/\\]/); let id = parts.shift(); let isRelativeImport = false; if (id[0] === '@' && parts.length > 0) { // scoped packages id += `/${parts.shift()}`; } else if (id[0] === '.') { // an import relative to the parent dir of the importer id = resolve(basedir, importee); isRelativeImport = true; } if ( !isRelativeImport && resolveOnly.length && !resolveOnly.some((pattern) => pattern.test(id)) ) { if (normalizeInput(rollupOptions.input).includes(importee)) { return null; } return false; } let hasModuleSideEffects = nullFn; let hasPackageEntry = true; let packageBrowserField = false; let packageInfo; const filter = (pkg, pkgPath) => { const info = getPackageInfo({ cache: packageInfoCache, extensions, pkg, pkgPath, mainFields, preserveSymlinks, useBrowserOverrides }); ({ packageInfo, hasModuleSideEffects, hasPackageEntry, packageBrowserField } = info); return info.cachedPkg; }; let resolveOptions = { basedir, packageFilter: filter, readFile: readCachedFile, isFile: isFileCached, isDirectory: isDirCached, extensions }; if (preserveSymlinks !== undefined) { resolveOptions.preserveSymlinks = preserveSymlinks; } const importSpecifierList = []; if (importer === undefined && !importee[0].match(/^\.?\.?\//)) { // For module graph roots (i.e. when importer is undefined), we // need to handle 'path fragments` like `foo/bar` that are commonly // found in rollup config files. If importee doesn't look like a // relative or absolute path, we make it relative and attempt to // resolve it. If we don't find anything, we try resolving it as we // got it. importSpecifierList.push(`./${importee}`); } const importeeIsBuiltin = builtins.has(importee); if (importeeIsBuiltin && (!preferBuiltins || !isPreferBuiltinsSet)) { // The `resolve` library will not resolve packages with the same // name as a node built-in module. If we're resolving something // that's a builtin, and we don't prefer to find built-ins, we // first try to look up a local module with that name. If we don't // find anything, we resolve the builtin which just returns back // the built-in's name. importSpecifierList.push(`${importee}/`); } // TypeScript files may import '.js' to refer to either '.ts' or '.tsx' if (importer && importee.endsWith('.js')) { for (const ext of ['.ts', '.tsx']) { if (importer.endsWith(ext) && extensions.includes(ext)) { importSpecifierList.push(importee.replace(/.js$/, ext)); } } } importSpecifierList.push(importee); resolveOptions = Object.assign(resolveOptions, customResolveOptions); try { let resolved = await resolveImportSpecifiers(importSpecifierList, resolveOptions); if (resolved && packageBrowserField) { if (Object.prototype.hasOwnProperty.call(packageBrowserField, resolved)) { if (!packageBrowserField[resolved]) { browserMapCache.set(resolved, packageBrowserField); return ES6_BROWSER_EMPTY; } resolved = packageBrowserField[resolved]; } browserMapCache.set(resolved, packageBrowserField); } if (hasPackageEntry && !preserveSymlinks && resolved) { const fileExists = await exists(resolved); if (fileExists) { resolved = await realpath(resolved); } } idToPackageInfo.set(resolved, packageInfo); if (hasPackageEntry) { if (builtins.has(resolved) && preferBuiltins && isPreferBuiltinsSet) { return null; } else if (importeeIsBuiltin && preferBuiltins) { if (!isPreferBuiltinsSet) { this.warn( `preferring built-in module '${importee}' over local alternative at '${resolved}', pass 'preferBuiltins: false' to disable this behavior or 'preferBuiltins: true' to disable this warning` ); } return null; } else if (jail && resolved.indexOf(normalize(jail.trim(sep))) !== 0) { return null; } } if (resolved && options.modulesOnly) { const code = await readFile(resolved, 'utf-8'); if (isModule(code)) { return { id: `${resolved}${importSuffix}`, moduleSideEffects: hasModuleSideEffects(resolved) }; } return null; } const result = { id: `${resolved}${importSuffix}`, moduleSideEffects: hasModuleSideEffects(resolved) }; return result; } catch (error) { return null; } }, load(importee) { if (importee === ES6_BROWSER_EMPTY) { return 'export default {};'; } return null; }, getPackageInfoForId(id) { return idToPackageInfo.get(id); } }; } export default nodeResolve; export { DEFAULTS, nodeResolve };