@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 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). If no page links to one, it's listed with a hint, and you can give it a URL:
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 each dynamic route |
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.some((r) => r.status === "skipped") ? 'Give dynamic routes a real URL with `samples` in your config, e.g. `{ "/products/[id]": "/products/42" }`.' : void 0
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"><span class="mark" aria-hidden="true"></span><span>breakhound</span></div>
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: 8px; font-weight: 650; letter-spacing: -.01em; }
1661
- .mark { width: 18px; height: 18px; border-radius: 5px; background: linear-gradient(135deg, var(--accent) 0 55%, var(--error) 55%); }
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 = String.raw`
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 + '–' + r.to + 'px');
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 + '–' + max + 'px',
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
- dynamic ? el('p', {}, 'Give dynamic routes a real URL in your config: ', el('code', { text: "samples: { '" + dynamic.route + "': '" + example + "' }" })) : false,
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) this.add({ url: this.urlFor(sample), route: pattern.path });
3132
- else this.waiting.set(pattern.path, pattern);
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.waiting.size) {
3237
+ if (this.patterns.size) {
3139
3238
  for (const link of links) {
3140
3239
  const route = routeFor(link, this.patterns.values());
3141
- if (route && this.waiting.has(route.path)) {
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
- if (skipped.length) {
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 each a real URL with samples in your config, e.g.`
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
- const first = skipped[0]?.route;
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: "No page links to it, so there was no real URL to try.",
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
- // A real URL for dynamic routes that no page links to:
5910
- // samples: { '/products/[id]': '/products/42' },
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)."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seybel/breakhound",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Find where your layout breaks across every viewport width, then hand the fix to your coding agent.",
5
5
  "keywords": [
6
6
  "responsive",