@koda-sl/baker-cli 0.223.0 → 0.224.0-dev.33d1391b8

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
@@ -5521,15 +5521,18 @@ baker brand fonts fetch "Inter" --subsets latin,latin-ext # add accented-glyph
5521
5521
  - **`check`** runs two checks in order. First, every `--font-*` family in `src/styles/global.css` must be provided by an `@font-face` or a Google import; a family provided by nothing exits 2 with `FONT_NOT_LOADED` and lists them at `error.fix.unresolved`. Then, for the families requested from Google, it asks Google what it *actually* serves and reports `requested` / `served` / `missing` plus the available unicode subsets, exiting 2 with `FONT_NOT_SERVED` when the brand asks for something Google won't serve — distinguishing an unknown family name from a family that exists at other weights, since the fix differs.
5522
5522
  - **`check` used to pass on the worst case.** With no Google `@import` it answered `selfHosted: true` without looking at whether anything hosted the font, so a stylesheet declaring `--font-sans: "Inter"` and loading nothing was reported healthy. Measured across the client fleet, 56 of 60 repos have no Google import, so that was the answer almost everyone got; the only thing that caught it was the scaffold validator at verify time.
5523
5523
  - **Failures carry the numbers, so nothing needs a second call.** `check`'s error keeps the full per-family breakdown at `error.fix.families` (healthy families included, so a multi-family brand shows what not to touch), and both commands probe the standard weights to report the ones that *do* exist at `error.fix.served` — the remediation is "retry with one of these" rather than "run `check`", which would be the command that just failed.
5524
- - **`fetch`** downloads the family into `src/brand/fonts/` **and writes its `@font-face` into `global.css` above `@theme`**, so the family loads without a second step. It is idempotent — re-running replaces that family's faces rather than stacking duplicates — and it leaves other families, and any commented-out block, untouched. Defaults to the `latin` subset; `--subsets latin,latin-ext` adds accented-glyph coverage for Spanish and Portuguese copy. One file per weight *and* subset, since Google ships them separately and `unicode-range` is carried through verbatim.
5525
- - **A re-run keeps the weights the stylesheet already self-hosts.** `fetch` puts the downloaded faces *in charge* of the family, so it has to know which weights already exist — and the first fetch is what removes the Google `@import`, leaving the stylesheet's own `@font-face` rules as the only surviving record of them. It reads those. Passing `--weights` on a re-run therefore **narrows** the family to exactly what you passed: every other weight is dropped from the stylesheet and its file orphaned, with nothing failing, because the family stays resolved either way. A variable face declared as a range (`font-weight: 100 900`) counts as every step in that range, not as its first number — read as `100` alone, a re-fetch replaced the whole range with a single hairline rule.
5524
+ - **`fetch`** downloads the family into `src/brand/fonts/` **and writes its `@font-face` into `global.css` above `@theme`**, so the family loads without a second step. It is idempotent — re-running replaces that family's faces rather than stacking duplicates — and it leaves other families, any commented-out block, and any face it cannot itself write untouched. Defaults to the subsets the family already self-hosts, else `latin`; `--subsets latin,latin-ext` adds accented-glyph coverage for Spanish and Portuguese copy. One file per weight *and* subset, since Google ships them separately and `unicode-range` is carried through verbatim.
5525
+ - **A re-run keeps the weights and subsets the stylesheet already self-hosts.** `fetch` puts the downloaded faces *in charge* of the family, so it has to know which weights already exist — and the first fetch is what removes the Google `@import`, leaving the stylesheet's own `@font-face` rules as the only surviving record of them. It reads those. Passing `--weights` on a re-run therefore **narrows** the family to exactly what you passed: every other weight is dropped from the stylesheet and its file orphaned, with nothing failing, because the family stays resolved either way. A variable face declared as a range (`font-weight: 100 900`) counts as every step in that range, not as its first number — read as `100` alone, a re-fetch replaced the whole range with a single hairline rule. `--subsets` works the same way and for the same reason: the default is what the family already self-hosts (read back from the file names `fetch` wrote), so a bare re-run after `--subsets latin,latin-ext` no longer drops the latin-ext face and leaves accented copy on a fallback.
5526
+ - **`--italic` self-hosts the family's italic too, and a stylesheet that already loads one turns it on by itself.** There is deliberately no `--no-italic`: `--weights` and `--subsets` narrow because a brand really does drop a weight, but narrowing an italic away is the silent loss this path exists to prevent — delete the rule if you mean it. A face `fetch` cannot write is never deleted either, so a hand-made `oblique` survives untouched.
5527
+ - **It always asks the catalogue about italic, and says what it found.** The request costs the same either way, so when a family ships an italic the brand did not take, that goes in the hints — and when you ask for one the catalogue does not ship, that goes in the hints too, instead of reading as success while the browser slants the upright face. Which styles landed comes back as `data.styles`.
5526
5528
  - **Drop the family from the Google `@import` afterwards** — otherwise the page self-hosts *and* still calls Google. Record the weights in `src/brand/BRAND.md` in the same change. (`fetch` used to only print the block for you to paste; the download happened either way, so a skipped paste left the files on disk with nothing loading them. One client repo had 44 font files and zero `@font-face` rules.)
5527
5529
  - **The written `src:` is relative to `global.css` (`../brand/fonts/…`) on purpose — keep it that way.** A root-relative `/src/brand/fonts/…` reads to the bundler as a path something else already serves, so the file never lands in the published build and every page 404s it. That failure is silent: an unfetchable face falls back to a system font, so the site just renders off-brand. Kept relative, the bundler resolves it on disk, content-hashes it, and ships it. The scaffold's `validate-public-asset-refs` blocks the root-relative form.
5528
5530
  - **`adopt`** moves brand faces a page already self-hosts into `global.css`. A `--font-*` token in `@theme` is global, but a page's own `<style>` ships only with that page — so one page declaring the face leaves every other page on a fallback, which is invisible unless you happen to load one of them. It never downloads: re-fetching the same family would hand the working page a different build of the face. Refuses a family that two files declare differently (`ADOPTION_CONFLICT`) and one whose files are not in `src/brand/fonts/` (`FONT_FILE_MISSING`); `NOTHING_TO_ADOPT` means no page declares it at all, and that one is genuinely a `fetch`.
5529
5531
  - **`check` tells you which of the two to run.** Its `error.fix.action` says `adopt` when a page already self-hosts the family and `fetch` when nothing does — worth following, because reaching for `fetch` on a face the repo already ships is the exact substitution `adopt` exists to prevent.
5530
5532
  - **The scaffold sync adopts too, unattended.** The `2026-08-20-wire-unresolved-brand-fonts` migration runs the same planner at the front of every sync, before it asks any catalogue, so a client whose brand face lives in one page's `<style>` is repaired without anyone opening a Session. It diverges in one place: a conflict there falls through to downloading rather than refusing, because refusing would leave that client frozen out of every future scaffold update with nobody watching to reconcile it. It draws the Google-outage line in the same place, and has to: an unreachable catalogue is reported as *retryable*, which is what stops the migration runner recording — and so permanently disabling — a repair that only hit a bad minute.
5531
5533
  - **Two catalogues, Google first.** `fetch` resolves against Google Fonts, then Fontsource — which carries open-licence families Google does not (Open Sauce One, and ~2000 more). Google wins a family both carry, so a re-run can never swap which foundry's file the brand ships; the winner is reported as `data.provider`. When no catalogue has the name, the error carries `fix.didYouMean` — a client asking for "Open Sauce" gets back One / Sans / Two rather than a dead end.
5532
- - **Leaving Google takes more than one refusal.** Google answers `400` both to a name it has never heard of *and* to a weight it does not ship, so one rejection cannot mean "try elsewhere" read that way, `fetch "Lato" --weights 400,500` self-hosted Fontsource's Lato, because Google has no Lato 500. On a refusal the command re-asks Google one weight at a time, and if the family exists at any of them it fails `FONT_NOT_SERVED` with `fix.served` rather than switching foundry. Note Google's API is **case-sensitive** (`family=roboto` → 400, `family=Roboto` → 200), so a mis-cased name is one of the ways a Google family ends up being looked for elsewhere.
5534
+ - **Google refuses a request only when EVERY tuple in it is missing.** Measured, not assumed: `Lato:wght@400;500` answers **200 with 400 alone** — the missing weight is silently dropped, not rejected — while `Lato:wght@500` on its own is a `400`. So a refusal is true both of a name Google has never heard of *and* of a family asked for at weights it does not ship, and one rejection cannot mean "try elsewhere": read that way, `fetch "Lato" --weights 500` self-hosts Fontsource's Lato. On a refusal the command re-asks Google **once, for all nine standard weights**, and if the family exists at any of them it fails `FONT_NOT_SERVED` with `fix.served` rather than switching foundry. Note Google's API is **case-sensitive** (`family=roboto` → 400, `family=Roboto` → 200), so a mis-cased name is one of the ways a Google family ends up being looked for elsewhere.
5535
+ - **That probe is one request, and it matters that it is.** It used to fan out nine concurrent requests per family, on the belief above. Google answers a rapid series of requests with **`400`** — indistinguishable from "does not carry it" — so a three-family brand firing 27 requests could be told a real family is unknown, and act on it by self-hosting another foundry's build. The same fan-out ran unattended in the scaffold's wiring migration, where the substitute gets committed to the client repo.
5533
5536
  - **An unanswered catalogue is never a verdict, on either of them.** A lookup that times out or 5xxs fails `CATALOGUE_UNREACHABLE` (with `fix.provider`) and asks you to re-run, instead of self-hosting a different foundry's build on the strength of one failed request. That holds for Fontsource too: only its `404` means "no such family", so an outage there can no longer surface as "no font catalogue carries a family called X" — advice that would send you to check the spelling, or to hand-write an `@font-face` for a face Fontsource has.
5534
5537
  - **Multi-subset Fontsource families need the API's `unicode-range`, and get it.** Two `@font-face` rules with the same family, style and weight and no range both claim every character, so the later silently wins and the other file is downloaded, fingerprinted, shipped and never read. The ~120 families Fontsource hosts itself are single-subset with no range (nothing to disambiguate); the ~1976 it mirrors from Google are multi-subset and carry the map, which `fetch` now emits. If a multi-subset family ever reports no range, `fetch` fails `SUBSET_AMBIGUOUS` rather than writing two rules that cannot both apply.
5535
5538
  - **Licensing:** Google Fonts and Fontsource carry open-licensed families, safe to self-host. A client's own commercial typeface is not — confirm before re-hosting one.
package/dist/cli.js CHANGED
@@ -21272,24 +21272,28 @@ function renderFontFaceCss(faces) {
21272
21272
  }
21273
21273
  var FONT_PROVIDERS = ["google", "fontsource"];
21274
21274
  var FONTSOURCE_API = "https://api.fontsource.org/v1";
21275
+ var FONT_STYLES = ["normal", "italic"];
21276
+ var ITAL_AXIS = { normal: 0, italic: 1 };
21275
21277
  function fontsourceId(family) {
21276
21278
  return family.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
21277
21279
  }
21278
- function fontsourceFaces(meta, weights, subsets) {
21280
+ function fontsourceFaces(meta, weights, subsets, styles = ["normal"]) {
21279
21281
  const faces = [];
21280
21282
  for (const subset of subsets.filter((s) => meta.subsets.includes(s))) {
21281
21283
  for (const weight of weights.filter((w) => meta.weights.includes(w))) {
21282
- const url = meta.variants[String(weight)]?.normal?.[subset]?.url?.woff2;
21283
- if (!url) continue;
21284
- faces.push({
21285
- subset,
21286
- family: meta.family,
21287
- style: "normal",
21288
- weight,
21289
- url,
21290
- unicodeRange: meta.unicodeRange?.[subset] ?? "",
21291
- provider: "fontsource"
21292
- });
21284
+ for (const style of styles.filter((style2) => meta.styles.includes(style2))) {
21285
+ const url = meta.variants[String(weight)]?.[style]?.[subset]?.url?.woff2;
21286
+ if (!url) continue;
21287
+ faces.push({
21288
+ subset,
21289
+ family: meta.family,
21290
+ style,
21291
+ weight,
21292
+ url,
21293
+ unicodeRange: meta.unicodeRange?.[subset] ?? "",
21294
+ provider: "fontsource"
21295
+ });
21296
+ }
21293
21297
  }
21294
21298
  }
21295
21299
  return faces;
@@ -21319,29 +21323,30 @@ function asFontsourceMeta(body) {
21319
21323
  const ok = typeof meta.family === "string" && Array.isArray(meta.weights) && Array.isArray(meta.styles) && Array.isArray(meta.subsets) && typeof meta.variants === "object" && meta.variants !== null;
21320
21324
  return ok ? meta : null;
21321
21325
  }
21322
- async function probeGoogleWeights(family, lookup) {
21323
- const probes = await Promise.all(
21324
- STANDARD_WEIGHTS.map(async (weight) => ({ weight, got: await lookup(googleFontCssUrl(family, [weight])) }))
21325
- );
21326
- if (probes.some((probe) => probe.got.status === "unreachable")) return null;
21327
- return probes.filter((probe) => probe.got.status === "served").map((probe) => probe.weight);
21326
+ async function probeGoogleFaces(family, lookup) {
21327
+ const got = await lookup(googleFontCssUrl(family, STANDARD_WEIGHTS, FONT_STYLES));
21328
+ if (got.status === "unreachable") return null;
21329
+ return got.status === "served" ? parseGoogleFontFaces(got.css) : [];
21328
21330
  }
21329
- async function resolveFromGoogle(family, weights, subsets, deps) {
21330
- const google = await deps.lookupGoogleCss(googleFontCssUrl(family, weights));
21331
+ async function resolveFromGoogle(family, weights, subsets, styles, deps) {
21332
+ const google = await deps.lookupGoogleCss(googleFontCssUrl(family, weights, FONT_STYLES));
21331
21333
  if (google.status === "unreachable") return { ok: false, reason: "unreachable", provider: "google" };
21332
21334
  if (google.status === "absent") {
21333
- const served2 = await probeGoogleWeights(family, deps.lookupGoogleCss);
21335
+ const served2 = await probeGoogleFaces(family, deps.lookupGoogleCss);
21334
21336
  if (served2 === null) return { ok: false, reason: "unreachable", provider: "google" };
21335
- return served2.length > 0 ? { ok: false, reason: "weight", provider: "google", available: served2 } : null;
21337
+ const available = [...new Set(served2.map((face) => face.weight))].sort((a, b) => a - b);
21338
+ return available.length > 0 ? { ok: false, reason: "weight", provider: "google", available } : null;
21336
21339
  }
21337
21340
  const served = parseGoogleFontFaces(google.css);
21338
21341
  if (served.length === 0) return null;
21339
- const faces = served.filter((face) => subsets.includes(face.subset)).map((face) => ({ ...face, provider: "google" }));
21340
- if (faces.length > 0) return { ok: true, provider: "google", faces };
21342
+ const availableStyles = [...new Set(served.map((face) => face.style))].sort();
21343
+ const inSubset = served.filter((face) => subsets.includes(face.subset));
21344
+ const faces = inSubset.filter((face) => styles.includes(face.style)).map((face) => ({ ...face, provider: "google" }));
21345
+ if (faces.length > 0) return { ok: true, provider: "google", faces, availableStyles };
21341
21346
  return { ok: false, reason: "subset", provider: "google", available: [...new Set(served.map((f) => f.subset))] };
21342
21347
  }
21343
- async function resolveBrandFaces(family, weights, subsets, deps) {
21344
- const google = await resolveFromGoogle(family, weights, subsets, deps);
21348
+ async function resolveBrandFaces(family, weights, subsets, deps, styles = ["normal"]) {
21349
+ const google = await resolveFromGoogle(family, weights, subsets, styles, deps);
21345
21350
  if (google) return google;
21346
21351
  let body;
21347
21352
  try {
@@ -21351,12 +21356,12 @@ async function resolveBrandFaces(family, weights, subsets, deps) {
21351
21356
  }
21352
21357
  const meta = asFontsourceMeta(body);
21353
21358
  if (meta) {
21354
- const faces = fontsourceFaces(meta, weights, subsets);
21359
+ const faces = fontsourceFaces(meta, weights, subsets, styles);
21355
21360
  const ambiguous = undisambiguatedSubsets(faces);
21356
21361
  if (ambiguous.length > 0) {
21357
21362
  return { ok: false, reason: "ambiguous-subsets", provider: "fontsource", available: meta.subsets };
21358
21363
  }
21359
- if (faces.length > 0) return { ok: true, provider: "fontsource", faces };
21364
+ if (faces.length > 0) return { ok: true, provider: "fontsource", faces, availableStyles: [...meta.styles].sort() };
21360
21365
  if (!subsets.some((subset) => meta.subsets.includes(subset))) {
21361
21366
  return { ok: false, reason: "subset", provider: "fontsource", available: meta.subsets };
21362
21367
  }
@@ -21453,18 +21458,28 @@ function resolveFetchWeights(flag, declared) {
21453
21458
  }
21454
21459
  return declared?.length ? declared : [400];
21455
21460
  }
21456
- function googleFontCssUrl(family, weights) {
21461
+ var DEFAULT_SUBSETS = ["latin"];
21462
+ function resolveFetchSubsets(flag, selfHosted) {
21463
+ const asked = String(flag ?? "").split(",").map((subset) => subset.trim()).filter(Boolean);
21464
+ const named = flag ? [...new Set(asked)] : selfHosted;
21465
+ return named.length > 0 ? named : [...DEFAULT_SUBSETS];
21466
+ }
21467
+ function resolveFetchStyles(italic, selfHosted) {
21468
+ return italic || selfHosted.includes("italic") ? ["normal", "italic"] : ["normal"];
21469
+ }
21470
+ function googleFontCssUrl(family, weights, styles = ["normal"]) {
21457
21471
  const name = family.trim().split(/\s+/).map(encodeURIComponent).join("+");
21458
- const axis = [...new Set(weights)].sort((a, b) => a - b).join(";");
21459
- const spec = axis ? `${name}:wght@${axis}` : name;
21472
+ const sorted = [...new Set(weights)].sort((a, b) => a - b);
21473
+ if (sorted.length === 0) return `${GOOGLE_CSS2}?family=${name}&display=swap`;
21474
+ const wanted = FONT_STYLES.filter((style) => styles.includes(style));
21475
+ const spec = wanted.length > 1 ? `${name}:ital,wght@${wanted.flatMap((style) => sorted.map((w) => `${ITAL_AXIS[style]},${w}`)).join(";")}` : `${name}:wght@${sorted.join(";")}`;
21460
21476
  return `${GOOGLE_CSS2}?family=${spec}&display=swap`;
21461
21477
  }
21462
21478
  var STANDARD_WEIGHTS = [100, 200, 300, 400, 500, 600, 700, 800, 900];
21463
21479
  async function probeServedWeights(family, fetchCss) {
21464
- const probes = await Promise.all(
21465
- STANDARD_WEIGHTS.map(async (weight) => await fetchCss(googleFontCssUrl(family, [weight])) ? weight : null)
21466
- );
21467
- return probes.filter((w) => w !== null);
21480
+ const css = await fetchCss(googleFontCssUrl(family, STANDARD_WEIGHTS));
21481
+ if (css === null) return [];
21482
+ return [...new Set(parseGoogleFontFaces(css).map((face) => face.weight))].sort((a, b) => a - b);
21468
21483
  }
21469
21484
  async function checkFontRequests(requests, fetchCss) {
21470
21485
  const results = [];
@@ -21546,6 +21561,34 @@ function selfHostedWeights(css, family) {
21546
21561
  }
21547
21562
  return [...weights].sort((a, b) => a - b);
21548
21563
  }
21564
+ function fontFileSubset(href) {
21565
+ const file = posix.basename((href.split("?")[0] ?? "").split("#")[0] ?? "");
21566
+ return /^[A-Za-z0-9]+-[1-9]00(?:italic)?-([\w-]+)\.woff2$/.exec(file)?.[1] ?? null;
21567
+ }
21568
+ function selfHostedSubsets(css, family) {
21569
+ const target = normalizeFamily(family);
21570
+ const subsets = /* @__PURE__ */ new Set();
21571
+ for (const rule of stripCssComments(css).matchAll(/@font-face\s*\{([^}]*)\}/gi)) {
21572
+ const body = rule[1] ?? "";
21573
+ const declared = body.match(/font-family\s*:\s*([^;]+)/i)?.[1];
21574
+ if (!declared || normalizeFamily(declared) !== target) continue;
21575
+ for (const url of body.matchAll(/url\(\s*['"]?([^'")]+)['"]?\s*\)/gi)) {
21576
+ const subset = fontFileSubset(url[1] ?? "");
21577
+ if (subset) subsets.add(subset);
21578
+ }
21579
+ }
21580
+ return [...subsets].sort();
21581
+ }
21582
+ function selfHostedStyles(css, family) {
21583
+ const target = normalizeFamily(family);
21584
+ const styles = /* @__PURE__ */ new Set();
21585
+ for (const rule of stripCssComments(css).matchAll(/@font-face\s*\{([^}]*)\}/gi)) {
21586
+ const body = rule[1] ?? "";
21587
+ const declared = body.match(/font-family\s*:\s*([^;]+)/i)?.[1];
21588
+ if (declared && normalizeFamily(declared) === target) styles.add(faceStyle(body));
21589
+ }
21590
+ return [...styles].sort();
21591
+ }
21549
21592
  function fontsourceFamilies(css) {
21550
21593
  const out = /* @__PURE__ */ new Set();
21551
21594
  for (const imported of stripCssComments(css).matchAll(/@fontsource(-variable)?\/([\w-]+)/gi)) {
@@ -21566,21 +21609,32 @@ function unresolvedBrandFamilies(css) {
21566
21609
  for (const request of googleFontRequests(stripCssComments(css))) loaded.add(normalizeFamily(request.family));
21567
21610
  return declared.filter((family) => !loaded.has(family)).map((family) => asAuthored(css, family));
21568
21611
  }
21569
- function stripFontFaceBlocks(source, family) {
21612
+ function stripFontFaceBlocks(source, family, styles) {
21570
21613
  const target = normalizeFamily(family);
21571
21614
  const ranges = [];
21572
- for (const rule of stripCssComments(source).matchAll(/@font-face\s*\{[^}]*\}/gi)) {
21573
- const declared = rule[0].match(/font-family\s*:\s*([^;]+)/i)?.[1];
21574
- if (declared && normalizeFamily(declared) === target && rule.index !== void 0) {
21575
- ranges.push([rule.index, rule.index + rule[0].length]);
21576
- }
21615
+ for (const rule of stripCssComments(source).matchAll(/@font-face\s*\{([^}]*)\}/gi)) {
21616
+ const body = rule[1] ?? "";
21617
+ const declared = body.match(/font-family\s*:\s*([^;]+)/i)?.[1];
21618
+ if (!declared || normalizeFamily(declared) !== target || rule.index === void 0) continue;
21619
+ if (styles && !styles.has(faceStyle(body))) continue;
21620
+ ranges.push([rule.index, rule.index + rule[0].length]);
21577
21621
  }
21578
21622
  let out = source;
21579
21623
  for (const [start, end] of ranges.reverse()) out = `${out.slice(0, start)}${out.slice(end)}`;
21580
21624
  return out.replace(/\n{3,}/g, "\n\n");
21581
21625
  }
21626
+ function faceStyle(body) {
21627
+ return body.match(/font-style\s*:\s*([\w-]+)/i)?.[1]?.trim().toLowerCase() ?? "normal";
21628
+ }
21629
+ function declaredStyles(block) {
21630
+ const styles = /* @__PURE__ */ new Set();
21631
+ for (const rule of stripCssComments(block).matchAll(/@font-face\s*\{([^}]*)\}/gi))
21632
+ styles.add(faceStyle(rule[1] ?? ""));
21633
+ return styles;
21634
+ }
21582
21635
  function upsertFontFaceCss(css, family, block) {
21583
- const out = stripFontFaceBlocks(css, family);
21636
+ const styles = declaredStyles(block);
21637
+ const out = stripFontFaceBlocks(css, family, styles.size > 0 ? styles : void 0);
21584
21638
  const themeAt = stripCssComments(out).search(/@theme\b/);
21585
21639
  if (themeAt < 0) return `${out.trimEnd()}
21586
21640
 
@@ -21600,7 +21654,7 @@ ${tail}`;
21600
21654
  // src/commands/brand/fonts.ts
21601
21655
  var GLOBAL_CSS = path2.join("src", "styles", "global.css");
21602
21656
  var FONTS_DIR = path2.join("src", "brand", "fonts");
21603
- var DEFAULT_SUBSETS = "latin";
21657
+ var DEFAULT_SUBSETS_LABEL = DEFAULT_SUBSETS.join(",");
21604
21658
  var BROWSER_UA = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120.0 Safari/537.36";
21605
21659
  registerSchema({
21606
21660
  command: "brand.fonts.check",
@@ -21624,7 +21678,12 @@ registerSchema({
21624
21678
  },
21625
21679
  subsets: {
21626
21680
  type: "string",
21627
- description: `Comma-separated unicode subsets (default: ${DEFAULT_SUBSETS})`,
21681
+ description: `Comma-separated unicode subsets (default: the subsets global.css already self-hosts, else ${DEFAULT_SUBSETS_LABEL})`,
21682
+ required: false
21683
+ },
21684
+ italic: {
21685
+ type: "boolean",
21686
+ description: "Also self-host the family's italic faces. On automatically when global.css already loads one, so a re-fetch never strands it.",
21628
21687
  required: false
21629
21688
  }
21630
21689
  }
@@ -21669,15 +21728,34 @@ var WIRING_HINT = {
21669
21728
  };
21670
21729
  var PROVIDER_DEPS = { lookupGoogleCss, fetchJson };
21671
21730
  var PROVIDER_LABEL = { google: "Google Fonts", fontsource: "Fontsource" };
21731
+ function italicHints(family, wantedStyles, availableStyles, downloadedStyles, provider) {
21732
+ const wanted = wantedStyles.includes("italic");
21733
+ const available = availableStyles.includes("italic");
21734
+ if (wanted && !downloadedStyles.includes("italic")) {
21735
+ return [
21736
+ `NO ITALIC: ${PROVIDER_LABEL[provider]} does not ship an italic for "${family}", so none was written. Drop italic from src/brand/BRAND.md, or the browser will slant the upright face instead.`
21737
+ ];
21738
+ }
21739
+ if (!wanted && available) {
21740
+ return [
21741
+ `${PROVIDER_LABEL[provider]} also ships an italic for "${family}", which was NOT downloaded. If the brand uses italic anywhere, re-run with \`--italic\` \u2014 otherwise the browser slants the upright face, which is not the same shapes.`
21742
+ ];
21743
+ }
21744
+ return [];
21745
+ }
21672
21746
  function fetchHints({
21673
21747
  family,
21674
21748
  downloadedWeights,
21749
+ downloadedStyles,
21675
21750
  incomplete,
21676
21751
  wiring,
21677
- provider
21752
+ provider,
21753
+ wantedStyles,
21754
+ availableStyles
21678
21755
  }) {
21679
21756
  return [
21680
21757
  WIRING_HINT[wiring](family),
21758
+ ...italicHints(family, wantedStyles, availableStyles, downloadedStyles, provider),
21681
21759
  `Record the exact weights (${downloadedWeights.join(", ")}) in src/brand/BRAND.md \u2014 a weight listed there but not downloaded renders as a synthesized fallback.`,
21682
21760
  ...incomplete.length > 0 ? [
21683
21761
  // The remedy is provider-specific: only a Google family has an @import
@@ -21838,7 +21916,14 @@ var fontsFetchCommand = defineCommand93({
21838
21916
  type: "string",
21839
21917
  description: "Comma-separated weights (default: the weights global.css already requests or self-hosts, else 400)"
21840
21918
  },
21841
- subsets: { type: "string", description: `Comma-separated unicode subsets (default: ${DEFAULT_SUBSETS})` }
21919
+ subsets: {
21920
+ type: "string",
21921
+ description: `Comma-separated unicode subsets (default: the subsets global.css already self-hosts, else ${DEFAULT_SUBSETS_LABEL})`
21922
+ },
21923
+ italic: {
21924
+ type: "boolean",
21925
+ description: "Also self-host the family's italic faces (default: on when global.css already loads one, off otherwise)"
21926
+ }
21842
21927
  },
21843
21928
  async run({ args }) {
21844
21929
  const family = String(args.family).trim();
@@ -21850,10 +21935,9 @@ var fontsFetchCommand = defineCommand93({
21850
21935
  if (weights.length === 0) {
21851
21936
  fail("INVALID_WEIGHTS", `"${args.weights}" has no usable weight \u2014 pass whole hundreds, e.g. --weights 400,700.`);
21852
21937
  }
21853
- const wanted = new Set(
21854
- String(args.subsets ?? DEFAULT_SUBSETS).split(",").map((s) => s.trim()).filter(Boolean)
21855
- );
21856
- const resolved = await resolveBrandFaces(family, weights, [...wanted], PROVIDER_DEPS);
21938
+ const wanted = new Set(resolveFetchSubsets(args.subsets, selfHostedSubsets(css, family)));
21939
+ const styles = resolveFetchStyles(args.italic, selfHostedStyles(css, family));
21940
+ const resolved = await resolveBrandFaces(family, weights, [...wanted], PROVIDER_DEPS, styles);
21857
21941
  if (!resolved.ok) return failResolution(family, weights, resolved);
21858
21942
  const faces = resolved.faces;
21859
21943
  const fontsDir = path2.resolve(process.cwd(), FONTS_DIR);
@@ -21877,6 +21961,7 @@ var fontsFetchCommand = defineCommand93({
21877
21961
  }
21878
21962
  const fontFace = renderFontFaceCss(downloaded);
21879
21963
  const downloadedWeights = [...new Set(downloaded.map((f) => f.weight))].sort((a, b) => a - b);
21964
+ const downloadedStyles = [...new Set(downloaded.map((f) => f.style))].sort();
21880
21965
  const incomplete = weights.filter((w) => !downloadedWeights.includes(w));
21881
21966
  const wiring = await wireFontFaceIntoStylesheet(css, family, fontFace);
21882
21967
  writeJson({
@@ -21885,11 +21970,21 @@ var fontsFetchCommand = defineCommand93({
21885
21970
  family,
21886
21971
  provider: resolved.provider,
21887
21972
  weights: downloadedWeights,
21973
+ styles: downloadedStyles,
21888
21974
  files: written,
21889
21975
  fontFace,
21890
21976
  stylesheet: wiring
21891
21977
  },
21892
- hints: fetchHints({ family, downloadedWeights, incomplete, wiring, provider: resolved.provider })
21978
+ hints: fetchHints({
21979
+ family,
21980
+ downloadedWeights,
21981
+ downloadedStyles,
21982
+ incomplete,
21983
+ wiring,
21984
+ provider: resolved.provider,
21985
+ wantedStyles: styles,
21986
+ availableStyles: resolved.availableStyles
21987
+ })
21893
21988
  });
21894
21989
  }
21895
21990
  });