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 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 === IConformanceTestStatus.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 = 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 => { 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 ) } }