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.
Files changed (135) hide show
  1. package/build-astro.ts +183 -13
  2. package/build-next.ts +1361 -0
  3. package/build-static.ts +7 -5
  4. package/dist/bin/cli.js +2 -2
  5. package/dist/build-static.js +6 -6
  6. package/dist/chunks/{chunk-HNLUO36W.js → chunk-GZHGVVW3.js} +2 -2
  7. package/dist/chunks/chunk-GZHGVVW3.js.map +7 -0
  8. package/dist/chunks/{chunk-LPVETICS.js → chunk-H3GJ4H2U.js} +185 -1
  9. package/dist/chunks/chunk-H3GJ4H2U.js.map +7 -0
  10. package/dist/chunks/{chunk-CXCBV2M7.js → chunk-IGYR22T6.js} +76 -270
  11. package/dist/chunks/chunk-IGYR22T6.js.map +7 -0
  12. package/dist/chunks/{chunk-LHLHPYSP.js → chunk-JGP5A3Y5.js} +12 -11
  13. package/dist/chunks/chunk-JGP5A3Y5.js.map +7 -0
  14. package/dist/chunks/{chunk-7NIC4I3V.js → chunk-JGWFTO6P.js} +167 -21
  15. package/dist/chunks/chunk-JGWFTO6P.js.map +7 -0
  16. package/dist/chunks/{chunk-EDQSMAMP.js → chunk-O3NAGJP4.js} +85 -4
  17. package/dist/chunks/chunk-O3NAGJP4.js.map +7 -0
  18. package/dist/chunks/{chunk-H4JSCDNW.js → chunk-QB2LNO4W.js} +24 -1
  19. package/dist/chunks/chunk-QB2LNO4W.js.map +7 -0
  20. package/dist/chunks/{chunk-A725KYFK.js → chunk-R6XHAFBF.js} +561 -112
  21. package/dist/chunks/chunk-R6XHAFBF.js.map +7 -0
  22. package/dist/chunks/{chunk-J23ZX5AP.js → chunk-X754AHS5.js} +277 -1
  23. package/dist/chunks/chunk-X754AHS5.js.map +7 -0
  24. package/dist/chunks/{chunk-2QK6U5UK.js → chunk-YBLHKYFF.js} +12 -2
  25. package/dist/chunks/chunk-YBLHKYFF.js.map +7 -0
  26. package/dist/chunks/{constants-GWBAD66U.js → constants-STK2YBIW.js} +2 -2
  27. package/dist/entries/server-router.js +7 -7
  28. package/dist/lib/client/index.js +354 -59
  29. package/dist/lib/client/index.js.map +4 -4
  30. package/dist/lib/server/index.js +1458 -190
  31. package/dist/lib/server/index.js.map +4 -4
  32. package/dist/lib/shared/index.js +202 -34
  33. package/dist/lib/shared/index.js.map +4 -4
  34. package/dist/lib/test-utils/index.js +1 -1
  35. package/entries/client-router.tsx +5 -165
  36. package/lib/client/ErrorBoundary.test.tsx +27 -25
  37. package/lib/client/ErrorBoundary.tsx +34 -19
  38. package/lib/client/core/ComponentBuilder.ts +19 -2
  39. package/lib/client/core/builders/embedBuilder.ts +8 -4
  40. package/lib/client/core/builders/listBuilder.ts +23 -4
  41. package/lib/client/fontFamiliesService.test.ts +76 -0
  42. package/lib/client/fontFamiliesService.ts +69 -0
  43. package/lib/client/hmrCssReload.ts +160 -0
  44. package/lib/client/hooks/useColorVariables.ts +2 -0
  45. package/lib/client/index.ts +4 -0
  46. package/lib/client/meno-filter/ui.ts +2 -0
  47. package/lib/client/routing/RouteLoader.test.ts +2 -2
  48. package/lib/client/routing/RouteLoader.ts +8 -2
  49. package/lib/client/routing/Router.tsx +81 -15
  50. package/lib/client/scripts/ScriptExecutor.test.ts +143 -0
  51. package/lib/client/scripts/ScriptExecutor.ts +56 -2
  52. package/lib/client/styles/StyleInjector.ts +20 -5
  53. package/lib/client/styles/UtilityClassCollector.ts +7 -1
  54. package/lib/client/styles/cspNonce.test.ts +67 -0
  55. package/lib/client/styles/cspNonce.ts +63 -0
  56. package/lib/client/templateEngine.test.ts +80 -0
  57. package/lib/client/templateEngine.ts +5 -0
  58. package/lib/server/astro/cmsPageEmitter.ts +35 -5
  59. package/lib/server/astro/componentEmitter.ts +61 -5
  60. package/lib/server/astro/nodeToAstro.ts +149 -11
  61. package/lib/server/astro/normalizeOrphanTemplateProps.test.ts +264 -0
  62. package/lib/server/astro/normalizeOrphanTemplateProps.ts +184 -0
  63. package/lib/server/createServer.ts +11 -0
  64. package/lib/server/draftPageStore.ts +49 -0
  65. package/lib/server/fileWatcher.ts +62 -2
  66. package/lib/server/index.ts +13 -1
  67. package/lib/server/providers/fileSystemPageProvider.ts +8 -0
  68. package/lib/server/routes/api/components.ts +9 -4
  69. package/lib/server/routes/api/core-routes.ts +2 -2
  70. package/lib/server/routes/api/pages.ts +14 -22
  71. package/lib/server/routes/api/shared.ts +56 -0
  72. package/lib/server/routes/index.ts +90 -0
  73. package/lib/server/routes/pages.ts +13 -6
  74. package/lib/server/services/componentService.test.ts +199 -2
  75. package/lib/server/services/componentService.ts +354 -49
  76. package/lib/server/services/fileWatcherService.ts +4 -24
  77. package/lib/server/services/pageService.test.ts +23 -0
  78. package/lib/server/services/pageService.ts +124 -6
  79. package/lib/server/ssr/attributeBuilder.ts +8 -2
  80. package/lib/server/ssr/buildErrorOverlay.ts +1 -1
  81. package/lib/server/ssr/errorOverlay.test.ts +21 -2
  82. package/lib/server/ssr/errorOverlay.ts +38 -11
  83. package/lib/server/ssr/htmlGenerator.test.ts +53 -13
  84. package/lib/server/ssr/htmlGenerator.ts +71 -27
  85. package/lib/server/ssr/liveReloadIntegration.test.ts +123 -2
  86. package/lib/server/ssr/ssrRenderer.test.ts +67 -0
  87. package/lib/server/ssr/ssrRenderer.ts +94 -9
  88. package/lib/server/websocketManager.ts +0 -1
  89. package/lib/shared/componentRefs.ts +45 -0
  90. package/lib/shared/constants.ts +8 -0
  91. package/lib/shared/cssGeneration.ts +2 -0
  92. package/lib/shared/cssProperties.ts +184 -0
  93. package/lib/shared/expressionEvaluator.ts +54 -0
  94. package/lib/shared/fontCss.ts +101 -0
  95. package/lib/shared/fontLoader.ts +8 -86
  96. package/lib/shared/friendlyError.test.ts +87 -0
  97. package/lib/shared/friendlyError.ts +121 -0
  98. package/lib/shared/hrefRefs.test.ts +130 -0
  99. package/lib/shared/hrefRefs.ts +100 -0
  100. package/lib/shared/index.ts +52 -0
  101. package/lib/shared/inlineSvgStyleRules.test.ts +108 -0
  102. package/lib/shared/inlineSvgStyleRules.ts +134 -0
  103. package/lib/shared/interfaces/contentProvider.ts +13 -0
  104. package/lib/shared/itemTemplateUtils.test.ts +14 -0
  105. package/lib/shared/itemTemplateUtils.ts +4 -1
  106. package/lib/shared/registry/NodeTypeDefinition.ts +1 -1
  107. package/lib/shared/registry/nodeTypes/LinkNodeType.ts +1 -1
  108. package/lib/shared/slugTranslator.test.ts +24 -0
  109. package/lib/shared/slugTranslator.ts +24 -0
  110. package/lib/shared/styleNodeUtils.ts +4 -1
  111. package/lib/shared/tree/PathBuilder.test.ts +128 -1
  112. package/lib/shared/tree/PathBuilder.ts +83 -31
  113. package/lib/shared/types/comment.ts +99 -0
  114. package/lib/shared/types/index.ts +12 -0
  115. package/lib/shared/types/rendering.ts +8 -0
  116. package/lib/shared/utilityClassConfig.ts +4 -2
  117. package/lib/shared/utilityClassMapper.test.ts +24 -0
  118. package/lib/shared/validation/commentValidators.ts +69 -0
  119. package/lib/shared/validation/index.ts +1 -0
  120. package/lib/shared/viewportUnits.integration.test.ts +42 -0
  121. package/lib/shared/viewportUnits.test.ts +103 -0
  122. package/lib/shared/viewportUnits.ts +63 -0
  123. package/lib/test-utils/dom-setup.ts +6 -0
  124. package/package.json +1 -1
  125. package/dist/chunks/chunk-2QK6U5UK.js.map +0 -7
  126. package/dist/chunks/chunk-7NIC4I3V.js.map +0 -7
  127. package/dist/chunks/chunk-A725KYFK.js.map +0 -7
  128. package/dist/chunks/chunk-CXCBV2M7.js.map +0 -7
  129. package/dist/chunks/chunk-EDQSMAMP.js.map +0 -7
  130. package/dist/chunks/chunk-H4JSCDNW.js.map +0 -7
  131. package/dist/chunks/chunk-HNLUO36W.js.map +0 -7
  132. package/dist/chunks/chunk-J23ZX5AP.js.map +0 -7
  133. package/dist/chunks/chunk-LHLHPYSP.js.map +0 -7
  134. package/dist/chunks/chunk-LPVETICS.js.map +0 -7
  135. /package/dist/chunks/{constants-GWBAD66U.js.map → constants-STK2YBIW.js.map} +0 -0
@@ -1,6 +1,5 @@
1
1
  import {
2
2
  CachedConfigLoader,
3
- buildSlugIndex,
4
3
  colorService,
5
4
  formatJsonErrorMessage,
6
5
  generateFontCSS,
@@ -13,9 +12,8 @@ import {
13
12
  loadProjectConfig,
14
13
  mapPageNameToPath,
15
14
  parseJSON,
16
- resolveSlugToPageId,
17
15
  variableService
18
- } from "./chunk-CXCBV2M7.js";
16
+ } from "./chunk-IGYR22T6.js";
19
17
  import {
20
18
  configService
21
19
  } from "./chunk-2MHDV5BF.js";
@@ -37,14 +35,18 @@ import {
37
35
  writeFile
38
36
  } from "./chunk-WQFG7PAH.js";
39
37
  import {
40
- extractStringValue
41
- } from "./chunk-H4JSCDNW.js";
38
+ extractStringValue,
39
+ rewriteComponentRefs
40
+ } from "./chunk-QB2LNO4W.js";
42
41
  import {
43
- isPathWithinRoot
44
- } from "./chunk-J23ZX5AP.js";
42
+ buildSlugIndex,
43
+ isPathWithinRoot,
44
+ resolveSlugToPageId
45
+ } from "./chunk-X754AHS5.js";
45
46
  import {
46
- addItemUrls
47
- } from "./chunk-7NIC4I3V.js";
47
+ addItemUrls,
48
+ toFriendlyError
49
+ } from "./chunk-JGWFTO6P.js";
48
50
  import {
49
51
  parseLocaleFromPath
50
52
  } from "./chunk-AZQYF6KE.js";
@@ -55,7 +57,7 @@ import {
55
57
  SERVER_PORT,
56
58
  WEBSOCKET_STATES,
57
59
  init_constants
58
- } from "./chunk-2QK6U5UK.js";
60
+ } from "./chunk-YBLHKYFF.js";
59
61
 
60
62
  // lib/server/pageCache.ts
61
63
  var PageCache = class {
@@ -540,6 +542,39 @@ var PageService = class {
540
542
  const lineMap = buildLineMap(content);
541
543
  this.pageCache.set(path2, content, lineMap);
542
544
  }
545
+ /**
546
+ * Re-read a single page from disk via the provider and refresh the cache.
547
+ *
548
+ * Used by the file watcher for live reload. Provider-based (not a hardcoded
549
+ * JSON read), so it works for any format — the FileSystem provider returns the
550
+ * raw JSON and the Astro provider returns the parsed model serialized to JSON.
551
+ *
552
+ * @returns true if the cache changed (page updated or confirmed-deleted) and a
553
+ * reload broadcast is warranted; false if the read failed transiently (e.g. a
554
+ * partial mid-write or parse error), in which case the cache is left intact and
555
+ * the next watcher event retries.
556
+ */
557
+ async reloadPageFromDisk(path2) {
558
+ if (!this.provider) return false;
559
+ let content = null;
560
+ try {
561
+ content = await this.provider.get(path2);
562
+ } catch {
563
+ return false;
564
+ }
565
+ if (content) {
566
+ this.pageCache.set(path2, content, buildLineMap(content));
567
+ return true;
568
+ }
569
+ try {
570
+ if (!await this.provider.exists(path2)) {
571
+ this.pageCache.delete(path2);
572
+ return true;
573
+ }
574
+ } catch {
575
+ }
576
+ return false;
577
+ }
543
578
  /**
544
579
  * Delete page from cache and optionally from storage
545
580
  *
@@ -616,6 +651,19 @@ var PageService = class {
616
651
  getLineMap(path2) {
617
652
  return this.pageCache.getLineMap(path2);
618
653
  }
654
+ /**
655
+ * Pages base directory — provider-aware (e.g. `src/pages` for astro projects),
656
+ * falling back to `projectPaths.pages()` when the provider doesn't expose
657
+ * `baseDir()` (or no provider is set). Used by the folder/move/rename ops that
658
+ * touch the filesystem directly.
659
+ */
660
+ pagesBaseDir() {
661
+ return this.provider?.baseDir?.() ?? projectPaths.pages();
662
+ }
663
+ /** Page file extension (e.g. `.astro` for astro projects), defaulting to `.json`. */
664
+ pageExt() {
665
+ return this.provider?.extension?.() ?? ".json";
666
+ }
619
667
  /**
620
668
  * Get all page folders
621
669
  *
@@ -624,7 +672,7 @@ var PageService = class {
624
672
  * @returns Sorted array of folder names
625
673
  */
626
674
  getAllFolders() {
627
- const pagesDir = projectPaths.pages();
675
+ const pagesDir = this.pagesBaseDir();
628
676
  if (!existsSync(pagesDir)) {
629
677
  return [];
630
678
  }
@@ -666,7 +714,7 @@ var PageService = class {
666
714
  throw new Error("Each folder segment can only contain lowercase letters, numbers, dashes, and underscores");
667
715
  }
668
716
  }
669
- const pagesDir = projectPaths.pages();
717
+ const pagesDir = this.pagesBaseDir();
670
718
  const folderPath = join(pagesDir, trimmed);
671
719
  if (existsSync(folderPath)) {
672
720
  throw new Error("Folder already exists");
@@ -684,12 +732,13 @@ var PageService = class {
684
732
  */
685
733
  async movePage(pagePath, newFolder) {
686
734
  const { rename } = await import("fs/promises");
687
- const pagesDir = projectPaths.pages();
735
+ const pagesDir = this.pagesBaseDir();
736
+ const ext = this.pageExt();
688
737
  const pageName = pagePath === "/" ? "index" : pagePath.substring(1);
689
738
  const slashIndex = pageName.lastIndexOf("/");
690
739
  const baseName = slashIndex >= 0 ? pageName.substring(slashIndex + 1) : pageName;
691
- const sourceFile = join(pagesDir, `${pageName}.json`);
692
- const targetFile = newFolder ? join(pagesDir, newFolder, `${baseName}.json`) : join(pagesDir, `${baseName}.json`);
740
+ const sourceFile = join(pagesDir, `${pageName}${ext}`);
741
+ const targetFile = newFolder ? join(pagesDir, newFolder, `${baseName}${ext}`) : join(pagesDir, `${baseName}${ext}`);
693
742
  if (newFolder) {
694
743
  const targetDir = join(pagesDir, newFolder);
695
744
  if (!existsSync(targetDir)) {
@@ -719,6 +768,60 @@ var PageService = class {
719
768
  }
720
769
  }
721
770
  }
771
+ /**
772
+ * Rename a page (file rename + cache update).
773
+ *
774
+ * Renames the underlying .json file from `oldPath`'s filename to `newPath`'s
775
+ * filename, keeping the same folder. Updates the in-memory cache so the
776
+ * page is now keyed at `newPath`. Folder moves stay with `movePage()` —
777
+ * this is purely "change the URL slug of an existing page" for
778
+ * single-locale projects.
779
+ *
780
+ * @param oldPath - Current page path (e.g., "/about" or "/blog/post")
781
+ * @param newPath - New page path with same folder (e.g., "/about-us" or "/blog/new-slug")
782
+ * @throws {Error} If oldPath doesn't exist, newPath already exists, or paths
783
+ * reference different folders.
784
+ */
785
+ async renamePage(oldPath, newPath) {
786
+ if (oldPath === newPath) return;
787
+ if (newPath === "/") {
788
+ throw new Error("Cannot rename a page to the index path");
789
+ }
790
+ if (!newPath.startsWith("/")) {
791
+ throw new Error('newPath must start with "/"');
792
+ }
793
+ const oldName = oldPath === "/" ? "index" : oldPath.substring(1);
794
+ const newName = newPath.substring(1);
795
+ const oldSlash = oldName.lastIndexOf("/");
796
+ const newSlash = newName.lastIndexOf("/");
797
+ const oldFolder = oldSlash >= 0 ? oldName.substring(0, oldSlash) : "";
798
+ const newFolder = newSlash >= 0 ? newName.substring(0, newSlash) : "";
799
+ if (oldFolder !== newFolder) {
800
+ throw new Error("renamePage only changes the filename; use movePage to change folder");
801
+ }
802
+ const baseSegment = newSlash >= 0 ? newName.substring(newSlash + 1) : newName;
803
+ if (!/^[a-z0-9][a-z0-9-_]*$/.test(baseSegment)) {
804
+ throw new Error("Page slug can only contain lowercase letters, numbers, dashes, and underscores");
805
+ }
806
+ const { rename } = await import("fs/promises");
807
+ const pagesDir = this.pagesBaseDir();
808
+ const ext = this.pageExt();
809
+ const sourceFile = join(pagesDir, `${oldName}${ext}`);
810
+ const targetFile = join(pagesDir, `${newName}${ext}`);
811
+ if (!existsSync(sourceFile)) {
812
+ throw new Error(`Page not found: ${oldPath}`);
813
+ }
814
+ if (existsSync(targetFile)) {
815
+ throw new Error(`Page already exists: ${newPath}`);
816
+ }
817
+ await rename(sourceFile, targetFile);
818
+ const content = this.pageCache.getContent(oldPath);
819
+ if (content) {
820
+ this.pageCache.delete(oldPath);
821
+ const lineMap = buildLineMap(content);
822
+ this.pageCache.set(newPath, content, lineMap);
823
+ }
824
+ }
722
825
  /**
723
826
  * Get slug mappings for all pages
724
827
  *
@@ -765,6 +868,7 @@ var ComponentService = class {
765
868
  componentCategories = /* @__PURE__ */ new Map();
766
869
  fs;
767
870
  loader;
871
+ writer;
768
872
  loadErrors = /* @__PURE__ */ new Map();
769
873
  loadWarnings = [];
770
874
  /**
@@ -772,11 +876,26 @@ var ComponentService = class {
772
876
  *
773
877
  * @param options - Optional configuration for dependency injection
774
878
  * @param options.fs - Optional file system interface for testing
775
- * @param options.loader - Optional component loader interface for testing
879
+ * @param options.loader - Optional component loader interface for testing (READ)
880
+ * @param options.writer - Optional component writer for non-JSON on-disk formats
881
+ * (WRITE). When omitted, the service writes JSON exactly as before — legacy
882
+ * projects are byte-identical. The astro bootstrap injects an AstroComponentWriter
883
+ * so saves emit `.astro` files under `src/components/`.
776
884
  */
777
885
  constructor(options) {
778
886
  this.fs = options?.fs;
779
887
  this.loader = options?.loader;
888
+ this.writer = options?.writer;
889
+ }
890
+ /**
891
+ * Base components directory — `<root>/components` for JSON, or whatever the
892
+ * injected writer owns (e.g. `<root>/src/components` for astro). All path
893
+ * resolution (categories, folders, save/move/rename targets) routes through
894
+ * here so a single switch covers every write op.
895
+ * @internal
896
+ */
897
+ componentsBaseDir() {
898
+ return this.writer ? this.writer.componentsDir() : projectPaths.components();
780
899
  }
781
900
  /**
782
901
  * Load all components from the components directory
@@ -795,28 +914,32 @@ var ComponentService = class {
795
914
  * ```
796
915
  */
797
916
  async loadAllComponents() {
798
- this.components.clear();
799
- this.componentCategories.clear();
800
- this.loadErrors.clear();
801
- this.loadWarnings = [];
917
+ const nextComponents = /* @__PURE__ */ new Map();
918
+ const nextCategories = /* @__PURE__ */ new Map();
919
+ const nextErrors = /* @__PURE__ */ new Map();
920
+ let nextWarnings = [];
802
921
  let loadedComponents;
803
922
  if (this.loader) {
804
923
  loadedComponents = await this.loader.loadDirectory(projectPaths.components());
805
924
  } else {
806
925
  const result = await loadComponentDirectory(projectPaths.components());
807
926
  loadedComponents = result.components;
808
- this.loadWarnings = result.warnings;
927
+ nextWarnings = result.warnings;
809
928
  for (const error of result.errors) {
810
- this.loadErrors.set(error.componentName, error);
929
+ nextErrors.set(error.componentName, error);
811
930
  }
812
931
  }
813
932
  loadedComponents.forEach((value, key) => {
814
933
  const componentWithCategory = value;
815
934
  const category = componentWithCategory._category;
816
- this.componentCategories.set(key, category);
935
+ nextCategories.set(key, category);
817
936
  const { _category, _relativePath, ...cleanDef } = componentWithCategory;
818
- this.components.set(key, cleanDef);
937
+ nextComponents.set(key, cleanDef);
819
938
  });
939
+ this.components = nextComponents;
940
+ this.componentCategories = nextCategories;
941
+ this.loadErrors = nextErrors;
942
+ this.loadWarnings = nextWarnings;
820
943
  }
821
944
  /**
822
945
  * Get component by name
@@ -968,10 +1091,60 @@ var ComponentService = class {
968
1091
  * @internal
969
1092
  */
970
1093
  getComponentDir(name, category) {
971
- const componentsDir = projectPaths.components();
972
- const cat = category ?? this.componentCategories.get(name);
1094
+ const componentsDir = this.componentsBaseDir();
1095
+ const cat = this.resolveComponentCategory(name, category);
973
1096
  return cat ? join2(componentsDir, cat) : componentsDir;
974
1097
  }
1098
+ /**
1099
+ * Resolve which category a save should land in. Priority:
1100
+ * 1. Explicit caller-supplied category (including '' for root)
1101
+ * 2. In-memory categories cache
1102
+ * 3. Existing file on disk (defense against a stale/empty cache)
1103
+ * Falls back to root only when none of the above resolves. The disk
1104
+ * scan in step 3 closes the race where a save fires while
1105
+ * loadAllComponents has been called but hasn't rebuilt the cache yet.
1106
+ * @internal
1107
+ */
1108
+ resolveComponentCategory(name, category) {
1109
+ if (category !== void 0) {
1110
+ return category || void 0;
1111
+ }
1112
+ if (this.componentCategories.has(name)) {
1113
+ return this.componentCategories.get(name);
1114
+ }
1115
+ return this.findComponentCategoryOnDisk(name);
1116
+ }
1117
+ /**
1118
+ * Look on disk for an existing `<name>.json` and return its category.
1119
+ * Returns undefined for "exists at root" or "not found anywhere" — both
1120
+ * map to writing at root, which is the right default.
1121
+ * @internal
1122
+ */
1123
+ findComponentCategoryOnDisk(name) {
1124
+ const componentsDir = this.componentsBaseDir();
1125
+ if (!existsSync2(componentsDir)) return void 0;
1126
+ const ext = this.writer ? null : ".json";
1127
+ const existsAt = (dir) => {
1128
+ if (ext) return existsSync2(join2(dir, `${name}${ext}`));
1129
+ try {
1130
+ return readdirSync2(dir, { withFileTypes: true }).some(
1131
+ (e) => e.isFile() && (e.name === `${name}.json` || e.name === `${name}.astro`)
1132
+ );
1133
+ } catch {
1134
+ return false;
1135
+ }
1136
+ };
1137
+ if (existsAt(componentsDir)) return void 0;
1138
+ try {
1139
+ for (const entry of readdirSync2(componentsDir, { withFileTypes: true })) {
1140
+ if (entry.isDirectory() && existsAt(join2(componentsDir, entry.name))) {
1141
+ return entry.name;
1142
+ }
1143
+ }
1144
+ } catch {
1145
+ }
1146
+ return void 0;
1147
+ }
975
1148
  /**
976
1149
  * Get component JavaScript from .js file
977
1150
  *
@@ -1029,18 +1202,23 @@ var ComponentService = class {
1029
1202
  * ```
1030
1203
  */
1031
1204
  async saveComponent(name, data, category) {
1032
- const writeFile2 = this.fs ? this.fs.writeFile.bind(this.fs) : (await import("fs/promises")).writeFile;
1033
1205
  const dataWithoutJS = JSON.parse(JSON.stringify(data));
1034
1206
  if (dataWithoutJS?.component?.javascript !== void 0) {
1035
1207
  delete dataWithoutJS.component.javascript;
1036
1208
  }
1037
- const targetCategory = category !== void 0 ? category || void 0 : this.componentCategories.get(name);
1038
- const componentDir = this.getComponentDir(name, targetCategory);
1039
- if (targetCategory && !existsSync2(componentDir)) {
1040
- mkdirSync2(componentDir, { recursive: true });
1209
+ const targetCategory = this.resolveComponentCategory(name, category);
1210
+ const componentsDir = this.componentsBaseDir();
1211
+ const componentDir = targetCategory ? join2(componentsDir, targetCategory) : componentsDir;
1212
+ if (this.writer) {
1213
+ await this.writer.writeComponent(componentDir, name, dataWithoutJS);
1214
+ } else {
1215
+ const writeFile2 = this.fs ? this.fs.writeFile.bind(this.fs) : (await import("fs/promises")).writeFile;
1216
+ if (targetCategory && !existsSync2(componentDir)) {
1217
+ mkdirSync2(componentDir, { recursive: true });
1218
+ }
1219
+ const filePath = join2(componentDir, `${name}.json`);
1220
+ await writeFile2(filePath, JSON.stringify(dataWithoutJS, null, 2), "utf-8");
1041
1221
  }
1042
- const filePath = join2(componentDir, `${name}.json`);
1043
- await writeFile2(filePath, JSON.stringify(dataWithoutJS, null, 2), "utf-8");
1044
1222
  this.components.set(name, dataWithoutJS);
1045
1223
  this.componentCategories.set(name, targetCategory);
1046
1224
  }
@@ -1062,8 +1240,15 @@ var ComponentService = class {
1062
1240
  * ```
1063
1241
  */
1064
1242
  async saveComponentJavaScript(name, javascript) {
1065
- const writeFile2 = this.fs ? this.fs.writeFile.bind(this.fs) : (await import("fs/promises")).writeFile;
1066
1243
  const componentDir = this.getComponentDir(name);
1244
+ if (this.writer) {
1245
+ const current = this.components.get(name);
1246
+ await this.writer.writeJavaScript(componentDir, name, javascript || "", current);
1247
+ const next = current ? { ...current, component: { ...current.component, javascript: javascript || "" } } : { component: { javascript: javascript || "" } };
1248
+ this.components.set(name, next);
1249
+ return;
1250
+ }
1251
+ const writeFile2 = this.fs ? this.fs.writeFile.bind(this.fs) : (await import("fs/promises")).writeFile;
1067
1252
  const jsFilePath = join2(componentDir, `${name}.js`);
1068
1253
  await writeFile2(jsFilePath, javascript || "", "utf-8");
1069
1254
  const componentPath = join2(componentDir, `${name}.json`);
@@ -1101,8 +1286,15 @@ var ComponentService = class {
1101
1286
  * ```
1102
1287
  */
1103
1288
  async saveComponentCSS(name, css) {
1104
- const writeFile2 = this.fs ? this.fs.writeFile.bind(this.fs) : (await import("fs/promises")).writeFile;
1105
1289
  const componentDir = this.getComponentDir(name);
1290
+ if (this.writer) {
1291
+ const current = this.components.get(name);
1292
+ await this.writer.writeCSS(componentDir, name, css || "", current);
1293
+ const next = current ? { ...current, component: { ...current.component, css: css || "" } } : { component: { css: css || "" } };
1294
+ this.components.set(name, next);
1295
+ return;
1296
+ }
1297
+ const writeFile2 = this.fs ? this.fs.writeFile.bind(this.fs) : (await import("fs/promises")).writeFile;
1106
1298
  const cssFilePath = join2(componentDir, `${name}.css`);
1107
1299
  await writeFile2(cssFilePath, css || "", "utf-8");
1108
1300
  const componentPath = join2(componentDir, `${name}.json`);
@@ -1137,7 +1329,7 @@ var ComponentService = class {
1137
1329
  * ```
1138
1330
  */
1139
1331
  getAllFolders() {
1140
- const componentsDir = projectPaths.components();
1332
+ const componentsDir = this.componentsBaseDir();
1141
1333
  if (!existsSync2(componentsDir)) {
1142
1334
  return [];
1143
1335
  }
@@ -1172,7 +1364,7 @@ var ComponentService = class {
1172
1364
  if (sanitized !== folderName.trim()) {
1173
1365
  throw new Error("Folder name can only contain lowercase letters, numbers, dashes, and underscores");
1174
1366
  }
1175
- const componentsDir = projectPaths.components();
1367
+ const componentsDir = this.componentsBaseDir();
1176
1368
  const folderPath = join2(componentsDir, folderName);
1177
1369
  if (existsSync2(folderPath)) {
1178
1370
  throw new Error("Folder already exists");
@@ -1206,19 +1398,23 @@ var ComponentService = class {
1206
1398
  if (currentCategory === targetCategory) {
1207
1399
  return;
1208
1400
  }
1209
- const componentsDir = projectPaths.components();
1401
+ const componentsDir = this.componentsBaseDir();
1210
1402
  const sourceDir = currentCategory ? join2(componentsDir, currentCategory) : componentsDir;
1211
1403
  const targetDir = targetCategory ? join2(componentsDir, targetCategory) : componentsDir;
1212
- if (targetCategory && !existsSync2(targetDir)) {
1213
- mkdirSync2(targetDir, { recursive: true });
1214
- }
1215
- const extensions = [".json", ".js", ".css"];
1216
- const { rename } = await import("fs/promises");
1217
- for (const ext of extensions) {
1218
- const sourcePath = join2(sourceDir, `${name}${ext}`);
1219
- const targetPath = join2(targetDir, `${name}${ext}`);
1220
- if (existsSync2(sourcePath)) {
1221
- await rename(sourcePath, targetPath);
1404
+ if (this.writer) {
1405
+ await this.writer.moveComponent(sourceDir, targetDir, name);
1406
+ } else {
1407
+ if (targetCategory && !existsSync2(targetDir)) {
1408
+ mkdirSync2(targetDir, { recursive: true });
1409
+ }
1410
+ const extensions = [".json", ".js", ".css"];
1411
+ const { rename } = await import("fs/promises");
1412
+ for (const ext of extensions) {
1413
+ const sourcePath = join2(sourceDir, `${name}${ext}`);
1414
+ const targetPath = join2(targetDir, `${name}${ext}`);
1415
+ if (existsSync2(sourcePath)) {
1416
+ await rename(sourcePath, targetPath);
1417
+ }
1222
1418
  }
1223
1419
  }
1224
1420
  this.componentCategories.set(name, targetCategory);
@@ -1232,6 +1428,112 @@ var ComponentService = class {
1232
1428
  }
1233
1429
  }
1234
1430
  }
1431
+ /**
1432
+ * Rename a component across the project.
1433
+ *
1434
+ * Renames the component's `.json`/`.js`/`.css` files on disk (respecting its
1435
+ * category folder), updates the in-memory caches, rewrites every
1436
+ * `{ type: "component", component: "<oldName>" }` reference inside other
1437
+ * component structures, and — if `pageService` is provided — also walks every
1438
+ * page/template and rewrites references there, saving any pages that changed.
1439
+ *
1440
+ * Matching is structural (only `node.component` string equality), not
1441
+ * text-based, so it never touches HTML tags, CSS classes, prop values, or
1442
+ * embedded markup that happen to contain the name as a substring.
1443
+ *
1444
+ * @throws {Error} If old/new name is missing, names are identical, new name
1445
+ * isn't a valid JS identifier, the source component doesn't exist, or the
1446
+ * new name is already taken.
1447
+ */
1448
+ async renameComponent(oldName, newName, pageService) {
1449
+ if (!oldName || typeof oldName !== "string") {
1450
+ throw new Error("oldName is required");
1451
+ }
1452
+ if (!newName || typeof newName !== "string") {
1453
+ throw new Error("newName is required");
1454
+ }
1455
+ const trimmedNew = newName.trim();
1456
+ if (oldName === trimmedNew) {
1457
+ throw new Error("newName must differ from oldName");
1458
+ }
1459
+ if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(trimmedNew)) {
1460
+ throw new Error("newName must be a valid JavaScript identifier");
1461
+ }
1462
+ if (!this.components.has(oldName)) {
1463
+ throw new Error(`Component "${oldName}" not found`);
1464
+ }
1465
+ if (this.components.has(trimmedNew)) {
1466
+ throw new Error(`Component "${trimmedNew}" already exists`);
1467
+ }
1468
+ const category = this.componentCategories.get(oldName);
1469
+ const componentsDir = this.componentsBaseDir();
1470
+ const dir = category ? join2(componentsDir, category) : componentsDir;
1471
+ if (this.writer) {
1472
+ if (await this.writer.componentExists(dir, trimmedNew)) {
1473
+ throw new Error(`Component "${trimmedNew}" already exists in components/${category ?? ""}`);
1474
+ }
1475
+ await this.writer.moveComponent(dir, dir, oldName, trimmedNew);
1476
+ } else {
1477
+ for (const ext of [".json", ".js", ".css"]) {
1478
+ if (existsSync2(join2(dir, `${trimmedNew}${ext}`))) {
1479
+ throw new Error(`File "${trimmedNew}${ext}" already exists in components/${category ?? ""}`);
1480
+ }
1481
+ }
1482
+ const { rename } = await import("fs/promises");
1483
+ for (const ext of [".json", ".js", ".css"]) {
1484
+ const src = join2(dir, `${oldName}${ext}`);
1485
+ const dst = join2(dir, `${trimmedNew}${ext}`);
1486
+ if (existsSync2(src)) {
1487
+ await rename(src, dst);
1488
+ }
1489
+ }
1490
+ }
1491
+ const def = this.components.get(oldName);
1492
+ this.components.delete(oldName);
1493
+ this.componentCategories.delete(oldName);
1494
+ this.components.set(trimmedNew, def);
1495
+ this.componentCategories.set(trimmedNew, category);
1496
+ let componentRefs = 0;
1497
+ const writeFile2 = this.fs ? this.fs.writeFile.bind(this.fs) : (await import("fs/promises")).writeFile;
1498
+ for (const [name, otherDef] of this.components.entries()) {
1499
+ if (name === trimmedNew) continue;
1500
+ const structure = otherDef.component?.structure;
1501
+ if (!structure) continue;
1502
+ if (rewriteComponentRefs(structure, oldName, trimmedNew)) {
1503
+ const otherCategory = this.componentCategories.get(name);
1504
+ const otherDir = otherCategory ? join2(componentsDir, otherCategory) : componentsDir;
1505
+ const persisted = JSON.parse(JSON.stringify(otherDef));
1506
+ if (persisted?.component?.javascript !== void 0) {
1507
+ delete persisted.component.javascript;
1508
+ }
1509
+ if (this.writer) {
1510
+ await this.writer.writeComponent(otherDir, name, persisted);
1511
+ } else {
1512
+ await writeFile2(join2(otherDir, `${name}.json`), JSON.stringify(persisted, null, 2), "utf-8");
1513
+ }
1514
+ componentRefs++;
1515
+ }
1516
+ }
1517
+ let pageRefs = 0;
1518
+ if (pageService) {
1519
+ const pagePaths = pageService.getAllPagePaths();
1520
+ for (const path2 of pagePaths) {
1521
+ const pageData = pageService.getPageData(path2);
1522
+ if (!pageData || !("root" in pageData) || !pageData.root) continue;
1523
+ if (rewriteComponentRefs(pageData.root, oldName, trimmedNew)) {
1524
+ await pageService.savePage(path2, pageData);
1525
+ pageRefs++;
1526
+ }
1527
+ }
1528
+ }
1529
+ return {
1530
+ oldName,
1531
+ newName: trimmedNew,
1532
+ category,
1533
+ componentRefs,
1534
+ pageRefs
1535
+ };
1536
+ }
1235
1537
  };
1236
1538
 
1237
1539
  // lib/server/services/EnumService.ts
@@ -1328,13 +1630,9 @@ var EnumService = class extends CachedConfigLoader {
1328
1630
  };
1329
1631
  var enumService = new EnumService();
1330
1632
 
1331
- // lib/server/services/fileWatcherService.ts
1332
- import { join as join3 } from "path";
1333
- import { existsSync as existsSync4 } from "fs";
1334
-
1335
1633
  // lib/server/fileWatcher.ts
1336
1634
  import { watch, existsSync as existsSync3 } from "fs";
1337
- import { basename, dirname } from "path";
1635
+ import { basename, dirname, join as join3 } from "path";
1338
1636
  function attachWhenDirExists(dirPath, attach, setWatcher) {
1339
1637
  if (existsSync3(dirPath)) {
1340
1638
  setWatcher(attach());
@@ -1369,6 +1667,10 @@ var FileWatcher = class {
1369
1667
  imagesWatcher = null;
1370
1668
  librariesWatcher = null;
1371
1669
  projectConfigWatcher = null;
1670
+ // Astro-format projects keep pages/components/CMS under src/ as .astro/.json.
1671
+ astroPagesWatcher = null;
1672
+ astroComponentsWatcher = null;
1673
+ astroContentWatcher = null;
1372
1674
  /**
1373
1675
  * Start watching components directory
1374
1676
  * Watches both .json and .js files to detect component definition and JavaScript changes
@@ -1565,6 +1867,39 @@ var FileWatcher = class {
1565
1867
  }
1566
1868
  );
1567
1869
  }
1870
+ /**
1871
+ * Astro-format projects keep pages/components/CMS under src/ as .astro/.json.
1872
+ * The onPageChange/onComponentChange/onCMSChange callbacks are format-agnostic
1873
+ * (reload goes through the active provider), so file→editor live reload works
1874
+ * for `.astro` the same way it does for JSON. These are no-ops in JSON projects
1875
+ * (the src/ dirs don't exist).
1876
+ */
1877
+ watchAstroPages(dirPath = join3(getProjectRoot(), "src", "pages")) {
1878
+ if (!existsSync3(dirPath)) return;
1879
+ this.astroPagesWatcher = watch(dirPath, { recursive: true }, async (_event, filename) => {
1880
+ if (filename && filename.endsWith(".astro") && !filename.includes("[")) {
1881
+ const pagePath = mapPageNameToPath(filename.replace(/\.astro$/, ""));
1882
+ if (this.callbacks.onPageChange) await this.callbacks.onPageChange(pagePath);
1883
+ }
1884
+ });
1885
+ }
1886
+ watchAstroComponents(dirPath = join3(getProjectRoot(), "src", "components")) {
1887
+ if (!existsSync3(dirPath)) return;
1888
+ this.astroComponentsWatcher = watch(dirPath, { recursive: true }, async (_event, filename) => {
1889
+ if (filename && filename.endsWith(".astro") && this.callbacks.onComponentChange) {
1890
+ await this.callbacks.onComponentChange();
1891
+ }
1892
+ });
1893
+ }
1894
+ watchAstroContent(dirPath = join3(getProjectRoot(), "src", "content")) {
1895
+ if (!existsSync3(dirPath)) return;
1896
+ this.astroContentWatcher = watch(dirPath, { recursive: true }, async (_event, filename) => {
1897
+ if (filename && filename.endsWith(".json") && this.callbacks.onCMSChange) {
1898
+ const collection = filename.split("/")[0];
1899
+ await this.callbacks.onCMSChange(collection);
1900
+ }
1901
+ });
1902
+ }
1568
1903
  /**
1569
1904
  * Start watching all directories
1570
1905
  */
@@ -1579,6 +1914,9 @@ var FileWatcher = class {
1579
1914
  this.watchImages();
1580
1915
  this.watchLibraries();
1581
1916
  this.watchProjectConfig();
1917
+ this.watchAstroPages();
1918
+ this.watchAstroComponents();
1919
+ this.watchAstroContent();
1582
1920
  }
1583
1921
  /**
1584
1922
  * Stop watching all directories
@@ -1624,6 +1962,18 @@ var FileWatcher = class {
1624
1962
  this.projectConfigWatcher.close();
1625
1963
  this.projectConfigWatcher = null;
1626
1964
  }
1965
+ if (this.astroPagesWatcher) {
1966
+ this.astroPagesWatcher.close();
1967
+ this.astroPagesWatcher = null;
1968
+ }
1969
+ if (this.astroComponentsWatcher) {
1970
+ this.astroComponentsWatcher.close();
1971
+ this.astroComponentsWatcher = null;
1972
+ }
1973
+ if (this.astroContentWatcher) {
1974
+ this.astroContentWatcher.close();
1975
+ this.astroContentWatcher = null;
1976
+ }
1627
1977
  }
1628
1978
  /**
1629
1979
  * Check if watchers are active
@@ -1656,18 +2006,8 @@ var FileWatcherService = class {
1656
2006
  this.wsManager.broadcastUpdate("all");
1657
2007
  },
1658
2008
  onPageChange: async (pagePath) => {
1659
- const pageName = pagePath === "/" ? "index" : pagePath.substring(1);
1660
- const filePath = pageName.startsWith("templates/") ? join3(projectPaths.templates(), `${pageName.substring("templates/".length)}.json`) : join3(projectPaths.pages(), `${pageName}.json`);
1661
- const content = await loadJSONFile(filePath);
1662
- if (content) {
1663
- const lineMap = buildLineMap(content);
1664
- this.pageCache.set(pagePath, content, lineMap);
2009
+ if (await this.pageService.reloadPageFromDisk(pagePath)) {
1665
2010
  this.wsManager.broadcastUpdate(pagePath);
1666
- } else {
1667
- if (!existsSync4(filePath)) {
1668
- this.pageCache.delete(pagePath);
1669
- this.wsManager.broadcastUpdate(pagePath);
1670
- }
1671
2011
  }
1672
2012
  if (pagePath.startsWith("/templates/") && this.cmsService) {
1673
2013
  if (this.refreshSchemasTimer !== null) {
@@ -1862,6 +2202,41 @@ function jsonResponse(data, options = {}) {
1862
2202
  function errorResponse(message, status = 500) {
1863
2203
  return jsonResponse({ error: message }, { status });
1864
2204
  }
2205
+ function fnv1a(str) {
2206
+ let hash = 2166136261;
2207
+ for (let i = 0; i < str.length; i++) {
2208
+ hash ^= str.charCodeAt(i);
2209
+ hash = Math.imul(hash, 16777619);
2210
+ }
2211
+ return (hash >>> 0).toString(36);
2212
+ }
2213
+ function cachedJsonResponse(req, body, options = {}) {
2214
+ const corsHeaders = createCorsHeaders();
2215
+ const contentType = options.contentType ?? "application/json";
2216
+ const etag = `W/"${fnv1a(body)}"`;
2217
+ const ifNoneMatch = req.headers.get("if-none-match");
2218
+ if (ifNoneMatch && ifNoneMatch === etag) {
2219
+ return new Response(null, {
2220
+ status: 304,
2221
+ headers: {
2222
+ "ETag": etag,
2223
+ "Cache-Control": "no-cache",
2224
+ ...corsHeaders
2225
+ }
2226
+ });
2227
+ }
2228
+ return new Response(body, {
2229
+ status: options.status ?? 200,
2230
+ headers: {
2231
+ "Content-Type": contentType,
2232
+ "ETag": etag,
2233
+ // `no-cache` (not `no-store`) — browser may keep the body but must
2234
+ // revalidate every time. Pairs with the 304 branch above.
2235
+ "Cache-Control": "no-cache",
2236
+ ...corsHeaders
2237
+ }
2238
+ });
2239
+ }
1865
2240
 
1866
2241
  // lib/server/routes/api/cms.ts
1867
2242
  function handleCollectionsRoute(cmsService) {
@@ -1958,32 +2333,21 @@ function handlePageDataRoute(url, pageService) {
1958
2333
  return jsonResponse({ error: "Page not found" }, { status: 404 });
1959
2334
  }
1960
2335
  }
1961
- function handlePageContentRoute(url, pageService) {
2336
+ function handlePageContentRoute(req, url, pageService) {
1962
2337
  const page = url.searchParams.get("page") || "/";
1963
2338
  const content = pageService.getPage(page);
1964
- const corsHeaders = createCorsHeaders();
1965
2339
  if (content) {
1966
- return new Response(content, {
1967
- headers: {
1968
- "Content-Type": "application/json",
1969
- "Cache-Control": "no-store, max-age=0",
1970
- "Pragma": "no-cache",
1971
- "Expires": "0",
1972
- ...corsHeaders
1973
- }
1974
- });
1975
- } else {
1976
- return new Response("Page not found", {
1977
- status: 404,
1978
- headers: {
1979
- "Content-Type": "application/json",
1980
- "Cache-Control": "no-store, max-age=0",
1981
- "Pragma": "no-cache",
1982
- "Expires": "0",
1983
- ...corsHeaders
1984
- }
1985
- });
2340
+ return cachedJsonResponse(req, content);
1986
2341
  }
2342
+ const corsHeaders = createCorsHeaders();
2343
+ return new Response("Page not found", {
2344
+ status: 404,
2345
+ headers: {
2346
+ "Content-Type": "application/json",
2347
+ "Cache-Control": "no-store, max-age=0",
2348
+ ...corsHeaders
2349
+ }
2350
+ });
1987
2351
  }
1988
2352
  function handleSlugMappingsRoute(pageService) {
1989
2353
  const mappings = pageService.getSlugMappings();
@@ -1991,7 +2355,7 @@ function handleSlugMappingsRoute(pageService) {
1991
2355
  }
1992
2356
 
1993
2357
  // lib/server/routes/api/components.ts
1994
- function handleComponentsRoute(componentService) {
2358
+ function handleComponentsRoute(req, componentService) {
1995
2359
  const componentsWithCategories = componentService.getAllComponentsWithCategories();
1996
2360
  const result = {};
1997
2361
  for (const [name, info] of Object.entries(componentsWithCategories)) {
@@ -2004,7 +2368,7 @@ function handleComponentsRoute(componentService) {
2004
2368
  if (diagnostics.errors.length > 0 || diagnostics.warnings.length > 0) {
2005
2369
  result._diagnostics = diagnostics;
2006
2370
  }
2007
- return jsonResponse(result);
2371
+ return cachedJsonResponse(req, JSON.stringify(result));
2008
2372
  }
2009
2373
  function handleComponentDataRoute(url, componentService) {
2010
2374
  const componentName = url.pathname.replace("/api/component-data/", "");
@@ -2293,13 +2657,13 @@ async function handleCoreApiRoutes(req, url, context) {
2293
2657
  }
2294
2658
  if (url.pathname === API_ROUTES.PAGE_CONTENT && req.method === "GET") {
2295
2659
  return await handleRouteError(
2296
- () => Promise.resolve(handlePageContentRoute(url, pageService)),
2660
+ () => Promise.resolve(handlePageContentRoute(req, url, pageService)),
2297
2661
  "Failed to fetch page content"
2298
2662
  );
2299
2663
  }
2300
2664
  if (url.pathname === API_ROUTES.COMPONENTS && req.method === "GET") {
2301
2665
  return await handleRouteError(
2302
- () => Promise.resolve(handleComponentsRoute(componentService)),
2666
+ () => Promise.resolve(handleComponentsRoute(req, componentService)),
2303
2667
  "Failed to fetch components list"
2304
2668
  );
2305
2669
  }
@@ -2503,13 +2867,21 @@ function safeJsonForScript(data) {
2503
2867
  }
2504
2868
  function generateErrorPage(error, context, cspNonce) {
2505
2869
  const errorInfo = extractErrorInfo(error);
2870
+ const friendly = toFriendlyError(errorInfo.message);
2506
2871
  const errorMessage = escapeHtml(errorInfo.message);
2507
2872
  const errorStack = errorInfo.stack ? escapeHtml(errorInfo.stack) : "";
2873
+ const renderInline = (s) => escapeHtml(s).replace(/`([^`]+)`/g, "<code>$1</code>");
2874
+ const friendlyTitle = escapeHtml(friendly.title);
2875
+ const friendlyMessage = renderInline(friendly.friendlyMessage);
2876
+ const friendlyHint = friendly.hint ? renderInline(friendly.hint) : "";
2508
2877
  const nonceAttr = cspNonce ? ` nonce="${cspNonce}"` : "";
2509
2878
  const errorDataJson = safeJsonForScript({
2510
2879
  type: "PREVIEW_ERROR",
2511
2880
  error: {
2512
- message: errorInfo.message,
2881
+ message: friendly.friendlyMessage,
2882
+ title: friendly.title,
2883
+ hint: friendly.hint,
2884
+ raw: errorInfo.message,
2513
2885
  stack: errorInfo.stack,
2514
2886
  context
2515
2887
  }
@@ -2523,7 +2895,7 @@ function generateErrorPage(error, context, cspNonce) {
2523
2895
  <meta charset="UTF-8">
2524
2896
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
2525
2897
  <title>Preview Error</title>
2526
- <style>
2898
+ <style${nonceAttr}>
2527
2899
  * { margin: 0; padding: 0; box-sizing: border-box; }
2528
2900
  body {
2529
2901
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
@@ -2560,6 +2932,21 @@ function generateErrorPage(error, context, cspNonce) {
2560
2932
  text-transform: uppercase;
2561
2933
  letter-spacing: 0.5px;
2562
2934
  }
2935
+ .error-friendly {
2936
+ font-size: 15px;
2937
+ line-height: 1.6;
2938
+ color: #e8e8e8;
2939
+ margin-bottom: 16px;
2940
+ }
2941
+ .error-friendly code {
2942
+ font-family: 'SF Mono', Menlo, Monaco, 'Courier New', monospace;
2943
+ font-size: 13px;
2944
+ background: #1a1a1a;
2945
+ border: 1px solid #444;
2946
+ border-radius: 4px;
2947
+ padding: 1px 5px;
2948
+ color: #ffb86b;
2949
+ }
2563
2950
  .error-message {
2564
2951
  background: #1a1a1a;
2565
2952
  border: 1px solid #444;
@@ -2637,25 +3024,24 @@ function generateErrorPage(error, context, cspNonce) {
2637
3024
  <div class="error-container">
2638
3025
  <div class="error-header">
2639
3026
  <span class="error-icon">\u26A0\uFE0F</span>
2640
- <span class="error-title">Render Error</span>
3027
+ <span class="error-title">${friendlyTitle}</span>
2641
3028
  </div>
2642
3029
  <div class="error-body">
2643
3030
  ${context ? `<div class="error-context">${escapeHtml(context)}</div>` : ""}
2644
- <div class="error-message">${errorMessage}</div>
2645
- ${errorStack ? `
3031
+ <p class="error-friendly">${friendlyMessage}</p>
2646
3032
  <div class="error-stack">
2647
3033
  <button class="stack-toggle" id="stackToggle">
2648
3034
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2649
3035
  <polyline points="9 18 15 12 9 6"></polyline>
2650
3036
  </svg>
2651
- <span>Show Stack Trace</span>
3037
+ <span>Show technical details</span>
2652
3038
  </button>
2653
- <div class="stack-content" id="stackContent">${errorStack}</div>
3039
+ <div class="stack-content" id="stackContent"><div class="error-message">${errorMessage}</div>${errorStack ? `
3040
+ ${errorStack}` : ""}</div>
2654
3041
  </div>
2655
- ` : ""}
2656
3042
  </div>
2657
3043
  <div class="error-footer">
2658
- <span class="error-hint">Check your component JavaScript for errors</span>
3044
+ <span class="error-hint">${friendlyHint}</span>
2659
3045
  <button class="copy-btn" id="copyBtn">
2660
3046
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2661
3047
  <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
@@ -2683,7 +3069,7 @@ function generateErrorPage(error, context, cspNonce) {
2683
3069
  stackToggle.addEventListener('click', function() {
2684
3070
  var isVisible = stackContent.classList.toggle('visible');
2685
3071
  stackToggle.classList.toggle('expanded', isVisible);
2686
- stackToggle.querySelector('span').textContent = isVisible ? 'Hide Stack Trace' : 'Show Stack Trace';
3072
+ stackToggle.querySelector('span').textContent = isVisible ? 'Hide technical details' : 'Show technical details';
2687
3073
  });
2688
3074
  }
2689
3075
 
@@ -2729,7 +3115,7 @@ function generateCspNonce() {
2729
3115
  async function handlePageRoute(url, context, req) {
2730
3116
  const { pageService, componentService, cmsService, injectLiveReload, isEditor, serverPort } = context;
2731
3117
  const pagePath = url.pathname;
2732
- const injectEditorAttrs = req?.headers.get(EDITOR_HEADER) === "1";
3118
+ const injectEditorAttrs = injectLiveReload === true || req?.headers.get(EDITOR_HEADER) === "1";
2733
3119
  const cspNonce = generateCspNonce();
2734
3120
  const i18nConfig = await loadI18nConfig();
2735
3121
  const { locale, pathWithoutLocale } = parseLocaleFromPath(pagePath, i18nConfig);
@@ -2849,7 +3235,7 @@ async function handlePageRoute(url, context, req) {
2849
3235
  });
2850
3236
  }
2851
3237
  }
2852
- const pageContent = pageService.getPage(lookupPath);
3238
+ const pageContent = context.draftPageStore?.get(lookupPath) ?? pageService.getPage(lookupPath);
2853
3239
  if (pageContent) {
2854
3240
  try {
2855
3241
  const pageData = parseJSON(pageContent);
@@ -3020,6 +3406,53 @@ async function handleRoutes(req, url, server, context) {
3020
3406
  logResponseTime(startTime, req);
3021
3407
  return void 0;
3022
3408
  }
3409
+ if (url.pathname === "/__draft-page" && req.method === "POST") {
3410
+ const corsHeaders = { "Access-Control-Allow-Origin": "*" };
3411
+ const store = context.draftPageStore;
3412
+ const ws = context.wsManager;
3413
+ if (!store || !ws || ws.getClientCount() === 0) {
3414
+ logResponseTime(startTime, req);
3415
+ return new Response(null, { status: 204, headers: corsHeaders });
3416
+ }
3417
+ try {
3418
+ const body = await req.json();
3419
+ if (typeof body.path === "string" && typeof body.content === "string" && body.path.startsWith("/")) {
3420
+ store.set(body.path, body.content);
3421
+ ws.broadcastUpdate(body.path);
3422
+ }
3423
+ } catch {
3424
+ }
3425
+ logResponseTime(startTime, req);
3426
+ return new Response(null, { status: 204, headers: corsHeaders });
3427
+ }
3428
+ if (url.pathname === "/__draft-page/clear" && req.method === "POST") {
3429
+ const corsHeaders = { "Access-Control-Allow-Origin": "*" };
3430
+ const store = context.draftPageStore;
3431
+ if (store) {
3432
+ try {
3433
+ const body = await req.json();
3434
+ if (typeof body.path === "string") {
3435
+ store.clear(body.path);
3436
+ context.wsManager?.broadcastUpdate(body.path);
3437
+ }
3438
+ } catch {
3439
+ }
3440
+ }
3441
+ logResponseTime(startTime, req);
3442
+ return new Response(null, { status: 204, headers: corsHeaders });
3443
+ }
3444
+ if (url.pathname === "/__draft-page/clients" && req.method === "GET") {
3445
+ const count = context.wsManager?.getClientCount() ?? 0;
3446
+ logResponseTime(startTime, req);
3447
+ return new Response(JSON.stringify({ count }), {
3448
+ status: 200,
3449
+ headers: {
3450
+ "Content-Type": "application/json",
3451
+ "Cache-Control": "no-store",
3452
+ "Access-Control-Allow-Origin": "*"
3453
+ }
3454
+ });
3455
+ }
3023
3456
  const cmsContext = cmsService && cmsProvider ? { cmsService, cmsProvider } : void 0;
3024
3457
  const apiResponse = await handleApiRoutes(
3025
3458
  req,
@@ -3081,6 +3514,13 @@ async function handleRoutes(req, url, server, context) {
3081
3514
  return response2;
3082
3515
  }
3083
3516
  }
3517
+ if (url.pathname.startsWith("/api/")) {
3518
+ logResponseTime(startTime, req);
3519
+ return new Response(
3520
+ JSON.stringify({ error: "Not Found", path: url.pathname }),
3521
+ { status: 404, headers: { "Content-Type": "application/json" } }
3522
+ );
3523
+ }
3084
3524
  if (url.pathname === "/" || url.pathname.startsWith("/") && !url.pathname.includes(".")) {
3085
3525
  const response2 = await handlePageRoute(url, context, req);
3086
3526
  logResponseTime(startTime, req);
@@ -3109,6 +3549,7 @@ async function createServer(config) {
3109
3549
  wsManager,
3110
3550
  cmsService,
3111
3551
  cmsProvider,
3552
+ draftPageStore,
3112
3553
  additionalRoutes = [],
3113
3554
  onWSMessage,
3114
3555
  injectLiveReload,
@@ -3124,7 +3565,9 @@ async function createServer(config) {
3124
3565
  cmsService,
3125
3566
  cmsProvider,
3126
3567
  injectLiveReload,
3127
- isEditor
3568
+ isEditor,
3569
+ wsManager,
3570
+ draftPageStore
3128
3571
  };
3129
3572
  let boundPort;
3130
3573
  let lastError;
@@ -3210,7 +3653,7 @@ async function createServer(config) {
3210
3653
  }
3211
3654
 
3212
3655
  // lib/server/providers/fileSystemPageProvider.ts
3213
- import { existsSync as existsSync5, readdirSync as readdirSync3, mkdirSync as mkdirSync3, rmdirSync as rmdirSync3 } from "fs";
3656
+ import { existsSync as existsSync4, readdirSync as readdirSync3, mkdirSync as mkdirSync3, rmdirSync as rmdirSync3 } from "fs";
3214
3657
  import { join as join4, dirname as dirname2 } from "path";
3215
3658
 
3216
3659
  // lib/shared/utils/fileUtils.ts
@@ -3219,7 +3662,7 @@ var stripExtension = (name) => {
3219
3662
  const lastDotIndex = name.lastIndexOf(".");
3220
3663
  return lastDotIndex > 0 ? name.substring(0, lastDotIndex) : name;
3221
3664
  };
3222
- var mapPageNameToPath3 = (pageName) => {
3665
+ var mapPageNameToPath2 = (pageName) => {
3223
3666
  return pageName === "index" ? "/" : `/${pageName}`;
3224
3667
  };
3225
3668
  var mapPathToPageName = (path2) => {
@@ -3244,10 +3687,10 @@ var FileSystemPageProvider = class {
3244
3687
  }
3245
3688
  async loadAll() {
3246
3689
  const pages = /* @__PURE__ */ new Map();
3247
- if (existsSync5(this.pagesDir)) {
3690
+ if (existsSync4(this.pagesDir)) {
3248
3691
  await this.scanDirectory(this.pagesDir, "", pages);
3249
3692
  }
3250
- if (this.templatesDir && existsSync5(this.templatesDir)) {
3693
+ if (this.templatesDir && existsSync4(this.templatesDir)) {
3251
3694
  await this.scanDirectory(this.templatesDir, "templates", pages);
3252
3695
  }
3253
3696
  return pages;
@@ -3266,7 +3709,7 @@ var FileSystemPageProvider = class {
3266
3709
  const pageName = prefix ? `${prefix}/${stripExtension(entry.name)}` : stripExtension(entry.name);
3267
3710
  const content = await loadJSONFile2(join4(dir, entry.name));
3268
3711
  if (content) {
3269
- const pagePath = mapPageNameToPath3(pageName);
3712
+ const pagePath = mapPageNameToPath2(pageName);
3270
3713
  pages.set(pagePath, content);
3271
3714
  }
3272
3715
  }),
@@ -3307,7 +3750,7 @@ var FileSystemPageProvider = class {
3307
3750
  const { writeFile: writeFile2 } = await import("fs/promises");
3308
3751
  const filePath = this.resolveFilePath(path2);
3309
3752
  const dir = dirname2(filePath);
3310
- if (!existsSync5(dir)) {
3753
+ if (!existsSync4(dir)) {
3311
3754
  mkdirSync3(dir, { recursive: true });
3312
3755
  }
3313
3756
  await writeFile2(filePath, content, "utf-8");
@@ -3316,7 +3759,7 @@ var FileSystemPageProvider = class {
3316
3759
  const { unlink } = await import("fs/promises");
3317
3760
  const filePath = this.resolveFilePath(path2);
3318
3761
  const rootDir = this.resolveRootDir(path2);
3319
- if (existsSync5(filePath)) {
3762
+ if (existsSync4(filePath)) {
3320
3763
  await unlink(filePath);
3321
3764
  let dir = dirname2(filePath);
3322
3765
  while (dir !== rootDir) {
@@ -3336,7 +3779,13 @@ var FileSystemPageProvider = class {
3336
3779
  }
3337
3780
  async exists(path2) {
3338
3781
  const filePath = this.resolveFilePath(path2);
3339
- return existsSync5(filePath);
3782
+ return existsSync4(filePath);
3783
+ }
3784
+ baseDir() {
3785
+ return this.pagesDir;
3786
+ }
3787
+ extension() {
3788
+ return ".json";
3340
3789
  }
3341
3790
  };
3342
3791
 
@@ -3369,4 +3818,4 @@ export {
3369
3818
  createServer,
3370
3819
  FileSystemPageProvider
3371
3820
  };
3372
- //# sourceMappingURL=chunk-A725KYFK.js.map
3821
+ //# sourceMappingURL=chunk-R6XHAFBF.js.map