@cleartrip/frontguard 0.3.4 → 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 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 path20 = t3.slice(tab2 + 1);
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 (path20) files.push(path20);
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 relFile = toRepoRelativePath(cwd, row.filePath);
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: relFile,
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,