meno-core 1.0.52 → 1.0.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build-astro.ts +183 -13
- package/build-next.ts +1361 -0
- package/build-static.ts +7 -5
- package/dist/bin/cli.js +2 -2
- package/dist/build-static.js +6 -6
- package/dist/chunks/{chunk-HNLUO36W.js → chunk-GZHGVVW3.js} +2 -2
- package/dist/chunks/chunk-GZHGVVW3.js.map +7 -0
- package/dist/chunks/{chunk-LPVETICS.js → chunk-H3GJ4H2U.js} +185 -1
- package/dist/chunks/chunk-H3GJ4H2U.js.map +7 -0
- package/dist/chunks/{chunk-CXCBV2M7.js → chunk-IGYR22T6.js} +76 -270
- package/dist/chunks/chunk-IGYR22T6.js.map +7 -0
- package/dist/chunks/{chunk-LHLHPYSP.js → chunk-JGP5A3Y5.js} +12 -11
- package/dist/chunks/chunk-JGP5A3Y5.js.map +7 -0
- package/dist/chunks/{chunk-7NIC4I3V.js → chunk-JGWFTO6P.js} +167 -21
- package/dist/chunks/chunk-JGWFTO6P.js.map +7 -0
- package/dist/chunks/{chunk-EDQSMAMP.js → chunk-O3NAGJP4.js} +85 -4
- package/dist/chunks/chunk-O3NAGJP4.js.map +7 -0
- package/dist/chunks/{chunk-H4JSCDNW.js → chunk-QB2LNO4W.js} +24 -1
- package/dist/chunks/chunk-QB2LNO4W.js.map +7 -0
- package/dist/chunks/{chunk-A725KYFK.js → chunk-R6XHAFBF.js} +561 -112
- package/dist/chunks/chunk-R6XHAFBF.js.map +7 -0
- package/dist/chunks/{chunk-J23ZX5AP.js → chunk-X754AHS5.js} +277 -1
- package/dist/chunks/chunk-X754AHS5.js.map +7 -0
- package/dist/chunks/{chunk-2QK6U5UK.js → chunk-YBLHKYFF.js} +12 -2
- package/dist/chunks/chunk-YBLHKYFF.js.map +7 -0
- package/dist/chunks/{constants-GWBAD66U.js → constants-STK2YBIW.js} +2 -2
- package/dist/entries/server-router.js +7 -7
- package/dist/lib/client/index.js +354 -59
- package/dist/lib/client/index.js.map +4 -4
- package/dist/lib/server/index.js +1458 -190
- package/dist/lib/server/index.js.map +4 -4
- package/dist/lib/shared/index.js +202 -34
- package/dist/lib/shared/index.js.map +4 -4
- package/dist/lib/test-utils/index.js +1 -1
- package/entries/client-router.tsx +5 -165
- package/lib/client/ErrorBoundary.test.tsx +27 -25
- package/lib/client/ErrorBoundary.tsx +34 -19
- package/lib/client/core/ComponentBuilder.ts +19 -2
- package/lib/client/core/builders/embedBuilder.ts +8 -4
- package/lib/client/core/builders/listBuilder.ts +23 -4
- package/lib/client/fontFamiliesService.test.ts +76 -0
- package/lib/client/fontFamiliesService.ts +69 -0
- package/lib/client/hmrCssReload.ts +160 -0
- package/lib/client/hooks/useColorVariables.ts +2 -0
- package/lib/client/index.ts +4 -0
- package/lib/client/meno-filter/ui.ts +2 -0
- package/lib/client/routing/RouteLoader.test.ts +2 -2
- package/lib/client/routing/RouteLoader.ts +8 -2
- package/lib/client/routing/Router.tsx +81 -15
- package/lib/client/scripts/ScriptExecutor.test.ts +143 -0
- package/lib/client/scripts/ScriptExecutor.ts +56 -2
- package/lib/client/styles/StyleInjector.ts +20 -5
- package/lib/client/styles/UtilityClassCollector.ts +7 -1
- package/lib/client/styles/cspNonce.test.ts +67 -0
- package/lib/client/styles/cspNonce.ts +63 -0
- package/lib/client/templateEngine.test.ts +80 -0
- package/lib/client/templateEngine.ts +5 -0
- package/lib/server/astro/cmsPageEmitter.ts +35 -5
- package/lib/server/astro/componentEmitter.ts +61 -5
- package/lib/server/astro/nodeToAstro.ts +149 -11
- package/lib/server/astro/normalizeOrphanTemplateProps.test.ts +264 -0
- package/lib/server/astro/normalizeOrphanTemplateProps.ts +184 -0
- package/lib/server/createServer.ts +11 -0
- package/lib/server/draftPageStore.ts +49 -0
- package/lib/server/fileWatcher.ts +62 -2
- package/lib/server/index.ts +13 -1
- package/lib/server/providers/fileSystemPageProvider.ts +8 -0
- package/lib/server/routes/api/components.ts +9 -4
- package/lib/server/routes/api/core-routes.ts +2 -2
- package/lib/server/routes/api/pages.ts +14 -22
- package/lib/server/routes/api/shared.ts +56 -0
- package/lib/server/routes/index.ts +90 -0
- package/lib/server/routes/pages.ts +13 -6
- package/lib/server/services/componentService.test.ts +199 -2
- package/lib/server/services/componentService.ts +354 -49
- package/lib/server/services/fileWatcherService.ts +4 -24
- package/lib/server/services/pageService.test.ts +23 -0
- package/lib/server/services/pageService.ts +124 -6
- package/lib/server/ssr/attributeBuilder.ts +8 -2
- package/lib/server/ssr/buildErrorOverlay.ts +1 -1
- package/lib/server/ssr/errorOverlay.test.ts +21 -2
- package/lib/server/ssr/errorOverlay.ts +38 -11
- package/lib/server/ssr/htmlGenerator.test.ts +53 -13
- package/lib/server/ssr/htmlGenerator.ts +71 -27
- package/lib/server/ssr/liveReloadIntegration.test.ts +123 -2
- package/lib/server/ssr/ssrRenderer.test.ts +67 -0
- package/lib/server/ssr/ssrRenderer.ts +94 -9
- package/lib/server/websocketManager.ts +0 -1
- package/lib/shared/componentRefs.ts +45 -0
- package/lib/shared/constants.ts +8 -0
- package/lib/shared/cssGeneration.ts +2 -0
- package/lib/shared/cssProperties.ts +184 -0
- package/lib/shared/expressionEvaluator.ts +54 -0
- package/lib/shared/fontCss.ts +101 -0
- package/lib/shared/fontLoader.ts +8 -86
- package/lib/shared/friendlyError.test.ts +87 -0
- package/lib/shared/friendlyError.ts +121 -0
- package/lib/shared/hrefRefs.test.ts +130 -0
- package/lib/shared/hrefRefs.ts +100 -0
- package/lib/shared/index.ts +52 -0
- package/lib/shared/inlineSvgStyleRules.test.ts +108 -0
- package/lib/shared/inlineSvgStyleRules.ts +134 -0
- package/lib/shared/interfaces/contentProvider.ts +13 -0
- package/lib/shared/itemTemplateUtils.test.ts +14 -0
- package/lib/shared/itemTemplateUtils.ts +4 -1
- package/lib/shared/registry/NodeTypeDefinition.ts +1 -1
- package/lib/shared/registry/nodeTypes/LinkNodeType.ts +1 -1
- package/lib/shared/slugTranslator.test.ts +24 -0
- package/lib/shared/slugTranslator.ts +24 -0
- package/lib/shared/styleNodeUtils.ts +4 -1
- package/lib/shared/tree/PathBuilder.test.ts +128 -1
- package/lib/shared/tree/PathBuilder.ts +83 -31
- package/lib/shared/types/comment.ts +99 -0
- package/lib/shared/types/index.ts +12 -0
- package/lib/shared/types/rendering.ts +8 -0
- package/lib/shared/utilityClassConfig.ts +4 -2
- package/lib/shared/utilityClassMapper.test.ts +24 -0
- package/lib/shared/validation/commentValidators.ts +69 -0
- package/lib/shared/validation/index.ts +1 -0
- package/lib/shared/viewportUnits.integration.test.ts +42 -0
- package/lib/shared/viewportUnits.test.ts +103 -0
- package/lib/shared/viewportUnits.ts +63 -0
- package/lib/test-utils/dom-setup.ts +6 -0
- package/package.json +1 -1
- package/dist/chunks/chunk-2QK6U5UK.js.map +0 -7
- package/dist/chunks/chunk-7NIC4I3V.js.map +0 -7
- package/dist/chunks/chunk-A725KYFK.js.map +0 -7
- package/dist/chunks/chunk-CXCBV2M7.js.map +0 -7
- package/dist/chunks/chunk-EDQSMAMP.js.map +0 -7
- package/dist/chunks/chunk-H4JSCDNW.js.map +0 -7
- package/dist/chunks/chunk-HNLUO36W.js.map +0 -7
- package/dist/chunks/chunk-J23ZX5AP.js.map +0 -7
- package/dist/chunks/chunk-LHLHPYSP.js.map +0 -7
- package/dist/chunks/chunk-LPVETICS.js.map +0 -7
- /package/dist/chunks/{constants-GWBAD66U.js.map → constants-STK2YBIW.js.map} +0 -0
|
@@ -147,6 +147,46 @@ export class PageService {
|
|
|
147
147
|
this.pageCache.set(path, content, lineMap);
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Re-read a single page from disk via the provider and refresh the cache.
|
|
152
|
+
*
|
|
153
|
+
* Used by the file watcher for live reload. Provider-based (not a hardcoded
|
|
154
|
+
* JSON read), so it works for any format — the FileSystem provider returns the
|
|
155
|
+
* raw JSON and the Astro provider returns the parsed model serialized to JSON.
|
|
156
|
+
*
|
|
157
|
+
* @returns true if the cache changed (page updated or confirmed-deleted) and a
|
|
158
|
+
* reload broadcast is warranted; false if the read failed transiently (e.g. a
|
|
159
|
+
* partial mid-write or parse error), in which case the cache is left intact and
|
|
160
|
+
* the next watcher event retries.
|
|
161
|
+
*/
|
|
162
|
+
async reloadPageFromDisk(path: string): Promise<boolean> {
|
|
163
|
+
if (!this.provider) return false;
|
|
164
|
+
|
|
165
|
+
let content: string | null = null;
|
|
166
|
+
try {
|
|
167
|
+
content = await this.provider.get(path);
|
|
168
|
+
} catch {
|
|
169
|
+
// Transient (mid-write / partial .astro parse error) — keep the cache as-is.
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (content) {
|
|
174
|
+
this.pageCache.set(path, content, buildLineMap(content));
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// No content: only drop from cache if the page is truly gone, not mid-write.
|
|
179
|
+
try {
|
|
180
|
+
if (!(await this.provider.exists(path))) {
|
|
181
|
+
this.pageCache.delete(path);
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
} catch {
|
|
185
|
+
/* leave cache intact */
|
|
186
|
+
}
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
150
190
|
/**
|
|
151
191
|
* Delete page from cache and optionally from storage
|
|
152
192
|
*
|
|
@@ -230,6 +270,21 @@ export class PageService {
|
|
|
230
270
|
return this.pageCache.getLineMap(path);
|
|
231
271
|
}
|
|
232
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Pages base directory — provider-aware (e.g. `src/pages` for astro projects),
|
|
275
|
+
* falling back to `projectPaths.pages()` when the provider doesn't expose
|
|
276
|
+
* `baseDir()` (or no provider is set). Used by the folder/move/rename ops that
|
|
277
|
+
* touch the filesystem directly.
|
|
278
|
+
*/
|
|
279
|
+
private pagesBaseDir(): string {
|
|
280
|
+
return this.provider?.baseDir?.() ?? projectPaths.pages();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Page file extension (e.g. `.astro` for astro projects), defaulting to `.json`. */
|
|
284
|
+
private pageExt(): string {
|
|
285
|
+
return this.provider?.extension?.() ?? '.json';
|
|
286
|
+
}
|
|
287
|
+
|
|
233
288
|
/**
|
|
234
289
|
* Get all page folders
|
|
235
290
|
*
|
|
@@ -238,7 +293,7 @@ export class PageService {
|
|
|
238
293
|
* @returns Sorted array of folder names
|
|
239
294
|
*/
|
|
240
295
|
getAllFolders(): string[] {
|
|
241
|
-
const pagesDir =
|
|
296
|
+
const pagesDir = this.pagesBaseDir();
|
|
242
297
|
if (!existsSync(pagesDir)) {
|
|
243
298
|
return [];
|
|
244
299
|
}
|
|
@@ -284,7 +339,7 @@ export class PageService {
|
|
|
284
339
|
}
|
|
285
340
|
}
|
|
286
341
|
|
|
287
|
-
const pagesDir =
|
|
342
|
+
const pagesDir = this.pagesBaseDir();
|
|
288
343
|
const folderPath = join(pagesDir, trimmed);
|
|
289
344
|
|
|
290
345
|
if (existsSync(folderPath)) {
|
|
@@ -305,7 +360,8 @@ export class PageService {
|
|
|
305
360
|
*/
|
|
306
361
|
async movePage(pagePath: string, newFolder: string): Promise<void> {
|
|
307
362
|
const { rename } = await import('fs/promises');
|
|
308
|
-
const pagesDir =
|
|
363
|
+
const pagesDir = this.pagesBaseDir();
|
|
364
|
+
const ext = this.pageExt();
|
|
309
365
|
|
|
310
366
|
// Convert page path to file name: "/blog/post" -> "blog/post", "/" -> "index"
|
|
311
367
|
const pageName = pagePath === '/' ? 'index' : pagePath.substring(1);
|
|
@@ -313,10 +369,10 @@ export class PageService {
|
|
|
313
369
|
const slashIndex = pageName.lastIndexOf('/');
|
|
314
370
|
const baseName = slashIndex >= 0 ? pageName.substring(slashIndex + 1) : pageName;
|
|
315
371
|
|
|
316
|
-
const sourceFile = join(pagesDir, `${pageName}
|
|
372
|
+
const sourceFile = join(pagesDir, `${pageName}${ext}`);
|
|
317
373
|
const targetFile = newFolder
|
|
318
|
-
? join(pagesDir, newFolder, `${baseName}
|
|
319
|
-
: join(pagesDir, `${baseName}
|
|
374
|
+
? join(pagesDir, newFolder, `${baseName}${ext}`)
|
|
375
|
+
: join(pagesDir, `${baseName}${ext}`);
|
|
320
376
|
|
|
321
377
|
// Create target directory if needed
|
|
322
378
|
if (newFolder) {
|
|
@@ -359,6 +415,68 @@ export class PageService {
|
|
|
359
415
|
}
|
|
360
416
|
}
|
|
361
417
|
|
|
418
|
+
/**
|
|
419
|
+
* Rename a page (file rename + cache update).
|
|
420
|
+
*
|
|
421
|
+
* Renames the underlying .json file from `oldPath`'s filename to `newPath`'s
|
|
422
|
+
* filename, keeping the same folder. Updates the in-memory cache so the
|
|
423
|
+
* page is now keyed at `newPath`. Folder moves stay with `movePage()` —
|
|
424
|
+
* this is purely "change the URL slug of an existing page" for
|
|
425
|
+
* single-locale projects.
|
|
426
|
+
*
|
|
427
|
+
* @param oldPath - Current page path (e.g., "/about" or "/blog/post")
|
|
428
|
+
* @param newPath - New page path with same folder (e.g., "/about-us" or "/blog/new-slug")
|
|
429
|
+
* @throws {Error} If oldPath doesn't exist, newPath already exists, or paths
|
|
430
|
+
* reference different folders.
|
|
431
|
+
*/
|
|
432
|
+
async renamePage(oldPath: string, newPath: string): Promise<void> {
|
|
433
|
+
if (oldPath === newPath) return;
|
|
434
|
+
if (newPath === '/') {
|
|
435
|
+
throw new Error('Cannot rename a page to the index path');
|
|
436
|
+
}
|
|
437
|
+
if (!newPath.startsWith('/')) {
|
|
438
|
+
throw new Error('newPath must start with "/"');
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const oldName = oldPath === '/' ? 'index' : oldPath.substring(1);
|
|
442
|
+
const newName = newPath.substring(1);
|
|
443
|
+
|
|
444
|
+
const oldSlash = oldName.lastIndexOf('/');
|
|
445
|
+
const newSlash = newName.lastIndexOf('/');
|
|
446
|
+
const oldFolder = oldSlash >= 0 ? oldName.substring(0, oldSlash) : '';
|
|
447
|
+
const newFolder = newSlash >= 0 ? newName.substring(0, newSlash) : '';
|
|
448
|
+
if (oldFolder !== newFolder) {
|
|
449
|
+
throw new Error('renamePage only changes the filename; use movePage to change folder');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const baseSegment = newSlash >= 0 ? newName.substring(newSlash + 1) : newName;
|
|
453
|
+
if (!/^[a-z0-9][a-z0-9-_]*$/.test(baseSegment)) {
|
|
454
|
+
throw new Error('Page slug can only contain lowercase letters, numbers, dashes, and underscores');
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const { rename } = await import('fs/promises');
|
|
458
|
+
const pagesDir = this.pagesBaseDir();
|
|
459
|
+
const ext = this.pageExt();
|
|
460
|
+
const sourceFile = join(pagesDir, `${oldName}${ext}`);
|
|
461
|
+
const targetFile = join(pagesDir, `${newName}${ext}`);
|
|
462
|
+
|
|
463
|
+
if (!existsSync(sourceFile)) {
|
|
464
|
+
throw new Error(`Page not found: ${oldPath}`);
|
|
465
|
+
}
|
|
466
|
+
if (existsSync(targetFile)) {
|
|
467
|
+
throw new Error(`Page already exists: ${newPath}`);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
await rename(sourceFile, targetFile);
|
|
471
|
+
|
|
472
|
+
const content = this.pageCache.getContent(oldPath);
|
|
473
|
+
if (content) {
|
|
474
|
+
this.pageCache.delete(oldPath);
|
|
475
|
+
const lineMap = buildLineMap(content);
|
|
476
|
+
this.pageCache.set(newPath, content, lineMap);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
362
480
|
/**
|
|
363
481
|
* Get slug mappings for all pages
|
|
364
482
|
*
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Pure functions with no external dependencies
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { rewriteViewportUnits } from '../../shared/viewportUnits';
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* Escape HTML special characters to prevent XSS
|
|
8
10
|
*/
|
|
@@ -127,13 +129,17 @@ export function styleToString(style: Record<string, string | number> | undefined
|
|
|
127
129
|
for (const [key, value] of Object.entries(style)) {
|
|
128
130
|
if (value === null || value === undefined) continue;
|
|
129
131
|
|
|
132
|
+
// Route every value through the viewport-unit rewriter so design-canvas
|
|
133
|
+
// mode can pin vh/vw to a stable px value. See viewportUnits.ts.
|
|
134
|
+
const rewritten = rewriteViewportUnits(String(value));
|
|
135
|
+
|
|
130
136
|
// Handle CSS variables (already in kebab-case with -- prefix)
|
|
131
137
|
if (key.startsWith('--')) {
|
|
132
|
-
declarations.push(`${key}: ${escapeHtml(
|
|
138
|
+
declarations.push(`${key}: ${escapeHtml(rewritten)}`);
|
|
133
139
|
} else {
|
|
134
140
|
// Convert camelCase to kebab-case for regular CSS properties
|
|
135
141
|
const cssProperty = key.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
136
|
-
declarations.push(`${cssProperty}: ${escapeHtml(
|
|
142
|
+
declarations.push(`${cssProperty}: ${escapeHtml(rewritten)}`);
|
|
137
143
|
}
|
|
138
144
|
}
|
|
139
145
|
|
|
@@ -73,7 +73,7 @@ export function generateBuildErrorPage(data: BuildErrorsData, cspNonce?: string)
|
|
|
73
73
|
<meta charset="UTF-8">
|
|
74
74
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
75
75
|
<title>Build Failed</title>
|
|
76
|
-
<style>
|
|
76
|
+
<style${nonceAttr}>
|
|
77
77
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
78
78
|
|
|
79
79
|
body {
|
|
@@ -49,12 +49,31 @@ describe('generateErrorPage', () => {
|
|
|
49
49
|
expect(html).toContain('[object Object]');
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
-
it('includes
|
|
52
|
+
it('includes technical-details toggle when error has stack', () => {
|
|
53
53
|
const error = new Error('Test error');
|
|
54
54
|
const html = generateErrorPage(error);
|
|
55
55
|
|
|
56
56
|
expect(html).toContain('stackToggle');
|
|
57
|
-
expect(html).toContain('Show
|
|
57
|
+
expect(html).toContain('Show technical details');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('shows a friendly title/message and keeps the raw message available', () => {
|
|
61
|
+
const html = generateErrorPage(new Error("Cannot read properties of null (reading 'length')"));
|
|
62
|
+
|
|
63
|
+
// Friendly, plain-language copy is shown...
|
|
64
|
+
expect(html).toContain("A section couldn't load its content");
|
|
65
|
+
expect(html).toContain('error-friendly');
|
|
66
|
+
// ...while the raw message is still present (under technical details / copy).
|
|
67
|
+
expect(html).toContain("Cannot read properties of null (reading 'length')");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('sends friendly + raw fields in the PREVIEW_ERROR payload', () => {
|
|
71
|
+
const html = generateErrorPage(new Error("Cannot read properties of null (reading 'length')"));
|
|
72
|
+
|
|
73
|
+
expect(html).toContain('"title"');
|
|
74
|
+
expect(html).toContain('"raw"');
|
|
75
|
+
// The visible message field carries the friendly text, not the raw JS error.
|
|
76
|
+
expect(html).toContain("A section couldn't load its content");
|
|
58
77
|
});
|
|
59
78
|
|
|
60
79
|
it('sends postMessage to parent window', () => {
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Also sends error details to parent editor via postMessage
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { toFriendlyError } from '../../shared/friendlyError';
|
|
8
|
+
|
|
7
9
|
interface ErrorInfo {
|
|
8
10
|
message: string;
|
|
9
11
|
stack?: string;
|
|
@@ -60,15 +62,27 @@ function safeJsonForScript(data: unknown): string {
|
|
|
60
62
|
*/
|
|
61
63
|
export function generateErrorPage(error: unknown, context?: string, cspNonce?: string): string {
|
|
62
64
|
const errorInfo = extractErrorInfo(error);
|
|
65
|
+
const friendly = toFriendlyError(errorInfo.message);
|
|
63
66
|
const errorMessage = escapeHtml(errorInfo.message);
|
|
64
67
|
const errorStack = errorInfo.stack ? escapeHtml(errorInfo.stack) : '';
|
|
68
|
+
// Escape first (XSS-safe), then turn `backtick` spans into <code> for display.
|
|
69
|
+
const renderInline = (s: string) =>
|
|
70
|
+
escapeHtml(s).replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
71
|
+
const friendlyTitle = escapeHtml(friendly.title);
|
|
72
|
+
const friendlyMessage = renderInline(friendly.friendlyMessage);
|
|
73
|
+
const friendlyHint = friendly.hint ? renderInline(friendly.hint) : '';
|
|
65
74
|
const nonceAttr = cspNonce ? ` nonce="${cspNonce}"` : '';
|
|
66
75
|
|
|
67
|
-
// Safely encode error data for script
|
|
76
|
+
// Safely encode error data for script. Includes friendly fields so the parent
|
|
77
|
+
// editor can show plain-language text without re-mapping, plus the raw message
|
|
78
|
+
// and stack so "Copy" still yields the exact original for debugging.
|
|
68
79
|
const errorDataJson = safeJsonForScript({
|
|
69
80
|
type: 'PREVIEW_ERROR',
|
|
70
81
|
error: {
|
|
71
|
-
message:
|
|
82
|
+
message: friendly.friendlyMessage,
|
|
83
|
+
title: friendly.title,
|
|
84
|
+
hint: friendly.hint,
|
|
85
|
+
raw: errorInfo.message,
|
|
72
86
|
stack: errorInfo.stack,
|
|
73
87
|
context,
|
|
74
88
|
},
|
|
@@ -85,7 +99,7 @@ export function generateErrorPage(error: unknown, context?: string, cspNonce?: s
|
|
|
85
99
|
<meta charset="UTF-8">
|
|
86
100
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
87
101
|
<title>Preview Error</title>
|
|
88
|
-
<style>
|
|
102
|
+
<style${nonceAttr}>
|
|
89
103
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
90
104
|
body {
|
|
91
105
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
@@ -122,6 +136,21 @@ export function generateErrorPage(error: unknown, context?: string, cspNonce?: s
|
|
|
122
136
|
text-transform: uppercase;
|
|
123
137
|
letter-spacing: 0.5px;
|
|
124
138
|
}
|
|
139
|
+
.error-friendly {
|
|
140
|
+
font-size: 15px;
|
|
141
|
+
line-height: 1.6;
|
|
142
|
+
color: #e8e8e8;
|
|
143
|
+
margin-bottom: 16px;
|
|
144
|
+
}
|
|
145
|
+
.error-friendly code {
|
|
146
|
+
font-family: 'SF Mono', Menlo, Monaco, 'Courier New', monospace;
|
|
147
|
+
font-size: 13px;
|
|
148
|
+
background: #1a1a1a;
|
|
149
|
+
border: 1px solid #444;
|
|
150
|
+
border-radius: 4px;
|
|
151
|
+
padding: 1px 5px;
|
|
152
|
+
color: #ffb86b;
|
|
153
|
+
}
|
|
125
154
|
.error-message {
|
|
126
155
|
background: #1a1a1a;
|
|
127
156
|
border: 1px solid #444;
|
|
@@ -199,25 +228,23 @@ export function generateErrorPage(error: unknown, context?: string, cspNonce?: s
|
|
|
199
228
|
<div class="error-container">
|
|
200
229
|
<div class="error-header">
|
|
201
230
|
<span class="error-icon">⚠️</span>
|
|
202
|
-
<span class="error-title"
|
|
231
|
+
<span class="error-title">${friendlyTitle}</span>
|
|
203
232
|
</div>
|
|
204
233
|
<div class="error-body">
|
|
205
234
|
${context ? `<div class="error-context">${escapeHtml(context)}</div>` : ''}
|
|
206
|
-
<
|
|
207
|
-
${errorStack ? `
|
|
235
|
+
<p class="error-friendly">${friendlyMessage}</p>
|
|
208
236
|
<div class="error-stack">
|
|
209
237
|
<button class="stack-toggle" id="stackToggle">
|
|
210
238
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
211
239
|
<polyline points="9 18 15 12 9 6"></polyline>
|
|
212
240
|
</svg>
|
|
213
|
-
<span>Show
|
|
241
|
+
<span>Show technical details</span>
|
|
214
242
|
</button>
|
|
215
|
-
<div class="stack-content" id="stackContent">${errorStack}</div>
|
|
243
|
+
<div class="stack-content" id="stackContent"><div class="error-message">${errorMessage}</div>${errorStack ? `\n${errorStack}` : ''}</div>
|
|
216
244
|
</div>
|
|
217
|
-
` : ''}
|
|
218
245
|
</div>
|
|
219
246
|
<div class="error-footer">
|
|
220
|
-
<span class="error-hint"
|
|
247
|
+
<span class="error-hint">${friendlyHint}</span>
|
|
221
248
|
<button class="copy-btn" id="copyBtn">
|
|
222
249
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
223
250
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
|
@@ -245,7 +272,7 @@ export function generateErrorPage(error: unknown, context?: string, cspNonce?: s
|
|
|
245
272
|
stackToggle.addEventListener('click', function() {
|
|
246
273
|
var isVisible = stackContent.classList.toggle('visible');
|
|
247
274
|
stackToggle.classList.toggle('expanded', isVisible);
|
|
248
|
-
stackToggle.querySelector('span').textContent = isVisible ? 'Hide
|
|
275
|
+
stackToggle.querySelector('span').textContent = isVisible ? 'Hide technical details' : 'Show technical details';
|
|
249
276
|
});
|
|
250
277
|
}
|
|
251
278
|
|
|
@@ -685,44 +685,84 @@ describe('generateSSRHTML', () => {
|
|
|
685
685
|
// which is what preserves runtime JS state (e.g. open dropdowns) across
|
|
686
686
|
// pure style edits in the editor.
|
|
687
687
|
describe('hotReload is content-aware', () => {
|
|
688
|
-
test('
|
|
688
|
+
test('updates #meno-styles in place, never replacing the node (CSP nonce safety)', async () => {
|
|
689
689
|
const result = (await generateSSRHTML({
|
|
690
690
|
pageData: minimalPage as any,
|
|
691
691
|
injectLiveReload: true,
|
|
692
692
|
})) as string;
|
|
693
|
+
// Only swap when the CSS actually changed...
|
|
693
694
|
expect(result).toContain('os.textContent!==ns.textContent');
|
|
695
|
+
// ...and do it by mutating the existing (CSP-approved) node's text.
|
|
696
|
+
expect(result).toContain('os.textContent=ns.textContent');
|
|
697
|
+
// Replacing the <style> node would insert one carrying the freshly
|
|
698
|
+
// fetched request's rotated nonce, which the iframe's locked-in CSP
|
|
699
|
+
// rejects — dropping all CSS. Guard against a regression to node swap.
|
|
700
|
+
expect(result).not.toContain('replaceChild(ns.cloneNode');
|
|
694
701
|
});
|
|
695
702
|
|
|
696
|
-
test('
|
|
703
|
+
test('stamps the original document nonce on recreated CMS context scripts', async () => {
|
|
697
704
|
const result = (await generateSSRHTML({
|
|
698
705
|
pageData: minimalPage as any,
|
|
699
706
|
injectLiveReload: true,
|
|
700
707
|
})) as string;
|
|
701
|
-
//
|
|
708
|
+
// Read the original page nonce once from the SSR-emitted meta tag...
|
|
709
|
+
expect(result).toContain(`document.querySelector('meta[name="csp-nonce"]')`);
|
|
710
|
+
// ...and stamp it on the inline <script id="meno-cms-*"> nodes the
|
|
711
|
+
// hot-reload recreates, so nonce-only script-src doesn't drop them
|
|
712
|
+
// (which would silently break CMS data hot-reload in the preview).
|
|
713
|
+
expect(result).toContain('if(docNonce)c.nonce=docNonce');
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
test('compares /_scripts/ src (cache-buster stripped) to detect a JS edit', async () => {
|
|
717
|
+
const result = (await generateSSRHTML({
|
|
718
|
+
pageData: minimalPage as any,
|
|
719
|
+
injectLiveReload: true,
|
|
720
|
+
})) as string;
|
|
721
|
+
// strip() removes ?_r=... so a script reload is keyed on a real content-hash change
|
|
702
722
|
expect(result).toContain('strip(oscr.getAttribute');
|
|
703
723
|
expect(result).toContain('strip(nscr.getAttribute');
|
|
704
|
-
|
|
724
|
+
// a changed hash is one of the two triggers for a hard reset
|
|
725
|
+
expect(result).toContain('oss!==nss');
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
test('hard-resets #root via innerHTML only on a server-structure or JS change', async () => {
|
|
729
|
+
const result = (await generateSSRHTML({
|
|
730
|
+
pageData: minimalPage as any,
|
|
731
|
+
injectLiveReload: true,
|
|
732
|
+
})) as string;
|
|
733
|
+
// Structure is judged old-server vs new-server (structKey), NOT live-vs-server,
|
|
734
|
+
// so runtime DOM mutations (slider clones, injected backdrops) don't trigger a
|
|
735
|
+
// rebuild. The wholesale innerHTML replace lives only in the hard-reset branch.
|
|
736
|
+
expect(result).toContain('structKey(lastSrvRoot)!==structKey(nr)');
|
|
737
|
+
expect(result).toContain('or.innerHTML=nr.innerHTML');
|
|
738
|
+
expect(result).toContain('hardReset');
|
|
705
739
|
});
|
|
706
740
|
|
|
707
|
-
test('
|
|
741
|
+
test('keys the diff on data-element-path + data-cms-item-index (CMS list disambiguation)', async () => {
|
|
708
742
|
const result = (await generateSSRHTML({
|
|
709
743
|
pageData: minimalPage as any,
|
|
710
744
|
injectLiveReload: true,
|
|
711
745
|
})) as string;
|
|
712
|
-
//
|
|
713
|
-
//
|
|
714
|
-
expect(result).toContain('
|
|
746
|
+
// CMS list items share a data-element-path; the item index must be
|
|
747
|
+
// folded into the diff key or one item's data leaks into siblings.
|
|
748
|
+
expect(result).toContain('data-cms-item-index');
|
|
749
|
+
// ek() composes path + item-index; the map is built and read via ek().
|
|
750
|
+
expect(result).toContain("getAttribute('data-cms-item-index')");
|
|
751
|
+
expect(result).toContain('sbp[ek(se[i])]=se[i]');
|
|
752
|
+
expect(result).toContain('p=ek(c)');
|
|
753
|
+
// Must NOT regress to keying the server map on path alone.
|
|
754
|
+
expect(result).not.toContain("sbp[se[i].getAttribute('data-element-path')]");
|
|
715
755
|
});
|
|
716
756
|
|
|
717
|
-
test('patches #root in place via
|
|
757
|
+
test('soft-patches #root in place via softSync (no innerHTML replace)', async () => {
|
|
718
758
|
const result = (await generateSSRHTML({
|
|
719
759
|
pageData: minimalPage as any,
|
|
720
760
|
injectLiveReload: true,
|
|
721
761
|
})) as string;
|
|
722
|
-
//
|
|
723
|
-
//
|
|
724
|
-
//
|
|
725
|
-
expect(result).toContain('
|
|
762
|
+
// On a pure style/content edit softSync walks the tree by data-element-path
|
|
763
|
+
// and updates attrs/classes/text in place, so event handlers, DOM identity
|
|
764
|
+
// and JS-built nodes survive.
|
|
765
|
+
expect(result).toContain('softSync(or,nr,lastSrvRoot)');
|
|
726
766
|
expect(result).toContain('querySelectorAll(\'[data-element-path]\')');
|
|
727
767
|
});
|
|
728
768
|
|
|
@@ -14,6 +14,7 @@ import { colorService } from '../services/ColorService';
|
|
|
14
14
|
import { generateThemeColorVariablesCSS, generateVariablesCSS } from '../cssGenerator';
|
|
15
15
|
import { variableService } from '../services/VariableService';
|
|
16
16
|
import { generateUtilityCSS, extractUtilityClassesFromHTML, generateAllInteractiveCSS } from '../../shared/cssGeneration';
|
|
17
|
+
import { rewriteViewportUnits } from '../../shared/viewportUnits';
|
|
17
18
|
import { printMissingStyleWarnings } from '../validateStyleCoverage';
|
|
18
19
|
import { formHandlerScript, needsFormHandler } from '../../client/scripts/formHandler';
|
|
19
20
|
import { menoFilterScript, needsMenoFilter } from '../../client/meno-filter/script.generated';
|
|
@@ -305,7 +306,7 @@ export async function generateSSRHTML(
|
|
|
305
306
|
// dedicated class so nonces aren't needed for this purely-presentational
|
|
306
307
|
// effect.
|
|
307
308
|
const menoBadgeHtml = configService.getShowMenoBadge()
|
|
308
|
-
? `<style>.meno-badge{position:fixed;bottom:12px;left:12px;z-index:9999;background:#000;color:#fff;padding:4px 10px;border-radius:6px;font-size:12px;font-family:system-ui,sans-serif;text-decoration:none;opacity:0.8;transition:opacity 0.2s}.meno-badge:hover,.meno-badge:focus{opacity:1}</style><a class="meno-badge" href="https://meno.so" target="_blank" rel="noopener">Made in Meno</a>`
|
|
309
|
+
? `<style${nonceAttr}>.meno-badge{position:fixed;bottom:12px;left:12px;z-index:9999;background:#000;color:#fff;padding:4px 10px;border-radius:6px;font-size:12px;font-family:system-ui,sans-serif;text-decoration:none;opacity:0.8;transition:opacity 0.2s}.meno-badge:hover,.meno-badge:focus{opacity:1}</style><a class="meno-badge" href="https://meno.so" target="_blank" rel="noopener">Made in Meno</a>`
|
|
309
310
|
: '';
|
|
310
311
|
const mergedCustomCode = {
|
|
311
312
|
head: [globalCustomCode.head, pageCustomCode?.head].filter(Boolean).join('\n'),
|
|
@@ -481,8 +482,14 @@ picture {
|
|
|
481
482
|
interactiveCSS
|
|
482
483
|
].filter(Boolean).join('\n');
|
|
483
484
|
|
|
485
|
+
// Rewrite vh/vw to calc(var(--design-vh, 1vh) * N) so the studio's design
|
|
486
|
+
// canvas can pin viewport units to a stable height (avoids the
|
|
487
|
+
// iframe.height ↔ 100vh feedback loop). var() fallback to 1vh means this
|
|
488
|
+
// is a no-op in production / page mode where --design-vh is unset.
|
|
489
|
+
const cssWithStableViewport = rewriteViewportUnits(combinedCSS);
|
|
490
|
+
|
|
484
491
|
// Minify CSS in production mode
|
|
485
|
-
const finalCSS = useBundled ? minifyCSS(
|
|
492
|
+
const finalCSS = useBundled ? minifyCSS(cssWithStableViewport) : cssWithStableViewport;
|
|
486
493
|
|
|
487
494
|
// Load prefetch config for client-side prefetching
|
|
488
495
|
const prefetchConfig = await loadPrefetchConfig();
|
|
@@ -540,31 +547,68 @@ picture {
|
|
|
540
547
|
: `location.origin.replace('http','ws')+'/hmr'`;
|
|
541
548
|
// True hot reload for the studio's static preview iframe.
|
|
542
549
|
//
|
|
543
|
-
//
|
|
544
|
-
//
|
|
545
|
-
//
|
|
546
|
-
//
|
|
547
|
-
//
|
|
548
|
-
//
|
|
549
|
-
//
|
|
550
|
-
//
|
|
551
|
-
//
|
|
552
|
-
//
|
|
553
|
-
//
|
|
554
|
-
//
|
|
555
|
-
//
|
|
556
|
-
//
|
|
557
|
-
//
|
|
558
|
-
//
|
|
550
|
+
// The central question on every edit is: did the EDITOR change page
|
|
551
|
+
// *structure*, or only styles/content? We answer it by comparing the
|
|
552
|
+
// previous server render (lastSrvRoot) against the freshly fetched one via
|
|
553
|
+
// structKey() — the sorted set of `data-element-path` keys. This is
|
|
554
|
+
// deliberately independent of the LIVE DOM, which user JS routinely mutates
|
|
555
|
+
// at runtime (a slider clones slides, a nav injects a backdrop); those
|
|
556
|
+
// runtime mutations must NOT be mistaken for editor structural changes.
|
|
557
|
+
//
|
|
558
|
+
// * SOFT update (server structure unchanged AND /_scripts hash unchanged):
|
|
559
|
+
// a pure style/content/attribute edit. softSync() patches matching
|
|
560
|
+
// elements in place (attributes, classes, text) WITHOUT replacing #root
|
|
561
|
+
// and WITHOUT re-running component JS. DOM identity, event listeners,
|
|
562
|
+
// JS-built children (slider clones) and runtime state (open menus,
|
|
563
|
+
// carousel position, counters) all survive. `#meno-styles` is updated in
|
|
564
|
+
// place so the new CSS applies live. This is what lets you tweak styles
|
|
565
|
+
// with the navigation open without it snapping shut.
|
|
566
|
+
// * HARD reset (server structure changed — nodes added/removed/reparented —
|
|
567
|
+
// OR the /_scripts/{hash}.js URL changed because user JS was edited; the
|
|
568
|
+
// URL is content-addressed, see scriptCache.hashContent): #root is
|
|
569
|
+
// wholesale `innerHTML`-replaced from the server markup and the component
|
|
570
|
+
// script is re-run (old `/_scripts` tag removed, cache-busted copy
|
|
571
|
+
// appended, DOMContentLoaded re-dispatched). Interactive state resets,
|
|
572
|
+
// which is expected when structure or behavior actually changed. Because
|
|
573
|
+
// the whole subtree is replaced first there are no surviving listeners to
|
|
574
|
+
// double-bind, so the re-run is clean.
|
|
575
|
+
//
|
|
576
|
+
// softSync() preserves runtime mutations the same way for classes AND
|
|
577
|
+
// attributes: a value that differs from the previous server snapshot but
|
|
578
|
+
// that the new server render left unchanged is treated as a runtime addition
|
|
579
|
+
// (e.g. `nav-dropdown-open`, `aria-expanded="true"`, a JS-set inline `style`)
|
|
580
|
+
// and kept; a value the server itself changed is an editor edit and wins.
|
|
581
|
+
// Element identity relies on `data-element-path`, emitted on every element
|
|
582
|
+
// when the request comes through the studio's `/__static__/` proxy (which
|
|
583
|
+
// sets `x-meno-editor: 1`).
|
|
584
|
+
//
|
|
585
|
+
// CMS list items all share the SAME `data-element-path` — processList()
|
|
586
|
+
// advances the per-item index in `cmsItemIndexPath`, not in `elementPath` —
|
|
587
|
+
// so keying the diff on path alone collapses every card onto the LAST item
|
|
588
|
+
// sharing that path, and editing one item leaks its data into its siblings'
|
|
589
|
+
// cards. ek() therefore composes the path with `data-cms-item-index` (the
|
|
590
|
+
// disambiguator the SSR already emits per item) so each card patches from
|
|
591
|
+
// its own server counterpart, regardless of any client-side (MenoFilter)
|
|
592
|
+
// reordering of the live list DOM. Non-list elements have no item-index, so
|
|
593
|
+
// their key — and behavior — is unchanged.
|
|
594
|
+
//
|
|
595
|
+
// The `#meno-styles` stylesheet is updated IN PLACE (`os.textContent=...`),
|
|
596
|
+
// never by replacing the node. Under the editor preview's hardened CSP,
|
|
597
|
+
// `style-src` is nonce-only and the nonce rotates every request (see
|
|
598
|
+
// pages.ts / cspNonce.ts). Swapping in a `<style>` parsed from the freshly
|
|
599
|
+
// fetched HTML would carry that request's new nonce — which no longer
|
|
600
|
+
// matches the iframe document's locked-in CSP — so Chromium would drop the
|
|
601
|
+
// node and ALL CSS would vanish. Mutating the already-approved node's text
|
|
602
|
+
// doesn't re-trigger nonce validation (same trick StyleInjector uses in dev).
|
|
559
603
|
//
|
|
560
|
-
//
|
|
561
|
-
//
|
|
562
|
-
//
|
|
563
|
-
//
|
|
564
|
-
//
|
|
565
|
-
//
|
|
604
|
+
// For the same reason, the `<script id="meno-cms-*">` context scripts (which
|
|
605
|
+
// ARE recreated, since an inline script must be a fresh node to re-execute)
|
|
606
|
+
// are stamped with `docNonce` — the ORIGINAL page nonce read once from
|
|
607
|
+
// `<meta name="csp-nonce">`, not the rotated nonce in the freshly fetched
|
|
608
|
+
// HTML. Without it, nonce-only `script-src` would drop the recreated script
|
|
609
|
+
// and CMS hot-reload (updated `window.__MENO_CMS__` data) would silently fail.
|
|
566
610
|
const liveReloadScript = injectLiveReload
|
|
567
|
-
? `<script${nonceAttr}>(function(){var ws,timer,gen=0,lastSrvRoot=null;function strip(s){return s?s.replace(/[?&]_r=\\d+/,''):''}function classList(el){return (el.getAttribute('class')||'').split(/\\s+/).filter(Boolean)}function syncEl(cur,srv,old){var cc=classList(cur),sc=classList(srv),oc=old?new Set(classList(old)):new Set();var rt=cc.filter(function(c){return !oc.has(c)});var seen=new Set(),fin=[];sc.concat(rt).forEach(function(c){if(!seen.has(c)){seen.add(c);fin.push(c)}});var fs=fin.join(' ');if((cur.getAttribute('class')||'')!==fs){if(fs)cur.setAttribute('class',fs);else cur.removeAttribute('class')}for(var i=0;i<srv.attributes.length;i++){var a=srv.attributes[i];if(a.name==='class')continue;if(cur.getAttribute(a.name)!==a.value)cur.setAttribute(a.name,a.value)}if(old){for(var i=0;i<old.attributes.length;i++){var a=old.attributes[i];if(a.name==='class')continue;if(!srv.hasAttribute(a.name)&&cur.hasAttribute(a.name))cur.removeAttribute(a.name)}}}function syncText(cur,srv){var cc=cur.childNodes,sc=srv.childNodes;for(var i=0;i<sc.length;i++){var s=sc[i],c=cc[i];if(s.nodeType===3&&c&&c.nodeType===3){if(c.textContent!==s.textContent)c.textContent=s.textContent}}}function
|
|
611
|
+
? `<script${nonceAttr}>(function(){var ws,timer,gen=0,lastSrvRoot=null,dn=document.querySelector('meta[name="csp-nonce"]'),docNonce=dn?dn.getAttribute('content'):'';function strip(s){return s?s.replace(/[?&]_r=\\d+/,''):''}function classList(el){return (el.getAttribute('class')||'').split(/\\s+/).filter(Boolean)}function syncEl(cur,srv,old){var cc=classList(cur),sc=classList(srv),oc=old?new Set(classList(old)):new Set();var rt=cc.filter(function(c){return !oc.has(c)});var seen=new Set(),fin=[];sc.concat(rt).forEach(function(c){if(!seen.has(c)){seen.add(c);fin.push(c)}});var fs=fin.join(' ');if((cur.getAttribute('class')||'')!==fs){if(fs)cur.setAttribute('class',fs);else cur.removeAttribute('class')}for(var i=0;i<srv.attributes.length;i++){var a=srv.attributes[i];if(a.name==='class')continue;var ov=old?old.getAttribute(a.name):null;if(a.value!==ov){if(cur.getAttribute(a.name)!==a.value)cur.setAttribute(a.name,a.value)}}if(old){for(var i=0;i<old.attributes.length;i++){var a=old.attributes[i];if(a.name==='class')continue;if(!srv.hasAttribute(a.name)&&cur.hasAttribute(a.name))cur.removeAttribute(a.name)}}}function syncText(cur,srv){var cc=cur.childNodes,sc=srv.childNodes;for(var i=0;i<sc.length;i++){var s=sc[i],c=cc[i];if(s.nodeType===3&&c&&c.nodeType===3){if(c.textContent!==s.textContent)c.textContent=s.textContent}}}function ek(el){var p=el.getAttribute('data-element-path'),ci=el.getAttribute('data-cms-item-index');return ci?p+'|'+ci:p}function structKey(root){var e=root.querySelectorAll('[data-element-path]'),k=[];for(var i=0;i<e.length;i++)k.push(ek(e[i]));return k.sort().join('~')}function softSync(curR,srvR,oldR){var ce=curR.querySelectorAll('[data-element-path]');var sbp={},se=srvR.querySelectorAll('[data-element-path]');for(var i=0;i<se.length;i++)sbp[ek(se[i])]=se[i];var obp={};if(oldR){var oe=oldR.querySelectorAll('[data-element-path]');for(var i=0;i<oe.length;i++)obp[ek(oe[i])]=oe[i]}for(var i=0;i<ce.length;i++){var c=ce[i],p=ek(c),s=sbp[p];if(!s)continue;syncEl(c,s,obp[p]);syncText(c,s)}syncText(curR,srvR)}function connect(){ws=new WebSocket(${wsUrl});ws.onmessage=function(e){var d=JSON.parse(e.data);if(d.type==='hmr:libraries-update'){location.reload()}else if(d.type==='hmr:update'||d.type==='hmr:cms-update'||d.type==='hmr:colors-update'||d.type==='hmr:variables-update')hotReload()};ws.onclose=function(){clearTimeout(timer);timer=setTimeout(connect,1000)}}function hotReload(){var g=++gen;var sx=window.scrollX,sy=window.scrollY;fetch(location.href,{cache:'no-store'}).then(function(r){return r.text()}).then(function(html){if(g!==gen)return;var p=new DOMParser();var d=p.parseFromString(html,'text/html');var or=document.getElementById('root'),nr=d.getElementById('root');var oscr=document.querySelector('script[src^="/_scripts/"]'),nscr=d.querySelector('script[src^="/_scripts/"]');var oss=oscr?strip(oscr.getAttribute('src')):'',nss=nscr?strip(nscr.getAttribute('src')):'';var hardReset=(oss!==nss)||!lastSrvRoot||!or||!nr||structKey(lastSrvRoot)!==structKey(nr);if(or&&nr){if(hardReset){if(or.innerHTML!==nr.innerHTML)or.innerHTML=nr.innerHTML}else{softSync(or,nr,lastSrvRoot)}}if(nr)lastSrvRoot=nr.cloneNode(true);var os=document.getElementById('meno-styles'),ns=d.getElementById('meno-styles');if(os&&ns&&os.textContent!==ns.textContent)os.textContent=ns.textContent;var nh=d.documentElement;if(nh){var nl=nh.getAttribute('lang')||'en',nt=nh.getAttribute('theme')||'light';if(document.documentElement.getAttribute('lang')!==nl)document.documentElement.setAttribute('lang',nl);if(document.documentElement.getAttribute('theme')!==nt)document.documentElement.setAttribute('theme',nt)}var ocms=document.querySelectorAll('script[id^="meno-cms-"]'),ncms=d.querySelectorAll('script[id^="meno-cms-"]');var ock=JSON.stringify(Array.prototype.map.call(ocms,function(s){return [s.id,s.textContent]}));var nck=JSON.stringify(Array.prototype.map.call(ncms,function(s){return [s.id,s.textContent]}));if(ock!==nck){ocms.forEach(function(s){s.remove()});ncms.forEach(function(s){var c=document.createElement('script');c.type=s.type;c.id=s.id;if(docNonce)c.nonce=docNonce;c.textContent=s.textContent;document.head.appendChild(c)})}window.__menoHotReload=true;var olib=document.querySelectorAll('body > script[src^="/libraries/"]'),nlib=d.querySelectorAll('body > script[src^="/libraries/"]');var olk=JSON.stringify(Array.prototype.map.call(olib,function(s){return strip(s.getAttribute('src'))}).sort());var nlk=JSON.stringify(Array.prototype.map.call(nlib,function(s){return strip(s.getAttribute('src'))}).sort());if(olk!==nlk){olib.forEach(function(o){o.remove()});nlib.forEach(function(n){var src=n.getAttribute('src');var ls=document.createElement('script');ls.src=src+(src.indexOf('?')>-1?'&':'?')+'_r='+Date.now();document.body.appendChild(ls)})}if(!hardReset){window.scrollTo(sx,sy)}else{if(oscr)oscr.remove();if(nscr){var src=nscr.getAttribute('src');var s=document.createElement('script');s.src=src+(src.indexOf('?')>-1?'&':'?')+'_r='+Date.now();s.onload=function(){document.dispatchEvent(new Event('DOMContentLoaded'));window.scrollTo(sx,sy)};s.onerror=function(){window.scrollTo(sx,sy)};document.body.appendChild(s)}else{document.dispatchEvent(new Event('DOMContentLoaded'));window.scrollTo(sx,sy)}}}).catch(function(){location.reload()})}var iR=document.getElementById('root');if(iR)lastSrvRoot=iR.cloneNode(true);connect()})()</script>`
|
|
568
612
|
: '';
|
|
569
613
|
|
|
570
614
|
// Scroll position handlers for preview mode iframe switching
|
|
@@ -582,8 +626,8 @@ picture {
|
|
|
582
626
|
<head>
|
|
583
627
|
<meta charset="UTF-8">
|
|
584
628
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
585
|
-
${iconTags ? iconTags + '\n ' : ''}${scriptPreloadTag ? scriptPreloadTag + '\n ' : ''}${imagePreloadTags ? imagePreloadTags + '\n ' : ''}${fontPreloadTags ? fontPreloadTags + '\n ' : ''}${libraryTags.headCSS ? libraryTags.headCSS + '\n ' : ''}${libraryTags.headJS ? libraryTags.headJS + '\n ' : ''}${rendered.meta}
|
|
586
|
-
${configInlineScript}${cmsInlineScript}${clientDataScripts}<style id="meno-styles">${styleContent}</style>${mergedCustomCode.head ? '\n ' + mergedCustomCode.head : ''}
|
|
629
|
+
${cspNonce ? `<meta name="csp-nonce" content="${cspNonce}">\n ` : ''}${iconTags ? iconTags + '\n ' : ''}${scriptPreloadTag ? scriptPreloadTag + '\n ' : ''}${imagePreloadTags ? imagePreloadTags + '\n ' : ''}${fontPreloadTags ? fontPreloadTags + '\n ' : ''}${libraryTags.headCSS ? libraryTags.headCSS + '\n ' : ''}${libraryTags.headJS ? libraryTags.headJS + '\n ' : ''}${rendered.meta}
|
|
630
|
+
${configInlineScript}${cmsInlineScript}${clientDataScripts}<style id="meno-styles"${nonceAttr}>${styleContent}</style>${mergedCustomCode.head ? '\n ' + mergedCustomCode.head : ''}
|
|
587
631
|
</head>
|
|
588
632
|
<body>${mergedCustomCode.bodyStart ? '\n ' + mergedCustomCode.bodyStart : ''}
|
|
589
633
|
<div id="root">
|