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.
- package/bin/cli.ts +18 -1
- package/build-static.ts +336 -16
- package/lib/client/ClientInitializer.ts +4 -0
- package/lib/client/core/ComponentBuilder.test.ts +530 -9
- package/lib/client/core/ComponentBuilder.ts +97 -17
- package/lib/client/core/ComponentRenderer.tsx +6 -2
- package/lib/client/core/builders/cmsListBuilder.ts +191 -12
- package/lib/client/core/builders/embedBuilder.ts +8 -9
- package/lib/client/core/builders/index.ts +2 -2
- package/lib/client/core/builders/{objectLinkBuilder.ts → linkNodeBuilder.ts} +24 -24
- package/lib/client/core/builders/localeListBuilder.ts +5 -5
- package/lib/client/core/builders/types.ts +8 -0
- package/lib/client/hmr/HMRManager.tsx +64 -36
- package/lib/client/hmrWebSocket.ts +15 -0
- package/lib/client/meno-filter/MenoFilter.test.ts +1230 -0
- package/lib/client/meno-filter/MenoFilter.ts +1281 -0
- package/lib/client/meno-filter/bindings.ts +554 -0
- package/lib/client/meno-filter/constants.ts +72 -0
- package/lib/client/meno-filter/index.ts +58 -0
- package/lib/client/meno-filter/init.ts +259 -0
- package/lib/client/meno-filter/renderer.ts +410 -0
- package/lib/client/meno-filter/script.generated.ts +56 -0
- package/lib/client/meno-filter/types.ts +165 -0
- package/lib/client/meno-filter/ui.ts +364 -0
- package/lib/client/meno-filter/updates.ts +674 -0
- package/lib/client/meno-filter/utils.ts +44 -0
- package/lib/client/routing/Router.tsx +32 -4
- package/lib/client/templateEngine.ts +39 -14
- package/lib/server/createServer.ts +7 -0
- package/lib/server/index.ts +7 -1
- package/lib/server/routes/api/cms.ts +6 -1
- package/lib/server/routes/api/core-routes.ts +26 -0
- package/lib/server/routes/index.ts +45 -0
- package/lib/server/routes/pages.ts +113 -15
- package/lib/server/scriptCache.ts +34 -0
- package/lib/server/services/configService.ts +48 -0
- package/lib/server/services/fileWatcherService.ts +10 -2
- package/lib/server/ssr/attributeBuilder.ts +6 -2
- package/lib/server/ssr/buildErrorOverlay.ts +341 -0
- package/lib/server/ssr/clientDataInjector.test.ts +337 -0
- package/lib/server/ssr/clientDataInjector.ts +161 -0
- package/lib/server/ssr/errorOverlay.test.ts +73 -0
- package/lib/server/ssr/errorOverlay.ts +263 -0
- package/lib/server/ssr/htmlGenerator.ts +89 -6
- package/lib/server/ssr/index.ts +2 -1
- package/lib/server/ssr/jsCollector.ts +107 -6
- package/lib/server/ssr/ssrRenderer.ts +332 -57
- package/lib/server/ssrRenderer.test.ts +307 -11
- package/lib/server/validateStyleCoverage.ts +10 -11
- package/lib/shared/cmsQueryParser.test.ts +288 -0
- package/lib/shared/cmsQueryParser.ts +188 -0
- package/lib/shared/constants.test.ts +1 -1
- package/lib/shared/constants.ts +5 -1
- package/lib/shared/cssGeneration.test.ts +113 -0
- package/lib/shared/cssGeneration.ts +40 -2
- package/lib/shared/cssProperties.ts +84 -7
- package/lib/shared/elementClassName.test.ts +6 -6
- package/lib/shared/elementClassName.ts +4 -4
- package/lib/shared/globalTemplateContext.ts +45 -0
- package/lib/shared/index.ts +8 -0
- package/lib/shared/interactiveStyles.test.ts +16 -16
- package/lib/shared/itemTemplateUtils.test.ts +151 -1
- package/lib/shared/itemTemplateUtils.ts +113 -2
- package/lib/shared/libraryLoader.ts +128 -0
- package/lib/shared/nodeUtils.test.ts +134 -11
- package/lib/shared/nodeUtils.ts +95 -5
- package/lib/shared/registry/index.ts +1 -1
- package/lib/shared/registry/nodeTypes/CMSListNodeType.ts +8 -1
- package/lib/shared/registry/nodeTypes/ComponentInstanceNodeType.ts +3 -2
- package/lib/shared/registry/nodeTypes/EmbedNodeType.ts +5 -2
- package/lib/shared/registry/nodeTypes/HtmlNodeType.ts +3 -2
- package/lib/shared/registry/nodeTypes/{ObjectLinkNodeType.ts → LinkNodeType.ts} +13 -10
- package/lib/shared/registry/nodeTypes/LocaleListNodeType.ts +5 -2
- package/lib/shared/registry/nodeTypes/index.ts +5 -5
- package/lib/shared/tree/PathBuilder.test.ts +395 -0
- package/lib/shared/tree/PathBuilder.ts +38 -12
- package/lib/shared/treePathUtils.test.ts +2 -2
- package/lib/shared/treePathUtils.ts +2 -2
- package/lib/shared/types/api.ts +3 -0
- package/lib/shared/types/cms.ts +86 -2
- package/lib/shared/types/config.ts +38 -0
- package/lib/shared/types/index.ts +17 -1
- package/lib/shared/types/libraries.ts +65 -0
- package/lib/shared/types/nodes.ts +1 -1
- package/lib/shared/utilityClassConfig.ts +151 -4
- package/lib/shared/utilityClassMapper.test.ts +30 -0
- package/lib/shared/utilityClassMapper.ts +54 -1
- package/lib/shared/utils.ts +21 -3
- package/lib/shared/validation/schemas.ts +111 -11
- package/package.json +3 -1
- 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.
|
|
93
|
+
if (result.outputs.length > 0) {
|
|
49
94
|
return await result.outputs[0].text();
|
|
50
95
|
}
|
|
51
|
-
|
|
52
|
-
|
|
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:
|
|
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
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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({
|