From 0e3445b6a16bd7b388c8b1ea31129e0df5c8b29e Mon Sep 17 00:00:00 2001 From: kzc Date: Fri, 12 Feb 2021 08:47:19 -0500 Subject: [PATCH] implement `validate` output option and `--validate` CLI option (#3952) * implement `validate` output option and `--validate` CLI option * optionally verifies the generated output by parsing it with acorn * disabled by default * enable the validate option in the majority of tests * increase mocha test timeout due to circleci node-v14-latest * Add missing documentation * Refine error output and tests * Normalized options should be normalized * Skip test on windows Co-authored-by: Lukas Taegert-Atkinson Co-authored-by: Lukas Taegert-Atkinson --- cli/help.md | 1 + docs/01-command-line-reference.md | 2 ++ docs/02-javascript-api.md | 1 + docs/999-big-list-of-options.md | 7 ++++++ src/Bundle.ts | 17 +++++++++++++- src/rollup/types.d.ts | 2 ++ src/utils/error.ts | 13 +++++++++++ src/utils/options/mergeOptions.ts | 3 ++- src/utils/options/normalizeOutputOptions.ts | 3 ++- test/chunking-form/index.js | 3 ++- test/cli/samples/validate/_config.js | 18 +++++++++++++++ test/cli/samples/validate/main.js | 1 + test/form/index.js | 3 ++- .../samples/output-options-hook/_config.js | 3 ++- .../samples/validate-output/_config.js | 23 +++++++++++++++++++ test/function/samples/validate-output/main.js | 1 + test/misc/misc.js | 2 +- test/misc/optionList.js | 4 ++-- test/test.js | 2 +- 19 files changed, 99 insertions(+), 10 deletions(-) create mode 100644 test/cli/samples/validate/_config.js create mode 100644 test/cli/samples/validate/main.js create mode 100644 test/function/samples/validate-output/_config.js create mode 100644 test/function/samples/validate-output/main.js diff --git a/cli/help.md b/cli/help.md index 17ffc652150..b6b853dc50b 100644 --- a/cli/help.md +++ b/cli/help.md @@ -72,6 +72,7 @@ Basic options: --watch.skipWrite Do not write files to disk when watching --watch.exclude Exclude files from being watched --watch.include Limit watching to specified files +--validate Validate output Examples: diff --git a/docs/01-command-line-reference.md b/docs/01-command-line-reference.md index 099903738de..f3438bf71c1 100755 --- a/docs/01-command-line-reference.md +++ b/docs/01-command-line-reference.md @@ -83,6 +83,7 @@ export default { // can be an array (for multiple inputs) sourcemapExcludeSources, sourcemapFile, sourcemapPathTransform, + validate, // danger zone amd, @@ -330,6 +331,7 @@ Many options have command line equivalents. In those cases, any arguments passed --watch.skipWrite Do not write files to disk when watching --watch.exclude Exclude files from being watched --watch.include Limit watching to specified files +--validate Validate output ``` 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/02-javascript-api.md b/docs/02-javascript-api.md index cfa9429466d..cf10af2ae1b 100755 --- a/docs/02-javascript-api.md +++ b/docs/02-javascript-api.md @@ -148,6 +148,7 @@ const outputOptions = { sourcemapExcludeSources, sourcemapFile, sourcemapPathTransform, + validate, // danger zone amd, diff --git a/docs/999-big-list-of-options.md b/docs/999-big-list-of-options.md index c8edd7274ab..d70ca194b8e 100755 --- a/docs/999-big-list-of-options.md +++ b/docs/999-big-list-of-options.md @@ -924,6 +924,13 @@ export default ({ }); ``` +#### output.validate +Type: `boolean`
+CLI: `--validate`/`--no-validate`
+Default: `false` + +Re-parses each generated chunk to detect if the generated code is valid JavaScript. This can be useful to debug output generated by plugins that use the [`renderChunk`](guide/en/#renderchunk) hook to transform code. + #### preserveEntrySignatures Type: `"strict" | "allow-extension" | "exports-only" | false`
CLI: `--preserveEntrySignatures `/`--no-preserveEntrySignatures`
diff --git a/src/Bundle.ts b/src/Bundle.ts index 1d161350104..21afff03c38 100644 --- a/src/Bundle.ts +++ b/src/Bundle.ts @@ -14,7 +14,12 @@ import { import { Addons, createAddons } from './utils/addons'; import { getChunkAssignments } from './utils/chunkAssignment'; import commondir from './utils/commondir'; -import { errCannotAssignModuleToChunk, error, warnDeprecation } from './utils/error'; +import { + errCannotAssignModuleToChunk, + errChunkInvalid, + error, + warnDeprecation +} from './utils/error'; import { sortByExecutionOrder } from './utils/executionOrder'; import { FILE_PLACEHOLDER } from './utils/FileEmitter'; import { basename, isAbsolute } from './utils/path'; @@ -174,6 +179,16 @@ export default class Bundle { ); file.type = 'asset'; } + if (this.outputOptions.validate && typeof file.code == 'string') { + try { + this.graph.contextParse(file.code, { + allowHashBang: true, + ecmaVersion: 'latest' + }); + } catch (exception) { + return error(errChunkInvalid(file, exception)); + } + } } this.pluginDriver.finaliseAssets(); } diff --git a/src/rollup/types.d.ts b/src/rollup/types.d.ts index a185e66f80a..b12911dc00c 100644 --- a/src/rollup/types.d.ts +++ b/src/rollup/types.d.ts @@ -653,6 +653,7 @@ export interface OutputOptions { sourcemapPathTransform?: SourcemapPathTransformOption; strict?: boolean; systemNullSetters?: boolean; + validate?: boolean; } export interface NormalizedOutputOptions { @@ -696,6 +697,7 @@ export interface NormalizedOutputOptions { sourcemapPathTransform: SourcemapPathTransformOption | undefined; strict: boolean; systemNullSetters: boolean; + validate: boolean; } export type WarningHandlerWithDefault = ( diff --git a/src/utils/error.ts b/src/utils/error.ts index f4066823a61..176a8fc3312 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -45,6 +45,7 @@ export const enum Errors { BAD_LOADER = 'BAD_LOADER', CANNOT_EMIT_FROM_OPTIONS_HOOK = 'CANNOT_EMIT_FROM_OPTIONS_HOOK', CHUNK_NOT_GENERATED = 'CHUNK_NOT_GENERATED', + CHUNK_INVALID = 'CHUNK_INVALID', CIRCULAR_REEXPORT = 'CIRCULAR_REEXPORT', CYCLIC_CROSS_CHUNK_REEXPORT = 'CYCLIC_CROSS_CHUNK_REEXPORT', DEPRECATED_FEATURE = 'DEPRECATED_FEATURE', @@ -93,6 +94,18 @@ export function errChunkNotGeneratedForFileName(name: string) { }; } +export function errChunkInvalid( + { fileName, code }: { code: string; fileName: string }, + exception: { loc: { column: number; line: number }; message: string } +) { + const errorProps = { + code: Errors.CHUNK_INVALID, + message: `Chunk "${fileName}" is not valid JavaScript: ${exception.message}.` + }; + augmentCodeLocation(errorProps, exception.loc, code, fileName); + return errorProps; +} + export function errCircularReexport(exportName: string, importedModule: string) { return { code: Errors.CIRCULAR_REEXPORT, diff --git a/src/utils/options/mergeOptions.ts b/src/utils/options/mergeOptions.ts index cb6cd097027..843109df900 100644 --- a/src/utils/options/mergeOptions.ts +++ b/src/utils/options/mergeOptions.ts @@ -227,7 +227,8 @@ function mergeOutputOptions( sourcemapFile: getOption('sourcemapFile'), sourcemapPathTransform: getOption('sourcemapPathTransform'), strict: getOption('strict'), - systemNullSetters: getOption('systemNullSetters') + systemNullSetters: getOption('systemNullSetters'), + validate: getOption('validate') }; warnUnknownOptions(config, Object.keys(outputOptions), 'output options', warn); diff --git a/src/utils/options/normalizeOutputOptions.ts b/src/utils/options/normalizeOutputOptions.ts index 8fc9c9e99df..5cf14024e29 100644 --- a/src/utils/options/normalizeOutputOptions.ts +++ b/src/utils/options/normalizeOutputOptions.ts @@ -73,7 +73,8 @@ export function normalizeOutputOptions( | SourcemapPathTransformOption | undefined, strict: (config.strict as boolean | undefined) ?? true, - systemNullSetters: (config.systemNullSetters as boolean | undefined) || false + systemNullSetters: (config.systemNullSetters as boolean | undefined) || false, + validate: (config.validate as boolean | undefined) || false }; warnUnknownOptions(config, Object.keys(outputOptions), 'output options', inputOptions.onwarn); diff --git a/test/chunking-form/index.js b/test/chunking-form/index.js index 4e34c6b9f41..509be3cbe44 100644 --- a/test/chunking-form/index.js +++ b/test/chunking-form/index.js @@ -44,7 +44,8 @@ runTestSuiteWithSamples('chunking form', path.resolve(__dirname, 'samples'), (di dir: `${dir}/_actual/${format}`, exports: 'auto', format, - chunkFileNames: 'generated-[name].js' + chunkFileNames: 'generated-[name].js', + validate: true }, (config.options || {}).output || {} ), diff --git a/test/cli/samples/validate/_config.js b/test/cli/samples/validate/_config.js new file mode 100644 index 00000000000..934ca67c2a5 --- /dev/null +++ b/test/cli/samples/validate/_config.js @@ -0,0 +1,18 @@ +const { assertIncludes } = require('../../../utils.js'); + +module.exports = { + description: 'use CLI --validate to test whether output is well formed', + skipIfWindows: true, + command: `rollup main.js --silent --outro 'console.log("end"); /*' -o _actual/out.js --validate`, + error: () => true, + stderr: stderr => + assertIncludes( + stderr, + `[!] Error: Chunk "out.js" is not valid JavaScript: Unterminated comment (3:20). +out.js (3:20) +1: console.log(2 ); +2: +3: console.log("end"); /* + ^` + ) +}; diff --git a/test/cli/samples/validate/main.js b/test/cli/samples/validate/main.js new file mode 100644 index 00000000000..8b068518106 --- /dev/null +++ b/test/cli/samples/validate/main.js @@ -0,0 +1 @@ +console.log(1 ? 2 : 3); diff --git a/test/form/index.js b/test/form/index.js index 4e329c42987..dc5b34e9e7b 100644 --- a/test/form/index.js +++ b/test/form/index.js @@ -47,7 +47,8 @@ runTestSuiteWithSamples('form', path.resolve(__dirname, 'samples'), (dir, config { exports: 'auto', file: inputFile, - format: defaultFormat + format: defaultFormat, + // validate: true // add when systemjs bugs fixed }, (config.options || {}).output || {} ), diff --git a/test/function/samples/output-options-hook/_config.js b/test/function/samples/output-options-hook/_config.js index b6855ab3c91..904cf55577e 100644 --- a/test/function/samples/output-options-hook/_config.js +++ b/test/function/samples/output-options-hook/_config.js @@ -45,7 +45,8 @@ module.exports = { sourcemap: false, sourcemapExcludeSources: false, strict: true, - systemNullSetters: false + systemNullSetters: false, + validate: false }); assert.strictEqual(options.banner(), 'exports.bar = 43;'); assert.ok(/^\d+\.\d+\.\d+/.test(this.meta.rollupVersion)); diff --git a/test/function/samples/validate-output/_config.js b/test/function/samples/validate-output/_config.js new file mode 100644 index 00000000000..9850e118ef0 --- /dev/null +++ b/test/function/samples/validate-output/_config.js @@ -0,0 +1,23 @@ +module.exports = { + description: 'handles validate failure', + options: { + output: { + outro: '/*', + validate: true + } + }, + generateError: { + code: 'CHUNK_INVALID', + message: 'Chunk "main.js" is not valid JavaScript: Unterminated comment (5:0).', + frame: ` +3: throw new Error('Not executed'); +4: +5: /* + ^`, + loc: { + column: 0, + file: 'main.js', + line: 5 + } + } +}; diff --git a/test/function/samples/validate-output/main.js b/test/function/samples/validate-output/main.js new file mode 100644 index 00000000000..a9244a453fb --- /dev/null +++ b/test/function/samples/validate-output/main.js @@ -0,0 +1 @@ +throw new Error('Not executed'); diff --git a/test/misc/misc.js b/test/misc/misc.js index 2840e429307..347cd46f3f5 100644 --- a/test/misc/misc.js +++ b/test/misc/misc.js @@ -1,6 +1,6 @@ const assert = require('assert'); const rollup = require('../../dist/rollup'); -const { loader } = require('../utils.js'); +const { assertIncludes, loader } = require('../utils.js'); describe('misc', () => { it('avoids modification of options or their properties', () => { diff --git a/test/misc/optionList.js b/test/misc/optionList.js index 3d013653035..05392922320 100644 --- a/test/misc/optionList.js +++ b/test/misc/optionList.js @@ -1,6 +1,6 @@ exports.input = 'acorn, acornInjectPlugins, cache, context, experimentalCacheExpiry, external, inlineDynamicImports, input, manualChunks, moduleContext, onwarn, perf, plugins, preserveEntrySignatures, preserveModules, preserveSymlinks, shimMissingExports, strictDeprecations, treeshake, watch'; exports.flags = - 'acorn, acornInjectPlugins, amd, assetFileNames, banner, c, cache, chunkFileNames, compact, config, context, d, dir, dynamicImportFunction, e, entryFileNames, environment, esModule, experimentalCacheExpiry, exports, extend, external, externalLiveBindings, f, failAfterWarnings, file, footer, format, freeze, g, globals, h, hoistTransitiveImports, i, indent, inlineDynamicImports, input, interop, intro, m, manualChunks, minifyInternalExports, moduleContext, n, name, namespaceToStringTag, noConflict, o, onwarn, outro, p, paths, perf, plugin, plugins, preferConst, preserveEntrySignatures, preserveModules, preserveModulesRoot, preserveSymlinks, shimMissingExports, silent, sourcemap, sourcemapExcludeSources, sourcemapFile, stdin, strict, strictDeprecations, systemNullSetters, treeshake, v, w, waitForBundleInput, watch'; + 'acorn, acornInjectPlugins, amd, assetFileNames, banner, c, cache, chunkFileNames, compact, config, context, d, dir, dynamicImportFunction, e, entryFileNames, environment, esModule, experimentalCacheExpiry, exports, extend, external, externalLiveBindings, f, failAfterWarnings, file, footer, format, freeze, g, globals, h, hoistTransitiveImports, i, indent, inlineDynamicImports, input, interop, intro, m, manualChunks, minifyInternalExports, moduleContext, n, name, namespaceToStringTag, noConflict, o, onwarn, outro, p, paths, perf, plugin, plugins, preferConst, preserveEntrySignatures, preserveModules, preserveModulesRoot, preserveSymlinks, shimMissingExports, silent, sourcemap, sourcemapExcludeSources, sourcemapFile, stdin, strict, strictDeprecations, systemNullSetters, treeshake, v, validate, w, waitForBundleInput, watch'; exports.output = - 'amd, assetFileNames, banner, chunkFileNames, compact, dir, dynamicImportFunction, entryFileNames, esModule, exports, extend, externalLiveBindings, file, footer, format, freeze, globals, hoistTransitiveImports, indent, inlineDynamicImports, interop, intro, manualChunks, minifyInternalExports, name, namespaceToStringTag, noConflict, outro, paths, plugins, preferConst, preserveModules, preserveModulesRoot, sourcemap, sourcemapExcludeSources, sourcemapFile, sourcemapPathTransform, strict, systemNullSetters'; + 'amd, assetFileNames, banner, chunkFileNames, compact, dir, dynamicImportFunction, entryFileNames, esModule, exports, extend, externalLiveBindings, file, footer, format, freeze, globals, hoistTransitiveImports, indent, inlineDynamicImports, interop, intro, manualChunks, minifyInternalExports, name, namespaceToStringTag, noConflict, outro, paths, plugins, preferConst, preserveModules, preserveModulesRoot, sourcemap, sourcemapExcludeSources, sourcemapFile, sourcemapPathTransform, strict, systemNullSetters, validate'; diff --git a/test/test.js b/test/test.js index 699c48846de..119b13391e1 100644 --- a/test/test.js +++ b/test/test.js @@ -1,7 +1,7 @@ require('source-map-support').install(); describe('rollup', function () { - this.timeout(15000); + this.timeout(30000); require('./misc/index.js'); require('./function/index.js'); require('./form/index.js');