@liiift-studio/sanity-font-manager 2.7.0 → 2.7.1

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 (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +695 -437
  3. package/dist/index.js +1634 -17839
  4. package/dist/index.mjs +1551 -17745
  5. package/package.json +83 -83
  6. package/src/components/BatchUploadFonts.jsx +655 -655
  7. package/src/components/BulkActions.jsx +99 -99
  8. package/src/components/ExistingDocumentResolver.jsx +152 -152
  9. package/src/components/FontReviewCard.jsx +455 -455
  10. package/src/components/FontScriptUploaderComponent.jsx +463 -463
  11. package/src/components/GenerateCollectionsPairsComponent.jsx +259 -259
  12. package/src/components/KeyValueInput.jsx +95 -95
  13. package/src/components/KeyValueReferenceInput.jsx +267 -267
  14. package/src/components/NestedObjectArraySelector.jsx +146 -146
  15. package/src/components/PriceInput.jsx +26 -26
  16. package/src/components/PrimaryCollectionGeneratorTypeface.jsx +116 -116
  17. package/src/components/RegenerateSubfamiliesComponent.jsx +185 -185
  18. package/src/components/SetOTF.jsx +87 -87
  19. package/src/components/SingleUploaderTool.jsx +674 -674
  20. package/src/components/StatusDisplay.jsx +26 -26
  21. package/src/components/StyleCountInput.jsx +16 -16
  22. package/src/components/UpdateScriptsComponent.jsx +76 -76
  23. package/src/components/UploadButton.jsx +43 -43
  24. package/src/components/UploadModal.jsx +309 -309
  25. package/src/components/UploadScriptsComponent.jsx +539 -539
  26. package/src/components/UploadStep1Settings.jsx +272 -272
  27. package/src/components/UploadStep2Review.jsx +478 -478
  28. package/src/components/UploadStep3Execute.jsx +234 -234
  29. package/src/components/UploadStep3bInstances.jsx +396 -396
  30. package/src/components/UploadSummary.jsx +196 -196
  31. package/src/components/VariableInstanceReferencesInput.jsx +190 -190
  32. package/src/hooks/useNestedObjects.js +92 -92
  33. package/src/hooks/useSanityClient.js +9 -9
  34. package/src/index.js +120 -120
  35. package/src/schema/openTypeField.js +1995 -1995
  36. package/src/schema/styleCountField.js +12 -12
  37. package/src/schema/stylesField.js +302 -302
  38. package/src/schema/stylisticSetField.js +301 -301
  39. package/src/utils/buildUploadPlan.js +326 -326
  40. package/src/utils/executeUploadPlan.js +430 -430
  41. package/src/utils/executionReducer.js +56 -56
  42. package/src/utils/fontHelpers.js +281 -281
  43. package/src/utils/generateCssFile.js +207 -207
  44. package/src/utils/generateFontData.js +98 -98
  45. package/src/utils/generateFontFile.js +38 -38
  46. package/src/utils/generateKeywords.js +185 -185
  47. package/src/utils/generateSubset.js +45 -45
  48. package/src/utils/getEmptyFontKit.js +101 -101
  49. package/src/utils/parseFont.js +56 -56
  50. package/src/utils/parseVariableFontInstances.js +301 -301
  51. package/src/utils/planReducer.js +531 -531
  52. package/src/utils/planTypes.js +183 -183
  53. package/src/utils/processFontFiles.js +530 -530
  54. package/src/utils/regenerateFontData.js +146 -146
  55. package/src/utils/resolveExistingFont.js +87 -87
  56. package/src/utils/retitleFontEntries.js +154 -154
  57. package/src/utils/sanitizeForSanityId.js +65 -65
  58. package/src/utils/setupDecompressors.js +27 -27
  59. package/src/utils/updateFontPrices.js +94 -94
  60. package/src/utils/updateTypefaceDocument.js +162 -162
  61. package/src/utils/uploadFontFiles.js +405 -405
  62. package/src/utils/utils.js +24 -24
@@ -1,154 +1,154 @@
1
- // Lightweight retitle pass — re-derives title, documentId, weightName, and
2
- // subfamilyName from cached parsedMetadata when preserveShortenedNames changes.
3
- // No fontkit parsing, no file I/O, no Sanity queries — pure string manipulation.
4
-
5
- import { expandAbbreviations, generateStyleKeywords } from './generateKeywords.js';
6
- import { formatFontTitle, addItalicToFontTitle, processSubfamilyName } from './processFontFiles.js';
7
- import { sanitizeForSanityId } from './sanitizeForSanityId.js';
8
-
9
- const { weightKeywordList: WEIGHT_KW, italicKeywordList: ITALIC_KW } = generateStyleKeywords();
10
-
11
- /**
12
- * Re-derive title, documentId, weightName, and subfamilyName for a single
13
- * font entry using cached metadata and the new preserveShortenedNames setting.
14
- * Skips entries with user overrides on title (user edits take precedence).
15
- *
16
- * @param {object} entry - Font plan entry with parsedMetadata and decisions
17
- * @param {boolean} preserveShortenedNames - New setting value
18
- * @param {string} typefaceTitle - Parent typeface title (for prefix stripping)
19
- * @returns {object} Updated entry (shallow copy if changed, same ref if unchanged)
20
- */
21
- export function retitleFontEntry(entry, preserveShortenedNames, typefaceTitle) {
22
- if (!entry || !entry.parsedMetadata) return entry;
23
-
24
- // Skip entries with user overrides on title — user edits take precedence
25
- if (entry.decisions?.title?.userOverride) return entry;
26
-
27
- const meta = entry.parsedMetadata;
28
- const fullName = meta.fullName || '';
29
- const familyName = meta.familyName || '';
30
- const trimmedTitle = (typefaceTitle || '').trim();
31
-
32
- // Re-derive weightName from the original detected value
33
- let weightName = entry.decisions?.weightName?.detected || entry.weightName || '';
34
- if (!preserveShortenedNames) {
35
- weightName = expandAbbreviations(weightName);
36
- }
37
-
38
- // Re-derive subfamilyName
39
- const nameId4Remainder = fullName ? fullName.replace(trimmedTitle, '').trim() : '';
40
- const nameId1Remainder = familyName ? familyName.replace(trimmedTitle, '').trim() : '';
41
- let subfamilyName = nameId4Remainder || nameId1Remainder;
42
-
43
- if (!preserveShortenedNames) {
44
- subfamilyName = expandAbbreviations(subfamilyName);
45
- }
46
-
47
- subfamilyName = processSubfamilyName(subfamilyName, WEIGHT_KW, ITALIC_KW, preserveShortenedNames);
48
-
49
- // Strip style-only names from subfamily
50
- subfamilyName = subfamilyName
51
- .replace(/\b(Italic|Slant|Slanted|Oblique|Backslant|Roman|Upright)\b/gi, '')
52
- .replace(/\s+/g, ' ')
53
- .trim();
54
-
55
- // Strip subfamily from weightName to avoid duplication
56
- if (subfamilyName !== '') {
57
- weightName = weightName
58
- .replace(`${subfamilyName} `, '')
59
- .replace(` ${subfamilyName}`, '')
60
- .trim();
61
- }
62
-
63
- // Re-derive title
64
- let fontTitle = fullName.trim() || '';
65
- fontTitle = formatFontTitle(fontTitle, preserveShortenedNames);
66
-
67
- // Variable fonts get " VF" suffix
68
- if (entry.variableFont) {
69
- if (!fontTitle.toLowerCase().includes('vf')) {
70
- fontTitle = fontTitle + ' VF';
71
- }
72
- subfamilyName = '';
73
- }
74
-
75
- // Add italic suffix if needed
76
- if (!(entry.variableFont && fontTitle.toLowerCase().includes('italic'))) {
77
- // Build a minimal font-like object for addItalicToFontTitle
78
- const italicKW = entry.parsedMetadata.italicAngle !== 0 ? 'Italic' : '';
79
- fontTitle = addItalicToFontTitle(
80
- { opentype: { tables: { post: { italicAngle: entry.parsedMetadata.italicAngle || 0 } } } },
81
- fontTitle,
82
- italicKW,
83
- entry.style,
84
- preserveShortenedNames
85
- );
86
- }
87
-
88
- const documentId = sanitizeForSanityId(fontTitle);
89
-
90
- // Only return a new object if something actually changed
91
- if (
92
- entry.title === fontTitle &&
93
- entry.documentId === documentId &&
94
- entry.weightName === weightName &&
95
- entry.subfamily === (entry.variableFont ? '' : (subfamilyName || 'Regular'))
96
- ) {
97
- return entry;
98
- }
99
-
100
- const finalSubfamily = entry.variableFont ? '' : (subfamilyName || 'Regular');
101
-
102
- return {
103
- ...entry,
104
- title: fontTitle,
105
- documentId,
106
- weightName,
107
- subfamily: finalSubfamily,
108
- decisions: {
109
- ...entry.decisions,
110
- title: { ...entry.decisions.title, processed: fontTitle },
111
- documentId: { ...entry.decisions.documentId, generated: documentId },
112
- weightName: { ...entry.decisions.weightName, detected: weightName },
113
- subfamily: { ...entry.decisions.subfamily, detected: finalSubfamily },
114
- },
115
- };
116
- }
117
-
118
- /**
119
- * Retitle all font entries in a plan. Returns a new fonts map.
120
- * Runs collision detection after retitling.
121
- *
122
- * @param {object} fonts - plan.fonts map
123
- * @param {boolean} preserveShortenedNames
124
- * @param {string} typefaceTitle
125
- * @returns {object} New fonts map with updated titles and _idConflict flags
126
- */
127
- export function retitleAllFonts(fonts, preserveShortenedNames, typefaceTitle) {
128
- const updated = {};
129
- for (const [tempId, entry] of Object.entries(fonts)) {
130
- updated[tempId] = retitleFontEntry(entry, preserveShortenedNames, typefaceTitle);
131
- }
132
-
133
- // Run collision detection
134
- const idMap = {};
135
- for (const [tempId, font] of Object.entries(updated)) {
136
- updated[tempId] = { ...font, _idConflict: false };
137
- const docId = font.documentId;
138
- if (!idMap[docId]) {
139
- idMap[docId] = [tempId];
140
- } else {
141
- idMap[docId].push(tempId);
142
- }
143
- }
144
-
145
- for (const ids of Object.values(idMap)) {
146
- if (ids.length > 1) {
147
- for (const id of ids) {
148
- updated[id] = { ...updated[id], _idConflict: true };
149
- }
150
- }
151
- }
152
-
153
- return updated;
154
- }
1
+ // Lightweight retitle pass — re-derives title, documentId, weightName, and
2
+ // subfamilyName from cached parsedMetadata when preserveShortenedNames changes.
3
+ // No fontkit parsing, no file I/O, no Sanity queries — pure string manipulation.
4
+
5
+ import { expandAbbreviations, generateStyleKeywords } from './generateKeywords.js';
6
+ import { formatFontTitle, addItalicToFontTitle, processSubfamilyName } from './processFontFiles.js';
7
+ import { sanitizeForSanityId } from './sanitizeForSanityId.js';
8
+
9
+ const { weightKeywordList: WEIGHT_KW, italicKeywordList: ITALIC_KW } = generateStyleKeywords();
10
+
11
+ /**
12
+ * Re-derive title, documentId, weightName, and subfamilyName for a single
13
+ * font entry using cached metadata and the new preserveShortenedNames setting.
14
+ * Skips entries with user overrides on title (user edits take precedence).
15
+ *
16
+ * @param {object} entry - Font plan entry with parsedMetadata and decisions
17
+ * @param {boolean} preserveShortenedNames - New setting value
18
+ * @param {string} typefaceTitle - Parent typeface title (for prefix stripping)
19
+ * @returns {object} Updated entry (shallow copy if changed, same ref if unchanged)
20
+ */
21
+ export function retitleFontEntry(entry, preserveShortenedNames, typefaceTitle) {
22
+ if (!entry || !entry.parsedMetadata) return entry;
23
+
24
+ // Skip entries with user overrides on title — user edits take precedence
25
+ if (entry.decisions?.title?.userOverride) return entry;
26
+
27
+ const meta = entry.parsedMetadata;
28
+ const fullName = meta.fullName || '';
29
+ const familyName = meta.familyName || '';
30
+ const trimmedTitle = (typefaceTitle || '').trim();
31
+
32
+ // Re-derive weightName from the original detected value
33
+ let weightName = entry.decisions?.weightName?.detected || entry.weightName || '';
34
+ if (!preserveShortenedNames) {
35
+ weightName = expandAbbreviations(weightName);
36
+ }
37
+
38
+ // Re-derive subfamilyName
39
+ const nameId4Remainder = fullName ? fullName.replace(trimmedTitle, '').trim() : '';
40
+ const nameId1Remainder = familyName ? familyName.replace(trimmedTitle, '').trim() : '';
41
+ let subfamilyName = nameId4Remainder || nameId1Remainder;
42
+
43
+ if (!preserveShortenedNames) {
44
+ subfamilyName = expandAbbreviations(subfamilyName);
45
+ }
46
+
47
+ subfamilyName = processSubfamilyName(subfamilyName, WEIGHT_KW, ITALIC_KW, preserveShortenedNames);
48
+
49
+ // Strip style-only names from subfamily
50
+ subfamilyName = subfamilyName
51
+ .replace(/\b(Italic|Slant|Slanted|Oblique|Backslant|Roman|Upright)\b/gi, '')
52
+ .replace(/\s+/g, ' ')
53
+ .trim();
54
+
55
+ // Strip subfamily from weightName to avoid duplication
56
+ if (subfamilyName !== '') {
57
+ weightName = weightName
58
+ .replace(`${subfamilyName} `, '')
59
+ .replace(` ${subfamilyName}`, '')
60
+ .trim();
61
+ }
62
+
63
+ // Re-derive title
64
+ let fontTitle = fullName.trim() || '';
65
+ fontTitle = formatFontTitle(fontTitle, preserveShortenedNames);
66
+
67
+ // Variable fonts get " VF" suffix
68
+ if (entry.variableFont) {
69
+ if (!fontTitle.toLowerCase().includes('vf')) {
70
+ fontTitle = fontTitle + ' VF';
71
+ }
72
+ subfamilyName = '';
73
+ }
74
+
75
+ // Add italic suffix if needed
76
+ if (!(entry.variableFont && fontTitle.toLowerCase().includes('italic'))) {
77
+ // Build a minimal font-like object for addItalicToFontTitle
78
+ const italicKW = entry.parsedMetadata.italicAngle !== 0 ? 'Italic' : '';
79
+ fontTitle = addItalicToFontTitle(
80
+ { opentype: { tables: { post: { italicAngle: entry.parsedMetadata.italicAngle || 0 } } } },
81
+ fontTitle,
82
+ italicKW,
83
+ entry.style,
84
+ preserveShortenedNames
85
+ );
86
+ }
87
+
88
+ const documentId = sanitizeForSanityId(fontTitle);
89
+
90
+ // Only return a new object if something actually changed
91
+ if (
92
+ entry.title === fontTitle &&
93
+ entry.documentId === documentId &&
94
+ entry.weightName === weightName &&
95
+ entry.subfamily === (entry.variableFont ? '' : (subfamilyName || 'Regular'))
96
+ ) {
97
+ return entry;
98
+ }
99
+
100
+ const finalSubfamily = entry.variableFont ? '' : (subfamilyName || 'Regular');
101
+
102
+ return {
103
+ ...entry,
104
+ title: fontTitle,
105
+ documentId,
106
+ weightName,
107
+ subfamily: finalSubfamily,
108
+ decisions: {
109
+ ...entry.decisions,
110
+ title: { ...entry.decisions.title, processed: fontTitle },
111
+ documentId: { ...entry.decisions.documentId, generated: documentId },
112
+ weightName: { ...entry.decisions.weightName, detected: weightName },
113
+ subfamily: { ...entry.decisions.subfamily, detected: finalSubfamily },
114
+ },
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Retitle all font entries in a plan. Returns a new fonts map.
120
+ * Runs collision detection after retitling.
121
+ *
122
+ * @param {object} fonts - plan.fonts map
123
+ * @param {boolean} preserveShortenedNames
124
+ * @param {string} typefaceTitle
125
+ * @returns {object} New fonts map with updated titles and _idConflict flags
126
+ */
127
+ export function retitleAllFonts(fonts, preserveShortenedNames, typefaceTitle) {
128
+ const updated = {};
129
+ for (const [tempId, entry] of Object.entries(fonts)) {
130
+ updated[tempId] = retitleFontEntry(entry, preserveShortenedNames, typefaceTitle);
131
+ }
132
+
133
+ // Run collision detection
134
+ const idMap = {};
135
+ for (const [tempId, font] of Object.entries(updated)) {
136
+ updated[tempId] = { ...font, _idConflict: false };
137
+ const docId = font.documentId;
138
+ if (!idMap[docId]) {
139
+ idMap[docId] = [tempId];
140
+ } else {
141
+ idMap[docId].push(tempId);
142
+ }
143
+ }
144
+
145
+ for (const ids of Object.values(idMap)) {
146
+ if (ids.length > 1) {
147
+ for (const id of ids) {
148
+ updated[id] = { ...updated[id], _idConflict: true };
149
+ }
150
+ }
151
+ }
152
+
153
+ return updated;
154
+ }
@@ -1,65 +1,65 @@
1
- // Converts arbitrary strings into valid Sanity document IDs (lowercase, hyphens, no special characters)
2
- import slugify from 'slugify';
3
-
4
- /**
5
- * Sanitizes a string into a valid Sanity document ID.
6
- *
7
- * Sanity ID requirements:
8
- * - Must start with a letter or underscore (not a number or hyphen)
9
- * - Can only contain lowercase letters (a-z), numbers (0-9), hyphens (-), and underscores (_)
10
- * - Must be between 1 and 128 characters
11
- *
12
- * @param {string} str - The raw string (e.g. font title or filename) to sanitize
13
- * @returns {string} A valid Sanity document ID
14
- */
15
- export function sanitizeForSanityId(str) {
16
- if (!str || typeof str !== 'string') {
17
- return 'font-' + Date.now();
18
- }
19
-
20
- let sanitized = str.toLowerCase().trim();
21
-
22
- // Replace common symbols before slugify
23
- sanitized = sanitized.replace(/\+/g, 'plus');
24
- sanitized = sanitized.replace(/&/g, 'and');
25
- sanitized = sanitized.replace(/@/g, 'at');
26
-
27
- sanitized = slugify(sanitized, {
28
- replacement: '-',
29
- remove: /[^\w\s-]/g,
30
- lower: true,
31
- strict: true,
32
- locale: 'en',
33
- trim: true,
34
- });
35
-
36
- // Strip any characters that still aren't lowercase-alphanumeric, hyphens, or underscores
37
- sanitized = sanitized.replace(/[^a-z0-9\-_]/g, '-');
38
-
39
- // Collapse repeated hyphens and strip leading/trailing hyphens or underscores
40
- sanitized = sanitized.replace(/-+/g, '-');
41
- sanitized = sanitized.replace(/^[-_]+|[-_]+$/g, '');
42
-
43
- // IDs must not start with a number or hyphen
44
- if (sanitized && !/^[a-z_]/.test(sanitized)) {
45
- sanitized = 'font_' + sanitized;
46
- }
47
-
48
- if (!sanitized) {
49
- sanitized = 'font_' + Date.now();
50
- }
51
-
52
- // Sanity hard-caps IDs at 128 characters
53
- if (sanitized.length > 128) {
54
- const hash = Math.random().toString(36).substring(2, 8);
55
- sanitized = sanitized.substring(0, 120) + '_' + hash;
56
- }
57
-
58
- // Paranoid final validation
59
- if (!/^[a-z_][a-z0-9\-_]*$/.test(sanitized)) {
60
- console.warn(`ID sanitization produced invalid result: "${sanitized}", using fallback`);
61
- sanitized = 'font_' + Date.now();
62
- }
63
-
64
- return sanitized;
65
- }
1
+ // Converts arbitrary strings into valid Sanity document IDs (lowercase, hyphens, no special characters)
2
+ import slugify from 'slugify';
3
+
4
+ /**
5
+ * Sanitizes a string into a valid Sanity document ID.
6
+ *
7
+ * Sanity ID requirements:
8
+ * - Must start with a letter or underscore (not a number or hyphen)
9
+ * - Can only contain lowercase letters (a-z), numbers (0-9), hyphens (-), and underscores (_)
10
+ * - Must be between 1 and 128 characters
11
+ *
12
+ * @param {string} str - The raw string (e.g. font title or filename) to sanitize
13
+ * @returns {string} A valid Sanity document ID
14
+ */
15
+ export function sanitizeForSanityId(str) {
16
+ if (!str || typeof str !== 'string') {
17
+ return 'font-' + Date.now();
18
+ }
19
+
20
+ let sanitized = str.toLowerCase().trim();
21
+
22
+ // Replace common symbols before slugify
23
+ sanitized = sanitized.replace(/\+/g, 'plus');
24
+ sanitized = sanitized.replace(/&/g, 'and');
25
+ sanitized = sanitized.replace(/@/g, 'at');
26
+
27
+ sanitized = slugify(sanitized, {
28
+ replacement: '-',
29
+ remove: /[^\w\s-]/g,
30
+ lower: true,
31
+ strict: true,
32
+ locale: 'en',
33
+ trim: true,
34
+ });
35
+
36
+ // Strip any characters that still aren't lowercase-alphanumeric, hyphens, or underscores
37
+ sanitized = sanitized.replace(/[^a-z0-9\-_]/g, '-');
38
+
39
+ // Collapse repeated hyphens and strip leading/trailing hyphens or underscores
40
+ sanitized = sanitized.replace(/-+/g, '-');
41
+ sanitized = sanitized.replace(/^[-_]+|[-_]+$/g, '');
42
+
43
+ // IDs must not start with a number or hyphen
44
+ if (sanitized && !/^[a-z_]/.test(sanitized)) {
45
+ sanitized = 'font_' + sanitized;
46
+ }
47
+
48
+ if (!sanitized) {
49
+ sanitized = 'font_' + Date.now();
50
+ }
51
+
52
+ // Sanity hard-caps IDs at 128 characters
53
+ if (sanitized.length > 128) {
54
+ const hash = Math.random().toString(36).substring(2, 8);
55
+ sanitized = sanitized.substring(0, 120) + '_' + hash;
56
+ }
57
+
58
+ // Paranoid final validation
59
+ if (!/^[a-z_][a-z0-9\-_]*$/.test(sanitized)) {
60
+ console.warn(`ID sanitization produced invalid result: "${sanitized}", using fallback`);
61
+ sanitized = 'font_' + Date.now();
62
+ }
63
+
64
+ return sanitized;
65
+ }
@@ -1,27 +1,27 @@
1
- // Sets up globalThis.pako and globalThis.unbrotli for lib-font WOFF/WOFF2 decompression.
2
- // Must be imported before lib-font.
3
-
4
- import pako from 'pako';
5
-
6
- // Set pako for WOFF (zlib) decompression
7
- globalThis.pako = pako;
8
-
9
- // Set unbrotli for WOFF2 (brotli) decompression
10
- // The vendor unbrotli.js UMD sets globalThis.unbrotli in browser contexts.
11
- // In Node/bundler contexts it exports via module.exports instead.
12
- // We use a side-effect import and then check if the global was set.
13
- import '../vendor/unbrotli.js';
14
-
15
- // If the UMD didn't set the global (CJS path in Node/bundler), try to require it
16
- if (!globalThis.unbrotli) {
17
- try {
18
- // In bundler context, the UMD file's module.exports is available
19
- // tsup will resolve this at build time
20
- const brotli = require('../vendor/unbrotli.js');
21
- if (typeof brotli === 'function') {
22
- globalThis.unbrotli = brotli;
23
- }
24
- } catch {
25
- // Silently fail — WOFF2 parsing will error gracefully
26
- }
27
- }
1
+ // Sets up globalThis.pako and globalThis.unbrotli for lib-font WOFF/WOFF2 decompression.
2
+ // Must be imported before lib-font.
3
+
4
+ import pako from 'pako';
5
+
6
+ // Set pako for WOFF (zlib) decompression
7
+ globalThis.pako = pako;
8
+
9
+ // Set unbrotli for WOFF2 (brotli) decompression
10
+ // The vendor unbrotli.js UMD sets globalThis.unbrotli in browser contexts.
11
+ // In Node/bundler contexts it exports via module.exports instead.
12
+ // We use a side-effect import and then check if the global was set.
13
+ import '../vendor/unbrotli.js';
14
+
15
+ // If the UMD didn't set the global (CJS path in Node/bundler), try to require it
16
+ if (!globalThis.unbrotli) {
17
+ try {
18
+ // In bundler context, the UMD file's module.exports is available
19
+ // tsup will resolve this at build time
20
+ const brotli = require('../vendor/unbrotli.js');
21
+ if (typeof brotli === 'function') {
22
+ globalThis.unbrotli = brotli;
23
+ }
24
+ } catch {
25
+ // Silently fail — WOFF2 parsing will error gracefully
26
+ }
27
+ }