react-saasify-chrisvxd
Version:
React components for Saasify web clients.
191 lines (161 loc) • 5.56 kB
JavaScript
const types = require('@babel/types');
const template = require('@babel/template').default;
const traverse = require('@babel/traverse').default;
const urlJoin = require('../utils/urlJoin');
const isURL = require('../utils/is-url');
const nodeBuiltins = require('node-libs-browser');
const requireTemplate = template('require("_bundle_loader")');
const argTemplate = template('require.resolve(MODULE)');
const serviceWorkerPattern = ['navigator', 'serviceWorker', 'register'];
module.exports = {
ImportDeclaration(node, asset) {
asset.isES6Module = true;
addDependency(asset, node.source);
},
ExportNamedDeclaration(node, asset) {
asset.isES6Module = true;
if (node.source) {
addDependency(asset, node.source);
}
},
ExportAllDeclaration(node, asset) {
asset.isES6Module = true;
addDependency(asset, node.source);
},
ExportDefaultDeclaration(node, asset) {
asset.isES6Module = true;
},
CallExpression(node, asset, ancestors) {
let {callee, arguments: args} = node;
let isRequire =
types.isIdentifier(callee) &&
callee.name === 'require' &&
args.length === 1 &&
types.isStringLiteral(args[0]) &&
!hasBinding(ancestors, 'require') &&
!isInFalsyBranch(ancestors);
if (isRequire) {
let optional = ancestors.some(a => types.isTryStatement(a)) || undefined;
addDependency(asset, args[0], {optional});
return;
}
let isDynamicImport =
callee.type === 'Import' &&
args.length === 1 &&
types.isStringLiteral(args[0]);
if (isDynamicImport) {
if (isURL(args[0].value)) return;
asset.addDependency('_bundle_loader');
addDependency(asset, args[0], {dynamic: true});
node.callee = requireTemplate().expression;
node.arguments[0] = argTemplate({MODULE: args[0]}).expression;
asset.isAstDirty = true;
return;
}
const isRegisterServiceWorker =
types.isStringLiteral(args[0]) &&
types.matchesPattern(callee, serviceWorkerPattern);
if (isRegisterServiceWorker) {
// Treat service workers as an entry point so filenames remain consistent across builds.
// https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#avoid_changing_the_url_of_your_service_worker_script
addURLDependency(asset, args[0], {entry: true, isolated: true});
return;
}
},
NewExpression(node, asset) {
const {callee, arguments: args} = node;
const isWebWorker =
callee.type === 'Identifier' &&
(callee.name === 'Worker' || callee.name === 'SharedWorker') &&
args.length === 1 &&
types.isStringLiteral(args[0]);
if (isWebWorker) {
addURLDependency(asset, args[0], {isolated: true});
return;
}
}
};
function hasBinding(node, name) {
if (Array.isArray(node)) {
return node.some(ancestor => hasBinding(ancestor, name));
} else if (
types.isProgram(node) ||
types.isBlockStatement(node) ||
types.isBlock(node)
) {
return node.body.some(statement => hasBinding(statement, name));
} else if (
types.isFunctionDeclaration(node) ||
types.isFunctionExpression(node) ||
types.isArrowFunctionExpression(node)
) {
return (
(node.id && node.id.name === name) ||
node.params.some(
param => types.isIdentifier(param) && param.name === name
)
);
} else if (types.isVariableDeclaration(node)) {
return node.declarations.some(declaration => declaration.id.name === name);
}
return false;
}
function isInFalsyBranch(ancestors) {
// Check if any ancestors are if statements
return ancestors.some((node, index) => {
if (types.isIfStatement(node)) {
let res = evaluateExpression(node.test);
if (res && res.confident) {
// If the test is truthy, exclude the dep if it is in the alternate branch.
// If the test if falsy, exclude the dep if it is in the consequent branch.
let child = ancestors[index + 1];
return res.value ? child === node.alternate : child === node.consequent;
}
}
});
}
function evaluateExpression(node) {
// Wrap the node in a standalone program so we can traverse it
node = types.file(types.program([types.expressionStatement(node)]));
// Find the first expression and evaluate it.
let res = null;
traverse(node, {
Expression(path) {
res = path.evaluate();
path.stop();
}
});
return res;
}
function addDependency(asset, node, opts = {}) {
// Don't bundle node builtins
if (asset.options.target === 'node' && node.value in nodeBuiltins) {
return;
}
// If this came from an inline <script> tag, throw an error.
// TODO: run JSPackager on inline script tags.
let inlineHTML =
asset.options.rendition && asset.options.rendition.inlineHTML;
if (inlineHTML) {
let err = new Error(
'Imports and requires are not supported inside inline <script> tags yet.'
);
err.loc = node.loc && node.loc.start;
throw err;
}
if (!asset.options.bundleNodeModules) {
const isRelativeImport = /^[/~.]/.test(node.value);
if (!isRelativeImport) return;
}
opts.loc = node.loc && node.loc.start;
asset.addDependency(node.value, opts);
}
function addURLDependency(asset, node, opts = {}) {
opts.loc = node.loc && node.loc.start;
let assetPath = asset.addURLDependency(node.value, opts);
if (!isURL(assetPath)) {
assetPath = urlJoin(asset.options.publicURL, assetPath);
}
node.value = assetPath;
asset.isAstDirty = true;
}