From 0edd6a031ac660421fca465cbb182b4b58897d97 Mon Sep 17 00:00:00 2001 From: Prateek Bhatnagar Date: Thu, 23 Jan 2020 13:09:12 -0800 Subject: [PATCH] Adding conformance webpack plugin (#9716) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * adding Conformance Plugin behind a flag * fixing compiler ts error * fixing spelling errors 🤦🏻‍♂️ * addressing comments * bug fix * making it const enum * reverting const enum Co-authored-by: Joe Haddad --- packages/next/build/webpack-config.ts | 8 + .../TestInterface.ts | 36 +++++ .../checks/minification-conformance-check.ts | 27 ++++ .../webpack-conformance-plugin/constants.ts | 8 + .../webpack-conformance-plugin/index.ts | 149 ++++++++++++++++++ packages/next/package.json | 2 + yarn.lock | 19 ++- 7 files changed, 247 insertions(+), 2 deletions(-) create mode 100644 packages/next/build/webpack/plugins/webpack-conformance-plugin/TestInterface.ts create mode 100644 packages/next/build/webpack/plugins/webpack-conformance-plugin/checks/minification-conformance-check.ts create mode 100644 packages/next/build/webpack/plugins/webpack-conformance-plugin/constants.ts create mode 100644 packages/next/build/webpack/plugins/webpack-conformance-plugin/index.ts diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index ab38642e45bf46d..b94f2fd6526c772 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -44,6 +44,9 @@ import { ProfilingPlugin } from './webpack/plugins/profiling-plugin' import { ReactLoadablePlugin } from './webpack/plugins/react-loadable-plugin' import { ServerlessPlugin } from './webpack/plugins/serverless-plugin' import { TerserPlugin } from './webpack/plugins/terser-webpack-plugin/src/index' +import WebpackConformancePlugin, { + MinificationConformanceCheck, +} from './webpack/plugins/webpack-conformance-plugin' type ExcludesFalse = (x: T | false) => x is T @@ -825,6 +828,11 @@ export default async function getBaseWebpackConfig( chunkFilename: (inputChunkName: string) => inputChunkName.replace(/\.js$/, '.module.js'), }), + config.experimental.conformance && + !dev && + new WebpackConformancePlugin({ + tests: [new MinificationConformanceCheck()], + }), ].filter((Boolean as any) as ExcludesFalse), } diff --git a/packages/next/build/webpack/plugins/webpack-conformance-plugin/TestInterface.ts b/packages/next/build/webpack/plugins/webpack-conformance-plugin/TestInterface.ts new file mode 100644 index 000000000000000..47a2688b4b8400a --- /dev/null +++ b/packages/next/build/webpack/plugins/webpack-conformance-plugin/TestInterface.ts @@ -0,0 +1,36 @@ +import { NodePath } from 'ast-types/lib/node-path' + +export interface IConformanceAnomaly { + message: string + stack_trace?: string +} + +export enum IConformanceTestStatus { + SUCCESS, + FAILED, +} +export interface IConformanceTestResult { + result: IConformanceTestStatus + warnings?: Array + errors?: Array +} + +export interface IParsedModuleDetails { + request: string +} + +export type NodeInspector = ( + node: NodePath, + details: IParsedModuleDetails +) => IConformanceTestResult + +export interface IGetAstNodeResult { + visitor: string + inspectNode: NodeInspector +} + +export interface IWebpackConformanceTest { + buildStared?: (options: any) => IConformanceTestResult + getAstNode?: () => IGetAstNodeResult[] + buildCompleted?: (assets: any) => IConformanceTestResult +} diff --git a/packages/next/build/webpack/plugins/webpack-conformance-plugin/checks/minification-conformance-check.ts b/packages/next/build/webpack/plugins/webpack-conformance-plugin/checks/minification-conformance-check.ts new file mode 100644 index 000000000000000..dbef59f524cda0e --- /dev/null +++ b/packages/next/build/webpack/plugins/webpack-conformance-plugin/checks/minification-conformance-check.ts @@ -0,0 +1,27 @@ +import { + IWebpackConformanceTest, + IConformanceTestResult, + IConformanceTestStatus, +} from '../TestInterface' +import { CONFORMANCE_ERROR_PREFIX } from '../constants' + +export class MinificationConformanceCheck implements IWebpackConformanceTest { + public buildStared(options: any): IConformanceTestResult { + // TODO(prateekbh@): Implement warning for using Terser maybe? + + if (options.optimization.minimize === false) { + return { + result: IConformanceTestStatus.FAILED, + errors: [ + { + message: `${CONFORMANCE_ERROR_PREFIX}: Minification is disabled for this build.\nDisabling minification can result in serious performance degradation.`, + }, + ], + } + } else { + return { + result: IConformanceTestStatus.SUCCESS, + } + } + } +} diff --git a/packages/next/build/webpack/plugins/webpack-conformance-plugin/constants.ts b/packages/next/build/webpack/plugins/webpack-conformance-plugin/constants.ts new file mode 100644 index 000000000000000..3f0a238f0fb0021 --- /dev/null +++ b/packages/next/build/webpack/plugins/webpack-conformance-plugin/constants.ts @@ -0,0 +1,8 @@ +import chalk from 'chalk' + +const { red, yellow } = chalk + +export const CONFORMANCE_ERROR_PREFIX: string = red('[BUILD CONFORMANCE ERROR]') +export const CONFORMANCE_WARNING_PREFIX: string = yellow( + '[BUILD CONFORMANCE WARNING]' +) diff --git a/packages/next/build/webpack/plugins/webpack-conformance-plugin/index.ts b/packages/next/build/webpack/plugins/webpack-conformance-plugin/index.ts new file mode 100644 index 000000000000000..c52be0a5da59455 --- /dev/null +++ b/packages/next/build/webpack/plugins/webpack-conformance-plugin/index.ts @@ -0,0 +1,149 @@ +import { Compiler, compilation } from 'webpack' +import { + IConformanceTestResult, + IWebpackConformanceTest, + IConformanceAnomaly, + IGetAstNodeResult, + NodeInspector, + IConformanceTestStatus, +} from './TestInterface' +import { NodePath } from 'ast-types/lib/node-path' +import { visit } from 'recast' + +export { MinificationConformanceCheck } from './checks/minification-conformance-check' +// export { ReactSyncScriptsConformanceTest } from './tests/react-sync-scripts-conformance'; + +export interface IWebpackConformancePluginOptions { + tests: IWebpackConformanceTest[] +} + +interface VisitorMap { + [key: string]: (path: NodePath) => void +} + +export default class WebpackConformancePlugin { + private tests: IWebpackConformanceTest[] + private errors: Array + private warnings: Array + private compiler?: Compiler + + constructor(options: IWebpackConformancePluginOptions) { + this.tests = [] + if (options.tests) { + this.tests.push(...options.tests) + } + this.errors = [] + this.warnings = [] + } + + private gatherResults(results: Array): void { + results.forEach(result => { + if (result.result === IConformanceTestStatus.FAILED) { + result.errors && this.errors.push(...result.errors) + result.warnings && this.warnings.push(...result.warnings) + } + }) + } + + private buildStartedHandler = ( + compilation: compilation.Compilation, + callback: () => void + ) => { + const buildStartedResults: IConformanceTestResult[] = this.tests.map( + test => { + if (test.buildStared && this.compiler) { + return test.buildStared(this.compiler.options) + } + return { + result: IConformanceTestStatus.SUCCESS, + } as IConformanceTestResult + } + ) + + this.gatherResults(buildStartedResults) + callback() + } + + private buildCompletedHandler = ( + compilation: compilation.Compilation, + cb: () => void + ): void => { + const buildCompletedResults: IConformanceTestResult[] = this.tests.map( + test => { + if (test.buildCompleted) { + return test.buildCompleted(compilation.assets) + } + return { + result: IConformanceTestStatus.SUCCESS, + } as IConformanceTestResult + } + ) + + this.gatherResults(buildCompletedResults) + compilation.errors.push(...this.errors) + compilation.warnings.push(...this.warnings) + cb() + } + + private parserHandler = (factory: compilation.NormalModuleFactory): void => { + const JS_TYPES = ['auto', 'esm', 'dynamic'] + const collectedVisitors: Map = new Map() + // Collect all intereseted visitors from all tests. + this.tests.forEach(test => { + if (test.getAstNode) { + const getAstNodeCallbacks: IGetAstNodeResult[] = test.getAstNode() + getAstNodeCallbacks.forEach(result => { + if (!collectedVisitors.has(result.visitor)) { + collectedVisitors.set(result.visitor, []) + } + // @ts-ignore + collectedVisitors.get(result.visitor).push(result.inspectNode) + }) + } + }) + + // Do an extra walk per module and add interested visitors to the walk. + for (const type of JS_TYPES) { + factory.hooks.parser + .for('javascript/' + type) + .tap(this.constructor.name, parser => { + parser.hooks.program.tap(this.constructor.name, (ast: any) => { + const visitors: VisitorMap = {} + const that = this + for (const visitorKey of collectedVisitors.keys()) { + visitors[visitorKey] = function(path: NodePath) { + const callbacks = collectedVisitors.get(visitorKey) || [] + callbacks.forEach(cb => { + if (!cb) { + return + } + const { request } = parser.state.module + const outcome = cb(path, { request }) + that.gatherResults([outcome]) + }) + this.traverse(path) + return false + } + } + visit(ast, visitors) + }) + }) + } + } + + public apply(compiler: Compiler) { + this.compiler = compiler + compiler.hooks.make.tapAsync( + this.constructor.name, + this.buildStartedHandler + ) + compiler.hooks.emit.tapAsync( + this.constructor.name, + this.buildCompletedHandler + ) + compiler.hooks.normalModuleFactory.tap( + this.constructor.name, + this.parserHandler + ) + } +} diff --git a/packages/next/package.json b/packages/next/package.json index 82de83768bd4657..8da37d778c11020 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -122,6 +122,7 @@ "raw-body": "2.4.0", "react-error-overlay": "5.1.6", "react-is": "16.8.6", + "recast": "0.18.5", "resolve-url-loader": "3.1.1", "sass-loader": "8.0.2", "send": "0.17.1", @@ -185,6 +186,7 @@ "@types/webpack-sources": "0.1.5", "@zeit/ncc": "0.18.5", "arg": "4.1.0", + "ast-types": "0.13.2", "babel-plugin-dynamic-import-node": "2.3.0", "nanoid": "2.0.3", "resolve": "1.11.0", diff --git a/yarn.lock b/yarn.lock index 907b2f185d5e2d6..e1b0f6dd1036547 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3395,6 +3395,11 @@ assign-symbols@^1.0.0: resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= +ast-types@0.13.2: + version "0.13.2" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.2.tgz#df39b677a911a83f3a049644fb74fdded23cea48" + integrity sha512-uWMHxJxtfj/1oZClOxDEV1sQ1HCDkA4MG8Gr69KKeBjEVH0R84WlejZ0y2DcwyBlpAEMltmVYkVgqfLFb2oyiA== + astral-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" @@ -6072,7 +6077,7 @@ esprima@^3.1.3: resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM= -esprima@^4.0.0: +esprima@^4.0.0, esprima@~4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== @@ -12230,7 +12235,7 @@ pretty-hrtime@^1.0.3: resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE= -private@^0.1.6: +private@^0.1.6, private@^0.1.8: version "0.1.8" resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg== @@ -12753,6 +12758,16 @@ realpath-native@^1.1.0: dependencies: util.promisify "^1.0.0" +recast@0.18.5: + version "0.18.5" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.18.5.tgz#9d5adbc07983a3c8145f3034812374a493e0fe4d" + integrity sha512-sD1WJrpLQAkXGyQZyGzTM75WJvyAd98II5CHdK3IYbt/cZlU0UzCRVU11nUFNXX9fBVEt4E9ajkMjBlUlG+Oog== + dependencies: + ast-types "0.13.2" + esprima "~4.0.0" + private "^0.1.8" + source-map "~0.6.1" + redent@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde"