@cleartrip/frontguard 0.3.3 → 0.3.4

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
@@ -61,8 +61,8 @@ The PR comment includes:
61
61
 
62
62
  | Check | What it does | Default |
63
63
  |-------|-------------|---------|
64
- | **eslint** | Runs ESLint with your project config | Enabled |
65
- | **prettier** | Verifies Prettier formatting | Enabled |
64
+ | **eslint** | Runs ESLint with your project config (skips `frontguard.config.*` by default) | Enabled |
65
+ | **prettier** | Verifies Prettier formatting (skips `frontguard.config.*` by default) | Enabled |
66
66
  | **typescript** | Runs `tsc --noEmit` for type errors | Enabled |
67
67
  | **secrets** | Scans for leaked tokens, keys, passwords | Enabled |
68
68
  | **pr-hygiene** | Validates PR description, sections, AI disclosure | Enabled |
@@ -168,6 +168,14 @@ export default defineConfig({
168
168
 
169
169
  Run `frontguard init` to generate a config file with every option documented as comments.
170
170
 
171
+ ### ESLint / Prettier vs `frontguard.config.*`
172
+
173
+ Defaults exclude `frontguard.config.mjs`, `frontguard.config.js`, and `frontguard.config.cjs` via `checks.eslint.ignorePatterns` and `checks.prettier.ignorePatterns` (picomatch globs, repo-relative). That keeps tool config from being judged by product rules. Set `ignorePatterns: []` on either check if you want those files linted/formatted. To extend the list in shared config, spread `DEFAULT_FRONTGUARD_CONFIG_IGNORE_PATTERNS` from `@cleartrip/frontguard`.
174
+
175
+ ### Bitbucket PR comment table
176
+
177
+ The short PR comment lists **only checks that reported findings**. Skipped checks stay out of that table; the full HTML report still shows every check, including skips.
178
+
171
179
  ### Shared Org Config
172
180
 
173
181
  Publish a shared config package and extend it:
package/dist/cli.js CHANGED
@@ -8,8 +8,9 @@ import path6, { sep, normalize, delimiter, resolve, dirname } from 'path';
8
8
  import fs from 'fs/promises';
9
9
  import { execFileSync, spawn } from 'child_process';
10
10
  import { createRequire } from 'module';
11
- import fs2 from 'fs';
11
+ import fs3 from 'fs';
12
12
  import { pathToFileURL } from 'url';
13
+ import picomatch from 'picomatch';
13
14
  import fg from 'fast-glob';
14
15
  import { pipeline } from 'stream/promises';
15
16
  import { PassThrough } from 'stream';
@@ -2430,16 +2431,28 @@ export default defineConfig({
2430
2431
  checks: {
2431
2432
  // \u2500\u2500\u2500 ESLint \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\u2500\u2500\u2500\u2500\u2500
2432
2433
  // Runs ESLint using your project config on PR-scoped files.
2434
+ // Default ignores frontguard.config.{mjs,js,cjs} (tool config, not app code).
2433
2435
  // eslint: {
2434
2436
  // enabled: true,
2435
- // glob: '**/*.{js,cjs,mjs,jsx,ts,tsx}', // files to lint
2437
+ // glob: '**/*.{js,cjs,mjs,jsx,ts,tsx}',
2438
+ // ignorePatterns: [
2439
+ // '**/frontguard.config.mjs',
2440
+ // '**/frontguard.config.js',
2441
+ // '**/frontguard.config.cjs',
2442
+ // ],
2436
2443
  // },
2437
2444
 
2438
2445
  // \u2500\u2500\u2500 Prettier \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\u2500\u2500\u2500
2439
2446
  // Checks that files match Prettier formatting.
2447
+ // Default ignores the same FrontGuard config filenames as ESLint.
2440
2448
  // prettier: {
2441
2449
  // enabled: true,
2442
2450
  // glob: '**/*.{js,cjs,mjs,jsx,ts,tsx,json,md,css,scss,yml,yaml}',
2451
+ // ignorePatterns: [
2452
+ // '**/frontguard.config.mjs',
2453
+ // '**/frontguard.config.js',
2454
+ // '**/frontguard.config.cjs',
2455
+ // ],
2443
2456
  // },
2444
2457
 
2445
2458
  // \u2500\u2500\u2500 TypeScript \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\u2500
@@ -2837,18 +2850,13 @@ function mdTableCell(s3) {
2837
2850
  return s3.replace(/\|/g, "\\|").replace(/\r?\n/g, " ").trim();
2838
2851
  }
2839
2852
  function statusForPrTable(r4) {
2840
- if (r4.skipped) {
2841
- const t3 = r4.skipped.replace(/\s+/g, " ").trim();
2842
- const short = t3.length > 120 ? `${t3.slice(0, 117)}\u2026` : t3;
2843
- return `Skipped \u2014 ${short}`;
2844
- }
2845
2853
  const n3 = r4.findings.length;
2846
2854
  const blocks = r4.findings.filter((f4) => f4.severity === "block").length;
2847
2855
  if (blocks > 0) return `${n3} issue(s), ${blocks} blocking`;
2848
2856
  return `${n3} issue(s)`;
2849
2857
  }
2850
2858
  function checkNeedsRow(r4) {
2851
- return Boolean(r4.skipped) || r4.findings.length > 0;
2859
+ return !r4.skipped && r4.findings.length > 0;
2852
2860
  }
2853
2861
  function formatBitbucketPrSnippet(report) {
2854
2862
  const publicReport = process.env.FRONTGUARD_PUBLIC_REPORT_URL?.trim();
@@ -3067,16 +3075,99 @@ function createDefu2(merger) {
3067
3075
  );
3068
3076
  }
3069
3077
  var defu2 = createDefu2();
3078
+ function normalizePrPath(p2) {
3079
+ return p2.replace(/\\/g, "/").replace(/^\.\//, "");
3080
+ }
3081
+ function hasPrFileList(pr) {
3082
+ return Boolean(pr?.files?.length);
3083
+ }
3084
+ function prPathSet(pr) {
3085
+ if (!hasPrFileList(pr)) return null;
3086
+ return new Set(pr.files.map(normalizePrPath));
3087
+ }
3088
+ function isPathInPrScope(relOrAbs, cwd, prSet) {
3089
+ const raw = relOrAbs.replace(/^file:\/\//, "");
3090
+ let rel = normalizePrPath(raw);
3091
+ if (path6.isAbsolute(raw)) {
3092
+ rel = normalizePrPath(path6.relative(cwd, raw));
3093
+ }
3094
+ if (prSet.has(rel)) return true;
3095
+ const trimmed = rel.replace(/^\.\//, "");
3096
+ if (prSet.has(trimmed)) return true;
3097
+ return false;
3098
+ }
3099
+ var ESLINT_EXT = /\.(js|cjs|mjs|jsx|ts|tsx)$/i;
3100
+ var PRETTIER_EXT = /\.(js|cjs|mjs|jsx|ts|tsx|json|md|css|scss|sass|less|yml|yaml|vue|svelte)$/i;
3101
+ var CYCLES_EXT = /\.(tsx?|jsx?|mjs|cjs|js)$/i;
3102
+ function filterPrFilesForEslint(pr) {
3103
+ if (!hasPrFileList(pr)) return null;
3104
+ return pr.files.map(normalizePrPath).filter((f4) => ESLINT_EXT.test(f4));
3105
+ }
3106
+ function filterPrFilesForPrettier(pr) {
3107
+ if (!hasPrFileList(pr)) return null;
3108
+ return pr.files.map(normalizePrPath).filter((f4) => PRETTIER_EXT.test(f4));
3109
+ }
3110
+ function filterPrFilesForMadge(pr) {
3111
+ if (!hasPrFileList(pr)) return null;
3112
+ return pr.files.map(normalizePrPath).filter((f4) => CYCLES_EXT.test(f4));
3113
+ }
3114
+ async function existingRepoPaths(cwd, rels) {
3115
+ const out = [];
3116
+ for (const rel of rels) {
3117
+ try {
3118
+ await fs.access(path6.join(cwd, rel));
3119
+ out.push(rel);
3120
+ } catch {
3121
+ }
3122
+ }
3123
+ return out;
3124
+ }
3125
+ function filterTscOutputToPrFiles(output, cwd, prSet) {
3126
+ const lines = output.split("\n");
3127
+ const kept = [];
3128
+ for (const line of lines) {
3129
+ const m3 = /^(.+?)\(\d+,\d+\):\s/.exec(line);
3130
+ if (m3?.[1]) {
3131
+ if (isPathInPrScope(m3[1], cwd, prSet)) kept.push(line);
3132
+ continue;
3133
+ }
3134
+ }
3135
+ return kept.join("\n").trim();
3136
+ }
3137
+
3138
+ // src/lib/glob-ignore.ts
3139
+ var DEFAULT_FRONTGUARD_CONFIG_IGNORE_PATTERNS = [
3140
+ "**/frontguard.config.mjs",
3141
+ "**/frontguard.config.js",
3142
+ "**/frontguard.config.cjs"
3143
+ ];
3144
+ function pathMatchesIgnorePatterns(relPath, patterns) {
3145
+ if (patterns.length === 0) return false;
3146
+ const posixRel = normalizePrPath(relPath);
3147
+ for (const pattern of patterns) {
3148
+ const pm = picomatch(pattern, { dot: true });
3149
+ if (pm(posixRel) || pm(`./${posixRel}`)) return true;
3150
+ }
3151
+ return false;
3152
+ }
3153
+ function negatedPrettierGlobs(patterns) {
3154
+ return patterns.map((p2) => p2.startsWith("!") ? p2 : `!${p2}`);
3155
+ }
3070
3156
 
3071
3157
  // src/config/defaults.ts
3072
3158
  var defaultConfig = {
3073
3159
  mode: "warn",
3074
3160
  rules: {},
3075
3161
  checks: {
3076
- eslint: { enabled: true, glob: "**/*.{js,cjs,mjs,jsx,ts,tsx}" },
3162
+ eslint: {
3163
+ enabled: true,
3164
+ glob: "**/*.{js,cjs,mjs,jsx,ts,tsx}",
3165
+ ignorePatterns: [...DEFAULT_FRONTGUARD_CONFIG_IGNORE_PATTERNS]
3166
+ },
3077
3167
  prettier: {
3078
3168
  enabled: true,
3079
- glob: "**/*.{js,cjs,mjs,jsx,ts,tsx,json,md,css,scss,yml,yaml}"
3169
+ glob: "**/*.{js,cjs,mjs,jsx,ts,tsx,json,md,css,scss,yml,yaml}",
3170
+ ignorePatterns: [...DEFAULT_FRONTGUARD_CONFIG_IGNORE_PATTERNS]
3080
3171
  },
3081
3172
  typescript: { enabled: true },
3082
3173
  secrets: { enabled: true },
@@ -3203,7 +3294,7 @@ async function loadConfig(cwd) {
3203
3294
  const loadErrors = [];
3204
3295
  for (const name of CONFIG_NAMES) {
3205
3296
  const full = path6.join(cwd, name);
3206
- if (!fs2.existsSync(full)) continue;
3297
+ if (!fs3.existsSync(full)) continue;
3207
3298
  try {
3208
3299
  const mod = await importConfig(full);
3209
3300
  userFile = normalizeExport(mod);
@@ -3374,65 +3465,6 @@ function stripFrontmatter(content) {
3374
3465
  if (endIdx === -1) return content;
3375
3466
  return content.slice(endIdx + 3).trim();
3376
3467
  }
3377
- function normalizePrPath(p2) {
3378
- return p2.replace(/\\/g, "/").replace(/^\.\//, "");
3379
- }
3380
- function hasPrFileList(pr) {
3381
- return Boolean(pr?.files?.length);
3382
- }
3383
- function prPathSet(pr) {
3384
- if (!hasPrFileList(pr)) return null;
3385
- return new Set(pr.files.map(normalizePrPath));
3386
- }
3387
- function isPathInPrScope(relOrAbs, cwd, prSet) {
3388
- const raw = relOrAbs.replace(/^file:\/\//, "");
3389
- let rel = normalizePrPath(raw);
3390
- if (path6.isAbsolute(raw)) {
3391
- rel = normalizePrPath(path6.relative(cwd, raw));
3392
- }
3393
- if (prSet.has(rel)) return true;
3394
- const trimmed = rel.replace(/^\.\//, "");
3395
- if (prSet.has(trimmed)) return true;
3396
- return false;
3397
- }
3398
- var ESLINT_EXT = /\.(js|cjs|mjs|jsx|ts|tsx)$/i;
3399
- var PRETTIER_EXT = /\.(js|cjs|mjs|jsx|ts|tsx|json|md|css|scss|sass|less|yml|yaml|vue|svelte)$/i;
3400
- var CYCLES_EXT = /\.(tsx?|jsx?|mjs|cjs|js)$/i;
3401
- function filterPrFilesForEslint(pr) {
3402
- if (!hasPrFileList(pr)) return null;
3403
- return pr.files.map(normalizePrPath).filter((f4) => ESLINT_EXT.test(f4));
3404
- }
3405
- function filterPrFilesForPrettier(pr) {
3406
- if (!hasPrFileList(pr)) return null;
3407
- return pr.files.map(normalizePrPath).filter((f4) => PRETTIER_EXT.test(f4));
3408
- }
3409
- function filterPrFilesForMadge(pr) {
3410
- if (!hasPrFileList(pr)) return null;
3411
- return pr.files.map(normalizePrPath).filter((f4) => CYCLES_EXT.test(f4));
3412
- }
3413
- async function existingRepoPaths(cwd, rels) {
3414
- const out = [];
3415
- for (const rel of rels) {
3416
- try {
3417
- await fs.access(path6.join(cwd, rel));
3418
- out.push(rel);
3419
- } catch {
3420
- }
3421
- }
3422
- return out;
3423
- }
3424
- function filterTscOutputToPrFiles(output, cwd, prSet) {
3425
- const lines = output.split("\n");
3426
- const kept = [];
3427
- for (const line of lines) {
3428
- const m3 = /^(.+?)\(\d+,\d+\):\s/.exec(line);
3429
- if (m3?.[1]) {
3430
- if (isPathInPrScope(m3[1], cwd, prSet)) kept.push(line);
3431
- continue;
3432
- }
3433
- }
3434
- return kept.join("\n").trim();
3435
- }
3436
3468
  function stripFileUrl(p2) {
3437
3469
  let s3 = p2.trim();
3438
3470
  if (!/^file:/i.test(s3)) return s3;
@@ -4230,6 +4262,8 @@ async function runEslint(cwd, config, _stack, pr) {
4230
4262
  };
4231
4263
  }
4232
4264
  const glob = config.checks.eslint.glob ?? "**/*.{js,cjs,mjs,jsx,ts,tsx}";
4265
+ const ignorePatterns = config.checks.eslint.ignorePatterns ?? DEFAULT_FRONTGUARD_CONFIG_IGNORE_PATTERNS;
4266
+ const ignoreArgs = ignorePatterns.flatMap((p2) => ["--ignore-pattern", p2]);
4233
4267
  const prPaths = filterPrFilesForEslint(pr);
4234
4268
  let exitCode;
4235
4269
  let stdout2;
@@ -4244,7 +4278,8 @@ async function runEslint(cwd, config, _stack, pr) {
4244
4278
  };
4245
4279
  }
4246
4280
  const existing = await existingRepoPaths(cwd, prPaths);
4247
- if (existing.length === 0) {
4281
+ const lintable = existing.filter((p2) => !pathMatchesIgnorePatterns(p2, ignorePatterns));
4282
+ if (lintable.length === 0) {
4248
4283
  return {
4249
4284
  checkId: "eslint",
4250
4285
  findings: [],
@@ -4252,10 +4287,11 @@ async function runEslint(cwd, config, _stack, pr) {
4252
4287
  skipped: hasPrFileList(pr) ? "no lintable files in PR diff (added/modified only)" : "no files"
4253
4288
  };
4254
4289
  }
4255
- ({ exitCode, stdout: stdout2, stderr } = await runEslintOnPaths(cwd, existing));
4290
+ ({ exitCode, stdout: stdout2, stderr } = await runEslintOnPaths(cwd, lintable));
4256
4291
  } else {
4257
4292
  ({ exitCode, stdout: stdout2, stderr } = await runNpmBinary(cwd, "eslint", [
4258
4293
  glob,
4294
+ ...ignoreArgs,
4259
4295
  "--max-warnings",
4260
4296
  "0",
4261
4297
  "--no-error-on-unmatched-pattern",
@@ -4357,6 +4393,8 @@ async function runPrettier(cwd, config, pr) {
4357
4393
  };
4358
4394
  }
4359
4395
  const glob = config.checks.prettier.glob ?? "**/*.{js,cjs,mjs,jsx,ts,tsx,json,md,css,scss,yml,yaml}";
4396
+ const ignorePatterns = config.checks.prettier.ignorePatterns ?? DEFAULT_FRONTGUARD_CONFIG_IGNORE_PATTERNS;
4397
+ const prettierNegations = negatedPrettierGlobs(ignorePatterns);
4360
4398
  let exitCode;
4361
4399
  let stdout2;
4362
4400
  let stderr;
@@ -4371,7 +4409,8 @@ async function runPrettier(cwd, config, pr) {
4371
4409
  };
4372
4410
  }
4373
4411
  const existing = await existingRepoPaths(cwd, prPaths);
4374
- if (existing.length === 0) {
4412
+ const formattable = existing.filter((p2) => !pathMatchesIgnorePatterns(p2, ignorePatterns));
4413
+ if (formattable.length === 0) {
4375
4414
  return {
4376
4415
  checkId: "prettier",
4377
4416
  findings: [],
@@ -4379,12 +4418,13 @@ async function runPrettier(cwd, config, pr) {
4379
4418
  skipped: hasPrFileList(pr) ? "no formattable files in PR diff" : "no files"
4380
4419
  };
4381
4420
  }
4382
- ({ exitCode, stdout: stdout2, stderr } = await runPrettierOnPaths(cwd, existing));
4421
+ ({ exitCode, stdout: stdout2, stderr } = await runPrettierOnPaths(cwd, formattable));
4383
4422
  } else {
4384
4423
  ({ exitCode, stdout: stdout2, stderr } = await runNpmBinary(cwd, "prettier", [
4385
4424
  "--check",
4425
+ "--ignore-unknown",
4386
4426
  glob,
4387
- "--ignore-unknown"
4427
+ ...prettierNegations
4388
4428
  ]));
4389
4429
  }
4390
4430
  const findings = [];