diff --git a/src/Chunk.ts b/src/Chunk.ts index 550f3bb4d34..b3fc3bb13ee 100644 --- a/src/Chunk.ts +++ b/src/Chunk.ts @@ -593,6 +593,10 @@ export default class Chunk { else magicString.addSource(new MagicString(rendered)); } } + module.syntheticExports.forEach(s => { + const rendered = s.renderBlock(renderOptions); + magicString.addSource(new MagicString(rendered)); + }); } const { renderedExports, removedExports } = module.getRenderedExports(); renderedModules[module.id] = { diff --git a/src/Module.ts b/src/Module.ts index 6124b2dce5d..b7b8689b2a3 100644 --- a/src/Module.ts +++ b/src/Module.ts @@ -25,6 +25,7 @@ import { PathTracker, UNKNOWN_PATH } from './ast/utils/PathTracker'; import ExportShimVariable from './ast/variables/ExportShimVariable'; import ExternalVariable from './ast/variables/ExternalVariable'; import NamespaceVariable from './ast/variables/NamespaceVariable'; +import SyntheticNamedExport from './ast/variables/SyntheticNamedExport'; import Variable from './ast/variables/Variable'; import Chunk from './Chunk'; import ExternalModule from './ExternalModule'; @@ -206,6 +207,8 @@ export default class Module { scope!: ModuleScope; sourcemapChain!: DecodedSourceMapOrMissing[]; sources = new Set(); + syntheticExports = new Map(); + syntheticNamedExports: boolean; transformFiles?: EmittedFile[]; userChunkNames = new Set(); usesTopLevelAwait = false; @@ -221,12 +224,19 @@ export default class Module { private transformDependencies: string[] = []; private transitiveReexports: string[] | null = null; - constructor(graph: Graph, id: string, moduleSideEffects: boolean, isEntry: boolean) { + constructor( + graph: Graph, + id: string, + moduleSideEffects: boolean, + syntheticNamedExports: boolean, + isEntry: boolean + ) { this.id = id; this.graph = graph; this.excludeFromSourcemap = /\0/.test(id); this.context = graph.getModuleContext(id); this.moduleSideEffects = moduleSideEffects; + this.syntheticNamedExports = syntheticNamedExports; this.isEntryPoint = isEntry; } @@ -439,9 +449,23 @@ export default class Module { // we don't want to create shims when we are just // probing export * modules for exports - if (this.graph.shimMissingExports && !isExportAllSearch) { - this.shimMissingExport(name); - return this.exportShimVariable; + if (!isExportAllSearch) { + if (this.syntheticNamedExports) { + let syntheticExport = this.syntheticExports.get(name); + if (!syntheticExport && !this.exports[name]) { + const defaultVariable = this.astContext.traceExport('default'); + syntheticExport = new SyntheticNamedExport(this.astContext, name, defaultVariable as any); + this.syntheticExports.set(name, syntheticExport); + } + if (syntheticExport) { + return syntheticExport; + } + } + + if (this.graph.shimMissingExports) { + this.shimMissingExport(name); + return this.exportShimVariable; + } } return undefined as any; } @@ -534,6 +558,7 @@ export default class Module { originalSourcemap, resolvedIds, sourcemapChain, + syntheticNamedExports, transformDependencies, transformFiles }: TransformModuleJSON & { @@ -551,6 +576,9 @@ export default class Module { if (typeof moduleSideEffects === 'boolean') { this.moduleSideEffects = moduleSideEffects; } + if (typeof syntheticNamedExports === 'boolean') { + this.syntheticNamedExports = syntheticNamedExports; + } timeStart('generate ast', 3); @@ -634,6 +662,7 @@ export default class Module { originalSourcemap: this.originalSourcemap, resolvedIds: this.resolvedIds, sourcemapChain: this.sourcemapChain, + syntheticNamedExports: this.syntheticNamedExports, transformDependencies: this.transformDependencies, transformFiles: this.transformFiles }; diff --git a/src/ModuleLoader.ts b/src/ModuleLoader.ts index 9c6e21f8256..dac3cf49356 100644 --- a/src/ModuleLoader.ts +++ b/src/ModuleLoader.ts @@ -17,6 +17,7 @@ import { errBadLoader, errCannotAssignModuleToChunk, errEntryCannotBeExternal, + errExternalSyntheticExports, errInternalIdCannotBeExternal, errInvalidOption, errNamespaceConflict, @@ -272,6 +273,7 @@ export class ModuleLoader { id: string, importer: string, moduleSideEffects: boolean, + syntheticNamedExports: boolean, isEntry: boolean ): Promise { const existingModule = this.modulesById.get(id); @@ -280,7 +282,13 @@ export class ModuleLoader { return Promise.resolve(existingModule); } - const module: Module = new Module(this.graph, id, moduleSideEffects, isEntry); + const module: Module = new Module( + this.graph, + id, + moduleSideEffects, + syntheticNamedExports, + isEntry + ); this.modulesById.set(id, module); this.graph.watchFiles[id] = true; const manualChunkAlias = this.getManualChunk(id); @@ -321,6 +329,9 @@ export class ModuleLoader { if (typeof sourceDescription.moduleSideEffects === 'boolean') { module.moduleSideEffects = sourceDescription.moduleSideEffects; } + if (typeof sourceDescription.syntheticNamedExports === 'boolean') { + module.syntheticNamedExports = sourceDescription.syntheticNamedExports; + } return transform(this.graph, sourceDescription, module); }) .then((source: TransformModuleJSON | ModuleJSON) => { @@ -370,7 +381,13 @@ export class ModuleLoader { } return Promise.resolve(externalModule); } else { - return this.fetchModule(resolvedId.id, importer, resolvedId.moduleSideEffects, false); + return this.fetchModule( + resolvedId.id, + importer, + resolvedId.moduleSideEffects, + resolvedId.syntheticNamedExports, + false + ); } } @@ -387,7 +404,8 @@ export class ModuleLoader { return { external: true, id: source, - moduleSideEffects: this.hasModuleSideEffects(source, true) + moduleSideEffects: this.hasModuleSideEffects(source, true), + syntheticNamedExports: false }; } return resolvedId; @@ -407,7 +425,7 @@ export class ModuleLoader { : resolveIdResult; if (typeof id === 'string') { - return this.fetchModule(id, undefined as any, true, isEntry); + return this.fetchModule(id, undefined as any, true, false, isEntry); } return error(errUnresolvedEntry(unresolvedId)); }); @@ -420,6 +438,7 @@ export class ModuleLoader { let id = ''; let external = false; let moduleSideEffects = null; + let syntheticNamedExports = false; if (resolveIdResult) { if (typeof resolveIdResult === 'object') { id = resolveIdResult.id; @@ -429,6 +448,9 @@ export class ModuleLoader { if (typeof resolveIdResult.moduleSideEffects === 'boolean') { moduleSideEffects = resolveIdResult.moduleSideEffects; } + if (typeof resolveIdResult.syntheticNamedExports === 'boolean') { + syntheticNamedExports = resolveIdResult.syntheticNamedExports; + } } else { if (this.isExternal(resolveIdResult, importer, true)) { external = true; @@ -442,13 +464,18 @@ export class ModuleLoader { } external = true; } + if (external && syntheticNamedExports) { + syntheticNamedExports = false; + this.graph.warn(errExternalSyntheticExports(source, importer)); + } return { external, id, moduleSideEffects: typeof moduleSideEffects === 'boolean' ? moduleSideEffects - : this.hasModuleSideEffects(id, external) + : this.hasModuleSideEffects(id, external), + syntheticNamedExports }; } diff --git a/src/ast/variables/SyntheticNamedExport.ts b/src/ast/variables/SyntheticNamedExport.ts new file mode 100644 index 00000000000..19d4ac85674 --- /dev/null +++ b/src/ast/variables/SyntheticNamedExport.ts @@ -0,0 +1,33 @@ +import Module, { AstContext } from '../../Module'; +import { RenderOptions } from '../../utils/renderHelpers'; +import { InclusionContext } from '../ExecutionContext'; +import ExportDefaultVariable from './ExportDefaultVariable'; +import Variable from './Variable'; + +export default class SyntheticNamedExport extends Variable { + context: AstContext; + defaultVariable: ExportDefaultVariable; + module: Module; + + constructor(context: AstContext, name: string, defaultVariable: ExportDefaultVariable) { + super(name); + this.context = context; + this.module = context.module; + this.defaultVariable = defaultVariable; + } + + include(context: InclusionContext) { + if (!this.included) { + this.included = true; + this.context.includeVariable(context, this.defaultVariable); + } + } + + renderBlock(options: RenderOptions) { + const _ = options.compact ? '' : ' '; + const name = this.getName(); + const defaultVariable = this.defaultVariable.getName(); + const output = `${options.varOrConst} ${name}${_}=${_}${defaultVariable}.${this.name};`; + return output; + } +} diff --git a/src/rollup/types.d.ts b/src/rollup/types.d.ts index 6c8397dee79..3e5b7859e5f 100644 --- a/src/rollup/types.d.ts +++ b/src/rollup/types.d.ts @@ -93,6 +93,7 @@ export interface SourceDescription { code: string; map?: SourceMapInput; moduleSideEffects?: boolean | null; + syntheticNamedExports?: boolean; } export interface TransformSourceDescription extends SourceDescription { @@ -109,6 +110,7 @@ export interface TransformModuleJSON { originalSourcemap: ExistingDecodedSourceMap | null; resolvedIds?: ResolvedIdMap; sourcemapChain: DecodedSourceMapOrMissing[]; + syntheticNamedExports: boolean | null; transformDependencies: string[]; } @@ -199,6 +201,7 @@ export interface ResolvedId { external: boolean; id: string; moduleSideEffects: boolean; + syntheticNamedExports: boolean; } export interface ResolvedIdMap { @@ -209,6 +212,7 @@ interface PartialResolvedId { external?: boolean; id: string; moduleSideEffects?: boolean | null; + syntheticNamedExports?: boolean; } export type ResolveIdResult = string | false | null | undefined | PartialResolvedId; diff --git a/src/utils/error.ts b/src/utils/error.ts index 9adad000a88..40a1546516e 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -54,7 +54,8 @@ export enum Errors { PLUGIN_ERROR = 'PLUGIN_ERROR', UNRESOLVED_ENTRY = 'UNRESOLVED_ENTRY', UNRESOLVED_IMPORT = 'UNRESOLVED_IMPORT', - VALIDATION_ERROR = 'VALIDATION_ERROR' + VALIDATION_ERROR = 'VALIDATION_ERROR', + EXTERNAL_SYNTHETIC_EXPORTS = 'EXTERNAL_SYNTHETIC_EXPORTS' } export function errAssetNotFinalisedForFileName(name: string) { @@ -263,6 +264,15 @@ export function errUnresolvedImportTreatedAsExternal(source: string, importer: s }; } +export function errExternalSyntheticExports(source: string, importer: string) { + return { + code: Errors.EXTERNAL_SYNTHETIC_EXPORTS, + importer: relativeId(importer), + message: `External '${source}' can not have syntheticNamedExports`, + source + }; +} + export function errFailedValidation(message: string) { return { code: Errors.VALIDATION_ERROR, diff --git a/src/utils/transform.ts b/src/utils/transform.ts index 3e16df29471..4cb9d437f8f 100644 --- a/src/utils/transform.ts +++ b/src/utils/transform.ts @@ -35,6 +35,7 @@ export default function transform( const emittedFiles: EmittedFile[] = []; let customTransformCache = false; let moduleSideEffects: boolean | null = null; + let syntheticNamedExports: boolean | null = null; let trackedPluginCache: { cache: PluginCache; used: boolean }; let curPlugin: Plugin; const curSource: string = source.code; @@ -82,6 +83,9 @@ export default function transform( if (typeof result.moduleSideEffects === 'boolean') { moduleSideEffects = result.moduleSideEffects; } + if (typeof result.syntheticNamedExports === 'boolean') { + syntheticNamedExports = result.syntheticNamedExports; + } } else { return code; } @@ -193,6 +197,7 @@ export default function transform( originalCode, originalSourcemap, sourcemapChain, + syntheticNamedExports, transformDependencies }; }); diff --git a/test/chunking-form/samples/synthetic-named-exports/_config.js b/test/chunking-form/samples/synthetic-named-exports/_config.js new file mode 100644 index 00000000000..32fd1889909 --- /dev/null +++ b/test/chunking-form/samples/synthetic-named-exports/_config.js @@ -0,0 +1,19 @@ +module.exports = { + description: 'simple chunking', + options: { + input: ['main.js'], + plugins: [ + { + resolveId(id) { + if (id === './dep1.js') { + return { + id, + syntheticNamedExports: true + }; + } + return null; + } + } + ] + } +}; diff --git a/test/chunking-form/samples/synthetic-named-exports/_expected/amd/main.js b/test/chunking-form/samples/synthetic-named-exports/_expected/amd/main.js new file mode 100644 index 00000000000..2daeb70bd6e --- /dev/null +++ b/test/chunking-form/samples/synthetic-named-exports/_expected/amd/main.js @@ -0,0 +1,14 @@ +define(function () { 'use strict'; + + const d = { + fn: 42 + }; + + var fn = d.fn; + + var foo = d.foo; + + console.log(fn); + console.log(foo); + +}); diff --git a/test/chunking-form/samples/synthetic-named-exports/_expected/cjs/main.js b/test/chunking-form/samples/synthetic-named-exports/_expected/cjs/main.js new file mode 100644 index 00000000000..7792b6b1ae7 --- /dev/null +++ b/test/chunking-form/samples/synthetic-named-exports/_expected/cjs/main.js @@ -0,0 +1,12 @@ +'use strict'; + +const d = { + fn: 42 +}; + +var fn = d.fn; + +var foo = d.foo; + +console.log(fn); +console.log(foo); diff --git a/test/chunking-form/samples/synthetic-named-exports/_expected/es/main.js b/test/chunking-form/samples/synthetic-named-exports/_expected/es/main.js new file mode 100644 index 00000000000..5cf1d7a0823 --- /dev/null +++ b/test/chunking-form/samples/synthetic-named-exports/_expected/es/main.js @@ -0,0 +1,10 @@ +const d = { + fn: 42 +}; + +var fn = d.fn; + +var foo = d.foo; + +console.log(fn); +console.log(foo); diff --git a/test/chunking-form/samples/synthetic-named-exports/_expected/system/main.js b/test/chunking-form/samples/synthetic-named-exports/_expected/system/main.js new file mode 100644 index 00000000000..02a86cc820e --- /dev/null +++ b/test/chunking-form/samples/synthetic-named-exports/_expected/system/main.js @@ -0,0 +1,19 @@ +System.register([], function () { + 'use strict'; + return { + execute: function () { + + const d = { + fn: 42 + }; + + var fn = d.fn; + + var foo = d.foo; + + console.log(fn); + console.log(foo); + + } + }; +}); diff --git a/test/chunking-form/samples/synthetic-named-exports/dep1.js b/test/chunking-form/samples/synthetic-named-exports/dep1.js new file mode 100644 index 00000000000..0fce37e6b41 --- /dev/null +++ b/test/chunking-form/samples/synthetic-named-exports/dep1.js @@ -0,0 +1,4 @@ +const d = { + fn: 42 +}; +export default d; \ No newline at end of file diff --git a/test/chunking-form/samples/synthetic-named-exports/main.js b/test/chunking-form/samples/synthetic-named-exports/main.js new file mode 100644 index 00000000000..8a5f83cb1f3 --- /dev/null +++ b/test/chunking-form/samples/synthetic-named-exports/main.js @@ -0,0 +1,4 @@ +import {fn, foo} from './dep1.js'; + +console.log(fn); +console.log(foo); \ No newline at end of file