diff --git a/apps/composer/app/fonts.ts b/apps/composer/app/fonts.ts index 524a79c..eabfc7b 100644 --- a/apps/composer/app/fonts.ts +++ b/apps/composer/app/fonts.ts @@ -1,38 +1,70 @@ /** * The Aesthetic Function type set (matches af-site/assets/af.css): Oswald * for headlines, IBM Plex Sans for body, IBM Plex Mono for labels and - * buttons, Jost for the wordmark. next/font downloads at build time and - * self-hosts under _next/static/media — no runtime request to Google, so - * the production smoke suite's clean-network check is unaffected. + * buttons, Jost for the wordmark. + * + * These were `next/font/google` declarations. Next self-hosted the result, so + * nothing was fetched at runtime — but the BUILD downloaded every face from + * fonts.gstatic.com, so a CDN hiccup failed the build outright and blocked + * merges and deploys. The faces now come from committed .woff2 files in + * packages/fonts, which no build has to go to the network for. + * + * The files there are the exact bytes Google was already serving this repo, and + * the descriptors below reproduce the @font-face rules those declarations + * generated — same weights, same styles, same font-display, same CSS variable + * names. See packages/fonts/README.md for provenance, subset and licensing. + * + * `adjustFontFallback: false` + an explicit `fallback` is not a change of + * fallback, it is what keeps the fallback the same: next/font/local would + * recompute the metric-matched face from our subset files and land on slightly + * different numbers. packages/fonts/fallbacks.css pins the faces at the values + * next/font/google emitted, and is imported here so the declarations and the + * faces they name travel together. */ -import { IBM_Plex_Mono, IBM_Plex_Sans, Jost, Oswald } from "next/font/google"; +import localFont from "next/font/local"; -export const oswald = Oswald({ - subsets: ["latin"], - weight: ["600"], +import "../../../packages/fonts/fallbacks.css"; + +export const oswald = localFont({ + src: [{ path: "../../../packages/fonts/files/oswald-600-latin.woff2", weight: "600", style: "normal" }], variable: "--font-oswald", display: "swap", + adjustFontFallback: false, + fallback: ["Oswald Fallback"], }); -export const plexSans = IBM_Plex_Sans({ - subsets: ["latin"], - weight: ["400", "500", "600"], +// One variable file (wght 100..700) serves all three weights, exactly as the +// single file Google served for all three did. +export const plexSans = localFont({ + src: [ + { path: "../../../packages/fonts/files/ibm-plex-sans-variable-latin.woff2", weight: "400", style: "normal" }, + { path: "../../../packages/fonts/files/ibm-plex-sans-variable-latin.woff2", weight: "500", style: "normal" }, + { path: "../../../packages/fonts/files/ibm-plex-sans-variable-latin.woff2", weight: "600", style: "normal" }, + ], variable: "--font-plex-sans", display: "swap", + adjustFontFallback: false, + fallback: ["IBM Plex Sans Fallback"], }); -export const plexMono = IBM_Plex_Mono({ - subsets: ["latin"], - weight: ["400", "500", "600"], +export const plexMono = localFont({ + src: [ + { path: "../../../packages/fonts/files/ibm-plex-mono-400-latin.woff2", weight: "400", style: "normal" }, + { path: "../../../packages/fonts/files/ibm-plex-mono-500-latin.woff2", weight: "500", style: "normal" }, + { path: "../../../packages/fonts/files/ibm-plex-mono-600-latin.woff2", weight: "600", style: "normal" }, + ], variable: "--font-plex-mono", display: "swap", + adjustFontFallback: false, + fallback: ["IBM Plex Mono Fallback"], }); -export const jost = Jost({ - subsets: ["latin"], - weight: ["400"], +export const jost = localFont({ + src: [{ path: "../../../packages/fonts/files/jost-400-latin.woff2", weight: "400", style: "normal" }], variable: "--font-jost", display: "swap", + adjustFontFallback: false, + fallback: ["Jost Fallback"], }); export const fontVariables = [ diff --git a/apps/web/app/fonts.ts b/apps/web/app/fonts.ts index 524a79c..eabfc7b 100644 --- a/apps/web/app/fonts.ts +++ b/apps/web/app/fonts.ts @@ -1,38 +1,70 @@ /** * The Aesthetic Function type set (matches af-site/assets/af.css): Oswald * for headlines, IBM Plex Sans for body, IBM Plex Mono for labels and - * buttons, Jost for the wordmark. next/font downloads at build time and - * self-hosts under _next/static/media — no runtime request to Google, so - * the production smoke suite's clean-network check is unaffected. + * buttons, Jost for the wordmark. + * + * These were `next/font/google` declarations. Next self-hosted the result, so + * nothing was fetched at runtime — but the BUILD downloaded every face from + * fonts.gstatic.com, so a CDN hiccup failed the build outright and blocked + * merges and deploys. The faces now come from committed .woff2 files in + * packages/fonts, which no build has to go to the network for. + * + * The files there are the exact bytes Google was already serving this repo, and + * the descriptors below reproduce the @font-face rules those declarations + * generated — same weights, same styles, same font-display, same CSS variable + * names. See packages/fonts/README.md for provenance, subset and licensing. + * + * `adjustFontFallback: false` + an explicit `fallback` is not a change of + * fallback, it is what keeps the fallback the same: next/font/local would + * recompute the metric-matched face from our subset files and land on slightly + * different numbers. packages/fonts/fallbacks.css pins the faces at the values + * next/font/google emitted, and is imported here so the declarations and the + * faces they name travel together. */ -import { IBM_Plex_Mono, IBM_Plex_Sans, Jost, Oswald } from "next/font/google"; +import localFont from "next/font/local"; -export const oswald = Oswald({ - subsets: ["latin"], - weight: ["600"], +import "../../../packages/fonts/fallbacks.css"; + +export const oswald = localFont({ + src: [{ path: "../../../packages/fonts/files/oswald-600-latin.woff2", weight: "600", style: "normal" }], variable: "--font-oswald", display: "swap", + adjustFontFallback: false, + fallback: ["Oswald Fallback"], }); -export const plexSans = IBM_Plex_Sans({ - subsets: ["latin"], - weight: ["400", "500", "600"], +// One variable file (wght 100..700) serves all three weights, exactly as the +// single file Google served for all three did. +export const plexSans = localFont({ + src: [ + { path: "../../../packages/fonts/files/ibm-plex-sans-variable-latin.woff2", weight: "400", style: "normal" }, + { path: "../../../packages/fonts/files/ibm-plex-sans-variable-latin.woff2", weight: "500", style: "normal" }, + { path: "../../../packages/fonts/files/ibm-plex-sans-variable-latin.woff2", weight: "600", style: "normal" }, + ], variable: "--font-plex-sans", display: "swap", + adjustFontFallback: false, + fallback: ["IBM Plex Sans Fallback"], }); -export const plexMono = IBM_Plex_Mono({ - subsets: ["latin"], - weight: ["400", "500", "600"], +export const plexMono = localFont({ + src: [ + { path: "../../../packages/fonts/files/ibm-plex-mono-400-latin.woff2", weight: "400", style: "normal" }, + { path: "../../../packages/fonts/files/ibm-plex-mono-500-latin.woff2", weight: "500", style: "normal" }, + { path: "../../../packages/fonts/files/ibm-plex-mono-600-latin.woff2", weight: "600", style: "normal" }, + ], variable: "--font-plex-mono", display: "swap", + adjustFontFallback: false, + fallback: ["IBM Plex Mono Fallback"], }); -export const jost = Jost({ - subsets: ["latin"], - weight: ["400"], +export const jost = localFont({ + src: [{ path: "../../../packages/fonts/files/jost-400-latin.woff2", weight: "400", style: "normal" }], variable: "--font-jost", display: "swap", + adjustFontFallback: false, + fallback: ["Jost Fallback"], }); export const fontVariables = [ diff --git a/packages/fonts/OFL.txt b/packages/fonts/OFL.txt new file mode 100644 index 0000000..34d74a5 --- /dev/null +++ b/packages/fonts/OFL.txt @@ -0,0 +1,113 @@ +The four families vendored in ./files are each licensed under the SIL Open +Font License, Version 1.1. Every one of them declares that license in its own +`licenseURL` name record (http[s]://scripts.sil.org/OFL) — see ./README.md for +how to re-read that from the binaries. Their copyright notices, taken verbatim +from the `copyright` name record of the vendored files: + + Oswald + Copyright 2016 The Oswald Project Authors + (https://github.com/googlefonts/OswaldFont) + + IBM Plex Sans + Copyright 2019 IBM Corp. All rights reserved. + with Reserved Font Name "Plex" + + IBM Plex Mono + Copyright 2017 IBM Corp. All rights reserved. + with Reserved Font Name "Plex" + + Jost + Copyright 2020 The Jost Project Authors + (https://github.com/indestructible-type/Jost) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/packages/fonts/README.md b/packages/fonts/README.md new file mode 100644 index 0000000..74da932 --- /dev/null +++ b/packages/fonts/README.md @@ -0,0 +1,112 @@ +# @dspack-studio/fonts + +The Aesthetic Function type set, committed as `.woff2` files. + +## Why this package exists + +`apps/web` and `apps/composer` used to declare their type with `next/font/google`. +Next self-hosts the result, so nothing was fetched at *runtime* — but the **build** +downloaded every face from `fonts.gstatic.com`, which made every build, every CI +run and every deploy depend on a third-party CDN being up. When it hiccuped the +build did not degrade, it failed: + +``` +[Error: Failed to fetch font file from `https://fonts.gstatic.com/s/ibmplexsans/...woff2`.] +An error occurred in `next/font`. +> Build failed because of webpack errors +``` + +Both apps now declare the same faces with `next/font/local`, pointed at the files +in `./files`. The bytes are in git, so a build needs no network for type at all. + +## What is here + +Six faces, 90.5 KB total, `latin` subset: + +| File | Family | Weights | Bytes | +| --- | --- | ---: | ---: | +| `oswald-600-latin.woff2` | Oswald 4.103 | 600 | 12,776 | +| `ibm-plex-sans-variable-latin.woff2` | IBM Plex Sans 3.201 | 400 / 500 / 600 | 40,240 | +| `ibm-plex-mono-400-latin.woff2` | IBM Plex Mono 2.3 | 400 | 10,052 | +| `ibm-plex-mono-500-latin.woff2` | IBM Plex Mono 2.3 | 500 | 10,060 | +| `ibm-plex-mono-600-latin.woff2` | IBM Plex Mono 2.3 | 600 | 10,120 | +| `jost-400-latin.woff2` | Jost 3.710 | 400 | 9,404 | + +IBM Plex Sans is a variable font with a `wght 100..700` axis, so one file serves +all three weights — the same single file Google served for all three before. + +`catalog.json` pins each file by sha256. `node verify.mjs` re-checks every one of +them offline, and runs as this package's `test` script, so `pnpm test` covers it. + +## Source and provenance + +These are **the exact bytes `next/font/google` was already downloading** for this +repo — lifted from the `_next/static/media` output of a build on `main` +(`2c97210`), not re-exported or re-subset from somewhere else. That is deliberate: +it makes "no visual change" true by construction rather than by inspection. Same +files, same weights, same `font-display`, same CSS variable names. + +Independent corroboration: all eight IBM Plex latin and latin-ext faces Google +served this repo are byte-identical (sha256) to the files vendored in the +`af-site` brand repo, which pins its own roster against the `google/fonts` `ofl/` +tree. The four IBM Plex faces used here are among them — two repos arrived at the +same bytes by different routes. + +Oswald and Jost do **not** match af-site byte for byte, and should not: both are +variable fonts upstream, and `next/font/google` asked Google to pin the axis to +the one weight this product uses (`Oswald:wght@600`, `Jost:wght@400`), which +yields a smaller static instance than the axis-preserving file af-site vendors. +Same family, same version, same rendered weight. + +## Subset + +`latin` only, matching the `subsets: ["latin"]` the apps have always declared. + +`next/font/google` emits `@font-face` rules for *every* subset Google returns and +only **preloads** the declared ones, so builds on `main` shipped 29 files (adding +latin-ext, greek, cyrillic, cyrillic-ext, vietnamese) of which these same 6 were +the preloaded latin set. Dropping the other 23 is safe here and was checked, not +assumed: a scan of `apps/`, `packages/`, `e2e/`, `acceptance/` and the demo project +found **zero** codepoints in the latin-ext, Greek, Cyrillic or Vietnamese ranges. +The only non-ASCII characters the product renders are arrows and marks +(`←`, `→`, `✓`, `✗`, `↳`, …) which fall outside *every* subset Google served, so +they resolved to the system stack before this change and still do. + +Note the practical edge: `latin` covers `U+0000–U+00FF`, so all of the Western +European accented letters (`é à ö ñ ç ü …`) render in the brand faces. Text a user +authors in Polish, Czech, Turkish, Baltic or Vietnamese uses `latin-ext` +codepoints and will fall back to the system stack — as it did before for anything +beyond that, and as `af-site` also accepts by contract. If that ever needs to +change, add the latin-ext faces here and give each family its own `unicode-range`; +`next/font/local` applies `declarations` to every `src` entry in a call, so a +second subset needs either a merged file or a separate call per subset. + +## Licensing + +All four families are SIL Open Font License 1.1. This is not taken on trust — +each vendored binary declares it in its own `licenseURL` name record: + +| Family | `licenseURL` | +| --- | --- | +| Oswald | `https://scripts.sil.org/OFL` | +| IBM Plex Sans | `http://scripts.sil.org/OFL` | +| IBM Plex Mono | `http://scripts.sil.org/OFL` | +| Jost | `http://scripts.sil.org/OFL` | + +`OFL.txt` carries the full license text and each family's copyright notice, as +OFL 1.1 clause 2 requires of a redistribution. Copyright lines there are quoted +verbatim from the `copyright` name record of the vendored files. + +To re-read those records from the binaries: + +```js +import fontkit from "fontkit"; +const font = fontkit.openSync("files/oswald-600-latin.woff2"); +console.log(font.copyright, font.name.records.licenseURL); +``` + +## Changing the type set + +Don't do it here alone. These faces mirror `af-site/assets/af.css`, which is the +brand source of truth for Aesthetic Function type. Change that first, then bring +the files across and re-run `node verify.mjs` after updating `catalog.json`. diff --git a/packages/fonts/catalog.json b/packages/fonts/catalog.json new file mode 100644 index 0000000..7b47e64 --- /dev/null +++ b/packages/fonts/catalog.json @@ -0,0 +1,99 @@ +{ + "$comment": "Byte-pinned roster of the vendored Aesthetic Function type set. These are the exact files next/font/google previously downloaded from fonts.gstatic.com at build time for apps/web and apps/composer; they are now committed so no build touches the network. Regenerate nothing by hand: `node verify.mjs` re-hashes every file against this record, offline.", + "source": "Google Fonts (fonts.gstatic.com), via next/font/google in this repo at commit 2c97210", + "subset": "latin", + "license": { + "id": "OFL-1.1", + "text": "OFL.txt", + "evidence": "every family declares scripts.sil.org/OFL in its licenseURL name record" + }, + "totalBytes": 92652, + "faces": [ + { + "file": "oswald-600-latin.woff2", + "family": "Oswald", + "weights": [ + "600" + ], + "style": "normal", + "version": "4.103", + "variable": false, + "subset": "latin", + "license": "OFL-1.1", + "bytes": 12776, + "sha256": "b5298a57a4f1058d9cf45db659c5b29f21f55b6f9fd0efc6faa13b1b8db523b6" + }, + { + "file": "ibm-plex-sans-variable-latin.woff2", + "family": "IBM Plex Sans", + "weights": [ + "400", + "500", + "600" + ], + "style": "normal", + "version": "3.201", + "variable": "wght 100..700", + "subset": "latin", + "license": "OFL-1.1", + "bytes": 40240, + "sha256": "056e4e2459f57a0033c8c9c844ff19d6e42ac8602027803d4345823bcc939818" + }, + { + "file": "ibm-plex-mono-400-latin.woff2", + "family": "IBM Plex Mono", + "weights": [ + "400" + ], + "style": "normal", + "version": "2.3", + "variable": false, + "subset": "latin", + "license": "OFL-1.1", + "bytes": 10052, + "sha256": "c36f509c0a8f9f85f29cb44bc8701d8a9e0b14c499e77a884f789ead7093a7ac" + }, + { + "file": "ibm-plex-mono-500-latin.woff2", + "family": "IBM Plex Mono", + "weights": [ + "500" + ], + "style": "normal", + "version": "2.3", + "variable": false, + "subset": "latin", + "license": "OFL-1.1", + "bytes": 10060, + "sha256": "a76f53ca6612e7b3828eec2311098675b7f9849ae4169a8bcef6302aec02a6c0" + }, + { + "file": "ibm-plex-mono-600-latin.woff2", + "family": "IBM Plex Mono", + "weights": [ + "600" + ], + "style": "normal", + "version": "2.3", + "variable": false, + "subset": "latin", + "license": "OFL-1.1", + "bytes": 10120, + "sha256": "ad4580d8cb4b5f627c2d18457656732f7f7b070f7837fbc380e08054157e6f6c" + }, + { + "file": "jost-400-latin.woff2", + "family": "Jost", + "weights": [ + "400" + ], + "style": "normal", + "version": "3.710", + "variable": false, + "subset": "latin", + "license": "OFL-1.1", + "bytes": 9404, + "sha256": "b249869fc8fb37ef0328d6f603283863ce373f52d1a4af6380f54d991f45dec1" + } + ] +} diff --git a/packages/fonts/fallbacks.css b/packages/fonts/fallbacks.css new file mode 100644 index 0000000..4a1f060 --- /dev/null +++ b/packages/fonts/fallbacks.css @@ -0,0 +1,62 @@ +/* + * Metric-matched fallback faces for the vendored type set. + * + * `next/font` normally generates these for you: it synthesises an + * " Fallback" face from `local("Arial")` plus overrides, so that text + * painted during `font-display: swap` — and any character the face does not + * carry — occupies close to the space the real font will. + * + * The generated numbers are NOT stable across the google -> local move. + * next/font/google reads them from a metrics database keyed on the family; + * next/font/local recomputes them from whichever file you hand it, and our + * files are the `latin` subset, whose OS/2 xAvgCharWidth differs from the full + * font's. Letting it recompute shifted IBM Plex Mono's size-adjust from 134.59% + * to 131.49% and visibly narrowed every character the subset does not carry + * (the `→` in the pipeline views, for one). + * + * So the faces are pinned here instead, at exactly the values next/font/google + * emitted on main (2c97210), and the loaders pass `adjustFontFallback: false` + * so nothing overwrites them. That keeps the rendered page pixel-identical + * across this change, and it pins the fallback metrics against drift the same + * way catalog.json pins the font bytes. + * + * If the type set ever changes, regenerate these from a build rather than + * editing by hand: + * grep -o '@font-face{font-family:[^}]*Fallback[^}]*}' apps/web/out/_next/static/css/*.css + */ + +@font-face { + font-family: "Oswald Fallback"; + src: local("Arial"); + ascent-override: 146.51%; + descent-override: 35.49%; + line-gap-override: 0%; + size-adjust: 81.43%; +} + +@font-face { + font-family: "IBM Plex Sans Fallback"; + src: local("Arial"); + ascent-override: 101.32%; + descent-override: 27.18%; + line-gap-override: 0%; + size-adjust: 101.17%; +} + +@font-face { + font-family: "IBM Plex Mono Fallback"; + src: local("Arial"); + ascent-override: 76.16%; + descent-override: 20.43%; + line-gap-override: 0%; + size-adjust: 134.59%; +} + +@font-face { + font-family: "Jost Fallback"; + src: local("Arial"); + ascent-override: 111.45%; + descent-override: 39.06%; + line-gap-override: 0%; + size-adjust: 96.01%; +} diff --git a/packages/fonts/files/ibm-plex-mono-400-latin.woff2 b/packages/fonts/files/ibm-plex-mono-400-latin.woff2 new file mode 100644 index 0000000..52b6c75 Binary files /dev/null and b/packages/fonts/files/ibm-plex-mono-400-latin.woff2 differ diff --git a/packages/fonts/files/ibm-plex-mono-500-latin.woff2 b/packages/fonts/files/ibm-plex-mono-500-latin.woff2 new file mode 100644 index 0000000..3308bce Binary files /dev/null and b/packages/fonts/files/ibm-plex-mono-500-latin.woff2 differ diff --git a/packages/fonts/files/ibm-plex-mono-600-latin.woff2 b/packages/fonts/files/ibm-plex-mono-600-latin.woff2 new file mode 100644 index 0000000..c6759ef Binary files /dev/null and b/packages/fonts/files/ibm-plex-mono-600-latin.woff2 differ diff --git a/packages/fonts/files/ibm-plex-sans-variable-latin.woff2 b/packages/fonts/files/ibm-plex-sans-variable-latin.woff2 new file mode 100644 index 0000000..e5ebabb Binary files /dev/null and b/packages/fonts/files/ibm-plex-sans-variable-latin.woff2 differ diff --git a/packages/fonts/files/jost-400-latin.woff2 b/packages/fonts/files/jost-400-latin.woff2 new file mode 100644 index 0000000..922cde3 Binary files /dev/null and b/packages/fonts/files/jost-400-latin.woff2 differ diff --git a/packages/fonts/files/oswald-600-latin.woff2 b/packages/fonts/files/oswald-600-latin.woff2 new file mode 100644 index 0000000..f63e376 Binary files /dev/null and b/packages/fonts/files/oswald-600-latin.woff2 differ diff --git a/packages/fonts/package.json b/packages/fonts/package.json new file mode 100644 index 0000000..b2bbe66 --- /dev/null +++ b/packages/fonts/package.json @@ -0,0 +1,10 @@ +{ + "name": "@dspack-studio/fonts", + "version": "0.1.0", + "private": true, + "description": "The Aesthetic Function type set, vendored as committed .woff2 files so no build fetches type from a third-party origin. Consumed by apps/web and apps/composer through next/font/local.", + "license": "Apache-2.0 AND OFL-1.1", + "scripts": { + "test": "node verify.mjs" + } +} diff --git a/packages/fonts/verify.mjs b/packages/fonts/verify.mjs new file mode 100644 index 0000000..3e301ea --- /dev/null +++ b/packages/fonts/verify.mjs @@ -0,0 +1,51 @@ +/** + * Re-hash every vendored face against catalog.json — offline, no network. + * + * This is the teeth on the fix it belongs to: the apps stopped fetching type + * from fonts.gstatic.com at build time, so the bytes that used to arrive over + * the wire now live in git. A silent corruption or a well-meaning "re-export" + * would change what the product renders with nothing to catch it. This does. + * + * node packages/fonts/verify.mjs + */ +import fs from "node:fs"; +import path from "node:path"; +import crypto from "node:crypto"; +import { fileURLToPath } from "node:url"; + +const HERE = path.dirname(fileURLToPath(import.meta.url)); +const catalog = JSON.parse(fs.readFileSync(path.join(HERE, "catalog.json"), "utf8")); + +const failures = []; +let bytes = 0; + +for (const face of catalog.faces) { + const file = path.join(HERE, "files", face.file); + if (!fs.existsSync(file)) { + failures.push(`${face.file}: missing`); + continue; + } + const buf = fs.readFileSync(file); + const sha256 = crypto.createHash("sha256").update(buf).digest("hex"); + if (buf.length !== face.bytes) failures.push(`${face.file}: ${buf.length} bytes, catalog says ${face.bytes}`); + else if (sha256 !== face.sha256) failures.push(`${face.file}: sha256 ${sha256}, catalog says ${face.sha256}`); + bytes += buf.length; +} + +// A face on disk that the catalog does not know about is unpinned, and an +// unpinned face is exactly the thing this record exists to prevent. +const known = new Set(catalog.faces.map((f) => f.file)); +for (const file of fs.readdirSync(path.join(HERE, "files"))) { + if (!known.has(file)) failures.push(`${file}: on disk but not in catalog.json`); +} + +if (failures.length > 0) { + console.error("fonts: catalog verification FAILED"); + for (const f of failures) console.error(` - ${f}`); + process.exit(1); +} + +console.log( + `fonts: ${catalog.faces.length} faces verified against catalog.json ` + + `(${(bytes / 1024).toFixed(1)} KB, subset ${catalog.subset}, ${catalog.license.id})` +); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 094ea72..bf8daba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -329,6 +329,8 @@ importers: specifier: ^3.0.0 version: 3.2.7(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.0) + packages/fonts: {} + packages/replay: devDependencies: '@dspack-studio/agui-bridge':