meno-core 1.0.45 → 1.0.46
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 +214 -63
- package/dist/bin/cli.js +2 -2
- package/dist/build-static.js +7 -7
- package/dist/chunks/{chunk-NZTSJS5C.js → chunk-2QK6U5UK.js} +3 -2
- package/dist/chunks/{chunk-NZTSJS5C.js.map → chunk-2QK6U5UK.js.map} +2 -2
- package/dist/chunks/{chunk-BZQKEJQY.js → chunk-77ZB6353.js} +29 -18
- package/dist/chunks/chunk-77ZB6353.js.map +7 -0
- package/dist/chunks/{chunk-TVH3TC2T.js → chunk-C6U5T5S5.js} +6 -6
- package/dist/chunks/{chunk-5ZASE4IG.js → chunk-FED5MME6.js} +234 -11
- package/dist/chunks/{chunk-5ZASE4IG.js.map → chunk-FED5MME6.js.map} +3 -3
- package/dist/chunks/{chunk-5Z5VQRTJ.js → chunk-I7YIGZXT.js} +4 -4
- package/dist/chunks/{chunk-5Z5VQRTJ.js.map → chunk-I7YIGZXT.js.map} +2 -2
- package/dist/chunks/{chunk-OUNJ76QM.js → chunk-ORN7S4AP.js} +5 -5
- package/dist/chunks/{chunk-GYF3ABI3.js → chunk-UUA5LEWF.js} +3 -3
- package/dist/chunks/{chunk-GYF3ABI3.js.map → chunk-UUA5LEWF.js.map} +2 -2
- package/dist/chunks/{chunk-WQSG5WHC.js → chunk-ZTKHJQ2Z.js} +2 -2
- package/dist/chunks/{chunk-F7MA62WG.js → chunk-ZWYDT3QJ.js} +3 -3
- package/dist/chunks/{configService-6KTT6GRT.js → configService-DYCUEURL.js} +3 -3
- package/dist/chunks/{constants-L5IKLB6U.js → constants-GWBAD66U.js} +2 -2
- package/dist/entries/server-router.js +7 -7
- package/dist/lib/client/index.js +4 -4
- package/dist/lib/server/index.js +586 -142
- package/dist/lib/server/index.js.map +3 -3
- package/dist/lib/shared/index.js +7 -3
- package/dist/lib/shared/index.js.map +2 -2
- package/dist/lib/test-utils/index.js +1 -1
- package/lib/client/templateEngine.test.ts +64 -0
- package/lib/server/astro/astroEmitHelpers.ts +18 -0
- package/lib/server/astro/cmsPageEmitter.ts +31 -1
- package/lib/server/astro/componentEmitter.test.ts +59 -0
- package/lib/server/astro/componentEmitter.ts +43 -10
- package/lib/server/astro/cssCollector.ts +58 -11
- package/lib/server/astro/nodeToAstro.test.ts +397 -5
- package/lib/server/astro/nodeToAstro.ts +478 -63
- package/lib/server/astro/pageEmitter.ts +31 -1
- package/lib/server/astro/tailwindMapper.test.ts +119 -0
- package/lib/server/astro/tailwindMapper.ts +67 -1
- package/lib/server/runtime/httpServer.ts +12 -4
- package/lib/server/ssr/htmlGenerator.ts +1 -1
- package/lib/server/ssr/jsCollector.ts +2 -2
- package/lib/server/ssr/ssrRenderer.test.ts +32 -0
- package/lib/server/ssr/ssrRenderer.ts +26 -11
- package/lib/shared/constants.ts +1 -0
- package/lib/shared/cssGeneration.test.ts +109 -3
- package/lib/shared/cssGeneration.ts +98 -13
- package/lib/shared/cssNamedColors.ts +47 -0
- package/lib/shared/cssProperties.ts +2 -2
- package/lib/shared/index.ts +1 -0
- package/package.json +1 -1
- package/dist/chunks/chunk-BZQKEJQY.js.map +0 -7
- /package/dist/chunks/{chunk-TVH3TC2T.js.map → chunk-C6U5T5S5.js.map} +0 -0
- /package/dist/chunks/{chunk-OUNJ76QM.js.map → chunk-ORN7S4AP.js.map} +0 -0
- /package/dist/chunks/{chunk-WQSG5WHC.js.map → chunk-ZTKHJQ2Z.js.map} +0 -0
- /package/dist/chunks/{chunk-F7MA62WG.js.map → chunk-ZWYDT3QJ.js.map} +0 -0
- /package/dist/chunks/{configService-6KTT6GRT.js.map → configService-DYCUEURL.js.map} +0 -0
- /package/dist/chunks/{constants-L5IKLB6U.js.map → constants-GWBAD66U.js.map} +0 -0
|
@@ -9,6 +9,7 @@ import { DEFAULT_BREAKPOINTS } from '../../shared/breakpoints';
|
|
|
9
9
|
import { nodeToAstro, type AstroEmitContext } from './nodeToAstro';
|
|
10
10
|
import type { ImageMetadataMap } from '../ssr/imageMetadata';
|
|
11
11
|
import type { SlugMap } from '../../shared/slugTranslator';
|
|
12
|
+
import type { ResponsiveScales } from '../../shared/responsiveScaling';
|
|
12
13
|
|
|
13
14
|
// ---------------------------------------------------------------------------
|
|
14
15
|
// Types
|
|
@@ -43,6 +44,8 @@ export interface PageEmitOptions {
|
|
|
43
44
|
pageName: string;
|
|
44
45
|
/** Breakpoint config for responsive Tailwind classes */
|
|
45
46
|
breakpoints?: BreakpointConfig;
|
|
47
|
+
/** Responsive scales config for auto-scaling at breakpoints */
|
|
48
|
+
responsiveScales?: ResponsiveScales;
|
|
46
49
|
/** Image metadata map for responsive image generation */
|
|
47
50
|
imageMetadataMap?: ImageMetadataMap;
|
|
48
51
|
/** I18n config for locale list rendering */
|
|
@@ -53,6 +56,12 @@ export interface PageEmitOptions {
|
|
|
53
56
|
slugMappings?: SlugMap[];
|
|
54
57
|
/** Image format: 'webp' uses plain <img>, 'avif' uses <picture> */
|
|
55
58
|
imageFormat?: 'webp' | 'avif';
|
|
59
|
+
/**
|
|
60
|
+
* Raw-HTML slice → processed HTML captured during the page's SSR pass.
|
|
61
|
+
* Ensures `<Fragment set:html>` matches SSR output for rich-text content
|
|
62
|
+
* (image rewriting, component expansion, link localization).
|
|
63
|
+
*/
|
|
64
|
+
processedRawHtml?: Map<string, string>;
|
|
56
65
|
}
|
|
57
66
|
|
|
58
67
|
// ---------------------------------------------------------------------------
|
|
@@ -60,7 +69,12 @@ export interface PageEmitOptions {
|
|
|
60
69
|
// ---------------------------------------------------------------------------
|
|
61
70
|
|
|
62
71
|
function escapeTemplateLiteral(s: string): string {
|
|
63
|
-
return s
|
|
72
|
+
return s
|
|
73
|
+
.replace(/\\/g, '\\\\')
|
|
74
|
+
.replace(/`/g, '\\`')
|
|
75
|
+
.replace(/\$\{/g, '\\${')
|
|
76
|
+
.replace(/\u2028/g, '\\u2028')
|
|
77
|
+
.replace(/\u2029/g, '\\u2029');
|
|
64
78
|
}
|
|
65
79
|
|
|
66
80
|
function escapeJSX(s: string): string {
|
|
@@ -100,11 +114,13 @@ export function emitAstroPage(options: PageEmitOptions): string {
|
|
|
100
114
|
ssrFallbacks,
|
|
101
115
|
pageName,
|
|
102
116
|
breakpoints: breakpointsOpt,
|
|
117
|
+
responsiveScales,
|
|
103
118
|
imageMetadataMap,
|
|
104
119
|
i18nConfig,
|
|
105
120
|
currentPageSlugMap,
|
|
106
121
|
slugMappings,
|
|
107
122
|
imageFormat,
|
|
123
|
+
processedRawHtml,
|
|
108
124
|
} = options;
|
|
109
125
|
|
|
110
126
|
const breakpoints = breakpointsOpt ?? DEFAULT_BREAKPOINTS;
|
|
@@ -127,6 +143,7 @@ export function emitAstroPage(options: PageEmitOptions): string {
|
|
|
127
143
|
fileType: 'page',
|
|
128
144
|
fileName: pageName,
|
|
129
145
|
breakpoints,
|
|
146
|
+
responsiveScales,
|
|
130
147
|
imageMetadataMap,
|
|
131
148
|
locale,
|
|
132
149
|
i18nConfig,
|
|
@@ -136,6 +153,9 @@ export function emitAstroPage(options: PageEmitOptions): string {
|
|
|
136
153
|
slugMappings,
|
|
137
154
|
i18nDefaultLocale: i18nConfig?.defaultLocale,
|
|
138
155
|
imageFormat,
|
|
156
|
+
processedRawHtml,
|
|
157
|
+
imageImports: new Map<string, string>(),
|
|
158
|
+
fileDepth,
|
|
139
159
|
};
|
|
140
160
|
|
|
141
161
|
// Emit the template body
|
|
@@ -152,6 +172,16 @@ export function emitAstroPage(options: PageEmitOptions): string {
|
|
|
152
172
|
|
|
153
173
|
importLines.push(`import BaseLayout from '${layoutImportPath}';`);
|
|
154
174
|
|
|
175
|
+
// Static image imports (astro:assets <Picture>). Map size is the single
|
|
176
|
+
// source of truth — a separate boolean would be lost by child-ctx spreads.
|
|
177
|
+
if (ctx.imageImports && ctx.imageImports.size > 0) {
|
|
178
|
+
importLines.push(`import { Picture } from 'astro:assets';`);
|
|
179
|
+
const sortedImages = Array.from(ctx.imageImports.entries()).sort(([a], [b]) => a.localeCompare(b));
|
|
180
|
+
for (const [varName, importPath] of sortedImages) {
|
|
181
|
+
importLines.push(`import ${varName} from '${importPath}';`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
155
185
|
// Sort component imports alphabetically
|
|
156
186
|
const componentImports = Array.from(ctx.imports).sort();
|
|
157
187
|
for (const comp of componentImports) {
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for tailwindMapper.ts — specifically the auto-responsive scaling
|
|
3
|
+
* pass in `responsiveStylesToTailwind`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, test, expect } from 'bun:test';
|
|
7
|
+
import { responsiveStylesToTailwind } from './tailwindMapper';
|
|
8
|
+
import { DEFAULT_BREAKPOINTS } from '../../shared/breakpoints';
|
|
9
|
+
import type { ResponsiveScales } from '../../shared/responsiveScaling';
|
|
10
|
+
|
|
11
|
+
const scalesEnabled: ResponsiveScales = {
|
|
12
|
+
enabled: true,
|
|
13
|
+
baseReference: 16,
|
|
14
|
+
padding: { tablet: 0.75, mobile: 0.5 },
|
|
15
|
+
fontSize: { tablet: 0.88, mobile: 0.75 },
|
|
16
|
+
gap: { tablet: 0.65, mobile: 0.4 },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const scalesDisabled: ResponsiveScales = {
|
|
20
|
+
...scalesEnabled,
|
|
21
|
+
enabled: false,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
describe('responsiveStylesToTailwind — auto-responsive scaling', () => {
|
|
25
|
+
test('emits scaled max-[Npx]: variants for base scalable property', () => {
|
|
26
|
+
const { classes } = responsiveStylesToTailwind(
|
|
27
|
+
{ base: { padding: '40px' } },
|
|
28
|
+
DEFAULT_BREAKPOINTS,
|
|
29
|
+
scalesEnabled
|
|
30
|
+
);
|
|
31
|
+
// base: 40px; tablet: 40 + (40-16)*(0.75-1) = 34px; mobile: 40 + (40-16)*(0.5-1) = 28px
|
|
32
|
+
expect(classes).toEqual([
|
|
33
|
+
'p-[40px]',
|
|
34
|
+
'max-[1024px]:p-[34px]',
|
|
35
|
+
'max-[540px]:p-[28px]',
|
|
36
|
+
]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('explicit breakpoint override wins — no auto-scaled variant for that breakpoint/property pair', () => {
|
|
40
|
+
const { classes } = responsiveStylesToTailwind(
|
|
41
|
+
{ base: { padding: '40px' }, tablet: { padding: '30px' } },
|
|
42
|
+
DEFAULT_BREAKPOINTS,
|
|
43
|
+
scalesEnabled
|
|
44
|
+
);
|
|
45
|
+
expect(classes).toContain('p-[40px]');
|
|
46
|
+
expect(classes).toContain('max-[1024px]:p-[30px]');
|
|
47
|
+
// no max-[1024px]:p-[34px] auto-scaled variant
|
|
48
|
+
expect(classes).not.toContain('max-[1024px]:p-[34px]');
|
|
49
|
+
// mobile still gets auto-scaled from base since it has no explicit override
|
|
50
|
+
expect(classes).toContain('max-[540px]:p-[28px]');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('scaling disabled → output identical to no-scaling path', () => {
|
|
54
|
+
const withScales = responsiveStylesToTailwind(
|
|
55
|
+
{ base: { padding: '40px' } },
|
|
56
|
+
DEFAULT_BREAKPOINTS,
|
|
57
|
+
scalesDisabled
|
|
58
|
+
);
|
|
59
|
+
const withoutScales = responsiveStylesToTailwind(
|
|
60
|
+
{ base: { padding: '40px' } },
|
|
61
|
+
DEFAULT_BREAKPOINTS
|
|
62
|
+
);
|
|
63
|
+
expect(withScales.classes).toEqual(withoutScales.classes);
|
|
64
|
+
expect(withScales.classes).toEqual(['p-[40px]']);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('values at or below baseReference produce no scaled variants', () => {
|
|
68
|
+
const { classes } = responsiveStylesToTailwind(
|
|
69
|
+
{ base: { padding: '8px' } }, // 8 < 16 → stays unchanged → skipped
|
|
70
|
+
DEFAULT_BREAKPOINTS,
|
|
71
|
+
scalesEnabled
|
|
72
|
+
);
|
|
73
|
+
expect(classes).toEqual(['p-[8px]']);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('non-scalable property (e.g. color) is left untouched', () => {
|
|
77
|
+
const { classes } = responsiveStylesToTailwind(
|
|
78
|
+
{ base: { color: '#abc' } },
|
|
79
|
+
DEFAULT_BREAKPOINTS,
|
|
80
|
+
scalesEnabled
|
|
81
|
+
);
|
|
82
|
+
// Just the base color class — no max-[Npx] prefix because color isn't in any scale category
|
|
83
|
+
expect(classes.filter((c) => c.startsWith('max-['))).toEqual([]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('multi-value properties scale every numeric part', () => {
|
|
87
|
+
const { classes } = responsiveStylesToTailwind(
|
|
88
|
+
{ base: { padding: '40px 80px' } },
|
|
89
|
+
DEFAULT_BREAKPOINTS,
|
|
90
|
+
scalesEnabled
|
|
91
|
+
);
|
|
92
|
+
// tablet: 40→34, 80→64; mobile: 40→28, 80→48
|
|
93
|
+
expect(classes).toContain('max-[1024px]:p-[34px_64px]');
|
|
94
|
+
expect(classes).toContain('max-[540px]:p-[28px_48px]');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('flat style object (no `base`) gets no auto-scaling', () => {
|
|
98
|
+
const { classes } = responsiveStylesToTailwind(
|
|
99
|
+
{ padding: '40px' },
|
|
100
|
+
DEFAULT_BREAKPOINTS,
|
|
101
|
+
scalesEnabled
|
|
102
|
+
);
|
|
103
|
+
// A flat style is treated as base with no breakpoints — nothing to
|
|
104
|
+
// auto-scale *into*. The scaling pass only runs on the responsive branch.
|
|
105
|
+
expect(classes.filter((c) => c.startsWith('max-['))).toEqual([]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('fontSize scales using the fontSize category', () => {
|
|
109
|
+
const { classes } = responsiveStylesToTailwind(
|
|
110
|
+
{ base: { fontSize: '100px' } },
|
|
111
|
+
DEFAULT_BREAKPOINTS,
|
|
112
|
+
scalesEnabled
|
|
113
|
+
);
|
|
114
|
+
// tablet: 100 + (100-16)*(0.88-1) = 100 - 10.08 = round(89.92) = 90
|
|
115
|
+
// mobile: 100 + (100-16)*(0.75-1) = 100 - 21 = 79
|
|
116
|
+
expect(classes).toContain('max-[1024px]:text-[length:90px]');
|
|
117
|
+
expect(classes).toContain('max-[540px]:text-[length:79px]');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -10,6 +10,8 @@ import type {
|
|
|
10
10
|
StyleMapping,
|
|
11
11
|
} from '../../shared/types/styles';
|
|
12
12
|
import type { BreakpointConfig } from '../../shared/breakpoints';
|
|
13
|
+
import type { ResponsiveScales, CSSPropertyType } from '../../shared/responsiveScaling';
|
|
14
|
+
import { getScaleMultiplier, scalePropertyValue } from '../../shared/responsiveScaling';
|
|
13
15
|
|
|
14
16
|
// ---------------------------------------------------------------------------
|
|
15
17
|
// Helpers
|
|
@@ -551,7 +553,8 @@ export function stylesToTailwind(
|
|
|
551
553
|
*/
|
|
552
554
|
export function responsiveStylesToTailwind(
|
|
553
555
|
style: StyleObject | ResponsiveStyleObject | null | undefined,
|
|
554
|
-
breakpoints: BreakpointConfig
|
|
556
|
+
breakpoints: BreakpointConfig,
|
|
557
|
+
responsiveScales?: ResponsiveScales
|
|
555
558
|
): { classes: string[]; dynamicStyles: Record<string, string> } {
|
|
556
559
|
if (!style) return { classes: [], dynamicStyles: {} };
|
|
557
560
|
|
|
@@ -597,6 +600,13 @@ export function responsiveStylesToTailwind(
|
|
|
597
600
|
allClasses.push(...classes.map(cls => `${prefix}${cls}`));
|
|
598
601
|
Object.assign(allDynamicStyles, dynamicStyles);
|
|
599
602
|
}
|
|
603
|
+
|
|
604
|
+
// Auto-responsive scaling: for each scalable base property, emit scaled
|
|
605
|
+
// max-[Npx]: variants for every enabled breakpoint that doesn't already
|
|
606
|
+
// have an explicit override for that property.
|
|
607
|
+
if (responsiveScales?.enabled === true && responsive.base) {
|
|
608
|
+
appendAutoScaledClasses(responsive, breakpoints, responsiveScales, allClasses);
|
|
609
|
+
}
|
|
600
610
|
} else {
|
|
601
611
|
// Flat style object — treat as base
|
|
602
612
|
const { classes, dynamicStyles } = stylesToTailwind(style as StyleObject);
|
|
@@ -606,3 +616,59 @@ export function responsiveStylesToTailwind(
|
|
|
606
616
|
|
|
607
617
|
return { classes: allClasses, dynamicStyles: allDynamicStyles };
|
|
608
618
|
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Emit `max-[Npx]:` class variants with pre-scaled values for every base
|
|
622
|
+
* property in a scale category, skipping breakpoints where the author set
|
|
623
|
+
* an explicit override for the same property.
|
|
624
|
+
*
|
|
625
|
+
* Breakpoints are processed in descending pixel order so the emitted class
|
|
626
|
+
* order matches the cascade order Tailwind's variant compiler expects.
|
|
627
|
+
*/
|
|
628
|
+
function appendAutoScaledClasses(
|
|
629
|
+
responsive: ResponsiveStyleObject,
|
|
630
|
+
breakpoints: BreakpointConfig,
|
|
631
|
+
responsiveScales: ResponsiveScales,
|
|
632
|
+
out: string[]
|
|
633
|
+
): void {
|
|
634
|
+
const base = responsive.base;
|
|
635
|
+
if (!base) return;
|
|
636
|
+
|
|
637
|
+
const baseRef = responsiveScales.baseReference ?? 16;
|
|
638
|
+
|
|
639
|
+
const sortedBps = Object.entries(breakpoints)
|
|
640
|
+
.map(([name, cfg]) => ({ name, value: cfg?.breakpoint }))
|
|
641
|
+
.filter((bp): bp is { name: string; value: number } =>
|
|
642
|
+
typeof bp.value === 'number' && bp.value > 0
|
|
643
|
+
)
|
|
644
|
+
.sort((a, b) => b.value - a.value);
|
|
645
|
+
|
|
646
|
+
for (const [property, value] of Object.entries(base)) {
|
|
647
|
+
if (isStyleMapping(value)) continue;
|
|
648
|
+
if (value == null) continue;
|
|
649
|
+
|
|
650
|
+
const strValue = String(value);
|
|
651
|
+
if (strValue === '' || hasTemplateExpression(strValue)) continue;
|
|
652
|
+
|
|
653
|
+
for (const { name: bpName, value: bpPixels } of sortedBps) {
|
|
654
|
+
// Explicit override at this breakpoint wins — skip auto-scaling.
|
|
655
|
+
const bpBranch = responsive[bpName] as StyleObject | undefined;
|
|
656
|
+
if (bpBranch && property in bpBranch) continue;
|
|
657
|
+
|
|
658
|
+
const scale = getScaleMultiplier(
|
|
659
|
+
responsiveScales,
|
|
660
|
+
property as CSSPropertyType,
|
|
661
|
+
bpName
|
|
662
|
+
);
|
|
663
|
+
if (scale == null) continue;
|
|
664
|
+
|
|
665
|
+
const scaledValue = scalePropertyValue(strValue, baseRef, scale);
|
|
666
|
+
if (scaledValue == null || scaledValue === strValue) continue;
|
|
667
|
+
|
|
668
|
+
const scaledClass = propertyToTailwind(property, scaledValue);
|
|
669
|
+
if (!scaledClass) continue;
|
|
670
|
+
|
|
671
|
+
out.push(`max-[${bpPixels}px]:${scaledClass}`);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
@@ -7,7 +7,12 @@ const isBun = typeof globalThis.Bun !== 'undefined';
|
|
|
7
7
|
|
|
8
8
|
export interface RuntimeServer {
|
|
9
9
|
port: number;
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Stop the server. When `force` is true, active connections are
|
|
12
|
+
* closed immediately (Bun: closeActiveConnections=true). Defaults to
|
|
13
|
+
* a graceful stop that lets in-flight requests finish.
|
|
14
|
+
*/
|
|
15
|
+
stop(force?: boolean): void;
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
export interface RuntimeWSClient {
|
|
@@ -76,8 +81,8 @@ function createBunServer(options: ServerOptions): RuntimeServer {
|
|
|
76
81
|
|
|
77
82
|
return {
|
|
78
83
|
port: server.port ?? port,
|
|
79
|
-
stop() {
|
|
80
|
-
server.stop();
|
|
84
|
+
stop(force?: boolean) {
|
|
85
|
+
server.stop(force === true);
|
|
81
86
|
},
|
|
82
87
|
};
|
|
83
88
|
}
|
|
@@ -219,7 +224,10 @@ async function createNodeServer(options: ServerOptions): Promise<RuntimeServer>
|
|
|
219
224
|
|
|
220
225
|
resolve({
|
|
221
226
|
port: actualPort,
|
|
222
|
-
stop() {
|
|
227
|
+
stop(_force?: boolean) {
|
|
228
|
+
// Node http.Server.close is graceful by default; the force
|
|
229
|
+
// flag is accepted for API parity with Bun but currently has
|
|
230
|
+
// no additional effect on the Node path.
|
|
223
231
|
server.close();
|
|
224
232
|
},
|
|
225
233
|
});
|
|
@@ -388,7 +388,7 @@ export async function generateSSRHTML(
|
|
|
388
388
|
|
|
389
389
|
// Generate interactive styles CSS from map collected during render
|
|
390
390
|
const interactiveCSS = rendered.interactiveStylesMap.size > 0
|
|
391
|
-
? generateAllInteractiveCSS(rendered.interactiveStylesMap, breakpointConfig, remConversionConfig)
|
|
391
|
+
? generateAllInteractiveCSS(rendered.interactiveStylesMap, breakpointConfig, remConversionConfig, responsiveScalesConfig)
|
|
392
392
|
: '';
|
|
393
393
|
|
|
394
394
|
// Print warnings for any unmapped styles found during build
|
|
@@ -128,8 +128,8 @@ __meno.initComponent("${name}", function(el, props) {
|
|
|
128
128
|
${js}
|
|
129
129
|
});`);
|
|
130
130
|
} else {
|
|
131
|
-
// No defineVars -
|
|
132
|
-
jsCodeBlocks.push(`// Component: ${name}\n${js}
|
|
131
|
+
// No defineVars - wrap in IIFE to prevent variable conflicts between components
|
|
132
|
+
jsCodeBlocks.push(`// Component: ${name}\n(function(){\n${js}\n})();`);
|
|
133
133
|
}
|
|
134
134
|
};
|
|
135
135
|
|
|
@@ -1891,6 +1891,38 @@ describe('ssrRenderer', () => {
|
|
|
1891
1891
|
const html = await render(node);
|
|
1892
1892
|
expect(html).toContain('<p>Rich <strong>text</strong></p>');
|
|
1893
1893
|
});
|
|
1894
|
+
|
|
1895
|
+
test('exposes processedRawHtmlCollector mapping raw slice → processed HTML', async () => {
|
|
1896
|
+
const { RAW_HTML_PREFIX } = await import('../../shared/constants');
|
|
1897
|
+
const rawSlice = '<p>Rich <strong>text</strong></p>';
|
|
1898
|
+
const node = {
|
|
1899
|
+
type: 'node',
|
|
1900
|
+
tag: 'div',
|
|
1901
|
+
children: `${RAW_HTML_PREFIX}${rawSlice}`,
|
|
1902
|
+
};
|
|
1903
|
+
const result = await buildComponentHTML(node as any);
|
|
1904
|
+
expect(result.processedRawHtmlCollector).toBeInstanceOf(Map);
|
|
1905
|
+
expect(result.processedRawHtmlCollector.get(rawSlice)).toBe(rawSlice);
|
|
1906
|
+
});
|
|
1907
|
+
|
|
1908
|
+
test('collector records localized links for non-default locale rich text', async () => {
|
|
1909
|
+
const { RAW_HTML_PREFIX } = await import('../../shared/constants');
|
|
1910
|
+
const rawSlice = '<p>See <a href="/about">story</a></p>';
|
|
1911
|
+
const node = {
|
|
1912
|
+
type: 'node',
|
|
1913
|
+
tag: 'div',
|
|
1914
|
+
children: `${RAW_HTML_PREFIX}${rawSlice}`,
|
|
1915
|
+
};
|
|
1916
|
+
const result = await buildComponentHTML(
|
|
1917
|
+
node as any,
|
|
1918
|
+
{},
|
|
1919
|
+
{},
|
|
1920
|
+
'fr',
|
|
1921
|
+
{ defaultLocale: 'en', locales: [{ code: 'en' }, { code: 'fr' }] } as any,
|
|
1922
|
+
);
|
|
1923
|
+
const processed = result.processedRawHtmlCollector.get(rawSlice);
|
|
1924
|
+
expect(processed).toContain('href="/fr/about"');
|
|
1925
|
+
});
|
|
1894
1926
|
});
|
|
1895
1927
|
|
|
1896
1928
|
// -----------------------------------------------------------------------
|
|
@@ -119,6 +119,12 @@ interface SSRContext {
|
|
|
119
119
|
isProductionBuild?: boolean;
|
|
120
120
|
/** Collector for SSR fallback HTML of complex nodes (list, locale-list) keyed by element path */
|
|
121
121
|
ssrFallbackCollector?: Map<string, string>;
|
|
122
|
+
/**
|
|
123
|
+
* Collector mapping the raw slice of a RAW_HTML_PREFIX string to its fully processed HTML
|
|
124
|
+
* (after image rewriting, component expansion, and link localization). Consumed by the
|
|
125
|
+
* Astro exporter so its `<Fragment set:html>` output matches SSR exactly.
|
|
126
|
+
*/
|
|
127
|
+
processedRawHtmlCollector?: Map<string, string>;
|
|
122
128
|
/** Image format: 'webp' uses plain <img>, 'avif' uses <picture> with AVIF+WebP sources */
|
|
123
129
|
imageFormat?: 'webp' | 'avif';
|
|
124
130
|
}
|
|
@@ -369,7 +375,7 @@ export async function buildComponentHTML(
|
|
|
369
375
|
cmsContext?: CMSContext,
|
|
370
376
|
cmsService?: CMSService,
|
|
371
377
|
isProductionBuild?: boolean
|
|
372
|
-
): Promise<{ html: string; interactiveStylesMap: Map<string, InteractiveStyles>; preloadImages: PreloadImage[]; neededCollections: Set<string>; ssrFallbackCollector: Map<string, string> }> {
|
|
378
|
+
): Promise<{ html: string; interactiveStylesMap: Map<string, InteractiveStyles>; preloadImages: PreloadImage[]; neededCollections: Set<string>; ssrFallbackCollector: Map<string, string>; processedRawHtmlCollector: Map<string, string> }> {
|
|
373
379
|
// Create map to collect interactive styles during render
|
|
374
380
|
const interactiveStylesMap = new Map<string, InteractiveStyles>();
|
|
375
381
|
// Create array to collect high-priority images for preloading
|
|
@@ -378,8 +384,10 @@ export async function buildComponentHTML(
|
|
|
378
384
|
const neededCollections = new Set<string>();
|
|
379
385
|
// Create map to collect SSR fallback HTML for complex nodes (list, locale-list)
|
|
380
386
|
const ssrFallbackCollector = new Map<string, string>();
|
|
387
|
+
// Create map to collect raw-HTML → processed-HTML for Astro exporter parity
|
|
388
|
+
const processedRawHtmlCollector = new Map<string, string>();
|
|
381
389
|
|
|
382
|
-
if (!node) return { html: '', interactiveStylesMap, preloadImages, neededCollections, ssrFallbackCollector };
|
|
390
|
+
if (!node) return { html: '', interactiveStylesMap, preloadImages, neededCollections, ssrFallbackCollector, processedRawHtmlCollector };
|
|
383
391
|
|
|
384
392
|
// Register components for this render
|
|
385
393
|
ssrComponentRegistry.merge(globalComponents);
|
|
@@ -411,12 +419,13 @@ export async function buildComponentHTML(
|
|
|
411
419
|
neededCollections, // Track collections that need client-side data
|
|
412
420
|
isProductionBuild,
|
|
413
421
|
ssrFallbackCollector, // Collect SSR fallback HTML for complex nodes
|
|
422
|
+
processedRawHtmlCollector, // Collect raw→processed HTML for Astro exporter
|
|
414
423
|
imageFormat: configService.getImageFormat(),
|
|
415
424
|
};
|
|
416
425
|
|
|
417
426
|
const html = await renderNode(node, ctx);
|
|
418
427
|
|
|
419
|
-
return { html, interactiveStylesMap, preloadImages, neededCollections, ssrFallbackCollector };
|
|
428
|
+
return { html, interactiveStylesMap, preloadImages, neededCollections, ssrFallbackCollector, processedRawHtmlCollector };
|
|
420
429
|
}
|
|
421
430
|
|
|
422
431
|
/**
|
|
@@ -803,10 +812,12 @@ async function renderNode(
|
|
|
803
812
|
}
|
|
804
813
|
// Check for raw HTML marker (from rich-text fields) - don't escape
|
|
805
814
|
if (text.startsWith(RAW_HTML_PREFIX)) {
|
|
806
|
-
|
|
815
|
+
const rawSlice = text.slice(RAW_HTML_PREFIX.length);
|
|
816
|
+
let rawHtml = rawSlice;
|
|
807
817
|
if (ctx.imageMetadataMap) rawHtml = rewriteRichTextImages(rawHtml, ctx.imageMetadataMap, ctx.imageFormat);
|
|
808
818
|
rawHtml = await expandRichTextComponents(rawHtml, ctx);
|
|
809
819
|
rawHtml = localizeRichTextLinks(rawHtml, ctx);
|
|
820
|
+
ctx.processedRawHtmlCollector?.set(rawSlice, rawHtml);
|
|
810
821
|
return rawHtml;
|
|
811
822
|
}
|
|
812
823
|
return escapeHtml(text);
|
|
@@ -823,10 +834,12 @@ async function renderNode(
|
|
|
823
834
|
}
|
|
824
835
|
// Check for raw HTML marker (from rich-text fields) - don't escape
|
|
825
836
|
if (text.startsWith(RAW_HTML_PREFIX)) {
|
|
826
|
-
|
|
837
|
+
const rawSlice = text.slice(RAW_HTML_PREFIX.length);
|
|
838
|
+
let rawHtml = rawSlice;
|
|
827
839
|
if (ctx.imageMetadataMap) rawHtml = rewriteRichTextImages(rawHtml, ctx.imageMetadataMap, ctx.imageFormat);
|
|
828
840
|
rawHtml = await expandRichTextComponents(rawHtml, ctx);
|
|
829
841
|
rawHtml = localizeRichTextLinks(rawHtml, ctx);
|
|
842
|
+
ctx.processedRawHtmlCollector?.set(rawSlice, rawHtml);
|
|
830
843
|
return rawHtml;
|
|
831
844
|
}
|
|
832
845
|
return escapeHtml(text);
|
|
@@ -1428,7 +1441,7 @@ async function renderHtmlElement(
|
|
|
1428
1441
|
if (voidElements.includes(tag.toLowerCase())) {
|
|
1429
1442
|
// Special handling for img tags - inject srcset and render as <picture> when AVIF available
|
|
1430
1443
|
if (tag.toLowerCase() === 'img') {
|
|
1431
|
-
return renderImageElement(propsWithStyleAndAttrs, classAttr, attrs, ctx);
|
|
1444
|
+
return renderImageElement(propsWithStyleAndAttrs, classAttr, styleAttr, attrs, ctx);
|
|
1432
1445
|
}
|
|
1433
1446
|
|
|
1434
1447
|
return `<${tag}${classAttr}${styleAttr}${attrs} />`;
|
|
@@ -1443,6 +1456,7 @@ async function renderHtmlElement(
|
|
|
1443
1456
|
function renderImageElement(
|
|
1444
1457
|
propsWithStyleAndAttrs: Record<string, unknown>,
|
|
1445
1458
|
classAttr: string,
|
|
1459
|
+
styleAttr: string,
|
|
1446
1460
|
attrs: string,
|
|
1447
1461
|
ctx: SSRContext
|
|
1448
1462
|
): string {
|
|
@@ -1525,7 +1539,7 @@ function renderImageElement(
|
|
|
1525
1539
|
? ` class="${escapeHtml(imgClasses.join(' '))}"`
|
|
1526
1540
|
: '';
|
|
1527
1541
|
|
|
1528
|
-
return `<picture${pictureClassAttr}>` +
|
|
1542
|
+
return `<picture${pictureClassAttr}${styleAttr}>` +
|
|
1529
1543
|
`<source type="image/avif" srcset="${escapeHtml(metadata.avifSrcset)}" sizes="${escapeHtml(sizesAttr)}" />` +
|
|
1530
1544
|
`<source type="image/webp" srcset="${escapeHtml(metadata.srcset)}" sizes="${escapeHtml(sizesAttr)}" />` +
|
|
1531
1545
|
`<img${imgClassAttr}${imgAttrs}${attrs} />` +
|
|
@@ -1538,7 +1552,7 @@ function renderImageElement(
|
|
|
1538
1552
|
imgAttrs += ` sizes="${escapeHtml(sizesAttr)}"`;
|
|
1539
1553
|
}
|
|
1540
1554
|
|
|
1541
|
-
return `<img${classAttr}${imgAttrs}${attrs} />`;
|
|
1555
|
+
return `<img${classAttr}${styleAttr}${imgAttrs}${attrs} />`;
|
|
1542
1556
|
}
|
|
1543
1557
|
|
|
1544
1558
|
/**
|
|
@@ -1699,7 +1713,7 @@ export async function renderPageSSR(
|
|
|
1699
1713
|
cmsContext?: CMSContext,
|
|
1700
1714
|
cmsService?: CMSService,
|
|
1701
1715
|
isProductionBuild?: boolean
|
|
1702
|
-
): Promise<{ html: string; meta: string; title: string; javascript: string; componentCSS?: string; locale: string; interactiveStylesMap: Map<string, InteractiveStyles>; preloadImages: PreloadImage[]; neededCollections: Set<string>; ssrFallbackCollector: Map<string, string> }> {
|
|
1716
|
+
): Promise<{ html: string; meta: string; title: string; javascript: string; componentCSS?: string; locale: string; interactiveStylesMap: Map<string, InteractiveStyles>; preloadImages: PreloadImage[]; neededCollections: Set<string>; ssrFallbackCollector: Map<string, string>; processedRawHtmlCollector: Map<string, string> }> {
|
|
1703
1717
|
// Extract page content
|
|
1704
1718
|
const rootNode = pageData?.root || undefined;
|
|
1705
1719
|
if (!rootNode) {
|
|
@@ -1735,9 +1749,9 @@ export async function renderPageSSR(
|
|
|
1735
1749
|
|
|
1736
1750
|
// Render the component tree to HTML with i18n and CMS support
|
|
1737
1751
|
// Also collect interactive styles, preload images, and needed collections during render
|
|
1738
|
-
const { html: contentHTML, interactiveStylesMap, preloadImages, neededCollections, ssrFallbackCollector } = rootNode
|
|
1752
|
+
const { html: contentHTML, interactiveStylesMap, preloadImages, neededCollections, ssrFallbackCollector, processedRawHtmlCollector } = rootNode
|
|
1739
1753
|
? await buildComponentHTML(rootNode, globalComponents, pageComponents, effectiveLocale, config, slugMappings, pagePath, cmsContext, cmsService, isProductionBuild)
|
|
1740
|
-
: { html: '', interactiveStylesMap: new Map<string, InteractiveStyles>(), preloadImages: [], neededCollections: new Set<string>(), ssrFallbackCollector: new Map<string, string>() };
|
|
1754
|
+
: { html: '', interactiveStylesMap: new Map<string, InteractiveStyles>(), preloadImages: [], neededCollections: new Set<string>(), ssrFallbackCollector: new Map<string, string>(), processedRawHtmlCollector: new Map<string, string>() };
|
|
1741
1755
|
|
|
1742
1756
|
// Collect JavaScript and CSS from all components
|
|
1743
1757
|
const javascript = await collectComponentJavaScript(globalComponents, pageComponents);
|
|
@@ -1769,5 +1783,6 @@ export async function renderPageSSR(
|
|
|
1769
1783
|
preloadImages,
|
|
1770
1784
|
neededCollections,
|
|
1771
1785
|
ssrFallbackCollector,
|
|
1786
|
+
processedRawHtmlCollector,
|
|
1772
1787
|
};
|
|
1773
1788
|
}
|
package/lib/shared/constants.ts
CHANGED
|
@@ -139,6 +139,7 @@ export const IFRAME_MESSAGE_TYPES = {
|
|
|
139
139
|
DUPLICATE_ELEMENT: 'DUPLICATE_ELEMENT',
|
|
140
140
|
MAKE_COMPONENT: 'MAKE_COMPONENT',
|
|
141
141
|
SWAP_TO_COMPONENT: 'SWAP_TO_COMPONENT',
|
|
142
|
+
SMART_CONNECT: 'SMART_CONNECT',
|
|
142
143
|
} as const;
|
|
143
144
|
|
|
144
145
|
// Component node type constants
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import { generateRuleForClass, generateUtilityCSS, extractUtilityClassesFromHTML } from './cssGeneration';
|
|
2
|
+
import { generateRuleForClass, generateUtilityCSS, generateSingleClassCSS, extractUtilityClassesFromHTML, generateInteractiveCSS } from './cssGeneration';
|
|
3
|
+
import { DEFAULT_BREAKPOINTS } from './breakpoints';
|
|
4
|
+
import type { ResponsiveScales } from './responsiveScaling';
|
|
5
|
+
import type { InteractiveStyles } from './types/styles';
|
|
3
6
|
|
|
4
7
|
describe('extractUtilityClassesFromHTML', () => {
|
|
5
8
|
test('extracts ins- classes', () => {
|
|
@@ -102,9 +105,33 @@ describe('cssGeneration', () => {
|
|
|
102
105
|
expect(rule).toBe('border: 1px solid var(--primary);');
|
|
103
106
|
});
|
|
104
107
|
|
|
105
|
-
test('border-color
|
|
108
|
+
test('border-color with named CSS color outputs directly', () => {
|
|
106
109
|
const rule = generateRuleForClass('bc-red');
|
|
107
|
-
expect(rule).toBe('border-color:
|
|
110
|
+
expect(rule).toBe('border-color: red;');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('border-color with variable uses var()', () => {
|
|
114
|
+
const rule = generateRuleForClass('bc-primary');
|
|
115
|
+
expect(rule).toBe('border-color: var(--primary);');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('CSS named colors vs variables', () => {
|
|
120
|
+
test('color with named color outputs directly', () => {
|
|
121
|
+
expect(generateRuleForClass('c-red')).toBe('color: red;');
|
|
122
|
+
expect(generateRuleForClass('c-blue')).toBe('color: blue;');
|
|
123
|
+
expect(generateRuleForClass('c-transparent')).toBe('color: transparent;');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('background-color with named color outputs directly', () => {
|
|
127
|
+
expect(generateRuleForClass('bgc-green')).toBe('background-color: green;');
|
|
128
|
+
expect(generateRuleForClass('bgc-white')).toBe('background-color: white;');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('color with variable name still uses var()', () => {
|
|
132
|
+
expect(generateRuleForClass('c-primary')).toBe('color: var(--primary);');
|
|
133
|
+
expect(generateRuleForClass('c-text')).toBe('color: var(--text);');
|
|
134
|
+
expect(generateRuleForClass('bgc-background')).toBe('background-color: var(--background);');
|
|
108
135
|
});
|
|
109
136
|
});
|
|
110
137
|
|
|
@@ -190,3 +217,82 @@ describe('cssGeneration', () => {
|
|
|
190
217
|
});
|
|
191
218
|
});
|
|
192
219
|
});
|
|
220
|
+
|
|
221
|
+
describe('generateSingleClassCSS - responsive classes', () => {
|
|
222
|
+
test('generates correct media query for tablet-prefixed class', () => {
|
|
223
|
+
const css = generateSingleClassCSS('t-fs-132px', DEFAULT_BREAKPOINTS);
|
|
224
|
+
expect(css).toContain('@media (max-width: 1024px)');
|
|
225
|
+
expect(css).toContain('font-size: 132px');
|
|
226
|
+
expect(css).toContain('.t-fs-132px');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('generates correct media query for mobile-prefixed class', () => {
|
|
230
|
+
const css = generateSingleClassCSS('mob-fs-24px', DEFAULT_BREAKPOINTS);
|
|
231
|
+
expect(css).toContain('@media (max-width: 540px)');
|
|
232
|
+
expect(css).toContain('font-size: 24px');
|
|
233
|
+
expect(css).toContain('.mob-fs-24px');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('generates base rule for non-prefixed class', () => {
|
|
237
|
+
const css = generateSingleClassCSS('fs-48px', DEFAULT_BREAKPOINTS);
|
|
238
|
+
expect(css).toContain('.fs-48px');
|
|
239
|
+
expect(css).toContain('font-size: 48px');
|
|
240
|
+
expect(css).not.toContain('@media');
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe('generateInteractiveCSS — auto-responsive scaling', () => {
|
|
245
|
+
const scalesEnabled: ResponsiveScales = {
|
|
246
|
+
enabled: true,
|
|
247
|
+
baseReference: 16,
|
|
248
|
+
padding: { tablet: 0.75, mobile: 0.5 },
|
|
249
|
+
fontSize: { tablet: 0.88, mobile: 0.75 },
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
test('flat interactive style gets scaled @media overrides', () => {
|
|
253
|
+
const styles: InteractiveStyles = [
|
|
254
|
+
{ prefix: '', postfix: ':hover', style: { fontSize: '100px' } },
|
|
255
|
+
];
|
|
256
|
+
const css = generateInteractiveCSS('c_heading_text', styles, DEFAULT_BREAKPOINTS, undefined, scalesEnabled);
|
|
257
|
+
expect(css).toContain('.c_heading_text:hover { font-size: 100px; }');
|
|
258
|
+
// tablet: 100 + (100-16)*(0.88-1) = 89.92 → 90
|
|
259
|
+
expect(css).toContain('@media (max-width: 1024px) { .c_heading_text:hover { font-size: 90px; } }');
|
|
260
|
+
// mobile: 100 + (100-16)*(0.75-1) = 79
|
|
261
|
+
expect(css).toContain('@media (max-width: 540px) { .c_heading_text:hover { font-size: 79px; } }');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test('responsive style auto-scales properties not explicitly overridden', () => {
|
|
265
|
+
const styles: InteractiveStyles = [
|
|
266
|
+
{
|
|
267
|
+
prefix: '',
|
|
268
|
+
postfix: ':hover',
|
|
269
|
+
style: {
|
|
270
|
+
base: { padding: '40px', fontSize: '100px' },
|
|
271
|
+
tablet: { fontSize: '50px' }, // explicit tablet fontSize wins
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
];
|
|
275
|
+
const css = generateInteractiveCSS('c_btn', styles, DEFAULT_BREAKPOINTS, undefined, scalesEnabled);
|
|
276
|
+
// Tablet @media must contain the explicit fontSize AND the auto-scaled padding
|
|
277
|
+
// padding tablet: 40 + (40-16)*(0.75-1) = 34
|
|
278
|
+
expect(css).toMatch(/@media \(max-width: 1024px\) \{[^}]*font-size:\s*50px[^}]*\}/);
|
|
279
|
+
expect(css).toMatch(/@media \(max-width: 1024px\) \{[^}]*padding:\s*34px[^}]*\}/);
|
|
280
|
+
// Mobile: no explicit branch — both padding and fontSize auto-scale from base
|
|
281
|
+
// padding mobile: 40 + (40-16)*(-0.5) = 28; fontSize mobile: 79
|
|
282
|
+
expect(css).toMatch(/@media \(max-width: 540px\) \{[^}]*padding:\s*28px[^}]*\}/);
|
|
283
|
+
expect(css).toMatch(/@media \(max-width: 540px\) \{[^}]*font-size:\s*79px[^}]*\}/);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test('scaling disabled is a no-op', () => {
|
|
287
|
+
const styles: InteractiveStyles = [
|
|
288
|
+
{ prefix: '', postfix: ':hover', style: { fontSize: '100px' } },
|
|
289
|
+
];
|
|
290
|
+
const cssNoScales = generateInteractiveCSS('c_heading', styles, DEFAULT_BREAKPOINTS);
|
|
291
|
+
const cssDisabled = generateInteractiveCSS('c_heading', styles, DEFAULT_BREAKPOINTS, undefined, {
|
|
292
|
+
...scalesEnabled,
|
|
293
|
+
enabled: false,
|
|
294
|
+
});
|
|
295
|
+
expect(cssNoScales).toBe(cssDisabled);
|
|
296
|
+
expect(cssNoScales).not.toContain('@media');
|
|
297
|
+
});
|
|
298
|
+
});
|