Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding conformance webpack plugin #9716

Merged
merged 16 commits into from Jan 23, 2020
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -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),
}

Expand Down
@@ -0,0 +1,36 @@
import { NodePath } from 'ast-types/lib/node-path'

export interface IConformanceAnomaly {
message: string
Timer marked this conversation as resolved.
Show resolved Hide resolved
stack_trace?: string
}

export enum IConformanceTestStatus {
Timer marked this conversation as resolved.
Show resolved Hide resolved
SUCCESS,
FAILED,
}
export interface IConformanceTestResult {
result: IConformanceTestStatus
warnings?: Array<IConformanceAnomaly>
errors?: Array<IConformanceAnomaly>
}

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
}
@@ -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,
}
}
}
}
@@ -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]'
)
@@ -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<IConformanceAnomaly>
private warnings: Array<IConformanceAnomaly>
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 && 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<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 @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
19 changes: 17 additions & 2 deletions yarn.lock
Expand Up @@ -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"
Expand Down Expand Up @@ -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==
Expand Down Expand Up @@ -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==
Expand Down Expand Up @@ -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"
Expand Down