Skip to content

Commit

Permalink
adding Conformance Plugin behind a flag
Browse files Browse the repository at this point in the history
  • Loading branch information
prateekbh committed Jan 14, 2020
1 parent 84264f8 commit e082532
Show file tree
Hide file tree
Showing 7 changed files with 244 additions and 2 deletions.
8 changes: 8 additions & 0 deletions packages/next/build/webpack-config.ts
Expand Up @@ -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 = <T>(x: T | false) => x is T

Expand Down Expand Up @@ -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),
}

Expand Down
@@ -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<IConformanceAnamoly>
errors?: Array<IConformanceAnamoly>
}

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
}
@@ -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',
}
}
}
}
@@ -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]'
)
@@ -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<IConformanceAnamoly>
private warnings: Array<IConformanceAnamoly>
private compiler: Compiler

constructor(options: IWebpackConformancePluginOptions) {
this.tests = []
if (options.tests) {
this.tests.push(...options.tests)
}
this.errors = []
this.warnings = []
}

private gatherResults(results: Array<IConformanceTestResult>): 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<IConformanceTestResult>) => {
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<string, [NodeInspector?]> = 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
)
}
}
2 changes: 2 additions & 0 deletions packages/next/package.json
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
19 changes: 17 additions & 2 deletions yarn.lock
Expand Up @@ -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"
Expand Down Expand Up @@ -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==
Expand Down Expand Up @@ -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==
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit e082532

Please sign in to comment.