@pro-laico/payload-fonts 0.0.0

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.
Files changed (88) hide show
  1. package/LICENSE.md +22 -0
  2. package/README.md +19 -0
  3. package/dist/access/authd.d.ts +19 -0
  4. package/dist/access/authd.d.ts.map +1 -0
  5. package/dist/access/authd.js +18 -0
  6. package/dist/access/authd.js.map +1 -0
  7. package/dist/collections/font.d.ts +29 -0
  8. package/dist/collections/font.d.ts.map +1 -0
  9. package/dist/collections/font.js +327 -0
  10. package/dist/collections/font.js.map +1 -0
  11. package/dist/collections/fontOptimized.d.ts +20 -0
  12. package/dist/collections/fontOptimized.d.ts.map +1 -0
  13. package/dist/collections/fontOptimized.js +111 -0
  14. package/dist/collections/fontOptimized.js.map +1 -0
  15. package/dist/collections/fontOriginal.d.ts +25 -0
  16. package/dist/collections/fontOriginal.d.ts.map +1 -0
  17. package/dist/collections/fontOriginal.js +49 -0
  18. package/dist/collections/fontOriginal.js.map +1 -0
  19. package/dist/components/DevFonts.d.ts +52 -0
  20. package/dist/components/DevFonts.d.ts.map +1 -0
  21. package/dist/components/DevFonts.js +75 -0
  22. package/dist/components/DevFonts.js.map +1 -0
  23. package/dist/components/admin/FontOriginalUpload.d.ts +15 -0
  24. package/dist/components/admin/FontOriginalUpload.d.ts.map +1 -0
  25. package/dist/components/admin/FontOriginalUpload.js +22 -0
  26. package/dist/components/admin/FontOriginalUpload.js.map +1 -0
  27. package/dist/components/admin/FontOriginalUpload.scss +9 -0
  28. package/dist/endpoints/exportFonts.d.ts +49 -0
  29. package/dist/endpoints/exportFonts.d.ts.map +1 -0
  30. package/dist/endpoints/exportFonts.js +162 -0
  31. package/dist/endpoints/exportFonts.js.map +1 -0
  32. package/dist/extractFonts.d.ts +20 -0
  33. package/dist/extractFonts.d.ts.map +1 -0
  34. package/dist/extractFonts.js +18 -0
  35. package/dist/extractFonts.js.map +1 -0
  36. package/dist/fields/font.d.ts +14 -0
  37. package/dist/fields/font.d.ts.map +1 -0
  38. package/dist/fields/font.js +37 -0
  39. package/dist/fields/font.js.map +1 -0
  40. package/dist/globals/fontSet.d.ts +15 -0
  41. package/dist/globals/fontSet.d.ts.map +1 -0
  42. package/dist/globals/fontSet.js +25 -0
  43. package/dist/globals/fontSet.js.map +1 -0
  44. package/dist/hooks/optimizeFont.d.ts +56 -0
  45. package/dist/hooks/optimizeFont.d.ts.map +1 -0
  46. package/dist/hooks/optimizeFont.js +185 -0
  47. package/dist/hooks/optimizeFont.js.map +1 -0
  48. package/dist/hooks/optimizeFromOriginals.d.ts +43 -0
  49. package/dist/hooks/optimizeFromOriginals.d.ts.map +1 -0
  50. package/dist/hooks/optimizeFromOriginals.js +313 -0
  51. package/dist/hooks/optimizeFromOriginals.js.map +1 -0
  52. package/dist/index.d.ts +8 -0
  53. package/dist/index.d.ts.map +1 -0
  54. package/dist/index.js +14 -0
  55. package/dist/index.js.map +1 -0
  56. package/dist/lib/activeFonts.d.ts +64 -0
  57. package/dist/lib/activeFonts.d.ts.map +1 -0
  58. package/dist/lib/activeFonts.js +139 -0
  59. package/dist/lib/activeFonts.js.map +1 -0
  60. package/dist/lib/families.d.ts +38 -0
  61. package/dist/lib/families.d.ts.map +1 -0
  62. package/dist/lib/families.js +44 -0
  63. package/dist/lib/families.js.map +1 -0
  64. package/dist/lib/mergeConfig.d.ts +28 -0
  65. package/dist/lib/mergeConfig.d.ts.map +1 -0
  66. package/dist/lib/mergeConfig.js +73 -0
  67. package/dist/lib/mergeConfig.js.map +1 -0
  68. package/dist/lib/refs.d.ts +8 -0
  69. package/dist/lib/refs.d.ts.map +1 -0
  70. package/dist/lib/refs.js +11 -0
  71. package/dist/lib/refs.js.map +1 -0
  72. package/dist/lib/uploadBytes.d.ts +26 -0
  73. package/dist/lib/uploadBytes.d.ts.map +1 -0
  74. package/dist/lib/uploadBytes.js +98 -0
  75. package/dist/lib/uploadBytes.js.map +1 -0
  76. package/dist/plugin.d.ts +57 -0
  77. package/dist/plugin.d.ts.map +1 -0
  78. package/dist/plugin.js +93 -0
  79. package/dist/plugin.js.map +1 -0
  80. package/dist/scripts/cli.d.ts +3 -0
  81. package/dist/scripts/cli.d.ts.map +1 -0
  82. package/dist/scripts/cli.js +13 -0
  83. package/dist/scripts/cli.js.map +1 -0
  84. package/dist/scripts/downloadFonts.d.ts +37 -0
  85. package/dist/scripts/downloadFonts.d.ts.map +1 -0
  86. package/dist/scripts/downloadFonts.js +216 -0
  87. package/dist/scripts/downloadFonts.js.map +1 -0
  88. package/package.json +98 -0
@@ -0,0 +1,37 @@
1
+ import { resolveFontFamilies } from "../lib/families.js";
2
+ /**
3
+ * The per-family relationship fields for the `fontSet` global — one slot per configured family
4
+ * (`sans / serif / mono / display` by default), pointing at this package's `font` typeface
5
+ * collection and laid out two-per-row. Used by the standalone `fontSet` global
6
+ * (`globals/fontSet.ts`), and exported so a project assembling its own selection surface (a
7
+ * different global, or a group on an existing collection) can reuse the exact same slots.
8
+ */ export const fontUploadFields = ({ fontSlug = 'font', families } = {})=>{
9
+ const relationTo = fontSlug;
10
+ const resolved = resolveFontFamilies(families);
11
+ // Each slot selects ONE typeface (`font`) whose `family` family matches. A typeface carries its
12
+ // own weight files, which the download generator collapses into a single `next/font/local`
13
+ // declaration (one `src` per file). The slot is a single relationship — the multiplicity
14
+ // lives inside the typeface, not here.
15
+ const slots = resolved.map((family)=>({
16
+ name: family.key,
17
+ label: family.label,
18
+ type: 'relationship',
19
+ relationTo,
20
+ filterOptions: {
21
+ family: {
22
+ equals: family.key
23
+ }
24
+ }
25
+ }));
26
+ // Lay the slots out two per row, matching the original sans/serif | mono/display grid.
27
+ const rows = [];
28
+ for(let i = 0; i < slots.length; i += 2){
29
+ rows.push({
30
+ type: 'row',
31
+ fields: slots.slice(i, i + 2)
32
+ });
33
+ }
34
+ return rows;
35
+ };
36
+
37
+ //# sourceMappingURL=font.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/fields/font.ts"],"sourcesContent":["import type { CollectionSlug, Field } from 'payload'\n\nimport { type FontFamilyConfig, resolveFontFamilies } from '../lib/families'\n\n/**\n * The per-family relationship fields for the `fontSet` global — one slot per configured family\n * (`sans / serif / mono / display` by default), pointing at this package's `font` typeface\n * collection and laid out two-per-row. Used by the standalone `fontSet` global\n * (`globals/fontSet.ts`), and exported so a project assembling its own selection surface (a\n * different global, or a group on an existing collection) can reuse the exact same slots.\n */\nexport const fontUploadFields = ({ fontSlug = 'font', families }: { fontSlug?: string; families?: FontFamilyConfig[] } = {}): Field[] => {\n const relationTo = fontSlug as CollectionSlug\n const resolved = resolveFontFamilies(families)\n // Each slot selects ONE typeface (`font`) whose `family` family matches. A typeface carries its\n // own weight files, which the download generator collapses into a single `next/font/local`\n // declaration (one `src` per file). The slot is a single relationship — the multiplicity\n // lives inside the typeface, not here.\n const slots: Field[] = resolved.map((family) => ({\n name: family.key,\n label: family.label,\n type: 'relationship',\n relationTo,\n filterOptions: { family: { equals: family.key } },\n }))\n\n // Lay the slots out two per row, matching the original sans/serif | mono/display grid.\n const rows: Field[] = []\n for (let i = 0; i < slots.length; i += 2) {\n rows.push({ type: 'row', fields: slots.slice(i, i + 2) })\n }\n return rows\n}\n"],"names":["resolveFontFamilies","fontUploadFields","fontSlug","families","relationTo","resolved","slots","map","family","name","key","label","type","filterOptions","equals","rows","i","length","push","fields","slice"],"mappings":"AAEA,SAAgCA,mBAAmB,QAAQ,qBAAiB;AAE5E;;;;;;CAMC,GACD,OAAO,MAAMC,mBAAmB,CAAC,EAAEC,WAAW,MAAM,EAAEC,QAAQ,EAAwD,GAAG,CAAC,CAAC;IACzH,MAAMC,aAAaF;IACnB,MAAMG,WAAWL,oBAAoBG;IACrC,gGAAgG;IAChG,2FAA2F;IAC3F,yFAAyF;IACzF,uCAAuC;IACvC,MAAMG,QAAiBD,SAASE,GAAG,CAAC,CAACC,SAAY,CAAA;YAC/CC,MAAMD,OAAOE,GAAG;YAChBC,OAAOH,OAAOG,KAAK;YACnBC,MAAM;YACNR;YACAS,eAAe;gBAAEL,QAAQ;oBAAEM,QAAQN,OAAOE,GAAG;gBAAC;YAAE;QAClD,CAAA;IAEA,uFAAuF;IACvF,MAAMK,OAAgB,EAAE;IACxB,IAAK,IAAIC,IAAI,GAAGA,IAAIV,MAAMW,MAAM,EAAED,KAAK,EAAG;QACxCD,KAAKG,IAAI,CAAC;YAAEN,MAAM;YAAOO,QAAQb,MAAMc,KAAK,CAACJ,GAAGA,IAAI;QAAG;IACzD;IACA,OAAOD;AACT,EAAC"}
@@ -0,0 +1,15 @@
1
+ import type { GlobalConfig } from 'payload';
2
+ import type { FontFamilyConfig } from '../lib/families';
3
+ /** Slug of the standalone font-selection global. */
4
+ export declare const FONT_SET_SLUG = "fontSet";
5
+ /**
6
+ * A singleton global holding the active font choices — one slot per configured family (sans /
7
+ * serif / mono / display by default). The fonts export endpoint reads it to resolve which
8
+ * typefaces to ship, so a project can drive `next/font/local` from a CMS-managed selection.
9
+ * Registered by `fontsPlugin()` by default (pass `includeFontSet: false` to skip it).
10
+ */
11
+ export declare const createFontSetGlobal: ({ fontSlug, families, }?: {
12
+ fontSlug?: string;
13
+ families?: FontFamilyConfig[];
14
+ }) => GlobalConfig;
15
+ //# sourceMappingURL=fontSet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fontSet.d.ts","sourceRoot":"","sources":["../../src/globals/fontSet.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAI3C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAEvD,oDAAoD;AACpD,eAAO,MAAM,aAAa,YAAY,CAAA;AAEtC;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAAI,0BAGjC;IACD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,gBAAgB,EAAE,CAAA;CACzB,KAAG,YASP,CAAA"}
@@ -0,0 +1,25 @@
1
+ import { authd } from "../access/authd.js";
2
+ import { fontUploadFields } from "../fields/font.js";
3
+ /** Slug of the standalone font-selection global. */ export const FONT_SET_SLUG = 'fontSet';
4
+ /**
5
+ * A singleton global holding the active font choices — one slot per configured family (sans /
6
+ * serif / mono / display by default). The fonts export endpoint reads it to resolve which
7
+ * typefaces to ship, so a project can drive `next/font/local` from a CMS-managed selection.
8
+ * Registered by `fontsPlugin()` by default (pass `includeFontSet: false` to skip it).
9
+ */ export const createFontSetGlobal = ({ fontSlug = 'font', families } = {})=>({
10
+ slug: FONT_SET_SLUG,
11
+ admin: {
12
+ group: 'Assets',
13
+ description: 'Choose which uploaded typeface fills each slot. This is what actually puts a font on your site — upload in Font first, then activate it here.'
14
+ },
15
+ access: {
16
+ read: authd,
17
+ update: authd
18
+ },
19
+ fields: fontUploadFields({
20
+ fontSlug,
21
+ families
22
+ })
23
+ });
24
+
25
+ //# sourceMappingURL=fontSet.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/globals/fontSet.ts"],"sourcesContent":["import type { GlobalConfig } from 'payload'\n\nimport { authd } from '../access/authd'\nimport { fontUploadFields } from '../fields/font'\nimport type { FontFamilyConfig } from '../lib/families'\n\n/** Slug of the standalone font-selection global. */\nexport const FONT_SET_SLUG = 'fontSet'\n\n/**\n * A singleton global holding the active font choices — one slot per configured family (sans /\n * serif / mono / display by default). The fonts export endpoint reads it to resolve which\n * typefaces to ship, so a project can drive `next/font/local` from a CMS-managed selection.\n * Registered by `fontsPlugin()` by default (pass `includeFontSet: false` to skip it).\n */\nexport const createFontSetGlobal = ({\n fontSlug = 'font',\n families,\n}: {\n fontSlug?: string\n families?: FontFamilyConfig[]\n} = {}): GlobalConfig => ({\n slug: FONT_SET_SLUG,\n admin: {\n group: 'Assets',\n description:\n 'Choose which uploaded typeface fills each slot. This is what actually puts a font on your site — upload in Font first, then activate it here.',\n },\n access: { read: authd, update: authd },\n fields: fontUploadFields({ fontSlug, families }),\n})\n"],"names":["authd","fontUploadFields","FONT_SET_SLUG","createFontSetGlobal","fontSlug","families","slug","admin","group","description","access","read","update","fields"],"mappings":"AAEA,SAASA,KAAK,QAAQ,qBAAiB;AACvC,SAASC,gBAAgB,QAAQ,oBAAgB;AAGjD,kDAAkD,GAClD,OAAO,MAAMC,gBAAgB,UAAS;AAEtC;;;;;CAKC,GACD,OAAO,MAAMC,sBAAsB,CAAC,EAClCC,WAAW,MAAM,EACjBC,QAAQ,EAIT,GAAG,CAAC,CAAC,GAAoB,CAAA;QACxBC,MAAMJ;QACNK,OAAO;YACLC,OAAO;YACPC,aACE;QACJ;QACAC,QAAQ;YAAEC,MAAMX;YAAOY,QAAQZ;QAAM;QACrCa,QAAQZ,iBAAiB;YAAEG;YAAUC;QAAS;IAChD,CAAA,EAAE"}
@@ -0,0 +1,56 @@
1
+ /** The subsetter charset: a built-in preset name, or an explicit string of characters to keep. */
2
+ export type Charset = 'latin' | 'latin-ext' | (string & {});
3
+ /**
4
+ * Resolve a charset option to the set of characters the subsetter should keep. A known
5
+ * preset name ('latin' | 'latin-ext') expands to its ranges; any other string is treated
6
+ * as an explicit, verbatim list of characters to retain.
7
+ */
8
+ export declare const resolveCharsetText: (charset?: Charset) => string;
9
+ /** What {@link detectMetadata} reads out of a font binary. */
10
+ export interface FontFileMetadata {
11
+ familyName?: string;
12
+ weight?: string;
13
+ style?: 'normal' | 'italic';
14
+ isVariable: boolean;
15
+ /** The file ALSO carries italics through a variation axis (an `ital` axis reaching 1, or a
16
+ * negative `slnt` range) while its default instance is upright — one file, both styles. */
17
+ italCapable?: boolean;
18
+ /** For `slnt`-based italics: the positive CSS oblique angle (deg) matching the axis extreme. */
19
+ obliqueAngle?: number;
20
+ }
21
+ /**
22
+ * Best-effort read of weight / style / family from a font's metadata via the bundled
23
+ * `fontkit` dependency. Returns `null` when the font can't be parsed.
24
+ *
25
+ * A `wght` variation axis marks a variable font: we keep its full `min max` range (e.g.
26
+ * `'100 900'`) — exactly what `next/font/local` expects — instead of clamping to the
27
+ * single default-instance weight class as we do for static fonts.
28
+ *
29
+ * An upright variable file whose axes ALSO cover italics — a true `ital` axis reaching 1, or a
30
+ * `slnt` axis leaning forward (negative min) — is flagged `italCapable`, so the serving layers
31
+ * can emit an italic `@font-face` from the same file (CSS maps `font-style: italic` onto `ital`
32
+ * and `font-style: oblique <angle>` onto `slnt`).
33
+ */
34
+ export declare function detectMetadata(buffer: Buffer): Promise<FontFileMetadata | null>;
35
+ /**
36
+ * Subset a font buffer to a WOFF2 using the bundled `subset-font` (harfbuzz). Variable
37
+ * axes are preserved (harfbuzz keeps `fvar`/`gvar` unless axes are explicitly pinned), so
38
+ * a variable font stays variable through the subset.
39
+ */
40
+ export declare function subsetToWoff2(buffer: Buffer, charsetText: string): Promise<Buffer>;
41
+ /**
42
+ * The most common deployment mistake: a bundler (Next/Turbopack) bundles the harfbuzz / fontkit
43
+ * wasm + native assets, rewriting their load paths to virtual ones that don't exist on disk. The
44
+ * subset then throws at runtime, fonts upload but never get subsetted, and nothing is served.
45
+ * True when an error is that load failure (vs. an ordinary bad-font error).
46
+ */
47
+ export declare const isSubsetterLoadError: (err: unknown) => boolean;
48
+ /**
49
+ * Boot-time probe for that mistake. Importing `subset-font` is NOT enough to detect it — harfbuzz
50
+ * reads its wasm lazily (`_.once`) on the FIRST SUBSET CALL — so this runs a real subset on a
51
+ * garbage buffer: the wasm loads (and fails loudly if bundled) before the buffer is ever parsed.
52
+ * Resolves `null` when the subsetter is healthy — a bad-font error from the garbage buffer proves
53
+ * the wasm loaded — or the load error for the caller to report.
54
+ */
55
+ export declare function probeSubsetter(): Promise<unknown | null>;
56
+ //# sourceMappingURL=optimizeFont.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"optimizeFont.d.ts","sourceRoot":"","sources":["../../src/hooks/optimizeFont.ts"],"names":[],"mappings":"AAiCA,kGAAkG;AAClG,MAAM,MAAM,OAAO,GAAG,OAAO,GAAG,WAAW,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;AAQ3D;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,UAAS,OAAiB,KAAG,MAG/D,CAAA;AAqBD,8DAA8D;AAC9D,MAAM,WAAW,gBAAgB;IAC/B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAA;IAC3B,UAAU,EAAE,OAAO,CAAA;IACnB;gGAC4F;IAC5F,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,gGAAgG;IAChG,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAwCrF;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGxF;AAED;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,OAAO,KAAG,OAMnD,CAAA;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,IAAI,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAO9D"}
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Built-in charset presets, expressed as inclusive Unicode code-point ranges. Each
3
+ * preset is expanded to a string of characters that the subsetter keeps; everything
4
+ * else is dropped to shrink the file.
5
+ */ const CHARSET_PRESETS = {
6
+ // Printable ASCII + Latin-1 Supplement (Western-European accents: é ñ ü ç …) plus the
7
+ // typographic punctuation CMS/word-processor content relies on (curly quotes, en/em
8
+ // dashes, ellipsis, €, ™) so common copy doesn't fall back.
9
+ latin: [
10
+ [
11
+ 0x20,
12
+ 0x7e
13
+ ],
14
+ [
15
+ 0xa0,
16
+ 0xff
17
+ ],
18
+ [
19
+ 0x2010,
20
+ 0x2014
21
+ ],
22
+ [
23
+ 0x2018,
24
+ 0x201f
25
+ ],
26
+ [
27
+ 0x2022,
28
+ 0x2022
29
+ ],
30
+ [
31
+ 0x2026,
32
+ 0x2026
33
+ ],
34
+ [
35
+ 0x20ac,
36
+ 0x20ac
37
+ ],
38
+ [
39
+ 0x2122,
40
+ 0x2122
41
+ ]
42
+ ],
43
+ // Everything in `latin` plus Latin Extended-A/B (Central/Eastern-European: ą ž ł ő …).
44
+ 'latin-ext': [
45
+ [
46
+ 0x20,
47
+ 0x7e
48
+ ],
49
+ [
50
+ 0xa0,
51
+ 0xff
52
+ ],
53
+ [
54
+ 0x100,
55
+ 0x24f
56
+ ],
57
+ [
58
+ 0x2010,
59
+ 0x2014
60
+ ],
61
+ [
62
+ 0x2018,
63
+ 0x201f
64
+ ],
65
+ [
66
+ 0x2022,
67
+ 0x2022
68
+ ],
69
+ [
70
+ 0x2026,
71
+ 0x2026
72
+ ],
73
+ [
74
+ 0x20ac,
75
+ 0x20ac
76
+ ],
77
+ [
78
+ 0x2122,
79
+ 0x2122
80
+ ]
81
+ ]
82
+ };
83
+ const rangesToText = (ranges)=>{
84
+ let text = '';
85
+ for (const [start, end] of ranges)for(let cp = start; cp <= end; cp++)text += String.fromCodePoint(cp);
86
+ return text;
87
+ };
88
+ /**
89
+ * Resolve a charset option to the set of characters the subsetter should keep. A known
90
+ * preset name ('latin' | 'latin-ext') expands to its ranges; any other string is treated
91
+ * as an explicit, verbatim list of characters to retain.
92
+ */ export const resolveCharsetText = (charset = 'latin')=>{
93
+ const preset = CHARSET_PRESETS[charset];
94
+ return preset ? rangesToText(preset) : charset;
95
+ };
96
+ /** Clamp an arbitrary OS/2 weight class to the nearest standard 100–900 step. */ const normalizeWeight = (weight)=>{
97
+ if (!weight || Number.isNaN(weight)) return undefined;
98
+ return String(Math.min(900, Math.max(100, Math.round(weight / 100) * 100)));
99
+ };
100
+ /**
101
+ * Best-effort read of weight / style / family from a font's metadata via the bundled
102
+ * `fontkit` dependency. Returns `null` when the font can't be parsed.
103
+ *
104
+ * A `wght` variation axis marks a variable font: we keep its full `min max` range (e.g.
105
+ * `'100 900'`) — exactly what `next/font/local` expects — instead of clamping to the
106
+ * single default-instance weight class as we do for static fonts.
107
+ *
108
+ * An upright variable file whose axes ALSO cover italics — a true `ital` axis reaching 1, or a
109
+ * `slnt` axis leaning forward (negative min) — is flagged `italCapable`, so the serving layers
110
+ * can emit an italic `@font-face` from the same file (CSS maps `font-style: italic` onto `ital`
111
+ * and `font-style: oblique <angle>` onto `slnt`).
112
+ */ export async function detectMetadata(buffer) {
113
+ try {
114
+ // fontkit v2 ships `create` as a NAMED export under node's ESM interop (no usable
115
+ // `default`), but bundlers may synthesize a default — accept either, or detection
116
+ // silently no-ops and weight/style/family never autofill.
117
+ const mod = await import("fontkit");
118
+ const create = mod.create ?? mod.default?.create;
119
+ if (!create) return null;
120
+ const font = create(buffer);
121
+ const italic = Boolean(typeof font.italicAngle === 'number' && font.italicAngle !== 0 || (font['OS/2']?.fsSelection ?? 0) & 0x01 || /italic|oblique/i.test(font.subfamilyName ?? ''));
122
+ const wght = font.variationAxes?.wght;
123
+ const weight = wght ? `${Math.round(wght.min)} ${Math.round(wght.max)}` : normalizeWeight(font['OS/2']?.usWeightClass);
124
+ let italCapable;
125
+ let obliqueAngle;
126
+ if (!italic) {
127
+ const ital = font.variationAxes?.ital;
128
+ const slnt = font.variationAxes?.slnt;
129
+ if (ital && ital.max >= 1) italCapable = true;
130
+ else if (slnt && slnt.min < 0) {
131
+ italCapable = true;
132
+ obliqueAngle = Math.round(Math.abs(slnt.min));
133
+ }
134
+ }
135
+ return {
136
+ familyName: font.familyName ?? undefined,
137
+ weight,
138
+ style: italic ? 'italic' : 'normal',
139
+ isVariable: Boolean(wght),
140
+ ...italCapable ? {
141
+ italCapable
142
+ } : {},
143
+ ...obliqueAngle ? {
144
+ obliqueAngle
145
+ } : {}
146
+ };
147
+ } catch {
148
+ return null;
149
+ }
150
+ }
151
+ /**
152
+ * Subset a font buffer to a WOFF2 using the bundled `subset-font` (harfbuzz). Variable
153
+ * axes are preserved (harfbuzz keeps `fvar`/`gvar` unless axes are explicitly pinned), so
154
+ * a variable font stays variable through the subset.
155
+ */ export async function subsetToWoff2(buffer, charsetText) {
156
+ const subsetFont = (await import("subset-font")).default;
157
+ return subsetFont(buffer, charsetText, {
158
+ targetFormat: 'woff2'
159
+ });
160
+ }
161
+ /**
162
+ * The most common deployment mistake: a bundler (Next/Turbopack) bundles the harfbuzz / fontkit
163
+ * wasm + native assets, rewriting their load paths to virtual ones that don't exist on disk. The
164
+ * subset then throws at runtime, fonts upload but never get subsetted, and nothing is served.
165
+ * True when an error is that load failure (vs. an ordinary bad-font error).
166
+ */ export const isSubsetterLoadError = (err)=>{
167
+ const msg = err instanceof Error ? `${err.message}\n${err.stack ?? ''}` : String(err);
168
+ return /(hb-subset\.wasm|harfbuzzjs|subset-font|fontkit)/i.test(msg) && /(ENOENT|no such file|cannot find module|failed to load|MODULE_NOT_FOUND)/i.test(msg);
169
+ };
170
+ /**
171
+ * Boot-time probe for that mistake. Importing `subset-font` is NOT enough to detect it — harfbuzz
172
+ * reads its wasm lazily (`_.once`) on the FIRST SUBSET CALL — so this runs a real subset on a
173
+ * garbage buffer: the wasm loads (and fails loudly if bundled) before the buffer is ever parsed.
174
+ * Resolves `null` when the subsetter is healthy — a bad-font error from the garbage buffer proves
175
+ * the wasm loaded — or the load error for the caller to report.
176
+ */ export async function probeSubsetter() {
177
+ try {
178
+ await subsetToWoff2(Buffer.from('not a font'), 'a');
179
+ } catch (err) {
180
+ if (isSubsetterLoadError(err)) return err;
181
+ }
182
+ return null;
183
+ }
184
+
185
+ //# sourceMappingURL=optimizeFont.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/optimizeFont.ts"],"sourcesContent":["/**\n * Built-in charset presets, expressed as inclusive Unicode code-point ranges. Each\n * preset is expanded to a string of characters that the subsetter keeps; everything\n * else is dropped to shrink the file.\n */\nconst CHARSET_PRESETS: Record<string, Array<[number, number]>> = {\n // Printable ASCII + Latin-1 Supplement (Western-European accents: é ñ ü ç …) plus the\n // typographic punctuation CMS/word-processor content relies on (curly quotes, en/em\n // dashes, ellipsis, €, ™) so common copy doesn't fall back.\n latin: [\n [0x20, 0x7e],\n [0xa0, 0xff],\n [0x2010, 0x2014],\n [0x2018, 0x201f],\n [0x2022, 0x2022],\n [0x2026, 0x2026],\n [0x20ac, 0x20ac],\n [0x2122, 0x2122],\n ],\n // Everything in `latin` plus Latin Extended-A/B (Central/Eastern-European: ą ž ł ő …).\n 'latin-ext': [\n [0x20, 0x7e],\n [0xa0, 0xff],\n [0x100, 0x24f],\n [0x2010, 0x2014],\n [0x2018, 0x201f],\n [0x2022, 0x2022],\n [0x2026, 0x2026],\n [0x20ac, 0x20ac],\n [0x2122, 0x2122],\n ],\n}\n\n/** The subsetter charset: a built-in preset name, or an explicit string of characters to keep. */\nexport type Charset = 'latin' | 'latin-ext' | (string & {})\n\nconst rangesToText = (ranges: Array<[number, number]>): string => {\n let text = ''\n for (const [start, end] of ranges) for (let cp = start; cp <= end; cp++) text += String.fromCodePoint(cp)\n return text\n}\n\n/**\n * Resolve a charset option to the set of characters the subsetter should keep. A known\n * preset name ('latin' | 'latin-ext') expands to its ranges; any other string is treated\n * as an explicit, verbatim list of characters to retain.\n */\nexport const resolveCharsetText = (charset: Charset = 'latin'): string => {\n const preset = CHARSET_PRESETS[charset]\n return preset ? rangesToText(preset) : charset\n}\n\n/** The slice of fontkit's `Font` we read for weight/style/family detection. */\ntype FontkitFont = {\n familyName?: string | null\n subfamilyName?: string | null\n italicAngle?: number\n /** Variation axes by tag (variable fonts only); we read `wght` for the weight range. */\n variationAxes?: Record<string, { min: number; default: number; max: number }> | null\n 'OS/2'?: { usWeightClass?: number; fsSelection?: number } | null\n}\n\n/** subset-font's default export: subset + convert in one call. */\ntype SubsetFontFn = (buffer: Buffer, text: string, options: { targetFormat: 'woff2' | 'woff' | 'sfnt' }) => Promise<Buffer>\n\n/** Clamp an arbitrary OS/2 weight class to the nearest standard 100–900 step. */\nconst normalizeWeight = (weight?: number): string | undefined => {\n if (!weight || Number.isNaN(weight)) return undefined\n return String(Math.min(900, Math.max(100, Math.round(weight / 100) * 100)))\n}\n\n/** What {@link detectMetadata} reads out of a font binary. */\nexport interface FontFileMetadata {\n familyName?: string\n weight?: string\n style?: 'normal' | 'italic'\n isVariable: boolean\n /** The file ALSO carries italics through a variation axis (an `ital` axis reaching 1, or a\n * negative `slnt` range) while its default instance is upright — one file, both styles. */\n italCapable?: boolean\n /** For `slnt`-based italics: the positive CSS oblique angle (deg) matching the axis extreme. */\n obliqueAngle?: number\n}\n\n/**\n * Best-effort read of weight / style / family from a font's metadata via the bundled\n * `fontkit` dependency. Returns `null` when the font can't be parsed.\n *\n * A `wght` variation axis marks a variable font: we keep its full `min max` range (e.g.\n * `'100 900'`) — exactly what `next/font/local` expects — instead of clamping to the\n * single default-instance weight class as we do for static fonts.\n *\n * An upright variable file whose axes ALSO cover italics — a true `ital` axis reaching 1, or a\n * `slnt` axis leaning forward (negative min) — is flagged `italCapable`, so the serving layers\n * can emit an italic `@font-face` from the same file (CSS maps `font-style: italic` onto `ital`\n * and `font-style: oblique <angle>` onto `slnt`).\n */\nexport async function detectMetadata(buffer: Buffer): Promise<FontFileMetadata | null> {\n try {\n // fontkit v2 ships `create` as a NAMED export under node's ESM interop (no usable\n // `default`), but bundlers may synthesize a default — accept either, or detection\n // silently no-ops and weight/style/family never autofill.\n const mod = (await import('fontkit')) as unknown as { create?: (b: Buffer) => unknown; default?: { create?: (b: Buffer) => unknown } }\n const create = mod.create ?? mod.default?.create\n if (!create) return null\n const font = create(buffer) as FontkitFont\n const italic = Boolean(\n (typeof font.italicAngle === 'number' && font.italicAngle !== 0) ||\n (font['OS/2']?.fsSelection ?? 0) & 0x01 ||\n /italic|oblique/i.test(font.subfamilyName ?? ''),\n )\n const wght = font.variationAxes?.wght\n const weight = wght ? `${Math.round(wght.min)} ${Math.round(wght.max)}` : normalizeWeight(font['OS/2']?.usWeightClass)\n\n let italCapable: boolean | undefined\n let obliqueAngle: number | undefined\n if (!italic) {\n const ital = font.variationAxes?.ital\n const slnt = font.variationAxes?.slnt\n if (ital && ital.max >= 1) italCapable = true\n else if (slnt && slnt.min < 0) {\n italCapable = true\n obliqueAngle = Math.round(Math.abs(slnt.min))\n }\n }\n\n return {\n familyName: font.familyName ?? undefined,\n weight,\n style: italic ? 'italic' : 'normal',\n isVariable: Boolean(wght),\n ...(italCapable ? { italCapable } : {}),\n ...(obliqueAngle ? { obliqueAngle } : {}),\n }\n } catch {\n return null\n }\n}\n\n/**\n * Subset a font buffer to a WOFF2 using the bundled `subset-font` (harfbuzz). Variable\n * axes are preserved (harfbuzz keeps `fvar`/`gvar` unless axes are explicitly pinned), so\n * a variable font stays variable through the subset.\n */\nexport async function subsetToWoff2(buffer: Buffer, charsetText: string): Promise<Buffer> {\n const subsetFont = (await import('subset-font')).default as unknown as SubsetFontFn\n return subsetFont(buffer, charsetText, { targetFormat: 'woff2' })\n}\n\n/**\n * The most common deployment mistake: a bundler (Next/Turbopack) bundles the harfbuzz / fontkit\n * wasm + native assets, rewriting their load paths to virtual ones that don't exist on disk. The\n * subset then throws at runtime, fonts upload but never get subsetted, and nothing is served.\n * True when an error is that load failure (vs. an ordinary bad-font error).\n */\nexport const isSubsetterLoadError = (err: unknown): boolean => {\n const msg = err instanceof Error ? `${err.message}\\n${err.stack ?? ''}` : String(err)\n return (\n /(hb-subset\\.wasm|harfbuzzjs|subset-font|fontkit)/i.test(msg) &&\n /(ENOENT|no such file|cannot find module|failed to load|MODULE_NOT_FOUND)/i.test(msg)\n )\n}\n\n/**\n * Boot-time probe for that mistake. Importing `subset-font` is NOT enough to detect it — harfbuzz\n * reads its wasm lazily (`_.once`) on the FIRST SUBSET CALL — so this runs a real subset on a\n * garbage buffer: the wasm loads (and fails loudly if bundled) before the buffer is ever parsed.\n * Resolves `null` when the subsetter is healthy — a bad-font error from the garbage buffer proves\n * the wasm loaded — or the load error for the caller to report.\n */\nexport async function probeSubsetter(): Promise<unknown | null> {\n try {\n await subsetToWoff2(Buffer.from('not a font'), 'a')\n } catch (err) {\n if (isSubsetterLoadError(err)) return err\n }\n return null\n}\n"],"names":["CHARSET_PRESETS","latin","rangesToText","ranges","text","start","end","cp","String","fromCodePoint","resolveCharsetText","charset","preset","normalizeWeight","weight","Number","isNaN","undefined","Math","min","max","round","detectMetadata","buffer","mod","create","default","font","italic","Boolean","italicAngle","fsSelection","test","subfamilyName","wght","variationAxes","usWeightClass","italCapable","obliqueAngle","ital","slnt","abs","familyName","style","isVariable","subsetToWoff2","charsetText","subsetFont","targetFormat","isSubsetterLoadError","err","msg","Error","message","stack","probeSubsetter","Buffer","from"],"mappings":"AAAA;;;;CAIC,GACD,MAAMA,kBAA2D;IAC/D,sFAAsF;IACtF,oFAAoF;IACpF,4DAA4D;IAC5DC,OAAO;QACL;YAAC;YAAM;SAAK;QACZ;YAAC;YAAM;SAAK;QACZ;YAAC;YAAQ;SAAO;QAChB;YAAC;YAAQ;SAAO;QAChB;YAAC;YAAQ;SAAO;QAChB;YAAC;YAAQ;SAAO;QAChB;YAAC;YAAQ;SAAO;QAChB;YAAC;YAAQ;SAAO;KACjB;IACD,uFAAuF;IACvF,aAAa;QACX;YAAC;YAAM;SAAK;QACZ;YAAC;YAAM;SAAK;QACZ;YAAC;YAAO;SAAM;QACd;YAAC;YAAQ;SAAO;QAChB;YAAC;YAAQ;SAAO;QAChB;YAAC;YAAQ;SAAO;QAChB;YAAC;YAAQ;SAAO;QAChB;YAAC;YAAQ;SAAO;QAChB;YAAC;YAAQ;SAAO;KACjB;AACH;AAKA,MAAMC,eAAe,CAACC;IACpB,IAAIC,OAAO;IACX,KAAK,MAAM,CAACC,OAAOC,IAAI,IAAIH,OAAQ,IAAK,IAAII,KAAKF,OAAOE,MAAMD,KAAKC,KAAMH,QAAQI,OAAOC,aAAa,CAACF;IACtG,OAAOH;AACT;AAEA;;;;CAIC,GACD,OAAO,MAAMM,qBAAqB,CAACC,UAAmB,OAAO;IAC3D,MAAMC,SAASZ,eAAe,CAACW,QAAQ;IACvC,OAAOC,SAASV,aAAaU,UAAUD;AACzC,EAAC;AAeD,+EAA+E,GAC/E,MAAME,kBAAkB,CAACC;IACvB,IAAI,CAACA,UAAUC,OAAOC,KAAK,CAACF,SAAS,OAAOG;IAC5C,OAAOT,OAAOU,KAAKC,GAAG,CAAC,KAAKD,KAAKE,GAAG,CAAC,KAAKF,KAAKG,KAAK,CAACP,SAAS,OAAO;AACvE;AAeA;;;;;;;;;;;;CAYC,GACD,OAAO,eAAeQ,eAAeC,MAAc;IACjD,IAAI;QACF,kFAAkF;QAClF,kFAAkF;QAClF,0DAA0D;QAC1D,MAAMC,MAAO,MAAM,MAAM,CAAC;QAC1B,MAAMC,SAASD,IAAIC,MAAM,IAAID,IAAIE,OAAO,EAAED;QAC1C,IAAI,CAACA,QAAQ,OAAO;QACpB,MAAME,OAAOF,OAAOF;QACpB,MAAMK,SAASC,QACb,AAAC,OAAOF,KAAKG,WAAW,KAAK,YAAYH,KAAKG,WAAW,KAAK,KAC5D,AAACH,CAAAA,IAAI,CAAC,OAAO,EAAEI,eAAe,CAAA,IAAK,QACnC,kBAAkBC,IAAI,CAACL,KAAKM,aAAa,IAAI;QAEjD,MAAMC,OAAOP,KAAKQ,aAAa,EAAED;QACjC,MAAMpB,SAASoB,OAAO,GAAGhB,KAAKG,KAAK,CAACa,KAAKf,GAAG,EAAE,CAAC,EAAED,KAAKG,KAAK,CAACa,KAAKd,GAAG,GAAG,GAAGP,gBAAgBc,IAAI,CAAC,OAAO,EAAES;QAExG,IAAIC;QACJ,IAAIC;QACJ,IAAI,CAACV,QAAQ;YACX,MAAMW,OAAOZ,KAAKQ,aAAa,EAAEI;YACjC,MAAMC,OAAOb,KAAKQ,aAAa,EAAEK;YACjC,IAAID,QAAQA,KAAKnB,GAAG,IAAI,GAAGiB,cAAc;iBACpC,IAAIG,QAAQA,KAAKrB,GAAG,GAAG,GAAG;gBAC7BkB,cAAc;gBACdC,eAAepB,KAAKG,KAAK,CAACH,KAAKuB,GAAG,CAACD,KAAKrB,GAAG;YAC7C;QACF;QAEA,OAAO;YACLuB,YAAYf,KAAKe,UAAU,IAAIzB;YAC/BH;YACA6B,OAAOf,SAAS,WAAW;YAC3BgB,YAAYf,QAAQK;YACpB,GAAIG,cAAc;gBAAEA;YAAY,IAAI,CAAC,CAAC;YACtC,GAAIC,eAAe;gBAAEA;YAAa,IAAI,CAAC,CAAC;QAC1C;IACF,EAAE,OAAM;QACN,OAAO;IACT;AACF;AAEA;;;;CAIC,GACD,OAAO,eAAeO,cAActB,MAAc,EAAEuB,WAAmB;IACrE,MAAMC,aAAa,AAAC,CAAA,MAAM,MAAM,CAAC,cAAa,EAAGrB,OAAO;IACxD,OAAOqB,WAAWxB,QAAQuB,aAAa;QAAEE,cAAc;IAAQ;AACjE;AAEA;;;;;CAKC,GACD,OAAO,MAAMC,uBAAuB,CAACC;IACnC,MAAMC,MAAMD,eAAeE,QAAQ,GAAGF,IAAIG,OAAO,CAAC,EAAE,EAAEH,IAAII,KAAK,IAAI,IAAI,GAAG9C,OAAO0C;IACjF,OACE,oDAAoDlB,IAAI,CAACmB,QACzD,4EAA4EnB,IAAI,CAACmB;AAErF,EAAC;AAED;;;;;;CAMC,GACD,OAAO,eAAeI;IACpB,IAAI;QACF,MAAMV,cAAcW,OAAOC,IAAI,CAAC,eAAe;IACjD,EAAE,OAAOP,KAAK;QACZ,IAAID,qBAAqBC,MAAM,OAAOA;IACxC;IACA,OAAO;AACT"}
@@ -0,0 +1,43 @@
1
+ import type { CollectionAfterChangeHook, CollectionBeforeDeleteHook } from 'payload';
2
+ import { type Charset } from './optimizeFont';
3
+ /**
4
+ * Every `fontOriginal` id a typeface doc references across all of its slots. Each original
5
+ * belongs to exactly one typeface — enforced by the create-only upload slots (UI) and the
6
+ * `rejectSharedOriginals` guard (data layer) — so cleanup can delete a de-referenced or
7
+ * deleted original outright, with no shared-original / concurrent-delete hazard.
8
+ */
9
+ export declare const originalIdsFromDoc: (data: Record<string, unknown>) => Array<string | number>;
10
+ export interface OptimizeFromOriginalsOptions {
11
+ /** Characters to keep, or a preset name ('latin' | 'latin-ext'). Default 'latin'. */
12
+ charset?: Charset;
13
+ /** Slug of the archival original collection. Default 'fontOriginal'. */
14
+ originalSlug?: string;
15
+ /** Slug of the optimized (served) collection. Default 'fontOptimized'. */
16
+ optimizedSlug?: string;
17
+ }
18
+ /**
19
+ * `afterChange` for the `Font` typeface: reconcile its served `fontOptimized` files
20
+ * against the `fontOriginal` files referenced in its slots.
21
+ *
22
+ * Keyed by source-original id: a newly-referenced original is fetched, subsetted to WOFF2
23
+ * (variable weight range read from the `wght` axis; static weight/style from the row) and
24
+ * created; an original that's gone has its optimized deleted; an unchanged original is kept
25
+ * (only its weight/style metadata is synced if the row changed). Best-effort — a single bad
26
+ * font logs a warning and is skipped, never failing the typeface save. Touches only
27
+ * `fontOptimized`, so it can't re-trigger this hook.
28
+ */
29
+ export declare const optimizeFromOriginalsHook: (opts?: OptimizeFromOriginalsOptions) => CollectionAfterChangeHook;
30
+ /**
31
+ * `beforeDelete` for the `Font` typeface: cascade-delete its served `fontOptimized` files and
32
+ * the `fontOriginal` files its slots referenced, so nothing orphans in storage. Best-effort.
33
+ *
34
+ * It runs `beforeDelete` (not after) on purpose: deleting the `font` doc triggers Payload's
35
+ * dangling-reference cleanup, which nulls `fontOptimized.font` — so by `afterDelete` the served
36
+ * files can no longer be found by their owning typeface. Here the relationship is still intact,
37
+ * so we resolve them by `font` first (and read the originals off the doc's own upload slots).
38
+ */
39
+ export declare const cleanupFontAssetsHook: (opts?: {
40
+ originalSlug?: string;
41
+ optimizedSlug?: string;
42
+ }) => CollectionBeforeDeleteHook;
43
+ //# sourceMappingURL=optimizeFromOriginals.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"optimizeFromOriginals.d.ts","sourceRoot":"","sources":["../../src/hooks/optimizeFromOriginals.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,0BAA0B,EAAkB,MAAM,SAAS,CAAA;AAIpG,OAAO,EAAE,KAAK,OAAO,EAA2E,MAAM,gBAAgB,CAAA;AAatH;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,KAAK,CAAC,MAAM,GAAG,MAAM,CAYvF,CAAA;AAgCD,MAAM,WAAW,4BAA4B;IAC3C,qFAAqF;IACrF,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,wEAAwE;IACxE,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,yBAAyB,GAAI,OAAM,4BAAiC,KAAG,yBAoInF,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,GAAI,OAAM;IAAE,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAO,KAAG,0BA+CpG,CAAA"}