@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.
- package/README.md +5 -0
- package/dist/ToddleApiService.d.ts +23 -0
- package/dist/ToddleApiService.js +54 -0
- package/dist/ToddleApiService.js.map +1 -0
- package/dist/ToddleRoute.d.ts +20 -0
- package/dist/ToddleRoute.js +53 -0
- package/dist/ToddleRoute.js.map +1 -0
- package/dist/components/utils.d.ts +8 -0
- package/dist/components/utils.js +43 -0
- package/dist/components/utils.js.map +1 -0
- package/dist/const.d.ts +1 -0
- package/dist/const.js +18 -0
- package/dist/const.js.map +1 -0
- package/dist/custom-code/codeRefs.d.ts +30 -0
- package/dist/custom-code/codeRefs.js +176 -0
- package/dist/custom-code/codeRefs.js.map +1 -0
- package/dist/rendering/api.d.ts +13 -0
- package/dist/rendering/api.js +2 -0
- package/dist/rendering/api.js.map +1 -0
- package/dist/rendering/attributes.d.ts +15 -0
- package/dist/rendering/attributes.js +76 -0
- package/dist/rendering/attributes.js.map +1 -0
- package/dist/rendering/components.d.ts +21 -0
- package/dist/rendering/components.js +382 -0
- package/dist/rendering/components.js.map +1 -0
- package/dist/rendering/cookies.d.ts +3 -0
- package/dist/rendering/cookies.js +6 -0
- package/dist/rendering/cookies.js.map +1 -0
- package/dist/rendering/equals.d.ts +1 -0
- package/dist/rendering/equals.js +8 -0
- package/dist/rendering/equals.js.map +1 -0
- package/dist/rendering/fonts.d.ts +6 -0
- package/dist/rendering/fonts.js +67 -0
- package/dist/rendering/fonts.js.map +1 -0
- package/dist/rendering/formulaContext.d.ts +38 -0
- package/dist/rendering/formulaContext.js +120 -0
- package/dist/rendering/formulaContext.js.map +1 -0
- package/dist/rendering/head.d.ts +28 -0
- package/dist/rendering/head.js +252 -0
- package/dist/rendering/head.js.map +1 -0
- package/dist/rendering/html.d.ts +12 -0
- package/dist/rendering/html.js +14 -0
- package/dist/rendering/html.js.map +1 -0
- package/dist/rendering/request.d.ts +2 -0
- package/dist/rendering/request.js +11 -0
- package/dist/rendering/request.js.map +1 -0
- package/dist/rendering/speculation.d.ts +9 -0
- package/dist/rendering/speculation.js +22 -0
- package/dist/rendering/speculation.js.map +1 -0
- package/dist/rendering/template.d.ts +10 -0
- package/dist/rendering/template.js +36 -0
- package/dist/rendering/template.js.map +1 -0
- package/dist/rendering/testData.d.ts +2 -0
- package/dist/rendering/testData.js +58 -0
- package/dist/rendering/testData.js.map +1 -0
- package/dist/routing/routing.d.ts +26 -0
- package/dist/routing/routing.js +90 -0
- package/dist/routing/routing.js.map +1 -0
- package/dist/ssr.types.d.ts +101 -0
- package/dist/ssr.types.js +2 -0
- package/dist/ssr.types.js.map +1 -0
- package/dist/utils/headers.d.ts +12 -0
- package/dist/utils/headers.js +22 -0
- package/dist/utils/headers.js.map +1 -0
- package/dist/utils/media.d.ts +22 -0
- package/dist/utils/media.js +34 -0
- package/dist/utils/media.js.map +1 -0
- package/dist/utils/nanoid.d.ts +1 -0
- package/dist/utils/nanoid.js +19 -0
- package/dist/utils/nanoid.js.map +1 -0
- package/dist/utils/tags.d.ts +22 -0
- package/dist/utils/tags.js +23 -0
- package/dist/utils/tags.js.map +1 -0
- package/package.json +22 -0
- package/src/ToddleApiService.ts +67 -0
- package/src/ToddleRoute.ts +70 -0
- package/src/components/utils.test.ts +90 -0
- package/src/components/utils.ts +77 -0
- package/src/const.ts +17 -0
- package/src/custom-code/codeRefs.ts +271 -0
- package/src/rendering/api.ts +21 -0
- package/src/rendering/attributes.ts +117 -0
- package/src/rendering/components.ts +579 -0
- package/src/rendering/cookies.ts +10 -0
- package/src/rendering/equals.ts +9 -0
- package/src/rendering/fonts.ts +83 -0
- package/src/rendering/formulaContext.test.ts +57 -0
- package/src/rendering/formulaContext.ts +188 -0
- package/src/rendering/head.ts +391 -0
- package/src/rendering/html.ts +33 -0
- package/src/rendering/request.ts +19 -0
- package/src/rendering/speculation.ts +21 -0
- package/src/rendering/template.test.ts +18 -0
- package/src/rendering/template.ts +63 -0
- package/src/rendering/testData.test.ts +186 -0
- package/src/rendering/testData.ts +69 -0
- package/src/routing/routing.test.ts +97 -0
- package/src/routing/routing.ts +152 -0
- package/src/ssr.types.ts +117 -0
- package/src/utils/headers.ts +23 -0
- package/src/utils/media.test.ts +130 -0
- package/src/utils/media.ts +46 -0
- package/src/utils/nanoid.ts +21 -0
- 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
|
+
}
|