Remove outdated webpack conformance experiment (#28846)

This commit is contained in:
Tim Neutkens 2021-09-07 13:27:23 +02:00 committed by GitHub
parent 49a2fa2b07
commit cab846481d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 0 additions and 738 deletions

View file

@ -44,12 +44,6 @@ import PagesManifestPlugin from './webpack/plugins/pages-manifest-plugin'
import { ProfilingPlugin } from './webpack/plugins/profiling-plugin'
import { ReactLoadablePlugin } from './webpack/plugins/react-loadable-plugin'
import { ServerlessPlugin } from './webpack/plugins/serverless-plugin'
import WebpackConformancePlugin, {
DuplicatePolyfillsConformanceCheck,
GranularChunksConformanceCheck,
MinificationConformanceCheck,
ReactSyncScriptsConformanceCheck,
} from './webpack/plugins/webpack-conformance-plugin'
import { WellKnownErrorsPlugin } from './webpack/plugins/wellknown-errors-plugin'
import { regexLikeCss } from './webpack/config/blocks/css'
import { CopyFilePlugin } from './webpack/plugins/copy-file-plugin'
@ -668,30 +662,6 @@ export default async function getBaseWebpackConfig(
const crossOrigin = config.crossOrigin
const conformanceConfig = Object.assign(
{
ReactSyncScriptsConformanceCheck: {
enabled: true,
},
MinificationConformanceCheck: {
enabled: true,
},
DuplicatePolyfillsConformanceCheck: {
enabled: true,
BlockedAPIToBePolyfilled: Object.assign(
[],
['fetch'],
config.conformance?.DuplicatePolyfillsConformanceCheck
?.BlockedAPIToBePolyfilled || []
),
},
GranularChunksConformanceCheck: {
enabled: true,
},
},
config.conformance
)
const esmExternals = !!config.experimental?.esmExternals
const looseEsmExternals = config.experimental?.esmExternals === 'loose'
@ -1335,34 +1305,6 @@ export default async function getBaseWebpackConfig(
isLikeServerless,
})
})(),
config.experimental.conformance &&
!isWebpack5 &&
!dev &&
new WebpackConformancePlugin({
tests: [
!isServer &&
conformanceConfig.MinificationConformanceCheck.enabled &&
new MinificationConformanceCheck(),
conformanceConfig.ReactSyncScriptsConformanceCheck.enabled &&
new ReactSyncScriptsConformanceCheck({
AllowedSources:
conformanceConfig.ReactSyncScriptsConformanceCheck
.allowedSources || [],
}),
!isServer &&
conformanceConfig.DuplicatePolyfillsConformanceCheck.enabled &&
new DuplicatePolyfillsConformanceCheck({
BlockedAPIToBePolyfilled:
conformanceConfig.DuplicatePolyfillsConformanceCheck
.BlockedAPIToBePolyfilled,
}),
!isServer &&
conformanceConfig.GranularChunksConformanceCheck.enabled &&
new GranularChunksConformanceCheck(
splitChunksConfigs.prodGranular
),
].filter(Boolean),
}),
new WellKnownErrorsPlugin(),
!isServer &&
new CopyFilePlugin({

View file

@ -1,39 +0,0 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { NodePath } from 'ast-types/lib/node-path'
export interface IConformanceAnomaly {
message: string
stack_trace?: string
}
// eslint typescript has a bug with TS enums
/* eslint-disable no-shadow */
export enum IConformanceTestStatus {
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
}

View file

@ -1,213 +0,0 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { namedTypes } from 'ast-types'
// eslint-disable-next-line import/no-extraneous-dependencies
import { NodePath } from 'ast-types/lib/node-path'
import { types } from 'next/dist/compiled/recast'
import {
CONFORMANCE_ERROR_PREFIX,
CONFORMANCE_WARNING_PREFIX,
} from '../constants'
import {
IConformanceTestResult,
IConformanceTestStatus,
IGetAstNodeResult,
IParsedModuleDetails,
IWebpackConformanceTest,
} from '../TestInterface'
import {
isNodeCreatingScriptElement,
reducePropsToObject,
} from '../utils/ast-utils'
import { getLocalFileName } from '../utils/file-utils'
function getMessage(
property: string,
request: string,
isWarning: Boolean = false
): string {
if (isWarning) {
return `${CONFORMANCE_WARNING_PREFIX}: Found a ${property} polyfill in ${getLocalFileName(
request
)}.`
}
return `${CONFORMANCE_ERROR_PREFIX}: Found a ${property} polyfill in ${getLocalFileName(
request
)}.`
}
export interface DuplicatePolyfillsConformanceTestSettings {
BlockedAPIToBePolyfilled?: string[]
}
const BANNED_LEFT_OBJECT_TYPES = ['Identifier', 'ThisExpression']
export class DuplicatePolyfillsConformanceCheck
implements IWebpackConformanceTest
{
private BlockedAPIs: string[] = []
constructor(options: DuplicatePolyfillsConformanceTestSettings = {}) {
this.BlockedAPIs = options.BlockedAPIToBePolyfilled || []
}
public getAstNode(): IGetAstNodeResult[] {
const EARLY_EXIT_SUCCESS_RESULT: IConformanceTestResult = {
result: IConformanceTestStatus.SUCCESS,
}
return [
{
visitor: 'visitAssignmentExpression',
inspectNode: (
path: NodePath<namedTypes.AssignmentExpression>,
{ request }: IParsedModuleDetails
): IConformanceTestResult => {
const { node } = path
const left = node.left as namedTypes.MemberExpression
/**
* We're only interested in code like `foo.fetch = bar;`.
* For anything else we exit with a success.
* Also foo in foo.bar needs to be either Identifier or `this` and not someFunction().fetch;
*/
if (
left.type !== 'MemberExpression' ||
!BANNED_LEFT_OBJECT_TYPES.includes(left.object.type) ||
left.property.type !== 'Identifier'
) {
return EARLY_EXIT_SUCCESS_RESULT
}
if (!this.BlockedAPIs.includes(left.property.name)) {
return EARLY_EXIT_SUCCESS_RESULT
}
/**
* Here we know the code is `foo.(fetch/URL) = something.
* If foo === this/self, fail it immediately.
* check for this.[fetch|URL(...BlockedAPIs)]/ self.[fetch|URL(...BlockedAPIs)]
**/
if (isNodeThisOrSelf(left.object)) {
return {
result: IConformanceTestStatus.FAILED,
warnings: [
{
message: getMessage(left.property.name, request),
},
],
}
}
/**
* we now are sure the code under examination is
* `globalVar.[fetch|URL(...BlockedAPIs)] = something`
**/
const objectName = (left.object as namedTypes.Identifier).name
const allBindings = path.scope.lookup(objectName)
if (!allBindings) {
/**
* we have absolutely no idea where globalVar came from,
* so lets just exit
**/
return EARLY_EXIT_SUCCESS_RESULT
}
try {
const sourcePath = allBindings.bindings[objectName][0]
const originPath = sourcePath.parentPath
const {
node: originNode,
}: { node: namedTypes.VariableDeclarator } = originPath
if (
originNode.type === 'VariableDeclarator' &&
isNodeThisOrSelf(originNode.init)
) {
return {
result: IConformanceTestStatus.FAILED,
warnings: [
{
message: getMessage(left.property.name, request),
},
],
}
}
if (
originPath.name === 'params' &&
originPath.parentPath.firstInStatement()
) {
/**
* We do not know what will be the value of this param at runtime so we just throw a warning.
* ```
* (function(scope){
* ....
* scope.fetch = new Fetch();
* })(.....)
* ```
*/
return {
result: IConformanceTestStatus.FAILED,
warnings: [
{
message: getMessage(left.property.name, request, true),
},
],
}
}
} catch (e) {
return EARLY_EXIT_SUCCESS_RESULT
}
return EARLY_EXIT_SUCCESS_RESULT
},
},
{
visitor: 'visitCallExpression',
inspectNode: (path: NodePath) => {
const { node }: { node: types.namedTypes.CallExpression } = path
if (!node.arguments || node.arguments.length < 2) {
return EARLY_EXIT_SUCCESS_RESULT
}
if (isNodeCreatingScriptElement(node)) {
const propsNode = node
.arguments[1] as types.namedTypes.ObjectExpression
if (!propsNode.properties) {
return EARLY_EXIT_SUCCESS_RESULT
}
const props: {
[key: string]: string
} = reducePropsToObject(propsNode)
if (!('src' in props)) {
return EARLY_EXIT_SUCCESS_RESULT
}
const foundBannedPolyfill = doesScriptLoadBannedAPIfromPolyfillIO(
props.src,
this.BlockedAPIs
)
if (foundBannedPolyfill) {
return {
result: IConformanceTestStatus.FAILED,
warnings: [
{
message: `${CONFORMANCE_WARNING_PREFIX}: Found polyfill.io loading polyfill for ${foundBannedPolyfill}.`,
},
],
}
}
}
return EARLY_EXIT_SUCCESS_RESULT
},
},
]
}
}
function isNodeThisOrSelf(node: any): boolean {
return (
node.type === 'ThisExpression' ||
(node.type === 'Identifier' && node.name === 'self')
)
}
function doesScriptLoadBannedAPIfromPolyfillIO(
source: string,
blockedAPIs: string[]
): string | undefined {
const url = new URL(source)
if (url.hostname === 'polyfill.io' && url.searchParams.has('features')) {
const requestedAPIs = (url.searchParams.get('features') || '').split(',')
return blockedAPIs.find((api) => requestedAPIs.includes(api))
}
}

View file

@ -1,102 +0,0 @@
/* @eslint-disable no-redeclare */
import chalk from 'chalk'
import {
CONFORMANCE_ERROR_PREFIX,
CONFORMANCE_WARNING_PREFIX,
} from '../constants'
import {
IConformanceTestResult,
IConformanceTestStatus,
IWebpackConformanceTest,
} from '../TestInterface'
import { deepEqual } from '../utils/utils'
function getWarningMessage(modifiedProp: string) {
return (
`${CONFORMANCE_WARNING_PREFIX}: The splitChunks config has been carefully ` +
`crafted to optimize build size and build times. Modifying - ${chalk.bold(
modifiedProp
)} could result in slower builds and increased code duplication`
)
}
function getErrorMessage(message: string) {
return (
`${CONFORMANCE_ERROR_PREFIX}: The splitChunks config has been carefully ` +
`crafted to optimize build size and build times. Please avoid changes to ${chalk.bold(
message
)}`
)
}
/* @eslint-disable-next-line no-redeclare */
export class GranularChunksConformanceCheck implements IWebpackConformanceTest {
granularChunksConfig: any
constructor(granularChunksConfig: any) {
this.granularChunksConfig = granularChunksConfig
}
public buildStared(options: any): IConformanceTestResult {
const userSplitChunks = options.optimization.splitChunks
const warnings = []
const errors = []
if (
userSplitChunks.maxInitialRequests !==
this.granularChunksConfig.maxInitialRequests
) {
warnings.push('splitChunks.maxInitialRequests')
}
if (userSplitChunks.minSize !== this.granularChunksConfig.minSize) {
warnings.push('splitChunks.minSize')
}
const userCacheGroup = userSplitChunks.cacheGroups
const originalCacheGroup = this.granularChunksConfig.cacheGroups
if (userCacheGroup.vendors !== false) {
errors.push('splitChunks.cacheGroups.vendors')
}
if (!deepEqual(userCacheGroup.framework, originalCacheGroup.framework)) {
errors.push('splitChunks.cacheGroups.framework')
}
if (!deepEqual(userCacheGroup.lib, originalCacheGroup.lib)) {
errors.push('splitChunks.cacheGroups.lib')
}
if (!deepEqual(userCacheGroup.commons, originalCacheGroup.commons)) {
errors.push('splitChunks.cacheGroups.commons')
}
if (!deepEqual(userCacheGroup.shared, originalCacheGroup.shared)) {
errors.push('splitChunks.cacheGroups.shared')
}
if (!warnings.length && !errors.length) {
return {
result: IConformanceTestStatus.SUCCESS,
}
}
const failedResult: IConformanceTestResult = {
result: IConformanceTestStatus.FAILED,
}
if (warnings.length) {
failedResult.warnings = warnings.map((warning) => ({
message: getWarningMessage(warning),
}))
}
if (errors.length) {
failedResult.warnings = errors.map((error) => ({
message: getErrorMessage(error),
}))
}
return failedResult
}
}

View file

@ -1,35 +0,0 @@
import {
IWebpackConformanceTest,
IConformanceTestResult,
IConformanceTestStatus,
} from '../TestInterface'
import { CONFORMANCE_ERROR_PREFIX } from '../constants'
const EARLY_EXIT_RESULT: IConformanceTestResult = {
result: IConformanceTestStatus.SUCCESS,
}
export class MinificationConformanceCheck implements IWebpackConformanceTest {
public buildStared(options: any): IConformanceTestResult {
if (options.output.path.endsWith('/server')) {
return EARLY_EXIT_RESULT
}
// TODO(prateekbh@): Implement warning for using Terser maybe?
const { optimization } = options
if (
optimization &&
(optimization.minimize !== true ||
(optimization.minimizer && optimization.minimizer.length === 0))
) {
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 EARLY_EXIT_RESULT
}
}
}

View file

@ -1,87 +0,0 @@
import {
IWebpackConformanceTest,
IGetAstNodeResult,
IParsedModuleDetails,
IConformanceTestResult,
IConformanceTestStatus,
} from '../TestInterface'
import {
CONFORMANCE_ERROR_PREFIX,
CONFORMANCE_WARNING_PREFIX,
} from '../constants'
// eslint-disable-next-line import/no-extraneous-dependencies
import { namedTypes } from 'ast-types/'
// eslint-disable-next-line import/no-extraneous-dependencies
import { NodePath } from 'ast-types/lib/node-path'
import { getLocalFileName } from '../utils/file-utils'
import { isNodeCreatingScriptElement } from '../utils/ast-utils'
export const ErrorMessage: string = `${CONFORMANCE_ERROR_PREFIX}: A sync script was found in a react module.`
export const WarningMessage: string = `${CONFORMANCE_WARNING_PREFIX}: A sync script was found in a react module.`
export const ErrorDescription = ``
const EARLY_EXIT_SUCCESS_RESULT: IConformanceTestResult = {
result: IConformanceTestStatus.SUCCESS,
}
export interface ReactSyncScriptsConformanceCheckOptions {
AllowedSources?: String[]
}
export class ReactSyncScriptsConformanceCheck
implements IWebpackConformanceTest
{
private allowedSources: String[] = []
constructor({
AllowedSources,
}: ReactSyncScriptsConformanceCheckOptions = {}) {
if (AllowedSources) {
this.allowedSources = AllowedSources
}
}
public getAstNode(): IGetAstNodeResult[] {
return [
{
visitor: 'visitCallExpression',
inspectNode: (path: NodePath, { request }: IParsedModuleDetails) => {
const { node }: { node: namedTypes.CallExpression } = path
if (!node.arguments || node.arguments.length < 2) {
return EARLY_EXIT_SUCCESS_RESULT
}
if (isNodeCreatingScriptElement(node)) {
const propsNode = node.arguments[1] as namedTypes.ObjectExpression
if (!propsNode.properties) {
return EARLY_EXIT_SUCCESS_RESULT
}
const props: {
[key: string]: string
} = propsNode.properties.reduce((originalProps, prop: any) => {
// @ts-ignore
originalProps[prop.key.name] = prop.value.value
return originalProps
}, {})
if (
'defer' in props ||
'async' in props ||
!('src' in props) ||
this.allowedSources.includes(props.src)
) {
return EARLY_EXIT_SUCCESS_RESULT
}
// Todo: Add an absolute error case for modern js when class is a subclass of next/head.
return {
result: IConformanceTestStatus.FAILED,
warnings: [
{
message: `${WarningMessage} ${getLocalFileName(
request
)}. This can potentially delay FCP/FP metrics.`,
},
],
}
}
return EARLY_EXIT_SUCCESS_RESULT
},
},
]
}
}

View file

@ -1,8 +0,0 @@
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]'
)

View file

@ -1,155 +0,0 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { NodePath } from 'ast-types/lib/node-path'
import { visit } from 'next/dist/compiled/recast'
import { webpack } from 'next/dist/compiled/webpack/webpack'
import {
IConformanceAnomaly,
IConformanceTestResult,
IConformanceTestStatus,
IGetAstNodeResult,
IWebpackConformanceTest,
NodeInspector,
} from './TestInterface'
export { DuplicatePolyfillsConformanceCheck } from './checks/duplicate-polyfills-conformance-check'
export { GranularChunksConformanceCheck } from './checks/granular-chunks-conformance'
export { MinificationConformanceCheck } from './checks/minification-conformance-check'
export { ReactSyncScriptsConformanceCheck } from './checks/react-sync-scripts-conformance-check'
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?: webpack.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 === IConformanceTestStatus.FAILED) {
result.errors && this.errors.push(...result.errors)
result.warnings && this.warnings.push(...result.warnings)
}
})
}
private buildStartedHandler = (
_compilation: webpack.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: webpack.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: webpack.compilation.NormalModuleFactory
): void => {
const JS_TYPES = ['auto', 'esm', 'dynamic']
const collectedVisitors: Map<string, [NodeInspector?]> = new Map()
// Collect all interested 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, [])
}
;(collectedVisitors.get(result.visitor) as NodeInspector[]).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: any) => {
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: webpack.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
)
}
}

View file

@ -1,26 +0,0 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { namedTypes } from 'ast-types'
import { types } from 'next/dist/compiled/recast'
export function isNodeCreatingScriptElement(node: namedTypes.CallExpression) {
const callee = node.callee as namedTypes.Identifier
if (callee.type !== 'Identifier') {
return false
}
const componentNode = node.arguments[0] as namedTypes.Literal
if (componentNode.type !== 'Literal') {
return false
}
// Next has pragma: __jsx.
return callee.name === '__jsx' && componentNode.value === 'script'
}
export function reducePropsToObject(
propsNode: types.namedTypes.ObjectExpression
) {
return propsNode.properties.reduce((originalProps, prop: any) => {
// @ts-ignore
originalProps[prop.key.name] = prop.value.value
return originalProps
}, {})
}

View file

@ -1,5 +0,0 @@
const cwd = process.cwd()
export function getLocalFileName(request: string) {
return request.substr(request.lastIndexOf(cwd) + cwd.length)
}

View file

@ -1,10 +0,0 @@
const assert = require('assert').strict
export function deepEqual(a: any, b: any) {
try {
assert.deepStrictEqual(a, b)
return true
} catch (_) {
return false
}
}