From e082532895dda96905131a7c21b01fb7e03546f6 Mon Sep 17 00:00:00 2001 From: Prateek Bhatnagar Date: Tue, 14 Jan 2020 10:52:28 -0800 Subject: [PATCH] adding Conformance Plugin behind a flag --- packages/next/build/webpack-config.ts | 8 + .../TestInterface.ts | 32 ++++ .../checks/minification-conformance-check.ts | 26 +++ .../webpack-conformance-plugin/constants.ts | 6 + .../webpack-conformance-plugin/index.ts | 153 ++++++++++++++++++ packages/next/package.json | 2 + yarn.lock | 19 ++- 7 files changed, 244 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 b78604b92abe68d..f03abafa271435c 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 @@ -818,6 +821,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..5dcd10a2d07686d --- /dev/null +++ b/packages/next/build/webpack/plugins/webpack-conformance-plugin/TestInterface.ts @@ -0,0 +1,32 @@ +import { NodePath } from 'ast-types/lib/node-path' + +export interface IConformanceAnamoly { + message: string + stack_trace?: string +} + +export interface IConformanceTestResult { + result: 'SUCCESS' | 'FAILED' + 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 IWebpackConformanctTest { + 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..32db419574b0bac --- /dev/null +++ b/packages/next/build/webpack/plugins/webpack-conformance-plugin/checks/minification-conformance-check.ts @@ -0,0 +1,26 @@ +import { + IWebpackConformanctTest, + IConformanceTestResult, +} from '../TestInterface' +import { CONFORMANCE_ERROR_PREFIX } from '../constants' + +export class MinificationConformanceCheck implements IWebpackConformanctTest { + public buildStared(options: any): IConformanceTestResult { + // TODO(prateekbh@): Implement warning for using Terser maybe? + + if (options.optimization.minimize === false) { + return { + result: 'FAILED', + errors: [ + { + message: `${CONFORMANCE_ERROR_PREFIX}: Minification is disabled for this build.\nDisabling minification can result in serious performance degradation.`, + }, + ], + } + } else { + return { + result: '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..d3a95eb097bf6d9 --- /dev/null +++ b/packages/next/build/webpack/plugins/webpack-conformance-plugin/constants.ts @@ -0,0 +1,6 @@ +import { red, yellow } from 'kleur' + +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..51e204260bf75bc --- /dev/null +++ b/packages/next/build/webpack/plugins/webpack-conformance-plugin/index.ts @@ -0,0 +1,153 @@ +import { Compiler, compilation } from 'webpack' +import { + IConformanceTestResult, + IWebpackConformanctTest, + IConformanceAnamoly, + IGetAstNodeResult, + NodeInspector, +} 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: IWebpackConformanctTest[] +} + +interface VisitorMap { + [key: string]: (path: NodePath) => void +} + +export default class WebpackConformancePlugin { + private tests: IWebpackConformanctTest[] + 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 === '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) { + return test.buildStared(this.compiler.options) + } + return { + result: 'SUCCESS', + } as IConformanceTestResult + } + ) + + Promise.all(buildStartedResults).then( + (results: Array) => { + this.gatherResults(results) + } + ) + + 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: '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 3eae6bb1921b5bb..44df09239d2e98a 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -123,6 +123,7 @@ "raw-body": "2.4.0", "react-error-overlay": "5.1.6", "react-is": "16.8.6", + "recast": "0.18.5", "send": "0.17.1", "source-map": "0.6.1", "string-hash": "1.1.3", @@ -184,6 +185,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 dd3fd793296950e..31ca06f0f895b98 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3371,6 +3371,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" @@ -5993,7 +5998,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== @@ -12130,7 +12135,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== @@ -12653,6 +12658,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"