@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 +23 -5
- package/dist/cli.js +74 -16
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
|
2772
|
-
|
|
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.
|
|
2782
|
+
actions.push("created frontguard.config.mjs");
|
|
2775
2783
|
} else {
|
|
2776
|
-
actions.push("frontguard
|
|
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: "
|
|
5153
|
-
detail:
|
|
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: `
|
|
5160
|
-
detail:
|
|
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:
|
|
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.
|
|
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
|
});
|