diff --git a/docs/999-big-list-of-options.md b/docs/999-big-list-of-options.md index 42fa0f9cf06..a568fdd6559 100755 --- a/docs/999-big-list-of-options.md +++ b/docs/999-big-list-of-options.md @@ -1379,11 +1379,21 @@ Default: `false` If this option is provided, bundling will not fail if bindings are imported from a file that does not define these bindings. Instead, new variables will be created for these bindings with the value `undefined`. #### treeshake -Type: `boolean | { annotations?: boolean, moduleSideEffects?: ModuleSideEffectsOption, propertyReadSideEffects?: boolean | 'always', tryCatchDeoptimization?: boolean, unknownGlobalSideEffects?: boolean }`
+Type: `boolean | "smallest" | "safest" | "recommended" | { annotations?: boolean, moduleSideEffects?: ModuleSideEffectsOption, preset?: "smallest" | "safest" | "recommended", propertyReadSideEffects?: boolean | 'always', tryCatchDeoptimization?: boolean, unknownGlobalSideEffects?: boolean }`
CLI: `--treeshake`/`--no-treeshake`
Default: `true` -Whether to apply tree-shaking and to fine-tune the tree-shaking process. Setting this option to `false` will produce bigger bundles but may improve build performance. If you discover a bug caused by the tree-shaking algorithm, please file an issue! +Whether to apply tree-shaking and to fine-tune the tree-shaking process. Setting this option to `false` will produce bigger bundles but may improve build performance. You may also choose one of three presets that will automatically be updated if new options are added: + +* `"smallest"` will choose option values for you to minimize output size as much as possible. This should work for most code bases as long as you do not rely on certain patterns, which are currently: + * getters with side effects will only be retained if the return value is used (`treeshake.propertyReadSideEffects: false`) + * code from imported modules will only be retained if at least one exported value is used (`treeshake.moduleSideEffects: false`) + * you should not bundle polyfills that rely on detecting broken builtins (`treeshake.tryCatchDeoptimization: false`) + * some semantic errors may be swallowed (`treeshake.unknownGlobalSideEffects: false`) +* `"recommended"` should work well for most usage patterns. Some semantic errors may be swallowed, though (`treeshake.unknownGlobalSideEffects: false`) +* `"safest"` tries to be as spec compliant as possible while still providing some basic tree-shaking capabilities. This is currently equivalent to `true` but this may change in the next major version. + +If you discover a bug caused by the tree-shaking algorithm, please file an issue! Setting this option to an object implies tree-shaking is enabled and grants the following additional options: **treeshake.annotations**
@@ -1492,6 +1502,22 @@ console.log(foo); Note that despite the name, this option does not "add" side effects to modules that do not have side effects. If it is important that e.g. an empty module is "included" in the bundle because you need this for dependency tracking, the plugin interface allows you to designate modules as being excluded from tree-shaking via the [`resolveId`](guide/en/#resolveid), [`load`](guide/en/#load) or [`transform`](guide/en/#transform) hook. +**treeshake.preset**
+Type: `"smallest" | "safest" | "recommended"`
+CLI: `--treeshake `
+ +Allows choosing one of the presets listed above while overriding some of the options. + +```js +export default { + treeshake: { + preset: 'smallest', + propertyReadSideEffects: true + } + // ... +} +``` + **treeshake.propertyReadSideEffects**
Type: `boolean | 'always'`
CLI: `--treeshake.propertyReadSideEffects`/`--no-treeshake.propertyReadSideEffects`
diff --git a/src/rollup/rollup.ts b/src/rollup/rollup.ts index 1877bcb0ffc..fd8241fb852 100644 --- a/src/rollup/rollup.ts +++ b/src/rollup/rollup.ts @@ -219,7 +219,7 @@ function getOutputOptions( setAssetSource: emitError }; } - ) as GenericConfigObject, + ), inputOptions, unsetInputOptions ); diff --git a/src/rollup/types.d.ts b/src/rollup/types.d.ts index cab04c563aa..8c3a17c3e05 100644 --- a/src/rollup/types.d.ts +++ b/src/rollup/types.d.ts @@ -474,9 +474,12 @@ export interface OutputPlugin extends Partial, Partial Record | undefined = value => + (typeof value === 'object' ? value : {}) as Record | undefined ) => { - const commandOption = normalizeObjectOptionValue(overrides[name]); - const configOption = normalizeObjectOptionValue(config[name]); + const commandOption = normalizeObjectOptionValue(overrides[name], objectifyValue); + const configOption = normalizeObjectOptionValue(config[name], objectifyValue); if (commandOption !== undefined) { return commandOption && { ...configOption, ...commandOption }; } return configOption; }; +const objectifyTreeshakeOption = (value: unknown): Record => { + if (typeof value === 'string') { + const preset = treeshakePresets[value as TreeshakingPreset]; + if (preset) { + return preset as unknown as Record; + } + error( + errInvalidOption( + 'treeshake', + `valid values are false, true, ${printQuotedStringList( + Object.keys(treeshakePresets) + )}. You can also supply an object for more fine-grained control` + ) + ); + } + return typeof value === 'object' ? (value as Record) : {}; +}; + const getWatch = (config: GenericConfigObject, overrides: GenericConfigObject, name: string) => config.watch !== false && getObjectOption(config, overrides, name); export const normalizeObjectOptionValue = ( - optionValue: unknown + optionValue: unknown, + objectifyValue: (value: unknown) => Record | undefined ): Record | undefined => { if (!optionValue) { return optionValue as undefined; } if (Array.isArray(optionValue)) { - return optionValue.reduce((result, value) => value && result && { ...result, ...value }, {}); - } - if (typeof optionValue !== 'object') { - return {}; + return optionValue.reduce( + (result, value) => value && result && { ...result, ...objectifyValue(value) }, + {} + ); } - return optionValue as Record; + return objectifyValue(optionValue); }; type CompleteOutputOptions = { diff --git a/src/utils/options/normalizeInputOptions.ts b/src/utils/options/normalizeInputOptions.ts index 120199f985f..7fdc96de610 100644 --- a/src/utils/options/normalizeInputOptions.ts +++ b/src/utils/options/normalizeInputOptions.ts @@ -1,25 +1,25 @@ import * as acorn from 'acorn'; import { - ExternalOption, HasModuleSideEffects, - InputOption, InputOptions, - ManualChunksOption, ModuleSideEffectsOption, NormalizedInputOptions, PreserveEntrySignaturesOption, PureModulesOption, RollupBuild, - RollupCache, - TreeshakingOptions, - WarningHandler, - WarningHandlerWithDefault + WarningHandler } from '../../rollup/types'; import { ensureArray } from '../ensureArray'; -import { errInvalidOption, warnDeprecationWithOptions } from '../error'; +import { errInvalidOption, error, warnDeprecationWithOptions } from '../error'; import { resolve } from '../path'; +import { printQuotedStringList } from '../printStringList'; import relativeId from '../relativeId'; -import { defaultOnWarn, GenericConfigObject, warnUnknownOptions } from './options'; +import { + defaultOnWarn, + GenericConfigObject, + treeshakePresets, + warnUnknownOptions +} from './options'; export interface CommandConfigObject { [key: string]: unknown; @@ -27,7 +27,7 @@ export interface CommandConfigObject { globals: { [id: string]: string } | undefined; } -export function normalizeInputOptions(config: GenericConfigObject): { +export function normalizeInputOptions(config: InputOptions): { options: NormalizedInputOptions; unsetOptions: Set; } { @@ -35,35 +35,34 @@ export function normalizeInputOptions(config: GenericConfigObject): { // if the user did not select an explicit value const unsetOptions = new Set(); - const context = (config.context as string | undefined) ?? 'undefined'; + const context = config.context ?? 'undefined'; const onwarn = getOnwarn(config); - const strictDeprecations = (config.strictDeprecations as boolean | undefined) || false; + const strictDeprecations = config.strictDeprecations || false; const options: NormalizedInputOptions & InputOptions = { - acorn: getAcorn(config) as unknown as Record, + acorn: getAcorn(config) as unknown as NormalizedInputOptions['acorn'], acornInjectPlugins: getAcornInjectPlugins(config), cache: getCache(config), context, - experimentalCacheExpiry: (config.experimentalCacheExpiry as number | undefined) ?? 10, - external: getIdMatcher(config.external as ExternalOption), + experimentalCacheExpiry: config.experimentalCacheExpiry ?? 10, + external: getIdMatcher(config.external), inlineDynamicImports: getInlineDynamicImports(config, onwarn, strictDeprecations), input: getInput(config), - makeAbsoluteExternalsRelative: - (config.makeAbsoluteExternalsRelative as boolean | 'ifRelativeSource' | undefined) ?? true, + makeAbsoluteExternalsRelative: config.makeAbsoluteExternalsRelative ?? true, manualChunks: getManualChunks(config, onwarn, strictDeprecations), moduleContext: getModuleContext(config, context), onwarn, - perf: (config.perf as boolean | undefined) || false, - plugins: ensureArray(config.plugins) as Plugin[], + perf: config.perf || false, + plugins: ensureArray(config.plugins), preserveEntrySignatures: getPreserveEntrySignatures(config, unsetOptions), preserveModules: getPreserveModules(config, onwarn, strictDeprecations), - preserveSymlinks: (config.preserveSymlinks as boolean | undefined) || false, - shimMissingExports: (config.shimMissingExports as boolean | undefined) || false, + preserveSymlinks: config.preserveSymlinks || false, + shimMissingExports: config.shimMissingExports || false, strictDeprecations, treeshake: getTreeshake(config, onwarn, strictDeprecations) }; warnUnknownOptions( - config, + config as GenericConfigObject, [...Object.keys(options), 'watch'], 'input options', options.onwarn, @@ -72,8 +71,9 @@ export function normalizeInputOptions(config: GenericConfigObject): { return { options, unsetOptions }; } -const getOnwarn = (config: GenericConfigObject): WarningHandler => { - return config.onwarn +const getOnwarn = (config: InputOptions): NormalizedInputOptions['onwarn'] => { + const { onwarn } = config; + return onwarn ? warning => { warning.toString = () => { let str = ''; @@ -85,25 +85,25 @@ const getOnwarn = (config: GenericConfigObject): WarningHandler => { return str; }; - (config.onwarn as WarningHandlerWithDefault)(warning, defaultOnWarn); + onwarn(warning, defaultOnWarn); } : defaultOnWarn; }; -const getAcorn = (config: GenericConfigObject): acorn.Options => ({ +const getAcorn = (config: InputOptions): acorn.Options => ({ allowAwaitOutsideFunction: true, ecmaVersion: 'latest', preserveParens: false, sourceType: 'module', - ...(config.acorn as Record) + ...config.acorn }); -const getAcornInjectPlugins = (config: GenericConfigObject): (() => unknown)[] => - ensureArray(config.acornInjectPlugins) as any; +const getAcornInjectPlugins = ( + config: InputOptions +): NormalizedInputOptions['acornInjectPlugins'] => ensureArray(config.acornInjectPlugins); -const getCache = (config: GenericConfigObject): false | undefined | RollupCache => { - return (config.cache as RollupBuild)?.cache || (config.cache as false | undefined | RollupCache); -}; +const getCache = (config: InputOptions): NormalizedInputOptions['cache'] => + (config.cache as unknown as RollupBuild)?.cache || config.cache; const getIdMatcher = >( option: @@ -136,11 +136,11 @@ const getIdMatcher = >( }; const getInlineDynamicImports = ( - config: GenericConfigObject, + config: InputOptions, warn: WarningHandler, strictDeprecations: boolean -): boolean | undefined => { - const configInlineDynamicImports = config.inlineDynamicImports as boolean | undefined; +): NormalizedInputOptions['inlineDynamicImports'] => { + const configInlineDynamicImports = config.inlineDynamicImports; if (configInlineDynamicImports) { warnDeprecationWithOptions( 'The "inlineDynamicImports" option is deprecated. Use the "output.inlineDynamicImports" option instead.', @@ -152,17 +152,17 @@ const getInlineDynamicImports = ( return configInlineDynamicImports; }; -const getInput = (config: GenericConfigObject): string[] | { [entryAlias: string]: string } => { - const configInput = config.input as InputOption | undefined; +const getInput = (config: InputOptions): NormalizedInputOptions['input'] => { + const configInput = config.input; return configInput == null ? [] : typeof configInput === 'string' ? [configInput] : configInput; }; const getManualChunks = ( - config: GenericConfigObject, + config: InputOptions, warn: WarningHandler, strictDeprecations: boolean -): ManualChunksOption | undefined => { - const configManualChunks = config.manualChunks as ManualChunksOption | undefined; +): NormalizedInputOptions['manualChunks'] => { + const configManualChunks = config.manualChunks; if (configManualChunks) { warnDeprecationWithOptions( 'The "manualChunks" option is deprecated. Use the "output.manualChunks" option instead.', @@ -175,9 +175,9 @@ const getManualChunks = ( }; const getModuleContext = ( - config: GenericConfigObject, + config: InputOptions, context: string -): ((id: string) => string) => { +): NormalizedInputOptions['moduleContext'] => { const configModuleContext = config.moduleContext as | ((id: string) => string | null | undefined) | { [id: string]: string } @@ -196,9 +196,9 @@ const getModuleContext = ( }; const getPreserveEntrySignatures = ( - config: GenericConfigObject, + config: InputOptions, unsetOptions: Set -): PreserveEntrySignaturesOption => { +): NormalizedInputOptions['preserveEntrySignatures'] => { const configPreserveEntrySignatures = config.preserveEntrySignatures as | PreserveEntrySignaturesOption | undefined; @@ -209,11 +209,11 @@ const getPreserveEntrySignatures = ( }; const getPreserveModules = ( - config: GenericConfigObject, + config: InputOptions, warn: WarningHandler, strictDeprecations: boolean -): boolean | undefined => { - const configPreserveModules = config.preserveModules as boolean | undefined; +): NormalizedInputOptions['preserveModules'] => { + const configPreserveModules = config.preserveModules; if (configPreserveModules) { warnDeprecationWithOptions( 'The "preserveModules" option is deprecated. Use the "output.preserveModules" option instead.', @@ -226,23 +226,24 @@ const getPreserveModules = ( }; const getTreeshake = ( - config: GenericConfigObject, + config: InputOptions, warn: WarningHandler, strictDeprecations: boolean -): - | false - | { - annotations: boolean; - moduleSideEffects: HasModuleSideEffects; - propertyReadSideEffects: boolean | 'always'; - tryCatchDeoptimization: boolean; - unknownGlobalSideEffects: boolean; - } => { - const configTreeshake = config.treeshake as boolean | TreeshakingOptions; +): NormalizedInputOptions['treeshake'] => { + const configTreeshake = config.treeshake; if (configTreeshake === false) { return false; } - if (configTreeshake && configTreeshake !== true) { + if (!configTreeshake || configTreeshake === true) { + return { + annotations: true, + moduleSideEffects: () => true, + propertyReadSideEffects: true, + tryCatchDeoptimization: true, + unknownGlobalSideEffects: true + }; + } + if (typeof configTreeshake === 'object') { 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'"`, @@ -251,33 +252,54 @@ const getTreeshake = ( strictDeprecations ); } + let configWithPreset = configTreeshake; + const presetName = configTreeshake.preset; + if (presetName) { + const preset = treeshakePresets[presetName]; + if (preset) { + configWithPreset = { ...preset, ...configTreeshake }; + } else { + error( + errInvalidOption( + 'treeshake.preset', + `valid values are ${printQuotedStringList(Object.keys(treeshakePresets))}` + ) + ); + } + } return { - annotations: configTreeshake.annotations !== false, - moduleSideEffects: getHasModuleSideEffects( - configTreeshake.moduleSideEffects, - configTreeshake.pureExternalModules, - warn - ), + annotations: configWithPreset.annotations !== false, + moduleSideEffects: configTreeshake.pureExternalModules + ? getHasModuleSideEffects( + configTreeshake.moduleSideEffects, + configTreeshake.pureExternalModules + ) + : getHasModuleSideEffects(configWithPreset.moduleSideEffects, undefined), propertyReadSideEffects: - (configTreeshake.propertyReadSideEffects === 'always' && 'always') || - configTreeshake.propertyReadSideEffects !== false, - tryCatchDeoptimization: configTreeshake.tryCatchDeoptimization !== false, - unknownGlobalSideEffects: configTreeshake.unknownGlobalSideEffects !== false + configWithPreset.propertyReadSideEffects === 'always' + ? 'always' + : configWithPreset.propertyReadSideEffects !== false, + tryCatchDeoptimization: configWithPreset.tryCatchDeoptimization !== false, + unknownGlobalSideEffects: configWithPreset.unknownGlobalSideEffects !== false }; } - return { - annotations: true, - moduleSideEffects: () => true, - propertyReadSideEffects: true, - tryCatchDeoptimization: true, - unknownGlobalSideEffects: true - }; + const preset = treeshakePresets[configTreeshake]; + if (preset) { + return preset; + } + error( + errInvalidOption( + 'treeshake', + `valid values are false, true, ${printQuotedStringList( + Object.keys(treeshakePresets) + )}. You can also supply an object for more fine-grained control` + ) + ); }; const getHasModuleSideEffects = ( moduleSideEffectsOption: ModuleSideEffectsOption | undefined, - pureExternalModules: PureModulesOption | undefined, - warn: WarningHandler + pureExternalModules: PureModulesOption | undefined ): HasModuleSideEffects => { if (typeof moduleSideEffectsOption === 'boolean') { return () => moduleSideEffectsOption; @@ -294,7 +316,7 @@ const getHasModuleSideEffects = ( return id => ids.has(id); } if (moduleSideEffectsOption) { - warn( + error( errInvalidOption( 'treeshake.moduleSideEffects', 'please use one of false, "no-external", a function or an array' diff --git a/src/utils/options/normalizeOutputOptions.ts b/src/utils/options/normalizeOutputOptions.ts index a67860d0ef8..00d12056289 100644 --- a/src/utils/options/normalizeOutputOptions.ts +++ b/src/utils/options/normalizeOutputOptions.ts @@ -1,13 +1,8 @@ import { - GetInterop, - GlobalsOption, InternalModuleFormat, InteropType, - ManualChunksOption, - ModuleFormat, NormalizedInputOptions, NormalizedOutputOptions, - OptionsPaths, OutputOptions, SourcemapPathTransformOption } from '../../rollup/types'; @@ -18,7 +13,7 @@ import { sanitizeFileName as defaultSanitizeFileName } from '../sanitizeFileName import { GenericConfigObject, warnUnknownOptions } from './options'; export function normalizeOutputOptions( - config: GenericConfigObject, + config: OutputOptions, inputOptions: NormalizedInputOptions, unsetInputOptions: Set ): { options: NormalizedOutputOptions; unsetOptions: Set } { @@ -26,7 +21,7 @@ export function normalizeOutputOptions( // if the user did not select an explicit value const unsetOptions = new Set(unsetInputOptions); - const compact = (config.compact as boolean | undefined) || false; + const compact = config.compact || false; const format = getFormat(config); const inlineDynamicImports = getInlineDynamicImports(config, inputOptions); const preserveModules = getPreserveModules(config, inlineDynamicImports, inputOptions); @@ -34,65 +29,70 @@ export function normalizeOutputOptions( const outputOptions: NormalizedOutputOptions & OutputOptions = { amd: getAmd(config), - assetFileNames: - (config.assetFileNames as string | undefined) ?? 'assets/[name]-[hash][extname]', + assetFileNames: config.assetFileNames ?? 'assets/[name]-[hash][extname]', banner: getAddon(config, 'banner'), - chunkFileNames: (config.chunkFileNames as string | undefined) ?? '[name]-[hash].js', + chunkFileNames: config.chunkFileNames ?? '[name]-[hash].js', compact, dir: getDir(config, file), dynamicImportFunction: getDynamicImportFunction(config, inputOptions), entryFileNames: getEntryFileNames(config, unsetOptions), - esModule: (config.esModule as boolean | undefined) ?? true, + esModule: config.esModule ?? true, exports: getExports(config, unsetOptions), - extend: (config.extend as boolean | undefined) || false, - externalLiveBindings: (config.externalLiveBindings as boolean | undefined) ?? true, + extend: config.extend || false, + externalLiveBindings: config.externalLiveBindings ?? 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, + freeze: config.freeze ?? true, + globals: config.globals || {}, + hoistTransitiveImports: config.hoistTransitiveImports ?? true, indent: getIndent(config, compact), inlineDynamicImports, interop: getInterop(config, inputOptions), intro: getAddon(config, 'intro'), manualChunks: getManualChunks(config, inlineDynamicImports, preserveModules, inputOptions), 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, + name: config.name, + namespaceToStringTag: config.namespaceToStringTag || false, + noConflict: config.noConflict || false, outro: getAddon(config, 'outro'), - paths: (config.paths as OptionsPaths | undefined) || {}, - plugins: ensureArray(config.plugins) as Plugin[], - preferConst: (config.preferConst as boolean | undefined) || false, + paths: config.paths || {}, + plugins: ensureArray(config.plugins), + preferConst: config.preferConst || false, preserveModules, preserveModulesRoot: getPreserveModulesRoot(config), - sanitizeFileName: (typeof config.sanitizeFileName === 'function' - ? config.sanitizeFileName - : config.sanitizeFileName === false - ? id => id - : defaultSanitizeFileName) as NormalizedOutputOptions['sanitizeFileName'], - sourcemap: (config.sourcemap as boolean | 'inline' | 'hidden' | undefined) || false, - sourcemapExcludeSources: (config.sourcemapExcludeSources as boolean | undefined) || false, - sourcemapFile: config.sourcemapFile as string | undefined, + sanitizeFileName: + typeof config.sanitizeFileName === 'function' + ? config.sanitizeFileName + : config.sanitizeFileName === false + ? id => id + : defaultSanitizeFileName, + sourcemap: config.sourcemap || false, + sourcemapExcludeSources: config.sourcemapExcludeSources || false, + sourcemapFile: config.sourcemapFile, sourcemapPathTransform: config.sourcemapPathTransform as | SourcemapPathTransformOption | undefined, - strict: (config.strict as boolean | undefined) ?? true, - systemNullSetters: (config.systemNullSetters as boolean | undefined) || false, - validate: (config.validate as boolean | undefined) || false + strict: config.strict ?? true, + systemNullSetters: config.systemNullSetters || false, + validate: config.validate || false }; - warnUnknownOptions(config, Object.keys(outputOptions), 'output options', inputOptions.onwarn); + warnUnknownOptions( + config as GenericConfigObject, + Object.keys(outputOptions), + 'output options', + inputOptions.onwarn + ); return { options: outputOptions, unsetOptions }; } const getFile = ( - config: GenericConfigObject, + config: OutputOptions, preserveModules: boolean, inputOptions: NormalizedInputOptions -): string | undefined => { - const file = config.file as string | undefined; +): NormalizedOutputOptions['file'] => { + const { file } = config; if (typeof file === 'string') { if (preserveModules) { return error({ @@ -110,8 +110,8 @@ const getFile = ( return file; }; -const getFormat = (config: GenericConfigObject): InternalModuleFormat => { - const configFormat = config.format as ModuleFormat | undefined; +const getFormat = (config: OutputOptions): NormalizedOutputOptions['format'] => { + const configFormat = config.format; switch (configFormat) { case undefined: case 'es': @@ -137,12 +137,11 @@ const getFormat = (config: GenericConfigObject): InternalModuleFormat => { }; const getInlineDynamicImports = ( - config: GenericConfigObject, + config: OutputOptions, inputOptions: NormalizedInputOptions -): boolean => { +): NormalizedOutputOptions['inlineDynamicImports'] => { const inlineDynamicImports = - ((config.inlineDynamicImports as boolean | undefined) ?? inputOptions.inlineDynamicImports) || - false; + (config.inlineDynamicImports ?? inputOptions.inlineDynamicImports) || false; const { input } = inputOptions; if (inlineDynamicImports && (Array.isArray(input) ? input : Object.keys(input)).length > 1) { return error({ @@ -154,12 +153,11 @@ const getInlineDynamicImports = ( }; const getPreserveModules = ( - config: GenericConfigObject, + config: OutputOptions, inlineDynamicImports: boolean, inputOptions: NormalizedInputOptions -): boolean => { - const preserveModules = - ((config.preserveModules as boolean | undefined) ?? inputOptions.preserveModules) || false; +): NormalizedOutputOptions['preserveModules'] => { + const preserveModules = (config.preserveModules ?? inputOptions.preserveModules) || false; if (preserveModules) { if (inlineDynamicImports) { return error({ @@ -178,20 +176,22 @@ const getPreserveModules = ( return preserveModules; }; -const getPreserveModulesRoot = (config: GenericConfigObject): string | undefined => { - const preserveModulesRoot = config.preserveModulesRoot as string | null | undefined; +const getPreserveModulesRoot = ( + config: OutputOptions +): NormalizedOutputOptions['preserveModulesRoot'] => { + const { preserveModulesRoot } = config; if (preserveModulesRoot === null || preserveModulesRoot === undefined) { return undefined; } return resolve(preserveModulesRoot); }; -const getAmd = (config: GenericConfigObject): NormalizedOutputOptions['amd'] => { +const getAmd = (config: OutputOptions): NormalizedOutputOptions['amd'] => { const collection: { autoId: boolean; basePath: string; define: string; id?: string } = { autoId: false, basePath: '', define: 'define', - ...(config.amd as OutputOptions['amd']) + ...config.amd }; if ((collection.autoId || collection.basePath) && collection.id) { @@ -225,16 +225,21 @@ const getAmd = (config: GenericConfigObject): NormalizedOutputOptions['amd'] => return normalized; }; -const getAddon = (config: GenericConfigObject, name: string): (() => string | Promise) => { - const configAddon = config[name] as string | (() => string | Promise); +const getAddon = (config: OutputOptions, name: string): (() => string | Promise) => { + const configAddon = (config as GenericConfigObject)[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; +const getDir = ( + config: OutputOptions, + file: string | undefined +): NormalizedOutputOptions['dir'] => { + const { dir } = config; if (typeof dir === 'string' && typeof file === 'string') { return error({ code: 'INVALID_OPTION', @@ -246,10 +251,10 @@ const getDir = (config: GenericConfigObject, file: string | undefined): string | }; const getDynamicImportFunction = ( - config: GenericConfigObject, + config: OutputOptions, inputOptions: NormalizedInputOptions -): string | undefined => { - const configDynamicImportFunction = config.dynamicImportFunction as string | undefined; +): NormalizedOutputOptions['dynamicImportFunction'] => { + const configDynamicImportFunction = config.dynamicImportFunction; if (configDynamicImportFunction) { warnDeprecation( `The "output.dynamicImportFunction" option is deprecated. Use the "renderDynamicImport" plugin hook instead.`, @@ -260,8 +265,11 @@ const getDynamicImportFunction = ( return configDynamicImportFunction; }; -const getEntryFileNames = (config: GenericConfigObject, unsetOptions: Set): string => { - const configEntryFileNames = config.entryFileNames as string | undefined; +const getEntryFileNames = ( + config: OutputOptions, + unsetOptions: Set +): NormalizedOutputOptions['entryFileNames'] => { + const configEntryFileNames = config.entryFileNames; if (configEntryFileNames == null) { unsetOptions.add('entryFileNames'); } @@ -269,32 +277,33 @@ const getEntryFileNames = (config: GenericConfigObject, unsetOptions: Set -): 'default' | 'named' | 'none' | 'auto' { - const configExports = config.exports as string | undefined; +): NormalizedOutputOptions['exports'] { + const configExports = config.exports; if (configExports == null) { unsetOptions.add('exports'); } else if (!['default', 'named', 'none', 'auto'].includes(configExports)) { return error(errInvalidExportOptionValue(configExports)); } - return (configExports as 'default' | 'named' | 'none' | 'auto') || 'auto'; + return configExports || 'auto'; } -const getIndent = (config: GenericConfigObject, compact: boolean): string | true => { +const getIndent = (config: OutputOptions, compact: boolean): NormalizedOutputOptions['indent'] => { if (compact) { return ''; } - const configIndent = config.indent as string | boolean | undefined; + const configIndent = config.indent; return configIndent === false ? '' : configIndent ?? true; }; const ALLOWED_INTEROP_TYPES = new Set(['auto', 'esModule', 'default', 'defaultOnly', true, false]); + const getInterop = ( - config: GenericConfigObject, + config: OutputOptions, inputOptions: NormalizedInputOptions -): GetInterop => { - const configInterop = config.interop as InteropType | GetInterop | undefined; +): NormalizedOutputOptions['interop'] => { + const configInterop = config.interop; const validatedInteropTypes = new Set(); const validateInterop = (interop: InteropType): InteropType => { if (!validatedInteropTypes.has(interop)) { @@ -341,13 +350,12 @@ const getInterop = ( }; const getManualChunks = ( - config: GenericConfigObject, + config: OutputOptions, inlineDynamicImports: boolean, preserveModules: boolean, inputOptions: NormalizedInputOptions -): ManualChunksOption => { - const configManualChunks = - (config.manualChunks as ManualChunksOption | undefined) || inputOptions.manualChunks; +): NormalizedOutputOptions['manualChunks'] => { + const configManualChunks = config.manualChunks || inputOptions.manualChunks; if (configManualChunks) { if (inlineDynamicImports) { return error({ @@ -367,9 +375,8 @@ const getManualChunks = ( }; const getMinifyInternalExports = ( - config: GenericConfigObject, + config: OutputOptions, format: InternalModuleFormat, compact: boolean -): boolean => - (config.minifyInternalExports as boolean | undefined) ?? - (compact || format === 'es' || format === 'system'); +): NormalizedOutputOptions['minifyInternalExports'] => + config.minifyInternalExports ?? (compact || format === 'es' || format === 'system'); diff --git a/src/utils/options/options.ts b/src/utils/options/options.ts index 1d7c1465bed..22cf89d2923 100644 --- a/src/utils/options/options.ts +++ b/src/utils/options/options.ts @@ -1,4 +1,4 @@ -import { WarningHandler } from '../../rollup/types'; +import { InputOptions, NormalizedInputOptions, WarningHandler } from '../../rollup/types'; export interface GenericConfigObject { [key: string]: unknown; @@ -28,3 +28,33 @@ export function warnUnknownOptions( }); } } + +type ObjectValue = Base extends Record ? Base : never; + +export const treeshakePresets: { + [key in NonNullable< + ObjectValue['preset'] + >]: NormalizedInputOptions['treeshake']; +} = { + recommended: { + annotations: true, + moduleSideEffects: () => true, + propertyReadSideEffects: true, + tryCatchDeoptimization: true, + unknownGlobalSideEffects: false + }, + safest: { + annotations: true, + moduleSideEffects: () => true, + propertyReadSideEffects: true, + tryCatchDeoptimization: true, + unknownGlobalSideEffects: true + }, + smallest: { + annotations: true, + moduleSideEffects: () => false, + propertyReadSideEffects: false, + tryCatchDeoptimization: false, + unknownGlobalSideEffects: false + } +}; diff --git a/test/cli/samples/treeshake-preset-override/_config.js b/test/cli/samples/treeshake-preset-override/_config.js new file mode 100644 index 00000000000..dc8435858f8 --- /dev/null +++ b/test/cli/samples/treeshake-preset-override/_config.js @@ -0,0 +1,5 @@ +module.exports = { + description: 'overrides the treeshake option when using presets', + command: + 'rollup --config --treeshake recommended --treeshake.unknownGlobalSideEffects --no-treeshake.moduleSideEffects' +}; diff --git a/test/cli/samples/treeshake-preset-override/_expected.js b/test/cli/samples/treeshake-preset-override/_expected.js new file mode 100644 index 00000000000..a4958370a2b --- /dev/null +++ b/test/cli/samples/treeshake-preset-override/_expected.js @@ -0,0 +1,13 @@ +console.log('main'); + +({ + get foo() { + console.log('effect'); + } +}.foo); + +try { + const noeffect = 1; +} catch {} + +unknownGlobal; diff --git a/test/cli/samples/treeshake-preset-override/dep.js b/test/cli/samples/treeshake-preset-override/dep.js new file mode 100644 index 00000000000..b74a9837c07 --- /dev/null +++ b/test/cli/samples/treeshake-preset-override/dep.js @@ -0,0 +1 @@ +console.log('dep'); diff --git a/test/cli/samples/treeshake-preset-override/main.js b/test/cli/samples/treeshake-preset-override/main.js new file mode 100644 index 00000000000..2ef8f761fe7 --- /dev/null +++ b/test/cli/samples/treeshake-preset-override/main.js @@ -0,0 +1,15 @@ +import './dep.js'; + +console.log('main'); + +({ + get foo() { + console.log('effect'); + } +}.foo); + +try { + const noeffect = 1; +} catch {} + +unknownGlobal; diff --git a/test/cli/samples/treeshake-preset-override/rollup.config.js b/test/cli/samples/treeshake-preset-override/rollup.config.js new file mode 100644 index 00000000000..d905c343e65 --- /dev/null +++ b/test/cli/samples/treeshake-preset-override/rollup.config.js @@ -0,0 +1,10 @@ +export default { + input: 'main.js', + treeshake: { + unknownGlobalSideEffects: false, + tryCatchDeoptimization: false + }, + output: { + format: 'es' + } +} \ No newline at end of file diff --git a/test/cli/samples/treeshake-unknown-preset/_config.js b/test/cli/samples/treeshake-unknown-preset/_config.js new file mode 100644 index 00000000000..00ac3f303b0 --- /dev/null +++ b/test/cli/samples/treeshake-unknown-preset/_config.js @@ -0,0 +1,13 @@ +const { assertIncludes } = require('../../../utils.js'); + +module.exports = { + description: 'overrides the treeshake option when using presets', + command: 'rollup main.js --format es --treeshake unknown', + error: () => true, + stderr: stderr => { + assertIncludes( + stderr, + '[!] Error: Invalid value for option "treeshake" - valid values are false, true, "recommended", "safest" and "smallest". You can also supply an object for more fine-grained control.\n' + ); + } +}; diff --git a/test/cli/samples/treeshake-unknown-preset/main.js b/test/cli/samples/treeshake-unknown-preset/main.js new file mode 100644 index 00000000000..c0b933d7b56 --- /dev/null +++ b/test/cli/samples/treeshake-unknown-preset/main.js @@ -0,0 +1 @@ +console.log('main'); diff --git a/test/form/samples/treeshake-presets/preset-with-override/_config.js b/test/form/samples/treeshake-presets/preset-with-override/_config.js new file mode 100644 index 00000000000..73bd2448ad1 --- /dev/null +++ b/test/form/samples/treeshake-presets/preset-with-override/_config.js @@ -0,0 +1,25 @@ +const assert = require('assert'); +const path = require('path'); + +module.exports = { + description: 'allows using the "preset" option with overrides', + options: { + treeshake: { + preset: 'smallest', + unknownGlobalSideEffects: true + }, + plugins: [ + { + buildStart(options) { + assert.strictEqual(options.treeshake.propertyReadSideEffects, false); + assert.strictEqual(options.treeshake.tryCatchDeoptimization, false); + assert.strictEqual(options.treeshake.unknownGlobalSideEffects, true); + assert.strictEqual( + options.treeshake.moduleSideEffects(path.join(__dirname, 'dep.js')), + false + ); + } + } + ] + } +}; diff --git a/test/form/samples/treeshake-presets/preset-with-override/_expected.js b/test/form/samples/treeshake-presets/preset-with-override/_expected.js new file mode 100644 index 00000000000..b731633f86b --- /dev/null +++ b/test/form/samples/treeshake-presets/preset-with-override/_expected.js @@ -0,0 +1,3 @@ +console.log('main'); + +unknownGlobal; diff --git a/test/form/samples/treeshake-presets/preset-with-override/dep.js b/test/form/samples/treeshake-presets/preset-with-override/dep.js new file mode 100644 index 00000000000..b74a9837c07 --- /dev/null +++ b/test/form/samples/treeshake-presets/preset-with-override/dep.js @@ -0,0 +1 @@ +console.log('dep'); diff --git a/test/form/samples/treeshake-presets/preset-with-override/main.js b/test/form/samples/treeshake-presets/preset-with-override/main.js new file mode 100644 index 00000000000..2ef8f761fe7 --- /dev/null +++ b/test/form/samples/treeshake-presets/preset-with-override/main.js @@ -0,0 +1,15 @@ +import './dep.js'; + +console.log('main'); + +({ + get foo() { + console.log('effect'); + } +}.foo); + +try { + const noeffect = 1; +} catch {} + +unknownGlobal; diff --git a/test/form/samples/treeshake-presets/recommended/_config.js b/test/form/samples/treeshake-presets/recommended/_config.js new file mode 100644 index 00000000000..e943af3e2aa --- /dev/null +++ b/test/form/samples/treeshake-presets/recommended/_config.js @@ -0,0 +1,22 @@ +const assert = require('assert'); +const path = require('path'); + +module.exports = { + description: 'handles treeshake preset "recommended"', + options: { + treeshake: 'recommended', + plugins: [ + { + buildStart(options) { + assert.strictEqual(options.treeshake.propertyReadSideEffects, true); + assert.strictEqual(options.treeshake.tryCatchDeoptimization, true); + assert.strictEqual(options.treeshake.unknownGlobalSideEffects, false); + assert.strictEqual( + options.treeshake.moduleSideEffects(path.join(__dirname, 'dep.js')), + true + ); + } + } + ] + } +}; diff --git a/test/form/samples/treeshake-presets/recommended/_expected.js b/test/form/samples/treeshake-presets/recommended/_expected.js new file mode 100644 index 00000000000..65f08abb6b4 --- /dev/null +++ b/test/form/samples/treeshake-presets/recommended/_expected.js @@ -0,0 +1,13 @@ +console.log('dep'); + +console.log('main'); + +({ + get foo() { + console.log('effect'); + } +}.foo); + +try { + const noeffect = 1; +} catch {} diff --git a/test/form/samples/treeshake-presets/recommended/dep.js b/test/form/samples/treeshake-presets/recommended/dep.js new file mode 100644 index 00000000000..b74a9837c07 --- /dev/null +++ b/test/form/samples/treeshake-presets/recommended/dep.js @@ -0,0 +1 @@ +console.log('dep'); diff --git a/test/form/samples/treeshake-presets/recommended/main.js b/test/form/samples/treeshake-presets/recommended/main.js new file mode 100644 index 00000000000..2ef8f761fe7 --- /dev/null +++ b/test/form/samples/treeshake-presets/recommended/main.js @@ -0,0 +1,15 @@ +import './dep.js'; + +console.log('main'); + +({ + get foo() { + console.log('effect'); + } +}.foo); + +try { + const noeffect = 1; +} catch {} + +unknownGlobal; diff --git a/test/form/samples/treeshake-presets/safest/_config.js b/test/form/samples/treeshake-presets/safest/_config.js new file mode 100644 index 00000000000..35b71dfd8a1 --- /dev/null +++ b/test/form/samples/treeshake-presets/safest/_config.js @@ -0,0 +1,22 @@ +const assert = require('assert'); +const path = require('path'); + +module.exports = { + description: 'handles treeshake preset "safest"', + options: { + treeshake: 'safest', + plugins: [ + { + buildStart(options) { + assert.strictEqual(options.treeshake.propertyReadSideEffects, true); + assert.strictEqual(options.treeshake.tryCatchDeoptimization, true); + assert.strictEqual(options.treeshake.unknownGlobalSideEffects, true); + assert.strictEqual( + options.treeshake.moduleSideEffects(path.join(__dirname, 'dep.js')), + true + ); + } + } + ] + } +}; diff --git a/test/form/samples/treeshake-presets/safest/_expected.js b/test/form/samples/treeshake-presets/safest/_expected.js new file mode 100644 index 00000000000..3df383b997e --- /dev/null +++ b/test/form/samples/treeshake-presets/safest/_expected.js @@ -0,0 +1,15 @@ +console.log('dep'); + +console.log('main'); + +({ + get foo() { + console.log('effect'); + } +}.foo); + +try { + const noeffect = 1; +} catch {} + +unknownGlobal; diff --git a/test/form/samples/treeshake-presets/safest/dep.js b/test/form/samples/treeshake-presets/safest/dep.js new file mode 100644 index 00000000000..b74a9837c07 --- /dev/null +++ b/test/form/samples/treeshake-presets/safest/dep.js @@ -0,0 +1 @@ +console.log('dep'); diff --git a/test/form/samples/treeshake-presets/safest/main.js b/test/form/samples/treeshake-presets/safest/main.js new file mode 100644 index 00000000000..2ef8f761fe7 --- /dev/null +++ b/test/form/samples/treeshake-presets/safest/main.js @@ -0,0 +1,15 @@ +import './dep.js'; + +console.log('main'); + +({ + get foo() { + console.log('effect'); + } +}.foo); + +try { + const noeffect = 1; +} catch {} + +unknownGlobal; diff --git a/test/form/samples/treeshake-presets/smallest/_config.js b/test/form/samples/treeshake-presets/smallest/_config.js new file mode 100644 index 00000000000..ac49dae5a46 --- /dev/null +++ b/test/form/samples/treeshake-presets/smallest/_config.js @@ -0,0 +1,22 @@ +const assert = require('assert'); +const path = require('path'); + +module.exports = { + description: 'handles treeshake preset "smallest"', + options: { + treeshake: 'smallest', + plugins: [ + { + buildStart(options) { + assert.strictEqual(options.treeshake.propertyReadSideEffects, false); + assert.strictEqual(options.treeshake.tryCatchDeoptimization, false); + assert.strictEqual(options.treeshake.unknownGlobalSideEffects, false); + assert.strictEqual( + options.treeshake.moduleSideEffects(path.join(__dirname, 'dep.js')), + false + ); + } + } + ] + } +}; diff --git a/test/form/samples/treeshake-presets/smallest/_expected.js b/test/form/samples/treeshake-presets/smallest/_expected.js new file mode 100644 index 00000000000..c0b933d7b56 --- /dev/null +++ b/test/form/samples/treeshake-presets/smallest/_expected.js @@ -0,0 +1 @@ +console.log('main'); diff --git a/test/form/samples/treeshake-presets/smallest/dep.js b/test/form/samples/treeshake-presets/smallest/dep.js new file mode 100644 index 00000000000..b74a9837c07 --- /dev/null +++ b/test/form/samples/treeshake-presets/smallest/dep.js @@ -0,0 +1 @@ +console.log('dep'); diff --git a/test/form/samples/treeshake-presets/smallest/main.js b/test/form/samples/treeshake-presets/smallest/main.js new file mode 100644 index 00000000000..2ef8f761fe7 --- /dev/null +++ b/test/form/samples/treeshake-presets/smallest/main.js @@ -0,0 +1,15 @@ +import './dep.js'; + +console.log('main'); + +({ + get foo() { + console.log('effect'); + } +}.foo); + +try { + const noeffect = 1; +} catch {} + +unknownGlobal; diff --git a/test/form/samples/treeshake-presets/true/_config.js b/test/form/samples/treeshake-presets/true/_config.js new file mode 100644 index 00000000000..9a024352632 --- /dev/null +++ b/test/form/samples/treeshake-presets/true/_config.js @@ -0,0 +1,22 @@ +const assert = require('assert'); +const path = require('path'); + +module.exports = { + description: 'handles treeshake preset true', + options: { + treeshake: true, + plugins: [ + { + buildStart(options) { + assert.strictEqual(options.treeshake.propertyReadSideEffects, true); + assert.strictEqual(options.treeshake.tryCatchDeoptimization, true); + assert.strictEqual(options.treeshake.unknownGlobalSideEffects, true); + assert.strictEqual( + options.treeshake.moduleSideEffects(path.join(__dirname, 'dep.js')), + true + ); + } + } + ] + } +}; diff --git a/test/form/samples/treeshake-presets/true/_expected.js b/test/form/samples/treeshake-presets/true/_expected.js new file mode 100644 index 00000000000..3df383b997e --- /dev/null +++ b/test/form/samples/treeshake-presets/true/_expected.js @@ -0,0 +1,15 @@ +console.log('dep'); + +console.log('main'); + +({ + get foo() { + console.log('effect'); + } +}.foo); + +try { + const noeffect = 1; +} catch {} + +unknownGlobal; diff --git a/test/form/samples/treeshake-presets/true/dep.js b/test/form/samples/treeshake-presets/true/dep.js new file mode 100644 index 00000000000..b74a9837c07 --- /dev/null +++ b/test/form/samples/treeshake-presets/true/dep.js @@ -0,0 +1 @@ +console.log('dep'); diff --git a/test/form/samples/treeshake-presets/true/main.js b/test/form/samples/treeshake-presets/true/main.js new file mode 100644 index 00000000000..2ef8f761fe7 --- /dev/null +++ b/test/form/samples/treeshake-presets/true/main.js @@ -0,0 +1,15 @@ +import './dep.js'; + +console.log('main'); + +({ + get foo() { + console.log('effect'); + } +}.foo); + +try { + const noeffect = 1; +} catch {} + +unknownGlobal; diff --git a/test/function/samples/module-side-effects/invalid-option/_config.js b/test/function/samples/module-side-effects/invalid-option/_config.js index 0641fcdbfb2..fd2c0021e0b 100644 --- a/test/function/samples/module-side-effects/invalid-option/_config.js +++ b/test/function/samples/module-side-effects/invalid-option/_config.js @@ -5,11 +5,9 @@ module.exports = { moduleSideEffects: 'what-is-this?' } }, - warnings: [ - { - code: 'INVALID_OPTION', - message: - 'Invalid value for option "treeshake.moduleSideEffects" - please use one of false, "no-external", a function or an array.' - } - ] + error: { + code: 'INVALID_OPTION', + message: + 'Invalid value for option "treeshake.moduleSideEffects" - please use one of false, "no-external", a function or an array.' + } }; diff --git a/test/function/samples/unknown-treeshake-preset/_config.js b/test/function/samples/unknown-treeshake-preset/_config.js new file mode 100644 index 00000000000..04145758989 --- /dev/null +++ b/test/function/samples/unknown-treeshake-preset/_config.js @@ -0,0 +1,11 @@ +module.exports = { + description: 'throws for unknown presets for the treeshake option', + options: { + treeshake: { preset: 'some-string' } + }, + error: { + code: 'INVALID_OPTION', + message: + 'Invalid value for option "treeshake.preset" - valid values are "recommended", "safest" and "smallest".' + } +}; diff --git a/test/function/samples/unknown-treeshake-preset/_expected.js b/test/function/samples/unknown-treeshake-preset/_expected.js new file mode 100644 index 00000000000..65f08abb6b4 --- /dev/null +++ b/test/function/samples/unknown-treeshake-preset/_expected.js @@ -0,0 +1,13 @@ +console.log('dep'); + +console.log('main'); + +({ + get foo() { + console.log('effect'); + } +}.foo); + +try { + const noeffect = 1; +} catch {} diff --git a/test/function/samples/unknown-treeshake-preset/main.js b/test/function/samples/unknown-treeshake-preset/main.js new file mode 100644 index 00000000000..7e37f689f8e --- /dev/null +++ b/test/function/samples/unknown-treeshake-preset/main.js @@ -0,0 +1 @@ +throw new Error('not executed'); diff --git a/test/function/samples/unknown-treeshake-value/_config.js b/test/function/samples/unknown-treeshake-value/_config.js new file mode 100644 index 00000000000..d4aceb40a81 --- /dev/null +++ b/test/function/samples/unknown-treeshake-value/_config.js @@ -0,0 +1,11 @@ +module.exports = { + description: 'throws for unknown string values for the treeshake option', + options: { + treeshake: 'some-string' + }, + error: { + code: 'INVALID_OPTION', + message: + 'Invalid value for option "treeshake" - valid values are false, true, "recommended", "safest" and "smallest". You can also supply an object for more fine-grained control.' + } +}; diff --git a/test/function/samples/unknown-treeshake-value/_expected.js b/test/function/samples/unknown-treeshake-value/_expected.js new file mode 100644 index 00000000000..65f08abb6b4 --- /dev/null +++ b/test/function/samples/unknown-treeshake-value/_expected.js @@ -0,0 +1,13 @@ +console.log('dep'); + +console.log('main'); + +({ + get foo() { + console.log('effect'); + } +}.foo); + +try { + const noeffect = 1; +} catch {} diff --git a/test/function/samples/unknown-treeshake-value/main.js b/test/function/samples/unknown-treeshake-value/main.js new file mode 100644 index 00000000000..7e37f689f8e --- /dev/null +++ b/test/function/samples/unknown-treeshake-value/main.js @@ -0,0 +1 @@ +throw new Error('not executed');