diff --git a/compiler/packages/snap/package.json b/compiler/packages/snap/package.json index 085422ab8303..92a170b365d5 100644 --- a/compiler/packages/snap/package.json +++ b/compiler/packages/snap/package.json @@ -22,6 +22,7 @@ }, "dependencies": { "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.19.1", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/preset-flow": "^7.7.4", "@babel/preset-typescript": "^7.26.0", @@ -58,6 +59,7 @@ "resolutions": { "./**/@babel/parser": "7.7.4", "./**/@babel/types": "7.7.4", + "@babel/generator": "7.2.0", "@babel/preset-flow": "7.22.5" } } diff --git a/compiler/packages/snap/src/minimize.ts b/compiler/packages/snap/src/minimize.ts new file mode 100644 index 000000000000..30848241f7e0 --- /dev/null +++ b/compiler/packages/snap/src/minimize.ts @@ -0,0 +1,2070 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import type {PluginObj} from '@babel/core'; +import {transformFromAstSync} from '@babel/core'; +import generate from '@babel/generator'; +import traverse from '@babel/traverse'; +import * as t from '@babel/types'; +import type {parseConfigPragmaForTests as ParseConfigPragma} from 'babel-plugin-react-compiler/src/Utils/TestUtils'; +import fs from 'fs'; +import path from 'path'; +import {parseInput, parseLanguage, parseSourceType} from './compiler.js'; +import {PARSE_CONFIG_PRAGMA_IMPORT, PROJECT_SRC} from './constants.js'; + +type MinimizeOptions = { + path: string; +}; + +type CompileSuccess = {kind: 'success'}; +type CompileParseError = {kind: 'parse_error'; message: string}; +type CompileErrors = { + kind: 'errors'; + errors: Array<{category: string; reason: string}>; +}; +type CompileResult = CompileSuccess | CompileParseError | CompileErrors; + +/** + * Compile code and extract error information + */ +function compileAndGetError( + code: string, + filename: string, + language: 'flow' | 'typescript', + sourceType: 'module' | 'script', + plugin: PluginObj, + parseConfigPragmaFn: typeof ParseConfigPragma, +): CompileResult { + let ast: t.File; + try { + ast = parseInput(code, filename, language, sourceType); + } catch (e: unknown) { + return {kind: 'parse_error', message: (e as Error).message}; + } + + const firstLine = code.substring(0, code.indexOf('\n')); + const config = parseConfigPragmaFn(firstLine, {compilationMode: 'all'}); + const options = { + ...config, + environment: { + ...config.environment, + }, + logger: { + logEvent: () => {}, + debugLogIRs: () => {}, + }, + enableReanimatedCheck: false, + }; + + try { + transformFromAstSync(ast, code, { + filename: '/' + filename, + highlightCode: false, + retainLines: true, + compact: true, + plugins: [[plugin, options]], + sourceType: 'module', + ast: false, + cloneInputAst: true, + configFile: false, + babelrc: false, + }); + return {kind: 'success'}; + } catch (e: unknown) { + const error = e as Error & { + details?: Array<{category: string; reason: string}>; + }; + // Check if this is a CompilerError with details + if (error.details && error.details.length > 0) { + return { + kind: 'errors', + errors: error.details.map(detail => ({ + category: detail.category, + reason: detail.reason, + })), + }; + } + // Fallback for other errors - use error name/message + return { + kind: 'errors', + errors: [ + { + category: error.name ?? 'Error', + reason: error.message, + }, + ], + }; + } +} + +/** + * Check if two compile errors match + */ +function errorsMatch(a: CompileErrors, b: CompileResult): boolean { + if (b.kind !== 'errors') { + return false; + } + if (a.errors.length !== b.errors.length) { + return false; + } + for (let i = 0; i < a.errors.length; i++) { + if ( + a.errors[i].category !== b.errors[i].category || + a.errors[i].reason !== b.errors[i].reason + ) { + return false; + } + } + return true; +} + +/** + * Convert AST to code string + */ +function astToCode(ast: t.File): string { + return generate(ast).code; +} + +/** + * Clone an AST node deeply + */ +function cloneAst(ast: t.File): t.File { + return t.cloneNode(ast, true); +} + +/** + * Generator that yields ASTs with statements removed one at a time + */ +function* removeStatements(ast: t.File): Generator { + // Collect all statement locations: which container (by index) and which statement index + const statementLocations: Array<{containerIndex: number; stmtIndex: number}> = + []; + let containerIndex = 0; + + t.traverseFast(ast, node => { + if (t.isBlockStatement(node) || t.isProgram(node)) { + const body = node.body as t.Statement[]; + // Iterate in reverse order so removing later statements first + for (let i = body.length - 1; i >= 0; i--) { + statementLocations.push({containerIndex, stmtIndex: i}); + } + containerIndex++; + } + }); + + for (const { + containerIndex: targetContainerIdx, + stmtIndex, + } of statementLocations) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + t.traverseFast(cloned, node => { + if (modified) return; + if (t.isBlockStatement(node) || t.isProgram(node)) { + if (idx === targetContainerIdx) { + const body = node.body as t.Statement[]; + if (stmtIndex < body.length) { + body.splice(stmtIndex, 1); + modified = true; + } + } + idx++; + } + }); + + if (modified) { + yield cloned; + } + } +} + +/** + * Generator that yields ASTs with call arguments removed one at a time + */ +function* removeCallArguments(ast: t.File): Generator { + // Collect all call expressions with their argument counts + const callSites: Array<{callIndex: number; argCount: number}> = []; + let callIndex = 0; + t.traverseFast(ast, node => { + if (t.isCallExpression(node) && node.arguments.length > 0) { + callSites.push({callIndex, argCount: node.arguments.length}); + callIndex++; + } + }); + + // For each call site, try removing each argument one at a time (from end to start) + for (const {callIndex: targetCallIdx, argCount} of callSites) { + for (let argIdx = argCount - 1; argIdx >= 0; argIdx--) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + t.traverseFast(cloned, node => { + if (modified) return; + if (t.isCallExpression(node) && node.arguments.length > 0) { + if (idx === targetCallIdx && argIdx < node.arguments.length) { + node.arguments.splice(argIdx, 1); + modified = true; + } + idx++; + } + }); + + if (modified) { + yield cloned; + } + } + } +} + +/** + * Generator that simplifies call expressions by replacing them with their arguments. + * For single argument: foo(x) -> x + * For multiple arguments: foo(x, y) -> [x, y] + */ +function* simplifyCallExpressions(ast: t.File): Generator { + // Count call expressions with arguments + let callCount = 0; + t.traverseFast(ast, node => { + if (t.isCallExpression(node) && node.arguments.length > 0) { + callCount++; + } + }); + + // For each call, try replacing with arguments + for (let targetIdx = 0; targetIdx < callCount; targetIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + CallExpression(path) { + if (modified) return; + if (path.node.arguments.length > 0 && idx === targetIdx) { + const args = path.node.arguments; + // Filter to only Expression arguments (not SpreadElement) + const exprArgs = args.filter((arg): arg is t.Expression => + t.isExpression(arg), + ); + if (exprArgs.length === 0) { + idx++; + return; + } + if (exprArgs.length === 1) { + // Single argument: replace call with the argument + path.replaceWith(exprArgs[0]); + } else { + // Multiple arguments: replace call with array of arguments + path.replaceWith(t.arrayExpression(exprArgs)); + } + modified = true; + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } + + // Also try replacing with each individual argument for multi-arg calls + for (let targetIdx = 0; targetIdx < callCount; targetIdx++) { + // First, find the arg count for this call + let argCount = 0; + let currentIdx = 0; + t.traverseFast(ast, node => { + if (t.isCallExpression(node) && node.arguments.length > 0) { + if (currentIdx === targetIdx) { + argCount = node.arguments.length; + } + currentIdx++; + } + }); + + // Try replacing with each argument individually + for (let argIdx = 0; argIdx < argCount; argIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + CallExpression(path) { + if (modified) return; + if (path.node.arguments.length > 0 && idx === targetIdx) { + const arg = path.node.arguments[argIdx]; + if (t.isExpression(arg)) { + path.replaceWith(arg); + modified = true; + } + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } + } +} + +/** + * Generator that simplifies conditional expressions (a ? b : c) -> a, b, or c + */ +function* simplifyConditionals(ast: t.File): Generator { + // Count conditionals + let condCount = 0; + t.traverseFast(ast, node => { + if (t.isConditionalExpression(node)) { + condCount++; + } + }); + + // Try replacing with test condition + for (let targetIdx = 0; targetIdx < condCount; targetIdx++) { + const cloned = cloneAst(ast); + let modified = false; + let idx = 0; + + traverse(cloned, { + ConditionalExpression(path) { + if (modified) return; + if (idx === targetIdx) { + path.replaceWith(path.node.test); + modified = true; + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } + + // Try replacing with consequent + for (let targetIdx = 0; targetIdx < condCount; targetIdx++) { + const cloned = cloneAst(ast); + let modified = false; + let idx = 0; + + traverse(cloned, { + ConditionalExpression(path) { + if (modified) return; + if (idx === targetIdx) { + path.replaceWith(path.node.consequent); + modified = true; + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } + + // Also try replacing with alternate + for (let targetIdx = 0; targetIdx < condCount; targetIdx++) { + const cloned = cloneAst(ast); + let modified = false; + let idx = 0; + + traverse(cloned, { + ConditionalExpression(path) { + if (modified) return; + if (idx === targetIdx) { + path.replaceWith(path.node.alternate); + modified = true; + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } +} + +/** + * Generator that simplifies logical expressions (a && b) -> a or b + */ +function* simplifyLogicalExpressions(ast: t.File): Generator { + // Count logical expressions + let logicalCount = 0; + t.traverseFast(ast, node => { + if (t.isLogicalExpression(node)) { + logicalCount++; + } + }); + + // Try replacing with left side + for (let targetIdx = 0; targetIdx < logicalCount; targetIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + LogicalExpression(path) { + if (modified) return; + if (idx === targetIdx) { + path.replaceWith(path.node.left); + modified = true; + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } + + // Try replacing with right side + for (let targetIdx = 0; targetIdx < logicalCount; targetIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + LogicalExpression(path) { + if (modified) return; + if (idx === targetIdx) { + path.replaceWith(path.node.right); + modified = true; + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } +} + +/** + * Generator that simplifies optional chains (a?.b) -> a.b + */ +function* simplifyOptionalChains(ast: t.File): Generator { + // Count optional expressions + let optionalCount = 0; + t.traverseFast(ast, node => { + if ( + t.isOptionalMemberExpression(node) || + t.isOptionalCallExpression(node) + ) { + optionalCount++; + } + }); + + for (let targetIdx = 0; targetIdx < optionalCount; targetIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + OptionalMemberExpression(path) { + if (modified) return; + if (idx === targetIdx) { + const {object, property, computed} = path.node; + path.replaceWith(t.memberExpression(object, property, computed)); + modified = true; + } + idx++; + }, + OptionalCallExpression(path) { + if (modified) return; + if (idx === targetIdx) { + const {callee, arguments: args} = path.node; + if (t.isExpression(callee)) { + path.replaceWith(t.callExpression(callee, args)); + modified = true; + } + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } +} + +/** + * Generator that simplifies await expressions: await expr -> expr + */ +function* simplifyAwaitExpressions(ast: t.File): Generator { + // Count await expressions + let awaitCount = 0; + t.traverseFast(ast, node => { + if (t.isAwaitExpression(node)) { + awaitCount++; + } + }); + + for (let targetIdx = 0; targetIdx < awaitCount; targetIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + AwaitExpression(path) { + if (modified) return; + if (idx === targetIdx) { + path.replaceWith(path.node.argument); + modified = true; + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } +} + +/** + * Generator that simplifies if statements: + * - Replace with test expression (as expression statement) + * - Replace with consequent block + * - Replace with alternate block (if present) + */ +function* simplifyIfStatements(ast: t.File): Generator { + // Count if statements + let ifCount = 0; + t.traverseFast(ast, node => { + if (t.isIfStatement(node)) { + ifCount++; + } + }); + + // Try replacing with test expression + for (let targetIdx = 0; targetIdx < ifCount; targetIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + IfStatement(path) { + if (modified) return; + if (idx === targetIdx) { + path.replaceWith(t.expressionStatement(path.node.test)); + modified = true; + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } + + // Try replacing with consequent + for (let targetIdx = 0; targetIdx < ifCount; targetIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + IfStatement(path) { + if (modified) return; + if (idx === targetIdx) { + path.replaceWith(path.node.consequent); + modified = true; + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } + + // Try replacing with alternate (if present) + for (let targetIdx = 0; targetIdx < ifCount; targetIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + IfStatement(path) { + if (modified) return; + if (idx === targetIdx && path.node.alternate) { + path.replaceWith(path.node.alternate); + modified = true; + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } +} + +/** + * Generator that simplifies switch statements: + * - Replace with discriminant expression + * - Replace with each case's consequent statements + */ +function* simplifySwitchStatements(ast: t.File): Generator { + // Count switch statements + let switchCount = 0; + t.traverseFast(ast, node => { + if (t.isSwitchStatement(node)) { + switchCount++; + } + }); + + // Try replacing with discriminant + for (let targetIdx = 0; targetIdx < switchCount; targetIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + SwitchStatement(path) { + if (modified) return; + if (idx === targetIdx) { + path.replaceWith(t.expressionStatement(path.node.discriminant)); + modified = true; + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } + + // For each switch, try replacing with each case's body + for (let targetIdx = 0; targetIdx < switchCount; targetIdx++) { + // Find case count for this switch + let caseCount = 0; + let currentIdx = 0; + t.traverseFast(ast, node => { + if (t.isSwitchStatement(node)) { + if (currentIdx === targetIdx) { + caseCount = node.cases.length; + } + currentIdx++; + } + }); + + for (let caseIdx = 0; caseIdx < caseCount; caseIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + SwitchStatement(path) { + if (modified) return; + if (idx === targetIdx) { + const switchCase = path.node.cases[caseIdx]; + if (switchCase && switchCase.consequent.length > 0) { + path.replaceWithMultiple(switchCase.consequent); + modified = true; + } + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } + } +} + +/** + * Generator that simplifies while statements: + * - Replace with test expression + * - Replace with body + */ +function* simplifyWhileStatements(ast: t.File): Generator { + // Count while statements + let whileCount = 0; + t.traverseFast(ast, node => { + if (t.isWhileStatement(node)) { + whileCount++; + } + }); + + // Try replacing with test + for (let targetIdx = 0; targetIdx < whileCount; targetIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + WhileStatement(path) { + if (modified) return; + if (idx === targetIdx) { + path.replaceWith(t.expressionStatement(path.node.test)); + modified = true; + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } + + // Try replacing with body + for (let targetIdx = 0; targetIdx < whileCount; targetIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + WhileStatement(path) { + if (modified) return; + if (idx === targetIdx) { + path.replaceWith(path.node.body); + modified = true; + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } +} + +/** + * Generator that simplifies do-while statements: + * - Replace with test expression + * - Replace with body + */ +function* simplifyDoWhileStatements(ast: t.File): Generator { + // Count do-while statements + let doWhileCount = 0; + t.traverseFast(ast, node => { + if (t.isDoWhileStatement(node)) { + doWhileCount++; + } + }); + + // Try replacing with test + for (let targetIdx = 0; targetIdx < doWhileCount; targetIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + DoWhileStatement(path) { + if (modified) return; + if (idx === targetIdx) { + path.replaceWith(t.expressionStatement(path.node.test)); + modified = true; + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } + + // Try replacing with body + for (let targetIdx = 0; targetIdx < doWhileCount; targetIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + DoWhileStatement(path) { + if (modified) return; + if (idx === targetIdx) { + path.replaceWith(path.node.body); + modified = true; + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } +} + +/** + * Generator that simplifies for statements: + * - Replace with init (if expression) + * - Replace with test expression + * - Replace with update expression + * - Replace with body + */ +function* simplifyForStatements(ast: t.File): Generator { + // Count for statements + let forCount = 0; + t.traverseFast(ast, node => { + if (t.isForStatement(node)) { + forCount++; + } + }); + + // Try replacing with init (if it's an expression) + for (let targetIdx = 0; targetIdx < forCount; targetIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + ForStatement(path) { + if (modified) return; + if (idx === targetIdx && path.node.init) { + if (t.isExpression(path.node.init)) { + path.replaceWith(t.expressionStatement(path.node.init)); + } else { + // It's a VariableDeclaration + path.replaceWith(path.node.init); + } + modified = true; + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } + + // Try replacing with test + for (let targetIdx = 0; targetIdx < forCount; targetIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + ForStatement(path) { + if (modified) return; + if (idx === targetIdx && path.node.test) { + path.replaceWith(t.expressionStatement(path.node.test)); + modified = true; + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } + + // Try replacing with update + for (let targetIdx = 0; targetIdx < forCount; targetIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + ForStatement(path) { + if (modified) return; + if (idx === targetIdx && path.node.update) { + path.replaceWith(t.expressionStatement(path.node.update)); + modified = true; + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } + + // Try replacing with body + for (let targetIdx = 0; targetIdx < forCount; targetIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + ForStatement(path) { + if (modified) return; + if (idx === targetIdx) { + path.replaceWith(path.node.body); + modified = true; + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } +} + +/** + * Generator that simplifies for-in statements: + * - Replace with left (variable declaration or expression) + * - Replace with right expression + * - Replace with body + */ +function* simplifyForInStatements(ast: t.File): Generator { + // Count for-in statements + let forInCount = 0; + t.traverseFast(ast, node => { + if (t.isForInStatement(node)) { + forInCount++; + } + }); + + // Try replacing with left + for (let targetIdx = 0; targetIdx < forInCount; targetIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + ForInStatement(path) { + if (modified) return; + if (idx === targetIdx) { + const left = path.node.left; + if (t.isExpression(left)) { + path.replaceWith(t.expressionStatement(left)); + } else { + path.replaceWith(left); + } + modified = true; + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } + + // Try replacing with right + for (let targetIdx = 0; targetIdx < forInCount; targetIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + ForInStatement(path) { + if (modified) return; + if (idx === targetIdx) { + path.replaceWith(t.expressionStatement(path.node.right)); + modified = true; + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } + + // Try replacing with body + for (let targetIdx = 0; targetIdx < forInCount; targetIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + ForInStatement(path) { + if (modified) return; + if (idx === targetIdx) { + path.replaceWith(path.node.body); + modified = true; + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } +} + +/** + * Generator that simplifies for-of statements: + * - Replace with left (variable declaration or expression) + * - Replace with right expression + * - Replace with body + */ +function* simplifyForOfStatements(ast: t.File): Generator { + // Count for-of statements + let forOfCount = 0; + t.traverseFast(ast, node => { + if (t.isForOfStatement(node)) { + forOfCount++; + } + }); + + // Try replacing with left + for (let targetIdx = 0; targetIdx < forOfCount; targetIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + ForOfStatement(path) { + if (modified) return; + if (idx === targetIdx) { + const left = path.node.left; + if (t.isExpression(left)) { + path.replaceWith(t.expressionStatement(left)); + } else { + path.replaceWith(left); + } + modified = true; + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } + + // Try replacing with right + for (let targetIdx = 0; targetIdx < forOfCount; targetIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + ForOfStatement(path) { + if (modified) return; + if (idx === targetIdx) { + path.replaceWith(t.expressionStatement(path.node.right)); + modified = true; + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } + + // Try replacing with body + for (let targetIdx = 0; targetIdx < forOfCount; targetIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + ForOfStatement(path) { + if (modified) return; + if (idx === targetIdx) { + path.replaceWith(path.node.body); + modified = true; + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } +} + +/** + * Generator that simplifies variable declarations by removing init expressions. + * let x = expr; -> let x; + * var x = expr; -> var x; + * Note: const without init is invalid, so we skip const declarations. + */ +function* simplifyVariableDeclarations(ast: t.File): Generator { + // Collect all variable declarators with init expressions (excluding const) + const declaratorSites: Array<{declIndex: number}> = []; + let declIndex = 0; + t.traverseFast(ast, node => { + if (t.isVariableDeclaration(node) && node.kind !== 'const') { + for (const declarator of node.declarations) { + if (declarator.init) { + declaratorSites.push({declIndex}); + declIndex++; + } + } + } + }); + + // Try removing init from each declarator + for (const {declIndex: targetDeclIdx} of declaratorSites) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + t.traverseFast(cloned, node => { + if (modified) return; + if (t.isVariableDeclaration(node) && node.kind !== 'const') { + for (const declarator of node.declarations) { + if (declarator.init) { + if (idx === targetDeclIdx) { + declarator.init = null; + modified = true; + return; + } + idx++; + } + } + } + }); + + if (modified) { + yield cloned; + } + } +} + +/** + * Generator that simplifies try/catch/finally statements: + * - Replace with try block contents + * - Replace with catch block contents (if present) + * - Replace with finally block contents (if present) + */ +function* simplifyTryStatements(ast: t.File): Generator { + // Count try statements + let tryCount = 0; + t.traverseFast(ast, node => { + if (t.isTryStatement(node)) { + tryCount++; + } + }); + + // Try replacing with try block contents + for (let targetIdx = 0; targetIdx < tryCount; targetIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + TryStatement(path) { + if (modified) return; + if (idx === targetIdx) { + path.replaceWith(path.node.block); + modified = true; + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } + + // Try replacing with catch block contents (if present) + for (let targetIdx = 0; targetIdx < tryCount; targetIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + TryStatement(path) { + if (modified) return; + if (idx === targetIdx && path.node.handler) { + path.replaceWith(path.node.handler.body); + modified = true; + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } + + // Try replacing with finally block contents (if present) + for (let targetIdx = 0; targetIdx < tryCount; targetIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + TryStatement(path) { + if (modified) return; + if (idx === targetIdx && path.node.finalizer) { + path.replaceWith(path.node.finalizer); + modified = true; + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } +} + +/** + * Generator that simplifies single-statement block statements: + * { statement } -> statement + */ +function* simplifySingleStatementBlocks(ast: t.File): Generator { + // Count block statements with exactly one statement + let blockCount = 0; + t.traverseFast(ast, node => { + if (t.isBlockStatement(node) && node.body.length === 1) { + blockCount++; + } + }); + + for (let targetIdx = 0; targetIdx < blockCount; targetIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + BlockStatement(path) { + if (modified) return; + if (path.node.body.length === 1 && idx === targetIdx) { + // Don't unwrap blocks that require BlockStatement syntax + if ( + t.isFunction(path.parent) || + t.isCatchClause(path.parent) || + t.isClassMethod(path.parent) || + t.isObjectMethod(path.parent) || + t.isTryStatement(path.parent) + ) { + idx++; + return; + } + path.replaceWith(path.node.body[0]); + modified = true; + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } +} + +/** + * Generator that removes array elements one at a time + */ +function* removeArrayElements(ast: t.File): Generator { + // Collect all array expressions with their element counts + const arraySites: Array<{arrayIndex: number; elementCount: number}> = []; + let arrayIndex = 0; + t.traverseFast(ast, node => { + if (t.isArrayExpression(node) && node.elements.length > 0) { + arraySites.push({arrayIndex, elementCount: node.elements.length}); + arrayIndex++; + } + }); + + // For each array, try removing each element one at a time (from end to start) + for (const {arrayIndex: targetArrayIdx, elementCount} of arraySites) { + for (let elemIdx = elementCount - 1; elemIdx >= 0; elemIdx--) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + t.traverseFast(cloned, node => { + if (modified) return; + if (t.isArrayExpression(node) && node.elements.length > 0) { + if (idx === targetArrayIdx && elemIdx < node.elements.length) { + node.elements.splice(elemIdx, 1); + modified = true; + } + idx++; + } + }); + + if (modified) { + yield cloned; + } + } + } +} + +/** + * Generator that removes JSX element attributes (props) one at a time + */ +function* removeJSXAttributes(ast: t.File): Generator { + // Collect all JSX elements with their attribute counts + const jsxSites: Array<{jsxIndex: number; attrCount: number}> = []; + let jsxIndex = 0; + t.traverseFast(ast, node => { + if (t.isJSXOpeningElement(node) && node.attributes.length > 0) { + jsxSites.push({jsxIndex, attrCount: node.attributes.length}); + jsxIndex++; + } + }); + + // For each JSX element, try removing each attribute one at a time (from end to start) + for (const {jsxIndex: targetJsxIdx, attrCount} of jsxSites) { + for (let attrIdx = attrCount - 1; attrIdx >= 0; attrIdx--) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + t.traverseFast(cloned, node => { + if (modified) return; + if (t.isJSXOpeningElement(node) && node.attributes.length > 0) { + if (idx === targetJsxIdx && attrIdx < node.attributes.length) { + node.attributes.splice(attrIdx, 1); + modified = true; + } + idx++; + } + }); + + if (modified) { + yield cloned; + } + } + } +} + +/** + * Generator that removes JSX element children one at a time + */ +function* removeJSXChildren(ast: t.File): Generator { + // Collect all JSX elements with children + const jsxSites: Array<{jsxIndex: number; childCount: number}> = []; + let jsxIndex = 0; + t.traverseFast(ast, node => { + if (t.isJSXElement(node) && node.children.length > 0) { + jsxSites.push({jsxIndex, childCount: node.children.length}); + jsxIndex++; + } + }); + + // For each JSX element, try removing each child one at a time (from end to start) + for (const {jsxIndex: targetJsxIdx, childCount} of jsxSites) { + for (let childIdx = childCount - 1; childIdx >= 0; childIdx--) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + t.traverseFast(cloned, node => { + if (modified) return; + if (t.isJSXElement(node) && node.children.length > 0) { + if (idx === targetJsxIdx && childIdx < node.children.length) { + node.children.splice(childIdx, 1); + modified = true; + } + idx++; + } + }); + + if (modified) { + yield cloned; + } + } + } +} + +/** + * Generator that removes JSX fragment children one at a time + */ +function* removeJSXFragmentChildren(ast: t.File): Generator { + // Collect all JSX fragments with children + const fragmentSites: Array<{fragIndex: number; childCount: number}> = []; + let fragIndex = 0; + t.traverseFast(ast, node => { + if (t.isJSXFragment(node) && node.children.length > 0) { + fragmentSites.push({fragIndex, childCount: node.children.length}); + fragIndex++; + } + }); + + // For each fragment, try removing each child one at a time (from end to start) + for (const {fragIndex: targetFragIdx, childCount} of fragmentSites) { + for (let childIdx = childCount - 1; childIdx >= 0; childIdx--) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + t.traverseFast(cloned, node => { + if (modified) return; + if (t.isJSXFragment(node) && node.children.length > 0) { + if (idx === targetFragIdx && childIdx < node.children.length) { + node.children.splice(childIdx, 1); + modified = true; + } + idx++; + } + }); + + if (modified) { + yield cloned; + } + } + } +} + +/** + * Generator that replaces single-element arrays with the element itself + */ +function* simplifySingleElementArrays(ast: t.File): Generator { + // Count single-element arrays + let arrayCount = 0; + t.traverseFast(ast, node => { + if (t.isArrayExpression(node) && node.elements.length === 1) { + arrayCount++; + } + }); + + for (let targetIdx = 0; targetIdx < arrayCount; targetIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + ArrayExpression(path) { + if (modified) return; + if (path.node.elements.length === 1 && idx === targetIdx) { + const elem = path.node.elements[0]; + if (t.isExpression(elem)) { + path.replaceWith(elem); + modified = true; + } + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } +} + +/** + * Generator that replaces single-property objects with the property value. + * For regular properties: {key: value} -> value + * For computed properties: {[key]: value} -> key (also try value) + */ +function* simplifySinglePropertyObjects(ast: t.File): Generator { + // Count single-property objects + let objectCount = 0; + t.traverseFast(ast, node => { + if (t.isObjectExpression(node) && node.properties.length === 1) { + objectCount++; + } + }); + + // Try replacing with value + for (let targetIdx = 0; targetIdx < objectCount; targetIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + ObjectExpression(path) { + if (modified) return; + if (path.node.properties.length === 1 && idx === targetIdx) { + const prop = path.node.properties[0]; + if (t.isObjectProperty(prop) && t.isExpression(prop.value)) { + path.replaceWith(prop.value); + modified = true; + } + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } + + // For computed properties, also try replacing with key + for (let targetIdx = 0; targetIdx < objectCount; targetIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + ObjectExpression(path) { + if (modified) return; + if (path.node.properties.length === 1 && idx === targetIdx) { + const prop = path.node.properties[0]; + if ( + t.isObjectProperty(prop) && + prop.computed && + t.isExpression(prop.key) + ) { + path.replaceWith(prop.key); + modified = true; + } + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } +} + +/** + * Generator that removes object properties one at a time + */ +function* removeObjectProperties(ast: t.File): Generator { + // Collect all object expressions with their property counts + const objectSites: Array<{objectIndex: number; propCount: number}> = []; + let objectIndex = 0; + t.traverseFast(ast, node => { + if (t.isObjectExpression(node) && node.properties.length > 0) { + objectSites.push({objectIndex, propCount: node.properties.length}); + objectIndex++; + } + }); + + // For each object, try removing each property one at a time (from end to start) + for (const {objectIndex: targetObjIdx, propCount} of objectSites) { + for (let propIdx = propCount - 1; propIdx >= 0; propIdx--) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + t.traverseFast(cloned, node => { + if (modified) return; + if (t.isObjectExpression(node) && node.properties.length > 0) { + if (idx === targetObjIdx && propIdx < node.properties.length) { + node.properties.splice(propIdx, 1); + modified = true; + } + idx++; + } + }); + + if (modified) { + yield cloned; + } + } + } +} + +/** + * Generator that simplifies assignment expressions (a = b) -> a or b + */ +function* simplifyAssignmentExpressions(ast: t.File): Generator { + // Count assignment expressions + let assignmentCount = 0; + t.traverseFast(ast, node => { + if (t.isAssignmentExpression(node)) { + assignmentCount++; + } + }); + + // Try replacing with left side (assignment target) + for (let targetIdx = 0; targetIdx < assignmentCount; targetIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + AssignmentExpression(path) { + if (modified) return; + if (idx === targetIdx) { + const left = path.node.left; + if (t.isExpression(left)) { + path.replaceWith(left); + modified = true; + } + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } + + // Try replacing with right side (assignment value) + for (let targetIdx = 0; targetIdx < assignmentCount; targetIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + AssignmentExpression(path) { + if (modified) return; + if (idx === targetIdx) { + path.replaceWith(path.node.right); + modified = true; + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } +} + +/** + * Generator that simplifies binary expressions (a + b) -> a or b + */ +function* simplifyBinaryExpressions(ast: t.File): Generator { + // Count binary expressions + let binaryCount = 0; + t.traverseFast(ast, node => { + if (t.isBinaryExpression(node)) { + binaryCount++; + } + }); + + // Try replacing with left side + for (let targetIdx = 0; targetIdx < binaryCount; targetIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + BinaryExpression(path) { + if (modified) return; + if (idx === targetIdx) { + path.replaceWith(path.node.left); + modified = true; + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } + + // Try replacing with right side + for (let targetIdx = 0; targetIdx < binaryCount; targetIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + BinaryExpression(path) { + if (modified) return; + if (idx === targetIdx) { + path.replaceWith(path.node.right); + modified = true; + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } +} + +/** + * Generator that simplifies member expressions (obj.value) -> obj + * For computed expressions: obj[key] -> obj or key + */ +function* simplifyMemberExpressions(ast: t.File): Generator { + // Count member expressions + let memberCount = 0; + t.traverseFast(ast, node => { + if (t.isMemberExpression(node)) { + memberCount++; + } + }); + + // Try replacing with object + for (let targetIdx = 0; targetIdx < memberCount; targetIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + MemberExpression(path) { + if (modified) return; + if (idx === targetIdx) { + path.replaceWith(path.node.object); + modified = true; + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } + + // For computed expressions, also try replacing with key + for (let targetIdx = 0; targetIdx < memberCount; targetIdx++) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + traverse(cloned, { + MemberExpression(path) { + if (modified) return; + if (idx === targetIdx && path.node.computed) { + const property = path.node.property; + if (t.isExpression(property)) { + path.replaceWith(property); + modified = true; + } + } + idx++; + }, + }); + + if (modified) { + yield cloned; + } + } +} + +/** + * Helper to collect all unique identifier names in the AST + */ +function collectUniqueIdentifierNames(ast: t.File): Set { + const names = new Set(); + t.traverseFast(ast, node => { + if (t.isIdentifier(node)) { + names.add(node.name); + } + }); + return names; +} + +/** + * Helper to rename all occurrences of an identifier throughout the AST + */ +function renameAllIdentifiers( + ast: t.File, + oldName: string, + newName: string, +): boolean { + let modified = false; + t.traverseFast(ast, node => { + if (t.isIdentifier(node) && node.name === oldName) { + node.name = newName; + modified = true; + } + }); + return modified; +} + +/** + * Generator that simplifies identifiers by removing "on" prefix. + * onClick -> Click + */ +function* simplifyIdentifiersRemoveOnPrefix(ast: t.File): Generator { + const names = collectUniqueIdentifierNames(ast); + + for (const name of names) { + // Check if name starts with "on" followed by uppercase letter + if ( + name.length > 2 && + name.startsWith('on') && + name[2] === name[2].toUpperCase() + ) { + const newName = name.slice(2); + // Skip if the new name would conflict with an existing identifier + if (names.has(newName)) { + continue; + } + const cloned = cloneAst(ast); + if (renameAllIdentifiers(cloned, name, newName)) { + yield cloned; + } + } + } +} + +/** + * Generator that simplifies identifiers by removing "Ref" suffix. + * inputRef -> input + */ +function* simplifyIdentifiersRemoveRefSuffix(ast: t.File): Generator { + const names = collectUniqueIdentifierNames(ast); + + for (const name of names) { + // Check if name ends with "Ref" and has more characters before it + if (name.length > 3 && name.endsWith('Ref')) { + const newName = name.slice(0, -3); + // Skip if the new name would conflict with an existing identifier + if (names.has(newName)) { + continue; + } + // Skip if new name would be empty or just whitespace + if (newName.length === 0) { + continue; + } + const cloned = cloneAst(ast); + if (renameAllIdentifiers(cloned, name, newName)) { + yield cloned; + } + } + } +} + +/** + * Generator that rewrites "ref" identifier to "ref_" to avoid conflicts. + */ +function* simplifyIdentifiersRenameRef(ast: t.File): Generator { + const names = collectUniqueIdentifierNames(ast); + + if (names.has('ref')) { + // Only rename if ref_ doesn't already exist + if (!names.has('ref_')) { + const cloned = cloneAst(ast); + if (renameAllIdentifiers(cloned, 'ref', 'ref_')) { + yield cloned; + } + } + } +} + +/** + * All simplification strategies in order of priority (coarse to fine) + */ +const simplificationStrategies = [ + {name: 'removeStatements', generator: removeStatements}, + {name: 'removeCallArguments', generator: removeCallArguments}, + {name: 'removeArrayElements', generator: removeArrayElements}, + {name: 'removeObjectProperties', generator: removeObjectProperties}, + {name: 'removeJSXAttributes', generator: removeJSXAttributes}, + {name: 'removeJSXChildren', generator: removeJSXChildren}, + {name: 'removeJSXFragmentChildren', generator: removeJSXFragmentChildren}, + {name: 'simplifyCallExpressions', generator: simplifyCallExpressions}, + {name: 'simplifyConditionals', generator: simplifyConditionals}, + {name: 'simplifyLogicalExpressions', generator: simplifyLogicalExpressions}, + {name: 'simplifyBinaryExpressions', generator: simplifyBinaryExpressions}, + { + name: 'simplifyAssignmentExpressions', + generator: simplifyAssignmentExpressions, + }, + {name: 'simplifySingleElementArrays', generator: simplifySingleElementArrays}, + { + name: 'simplifySinglePropertyObjects', + generator: simplifySinglePropertyObjects, + }, + {name: 'simplifyMemberExpressions', generator: simplifyMemberExpressions}, + {name: 'simplifyOptionalChains', generator: simplifyOptionalChains}, + {name: 'simplifyAwaitExpressions', generator: simplifyAwaitExpressions}, + {name: 'simplifyIfStatements', generator: simplifyIfStatements}, + {name: 'simplifySwitchStatements', generator: simplifySwitchStatements}, + {name: 'simplifyWhileStatements', generator: simplifyWhileStatements}, + {name: 'simplifyDoWhileStatements', generator: simplifyDoWhileStatements}, + {name: 'simplifyForStatements', generator: simplifyForStatements}, + {name: 'simplifyForInStatements', generator: simplifyForInStatements}, + {name: 'simplifyForOfStatements', generator: simplifyForOfStatements}, + { + name: 'simplifyVariableDeclarations', + generator: simplifyVariableDeclarations, + }, + {name: 'simplifyTryStatements', generator: simplifyTryStatements}, + { + name: 'simplifySingleStatementBlocks', + generator: simplifySingleStatementBlocks, + }, + { + name: 'simplifyIdentifiersRemoveOnPrefix', + generator: simplifyIdentifiersRemoveOnPrefix, + }, + { + name: 'simplifyIdentifiersRemoveRefSuffix', + generator: simplifyIdentifiersRemoveRefSuffix, + }, + { + name: 'simplifyIdentifiersRenameRef', + generator: simplifyIdentifiersRenameRef, + }, +]; + +type MinimizeResult = + | {kind: 'success'} + | {kind: 'minimal'} + | {kind: 'minimized'; source: string}; + +/** + * Core minimization loop that attempts to reduce the input source code + * while preserving the compiler error. + */ +export function minimize( + input: string, + filename: string, + language: 'flow' | 'typescript', + sourceType: 'module' | 'script', +): MinimizeResult { + // Load the compiler plugin + const importedCompilerPlugin = require(PROJECT_SRC) as Record< + string, + unknown + >; + const BabelPluginReactCompiler = importedCompilerPlugin[ + 'default' + ] as PluginObj; + const parseConfigPragmaForTests = importedCompilerPlugin[ + PARSE_CONFIG_PRAGMA_IMPORT + ] as typeof ParseConfigPragma; + + // Get the initial error + const initialResult = compileAndGetError( + input, + filename, + language, + sourceType, + BabelPluginReactCompiler, + parseConfigPragmaForTests, + ); + + if (initialResult.kind === 'success') { + return {kind: 'success'}; + } + + if (initialResult.kind === 'parse_error') { + return {kind: 'success'}; + } + + const targetError = initialResult; + + // Parse the initial AST + let currentAst = parseInput(input, filename, language, sourceType); + let currentCode = input; + let changed = true; + let iterations = 0; + const maxIterations = 1000; // Safety limit + + process.stdout.write('\nMinimizing'); + + while (changed && iterations < maxIterations) { + changed = false; + iterations++; + + // Try each simplification strategy + for (const strategy of simplificationStrategies) { + const generator = strategy.generator(currentAst); + + for (const candidateAst of generator) { + let candidateCode: string; + try { + candidateCode = astToCode(candidateAst); + } catch { + // If code generation fails, skip this candidate + continue; + } + + const result = compileAndGetError( + candidateCode, + filename, + language, + sourceType, + BabelPluginReactCompiler, + parseConfigPragmaForTests, + ); + + if (errorsMatch(targetError, result)) { + // This simplification preserves the error, keep it + currentAst = candidateAst; + currentCode = candidateCode; + changed = true; + process.stdout.write('.'); + break; // Restart from the beginning with the new AST + } + } + + if (changed) { + break; // Restart the outer loop + } + } + } + + console.log('\n'); + + // Check if any minimization was achieved + if (currentCode === input) { + return {kind: 'minimal'}; + } + + return {kind: 'minimized', source: currentCode}; +} + +/** + * Main minimize function that reads the input file, runs minimization, + * and reports results. + */ +export async function runMinimize(options: MinimizeOptions): Promise { + // Resolve the input path + const inputPath = path.isAbsolute(options.path) + ? options.path + : path.resolve(process.cwd(), options.path); + + // Check if file exists + if (!fs.existsSync(inputPath)) { + console.error(`Error: File not found: ${inputPath}`); + process.exit(1); + } + + // Read the input file + const input = fs.readFileSync(inputPath, 'utf-8'); + const filename = path.basename(inputPath); + const firstLine = input.substring(0, input.indexOf('\n')); + const language = parseLanguage(firstLine); + const sourceType = parseSourceType(firstLine); + + console.log(`Minimizing: ${inputPath}`); + + const originalLines = input.split('\n').length; + + // Run the minimization + const result = minimize(input, filename, language, sourceType); + + if (result.kind === 'success') { + console.log('Could not minimize: the input compiles successfully.'); + process.exit(0); + } + + if (result.kind === 'minimal') { + console.log( + 'Could not minimize: the input fails but is already minimal and cannot be reduced further.', + ); + process.exit(0); + } + + // Output the minimized code + console.log('--- Minimized Code ---'); + console.log(result.source); + + const minimizedLines = result.source.split('\n').length; + console.log( + `\nReduced from ${originalLines} lines to ${minimizedLines} lines`, + ); +} diff --git a/compiler/packages/snap/src/runner.ts b/compiler/packages/snap/src/runner.ts index 21320048eb21..f127bd5a35e5 100644 --- a/compiler/packages/snap/src/runner.ts +++ b/compiler/packages/snap/src/runner.ts @@ -23,6 +23,7 @@ import { } from './runner-watch'; import * as runnerWorker from './runner-worker'; import {execSync} from 'child_process'; +import {runMinimize} from './minimize'; const WORKER_PATH = require.resolve('./runner-worker.js'); const NUM_WORKERS = cpus().length - 1; @@ -38,40 +39,76 @@ type RunnerOptions = { debug: boolean; }; -const opts: RunnerOptions = yargs - .boolean('sync') - .describe( - 'sync', - 'Run compiler in main thread (instead of using worker threads or subprocesses). Defaults to false.', - ) - .default('sync', false) - .boolean('worker-threads') - .describe( - 'worker-threads', - 'Run compiler in worker threads (instead of subprocesses). Defaults to true.', +async function runTestCommand(opts: RunnerOptions): Promise { + await main(opts); +} + +async function runMinimizeCommand(path: string): Promise { + await runMinimize({path}); +} + +yargs(hideBin(process.argv)) + .command( + ['test', '$0'], + 'Run compiler tests', + yargs => { + return yargs + .boolean('sync') + .describe( + 'sync', + 'Run compiler in main thread (instead of using worker threads or subprocesses). Defaults to false.', + ) + .default('sync', false) + .boolean('worker-threads') + .describe( + 'worker-threads', + 'Run compiler in worker threads (instead of subprocesses). Defaults to true.', + ) + .default('worker-threads', true) + .boolean('watch') + .describe( + 'watch', + 'Run compiler in watch mode, re-running after changes', + ) + .alias('w', 'watch') + .default('watch', false) + .boolean('update') + .alias('u', 'update') + .describe('update', 'Update fixtures') + .default('update', false) + .string('pattern') + .alias('p', 'pattern') + .describe( + 'pattern', + 'Optional glob pattern to filter fixtures (e.g., "error.*", "use-memo")', + ) + .boolean('debug') + .alias('d', 'debug') + .describe('debug', 'Enable debug logging to print HIR for each pass') + .default('debug', false); + }, + async argv => { + await runTestCommand(argv as RunnerOptions); + }, ) - .default('worker-threads', true) - .boolean('watch') - .describe('watch', 'Run compiler in watch mode, re-running after changes') - .alias('w', 'watch') - .default('watch', false) - .boolean('update') - .alias('u', 'update') - .describe('update', 'Update fixtures') - .default('update', false) - .string('pattern') - .alias('p', 'pattern') - .describe( - 'pattern', - 'Optional glob pattern to filter fixtures (e.g., "error.*", "use-memo")', + .command( + 'minimize', + 'Minimize a test case to reproduce a compiler error', + yargs => { + return yargs + .string('path') + .alias('p', 'path') + .describe('path', 'Path to the file to minimize') + .demandOption('path'); + }, + async argv => { + await runMinimizeCommand(argv.path as string); + }, ) - .boolean('debug') - .alias('d', 'debug') - .describe('debug', 'Enable debug logging to print HIR for each pass') - .default('debug', false) .help('help') .strict() - .parseSync(hideBin(process.argv)) as RunnerOptions; + .demandCommand() + .parse(); /** * Do a test run and return the test results @@ -82,6 +119,7 @@ async function runFixtures( compilerVersion: number, debug: boolean, requireSingleFixture: boolean, + sync: boolean, ): Promise { // We could in theory be fancy about tracking the contents of the fixtures // directory via our file subscription, but it's simpler to just re-read @@ -91,7 +129,7 @@ async function runFixtures( const shouldLog = debug && (!requireSingleFixture || isOnlyFixture); let entries: Array<[string, TestResult]>; - if (!opts.sync) { + if (!sync) { // Note: promise.all to ensure parallelism when enabled const work: Array> = []; for (const [fixtureName, fixture] of fixtures) { @@ -123,6 +161,7 @@ async function runFixtures( async function onChange( worker: Worker & typeof runnerWorker, state: RunnerState, + sync: boolean, ) { const {compilerVersion, isCompilerBuildValid, mode, filter, debug} = state; if (isCompilerBuildValid) { @@ -140,6 +179,7 @@ async function onChange( compilerVersion, debug, true, // requireSingleFixture in watch mode + sync, ); const end = performance.now(); @@ -192,7 +232,11 @@ export async function main(opts: RunnerOptions): Promise { const shouldWatch = opts.watch; if (shouldWatch) { - makeWatchRunner(state => onChange(worker, state), opts.debug, opts.pattern); + makeWatchRunner( + state => onChange(worker, state, opts.sync), + opts.debug, + opts.pattern, + ); if (opts.pattern) { /** * Warm up wormers when in watch mode. Loading the Forget babel plugin @@ -251,6 +295,7 @@ export async function main(opts: RunnerOptions): Promise { 0, opts.debug, false, // no requireSingleFixture in non-watch mode + opts.sync, ); if (opts.update) { update(results); @@ -269,5 +314,3 @@ export async function main(opts: RunnerOptions): Promise { ); } } - -main(opts).catch(error => console.error(error)); diff --git a/compiler/yarn.lock b/compiler/yarn.lock index 764200c2ce9d..f480ff498cdf 100644 --- a/compiler/yarn.lock +++ b/compiler/yarn.lock @@ -268,6 +268,17 @@ source-map "^0.5.0" trim-right "^1.0.1" +"@babel/generator@^7.19.1": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.6.tgz#48dcc65d98fcc8626a48f72b62e263d25fc3c3f1" + integrity sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw== + dependencies: + "@babel/parser" "^7.28.6" + "@babel/types" "^7.28.6" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" + jsesc "^3.0.2" + "@babel/generator@^7.2.0", "@babel/generator@^7.26.0", "@babel/generator@^7.26.10", "@babel/generator@^7.26.3", "@babel/generator@^7.27.0", "@babel/generator@^7.7.2", "@babel/generator@^7.7.4": version "7.27.0" resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz" @@ -662,6 +673,13 @@ resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz" integrity sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg== +"@babel/parser@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.6.tgz#f01a8885b7fa1e56dd8a155130226cd698ef13fd" + integrity sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ== + dependencies: + "@babel/types" "^7.28.6" + "@babel/parser@^7.7.4": version "7.21.4" resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz" @@ -1592,7 +1610,7 @@ debug "^4.3.1" globals "^11.1.0" -"@babel/types@7.26.3", "@babel/types@^7.0.0", "@babel/types@^7.19.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.20.2", "@babel/types@^7.20.7", "@babel/types@^7.21.2", "@babel/types@^7.24.7", "@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.26.10", "@babel/types@^7.26.3", "@babel/types@^7.27.0", "@babel/types@^7.27.1", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.4": +"@babel/types@7.26.3", "@babel/types@^7.0.0", "@babel/types@^7.19.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.20.2", "@babel/types@^7.20.7", "@babel/types@^7.21.2", "@babel/types@^7.24.7", "@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.26.10", "@babel/types@^7.26.3", "@babel/types@^7.27.0", "@babel/types@^7.27.1", "@babel/types@^7.28.6", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.4": version "7.26.3" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.3.tgz#37e79830f04c2b5687acc77db97fbc75fb81f3c0" integrity sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA== @@ -2732,6 +2750,14 @@ "@types/yargs" "^17.0.8" chalk "^4.0.0" +"@jridgewell/gen-mapping@^0.3.12": + version "0.3.13" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" + integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + "@jridgewell/trace-mapping" "^0.3.24" + "@jridgewell/gen-mapping@^0.3.2": version "0.3.8" resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz" @@ -2765,6 +2791,11 @@ resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== +"@jridgewell/sourcemap-codec@^1.5.0": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== + "@jridgewell/trace-mapping@0.3.9": version "0.3.9" resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz" @@ -2789,6 +2820,14 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@jridgewell/trace-mapping@^0.3.28": + version "0.3.31" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" + integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + "@modelcontextprotocol/sdk@^1.9.0": version "1.9.0" resolved "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.9.0.tgz"