license import traverse , { NodePath } from 'babel-traverse' ; import * as babel from 'babel-types' ; import * as clone from 'clone' ; import { FileRelativeUrl , Import , PackageRelativeUrl , ResolvedUrl } from 'polymer-analyzer' ; import { rollup } from 'rollup' ; import { 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 { 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 ) { } async rollup ( url , code ) { 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 ] ) ; } } const jsImportResolvedUrls = new Map < ResolvedUrl , Map < string , ResolvedUrl > > ( ) ; const rollupBundle = await rollup ( { input , external , onwarn : ( warning ) => { } , treeshake : false , plugins : [ { name : 'analyzerPlugin' , resolveId : ( importee , importer ? ) => { if ( importee === input ) { return input ; } if ( importer ) { if ( jsImportResolvedUrls . has ( importer as ResolvedUrl ) ) { const resolutions = jsImportResolvedUrls . get ( importer as ResolvedUrl ) ! ; if ( resolutions . has ( importee ) ) { return resolutions . get ( importee ) ; } } return this . bundler . analyzer . urlResolver . resolve ( importer === input ? url : importer as ResolvedUrl , importee as FileRelativeUrl ) ! as string ; } return this . bundler . analyzer . urlResolver . resolve ( importee as PackageRelativeUrl ) ! as string ; } , load : ( id ) => { if ( id === input ) { return code ; } if ( this . bundle . bundle . files . has ( id ) ) { const document = getAnalysisDocument ( analysis , id ) ; if ( ! jsImportResolvedUrls . has ( id ) ) { const jsImports = document . getFeatures ( { kind : 'js-import' , imported : false , externalPackages : true , excludeBackreferences : true , } ) as Set < Import > ; const resolutions = new Map < string , ResolvedUrl > ( ) ; jsImportResolvedUrls . set ( id , resolutions ) ; for ( const jsImport of jsImports ) { const source = jsImport . astNode && jsImport . astNode . source && jsImport . astNode . source . value ; if ( source && jsImport . document !== undefined ) { resolutions . set ( source , jsImport . document . url ) ; } } } return document . parsedDocument . contents ; } } , } , ] , experimentalDynamicImport : true , } ) ; const { code : rolledUpCode } = await rollupBundle . generate ( { format : 'es' , freeze : false , } ) ; const rolledUpUrl = getFileExtension ( url ) === '.js' ? url : appendUrlPath ( url , '_inline_es6_module.js' ) ; const rolledUpDocument = await this . bundler . analyzeContents ( rolledUpUrl as ResolvedUrl , rolledUpCode ) ; const babelFile = rolledUpDocument . parsedDocument . ast ; this . _rewriteImportStatements ( url , babelFile ) ; this . _deduplicateImportStatements ( babelFile ) ; const { code : rewrittenCode } = serialize ( babelFile ) ; return { code : rewrittenCode , map : undefined } ; } /**
     * 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.
     *
     * Before:
     *     import {a} from './module-1.js';
     *     import {b} from './module-1.js';
     *     import {c} from './module-2.js';
     * After:
     *     import {a,b} from './module-1.js';
     *     import {c} from './module-2.js';
     */ private _deduplicateImportStatements ( node ) { const importDeclarations = new Map < string , babel . ImportDeclaration > ( ) ; traverse ( node , { noScope : true , ImportDeclaration : { enter ( path ) { const importDeclaration = path . node ; if ( ! babel . isImportDeclaration ( importDeclaration ) ) { return ; } 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 import declarations source URLs 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 as babel . ImportDeclaration ; if ( ! babel . isStringLiteral ( importDeclaration . source ) ) { return ; } const source = importDeclaration . source . value as ResolvedUrl ; const sourceBundle = this_ . manifest . getBundleForFile ( source ) ; if ( ! sourceBundle ) { importDeclaration . source . value = 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(({$moduleA}) => $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 . objectPattern ( [ babel . objectProperty ( babel . identifier ( exportName ) , babel . identifier ( exportName ) , undefined , true ) as any ] ) , ] , babel . identifier ( exportName ) ) ] ) ; 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 ) } ) ; } }