UNPKG

@vercel/webpack-asset-relocator-loader

Version:

Asset relocation loader used in ncc for performing Node.js builds while emitting and relocating any asset references.

1,302 lines (1,215 loc) 54.7 kB
const path = require('path'); const { readFileSync, readFile, stat, lstat, readlink, statSync } = require('graceful-fs'); const { walk } = require('estree-walker'); const MagicString = require('magic-string'); const { attachScopes } = require('rollup-pluginutils'); const evaluate = require('./utils/static-eval'); let acorn = require('acorn'); const bindings = require('bindings'); const getUniqueAssetName = require('./utils/dedupe-names'); const sharedlibEmit = require('./utils/sharedlib-emit'); const glob = require('glob'); const getPackageBase = require('./utils/get-package-base'); const getPackageScope = require('./utils/get-package-scope'); const { pregyp, nbind } = require('./utils/binary-locators'); const handleSpecialCases = require('./utils/special-cases'); const { getOptions } = require("loader-utils"); const resolve = require('resolve'); //resolve should be external to be patched by yarn const mergeSourceMaps = require('./utils/merge-source-maps'); const os = require('os'); const nodeGypBuild = require('node-gyp-build'); const { pathToFileURL, fileURLToPath } = require('url'); acorn = acorn.Parser.extend( require("acorn-class-fields"), require("acorn-static-class-features"), require("acorn-private-class-elements") ); const extensions = ['.js', '.json', '.node']; const { UNKNOWN, FUNCTION, WILDCARD, wildcardRegEx } = evaluate; function isIdentifierRead(node, parent) { switch (parent.type) { case 'ObjectPattern': case 'ArrayPattern': // Note: default values not currently supported return false; // disregard `bar` in `bar = thing()` case 'AssignmentExpression': return parent.right === node; case 'MemberExpression': return parent.computed || node === parent.object; // disregard the `bar` in `{ bar: foo }` case 'Property': return node === parent.value; // disregard the `bar` in `class Foo { bar () {...} }` case 'MethodDefinition': return false; // disregard the `bar` in var bar = asdf case 'VariableDeclarator': return parent.id !== node; // disregard the `bar` in `export { foo as bar }` case 'ExportSpecifier': return false; // disregard the `bar` in `function (bar) {}` case 'FunctionExpression': case 'FunctionDeclaration': case 'ArrowFunctionExpression': return false; default: return true; } } function isVarLoop (node) { return node.type === 'ForStatement' || node.type === 'ForInStatement' || node.type === 'ForOfStatement'; } function isLoop (node) { return node.type === 'ForStatement' || node.type === 'ForInStatement' || node.type === 'ForOfStatement' || node.type === 'WhileStatement' || node.type === 'DoWhileStatement'; } const stateMap = new Map(); let lastState; let stateId = 0; function getAssetState (options, compilation) { let state = stateMap.get(compilation); if (!state) { stateMap.set(compilation, state = { stateId: ++stateId, entryIds: getEntryIds(compilation), assets: Object.create(null), assetNames: Object.create(null), assetMeta: Object.create(null), assetSymlinks: Object.create(null), hadOptions: false }); } if (!state.hadOptions) { state.hadOptions = true; if (options && options.existingAssetNames) { options.existingAssetNames.forEach(assetName => { state.assetNames[assetName] = true; }); } } return lastState = state; } const flattenArray = arr => Array.prototype.concat.apply([], arr); function getEntryIds (compilation) { if (compilation.options.entry) { if (typeof compilation.options.entry === 'string') { try { return [resolve.sync(compilation.options.entry, { extensions })]; } catch (e) { return; } } else if (typeof compilation.options.entry === 'object') { try { return flattenArray(Object.values(compilation.options.entry) .map(entry => { if (typeof entry === "string") { return [entry]; } if (entry && Array.isArray(entry.import)) { return entry.import; } return []; }) ).map(entryString => resolve.sync(entryString, { extensions })); } catch (e) { return; } } } } function assetBase (outputAssetBase) { if (!outputAssetBase) return ''; if (outputAssetBase.endsWith('/') || outputAssetBase.endsWith('\\')) return outputAssetBase; return outputAssetBase + '/'; } const staticProcess = { cwd: () => { return cwd; }, env: { NODE_ENV: UNKNOWN, [UNKNOWN]: true }, [UNKNOWN]: true }; // unique symbol value to identify express instance in static analysis const EXPRESS_SET = Symbol(); const EXPRESS_ENGINE = Symbol(); const NBIND_INIT = Symbol(); const FS_FN = Symbol(); const RESOLVE_FROM = Symbol(); const BINDINGS = Symbol(); const NODE_GYP_BUILD = Symbol(); const fsSymbols = { access: FS_FN, accessSync: FS_FN, createReadStream: FS_FN, exists: FS_FN, existsSync: FS_FN, fstat: FS_FN, fstatSync: FS_FN, lstat: FS_FN, lstatSync: FS_FN, open: FS_FN, readFile: FS_FN, readFileSync: FS_FN, stat: FS_FN, statSync: FS_FN }; const staticModules = Object.assign(Object.create(null), { bindings: { default: BINDINGS }, express: { default: function () { return { [UNKNOWN]: true, set: EXPRESS_SET, engine: EXPRESS_ENGINE }; } }, fs: { default: fsSymbols, ...fsSymbols }, process: { default: staticProcess, ...staticProcess }, // populated below path: { default: {} }, os: { default: os, ...os }, 'node:path': undefined, // point to the same reference as "path" below 'node-pre-gyp': pregyp, 'node-pre-gyp/lib/pre-binding': pregyp, 'node-pre-gyp/lib/pre-binding.js': pregyp, '@mapbox/node-pre-gyp': pregyp, '@mapbox/node-pre-gyp/lib/pre-binding': pregyp, '@mapbox/node-pre-gyp/lib/pre-binding.js': pregyp, 'node-gyp-build': { default: NODE_GYP_BUILD }, 'nbind': { init: NBIND_INIT, default: { init: NBIND_INIT } }, 'resolve-from': { default: RESOLVE_FROM } }); staticModules["node:path"] = staticModules.path; const globalBindings = { MONGOOSE_DRIVER_PATH: undefined, URL: URL }; globalBindings.global = globalBindings.GLOBAL = globalBindings.globalThis = globalBindings; // call expression triggers const TRIGGER = Symbol(); pregyp.find[TRIGGER] = true; const staticPath = staticModules.path; Object.keys(path).forEach(name => { const pathFn = path[name]; if (typeof pathFn === 'function') { const fn = function () { return pathFn.apply(this, arguments); }; fn[TRIGGER] = true; staticPath[name] = staticPath.default[name] = fn; } else { staticPath[name] = staticPath.default[name] = pathFn; } }); // overload path.resolve to support custom cwd staticPath.resolve = staticPath.default.resolve = function (...args) { return path.resolve.apply(this, [cwd, ...args]); }; staticPath.resolve[TRIGGER] = true; const excludeAssetExtensions = new Set(['.h', '.cmake', '.c', '.cpp']); const excludeAssetFiles = new Set(['CHANGELOG.md', 'README.md', 'readme.md', 'changelog.md']); let cwd; function backtrack (self, parent) { if (!parent || parent.type !== 'ArrayExpression') return self.skip(); } const absoluteRegEx = /^\/[^\/]+|^[a-z]:[\\/][^\\/]+/i; function isAbsolutePathOrUrl(str) { if (str instanceof URL) return str.protocol === 'file:'; if (typeof str === 'string') { if (str.startsWith('file:')) { try { new URL(str); return true; } catch { return false; } } return absoluteRegEx.test(str); } return false; } const BOUND_REQUIRE = Symbol(); function generateWildcardRequire(dir, wildcardPath, wildcardParam, wildcardBlocks, log) { const wildcardBlockIndex = wildcardBlocks.length; const wildcardPathNormalized = wildcardPath.split(path.sep).join(path.posix.sep) const dirNormalized = dir.split(path.sep).join(path.posix.sep) const trailingWildcard = wildcardPathNormalized.endsWith(WILDCARD); const wildcardIndex = wildcardPathNormalized.indexOf(WILDCARD); const wildcardPrefix = wildcardPathNormalized.substr(0, wildcardIndex); const wildcardSuffix = wildcardPathNormalized.substr(wildcardIndex + 1); const endPattern = wildcardSuffix ? '?(.@(js|json|node))' : '.@(js|json|node)'; // sync to support no emission case if (log) console.log('Generating wildcard requires for ' + wildcardPathNormalized.replace(WILDCARD, '*')); let options = glob.sync(wildcardPrefix + '**' + wildcardSuffix + endPattern, { mark: true, ignore: 'node_modules/**/*' }); if (!options.length) return; const optionConditions = options.map((file, index) => { const arg = JSON.stringify(file.substring(wildcardPrefix.length, file.lastIndexOf(wildcardSuffix))); let relPath = path.posix.relative(dirNormalized, file); if (!relPath.startsWith('../')) relPath = './' + relPath; let condition = index === 0 ? ' ' : ' else '; if (trailingWildcard && arg.endsWith('.js"')) condition += `if (arg === ${arg} || arg === ${arg.substr(0, arg.length - 4)}")`; else if (trailingWildcard && arg.endsWith('.json"')) condition += `if (arg === ${arg} || arg === ${arg.substr(0, arg.length - 6)}")`; else if (trailingWildcard && arg.endsWith('.node"')) condition += `if (arg === ${arg} || arg === ${arg.substr(0, arg.length - 6)}")`; else condition += `if (arg === ${arg})`; condition += ` return require(${JSON.stringify(relPath)});`; return condition; }).join('\n'); wildcardBlocks.push(`function __ncc_wildcard$${wildcardBlockIndex} (arg) {\n${optionConditions}\n}`); return `__ncc_wildcard$${wildcardBlockIndex}(${wildcardParam})`; } function injectPathHook (compilation, outputAssetBase) { const esm = compilation.outputOptions.module; const { RuntimeModule, RuntimeGlobals } = compilation.compiler.webpack class AssetRelocatorLoaderRuntimeModule extends RuntimeModule { constructor({ relBase }) { super('asset-relocator-loader'); this.relBase = relBase } generate() { const requireBase = `${esm ? "new URL('.', import.meta.url).pathname.slice(import.meta.url.match(/^file:\\/\\/\\/\\w:/) ? 1 : 0, -1)" : '__dirname'} + ${JSON.stringify(this.relBase + '/' + assetBase(outputAssetBase))}`; return `if (typeof __webpack_require__ !== 'undefined') __webpack_require__.ab = ${requireBase};` } shouldIsolate() { return false; } } compilation.hooks.runtimeRequirementInTree .for(RuntimeGlobals.require) .tap('asset-relocator-loader', (chunk) => { let relBase = ''; if (chunk.name) { relBase = path.relative(path.dirname(chunk.name), '.').replace(/\\/g, '/'); if (relBase.length) { relBase = '/' + relBase; } } try { compilation.addRuntimeModule(chunk, new AssetRelocatorLoaderRuntimeModule({ relBase })); } catch (error) { console.error(error); } return true; }); } module.exports = async function (content, map) { if (this.cacheable) this.cacheable(); this.async(); const id = this.resourcePath; const dir = path.dirname(id); // injection to set __webpack_require__.ab const options = getOptions(this) || {}; injectPathHook(this._compilation, options.outputAssetBase); if (id.endsWith('.node')) { const assetState = getAssetState(options, this._compilation); const pkgBase = getPackageBase(this.resourcePath) || dir; await sharedlibEmit(pkgBase, assetState, assetBase(options.outputAssetBase), this.emitFile); let name; if (!(name = assetState.assets[id])) name = assetState.assets[id] = getUniqueAssetName(id.substr(pkgBase.length + 1).replace(/\\/g, '/'), id, assetState.assetNames); const permissions = await new Promise((resolve, reject) => stat(id, (err, stats) => err ? reject(err) : resolve(stats.mode)) ); assetState.assetMeta[name] = { path: id, permissions }; this.emitFile(assetBase(options.outputAssetBase) + name, content); this.callback(null, 'module.exports = __non_webpack_require__(__webpack_require__.ab + ' + JSON.stringify(name) + ')'); return; } if (id.endsWith('.json')) return this.callback(null, code, map); let code = content.toString(); if (typeof options.production === 'boolean' && staticProcess.env.NODE_ENV === UNKNOWN) { staticProcess.env.NODE_ENV = options.production ? 'production' : 'dev'; } if (!cwd) { if (typeof options.cwd === 'string') cwd = path.resolve(options.cwd); else cwd = process.cwd(); } const assetState = getAssetState(options, this._compilation); const entryIds = assetState.entryIds; // calculate the base-level package folder to load bindings from const pkgBase = getPackageBase(id); const emitAsset = (assetPath) => { // JS assets to support require(assetPath) and not fs-based handling // NB package.json is ambiguous here... let outName = path.basename(assetPath); if (assetPath.endsWith('.node')) { // retain directory depth structure for binaries for rpath to work out if (pkgBase) outName = assetPath.substr(pkgBase.length + 1).replace(/\\/g, '/'); // If the asset is a ".node" binary, then glob for possible shared // libraries that should also be included const nextPromise = sharedlibEmit(pkgBase, assetState, assetBase(options.outputAssetBase), this.emitFile); assetEmissionPromises = assetEmissionPromises.then(() => { return nextPromise; }); } let name; if (!(name = assetState.assets[assetPath])) { name = assetState.assets[assetPath] = getUniqueAssetName(outName, assetPath, assetState.assetNames); if (options.debugLog) console.log('Emitting ' + assetPath + ' for static use in module ' + id); } assetEmissionPromises = assetEmissionPromises.then(async () => { const [source, stats] = await Promise.all([ new Promise((resolve, reject) => readFile(assetPath, (err, source) => err ? reject(err) : resolve(source)) ), await new Promise((resolve, reject) => lstat(assetPath, (err, stats) => err ? reject(err) : resolve(stats)) ) ]); if (stats.isSymbolicLink()) { const symlink = await new Promise((resolve, reject) => { readlink(assetPath, (err, path) => err ? reject(err) : resolve(path)); }); const baseDir = path.dirname(assetPath); assetState.assetSymlinks[assetBase(options.outputAssetBase) + name] = path.relative(baseDir, path.resolve(baseDir, symlink)); } else { assetState.assetMeta[assetBase(options.outputAssetBase) + name] = { path: assetPath, permissions: stats.mode }; this.addDependency(assetPath); this.emitFile(assetBase(options.outputAssetBase) + name, source); } }); return "__webpack_require__.ab + " + JSON.stringify(name).replace(/\\/g, '/'); }; const emitAssetDirectory = (wildcardPath, wildcards) => { const wildcardIndex = wildcardPath.indexOf(WILDCARD); const dirIndex = wildcardIndex === -1 ? wildcardPath.length : wildcardPath.lastIndexOf(path.sep, wildcardIndex); const assetDirPath = wildcardPath.substr(0, dirIndex); const patternPath = wildcardPath.substr(dirIndex); const wildcardPattern = patternPath.replace(wildcardRegEx, (match, index) => { return patternPath[index - 1] === path.sep ? '**/*' : '*/**/*'; }) || '/**/*'; if (options.debugLog) console.log('Emitting directory ' + assetDirPath + wildcardPattern + ' for static use in module ' + id); const dirName = path.basename(assetDirPath); const name = assetState.assets[assetDirPath] || (assetState.assets[assetDirPath] = getUniqueAssetName(dirName, assetDirPath, assetState.assetNames, true)); assetState.assets[assetDirPath] = name; // this used to be async but had to switch to support no emission for no detection const files = glob.sync(assetDirPath + wildcardPattern, { mark: true, ignore: 'node_modules/**/*' }).filter(name => !excludeAssetExtensions.has(path.extname(name)) && !excludeAssetFiles.has(path.basename(name)) && !name.endsWith('/') ); if (!files.length) return; assetEmissionPromises = assetEmissionPromises.then(async () => { await Promise.all(files.map(async file => { const [source, stats] = await Promise.all([ new Promise((resolve, reject) => readFile(file, (err, source) => err ? reject(err) : resolve(source)) ), await new Promise((resolve, reject) => lstat(file, (err, stats) => err ? reject(err) : resolve(stats)) ) ]); if (stats.isSymbolicLink()) { const symlink = await new Promise((resolve, reject) => { readlink(file, (err, path) => err ? reject(err) : resolve(path)); }); const baseDir = path.dirname(file); assetState.assetSymlinks[assetBase(options.outputAssetBase) + name + file.substr(assetDirPath.length)] = path.relative(baseDir, path.resolve(baseDir, symlink)).replace(/\\/g, '/'); } else { assetState.assetMeta[assetBase(options.outputAssetBase) + name + file.substr(assetDirPath.length)] = { path: file, permissions: stats.mode }; this.addDependency(file); this.emitFile(assetBase(options.outputAssetBase) + name + file.substr(assetDirPath.length), source); } })); }); let assetExpressions = ''; let firstPrefix = ''; if (wildcards) { let curPattern = patternPath; let first = true; for (const wildcard of wildcards) { const nextWildcardIndex = curPattern.indexOf(WILDCARD); const wildcardPrefix = curPattern.substr(0, nextWildcardIndex); curPattern = curPattern.substr(nextWildcardIndex + 1); if (first) { firstPrefix = wildcardPrefix; first = false; } else { assetExpressions += " + \'" + JSON.stringify(wildcardPrefix).slice(1, -1) + "'"; } if (wildcard.type === 'SpreadElement') assetExpressions += " + " + code.substring(wildcard.argument.start, wildcard.argument.end) + ".join('/')"; else assetExpressions += " + " + code.substring(wildcard.start, wildcard.end); } if (curPattern.length) { assetExpressions += " + \'" + JSON.stringify(curPattern.replace(/\\/g, '/')).slice(1, -1) + "'"; } } return "__webpack_require__.ab + " + JSON.stringify((name + firstPrefix).replace(/\\/g, '/')) + assetExpressions; }; let assetEmissionPromises = Promise.resolve(); const magicString = new MagicString(code); let ast, isESM; try { ast = acorn.parse(code, { allowReturnOutsideFunction: true, ecmaVersion: 2020 }); isESM = false; } catch (e) {} if (!ast) { try { ast = acorn.parse(code, { sourceType: 'module', ecmaVersion: 2020, allowAwaitOutsideFunction: true }); isESM = true; } catch (e) { // Parser errors just skip analysis return this.callback(null, code, map); } } let scope = attachScopes(ast, 'scope'); let transformed = false; const importMetaUrl = pathToFileURL(id).href; const knownBindings = Object.assign(Object.create(null), { __dirname: { shadowDepth: 0, value: path.resolve(id, '..') }, __filename: { shadowDepth: 0, value: id }, process: { shadowDepth: 0, value: staticProcess } }); if (!isESM) { knownBindings.require = { shadowDepth: 0, value: { [FUNCTION] (specifier) { const m = staticModules[specifier]; return m.default; }, resolve (specifier) { return resolve.sync(specifier, { basedir: dir, extensions }); } } }; knownBindings.require.value.resolve[TRIGGER] = true; } let wildcardBlocks = []; function setKnownBinding (name, value) { // require is somewhat special in that we shadow it but don't // statically analyze it ("known unknown" of sorts) if (name === 'require') return; knownBindings[name] = { shadowDepth: 0, value: value }; } function getKnownBinding (name) { const binding = knownBindings[name]; if (binding) { if (binding.shadowDepth === 0) { return binding.value; } } } if (isESM) { for (const decl of ast.body) { if (decl.type === 'ImportDeclaration') { const source = decl.source.value; const staticModule = staticModules[source]; if (staticModule) { for (const impt of decl.specifiers) { if (impt.type === 'ImportNamespaceSpecifier') setKnownBinding(impt.local.name, staticModule); else if (impt.type === 'ImportDefaultSpecifier' && 'default' in staticModule) setKnownBinding(impt.local.name, staticModule.default); else if (impt.type === 'ImportSpecifier' && impt.imported.name in staticModule) setKnownBinding(impt.local.name, staticModule[impt.imported.name]); } } } } } function computePureStaticValue (expr, computeBranches = true) { const vars = Object.create(null); Object.keys(knownBindings).forEach(name => { vars[name] = getKnownBinding(name); }); Object.keys(globalBindings).forEach(name => { vars[name] = globalBindings[name]; }); vars['import.meta'] = { url: importMetaUrl }; // evaluate returns undefined for non-statically-analyzable const result = evaluate(expr, vars, computeBranches); return result; } // statically determinable leaves are tracked, and inlined when the // greatest parent statically known leaf computation corresponds to an asset path let staticChildNode, staticChildValue; // Express engine opt-out let definedExpressEngines = false; // track the name of any function that wraps "require" let boundRequireName; // detect require('asdf'); function isStaticRequire (node) { return node && node.type === 'CallExpression' && node.callee.type === 'Identifier' && node.callee.name === 'require' && knownBindings.require.shadowDepth === 0 && node.arguments.length === 1 && node.arguments[0].type === 'Literal'; } ({ ast = ast, scope = scope, transformed = transformed } = handleSpecialCases({ id, ast, scope, pkgBase, magicString, options, emitAsset, emitAssetDirectory }) || {}); walk(ast, { enter (node, parent) { if (node.scope) { scope = node.scope; for (const id in node.scope.declarations) { if (id in knownBindings) knownBindings[id].shadowDepth++; } } if (staticChildNode) return backtrack(this, parent); if (node.type === 'Identifier') { if (isIdentifierRead(node, parent)) { let binding; // detect asset leaf expression triggers (if not already) // __dirname, __filename // Could add import.meta.url, even path-like environment variables if (typeof (binding = getKnownBinding(node.name)) === 'string' && binding.match(absoluteRegEx) || binding && (typeof binding === 'function' || typeof binding === 'object') && binding[TRIGGER]) { staticChildValue = { value: typeof binding === 'string' ? binding : undefined }; staticChildNode = node; return this.skip(); } // free require -> __non_webpack_require__ else if (!isESM && node.name === 'require' && knownBindings.require.shadowDepth === 0 && parent.type !== 'UnaryExpression') { magicString.overwrite(node.start, node.end, '__non_webpack_require__'); transformed = true; return this.skip(); } // __non_webpack_require__ -> eval('require') else if (!isESM && node.name === '__non_webpack_require__' && parent.type !== 'UnaryExpression') { magicString.overwrite(node.start, node.end, 'eval("require")'); transformed = true; return this.skip(); } } } else if (node.type === 'MemberExpression' && node.object.type === 'MetaProperty' && node.object.meta.name === 'import' && node.object.property.name === 'meta' && (node.property.computed ? node.property.value : node.property.name) === 'url') { staticChildValue = { value: importMetaUrl }; staticChildNode = node; return this.skip(); } // require else if (!isESM && node.type === 'CallExpression' && node.callee.type === 'Identifier' && node.callee.name === 'require' && knownBindings.require.shadowDepth === 0 && node.arguments.length) { const expression = node.arguments[0]; const { result: computed, sawIdentifier } = computePureStaticValue(expression, true); // no clue what the require is for, Webpack won't know either // -> turn it into a runtime dynamic require if (!computed) { // require(a || 'asdf') -> require('asdf') special case if (expression.type === 'LogicalExpression' && expression.operator === '||' && expression.left.type === 'Identifier') { transformed = true; magicString.overwrite(expression.start, expression.end, code.substring(expression.right.start, expression.right.end)); return this.skip(); } transformed = true; magicString.overwrite(node.callee.start, node.callee.end, '__non_webpack_require__'); return this.skip(); } // we found the exact value for the require, and it used a binding from our analysis // -> inline the computed value for Webpack to use else if (typeof computed.value === 'string' && sawIdentifier) { if (computed.wildcards) { const wildcardPath = path.resolve(dir, computed.value); if (computed.wildcards.length === 1 && validAssetEmission(wildcardPath)) { const emission = generateWildcardRequire(dir, wildcardPath, code.substring(computed.wildcards[0].start, computed.wildcards[0].end), wildcardBlocks, options.debugLog); if (emission) { magicString.overwrite(node.start, node.end, emission); transformed = true; return this.skip(); } } } else if (computed.value) { let inline; if (options.customEmit) inline = options.customEmit(computed.value, true); if (inline === undefined) inline = JSON.stringify(computed.value); if (inline !== false) { magicString.overwrite(expression.start, expression.end, inline); transformed = true; return this.skip(); } } } // branched require, and it used a binding from our analysis // -> inline the computed values for Webpack else if (computed && typeof computed.then === 'string' && typeof computed.else === 'string' && sawIdentifier) { const conditionValue = computePureStaticValue(computed.test, true).result; // inline the known branch if possible if (conditionValue && 'value' in conditionValue) { if (conditionValue) { transformed = true; magicString.overwrite(expression.start, expression.end, JSON.stringify(computed.then)); return this.skip(); } else { transformed = true; magicString.overwrite(expression.start, expression.end, JSON.stringify(computed.else)); return this.skip(); } } else { const test = code.substring(computed.test.start, computed.test.end); transformed = true; magicString.overwrite(expression.start, expression.end, `${test} ? ${JSON.stringify(computed.then)} : ${JSON.stringify(computed.else)}`); return this.skip(); } } // Special cases else if (parent.type === 'CallExpression' && parent.callee === node) { // require('pkginfo')(module, ...string[]) if (computed.value === 'pkginfo' && parent.arguments.length && parent.arguments[0].type === 'Identifier' && parent.arguments[0].name === 'module') { let filterValues = new Set(); for (let i = 1; i < parent.arguments.length; i++) { if (parent.arguments[i].type === 'Literal') filterValues.add(parent.arguments[i].value); } const scope = getPackageScope(id); if (scope) { try { var pkg = JSON.parse(readFileSync(scope + '/package.json')); if (filterValues.size) { for (var p in pkg) { if (!filterValues.has(p)) delete pkg[p]; } } } catch (e) {} if (pkg) { transformed = true; magicString.overwrite(parent.start, parent.end, `Object.assign(module.exports, ${JSON.stringify(pkg)})`); return this.skip(); } } } // leave it to webpack return this.skip(); } else { // if there is a custom emit and we have a string, then allow the custom emit to handle it if (typeof computed.value === 'string' && options.customEmit) { const customEmit = options.customEmit(computed.value, { id, isRequire: true }); if (customEmit) { magicString.overwrite(node.start, node.end, '__non_webpack_require__(' + customEmit + ')'); transformed = true; return this.skip(); } } // leave it to webpack return this.skip(); } } // require.main handling else if (!isESM && node.type === 'MemberExpression' && node.object.type === 'Identifier' && node.object.name === 'require' && knownBindings.require.shadowDepth === 0 && node.property.type === 'Identifier' && !node.computed) { if (node.property.name === 'main' && parent && parent.type === 'BinaryExpression' && (parent.operator === '==' || parent.operator === '===')) { let other; other = parent.right === node ? parent.left : parent.right; if (other.type === 'Identifier' && other.name === 'module') { // inline the require.main check to be the target require.main check if this is the entry, // and false otherwise if (entryIds && entryIds.indexOf(id) !== -1) { // require.main === module -> __non_webpack_require__.main == __non_webpack_require__.cache[eval('__filename')] // can be simplified if we get a way to get outer "module" in Webpack magicString.overwrite(other.start, other.end, "__non_webpack_require__.cache[eval('__filename')]"); } else { magicString.overwrite(parent.start, parent.end, "false"); transformed = true; return this.skip(); } } } if (node.property.name === 'ensure') { // leave require.ensure to webpack return this.skip(); } } // module.require handling else if (!isESM && node.type === 'MemberExpression' && node.object.type === 'Identifier' && node.object.name === 'module' && 'module' in knownBindings === false && node.property.type === 'Identifier' && !node.computed && node.property.name === 'require') { magicString.overwrite(node.start, node.end, 'require'); node.type = 'Identifier'; node.name = 'require'; transformed = true; } // Call expression cases and asset triggers // - fs triggers: fs.readFile(...) // - require.resolve() // - bindings()(...) // - nodegyp() // - etc. else if (node.type === 'CallExpression') { const calleeValue = computePureStaticValue(node.callee, false).result; // if we have a direct pure static function, // and that function has a [TRIGGER] symbol -> trigger asset emission from it if (calleeValue && typeof calleeValue.value === 'function' && calleeValue.value[TRIGGER]) { staticChildValue = computePureStaticValue(node, true).result; // if it computes, then we start backtrackingelse if (staticChildValue) { staticChildNode = node; return backtrack(this, parent); } } // handle well-known function symbol cases else if (calleeValue && typeof calleeValue.value === 'symbol') { switch (calleeValue.value) { // customRequireWrapper('...') -> wrapperMod(require('...'), '...') case BOUND_REQUIRE: if (node.arguments.length === 1 && node.arguments[0].type === 'Literal' && node.callee.type === 'Identifier' && knownBindings.require.shadowDepth === 0) { transformed = true; magicString.overwrite(node.callee.start, node.callee.end, 'require'); magicString.appendRight(node.start, boundRequireName + '('); magicString.appendLeft(node.end, ', ' + code.substring(node.arguments[0].start, node.arguments[0].end) + ')'); return this.skip(); } break; // require('bindings')(...) case BINDINGS: if (node.arguments.length > 0) { const arg = computePureStaticValue(node.arguments[0], false).result; if (arg && arg.value) { let staticBindingsInstance = false; let opts; if (typeof arg.value === 'object') opts = arg.value; else if (typeof arg.value === 'string') opts = { bindings: arg.value }; if (!opts.path) { staticBindingsInstance = true; opts.path = true; } opts.module_root = pkgBase; let resolved; try { resolved = bindings(opts); } catch (e) {} if (resolved) { staticChildValue = { value: resolved }; staticChildNode = node; emitStaticChildAsset(staticBindingsInstance); return backtrack(this, parent); } } } break; case NODE_GYP_BUILD: if (node.arguments.length > 0) { const arg = computePureStaticValue(node.arguments[0], false).result; if (arg && arg.value) { transformed = true; let resolved; try { resolved = nodeGypBuild.path(arg.value); } catch (e) {} if (resolved) { staticChildValue = { value: resolved }; staticChildNode = node; emitStaticChildAsset(path); return backtrack(this, parent); } } } break; // resolveFrom(__dirname, ...) -> require.resolve(...) case RESOLVE_FROM: if (node.arguments.length === 2 && node.arguments[0].type === 'Identifier' && node.arguments[0].name === '__dirname' && knownBindings.__dirname.shadowDepth === 0) { transformed = true; magicString.overwrite(node.start, node.arguments[0].end + 1, 'require.resolve('); return this.skip(); } break; // nbind.init(...) -> require('./resolved.node') case NBIND_INIT: if (node.arguments.length > 0) { const arg = computePureStaticValue(node.arguments[0], false).result; if (arg && arg.value) { const bindingInfo = nbind(arg.value); if (bindingInfo) { bindingInfo.path = path.relative(dir, bindingInfo.path); transformed = true; const bindingPath = JSON.stringify(bindingInfo.path.replace(/\\/g, '/')); magicString.overwrite(node.start, node.end, `({ bind: require(${bindingPath}).NBind.bind_value, lib: require(${bindingPath}) })`); return this.skip(); } } } break; // Express templates: // app.set("view engine", [name]) -> app.engine([name], require([name]).__express).set("view engine", [name]) case EXPRESS_SET: if (node.arguments.length === 2 && node.arguments[0].type === 'Literal' && node.arguments[0].value === 'view engine' && !definedExpressEngines) { transformed = true; const name = code.substring(node.arguments[1].start, node.arguments[1].end); magicString.appendRight(node.callee.object.end, `.engine(${name}, require(${name}).__express)`); return this.skip(); } break; // app.engine('name', ...) causes opt-out of express rewrite case EXPRESS_ENGINE: definedExpressEngines = true; break; case FS_FN: if (node.arguments[0]) { staticChildValue = computePureStaticValue(node.arguments[0], true).result; // if it computes, then we start backtracking if (staticChildValue) { staticChildNode = node.arguments[0]; return backtrack(this, parent); } } break; } } } else if (node.type === 'VariableDeclaration' && !isVarLoop(parent)) { for (const decl of node.declarations) { if (!decl.init) continue; const computed = computePureStaticValue(decl.init, false).result; if (computed && 'value' in computed) { // var known = ...; if (decl.id.type === 'Identifier') { setKnownBinding(decl.id.name, computed.value); } // var { known } = ...; else if (decl.id.type === 'ObjectPattern') { for (const prop of decl.id.properties) { if (prop.type !== 'Property' || prop.key.type !== 'Identifier' || prop.value.type !== 'Identifier' || typeof computed.value !== 'object' || computed.value === null || !(prop.key.name in computed.value)) continue; setKnownBinding(prop.value.name, computed.value[prop.key.name]); } } if (isAbsolutePathOrUrl(computed.value)) { staticChildValue = computed; staticChildNode = decl.init; emitStaticChildAsset(); return backtrack(this, parent); } } } } else if (node.type === 'AssignmentExpression' && !isLoop(parent)) { const computed = computePureStaticValue(node.right, false).result; if (computed && 'value' in computed) { // var known = ... if (node.left.type === 'Identifier') { setKnownBinding(node.left.name, computed.value); } // var { known } = ... else if (node.left.type === 'ObjectPattern') { for (const prop of node.left.properties) { if (prop.type !== 'Property' || prop.key.type !== 'Identifier' || prop.value.type !== 'Identifier' || typeof computed.value !== 'object' || computed.value === null || !(prop.key.name in computed.value)) continue; setKnownBinding(prop.value.name, computed.value[prop.key.name]); } } if (isAbsolutePathOrUrl(computed.value)) { staticChildValue = computed; staticChildNode = node.right; emitStaticChildAsset(); return backtrack(this, parent); } } // require = require('esm')(...) if (!isESM && node.right.type === 'CallExpression' && isStaticRequire(node.right.callee) && node.right.callee.arguments[0].value === 'esm' && node.left.type === 'Identifier' && node.left.name === 'require') { transformed = true; magicString.overwrite(node.start, node.end, ''); return this.skip(); } } // condition ? require('a') : require('b') // attempt to inline known branch based on variable analysis else if (!isESM && node.type === 'ConditionalExpression' && isStaticRequire(node.consequent) && isStaticRequire(node.alternate)) { const computed = computePureStaticValue(node.test, false).result; if (computed && 'value' in computed) { transformed = true; if (computed.value) { magicString.overwrite(node.start, node.end, code.substring(node.consequent.start, node.consequent.end)); } else { magicString.overwrite(node.start, node.end, code.substring(node.alternate.start, node.alternate.end)); } return this.skip(); } } // function p (x) { ...; var y = require(x); ...; return y; } -> additional function p_mod (y) { ...; ...; return y; } else if (!isESM && (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') && (node.arguments || node.params)[0] && (node.arguments || node.params)[0].type === 'Identifier') { let fnName, args; if ((node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression') && parent.type === 'VariableDeclarator' && parent.id.type === 'Identifier') { fnName = parent.id; args = node.arguments || node.params; } else if (node.id) { fnName = node.id; args = node.arguments || node.params; } if (fnName && node.body.body) { let requireDecl, requireDeclaration, returned = false; for (let i = 0; i < node.body.body.length; i++) { if (node.body.body[i].type === 'VariableDeclaration' && !requireDecl) { requireDecl = node.body.body[i].declarations.find(decl => decl.id.type === 'Identifier' && decl.init && decl.init.type === 'CallExpression' && decl.init.callee.type === 'Identifier' && decl.init.callee.name === 'require' && knownBindings.require.shadowDepth === 0 && decl.init.arguments[0] && decl.init.arguments[0].type === 'Identifier' && decl.init.arguments[0].name === args[0].name ); if (requireDecl) requireDeclaration = node.body.body[i]; } if (requireDecl && node.body.body[i].type === 'ReturnStatement' && node.body.body[i].argument && node.body.body[i].argument.type === 'Identifier' && node.body.body[i].argument.name === requireDecl.id.name) { returned = true; break; } } if (returned) { let prefix = ';'; const wrapArgs = node.type === 'ArrowFunctionExpression' && node.params[0].start === node.start; if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') { node = parent; prefix = ','; } boundRequireName = fnName.name + '$$mod'; setKnownBinding(fnName.name, BOUND_REQUIRE); const newFn = prefix + code.substring(node.start, fnName.start) + boundRequireName + code.substring(fnName.end, args[0].start + !wrapArgs) + (wrapArgs ? '(' : '') + requireDecl.id.name + ', ' + code.substring(args[0].start, args[args.length - 1].end + !wrapArgs) + (wrapArgs ? ')' : '') + code.substring(args[0].end + !wrapArgs, requireDeclaration.start) + code.substring(requireDeclaration.end, node.end); magicString.appendRight(node.end, newFn); } } } }, leave (node, parent) { if (node.scope) { scope = scope.parent; for (const id in node.scope.declarations) { if (id in knownBindings) { if (knownBindings[id].shadowDepth > 0) knownBindings[id].shadowDepth--; else delete knownBindings[id]; } } } // computing a static expression outward // -> compute and backtrack if (staticChildNode) { const curStaticValue = computePureStaticValue(node, true).result; if (curStaticValue) { if ('value' in curStaticValue && typeof curStaticValue.value !== 'symbol' || typeof curStaticValue.then !== 'symbol' && typeof curStaticValue.else !== 'symbol') { staticChildValue = curStaticValue; staticChildNode = node; return; } } // no static value -> see if we should emit the asset if it exists emitStaticChildAsset(); } } }); if (!transformed) return this.callback(null, code, map); assetEmissionPromises.then(() => { if (wildcardBlocks.length) magicString.appendLeft(ast.body[0].start, wildcardBlocks.join('\n') + '\n'); code = magicString.toString(); map = map || magicString.generateMap(); if (map) { map.sources = [id]; // map.sources = map.sources.map(name => name.indexOf('!') !== -1 ? name.split('!')[1] : name); } this.callback(null, code, map); }); function validAssetEmission (assetPath) { if (!assetPath) return; // do not emit own id if (assetPath === id) return; let wildcardSuffix = ''; if (assetPath.endsWith(path.sep)) wildcardSuffix = path.sep; else if (assetPath.endsWith(path.sep + WILDCARD)) wildcardSuffix = path.sep + WILDCARD; else if (assetPath.endsWith(WILDCARD)) wildcardSuffix = WILDCARD; // do not emit __dirname if (!options.emitDirnameAll && (assetPath === dir + wildcardSuffix)) return; // do not emit cwd if (!options.emitFilterAssetBaseAll && (assetPath === (options.filterAssetBase || cwd) + wildcardSuffix)) return; // do not emit node_modules if (assetPath.endsWith(path.sep + 'node_modules' + wildcardSuffix)) return; // do not emit directories above __dirname if (dir.startsWith(assetPath.substr(0, assetPath.length - wildcardSuffix.length) + path.sep)) return; // do not emit asset directories higher than the node_modules base if a package if (pkgBase) { const nodeModulesBase = id.substr(0, id.indexOf(path.sep + 'node_modules')) + path.sep + 'node_modules' + path.sep; if (!assetPath.startsWith(nodeModulesBase)) { if (options.debugLog) { if (assetEmission(assetPath)) console.log('Skipping asset emission of ' + assetPath.replace(wildcardRegEx, '*') + ' for ' + id + ' as it is outside the package base ' + pkgBase); } return; } } // otherwise, do not emit assets outside of the filterAssetBase else if (!assetPath.startsWith(options.filterAssetBase || cwd)) { if (options.debugLog) { if (assetEmission(assetPath)) console.log('Skipping asset emission of ' + assetPath.replace(wildcardRegEx, '*') + ' for ' + id + ' as it is outside the filterAssetBase directory ' + (options.filterAssetBase || cwd)); } return; } // finally, use custom emit filter if (options.customEmit) { const customEmit = options.customEmit(assetPath, { id, isRequire: false }); if (customEmit === false) return; if (typeof customEmit === 'string') return () => customEmit; } return assetEmission(assetPath); } function assetEmission (assetPath) { // verify the asset file / directory exists const wildcardIndex = assetPath.indexOf(WILDCARD); const dirIndex = wildcardIndex === -1 ? assetPath.length : assetPath.lastIndexOf(path.sep, wildcardIndex); const basePath = assetPath.substr(0, dirIndex); try { const stats = statSync(basePath); if (wildcardIndex !== -1 && stats.isFile()) return; if (stats.isFile()) return emitAsset; if (stats.isDirectory()) return emitAssetDirectory; } catch (e) { return; } } function resolveAbsolutePathOrUrl (value) {