@seybel/breakhound 1.0.2 → 1.0.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 +9 -2
- package/dist/cli.js +143 -29
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/schema.json +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -78,7 +78,13 @@ npx @seybel/breakhound --url https://staging.example.com # every route, on ano
|
|
|
78
78
|
| Angular | your route definitions, from `src/main.ts` on, with lazily loaded `loadChildren` routes under their path |
|
|
79
79
|
| Anything else | links followed from the home page (up to 50 pages, a few per section) |
|
|
80
80
|
|
|
81
|
-
**Dynamic routes** like `/blog/[slug]` are scanned once, using a real link found on your pages (routes that share a template share a layout).
|
|
81
|
+
**Dynamic routes** like `/blog/[slug]` are scanned once, using a real link found on your pages (routes that share a template share a layout). Routes below one reuse its values: once `/cohorts/css-101` is found, `/cohorts/[slug]/settings` is scanned as `/cohorts/css-101/settings`. If something is still missing, it's listed with a hint naming the parameters. Give them a real value once, by name, and every route that uses them is filled in:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
params: { slug: 'css-101', submissionId: '42' }
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Or give one route its own URL, which wins over `params`:
|
|
82
88
|
|
|
83
89
|
```ts
|
|
84
90
|
samples: { '/products/[id]': '/products/42' }
|
|
@@ -296,7 +302,8 @@ Running breakhound through `npx` without installing it, you get `breakhound.conf
|
|
|
296
302
|
| `baseUrl` | Scan this server instead of finding or starting the dev server |
|
|
297
303
|
| `routes` | Scan exactly these routes instead of discovering them |
|
|
298
304
|
| `include`, `exclude` | Route globs: `*` is one segment, `**` any number |
|
|
299
|
-
| `samples` | A real URL for
|
|
305
|
+
| `samples` | A real URL for a dynamic route |
|
|
306
|
+
| `params` | Real values for dynamic route parameters by name, for every route that uses them |
|
|
300
307
|
| `maxRoutes` | Most routes per run (default 100) |
|
|
301
308
|
| `server` | How to start the app: `{ command: 'npm run preview', port: 4173, timeout: 120 }` |
|
|
302
309
|
| `widths` | `{ min, max, step }` in CSS pixels |
|
package/dist/cli.js
CHANGED
|
@@ -1303,6 +1303,24 @@ function plural(n, word) {
|
|
|
1303
1303
|
function routeLabel(route) {
|
|
1304
1304
|
return route.route || new URL(route.url).pathname;
|
|
1305
1305
|
}
|
|
1306
|
+
function leftOutHint(routes) {
|
|
1307
|
+
const skipped = routes.filter((r) => r.status === "skipped");
|
|
1308
|
+
if (!skipped.length) return null;
|
|
1309
|
+
const names = /* @__PURE__ */ new Set();
|
|
1310
|
+
for (const r of skipped) {
|
|
1311
|
+
const named = r.error?.match(/nothing gives (.+) a value/)?.[1] ?? "";
|
|
1312
|
+
for (const m of named.matchAll(/\[([^\]]+)\]/g)) names.add(m[1]);
|
|
1313
|
+
}
|
|
1314
|
+
const key = (n) => /^[A-Za-z_$][\w$]*$/.test(n) ? n : `'${n}'`;
|
|
1315
|
+
if (names.size) return `params: { ${[...names].map((n) => `${key(n)}: '1'`).join(", ")} }`;
|
|
1316
|
+
const first = skipped[0].route;
|
|
1317
|
+
const example = first.replace(/\[\[?\.\.\.[^\]]+\]\]?/g, "a/b").replace(/\[\[?[^\]]+\]\]?/g, "1");
|
|
1318
|
+
return `samples: { '${first}': '${example}' }`;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// ../report/src/brand.ts
|
|
1322
|
+
var MARK_PNG = "data:image/png;base64,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";
|
|
1323
|
+
var WORDMARK_PNG = "data:image/png;base64,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";
|
|
1306
1324
|
|
|
1307
1325
|
// ../report/src/markdown.ts
|
|
1308
1326
|
var SEVERITY_ICON = { error: "\u2716", warning: "\u26A0", info: "\u2139" };
|
|
@@ -1364,7 +1382,7 @@ function renderSummary(result) {
|
|
|
1364
1382
|
[
|
|
1365
1383
|
"Routes left out",
|
|
1366
1384
|
result.routes.filter((r) => r.status === "redirect" || r.status === "skipped"),
|
|
1367
|
-
result.routes
|
|
1385
|
+
leftOutHint(result.routes) ? `Give dynamic routes real values in your config, e.g. \`${leftOutHint(result.routes)}\` (replace the 1s with real ones).` : void 0
|
|
1368
1386
|
]
|
|
1369
1387
|
];
|
|
1370
1388
|
for (const [title, routes, hint] of sections) {
|
|
@@ -1547,6 +1565,7 @@ function buildData(result, options) {
|
|
|
1547
1565
|
baseUrl: result.project.baseUrl
|
|
1548
1566
|
},
|
|
1549
1567
|
summary: result.summary,
|
|
1568
|
+
leftOutHint: leftOutHint(result.routes),
|
|
1550
1569
|
routes: result.routes.map((r) => ({
|
|
1551
1570
|
route: routeLabel(r),
|
|
1552
1571
|
url: r.url,
|
|
@@ -1586,7 +1605,7 @@ function renderHtml(result, options = {}) {
|
|
|
1586
1605
|
<body>
|
|
1587
1606
|
<header class="top">
|
|
1588
1607
|
<div class="wrap top-inner">
|
|
1589
|
-
<div class="brand"
|
|
1608
|
+
<div class="brand" role="img" aria-label="breakhound"><span class="mark"></span><span class="word"></span></div>
|
|
1590
1609
|
<div class="top-actions">
|
|
1591
1610
|
<button type="button" class="btn primary" id="copy-all">Copy fix prompt</button>
|
|
1592
1611
|
</div>
|
|
@@ -1657,8 +1676,10 @@ code, .mono { font-family: var(--mono); font-size: 13px; }
|
|
|
1657
1676
|
|
|
1658
1677
|
.top { position: sticky; top: 0; z-index: 10; background: color-mix(in srgb, var(--bg) 88%, transparent); backdrop-filter: blur(8px); border-bottom: 1px solid var(--border); }
|
|
1659
1678
|
.top-inner { display: flex; align-items: center; justify-content: space-between; gap: 12px; height: 56px; }
|
|
1660
|
-
.brand { display: flex; align-items: center; gap:
|
|
1661
|
-
.mark {
|
|
1679
|
+
.brand { display: flex; align-items: center; gap: 7px; color: var(--text); }
|
|
1680
|
+
.brand .mark, .brand .word { display: inline-block; background: currentColor; mask-position: center; mask-size: contain; mask-repeat: no-repeat; }
|
|
1681
|
+
.brand .mark { width: 27px; height: 20px; mask-image: url("${MARK_PNG}"); }
|
|
1682
|
+
.brand .word { width: 95px; height: 18px; mask-image: url("${WORDMARK_PNG}"); }
|
|
1662
1683
|
.btn { border: 1px solid var(--border); background: var(--surface); border-radius: 8px; padding: 7px 12px; cursor: pointer; white-space: nowrap; }
|
|
1663
1684
|
.btn:hover { border-color: var(--tick); }
|
|
1664
1685
|
.btn.primary { background: var(--accent); color: var(--accent-ink); border-color: transparent; font-weight: 600; }
|
|
@@ -1773,7 +1794,7 @@ dd code { background: var(--surface-2); padding: 1px 5px; border-radius: 4px; }
|
|
|
1773
1794
|
a { color: inherit; }
|
|
1774
1795
|
}
|
|
1775
1796
|
`;
|
|
1776
|
-
var JS =
|
|
1797
|
+
var JS = `
|
|
1777
1798
|
(() => {
|
|
1778
1799
|
const D = JSON.parse(document.getElementById('bh-data').textContent);
|
|
1779
1800
|
const $ = (id) => document.getElementById(id);
|
|
@@ -1792,7 +1813,7 @@ var JS = String.raw`
|
|
|
1792
1813
|
for (const kid of kids.flat(Infinity)) if (kid != null && kid !== false) n.append(kid);
|
|
1793
1814
|
return n;
|
|
1794
1815
|
};
|
|
1795
|
-
const range = (r) => (r.from === r.to ? r.from + 'px' : r.from + '
|
|
1816
|
+
const range = (r) => (r.from === r.to ? r.from + 'px' : r.from + '\u2013' + r.to + 'px');
|
|
1796
1817
|
const ranges = (rs) => rs.map(range).join(', ');
|
|
1797
1818
|
const plural = (n, w) => n + ' ' + w + (n === 1 ? '' : 's');
|
|
1798
1819
|
|
|
@@ -1818,10 +1839,10 @@ var JS = String.raw`
|
|
|
1818
1839
|
const started = new Date(D.meta.startedAt);
|
|
1819
1840
|
const scanned = D.routes.filter((r) => r.status === 'scanned');
|
|
1820
1841
|
$('meta').textContent = [
|
|
1821
|
-
D.meta.baseUrl, plural(scanned.length, 'page'), min + '
|
|
1842
|
+
D.meta.baseUrl, plural(scanned.length, 'page'), min + '\u2013' + max + 'px',
|
|
1822
1843
|
started.toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' }),
|
|
1823
1844
|
(D.meta.durationMs / 1000).toFixed(1) + 's',
|
|
1824
|
-
].filter(Boolean).join('
|
|
1845
|
+
].filter(Boolean).join(' \xB7 ');
|
|
1825
1846
|
$('version').textContent = D.meta.version;
|
|
1826
1847
|
$('copy-all').addEventListener('click', () => copy(D.prompt, 'Fix prompt copied. Paste it into your coding agent.'));
|
|
1827
1848
|
if (!s.issues) $('copy-all').hidden = true;
|
|
@@ -1891,11 +1912,9 @@ var JS = String.raw`
|
|
|
1891
1912
|
}
|
|
1892
1913
|
const skipped = D.routes.filter((r) => r.status === 'skipped' || r.status === 'redirect');
|
|
1893
1914
|
if (skipped.length) {
|
|
1894
|
-
const dynamic = skipped.find((r) => r.status === 'skipped');
|
|
1895
|
-
const example = dynamic && dynamic.route.replace(/\[\[?\.\.\.[^\]]+\]\]?/g, 'a/b').replace(/\[\[?[^\]]+\]\]?/g, '1');
|
|
1896
1915
|
$('failed').append(el('div', { class: 'callout' },
|
|
1897
1916
|
el('h2', { text: plural(skipped.length, 'route') + ' left out' }),
|
|
1898
|
-
|
|
1917
|
+
D.leftOutHint ? el('p', {}, 'Give dynamic routes real values in your config (replace the 1s with real ones): ', el('code', { text: D.leftOutHint })) : false,
|
|
1899
1918
|
listing(skipped)));
|
|
1900
1919
|
}
|
|
1901
1920
|
const shaky = D.routes.filter((r) => r.warnings.length);
|
|
@@ -2217,9 +2236,6 @@ function globMatches(glob, path) {
|
|
|
2217
2236
|
}
|
|
2218
2237
|
return new RegExp(`^${source}/?$`).test(path);
|
|
2219
2238
|
}
|
|
2220
|
-
function examplePath(path) {
|
|
2221
|
-
return path.replace(/\[\[?\.\.\.[^\]]+\]\]?/g, "a/b").replace(/\[\[?[^\]]+\]\]?/g, "1");
|
|
2222
|
-
}
|
|
2223
2239
|
|
|
2224
2240
|
// ../routes/src/syntax.ts
|
|
2225
2241
|
function joinPaths(base, path) {
|
|
@@ -3055,26 +3071,44 @@ function detectProject(cwd) {
|
|
|
3055
3071
|
}
|
|
3056
3072
|
|
|
3057
3073
|
// ../routes/src/queue.ts
|
|
3074
|
+
function namesIn(label) {
|
|
3075
|
+
return [...label.matchAll(/\[\[?(?:\.\.\.)?([^\]]+)\]\]?/g)].map((m) => m[1]);
|
|
3076
|
+
}
|
|
3077
|
+
function isPlainParam(s) {
|
|
3078
|
+
return s.kind === "param" && !s.optional && namesIn(s.label).length === 1 && s.label.startsWith("[") && s.label.endsWith("]");
|
|
3079
|
+
}
|
|
3080
|
+
function prefixOf(segments, i) {
|
|
3081
|
+
return `/${segments.slice(0, i + 1).map((s) => s.kind === "static" ? s.value : s.label).join("/")}`;
|
|
3082
|
+
}
|
|
3058
3083
|
function trimSlash(path) {
|
|
3059
3084
|
return path.length > 1 ? path.replace(/\/+$/, "") : path;
|
|
3060
3085
|
}
|
|
3061
3086
|
var RouteQueue = class {
|
|
3062
|
-
constructor(baseUrl, samples, crawler, hashRouting = false) {
|
|
3087
|
+
constructor(baseUrl, samples, crawler, hashRouting = false, params = {}) {
|
|
3063
3088
|
this.baseUrl = baseUrl;
|
|
3064
3089
|
this.samples = samples;
|
|
3065
3090
|
this.crawler = crawler;
|
|
3066
3091
|
this.hashRouting = hashRouting;
|
|
3092
|
+
this.params = params;
|
|
3067
3093
|
this.basePath = baseUrl ? new URL(baseUrl).pathname.replace(/\/+$/, "") : "";
|
|
3068
3094
|
}
|
|
3069
3095
|
baseUrl;
|
|
3070
3096
|
samples;
|
|
3071
3097
|
crawler;
|
|
3072
3098
|
hashRouting;
|
|
3099
|
+
params;
|
|
3073
3100
|
jobs = [];
|
|
3074
3101
|
/** App paths already scanned or planned, so redirects to them aren't scanned twice. */
|
|
3075
3102
|
planned = /* @__PURE__ */ new Set();
|
|
3076
3103
|
/** Dynamic routes still waiting for a link that fits them. */
|
|
3077
3104
|
waiting = /* @__PURE__ */ new Map();
|
|
3105
|
+
/** For routes left waiting at the end: the parameters nothing gave a value. */
|
|
3106
|
+
missing = /* @__PURE__ */ new Map();
|
|
3107
|
+
/**
|
|
3108
|
+
* Values parameters took in real URLs, under the route they belong to: `/cohorts/[slug]` →
|
|
3109
|
+
* `css-101`. Routes below it reuse them, since a child of a real page is real too.
|
|
3110
|
+
*/
|
|
3111
|
+
seen = /* @__PURE__ */ new Map();
|
|
3078
3112
|
patterns = /* @__PURE__ */ new Map();
|
|
3079
3113
|
order = 0;
|
|
3080
3114
|
/** Where the app is mounted on its server, e.g. "/docs"; "" at the root. */
|
|
@@ -3128,17 +3162,84 @@ var RouteQueue = class {
|
|
|
3128
3162
|
return;
|
|
3129
3163
|
}
|
|
3130
3164
|
const sample = this.samples[pattern.path];
|
|
3131
|
-
if (sample)
|
|
3132
|
-
|
|
3165
|
+
if (sample) {
|
|
3166
|
+
const samplePath = /^https?:\/\//i.test(sample) ? this.appPath(new URL(sample).pathname) : sample;
|
|
3167
|
+
if (samplePath !== null) this.learn(pattern, samplePath);
|
|
3168
|
+
this.add({ url: this.urlFor(sample), route: pattern.path });
|
|
3169
|
+
} else this.waiting.set(pattern.path, pattern);
|
|
3170
|
+
}
|
|
3171
|
+
/** Remembers the values a real URL gives a route's parameters. */
|
|
3172
|
+
learn(pattern, path) {
|
|
3173
|
+
if (pattern.segments.some((s) => s.kind === "rest" || s.kind === "param" && s.optional))
|
|
3174
|
+
return;
|
|
3175
|
+
const parts = path.split(/[?#]/)[0].split("/").filter(Boolean);
|
|
3176
|
+
if (parts.length !== pattern.segments.length) return;
|
|
3177
|
+
pattern.segments.forEach((s, i) => {
|
|
3178
|
+
if (isPlainParam(s)) {
|
|
3179
|
+
const key = prefixOf(pattern.segments, i);
|
|
3180
|
+
if (!this.seen.has(key)) this.seen.set(key, parts[i]);
|
|
3181
|
+
}
|
|
3182
|
+
});
|
|
3183
|
+
}
|
|
3184
|
+
/**
|
|
3185
|
+
* A URL for a dynamic route from values already known: a parent route's real URL, then the
|
|
3186
|
+
* config's params by name. Null, with the names still needed, when something is missing.
|
|
3187
|
+
*/
|
|
3188
|
+
build(pattern) {
|
|
3189
|
+
const parts = [];
|
|
3190
|
+
const missing = [];
|
|
3191
|
+
pattern.segments.forEach((s, i) => {
|
|
3192
|
+
if (s.kind === "static") {
|
|
3193
|
+
parts.push(s.value);
|
|
3194
|
+
return;
|
|
3195
|
+
}
|
|
3196
|
+
const inherited = isPlainParam(s) ? this.seen.get(prefixOf(pattern.segments, i)) : void 0;
|
|
3197
|
+
if (inherited !== void 0) {
|
|
3198
|
+
parts.push(inherited);
|
|
3199
|
+
return;
|
|
3200
|
+
}
|
|
3201
|
+
const names = namesIn(s.label);
|
|
3202
|
+
const absent = names.filter((n) => this.params[n] === void 0);
|
|
3203
|
+
if (absent.length) {
|
|
3204
|
+
if (!s.optional) missing.push(...absent);
|
|
3205
|
+
return;
|
|
3206
|
+
}
|
|
3207
|
+
let value = s.label;
|
|
3208
|
+
for (const n of names)
|
|
3209
|
+
value = value.replace(/\[\[?(?:\.\.\.)?[^\]]+\]\]?/, () => this.params[n]);
|
|
3210
|
+
parts.push(value.replace(/^\/+|\/+$/g, ""));
|
|
3211
|
+
});
|
|
3212
|
+
return missing.length ? { missing: [...new Set(missing)] } : { path: `/${parts.join("/")}` };
|
|
3213
|
+
}
|
|
3214
|
+
/**
|
|
3215
|
+
* Gives the routes no link fitted a URL from known values, once the links have had their
|
|
3216
|
+
* chance. Returns how many were added; the rest stay waiting, with what they're missing.
|
|
3217
|
+
*/
|
|
3218
|
+
fillWaiting() {
|
|
3219
|
+
let added = 0;
|
|
3220
|
+
for (const [key, pattern] of this.waiting) {
|
|
3221
|
+
const built = this.build(pattern);
|
|
3222
|
+
if ("missing" in built) {
|
|
3223
|
+
this.missing.set(key, built.missing);
|
|
3224
|
+
continue;
|
|
3225
|
+
}
|
|
3226
|
+
this.waiting.delete(key);
|
|
3227
|
+
this.missing.delete(key);
|
|
3228
|
+
this.add({ url: this.urlFor(built.path), route: pattern.path });
|
|
3229
|
+
added++;
|
|
3230
|
+
}
|
|
3231
|
+
return added;
|
|
3133
3232
|
}
|
|
3134
3233
|
/** Links from a scanned page: samples for dynamic routes, and new pages when crawling. */
|
|
3135
3234
|
addLinks(found) {
|
|
3136
3235
|
if (!this.baseUrl) return;
|
|
3137
3236
|
const links = found.map((link) => this.linkPath(link)).filter((p) => p !== null && isCrawlable(p));
|
|
3138
|
-
if (this.
|
|
3237
|
+
if (this.patterns.size) {
|
|
3139
3238
|
for (const link of links) {
|
|
3140
3239
|
const route = routeFor(link, this.patterns.values());
|
|
3141
|
-
if (route
|
|
3240
|
+
if (!route?.dynamic) continue;
|
|
3241
|
+
this.learn(route, link);
|
|
3242
|
+
if (this.waiting.has(route.path)) {
|
|
3142
3243
|
this.waiting.delete(route.path);
|
|
3143
3244
|
this.add({ url: this.urlFor(link), route: route.path });
|
|
3144
3245
|
}
|
|
@@ -4193,6 +4294,7 @@ var KNOWN_KEYS = /* @__PURE__ */ new Set([
|
|
|
4193
4294
|
"include",
|
|
4194
4295
|
"exclude",
|
|
4195
4296
|
"samples",
|
|
4297
|
+
"params",
|
|
4196
4298
|
"maxRoutes",
|
|
4197
4299
|
"server",
|
|
4198
4300
|
"widths",
|
|
@@ -4239,6 +4341,14 @@ function validateConfig(config) {
|
|
|
4239
4341
|
);
|
|
4240
4342
|
}
|
|
4241
4343
|
}
|
|
4344
|
+
if (config.params !== void 0) {
|
|
4345
|
+
const ok = config.params && typeof config.params === "object" && !Array.isArray(config.params) && Object.values(config.params).every((v) => typeof v === "string" && v !== "");
|
|
4346
|
+
if (!ok) {
|
|
4347
|
+
throw new Error(
|
|
4348
|
+
`params must give dynamic route parameters a real value by name, like { "slug": "css-101" }`
|
|
4349
|
+
);
|
|
4350
|
+
}
|
|
4351
|
+
}
|
|
4242
4352
|
if (config.states !== void 0 && typeof config.states !== "boolean" && !(Number.isInteger(config.states) && config.states >= 0)) {
|
|
4243
4353
|
throw new Error("states must be true, false, or the most menus and dialogs to open per page");
|
|
4244
4354
|
}
|
|
@@ -4885,13 +4995,12 @@ function printNotes(result, hasSavedLogin) {
|
|
|
4885
4995
|
` ${styleText4("yellow", "\u25CB")} ${plural22(login, "page needs", "pages need")} a login. ${how}`
|
|
4886
4996
|
);
|
|
4887
4997
|
}
|
|
4888
|
-
|
|
4998
|
+
const hint = leftOutHint(skipped);
|
|
4999
|
+
if (hint) {
|
|
4889
5000
|
console.log(
|
|
4890
|
-
` ${styleText4("dim", "\u2013")} ${plural22(skipped.length, "dynamic route", "dynamic routes")} had no link to follow. Give
|
|
5001
|
+
` ${styleText4("dim", "\u2013")} ${plural22(skipped.length, "dynamic route", "dynamic routes")} had no link to follow. Give them real values in your config (replace the 1s), e.g.`
|
|
4891
5002
|
);
|
|
4892
|
-
|
|
4893
|
-
if (first)
|
|
4894
|
-
console.log(styleText4("dim", ` samples: { '${first}': '${examplePath(first)}' }`));
|
|
5003
|
+
console.log(styleText4("dim", ` ${hint}`));
|
|
4895
5004
|
}
|
|
4896
5005
|
}
|
|
4897
5006
|
|
|
@@ -5103,6 +5212,9 @@ async function pool(items, size, work) {
|
|
|
5103
5212
|
};
|
|
5104
5213
|
await Promise.all(Array.from({ length: Math.min(size, items.length) }, worker));
|
|
5105
5214
|
}
|
|
5215
|
+
function leftOutReason(missing) {
|
|
5216
|
+
return missing.length ? `No page links to it, and nothing gives ${missing.map((n) => `[${n}]`).join(", ")} a value.` : "No page links to it, so there was no real URL to try.";
|
|
5217
|
+
}
|
|
5106
5218
|
async function drain(queue, size, work) {
|
|
5107
5219
|
const active = /* @__PURE__ */ new Set();
|
|
5108
5220
|
for (; ; ) {
|
|
@@ -5111,7 +5223,8 @@ async function drain(queue, size, work) {
|
|
|
5111
5223
|
const p = work(job).finally(() => active.delete(p));
|
|
5112
5224
|
active.add(p);
|
|
5113
5225
|
}
|
|
5114
|
-
if (!active.size) return;
|
|
5226
|
+
if (!active.size && !queue.fillWaiting()) return;
|
|
5227
|
+
if (!active.size) continue;
|
|
5115
5228
|
await Promise.race(active);
|
|
5116
5229
|
}
|
|
5117
5230
|
}
|
|
@@ -5159,7 +5272,8 @@ async function scan(req) {
|
|
|
5159
5272
|
app.baseUrl,
|
|
5160
5273
|
config.samples ?? {},
|
|
5161
5274
|
plan.crawl ? new Crawler(max2, (path) => isWanted(path, config)) : null,
|
|
5162
|
-
plan.hashRouting
|
|
5275
|
+
plan.hashRouting,
|
|
5276
|
+
config.params ?? {}
|
|
5163
5277
|
);
|
|
5164
5278
|
for (const url of plan.urls) queue.add({ url, route: routeOf(url, cwd) });
|
|
5165
5279
|
for (const p of plan.patterns) {
|
|
@@ -5307,7 +5421,7 @@ async function scan(req) {
|
|
|
5307
5421
|
url: "",
|
|
5308
5422
|
finalUrl: "",
|
|
5309
5423
|
status: "skipped",
|
|
5310
|
-
error:
|
|
5424
|
+
error: leftOutReason(queue.missing.get(pattern.path) ?? []),
|
|
5311
5425
|
pageErrors: [],
|
|
5312
5426
|
warnings: [],
|
|
5313
5427
|
breakpoints: { observed: [], declared: [], unexplained: [] },
|
|
@@ -5906,8 +6020,8 @@ var TEMPLATE = (baseUrl, typed) => `${typed ? "import { defineConfig } from '@se
|
|
|
5906
6020
|
// Every route is found automatically. To narrow it down:
|
|
5907
6021
|
// include: ['/blog/**'],
|
|
5908
6022
|
// exclude: ['/admin/**'],
|
|
5909
|
-
//
|
|
5910
|
-
//
|
|
6023
|
+
// Real values for dynamic route parameters no page links to, by name:
|
|
6024
|
+
// params: { slug: 'my-first-post', id: '42' },
|
|
5911
6025
|
|
|
5912
6026
|
// Viewport widths to sweep, in CSS pixels.
|
|
5913
6027
|
widths: { min: 320, max: 1920 },
|
package/dist/index.d.cts
CHANGED
|
@@ -172,6 +172,12 @@ export interface BreakhoundConfig {
|
|
|
172
172
|
exclude?: string[];
|
|
173
173
|
/** A real URL for each dynamic route, e.g. { '/products/[id]': '/products/42' }. */
|
|
174
174
|
samples?: Record<string, string>;
|
|
175
|
+
/**
|
|
176
|
+
* Values for dynamic route parameters by name, for routes no link on your pages fits,
|
|
177
|
+
* e.g. { slug: 'css-101', submissionId: '42' }. Every route using those names is filled in;
|
|
178
|
+
* a parent route's real URL fills its own parameters for the routes below it already.
|
|
179
|
+
*/
|
|
180
|
+
params?: Record<string, string>;
|
|
175
181
|
/** Most routes to scan in one run (default 100). */
|
|
176
182
|
maxRoutes?: number;
|
|
177
183
|
/** How to start the app when it isn't running, if not with its "dev" script. */
|
package/dist/index.d.ts
CHANGED
|
@@ -172,6 +172,12 @@ export interface BreakhoundConfig {
|
|
|
172
172
|
exclude?: string[];
|
|
173
173
|
/** A real URL for each dynamic route, e.g. { '/products/[id]': '/products/42' }. */
|
|
174
174
|
samples?: Record<string, string>;
|
|
175
|
+
/**
|
|
176
|
+
* Values for dynamic route parameters by name, for routes no link on your pages fits,
|
|
177
|
+
* e.g. { slug: 'css-101', submissionId: '42' }. Every route using those names is filled in;
|
|
178
|
+
* a parent route's real URL fills its own parameters for the routes below it already.
|
|
179
|
+
*/
|
|
180
|
+
params?: Record<string, string>;
|
|
175
181
|
/** Most routes to scan in one run (default 100). */
|
|
176
182
|
maxRoutes?: number;
|
|
177
183
|
/** How to start the app when it isn't running, if not with its "dev" script. */
|
package/dist/schema.json
CHANGED
|
@@ -38,6 +38,13 @@
|
|
|
38
38
|
},
|
|
39
39
|
"description": "A real URL for each dynamic route, e.g. { '/products/[id]': '/products/42' }."
|
|
40
40
|
},
|
|
41
|
+
"params": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"additionalProperties": {
|
|
44
|
+
"type": "string"
|
|
45
|
+
},
|
|
46
|
+
"description": "Values for dynamic route parameters by name, for routes no link on your pages fits, e.g. { slug: 'css-101', submissionId: '42' }. Every route using those names is filled in; a parent route's real URL fills its own parameters for the routes below it already."
|
|
47
|
+
},
|
|
41
48
|
"maxRoutes": {
|
|
42
49
|
"type": "number",
|
|
43
50
|
"description": "Most routes to scan in one run (default 100)."
|