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.
Files changed (63) hide show
  1. package/build-astro.ts +211 -124
  2. package/dist/bin/cli.js +2 -2
  3. package/dist/build-static.js +7 -7
  4. package/dist/chunks/{chunk-NZTSJS5C.js → chunk-2QK6U5UK.js} +3 -2
  5. package/dist/chunks/{chunk-NZTSJS5C.js.map → chunk-2QK6U5UK.js.map} +2 -2
  6. package/dist/chunks/{chunk-TVH3TC2T.js → chunk-47UNLQUU.js} +6 -6
  7. package/dist/chunks/{chunk-F7MA62WG.js → chunk-BCLGRZ3U.js} +5 -5
  8. package/dist/chunks/{chunk-F7MA62WG.js.map → chunk-BCLGRZ3U.js.map} +2 -2
  9. package/dist/chunks/{chunk-5ZASE4IG.js → chunk-FED5MME6.js} +234 -11
  10. package/dist/chunks/{chunk-5ZASE4IG.js.map → chunk-FED5MME6.js.map} +3 -3
  11. package/dist/chunks/{chunk-BZQKEJQY.js → chunk-FGUZOYJX.js} +49 -30
  12. package/dist/chunks/chunk-FGUZOYJX.js.map +7 -0
  13. package/dist/chunks/{chunk-5Z5VQRTJ.js → chunk-I7YIGZXT.js} +4 -4
  14. package/dist/chunks/{chunk-5Z5VQRTJ.js.map → chunk-I7YIGZXT.js.map} +2 -2
  15. package/dist/chunks/{chunk-OUNJ76QM.js → chunk-LJFB5EBT.js} +5 -5
  16. package/dist/chunks/{chunk-GYF3ABI3.js → chunk-UUA5LEWF.js} +3 -3
  17. package/dist/chunks/{chunk-GYF3ABI3.js.map → chunk-UUA5LEWF.js.map} +2 -2
  18. package/dist/chunks/{chunk-WQSG5WHC.js → chunk-ZTKHJQ2Z.js} +2 -2
  19. package/dist/chunks/{configService-6KTT6GRT.js → configService-DYCUEURL.js} +3 -3
  20. package/dist/chunks/{constants-L5IKLB6U.js → constants-GWBAD66U.js} +2 -2
  21. package/dist/entries/server-router.js +7 -7
  22. package/dist/lib/client/index.js +7 -5
  23. package/dist/lib/client/index.js.map +2 -2
  24. package/dist/lib/server/index.js +631 -208
  25. package/dist/lib/server/index.js.map +3 -3
  26. package/dist/lib/shared/index.js +7 -3
  27. package/dist/lib/shared/index.js.map +2 -2
  28. package/dist/lib/test-utils/index.js +1 -1
  29. package/lib/client/core/ComponentBuilder.test.ts +21 -0
  30. package/lib/client/core/ComponentBuilder.ts +8 -1
  31. package/lib/client/templateEngine.test.ts +64 -0
  32. package/lib/server/astro/astroEmitHelpers.ts +23 -0
  33. package/lib/server/astro/cmsPageEmitter.ts +46 -3
  34. package/lib/server/astro/componentEmitter.test.ts +59 -0
  35. package/lib/server/astro/componentEmitter.ts +53 -12
  36. package/lib/server/astro/cssCollector.ts +58 -11
  37. package/lib/server/astro/nodeToAstro.test.ts +397 -5
  38. package/lib/server/astro/nodeToAstro.ts +494 -65
  39. package/lib/server/astro/pageEmitter.ts +46 -3
  40. package/lib/server/astro/tailwindMapper.test.ts +119 -0
  41. package/lib/server/astro/tailwindMapper.ts +67 -1
  42. package/lib/server/runtime/httpServer.ts +12 -4
  43. package/lib/server/ssr/htmlGenerator.test.ts +3 -2
  44. package/lib/server/ssr/htmlGenerator.ts +6 -1
  45. package/lib/server/ssr/imageMetadata.ts +15 -9
  46. package/lib/server/ssr/jsCollector.ts +2 -2
  47. package/lib/server/ssr/ssrRenderer.test.ts +79 -0
  48. package/lib/server/ssr/ssrRenderer.ts +35 -20
  49. package/lib/shared/constants.ts +1 -0
  50. package/lib/shared/cssGeneration.test.ts +109 -3
  51. package/lib/shared/cssGeneration.ts +98 -13
  52. package/lib/shared/cssNamedColors.ts +47 -0
  53. package/lib/shared/cssProperties.ts +2 -2
  54. package/lib/shared/index.ts +1 -0
  55. package/lib/shared/styleNodeUtils.test.ts +47 -1
  56. package/lib/shared/styleNodeUtils.ts +7 -7
  57. package/package.json +1 -1
  58. package/dist/chunks/chunk-BZQKEJQY.js.map +0 -7
  59. /package/dist/chunks/{chunk-TVH3TC2T.js.map → chunk-47UNLQUU.js.map} +0 -0
  60. /package/dist/chunks/{chunk-OUNJ76QM.js.map → chunk-LJFB5EBT.js.map} +0 -0
  61. /package/dist/chunks/{chunk-WQSG5WHC.js.map → chunk-ZTKHJQ2Z.js.map} +0 -0
  62. /package/dist/chunks/{configService-6KTT6GRT.js.map → configService-DYCUEURL.js.map} +0 -0
  63. /package/dist/chunks/{constants-L5IKLB6U.js.map → constants-GWBAD66U.js.map} +0 -0
package/build-astro.ts CHANGED
@@ -29,11 +29,10 @@ import { isItemDraftForLocale } from "./lib/shared/types";
29
29
  import type { SlugMap } from "./lib/shared/slugTranslator";
30
30
  import { renderPageSSR } from "./lib/server/ssr/ssrRenderer";
31
31
  import { generateThemeColorVariablesCSS, generateVariablesCSS } from "./lib/server/cssGenerator";
32
- import { generateAllInteractiveCSS } from "./lib/shared/cssGeneration";
33
32
  import { colorService } from "./lib/server/services/ColorService";
34
33
  import { variableService } from "./lib/server/services/VariableService";
35
34
  import { configService } from "./lib/server/services/configService";
36
- import { loadBreakpointConfig, loadResponsiveScalesConfig } from "./lib/server/jsonLoader";
35
+ import { loadBreakpointConfig, loadResponsiveScalesConfig, loadIconsConfig } from "./lib/server/jsonLoader";
37
36
  import type { InteractiveStyles } from "./lib/shared/types/styles";
38
37
  import { collectComponentLibraries, filterLibrariesByContext, mergeLibraries, generateLibraryTags } from "./lib/shared/libraryLoader";
39
38
  import { migrateTemplatesDirectory } from "./lib/server/migrateTemplates";
@@ -41,7 +40,8 @@ import { emitAstroComponent } from "./lib/server/astro/componentEmitter";
41
40
  import { emitAstroPage } from "./lib/server/astro/pageEmitter";
42
41
  import { emitCMSPage } from './lib/server/astro/cmsPageEmitter';
43
42
  import { collectAllMappingClasses } from "./lib/server/astro/cssCollector";
44
- import { buildImageMetadataMap } from "./lib/server/ssr/imageMetadata";
43
+ import { buildImageMetadataMap, RESPONSIVE_WIDTHS } from "./lib/server/ssr/imageMetadata";
44
+ import { needsFormHandler, formHandlerScript } from "./lib/client/scripts/formHandler";
45
45
 
46
46
 
47
47
  // ---------------------------------------------------------------------------
@@ -52,19 +52,50 @@ function hashContent(content: string): string {
52
52
  return createHash('sha256').update(content).digest('hex').slice(0, 8);
53
53
  }
54
54
 
55
- function copyDirectory(src: string, dest: string): void {
55
+ function writePageScript(javascript: string | undefined, scriptsDir: string): string[] {
56
+ if (!javascript) return [];
57
+ const hash = hashContent(javascript);
58
+ const scriptFile = `${hash}.js`;
59
+ if (!existsSync(scriptsDir)) {
60
+ mkdirSync(scriptsDir, { recursive: true });
61
+ }
62
+ const fullScriptPath = join(scriptsDir, scriptFile);
63
+ if (!existsSync(fullScriptPath)) {
64
+ writeFileSync(fullScriptPath, javascript, 'utf-8');
65
+ }
66
+ return [`/_scripts/${scriptFile}`];
67
+ }
68
+
69
+ function copyDirectory(
70
+ src: string,
71
+ dest: string,
72
+ filter?: (filename: string) => boolean,
73
+ ): void {
56
74
  if (!existsSync(src)) return;
57
75
  if (!existsSync(dest)) mkdirSync(dest, { recursive: true });
58
76
  const files = readdirSync(src);
59
77
  for (const file of files) {
78
+ if (filter && !filter(file)) continue;
60
79
  const srcPath = join(src, file);
61
80
  const destPath = join(dest, file);
62
81
  const stat = statSync(srcPath);
63
- if (stat.isDirectory()) copyDirectory(srcPath, destPath);
82
+ if (stat.isDirectory()) copyDirectory(srcPath, destPath, filter);
64
83
  else copyFileSync(srcPath, destPath);
65
84
  }
66
85
  }
67
86
 
87
+ // Astro's <Picture> re-derives responsive variants from originals at build
88
+ // time, so the pre-baked -{width}.webp/.avif files and the SSR-only
89
+ // manifest.json are dead weight in the exported project.
90
+ const imageVariantSuffixRe = new RegExp(
91
+ `-(${RESPONSIVE_WIDTHS.join('|')})\\.(webp|avif)$`,
92
+ );
93
+ function shouldCopyImageForAstro(filename: string): boolean {
94
+ if (filename === 'manifest.json') return false;
95
+ if (imageVariantSuffixRe.test(filename)) return false;
96
+ return true;
97
+ }
98
+
68
99
  function isCMSPage(pageData: JSONPage): boolean {
69
100
  return pageData.meta?.source === 'cms' && !!pageData.meta?.cms;
70
101
  }
@@ -208,6 +239,8 @@ interface PageRenderResult {
208
239
  isCMSPage?: boolean;
209
240
  /** SSR fallback HTML for complex nodes (list, locale-list) keyed by element path */
210
241
  ssrFallbackCollector?: Map<string, string>;
242
+ /** Raw-HTML slice → processed HTML captured during SSR (for Astro exporter parity) */
243
+ processedRawHtmlCollector?: Map<string, string>;
211
244
  }
212
245
 
213
246
  interface AstroBuildStats {
@@ -264,10 +297,6 @@ export async function buildAstroProject(
264
297
  projectRoot?: string,
265
298
  outputDir?: string
266
299
  ): Promise<AstroBuildStats> {
267
- const startTime = Date.now();
268
-
269
- console.log('🏗️ Building Astro export...\n');
270
-
271
300
  // ----------------------------------------------------------
272
301
  // 1. Setup: load project configuration
273
302
  // ----------------------------------------------------------
@@ -277,7 +306,6 @@ export async function buildAstroProject(
277
306
  const siteUrl = (projectConfig as { siteUrl?: string }).siteUrl?.replace(/\/$/, '') || '';
278
307
 
279
308
  const i18nConfig = await loadI18nConfig();
280
- console.log(`🌐 Locales: ${i18nConfig.locales.map(l => l.code).join(', ')} (default: ${i18nConfig.defaultLocale})\n`);
281
309
 
282
310
  await migrateTemplatesDirectory();
283
311
 
@@ -286,12 +314,10 @@ export async function buildAstroProject(
286
314
  components.forEach((value, key) => { globalComponents[key] = value; });
287
315
  for (const w of warnings) console.warn(` Warning: ${w}`);
288
316
  for (const e of compErrors) console.error(` Error: ${e}`);
289
- console.log(`Loaded ${components.size} global component(s)\n`);
290
317
 
291
318
  const cmsProvider = new FileSystemCMSProvider(projectPaths.templates(), projectPaths.cms());
292
319
  const cmsService = new CMSService(cmsProvider);
293
320
  await cmsService.initialize();
294
- console.log('CMS service initialized\n');
295
321
 
296
322
  const themeConfig = await colorService.loadThemeConfig();
297
323
  const variablesConfig = await variableService.loadConfig();
@@ -305,9 +331,6 @@ export async function buildAstroProject(
305
331
 
306
332
  // Build image metadata map for responsive image generation
307
333
  const imageMetadataMap = await buildImageMetadataMap();
308
- if (imageMetadataMap.size > 0) {
309
- console.log(`Loaded image metadata for ${imageMetadataMap.size} image(s)\n`);
310
- }
311
334
 
312
335
  // ----------------------------------------------------------
313
336
  // 2. Clean and create output directory
@@ -346,8 +369,6 @@ export async function buildAstroProject(
346
369
  return { pages: 0, cmsPages: 0, collections: 0, errors: 0 };
347
370
  }
348
371
 
349
- console.log(`Found ${pageFiles.length} page(s) to process\n`);
350
-
351
372
  // Collect slug mappings (first pass)
352
373
  const slugMappings: SlugMap[] = [];
353
374
  for (const file of pageFiles) {
@@ -372,6 +393,7 @@ export async function buildAstroProject(
372
393
  const allComponentCSS = new Set<string>();
373
394
  const jsContents = new Map<string, string>(); // hash -> JS content
374
395
  let errorCount = 0;
396
+ let projectNeedsFormHandler = false;
375
397
 
376
398
  // Helper to merge interactive styles maps
377
399
  function mergeInteractiveStyles(source: Map<string, InteractiveStyles>): void {
@@ -384,7 +406,7 @@ export async function buildAstroProject(
384
406
 
385
407
  // Helper to process a render result
386
408
  function processRenderResult(
387
- result: { html: string; meta: string; title: string; javascript: string; componentCSS?: string; locale: string; interactiveStylesMap: Map<string, InteractiveStyles>; preloadImages: any[]; neededCollections: Set<string>; ssrFallbackCollector?: Map<string, string> },
409
+ result: { html: string; meta: string; title: string; javascript: string; componentCSS?: string; locale: string; interactiveStylesMap: Map<string, InteractiveStyles>; preloadImages: any[]; neededCollections: Set<string>; ssrFallbackCollector?: Map<string, string>; processedRawHtmlCollector?: Map<string, string> },
388
410
  urlPath: string,
389
411
  astroFilePath: string,
390
412
  fileDepth: number,
@@ -408,6 +430,11 @@ export async function buildAstroProject(
408
430
  }
409
431
  }
410
432
 
433
+ // Detect forms that need the fetch handler
434
+ if (!projectNeedsFormHandler && needsFormHandler(result.html)) {
435
+ projectNeedsFormHandler = true;
436
+ }
437
+
411
438
  allResults.push({
412
439
  html: result.html,
413
440
  meta: result.meta,
@@ -423,6 +450,7 @@ export async function buildAstroProject(
423
450
  pageName,
424
451
  isCMSPage,
425
452
  ssrFallbackCollector: result.ssrFallbackCollector,
453
+ processedRawHtmlCollector: result.processedRawHtmlCollector,
426
454
  });
427
455
  }
428
456
 
@@ -444,7 +472,6 @@ export async function buildAstroProject(
444
472
  // Skip draft pages in production
445
473
  const isDevBuild = process.env.MENO_DEV_BUILD === 'true';
446
474
  if (pageData.meta?.draft === true && !isDevBuild) {
447
- console.log(` Skipping draft: ${basePath}`);
448
475
  continue;
449
476
  }
450
477
 
@@ -487,7 +514,6 @@ export async function buildAstroProject(
487
514
  );
488
515
 
489
516
  processRenderResult(result, urlPath, astroFilePath, fileDepth, pageData, pageName, false);
490
- console.log(` Rendered: ${urlPath}`);
491
517
  }
492
518
  } catch (error: any) {
493
519
  console.error(` Error rendering ${basePath}:`, error?.message || error);
@@ -499,9 +525,56 @@ export async function buildAstroProject(
499
525
  const fontPreloads = generateFontPreloadTags();
500
526
  const mergedLibraries = mergeLibraries(globalLibraries, componentLibraries);
501
527
  const buildLibraries = filterLibrariesByContext(mergedLibraries, 'build');
502
- const libraryTags = generateLibraryTags(buildLibraries);
528
+
529
+ // Mirror htmlGenerator.ts: local CSS libraries with `inline !== false` are inlined
530
+ // into a <style> tag, otherwise the file is copied to public/ so the <link href>
531
+ // resolves. This keeps any `/some-file.css` referenced in project.config.json
532
+ // libraries working, not just the special `custom.css` case.
533
+ const inlineContents = new Map<string, string>();
534
+ const localLibsToCopy: string[] = [];
535
+ for (const css of buildLibraries.css || []) {
536
+ if (!css.url.startsWith('/')) continue;
537
+ const shouldInline = css.inline !== false;
538
+ const relPath = css.url.slice(1);
539
+ const srcPath = join(projectPaths.project, relPath);
540
+ if (!existsSync(srcPath)) continue;
541
+ if (shouldInline) {
542
+ try {
543
+ inlineContents.set(css.url, await readFile(srcPath, 'utf-8'));
544
+ } catch {
545
+ localLibsToCopy.push(relPath);
546
+ }
547
+ } else {
548
+ localLibsToCopy.push(relPath);
549
+ }
550
+ }
551
+ for (const js of buildLibraries.js || []) {
552
+ if (js.url.startsWith('/')) {
553
+ const relPath = js.url.slice(1);
554
+ if (existsSync(join(projectPaths.project, relPath))) {
555
+ localLibsToCopy.push(relPath);
556
+ }
557
+ }
558
+ }
559
+ const libraryTags = generateLibraryTags(buildLibraries, inlineContents);
503
560
  const defaultTheme = themeConfig.default || 'light';
504
561
 
562
+ // Global customCode (head/bodyStart/bodyEnd) and icons from project.config.json
563
+ // are page-independent, so we bake them directly into BaseLayout.astro rather
564
+ // than plumbing them through every page's props. Mirrors htmlGenerator.ts's
565
+ // SSR output (htmlGenerator.ts:269-507 for customCode, 368-473 for icons).
566
+ const customCode = configService.getCustomCode();
567
+ const iconsConfig = await loadIconsConfig();
568
+ const faviconTag = iconsConfig.favicon
569
+ ? `<link rel="icon" href="${iconsConfig.favicon.replace(/"/g, '&quot;')}" />`
570
+ : '';
571
+ const appleTouchIconTag = iconsConfig.appleTouchIcon
572
+ ? `<link rel="apple-touch-icon" href="${iconsConfig.appleTouchIcon.replace(/"/g, '&quot;')}" />`
573
+ : '';
574
+ const iconTagsHtml = [faviconTag, appleTouchIconTag].filter(Boolean).join('\n ');
575
+
576
+ const remConversionConfig = configService.getRemConversion();
577
+
505
578
  // ---------- CMS template pages ----------
506
579
  const templatesDir = projectPaths.templates();
507
580
  const templateSchemas: CMSSchema[] = [];
@@ -510,10 +583,6 @@ export async function buildAstroProject(
510
583
  if (existsSync(templatesDir)) {
511
584
  const templateFiles = readdirSync(templatesDir).filter(f => f.endsWith('.json'));
512
585
 
513
- if (templateFiles.length > 0) {
514
- console.log(`\nProcessing ${templateFiles.length} CMS template(s)...\n`);
515
- }
516
-
517
586
  for (const file of templateFiles) {
518
587
  const templateContent = await loadJSONFile(join(templatesDir, file));
519
588
  if (!templateContent) continue;
@@ -523,7 +592,6 @@ export async function buildAstroProject(
523
592
 
524
593
  const isDevBuild = process.env.MENO_DEV_BUILD === 'true';
525
594
  if (pageData.meta?.draft === true && !isDevBuild) {
526
- console.log(` Skipping draft template: ${file}`);
527
595
  continue;
528
596
  }
529
597
 
@@ -534,18 +602,11 @@ export async function buildAstroProject(
534
602
 
535
603
  const cmsSchema = pageData.meta!.cms as CMSSchema;
536
604
  templateSchemas.push(cmsSchema);
537
- console.log(` CMS Collection: ${cmsSchema.id}`);
538
605
 
539
606
  // Count items for stats
540
607
  const items = await cmsService.queryItems({ collection: cmsSchema.id });
541
608
  const itemCount = items.length;
542
609
 
543
- if (itemCount === 0) {
544
- console.log(` No items found in cms/${cmsSchema.id}/`);
545
- } else {
546
- console.log(` Found ${itemCount} item(s)`);
547
- }
548
-
549
610
  // Render SSR once for metadata collection (interactive styles, component CSS, JS)
550
611
  const defaultLocale = i18nConfig.defaultLocale;
551
612
  const dummyPath = cmsSchema.urlPattern.replace('{{slug}}', '__placeholder__');
@@ -626,11 +687,14 @@ export async function buildAstroProject(
626
687
  ssrFallbacks,
627
688
  pageName: file.replace('.json', ''),
628
689
  breakpoints,
690
+ responsiveScales,
629
691
  imageMetadataMap,
630
692
  i18nConfig,
631
693
  isMultiLocale: false, // Each file handles one locale
632
694
  slugMappings,
633
695
  imageFormat: configService.getImageFormat(),
696
+ processedRawHtml: metaResult.processedRawHtmlCollector,
697
+ remConfig: remConversionConfig,
634
698
  });
635
699
 
636
700
  const astroFileFull = join(pagesOutDir, astroFilePath);
@@ -642,8 +706,6 @@ export async function buildAstroProject(
642
706
  await writeFile(astroFileFull, astroContent, 'utf-8');
643
707
  }
644
708
 
645
- console.log(` Generated: ${pathPrefix}[slug].astro (${itemCount} items × ${localesToEmit.length} locale(s))`);
646
-
647
709
  cmsPageCount += itemCount * i18nConfig.locales.length;
648
710
  } catch (error: any) {
649
711
  console.error(` Error processing template ${file}:`, error?.message || error);
@@ -656,39 +718,56 @@ export async function buildAstroProject(
656
718
  // 6. Generate global CSS (Tailwind + theme + interactive styles)
657
719
  // ----------------------------------------------------------
658
720
  // Collect Tailwind safelist classes from mapping variants
659
- const mappingClasses = collectAllMappingClasses(globalComponents, breakpoints);
721
+ const mappingClasses = collectAllMappingClasses(globalComponents, breakpoints, responsiveScales);
660
722
 
661
723
  const fontCSS = generateFontCSS();
662
724
  const themeColorCSS = generateThemeColorVariablesCSS(themeConfig);
663
725
  const variablesCSS = generateVariablesCSS(variablesConfig, breakpoints, responsiveScales);
664
- const remConversionConfig = configService.getRemConversion();
665
- const interactiveCSS = generateAllInteractiveCSS(allInteractiveStyles, breakpoints, remConversionConfig);
666
726
  const componentCSSCombined = Array.from(allComponentCSS).join('\n');
667
727
 
668
728
  const baseCSS = `@layer base {
669
729
  * { margin: 0; padding: 0; box-sizing: border-box; }
670
730
  body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif; }
671
731
  button { background: none; border: none; padding: 0; font: inherit; cursor: pointer; outline: inherit; }
672
- img { display: block; width: 100%; height: 100%; }
732
+ img { max-width: 100%; height: auto; }
673
733
  picture { display: block; }
674
- .olink { text-decoration: none; display: block; }
734
+ .olink { text-decoration: none; display: block; color: inherit; }
675
735
  .oem { display: inline-block; }
676
736
  }`;
677
737
 
678
- const tailwindDirectives = `@tailwind base;
679
- @tailwind components;
680
- @tailwind utilities;`;
738
+ const safelistClasses = Array.from(mappingClasses);
739
+ const safelistDirectives = safelistClasses
740
+ .map(c => `@source inline("${c}");`)
741
+ .join('\n');
742
+ const tailwindDirectives = safelistDirectives
743
+ ? `@import "tailwindcss";\n\n${safelistDirectives}`
744
+ : `@import "tailwindcss";`;
681
745
 
682
- const globalCSS = [tailwindDirectives, fontCSS, themeColorCSS, variablesCSS, baseCSS, componentCSSCombined, interactiveCSS]
746
+ const globalCSS = [tailwindDirectives, fontCSS, themeColorCSS, variablesCSS, baseCSS, componentCSSCombined]
683
747
  .filter(Boolean)
684
748
  .join('\n\n');
685
749
 
686
750
  await writeFile(join(stylesDir, 'global.css'), globalCSS, 'utf-8');
687
- console.log(`\nGenerated global.css (${(globalCSS.length / 1024).toFixed(1)} KB)`);
688
751
 
689
752
  // ----------------------------------------------------------
690
753
  // 7. Generate BaseLayout.astro
691
754
  // ----------------------------------------------------------
755
+ // Escape for embedding inside Astro <Fragment set:html={`...`}> template
756
+ // literals in the generated BaseLayout file.
757
+ const escForTemplateLiteral = (s: string) => s
758
+ .replace(/\\/g, '\\\\')
759
+ .replace(/`/g, '\\`')
760
+ .replace(/\$\{/g, '\\${');
761
+
762
+ const customHeadLiteral = escForTemplateLiteral(customCode.head || '');
763
+ const customBodyStartLiteral = escForTemplateLiteral(customCode.bodyStart || '');
764
+ const customBodyEndLiteral = escForTemplateLiteral(customCode.bodyEnd || '');
765
+ const iconTagsLiteral = escForTemplateLiteral(iconTagsHtml);
766
+
767
+ const formHandlerBlock = projectNeedsFormHandler
768
+ ? `\n <script is:inline>\n${formHandlerScript}\n </script>`
769
+ : '';
770
+
692
771
  const baseLayoutContent = `---
693
772
  import '../styles/global.css';
694
773
 
@@ -709,22 +788,25 @@ const { title, meta = '', scripts = [], locale = 'en', theme = '${themeConfig.de
709
788
  <head>
710
789
  <meta charset="UTF-8">
711
790
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
791
+ <Fragment set:html={\`${iconTagsLiteral}\`} />
712
792
  <Fragment set:html={fontPreloads} />
713
793
  <Fragment set:html={libraryTags.headCSS || ''} />
714
794
  <Fragment set:html={libraryTags.headJS || ''} />
715
795
  <Fragment set:html={meta} />
796
+ <Fragment set:html={\`${customHeadLiteral}\`} />
716
797
  <title>{title}</title>
717
798
  </head>
718
799
  <body>
800
+ <Fragment set:html={\`${customBodyStartLiteral}\`} />
719
801
  <slot />
720
802
  {scripts.map((s) => <script src={s} />)}
721
803
  <Fragment set:html={libraryTags.bodyEndJS || ''} />
804
+ <Fragment set:html={\`${customBodyEndLiteral}\`} />${formHandlerBlock}
722
805
  </body>
723
806
  </html>
724
807
  `;
725
808
 
726
809
  await writeFile(join(layoutsDir, 'BaseLayout.astro'), baseLayoutContent, 'utf-8');
727
- console.log('Generated BaseLayout.astro');
728
810
 
729
811
  // ----------------------------------------------------------
730
812
  // 7.5. Generate component .astro files
@@ -732,38 +814,21 @@ const { title, meta = '', scripts = [], locale = 'en', theme = '${themeConfig.de
732
814
  let componentFileCount = 0;
733
815
  for (const [compName, compDef] of Object.entries(globalComponents)) {
734
816
  try {
735
- const astroContent = emitAstroComponent(compName, compDef, globalComponents, breakpoints, i18nConfig.defaultLocale);
817
+ const astroContent = emitAstroComponent(compName, compDef, globalComponents, breakpoints, i18nConfig.defaultLocale, responsiveScales, remConversionConfig);
736
818
  await writeFile(join(componentsOutDir, `${compName}.astro`), astroContent, 'utf-8');
737
819
  componentFileCount++;
738
820
  } catch (error: any) {
739
821
  console.warn(` Warning: could not generate component ${compName}: ${error?.message}`);
740
822
  }
741
823
  }
742
- console.log(`Generated ${componentFileCount} component .astro file(s)`);
743
-
744
824
  // ----------------------------------------------------------
745
825
  // 8. Generate .astro page files (component-structured)
746
826
  // ----------------------------------------------------------
747
827
  for (const result of allResults) {
748
828
  const importPath = layoutImportPath(result.fileDepth);
749
829
 
750
- // Write JavaScript to public/_scripts/ if present
751
- const scriptPaths: string[] = [];
752
- if (result.javascript) {
753
- const hash = hashContent(result.javascript);
754
- const scriptFile = `${hash}.js`;
755
- const scriptPublicPath = `/_scripts/${scriptFile}`;
756
-
757
- if (!existsSync(scriptsDir)) {
758
- mkdirSync(scriptsDir, { recursive: true });
759
- }
760
-
761
- const fullScriptPath = join(scriptsDir, scriptFile);
762
- if (!existsSync(fullScriptPath)) {
763
- await writeFile(fullScriptPath, result.javascript, 'utf-8');
764
- }
765
- scriptPaths.push(scriptPublicPath);
766
- }
830
+ // Write JavaScript to public/_scripts/ if present (only needed for SSR fallback pages)
831
+ let scriptPaths: string[] = [];
767
832
 
768
833
  let astroContent: string;
769
834
 
@@ -779,6 +844,8 @@ const { title, meta = '', scripts = [], locale = 'en', theme = '${themeConfig.de
779
844
  ? computePageSlugMap(result.pageData.meta.slugs, i18nConfig)
780
845
  : undefined;
781
846
 
847
+ // Component-structured pages don't need page-level _scripts/*.js
848
+ // because each .astro component already has its own inline <script>
782
849
  astroContent = emitAstroPage({
783
850
  pageData: result.pageData,
784
851
  globalComponents,
@@ -788,25 +855,30 @@ const { title, meta = '', scripts = [], locale = 'en', theme = '${themeConfig.de
788
855
  theme: defaultTheme,
789
856
  fontPreloads,
790
857
  libraryTags,
791
- scriptPaths,
858
+ scriptPaths: [],
792
859
  layoutImportPath: importPath,
793
860
  fileDepth: result.fileDepth,
794
861
  ssrFallbacks,
795
862
  pageName: result.pageName || 'index',
796
863
  breakpoints,
864
+ responsiveScales,
797
865
  imageMetadataMap,
798
866
  i18nConfig: i18nConfig.locales.length > 1 ? i18nConfig : undefined,
799
867
  currentPageSlugMap: pageSlugMap,
800
868
  slugMappings: i18nConfig.locales.length > 1 ? slugMappings : undefined,
801
869
  imageFormat: configService.getImageFormat(),
870
+ processedRawHtml: result.processedRawHtmlCollector,
871
+ remConfig: remConversionConfig,
802
872
  });
803
873
  } catch (error: any) {
804
- // Fallback to SSR HTML if component emission fails
874
+ // Fallback to SSR HTML if component emission fails — needs page-level script
805
875
  console.warn(` Warning: component emission failed for ${result.urlPath}, using SSR fallback: ${error?.message}`);
876
+ scriptPaths = writePageScript(result.javascript, scriptsDir);
806
877
  astroContent = buildSSRFallbackPage(result, importPath, fontPreloads, libraryTags, defaultTheme, scriptPaths);
807
878
  }
808
879
  } else {
809
- // Pages without pageData: use SSR fallback
880
+ // Pages without pageData: use SSR fallback — needs page-level script
881
+ scriptPaths = writePageScript(result.javascript, scriptsDir);
810
882
  astroContent = buildSSRFallbackPage(result, importPath, fontPreloads, libraryTags, defaultTheme, scriptPaths);
811
883
  }
812
884
 
@@ -819,7 +891,26 @@ const { title, meta = '', scripts = [], locale = 'en', theme = '${themeConfig.de
819
891
  await writeFile(astroFileFull, astroContent, 'utf-8');
820
892
  }
821
893
 
822
- console.log(`Generated ${allResults.length} .astro page file(s)`);
894
+ // ----------------------------------------------------------
895
+ // 8.5. Generate robots.txt endpoint
896
+ // ----------------------------------------------------------
897
+ const robotsTsContent = `import type { APIRoute } from 'astro';
898
+
899
+ export const GET: APIRoute = () => {
900
+ const siteUrl = import.meta.env.SITE;
901
+ const robotsTxt = [
902
+ 'User-agent: *',
903
+ 'Allow: /',
904
+ '',
905
+ siteUrl ? \`Sitemap: \${siteUrl}/sitemap-index.xml\` : '',
906
+ ].filter(Boolean).join('\\n');
907
+
908
+ return new Response(robotsTxt, {
909
+ headers: { 'Content-Type': 'text/plain; charset=utf-8' },
910
+ });
911
+ };
912
+ `;
913
+ await writeFile(join(pagesOutDir, 'robots.txt.ts'), robotsTsContent, 'utf-8');
823
914
 
824
915
  // ----------------------------------------------------------
825
916
  // 9. Generate CMS content collections (if templates exist)
@@ -871,7 +962,7 @@ const { title, meta = '', scripts = [], locale = 'en', theme = '${themeConfig.de
871
962
  }
872
963
 
873
964
  collectionDefs.push(` '${schema.id}': defineCollection({
874
- type: 'data',
965
+ loader: glob({ pattern: '**/*.json', base: './src/content/${schema.id}' }),
875
966
  schema: z.object({
876
967
  ${fieldDefs.join(',\n')}
877
968
  })
@@ -880,8 +971,9 @@ ${fieldDefs.join(',\n')}
880
971
  collectionCount++;
881
972
  }
882
973
 
883
- // Write src/content/config.ts
974
+ // Write src/content.config.ts (Astro 6 location — legacy src/content/config.ts is rejected)
884
975
  const configContent = `import { z, defineCollection } from 'astro:content';
976
+ import { glob } from 'astro/loaders';
885
977
 
886
978
  const collections = {
887
979
  ${collectionDefs.join(',\n')}
@@ -890,33 +982,53 @@ ${collectionDefs.join(',\n')}
890
982
  export { collections };
891
983
  `;
892
984
 
893
- await writeFile(join(contentDir, 'config.ts'), configContent, 'utf-8');
894
- console.log(`Generated ${collectionCount} content collection(s) with config.ts`);
985
+ await writeFile(join(srcDir, 'content.config.ts'), configContent, 'utf-8');
895
986
  }
896
987
 
897
988
  // ----------------------------------------------------------
898
- // 10. Copy assets to public/
989
+ // 10. Copy assets
899
990
  // ----------------------------------------------------------
900
- const assetDirs = ['fonts', 'images', 'icons', 'videos', 'assets'];
901
- let copiedAssets = 0;
991
+ // Images go to src/assets/images so Astro's asset pipeline can process
992
+ // them via astro:assets `<Picture>` (hashing, on-edit reprocessing).
993
+ // Everything else stays in public/ — fonts/icons/videos need stable URLs.
994
+
995
+ const imagesSrcDir = join(projectPaths.project, 'images');
996
+ if (existsSync(imagesSrcDir)) {
997
+ // src/assets/images: used by static <Picture> via ESM imports + Vite asset
998
+ // pipeline. Pre-baked responsive variants and manifest.json are excluded —
999
+ // Astro regenerates those from the originals.
1000
+ copyDirectory(imagesSrcDir, join(srcDir, 'assets', 'images'), shouldCopyImageForAstro);
1001
+ // public/images: used by the legacy <img>/<picture> srcset path and
1002
+ // rich-text image rewrites, which emit plain `/images/...` URLs pointing at
1003
+ // the pre-built variants. Without this mirror, any image not routed through
1004
+ // the static Picture path (variants referenced directly in a srcset,
1005
+ // CMS/template-bound images, component-prop images) 404s in `astro dev`.
1006
+ copyDirectory(imagesSrcDir, join(publicDir, 'images'));
1007
+ }
902
1008
 
903
- for (const dir of assetDirs) {
1009
+ const publicAssetDirs = ['fonts', 'icons', 'videos', 'assets'];
1010
+ for (const dir of publicAssetDirs) {
904
1011
  const srcAssetDir = join(projectPaths.project, dir);
905
1012
  if (existsSync(srcAssetDir)) {
906
1013
  copyDirectory(srcAssetDir, join(publicDir, dir));
907
- copiedAssets++;
908
- }
1014
+ }
909
1015
  }
910
1016
 
911
1017
  // Copy libraries folder if it exists
912
1018
  const librariesDir = join(projectPaths.project, 'libraries');
913
1019
  if (existsSync(librariesDir)) {
914
1020
  copyDirectory(librariesDir, join(publicDir, 'libraries'));
915
- copiedAssets++;
916
1021
  }
917
1022
 
918
- if (copiedAssets > 0) {
919
- console.log(`Copied ${copiedAssets} asset director${copiedAssets === 1 ? 'y' : 'ies'} to public/`);
1023
+ // Copy any project-root library files referenced by absolute URL in
1024
+ // project.config.json (e.g. `/custom.css`). Only copies entries we already
1025
+ // validated exist on disk during library tag generation above.
1026
+ for (const relPath of localLibsToCopy) {
1027
+ const srcPath = join(projectPaths.project, relPath);
1028
+ const destPath = join(publicDir, relPath);
1029
+ const destDir = destPath.substring(0, destPath.lastIndexOf('/'));
1030
+ if (destDir && !existsSync(destDir)) mkdirSync(destDir, { recursive: true });
1031
+ copyFileSync(srcPath, destPath);
920
1032
  }
921
1033
 
922
1034
  // ----------------------------------------------------------
@@ -936,9 +1048,14 @@ export { collections };
936
1048
  preview: 'astro preview',
937
1049
  },
938
1050
  dependencies: {
939
- 'astro': '^4.0.0',
940
- '@astrojs/tailwind': '^5.0.0',
941
- 'tailwindcss': '^3.4.0',
1051
+ 'astro': '^6.0.0',
1052
+ '@astrojs/sitemap': '^3.0.0',
1053
+ '@tailwindcss/vite': '^4.0.0',
1054
+ 'tailwindcss': '^4.0.0',
1055
+ },
1056
+ // Astro 6 expects Vite 7; pin it so npm doesn't pull Vite 8+ and warn.
1057
+ overrides: {
1058
+ 'vite': '^7.0.0',
942
1059
  },
943
1060
  };
944
1061
 
@@ -951,31 +1068,17 @@ export { collections };
951
1068
  : '';
952
1069
 
953
1070
  const astroConfig = `import { defineConfig } from 'astro/config';
954
- import tailwind from '@astrojs/tailwind';
1071
+ import tailwindcss from '@tailwindcss/vite';
1072
+ import sitemap from '@astrojs/sitemap';
955
1073
 
956
1074
  export default defineConfig({${siteUrl ? `\n site: '${siteUrl}',` : ''}${i18nBlock}
957
- integrations: [tailwind({ applyBaseStyles: false })],
958
- });
959
- `;
960
-
961
- // tailwind.config.mjs
962
- const safelistArray = Array.from(mappingClasses);
963
- const safelistLiteral = safelistArray.length > 0
964
- ? `\n safelist: [\n${safelistArray.map(c => ` '${c}'`).join(',\n')}\n ],`
965
- : '';
966
-
967
- const tailwindConfig = `/** @type {import('tailwindcss').Config} */
968
- export default {
969
- content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],${safelistLiteral}
970
- theme: {
971
- extend: {},
1075
+ integrations: [sitemap()],
1076
+ vite: {
1077
+ plugins: [tailwindcss()],
972
1078
  },
973
- plugins: [],
974
- };
1079
+ });
975
1080
  `;
976
1081
 
977
- await writeFile(join(outDir, 'tailwind.config.mjs'), tailwindConfig, 'utf-8');
978
-
979
1082
  await writeFile(join(outDir, 'astro.config.mjs'), astroConfig, 'utf-8');
980
1083
 
981
1084
  // tsconfig.json
@@ -985,30 +1088,14 @@ export default {
985
1088
 
986
1089
  await writeFile(join(outDir, 'tsconfig.json'), JSON.stringify(tsConfig, null, 2), 'utf-8');
987
1090
 
988
- console.log('Generated package.json, astro.config.mjs, tailwind.config.mjs, tsconfig.json');
1091
+ // src/env.d.ts — resolves astro:assets and other virtual module types in IDE
1092
+ await writeFile(join(outDir, 'src', 'env.d.ts'), '/// <reference path="../.astro/types.d.ts" />\n', 'utf-8');
989
1093
 
990
1094
  // ----------------------------------------------------------
991
1095
  // 12. Summary
992
1096
  // ----------------------------------------------------------
993
- const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
994
1097
  const totalPages = allResults.length;
995
1098
 
996
- console.log('\n' + '='.repeat(50));
997
- console.log('Astro export complete!');
998
- console.log(` Pages: ${totalPages - cmsPageCount}`);
999
- if (cmsPageCount > 0) {
1000
- console.log(` CMS pages: ${cmsPageCount}`);
1001
- }
1002
- if (collectionCount > 0) {
1003
- console.log(` Content collections: ${collectionCount}`);
1004
- }
1005
- if (errorCount > 0) {
1006
- console.log(` Errors: ${errorCount}`);
1007
- }
1008
- console.log(` Time: ${elapsed}s`);
1009
- console.log(` Output: ${outDir}`);
1010
- console.log('');
1011
-
1012
1099
  return {
1013
1100
  pages: totalPages - cmsPageCount,
1014
1101
  cmsPages: cmsPageCount,