UNPKG

babel-plugin-rewire-exports

Version:

Babel plugin for stubbing (ES6, ES2015) module exports

282 lines (251 loc) 9.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = _default; var _template = _interopRequireDefault(require("@babel/template")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _default({ types: t }) { const defaultIdentifier = t.identifier('default'); const rewireIdentifier = t.identifier('rewire'); const restoreIdentifier = t.identifier('restore'); const stubIdentifier = t.identifier('$stub'); const VISITED = Symbol('visited'); const buildStub = (0, _template.default)(` export function REWIRE(STUB) { LOCAL = STUB; } `, { sourceType: 'module' }); const buildRestore = (0, _template.default)(` export function RESTORE() { BODY } `, { sourceType: 'module' }); function buildNamedExport(local, exported) { return markVisited(t.exportNamedDeclaration(null, [t.exportSpecifier(t.identifier(local.name), t.identifier(exported.name))])); } function markVisited(node) { node[VISITED] = true; return node; } function captureVariableDeclarations(path) { return Object.values(path.getOuterBindingIdentifiers(path)).map(([id]) => { return { exported: t.cloneNode(id), local: id }; }); } return { name: 'rewire-exports', visitor: { Program: { enter(path, state) { state.exports = []; }, exit(path, { exports }) { if (!exports.length) return; // de-duplicate the exports const unique = exports.reduce((acc, e) => { const key = e.exported.name; if (!acc[key]) { acc[key] = e; } return acc; }, {}); exports = Object.keys(unique).map(k => unique[k]); // generate temp variables if it's required to capture original values const tempVars = []; exports.filter(e => !e.original).forEach(e => { const { exported, local } = e; if (path.scope.hasBinding(exported.name) && exported.name !== local.name) { e.original = exported; } else { const temp = e.original = path.scope.generateUidIdentifierBasedOnNode(exported); tempVars.push(t.variableDeclarator(temp, local)); } }); // generate new IDs to keep sourcemaps clean const rewired = exports.map(({ exported, local, original }) => ({ exported: t.identifier(exported.name), local: t.identifier(local.name), original: t.identifier(original.name) })); // generate stub functions const hasConflictingBinding = path.scope.hasOwnBinding('rewire'); const stubs = rewired.map(({ exported, local }) => { let rewire = t.isIdentifier(exported, defaultIdentifier) && !hasConflictingBinding ? rewireIdentifier : t.identifier(`rewire$${exported.name}`); return markVisited(buildStub({ REWIRE: rewire, LOCAL: local, STUB: stubIdentifier })); }); // generate restore function const restore = path.scope.hasOwnBinding('restore') ? t.identifier('restore$rewire') : restoreIdentifier; const assignments = rewired.map(({ local, original }) => t.expressionStatement(t.assignmentExpression('=', local, original))); const body = [...stubs, markVisited(buildRestore({ RESTORE: restore, BODY: assignments }))]; if (tempVars.length) { body.unshift(t.variableDeclaration('var', tempVars)); } path.pushContainer('body', body); } }, // export default ExportDefaultDeclaration(path, { exports, opts }) { const declaration = path.node.declaration; const isIdentifier = t.isIdentifier(declaration); const binding = isIdentifier && path.scope.getBinding(declaration.name); if (opts.unsafeConst && binding && binding.kind === 'const') { // allow to rewire constants binding.kind = 'let'; binding.path.parent.kind = 'let'; } const isImmutable = !binding || ['const', 'module'].includes(binding.kind); if (isIdentifier && !isImmutable) { // export default foo exports.push({ exported: defaultIdentifier, local: declaration }); path.replaceWith(buildNamedExport(declaration, defaultIdentifier)); } else if (t.isFunctionDeclaration(declaration)) { //export default function () {} const id = declaration.id || path.scope.generateUidIdentifier('default'); exports.push({ exported: defaultIdentifier, local: id }); path.replaceWith(buildNamedExport(id, defaultIdentifier)); path.scope.removeBinding(id.name); path.scope.push({ id, init: t.functionExpression(declaration.id, declaration.params, declaration.body, declaration.generator, declaration.async), unique: true }); } else if (t.isClassDeclaration(declaration)) { //export default class {} const id = declaration.id || path.scope.generateUidIdentifier('default'); exports.push({ exported: defaultIdentifier, local: id }); const [varDeclaration] = path.replaceWithMultiple([t.variableDeclaration('var', [t.variableDeclarator(id, t.classExpression(declaration.id, declaration.superClass, declaration.body, declaration.decorators || []))]), buildNamedExport(id, defaultIdentifier)]); path.scope.registerDeclaration(varDeclaration); } else { // export default ... const id = path.scope.generateUidIdentifier('default'); exports.push({ exported: defaultIdentifier, local: id }); const [varDeclaration] = path.replaceWithMultiple([t.variableDeclaration('var', [t.variableDeclarator(id, declaration)]), buildNamedExport(id, defaultIdentifier)]); path.scope.registerDeclaration(varDeclaration); } }, // export {} ExportNamedDeclaration(path, { exports, opts }) { if (path.node[VISITED]) return; // export { foo } from './bar.js' if (path.node.source) return; const declaration = path.node.declaration; if (t.isVariableDeclaration(declaration)) { // export const foo = 'bar' if (declaration.kind === 'const') { if (opts.unsafeConst) { declaration.kind = 'let'; // convert const to let } else { // convert export variable declaration to export specifier // export const foo = 'bar'; → const foo = 'bar'; export { foo }; const identifiers = captureVariableDeclarations(path); const [varDeclaration] = path.replaceWithMultiple([declaration, t.exportNamedDeclaration(null, identifiers.map(({ exported, local }) => t.exportSpecifier(t.identifier(local.name), t.identifier(exported.name))))]); path.scope.registerDeclaration(varDeclaration); return; // visitor will handle the added export specifier later } } exports.push(...captureVariableDeclarations(path)); } else if (t.isFunctionDeclaration(declaration)) { // export function foo() {} const id = declaration.id; exports.push({ exported: t.cloneNode(id), local: id }); path.replaceWith(buildNamedExport(id, id)); path.scope.removeBinding(id.name); path.scope.push({ id, init: t.functionExpression(declaration.id, declaration.params, declaration.body, declaration.generator, declaration.async), unique: true }); } else if (t.isClassDeclaration(declaration)) { // export class Foo {} const id = declaration.id; exports.push({ exported: t.cloneNode(id), local: id }); const [varDeclaration] = path.replaceWithMultiple([t.variableDeclaration('var', [t.variableDeclarator(id, t.classExpression(id, declaration.superClass, declaration.body, declaration.decorators || []))]), buildNamedExport(id, id)]); path.scope.registerDeclaration(varDeclaration); } else { // export {foo} path.node.specifiers.forEach(node => { const { exported, local } = node; const binding = path.scope.getBinding(local.name); if (!binding) return; if (opts.unsafeConst && binding.kind === 'const') { // allow to rewire constants binding.kind = 'let'; binding.path.parent.kind = 'let'; } else if (['const', 'module'].includes(binding.kind)) { // const and imports const id = path.scope.generateUidIdentifier(local.name); exports.push({ exported, local: id }); const [varDeclaration] = path.insertBefore(t.variableDeclaration('var', [t.variableDeclarator(id, local)])); path.scope.registerDeclaration(varDeclaration); node.local = id; return; } exports.push({ exported, local }); }); } } } }; }