[healthcheck] Refactor checks into separate files

Makes it easier to extend later, if we want to add more checks.

ghstack-source-id: 6fb3435555f1b988e1a185bfda8be9418eb622c5
Pull Request resolved: https://github.com/facebook/react-forget/pull/2924
This commit is contained in:
Sathya Gunsasekaran
2024-05-01 16:05:11 +01:00
parent c464445b91
commit 4ce5c56fee
4 changed files with 177 additions and 120 deletions
@@ -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.`));
}
},
};
@@ -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<LoggerEvent> = [];
const ActionableFailures: Array<LoggerEvent> = [];
const OtherFailures: Array<LoggerEvent> = [];
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<PluginOptions> = {
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.`
)
);
},
};
@@ -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 = /\<StrictMode\>/;
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."));
}
},
};
+11 -120
View File
@@ -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<LoggerEvent> = [];
const ACTIONABLE_FAILURES: Array<LoggerEvent> = [];
const OTHER_FAILURES: Array<LoggerEvent> = [];
let STRICT_MODE_USAGE = false;
const StrictModeRE = /\<StrictMode\>/;
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<PluginOptions> = {
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();