diff --git a/cli/cli.ts b/cli/cli.ts index 1243fc45a51..43453e7cc2f 100644 --- a/cli/cli.ts +++ b/cli/cli.ts @@ -1,7 +1,7 @@ import help from 'help.md'; import { version } from 'package.json'; import argParser from 'yargs-parser'; -import { commandAliases } from '../src/utils/mergeOptions'; +import { commandAliases } from '../src/utils/options/mergeOptions'; import run from './run/index'; const command = argParser(process.argv.slice(2), { diff --git a/cli/help.md b/cli/help.md index fe5a902d835..49cee04a0e8 100644 --- a/cli/help.md +++ b/cli/help.md @@ -56,10 +56,15 @@ Basic options: --strictDeprecations Throw errors for deprecated features --no-treeshake Disable tree-shaking optimisations --no-treeshake.annotations Ignore pure call annotations ---no-treeshake.no-moduleSideEffects Assume modules have no side-effects ---no-treeshake.no-propertyReadSideEffects Ignore property access side-effects ---no-treeshake.no-tryCatchDeoptimization Do not turn off try-catch-tree-shaking ---no-treeshake.no-unknownGlobalSideEffects Assume unknown globals do not throw +--no-treeshake.moduleSideEffects Assume modules have no side-effects +--no-treeshake.propertyReadSideEffects Ignore property access side-effects +--no-treeshake.tryCatchDeoptimization Do not turn off try-catch-tree-shaking +--no-treeshake.unknownGlobalSideEffects Assume unknown globals do not throw +--watch.buildDelay Throttle watch rebuilds +--no-watch.clearScreen Do not clear the screen when rebuilding +--watch.skipWrite Do not write files to disk when watching +--watch.exclude Exclude files from being watched +--watch.include Limit watching to specified files Examples: diff --git a/cli/run/batchWarnings.ts b/cli/run/batchWarnings.ts index 131b6fdf07f..f9730548a1b 100644 --- a/cli/run/batchWarnings.ts +++ b/cli/run/batchWarnings.ts @@ -80,7 +80,7 @@ const immediateHandlers: { .map((name: string) => `'${name}'`) .join(', ')} and '${warning.modules!.slice(-1)}'`; stderr( - `Creating a browser bundle that depends on ${detail}. You might need to include https://www.npmjs.com/package/rollup-plugin-node-builtins` + `Creating a browser bundle that depends on ${detail}. You might need to include https://github.com/ionic-team/rollup-plugin-node-polyfills` ); } }; diff --git a/cli/run/loadConfigFile.ts b/cli/run/loadConfigFile.ts index 524edfaf56e..7049b9ba2a3 100644 --- a/cli/run/loadConfigFile.ts +++ b/cli/run/loadConfigFile.ts @@ -4,8 +4,8 @@ import { pathToFileURL } from 'url'; import * as rollup from '../../src/node-entry'; import { MergedRollupOptions } from '../../src/rollup/types'; import { error } from '../../src/utils/error'; -import { mergeOptions } from '../../src/utils/mergeOptions'; -import { GenericConfigObject } from '../../src/utils/parseOptions'; +import { mergeOptions } from '../../src/utils/options/mergeOptions'; +import { GenericConfigObject } from '../../src/utils/options/options'; import relativeId from '../../src/utils/relativeId'; import { stderr } from '../logging'; import batchWarnings, { BatchWarnings } from './batchWarnings'; diff --git a/cli/run/loadConfigFromCommand.ts b/cli/run/loadConfigFromCommand.ts index 9320d0f0aa7..424a484a254 100644 --- a/cli/run/loadConfigFromCommand.ts +++ b/cli/run/loadConfigFromCommand.ts @@ -1,5 +1,5 @@ import { MergedRollupOptions } from '../../src/rollup/types'; -import { mergeOptions } from '../../src/utils/mergeOptions'; +import { mergeOptions } from '../../src/utils/options/mergeOptions'; import batchWarnings, { BatchWarnings } from './batchWarnings'; import { addCommandPluginsToInputOptions } from './commandPlugins'; import { stdinName } from './stdin'; diff --git a/docs/01-command-line-reference.md b/docs/01-command-line-reference.md index eb3f8327220..d0c8ee205fb 100755 --- a/docs/01-command-line-reference.md +++ b/docs/01-command-line-reference.md @@ -312,10 +312,15 @@ Many options have command line equivalents. In those cases, any arguments passed --strictDeprecations Throw errors for deprecated features --no-treeshake Disable tree-shaking optimisations --no-treeshake.annotations Ignore pure call annotations ---no-treeshake.no-moduleSideEffects Assume modules have no side-effects ---no-treeshake.no-propertyReadSideEffects Ignore property access side-effects ---no-treeshake.no-tryCatchDeoptimization Do not turn off try-catch-tree-shaking ---no-treeshake.no-unknownGlobalSideEffects Assume unknown globals do not throw +--no-treeshake.moduleSideEffects Assume modules have no side-effects +--no-treeshake.propertyReadSideEffects Ignore property access side-effects +--no-treeshake.tryCatchDeoptimization Do not turn off try-catch-tree-shaking +--no-treeshake.unknownGlobalSideEffects Assume unknown globals do not throw +--watch.buildDelay Throttle watch rebuilds +--no-watch.clearScreen Do not clear the screen when rebuilding +--watch.skipWrite Do not write files to disk when watching +--watch.exclude Exclude files from being watched +--watch.include Limit watching to specified files ``` The flags listed below are only available via the command line interface. All other flags correspond to and override their config file equivalents, see the [big list of options](guide/en/#big-list-of-options) for details. diff --git a/docs/05-plugin-development.md b/docs/05-plugin-development.md index a9a035ae13d..7714d97253f 100644 --- a/docs/05-plugin-development.md +++ b/docs/05-plugin-development.md @@ -90,7 +90,7 @@ Kind: `async, parallel`
Previous Hook: [`options`](guide/en/#options)
Next Hook: [`resolveId`](guide/en/#resolveid) to resolve each entry point in parallel. -Called on each `rollup.rollup` build. This is the recommended hook to use when you need access to the options passed to `rollup.rollup()` as it will take the transformations by all [`options`](guide/en/#options) hooks into account. +Called on each `rollup.rollup` build. This is the recommended hook to use when you need access to the options passed to `rollup.rollup()` as it takes the transformations by all [`options`](guide/en/#options) hooks into account and also contains the right default values for unset options. #### `load` Type: `(id: string) => string | null | { code: string, map?: string | SourceMap, ast? : ESTree.Program, moduleSideEffects?: boolean | null, syntheticNamedExports?: boolean | null }`
@@ -409,7 +409,7 @@ Kind: `async, parallel`
Previous Hook: [`outputOptions`](guide/en/#outputoptions)
Next Hook: [`banner`](guide/en/#banner), [`footer`](guide/en/#footer), [`intro`](guide/en/#intro) and [`outro`](guide/en/#outro) run in parallel. -Called initially each time `bundle.generate()` or `bundle.write()` is called. To get notified when generation has completed, use the `generateBundle` and `renderError` hooks. This is the recommended hook to use when you need access to the output options passed to `bundle.generate()` or `bundle.write()` as it will take the transformations by all [`outputOptions`](guide/en/#outputoptions) hooks into account. It also receives the input options passed to `rollup.rollup()` so that plugins that can be used as output plugins, i.e. plugins that only use `generate` phase hooks, can get access to them. +Called initially each time `bundle.generate()` or `bundle.write()` is called. To get notified when generation has completed, use the `generateBundle` and `renderError` hooks. This is the recommended hook to use when you need access to the output options passed to `bundle.generate()` or `bundle.write()` as it takes the transformations by all [`outputOptions`](guide/en/#outputoptions) hooks into account and also contains the right default values for unset options. It also receives the input options passed to `rollup.rollup()` so that plugins that can be used as output plugins, i.e. plugins that only use `generate` phase hooks, can get access to them. #### `resolveFileUrl` Type: `({chunkId: string, fileName: string, format: string, moduleId: string, referenceId: string, relativePath: string}) => string | null`
diff --git a/docs/999-big-list-of-options.md b/docs/999-big-list-of-options.md index 5178fed59d5..0b1bf1677a7 100755 --- a/docs/999-big-list-of-options.md +++ b/docs/999-big-list-of-options.md @@ -1108,7 +1108,7 @@ class Impure { **treeshake.moduleSideEffects**
Type: `boolean | "no-external" | string[] | (id: string, external: boolean) => boolean`
-CLI: `--treeshake.moduleSideEffects`/`--no-treeshake.moduleSideEffects`
+CLI: `--treeshake.moduleSideEffects`/`--no-treeshake.moduleSideEffects`/`--treeshake.moduleSideEffects no-external`
Default: `true` If `false`, assume modules and external dependencies from which nothing is imported do not have other side-effects like mutating global variables or logging without checking. For external dependencies, this will suppress empty imports: @@ -1276,6 +1276,7 @@ These options only take effect when running Rollup with the `--watch` flag, or u #### watch.buildDelay Type: `number`
+CLI: `--watch.buildDelay `
Default: `0` Configures how long Rollup will wait for further changes until it triggers a rebuild in milliseconds. By default, Rollup does not wait but there is a small debounce timeout configured in the chokidar instance. Setting this to a value greater than `0` will mean that Rollup will only triger a rebuild if there was no change for the configured number of milliseconds. If several configurations are watched, Rollup will use the largest configured build delay. @@ -1287,18 +1288,21 @@ An optional object of watch options that will be passed to the bundled [chokidar #### watch.clearScreen Type: `boolean`
+CLI: `--watch.clearScreen`/`--no-watch.clearScreen`
Default: `true` Whether to clear the screen when a rebuild is triggered. #### watch.skipWrite Type: `boolean`
+CLI: `--watch.skipWrite`/`--no-watch.skipWrite`
Default: `false` Whether to skip the `bundle.write()` step when a rebuild is triggered. #### watch.exclude -Type: `string` +Type: `string`
+CLI: `--watch.exclude ` Prevent files from being watched: @@ -1313,9 +1317,10 @@ export default { ``` #### watch.include -Type: `string` +Type: `string`
+CLI: `--watch.include ` -Limit the file-watching to certain files: +Limit the file-watching to certain files. Note that this only filters the module graph but does not allow to add additional watch files: ```js // rollup.config.js diff --git a/src/Bundle.ts b/src/Bundle.ts new file mode 100644 index 00000000000..5fba855a5fc --- /dev/null +++ b/src/Bundle.ts @@ -0,0 +1,172 @@ +import Chunk from './Chunk'; +import { + NormalizedInputOptions, + NormalizedOutputOptions, + OutputBundle, + OutputBundleWithPlaceholders, + OutputChunk +} from './rollup/types'; +import { Addons, createAddons } from './utils/addons'; +import commondir from './utils/commondir'; +import { error, warnDeprecation } from './utils/error'; +import { FILE_PLACEHOLDER } from './utils/FileEmitter'; +import { basename, isAbsolute } from './utils/path'; +import { PluginDriver } from './utils/PluginDriver'; +import { timeEnd, timeStart } from './utils/timers'; + +export default class Bundle { + constructor( + private readonly outputOptions: NormalizedOutputOptions, + private readonly unsetOptions: Set, + private readonly inputOptions: NormalizedInputOptions, + private readonly pluginDriver: PluginDriver, + private readonly chunks: Chunk[] + ) {} + + async generate(isWrite: boolean): Promise { + timeStart('GENERATE', 1); + const inputBase = commondir(getAbsoluteEntryModulePaths(this.chunks)); + const outputBundle: OutputBundleWithPlaceholders = Object.create(null); + this.pluginDriver.setOutputBundle(outputBundle, this.outputOptions.assetFileNames); + try { + await this.pluginDriver.hookParallel('renderStart', [this.outputOptions, this.inputOptions]); + if (this.chunks.length > 1) { + validateOptionsForMultiChunkOutput(this.outputOptions); + } + + const addons = await createAddons(this.outputOptions, this.pluginDriver); + for (const chunk of this.chunks) { + chunk.generateExports(this.outputOptions); + } + for (const chunk of this.chunks) { + chunk.preRender(this.outputOptions, inputBase, this.pluginDriver); + } + this.assignChunkIds(inputBase, addons, outputBundle); + assignChunksToBundle(this.chunks, outputBundle); + + await Promise.all( + this.chunks.map(chunk => { + const outputChunk = outputBundle[chunk.id!] as OutputChunk; + return chunk + .render(this.outputOptions, addons, outputChunk, this.pluginDriver) + .then(rendered => { + outputChunk.code = rendered.code; + outputChunk.map = rendered.map; + }); + }) + ); + } catch (error) { + await this.pluginDriver.hookParallel('renderError', [error]); + throw error; + } + await this.pluginDriver.hookSeq('generateBundle', [ + this.outputOptions, + outputBundle as OutputBundle, + isWrite + ]); + for (const key of Object.keys(outputBundle)) { + const file = outputBundle[key] as any; + if (!file.type) { + warnDeprecation( + 'A plugin is directly adding properties to the bundle object in the "generateBundle" hook. This is deprecated and will be removed in a future Rollup version, please use "this.emitFile" instead.', + true, + this.inputOptions + ); + file.type = 'asset'; + } + } + this.pluginDriver.finaliseAssets(); + + timeEnd('GENERATE', 1); + return outputBundle as OutputBundle; + } + + private assignChunkIds(inputBase: string, addons: Addons, bundle: OutputBundleWithPlaceholders) { + const entryChunks: Chunk[] = []; + const otherChunks: Chunk[] = []; + for (const chunk of this.chunks) { + (chunk.facadeModule && chunk.facadeModule.isUserDefinedEntryPoint + ? entryChunks + : otherChunks + ).push(chunk); + } + + // make sure entry chunk names take precedence with regard to deconflicting + const chunksForNaming: Chunk[] = entryChunks.concat(otherChunks); + for (const chunk of chunksForNaming) { + if (this.outputOptions.file) { + chunk.id = basename(this.outputOptions.file); + } else if (this.inputOptions.preserveModules) { + chunk.id = chunk.generateIdPreserveModules( + inputBase, + this.outputOptions, + bundle, + this.unsetOptions + ); + } else { + chunk.id = chunk.generateId(addons, this.outputOptions, bundle, true, this.pluginDriver); + } + bundle[chunk.id] = FILE_PLACEHOLDER; + } + } +} + +function getAbsoluteEntryModulePaths(chunks: Chunk[]): string[] { + const absoluteEntryModulePaths: string[] = []; + for (const chunk of chunks) { + for (const entryModule of chunk.entryModules) { + if (isAbsolute(entryModule.id)) { + absoluteEntryModulePaths.push(entryModule.id); + } + } + } + return absoluteEntryModulePaths; +} + +function validateOptionsForMultiChunkOutput(outputOptions: NormalizedOutputOptions) { + if (outputOptions.format === 'umd' || outputOptions.format === 'iife') + return error({ + code: 'INVALID_OPTION', + message: 'UMD and IIFE output formats are not supported for code-splitting builds.' + }); + if (typeof outputOptions.file === 'string') + return error({ + code: 'INVALID_OPTION', + message: + 'When building multiple chunks, the "output.dir" option must be used, not "output.file". ' + + 'To inline dynamic imports, set the "inlineDynamicImports" option.' + }); + if (outputOptions.sourcemapFile) + return error({ + code: 'INVALID_OPTION', + message: '"output.sourcemapFile" is only supported for single-file builds.' + }); +} + +function assignChunksToBundle( + chunks: Chunk[], + outputBundle: OutputBundleWithPlaceholders +): OutputBundle { + for (let i = 0; i < chunks.length; i++) { + const chunk = chunks[i]; + const facadeModule = chunk.facadeModule; + + outputBundle[chunk.id!] = { + code: undefined as any, + dynamicImports: chunk.getDynamicImportIds(), + exports: chunk.getExportNames(), + facadeModuleId: facadeModule && facadeModule.id, + fileName: chunk.id, + imports: chunk.getImportIds(), + isDynamicEntry: chunk.isDynamicEntry, + isEntry: facadeModule !== null && facadeModule.isEntryPoint, + map: undefined, + modules: chunk.renderedModules, + get name() { + return chunk.getChunkName(); + }, + type: 'chunk' + } as OutputChunk; + } + return outputBundle as OutputBundle; +} diff --git a/src/Chunk.ts b/src/Chunk.ts index 773f4b03001..d74890d50ef 100644 --- a/src/Chunk.ts +++ b/src/Chunk.ts @@ -11,16 +11,17 @@ import SyntheticNamedExportVariable from './ast/variables/SyntheticNamedExportVa import Variable from './ast/variables/Variable'; import ExternalModule from './ExternalModule'; import finalisers from './finalisers/index'; -import Graph from './Graph'; import Module from './Module'; import { DecodedSourceMapOrMissing, GlobalsOption, InternalModuleFormat, - OutputOptions, + NormalizedInputOptions, + NormalizedOutputOptions, PreRenderedChunk, RenderedChunk, - RenderedModule + RenderedModule, + WarningHandler } from './rollup/types'; import { Addons } from './utils/addons'; import { collapseSourcemaps } from './utils/collapseSourcemaps'; @@ -29,6 +30,7 @@ import { deconflictChunk } from './utils/deconflictChunk'; import { errFailedValidation, error } from './utils/error'; import { sortByExecutionOrder } from './utils/executionOrder'; import { assignExportsToMangledNames, assignExportsToNames } from './utils/exportNames'; +import getExportMode from './utils/getExportMode'; import getIndentString from './utils/getIndentString'; import { makeLegal } from './utils/identifierHelpers'; import { basename, dirname, extname, isAbsolute, normalize, resolve } from './utils/path'; @@ -89,23 +91,17 @@ const NON_ASSET_EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx']; function getGlobalName( module: ExternalModule, - globals: GlobalsOption | undefined, - graph: Graph, - hasExports: boolean + globals: GlobalsOption, + hasExports: boolean, + warn: WarningHandler ) { - let globalName: string | undefined; - if (typeof globals === 'function') { - globalName = globals(module.id); - } else if (globals) { - globalName = globals[module.id]; - } - + const globalName = typeof globals === 'function' ? globals(module.id) : globals[module.id]; if (globalName) { return globalName; } if (hasExports) { - graph.warn({ + warn({ code: 'MISSING_GLOBAL_NAME', guess: module.variableName, message: `No name was provided for external module '${module.id}' in output.globals – guessing '${module.variableName}'`, @@ -117,11 +113,13 @@ function getGlobalName( export default class Chunk { private static generateFacade( - graph: Graph, + inputOptions: NormalizedInputOptions, + unsetOptions: Set, + modulesById: Map, facadedModule: Module, facadeName: FacadeName ): Chunk { - const chunk = new Chunk(graph, []); + const chunk = new Chunk([], inputOptions, unsetOptions, modulesById); chunk.assignFacadeName(facadeName, facadedModule); if (!facadedModule.facadeChunk) { facadedModule.facadeChunk = chunk; @@ -146,12 +144,10 @@ export default class Chunk { execIndex: number; exportMode: 'none' | 'named' | 'default' = 'named'; facadeModule: Module | null = null; - graph: Graph; id: string | null = null; indentString: string = undefined as any; isDynamicEntry = false; manualChunkAlias: string | null = null; - orderedModules: Module[]; renderedModules?: { [moduleId: string]: RenderedModule; }; @@ -179,9 +175,12 @@ export default class Chunk { private sortedExportNames: string[] | null = null; private strictFacade = false; - constructor(graph: Graph, orderedModules: Module[]) { - this.graph = graph; - this.orderedModules = orderedModules; + constructor( + private readonly orderedModules: Module[], + private readonly inputOptions: NormalizedInputOptions, + private readonly unsetOptions: Set, + private readonly modulesById: Map + ) { this.execIndex = orderedModules.length > 0 ? orderedModules[0].execIndex : Infinity; for (const module of orderedModules) { @@ -223,9 +222,9 @@ export default class Chunk { moduleExportNamesByVariable.size === 0 && module.isUserDefinedEntryPoint && module.preserveSignature === 'strict' && - this.graph.preserveEntrySignatures === undefined + this.unsetOptions.has('preserveEntrySignatures') ) { - this.graph.warn({ + this.inputOptions.onwarn({ code: 'EMPTY_FACADE', id: module.id, message: `To preserve the export signature of the entry module "${relativeId( @@ -247,7 +246,7 @@ export default class Chunk { return true; } - generateExports(options: OutputOptions) { + generateExports(options: NormalizedOutputOptions) { this.sortedExportNames = null; this.exportsByName = Object.create(null); const remainingExports = new Set(this.exports); @@ -263,15 +262,18 @@ export default class Chunk { remainingExports.delete(variable); } } - if ( - options.minifyInternalExports === true || - (typeof options.minifyInternalExports !== 'boolean' && - (options.format === 'system' || options.format === 'es' || options.compact)) - ) { + if (options.minifyInternalExports) { assignExportsToMangledNames(remainingExports, this.exportsByName); } else { assignExportsToNames(remainingExports, this.exportsByName); } + if (this.inputOptions.preserveModules || (this.facadeModule && this.facadeModule.isEntryPoint)) + this.exportMode = getExportMode( + this, + options, + this.facadeModule!.id, + this.inputOptions.onwarn + ); } generateFacades(): Chunk[] { @@ -301,7 +303,7 @@ export default class Chunk { } if ( !this.facadeModule && - (this.graph.preserveModules || + (this.inputOptions.preserveModules || module.preserveSignature !== 'strict' || this.canModuleBeFacade(module, exposedVariables)) ) { @@ -315,7 +317,15 @@ export default class Chunk { } for (const facadeName of requiredFacades) { - facades.push(Chunk.generateFacade(this.graph, module, facadeName)); + facades.push( + Chunk.generateFacade( + this.inputOptions, + this.unsetOptions, + this.modulesById, + module, + facadeName + ) + ); } } for (const module of dynamicEntryModules) { @@ -340,7 +350,7 @@ export default class Chunk { generateId( addons: Addons, - options: OutputOptions, + options: NormalizedOutputOptions, existingNames: Record, includeHash: boolean, outputPluginDriver: PluginDriver @@ -350,11 +360,11 @@ export default class Chunk { } const [pattern, patternName] = this.facadeModule && this.facadeModule.isUserDefinedEntryPoint - ? [options.entryFileNames || '[name].js', 'output.entryFileNames'] - : [options.chunkFileNames || '[name]-[hash].js', 'output.chunkFileNames']; + ? [options.entryFileNames, 'output.entryFileNames'] + : [options.chunkFileNames, 'output.chunkFileNames']; return makeUnique( renderNamePattern(pattern, patternName, { - format: () => options.format as string, + format: () => options.format, hash: () => includeHash ? this.computeContentHashWithDependencies( @@ -372,29 +382,29 @@ export default class Chunk { generateIdPreserveModules( preserveModulesRelativeDir: string, - options: OutputOptions, - existingNames: Record + options: NormalizedOutputOptions, + existingNames: Record, + unsetOptions: Set ): string { const id = this.orderedModules[0].id; const sanitizedId = sanitizeFileName(id); - let path: string; if (isAbsolute(id)) { const extension = extname(id); - - const name = renderNamePattern( - options.entryFileNames || - (NON_ASSET_EXTENSIONS.includes(extension) ? '[name].js' : '[name][extname].js'), - 'output.entryFileNames', - { + const pattern = unsetOptions.has('entryFileNames') + ? NON_ASSET_EXTENSIONS.includes(extension) + ? '[name].js' + : '[name][extname].js' + : options.entryFileNames; + path = relative( + preserveModulesRelativeDir, + `${dirname(sanitizedId)}/${renderNamePattern(pattern, 'output.entryFileNames', { ext: () => extension.substr(1), extname: () => extension, format: () => options.format as string, name: () => this.getChunkName() - } + })}` ); - - path = relative(preserveModulesRelativeDir, `${dirname(sanitizedId)}/${name}`); } else { path = `_virtual/${basename(sanitizedId)}`; } @@ -449,7 +459,7 @@ export default class Chunk { } getVariableExportName(variable: Variable): string { - if (this.graph.preserveModules && variable instanceof NamespaceVariable) { + if (this.inputOptions.preserveModules && variable instanceof NamespaceVariable) { return '*'; } for (const exportName of Object.keys(this.exportsByName)) { @@ -467,31 +477,31 @@ export default class Chunk { } // prerender allows chunk hashes and names to be generated before finalizing - preRender(options: OutputOptions, inputBase: string, outputPluginDriver: PluginDriver) { + preRender(options: NormalizedOutputOptions, inputBase: string, outputPluginDriver: PluginDriver) { timeStart('render modules', 3); const magicString = new MagicStringBundle({ separator: options.compact ? '' : '\n\n' }); this.usedModules = []; - this.indentString = options.compact ? '' : getIndentString(this.orderedModules, options); + this.indentString = getIndentString(this.orderedModules, options); const n = options.compact ? '' : '\n'; const _ = options.compact ? '' : ' '; const renderOptions: RenderOptions = { - compact: options.compact as boolean, - dynamicImportFunction: options.dynamicImportFunction as string, - format: options.format as InternalModuleFormat, - freeze: options.freeze !== false, + compact: options.compact, + dynamicImportFunction: options.dynamicImportFunction, + format: options.format, + freeze: options.freeze, indent: this.indentString, - namespaceToStringTag: options.namespaceToStringTag === true, + namespaceToStringTag: options.namespaceToStringTag, outputPluginDriver, varOrConst: options.preferConst ? 'const' : 'var' }; // for static and dynamic entry points, inline the execution list to avoid loading latency if ( - options.hoistTransitiveImports !== false && - !this.graph.preserveModules && + options.hoistTransitiveImports && + !this.inputOptions.preserveModules && this.facadeModule !== null ) { for (const dep of this.dependencies) { @@ -520,7 +530,7 @@ export default class Chunk { this.usedModules.push(module); } const namespace = module.namespace; - if (namespace.included && !this.graph.preserveModules) { + if (namespace.included && !this.inputOptions.preserveModules) { const rendered = namespace.renderBlock(renderOptions); if (namespace.renderFirst()) hoistedSource += n + rendered; else magicString.addSource(new MagicString(rendered)); @@ -552,7 +562,7 @@ export default class Chunk { if (this.isEmpty && this.getExportNames().length === 0 && this.dependencies.size === 0) { const chunkName = this.getChunkName(); - this.graph.warn({ + this.inputOptions.onwarn({ chunkName, code: 'EMPTY_BUNDLE', message: `Generated an empty chunk: "${chunkName}"` @@ -563,15 +573,13 @@ export default class Chunk { this.renderedDependencies = this.getChunkDependencyDeclarations(options); this.renderedExports = - this.exportMode === 'none' - ? [] - : this.getChunkExportDeclarations(options.format as InternalModuleFormat); + this.exportMode === 'none' ? [] : this.getChunkExportDeclarations(options.format); timeEnd('render modules', 3); } async render( - options: OutputOptions, + options: NormalizedOutputOptions, addons: Addons, outputChunk: RenderedChunk, outputPluginDriver: PluginDriver @@ -579,10 +587,10 @@ export default class Chunk { timeStart('render format', 3); const chunkId = this.id!; - const format = options.format as InternalModuleFormat; + const format = options.format; const finalise = finalisers[format]; if (options.dynamicImportFunction && format !== 'es') { - this.graph.warn({ + this.inputOptions.onwarn({ code: 'INVALID_OPTION', message: '"output.dynamicImportFunction" is ignored for formats other than "es".' }); @@ -641,13 +649,13 @@ export default class Chunk { indentString: this.indentString, intro: addons.intro!, isEntryModuleFacade: - this.graph.preserveModules || + this.inputOptions.preserveModules || (this.facadeModule !== null && this.facadeModule.isEntryPoint), namedExportsMode: this.exportMode !== 'default', outro: addons.outro!, usesTopLevelAwait, varOrConst: options.preferConst ? 'const' : 'var', - warn: this.graph.warn.bind(this.graph) + warn: this.inputOptions.onwarn }, options ); @@ -677,12 +685,12 @@ export default class Chunk { const decodedMap = magicString.generateDecodedMap({}); map = collapseSourcemaps( - this.graph, file, decodedMap, this.usedModules, chunkSourcemapChain, - options.sourcemapExcludeSources! + options.sourcemapExcludeSources, + this.inputOptions.onwarn ); map.sources = map.sources .map(sourcePath => { @@ -735,7 +743,7 @@ export default class Chunk { private computeContentHashWithDependencies( addons: Addons, - options: OutputOptions, + options: NormalizedOutputOptions, existingNames: Record, outputPluginDriver: PluginDriver ): string { @@ -772,7 +780,7 @@ export default class Chunk { exportingModule && exportingModule.chunk && exportingModule.chunk !== this && - !(importedVariable instanceof NamespaceVariable && this.graph.preserveModules) + !(importedVariable instanceof NamespaceVariable && this.inputOptions.preserveModules) ) { exportingModule.chunk.exports.add(importedVariable); if (isSynthetic) { @@ -782,7 +790,7 @@ export default class Chunk { } } - private finaliseDynamicImports(options: OutputOptions) { + private finaliseDynamicImports(options: NormalizedOutputOptions) { const stripKnownJsExtensions = options.format === 'amd'; for (const [module, code] of this.renderedModuleSources) { for (const { node, resolution } of module.dynamicImports) { @@ -830,7 +838,7 @@ export default class Chunk { } private getChunkDependencyDeclarations( - options: OutputOptions + options: NormalizedOutputOptions ): Map { const reexportDeclarations = new Map(); @@ -839,8 +847,8 @@ export default class Chunk { let importName: string; let needsLiveBinding = false; if (exportName[0] === '*') { - needsLiveBinding = options.externalLiveBindings !== false; - exportChunk = this.graph.moduleById.get(exportName.substr(1)) as ExternalModule; + needsLiveBinding = options.externalLiveBindings; + exportChunk = this.modulesById.get(exportName.substr(1)) as ExternalModule; importName = exportName = '*'; } else { const variable = this.exportsByName[exportName]; @@ -854,7 +862,7 @@ export default class Chunk { } else { exportChunk = module; importName = variable.name; - needsLiveBinding = options.externalLiveBindings !== false; + needsLiveBinding = options.externalLiveBindings; } } let reexportDeclaration = reexportDeclarations.get(exportChunk); @@ -906,8 +914,8 @@ export default class Chunk { globalName = getGlobalName( dep, options.globals, - this.graph, - exportsNames || exportsDefault + exportsNames || exportsDefault, + this.inputOptions.onwarn )!; } } @@ -1036,7 +1044,7 @@ export default class Chunk { } } - private setExternalRenderPaths(options: OutputOptions, inputBase: string) { + private setExternalRenderPaths(options: NormalizedOutputOptions, inputBase: string) { for (const dependency of [...this.dependencies, ...this.dynamicDependencies]) { if (dependency instanceof ExternalModule) { dependency.setRenderPath(options, inputBase); @@ -1044,7 +1052,7 @@ export default class Chunk { } } - private setIdentifierRenderResolutions(options: OutputOptions) { + private setIdentifierRenderResolutions(options: NormalizedOutputOptions) { const syntheticExports = new Set(); for (const exportName of this.getExportNames()) { @@ -1089,8 +1097,8 @@ export default class Chunk { this.imports, usedNames, options.format as string, - options.interop !== false, - this.graph.preserveModules, + options.interop, + this.inputOptions.preserveModules, syntheticExports ); } @@ -1106,7 +1114,7 @@ export default class Chunk { if (variable.module && (variable.module as Module).chunk !== this) { this.imports.add(variable); if ( - !(variable instanceof NamespaceVariable && this.graph.preserveModules) && + !(variable instanceof NamespaceVariable && this.inputOptions.preserveModules) && variable.module instanceof Module ) { variable.module.chunk!.exports.add(variable); diff --git a/src/ExternalModule.ts b/src/ExternalModule.ts index f6e43bc64c3..486b8638a69 100644 --- a/src/ExternalModule.ts +++ b/src/ExternalModule.ts @@ -1,6 +1,5 @@ import ExternalVariable from './ast/variables/ExternalVariable'; -import Graph from './Graph'; -import { OutputOptions } from './rollup/types'; +import { NormalizedInputOptions, NormalizedOutputOptions } from './rollup/types'; import { makeLegal } from './utils/identifierHelpers'; import { isAbsolute, normalize, relative } from './utils/path'; @@ -23,10 +22,11 @@ export default class ExternalModule { used = false; variableName: string; - private graph: Graph; - - constructor(graph: Graph, id: string, moduleSideEffects: boolean) { - this.graph = graph; + constructor( + private readonly options: NormalizedInputOptions, + id: string, + moduleSideEffects: boolean + ) { this.id = id; this.execIndex = Infinity; this.moduleSideEffects = moduleSideEffects; @@ -54,12 +54,9 @@ export default class ExternalModule { return declaration; } - setRenderPath(options: OutputOptions, inputBase: string) { - this.renderPath = ''; - if (options.paths) { - this.renderPath = - typeof options.paths === 'function' ? options.paths(this.id) : options.paths[this.id]; - } + setRenderPath(options: NormalizedOutputOptions, inputBase: string) { + this.renderPath = + typeof options.paths === 'function' ? options.paths(this.id) : options.paths[this.id]; if (!this.renderPath) { if (!isAbsolute(this.id)) { this.renderPath = this.id; @@ -98,7 +95,7 @@ export default class ExternalModule { .map(name => `'${name}'`) .join(', ')} and '${unused.slice(-1)}' are`; - this.graph.warn({ + this.options.onwarn({ code: 'UNUSED_EXTERNAL_IMPORT', message: `${names} imported from external module '${this.id}' but never used`, names: unused, diff --git a/src/Graph.ts b/src/Graph.ts index 1b20a7dd5c6..84dccd5c2b7 100644 --- a/src/Graph.ts +++ b/src/Graph.ts @@ -1,43 +1,29 @@ import * as acorn from 'acorn'; -import injectClassFields from 'acorn-class-fields'; -import injectImportMeta from 'acorn-import-meta'; -import injectStaticClassFeatures from 'acorn-static-class-features'; import GlobalScope from './ast/scopes/GlobalScope'; import { PathTracker } from './ast/utils/PathTracker'; import Chunk from './Chunk'; import ExternalModule from './ExternalModule'; -import Module, { defaultAcornOptions } from './Module'; +import Module from './Module'; import { ModuleLoader, UnresolvedModule } from './ModuleLoader'; import { - InputOptions, - IsExternal, - ManualChunksOption, ModuleInfo, ModuleJSON, - PreserveEntrySignaturesOption, + NormalizedInputOptions, RollupCache, - RollupWarning, RollupWatcher, - SerializablePluginCache, - TreeshakingOptions, - WarningHandler + SerializablePluginCache } from './rollup/types'; import { BuildPhase } from './utils/buildPhase'; import { getChunkAssignments } from './utils/chunkAssignment'; -import { errDeprecation, error } from './utils/error'; import { analyseModuleExecution, sortByExecutionOrder } from './utils/executionOrder'; -import { resolve } from './utils/path'; import { PluginDriver } from './utils/PluginDriver'; import relativeId from './utils/relativeId'; import { timeEnd, timeStart } from './utils/timers'; import { markModuleAndImpureDependenciesAsExecuted } from './utils/traverseStaticDependencies'; function normalizeEntryModules( - entryModules: string | string[] | Record + entryModules: string[] | Record ): UnresolvedModule[] { - if (typeof entryModules === 'string') { - return [{ fileName: null, name: null, id: entryModules, importer: undefined }]; - } if (Array.isArray(entryModules)) { return entryModules.map(id => ({ fileName: null, name: null, id, importer: undefined })); } @@ -50,42 +36,36 @@ function normalizeEntryModules( } export default class Graph { - acornOptions: acorn.Options; acornParser: typeof acorn.Parser; cachedModules: Map; contextParse: (code: string, acornOptions?: acorn.Options) => acorn.Node; deoptimizationTracker: PathTracker; - getModuleContext: (id: string) => string; - moduleById = new Map(); moduleLoader: ModuleLoader; + modulesById = new Map(); needsTreeshakingPass = false; phase: BuildPhase = BuildPhase.LOAD_AND_PARSE; pluginDriver: PluginDriver; - preserveEntrySignatures: PreserveEntrySignaturesOption | undefined; - preserveModules: boolean; scope: GlobalScope; - shimMissingExports: boolean; - treeshakingOptions?: TreeshakingOptions; watchFiles: Record = Object.create(null); - private cacheExpiry: number; - private context: string; + private entryModules: Module[] = []; private externalModules: ExternalModule[] = []; + private manualChunkModulesByAlias: Record = {}; private modules: Module[] = []; - private onwarn: WarningHandler; private pluginCache?: Record; - private strictDeprecations: boolean; - constructor(options: InputOptions, watcher: RollupWatcher | null) { - this.onwarn = options.onwarn as WarningHandler; + constructor( + private readonly options: NormalizedInputOptions, + private readonly unsetOptions: Set, + watcher: RollupWatcher | null + ) { this.deoptimizationTracker = new PathTracker(); this.cachedModules = new Map(); - if (options.cache) { - if (options.cache.modules) - for (const module of options.cache.modules) this.cachedModules.set(module.id, module); - } if (options.cache !== false) { - this.pluginCache = (options.cache && options.cache.plugins) || Object.create(null); + if (options.cache?.modules) { + for (const module of options.cache.modules) this.cachedModules.set(module.id, module); + } + this.pluginCache = options.cache?.plugins || Object.create(null); // increment access counter for (const name in this.pluginCache) { @@ -93,46 +73,13 @@ export default class Graph { for (const key of Object.keys(cache)) cache[key][0]++; } } - this.preserveModules = options.preserveModules!; - this.preserveEntrySignatures = options.preserveEntrySignatures!; - this.strictDeprecations = options.strictDeprecations!; - - this.cacheExpiry = options.experimentalCacheExpiry!; - - if (options.treeshake !== false) { - this.treeshakingOptions = - options.treeshake && options.treeshake !== true - ? { - annotations: options.treeshake.annotations !== false, - moduleSideEffects: options.treeshake.moduleSideEffects, - propertyReadSideEffects: options.treeshake.propertyReadSideEffects !== false, - pureExternalModules: options.treeshake.pureExternalModules, - tryCatchDeoptimization: options.treeshake.tryCatchDeoptimization !== false, - unknownGlobalSideEffects: options.treeshake.unknownGlobalSideEffects !== false - } - : { - annotations: true, - moduleSideEffects: true, - propertyReadSideEffects: true, - tryCatchDeoptimization: true, - unknownGlobalSideEffects: true - }; - if (typeof this.treeshakingOptions.pureExternalModules !== 'undefined') { - this.warnDeprecation( - `The "treeshake.pureExternalModules" option is deprecated. The "treeshake.moduleSideEffects" option should be used instead. "treeshake.pureExternalModules: true" is equivalent to "treeshake.moduleSideEffects: 'no-external'"`, - true - ); - } - } - this.contextParse = (code: string, options: acorn.Options = {}) => this.acornParser.parse(code, { - ...defaultAcornOptions, - ...options, - ...this.acornOptions + ...this.options.acorn, + ...options }); - this.pluginDriver = new PluginDriver(this, options.plugins!, this.pluginCache); + this.pluginDriver = new PluginDriver(this, options, options.plugins, this.pluginCache); if (watcher) { const handleChange = (id: string) => this.pluginDriver.hookSeqSync('watchChange', [id]); @@ -142,97 +89,73 @@ export default class Graph { }); } - this.shimMissingExports = options.shimMissingExports as boolean; this.scope = new GlobalScope(); - this.context = String(options.context); - - const optionsModuleContext = options.moduleContext; - if (typeof optionsModuleContext === 'function') { - this.getModuleContext = id => optionsModuleContext(id) || this.context; - } else if (typeof optionsModuleContext === 'object') { - const moduleContext = new Map(); - for (const key in optionsModuleContext) { - moduleContext.set(resolve(key), optionsModuleContext[key]); - } - this.getModuleContext = id => moduleContext.get(id) || this.context; - } else { - this.getModuleContext = () => this.context; - } - - this.acornOptions = { allowAwaitOutsideFunction: true, ...options.acorn }; - const acornPluginsToInject: Function[] = [ - injectImportMeta, - injectClassFields, - injectStaticClassFeatures - ]; - const acornInjectPlugins = options.acornInjectPlugins; - acornPluginsToInject.push( - ...(Array.isArray(acornInjectPlugins) - ? acornInjectPlugins - : acornInjectPlugins - ? [acornInjectPlugins] - : []) - ); - this.acornParser = acorn.Parser.extend(...(acornPluginsToInject as any)); - this.moduleLoader = new ModuleLoader( - this, - this.moduleById, - this.pluginDriver, - options.preserveSymlinks === true, - options.external as (string | RegExp)[] | IsExternal, - (this.treeshakingOptions ? this.treeshakingOptions.moduleSideEffects : null)!, - (this.treeshakingOptions ? this.treeshakingOptions.pureExternalModules : false)! - ); + this.acornParser = acorn.Parser.extend(...(options.acornInjectPlugins as any)); + this.moduleLoader = new ModuleLoader(this, this.modulesById, this.options, this.pluginDriver); } - async build( - entryModuleIds: string | string[] | Record, - manualChunks: ManualChunksOption | void, - inlineDynamicImports: boolean - ): Promise { - // Phase 1 – discovery. We load the entry module and find which - // modules it imports, and import those, until we have all - // of the entry module's dependencies - timeStart('parse modules', 2); - const { entryModules, manualChunkModulesByAlias } = await this.parseModules( - entryModuleIds, - manualChunks - ); - timeEnd('parse modules', 2); + async build(): Promise { + timeStart('generate module graph', 2); + await this.generateModuleGraph(); + timeEnd('generate module graph', 2); - // Phase 2 - linking. We populate the module dependency links and - // determine the topological execution order for the bundle - timeStart('analyse dependency graph', 2); + timeStart('link and order modules', 2); this.phase = BuildPhase.ANALYSE; - this.link(entryModules); - timeEnd('analyse dependency graph', 2); + this.linkAndOrderModules(); + timeEnd('link and order modules', 2); - // Phase 3 – marking. We include all statements that should be included timeStart('mark included statements', 2); - this.includeStatements(entryModules); + this.includeStatements(); timeEnd('mark included statements', 2); - // Phase 4 – we construct the chunks, working out the optimal chunking using - // entry point graph colouring, before generating the import and export facades timeStart('generate chunks', 2); - const chunks = this.generateChunks( - entryModules, - manualChunkModulesByAlias, - inlineDynamicImports - ); - this.phase = BuildPhase.GENERATE; + const chunks = this.generateChunks(); timeEnd('generate chunks', 2); + this.phase = BuildPhase.GENERATE; return chunks; } + generateChunks(): Chunk[] { + const chunks: Chunk[] = []; + if (this.options.preserveModules) { + for (const module of this.modules) { + if ( + module.isIncluded() || + module.isEntryPoint || + module.includedDynamicImporters.length > 0 + ) { + const chunk = new Chunk([module], this.options, this.unsetOptions, this.modulesById); + chunk.entryModules = [module]; + chunks.push(chunk); + } + } + } else { + for (const chunkModules of this.options.inlineDynamicImports + ? [this.modules] + : getChunkAssignments(this.entryModules, this.manualChunkModulesByAlias)) { + sortByExecutionOrder(chunkModules); + chunks.push(new Chunk(chunkModules, this.options, this.unsetOptions, this.modulesById)); + } + } + + for (const chunk of chunks) { + chunk.link(); + } + const facades: Chunk[] = []; + for (const chunk of chunks) { + facades.push(...chunk.generateFacades()); + } + return [...chunks, ...facades]; + } + getCache(): RollupCache { // handle plugin cache eviction for (const name in this.pluginCache) { const cache = this.pluginCache[name]; let allDeleted = true; for (const key of Object.keys(cache)) { - if (cache[key][0] >= this.cacheExpiry) delete cache[key]; + if (cache[key][0] >= this.options.experimentalCacheExpiry) delete cache[key]; else allDeleted = false; } if (allDeleted) delete this.pluginCache[name]; @@ -245,7 +168,7 @@ export default class Graph { } getModuleInfo = (moduleId: string): ModuleInfo => { - const foundModule = this.moduleById.get(moduleId); + const foundModule = this.modulesById.get(moduleId); if (foundModule == null) { throw new Error(`Unable to find module ${moduleId}`); } @@ -273,77 +196,38 @@ export default class Graph { }; }; - warn(warning: RollupWarning) { - warning.toString = () => { - let str = ''; - - if (warning.plugin) str += `(${warning.plugin} plugin) `; - if (warning.loc) - str += `${relativeId(warning.loc.file!)} (${warning.loc.line}:${warning.loc.column}) `; - str += warning.message; - - return str; - }; - - this.onwarn(warning); - } - - warnDeprecation(deprecation: string | RollupWarning, activeDeprecation: boolean): void { - if (activeDeprecation || this.strictDeprecations) { - const warning = errDeprecation(deprecation); - if (this.strictDeprecations) { - return error(warning); - } - this.warn(warning); - } - } - - private generateChunks( - entryModules: Module[], - manualChunkModulesByAlias: Record, - inlineDynamicImports: boolean - ): Chunk[] { - const chunks: Chunk[] = []; - if (this.preserveModules) { - for (const module of this.modules) { - if ( - module.isIncluded() || - module.isEntryPoint || - module.includedDynamicImporters.length > 0 - ) { - const chunk = new Chunk(this, [module]); - chunk.entryModules = [module]; - chunks.push(chunk); - } - } - } else { - for (const chunkModules of inlineDynamicImports - ? [this.modules] - : getChunkAssignments(entryModules, manualChunkModulesByAlias)) { - sortByExecutionOrder(chunkModules); - chunks.push(new Chunk(this, chunkModules)); - } + private async generateModuleGraph(): Promise { + const { manualChunks } = this.options; + [ + { entryModules: this.entryModules, manualChunkModulesByAlias: this.manualChunkModulesByAlias } + ] = await Promise.all([ + this.moduleLoader.addEntryModules(normalizeEntryModules(this.options.input), true), + typeof manualChunks === 'object' ? this.moduleLoader.addManualChunks(manualChunks) : null + ]); + if (typeof manualChunks === 'function') { + this.moduleLoader.assignManualChunks(manualChunks); } - - for (const chunk of chunks) { - chunk.link(); + if (this.entryModules.length === 0) { + throw new Error('You must supply options.input to rollup'); } - const facades: Chunk[] = []; - for (const chunk of chunks) { - facades.push(...chunk.generateFacades()); + for (const module of this.modulesById.values()) { + if (module instanceof Module) { + this.modules.push(module); + } else { + this.externalModules.push(module); + } } - return [...chunks, ...facades]; } - private includeStatements(entryModules: Module[]) { - for (const module of entryModules) { + private includeStatements() { + for (const module of this.entryModules) { if (module.preserveSignature !== false) { module.includeAllExports(); } else { markModuleAndImpureDependenciesAsExecuted(module); } } - if (this.treeshakingOptions) { + if (this.options.treeshake) { let treeshakingPass = 1; do { timeStart(`treeshaking pass ${treeshakingPass}`, 3); @@ -361,13 +245,13 @@ export default class Graph { for (const externalModule of this.externalModules) externalModule.warnUnusedImports(); } - private link(entryModules: Module[]) { + private linkAndOrderModules() { for (const module of this.modules) { module.linkDependencies(); } - const { orderedModules, cyclePaths } = analyseModuleExecution(entryModules); + const { orderedModules, cyclePaths } = analyseModuleExecution(this.entryModules); for (const cyclePath of cyclePaths) { - this.warn({ + this.options.onwarn({ code: 'CIRCULAR_DEPENDENCY', cycle: cyclePath, importer: cyclePath[0], @@ -381,32 +265,6 @@ export default class Graph { this.warnForMissingExports(); } - private async parseModules( - entryModuleIds: string | string[] | Record, - manualChunks: ManualChunksOption | void - ): Promise<{ entryModules: Module[]; manualChunkModulesByAlias: Record }> { - const [{ entryModules, manualChunkModulesByAlias }] = await Promise.all([ - this.moduleLoader.addEntryModules(normalizeEntryModules(entryModuleIds), true), - manualChunks && - typeof manualChunks === 'object' && - this.moduleLoader.addManualChunks(manualChunks) - ]); - if (typeof manualChunks === 'function') { - this.moduleLoader.assignManualChunks(manualChunks); - } - if (entryModules.length === 0) { - throw new Error('You must supply options.input to rollup'); - } - for (const module of this.moduleById.values()) { - if (module instanceof Module) { - this.modules.push(module); - } else { - this.externalModules.push(module); - } - } - return { entryModules, manualChunkModulesByAlias }; - } - private warnForMissingExports() { for (const module of this.modules) { for (const importName of Object.keys(module.importDescriptions)) { diff --git a/src/Module.ts b/src/Module.ts index eb51f138b84..500fb50fbda 100644 --- a/src/Module.ts +++ b/src/Module.ts @@ -34,6 +34,7 @@ import { EmittedFile, ExistingDecodedSourceMap, ModuleJSON, + NormalizedInputOptions, PreserveEntrySignaturesOption, ResolvedIdMap, RollupError, @@ -86,7 +87,6 @@ export interface AstContext { ) => void; addImport: (node: ImportDeclaration) => void; addImportMeta: (node: MetaProperty) => void; - annotations: boolean; code: string; deoptimizationTracker: PathTracker; error: (props: RollupError, pos?: number) => never; @@ -103,28 +103,16 @@ export interface AstContext { module: Module; // not to be used for tree-shaking moduleContext: string; nodeConstructors: { [name: string]: typeof NodeBase }; - preserveModules: boolean; - propertyReadSideEffects: boolean; + options: NormalizedInputOptions; traceExport: (name: string) => Variable; traceVariable: (name: string) => Variable | null; - treeshake: boolean; - tryCatchDeoptimization: boolean; - unknownGlobalSideEffects: boolean; usesTopLevelAwait: boolean; warn: (warning: RollupWarning, pos?: number) => void; - warnDeprecation: (deprecation: string | RollupWarning, activeDeprecation: boolean) => void; } -export const defaultAcornOptions: acorn.Options = { - ecmaVersion: 2020, - preserveParens: false, - sourceType: 'module' -}; - function tryParse(module: Module, Parser: typeof acorn.Parser, acornOptions: acorn.Options) { try { return Parser.parse(module.code, { - ...defaultAcornOptions, ...acornOptions, onComment: (block: boolean, text: string, start: number, end: number) => module.comments.push({ block, text, start, end }) @@ -219,7 +207,7 @@ export default class Module { namespace!: NamespaceVariable; originalCode!: string; originalSourcemap!: ExistingDecodedSourceMap | null; - preserveSignature: PreserveEntrySignaturesOption = this.graph.preserveEntrySignatures ?? 'strict'; + preserveSignature: PreserveEntrySignaturesOption = this.options.preserveEntrySignatures; reexportDescriptions: { [name: string]: ReexportDescription } = Object.create(null); resolvedIds!: ResolvedIdMap; scope!: ModuleScope; @@ -249,12 +237,13 @@ export default class Module { constructor( private readonly graph: Graph, public readonly id: string, + private readonly options: NormalizedInputOptions, public moduleSideEffects: boolean, public syntheticNamedExports: boolean, public isEntryPoint: boolean ) { this.excludeFromSourcemap = /\0/.test(id); - this.context = graph.getModuleContext(id); + this.context = options.moduleContext(id); } basename() { @@ -346,7 +335,7 @@ export default class Module { if ( this.isEntryPoint || this.includedDynamicImporters.length > 0 || - this.graph.preserveModules + this.options.preserveModules ) { dependencyVariables = new Set(dependencyVariables); for (const exportName of [...this.getReexports(), ...this.getExports()]) { @@ -365,7 +354,7 @@ export default class Module { } relevantDependencies.add(variable.module!); } - if (this.graph.treeshakingOptions) { + if (this.options.treeshake) { const possibleDependencies = new Set(this.dependencies); for (const dependency of possibleDependencies) { if ( @@ -464,7 +453,7 @@ export default class Module { return this.namespace; } else { // export * from 'external' - const module = this.graph.moduleById.get(name.slice(1)) as ExternalModule; + const module = this.graph.modulesById.get(name.slice(1)) as ExternalModule; return module.getVariableForExportName('*'); } } @@ -527,7 +516,7 @@ export default class Module { return syntheticExport; } - if (this.graph.shimMissingExports) { + if (this.options.shimMissingExports) { this.shimMissingExport(name); return this.exportShimVariable; } @@ -582,7 +571,7 @@ export default class Module { linkDependencies() { for (const source of this.sources) { - this.dependencies.add(this.graph.moduleById.get(this.resolvedIds[source].id)!); + this.dependencies.add(this.graph.modulesById.get(this.resolvedIds[source].id)!); } for (const { resolution } of this.dynamicImports) { if (resolution instanceof Module || resolution instanceof ExternalModule) { @@ -595,7 +584,7 @@ export default class Module { const externalExportAllModules: ExternalModule[] = []; for (const source of this.exportAllSources) { - const module = this.graph.moduleById.get(this.resolvedIds[source].id) as + const module = this.graph.modulesById.get(this.resolvedIds[source].id) as | Module | ExternalModule; (module instanceof ExternalModule ? externalExportAllModules : this.exportAllModules).push( @@ -651,7 +640,7 @@ export default class Module { if (ast) { this.esTreeAst = ast; } else { - this.esTreeAst = tryParse(this, this.graph.acornParser, this.graph.acornOptions); + this.esTreeAst = tryParse(this, this.graph.acornParser, this.options.acorn); for (const comment of this.comments) { if (!comment.block && SOURCEMAPPING_URL_RE.test(comment.text)) { this.alwaysRemovedCode.push([comment.start, comment.end]); @@ -683,7 +672,6 @@ export default class Module { addExport: this.addExport.bind(this), addImport: this.addImport.bind(this), addImportMeta: this.addImportMeta.bind(this), - annotations: (this.graph.treeshakingOptions && this.graph.treeshakingOptions.annotations)!, code, // Only needed for debugging deoptimizationTracker: this.graph.deoptimizationTracker, error: this.error.bind(this), @@ -702,19 +690,11 @@ export default class Module { module: this, moduleContext: this.context, nodeConstructors, - preserveModules: this.graph.preserveModules, - propertyReadSideEffects: (!this.graph.treeshakingOptions || - this.graph.treeshakingOptions.propertyReadSideEffects)!, + options: this.options, traceExport: this.getVariableForExportName.bind(this), traceVariable: this.traceVariable.bind(this), - treeshake: !!this.graph.treeshakingOptions, - tryCatchDeoptimization: (!this.graph.treeshakingOptions || - this.graph.treeshakingOptions.tryCatchDeoptimization)!, - unknownGlobalSideEffects: (!this.graph.treeshakingOptions || - this.graph.treeshakingOptions.unknownGlobalSideEffects)!, usesTopLevelAwait: false, - warn: this.warn.bind(this), - warnDeprecation: this.graph.warnDeprecation.bind(this.graph) + warn: this.warn.bind(this) }; this.scope = new ModuleScope(this.graph.scope, this.astContext); @@ -789,7 +769,7 @@ export default class Module { } warning.id = this.id; - this.graph.warn(warning); + this.options.onwarn(warning); } private addDynamicImport(node: ImportExpression) { @@ -905,7 +885,7 @@ export default class Module { for (const name of Object.keys(importDescription)) { const specifier = importDescription[name]; const id = this.resolvedIds[specifier.source].id; - specifier.module = this.graph.moduleById.get(id) as Module | ExternalModule; + specifier.module = this.graph.modulesById.get(id) as Module | ExternalModule; } } @@ -949,7 +929,7 @@ export default class Module { } private shimMissingExport(name: string): void { - this.graph.warn({ + this.options.onwarn({ code: 'SHIMMED_EXPORT', exporter: relativeId(this.id), exportName: name, diff --git a/src/ModuleLoader.ts b/src/ModuleLoader.ts index b8d37c610cb..b5a7bc03404 100644 --- a/src/ModuleLoader.ts +++ b/src/ModuleLoader.ts @@ -4,9 +4,8 @@ import Graph from './Graph'; import Module from './Module'; import { GetManualChunk, - IsExternal, - ModuleSideEffectsOption, - PureModulesOption, + HasModuleSideEffects, + NormalizedInputOptions, ResolvedId, ResolveIdResult, SourceDescription @@ -17,7 +16,6 @@ import { errEntryCannotBeExternal, errExternalSyntheticExports, errInternalIdCannotBeExternal, - errInvalidOption, errNamespaceConflict, error, errUnresolvedEntry, @@ -39,75 +37,8 @@ export interface UnresolvedModule { name: string | null; } -function normalizeRelativeExternalId(source: string, importer: string | undefined) { - return isRelative(source) - ? importer - ? resolve(importer, '..', source) - : resolve(source) - : source; -} - -function getIdMatcher>( - option: boolean | (string | RegExp)[] | ((id: string, ...args: T) => boolean | null | undefined) -): (id: string, ...args: T) => boolean { - if (option === true) { - return () => true; - } - if (typeof option === 'function') { - return (id, ...args) => (!id.startsWith('\0') && option(id, ...args)) || false; - } - if (option) { - const ids = new Set(); - const matchers: RegExp[] = []; - for (const value of option) { - if (value instanceof RegExp) { - matchers.push(value); - } else { - ids.add(value); - } - } - return (id => ids.has(id) || matchers.some(matcher => matcher.test(id))) as ( - id: string, - ...args: T - ) => boolean; - } - return () => false; -} - -function getHasModuleSideEffects( - moduleSideEffectsOption: ModuleSideEffectsOption, - pureExternalModules: PureModulesOption, - graph: Graph -): (id: string, external: boolean) => boolean { - if (typeof moduleSideEffectsOption === 'boolean') { - return () => moduleSideEffectsOption; - } - if (moduleSideEffectsOption === 'no-external') { - return (_id, external) => !external; - } - if (typeof moduleSideEffectsOption === 'function') { - return (id, external) => - !id.startsWith('\0') ? moduleSideEffectsOption(id, external) !== false : true; - } - if (Array.isArray(moduleSideEffectsOption)) { - const ids = new Set(moduleSideEffectsOption); - return id => ids.has(id); - } - if (moduleSideEffectsOption) { - graph.warn( - errInvalidOption( - 'treeshake.moduleSideEffects', - 'please use one of false, "no-external", a function or an array' - ) - ); - } - const isPureExternalModule = getIdMatcher(pureExternalModules); - return (id, external) => !(external && isPureExternalModule(id)); -} - export class ModuleLoader { - readonly isExternal: IsExternal; - private readonly hasModuleSideEffects: (id: string, external: boolean) => boolean; + private readonly hasModuleSideEffects: HasModuleSideEffects; private readonly indexedEntryModules: { index: number; module: Module }[] = []; private latestLoadModulesPromise: Promise = Promise.resolve(); private readonly manualChunkModules: Record = {}; @@ -116,18 +47,12 @@ export class ModuleLoader { constructor( private readonly graph: Graph, private readonly modulesById: Map, - private readonly pluginDriver: PluginDriver, - private readonly preserveSymlinks: boolean, - external: (string | RegExp)[] | IsExternal, - moduleSideEffects: ModuleSideEffectsOption, - pureExternalModules: PureModulesOption + private readonly options: NormalizedInputOptions, + private readonly pluginDriver: PluginDriver ) { - this.isExternal = getIdMatcher(external); - this.hasModuleSideEffects = getHasModuleSideEffects( - moduleSideEffects, - pureExternalModules, - graph - ); + this.hasModuleSideEffects = options.treeshake + ? options.treeshake.moduleSideEffects + : () => true; } async addEntryModules( @@ -226,9 +151,9 @@ export class ModuleLoader { skip: number | null = null ): Promise { return this.normalizeResolveIdResult( - this.isExternal(source, importer, false) + this.options.external(source, importer, false) ? false - : await resolveId(source, importer, this.preserveSymlinks, this.pluginDriver, skip), + : await resolveId(source, importer, this.options.preserveSymlinks, this.pluginDriver, skip), importer, source @@ -273,7 +198,9 @@ export class ModuleLoader { if (typeof sourceDescription.syntheticNamedExports === 'boolean') { module.syntheticNamedExports = sourceDescription.syntheticNamedExports; } - module.setSource(await transform(this.graph, sourceDescription, module)); + module.setSource( + await transform(sourceDescription, module, this.pluginDriver, this.options.onwarn) + ); } } @@ -356,6 +283,7 @@ export class ModuleLoader { const module: Module = new Module( this.graph, id, + this.options, moduleSideEffects, syntheticNamedExports, isEntry @@ -376,7 +304,7 @@ export class ModuleLoader { if (exportAllModule instanceof ExternalModule) continue; for (const name in exportAllModule!.exportsAll) { if (name in module.exportsAll) { - this.graph.warn(errNamespaceConflict(name, module, exportAllModule!)); + this.options.onwarn(errNamespaceConflict(name, module, exportAllModule!)); } else { module.exportsAll[name] = exportAllModule!.exportsAll[name]; } @@ -394,7 +322,7 @@ export class ModuleLoader { if (!this.modulesById.has(resolvedId.id)) { this.modulesById.set( resolvedId.id, - new ExternalModule(this.graph, resolvedId.id, resolvedId.moduleSideEffects) + new ExternalModule(this.options, resolvedId.id, resolvedId.moduleSideEffects) ); } @@ -423,7 +351,7 @@ export class ModuleLoader { if (isRelative(source)) { return error(errUnresolvedImport(source, importer)); } - this.graph.warn(errUnresolvedImportTreatedAsExternal(source, importer)); + this.options.onwarn(errUnresolvedImportTreatedAsExternal(source, importer)); return { external: true, id: source, @@ -432,7 +360,7 @@ export class ModuleLoader { }; } else { if (resolvedId.external && resolvedId.syntheticNamedExports) { - this.graph.warn(errExternalSyntheticExports(source, importer)); + this.options.onwarn(errExternalSyntheticExports(source, importer)); } } return resolvedId; @@ -446,7 +374,7 @@ export class ModuleLoader { const resolveIdResult = await resolveId( unresolvedId, importer, - this.preserveSymlinks, + this.options.preserveSymlinks, this.pluginDriver, null ); @@ -487,14 +415,14 @@ export class ModuleLoader { syntheticNamedExports = resolveIdResult.syntheticNamedExports; } } else { - if (this.isExternal(resolveIdResult, importer, true)) { + if (this.options.external(resolveIdResult, importer, true)) { external = true; } id = external ? normalizeRelativeExternalId(resolveIdResult, importer) : resolveIdResult; } } else { id = normalizeRelativeExternalId(source, importer); - if (resolveIdResult !== false && !this.isExternal(id, importer, true)) { + if (resolveIdResult !== false && !this.options.external(id, importer, true)) { return null; } external = true; @@ -545,3 +473,11 @@ export class ModuleLoader { ); } } + +function normalizeRelativeExternalId(source: string, importer: string | undefined): string { + return isRelative(source) + ? importer + ? resolve(importer, '..', source) + : resolve(source) + : source; +} diff --git a/src/ast/nodes/CallExpression.ts b/src/ast/nodes/CallExpression.ts index 3442e9211af..d440a3fb8e1 100644 --- a/src/ast/nodes/CallExpression.ts +++ b/src/ast/nodes/CallExpression.ts @@ -1,4 +1,5 @@ import MagicString from 'magic-string'; +import { NormalizedTreeshakingOptions } from '../../rollup/types'; import { BLANK } from '../../utils/blank'; import { findFirstOccurrenceOutsideComment, @@ -153,7 +154,11 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt for (const argument of this.arguments) { if (argument.hasEffects(context)) return true; } - if (this.context.annotations && this.annotatedPure) return false; + if ( + (this.context.options.treeshake as NormalizedTreeshakingOptions).annotations && + this.annotatedPure + ) + return false; return ( this.callee.hasEffects(context) || this.callee.hasEffectsWhenCalledAtPath(EMPTY_PATH, this.callOptions, context) diff --git a/src/ast/nodes/Identifier.ts b/src/ast/nodes/Identifier.ts index 4967d548deb..b9545a8aa12 100644 --- a/src/ast/nodes/Identifier.ts +++ b/src/ast/nodes/Identifier.ts @@ -1,5 +1,6 @@ import isReference from 'is-reference'; import MagicString from 'magic-string'; +import { NormalizedTreeshakingOptions } from '../../rollup/types'; import { BLANK } from '../../utils/blank'; import { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers'; import { CallOptions } from '../CallOptions'; @@ -101,7 +102,7 @@ export default class Identifier extends NodeBase implements PatternNode { hasEffects(): boolean { return ( - this.context.unknownGlobalSideEffects && + (this.context.options.treeshake as NormalizedTreeshakingOptions).unknownGlobalSideEffects && this.variable instanceof GlobalVariable && this.variable.hasEffectsWhenAccessedAtPath(EMPTY_PATH) ); diff --git a/src/ast/nodes/ImportExpression.ts b/src/ast/nodes/ImportExpression.ts index 42f27d2b17d..b2218be2487 100644 --- a/src/ast/nodes/ImportExpression.ts +++ b/src/ast/nodes/ImportExpression.ts @@ -1,7 +1,7 @@ import MagicString from 'magic-string'; import ExternalModule from '../../ExternalModule'; import Module from '../../Module'; -import { OutputOptions } from '../../rollup/types'; +import { NormalizedOutputOptions } from '../../rollup/types'; import { findFirstOccurrenceOutsideComment, RenderOptions } from '../../utils/renderHelpers'; import { INTEROP_NAMESPACE_VARIABLE } from '../../utils/variableNames'; import { InclusionContext } from '../ExecutionContext'; @@ -67,7 +67,7 @@ export default class Import extends NodeBase { code: MagicString, resolution: string, namespaceExportName: string | false, - options: OutputOptions + options: NormalizedOutputOptions ) { code.overwrite(this.source.start, this.source.end, resolution); if (namespaceExportName) { diff --git a/src/ast/nodes/MemberExpression.ts b/src/ast/nodes/MemberExpression.ts index 65d9880ba83..3aa93c3abc5 100644 --- a/src/ast/nodes/MemberExpression.ts +++ b/src/ast/nodes/MemberExpression.ts @@ -1,4 +1,5 @@ import MagicString from 'magic-string'; +import { NormalizedTreeshakingOptions } from '../../rollup/types'; import { BLANK } from '../../utils/blank'; import relativeId from '../../utils/relativeId'; import { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers'; @@ -175,7 +176,7 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE return ( this.property.hasEffects(context) || this.object.hasEffects(context) || - (this.context.propertyReadSideEffects && + ((this.context.options.treeshake as NormalizedTreeshakingOptions).propertyReadSideEffects && this.object.hasEffectsWhenAccessedAtPath([this.propertyKey!], context)) ); } diff --git a/src/ast/nodes/MetaProperty.ts b/src/ast/nodes/MetaProperty.ts index ff169fae919..a90cbf0b7f8 100644 --- a/src/ast/nodes/MetaProperty.ts +++ b/src/ast/nodes/MetaProperty.ts @@ -1,5 +1,6 @@ import MagicString from 'magic-string'; import { InternalModuleFormat } from '../../rollup/types'; +import { warnDeprecation } from '../../utils/error'; import { dirname, normalize, relative } from '../../utils/path'; import { PluginDriver } from '../../utils/PluginDriver'; import { ObjectPathKey } from '../utils/PathTracker'; @@ -78,16 +79,18 @@ export default class MetaProperty extends NodeBase { referenceId = metaProperty.substr(FILE_PREFIX.length); fileName = outputPluginDriver.getFileName(referenceId); } else if (metaProperty.startsWith(ASSET_PREFIX)) { - this.context.warnDeprecation( + warnDeprecation( `Using the "${ASSET_PREFIX}" prefix to reference files is deprecated. Use the "${FILE_PREFIX}" prefix instead.`, - true + true, + this.context.options ); assetReferenceId = metaProperty.substr(ASSET_PREFIX.length); fileName = outputPluginDriver.getFileName(assetReferenceId); } else { - this.context.warnDeprecation( + warnDeprecation( `Using the "${CHUNK_PREFIX}" prefix to reference files is deprecated. Use the "${FILE_PREFIX}" prefix instead.`, - true + true, + this.context.options ); chunkReferenceId = metaProperty.substr(CHUNK_PREFIX.length); fileName = outputPluginDriver.getFileName(chunkReferenceId); diff --git a/src/ast/nodes/NewExpression.ts b/src/ast/nodes/NewExpression.ts index d7acdfd276d..280df11bc13 100644 --- a/src/ast/nodes/NewExpression.ts +++ b/src/ast/nodes/NewExpression.ts @@ -1,3 +1,4 @@ +import { NormalizedTreeshakingOptions } from '../../rollup/types'; import { CallOptions } from '../CallOptions'; import { HasEffectsContext } from '../ExecutionContext'; import { EMPTY_PATH, ObjectPath, UNKNOWN_PATH } from '../utils/PathTracker'; @@ -24,7 +25,11 @@ export default class NewExpression extends NodeBase { for (const argument of this.arguments) { if (argument.hasEffects(context)) return true; } - if (this.context.annotations && this.annotatedPure) return false; + if ( + (this.context.options.treeshake as NormalizedTreeshakingOptions).annotations && + this.annotatedPure + ) + return false; return ( this.callee.hasEffects(context) || this.callee.hasEffectsWhenCalledAtPath(EMPTY_PATH, this.callOptions, context) diff --git a/src/ast/nodes/TryStatement.ts b/src/ast/nodes/TryStatement.ts index f0a0b2be236..0727b671c89 100644 --- a/src/ast/nodes/TryStatement.ts +++ b/src/ast/nodes/TryStatement.ts @@ -1,3 +1,4 @@ +import { NormalizedTreeshakingOptions } from '../../rollup/types'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; import BlockStatement from './BlockStatement'; import CatchClause from './CatchClause'; @@ -14,7 +15,7 @@ export default class TryStatement extends StatementBase { hasEffects(context: HasEffectsContext): boolean { return ( - (this.context.tryCatchDeoptimization + ((this.context.options.treeshake as NormalizedTreeshakingOptions).tryCatchDeoptimization ? this.block.body.length > 0 : this.block.hasEffects(context)) || (this.finalizer !== null && this.finalizer.hasEffects(context)) @@ -22,13 +23,15 @@ export default class TryStatement extends StatementBase { } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { + const tryCatchDeoptimization = (this.context.options.treeshake as NormalizedTreeshakingOptions) + ?.tryCatchDeoptimization; const { brokenFlow } = context; - if (!this.directlyIncluded || !this.context.tryCatchDeoptimization) { + if (!this.directlyIncluded || !tryCatchDeoptimization) { this.included = true; this.directlyIncluded = true; this.block.include( context, - this.context.tryCatchDeoptimization ? INCLUDE_PARAMETERS : includeChildrenRecursively + tryCatchDeoptimization ? INCLUDE_PARAMETERS : includeChildrenRecursively ); context.brokenFlow = brokenFlow; } diff --git a/src/ast/variables/NamespaceVariable.ts b/src/ast/variables/NamespaceVariable.ts index 0b74e3c6089..eab2b002885 100644 --- a/src/ast/variables/NamespaceVariable.ts +++ b/src/ast/variables/NamespaceVariable.ts @@ -50,7 +50,7 @@ export default class NamespaceVariable extends Variable { } } this.mergedNamespaces = this.context.includeAndGetAdditionalMergedNamespaces(); - if (this.context.preserveModules) { + if (this.context.options.preserveModules) { for (const memberName in memberVariables) memberVariables[memberName].include(); } else { for (const memberName in memberVariables) diff --git a/src/finalisers/amd.ts b/src/finalisers/amd.ts index 2a2ebcefa8b..1f930e8324c 100644 --- a/src/finalisers/amd.ts +++ b/src/finalisers/amd.ts @@ -1,5 +1,5 @@ import { Bundle as MagicStringBundle } from 'magic-string'; -import { OutputOptions } from '../rollup/types'; +import { NormalizedOutputOptions } from '../rollup/types'; import { INTEROP_NAMESPACE_VARIABLE } from '../utils/variableNames'; import { FinaliserOptions } from './index'; import { compactEsModuleExport, esModuleExport } from './shared/esModuleExport'; @@ -33,7 +33,7 @@ export default function amd( varOrConst, warn }: FinaliserOptions, - options: OutputOptions + options: NormalizedOutputOptions ) { warnOnBuiltins(warn, dependencies); @@ -63,9 +63,8 @@ export default function amd( (amdOptions.id ? `'${amdOptions.id}',${_}` : ``) + (deps.length ? `[${deps.join(`,${_}`)}],${_}` : ``); - const useStrict = options.strict !== false ? `${_}'use strict';` : ``; - const define = amdOptions.define || 'define'; - const wrapperStart = `${define}(${params}function${_}(${args.join( + const useStrict = options.strict ? `${_}'use strict';` : ``; + const wrapperStart = `${amdOptions.define}(${params}function${_}(${args.join( `,${_}` )})${_}{${useStrict}${n}${n}`; @@ -75,7 +74,7 @@ export default function amd( magicString.prepend(interopBlock + n + n); } if (accessedGlobals.has(INTEROP_NAMESPACE_VARIABLE)) { - magicString.prepend(getInteropNamespace(_, n, t, options.externalLiveBindings !== false)); + magicString.prepend(getInteropNamespace(_, n, t, options.externalLiveBindings)); } if (intro) magicString.prepend(intro); diff --git a/src/finalisers/cjs.ts b/src/finalisers/cjs.ts index 08475600fc0..4539f2bd24d 100644 --- a/src/finalisers/cjs.ts +++ b/src/finalisers/cjs.ts @@ -1,5 +1,5 @@ import { Bundle as MagicStringBundle } from 'magic-string'; -import { OutputOptions } from '../rollup/types'; +import { NormalizedOutputOptions } from '../rollup/types'; import { INTEROP_DEFAULT_VARIABLE, INTEROP_NAMESPACE_VARIABLE } from '../utils/variableNames'; import { FinaliserOptions } from './index'; import { compactEsModuleExport, esModuleExport } from './shared/esModuleExport'; @@ -20,7 +20,7 @@ export default function cjs( outro, varOrConst }: FinaliserOptions, - options: OutputOptions + options: NormalizedOutputOptions ) { const n = options.compact ? '' : '\n'; const _ = options.compact ? '' : ' '; @@ -32,7 +32,6 @@ export default function cjs( : ''); let needsInterop = false; - const interop = options.interop !== false; let importBlock: string; let definingVariable = false; @@ -58,7 +57,7 @@ export default function cjs( options.compact && definingVariable ? ',' : `${importBlock ? `;${n}` : ''}${varOrConst} `; definingVariable = true; - if (!interop || isChunk || !exportsDefault || !namedExportsMode) { + if (!options.interop || isChunk || !exportsDefault || !namedExportsMode) { importBlock += `${name}${_}=${_}require('${id}')`; } else { needsInterop = true; @@ -80,7 +79,7 @@ export default function cjs( `?${_}${ex}['default']${_}:${_}${ex}${options.compact ? '' : '; '}}${n}${n}`; } if (accessedGlobals.has(INTEROP_NAMESPACE_VARIABLE)) { - intro += getInteropNamespace(_, n, t, options.externalLiveBindings !== false); + intro += getInteropNamespace(_, n, t, options.externalLiveBindings); } if (importBlock) intro += importBlock + n + n; diff --git a/src/finalisers/es.ts b/src/finalisers/es.ts index d98af4cb012..2f3f3977c33 100644 --- a/src/finalisers/es.ts +++ b/src/finalisers/es.ts @@ -1,12 +1,12 @@ import { Bundle as MagicStringBundle } from 'magic-string'; import { ChunkDependencies, ChunkExports, ImportSpecifier, ReexportSpecifier } from '../Chunk'; -import { OutputOptions } from '../rollup/types'; +import { NormalizedOutputOptions } from '../rollup/types'; import { FinaliserOptions } from './index'; export default function es( magicString: MagicStringBundle, { intro, outro, dependencies, exports, varOrConst }: FinaliserOptions, - options: OutputOptions + options: NormalizedOutputOptions ) { const _ = options.compact ? '' : ' '; const n = options.compact ? '' : '\n'; diff --git a/src/finalisers/iife.ts b/src/finalisers/iife.ts index 5f5aec49a33..06c1e2e5121 100644 --- a/src/finalisers/iife.ts +++ b/src/finalisers/iife.ts @@ -1,5 +1,5 @@ import { Bundle as MagicStringBundle } from 'magic-string'; -import { OutputOptions } from '../rollup/types'; +import { NormalizedOutputOptions } from '../rollup/types'; import { error } from '../utils/error'; import { isLegal } from '../utils/identifierHelpers'; import { FinaliserOptions } from './index'; @@ -25,7 +25,7 @@ export default function iife( varOrConst, warn }: FinaliserOptions, - options: OutputOptions + options: NormalizedOutputOptions ) { const _ = options.compact ? '' : ' '; const n = options.compact ? '' : '\n'; @@ -64,7 +64,7 @@ export default function iife( } } - const useStrict = options.strict !== false ? `${t}'use strict';${n}${n}` : ``; + const useStrict = options.strict ? `${t}'use strict';${n}${n}` : ``; let wrapperIntro = `(function${_}(${args.join(`,${_}`)})${_}{${n}${useStrict}`; @@ -101,8 +101,5 @@ export default function iife( if (exportBlock) magicString.append(n + n + exportBlock); if (outro) magicString.append(outro); - return magicString - .indent(t) - .prepend(wrapperIntro) - .append(wrapperOutro); + return magicString.indent(t).prepend(wrapperIntro).append(wrapperOutro); } diff --git a/src/finalisers/index.ts b/src/finalisers/index.ts index aa643bf6be2..adbae5ea39d 100644 --- a/src/finalisers/index.ts +++ b/src/finalisers/index.ts @@ -1,6 +1,6 @@ import { Bundle as MagicStringBundle } from 'magic-string'; import { ChunkDependencies, ChunkExports } from '../Chunk'; -import { OutputOptions, RollupWarning } from '../rollup/types'; +import { NormalizedOutputOptions, RollupWarning } from '../rollup/types'; import amd from './amd'; import cjs from './cjs'; import es from './es'; @@ -26,7 +26,7 @@ export interface FinaliserOptions { export type Finaliser = ( magicString: MagicStringBundle, finaliserOptions: FinaliserOptions, - options: OutputOptions + options: NormalizedOutputOptions ) => MagicStringBundle; export default { system, amd, cjs, es, iife, umd } as { diff --git a/src/finalisers/shared/getInteropBlock.ts b/src/finalisers/shared/getInteropBlock.ts index 0e458e54b9d..27456f23d79 100644 --- a/src/finalisers/shared/getInteropBlock.ts +++ b/src/finalisers/shared/getInteropBlock.ts @@ -1,16 +1,16 @@ import { ModuleDeclarationDependency } from '../../Chunk'; -import { OutputOptions } from '../../rollup/types'; +import { NormalizedOutputOptions } from '../../rollup/types'; export default function getInteropBlock( dependencies: ModuleDeclarationDependency[], - options: OutputOptions, + options: NormalizedOutputOptions, varOrConst: string ) { const _ = options.compact ? '' : ' '; return dependencies .map(({ name, exportsNames, exportsDefault, namedExportsMode }) => { - if (!namedExportsMode || !exportsDefault || options.interop === false) return null; + if (!(namedExportsMode && exportsDefault && options.interop)) return null; if (exportsNames) { return ( diff --git a/src/finalisers/shared/setupNamespace.ts b/src/finalisers/shared/setupNamespace.ts index 0f1c1425908..a2c3e03e8c3 100644 --- a/src/finalisers/shared/setupNamespace.ts +++ b/src/finalisers/shared/setupNamespace.ts @@ -4,15 +4,12 @@ import { property } from './sanitize'; export default function setupNamespace( name: string, root: string, - globals: GlobalsOption | undefined, + globals: GlobalsOption, compact: boolean | undefined ) { - const parts = name.split('.'); - if (globals) { - parts[0] = (typeof globals === 'function' ? globals(parts[0]) : globals[parts[0]]) || parts[0]; - } - const _ = compact ? '' : ' '; + const parts = name.split('.'); + parts[0] = (typeof globals === 'function' ? globals(parts[0]) : globals[parts[0]]) || parts[0]; parts.pop(); let acc = root; @@ -28,15 +25,13 @@ export default function setupNamespace( export function assignToDeepVariable( deepName: string, root: string, - globals: GlobalsOption | undefined, + globals: GlobalsOption, compact: boolean | undefined, assignment: string ): string { const _ = compact ? '' : ' '; const parts = deepName.split('.'); - if (globals) { - parts[0] = (typeof globals === 'function' ? globals(parts[0]) : globals[parts[0]]) || parts[0]; - } + parts[0] = (typeof globals === 'function' ? globals(parts[0]) : globals[parts[0]]) || parts[0]; const last = parts.pop(); let acc = root; diff --git a/src/finalisers/shared/warnOnBuiltins.ts b/src/finalisers/shared/warnOnBuiltins.ts index ea4be210a9a..2fe1c8015e8 100644 --- a/src/finalisers/shared/warnOnBuiltins.ts +++ b/src/finalisers/shared/warnOnBuiltins.ts @@ -25,8 +25,6 @@ const builtins = { zlib: true }; -// Creating a browser chunk that depends on Node.js built-in modules ('util'). You might need to include https://www.npmjs.com/package/rollup-plugin-node-builtins - export default function warnOnBuiltins( warn: (warning: RollupWarning) => void, dependencies: ChunkDependencies @@ -45,7 +43,7 @@ export default function warnOnBuiltins( warn({ code: 'MISSING_NODE_BUILTINS', - message: `Creating a browser bundle that depends on Node.js built-in ${detail}. You might need to include https://www.npmjs.com/package/rollup-plugin-node-builtins`, + message: `Creating a browser bundle that depends on Node.js built-in ${detail}. You might need to include https://github.com/ionic-team/rollup-plugin-node-polyfills`, modules: externalBuiltins }); } diff --git a/src/finalisers/system.ts b/src/finalisers/system.ts index 8c8209d250d..dad5ba9eb9a 100644 --- a/src/finalisers/system.ts +++ b/src/finalisers/system.ts @@ -1,6 +1,6 @@ import { Bundle as MagicStringBundle } from 'magic-string'; import { ChunkExports, ModuleDeclarations } from '../Chunk'; -import { OutputOptions } from '../rollup/types'; +import { NormalizedOutputOptions } from '../rollup/types'; import { MISSING_EXPORT_SHIM_VARIABLE } from '../utils/variableNames'; import { FinaliserOptions } from './index'; @@ -101,7 +101,7 @@ export default function system( usesTopLevelAwait, varOrConst }: FinaliserOptions, - options: OutputOptions + options: NormalizedOutputOptions ) { const n = options.compact ? '' : '\n'; const _ = options.compact ? '' : ' '; diff --git a/src/finalisers/umd.ts b/src/finalisers/umd.ts index 93c68a5ea27..ec6719e0709 100644 --- a/src/finalisers/umd.ts +++ b/src/finalisers/umd.ts @@ -1,5 +1,5 @@ import { Bundle as MagicStringBundle } from 'magic-string'; -import { GlobalsOption, OutputOptions } from '../rollup/types'; +import { NormalizedOutputOptions } from '../rollup/types'; import { error } from '../utils/error'; import { FinaliserOptions } from './index'; import { compactEsModuleExport, esModuleExport } from './shared/esModuleExport'; @@ -35,7 +35,7 @@ export default function umd( varOrConst, warn }: FinaliserOptions, - options: OutputOptions + options: NormalizedOutputOptions ) { const _ = options.compact ? '' : ' '; const n = options.compact ? '' : '\n'; @@ -59,15 +59,15 @@ export default function umd( const globalDeps = trimmedImports.map(module => globalProp(module.globalName, globalVar)); const factoryArgs = trimmedImports.map(m => m.name); - if (namedExportsMode && (hasExports || options.noConflict === true)) { + if (namedExportsMode && (hasExports || options.noConflict)) { amdDeps.unshift(`'exports'`); cjsDeps.unshift(`exports`); globalDeps.unshift( assignToDeepVariable( options.name!, globalVar, - options.globals as GlobalsOption, - options.compact!, + options.globals, + options.compact, `${options.extend ? `${globalProp(options.name!, globalVar)}${_}||${_}` : ''}{}` ) ); @@ -75,19 +75,17 @@ export default function umd( factoryArgs.unshift('exports'); } - const amdOptions = options.amd || {}; - const amdParams = - (amdOptions.id ? `'${amdOptions.id}',${_}` : ``) + + (options.amd.id ? `'${options.amd.id}',${_}` : ``) + (amdDeps.length ? `[${amdDeps.join(`,${_}`)}],${_}` : ``); - const define = amdOptions.define || 'define'; + const define = options.amd.define; const cjsExport = !namedExportsMode && hasExports ? `module.exports${_}=${_}` : ``; - const useStrict = options.strict !== false ? `${_}'use strict';${n}` : ``; + const useStrict = options.strict ? `${_}'use strict';${n}` : ``; let iifeExport; - if (options.noConflict === true) { + if (options.noConflict) { const noConflictExportsVar = options.compact ? 'e' : 'exports'; let factory; @@ -120,8 +118,8 @@ export default function umd( iifeExport = assignToDeepVariable( options.name!, globalVar, - options.globals as GlobalsOption, - options.compact!, + options.globals, + options.compact, iifeExport ); } @@ -167,9 +165,5 @@ export default function umd( magicString.append(n + n + (options.compact ? compactEsModuleExport : esModuleExport)); if (outro) magicString.append(outro); - return magicString - .trim() - .indent(t) - .append(wrapperOutro) - .prepend(wrapperIntro); + return magicString.trim().indent(t).append(wrapperOutro).prepend(wrapperIntro); } diff --git a/src/rollup/rollup.ts b/src/rollup/rollup.ts index 58bdca2c922..1bf8bbdaea9 100644 --- a/src/rollup/rollup.ts +++ b/src/rollup/rollup.ts @@ -1,138 +1,30 @@ import { version as rollupVersion } from 'package.json'; +import Bundle from '../Bundle'; import Chunk from '../Chunk'; import Graph from '../Graph'; -import { createAddons } from '../utils/addons'; -import { assignChunkIds } from '../utils/assignChunkIds'; -import commondir from '../utils/commondir'; +import { ensureArray } from '../utils/ensureArray'; import { errCannotEmitFromOptionsHook, error } from '../utils/error'; import { writeFile } from '../utils/fs'; -import getExportMode from '../utils/getExportMode'; -import { - ensureArray, - GenericConfigObject, - parseInputOptions, - parseOutputOptions -} from '../utils/parseOptions'; -import { basename, dirname, isAbsolute, resolve } from '../utils/path'; +import { normalizeInputOptions } from '../utils/options/normalizeInputOptions'; +import { normalizeOutputOptions } from '../utils/options/normalizeOutputOptions'; +import { GenericConfigObject } from '../utils/options/options'; +import { basename, dirname, resolve } from '../utils/path'; import { PluginDriver } from '../utils/PluginDriver'; import { ANONYMOUS_OUTPUT_PLUGIN_PREFIX, ANONYMOUS_PLUGIN_PREFIX } from '../utils/pluginUtils'; import { SOURCEMAPPING_URL } from '../utils/sourceMappingURL'; import { getTimings, initialiseTimers, timeEnd, timeStart } from '../utils/timers'; import { - InputOptions, + NormalizedInputOptions, + NormalizedOutputOptions, OutputAsset, - OutputBundle, - OutputBundleWithPlaceholders, OutputChunk, OutputOptions, Plugin, RollupBuild, RollupOutput, - RollupWatcher, - WarningHandler + RollupWatcher } from './types'; -function getAbsoluteEntryModulePaths(chunks: Chunk[]): string[] { - const absoluteEntryModulePaths: string[] = []; - for (const chunk of chunks) { - for (const entryModule of chunk.entryModules) { - if (isAbsolute(entryModule.id)) { - absoluteEntryModulePaths.push(entryModule.id); - } - } - } - return absoluteEntryModulePaths; -} - -function applyOptionHook(inputOptions: InputOptions, plugin: Plugin) { - if (plugin.options) - return plugin.options.call({ meta: { rollupVersion } }, inputOptions) || inputOptions; - - return inputOptions; -} - -function normalizePlugins(rawPlugins: any, anonymousPrefix: string): Plugin[] { - const plugins = ensureArray(rawPlugins); - for (let pluginIndex = 0; pluginIndex < plugins.length; pluginIndex++) { - const plugin = plugins[pluginIndex]; - if (!plugin.name) { - plugin.name = `${anonymousPrefix}${pluginIndex + 1}`; - } - } - return plugins; -} - -function getInputOptions(rawInputOptions: GenericConfigObject): InputOptions { - if (!rawInputOptions) { - throw new Error('You must supply an options object to rollup'); - } - let inputOptions = parseInputOptions(rawInputOptions); - inputOptions = inputOptions.plugins!.reduce(applyOptionHook, inputOptions); - inputOptions.plugins = normalizePlugins(inputOptions.plugins!, ANONYMOUS_PLUGIN_PREFIX); - - if (inputOptions.inlineDynamicImports) { - if (inputOptions.preserveModules) - return error({ - code: 'INVALID_OPTION', - message: `"preserveModules" does not support the "inlineDynamicImports" option.` - }); - if (inputOptions.manualChunks) - return error({ - code: 'INVALID_OPTION', - message: '"manualChunks" option is not supported for "inlineDynamicImports".' - }); - if ( - (inputOptions.input instanceof Array && inputOptions.input.length > 1) || - (typeof inputOptions.input === 'object' && Object.keys(inputOptions.input).length > 1) - ) - return error({ - code: 'INVALID_OPTION', - message: 'Multiple inputs are not supported for "inlineDynamicImports".' - }); - } else if (inputOptions.preserveModules) { - if (inputOptions.manualChunks) - return error({ - code: 'INVALID_OPTION', - message: '"preserveModules" does not support the "manualChunks" option.' - }); - if (inputOptions.preserveEntrySignatures === false) - return error({ - code: 'INVALID_OPTION', - message: '"preserveModules" does not support setting "preserveEntrySignatures" to "false".' - }); - } - - return inputOptions; -} - -function assignChunksToBundle( - chunks: Chunk[], - outputBundle: OutputBundleWithPlaceholders -): OutputBundle { - for (let i = 0; i < chunks.length; i++) { - const chunk = chunks[i]; - const facadeModule = chunk.facadeModule; - - outputBundle[chunk.id!] = { - code: undefined as any, - dynamicImports: chunk.getDynamicImportIds(), - exports: chunk.getExportNames(), - facadeModuleId: facadeModule && facadeModule.id, - fileName: chunk.id, - imports: chunk.getImportIds(), - isDynamicEntry: chunk.isDynamicEntry, - isEntry: facadeModule !== null && facadeModule.isEntryPoint, - map: undefined, - modules: chunk.renderedModules, - get name() { - return chunk.getChunkName(); - }, - type: 'chunk' - } as OutputChunk; - } - return outputBundle as OutputBundle; -} - export default function rollup(rawInputOptions: GenericConfigObject): Promise { return rollupInternal(rawInputOptions, null); } @@ -141,10 +33,10 @@ export async function rollupInternal( rawInputOptions: GenericConfigObject, watcher: RollupWatcher | null ): Promise { - const inputOptions = getInputOptions(rawInputOptions); + const { options: inputOptions, unsetOptions } = getInputOptions(rawInputOptions); initialiseTimers(inputOptions); - const graph = new Graph(inputOptions, watcher); + const graph = new Graph(inputOptions, unsetOptions, watcher); // remove the cache option from the memory after graph creation (cache is not used anymore) const useCache = rawInputOptions.cache !== false; @@ -156,11 +48,7 @@ export async function rollupInternal( let chunks: Chunk[]; try { await graph.pluginDriver.hookParallel('buildStart', [inputOptions]); - chunks = await graph.build( - inputOptions.input as string | string[] | Record, - inputOptions.manualChunks, - inputOptions.inlineDynamicImports! - ); + chunks = await graph.build(); } catch (err) { const watchFiles = Object.keys(graph.watchFiles); if (watchFiles.length > 0) { @@ -174,115 +62,37 @@ export async function rollupInternal( timeEnd('BUILD', 1); - function getOutputOptionsAndPluginDriver( - rawOutputOptions: GenericConfigObject - ): { outputOptions: OutputOptions; outputPluginDriver: PluginDriver } { - if (!rawOutputOptions) { - throw new Error('You must supply an options object'); - } - const outputPluginDriver = graph.pluginDriver.createOutputPluginDriver( - normalizePlugins(rawOutputOptions.plugins, ANONYMOUS_OUTPUT_PLUGIN_PREFIX) - ); - - return { - outputOptions: normalizeOutputOptions( - inputOptions as GenericConfigObject, - rawOutputOptions, - chunks.length > 1, - outputPluginDriver - ), - outputPluginDriver - }; - } - - async function generate( - outputOptions: OutputOptions, - isWrite: boolean, - outputPluginDriver: PluginDriver - ): Promise { - timeStart('GENERATE', 1); - - if (outputOptions.dynamicImportFunction) { - graph.warnDeprecation( - `The "output.dynamicImportFunction" option is deprecated. Use the "renderDynamicImport" plugin hook instead.`, - false - ); - } - const assetFileNames = outputOptions.assetFileNames || 'assets/[name]-[hash][extname]'; - const inputBase = commondir(getAbsoluteEntryModulePaths(chunks)); - const outputBundleWithPlaceholders: OutputBundleWithPlaceholders = Object.create(null); - outputPluginDriver.setOutputBundle(outputBundleWithPlaceholders, assetFileNames); - let outputBundle; - - try { - await outputPluginDriver.hookParallel('renderStart', [outputOptions, inputOptions]); - const addons = await createAddons(outputOptions, outputPluginDriver); - for (const chunk of chunks) { - chunk.generateExports(outputOptions); - if (inputOptions.preserveModules || (chunk.facadeModule && chunk.facadeModule.isEntryPoint)) - chunk.exportMode = getExportMode(chunk, outputOptions, chunk.facadeModule!.id); - } - for (const chunk of chunks) { - chunk.preRender(outputOptions, inputBase, outputPluginDriver); - } - assignChunkIds( - chunks, - inputOptions, - outputOptions, - inputBase, - addons, - outputBundleWithPlaceholders, - outputPluginDriver - ); - outputBundle = assignChunksToBundle(chunks, outputBundleWithPlaceholders); - - await Promise.all( - chunks.map(chunk => { - const outputChunk = outputBundleWithPlaceholders[chunk.id!] as OutputChunk; - return chunk - .render(outputOptions, addons, outputChunk, outputPluginDriver) - .then(rendered => { - outputChunk.code = rendered.code; - outputChunk.map = rendered.map; - }); - }) - ); - } catch (error) { - await outputPluginDriver.hookParallel('renderError', [error]); - throw error; - } - await outputPluginDriver.hookSeq('generateBundle', [outputOptions, outputBundle, isWrite]); - for (const key of Object.keys(outputBundle)) { - const file = outputBundle[key] as any; - if (!file.type) { - graph.warnDeprecation( - 'A plugin is directly adding properties to the bundle object in the "generateBundle" hook. This is deprecated and will be removed in a future Rollup version, please use "this.emitFile" instead.', - true - ); - file.type = 'asset'; - } - } - outputPluginDriver.finaliseAssets(); - - timeEnd('GENERATE', 1); - return outputBundle; - } - - const cache = useCache ? graph.getCache() : undefined; const result: RollupBuild = { - cache: cache!, - generate: (rawOutputOptions: OutputOptions) => { - const { outputOptions, outputPluginDriver } = getOutputOptionsAndPluginDriver( - rawOutputOptions as GenericConfigObject + cache: useCache ? graph.getCache() : undefined, + async generate(rawOutputOptions: OutputOptions) { + const { + options: outputOptions, + outputPluginDriver, + unsetOptions + } = getOutputOptionsAndPluginDriver( + rawOutputOptions as GenericConfigObject, + graph.pluginDriver, + inputOptions ); - return generate(outputOptions, false, outputPluginDriver).then(result => - createOutput(result) + const bundle = new Bundle( + outputOptions, + unsetOptions, + inputOptions, + outputPluginDriver, + chunks ); + return createOutput(await bundle.generate(false)); }, watchFiles: Object.keys(graph.watchFiles), - write: (rawOutputOptions: OutputOptions) => { - const { outputOptions, outputPluginDriver } = getOutputOptionsAndPluginDriver( - rawOutputOptions as GenericConfigObject + async write(rawOutputOptions: OutputOptions) { + const { + options: outputOptions, + outputPluginDriver, + unsetOptions + } = getOutputOptionsAndPluginDriver( + rawOutputOptions as GenericConfigObject, + graph.pluginDriver, + inputOptions ); if (!outputOptions.dir && !outputOptions.file) { return error({ @@ -290,33 +100,101 @@ export async function rollupInternal( message: 'You must specify "output.file" or "output.dir" for the build.' }); } - return generate(outputOptions, true, outputPluginDriver).then(async bundle => { - await Promise.all( - Object.keys(bundle).map(chunkId => writeOutputFile(bundle[chunkId], outputOptions)) - ); - await outputPluginDriver.hookParallel('writeBundle', [outputOptions, bundle]); - return createOutput(bundle); - }); + const bundle = new Bundle( + outputOptions, + unsetOptions, + inputOptions, + outputPluginDriver, + chunks + ); + const generated = await bundle.generate(true); + await Promise.all( + Object.keys(generated).map(chunkId => writeOutputFile(generated[chunkId], outputOptions)) + ); + await outputPluginDriver.hookParallel('writeBundle', [outputOptions, generated]); + return createOutput(generated); } }; - if (inputOptions.perf === true) result.getTimings = getTimings; + if (inputOptions.perf) result.getTimings = getTimings; return result; } -enum SortingFileType { - ENTRY_CHUNK = 0, - SECONDARY_CHUNK = 1, - ASSET = 2 +function getInputOptions( + rawInputOptions: GenericConfigObject +): { options: NormalizedInputOptions; unsetOptions: Set } { + if (!rawInputOptions) { + throw new Error('You must supply an options object to rollup'); + } + const rawPlugins = ensureArray(rawInputOptions.plugins) as Plugin[]; + const { options, unsetOptions } = normalizeInputOptions( + rawPlugins.reduce(applyOptionHook, rawInputOptions) + ); + normalizePlugins(options.plugins, ANONYMOUS_PLUGIN_PREFIX); + return { options, unsetOptions }; } -function getSortingFileType(file: OutputAsset | OutputChunk): SortingFileType { - if (file.type === 'asset') { - return SortingFileType.ASSET; +function applyOptionHook(inputOptions: GenericConfigObject, plugin: Plugin): GenericConfigObject { + if (plugin.options) + return ( + (plugin.options.call({ meta: { rollupVersion } }, inputOptions) as GenericConfigObject) || + inputOptions + ); + + return inputOptions; +} + +function normalizePlugins(plugins: Plugin[], anonymousPrefix: string): void { + for (let pluginIndex = 0; pluginIndex < plugins.length; pluginIndex++) { + const plugin = plugins[pluginIndex]; + if (!plugin.name) { + plugin.name = `${anonymousPrefix}${pluginIndex + 1}`; + } } - if (file.isEntry) { - return SortingFileType.ENTRY_CHUNK; +} + +function getOutputOptionsAndPluginDriver( + rawOutputOptions: GenericConfigObject, + inputPluginDriver: PluginDriver, + inputOptions: NormalizedInputOptions +): { + options: NormalizedOutputOptions; + outputPluginDriver: PluginDriver; + unsetOptions: Set; +} { + if (!rawOutputOptions) { + throw new Error('You must supply an options object'); } - return SortingFileType.SECONDARY_CHUNK; + const rawPlugins = ensureArray(rawOutputOptions.plugins) as Plugin[]; + normalizePlugins(rawPlugins, ANONYMOUS_OUTPUT_PLUGIN_PREFIX); + const outputPluginDriver = inputPluginDriver.createOutputPluginDriver(rawPlugins); + + return { + ...getOutputOptions(inputOptions, rawOutputOptions, outputPluginDriver), + outputPluginDriver + }; +} + +function getOutputOptions( + inputOptions: NormalizedInputOptions, + rawOutputOptions: GenericConfigObject, + outputPluginDriver: PluginDriver +): { options: NormalizedOutputOptions; unsetOptions: Set } { + return normalizeOutputOptions( + outputPluginDriver.hookReduceArg0Sync( + 'outputOptions', + [rawOutputOptions.output || rawOutputOptions] as [OutputOptions], + (outputOptions, result) => result || outputOptions, + pluginContext => { + const emitError = () => pluginContext.error(errCannotEmitFromOptionsHook()); + return { + ...pluginContext, + emitFile: emitError, + setAssetSource: emitError + }; + } + ) as GenericConfigObject, + inputOptions + ); } function createOutput(outputBundle: Record): RollupOutput { @@ -335,9 +213,25 @@ function createOutput(outputBundle: Record { const fileName = resolve(outputOptions.dir || dirname(outputOptions.file!), outputFile.fileName); let writeSourceMapPromise: Promise | undefined; @@ -362,70 +256,3 @@ function writeOutputFile( return Promise.all([writeFile(fileName, source), writeSourceMapPromise]); } - -function normalizeOutputOptions( - inputOptions: GenericConfigObject, - rawOutputOptions: GenericConfigObject, - hasMultipleChunks: boolean, - outputPluginDriver: PluginDriver -): OutputOptions { - const outputOptions = parseOutputOptions( - outputPluginDriver.hookReduceArg0Sync( - 'outputOptions', - [rawOutputOptions.output || inputOptions.output || rawOutputOptions] as [OutputOptions], - (outputOptions, result) => result || outputOptions, - pluginContext => { - const emitError = () => pluginContext.error(errCannotEmitFromOptionsHook()); - return { - ...pluginContext, - emitFile: emitError, - setAssetSource: emitError - }; - } - ) as GenericConfigObject, - inputOptions.onwarn as WarningHandler - ); - - if (typeof outputOptions.file === 'string') { - if (typeof outputOptions.dir === 'string') - return error({ - code: 'INVALID_OPTION', - message: - 'You must set either "output.file" for a single-file build or "output.dir" when generating multiple chunks.' - }); - if (inputOptions.preserveModules) { - return error({ - code: 'INVALID_OPTION', - message: - 'You must set "output.dir" instead of "output.file" when using the "preserveModules" option.' - }); - } - if (typeof inputOptions.input === 'object' && !Array.isArray(inputOptions.input)) - return error({ - code: 'INVALID_OPTION', - message: 'You must set "output.dir" instead of "output.file" when providing named inputs.' - }); - } - - if (hasMultipleChunks) { - if (outputOptions.format === 'umd' || outputOptions.format === 'iife') - return error({ - code: 'INVALID_OPTION', - message: 'UMD and IIFE output formats are not supported for code-splitting builds.' - }); - if (typeof outputOptions.file === 'string') - return error({ - code: 'INVALID_OPTION', - message: - 'When building multiple chunks, the "output.dir" option must be used, not "output.file". ' + - 'To inline dynamic imports, set the "inlineDynamicImports" option.' - }); - if (outputOptions.sourcemapFile) - return error({ - code: 'INVALID_OPTION', - message: '"output.sourcemapFile" is only supported for single-file builds.' - }); - } - - return outputOptions; -} diff --git a/src/rollup/types.d.ts b/src/rollup/types.d.ts index 6e80d55a489..525cef5c3e5 100644 --- a/src/rollup/types.d.ts +++ b/src/rollup/types.d.ts @@ -229,7 +229,7 @@ export type IsExternal = ( source: string, importer: string | undefined, isResolved: boolean -) => boolean | null | undefined; +) => boolean; export type IsPureModule = (id: string) => boolean | null | undefined; @@ -255,7 +255,7 @@ export type RenderChunkHook = ( this: PluginContext, code: string, chunk: RenderedChunk, - options: OutputOptions + options: NormalizedOutputOptions ) => | Promise<{ code: string; map?: SourceMapInput } | null> | { code: string; map?: SourceMapInput } @@ -328,7 +328,7 @@ export interface OutputBundleWithPlaceholders { export interface PluginHooks extends OutputPluginHooks { buildEnd: (this: PluginContext, err?: Error) => Promise | void; - buildStart: (this: PluginContext, options: InputOptions) => Promise | void; + buildStart: (this: PluginContext, options: NormalizedInputOptions) => Promise | void; load: LoadHook; options: (this: MinimalPluginContext, options: InputOptions) => InputOptions | null | undefined; resolveDynamicImport: ResolveDynamicImportHook; @@ -341,7 +341,7 @@ interface OutputPluginHooks { augmentChunkHash: (this: PluginContext, chunk: PreRenderedChunk) => string | void; generateBundle: ( this: PluginContext, - options: OutputOptions, + options: NormalizedOutputOptions, bundle: OutputBundle, isWrite: boolean ) => void | Promise; @@ -359,8 +359,8 @@ interface OutputPluginHooks { renderError: (this: PluginContext, err?: Error) => Promise | void; renderStart: ( this: PluginContext, - outputOptions: OutputOptions, - inputOptions: InputOptions + outputOptions: NormalizedOutputOptions, + inputOptions: NormalizedInputOptions ) => Promise | void; /** @deprecated Use `resolveFileUrl` instead */ resolveAssetUrl: ResolveAssetUrlHook; @@ -368,7 +368,7 @@ interface OutputPluginHooks { resolveImportMeta: ResolveImportMetaHook; writeBundle: ( this: PluginContext, - options: OutputOptions, + options: NormalizedOutputOptions, bundle: OutputBundle ) => void | Promise; } @@ -444,13 +444,30 @@ export interface TreeshakingOptions { tryCatchDeoptimization?: boolean; unknownGlobalSideEffects?: boolean; } + +export interface NormalizedTreeshakingOptions { + annotations: boolean; + moduleSideEffects: HasModuleSideEffects; + propertyReadSideEffects: boolean; + tryCatchDeoptimization: boolean; + unknownGlobalSideEffects: boolean; +} + interface GetManualChunkApi { getModuleIds: () => IterableIterator; getModuleInfo: GetModuleInfo; } export type GetManualChunk = (id: string, api: GetManualChunkApi) => string | null | undefined; -export type ExternalOption = (string | RegExp)[] | string | RegExp | IsExternal; +export type ExternalOption = + | (string | RegExp)[] + | string + | RegExp + | (( + source: string, + importer: string | undefined, + isResolved: boolean + ) => boolean | null | undefined); export type PureModulesOption = boolean | string[] | IsPureModule; export type GlobalsOption = { [name: string]: string } | ((name: string) => string); export type InputOption = string | string[] | { [entryAlias: string]: string }; @@ -459,7 +476,7 @@ export type ModuleSideEffectsOption = boolean | 'no-external' | string[] | HasMo export type PreserveEntrySignaturesOption = false | 'strict' | 'allow-extension'; export interface InputOptions { - acorn?: any; + acorn?: Object; acornInjectPlugins?: Function | Function[]; cache?: false | RollupCache; context?: string; @@ -468,7 +485,7 @@ export interface InputOptions { inlineDynamicImports?: boolean; input?: InputOption; manualChunks?: ManualChunksOption; - moduleContext?: ((id: string) => string) | { [id: string]: string }; + moduleContext?: ((id: string) => string | null | undefined) | { [id: string]: string }; onwarn?: WarningHandlerWithDefault; perf?: boolean; plugins?: Plugin[]; @@ -481,6 +498,28 @@ export interface InputOptions { watch?: WatcherOptions; } +export interface NormalizedInputOptions { + acorn: Object; + acornInjectPlugins: Function[]; + cache: false | undefined | RollupCache; + context: string; + experimentalCacheExpiry: number; + external: IsExternal; + inlineDynamicImports: boolean; + input: string[] | { [entryAlias: string]: string }; + manualChunks: ManualChunksOption; + moduleContext: (id: string) => string; + onwarn: WarningHandler; + perf: boolean; + plugins: Plugin[]; + preserveEntrySignatures: PreserveEntrySignaturesOption; + preserveModules: boolean; + preserveSymlinks: boolean; + shimMissingExports: boolean; + strictDeprecations: boolean; + treeshake: false | NormalizedTreeshakingOptions; +} + export type InternalModuleFormat = 'amd' | 'cjs' | 'es' | 'iife' | 'system' | 'umd'; export type ModuleFormat = InternalModuleFormat | 'commonjs' | 'esm' | 'module' | 'systemjs'; @@ -512,7 +551,7 @@ export interface OutputOptions { freeze?: boolean; globals?: GlobalsOption; hoistTransitiveImports?: boolean; - indent?: boolean; + indent?: string | boolean; interop?: boolean; intro?: string | (() => string | Promise); minifyInternalExports?: boolean; @@ -530,6 +569,47 @@ export interface OutputOptions { strict?: boolean; } +export interface NormalizedOutputOptions { + amd: { + define: string; + id?: string; + }; + assetFileNames: string; + banner: () => string | Promise; + chunkFileNames: string; + compact: boolean; + dir: string | undefined; + /** @deprecated Use the "renderDynamicImport" plugin hook instead. */ + dynamicImportFunction: string | undefined; + entryFileNames: string; + esModule: boolean; + exports: 'default' | 'named' | 'none' | 'auto'; + extend: boolean; + externalLiveBindings: boolean; + file: string | undefined; + footer: () => string | Promise; + format: InternalModuleFormat; + freeze: boolean; + globals: GlobalsOption; + hoistTransitiveImports: boolean; + indent: true | string; + interop: boolean; + intro: () => string | Promise; + minifyInternalExports: boolean; + name: string | undefined; + namespaceToStringTag: boolean; + noConflict: boolean; + outro: () => string | Promise; + paths: OptionsPaths; + plugins: OutputPlugin[]; + preferConst: boolean; + sourcemap: boolean | 'inline' | 'hidden'; + sourcemapExcludeSources: boolean; + sourcemapFile: string | undefined; + sourcemapPathTransform: ((sourcePath: string) => string) | undefined; + strict: boolean; +} + export type WarningHandlerWithDefault = ( warning: RollupWarning, defaultHandler: WarningHandler @@ -592,7 +672,7 @@ export interface RollupOutput { } export interface RollupBuild { - cache: RollupCache; + cache: RollupCache | undefined; generate: (outputOptions: OutputOptions) => Promise; getTimings?: () => SerializedTimings; watchFiles: string[]; diff --git a/src/utils/FileEmitter.ts b/src/utils/FileEmitter.ts index 966624dfa5e..37182cbc086 100644 --- a/src/utils/FileEmitter.ts +++ b/src/utils/FileEmitter.ts @@ -2,8 +2,10 @@ import Graph from '../Graph'; import Module from '../Module'; import { FilePlaceholder, + NormalizedInputOptions, OutputBundleWithPlaceholders, - PreserveEntrySignaturesOption + PreserveEntrySignaturesOption, + WarningHandler } from '../rollup/types'; import { BuildPhase } from './buildPhase'; import { createHash } from './crypto'; @@ -17,7 +19,8 @@ import { errFileReferenceIdNotFoundForFilename, errInvalidRollupPhaseForChunkEmission, errNoAssetSourceSet, - error + error, + warnDeprecation } from './error'; import { extname } from './path'; import { isPlainPathFragment } from './relativeId'; @@ -54,10 +57,10 @@ function generateAssetFileName( function reserveFileNameInBundle( fileName: string, bundle: OutputBundleWithPlaceholders, - graph: Graph + warn: WarningHandler ) { if (fileName in bundle) { - graph.warn(errFileNameConflict(fileName)); + warn(errFileNameConflict(fileName)); } bundle[fileName] = FILE_PLACEHOLDER; } @@ -142,11 +145,13 @@ function getChunkFileName(file: ConsumedChunk): string { export class FileEmitter { private filesByReferenceId: Map; - private graph: Graph; private output: OutputSpecificFileData | null = null; - constructor(graph: Graph, baseFileEmitter?: FileEmitter) { - this.graph = graph; + constructor( + private readonly graph: Graph, + private readonly options: NormalizedInputOptions, + baseFileEmitter?: FileEmitter + ) { this.filesByReferenceId = baseFileEmitter ? new Map(baseFileEmitter.filesByReferenceId) : new Map(); @@ -226,7 +231,7 @@ export class FileEmitter { }; for (const emittedFile of this.filesByReferenceId.values()) { if (emittedFile.fileName) { - reserveFileNameInBundle(emittedFile.fileName, this.output.bundle, this.graph); + reserveFileNameInBundle(emittedFile.fileName, this.output.bundle, this.options.onwarn); } } for (const [referenceId, consumedFile] of this.filesByReferenceId.entries()) { @@ -268,7 +273,7 @@ export class FileEmitter { ); if (this.output) { if (emittedAsset.fileName) { - reserveFileNameInBundle(emittedAsset.fileName, this.output.bundle, this.graph); + reserveFileNameInBundle(emittedAsset.fileName, this.output.bundle, this.options.onwarn); } if (source !== undefined) { this.finalizeAsset(consumedAsset, source, referenceId, this.output); @@ -335,13 +340,14 @@ export class FileEmitter { // We must not modify the original assets to avoid interaction between outputs const assetWithFileName = { ...consumedFile, source, fileName }; this.filesByReferenceId.set(referenceId, assetWithFileName); - const graph = this.graph; + const options = this.options; output.bundle[fileName] = { fileName, get isAsset(): true { - graph.warnDeprecation( + warnDeprecation( 'Accessing "isAsset" on files in the bundle is deprecated, please use "type === \'asset\'" instead', - true + true, + options ); return true; diff --git a/src/utils/PluginContext.ts b/src/utils/PluginContext.ts index a654c640f0e..e1126f1db41 100644 --- a/src/utils/PluginContext.ts +++ b/src/utils/PluginContext.ts @@ -1,6 +1,7 @@ import { version as rollupVersion } from 'package.json'; import Graph from '../Graph'; import { + NormalizedInputOptions, Plugin, PluginCache, PluginContext, @@ -8,7 +9,7 @@ import { SerializablePluginCache } from '../rollup/types'; import { BuildPhase } from './buildPhase'; -import { errInvalidRollupPhaseForAddWatchFile } from './error'; +import { errInvalidRollupPhaseForAddWatchFile, warnDeprecation } from './error'; import { FileEmitter } from './FileEmitter'; import { createPluginCache, getCacheForUncacheablePlugin, NO_CACHE } from './PluginCache'; import { @@ -23,18 +24,19 @@ function getDeprecatedContextHandler( newHandlerName: string, pluginName: string, activeDeprecation: boolean, - graph: Graph + options: NormalizedInputOptions ): H { let deprecationWarningShown = false; return (((...args: any[]) => { if (!deprecationWarningShown) { deprecationWarningShown = true; - graph.warnDeprecation( + warnDeprecation( { message: `The "this.${handlerName}" plugin context function used by plugin ${pluginName} is deprecated. The "this.${newHandlerName}" plugin context function should be used instead.`, plugin: pluginName }, - activeDeprecation + activeDeprecation, + options ); } return handler(...args); @@ -44,6 +46,7 @@ function getDeprecatedContextHandler( export function getPluginContexts( pluginCache: Record | void, graph: Graph, + options: NormalizedInputOptions, fileEmitter: FileEmitter ): (plugin: Plugin, pluginIndex: number) => PluginContext { const existingPluginNames = new Set(); @@ -88,7 +91,7 @@ export function getPluginContexts( 'emitFile', plugin.name, true, - graph + options ), emitChunk: getDeprecatedContextHandler( (id: string, options?: { name?: string }) => @@ -97,7 +100,7 @@ export function getPluginContexts( 'emitFile', plugin.name, true, - graph + options ), emitFile: fileEmitter.emitFile, error(err): never { @@ -109,7 +112,7 @@ export function getPluginContexts( 'getFileName', plugin.name, true, - graph + options ), getChunkFileName: getDeprecatedContextHandler( fileEmitter.getFileName, @@ -117,36 +120,37 @@ export function getPluginContexts( 'getFileName', plugin.name, true, - graph + options ), getFileName: fileEmitter.getFileName, - getModuleIds: () => graph.moduleById.keys(), + getModuleIds: () => graph.modulesById.keys(), getModuleInfo: graph.getModuleInfo, isExternal: getDeprecatedContextHandler( (id: string, parentId: string | undefined, isResolved = false) => - graph.moduleLoader.isExternal(id, parentId, isResolved), + options.external(id, parentId, isResolved), 'isExternal', 'resolve', plugin.name, true, - graph + options ), meta: { rollupVersion }, get moduleIds() { function* wrappedModuleIds() { - graph.warnDeprecation( + warnDeprecation( { message: `Accessing "this.moduleIds" on the plugin context by plugin ${plugin.name} is deprecated. The "this.getModuleIds" plugin context function should be used instead.`, plugin: plugin.name }, - false + false, + options ); yield* moduleIds; } - const moduleIds = graph.moduleById.keys(); + const moduleIds = graph.modulesById.keys(); return wrappedModuleIds(); }, parse: graph.contextParse, @@ -166,7 +170,7 @@ export function getPluginContexts( 'resolve', plugin.name, true, - graph + options ), setAssetSource: fileEmitter.setAssetSource, warn(warning) { @@ -174,7 +178,7 @@ export function getPluginContexts( if (warning.code) warning.pluginCode = warning.code; warning.code = 'PLUGIN_WARNING'; warning.plugin = plugin.name; - graph.warn(warning); + options.onwarn(warning); } }; return context; diff --git a/src/utils/PluginDriver.ts b/src/utils/PluginDriver.ts index 1c1445e1081..7825a90ad0e 100644 --- a/src/utils/PluginDriver.ts +++ b/src/utils/PluginDriver.ts @@ -4,6 +4,7 @@ import { AsyncPluginHooks, EmitFile, FirstPluginHooks, + NormalizedInputOptions, OutputBundleWithPlaceholders, OutputPluginHooks, ParallelPluginHooks, @@ -71,32 +72,37 @@ export class PluginDriver { ) => void; private fileEmitter: FileEmitter; - private graph: Graph; private pluginCache: Record | undefined; private pluginContexts: PluginContext[]; private plugins: Plugin[]; constructor( - graph: Graph, + private readonly graph: Graph, + private readonly options: NormalizedInputOptions, userPlugins: Plugin[], pluginCache: Record | undefined, basePluginDriver?: PluginDriver ) { - warnDeprecatedHooks(userPlugins, graph); - this.graph = graph; + warnDeprecatedHooks(userPlugins, options); this.pluginCache = pluginCache; - this.fileEmitter = new FileEmitter(graph, basePluginDriver && basePluginDriver.fileEmitter); + this.fileEmitter = new FileEmitter( + graph, + options, + basePluginDriver && basePluginDriver.fileEmitter + ); this.emitFile = this.fileEmitter.emitFile; this.getFileName = this.fileEmitter.getFileName; this.finaliseAssets = this.fileEmitter.assertAssetsFinalized; this.setOutputBundle = this.fileEmitter.setOutputBundle; this.plugins = userPlugins.concat(basePluginDriver ? basePluginDriver.plugins : []); - this.pluginContexts = this.plugins.map(getPluginContexts(pluginCache, graph, this.fileEmitter)); + this.pluginContexts = this.plugins.map( + getPluginContexts(pluginCache, graph, options, this.fileEmitter) + ); if (basePluginDriver) { for (const plugin of userPlugins) { for (const hook of inputHooks) { if (hook in plugin) { - graph.warn(errInputHookInOutputPlugin(plugin.name, hook)); + options.onwarn(errInputHookInOutputPlugin(plugin.name, hook)); } } } @@ -104,7 +110,7 @@ export class PluginDriver { } public createOutputPluginDriver(plugins: Plugin[]): PluginDriver { - return new PluginDriver(this.graph, plugins, this.pluginCache, this); + return new PluginDriver(this.graph, this.options, plugins, this.pluginCache, this); } // chains, first non-null result stops and returns diff --git a/src/utils/addons.ts b/src/utils/addons.ts index 90c43fb3934..a3ef4ab37eb 100644 --- a/src/utils/addons.ts +++ b/src/utils/addons.ts @@ -1,4 +1,4 @@ -import { OutputOptions } from '../rollup/types'; +import { NormalizedOutputOptions } from '../rollup/types'; import { error } from './error'; import { PluginDriver } from './PluginDriver'; @@ -9,32 +9,19 @@ export interface Addons { outro?: string; } -function evalIfFn( - strOrFn: string | (() => string | Promise) | undefined -): string | Promise { - switch (typeof strOrFn) { - case 'function': - return strOrFn(); - case 'string': - return strOrFn; - default: - return ''; - } -} - const concatSep = (out: string, next: string) => (next ? `${out}\n${next}` : out); const concatDblSep = (out: string, next: string) => (next ? `${out}\n\n${next}` : out); export async function createAddons( - options: OutputOptions, + options: NormalizedOutputOptions, outputPluginDriver: PluginDriver ): Promise { try { let [banner, footer, intro, outro] = await Promise.all([ - outputPluginDriver.hookReduceValue('banner', evalIfFn(options.banner), [], concatSep), - outputPluginDriver.hookReduceValue('footer', evalIfFn(options.footer), [], concatSep), - outputPluginDriver.hookReduceValue('intro', evalIfFn(options.intro), [], concatDblSep), - outputPluginDriver.hookReduceValue('outro', evalIfFn(options.outro), [], concatDblSep) + outputPluginDriver.hookReduceValue('banner', options.banner(), [], concatSep), + outputPluginDriver.hookReduceValue('footer', options.footer(), [], concatSep), + outputPluginDriver.hookReduceValue('intro', options.intro(), [], concatDblSep), + outputPluginDriver.hookReduceValue('outro', options.outro(), [], concatDblSep) ]); if (intro) intro += '\n\n'; if (outro) outro = `\n\n${outro}`; diff --git a/src/utils/assignChunkIds.ts b/src/utils/assignChunkIds.ts deleted file mode 100644 index 48dd39917e8..00000000000 --- a/src/utils/assignChunkIds.ts +++ /dev/null @@ -1,38 +0,0 @@ -import Chunk from '../Chunk'; -import { InputOptions, OutputBundleWithPlaceholders, OutputOptions } from '../rollup/types'; -import { Addons } from './addons'; -import { FILE_PLACEHOLDER } from './FileEmitter'; -import { basename } from './path'; -import { PluginDriver } from './PluginDriver'; - -export function assignChunkIds( - chunks: Chunk[], - inputOptions: InputOptions, - outputOptions: OutputOptions, - inputBase: string, - addons: Addons, - bundle: OutputBundleWithPlaceholders, - outputPluginDriver: PluginDriver -) { - const entryChunks: Chunk[] = []; - const otherChunks: Chunk[] = []; - for (const chunk of chunks) { - (chunk.facadeModule && chunk.facadeModule.isUserDefinedEntryPoint - ? entryChunks - : otherChunks - ).push(chunk); - } - - // make sure entry chunk names take precedence with regard to deconflicting - const chunksForNaming: Chunk[] = entryChunks.concat(otherChunks); - for (const chunk of chunksForNaming) { - if (outputOptions.file) { - chunk.id = basename(outputOptions.file); - } else if (inputOptions.preserveModules) { - chunk.id = chunk.generateIdPreserveModules(inputBase, outputOptions, bundle); - } else { - chunk.id = chunk.generateId(addons, outputOptions, bundle, true, outputPluginDriver); - } - bundle[chunk.id] = FILE_PLACEHOLDER; - } -} diff --git a/src/utils/collapseSourcemaps.ts b/src/utils/collapseSourcemaps.ts index b347bcde603..3d9eeaa6785 100644 --- a/src/utils/collapseSourcemaps.ts +++ b/src/utils/collapseSourcemaps.ts @@ -1,10 +1,10 @@ import { DecodedSourceMap, SourceMap } from 'magic-string'; -import Graph from '../Graph'; import Module from '../Module'; import { DecodedSourceMapOrMissing, ExistingDecodedSourceMap, - SourceMapSegment + SourceMapSegment, + WarningHandler } from '../rollup/types'; import { error } from './error'; import { basename, dirname, relative, resolve } from './path'; @@ -145,13 +145,13 @@ class Link { } } -function getLinkMap(graph: Graph) { +function getLinkMap(warn: WarningHandler) { return function linkMap(source: Source | Link, map: DecodedSourceMapOrMissing) { if (map.mappings) { return new Link(map, [source]); } - graph.warn({ + warn({ code: 'SOURCEMAP_BROKEN', message: `Sourcemap is likely to be incorrect: a plugin (${map.plugin}) was used to transform ` + @@ -199,14 +199,14 @@ function getCollapsedSourcemap( } export function collapseSourcemaps( - graph: Graph, file: string, map: DecodedSourceMap, modules: Module[], bundleSourcemapChain: DecodedSourceMapOrMissing[], - excludeContent: boolean | undefined + excludeContent: boolean | undefined, + warn: WarningHandler ) { - const linkMap = getLinkMap(graph); + const linkMap = getLinkMap(warn); const moduleSources = modules .filter(module => !module.excludeFromSourcemap) .map(module => @@ -239,11 +239,11 @@ export function collapseSourcemaps( } export function collapseSourcemap( - graph: Graph, id: string, originalCode: string, originalSourcemap: ExistingDecodedSourceMap | null, - sourcemapChain: DecodedSourceMapOrMissing[] + sourcemapChain: DecodedSourceMapOrMissing[], + warn: WarningHandler ): ExistingDecodedSourceMap | null { if (!sourcemapChain.length) { return originalSourcemap; @@ -254,7 +254,7 @@ export function collapseSourcemap( originalCode, originalSourcemap, sourcemapChain, - getLinkMap(graph) + getLinkMap(warn) ) as Link; const map = source.traceMappings(); return { version: 3, ...map }; diff --git a/src/utils/ensureArray.ts b/src/utils/ensureArray.ts new file mode 100644 index 00000000000..f108c6b3454 --- /dev/null +++ b/src/utils/ensureArray.ts @@ -0,0 +1,9 @@ +export function ensureArray(items: (T | null | undefined)[] | T | null | undefined): T[] { + if (Array.isArray(items)) { + return items.filter(Boolean) as T[]; + } + if (items) { + return [items]; + } + return []; +} diff --git a/src/utils/error.ts b/src/utils/error.ts index 90942250564..4483bfb4cc0 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -1,6 +1,11 @@ import { locate } from 'locate-character'; import Module from '../Module'; -import { RollupError, RollupWarning } from '../rollup/types'; +import { + NormalizedInputOptions, + RollupError, + RollupWarning, + WarningHandler +} from '../rollup/types'; import getCodeFrame from './getCodeFrame'; import relativeId from './relativeId'; @@ -207,8 +212,9 @@ export function errMixedExport(facadeModuleId: string, name?: string) { id: facadeModuleId, message: `Entry module "${relativeId( facadeModuleId - )}" is using named and default exports together. Consumers of your bundle will have to use \`${name || - 'chunk'}["default"]\` to access the default export, which may not be what you want. Use \`output.exports: "named"\` to disable this warning`, + )}" is using named and default exports together. Consumers of your bundle will have to use \`${ + name || 'chunk' + }["default"]\` to access the default export, which may not be what you want. Use \`output.exports: "named"\` to disable this warning`, url: `https://rollupjs.org/guide/en/#output-exports` }; } @@ -279,3 +285,31 @@ export function errFailedValidation(message: string) { message }; } + +export function warnDeprecation( + deprecation: string | RollupWarning, + activeDeprecation: boolean, + options: NormalizedInputOptions +): void { + warnDeprecationWithOptions( + deprecation, + activeDeprecation, + options.onwarn, + options.strictDeprecations + ); +} + +export function warnDeprecationWithOptions( + deprecation: string | RollupWarning, + activeDeprecation: boolean, + warn: WarningHandler, + strictDeprecations: boolean +): void { + if (activeDeprecation || strictDeprecations) { + const warning = errDeprecation(deprecation); + if (strictDeprecations) { + return error(warning); + } + warn(warning); + } +} diff --git a/src/utils/getExportMode.ts b/src/utils/getExportMode.ts index caa2a5ad931..c49209e7ac2 100644 --- a/src/utils/getExportMode.ts +++ b/src/utils/getExportMode.ts @@ -1,11 +1,12 @@ import Chunk from '../Chunk'; -import { OutputOptions } from '../rollup/types'; +import { NormalizedOutputOptions, WarningHandler } from '../rollup/types'; import { errIncompatibleExportOptionValue, errMixedExport, error } from './error'; export default function getExportMode( chunk: Chunk, - { exports: exportMode, name, format }: OutputOptions, - facadeModuleId: string + { exports: exportMode, name, format }: NormalizedOutputOptions, + facadeModuleId: string, + warn: WarningHandler ) { const exportKeys = chunk.getExportNames(); @@ -17,14 +18,14 @@ export default function getExportMode( return error(errIncompatibleExportOptionValue('none', exportKeys, facadeModuleId)); } - if (!exportMode || exportMode === 'auto') { + if (exportMode === 'auto') { if (exportKeys.length === 0) { exportMode = 'none'; } else if (exportKeys.length === 1 && exportKeys[0] === 'default') { exportMode = 'default'; } else { if (format !== 'es' && exportKeys.indexOf('default') !== -1) { - chunk.graph.warn(errMixedExport(facadeModuleId, name)); + warn(errMixedExport(facadeModuleId, name)); } exportMode = 'named'; } diff --git a/src/utils/getIndentString.ts b/src/utils/getIndentString.ts index 32fd4c8cc47..bd2ab3522c4 100644 --- a/src/utils/getIndentString.ts +++ b/src/utils/getIndentString.ts @@ -26,8 +26,8 @@ function guessIndentString(code: string) { return new Array(min + 1).join(' '); } -export default function getIndentString(modules: Module[], options: { indent?: boolean }) { - if (options.indent !== true) return options.indent || ''; +export default function getIndentString(modules: Module[], options: { indent: true | string }) { + if (options.indent !== true) return options.indent; for (let i = 0; i < modules.length; i++) { const indent = guessIndentString(modules[i].originalCode); diff --git a/src/utils/mergeOptions.ts b/src/utils/mergeOptions.ts deleted file mode 100644 index 18aaf277262..00000000000 --- a/src/utils/mergeOptions.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { MergedRollupOptions, WarningHandler } from '../rollup/types'; -import { - CommandConfigObject, - ensureArray, - GenericConfigObject, - parseInputOptions, - parseOutputOptions, - warnUnknownOptions, -} from './parseOptions'; - -export const commandAliases: { [key: string]: string } = { - c: 'config', - d: 'dir', - e: 'external', - f: 'format', - g: 'globals', - h: 'help', - i: 'input', - m: 'sourcemap', - n: 'name', - o: 'file', - p: 'plugin', - v: 'version', - w: 'watch', -}; - -export function mergeOptions( - config: GenericConfigObject, - rawCommandOptions: GenericConfigObject = { external: [], globals: undefined }, - defaultOnWarnHandler?: WarningHandler -): MergedRollupOptions { - const command = getCommandOptions(rawCommandOptions); - const inputOptions = parseInputOptions(config, command, defaultOnWarnHandler); - const warn = inputOptions.onwarn as WarningHandler; - if (command.output) { - Object.assign(command, command.output); - } - const outputOptionsArray = ensureArray(config.output) as GenericConfigObject[]; - if (outputOptionsArray.length === 0) outputOptionsArray.push({}); - const outputOptions = outputOptionsArray.map((singleOutputOptions) => - parseOutputOptions(singleOutputOptions, warn, command) - ); - - warnUnknownOptions( - command, - Object.keys(inputOptions).concat( - Object.keys(outputOptions[0]).filter((option) => option !== 'sourcemapPathTransform'), - Object.keys(commandAliases), - 'config', - 'environment', - 'plugin', - 'silent', - 'stdin' - ), - 'CLI flags', - warn, - /^_$|output$|config/ - ); - (inputOptions as MergedRollupOptions).output = outputOptions; - return inputOptions as MergedRollupOptions; -} - -function getCommandOptions(rawCommandOptions: GenericConfigObject): CommandConfigObject { - const external = - rawCommandOptions.external && typeof rawCommandOptions.external === 'string' - ? rawCommandOptions.external.split(',') - : []; - return { - ...rawCommandOptions, - external, - globals: - typeof rawCommandOptions.globals === 'string' - ? rawCommandOptions.globals.split(',').reduce((globals, globalDefinition) => { - const [id, variableName] = globalDefinition.split(':'); - globals[id] = variableName; - if (external.indexOf(id) === -1) { - external.push(id); - } - return globals; - }, Object.create(null)) - : undefined, - }; -} diff --git a/src/utils/options/mergeOptions.ts b/src/utils/options/mergeOptions.ts new file mode 100644 index 00000000000..d7d53ffee89 --- /dev/null +++ b/src/utils/options/mergeOptions.ts @@ -0,0 +1,225 @@ +import { + ExternalOption, + InputOptions, + MergedRollupOptions, + OutputOptions, + RollupCache, + WarningHandler, + WarningHandlerWithDefault +} from '../../rollup/types'; +import { ensureArray } from '../ensureArray'; +import { CommandConfigObject } from './normalizeInputOptions'; +import { defaultOnWarn, GenericConfigObject, warnUnknownOptions } from './options'; + +export const commandAliases: { [key: string]: string } = { + c: 'config', + d: 'dir', + e: 'external', + f: 'format', + g: 'globals', + h: 'help', + i: 'input', + m: 'sourcemap', + n: 'name', + o: 'file', + p: 'plugin', + v: 'version', + w: 'watch' +}; + +export function mergeOptions( + config: GenericConfigObject, + rawCommandOptions: GenericConfigObject = { external: [], globals: undefined }, + defaultOnWarnHandler: WarningHandler = defaultOnWarn +): MergedRollupOptions { + const command = getCommandOptions(rawCommandOptions); + const inputOptions = mergeInputOptions(config, command, defaultOnWarnHandler); + const warn = inputOptions.onwarn as WarningHandler; + if (command.output) { + Object.assign(command, command.output); + } + const outputOptionsArray = ensureArray(config.output) as GenericConfigObject[]; + if (outputOptionsArray.length === 0) outputOptionsArray.push({}); + const outputOptions = outputOptionsArray.map(singleOutputOptions => + mergeOutputOptions(singleOutputOptions, command, warn) + ); + + warnUnknownOptions( + command, + Object.keys(inputOptions).concat( + Object.keys(outputOptions[0]).filter(option => option !== 'sourcemapPathTransform'), + Object.keys(commandAliases), + 'config', + 'environment', + 'plugin', + 'silent', + 'stdin' + ), + 'CLI flags', + warn, + /^_$|output$|config/ + ); + (inputOptions as MergedRollupOptions).output = outputOptions; + return inputOptions as MergedRollupOptions; +} + +function getCommandOptions(rawCommandOptions: GenericConfigObject): CommandConfigObject { + const external = + rawCommandOptions.external && typeof rawCommandOptions.external === 'string' + ? rawCommandOptions.external.split(',') + : []; + return { + ...rawCommandOptions, + external, + globals: + typeof rawCommandOptions.globals === 'string' + ? rawCommandOptions.globals.split(',').reduce((globals, globalDefinition) => { + const [id, variableName] = globalDefinition.split(':'); + globals[id] = variableName; + if (external.indexOf(id) === -1) { + external.push(id); + } + return globals; + }, Object.create(null)) + : undefined + }; +} + +type CompleteInputOptions = { + [K in U]: InputOptions[K]; +}; + +function mergeInputOptions( + config: GenericConfigObject, + overrides: CommandConfigObject, + defaultOnWarnHandler: WarningHandler +): InputOptions { + const getOption = (name: string): any => overrides[name] ?? config[name]; + const inputOptions: CompleteInputOptions = { + acorn: getOption('acorn'), + acornInjectPlugins: config.acornInjectPlugins as Function | Function[] | undefined, + cache: config.cache as false | RollupCache | undefined, + context: getOption('context'), + experimentalCacheExpiry: getOption('experimentalCacheExpiry'), + external: getExternal(config, overrides), + inlineDynamicImports: getOption('inlineDynamicImports'), + input: getOption('input') || [], + manualChunks: getOption('manualChunks'), + moduleContext: getOption('moduleContext'), + onwarn: getOnWarn(config, defaultOnWarnHandler), + perf: getOption('perf'), + plugins: ensureArray(config.plugins) as Plugin[], + preserveEntrySignatures: getOption('preserveEntrySignatures'), + preserveModules: getOption('preserveModules'), + preserveSymlinks: getOption('preserveSymlinks'), + shimMissingExports: getOption('shimMissingExports'), + strictDeprecations: getOption('strictDeprecations'), + treeshake: getObjectOption(config, overrides, 'treeshake'), + watch: getObjectOption(config, overrides, 'watch') + }; + + warnUnknownOptions( + config, + Object.keys(inputOptions), + 'input options', + inputOptions.onwarn as WarningHandler, + /^output$/ + ); + return inputOptions; +} + +const getExternal = ( + config: GenericConfigObject, + overrides: CommandConfigObject +): ExternalOption => { + const configExternal = config.external as ExternalOption | undefined; + return typeof configExternal === 'function' + ? (source: string, importer: string | undefined, isResolved: boolean) => + configExternal(source, importer, isResolved) || overrides.external.indexOf(source) !== -1 + : ensureArray(configExternal).concat(overrides.external); +}; + +const getOnWarn = ( + config: GenericConfigObject, + defaultOnWarnHandler: WarningHandler +): WarningHandler => + config.onwarn + ? warning => (config.onwarn as WarningHandlerWithDefault)(warning, defaultOnWarnHandler) + : defaultOnWarnHandler; + +const getObjectOption = ( + config: GenericConfigObject, + overrides: GenericConfigObject, + name: string +) => { + const commandOption = normalizeObjectOptionValue(overrides[name]); + const configOption = normalizeObjectOptionValue(config[name]); + if (commandOption !== undefined) { + return commandOption && { ...configOption, ...commandOption }; + } + return configOption; +}; + +export const normalizeObjectOptionValue = (optionValue: any) => { + if (!optionValue) { + return optionValue; + } + if (Array.isArray(optionValue)) { + return optionValue.reduce((result, value) => value && result && { ...result, ...value }, {}); + } + if (typeof optionValue !== 'object') { + return {}; + } + return optionValue; +}; + +type CompleteOutputOptions = { + [K in U]: OutputOptions[K]; +}; + +function mergeOutputOptions( + config: GenericConfigObject, + overrides: GenericConfigObject, + warn: WarningHandler +): OutputOptions { + const getOption = (name: string): any => overrides[name] ?? config[name]; + const outputOptions: CompleteOutputOptions = { + amd: getObjectOption(config, overrides, 'amd'), + assetFileNames: getOption('assetFileNames'), + banner: getOption('banner'), + chunkFileNames: getOption('chunkFileNames'), + compact: getOption('compact'), + dir: getOption('dir'), + dynamicImportFunction: getOption('dynamicImportFunction'), + entryFileNames: getOption('entryFileNames'), + esModule: getOption('esModule'), + exports: getOption('exports'), + extend: getOption('extend'), + externalLiveBindings: getOption('externalLiveBindings'), + file: getOption('file'), + footer: getOption('footer'), + format: getOption('format'), + freeze: getOption('freeze'), + globals: getOption('globals'), + hoistTransitiveImports: getOption('hoistTransitiveImports'), + indent: getOption('indent'), + interop: getOption('interop'), + intro: getOption('intro'), + minifyInternalExports: getOption('minifyInternalExports'), + name: getOption('name'), + namespaceToStringTag: getOption('namespaceToStringTag'), + noConflict: getOption('noConflict'), + outro: getOption('outro'), + paths: getOption('paths'), + plugins: ensureArray(config.plugins) as Plugin[], + preferConst: getOption('preferConst'), + sourcemap: getOption('sourcemap'), + sourcemapExcludeSources: getOption('sourcemapExcludeSources'), + sourcemapFile: getOption('sourcemapFile'), + sourcemapPathTransform: getOption('sourcemapPathTransform'), + strict: getOption('strict') + }; + + warnUnknownOptions(config, Object.keys(outputOptions), 'output options', warn); + return outputOptions; +} diff --git a/src/utils/options/normalizeInputOptions.ts b/src/utils/options/normalizeInputOptions.ts new file mode 100644 index 00000000000..152477a1417 --- /dev/null +++ b/src/utils/options/normalizeInputOptions.ts @@ -0,0 +1,315 @@ +import injectClassFields from 'acorn-class-fields'; +import injectImportMeta from 'acorn-import-meta'; +import injectStaticClassFeatures from 'acorn-static-class-features'; +import { + ExternalOption, + HasModuleSideEffects, + InputOption, + InputOptions, + ManualChunksOption, + ModuleSideEffectsOption, + NormalizedInputOptions, + PreserveEntrySignaturesOption, + PureModulesOption, + RollupBuild, + RollupCache, + TreeshakingOptions, + WarningHandler, + WarningHandlerWithDefault +} from '../../rollup/types'; +import { ensureArray } from '../ensureArray'; +import { errInvalidOption, error, warnDeprecationWithOptions } from '../error'; +import { resolve } from '../path'; +import relativeId from '../relativeId'; +import { defaultOnWarn, GenericConfigObject, warnUnknownOptions } from './options'; + +export interface CommandConfigObject { + external: (string | RegExp)[]; + globals: { [id: string]: string } | undefined; + [key: string]: unknown; +} + +export function normalizeInputOptions( + config: GenericConfigObject +): { options: NormalizedInputOptions; unsetOptions: Set } { + // These are options that may trigger special warnings or behaviour later + // if the user did not select an explicit value + const unsetOptions = new Set(); + + const context = (config.context as string | undefined) ?? 'undefined'; + const inlineDynamicImports = (config.inlineDynamicImports as boolean | undefined) || false; + const onwarn = getOnwarn(config); + const preserveModules = getPreserveModules(config, inlineDynamicImports); + const strictDeprecations = (config.strictDeprecations as boolean | undefined) || false; + const options: NormalizedInputOptions & InputOptions = { + acorn: getAcorn(config), + acornInjectPlugins: getAcornInjectPlugins(config), + cache: getCache(config), + context, + experimentalCacheExpiry: (config.experimentalCacheExpiry as number | undefined) ?? 10, + external: getIdMatcher(config.external as ExternalOption), + inlineDynamicImports, + input: getInput(config, inlineDynamicImports), + manualChunks: getManualChunks(config, inlineDynamicImports, preserveModules), + moduleContext: getModuleContext(config, context), + onwarn, + perf: (config.perf as boolean | undefined) || false, + plugins: ensureArray(config.plugins) as Plugin[], + preserveEntrySignatures: getPreserveEntrySignatures(config, unsetOptions, preserveModules), + preserveModules, + preserveSymlinks: (config.preserveSymlinks as boolean | undefined) || false, + shimMissingExports: (config.shimMissingExports as boolean | undefined) || false, + strictDeprecations, + treeshake: getTreeshake(config, onwarn, strictDeprecations) + }; + + warnUnknownOptions( + config, + [...Object.keys(options), 'watch'], + 'input options', + options.onwarn, + /^(output)$/ + ); + return { options, unsetOptions }; +} + +const getOnwarn = (config: GenericConfigObject): WarningHandler => { + return config.onwarn + ? warning => { + warning.toString = () => { + let str = ''; + + if (warning.plugin) str += `(${warning.plugin} plugin) `; + if (warning.loc) + str += `${relativeId(warning.loc.file!)} (${warning.loc.line}:${warning.loc.column}) `; + str += warning.message; + + return str; + }; + (config.onwarn as WarningHandlerWithDefault)(warning, defaultOnWarn); + } + : defaultOnWarn; +}; + +const getPreserveModules = ( + config: GenericConfigObject, + inlineDynamicImports: boolean +): boolean => { + const preserveModules = (config.preserveModules as boolean | undefined) || false; + if (preserveModules && inlineDynamicImports) { + return error({ + code: 'INVALID_OPTION', + message: `"preserveModules" does not support the "inlineDynamicImports" option.` + }); + } + return preserveModules; +}; + +const getAcorn = (config: GenericConfigObject): acorn.Options => ({ + allowAwaitOutsideFunction: true, + ecmaVersion: 2020, + preserveParens: false, + sourceType: 'module', + ...(config.acorn as Object) +}); + +const getAcornInjectPlugins = (config: GenericConfigObject): Function[] => [ + injectImportMeta, + injectClassFields, + injectStaticClassFeatures, + ...(ensureArray(config.acornInjectPlugins) as any) +]; + +const getCache = (config: GenericConfigObject): false | undefined | RollupCache => { + return (config.cache as RollupBuild)?.cache || (config.cache as false | undefined | RollupCache); +}; + +const getIdMatcher = >( + option: + | undefined + | boolean + | string + | RegExp + | (string | RegExp)[] + | ((id: string, ...args: T) => boolean | null | undefined) +): ((id: string, ...args: T) => boolean) => { + if (option === true) { + return () => true; + } + if (typeof option === 'function') { + return (id, ...args) => (!id.startsWith('\0') && option(id, ...args)) || false; + } + if (option) { + const ids = new Set(); + const matchers: RegExp[] = []; + for (const value of ensureArray(option)) { + if (value instanceof RegExp) { + matchers.push(value); + } else { + ids.add(value); + } + } + return (id => ids.has(id) || matchers.some(matcher => matcher.test(id))) as ( + id: string, + ...args: T + ) => boolean; + } + return () => false; +}; + +const getInput = ( + config: GenericConfigObject, + inlineDynamicImports: boolean +): string[] | { [entryAlias: string]: string } => { + const configInput = config.input as InputOption | undefined; + const input = + configInput == null ? [] : typeof configInput === 'string' ? [configInput] : configInput; + if (inlineDynamicImports && (Array.isArray(input) ? input : Object.keys(input)).length > 1) { + return error({ + code: 'INVALID_OPTION', + message: 'Multiple inputs are not supported for "inlineDynamicImports".' + }); + } + return input; +}; + +const getManualChunks = ( + config: GenericConfigObject, + inlineDynamicImports: boolean, + preserveModules: boolean +): ManualChunksOption => { + const configManualChunks = config.manualChunks as ManualChunksOption; + if (configManualChunks) { + if (inlineDynamicImports) { + return error({ + code: 'INVALID_OPTION', + message: '"manualChunks" option is not supported for "inlineDynamicImports".' + }); + } + if (preserveModules) { + return error({ + code: 'INVALID_OPTION', + message: '"preserveModules" does not support the "manualChunks" option.' + }); + } + } + return configManualChunks || {}; +}; + +const getModuleContext = ( + config: GenericConfigObject, + context: string +): ((id: string) => string) => { + const configModuleContext = config.moduleContext as + | ((id: string) => string | null | undefined) + | { [id: string]: string } + | undefined; + if (typeof configModuleContext === 'function') { + return id => configModuleContext(id) ?? context; + } + if (configModuleContext) { + const contextByModuleId = Object.create(null); + for (const key of Object.keys(configModuleContext)) { + contextByModuleId[resolve(key)] = configModuleContext[key]; + } + return id => contextByModuleId[id] || context; + } + return () => context; +}; + +const getPreserveEntrySignatures = ( + config: GenericConfigObject, + unsetOptions: Set, + preserveModules: boolean +): PreserveEntrySignaturesOption => { + const configPreserveEntrySignatures = config.preserveEntrySignatures as + | PreserveEntrySignaturesOption + | undefined; + if (configPreserveEntrySignatures == null) { + unsetOptions.add('preserveEntrySignatures'); + } else if (configPreserveEntrySignatures === false && preserveModules) { + return error({ + code: 'INVALID_OPTION', + message: '"preserveModules" does not support setting "preserveEntrySignatures" to "false".' + }); + } + return configPreserveEntrySignatures ?? 'strict'; +}; + +const getTreeshake = ( + config: GenericConfigObject, + warn: WarningHandler, + strictDeprecations: boolean +): + | false + | { + annotations: boolean; + moduleSideEffects: HasModuleSideEffects; + propertyReadSideEffects: boolean; + tryCatchDeoptimization: boolean; + unknownGlobalSideEffects: boolean; + } => { + const configTreeshake = config.treeshake as boolean | TreeshakingOptions; + if (configTreeshake === false) { + return false; + } + if (configTreeshake && configTreeshake !== true) { + if (typeof configTreeshake.pureExternalModules !== 'undefined') { + warnDeprecationWithOptions( + `The "treeshake.pureExternalModules" option is deprecated. The "treeshake.moduleSideEffects" option should be used instead. "treeshake.pureExternalModules: true" is equivalent to "treeshake.moduleSideEffects: 'no-external'"`, + true, + warn, + strictDeprecations + ); + } + return { + annotations: configTreeshake.annotations !== false, + moduleSideEffects: getHasModuleSideEffects( + configTreeshake.moduleSideEffects, + configTreeshake.pureExternalModules, + warn + ), + propertyReadSideEffects: configTreeshake.propertyReadSideEffects !== false, + tryCatchDeoptimization: configTreeshake.tryCatchDeoptimization !== false, + unknownGlobalSideEffects: configTreeshake.unknownGlobalSideEffects !== false + }; + } + return { + annotations: true, + moduleSideEffects: () => true, + propertyReadSideEffects: true, + tryCatchDeoptimization: true, + unknownGlobalSideEffects: true + }; +}; + +const getHasModuleSideEffects = ( + moduleSideEffectsOption: ModuleSideEffectsOption | undefined, + pureExternalModules: PureModulesOption | undefined, + warn: WarningHandler +): HasModuleSideEffects => { + if (typeof moduleSideEffectsOption === 'boolean') { + return () => moduleSideEffectsOption; + } + if (moduleSideEffectsOption === 'no-external') { + return (_id, external) => !external; + } + if (typeof moduleSideEffectsOption === 'function') { + return (id, external) => + !id.startsWith('\0') ? moduleSideEffectsOption(id, external) !== false : true; + } + if (Array.isArray(moduleSideEffectsOption)) { + const ids = new Set(moduleSideEffectsOption); + return id => ids.has(id); + } + if (moduleSideEffectsOption) { + warn( + errInvalidOption( + 'treeshake.moduleSideEffects', + 'please use one of false, "no-external", a function or an array' + ) + ); + } + const isPureExternalModule = getIdMatcher(pureExternalModules); + return (id, external) => !(external && isPureExternalModule(id)); +}; diff --git a/src/utils/options/normalizeOutputOptions.ts b/src/utils/options/normalizeOutputOptions.ts new file mode 100644 index 00000000000..26375b45352 --- /dev/null +++ b/src/utils/options/normalizeOutputOptions.ts @@ -0,0 +1,195 @@ +import { + GlobalsOption, + InternalModuleFormat, + ModuleFormat, + NormalizedInputOptions, + NormalizedOutputOptions, + OptionsPaths, + OutputOptions +} from '../../rollup/types'; +import { ensureArray } from '../ensureArray'; +import { errInvalidExportOptionValue, error, warnDeprecation } from '../error'; +import { GenericConfigObject, warnUnknownOptions } from './options'; + +export function normalizeOutputOptions( + config: GenericConfigObject, + inputOptions: NormalizedInputOptions +): { options: NormalizedOutputOptions; unsetOptions: Set } { + // These are options that may trigger special warnings or behaviour later + // if the user did not select an explicit value + const unsetOptions = new Set(); + + const compact = (config.compact as boolean | undefined) || false; + const file = getFile(config, inputOptions); + const format = getFormat(config); + const outputOptions: NormalizedOutputOptions & OutputOptions = { + amd: getAmd(config), + assetFileNames: + (config.assetFileNames as string | undefined) ?? 'assets/[name]-[hash][extname]', + banner: getAddon(config, 'banner'), + chunkFileNames: (config.chunkFileNames as string | undefined) ?? '[name]-[hash].js', + compact, + dir: getDir(config, file), + dynamicImportFunction: getDynamicImportFunction(config, inputOptions), + entryFileNames: getEntryFileNames(config, unsetOptions), + esModule: (config.esModule as boolean | undefined) ?? true, + exports: getExports(config), + extend: (config.extend as boolean | undefined) || false, + externalLiveBindings: (config.externalLiveBindings as boolean | undefined) ?? true, + file, + footer: getAddon(config, 'footer'), + format, + freeze: (config.freeze as boolean | undefined) ?? true, + globals: (config.globals as GlobalsOption | undefined) || {}, + hoistTransitiveImports: (config.hoistTransitiveImports as boolean | undefined) ?? true, + indent: getIndent(config, compact), + interop: (config.interop as boolean | undefined) ?? true, + intro: getAddon(config, 'intro'), + minifyInternalExports: getMinifyInternalExports(config, format, compact), + name: config.name as string | undefined, + namespaceToStringTag: (config.namespaceToStringTag as boolean | undefined) || false, + noConflict: (config.noConflict as boolean | undefined) || false, + outro: getAddon(config, 'outro'), + paths: (config.paths as OptionsPaths | undefined) || {}, + plugins: ensureArray(config.plugins) as Plugin[], + preferConst: (config.preferConst as boolean | undefined) || false, + sourcemap: (config.sourcemap as boolean | 'inline' | 'hidden' | undefined) || false, + sourcemapExcludeSources: (config.sourcemapExcludeSources as boolean | undefined) || false, + sourcemapFile: config.sourcemapFile as string | undefined, + sourcemapPathTransform: config.sourcemapPathTransform as + | ((sourcePath: string) => string) + | undefined, + strict: (config.strict as boolean | undefined) ?? true + }; + + warnUnknownOptions(config, Object.keys(outputOptions), 'output options', inputOptions.onwarn); + return { options: outputOptions, unsetOptions }; +} + +const getFile = ( + config: GenericConfigObject, + inputOptions: NormalizedInputOptions +): string | undefined => { + const file = config.file as string | undefined; + if (typeof file === 'string') { + if (inputOptions.preserveModules) { + return error({ + code: 'INVALID_OPTION', + message: + 'You must set "output.dir" instead of "output.file" when using the "preserveModules" option.' + }); + } + if (!Array.isArray(inputOptions.input)) + return error({ + code: 'INVALID_OPTION', + message: 'You must set "output.dir" instead of "output.file" when providing named inputs.' + }); + } + return file; +}; + +const getFormat = (config: GenericConfigObject): InternalModuleFormat => { + const configFormat = config.format as ModuleFormat | undefined; + switch (configFormat) { + case undefined: + case 'es': + case 'esm': + case 'module': + return 'es'; + case 'cjs': + case 'commonjs': + return 'cjs'; + case 'system': + case 'systemjs': + return 'system'; + case 'amd': + case 'iife': + case 'umd': + return configFormat; + default: + return error({ + message: `You must specify "output.format", which can be one of "amd", "cjs", "system", "es", "iife" or "umd".`, + url: `https://rollupjs.org/guide/en/#output-format` + }); + } +}; + +const getAmd = ( + config: GenericConfigObject +): { + define: string; + id?: string; +} => ({ + define: 'define', + ...(config.amd as { + define?: string; + id?: string; + }) +}); + +const getAddon = (config: GenericConfigObject, name: string): (() => string | Promise) => { + const configAddon = config[name] as string | (() => string | Promise); + if (typeof configAddon === 'function') { + return configAddon; + } + return () => configAddon || ''; +}; + +const getDir = (config: GenericConfigObject, file: string | undefined): string | undefined => { + const dir = config.dir as string | undefined; + if (typeof dir === 'string' && typeof file === 'string') { + return error({ + code: 'INVALID_OPTION', + message: + 'You must set either "output.file" for a single-file build or "output.dir" when generating multiple chunks.' + }); + } + return dir; +}; + +const getDynamicImportFunction = ( + config: GenericConfigObject, + inputOptions: NormalizedInputOptions +): string | undefined => { + const configDynamicImportFunction = config.dynamicImportFunction as string | undefined; + if (configDynamicImportFunction) { + warnDeprecation( + `The "output.dynamicImportFunction" option is deprecated. Use the "renderDynamicImport" plugin hook instead.`, + false, + inputOptions + ); + } + return configDynamicImportFunction; +}; + +const getEntryFileNames = (config: GenericConfigObject, unsetOptions: Set): string => { + const configEntryFileNames = config.entryFileNames as string | undefined; + if (configEntryFileNames == null) { + unsetOptions.add('entryFileNames'); + } + return configEntryFileNames ?? '[name].js'; +}; + +function getExports(config: GenericConfigObject): 'default' | 'named' | 'none' | 'auto' { + const configExports = config.exports as string | undefined; + if (configExports && !['default', 'named', 'none', 'auto'].includes(configExports)) { + return error(errInvalidExportOptionValue(configExports)); + } + return (configExports as 'default' | 'named' | 'none' | 'auto') || 'auto'; +} + +const getIndent = (config: GenericConfigObject, compact: boolean): string | true => { + if (compact) { + return ''; + } + const configIndent = config.indent as string | boolean | undefined; + return configIndent === false ? '' : configIndent ?? true; +}; + +const getMinifyInternalExports = ( + config: GenericConfigObject, + format: InternalModuleFormat, + compact: boolean +): boolean => + (config.minifyInternalExports as boolean | undefined) ?? + (compact || format === 'es' || format === 'system'); diff --git a/src/utils/options/options.ts b/src/utils/options/options.ts new file mode 100644 index 00000000000..a7ddc54f640 --- /dev/null +++ b/src/utils/options/options.ts @@ -0,0 +1,30 @@ +import { WarningHandler } from '../../rollup/types'; + +export interface GenericConfigObject { + [key: string]: unknown; +} + +export const defaultOnWarn: WarningHandler = warning => console.warn(warning.message || warning); + +export function warnUnknownOptions( + passedOptions: GenericConfigObject, + validOptions: string[], + optionType: string, + warn: WarningHandler, + ignoredKeys: RegExp = /$./ +): void { + const validOptionSet = new Set(validOptions); + const unknownOptions = Object.keys(passedOptions).filter( + key => !(validOptionSet.has(key) || ignoredKeys.test(key)) + ); + if (unknownOptions.length > 0) { + warn({ + code: 'UNKNOWN_OPTION', + message: `Unknown ${optionType}: ${unknownOptions.join(', ')}. Allowed options: ${[ + ...validOptionSet + ] + .sort() + .join(', ')}` + }); + } +} diff --git a/src/utils/parseOptions.ts b/src/utils/parseOptions.ts deleted file mode 100644 index df8d547182b..00000000000 --- a/src/utils/parseOptions.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { - InputOptions, - ModuleFormat, - OutputOptions, - WarningHandler, - WarningHandlerWithDefault -} from '../rollup/types'; -import { errInvalidExportOptionValue, error } from './error'; - -export interface GenericConfigObject { - [key: string]: unknown; -} - -export interface CommandConfigObject { - external: (string | RegExp)[]; - globals: { [id: string]: string } | undefined; - [key: string]: unknown; -} - -const createGetOption = (config: GenericConfigObject, overrides: GenericConfigObject) => ( - name: string, - defaultValue?: unknown -): any => - overrides[name] !== undefined - ? overrides[name] - : config[name] !== undefined - ? config[name] - : defaultValue; - -const normalizeObjectOptionValue = (optionValue: any) => { - if (!optionValue) { - return optionValue; - } - if (typeof optionValue !== 'object') { - return {}; - } - return optionValue; -}; - -const getObjectOption = ( - config: GenericConfigObject, - overrides: GenericConfigObject, - name: string -) => { - const commandOption = normalizeObjectOptionValue(overrides[name]); - const configOption = normalizeObjectOptionValue(config[name]); - if (commandOption !== undefined) { - return commandOption && { ...configOption, ...commandOption }; - } - return configOption; -}; - -export function ensureArray(items: (T | null | undefined)[] | T | null | undefined): T[] { - if (Array.isArray(items)) { - return items.filter(Boolean) as T[]; - } - if (items) { - return [items]; - } - return []; -} - -const defaultOnWarn: WarningHandler = warning => { - if (typeof warning === 'string') { - console.warn(warning); - } else { - console.warn(warning.message); - } -}; - -const getOnWarn = ( - config: GenericConfigObject, - defaultOnWarnHandler: WarningHandler -): WarningHandler => - config.onwarn - ? warning => (config.onwarn as WarningHandlerWithDefault)(warning, defaultOnWarnHandler) - : defaultOnWarnHandler; - -const getExternal = (config: GenericConfigObject, overrides: CommandConfigObject) => { - const configExternal = config.external; - return typeof configExternal === 'function' - ? (id: string, ...rest: string[]) => - configExternal(id, ...rest) || overrides.external.indexOf(id) !== -1 - : (Array.isArray(configExternal) - ? configExternal - : configExternal - ? [configExternal] - : [] - ).concat(overrides.external); -}; - -export function parseInputOptions( - config: GenericConfigObject, - overrides: CommandConfigObject = { external: [], globals: undefined }, - defaultOnWarnHandler: WarningHandler = defaultOnWarn -): InputOptions { - const getOption = createGetOption(config, overrides); - const inputOptions: InputOptions = { - acorn: config.acorn, - acornInjectPlugins: config.acornInjectPlugins as any, - cache: getOption('cache'), - context: getOption('context'), - experimentalCacheExpiry: getOption('experimentalCacheExpiry', 10), - external: getExternal(config, overrides) as any, - inlineDynamicImports: getOption('inlineDynamicImports', false), - input: getOption('input', []), - manualChunks: getOption('manualChunks'), - moduleContext: config.moduleContext as any, - onwarn: getOnWarn(config, defaultOnWarnHandler), - perf: getOption('perf', false), - plugins: ensureArray(config.plugins as any), - preserveEntrySignatures: getOption('preserveEntrySignatures'), - preserveModules: getOption('preserveModules'), - preserveSymlinks: getOption('preserveSymlinks'), - shimMissingExports: getOption('shimMissingExports'), - strictDeprecations: getOption('strictDeprecations', false), - treeshake: getObjectOption(config, overrides, 'treeshake'), - watch: config.watch as any - }; - - // support rollup({ cache: prevBuildObject }) - if (inputOptions.cache && (inputOptions.cache as any).cache) - inputOptions.cache = (inputOptions.cache as any).cache; - - warnUnknownOptions( - config, - Object.keys(inputOptions), - 'input options', - inputOptions.onwarn as WarningHandler, - /^output$/ - ); - return inputOptions; -} - -export function parseOutputOptions( - config: GenericConfigObject, - warn: WarningHandler, - overrides: GenericConfigObject = {} -): OutputOptions { - const getOption = createGetOption(config, overrides); - const outputOptions = { - amd: { ...(config.amd as object), ...(overrides.amd as object) } as any, - assetFileNames: getOption('assetFileNames'), - banner: getOption('banner'), - chunkFileNames: getOption('chunkFileNames'), - compact: getOption('compact', false), - dir: getOption('dir'), - dynamicImportFunction: getOption('dynamicImportFunction'), - entryFileNames: getOption('entryFileNames'), - esModule: getOption('esModule', true), - exports: normalizeExports(getOption('exports')), - extend: getOption('extend'), - externalLiveBindings: getOption('externalLiveBindings', true), - file: getOption('file'), - footer: getOption('footer'), - format: normalizeFormat(getOption('format')), - freeze: getOption('freeze', true), - globals: getOption('globals'), - hoistTransitiveImports: getOption('hoistTransitiveImports', true), - indent: getOption('indent', true), - interop: getOption('interop', true), - intro: getOption('intro'), - minifyInternalExports: getOption('minifyInternalExports'), - name: getOption('name'), - namespaceToStringTag: getOption('namespaceToStringTag', false), - noConflict: getOption('noConflict'), - outro: getOption('outro'), - paths: getOption('paths'), - plugins: ensureArray(config.plugins as any), - preferConst: getOption('preferConst'), - sourcemap: getOption('sourcemap'), - sourcemapExcludeSources: getOption('sourcemapExcludeSources'), - sourcemapFile: getOption('sourcemapFile'), - sourcemapPathTransform: getOption('sourcemapPathTransform'), - strict: getOption('strict', true) - }; - - warnUnknownOptions(config, Object.keys(outputOptions), 'output options', warn); - return outputOptions; -} - -export function warnUnknownOptions( - passedOptions: GenericConfigObject, - validOptions: string[], - optionType: string, - warn: WarningHandler, - ignoredKeys: RegExp = /$./ -): void { - const validOptionSet = new Set(validOptions); - const unknownOptions = Object.keys(passedOptions).filter( - key => !(validOptionSet.has(key) || ignoredKeys.test(key)) - ); - if (unknownOptions.length > 0) { - warn({ - code: 'UNKNOWN_OPTION', - message: `Unknown ${optionType}: ${unknownOptions.join(', ')}. Allowed options: ${[ - ...validOptionSet - ] - .sort() - .join(', ')}` - }); - } -} - -function normalizeFormat(format: string): ModuleFormat { - switch (format) { - case undefined: - case 'es': - case 'esm': - case 'module': - return 'es'; - case 'cjs': - case 'commonjs': - return 'cjs'; - case 'system': - case 'systemjs': - return 'system'; - case 'amd': - case 'iife': - case 'umd': - return format; - default: - return error({ - message: `You must specify "output.format", which can be one of "amd", "cjs", "system", "es", "iife" or "umd".`, - url: `https://rollupjs.org/guide/en/#output-format` - }); - } -} - -function normalizeExports(exports: string | undefined): 'default' | 'named' | 'none' | 'auto' { - if (exports && !['default', 'named', 'none', 'auto'].includes(exports)) { - return error(errInvalidExportOptionValue(exports)); - } - return exports as 'default' | 'named' | 'none' | 'auto'; -} diff --git a/src/utils/pluginUtils.ts b/src/utils/pluginUtils.ts index 441eeb05c37..651122cf2dc 100644 --- a/src/utils/pluginUtils.ts +++ b/src/utils/pluginUtils.ts @@ -1,6 +1,5 @@ -import Graph from '../Graph'; -import { Plugin, RollupError } from '../rollup/types'; -import { error, Errors } from './error'; +import { NormalizedInputOptions, Plugin, RollupError } from '../rollup/types'; +import { error, Errors, warnDeprecation } from './error'; export const ANONYMOUS_PLUGIN_PREFIX = 'at position '; export const ANONYMOUS_OUTPUT_PLUGIN_PREFIX = 'at output position '; @@ -29,16 +28,17 @@ export const deprecatedHooks: { active: boolean; deprecated: string; replacement { active: true, deprecated: 'resolveAssetUrl', replacement: 'resolveFileUrl' } ]; -export function warnDeprecatedHooks(plugins: Plugin[], graph: Graph) { +export function warnDeprecatedHooks(plugins: Plugin[], options: NormalizedInputOptions) { for (const { active, deprecated, replacement } of deprecatedHooks) { for (const plugin of plugins) { if (deprecated in plugin) { - graph.warnDeprecation( + warnDeprecation( { message: `The "${deprecated}" hook used by plugin ${plugin.name} is deprecated. The "${replacement}" hook should be used instead.`, plugin: plugin.name }, - active + active, + options ); } } diff --git a/src/utils/renderChunk.ts b/src/utils/renderChunk.ts index 39cf4eddb9b..2b85bc0bd53 100644 --- a/src/utils/renderChunk.ts +++ b/src/utils/renderChunk.ts @@ -1,6 +1,6 @@ import { DecodedSourceMapOrMissing, - OutputOptions, + NormalizedOutputOptions, Plugin, RenderedChunk, SourceMapInput @@ -16,7 +16,7 @@ export default function renderChunk({ sourcemapChain }: { code: string; - options: OutputOptions; + options: NormalizedOutputOptions; outputPluginDriver: PluginDriver; renderChunk: RenderedChunk; sourcemapChain: DecodedSourceMapOrMissing[]; diff --git a/src/utils/renderHelpers.ts b/src/utils/renderHelpers.ts index 74a764a47d7..a9559ab3da1 100644 --- a/src/utils/renderHelpers.ts +++ b/src/utils/renderHelpers.ts @@ -6,7 +6,7 @@ import { treeshakeNode } from './treeshakeNode'; export interface RenderOptions { compact: boolean; - dynamicImportFunction: string; + dynamicImportFunction: string | undefined; format: InternalModuleFormat; freeze: boolean; indent: string; diff --git a/src/utils/transform.ts b/src/utils/transform.ts index 297a0dc04b7..903b7ce40ca 100644 --- a/src/utils/transform.ts +++ b/src/utils/transform.ts @@ -1,5 +1,4 @@ import MagicString, { SourceMap } from 'magic-string'; -import Graph from '../Graph'; import Module from '../Module'; import { DecodedSourceMapOrMissing, @@ -11,18 +10,21 @@ import { SourceDescription, TransformModuleJSON, TransformPluginContext, - TransformResult + TransformResult, + WarningHandler } from '../rollup/types'; import { collapseSourcemap } from './collapseSourcemaps'; import { decodedSourcemap } from './decodedSourcemap'; import { augmentCodeLocation } from './error'; import { getTrackedPluginCache } from './PluginCache'; +import { PluginDriver } from './PluginDriver'; import { throwPluginError } from './pluginUtils'; export default function transform( - graph: Graph, source: SourceDescription, - module: Module + module: Module, + pluginDriver: PluginDriver, + warn: WarningHandler ): Promise { const id = module.id; const sourcemapChain: DecodedSourceMapOrMissing[] = []; @@ -77,7 +79,7 @@ export default function transform( return result.code; } - return graph.pluginDriver + return pluginDriver .hookReduceArg0( 'transform', [curSource, id], @@ -106,16 +108,16 @@ export default function transform( emitAsset(name: string, source?: string | Uint8Array) { const emittedFile = { type: 'asset' as const, name, source }; emittedFiles.push({ ...emittedFile }); - return graph.pluginDriver.emitFile(emittedFile); + return pluginDriver.emitFile(emittedFile); }, emitChunk(id, options) { const emittedFile = { type: 'chunk' as const, id, name: options && options.name }; emittedFiles.push({ ...emittedFile }); - return graph.pluginDriver.emitFile(emittedFile); + return pluginDriver.emitFile(emittedFile); }, emitFile(emittedFile: EmittedFile) { emittedFiles.push(emittedFile); - return graph.pluginDriver.emitFile(emittedFile); + return pluginDriver.emitFile(emittedFile); }, addWatchFile(id: string) { transformDependencies.push(id); @@ -129,11 +131,11 @@ export default function transform( }, getCombinedSourcemap() { const combinedMap = collapseSourcemap( - graph, id, originalCode, originalSourcemap, - sourcemapChain + sourcemapChain, + warn ); if (!combinedMap) { const magicString = new MagicString(originalCode); diff --git a/src/watch/watch-proxy.ts b/src/watch/watch-proxy.ts index f8daa8d3f72..53e5abf5649 100644 --- a/src/watch/watch-proxy.ts +++ b/src/watch/watch-proxy.ts @@ -1,6 +1,6 @@ import { EventEmitter } from 'events'; import { RollupWatcher } from '../rollup/types'; -import { GenericConfigObject } from '../utils/parseOptions'; +import { GenericConfigObject } from '../utils/options/options'; import { loadFsEvents } from './fsevents-importer'; class WatchEmitter extends EventEmitter { diff --git a/src/watch/watch.ts b/src/watch/watch.ts index 5141fc330e7..1023616d643 100644 --- a/src/watch/watch.ts +++ b/src/watch/watch.ts @@ -9,8 +9,9 @@ import { RollupWatcher, WatcherOptions } from '../rollup/types'; -import { mergeOptions } from '../utils/mergeOptions'; -import { ensureArray, GenericConfigObject } from '../utils/parseOptions'; +import { ensureArray } from '../utils/ensureArray'; +import { mergeOptions } from '../utils/options/mergeOptions'; +import { GenericConfigObject } from '../utils/options/options'; import { FileWatcher } from './fileWatcher'; export class Watcher { @@ -209,7 +210,7 @@ export class Task { const previouslyWatched = this.watched; this.watched = new Set(); this.watchFiles = result.watchFiles; - this.cache = result.cache; + this.cache = result.cache!; for (const id of this.watchFiles) { this.watchFile(id); } diff --git a/test/cli/samples/config-multiple-getfilename/_config.js b/test/cli/samples/config-multiple-getfilename/_config.js new file mode 100644 index 00000000000..ba17660e7a0 --- /dev/null +++ b/test/cli/samples/config-multiple-getfilename/_config.js @@ -0,0 +1,5 @@ +module.exports = { + skip: true, + description: 'returns correct file names for multiple outputs (#3467)', + command: 'rollup -c' +}; diff --git a/test/cli/samples/config-multiple-getfilename/_expected/cjs-main.js b/test/cli/samples/config-multiple-getfilename/_expected/cjs-main.js new file mode 100644 index 00000000000..5a370cdb174 --- /dev/null +++ b/test/cli/samples/config-multiple-getfilename/_expected/cjs-main.js @@ -0,0 +1,5 @@ +'use strict'; + +var main = 42; + +module.exports = main; diff --git a/test/cli/samples/config-multiple-getfilename/_expected/cjs.txt b/test/cli/samples/config-multiple-getfilename/_expected/cjs.txt new file mode 100644 index 00000000000..9152b2c5a9e --- /dev/null +++ b/test/cli/samples/config-multiple-getfilename/_expected/cjs.txt @@ -0,0 +1 @@ +cjs-main.js \ No newline at end of file diff --git a/test/cli/samples/config-multiple-getfilename/_expected/es-main.js b/test/cli/samples/config-multiple-getfilename/_expected/es-main.js new file mode 100644 index 00000000000..d862de816a3 --- /dev/null +++ b/test/cli/samples/config-multiple-getfilename/_expected/es-main.js @@ -0,0 +1,3 @@ +var main = 42; + +export default main; diff --git a/test/cli/samples/config-multiple-getfilename/_expected/es.txt b/test/cli/samples/config-multiple-getfilename/_expected/es.txt new file mode 100644 index 00000000000..3c21391e2b3 --- /dev/null +++ b/test/cli/samples/config-multiple-getfilename/_expected/es.txt @@ -0,0 +1 @@ +es-main.js diff --git a/test/cli/samples/config-multiple-getfilename/main.js b/test/cli/samples/config-multiple-getfilename/main.js new file mode 100644 index 00000000000..7a4e8a723a4 --- /dev/null +++ b/test/cli/samples/config-multiple-getfilename/main.js @@ -0,0 +1 @@ +export default 42; diff --git a/test/cli/samples/config-multiple-getfilename/rollup.config.js b/test/cli/samples/config-multiple-getfilename/rollup.config.js new file mode 100644 index 00000000000..4f7eac6c6b2 --- /dev/null +++ b/test/cli/samples/config-multiple-getfilename/rollup.config.js @@ -0,0 +1,30 @@ +let fileReference; + +export default { + input: 'main.js', + plugins: { + name: 'test', + buildStart() { + fileReference = this.emitFile({ type: 'chunk', id: 'main.js' }); + }, + generateBundle(options) { + this.emitFile({ + type: 'asset', + fileName: `${options.format}.txt`, + source: this.getFileName(fileReference), + }); + }, + }, + output: [ + { + format: 'es', + dir: '_actual', + entryFileNames: 'es-[name].js', + }, + { + format: 'cjs', + dir: '_actual', + entryFileNames: 'cjs-[name].js', + }, + ], +}; diff --git a/test/cli/samples/merge-treeshake-false/_config.js b/test/cli/samples/merge-treeshake-false/_config.js new file mode 100644 index 00000000000..5ae50196368 --- /dev/null +++ b/test/cli/samples/merge-treeshake-false/_config.js @@ -0,0 +1,5 @@ +module.exports = { + description: 'sets all tree-shaking to false if one option disables it', + command: + 'rollup main.js --format es --external external --treeshake.moduleSideEffects no-external --no-treeshake --no-treeshake.unknownGlobalSideEffects' +}; diff --git a/test/cli/samples/merge-treeshake-false/_expected.js b/test/cli/samples/merge-treeshake-false/_expected.js new file mode 100644 index 00000000000..ca2429a4e7e --- /dev/null +++ b/test/cli/samples/merge-treeshake-false/_expected.js @@ -0,0 +1,7 @@ +import 'external'; + +console.log('included internal side-effect'); + +const unusedGlobal = someGlobal; + +console.log( 42 ); \ No newline at end of file diff --git a/test/cli/samples/merge-treeshake-false/lib.js b/test/cli/samples/merge-treeshake-false/lib.js new file mode 100644 index 00000000000..2304fe3b69f --- /dev/null +++ b/test/cli/samples/merge-treeshake-false/lib.js @@ -0,0 +1 @@ +console.log('included internal side-effect'); diff --git a/test/cli/samples/merge-treeshake-false/main.js b/test/cli/samples/merge-treeshake-false/main.js new file mode 100644 index 00000000000..f551b3001be --- /dev/null +++ b/test/cli/samples/merge-treeshake-false/main.js @@ -0,0 +1,6 @@ +import 'external'; +import './lib'; + +const unusedGlobal = someGlobal; + +console.log( 42 ); diff --git a/test/cli/samples/merge-treeshake/_config.js b/test/cli/samples/merge-treeshake/_config.js new file mode 100644 index 00000000000..ace0199f7d2 --- /dev/null +++ b/test/cli/samples/merge-treeshake/_config.js @@ -0,0 +1,5 @@ +module.exports = { + description: 'merges treeshake options', + command: + 'rollup main.js --format es --external external --treeshake.moduleSideEffects no-external --treeshake --no-treeshake.unknownGlobalSideEffects' +}; diff --git a/test/cli/samples/merge-treeshake/_expected.js b/test/cli/samples/merge-treeshake/_expected.js new file mode 100644 index 00000000000..4296e33337b --- /dev/null +++ b/test/cli/samples/merge-treeshake/_expected.js @@ -0,0 +1,3 @@ +console.log('included internal side-effect'); + +console.log( 42 ); \ No newline at end of file diff --git a/test/cli/samples/merge-treeshake/lib.js b/test/cli/samples/merge-treeshake/lib.js new file mode 100644 index 00000000000..2304fe3b69f --- /dev/null +++ b/test/cli/samples/merge-treeshake/lib.js @@ -0,0 +1 @@ +console.log('included internal side-effect'); diff --git a/test/cli/samples/merge-treeshake/main.js b/test/cli/samples/merge-treeshake/main.js new file mode 100644 index 00000000000..f551b3001be --- /dev/null +++ b/test/cli/samples/merge-treeshake/main.js @@ -0,0 +1,6 @@ +import 'external'; +import './lib'; + +const unusedGlobal = someGlobal; + +console.log( 42 ); diff --git a/test/cli/samples/pure-external-modules/_config.js b/test/cli/samples/pure-external-modules/_config.js deleted file mode 100644 index 7195c7c0e21..00000000000 --- a/test/cli/samples/pure-external-modules/_config.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - description: 'prunes pure unused external imports', - command: 'rollup main.js --format es --external external --treeshake.pureExternalModules' -}; diff --git a/test/cli/samples/pure-external-modules/_expected.js b/test/cli/samples/pure-external-modules/_expected.js deleted file mode 100644 index 5c72ff35124..00000000000 --- a/test/cli/samples/pure-external-modules/_expected.js +++ /dev/null @@ -1 +0,0 @@ -console.log( 42 ); diff --git a/test/cli/samples/pure-external-modules/main.js b/test/cli/samples/pure-external-modules/main.js deleted file mode 100644 index b3a88b7c928..00000000000 --- a/test/cli/samples/pure-external-modules/main.js +++ /dev/null @@ -1,7 +0,0 @@ -import { unused } from 'external'; - -function alsoUnused () { - unused(); -} - -console.log( 42 ); diff --git a/test/cli/samples/warn-multiple/_config.js b/test/cli/samples/warn-multiple/_config.js index b59306033c9..7e3e6c251c3 100644 --- a/test/cli/samples/warn-multiple/_config.js +++ b/test/cli/samples/warn-multiple/_config.js @@ -7,7 +7,7 @@ module.exports = { assertStderrIncludes( stderr, '(!) Missing shims for Node.js built-ins\n' + - "Creating a browser bundle that depends on 'url', 'assert' and 'path'. You might need to include https://www.npmjs.com/package/rollup-plugin-node-builtins\n" + "Creating a browser bundle that depends on 'url', 'assert' and 'path'. You might need to include https://github.com/ionic-team/rollup-plugin-node-polyfills\n" ); assertStderrIncludes( stderr, diff --git a/test/cli/samples/watch/no-clearScreen-command/_config.js b/test/cli/samples/watch/no-clearScreen-command/_config.js new file mode 100644 index 00000000000..827267699ae --- /dev/null +++ b/test/cli/samples/watch/no-clearScreen-command/_config.js @@ -0,0 +1,17 @@ +const assert = require('assert'); + +const UNDERLINE = '\u001b[4m'; + +module.exports = { + description: 'allows disabling clearing the screen from the command line', + command: 'node wrapper.js main.js --format es --file _actual.js --watch --no-watch.clearScreen', + env: { FORCE_COLOR: '1', TERM: 'xterm' }, + abortOnStderr(data) { + if (data.includes('waiting for changes')) { + return true; + } + }, + stderr(stderr) { + assert.strictEqual(stderr.slice(0, 10), `${UNDERLINE}rollup`); + } +}; diff --git a/test/cli/samples/watch/no-clearScreen-command/foo.js b/test/cli/samples/watch/no-clearScreen-command/foo.js new file mode 100644 index 00000000000..3b8dc9fedfc --- /dev/null +++ b/test/cli/samples/watch/no-clearScreen-command/foo.js @@ -0,0 +1 @@ +export var foo = 42; diff --git a/test/cli/samples/watch/no-clearScreen-command/main.js b/test/cli/samples/watch/no-clearScreen-command/main.js new file mode 100644 index 00000000000..7a4e8a723a4 --- /dev/null +++ b/test/cli/samples/watch/no-clearScreen-command/main.js @@ -0,0 +1 @@ +export default 42; diff --git a/test/cli/samples/watch/no-clearScreen-command/wrapper.js b/test/cli/samples/watch/no-clearScreen-command/wrapper.js new file mode 100755 index 00000000000..8f733cc8815 --- /dev/null +++ b/test/cli/samples/watch/no-clearScreen-command/wrapper.js @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +process.stdout.isTTY = true; +process.stderr.isTTY = true; +require('../../../../../dist/bin/rollup'); diff --git a/test/form/samples/amd-id/_config.js b/test/form/samples/amd-id/_config.js new file mode 100644 index 00000000000..2fb7c876ffa --- /dev/null +++ b/test/form/samples/amd-id/_config.js @@ -0,0 +1,4 @@ +module.exports = { + description: 'allows to declare an AMD id', + options: { output: { amd: { id: 'my-id' } } } +}; diff --git a/test/form/samples/amd-id/_expected/amd.js b/test/form/samples/amd-id/_expected/amd.js new file mode 100644 index 00000000000..9e9ba54188c --- /dev/null +++ b/test/form/samples/amd-id/_expected/amd.js @@ -0,0 +1,5 @@ +define('my-id', function () { 'use strict'; + + console.log(42); + +}); diff --git a/test/form/samples/amd-id/_expected/cjs.js b/test/form/samples/amd-id/_expected/cjs.js new file mode 100644 index 00000000000..d30cc2f7779 --- /dev/null +++ b/test/form/samples/amd-id/_expected/cjs.js @@ -0,0 +1,3 @@ +'use strict'; + +console.log(42); diff --git a/test/form/samples/amd-id/_expected/es.js b/test/form/samples/amd-id/_expected/es.js new file mode 100644 index 00000000000..753a47d529e --- /dev/null +++ b/test/form/samples/amd-id/_expected/es.js @@ -0,0 +1 @@ +console.log(42); diff --git a/test/form/samples/amd-id/_expected/iife.js b/test/form/samples/amd-id/_expected/iife.js new file mode 100644 index 00000000000..3a2d4df530d --- /dev/null +++ b/test/form/samples/amd-id/_expected/iife.js @@ -0,0 +1,6 @@ +(function () { + 'use strict'; + + console.log(42); + +}()); diff --git a/test/form/samples/amd-id/_expected/system.js b/test/form/samples/amd-id/_expected/system.js new file mode 100644 index 00000000000..424a0c00ec5 --- /dev/null +++ b/test/form/samples/amd-id/_expected/system.js @@ -0,0 +1,10 @@ +System.register([], function () { + 'use strict'; + return { + execute: function () { + + console.log(42); + + } + }; +}); diff --git a/test/form/samples/amd-id/_expected/umd.js b/test/form/samples/amd-id/_expected/umd.js new file mode 100644 index 00000000000..8f348b1d4f8 --- /dev/null +++ b/test/form/samples/amd-id/_expected/umd.js @@ -0,0 +1,8 @@ +(function (factory) { + typeof define === 'function' && define.amd ? define('my-id', factory) : + factory(); +}((function () { 'use strict'; + + console.log(42); + +}))); diff --git a/test/form/samples/amd-id/main.js b/test/form/samples/amd-id/main.js new file mode 100644 index 00000000000..753a47d529e --- /dev/null +++ b/test/form/samples/amd-id/main.js @@ -0,0 +1 @@ +console.log(42); diff --git a/test/form/samples/globals-function/_config.js b/test/form/samples/globals-function/_config.js new file mode 100644 index 00000000000..8c1bcef98a3 --- /dev/null +++ b/test/form/samples/globals-function/_config.js @@ -0,0 +1,10 @@ +module.exports = { + description: 'Externals aliases with deshadowing', + options: { + external: ['a', 'b'], + output: { + globals: id => `thisIs${id.toUpperCase()}`, + name: 'myBundle' + } + } +}; diff --git a/test/form/samples/globals-function/_expected/amd.js b/test/form/samples/globals-function/_expected/amd.js new file mode 100644 index 00000000000..1be5a31b0c4 --- /dev/null +++ b/test/form/samples/globals-function/_expected/amd.js @@ -0,0 +1,8 @@ +define(['a', 'b'], function (a, b) { 'use strict'; + + a = a && Object.prototype.hasOwnProperty.call(a, 'default') ? a['default'] : a; + b = b && Object.prototype.hasOwnProperty.call(b, 'default') ? b['default'] : b; + + console.log(a, b); + +}); diff --git a/test/form/samples/globals-function/_expected/cjs.js b/test/form/samples/globals-function/_expected/cjs.js new file mode 100644 index 00000000000..32abf36084c --- /dev/null +++ b/test/form/samples/globals-function/_expected/cjs.js @@ -0,0 +1,8 @@ +'use strict'; + +function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } + +var a = _interopDefault(require('a')); +var b = _interopDefault(require('b')); + +console.log(a, b); diff --git a/test/form/samples/globals-function/_expected/es.js b/test/form/samples/globals-function/_expected/es.js new file mode 100644 index 00000000000..ac37a038c14 --- /dev/null +++ b/test/form/samples/globals-function/_expected/es.js @@ -0,0 +1,4 @@ +import a from 'a'; +import b from 'b'; + +console.log(a, b); diff --git a/test/form/samples/globals-function/_expected/iife.js b/test/form/samples/globals-function/_expected/iife.js new file mode 100644 index 00000000000..1c702e5b0be --- /dev/null +++ b/test/form/samples/globals-function/_expected/iife.js @@ -0,0 +1,9 @@ +(function (a, b) { + 'use strict'; + + a = a && Object.prototype.hasOwnProperty.call(a, 'default') ? a['default'] : a; + b = b && Object.prototype.hasOwnProperty.call(b, 'default') ? b['default'] : b; + + console.log(a, b); + +}(thisIsA, thisIsB)); diff --git a/test/form/samples/globals-function/_expected/system.js b/test/form/samples/globals-function/_expected/system.js new file mode 100644 index 00000000000..932ae55e4ff --- /dev/null +++ b/test/form/samples/globals-function/_expected/system.js @@ -0,0 +1,16 @@ +System.register('myBundle', ['a', 'b'], function () { + 'use strict'; + var a, b; + return { + setters: [function (module) { + a = module.default; + }, function (module) { + b = module.default; + }], + execute: function () { + + console.log(a, b); + + } + }; +}); diff --git a/test/form/samples/globals-function/_expected/umd.js b/test/form/samples/globals-function/_expected/umd.js new file mode 100644 index 00000000000..0e3a426578e --- /dev/null +++ b/test/form/samples/globals-function/_expected/umd.js @@ -0,0 +1,12 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('a'), require('b')) : + typeof define === 'function' && define.amd ? define(['a', 'b'], factory) : + (global = global || self, factory(global.thisIsA, global.thisIsB)); +}(this, (function (a, b) { 'use strict'; + + a = a && Object.prototype.hasOwnProperty.call(a, 'default') ? a['default'] : a; + b = b && Object.prototype.hasOwnProperty.call(b, 'default') ? b['default'] : b; + + console.log(a, b); + +}))); diff --git a/test/form/samples/globals-function/main.js b/test/form/samples/globals-function/main.js new file mode 100644 index 00000000000..ac37a038c14 --- /dev/null +++ b/test/form/samples/globals-function/main.js @@ -0,0 +1,4 @@ +import a from 'a'; +import b from 'b'; + +console.log(a, b); diff --git a/test/form/samples/no-treeshake/_expected/amd.js b/test/form/samples/no-treeshake/_expected/amd.js index 4148d7a90bd..8358fa23eb0 100644 --- a/test/form/samples/no-treeshake/_expected/amd.js +++ b/test/form/samples/no-treeshake/_expected/amd.js @@ -45,6 +45,10 @@ define(['exports', 'external'], function (exports, external) { 'use strict'; } }); + try { + const x = 1; + } catch {} + exports.create = create; exports.getPrototypeOf = getPrototypeOf; exports.quux = quux; diff --git a/test/form/samples/no-treeshake/_expected/cjs.js b/test/form/samples/no-treeshake/_expected/cjs.js index 27872bf8fba..132fbdc7110 100644 --- a/test/form/samples/no-treeshake/_expected/cjs.js +++ b/test/form/samples/no-treeshake/_expected/cjs.js @@ -49,6 +49,10 @@ test({ } }); +try { + const x = 1; +} catch {} + exports.create = create; exports.getPrototypeOf = getPrototypeOf; exports.quux = quux; diff --git a/test/form/samples/no-treeshake/_expected/es.js b/test/form/samples/no-treeshake/_expected/es.js index b147a6d2916..8570fcb406f 100644 --- a/test/form/samples/no-treeshake/_expected/es.js +++ b/test/form/samples/no-treeshake/_expected/es.js @@ -45,4 +45,8 @@ test({ } }); +try { + const x = 1; +} catch {} + export { create, getPrototypeOf, quux, quux as strange }; diff --git a/test/form/samples/no-treeshake/_expected/iife.js b/test/form/samples/no-treeshake/_expected/iife.js index 5a4250d595e..d5d2cf2893d 100644 --- a/test/form/samples/no-treeshake/_expected/iife.js +++ b/test/form/samples/no-treeshake/_expected/iife.js @@ -46,6 +46,10 @@ var stirred = (function (exports, external) { } }); + try { + const x = 1; + } catch {} + exports.create = create; exports.getPrototypeOf = getPrototypeOf; exports.quux = quux; diff --git a/test/form/samples/no-treeshake/_expected/system.js b/test/form/samples/no-treeshake/_expected/system.js index 7efce8dae51..cbcfbad47d8 100644 --- a/test/form/samples/no-treeshake/_expected/system.js +++ b/test/form/samples/no-treeshake/_expected/system.js @@ -52,6 +52,10 @@ System.register('stirred', ['external'], function (exports) { } }); + try { + const x = 1; + } catch {} + } }; }); diff --git a/test/form/samples/no-treeshake/_expected/umd.js b/test/form/samples/no-treeshake/_expected/umd.js index 2d469f68437..ba892127e2a 100644 --- a/test/form/samples/no-treeshake/_expected/umd.js +++ b/test/form/samples/no-treeshake/_expected/umd.js @@ -49,6 +49,10 @@ } }); + try { + const x = 1; + } catch {} + exports.create = create; exports.getPrototypeOf = getPrototypeOf; exports.quux = quux; diff --git a/test/form/samples/no-treeshake/main.js b/test/form/samples/no-treeshake/main.js index fbd642ea623..47ab0fff75a 100644 --- a/test/form/samples/no-treeshake/main.js +++ b/test/form/samples/no-treeshake/main.js @@ -42,3 +42,7 @@ test({ var unused = 1; } }); + +try { + const x = 1; +} catch {} diff --git a/test/hooks/index.js b/test/hooks/index.js index 62ba5d14c94..b7df8ef8e40 100644 --- a/test/hooks/index.js +++ b/test/hooks/index.js @@ -16,11 +16,11 @@ describe('hooks', () => { loader({ newInput: `alert('hello')` }), { buildStart(options) { - assert.strictEqual(options.input, 'newInput'); + assert.deepStrictEqual(options.input, ['newInput']); assert.strictEqual(options.treeshake, false); }, options(options) { - assert.strictEqual(options.input, 'input'); + assert.deepStrictEqual(options.input, 'input'); assert.strictEqual(options.treeshake, false); assert.ok(/^\d+\.\d+\.\d+/.test(this.meta.rollupVersion)); return Object.assign({}, options, { input: 'newInput' }); @@ -39,12 +39,41 @@ describe('hooks', () => { loader({ input: `alert('hello')` }), { renderChunk(code, chunk, options) { - assert.strictEqual(options.banner, 'new banner'); - assert.strictEqual(options.format, 'cjs'); + assert.deepStrictEqual(JSON.parse(JSON.stringify(options)), { + amd: { + define: 'define' + }, + assetFileNames: 'assets/[name]-[hash][extname]', + chunkFileNames: '[name]-[hash].js', + compact: false, + entryFileNames: '[name].js', + esModule: true, + exports: 'auto', + extend: false, + externalLiveBindings: true, + format: 'cjs', + freeze: true, + globals: {}, + hoistTransitiveImports: true, + indent: true, + interop: true, + minifyInternalExports: false, + namespaceToStringTag: false, + noConflict: false, + paths: {}, + plugins: [], + preferConst: false, + sourcemap: false, + sourcemapExcludeSources: false, + strict: true + }); + assert.strictEqual(options.banner(), 'new banner'); }, outputOptions(options) { - assert.strictEqual(options.banner, 'banner'); - assert.strictEqual(options.format, 'cjs'); + assert.deepStrictEqual(JSON.parse(JSON.stringify(options)), { + banner: 'banner', + format: 'cjs' + }); assert.ok(/^\d+\.\d+\.\d+/.test(this.meta.rollupVersion)); return Object.assign({}, options, { banner: 'new banner' }); } diff --git a/test/load-config-file/index.js b/test/load-config-file/index.js index 91205e23354..314a7729e89 100644 --- a/test/load-config-file/index.js +++ b/test/load-config-file/index.js @@ -10,34 +10,20 @@ describe('loadConfigFile', () => { assert.strictEqual(warnings.count, 0); assert.deepStrictEqual(JSON.parse(JSON.stringify(options)), [ { - experimentalCacheExpiry: 10, external: [], - inlineDynamicImports: false, input: 'my-input', output: [ { - amd: {}, - compact: false, - esModule: true, - externalLiveBindings: true, file: 'my-file', format: 'es', - freeze: true, - hoistTransitiveImports: true, - indent: true, - interop: true, - namespaceToStringTag: false, - plugins: [], - strict: true + plugins: [] } ], - perf: false, plugins: [ { name: 'stdin' } - ], - strictDeprecations: false + ] } ]); }); diff --git a/test/load-config-file/samples/basic/rollup.config.js b/test/load-config-file/samples/basic/rollup.config.js index 93cadf7eab5..c9a778d299d 100644 --- a/test/load-config-file/samples/basic/rollup.config.js +++ b/test/load-config-file/samples/basic/rollup.config.js @@ -2,6 +2,6 @@ export default { input: 'my-input', output: { file: 'my-file', - format: 'esm' + format: 'es' } } diff --git a/test/misc/index.js b/test/misc/index.js index c516fc154c0..1cdb683e1e2 100644 --- a/test/misc/index.js +++ b/test/misc/index.js @@ -6,4 +6,3 @@ require('./in-memory-sourcemaps'); require('./misc'); require('./sanity-checks'); require('./umd'); -require('./write-bundle'); diff --git a/test/misc/misc.js b/test/misc/misc.js index 3558b8edd42..442ae6c16d6 100644 --- a/test/misc/misc.js +++ b/test/misc/misc.js @@ -49,7 +49,7 @@ describe('misc', () => { assert.equal(relevantWarnings.length, 1); assert.equal( relevantWarnings[0].message, - `Creating a browser bundle that depends on Node.js built-in module ('util'). You might need to include https://www.npmjs.com/package/rollup-plugin-node-builtins` + `Creating a browser bundle that depends on Node.js built-in module ('util'). You might need to include https://github.com/ionic-team/rollup-plugin-node-polyfills` ); }); }); diff --git a/test/misc/sanity-checks.js b/test/misc/sanity-checks.js index a4e8b665042..6ef7efe6c6a 100644 --- a/test/misc/sanity-checks.js +++ b/test/misc/sanity-checks.js @@ -11,121 +11,118 @@ describe('sanity checks', () => { assert.equal(typeof rollup.rollup, 'function'); }); - it('fails without options', () => { - return rollup - .rollup() - .then(() => { - throw new Error('Missing expected error'); - }) - .catch(err => { - assert.equal(err.message, 'You must supply an options object to rollup'); - }); + it('fails without options', async () => { + let error = null; + try { + await rollup.rollup(); + } catch (buildError) { + error = buildError; + } + assert.strictEqual(error && error.message, 'You must supply an options object to rollup'); }); - it('node API passes warning and default handler to custom onwarn function', () => { + it('node API passes warning and default handler to custom onwarn function', async () => { let args; - return rollup - .rollup({ - input: 'x', - plugins: [loader({ x: `eval(42);` })], - onwarn(warning, onwarn) { - args = [warning, onwarn]; - } - }) - .then(bundle => { - return bundle.generate({ format: 'es' }); - }) - .then(() => { - assert.equal(args[0].code, 'EVAL'); - assert.equal( - args[0].message, - 'Use of eval is strongly discouraged, as it poses security risks and may cause issues with minification' - ); - assert.equal(typeof args[1], 'function'); - }); - }); - - it('fails without options.input', () => { - return rollup - .rollup({}) - .then(() => { - throw new Error('Missing expected error'); - }) - .catch(err => { - assert.equal(err.message, 'You must supply options.input to rollup'); - }); + await rollup.rollup({ + input: 'x', + plugins: [loader({ x: `eval(42);` })], + onwarn(warning, onwarn) { + args = [warning, onwarn]; + } + }); + assert.equal(args[0].code, 'EVAL'); + assert.equal( + args[0].message, + 'Use of eval is strongly discouraged, as it poses security risks and may cause issues with minification' + ); + assert.equal(typeof args[1], 'function'); + }); + + it('fails without options.input', async () => { + let error = null; + try { + await rollup.rollup({}); + } catch (buildError) { + error = buildError; + } + assert.strictEqual(error && error.message, 'You must supply options.input to rollup'); }); - it('treats Literals as leaf nodes, even if first literal encountered is null', () => { - // this test has to be up here, otherwise the bug doesn't have - // an opportunity to present itself - return rollup.rollup({ + it('includes a newline at the end of the bundle', async () => { + const bundle = await rollup.rollup({ input: 'x', - plugins: [loader({ x: `var a = null; a = 'a string';` })] + plugins: [loader({ x: `console.log( 42 );` })] }); + const { + output: [{ code }] + } = await bundle.generate({ format: 'iife' }); + assert.ok(code[code.length - 1] === '\n'); }); - it('includes a newline at the end of the bundle', () => { - return rollup - .rollup({ - input: 'x', - plugins: [loader({ x: `console.log( 42 );` })] - }) - .then(bundle => { - return bundle.generate({ format: 'iife' }); - }) - .then(({ output: [{ code }] }) => { - assert.ok(code[code.length - 1] === '\n'); - }); + it('throws on missing output options when generating a bundle', async () => { + let error = null; + const bundle = await rollup.rollup({ + input: 'x', + plugins: [loader({ x: `console.log( 42 );` })] + }); + try { + await bundle.generate(); + } catch (generateError) { + error = generateError; + } + assert.strictEqual(error && error.message, 'You must supply an options object'); }); - it('throws on missing output options', () => { - const warnings = []; - - return rollup - .rollup({ - input: 'x', - plugins: [loader({ x: `console.log( 42 );` })], - onwarn: warning => warnings.push(warning) - }) - .then(bundle => { - assert.throws(() => { - bundle.generate(); - }, /You must supply an options object/); - }); + it('throws on missing output options when writing a bundle', async () => { + let error = null; + const bundle = await rollup.rollup({ + input: 'x', + plugins: [loader({ x: `console.log( 42 );` })] + }); + try { + await bundle.write(); + } catch (writeError) { + error = writeError; + } + assert.strictEqual(error && error.message, 'You must supply an options object'); + try { + await bundle.write({ format: 'es' }); + } catch (writeError) { + error = writeError; + } + assert.strictEqual( + error && error.message, + 'You must specify "output.file" or "output.dir" for the build.' + ); }); - it('throws on incorrect bundle.generate format option', () => { - const warnings = []; - return rollup - .rollup({ - input: 'x', - plugins: [loader({ x: `console.log( 42 );` })], - onwarn: warning => warnings.push(warning) - }) - .then(bundle => { - assert.throws(() => { - bundle.generate({ file: 'x', format: 'vanilla' }); - }, /You must specify "output\.format", which can be one of "amd", "cjs", "system", "es", "iife" or "umd"./); - }); + it('throws on incorrect bundle.generate format option', async () => { + let error = null; + const bundle = await rollup.rollup({ + input: 'x', + plugins: [loader({ x: `console.log( 42 );` })] + }); + try { + await bundle.generate({ file: 'x', format: 'vanilla' }); + } catch (generateError) { + error = generateError; + } + assert.strictEqual( + error && error.message, + 'You must specify "output.format", which can be one of "amd", "cjs", "system", "es", "iife" or "umd".' + ); }); - it('defaults to output format `es` if not specified', () => { - const warnings = []; - - return rollup - .rollup({ - input: 'x', - plugins: [loader({ x: `export function foo(x){ console.log(x); }` })], - onwarn: warning => warnings.push(warning) - }) - .then(bundle => { - return bundle.generate({}); - }) - .then(({ output: [{ code }] }) => { - assert.equal(code, `function foo(x){ console.log(x); }\n\nexport { foo };\n`); - }); + it('defaults to output format `es` if not specified', async () => { + const bundle = await rollup.rollup({ + input: 'x', + plugins: [loader({ x: `export function foo(x){ console.log(x); }` })] + }); + const { + output: [{ code }] + } = await bundle.generate({}); + assert.equal(code, `function foo(x){ console.log(x); }\n\nexport { foo };\n`); }); it('reuses existing error object', () => { @@ -156,93 +153,89 @@ describe('sanity checks', () => { }); }); - it('throws when using multiple inputs together with the "file" option', () => { - const warnings = []; - - return rollup - .rollup({ - input: ['x', 'y'], - plugins: [loader({ x: 'console.log( "x" );', y: 'console.log( "y" );' })], - onwarn: warning => warnings.push(warning) - }) - .then(bundle => { - assert.throws(() => { - bundle.generate({ file: 'x', format: 'es' }); - }, /When building multiple chunks, the "output\.dir" option must be used, not "output\.file"\. To inline dynamic imports, set the "inlineDynamicImports" option\./); - }); + it('throws when using multiple inputs together with the "file" option', async () => { + let error = null; + const bundle = await rollup.rollup({ + input: ['x', 'y'], + plugins: [loader({ x: 'console.log( "x" );', y: 'console.log( "y" );' })] + }); + try { + await bundle.generate({ file: 'x', format: 'es' }); + } catch (generateError) { + error = generateError; + } + assert.strictEqual( + error && error.message, + 'When building multiple chunks, the "output.dir" option must be used, not "output.file". To inline dynamic imports, set the "inlineDynamicImports" option.' + ); }); - it('does not throw when using a single element array of inputs together with the "file" option', () => { - const warnings = []; - - return rollup - .rollup({ - input: ['x'], - plugins: [loader({ x: 'console.log( "x" );' })], - onwarn: warning => warnings.push(warning) - }) - .then(bundle => bundle.generate({ file: 'x', format: 'es' })); + it('does not throw when using a single element array of inputs together with the "file" option', async () => { + const bundle = await rollup.rollup({ + input: ['x'], + plugins: [loader({ x: 'console.log( "x" );' })] + }); + await bundle.generate({ file: 'x', format: 'es' }); }); - it('throws when using dynamic imports with the "file" option', () => { - const warnings = []; - - return rollup - .rollup({ - input: 'x', - plugins: [loader({ x: 'console.log( "x" );import("y");', y: 'console.log( "y" );' })], - onwarn: warning => warnings.push(warning) - }) - .then(bundle => { - assert.throws(() => { - bundle.generate({ file: 'x', format: 'es' }); - }, /When building multiple chunks, the "output\.dir" option must be used, not "output\.file"\. To inline dynamic imports, set the "inlineDynamicImports" option\./); - }); + it('throws when using dynamic imports with the "file" option', async () => { + let error = null; + const bundle = await rollup.rollup({ + input: 'x', + plugins: [loader({ x: 'console.log( "x" );import("y");', y: 'console.log( "y" );' })] + }); + try { + await bundle.generate({ file: 'x', format: 'es' }); + } catch (generateError) { + error = generateError; + } + assert.strictEqual( + error && error.message, + 'When building multiple chunks, the "output.dir" option must be used, not "output.file". To inline dynamic imports, set the "inlineDynamicImports" option.' + ); }); - it('does not throw when using dynamic imports with the "file" option and "inlineDynamicImports"', () => { - const warnings = []; - - return rollup - .rollup({ - input: 'x', - inlineDynamicImports: true, - plugins: [loader({ x: 'console.log( "x" );import("y");', y: 'console.log( "y" );' })], - onwarn: warning => warnings.push(warning) - }) - .then(bundle => bundle.generate({ file: 'x', format: 'es' })); + it('does not throw when using dynamic imports with the "file" option and "inlineDynamicImports"', async () => { + const bundle = await rollup.rollup({ + input: 'x', + inlineDynamicImports: true, + plugins: [loader({ x: 'console.log( "x" );import("y");', y: 'console.log( "y" );' })] + }); + await bundle.generate({ file: 'x', format: 'es' }); }); - it('throws when using the object form of "input" together with the "file" option', () => { - const warnings = []; - - return rollup - .rollup({ - input: { main: 'x' }, - plugins: [loader({ x: 'console.log( "x" );' })], - onwarn: warning => warnings.push(warning) - }) - .then(bundle => { - assert.throws(() => { - bundle.generate({ file: 'x', format: 'es' }); - }, /You must set "output\.dir" instead of "output\.file" when providing named inputs\./); - }); + it('throws when using the object form of "input" together with the "file" option', async () => { + let error = null; + const bundle = await rollup.rollup({ + input: { main: 'x' }, + plugins: [loader({ x: 'console.log( "x" );' })] + }); + try { + await bundle.generate({ file: 'x', format: 'es' }); + } catch (generateError) { + error = generateError; + } + assert.strictEqual( + error && error.message, + 'You must set "output.dir" instead of "output.file" when providing named inputs.' + ); }); - it('throws when using preserveModules together with the "file" option', () => { - const warnings = []; - - return rollup - .rollup({ - input: 'x', - preserveModules: true, - plugins: [loader({ x: 'console.log( "x" );' })], - onwarn: warning => warnings.push(warning) - }) - .then(bundle => { - assert.throws(() => { - bundle.generate({ file: 'x', format: 'es' }); - }, /You must set "output\.dir" instead of "output\.file" when using the "preserveModules" option\./); - }); + it('throws when using preserveModules together with the "file" option', async () => { + let error = null; + const bundle = await rollup.rollup({ + input: 'x', + preserveModules: true, + plugins: [loader({ x: 'console.log( "x" );' })] + }); + try { + await bundle.generate({ file: 'x', format: 'es' }); + } catch (generateError) { + error = generateError; + } + assert.strictEqual( + error && error.message, + 'You must set "output.dir" instead of "output.file" when using the "preserveModules" option.' + ); }); }); diff --git a/test/misc/write-bundle.js b/test/misc/write-bundle.js deleted file mode 100644 index 5be5b07c6df..00000000000 --- a/test/misc/write-bundle.js +++ /dev/null @@ -1,50 +0,0 @@ -const assert = require('assert'); -const rollup = require('../../dist/rollup'); -const { loader } = require('../utils.js'); - -describe('bundle.write()', () => { - it('fails without options or options.file', () => { - return rollup - .rollup({ - input: 'x', - plugins: [ - { - resolveId: () => 'test', - load: () => '// empty' - } - ] - }) - .then(bundle => { - assert.throws(() => { - bundle.write(); - }, /You must supply an options object/); - - assert.throws(() => { - bundle.write({format: 'es'}); - }, /You must specify "output\.file"/); - }); - }); - - it('works when output options is an array', () => { - const warnings = []; - const options = { - input: 'x', - plugins: [loader({ x: `console.log( 42 );` })], - onwarn: warning => warnings.push(warning), - output: [ - { - format: 'cjs' - }, - { - format: 'es' - } - ] - }; - return rollup.rollup(options).then(bundle => { - assert.equal(warnings.length, 0, 'No warnings for UNKNOWN'); - assert.throws(() => { - return Promise.all(options.output.map(o => bundle.write(o))); - }, /You must specify "output\.file"./); - }); - }); -});