Skip to content

Commit dde6511

Browse files
committed
fmt
1 parent 9f51b9d commit dde6511

File tree

6 files changed

+73
-18
lines changed

6 files changed

+73
-18
lines changed

action.yml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ inputs:
2020
github-token:
2121
description: "GitHub token for posting PR comments. When set on pull_request events, findings are posted as a PR comment."
2222
required: false
23+
fail-on:
24+
description: "Exit with error code on diagnostics: error, warning, none"
25+
default: "error"
2326
node-version:
2427
description: "Node.js version to use"
2528
default: "20"
@@ -43,8 +46,9 @@ runs:
4346
INPUT_PROJECT: ${{ inputs.project }}
4447
INPUT_DIFF: ${{ inputs.diff }}
4548
INPUT_GITHUB_TOKEN: ${{ inputs.github-token }}
49+
INPUT_FAIL_ON: ${{ inputs.fail-on }}
4650
run: |
47-
FLAGS=""
51+
FLAGS="--fail-on $INPUT_FAIL_ON"
4852
if [ "$INPUT_VERBOSE" = "true" ]; then FLAGS="$FLAGS --verbose"; fi
4953
if [ -n "$INPUT_PROJECT" ]; then FLAGS="$FLAGS --project $INPUT_PROJECT"; fi
5054
if [ -n "$INPUT_DIFF" ]; then FLAGS="$FLAGS --diff $INPUT_DIFF"; fi
@@ -56,12 +60,15 @@ runs:
5660
fi
5761
5862
- id: score
63+
if: always()
5964
shell: bash
6065
env:
6166
INPUT_DIRECTORY: ${{ inputs.directory }}
6267
run: |
63-
SCORE=$(npx -y react-doctor@latest "$INPUT_DIRECTORY" --score 2>/dev/null || echo "")
64-
echo "score=$SCORE" >> "$GITHUB_OUTPUT"
68+
SCORE=$(npx -y react-doctor@latest "$INPUT_DIRECTORY" --score 2>/dev/null | tail -1 | tr -d '[:space:]')
69+
if [[ -n "$SCORE" && "$SCORE" =~ ^[0-9]+$ ]]; then
70+
echo "score=$SCORE" >> "$GITHUB_OUTPUT"
71+
fi
6572
6673
- if: ${{ inputs.github-token != '' && github.event_name == 'pull_request' }}
6774
uses: actions/github-script@v7

packages/react-doctor/src/cli.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {
99
Diagnostic,
1010
DiffInfo,
1111
EstimatedScoreResult,
12+
FailOnLevel,
1213
ReactDoctorConfig,
1314
ScanOptions,
1415
} from "./types.js";
@@ -37,8 +38,23 @@ interface CliFlags {
3738
ami: boolean;
3839
project?: string;
3940
diff?: boolean | string;
41+
failOn: string;
4042
}
4143

44+
const VALID_FAIL_ON_LEVELS = new Set<FailOnLevel>(["error", "warning", "none"]);
45+
46+
const isValidFailOnLevel = (level: string): level is FailOnLevel =>
47+
VALID_FAIL_ON_LEVELS.has(level as FailOnLevel);
48+
49+
const shouldFailForDiagnostics = (
50+
diagnostics: Diagnostic[],
51+
failOnLevel: FailOnLevel,
52+
): boolean => {
53+
if (failOnLevel === "none") return false;
54+
if (failOnLevel === "warning") return diagnostics.length > 0;
55+
return diagnostics.some((diagnostic) => diagnostic.severity === "error");
56+
};
57+
4258
const exitWithFixHint = () => {
4359
logger.break();
4460
logger.log("Cancelled.");
@@ -129,6 +145,7 @@ const program = new Command()
129145
.option("--diff [base]", "scan only files changed vs base branch")
130146
.option("--offline", "skip telemetry (anonymous, not stored, only used to calculate score)")
131147
.option("--no-ami", "skip Ami-related prompts")
148+
.option("--fail-on <level>", "exit with error code on diagnostics: error, warning, none", "none")
132149
.option("--fix", "open Ami to auto-fix all issues")
133150
.action(async (directory: string, flags: CliFlags) => {
134151
const isScoreOnly = flags.score;
@@ -203,6 +220,18 @@ const program = new Command()
203220
}
204221
}
205222

223+
const resolvedFailOn =
224+
program.getOptionValueSource("failOn") === "cli"
225+
? flags.failOn
226+
: (userConfig?.failOn ?? flags.failOn);
227+
const effectiveFailOn: FailOnLevel = isValidFailOnLevel(resolvedFailOn)
228+
? resolvedFailOn
229+
: "none";
230+
231+
if (shouldFailForDiagnostics(allDiagnostics, effectiveFailOn)) {
232+
process.exitCode = 1;
233+
}
234+
206235
if (flags.fix) {
207236
openAmiToFix(resolvedDirectory);
208237
}

packages/react-doctor/src/plugin/constants.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,12 @@ export const GOOGLE_FONTS_PATTERN = /fonts\.googleapis\.com/;
212212

213213
export const POLYFILL_SCRIPT_PATTERN = /polyfill\.io|polyfill\.min\.js|cdn\.polyfill/;
214214

215+
export const EXECUTABLE_SCRIPT_TYPES = new Set([
216+
"text/javascript",
217+
"application/javascript",
218+
"module",
219+
]);
220+
215221
export const APP_DIRECTORY_PATTERN = /\/app\//;
216222

217223
export const ROUTE_HANDLER_FILE_PATTERN = /\/route\.(tsx?|jsx?)$/;

packages/react-doctor/src/plugin/rules/nextjs.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
APP_DIRECTORY_PATTERN,
33
EFFECT_HOOK_NAMES,
4+
EXECUTABLE_SCRIPT_TYPES,
45
GOOGLE_FONTS_PATTERN,
56
INTERNAL_PAGE_PATH_PATTERN,
67
MUTATING_ROUTE_SEGMENTS,
@@ -267,6 +268,11 @@ export const nextjsNoNativeScript: Rule = {
267268
JSXOpeningElement(node: EsTreeNode) {
268269
if (node.name?.type !== "JSXIdentifier" || node.name.name !== "script") return;
269270

271+
const typeAttribute = findJsxAttribute(node.attributes ?? [], "type");
272+
const typeValue =
273+
typeAttribute?.value?.type === "Literal" ? typeAttribute.value.value : null;
274+
if (typeof typeValue === "string" && !EXECUTABLE_SCRIPT_TYPES.has(typeValue)) return;
275+
270276
context.report({
271277
node,
272278
message:

packages/react-doctor/src/scan.ts

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -496,19 +496,21 @@ export const scan = async (
496496
return lintDiagnostics;
497497
} catch (error) {
498498
didLintFail = true;
499-
const errorMessage = error instanceof Error ? error.message : String(error);
500-
const isNativeBindingError = errorMessage.includes("native binding");
501-
502-
if (isNativeBindingError) {
503-
lintSpinner?.fail(
504-
`Lint checks failed — oxlint native binding not found (Node ${process.version}).`,
505-
);
506-
logger.dim(
507-
` Upgrade to Node ${OXLINT_NODE_REQUIREMENT} or run: npx -p oxlint@latest react-doctor@latest`,
508-
);
509-
} else {
510-
lintSpinner?.fail("Lint checks failed (non-fatal, skipping).");
511-
logger.error(errorMessage);
499+
if (!options.scoreOnly) {
500+
const errorMessage = error instanceof Error ? error.message : String(error);
501+
const isNativeBindingError = errorMessage.includes("native binding");
502+
503+
if (isNativeBindingError) {
504+
lintSpinner?.fail(
505+
`Lint checks failed — oxlint native binding not found (Node ${process.version}).`,
506+
);
507+
logger.dim(
508+
` Upgrade to Node ${OXLINT_NODE_REQUIREMENT} or run: npx -p oxlint@latest react-doctor@latest`,
509+
);
510+
} else {
511+
lintSpinner?.fail("Lint checks failed (non-fatal, skipping).");
512+
logger.error(errorMessage);
513+
}
512514
}
513515
return [];
514516
}
@@ -527,8 +529,10 @@ export const scan = async (
527529
return knipDiagnostics;
528530
} catch (error) {
529531
didDeadCodeFail = true;
530-
deadCodeSpinner?.fail("Dead code detection failed (non-fatal, skipping).");
531-
logger.error(String(error));
532+
if (!options.scoreOnly) {
533+
deadCodeSpinner?.fail("Dead code detection failed (non-fatal, skipping).");
534+
logger.error(String(error));
535+
}
532536
return [];
533537
}
534538
})()

packages/react-doctor/src/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
export type FailOnLevel = "error" | "warning" | "none";
2+
13
export type Framework = "nextjs" | "vite" | "cra" | "remix" | "gatsby" | "expo" | "react-native" | "unknown";
24

35
export interface ProjectInfo {
@@ -163,4 +165,5 @@ export interface ReactDoctorConfig {
163165
deadCode?: boolean;
164166
verbose?: boolean;
165167
diff?: boolean | string;
168+
failOn?: FailOnLevel;
166169
}

0 commit comments

Comments
 (0)