meno-core 1.0.49 → 1.0.51

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 (66) hide show
  1. package/build-astro.ts +6 -2
  2. package/build-static.ts +8 -1
  3. package/dist/bin/cli.js +1 -1
  4. package/dist/build-static.js +5 -5
  5. package/dist/chunks/{chunk-KPU2XHOS.js → chunk-2MHDV5BF.js} +11 -1
  6. package/dist/chunks/chunk-2MHDV5BF.js.map +7 -0
  7. package/dist/chunks/{chunk-JER5NQVM.js → chunk-3KJ6SJZC.js} +5 -5
  8. package/dist/chunks/{chunk-JER5NQVM.js.map → chunk-3KJ6SJZC.js.map} +2 -2
  9. package/dist/chunks/{chunk-S2CX6HFM.js → chunk-7NIC4I3V.js} +42 -20
  10. package/dist/chunks/chunk-7NIC4I3V.js.map +7 -0
  11. package/dist/chunks/{chunk-EQYDSPBB.js → chunk-DM54NPEC.js} +114 -31
  12. package/dist/chunks/chunk-DM54NPEC.js.map +7 -0
  13. package/dist/chunks/{chunk-LKAGAQ3M.js → chunk-EDQSMAMP.js} +13 -2
  14. package/dist/chunks/{chunk-LKAGAQ3M.js.map → chunk-EDQSMAMP.js.map} +2 -2
  15. package/dist/chunks/{chunk-4OFZP5NQ.js → chunk-HNLUO36W.js} +15 -4
  16. package/dist/chunks/chunk-HNLUO36W.js.map +7 -0
  17. package/dist/chunks/{chunk-6IVUG7FY.js → chunk-LPVETICS.js} +19 -2
  18. package/dist/chunks/{chunk-6IVUG7FY.js.map → chunk-LPVETICS.js.map} +2 -2
  19. package/dist/chunks/{chunk-CHD5UCFF.js → chunk-V7CD7V7W.js} +149 -46
  20. package/dist/chunks/chunk-V7CD7V7W.js.map +7 -0
  21. package/dist/chunks/{configService-CCA6AIDI.js → configService-R3OGU2UD.js} +2 -2
  22. package/dist/entries/server-router.js +5 -5
  23. package/dist/lib/client/index.js +41 -15
  24. package/dist/lib/client/index.js.map +3 -3
  25. package/dist/lib/server/index.js +12 -10
  26. package/dist/lib/server/index.js.map +2 -2
  27. package/dist/lib/shared/index.js +2 -2
  28. package/lib/client/core/ComponentBuilder.test.ts +34 -0
  29. package/lib/client/core/ComponentBuilder.ts +25 -3
  30. package/lib/client/core/builders/embedBuilder.ts +13 -5
  31. package/lib/client/core/builders/linkNodeBuilder.ts +13 -5
  32. package/lib/client/core/builders/localeListBuilder.ts +13 -5
  33. package/lib/client/templateEngine.ts +24 -0
  34. package/lib/server/fileWatcher.test.ts +134 -0
  35. package/lib/server/fileWatcher.ts +100 -32
  36. package/lib/server/jsonLoader.ts +1 -0
  37. package/lib/server/providers/fileSystemCMSProvider.ts +46 -14
  38. package/lib/server/routes/pages.ts +37 -2
  39. package/lib/server/services/cmsService.ts +21 -0
  40. package/lib/server/services/configService.ts +21 -0
  41. package/lib/server/services/fileWatcherService.ts +17 -0
  42. package/lib/server/ssr/buildErrorOverlay.ts +22 -4
  43. package/lib/server/ssr/errorOverlay.ts +11 -3
  44. package/lib/server/ssr/htmlGenerator.nonce.test.ts +165 -0
  45. package/lib/server/ssr/htmlGenerator.ts +36 -9
  46. package/lib/server/ssr/liveReloadIntegration.test.ts +3 -1
  47. package/lib/server/ssr/metaTagGenerator.ts +35 -5
  48. package/lib/server/ssr/ssrRenderer.test.ts +258 -0
  49. package/lib/server/ssr/ssrRenderer.ts +47 -5
  50. package/lib/server/ssrRenderer.test.ts +87 -2
  51. package/lib/server/webflow/buildWebflow.ts +1 -1
  52. package/lib/server/websocketManager.test.ts +61 -6
  53. package/lib/server/websocketManager.ts +25 -1
  54. package/lib/shared/cssProperties.test.ts +28 -0
  55. package/lib/shared/cssProperties.ts +27 -1
  56. package/lib/shared/types/api.ts +10 -1
  57. package/lib/shared/types/cms.ts +18 -9
  58. package/lib/shared/validation/schemas.test.ts +93 -0
  59. package/lib/shared/validation/schemas.ts +56 -15
  60. package/package.json +1 -1
  61. package/dist/chunks/chunk-4OFZP5NQ.js.map +0 -7
  62. package/dist/chunks/chunk-CHD5UCFF.js.map +0 -7
  63. package/dist/chunks/chunk-EQYDSPBB.js.map +0 -7
  64. package/dist/chunks/chunk-KPU2XHOS.js.map +0 -7
  65. package/dist/chunks/chunk-S2CX6HFM.js.map +0 -7
  66. /package/dist/chunks/{configService-CCA6AIDI.js.map → configService-R3OGU2UD.js.map} +0 -0
@@ -15,10 +15,10 @@ import {
15
15
  parseJSON,
16
16
  resolveSlugToPageId,
17
17
  variableService
18
- } from "./chunk-EQYDSPBB.js";
18
+ } from "./chunk-DM54NPEC.js";
19
19
  import {
20
20
  configService
21
- } from "./chunk-KPU2XHOS.js";
21
+ } from "./chunk-2MHDV5BF.js";
22
22
  import {
23
23
  bundleFile,
24
24
  createRuntimeServer,
@@ -44,7 +44,7 @@ import {
44
44
  } from "./chunk-J23ZX5AP.js";
45
45
  import {
46
46
  addItemUrls
47
- } from "./chunk-S2CX6HFM.js";
47
+ } from "./chunk-7NIC4I3V.js";
48
48
  import {
49
49
  parseLocaleFromPath
50
50
  } from "./chunk-AZQYF6KE.js";
@@ -217,6 +217,27 @@ var WebSocketManager = class {
217
217
  collection
218
218
  });
219
219
  }
220
+ /**
221
+ * Broadcast CMS collections-list update notification.
222
+ * Emitted when a template file is added, removed, or its schema changes —
223
+ * tells connected clients to re-fetch the collections list.
224
+ */
225
+ broadcastCollectionsUpdate() {
226
+ this.broadcast({
227
+ type: "hmr:cms-collections-update"
228
+ });
229
+ }
230
+ /**
231
+ * Broadcast project.config.json update notification.
232
+ * Emitted when project.config.json changes (e.g. an AI tool adds a new
233
+ * locale) — tells connected clients to re-fetch config-derived state
234
+ * such as the i18n locale list.
235
+ */
236
+ broadcastConfigUpdate() {
237
+ this.broadcast({
238
+ type: "hmr:config-update"
239
+ });
240
+ }
220
241
  /**
221
242
  * Get number of connected clients
222
243
  */
@@ -1313,6 +1334,27 @@ import { existsSync as existsSync4 } from "fs";
1313
1334
 
1314
1335
  // lib/server/fileWatcher.ts
1315
1336
  import { watch, existsSync as existsSync3 } from "fs";
1337
+ import { basename, dirname } from "path";
1338
+ function attachWhenDirExists(dirPath, attach, setWatcher) {
1339
+ if (existsSync3(dirPath)) {
1340
+ setWatcher(attach());
1341
+ return;
1342
+ }
1343
+ const parentDir = dirname(dirPath);
1344
+ const targetName = basename(dirPath);
1345
+ if (!existsSync3(parentDir)) {
1346
+ return;
1347
+ }
1348
+ let parentWatcher = null;
1349
+ parentWatcher = watch(parentDir, (_event, filename) => {
1350
+ if (filename !== targetName) return;
1351
+ if (!existsSync3(dirPath)) return;
1352
+ parentWatcher?.close();
1353
+ parentWatcher = null;
1354
+ setWatcher(attach());
1355
+ });
1356
+ setWatcher(parentWatcher);
1357
+ }
1316
1358
  var FileWatcher = class {
1317
1359
  constructor(callbacks) {
1318
1360
  this.callbacks = callbacks;
@@ -1326,6 +1368,7 @@ var FileWatcher = class {
1326
1368
  cmsWatcher = null;
1327
1369
  imagesWatcher = null;
1328
1370
  librariesWatcher = null;
1371
+ projectConfigWatcher = null;
1329
1372
  /**
1330
1373
  * Start watching components directory
1331
1374
  * Watches both .json and .js files to detect component definition and JavaScript changes
@@ -1369,23 +1412,29 @@ var FileWatcher = class {
1369
1412
  );
1370
1413
  }
1371
1414
  /**
1372
- * Start watching root templates directory for CMS template changes
1415
+ * Start watching root templates directory for CMS template changes.
1416
+ * Falls back to a deferred-attach watcher on the project root when
1417
+ * `templates/` doesn't exist yet (blank projects, projects that have never
1418
+ * had a CMS collection).
1373
1419
  */
1374
1420
  watchTemplates(dirPath = projectPaths.templates()) {
1375
- if (!existsSync3(dirPath)) {
1376
- return;
1377
- }
1378
- this.templatesWatcher = watch(
1421
+ attachWhenDirExists(
1379
1422
  dirPath,
1380
- { recursive: true },
1381
- async (event, filename) => {
1382
- if (filename && filename.endsWith(".json")) {
1383
- const pageName = `templates/${filename.replace(".json", "")}`;
1384
- const pagePath = mapPageNameToPath(pageName);
1385
- if (this.callbacks.onPageChange) {
1386
- await this.callbacks.onPageChange(pagePath);
1423
+ () => watch(
1424
+ dirPath,
1425
+ { recursive: true },
1426
+ async (event, filename) => {
1427
+ if (filename && filename.endsWith(".json")) {
1428
+ const pageName = `templates/${filename.replace(".json", "")}`;
1429
+ const pagePath = mapPageNameToPath(pageName);
1430
+ if (this.callbacks.onPageChange) {
1431
+ await this.callbacks.onPageChange(pagePath);
1432
+ }
1387
1433
  }
1388
1434
  }
1435
+ ),
1436
+ (w) => {
1437
+ this.templatesWatcher = w;
1389
1438
  }
1390
1439
  );
1391
1440
  }
@@ -1438,23 +1487,27 @@ var FileWatcher = class {
1438
1487
  );
1439
1488
  }
1440
1489
  /**
1441
- * Start watching CMS directory
1442
- * Watches for changes in CMS content files (cms/{collection}/*.json)
1490
+ * Start watching CMS directory.
1491
+ * Watches for changes in CMS content files (cms/{collection}/*.json).
1492
+ * Falls back to a deferred-attach watcher when `cms/` doesn't exist yet.
1443
1493
  */
1444
1494
  watchCMS(dirPath = projectPaths.cms()) {
1445
- if (!existsSync3(dirPath)) {
1446
- return;
1447
- }
1448
- this.cmsWatcher = watch(
1495
+ attachWhenDirExists(
1449
1496
  dirPath,
1450
- { recursive: true },
1451
- async (event, filename) => {
1452
- if (filename && filename.endsWith(".json")) {
1453
- const collection = filename.split("/")[0];
1454
- if (this.callbacks.onCMSChange) {
1455
- await this.callbacks.onCMSChange(collection);
1497
+ () => watch(
1498
+ dirPath,
1499
+ { recursive: true },
1500
+ async (event, filename) => {
1501
+ if (filename && filename.endsWith(".json")) {
1502
+ const collection = filename.split("/")[0];
1503
+ if (this.callbacks.onCMSChange) {
1504
+ await this.callbacks.onCMSChange(collection);
1505
+ }
1456
1506
  }
1457
1507
  }
1508
+ ),
1509
+ (w) => {
1510
+ this.cmsWatcher = w;
1458
1511
  }
1459
1512
  );
1460
1513
  }
@@ -1475,6 +1528,24 @@ var FileWatcher = class {
1475
1528
  }
1476
1529
  );
1477
1530
  }
1531
+ /**
1532
+ * Start watching project.config.json for changes.
1533
+ * Picks up edits to i18n locales, breakpoints, libraries, icons, etc. so the
1534
+ * studio reflects external writes (e.g. an AI tool adding a new locale) without
1535
+ * a dev-server restart.
1536
+ */
1537
+ watchProjectConfig() {
1538
+ const dirPath = getProjectRoot();
1539
+ this.projectConfigWatcher = watch(
1540
+ dirPath,
1541
+ { recursive: false },
1542
+ async (_event, filename) => {
1543
+ if (filename === "project.config.json" && this.callbacks.onProjectConfigChange) {
1544
+ await this.callbacks.onProjectConfigChange();
1545
+ }
1546
+ }
1547
+ );
1548
+ }
1478
1549
  /**
1479
1550
  * Start watching libraries directory for CSS/JS changes
1480
1551
  */
@@ -1507,6 +1578,7 @@ var FileWatcher = class {
1507
1578
  this.watchCMS();
1508
1579
  this.watchImages();
1509
1580
  this.watchLibraries();
1581
+ this.watchProjectConfig();
1510
1582
  }
1511
1583
  /**
1512
1584
  * Stop watching all directories
@@ -1548,6 +1620,10 @@ var FileWatcher = class {
1548
1620
  this.librariesWatcher.close();
1549
1621
  this.librariesWatcher = null;
1550
1622
  }
1623
+ if (this.projectConfigWatcher) {
1624
+ this.projectConfigWatcher.close();
1625
+ this.projectConfigWatcher = null;
1626
+ }
1551
1627
  }
1552
1628
  /**
1553
1629
  * Check if watchers are active
@@ -1598,9 +1674,11 @@ var FileWatcherService = class {
1598
1674
  clearTimeout(this.refreshSchemasTimer);
1599
1675
  }
1600
1676
  const cmsService = this.cmsService;
1677
+ const wsManager = this.wsManager;
1601
1678
  this.refreshSchemasTimer = setTimeout(async () => {
1602
1679
  this.refreshSchemasTimer = null;
1603
1680
  await cmsService.refreshSchemas();
1681
+ wsManager.broadcastCollectionsUpdate();
1604
1682
  }, 50);
1605
1683
  }
1606
1684
  },
@@ -1617,11 +1695,17 @@ var FileWatcherService = class {
1617
1695
  this.wsManager.broadcastEnumsUpdate();
1618
1696
  },
1619
1697
  onCMSChange: async (collection) => {
1698
+ this.cmsService?.clearItemsCache(collection);
1620
1699
  this.wsManager.broadcastCMSUpdate(collection);
1621
1700
  },
1622
1701
  onImageAdded: this.onImageAdded,
1623
1702
  onLibraryChange: async () => {
1624
1703
  this.wsManager.broadcastLibrariesUpdate();
1704
+ },
1705
+ onProjectConfigChange: async () => {
1706
+ configService.reset();
1707
+ await configService.load();
1708
+ this.wsManager.broadcastConfigUpdate();
1625
1709
  }
1626
1710
  });
1627
1711
  this.fileWatcher.watchAll();
@@ -2385,6 +2469,9 @@ async function handleApiRoutes(req, url, pageService, componentService, cmsConte
2385
2469
  }
2386
2470
  }
2387
2471
 
2472
+ // lib/server/routes/pages.ts
2473
+ import { randomBytes } from "crypto";
2474
+
2388
2475
  // lib/shared/pathUtils.ts
2389
2476
  function getStaticFilePath(pagePath, distDir = "./dist") {
2390
2477
  if (pagePath === "/") {
@@ -2414,10 +2501,11 @@ function escapeHtml(str) {
2414
2501
  function safeJsonForScript(data) {
2415
2502
  return JSON.stringify(data).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
2416
2503
  }
2417
- function generateErrorPage(error, context) {
2504
+ function generateErrorPage(error, context, cspNonce) {
2418
2505
  const errorInfo = extractErrorInfo(error);
2419
2506
  const errorMessage = escapeHtml(errorInfo.message);
2420
2507
  const errorStack = errorInfo.stack ? escapeHtml(errorInfo.stack) : "";
2508
+ const nonceAttr = cspNonce ? ` nonce="${cspNonce}"` : "";
2421
2509
  const errorDataJson = safeJsonForScript({
2422
2510
  type: "PREVIEW_ERROR",
2423
2511
  error: {
@@ -2577,7 +2665,7 @@ function generateErrorPage(error, context) {
2577
2665
  </button>
2578
2666
  </div>
2579
2667
  </div>
2580
- <script>
2668
+ <script${nonceAttr}>
2581
2669
  (function() {
2582
2670
  // Send error to parent editor
2583
2671
  if (window.parent && window.parent !== window) {
@@ -2634,10 +2722,15 @@ function hashContent2(content) {
2634
2722
 
2635
2723
  // lib/server/routes/pages.ts
2636
2724
  var EDITOR_HEADER = "x-meno-editor";
2725
+ var CSP_NONCE_HEADER = "x-meno-csp-nonce";
2726
+ function generateCspNonce() {
2727
+ return randomBytes(16).toString("base64");
2728
+ }
2637
2729
  async function handlePageRoute(url, context, req) {
2638
2730
  const { pageService, componentService, cmsService, injectLiveReload, isEditor, serverPort } = context;
2639
2731
  const pagePath = url.pathname;
2640
2732
  const injectEditorAttrs = req?.headers.get(EDITOR_HEADER) === "1";
2733
+ const cspNonce = generateCspNonce();
2641
2734
  const i18nConfig = await loadI18nConfig();
2642
2735
  const { locale, pathWithoutLocale } = parseLocaleFromPath(pagePath, i18nConfig);
2643
2736
  const slugMappings = pageService.getSlugMappings();
@@ -2678,7 +2771,8 @@ async function handlePageRoute(url, context, req) {
2678
2771
  injectEditorAttrs,
2679
2772
  isEditor,
2680
2773
  serverPort,
2681
- returnSeparateJS: true
2774
+ returnSeparateJS: true,
2775
+ cspNonce
2682
2776
  });
2683
2777
  let finalHtml = result.html;
2684
2778
  if (result.javascript) {
@@ -2692,7 +2786,8 @@ async function handlePageRoute(url, context, req) {
2692
2786
  "Content-Type": "text/html; charset=utf-8",
2693
2787
  "Cache-Control": "no-store, max-age=0",
2694
2788
  "Pragma": "no-cache",
2695
- "Expires": "0"
2789
+ "Expires": "0",
2790
+ [CSP_NONCE_HEADER]: cspNonce
2696
2791
  }
2697
2792
  });
2698
2793
  }
@@ -2710,22 +2805,25 @@ async function handlePageRoute(url, context, req) {
2710
2805
  pageLibraries: typedPageData.meta?.libraries,
2711
2806
  pageCustomCode: typedPageData.meta?.customCode,
2712
2807
  injectEditorAttrs,
2713
- isEditor
2808
+ isEditor,
2809
+ cspNonce
2714
2810
  });
2715
2811
  return new Response(ssrHTML, {
2716
2812
  headers: {
2717
2813
  "Content-Type": "text/html; charset=utf-8",
2718
2814
  "Cache-Control": "no-store, max-age=0",
2719
2815
  "Pragma": "no-cache",
2720
- "Expires": "0"
2816
+ "Expires": "0",
2817
+ [CSP_NONCE_HEADER]: cspNonce
2721
2818
  }
2722
2819
  });
2723
2820
  } catch (error) {
2724
2821
  console.error("Error rendering CMS page:", error);
2725
- return new Response(generateErrorPage(error, `Error rendering template: ${cmsTemplatePath}`), {
2822
+ return new Response(generateErrorPage(error, `Error rendering template: ${cmsTemplatePath}`, cspNonce), {
2726
2823
  headers: {
2727
2824
  "Content-Type": "text/html; charset=utf-8",
2728
- "Cache-Control": "no-store"
2825
+ "Cache-Control": "no-store",
2826
+ [CSP_NONCE_HEADER]: cspNonce
2729
2827
  }
2730
2828
  });
2731
2829
  }
@@ -2773,7 +2871,8 @@ async function handlePageRoute(url, context, req) {
2773
2871
  injectEditorAttrs,
2774
2872
  isEditor,
2775
2873
  serverPort,
2776
- returnSeparateJS: true
2874
+ returnSeparateJS: true,
2875
+ cspNonce
2777
2876
  });
2778
2877
  let finalHtml = result.html;
2779
2878
  if (result.javascript) {
@@ -2787,7 +2886,8 @@ async function handlePageRoute(url, context, req) {
2787
2886
  "Content-Type": "text/html; charset=utf-8",
2788
2887
  "Cache-Control": "no-store, max-age=0",
2789
2888
  "Pragma": "no-cache",
2790
- "Expires": "0"
2889
+ "Expires": "0",
2890
+ [CSP_NONCE_HEADER]: cspNonce
2791
2891
  }
2792
2892
  });
2793
2893
  }
@@ -2803,22 +2903,25 @@ async function handlePageRoute(url, context, req) {
2803
2903
  pageLibraries: pageData.meta?.libraries,
2804
2904
  pageCustomCode: pageData.meta?.customCode,
2805
2905
  injectEditorAttrs,
2806
- isEditor
2906
+ isEditor,
2907
+ cspNonce
2807
2908
  });
2808
2909
  return new Response(ssrHTML, {
2809
2910
  headers: {
2810
2911
  "Content-Type": "text/html; charset=utf-8",
2811
2912
  "Cache-Control": "no-store, max-age=0",
2812
2913
  "Pragma": "no-cache",
2813
- "Expires": "0"
2914
+ "Expires": "0",
2915
+ [CSP_NONCE_HEADER]: cspNonce
2814
2916
  }
2815
2917
  });
2816
2918
  } catch (error) {
2817
2919
  console.error("Error rendering page:", error);
2818
- return new Response(generateErrorPage(error, `Error rendering page: ${lookupPath}`), {
2920
+ return new Response(generateErrorPage(error, `Error rendering page: ${lookupPath}`, cspNonce), {
2819
2921
  headers: {
2820
2922
  "Content-Type": "text/html; charset=utf-8",
2821
- "Cache-Control": "no-store"
2923
+ "Cache-Control": "no-store",
2924
+ [CSP_NONCE_HEADER]: cspNonce
2822
2925
  }
2823
2926
  });
2824
2927
  }
@@ -3108,7 +3211,7 @@ async function createServer(config) {
3108
3211
 
3109
3212
  // lib/server/providers/fileSystemPageProvider.ts
3110
3213
  import { existsSync as existsSync5, readdirSync as readdirSync3, mkdirSync as mkdirSync3, rmdirSync as rmdirSync3 } from "fs";
3111
- import { join as join4, dirname } from "path";
3214
+ import { join as join4, dirname as dirname2 } from "path";
3112
3215
 
3113
3216
  // lib/shared/utils/fileUtils.ts
3114
3217
  var isJSONFile = (name) => name.endsWith(".json");
@@ -3203,7 +3306,7 @@ var FileSystemPageProvider = class {
3203
3306
  async save(path2, content) {
3204
3307
  const { writeFile: writeFile2 } = await import("fs/promises");
3205
3308
  const filePath = this.resolveFilePath(path2);
3206
- const dir = dirname(filePath);
3309
+ const dir = dirname2(filePath);
3207
3310
  if (!existsSync5(dir)) {
3208
3311
  mkdirSync3(dir, { recursive: true });
3209
3312
  }
@@ -3215,7 +3318,7 @@ var FileSystemPageProvider = class {
3215
3318
  const rootDir = this.resolveRootDir(path2);
3216
3319
  if (existsSync5(filePath)) {
3217
3320
  await unlink(filePath);
3218
- let dir = dirname(filePath);
3321
+ let dir = dirname2(filePath);
3219
3322
  while (dir !== rootDir) {
3220
3323
  try {
3221
3324
  const remaining = readdirSync3(dir);
@@ -3227,7 +3330,7 @@ var FileSystemPageProvider = class {
3227
3330
  } catch {
3228
3331
  break;
3229
3332
  }
3230
- dir = dirname(dir);
3333
+ dir = dirname2(dir);
3231
3334
  }
3232
3335
  }
3233
3336
  }
@@ -3266,4 +3369,4 @@ export {
3266
3369
  createServer,
3267
3370
  FileSystemPageProvider
3268
3371
  };
3269
- //# sourceMappingURL=chunk-CHD5UCFF.js.map
3372
+ //# sourceMappingURL=chunk-V7CD7V7W.js.map