@cleartrip/frontguard 0.1.8 → 0.2.0
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/dist/cli.js +217 -206
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +14 -3
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/bitbucket-pipelines.yml +45 -54
- package/templates/freekit-ci-setup.md +27 -0
package/dist/cli.js
CHANGED
|
@@ -6,10 +6,10 @@ import * as tty from 'tty';
|
|
|
6
6
|
import { WriteStream } from 'tty';
|
|
7
7
|
import path4, { sep, normalize, delimiter, resolve, dirname } from 'path';
|
|
8
8
|
import fs from 'fs/promises';
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
10
|
+
import { execFileSync, spawn } from 'child_process';
|
|
9
11
|
import { createRequire } from 'module';
|
|
10
|
-
import
|
|
11
|
-
import { pathToFileURL } from 'url';
|
|
12
|
-
import { spawn } from 'child_process';
|
|
12
|
+
import fs2 from 'fs';
|
|
13
13
|
import { pipeline } from 'stream/promises';
|
|
14
14
|
import { PassThrough } from 'stream';
|
|
15
15
|
import fg from 'fast-glob';
|
|
@@ -2398,50 +2398,9 @@ async function runMain(cmd, opts = {}) {
|
|
|
2398
2398
|
process.exit(1);
|
|
2399
2399
|
}
|
|
2400
2400
|
}
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
pull_request:
|
|
2405
|
-
types: [opened, synchronize, reopened]
|
|
2406
|
-
|
|
2407
|
-
permissions:
|
|
2408
|
-
contents: read
|
|
2409
|
-
pull-requests: write
|
|
2410
|
-
|
|
2411
|
-
concurrency:
|
|
2412
|
-
group: frontguard-\${{ github.workflow }}-\${{ github.event.pull_request.number || github.ref }}
|
|
2413
|
-
cancel-in-progress: true
|
|
2414
|
-
|
|
2415
|
-
jobs:
|
|
2416
|
-
review-brief:
|
|
2417
|
-
runs-on: ubuntu-latest
|
|
2418
|
-
steps:
|
|
2419
|
-
- uses: actions/checkout@v4
|
|
2420
|
-
with:
|
|
2421
|
-
fetch-depth: 0
|
|
2422
|
-
|
|
2423
|
-
- uses: actions/setup-node@v4
|
|
2424
|
-
with:
|
|
2425
|
-
node-version: 20
|
|
2426
|
-
|
|
2427
|
-
- name: Install dependencies
|
|
2428
|
-
run: |
|
|
2429
|
-
if [ -f pnpm-lock.yaml ]; then
|
|
2430
|
-
corepack enable
|
|
2431
|
-
pnpm install --frozen-lockfile || pnpm install
|
|
2432
|
-
elif [ -f yarn.lock ]; then
|
|
2433
|
-
yarn install --frozen-lockfile || yarn install
|
|
2434
|
-
elif [ -f package-lock.json ]; then
|
|
2435
|
-
npm ci || npm install
|
|
2436
|
-
else
|
|
2437
|
-
npm install
|
|
2438
|
-
fi
|
|
2439
|
-
|
|
2440
|
-
- name: FrontGuard (Phase 1 \u2014 warn-only)
|
|
2441
|
-
run: npx @cleartrip/frontguard run --ci
|
|
2442
|
-
env:
|
|
2443
|
-
GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
2444
|
-
`;
|
|
2401
|
+
function packageRoot() {
|
|
2402
|
+
return path4.resolve(path4.dirname(fileURLToPath(import.meta.url)), "..");
|
|
2403
|
+
}
|
|
2445
2404
|
var CONFIG = `import { defineConfig } from '@cleartrip/frontguard'
|
|
2446
2405
|
|
|
2447
2406
|
export default defineConfig({
|
|
@@ -2458,7 +2417,7 @@ export default defineConfig({
|
|
|
2458
2417
|
// },
|
|
2459
2418
|
|
|
2460
2419
|
// checks: {
|
|
2461
|
-
// bundle: {
|
|
2420
|
+
// bundle: { baselineRef: 'main', maxDeltaBytes: 50_000 }, // ref for git baseline file
|
|
2462
2421
|
// cycles: { enabled: true },
|
|
2463
2422
|
// deadCode: { enabled: true, gate: 'info' },
|
|
2464
2423
|
// // LLM: cloud keys in CI, or local Ollama (no API key) on dev/self-hosted runners:
|
|
@@ -2496,27 +2455,35 @@ If **Yes**, list tools and what they touched (helps reviewers run a stricter fir
|
|
|
2496
2455
|
## AI assistance (optional detail)
|
|
2497
2456
|
- [ ] I have reviewed every AI-suggested line for security, auth, and product correctness
|
|
2498
2457
|
`;
|
|
2499
|
-
async function ensureDir(dir) {
|
|
2500
|
-
await fs.mkdir(dir, { recursive: true });
|
|
2501
|
-
}
|
|
2502
2458
|
async function initFrontGuard(cwd) {
|
|
2503
|
-
const
|
|
2504
|
-
|
|
2505
|
-
const
|
|
2506
|
-
|
|
2459
|
+
const root = packageRoot();
|
|
2460
|
+
const tplPath = path4.join(root, "templates", "bitbucket-pipelines.yml");
|
|
2461
|
+
const outPipeline = path4.join(cwd, "bitbucket-pipelines.frontguard.example.yml");
|
|
2462
|
+
try {
|
|
2463
|
+
await fs.access(outPipeline);
|
|
2464
|
+
} catch {
|
|
2465
|
+
try {
|
|
2466
|
+
const yml = await fs.readFile(tplPath, "utf8");
|
|
2467
|
+
await fs.writeFile(outPipeline, yml, "utf8");
|
|
2468
|
+
} catch {
|
|
2469
|
+
await fs.writeFile(
|
|
2470
|
+
outPipeline,
|
|
2471
|
+
"# Copy bitbucket-pipelines.yml from @cleartrip/frontguard/templates in node_modules\n",
|
|
2472
|
+
"utf8"
|
|
2473
|
+
);
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2507
2476
|
const cfgPath = path4.join(cwd, "frontguard.config.js");
|
|
2508
2477
|
try {
|
|
2509
2478
|
await fs.access(cfgPath);
|
|
2510
2479
|
} catch {
|
|
2511
2480
|
await fs.writeFile(cfgPath, CONFIG, "utf8");
|
|
2512
2481
|
}
|
|
2513
|
-
const
|
|
2514
|
-
await ensureDir(tplRoot);
|
|
2515
|
-
const tplPath = path4.join(tplRoot, "pull_request_template.md");
|
|
2482
|
+
const tplPr = path4.join(cwd, "pull_request_template.md");
|
|
2516
2483
|
try {
|
|
2517
|
-
await fs.access(
|
|
2484
|
+
await fs.access(tplPr);
|
|
2518
2485
|
} catch {
|
|
2519
|
-
await fs.writeFile(
|
|
2486
|
+
await fs.writeFile(tplPr, PR_TEMPLATE, "utf8");
|
|
2520
2487
|
}
|
|
2521
2488
|
}
|
|
2522
2489
|
|
|
@@ -2544,6 +2511,11 @@ function bitbucketDownloadsPageUrl() {
|
|
|
2544
2511
|
}
|
|
2545
2512
|
function formatBitbucketPrSnippet(report) {
|
|
2546
2513
|
const publicReport = process.env.FRONTGUARD_PUBLIC_REPORT_URL?.trim();
|
|
2514
|
+
const linkOnly = process.env.FRONTGUARD_BITBUCKET_COMMENT_LINK_ONLY === "1";
|
|
2515
|
+
if (linkOnly && publicReport) {
|
|
2516
|
+
return publicReport.endsWith("\n") ? publicReport : `${publicReport}
|
|
2517
|
+
`;
|
|
2518
|
+
}
|
|
2547
2519
|
const downloadsName = process.env.FRONTGUARD_REPORT_DOWNLOAD_NAME?.trim();
|
|
2548
2520
|
const downloadsPage = bitbucketDownloadsPageUrl();
|
|
2549
2521
|
const pipeline = bitbucketPipelineResultsUrl();
|
|
@@ -2649,110 +2621,86 @@ function parseAiDisclosure(body) {
|
|
|
2649
2621
|
return { assisted, explicitNo, ambiguous };
|
|
2650
2622
|
}
|
|
2651
2623
|
|
|
2652
|
-
// src/ci/
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
headRef: pr.head?.ref ?? "",
|
|
2670
|
-
additions: pr.additions ?? 0,
|
|
2671
|
-
deletions: pr.deletions ?? 0,
|
|
2672
|
-
changedFiles: pr.changed_files ?? files.length,
|
|
2673
|
-
files,
|
|
2674
|
-
aiAssisted: ai.assisted,
|
|
2675
|
-
aiExplicitNo: ai.explicitNo,
|
|
2676
|
-
aiDisclosureAmbiguous: ai.ambiguous
|
|
2677
|
-
};
|
|
2678
|
-
} catch {
|
|
2679
|
-
return null;
|
|
2680
|
-
}
|
|
2681
|
-
}
|
|
2682
|
-
|
|
2683
|
-
// src/lib/http-fetch.ts
|
|
2684
|
-
function getFetch() {
|
|
2685
|
-
const f4 = globalThis.fetch;
|
|
2686
|
-
if (typeof f4 !== "function") {
|
|
2687
|
-
throw new Error(
|
|
2688
|
-
"FrontGuard needs Node.js 18+ (global fetch). Use NODE_VERSION / image >= 18 in CI."
|
|
2689
|
-
);
|
|
2690
|
-
}
|
|
2691
|
-
return f4;
|
|
2692
|
-
}
|
|
2693
|
-
|
|
2694
|
-
// src/ci/pr-comment.ts
|
|
2695
|
-
var MARKER = "<!-- frontguard:brief -->";
|
|
2696
|
-
async function resolvePrNumber() {
|
|
2697
|
-
const raw = process.env.FRONTGUARD_PR_NUMBER ?? process.env.PR_NUMBER;
|
|
2698
|
-
const n3 = Number(raw);
|
|
2699
|
-
if (Number.isFinite(n3) && n3 > 0) return n3;
|
|
2700
|
-
const path17 = process.env.GITHUB_EVENT_PATH;
|
|
2701
|
-
if (!path17) return null;
|
|
2702
|
-
try {
|
|
2703
|
-
const payload = JSON.parse(await fs.readFile(path17, "utf8"));
|
|
2704
|
-
const num = payload.pull_request?.number;
|
|
2705
|
-
return typeof num === "number" && num > 0 ? num : null;
|
|
2706
|
-
} catch {
|
|
2707
|
-
return null;
|
|
2624
|
+
// src/ci/pr-context.ts
|
|
2625
|
+
function gitTrimmed(cwd, args) {
|
|
2626
|
+
return execFileSync("git", ["-C", cwd, ...args], {
|
|
2627
|
+
encoding: "utf8",
|
|
2628
|
+
maxBuffer: 20 * 1024 * 1024
|
|
2629
|
+
}).trimEnd();
|
|
2630
|
+
}
|
|
2631
|
+
function resolveCompareRef(cwd, destBranch) {
|
|
2632
|
+
const safe = destBranch.trim();
|
|
2633
|
+
if (!safe || safe.startsWith("-") || safe.includes("..")) return null;
|
|
2634
|
+
const candidates = [safe, `origin/${safe}`];
|
|
2635
|
+
for (const c4 of candidates) {
|
|
2636
|
+
try {
|
|
2637
|
+
gitTrimmed(cwd, ["rev-parse", "--verify", `${c4}^{commit}`]);
|
|
2638
|
+
return c4;
|
|
2639
|
+
} catch {
|
|
2640
|
+
}
|
|
2708
2641
|
}
|
|
2642
|
+
return null;
|
|
2709
2643
|
}
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2644
|
+
function parseNumstat(output) {
|
|
2645
|
+
let additions = 0;
|
|
2646
|
+
let deletions = 0;
|
|
2647
|
+
const files = [];
|
|
2648
|
+
for (const line of output.split("\n")) {
|
|
2649
|
+
const t3 = line.trim();
|
|
2650
|
+
if (!t3) continue;
|
|
2651
|
+
const tab = t3.indexOf(" ");
|
|
2652
|
+
if (tab < 0) continue;
|
|
2653
|
+
const tab2 = t3.indexOf(" ", tab + 1);
|
|
2654
|
+
if (tab2 < 0) continue;
|
|
2655
|
+
const aStr = t3.slice(0, tab);
|
|
2656
|
+
const dStr = t3.slice(tab + 1, tab2);
|
|
2657
|
+
const path17 = t3.slice(tab2 + 1);
|
|
2658
|
+
const a3 = aStr === "-" ? 0 : Number(aStr);
|
|
2659
|
+
const d3 = dStr === "-" ? 0 : Number(dStr);
|
|
2660
|
+
if (!Number.isFinite(a3) || !Number.isFinite(d3)) continue;
|
|
2661
|
+
additions += a3;
|
|
2662
|
+
deletions += d3;
|
|
2663
|
+
if (path17) files.push(path17);
|
|
2664
|
+
}
|
|
2665
|
+
return { additions, deletions, files };
|
|
2666
|
+
}
|
|
2667
|
+
function buildBitbucketPrContext(cwd) {
|
|
2668
|
+
const prId = process.env.BITBUCKET_PR_ID?.trim();
|
|
2669
|
+
if (!prId) return null;
|
|
2670
|
+
const dest = process.env.BITBUCKET_PR_DESTINATION_BRANCH?.trim() || "main";
|
|
2671
|
+
const body = process.env.FRONTGUARD_PR_BODY?.trim() ?? "";
|
|
2672
|
+
const headRef = process.env.BITBUCKET_PR_SOURCE_BRANCH?.trim() ?? "";
|
|
2673
|
+
const title = process.env.BITBUCKET_PR_TITLE?.trim() ?? "";
|
|
2674
|
+
let additions = 0;
|
|
2675
|
+
let deletions = 0;
|
|
2676
|
+
let files = [];
|
|
2677
|
+
const destRef = resolveCompareRef(cwd, dest);
|
|
2678
|
+
if (destRef) {
|
|
2679
|
+
try {
|
|
2680
|
+
const raw = gitTrimmed(cwd, ["diff", `${destRef}...HEAD`, "--numstat"]);
|
|
2681
|
+
const stat = parseNumstat(raw);
|
|
2682
|
+
additions = stat.additions;
|
|
2683
|
+
deletions = stat.deletions;
|
|
2684
|
+
files = stat.files;
|
|
2685
|
+
} catch {
|
|
2686
|
+
}
|
|
2721
2687
|
}
|
|
2722
|
-
const
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2688
|
+
const ai = parseAiDisclosure(body);
|
|
2689
|
+
return {
|
|
2690
|
+
number: Number.parseInt(prId, 10) || 0,
|
|
2691
|
+
title,
|
|
2692
|
+
body,
|
|
2693
|
+
baseRef: dest,
|
|
2694
|
+
headRef,
|
|
2695
|
+
additions,
|
|
2696
|
+
deletions,
|
|
2697
|
+
changedFiles: files.length,
|
|
2698
|
+
files,
|
|
2699
|
+
...ai
|
|
2728
2700
|
};
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
const fetch = getFetch();
|
|
2733
|
-
const listRes = await fetch(listUrl, { headers });
|
|
2734
|
-
if (!listRes.ok) {
|
|
2735
|
-
return;
|
|
2736
|
-
}
|
|
2737
|
-
const comments = await listRes.json();
|
|
2738
|
-
const existing = comments.find(
|
|
2739
|
-
(c4) => typeof c4.body === "string" && c4.body.includes(MARKER)
|
|
2740
|
-
);
|
|
2741
|
-
if (existing) {
|
|
2742
|
-
const patchUrl = `${apiBase}/repos/${owner}/${name}/issues/comments/${existing.id}`;
|
|
2743
|
-
await fetch(patchUrl, {
|
|
2744
|
-
method: "PATCH",
|
|
2745
|
-
headers,
|
|
2746
|
-
body: JSON.stringify({ body: prefixed })
|
|
2747
|
-
});
|
|
2748
|
-
return;
|
|
2749
|
-
}
|
|
2750
|
-
const postUrl = `${apiBase}/repos/${owner}/${name}/issues/${prNumber}/comments`;
|
|
2751
|
-
await fetch(postUrl, {
|
|
2752
|
-
method: "POST",
|
|
2753
|
-
headers,
|
|
2754
|
-
body: JSON.stringify({ body: prefixed })
|
|
2755
|
-
});
|
|
2701
|
+
}
|
|
2702
|
+
function readPrContext(cwd) {
|
|
2703
|
+
return buildBitbucketPrContext(cwd);
|
|
2756
2704
|
}
|
|
2757
2705
|
|
|
2758
2706
|
// node_modules/defu/dist/defu.mjs
|
|
@@ -2839,7 +2787,7 @@ var defaultConfig = {
|
|
|
2839
2787
|
tsAnyDeltaToBlock: true
|
|
2840
2788
|
}
|
|
2841
2789
|
},
|
|
2842
|
-
prSize: { warnLines: 400, softBlockLines: 800 },
|
|
2790
|
+
prSize: { enabled: true, warnLines: 400, softBlockLines: 800 },
|
|
2843
2791
|
tsAnyDelta: {
|
|
2844
2792
|
enabled: true,
|
|
2845
2793
|
gate: "warn",
|
|
@@ -2847,24 +2795,25 @@ var defaultConfig = {
|
|
|
2847
2795
|
maxAdded: 0
|
|
2848
2796
|
},
|
|
2849
2797
|
cycles: {
|
|
2850
|
-
enabled:
|
|
2798
|
+
enabled: true,
|
|
2851
2799
|
gate: "warn",
|
|
2852
2800
|
entries: ["src"],
|
|
2853
2801
|
extraArgs: []
|
|
2854
2802
|
},
|
|
2855
2803
|
deadCode: {
|
|
2856
|
-
enabled:
|
|
2804
|
+
enabled: true,
|
|
2857
2805
|
gate: "info",
|
|
2858
2806
|
extraArgs: [],
|
|
2859
2807
|
maxReportLines: 80
|
|
2860
2808
|
},
|
|
2861
2809
|
bundle: {
|
|
2862
|
-
enabled:
|
|
2810
|
+
enabled: true,
|
|
2863
2811
|
gate: "warn",
|
|
2864
2812
|
runBuild: true,
|
|
2865
2813
|
buildCommand: "npm run build",
|
|
2866
2814
|
measureGlobs: ["dist/**/*", "build/static/**/*", ".next/static/**/*"],
|
|
2867
2815
|
baselinePath: ".frontguard/bundle-baseline.json",
|
|
2816
|
+
baselineRef: "main",
|
|
2868
2817
|
maxDeltaBytes: null,
|
|
2869
2818
|
maxTotalBytes: null
|
|
2870
2819
|
},
|
|
@@ -2931,7 +2880,7 @@ async function loadConfig(cwd) {
|
|
|
2931
2880
|
let userFile = null;
|
|
2932
2881
|
for (const name of CONFIG_NAMES) {
|
|
2933
2882
|
const full = path4.join(cwd, name);
|
|
2934
|
-
if (!
|
|
2883
|
+
if (!fs2.existsSync(full)) continue;
|
|
2935
2884
|
try {
|
|
2936
2885
|
const mod = await importConfig(full);
|
|
2937
2886
|
userFile = normalizeExport(mod);
|
|
@@ -4078,7 +4027,7 @@ function runPrHygiene(config, pr) {
|
|
|
4078
4027
|
checkId: "pr-hygiene",
|
|
4079
4028
|
findings: [],
|
|
4080
4029
|
durationMs: Math.round(performance.now() - t0),
|
|
4081
|
-
skipped: "
|
|
4030
|
+
skipped: "no PR context (run in a Bitbucket PR pipeline with BITBUCKET_PR_ID, or set FRONTGUARD_PR_BODY for description checks)"
|
|
4082
4031
|
};
|
|
4083
4032
|
}
|
|
4084
4033
|
const findings = [];
|
|
@@ -4159,12 +4108,20 @@ function sectionMentioned(body, hint) {
|
|
|
4159
4108
|
// src/checks/pr-size.ts
|
|
4160
4109
|
function runPrSize(config, pr) {
|
|
4161
4110
|
const t0 = performance.now();
|
|
4111
|
+
if (!config.checks.prSize.enabled) {
|
|
4112
|
+
return {
|
|
4113
|
+
checkId: "pr-size",
|
|
4114
|
+
findings: [],
|
|
4115
|
+
durationMs: 0,
|
|
4116
|
+
skipped: "disabled in config"
|
|
4117
|
+
};
|
|
4118
|
+
}
|
|
4162
4119
|
if (!pr) {
|
|
4163
4120
|
return {
|
|
4164
4121
|
checkId: "pr-size",
|
|
4165
4122
|
findings: [],
|
|
4166
4123
|
durationMs: Math.round(performance.now() - t0),
|
|
4167
|
-
skipped: "
|
|
4124
|
+
skipped: "no PR context (run in a Bitbucket pull-request pipeline so BITBUCKET_PR_* and git diff are available)"
|
|
4168
4125
|
};
|
|
4169
4126
|
}
|
|
4170
4127
|
const findings = [];
|
|
@@ -4234,13 +4191,18 @@ async function gitDiffForReview(cwd, baseRef, maxChars) {
|
|
|
4234
4191
|
}
|
|
4235
4192
|
}
|
|
4236
4193
|
async function resolveDiffBaseRef(cwd, fallback) {
|
|
4237
|
-
const
|
|
4238
|
-
if (
|
|
4239
|
-
const origin = `origin/${
|
|
4194
|
+
const bb = process.env.BITBUCKET_PR_DESTINATION_BRANCH?.trim();
|
|
4195
|
+
if (bb) {
|
|
4196
|
+
const origin = `origin/${bb}`;
|
|
4240
4197
|
try {
|
|
4241
4198
|
await W2("git", ["rev-parse", "--verify", origin], { nodeOptions: { cwd } });
|
|
4242
4199
|
return origin;
|
|
4243
4200
|
} catch {
|
|
4201
|
+
try {
|
|
4202
|
+
await W2("git", ["rev-parse", "--verify", bb], { nodeOptions: { cwd } });
|
|
4203
|
+
return bb;
|
|
4204
|
+
} catch {
|
|
4205
|
+
}
|
|
4244
4206
|
}
|
|
4245
4207
|
}
|
|
4246
4208
|
try {
|
|
@@ -4519,6 +4481,39 @@ async function gitOkQuick(cwd) {
|
|
|
4519
4481
|
function tokenizeCommand(cmd) {
|
|
4520
4482
|
return cmd.trim().split(/\s+/).map((t3) => t3.trim()).filter(Boolean);
|
|
4521
4483
|
}
|
|
4484
|
+
function npmScriptFromBuildCommand(cmd) {
|
|
4485
|
+
const t3 = cmd.trim();
|
|
4486
|
+
if (/^yarn\s+build\b/i.test(t3)) return "build";
|
|
4487
|
+
const np = /^(?:npm|pnpm)\s+run\s+(\S+)/i.exec(t3);
|
|
4488
|
+
if (np?.[1]) return np[1];
|
|
4489
|
+
const yr = /^yarn\s+run\s+(\S+)/i.exec(t3);
|
|
4490
|
+
if (yr?.[1]) return yr[1];
|
|
4491
|
+
return null;
|
|
4492
|
+
}
|
|
4493
|
+
async function bundleBuildPrecheck(cwd, buildCommand) {
|
|
4494
|
+
const script = npmScriptFromBuildCommand(buildCommand);
|
|
4495
|
+
if (!script) return { run: true };
|
|
4496
|
+
let scripts;
|
|
4497
|
+
try {
|
|
4498
|
+
const raw = await fs.readFile(path4.join(cwd, "package.json"), "utf8");
|
|
4499
|
+
const pkg = JSON.parse(raw);
|
|
4500
|
+
scripts = pkg.scripts;
|
|
4501
|
+
} catch {
|
|
4502
|
+
return {
|
|
4503
|
+
run: false,
|
|
4504
|
+
message: "Skipped bundle build \u2014 no readable package.json",
|
|
4505
|
+
detail: "Set checks.bundle.buildCommand to your real build (e.g. `vite build`), set checks.bundle.runBuild to false, or add a package.json with the matching script."
|
|
4506
|
+
};
|
|
4507
|
+
}
|
|
4508
|
+
if (!scripts?.[script]) {
|
|
4509
|
+
return {
|
|
4510
|
+
run: false,
|
|
4511
|
+
message: `Skipped bundle build \u2014 no scripts.${script} in package.json`,
|
|
4512
|
+
detail: "The bundle check runs a production build, then sums file sizes under checks.bundle.measureGlobs (dist/, build/static/, .next/static/, etc.). Libraries and non-web repos often have no `build` script \u2014 set checks.bundle.runBuild to false and optionally tune measureGlobs, or set checks.bundle.buildCommand to whatever produces your artifacts (e.g. `pnpm run compile`, `npx vite build`)."
|
|
4513
|
+
};
|
|
4514
|
+
}
|
|
4515
|
+
return { run: true };
|
|
4516
|
+
}
|
|
4522
4517
|
async function runBundle(cwd, config, stack) {
|
|
4523
4518
|
const t0 = performance.now();
|
|
4524
4519
|
const cfg = config.checks.bundle;
|
|
@@ -4538,6 +4533,7 @@ async function runBundle(cwd, config, stack) {
|
|
|
4538
4533
|
skipped: "skipped for React Native (configure web artifacts if needed)"
|
|
4539
4534
|
};
|
|
4540
4535
|
}
|
|
4536
|
+
const preFindings = [];
|
|
4541
4537
|
if (cfg.runBuild) {
|
|
4542
4538
|
const parts = tokenizeCommand(cfg.buildCommand);
|
|
4543
4539
|
if (parts.length === 0) {
|
|
@@ -4553,21 +4549,31 @@ async function runBundle(cwd, config, stack) {
|
|
|
4553
4549
|
durationMs: Math.round(performance.now() - t0)
|
|
4554
4550
|
};
|
|
4555
4551
|
}
|
|
4556
|
-
const
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4552
|
+
const pre = await bundleBuildPrecheck(cwd, cfg.buildCommand);
|
|
4553
|
+
if (!pre.run) {
|
|
4554
|
+
preFindings.push({
|
|
4555
|
+
id: "bundle-build-skipped",
|
|
4556
|
+
severity: "info",
|
|
4557
|
+
message: pre.message,
|
|
4558
|
+
detail: pre.detail
|
|
4559
|
+
});
|
|
4560
|
+
} else {
|
|
4561
|
+
const [bin, ...args] = parts;
|
|
4562
|
+
const res = await W2(bin, args, { nodeOptions: { cwd } });
|
|
4563
|
+
if ((res.exitCode ?? 0) !== 0) {
|
|
4564
|
+
return {
|
|
4565
|
+
checkId: "bundle",
|
|
4566
|
+
findings: [
|
|
4567
|
+
{
|
|
4568
|
+
id: "bundle-build",
|
|
4569
|
+
severity: gateSeverity4(cfg.gate),
|
|
4570
|
+
message: "Build command failed \u2014 cannot measure bundle",
|
|
4571
|
+
detail: [res.stdout, res.stderr].filter(Boolean).join("\n").slice(0, 8e3)
|
|
4572
|
+
}
|
|
4573
|
+
],
|
|
4574
|
+
durationMs: Math.round(performance.now() - t0)
|
|
4575
|
+
};
|
|
4576
|
+
}
|
|
4571
4577
|
}
|
|
4572
4578
|
}
|
|
4573
4579
|
const total = await sumGlobBytes(cwd, cfg.measureGlobs);
|
|
@@ -4575,6 +4581,7 @@ async function runBundle(cwd, config, stack) {
|
|
|
4575
4581
|
return {
|
|
4576
4582
|
checkId: "bundle",
|
|
4577
4583
|
findings: [
|
|
4584
|
+
...preFindings,
|
|
4578
4585
|
{
|
|
4579
4586
|
id: "bundle-empty",
|
|
4580
4587
|
severity: "info",
|
|
@@ -4584,9 +4591,9 @@ async function runBundle(cwd, config, stack) {
|
|
|
4584
4591
|
durationMs: Math.round(performance.now() - t0)
|
|
4585
4592
|
};
|
|
4586
4593
|
}
|
|
4587
|
-
const baseRef = await gitOkQuick(cwd) ? await resolveDiffBaseRef(cwd,
|
|
4594
|
+
const baseRef = await gitOkQuick(cwd) ? await resolveDiffBaseRef(cwd, cfg.baselineRef) : null;
|
|
4588
4595
|
const baseline = await readBaseline(cwd, cfg.baselinePath, baseRef);
|
|
4589
|
-
const findings = [];
|
|
4596
|
+
const findings = [...preFindings];
|
|
4590
4597
|
const infoLines = [
|
|
4591
4598
|
`Measured ${total} bytes (${(total / 1024 / 1024).toFixed(2)} MiB)`,
|
|
4592
4599
|
baseline ? `Baseline from \`${cfg.baselinePath}\`: ${baseline.totalBytes} bytes` : `No baseline at \`${cfg.baselinePath}\` (commit a baseline JSON to compare)`
|
|
@@ -5065,8 +5072,8 @@ function buildHtmlReport(p2) {
|
|
|
5065
5072
|
} else {
|
|
5066
5073
|
for (const cid of checkOrder) {
|
|
5067
5074
|
const group = sortFindings(cwd, byCheck.get(cid));
|
|
5068
|
-
|
|
5069
|
-
warningsHtml +=
|
|
5075
|
+
const cards = group.map(({ r: r4, f: f4 }) => renderFindingCard(cwd, r4, f4)).join("\n");
|
|
5076
|
+
warningsHtml += `<details class="warn-check"><summary>${escapeHtml(cid)} <span class="count">(${group.length})</span></summary><div class="details-body warn-check-body">${cards}</div></details>`;
|
|
5070
5077
|
}
|
|
5071
5078
|
}
|
|
5072
5079
|
const infoHtml = infoItems.length === 0 ? '<p class="muted">No info notes.</p>' : infoItems.map(({ r: r4, f: f4 }) => renderFindingCard(cwd, r4, f4)).join("\n");
|
|
@@ -5100,14 +5107,16 @@ function buildHtmlReport(p2) {
|
|
|
5100
5107
|
}
|
|
5101
5108
|
h1 { font-size: 1.5rem; margin: 0 0 1rem; letter-spacing: -0.02em; }
|
|
5102
5109
|
h2 { font-size: 1.15rem; margin: 2rem 0 0.75rem; color: var(--accent); border-bottom: 1px solid var(--border); padding-bottom: 0.35rem; }
|
|
5103
|
-
|
|
5104
|
-
|
|
5110
|
+
details.warn-check { margin-bottom: 0.45rem; }
|
|
5111
|
+
details.warn-check:last-child { margin-bottom: 0; }
|
|
5112
|
+
details.warn-check > summary { color: var(--warn); font-size: 0.95rem; }
|
|
5113
|
+
details.warn-check .count { color: var(--muted); font-weight: normal; }
|
|
5114
|
+
.warn-check-body { padding-top: 0.35rem; }
|
|
5105
5115
|
h4 { font-size: 0.95rem; margin: 0 0 0.5rem; font-weight: 600; }
|
|
5106
5116
|
h5 { font-size: 0.8rem; margin: 0 0 0.35rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--accent); }
|
|
5107
5117
|
.badges { margin-bottom: 1.25rem; display: flex; flex-wrap: wrap; gap: 0.35rem; align-items: center; }
|
|
5108
5118
|
.badge { height: 28px; width: auto; max-width: 100%; image-rendering: crisp-edges; }
|
|
5109
5119
|
details { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; margin-bottom: 0.75rem; }
|
|
5110
|
-
details.open-default { border-color: #3d4f6a; }
|
|
5111
5120
|
summary {
|
|
5112
5121
|
cursor: pointer; padding: 0.65rem 1rem; font-weight: 600;
|
|
5113
5122
|
list-style: none; display: flex; align-items: center; gap: 0.5rem;
|
|
@@ -5172,12 +5181,12 @@ function buildHtmlReport(p2) {
|
|
|
5172
5181
|
<tbody>${checkRows}</tbody>
|
|
5173
5182
|
</table>
|
|
5174
5183
|
|
|
5175
|
-
<details
|
|
5184
|
+
<details>
|
|
5176
5185
|
<summary>Blocking (${blocks})</summary>
|
|
5177
5186
|
<div class="details-body">${blockingHtml}</div>
|
|
5178
5187
|
</details>
|
|
5179
5188
|
|
|
5180
|
-
<details
|
|
5189
|
+
<details>
|
|
5181
5190
|
<summary>Warnings (${warns})</summary>
|
|
5182
5191
|
<div class="details-body">${warningsHtml}</div>
|
|
5183
5192
|
</details>
|
|
@@ -5592,6 +5601,17 @@ function formatConsole(p2) {
|
|
|
5592
5601
|
return lines.join("\n");
|
|
5593
5602
|
}
|
|
5594
5603
|
|
|
5604
|
+
// src/lib/http-fetch.ts
|
|
5605
|
+
function getFetch() {
|
|
5606
|
+
const f4 = globalThis.fetch;
|
|
5607
|
+
if (typeof f4 !== "function") {
|
|
5608
|
+
throw new Error(
|
|
5609
|
+
"FrontGuard needs Node.js 18+ (global fetch). Use NODE_VERSION / image >= 18 in CI."
|
|
5610
|
+
);
|
|
5611
|
+
}
|
|
5612
|
+
return f4;
|
|
5613
|
+
}
|
|
5614
|
+
|
|
5595
5615
|
// src/llm/ollama.ts
|
|
5596
5616
|
async function callOllamaChat(opts) {
|
|
5597
5617
|
const fetch = getFetch();
|
|
@@ -5929,7 +5949,7 @@ async function runFrontGuard(opts) {
|
|
|
5929
5949
|
const config = await loadConfig(opts.cwd);
|
|
5930
5950
|
const mode = opts.enforce ? "enforce" : config.mode;
|
|
5931
5951
|
const stack = await detectStack(opts.cwd);
|
|
5932
|
-
const pr =
|
|
5952
|
+
const pr = readPrContext(opts.cwd);
|
|
5933
5953
|
const restrictFiles = pr?.files?.length ? pr.files : null;
|
|
5934
5954
|
const [
|
|
5935
5955
|
eslint,
|
|
@@ -6018,9 +6038,6 @@ FrontGuard: wrote Bitbucket PR comment text to ${abs} (${snippet.length} bytes).
|
|
|
6018
6038
|
g.stdout.write(report.consoleText + "\n\n");
|
|
6019
6039
|
g.stdout.write(report.markdown + "\n");
|
|
6020
6040
|
}
|
|
6021
|
-
if (opts.ci && g.env.GITHUB_TOKEN) {
|
|
6022
|
-
await upsertBriefComment(report.markdown);
|
|
6023
|
-
}
|
|
6024
6041
|
const hasBlock = results.some((r4) => r4.findings.some((f4) => f4.severity === "block"));
|
|
6025
6042
|
g.exitCode = mode === "enforce" && hasBlock ? 1 : 0;
|
|
6026
6043
|
}
|
|
@@ -6029,13 +6046,13 @@ FrontGuard: wrote Bitbucket PR comment text to ${abs} (${snippet.length} bytes).
|
|
|
6029
6046
|
var init2 = defineCommand({
|
|
6030
6047
|
meta: {
|
|
6031
6048
|
name: "init",
|
|
6032
|
-
description: "Add
|
|
6049
|
+
description: "Add Bitbucket pipeline example, pull_request_template.md, and frontguard.config.js"
|
|
6033
6050
|
},
|
|
6034
6051
|
run: async () => {
|
|
6035
6052
|
const cwd = g.cwd();
|
|
6036
6053
|
await initFrontGuard(cwd);
|
|
6037
6054
|
g.stdout.write(
|
|
6038
|
-
"FrontGuard initialized.\n\
|
|
6055
|
+
"FrontGuard initialized for Bitbucket.\n\n \u2022 bitbucket-pipelines.frontguard.example.yml \u2014 merge into your pipeline\n \u2022 pull_request_template.md \u2014 PR description template (Bitbucket Cloud)\n \u2022 frontguard.config.js (if missing)\n\nAdd the package as a devDependency so CI matches local runs:\n npm install -D @cleartrip/frontguard\n yarn add -D @cleartrip/frontguard\n"
|
|
6039
6056
|
);
|
|
6040
6057
|
}
|
|
6041
6058
|
});
|
|
@@ -6045,11 +6062,6 @@ var run = defineCommand({
|
|
|
6045
6062
|
description: "Run checks and print the review brief"
|
|
6046
6063
|
},
|
|
6047
6064
|
args: {
|
|
6048
|
-
ci: {
|
|
6049
|
-
type: "boolean",
|
|
6050
|
-
description: "Upsert PR comment when GITHUB_TOKEN is available",
|
|
6051
|
-
default: false
|
|
6052
|
-
},
|
|
6053
6065
|
markdown: {
|
|
6054
6066
|
type: "boolean",
|
|
6055
6067
|
description: "Print markdown only",
|
|
@@ -6076,7 +6088,6 @@ var run = defineCommand({
|
|
|
6076
6088
|
run: async ({ args }) => {
|
|
6077
6089
|
await runFrontGuard({
|
|
6078
6090
|
cwd: g.cwd(),
|
|
6079
|
-
ci: Boolean(args.ci),
|
|
6080
6091
|
markdown: Boolean(args.markdown),
|
|
6081
6092
|
enforce: Boolean(args.enforce),
|
|
6082
6093
|
append: typeof args.append === "string" ? args.append : null,
|