license import traverse , { NodePath } from 'babel-traverse' ; import * as babel from 'babel-types' ; import * as clone from 'clone' ; import { Document , FileRelativeUrl , ResolvedUrl } from 'polymer-analyzer' ; import { Analysis } from 'polymer-analyzer' ; import { rollup } from 'rollup' ; import { assertIsJsDocument , getAnalysisDocument } from './analyzer-utils' ; import { serialize } from './babel-utils' ; import { AssignedBundle , BundleManifest } from './bundle-manifest' ; import { Bundler } from './bundler' ; import { getOrSetBundleModuleExportName } from './es6-module-utils' ; import { appendUrlPath , ensureLeadingDot , getFileExtension } from './url-utils' ; import { generateUniqueIdentifierName , rewriteObject } from './utils' ; /**
 * Utility class to rollup/merge ES6 modules code using rollup and rewrite
 * import statements to point to appropriate bundles.
 */ export class Es6Rewriter { constructor ( public bundler , public manifest , public bundle ) { } param url param code param document async rollup ( url , code , document ? ) { const input = '*bundle*' ; const analysis = await this . bundler . analyzer . analyze ( [ ... this . bundle . bundle . files ] ) ; const external = [ ] ; for ( const [ url , bundle ] of this . manifest . bundles ) { if ( url !== this . bundle . url ) { external . push ( ... [ ... bundle . files , url ] ) ; } } external . push ( ... this . bundler . excludes ) ; const jsImportResolvedUrls = new Map < string , Map < string , ResolvedUrl > > ( ) ; if ( document ) { jsImportResolvedUrls . set ( input , this . getEs6ImportResolutions ( document ) ) ; } const rollupBundle = await rollup ( { input , external , onwarn : ( warning ) => { } , treeshake : this . bundler . treeshake , plugins : [ { name : 'analyzerPlugin' , resolveId : ( importee , importer ? ) => importee , load : ( id ) => { if ( id === input ) { return code ; } if ( this . bundle . bundle . files . has ( id ) ) { const document = assertIsJsDocument ( getAnalysisDocument ( analysis , id ) ) ; const ast = clone ( document . parsedDocument . ast ) ; if ( ! jsImportResolvedUrls . has ( id ) ) { jsImportResolvedUrls . set ( id , this . getEs6ImportResolutions ( document ) ) ; } this . rewriteEs6SourceUrlsToResolved ( ast , jsImportResolvedUrls . get ( id ) ! ) ; this . rewriteExportAllToNamedExports ( ast , analysis ) ; const serialized = serialize ( ast ) ; if ( this . bundle . url === id || ! document . parsedDocument . contents . includes ( 'import.meta' ) ) { return serialized . code ; } const relativeUrl = ensureLeadingDot ( this . bundler . analyzer . urlResolver . relative ( this . bundle . url , id ) ) ; const newAst = this . _rewriteImportMetaToBundleMeta ( generateUniqueIdentifierName ( 'bundledImportMeta' , serialized . code ) , ast , relativeUrl ) ; const newCode = serialize ( newAst ) . code ; return newCode ; } return null ; } } , ] , } ) ; const { output } = await rollupBundle . generate ( { format : 'es' , freeze : false , } ) ; if ( output . length !== 1 ) { throw new Error ( `Failed to bundle.  Rollup generated ${ output . length } chunks or assets.  Expected 1.` ) ; } const chunkOrAsset = output [ 0 ] ; const rolledUpCode = chunkOrAsset . code || '' ; const rolledUpUrl = getFileExtension ( url ) === '.js' ? url : appendUrlPath ( url , '_inline_es6_module.js' ) ; const rolledUpDocument = await this . bundler . analyzeContents ( rolledUpUrl as ResolvedUrl , rolledUpCode ) ; const babelFile = assertIsJsDocument ( rolledUpDocument ) . parsedDocument . ast ; this . _rewriteExportStatements ( url , babelFile ) ; this . _rewriteImportStatements ( url , babelFile ) ; this . _deduplicateImportStatements ( babelFile ) ; const { code : rewrittenCode } = serialize ( babelFile ) ; return { code : rewrittenCode , map : undefined } ; } getEs6ImportResolutions ( document ) { const jsImports = document . getFeatures ( { kind : 'js-import' , imported : false , externalPackages : true , excludeBackreferences : true , } ) ; const resolutions = new Map < string , ResolvedUrl > ( ) ; for ( const jsImport of jsImports ) { const node = jsImport . astNode . node ; if ( 'source' in node ) { if ( node . source && jsImport . document !== undefined ) { resolutions . set ( node . source . value , jsImport . document . url ) ; } } else if ( node . callee && node . callee . type + '' === 'Import' && jsImport . document !== undefined ) { const source = node . arguments [ 0 ] ; if ( source ) { if ( babel . isStringLiteral ( source ) ) { resolutions . set ( source . value , jsImport . document . url ) ; } } } } return resolutions ; } rewriteEs6SourceUrlsToResolved ( node , jsImportResolvedUrls ) { const rewriteDeclarationSource = { enter ( path ) { const declaration = path . node ; const source = declaration . source && babel . isStringLiteral ( declaration . source ) && declaration . source . value ; if ( ! source ) { return ; } const resolution = jsImportResolvedUrls . get ( source ) ; if ( resolution ) { declaration . source ! . value = resolution ; } } } ; traverse ( node , { noScope : true , ExportAllDeclaration : rewriteDeclarationSource , ExportNamedDeclaration : rewriteDeclarationSource , ImportDeclaration : rewriteDeclarationSource , CallExpression : { enter ( path ) { const callExpression = path . node ; const callee = callExpression [ 'callee' ] ! ; const callArguments = callExpression [ 'arguments' ] ! ; if ( ! callee || ! callArguments || callArguments . length < 1 || ! babel . isStringLiteral ( callArguments [ 0 ] ! ) ) { return ; } if ( callee . type === 'Import' ) { callArguments [ 0 ] . value = jsImportResolvedUrls . get ( callArguments [ 0 ] . value ) ; } } } } ) ; } rewriteExportAllToNamedExports ( node , analysis ) { traverse ( node , { noScope : true , ExportAllDeclaration : { enter ( path ) { const exportAllDeclaration = path . node ; const sourceUrl = babel . isStringLiteral ( exportAllDeclaration . source ) && exportAllDeclaration . source . value ; if ( ! sourceUrl ) { return ; } const sourceDocument = getAnalysisDocument ( analysis , sourceUrl ) ; const documentExports = sourceDocument . getFeatures ( { kind : 'export' } ) ; const specifiers = [ ] ; for ( const documentExport of documentExports ) { for ( const exportIdentifier of documentExport . identifiers ) { const identifierValue = exportIdentifier . valueOf ( ) ; if ( identifierValue !== 'default' ) { specifiers . push ( babel . exportSpecifier ( babel . identifier ( identifierValue ) , babel . identifier ( identifierValue ) ) ) ; } } } const namedExportDeclaration = babel . exportNamedDeclaration ( undefined , specifiers , babel . stringLiteral ( sourceUrl ) ) ; rewriteObject ( exportAllDeclaration , namedExportDeclaration ) ; } } } ) ; } /**
     * Attempts to reduce the number of distinct import declarations by
     * combining those referencing the same source into the same declaration.
     * Results in deduplication of imports of the same item as well.  It
     * should NOT touch dynamic imports at all.
     *
     * Before:
     *     import {a} from './module-1.js';
     *     import {b} from './module-1.js';
     *     import {c} from './module-2.js';
     *     import('./module-3.js');
     *     import('./module-3.js');
     * After:
     *     import {a,b} from './module-1.js';
     *     import {c} from './module-2.js';
     *     import('./module-3.js');
     *     import('./module-3.js');
     */ private _deduplicateImportStatements ( node ) { const importDeclarations = new Map < string , babel . ImportDeclaration > ( ) ; traverse ( node , { noScope : true , ImportDeclaration : { enter ( path ) { const importDeclaration = path . node ; const source = babel . isStringLiteral ( importDeclaration . source ) && importDeclaration . source . value ; if ( ! source ) { return ; } const hasNamespaceSpecifier = importDeclaration . specifiers . some ( ( s ) => babel . isImportNamespaceSpecifier ( s ) ) ; const hasDefaultSpecifier = importDeclaration . specifiers . some ( ( s ) => babel . isImportDefaultSpecifier ( s ) ) ; if ( ! importDeclarations . has ( source ) && ! hasNamespaceSpecifier && ! hasDefaultSpecifier ) { importDeclarations . set ( source , importDeclaration ) ; } else if ( importDeclarations . has ( source ) ) { const existingDeclaration = importDeclarations . get ( source ) ! ; for ( const specifier of importDeclaration . specifiers ) { existingDeclaration . specifiers . push ( specifier ) ; } path . remove ( ) ; } } } } ) ; } /**
     * Rewrite export declarations source URLs to reference the bundle URL for
     * bundled files.
     */ private _rewriteExportStatements ( baseUrl , node ) { const this_ = this ; traverse ( node , { noScope : true , ExportNamedDeclaration : { enter ( path ) { const exportNamedDeclaration = path . node ; if ( ! exportNamedDeclaration . source || ! babel . isStringLiteral ( exportNamedDeclaration . source ) ) { return ; } const source = exportNamedDeclaration . source . value as ResolvedUrl ; const sourceBundle = this_ . manifest . getBundleForFile ( source ) ; if ( ! sourceBundle ) { exportNamedDeclaration . source . value = ensureLeadingDot ( this_ . bundler . analyzer . urlResolver . relative ( baseUrl , source ) ) ; return ; } exportNamedDeclaration . source . value = ensureLeadingDot ( this_ . bundler . analyzer . urlResolver . relative ( baseUrl , sourceBundle . url ) ) ; } } } ) ; } /**
     * Rewrite import declarations source URLs to reference the bundle URL for
     * bundled files and import names to correspond to names as exported by
     * bundles.
     */ private _rewriteImportStatements ( baseUrl , node ) { const this_ = this ; traverse ( node , { noScope : true , enter ( path ) { if ( path . node . type === 'Import' ) { this_ . _rewriteDynamicImport ( baseUrl , node , path ) ; } } , } ) ; traverse ( node , { noScope : true , ImportDeclaration : { enter ( path ) { const importDeclaration = path . node ; if ( ! babel . isStringLiteral ( importDeclaration . source ) ) { return ; } const source = importDeclaration . source . value as ResolvedUrl ; const sourceBundle = this_ . manifest . getBundleForFile ( source ) ; if ( ! sourceBundle ) { importDeclaration . source . value = ensureLeadingDot ( this_ . bundler . analyzer . urlResolver . relative ( baseUrl , source ) ) ; return ; } for ( const specifier of importDeclaration . specifiers ) { if ( babel . isImportSpecifier ( specifier ) ) { this_ . _rewriteImportSpecifierName ( specifier , source , sourceBundle ) ; } if ( babel . isImportDefaultSpecifier ( specifier ) ) { this_ . _rewriteImportDefaultSpecifier ( specifier , source , sourceBundle ) ; } if ( babel . isImportNamespaceSpecifier ( specifier ) ) { this_ . _rewriteImportNamespaceSpecifier ( specifier , source , sourceBundle ) ; } } importDeclaration . source . value = ensureLeadingDot ( this_ . bundler . analyzer . urlResolver . relative ( baseUrl , sourceBundle . url ) ) ; } } } ) ; } /**
     * Extends dynamic import statements to extract the explicitly namespace
     * export for the imported module.
     *
     * Before:
     *     import('./module-a.js')
     *         .then((moduleA) => moduleA.doSomething());
     *
     * After:
     *     import('./bundle_1.js')
     *         .then(bundle => bundle && bundle.$moduleA || {})
     *         .then((moduleA) => moduleA.doSomething());
     */ private _rewriteDynamicImport ( baseUrl , root , importNodePath ) { if ( ! importNodePath ) { return ; } const importCallExpression = importNodePath . parent ; if ( ! importCallExpression || ! babel . isCallExpression ( importCallExpression ) ) { return ; } const importCallArgument = importCallExpression . arguments [ 0 ] ; if ( ! babel . isStringLiteral ( importCallArgument ) ) { return ; } const sourceUrl = importCallArgument . value ; const resolvedSourceUrl = this . bundler . analyzer . urlResolver . resolve ( baseUrl , sourceUrl as FileRelativeUrl ) ; if ( ! resolvedSourceUrl ) { return ; } const sourceBundle = this . manifest . getBundleForFile ( resolvedSourceUrl ) ; let exportName ; if ( sourceBundle ) { exportName = getOrSetBundleModuleExportName ( sourceBundle , resolvedSourceUrl , '*' ) ; } if ( ! sourceBundle || exportName === '*' ) { const relativeSourceUrl = ensureLeadingDot ( this . bundler . analyzer . urlResolver . relative ( baseUrl , resolvedSourceUrl ) ) ; importCallArgument . value = relativeSourceUrl ; return ; } const relativeSourceUrl = ensureLeadingDot ( this . bundler . analyzer . urlResolver . relative ( baseUrl , sourceBundle . url ) ) ; importCallArgument . value = relativeSourceUrl ; const importCallExpressionParent = importNodePath . parentPath . parent ! ; if ( ! importCallExpressionParent ) { return ; } const thenifiedCallExpression = babel . callExpression ( babel . memberExpression ( clone ( importCallExpression ) , babel . identifier ( 'then' ) ) , [ babel . arrowFunctionExpression ( [ babel . identifier ( 'bundle' ) ] , babel . logicalExpression ( '||' , babel . logicalExpression ( '&&' , babel . identifier ( 'bundle' ) , babel . memberExpression ( babel . identifier ( 'bundle' ) , babel . identifier ( exportName ) ) ) , babel . objectExpression ( [ ] ) ) ) ] ) ; rewriteObject ( importCallExpression , thenifiedCallExpression ) ; } /**
     * Changes an import specifier to use the exported name defined in the bundle.
     *
     * Before:
     *     import {something} from './module-a.js';
     *
     * After:
     *     import {something_1} from './bundle_1.js';
     */ private _rewriteImportSpecifierName ( specifier , source , sourceBundle ) { const originalExportName = specifier . imported . name ; const exportName = getOrSetBundleModuleExportName ( sourceBundle , source , originalExportName ) ; specifier . imported . name = exportName ; } /**
     * Changes an import specifier to use the exported name for original module's
     * default as defined in the bundle.
     *
     * Before:
     *     import moduleA from './module-a.js';
     *
     * After:
     *     import {$moduleADefault} from './bundle_1.js';
     */ private _rewriteImportDefaultSpecifier ( specifier , source , sourceBundle ) { const exportName = getOrSetBundleModuleExportName ( sourceBundle , source , 'default' ) ; if ( exportName === 'default' ) { return ; } const importSpecifier = specifier as any as babel . ImportSpecifier ; Object . assign ( importSpecifier , { type : 'ImportSpecifier' , imported : babel . identifier ( exportName ) } ) ; } /**
     * Changes an import specifier to use the exported name for original module's
     * namespace as defined in the bundle.
     *
     * Before:
     *     import * as moduleA from './module-a.js';
     *
     * After:
     *     import {$moduleA} from './bundle_1.js';
     */ private _rewriteImportNamespaceSpecifier ( specifier , source , sourceBundle ) { const exportName = getOrSetBundleModuleExportName ( sourceBundle , source , '*' ) ; if ( exportName === '*' ) { return ; } const importSpecifier = specifier as any as babel . ImportSpecifier ; Object . assign ( importSpecifier , { type : 'ImportSpecifier' , imported : babel . identifier ( exportName ) } ) ; } private _rewriteImportMetaToBundleMeta ( bundledImportMetaIdentifierName , moduleFile , relativeUrl ) { const bundledImportMetaDeclaration = babel . variableDeclaration ( 'const' , [ babel . variableDeclarator ( babel . identifier ( bundledImportMetaIdentifierName ) , babel . objectExpression ( [ babel . spreadProperty ( babel . memberExpression ( babel . identifier ( 'import' ) , babel . identifier ( 'meta' ) ) ) , babel . objectProperty ( babel . identifier ( 'url' ) , babel . memberExpression ( babel . newExpression ( babel . identifier ( 'URL' ) , [ babel . stringLiteral ( relativeUrl ) , babel . memberExpression ( babel . memberExpression ( babel . identifier ( 'import' ) , babel . identifier ( 'meta' ) ) , babel . identifier ( 'url' ) ) ] ) , babel . identifier ( 'href' ) ) ) ] ) ) ] ) ; const newModuleFile = clone ( moduleFile ) ; traverse ( newModuleFile , { noScope : true , MetaProperty : { enter ( path ) { const metaProperty = path . node ; if ( metaProperty . meta . name !== 'import' && metaProperty . property . name !== 'meta' ) { return ; } const bundledImportMeta = babel . identifier ( bundledImportMetaIdentifierName ) ; path . replaceWith ( bundledImportMeta ) ; } , } , } ) ; newModuleFile . program . body . unshift ( bundledImportMetaDeclaration ) ; return newModuleFile ; } }