meno-core 1.0.52 → 1.0.53
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/build-astro.ts +183 -13
- package/build-next.ts +1361 -0
- package/build-static.ts +7 -5
- package/dist/bin/cli.js +2 -2
- package/dist/build-static.js +6 -6
- package/dist/chunks/{chunk-HNLUO36W.js → chunk-GZHGVVW3.js} +2 -2
- package/dist/chunks/chunk-GZHGVVW3.js.map +7 -0
- package/dist/chunks/{chunk-LPVETICS.js → chunk-H3GJ4H2U.js} +185 -1
- package/dist/chunks/chunk-H3GJ4H2U.js.map +7 -0
- package/dist/chunks/{chunk-CXCBV2M7.js → chunk-IGYR22T6.js} +76 -270
- package/dist/chunks/chunk-IGYR22T6.js.map +7 -0
- package/dist/chunks/{chunk-LHLHPYSP.js → chunk-JGP5A3Y5.js} +12 -11
- package/dist/chunks/chunk-JGP5A3Y5.js.map +7 -0
- package/dist/chunks/{chunk-7NIC4I3V.js → chunk-JGWFTO6P.js} +167 -21
- package/dist/chunks/chunk-JGWFTO6P.js.map +7 -0
- package/dist/chunks/{chunk-EDQSMAMP.js → chunk-O3NAGJP4.js} +85 -4
- package/dist/chunks/chunk-O3NAGJP4.js.map +7 -0
- package/dist/chunks/{chunk-H4JSCDNW.js → chunk-QB2LNO4W.js} +24 -1
- package/dist/chunks/chunk-QB2LNO4W.js.map +7 -0
- package/dist/chunks/{chunk-A725KYFK.js → chunk-R6XHAFBF.js} +561 -112
- package/dist/chunks/chunk-R6XHAFBF.js.map +7 -0
- package/dist/chunks/{chunk-J23ZX5AP.js → chunk-X754AHS5.js} +277 -1
- package/dist/chunks/chunk-X754AHS5.js.map +7 -0
- package/dist/chunks/{chunk-2QK6U5UK.js → chunk-YBLHKYFF.js} +12 -2
- package/dist/chunks/chunk-YBLHKYFF.js.map +7 -0
- package/dist/chunks/{constants-GWBAD66U.js → constants-STK2YBIW.js} +2 -2
- package/dist/entries/server-router.js +7 -7
- package/dist/lib/client/index.js +354 -59
- package/dist/lib/client/index.js.map +4 -4
- package/dist/lib/server/index.js +1458 -190
- package/dist/lib/server/index.js.map +4 -4
- package/dist/lib/shared/index.js +202 -34
- package/dist/lib/shared/index.js.map +4 -4
- package/dist/lib/test-utils/index.js +1 -1
- package/entries/client-router.tsx +5 -165
- package/lib/client/ErrorBoundary.test.tsx +27 -25
- package/lib/client/ErrorBoundary.tsx +34 -19
- package/lib/client/core/ComponentBuilder.ts +19 -2
- package/lib/client/core/builders/embedBuilder.ts +8 -4
- package/lib/client/core/builders/listBuilder.ts +23 -4
- package/lib/client/fontFamiliesService.test.ts +76 -0
- package/lib/client/fontFamiliesService.ts +69 -0
- package/lib/client/hmrCssReload.ts +160 -0
- package/lib/client/hooks/useColorVariables.ts +2 -0
- package/lib/client/index.ts +4 -0
- package/lib/client/meno-filter/ui.ts +2 -0
- package/lib/client/routing/RouteLoader.test.ts +2 -2
- package/lib/client/routing/RouteLoader.ts +8 -2
- package/lib/client/routing/Router.tsx +81 -15
- package/lib/client/scripts/ScriptExecutor.test.ts +143 -0
- package/lib/client/scripts/ScriptExecutor.ts +56 -2
- package/lib/client/styles/StyleInjector.ts +20 -5
- package/lib/client/styles/UtilityClassCollector.ts +7 -1
- package/lib/client/styles/cspNonce.test.ts +67 -0
- package/lib/client/styles/cspNonce.ts +63 -0
- package/lib/client/templateEngine.test.ts +80 -0
- package/lib/client/templateEngine.ts +5 -0
- package/lib/server/astro/cmsPageEmitter.ts +35 -5
- package/lib/server/astro/componentEmitter.ts +61 -5
- package/lib/server/astro/nodeToAstro.ts +149 -11
- package/lib/server/astro/normalizeOrphanTemplateProps.test.ts +264 -0
- package/lib/server/astro/normalizeOrphanTemplateProps.ts +184 -0
- package/lib/server/createServer.ts +11 -0
- package/lib/server/draftPageStore.ts +49 -0
- package/lib/server/fileWatcher.ts +62 -2
- package/lib/server/index.ts +13 -1
- package/lib/server/providers/fileSystemPageProvider.ts +8 -0
- package/lib/server/routes/api/components.ts +9 -4
- package/lib/server/routes/api/core-routes.ts +2 -2
- package/lib/server/routes/api/pages.ts +14 -22
- package/lib/server/routes/api/shared.ts +56 -0
- package/lib/server/routes/index.ts +90 -0
- package/lib/server/routes/pages.ts +13 -6
- package/lib/server/services/componentService.test.ts +199 -2
- package/lib/server/services/componentService.ts +354 -49
- package/lib/server/services/fileWatcherService.ts +4 -24
- package/lib/server/services/pageService.test.ts +23 -0
- package/lib/server/services/pageService.ts +124 -6
- package/lib/server/ssr/attributeBuilder.ts +8 -2
- package/lib/server/ssr/buildErrorOverlay.ts +1 -1
- package/lib/server/ssr/errorOverlay.test.ts +21 -2
- package/lib/server/ssr/errorOverlay.ts +38 -11
- package/lib/server/ssr/htmlGenerator.test.ts +53 -13
- package/lib/server/ssr/htmlGenerator.ts +71 -27
- package/lib/server/ssr/liveReloadIntegration.test.ts +123 -2
- package/lib/server/ssr/ssrRenderer.test.ts +67 -0
- package/lib/server/ssr/ssrRenderer.ts +94 -9
- package/lib/server/websocketManager.ts +0 -1
- package/lib/shared/componentRefs.ts +45 -0
- package/lib/shared/constants.ts +8 -0
- package/lib/shared/cssGeneration.ts +2 -0
- package/lib/shared/cssProperties.ts +184 -0
- package/lib/shared/expressionEvaluator.ts +54 -0
- package/lib/shared/fontCss.ts +101 -0
- package/lib/shared/fontLoader.ts +8 -86
- package/lib/shared/friendlyError.test.ts +87 -0
- package/lib/shared/friendlyError.ts +121 -0
- package/lib/shared/hrefRefs.test.ts +130 -0
- package/lib/shared/hrefRefs.ts +100 -0
- package/lib/shared/index.ts +52 -0
- package/lib/shared/inlineSvgStyleRules.test.ts +108 -0
- package/lib/shared/inlineSvgStyleRules.ts +134 -0
- package/lib/shared/interfaces/contentProvider.ts +13 -0
- package/lib/shared/itemTemplateUtils.test.ts +14 -0
- package/lib/shared/itemTemplateUtils.ts +4 -1
- package/lib/shared/registry/NodeTypeDefinition.ts +1 -1
- package/lib/shared/registry/nodeTypes/LinkNodeType.ts +1 -1
- package/lib/shared/slugTranslator.test.ts +24 -0
- package/lib/shared/slugTranslator.ts +24 -0
- package/lib/shared/styleNodeUtils.ts +4 -1
- package/lib/shared/tree/PathBuilder.test.ts +128 -1
- package/lib/shared/tree/PathBuilder.ts +83 -31
- package/lib/shared/types/comment.ts +99 -0
- package/lib/shared/types/index.ts +12 -0
- package/lib/shared/types/rendering.ts +8 -0
- package/lib/shared/utilityClassConfig.ts +4 -2
- package/lib/shared/utilityClassMapper.test.ts +24 -0
- package/lib/shared/validation/commentValidators.ts +69 -0
- package/lib/shared/validation/index.ts +1 -0
- package/lib/shared/viewportUnits.integration.test.ts +42 -0
- package/lib/shared/viewportUnits.test.ts +103 -0
- package/lib/shared/viewportUnits.ts +63 -0
- package/lib/test-utils/dom-setup.ts +6 -0
- package/package.json +1 -1
- package/dist/chunks/chunk-2QK6U5UK.js.map +0 -7
- package/dist/chunks/chunk-7NIC4I3V.js.map +0 -7
- package/dist/chunks/chunk-A725KYFK.js.map +0 -7
- package/dist/chunks/chunk-CXCBV2M7.js.map +0 -7
- package/dist/chunks/chunk-EDQSMAMP.js.map +0 -7
- package/dist/chunks/chunk-H4JSCDNW.js.map +0 -7
- package/dist/chunks/chunk-HNLUO36W.js.map +0 -7
- package/dist/chunks/chunk-J23ZX5AP.js.map +0 -7
- package/dist/chunks/chunk-LHLHPYSP.js.map +0 -7
- package/dist/chunks/chunk-LPVETICS.js.map +0 -7
- /package/dist/chunks/{constants-GWBAD66U.js.map → constants-STK2YBIW.js.map} +0 -0
|
@@ -85,6 +85,35 @@ export const CSS_PROPERTIES_DEFINITION: Record<string, CSSPropertyDefinition> =
|
|
|
85
85
|
values: ['repeat', 'repeat-x', 'repeat-y', 'no-repeat'],
|
|
86
86
|
type: 'select',
|
|
87
87
|
},
|
|
88
|
+
backgroundAttachment: {
|
|
89
|
+
values: ['scroll', 'fixed', 'local'],
|
|
90
|
+
type: 'select',
|
|
91
|
+
},
|
|
92
|
+
backgroundOrigin: {
|
|
93
|
+
values: ['border-box', 'padding-box', 'content-box'],
|
|
94
|
+
type: 'select',
|
|
95
|
+
},
|
|
96
|
+
backgroundBlendMode: {
|
|
97
|
+
values: ['normal', 'multiply', 'screen', 'overlay', 'darken', 'lighten', 'color-dodge', 'color-burn', 'hard-light', 'soft-light', 'difference', 'exclusion', 'hue', 'saturation', 'color', 'luminosity'],
|
|
98
|
+
type: 'select',
|
|
99
|
+
},
|
|
100
|
+
// Gradient-text recipe (`background: linear-gradient(...); background-clip:
|
|
101
|
+
// text; -webkit-background-clip: text; -webkit-text-fill-color: transparent`).
|
|
102
|
+
// The `Webkit*` capitalization is intentional — utilityClassMapper's
|
|
103
|
+
// camel→kebab emitter only prepends `-` when the first letter is uppercase,
|
|
104
|
+
// so `WebkitBackgroundClip` round-trips back to `-webkit-background-clip`.
|
|
105
|
+
backgroundClip: {
|
|
106
|
+
values: ['border-box', 'padding-box', 'content-box', 'text'],
|
|
107
|
+
type: 'select',
|
|
108
|
+
},
|
|
109
|
+
WebkitBackgroundClip: {
|
|
110
|
+
values: ['border-box', 'padding-box', 'content-box', 'text'],
|
|
111
|
+
type: 'select',
|
|
112
|
+
},
|
|
113
|
+
WebkitTextFillColor: { type: 'string' },
|
|
114
|
+
WebkitTextStroke: { type: 'string' },
|
|
115
|
+
WebkitTextStrokeWidth: { type: 'string' },
|
|
116
|
+
WebkitTextStrokeColor: { type: 'string' },
|
|
88
117
|
color: { type: 'string' },
|
|
89
118
|
opacity: { type: 'number' },
|
|
90
119
|
|
|
@@ -167,10 +196,63 @@ export const CSS_PROPERTIES_DEFINITION: Record<string, CSSPropertyDefinition> =
|
|
|
167
196
|
values: ['none', 'underline', 'overline', 'line-through'],
|
|
168
197
|
type: 'select',
|
|
169
198
|
},
|
|
199
|
+
textDecorationLine: {
|
|
200
|
+
values: ['none', 'underline', 'overline', 'line-through'],
|
|
201
|
+
type: 'select',
|
|
202
|
+
},
|
|
203
|
+
textDecorationStyle: {
|
|
204
|
+
values: ['solid', 'double', 'dotted', 'dashed', 'wavy'],
|
|
205
|
+
type: 'select',
|
|
206
|
+
},
|
|
207
|
+
textDecorationColor: { type: 'string' },
|
|
208
|
+
textDecorationThickness: { type: 'string' },
|
|
209
|
+
textUnderlineOffset: { type: 'string' },
|
|
210
|
+
textUnderlinePosition: { type: 'string' },
|
|
170
211
|
textTransform: {
|
|
171
212
|
values: ['none', 'capitalize', 'uppercase', 'lowercase'],
|
|
172
213
|
type: 'select',
|
|
173
214
|
},
|
|
215
|
+
textRendering: {
|
|
216
|
+
values: ['auto', 'optimizeSpeed', 'optimizeLegibility', 'geometricPrecision'],
|
|
217
|
+
type: 'select',
|
|
218
|
+
},
|
|
219
|
+
fontVariant: { type: 'string' },
|
|
220
|
+
fontStretch: { type: 'string' },
|
|
221
|
+
fontFeatureSettings: { type: 'string' },
|
|
222
|
+
fontVariationSettings: { type: 'string' },
|
|
223
|
+
fontKerning: {
|
|
224
|
+
values: ['auto', 'normal', 'none'],
|
|
225
|
+
type: 'select',
|
|
226
|
+
},
|
|
227
|
+
WebkitFontSmoothing: {
|
|
228
|
+
values: ['auto', 'none', 'antialiased', 'subpixel-antialiased'],
|
|
229
|
+
type: 'select',
|
|
230
|
+
},
|
|
231
|
+
MozOsxFontSmoothing: {
|
|
232
|
+
values: ['auto', 'grayscale'],
|
|
233
|
+
type: 'select',
|
|
234
|
+
},
|
|
235
|
+
hyphens: {
|
|
236
|
+
values: ['none', 'manual', 'auto'],
|
|
237
|
+
type: 'select',
|
|
238
|
+
},
|
|
239
|
+
WebkitHyphens: {
|
|
240
|
+
values: ['none', 'manual', 'auto'],
|
|
241
|
+
type: 'select',
|
|
242
|
+
},
|
|
243
|
+
writingMode: {
|
|
244
|
+
values: ['horizontal-tb', 'vertical-rl', 'vertical-lr'],
|
|
245
|
+
type: 'select',
|
|
246
|
+
},
|
|
247
|
+
direction: {
|
|
248
|
+
values: ['ltr', 'rtl'],
|
|
249
|
+
type: 'select',
|
|
250
|
+
},
|
|
251
|
+
tabSize: { type: 'string' },
|
|
252
|
+
wordWrap: {
|
|
253
|
+
values: ['normal', 'break-word'],
|
|
254
|
+
type: 'select',
|
|
255
|
+
},
|
|
174
256
|
letterSpacing: { type: 'string' },
|
|
175
257
|
wordSpacing: { type: 'string' },
|
|
176
258
|
wordBreak: {
|
|
@@ -181,6 +263,10 @@ export const CSS_PROPERTIES_DEFINITION: Record<string, CSSPropertyDefinition> =
|
|
|
181
263
|
values: ['normal', 'break-word', 'anywhere'],
|
|
182
264
|
type: 'select',
|
|
183
265
|
},
|
|
266
|
+
textWrap: {
|
|
267
|
+
values: ['wrap', 'nowrap', 'balance', 'pretty', 'stable'],
|
|
268
|
+
type: 'select',
|
|
269
|
+
},
|
|
184
270
|
textIndent: { type: 'string' },
|
|
185
271
|
verticalAlign: {
|
|
186
272
|
values: ['baseline', 'top', 'middle', 'bottom', 'text-top', 'text-bottom', 'sub', 'super'],
|
|
@@ -192,9 +278,20 @@ export const CSS_PROPERTIES_DEFINITION: Record<string, CSSPropertyDefinition> =
|
|
|
192
278
|
textShadow: { type: 'string' },
|
|
193
279
|
filter: { type: 'string' },
|
|
194
280
|
backdropFilter: { type: 'string' },
|
|
281
|
+
WebkitBackdropFilter: { type: 'string' },
|
|
195
282
|
transform: { type: 'string' },
|
|
196
283
|
transformOrigin: { type: 'string' },
|
|
284
|
+
transformStyle: {
|
|
285
|
+
values: ['flat', 'preserve-3d'],
|
|
286
|
+
type: 'select',
|
|
287
|
+
},
|
|
288
|
+
perspective: { type: 'string' },
|
|
289
|
+
perspectiveOrigin: { type: 'string' },
|
|
197
290
|
transition: { type: 'string' },
|
|
291
|
+
transitionProperty: { type: 'string' },
|
|
292
|
+
transitionDuration: { type: 'string' },
|
|
293
|
+
transitionTimingFunction: { type: 'string' },
|
|
294
|
+
transitionDelay: { type: 'string' },
|
|
198
295
|
animation: { type: 'string' },
|
|
199
296
|
backfaceVisibility: {
|
|
200
297
|
values: ['visible', 'hidden'],
|
|
@@ -204,7 +301,38 @@ export const CSS_PROPERTIES_DEFINITION: Record<string, CSSPropertyDefinition> =
|
|
|
204
301
|
values: ['normal', 'multiply', 'screen', 'overlay', 'darken', 'lighten', 'color-dodge', 'color-burn', 'hard-light', 'soft-light', 'difference', 'exclusion', 'hue', 'saturation', 'color', 'luminosity'],
|
|
205
302
|
type: 'select',
|
|
206
303
|
},
|
|
304
|
+
isolation: {
|
|
305
|
+
values: ['auto', 'isolate'],
|
|
306
|
+
type: 'select',
|
|
307
|
+
},
|
|
308
|
+
willChange: { type: 'string' },
|
|
309
|
+
imageRendering: {
|
|
310
|
+
values: ['auto', 'crisp-edges', 'pixelated', 'smooth'],
|
|
311
|
+
type: 'select',
|
|
312
|
+
},
|
|
207
313
|
clipPath: { type: 'string' },
|
|
314
|
+
WebkitClipPath: { type: 'string' },
|
|
315
|
+
// Mask family — gradient/SVG masks. Emit both the standard and `-webkit-`
|
|
316
|
+
// form because Safari still needs the prefix.
|
|
317
|
+
maskImage: { type: 'string' },
|
|
318
|
+
WebkitMaskImage: { type: 'string' },
|
|
319
|
+
maskSize: { type: 'string' },
|
|
320
|
+
WebkitMaskSize: { type: 'string' },
|
|
321
|
+
maskPosition: { type: 'string' },
|
|
322
|
+
WebkitMaskPosition: { type: 'string' },
|
|
323
|
+
maskRepeat: { type: 'string' },
|
|
324
|
+
WebkitMaskRepeat: { type: 'string' },
|
|
325
|
+
maskOrigin: { type: 'string' },
|
|
326
|
+
WebkitMaskOrigin: { type: 'string' },
|
|
327
|
+
maskClip: { type: 'string' },
|
|
328
|
+
WebkitMaskClip: { type: 'string' },
|
|
329
|
+
maskComposite: { type: 'string' },
|
|
330
|
+
WebkitMaskComposite: { type: 'string' },
|
|
331
|
+
maskMode: { type: 'string' },
|
|
332
|
+
maskType: {
|
|
333
|
+
values: ['luminance', 'alpha'],
|
|
334
|
+
type: 'select',
|
|
335
|
+
},
|
|
208
336
|
|
|
209
337
|
// Overflow & Content
|
|
210
338
|
overflow: {
|
|
@@ -246,6 +374,27 @@ export const CSS_PROPERTIES_DEFINITION: Record<string, CSSPropertyDefinition> =
|
|
|
246
374
|
values: ['auto', 'none', 'text', 'all'],
|
|
247
375
|
type: 'select',
|
|
248
376
|
},
|
|
377
|
+
WebkitUserSelect: {
|
|
378
|
+
values: ['auto', 'none', 'text', 'all'],
|
|
379
|
+
type: 'select',
|
|
380
|
+
},
|
|
381
|
+
MozUserSelect: {
|
|
382
|
+
values: ['auto', 'none', 'text', 'all'],
|
|
383
|
+
type: 'select',
|
|
384
|
+
},
|
|
385
|
+
caretColor: { type: 'string' },
|
|
386
|
+
appearance: {
|
|
387
|
+
values: ['none', 'auto', 'menulist-button', 'textfield'],
|
|
388
|
+
type: 'select',
|
|
389
|
+
},
|
|
390
|
+
WebkitAppearance: {
|
|
391
|
+
values: ['none', 'auto', 'menulist-button', 'textfield'],
|
|
392
|
+
type: 'select',
|
|
393
|
+
},
|
|
394
|
+
MozAppearance: {
|
|
395
|
+
values: ['none', 'auto', 'menulist-button', 'textfield'],
|
|
396
|
+
type: 'select',
|
|
397
|
+
},
|
|
249
398
|
|
|
250
399
|
// Outline
|
|
251
400
|
outline: { type: 'string' },
|
|
@@ -297,6 +446,41 @@ export const CSS_PROPERTIES_DEFINITION: Record<string, CSSPropertyDefinition> =
|
|
|
297
446
|
values: ['auto', 'smooth'],
|
|
298
447
|
type: 'select',
|
|
299
448
|
},
|
|
449
|
+
scrollSnapType: { type: 'string' },
|
|
450
|
+
scrollSnapAlign: {
|
|
451
|
+
values: ['none', 'start', 'end', 'center'],
|
|
452
|
+
type: 'select',
|
|
453
|
+
},
|
|
454
|
+
scrollSnapStop: {
|
|
455
|
+
values: ['normal', 'always'],
|
|
456
|
+
type: 'select',
|
|
457
|
+
},
|
|
458
|
+
scrollMarginTop: { type: 'string' },
|
|
459
|
+
scrollMarginRight: { type: 'string' },
|
|
460
|
+
scrollMarginBottom: { type: 'string' },
|
|
461
|
+
scrollMarginLeft: { type: 'string' },
|
|
462
|
+
scrollPaddingTop: { type: 'string' },
|
|
463
|
+
scrollPaddingRight: { type: 'string' },
|
|
464
|
+
scrollPaddingBottom: { type: 'string' },
|
|
465
|
+
scrollPaddingLeft: { type: 'string' },
|
|
466
|
+
overscrollBehavior: {
|
|
467
|
+
values: ['auto', 'contain', 'none'],
|
|
468
|
+
type: 'select',
|
|
469
|
+
},
|
|
470
|
+
overscrollBehaviorX: {
|
|
471
|
+
values: ['auto', 'contain', 'none'],
|
|
472
|
+
type: 'select',
|
|
473
|
+
},
|
|
474
|
+
overscrollBehaviorY: {
|
|
475
|
+
values: ['auto', 'contain', 'none'],
|
|
476
|
+
type: 'select',
|
|
477
|
+
},
|
|
478
|
+
// CSS Containment — container queries depend on these.
|
|
479
|
+
containerType: {
|
|
480
|
+
values: ['normal', 'size', 'inline-size'],
|
|
481
|
+
type: 'select',
|
|
482
|
+
},
|
|
483
|
+
containerName: { type: 'string' },
|
|
300
484
|
accentColor: { type: 'string' },
|
|
301
485
|
|
|
302
486
|
// SVG
|
|
@@ -159,3 +159,57 @@ export function isComplexExpression(expression: string): boolean {
|
|
|
159
159
|
// Excludes dots (.) which are used for simple field access
|
|
160
160
|
return /[?:+\-*/%<>=!&|]/.test(expression);
|
|
161
161
|
}
|
|
162
|
+
|
|
163
|
+
/** jsep node types `evaluateNode` (above) can evaluate — the rest throw at runtime. */
|
|
164
|
+
const SUPPORTED_NODE_TYPES = new Set([
|
|
165
|
+
'Identifier',
|
|
166
|
+
'Literal',
|
|
167
|
+
'MemberExpression',
|
|
168
|
+
'BinaryExpression',
|
|
169
|
+
'LogicalExpression',
|
|
170
|
+
'ConditionalExpression',
|
|
171
|
+
'UnaryExpression',
|
|
172
|
+
'ArrayExpression',
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
/** Child-expression slots jsep produces on the supported node types. */
|
|
176
|
+
const CHILD_KEYS = ['object', 'property', 'left', 'right', 'test', 'consequent', 'alternate', 'argument'];
|
|
177
|
+
|
|
178
|
+
/** Recursively verify every node in a jsep AST is a type this evaluator supports. */
|
|
179
|
+
function isSupportedNode(node: Expression | null | undefined): boolean {
|
|
180
|
+
if (!node) return true;
|
|
181
|
+
if (!SUPPORTED_NODE_TYPES.has(node.type)) return false; // CallExpression, Compound, …
|
|
182
|
+
const n = node as unknown as Record<string, unknown>;
|
|
183
|
+
for (const key of CHILD_KEYS) {
|
|
184
|
+
const child = n[key];
|
|
185
|
+
if (child && typeof child === 'object' && 'type' in child) {
|
|
186
|
+
if (!isSupportedNode(child as Expression)) return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (Array.isArray(n.elements)) {
|
|
190
|
+
for (const el of n.elements) {
|
|
191
|
+
if (el && !isSupportedNode(el as Expression)) return false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* True when `expression` is one the template engine can actually evaluate (see
|
|
199
|
+
* {@link safeEvaluate}): jsep parses it and every node is a whitelisted type —
|
|
200
|
+
* identifier, member access, literal, the supported operators, ternary, or array.
|
|
201
|
+
*
|
|
202
|
+
* Returns false for anything with a function/method call (`x.toFixed(2)`), a compound
|
|
203
|
+
* (`a, b`), or a syntax error. Those are arbitrary JS that the Meno model cannot
|
|
204
|
+
* represent as a `{{template}}` binding — the meno-astro codec preserves them verbatim
|
|
205
|
+
* instead of coercing them into a fake binding.
|
|
206
|
+
*/
|
|
207
|
+
export function isSupportedTemplateExpression(expression: string): boolean {
|
|
208
|
+
const expr = expression.trim();
|
|
209
|
+
if (!expr) return false;
|
|
210
|
+
try {
|
|
211
|
+
return isSupportedNode(jsep(expr));
|
|
212
|
+
} catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure `@font-face` / preload generation from a list of font configs.
|
|
3
|
+
*
|
|
4
|
+
* No filesystem or project-context coupling — the caller passes the `fonts`
|
|
5
|
+
* array (read however it likes). This is the single source of truth for font
|
|
6
|
+
* CSS formatting, shared by every renderer:
|
|
7
|
+
* - meno-core SSR → via {@link ../shared/fontLoader} (cached config)
|
|
8
|
+
* - meno-core JSON→Astro → build-astro.ts
|
|
9
|
+
* - meno-astro dialect twin → meno-astro/server `loadFontCss` → BaseLayout.astro
|
|
10
|
+
*
|
|
11
|
+
* Keeping it server-free means the Astro `BaseLayout` can import it without
|
|
12
|
+
* dragging meno-core's server runtime into the build.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface FontConfig {
|
|
16
|
+
path: string;
|
|
17
|
+
family?: string;
|
|
18
|
+
weight?: number;
|
|
19
|
+
weightMax?: number; // If set, font is variable with weight range [weight, weightMax]
|
|
20
|
+
style?: 'normal' | 'italic';
|
|
21
|
+
fontDisplay?: 'auto' | 'block' | 'swap' | 'fallback' | 'optional';
|
|
22
|
+
unicodeRange?: string;
|
|
23
|
+
/** Legacy alias for `path` (emitted by the website-import flow). */
|
|
24
|
+
src?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Detect the `@font-face` `format()` value from a file extension. */
|
|
28
|
+
function getFontFormat(path: string): string {
|
|
29
|
+
if (path.endsWith('.woff2')) return 'woff2';
|
|
30
|
+
if (path.endsWith('.woff')) return 'woff';
|
|
31
|
+
if (path.endsWith('.ttf')) return 'truetype';
|
|
32
|
+
if (path.endsWith('.otf')) return 'opentype';
|
|
33
|
+
return 'truetype';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Font `type` attribute for a `<link rel="preload">`. */
|
|
37
|
+
function getFontMimeType(path: string): string {
|
|
38
|
+
if (path.endsWith('.woff2')) return 'font/woff2';
|
|
39
|
+
if (path.endsWith('.woff')) return 'font/woff';
|
|
40
|
+
if (path.endsWith('.ttf')) return 'font/ttf';
|
|
41
|
+
if (path.endsWith('.otf')) return 'font/otf';
|
|
42
|
+
return 'font/ttf';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Derive a family name from a path when none is configured.
|
|
47
|
+
* Example: "/fonts/geomanist-regular.ttf" -> "Geomanist Regular".
|
|
48
|
+
*/
|
|
49
|
+
export function extractFamilyName(path: string): string {
|
|
50
|
+
const filename = path.split('/').pop() || 'Font';
|
|
51
|
+
const name = filename.replace(/\.(ttf|woff2?|otf)$/i, '');
|
|
52
|
+
return name
|
|
53
|
+
.split('-')
|
|
54
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
55
|
+
.join(' ');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Build the `@font-face` rules for the given fonts (empty string for none). */
|
|
59
|
+
export function fontFaceCss(fonts: FontConfig[]): string {
|
|
60
|
+
return (fonts || [])
|
|
61
|
+
.filter((font) => font.path || font.src)
|
|
62
|
+
.map((font) => {
|
|
63
|
+
// Support both "path" (standard) and "src" (legacy from website import)
|
|
64
|
+
const fontPath = (font.path || font.src) as string;
|
|
65
|
+
const format = getFontFormat(fontPath);
|
|
66
|
+
const family = font.family || extractFamilyName(fontPath);
|
|
67
|
+
const weight = font.weight ?? 400;
|
|
68
|
+
const weightMax = font.weightMax;
|
|
69
|
+
const style = font.style ?? 'normal';
|
|
70
|
+
const fontDisplay = font.fontDisplay;
|
|
71
|
+
|
|
72
|
+
// Variable fonts use weight range syntax: "100 900"
|
|
73
|
+
const fontWeight = weightMax != null ? `${weight} ${weightMax}` : weight;
|
|
74
|
+
const unicodeRange = font.unicodeRange;
|
|
75
|
+
|
|
76
|
+
return `@font-face {
|
|
77
|
+
font-family: '${family}';
|
|
78
|
+
src: url('${fontPath}') format('${format}');
|
|
79
|
+
font-weight: ${fontWeight};
|
|
80
|
+
font-style: ${style};${fontDisplay ? `\n font-display: ${fontDisplay};` : ''}${unicodeRange ? `\n unicode-range: ${unicodeRange};` : ''}
|
|
81
|
+
}`;
|
|
82
|
+
})
|
|
83
|
+
.join('\n\n');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build `<link rel="preload">` tags for the given fonts (empty string for none).
|
|
88
|
+
* `crossorigin` is required for the preloaded font to be reused by the CSS.
|
|
89
|
+
*/
|
|
90
|
+
export function fontPreloadLinks(fonts: FontConfig[]): string {
|
|
91
|
+
if (!fonts || fonts.length === 0) return '';
|
|
92
|
+
|
|
93
|
+
return fonts
|
|
94
|
+
.filter((font) => font.path || font.src)
|
|
95
|
+
.map((font) => {
|
|
96
|
+
const fontPath = (font.path || font.src) as string;
|
|
97
|
+
const mimeType = getFontMimeType(fontPath);
|
|
98
|
+
return `<link rel="preload" href="${fontPath}" as="font" type="${mimeType}" crossorigin>`;
|
|
99
|
+
})
|
|
100
|
+
.join('\n ');
|
|
101
|
+
}
|
package/lib/shared/fontLoader.ts
CHANGED
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
import { projectPaths } from '../server/projectContext';
|
|
2
2
|
import { readJsonFile, fileExists } from '../server/runtime';
|
|
3
|
+
import { fontFaceCss, fontPreloadLinks, extractFamilyName, type FontConfig } from './fontCss';
|
|
3
4
|
|
|
4
|
-
export
|
|
5
|
-
path: string;
|
|
6
|
-
family?: string;
|
|
7
|
-
weight?: number;
|
|
8
|
-
weightMax?: number; // If set, font is variable with weight range [weight, weightMax]
|
|
9
|
-
style?: 'normal' | 'italic';
|
|
10
|
-
fontDisplay?: 'auto' | 'block' | 'swap' | 'fallback' | 'optional';
|
|
11
|
-
unicodeRange?: string;
|
|
12
|
-
}
|
|
5
|
+
export type { FontConfig };
|
|
13
6
|
|
|
14
7
|
interface ProjectConfig {
|
|
15
8
|
fonts?: FontConfig[];
|
|
@@ -39,92 +32,21 @@ export function getProjectConfig(): ProjectConfig {
|
|
|
39
32
|
}
|
|
40
33
|
|
|
41
34
|
/**
|
|
42
|
-
*
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (path.endsWith('.woff2')) return 'woff2';
|
|
46
|
-
if (path.endsWith('.woff')) return 'woff';
|
|
47
|
-
if (path.endsWith('.ttf')) return 'truetype';
|
|
48
|
-
if (path.endsWith('.otf')) return 'opentype';
|
|
49
|
-
return 'truetype';
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Extract font family name from path if not provided
|
|
54
|
-
* Example: "/fonts/geomanist-regular.ttf" -> "Geomanist"
|
|
35
|
+
* Generate `@font-face` CSS for the project's configured fonts.
|
|
36
|
+
* Thin wrapper over the pure {@link fontFaceCss} (formatting lives in `./fontCss`
|
|
37
|
+
* so renderers without a project-context cache — e.g. the Astro twin — can reuse it).
|
|
55
38
|
*/
|
|
56
|
-
function extractFamilyName(path: string): string {
|
|
57
|
-
const filename = path.split('/').pop() || 'Font';
|
|
58
|
-
const name = filename.replace(/\.(ttf|woff2?|otf)$/i, '');
|
|
59
|
-
// Capitalize and replace hyphens with spaces
|
|
60
|
-
return name
|
|
61
|
-
.split('-')
|
|
62
|
-
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
63
|
-
.join(' ');
|
|
64
|
-
}
|
|
65
|
-
|
|
66
39
|
export function generateFontCSS(): string {
|
|
67
|
-
|
|
68
|
-
const fonts = config.fonts || [];
|
|
69
|
-
|
|
70
|
-
return fonts
|
|
71
|
-
.filter((font: any) => font.path || font.src)
|
|
72
|
-
.map((font: any) => {
|
|
73
|
-
// Support both "path" (standard) and "src" (legacy from website import)
|
|
74
|
-
const fontPath: string = font.path || font.src;
|
|
75
|
-
const format = getFontFormat(fontPath);
|
|
76
|
-
const family = font.family || extractFamilyName(fontPath);
|
|
77
|
-
const weight = font.weight ?? 400;
|
|
78
|
-
const weightMax = font.weightMax;
|
|
79
|
-
const style = font.style ?? 'normal';
|
|
80
|
-
const fontDisplay = font.fontDisplay;
|
|
81
|
-
|
|
82
|
-
// Variable fonts use weight range syntax: "100 900"
|
|
83
|
-
const fontWeight = weightMax != null ? `${weight} ${weightMax}` : weight;
|
|
84
|
-
|
|
85
|
-
const unicodeRange = font.unicodeRange;
|
|
86
|
-
|
|
87
|
-
return `@font-face {
|
|
88
|
-
font-family: '${family}';
|
|
89
|
-
src: url('${fontPath}') format('${format}');
|
|
90
|
-
font-weight: ${fontWeight};
|
|
91
|
-
font-style: ${style};${fontDisplay ? `\n font-display: ${fontDisplay};` : ''}${unicodeRange ? `\n unicode-range: ${unicodeRange};` : ''}
|
|
92
|
-
}`;
|
|
93
|
-
})
|
|
94
|
-
.join('\n\n');
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Get font MIME type for preload "type" attribute
|
|
99
|
-
*/
|
|
100
|
-
function getFontMimeType(path: string): string {
|
|
101
|
-
if (path.endsWith('.woff2')) return 'font/woff2';
|
|
102
|
-
if (path.endsWith('.woff')) return 'font/woff';
|
|
103
|
-
if (path.endsWith('.ttf')) return 'font/ttf';
|
|
104
|
-
if (path.endsWith('.otf')) return 'font/otf';
|
|
105
|
-
return 'font/ttf';
|
|
40
|
+
return fontFaceCss(getProjectConfig().fonts || []);
|
|
106
41
|
}
|
|
107
42
|
|
|
108
43
|
/**
|
|
109
|
-
* Generate font preload link tags for early font loading
|
|
44
|
+
* Generate font preload link tags for early font loading.
|
|
110
45
|
* This prevents font swap flash on SPA navigation by ensuring fonts are
|
|
111
46
|
* loaded and cached before they're needed.
|
|
112
47
|
*/
|
|
113
48
|
export function generateFontPreloadTags(): string {
|
|
114
|
-
|
|
115
|
-
const fonts = config.fonts || [];
|
|
116
|
-
|
|
117
|
-
if (fonts.length === 0) return '';
|
|
118
|
-
|
|
119
|
-
return fonts
|
|
120
|
-
.filter((font: any) => font.path || font.src)
|
|
121
|
-
.map((font: any) => {
|
|
122
|
-
const fontPath: string = font.path || font.src;
|
|
123
|
-
const mimeType = getFontMimeType(fontPath);
|
|
124
|
-
// crossorigin is required for fonts to be cached properly
|
|
125
|
-
return `<link rel="preload" href="${fontPath}" as="font" type="${mimeType}" crossorigin>`;
|
|
126
|
-
})
|
|
127
|
-
.join('\n ');
|
|
49
|
+
return fontPreloadLinks(getProjectConfig().fonts || []);
|
|
128
50
|
}
|
|
129
51
|
|
|
130
52
|
export function getFontFamilies(): Record<string, number[]> {
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the friendly-error mapper.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'bun:test';
|
|
6
|
+
import { toFriendlyError } from './friendlyError';
|
|
7
|
+
|
|
8
|
+
describe('toFriendlyError', () => {
|
|
9
|
+
const cases: Array<{ name: string; input: string; expectTitle: string; expectHint?: boolean }> = [
|
|
10
|
+
{
|
|
11
|
+
name: 'null property read (the reported crash)',
|
|
12
|
+
input: "Cannot read properties of null (reading 'length')",
|
|
13
|
+
expectTitle: "A section couldn't load its content",
|
|
14
|
+
expectHint: true,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: 'undefined property read (older phrasing)',
|
|
18
|
+
input: "Cannot read property 'map' of undefined",
|
|
19
|
+
expectTitle: "A section couldn't load its content",
|
|
20
|
+
expectHint: true,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'not a function',
|
|
24
|
+
input: 'foo.bar is not a function',
|
|
25
|
+
expectTitle: "A piece of code didn't run as expected",
|
|
26
|
+
expectHint: true,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'not defined',
|
|
30
|
+
input: 'myVar is not defined',
|
|
31
|
+
expectTitle: 'A piece of code referenced something missing',
|
|
32
|
+
expectHint: true,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'network failure',
|
|
36
|
+
input: 'Failed to fetch',
|
|
37
|
+
expectTitle: "Couldn't reach the network",
|
|
38
|
+
expectHint: true,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'module load failure',
|
|
42
|
+
input: 'Cannot find module ./missing',
|
|
43
|
+
expectTitle: "A required file couldn't be loaded",
|
|
44
|
+
expectHint: true,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'malformed JSON',
|
|
48
|
+
input: 'Unexpected token < in JSON at position 0',
|
|
49
|
+
expectTitle: 'Some data was malformed',
|
|
50
|
+
expectHint: true,
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
for (const c of cases) {
|
|
55
|
+
it(`maps ${c.name}`, () => {
|
|
56
|
+
const friendly = toFriendlyError(c.input);
|
|
57
|
+
expect(friendly.title).toBe(c.expectTitle);
|
|
58
|
+
expect(friendly.friendlyMessage.length).toBeGreaterThan(0);
|
|
59
|
+
expect(friendly.raw).toBe(c.input);
|
|
60
|
+
if (c.expectHint) expect(friendly.hint).toBeTruthy();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
it('includes the offending property name when present', () => {
|
|
65
|
+
const friendly = toFriendlyError("Cannot read properties of null (reading 'length')");
|
|
66
|
+
expect(friendly.friendlyMessage).toContain('length');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('falls back reassuringly for unknown errors but keeps the raw text', () => {
|
|
70
|
+
const friendly = toFriendlyError('Some totally unexpected gibberish');
|
|
71
|
+
expect(friendly.title).toBe('This section ran into a problem');
|
|
72
|
+
expect(friendly.raw).toBe('Some totally unexpected gibberish');
|
|
73
|
+
expect(friendly.hint).toBeUndefined();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('accepts an Error instance', () => {
|
|
77
|
+
const friendly = toFriendlyError(new Error('boom is not a function'));
|
|
78
|
+
expect(friendly.title).toBe("A piece of code didn't run as expected");
|
|
79
|
+
expect(friendly.raw).toBe('boom is not a function');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('accepts non-error values without throwing', () => {
|
|
83
|
+
const friendly = toFriendlyError({ weird: true });
|
|
84
|
+
expect(friendly.raw).toBe('[object Object]');
|
|
85
|
+
expect(friendly.title.length).toBeGreaterThan(0);
|
|
86
|
+
});
|
|
87
|
+
});
|