You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
217 lines
6.4 KiB
JavaScript
217 lines
6.4 KiB
JavaScript
4 years ago
|
const acorn = require('acorn');
|
||
|
const stage3 = require('acorn-stage3');
|
||
|
const walk = require('acorn-walk');
|
||
|
const astring = require('astring');
|
||
|
const path = require('path');
|
||
|
const fsp = require('fs').promises;
|
||
|
|
||
|
const dirWatchers = {};
|
||
|
|
||
|
const parser = acorn.Parser.extend(stage3);
|
||
|
async function requireContext(filePath) {
|
||
|
const source = await fsp.readFile(filePath, 'utf8');
|
||
|
if (!/\brequire\s*\.\s*context\s*\(/.test(source)) {
|
||
|
delete dirWatchers[filePath];
|
||
|
return null;
|
||
|
}
|
||
|
const ast = parser.parse(source,
|
||
|
{ sourceType: 'module', ecmaVersion: 'latest', locations: true });
|
||
|
const base = path.dirname(path.resolve(filePath));
|
||
|
const nodes = [];
|
||
|
walk.simple(ast, {
|
||
|
CallExpression(node) {
|
||
|
const { callee } = node;
|
||
|
const args = node.arguments;
|
||
|
if (callee.type !== 'MemberExpression') return;
|
||
|
if (callee.object.name !== 'require') return;
|
||
|
if (callee.property.name !== 'context') return;
|
||
|
if (args.length === 0) return;
|
||
|
if (!args.every((arg) => arg.type === 'Literal')) return;
|
||
|
if (args.length > 2 && !args[2].regex) return;
|
||
|
nodes.push(node);
|
||
|
},
|
||
|
});
|
||
|
if (nodes.length === 0) {
|
||
|
delete dirWatchers[filePath];
|
||
|
return null;
|
||
|
}
|
||
|
const imports = [];
|
||
|
const dirs = [];
|
||
|
await Promise.all(nodes.map(async (node) => {
|
||
|
const args = node.arguments;
|
||
|
const directory = path.resolve(base, args[0].value);
|
||
|
const recurse = args[1] && args[1].value;
|
||
|
const regExp = args[2] && new RegExp(args[2].regex.pattern, args[2].regex.flags);
|
||
|
dirs.push(directory);
|
||
|
async function getFiles(dir, recurse, pattern) {
|
||
|
try {
|
||
|
const dirents = await fsp.readdir(dir, { withFileTypes: true });
|
||
|
const files = await Promise.all(dirents.map((dirent) => {
|
||
|
const res = path.resolve(dir, dirent.name);
|
||
|
if (dirent.isDirectory()) {
|
||
|
return (recurse !== false) && getFiles(res);
|
||
|
}
|
||
|
return (!pattern || pattern.test(res)) ? res : null;
|
||
|
}));
|
||
|
return Array.prototype.concat(...files);
|
||
|
} catch (error) {
|
||
|
if (error.code === 'ENOENT') return [];
|
||
|
throw error;
|
||
|
}
|
||
|
}
|
||
|
const files = (await getFiles(directory, recurse, regExp))
|
||
|
.filter((file) => file).map((file) => {
|
||
|
file = path.relative(base, file);
|
||
|
if (!file.startsWith('/') && !file.startsWith('.')) file = `./${file}`;
|
||
|
return file;
|
||
|
});
|
||
|
const modules = files.map((file) => Math.random().toString(36).slice(2).replace(/\d/g, '') + file.split('/').pop().split('.')[0]
|
||
|
.replace(/^\w/, (c) => c.toUpperCase())
|
||
|
.replace(/[-_]\w/g, (c) => c[1].toUpperCase()).replace(/\W/g, '$'));
|
||
|
files.forEach((file, i) => {
|
||
|
imports.push({
|
||
|
type: 'ImportDeclaration',
|
||
|
specifiers: [
|
||
|
{
|
||
|
type: 'ImportDefaultSpecifier',
|
||
|
local: { type: 'Identifier', name: modules[i] },
|
||
|
},
|
||
|
],
|
||
|
source: { type: 'Literal', value: file, raw: JSON.stringify(file) },
|
||
|
});
|
||
|
});
|
||
|
const keys = files.map((file) => `./${path.relative(directory, path.resolve(base, file))}`);
|
||
|
const contextKeys = {
|
||
|
type: 'ArrayExpression',
|
||
|
elements: keys.map((file) => (
|
||
|
{ type: 'Literal', value: file, raw: JSON.stringify(file) }
|
||
|
)),
|
||
|
};
|
||
|
const contextMap = {
|
||
|
type: 'ObjectExpression',
|
||
|
properties: keys.map((key, i) => ({
|
||
|
type: 'Property',
|
||
|
method: false,
|
||
|
shorthand: false,
|
||
|
computed: false,
|
||
|
key: { type: 'Literal', value: key, raw: JSON.stringify(key) },
|
||
|
value: {
|
||
|
type: 'ObjectExpression',
|
||
|
properties: [{
|
||
|
type: 'Property',
|
||
|
method: false,
|
||
|
shorthand: false,
|
||
|
computed: false,
|
||
|
key: { type: 'Identifier', name: 'default' },
|
||
|
value: { type: 'Identifier', name: modules[i] },
|
||
|
kind: 'init',
|
||
|
}],
|
||
|
},
|
||
|
kind: 'init',
|
||
|
})),
|
||
|
};
|
||
|
const contextFn = {
|
||
|
type: 'VariableDeclaration',
|
||
|
declarations: [
|
||
|
{
|
||
|
type: 'VariableDeclarator',
|
||
|
id: { type: 'Identifier', name: 'context' },
|
||
|
init: {
|
||
|
type: 'ArrowFunctionExpression',
|
||
|
id: null,
|
||
|
expression: true,
|
||
|
generator: false,
|
||
|
async: false,
|
||
|
params: [{ type: 'Identifier', name: 'id' }],
|
||
|
body: {
|
||
|
type: 'MemberExpression',
|
||
|
object: contextMap,
|
||
|
property: { type: 'Identifier', name: 'id' },
|
||
|
computed: true,
|
||
|
optional: false,
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
],
|
||
|
kind: 'let',
|
||
|
};
|
||
|
const keyFn = {
|
||
|
type: 'ExpressionStatement',
|
||
|
expression: {
|
||
|
type: 'AssignmentExpression',
|
||
|
operator: '=',
|
||
|
left: {
|
||
|
type: 'MemberExpression',
|
||
|
object: { type: 'Identifier', name: 'context' },
|
||
|
property: { type: 'Identifier', name: 'keys' },
|
||
|
computed: false,
|
||
|
optional: false,
|
||
|
},
|
||
|
right: {
|
||
|
type: 'ArrowFunctionExpression',
|
||
|
id: null,
|
||
|
expression: true,
|
||
|
generator: false,
|
||
|
async: false,
|
||
|
params: [],
|
||
|
body: contextKeys,
|
||
|
},
|
||
|
},
|
||
|
};
|
||
|
const contextExpr = {
|
||
|
type: 'CallExpression',
|
||
|
callee: {
|
||
|
type: 'ArrowFunctionExpression',
|
||
|
id: null,
|
||
|
expression: false,
|
||
|
generator: false,
|
||
|
async: false,
|
||
|
params: [],
|
||
|
body: {
|
||
|
type: 'BlockStatement',
|
||
|
body: [
|
||
|
contextFn,
|
||
|
keyFn,
|
||
|
{
|
||
|
type: 'ReturnStatement',
|
||
|
argument: {
|
||
|
type: 'Identifier',
|
||
|
name: 'context',
|
||
|
},
|
||
|
},
|
||
|
],
|
||
|
},
|
||
|
},
|
||
|
arguments: [],
|
||
|
optional: false,
|
||
|
};
|
||
|
delete node.callee;
|
||
|
delete node.arguments;
|
||
|
delete node.optional;
|
||
|
Object.assign(node, contextExpr);
|
||
|
}));
|
||
|
ast.body.unshift(...imports);
|
||
|
dirWatchers[filePath] = dirs;
|
||
|
return astring.generate(ast);
|
||
|
}
|
||
|
|
||
|
module.exports = function (snowpackConfig, pluginOptions) {
|
||
|
return {
|
||
|
name: 'require-context-plugin',
|
||
|
resolve: {
|
||
|
input: Array.from(pluginOptions.input || ['.js']),
|
||
|
output: ['.js'],
|
||
|
},
|
||
|
onChange({ filePath }) {
|
||
|
for (const [source, dirs] of Object.entries(dirWatchers)) {
|
||
|
if (dirs.some((dir) => filePath.startsWith(dir))) {
|
||
|
this.markChanged(source);
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
load({ filePath }) {
|
||
|
return requireContext(filePath);
|
||
|
},
|
||
|
};
|
||
|
};
|