blob: 2f5a15e4cbac7b871f1dcab851d28bbdbb561bef [file] [log] [blame]
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var path = require('path');
var estreeWalker = require('estree-walker');
var MagicString = require('magic-string');
var fastGlob = require('fast-glob');
var astring = require('astring');
var pluginutils = require('@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 = pluginutils.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;
estreeWalker.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 = astring.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;
}
};
}
exports.VariableDynamicImportError = VariableDynamicImportError;
exports.default = dynamicImportVariables;
exports.dynamicImportToGlob = dynamicImportToGlob;
module.exports = Object.assign(exports.default, exports);
//# sourceMappingURL=index.js.map