meno-core 1.0.47 → 1.0.49
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 +2 -2
- package/dist/build-static.js +7 -7
- package/dist/chunks/{chunk-UUA5LEWF.js → chunk-6IVUG7FY.js} +138 -7
- package/dist/chunks/chunk-6IVUG7FY.js.map +7 -0
- package/dist/chunks/{chunk-XSWR3QLI.js → chunk-AZQYF6KE.js} +261 -130
- package/dist/chunks/chunk-AZQYF6KE.js.map +7 -0
- package/dist/chunks/{chunk-47UNLQUU.js → chunk-CHD5UCFF.js} +57 -12
- package/dist/chunks/chunk-CHD5UCFF.js.map +7 -0
- package/dist/chunks/{chunk-FGUZOYJX.js → chunk-EQYDSPBB.js} +435 -131
- package/dist/chunks/chunk-EQYDSPBB.js.map +7 -0
- package/dist/chunks/{chunk-IF3RATBY.js → chunk-H4JSCDNW.js} +2 -2
- package/dist/chunks/{chunk-KITQJYZV.js → chunk-J23ZX5AP.js} +40 -4
- package/dist/chunks/chunk-J23ZX5AP.js.map +7 -0
- package/dist/chunks/{chunk-LJFB5EBT.js → chunk-JER5NQVM.js} +5 -5
- package/dist/chunks/{chunk-ZTKHJQ2Z.js → chunk-KPU2XHOS.js} +5 -2
- package/dist/chunks/{chunk-ZTKHJQ2Z.js.map → chunk-KPU2XHOS.js.map} +2 -2
- package/dist/chunks/{chunk-BCLGRZ3U.js → chunk-LKAGAQ3M.js} +2 -2
- package/dist/chunks/{chunk-FED5MME6.js → chunk-S2CX6HFM.js} +262 -26
- package/dist/chunks/chunk-S2CX6HFM.js.map +7 -0
- package/dist/chunks/{configService-DYCUEURL.js → configService-CCA6AIDI.js} +3 -3
- package/dist/entries/server-router.js +9 -9
- package/dist/entries/server-router.js.map +2 -2
- package/dist/lib/client/index.js +64 -20
- package/dist/lib/client/index.js.map +3 -3
- package/dist/lib/server/index.js +1737 -296
- package/dist/lib/server/index.js.map +4 -4
- package/dist/lib/shared/index.js +50 -10
- package/dist/lib/shared/index.js.map +3 -3
- package/entries/server-router.tsx +6 -2
- package/lib/client/core/ComponentBuilder.test.ts +17 -0
- package/lib/client/core/ComponentBuilder.ts +25 -1
- package/lib/client/core/builders/embedBuilder.ts +15 -2
- package/lib/client/core/builders/linkNodeBuilder.ts +15 -2
- package/lib/client/core/builders/localeListBuilder.ts +17 -6
- package/lib/client/styles/StyleInjector.ts +3 -2
- package/lib/client/theme.ts +4 -4
- package/lib/server/cssGenerator.test.ts +64 -1
- package/lib/server/cssGenerator.ts +48 -9
- 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/providers/fileSystemCMSProvider.test.ts +163 -0
- package/lib/server/providers/fileSystemCMSProvider.ts +200 -11
- package/lib/server/routes/api/variables.ts +4 -2
- package/lib/server/routes/index.ts +1 -1
- package/lib/server/routes/pages.ts +23 -1
- package/lib/server/services/cmsService.test.ts +246 -0
- package/lib/server/services/cmsService.ts +122 -5
- package/lib/server/services/configService.ts +5 -0
- package/lib/server/ssr/attributeBuilder.ts +41 -0
- package/lib/server/ssr/htmlGenerator.test.ts +114 -2
- package/lib/server/ssr/htmlGenerator.ts +53 -6
- package/lib/server/ssr/liveReloadIntegration.test.ts +209 -0
- package/lib/server/ssr/ssrRenderer.test.ts +362 -1
- package/lib/server/ssr/ssrRenderer.ts +216 -72
- 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/cssGeneration.test.ts +267 -1
- package/lib/shared/cssGeneration.ts +240 -18
- package/lib/shared/cssProperties.test.ts +247 -1
- package/lib/shared/cssProperties.ts +196 -6
- package/lib/shared/elementClassName.test.ts +15 -0
- package/lib/shared/elementClassName.ts +7 -3
- package/lib/shared/interfaces/contentProvider.ts +39 -6
- package/lib/shared/pathSecurity.ts +16 -0
- package/lib/shared/registry/nodeTypes/ListNodeType.ts +1 -1
- package/lib/shared/responsiveScaling.test.ts +143 -0
- package/lib/shared/responsiveScaling.ts +253 -2
- package/lib/shared/themeDefaults.test.ts +3 -3
- package/lib/shared/themeDefaults.ts +3 -3
- package/lib/shared/types/cms.ts +28 -3
- package/lib/shared/types/index.ts +2 -0
- package/lib/shared/types/variables.ts +37 -0
- package/lib/shared/utilityClassConfig.ts +3 -0
- package/lib/shared/utilityClassMapper.test.ts +123 -0
- package/lib/shared/utilityClassMapper.ts +179 -8
- package/lib/shared/validation/schemas.ts +15 -1
- package/lib/shared/validation/validators.ts +26 -1
- package/package.json +1 -1
- package/dist/chunks/chunk-47UNLQUU.js.map +0 -7
- package/dist/chunks/chunk-FED5MME6.js.map +0 -7
- package/dist/chunks/chunk-FGUZOYJX.js.map +0 -7
- package/dist/chunks/chunk-KITQJYZV.js.map +0 -7
- package/dist/chunks/chunk-UUA5LEWF.js.map +0 -7
- package/dist/chunks/chunk-XSWR3QLI.js.map +0 -7
- /package/dist/chunks/{chunk-IF3RATBY.js.map → chunk-H4JSCDNW.js.map} +0 -0
- /package/dist/chunks/{chunk-LJFB5EBT.js.map → chunk-JER5NQVM.js.map} +0 -0
- /package/dist/chunks/{chunk-BCLGRZ3U.js.map → chunk-LKAGAQ3M.js.map} +0 -0
- /package/dist/chunks/{configService-DYCUEURL.js.map → configService-CCA6AIDI.js.map} +0 -0
|
@@ -8,6 +8,7 @@ import type { ReactElement } from "react";
|
|
|
8
8
|
import type { LocaleListNode, StyleObject, ResponsiveStyleObject, InteractiveStyles } from "../../../shared/types";
|
|
9
9
|
import { extractAttributesFromNode } from "../../../shared/attributeNodeUtils";
|
|
10
10
|
import { responsiveStylesToClasses } from "../../../shared/utilityClassMapper";
|
|
11
|
+
import { getCachedResponsiveScalesConfig } from "../../responsiveStyleResolver";
|
|
11
12
|
import { pathToString } from "../../../shared/pathArrayUtils";
|
|
12
13
|
import { generateElementClassName, type ElementClassContext } from "../../../shared/elementClassName";
|
|
13
14
|
import { InteractiveStylesRegistry } from "../../InteractiveStylesRegistry";
|
|
@@ -73,9 +74,16 @@ export function buildLocaleList(
|
|
|
73
74
|
// Start building className
|
|
74
75
|
let classNames: string[] = [];
|
|
75
76
|
|
|
77
|
+
// Resolve fluid mode once for all conversions in this builder
|
|
78
|
+
const scales = getCachedResponsiveScalesConfig();
|
|
79
|
+
const fluidActive = scales?.enabled === true && scales?.mode === 'fluid';
|
|
80
|
+
|
|
76
81
|
// Convert container styles to utility classes
|
|
77
82
|
if (node.style) {
|
|
78
|
-
const utilityClasses = responsiveStylesToClasses(
|
|
83
|
+
const utilityClasses = responsiveStylesToClasses(
|
|
84
|
+
node.style as StyleObject | ResponsiveStyleObject,
|
|
85
|
+
{ fluidActive, responsiveScales: scales ?? undefined }
|
|
86
|
+
);
|
|
79
87
|
UtilityClassCollector.collect(utilityClasses);
|
|
80
88
|
classNames.push(...utilityClasses);
|
|
81
89
|
}
|
|
@@ -123,7 +131,10 @@ export function buildLocaleList(
|
|
|
123
131
|
const previewClasses: string[] = [];
|
|
124
132
|
for (const rule of nodeInteractiveStyles) {
|
|
125
133
|
if (rule.previewProp && ctx.componentResolvedProps[rule.previewProp] === true) {
|
|
126
|
-
const styleClasses = responsiveStylesToClasses(
|
|
134
|
+
const styleClasses = responsiveStylesToClasses(
|
|
135
|
+
rule.style as StyleObject | ResponsiveStyleObject,
|
|
136
|
+
{ fluidActive, responsiveScales: scales ?? undefined }
|
|
137
|
+
);
|
|
127
138
|
UtilityClassCollector.collect(styleClasses);
|
|
128
139
|
previewClasses.push(...styleClasses);
|
|
129
140
|
}
|
|
@@ -169,10 +180,10 @@ export function buildLocaleList(
|
|
|
169
180
|
const currentLocaleCode = locale || i18nConfig?.defaultLocale || 'en';
|
|
170
181
|
|
|
171
182
|
// Convert item styles, active item styles, separator styles, and flag styles to utility classes
|
|
172
|
-
const itemClasses = node.itemStyle ? responsiveStylesToClasses(node.itemStyle as StyleObject | ResponsiveStyleObject) : [];
|
|
173
|
-
const activeItemClasses = node.activeItemStyle ? responsiveStylesToClasses(node.activeItemStyle as StyleObject | ResponsiveStyleObject) : [];
|
|
174
|
-
const separatorClasses = node.separatorStyle ? responsiveStylesToClasses(node.separatorStyle as StyleObject | ResponsiveStyleObject) : [];
|
|
175
|
-
const flagClasses = node.flagStyle ? responsiveStylesToClasses(node.flagStyle as StyleObject | ResponsiveStyleObject) : [];
|
|
183
|
+
const itemClasses = node.itemStyle ? responsiveStylesToClasses(node.itemStyle as StyleObject | ResponsiveStyleObject, { fluidActive, responsiveScales: scales ?? undefined }) : [];
|
|
184
|
+
const activeItemClasses = node.activeItemStyle ? responsiveStylesToClasses(node.activeItemStyle as StyleObject | ResponsiveStyleObject, { fluidActive, responsiveScales: scales ?? undefined }) : [];
|
|
185
|
+
const separatorClasses = node.separatorStyle ? responsiveStylesToClasses(node.separatorStyle as StyleObject | ResponsiveStyleObject, { fluidActive, responsiveScales: scales ?? undefined }) : [];
|
|
186
|
+
const flagClasses = node.flagStyle ? responsiveStylesToClasses(node.flagStyle as StyleObject | ResponsiveStyleObject, { fluidActive, responsiveScales: scales ?? undefined }) : [];
|
|
176
187
|
UtilityClassCollector.collect(itemClasses);
|
|
177
188
|
UtilityClassCollector.collect(activeItemClasses);
|
|
178
189
|
UtilityClassCollector.collect(separatorClasses);
|
|
@@ -12,7 +12,7 @@ import type { ComponentRegistry } from '../componentRegistry';
|
|
|
12
12
|
import type { ElementRegistry } from '../elementRegistry';
|
|
13
13
|
import { hasTemplates, processCodeTemplates } from '../templateEngine';
|
|
14
14
|
import { generateAllInteractiveCSS } from '../../shared/cssGeneration';
|
|
15
|
-
import { getCachedBreakpointConfig, getCachedRemConversionConfig } from '../responsiveStyleResolver';
|
|
15
|
+
import { getCachedBreakpointConfig, getCachedRemConversionConfig, getCachedResponsiveScalesConfig } from '../responsiveStyleResolver';
|
|
16
16
|
import { DEFAULT_BREAKPOINTS } from '../../shared/breakpoints';
|
|
17
17
|
import { InteractiveStylesRegistry } from '../InteractiveStylesRegistry';
|
|
18
18
|
import { UtilityClassCollector } from './UtilityClassCollector';
|
|
@@ -153,7 +153,8 @@ export class StyleInjector {
|
|
|
153
153
|
|
|
154
154
|
const breakpointConfig = getCachedBreakpointConfig() || DEFAULT_BREAKPOINTS;
|
|
155
155
|
const remConversionConfig = getCachedRemConversionConfig() || undefined;
|
|
156
|
-
const
|
|
156
|
+
const responsiveScalesConfig = getCachedResponsiveScalesConfig() || undefined;
|
|
157
|
+
const interactiveCSS = generateAllInteractiveCSS(interactiveStylesMap, breakpointConfig, remConversionConfig, responsiveScalesConfig);
|
|
157
158
|
if (!interactiveCSS) return;
|
|
158
159
|
|
|
159
160
|
if (document.head) {
|
package/lib/client/theme.ts
CHANGED
|
@@ -77,9 +77,9 @@ const lightThemeColors: ThemeColors = {
|
|
|
77
77
|
backgroundTertiary: '#eaeef2',
|
|
78
78
|
border: '#d0d7de',
|
|
79
79
|
borderSecondary: '#d1d5da',
|
|
80
|
-
text: '#
|
|
80
|
+
text: '#1a1a1a',
|
|
81
81
|
textSecondary: '#586069',
|
|
82
|
-
textMuted: '#
|
|
82
|
+
textMuted: '#525a63',
|
|
83
83
|
codeString: '#032f62',
|
|
84
84
|
codeNumber: '#005cc5',
|
|
85
85
|
codeKey: '#005cc5',
|
|
@@ -125,9 +125,9 @@ const darkThemeColors: ThemeColors = {
|
|
|
125
125
|
backgroundTertiary: '#252525',
|
|
126
126
|
border: '#333333',
|
|
127
127
|
borderSecondary: '#444444',
|
|
128
|
-
text: '#
|
|
128
|
+
text: '#ebebeb',
|
|
129
129
|
textSecondary: '#cccccc',
|
|
130
|
-
textMuted: '#
|
|
130
|
+
textMuted: '#b0b0b0',
|
|
131
131
|
codeString: '#ffffff',
|
|
132
132
|
codeNumber: '#b5cea8',
|
|
133
133
|
codeKey: '#9cdcfe',
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { describe, test, expect } from 'bun:test';
|
|
2
|
-
import { generateColorVariablesCSS, generateThemeColorVariablesCSS } from './cssGenerator';
|
|
2
|
+
import { generateColorVariablesCSS, generateThemeColorVariablesCSS, generateVariablesCSS } from './cssGenerator';
|
|
3
3
|
import type { ColorVariables, ThemeConfig } from '../shared/types/colors';
|
|
4
|
+
import type { ResponsiveScales } from '../shared/responsiveScaling';
|
|
5
|
+
import { DEFAULT_BREAKPOINTS } from '../shared/breakpoints';
|
|
4
6
|
|
|
5
7
|
describe('cssGenerator', () => {
|
|
6
8
|
describe('generateColorVariablesCSS', () => {
|
|
@@ -169,4 +171,65 @@ describe('cssGenerator', () => {
|
|
|
169
171
|
expect(css).toContain('\n\n');
|
|
170
172
|
});
|
|
171
173
|
});
|
|
174
|
+
|
|
175
|
+
describe('generateVariablesCSS — fluid mode', () => {
|
|
176
|
+
const fluidScales: ResponsiveScales = {
|
|
177
|
+
enabled: true,
|
|
178
|
+
mode: 'fluid',
|
|
179
|
+
baseReference: 16,
|
|
180
|
+
fluidRange: { min: 320, max: 1440 },
|
|
181
|
+
siteMargin: { min: 16, max: 32 },
|
|
182
|
+
fontSize: { tablet: 0.88, mobile: 0.75 },
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
test('emits --site-margin clamp on :root when fluid mode is enabled (no user vars)', () => {
|
|
186
|
+
const css = generateVariablesCSS(
|
|
187
|
+
{ variables: [] },
|
|
188
|
+
DEFAULT_BREAKPOINTS,
|
|
189
|
+
fluidScales
|
|
190
|
+
);
|
|
191
|
+
expect(css).toContain('--site-margin: clamp(16px,');
|
|
192
|
+
expect(css).toContain(', 32px)');
|
|
193
|
+
expect(css).not.toContain('@media');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
test('emits user variables with clamp() in fluid mode (no @media)', () => {
|
|
198
|
+
const css = generateVariablesCSS(
|
|
199
|
+
{
|
|
200
|
+
variables: [
|
|
201
|
+
{ name: 'h1-fs', cssVar: '--h1-fs', value: '32px', type: 'fontSize' },
|
|
202
|
+
],
|
|
203
|
+
},
|
|
204
|
+
DEFAULT_BREAKPOINTS,
|
|
205
|
+
fluidScales
|
|
206
|
+
);
|
|
207
|
+
// mobile fontSize scale=0.75 → MIN=28
|
|
208
|
+
expect(css).toContain('--h1-fs: clamp(28px,');
|
|
209
|
+
expect(css).toContain(', 32px)');
|
|
210
|
+
expect(css).not.toContain('@media');
|
|
211
|
+
// and the site-margin var still emitted
|
|
212
|
+
expect(css).toContain('--site-margin: clamp(16px,');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('breakpoints mode (default) keeps @media blocks for vars and DOES NOT emit --site-margin', () => {
|
|
216
|
+
const css = generateVariablesCSS(
|
|
217
|
+
{
|
|
218
|
+
variables: [
|
|
219
|
+
{ name: 'h1-fs', cssVar: '--h1-fs', value: '32px', type: 'fontSize' },
|
|
220
|
+
],
|
|
221
|
+
},
|
|
222
|
+
DEFAULT_BREAKPOINTS,
|
|
223
|
+
{ ...fluidScales, mode: 'breakpoints' }
|
|
224
|
+
);
|
|
225
|
+
expect(css).not.toContain('--site-margin');
|
|
226
|
+
expect(css).toContain('@media (max-width: 1024px)');
|
|
227
|
+
expect(css).not.toContain('clamp(');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('returns empty string when no user vars AND fluid disabled', () => {
|
|
231
|
+
const css = generateVariablesCSS({ variables: [] }, DEFAULT_BREAKPOINTS);
|
|
232
|
+
expect(css).toBe('');
|
|
233
|
+
});
|
|
234
|
+
});
|
|
172
235
|
});
|
|
@@ -9,7 +9,14 @@ import { resolvePaletteColor } from '../shared/types/colors';
|
|
|
9
9
|
import type { VariablesConfig, CSSVariable } from '../shared/types/variables';
|
|
10
10
|
import type { BreakpointConfig } from '../shared/breakpoints';
|
|
11
11
|
import type { ResponsiveScales, BreakpointScales } from '../shared/responsiveScaling';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
scalePropertyValue,
|
|
14
|
+
buildSiteMarginClamp,
|
|
15
|
+
buildFluidPropertyValue,
|
|
16
|
+
getSmallestBreakpointName,
|
|
17
|
+
DEFAULT_FLUID_RANGE,
|
|
18
|
+
DEFAULT_SITE_MARGIN,
|
|
19
|
+
} from '../shared/responsiveScaling';
|
|
13
20
|
|
|
14
21
|
/**
|
|
15
22
|
* Generate CSS color variable declarations from color variables
|
|
@@ -75,18 +82,50 @@ export function generateVariablesCSS(
|
|
|
75
82
|
breakpoints?: BreakpointConfig,
|
|
76
83
|
responsiveScales?: ResponsiveScales
|
|
77
84
|
): string {
|
|
78
|
-
|
|
79
|
-
|
|
85
|
+
const cssBlocks: string[] = [];
|
|
86
|
+
const fluidActive =
|
|
87
|
+
responsiveScales?.enabled === true && responsiveScales?.mode === 'fluid';
|
|
88
|
+
|
|
89
|
+
// Build base :root block: user-defined variables + (in fluid mode) --site-margin.
|
|
90
|
+
// In fluid mode user variables get clamp() values directly on :root rather
|
|
91
|
+
// than @media overrides — so we run them through buildFluidPropertyValue here.
|
|
92
|
+
const fluidRange = responsiveScales?.fluidRange ?? DEFAULT_FLUID_RANGE;
|
|
93
|
+
const fluidBaseRef = responsiveScales?.baseReference || 16;
|
|
94
|
+
const smallestBp = fluidActive ? getSmallestBreakpointName(breakpoints) : null;
|
|
95
|
+
|
|
96
|
+
const baseVars: string[] = (config.variables ?? []).map(v => {
|
|
97
|
+
if (fluidActive && smallestBp && v.type !== 'none') {
|
|
98
|
+
const categoryScales = responsiveScales?.[v.type] as BreakpointScales | undefined;
|
|
99
|
+
const scale = categoryScales?.[smallestBp];
|
|
100
|
+
if (scale != null && scale !== 1) {
|
|
101
|
+
const fluid = buildFluidPropertyValue(
|
|
102
|
+
v.value,
|
|
103
|
+
scale,
|
|
104
|
+
fluidRange.min,
|
|
105
|
+
fluidRange.max,
|
|
106
|
+
fluidBaseRef
|
|
107
|
+
);
|
|
108
|
+
if (fluid) return ` ${v.cssVar}: ${fluid};`;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return ` ${v.cssVar}: ${v.value};`;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (fluidActive) {
|
|
115
|
+
const siteMargin = responsiveScales?.siteMargin ?? DEFAULT_SITE_MARGIN;
|
|
116
|
+
baseVars.push(` --site-margin: ${buildSiteMarginClamp(siteMargin, fluidRange)};`);
|
|
80
117
|
}
|
|
81
118
|
|
|
82
|
-
|
|
119
|
+
if (baseVars.length === 0) {
|
|
120
|
+
return '';
|
|
121
|
+
}
|
|
83
122
|
|
|
84
|
-
// Base :root block with all variables
|
|
85
|
-
const baseVars = config.variables.map(v => ` ${v.cssVar}: ${v.value};`);
|
|
86
123
|
cssBlocks.push(`:root {\n${baseVars.join('\n')}\n}`);
|
|
87
124
|
|
|
88
|
-
// Generate @media blocks for responsive scaling
|
|
89
|
-
|
|
125
|
+
// Generate @media blocks for responsive scaling — only in breakpoints mode.
|
|
126
|
+
// In fluid mode all scaling is encoded in clamp() on the base :root block.
|
|
127
|
+
const userVariables = config.variables ?? [];
|
|
128
|
+
if (breakpoints && responsiveScales?.enabled && !fluidActive && userVariables.length > 0) {
|
|
90
129
|
const baseRef = responsiveScales.baseReference || 16;
|
|
91
130
|
|
|
92
131
|
// Sort breakpoints by value descending (largest first)
|
|
@@ -96,7 +135,7 @@ export function generateVariablesCSS(
|
|
|
96
135
|
for (const [bpName, bpEntry] of sortedBreakpoints) {
|
|
97
136
|
const scaledVars: string[] = [];
|
|
98
137
|
|
|
99
|
-
for (const variable of
|
|
138
|
+
for (const variable of userVariables) {
|
|
100
139
|
// Per-variable override takes priority — stored string is the CSS value
|
|
101
140
|
if (variable.scales && variable.scales[bpName]) {
|
|
102
141
|
const overrideValue = variable.scales[bpName];
|
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
|
|
@@ -341,5 +341,168 @@ describe('FileSystemCMSProvider', () => {
|
|
|
341
341
|
const items = await provider.getItems('valid-collection');
|
|
342
342
|
expect(items).toEqual([]);
|
|
343
343
|
});
|
|
344
|
+
|
|
345
|
+
it('should reject filenames ending in reserved .draft suffix', async () => {
|
|
346
|
+
const item = { _id: '99', _filename: 'foo.draft', title: 'Sneaky', slug: 'foo' };
|
|
347
|
+
await expect(provider.saveItem('blog-posts', item))
|
|
348
|
+
.rejects.toThrow(/\.draft.*reserved/);
|
|
349
|
+
await expect(provider.deleteItem('blog-posts', 'foo.draft'))
|
|
350
|
+
.rejects.toThrow(/\.draft.*reserved/);
|
|
351
|
+
await expect(provider.getItemByFilename('blog-posts', 'foo.draft'))
|
|
352
|
+
.rejects.toThrow(/\.draft.*reserved/);
|
|
353
|
+
});
|
|
344
354
|
});
|
|
355
|
+
|
|
356
|
+
describe('drafts', () => {
|
|
357
|
+
it('saveDraft writes a sibling .draft.json file and getDraft reads it back', async () => {
|
|
358
|
+
const draft = {
|
|
359
|
+
_id: '1',
|
|
360
|
+
_filename: 'hello-world',
|
|
361
|
+
title: 'Hello World — WIP edits',
|
|
362
|
+
slug: 'hello-world',
|
|
363
|
+
content: 'Updated content (not yet published)',
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
await provider.saveDraft('blog-posts', draft);
|
|
367
|
+
|
|
368
|
+
expect(existsSync(join(CMS_DIR, 'blog-posts', 'hello-world.draft.json'))).toBe(true);
|
|
369
|
+
|
|
370
|
+
const read = await provider.getDraft('blog-posts', 'hello-world');
|
|
371
|
+
expect(read?.title).toBe('Hello World — WIP edits');
|
|
372
|
+
expect(read?._isDraft).toBe(true);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('drafts do not appear in published-only queries (getItems, getItemByFilename)', async () => {
|
|
376
|
+
// Pre-existing item: hello-world (published). Add a draft for it.
|
|
377
|
+
await provider.saveDraft('blog-posts', {
|
|
378
|
+
_id: '1',
|
|
379
|
+
_filename: 'hello-world',
|
|
380
|
+
title: 'Draft title',
|
|
381
|
+
slug: 'hello-world',
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Add a draft-only item (no published file)
|
|
385
|
+
await provider.saveDraft('blog-posts', {
|
|
386
|
+
_id: 'new-1',
|
|
387
|
+
_filename: 'brand-new',
|
|
388
|
+
title: 'Brand new draft',
|
|
389
|
+
slug: 'brand-new',
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const items = await provider.getItems('blog-posts');
|
|
393
|
+
// Still only the two original published items
|
|
394
|
+
expect(items).toHaveLength(2);
|
|
395
|
+
expect(items.find(i => i._filename === 'brand-new')).toBeUndefined();
|
|
396
|
+
// hello-world's published title is unchanged
|
|
397
|
+
expect(items.find(i => i._filename === 'hello-world')?.title).toBe('Hello World');
|
|
398
|
+
|
|
399
|
+
const single = await provider.getItemByFilename('blog-posts', 'brand-new');
|
|
400
|
+
expect(single).toBeNull();
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it('getAllDrafts returns drafts with _isDraft transient flag', async () => {
|
|
404
|
+
await provider.saveDraft('blog-posts', {
|
|
405
|
+
_id: '1', _filename: 'hello-world', title: 'WIP', slug: 'hello-world',
|
|
406
|
+
});
|
|
407
|
+
await provider.saveDraft('blog-posts', {
|
|
408
|
+
_id: 'new', _filename: 'brand-new', title: 'New', slug: 'brand-new',
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
const drafts = await provider.getAllDrafts('blog-posts');
|
|
412
|
+
expect(drafts).toHaveLength(2);
|
|
413
|
+
expect(drafts.every(d => d._isDraft === true)).toBe(true);
|
|
414
|
+
expect(drafts.map(d => d._filename).sort()).toEqual(['brand-new', 'hello-world']);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('hasDraft reflects whether a draft file exists', async () => {
|
|
418
|
+
expect(await provider.hasDraft('blog-posts', 'hello-world')).toBe(false);
|
|
419
|
+
await provider.saveDraft('blog-posts', {
|
|
420
|
+
_id: '1', _filename: 'hello-world', title: 'WIP', slug: 'hello-world',
|
|
421
|
+
});
|
|
422
|
+
expect(await provider.hasDraft('blog-posts', 'hello-world')).toBe(true);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('discardDraft removes the draft file and is a no-op when none exists', async () => {
|
|
426
|
+
await expect(provider.discardDraft('blog-posts', 'hello-world')).resolves.toBeUndefined();
|
|
427
|
+
|
|
428
|
+
await provider.saveDraft('blog-posts', {
|
|
429
|
+
_id: '1', _filename: 'hello-world', title: 'WIP', slug: 'hello-world',
|
|
430
|
+
});
|
|
431
|
+
expect(existsSync(join(CMS_DIR, 'blog-posts', 'hello-world.draft.json'))).toBe(true);
|
|
432
|
+
|
|
433
|
+
await provider.discardDraft('blog-posts', 'hello-world');
|
|
434
|
+
expect(existsSync(join(CMS_DIR, 'blog-posts', 'hello-world.draft.json'))).toBe(false);
|
|
435
|
+
// Published file is untouched
|
|
436
|
+
expect(existsSync(join(CMS_DIR, 'blog-posts', 'hello-world.json'))).toBe(true);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('publishDraft promotes draft to published and removes the draft file', async () => {
|
|
440
|
+
await provider.saveDraft('blog-posts', {
|
|
441
|
+
_id: '1',
|
|
442
|
+
_filename: 'hello-world',
|
|
443
|
+
title: 'Updated Title',
|
|
444
|
+
slug: 'hello-world',
|
|
445
|
+
content: 'New content',
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
const result = await provider.publishDraft('blog-posts', 'hello-world');
|
|
449
|
+
expect(result.title).toBe('Updated Title');
|
|
450
|
+
|
|
451
|
+
// Published file now has the new content
|
|
452
|
+
const item = await provider.getItemByFilename('blog-posts', 'hello-world');
|
|
453
|
+
expect(item?.title).toBe('Updated Title');
|
|
454
|
+
expect(item?.content).toBe('New content');
|
|
455
|
+
|
|
456
|
+
// Draft file is gone
|
|
457
|
+
expect(existsSync(join(CMS_DIR, 'blog-posts', 'hello-world.draft.json'))).toBe(false);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('publishDraft of a draft-only item creates the published file', async () => {
|
|
461
|
+
await provider.saveDraft('blog-posts', {
|
|
462
|
+
_id: 'new', _filename: 'brand-new', title: 'New post', slug: 'brand-new',
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
const result = await provider.publishDraft('blog-posts', 'brand-new');
|
|
466
|
+
expect(result._filename).toBe('brand-new');
|
|
467
|
+
|
|
468
|
+
const items = await provider.getItems('blog-posts');
|
|
469
|
+
expect(items.find(i => i._filename === 'brand-new')?.title).toBe('New post');
|
|
470
|
+
expect(existsSync(join(CMS_DIR, 'blog-posts', 'brand-new.draft.json'))).toBe(false);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it('publishDraft throws when no draft exists', async () => {
|
|
474
|
+
await expect(provider.publishDraft('blog-posts', 'hello-world'))
|
|
475
|
+
.rejects.toThrow(/No draft to publish/);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('deleteItem removes both published and draft files', async () => {
|
|
479
|
+
await provider.saveDraft('blog-posts', {
|
|
480
|
+
_id: '1', _filename: 'hello-world', title: 'WIP', slug: 'hello-world',
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
await provider.deleteItem('blog-posts', 'hello-world');
|
|
484
|
+
|
|
485
|
+
expect(existsSync(join(CMS_DIR, 'blog-posts', 'hello-world.json'))).toBe(false);
|
|
486
|
+
expect(existsSync(join(CMS_DIR, 'blog-posts', 'hello-world.draft.json'))).toBe(false);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it('saveItem strips transient _isDraft / _hasDraft fields before writing', async () => {
|
|
490
|
+
const item = {
|
|
491
|
+
_id: '1',
|
|
492
|
+
_filename: 'hello-world',
|
|
493
|
+
title: 'Hello',
|
|
494
|
+
slug: 'hello-world',
|
|
495
|
+
_isDraft: true,
|
|
496
|
+
_hasDraft: true,
|
|
497
|
+
};
|
|
498
|
+
await provider.saveItem('blog-posts', item as any);
|
|
499
|
+
|
|
500
|
+
const { readFile } = await import('fs/promises');
|
|
501
|
+
const raw = await readFile(join(CMS_DIR, 'blog-posts', 'hello-world.json'), 'utf-8');
|
|
502
|
+
const parsed = JSON.parse(raw);
|
|
503
|
+
expect(parsed._isDraft).toBeUndefined();
|
|
504
|
+
expect(parsed._hasDraft).toBeUndefined();
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
|
|
345
508
|
});
|