@cleartrip/frontguard 0.3.5 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/dist/cli.js +236 -4
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +29 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -71,6 +71,7 @@ The PR comment includes:
|
|
|
71
71
|
| **cycles** | Detects circular imports via madge | Enabled |
|
|
72
72
|
| **dead-code** | Finds unused exports via ts-prune | Enabled |
|
|
73
73
|
| **bundle** | Measures bundle size, compares to baseline | Enabled |
|
|
74
|
+
| **react-native** | Metro config, optional align-deps / doctor, SwiftLint + native hints (RN deps only) | Enabled |
|
|
74
75
|
| **core-web-vitals** | Static CWV hints in JSX/TSX | Enabled |
|
|
75
76
|
| **ai-assisted-strict** | Static heuristics on AI-marked code / AI PRs | **Off** (under development) |
|
|
76
77
|
| **custom-rules** | Your own pattern checks defined in config | Enabled |
|
|
@@ -78,6 +79,22 @@ The PR comment includes:
|
|
|
78
79
|
|
|
79
80
|
---
|
|
80
81
|
|
|
82
|
+
## React Native check (`react-native`)
|
|
83
|
+
|
|
84
|
+
Runs when `react-native` is in dependencies (skipped otherwise).
|
|
85
|
+
|
|
86
|
+
| Sub-feature | Default | Notes |
|
|
87
|
+
|-------------|---------|--------|
|
|
88
|
+
| Metro config | **On** | Expects `metro.config.js` (or `.mjs` / `.cjs` / `.ts`) at project root. |
|
|
89
|
+
| `@rnx-kit/align-deps` | **Off** | Set `checks.reactNative.runAlignDeps: true` (uses `npx`, needs registry). Tune `alignDepsArgs` (e.g. `['--requirements', 'react-native@0.76']`). |
|
|
90
|
+
| `react-native doctor` | **Off** | Set `runDoctor: true` where the local CLI exists (can be slow / macOS-heavy). |
|
|
91
|
+
| SwiftLint | **On** (PR only) | If the PR touches `*.swift` and `swiftlint` is on `PATH`, violations are reported; otherwise an info hint. |
|
|
92
|
+
| Android / ObjC hints | **On** (PR only) | Info reminders when `android/**/*.kt|java` or `.m` / `.mm` change — does not run Gradle or Xcode. |
|
|
93
|
+
|
|
94
|
+
This is intentionally lightweight; you can extend CI with Detekt, Android Lint, or deeper Metro analysis later.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
81
98
|
## Bundle Size Strategies
|
|
82
99
|
|
|
83
100
|
The bundle check supports multiple strategies for extracting the size metric:
|
package/dist/cli.js
CHANGED
|
@@ -2517,6 +2517,19 @@ export default defineConfig({
|
|
|
2517
2517
|
// maxReportLines: 80,
|
|
2518
2518
|
// },
|
|
2519
2519
|
|
|
2520
|
+
// \u2500\u2500\u2500 React Native (when react-native is a dependency) \u2500\u2500\u2500\u2500\u2500\u2500
|
|
2521
|
+
// Metro config, optional @rnx-kit/align-deps / doctor, SwiftLint on PR .swift, native hints.
|
|
2522
|
+
// reactNative: {
|
|
2523
|
+
// enabled: true,
|
|
2524
|
+
// gate: 'info',
|
|
2525
|
+
// requireMetroConfig: true,
|
|
2526
|
+
// runAlignDeps: false, // npx @rnx-kit/align-deps (registry access)
|
|
2527
|
+
// alignDepsArgs: ['--requirements', 'react-native'],
|
|
2528
|
+
// runDoctor: false, // local react-native doctor (slow / env-specific)
|
|
2529
|
+
// swiftLintOnChangedSwift: true,
|
|
2530
|
+
// hintAndroidNativeOnPr: true,
|
|
2531
|
+
// },
|
|
2532
|
+
|
|
2520
2533
|
// \u2500\u2500\u2500 Bundle size \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2521
2534
|
// Measures bundle after build and compares to a baseline.
|
|
2522
2535
|
//
|
|
@@ -2971,13 +2984,13 @@ function parseNumstat(output) {
|
|
|
2971
2984
|
if (tab2 < 0) continue;
|
|
2972
2985
|
const aStr = t3.slice(0, tab);
|
|
2973
2986
|
const dStr = t3.slice(tab + 1, tab2);
|
|
2974
|
-
const
|
|
2987
|
+
const path21 = t3.slice(tab2 + 1);
|
|
2975
2988
|
const a3 = aStr === "-" ? 0 : Number(aStr);
|
|
2976
2989
|
const d3 = dStr === "-" ? 0 : Number(dStr);
|
|
2977
2990
|
if (!Number.isFinite(a3) || !Number.isFinite(d3)) continue;
|
|
2978
2991
|
additions += a3;
|
|
2979
2992
|
deletions += d3;
|
|
2980
|
-
if (
|
|
2993
|
+
if (path21) files.push(path21);
|
|
2981
2994
|
}
|
|
2982
2995
|
return { additions, deletions, files };
|
|
2983
2996
|
}
|
|
@@ -3220,6 +3233,16 @@ var defaultConfig = {
|
|
|
3220
3233
|
maxDeltaBytes: null,
|
|
3221
3234
|
maxTotalBytes: null
|
|
3222
3235
|
},
|
|
3236
|
+
reactNative: {
|
|
3237
|
+
enabled: true,
|
|
3238
|
+
gate: "info",
|
|
3239
|
+
requireMetroConfig: true,
|
|
3240
|
+
runAlignDeps: false,
|
|
3241
|
+
alignDepsArgs: ["--requirements", "react-native"],
|
|
3242
|
+
runDoctor: false,
|
|
3243
|
+
swiftLintOnChangedSwift: true,
|
|
3244
|
+
hintAndroidNativeOnPr: true
|
|
3245
|
+
},
|
|
3223
3246
|
coreWebVitals: {
|
|
3224
3247
|
enabled: true,
|
|
3225
3248
|
gate: "warn",
|
|
@@ -4319,13 +4342,13 @@ async function runEslint(cwd, config, _stack, pr) {
|
|
|
4319
4342
|
try {
|
|
4320
4343
|
const rows = JSON.parse(stdout2);
|
|
4321
4344
|
for (const row of rows) {
|
|
4322
|
-
const
|
|
4345
|
+
const relFile2 = toRepoRelativePath(cwd, row.filePath);
|
|
4323
4346
|
for (const m3 of row.messages) {
|
|
4324
4347
|
findings.push({
|
|
4325
4348
|
id: `eslint-${m3.ruleId ?? "unknown"}`,
|
|
4326
4349
|
severity: "warn",
|
|
4327
4350
|
message: m3.message,
|
|
4328
|
-
file:
|
|
4351
|
+
file: relFile2,
|
|
4329
4352
|
detail: m3.line ? `line ${m3.line}` : void 0
|
|
4330
4353
|
});
|
|
4331
4354
|
}
|
|
@@ -5512,6 +5535,211 @@ function dedupeFindings(f4) {
|
|
|
5512
5535
|
}
|
|
5513
5536
|
return out;
|
|
5514
5537
|
}
|
|
5538
|
+
function gateSeverity6(g4) {
|
|
5539
|
+
return g4 === "block" ? "block" : g4 === "info" ? "info" : "warn";
|
|
5540
|
+
}
|
|
5541
|
+
var METRO_CANDIDATES = [
|
|
5542
|
+
"metro.config.js",
|
|
5543
|
+
"metro.config.mjs",
|
|
5544
|
+
"metro.config.cjs",
|
|
5545
|
+
"metro.config.ts"
|
|
5546
|
+
];
|
|
5547
|
+
var SWIFTLINT_MAX_FINDINGS = 40;
|
|
5548
|
+
function prSwiftFiles(pr) {
|
|
5549
|
+
if (!hasPrFileList(pr)) return [];
|
|
5550
|
+
return pr.files.map(normalizePrPath).filter((f4) => /\.swift$/i.test(f4));
|
|
5551
|
+
}
|
|
5552
|
+
function prTouchesAndroidNative(pr) {
|
|
5553
|
+
if (!hasPrFileList(pr)) return false;
|
|
5554
|
+
return pr.files.some((f4) => {
|
|
5555
|
+
const n3 = normalizePrPath(f4);
|
|
5556
|
+
return /(^|\/)android\/.*\.(kt|kts|java)$/i.test(n3);
|
|
5557
|
+
});
|
|
5558
|
+
}
|
|
5559
|
+
function prTouchesObjc(pr) {
|
|
5560
|
+
if (!hasPrFileList(pr)) return false;
|
|
5561
|
+
return pr.files.some((f4) => /\.(m|mm)$/i.test(normalizePrPath(f4)));
|
|
5562
|
+
}
|
|
5563
|
+
function truncate5(s3, max) {
|
|
5564
|
+
if (s3.length <= max) return s3;
|
|
5565
|
+
return s3.slice(0, max) + "\u2026";
|
|
5566
|
+
}
|
|
5567
|
+
function parseSwiftLintJson(raw) {
|
|
5568
|
+
try {
|
|
5569
|
+
const j2 = JSON.parse(raw);
|
|
5570
|
+
if (Array.isArray(j2)) return j2;
|
|
5571
|
+
if (j2 && typeof j2 === "object" && Array.isArray(j2.violations)) {
|
|
5572
|
+
return j2.violations;
|
|
5573
|
+
}
|
|
5574
|
+
} catch {
|
|
5575
|
+
}
|
|
5576
|
+
return [];
|
|
5577
|
+
}
|
|
5578
|
+
function relFile(cwd, file) {
|
|
5579
|
+
if (!file) return void 0;
|
|
5580
|
+
if (path6.isAbsolute(file)) {
|
|
5581
|
+
try {
|
|
5582
|
+
return normalizePrPath(path6.relative(cwd, file));
|
|
5583
|
+
} catch {
|
|
5584
|
+
return file;
|
|
5585
|
+
}
|
|
5586
|
+
}
|
|
5587
|
+
return normalizePrPath(file);
|
|
5588
|
+
}
|
|
5589
|
+
async function runReactNative(cwd, config, stack, pr) {
|
|
5590
|
+
const t0 = performance.now();
|
|
5591
|
+
const cfg = config.checks.reactNative;
|
|
5592
|
+
if (!stack.hasReactNative) {
|
|
5593
|
+
return {
|
|
5594
|
+
checkId: "react-native",
|
|
5595
|
+
findings: [],
|
|
5596
|
+
durationMs: Math.round(performance.now() - t0),
|
|
5597
|
+
skipped: "not a React Native project"
|
|
5598
|
+
};
|
|
5599
|
+
}
|
|
5600
|
+
if (!cfg.enabled) {
|
|
5601
|
+
return {
|
|
5602
|
+
checkId: "react-native",
|
|
5603
|
+
findings: [],
|
|
5604
|
+
durationMs: 0,
|
|
5605
|
+
skipped: "disabled in config"
|
|
5606
|
+
};
|
|
5607
|
+
}
|
|
5608
|
+
const findings = [];
|
|
5609
|
+
const sev2 = gateSeverity6(cfg.gate);
|
|
5610
|
+
if (cfg.requireMetroConfig) {
|
|
5611
|
+
let found = false;
|
|
5612
|
+
for (const name of METRO_CANDIDATES) {
|
|
5613
|
+
if (await pathExists(path6.join(cwd, name))) {
|
|
5614
|
+
found = true;
|
|
5615
|
+
break;
|
|
5616
|
+
}
|
|
5617
|
+
}
|
|
5618
|
+
if (!found) {
|
|
5619
|
+
findings.push({
|
|
5620
|
+
id: "rn-metro-config-missing",
|
|
5621
|
+
severity: sev2,
|
|
5622
|
+
message: "No Metro config found (expected metro.config.js|mjs|cjs|ts at project root)",
|
|
5623
|
+
detail: "Metro bundles JavaScript for React Native. Expo and RN templates normally include metro.config.js."
|
|
5624
|
+
});
|
|
5625
|
+
}
|
|
5626
|
+
}
|
|
5627
|
+
if (cfg.runAlignDeps) {
|
|
5628
|
+
const r4 = await runNpx(cwd, ["--yes", "@rnx-kit/align-deps", ...cfg.alignDepsArgs]);
|
|
5629
|
+
if ((r4.exitCode ?? 0) !== 0) {
|
|
5630
|
+
findings.push({
|
|
5631
|
+
id: "rn-align-deps-failed",
|
|
5632
|
+
severity: sev2,
|
|
5633
|
+
message: "@rnx-kit/align-deps reported dependency mismatches",
|
|
5634
|
+
detail: truncate5(
|
|
5635
|
+
[r4.stdout, r4.stderr].filter(Boolean).join("\n") || `exit ${r4.exitCode}`,
|
|
5636
|
+
8e3
|
|
5637
|
+
)
|
|
5638
|
+
});
|
|
5639
|
+
}
|
|
5640
|
+
}
|
|
5641
|
+
if (cfg.runDoctor) {
|
|
5642
|
+
const r4 = await runNpmBinary(cwd, "react-native", ["doctor"]);
|
|
5643
|
+
if (r4.exitCode === 127 || /not recognized|command not found|ENOENT/i.test(r4.stderr)) {
|
|
5644
|
+
findings.push({
|
|
5645
|
+
id: "rn-doctor-skipped",
|
|
5646
|
+
severity: "info",
|
|
5647
|
+
message: "react-native doctor not run (CLI binary not found in this project)",
|
|
5648
|
+
detail: "Install dependencies so `node_modules/.bin/react-native` exists, or only enable runDoctor on agents with the RN CLI."
|
|
5649
|
+
});
|
|
5650
|
+
} else if ((r4.exitCode ?? 0) !== 0) {
|
|
5651
|
+
findings.push({
|
|
5652
|
+
id: "rn-doctor-issues",
|
|
5653
|
+
severity: sev2,
|
|
5654
|
+
message: "react-native doctor exited with errors",
|
|
5655
|
+
detail: truncate5([r4.stdout, r4.stderr].filter(Boolean).join("\n"), 8e3)
|
|
5656
|
+
});
|
|
5657
|
+
}
|
|
5658
|
+
}
|
|
5659
|
+
const swifts = prSwiftFiles(pr);
|
|
5660
|
+
if (cfg.swiftLintOnChangedSwift && swifts.length > 0) {
|
|
5661
|
+
const existing = await existingRepoPaths(cwd, swifts);
|
|
5662
|
+
if (existing.length > 0) {
|
|
5663
|
+
let ver;
|
|
5664
|
+
try {
|
|
5665
|
+
ver = await W2("swiftlint", ["version"], { nodeOptions: { cwd } });
|
|
5666
|
+
} catch {
|
|
5667
|
+
ver = { exitCode: 1, stdout: "", stderr: "" };
|
|
5668
|
+
}
|
|
5669
|
+
if ((ver.exitCode ?? 0) !== 0) {
|
|
5670
|
+
findings.push({
|
|
5671
|
+
id: "rn-swiftlint-unavailable",
|
|
5672
|
+
severity: "info",
|
|
5673
|
+
message: "PR changes Swift files but SwiftLint is not on PATH",
|
|
5674
|
+
detail: `Touched: ${existing.slice(0, 12).join(", ")}${existing.length > 12 ? "\u2026" : ""}`
|
|
5675
|
+
});
|
|
5676
|
+
} else {
|
|
5677
|
+
let r4;
|
|
5678
|
+
try {
|
|
5679
|
+
r4 = await W2("swiftlint", ["lint", "--reporter", "json", "--quiet", ...existing], {
|
|
5680
|
+
nodeOptions: { cwd }
|
|
5681
|
+
});
|
|
5682
|
+
} catch (e3) {
|
|
5683
|
+
r4 = {
|
|
5684
|
+
exitCode: 1,
|
|
5685
|
+
stdout: "",
|
|
5686
|
+
stderr: e3 instanceof Error ? e3.message : String(e3)
|
|
5687
|
+
};
|
|
5688
|
+
}
|
|
5689
|
+
const out = (r4.stdout ?? "").trim();
|
|
5690
|
+
const violations = out ? parseSwiftLintJson(out) : [];
|
|
5691
|
+
if (violations.length > 0) {
|
|
5692
|
+
const slice = violations.slice(0, SWIFTLINT_MAX_FINDINGS);
|
|
5693
|
+
for (const v3 of slice) {
|
|
5694
|
+
const isErr = (v3.severity ?? "").toLowerCase() === "error";
|
|
5695
|
+
findings.push({
|
|
5696
|
+
id: `rn-swiftlint-${(v3.rule_id ?? "rule").replace(/[^a-z0-9-]/gi, "-")}`,
|
|
5697
|
+
severity: isErr ? sev2 : "warn",
|
|
5698
|
+
message: v3.reason ?? "SwiftLint violation",
|
|
5699
|
+
file: relFile(cwd, v3.file),
|
|
5700
|
+
detail: v3.line != null ? `line ${v3.line}` : void 0
|
|
5701
|
+
});
|
|
5702
|
+
}
|
|
5703
|
+
if (violations.length > SWIFTLINT_MAX_FINDINGS) {
|
|
5704
|
+
findings.push({
|
|
5705
|
+
id: "rn-swiftlint-truncated",
|
|
5706
|
+
severity: "info",
|
|
5707
|
+
message: `SwiftLint: ${violations.length - SWIFTLINT_MAX_FINDINGS} more violation(s) not listed`
|
|
5708
|
+
});
|
|
5709
|
+
}
|
|
5710
|
+
} else if ((r4.exitCode ?? 0) !== 0) {
|
|
5711
|
+
findings.push({
|
|
5712
|
+
id: "rn-swiftlint-failed",
|
|
5713
|
+
severity: sev2,
|
|
5714
|
+
message: "SwiftLint failed",
|
|
5715
|
+
detail: truncate5([out, r4.stderr].filter(Boolean).join("\n") || `exit ${r4.exitCode}`, 4e3)
|
|
5716
|
+
});
|
|
5717
|
+
}
|
|
5718
|
+
}
|
|
5719
|
+
}
|
|
5720
|
+
}
|
|
5721
|
+
if (cfg.hintAndroidNativeOnPr && prTouchesAndroidNative(pr)) {
|
|
5722
|
+
findings.push({
|
|
5723
|
+
id: "rn-android-native-hint",
|
|
5724
|
+
severity: "info",
|
|
5725
|
+
message: "PR touches Android Kotlin/Java under android/",
|
|
5726
|
+
detail: "Consider Android Lint (`./gradlew :app:lint`) or Detekt in CI when native sources change."
|
|
5727
|
+
});
|
|
5728
|
+
}
|
|
5729
|
+
if (hasPrFileList(pr) && prTouchesObjc(pr)) {
|
|
5730
|
+
findings.push({
|
|
5731
|
+
id: "rn-ios-objc-hint",
|
|
5732
|
+
severity: "info",
|
|
5733
|
+
message: "PR touches Objective-C (.m / .mm) sources",
|
|
5734
|
+
detail: "Consider clang-format or Xcode Analyze on changed native files."
|
|
5735
|
+
});
|
|
5736
|
+
}
|
|
5737
|
+
return {
|
|
5738
|
+
checkId: "react-native",
|
|
5739
|
+
findings,
|
|
5740
|
+
durationMs: Math.round(performance.now() - t0)
|
|
5741
|
+
};
|
|
5742
|
+
}
|
|
5515
5743
|
var DEFAULT_GLOB = "**/*.{ts,tsx,js,jsx,mjs,cjs}";
|
|
5516
5744
|
async function runCustomRules(cwd, config, restrictToFiles) {
|
|
5517
5745
|
const t0 = performance.now();
|
|
@@ -6106,6 +6334,7 @@ var CHECK_DESCRIPTIONS = {
|
|
|
6106
6334
|
cycles: "Runs madge for circular dependencies on TypeScript/JavaScript entry points. Import cycles can cause brittle builds and load order bugs.",
|
|
6107
6335
|
"dead-code": "Runs ts-prune to find unused exports in the TypeScript project. Helps trim dead surface area.",
|
|
6108
6336
|
bundle: "Measures bundle size using a stack-aware strategy (Next.js build output, Vite/CRA file sizes, or custom command) and compares to a checked-in baseline.",
|
|
6337
|
+
"react-native": "When react-native is a dependency: Metro config presence, optional @rnx-kit/align-deps and react-native doctor, SwiftLint on changed Swift in PRs, and light hints for Android/iOS native edits.",
|
|
6109
6338
|
"core-web-vitals": "Static hints in JSX/TSX related to Core Web Vitals (e.g. LCP-friendly images, main-thread hygiene). Not a substitute for real field metrics.",
|
|
6110
6339
|
"ai-assisted-strict": "Under development \u2014 off by default. When enabled: scans @frontguard-ai regions or AI-disclosed PR files for risky patterns (eval, XSS sinks, etc.) and AST heuristics.",
|
|
6111
6340
|
"pr-hygiene": "Validates PR metadata when CI provides PR context: description length, checklist items, and similar hygiene rules from config.",
|
|
@@ -7385,6 +7614,7 @@ async function runFrontGuard(opts) {
|
|
|
7385
7614
|
cycles,
|
|
7386
7615
|
deadCode,
|
|
7387
7616
|
coreWebVitals,
|
|
7617
|
+
reactNative,
|
|
7388
7618
|
tsAnyDelta,
|
|
7389
7619
|
customRules,
|
|
7390
7620
|
aiStrict
|
|
@@ -7396,6 +7626,7 @@ async function runFrontGuard(opts) {
|
|
|
7396
7626
|
runCycles(opts.cwd, config, stack, pr),
|
|
7397
7627
|
runDeadCode(opts.cwd, config, stack, pr),
|
|
7398
7628
|
runCoreWebVitals(opts.cwd, config, stack, pr),
|
|
7629
|
+
runReactNative(opts.cwd, config, stack, pr),
|
|
7399
7630
|
runTsAnyDelta(opts.cwd, config, stack),
|
|
7400
7631
|
runCustomRules(opts.cwd, config, restrictFiles),
|
|
7401
7632
|
runAiAssistedStrict(opts.cwd, config, pr)
|
|
@@ -7412,6 +7643,7 @@ async function runFrontGuard(opts) {
|
|
|
7412
7643
|
deadCode,
|
|
7413
7644
|
bundle,
|
|
7414
7645
|
coreWebVitals,
|
|
7646
|
+
reactNative,
|
|
7415
7647
|
aiStrict,
|
|
7416
7648
|
prHygiene,
|
|
7417
7649
|
prSize,
|