From bc5c9a9274f7936688d2fc904cd26110cda748f9 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Fri, 24 Jan 2020 18:13:28 +0100 Subject: [PATCH] Refactoring option normalization --- cli/help.md | 1 - cli/run/index.ts | 14 +- cli/run/loadConfigFile.ts | 4 +- cli/run/watch.ts | 42 +-- src/rollup/rollup.ts | 93 ++---- src/utils/mergeOptions.ts | 272 ++---------------- src/utils/parseOptions.ts | 212 ++++++++++++++ src/watch/watch-proxy.ts | 2 +- src/watch/watch.ts | 7 +- .../merge-deprecations/rollup.config.js | 2 - .../warn-unknown-options/rollup.config.js | 2 +- .../samples/banner-and-footer/_config.js | 6 +- .../warns-for-invalid-options/_config.js | 23 ++ .../samples/warns-for-invalid-options/main.js | 1 + test/hooks/index.js | 2 +- test/misc/deprecations.js | 24 -- test/misc/sanity-checks.js | 20 +- 17 files changed, 320 insertions(+), 407 deletions(-) create mode 100644 src/utils/parseOptions.ts create mode 100644 test/function/samples/warns-for-invalid-options/_config.js create mode 100644 test/function/samples/warns-for-invalid-options/main.js diff --git a/cli/help.md b/cli/help.md index 44045eb9743..89560bf36f5 100644 --- a/cli/help.md +++ b/cli/help.md @@ -52,7 +52,6 @@ Basic options: --no-treeshake Disable tree-shaking optimisations --no-treeshake.annotations Ignore pure call annotations --no-treeshake.propertyReadSideEffects Ignore property access side-effects ---treeshake.pureExternalModules Assume side-effect free externals Examples: diff --git a/cli/run/index.ts b/cli/run/index.ts index d44a32a101b..190ff1efd21 100644 --- a/cli/run/index.ts +++ b/cli/run/index.ts @@ -1,7 +1,7 @@ import { realpathSync } from 'fs'; import relative from 'require-relative'; -import { WarningHandler } from '../../src/rollup/types'; -import mergeOptions, { GenericConfigObject } from '../../src/utils/mergeOptions'; +import { mergeOptions } from '../../src/utils/mergeOptions'; +import { GenericConfigObject } from '../../src/utils/parseOptions'; import { getAliasName } from '../../src/utils/relativeId'; import { handleError } from '../logging'; import batchWarnings from './batchWarnings'; @@ -107,14 +107,8 @@ async function execute( } else { for (const config of configs) { const warnings = batchWarnings(); - const { inputOptions, outputOptions, optionError } = mergeOptions({ - command, - config, - defaultOnWarnHandler: warnings.add - }); - if (optionError) { - (inputOptions.onwarn as WarningHandler)({ code: 'UNKNOWN_OPTION', message: optionError }); - } + const { inputOptions, outputOptions } = mergeOptions(config, command, + warnings.add); if (command.stdin !== false) { inputOptions.plugins!.push(stdinPlugin()); } diff --git a/cli/run/loadConfigFile.ts b/cli/run/loadConfigFile.ts index da7a911fc95..c9e02d1160a 100644 --- a/cli/run/loadConfigFile.ts +++ b/cli/run/loadConfigFile.ts @@ -2,7 +2,7 @@ import path from 'path'; import tc from 'turbocolor'; import * as rollup from '../../src/node-entry'; import { RollupBuild, RollupOutput } from '../../src/rollup/types'; -import { GenericConfigObject } from '../../src/utils/mergeOptions'; +import { GenericConfigObject } from '../../src/utils/parseOptions'; import relativeId from '../../src/utils/relativeId'; import { handleError, stderr } from '../logging'; import batchWarnings from './batchWarnings'; @@ -73,4 +73,4 @@ export default function loadConfigFile( return Array.isArray(configs) ? configs : [configs]; }); }); -} \ No newline at end of file +} diff --git a/cli/run/watch.ts b/cli/run/watch.ts index e437c71abc9..9397eb73948 100644 --- a/cli/run/watch.ts +++ b/cli/run/watch.ts @@ -4,12 +4,9 @@ import ms from 'pretty-ms'; import onExit from 'signal-exit'; import tc from 'turbocolor'; import * as rollup from '../../src/node-entry'; -import { - RollupWatcher, - RollupWatchOptions, - WarningHandler -} from '../../src/rollup/types'; -import mergeOptions, { GenericConfigObject } from '../../src/utils/mergeOptions'; +import { RollupWatcher, RollupWatchOptions } from '../../src/rollup/types'; +import { mergeOptions } from '../../src/utils/mergeOptions'; +import { GenericConfigObject } from '../../src/utils/parseOptions'; import relativeId from '../../src/utils/relativeId'; import { handleError, stderr } from '../logging'; import batchWarnings from './batchWarnings'; @@ -26,9 +23,7 @@ export default function watch( const isTTY = Boolean(process.stderr.isTTY); const warnings = batchWarnings(); const initialConfigs = processConfigs(configs); - const clearScreen = initialConfigs.every( - config => config.watch!.clearScreen !== false - ); + const clearScreen = initialConfigs.every(config => config.watch!.clearScreen !== false); const resetScreen = getResetScreen(isTTY && clearScreen); let watcher: RollupWatcher; @@ -36,25 +31,12 @@ export default function watch( function processConfigs(configs: GenericConfigObject[]): RollupWatchOptions[] { return configs.map(options => { - const merged = mergeOptions({ - command, - config: options, - defaultOnWarnHandler: warnings.add - }); - + const { inputOptions, outputOptions } = mergeOptions(options, command, warnings.add); const result: RollupWatchOptions = { - ...merged.inputOptions, - output: merged.outputOptions + ...inputOptions, + output: outputOptions }; - if (!result.watch) result.watch = {}; - - if (merged.optionError) - (merged.inputOptions.onwarn as WarningHandler)({ - code: 'UNKNOWN_OPTION', - message: merged.optionError - }); - return result; }); } @@ -87,9 +69,7 @@ export default function watch( } stderr( tc.cyan( - `bundles ${tc.bold(input)} → ${tc.bold( - event.output.map(relativeId).join(', ') - )}...` + `bundles ${tc.bold(input)} → ${tc.bold(event.output.map(relativeId).join(', '))}...` ) ); } @@ -100,9 +80,9 @@ export default function watch( if (!silent) stderr( tc.green( - `created ${tc.bold( - event.output.map(relativeId).join(', ') - )} in ${tc.bold(ms(event.duration))}` + `created ${tc.bold(event.output.map(relativeId).join(', '))} in ${tc.bold( + ms(event.duration) + )}` ) ); if (event.result && event.result.getTimings) { diff --git a/src/rollup/rollup.ts b/src/rollup/rollup.ts index b51ed14455c..cf965527798 100644 --- a/src/rollup/rollup.ts +++ b/src/rollup/rollup.ts @@ -8,7 +8,12 @@ import commondir from '../utils/commondir'; import { errCannotEmitFromOptionsHook, errInvalidExportOptionValue, error } from '../utils/error'; import { writeFile } from '../utils/fs'; import getExportMode from '../utils/getExportMode'; -import mergeOptions, { ensureArray, GenericConfigObject } from '../utils/mergeOptions'; +import { + ensureArray, + GenericConfigObject, + parseInputOptions, + parseOutputOptions +} from '../utils/parseOptions'; import { basename, dirname, isAbsolute, resolve } from '../utils/path'; import { PluginDriver } from '../utils/PluginDriver'; import { ANONYMOUS_OUTPUT_PLUGIN_PREFIX, ANONYMOUS_PLUGIN_PREFIX } from '../utils/pluginUtils'; @@ -53,12 +58,6 @@ function getAbsoluteEntryModulePaths(chunks: Chunk[]): string[] { return absoluteEntryModulePaths; } -const throwAsyncGenerateError = { - get() { - throw new Error(`bundle.generate(...) now returns a Promise instead of a { code, map } object`); - } -}; - function applyOptionHook(inputOptions: InputOptions, plugin: Plugin) { if (plugin.options) return plugin.options.call({ meta: { rollupVersion } }, inputOptions) || inputOptions; @@ -81,13 +80,7 @@ function getInputOptions(rawInputOptions: GenericConfigObject): InputOptions { if (!rawInputOptions) { throw new Error('You must supply an options object to rollup'); } - let { inputOptions, optionError } = mergeOptions({ - config: rawInputOptions - }); - - if (optionError) - (inputOptions.onwarn as WarningHandler)({ message: optionError, code: 'UNKNOWN_OPTION' }); - + let inputOptions = parseInputOptions(rawInputOptions); inputOptions = inputOptions.plugins!.reduce(applyOptionHook, inputOptions); inputOptions.plugins = normalizePlugins(inputOptions.plugins!, ANONYMOUS_PLUGIN_PREFIX); @@ -300,21 +293,18 @@ export async function rollupInternal( const cache = useCache ? graph.getCache() : undefined; const result: RollupBuild = { cache: cache!, - generate: ((rawOutputOptions: GenericConfigObject) => { + generate: (rawOutputOptions: OutputOptions) => { const { outputOptions, outputPluginDriver } = getOutputOptionsAndPluginDriver( - rawOutputOptions + rawOutputOptions as GenericConfigObject ); - const promise = generate(outputOptions, false, outputPluginDriver).then(result => + return generate(outputOptions, false, outputPluginDriver).then(result => createOutput(result) ); - Object.defineProperty(promise, 'code', throwAsyncGenerateError); - Object.defineProperty(promise, 'map', throwAsyncGenerateError); - return promise; - }) as any, + }, watchFiles: Object.keys(graph.watchFiles), - write: ((rawOutputOptions: GenericConfigObject) => { + write: (rawOutputOptions: OutputOptions) => { const { outputOptions, outputPluginDriver } = getOutputOptionsAndPluginDriver( - rawOutputOptions + rawOutputOptions as GenericConfigObject ); if (!outputOptions.dir && !outputOptions.file) { return error({ @@ -323,37 +313,13 @@ export async function rollupInternal( }); } return generate(outputOptions, true, outputPluginDriver).then(async bundle => { - let chunkCount = 0; - for (const fileName of Object.keys(bundle)) { - const file = bundle[fileName]; - if (file.type === 'asset') continue; - chunkCount++; - if (chunkCount > 1) break; - } - if (chunkCount > 1) { - if (outputOptions.sourcemapFile) - return error({ - code: 'INVALID_OPTION', - message: '"output.sourcemapFile" is only supported for single-file 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".' + - (typeof inputOptions.input !== 'string' || - inputOptions.inlineDynamicImports === true - ? '' - : ' To inline dynamic imports, set the "inlineDynamicImports" option.') - }); - } await Promise.all( Object.keys(bundle).map(chunkId => writeOutputFile(bundle[chunkId], outputOptions)) ); await outputPluginDriver.hookParallel('writeBundle', [bundle]); return createOutput(bundle); }); - }) as any + } }; if (inputOptions.perf === true) result.getTimings = getTimings; return result; @@ -425,25 +391,16 @@ function normalizeOutputOptions( hasMultipleChunks: boolean, outputPluginDriver: PluginDriver ): OutputOptions { - const mergedOptions = mergeOptions({ - config: { - output: { - ...rawOutputOptions, - ...(rawOutputOptions.output as object), - ...(inputOptions.output as object) - } - } - }); - - if (mergedOptions.optionError) throw new Error(mergedOptions.optionError); - - // now outputOptions is an array, but rollup.rollup API doesn't support arrays - const mergedOutputOptions = mergedOptions.outputOptions[0]; + const outputOptionBase = rawOutputOptions.output || inputOptions.output || rawOutputOptions; + let outputOptions = parseOutputOptions( + outputOptionBase as GenericConfigObject, + inputOptions.onwarn as WarningHandler + ); const outputOptionsReducer = (outputOptions: OutputOptions, result: OutputOptions) => result || outputOptions; - const outputOptions = outputPluginDriver.hookReduceArg0Sync( + outputOptions = outputPluginDriver.hookReduceArg0Sync( 'outputOptions', - [mergedOutputOptions], + [outputOptions], outputOptionsReducer, pluginContext => { const emitError = () => pluginContext.error(errCannotEmitFromOptionsHook()); @@ -488,7 +445,13 @@ function normalizeOutputOptions( return error({ code: 'INVALID_OPTION', message: - 'You must set "output.dir" instead of "output.file" when generating multiple chunks.' + '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.' }); } diff --git a/src/utils/mergeOptions.ts b/src/utils/mergeOptions.ts index ca62fc73dcd..e739676f6b4 100644 --- a/src/utils/mergeOptions.ts +++ b/src/utils/mergeOptions.ts @@ -1,91 +1,12 @@ +import { InputOptions, OutputOptions, WarningHandler } from '../rollup/types'; import { - InputOptions, - OutputOptions, - WarningHandler, - WarningHandlerWithDefault -} from '../rollup/types'; - -export interface GenericConfigObject { - [key: string]: unknown; -} - -export interface CommandConfigObject { - external: string[]; - globals: { [id: string]: string } | undefined; - [key: string]: unknown; -} - -const createGetOption = (config: GenericConfigObject, command: GenericConfigObject) => ( - name: string, - defaultValue?: unknown -): any => - command[name] !== undefined - ? command[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, - command: GenericConfigObject, - name: string -) => { - const commandOption = normalizeObjectOptionValue(command[name]); - const configOption = normalizeObjectOptionValue(config[name]); - if (commandOption !== undefined) { - return commandOption && configOption ? { ...configOption, ...commandOption } : 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 = defaultOnWarn -): WarningHandler => - config.onwarn - ? warning => (config.onwarn as WarningHandlerWithDefault)(warning, defaultOnWarnHandler) - : defaultOnWarnHandler; - -const getExternal = (config: GenericConfigObject, command: CommandConfigObject) => { - const configExternal = config.external; - return typeof configExternal === 'function' - ? (id: string, ...rest: string[]) => - configExternal(id, ...rest) || command.external.indexOf(id) !== -1 - : (typeof config.external === 'string' - ? [configExternal] - : Array.isArray(configExternal) - ? configExternal - : [] - ).concat(command.external); -}; + CommandConfigObject, + ensureArray, + GenericConfigObject, + parseInputOptions, + parseOutputOptions, + warnUnknownOptions +} from './parseOptions'; export const commandAliases: { [key: string]: string } = { c: 'config', @@ -102,95 +23,47 @@ export const commandAliases: { [key: string]: string } = { w: 'watch' }; -export default function mergeOptions({ - config = {}, - command: rawCommandOptions = {}, - defaultOnWarnHandler -}: { - command?: GenericConfigObject; - config: GenericConfigObject; - defaultOnWarnHandler?: WarningHandler; -}): { +export function mergeOptions( + config: GenericConfigObject, + rawCommandOptions: GenericConfigObject = { external: [], globals: undefined }, + defaultOnWarnHandler?: WarningHandler +): { inputOptions: InputOptions; - optionError: string | null; - outputOptions: any; + outputOptions: OutputOptions[]; } { const command = getCommandOptions(rawCommandOptions); - const inputOptions = getInputOptions(config, command, defaultOnWarnHandler as WarningHandler); - + const inputOptions = parseInputOptions(config, command, defaultOnWarnHandler); + const warn = inputOptions.onwarn as WarningHandler; if (command.output) { Object.assign(command, command.output); } - const output = config.output; - const normalizedOutputOptions = Array.isArray(output) ? output : output ? [output] : []; - if (normalizedOutputOptions.length === 0) normalizedOutputOptions.push({}); - const outputOptions = normalizedOutputOptions.map(singleOutputOptions => - getOutputOptions(singleOutputOptions, command) - ); - - const unknownOptionErrors: string[] = []; - const validInputOptions = Object.keys(inputOptions); - addUnknownOptionErrors( - unknownOptionErrors, - Object.keys(config), - validInputOptions, - 'input option', - /^output$/ - ); - - const validOutputOptions = Object.keys(outputOptions[0]); - addUnknownOptionErrors( - unknownOptionErrors, - outputOptions.reduce((allKeys, options) => allKeys.concat(Object.keys(options)), []), - validOutputOptions, - 'output option' + const outputOptionsArray = ensureArray(config.output) as GenericConfigObject[]; + if (outputOptionsArray.length === 0) outputOptionsArray.push({}); + const outputOptions = outputOptionsArray.map(singleOutputOptions => + parseOutputOptions(singleOutputOptions, warn, command) ); - const validCliOutputOptions = validOutputOptions.filter( - option => option !== 'sourcemapPathTransform' - ); - addUnknownOptionErrors( - unknownOptionErrors, - Object.keys(command), - validInputOptions.concat( - validCliOutputOptions, + warnUnknownOptions( + command, + Object.keys(inputOptions).concat( + Object.keys(outputOptions[0]).filter(option => option !== 'sourcemapPathTransform'), Object.keys(commandAliases), 'config', 'environment', 'silent', 'stdin' ), - 'CLI flag', - /^_|output|(config.*)$/ + 'CLI flags', + warn, + /^_$|output$|config/ ); - return { inputOptions, - optionError: unknownOptionErrors.length > 0 ? unknownOptionErrors.join('\n') : null, outputOptions }; } -function addUnknownOptionErrors( - errors: string[], - options: string[], - validOptions: string[], - optionType: string, - ignoredKeys: RegExp = /$./ -) { - const validOptionSet = new Set(validOptions); - const unknownOptions = options.filter(key => !validOptionSet.has(key) && !ignoredKeys.test(key)); - if (unknownOptions.length > 0) - errors.push( - `Unknown ${optionType}: ${unknownOptions.join(', ')}. Allowed options: ${Array.from( - validOptionSet - ) - .sort() - .join(', ')}` - ); -} - function getCommandOptions(rawCommandOptions: GenericConfigObject): CommandConfigObject { const external = rawCommandOptions.external && typeof rawCommandOptions.external === 'string' @@ -212,94 +85,3 @@ function getCommandOptions(rawCommandOptions: GenericConfigObject): CommandConfi : undefined }; } - -function getInputOptions( - config: GenericConfigObject, - command: CommandConfigObject = { external: [], globals: undefined }, - defaultOnWarnHandler: WarningHandler -): InputOptions { - const getOption = createGetOption(config, command); - const inputOptions: InputOptions = { - acorn: config.acorn, - acornInjectPlugins: config.acornInjectPlugins as any, - cache: getOption('cache'), - chunkGroupingSize: getOption('chunkGroupingSize', 5000), - context: getOption('context'), - experimentalCacheExpiry: getOption('experimentalCacheExpiry', 10), - experimentalOptimizeChunks: getOption('experimentalOptimizeChunks'), - external: getExternal(config, command) 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), - preserveModules: getOption('preserveModules'), - preserveSymlinks: getOption('preserveSymlinks'), - shimMissingExports: getOption('shimMissingExports'), - strictDeprecations: getOption('strictDeprecations', false), - treeshake: getObjectOption(config, command, '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; - - return inputOptions; -} - -function getOutputOptions( - config: GenericConfigObject, - command: GenericConfigObject = {} -): OutputOptions { - const getOption = createGetOption(config, command); - let format = getOption('format'); - - // Handle format aliases - switch (format) { - case undefined: - case 'esm': - case 'module': - format = 'es'; - break; - case 'commonjs': - format = 'cjs'; - } - - return { - amd: { ...(config.amd as object), ...(command.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: getOption('exports'), - extend: getOption('extend'), - externalLiveBindings: getOption('externalLiveBindings', true), - file: getOption('file'), - footer: getOption('footer'), - format, - freeze: getOption('freeze', true), - globals: getOption('globals'), - indent: getOption('indent', true), - interop: getOption('interop', true), - intro: getOption('intro'), - 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) - }; -} diff --git a/src/utils/parseOptions.ts b/src/utils/parseOptions.ts new file mode 100644 index 00000000000..69af1fafb97 --- /dev/null +++ b/src/utils/parseOptions.ts @@ -0,0 +1,212 @@ +import { + InputOptions, + OutputOptions, + WarningHandler, + WarningHandlerWithDefault +} from '../rollup/types'; + +export interface GenericConfigObject { + [key: string]: unknown; +} + +export interface CommandConfigObject { + external: string[]; + 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 ? { ...configOption, ...commandOption } : 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 + : (typeof config.external === 'string' + ? [configExternal] + : Array.isArray(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'), + chunkGroupingSize: getOption('chunkGroupingSize', 5000), + context: getOption('context'), + experimentalCacheExpiry: getOption('experimentalCacheExpiry', 10), + experimentalOptimizeChunks: getOption('experimentalOptimizeChunks'), + 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), + 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); + let format = getOption('format'); + + // Handle format aliases + switch (format) { + case undefined: + case 'esm': + case 'module': + format = 'es'; + break; + case 'commonjs': + format = 'cjs'; + } + 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: getOption('exports'), + extend: getOption('extend'), + externalLiveBindings: getOption('externalLiveBindings', true), + file: getOption('file'), + footer: getOption('footer'), + format, + freeze: getOption('freeze', true), + globals: getOption('globals'), + indent: getOption('indent', true), + interop: getOption('interop', true), + intro: getOption('intro'), + 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: ${Array.from( + validOptionSet + ) + .sort() + .join(', ')}` + }); + } +} diff --git a/src/watch/watch-proxy.ts b/src/watch/watch-proxy.ts index 66bf7e4499d..f8daa8d3f72 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/mergeOptions'; +import { GenericConfigObject } from '../utils/parseOptions'; import { loadFsEvents } from './fsevents-importer'; class WatchEmitter extends EventEmitter { diff --git a/src/watch/watch.ts b/src/watch/watch.ts index 896a5f7105e..a9a85a1bd81 100644 --- a/src/watch/watch.ts +++ b/src/watch/watch.ts @@ -9,7 +9,8 @@ import { RollupWatcher, WatcherOptions } from '../rollup/types'; -import mergeOptions, { GenericConfigObject } from '../utils/mergeOptions'; +import { mergeOptions } from '../utils/mergeOptions'; +import { GenericConfigObject } from '../utils/parseOptions'; import { FileWatcher } from './fileWatcher'; const DELAY = 200; @@ -116,9 +117,7 @@ export class Task { this.closed = false; this.watched = new Set(); - const { inputOptions, outputOptions } = mergeOptions({ - config - }); + const { inputOptions, outputOptions } = mergeOptions(config); this.inputOptions = inputOptions; this.outputs = outputOptions; this.outputFiles = this.outputs.map(output => { diff --git a/test/cli/samples/merge-deprecations/rollup.config.js b/test/cli/samples/merge-deprecations/rollup.config.js index 5a846aebd89..67003241f2d 100644 --- a/test/cli/samples/merge-deprecations/rollup.config.js +++ b/test/cli/samples/merge-deprecations/rollup.config.js @@ -1,7 +1,5 @@ var replace = require( 'rollup-plugin-replace' ); -let warnings = []; - module.exports = { entry: 'main.js', input: 'main.js', diff --git a/test/cli/samples/warn-unknown-options/rollup.config.js b/test/cli/samples/warn-unknown-options/rollup.config.js index beb9fb29c69..60b281e8f61 100644 --- a/test/cli/samples/warn-unknown-options/rollup.config.js +++ b/test/cli/samples/warn-unknown-options/rollup.config.js @@ -18,7 +18,7 @@ module.exports = commands => ({ assert.equal(warning.code, 'UNKNOWN_OPTION'); assert.equal( warning.message, - `Unknown CLI flag: unknownOption. Allowed options: ${ + `Unknown CLI flags: unknownOption. Allowed options: ${ require('../../../misc/optionList').flags }` ); diff --git a/test/function/samples/banner-and-footer/_config.js b/test/function/samples/banner-and-footer/_config.js index b9b035d9318..40d3b05b3c8 100644 --- a/test/function/samples/banner-and-footer/_config.js +++ b/test/function/samples/banner-and-footer/_config.js @@ -1,8 +1,10 @@ module.exports = { description: 'adds a banner/footer', options: { - banner: '/* this is a banner */', - footer: () => Promise.resolve('/* this is a footer */'), + output: { + banner: '/* this is a banner */', + footer: () => Promise.resolve('/* this is a footer */') + }, plugins: [ { banner: '/* first banner */', diff --git a/test/function/samples/warns-for-invalid-options/_config.js b/test/function/samples/warns-for-invalid-options/_config.js new file mode 100644 index 00000000000..d4bdcdbefb7 --- /dev/null +++ b/test/function/samples/warns-for-invalid-options/_config.js @@ -0,0 +1,23 @@ +module.exports = { + description: 'warns for invalid options', + options: { + myInvalidInputOption: true, + output: { + myInvalidOutputOption: true + } + }, + warnings: [ + { + code: 'UNKNOWN_OPTION', + message: + 'Unknown input options: myInvalidInputOption. Allowed options: ' + + require('../../../misc/optionList').input + }, + { + code: 'UNKNOWN_OPTION', + message: + 'Unknown output options: myInvalidOutputOption. Allowed options: ' + + require('../../../misc/optionList').output + } + ] +}; diff --git a/test/function/samples/warns-for-invalid-options/main.js b/test/function/samples/warns-for-invalid-options/main.js new file mode 100644 index 00000000000..7a4e8a723a4 --- /dev/null +++ b/test/function/samples/warns-for-invalid-options/main.js @@ -0,0 +1 @@ +export default 42; diff --git a/test/hooks/index.js b/test/hooks/index.js index 987382495d7..873a9a1906c 100644 --- a/test/hooks/index.js +++ b/test/hooks/index.js @@ -945,7 +945,7 @@ describe('hooks', () => { watcher.close(); assert.strictEqual( err.message, - 'You must set "output.dir" instead of "output.file" when generating multiple chunks.' + 'When building multiple chunks, the "output.dir" option must be used, not "output.file". To inline dynamic imports, set the "inlineDynamicImports" option.' ); }) .then(() => watcher.close()); diff --git a/test/misc/deprecations.js b/test/misc/deprecations.js index d09dce716ba..ff6e0a6e2b1 100644 --- a/test/misc/deprecations.js +++ b/test/misc/deprecations.js @@ -3,30 +3,6 @@ const rollup = require('../../dist/rollup'); const { loader } = require('../utils.js'); describe('deprecations', () => { - it('throws a useful error on accessing code/map properties of bundle.generate promise', () => { - return rollup - .rollup({ - input: 'x', - plugins: [loader({ x: `console.log( 42 );` })] - }) - .then(bundle => { - let errored = false; - - try { - const { code, map } = bundle.generate({ format: 'es' }); - console.log(code, map); - } catch (err) { - assert.equal( - err.message, - `bundle.generate(...) now returns a Promise instead of a { code, map } object` - ); - errored = true; - } - - assert.ok(errored); - }); - }); - it('supports esm format alias', () => { return rollup .rollup({ input: 'x', plugins: [loader({ x: 'export const x = function () {}' })] }) diff --git a/test/misc/sanity-checks.js b/test/misc/sanity-checks.js index 3e332f0252c..e5371fb98a0 100644 --- a/test/misc/sanity-checks.js +++ b/test/misc/sanity-checks.js @@ -56,22 +56,6 @@ describe('sanity checks', () => { }); }); - it('fails with invalid keys', () => { - const warnings = []; - const onwarn = warning => warnings.push(warning); - return rollup - .rollup({ input: 'x', onwarn, plUgins: [], plugins: [loader({ x: `console.log( 42 );` })] }) - .then(() => { - assert.deepEqual(warnings, [ - { - code: 'UNKNOWN_OPTION', - message: - 'Unknown input option: plUgins. Allowed options: ' + require('./optionList').input - } - ]); - }); - }); - 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 @@ -184,7 +168,7 @@ describe('sanity checks', () => { .then(bundle => { assert.throws(() => { bundle.generate({ file: 'x', format: 'es' }); - }, /You must set "output\.dir" instead of "output\.file" when generating multiple chunks\./); + }, /When building multiple chunks, the "output\.dir" option must be used, not "output\.file"\. To inline dynamic imports, set the "inlineDynamicImports" option\./); }); }); @@ -212,7 +196,7 @@ describe('sanity checks', () => { .then(bundle => { assert.throws(() => { bundle.generate({ file: 'x', format: 'es' }); - }, /You must set "output\.dir" instead of "output\.file" when generating multiple chunks\./); + }, /When building multiple chunks, the "output\.dir" option must be used, not "output\.file"\. To inline dynamic imports, set the "inlineDynamicImports" option\./); }); });