@nordcraft/ssr 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +5 -0
  2. package/dist/ToddleApiService.d.ts +23 -0
  3. package/dist/ToddleApiService.js +54 -0
  4. package/dist/ToddleApiService.js.map +1 -0
  5. package/dist/ToddleRoute.d.ts +20 -0
  6. package/dist/ToddleRoute.js +53 -0
  7. package/dist/ToddleRoute.js.map +1 -0
  8. package/dist/components/utils.d.ts +8 -0
  9. package/dist/components/utils.js +43 -0
  10. package/dist/components/utils.js.map +1 -0
  11. package/dist/const.d.ts +1 -0
  12. package/dist/const.js +18 -0
  13. package/dist/const.js.map +1 -0
  14. package/dist/custom-code/codeRefs.d.ts +30 -0
  15. package/dist/custom-code/codeRefs.js +176 -0
  16. package/dist/custom-code/codeRefs.js.map +1 -0
  17. package/dist/rendering/api.d.ts +13 -0
  18. package/dist/rendering/api.js +2 -0
  19. package/dist/rendering/api.js.map +1 -0
  20. package/dist/rendering/attributes.d.ts +15 -0
  21. package/dist/rendering/attributes.js +76 -0
  22. package/dist/rendering/attributes.js.map +1 -0
  23. package/dist/rendering/components.d.ts +21 -0
  24. package/dist/rendering/components.js +382 -0
  25. package/dist/rendering/components.js.map +1 -0
  26. package/dist/rendering/cookies.d.ts +3 -0
  27. package/dist/rendering/cookies.js +6 -0
  28. package/dist/rendering/cookies.js.map +1 -0
  29. package/dist/rendering/equals.d.ts +1 -0
  30. package/dist/rendering/equals.js +8 -0
  31. package/dist/rendering/equals.js.map +1 -0
  32. package/dist/rendering/fonts.d.ts +6 -0
  33. package/dist/rendering/fonts.js +67 -0
  34. package/dist/rendering/fonts.js.map +1 -0
  35. package/dist/rendering/formulaContext.d.ts +38 -0
  36. package/dist/rendering/formulaContext.js +120 -0
  37. package/dist/rendering/formulaContext.js.map +1 -0
  38. package/dist/rendering/head.d.ts +28 -0
  39. package/dist/rendering/head.js +252 -0
  40. package/dist/rendering/head.js.map +1 -0
  41. package/dist/rendering/html.d.ts +12 -0
  42. package/dist/rendering/html.js +14 -0
  43. package/dist/rendering/html.js.map +1 -0
  44. package/dist/rendering/request.d.ts +2 -0
  45. package/dist/rendering/request.js +11 -0
  46. package/dist/rendering/request.js.map +1 -0
  47. package/dist/rendering/speculation.d.ts +9 -0
  48. package/dist/rendering/speculation.js +22 -0
  49. package/dist/rendering/speculation.js.map +1 -0
  50. package/dist/rendering/template.d.ts +10 -0
  51. package/dist/rendering/template.js +36 -0
  52. package/dist/rendering/template.js.map +1 -0
  53. package/dist/rendering/testData.d.ts +2 -0
  54. package/dist/rendering/testData.js +58 -0
  55. package/dist/rendering/testData.js.map +1 -0
  56. package/dist/routing/routing.d.ts +26 -0
  57. package/dist/routing/routing.js +90 -0
  58. package/dist/routing/routing.js.map +1 -0
  59. package/dist/ssr.types.d.ts +101 -0
  60. package/dist/ssr.types.js +2 -0
  61. package/dist/ssr.types.js.map +1 -0
  62. package/dist/utils/headers.d.ts +12 -0
  63. package/dist/utils/headers.js +22 -0
  64. package/dist/utils/headers.js.map +1 -0
  65. package/dist/utils/media.d.ts +22 -0
  66. package/dist/utils/media.js +34 -0
  67. package/dist/utils/media.js.map +1 -0
  68. package/dist/utils/nanoid.d.ts +1 -0
  69. package/dist/utils/nanoid.js +19 -0
  70. package/dist/utils/nanoid.js.map +1 -0
  71. package/dist/utils/tags.d.ts +22 -0
  72. package/dist/utils/tags.js +23 -0
  73. package/dist/utils/tags.js.map +1 -0
  74. package/package.json +22 -0
  75. package/src/ToddleApiService.ts +67 -0
  76. package/src/ToddleRoute.ts +70 -0
  77. package/src/components/utils.test.ts +90 -0
  78. package/src/components/utils.ts +77 -0
  79. package/src/const.ts +17 -0
  80. package/src/custom-code/codeRefs.ts +271 -0
  81. package/src/rendering/api.ts +21 -0
  82. package/src/rendering/attributes.ts +117 -0
  83. package/src/rendering/components.ts +579 -0
  84. package/src/rendering/cookies.ts +10 -0
  85. package/src/rendering/equals.ts +9 -0
  86. package/src/rendering/fonts.ts +83 -0
  87. package/src/rendering/formulaContext.test.ts +57 -0
  88. package/src/rendering/formulaContext.ts +188 -0
  89. package/src/rendering/head.ts +391 -0
  90. package/src/rendering/html.ts +33 -0
  91. package/src/rendering/request.ts +19 -0
  92. package/src/rendering/speculation.ts +21 -0
  93. package/src/rendering/template.test.ts +18 -0
  94. package/src/rendering/template.ts +63 -0
  95. package/src/rendering/testData.test.ts +186 -0
  96. package/src/rendering/testData.ts +69 -0
  97. package/src/routing/routing.test.ts +97 -0
  98. package/src/routing/routing.ts +152 -0
  99. package/src/ssr.types.ts +117 -0
  100. package/src/utils/headers.ts +23 -0
  101. package/src/utils/media.test.ts +130 -0
  102. package/src/utils/media.ts +46 -0
  103. package/src/utils/nanoid.ts +21 -0
  104. package/src/utils/tags.ts +26 -0
@@ -0,0 +1,579 @@
1
+ import type {
2
+ ApiStatus,
3
+ LegacyApiStatus,
4
+ } from '@nordcraft/core/dist/api/apiTypes'
5
+ import type {
6
+ Component,
7
+ ComponentData,
8
+ NodeModel,
9
+ SupportedNamespaces,
10
+ } from '@nordcraft/core/dist/component/component.types'
11
+ import { ToddleComponent } from '@nordcraft/core/dist/component/ToddleComponent'
12
+ import type {
13
+ FormulaContext,
14
+ ToddleServerEnv,
15
+ } from '@nordcraft/core/dist/formula/formula'
16
+ import { applyFormula } from '@nordcraft/core/dist/formula/formula'
17
+ import {
18
+ getClassName,
19
+ toValidClassName,
20
+ } from '@nordcraft/core/dist/styling/className'
21
+ import { mapValues } from '@nordcraft/core/dist/utils/collections'
22
+ import { isDefined, toBoolean } from '@nordcraft/core/dist/utils/util'
23
+ import { escapeAttrValue } from 'xss'
24
+ import { VOID_HTML_ELEMENTS } from '../const'
25
+ import type { ProjectFiles } from '../ssr.types'
26
+ import type { ApiCache, ApiEvaluator } from './api'
27
+ import { getNodeAttrs, toEncodedText } from './attributes'
28
+
29
+ const renderComponent = async ({
30
+ apiCache,
31
+ children,
32
+ component,
33
+ data,
34
+ env,
35
+ evaluateComponentApis,
36
+ files,
37
+ toddle,
38
+ includedComponents,
39
+ instance,
40
+ packageName,
41
+ projectId,
42
+ req,
43
+ updateApiCache,
44
+ namespace,
45
+ }: {
46
+ apiCache: ApiCache
47
+ children?: Record<string, string>
48
+ component: Component
49
+ data: ComponentData
50
+ env: ToddleServerEnv
51
+ evaluateComponentApis: ApiEvaluator
52
+ files: ProjectFiles
53
+ toddle: FormulaContext['toddle']
54
+ includedComponents: Component[]
55
+ instance: Record<string, string>
56
+ packageName: string | undefined
57
+ projectId: string
58
+ req: Request
59
+ namespace?: SupportedNamespaces
60
+ updateApiCache: (key: string, value: ApiStatus) => void
61
+ }): Promise<string> => {
62
+ const renderNode = async ({
63
+ id,
64
+ node,
65
+ data,
66
+ packageName,
67
+ isComponentRootNode = false,
68
+ namespace,
69
+ }: {
70
+ id: string
71
+ node: NodeModel | undefined
72
+ data: ComponentData
73
+ packageName: string | undefined
74
+ isComponentRootNode?: boolean
75
+ namespace?: SupportedNamespaces
76
+ }): Promise<string> => {
77
+ if (!node) {
78
+ return ''
79
+ }
80
+
81
+ const formulaContext: FormulaContext = {
82
+ data,
83
+ component,
84
+ package: packageName,
85
+ env,
86
+ toddle,
87
+ }
88
+ if (node.repeat) {
89
+ const items = applyFormula(node.repeat, formulaContext)
90
+ if (!Array.isArray(items)) {
91
+ return ''
92
+ }
93
+
94
+ const nodeItems = await Promise.all(
95
+ items.map((Item, Index) =>
96
+ renderNode({
97
+ id,
98
+ node: { ...node, repeat: undefined },
99
+ data: {
100
+ ...data,
101
+ ListItem: data.ListItem
102
+ ? { Index, Item, Parent: data.ListItem }
103
+ : { Index, Item },
104
+ },
105
+ namespace,
106
+ packageName,
107
+ }),
108
+ ),
109
+ )
110
+ return nodeItems.join('')
111
+ }
112
+ if (
113
+ node.condition &&
114
+ !toBoolean(applyFormula(node.condition, formulaContext))
115
+ ) {
116
+ return ''
117
+ }
118
+
119
+ switch (node.type) {
120
+ case 'text': {
121
+ if (!namespace || namespace === 'http://www.w3.org/1999/xhtml') {
122
+ return `<span data-node-type="text" data-node-id="${id}">${toEncodedText(
123
+ String(applyFormula(node.value, formulaContext)),
124
+ )}</span>`
125
+ }
126
+
127
+ return toEncodedText(String(applyFormula(node.value, formulaContext)))
128
+ }
129
+ case 'slot': {
130
+ const defaultChild = children?.[node.name ?? 'default']
131
+ if (defaultChild) {
132
+ return defaultChild
133
+ } else {
134
+ const slotChildren = await Promise.all(
135
+ node.children.map((child) =>
136
+ renderNode({
137
+ id: child,
138
+ node: component.nodes[child],
139
+ data,
140
+ packageName,
141
+ namespace,
142
+ }),
143
+ ),
144
+ )
145
+ return slotChildren.join('')
146
+ }
147
+ }
148
+ case 'element': {
149
+ switch (node.tag.toLocaleLowerCase()) {
150
+ case 'script': {
151
+ // we do not want to run scripts twice.
152
+ return ''
153
+ }
154
+ case 'svg': {
155
+ namespace = 'http://www.w3.org/2000/svg'
156
+ break
157
+ }
158
+ case 'math': {
159
+ namespace = 'http://www.w3.org/1998/Math/MathML'
160
+ break
161
+ }
162
+ }
163
+
164
+ const nodeAttrs = getNodeAttrs({
165
+ node,
166
+ data,
167
+ component,
168
+ packageName,
169
+ env,
170
+ toddle,
171
+ })
172
+ const classHash = getClassName([node.style, node.variants])
173
+ let classList = Object.entries(node.classes)
174
+ .filter(([_, { formula }]) =>
175
+ toBoolean(applyFormula(formula, formulaContext)),
176
+ )
177
+ .map(([className]) => className)
178
+ .join(' ')
179
+ if (instance && id === 'root') {
180
+ Object.entries(instance).forEach(([key, value]) => {
181
+ classList += ' ' + toValidClassName(`${key}:${value}`)
182
+ })
183
+ }
184
+ let innerHTML = ''
185
+
186
+ if (
187
+ ['script', 'style'].includes(node.tag.toLocaleLowerCase()) === false
188
+ ) {
189
+ const childNodes = await Promise.all(
190
+ node.children.map((child) =>
191
+ renderNode({
192
+ id: child,
193
+ namespace,
194
+ node: component.nodes[child],
195
+ data,
196
+ packageName,
197
+ }),
198
+ ),
199
+ )
200
+ innerHTML = childNodes.join('')
201
+ }
202
+ if (node.tag.toLocaleLowerCase() === 'style') {
203
+ // render style content as text
204
+ const textNode = node.children[0]
205
+ ? component.nodes[node.children[0]]
206
+ : undefined
207
+ if (textNode?.type === 'text') {
208
+ innerHTML = String(applyFormula(textNode.value, formulaContext))
209
+ }
210
+ }
211
+ const tag =
212
+ component.version === 2 && isComponentRootNode
213
+ ? `${packageName ?? projectId}-${node.tag}`
214
+ : node.tag
215
+ const nodeClasses = `${classHash} ${classList}`.trim()
216
+ if (!VOID_HTML_ELEMENTS.includes(tag)) {
217
+ return `<${tag} ${nodeAttrs} data-node-id="${escapeAttrValue(
218
+ id,
219
+ )}" class="${escapeAttrValue(nodeClasses)}">${innerHTML}</${tag}>`
220
+ } else {
221
+ return `<${tag} ${nodeAttrs} data-node-id="${escapeAttrValue(
222
+ id,
223
+ )}" class="${escapeAttrValue(nodeClasses)}" />`
224
+ }
225
+ }
226
+ case 'component': {
227
+ const attrs = mapValues(node.attrs, (formula) =>
228
+ applyFormula(formula, formulaContext),
229
+ )
230
+
231
+ const contexts = {
232
+ ...data.Contexts,
233
+ [component.name]: Object.fromEntries(
234
+ Object.entries(component.formulas ?? {})
235
+ .filter(([, formula]) => formula.exposeInContext)
236
+ .map(([key, formula]) => [
237
+ key,
238
+ applyFormula(formula.formula, formulaContext),
239
+ ]),
240
+ ),
241
+ }
242
+
243
+ let _childComponent: Component | undefined
244
+ // `node.package` is stored statically on nodes when inserted from the catalog
245
+ const _packageName = node.package ?? packageName
246
+ if (_packageName) {
247
+ _childComponent =
248
+ files.packages?.[_packageName]?.components[node.name] ??
249
+ files.components[node.name]
250
+ } else {
251
+ _childComponent = files.components[node.name]
252
+ }
253
+ if (!isDefined(_childComponent)) {
254
+ // eslint-disable-next-line no-console
255
+ console.warn(
256
+ `Unable to find component ${[packageName, node.name]
257
+ .filter(isDefined)
258
+ .join('/')} in files`,
259
+ )
260
+ return ''
261
+ }
262
+ // help Typescript know that childComponent is defined
263
+ const childComponent = _childComponent
264
+
265
+ const isLocalComponent = includedComponents.some(
266
+ (c) => c.name === childComponent.name,
267
+ )
268
+
269
+ // Evaluate the child component apis before rendering to make sure we have api data for potential contexts
270
+ const apis = await evaluateComponentApis({
271
+ component: new ToddleComponent({
272
+ component: childComponent,
273
+ getComponent: (name, packageName) => {
274
+ const nodeLookupKey = [packageName, name]
275
+ .filter(isDefined)
276
+ .join('/')
277
+ const component = packageName
278
+ ? files.packages?.[packageName]?.components[name]
279
+ : files.components[name]
280
+ if (!component) {
281
+ // eslint-disable-next-line no-console
282
+ console.warn(
283
+ `Unable to find component ${nodeLookupKey} in files`,
284
+ )
285
+ return undefined
286
+ }
287
+
288
+ return component
289
+ },
290
+ packageName,
291
+ globalFormulas: {
292
+ formulas: files.formulas,
293
+ packages: files.packages,
294
+ },
295
+ }),
296
+ formulaContext: {
297
+ data: {
298
+ Location: formulaContext.data.Location,
299
+ Attributes: attrs,
300
+ Contexts: contexts,
301
+ Variables: mapValues(
302
+ childComponent.variables,
303
+ ({ initialValue }) => {
304
+ return applyFormula(initialValue, formulaContext)
305
+ },
306
+ ),
307
+ Apis: {},
308
+ },
309
+ component: childComponent,
310
+ package:
311
+ node.package ?? (isLocalComponent ? undefined : packageName),
312
+ env,
313
+ toddle,
314
+ },
315
+ req,
316
+ apiCache,
317
+ updateApiCache,
318
+ })
319
+
320
+ const childNodes = await Promise.all(
321
+ node.children.map((child) =>
322
+ renderNode({
323
+ id: child,
324
+ namespace,
325
+ node: component.nodes[child],
326
+ data: {
327
+ ...data,
328
+ Contexts: {
329
+ ...contexts,
330
+ [childComponent.name]: Object.fromEntries(
331
+ Object.entries(childComponent.formulas ?? {})
332
+ .filter(([, formula]) => formula.exposeInContext)
333
+ .map(([key, formula]) => [
334
+ key,
335
+ applyFormula(formula.formula, {
336
+ component: childComponent,
337
+ package: _packageName,
338
+ data: {
339
+ Contexts: {
340
+ ...data.Contexts,
341
+ ...Object.fromEntries(
342
+ Object.entries(childComponent.formulas ?? {})
343
+ .filter(
344
+ ([, formula]) => formula.exposeInContext,
345
+ )
346
+ .map(([key, formula]) => [
347
+ key,
348
+ applyFormula(formula.formula, {
349
+ data: {
350
+ Attributes: attrs,
351
+ Apis: { ...data.Apis, ...apis },
352
+ },
353
+ component,
354
+ package: _packageName,
355
+ env,
356
+ toddle,
357
+ }),
358
+ ]),
359
+ ),
360
+ },
361
+ Apis: apis,
362
+ Attributes: attrs,
363
+ Variables: mapValues(
364
+ childComponent.variables,
365
+ ({ initialValue }) => {
366
+ return applyFormula(initialValue, {
367
+ data: {
368
+ Attributes: attrs,
369
+ },
370
+ component,
371
+ package: _packageName,
372
+ env,
373
+ toddle,
374
+ })
375
+ },
376
+ ),
377
+ },
378
+ env,
379
+ toddle,
380
+ }),
381
+ ]),
382
+ ),
383
+ },
384
+ },
385
+ // pass package name to child component if it's defined
386
+ packageName: node.package ?? packageName,
387
+ }),
388
+ ),
389
+ )
390
+
391
+ const children: Record<string, string> = {}
392
+ childNodes.forEach((childNode, i) => {
393
+ // Add children to the correct slot in the right order
394
+ const slotName =
395
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
396
+ component.nodes[node.children[i]]?.slot ?? 'default'
397
+ children[slotName] = `${children[slotName] ?? ''} ${childNode}`
398
+ })
399
+
400
+ return createComponent({
401
+ attrs,
402
+ component: childComponent,
403
+ contexts,
404
+ children,
405
+ packageName:
406
+ node.package ?? (isLocalComponent ? undefined : packageName),
407
+ // If the root node is another component, then append and forward previous instance
408
+ instance:
409
+ id === 'root'
410
+ ? { ...instance, [component.name]: 'root' }
411
+ : { [component.name]: id },
412
+ apis,
413
+ env,
414
+ includedComponents,
415
+ formulaContext,
416
+ files,
417
+ apiCache,
418
+ updateApiCache,
419
+ projectId,
420
+ namespace,
421
+ evaluateComponentApis,
422
+ req,
423
+ })
424
+ }
425
+ }
426
+ }
427
+ return renderNode({
428
+ id: 'root',
429
+ node: component.nodes.root,
430
+ data,
431
+ packageName,
432
+ isComponentRootNode: true,
433
+ namespace,
434
+ })
435
+ }
436
+
437
+ const createComponent = async ({
438
+ apiCache,
439
+ apis,
440
+ attrs,
441
+ children,
442
+ component,
443
+ contexts,
444
+ env,
445
+ evaluateComponentApis,
446
+ files,
447
+ formulaContext,
448
+ includedComponents,
449
+ instance,
450
+ packageName,
451
+ projectId,
452
+ req,
453
+ updateApiCache,
454
+ namespace,
455
+ }: {
456
+ apiCache: ApiCache
457
+ apis: Record<
458
+ string,
459
+ | LegacyApiStatus
460
+ | (ApiStatus & {
461
+ inputs?: Record<string, unknown>
462
+ })
463
+ >
464
+ attrs: Record<string, any>
465
+ children?: Record<string, string>
466
+ component: Component
467
+ contexts?: Record<string, Record<string, any>>
468
+ env: ToddleServerEnv
469
+ evaluateComponentApis: ApiEvaluator
470
+ files: ProjectFiles
471
+ formulaContext: FormulaContext
472
+ includedComponents: Component[]
473
+ instance: Record<string, string>
474
+ packageName: string | undefined
475
+ projectId: string
476
+ req: Request
477
+ updateApiCache: (key: string, value: ApiStatus) => void
478
+ namespace?: SupportedNamespaces
479
+ }): Promise<string> => {
480
+ const data: ComponentData = {
481
+ Location: formulaContext.data.Location,
482
+ Attributes: attrs,
483
+ Contexts: contexts,
484
+ Variables: mapValues(component.variables, ({ initialValue }) => {
485
+ return applyFormula(initialValue, {
486
+ ...formulaContext,
487
+ data: {
488
+ ...formulaContext.data,
489
+ Contexts: contexts,
490
+ },
491
+ })
492
+ }),
493
+ Apis: apis,
494
+ }
495
+ data.Contexts = {
496
+ ...data.Contexts,
497
+ ...Object.fromEntries(
498
+ Object.entries(component.formulas ?? {})
499
+ .filter(([, formula]) => formula.exposeInContext)
500
+ .map(([key, formula]) => [
501
+ key,
502
+ applyFormula(formula.formula, {
503
+ ...formulaContext,
504
+ data,
505
+ }),
506
+ ]),
507
+ ),
508
+ }
509
+
510
+ return renderComponent({
511
+ apiCache,
512
+ children,
513
+ component,
514
+ data,
515
+ env,
516
+ evaluateComponentApis,
517
+ files,
518
+ includedComponents,
519
+ instance,
520
+ packageName,
521
+ projectId,
522
+ namespace,
523
+ req,
524
+ toddle: formulaContext.toddle,
525
+ updateApiCache,
526
+ })
527
+ }
528
+
529
+ /**
530
+ * Renders a page body for a given ToddleComponent
531
+ */
532
+ export const renderPageBody = async ({
533
+ component,
534
+ env,
535
+ evaluateComponentApis,
536
+ files,
537
+ formulaContext,
538
+ includedComponents,
539
+ req,
540
+ projectId,
541
+ }: {
542
+ component: ToddleComponent<string>
543
+ env: ToddleServerEnv
544
+ evaluateComponentApis: ApiEvaluator
545
+ files: ProjectFiles
546
+ formulaContext: FormulaContext
547
+ includedComponents: Component[]
548
+ req: Request
549
+ projectId: string
550
+ }) => {
551
+ const apiCache: ApiCache = {}
552
+ const updateApiCache = (key: string, value: ApiStatus) =>
553
+ (apiCache[key] = value)
554
+ const apis = await evaluateComponentApis({
555
+ component,
556
+ formulaContext,
557
+ req,
558
+ apiCache,
559
+ updateApiCache,
560
+ })
561
+ formulaContext.data.Apis = apis
562
+
563
+ const html = await renderComponent({
564
+ apiCache,
565
+ component,
566
+ data: formulaContext.data,
567
+ env,
568
+ evaluateComponentApis,
569
+ files,
570
+ includedComponents,
571
+ instance: {},
572
+ packageName: undefined,
573
+ projectId,
574
+ req,
575
+ toddle: formulaContext.toddle,
576
+ updateApiCache,
577
+ })
578
+ return { html, apiCache }
579
+ }
@@ -0,0 +1,10 @@
1
+ import { isDefined } from '@nordcraft/core/dist/utils/util'
2
+ import { parse } from 'cookie'
3
+
4
+ export const getRequestCookies = (req: Request) =>
5
+ Object.fromEntries(
6
+ Object.entries(parse(req.headers.get('cookie') ?? '')).filter(
7
+ // Ensure that both key and value are defined
8
+ (kv): kv is [string, string] => isDefined(kv[0]) && isDefined(kv[1]),
9
+ ),
10
+ )
@@ -0,0 +1,9 @@
1
+ import type { Toddle } from '@nordcraft/core/dist/types'
2
+ import fastDeepEqual from 'fast-deep-equal'
3
+
4
+ export const initIsEqual = () => {
5
+ const toddle: Pick<Toddle<never, never>, 'isEqual'> = {
6
+ isEqual: fastDeepEqual,
7
+ }
8
+ ;(globalThis as any).toddle = toddle
9
+ }
@@ -0,0 +1,83 @@
1
+ import type { FontFamily } from '@nordcraft/core/dist/styling/theme'
2
+ import { easySort } from '@nordcraft/core/dist/utils/collections'
3
+
4
+ export const getFontCssUrl = ({
5
+ fonts,
6
+ baseForAbsoluteUrls,
7
+ basePath = '/.toddle/fonts/stylesheet/css2',
8
+ }: {
9
+ fonts: FontFamily[]
10
+ baseForAbsoluteUrls?: string
11
+ basePath?: string
12
+ }): Record<'swap', string> | undefined => {
13
+ if (fonts.length === 0) {
14
+ return
15
+ }
16
+ const searchParams = new URLSearchParams()
17
+ searchParams.set('display', 'swap')
18
+ for (const font of fonts) {
19
+ const sortedWeights = easySort(
20
+ font.variants.filter((v) => !Number.isNaN(Number(v.weight))),
21
+ (v) => Number(v.weight),
22
+ )
23
+ if (sortedWeights.length === 0) {
24
+ continue
25
+ }
26
+ const italicRange = sortedWeights.filter((v) => v.italic)
27
+ const standardRange = sortedWeights.filter((v) => !v.italic)
28
+ // Utility function for returning a single weight or a range of weights
29
+ // e.g. 400..700
30
+ // TODO: enable when we start supporting variable fonts
31
+ // const encodeVariableRange = (range: { weight: string }[]) => {
32
+ // if (range.length === 1) {
33
+ // return String(range[0].weight)
34
+ // }
35
+ // return `${range[0].weight}..${range[range.length - 1].weight}`
36
+ // }
37
+ const encodeStaticRange = (range: { weight: string }[], index?: number) =>
38
+ range
39
+ .map(
40
+ (v) => `${typeof index === 'number' ? `${index},` : ''}${v.weight}`,
41
+ )
42
+ .join(';')
43
+ // If the font has italic variants, we need to use multiple axes
44
+ // See other axis definitions here https://fonts.google.com/variablefonts#axis-definitions
45
+ const hasItalicVariants = italicRange.length > 0
46
+ const wght = [standardRange, italicRange]
47
+ .map((range, index) =>
48
+ encodeStaticRange(range, hasItalicVariants ? index : undefined),
49
+ )
50
+ // TODO: When we have information about whether a font is variable, use the below code for variable fonts
51
+ // range.length > 0
52
+ // ? `${hasItalicVariants ? `${index},` : ''}${encodeVariableRange(
53
+ // range,
54
+ // )}`
55
+ // : undefined,
56
+ // )
57
+ .filter(Boolean)
58
+ .join(';')
59
+ let familyValue = font.family
60
+ if (hasItalicVariants) {
61
+ // See https://fonts.google.com/knowledge/glossary/italic_axis
62
+ familyValue += `:ital,wght@${wght}`
63
+ } else {
64
+ familyValue += `:wght@${wght}`
65
+ }
66
+ searchParams.append('family', familyValue)
67
+ }
68
+ const path = `${basePath}?${searchParams.toString()}`
69
+ try {
70
+ const url =
71
+ typeof baseForAbsoluteUrls === 'string'
72
+ ? new URL(path, baseForAbsoluteUrls).toString()
73
+ : path
74
+ return {
75
+ // Eventually, we expect to support multiple types of font-display properties
76
+ // and we might need to return a url for each type of font-display (e.g. swap, block, fallback)
77
+ swap: url,
78
+ }
79
+ } catch (e) {
80
+ // eslint-disable-next-line no-console
81
+ console.error(e)
82
+ }
83
+ }