meno-core 1.0.46 → 1.0.48
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 +13 -77
- package/dist/build-static.js +7 -7
- package/dist/chunks/{chunk-FED5MME6.js → chunk-3FHJUHAS.js} +5 -4
- package/dist/chunks/chunk-3FHJUHAS.js.map +7 -0
- package/dist/chunks/{chunk-XSWR3QLI.js → chunk-B2RTLDXY.js} +130 -130
- package/dist/chunks/chunk-B2RTLDXY.js.map +7 -0
- package/dist/chunks/{chunk-ZTKHJQ2Z.js → chunk-BJRKEPMP.js} +2 -2
- package/dist/chunks/{chunk-ORN7S4AP.js → chunk-D5E3OKSL.js} +5 -5
- package/dist/chunks/{chunk-KITQJYZV.js → chunk-EK4KESLU.js} +33 -3
- package/dist/chunks/chunk-EK4KESLU.js.map +7 -0
- package/dist/chunks/{chunk-C6U5T5S5.js → chunk-NKUV77SR.js} +43 -10
- package/dist/chunks/chunk-NKUV77SR.js.map +7 -0
- package/dist/chunks/{chunk-ZWYDT3QJ.js → chunk-NP76N4HQ.js} +4 -4
- package/dist/chunks/{chunk-ZWYDT3QJ.js.map → chunk-NP76N4HQ.js.map} +2 -2
- package/dist/chunks/{chunk-IF3RATBY.js → chunk-RQSTH2BS.js} +2 -2
- package/dist/chunks/{chunk-77ZB6353.js → chunk-TPQ7APVQ.js} +42 -86
- package/dist/chunks/chunk-TPQ7APVQ.js.map +7 -0
- package/dist/chunks/{configService-DYCUEURL.js → configService-IGJEC3MC.js} +3 -3
- package/dist/entries/server-router.js +8 -8
- package/dist/lib/client/index.js +16 -4
- package/dist/lib/client/index.js.map +2 -2
- package/dist/lib/server/index.js +1803 -383
- package/dist/lib/server/index.js.map +4 -4
- package/dist/lib/shared/index.js +8 -4
- package/dist/lib/shared/index.js.map +1 -1
- package/lib/client/core/ComponentBuilder.test.ts +38 -0
- package/lib/client/core/ComponentBuilder.ts +25 -1
- package/lib/server/astro/astroEmitHelpers.ts +5 -0
- package/lib/server/astro/cmsPageEmitter.ts +15 -2
- package/lib/server/astro/componentEmitter.ts +13 -5
- package/lib/server/astro/nodeToAstro.ts +23 -9
- package/lib/server/astro/pageEmitter.ts +15 -2
- package/lib/server/index.ts +1 -1
- package/lib/server/jsonLoader.test.ts +0 -17
- package/lib/server/jsonLoader.ts +0 -81
- package/lib/server/routes/api/variables.ts +4 -2
- package/lib/server/ssr/htmlGenerator.test.ts +4 -4
- package/lib/server/ssr/htmlGenerator.ts +7 -2
- package/lib/server/ssr/imageMetadata.ts +15 -9
- package/lib/server/ssr/ssrRenderer.test.ts +103 -1
- package/lib/server/ssr/ssrRenderer.ts +41 -35
- package/lib/server/utils/jsonLineMapper.test.ts +53 -1
- package/lib/server/utils/jsonLineMapper.ts +43 -3
- package/lib/server/webflow/buildWebflow.ts +343 -123
- package/lib/server/webflow/index.ts +1 -0
- package/lib/server/webflow/nodeToWebflow.test.ts +3170 -0
- package/lib/server/webflow/nodeToWebflow.ts +2141 -129
- package/lib/server/webflow/styleMapper.test.ts +389 -0
- package/lib/server/webflow/styleMapper.ts +517 -63
- package/lib/server/webflow/templateWrapper.ts +49 -0
- package/lib/server/webflow/types.ts +218 -18
- package/lib/shared/elementClassName.test.ts +15 -0
- package/lib/shared/elementClassName.ts +7 -3
- package/lib/shared/registry/nodeTypes/ListNodeType.ts +1 -1
- package/lib/shared/styleNodeUtils.test.ts +47 -1
- package/lib/shared/styleNodeUtils.ts +7 -7
- package/lib/shared/types/index.ts +1 -0
- package/lib/shared/types/variables.ts +37 -0
- package/package.json +1 -1
- package/dist/chunks/chunk-77ZB6353.js.map +0 -7
- package/dist/chunks/chunk-C6U5T5S5.js.map +0 -7
- package/dist/chunks/chunk-FED5MME6.js.map +0 -7
- package/dist/chunks/chunk-KITQJYZV.js.map +0 -7
- package/dist/chunks/chunk-XSWR3QLI.js.map +0 -7
- /package/dist/chunks/{chunk-ZTKHJQ2Z.js.map → chunk-BJRKEPMP.js.map} +0 -0
- /package/dist/chunks/{chunk-ORN7S4AP.js.map → chunk-D5E3OKSL.js.map} +0 -0
- /package/dist/chunks/{chunk-IF3RATBY.js.map → chunk-RQSTH2BS.js.map} +0 -0
- /package/dist/chunks/{configService-DYCUEURL.js.map → configService-IGJEC3MC.js.map} +0 -0
|
@@ -39,7 +39,7 @@ import { isI18nValue, resolveI18nValue, DEFAULT_I18N_CONFIG, buildLocalizedPath
|
|
|
39
39
|
import { transformCMSTemplate, isTemplateExpression, transformItemTemplate, replaceItemMetaVars, rewriteItemVar } from './templateTransformer';
|
|
40
40
|
import type { SlugMap } from '../../shared/slugTranslator';
|
|
41
41
|
import { buildSlugIndex, translatePath } from '../../shared/slugTranslator';
|
|
42
|
-
import { stripRawHtmlPrefixDeep } from './astroEmitHelpers';
|
|
42
|
+
import { stripRawHtmlPrefixDeep, astroComponentName } from './astroEmitHelpers';
|
|
43
43
|
|
|
44
44
|
// ---------------------------------------------------------------------------
|
|
45
45
|
// Types
|
|
@@ -127,6 +127,8 @@ export interface AstroEmitContext {
|
|
|
127
127
|
imageImports?: Map<string, string>;
|
|
128
128
|
/** File depth (relative to src/pages or src/components) for import path computation */
|
|
129
129
|
fileDepth?: number;
|
|
130
|
+
/** Collected interactive styles during emission: elementClass → InteractiveStyles */
|
|
131
|
+
collectedInteractiveStyles?: Map<string, InteractiveStyles>;
|
|
130
132
|
}
|
|
131
133
|
|
|
132
134
|
// ---------------------------------------------------------------------------
|
|
@@ -1029,7 +1031,9 @@ function emitImageNode(node: HtmlNode, ctx: AstroEmitContext): string {
|
|
|
1029
1031
|
node.generateElementClass
|
|
1030
1032
|
) {
|
|
1031
1033
|
elementClass = buildElementClass(ctx, node.label);
|
|
1032
|
-
|
|
1034
|
+
if (elementClass && node.interactiveStyles?.length && ctx.collectedInteractiveStyles) {
|
|
1035
|
+
ctx.collectedInteractiveStyles.set(elementClass, node.interactiveStyles as InteractiveStyles);
|
|
1036
|
+
}
|
|
1033
1037
|
}
|
|
1034
1038
|
|
|
1035
1039
|
const { classExpr, styleAttr } = buildClassAndStyleExpression(
|
|
@@ -1216,7 +1220,9 @@ function emitHtmlNode(node: HtmlNode, ctx: AstroEmitContext): string {
|
|
|
1216
1220
|
node.generateElementClass
|
|
1217
1221
|
) {
|
|
1218
1222
|
elementClass = buildElementClass(ctx, label);
|
|
1219
|
-
|
|
1223
|
+
if (elementClass && node.interactiveStyles?.length && ctx.collectedInteractiveStyles) {
|
|
1224
|
+
ctx.collectedInteractiveStyles.set(elementClass, node.interactiveStyles as InteractiveStyles);
|
|
1225
|
+
}
|
|
1220
1226
|
}
|
|
1221
1227
|
|
|
1222
1228
|
const { classExpr, styleAttr } = buildClassAndStyleExpression(style, node.interactiveStyles as InteractiveStyles | undefined, elementClass, ctx);
|
|
@@ -1243,8 +1249,8 @@ function emitHtmlNode(node: HtmlNode, ctx: AstroEmitContext): string {
|
|
|
1243
1249
|
}
|
|
1244
1250
|
|
|
1245
1251
|
function emitComponentInstance(node: ComponentInstanceNode, ctx: AstroEmitContext): string {
|
|
1246
|
-
const name = node.component;
|
|
1247
|
-
ctx.imports.add(
|
|
1252
|
+
const name = astroComponentName(node.component);
|
|
1253
|
+
ctx.imports.add(node.component);
|
|
1248
1254
|
|
|
1249
1255
|
const ifExpr = emitIfOpen(node, ctx);
|
|
1250
1256
|
|
|
@@ -1355,7 +1361,9 @@ function emitEmbedNode(node: EmbedNode, ctx: AstroEmitContext): string {
|
|
|
1355
1361
|
node.generateElementClass
|
|
1356
1362
|
) {
|
|
1357
1363
|
elementClass = buildElementClass(ctx, node.label);
|
|
1358
|
-
|
|
1364
|
+
if (elementClass && node.interactiveStyles?.length && ctx.collectedInteractiveStyles) {
|
|
1365
|
+
ctx.collectedInteractiveStyles.set(elementClass, node.interactiveStyles as InteractiveStyles);
|
|
1366
|
+
}
|
|
1359
1367
|
}
|
|
1360
1368
|
|
|
1361
1369
|
const { classExpr, styleAttr } = buildClassAndStyleExpression(style, node.interactiveStyles as InteractiveStyles | undefined, elementClass, ctx);
|
|
@@ -1428,7 +1436,9 @@ function emitLinkNode(node: LinkNode, ctx: AstroEmitContext): string {
|
|
|
1428
1436
|
node.generateElementClass
|
|
1429
1437
|
) {
|
|
1430
1438
|
elementClass = buildElementClass(ctx, node.label);
|
|
1431
|
-
|
|
1439
|
+
if (elementClass && node.interactiveStyles?.length && ctx.collectedInteractiveStyles) {
|
|
1440
|
+
ctx.collectedInteractiveStyles.set(elementClass, node.interactiveStyles as InteractiveStyles);
|
|
1441
|
+
}
|
|
1432
1442
|
}
|
|
1433
1443
|
|
|
1434
1444
|
// Build class expression with olink base class
|
|
@@ -1525,7 +1535,9 @@ function emitImageTypeNode(node: any, ctx: AstroEmitContext): string {
|
|
|
1525
1535
|
node.generateElementClass
|
|
1526
1536
|
) {
|
|
1527
1537
|
elementClass = buildElementClass(ctx, node.label);
|
|
1528
|
-
|
|
1538
|
+
if (elementClass && node.interactiveStyles?.length && ctx.collectedInteractiveStyles) {
|
|
1539
|
+
ctx.collectedInteractiveStyles.set(elementClass, node.interactiveStyles as InteractiveStyles);
|
|
1540
|
+
}
|
|
1529
1541
|
}
|
|
1530
1542
|
|
|
1531
1543
|
const { classExpr, styleAttr } = buildClassAndStyleExpression(
|
|
@@ -1784,7 +1796,9 @@ function emitLocaleListNode(node: LocaleListNode, ctx: AstroEmitContext): string
|
|
|
1784
1796
|
let elementClass: string | null = null;
|
|
1785
1797
|
if ((node.interactiveStyles && (node.interactiveStyles as InteractiveStyles).length > 0) || node.generateElementClass) {
|
|
1786
1798
|
elementClass = buildElementClass(ctx, node.label);
|
|
1787
|
-
|
|
1799
|
+
if (elementClass && (node.interactiveStyles as InteractiveStyles)?.length && ctx.collectedInteractiveStyles) {
|
|
1800
|
+
ctx.collectedInteractiveStyles.set(elementClass, node.interactiveStyles as InteractiveStyles);
|
|
1801
|
+
}
|
|
1788
1802
|
}
|
|
1789
1803
|
|
|
1790
1804
|
const { classExpr: containerClassExpr, styleAttr: containerStyleAttr } = buildClassAndStyleExpression(
|
|
@@ -10,6 +10,10 @@ import { nodeToAstro, type AstroEmitContext } from './nodeToAstro';
|
|
|
10
10
|
import type { ImageMetadataMap } from '../ssr/imageMetadata';
|
|
11
11
|
import type { SlugMap } from '../../shared/slugTranslator';
|
|
12
12
|
import type { ResponsiveScales } from '../../shared/responsiveScaling';
|
|
13
|
+
import type { InteractiveStyles } from '../../shared/types/styles';
|
|
14
|
+
import type { RemConversionConfig } from '../../shared/pxToRem';
|
|
15
|
+
import { generateAllInteractiveCSS } from '../../shared/cssGeneration';
|
|
16
|
+
import { astroComponentName } from './astroEmitHelpers';
|
|
13
17
|
|
|
14
18
|
// ---------------------------------------------------------------------------
|
|
15
19
|
// Types
|
|
@@ -62,6 +66,8 @@ export interface PageEmitOptions {
|
|
|
62
66
|
* (image rewriting, component expansion, link localization).
|
|
63
67
|
*/
|
|
64
68
|
processedRawHtml?: Map<string, string>;
|
|
69
|
+
/** Rem conversion config for interactive CSS generation */
|
|
70
|
+
remConfig?: RemConversionConfig;
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
// ---------------------------------------------------------------------------
|
|
@@ -121,6 +127,7 @@ export function emitAstroPage(options: PageEmitOptions): string {
|
|
|
121
127
|
slugMappings,
|
|
122
128
|
imageFormat,
|
|
123
129
|
processedRawHtml,
|
|
130
|
+
remConfig,
|
|
124
131
|
} = options;
|
|
125
132
|
|
|
126
133
|
const breakpoints = breakpointsOpt ?? DEFAULT_BREAKPOINTS;
|
|
@@ -156,6 +163,7 @@ export function emitAstroPage(options: PageEmitOptions): string {
|
|
|
156
163
|
processedRawHtml,
|
|
157
164
|
imageImports: new Map<string, string>(),
|
|
158
165
|
fileDepth,
|
|
166
|
+
collectedInteractiveStyles: new Map<string, InteractiveStyles>(),
|
|
159
167
|
};
|
|
160
168
|
|
|
161
169
|
// Emit the template body
|
|
@@ -186,7 +194,7 @@ export function emitAstroPage(options: PageEmitOptions): string {
|
|
|
186
194
|
const componentImports = Array.from(ctx.imports).sort();
|
|
187
195
|
for (const comp of componentImports) {
|
|
188
196
|
const path = componentImportPath(fileDepth, comp);
|
|
189
|
-
importLines.push(`import ${comp} from '${path}';`);
|
|
197
|
+
importLines.push(`import ${astroComponentName(comp)} from '${path}';`);
|
|
190
198
|
}
|
|
191
199
|
|
|
192
200
|
// Build script paths array
|
|
@@ -205,6 +213,11 @@ export function emitAstroPage(options: PageEmitOptions): string {
|
|
|
205
213
|
? '\n' + ctx.frontmatterLines.join('\n')
|
|
206
214
|
: '';
|
|
207
215
|
|
|
216
|
+
// Generate interactive style block for page-level interactive styles
|
|
217
|
+
const interactiveStyleSection = ctx.collectedInteractiveStyles!.size > 0
|
|
218
|
+
? `\n<style is:global>\n${generateAllInteractiveCSS(ctx.collectedInteractiveStyles!, breakpoints, remConfig, responsiveScales)}\n</style>\n`
|
|
219
|
+
: '';
|
|
220
|
+
|
|
208
221
|
return `---
|
|
209
222
|
${importLines.join('\n')}${extraFrontmatter}
|
|
210
223
|
---
|
|
@@ -220,7 +233,7 @@ ${importLines.join('\n')}${extraFrontmatter}
|
|
|
220
233
|
<div id="root">
|
|
221
234
|
${templateBody} </div>
|
|
222
235
|
</BaseLayout>
|
|
223
|
-
`;
|
|
236
|
+
${interactiveStyleSection}`;
|
|
224
237
|
}
|
|
225
238
|
|
|
226
239
|
/**
|
package/lib/server/index.ts
CHANGED
|
@@ -66,7 +66,7 @@ export { buildStaticPages } from '../../build-static';
|
|
|
66
66
|
export { buildAstroProject } from '../../build-astro';
|
|
67
67
|
|
|
68
68
|
// Webflow export
|
|
69
|
-
export { buildWebflowPayload } from './webflow';
|
|
69
|
+
export { buildWebflowPayload, wrapInWebflowTemplate } from './webflow';
|
|
70
70
|
export type {
|
|
71
71
|
WebflowExportPayload,
|
|
72
72
|
WebflowPage,
|
|
@@ -5,13 +5,10 @@ import {
|
|
|
5
5
|
mapPathToPageName,
|
|
6
6
|
getBreakpointConfig,
|
|
7
7
|
setBreakpointConfig,
|
|
8
|
-
getResponsiveScalesConfig,
|
|
9
|
-
setResponsiveScalesConfig,
|
|
10
8
|
getI18nConfig,
|
|
11
9
|
setI18nConfig,
|
|
12
10
|
} from './jsonLoader';
|
|
13
11
|
import { DEFAULT_BREAKPOINTS } from '../shared/breakpoints';
|
|
14
|
-
import { DEFAULT_RESPONSIVE_SCALES } from '../shared/responsiveScaling';
|
|
15
12
|
import { DEFAULT_I18N_CONFIG } from '../shared/i18n';
|
|
16
13
|
|
|
17
14
|
describe('jsonLoader', () => {
|
|
@@ -76,20 +73,6 @@ describe('jsonLoader', () => {
|
|
|
76
73
|
});
|
|
77
74
|
});
|
|
78
75
|
|
|
79
|
-
describe('responsive scales config cache', () => {
|
|
80
|
-
test('returns default config initially', () => {
|
|
81
|
-
const config = getResponsiveScalesConfig();
|
|
82
|
-
expect(config.enabled).toBe(DEFAULT_RESPONSIVE_SCALES.enabled);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test('returns cached config after set', () => {
|
|
86
|
-
const customConfig = { enabled: true, baseReference: 20 };
|
|
87
|
-
setResponsiveScalesConfig(customConfig);
|
|
88
|
-
const config = getResponsiveScalesConfig();
|
|
89
|
-
expect(config.baseReference).toBe(20);
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
|
|
93
76
|
describe('i18n config cache', () => {
|
|
94
77
|
test('returns default config initially', () => {
|
|
95
78
|
const config = getI18nConfig();
|
package/lib/server/jsonLoader.ts
CHANGED
|
@@ -9,8 +9,6 @@ import type { ComponentDefinition } from '../shared/types';
|
|
|
9
9
|
import type { BreakpointConfig, BreakpointConfigInput, BreakpointEntry } from '../shared/breakpoints';
|
|
10
10
|
import { readTextFile, fileExists } from './runtime';
|
|
11
11
|
import { DEFAULT_BREAKPOINTS, normalizeBreakpointConfig } from '../shared/breakpoints';
|
|
12
|
-
import type { ResponsiveScales, BreakpointScales } from '../shared/responsiveScaling';
|
|
13
|
-
import { DEFAULT_RESPONSIVE_SCALES } from '../shared/responsiveScaling';
|
|
14
12
|
import type { I18nConfig } from '../shared/types/components';
|
|
15
13
|
import { DEFAULT_I18N_CONFIG, migrateI18nConfig } from '../shared/i18n';
|
|
16
14
|
import type { PrefetchConfig } from '../shared/types/prefetch';
|
|
@@ -385,85 +383,6 @@ export function setBreakpointConfig(config: BreakpointConfig): void {
|
|
|
385
383
|
cachedBreakpoints = config;
|
|
386
384
|
}
|
|
387
385
|
|
|
388
|
-
/**
|
|
389
|
-
* Deep merge scale categories, preserving user-defined breakpoints
|
|
390
|
-
* while filling in missing values from defaults
|
|
391
|
-
*/
|
|
392
|
-
function mergeScaleCategory(
|
|
393
|
-
userScales: BreakpointScales | undefined,
|
|
394
|
-
defaultScales: BreakpointScales | undefined
|
|
395
|
-
): BreakpointScales | undefined {
|
|
396
|
-
if (!userScales && !defaultScales) return undefined;
|
|
397
|
-
if (!userScales) return defaultScales ? { ...defaultScales } : undefined;
|
|
398
|
-
if (!defaultScales) return { ...userScales };
|
|
399
|
-
|
|
400
|
-
// User scales take precedence, but include defaults for breakpoints not specified
|
|
401
|
-
return {
|
|
402
|
-
...defaultScales,
|
|
403
|
-
...userScales,
|
|
404
|
-
};
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
/**
|
|
408
|
-
* Load and validate responsive scales configuration from project.config.json
|
|
409
|
-
* Supports dynamic breakpoints - scales are keyed by breakpoint name
|
|
410
|
-
*/
|
|
411
|
-
export async function loadResponsiveScalesConfig(): Promise<ResponsiveScales> {
|
|
412
|
-
try {
|
|
413
|
-
const configContent = await loadJSONFile(projectPaths.config());
|
|
414
|
-
if (configContent) {
|
|
415
|
-
const config = parseJSON<{ responsiveScales?: Partial<ResponsiveScales> }>(configContent);
|
|
416
|
-
|
|
417
|
-
if (config.responsiveScales && typeof config.responsiveScales === 'object') {
|
|
418
|
-
// Deep merge scale categories to preserve user breakpoint definitions
|
|
419
|
-
// while filling in missing values from defaults
|
|
420
|
-
const scales: ResponsiveScales = {
|
|
421
|
-
enabled: config.responsiveScales.enabled ?? DEFAULT_RESPONSIVE_SCALES.enabled,
|
|
422
|
-
baseReference: config.responsiveScales.baseReference ?? DEFAULT_RESPONSIVE_SCALES.baseReference,
|
|
423
|
-
fontSize: mergeScaleCategory(
|
|
424
|
-
config.responsiveScales.fontSize as BreakpointScales | undefined,
|
|
425
|
-
DEFAULT_RESPONSIVE_SCALES.fontSize
|
|
426
|
-
),
|
|
427
|
-
padding: mergeScaleCategory(
|
|
428
|
-
config.responsiveScales.padding as BreakpointScales | undefined,
|
|
429
|
-
DEFAULT_RESPONSIVE_SCALES.padding
|
|
430
|
-
),
|
|
431
|
-
margin: mergeScaleCategory(
|
|
432
|
-
config.responsiveScales.margin as BreakpointScales | undefined,
|
|
433
|
-
DEFAULT_RESPONSIVE_SCALES.margin
|
|
434
|
-
),
|
|
435
|
-
gap: mergeScaleCategory(
|
|
436
|
-
config.responsiveScales.gap as BreakpointScales | undefined,
|
|
437
|
-
DEFAULT_RESPONSIVE_SCALES.gap
|
|
438
|
-
),
|
|
439
|
-
};
|
|
440
|
-
|
|
441
|
-
return scales;
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
} catch (error) {
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
return { ...DEFAULT_RESPONSIVE_SCALES };
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/**
|
|
451
|
-
* Get responsive scales config synchronously (for cases where async is not available)
|
|
452
|
-
* Uses cached value if available, otherwise defaults
|
|
453
|
-
*/
|
|
454
|
-
let cachedResponsiveScales: ResponsiveScales | null = null;
|
|
455
|
-
|
|
456
|
-
export function getResponsiveScalesConfig(): ResponsiveScales {
|
|
457
|
-
if (cachedResponsiveScales) {
|
|
458
|
-
return cachedResponsiveScales;
|
|
459
|
-
}
|
|
460
|
-
return { ...DEFAULT_RESPONSIVE_SCALES };
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
export function setResponsiveScalesConfig(config: ResponsiveScales): void {
|
|
464
|
-
cachedResponsiveScales = config;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
386
|
/**
|
|
468
387
|
* Load and validate i18n configuration from project.config.json
|
|
469
388
|
* Automatically migrates old string[] format to new LocaleConfig[] format
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
import { variableService } from '../../services/VariableService';
|
|
7
7
|
import { generateVariablesCSS } from '../../cssGenerator';
|
|
8
|
-
import { loadBreakpointConfig
|
|
8
|
+
import { loadBreakpointConfig } from '../../jsonLoader';
|
|
9
|
+
import { configService } from '../../services/configService';
|
|
9
10
|
import { jsonResponse } from './shared';
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -22,9 +23,10 @@ export async function handleVariablesStatusRoute(): Promise<Response> {
|
|
|
22
23
|
* Returns generated CSS custom properties from variables.json
|
|
23
24
|
*/
|
|
24
25
|
export async function handleVariablesCSSRoute(): Promise<Response> {
|
|
26
|
+
await configService.load();
|
|
25
27
|
const variablesConfig = await variableService.loadConfig();
|
|
26
28
|
const breakpointConfig = await loadBreakpointConfig();
|
|
27
|
-
const responsiveScalesConfig =
|
|
29
|
+
const responsiveScalesConfig = configService.getResponsiveScales();
|
|
28
30
|
const css = generateVariablesCSS(variablesConfig, breakpointConfig, responsiveScalesConfig);
|
|
29
31
|
return new Response(css, {
|
|
30
32
|
headers: { 'Content-Type': 'text/css' }
|
|
@@ -27,6 +27,7 @@ mock.module('./ssrRenderer', () => ({
|
|
|
27
27
|
const mockConfigService = {
|
|
28
28
|
load: mock(async () => {}),
|
|
29
29
|
getLibraries: mock(() => undefined),
|
|
30
|
+
getResponsiveScales: mock(() => ({ enabled: false, baseReference: 16 })),
|
|
30
31
|
};
|
|
31
32
|
|
|
32
33
|
mock.module('../services/configService', () => ({
|
|
@@ -34,13 +35,11 @@ mock.module('../services/configService', () => ({
|
|
|
34
35
|
}));
|
|
35
36
|
|
|
36
37
|
const mockLoadBreakpointConfig = mock(async () => ({ breakpoints: [] }));
|
|
37
|
-
const mockLoadResponsiveScalesConfig = mock(async () => ({}));
|
|
38
38
|
const mockLoadIconsConfig = mock(async () => ({ favicon: '', appleTouchIcon: '' }));
|
|
39
39
|
const mockLoadPrefetchConfig = mock(async () => ({ enabled: false }));
|
|
40
40
|
|
|
41
41
|
mock.module('../jsonLoader', () => ({
|
|
42
42
|
loadBreakpointConfig: mockLoadBreakpointConfig,
|
|
43
|
-
loadResponsiveScalesConfig: mockLoadResponsiveScalesConfig,
|
|
44
43
|
loadIconsConfig: mockLoadIconsConfig,
|
|
45
44
|
loadPrefetchConfig: mockLoadPrefetchConfig,
|
|
46
45
|
}));
|
|
@@ -976,10 +975,11 @@ describe('generateSSRHTML', () => {
|
|
|
976
975
|
expect(result).toContain('cursor: pointer');
|
|
977
976
|
});
|
|
978
977
|
|
|
979
|
-
test('should include img width/height
|
|
978
|
+
test('should include img max-width/height auto reset', async () => {
|
|
980
979
|
const result = (await generateSSRHTML(minimalPage)) as string;
|
|
981
980
|
expect(result).toContain('img {');
|
|
982
|
-
expect(result).toContain('width: 100%');
|
|
981
|
+
expect(result).toContain('max-width: 100%');
|
|
982
|
+
expect(result).toContain('height: auto');
|
|
983
983
|
});
|
|
984
984
|
|
|
985
985
|
test('should include olink and oem utility classes', async () => {
|
|
@@ -8,7 +8,7 @@ import type { ComponentDefinition, JSONPage, PageLibrariesConfig, CustomCodeConf
|
|
|
8
8
|
import type { SlugMap } from '../../shared/slugTranslator';
|
|
9
9
|
import type { CMSService } from '../services/cmsService';
|
|
10
10
|
import { configService } from '../services/configService';
|
|
11
|
-
import { loadBreakpointConfig,
|
|
11
|
+
import { loadBreakpointConfig, loadIconsConfig, loadPrefetchConfig } from '../jsonLoader';
|
|
12
12
|
import { generateFontCSS, generateFontPreloadTags, loadProjectConfig } from '../../shared/fontLoader';
|
|
13
13
|
import { colorService } from '../services/ColorService';
|
|
14
14
|
import { generateThemeColorVariablesCSS, generateVariablesCSS } from '../cssGenerator';
|
|
@@ -378,7 +378,7 @@ export async function generateSSRHTML(
|
|
|
378
378
|
// Extract and generate utility CSS from rendered HTML
|
|
379
379
|
const usedUtilityClasses = extractUtilityClassesFromHTML(rendered.html);
|
|
380
380
|
const breakpointConfig = await loadBreakpointConfig();
|
|
381
|
-
const responsiveScalesConfig =
|
|
381
|
+
const responsiveScalesConfig = configService.getResponsiveScales();
|
|
382
382
|
|
|
383
383
|
// Load and generate CSS custom properties from variables.json
|
|
384
384
|
const variablesConfig = await variableService.loadConfig();
|
|
@@ -411,12 +411,17 @@ button {
|
|
|
411
411
|
cursor: pointer;
|
|
412
412
|
outline: inherit;
|
|
413
413
|
}
|
|
414
|
+
img {
|
|
415
|
+
max-width: 100%;
|
|
416
|
+
height: auto;
|
|
417
|
+
}
|
|
414
418
|
picture {
|
|
415
419
|
display: block;
|
|
416
420
|
}
|
|
417
421
|
.olink {
|
|
418
422
|
text-decoration: none;
|
|
419
423
|
display: block;
|
|
424
|
+
color: inherit;
|
|
420
425
|
}
|
|
421
426
|
.oem {
|
|
422
427
|
display: inline-block;
|
|
@@ -88,15 +88,21 @@ export async function buildImageMetadataMap(): Promise<ImageMetadataMap> {
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
// Add main image as largest option (use original width or 2400 as fallback)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
91
|
+
// Add main image as largest option (use original width or 2400 as fallback).
|
|
92
|
+
// Only do this when at least one responsive variant exists — a single-entry
|
|
93
|
+
// srcset paired with sizes="100vw" makes the browser scale tiny images
|
|
94
|
+
// (e.g. 24×24 icons) up to full viewport width.
|
|
95
|
+
const hasVariants = entry.variants.length > 0;
|
|
96
|
+
if (hasVariants) {
|
|
97
|
+
const mainWidth = entry.width || 2400;
|
|
98
|
+
if (entry.formats.webp) {
|
|
99
|
+
webpSrcsetParts.push(`/images/${encodeURIComponent(`${baseName}.webp`)} ${mainWidth}w`);
|
|
100
|
+
} else {
|
|
101
|
+
webpSrcsetParts.push(`/images/${encodeURIComponent(filename)} ${mainWidth}w`);
|
|
102
|
+
}
|
|
103
|
+
if (entry.formats.avif) {
|
|
104
|
+
avifSrcsetParts.push(`/images/${encodeURIComponent(`${baseName}.avif`)} ${mainWidth}w`);
|
|
105
|
+
}
|
|
100
106
|
}
|
|
101
107
|
|
|
102
108
|
const metadata: ImageMetadata = {
|
|
@@ -31,7 +31,6 @@ mock.module('../jsonLoader', () => ({
|
|
|
31
31
|
],
|
|
32
32
|
}),
|
|
33
33
|
loadIconsConfig: async () => ({ favicon: '', appleTouchIcon: '' }),
|
|
34
|
-
loadResponsiveScalesConfig: async () => ({}),
|
|
35
34
|
loadPrefetchConfig: async () => ({ enabled: false }),
|
|
36
35
|
}));
|
|
37
36
|
|
|
@@ -717,6 +716,20 @@ describe('ssrRenderer', () => {
|
|
|
717
716
|
expect(html).toBe('');
|
|
718
717
|
});
|
|
719
718
|
|
|
719
|
+
test('skips node with unresolved string-template if', async () => {
|
|
720
|
+
// {{buttonText}} stays unresolved when the referenced prop is undefined
|
|
721
|
+
// (evaluateTemplate's backward-compat fallback returns the original template).
|
|
722
|
+
// Treat unresolved templates as falsy so the node is hidden.
|
|
723
|
+
const node = {
|
|
724
|
+
type: 'link',
|
|
725
|
+
href: '/page',
|
|
726
|
+
children: 'link',
|
|
727
|
+
if: '{{buttonText}}',
|
|
728
|
+
};
|
|
729
|
+
const html = await render(node);
|
|
730
|
+
expect(html).toBe('');
|
|
731
|
+
});
|
|
732
|
+
|
|
720
733
|
test('evaluates boolean mapping if condition - renders when mapped to true', async () => {
|
|
721
734
|
// Component renders within buildComponentHTML with componentResolvedProps
|
|
722
735
|
// We need to use a component that sets up the mapping context
|
|
@@ -1478,6 +1491,95 @@ describe('ssrRenderer', () => {
|
|
|
1478
1491
|
expect(result.html).toContain('Submit');
|
|
1479
1492
|
});
|
|
1480
1493
|
|
|
1494
|
+
test('applies instance styles when component root is a LinkNode', async () => {
|
|
1495
|
+
// Mirrors the figmaexport Button: component structure rooted in a LinkNode.
|
|
1496
|
+
// Instance style overrides must be merged onto the rendered <a>.
|
|
1497
|
+
const buttonDef: ComponentDefinition = {
|
|
1498
|
+
component: {
|
|
1499
|
+
structure: {
|
|
1500
|
+
type: 'link',
|
|
1501
|
+
href: '{{link}}',
|
|
1502
|
+
style: { base: { backgroundColor: '#ffffff', color: '#000000' } },
|
|
1503
|
+
children: ['{{text}}'],
|
|
1504
|
+
},
|
|
1505
|
+
interface: {
|
|
1506
|
+
text: { type: 'string', default: 'Buy Tickets' },
|
|
1507
|
+
link: { type: 'link', default: { href: '/tickets' } },
|
|
1508
|
+
},
|
|
1509
|
+
acceptsStyles: true,
|
|
1510
|
+
} as any,
|
|
1511
|
+
};
|
|
1512
|
+
|
|
1513
|
+
const pageData: JSONPage = {
|
|
1514
|
+
root: {
|
|
1515
|
+
type: 'node',
|
|
1516
|
+
tag: 'div',
|
|
1517
|
+
children: [
|
|
1518
|
+
{
|
|
1519
|
+
type: 'component',
|
|
1520
|
+
component: 'Button',
|
|
1521
|
+
props: { text: 'Partner with us', link: '/partner' },
|
|
1522
|
+
style: {
|
|
1523
|
+
base: { backgroundColor: '#252525', color: '#ffffff' },
|
|
1524
|
+
tablet: {},
|
|
1525
|
+
mobile: {},
|
|
1526
|
+
},
|
|
1527
|
+
},
|
|
1528
|
+
],
|
|
1529
|
+
} as any,
|
|
1530
|
+
};
|
|
1531
|
+
|
|
1532
|
+
const result = await renderPageSSR(pageData, { Button: buttonDef });
|
|
1533
|
+
// The <a> should exist with the component's default class plus the instance override classes.
|
|
1534
|
+
expect(result.html).toMatch(/<a\s+href="\/partner"[^>]*class="[^"]*olink[^"]*"/);
|
|
1535
|
+
// Instance style for backgroundColor "#252525" produces the bgc-#252525 utility class.
|
|
1536
|
+
expect(result.html).toContain('bgc-#252525');
|
|
1537
|
+
// Instance style for color "#ffffff" (the override) produces c-#ffffff.
|
|
1538
|
+
expect(result.html).toContain('c-#ffffff');
|
|
1539
|
+
});
|
|
1540
|
+
|
|
1541
|
+
test('instance style overrides structure style on the same property', async () => {
|
|
1542
|
+
// Regression: previously the structure's mb-0 and the instance's mb-32px both
|
|
1543
|
+
// ended up as same-specificity utility classes on the rendered element, and
|
|
1544
|
+
// the structure's class won by source order — opposite of client behavior.
|
|
1545
|
+
// Instance styles must replace structure styles per-property at merge time.
|
|
1546
|
+
const buttonDef: ComponentDefinition = {
|
|
1547
|
+
component: {
|
|
1548
|
+
structure: {
|
|
1549
|
+
type: 'node',
|
|
1550
|
+
tag: 'button',
|
|
1551
|
+
style: { base: { marginBottom: 0, padding: '8px' } },
|
|
1552
|
+
children: ['{{text}}'],
|
|
1553
|
+
},
|
|
1554
|
+
interface: {
|
|
1555
|
+
text: { type: 'string', default: 'Click' },
|
|
1556
|
+
},
|
|
1557
|
+
acceptsStyles: true,
|
|
1558
|
+
} as any,
|
|
1559
|
+
};
|
|
1560
|
+
|
|
1561
|
+
const pageData: JSONPage = {
|
|
1562
|
+
root: {
|
|
1563
|
+
type: 'node',
|
|
1564
|
+
tag: 'div',
|
|
1565
|
+
children: [
|
|
1566
|
+
{
|
|
1567
|
+
type: 'component',
|
|
1568
|
+
component: 'Button',
|
|
1569
|
+
props: { text: 'Hi' },
|
|
1570
|
+
style: { base: { marginBottom: '32px' } },
|
|
1571
|
+
},
|
|
1572
|
+
],
|
|
1573
|
+
} as any,
|
|
1574
|
+
};
|
|
1575
|
+
|
|
1576
|
+
const result = await renderPageSSR(pageData, { Button: buttonDef });
|
|
1577
|
+
expect(result.html).toContain('mb-32px');
|
|
1578
|
+
expect(result.html).not.toContain('mb-0');
|
|
1579
|
+
// Untouched structure properties remain.
|
|
1580
|
+
expect(result.html).toContain('p-8px');
|
|
1581
|
+
});
|
|
1582
|
+
|
|
1481
1583
|
test('extracts locale from path when not explicitly provided', async () => {
|
|
1482
1584
|
const i18nConfig = {
|
|
1483
1585
|
defaultLocale: 'en',
|