meno-core 1.1.3 → 1.1.5

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 (57) hide show
  1. package/dist/chunks/{chunk-3JXK2QFU.js → chunk-H6EOTPJA.js} +2 -2
  2. package/dist/chunks/{chunk-LOZL5HOF.js → chunk-RLBV2R6J.js} +603 -17
  3. package/dist/chunks/chunk-RLBV2R6J.js.map +7 -0
  4. package/dist/chunks/{chunk-FQBIC2OB.js → chunk-YMBUSHII.js} +1 -1
  5. package/dist/chunks/{chunk-FQBIC2OB.js.map → chunk-YMBUSHII.js.map} +1 -1
  6. package/dist/lib/client/index.js +46 -28
  7. package/dist/lib/client/index.js.map +2 -2
  8. package/dist/lib/server/index.js +1304 -241
  9. package/dist/lib/server/index.js.map +4 -4
  10. package/dist/lib/shared/index.js +4 -2
  11. package/dist/lib/shared/index.js.map +1 -1
  12. package/lib/client/core/ComponentBuilder.test.ts +32 -0
  13. package/lib/client/core/ComponentBuilder.ts +30 -0
  14. package/lib/client/theme.test.ts +2 -2
  15. package/lib/client/theme.ts +28 -26
  16. package/lib/server/createServer.ts +5 -1
  17. package/lib/server/cssAudit.test.ts +270 -0
  18. package/lib/server/cssAudit.ts +815 -0
  19. package/lib/server/deMirror.test.ts +61 -0
  20. package/lib/server/deMirror.ts +317 -0
  21. package/lib/server/fileWatcher.test.ts +113 -0
  22. package/lib/server/fileWatcher.ts +42 -1
  23. package/lib/server/index.ts +19 -0
  24. package/lib/server/routes/api/core-routes.ts +18 -0
  25. package/lib/server/routes/api/cssAudit.test.ts +101 -0
  26. package/lib/server/routes/api/cssAudit.ts +138 -0
  27. package/lib/server/routes/api/deMirror.test.ts +105 -0
  28. package/lib/server/routes/api/deMirror.ts +127 -0
  29. package/lib/server/routes/api/pages.ts +2 -2
  30. package/lib/server/services/componentService.test.ts +43 -0
  31. package/lib/server/services/componentService.ts +49 -12
  32. package/lib/server/services/configService.ts +0 -5
  33. package/lib/server/services/pageService.test.ts +15 -0
  34. package/lib/server/services/pageService.ts +44 -22
  35. package/lib/server/ssr/htmlGenerator.test.ts +29 -1
  36. package/lib/server/ssr/htmlGenerator.ts +86 -14
  37. package/lib/server/themeCssCodec.test.ts +67 -0
  38. package/lib/server/themeCssCodec.ts +67 -5
  39. package/lib/server/webflow/templateWrapper.ts +54 -0
  40. package/lib/shared/cssGeneration.test.ts +64 -0
  41. package/lib/shared/cssGeneration.ts +6 -1
  42. package/lib/shared/interfaces/contentProvider.ts +9 -0
  43. package/lib/shared/tailwindThemeScale.ts +1 -0
  44. package/lib/shared/types/cms.ts +1 -0
  45. package/lib/shared/types/comment.ts +9 -3
  46. package/lib/shared/utilityClassConfig.ts +471 -10
  47. package/lib/shared/utilityClassMapper.test.ts +173 -2
  48. package/lib/shared/utilityClassMapper.ts +37 -0
  49. package/lib/shared/utilityClassNames.ts +164 -0
  50. package/lib/shared/utils/fileUtils.ts +11 -0
  51. package/lib/shared/validation/schemas.ts +1 -0
  52. package/lib/shared/viewportUnits.test.ts +34 -1
  53. package/lib/shared/viewportUnits.ts +43 -0
  54. package/package.json +3 -1
  55. package/vite.config.ts +4 -1
  56. package/dist/chunks/chunk-LOZL5HOF.js.map +0 -7
  57. /package/dist/chunks/{chunk-3JXK2QFU.js.map → chunk-H6EOTPJA.js.map} +0 -0
@@ -20,7 +20,7 @@ import {
20
20
  resolveSlugToPageId,
21
21
  rewriteComponentRefs,
22
22
  translatePath
23
- } from "../../chunks/chunk-FQBIC2OB.js";
23
+ } from "../../chunks/chunk-YMBUSHII.js";
24
24
  import {
25
25
  syncNetlifyLocale404Block
26
26
  } from "../../chunks/chunk-2AR55GYH.js";
@@ -33,7 +33,7 @@ import {
33
33
  processStructure,
34
34
  resolveHtmlMapping,
35
35
  skipEmptyTemplateAttributes
36
- } from "../../chunks/chunk-3JXK2QFU.js";
36
+ } from "../../chunks/chunk-H6EOTPJA.js";
37
37
  import {
38
38
  DEFAULT_BREAKPOINTS,
39
39
  DEFAULT_FLUID_RANGE,
@@ -86,13 +86,14 @@ import {
86
86
  resolveTemplateRawValue,
87
87
  responsiveStylesToClasses,
88
88
  rewriteViewportUnits,
89
+ rewriteViewportUnitsInStylesheet,
89
90
  scalePropertyValue,
90
91
  singularize,
91
92
  toFriendlyError,
92
93
  validateCMSDraftItem,
93
94
  validateCMSItem,
94
95
  validateComponentDefinition
95
- } from "../../chunks/chunk-LOZL5HOF.js";
96
+ } from "../../chunks/chunk-RLBV2R6J.js";
96
97
  import {
97
98
  DEFAULT_I18N_CONFIG,
98
99
  buildLocalizedPath,
@@ -165,10 +166,10 @@ var WebSocketManager = class {
165
166
  /**
166
167
  * Broadcast update notification
167
168
  */
168
- broadcastUpdate(path3) {
169
+ broadcastUpdate(path5) {
169
170
  this.broadcast({
170
171
  type: "hmr:update",
171
- path: path3 || "all"
172
+ path: path5 || "all"
172
173
  });
173
174
  }
174
175
  /**
@@ -1093,9 +1094,6 @@ var ConfigService = class {
1093
1094
  }
1094
1095
  return this.config.customCode;
1095
1096
  }
1096
- getShowMenoBadge() {
1097
- return this.config?.showMenoBadge === true;
1098
- }
1099
1097
  getImageFormat() {
1100
1098
  if (this.config?.imageFormat === "avif") return "avif";
1101
1099
  return "webp";
@@ -1518,8 +1516,8 @@ function cachedJsonResponse(req, body, options = {}) {
1518
1516
  }
1519
1517
 
1520
1518
  // lib/server/routes/api/pages.ts
1521
- function handlePagesRoute(pageService) {
1522
- const pages = pageService.getAllPagesWithInfo();
1519
+ async function handlePagesRoute(pageService) {
1520
+ const pages = await pageService.getAllPagesWithInfo();
1523
1521
  return jsonResponse({ pages });
1524
1522
  }
1525
1523
  function handlePageDataRoute(url, pageService) {
@@ -1641,11 +1639,11 @@ function handleComponentUsageRoute(pageService, componentService) {
1641
1639
  }
1642
1640
  }
1643
1641
  const pagePaths = pageService.getAllPagePaths();
1644
- for (const path3 of pagePaths) {
1645
- const pageData = pageService.getPageData(path3);
1642
+ for (const path5 of pagePaths) {
1643
+ const pageData = pageService.getPageData(path5);
1646
1644
  if (pageData && "root" in pageData && pageData.root) {
1647
- const name = path3 === "/" ? "index" : path3.replace(/^\//, "").replace(/\//g, "-");
1648
- walkTree(pageData.root, name, path3, "page");
1645
+ const name = path5 === "/" ? "index" : path5.replace(/^\//, "").replace(/\//g, "-");
1646
+ walkTree(pageData.root, name, path5, "page");
1649
1647
  }
1650
1648
  }
1651
1649
  const allComponents = componentService.getAllComponents();
@@ -1755,9 +1753,24 @@ function sectionLabelForGroup(group) {
1755
1753
  if (!group || group === "other") return OTHER_LABEL;
1756
1754
  return VARIABLE_GROUPS.find((g) => g.value === group)?.label ?? OTHER_LABEL;
1757
1755
  }
1758
- var LABEL_TO_GROUP = new Map(VARIABLE_GROUPS.map((g) => [g.label.toLowerCase(), g.value]));
1756
+ function normalizeSectionLabel(s) {
1757
+ return s.trim().toLowerCase().replace(/[-–—]+/g, " ").replace(/\s+/g, " ").trim();
1758
+ }
1759
+ var NORMALIZED_LABEL_TO_GROUP = new Map(
1760
+ VARIABLE_GROUPS.map((g) => [normalizeSectionLabel(g.label), g.value])
1761
+ );
1762
+ var PREFIX_LABELS = VARIABLE_GROUPS.filter((g) => g.value !== "other").map((g) => ({ norm: normalizeSectionLabel(g.label), group: g.value })).sort((a, b) => b.norm.length - a.norm.length);
1759
1763
  function groupForSectionLabel(label) {
1760
- return LABEL_TO_GROUP.get(label.trim().toLowerCase()) ?? null;
1764
+ const norm = normalizeSectionLabel(label);
1765
+ const exact = NORMALIZED_LABEL_TO_GROUP.get(norm);
1766
+ if (exact) return exact;
1767
+ for (const { norm: labelNorm, group } of PREFIX_LABELS) {
1768
+ if (norm.startsWith(labelNorm)) {
1769
+ const next = norm.charAt(labelNorm.length);
1770
+ if (next === "" || !/[a-z0-9]/.test(next)) return group;
1771
+ }
1772
+ }
1773
+ return null;
1761
1774
  }
1762
1775
  function isColorValue(value) {
1763
1776
  const v = value.trim().toLowerCase();
@@ -1766,6 +1779,11 @@ function isColorValue(value) {
1766
1779
  if (v === "transparent" || v === "currentcolor") return true;
1767
1780
  return false;
1768
1781
  }
1782
+ var NAME_HINT_GROUPS = [
1783
+ { needle: /(^|[-_])tracking([-_]|$)/, group: "letter-spacing" },
1784
+ { needle: /(^|[-_])leading([-_]|$)/, group: "line-height" },
1785
+ { needle: /(^|[-_])(radius|rounded)([-_]|$)/, group: "border-radius" }
1786
+ ];
1769
1787
  function inferGroup(cssVar, value) {
1770
1788
  const v = value.trim().toLowerCase();
1771
1789
  if (/^\d+(\.\d+)?m?s$/.test(v)) return "duration";
@@ -1776,6 +1794,9 @@ function inferGroup(cssVar, value) {
1776
1794
  }
1777
1795
  if (/^\d*\.\d+$/.test(v) || /^[1-9](\.\d+)?$/.test(v)) return "line-height";
1778
1796
  const name = cssVar.replace(/^--/, "").toLowerCase();
1797
+ for (const { needle, group } of NAME_HINT_GROUPS) {
1798
+ if (needle.test(name)) return group;
1799
+ }
1779
1800
  for (const g of VARIABLE_GROUPS) {
1780
1801
  if (g.value !== "other" && name.includes(g.value)) return g.value;
1781
1802
  }
@@ -2057,8 +2078,7 @@ function parseThemeCss(css, breakpoints) {
2057
2078
  const dn = inner.match(/^colors\s*:\s*([\w-]+)/i);
2058
2079
  if (dn) defaultName = dn[1];
2059
2080
  } else {
2060
- const g = groupForSectionLabel(inner);
2061
- if (g) section = g;
2081
+ section = groupForSectionLabel(inner);
2062
2082
  }
2063
2083
  continue;
2064
2084
  }
@@ -2134,9 +2154,9 @@ function enqueue(fn) {
2134
2154
  );
2135
2155
  return run;
2136
2156
  }
2137
- async function readJsonSafe(path3) {
2157
+ async function readJsonSafe(path5) {
2138
2158
  try {
2139
- return JSON.parse(await readTextFile(path3));
2159
+ return JSON.parse(await readTextFile(path5));
2140
2160
  } catch {
2141
2161
  return null;
2142
2162
  }
@@ -2148,9 +2168,9 @@ function emptyModel() {
2148
2168
  return { themes: { default: "default", themes: { default: { colors: {} } } }, variables: [], raw: [] };
2149
2169
  }
2150
2170
  async function readModel(root) {
2151
- const path3 = themeCssPath(root);
2152
- if (!await fileExists(path3)) return emptyModel();
2153
- return parseThemeCss(await readTextFile(path3), configService.getBreakpoints());
2171
+ const path5 = themeCssPath(root);
2172
+ if (!await fileExists(path5)) return emptyModel();
2173
+ return parseThemeCss(await readTextFile(path5), configService.getBreakpoints());
2154
2174
  }
2155
2175
  async function writeModel(root, model) {
2156
2176
  const css = serializeThemeCss(model, scaleConfig());
@@ -2880,8 +2900,880 @@ async function handleItemRoute(collection, slug, cmsService) {
2880
2900
  return jsonResponse({ item: match.item });
2881
2901
  }
2882
2902
 
2883
- // lib/server/routes/api/functions.ts
2903
+ // lib/server/routes/api/cssAudit.ts
2904
+ import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync } from "node:fs";
2884
2905
  import path from "node:path";
2906
+
2907
+ // lib/server/cssAudit.ts
2908
+ import * as csstree from "css-tree";
2909
+ var DEFAULT_UTILITY_HINTS = [
2910
+ // Spacing / sizing
2911
+ "^(margin|padding|gap)(-|$)",
2912
+ "^no-(padding|margin|overflow)",
2913
+ "(^|-)auto-margin$",
2914
+ "^(max|min)-(w|h|width|height)",
2915
+ "^(w|h)-[0-9]",
2916
+ "^size-[0-9]",
2917
+ "^(small|medium|large)(-|$)",
2918
+ // Layout / display
2919
+ "^align(-|$)",
2920
+ "^justify(-|$)",
2921
+ "^(grid|col|flex)-",
2922
+ "^container(-|$)",
2923
+ "(^|-)(overflow|opacity|relative|absolute|sticky|inline|centered)(-|$)",
2924
+ "^(hide|show|hidden|visible)$",
2925
+ // State / Webflow combo classes
2926
+ "^is--?",
2927
+ "^has--?",
2928
+ "^on-h[0-9]",
2929
+ "^type-[0-9]+$",
2930
+ "(^|-)active$",
2931
+ // Colors
2932
+ "(^|-)(white|black|dark|light|primary|secondary|grey|gray)(-|$)",
2933
+ // Cross-cutting UI
2934
+ "(^|-)arrow",
2935
+ // Relume / newer-Webflow utility families (text-size-*, heading-style-*, spacer-*, max-width-*)
2936
+ "^text-(size|align|color|weight|style)",
2937
+ "^heading-style",
2938
+ "^(spacer|max-width|background-color)(-|$)"
2939
+ ];
2940
+ var DEFAULT_WEBFLOW_AUTO = [
2941
+ "^(div-block|text-block|link-block|bold-text|block-quote|rich-text-block|html-embed)(-\\d+)?$",
2942
+ "^(heading|paragraph|image|list|list-item|figure|grid|columns?|field-label|text-field)-\\d+$"
2943
+ ];
2944
+ var CLASS_TOKEN_RE = /\.(-?[_a-zA-Z][\w-]*)/g;
2945
+ var FUNCTIONAL_PSEUDO_RE = /:(not|is|where|has|matches)\([^()]*\)/gi;
2946
+ function emptyBucket() {
2947
+ return { classes: 0, rules: 0, declarations: 0, approxBytes: 0 };
2948
+ }
2949
+ function classTokens(selector) {
2950
+ const out = [];
2951
+ let m;
2952
+ CLASS_TOKEN_RE.lastIndex = 0;
2953
+ while ((m = CLASS_TOKEN_RE.exec(selector)) !== null) {
2954
+ if (m[1]) out.push(m[1]);
2955
+ }
2956
+ return out;
2957
+ }
2958
+ function sectionPrefix(name) {
2959
+ const seg = name.split(/[-_]/)[0] ?? name;
2960
+ const stripped = seg.replace(/\d+$/, "");
2961
+ return stripped.length > 0 ? stripped : seg;
2962
+ }
2963
+ function commonPrefixLength(a, b) {
2964
+ const max = Math.min(a.length, b.length);
2965
+ let i = 0;
2966
+ while (i < max && a[i] === b[i]) i++;
2967
+ return i;
2968
+ }
2969
+ function editDistance(a, b) {
2970
+ const m = a.length;
2971
+ const n = b.length;
2972
+ if (m === 0) return n;
2973
+ if (n === 0) return m;
2974
+ let prev = Array.from({ length: n + 1 }, (_, i) => i);
2975
+ let curr = new Array(n + 1).fill(0);
2976
+ for (let i = 1; i <= m; i++) {
2977
+ curr[0] = i;
2978
+ for (let j = 1; j <= n; j++) {
2979
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
2980
+ curr[j] = Math.min((prev[j] ?? 0) + 1, (curr[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
2981
+ }
2982
+ [prev, curr] = [curr, prev];
2983
+ }
2984
+ return prev[n] ?? 0;
2985
+ }
2986
+ function nearestKeptPrefix(prefix, kept) {
2987
+ let best = null;
2988
+ let bestShared = -1;
2989
+ for (const candidate of kept) {
2990
+ if (Math.abs(candidate.length - prefix.length) > 1) continue;
2991
+ const shared = commonPrefixLength(prefix, candidate);
2992
+ if (shared < 2) continue;
2993
+ if (editDistance(prefix, candidate) !== 1) continue;
2994
+ if (shared > bestShared) {
2995
+ bestShared = shared;
2996
+ best = candidate;
2997
+ }
2998
+ }
2999
+ return best;
3000
+ }
3001
+ function analyzeSelector(selector) {
3002
+ const cleaned = selector.replace(FUNCTIONAL_PSEUDO_RE, " ");
3003
+ const segments = cleaned.split(/\s*[>+~]\s*|\s+/).filter((s) => s.trim().length > 0);
3004
+ const subject = segments[segments.length - 1] ?? cleaned;
3005
+ const subjectClasses = classTokens(subject);
3006
+ const allClasses = classTokens(selector);
3007
+ const compound = segments.length > 1 || subjectClasses.length > 1;
3008
+ return { allClasses, subjectClasses, compound };
3009
+ }
3010
+ function countDeclarations(block) {
3011
+ let count = 0;
3012
+ if (block && block.type === "Block") {
3013
+ block.children.forEach((child) => {
3014
+ if (child.type === "Declaration") count++;
3015
+ });
3016
+ }
3017
+ return count;
3018
+ }
3019
+ function extractRules(css) {
3020
+ const ast = csstree.parse(css, { parseValue: false, parseCustomProperty: false });
3021
+ const out = [];
3022
+ const visit = (container, media, insideSkippedAtrule) => {
3023
+ const children = "children" in container ? container.children : null;
3024
+ if (!children) return;
3025
+ children.forEach((node) => {
3026
+ if (node.type === "Atrule") {
3027
+ const name = node.name.toLowerCase();
3028
+ if (name === "media") {
3029
+ const prelude = node.prelude ? csstree.generate(node.prelude) : "";
3030
+ if (node.block) visit(node.block, prelude, false);
3031
+ } else if (name === "supports") {
3032
+ if (node.block) visit(node.block, media, false);
3033
+ } else if (node.block) {
3034
+ visit(node.block, media, true);
3035
+ }
3036
+ return;
3037
+ }
3038
+ if (node.type !== "Rule" || insideSkippedAtrule) return;
3039
+ if (node.prelude?.type !== "SelectorList") return;
3040
+ const decls = countDeclarations(node.block);
3041
+ const ruleBytes = csstree.generate(node).length;
3042
+ const classSelectors = [];
3043
+ node.prelude.children.forEach((selectorNode) => {
3044
+ const text = csstree.generate(selectorNode);
3045
+ const shape = analyzeSelector(text);
3046
+ if (shape.allClasses.length > 0) classSelectors.push({ text, shape });
3047
+ });
3048
+ if (classSelectors.length === 0) return;
3049
+ const perBytes = Math.max(1, Math.round(ruleBytes / classSelectors.length));
3050
+ for (const { text, shape } of classSelectors) {
3051
+ out.push({
3052
+ selector: text,
3053
+ media,
3054
+ classes: shape.allClasses,
3055
+ subjectClasses: shape.subjectClasses,
3056
+ declarations: decls,
3057
+ bytes: perBytes,
3058
+ compound: shape.compound
3059
+ });
3060
+ }
3061
+ });
3062
+ };
3063
+ visit(ast, null, false);
3064
+ return out;
3065
+ }
3066
+ function auditStylesheet(input) {
3067
+ const { css, pages, options = {} } = input;
3068
+ const selectorSampleLimit = options.selectorSampleLimit ?? 5;
3069
+ const flagLimit = options.flagLimit ?? 200;
3070
+ const minSectionClasses = options.minSectionClasses ?? 3;
3071
+ const miscBucketName = options.miscBucketName ?? "misc";
3072
+ const mergeTypos = options.mergeTypos ?? true;
3073
+ const utilityHints = (options.utilityHints ?? DEFAULT_UTILITY_HINTS).map((s) => new RegExp(s));
3074
+ const webflowAutoPatterns = (options.webflowAuto ?? DEFAULT_WEBFLOW_AUTO).map((s) => new RegExp(s));
3075
+ const pageCount = pages.length;
3076
+ const sharedMinPages = options.sharedMinPages ?? Math.max(2, Math.ceil(pageCount / 2));
3077
+ const pageUseCount = /* @__PURE__ */ new Map();
3078
+ for (const page of pages) {
3079
+ for (const cls of new Set(page.classes)) {
3080
+ pageUseCount.set(cls, (pageUseCount.get(cls) ?? 0) + 1);
3081
+ }
3082
+ }
3083
+ const ast = csstree.parse(css, { parseValue: false, parseCustomProperty: false });
3084
+ const classMap = /* @__PURE__ */ new Map();
3085
+ const rules = [];
3086
+ const breakpoints = [];
3087
+ const seenMedia = /* @__PURE__ */ new Set();
3088
+ const base = emptyBucket();
3089
+ let totalRules = 0;
3090
+ let totalSelectors = 0;
3091
+ let totalDeclarations = 0;
3092
+ const accumFor = (name) => {
3093
+ let a = classMap.get(name);
3094
+ if (!a) {
3095
+ a = { name, ruleCount: 0, declarations: 0, breakpoints: /* @__PURE__ */ new Set(), selectors: [], ownsSimple: false };
3096
+ classMap.set(name, a);
3097
+ }
3098
+ return a;
3099
+ };
3100
+ const visit = (container, media, insideSkippedAtrule) => {
3101
+ const children = "children" in container ? container.children : null;
3102
+ if (!children) return;
3103
+ children.forEach((node) => {
3104
+ if (node.type === "Atrule") {
3105
+ const name = node.name.toLowerCase();
3106
+ if (name === "media") {
3107
+ const prelude = node.prelude ? csstree.generate(node.prelude) : "";
3108
+ if (!seenMedia.has(prelude)) {
3109
+ seenMedia.add(prelude);
3110
+ breakpoints.push(prelude);
3111
+ }
3112
+ if (node.block) visit(node.block, prelude, false);
3113
+ } else if (name === "supports") {
3114
+ if (node.block) visit(node.block, media, false);
3115
+ } else if (node.block) {
3116
+ visit(node.block, media, true);
3117
+ }
3118
+ return;
3119
+ }
3120
+ if (node.type !== "Rule" || insideSkippedAtrule) return;
3121
+ if (node.prelude?.type !== "SelectorList") return;
3122
+ const ruleBytes = csstree.generate(node).length;
3123
+ const decls = countDeclarations(node.block);
3124
+ totalDeclarations += decls;
3125
+ totalRules += 1;
3126
+ const ruleAllClasses = /* @__PURE__ */ new Set();
3127
+ const ruleSubjectClasses = /* @__PURE__ */ new Set();
3128
+ let hasAnyClass = false;
3129
+ node.prelude.children.forEach((selectorNode) => {
3130
+ totalSelectors += 1;
3131
+ const selectorText = csstree.generate(selectorNode);
3132
+ const shape = analyzeSelector(selectorText);
3133
+ if (shape.allClasses.length === 0) return;
3134
+ hasAnyClass = true;
3135
+ for (const c of shape.allClasses) ruleAllClasses.add(c);
3136
+ for (const c of shape.subjectClasses) ruleSubjectClasses.add(c);
3137
+ const subjectSet = new Set(shape.subjectClasses);
3138
+ for (const cls of shape.allClasses) {
3139
+ const a = accumFor(cls);
3140
+ a.ruleCount += 1;
3141
+ a.declarations += decls;
3142
+ a.breakpoints.add(media ?? "");
3143
+ if (a.selectors.length < selectorSampleLimit && !a.selectors.includes(selectorText)) {
3144
+ a.selectors.push(selectorText);
3145
+ }
3146
+ if (subjectSet.size === 1 && subjectSet.has(cls)) a.ownsSimple = true;
3147
+ }
3148
+ });
3149
+ if (!hasAnyClass) {
3150
+ base.rules += 1;
3151
+ base.declarations += decls;
3152
+ base.approxBytes += ruleBytes;
3153
+ return;
3154
+ }
3155
+ rules.push({
3156
+ media,
3157
+ bytes: ruleBytes,
3158
+ declarations: decls,
3159
+ classes: [...ruleAllClasses],
3160
+ subjectClasses: [...ruleSubjectClasses]
3161
+ });
3162
+ });
3163
+ };
3164
+ visit(ast, null, false);
3165
+ const prelim = /* @__PURE__ */ new Map();
3166
+ const sectionSizes = /* @__PURE__ */ new Map();
3167
+ for (const a of classMap.values()) {
3168
+ const pagesUsedIn = pageUseCount.get(a.name) ?? 0;
3169
+ let category;
3170
+ let bucket;
3171
+ if (/^w-/.test(a.name)) {
3172
+ category = "vendor";
3173
+ bucket = "vendor";
3174
+ } else if (webflowAutoPatterns.some((re) => re.test(a.name))) {
3175
+ category = "webflow-auto";
3176
+ bucket = "webflow-auto";
3177
+ } else if (utilityHints.some((re) => re.test(a.name)) || pagesUsedIn >= sharedMinPages) {
3178
+ category = "shared";
3179
+ bucket = "shared";
3180
+ } else {
3181
+ category = "section";
3182
+ bucket = sectionPrefix(a.name);
3183
+ sectionSizes.set(bucket, (sectionSizes.get(bucket) ?? 0) + 1);
3184
+ }
3185
+ prelim.set(a.name, { category, bucket, pagesUsedIn, accum: a });
3186
+ }
3187
+ const keptSections = /* @__PURE__ */ new Set();
3188
+ for (const [prefix, count] of sectionSizes) {
3189
+ if (count >= minSectionClasses) keptSections.add(prefix);
3190
+ }
3191
+ const sectionRemap = /* @__PURE__ */ new Map();
3192
+ for (const [prefix, count] of sectionSizes) {
3193
+ if (keptSections.has(prefix) || count === 0) continue;
3194
+ const near = mergeTypos && prefix.length >= 4 ? nearestKeptPrefix(prefix, keptSections) : null;
3195
+ sectionRemap.set(prefix, near ? { bucket: near, reason: "typo" } : { bucket: miscBucketName, reason: "small" });
3196
+ }
3197
+ const foldedAgg = /* @__PURE__ */ new Map();
3198
+ const classified = /* @__PURE__ */ new Map();
3199
+ const classes = [];
3200
+ for (const [name, p] of prelim) {
3201
+ let bucket = p.bucket;
3202
+ if (p.category === "section") {
3203
+ const remap = sectionRemap.get(p.bucket);
3204
+ if (remap) {
3205
+ bucket = remap.bucket;
3206
+ const key = `${p.bucket}\u2192${remap.bucket}`;
3207
+ const agg = foldedAgg.get(key) ?? { from: p.bucket, into: remap.bucket, reason: remap.reason, classes: 0 };
3208
+ agg.classes += 1;
3209
+ foldedAgg.set(key, agg);
3210
+ }
3211
+ }
3212
+ classified.set(name, { category: p.category, bucket });
3213
+ classes.push({
3214
+ name,
3215
+ category: p.category,
3216
+ proposedBucket: bucket,
3217
+ ruleCount: p.accum.ruleCount,
3218
+ declarations: p.accum.declarations,
3219
+ breakpoints: [...p.accum.breakpoints].sort(),
3220
+ pagesUsedIn: p.pagesUsedIn,
3221
+ selectors: p.accum.selectors,
3222
+ compoundOnly: !p.accum.ownsSimple
3223
+ });
3224
+ }
3225
+ classes.sort((x, y) => y.declarations - x.declarations || x.name.localeCompare(y.name));
3226
+ const foldedSections = [...foldedAgg.values()].map(({ from, into, classes: c, reason }) => ({ from, into, classes: c, reason })).sort((a, b) => b.classes - a.classes || a.from.localeCompare(b.from));
3227
+ const vendor = emptyBucket();
3228
+ const webflowAuto = emptyBucket();
3229
+ const shared = emptyBucket();
3230
+ const sections = {};
3231
+ const crossBoundary = [];
3232
+ const bucketStat = (bucket) => {
3233
+ if (bucket === "vendor") return vendor;
3234
+ if (bucket === "webflow-auto") return webflowAuto;
3235
+ if (bucket === "shared") return shared;
3236
+ let stat = sections[bucket];
3237
+ if (!stat) {
3238
+ stat = emptyBucket();
3239
+ sections[bucket] = stat;
3240
+ }
3241
+ return stat;
3242
+ };
3243
+ for (const rule of rules) {
3244
+ const ownerPool = rule.subjectClasses.length > 0 ? rule.subjectClasses : rule.classes;
3245
+ let owner = "base";
3246
+ let bestRank = -1;
3247
+ const sectionBucketsTouched = /* @__PURE__ */ new Set();
3248
+ for (const cls of rule.classes) {
3249
+ const c = classified.get(cls);
3250
+ if (c?.category === "section" && c.bucket !== miscBucketName) sectionBucketsTouched.add(c.bucket);
3251
+ }
3252
+ for (const cls of ownerPool) {
3253
+ const c = classified.get(cls);
3254
+ if (!c) continue;
3255
+ const rank = c.category === "section" ? 4 : c.category === "shared" ? 3 : c.category === "webflow-auto" ? 2 : 1;
3256
+ if (rank > bestRank) {
3257
+ bestRank = rank;
3258
+ owner = c.bucket;
3259
+ }
3260
+ }
3261
+ const stat = bucketStat(owner);
3262
+ stat.rules += 1;
3263
+ stat.declarations += rule.declarations;
3264
+ stat.approxBytes += rule.bytes;
3265
+ if (sectionBucketsTouched.size >= 2 && crossBoundary.length < flagLimit) {
3266
+ crossBoundary.push({
3267
+ selector: rule.classes.map((c) => `.${c}`).join(""),
3268
+ media: rule.media,
3269
+ classes: rule.classes,
3270
+ buckets: [...sectionBucketsTouched].sort()
3271
+ });
3272
+ }
3273
+ }
3274
+ for (const info of classes) {
3275
+ bucketStat(info.proposedBucket).classes += 1;
3276
+ }
3277
+ const fuzzyMiddle = [];
3278
+ const deadClasses = [];
3279
+ for (const info of classes) {
3280
+ if (info.category === "section" && info.pagesUsedIn >= 2 && info.pagesUsedIn < sharedMinPages) {
3281
+ fuzzyMiddle.push(info.name);
3282
+ }
3283
+ if (info.category !== "vendor" && info.pagesUsedIn === 0) {
3284
+ deadClasses.push(info.name);
3285
+ }
3286
+ }
3287
+ const pageOnlyClasses = [];
3288
+ for (const cls of pageUseCount.keys()) {
3289
+ if (!classMap.has(cls)) pageOnlyClasses.push(cls);
3290
+ }
3291
+ pageOnlyClasses.sort();
3292
+ deadClasses.sort();
3293
+ return {
3294
+ source: {
3295
+ bytes: css.length,
3296
+ rules: totalRules,
3297
+ selectors: totalSelectors,
3298
+ declarations: totalDeclarations,
3299
+ pageCount,
3300
+ sharedMinPages
3301
+ },
3302
+ breakpoints,
3303
+ buckets: { base, vendor, webflowAuto, shared, sections },
3304
+ classes,
3305
+ flags: {
3306
+ crossBoundarySelectors: crossBoundary,
3307
+ fuzzyMiddle: fuzzyMiddle.slice(0, flagLimit),
3308
+ deadClasses: deadClasses.slice(0, flagLimit),
3309
+ pageOnlyClasses: pageOnlyClasses.slice(0, flagLimit),
3310
+ foldedSections
3311
+ }
3312
+ };
3313
+ }
3314
+ function collectClassesFromPageData(pageData) {
3315
+ const out = /* @__PURE__ */ new Set();
3316
+ const walk = (value) => {
3317
+ if (!value) return;
3318
+ if (Array.isArray(value)) {
3319
+ for (const item of value) walk(item);
3320
+ return;
3321
+ }
3322
+ if (typeof value === "object") {
3323
+ for (const [key, v] of Object.entries(value)) {
3324
+ if ((key === "class" || key === "className") && typeof v === "string") {
3325
+ for (const token of v.split(/\s+/)) {
3326
+ if (token) out.add(token);
3327
+ }
3328
+ } else {
3329
+ walk(v);
3330
+ }
3331
+ }
3332
+ }
3333
+ };
3334
+ walk(pageData);
3335
+ return [...out];
3336
+ }
3337
+ function renderAuditMarkdown(report) {
3338
+ const { source, buckets, breakpoints, flags } = report;
3339
+ const kb = (n) => `${(n / 1024).toFixed(1)} KB`;
3340
+ const lines = [];
3341
+ lines.push("# CSS audit");
3342
+ lines.push("");
3343
+ lines.push(
3344
+ `**${kb(source.bytes)}** \xB7 ${source.rules} rules \xB7 ${source.selectors} selectors \xB7 ${source.declarations} declarations \xB7 ${source.pageCount} pages \xB7 shared threshold = ${source.sharedMinPages} pages`
3345
+ );
3346
+ lines.push("");
3347
+ lines.push(`Breakpoints: ${breakpoints.length ? breakpoints.map((b) => `\`${b}\``).join(", ") : "(none)"}`);
3348
+ lines.push("");
3349
+ lines.push("## Proposed buckets");
3350
+ lines.push("");
3351
+ lines.push("| bucket | classes | rules | declarations | ~size |");
3352
+ lines.push("| --- | --- | --- | --- | --- |");
3353
+ const row = (name, b) => `| ${name} | ${b.classes} | ${b.rules} | ${b.declarations} | ${kb(b.approxBytes)} |`;
3354
+ lines.push(row("base (reset/typography)", buckets.base));
3355
+ lines.push(row("vendor (.w-*)", buckets.vendor));
3356
+ lines.push(row("webflow-auto (div-block/text-block/\u2026)", buckets.webflowAuto));
3357
+ lines.push(row("shared", buckets.shared));
3358
+ const sectionEntries = Object.entries(buckets.sections).sort((a, b) => b[1].approxBytes - a[1].approxBytes);
3359
+ for (const [prefix, stat] of sectionEntries) {
3360
+ if (prefix === "misc") continue;
3361
+ lines.push(row(`section: ${prefix}`, stat));
3362
+ }
3363
+ const misc = buckets.sections.misc;
3364
+ if (misc) lines.push(row("misc (folded small/orphan)", misc));
3365
+ lines.push("");
3366
+ lines.push(`_${sectionEntries.filter(([p]) => p !== "misc").length} real sections + ${misc ? "misc" : "no misc"}_`);
3367
+ lines.push("");
3368
+ lines.push("## Flags");
3369
+ lines.push("");
3370
+ lines.push(`- Cross-boundary selectors (touch \u22652 sections): **${flags.crossBoundarySelectors.length}**`);
3371
+ lines.push(`- Fuzzy-middle classes (near shared threshold): **${flags.fuzzyMiddle.length}**`);
3372
+ lines.push(`- Dead classes (defined, unused on any page): **${flags.deadClasses.length}**`);
3373
+ lines.push(`- Page-only classes (used in pages, not in this sheet): **${flags.pageOnlyClasses.length}**`);
3374
+ const foldedClasses = flags.foldedSections.reduce((n, f) => n + f.classes, 0);
3375
+ lines.push(
3376
+ `- Folded section buckets (small/typo \u2192 misc/kept): **${flags.foldedSections.length}** (${foldedClasses} classes)`
3377
+ );
3378
+ const typos = flags.foldedSections.filter((f) => f.reason === "typo");
3379
+ if (typos.length) {
3380
+ lines.push("");
3381
+ lines.push("Typo merges:");
3382
+ for (const f of typos) lines.push(`- \`${f.from}\` \u2192 \`${f.into}\` (${f.classes})`);
3383
+ }
3384
+ if (flags.crossBoundarySelectors.length) {
3385
+ lines.push("");
3386
+ lines.push("Sample cross-boundary selectors:");
3387
+ for (const f of flags.crossBoundarySelectors.slice(0, 10)) {
3388
+ lines.push(`- \`${f.selector}\`${f.media ? ` @ ${f.media}` : ""} \u2192 ${f.buckets.join(" + ")}`);
3389
+ }
3390
+ }
3391
+ return lines.join("\n");
3392
+ }
3393
+
3394
+ // lib/server/routes/api/cssAudit.ts
3395
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".meno", "rendered-websites"]);
3396
+ function findMirrorCss(root, maxDepth = 6) {
3397
+ const found = [];
3398
+ const walk = (dir, depth) => {
3399
+ if (depth > maxDepth) return;
3400
+ let entries;
3401
+ try {
3402
+ entries = readdirSync2(dir);
3403
+ } catch {
3404
+ return;
3405
+ }
3406
+ for (const entry of entries) {
3407
+ const full = path.join(dir, entry);
3408
+ let isDir = false;
3409
+ try {
3410
+ isDir = statSync(full).isDirectory();
3411
+ } catch {
3412
+ continue;
3413
+ }
3414
+ if (isDir) {
3415
+ if (SKIP_DIRS.has(entry)) continue;
3416
+ walk(full, depth + 1);
3417
+ } else if (entry.endsWith(".css") && full.replace(/\\/g, "/").includes("/_mirror/css/")) {
3418
+ found.push(full);
3419
+ }
3420
+ }
3421
+ };
3422
+ walk(root, 0);
3423
+ return found.sort();
3424
+ }
3425
+ function resolveCssParam(root, rel) {
3426
+ const resolved = path.resolve(root, rel);
3427
+ const normalizedRoot = path.resolve(root);
3428
+ if (resolved !== normalizedRoot && !resolved.startsWith(normalizedRoot + path.sep)) return null;
3429
+ return existsSync5(resolved) ? resolved : null;
3430
+ }
3431
+ function handleCssAuditRoute(url, pageService) {
3432
+ const root = getProjectRoot();
3433
+ const cssParam = url.searchParams.get("css");
3434
+ let cssFiles;
3435
+ if (cssParam) {
3436
+ const resolved = resolveCssParam(root, cssParam);
3437
+ if (!resolved) {
3438
+ return jsonResponse({ error: `Stylesheet not found or outside project: ${cssParam}` }, { status: 404 });
3439
+ }
3440
+ cssFiles = [resolved];
3441
+ } else {
3442
+ cssFiles = findMirrorCss(root);
3443
+ if (cssFiles.length === 0) {
3444
+ return jsonResponse(
3445
+ {
3446
+ error: "No mirror stylesheet found",
3447
+ hint: "Expected a Webflow export at public/_mirror/css/*.css. Pass ?css=<relative path> to audit a specific stylesheet."
3448
+ },
3449
+ { status: 404 }
3450
+ );
3451
+ }
3452
+ }
3453
+ const css = cssFiles.map((f) => readFileSync2(f, "utf8")).join("\n");
3454
+ const pages = [];
3455
+ for (const pagePath of pageService.getAllPagePaths()) {
3456
+ const data = pageService.getPageData(pagePath);
3457
+ if (!data) continue;
3458
+ pages.push({ path: pagePath, classes: collectClassesFromPageData(data) });
3459
+ }
3460
+ const intParam = (name) => {
3461
+ const raw = url.searchParams.get(name);
3462
+ if (!raw) return void 0;
3463
+ const n = Number.parseInt(raw, 10);
3464
+ return Number.isFinite(n) ? n : void 0;
3465
+ };
3466
+ const options = {};
3467
+ const sharedMinPages = intParam("sharedMinPages");
3468
+ const minSectionClasses = intParam("minSectionClasses");
3469
+ if (sharedMinPages !== void 0) options.sharedMinPages = sharedMinPages;
3470
+ if (minSectionClasses !== void 0) options.minSectionClasses = minSectionClasses;
3471
+ const report = auditStylesheet({
3472
+ css,
3473
+ pages,
3474
+ options: Object.keys(options).length > 0 ? options : void 0
3475
+ });
3476
+ const cssFilesRel = cssFiles.map((f) => path.relative(root, f));
3477
+ const format = url.searchParams.get("format");
3478
+ if (format === "md" || format === "markdown") {
3479
+ const md = `${renderAuditMarkdown(report)}
3480
+
3481
+ _Audited: ${cssFilesRel.join(", ")}_
3482
+ `;
3483
+ return new Response(md, {
3484
+ headers: { "Content-Type": "text/markdown; charset=utf-8", "Cache-Control": "no-store, max-age=0" }
3485
+ });
3486
+ }
3487
+ return jsonResponse({ ...report, cssFiles: cssFilesRel });
3488
+ }
3489
+
3490
+ // lib/server/routes/api/deMirror.ts
3491
+ import { readFileSync as readFileSync3 } from "node:fs";
3492
+ import path2 from "node:path";
3493
+
3494
+ // lib/server/deMirror.ts
3495
+ import * as csstree2 from "css-tree";
3496
+ var CLASS_TOKEN_RE2 = /\.(-?[_a-zA-Z][\w-]*)/g;
3497
+ var FUNCTIONAL_PSEUDO_RE2 = /:(not|is|where|has|matches)\([^()]*\)/gi;
3498
+ function classTokens2(selector) {
3499
+ const out = [];
3500
+ let m;
3501
+ CLASS_TOKEN_RE2.lastIndex = 0;
3502
+ while ((m = CLASS_TOKEN_RE2.exec(selector)) !== null) {
3503
+ if (m[1]) out.push(m[1]);
3504
+ }
3505
+ return out;
3506
+ }
3507
+ function subjectClassesOf(selector) {
3508
+ const cleaned = selector.replace(FUNCTIONAL_PSEUDO_RE2, " ");
3509
+ const segments = cleaned.split(/\s*[>+~]\s*|\s+/).filter((s) => s.trim().length > 0);
3510
+ const subject = segments[segments.length - 1] ?? cleaned;
3511
+ return classTokens2(subject);
3512
+ }
3513
+ function declBlock(block) {
3514
+ const lines = [];
3515
+ if (block && block.type === "Block") {
3516
+ block.children.forEach((child) => {
3517
+ if (child.type !== "Declaration") return;
3518
+ const g = csstree2.generate(child);
3519
+ const i = g.indexOf(":");
3520
+ lines.push(` ${i > 0 ? `${g.slice(0, i)}: ${g.slice(i + 1)}` : g};`);
3521
+ });
3522
+ }
3523
+ return { text: lines.join("\n"), count: lines.length };
3524
+ }
3525
+ function captureClassRules(css) {
3526
+ const ast = csstree2.parse(css, { parseValue: false, parseCustomProperty: false });
3527
+ const out = [];
3528
+ const visit = (container, media, skip) => {
3529
+ const children = "children" in container ? container.children : null;
3530
+ if (!children) return;
3531
+ children.forEach((node) => {
3532
+ if (node.type === "Atrule") {
3533
+ const name = node.name.toLowerCase();
3534
+ if (name === "media") {
3535
+ const prelude = node.prelude ? csstree2.generate(node.prelude) : "";
3536
+ if (node.block) visit(node.block, prelude, false);
3537
+ } else if (name === "supports") {
3538
+ if (node.block) visit(node.block, media, false);
3539
+ } else if (node.block) {
3540
+ visit(node.block, media, true);
3541
+ }
3542
+ return;
3543
+ }
3544
+ if (node.type !== "Rule" || skip) return;
3545
+ if (node.prelude?.type !== "SelectorList") return;
3546
+ const selectors = [];
3547
+ const subject = /* @__PURE__ */ new Set();
3548
+ const all = /* @__PURE__ */ new Set();
3549
+ node.prelude.children.forEach((sel) => {
3550
+ const text = csstree2.generate(sel);
3551
+ const cls = classTokens2(text);
3552
+ if (cls.length === 0) return;
3553
+ selectors.push(text);
3554
+ for (const c of cls) all.add(c);
3555
+ for (const c of subjectClassesOf(text)) subject.add(c);
3556
+ });
3557
+ if (selectors.length === 0) return;
3558
+ const { text: decls, count } = declBlock(node.block);
3559
+ out.push({
3560
+ selector: selectors.join(", "),
3561
+ media,
3562
+ subjectClasses: [...subject],
3563
+ allClasses: [...all],
3564
+ declarations: count,
3565
+ bytes: csstree2.generate(node).length,
3566
+ text: `${selectors.join(",\n")} {
3567
+ ${decls}
3568
+ }`
3569
+ });
3570
+ });
3571
+ };
3572
+ visit(ast, null, false);
3573
+ return out;
3574
+ }
3575
+ function assembleSlice(rules) {
3576
+ const base = rules.filter((r) => r.media === null);
3577
+ const byMedia = /* @__PURE__ */ new Map();
3578
+ for (const r of rules) {
3579
+ if (r.media === null) continue;
3580
+ const list = byMedia.get(r.media) ?? [];
3581
+ list.push(r);
3582
+ byMedia.set(r.media, list);
3583
+ }
3584
+ const parts = [];
3585
+ for (const r of base) parts.push(r.text);
3586
+ for (const [media, list] of byMedia) {
3587
+ const inner = list.map((r) => r.text.replace(/^/gm, " ")).join("\n");
3588
+ parts.push(`@media ${media} {
3589
+ ${inner}
3590
+ }`);
3591
+ }
3592
+ return parts.join("\n\n");
3593
+ }
3594
+ function planDeMirror(input) {
3595
+ const { css, components, options = {} } = input;
3596
+ const warningSampleLimit = options.warningSampleLimit ?? 40;
3597
+ const compClasses = components.map((c) => ({ name: c.name, set: new Set(c.classes) }));
3598
+ const rules = captureClassRules(css);
3599
+ const perComponent = /* @__PURE__ */ new Map();
3600
+ const sharedSelectors = [];
3601
+ const deadSelectors = [];
3602
+ let movedRules = 0;
3603
+ let movedBytes = 0;
3604
+ let sharedRules = 0;
3605
+ let deadRules = 0;
3606
+ let sharedBytes = 0;
3607
+ let deadBytes = 0;
3608
+ let totalBytes = 0;
3609
+ for (const rule of rules) {
3610
+ totalBytes += rule.bytes;
3611
+ const need = rule.subjectClasses.length > 0 ? rule.subjectClasses : rule.allClasses;
3612
+ const owners = [];
3613
+ for (const c of compClasses) {
3614
+ if (need.every((cls) => c.set.has(cls))) owners.push(c.name);
3615
+ }
3616
+ if (owners.length === 1) {
3617
+ const name = owners[0];
3618
+ const list = perComponent.get(name) ?? [];
3619
+ list.push(rule);
3620
+ perComponent.set(name, list);
3621
+ movedRules += 1;
3622
+ movedBytes += rule.bytes;
3623
+ } else if (owners.length === 0) {
3624
+ deadRules += 1;
3625
+ deadBytes += rule.bytes;
3626
+ if (deadSelectors.length < warningSampleLimit) deadSelectors.push(rule.selector);
3627
+ } else {
3628
+ sharedRules += 1;
3629
+ sharedBytes += rule.bytes;
3630
+ if (sharedSelectors.length < warningSampleLimit) sharedSelectors.push(rule.selector);
3631
+ }
3632
+ }
3633
+ const componentsOut = [];
3634
+ for (const [name, list] of perComponent) {
3635
+ const breakpoints = [...new Set(list.map((r) => r.media ?? ""))].sort();
3636
+ componentsOut.push({
3637
+ name,
3638
+ ruleCount: list.length,
3639
+ declarations: list.reduce((n, r) => n + r.declarations, 0),
3640
+ approxBytes: list.reduce((n, r) => n + r.bytes, 0),
3641
+ breakpoints,
3642
+ css: assembleSlice(list)
3643
+ });
3644
+ }
3645
+ componentsOut.sort((a, b) => b.approxBytes - a.approxBytes || a.name.localeCompare(b.name));
3646
+ const roundTrip = movedRules + sharedRules + deadRules === rules.length;
3647
+ return {
3648
+ source: { classRules: rules.length, classRuleBytes: totalBytes, componentCount: components.length },
3649
+ components: componentsOut,
3650
+ global: { sharedRules, deadRules, approxBytes: sharedBytes + deadBytes },
3651
+ moved: { rules: movedRules, approxBytes: movedBytes, components: componentsOut.length },
3652
+ warnings: { sharedSelectors, deadSelectors },
3653
+ roundTrip
3654
+ };
3655
+ }
3656
+ function renderDeMirrorMarkdown(plan) {
3657
+ const kb = (n) => `${(n / 1024).toFixed(1)} KB`;
3658
+ const lines = [];
3659
+ lines.push("# De-mirror plan");
3660
+ lines.push("");
3661
+ lines.push(
3662
+ `${plan.source.classRules} class rules (${kb(plan.source.classRuleBytes)}) across ${plan.source.componentCount} components \xB7 round-trip ${plan.roundTrip ? "\u2705 lossless" : "\u274C FAILED"}`
3663
+ );
3664
+ lines.push("");
3665
+ lines.push(
3666
+ `**Moves onto components:** ${plan.moved.rules} rules \u2192 ${plan.moved.components} components (${kb(plan.moved.approxBytes)})`
3667
+ );
3668
+ lines.push(
3669
+ `**Stays global:** ${plan.global.sharedRules} shared + ${plan.global.deadRules} dead = ${kb(plan.global.approxBytes)}`
3670
+ );
3671
+ lines.push("");
3672
+ lines.push("## Slices (largest first)");
3673
+ lines.push("");
3674
+ lines.push("| component | rules | declarations | ~size | breakpoints |");
3675
+ lines.push("| --- | --- | --- | --- | --- |");
3676
+ for (const c of plan.components.slice(0, 40)) {
3677
+ lines.push(`| ${c.name} | ${c.ruleCount} | ${c.declarations} | ${kb(c.approxBytes)} | ${c.breakpoints.length} |`);
3678
+ }
3679
+ if (plan.components.length > 40) lines.push(`| \u2026 +${plan.components.length - 40} more | | | | |`);
3680
+ lines.push("");
3681
+ lines.push(
3682
+ `_Shared (kept global): ${plan.warnings.sharedSelectors.slice(0, 6).join(", ") || "none"}${plan.warnings.sharedSelectors.length > 6 ? " \u2026" : ""}_`
3683
+ );
3684
+ return lines.join("\n");
3685
+ }
3686
+
3687
+ // lib/server/routes/api/deMirror.ts
3688
+ function componentsUsedByPage(pageData, allNames) {
3689
+ const hay = JSON.stringify(pageData ?? "");
3690
+ const used = /* @__PURE__ */ new Set();
3691
+ for (const name of allNames) {
3692
+ if (hay.includes(`"${name}"`)) used.add(name);
3693
+ }
3694
+ return used;
3695
+ }
3696
+ function handleDeMirrorRoute(url, pageService, componentService) {
3697
+ const root = getProjectRoot();
3698
+ const cssParam = url.searchParams.get("css");
3699
+ let cssFiles;
3700
+ if (cssParam) {
3701
+ const resolved = resolveCssParam(root, cssParam);
3702
+ if (!resolved) {
3703
+ return jsonResponse({ error: `Stylesheet not found or outside project: ${cssParam}` }, { status: 404 });
3704
+ }
3705
+ cssFiles = [resolved];
3706
+ } else {
3707
+ cssFiles = findMirrorCss(root);
3708
+ if (cssFiles.length === 0) {
3709
+ return jsonResponse(
3710
+ {
3711
+ error: "No mirror stylesheet found",
3712
+ hint: "Expected a Webflow export at public/_mirror/css/*.css. Pass ?css=<relative path> to target a specific stylesheet."
3713
+ },
3714
+ { status: 404 }
3715
+ );
3716
+ }
3717
+ }
3718
+ const css = cssFiles.map((f) => readFileSync3(f, "utf8")).join("\n");
3719
+ const all = componentService.getAllComponents();
3720
+ const components = Object.entries(all).map(([name, def]) => ({
3721
+ name,
3722
+ classes: collectClassesFromPageData(def)
3723
+ }));
3724
+ if (components.length === 0) {
3725
+ return jsonResponse(
3726
+ {
3727
+ error: "No components found",
3728
+ hint: "Componentize the project first (e.g. /componentize-mirror), then de-mirror."
3729
+ },
3730
+ { status: 422 }
3731
+ );
3732
+ }
3733
+ const plan = planDeMirror({ css, components });
3734
+ const slug = url.searchParams.get("slug");
3735
+ let scoped = plan.components;
3736
+ let scopedNote;
3737
+ if (slug) {
3738
+ const pagePath = slug === "index" ? "/" : `/${slug.replace(/^\//, "")}`;
3739
+ const pageData = pageService.getPageData(pagePath);
3740
+ if (!pageData) {
3741
+ return jsonResponse({ error: `Page not found: ${slug}` }, { status: 404 });
3742
+ }
3743
+ const used = componentsUsedByPage(
3744
+ pageData,
3745
+ components.map((c) => c.name)
3746
+ );
3747
+ scoped = plan.components.filter((c) => used.has(c.name));
3748
+ scopedNote = `filtered to ${scoped.length} component(s) used by /${slug} (ownership computed project-wide)`;
3749
+ }
3750
+ const cssFilesRel = cssFiles.map((f) => path2.relative(root, f));
3751
+ const view = { ...plan, components: scoped };
3752
+ const format = url.searchParams.get("format");
3753
+ if (format === "md" || format === "markdown") {
3754
+ const md = `${renderDeMirrorMarkdown(view)}
3755
+ ${scopedNote ? `
3756
+ _${scopedNote}_
3757
+ ` : ""}
3758
+ _Sheet: ${cssFilesRel.join(", ")}_
3759
+ `;
3760
+ return new Response(md, {
3761
+ headers: { "Content-Type": "text/markdown; charset=utf-8", "Cache-Control": "no-store, max-age=0" }
3762
+ });
3763
+ }
3764
+ const includeSlices = url.searchParams.get("slices") === "true";
3765
+ const responseComponents = includeSlices ? scoped : scoped.map(({ css: _css, ...meta }) => meta);
3766
+ return jsonResponse({
3767
+ ...view,
3768
+ components: responseComponents,
3769
+ cssFiles: cssFilesRel,
3770
+ ...scopedNote ? { scopedNote } : {},
3771
+ dryRun: true
3772
+ });
3773
+ }
3774
+
3775
+ // lib/server/routes/api/functions.ts
3776
+ import path3 from "node:path";
2885
3777
  async function loadProjectEnv() {
2886
3778
  const envPath = projectPaths.env();
2887
3779
  const env = {};
@@ -2915,7 +3807,7 @@ async function handleFunctionsRoute(req, url) {
2915
3807
  return void 0;
2916
3808
  }
2917
3809
  const functionPath = url.pathname.replace("/api/", "");
2918
- const functionFilePath = path.join(projectPaths.functions(), "api", `${functionPath}.ts`);
3810
+ const functionFilePath = path3.join(projectPaths.functions(), "api", `${functionPath}.ts`);
2919
3811
  if (!await fileExists(functionFilePath)) {
2920
3812
  const jsFilePath = functionFilePath.replace(".ts", ".js");
2921
3813
  if (!await fileExists(jsFilePath)) {
@@ -3072,6 +3964,18 @@ async function handleCoreApiRoutes(req, url, context) {
3072
3964
  if (url.pathname === "/api/enums" && req.method === "GET") {
3073
3965
  return await handleRouteError(() => handleEnumsRoute(), "Failed to fetch enums");
3074
3966
  }
3967
+ if (url.pathname === "/api/css-audit" && req.method === "GET") {
3968
+ return await handleRouteError(
3969
+ () => Promise.resolve(handleCssAuditRoute(url, pageService)),
3970
+ "Failed to generate CSS audit"
3971
+ );
3972
+ }
3973
+ if (url.pathname === "/api/de-mirror" && req.method === "GET") {
3974
+ return await handleRouteError(
3975
+ () => Promise.resolve(handleDeMirrorRoute(url, pageService, componentService)),
3976
+ "Failed to generate de-mirror plan"
3977
+ );
3978
+ }
3075
3979
  if (url.pathname === "/api/usage" && req.method === "GET") {
3076
3980
  return await handleRouteError(() => {
3077
3981
  const cmsCollections = cmsService ? cmsService.getAllSchemas().size : 0;
@@ -3647,21 +4551,21 @@ function processCMSPropsTemplate(props, cmsItem, locale, i18nConfig) {
3647
4551
  }
3648
4552
 
3649
4553
  // lib/server/ssr/imageMetadata.ts
3650
- import * as path2 from "node:path";
4554
+ import * as path4 from "node:path";
3651
4555
  var RESPONSIVE_WIDTHS = [500, 800, 1080, 1600, 2400];
3652
4556
  var DEFAULT_SIZES = "100vw";
3653
4557
  async function buildImageMetadataMap() {
3654
4558
  const metadataMap = /* @__PURE__ */ new Map();
3655
4559
  try {
3656
4560
  const imagesDir = projectPaths.images();
3657
- const manifestPath = path2.join(imagesDir, "manifest.json");
4561
+ const manifestPath = path4.join(imagesDir, "manifest.json");
3658
4562
  if (!await fileExists(manifestPath)) {
3659
4563
  return metadataMap;
3660
4564
  }
3661
4565
  const manifestContent = await readTextFile(manifestPath);
3662
4566
  const manifest = JSON.parse(manifestContent);
3663
4567
  for (const [filename, entry] of Object.entries(manifest.images)) {
3664
- const baseName = path2.basename(filename, path2.extname(filename));
4568
+ const baseName = path4.basename(filename, path4.extname(filename));
3665
4569
  const webpSrcsetParts = [];
3666
4570
  const avifSrcsetParts = [];
3667
4571
  for (const variant of entry.variants) {
@@ -3839,8 +4743,8 @@ function resolveFilterValue(value, scope) {
3839
4743
  if (typeof value !== "string" || !value.startsWith("{{") || !value.endsWith("}}")) {
3840
4744
  return value;
3841
4745
  }
3842
- const path3 = value.slice(2, -2).trim();
3843
- const resolved = scope ? getNestedValue(scope, path3) : void 0;
4746
+ const path5 = value.slice(2, -2).trim();
4747
+ const resolved = scope ? getNestedValue(scope, path5) : void 0;
3844
4748
  return resolved !== void 0 ? resolved : UNRESOLVED_FILTER_VALUE;
3845
4749
  }
3846
4750
  function resolveFilterTemplates(filter, scope, collection) {
@@ -4250,8 +5154,8 @@ function getPropItems(source, ctx) {
4250
5154
  if (source.startsWith("{{") && source.endsWith("}}")) {
4251
5155
  const templateCtx = ctx.templateContext;
4252
5156
  if (templateCtx) {
4253
- const path3 = source.slice(2, -2).trim();
4254
- const resolved = getNestedValue(templateCtx, path3);
5157
+ const path5 = source.slice(2, -2).trim();
5158
+ const resolved = getNestedValue(templateCtx, path5);
4255
5159
  if (Array.isArray(resolved)) {
4256
5160
  return resolved;
4257
5161
  } else if (resolved !== void 0) {
@@ -4288,12 +5192,12 @@ function buildNodeElementClass(ctx, label, isSlotContent2) {
4288
5192
  const effectiveFileType = useComponentContext ? "component" : "page";
4289
5193
  const effectiveFileName = useComponentContext ? ctx.componentContext : pagePath ? pagePath.replace(/^\//, "").replace(/\//g, "_") || "index" : "page";
4290
5194
  const rawPath = ctx.elementPath || [];
4291
- const path3 = useComponentContext && ctx.componentRootPath ? rawPath.slice(ctx.componentRootPath.length) : rawPath;
5195
+ const path5 = useComponentContext && ctx.componentRootPath ? rawPath.slice(ctx.componentRootPath.length) : rawPath;
4292
5196
  const elementClassCtx = {
4293
5197
  fileType: effectiveFileType,
4294
5198
  fileName: effectiveFileName || "page",
4295
5199
  label,
4296
- path: path3
5200
+ path: path5
4297
5201
  };
4298
5202
  return generateElementClassName(elementClassCtx);
4299
5203
  }
@@ -5454,6 +6358,7 @@ function generateAllInlineDataScripts(collections) {
5454
6358
  }
5455
6359
 
5456
6360
  // lib/server/ssr/htmlGenerator.ts
6361
+ import { posix } from "node:path";
5457
6362
  function generateImagePreloadTags(preloadImages) {
5458
6363
  if (preloadImages.length === 0) return "";
5459
6364
  return preloadImages.map(
@@ -5464,6 +6369,43 @@ function minifyCSS(code) {
5464
6369
  if (!code.trim()) return code;
5465
6370
  return code.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\s*([{};:,>~])\s*/g, "$1").replace(/\s+/g, " ").replace(/\{\s+/g, "{").replace(/\s+\}/g, "}").replace(/;}/g, "}").trim();
5466
6371
  }
6372
+ async function collectPageCssImports(frontmatter, pagePath, nonceAttr) {
6373
+ if (!frontmatter?.includes(".css")) return "";
6374
+ const seen = /* @__PURE__ */ new Set();
6375
+ const specs = [];
6376
+ const re = /import\b[^'"]*['"]([^'"]+\.css)(?:\?[^'"]*)?['"]/g;
6377
+ let m;
6378
+ while ((m = re.exec(frontmatter)) !== null) {
6379
+ const spec = m[1];
6380
+ if (!spec || seen.has(spec)) continue;
6381
+ seen.add(spec);
6382
+ if (spec.endsWith("styles/theme.css")) continue;
6383
+ specs.push(spec);
6384
+ }
6385
+ if (specs.length === 0) return "";
6386
+ const pageDir = posix.join("src/pages", posix.dirname(pagePath || "/"));
6387
+ const tags = [];
6388
+ for (const spec of specs) {
6389
+ if (/^(?:https?:)?\/\//.test(spec)) {
6390
+ tags.push(`<link rel="stylesheet" href="${escapeHtml(spec)}">`);
6391
+ continue;
6392
+ }
6393
+ const candidates = spec.startsWith("/") ? [spec.slice(1)] : [posix.normalize(posix.join(pageDir, spec)), `src/${spec.replace(/^(?:\.\.?\/)+/, "")}`];
6394
+ let css = null;
6395
+ for (const rel of candidates) {
6396
+ if (rel.startsWith("..")) continue;
6397
+ try {
6398
+ css = await readTextFile(resolveProjectPath(rel));
6399
+ break;
6400
+ } catch {
6401
+ }
6402
+ }
6403
+ if (css == null) continue;
6404
+ const content = rewriteViewportUnitsInStylesheet(css).replace(/<\/style/gi, "<\\/style");
6405
+ tags.push(`<style${nonceAttr} data-meno-page-css="${escapeHtml(spec)}">${content}</style>`);
6406
+ }
6407
+ return tags.join("\n ");
6408
+ }
5467
6409
  async function generateSSRHTML(pageDataOrOptions, globalComponents = {}, pagePath = "/", baseUrl = "", useBuiltBundle = false, locale, slugMappings, cmsContext, cmsService, externalScriptPath) {
5468
6410
  let options;
5469
6411
  if ("pageData" in pageDataOrOptions) {
@@ -5485,7 +6427,7 @@ async function generateSSRHTML(pageDataOrOptions, globalComponents = {}, pagePat
5485
6427
  const {
5486
6428
  pageData,
5487
6429
  globalComponents: components = {},
5488
- pagePath: path3 = "/",
6430
+ pagePath: path5 = "/",
5489
6431
  baseUrl: base = "",
5490
6432
  useBuiltBundle: useBundled = false,
5491
6433
  locale: loc,
@@ -5509,7 +6451,7 @@ async function generateSSRHTML(pageDataOrOptions, globalComponents = {}, pagePat
5509
6451
  const rendered = await renderPageSSR(
5510
6452
  pageData,
5511
6453
  components,
5512
- path3,
6454
+ path5,
5513
6455
  base,
5514
6456
  loc,
5515
6457
  void 0,
@@ -5546,11 +6488,10 @@ async function generateSSRHTML(pageDataOrOptions, globalComponents = {}, pagePat
5546
6488
  await configService.load();
5547
6489
  const globalLibraries = configService.getLibraries() || { js: [], css: [] };
5548
6490
  const globalCustomCode = configService.getCustomCode();
5549
- const menoBadgeHtml = configService.getShowMenoBadge() ? `<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>` : "";
5550
6491
  const mergedCustomCode = {
5551
6492
  head: [globalCustomCode.head, pageCustomCode?.head].filter(Boolean).join("\n"),
5552
6493
  bodyStart: [globalCustomCode.bodyStart, pageCustomCode?.bodyStart].filter(Boolean).join("\n"),
5553
- bodyEnd: [globalCustomCode.bodyEnd, pageCustomCode?.bodyEnd, menoBadgeHtml].filter(Boolean).join("\n")
6494
+ bodyEnd: [globalCustomCode.bodyEnd, pageCustomCode?.bodyEnd].filter(Boolean).join("\n")
5554
6495
  };
5555
6496
  const componentLibraries = collectComponentLibraries(components, pageData.components || {});
5556
6497
  const globalPlusComponent = mergeLibraries(globalLibraries, componentLibraries) || { js: [], css: [] };
@@ -5696,7 +6637,7 @@ picture {
5696
6637
  line-height: normal;
5697
6638
  }`;
5698
6639
  const combinedCSS = [fontCSS, themeColorVariablesCSS, variablesCSS, baseCSS, componentCSS, utilityCSS, interactiveCSS].filter(Boolean).join("\n");
5699
- const cssWithStableViewport = rewriteViewportUnits(combinedCSS);
6640
+ const cssWithStableViewport = rewriteViewportUnitsInStylesheet(combinedCSS);
5700
6641
  const finalCSS = useBundled ? minifyCSS(cssWithStableViewport) : cssWithStableViewport;
5701
6642
  const prefetchConfig = await loadPrefetchConfig();
5702
6643
  const menoConfig = prefetchConfig.enabled ? { prefetch: prefetchConfig } : {};
@@ -5720,11 +6661,12 @@ ${externalJavaScript}`;
5720
6661
  const scriptPreloadTag = extScriptPath ? `<link rel="preload" href="${extScriptPath}" as="script">` : "";
5721
6662
  const imagePreloadTags = generateImagePreloadTags(rendered.preloadImages);
5722
6663
  const wsUrl = serverPort ? `'ws://localhost:${serverPort}/hmr'` : `location.origin.replace('http','ws')+'/hmr'`;
5723
- const liveReloadScript = injectLiveReload ? `<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>` : "";
5724
- const scrollHandlerScript = injectLiveReload ? `<script${nonceAttr}>(function(){window.addEventListener('message',function(e){if(e.data.type==='GET_SCROLL_POSITION'){window.parent.postMessage({type:'SCROLL_POSITION_RESPONSE',scrollX:window.scrollX,scrollY:window.scrollY},'*')}else if(e.data.type==='SET_SCROLL_POSITION'){window.scrollTo(e.data.scrollX,e.data.scrollY)}})})()</script>` : "";
6664
+ const liveReloadScript = injectLiveReload ? `<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 iScroll(x,y){var de=document.documentElement,b=document.body,pd=de.style.scrollBehavior,pb=b?b.style.scrollBehavior:'';de.style.scrollBehavior='auto';if(b)b.style.scrollBehavior='auto';try{window.scrollTo(x,y)}catch(e){}de.style.scrollBehavior=pd;if(b)b.style.scrollBehavior=pb}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){iScroll(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'));iScroll(sx,sy)};s.onerror=function(){iScroll(sx,sy)};document.body.appendChild(s)}else{document.dispatchEvent(new Event('DOMContentLoaded'));iScroll(sx,sy)}}}).catch(function(){location.reload()})}var iR=document.getElementById('root');if(iR)lastSrvRoot=iR.cloneNode(true);connect()})()</script>` : "";
6665
+ const scrollHandlerScript = injectLiveReload ? `<script${nonceAttr}>(function(){window.addEventListener('message',function(e){if(e.data.type==='GET_SCROLL_POSITION'){window.parent.postMessage({type:'SCROLL_POSITION_RESPONSE',scrollX:window.scrollX,scrollY:window.scrollY},'*')}else if(e.data.type==='SET_SCROLL_POSITION'){var de=document.documentElement,b=document.body,pd=de.style.scrollBehavior,pb=b?b.style.scrollBehavior:'';de.style.scrollBehavior='auto';if(b)b.style.scrollBehavior='auto';window.scrollTo(e.data.scrollX,e.data.scrollY);de.style.scrollBehavior=pd;if(b)b.style.scrollBehavior=pb}})})()</script>` : "";
5725
6666
  const styleContent = useBundled ? finalCSS : `
5726
6667
  ${combinedCSS.split("\n").join("\n ")}
5727
6668
  `;
6669
+ const pageCssTags = await collectPageCssImports(pageData._frontmatter, pagePath, nonceAttr);
5728
6670
  const htmlDocument = `<!DOCTYPE html>
5729
6671
  <html lang="${rendered.locale}" theme="${themeConfig.default}">
5730
6672
  <head>
@@ -5738,7 +6680,8 @@ ${externalJavaScript}`;
5738
6680
  ` : ""}${libraryTags.headCSS ? `${libraryTags.headCSS}
5739
6681
  ` : ""}${libraryTags.headJS ? `${libraryTags.headJS}
5740
6682
  ` : ""}${rendered.meta}
5741
- ${configInlineScript}${cmsInlineScript}${clientDataScripts}<style id="meno-styles"${nonceAttr}>${styleContent}</style>${mergedCustomCode.head ? `
6683
+ ${configInlineScript}${cmsInlineScript}${clientDataScripts}<style id="meno-styles"${nonceAttr}>${styleContent}</style>${pageCssTags ? `
6684
+ ${pageCssTags}` : ""}${mergedCustomCode.head ? `
5742
6685
  ${mergedCustomCode.head}` : ""}
5743
6686
  </head>
5744
6687
  <body>${mergedCustomCode.bodyStart ? `
@@ -7146,8 +8089,8 @@ function singularizeWord(word) {
7146
8089
  if (word.endsWith("s")) return word.slice(0, -1);
7147
8090
  return word;
7148
8091
  }
7149
- function getNestedValue2(obj, path3) {
7150
- return path3.split(".").reduce((current, key) => {
8092
+ function getNestedValue2(obj, path5) {
8093
+ return path5.split(".").reduce((current, key) => {
7151
8094
  return current && typeof current === "object" ? current[key] : void 0;
7152
8095
  }, obj);
7153
8096
  }
@@ -8628,7 +9571,11 @@ async function createServer(config) {
8628
9571
  try {
8629
9572
  const server = await createRuntimeServer({
8630
9573
  port: currentPort,
8631
- hostname: "localhost",
9574
+ // Default `localhost` resolves to IPv6 `::1` only on macOS, so a
9575
+ // conventional `127.0.0.1 meno.local` hosts alias can't reach it. MENO_HOST
9576
+ // lets the standalone launcher bind broadly (e.g. `::` dual-stack) so both
9577
+ // IPv4 and IPv6 aliases work. Unset → unchanged `localhost` behavior.
9578
+ hostname: process.env.MENO_HOST || "localhost",
8632
9579
  wsPath: HMR_ROUTE,
8633
9580
  async fetch(req, upgradeWebSocket) {
8634
9581
  const url = new URL(req.url);
@@ -9023,8 +9970,8 @@ ${plainTextErrors}`;
9023
9970
  }
9024
9971
 
9025
9972
  // lib/server/services/pageService.ts
9026
- import { existsSync as existsSync5, readdirSync as readdirSync2, mkdirSync, rmdirSync } from "node:fs";
9027
- import { join as join4 } from "node:path";
9973
+ import { existsSync as existsSync6, readdirSync as readdirSync3, mkdirSync, rmdirSync } from "node:fs";
9974
+ import { join as join4, relative, sep } from "node:path";
9028
9975
 
9029
9976
  // lib/server/utils/jsonLineMapper.ts
9030
9977
  function buildLineMap(jsonText) {
@@ -9068,13 +10015,13 @@ function buildLineMap(jsonText) {
9068
10015
  pos++;
9069
10016
  }
9070
10017
  }
9071
- function parseValue(path3, trackChildren) {
10018
+ function parseValue(path5, trackChildren) {
9072
10019
  skipWhitespace();
9073
10020
  const startPos = pos;
9074
10021
  if (jsonText[pos] === "{") {
9075
- parseObject(path3, trackChildren);
10022
+ parseObject(path5, trackChildren);
9076
10023
  } else if (jsonText[pos] === "[") {
9077
- parseArray(path3, trackChildren);
10024
+ parseArray(path5, trackChildren);
9078
10025
  } else if (jsonText[pos] === '"') {
9079
10026
  parseString();
9080
10027
  } else if (jsonText[pos] === "t") {
@@ -9088,14 +10035,14 @@ function buildLineMap(jsonText) {
9088
10035
  }
9089
10036
  return { start: startPos, end: pos };
9090
10037
  }
9091
- function recordTrackedRoot(path3) {
9092
- const { start, end } = parseValue(path3, true);
10038
+ function recordTrackedRoot(path5) {
10039
+ const { start, end } = parseValue(path5, true);
9093
10040
  lineMap.set("", {
9094
10041
  startLine: charToLine[start] ?? 0,
9095
10042
  endLine: charToLine[end - 1] || charToLine[start] || 0
9096
10043
  });
9097
10044
  }
9098
- function parseObject(path3, trackChildren) {
10045
+ function parseObject(path5, trackChildren) {
9099
10046
  pos++;
9100
10047
  skipWhitespace();
9101
10048
  while (pos < jsonText.length && jsonText[pos] !== "}") {
@@ -9104,14 +10051,14 @@ function buildLineMap(jsonText) {
9104
10051
  skipWhitespace();
9105
10052
  pos++;
9106
10053
  skipWhitespace();
9107
- if (key === "root" && path3.length === 0) {
9108
- recordTrackedRoot(path3);
9109
- } else if (key === "component" && path3.length === 0) {
9110
- parseComponentWrapper(path3);
10054
+ if (key === "root" && path5.length === 0) {
10055
+ recordTrackedRoot(path5);
10056
+ } else if (key === "component" && path5.length === 0) {
10057
+ parseComponentWrapper(path5);
9111
10058
  } else if (key === "children" && trackChildren) {
9112
- parseValue(path3, true);
10059
+ parseValue(path5, true);
9113
10060
  } else {
9114
- parseValue(path3, false);
10061
+ parseValue(path5, false);
9115
10062
  }
9116
10063
  skipWhitespace();
9117
10064
  if (jsonText[pos] === ",") pos++;
@@ -9119,9 +10066,9 @@ function buildLineMap(jsonText) {
9119
10066
  }
9120
10067
  pos++;
9121
10068
  }
9122
- function parseComponentWrapper(path3) {
10069
+ function parseComponentWrapper(path5) {
9123
10070
  if (jsonText[pos] !== "{") {
9124
- parseValue(path3, false);
10071
+ parseValue(path5, false);
9125
10072
  return;
9126
10073
  }
9127
10074
  pos++;
@@ -9133,9 +10080,9 @@ function buildLineMap(jsonText) {
9133
10080
  pos++;
9134
10081
  skipWhitespace();
9135
10082
  if (key === "structure") {
9136
- recordTrackedRoot(path3);
10083
+ recordTrackedRoot(path5);
9137
10084
  } else {
9138
- parseValue(path3, false);
10085
+ parseValue(path5, false);
9139
10086
  }
9140
10087
  skipWhitespace();
9141
10088
  if (jsonText[pos] === ",") pos++;
@@ -9143,12 +10090,12 @@ function buildLineMap(jsonText) {
9143
10090
  }
9144
10091
  pos++;
9145
10092
  }
9146
- function parseArray(path3, trackChildren) {
10093
+ function parseArray(path5, trackChildren) {
9147
10094
  pos++;
9148
10095
  skipWhitespace();
9149
10096
  let index = 0;
9150
10097
  while (pos < jsonText.length && jsonText[pos] !== "]") {
9151
- const childPath = [...path3, index];
10098
+ const childPath = [...path5, index];
9152
10099
  const { start, end } = parseValue(childPath, trackChildren);
9153
10100
  if (trackChildren) {
9154
10101
  const pathStr = childPath.join(",");
@@ -9221,9 +10168,12 @@ var PageService = class {
9221
10168
  return;
9222
10169
  }
9223
10170
  const pages = await this.provider.loadAll();
9224
- for (const [path3, content] of pages) {
10171
+ for (const [path5, content] of pages) {
9225
10172
  const lineMap = buildLineMap(content);
9226
- this.pageCache.set(path3, content, lineMap);
10173
+ this.pageCache.set(path5, content, lineMap);
10174
+ }
10175
+ for (const path5 of this.pageCache.keys()) {
10176
+ if (!pages.has(path5)) this.pageCache.delete(path5);
9227
10177
  }
9228
10178
  }
9229
10179
  /**
@@ -9240,8 +10190,8 @@ var PageService = class {
9240
10190
  * }
9241
10191
  * ```
9242
10192
  */
9243
- getPage(path3) {
9244
- return this.pageCache.getContent(path3);
10193
+ getPage(path5) {
10194
+ return this.pageCache.getContent(path5);
9245
10195
  }
9246
10196
  /**
9247
10197
  * Get parsed page data
@@ -9260,8 +10210,8 @@ var PageService = class {
9260
10210
  * }
9261
10211
  * ```
9262
10212
  */
9263
- getPageData(path3) {
9264
- const content = this.pageCache.getContent(path3);
10213
+ getPageData(path5) {
10214
+ const content = this.pageCache.getContent(path5);
9265
10215
  if (!content) {
9266
10216
  return null;
9267
10217
  }
@@ -9291,15 +10241,15 @@ var PageService = class {
9291
10241
  * });
9292
10242
  * ```
9293
10243
  */
9294
- async savePage(path3, data) {
10244
+ async savePage(path5, data) {
9295
10245
  if (!this.provider) {
9296
10246
  throw new Error("PageProvider not set");
9297
10247
  }
9298
10248
  const { _lineMap, ...dataToSave } = data;
9299
10249
  const content = JSON.stringify(dataToSave, null, 2);
9300
- await this.provider.save(path3, content);
10250
+ await this.provider.save(path5, content);
9301
10251
  const lineMap = buildLineMap(content);
9302
- this.pageCache.set(path3, content, lineMap);
10252
+ this.pageCache.set(path5, content, lineMap);
9303
10253
  }
9304
10254
  /**
9305
10255
  * Re-read a single page from disk via the provider and refresh the cache.
@@ -9313,22 +10263,22 @@ var PageService = class {
9313
10263
  * partial mid-write or parse error), in which case the cache is left intact and
9314
10264
  * the next watcher event retries.
9315
10265
  */
9316
- async reloadPageFromDisk(path3) {
10266
+ async reloadPageFromDisk(path5) {
9317
10267
  if (!this.provider) return false;
9318
10268
  let content = null;
9319
10269
  try {
9320
- content = await this.provider.get(path3);
10270
+ content = await this.provider.get(path5);
9321
10271
  } catch (error) {
9322
- logRuntimeError("pageService.reloadPageFromDisk", error, { path: path3 });
10272
+ logRuntimeError("pageService.reloadPageFromDisk", error, { path: path5 });
9323
10273
  return false;
9324
10274
  }
9325
10275
  if (content) {
9326
- this.pageCache.set(path3, content, buildLineMap(content));
10276
+ this.pageCache.set(path5, content, buildLineMap(content));
9327
10277
  return true;
9328
10278
  }
9329
10279
  try {
9330
- if (!await this.provider.exists(path3)) {
9331
- this.pageCache.delete(path3);
10280
+ if (!await this.provider.exists(path5)) {
10281
+ this.pageCache.delete(path5);
9332
10282
  return true;
9333
10283
  }
9334
10284
  } catch {
@@ -9351,10 +10301,10 @@ var PageService = class {
9351
10301
  * await pageService.deletePage('/old-page', true);
9352
10302
  * ```
9353
10303
  */
9354
- async deletePage(path3, deleteFromStorage = false) {
9355
- this.pageCache.delete(path3);
10304
+ async deletePage(path5, deleteFromStorage = false) {
10305
+ this.pageCache.delete(path5);
9356
10306
  if (deleteFromStorage && this.provider) {
9357
- await this.provider.delete(path3);
10307
+ await this.provider.delete(path5);
9358
10308
  }
9359
10309
  }
9360
10310
  /**
@@ -9382,26 +10332,34 @@ var PageService = class {
9382
10332
  *
9383
10333
  * @example
9384
10334
  * ```typescript
9385
- * const pages = pageService.getAllPagesWithInfo();
10335
+ * const pages = await pageService.getAllPagesWithInfo();
9386
10336
  * // [{ path: "/", isDraft: false }, { path: "/about", isDraft: true }]
9387
10337
  * ```
9388
10338
  */
9389
- getAllPagesWithInfo() {
10339
+ async getAllPagesWithInfo() {
9390
10340
  const paths = this.pageCache.keys().filter((p) => !p.startsWith("/_sketch/"));
9391
- return paths.map((path3) => {
9392
- const pageData = this.getPageData(path3);
9393
- const pathWithoutSlash = path3 === "/" ? "index" : path3.substring(1);
9394
- const slashIndex = pathWithoutSlash.lastIndexOf("/");
9395
- const folder = slashIndex > 0 ? pathWithoutSlash.substring(0, slashIndex) : void 0;
9396
- const metaSource = pageData?.meta?.source;
9397
- const source = metaSource === "cms" || metaSource === "ssr" ? metaSource : void 0;
9398
- return {
9399
- path: path3,
9400
- isDraft: pageData?.meta?.draft === true,
9401
- ...folder ? { folder } : {},
9402
- ...source ? { source } : {}
9403
- };
9404
- });
10341
+ return Promise.all(
10342
+ paths.map(async (path5) => {
10343
+ const pageData = this.getPageData(path5);
10344
+ const pathWithoutSlash = path5 === "/" ? "index" : path5.substring(1);
10345
+ const slashIndex = pathWithoutSlash.lastIndexOf("/");
10346
+ const folder = slashIndex > 0 ? pathWithoutSlash.substring(0, slashIndex) : void 0;
10347
+ const metaSource = pageData?.meta?.source;
10348
+ const source = metaSource === "cms" || metaSource === "ssr" ? metaSource : void 0;
10349
+ let sourcePath;
10350
+ if (source === "cms" && this.provider?.resolveSourcePath) {
10351
+ const abs = await this.provider.resolveSourcePath(path5);
10352
+ if (abs) sourcePath = relative(getProjectRoot(), abs).split(sep).join("/");
10353
+ }
10354
+ return {
10355
+ path: path5,
10356
+ isDraft: pageData?.meta?.draft === true,
10357
+ ...folder ? { folder } : {},
10358
+ ...source ? { source } : {},
10359
+ ...sourcePath ? { sourcePath } : {}
10360
+ };
10361
+ })
10362
+ );
9405
10363
  }
9406
10364
  /**
9407
10365
  * Get line map for a page
@@ -9411,8 +10369,8 @@ var PageService = class {
9411
10369
  * @param path - Page path (e.g., "/" or "/about")
9412
10370
  * @returns LineMap object or undefined if page not found
9413
10371
  */
9414
- getLineMap(path3) {
9415
- return this.pageCache.getLineMap(path3);
10372
+ getLineMap(path5) {
10373
+ return this.pageCache.getLineMap(path5);
9416
10374
  }
9417
10375
  /**
9418
10376
  * Pages base directory — provider-aware (e.g. `src/pages` for astro projects),
@@ -9436,12 +10394,12 @@ var PageService = class {
9436
10394
  */
9437
10395
  getAllFolders() {
9438
10396
  const pagesDir = this.pagesBaseDir();
9439
- if (!existsSync5(pagesDir)) {
10397
+ if (!existsSync6(pagesDir)) {
9440
10398
  return [];
9441
10399
  }
9442
10400
  const collectFolders = (dir, prefix) => {
9443
10401
  try {
9444
- const entries = readdirSync2(dir, { withFileTypes: true });
10402
+ const entries = readdirSync3(dir, { withFileTypes: true });
9445
10403
  const result = [];
9446
10404
  for (const entry of entries) {
9447
10405
  if (!entry.isDirectory()) continue;
@@ -9479,7 +10437,7 @@ var PageService = class {
9479
10437
  }
9480
10438
  const pagesDir = this.pagesBaseDir();
9481
10439
  const folderPath = join4(pagesDir, trimmed);
9482
- if (existsSync5(folderPath)) {
10440
+ if (existsSync6(folderPath)) {
9483
10441
  throw new Error("Folder already exists");
9484
10442
  }
9485
10443
  mkdirSync(folderPath, { recursive: true });
@@ -9507,11 +10465,11 @@ var PageService = class {
9507
10465
  }
9508
10466
  if (newFolder) {
9509
10467
  const targetDir = join4(pagesDir, newFolder);
9510
- if (!existsSync5(targetDir)) {
10468
+ if (!existsSync6(targetDir)) {
9511
10469
  mkdirSync(targetDir, { recursive: true });
9512
10470
  }
9513
10471
  }
9514
- if (!existsSync5(sourceFile)) {
10472
+ if (!existsSync6(sourceFile)) {
9515
10473
  throw new Error(`Page not found: ${pagePath}`);
9516
10474
  }
9517
10475
  await rename2(sourceFile, targetFile);
@@ -9526,7 +10484,7 @@ var PageService = class {
9526
10484
  const sourceDir = slashIndex >= 0 ? join4(pagesDir, pageName.substring(0, slashIndex)) : null;
9527
10485
  if (sourceDir && sourceDir !== pagesDir) {
9528
10486
  try {
9529
- const remaining = readdirSync2(sourceDir);
10487
+ const remaining = readdirSync3(sourceDir);
9530
10488
  if (remaining.length === 0) {
9531
10489
  rmdirSync(sourceDir);
9532
10490
  }
@@ -9574,10 +10532,10 @@ var PageService = class {
9574
10532
  const ext = this.pageExt();
9575
10533
  const sourceFile = join4(pagesDir, `${oldName}${ext}`);
9576
10534
  const targetFile = join4(pagesDir, `${newName}${ext}`);
9577
- if (!existsSync5(sourceFile)) {
10535
+ if (!existsSync6(sourceFile)) {
9578
10536
  throw new Error(`Page not found: ${oldPath}`);
9579
10537
  }
9580
- if (existsSync5(targetFile)) {
10538
+ if (existsSync6(targetFile)) {
9581
10539
  throw new Error(`Page already exists: ${newPath}`);
9582
10540
  }
9583
10541
  await rename2(sourceFile, targetFile);
@@ -9605,17 +10563,17 @@ var PageService = class {
9605
10563
  getSlugMappings() {
9606
10564
  const mappings = [];
9607
10565
  const paths = this.pageCache.keys();
9608
- for (const path3 of paths) {
9609
- const pageData = this.getPageData(path3);
10566
+ for (const path5 of paths) {
10567
+ const pageData = this.getPageData(path5);
9610
10568
  if (pageData?.meta?.slugs) {
9611
- const pageId = path3 === "/" ? "index" : path3.substring(1);
10569
+ const pageId = path5 === "/" ? "index" : path5.substring(1);
9612
10570
  mappings.push({
9613
10571
  pageId,
9614
10572
  slugs: pageData.meta.slugs
9615
10573
  });
9616
10574
  } else {
9617
- const pageId = path3 === "/" ? "index" : path3.substring(1);
9618
- const defaultSlug = path3 === "/" ? "" : path3.substring(1);
10575
+ const pageId = path5 === "/" ? "index" : path5.substring(1);
10576
+ const defaultSlug = path5 === "/" ? "" : path5.substring(1);
9619
10577
  mappings.push({
9620
10578
  pageId,
9621
10579
  slugs: { _default: defaultSlug }
@@ -9628,7 +10586,7 @@ var PageService = class {
9628
10586
 
9629
10587
  // lib/server/services/componentService.ts
9630
10588
  import { join as join5 } from "node:path";
9631
- import { existsSync as existsSync6, mkdirSync as mkdirSync2, readdirSync as readdirSync3, rmdirSync as rmdirSync2 } from "node:fs";
10589
+ import { existsSync as existsSync7, mkdirSync as mkdirSync2, readdirSync as readdirSync4, rmdirSync as rmdirSync2 } from "node:fs";
9632
10590
  var ComponentService = class {
9633
10591
  components = /* @__PURE__ */ new Map();
9634
10592
  componentCategories = /* @__PURE__ */ new Map();
@@ -9637,6 +10595,36 @@ var ComponentService = class {
9637
10595
  writer;
9638
10596
  loadErrors = /* @__PURE__ */ new Map();
9639
10597
  loadWarnings = [];
10598
+ /** Per-component serialization of whole-file re-emits. @see withComponentWriteLock */
10599
+ writeLocks = /* @__PURE__ */ new Map();
10600
+ /**
10601
+ * Serialize whole-file re-emits for one component.
10602
+ *
10603
+ * `saveComponentCSS` and `saveComponentJavaScript` each read the in-memory
10604
+ * definition, re-emit the ENTIRE `.astro` file, then write the result back.
10605
+ * Fired concurrently — the Custom Code modal's Save issues a JS save AND a CSS
10606
+ * save together — their read-modify-write races: each reads a snapshot taken
10607
+ * before the other's `components.set`, so the writer that lands last clobbers
10608
+ * the other field with a stale value (a fresh CSS edit silently reverting is
10609
+ * the visible symptom). Chaining per component name makes each op observe the
10610
+ * previous op's committed state. Prior failures are swallowed on the chain tail
10611
+ * so one bad write can't wedge the queue.
10612
+ */
10613
+ withComponentWriteLock(name, fn) {
10614
+ const prev = this.writeLocks.get(name) ?? Promise.resolve();
10615
+ const run = prev.then(fn, fn);
10616
+ const tail = run.then(
10617
+ () => {
10618
+ },
10619
+ () => {
10620
+ }
10621
+ );
10622
+ this.writeLocks.set(name, tail);
10623
+ tail.finally(() => {
10624
+ if (this.writeLocks.get(name) === tail) this.writeLocks.delete(name);
10625
+ });
10626
+ return run;
10627
+ }
9640
10628
  /**
9641
10629
  * Creates a new ComponentService instance
9642
10630
  *
@@ -9891,12 +10879,12 @@ var ComponentService = class {
9891
10879
  */
9892
10880
  findComponentCategoryOnDisk(name) {
9893
10881
  const componentsDir = this.componentsBaseDir();
9894
- if (!existsSync6(componentsDir)) return void 0;
10882
+ if (!existsSync7(componentsDir)) return void 0;
9895
10883
  const ext = this.writer ? null : ".json";
9896
10884
  const existsAt = (dir) => {
9897
- if (ext) return existsSync6(join5(dir, `${name}${ext}`));
10885
+ if (ext) return existsSync7(join5(dir, `${name}${ext}`));
9898
10886
  try {
9899
- return readdirSync3(dir, { withFileTypes: true }).some(
10887
+ return readdirSync4(dir, { withFileTypes: true }).some(
9900
10888
  (e) => e.isFile() && (e.name === `${name}.json` || e.name === `${name}.astro`)
9901
10889
  );
9902
10890
  } catch {
@@ -9905,7 +10893,7 @@ var ComponentService = class {
9905
10893
  };
9906
10894
  if (existsAt(componentsDir)) return void 0;
9907
10895
  try {
9908
- for (const entry of readdirSync3(componentsDir, { withFileTypes: true })) {
10896
+ for (const entry of readdirSync4(componentsDir, { withFileTypes: true })) {
9909
10897
  if (entry.isDirectory() && existsAt(join5(componentsDir, entry.name))) {
9910
10898
  return entry.name;
9911
10899
  }
@@ -9986,7 +10974,7 @@ var ComponentService = class {
9986
10974
  await this.writer.writeComponent(componentDir, name, dataWithJS);
9987
10975
  } else {
9988
10976
  const writeFile3 = this.fs ? this.fs.writeFile.bind(this.fs) : (await import("node:fs/promises")).writeFile;
9989
- if (targetCategory && !existsSync6(componentDir)) {
10977
+ if (targetCategory && !existsSync7(componentDir)) {
9990
10978
  mkdirSync2(componentDir, { recursive: true });
9991
10979
  }
9992
10980
  const filePath = join5(componentDir, `${name}.json`);
@@ -10015,10 +11003,12 @@ var ComponentService = class {
10015
11003
  async saveComponentJavaScript(name, javascript) {
10016
11004
  const componentDir = this.getComponentDir(name);
10017
11005
  if (this.writer) {
10018
- const current = this.components.get(name);
10019
- await this.writer.writeJavaScript(componentDir, name, javascript || "", current);
10020
- const next = current ? { ...current, component: { ...current.component, javascript: javascript || "" } } : { component: { javascript: javascript || "" } };
10021
- this.components.set(name, next);
11006
+ await this.withComponentWriteLock(name, async () => {
11007
+ const current = this.components.get(name);
11008
+ await this.writer.writeJavaScript(componentDir, name, javascript || "", current);
11009
+ const next = current ? { ...current, component: { ...current.component, javascript: javascript || "" } } : { component: { javascript: javascript || "" } };
11010
+ this.components.set(name, next);
11011
+ });
10022
11012
  return;
10023
11013
  }
10024
11014
  const writeFile3 = this.fs ? this.fs.writeFile.bind(this.fs) : (await import("node:fs/promises")).writeFile;
@@ -10061,10 +11051,12 @@ var ComponentService = class {
10061
11051
  async saveComponentCSS(name, css) {
10062
11052
  const componentDir = this.getComponentDir(name);
10063
11053
  if (this.writer) {
10064
- const current = this.components.get(name);
10065
- await this.writer.writeCSS(componentDir, name, css || "", current);
10066
- const next = current ? { ...current, component: { ...current.component, css: css || "" } } : { component: { css: css || "" } };
10067
- this.components.set(name, next);
11054
+ await this.withComponentWriteLock(name, async () => {
11055
+ const current = this.components.get(name);
11056
+ await this.writer.writeCSS(componentDir, name, css || "", current);
11057
+ const next = current ? { ...current, component: { ...current.component, css: css || "" } } : { component: { css: css || "" } };
11058
+ this.components.set(name, next);
11059
+ });
10068
11060
  return;
10069
11061
  }
10070
11062
  const writeFile3 = this.fs ? this.fs.writeFile.bind(this.fs) : (await import("node:fs/promises")).writeFile;
@@ -10103,11 +11095,11 @@ var ComponentService = class {
10103
11095
  */
10104
11096
  getAllFolders() {
10105
11097
  const componentsDir = this.componentsBaseDir();
10106
- if (!existsSync6(componentsDir)) {
11098
+ if (!existsSync7(componentsDir)) {
10107
11099
  return [];
10108
11100
  }
10109
11101
  try {
10110
- const entries = readdirSync3(componentsDir, { withFileTypes: true });
11102
+ const entries = readdirSync4(componentsDir, { withFileTypes: true });
10111
11103
  return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
10112
11104
  } catch {
10113
11105
  return [];
@@ -10139,7 +11131,7 @@ var ComponentService = class {
10139
11131
  }
10140
11132
  const componentsDir = this.componentsBaseDir();
10141
11133
  const folderPath = join5(componentsDir, folderName);
10142
- if (existsSync6(folderPath)) {
11134
+ if (existsSync7(folderPath)) {
10143
11135
  throw new Error("Folder already exists");
10144
11136
  }
10145
11137
  mkdirSync2(folderPath, { recursive: true });
@@ -10177,7 +11169,7 @@ var ComponentService = class {
10177
11169
  if (this.writer) {
10178
11170
  await this.writer.moveComponent(sourceDir, targetDir, name);
10179
11171
  } else {
10180
- if (targetCategory && !existsSync6(targetDir)) {
11172
+ if (targetCategory && !existsSync7(targetDir)) {
10181
11173
  mkdirSync2(targetDir, { recursive: true });
10182
11174
  }
10183
11175
  const extensions = [".json", ".js", ".css"];
@@ -10185,7 +11177,7 @@ var ComponentService = class {
10185
11177
  for (const ext of extensions) {
10186
11178
  const sourcePath = join5(sourceDir, `${name}${ext}`);
10187
11179
  const targetPath = join5(targetDir, `${name}${ext}`);
10188
- if (existsSync6(sourcePath)) {
11180
+ if (existsSync7(sourcePath)) {
10189
11181
  await rename2(sourcePath, targetPath);
10190
11182
  }
10191
11183
  }
@@ -10193,7 +11185,7 @@ var ComponentService = class {
10193
11185
  this.componentCategories.set(name, targetCategory);
10194
11186
  if (currentCategory) {
10195
11187
  try {
10196
- const remaining = readdirSync3(sourceDir);
11188
+ const remaining = readdirSync4(sourceDir);
10197
11189
  if (remaining.length === 0) {
10198
11190
  rmdirSync2(sourceDir);
10199
11191
  }
@@ -10248,7 +11240,7 @@ var ComponentService = class {
10248
11240
  await this.writer.moveComponent(dir, dir, oldName, trimmedNew);
10249
11241
  } else {
10250
11242
  for (const ext of [".json", ".js", ".css"]) {
10251
- if (existsSync6(join5(dir, `${trimmedNew}${ext}`))) {
11243
+ if (existsSync7(join5(dir, `${trimmedNew}${ext}`))) {
10252
11244
  throw new Error(`File "${trimmedNew}${ext}" already exists in components/${category ?? ""}`);
10253
11245
  }
10254
11246
  }
@@ -10256,7 +11248,7 @@ var ComponentService = class {
10256
11248
  for (const ext of [".json", ".js", ".css"]) {
10257
11249
  const src = join5(dir, `${oldName}${ext}`);
10258
11250
  const dst = join5(dir, `${trimmedNew}${ext}`);
10259
- if (existsSync6(src)) {
11251
+ if (existsSync7(src)) {
10260
11252
  await rename2(src, dst);
10261
11253
  }
10262
11254
  }
@@ -10290,11 +11282,11 @@ var ComponentService = class {
10290
11282
  let pageRefs = 0;
10291
11283
  if (pageService) {
10292
11284
  const pagePaths = pageService.getAllPagePaths();
10293
- for (const path3 of pagePaths) {
10294
- const pageData = pageService.getPageData(path3);
11285
+ for (const path5 of pagePaths) {
11286
+ const pageData = pageService.getPageData(path5);
10295
11287
  if (!pageData || !("root" in pageData) || !pageData.root) continue;
10296
11288
  if (rewriteComponentRefs(pageData.root, oldName, trimmedNew)) {
10297
- await pageService.savePage(path3, pageData);
11289
+ await pageService.savePage(path5, pageData);
10298
11290
  pageRefs++;
10299
11291
  }
10300
11292
  }
@@ -10452,12 +11444,12 @@ var CMSService = class {
10452
11444
  * @param locale - Optional locale for i18n slug matching
10453
11445
  * @returns CMSRouteMatch if matched, null otherwise
10454
11446
  */
10455
- async matchRoute(path3, locale) {
11447
+ async matchRoute(path5, locale) {
10456
11448
  if (!this.provider) {
10457
11449
  return null;
10458
11450
  }
10459
11451
  for (const route of this.routePatterns) {
10460
- const match = path3.match(route.regex);
11452
+ const match = path5.match(route.regex);
10461
11453
  if (match) {
10462
11454
  const urlSlug = match[route.slugGroup];
10463
11455
  if (urlSlug === void 0) continue;
@@ -10566,11 +11558,11 @@ var CMSService = class {
10566
11558
  if (options?.excludeDrafts && isItemDraftForLocale(item, locale)) continue;
10567
11559
  const localizedSlug = slugValue[locale];
10568
11560
  if (localizedSlug) {
10569
- const path3 = urlPattern.replace("{{slug}}", localizedSlug);
11561
+ const path5 = urlPattern.replace("{{slug}}", localizedSlug);
10570
11562
  if (locale === defaultLocale) {
10571
- urls.push(path3);
11563
+ urls.push(path5);
10572
11564
  } else {
10573
- urls.push(`/${locale}${path3}`);
11565
+ urls.push(`/${locale}${path5}`);
10574
11566
  }
10575
11567
  }
10576
11568
  }
@@ -10822,7 +11814,7 @@ var CMSService = class {
10822
11814
  };
10823
11815
 
10824
11816
  // lib/server/providers/fileSystemCMSProvider.ts
10825
- import { existsSync as existsSync7, readdirSync as readdirSync4, mkdirSync as mkdirSync3 } from "node:fs";
11817
+ import { existsSync as existsSync8, readdirSync as readdirSync5, mkdirSync as mkdirSync3 } from "node:fs";
10826
11818
  import { join as join6 } from "node:path";
10827
11819
  var log6 = createLogger("FileSystemCMSProvider");
10828
11820
  var DRAFT_FILE_SUFFIX = `${CMS_DRAFT_SUFFIX}.json`;
@@ -10895,10 +11887,10 @@ var FileSystemCMSProvider = class {
10895
11887
  return this.schemaCache;
10896
11888
  }
10897
11889
  const schemas = /* @__PURE__ */ new Map();
10898
- if (!existsSync7(this.templatesDir)) {
11890
+ if (!existsSync8(this.templatesDir)) {
10899
11891
  return schemas;
10900
11892
  }
10901
- const files = readdirSync4(this.templatesDir);
11893
+ const files = readdirSync5(this.templatesDir);
10902
11894
  const jsonFiles = files.filter((f) => f.endsWith(".json"));
10903
11895
  const results = await Promise.all(
10904
11896
  jsonFiles.map(async (file) => {
@@ -10928,10 +11920,10 @@ var FileSystemCMSProvider = class {
10928
11920
  async getItems(collection) {
10929
11921
  this.validateCollection(collection);
10930
11922
  const collectionDir = join6(this.cmsDir, collection);
10931
- if (!existsSync7(collectionDir)) {
11923
+ if (!existsSync8(collectionDir)) {
10932
11924
  return [];
10933
11925
  }
10934
- const files = readdirSync4(collectionDir);
11926
+ const files = readdirSync5(collectionDir);
10935
11927
  const jsonFiles = files.filter((f) => f.endsWith(".json") && !f.endsWith(DRAFT_FILE_SUFFIX));
10936
11928
  const results = await Promise.all(
10937
11929
  jsonFiles.map(async (file) => {
@@ -11021,7 +12013,7 @@ var FileSystemCMSProvider = class {
11021
12013
  }
11022
12014
  this.validateFilename(filename);
11023
12015
  const collectionDir = join6(this.cmsDir, collection);
11024
- if (!existsSync7(collectionDir)) {
12016
+ if (!existsSync8(collectionDir)) {
11025
12017
  mkdirSync3(collectionDir, { recursive: true });
11026
12018
  }
11027
12019
  const itemData = stripTransient(item);
@@ -11036,11 +12028,11 @@ var FileSystemCMSProvider = class {
11036
12028
  this.validateFilename(filename);
11037
12029
  const { unlink } = await import("node:fs/promises");
11038
12030
  const publishedPath = join6(this.cmsDir, collection, `${filename}.json`);
11039
- if (existsSync7(publishedPath)) {
12031
+ if (existsSync8(publishedPath)) {
11040
12032
  await unlink(publishedPath);
11041
12033
  }
11042
12034
  const draftPath = this.draftPath(collection, filename);
11043
- if (existsSync7(draftPath)) {
12035
+ if (existsSync8(draftPath)) {
11044
12036
  await unlink(draftPath);
11045
12037
  }
11046
12038
  }
@@ -11074,8 +12066,8 @@ var FileSystemCMSProvider = class {
11074
12066
  async getAllDrafts(collection) {
11075
12067
  this.validateCollection(collection);
11076
12068
  const collectionDir = join6(this.cmsDir, collection);
11077
- if (!existsSync7(collectionDir)) return [];
11078
- const files = readdirSync4(collectionDir).filter((f) => f.endsWith(DRAFT_FILE_SUFFIX));
12069
+ if (!existsSync8(collectionDir)) return [];
12070
+ const files = readdirSync5(collectionDir).filter((f) => f.endsWith(DRAFT_FILE_SUFFIX));
11079
12071
  const results = await Promise.all(
11080
12072
  files.map(async (file) => {
11081
12073
  const filePath = join6(collectionDir, file);
@@ -11100,7 +12092,7 @@ var FileSystemCMSProvider = class {
11100
12092
  async hasDraft(collection, filename) {
11101
12093
  this.validateCollection(collection);
11102
12094
  this.validateFilename(filename);
11103
- return existsSync7(this.draftPath(collection, filename));
12095
+ return existsSync8(this.draftPath(collection, filename));
11104
12096
  }
11105
12097
  /**
11106
12098
  * Save the draft version of an item. Loose validation — drafts may have
@@ -11135,7 +12127,7 @@ var FileSystemCMSProvider = class {
11135
12127
  }
11136
12128
  this.validateFilename(filename);
11137
12129
  const collectionDir = join6(this.cmsDir, collection);
11138
- if (!existsSync7(collectionDir)) {
12130
+ if (!existsSync8(collectionDir)) {
11139
12131
  mkdirSync3(collectionDir, { recursive: true });
11140
12132
  }
11141
12133
  const itemData = stripTransient(item);
@@ -11155,7 +12147,7 @@ var FileSystemCMSProvider = class {
11155
12147
  this.validateFilename(filename);
11156
12148
  const { unlink } = await import("node:fs/promises");
11157
12149
  const filePath = this.draftPath(collection, filename);
11158
- if (existsSync7(filePath)) {
12150
+ if (existsSync8(filePath)) {
11159
12151
  await unlink(filePath);
11160
12152
  }
11161
12153
  }
@@ -11184,13 +12176,13 @@ var FileSystemCMSProvider = class {
11184
12176
  throw new Error(`Cannot publish invalid draft: ${messages}`);
11185
12177
  }
11186
12178
  const collectionDir = join6(this.cmsDir, collection);
11187
- if (!existsSync7(collectionDir)) {
12179
+ if (!existsSync8(collectionDir)) {
11188
12180
  mkdirSync3(collectionDir, { recursive: true });
11189
12181
  }
11190
12182
  const itemData = stripTransient(validation.data);
11191
12183
  const publishedPath = join6(collectionDir, `${filename}.json`);
11192
12184
  await writeFile3(publishedPath, JSON.stringify(itemData, null, 2), "utf-8");
11193
- if (existsSync7(draftFilePath)) {
12185
+ if (existsSync8(draftFilePath)) {
11194
12186
  await unlink(draftFilePath);
11195
12187
  }
11196
12188
  return normalizeItem(itemData, filename);
@@ -11211,18 +12203,18 @@ var FileSystemCMSProvider = class {
11211
12203
  );
11212
12204
  }
11213
12205
  const { writeFile: writeFile3, mkdir } = await import("node:fs/promises");
11214
- if (!existsSync7(this.templatesDir)) {
12206
+ if (!existsSync8(this.templatesDir)) {
11215
12207
  await mkdir(this.templatesDir, { recursive: true });
11216
12208
  }
11217
12209
  const pageFilePath = join6(this.templatesDir, `${collectionId}.json`);
11218
- if (existsSync7(pageFilePath)) {
12210
+ if (existsSync8(pageFilePath)) {
11219
12211
  throw new Error(`Page file already exists: templates/${collectionId}.json`);
11220
12212
  }
11221
12213
  await writeFile3(pageFilePath, JSON.stringify(pageData, null, 2), "utf-8");
11222
12214
  const schemaSource = pageData?.meta?.cms?.source;
11223
12215
  if (schemaSource !== "sanity") {
11224
12216
  const collectionDir = join6(this.cmsDir, collectionId);
11225
- if (!existsSync7(collectionDir)) {
12217
+ if (!existsSync8(collectionDir)) {
11226
12218
  await mkdir(collectionDir, { recursive: true });
11227
12219
  }
11228
12220
  }
@@ -11238,12 +12230,12 @@ var FileSystemCMSProvider = class {
11238
12230
  `Invalid collection ID: "${collectionId}". Collection IDs must contain only letters, numbers, hyphens, and underscores.`
11239
12231
  );
11240
12232
  }
11241
- const { readFile: readFile2, writeFile: writeFile3 } = await import("node:fs/promises");
12233
+ const { readFile: readFile3, writeFile: writeFile3 } = await import("node:fs/promises");
11242
12234
  const pageFilePath = join6(this.templatesDir, `${collectionId}.json`);
11243
- if (!existsSync7(pageFilePath)) {
12235
+ if (!existsSync8(pageFilePath)) {
11244
12236
  throw new Error(`Collection not found: ${collectionId}`);
11245
12237
  }
11246
- const content = await readFile2(pageFilePath, "utf-8");
12238
+ const content = await readFile3(pageFilePath, "utf-8");
11247
12239
  const pageData = JSON.parse(content);
11248
12240
  pageData.meta.cms = {
11249
12241
  ...pageData.meta.cms,
@@ -11262,26 +12254,26 @@ var PageCache = class {
11262
12254
  /**
11263
12255
  * Store page content and line map in cache
11264
12256
  */
11265
- set(path3, content, lineMap) {
11266
- this.cache.set(path3, { content, lineMap });
12257
+ set(path5, content, lineMap) {
12258
+ this.cache.set(path5, { content, lineMap });
11267
12259
  }
11268
12260
  /**
11269
12261
  * Get cached page data
11270
12262
  */
11271
- get(path3) {
11272
- return this.cache.get(path3);
12263
+ get(path5) {
12264
+ return this.cache.get(path5);
11273
12265
  }
11274
12266
  /**
11275
12267
  * Get page content from cache
11276
12268
  */
11277
- getContent(path3) {
11278
- return this.cache.get(path3)?.content;
12269
+ getContent(path5) {
12270
+ return this.cache.get(path5)?.content;
11279
12271
  }
11280
12272
  /**
11281
12273
  * Get line map from cache
11282
12274
  */
11283
- getLineMap(path3) {
11284
- return this.cache.get(path3)?.lineMap;
12275
+ getLineMap(path5) {
12276
+ return this.cache.get(path5)?.lineMap;
11285
12277
  }
11286
12278
  /**
11287
12279
  * Get line range for a specific element path
@@ -11292,14 +12284,14 @@ var PageCache = class {
11292
12284
  /**
11293
12285
  * Delete page from cache
11294
12286
  */
11295
- delete(path3) {
11296
- this.cache.delete(path3);
12287
+ delete(path5) {
12288
+ this.cache.delete(path5);
11297
12289
  }
11298
12290
  /**
11299
12291
  * Check if page exists in cache
11300
12292
  */
11301
- has(path3) {
11302
- return this.cache.has(path3);
12293
+ has(path5) {
12294
+ return this.cache.has(path5);
11303
12295
  }
11304
12296
  /**
11305
12297
  * Get all page paths
@@ -11338,21 +12330,21 @@ var DraftPageStore = class {
11338
12330
  * Store a draft for a page path. The value is the raw JSON string the
11339
12331
  * SSR pipeline expects, matching PageService.getPage()'s contract.
11340
12332
  */
11341
- set(path3, content) {
11342
- this.drafts.set(path3, content);
12333
+ set(path5, content) {
12334
+ this.drafts.set(path5, content);
11343
12335
  }
11344
12336
  /**
11345
12337
  * Get the current draft string for a page path, if any.
11346
12338
  */
11347
- get(path3) {
11348
- return this.drafts.get(path3);
12339
+ get(path5) {
12340
+ return this.drafts.get(path5);
11349
12341
  }
11350
12342
  /**
11351
12343
  * Drop the draft for a specific path. Called when the page is saved to
11352
12344
  * disk so subsequent renders read the persisted version.
11353
12345
  */
11354
- clear(path3) {
11355
- this.drafts.delete(path3);
12346
+ clear(path5) {
12347
+ this.drafts.delete(path5);
11356
12348
  }
11357
12349
  /**
11358
12350
  * Drop every draft. Used on shutdown / project switch.
@@ -11360,14 +12352,19 @@ var DraftPageStore = class {
11360
12352
  clearAll() {
11361
12353
  this.drafts.clear();
11362
12354
  }
11363
- has(path3) {
11364
- return this.drafts.has(path3);
12355
+ has(path5) {
12356
+ return this.drafts.has(path5);
11365
12357
  }
11366
12358
  };
11367
12359
 
11368
12360
  // lib/server/fileWatcher.ts
11369
- import { watch, existsSync as existsSync8, statSync, readdirSync as readdirSync5 } from "node:fs";
11370
- import { basename as basename2, dirname as dirname2, join as join7, relative } from "node:path";
12361
+ import { watch, existsSync as existsSync9, statSync as statSync2, readdirSync as readdirSync6 } from "node:fs";
12362
+ import { basename as basename2, dirname as dirname2, join as join7, relative as relative2 } from "node:path";
12363
+
12364
+ // lib/shared/utils/fileUtils.ts
12365
+ var collapseFolderIndex = (name) => name.endsWith("/index") ? name.slice(0, -"/index".length) : name;
12366
+
12367
+ // lib/server/fileWatcher.ts
11371
12368
  function watchRecursive(root, listener) {
11372
12369
  const nested = [];
11373
12370
  const watchedDirs = /* @__PURE__ */ new Set();
@@ -11375,17 +12372,17 @@ function watchRecursive(root, listener) {
11375
12372
  const coverNewDir = (abs) => {
11376
12373
  let isDir = false;
11377
12374
  try {
11378
- isDir = statSync(abs).isDirectory();
12375
+ isDir = statSync2(abs).isDirectory();
11379
12376
  } catch {
11380
12377
  return;
11381
12378
  }
11382
12379
  if (!isDir || watchedDirs.has(abs)) return;
11383
12380
  attachDir(abs);
11384
12381
  try {
11385
- for (const e of readdirSync5(abs, { withFileTypes: true })) {
12382
+ for (const e of readdirSync6(abs, { withFileTypes: true })) {
11386
12383
  const child = join7(abs, e.name);
11387
12384
  if (e.isDirectory()) coverNewDir(child);
11388
- else listener("rename", relative(root, child));
12385
+ else listener("rename", relative2(root, child));
11389
12386
  }
11390
12387
  } catch {
11391
12388
  }
@@ -11398,7 +12395,7 @@ function watchRecursive(root, listener) {
11398
12395
  w = watch(absDir, { recursive: true }, (event, filename) => {
11399
12396
  if (!filename) return;
11400
12397
  const abs = join7(absDir, filename);
11401
- listener(event, relative(root, abs));
12398
+ listener(event, relative2(root, abs));
11402
12399
  coverNewDir(abs);
11403
12400
  });
11404
12401
  } catch {
@@ -11423,19 +12420,19 @@ function watchRecursive(root, listener) {
11423
12420
  return handle;
11424
12421
  }
11425
12422
  function attachWhenDirExists(dirPath, attach, setWatcher) {
11426
- if (existsSync8(dirPath)) {
12423
+ if (existsSync9(dirPath)) {
11427
12424
  setWatcher(attach());
11428
12425
  return;
11429
12426
  }
11430
12427
  const parentDir = dirname2(dirPath);
11431
12428
  const targetName = basename2(dirPath);
11432
- if (!existsSync8(parentDir)) {
12429
+ if (!existsSync9(parentDir)) {
11433
12430
  return;
11434
12431
  }
11435
12432
  let parentWatcher = null;
11436
12433
  parentWatcher = watch(parentDir, (_event, filename) => {
11437
12434
  if (filename !== targetName) return;
11438
- if (!existsSync8(dirPath)) return;
12435
+ if (!existsSync9(dirPath)) return;
11439
12436
  parentWatcher?.close();
11440
12437
  parentWatcher = null;
11441
12438
  setWatcher(attach());
@@ -11451,6 +12448,7 @@ var FileWatcher = class {
11451
12448
  templatesWatcher = null;
11452
12449
  colorsWatcher = null;
11453
12450
  variablesWatcher = null;
12451
+ themeCssWatcher = null;
11454
12452
  enumsWatcher = null;
11455
12453
  cmsWatcher = null;
11456
12454
  imagesWatcher = null;
@@ -11465,7 +12463,7 @@ var FileWatcher = class {
11465
12463
  * Watches both .json and .js files to detect component definition and JavaScript changes
11466
12464
  */
11467
12465
  watchComponents(dirPath = projectPaths.components()) {
11468
- if (!existsSync8(dirPath)) {
12466
+ if (!existsSync9(dirPath)) {
11469
12467
  return;
11470
12468
  }
11471
12469
  this.componentsWatcher = watchRecursive(dirPath, async (_event, filename) => {
@@ -11481,7 +12479,7 @@ var FileWatcher = class {
11481
12479
  * Start watching pages directory
11482
12480
  */
11483
12481
  watchPages(dirPath = projectPaths.pages()) {
11484
- if (!existsSync8(dirPath)) {
12482
+ if (!existsSync9(dirPath)) {
11485
12483
  return;
11486
12484
  }
11487
12485
  this.pagesWatcher = watchRecursive(dirPath, async (_event, filename) => {
@@ -11521,7 +12519,7 @@ var FileWatcher = class {
11521
12519
  * Start watching colors.json file
11522
12520
  */
11523
12521
  watchColors(filePath = projectPaths.colors()) {
11524
- if (!existsSync8(filePath)) {
12522
+ if (!existsSync9(filePath)) {
11525
12523
  return;
11526
12524
  }
11527
12525
  const dirPath = getProjectRoot();
@@ -11542,6 +12540,33 @@ var FileWatcher = class {
11542
12540
  }
11543
12541
  });
11544
12542
  }
12543
+ /**
12544
+ * Start watching `src/styles/theme.css` — the token source of truth for
12545
+ * astro-format projects, where colors AND variables share the one file
12546
+ * (colors.json / variables.json were migrated away, so the two watchers
12547
+ * above never fire there). An external edit (e.g. an AI tool adding tokens)
12548
+ * must invalidate both service caches and refresh connected clients, or the
12549
+ * new tokens only appear after a dev-server restart. Deferred attach:
12550
+ * `src/styles` may not exist yet (theme.css lands later via migration or an
12551
+ * external tool). No-op in legacy JSON projects (no `src/`).
12552
+ */
12553
+ watchThemeCss(dirPath = join7(getProjectRoot(), "src", "styles")) {
12554
+ attachWhenDirExists(
12555
+ dirPath,
12556
+ () => watch(dirPath, { recursive: false }, async (_event, filename) => {
12557
+ if (filename !== "theme.css") return;
12558
+ if (this.callbacks.onColorsChange) {
12559
+ await this.callbacks.onColorsChange();
12560
+ }
12561
+ if (this.callbacks.onVariablesChange) {
12562
+ await this.callbacks.onVariablesChange();
12563
+ }
12564
+ }),
12565
+ (w) => {
12566
+ this.themeCssWatcher = w;
12567
+ }
12568
+ );
12569
+ }
11545
12570
  /**
11546
12571
  * Start watching enums.json file
11547
12572
  */
@@ -11578,7 +12603,7 @@ var FileWatcher = class {
11578
12603
  * Start watching images directory for new source images
11579
12604
  */
11580
12605
  watchImages(dirPath = projectPaths.images()) {
11581
- if (!existsSync8(dirPath)) {
12606
+ if (!existsSync9(dirPath)) {
11582
12607
  return;
11583
12608
  }
11584
12609
  this.imagesWatcher = watch(dirPath, { recursive: false }, async (event, filename) => {
@@ -11605,7 +12630,7 @@ var FileWatcher = class {
11605
12630
  * Start watching libraries directory for CSS/JS changes
11606
12631
  */
11607
12632
  watchLibraries(dirPath = projectPaths.libraries()) {
11608
- if (!existsSync8(dirPath)) {
12633
+ if (!existsSync9(dirPath)) {
11609
12634
  return;
11610
12635
  }
11611
12636
  this.librariesWatcher = watchRecursive(dirPath, async (_event, filename) => {
@@ -11638,7 +12663,8 @@ var FileWatcher = class {
11638
12663
  if (this.callbacks.onCMSTemplateChange) await this.callbacks.onCMSTemplateChange();
11639
12664
  return;
11640
12665
  }
11641
- const pagePath = mapPageNameToPath(filename.replace(/\.astro$/, ""));
12666
+ const pageName = collapseFolderIndex(filename.replace(/\.astro$/, ""));
12667
+ const pagePath = mapPageNameToPath(pageName);
11642
12668
  if (this.callbacks.onPageChange) await this.callbacks.onPageChange(pagePath);
11643
12669
  }),
11644
12670
  (w) => {
@@ -11682,6 +12708,7 @@ var FileWatcher = class {
11682
12708
  this.watchTemplates();
11683
12709
  this.watchColors();
11684
12710
  this.watchVariables();
12711
+ this.watchThemeCss();
11685
12712
  this.watchEnums();
11686
12713
  this.watchCMS();
11687
12714
  this.watchImages();
@@ -11715,6 +12742,10 @@ var FileWatcher = class {
11715
12742
  this.variablesWatcher.close();
11716
12743
  this.variablesWatcher = null;
11717
12744
  }
12745
+ if (this.themeCssWatcher) {
12746
+ this.themeCssWatcher.close();
12747
+ this.themeCssWatcher = null;
12748
+ }
11718
12749
  if (this.enumsWatcher) {
11719
12750
  this.enumsWatcher.close();
11720
12751
  this.enumsWatcher = null;
@@ -11806,9 +12837,9 @@ var FileWatcherService = class {
11806
12837
  this.refreshSchemasTimer = null;
11807
12838
  await cmsService.refreshSchemas();
11808
12839
  const ids = new Set(cmsService.getAllSchemas().keys());
11809
- for (const path3 of pageService.getAllPagePaths()) {
11810
- if (path3.startsWith("/templates/") && !ids.has(path3.slice("/templates/".length))) {
11811
- await pageService.deletePage(path3);
12840
+ for (const path5 of pageService.getAllPagePaths()) {
12841
+ if (path5.startsWith("/templates/") && !ids.has(path5.slice("/templates/".length))) {
12842
+ await pageService.deletePage(path5);
11812
12843
  }
11813
12844
  }
11814
12845
  for (const id of ids) {
@@ -11862,20 +12893,20 @@ var FileWatcherService = class {
11862
12893
  };
11863
12894
 
11864
12895
  // lib/server/migrateTemplates.ts
11865
- import { existsSync as existsSync9 } from "node:fs";
12896
+ import { existsSync as existsSync10 } from "node:fs";
11866
12897
  import { rename } from "node:fs/promises";
11867
12898
  import { join as join8 } from "node:path";
11868
12899
  async function migrateTemplatesDirectory() {
11869
12900
  const oldDir = join8(projectPaths.pages(), "templates");
11870
12901
  const newDir = projectPaths.templates();
11871
- if (!existsSync9(oldDir)) return;
11872
- if (existsSync9(newDir)) return;
12902
+ if (!existsSync10(oldDir)) return;
12903
+ if (existsSync10(newDir)) return;
11873
12904
  await rename(oldDir, newDir);
11874
12905
  console.log("Migrated CMS templates: pages/templates/ \u2192 templates/");
11875
12906
  }
11876
12907
 
11877
12908
  // build-astro.ts
11878
- import { existsSync as existsSync10, readdirSync as readdirSync6, mkdirSync as mkdirSync4, rmSync, statSync as statSync2, copyFileSync, writeFileSync } from "node:fs";
12909
+ import { existsSync as existsSync11, readdirSync as readdirSync7, mkdirSync as mkdirSync4, rmSync, statSync as statSync3, copyFileSync, writeFileSync } from "node:fs";
11879
12910
  import { writeFile as writeFile2, readFile } from "node:fs/promises";
11880
12911
  import { join as join9 } from "node:path";
11881
12912
  import { createHash } from "node:crypto";
@@ -12961,8 +13992,8 @@ function injectInlineStyle(styleAttr, extraCss) {
12961
13992
  if (dynLiteralMatch) {
12962
13993
  const existing = dynLiteralMatch[1] ?? "";
12963
13994
  const trimmed = existing.trimEnd();
12964
- const sep = trimmed.length > 0 && !trimmed.endsWith(";") ? ";" : "";
12965
- return ` style={\`${existing}${sep}${extraCss}\`}`;
13995
+ const sep2 = trimmed.length > 0 && !trimmed.endsWith(";") ? ";" : "";
13996
+ return ` style={\`${existing}${sep2}${extraCss}\`}`;
12966
13997
  }
12967
13998
  const dynExprMatch = styleAttr.match(/^ style=\{([\s\S]*)\}$/);
12968
13999
  if (dynExprMatch) {
@@ -12970,8 +14001,8 @@ function injectInlineStyle(styleAttr, extraCss) {
12970
14001
  }
12971
14002
  return styleAttr.replace(/style="([^"]*)"/, (_, existing) => {
12972
14003
  const trimmed = existing.trimEnd();
12973
- const sep = trimmed.length > 0 && !trimmed.endsWith(";") ? ";" : "";
12974
- return `style="${existing}${sep}${extraCss}"`;
14004
+ const sep2 = trimmed.length > 0 && !trimmed.endsWith(";") ? ";" : "";
14005
+ return `style="${existing}${sep2}${extraCss}"`;
12975
14006
  });
12976
14007
  }
12977
14008
  var PICTURE_WIDTHS = [500, 800, 1080, 1600, 2400];
@@ -13697,7 +14728,7 @@ function emitLocaleListNode(node, ctx) {
13697
14728
  const code = localeConfig.code;
13698
14729
  const isCurrent = code === currentLocale;
13699
14730
  if (!showCurrent && isCurrent) continue;
13700
- const path3 = slugMap[code] || "/";
14731
+ const path5 = slugMap[code] || "/";
13701
14732
  const classes = isCurrent ? activeItemClasses : itemClasses;
13702
14733
  const classAttr = classes.length > 0 ? ` class="${classes.join(" ")}"` : "";
13703
14734
  const currentAttr = isCurrent ? ' data-current="true"' : ' data-current="false"';
@@ -13722,7 +14753,7 @@ function emitLocaleListNode(node, ctx) {
13722
14753
  }
13723
14754
  linkContent += `<div>${escapeJSX(displayText)}</div>`;
13724
14755
  links.push(
13725
- `${ind(ctx)} <a href="${escapeJSX(path3)}"${hreflangAttr}${currentAttr} data-locale="${escapeJSX(code)}"${classAttr}>${linkContent}</a>`
14756
+ `${ind(ctx)} <a href="${escapeJSX(path5)}"${hreflangAttr}${currentAttr} data-locale="${escapeJSX(code)}"${classAttr}>${linkContent}</a>`
13726
14757
  );
13727
14758
  }
13728
14759
  let linksContent;
@@ -14216,8 +15247,8 @@ function emitAstroPage(options) {
14216
15247
  }
14217
15248
  const componentImports = Array.from(ctx.imports).sort();
14218
15249
  for (const comp of componentImports) {
14219
- const path3 = componentImportPath(fileDepth, comp);
14220
- importLines.push(`import ${astroComponentName(comp)} from '${path3}';`);
15250
+ const path5 = componentImportPath(fileDepth, comp);
15251
+ importLines.push(`import ${astroComponentName(comp)} from '${path5}';`);
14221
15252
  }
14222
15253
  const scriptsArrayLiteral = scriptPaths.length > 0 ? `[${scriptPaths.map((s) => `"${s}"`).join(", ")}]` : "[]";
14223
15254
  const libraryTagsLiteral = `{ headCSS: \`${escapeTemplateLiteral2(libraryTags.headCSS || "")}\`, headJS: \`${escapeTemplateLiteral2(libraryTags.headJS || "")}\`, bodyEndJS: \`${escapeTemplateLiteral2(libraryTags.bodyEndJS || "")}\` }`;
@@ -14556,8 +15587,8 @@ function emitCMSPage(options) {
14556
15587
  }
14557
15588
  const componentImports = Array.from(ctx.imports).sort();
14558
15589
  for (const comp of componentImports) {
14559
- const path3 = componentImportPath2(fileDepth, comp);
14560
- importLines.push(`import ${astroComponentName(comp)} from '${path3}';`);
15590
+ const path5 = componentImportPath2(fileDepth, comp);
15591
+ importLines.push(`import ${astroComponentName(comp)} from '${path5}';`);
14561
15592
  }
14562
15593
  const staticPaths = buildGetStaticPaths(cmsSchema, isMultiLocale, i18nConfig, locale);
14563
15594
  const scriptsArrayLiteral = scriptPaths.length > 0 ? `[${scriptPaths.map((s) => `"${s}"`).join(", ")}]` : "[]";
@@ -14730,24 +15761,24 @@ function writePageScript(javascript, scriptsDir) {
14730
15761
  if (!javascript) return [];
14731
15762
  const hash = hashContent3(javascript);
14732
15763
  const scriptFile = `${hash}.js`;
14733
- if (!existsSync10(scriptsDir)) {
15764
+ if (!existsSync11(scriptsDir)) {
14734
15765
  mkdirSync4(scriptsDir, { recursive: true });
14735
15766
  }
14736
15767
  const fullScriptPath = join9(scriptsDir, scriptFile);
14737
- if (!existsSync10(fullScriptPath)) {
15768
+ if (!existsSync11(fullScriptPath)) {
14738
15769
  writeFileSync(fullScriptPath, javascript, "utf-8");
14739
15770
  }
14740
15771
  return [`/_scripts/${scriptFile}`];
14741
15772
  }
14742
15773
  function copyDirectory(src, dest, filter) {
14743
- if (!existsSync10(src)) return;
14744
- if (!existsSync10(dest)) mkdirSync4(dest, { recursive: true });
14745
- const files = readdirSync6(src);
15774
+ if (!existsSync11(src)) return;
15775
+ if (!existsSync11(dest)) mkdirSync4(dest, { recursive: true });
15776
+ const files = readdirSync7(src);
14746
15777
  for (const file of files) {
14747
15778
  if (filter && !filter(file)) continue;
14748
15779
  const srcPath = join9(src, file);
14749
15780
  const destPath = join9(dest, file);
14750
- const stat = statSync2(srcPath);
15781
+ const stat = statSync3(srcPath);
14751
15782
  if (stat.isDirectory()) copyDirectory(srcPath, destPath, filter);
14752
15783
  else copyFileSync(srcPath, destPath);
14753
15784
  }
@@ -14763,8 +15794,8 @@ function isCMSPage(pageData) {
14763
15794
  }
14764
15795
  function scanJSONFiles(dir, prefix = "") {
14765
15796
  const results = [];
14766
- if (!existsSync10(dir)) return results;
14767
- const entries = readdirSync6(dir, { withFileTypes: true });
15797
+ if (!existsSync11(dir)) return results;
15798
+ const entries = readdirSync7(dir, { withFileTypes: true });
14768
15799
  for (const entry of entries) {
14769
15800
  if (entry.isFile() && entry.name.endsWith(".json")) {
14770
15801
  results.push(prefix ? `${prefix}/${entry.name}` : entry.name);
@@ -14936,7 +15967,7 @@ async function buildAstroProject(_projectRoot, outputDir) {
14936
15967
  const componentLibraries = collectComponentLibraries(globalComponents);
14937
15968
  const imageMetadataMap = await buildImageMetadataMap();
14938
15969
  const outDir = outputDir || join9(projectPaths.project, "astro-export");
14939
- if (existsSync10(outDir)) {
15970
+ if (existsSync11(outDir)) {
14940
15971
  rmSync(outDir, { recursive: true, force: true });
14941
15972
  }
14942
15973
  mkdirSync4(outDir, { recursive: true });
@@ -14951,7 +15982,7 @@ async function buildAstroProject(_projectRoot, outputDir) {
14951
15982
  mkdirSync4(d, { recursive: true });
14952
15983
  }
14953
15984
  const pagesDir = projectPaths.pages();
14954
- if (!existsSync10(pagesDir)) {
15985
+ if (!existsSync11(pagesDir)) {
14955
15986
  console.error("Pages directory not found!");
14956
15987
  return { pages: 0, cmsPages: 0, collections: 0, errors: 1 };
14957
15988
  }
@@ -15082,7 +16113,7 @@ async function buildAstroProject(_projectRoot, outputDir) {
15082
16113
  const shouldInline = css.inline !== false;
15083
16114
  const relPath = css.url.slice(1);
15084
16115
  const srcPath = join9(projectPaths.project, relPath);
15085
- if (!existsSync10(srcPath)) continue;
16116
+ if (!existsSync11(srcPath)) continue;
15086
16117
  if (shouldInline) {
15087
16118
  try {
15088
16119
  inlineContents.set(css.url, await readFile(srcPath, "utf-8"));
@@ -15096,7 +16127,7 @@ async function buildAstroProject(_projectRoot, outputDir) {
15096
16127
  for (const js of buildLibraries.js || []) {
15097
16128
  if (js.url.startsWith("/")) {
15098
16129
  const relPath = js.url.slice(1);
15099
- if (existsSync10(join9(projectPaths.project, relPath))) {
16130
+ if (existsSync11(join9(projectPaths.project, relPath))) {
15100
16131
  localLibsToCopy.push(relPath);
15101
16132
  }
15102
16133
  }
@@ -15117,8 +16148,8 @@ async function buildAstroProject(_projectRoot, outputDir) {
15117
16148
  const cmsConsumerComponents = computeCmsConsumerComponents(globalComponents);
15118
16149
  const collectionUrlExpr = /* @__PURE__ */ new Map();
15119
16150
  const mergedRichTextFields = /* @__PURE__ */ new Set();
15120
- if (existsSync10(templatesDir)) {
15121
- for (const file of readdirSync6(templatesDir).filter((f) => f.endsWith(".json"))) {
16151
+ if (existsSync11(templatesDir)) {
16152
+ for (const file of readdirSync7(templatesDir).filter((f) => f.endsWith(".json"))) {
15122
16153
  const tc = await loadJSONFile(join9(templatesDir, file));
15123
16154
  if (!tc) continue;
15124
16155
  try {
@@ -15133,8 +16164,8 @@ async function buildAstroProject(_projectRoot, outputDir) {
15133
16164
  }
15134
16165
  }
15135
16166
  }
15136
- if (existsSync10(templatesDir)) {
15137
- const templateFiles = readdirSync6(templatesDir).filter((f) => f.endsWith(".json"));
16167
+ if (existsSync11(templatesDir)) {
16168
+ const templateFiles = readdirSync7(templatesDir).filter((f) => f.endsWith(".json"));
15138
16169
  for (const file of templateFiles) {
15139
16170
  const templateContent = await loadJSONFile(join9(templatesDir, file));
15140
16171
  if (!templateContent) continue;
@@ -15227,7 +16258,7 @@ async function buildAstroProject(_projectRoot, outputDir) {
15227
16258
  });
15228
16259
  const astroFileFull = join9(pagesOutDir, astroFilePath);
15229
16260
  const astroFileDir = astroFileFull.substring(0, astroFileFull.lastIndexOf("/"));
15230
- if (!existsSync10(astroFileDir)) {
16261
+ if (!existsSync11(astroFileDir)) {
15231
16262
  mkdirSync4(astroFileDir, { recursive: true });
15232
16263
  }
15233
16264
  await writeFile2(astroFileFull, astroContent, "utf-8");
@@ -15378,7 +16409,7 @@ const { title, meta = '', scripts = [], locale = 'en', theme = '${themeConfig.de
15378
16409
  }
15379
16410
  const astroFileFull = join9(pagesOutDir, result.astroFilePath);
15380
16411
  const astroFileDir = astroFileFull.substring(0, astroFileFull.lastIndexOf("/"));
15381
- if (!existsSync10(astroFileDir)) {
16412
+ if (!existsSync11(astroFileDir)) {
15382
16413
  mkdirSync4(astroFileDir, { recursive: true });
15383
16414
  }
15384
16415
  await writeFile2(astroFileFull, astroContent, "utf-8");
@@ -15410,9 +16441,9 @@ export const GET: APIRoute = () => {
15410
16441
  mkdirSync4(collectionDir, { recursive: true });
15411
16442
  const richTextFieldNames = Object.entries(schema.fields || {}).filter(([, fd]) => fd.type === "rich-text").map(([fn]) => fn);
15412
16443
  const cmsItemsDir = join9(projectPaths.cms(), schema.id);
15413
- if (existsSync10(cmsItemsDir)) {
16444
+ if (existsSync11(cmsItemsDir)) {
15414
16445
  const isDevBuild = process.env.MENO_DEV_BUILD === "true";
15415
- const itemFiles = readdirSync6(cmsItemsDir).filter(
16446
+ const itemFiles = readdirSync7(cmsItemsDir).filter(
15416
16447
  (f) => f.endsWith(".json") && (isDevBuild || !f.endsWith(`${CMS_DRAFT_SUFFIX}.json`))
15417
16448
  );
15418
16449
  for (const itemFile of itemFiles) {
@@ -15459,26 +16490,26 @@ export { collections };
15459
16490
  await writeFile2(join9(srcDir, "content.config.ts"), configContent, "utf-8");
15460
16491
  }
15461
16492
  const imagesSrcDir = join9(projectPaths.project, "images");
15462
- if (existsSync10(imagesSrcDir)) {
16493
+ if (existsSync11(imagesSrcDir)) {
15463
16494
  copyDirectory(imagesSrcDir, join9(srcDir, "assets", "images"), shouldCopyImageForAstro);
15464
16495
  copyDirectory(imagesSrcDir, join9(publicDir, "images"));
15465
16496
  }
15466
16497
  const publicAssetDirs = ["fonts", "icons", "videos", "assets"];
15467
16498
  for (const dir of publicAssetDirs) {
15468
16499
  const srcAssetDir = join9(projectPaths.project, dir);
15469
- if (existsSync10(srcAssetDir)) {
16500
+ if (existsSync11(srcAssetDir)) {
15470
16501
  copyDirectory(srcAssetDir, join9(publicDir, dir));
15471
16502
  }
15472
16503
  }
15473
16504
  const librariesDir = join9(projectPaths.project, "libraries");
15474
- if (existsSync10(librariesDir)) {
16505
+ if (existsSync11(librariesDir)) {
15475
16506
  copyDirectory(librariesDir, join9(publicDir, "libraries"));
15476
16507
  }
15477
16508
  for (const relPath of localLibsToCopy) {
15478
16509
  const srcPath = join9(projectPaths.project, relPath);
15479
16510
  const destPath = join9(publicDir, relPath);
15480
16511
  const destDir = destPath.substring(0, destPath.lastIndexOf("/"));
15481
- if (destDir && !existsSync10(destDir)) mkdirSync4(destDir, { recursive: true });
16512
+ if (destDir && !existsSync11(destDir)) mkdirSync4(destDir, { recursive: true });
15482
16513
  copyFileSync(srcPath, destPath);
15483
16514
  }
15484
16515
  const packageJson = {
@@ -15548,6 +16579,32 @@ export default defineConfig({${siteUrl ? `
15548
16579
 
15549
16580
  // lib/server/index.ts
15550
16581
  init_constants();
16582
+
16583
+ // lib/server/webflow/templateWrapper.ts
16584
+ import { readFile as readFile2 } from "node:fs/promises";
16585
+ var cachedTemplate = null;
16586
+ async function getWebflowTemplate(appName) {
16587
+ if (cachedTemplate) return cachedTemplate;
16588
+ const url = `https://webflow-ext.com/template/v2?name=${encodeURIComponent(appName)}`;
16589
+ const res = await fetch(url);
16590
+ if (!res.ok) throw new Error(`Failed to fetch Webflow template: ${res.status}`);
16591
+ cachedTemplate = await res.text();
16592
+ return cachedTemplate;
16593
+ }
16594
+ async function wrapInWebflowTemplate(html, manifestPath) {
16595
+ try {
16596
+ const manifest = JSON.parse(await readFile2(manifestPath, "utf-8"));
16597
+ const template = await getWebflowTemplate(manifest.name || "Export to Meno");
16598
+ const headMatch = html.match(/<head[^>]*>([\s\S]*?)<\/head>/i);
16599
+ const bodyMatch = html.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
16600
+ const headContent = headMatch?.[1] ?? "";
16601
+ const bodyContent = bodyMatch?.[1] ?? "";
16602
+ return template.replace("{{ui}}", headContent + bodyContent);
16603
+ } catch (err) {
16604
+ console.warn("Could not fetch Webflow wrapper template:", err.message);
16605
+ return html;
16606
+ }
16607
+ }
15551
16608
  export {
15552
16609
  CMSService,
15553
16610
  ColorService,
@@ -15567,6 +16624,7 @@ export {
15567
16624
  SERVE_PORT,
15568
16625
  VariableService,
15569
16626
  WebSocketManager,
16627
+ auditStylesheet,
15570
16628
  buildAstroProject,
15571
16629
  buildAttributes,
15572
16630
  buildComponentHTML,
@@ -15574,6 +16632,7 @@ export {
15574
16632
  buildLineMap,
15575
16633
  bundleFile,
15576
16634
  clearJSValidationCache,
16635
+ collectClassesFromPageData,
15577
16636
  collectComponentCSS,
15578
16637
  collectComponentJavaScript,
15579
16638
  colorService,
@@ -15588,6 +16647,7 @@ export {
15588
16647
  errorResponse,
15589
16648
  escapeHtml,
15590
16649
  extractPageMeta,
16650
+ extractRules,
15591
16651
  fileExists,
15592
16652
  formHandlerScript,
15593
16653
  generateBuildErrorPage,
@@ -15634,12 +16694,14 @@ export {
15634
16694
  readJsonFile,
15635
16695
  readTextFile,
15636
16696
  regenerateThemeCss,
16697
+ renderAuditMarkdown,
15637
16698
  renderPageSSR,
15638
16699
  resetFontConfig,
15639
16700
  resolvePackagePath,
15640
16701
  resolveProjectPath,
15641
16702
  saveColors,
15642
16703
  saveVariables,
16704
+ sectionPrefix,
15643
16705
  serializeThemeCss,
15644
16706
  serveFile,
15645
16707
  setProjectRoot,
@@ -15651,6 +16713,7 @@ export {
15651
16713
  watchRecursive,
15652
16714
  withErrorHandling,
15653
16715
  withLogging,
16716
+ wrapInWebflowTemplate,
15654
16717
  writeFile
15655
16718
  };
15656
16719
  //# sourceMappingURL=index.js.map