meno-core 1.0.45 → 1.0.47
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 +211 -124
- 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-TVH3TC2T.js → chunk-47UNLQUU.js} +6 -6
- package/dist/chunks/{chunk-F7MA62WG.js → chunk-BCLGRZ3U.js} +5 -5
- package/dist/chunks/{chunk-F7MA62WG.js.map → chunk-BCLGRZ3U.js.map} +2 -2
- 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-BZQKEJQY.js → chunk-FGUZOYJX.js} +49 -30
- package/dist/chunks/chunk-FGUZOYJX.js.map +7 -0
- 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-LJFB5EBT.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/{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 +7 -5
- package/dist/lib/client/index.js.map +2 -2
- package/dist/lib/server/index.js +631 -208
- 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/core/ComponentBuilder.test.ts +21 -0
- package/lib/client/core/ComponentBuilder.ts +8 -1
- package/lib/client/templateEngine.test.ts +64 -0
- package/lib/server/astro/astroEmitHelpers.ts +23 -0
- package/lib/server/astro/cmsPageEmitter.ts +46 -3
- package/lib/server/astro/componentEmitter.test.ts +59 -0
- package/lib/server/astro/componentEmitter.ts +53 -12
- package/lib/server/astro/cssCollector.ts +58 -11
- package/lib/server/astro/nodeToAstro.test.ts +397 -5
- package/lib/server/astro/nodeToAstro.ts +494 -65
- package/lib/server/astro/pageEmitter.ts +46 -3
- 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.test.ts +3 -2
- package/lib/server/ssr/htmlGenerator.ts +6 -1
- package/lib/server/ssr/imageMetadata.ts +15 -9
- package/lib/server/ssr/jsCollector.ts +2 -2
- package/lib/server/ssr/ssrRenderer.test.ts +79 -0
- package/lib/server/ssr/ssrRenderer.ts +35 -20
- 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/lib/shared/styleNodeUtils.test.ts +47 -1
- package/lib/shared/styleNodeUtils.ts +7 -7
- package/package.json +1 -1
- package/dist/chunks/chunk-BZQKEJQY.js.map +0 -7
- /package/dist/chunks/{chunk-TVH3TC2T.js.map → chunk-47UNLQUU.js.map} +0 -0
- /package/dist/chunks/{chunk-OUNJ76QM.js.map → chunk-LJFB5EBT.js.map} +0 -0
- /package/dist/chunks/{chunk-WQSG5WHC.js.map → chunk-ZTKHJQ2Z.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
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { RAW_HTML_PREFIX } from '../../shared/constants';
|
|
2
|
+
|
|
3
|
+
export function stripRawHtmlPrefixDeep<T>(value: T): T {
|
|
4
|
+
if (typeof value === 'string') {
|
|
5
|
+
return (value.startsWith(RAW_HTML_PREFIX) ? value.slice(RAW_HTML_PREFIX.length) : value) as T;
|
|
6
|
+
}
|
|
7
|
+
if (Array.isArray(value)) {
|
|
8
|
+
return value.map((item) => stripRawHtmlPrefixDeep(item)) as unknown as T;
|
|
9
|
+
}
|
|
10
|
+
if (value !== null && typeof value === 'object') {
|
|
11
|
+
const out: Record<string, unknown> = {};
|
|
12
|
+
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
13
|
+
out[k] = stripRawHtmlPrefixDeep(v);
|
|
14
|
+
}
|
|
15
|
+
return out as T;
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Ensure a component name starts with an uppercase letter so Astro treats it as a component tag, not an HTML element. */
|
|
21
|
+
export function astroComponentName(name: string): string {
|
|
22
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
23
|
+
}
|
|
@@ -10,6 +10,11 @@ import { nodeToAstro, type AstroEmitContext } from './nodeToAstro';
|
|
|
10
10
|
import { transformCMSTemplate } from './templateTransformer';
|
|
11
11
|
import type { ImageMetadataMap } from '../ssr/imageMetadata';
|
|
12
12
|
import type { SlugMap } from '../../shared/slugTranslator';
|
|
13
|
+
import type { ResponsiveScales } from '../../shared/responsiveScaling';
|
|
14
|
+
import type { InteractiveStyles } from '../../shared/types/styles';
|
|
15
|
+
import type { RemConversionConfig } from '../../shared/pxToRem';
|
|
16
|
+
import { generateAllInteractiveCSS } from '../../shared/cssGeneration';
|
|
17
|
+
import { astroComponentName } from './astroEmitHelpers';
|
|
13
18
|
|
|
14
19
|
// ---------------------------------------------------------------------------
|
|
15
20
|
// Types
|
|
@@ -46,6 +51,8 @@ export interface CMSPageEmitOptions {
|
|
|
46
51
|
pageName: string;
|
|
47
52
|
/** Breakpoint config for responsive Tailwind classes */
|
|
48
53
|
breakpoints?: BreakpointConfig;
|
|
54
|
+
/** Responsive scales config for auto-scaling at breakpoints */
|
|
55
|
+
responsiveScales?: ResponsiveScales;
|
|
49
56
|
/** Image metadata map for responsive image generation */
|
|
50
57
|
imageMetadataMap?: ImageMetadataMap;
|
|
51
58
|
/** Internationalization config */
|
|
@@ -56,6 +63,14 @@ export interface CMSPageEmitOptions {
|
|
|
56
63
|
slugMappings?: SlugMap[];
|
|
57
64
|
/** Image format: 'webp' uses plain <img>, 'avif' uses <picture> */
|
|
58
65
|
imageFormat?: 'webp' | 'avif';
|
|
66
|
+
/**
|
|
67
|
+
* Raw-HTML slice → processed HTML captured during the template's SSR pass.
|
|
68
|
+
* Ensures `<Fragment set:html>` matches SSR output for rich-text content
|
|
69
|
+
* (image rewriting, component expansion, link localization).
|
|
70
|
+
*/
|
|
71
|
+
processedRawHtml?: Map<string, string>;
|
|
72
|
+
/** Rem conversion config for interactive CSS generation */
|
|
73
|
+
remConfig?: RemConversionConfig;
|
|
59
74
|
}
|
|
60
75
|
|
|
61
76
|
// ---------------------------------------------------------------------------
|
|
@@ -63,7 +78,12 @@ export interface CMSPageEmitOptions {
|
|
|
63
78
|
// ---------------------------------------------------------------------------
|
|
64
79
|
|
|
65
80
|
function escapeTemplateLiteral(s: string): string {
|
|
66
|
-
return s
|
|
81
|
+
return s
|
|
82
|
+
.replace(/\\/g, '\\\\')
|
|
83
|
+
.replace(/`/g, '\\`')
|
|
84
|
+
.replace(/\$\{/g, '\\${')
|
|
85
|
+
.replace(/\u2028/g, '\\u2028')
|
|
86
|
+
.replace(/\u2029/g, '\\u2029');
|
|
67
87
|
}
|
|
68
88
|
|
|
69
89
|
function escapeJSX(s: string): string {
|
|
@@ -247,10 +267,13 @@ export function emitCMSPage(options: CMSPageEmitOptions): string {
|
|
|
247
267
|
ssrFallbacks,
|
|
248
268
|
pageName,
|
|
249
269
|
breakpoints: breakpointsOpt,
|
|
270
|
+
responsiveScales,
|
|
250
271
|
imageMetadataMap,
|
|
251
272
|
i18nConfig,
|
|
252
273
|
isMultiLocale,
|
|
253
274
|
slugMappings,
|
|
275
|
+
processedRawHtml,
|
|
276
|
+
remConfig,
|
|
254
277
|
} = options;
|
|
255
278
|
|
|
256
279
|
const breakpoints = breakpointsOpt ?? DEFAULT_BREAKPOINTS;
|
|
@@ -289,6 +312,7 @@ export function emitCMSPage(options: CMSPageEmitOptions): string {
|
|
|
289
312
|
fileType: 'page',
|
|
290
313
|
fileName: pageName,
|
|
291
314
|
breakpoints,
|
|
315
|
+
responsiveScales,
|
|
292
316
|
imageMetadataMap,
|
|
293
317
|
locale,
|
|
294
318
|
cmsMode: true,
|
|
@@ -298,6 +322,10 @@ export function emitCMSPage(options: CMSPageEmitOptions): string {
|
|
|
298
322
|
slugMappings,
|
|
299
323
|
i18nDefaultLocale: i18nConfig.defaultLocale,
|
|
300
324
|
imageFormat: options.imageFormat,
|
|
325
|
+
processedRawHtml,
|
|
326
|
+
imageImports: new Map<string, string>(),
|
|
327
|
+
fileDepth,
|
|
328
|
+
collectedInteractiveStyles: new Map<string, InteractiveStyles>(),
|
|
301
329
|
};
|
|
302
330
|
|
|
303
331
|
// Emit the template body
|
|
@@ -308,11 +336,21 @@ export function emitCMSPage(options: CMSPageEmitOptions): string {
|
|
|
308
336
|
importLines.push(`import { getCollection } from 'astro:content';`);
|
|
309
337
|
importLines.push(`import BaseLayout from '${layoutImportPath}';`);
|
|
310
338
|
|
|
339
|
+
// Static image imports (astro:assets <Picture>). Map size is the single
|
|
340
|
+
// source of truth — a separate boolean would be lost by child-ctx spreads.
|
|
341
|
+
if (ctx.imageImports && ctx.imageImports.size > 0) {
|
|
342
|
+
importLines.push(`import { Picture } from 'astro:assets';`);
|
|
343
|
+
const sortedImages = Array.from(ctx.imageImports.entries()).sort(([a], [b]) => a.localeCompare(b));
|
|
344
|
+
for (const [varName, importPath] of sortedImages) {
|
|
345
|
+
importLines.push(`import ${varName} from '${importPath}';`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
311
349
|
// Sort component imports alphabetically
|
|
312
350
|
const componentImports = Array.from(ctx.imports).sort();
|
|
313
351
|
for (const comp of componentImports) {
|
|
314
352
|
const path = componentImportPath(fileDepth, comp);
|
|
315
|
-
importLines.push(`import ${comp} from '${path}';`);
|
|
353
|
+
importLines.push(`import ${astroComponentName(comp)} from '${path}';`);
|
|
316
354
|
}
|
|
317
355
|
|
|
318
356
|
// Build getStaticPaths
|
|
@@ -342,6 +380,11 @@ export function emitCMSPage(options: CMSPageEmitOptions): string {
|
|
|
342
380
|
return v ?? '';
|
|
343
381
|
}`;
|
|
344
382
|
|
|
383
|
+
// Generate interactive style block for page-level interactive styles
|
|
384
|
+
const interactiveStyleSection = ctx.collectedInteractiveStyles!.size > 0
|
|
385
|
+
? `\n<style is:global>\n${generateAllInteractiveCSS(ctx.collectedInteractiveStyles!, breakpoints, remConfig, responsiveScales)}\n</style>\n`
|
|
386
|
+
: '';
|
|
387
|
+
|
|
345
388
|
return `---
|
|
346
389
|
${importLines.join('\n')}
|
|
347
390
|
|
|
@@ -361,7 +404,7 @@ ${resolverHelper}
|
|
|
361
404
|
<div id="root">
|
|
362
405
|
${templateBody} </div>
|
|
363
406
|
</BaseLayout>
|
|
364
|
-
`;
|
|
407
|
+
${interactiveStyleSection}`;
|
|
365
408
|
}
|
|
366
409
|
|
|
367
410
|
/**
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import { emitAstroComponent } from './componentEmitter';
|
|
3
|
+
import type { ComponentDefinition } from '../../shared/types';
|
|
4
|
+
|
|
5
|
+
function makeComponent(overrides: Partial<ComponentDefinition['component']> = {}): ComponentDefinition {
|
|
6
|
+
return {
|
|
7
|
+
component: {
|
|
8
|
+
interface: {},
|
|
9
|
+
structure: { type: 'node', tag: 'div', children: [] } as unknown as ComponentDefinition['component']['structure'],
|
|
10
|
+
...overrides,
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('emitAstroComponent — RAW_HTML_PREFIX stripping', () => {
|
|
16
|
+
test('strips marker from rich-text string default in frontmatter', () => {
|
|
17
|
+
const def = makeComponent({
|
|
18
|
+
interface: {
|
|
19
|
+
text2: {
|
|
20
|
+
type: 'rich-text',
|
|
21
|
+
default: '<!--MENO_RAW_HTML-->SEO<br>Audit Blueprint',
|
|
22
|
+
} as unknown as import('../../shared/types').PropDefinition,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
const out = emitAstroComponent('SeoAudit', def, {});
|
|
26
|
+
expect(out).not.toContain('MENO_RAW_HTML');
|
|
27
|
+
expect(out).toContain('text2 = "SEO<br>Audit Blueprint"');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('strips marker from i18n default values', () => {
|
|
31
|
+
const def = makeComponent({
|
|
32
|
+
interface: {
|
|
33
|
+
text2: {
|
|
34
|
+
type: 'rich-text',
|
|
35
|
+
default: {
|
|
36
|
+
_i18n: true,
|
|
37
|
+
en: '<!--MENO_RAW_HTML-->SEO<br>Audit Blueprint',
|
|
38
|
+
pl: '<!--MENO_RAW_HTML-->Audyt SEO',
|
|
39
|
+
},
|
|
40
|
+
} as unknown as import('../../shared/types').PropDefinition,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
const out = emitAstroComponent('SeoAudit', def, {});
|
|
44
|
+
expect(out).not.toContain('MENO_RAW_HTML');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('leaves non-rich-text string defaults unchanged', () => {
|
|
48
|
+
const def = makeComponent({
|
|
49
|
+
interface: {
|
|
50
|
+
title: {
|
|
51
|
+
type: 'string',
|
|
52
|
+
default: 'Hello world',
|
|
53
|
+
} as unknown as import('../../shared/types').PropDefinition,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
const out = emitAstroComponent('Heading', def, {});
|
|
57
|
+
expect(out).toContain('title = "Hello world"');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -12,6 +12,11 @@ import type { BasePropDefinition, ListPropDefinition, LinkPropValue } from '../.
|
|
|
12
12
|
import type { BreakpointConfig } from '../../shared/breakpoints';
|
|
13
13
|
import { DEFAULT_BREAKPOINTS } from '../../shared/breakpoints';
|
|
14
14
|
import { nodeToAstro, type AstroEmitContext } from './nodeToAstro';
|
|
15
|
+
import type { ResponsiveScales } from '../../shared/responsiveScaling';
|
|
16
|
+
import { stripRawHtmlPrefixDeep, astroComponentName } from './astroEmitHelpers';
|
|
17
|
+
import type { InteractiveStyles } from '../../shared/types/styles';
|
|
18
|
+
import type { RemConversionConfig } from '../../shared/pxToRem';
|
|
19
|
+
import { generateAllInteractiveCSS } from '../../shared/cssGeneration';
|
|
15
20
|
|
|
16
21
|
// ---------------------------------------------------------------------------
|
|
17
22
|
// Helpers
|
|
@@ -49,7 +54,7 @@ function propDefToTSType(def: PropDefinition): string {
|
|
|
49
54
|
*/
|
|
50
55
|
function formatDefault(def: PropDefinition): string | null {
|
|
51
56
|
if (!('default' in def) || def.default === undefined) return null;
|
|
52
|
-
const val = def.default;
|
|
57
|
+
const val = stripRawHtmlPrefixDeep(def.default);
|
|
53
58
|
|
|
54
59
|
if (typeof val === 'string') return JSON.stringify(val);
|
|
55
60
|
if (typeof val === 'number' || typeof val === 'boolean') return String(val);
|
|
@@ -129,7 +134,9 @@ export function emitAstroComponent(
|
|
|
129
134
|
def: ComponentDefinition,
|
|
130
135
|
allComponents: Record<string, ComponentDefinition>,
|
|
131
136
|
breakpoints: BreakpointConfig = DEFAULT_BREAKPOINTS,
|
|
132
|
-
defaultLocale: string = 'en'
|
|
137
|
+
defaultLocale: string = 'en',
|
|
138
|
+
responsiveScales?: ResponsiveScales,
|
|
139
|
+
remConfig?: RemConversionConfig
|
|
133
140
|
): string {
|
|
134
141
|
const comp = def.component;
|
|
135
142
|
const propDefs = comp.interface || {};
|
|
@@ -152,7 +159,11 @@ export function emitAstroComponent(
|
|
|
152
159
|
fileType: 'component',
|
|
153
160
|
fileName: name,
|
|
154
161
|
breakpoints,
|
|
162
|
+
responsiveScales,
|
|
155
163
|
defaultLocale,
|
|
164
|
+
imageImports: new Map<string, string>(),
|
|
165
|
+
fileDepth: 0, // components live at src/components/
|
|
166
|
+
collectedInteractiveStyles: new Map<string, InteractiveStyles>(),
|
|
156
167
|
};
|
|
157
168
|
|
|
158
169
|
// Emit the template body
|
|
@@ -162,15 +173,25 @@ export function emitAstroComponent(
|
|
|
162
173
|
templateBody = mergeClassNameOntoRoot(templateBody);
|
|
163
174
|
|
|
164
175
|
// Build frontmatter (includes class prop for instance style support)
|
|
165
|
-
const frontmatter = buildFrontmatter(
|
|
176
|
+
const frontmatter = buildFrontmatter(
|
|
177
|
+
name,
|
|
178
|
+
propDefs,
|
|
179
|
+
ctx.imports,
|
|
180
|
+
ctx.dynamicTags,
|
|
181
|
+
ctx.needsI18nResolver ? defaultLocale : undefined,
|
|
182
|
+
ctx.imageImports
|
|
183
|
+
);
|
|
166
184
|
|
|
167
185
|
// Build style/script sections
|
|
168
186
|
const styleSection = comp.css ? `\n<style>\n${comp.css}\n</style>\n` : '';
|
|
187
|
+
const interactiveStyleSection = ctx.collectedInteractiveStyles!.size > 0
|
|
188
|
+
? `\n<style is:global>\n${generateAllInteractiveCSS(ctx.collectedInteractiveStyles!, breakpoints, remConfig, responsiveScales)}\n</style>\n`
|
|
189
|
+
: '';
|
|
169
190
|
const scriptSection = comp.javascript
|
|
170
191
|
? buildScriptSection(comp.javascript, comp, propDefs)
|
|
171
192
|
: '';
|
|
172
193
|
|
|
173
|
-
return `---\n${frontmatter}---\n${templateBody}${styleSection}${scriptSection}`;
|
|
194
|
+
return `---\n${frontmatter}---\n${templateBody}${styleSection}${interactiveStyleSection}${scriptSection}`;
|
|
174
195
|
}
|
|
175
196
|
|
|
176
197
|
/**
|
|
@@ -181,13 +202,24 @@ function buildFrontmatter(
|
|
|
181
202
|
propDefs: Record<string, PropDefinition>,
|
|
182
203
|
imports: Set<string>,
|
|
183
204
|
dynamicTags?: Map<string, string>,
|
|
184
|
-
i18nDefaultLocale?: string
|
|
205
|
+
i18nDefaultLocale?: string,
|
|
206
|
+
imageImports?: Map<string, string>
|
|
185
207
|
): string {
|
|
186
208
|
const lines: string[] = [];
|
|
187
209
|
|
|
188
210
|
// Component imports
|
|
189
211
|
for (const imp of Array.from(imports).sort()) {
|
|
190
|
-
lines.push(`import ${imp} from './${imp}.astro';`);
|
|
212
|
+
lines.push(`import ${astroComponentName(imp)} from './${imp}.astro';`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Static image imports (astro:assets <Picture>). Map size is the single
|
|
216
|
+
// source of truth — a separate boolean would be lost by child-ctx spreads.
|
|
217
|
+
if (imageImports && imageImports.size > 0) {
|
|
218
|
+
lines.push(`import { Picture } from 'astro:assets';`);
|
|
219
|
+
const sortedImages = Array.from(imageImports.entries()).sort(([a], [b]) => a.localeCompare(b));
|
|
220
|
+
for (const [varName, importPath] of sortedImages) {
|
|
221
|
+
lines.push(`import ${varName} from '${importPath}';`);
|
|
222
|
+
}
|
|
191
223
|
}
|
|
192
224
|
|
|
193
225
|
if (lines.length > 0) lines.push('');
|
|
@@ -314,16 +346,25 @@ function transformDefineVarsJS(js: string, varNames: string[]): string {
|
|
|
314
346
|
|
|
315
347
|
/**
|
|
316
348
|
* Build the script section with proper el/props initialization.
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
349
|
+
* Captures el immediately via document.currentScript.previousElementSibling,
|
|
350
|
+
* then defers actual JS execution until DOMContentLoaded so that external
|
|
351
|
+
* libraries (e.g. Swiper loaded with defer) are available.
|
|
320
352
|
*/
|
|
321
353
|
function buildScriptSection(
|
|
322
354
|
js: string,
|
|
323
355
|
comp: StructuredComponentDefinition,
|
|
324
356
|
propDefs: Record<string, PropDefinition>
|
|
325
357
|
): string {
|
|
326
|
-
|
|
358
|
+
// Capture el immediately (document.currentScript is only valid during script execution)
|
|
359
|
+
// but defer the component init until DOMContentLoaded so deferred libraries are ready
|
|
360
|
+
// Remove whitespace-only text nodes so Astro's formatted HTML matches SSR's minified output.
|
|
361
|
+
// Without this, scripts that use firstChild/childNodes pick up whitespace between elements.
|
|
362
|
+
const cleanTextNodes = `(function _w(n){var c=n.firstChild,x;while(c){x=c.nextSibling;if(c.nodeType===3){if(!c.textContent.trim())n.removeChild(c)}else if(c.nodeType===1){if(c.tagName==='SCRIPT')n.removeChild(c);else _w(c)}c=x}})(el);`;
|
|
363
|
+
|
|
364
|
+
const deferWrapper = (innerJS: string) =>
|
|
365
|
+
`var el = document.currentScript.previousElementSibling;\n` +
|
|
366
|
+
`function __init__() {\n${cleanTextNodes}\n${innerJS}\n}\n` +
|
|
367
|
+
`if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', __init__); } else { __init__(); }`;
|
|
327
368
|
|
|
328
369
|
if (comp.defineVars) {
|
|
329
370
|
const vars = comp.defineVars === true
|
|
@@ -333,9 +374,9 @@ function buildScriptSection(
|
|
|
333
374
|
if (vars.length > 0) {
|
|
334
375
|
const transformedJS = transformDefineVarsJS(js, vars);
|
|
335
376
|
const defineVarsObj = `{ ${vars.join(', ')} }`;
|
|
336
|
-
return `\n<script define:vars={${defineVarsObj}}>\n
|
|
377
|
+
return `\n<script define:vars={${defineVarsObj}}>\n(function(){\n${deferWrapper(transformedJS)}\n})();\n</script>\n`;
|
|
337
378
|
}
|
|
338
379
|
}
|
|
339
380
|
|
|
340
|
-
return `\n<script is:inline>\n
|
|
381
|
+
return `\n<script is:inline>\n(function(){\n${deferWrapper(js)}\n})();\n</script>\n`;
|
|
341
382
|
}
|
|
@@ -14,6 +14,8 @@ import type {
|
|
|
14
14
|
import type { BreakpointConfig } from '../../shared/breakpoints';
|
|
15
15
|
import { DEFAULT_BREAKPOINTS } from '../../shared/breakpoints';
|
|
16
16
|
import { propertyToTailwind } from './tailwindMapper';
|
|
17
|
+
import type { ResponsiveScales, CSSPropertyType } from '../../shared/responsiveScaling';
|
|
18
|
+
import { getScaleMultiplier, scalePropertyValue } from '../../shared/responsiveScaling';
|
|
17
19
|
|
|
18
20
|
// ---------------------------------------------------------------------------
|
|
19
21
|
// Helpers
|
|
@@ -40,7 +42,8 @@ function isResponsiveStyle(
|
|
|
40
42
|
function collectFromStyle(
|
|
41
43
|
style: StyleObject | ResponsiveStyleObject | undefined,
|
|
42
44
|
classes: Set<string>,
|
|
43
|
-
breakpoints: BreakpointConfig
|
|
45
|
+
breakpoints: BreakpointConfig,
|
|
46
|
+
responsiveScales?: ResponsiveScales
|
|
44
47
|
): void {
|
|
45
48
|
if (!style) return;
|
|
46
49
|
|
|
@@ -48,24 +51,46 @@ function collectFromStyle(
|
|
|
48
51
|
for (const [bp, bpStyle] of Object.entries(style)) {
|
|
49
52
|
if (!bpStyle) continue;
|
|
50
53
|
let prefix = '';
|
|
54
|
+
let bpName: string | undefined;
|
|
51
55
|
if (bp !== 'base') {
|
|
52
56
|
const bpValue = breakpoints[bp]?.breakpoint;
|
|
53
57
|
if (bpValue) {
|
|
54
58
|
prefix = `max-[${bpValue}px]:`;
|
|
59
|
+
bpName = bp;
|
|
55
60
|
}
|
|
56
61
|
}
|
|
57
|
-
collectFromFlatStyle(bpStyle, prefix, classes);
|
|
62
|
+
collectFromFlatStyle(bpStyle, prefix, classes, breakpoints, responsiveScales, bpName);
|
|
58
63
|
}
|
|
59
64
|
} else {
|
|
60
|
-
collectFromFlatStyle(style, '', classes);
|
|
65
|
+
collectFromFlatStyle(style, '', classes, breakpoints, responsiveScales);
|
|
61
66
|
}
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
function collectFromFlatStyle(
|
|
65
70
|
style: StyleObject,
|
|
66
71
|
prefix: string,
|
|
67
|
-
classes: Set<string
|
|
72
|
+
classes: Set<string>,
|
|
73
|
+
breakpoints: BreakpointConfig,
|
|
74
|
+
responsiveScales?: ResponsiveScales,
|
|
75
|
+
sourceBreakpoint?: string
|
|
68
76
|
): void {
|
|
77
|
+
// Only auto-scale when mapping value lives in the base branch (or a flat
|
|
78
|
+
// top-level style). Mappings inside an explicit breakpoint branch already
|
|
79
|
+
// describe the override the author wants — don't re-scale them.
|
|
80
|
+
const shouldAutoScale =
|
|
81
|
+
responsiveScales?.enabled === true && sourceBreakpoint === undefined;
|
|
82
|
+
|
|
83
|
+
const scaleBreakpoints = shouldAutoScale
|
|
84
|
+
? Object.entries(breakpoints)
|
|
85
|
+
.map(([name, cfg]) => ({ name, value: cfg?.breakpoint }))
|
|
86
|
+
.filter((bp): bp is { name: string; value: number } =>
|
|
87
|
+
typeof bp.value === 'number' && bp.value > 0
|
|
88
|
+
)
|
|
89
|
+
.sort((a, b) => b.value - a.value)
|
|
90
|
+
: [];
|
|
91
|
+
|
|
92
|
+
const baseRef = responsiveScales?.baseReference ?? 16;
|
|
93
|
+
|
|
69
94
|
for (const [property, value] of Object.entries(style)) {
|
|
70
95
|
if (!isStyleMapping(value)) continue;
|
|
71
96
|
|
|
@@ -75,6 +100,26 @@ function collectFromFlatStyle(
|
|
|
75
100
|
if (twClass) {
|
|
76
101
|
classes.add(prefix ? `${prefix}${twClass}` : twClass);
|
|
77
102
|
}
|
|
103
|
+
|
|
104
|
+
// Safelist auto-scaled `max-[Npx]:` variants so Tailwind generates
|
|
105
|
+
// CSS for each breakpoint-scaled class the runtime might produce.
|
|
106
|
+
if (shouldAutoScale && responsiveScales) {
|
|
107
|
+
const strValue = String(cssValue);
|
|
108
|
+
if (strValue === '') continue;
|
|
109
|
+
for (const { name: bpName, value: bpPixels } of scaleBreakpoints) {
|
|
110
|
+
const scale = getScaleMultiplier(
|
|
111
|
+
responsiveScales,
|
|
112
|
+
property as CSSPropertyType,
|
|
113
|
+
bpName
|
|
114
|
+
);
|
|
115
|
+
if (scale == null) continue;
|
|
116
|
+
const scaledValue = scalePropertyValue(strValue, baseRef, scale);
|
|
117
|
+
if (scaledValue == null || scaledValue === strValue) continue;
|
|
118
|
+
const scaledClass = propertyToTailwind(property, scaledValue);
|
|
119
|
+
if (!scaledClass) continue;
|
|
120
|
+
classes.add(`max-[${bpPixels}px]:${scaledClass}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
78
123
|
}
|
|
79
124
|
}
|
|
80
125
|
}
|
|
@@ -85,27 +130,28 @@ function collectFromFlatStyle(
|
|
|
85
130
|
function walkNode(
|
|
86
131
|
node: ComponentNode | ComponentNode[] | string | number | null | undefined,
|
|
87
132
|
classes: Set<string>,
|
|
88
|
-
breakpoints: BreakpointConfig
|
|
133
|
+
breakpoints: BreakpointConfig,
|
|
134
|
+
responsiveScales?: ResponsiveScales
|
|
89
135
|
): void {
|
|
90
136
|
if (!node || typeof node === 'string' || typeof node === 'number') return;
|
|
91
137
|
|
|
92
138
|
if (Array.isArray(node)) {
|
|
93
139
|
for (const child of node) {
|
|
94
|
-
walkNode(child, classes, breakpoints);
|
|
140
|
+
walkNode(child, classes, breakpoints, responsiveScales);
|
|
95
141
|
}
|
|
96
142
|
return;
|
|
97
143
|
}
|
|
98
144
|
|
|
99
145
|
// Collect from style
|
|
100
146
|
if ('style' in node && node.style) {
|
|
101
|
-
collectFromStyle(node.style as StyleObject | ResponsiveStyleObject, classes, breakpoints);
|
|
147
|
+
collectFromStyle(node.style as StyleObject | ResponsiveStyleObject, classes, breakpoints, responsiveScales);
|
|
102
148
|
}
|
|
103
149
|
|
|
104
150
|
// Collect from interactive styles
|
|
105
151
|
if ('interactiveStyles' in node && Array.isArray((node as any).interactiveStyles)) {
|
|
106
152
|
for (const rule of (node as any).interactiveStyles) {
|
|
107
153
|
if (rule.style) {
|
|
108
|
-
collectFromStyle(rule.style, classes, breakpoints);
|
|
154
|
+
collectFromStyle(rule.style, classes, breakpoints, responsiveScales);
|
|
109
155
|
}
|
|
110
156
|
}
|
|
111
157
|
}
|
|
@@ -114,7 +160,7 @@ function walkNode(
|
|
|
114
160
|
if ('children' in node && node.children) {
|
|
115
161
|
if (Array.isArray(node.children)) {
|
|
116
162
|
for (const child of node.children) {
|
|
117
|
-
walkNode(child as ComponentNode, classes, breakpoints);
|
|
163
|
+
walkNode(child as ComponentNode, classes, breakpoints, responsiveScales);
|
|
118
164
|
}
|
|
119
165
|
}
|
|
120
166
|
}
|
|
@@ -132,14 +178,15 @@ function walkNode(
|
|
|
132
178
|
*/
|
|
133
179
|
export function collectAllMappingClasses(
|
|
134
180
|
componentDefs: Record<string, ComponentDefinition>,
|
|
135
|
-
breakpoints: BreakpointConfig = DEFAULT_BREAKPOINTS
|
|
181
|
+
breakpoints: BreakpointConfig = DEFAULT_BREAKPOINTS,
|
|
182
|
+
responsiveScales?: ResponsiveScales
|
|
136
183
|
): Set<string> {
|
|
137
184
|
const classes = new Set<string>();
|
|
138
185
|
|
|
139
186
|
for (const def of Object.values(componentDefs)) {
|
|
140
187
|
const structure = def.component?.structure;
|
|
141
188
|
if (structure) {
|
|
142
|
-
walkNode(structure, classes, breakpoints);
|
|
189
|
+
walkNode(structure, classes, breakpoints, responsiveScales);
|
|
143
190
|
}
|
|
144
191
|
}
|
|
145
192
|
|