diff --git a/compiler/packages/healthcheck/src/checks/libraryCompat.ts b/compiler/packages/healthcheck/src/checks/libraryCompat.ts new file mode 100644 index 0000000000..b6a2866b86 --- /dev/null +++ b/compiler/packages/healthcheck/src/checks/libraryCompat.ts @@ -0,0 +1,37 @@ +/** + * 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 chalk from "chalk"; +import { config } from "../config"; + +const packageJsonRE = /package\.json$/; +const knownIncompatibleLibrariesUsage = new Set(); + +export default { + run(source: string, path: string): void { + if (packageJsonRE.exec(path) !== null) { + const contents = JSON.parse(source); + const deps = contents.dependencies; + for (const library of config.knownIncompatibleLibraries) { + if (Object.hasOwn(deps, library)) { + knownIncompatibleLibrariesUsage.add(library); + } + } + } + }, + + report(): void { + if (knownIncompatibleLibrariesUsage.size > 0) { + console.log(chalk.red(`Found the following incompatible libraries:`)); + for (const library of knownIncompatibleLibrariesUsage) { + console.log(library); + } + } else { + console.log(chalk.green(`Found no usage of incompatible libraries.`)); + } + }, +}; diff --git a/compiler/packages/healthcheck/src/checks/reactCompiler.ts b/compiler/packages/healthcheck/src/checks/reactCompiler.ts new file mode 100644 index 0000000000..11ee58a749 --- /dev/null +++ b/compiler/packages/healthcheck/src/checks/reactCompiler.ts @@ -0,0 +1,97 @@ +/** + * 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 { + ErrorSeverity, + runReactForgetBabelPlugin, + type CompilerErrorDetailOptions, + type PluginOptions, +} from "babel-plugin-react-forget/src"; +import { LoggerEvent } from "babel-plugin-react-forget/src/Entrypoint"; +import chalk from "chalk"; + +const SucessfulCompilation: Array = []; +const ActionableFailures: Array = []; +const OtherFailures: Array = []; + +const logger = { + logEvent(_: string | null, event: LoggerEvent) { + switch (event.kind) { + case "CompileSuccess": { + SucessfulCompilation.push(event); + return; + } + case "CompileError": { + if (isActionableDiagnostic(event.detail)) { + ActionableFailures.push(event); + return; + } + OtherFailures.push(event); + return; + } + case "CompileDiagnostic": + case "PipelineError": + OtherFailures.push(event); + return; + } + }, +}; + +const COMPILER_OPTIONS: Partial = { + noEmit: true, + compilationMode: "infer", + panicThreshold: "critical_errors", + logger, +}; + +function isActionableDiagnostic(detail: CompilerErrorDetailOptions) { + switch (detail.severity) { + case ErrorSeverity.InvalidReact: + case ErrorSeverity.InvalidJS: + return true; + case ErrorSeverity.InvalidConfig: + case ErrorSeverity.Invariant: + case ErrorSeverity.CannotPreserveMemoization: + case ErrorSeverity.Todo: + return false; + default: + throw new Error("Unhandled error severity"); + } +} + +function compile(sourceCode: string, filename: string) { + try { + runReactForgetBabelPlugin( + sourceCode, + filename, + "typescript", + COMPILER_OPTIONS + ); + } catch {} +} + +const JsFileExtensionRE = /(js|ts|jsx|tsx|mjs)$/; + +export default { + run(source: string, path: string): void { + if (JsFileExtensionRE.exec(path) !== null) { + compile(source, path); + } + }, + + report(): void { + const totalComponents = + SucessfulCompilation.length + + OtherFailures.length + + ActionableFailures.length; + console.log( + chalk.green( + `Successfully compiled ${SucessfulCompilation.length} out of ${totalComponents} components.` + ) + ); + }, +}; diff --git a/compiler/packages/healthcheck/src/checks/strictMode.ts b/compiler/packages/healthcheck/src/checks/strictMode.ts new file mode 100644 index 0000000000..8992eab5f6 --- /dev/null +++ b/compiler/packages/healthcheck/src/checks/strictMode.ts @@ -0,0 +1,32 @@ +/** + * 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 chalk from "chalk"; + +const JsFileExtensionRE = /(js|ts|jsx|tsx|mjs)$/; +const StrictModeRE = /\/; +let StrictModeUsage = false; + +export default { + run(source: string, path: string): void { + if (JsFileExtensionRE.exec(path) === null) { + return; + } + + if (!StrictModeUsage) { + StrictModeUsage = StrictModeRE.exec(source) !== null; + } + }, + + report(): void { + if (StrictModeUsage) { + console.log(chalk.green("StrictMode usage found.")); + } else { + console.log(chalk.red("StrictMode usage not found.")); + } + }, +}; diff --git a/compiler/packages/healthcheck/src/index.ts b/compiler/packages/healthcheck/src/index.ts index c5de001d33..7ccb6bf7fe 100644 --- a/compiler/packages/healthcheck/src/index.ts +++ b/compiler/packages/healthcheck/src/index.ts @@ -5,82 +5,13 @@ * LICENSE file in the root directory of this source tree. */ -import { - ErrorSeverity, - runReactForgetBabelPlugin, - type CompilerErrorDetailOptions, - type PluginOptions, -} from "babel-plugin-react-forget/src"; -import { LoggerEvent } from "babel-plugin-react-forget/src/Entrypoint"; -import chalk from "chalk"; import { glob } from "fast-glob"; import * as fs from "fs/promises"; import ora from "ora"; import yargs from "yargs/yargs"; -import { config } from "./config"; - -const SUCCESS: Array = []; -const ACTIONABLE_FAILURES: Array = []; -const OTHER_FAILURES: Array = []; -let STRICT_MODE_USAGE = false; - -const StrictModeRE = /\/; - -const logger = { - logEvent(_: string | null, event: LoggerEvent) { - switch (event.kind) { - case "CompileSuccess": { - SUCCESS.push(event); - return; - } - case "CompileError": { - if (isActionableDiagnostic(event.detail)) { - ACTIONABLE_FAILURES.push(event); - return; - } - OTHER_FAILURES.push(event); - return; - } - case "CompileDiagnostic": - case "PipelineError": - OTHER_FAILURES.push(event); - return; - } - }, -}; - -const COMPILER_OPTIONS: Partial = { - noEmit: true, - compilationMode: "infer", - panicThreshold: "critical_errors", - logger, -}; - -function isActionableDiagnostic(detail: CompilerErrorDetailOptions) { - switch (detail.severity) { - case ErrorSeverity.InvalidReact: - case ErrorSeverity.InvalidJS: - return true; - case ErrorSeverity.InvalidConfig: - case ErrorSeverity.Invariant: - case ErrorSeverity.CannotPreserveMemoization: - case ErrorSeverity.Todo: - return false; - default: - throw new Error("Unhandled error severity"); - } -} - -function compile(sourceCode: string, filename: string) { - try { - runReactForgetBabelPlugin( - sourceCode, - filename, - "typescript", - COMPILER_OPTIONS - ); - } catch {} -} +import libraryCompatCheck from "./checks/libraryCompat"; +import reactCompilerCheck from "./checks/reactCompiler"; +import strictModeCheck from "./checks/strictMode"; async function main() { const argv = yargs(process.argv.slice(2)) @@ -93,14 +24,9 @@ async function main() { }) .parseSync(); - const spinner = ora("Compiling").start(); + const spinner = ora("Checking").start(); let src = argv.src; - // no file extension specified - if (!src.includes(".")) { - src = src; - } - const globOptions = { onlyFiles: true, ignore: [ @@ -115,53 +41,18 @@ async function main() { ], }; - const jsFileExtensionRE = /(js|ts|jsx|tsx|mjs)$/; - const packageJsonRE = /package\.json$/; - const knownIncompatibleLibrariesUsage = new Set(); - for (const path of await glob(src, globOptions)) { const source = await fs.readFile(path, "utf-8"); - if (jsFileExtensionRE.exec(path) !== null) { - spinner.text = `Compiling ${path}`; - compile(source, path); - - if (!STRICT_MODE_USAGE) { - STRICT_MODE_USAGE = StrictModeRE.exec(source) !== null; - } - } else if (packageJsonRE.exec(path) !== null) { - const contents = JSON.parse(source); - const deps = contents.dependencies; - for (const library of config.knownIncompatibleLibraries) { - if (Object.hasOwn(deps, library)) { - knownIncompatibleLibrariesUsage.add(library); - } - } - } + spinner.text = `Checking ${path}`; + reactCompilerCheck.run(source, path); + strictModeCheck.run(source, path); + libraryCompatCheck.run(source, path); } spinner.stop(); - const totalComponents = - SUCCESS.length + OTHER_FAILURES.length + ACTIONABLE_FAILURES.length; - console.log( - chalk.green( - `Successfully compiled ${SUCCESS.length} out of ${totalComponents} components.` - ) - ); - - if (STRICT_MODE_USAGE) { - console.log(chalk.green("StrictMode usage found.")); - } else { - console.log(chalk.red("StrictMode usage not found.")); - } - - if (knownIncompatibleLibrariesUsage.size > 0) { - console.log(chalk.red(`Found the following incompatible libraries:`)); - for (const library of knownIncompatibleLibrariesUsage) { - console.log(library); - } - } else { - console.log(chalk.green(`Found no usage of incompatible libraries.`)); - } + reactCompilerCheck.report(); + strictModeCheck.report(); + libraryCompatCheck.report(); } main();