meno-core 1.0.16 → 1.0.19

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 (91) hide show
  1. package/bin/cli.ts +18 -1
  2. package/build-static.ts +336 -16
  3. package/lib/client/ClientInitializer.ts +4 -0
  4. package/lib/client/core/ComponentBuilder.test.ts +530 -9
  5. package/lib/client/core/ComponentBuilder.ts +97 -17
  6. package/lib/client/core/ComponentRenderer.tsx +6 -2
  7. package/lib/client/core/builders/cmsListBuilder.ts +191 -12
  8. package/lib/client/core/builders/embedBuilder.ts +8 -9
  9. package/lib/client/core/builders/index.ts +2 -2
  10. package/lib/client/core/builders/{objectLinkBuilder.ts → linkNodeBuilder.ts} +24 -24
  11. package/lib/client/core/builders/localeListBuilder.ts +5 -5
  12. package/lib/client/core/builders/types.ts +8 -0
  13. package/lib/client/hmr/HMRManager.tsx +64 -36
  14. package/lib/client/hmrWebSocket.ts +15 -0
  15. package/lib/client/meno-filter/MenoFilter.test.ts +1230 -0
  16. package/lib/client/meno-filter/MenoFilter.ts +1281 -0
  17. package/lib/client/meno-filter/bindings.ts +554 -0
  18. package/lib/client/meno-filter/constants.ts +72 -0
  19. package/lib/client/meno-filter/index.ts +58 -0
  20. package/lib/client/meno-filter/init.ts +259 -0
  21. package/lib/client/meno-filter/renderer.ts +410 -0
  22. package/lib/client/meno-filter/script.generated.ts +56 -0
  23. package/lib/client/meno-filter/types.ts +165 -0
  24. package/lib/client/meno-filter/ui.ts +364 -0
  25. package/lib/client/meno-filter/updates.ts +674 -0
  26. package/lib/client/meno-filter/utils.ts +44 -0
  27. package/lib/client/routing/Router.tsx +32 -4
  28. package/lib/client/templateEngine.ts +39 -14
  29. package/lib/server/createServer.ts +7 -0
  30. package/lib/server/index.ts +7 -1
  31. package/lib/server/routes/api/cms.ts +6 -1
  32. package/lib/server/routes/api/core-routes.ts +26 -0
  33. package/lib/server/routes/index.ts +45 -0
  34. package/lib/server/routes/pages.ts +113 -15
  35. package/lib/server/scriptCache.ts +34 -0
  36. package/lib/server/services/configService.ts +48 -0
  37. package/lib/server/services/fileWatcherService.ts +10 -2
  38. package/lib/server/ssr/attributeBuilder.ts +6 -2
  39. package/lib/server/ssr/buildErrorOverlay.ts +341 -0
  40. package/lib/server/ssr/clientDataInjector.test.ts +337 -0
  41. package/lib/server/ssr/clientDataInjector.ts +161 -0
  42. package/lib/server/ssr/errorOverlay.test.ts +73 -0
  43. package/lib/server/ssr/errorOverlay.ts +263 -0
  44. package/lib/server/ssr/htmlGenerator.ts +89 -6
  45. package/lib/server/ssr/index.ts +2 -1
  46. package/lib/server/ssr/jsCollector.ts +107 -6
  47. package/lib/server/ssr/ssrRenderer.ts +332 -57
  48. package/lib/server/ssrRenderer.test.ts +307 -11
  49. package/lib/server/validateStyleCoverage.ts +10 -11
  50. package/lib/shared/cmsQueryParser.test.ts +288 -0
  51. package/lib/shared/cmsQueryParser.ts +188 -0
  52. package/lib/shared/constants.test.ts +1 -1
  53. package/lib/shared/constants.ts +5 -1
  54. package/lib/shared/cssGeneration.test.ts +113 -0
  55. package/lib/shared/cssGeneration.ts +40 -2
  56. package/lib/shared/cssProperties.ts +84 -7
  57. package/lib/shared/elementClassName.test.ts +6 -6
  58. package/lib/shared/elementClassName.ts +4 -4
  59. package/lib/shared/globalTemplateContext.ts +45 -0
  60. package/lib/shared/index.ts +8 -0
  61. package/lib/shared/interactiveStyles.test.ts +16 -16
  62. package/lib/shared/itemTemplateUtils.test.ts +151 -1
  63. package/lib/shared/itemTemplateUtils.ts +113 -2
  64. package/lib/shared/libraryLoader.ts +128 -0
  65. package/lib/shared/nodeUtils.test.ts +134 -11
  66. package/lib/shared/nodeUtils.ts +95 -5
  67. package/lib/shared/registry/index.ts +1 -1
  68. package/lib/shared/registry/nodeTypes/CMSListNodeType.ts +8 -1
  69. package/lib/shared/registry/nodeTypes/ComponentInstanceNodeType.ts +3 -2
  70. package/lib/shared/registry/nodeTypes/EmbedNodeType.ts +5 -2
  71. package/lib/shared/registry/nodeTypes/HtmlNodeType.ts +3 -2
  72. package/lib/shared/registry/nodeTypes/{ObjectLinkNodeType.ts → LinkNodeType.ts} +13 -10
  73. package/lib/shared/registry/nodeTypes/LocaleListNodeType.ts +5 -2
  74. package/lib/shared/registry/nodeTypes/index.ts +5 -5
  75. package/lib/shared/tree/PathBuilder.test.ts +395 -0
  76. package/lib/shared/tree/PathBuilder.ts +38 -12
  77. package/lib/shared/treePathUtils.test.ts +2 -2
  78. package/lib/shared/treePathUtils.ts +2 -2
  79. package/lib/shared/types/api.ts +3 -0
  80. package/lib/shared/types/cms.ts +86 -2
  81. package/lib/shared/types/config.ts +38 -0
  82. package/lib/shared/types/index.ts +17 -1
  83. package/lib/shared/types/libraries.ts +65 -0
  84. package/lib/shared/types/nodes.ts +1 -1
  85. package/lib/shared/utilityClassConfig.ts +151 -4
  86. package/lib/shared/utilityClassMapper.test.ts +30 -0
  87. package/lib/shared/utilityClassMapper.ts +54 -1
  88. package/lib/shared/utils.ts +21 -3
  89. package/lib/shared/validation/schemas.ts +111 -11
  90. package/package.json +3 -1
  91. package/scripts/build-meno-filter.ts +110 -0
package/bin/cli.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  import { resolve, join } from 'path';
8
8
  import { existsSync, mkdirSync, writeFileSync, cpSync, readFileSync } from 'fs';
9
9
  import { setProjectRoot } from '../lib/server/projectContext';
10
+ import { generateBuildErrorPage, type BuildErrorsData } from '../lib/server/ssr/buildErrorOverlay';
10
11
 
11
12
  const args = process.argv.slice(2);
12
13
  const command = args[0];
@@ -105,6 +106,21 @@ async function startStaticServer(distPath: string) {
105
106
  const url = new URL(req.url);
106
107
  let pathname = url.pathname;
107
108
 
109
+ // Check for build errors and show overlay (except for _errors.json itself)
110
+ if (pathname !== '/_errors.json') {
111
+ const errorsPath = join(distPath, '_errors.json');
112
+ if (existsSync(errorsPath)) {
113
+ try {
114
+ const errorsData: BuildErrorsData = JSON.parse(readFileSync(errorsPath, 'utf-8'));
115
+ return new Response(generateBuildErrorPage(errorsData), {
116
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
117
+ });
118
+ } catch {
119
+ // If we can't parse errors file, continue normally
120
+ }
121
+ }
122
+ }
123
+
108
124
  // Default to index.html for root
109
125
  if (pathname === '/') {
110
126
  pathname = '/index.html';
@@ -191,7 +207,8 @@ async function runBuild(isDev: boolean = false) {
191
207
  console.log(`📁 Building project: ${projectRoot}${isDev ? ' (dev mode - including drafts)' : ''}`);
192
208
 
193
209
  // Import and run build
194
- await import('../build-static.ts');
210
+ const { buildStaticPages } = await import('../build-static.ts');
211
+ await buildStaticPages();
195
212
  }
196
213
 
197
214
  async function runServe() {
package/build-static.ts CHANGED
@@ -4,9 +4,10 @@
4
4
  * CSP-compliant: Extracts JavaScript to external files
5
5
  */
6
6
 
7
- import { existsSync, readdirSync, mkdirSync, rmSync, statSync, copyFileSync } from "fs";
8
- import { writeFile } from "fs/promises";
7
+ import { existsSync, readdirSync, mkdirSync, rmSync, statSync, copyFileSync, unlinkSync, writeFileSync } from "fs";
8
+ import { writeFile, readFile } from "fs/promises";
9
9
  import { join } from "path";
10
+ import type { BuildError, BuildErrorsData } from "./lib/server/ssr/buildErrorOverlay";
10
11
  import { createHash } from "crypto";
11
12
  import {
12
13
  loadJSONFile,
@@ -17,6 +18,8 @@ import {
17
18
  } from "./lib/server/jsonLoader";
18
19
  import { generateSSRHTML } from "./lib/server/ssrRenderer";
19
20
  import type { SSRHTMLResult } from "./lib/server/ssr/htmlGenerator";
21
+ import { prepareClientData, type ClientDataCollection } from "./lib/server/ssr/clientDataInjector";
22
+ import { clearJSValidationCache, getJSValidationErrors } from "./lib/server/ssr/jsCollector";
20
23
  import { projectPaths } from "./lib/server/projectContext";
21
24
  import { loadProjectConfig } from "./lib/shared/fontLoader";
22
25
  import { FileSystemCMSProvider } from "./lib/server/providers/fileSystemCMSProvider";
@@ -24,6 +27,12 @@ import { CMSService } from "./lib/server/services/cmsService";
24
27
  import { isI18nValue, resolveI18nValue } from "./lib/shared/i18n";
25
28
  import type { ComponentDefinition, JSONPage, CMSSchema, CMSItem, I18nConfig } from "./lib/shared/types";
26
29
  import type { SlugMap } from "./lib/shared/slugTranslator";
30
+ import { buildItemUrl } from "./lib/shared/itemTemplateUtils";
31
+
32
+ /**
33
+ * Collect build errors for error overlay
34
+ */
35
+ const buildErrors: BuildError[] = [];
27
36
 
28
37
  /**
29
38
  * Generate short hash from content for file naming
@@ -32,24 +41,91 @@ function hashContent(content: string): string {
32
41
  return createHash('sha256').update(content).digest('hex').slice(0, 8);
33
42
  }
34
43
 
44
+ /**
45
+ * Format a Bun build log entry to a readable string
46
+ */
47
+ function formatBunLog(log: any): string {
48
+ const parts: string[] = [];
49
+
50
+ // Try to get position info
51
+ if (log.position) {
52
+ const pos = log.position;
53
+ if (pos.file) parts.push(`File: ${pos.file}`);
54
+ if (pos.line !== undefined) parts.push(`Line ${pos.line}:${pos.column || 0}`);
55
+ if (pos.lineText) parts.push(` ${pos.lineText}`);
56
+ }
57
+
58
+ // Get the message
59
+ if (log.message) {
60
+ parts.push(log.message);
61
+ } else if (log.text) {
62
+ parts.push(log.text);
63
+ }
64
+
65
+ // If we couldn't extract anything useful, stringify the whole thing
66
+ if (parts.length === 0) {
67
+ try {
68
+ return JSON.stringify(log, null, 2);
69
+ } catch {
70
+ return String(log);
71
+ }
72
+ }
73
+
74
+ return parts.join('\n');
75
+ }
76
+
35
77
  /**
36
78
  * Minify JavaScript code using Bun's built-in bundler
79
+ * Throws on error instead of silently failing
37
80
  */
38
81
  async function minifyJS(code: string): Promise<string> {
39
82
  const tempFile = join('/tmp', `meno-minify-${Date.now()}.js`);
40
83
  try {
41
84
  await writeFile(tempFile, code, 'utf-8');
42
85
 
86
+ // Use throw: true to get detailed error information
43
87
  const result = await Bun.build({
44
88
  entrypoints: [tempFile],
45
89
  minify: true,
90
+ throw: true, // This makes Bun throw with detailed error info
46
91
  });
47
92
 
48
- if (result.success && result.outputs.length > 0) {
93
+ if (result.outputs.length > 0) {
49
94
  return await result.outputs[0].text();
50
95
  }
51
- // Fallback to original if minification fails
52
- return code;
96
+
97
+ throw new Error('JavaScript minification produced no output');
98
+ } catch (err: any) {
99
+ // Re-throw with formatted message that includes all details
100
+ let details = '';
101
+
102
+ // Bun's BuildError has a logs array with detailed info
103
+ if (err.logs && Array.isArray(err.logs)) {
104
+ details = err.logs.map((log: any) => {
105
+ const parts: string[] = [];
106
+ if (log.position?.line) {
107
+ parts.push(`Line ${log.position.line}:${log.position.column || 0}`);
108
+ }
109
+ if (log.position?.lineText) {
110
+ parts.push(log.position.lineText);
111
+ }
112
+ if (log.message) {
113
+ parts.push(log.message);
114
+ }
115
+ return parts.length > 0 ? parts.join('\n') : String(log);
116
+ }).join('\n\n');
117
+ }
118
+
119
+ // If no logs, try Bun.inspect for full error details
120
+ if (!details) {
121
+ try {
122
+ details = Bun.inspect(err);
123
+ } catch {
124
+ details = err.stack || err.message || String(err);
125
+ }
126
+ }
127
+
128
+ throw new Error(`JavaScript minification failed:\n${details}`);
53
129
  } finally {
54
130
  // Clean up temp file
55
131
  try {
@@ -284,6 +360,29 @@ function buildCMSItemPath(
284
360
  return urlPattern.replace('{{slug}}', String(slug));
285
361
  }
286
362
 
363
+ /**
364
+ * Generate static JSON data files for collections with 'static' strategy
365
+ * Output: /data/{collection}/index.json
366
+ */
367
+ async function generateStaticDataFiles(
368
+ staticCollections: Map<string, ClientDataCollection>,
369
+ distDir: string
370
+ ): Promise<void> {
371
+ if (staticCollections.size === 0) return;
372
+
373
+ console.log(`\n📦 Generating static data files...`);
374
+
375
+ for (const [collectionId, data] of staticCollections) {
376
+ const dataDir = join(distDir, 'data', collectionId);
377
+ if (!existsSync(dataDir)) {
378
+ mkdirSync(dataDir, { recursive: true });
379
+ }
380
+ const jsonPath = join(dataDir, 'index.json');
381
+ await writeFile(jsonPath, JSON.stringify(data.items), 'utf-8');
382
+ console.log(` ✅ /data/${collectionId}/index.json (${data.items.length} items)`);
383
+ }
384
+ }
385
+
287
386
  /**
288
387
  * Build CMS templates from pages/templates/ directory
289
388
  */
@@ -295,6 +394,7 @@ async function buildCMSTemplates(
295
394
  distDir: string,
296
395
  cmsService: CMSService,
297
396
  generatedUrls: Set<string>,
397
+ staticCollections: Map<string, ClientDataCollection>,
298
398
  siteUrl?: string
299
399
  ): Promise<{ success: number; errors: number }> {
300
400
  let successCount = 0;
@@ -344,12 +444,32 @@ async function buildCMSTemplates(
344
444
 
345
445
  console.log(` Found ${items.length} item(s)`);
346
446
 
447
+ // Prepare client data if clientData is enabled
448
+ let clientDataCollections: Map<string, ClientDataCollection> | undefined;
449
+ if (cmsSchema.clientData?.enabled) {
450
+ const clientData = prepareClientData(cmsSchema.id, items, cmsSchema.clientData);
451
+ if (clientData) {
452
+ if (clientData.strategy === 'inline') {
453
+ // Inline data embedded in HTML
454
+ clientDataCollections = new Map([[cmsSchema.id, clientData]]);
455
+ console.log(` 📦 Client data (inline): ${clientData.items.length} items (${clientData.config.fields?.length || 'all'} fields)`);
456
+ } else if (clientData.strategy === 'static') {
457
+ // Static data written to separate file
458
+ staticCollections.set(cmsSchema.id, clientData);
459
+ console.log(` 📦 Client data (static): ${clientData.items.length} items → /data/${cmsSchema.id}/index.json`);
460
+ }
461
+ }
462
+ }
463
+
347
464
  for (const item of items) {
348
465
  for (const localeConfig of i18nConfig.locales) {
349
466
  const locale = localeConfig.code;
350
467
  const baseUrl = siteUrl || "";
351
468
  const itemPath = buildCMSItemPath(cmsSchema.urlPattern, item, cmsSchema.slugField, locale, i18nConfig);
352
469
 
470
+ // Create CMS item with computed _url for {{cms._url}} template access
471
+ const itemWithUrl: CMSItem = { ...item, _url: itemPath };
472
+
353
473
  // Generate HTML with JS returned separately (CSP-compliant)
354
474
  const result = await generateSSRHTML({
355
475
  pageData,
@@ -359,9 +479,11 @@ async function buildCMSTemplates(
359
479
  useBuiltBundle: true,
360
480
  locale,
361
481
  slugMappings,
362
- cmsContext: { cms: item },
482
+ cmsContext: { cms: itemWithUrl },
363
483
  cmsService,
364
- returnSeparateJS: true
484
+ returnSeparateJS: true,
485
+ pageLibraries: pageData.meta?.libraries,
486
+ clientDataCollections,
365
487
  }) as SSRHTMLResult;
366
488
 
367
489
  // If there's JavaScript, write to external file and update HTML
@@ -388,8 +510,57 @@ async function buildCMSTemplates(
388
510
  successCount++;
389
511
  }
390
512
  }
391
- } catch (error) {
513
+ } catch (error: any) {
514
+ // Capture full error with as much detail as possible
515
+ let errorMessage: string;
516
+
517
+ if (error instanceof Error) {
518
+ // Check for AggregateError (multiple errors)
519
+ if ('errors' in error && Array.isArray(error.errors)) {
520
+ errorMessage = error.errors.map((e: any) => e.stack || e.message || String(e)).join('\n\n');
521
+ }
522
+ // Check for cause chain
523
+ else if (error.cause) {
524
+ const causeMsg = error.cause instanceof Error
525
+ ? (error.cause.stack || error.cause.message)
526
+ : String(error.cause);
527
+ errorMessage = `${error.stack || error.message}\n\nCaused by:\n${causeMsg}`;
528
+ }
529
+ // Bun's BuildMessage has logs array
530
+ else if ('logs' in error && Array.isArray(error.logs)) {
531
+ errorMessage = error.logs.map(formatBunLog).join('\n\n');
532
+ }
533
+ else {
534
+ errorMessage = error.stack || error.message;
535
+ }
536
+ } else if (typeof error === 'object' && error !== null) {
537
+ // Bun BuildOutput has logs
538
+ if (error.logs && Array.isArray(error.logs)) {
539
+ errorMessage = error.logs.map(formatBunLog).join('\n\n');
540
+ } else {
541
+ errorMessage = String(error);
542
+ }
543
+ } else {
544
+ errorMessage = String(error);
545
+ }
546
+
547
+ // If we still just have "Bundle failed", try to get more from Bun.inspect
548
+ if (errorMessage === 'Bundle failed' || errorMessage.includes('Bundle failed\n')) {
549
+ try {
550
+ // Bun.inspect gives the same output as console.log
551
+ const inspected = Bun.inspect(error);
552
+ if (inspected && inspected !== '[object Object]' && inspected.length > errorMessage.length) {
553
+ errorMessage = inspected;
554
+ }
555
+ } catch {}
556
+ }
557
+
392
558
  console.error(`❌ Error processing ${file}:`, error);
559
+ buildErrors.push({
560
+ file: `pages/templates/${file}`,
561
+ message: errorMessage,
562
+ type: errorMessage.includes('minification') || errorMessage.includes('minify') ? 'minify' : 'cms',
563
+ });
393
564
  errorCount++;
394
565
  }
395
566
  }
@@ -400,9 +571,17 @@ async function buildCMSTemplates(
400
571
  /**
401
572
  * Main build function
402
573
  */
403
- async function buildStaticPages(): Promise<void> {
574
+ export async function buildStaticPages(): Promise<void> {
404
575
  console.log("🏗️ Building static HTML files...\n");
405
576
 
577
+ // Clear previous build errors and JS validation cache
578
+ buildErrors.length = 0;
579
+ clearJSValidationCache();
580
+
581
+ // Reset configService to ensure it loads from the correct project directory
582
+ const { configService } = await import("./lib/server/services/configService");
583
+ configService.reset();
584
+
406
585
  // Load project config first
407
586
  const projectConfig = await loadProjectConfig();
408
587
  const siteUrl = (projectConfig as { siteUrl?: string }).siteUrl?.replace(/\/$/, ''); // Remove trailing slash
@@ -417,13 +596,29 @@ async function buildStaticPages(): Promise<void> {
417
596
  // Clean dist directory (removes editor files, old HTML)
418
597
  cleanDist();
419
598
 
599
+ // Clear the JS file cache since cleanDist() removed _scripts/
600
+ // Without this, cached entries would skip file creation on subsequent builds
601
+ jsFileCache.clear();
602
+
420
603
  // Copy fonts, images, icons, and functions directories to dist
421
604
  console.log("📦 Copying assets...");
422
605
  const distDir = projectPaths.dist();
606
+
607
+ // Delete old _errors.json if it exists (start fresh)
608
+ const errorsPath = join(distDir, '_errors.json');
609
+ if (existsSync(errorsPath)) {
610
+ unlinkSync(errorsPath);
611
+ }
423
612
  copyDirectory(projectPaths.fonts(), join(distDir, "fonts"));
424
613
  copyDirectory(projectPaths.images(), join(distDir, "images"));
425
614
  copyDirectory(projectPaths.icons(), join(distDir, "icons"));
426
615
 
616
+ // Copy libraries folder (downloaded external JS/CSS files)
617
+ const librariesDir = join(projectPaths.project, "libraries");
618
+ if (existsSync(librariesDir)) {
619
+ copyDirectory(librariesDir, join(distDir, "libraries"));
620
+ }
621
+
427
622
  // Copy functions folder for Cloudflare Pages
428
623
  const functionsDir = projectPaths.functions();
429
624
  if (existsSync(functionsDir)) {
@@ -452,6 +647,35 @@ async function buildStaticPages(): Promise<void> {
452
647
  }
453
648
  }
454
649
 
650
+ // Generate _headers from CSP config if no custom _headers file exists
651
+ if (!hostingFiles.includes('_headers')) {
652
+ await configService.load();
653
+ const cspConfig = configService.getCSP();
654
+ if (cspConfig && Object.keys(cspConfig).length > 0) {
655
+ const extraScripts = cspConfig.scriptSrc?.join(' ') || '';
656
+ const extraStyles = cspConfig.styleSrc?.join(' ') || '';
657
+ const extraConnect = cspConfig.connectSrc?.join(' ') || '';
658
+ const extraFrames = cspConfig.frameSrc?.join(' ') || '';
659
+ const extraFonts = cspConfig.fontSrc?.join(' ') || '';
660
+ const extraImgs = cspConfig.imgSrc?.join(' ') || '';
661
+
662
+ const cspDirectives = [
663
+ "default-src 'self'",
664
+ `script-src 'self' 'unsafe-inline' https://f.vimeocdn.com https://player.vimeo.com https://www.youtube.com https://s.ytimg.com ${extraScripts}`.trim(),
665
+ `style-src 'self' 'unsafe-inline' https://f.vimeocdn.com ${extraStyles}`.trim(),
666
+ `img-src 'self' data: https: ${extraImgs}`.trim(),
667
+ `connect-src 'self' https://vimeo.com https://*.vimeocdn.com ${extraConnect}`.trim(),
668
+ `frame-src https://player.vimeo.com https://vimeo.com https://www.youtube.com https://www.youtube-nocookie.com ${extraFrames}`.trim(),
669
+ `font-src 'self' data: ${extraFonts}`.trim(),
670
+ "media-src 'self' https: blob:"
671
+ ].join('; ');
672
+
673
+ const headersContent = `/*\n Content-Security-Policy: ${cspDirectives}\n`;
674
+ writeFileSync(join(distDir, '_headers'), headersContent);
675
+ hostingFiles.push('_headers (generated from csp config)');
676
+ }
677
+ }
678
+
455
679
  // Copy .well-known directory if exists
456
680
  const wellKnownDir = join(projectPaths.project, '.well-known');
457
681
  if (existsSync(wellKnownDir)) {
@@ -558,7 +782,8 @@ async function buildStaticPages(): Promise<void> {
558
782
  locale,
559
783
  slugMappings,
560
784
  cmsService,
561
- returnSeparateJS: true
785
+ returnSeparateJS: true,
786
+ pageLibraries: pageData.meta?.libraries,
562
787
  }) as SSRHTMLResult;
563
788
 
564
789
  // If there's JavaScript, write to external file and update HTML
@@ -578,6 +803,11 @@ async function buildStaticPages(): Promise<void> {
578
803
  mkdirSync(outputDir, { recursive: true });
579
804
  }
580
805
 
806
+ // Debug: show end of HTML
807
+ if (urlPath === '/') {
808
+ console.log('[DEBUG] Last 500 chars of HTML:', finalHtml.slice(-500));
809
+ }
810
+
581
811
  await writeFile(outputPath, finalHtml, "utf-8");
582
812
 
583
813
  generatedUrls.add(urlPath);
@@ -585,14 +815,64 @@ async function buildStaticPages(): Promise<void> {
585
815
  successCount++;
586
816
  }
587
817
 
588
- } catch (error) {
818
+ } catch (error: any) {
819
+ // Capture full error with as much detail as possible
820
+ let errorMessage: string;
821
+
822
+ if (error instanceof Error) {
823
+ // Check for AggregateError (multiple errors)
824
+ if ('errors' in error && Array.isArray(error.errors)) {
825
+ errorMessage = error.errors.map((e: any) => e.stack || e.message || String(e)).join('\n\n');
826
+ }
827
+ // Check for cause chain
828
+ else if (error.cause) {
829
+ const causeMsg = error.cause instanceof Error
830
+ ? (error.cause.stack || error.cause.message)
831
+ : String(error.cause);
832
+ errorMessage = `${error.stack || error.message}\n\nCaused by:\n${causeMsg}`;
833
+ }
834
+ // Bun's BuildMessage has logs array
835
+ else if ('logs' in error && Array.isArray(error.logs)) {
836
+ errorMessage = error.logs.map(formatBunLog).join('\n\n');
837
+ }
838
+ else {
839
+ errorMessage = error.stack || error.message;
840
+ }
841
+ } else if (typeof error === 'object' && error !== null) {
842
+ // Bun BuildOutput has logs
843
+ if (error.logs && Array.isArray(error.logs)) {
844
+ errorMessage = error.logs.map(formatBunLog).join('\n\n');
845
+ } else {
846
+ errorMessage = String(error);
847
+ }
848
+ } else {
849
+ errorMessage = String(error);
850
+ }
851
+
852
+ // If we still just have "Bundle failed", try to get more from Bun.inspect
853
+ if (errorMessage === 'Bundle failed' || errorMessage.includes('Bundle failed\n')) {
854
+ try {
855
+ // Bun.inspect gives the same output as console.log
856
+ const inspected = Bun.inspect(error);
857
+ if (inspected && inspected !== '[object Object]' && inspected.length > errorMessage.length) {
858
+ errorMessage = inspected;
859
+ }
860
+ } catch {}
861
+ }
862
+
589
863
  console.error(`❌ Error building ${basePath}:`, error);
864
+ buildErrors.push({
865
+ file: `pages/${file}`,
866
+ message: errorMessage,
867
+ type: errorMessage.includes('minification') || errorMessage.includes('minify') ? 'minify' : 'render',
868
+ });
590
869
  errorCount++;
591
870
  }
592
871
  }
593
872
 
594
873
  // Build CMS templates from pages/templates/
595
874
  const templatesDir = join(pagesDir, 'templates');
875
+ const staticCollections = new Map<string, ClientDataCollection>();
596
876
  const cmsResult = await buildCMSTemplates(
597
877
  templatesDir,
598
878
  globalComponents,
@@ -601,11 +881,15 @@ async function buildStaticPages(): Promise<void> {
601
881
  distDir,
602
882
  cmsService,
603
883
  generatedUrls,
884
+ staticCollections,
604
885
  siteUrl
605
886
  );
606
887
  successCount += cmsResult.success;
607
888
  errorCount += cmsResult.errors;
608
889
 
890
+ // Generate static data files for collections with 'static' strategy
891
+ await generateStaticDataFiles(staticCollections, distDir);
892
+
609
893
  // Generate SEO files (robots.txt and sitemap.xml)
610
894
  if (siteUrl) {
611
895
  await generateRobotsTxt(siteUrl, distDir);
@@ -626,6 +910,9 @@ async function buildStaticPages(): Promise<void> {
626
910
  console.log(` - fonts/ (Custom fonts)`);
627
911
  console.log(` - images/ (Image assets)`);
628
912
  console.log(` - icons/ (Favicon and icons)`);
913
+ if (staticCollections.size > 0) {
914
+ console.log(` - data/ (CMS collection data for client filtering)`);
915
+ }
629
916
  if (existsSync(functionsDir)) {
630
917
  console.log(` - functions/ (Cloudflare Pages Functions)`);
631
918
  }
@@ -633,11 +920,44 @@ async function buildStaticPages(): Promise<void> {
633
920
  console.log(` - robots.txt, sitemap.xml (SEO)`);
634
921
  }
635
922
  console.log(` - No React, no client-router ✓`);
923
+
924
+ // Collect component JS validation errors
925
+ const jsErrors = getJSValidationErrors();
926
+ for (const { component, error } of jsErrors) {
927
+ buildErrors.push({
928
+ file: `components/${component}`,
929
+ message: error,
930
+ type: 'minify',
931
+ });
932
+ }
933
+
934
+ // Write build errors to _errors.json for static server overlay
935
+ if (buildErrors.length > 0) {
936
+ // Deduplicate errors by message (same error may occur on multiple pages)
937
+ const seenMessages = new Set<string>();
938
+ const uniqueErrors = buildErrors.filter(err => {
939
+ if (seenMessages.has(err.message)) return false;
940
+ seenMessages.add(err.message);
941
+ return true;
942
+ });
943
+
944
+ const errorsData: BuildErrorsData = {
945
+ errors: uniqueErrors,
946
+ timestamp: Date.now(),
947
+ };
948
+ await writeFile(errorsPath, JSON.stringify(errorsData, null, 2), 'utf-8');
949
+ const countMsg = uniqueErrors.length === buildErrors.length
950
+ ? `${uniqueErrors.length} error${uniqueErrors.length === 1 ? '' : 's'}`
951
+ : `${uniqueErrors.length} unique error${uniqueErrors.length === 1 ? '' : 's'} (affected ${buildErrors.length} files)`;
952
+ console.log(`\n⚠️ Build errors written to dist/_errors.json (${countMsg})`);
953
+ }
636
954
  }
637
955
 
638
- // Run build
639
- buildStaticPages().catch((error) => {
640
- console.error("❌ Build failed:", error);
641
- process.exit(1);
642
- });
956
+ // Run build only when executed directly (CLI), not when imported as a module
957
+ if (import.meta.main) {
958
+ await buildStaticPages().catch((error) => {
959
+ console.error("❌ Build failed:", error);
960
+ process.exit(1);
961
+ });
962
+ }
643
963
 
@@ -10,6 +10,7 @@ import { StyleInjector } from "./styles/StyleInjector";
10
10
  import { ScriptExecutor } from "./scripts/ScriptExecutor";
11
11
  import { PrefetchService } from "./services/PrefetchService";
12
12
  import { elementRegistry } from "./elementRegistry";
13
+ import { setGlobalTemplateContext } from "../shared/globalTemplateContext";
13
14
  import type { PrefetchConfig } from "../shared/types/prefetch";
14
15
 
15
16
  /**
@@ -56,6 +57,9 @@ function isInEditorIframe(): boolean {
56
57
  * Initialize core client services
57
58
  */
58
59
  export function initializeClient(options: ClientInitOptions = {}): ClientServices {
60
+ // Set editor mode when running inside editor iframe (disables video autoplay, etc.)
61
+ setGlobalTemplateContext({ isEditorMode: isInEditorIframe() });
62
+
59
63
  const componentRegistry = globalComponentRegistry;
60
64
 
61
65
  const prefetchService = new PrefetchService({