Remove outdated webpack conformance experiment (#28846)
This commit is contained in:
parent
49a2fa2b07
commit
cab846481d
11 changed files with 0 additions and 738 deletions
|
@ -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({
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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]'
|
||||
)
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}, {})
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
const cwd = process.cwd()
|
||||
|
||||
export function getLocalFileName(request: string) {
|
||||
return request.substr(request.lastIndexOf(cwd) + cwd.length)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue