/** * Both used by zrender and echarts. */ const assert = require('assert'); const nodePath = require('path'); const basename = nodePath.basename; const extname = nodePath.extname; const babelTypes = require('@babel/types'); const babelTemplate = require('@babel/template'); const helperModuleTransforms = require('@babel/helper-module-transforms'); const isModule = helperModuleTransforms.isModule; const isSideEffectImport = helperModuleTransforms.isSideEffectImport; const ensureStatementsHoisted = helperModuleTransforms.ensureStatementsHoisted; module.exports = function ({types, template}, options) { return { visitor: { Program: { exit(path) { // For now this requires unambiguous rather that just sourceType // because Babel currently parses all files as sourceType:module. if (!isModule(path, true /* requireUnambiguous */)) { return; } // Rename the bindings auto-injected into the scope so there is no // risk of conflict between the bindings. path.scope.rename('exports'); path.scope.rename('module'); path.scope.rename('require'); path.scope.rename('__filename'); path.scope.rename('__dirname'); const meta = rewriteModuleStatementsAndPrepare(path); let headers = []; let tails = []; const checkExport = createExportChecker(); for (const [source, metadata] of meta.source) { headers.push(...buildRequireStatements(types, source, metadata)); headers.push(...buildNamespaceInitStatements(meta, metadata, checkExport)); } tails.push(...buildLocalExportStatements(meta, checkExport)); ensureStatementsHoisted(headers); // FIXME ensure tail? path.unshiftContainer('body', headers); path.pushContainer('body', tails); checkAssignOrUpdateExport(path, meta); } } } }; }; /** * Remove all imports and exports from the file, and return all metadata * needed to reconstruct the module's behavior. * @return {ModuleMetadata} */ function normalizeModuleAndLoadMetadata(programPath) { nameAnonymousExports(programPath); const {local, source} = getModuleMetadata(programPath); removeModuleDeclarations(programPath); // Reuse the imported namespace name if there is one. for (const [, metadata] of source) { if (metadata.importsNamespace.size > 0) { // This is kind of gross. If we stop using `loose: true` we should // just make this destructuring assignment. metadata.name = metadata.importsNamespace.values().next().value; } } return { exportName: 'exports', exportNameListName: null, local, source }; } /** * Get metadata about the imports and exports present in this module. */ function getModuleMetadata(programPath) { const localData = getLocalExportMetadata(programPath); const sourceData = new Map(); const getData = sourceNode => { const source = sourceNode.value; let data = sourceData.get(source); if (!data) { data = { name: programPath.scope.generateUidIdentifier( basename(source, extname(source)) ).name, interop: 'none', loc: null, // Data about the requested sources and names. imports: new Map(), // importsNamespace: import * as util from './a/b/util'; importsNamespace: new Set(), // Metadata about data that is passed directly from source to export. reexports: new Map(), reexportNamespace: new Set(), reexportAll: null, }; sourceData.set(source, data); } return data; }; programPath.get('body').forEach(child => { if (child.isImportDeclaration()) { const data = getData(child.node.source); if (!data.loc) { data.loc = child.node.loc; } child.get('specifiers').forEach(spec => { if (spec.isImportDefaultSpecifier()) { const localName = spec.get('local').node.name; data.imports.set(localName, 'default'); const reexport = localData.get(localName); if (reexport) { localData.delete(localName); reexport.names.forEach(name => { data.reexports.set(name, 'default'); }); } } else if (spec.isImportNamespaceSpecifier()) { const localName = spec.get('local').node.name; assert( data.importsNamespace.size === 0, `Duplicate import namespace: ${localName}` ); data.importsNamespace.add(localName); const reexport = localData.get(localName); if (reexport) { localData.delete(localName); reexport.names.forEach(name => { data.reexportNamespace.add(name); }); } } else if (spec.isImportSpecifier()) { const importName = spec.get('imported').node.name; const localName = spec.get('local').node.name; data.imports.set(localName, importName); const reexport = localData.get(localName); if (reexport) { localData.delete(localName); reexport.names.forEach(name => { data.reexports.set(name, importName); }); } } }); } else if (child.isExportAllDeclaration()) { const data = getData(child.node.source); if (!data.loc) { data.loc = child.node.loc; } data.reexportAll = { loc: child.node.loc, }; } else if (child.isExportNamedDeclaration() && child.node.source) { const data = getData(child.node.source); if (!data.loc) { data.loc = child.node.loc; } child.get('specifiers').forEach(spec => { if (!spec.isExportSpecifier()) { throw spec.buildCodeFrameError('Unexpected export specifier type'); } const importName = spec.get('local').node.name; const exportName = spec.get('exported').node.name; data.reexports.set(exportName, importName); if (exportName === '__esModule') { throw exportName.buildCodeFrameError('Illegal export "__esModule".'); } }); } }); for (const metadata of sourceData.values()) { if (metadata.importsNamespace.size > 0) { metadata.interop = 'namespace'; continue; } let needsDefault = false; let needsNamed = false; for (const importName of metadata.imports.values()) { if (importName === 'default') { needsDefault = true; } else { needsNamed = true; } } for (const importName of metadata.reexports.values()) { if (importName === 'default') { needsDefault = true; } else { needsNamed = true; } } if (needsDefault && needsNamed) { // TODO(logan): Using the namespace interop here is unfortunate. Revisit. metadata.interop = 'namespace'; } else if (needsDefault) { metadata.interop = 'default'; } } return { local: localData, source: sourceData, }; } /** * Get metadata about local variables that are exported. * @return {Map} */ function getLocalExportMetadata(programPath){ const bindingKindLookup = new Map(); programPath.get('body').forEach(child => { let kind; if (child.isImportDeclaration()) { kind = 'import'; } else { if (child.isExportDefaultDeclaration()) { child = child.get('declaration'); } if (child.isExportNamedDeclaration() && child.node.declaration) { child = child.get('declaration'); } if (child.isFunctionDeclaration()) { kind = 'hoisted'; } else if (child.isClassDeclaration()) { kind = 'block'; } else if (child.isVariableDeclaration({ kind: 'var' })) { kind = 'var'; } else if (child.isVariableDeclaration()) { kind = 'block'; } else { return; } } Object.keys(child.getOuterBindingIdentifiers()).forEach(name => { bindingKindLookup.set(name, kind); }); }); const localMetadata = new Map(); const getLocalMetadata = idPath => { const localName = idPath.node.name; let metadata = localMetadata.get(localName); if (!metadata) { const kind = bindingKindLookup.get(localName); if (kind === undefined) { throw idPath.buildCodeFrameError(`Exporting local "${localName}", which is not declared.`); } metadata = { names: [], kind, }; localMetadata.set(localName, metadata); } return metadata; }; programPath.get('body').forEach(child => { if (child.isExportNamedDeclaration() && !child.node.source) { if (child.node.declaration) { const declaration = child.get('declaration'); const ids = declaration.getOuterBindingIdentifierPaths(); Object.keys(ids).forEach(name => { if (name === '__esModule') { throw declaration.buildCodeFrameError('Illegal export "__esModule".'); } getLocalMetadata(ids[name]).names.push(name); }); } else { child.get('specifiers').forEach(spec => { const local = spec.get('local'); const exported = spec.get('exported'); if (exported.node.name === '__esModule') { throw exported.buildCodeFrameError('Illegal export "__esModule".'); } getLocalMetadata(local).names.push(exported.node.name); }); } } else if (child.isExportDefaultDeclaration()) { const declaration = child.get('declaration'); if ( declaration.isFunctionDeclaration() || declaration.isClassDeclaration() ) { getLocalMetadata(declaration.get('id')).names.push('default'); } else { // These should have been removed by the nameAnonymousExports() call. throw declaration.buildCodeFrameError('Unexpected default expression export.'); } } }); return localMetadata; } /** * Ensure that all exported values have local binding names. */ function nameAnonymousExports(programPath) { // Name anonymous exported locals. programPath.get('body').forEach(child => { if (!child.isExportDefaultDeclaration()) { return; } // export default foo; const declaration = child.get('declaration'); if (declaration.isFunctionDeclaration()) { if (!declaration.node.id) { declaration.node.id = declaration.scope.generateUidIdentifier('default'); } } else if (declaration.isClassDeclaration()) { if (!declaration.node.id) { declaration.node.id = declaration.scope.generateUidIdentifier('default'); } } else { const id = declaration.scope.generateUidIdentifier('default'); const namedDecl = babelTypes.exportNamedDeclaration(null, [ babelTypes.exportSpecifier(babelTypes.identifier(id.name), babelTypes.identifier('default')), ]); namedDecl._blockHoist = child.node._blockHoist; const varDecl = babelTypes.variableDeclaration('var', [ babelTypes.variableDeclarator(id, declaration.node), ]); varDecl._blockHoist = child.node._blockHoist; child.replaceWithMultiple([namedDecl, varDecl]); } }); } function removeModuleDeclarations(programPath) { programPath.get('body').forEach(child => { if (child.isImportDeclaration()) { child.remove(); } else if (child.isExportNamedDeclaration()) { if (child.node.declaration) { child.node.declaration._blockHoist = child.node._blockHoist; child.replaceWith(child.node.declaration); } else { child.remove(); } } else if (child.isExportDefaultDeclaration()) { // export default foo; const declaration = child.get('declaration'); if ( declaration.isFunctionDeclaration() || declaration.isClassDeclaration() ) { declaration._blockHoist = child.node._blockHoist; child.replaceWith(declaration); } else { // These should have been removed by the nameAnonymousExports() call. throw declaration.buildCodeFrameError('Unexpected default expression export.'); } } else if (child.isExportAllDeclaration()) { child.remove(); } }); } /** * Perform all of the generic ES6 module rewriting needed to handle initial * module processing. This function will rewrite the majority of the given * program to reference the modules described by the returned metadata, * and returns a list of statements for use when initializing the module. */ function rewriteModuleStatementsAndPrepare(path) { path.node.sourceType = 'script'; const meta = normalizeModuleAndLoadMetadata(path); return meta; } /** * Create the runtime initialization statements for a given requested source. * These will initialize all of the runtime import/export logic that * can't be handled statically by the statements created by * buildExportInitializationStatements(). */ function buildNamespaceInitStatements(meta, metadata, checkExport) { const statements = []; const {localImportName, localImportDefaultName} = getLocalImportName(metadata); for (const exportName of metadata.reexportNamespace) { // Assign export to namespace object. checkExport(exportName); statements.push(buildExport({exportName, localName: localImportName})); } // Source code: // import {color2 as color2Alias, color3, color4, color5} from 'xxx'; // export {default as b} from 'xxx'; // export {color2Alias}; // export {color3}; // let color5Renamed = color5 // export {color5Renamed}; // Only two entries in metadata.reexports: // 'color2Alias' => 'color2' // 'color3' => 'color3', // 'b' => 'default' // // And consider: // export {default as defaultAsBB} from './xx/yy'; // export {exportSingle} from './xx/yy'; // No entries in metadata.imports, and 'default' exists in metadata.reexports. for (const entry of metadata.reexports.entries()) { const exportName = entry[0]; checkExport(exportName); statements.push( (localImportDefaultName || entry[1] === 'default') ? buildExport({exportName, localName: localImportName}) : buildExport({exportName, namespace: localImportName, propName: entry[1]}) ); } if (metadata.reexportAll) { const statement = buildNamespaceReexport( meta, metadata.name, checkExport ); statement.loc = metadata.reexportAll.loc; // Iterate props creating getter for each prop. statements.push(statement); } return statements; } /** * Create a re-export initialization loop for a specific imported namespace. */ function buildNamespaceReexport(meta, namespace, checkExport) { checkExport(); return babelTemplate.statement(` (function() { for (var key in NAMESPACE) { if (NAMESPACE == null || !NAMESPACE.hasOwnProperty(key) || key === 'default' || key === '__esModule') return; VERIFY_NAME_LIST; exports[key] = NAMESPACE[key]; } })(); `)({ NAMESPACE: namespace, VERIFY_NAME_LIST: meta.exportNameListName ? babelTemplate.statement(` if (Object.prototype.hasOwnProperty.call(EXPORTS_LIST, key)) return; `)({EXPORTS_LIST: meta.exportNameListName}) : null }); } function buildRequireStatements(types, source, metadata) { let headers = []; const loadExpr = types.callExpression( types.identifier('require'), // replace `require('./src/xxx')` to `require('./lib/xxx')` // for echarts and zrender in old npm or webpack. [types.stringLiteral(source.replace('/src/', '/lib/'))] ); // side effect import: import 'xxx'; if (isSideEffectImport(metadata)) { let header = types.expressionStatement(loadExpr); header.loc = metadata.loc; headers.push(header); } else { const {localImportName, localImportDefaultName} = getLocalImportName(metadata); let reqHeader = types.variableDeclaration('var', [ types.variableDeclarator( types.identifier(localImportName), loadExpr ) ]); reqHeader.loc = metadata.loc; headers.push(reqHeader); if (!localImportDefaultName) { // src: // import {someInZrUtil1 as someInZrUtil1Alias, zz} from 'zrender/core/util'; // metadata.imports: // Map { 'someInZrUtil1Alias' => 'someInZrUtil1', 'zz' => 'zz' } for (const importEntry of metadata.imports) { headers.push( babelTemplate.statement(`var IMPORTNAME = NAMESPACE.PROPNAME;`)({ NAMESPACE: localImportName, IMPORTNAME: importEntry[0], PROPNAME: importEntry[1] }) ); } } } return headers; } function getLocalImportName(metadata) { const localImportDefaultName = getDefaultName(metadata.imports); assert( !localImportDefaultName || metadata.imports.size === 1, 'Forbiden that both import default and others.' ); return { localImportName: localImportDefaultName || metadata.name, localImportDefaultName }; } function getDefaultName(map) { for (const entry of map) { if (entry[1] === 'default') { return entry[0]; } } } function buildLocalExportStatements(meta, checkExport) { let tails = []; // All local export, for example: // Map { // 'localVarMame' => { // names: [ 'exportName1', 'exportName2' ], // kind: 'var' // }, for (const localEntry of meta.local) { for (const exportName of localEntry[1].names) { checkExport(exportName); tails.push(buildExport({exportName, localName: localEntry[0]})); } } return tails; } function createExportChecker() { let someHasBeenExported; return function checkExport(exportName) { assert( !someHasBeenExported || exportName !== 'default', `Forbiden that both export default and others.` ); someHasBeenExported = true; }; } function buildExport({exportName, namespace, propName, localName}) { const exportDefault = exportName === 'default'; const head = exportDefault ? 'module.exports' : `exports.${exportName}`; let opt = {}; // FIXME // Does `PRIORITY`, `LOCATION_PARAMS` recognised as babel-template placeholder? // We have to do this for workaround temporarily. if (/^[A-Z0-9_]+$/.test(localName)) { opt[localName] = localName; } return babelTemplate.statement( localName ? `${head} = ${localName};` : `${head} = ${namespace}.${propName};` )(opt); } /** * Consider this case: * export var a; * function inject(b) { * a = b; * } * It will be transpiled to: * var a; * exports.a = 1; * function inject(b) { * a = b; * } * That is a wrong transpilation, because the `export.a` will not * be assigned as `b` when `inject` called. * Of course, it can be transpiled correctly as: * var _locals = {}; * var a; * Object.defineProperty(exports, 'a', { * get: function () { return _locals[a]; } * }; * exports.a = a; * function inject(b) { * _locals[a] = b; * } * But it is not ES3 compatible. * So we just forbiden this usage here. */ function checkAssignOrUpdateExport(programPath, meta) { let visitor = { // Include: // `a++;` (no `path.get('left')`) // `x += 1212`; UpdateExpression: { exit: function exit(path, scope) { // console.log(arguments); let left = path.get('left'); if (left && left.isIdentifier()) { asertNotAssign(path, left.node.name); } } }, // Include: // `x = 5;` (`x` is an identifier.) // `c.d = 3;` (but `c.d` is not an identifier.) // `y = function () {}` // Exclude: // `var x = 121;` // `export var x = 121;` AssignmentExpression: { exit: function exit(path) { let left = path.get('left'); if (left.isIdentifier()) { asertNotAssign(path, left.node.name); } } } }; function asertNotAssign(path, localName) { // Ignore variables that is not in global scope. if (programPath.scope.getBinding(localName) !== path.scope.getBinding(localName)) { return; } for (const localEntry of meta.local) { assert( localName !== localEntry[0], `An exported variable \`${localEntry[0]}\` is forbiden to be assigned.` ); } } programPath.traverse(visitor); }