@cleartrip/frontguard 0.3.1 → 0.3.3

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
@@ -27,7 +27,7 @@ npx frontguard init
27
27
 
28
28
  This does three things:
29
29
 
30
- - **Creates `frontguard.config.js`** with all available options as commented examples
30
+ - **Creates `frontguard.config.mjs`** (ESM) with all available options as commented examples — works even when `package.json` is not `"type": "module"`
31
31
  - **Creates `pull_request_template.md`** with AI disclosure checkboxes
32
32
  - **Merges the FrontGuard step** into your existing `bitbucket-pipelines.yml` (or creates one if missing)
33
33
 
@@ -35,7 +35,7 @@ This does three things:
35
35
 
36
36
  ### 3. Configure
37
37
 
38
- Open `frontguard.config.js` and uncomment the checks you want. Every option is documented inline.
38
+ Open `frontguard.config.mjs` and uncomment the checks you want. Every option is documented inline.
39
39
 
40
40
  ### 4. Set Up CI
41
41
 
@@ -95,7 +95,7 @@ The bundle check supports multiple strategies for extracting the size metric:
95
95
 
96
96
  **Next.js (auto-detected):**
97
97
  ```js
98
- // frontguard.config.js — no strategy override needed
98
+ // frontguard.config.mjs — no strategy override needed
99
99
  bundle: {
100
100
  buildCommand: 'yarn build',
101
101
  maxDeltaBytes: 50_000,
@@ -122,7 +122,25 @@ Commit this to your base branch. FrontGuard compares the current build against i
122
122
 
123
123
  ## Configuration
124
124
 
125
- All configuration lives in `frontguard.config.js` at your project root:
125
+ FrontGuard loads the first file that exists, in order: **`frontguard.config.mjs`** **`frontguard.config.cjs`** **`frontguard.config.js`**.
126
+
127
+ - **`.mjs`** — ESM (`import` / `export default`). Use this in CommonJS-only repos (no `"type": "module"` needed). **Recommended.**
128
+ - **`.cjs`** — CommonJS without importing the package (plain object merges like `defineConfig`):
129
+
130
+ ```js
131
+ // frontguard.config.cjs
132
+ module.exports = {
133
+ checks: {
134
+ bundle: { buildCommand: 'yarn build:prod', bundleSizeStrategy: 'next' },
135
+ },
136
+ }
137
+ ```
138
+
139
+ - **`.js`** — Only reliable as ESM if your `package.json` has `"type": "module"`; otherwise Node may refuse to load it (you will see an ESM warning and defaults will apply).
140
+
141
+ **CI / monorepos:** Run `frontguard` with `cwd` set to the app root that contains `package.json` and the config file. **Git:** If you see `fatal: Needed a single revision`, increase clone depth or fetch the PR destination branch (e.g. `git fetch origin main:refs/remotes/origin/main`) so diff and baseline reads can resolve `origin/<branch>`.
142
+
143
+ All configuration lives in one of those files at your project root (same directory you run `frontguard` from):
126
144
 
127
145
  ```js
128
146
  import { defineConfig } from '@cleartrip/frontguard'
@@ -235,7 +253,7 @@ These features are **implemented in code but turned off by default** so day-to-d
235
253
 
236
254
  When you are ready to try them:
237
255
 
238
- 1. Set `checks.aiAssistedReview.enabled: true` for static heuristics on `@frontguard-ai` regions or AI-disclosed PRs (see `frontguard.config.js` comments).
256
+ 1. Set `checks.aiAssistedReview.enabled: true` for static heuristics on `@frontguard-ai` regions or AI-disclosed PRs (see `frontguard.config.mjs` comments).
239
257
  2. Set `checks.llm.enabled: true` and choose `provider: 'ollama' | 'openai' | 'anthropic'` for automated review text.
240
258
 
241
259
  **PR template:** The init flow still adds an **AI disclosure** section to `pull_request_template.md` for human reviewers. With AI-assisted review off, that disclosure is recorded as an info finding and does not enable extra static checks until you opt in.
package/dist/cli.js CHANGED
@@ -2768,12 +2768,20 @@ function findEndOfLastSection(lines, pipelinesIdx) {
2768
2768
  }
2769
2769
  async function initFrontGuard(cwd) {
2770
2770
  const actions = [];
2771
- const cfgPath = path6.join(cwd, "frontguard.config.js");
2772
- if (!await fileExists(cfgPath)) {
2771
+ const cfgCandidates = [
2772
+ "frontguard.config.mjs",
2773
+ "frontguard.config.js",
2774
+ "frontguard.config.cjs"
2775
+ ];
2776
+ const cfgPath = path6.join(cwd, "frontguard.config.mjs");
2777
+ const hasAny = await Promise.all(
2778
+ cfgCandidates.map((n3) => fileExists(path6.join(cwd, n3)))
2779
+ ).then((xs) => xs.some(Boolean));
2780
+ if (!hasAny) {
2773
2781
  await fs.writeFile(cfgPath, CONFIG_TEMPLATE, "utf8");
2774
- actions.push("created frontguard.config.js");
2782
+ actions.push("created frontguard.config.mjs");
2775
2783
  } else {
2776
- actions.push("frontguard.config.js already exists (skipped)");
2784
+ actions.push("frontguard config already exists (.mjs / .js / .cjs) \u2014 skipped");
2777
2785
  }
2778
2786
  const prTplPath = path6.join(cwd, "pull_request_template.md");
2779
2787
  if (!await fileExists(prTplPath)) {
@@ -3154,9 +3162,9 @@ function migrateLegacyConfigKeys(config) {
3154
3162
 
3155
3163
  // src/config/load.ts
3156
3164
  var CONFIG_NAMES = [
3157
- "frontguard.config.js",
3158
3165
  "frontguard.config.mjs",
3159
- "frontguard.config.cjs"
3166
+ "frontguard.config.cjs",
3167
+ "frontguard.config.js"
3160
3168
  ];
3161
3169
  async function importConfig(absolutePath) {
3162
3170
  const url = pathToFileURL(absolutePath).href;
@@ -3192,6 +3200,7 @@ async function loadExtendsLayer(cwd, spec) {
3192
3200
  }
3193
3201
  async function loadConfig(cwd) {
3194
3202
  let userFile = null;
3203
+ const loadErrors = [];
3195
3204
  for (const name of CONFIG_NAMES) {
3196
3205
  const full = path6.join(cwd, name);
3197
3206
  if (!fs2.existsSync(full)) continue;
@@ -3199,7 +3208,9 @@ async function loadConfig(cwd) {
3199
3208
  const mod = await importConfig(full);
3200
3209
  userFile = normalizeExport(mod);
3201
3210
  break;
3202
- } catch {
3211
+ } catch (e3) {
3212
+ const msg = e3 instanceof Error ? e3.message : String(e3);
3213
+ loadErrors.push(`${name}: ${msg}`);
3203
3214
  continue;
3204
3215
  }
3205
3216
  }
@@ -3208,6 +3219,21 @@ async function loadConfig(cwd) {
3208
3219
  migrateLegacyConfigKeys(orgLayer);
3209
3220
  if (userFile) migrateLegacyConfigKeys(userFile);
3210
3221
  const user = userFile ? stripExtends(userFile) : {};
3222
+ if (!userFile) {
3223
+ if (loadErrors.length > 0) {
3224
+ g.stderr.write(
3225
+ `FrontGuard: config file(s) exist under ${path6.resolve(cwd)} but failed to load:
3226
+ ${loadErrors.map((l3) => ` \u2022 ${l3}`).join("\n")}
3227
+ Common fix: use frontguard.config.mjs (ESM, no package.json "type" required), or frontguard.config.cjs with require(). See README.
3228
+ `
3229
+ );
3230
+ } else {
3231
+ g.stderr.write(
3232
+ `FrontGuard: no ${CONFIG_NAMES.join(", ")} found under ${path6.resolve(cwd)} \u2014 using built-in defaults only (e.g. checks.bundle.buildCommand defaults to "npm run build"). Add a config file or run from the directory that contains it.
3233
+ `
3234
+ );
3235
+ }
3236
+ }
3211
3237
  const base = structuredClone(defaultConfig);
3212
3238
  const withOrg = defu2(orgLayer, base);
3213
3239
  return defu2(user, withOrg);
@@ -5149,15 +5175,29 @@ async function bundleBuildPrecheck(cwd, buildCommand) {
5149
5175
  } catch {
5150
5176
  return {
5151
5177
  run: false,
5152
- message: "Skipped bundle build \u2014 no readable package.json",
5153
- 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."
5178
+ message: "Bundle check: build not run (unreadable package.json)",
5179
+ detail: [
5180
+ "Front Guard did not start the build because package.json could not be read next to the project root.",
5181
+ "No bundle size was measured.",
5182
+ "",
5183
+ "Fix: ensure package.json exists, or set checks.bundle.runBuild to false to only measure files already on disk."
5184
+ ].join("\n")
5154
5185
  };
5155
5186
  }
5156
5187
  if (!scripts?.[script]) {
5188
+ const keys = scripts ? Object.keys(scripts).sort().join(", ") : "(none)";
5157
5189
  return {
5158
5190
  run: false,
5159
- message: `Skipped bundle build \u2014 no scripts.${script} in package.json`,
5160
- detail: "The bundle check runs a production build, then measures the output. Libraries and non-web repos often have no `build` script \u2014 set checks.bundle.runBuild to false, or set checks.bundle.buildCommand to whatever produces your artifacts."
5191
+ message: `Bundle check: build not run (missing scripts["${script}"])`,
5192
+ detail: [
5193
+ `Configured checks.bundle.buildCommand is: \`${buildCommand}\``,
5194
+ `That maps to package.json scripts["${script}"], which is not defined.`,
5195
+ `Existing script names: ${keys}`,
5196
+ "",
5197
+ "Front Guard did not run a production build, so no bundle size was measured (nothing to compare to a baseline).",
5198
+ "",
5199
+ "Fix: set buildCommand to an existing script (e.g. `npm run build:prod` / `yarn run prod:build`), add the missing script, or set checks.bundle.runBuild to false if artifacts are produced elsewhere."
5200
+ ].join("\n")
5161
5201
  };
5162
5202
  }
5163
5203
  return { run: true };
@@ -5184,6 +5224,7 @@ async function runBundle(cwd, config, stack) {
5184
5224
  const strategy = resolveStrategy(cfg.bundleSizeStrategy, stack);
5185
5225
  const preFindings = [];
5186
5226
  let buildStdout = "";
5227
+ let buildExecuted = false;
5187
5228
  if (cfg.runBuild) {
5188
5229
  const parts = tokenizeCommand(cfg.buildCommand);
5189
5230
  if (parts.length === 0) {
@@ -5225,6 +5266,7 @@ async function runBundle(cwd, config, stack) {
5225
5266
  };
5226
5267
  }
5227
5268
  buildStdout = (res.stdout ?? "") + "\n" + (res.stderr ?? "");
5269
+ buildExecuted = true;
5228
5270
  }
5229
5271
  }
5230
5272
  let sizeResult = null;
@@ -5275,6 +5317,23 @@ async function runBundle(cwd, config, stack) {
5275
5317
  const total = sizeResult?.bytes ?? 0;
5276
5318
  const sizeLabel = sizeResult?.label ?? `(no bundle output detected for strategy "${strategy}")`;
5277
5319
  if (total === 0) {
5320
+ const buildSkippedPrecheck = preFindings.some((f4) => f4.id === "bundle-build-skipped");
5321
+ if (buildSkippedPrecheck) {
5322
+ return {
5323
+ checkId: "bundle",
5324
+ findings: preFindings,
5325
+ durationMs: Math.round(performance.now() - t0)
5326
+ };
5327
+ }
5328
+ const emptyDetail = buildExecuted ? [
5329
+ `Strategy "${strategy}" did not find a size after the build finished.`,
5330
+ sizeLabel,
5331
+ "",
5332
+ "For Next.js, confirm `next build` still prints the line `First Load JS shared by all`, or that `.next/static/**/*.js` exists. You can try bundleSizeStrategy `glob` or `custom` if your setup differs."
5333
+ ].join("\n") : [
5334
+ sizeLabel,
5335
+ cfg.runBuild ? "No production build ran successfully before this measurement (unexpected)." : "checks.bundle.runBuild is false \u2014 only existing files on disk are measured. Run a build earlier in the pipeline, or set runBuild to true."
5336
+ ].join("\n");
5278
5337
  return {
5279
5338
  checkId: "bundle",
5280
5339
  findings: [
@@ -5282,9 +5341,8 @@ async function runBundle(cwd, config, stack) {
5282
5341
  {
5283
5342
  id: "bundle-empty",
5284
5343
  severity: "info",
5285
- message: `No bundle size detected (strategy: ${strategy})`,
5286
- detail: `${sizeLabel}
5287
- Ensure the build produces artifacts, or switch to a different bundleSizeStrategy.`
5344
+ message: buildExecuted ? `No bundle size extracted (strategy: ${strategy})` : `No bundle size detected (strategy: ${strategy})`,
5345
+ detail: emptyDetail
5288
5346
  }
5289
5347
  ],
5290
5348
  durationMs: Math.round(performance.now() - t0)
@@ -6859,7 +6917,7 @@ function formatMarkdown(p2) {
6859
6917
  );
6860
6918
  sb.push("");
6861
6919
  sb.push(
6862
- "_Configure checks in `frontguard.config.js` \xB7 [Shields.io](https://shields.io) badge images load in Bitbucket PR comments (HTML tags like `<details>` are not supported there)._"
6920
+ "_Configure checks in `frontguard.config.mjs` (or `.cjs` / `.js`) \xB7 [Shields.io](https://shields.io) badge images load in Bitbucket PR comments (HTML tags like `<details>` are not supported there)._"
6863
6921
  );
6864
6922
  return sb.join("\n");
6865
6923
  }
@@ -7387,7 +7445,7 @@ var init2 = defineCommand({
7387
7445
  `);
7388
7446
  }
7389
7447
  g.stdout.write(
7390
- "\nNext steps:\n 1. Review frontguard.config.js \u2014 uncomment and tune the checks you need\n 2. Review bitbucket-pipelines.yml \u2014 check the merged FrontGuard step\n 3. Set BITBUCKET_ACCESS_TOKEN as a secured variable in your repo\n 4. Install the package: yarn add -D @cleartrip/frontguard\n\n"
7448
+ "\nNext steps:\n 1. Review frontguard.config.mjs \u2014 uncomment and tune the checks you need\n 2. Review bitbucket-pipelines.yml \u2014 check the merged FrontGuard step\n 3. Set BITBUCKET_ACCESS_TOKEN as a secured variable in your repo\n 4. Install the package: yarn add -D @cleartrip/frontguard\n\n"
7391
7449
  );
7392
7450
  }
7393
7451
  });