babel-plugin-ng-hot-reload
Version:
246 lines (244 loc) • 10.5 kB
JavaScript
/**
* Note: Please make sure to only use syntax which is compatible with IE11 here
* because the transformation runs after @babel/env!
*/
Object.defineProperty(exports, "__esModule", { value: true });
function default_1(babel, { angularGlobal = false, forceRefresh = true, preserveState = true, angularReference = `require('angular'), angular`, }) {
const { types: t, template } = babel;
let state = {
topLevelImports: new Set(),
topLevelExports: new Map(),
topLevelExportModule: new Set(),
pathsToRemove: new Set(),
pathsToReplace: new Map(),
};
const registerAngularUse = new Map();
const corePath = 'ng-hot-reload-core';
const EXPORTS_PREFIX = '__ngHotReload_';
const OUTER_EXPORT_VARIABLE = '__ngHotReload_outer_exports__';
const INNER_EXPORT_VARIABLE = '__ngHotReload_inner_exports__';
const ANGULAR_PACKAGE_NAME = 'angular';
const buildHotReloadTemplate = template(`
/* babel-plugin-ng-hot-reload */
var ${OUTER_EXPORT_VARIABLE} = (function(__ngHotReloadLoaderAngularGlobal) {
var ${INNER_EXPORT_VARIABLE};
var angular = module.hot ? (function() {
var loader = require(${JSON.stringify(corePath)});
return loader.decorateAngular({
angular: __ngHotReloadLoaderAngularGlobal,
forceRefresh: ${JSON.stringify(forceRefresh)},
preserveState: ${JSON.stringify(preserveState)}
});
})() : __ngHotReloadLoaderAngularGlobal;
try {
${INNER_EXPORT_VARIABLE} = (function() {
/* babel-plugin-ng-hot-reload end*/
%%source%%
/* babel-plugin-ng-hot-reload */
%%exports%%
})();
} finally {
(function() {
if (module.hot && angular.__ngHotReload$didRegisterProviders) {
module.hot.accept(function(err) {
if (err) {
console.error(err);
}
});
}
})();
}
return ${INNER_EXPORT_VARIABLE};
})(${angularReference});
/* babel-plugin-ng-hot-reload end */
`);
const visitor = {
Program: {
exit(path) {
// Only apply the hot-module-replacement template when usage of angular
// is detected
if (!registerAngularUse.has(path)) {
return;
}
const { node: { body: sourceBody }, } = path;
// Apply all transformations
state.pathsToRemove.forEach((pathToRemove) => pathToRemove.remove());
state.pathsToReplace.forEach((replacer, pathToReplace) => pathToReplace.replaceWith(replacer));
// Adds a return statement to the inner wrapper function which
// contains the exports from the module
// Also adds an destructor to the outside of the wrapper to make the
// exports from inside the wrapper available in global scope
//
// export default Controller;
// export const namedExport
//
// == becomes ==
//
// Appended to wrapped source:
// --
// return {
// __export_default: Controller,
// __export_namedExport: namedExport
// }
//
// Added to outer wrapper:
// --
// var OUTER_EXPORT_VARIABLE = (function() {...})();
//
// Appended to template:
// --
// var __export_default = OUTER_EXPORT_VARIABLE.__export_default,
// __export_namedExport = OUTER_EXPORT_VARIABLE.__export_namedExport;
//
// export {
// __export_default as default,
// __export_namedExport as namedExport
// };
const moduleExports = [];
const extractedExports = [];
const topLevelExports = [];
state.topLevelExports.forEach((value, key) => {
const identifierKey = `${EXPORTS_PREFIX}${key}`;
// Properties of the return statement
moduleExports.push(t.objectProperty(t.identifier(identifierKey), value));
// Properties for the outer const destructor
extractedExports.push(t.variableDeclarator(t.identifier(identifierKey), t.memberExpression(t.identifier(OUTER_EXPORT_VARIABLE), t.identifier(identifierKey))));
// Restore the topLevelexports
topLevelExports.push(t.exportSpecifier(t.identifier(identifierKey), t.identifier(key)));
});
// Wrap the properties in return statement
const exportsAsReturnStatement = t.returnStatement(t.objectExpression(moduleExports));
// build the template
const hotReloadTemplateAst = buildHotReloadTemplate({
source: sourceBody,
exports: exportsAsReturnStatement,
});
const finalBody = [
...state.topLevelImports.values(),
hotReloadTemplateAst,
extractedExports.length > 0 ? t.variableDeclaration('var', extractedExports) : undefined,
topLevelExports.length > 0 ? t.exportNamedDeclaration(null, topLevelExports) : undefined,
...state.topLevelExportModule.values(),
].filter(Boolean);
path.node.body = finalBody;
},
},
ImportDeclaration(path) {
const { node } = path;
// Check if the import is angular
if (node.source.value === ANGULAR_PACKAGE_NAME) {
const parentProgram = path.findParent((path) => path.isProgram());
registerAngularUse.set(parentProgram, true);
}
// Add import to the list and remove it for now
state.topLevelImports.add(node);
state.pathsToRemove.add(path);
},
ExportNamedDeclaration(path) {
// Check if we have an export from a source
// e.g:
// export { foo } from 'bar';
if (path.node.source) {
// Same behavior as ExportAllDeclaration
state.topLevelExportModule.add(path.node);
state.pathsToRemove.add(path);
return;
}
const declaration = path.get('declaration');
if (declaration.node !== null) {
if (declaration.type === 'VariableDeclaration') {
// Export variable declaration
// e.g:
// export const foo = 'bar',
// bar = 'foo';
const { declarations } = declaration.node;
declarations.forEach((declaration) => {
const identifier = declaration.id;
state.topLevelExports.set(identifier.name, identifier);
});
}
else {
// Export right before declaration
// e.g:
// export class Foo {};
const identifier = declaration.get('id').node;
state.topLevelExports.set(identifier.name, identifier);
}
// Replace the export declaration with the actual declaration
state.pathsToReplace.set(path, declaration);
}
else {
// Export specifier
// e.g:
// const foo = 'bar';
// const bar = 'foo';
// export { foo, bar as bar2 };
const { specifiers } = path.node;
if (specifiers && specifiers.length > 0) {
specifiers.forEach(({ local, exported }) => {
state.topLevelExports.set(exported.name, local);
});
}
// Remove the export
state.pathsToRemove.add(path);
}
},
ExportDefaultDeclaration(path) {
const declaration = path.get('declaration');
if (declaration.type === 'Identifier' || declaration.type === 'MemberExpression') {
// If export is a simple identifier we can use the node directly
//
// Identifier:
// const foo = 'bar';
// export default foo;
//
// MemberExpression:
// const obj = {
// foo: 'bar'
// };
// export default obj.foo;
state.topLevelExports.set('default', declaration.node);
// Remove the default export
state.pathsToRemove.add(path);
}
else {
// If we have a declaration in the default export we have to get the
// identifier through the `id` property
// e.g:
// export default class Foo {}
state.topLevelExports.set('default', declaration.get('id').node);
// Replace the export declaration with the actual declaration
state.pathsToReplace.set(path, declaration);
}
},
ExportAllDeclaration(path) {
// ExportAllDeclarations can be safely removed and later reapplied
state.topLevelExportModule.add(path.node);
state.pathsToRemove.add(path);
},
};
// When angular is used as global variable check for usage of the identifier
if (angularGlobal) {
visitor['Identifier'] = function (path) {
const { node } = path;
if (node.name === angularGlobal) {
const parentProgram = path.findParent((path) => path.isProgram());
registerAngularUse.set(parentProgram, true);
}
};
}
return {
name: 'ng-hot-reload',
post() {
// Clear the storage after each file
state.topLevelImports.clear();
state.topLevelExports.clear();
state.topLevelExportModule.clear();
state.pathsToRemove.clear();
state.pathsToReplace.clear();
},
visitor,
};
}
exports.default = default_1;
;