@liiift-studio/sanity-font-manager 2.4.0 → 2.5.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.
- package/dist/UploadModal-6LIX7XOK.js +6 -0
- package/dist/UploadModal-NME2W53V.mjs +6 -0
- package/dist/chunk-646WCBRR.mjs +7276 -0
- package/dist/chunk-FH4QKHOH.js +7276 -0
- package/dist/index.js +664 -1647
- package/dist/index.mjs +317 -1209
- package/package.json +5 -5
- package/src/components/BatchUploadFonts.jsx +57 -44
- package/src/components/BulkActions.jsx +99 -0
- package/src/components/ExistingDocumentResolver.jsx +152 -0
- package/src/components/FontReviewCard.jsx +415 -0
- package/src/components/SingleUploaderTool.jsx +3 -4
- package/src/components/UploadModal.jsx +268 -0
- package/src/components/UploadScriptsComponent.jsx +23 -21
- package/src/components/UploadStep1Settings.jsx +272 -0
- package/src/components/UploadStep2Review.jsx +472 -0
- package/src/components/UploadStep3Execute.jsx +234 -0
- package/src/components/UploadSummary.jsx +196 -0
- package/src/index.js +45 -0
- package/src/utils/buildUploadPlan.js +325 -0
- package/src/utils/executeUploadPlan.js +437 -0
- package/src/utils/executionReducer.js +56 -0
- package/src/utils/fontHelpers.js +267 -0
- package/src/utils/generateCssFile.js +79 -77
- package/src/utils/generateFontData.js +47 -94
- package/src/utils/getEmptyFontKit.js +19 -17
- package/src/utils/parseFont.js +55 -0
- package/src/utils/planReducer.js +517 -0
- package/src/utils/planTypes.js +183 -0
- package/src/utils/processFontFiles.js +120 -78
- package/src/utils/regenerateFontData.js +2 -2
- package/src/utils/resolveExistingFont.js +87 -0
- package/src/utils/uploadFontFiles.js +405 -405
|
@@ -1,405 +1,405 @@
|
|
|
1
|
-
// Core batch upload orchestrator — uploads each format to Sanity, generates CSS and metadata, resolves existing documents, then creates or updates font documents
|
|
2
|
-
|
|
3
|
-
import { nanoid } from 'nanoid';
|
|
4
|
-
import generateCssFile from './generateCssFile';
|
|
5
|
-
import generateFontData from './generateFontData';
|
|
6
|
-
import { parseVariableFontInstances } from './parseVariableFontInstances';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Uploads all font files to Sanity and creates/updates font documents.
|
|
10
|
-
* @param {Object} fontsObjects
|
|
11
|
-
* @param {Object} subfamilies
|
|
12
|
-
* @param {Object} client - Sanity client
|
|
13
|
-
* @param {string} inputPrice
|
|
14
|
-
* @param {Object} stylesObject - Existing typeface styles object
|
|
15
|
-
* @param {Function} setStatus
|
|
16
|
-
* @param {Function} setError
|
|
17
|
-
* @param {boolean} preserveFileNames - Use original filenames for asset naming
|
|
18
|
-
* @returns {Promise<Object>} fontRefs, variableRefs, failedFiles
|
|
19
|
-
*/
|
|
20
|
-
export const uploadFontFiles = async (
|
|
21
|
-
fontsObjects,
|
|
22
|
-
subfamilies,
|
|
23
|
-
client,
|
|
24
|
-
inputPrice,
|
|
25
|
-
stylesObject,
|
|
26
|
-
setStatus,
|
|
27
|
-
setError,
|
|
28
|
-
preserveFileNames = false,
|
|
29
|
-
) => {
|
|
30
|
-
let fontRefs = [];
|
|
31
|
-
let variableRefs = [];
|
|
32
|
-
let failedFiles = [];
|
|
33
|
-
|
|
34
|
-
const fontObjectKeys = Object.keys(fontsObjects);
|
|
35
|
-
|
|
36
|
-
// Upload files for each font
|
|
37
|
-
for (let i = 0; i < fontObjectKeys.length; i++) {
|
|
38
|
-
const id = fontObjectKeys[i];
|
|
39
|
-
const fontObject = fontsObjects[id];
|
|
40
|
-
const files = fontObject.files;
|
|
41
|
-
const fontKit = fontObject.fontKit;
|
|
42
|
-
let newFileInput = fontObject.fileInput;
|
|
43
|
-
|
|
44
|
-
fontObject.subfamily = subfamilies[id] ? subfamilies[id] : '';
|
|
45
|
-
fontObject.price = Number(inputPrice) ? Number(inputPrice) : 0;
|
|
46
|
-
if (fontObject.price > 0) fontObject.sell = true;
|
|
47
|
-
|
|
48
|
-
for (let j = 0; j < files.length; j++) {
|
|
49
|
-
const file = files[j];
|
|
50
|
-
const fileType = determineFileType(file);
|
|
51
|
-
|
|
52
|
-
// Use original filename for asset naming when preserveFileNames is enabled
|
|
53
|
-
const assetFilename = preserveFileNames && fontObject.originalFilename
|
|
54
|
-
? `${fontObject.originalFilename}.${fileType}`
|
|
55
|
-
: `${fontObject._id}.${fileType}`;
|
|
56
|
-
|
|
57
|
-
console.log(`[${i + 1}/${fontObjectKeys.length}][${j + 1}/${files.length}] Uploading font file: ${assetFilename}`);
|
|
58
|
-
setStatus(`[${i + 1}/${fontObjectKeys.length}][${j + 1}/${files.length}] Uploading font file: ${assetFilename}`);
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
const baseAsset = await client.assets.upload('file', file, { filename: assetFilename });
|
|
62
|
-
|
|
63
|
-
// Sanity deduplicates assets by SHA1 hash and reuses the existing asset doc,
|
|
64
|
-
// which means originalFilename may reflect a previous upload. Patch it to
|
|
65
|
-
// match the intended filename so downstream consumers see the correct name.
|
|
66
|
-
if (preserveFileNames && baseAsset.originalFilename !== assetFilename) {
|
|
67
|
-
try {
|
|
68
|
-
await client.patch(baseAsset._id).set({ originalFilename: assetFilename }).commit();
|
|
69
|
-
} catch (renameErr) {
|
|
70
|
-
console.warn('Could not rename asset — permissions may be restricted:', renameErr.message);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
newFileInput[fileType] = {
|
|
75
|
-
_type: 'file',
|
|
76
|
-
asset: { _ref: baseAsset._id, _type: 'reference' },
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
if (fileType === 'woff2') {
|
|
80
|
-
console.log(`[${i + 1}/${fontObjectKeys.length}][${j + 1}/${files.length}] Generating CSS for: ${fontObject.title}`);
|
|
81
|
-
setStatus(`[${i + 1}/${fontObjectKeys.length}][${j + 1}/${files.length}] Generating CSS for: ${fontObject.title}`);
|
|
82
|
-
newFileInput = await generateCssFile({
|
|
83
|
-
woff2File: file,
|
|
84
|
-
fileInput: newFileInput,
|
|
85
|
-
fontName: fontObject.title,
|
|
86
|
-
fileName: fontObject._id,
|
|
87
|
-
variableFont: fontObject.variableFont,
|
|
88
|
-
weight: fontObject.weight,
|
|
89
|
-
client: client,
|
|
90
|
-
style: fontObject.style,
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (fileType === 'ttf') {
|
|
95
|
-
console.log(`[${i + 1}/${fontObjectKeys.length}][${j + 1}/${files.length}] Generating font data for: ${fontObject.title}`);
|
|
96
|
-
setStatus(`[${i + 1}/${fontObjectKeys.length}][${j + 1}/${files.length}] Generating font data for: ${fontObject.title}`);
|
|
97
|
-
const metadata = await generateFontData({
|
|
98
|
-
fontId: fontObject._id,
|
|
99
|
-
url: baseAsset.url,
|
|
100
|
-
fontKit: fontKit,
|
|
101
|
-
client: client,
|
|
102
|
-
commit: false,
|
|
103
|
-
});
|
|
104
|
-
Object.assign(fontObject, metadata);
|
|
105
|
-
}
|
|
106
|
-
} catch (err) {
|
|
107
|
-
console.error('Error uploading font:', fontObject.title, err.message);
|
|
108
|
-
setStatus('Error uploading font: ' + err.message);
|
|
109
|
-
setError(true);
|
|
110
|
-
failedFiles.push({ name: file.name, fk: fontKit });
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
fontObject.fileInput = newFileInput;
|
|
115
|
-
fontsObjects[id] = fontObject;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Create or update Sanity documents
|
|
120
|
-
console.log('Creating/updating Sanity font documents:', fontsObjects);
|
|
121
|
-
|
|
122
|
-
for (let i = 0; i < fontObjectKeys.length; i++) {
|
|
123
|
-
const fontId = fontObjectKeys[i];
|
|
124
|
-
const font = fontsObjects[fontId];
|
|
125
|
-
|
|
126
|
-
const fontRef = await createOrUpdateFontDocument(font, client, setError);
|
|
127
|
-
|
|
128
|
-
if (fontRef) {
|
|
129
|
-
if (!font.variableFont) {
|
|
130
|
-
addToFontRefs(fontRef, font, fontRefs, stylesObject);
|
|
131
|
-
} else {
|
|
132
|
-
addToVariableRefs(fontRef, font, variableRefs, stylesObject);
|
|
133
|
-
}
|
|
134
|
-
console.log(`[${i + 1}/${fontObjectKeys.length}] ${fontRef._ref} created`);
|
|
135
|
-
setStatus(`[${i + 1}/${fontObjectKeys.length}] ${fontRef._ref} created`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return { fontRefs, variableRefs, failedFiles };
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Returns the file extension for a font file.
|
|
144
|
-
* @param {File} file
|
|
145
|
-
* @returns {string}
|
|
146
|
-
*/
|
|
147
|
-
const determineFileType = (file) => {
|
|
148
|
-
if (file.name.endsWith('.otf')) return 'otf';
|
|
149
|
-
if (file.name.endsWith('.ttf')) return 'ttf';
|
|
150
|
-
if (file.name.endsWith('.woff')) return 'woff';
|
|
151
|
-
if (file.name.endsWith('.woff2')) return 'woff2';
|
|
152
|
-
if (file.name.endsWith('.eot')) return 'eot';
|
|
153
|
-
if (file.name.endsWith('.svg')) return 'svg';
|
|
154
|
-
return '';
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Resolves whether a font document already exists in Sanity, returning match details
|
|
159
|
-
* and a recommendation for how to proceed.
|
|
160
|
-
*
|
|
161
|
-
* Resolution strategies (in priority order):
|
|
162
|
-
* 1. Exact _id match or draft _id match or slug.current match
|
|
163
|
-
* 2. Content match by typefaceName + weightName + style + subfamily + variableFont
|
|
164
|
-
*
|
|
165
|
-
* @param {Object} font - The font object with _id, typefaceName, weightName, style, subfamily, variableFont
|
|
166
|
-
* @param {Object} client - Sanity client (parameterized queries only)
|
|
167
|
-
* @returns {Promise<{ exact: Object|null, candidates: Object[], recommendation: string }>}
|
|
168
|
-
*/
|
|
169
|
-
export const resolveExistingFont = async (font, client) => {
|
|
170
|
-
const result = { exact: null, candidates: [], recommendation: 'create' };
|
|
171
|
-
|
|
172
|
-
try {
|
|
173
|
-
// Strategy 1: ID / slug match
|
|
174
|
-
const idMatches = await client.fetch(
|
|
175
|
-
`*[_type == 'font' && (_id == $id || _id == $draftId || slug.current == $id)]{
|
|
176
|
-
_id, title, weight, style, weightName, typefaceName, subfamily, variableFont,
|
|
177
|
-
fileInput, description, metaData, metrics, opentypeFeatures, characterSet,
|
|
178
|
-
scriptFileInput, variableInstanceReferences
|
|
179
|
-
}`,
|
|
180
|
-
{ id: font._id, draftId: `drafts.${font._id}` }
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
if (idMatches.length > 0) {
|
|
184
|
-
result.exact = idMatches[0];
|
|
185
|
-
result.recommendation = 'use-exact';
|
|
186
|
-
return result;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Strategy 2: Content match (only when ID query returns nothing)
|
|
190
|
-
const subfamily = font.subfamily || '';
|
|
191
|
-
const contentMatches = await client.fetch(
|
|
192
|
-
`*[_type == 'font'
|
|
193
|
-
&& lower(typefaceName) == lower($typefaceName)
|
|
194
|
-
&& lower(weightName) == lower($weightName)
|
|
195
|
-
&& lower(style) == lower($style)
|
|
196
|
-
&& (variableFont == $variableFont || (!defined(variableFont) && $variableFont == false))
|
|
197
|
-
&& (
|
|
198
|
-
lower(coalesce(subfamily, '')) == lower($subfamily)
|
|
199
|
-
|| (lower(coalesce(subfamily, '')) in ['', 'regular'] && lower($subfamily) in ['', 'regular'])
|
|
200
|
-
)
|
|
201
|
-
]{
|
|
202
|
-
_id, title, weight, style, weightName, typefaceName, subfamily, variableFont,
|
|
203
|
-
fileInput, description, metaData, metrics, opentypeFeatures, characterSet,
|
|
204
|
-
scriptFileInput, variableInstanceReferences
|
|
205
|
-
}`,
|
|
206
|
-
{
|
|
207
|
-
typefaceName: font.typefaceName,
|
|
208
|
-
weightName: font.weightName || '',
|
|
209
|
-
style: font.style || 'Regular',
|
|
210
|
-
variableFont: font.variableFont || false,
|
|
211
|
-
subfamily: subfamily === '' ? 'regular' : subfamily,
|
|
212
|
-
}
|
|
213
|
-
);
|
|
214
|
-
|
|
215
|
-
if (contentMatches.length === 1) {
|
|
216
|
-
result.candidates = contentMatches;
|
|
217
|
-
result.recommendation = 'use-candidate';
|
|
218
|
-
return result;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (contentMatches.length > 1) {
|
|
222
|
-
result.candidates = contentMatches;
|
|
223
|
-
result.recommendation = 'ambiguous';
|
|
224
|
-
console.warn(`Ambiguous font match for "${font.title}" — ${contentMatches.length} candidates found:`,
|
|
225
|
-
contentMatches.map(c => c._id));
|
|
226
|
-
return result;
|
|
227
|
-
}
|
|
228
|
-
} catch (err) {
|
|
229
|
-
console.error('Error resolving existing font:', font._id, err.message);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
return result;
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Creates a new font document or updates an existing one, returning its reference.
|
|
237
|
-
* Uses resolveExistingFont to determine whether to create or update.
|
|
238
|
-
* @param {Object} font
|
|
239
|
-
* @param {Object} client
|
|
240
|
-
* @param {Function} setError
|
|
241
|
-
* @returns {Promise<Object|null>}
|
|
242
|
-
*/
|
|
243
|
-
const createOrUpdateFontDocument = async (font, client, setError) => {
|
|
244
|
-
try {
|
|
245
|
-
const { exact, candidates, recommendation } = await resolveExistingFont(font, client);
|
|
246
|
-
|
|
247
|
-
const { files, fontKit } = font;
|
|
248
|
-
delete font.files;
|
|
249
|
-
delete font.fontKit;
|
|
250
|
-
// Remove temp field used by preserveFileNames — must not be saved to Sanity
|
|
251
|
-
delete font.originalFilename;
|
|
252
|
-
|
|
253
|
-
if (font.variableFont && font.variableInstances) {
|
|
254
|
-
const instanceMappings = await parseVariableFontInstances(font, client);
|
|
255
|
-
if (instanceMappings.length > 0) {
|
|
256
|
-
font.variableInstanceReferences = instanceMappings;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
let fontResponse;
|
|
261
|
-
if (recommendation === 'use-exact' && exact) {
|
|
262
|
-
fontResponse = await updateExistingFont(font, exact, client);
|
|
263
|
-
} else if (recommendation === 'use-candidate' && candidates.length === 1) {
|
|
264
|
-
// Reassign font._id to match the existing document so inbound references resolve
|
|
265
|
-
console.log(`Content-match: reassigning "${font._id}" → "${candidates[0]._id}"`);
|
|
266
|
-
font._id = candidates[0]._id;
|
|
267
|
-
fontResponse = await updateExistingFont(font, candidates[0], client);
|
|
268
|
-
} else {
|
|
269
|
-
// 'ambiguous' or 'create' — create a new document
|
|
270
|
-
fontResponse = await createNewFont(font, client);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
return {
|
|
274
|
-
_key: nanoid(),
|
|
275
|
-
_type: 'reference',
|
|
276
|
-
_ref: fontResponse._id,
|
|
277
|
-
_weak: true,
|
|
278
|
-
};
|
|
279
|
-
} catch (e) {
|
|
280
|
-
console.error('Error creating font:', font.title, font.subfamily, e);
|
|
281
|
-
setError(true);
|
|
282
|
-
return null;
|
|
283
|
-
}
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Patches an existing font document, merging file references and preserving stored metadata.
|
|
288
|
-
* @param {Object} font
|
|
289
|
-
* @param {Object} existingFont
|
|
290
|
-
* @param {Object} client
|
|
291
|
-
* @returns {Promise<Object>}
|
|
292
|
-
*/
|
|
293
|
-
const updateExistingFont = async (font, existingFont, client) => {
|
|
294
|
-
if (existingFont.fileInput) {
|
|
295
|
-
const newFileInput = { ...font.fileInput };
|
|
296
|
-
Object.keys(existingFont.fileInput).forEach(key => {
|
|
297
|
-
if (!newFileInput[key]) newFileInput[key] = existingFont.fileInput[key];
|
|
298
|
-
});
|
|
299
|
-
font.fileInput = newFileInput;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
font.metaData = !font?.metaData || Object.keys(font?.metaData || {}).length === 0
|
|
303
|
-
? existingFont?.metaData || {}
|
|
304
|
-
: font.metaData;
|
|
305
|
-
font.metrics = !font?.metrics || Object.keys(font?.metrics || {}).length === 0
|
|
306
|
-
? existingFont?.metrics || {}
|
|
307
|
-
: font.metrics;
|
|
308
|
-
font.opentypeFeatures = !font?.opentypeFeatures || Object.keys(font?.opentypeFeatures || {}).length === 0
|
|
309
|
-
? existingFont?.opentypeFeatures || {}
|
|
310
|
-
: font.opentypeFeatures;
|
|
311
|
-
font.characterSet = !font?.characterSet || Object.keys(font?.characterSet || {}).length === 0
|
|
312
|
-
? existingFont?.characterSet || {}
|
|
313
|
-
: font.characterSet;
|
|
314
|
-
font.scriptFileInput = existingFont?.scriptFileInput || {};
|
|
315
|
-
|
|
316
|
-
cleanMetadataValues(font);
|
|
317
|
-
|
|
318
|
-
if (font.variableFont && existingFont?.variableInstanceReferences &&
|
|
319
|
-
(!font.variableInstanceReferences || font.variableInstanceReferences.length === 0)) {
|
|
320
|
-
font.variableInstanceReferences = existingFont.variableInstanceReferences;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
console.log('Updating existing font:', font._id, font.title);
|
|
324
|
-
|
|
325
|
-
const patchObject = {
|
|
326
|
-
fileInput: font.fileInput,
|
|
327
|
-
subfamily: font.subfamily,
|
|
328
|
-
weight: font.weight,
|
|
329
|
-
};
|
|
330
|
-
|
|
331
|
-
if (font.variableInstanceReferences) {
|
|
332
|
-
patchObject.variableInstanceReferences = font.variableInstanceReferences;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
return await client.patch(font._id).set(patchObject).commit();
|
|
336
|
-
};
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* Creates a new font document in Sanity.
|
|
340
|
-
* @param {Object} font
|
|
341
|
-
* @param {Object} client
|
|
342
|
-
* @returns {Promise<Object>}
|
|
343
|
-
*/
|
|
344
|
-
const createNewFont = async (font, client) => {
|
|
345
|
-
console.log('Creating new font:', font._id, font.title);
|
|
346
|
-
if (font.metaData) cleanMetadataValues(font);
|
|
347
|
-
|
|
348
|
-
const newDocument = {
|
|
349
|
-
_key: nanoid(),
|
|
350
|
-
_id: font._id,
|
|
351
|
-
_type: 'font',
|
|
352
|
-
...font,
|
|
353
|
-
};
|
|
354
|
-
|
|
355
|
-
return await client.createOrReplace(newDocument);
|
|
356
|
-
};
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* Removes null metadata values and strips control characters from string values.
|
|
360
|
-
* @param {Object} font
|
|
361
|
-
*/
|
|
362
|
-
const cleanMetadataValues = (font) => {
|
|
363
|
-
if (!font.metaData) return;
|
|
364
|
-
Object.keys(font.metaData).forEach(key => {
|
|
365
|
-
if (font.metaData[key] == null) {
|
|
366
|
-
font.metaData[key] = '';
|
|
367
|
-
} else {
|
|
368
|
-
font.metaData[key] = font.metaData[key].replace(/[\x00-\x1f]/g, '');
|
|
369
|
-
}
|
|
370
|
-
});
|
|
371
|
-
};
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* Adds a font reference to fontRefs if it's not already in the existing styles.
|
|
375
|
-
* @param {Object} fontRef
|
|
376
|
-
* @param {Object} font
|
|
377
|
-
* @param {Array} fontRefs
|
|
378
|
-
* @param {Object} stylesObject
|
|
379
|
-
*/
|
|
380
|
-
const addToFontRefs = (fontRef, font, fontRefs, stylesObject) => {
|
|
381
|
-
if (stylesObject.fonts && stylesObject.fonts.length > 0) {
|
|
382
|
-
const fontExists = stylesObject.fonts.findIndex(f => f._ref === fontRef._ref);
|
|
383
|
-
const inFontRefs = fontRefs.findIndex(f => f._ref === fontRef._ref);
|
|
384
|
-
if (fontExists === -1 && inFontRefs === -1) fontRefs.push(fontRef);
|
|
385
|
-
} else {
|
|
386
|
-
fontRefs.push(fontRef);
|
|
387
|
-
}
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* Adds a variable font reference to variableRefs if it's not already in the existing styles.
|
|
392
|
-
* @param {Object} fontRef
|
|
393
|
-
* @param {Object} font
|
|
394
|
-
* @param {Array} variableRefs
|
|
395
|
-
* @param {Object} stylesObject
|
|
396
|
-
*/
|
|
397
|
-
const addToVariableRefs = (fontRef, font, variableRefs, stylesObject) => {
|
|
398
|
-
if (stylesObject?.variableFont?.length) {
|
|
399
|
-
const vfExists = stylesObject.variableFont.findIndex(f => f._ref === fontRef._ref);
|
|
400
|
-
const inVariableRefs = variableRefs.findIndex(f => f._ref === fontRef._ref);
|
|
401
|
-
if (vfExists === -1 && inVariableRefs === -1 && font.variableFont) variableRefs.push(fontRef);
|
|
402
|
-
} else {
|
|
403
|
-
variableRefs.push(fontRef);
|
|
404
|
-
}
|
|
405
|
-
};
|
|
1
|
+
// Core batch upload orchestrator — uploads each format to Sanity, generates CSS and metadata, resolves existing documents, then creates or updates font documents
|
|
2
|
+
|
|
3
|
+
import { nanoid } from 'nanoid';
|
|
4
|
+
import generateCssFile from './generateCssFile';
|
|
5
|
+
import generateFontData from './generateFontData';
|
|
6
|
+
import { parseVariableFontInstances } from './parseVariableFontInstances';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Uploads all font files to Sanity and creates/updates font documents.
|
|
10
|
+
* @param {Object} fontsObjects
|
|
11
|
+
* @param {Object} subfamilies
|
|
12
|
+
* @param {Object} client - Sanity client
|
|
13
|
+
* @param {string} inputPrice
|
|
14
|
+
* @param {Object} stylesObject - Existing typeface styles object
|
|
15
|
+
* @param {Function} setStatus
|
|
16
|
+
* @param {Function} setError
|
|
17
|
+
* @param {boolean} preserveFileNames - Use original filenames for asset naming
|
|
18
|
+
* @returns {Promise<Object>} fontRefs, variableRefs, failedFiles
|
|
19
|
+
*/
|
|
20
|
+
export const uploadFontFiles = async (
|
|
21
|
+
fontsObjects,
|
|
22
|
+
subfamilies,
|
|
23
|
+
client,
|
|
24
|
+
inputPrice,
|
|
25
|
+
stylesObject,
|
|
26
|
+
setStatus,
|
|
27
|
+
setError,
|
|
28
|
+
preserveFileNames = false,
|
|
29
|
+
) => {
|
|
30
|
+
let fontRefs = [];
|
|
31
|
+
let variableRefs = [];
|
|
32
|
+
let failedFiles = [];
|
|
33
|
+
|
|
34
|
+
const fontObjectKeys = Object.keys(fontsObjects);
|
|
35
|
+
|
|
36
|
+
// Upload files for each font
|
|
37
|
+
for (let i = 0; i < fontObjectKeys.length; i++) {
|
|
38
|
+
const id = fontObjectKeys[i];
|
|
39
|
+
const fontObject = fontsObjects[id];
|
|
40
|
+
const files = fontObject.files;
|
|
41
|
+
const fontKit = fontObject.fontKit;
|
|
42
|
+
let newFileInput = fontObject.fileInput;
|
|
43
|
+
|
|
44
|
+
fontObject.subfamily = subfamilies[id] ? subfamilies[id] : '';
|
|
45
|
+
fontObject.price = Number(inputPrice) ? Number(inputPrice) : 0;
|
|
46
|
+
if (fontObject.price > 0) fontObject.sell = true;
|
|
47
|
+
|
|
48
|
+
for (let j = 0; j < files.length; j++) {
|
|
49
|
+
const file = files[j];
|
|
50
|
+
const fileType = determineFileType(file);
|
|
51
|
+
|
|
52
|
+
// Use original filename for asset naming when preserveFileNames is enabled
|
|
53
|
+
const assetFilename = preserveFileNames && fontObject.originalFilename
|
|
54
|
+
? `${fontObject.originalFilename}.${fileType}`
|
|
55
|
+
: `${fontObject._id}.${fileType}`;
|
|
56
|
+
|
|
57
|
+
console.log(`[${i + 1}/${fontObjectKeys.length}][${j + 1}/${files.length}] Uploading font file: ${assetFilename}`);
|
|
58
|
+
setStatus(`[${i + 1}/${fontObjectKeys.length}][${j + 1}/${files.length}] Uploading font file: ${assetFilename}`);
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const baseAsset = await client.assets.upload('file', file, { filename: assetFilename });
|
|
62
|
+
|
|
63
|
+
// Sanity deduplicates assets by SHA1 hash and reuses the existing asset doc,
|
|
64
|
+
// which means originalFilename may reflect a previous upload. Patch it to
|
|
65
|
+
// match the intended filename so downstream consumers see the correct name.
|
|
66
|
+
if (preserveFileNames && baseAsset.originalFilename !== assetFilename) {
|
|
67
|
+
try {
|
|
68
|
+
await client.patch(baseAsset._id).set({ originalFilename: assetFilename }).commit();
|
|
69
|
+
} catch (renameErr) {
|
|
70
|
+
console.warn('Could not rename asset — permissions may be restricted:', renameErr.message);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
newFileInput[fileType] = {
|
|
75
|
+
_type: 'file',
|
|
76
|
+
asset: { _ref: baseAsset._id, _type: 'reference' },
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
if (fileType === 'woff2') {
|
|
80
|
+
console.log(`[${i + 1}/${fontObjectKeys.length}][${j + 1}/${files.length}] Generating CSS for: ${fontObject.title}`);
|
|
81
|
+
setStatus(`[${i + 1}/${fontObjectKeys.length}][${j + 1}/${files.length}] Generating CSS for: ${fontObject.title}`);
|
|
82
|
+
newFileInput = await generateCssFile({
|
|
83
|
+
woff2File: file,
|
|
84
|
+
fileInput: newFileInput,
|
|
85
|
+
fontName: fontObject.title,
|
|
86
|
+
fileName: fontObject._id,
|
|
87
|
+
variableFont: fontObject.variableFont,
|
|
88
|
+
weight: fontObject.weight,
|
|
89
|
+
client: client,
|
|
90
|
+
style: fontObject.style,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (fileType === 'ttf') {
|
|
95
|
+
console.log(`[${i + 1}/${fontObjectKeys.length}][${j + 1}/${files.length}] Generating font data for: ${fontObject.title}`);
|
|
96
|
+
setStatus(`[${i + 1}/${fontObjectKeys.length}][${j + 1}/${files.length}] Generating font data for: ${fontObject.title}`);
|
|
97
|
+
const metadata = await generateFontData({
|
|
98
|
+
fontId: fontObject._id,
|
|
99
|
+
url: baseAsset.url,
|
|
100
|
+
fontKit: fontKit,
|
|
101
|
+
client: client,
|
|
102
|
+
commit: false,
|
|
103
|
+
});
|
|
104
|
+
Object.assign(fontObject, metadata);
|
|
105
|
+
}
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.error('Error uploading font:', fontObject.title, err.message);
|
|
108
|
+
setStatus('Error uploading font: ' + err.message);
|
|
109
|
+
setError(true);
|
|
110
|
+
failedFiles.push({ name: file.name, fk: fontKit });
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fontObject.fileInput = newFileInput;
|
|
115
|
+
fontsObjects[id] = fontObject;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Create or update Sanity documents
|
|
120
|
+
console.log('Creating/updating Sanity font documents:', fontsObjects);
|
|
121
|
+
|
|
122
|
+
for (let i = 0; i < fontObjectKeys.length; i++) {
|
|
123
|
+
const fontId = fontObjectKeys[i];
|
|
124
|
+
const font = fontsObjects[fontId];
|
|
125
|
+
|
|
126
|
+
const fontRef = await createOrUpdateFontDocument(font, client, setError);
|
|
127
|
+
|
|
128
|
+
if (fontRef) {
|
|
129
|
+
if (!font.variableFont) {
|
|
130
|
+
addToFontRefs(fontRef, font, fontRefs, stylesObject);
|
|
131
|
+
} else {
|
|
132
|
+
addToVariableRefs(fontRef, font, variableRefs, stylesObject);
|
|
133
|
+
}
|
|
134
|
+
console.log(`[${i + 1}/${fontObjectKeys.length}] ${fontRef._ref} created`);
|
|
135
|
+
setStatus(`[${i + 1}/${fontObjectKeys.length}] ${fontRef._ref} created`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { fontRefs, variableRefs, failedFiles };
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Returns the file extension for a font file.
|
|
144
|
+
* @param {File} file
|
|
145
|
+
* @returns {string}
|
|
146
|
+
*/
|
|
147
|
+
const determineFileType = (file) => {
|
|
148
|
+
if (file.name.endsWith('.otf')) return 'otf';
|
|
149
|
+
if (file.name.endsWith('.ttf')) return 'ttf';
|
|
150
|
+
if (file.name.endsWith('.woff')) return 'woff';
|
|
151
|
+
if (file.name.endsWith('.woff2')) return 'woff2';
|
|
152
|
+
if (file.name.endsWith('.eot')) return 'eot';
|
|
153
|
+
if (file.name.endsWith('.svg')) return 'svg';
|
|
154
|
+
return '';
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Resolves whether a font document already exists in Sanity, returning match details
|
|
159
|
+
* and a recommendation for how to proceed.
|
|
160
|
+
*
|
|
161
|
+
* Resolution strategies (in priority order):
|
|
162
|
+
* 1. Exact _id match or draft _id match or slug.current match
|
|
163
|
+
* 2. Content match by typefaceName + weightName + style + subfamily + variableFont
|
|
164
|
+
*
|
|
165
|
+
* @param {Object} font - The font object with _id, typefaceName, weightName, style, subfamily, variableFont
|
|
166
|
+
* @param {Object} client - Sanity client (parameterized queries only)
|
|
167
|
+
* @returns {Promise<{ exact: Object|null, candidates: Object[], recommendation: string }>}
|
|
168
|
+
*/
|
|
169
|
+
export const resolveExistingFont = async (font, client) => {
|
|
170
|
+
const result = { exact: null, candidates: [], recommendation: 'create' };
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
// Strategy 1: ID / slug match
|
|
174
|
+
const idMatches = await client.fetch(
|
|
175
|
+
`*[_type == 'font' && (_id == $id || _id == $draftId || slug.current == $id)]{
|
|
176
|
+
_id, title, weight, style, weightName, typefaceName, subfamily, variableFont,
|
|
177
|
+
fileInput, description, metaData, metrics, opentypeFeatures, characterSet,
|
|
178
|
+
scriptFileInput, variableInstanceReferences
|
|
179
|
+
}`,
|
|
180
|
+
{ id: font._id, draftId: `drafts.${font._id}` }
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
if (idMatches.length > 0) {
|
|
184
|
+
result.exact = idMatches[0];
|
|
185
|
+
result.recommendation = 'use-exact';
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Strategy 2: Content match (only when ID query returns nothing)
|
|
190
|
+
const subfamily = font.subfamily || '';
|
|
191
|
+
const contentMatches = await client.fetch(
|
|
192
|
+
`*[_type == 'font'
|
|
193
|
+
&& lower(typefaceName) == lower($typefaceName)
|
|
194
|
+
&& lower(weightName) == lower($weightName)
|
|
195
|
+
&& lower(style) == lower($style)
|
|
196
|
+
&& (variableFont == $variableFont || (!defined(variableFont) && $variableFont == false))
|
|
197
|
+
&& (
|
|
198
|
+
lower(coalesce(subfamily, '')) == lower($subfamily)
|
|
199
|
+
|| (lower(coalesce(subfamily, '')) in ['', 'regular'] && lower($subfamily) in ['', 'regular'])
|
|
200
|
+
)
|
|
201
|
+
]{
|
|
202
|
+
_id, title, weight, style, weightName, typefaceName, subfamily, variableFont,
|
|
203
|
+
fileInput, description, metaData, metrics, opentypeFeatures, characterSet,
|
|
204
|
+
scriptFileInput, variableInstanceReferences
|
|
205
|
+
}`,
|
|
206
|
+
{
|
|
207
|
+
typefaceName: font.typefaceName,
|
|
208
|
+
weightName: font.weightName || '',
|
|
209
|
+
style: font.style || 'Regular',
|
|
210
|
+
variableFont: font.variableFont || false,
|
|
211
|
+
subfamily: subfamily === '' ? 'regular' : subfamily,
|
|
212
|
+
}
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
if (contentMatches.length === 1) {
|
|
216
|
+
result.candidates = contentMatches;
|
|
217
|
+
result.recommendation = 'use-candidate';
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (contentMatches.length > 1) {
|
|
222
|
+
result.candidates = contentMatches;
|
|
223
|
+
result.recommendation = 'ambiguous';
|
|
224
|
+
console.warn(`Ambiguous font match for "${font.title}" — ${contentMatches.length} candidates found:`,
|
|
225
|
+
contentMatches.map(c => c._id));
|
|
226
|
+
return result;
|
|
227
|
+
}
|
|
228
|
+
} catch (err) {
|
|
229
|
+
console.error('Error resolving existing font:', font._id, err.message);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return result;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Creates a new font document or updates an existing one, returning its reference.
|
|
237
|
+
* Uses resolveExistingFont to determine whether to create or update.
|
|
238
|
+
* @param {Object} font
|
|
239
|
+
* @param {Object} client
|
|
240
|
+
* @param {Function} setError
|
|
241
|
+
* @returns {Promise<Object|null>}
|
|
242
|
+
*/
|
|
243
|
+
const createOrUpdateFontDocument = async (font, client, setError) => {
|
|
244
|
+
try {
|
|
245
|
+
const { exact, candidates, recommendation } = await resolveExistingFont(font, client);
|
|
246
|
+
|
|
247
|
+
const { files, fontKit } = font;
|
|
248
|
+
delete font.files;
|
|
249
|
+
delete font.fontKit;
|
|
250
|
+
// Remove temp field used by preserveFileNames — must not be saved to Sanity
|
|
251
|
+
delete font.originalFilename;
|
|
252
|
+
|
|
253
|
+
if (font.variableFont && font.variableInstances) {
|
|
254
|
+
const instanceMappings = await parseVariableFontInstances(font, client);
|
|
255
|
+
if (instanceMappings.length > 0) {
|
|
256
|
+
font.variableInstanceReferences = instanceMappings;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
let fontResponse;
|
|
261
|
+
if (recommendation === 'use-exact' && exact) {
|
|
262
|
+
fontResponse = await updateExistingFont(font, exact, client);
|
|
263
|
+
} else if (recommendation === 'use-candidate' && candidates.length === 1) {
|
|
264
|
+
// Reassign font._id to match the existing document so inbound references resolve
|
|
265
|
+
console.log(`Content-match: reassigning "${font._id}" → "${candidates[0]._id}"`);
|
|
266
|
+
font._id = candidates[0]._id;
|
|
267
|
+
fontResponse = await updateExistingFont(font, candidates[0], client);
|
|
268
|
+
} else {
|
|
269
|
+
// 'ambiguous' or 'create' — create a new document
|
|
270
|
+
fontResponse = await createNewFont(font, client);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
_key: nanoid(),
|
|
275
|
+
_type: 'reference',
|
|
276
|
+
_ref: fontResponse._id,
|
|
277
|
+
_weak: true,
|
|
278
|
+
};
|
|
279
|
+
} catch (e) {
|
|
280
|
+
console.error('Error creating font:', font.title, font.subfamily, e);
|
|
281
|
+
setError(true);
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Patches an existing font document, merging file references and preserving stored metadata.
|
|
288
|
+
* @param {Object} font
|
|
289
|
+
* @param {Object} existingFont
|
|
290
|
+
* @param {Object} client
|
|
291
|
+
* @returns {Promise<Object>}
|
|
292
|
+
*/
|
|
293
|
+
const updateExistingFont = async (font, existingFont, client) => {
|
|
294
|
+
if (existingFont.fileInput) {
|
|
295
|
+
const newFileInput = { ...font.fileInput };
|
|
296
|
+
Object.keys(existingFont.fileInput).forEach(key => {
|
|
297
|
+
if (!newFileInput[key]) newFileInput[key] = existingFont.fileInput[key];
|
|
298
|
+
});
|
|
299
|
+
font.fileInput = newFileInput;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
font.metaData = !font?.metaData || Object.keys(font?.metaData || {}).length === 0
|
|
303
|
+
? existingFont?.metaData || {}
|
|
304
|
+
: font.metaData;
|
|
305
|
+
font.metrics = !font?.metrics || Object.keys(font?.metrics || {}).length === 0
|
|
306
|
+
? existingFont?.metrics || {}
|
|
307
|
+
: font.metrics;
|
|
308
|
+
font.opentypeFeatures = !font?.opentypeFeatures || Object.keys(font?.opentypeFeatures || {}).length === 0
|
|
309
|
+
? existingFont?.opentypeFeatures || {}
|
|
310
|
+
: font.opentypeFeatures;
|
|
311
|
+
font.characterSet = !font?.characterSet || Object.keys(font?.characterSet || {}).length === 0
|
|
312
|
+
? existingFont?.characterSet || {}
|
|
313
|
+
: font.characterSet;
|
|
314
|
+
font.scriptFileInput = existingFont?.scriptFileInput || {};
|
|
315
|
+
|
|
316
|
+
cleanMetadataValues(font);
|
|
317
|
+
|
|
318
|
+
if (font.variableFont && existingFont?.variableInstanceReferences &&
|
|
319
|
+
(!font.variableInstanceReferences || font.variableInstanceReferences.length === 0)) {
|
|
320
|
+
font.variableInstanceReferences = existingFont.variableInstanceReferences;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
console.log('Updating existing font:', font._id, font.title);
|
|
324
|
+
|
|
325
|
+
const patchObject = {
|
|
326
|
+
fileInput: font.fileInput,
|
|
327
|
+
subfamily: font.subfamily,
|
|
328
|
+
weight: font.weight,
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
if (font.variableInstanceReferences) {
|
|
332
|
+
patchObject.variableInstanceReferences = font.variableInstanceReferences;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return await client.patch(font._id).set(patchObject).commit();
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Creates a new font document in Sanity.
|
|
340
|
+
* @param {Object} font
|
|
341
|
+
* @param {Object} client
|
|
342
|
+
* @returns {Promise<Object>}
|
|
343
|
+
*/
|
|
344
|
+
const createNewFont = async (font, client) => {
|
|
345
|
+
console.log('Creating new font:', font._id, font.title);
|
|
346
|
+
if (font.metaData) cleanMetadataValues(font);
|
|
347
|
+
|
|
348
|
+
const newDocument = {
|
|
349
|
+
_key: nanoid(),
|
|
350
|
+
_id: font._id,
|
|
351
|
+
_type: 'font',
|
|
352
|
+
...font,
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
return await client.createOrReplace(newDocument);
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Removes null metadata values and strips control characters from string values.
|
|
360
|
+
* @param {Object} font
|
|
361
|
+
*/
|
|
362
|
+
const cleanMetadataValues = (font) => {
|
|
363
|
+
if (!font.metaData) return;
|
|
364
|
+
Object.keys(font.metaData).forEach(key => {
|
|
365
|
+
if (font.metaData[key] == null) {
|
|
366
|
+
font.metaData[key] = '';
|
|
367
|
+
} else {
|
|
368
|
+
font.metaData[key] = font.metaData[key].replace(/[\x00-\x1f]/g, '');
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Adds a font reference to fontRefs if it's not already in the existing styles.
|
|
375
|
+
* @param {Object} fontRef
|
|
376
|
+
* @param {Object} font
|
|
377
|
+
* @param {Array} fontRefs
|
|
378
|
+
* @param {Object} stylesObject
|
|
379
|
+
*/
|
|
380
|
+
const addToFontRefs = (fontRef, font, fontRefs, stylesObject) => {
|
|
381
|
+
if (stylesObject.fonts && stylesObject.fonts.length > 0) {
|
|
382
|
+
const fontExists = stylesObject.fonts.findIndex(f => f._ref === fontRef._ref);
|
|
383
|
+
const inFontRefs = fontRefs.findIndex(f => f._ref === fontRef._ref);
|
|
384
|
+
if (fontExists === -1 && inFontRefs === -1) fontRefs.push(fontRef);
|
|
385
|
+
} else {
|
|
386
|
+
fontRefs.push(fontRef);
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Adds a variable font reference to variableRefs if it's not already in the existing styles.
|
|
392
|
+
* @param {Object} fontRef
|
|
393
|
+
* @param {Object} font
|
|
394
|
+
* @param {Array} variableRefs
|
|
395
|
+
* @param {Object} stylesObject
|
|
396
|
+
*/
|
|
397
|
+
const addToVariableRefs = (fontRef, font, variableRefs, stylesObject) => {
|
|
398
|
+
if (stylesObject?.variableFont?.length) {
|
|
399
|
+
const vfExists = stylesObject.variableFont.findIndex(f => f._ref === fontRef._ref);
|
|
400
|
+
const inVariableRefs = variableRefs.findIndex(f => f._ref === fontRef._ref);
|
|
401
|
+
if (vfExists === -1 && inVariableRefs === -1 && font.variableFont) variableRefs.push(fontRef);
|
|
402
|
+
} else {
|
|
403
|
+
variableRefs.push(fontRef);
|
|
404
|
+
}
|
|
405
|
+
};
|