UNPKG

@rollup/plugin-dynamic-import-vars

Version:

Resolving dynamic imports that contain variables.

246 lines (206 loc) 7.55 kB
import path from 'path'; import { walk } from 'estree-walker'; import MagicString from 'magic-string'; import fastGlob from 'fast-glob'; import { generate } from 'astring'; import { createFilter } from '@rollup/pluginutils'; class VariableDynamicImportError extends Error {} /* eslint-disable-next-line no-template-curly-in-string */ const example = 'For example: import(`./foo/${bar}.js`).'; function sanitizeString(str) { if (str === '') return str; if (str.includes('*')) { throw new VariableDynamicImportError('A dynamic import cannot contain * characters.'); } return fastGlob.escapePath(str); } function templateLiteralToGlob(node) { let glob = ''; for (let i = 0; i < node.quasis.length; i += 1) { glob += sanitizeString(node.quasis[i].value.raw); if (node.expressions[i]) { glob += expressionToGlob(node.expressions[i]); } } return glob; } function callExpressionToGlob(node) { const { callee } = node; if ( callee.type === 'MemberExpression' && callee.property.type === 'Identifier' && callee.property.name === 'concat' ) { return `${expressionToGlob(callee.object)}${node.arguments.map(expressionToGlob).join('')}`; } return '*'; } function binaryExpressionToGlob(node) { if (node.operator !== '+') { throw new VariableDynamicImportError(`${node.operator} operator is not supported.`); } return `${expressionToGlob(node.left)}${expressionToGlob(node.right)}`; } function expressionToGlob(node) { switch (node.type) { case 'TemplateLiteral': return templateLiteralToGlob(node); case 'CallExpression': return callExpressionToGlob(node); case 'BinaryExpression': return binaryExpressionToGlob(node); case 'Literal': { return sanitizeString(node.value); } default: return '*'; } } const defaultProtocol = 'file:'; const ignoredProtocols = ['data:', 'http:', 'https:']; function shouldIgnore(glob) { const containsAsterisk = glob.includes('*'); const globURL = new URL(glob, defaultProtocol); const containsIgnoredProtocol = ignoredProtocols.some( (ignoredProtocol) => ignoredProtocol === globURL.protocol ); return !containsAsterisk || containsIgnoredProtocol; } function dynamicImportToGlob(node, sourceString) { let glob = expressionToGlob(node); if (shouldIgnore(glob)) { return null; } glob = glob.replace(/\*\*/g, '*'); if (glob.startsWith('*')) { throw new VariableDynamicImportError( `invalid import "${sourceString}". It cannot be statically analyzed. Variable dynamic imports must start with ./ and be limited to a specific directory. ${example}` ); } if (glob.startsWith('/')) { throw new VariableDynamicImportError( `invalid import "${sourceString}". Variable absolute imports are not supported, imports must start with ./ in the static part of the import. ${example}` ); } if (!glob.startsWith('./') && !glob.startsWith('../')) { throw new VariableDynamicImportError( `invalid import "${sourceString}". Variable bare imports are not supported, imports must start with ./ in the static part of the import. ${example}` ); } // Disallow ./*.ext const ownDirectoryStarExtension = /^\.\/\*\.\w+$/; if (ownDirectoryStarExtension.test(glob)) { throw new VariableDynamicImportError( `${ `invalid import "${sourceString}". Variable imports cannot import their own directory, ` + 'place imports in a separate directory or make the import filename more specific. ' }${example}` ); } if (path.extname(glob) === '') { throw new VariableDynamicImportError( `invalid import "${sourceString}". A file extension must be included in the static part of the import. ${example}` ); } return glob; } function dynamicImportVariables({ include, exclude, warnOnError, errorWhenNoFilesFound } = {}) { const filter = createFilter(include, exclude); return { name: 'rollup-plugin-dynamic-import-variables', transform(code, id) { if (!filter(id)) { return null; } const parsed = this.parse(code); let dynamicImportIndex = -1; let ms; walk(parsed, { enter: (node) => { if (node.type !== 'ImportExpression') { return; } dynamicImportIndex += 1; let importArg; if (node.arguments && node.arguments.length > 0) { // stringify the argument node, without indents, removing newlines and using single quote strings importArg = generate(node.arguments[0], { indent: '' }) .replace(/\n/g, '') .replace(/"/g, "'"); } try { // see if this is a variable dynamic import, and generate a glob expression const glob = dynamicImportToGlob(node.source, code.substring(node.start, node.end)); if (!glob) { // this was not a variable dynamic import return; } // execute the glob const result = fastGlob.sync(glob, { cwd: path.dirname(id) }); const paths = result.map((r) => r.startsWith('./') || r.startsWith('../') ? r : `./${r}` ); if (errorWhenNoFilesFound && paths.length === 0) { const error = new Error( `No files found in ${glob} when trying to dynamically load concatted string from ${id}` ); if (warnOnError) { this.warn(error); } else { this.error(error); } } // create magic string if it wasn't created already ms = ms || new MagicString(code); // unpack variable dynamic import into a function with import statements per file, rollup // will turn these into chunks automatically ms.prepend( `function __variableDynamicImportRuntime${dynamicImportIndex}__(path) { switch (path) { ${paths .map((p) => ` case '${p}': return import('${p}'${importArg ? `, ${importArg}` : ''});`) .join('\n')} ${` default: return new Promise(function(resolve, reject) { (typeof queueMicrotask === 'function' ? queueMicrotask : setTimeout)( reject.bind(null, new Error("Unknown variable dynamic import: " + path)) ); })\n`} } }\n\n` ); // call the runtime function instead of doing a dynamic import, the import specifier will // be evaluated at runtime and the correct import will be returned by the injected function ms.overwrite( node.start, node.start + 6, `__variableDynamicImportRuntime${dynamicImportIndex}__` ); } catch (error) { if (error instanceof VariableDynamicImportError) { // TODO: line number if (warnOnError) { this.warn(error); } else { this.error(error); } } else { this.error(error); } } } }); if (ms && dynamicImportIndex !== -1) { return { code: ms.toString(), map: ms.generateMap({ file: id, includeContent: true, hires: true }) }; } return null; } }; } export { VariableDynamicImportError, dynamicImportVariables as default, dynamicImportToGlob }; //# sourceMappingURL=index.js.map