diff --git a/docs/05-plugin-development.md b/docs/05-plugin-development.md index a65995864de..7b2a6562472 100644 --- a/docs/05-plugin-development.md +++ b/docs/05-plugin-development.md @@ -169,7 +169,7 @@ Phase: `generate` Cf. [`output.intro/output.outro`](guide/en/#outputintrooutputoutro). #### `load` -Type: `(id: string) => string | null | { code: string, map?: string | SourceMap, ast? : ESTree.Program, moduleSideEffects?: boolean | null }`
+Type: `(id: string) => string | null | { code: string, map?: string | SourceMap, ast? : ESTree.Program, moduleSideEffects?: boolean | null, syntheticNamedExports?: boolean | null }`
Kind: `async, first`
Phase: `build` @@ -177,7 +177,25 @@ Defines a custom loader. Returning `null` defers to other `load` functions (and If `false` is returned for `moduleSideEffects` and no other module imports anything from this module, then this module will not be included in the bundle without checking for actual side-effects inside the module. If `true` is returned, Rollup will use its default algorithm to include all statements in the module that have side-effects (such as modifying a global or exported variable). If `null` is returned or the flag is omitted, then `moduleSideEffects` will be determined by the first `resolveId` hook that resolved this module, the `treeshake.moduleSideEffects` option, or eventually default to `true`. The `transform` hook can override this. -You can use [`this.getModuleInfo`](guide/en/#thisgetmoduleinfomoduleid-string--moduleinfo) to find out the previous value of `moduleSideEffects` inside this hook. +If `true` is returned for `syntheticNamedExports`, this module will fallback the resolution of any missing named export to properties of the `default` export. The `transform` hook can override this. This option allows to have dynamic named exports that might not be declared in the module, such as in this example: + +**dep.js: (`{syntheticNamedExports: true}`)** + +``` +export default { + foo: 42, + bar: 'hello' +} +``` + +**main.js: (entry point)** + +```js +import { foo, bar } from './dep.js' +console.log(foo, bar); +``` + +You can use [`this.getModuleInfo`](guide/en/#thisgetmoduleinfomoduleid-string--moduleinfo) to find out the previous values of `moduleSideEffects` and `syntheticNamedExports` inside this hook. #### `options` Type: `(options: InputOptions) => InputOptions | null`
@@ -268,7 +286,7 @@ resolveFileUrl({fileName}) { ``` #### `resolveId` -Type: `(source: string, importer: string) => string | false | null | {id: string, external?: boolean, moduleSideEffects?: boolean | null}`
+Type: `(source: string, importer: string) => string | false | null | {id: string, external?: boolean, moduleSideEffects?: boolean | null, syntheticNamedExports?: boolean | null}`
Kind: `async, first`
Phase: `build` @@ -289,6 +307,24 @@ Relative ids, i.e. starting with `./` or `../`, will **not** be renormalized whe If `false` is returned for `moduleSideEffects` in the first hook that resolves a module id and no other module imports anything from this module, then this module will not be included without checking for actual side-effects inside the module. If `true` is returned, Rollup will use its default algorithm to include all statements in the module that have side-effects (such as modifying a global or exported variable). If `null` is returned or the flag is omitted, then `moduleSideEffects` will be determined by the `treeshake.moduleSideEffects` option or default to `true`. The `load` and `transform` hooks can override this. +If `true` is returned for `syntheticNamedExports`, this module will fallback the resolution of any missing named export to properties of the `default` export. The `load` and `transform` hooks can override this. This option allows to have dynamic named exports that might not be declared in the module, such as in this example: + +**dep.js: (`{syntheticNamedExports: true}`)** + +``` +export default { + foo: 42, + bar: 'hello' +} +``` + +**main.js: (entry point)** + +```js +import { foo, bar } from './dep.js' +console.log(foo, bar); +``` + #### `resolveImportMeta` Type: `(property: string | null, {chunkId: string, moduleId: string, format: string}) => string | null`
Kind: `sync, first`
@@ -313,7 +349,7 @@ resolveImportMeta(property, {moduleId}) { Note that since this hook has access to the filename of the current chunk, its return value will not be considered when generating the hash of this chunk. #### `transform` -Type: `(code: string, id: string) => string | null | { code: string, map?: string | SourceMap, ast? : ESTree.Program, moduleSideEffects?: boolean | null }`
+Type: `(code: string, id: string) => string | null | { code: string, map?: string | SourceMap, ast? : ESTree.Program, moduleSideEffects?: boolean | null, syntheticNamedExports?: boolean | null }`
Kind: `async, sequential`
Phase: `build` @@ -323,6 +359,24 @@ Note that in watch mode, the result of this hook is cached when rebuilding and t If `false` is returned for `moduleSideEffects` and no other module imports anything from this module, then this module will not be included without checking for actual side-effects inside the module. If `true` is returned, Rollup will use its default algorithm to include all statements in the module that have side-effects (such as modifying a global or exported variable). If `null` is returned or the flag is omitted, then `moduleSideEffects` will be determined by the first `resolveId` hook that resolved this module, the `treeshake.moduleSideEffects` option, or eventually default to `true`. +If `true` is returned for `syntheticNamedExports`, this module will fallback the resolution of any missing named export to properties of the `default` export. This option allows to have dynamic named exports that might not be declared in the module, such as in this example: + +**dep.js: (`{syntheticNamedExports: true}`)** + +``` +export default { + foo: 42, + bar: 'hello' +} +``` + +**main.js: (entry point)** + +```js +import { foo, bar } from './dep.js' +console.log(foo, bar); +``` + You can use [`this.getModuleInfo`](guide/en/#thisgetmoduleinfomoduleid-string--moduleinfo) to find out the previous value of `moduleSideEffects` inside this hook. #### `watchChange` diff --git a/src/Module.ts b/src/Module.ts index 790af80d82f..60fb236bed8 100644 --- a/src/Module.ts +++ b/src/Module.ts @@ -22,9 +22,11 @@ import TemplateLiteral from './ast/nodes/TemplateLiteral'; import VariableDeclaration from './ast/nodes/VariableDeclaration'; import ModuleScope from './ast/scopes/ModuleScope'; import { PathTracker, UNKNOWN_PATH } from './ast/utils/PathTracker'; +import ExportDefaultVariable from './ast/variables/ExportDefaultVariable'; import ExportShimVariable from './ast/variables/ExportShimVariable'; import ExternalVariable from './ast/variables/ExternalVariable'; import NamespaceVariable from './ast/variables/NamespaceVariable'; +import SyntheticNamedExportVariable from './ast/variables/SyntheticNamedExportVariable'; import Variable from './ast/variables/Variable'; import Chunk from './Chunk'; import ExternalModule from './ExternalModule'; @@ -39,7 +41,7 @@ import { RollupWarning, TransformModuleJSON } from './rollup/types'; -import { error } from './utils/error'; +import { error, Errors } from './utils/error'; import getCodeFrame from './utils/getCodeFrame'; import { getOriginalLocation } from './utils/getOriginalLocation'; import { makeLegal } from './utils/identifierHelpers'; @@ -206,6 +208,7 @@ export default class Module { scope!: ModuleScope; sourcemapChain!: DecodedSourceMapOrMissing[]; sources = new Set(); + syntheticNamedExports: boolean; transformFiles?: EmittedFile[]; userChunkNames = new Set(); usesTopLevelAwait = false; @@ -214,19 +217,28 @@ export default class Module { private ast!: Program; private astContext!: AstContext; private context: string; + private defaultExport: ExportDefaultVariable | null | undefined = null; private esTreeAst!: ESTree.Program; private graph: Graph; private magicString!: MagicString; private namespaceVariable: NamespaceVariable | null = null; + private syntheticExports = new Map(); 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; } @@ -299,6 +311,21 @@ export default class Module { return allExportNames; } + getDefaultExport() { + if (this.defaultExport === null) { + this.defaultExport = undefined; + this.defaultExport = this.getVariableForExportName('default') as ExportDefaultVariable; + } + if (!this.defaultExport) { + return error({ + code: Errors.SYNTHETIC_NAMED_EXPORTS_NEED_DEFAULT, + id: this.id, + message: `Modules with 'syntheticNamedExports' need a default export.` + }); + } + return this.defaultExport; + } + getDynamicImportExpressions(): (string | Node)[] { return this.dynamicImports.map(({ node }) => { const importArgument = node.source; @@ -342,7 +369,7 @@ export default class Module { getOrCreateNamespace(): NamespaceVariable { if (!this.namespaceVariable) { - this.namespaceVariable = new NamespaceVariable(this.astContext); + this.namespaceVariable = new NamespaceVariable(this.astContext, this.syntheticNamedExports); this.namespaceVariable.initialise(); } return this.namespaceVariable; @@ -439,9 +466,22 @@ 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) { + const defaultExport = this.getDefaultExport(); + syntheticExport = new SyntheticNamedExportVariable(this.astContext, name, defaultExport); + this.syntheticExports.set(name, syntheticExport); + return syntheticExport; + } + return syntheticExport; + } + + if (this.graph.shimMissingExports) { + this.shimMissingExport(name); + return this.exportShimVariable; + } } return undefined as any; } @@ -534,6 +574,7 @@ export default class Module { originalSourcemap, resolvedIds, sourcemapChain, + syntheticNamedExports, transformDependencies, transformFiles }: TransformModuleJSON & { @@ -551,6 +592,9 @@ export default class Module { if (typeof moduleSideEffects === 'boolean') { this.moduleSideEffects = moduleSideEffects; } + if (typeof syntheticNamedExports === 'boolean') { + this.syntheticNamedExports = syntheticNamedExports; + } timeStart('generate ast', 3); @@ -633,6 +677,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 faafbc9ac50..56e65b0a731 100644 --- a/src/ModuleLoader.ts +++ b/src/ModuleLoader.ts @@ -17,6 +17,7 @@ import { errBadLoader, errCannotAssignModuleToChunk, errEntryCannotBeExternal, + errExternalSyntheticExports, errInternalIdCannotBeExternal, errInvalidOption, errNamespaceConflict, @@ -243,7 +244,7 @@ export class ModuleLoader { module.id, (module.resolvedIds[source] = module.resolvedIds[source] || - this.handleMissingImports(await this.resolveId(source, module.id), source, module.id)) + this.handleResolveId(await this.resolveId(source, module.id), source, module.id)) ) ) as Promise[]), ...module.getDynamicImportExpressions().map((specifier, index) => @@ -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,11 +381,17 @@ 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 + ); } } - private handleMissingImports( + private handleResolveId( resolvedId: ResolvedId | null, source: string, importer: string @@ -387,8 +404,13 @@ export class ModuleLoader { return { external: true, id: source, - moduleSideEffects: this.hasModuleSideEffects(source, true) + moduleSideEffects: this.hasModuleSideEffects(source, true), + syntheticNamedExports: false }; + } else { + if (resolvedId.external && resolvedId.syntheticNamedExports) { + this.graph.warn(errExternalSyntheticExports(source, importer)); + } } return resolvedId; } @@ -407,7 +429,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 +442,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 +452,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; @@ -448,7 +474,8 @@ export class ModuleLoader { moduleSideEffects: typeof moduleSideEffects === 'boolean' ? moduleSideEffects - : this.hasModuleSideEffects(id, external) + : this.hasModuleSideEffects(id, external), + syntheticNamedExports }; } @@ -478,13 +505,9 @@ export class ModuleLoader { if (resolution == null) { return (module.resolvedIds[specifier] = module.resolvedIds[specifier] || - this.handleMissingImports( - await this.resolveId(specifier, module.id), - specifier, - module.id - )); + this.handleResolveId(await this.resolveId(specifier, module.id), specifier, module.id)); } - return this.handleMissingImports( + return this.handleResolveId( this.normalizeResolveIdResult(resolution, importer, specifier), specifier, importer diff --git a/src/ast/variables/NamespaceVariable.ts b/src/ast/variables/NamespaceVariable.ts index b72eee28c16..46b0f49138f 100644 --- a/src/ast/variables/NamespaceVariable.ts +++ b/src/ast/variables/NamespaceVariable.ts @@ -15,11 +15,13 @@ export default class NamespaceVariable extends Variable { private containsExternalNamespace = false; private referencedEarly = false; private references: Identifier[] = []; + private syntheticNamedExports: boolean; - constructor(context: AstContext) { + constructor(context: AstContext, syntheticNamedExports: boolean) { super(context.getModuleName()); this.context = context; this.module = context.module; + this.syntheticNamedExports = syntheticNamedExports; } addReference(identifier: Identifier) { @@ -99,10 +101,15 @@ export default class NamespaceVariable extends Variable { } const name = this.getName(); + let output = `{${n}${members.join(`,${n}`)}${n}}`; + if (this.syntheticNamedExports) { + output = `/*#__PURE__*/Object.assign(${output}, ${this.module.getDefaultExport().getName()})`; + } + if (options.freeze) { + output = `/*#__PURE__*/Object.freeze(${output})`; + } - const callee = options.freeze ? `/*#__PURE__*/Object.freeze` : ''; - const membersStr = members.join(`,${n}`); - let output = `${options.varOrConst} ${name}${_}=${_}${callee}({${n}${membersStr}${n}});`; + output = `${options.varOrConst} ${name}${_}=${_}${output};`; if (options.format === 'system' && this.exportName) { output += `${n}exports('${this.exportName}',${_}${name});`; diff --git a/src/ast/variables/SyntheticNamedExportVariable.ts b/src/ast/variables/SyntheticNamedExportVariable.ts new file mode 100644 index 00000000000..da07fd189d1 --- /dev/null +++ b/src/ast/variables/SyntheticNamedExportVariable.ts @@ -0,0 +1,25 @@ +import Module, { AstContext } from '../../Module'; +import { InclusionContext } from '../ExecutionContext'; +import ExportDefaultVariable from './ExportDefaultVariable'; +import Variable from './Variable'; + +export default class SyntheticNamedExportVariableVariable 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; + this.setRenderNames(defaultVariable.getName(), name); + } + + include(context: InclusionContext) { + if (!this.included) { + this.included = true; + this.context.includeVariable(context, this.defaultVariable); + } + } +} diff --git a/src/ast/variables/Variable.ts b/src/ast/variables/Variable.ts index 323059f48f3..187eb553e82 100644 --- a/src/ast/variables/Variable.ts +++ b/src/ast/variables/Variable.ts @@ -49,7 +49,7 @@ export default class Variable implements ExpressionEntity { getName(): string { const name = this.renderName || this.name; - return this.renderBaseName ? `${this.renderBaseName}.${name}` : name; + return this.renderBaseName ? `${this.renderBaseName}${getPropertyAccess(name)}` : name; } getReturnExpressionWhenCalledAtPath( @@ -107,3 +107,7 @@ export default class Variable implements ExpressionEntity { return this.name; } } + +const getPropertyAccess = (name: string) => { + return /^(?!\d)[\w$]+$/.test(name) ? `.${name}` : `[${JSON.stringify(name)}]`; +}; 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..3bacdd45afb 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -54,7 +54,9 @@ 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', + SYNTHETIC_NAMED_EXPORTS_NEED_DEFAULT = 'SYNTHETIC_NAMED_EXPORTS_NEED_DEFAULT' } export function errAssetNotFinalisedForFileName(name: string) { @@ -263,6 +265,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' enabled.`, + source + }; +} + export function errFailedValidation(message: string) { return { code: Errors.VALIDATION_ERROR, diff --git a/src/utils/transform.ts b/src/utils/transform.ts index 9c83aa32f45..4f27bfcd1ad 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/mixed-synthetic-named-exports-2/_config.js b/test/chunking-form/samples/mixed-synthetic-named-exports-2/_config.js new file mode 100644 index 00000000000..2bc10bd7bed --- /dev/null +++ b/test/chunking-form/samples/mixed-synthetic-named-exports-2/_config.js @@ -0,0 +1,20 @@ +module.exports = { + description: 'mixed synthetic named exports', + options: { + input: ['main.js'], + plugins: [ + { + transform(code, id) { + console.log(id); + if (id.endsWith('/dep1.js') || id.endsWith('/dep2.js')) { + return { + code, + syntheticNamedExports: true + }; + } + return null; + } + } + ] + } +}; diff --git a/test/chunking-form/samples/mixed-synthetic-named-exports-2/_expected/amd/main.js b/test/chunking-form/samples/mixed-synthetic-named-exports-2/_expected/amd/main.js new file mode 100644 index 00000000000..2d5a6a148b1 --- /dev/null +++ b/test/chunking-form/samples/mixed-synthetic-named-exports-2/_expected/amd/main.js @@ -0,0 +1,7 @@ +define(function () { 'use strict'; + + var dep2 = {bar: {foo: 'works'}}; + + console.log(dep2.bar.foo); + +}); diff --git a/test/chunking-form/samples/mixed-synthetic-named-exports-2/_expected/cjs/main.js b/test/chunking-form/samples/mixed-synthetic-named-exports-2/_expected/cjs/main.js new file mode 100644 index 00000000000..3a897a26f6d --- /dev/null +++ b/test/chunking-form/samples/mixed-synthetic-named-exports-2/_expected/cjs/main.js @@ -0,0 +1,5 @@ +'use strict'; + +var dep2 = {bar: {foo: 'works'}}; + +console.log(dep2.bar.foo); diff --git a/test/chunking-form/samples/mixed-synthetic-named-exports-2/_expected/es/main.js b/test/chunking-form/samples/mixed-synthetic-named-exports-2/_expected/es/main.js new file mode 100644 index 00000000000..f84e5168475 --- /dev/null +++ b/test/chunking-form/samples/mixed-synthetic-named-exports-2/_expected/es/main.js @@ -0,0 +1,3 @@ +var dep2 = {bar: {foo: 'works'}}; + +console.log(dep2.bar.foo); diff --git a/test/chunking-form/samples/mixed-synthetic-named-exports-2/_expected/system/main.js b/test/chunking-form/samples/mixed-synthetic-named-exports-2/_expected/system/main.js new file mode 100644 index 00000000000..0efe9ac0943 --- /dev/null +++ b/test/chunking-form/samples/mixed-synthetic-named-exports-2/_expected/system/main.js @@ -0,0 +1,12 @@ +System.register([], function () { + 'use strict'; + return { + execute: function () { + + var dep2 = {bar: {foo: 'works'}}; + + console.log(dep2.bar.foo); + + } + }; +}); diff --git a/test/chunking-form/samples/mixed-synthetic-named-exports-2/dep1.js b/test/chunking-form/samples/mixed-synthetic-named-exports-2/dep1.js new file mode 100644 index 00000000000..4561eb9a005 --- /dev/null +++ b/test/chunking-form/samples/mixed-synthetic-named-exports-2/dep1.js @@ -0,0 +1 @@ +export {bar as default} from './dep2.js'; \ No newline at end of file diff --git a/test/chunking-form/samples/mixed-synthetic-named-exports-2/dep2.js b/test/chunking-form/samples/mixed-synthetic-named-exports-2/dep2.js new file mode 100644 index 00000000000..8f16601da67 --- /dev/null +++ b/test/chunking-form/samples/mixed-synthetic-named-exports-2/dep2.js @@ -0,0 +1 @@ +export default {bar: {foo: 'works'}}; \ No newline at end of file diff --git a/test/chunking-form/samples/mixed-synthetic-named-exports-2/main.js b/test/chunking-form/samples/mixed-synthetic-named-exports-2/main.js new file mode 100644 index 00000000000..5a747a56c3b --- /dev/null +++ b/test/chunking-form/samples/mixed-synthetic-named-exports-2/main.js @@ -0,0 +1,2 @@ +import { foo } from './dep1.js'; +console.log(foo); \ No newline at end of file diff --git a/test/chunking-form/samples/mixed-synthetic-named-exports/_config.js b/test/chunking-form/samples/mixed-synthetic-named-exports/_config.js new file mode 100644 index 00000000000..eb273960237 --- /dev/null +++ b/test/chunking-form/samples/mixed-synthetic-named-exports/_config.js @@ -0,0 +1,19 @@ +module.exports = { + description: 'mixed synthetic named exports', + options: { + input: ['main.js'], + plugins: [ + { + resolveId(id) { + if (id === './dep1.js') { + return { + id, + syntheticNamedExports: true + }; + } + return null; + } + } + ] + } +}; diff --git a/test/chunking-form/samples/mixed-synthetic-named-exports/_expected/amd/main.js b/test/chunking-form/samples/mixed-synthetic-named-exports/_expected/amd/main.js new file mode 100644 index 00000000000..6285d62559e --- /dev/null +++ b/test/chunking-form/samples/mixed-synthetic-named-exports/_expected/amd/main.js @@ -0,0 +1,19 @@ +define(function () { 'use strict'; + + const d = { + fn: 42, + hello: 'hola' + }; + const foo = 100; + + var ns = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.assign({ + __proto__: null, + foo: foo, + 'default': d + }, d)); + + console.log(d.fn); + console.log(foo); + console.log(ns); + +}); diff --git a/test/chunking-form/samples/mixed-synthetic-named-exports/_expected/cjs/main.js b/test/chunking-form/samples/mixed-synthetic-named-exports/_expected/cjs/main.js new file mode 100644 index 00000000000..f2c1b375abb --- /dev/null +++ b/test/chunking-form/samples/mixed-synthetic-named-exports/_expected/cjs/main.js @@ -0,0 +1,17 @@ +'use strict'; + +const d = { + fn: 42, + hello: 'hola' +}; +const foo = 100; + +var ns = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.assign({ + __proto__: null, + foo: foo, + 'default': d +}, d)); + +console.log(d.fn); +console.log(foo); +console.log(ns); diff --git a/test/chunking-form/samples/mixed-synthetic-named-exports/_expected/es/main.js b/test/chunking-form/samples/mixed-synthetic-named-exports/_expected/es/main.js new file mode 100644 index 00000000000..3b23f6d33bb --- /dev/null +++ b/test/chunking-form/samples/mixed-synthetic-named-exports/_expected/es/main.js @@ -0,0 +1,15 @@ +const d = { + fn: 42, + hello: 'hola' +}; +const foo = 100; + +var ns = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.assign({ + __proto__: null, + foo: foo, + 'default': d +}, d)); + +console.log(d.fn); +console.log(foo); +console.log(ns); diff --git a/test/chunking-form/samples/mixed-synthetic-named-exports/_expected/system/main.js b/test/chunking-form/samples/mixed-synthetic-named-exports/_expected/system/main.js new file mode 100644 index 00000000000..1147e48c999 --- /dev/null +++ b/test/chunking-form/samples/mixed-synthetic-named-exports/_expected/system/main.js @@ -0,0 +1,24 @@ +System.register([], function () { + 'use strict'; + return { + execute: function () { + + const d = { + fn: 42, + hello: 'hola' + }; + const foo = 100; + + var ns = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.assign({ + __proto__: null, + foo: foo, + 'default': d + }, d)); + + console.log(d.fn); + console.log(foo); + console.log(ns); + + } + }; +}); diff --git a/test/chunking-form/samples/mixed-synthetic-named-exports/dep1.js b/test/chunking-form/samples/mixed-synthetic-named-exports/dep1.js new file mode 100644 index 00000000000..2f0388e7a09 --- /dev/null +++ b/test/chunking-form/samples/mixed-synthetic-named-exports/dep1.js @@ -0,0 +1,6 @@ +const d = { + fn: 42, + hello: 'hola' +}; +export const foo = 100; +export default d; \ No newline at end of file diff --git a/test/chunking-form/samples/mixed-synthetic-named-exports/main.js b/test/chunking-form/samples/mixed-synthetic-named-exports/main.js new file mode 100644 index 00000000000..9f9f926d5ad --- /dev/null +++ b/test/chunking-form/samples/mixed-synthetic-named-exports/main.js @@ -0,0 +1,6 @@ +import {fn, foo} from './dep1.js'; +import * as ns from './dep1.js'; + +console.log(fn); +console.log(foo); +console.log(ns); \ No newline at end of file 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..9327ae3aa2c --- /dev/null +++ b/test/chunking-form/samples/synthetic-named-exports/_config.js @@ -0,0 +1,30 @@ +module.exports = { + description: 'synthetic named exports', + options: { + input: ['main.js'], + plugins: [ + { + resolveId(id) { + if (id === './dep1.js') { + return id; + } + return null; + }, + load(id) { + if (id === './dep1.js') { + return { + code: ` +const d = { + fn: 42, + hello: 'hola' +}; +export default d;`, + 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..569d66e02bb --- /dev/null +++ b/test/chunking-form/samples/synthetic-named-exports/_expected/amd/main.js @@ -0,0 +1,12 @@ +define(function () { 'use strict'; + + const d = { + fn: 42, + hello: 'hola' + }; + + console.log(d.fn); + console.log(d.foo); + console.log(d["some-prop"]); + +}); 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..cd2f4f43d7b --- /dev/null +++ b/test/chunking-form/samples/synthetic-named-exports/_expected/cjs/main.js @@ -0,0 +1,10 @@ +'use strict'; + +const d = { + fn: 42, + hello: 'hola' +}; + +console.log(d.fn); +console.log(d.foo); +console.log(d["some-prop"]); 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..ecd2e3e91a7 --- /dev/null +++ b/test/chunking-form/samples/synthetic-named-exports/_expected/es/main.js @@ -0,0 +1,8 @@ +const d = { + fn: 42, + hello: 'hola' +}; + +console.log(d.fn); +console.log(d.foo); +console.log(d["some-prop"]); 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..6b161eef8ae --- /dev/null +++ b/test/chunking-form/samples/synthetic-named-exports/_expected/system/main.js @@ -0,0 +1,17 @@ +System.register([], function () { + 'use strict'; + return { + execute: function () { + + const d = { + fn: 42, + hello: 'hola' + }; + + console.log(d.fn); + console.log(d.foo); + console.log(d["some-prop"]); + + } + }; +}); 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..5b675b95c0f --- /dev/null +++ b/test/chunking-form/samples/synthetic-named-exports/main.js @@ -0,0 +1,6 @@ +import {fn, foo} from './dep1.js'; +import * as ns from './dep1.js'; + +console.log(fn); +console.log(foo); +console.log(ns['some-prop']); \ No newline at end of file diff --git a/test/form/samples/freeze/_expected/amd.js b/test/form/samples/freeze/_expected/amd.js index 60c6a2683a6..6d5f5acc935 100644 --- a/test/form/samples/freeze/_expected/amd.js +++ b/test/form/samples/freeze/_expected/amd.js @@ -3,11 +3,11 @@ define(['exports'], function (exports) { 'use strict'; const foo = 1; const bar = 2; - var namespace = ({ + var namespace = { __proto__: null, foo: foo, bar: bar - }); + }; console.log( Object.keys( namespace ) ); diff --git a/test/form/samples/freeze/_expected/cjs.js b/test/form/samples/freeze/_expected/cjs.js index c010fbc614a..2ee4ef34da1 100644 --- a/test/form/samples/freeze/_expected/cjs.js +++ b/test/form/samples/freeze/_expected/cjs.js @@ -5,11 +5,11 @@ Object.defineProperty(exports, '__esModule', { value: true }); const foo = 1; const bar = 2; -var namespace = ({ +var namespace = { __proto__: null, foo: foo, bar: bar -}); +}; console.log( Object.keys( namespace ) ); diff --git a/test/form/samples/freeze/_expected/es.js b/test/form/samples/freeze/_expected/es.js index 45de6b68c63..1285f913af8 100644 --- a/test/form/samples/freeze/_expected/es.js +++ b/test/form/samples/freeze/_expected/es.js @@ -1,11 +1,11 @@ const foo = 1; const bar = 2; -var namespace = ({ +var namespace = { __proto__: null, foo: foo, bar: bar -}); +}; console.log( Object.keys( namespace ) ); diff --git a/test/form/samples/freeze/_expected/iife.js b/test/form/samples/freeze/_expected/iife.js index 1dc95266499..f42a252dfcb 100644 --- a/test/form/samples/freeze/_expected/iife.js +++ b/test/form/samples/freeze/_expected/iife.js @@ -4,11 +4,11 @@ var myBundle = (function (exports) { const foo = 1; const bar = 2; - var namespace = ({ + var namespace = { __proto__: null, foo: foo, bar: bar - }); + }; console.log( Object.keys( namespace ) ); diff --git a/test/form/samples/freeze/_expected/system.js b/test/form/samples/freeze/_expected/system.js index c331d83ceac..61af5d66116 100644 --- a/test/form/samples/freeze/_expected/system.js +++ b/test/form/samples/freeze/_expected/system.js @@ -6,11 +6,11 @@ System.register('myBundle', [], function (exports) { const foo = 1; const bar = 2; - var namespace = ({ + var namespace = { __proto__: null, foo: foo, bar: bar - }); + }; console.log( Object.keys( namespace ) ); diff --git a/test/form/samples/freeze/_expected/umd.js b/test/form/samples/freeze/_expected/umd.js index 10fb997b6a2..1d2f030b1f6 100644 --- a/test/form/samples/freeze/_expected/umd.js +++ b/test/form/samples/freeze/_expected/umd.js @@ -7,11 +7,11 @@ const foo = 1; const bar = 2; - var namespace = ({ + var namespace = { __proto__: null, foo: foo, bar: bar - }); + }; console.log( Object.keys( namespace ) ); diff --git a/test/function/samples/context-resolve/_config.js b/test/function/samples/context-resolve/_config.js index 88aed16c12a..c269e5be58d 100644 --- a/test/function/samples/context-resolve/_config.js +++ b/test/function/samples/context-resolve/_config.js @@ -7,7 +7,8 @@ const tests = [ expected: { id: path.resolve(__dirname, 'existing.js'), external: false, - moduleSideEffects: true + moduleSideEffects: true, + syntheticNamedExports: false } }, { @@ -23,7 +24,8 @@ const tests = [ expected: { id: path.resolve(__dirname, 'marked-directly-external-relative'), external: true, - moduleSideEffects: true + moduleSideEffects: true, + syntheticNamedExports: false } }, { @@ -31,36 +33,63 @@ const tests = [ expected: { id: path.resolve(__dirname, 'marked-external-relative'), external: true, - moduleSideEffects: true + moduleSideEffects: true, + syntheticNamedExports: false } }, { source: 'marked-external-absolute', - expected: { id: 'marked-external-absolute', external: true, moduleSideEffects: true } + expected: { + id: 'marked-external-absolute', + external: true, + moduleSideEffects: true, + syntheticNamedExports: false + } }, { source: 'resolved-name', - expected: { id: 'resolved:resolved-name', external: false, moduleSideEffects: true } + expected: { + id: 'resolved:resolved-name', + external: false, + moduleSideEffects: true, + syntheticNamedExports: false + } }, { source: 'resolved-false', - expected: { id: 'resolved-false', external: true, moduleSideEffects: true } + expected: { + id: 'resolved-false', + external: true, + moduleSideEffects: true, + syntheticNamedExports: false + } }, { source: 'resolved-object', - expected: { id: 'resolved:resolved-object', external: false, moduleSideEffects: true } + expected: { + id: 'resolved:resolved-object', + external: false, + moduleSideEffects: true, + syntheticNamedExports: false + } }, { source: 'resolved-object-non-external', expected: { id: 'resolved:resolved-object-non-external', external: false, - moduleSideEffects: true + moduleSideEffects: true, + syntheticNamedExports: false } }, { source: 'resolved-object-external', - expected: { id: 'resolved:resolved-object-external', external: true, moduleSideEffects: true } + expected: { + id: 'resolved:resolved-object-external', + external: true, + moduleSideEffects: true, + syntheticNamedExports: false + } } ]; diff --git a/test/function/samples/external-synthetic-exports/_config.js b/test/function/samples/external-synthetic-exports/_config.js new file mode 100644 index 00000000000..bd60de83d00 --- /dev/null +++ b/test/function/samples/external-synthetic-exports/_config.js @@ -0,0 +1,31 @@ +module.exports = { + description: 'external modules can not have syntheticNamedExports', + options: { + plugins: [ + { + resolveId(id) { + if (id === 'dep') { + return { + id, + external: true, + syntheticNamedExports: true + }; + } + } + } + ] + }, + warnings: [ + { + code: 'EXTERNAL_SYNTHETIC_EXPORTS', + importer: 'main.js', + source: 'dep', + message: "External 'dep' can not have 'syntheticNamedExports' enabled." + } + ], + context: { + require() { + return 1; + } + } +}; diff --git a/test/function/samples/external-synthetic-exports/main.js b/test/function/samples/external-synthetic-exports/main.js new file mode 100644 index 00000000000..7ff5eca2612 --- /dev/null +++ b/test/function/samples/external-synthetic-exports/main.js @@ -0,0 +1,2 @@ +import { foo } from 'dep'; +foo; diff --git a/test/function/samples/synthetic-exports-need-default/_config.js b/test/function/samples/synthetic-exports-need-default/_config.js new file mode 100644 index 00000000000..7e77cd9721d --- /dev/null +++ b/test/function/samples/synthetic-exports-need-default/_config.js @@ -0,0 +1,25 @@ +const path = require('path'); + +module.exports = { + description: 'synthetic named exports moduleds need a default export', + options: { + plugins: [ + { + resolveId(id) { + if (id === './dep.js') { + return { + id, + syntheticNamedExports: true + }; + } + } + } + ] + }, + error: { + code: 'SYNTHETIC_NAMED_EXPORTS_NEED_DEFAULT', + id: './dep.js', + message: "Modules with 'syntheticNamedExports' need a default export.", + watchFiles: [path.resolve(__dirname, 'main.js'), './dep.js'] + } +}; diff --git a/test/function/samples/synthetic-exports-need-default/dep.js b/test/function/samples/synthetic-exports-need-default/dep.js new file mode 100644 index 00000000000..6a8018af412 --- /dev/null +++ b/test/function/samples/synthetic-exports-need-default/dep.js @@ -0,0 +1 @@ +export const foo = 1; \ No newline at end of file diff --git a/test/function/samples/synthetic-exports-need-default/main.js b/test/function/samples/synthetic-exports-need-default/main.js new file mode 100644 index 00000000000..a5e73b0d555 --- /dev/null +++ b/test/function/samples/synthetic-exports-need-default/main.js @@ -0,0 +1,3 @@ +import { a } from './dep.js'; +a; +console.log(a); diff --git a/test/incremental/index.js b/test/incremental/index.js index 147ba0789de..28041c41562 100644 --- a/test/incremental/index.js +++ b/test/incremental/index.js @@ -265,8 +265,18 @@ describe('incremental', () => { assert.equal(bundle.cache.modules[1].id, 'entry'); assert.deepEqual(bundle.cache.modules[1].resolvedIds, { - foo: { id: 'foo', external: false, moduleSideEffects: true }, - external: { id: 'external', external: true, moduleSideEffects: true } + foo: { + id: 'foo', + external: false, + moduleSideEffects: true, + syntheticNamedExports: false + }, + external: { + id: 'external', + external: true, + moduleSideEffects: true, + syntheticNamedExports: false + } }); }); });