@aglyn/tenant-runtime 1.0.0-beta.230 → 1.0.0-beta.231

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/tenant-runtime",
3
- "version": "1.0.0-beta.230",
3
+ "version": "1.0.0-beta.231",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,11 +25,11 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.230",
29
- "@aglyn/shared-data-types": "1.0.0-beta.230",
30
- "@aglyn/shared-ui-theme": "1.0.0-beta.230",
31
- "@aglyn/shared-util-http": "1.0.0-beta.230",
32
- "@aglyn/tenant-data-admin": "1.0.0-beta.230",
28
+ "@aglyn/aglyn": "1.0.0-beta.231",
29
+ "@aglyn/shared-data-types": "1.0.0-beta.231",
30
+ "@aglyn/shared-ui-theme": "1.0.0-beta.231",
31
+ "@aglyn/shared-util-http": "1.0.0-beta.231",
32
+ "@aglyn/tenant-data-admin": "1.0.0-beta.231",
33
33
  "@swc/helpers": "0.5.23"
34
34
  },
35
35
  "peerDependencies": {
@@ -14,24 +14,101 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
- import type { HostTheme } from '@aglyn/shared-data-types';
17
+ import type { HostTheme, HostThemeFont } from '@aglyn/shared-data-types';
18
18
  /** A font file's URL never changes its bytes: the version is in the path. */
19
19
  export declare const SELF_HOSTED_FONT_CACHE_CONTROL = "public, max-age=31536000, s-maxage=31536000, immutable";
20
- /** Test seam: the process cache would otherwise leak between cases. */
20
+ /** Test seam: the process caches would otherwise leak between cases. */
21
21
  export declare function resetSelfHostedFontsForTests(): void;
22
22
  /** What a page inlines in its head for its theme's fonts. */
23
23
  export interface SelfHostedThemeFonts {
24
- /** The `@font-face` rules, every `src` on the site's own origin. */
24
+ /**
25
+ * The `@font-face` rules, every `src` on the site's own origin, and each
26
+ * family's metric-matched fallback.
27
+ */
25
28
  css: string;
26
29
  /** The one or two files the first screen paints with, to preload. */
27
30
  preloads: string[];
28
31
  }
32
+ export interface SelfHostedThemeFontsOptions {
33
+ /** The site rendering, which an uploaded font's media URL is qualified with. */
34
+ hostId?: string | null;
35
+ /**
36
+ * The typography the site's theme is layered onto
37
+ * (`siteBaseOptions(...).typography`): a text style the theme leaves alone
38
+ * draws with the base's weight and family.
39
+ */
40
+ baseTypography?: Record<string, unknown>;
41
+ /**
42
+ * Fonts the base theme draws with that the site's theme does not list —
43
+ * the platform brand's face on the operator's own hosts.
44
+ */
45
+ baseFonts?: readonly HostThemeFont[];
46
+ }
47
+ /**
48
+ * An uploaded face's CSS `font-weight`: its weight, or for a variable file
49
+ * the range it draws (`100 900`), so every weight in between is drawn from
50
+ * the file rather than synthesized.
51
+ */
52
+ export declare function customFaceWeight(face: {
53
+ weight: number;
54
+ weightMax?: number;
55
+ }): string;
29
56
  /**
30
- * The theme's Google fonts as rules to inline and files to preload, or null
31
- * when the theme loads none or Google could not be read — in which case the
32
- * caller links the stylesheet as before.
57
+ * The theme's web fonts as rules to inline and files to preload, or null when
58
+ * the theme loads none.
59
+ *
60
+ * Every face is on the site's own origin: a Google face through
61
+ * {@link serveSelfHostedFont}, an uploaded one through the media route. When
62
+ * Google cannot be read the family is left out rather than linked from
63
+ * Google, and its metric-matched fallback still draws the text in the font's
64
+ * box; the page asks again on its next render.
65
+ */
66
+ export declare function selfHostedThemeFonts(theme: Pick<HostTheme, 'fonts' | 'typography'> | undefined, options?: SelfHostedThemeFontsOptions): Promise<SelfHostedThemeFonts | null>;
67
+ /** One file a theme's text is drawn from, as the font picker prices it. */
68
+ export interface ThemeFontCostFile {
69
+ style: 'normal' | 'italic';
70
+ /** A weight (`400`) or, for a variable file, its range (`100 900`). */
71
+ weight: string;
72
+ /** Its size in bytes; null when it could not be read. */
73
+ bytes: number | null;
74
+ }
75
+ /** What one family costs a visitor. */
76
+ export interface ThemeFontFamilyCost {
77
+ family: string;
78
+ source: 'google' | 'custom';
79
+ /** The Latin files, one per file however many faces it holds. */
80
+ files: ThemeFontCostFile[];
81
+ /** The bytes of the files whose size is known. */
82
+ bytes: number;
83
+ /** Every file was found and measured. */
84
+ complete: boolean;
85
+ }
86
+ /** What a theme's fonts cost a visitor, family by family. */
87
+ export interface ThemeFontDeliveryCost {
88
+ families: ThemeFontFamilyCost[];
89
+ bytes: number;
90
+ files: number;
91
+ complete: boolean;
92
+ }
93
+ /**
94
+ * What a theme's web fonts cost a visitor (AGL-3656): the Latin files a
95
+ * published page draws its text styles and italics from, and their sizes,
96
+ * chosen exactly as
97
+ * {@link selfHostedThemeFonts} chooses them — the same faces, the same
98
+ * nearest weights, the same pick between a file per weight and the variable
99
+ * file. The font picker shows it beside each choice.
100
+ *
101
+ * Latin, because that is the file an English-language page downloads: the
102
+ * other subsets are declared with their `unicode-range` and fetched only for
103
+ * text that needs them. A file holding several faces (the variable file, a
104
+ * shared range) is counted once.
105
+ *
106
+ * Bounded and cached like the page's own load, and never throws: anything it
107
+ * could not read is left out and the answer says it is not `complete`. An
108
+ * uploaded font's files are counted from the theme, and sized when the theme
109
+ * records their size.
33
110
  */
34
- export declare function selfHostedThemeFonts(theme: Pick<HostTheme, 'fonts' | 'typography'> | undefined): Promise<SelfHostedThemeFonts | null>;
111
+ export declare function themeFontDeliveryCost(theme: Pick<HostTheme, 'fonts' | 'typography'> | undefined, options?: SelfHostedThemeFontsOptions): Promise<ThemeFontDeliveryCost>;
35
112
  /**
36
113
  * One font file, fetched from Google and answered from the site's origin with
37
114
  * a year-long `immutable` policy. `path` is the part below `/s/` on Google's
@@ -15,34 +15,42 @@
15
15
  * limitations under the License.
16
16
  */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
17
  import { boundedAwait, createSettledValueCache } from "@aglyn/shared-util-http/bounded-await";
18
+ import { resolveMediaSrc } from "@aglyn/aglyn/app-utils/media-ref";
19
+ import { themeFontFacts } from "@aglyn/aglyn/plugin-manager/plugin-theme-font-catalog";
18
20
  // By path: the theme lib's barrel carries React providers a server module
19
21
  // has no use for (AGL-405).
20
- import { getGoogleFontsUrl } from "@aglyn/shared-ui-theme/util/host-theme";
21
- import { googleFontFileUrl, isSelfHostedFontPath, parseGoogleFontFaces, selfHostedFontFaceCss, selfHostedFontPreloads } from "@aglyn/shared-ui-theme/util/self-hosted-fonts";
22
+ import { googleFontFileUrl, googleFontSheetUrl, isSelfHostedFontPath, mergeSharedFiles, metricFallbackFontFaceCss, offeredWeights, parseGoogleFontFaces, siteFontFaceCss, siteFontFaceFromGoogle, siteFontPreloads, themeFontNeeds } from "@aglyn/shared-ui-theme/util/self-hosted-fonts";
22
23
  /**
23
- * The tenant's half of self-hosted theme fonts (AGL-3485): fetching Google's
24
- * stylesheet for a theme, and serving the files it names from the site's own
25
- * origin. What is done with the stylesheet, and why, is the note on
24
+ * The tenant's half of self-hosted theme fonts (AGL-3485, AGL-3656): working
25
+ * out which faces a theme draws with, asking Google for them, choosing how
26
+ * each family is delivered, and serving the files from the site's own
27
+ * origin. What is done with the rules, and why, is the note on
26
28
  * `@aglyn/shared-ui-theme/util/self-hosted-fonts`.
27
29
  */ /**
28
30
  * A current desktop Chrome. Google's CSS2 API picks the font FORMAT from the
29
31
  * user agent, and a server's default agent is answered with TrueType; this
30
32
  * one is answered with WOFF2, the format every browser a site supports reads.
31
33
  */ const WOFF2_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ' + '(KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36';
32
- /** How long one process trusts a stylesheet it fetched. */ const STYLESHEET_TTL_MS = 24 * 60 * 60 * 1000;
34
+ /** How long one process trusts a stylesheet, a file size or a choice. */ const STYLESHEET_TTL_MS = 24 * 60 * 60 * 1000;
33
35
  /** How long a failed fetch is remembered before it is tried again. */ const FAILURE_TTL_MS = 5 * 60 * 1000;
34
- /** How long a page render waits for Google before falling back. */ const STYLESHEET_TIMEOUT_MS = 2500;
36
+ /** How long a page render waits for Google before going without. */ const STYLESHEET_TIMEOUT_MS = 2500;
37
+ /** How long a render waits for a file's size before choosing without it. */ const SIZE_TIMEOUT_MS = 1500;
35
38
  /** The largest font file the route will pass on. */ const MAX_FONT_BYTES = 2 * 1024 * 1024;
36
39
  /** A font file's URL never changes its bytes: the version is in the path. */ export const SELF_HOSTED_FONT_CACHE_CONTROL = 'public, max-age=31536000, s-maxage=31536000, immutable';
37
40
  /**
38
- * What this process learned about each stylesheet: the faces, or null for a
39
- * failure. VALUES, never an in-flight promise — the settled-value cache
40
- * refuses one. A promise held across requests is awaited by renders that did
41
- * not create it, and one that never settles stalled every page of every site
42
- * on the instance until the function timed out (AGL-3565).
41
+ * What this process learned: each stylesheet's faces, each file's size, and
42
+ * each family's delivery. VALUES, never an in-flight promise — the
43
+ * settled-value cache refuses one. A promise held across requests is awaited
44
+ * by renders that did not create it, and one that never settles stalled every
45
+ * page of every site on the instance until the function timed out
46
+ * (AGL-3565).
43
47
  */ const stylesheets = createSettledValueCache();
44
- /** Test seam: the process cache would otherwise leak between cases. */ export function resetSelfHostedFontsForTests() {
48
+ const fileSizes = createSettledValueCache();
49
+ const deliveries = createSettledValueCache();
50
+ /** Test seam: the process caches would otherwise leak between cases. */ export function resetSelfHostedFontsForTests() {
45
51
  stylesheets.clear();
52
+ fileSizes.clear();
53
+ deliveries.clear();
46
54
  }
47
55
  /**
48
56
  * Google's faces for a stylesheet URL, or null; never waits past `ms`. The
@@ -65,26 +73,331 @@ import { googleFontFileUrl, isSelfHostedFontPath, parseGoogleFontFaces, selfHost
65
73
  }
66
74
  /**
67
75
  * Google's faces for a stylesheet URL, from this process's cache when it has
68
- * them. Null when the fetch failed, timed out or answered with nothing usable;
69
- * the page then links the stylesheet as it always did, rather than losing its
70
- * font. Asked through `fetch` with a day's revalidation, which on the server is
71
- * Next's data cache: a cold process reads the stored stylesheet rather than
72
- * asking Google again.
76
+ * them; null when the fetch failed, timed out or answered with nothing
77
+ * usable. Asked through `fetch` with a day's revalidation, which on the
78
+ * server is Next's data cache: a cold process reads the stored stylesheet
79
+ * rather than asking Google again.
73
80
  */ function facesFor(url) {
74
81
  return stylesheets.readThrough(url, ()=>fetchFaces(url, STYLESHEET_TIMEOUT_MS), (faces)=>faces ? STYLESHEET_TTL_MS : FAILURE_TTL_MS);
75
82
  }
83
+ /** A Google file's size in bytes, from a HEAD; null when it could not be read. */ function fileSize(path) {
84
+ return fileSizes.readThrough(path, ()=>boundedAwait((signal)=>fetch(googleFontFileUrl(path), {
85
+ method: 'HEAD',
86
+ signal,
87
+ cache: 'force-cache'
88
+ }).then((response)=>{
89
+ const length = Number(response.headers.get('content-length'));
90
+ return response.ok && length > 0 ? length : null;
91
+ }).catch(()=>null), SIZE_TIMEOUT_MS, null, 'self-hosted-fonts.size'), (size)=>size ? STYLESHEET_TTL_MS : FAILURE_TTL_MS);
92
+ }
93
+ /** The Latin file of a weight (upright unless asked) among some faces, if there is one. */ function latinFileOf(faces, weight, style = 'normal') {
94
+ var _faces_find;
95
+ return (_faces_find = faces.find((face)=>{
96
+ if (face.style !== style || face.subset && face.subset !== 'latin') return false;
97
+ const [low, high = low] = face.weight.split(' ').map(Number);
98
+ return weight >= low && weight <= high;
99
+ })) == null ? void 0 : _faces_find.path;
100
+ }
101
+ /**
102
+ * Each face from its own stylesheet, so a weight Google refuses costs that
103
+ * face rather than the family. One weight per request is also what makes
104
+ * Google answer a variable family with a static instance of that weight.
105
+ */ async function facesOneByOne(family, faces) {
106
+ const sheets = await Promise.all(faces.map((face)=>{
107
+ const url = googleFontSheetUrl(family, [
108
+ face
109
+ ]);
110
+ return url ? facesFor(url) : Promise.resolve(null);
111
+ }));
112
+ const found = sheets.flatMap((sheet)=>sheet != null ? sheet : []);
113
+ return found.length ? found : null;
114
+ }
115
+ /**
116
+ * The faces of a family the page asks Google for: each wanted weight as the
117
+ * nearest one the family offers, and an italic only of a family the catalog
118
+ * says has one — a request for one it lacks is refused, and a slanted upright
119
+ * is what the browser would draw anyway.
120
+ */ function facesToAsk(need, facts) {
121
+ var _facts_italics;
122
+ const weights = offeredWeights(need.weights, facts == null ? void 0 : facts.weights);
123
+ const italics = (facts == null ? void 0 : (_facts_italics = facts.italics) == null ? void 0 : _facts_italics.length) ? offeredWeights(need.italics, facts.italics) : [];
124
+ return [
125
+ ...weights.map((weight)=>({
126
+ weight,
127
+ style: 'normal'
128
+ })),
129
+ ...italics.map((weight)=>({
130
+ weight,
131
+ style: 'italic'
132
+ }))
133
+ ];
134
+ }
135
+ /**
136
+ * How one Google family is delivered: as a file per weight, or as the
137
+ * family's variable file holding every weight, whichever is smaller for the
138
+ * weights the page's text styles draw with (AGL-3656).
139
+ *
140
+ * A theme that sets body text and headlines in two weights of Open Sans pays
141
+ * 27 KB for two static Latin files rather than 43 KB for the variable one;
142
+ * aglyn.com, which draws Roboto Flex at seven weights, pays 34 KB for the
143
+ * variable file rather than seven statics. The sizes are the files' own,
144
+ * read once per process with a HEAD; a size that cannot be read leaves the
145
+ * statics, which put the smallest file on the first screen.
146
+ *
147
+ * A family that is not variable, or one the catalog does not know, is asked
148
+ * for a face at a time.
149
+ */ function deliveryFor(need, facts) {
150
+ const weights = offeredWeights(need.weights, facts == null ? void 0 : facts.weights);
151
+ const wanted = facesToAsk(need, facts);
152
+ const key = `${need.family.toLowerCase()}|${wanted.map((face)=>`${face.style[0]}${face.weight}`).join(',')}`;
153
+ return deliveries.readThrough(key, async ()=>{
154
+ var _drawn_;
155
+ if (!(facts == null ? void 0 : facts.variableWeights)) {
156
+ const faces = await facesOneByOne(need.family, wanted);
157
+ return {
158
+ faces,
159
+ decided: !!faces
160
+ };
161
+ }
162
+ const combinedUrl = googleFontSheetUrl(need.family, wanted);
163
+ const [statics, combined] = await Promise.all([
164
+ facesOneByOne(need.family, wanted),
165
+ combinedUrl ? facesFor(combinedUrl) : Promise.resolve(null)
166
+ ]);
167
+ if (!statics || !combined) return {
168
+ faces: statics != null ? statics : combined,
169
+ decided: false
170
+ };
171
+ const drawn = offeredWeights(need.textWeights.length ? need.textWeights : weights, facts.weights);
172
+ const staticPaths = [
173
+ ...new Set(drawn.map((weight)=>latinFileOf(statics, weight)))
174
+ ];
175
+ const combinedPath = latinFileOf(combined, (_drawn_ = drawn[0]) != null ? _drawn_ : 400);
176
+ if (!combinedPath || staticPaths.some((path)=>!path)) return {
177
+ faces: statics,
178
+ decided: false
179
+ };
180
+ const [combinedSize, ...staticSizes] = await Promise.all([
181
+ combinedPath,
182
+ ...staticPaths
183
+ ].map(fileSize));
184
+ if (!combinedSize || staticSizes.some((size)=>!size)) return {
185
+ faces: statics,
186
+ decided: false
187
+ };
188
+ const staticTotal = staticSizes.reduce((sum, size)=>sum + size, 0);
189
+ return {
190
+ faces: combinedSize < staticTotal ? combined : statics,
191
+ decided: true
192
+ };
193
+ }, // A choice made without every sheet and size is the statics by default,
194
+ // so it is kept only as long as a failure: the next render after that
195
+ // asks again rather than serving the default for a day.
196
+ (result)=>result.decided ? STYLESHEET_TTL_MS : FAILURE_TTL_MS).then((result)=>result.faces);
197
+ }
198
+ /** The category a stack's generic keyword names, for a font with none recorded. */ function categoryFromStack(stack) {
199
+ if (typeof stack !== 'string') return 'sans-serif';
200
+ if (/\bmonospace\b/i.test(stack)) return 'monospace';
201
+ if (/(^|,)\s*serif\b/i.test(stack)) return 'serif';
202
+ return 'sans-serif';
203
+ }
204
+ /**
205
+ * An uploaded face's CSS `font-weight`: its weight, or for a variable file
206
+ * the range it draws (`100 900`), so every weight in between is drawn from
207
+ * the file rather than synthesized.
208
+ */ export function customFaceWeight(face) {
209
+ const weight = Math.min(1000, Math.max(1, Math.round(Number(face.weight) || 400)));
210
+ const max = Math.round(Number(face.weightMax));
211
+ return Number.isFinite(max) && max > weight ? `${weight} ${Math.min(1000, max)}` : String(weight);
212
+ }
213
+ /**
214
+ * The theme's web fonts as rules to inline and files to preload, or null when
215
+ * the theme loads none.
216
+ *
217
+ * Every face is on the site's own origin: a Google face through
218
+ * {@link serveSelfHostedFont}, an uploaded one through the media route. When
219
+ * Google cannot be read the family is left out rather than linked from
220
+ * Google, and its metric-matched fallback still draws the text in the font's
221
+ * box; the page asks again on its next render.
222
+ */ export async function selfHostedThemeFonts(theme, options = {}) {
223
+ var _ref, _options_baseFonts, _ref1;
224
+ var _theme_typography, _options_baseTypography;
225
+ const fonts = [
226
+ ...(_ref = theme == null ? void 0 : theme.fonts) != null ? _ref : [],
227
+ ...(_options_baseFonts = options.baseFonts) != null ? _options_baseFonts : []
228
+ ];
229
+ const needs = themeFontNeeds(theme, options.baseTypography, options.baseFonts);
230
+ if (!needs.length) return null;
231
+ const stack = (_ref1 = theme == null ? void 0 : (_theme_typography = theme.typography) == null ? void 0 : _theme_typography.fontFamily) != null ? _ref1 : (_options_baseTypography = options.baseTypography) == null ? void 0 : _options_baseTypography['fontFamily'];
232
+ const families = await Promise.all(needs.map(async (need)=>{
233
+ var _ref, _font_metrics, _ref1, _font_category;
234
+ const font = fonts.find((entry)=>entry.family.trim().toLowerCase() === need.family.toLowerCase());
235
+ if (!font) return {
236
+ faces: [],
237
+ fallback: ''
238
+ };
239
+ if (font.source === 'custom') {
240
+ var _font_faces, _font_category1;
241
+ const faces = ((_font_faces = font.faces) != null ? _font_faces : []).flatMap((face)=>{
242
+ const url = resolveMediaSrc(face.src, {
243
+ hostId: options.hostId,
244
+ version: face.version
245
+ });
246
+ if (!url || face.style !== 'normal' && face.style !== 'italic') return [];
247
+ return [
248
+ _extends({
249
+ family: font.family.trim(),
250
+ style: face.style,
251
+ weight: customFaceWeight(face),
252
+ url
253
+ }, face.unicodeRange ? {
254
+ unicodeRange: face.unicodeRange
255
+ } : {})
256
+ ];
257
+ });
258
+ return {
259
+ faces,
260
+ fallback: faces.length ? metricFallbackFontFaceCss(font.family, font.metrics, (_font_category1 = font.category) != null ? _font_category1 : categoryFromStack(stack)) : ''
261
+ };
262
+ }
263
+ const facts = await themeFontFacts(need.family);
264
+ const google = (_ref = await deliveryFor(need, facts)) != null ? _ref : [];
265
+ const metrics = (_font_metrics = font.metrics) != null ? _font_metrics : facts == null ? void 0 : facts.metrics;
266
+ return {
267
+ faces: google.map(siteFontFaceFromGoogle),
268
+ fallback: metricFallbackFontFaceCss(font.family, metrics, (_ref1 = (_font_category = font.category) != null ? _font_category : facts == null ? void 0 : facts.category) != null ? _ref1 : categoryFromStack(stack))
269
+ };
270
+ }));
271
+ const faces = mergeSharedFiles(families.flatMap((family)=>family.faces));
272
+ const css = siteFontFaceCss(faces) + families.map((family)=>family.fallback).join('');
273
+ if (!css) return null;
274
+ return {
275
+ css,
276
+ preloads: siteFontPreloads(_extends({}, theme, {
277
+ fonts
278
+ }), faces, options.baseTypography)
279
+ };
280
+ }
281
+ /** The most families one question prices: a theme sets text in two or three. */ const COST_MAX_FAMILIES = 6;
76
282
  /**
77
- * The theme's Google fonts as rules to inline and files to preload, or null
78
- * when the theme loads none or Google could not be read — in which case the
79
- * caller links the stylesheet as before.
80
- */ export async function selfHostedThemeFonts(theme) {
81
- const url = getGoogleFontsUrl(theme == null ? void 0 : theme.fonts);
82
- if (!url) return null;
83
- const faces = await facesFor(url);
84
- if (!faces) return null;
283
+ * What a theme's web fonts cost a visitor (AGL-3656): the Latin files a
284
+ * published page draws its text styles and italics from, and their sizes,
285
+ * chosen exactly as
286
+ * {@link selfHostedThemeFonts} chooses them — the same faces, the same
287
+ * nearest weights, the same pick between a file per weight and the variable
288
+ * file. The font picker shows it beside each choice.
289
+ *
290
+ * Latin, because that is the file an English-language page downloads: the
291
+ * other subsets are declared with their `unicode-range` and fetched only for
292
+ * text that needs them. A file holding several faces (the variable file, a
293
+ * shared range) is counted once.
294
+ *
295
+ * Bounded and cached like the page's own load, and never throws: anything it
296
+ * could not read is left out and the answer says it is not `complete`. An
297
+ * uploaded font's files are counted from the theme, and sized when the theme
298
+ * records their size.
299
+ */ export async function themeFontDeliveryCost(theme, options = {}) {
300
+ const empty = {
301
+ families: [],
302
+ bytes: 0,
303
+ files: 0,
304
+ complete: true
305
+ };
306
+ try {
307
+ var _ref, _options_baseFonts;
308
+ const fonts = [
309
+ ...(_ref = theme == null ? void 0 : theme.fonts) != null ? _ref : [],
310
+ ...(_options_baseFonts = options.baseFonts) != null ? _options_baseFonts : []
311
+ ];
312
+ const needs = themeFontNeeds(theme, options.baseTypography, options.baseFonts).slice(0, COST_MAX_FAMILIES);
313
+ if (!needs.length) return empty;
314
+ const families = await Promise.all(needs.map(async (need)=>{
315
+ const font = fonts.find((entry)=>entry.family.trim().toLowerCase() === need.family.toLowerCase());
316
+ if (!font) return null;
317
+ if (font.source === 'custom') {
318
+ var _font_faces;
319
+ const files = ((_font_faces = font.faces) != null ? _font_faces : []).filter((face)=>face.style === 'normal' || face.style === 'italic').filter((face)=>coversLatinLetters(face.unicodeRange)).map((face)=>{
320
+ const bytes = Number(face.bytes);
321
+ return {
322
+ style: face.style,
323
+ weight: String(Math.round(Number(face.weight) || 400)),
324
+ bytes: Number.isFinite(bytes) && bytes > 0 ? bytes : null
325
+ };
326
+ });
327
+ return familyCost(font.family.trim(), 'custom', files);
328
+ }
329
+ const facts = await themeFontFacts(need.family);
330
+ const delivered = await deliveryFor(need, facts);
331
+ if (!delivered) return {
332
+ family: need.family,
333
+ source: 'google',
334
+ files: [],
335
+ bytes: 0,
336
+ complete: false
337
+ };
338
+ // The weights the text styles draw with, and the italics: a named
339
+ // weight no style uses (MUI's light) is declared and never fetched.
340
+ const wanted = facesToAsk(_extends({}, need, {
341
+ weights: need.textWeights.length ? need.textWeights : need.weights
342
+ }), facts);
343
+ const paths = new Map();
344
+ let missing = false;
345
+ for (const face of wanted){
346
+ const path = latinFileOf(delivered, face.weight, face.style);
347
+ if (!path) {
348
+ missing = true;
349
+ continue;
350
+ }
351
+ const declared = delivered.find((entry)=>entry.path === path);
352
+ if (declared && !paths.has(path)) paths.set(path, declared);
353
+ }
354
+ const entries = [
355
+ ...paths.entries()
356
+ ];
357
+ const sizes = await Promise.all(entries.map(([path])=>fileSize(path)));
358
+ const cost = familyCost(need.family, 'google', entries.map(([, face], index)=>({
359
+ style: face.style,
360
+ weight: face.weight,
361
+ bytes: sizes[index]
362
+ })));
363
+ return missing ? _extends({}, cost, {
364
+ complete: false
365
+ }) : cost;
366
+ }));
367
+ const priced = families.filter((entry)=>!!entry);
368
+ return {
369
+ families: priced,
370
+ bytes: priced.reduce((sum, entry)=>sum + entry.bytes, 0),
371
+ files: priced.reduce((sum, entry)=>sum + entry.files.length, 0),
372
+ complete: priced.every((entry)=>entry.complete)
373
+ };
374
+ } catch (unused) {
375
+ return _extends({}, empty, {
376
+ complete: false
377
+ });
378
+ }
379
+ }
380
+ /** Whether a `unicode-range` holds the Latin letters; none at all holds everything. */ function coversLatinLetters(range) {
381
+ if (!range) return true;
382
+ const letter = 0x41;
383
+ return range.split(',').some((part)=>{
384
+ const match = /^\s*U\+([0-9A-F?]{1,6})(?:-([0-9A-F]{1,6}))?\s*$/i.exec(part);
385
+ if (!match) return false;
386
+ const low = parseInt(match[1].replace(/\?/g, '0'), 16);
387
+ const high = match[2] ? parseInt(match[2], 16) : parseInt(match[1].replace(/\?/g, 'F'), 16);
388
+ return letter >= low && letter <= high;
389
+ });
390
+ }
391
+ function familyCost(family, source, files) {
85
392
  return {
86
- css: selfHostedFontFaceCss(faces),
87
- preloads: selfHostedFontPreloads(theme, faces)
393
+ family,
394
+ source,
395
+ files,
396
+ bytes: files.reduce((sum, file)=>{
397
+ var _file_bytes;
398
+ return sum + ((_file_bytes = file.bytes) != null ? _file_bytes : 0);
399
+ }, 0),
400
+ complete: files.length > 0 && files.every((file)=>file.bytes !== null)
88
401
  };
89
402
  }
90
403
  /**
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/self-hosted-fonts.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { HostTheme } from '@aglyn/shared-data-types'\nimport {\n boundedAwait,\n createSettledValueCache,\n} from '@aglyn/shared-util-http/bounded-await'\n// By path: the theme lib's barrel carries React providers a server module\n// has no use for (AGL-405).\nimport { getGoogleFontsUrl } from '@aglyn/shared-ui-theme/util/host-theme'\nimport {\n type GoogleFontFace,\n googleFontFileUrl,\n isSelfHostedFontPath,\n parseGoogleFontFaces,\n selfHostedFontFaceCss,\n selfHostedFontPreloads,\n} from '@aglyn/shared-ui-theme/util/self-hosted-fonts'\n\n/**\n * The tenant's half of self-hosted theme fonts (AGL-3485): fetching Google's\n * stylesheet for a theme, and serving the files it names from the site's own\n * origin. What is done with the stylesheet, and why, is the note on\n * `@aglyn/shared-ui-theme/util/self-hosted-fonts`.\n */\n\n/**\n * A current desktop Chrome. Google's CSS2 API picks the font FORMAT from the\n * user agent, and a server's default agent is answered with TrueType; this\n * one is answered with WOFF2, the format every browser a site supports reads.\n */\nconst WOFF2_USER_AGENT =\n 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ' +\n '(KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36'\n\n/** How long one process trusts a stylesheet it fetched. */\nconst STYLESHEET_TTL_MS = 24 * 60 * 60 * 1000\n\n/** How long a failed fetch is remembered before it is tried again. */\nconst FAILURE_TTL_MS = 5 * 60 * 1000\n\n/** How long a page render waits for Google before falling back. */\nconst STYLESHEET_TIMEOUT_MS = 2_500\n\n/** The largest font file the route will pass on. */\nconst MAX_FONT_BYTES = 2 * 1024 * 1024\n\n/** A font file's URL never changes its bytes: the version is in the path. */\nexport const SELF_HOSTED_FONT_CACHE_CONTROL =\n 'public, max-age=31536000, s-maxage=31536000, immutable'\n\n/**\n * What this process learned about each stylesheet: the faces, or null for a\n * failure. VALUES, never an in-flight promise — the settled-value cache\n * refuses one. A promise held across requests is awaited by renders that did\n * not create it, and one that never settles stalled every page of every site\n * on the instance until the function timed out (AGL-3565).\n */\nconst stylesheets = createSettledValueCache<string, GoogleFontFace[] | null>()\n\n/** Test seam: the process cache would otherwise leak between cases. */\nexport function resetSelfHostedFontsForTests(): void {\n stylesheets.clear()\n}\n\n/**\n * Google's faces for a stylesheet URL, or null; never waits past `ms`. The\n * deadline is a real timer (`boundedAwait`): an abort signal alone did not\n * bound Next's patched fetch on Vercel, whose promise could never settle.\n */\nfunction fetchFaces(url: string, ms: number): Promise<GoogleFontFace[] | null> {\n return boundedAwait(\n (signal) =>\n fetch(url, {\n headers: { 'User-Agent': WOFF2_USER_AGENT },\n signal,\n next: { revalidate: STYLESHEET_TTL_MS / 1000 },\n } as RequestInit)\n .then(async (response) => {\n if (!response.ok) return null\n const parsed = parseGoogleFontFaces(await response.text())\n return parsed.length ? parsed : null\n })\n .catch(() => null),\n ms,\n null,\n 'self-hosted-fonts.stylesheet',\n )\n}\n\n/**\n * Google's faces for a stylesheet URL, from this process's cache when it has\n * them. Null when the fetch failed, timed out or answered with nothing usable;\n * the page then links the stylesheet as it always did, rather than losing its\n * font. Asked through `fetch` with a day's revalidation, which on the server is\n * Next's data cache: a cold process reads the stored stylesheet rather than\n * asking Google again.\n */\nfunction facesFor(url: string): Promise<GoogleFontFace[] | null> {\n return stylesheets.readThrough(\n url,\n () => fetchFaces(url, STYLESHEET_TIMEOUT_MS),\n (faces) => (faces ? STYLESHEET_TTL_MS : FAILURE_TTL_MS),\n )\n}\n\n/** What a page inlines in its head for its theme's fonts. */\nexport interface SelfHostedThemeFonts {\n /** The `@font-face` rules, every `src` on the site's own origin. */\n css: string\n /** The one or two files the first screen paints with, to preload. */\n preloads: string[]\n}\n\n/**\n * The theme's Google fonts as rules to inline and files to preload, or null\n * when the theme loads none or Google could not be read — in which case the\n * caller links the stylesheet as before.\n */\nexport async function selfHostedThemeFonts(\n theme: Pick<HostTheme, 'fonts' | 'typography'> | undefined,\n): Promise<SelfHostedThemeFonts | null> {\n const url = getGoogleFontsUrl(theme?.fonts)\n if (!url) return null\n const faces = await facesFor(url)\n if (!faces) return null\n return {\n css: selfHostedFontFaceCss(faces),\n preloads: selfHostedFontPreloads(theme, faces),\n }\n}\n\n/**\n * One font file, fetched from Google and answered from the site's origin with\n * a year-long `immutable` policy. `path` is the part below `/s/` on Google's\n * origin (`inter/v18/….woff2`); anything else is a 404, so this route cannot\n * be pointed at any other URL.\n *\n * The edge keeps the answer for the year too (`s-maxage`), so Google is asked\n * once per file per edge region rather than once per visitor.\n */\nexport async function serveSelfHostedFont(path: string): Promise<Response> {\n const notFound = () =>\n new Response('Not found', {\n status: 404,\n headers: { 'Cache-Control': 'public, max-age=300' },\n })\n if (!isSelfHostedFontPath(path)) return notFound()\n let upstream: Response\n try {\n upstream = await fetch(googleFontFileUrl(path), {\n headers: { 'User-Agent': WOFF2_USER_AGENT },\n cache: 'force-cache',\n })\n } catch {\n return new Response('Font unavailable', {\n status: 502,\n headers: { 'Cache-Control': 'no-store' },\n })\n }\n if (upstream.status === 404) return notFound()\n const length = Number(upstream.headers.get('content-length') ?? 0)\n if (!upstream.ok || !upstream.body || length > MAX_FONT_BYTES) {\n return new Response('Font unavailable', {\n status: 502,\n headers: { 'Cache-Control': 'no-store' },\n })\n }\n return new Response(upstream.body, {\n status: 200,\n headers: {\n 'Content-Type': 'font/woff2',\n 'Cache-Control': SELF_HOSTED_FONT_CACHE_CONTROL,\n 'X-Content-Type-Options': 'nosniff',\n // Fonts are fetched in CORS mode; a site reached on two hostnames (its\n // own domain and its platform subdomain) shares one cached answer.\n 'Access-Control-Allow-Origin': '*',\n ...(length ? { 'Content-Length': String(length) } : {}),\n },\n })\n}\n"],"names":["boundedAwait","createSettledValueCache","getGoogleFontsUrl","googleFontFileUrl","isSelfHostedFontPath","parseGoogleFontFaces","selfHostedFontFaceCss","selfHostedFontPreloads","WOFF2_USER_AGENT","STYLESHEET_TTL_MS","FAILURE_TTL_MS","STYLESHEET_TIMEOUT_MS","MAX_FONT_BYTES","SELF_HOSTED_FONT_CACHE_CONTROL","stylesheets","resetSelfHostedFontsForTests","clear","fetchFaces","url","ms","signal","fetch","headers","next","revalidate","then","response","ok","parsed","text","length","catch","facesFor","readThrough","faces","selfHostedThemeFonts","theme","fonts","css","preloads","serveSelfHostedFont","path","upstream","notFound","Response","status","cache","Number","get","body","String"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/self-hosted-fonts.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type {\n HostTheme,\n HostThemeFont,\n HostThemeFontCategory,\n HostThemeFontMetrics,\n} from '@aglyn/shared-data-types'\nimport {\n boundedAwait,\n createSettledValueCache,\n} from '@aglyn/shared-util-http/bounded-await'\nimport { resolveMediaSrc } from '@aglyn/aglyn/app-utils/media-ref'\nimport {\n themeFontFacts,\n type ThemeFontFacts,\n} from '@aglyn/aglyn/plugin-manager/plugin-theme-font-catalog'\n// By path: the theme lib's barrel carries React providers a server module\n// has no use for (AGL-405).\nimport {\n type GoogleFontFace,\n googleFontFileUrl,\n googleFontSheetUrl,\n isSelfHostedFontPath,\n mergeSharedFiles,\n metricFallbackFontFaceCss,\n offeredWeights,\n parseGoogleFontFaces,\n type SiteFontFace,\n siteFontFaceCss,\n siteFontFaceFromGoogle,\n siteFontPreloads,\n type ThemeFontNeed,\n themeFontNeeds,\n} from '@aglyn/shared-ui-theme/util/self-hosted-fonts'\n\n/**\n * The tenant's half of self-hosted theme fonts (AGL-3485, AGL-3656): working\n * out which faces a theme draws with, asking Google for them, choosing how\n * each family is delivered, and serving the files from the site's own\n * origin. What is done with the rules, and why, is the note on\n * `@aglyn/shared-ui-theme/util/self-hosted-fonts`.\n */\n\n/**\n * A current desktop Chrome. Google's CSS2 API picks the font FORMAT from the\n * user agent, and a server's default agent is answered with TrueType; this\n * one is answered with WOFF2, the format every browser a site supports reads.\n */\nconst WOFF2_USER_AGENT =\n 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ' +\n '(KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36'\n\n/** How long one process trusts a stylesheet, a file size or a choice. */\nconst STYLESHEET_TTL_MS = 24 * 60 * 60 * 1000\n\n/** How long a failed fetch is remembered before it is tried again. */\nconst FAILURE_TTL_MS = 5 * 60 * 1000\n\n/** How long a page render waits for Google before going without. */\nconst STYLESHEET_TIMEOUT_MS = 2_500\n\n/** How long a render waits for a file's size before choosing without it. */\nconst SIZE_TIMEOUT_MS = 1_500\n\n/** The largest font file the route will pass on. */\nconst MAX_FONT_BYTES = 2 * 1024 * 1024\n\n/** A font file's URL never changes its bytes: the version is in the path. */\nexport const SELF_HOSTED_FONT_CACHE_CONTROL =\n 'public, max-age=31536000, s-maxage=31536000, immutable'\n\n/**\n * What this process learned: each stylesheet's faces, each file's size, and\n * each family's delivery. VALUES, never an in-flight promise — the\n * settled-value cache refuses one. A promise held across requests is awaited\n * by renders that did not create it, and one that never settles stalled every\n * page of every site on the instance until the function timed out\n * (AGL-3565).\n */\nconst stylesheets = createSettledValueCache<string, GoogleFontFace[] | null>()\nconst fileSizes = createSettledValueCache<string, number | null>()\n/** A family's delivery, and whether it was chosen with every fact in hand. */\ninterface Delivery {\n faces: GoogleFontFace[] | null\n /** False when a sheet or a size was missing and the statics were the default. */\n decided: boolean\n}\nconst deliveries = createSettledValueCache<string, Delivery>()\n\n/** Test seam: the process caches would otherwise leak between cases. */\nexport function resetSelfHostedFontsForTests(): void {\n stylesheets.clear()\n fileSizes.clear()\n deliveries.clear()\n}\n\n/**\n * Google's faces for a stylesheet URL, or null; never waits past `ms`. The\n * deadline is a real timer (`boundedAwait`): an abort signal alone did not\n * bound Next's patched fetch on Vercel, whose promise could never settle.\n */\nfunction fetchFaces(url: string, ms: number): Promise<GoogleFontFace[] | null> {\n return boundedAwait(\n (signal) =>\n fetch(url, {\n headers: { 'User-Agent': WOFF2_USER_AGENT },\n signal,\n next: { revalidate: STYLESHEET_TTL_MS / 1000 },\n } as RequestInit)\n .then(async (response) => {\n if (!response.ok) return null\n const parsed = parseGoogleFontFaces(await response.text())\n return parsed.length ? parsed : null\n })\n .catch(() => null),\n ms,\n null,\n 'self-hosted-fonts.stylesheet',\n )\n}\n\n/**\n * Google's faces for a stylesheet URL, from this process's cache when it has\n * them; null when the fetch failed, timed out or answered with nothing\n * usable. Asked through `fetch` with a day's revalidation, which on the\n * server is Next's data cache: a cold process reads the stored stylesheet\n * rather than asking Google again.\n */\nfunction facesFor(url: string): Promise<GoogleFontFace[] | null> {\n return stylesheets.readThrough(\n url,\n () => fetchFaces(url, STYLESHEET_TIMEOUT_MS),\n (faces) => (faces ? STYLESHEET_TTL_MS : FAILURE_TTL_MS),\n )\n}\n\n/** A Google file's size in bytes, from a HEAD; null when it could not be read. */\nfunction fileSize(path: string): Promise<number | null> {\n return fileSizes.readThrough(\n path,\n () =>\n boundedAwait(\n (signal) =>\n fetch(googleFontFileUrl(path), { method: 'HEAD', signal, cache: 'force-cache' })\n .then((response) => {\n const length = Number(response.headers.get('content-length'))\n return response.ok && length > 0 ? length : null\n })\n .catch(() => null),\n SIZE_TIMEOUT_MS,\n null,\n 'self-hosted-fonts.size',\n ),\n (size) => (size ? STYLESHEET_TTL_MS : FAILURE_TTL_MS),\n )\n}\n\n/** The Latin file of a weight (upright unless asked) among some faces, if there is one. */\nfunction latinFileOf(\n faces: readonly GoogleFontFace[],\n weight: number,\n style: 'normal' | 'italic' = 'normal',\n): string | undefined {\n return faces.find((face) => {\n if (face.style !== style || (face.subset && face.subset !== 'latin')) return false\n const [low, high = low] = face.weight.split(' ').map(Number)\n return weight >= low && weight <= high\n })?.path\n}\n\ntype Face = { weight: number; style: 'normal' | 'italic' }\n\n/**\n * Each face from its own stylesheet, so a weight Google refuses costs that\n * face rather than the family. One weight per request is also what makes\n * Google answer a variable family with a static instance of that weight.\n */\nasync function facesOneByOne(family: string, faces: readonly Face[]): Promise<GoogleFontFace[] | null> {\n const sheets = await Promise.all(\n faces.map((face) => {\n const url = googleFontSheetUrl(family, [face])\n return url ? facesFor(url) : Promise.resolve(null)\n }),\n )\n const found = sheets.flatMap((sheet) => sheet ?? [])\n return found.length ? found : null\n}\n\n/**\n * The faces of a family the page asks Google for: each wanted weight as the\n * nearest one the family offers, and an italic only of a family the catalog\n * says has one — a request for one it lacks is refused, and a slanted upright\n * is what the browser would draw anyway.\n */\nfunction facesToAsk(need: ThemeFontNeed, facts: ThemeFontFacts | undefined): Face[] {\n const weights = offeredWeights(need.weights, facts?.weights)\n const italics = facts?.italics?.length ? offeredWeights(need.italics, facts.italics) : []\n return [\n ...weights.map((weight) => ({ weight, style: 'normal' as const })),\n ...italics.map((weight) => ({ weight, style: 'italic' as const })),\n ]\n}\n\n/**\n * How one Google family is delivered: as a file per weight, or as the\n * family's variable file holding every weight, whichever is smaller for the\n * weights the page's text styles draw with (AGL-3656).\n *\n * A theme that sets body text and headlines in two weights of Open Sans pays\n * 27 KB for two static Latin files rather than 43 KB for the variable one;\n * aglyn.com, which draws Roboto Flex at seven weights, pays 34 KB for the\n * variable file rather than seven statics. The sizes are the files' own,\n * read once per process with a HEAD; a size that cannot be read leaves the\n * statics, which put the smallest file on the first screen.\n *\n * A family that is not variable, or one the catalog does not know, is asked\n * for a face at a time.\n */\nfunction deliveryFor(\n need: ThemeFontNeed,\n facts: ThemeFontFacts | undefined,\n): Promise<GoogleFontFace[] | null> {\n const weights = offeredWeights(need.weights, facts?.weights)\n const wanted = facesToAsk(need, facts)\n const key = `${need.family.toLowerCase()}|${wanted.map((face) => `${face.style[0]}${face.weight}`).join(',')}`\n return deliveries\n .readThrough(\n key,\n async (): Promise<Delivery> => {\n if (!facts?.variableWeights) {\n const faces = await facesOneByOne(need.family, wanted)\n return { faces, decided: !!faces }\n }\n const combinedUrl = googleFontSheetUrl(need.family, wanted)\n const [statics, combined] = await Promise.all([\n facesOneByOne(need.family, wanted),\n combinedUrl ? facesFor(combinedUrl) : Promise.resolve(null),\n ])\n if (!statics || !combined) return { faces: statics ?? combined, decided: false }\n const drawn = offeredWeights(need.textWeights.length ? need.textWeights : weights, facts.weights)\n const staticPaths = [...new Set(drawn.map((weight) => latinFileOf(statics, weight)))]\n const combinedPath = latinFileOf(combined, drawn[0] ?? 400)\n if (!combinedPath || staticPaths.some((path) => !path)) return { faces: statics, decided: false }\n const [combinedSize, ...staticSizes] = await Promise.all(\n [combinedPath, ...(staticPaths as string[])].map(fileSize),\n )\n if (!combinedSize || staticSizes.some((size) => !size)) return { faces: statics, decided: false }\n const staticTotal = (staticSizes as number[]).reduce((sum, size) => sum + size, 0)\n return { faces: combinedSize < staticTotal ? combined : statics, decided: true }\n },\n // A choice made without every sheet and size is the statics by default,\n // so it is kept only as long as a failure: the next render after that\n // asks again rather than serving the default for a day.\n (result) => (result.decided ? STYLESHEET_TTL_MS : FAILURE_TTL_MS),\n )\n .then((result) => result.faces)\n}\n\n/** The category a stack's generic keyword names, for a font with none recorded. */\nfunction categoryFromStack(stack: unknown): HostThemeFontCategory {\n if (typeof stack !== 'string') return 'sans-serif'\n if (/\\bmonospace\\b/i.test(stack)) return 'monospace'\n if (/(^|,)\\s*serif\\b/i.test(stack)) return 'serif'\n return 'sans-serif'\n}\n\n/** What a page inlines in its head for its theme's fonts. */\nexport interface SelfHostedThemeFonts {\n /**\n * The `@font-face` rules, every `src` on the site's own origin, and each\n * family's metric-matched fallback.\n */\n css: string\n /** The one or two files the first screen paints with, to preload. */\n preloads: string[]\n}\n\nexport interface SelfHostedThemeFontsOptions {\n /** The site rendering, which an uploaded font's media URL is qualified with. */\n hostId?: string | null\n /**\n * The typography the site's theme is layered onto\n * (`siteBaseOptions(...).typography`): a text style the theme leaves alone\n * draws with the base's weight and family.\n */\n baseTypography?: Record<string, unknown>\n /**\n * Fonts the base theme draws with that the site's theme does not list —\n * the platform brand's face on the operator's own hosts.\n */\n baseFonts?: readonly HostThemeFont[]\n}\n\n/**\n * An uploaded face's CSS `font-weight`: its weight, or for a variable file\n * the range it draws (`100 900`), so every weight in between is drawn from\n * the file rather than synthesized.\n */\nexport function customFaceWeight(face: { weight: number; weightMax?: number }): string {\n const weight = Math.min(1000, Math.max(1, Math.round(Number(face.weight) || 400)))\n const max = Math.round(Number(face.weightMax))\n return Number.isFinite(max) && max > weight ? `${weight} ${Math.min(1000, max)}` : String(weight)\n}\n\n/**\n * The theme's web fonts as rules to inline and files to preload, or null when\n * the theme loads none.\n *\n * Every face is on the site's own origin: a Google face through\n * {@link serveSelfHostedFont}, an uploaded one through the media route. When\n * Google cannot be read the family is left out rather than linked from\n * Google, and its metric-matched fallback still draws the text in the font's\n * box; the page asks again on its next render.\n */\nexport async function selfHostedThemeFonts(\n theme: Pick<HostTheme, 'fonts' | 'typography'> | undefined,\n options: SelfHostedThemeFontsOptions = {},\n): Promise<SelfHostedThemeFonts | null> {\n const fonts = [...(theme?.fonts ?? []), ...(options.baseFonts ?? [])]\n const needs = themeFontNeeds(theme, options.baseTypography, options.baseFonts)\n if (!needs.length) return null\n const stack =\n theme?.typography?.fontFamily ?? (options.baseTypography?.['fontFamily'] as string | undefined)\n\n const families = await Promise.all(\n needs.map(async (need) => {\n const font = fonts.find((entry) => entry.family.trim().toLowerCase() === need.family.toLowerCase())\n if (!font) return { faces: [] as SiteFontFace[], fallback: '' }\n if (font.source === 'custom') {\n const faces = (font.faces ?? []).flatMap((face): SiteFontFace[] => {\n const url = resolveMediaSrc(face.src, { hostId: options.hostId, version: face.version })\n if (!url || (face.style !== 'normal' && face.style !== 'italic')) return []\n return [\n {\n family: font.family.trim(),\n style: face.style,\n weight: customFaceWeight(face),\n url,\n ...(face.unicodeRange ? { unicodeRange: face.unicodeRange } : {}),\n },\n ]\n })\n return {\n faces,\n fallback: faces.length\n ? metricFallbackFontFaceCss(font.family, font.metrics, font.category ?? categoryFromStack(stack))\n : '',\n }\n }\n const facts = await themeFontFacts(need.family)\n const google = (await deliveryFor(need, facts)) ?? []\n const metrics: HostThemeFontMetrics | undefined = font.metrics ?? facts?.metrics\n return {\n faces: google.map(siteFontFaceFromGoogle),\n fallback: metricFallbackFontFaceCss(\n font.family,\n metrics,\n font.category ?? facts?.category ?? categoryFromStack(stack),\n ),\n }\n }),\n )\n\n const faces = mergeSharedFiles(families.flatMap((family) => family.faces))\n const css = siteFontFaceCss(faces) + families.map((family) => family.fallback).join('')\n if (!css) return null\n return {\n css,\n preloads: siteFontPreloads(\n { ...theme, fonts },\n faces,\n options.baseTypography,\n ),\n }\n}\n\n/** One file a theme's text is drawn from, as the font picker prices it. */\nexport interface ThemeFontCostFile {\n style: 'normal' | 'italic'\n /** A weight (`400`) or, for a variable file, its range (`100 900`). */\n weight: string\n /** Its size in bytes; null when it could not be read. */\n bytes: number | null\n}\n\n/** What one family costs a visitor. */\nexport interface ThemeFontFamilyCost {\n family: string\n source: 'google' | 'custom'\n /** The Latin files, one per file however many faces it holds. */\n files: ThemeFontCostFile[]\n /** The bytes of the files whose size is known. */\n bytes: number\n /** Every file was found and measured. */\n complete: boolean\n}\n\n/** What a theme's fonts cost a visitor, family by family. */\nexport interface ThemeFontDeliveryCost {\n families: ThemeFontFamilyCost[]\n bytes: number\n files: number\n complete: boolean\n}\n\n/** The most families one question prices: a theme sets text in two or three. */\nconst COST_MAX_FAMILIES = 6\n\n/**\n * What a theme's web fonts cost a visitor (AGL-3656): the Latin files a\n * published page draws its text styles and italics from, and their sizes,\n * chosen exactly as\n * {@link selfHostedThemeFonts} chooses them — the same faces, the same\n * nearest weights, the same pick between a file per weight and the variable\n * file. The font picker shows it beside each choice.\n *\n * Latin, because that is the file an English-language page downloads: the\n * other subsets are declared with their `unicode-range` and fetched only for\n * text that needs them. A file holding several faces (the variable file, a\n * shared range) is counted once.\n *\n * Bounded and cached like the page's own load, and never throws: anything it\n * could not read is left out and the answer says it is not `complete`. An\n * uploaded font's files are counted from the theme, and sized when the theme\n * records their size.\n */\nexport async function themeFontDeliveryCost(\n theme: Pick<HostTheme, 'fonts' | 'typography'> | undefined,\n options: SelfHostedThemeFontsOptions = {},\n): Promise<ThemeFontDeliveryCost> {\n const empty: ThemeFontDeliveryCost = { families: [], bytes: 0, files: 0, complete: true }\n try {\n const fonts = [...(theme?.fonts ?? []), ...(options.baseFonts ?? [])]\n const needs = themeFontNeeds(theme, options.baseTypography, options.baseFonts).slice(\n 0,\n COST_MAX_FAMILIES,\n )\n if (!needs.length) return empty\n const families = await Promise.all(\n needs.map(async (need): Promise<ThemeFontFamilyCost | null> => {\n const font = fonts.find(\n (entry) => entry.family.trim().toLowerCase() === need.family.toLowerCase(),\n )\n if (!font) return null\n if (font.source === 'custom') {\n const files = (font.faces ?? [])\n .filter((face) => face.style === 'normal' || face.style === 'italic')\n .filter((face) => coversLatinLetters(face.unicodeRange))\n .map((face): ThemeFontCostFile => {\n const bytes = Number((face as { bytes?: unknown }).bytes)\n return {\n style: face.style,\n weight: String(Math.round(Number(face.weight) || 400)),\n bytes: Number.isFinite(bytes) && bytes > 0 ? bytes : null,\n }\n })\n return familyCost(font.family.trim(), 'custom', files)\n }\n const facts = await themeFontFacts(need.family)\n const delivered = await deliveryFor(need, facts)\n if (!delivered) return { family: need.family, source: 'google', files: [], bytes: 0, complete: false }\n // The weights the text styles draw with, and the italics: a named\n // weight no style uses (MUI's light) is declared and never fetched.\n const wanted = facesToAsk(\n { ...need, weights: need.textWeights.length ? need.textWeights : need.weights },\n facts,\n )\n const paths = new Map<string, GoogleFontFace>()\n let missing = false\n for (const face of wanted) {\n const path = latinFileOf(delivered, face.weight, face.style)\n if (!path) {\n missing = true\n continue\n }\n const declared = delivered.find((entry) => entry.path === path)\n if (declared && !paths.has(path)) paths.set(path, declared)\n }\n const entries = [...paths.entries()]\n const sizes = await Promise.all(entries.map(([path]) => fileSize(path)))\n const cost = familyCost(\n need.family,\n 'google',\n entries.map(([, face], index) => ({ style: face.style, weight: face.weight, bytes: sizes[index] })),\n )\n return missing ? { ...cost, complete: false } : cost\n }),\n )\n const priced = families.filter((entry): entry is ThemeFontFamilyCost => !!entry)\n return {\n families: priced,\n bytes: priced.reduce((sum, entry) => sum + entry.bytes, 0),\n files: priced.reduce((sum, entry) => sum + entry.files.length, 0),\n complete: priced.every((entry) => entry.complete),\n }\n } catch {\n return { ...empty, complete: false }\n }\n}\n\n/** Whether a `unicode-range` holds the Latin letters; none at all holds everything. */\nfunction coversLatinLetters(range: string | undefined): boolean {\n if (!range) return true\n const letter = 0x41\n return range.split(',').some((part) => {\n const match = /^\\s*U\\+([0-9A-F?]{1,6})(?:-([0-9A-F]{1,6}))?\\s*$/i.exec(part)\n if (!match) return false\n const low = parseInt(match[1].replace(/\\?/g, '0'), 16)\n const high = match[2] ? parseInt(match[2], 16) : parseInt(match[1].replace(/\\?/g, 'F'), 16)\n return letter >= low && letter <= high\n })\n}\n\nfunction familyCost(\n family: string,\n source: 'google' | 'custom',\n files: ThemeFontCostFile[],\n): ThemeFontFamilyCost {\n return {\n family,\n source,\n files,\n bytes: files.reduce((sum, file) => sum + (file.bytes ?? 0), 0),\n complete: files.length > 0 && files.every((file) => file.bytes !== null),\n }\n}\n\n/**\n * One font file, fetched from Google and answered from the site's origin with\n * a year-long `immutable` policy. `path` is the part below `/s/` on Google's\n * origin (`inter/v18/….woff2`); anything else is a 404, so this route cannot\n * be pointed at any other URL.\n *\n * The edge keeps the answer for the year too (`s-maxage`), so Google is asked\n * once per file per edge region rather than once per visitor.\n */\nexport async function serveSelfHostedFont(path: string): Promise<Response> {\n const notFound = () =>\n new Response('Not found', {\n status: 404,\n headers: { 'Cache-Control': 'public, max-age=300' },\n })\n if (!isSelfHostedFontPath(path)) return notFound()\n let upstream: Response\n try {\n upstream = await fetch(googleFontFileUrl(path), {\n headers: { 'User-Agent': WOFF2_USER_AGENT },\n cache: 'force-cache',\n })\n } catch {\n return new Response('Font unavailable', {\n status: 502,\n headers: { 'Cache-Control': 'no-store' },\n })\n }\n if (upstream.status === 404) return notFound()\n const length = Number(upstream.headers.get('content-length') ?? 0)\n if (!upstream.ok || !upstream.body || length > MAX_FONT_BYTES) {\n return new Response('Font unavailable', {\n status: 502,\n headers: { 'Cache-Control': 'no-store' },\n })\n }\n return new Response(upstream.body, {\n status: 200,\n headers: {\n 'Content-Type': 'font/woff2',\n 'Cache-Control': SELF_HOSTED_FONT_CACHE_CONTROL,\n 'X-Content-Type-Options': 'nosniff',\n // Fonts are fetched in CORS mode; a site reached on two hostnames (its\n // own domain and its platform subdomain) shares one cached answer.\n 'Access-Control-Allow-Origin': '*',\n ...(length ? { 'Content-Length': String(length) } : {}),\n },\n })\n}\n"],"names":["boundedAwait","createSettledValueCache","resolveMediaSrc","themeFontFacts","googleFontFileUrl","googleFontSheetUrl","isSelfHostedFontPath","mergeSharedFiles","metricFallbackFontFaceCss","offeredWeights","parseGoogleFontFaces","siteFontFaceCss","siteFontFaceFromGoogle","siteFontPreloads","themeFontNeeds","WOFF2_USER_AGENT","STYLESHEET_TTL_MS","FAILURE_TTL_MS","STYLESHEET_TIMEOUT_MS","SIZE_TIMEOUT_MS","MAX_FONT_BYTES","SELF_HOSTED_FONT_CACHE_CONTROL","stylesheets","fileSizes","deliveries","resetSelfHostedFontsForTests","clear","fetchFaces","url","ms","signal","fetch","headers","next","revalidate","then","response","ok","parsed","text","length","catch","facesFor","readThrough","faces","fileSize","path","method","cache","Number","get","size","latinFileOf","weight","style","find","face","subset","low","high","split","map","facesOneByOne","family","sheets","Promise","all","resolve","found","flatMap","sheet","facesToAsk","need","facts","weights","italics","deliveryFor","wanted","key","toLowerCase","join","drawn","variableWeights","decided","combinedUrl","statics","combined","textWeights","staticPaths","Set","combinedPath","some","combinedSize","staticSizes","staticTotal","reduce","sum","result","categoryFromStack","stack","test","customFaceWeight","Math","min","max","round","weightMax","isFinite","String","selfHostedThemeFonts","theme","options","fonts","baseFonts","needs","baseTypography","typography","fontFamily","families","font","entry","trim","fallback","source","src","hostId","version","unicodeRange","metrics","category","google","css","preloads","COST_MAX_FAMILIES","themeFontDeliveryCost","empty","bytes","files","complete","slice","filter","coversLatinLetters","familyCost","delivered","paths","Map","missing","declared","has","set","entries","sizes","cost","index","priced","every","range","letter","part","match","exec","parseInt","replace","file","serveSelfHostedFont","upstream","notFound","Response","status","body"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAQD,SACEA,YAAY,EACZC,uBAAuB,QAClB,wCAAuC;AAC9C,SAASC,eAAe,QAAQ,mCAAkC;AAClE,SACEC,cAAc,QAET,wDAAuD;AAC9D,0EAA0E;AAC1E,4BAA4B;AAC5B,SAEEC,iBAAiB,EACjBC,kBAAkB,EAClBC,oBAAoB,EACpBC,gBAAgB,EAChBC,yBAAyB,EACzBC,cAAc,EACdC,oBAAoB,EAEpBC,eAAe,EACfC,sBAAsB,EACtBC,gBAAgB,EAEhBC,cAAc,QACT,gDAA+C;AAEtD;;;;;;CAMC,GAED;;;;CAIC,GACD,MAAMC,mBACJ,wEACA;AAEF,uEAAuE,GACvE,MAAMC,oBAAoB,KAAK,KAAK,KAAK;AAEzC,oEAAoE,GACpE,MAAMC,iBAAiB,IAAI,KAAK;AAEhC,kEAAkE,GAClE,MAAMC,wBAAwB;AAE9B,0EAA0E,GAC1E,MAAMC,kBAAkB;AAExB,kDAAkD,GAClD,MAAMC,iBAAiB,IAAI,OAAO;AAElC,2EAA2E,GAC3E,OAAO,MAAMC,iCACX,yDAAwD;AAE1D;;;;;;;CAOC,GACD,MAAMC,cAAcrB;AACpB,MAAMsB,YAAYtB;AAOlB,MAAMuB,aAAavB;AAEnB,sEAAsE,GACtE,OAAO,SAASwB;IACdH,YAAYI,KAAK;IACjBH,UAAUG,KAAK;IACfF,WAAWE,KAAK;AAClB;AAEA;;;;CAIC,GACD,SAASC,WAAWC,GAAW,EAAEC,EAAU;IACzC,OAAO7B,aACL,CAAC8B,SACCC,MAAMH,KAAK;YACTI,SAAS;gBAAE,cAAcjB;YAAiB;YAC1Ce;YACAG,MAAM;gBAAEC,YAAYlB,oBAAoB;YAAK;QAC/C,GACGmB,IAAI,CAAC,OAAOC;YACX,IAAI,CAACA,SAASC,EAAE,EAAE,OAAO;YACzB,MAAMC,SAAS5B,qBAAqB,MAAM0B,SAASG,IAAI;YACvD,OAAOD,OAAOE,MAAM,GAAGF,SAAS;QAClC,GACCG,KAAK,CAAC,IAAM,OACjBZ,IACA,MACA;AAEJ;AAEA;;;;;;CAMC,GACD,SAASa,SAASd,GAAW;IAC3B,OAAON,YAAYqB,WAAW,CAC5Bf,KACA,IAAMD,WAAWC,KAAKV,wBACtB,CAAC0B,QAAWA,QAAQ5B,oBAAoBC;AAE5C;AAEA,gFAAgF,GAChF,SAAS4B,SAASC,IAAY;IAC5B,OAAOvB,UAAUoB,WAAW,CAC1BG,MACA,IACE9C,aACE,CAAC8B,SACCC,MAAM3B,kBAAkB0C,OAAO;gBAAEC,QAAQ;gBAAQjB;gBAAQkB,OAAO;YAAc,GAC3Eb,IAAI,CAAC,CAACC;gBACL,MAAMI,SAASS,OAAOb,SAASJ,OAAO,CAACkB,GAAG,CAAC;gBAC3C,OAAOd,SAASC,EAAE,IAAIG,SAAS,IAAIA,SAAS;YAC9C,GACCC,KAAK,CAAC,IAAM,OACjBtB,iBACA,MACA,2BAEJ,CAACgC,OAAUA,OAAOnC,oBAAoBC;AAE1C;AAEA,yFAAyF,GACzF,SAASmC,YACPR,KAAgC,EAChCS,MAAc,EACdC,QAA6B,QAAQ;QAE9BV;IAAP,QAAOA,cAAAA,MAAMW,IAAI,CAAC,CAACC;QACjB,IAAIA,KAAKF,KAAK,KAAKA,SAAUE,KAAKC,MAAM,IAAID,KAAKC,MAAM,KAAK,SAAU,OAAO;QAC7E,MAAM,CAACC,KAAKC,OAAOD,GAAG,CAAC,GAAGF,KAAKH,MAAM,CAACO,KAAK,CAAC,KAAKC,GAAG,CAACZ;QACrD,OAAOI,UAAUK,OAAOL,UAAUM;IACpC,uBAJOf,YAIHE,IAAI;AACV;AAIA;;;;CAIC,GACD,eAAegB,cAAcC,MAAc,EAAEnB,KAAsB;IACjE,MAAMoB,SAAS,MAAMC,QAAQC,GAAG,CAC9BtB,MAAMiB,GAAG,CAAC,CAACL;QACT,MAAM5B,MAAMvB,mBAAmB0D,QAAQ;YAACP;SAAK;QAC7C,OAAO5B,MAAMc,SAASd,OAAOqC,QAAQE,OAAO,CAAC;IAC/C;IAEF,MAAMC,QAAQJ,OAAOK,OAAO,CAAC,CAACC,QAAUA,gBAAAA,QAAS,EAAE;IACnD,OAAOF,MAAM5B,MAAM,GAAG4B,QAAQ;AAChC;AAEA;;;;;CAKC,GACD,SAASG,WAAWC,IAAmB,EAAEC,KAAiC;QAExDA;IADhB,MAAMC,UAAUjE,eAAe+D,KAAKE,OAAO,EAAED,yBAAAA,MAAOC,OAAO;IAC3D,MAAMC,UAAUF,CAAAA,0BAAAA,iBAAAA,MAAOE,OAAO,qBAAdF,eAAgBjC,MAAM,IAAG/B,eAAe+D,KAAKG,OAAO,EAAEF,MAAME,OAAO,IAAI,EAAE;IACzF,OAAO;WACFD,QAAQb,GAAG,CAAC,CAACR,SAAY,CAAA;gBAAEA;gBAAQC,OAAO;YAAkB,CAAA;WAC5DqB,QAAQd,GAAG,CAAC,CAACR,SAAY,CAAA;gBAAEA;gBAAQC,OAAO;YAAkB,CAAA;KAChE;AACH;AAEA;;;;;;;;;;;;;;CAcC,GACD,SAASsB,YACPJ,IAAmB,EACnBC,KAAiC;IAEjC,MAAMC,UAAUjE,eAAe+D,KAAKE,OAAO,EAAED,yBAAAA,MAAOC,OAAO;IAC3D,MAAMG,SAASN,WAAWC,MAAMC;IAChC,MAAMK,MAAM,GAAGN,KAAKT,MAAM,CAACgB,WAAW,GAAG,CAAC,EAAEF,OAAOhB,GAAG,CAAC,CAACL,OAAS,GAAGA,KAAKF,KAAK,CAAC,EAAE,GAAGE,KAAKH,MAAM,EAAE,EAAE2B,IAAI,CAAC,MAAM;IAC9G,OAAOxD,WACJmB,WAAW,CACVmC,KACA;YAa6CG;QAZ3C,IAAI,EAACR,yBAAAA,MAAOS,eAAe,GAAE;YAC3B,MAAMtC,QAAQ,MAAMkB,cAAcU,KAAKT,MAAM,EAAEc;YAC/C,OAAO;gBAAEjC;gBAAOuC,SAAS,CAAC,CAACvC;YAAM;QACnC;QACA,MAAMwC,cAAc/E,mBAAmBmE,KAAKT,MAAM,EAAEc;QACpD,MAAM,CAACQ,SAASC,SAAS,GAAG,MAAMrB,QAAQC,GAAG,CAAC;YAC5CJ,cAAcU,KAAKT,MAAM,EAAEc;YAC3BO,cAAc1C,SAAS0C,eAAenB,QAAQE,OAAO,CAAC;SACvD;QACD,IAAI,CAACkB,WAAW,CAACC,UAAU,OAAO;YAAE1C,KAAK,EAAEyC,kBAAAA,UAAWC;YAAUH,SAAS;QAAM;QAC/E,MAAMF,QAAQxE,eAAe+D,KAAKe,WAAW,CAAC/C,MAAM,GAAGgC,KAAKe,WAAW,GAAGb,SAASD,MAAMC,OAAO;QAChG,MAAMc,cAAc;eAAI,IAAIC,IAAIR,MAAMpB,GAAG,CAAC,CAACR,SAAWD,YAAYiC,SAAShC;SAAU;QACrF,MAAMqC,eAAetC,YAAYkC,WAAUL,UAAAA,KAAK,CAAC,EAAE,YAARA,UAAY;QACvD,IAAI,CAACS,gBAAgBF,YAAYG,IAAI,CAAC,CAAC7C,OAAS,CAACA,OAAO,OAAO;YAAEF,OAAOyC;YAASF,SAAS;QAAM;QAChG,MAAM,CAACS,cAAc,GAAGC,YAAY,GAAG,MAAM5B,QAAQC,GAAG,CACtD;YAACwB;eAAkBF;SAAyB,CAAC3B,GAAG,CAAChB;QAEnD,IAAI,CAAC+C,gBAAgBC,YAAYF,IAAI,CAAC,CAACxC,OAAS,CAACA,OAAO,OAAO;YAAEP,OAAOyC;YAASF,SAAS;QAAM;QAChG,MAAMW,cAAc,AAACD,YAAyBE,MAAM,CAAC,CAACC,KAAK7C,OAAS6C,MAAM7C,MAAM;QAChF,OAAO;YAAEP,OAAOgD,eAAeE,cAAcR,WAAWD;YAASF,SAAS;QAAK;IACjF,GACA,wEAAwE;IACxE,sEAAsE;IACtE,wDAAwD;IACxD,CAACc,SAAYA,OAAOd,OAAO,GAAGnE,oBAAoBC,gBAEnDkB,IAAI,CAAC,CAAC8D,SAAWA,OAAOrD,KAAK;AAClC;AAEA,iFAAiF,GACjF,SAASsD,kBAAkBC,KAAc;IACvC,IAAI,OAAOA,UAAU,UAAU,OAAO;IACtC,IAAI,iBAAiBC,IAAI,CAACD,QAAQ,OAAO;IACzC,IAAI,mBAAmBC,IAAI,CAACD,QAAQ,OAAO;IAC3C,OAAO;AACT;AA6BA;;;;CAIC,GACD,OAAO,SAASE,iBAAiB7C,IAA4C;IAC3E,MAAMH,SAASiD,KAAKC,GAAG,CAAC,MAAMD,KAAKE,GAAG,CAAC,GAAGF,KAAKG,KAAK,CAACxD,OAAOO,KAAKH,MAAM,KAAK;IAC5E,MAAMmD,MAAMF,KAAKG,KAAK,CAACxD,OAAOO,KAAKkD,SAAS;IAC5C,OAAOzD,OAAO0D,QAAQ,CAACH,QAAQA,MAAMnD,SAAS,GAAGA,OAAO,CAAC,EAAEiD,KAAKC,GAAG,CAAC,MAAMC,MAAM,GAAGI,OAAOvD;AAC5F;AAEA;;;;;;;;;CASC,GACD,OAAO,eAAewD,qBACpBC,KAA0D,EAC1DC,UAAuC,CAAC,CAAC;cAEGA;QAI1CD,mBAAkCC;IAJpC,MAAMC,QAAQ;mBAAKF,yBAAAA,MAAOE,KAAK,mBAAI,EAAE;YAAOD,qBAAAA,QAAQE,SAAS,YAAjBF,qBAAqB,EAAE;KAAE;IACrE,MAAMG,QAAQpG,eAAegG,OAAOC,QAAQI,cAAc,EAAEJ,QAAQE,SAAS;IAC7E,IAAI,CAACC,MAAM1E,MAAM,EAAE,OAAO;IAC1B,MAAM2D,iBACJW,0BAAAA,oBAAAA,MAAOM,UAAU,qBAAjBN,kBAAmBO,UAAU,qBAAKN,0BAAAA,QAAQI,cAAc,qBAAtBJ,uBAAwB,CAAC,aAAa;IAE1E,MAAMO,WAAW,MAAMrD,QAAQC,GAAG,CAChCgD,MAAMrD,GAAG,CAAC,OAAOW;YAyBC,MACkC+C,eAM9CA,OAAAA;QA/BJ,MAAMA,OAAOP,MAAMzD,IAAI,CAAC,CAACiE,QAAUA,MAAMzD,MAAM,CAAC0D,IAAI,GAAG1C,WAAW,OAAOP,KAAKT,MAAM,CAACgB,WAAW;QAChG,IAAI,CAACwC,MAAM,OAAO;YAAE3E,OAAO,EAAE;YAAoB8E,UAAU;QAAG;QAC9D,IAAIH,KAAKI,MAAM,KAAK,UAAU;gBACbJ,aAgB4CA;YAhB3D,MAAM3E,QAAQ,EAAC2E,cAAAA,KAAK3E,KAAK,YAAV2E,cAAc,EAAE,EAAElD,OAAO,CAAC,CAACb;gBACxC,MAAM5B,MAAM1B,gBAAgBsD,KAAKoE,GAAG,EAAE;oBAAEC,QAAQd,QAAQc,MAAM;oBAAEC,SAAStE,KAAKsE,OAAO;gBAAC;gBACtF,IAAI,CAAClG,OAAQ4B,KAAKF,KAAK,KAAK,YAAYE,KAAKF,KAAK,KAAK,UAAW,OAAO,EAAE;gBAC3E,OAAO;oBACL;wBACES,QAAQwD,KAAKxD,MAAM,CAAC0D,IAAI;wBACxBnE,OAAOE,KAAKF,KAAK;wBACjBD,QAAQgD,iBAAiB7C;wBACzB5B;uBACI4B,KAAKuE,YAAY,GAAG;wBAAEA,cAAcvE,KAAKuE,YAAY;oBAAC,IAAI,CAAC;iBAElE;YACH;YACA,OAAO;gBACLnF;gBACA8E,UAAU9E,MAAMJ,MAAM,GAClBhC,0BAA0B+G,KAAKxD,MAAM,EAAEwD,KAAKS,OAAO,GAAET,kBAAAA,KAAKU,QAAQ,YAAbV,kBAAiBrB,kBAAkBC,UACxF;YACN;QACF;QACA,MAAM1B,QAAQ,MAAMtE,eAAeqE,KAAKT,MAAM;QAC9C,MAAMmE,UAAU,OAAA,MAAMtD,YAAYJ,MAAMC,kBAAxB,OAAmC,EAAE;QACrD,MAAMuD,WAA4CT,gBAAAA,KAAKS,OAAO,YAAZT,gBAAgB9C,yBAAAA,MAAOuD,OAAO;QAChF,OAAO;YACLpF,OAAOsF,OAAOrE,GAAG,CAACjD;YAClB8G,UAAUlH,0BACR+G,KAAKxD,MAAM,EACXiE,UACAT,SAAAA,iBAAAA,KAAKU,QAAQ,YAAbV,iBAAiB9C,yBAAAA,MAAOwD,QAAQ,YAAhCV,QAAoCrB,kBAAkBC;QAE1D;IACF;IAGF,MAAMvD,QAAQrC,iBAAiB+G,SAASjD,OAAO,CAAC,CAACN,SAAWA,OAAOnB,KAAK;IACxE,MAAMuF,MAAMxH,gBAAgBiC,SAAS0E,SAASzD,GAAG,CAAC,CAACE,SAAWA,OAAO2D,QAAQ,EAAE1C,IAAI,CAAC;IACpF,IAAI,CAACmD,KAAK,OAAO;IACjB,OAAO;QACLA;QACAC,UAAUvH,iBACR,aAAKiG;YAAOE;YACZpE,OACAmE,QAAQI,cAAc;IAE1B;AACF;AA+BA,8EAA8E,GAC9E,MAAMkB,oBAAoB;AAE1B;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,eAAeC,sBACpBxB,KAA0D,EAC1DC,UAAuC,CAAC,CAAC;IAEzC,MAAMwB,QAA+B;QAAEjB,UAAU,EAAE;QAAEkB,OAAO;QAAGC,OAAO;QAAGC,UAAU;IAAK;IACxF,IAAI;kBAC0C3B;QAA5C,MAAMC,QAAQ;uBAAKF,yBAAAA,MAAOE,KAAK,mBAAI,EAAE;gBAAOD,qBAAAA,QAAQE,SAAS,YAAjBF,qBAAqB,EAAE;SAAE;QACrE,MAAMG,QAAQpG,eAAegG,OAAOC,QAAQI,cAAc,EAAEJ,QAAQE,SAAS,EAAE0B,KAAK,CAClF,GACAN;QAEF,IAAI,CAACnB,MAAM1E,MAAM,EAAE,OAAO+F;QAC1B,MAAMjB,WAAW,MAAMrD,QAAQC,GAAG,CAChCgD,MAAMrD,GAAG,CAAC,OAAOW;YACf,MAAM+C,OAAOP,MAAMzD,IAAI,CACrB,CAACiE,QAAUA,MAAMzD,MAAM,CAAC0D,IAAI,GAAG1C,WAAW,OAAOP,KAAKT,MAAM,CAACgB,WAAW;YAE1E,IAAI,CAACwC,MAAM,OAAO;YAClB,IAAIA,KAAKI,MAAM,KAAK,UAAU;oBACbJ;gBAAf,MAAMkB,QAAQ,EAAClB,cAAAA,KAAK3E,KAAK,YAAV2E,cAAc,EAAE,EAC5BqB,MAAM,CAAC,CAACpF,OAASA,KAAKF,KAAK,KAAK,YAAYE,KAAKF,KAAK,KAAK,UAC3DsF,MAAM,CAAC,CAACpF,OAASqF,mBAAmBrF,KAAKuE,YAAY,GACrDlE,GAAG,CAAC,CAACL;oBACJ,MAAMgF,QAAQvF,OAAO,AAACO,KAA6BgF,KAAK;oBACxD,OAAO;wBACLlF,OAAOE,KAAKF,KAAK;wBACjBD,QAAQuD,OAAON,KAAKG,KAAK,CAACxD,OAAOO,KAAKH,MAAM,KAAK;wBACjDmF,OAAOvF,OAAO0D,QAAQ,CAAC6B,UAAUA,QAAQ,IAAIA,QAAQ;oBACvD;gBACF;gBACF,OAAOM,WAAWvB,KAAKxD,MAAM,CAAC0D,IAAI,IAAI,UAAUgB;YAClD;YACA,MAAMhE,QAAQ,MAAMtE,eAAeqE,KAAKT,MAAM;YAC9C,MAAMgF,YAAY,MAAMnE,YAAYJ,MAAMC;YAC1C,IAAI,CAACsE,WAAW,OAAO;gBAAEhF,QAAQS,KAAKT,MAAM;gBAAE4D,QAAQ;gBAAUc,OAAO,EAAE;gBAAED,OAAO;gBAAGE,UAAU;YAAM;YACrG,kEAAkE;YAClE,oEAAoE;YACpE,MAAM7D,SAASN,WACb,aAAKC;gBAAME,SAASF,KAAKe,WAAW,CAAC/C,MAAM,GAAGgC,KAAKe,WAAW,GAAGf,KAAKE,OAAO;gBAC7ED;YAEF,MAAMuE,QAAQ,IAAIC;YAClB,IAAIC,UAAU;YACd,KAAK,MAAM1F,QAAQqB,OAAQ;gBACzB,MAAM/B,OAAOM,YAAY2F,WAAWvF,KAAKH,MAAM,EAAEG,KAAKF,KAAK;gBAC3D,IAAI,CAACR,MAAM;oBACToG,UAAU;oBACV;gBACF;gBACA,MAAMC,WAAWJ,UAAUxF,IAAI,CAAC,CAACiE,QAAUA,MAAM1E,IAAI,KAAKA;gBAC1D,IAAIqG,YAAY,CAACH,MAAMI,GAAG,CAACtG,OAAOkG,MAAMK,GAAG,CAACvG,MAAMqG;YACpD;YACA,MAAMG,UAAU;mBAAIN,MAAMM,OAAO;aAAG;YACpC,MAAMC,QAAQ,MAAMtF,QAAQC,GAAG,CAACoF,QAAQzF,GAAG,CAAC,CAAC,CAACf,KAAK,GAAKD,SAASC;YACjE,MAAM0G,OAAOV,WACXtE,KAAKT,MAAM,EACX,UACAuF,QAAQzF,GAAG,CAAC,CAAC,GAAGL,KAAK,EAAEiG,QAAW,CAAA;oBAAEnG,OAAOE,KAAKF,KAAK;oBAAED,QAAQG,KAAKH,MAAM;oBAAEmF,OAAOe,KAAK,CAACE,MAAM;gBAAC,CAAA;YAElG,OAAOP,UAAU,aAAKM;gBAAMd,UAAU;iBAAUc;QAClD;QAEF,MAAME,SAASpC,SAASsB,MAAM,CAAC,CAACpB,QAAwC,CAAC,CAACA;QAC1E,OAAO;YACLF,UAAUoC;YACVlB,OAAOkB,OAAO3D,MAAM,CAAC,CAACC,KAAKwB,QAAUxB,MAAMwB,MAAMgB,KAAK,EAAE;YACxDC,OAAOiB,OAAO3D,MAAM,CAAC,CAACC,KAAKwB,QAAUxB,MAAMwB,MAAMiB,KAAK,CAACjG,MAAM,EAAE;YAC/DkG,UAAUgB,OAAOC,KAAK,CAAC,CAACnC,QAAUA,MAAMkB,QAAQ;QAClD;IACF,EAAE,eAAM;QACN,OAAO,aAAKH;YAAOG,UAAU;;IAC/B;AACF;AAEA,qFAAqF,GACrF,SAASG,mBAAmBe,KAAyB;IACnD,IAAI,CAACA,OAAO,OAAO;IACnB,MAAMC,SAAS;IACf,OAAOD,MAAMhG,KAAK,CAAC,KAAK+B,IAAI,CAAC,CAACmE;QAC5B,MAAMC,QAAQ,oDAAoDC,IAAI,CAACF;QACvE,IAAI,CAACC,OAAO,OAAO;QACnB,MAAMrG,MAAMuG,SAASF,KAAK,CAAC,EAAE,CAACG,OAAO,CAAC,OAAO,MAAM;QACnD,MAAMvG,OAAOoG,KAAK,CAAC,EAAE,GAAGE,SAASF,KAAK,CAAC,EAAE,EAAE,MAAME,SAASF,KAAK,CAAC,EAAE,CAACG,OAAO,CAAC,OAAO,MAAM;QACxF,OAAOL,UAAUnG,OAAOmG,UAAUlG;IACpC;AACF;AAEA,SAASmF,WACP/E,MAAc,EACd4D,MAA2B,EAC3Bc,KAA0B;IAE1B,OAAO;QACL1E;QACA4D;QACAc;QACAD,OAAOC,MAAM1C,MAAM,CAAC,CAACC,KAAKmE;gBAAgBA;mBAAPnE,QAAOmE,cAAAA,KAAK3B,KAAK,YAAV2B,cAAc;WAAI;QAC5DzB,UAAUD,MAAMjG,MAAM,GAAG,KAAKiG,MAAMkB,KAAK,CAAC,CAACQ,OAASA,KAAK3B,KAAK,KAAK;IACrE;AACF;AAEA;;;;;;;;CAQC,GACD,OAAO,eAAe4B,oBAAoBtH,IAAY;QAoB9BuH;IAnBtB,MAAMC,WAAW,IACf,IAAIC,SAAS,aAAa;YACxBC,QAAQ;YACRxI,SAAS;gBAAE,iBAAiB;YAAsB;QACpD;IACF,IAAI,CAAC1B,qBAAqBwC,OAAO,OAAOwH;IACxC,IAAID;IACJ,IAAI;QACFA,WAAW,MAAMtI,MAAM3B,kBAAkB0C,OAAO;YAC9Cd,SAAS;gBAAE,cAAcjB;YAAiB;YAC1CiC,OAAO;QACT;IACF,EAAE,eAAM;QACN,OAAO,IAAIuH,SAAS,oBAAoB;YACtCC,QAAQ;YACRxI,SAAS;gBAAE,iBAAiB;YAAW;QACzC;IACF;IACA,IAAIqI,SAASG,MAAM,KAAK,KAAK,OAAOF;IACpC,MAAM9H,SAASS,QAAOoH,wBAAAA,SAASrI,OAAO,CAACkB,GAAG,CAAC,6BAArBmH,wBAA0C;IAChE,IAAI,CAACA,SAAShI,EAAE,IAAI,CAACgI,SAASI,IAAI,IAAIjI,SAASpB,gBAAgB;QAC7D,OAAO,IAAImJ,SAAS,oBAAoB;YACtCC,QAAQ;YACRxI,SAAS;gBAAE,iBAAiB;YAAW;QACzC;IACF;IACA,OAAO,IAAIuI,SAASF,SAASI,IAAI,EAAE;QACjCD,QAAQ;QACRxI,SAAS;YACP,gBAAgB;YAChB,iBAAiBX;YACjB,0BAA0B;YAC1B,uEAAuE;YACvE,mEAAmE;YACnE,+BAA+B;WAC3BmB,SAAS;YAAE,kBAAkBoE,OAAOpE;QAAQ,IAAI,CAAC;IAEzD;AACF"}