meno-core 1.1.3 → 1.1.4

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 (50) hide show
  1. package/dist/chunks/{chunk-LOZL5HOF.js → chunk-UOF4MCAD.js} +64 -2
  2. package/dist/chunks/chunk-UOF4MCAD.js.map +7 -0
  3. package/dist/chunks/{chunk-3JXK2QFU.js → chunk-ZEDLSHYQ.js} +2 -2
  4. package/dist/lib/client/index.js +46 -28
  5. package/dist/lib/client/index.js.map +2 -2
  6. package/dist/lib/server/index.js +1267 -233
  7. package/dist/lib/server/index.js.map +4 -4
  8. package/dist/lib/shared/index.js +3 -1
  9. package/dist/lib/shared/index.js.map +1 -1
  10. package/lib/client/core/ComponentBuilder.test.ts +32 -0
  11. package/lib/client/core/ComponentBuilder.ts +30 -0
  12. package/lib/client/theme.test.ts +2 -2
  13. package/lib/client/theme.ts +28 -26
  14. package/lib/server/createServer.ts +5 -1
  15. package/lib/server/cssAudit.test.ts +270 -0
  16. package/lib/server/cssAudit.ts +815 -0
  17. package/lib/server/deMirror.test.ts +61 -0
  18. package/lib/server/deMirror.ts +317 -0
  19. package/lib/server/fileWatcher.test.ts +23 -0
  20. package/lib/server/fileWatcher.ts +6 -1
  21. package/lib/server/index.ts +19 -0
  22. package/lib/server/routes/api/core-routes.ts +18 -0
  23. package/lib/server/routes/api/cssAudit.test.ts +101 -0
  24. package/lib/server/routes/api/cssAudit.ts +138 -0
  25. package/lib/server/routes/api/deMirror.test.ts +105 -0
  26. package/lib/server/routes/api/deMirror.ts +127 -0
  27. package/lib/server/routes/api/pages.ts +2 -2
  28. package/lib/server/services/componentService.test.ts +43 -0
  29. package/lib/server/services/componentService.ts +49 -12
  30. package/lib/server/services/pageService.test.ts +15 -0
  31. package/lib/server/services/pageService.ts +44 -22
  32. package/lib/server/ssr/htmlGenerator.test.ts +29 -0
  33. package/lib/server/ssr/htmlGenerator.ts +83 -3
  34. package/lib/server/themeCssCodec.test.ts +67 -0
  35. package/lib/server/themeCssCodec.ts +67 -5
  36. package/lib/server/webflow/templateWrapper.ts +54 -0
  37. package/lib/shared/cssGeneration.test.ts +28 -0
  38. package/lib/shared/interfaces/contentProvider.ts +9 -0
  39. package/lib/shared/types/cms.ts +1 -0
  40. package/lib/shared/utilityClassConfig.ts +2 -0
  41. package/lib/shared/utilityClassMapper.test.ts +53 -0
  42. package/lib/shared/utilityClassNames.ts +74 -0
  43. package/lib/shared/utils/fileUtils.ts +11 -0
  44. package/lib/shared/validation/schemas.ts +1 -0
  45. package/lib/shared/viewportUnits.test.ts +34 -1
  46. package/lib/shared/viewportUnits.ts +43 -0
  47. package/package.json +3 -1
  48. package/vite.config.ts +4 -1
  49. package/dist/chunks/chunk-LOZL5HOF.js.map +0 -7
  50. /package/dist/chunks/{chunk-3JXK2QFU.js.map → chunk-ZEDLSHYQ.js.map} +0 -0
@@ -33,7 +33,7 @@ import {
33
33
  processStructure,
34
34
  resolveHtmlMapping,
35
35
  skipEmptyTemplateAttributes
36
- } from "../../chunks/chunk-3JXK2QFU.js";
36
+ } from "../../chunks/chunk-ZEDLSHYQ.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-UOF4MCAD.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
  /**
@@ -1518,8 +1519,8 @@ function cachedJsonResponse(req, body, options = {}) {
1518
1519
  }
1519
1520
 
1520
1521
  // lib/server/routes/api/pages.ts
1521
- function handlePagesRoute(pageService) {
1522
- const pages = pageService.getAllPagesWithInfo();
1522
+ async function handlePagesRoute(pageService) {
1523
+ const pages = await pageService.getAllPagesWithInfo();
1523
1524
  return jsonResponse({ pages });
1524
1525
  }
1525
1526
  function handlePageDataRoute(url, pageService) {
@@ -1641,11 +1642,11 @@ function handleComponentUsageRoute(pageService, componentService) {
1641
1642
  }
1642
1643
  }
1643
1644
  const pagePaths = pageService.getAllPagePaths();
1644
- for (const path3 of pagePaths) {
1645
- const pageData = pageService.getPageData(path3);
1645
+ for (const path5 of pagePaths) {
1646
+ const pageData = pageService.getPageData(path5);
1646
1647
  if (pageData && "root" in pageData && pageData.root) {
1647
- const name = path3 === "/" ? "index" : path3.replace(/^\//, "").replace(/\//g, "-");
1648
- walkTree(pageData.root, name, path3, "page");
1648
+ const name = path5 === "/" ? "index" : path5.replace(/^\//, "").replace(/\//g, "-");
1649
+ walkTree(pageData.root, name, path5, "page");
1649
1650
  }
1650
1651
  }
1651
1652
  const allComponents = componentService.getAllComponents();
@@ -1755,9 +1756,24 @@ function sectionLabelForGroup(group) {
1755
1756
  if (!group || group === "other") return OTHER_LABEL;
1756
1757
  return VARIABLE_GROUPS.find((g) => g.value === group)?.label ?? OTHER_LABEL;
1757
1758
  }
1758
- var LABEL_TO_GROUP = new Map(VARIABLE_GROUPS.map((g) => [g.label.toLowerCase(), g.value]));
1759
+ function normalizeSectionLabel(s) {
1760
+ return s.trim().toLowerCase().replace(/[-–—]+/g, " ").replace(/\s+/g, " ").trim();
1761
+ }
1762
+ var NORMALIZED_LABEL_TO_GROUP = new Map(
1763
+ VARIABLE_GROUPS.map((g) => [normalizeSectionLabel(g.label), g.value])
1764
+ );
1765
+ 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
1766
  function groupForSectionLabel(label) {
1760
- return LABEL_TO_GROUP.get(label.trim().toLowerCase()) ?? null;
1767
+ const norm = normalizeSectionLabel(label);
1768
+ const exact = NORMALIZED_LABEL_TO_GROUP.get(norm);
1769
+ if (exact) return exact;
1770
+ for (const { norm: labelNorm, group } of PREFIX_LABELS) {
1771
+ if (norm.startsWith(labelNorm)) {
1772
+ const next = norm.charAt(labelNorm.length);
1773
+ if (next === "" || !/[a-z0-9]/.test(next)) return group;
1774
+ }
1775
+ }
1776
+ return null;
1761
1777
  }
1762
1778
  function isColorValue(value) {
1763
1779
  const v = value.trim().toLowerCase();
@@ -1766,6 +1782,11 @@ function isColorValue(value) {
1766
1782
  if (v === "transparent" || v === "currentcolor") return true;
1767
1783
  return false;
1768
1784
  }
1785
+ var NAME_HINT_GROUPS = [
1786
+ { needle: /(^|[-_])tracking([-_]|$)/, group: "letter-spacing" },
1787
+ { needle: /(^|[-_])leading([-_]|$)/, group: "line-height" },
1788
+ { needle: /(^|[-_])(radius|rounded)([-_]|$)/, group: "border-radius" }
1789
+ ];
1769
1790
  function inferGroup(cssVar, value) {
1770
1791
  const v = value.trim().toLowerCase();
1771
1792
  if (/^\d+(\.\d+)?m?s$/.test(v)) return "duration";
@@ -1776,6 +1797,9 @@ function inferGroup(cssVar, value) {
1776
1797
  }
1777
1798
  if (/^\d*\.\d+$/.test(v) || /^[1-9](\.\d+)?$/.test(v)) return "line-height";
1778
1799
  const name = cssVar.replace(/^--/, "").toLowerCase();
1800
+ for (const { needle, group } of NAME_HINT_GROUPS) {
1801
+ if (needle.test(name)) return group;
1802
+ }
1779
1803
  for (const g of VARIABLE_GROUPS) {
1780
1804
  if (g.value !== "other" && name.includes(g.value)) return g.value;
1781
1805
  }
@@ -2057,8 +2081,7 @@ function parseThemeCss(css, breakpoints) {
2057
2081
  const dn = inner.match(/^colors\s*:\s*([\w-]+)/i);
2058
2082
  if (dn) defaultName = dn[1];
2059
2083
  } else {
2060
- const g = groupForSectionLabel(inner);
2061
- if (g) section = g;
2084
+ section = groupForSectionLabel(inner);
2062
2085
  }
2063
2086
  continue;
2064
2087
  }
@@ -2134,9 +2157,9 @@ function enqueue(fn) {
2134
2157
  );
2135
2158
  return run;
2136
2159
  }
2137
- async function readJsonSafe(path3) {
2160
+ async function readJsonSafe(path5) {
2138
2161
  try {
2139
- return JSON.parse(await readTextFile(path3));
2162
+ return JSON.parse(await readTextFile(path5));
2140
2163
  } catch {
2141
2164
  return null;
2142
2165
  }
@@ -2148,9 +2171,9 @@ function emptyModel() {
2148
2171
  return { themes: { default: "default", themes: { default: { colors: {} } } }, variables: [], raw: [] };
2149
2172
  }
2150
2173
  async function readModel(root) {
2151
- const path3 = themeCssPath(root);
2152
- if (!await fileExists(path3)) return emptyModel();
2153
- return parseThemeCss(await readTextFile(path3), configService.getBreakpoints());
2174
+ const path5 = themeCssPath(root);
2175
+ if (!await fileExists(path5)) return emptyModel();
2176
+ return parseThemeCss(await readTextFile(path5), configService.getBreakpoints());
2154
2177
  }
2155
2178
  async function writeModel(root, model) {
2156
2179
  const css = serializeThemeCss(model, scaleConfig());
@@ -2880,8 +2903,880 @@ async function handleItemRoute(collection, slug, cmsService) {
2880
2903
  return jsonResponse({ item: match.item });
2881
2904
  }
2882
2905
 
2883
- // lib/server/routes/api/functions.ts
2906
+ // lib/server/routes/api/cssAudit.ts
2907
+ import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync } from "node:fs";
2884
2908
  import path from "node:path";
2909
+
2910
+ // lib/server/cssAudit.ts
2911
+ import * as csstree from "css-tree";
2912
+ var DEFAULT_UTILITY_HINTS = [
2913
+ // Spacing / sizing
2914
+ "^(margin|padding|gap)(-|$)",
2915
+ "^no-(padding|margin|overflow)",
2916
+ "(^|-)auto-margin$",
2917
+ "^(max|min)-(w|h|width|height)",
2918
+ "^(w|h)-[0-9]",
2919
+ "^size-[0-9]",
2920
+ "^(small|medium|large)(-|$)",
2921
+ // Layout / display
2922
+ "^align(-|$)",
2923
+ "^justify(-|$)",
2924
+ "^(grid|col|flex)-",
2925
+ "^container(-|$)",
2926
+ "(^|-)(overflow|opacity|relative|absolute|sticky|inline|centered)(-|$)",
2927
+ "^(hide|show|hidden|visible)$",
2928
+ // State / Webflow combo classes
2929
+ "^is--?",
2930
+ "^has--?",
2931
+ "^on-h[0-9]",
2932
+ "^type-[0-9]+$",
2933
+ "(^|-)active$",
2934
+ // Colors
2935
+ "(^|-)(white|black|dark|light|primary|secondary|grey|gray)(-|$)",
2936
+ // Cross-cutting UI
2937
+ "(^|-)arrow",
2938
+ // Relume / newer-Webflow utility families (text-size-*, heading-style-*, spacer-*, max-width-*)
2939
+ "^text-(size|align|color|weight|style)",
2940
+ "^heading-style",
2941
+ "^(spacer|max-width|background-color)(-|$)"
2942
+ ];
2943
+ var DEFAULT_WEBFLOW_AUTO = [
2944
+ "^(div-block|text-block|link-block|bold-text|block-quote|rich-text-block|html-embed)(-\\d+)?$",
2945
+ "^(heading|paragraph|image|list|list-item|figure|grid|columns?|field-label|text-field)-\\d+$"
2946
+ ];
2947
+ var CLASS_TOKEN_RE = /\.(-?[_a-zA-Z][\w-]*)/g;
2948
+ var FUNCTIONAL_PSEUDO_RE = /:(not|is|where|has|matches)\([^()]*\)/gi;
2949
+ function emptyBucket() {
2950
+ return { classes: 0, rules: 0, declarations: 0, approxBytes: 0 };
2951
+ }
2952
+ function classTokens(selector) {
2953
+ const out = [];
2954
+ let m;
2955
+ CLASS_TOKEN_RE.lastIndex = 0;
2956
+ while ((m = CLASS_TOKEN_RE.exec(selector)) !== null) {
2957
+ if (m[1]) out.push(m[1]);
2958
+ }
2959
+ return out;
2960
+ }
2961
+ function sectionPrefix(name) {
2962
+ const seg = name.split(/[-_]/)[0] ?? name;
2963
+ const stripped = seg.replace(/\d+$/, "");
2964
+ return stripped.length > 0 ? stripped : seg;
2965
+ }
2966
+ function commonPrefixLength(a, b) {
2967
+ const max = Math.min(a.length, b.length);
2968
+ let i = 0;
2969
+ while (i < max && a[i] === b[i]) i++;
2970
+ return i;
2971
+ }
2972
+ function editDistance(a, b) {
2973
+ const m = a.length;
2974
+ const n = b.length;
2975
+ if (m === 0) return n;
2976
+ if (n === 0) return m;
2977
+ let prev = Array.from({ length: n + 1 }, (_, i) => i);
2978
+ let curr = new Array(n + 1).fill(0);
2979
+ for (let i = 1; i <= m; i++) {
2980
+ curr[0] = i;
2981
+ for (let j = 1; j <= n; j++) {
2982
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
2983
+ curr[j] = Math.min((prev[j] ?? 0) + 1, (curr[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
2984
+ }
2985
+ [prev, curr] = [curr, prev];
2986
+ }
2987
+ return prev[n] ?? 0;
2988
+ }
2989
+ function nearestKeptPrefix(prefix, kept) {
2990
+ let best = null;
2991
+ let bestShared = -1;
2992
+ for (const candidate of kept) {
2993
+ if (Math.abs(candidate.length - prefix.length) > 1) continue;
2994
+ const shared = commonPrefixLength(prefix, candidate);
2995
+ if (shared < 2) continue;
2996
+ if (editDistance(prefix, candidate) !== 1) continue;
2997
+ if (shared > bestShared) {
2998
+ bestShared = shared;
2999
+ best = candidate;
3000
+ }
3001
+ }
3002
+ return best;
3003
+ }
3004
+ function analyzeSelector(selector) {
3005
+ const cleaned = selector.replace(FUNCTIONAL_PSEUDO_RE, " ");
3006
+ const segments = cleaned.split(/\s*[>+~]\s*|\s+/).filter((s) => s.trim().length > 0);
3007
+ const subject = segments[segments.length - 1] ?? cleaned;
3008
+ const subjectClasses = classTokens(subject);
3009
+ const allClasses = classTokens(selector);
3010
+ const compound = segments.length > 1 || subjectClasses.length > 1;
3011
+ return { allClasses, subjectClasses, compound };
3012
+ }
3013
+ function countDeclarations(block) {
3014
+ let count = 0;
3015
+ if (block && block.type === "Block") {
3016
+ block.children.forEach((child) => {
3017
+ if (child.type === "Declaration") count++;
3018
+ });
3019
+ }
3020
+ return count;
3021
+ }
3022
+ function extractRules(css) {
3023
+ const ast = csstree.parse(css, { parseValue: false, parseCustomProperty: false });
3024
+ const out = [];
3025
+ const visit = (container, media, insideSkippedAtrule) => {
3026
+ const children = "children" in container ? container.children : null;
3027
+ if (!children) return;
3028
+ children.forEach((node) => {
3029
+ if (node.type === "Atrule") {
3030
+ const name = node.name.toLowerCase();
3031
+ if (name === "media") {
3032
+ const prelude = node.prelude ? csstree.generate(node.prelude) : "";
3033
+ if (node.block) visit(node.block, prelude, false);
3034
+ } else if (name === "supports") {
3035
+ if (node.block) visit(node.block, media, false);
3036
+ } else if (node.block) {
3037
+ visit(node.block, media, true);
3038
+ }
3039
+ return;
3040
+ }
3041
+ if (node.type !== "Rule" || insideSkippedAtrule) return;
3042
+ if (node.prelude?.type !== "SelectorList") return;
3043
+ const decls = countDeclarations(node.block);
3044
+ const ruleBytes = csstree.generate(node).length;
3045
+ const classSelectors = [];
3046
+ node.prelude.children.forEach((selectorNode) => {
3047
+ const text = csstree.generate(selectorNode);
3048
+ const shape = analyzeSelector(text);
3049
+ if (shape.allClasses.length > 0) classSelectors.push({ text, shape });
3050
+ });
3051
+ if (classSelectors.length === 0) return;
3052
+ const perBytes = Math.max(1, Math.round(ruleBytes / classSelectors.length));
3053
+ for (const { text, shape } of classSelectors) {
3054
+ out.push({
3055
+ selector: text,
3056
+ media,
3057
+ classes: shape.allClasses,
3058
+ subjectClasses: shape.subjectClasses,
3059
+ declarations: decls,
3060
+ bytes: perBytes,
3061
+ compound: shape.compound
3062
+ });
3063
+ }
3064
+ });
3065
+ };
3066
+ visit(ast, null, false);
3067
+ return out;
3068
+ }
3069
+ function auditStylesheet(input) {
3070
+ const { css, pages, options = {} } = input;
3071
+ const selectorSampleLimit = options.selectorSampleLimit ?? 5;
3072
+ const flagLimit = options.flagLimit ?? 200;
3073
+ const minSectionClasses = options.minSectionClasses ?? 3;
3074
+ const miscBucketName = options.miscBucketName ?? "misc";
3075
+ const mergeTypos = options.mergeTypos ?? true;
3076
+ const utilityHints = (options.utilityHints ?? DEFAULT_UTILITY_HINTS).map((s) => new RegExp(s));
3077
+ const webflowAutoPatterns = (options.webflowAuto ?? DEFAULT_WEBFLOW_AUTO).map((s) => new RegExp(s));
3078
+ const pageCount = pages.length;
3079
+ const sharedMinPages = options.sharedMinPages ?? Math.max(2, Math.ceil(pageCount / 2));
3080
+ const pageUseCount = /* @__PURE__ */ new Map();
3081
+ for (const page of pages) {
3082
+ for (const cls of new Set(page.classes)) {
3083
+ pageUseCount.set(cls, (pageUseCount.get(cls) ?? 0) + 1);
3084
+ }
3085
+ }
3086
+ const ast = csstree.parse(css, { parseValue: false, parseCustomProperty: false });
3087
+ const classMap = /* @__PURE__ */ new Map();
3088
+ const rules = [];
3089
+ const breakpoints = [];
3090
+ const seenMedia = /* @__PURE__ */ new Set();
3091
+ const base = emptyBucket();
3092
+ let totalRules = 0;
3093
+ let totalSelectors = 0;
3094
+ let totalDeclarations = 0;
3095
+ const accumFor = (name) => {
3096
+ let a = classMap.get(name);
3097
+ if (!a) {
3098
+ a = { name, ruleCount: 0, declarations: 0, breakpoints: /* @__PURE__ */ new Set(), selectors: [], ownsSimple: false };
3099
+ classMap.set(name, a);
3100
+ }
3101
+ return a;
3102
+ };
3103
+ const visit = (container, media, insideSkippedAtrule) => {
3104
+ const children = "children" in container ? container.children : null;
3105
+ if (!children) return;
3106
+ children.forEach((node) => {
3107
+ if (node.type === "Atrule") {
3108
+ const name = node.name.toLowerCase();
3109
+ if (name === "media") {
3110
+ const prelude = node.prelude ? csstree.generate(node.prelude) : "";
3111
+ if (!seenMedia.has(prelude)) {
3112
+ seenMedia.add(prelude);
3113
+ breakpoints.push(prelude);
3114
+ }
3115
+ if (node.block) visit(node.block, prelude, false);
3116
+ } else if (name === "supports") {
3117
+ if (node.block) visit(node.block, media, false);
3118
+ } else if (node.block) {
3119
+ visit(node.block, media, true);
3120
+ }
3121
+ return;
3122
+ }
3123
+ if (node.type !== "Rule" || insideSkippedAtrule) return;
3124
+ if (node.prelude?.type !== "SelectorList") return;
3125
+ const ruleBytes = csstree.generate(node).length;
3126
+ const decls = countDeclarations(node.block);
3127
+ totalDeclarations += decls;
3128
+ totalRules += 1;
3129
+ const ruleAllClasses = /* @__PURE__ */ new Set();
3130
+ const ruleSubjectClasses = /* @__PURE__ */ new Set();
3131
+ let hasAnyClass = false;
3132
+ node.prelude.children.forEach((selectorNode) => {
3133
+ totalSelectors += 1;
3134
+ const selectorText = csstree.generate(selectorNode);
3135
+ const shape = analyzeSelector(selectorText);
3136
+ if (shape.allClasses.length === 0) return;
3137
+ hasAnyClass = true;
3138
+ for (const c of shape.allClasses) ruleAllClasses.add(c);
3139
+ for (const c of shape.subjectClasses) ruleSubjectClasses.add(c);
3140
+ const subjectSet = new Set(shape.subjectClasses);
3141
+ for (const cls of shape.allClasses) {
3142
+ const a = accumFor(cls);
3143
+ a.ruleCount += 1;
3144
+ a.declarations += decls;
3145
+ a.breakpoints.add(media ?? "");
3146
+ if (a.selectors.length < selectorSampleLimit && !a.selectors.includes(selectorText)) {
3147
+ a.selectors.push(selectorText);
3148
+ }
3149
+ if (subjectSet.size === 1 && subjectSet.has(cls)) a.ownsSimple = true;
3150
+ }
3151
+ });
3152
+ if (!hasAnyClass) {
3153
+ base.rules += 1;
3154
+ base.declarations += decls;
3155
+ base.approxBytes += ruleBytes;
3156
+ return;
3157
+ }
3158
+ rules.push({
3159
+ media,
3160
+ bytes: ruleBytes,
3161
+ declarations: decls,
3162
+ classes: [...ruleAllClasses],
3163
+ subjectClasses: [...ruleSubjectClasses]
3164
+ });
3165
+ });
3166
+ };
3167
+ visit(ast, null, false);
3168
+ const prelim = /* @__PURE__ */ new Map();
3169
+ const sectionSizes = /* @__PURE__ */ new Map();
3170
+ for (const a of classMap.values()) {
3171
+ const pagesUsedIn = pageUseCount.get(a.name) ?? 0;
3172
+ let category;
3173
+ let bucket;
3174
+ if (/^w-/.test(a.name)) {
3175
+ category = "vendor";
3176
+ bucket = "vendor";
3177
+ } else if (webflowAutoPatterns.some((re) => re.test(a.name))) {
3178
+ category = "webflow-auto";
3179
+ bucket = "webflow-auto";
3180
+ } else if (utilityHints.some((re) => re.test(a.name)) || pagesUsedIn >= sharedMinPages) {
3181
+ category = "shared";
3182
+ bucket = "shared";
3183
+ } else {
3184
+ category = "section";
3185
+ bucket = sectionPrefix(a.name);
3186
+ sectionSizes.set(bucket, (sectionSizes.get(bucket) ?? 0) + 1);
3187
+ }
3188
+ prelim.set(a.name, { category, bucket, pagesUsedIn, accum: a });
3189
+ }
3190
+ const keptSections = /* @__PURE__ */ new Set();
3191
+ for (const [prefix, count] of sectionSizes) {
3192
+ if (count >= minSectionClasses) keptSections.add(prefix);
3193
+ }
3194
+ const sectionRemap = /* @__PURE__ */ new Map();
3195
+ for (const [prefix, count] of sectionSizes) {
3196
+ if (keptSections.has(prefix) || count === 0) continue;
3197
+ const near = mergeTypos && prefix.length >= 4 ? nearestKeptPrefix(prefix, keptSections) : null;
3198
+ sectionRemap.set(prefix, near ? { bucket: near, reason: "typo" } : { bucket: miscBucketName, reason: "small" });
3199
+ }
3200
+ const foldedAgg = /* @__PURE__ */ new Map();
3201
+ const classified = /* @__PURE__ */ new Map();
3202
+ const classes = [];
3203
+ for (const [name, p] of prelim) {
3204
+ let bucket = p.bucket;
3205
+ if (p.category === "section") {
3206
+ const remap = sectionRemap.get(p.bucket);
3207
+ if (remap) {
3208
+ bucket = remap.bucket;
3209
+ const key = `${p.bucket}\u2192${remap.bucket}`;
3210
+ const agg = foldedAgg.get(key) ?? { from: p.bucket, into: remap.bucket, reason: remap.reason, classes: 0 };
3211
+ agg.classes += 1;
3212
+ foldedAgg.set(key, agg);
3213
+ }
3214
+ }
3215
+ classified.set(name, { category: p.category, bucket });
3216
+ classes.push({
3217
+ name,
3218
+ category: p.category,
3219
+ proposedBucket: bucket,
3220
+ ruleCount: p.accum.ruleCount,
3221
+ declarations: p.accum.declarations,
3222
+ breakpoints: [...p.accum.breakpoints].sort(),
3223
+ pagesUsedIn: p.pagesUsedIn,
3224
+ selectors: p.accum.selectors,
3225
+ compoundOnly: !p.accum.ownsSimple
3226
+ });
3227
+ }
3228
+ classes.sort((x, y) => y.declarations - x.declarations || x.name.localeCompare(y.name));
3229
+ 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));
3230
+ const vendor = emptyBucket();
3231
+ const webflowAuto = emptyBucket();
3232
+ const shared = emptyBucket();
3233
+ const sections = {};
3234
+ const crossBoundary = [];
3235
+ const bucketStat = (bucket) => {
3236
+ if (bucket === "vendor") return vendor;
3237
+ if (bucket === "webflow-auto") return webflowAuto;
3238
+ if (bucket === "shared") return shared;
3239
+ let stat = sections[bucket];
3240
+ if (!stat) {
3241
+ stat = emptyBucket();
3242
+ sections[bucket] = stat;
3243
+ }
3244
+ return stat;
3245
+ };
3246
+ for (const rule of rules) {
3247
+ const ownerPool = rule.subjectClasses.length > 0 ? rule.subjectClasses : rule.classes;
3248
+ let owner = "base";
3249
+ let bestRank = -1;
3250
+ const sectionBucketsTouched = /* @__PURE__ */ new Set();
3251
+ for (const cls of rule.classes) {
3252
+ const c = classified.get(cls);
3253
+ if (c?.category === "section" && c.bucket !== miscBucketName) sectionBucketsTouched.add(c.bucket);
3254
+ }
3255
+ for (const cls of ownerPool) {
3256
+ const c = classified.get(cls);
3257
+ if (!c) continue;
3258
+ const rank = c.category === "section" ? 4 : c.category === "shared" ? 3 : c.category === "webflow-auto" ? 2 : 1;
3259
+ if (rank > bestRank) {
3260
+ bestRank = rank;
3261
+ owner = c.bucket;
3262
+ }
3263
+ }
3264
+ const stat = bucketStat(owner);
3265
+ stat.rules += 1;
3266
+ stat.declarations += rule.declarations;
3267
+ stat.approxBytes += rule.bytes;
3268
+ if (sectionBucketsTouched.size >= 2 && crossBoundary.length < flagLimit) {
3269
+ crossBoundary.push({
3270
+ selector: rule.classes.map((c) => `.${c}`).join(""),
3271
+ media: rule.media,
3272
+ classes: rule.classes,
3273
+ buckets: [...sectionBucketsTouched].sort()
3274
+ });
3275
+ }
3276
+ }
3277
+ for (const info of classes) {
3278
+ bucketStat(info.proposedBucket).classes += 1;
3279
+ }
3280
+ const fuzzyMiddle = [];
3281
+ const deadClasses = [];
3282
+ for (const info of classes) {
3283
+ if (info.category === "section" && info.pagesUsedIn >= 2 && info.pagesUsedIn < sharedMinPages) {
3284
+ fuzzyMiddle.push(info.name);
3285
+ }
3286
+ if (info.category !== "vendor" && info.pagesUsedIn === 0) {
3287
+ deadClasses.push(info.name);
3288
+ }
3289
+ }
3290
+ const pageOnlyClasses = [];
3291
+ for (const cls of pageUseCount.keys()) {
3292
+ if (!classMap.has(cls)) pageOnlyClasses.push(cls);
3293
+ }
3294
+ pageOnlyClasses.sort();
3295
+ deadClasses.sort();
3296
+ return {
3297
+ source: {
3298
+ bytes: css.length,
3299
+ rules: totalRules,
3300
+ selectors: totalSelectors,
3301
+ declarations: totalDeclarations,
3302
+ pageCount,
3303
+ sharedMinPages
3304
+ },
3305
+ breakpoints,
3306
+ buckets: { base, vendor, webflowAuto, shared, sections },
3307
+ classes,
3308
+ flags: {
3309
+ crossBoundarySelectors: crossBoundary,
3310
+ fuzzyMiddle: fuzzyMiddle.slice(0, flagLimit),
3311
+ deadClasses: deadClasses.slice(0, flagLimit),
3312
+ pageOnlyClasses: pageOnlyClasses.slice(0, flagLimit),
3313
+ foldedSections
3314
+ }
3315
+ };
3316
+ }
3317
+ function collectClassesFromPageData(pageData) {
3318
+ const out = /* @__PURE__ */ new Set();
3319
+ const walk = (value) => {
3320
+ if (!value) return;
3321
+ if (Array.isArray(value)) {
3322
+ for (const item of value) walk(item);
3323
+ return;
3324
+ }
3325
+ if (typeof value === "object") {
3326
+ for (const [key, v] of Object.entries(value)) {
3327
+ if ((key === "class" || key === "className") && typeof v === "string") {
3328
+ for (const token of v.split(/\s+/)) {
3329
+ if (token) out.add(token);
3330
+ }
3331
+ } else {
3332
+ walk(v);
3333
+ }
3334
+ }
3335
+ }
3336
+ };
3337
+ walk(pageData);
3338
+ return [...out];
3339
+ }
3340
+ function renderAuditMarkdown(report) {
3341
+ const { source, buckets, breakpoints, flags } = report;
3342
+ const kb = (n) => `${(n / 1024).toFixed(1)} KB`;
3343
+ const lines = [];
3344
+ lines.push("# CSS audit");
3345
+ lines.push("");
3346
+ lines.push(
3347
+ `**${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`
3348
+ );
3349
+ lines.push("");
3350
+ lines.push(`Breakpoints: ${breakpoints.length ? breakpoints.map((b) => `\`${b}\``).join(", ") : "(none)"}`);
3351
+ lines.push("");
3352
+ lines.push("## Proposed buckets");
3353
+ lines.push("");
3354
+ lines.push("| bucket | classes | rules | declarations | ~size |");
3355
+ lines.push("| --- | --- | --- | --- | --- |");
3356
+ const row = (name, b) => `| ${name} | ${b.classes} | ${b.rules} | ${b.declarations} | ${kb(b.approxBytes)} |`;
3357
+ lines.push(row("base (reset/typography)", buckets.base));
3358
+ lines.push(row("vendor (.w-*)", buckets.vendor));
3359
+ lines.push(row("webflow-auto (div-block/text-block/\u2026)", buckets.webflowAuto));
3360
+ lines.push(row("shared", buckets.shared));
3361
+ const sectionEntries = Object.entries(buckets.sections).sort((a, b) => b[1].approxBytes - a[1].approxBytes);
3362
+ for (const [prefix, stat] of sectionEntries) {
3363
+ if (prefix === "misc") continue;
3364
+ lines.push(row(`section: ${prefix}`, stat));
3365
+ }
3366
+ const misc = buckets.sections.misc;
3367
+ if (misc) lines.push(row("misc (folded small/orphan)", misc));
3368
+ lines.push("");
3369
+ lines.push(`_${sectionEntries.filter(([p]) => p !== "misc").length} real sections + ${misc ? "misc" : "no misc"}_`);
3370
+ lines.push("");
3371
+ lines.push("## Flags");
3372
+ lines.push("");
3373
+ lines.push(`- Cross-boundary selectors (touch \u22652 sections): **${flags.crossBoundarySelectors.length}**`);
3374
+ lines.push(`- Fuzzy-middle classes (near shared threshold): **${flags.fuzzyMiddle.length}**`);
3375
+ lines.push(`- Dead classes (defined, unused on any page): **${flags.deadClasses.length}**`);
3376
+ lines.push(`- Page-only classes (used in pages, not in this sheet): **${flags.pageOnlyClasses.length}**`);
3377
+ const foldedClasses = flags.foldedSections.reduce((n, f) => n + f.classes, 0);
3378
+ lines.push(
3379
+ `- Folded section buckets (small/typo \u2192 misc/kept): **${flags.foldedSections.length}** (${foldedClasses} classes)`
3380
+ );
3381
+ const typos = flags.foldedSections.filter((f) => f.reason === "typo");
3382
+ if (typos.length) {
3383
+ lines.push("");
3384
+ lines.push("Typo merges:");
3385
+ for (const f of typos) lines.push(`- \`${f.from}\` \u2192 \`${f.into}\` (${f.classes})`);
3386
+ }
3387
+ if (flags.crossBoundarySelectors.length) {
3388
+ lines.push("");
3389
+ lines.push("Sample cross-boundary selectors:");
3390
+ for (const f of flags.crossBoundarySelectors.slice(0, 10)) {
3391
+ lines.push(`- \`${f.selector}\`${f.media ? ` @ ${f.media}` : ""} \u2192 ${f.buckets.join(" + ")}`);
3392
+ }
3393
+ }
3394
+ return lines.join("\n");
3395
+ }
3396
+
3397
+ // lib/server/routes/api/cssAudit.ts
3398
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".meno", "rendered-websites"]);
3399
+ function findMirrorCss(root, maxDepth = 6) {
3400
+ const found = [];
3401
+ const walk = (dir, depth) => {
3402
+ if (depth > maxDepth) return;
3403
+ let entries;
3404
+ try {
3405
+ entries = readdirSync2(dir);
3406
+ } catch {
3407
+ return;
3408
+ }
3409
+ for (const entry of entries) {
3410
+ const full = path.join(dir, entry);
3411
+ let isDir = false;
3412
+ try {
3413
+ isDir = statSync(full).isDirectory();
3414
+ } catch {
3415
+ continue;
3416
+ }
3417
+ if (isDir) {
3418
+ if (SKIP_DIRS.has(entry)) continue;
3419
+ walk(full, depth + 1);
3420
+ } else if (entry.endsWith(".css") && full.replace(/\\/g, "/").includes("/_mirror/css/")) {
3421
+ found.push(full);
3422
+ }
3423
+ }
3424
+ };
3425
+ walk(root, 0);
3426
+ return found.sort();
3427
+ }
3428
+ function resolveCssParam(root, rel) {
3429
+ const resolved = path.resolve(root, rel);
3430
+ const normalizedRoot = path.resolve(root);
3431
+ if (resolved !== normalizedRoot && !resolved.startsWith(normalizedRoot + path.sep)) return null;
3432
+ return existsSync5(resolved) ? resolved : null;
3433
+ }
3434
+ function handleCssAuditRoute(url, pageService) {
3435
+ const root = getProjectRoot();
3436
+ const cssParam = url.searchParams.get("css");
3437
+ let cssFiles;
3438
+ if (cssParam) {
3439
+ const resolved = resolveCssParam(root, cssParam);
3440
+ if (!resolved) {
3441
+ return jsonResponse({ error: `Stylesheet not found or outside project: ${cssParam}` }, { status: 404 });
3442
+ }
3443
+ cssFiles = [resolved];
3444
+ } else {
3445
+ cssFiles = findMirrorCss(root);
3446
+ if (cssFiles.length === 0) {
3447
+ return jsonResponse(
3448
+ {
3449
+ error: "No mirror stylesheet found",
3450
+ hint: "Expected a Webflow export at public/_mirror/css/*.css. Pass ?css=<relative path> to audit a specific stylesheet."
3451
+ },
3452
+ { status: 404 }
3453
+ );
3454
+ }
3455
+ }
3456
+ const css = cssFiles.map((f) => readFileSync2(f, "utf8")).join("\n");
3457
+ const pages = [];
3458
+ for (const pagePath of pageService.getAllPagePaths()) {
3459
+ const data = pageService.getPageData(pagePath);
3460
+ if (!data) continue;
3461
+ pages.push({ path: pagePath, classes: collectClassesFromPageData(data) });
3462
+ }
3463
+ const intParam = (name) => {
3464
+ const raw = url.searchParams.get(name);
3465
+ if (!raw) return void 0;
3466
+ const n = Number.parseInt(raw, 10);
3467
+ return Number.isFinite(n) ? n : void 0;
3468
+ };
3469
+ const options = {};
3470
+ const sharedMinPages = intParam("sharedMinPages");
3471
+ const minSectionClasses = intParam("minSectionClasses");
3472
+ if (sharedMinPages !== void 0) options.sharedMinPages = sharedMinPages;
3473
+ if (minSectionClasses !== void 0) options.minSectionClasses = minSectionClasses;
3474
+ const report = auditStylesheet({
3475
+ css,
3476
+ pages,
3477
+ options: Object.keys(options).length > 0 ? options : void 0
3478
+ });
3479
+ const cssFilesRel = cssFiles.map((f) => path.relative(root, f));
3480
+ const format = url.searchParams.get("format");
3481
+ if (format === "md" || format === "markdown") {
3482
+ const md = `${renderAuditMarkdown(report)}
3483
+
3484
+ _Audited: ${cssFilesRel.join(", ")}_
3485
+ `;
3486
+ return new Response(md, {
3487
+ headers: { "Content-Type": "text/markdown; charset=utf-8", "Cache-Control": "no-store, max-age=0" }
3488
+ });
3489
+ }
3490
+ return jsonResponse({ ...report, cssFiles: cssFilesRel });
3491
+ }
3492
+
3493
+ // lib/server/routes/api/deMirror.ts
3494
+ import { readFileSync as readFileSync3 } from "node:fs";
3495
+ import path2 from "node:path";
3496
+
3497
+ // lib/server/deMirror.ts
3498
+ import * as csstree2 from "css-tree";
3499
+ var CLASS_TOKEN_RE2 = /\.(-?[_a-zA-Z][\w-]*)/g;
3500
+ var FUNCTIONAL_PSEUDO_RE2 = /:(not|is|where|has|matches)\([^()]*\)/gi;
3501
+ function classTokens2(selector) {
3502
+ const out = [];
3503
+ let m;
3504
+ CLASS_TOKEN_RE2.lastIndex = 0;
3505
+ while ((m = CLASS_TOKEN_RE2.exec(selector)) !== null) {
3506
+ if (m[1]) out.push(m[1]);
3507
+ }
3508
+ return out;
3509
+ }
3510
+ function subjectClassesOf(selector) {
3511
+ const cleaned = selector.replace(FUNCTIONAL_PSEUDO_RE2, " ");
3512
+ const segments = cleaned.split(/\s*[>+~]\s*|\s+/).filter((s) => s.trim().length > 0);
3513
+ const subject = segments[segments.length - 1] ?? cleaned;
3514
+ return classTokens2(subject);
3515
+ }
3516
+ function declBlock(block) {
3517
+ const lines = [];
3518
+ if (block && block.type === "Block") {
3519
+ block.children.forEach((child) => {
3520
+ if (child.type !== "Declaration") return;
3521
+ const g = csstree2.generate(child);
3522
+ const i = g.indexOf(":");
3523
+ lines.push(` ${i > 0 ? `${g.slice(0, i)}: ${g.slice(i + 1)}` : g};`);
3524
+ });
3525
+ }
3526
+ return { text: lines.join("\n"), count: lines.length };
3527
+ }
3528
+ function captureClassRules(css) {
3529
+ const ast = csstree2.parse(css, { parseValue: false, parseCustomProperty: false });
3530
+ const out = [];
3531
+ const visit = (container, media, skip) => {
3532
+ const children = "children" in container ? container.children : null;
3533
+ if (!children) return;
3534
+ children.forEach((node) => {
3535
+ if (node.type === "Atrule") {
3536
+ const name = node.name.toLowerCase();
3537
+ if (name === "media") {
3538
+ const prelude = node.prelude ? csstree2.generate(node.prelude) : "";
3539
+ if (node.block) visit(node.block, prelude, false);
3540
+ } else if (name === "supports") {
3541
+ if (node.block) visit(node.block, media, false);
3542
+ } else if (node.block) {
3543
+ visit(node.block, media, true);
3544
+ }
3545
+ return;
3546
+ }
3547
+ if (node.type !== "Rule" || skip) return;
3548
+ if (node.prelude?.type !== "SelectorList") return;
3549
+ const selectors = [];
3550
+ const subject = /* @__PURE__ */ new Set();
3551
+ const all = /* @__PURE__ */ new Set();
3552
+ node.prelude.children.forEach((sel) => {
3553
+ const text = csstree2.generate(sel);
3554
+ const cls = classTokens2(text);
3555
+ if (cls.length === 0) return;
3556
+ selectors.push(text);
3557
+ for (const c of cls) all.add(c);
3558
+ for (const c of subjectClassesOf(text)) subject.add(c);
3559
+ });
3560
+ if (selectors.length === 0) return;
3561
+ const { text: decls, count } = declBlock(node.block);
3562
+ out.push({
3563
+ selector: selectors.join(", "),
3564
+ media,
3565
+ subjectClasses: [...subject],
3566
+ allClasses: [...all],
3567
+ declarations: count,
3568
+ bytes: csstree2.generate(node).length,
3569
+ text: `${selectors.join(",\n")} {
3570
+ ${decls}
3571
+ }`
3572
+ });
3573
+ });
3574
+ };
3575
+ visit(ast, null, false);
3576
+ return out;
3577
+ }
3578
+ function assembleSlice(rules) {
3579
+ const base = rules.filter((r) => r.media === null);
3580
+ const byMedia = /* @__PURE__ */ new Map();
3581
+ for (const r of rules) {
3582
+ if (r.media === null) continue;
3583
+ const list = byMedia.get(r.media) ?? [];
3584
+ list.push(r);
3585
+ byMedia.set(r.media, list);
3586
+ }
3587
+ const parts = [];
3588
+ for (const r of base) parts.push(r.text);
3589
+ for (const [media, list] of byMedia) {
3590
+ const inner = list.map((r) => r.text.replace(/^/gm, " ")).join("\n");
3591
+ parts.push(`@media ${media} {
3592
+ ${inner}
3593
+ }`);
3594
+ }
3595
+ return parts.join("\n\n");
3596
+ }
3597
+ function planDeMirror(input) {
3598
+ const { css, components, options = {} } = input;
3599
+ const warningSampleLimit = options.warningSampleLimit ?? 40;
3600
+ const compClasses = components.map((c) => ({ name: c.name, set: new Set(c.classes) }));
3601
+ const rules = captureClassRules(css);
3602
+ const perComponent = /* @__PURE__ */ new Map();
3603
+ const sharedSelectors = [];
3604
+ const deadSelectors = [];
3605
+ let movedRules = 0;
3606
+ let movedBytes = 0;
3607
+ let sharedRules = 0;
3608
+ let deadRules = 0;
3609
+ let sharedBytes = 0;
3610
+ let deadBytes = 0;
3611
+ let totalBytes = 0;
3612
+ for (const rule of rules) {
3613
+ totalBytes += rule.bytes;
3614
+ const need = rule.subjectClasses.length > 0 ? rule.subjectClasses : rule.allClasses;
3615
+ const owners = [];
3616
+ for (const c of compClasses) {
3617
+ if (need.every((cls) => c.set.has(cls))) owners.push(c.name);
3618
+ }
3619
+ if (owners.length === 1) {
3620
+ const name = owners[0];
3621
+ const list = perComponent.get(name) ?? [];
3622
+ list.push(rule);
3623
+ perComponent.set(name, list);
3624
+ movedRules += 1;
3625
+ movedBytes += rule.bytes;
3626
+ } else if (owners.length === 0) {
3627
+ deadRules += 1;
3628
+ deadBytes += rule.bytes;
3629
+ if (deadSelectors.length < warningSampleLimit) deadSelectors.push(rule.selector);
3630
+ } else {
3631
+ sharedRules += 1;
3632
+ sharedBytes += rule.bytes;
3633
+ if (sharedSelectors.length < warningSampleLimit) sharedSelectors.push(rule.selector);
3634
+ }
3635
+ }
3636
+ const componentsOut = [];
3637
+ for (const [name, list] of perComponent) {
3638
+ const breakpoints = [...new Set(list.map((r) => r.media ?? ""))].sort();
3639
+ componentsOut.push({
3640
+ name,
3641
+ ruleCount: list.length,
3642
+ declarations: list.reduce((n, r) => n + r.declarations, 0),
3643
+ approxBytes: list.reduce((n, r) => n + r.bytes, 0),
3644
+ breakpoints,
3645
+ css: assembleSlice(list)
3646
+ });
3647
+ }
3648
+ componentsOut.sort((a, b) => b.approxBytes - a.approxBytes || a.name.localeCompare(b.name));
3649
+ const roundTrip = movedRules + sharedRules + deadRules === rules.length;
3650
+ return {
3651
+ source: { classRules: rules.length, classRuleBytes: totalBytes, componentCount: components.length },
3652
+ components: componentsOut,
3653
+ global: { sharedRules, deadRules, approxBytes: sharedBytes + deadBytes },
3654
+ moved: { rules: movedRules, approxBytes: movedBytes, components: componentsOut.length },
3655
+ warnings: { sharedSelectors, deadSelectors },
3656
+ roundTrip
3657
+ };
3658
+ }
3659
+ function renderDeMirrorMarkdown(plan) {
3660
+ const kb = (n) => `${(n / 1024).toFixed(1)} KB`;
3661
+ const lines = [];
3662
+ lines.push("# De-mirror plan");
3663
+ lines.push("");
3664
+ lines.push(
3665
+ `${plan.source.classRules} class rules (${kb(plan.source.classRuleBytes)}) across ${plan.source.componentCount} components \xB7 round-trip ${plan.roundTrip ? "\u2705 lossless" : "\u274C FAILED"}`
3666
+ );
3667
+ lines.push("");
3668
+ lines.push(
3669
+ `**Moves onto components:** ${plan.moved.rules} rules \u2192 ${plan.moved.components} components (${kb(plan.moved.approxBytes)})`
3670
+ );
3671
+ lines.push(
3672
+ `**Stays global:** ${plan.global.sharedRules} shared + ${plan.global.deadRules} dead = ${kb(plan.global.approxBytes)}`
3673
+ );
3674
+ lines.push("");
3675
+ lines.push("## Slices (largest first)");
3676
+ lines.push("");
3677
+ lines.push("| component | rules | declarations | ~size | breakpoints |");
3678
+ lines.push("| --- | --- | --- | --- | --- |");
3679
+ for (const c of plan.components.slice(0, 40)) {
3680
+ lines.push(`| ${c.name} | ${c.ruleCount} | ${c.declarations} | ${kb(c.approxBytes)} | ${c.breakpoints.length} |`);
3681
+ }
3682
+ if (plan.components.length > 40) lines.push(`| \u2026 +${plan.components.length - 40} more | | | | |`);
3683
+ lines.push("");
3684
+ lines.push(
3685
+ `_Shared (kept global): ${plan.warnings.sharedSelectors.slice(0, 6).join(", ") || "none"}${plan.warnings.sharedSelectors.length > 6 ? " \u2026" : ""}_`
3686
+ );
3687
+ return lines.join("\n");
3688
+ }
3689
+
3690
+ // lib/server/routes/api/deMirror.ts
3691
+ function componentsUsedByPage(pageData, allNames) {
3692
+ const hay = JSON.stringify(pageData ?? "");
3693
+ const used = /* @__PURE__ */ new Set();
3694
+ for (const name of allNames) {
3695
+ if (hay.includes(`"${name}"`)) used.add(name);
3696
+ }
3697
+ return used;
3698
+ }
3699
+ function handleDeMirrorRoute(url, pageService, componentService) {
3700
+ const root = getProjectRoot();
3701
+ const cssParam = url.searchParams.get("css");
3702
+ let cssFiles;
3703
+ if (cssParam) {
3704
+ const resolved = resolveCssParam(root, cssParam);
3705
+ if (!resolved) {
3706
+ return jsonResponse({ error: `Stylesheet not found or outside project: ${cssParam}` }, { status: 404 });
3707
+ }
3708
+ cssFiles = [resolved];
3709
+ } else {
3710
+ cssFiles = findMirrorCss(root);
3711
+ if (cssFiles.length === 0) {
3712
+ return jsonResponse(
3713
+ {
3714
+ error: "No mirror stylesheet found",
3715
+ hint: "Expected a Webflow export at public/_mirror/css/*.css. Pass ?css=<relative path> to target a specific stylesheet."
3716
+ },
3717
+ { status: 404 }
3718
+ );
3719
+ }
3720
+ }
3721
+ const css = cssFiles.map((f) => readFileSync3(f, "utf8")).join("\n");
3722
+ const all = componentService.getAllComponents();
3723
+ const components = Object.entries(all).map(([name, def]) => ({
3724
+ name,
3725
+ classes: collectClassesFromPageData(def)
3726
+ }));
3727
+ if (components.length === 0) {
3728
+ return jsonResponse(
3729
+ {
3730
+ error: "No components found",
3731
+ hint: "Componentize the project first (e.g. /componentize-mirror), then de-mirror."
3732
+ },
3733
+ { status: 422 }
3734
+ );
3735
+ }
3736
+ const plan = planDeMirror({ css, components });
3737
+ const slug = url.searchParams.get("slug");
3738
+ let scoped = plan.components;
3739
+ let scopedNote;
3740
+ if (slug) {
3741
+ const pagePath = slug === "index" ? "/" : `/${slug.replace(/^\//, "")}`;
3742
+ const pageData = pageService.getPageData(pagePath);
3743
+ if (!pageData) {
3744
+ return jsonResponse({ error: `Page not found: ${slug}` }, { status: 404 });
3745
+ }
3746
+ const used = componentsUsedByPage(
3747
+ pageData,
3748
+ components.map((c) => c.name)
3749
+ );
3750
+ scoped = plan.components.filter((c) => used.has(c.name));
3751
+ scopedNote = `filtered to ${scoped.length} component(s) used by /${slug} (ownership computed project-wide)`;
3752
+ }
3753
+ const cssFilesRel = cssFiles.map((f) => path2.relative(root, f));
3754
+ const view = { ...plan, components: scoped };
3755
+ const format = url.searchParams.get("format");
3756
+ if (format === "md" || format === "markdown") {
3757
+ const md = `${renderDeMirrorMarkdown(view)}
3758
+ ${scopedNote ? `
3759
+ _${scopedNote}_
3760
+ ` : ""}
3761
+ _Sheet: ${cssFilesRel.join(", ")}_
3762
+ `;
3763
+ return new Response(md, {
3764
+ headers: { "Content-Type": "text/markdown; charset=utf-8", "Cache-Control": "no-store, max-age=0" }
3765
+ });
3766
+ }
3767
+ const includeSlices = url.searchParams.get("slices") === "true";
3768
+ const responseComponents = includeSlices ? scoped : scoped.map(({ css: _css, ...meta }) => meta);
3769
+ return jsonResponse({
3770
+ ...view,
3771
+ components: responseComponents,
3772
+ cssFiles: cssFilesRel,
3773
+ ...scopedNote ? { scopedNote } : {},
3774
+ dryRun: true
3775
+ });
3776
+ }
3777
+
3778
+ // lib/server/routes/api/functions.ts
3779
+ import path3 from "node:path";
2885
3780
  async function loadProjectEnv() {
2886
3781
  const envPath = projectPaths.env();
2887
3782
  const env = {};
@@ -2915,7 +3810,7 @@ async function handleFunctionsRoute(req, url) {
2915
3810
  return void 0;
2916
3811
  }
2917
3812
  const functionPath = url.pathname.replace("/api/", "");
2918
- const functionFilePath = path.join(projectPaths.functions(), "api", `${functionPath}.ts`);
3813
+ const functionFilePath = path3.join(projectPaths.functions(), "api", `${functionPath}.ts`);
2919
3814
  if (!await fileExists(functionFilePath)) {
2920
3815
  const jsFilePath = functionFilePath.replace(".ts", ".js");
2921
3816
  if (!await fileExists(jsFilePath)) {
@@ -3072,6 +3967,18 @@ async function handleCoreApiRoutes(req, url, context) {
3072
3967
  if (url.pathname === "/api/enums" && req.method === "GET") {
3073
3968
  return await handleRouteError(() => handleEnumsRoute(), "Failed to fetch enums");
3074
3969
  }
3970
+ if (url.pathname === "/api/css-audit" && req.method === "GET") {
3971
+ return await handleRouteError(
3972
+ () => Promise.resolve(handleCssAuditRoute(url, pageService)),
3973
+ "Failed to generate CSS audit"
3974
+ );
3975
+ }
3976
+ if (url.pathname === "/api/de-mirror" && req.method === "GET") {
3977
+ return await handleRouteError(
3978
+ () => Promise.resolve(handleDeMirrorRoute(url, pageService, componentService)),
3979
+ "Failed to generate de-mirror plan"
3980
+ );
3981
+ }
3075
3982
  if (url.pathname === "/api/usage" && req.method === "GET") {
3076
3983
  return await handleRouteError(() => {
3077
3984
  const cmsCollections = cmsService ? cmsService.getAllSchemas().size : 0;
@@ -3647,21 +4554,21 @@ function processCMSPropsTemplate(props, cmsItem, locale, i18nConfig) {
3647
4554
  }
3648
4555
 
3649
4556
  // lib/server/ssr/imageMetadata.ts
3650
- import * as path2 from "node:path";
4557
+ import * as path4 from "node:path";
3651
4558
  var RESPONSIVE_WIDTHS = [500, 800, 1080, 1600, 2400];
3652
4559
  var DEFAULT_SIZES = "100vw";
3653
4560
  async function buildImageMetadataMap() {
3654
4561
  const metadataMap = /* @__PURE__ */ new Map();
3655
4562
  try {
3656
4563
  const imagesDir = projectPaths.images();
3657
- const manifestPath = path2.join(imagesDir, "manifest.json");
4564
+ const manifestPath = path4.join(imagesDir, "manifest.json");
3658
4565
  if (!await fileExists(manifestPath)) {
3659
4566
  return metadataMap;
3660
4567
  }
3661
4568
  const manifestContent = await readTextFile(manifestPath);
3662
4569
  const manifest = JSON.parse(manifestContent);
3663
4570
  for (const [filename, entry] of Object.entries(manifest.images)) {
3664
- const baseName = path2.basename(filename, path2.extname(filename));
4571
+ const baseName = path4.basename(filename, path4.extname(filename));
3665
4572
  const webpSrcsetParts = [];
3666
4573
  const avifSrcsetParts = [];
3667
4574
  for (const variant of entry.variants) {
@@ -3839,8 +4746,8 @@ function resolveFilterValue(value, scope) {
3839
4746
  if (typeof value !== "string" || !value.startsWith("{{") || !value.endsWith("}}")) {
3840
4747
  return value;
3841
4748
  }
3842
- const path3 = value.slice(2, -2).trim();
3843
- const resolved = scope ? getNestedValue(scope, path3) : void 0;
4749
+ const path5 = value.slice(2, -2).trim();
4750
+ const resolved = scope ? getNestedValue(scope, path5) : void 0;
3844
4751
  return resolved !== void 0 ? resolved : UNRESOLVED_FILTER_VALUE;
3845
4752
  }
3846
4753
  function resolveFilterTemplates(filter, scope, collection) {
@@ -4250,8 +5157,8 @@ function getPropItems(source, ctx) {
4250
5157
  if (source.startsWith("{{") && source.endsWith("}}")) {
4251
5158
  const templateCtx = ctx.templateContext;
4252
5159
  if (templateCtx) {
4253
- const path3 = source.slice(2, -2).trim();
4254
- const resolved = getNestedValue(templateCtx, path3);
5160
+ const path5 = source.slice(2, -2).trim();
5161
+ const resolved = getNestedValue(templateCtx, path5);
4255
5162
  if (Array.isArray(resolved)) {
4256
5163
  return resolved;
4257
5164
  } else if (resolved !== void 0) {
@@ -4288,12 +5195,12 @@ function buildNodeElementClass(ctx, label, isSlotContent2) {
4288
5195
  const effectiveFileType = useComponentContext ? "component" : "page";
4289
5196
  const effectiveFileName = useComponentContext ? ctx.componentContext : pagePath ? pagePath.replace(/^\//, "").replace(/\//g, "_") || "index" : "page";
4290
5197
  const rawPath = ctx.elementPath || [];
4291
- const path3 = useComponentContext && ctx.componentRootPath ? rawPath.slice(ctx.componentRootPath.length) : rawPath;
5198
+ const path5 = useComponentContext && ctx.componentRootPath ? rawPath.slice(ctx.componentRootPath.length) : rawPath;
4292
5199
  const elementClassCtx = {
4293
5200
  fileType: effectiveFileType,
4294
5201
  fileName: effectiveFileName || "page",
4295
5202
  label,
4296
- path: path3
5203
+ path: path5
4297
5204
  };
4298
5205
  return generateElementClassName(elementClassCtx);
4299
5206
  }
@@ -5454,6 +6361,7 @@ function generateAllInlineDataScripts(collections) {
5454
6361
  }
5455
6362
 
5456
6363
  // lib/server/ssr/htmlGenerator.ts
6364
+ import { posix } from "node:path";
5457
6365
  function generateImagePreloadTags(preloadImages) {
5458
6366
  if (preloadImages.length === 0) return "";
5459
6367
  return preloadImages.map(
@@ -5464,6 +6372,43 @@ function minifyCSS(code) {
5464
6372
  if (!code.trim()) return code;
5465
6373
  return code.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\s*([{};:,>~])\s*/g, "$1").replace(/\s+/g, " ").replace(/\{\s+/g, "{").replace(/\s+\}/g, "}").replace(/;}/g, "}").trim();
5466
6374
  }
6375
+ async function collectPageCssImports(frontmatter, pagePath, nonceAttr) {
6376
+ if (!frontmatter?.includes(".css")) return "";
6377
+ const seen = /* @__PURE__ */ new Set();
6378
+ const specs = [];
6379
+ const re = /import\b[^'"]*['"]([^'"]+\.css)(?:\?[^'"]*)?['"]/g;
6380
+ let m;
6381
+ while ((m = re.exec(frontmatter)) !== null) {
6382
+ const spec = m[1];
6383
+ if (!spec || seen.has(spec)) continue;
6384
+ seen.add(spec);
6385
+ if (spec.endsWith("styles/theme.css")) continue;
6386
+ specs.push(spec);
6387
+ }
6388
+ if (specs.length === 0) return "";
6389
+ const pageDir = posix.join("src/pages", posix.dirname(pagePath || "/"));
6390
+ const tags = [];
6391
+ for (const spec of specs) {
6392
+ if (/^(?:https?:)?\/\//.test(spec)) {
6393
+ tags.push(`<link rel="stylesheet" href="${escapeHtml(spec)}">`);
6394
+ continue;
6395
+ }
6396
+ const candidates = spec.startsWith("/") ? [spec.slice(1)] : [posix.normalize(posix.join(pageDir, spec)), `src/${spec.replace(/^(?:\.\.?\/)+/, "")}`];
6397
+ let css = null;
6398
+ for (const rel of candidates) {
6399
+ if (rel.startsWith("..")) continue;
6400
+ try {
6401
+ css = await readTextFile(resolveProjectPath(rel));
6402
+ break;
6403
+ } catch {
6404
+ }
6405
+ }
6406
+ if (css == null) continue;
6407
+ const content = rewriteViewportUnitsInStylesheet(css).replace(/<\/style/gi, "<\\/style");
6408
+ tags.push(`<style${nonceAttr} data-meno-page-css="${escapeHtml(spec)}">${content}</style>`);
6409
+ }
6410
+ return tags.join("\n ");
6411
+ }
5467
6412
  async function generateSSRHTML(pageDataOrOptions, globalComponents = {}, pagePath = "/", baseUrl = "", useBuiltBundle = false, locale, slugMappings, cmsContext, cmsService, externalScriptPath) {
5468
6413
  let options;
5469
6414
  if ("pageData" in pageDataOrOptions) {
@@ -5485,7 +6430,7 @@ async function generateSSRHTML(pageDataOrOptions, globalComponents = {}, pagePat
5485
6430
  const {
5486
6431
  pageData,
5487
6432
  globalComponents: components = {},
5488
- pagePath: path3 = "/",
6433
+ pagePath: path5 = "/",
5489
6434
  baseUrl: base = "",
5490
6435
  useBuiltBundle: useBundled = false,
5491
6436
  locale: loc,
@@ -5509,7 +6454,7 @@ async function generateSSRHTML(pageDataOrOptions, globalComponents = {}, pagePat
5509
6454
  const rendered = await renderPageSSR(
5510
6455
  pageData,
5511
6456
  components,
5512
- path3,
6457
+ path5,
5513
6458
  base,
5514
6459
  loc,
5515
6460
  void 0,
@@ -5696,7 +6641,7 @@ picture {
5696
6641
  line-height: normal;
5697
6642
  }`;
5698
6643
  const combinedCSS = [fontCSS, themeColorVariablesCSS, variablesCSS, baseCSS, componentCSS, utilityCSS, interactiveCSS].filter(Boolean).join("\n");
5699
- const cssWithStableViewport = rewriteViewportUnits(combinedCSS);
6644
+ const cssWithStableViewport = rewriteViewportUnitsInStylesheet(combinedCSS);
5700
6645
  const finalCSS = useBundled ? minifyCSS(cssWithStableViewport) : cssWithStableViewport;
5701
6646
  const prefetchConfig = await loadPrefetchConfig();
5702
6647
  const menoConfig = prefetchConfig.enabled ? { prefetch: prefetchConfig } : {};
@@ -5725,6 +6670,7 @@ ${externalJavaScript}`;
5725
6670
  const styleContent = useBundled ? finalCSS : `
5726
6671
  ${combinedCSS.split("\n").join("\n ")}
5727
6672
  `;
6673
+ const pageCssTags = await collectPageCssImports(pageData._frontmatter, pagePath, nonceAttr);
5728
6674
  const htmlDocument = `<!DOCTYPE html>
5729
6675
  <html lang="${rendered.locale}" theme="${themeConfig.default}">
5730
6676
  <head>
@@ -5738,7 +6684,8 @@ ${externalJavaScript}`;
5738
6684
  ` : ""}${libraryTags.headCSS ? `${libraryTags.headCSS}
5739
6685
  ` : ""}${libraryTags.headJS ? `${libraryTags.headJS}
5740
6686
  ` : ""}${rendered.meta}
5741
- ${configInlineScript}${cmsInlineScript}${clientDataScripts}<style id="meno-styles"${nonceAttr}>${styleContent}</style>${mergedCustomCode.head ? `
6687
+ ${configInlineScript}${cmsInlineScript}${clientDataScripts}<style id="meno-styles"${nonceAttr}>${styleContent}</style>${pageCssTags ? `
6688
+ ${pageCssTags}` : ""}${mergedCustomCode.head ? `
5742
6689
  ${mergedCustomCode.head}` : ""}
5743
6690
  </head>
5744
6691
  <body>${mergedCustomCode.bodyStart ? `
@@ -7146,8 +8093,8 @@ function singularizeWord(word) {
7146
8093
  if (word.endsWith("s")) return word.slice(0, -1);
7147
8094
  return word;
7148
8095
  }
7149
- function getNestedValue2(obj, path3) {
7150
- return path3.split(".").reduce((current, key) => {
8096
+ function getNestedValue2(obj, path5) {
8097
+ return path5.split(".").reduce((current, key) => {
7151
8098
  return current && typeof current === "object" ? current[key] : void 0;
7152
8099
  }, obj);
7153
8100
  }
@@ -8628,7 +9575,11 @@ async function createServer(config) {
8628
9575
  try {
8629
9576
  const server = await createRuntimeServer({
8630
9577
  port: currentPort,
8631
- hostname: "localhost",
9578
+ // Default `localhost` resolves to IPv6 `::1` only on macOS, so a
9579
+ // conventional `127.0.0.1 meno.local` hosts alias can't reach it. MENO_HOST
9580
+ // lets the standalone launcher bind broadly (e.g. `::` dual-stack) so both
9581
+ // IPv4 and IPv6 aliases work. Unset → unchanged `localhost` behavior.
9582
+ hostname: process.env.MENO_HOST || "localhost",
8632
9583
  wsPath: HMR_ROUTE,
8633
9584
  async fetch(req, upgradeWebSocket) {
8634
9585
  const url = new URL(req.url);
@@ -9023,8 +9974,8 @@ ${plainTextErrors}`;
9023
9974
  }
9024
9975
 
9025
9976
  // 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";
9977
+ import { existsSync as existsSync6, readdirSync as readdirSync3, mkdirSync, rmdirSync } from "node:fs";
9978
+ import { join as join4, relative, sep } from "node:path";
9028
9979
 
9029
9980
  // lib/server/utils/jsonLineMapper.ts
9030
9981
  function buildLineMap(jsonText) {
@@ -9068,13 +10019,13 @@ function buildLineMap(jsonText) {
9068
10019
  pos++;
9069
10020
  }
9070
10021
  }
9071
- function parseValue(path3, trackChildren) {
10022
+ function parseValue(path5, trackChildren) {
9072
10023
  skipWhitespace();
9073
10024
  const startPos = pos;
9074
10025
  if (jsonText[pos] === "{") {
9075
- parseObject(path3, trackChildren);
10026
+ parseObject(path5, trackChildren);
9076
10027
  } else if (jsonText[pos] === "[") {
9077
- parseArray(path3, trackChildren);
10028
+ parseArray(path5, trackChildren);
9078
10029
  } else if (jsonText[pos] === '"') {
9079
10030
  parseString();
9080
10031
  } else if (jsonText[pos] === "t") {
@@ -9088,14 +10039,14 @@ function buildLineMap(jsonText) {
9088
10039
  }
9089
10040
  return { start: startPos, end: pos };
9090
10041
  }
9091
- function recordTrackedRoot(path3) {
9092
- const { start, end } = parseValue(path3, true);
10042
+ function recordTrackedRoot(path5) {
10043
+ const { start, end } = parseValue(path5, true);
9093
10044
  lineMap.set("", {
9094
10045
  startLine: charToLine[start] ?? 0,
9095
10046
  endLine: charToLine[end - 1] || charToLine[start] || 0
9096
10047
  });
9097
10048
  }
9098
- function parseObject(path3, trackChildren) {
10049
+ function parseObject(path5, trackChildren) {
9099
10050
  pos++;
9100
10051
  skipWhitespace();
9101
10052
  while (pos < jsonText.length && jsonText[pos] !== "}") {
@@ -9104,14 +10055,14 @@ function buildLineMap(jsonText) {
9104
10055
  skipWhitespace();
9105
10056
  pos++;
9106
10057
  skipWhitespace();
9107
- if (key === "root" && path3.length === 0) {
9108
- recordTrackedRoot(path3);
9109
- } else if (key === "component" && path3.length === 0) {
9110
- parseComponentWrapper(path3);
10058
+ if (key === "root" && path5.length === 0) {
10059
+ recordTrackedRoot(path5);
10060
+ } else if (key === "component" && path5.length === 0) {
10061
+ parseComponentWrapper(path5);
9111
10062
  } else if (key === "children" && trackChildren) {
9112
- parseValue(path3, true);
10063
+ parseValue(path5, true);
9113
10064
  } else {
9114
- parseValue(path3, false);
10065
+ parseValue(path5, false);
9115
10066
  }
9116
10067
  skipWhitespace();
9117
10068
  if (jsonText[pos] === ",") pos++;
@@ -9119,9 +10070,9 @@ function buildLineMap(jsonText) {
9119
10070
  }
9120
10071
  pos++;
9121
10072
  }
9122
- function parseComponentWrapper(path3) {
10073
+ function parseComponentWrapper(path5) {
9123
10074
  if (jsonText[pos] !== "{") {
9124
- parseValue(path3, false);
10075
+ parseValue(path5, false);
9125
10076
  return;
9126
10077
  }
9127
10078
  pos++;
@@ -9133,9 +10084,9 @@ function buildLineMap(jsonText) {
9133
10084
  pos++;
9134
10085
  skipWhitespace();
9135
10086
  if (key === "structure") {
9136
- recordTrackedRoot(path3);
10087
+ recordTrackedRoot(path5);
9137
10088
  } else {
9138
- parseValue(path3, false);
10089
+ parseValue(path5, false);
9139
10090
  }
9140
10091
  skipWhitespace();
9141
10092
  if (jsonText[pos] === ",") pos++;
@@ -9143,12 +10094,12 @@ function buildLineMap(jsonText) {
9143
10094
  }
9144
10095
  pos++;
9145
10096
  }
9146
- function parseArray(path3, trackChildren) {
10097
+ function parseArray(path5, trackChildren) {
9147
10098
  pos++;
9148
10099
  skipWhitespace();
9149
10100
  let index = 0;
9150
10101
  while (pos < jsonText.length && jsonText[pos] !== "]") {
9151
- const childPath = [...path3, index];
10102
+ const childPath = [...path5, index];
9152
10103
  const { start, end } = parseValue(childPath, trackChildren);
9153
10104
  if (trackChildren) {
9154
10105
  const pathStr = childPath.join(",");
@@ -9221,9 +10172,12 @@ var PageService = class {
9221
10172
  return;
9222
10173
  }
9223
10174
  const pages = await this.provider.loadAll();
9224
- for (const [path3, content] of pages) {
10175
+ for (const [path5, content] of pages) {
9225
10176
  const lineMap = buildLineMap(content);
9226
- this.pageCache.set(path3, content, lineMap);
10177
+ this.pageCache.set(path5, content, lineMap);
10178
+ }
10179
+ for (const path5 of this.pageCache.keys()) {
10180
+ if (!pages.has(path5)) this.pageCache.delete(path5);
9227
10181
  }
9228
10182
  }
9229
10183
  /**
@@ -9240,8 +10194,8 @@ var PageService = class {
9240
10194
  * }
9241
10195
  * ```
9242
10196
  */
9243
- getPage(path3) {
9244
- return this.pageCache.getContent(path3);
10197
+ getPage(path5) {
10198
+ return this.pageCache.getContent(path5);
9245
10199
  }
9246
10200
  /**
9247
10201
  * Get parsed page data
@@ -9260,8 +10214,8 @@ var PageService = class {
9260
10214
  * }
9261
10215
  * ```
9262
10216
  */
9263
- getPageData(path3) {
9264
- const content = this.pageCache.getContent(path3);
10217
+ getPageData(path5) {
10218
+ const content = this.pageCache.getContent(path5);
9265
10219
  if (!content) {
9266
10220
  return null;
9267
10221
  }
@@ -9291,15 +10245,15 @@ var PageService = class {
9291
10245
  * });
9292
10246
  * ```
9293
10247
  */
9294
- async savePage(path3, data) {
10248
+ async savePage(path5, data) {
9295
10249
  if (!this.provider) {
9296
10250
  throw new Error("PageProvider not set");
9297
10251
  }
9298
10252
  const { _lineMap, ...dataToSave } = data;
9299
10253
  const content = JSON.stringify(dataToSave, null, 2);
9300
- await this.provider.save(path3, content);
10254
+ await this.provider.save(path5, content);
9301
10255
  const lineMap = buildLineMap(content);
9302
- this.pageCache.set(path3, content, lineMap);
10256
+ this.pageCache.set(path5, content, lineMap);
9303
10257
  }
9304
10258
  /**
9305
10259
  * Re-read a single page from disk via the provider and refresh the cache.
@@ -9313,22 +10267,22 @@ var PageService = class {
9313
10267
  * partial mid-write or parse error), in which case the cache is left intact and
9314
10268
  * the next watcher event retries.
9315
10269
  */
9316
- async reloadPageFromDisk(path3) {
10270
+ async reloadPageFromDisk(path5) {
9317
10271
  if (!this.provider) return false;
9318
10272
  let content = null;
9319
10273
  try {
9320
- content = await this.provider.get(path3);
10274
+ content = await this.provider.get(path5);
9321
10275
  } catch (error) {
9322
- logRuntimeError("pageService.reloadPageFromDisk", error, { path: path3 });
10276
+ logRuntimeError("pageService.reloadPageFromDisk", error, { path: path5 });
9323
10277
  return false;
9324
10278
  }
9325
10279
  if (content) {
9326
- this.pageCache.set(path3, content, buildLineMap(content));
10280
+ this.pageCache.set(path5, content, buildLineMap(content));
9327
10281
  return true;
9328
10282
  }
9329
10283
  try {
9330
- if (!await this.provider.exists(path3)) {
9331
- this.pageCache.delete(path3);
10284
+ if (!await this.provider.exists(path5)) {
10285
+ this.pageCache.delete(path5);
9332
10286
  return true;
9333
10287
  }
9334
10288
  } catch {
@@ -9351,10 +10305,10 @@ var PageService = class {
9351
10305
  * await pageService.deletePage('/old-page', true);
9352
10306
  * ```
9353
10307
  */
9354
- async deletePage(path3, deleteFromStorage = false) {
9355
- this.pageCache.delete(path3);
10308
+ async deletePage(path5, deleteFromStorage = false) {
10309
+ this.pageCache.delete(path5);
9356
10310
  if (deleteFromStorage && this.provider) {
9357
- await this.provider.delete(path3);
10311
+ await this.provider.delete(path5);
9358
10312
  }
9359
10313
  }
9360
10314
  /**
@@ -9382,26 +10336,34 @@ var PageService = class {
9382
10336
  *
9383
10337
  * @example
9384
10338
  * ```typescript
9385
- * const pages = pageService.getAllPagesWithInfo();
10339
+ * const pages = await pageService.getAllPagesWithInfo();
9386
10340
  * // [{ path: "/", isDraft: false }, { path: "/about", isDraft: true }]
9387
10341
  * ```
9388
10342
  */
9389
- getAllPagesWithInfo() {
10343
+ async getAllPagesWithInfo() {
9390
10344
  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
- });
10345
+ return Promise.all(
10346
+ paths.map(async (path5) => {
10347
+ const pageData = this.getPageData(path5);
10348
+ const pathWithoutSlash = path5 === "/" ? "index" : path5.substring(1);
10349
+ const slashIndex = pathWithoutSlash.lastIndexOf("/");
10350
+ const folder = slashIndex > 0 ? pathWithoutSlash.substring(0, slashIndex) : void 0;
10351
+ const metaSource = pageData?.meta?.source;
10352
+ const source = metaSource === "cms" || metaSource === "ssr" ? metaSource : void 0;
10353
+ let sourcePath;
10354
+ if (source === "cms" && this.provider?.resolveSourcePath) {
10355
+ const abs = await this.provider.resolveSourcePath(path5);
10356
+ if (abs) sourcePath = relative(getProjectRoot(), abs).split(sep).join("/");
10357
+ }
10358
+ return {
10359
+ path: path5,
10360
+ isDraft: pageData?.meta?.draft === true,
10361
+ ...folder ? { folder } : {},
10362
+ ...source ? { source } : {},
10363
+ ...sourcePath ? { sourcePath } : {}
10364
+ };
10365
+ })
10366
+ );
9405
10367
  }
9406
10368
  /**
9407
10369
  * Get line map for a page
@@ -9411,8 +10373,8 @@ var PageService = class {
9411
10373
  * @param path - Page path (e.g., "/" or "/about")
9412
10374
  * @returns LineMap object or undefined if page not found
9413
10375
  */
9414
- getLineMap(path3) {
9415
- return this.pageCache.getLineMap(path3);
10376
+ getLineMap(path5) {
10377
+ return this.pageCache.getLineMap(path5);
9416
10378
  }
9417
10379
  /**
9418
10380
  * Pages base directory — provider-aware (e.g. `src/pages` for astro projects),
@@ -9436,12 +10398,12 @@ var PageService = class {
9436
10398
  */
9437
10399
  getAllFolders() {
9438
10400
  const pagesDir = this.pagesBaseDir();
9439
- if (!existsSync5(pagesDir)) {
10401
+ if (!existsSync6(pagesDir)) {
9440
10402
  return [];
9441
10403
  }
9442
10404
  const collectFolders = (dir, prefix) => {
9443
10405
  try {
9444
- const entries = readdirSync2(dir, { withFileTypes: true });
10406
+ const entries = readdirSync3(dir, { withFileTypes: true });
9445
10407
  const result = [];
9446
10408
  for (const entry of entries) {
9447
10409
  if (!entry.isDirectory()) continue;
@@ -9479,7 +10441,7 @@ var PageService = class {
9479
10441
  }
9480
10442
  const pagesDir = this.pagesBaseDir();
9481
10443
  const folderPath = join4(pagesDir, trimmed);
9482
- if (existsSync5(folderPath)) {
10444
+ if (existsSync6(folderPath)) {
9483
10445
  throw new Error("Folder already exists");
9484
10446
  }
9485
10447
  mkdirSync(folderPath, { recursive: true });
@@ -9507,11 +10469,11 @@ var PageService = class {
9507
10469
  }
9508
10470
  if (newFolder) {
9509
10471
  const targetDir = join4(pagesDir, newFolder);
9510
- if (!existsSync5(targetDir)) {
10472
+ if (!existsSync6(targetDir)) {
9511
10473
  mkdirSync(targetDir, { recursive: true });
9512
10474
  }
9513
10475
  }
9514
- if (!existsSync5(sourceFile)) {
10476
+ if (!existsSync6(sourceFile)) {
9515
10477
  throw new Error(`Page not found: ${pagePath}`);
9516
10478
  }
9517
10479
  await rename2(sourceFile, targetFile);
@@ -9526,7 +10488,7 @@ var PageService = class {
9526
10488
  const sourceDir = slashIndex >= 0 ? join4(pagesDir, pageName.substring(0, slashIndex)) : null;
9527
10489
  if (sourceDir && sourceDir !== pagesDir) {
9528
10490
  try {
9529
- const remaining = readdirSync2(sourceDir);
10491
+ const remaining = readdirSync3(sourceDir);
9530
10492
  if (remaining.length === 0) {
9531
10493
  rmdirSync(sourceDir);
9532
10494
  }
@@ -9574,10 +10536,10 @@ var PageService = class {
9574
10536
  const ext = this.pageExt();
9575
10537
  const sourceFile = join4(pagesDir, `${oldName}${ext}`);
9576
10538
  const targetFile = join4(pagesDir, `${newName}${ext}`);
9577
- if (!existsSync5(sourceFile)) {
10539
+ if (!existsSync6(sourceFile)) {
9578
10540
  throw new Error(`Page not found: ${oldPath}`);
9579
10541
  }
9580
- if (existsSync5(targetFile)) {
10542
+ if (existsSync6(targetFile)) {
9581
10543
  throw new Error(`Page already exists: ${newPath}`);
9582
10544
  }
9583
10545
  await rename2(sourceFile, targetFile);
@@ -9605,17 +10567,17 @@ var PageService = class {
9605
10567
  getSlugMappings() {
9606
10568
  const mappings = [];
9607
10569
  const paths = this.pageCache.keys();
9608
- for (const path3 of paths) {
9609
- const pageData = this.getPageData(path3);
10570
+ for (const path5 of paths) {
10571
+ const pageData = this.getPageData(path5);
9610
10572
  if (pageData?.meta?.slugs) {
9611
- const pageId = path3 === "/" ? "index" : path3.substring(1);
10573
+ const pageId = path5 === "/" ? "index" : path5.substring(1);
9612
10574
  mappings.push({
9613
10575
  pageId,
9614
10576
  slugs: pageData.meta.slugs
9615
10577
  });
9616
10578
  } else {
9617
- const pageId = path3 === "/" ? "index" : path3.substring(1);
9618
- const defaultSlug = path3 === "/" ? "" : path3.substring(1);
10579
+ const pageId = path5 === "/" ? "index" : path5.substring(1);
10580
+ const defaultSlug = path5 === "/" ? "" : path5.substring(1);
9619
10581
  mappings.push({
9620
10582
  pageId,
9621
10583
  slugs: { _default: defaultSlug }
@@ -9628,7 +10590,7 @@ var PageService = class {
9628
10590
 
9629
10591
  // lib/server/services/componentService.ts
9630
10592
  import { join as join5 } from "node:path";
9631
- import { existsSync as existsSync6, mkdirSync as mkdirSync2, readdirSync as readdirSync3, rmdirSync as rmdirSync2 } from "node:fs";
10593
+ import { existsSync as existsSync7, mkdirSync as mkdirSync2, readdirSync as readdirSync4, rmdirSync as rmdirSync2 } from "node:fs";
9632
10594
  var ComponentService = class {
9633
10595
  components = /* @__PURE__ */ new Map();
9634
10596
  componentCategories = /* @__PURE__ */ new Map();
@@ -9637,6 +10599,36 @@ var ComponentService = class {
9637
10599
  writer;
9638
10600
  loadErrors = /* @__PURE__ */ new Map();
9639
10601
  loadWarnings = [];
10602
+ /** Per-component serialization of whole-file re-emits. @see withComponentWriteLock */
10603
+ writeLocks = /* @__PURE__ */ new Map();
10604
+ /**
10605
+ * Serialize whole-file re-emits for one component.
10606
+ *
10607
+ * `saveComponentCSS` and `saveComponentJavaScript` each read the in-memory
10608
+ * definition, re-emit the ENTIRE `.astro` file, then write the result back.
10609
+ * Fired concurrently — the Custom Code modal's Save issues a JS save AND a CSS
10610
+ * save together — their read-modify-write races: each reads a snapshot taken
10611
+ * before the other's `components.set`, so the writer that lands last clobbers
10612
+ * the other field with a stale value (a fresh CSS edit silently reverting is
10613
+ * the visible symptom). Chaining per component name makes each op observe the
10614
+ * previous op's committed state. Prior failures are swallowed on the chain tail
10615
+ * so one bad write can't wedge the queue.
10616
+ */
10617
+ withComponentWriteLock(name, fn) {
10618
+ const prev = this.writeLocks.get(name) ?? Promise.resolve();
10619
+ const run = prev.then(fn, fn);
10620
+ const tail = run.then(
10621
+ () => {
10622
+ },
10623
+ () => {
10624
+ }
10625
+ );
10626
+ this.writeLocks.set(name, tail);
10627
+ tail.finally(() => {
10628
+ if (this.writeLocks.get(name) === tail) this.writeLocks.delete(name);
10629
+ });
10630
+ return run;
10631
+ }
9640
10632
  /**
9641
10633
  * Creates a new ComponentService instance
9642
10634
  *
@@ -9891,12 +10883,12 @@ var ComponentService = class {
9891
10883
  */
9892
10884
  findComponentCategoryOnDisk(name) {
9893
10885
  const componentsDir = this.componentsBaseDir();
9894
- if (!existsSync6(componentsDir)) return void 0;
10886
+ if (!existsSync7(componentsDir)) return void 0;
9895
10887
  const ext = this.writer ? null : ".json";
9896
10888
  const existsAt = (dir) => {
9897
- if (ext) return existsSync6(join5(dir, `${name}${ext}`));
10889
+ if (ext) return existsSync7(join5(dir, `${name}${ext}`));
9898
10890
  try {
9899
- return readdirSync3(dir, { withFileTypes: true }).some(
10891
+ return readdirSync4(dir, { withFileTypes: true }).some(
9900
10892
  (e) => e.isFile() && (e.name === `${name}.json` || e.name === `${name}.astro`)
9901
10893
  );
9902
10894
  } catch {
@@ -9905,7 +10897,7 @@ var ComponentService = class {
9905
10897
  };
9906
10898
  if (existsAt(componentsDir)) return void 0;
9907
10899
  try {
9908
- for (const entry of readdirSync3(componentsDir, { withFileTypes: true })) {
10900
+ for (const entry of readdirSync4(componentsDir, { withFileTypes: true })) {
9909
10901
  if (entry.isDirectory() && existsAt(join5(componentsDir, entry.name))) {
9910
10902
  return entry.name;
9911
10903
  }
@@ -9986,7 +10978,7 @@ var ComponentService = class {
9986
10978
  await this.writer.writeComponent(componentDir, name, dataWithJS);
9987
10979
  } else {
9988
10980
  const writeFile3 = this.fs ? this.fs.writeFile.bind(this.fs) : (await import("node:fs/promises")).writeFile;
9989
- if (targetCategory && !existsSync6(componentDir)) {
10981
+ if (targetCategory && !existsSync7(componentDir)) {
9990
10982
  mkdirSync2(componentDir, { recursive: true });
9991
10983
  }
9992
10984
  const filePath = join5(componentDir, `${name}.json`);
@@ -10015,10 +11007,12 @@ var ComponentService = class {
10015
11007
  async saveComponentJavaScript(name, javascript) {
10016
11008
  const componentDir = this.getComponentDir(name);
10017
11009
  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);
11010
+ await this.withComponentWriteLock(name, async () => {
11011
+ const current = this.components.get(name);
11012
+ await this.writer.writeJavaScript(componentDir, name, javascript || "", current);
11013
+ const next = current ? { ...current, component: { ...current.component, javascript: javascript || "" } } : { component: { javascript: javascript || "" } };
11014
+ this.components.set(name, next);
11015
+ });
10022
11016
  return;
10023
11017
  }
10024
11018
  const writeFile3 = this.fs ? this.fs.writeFile.bind(this.fs) : (await import("node:fs/promises")).writeFile;
@@ -10061,10 +11055,12 @@ var ComponentService = class {
10061
11055
  async saveComponentCSS(name, css) {
10062
11056
  const componentDir = this.getComponentDir(name);
10063
11057
  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);
11058
+ await this.withComponentWriteLock(name, async () => {
11059
+ const current = this.components.get(name);
11060
+ await this.writer.writeCSS(componentDir, name, css || "", current);
11061
+ const next = current ? { ...current, component: { ...current.component, css: css || "" } } : { component: { css: css || "" } };
11062
+ this.components.set(name, next);
11063
+ });
10068
11064
  return;
10069
11065
  }
10070
11066
  const writeFile3 = this.fs ? this.fs.writeFile.bind(this.fs) : (await import("node:fs/promises")).writeFile;
@@ -10103,11 +11099,11 @@ var ComponentService = class {
10103
11099
  */
10104
11100
  getAllFolders() {
10105
11101
  const componentsDir = this.componentsBaseDir();
10106
- if (!existsSync6(componentsDir)) {
11102
+ if (!existsSync7(componentsDir)) {
10107
11103
  return [];
10108
11104
  }
10109
11105
  try {
10110
- const entries = readdirSync3(componentsDir, { withFileTypes: true });
11106
+ const entries = readdirSync4(componentsDir, { withFileTypes: true });
10111
11107
  return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
10112
11108
  } catch {
10113
11109
  return [];
@@ -10139,7 +11135,7 @@ var ComponentService = class {
10139
11135
  }
10140
11136
  const componentsDir = this.componentsBaseDir();
10141
11137
  const folderPath = join5(componentsDir, folderName);
10142
- if (existsSync6(folderPath)) {
11138
+ if (existsSync7(folderPath)) {
10143
11139
  throw new Error("Folder already exists");
10144
11140
  }
10145
11141
  mkdirSync2(folderPath, { recursive: true });
@@ -10177,7 +11173,7 @@ var ComponentService = class {
10177
11173
  if (this.writer) {
10178
11174
  await this.writer.moveComponent(sourceDir, targetDir, name);
10179
11175
  } else {
10180
- if (targetCategory && !existsSync6(targetDir)) {
11176
+ if (targetCategory && !existsSync7(targetDir)) {
10181
11177
  mkdirSync2(targetDir, { recursive: true });
10182
11178
  }
10183
11179
  const extensions = [".json", ".js", ".css"];
@@ -10185,7 +11181,7 @@ var ComponentService = class {
10185
11181
  for (const ext of extensions) {
10186
11182
  const sourcePath = join5(sourceDir, `${name}${ext}`);
10187
11183
  const targetPath = join5(targetDir, `${name}${ext}`);
10188
- if (existsSync6(sourcePath)) {
11184
+ if (existsSync7(sourcePath)) {
10189
11185
  await rename2(sourcePath, targetPath);
10190
11186
  }
10191
11187
  }
@@ -10193,7 +11189,7 @@ var ComponentService = class {
10193
11189
  this.componentCategories.set(name, targetCategory);
10194
11190
  if (currentCategory) {
10195
11191
  try {
10196
- const remaining = readdirSync3(sourceDir);
11192
+ const remaining = readdirSync4(sourceDir);
10197
11193
  if (remaining.length === 0) {
10198
11194
  rmdirSync2(sourceDir);
10199
11195
  }
@@ -10248,7 +11244,7 @@ var ComponentService = class {
10248
11244
  await this.writer.moveComponent(dir, dir, oldName, trimmedNew);
10249
11245
  } else {
10250
11246
  for (const ext of [".json", ".js", ".css"]) {
10251
- if (existsSync6(join5(dir, `${trimmedNew}${ext}`))) {
11247
+ if (existsSync7(join5(dir, `${trimmedNew}${ext}`))) {
10252
11248
  throw new Error(`File "${trimmedNew}${ext}" already exists in components/${category ?? ""}`);
10253
11249
  }
10254
11250
  }
@@ -10256,7 +11252,7 @@ var ComponentService = class {
10256
11252
  for (const ext of [".json", ".js", ".css"]) {
10257
11253
  const src = join5(dir, `${oldName}${ext}`);
10258
11254
  const dst = join5(dir, `${trimmedNew}${ext}`);
10259
- if (existsSync6(src)) {
11255
+ if (existsSync7(src)) {
10260
11256
  await rename2(src, dst);
10261
11257
  }
10262
11258
  }
@@ -10290,11 +11286,11 @@ var ComponentService = class {
10290
11286
  let pageRefs = 0;
10291
11287
  if (pageService) {
10292
11288
  const pagePaths = pageService.getAllPagePaths();
10293
- for (const path3 of pagePaths) {
10294
- const pageData = pageService.getPageData(path3);
11289
+ for (const path5 of pagePaths) {
11290
+ const pageData = pageService.getPageData(path5);
10295
11291
  if (!pageData || !("root" in pageData) || !pageData.root) continue;
10296
11292
  if (rewriteComponentRefs(pageData.root, oldName, trimmedNew)) {
10297
- await pageService.savePage(path3, pageData);
11293
+ await pageService.savePage(path5, pageData);
10298
11294
  pageRefs++;
10299
11295
  }
10300
11296
  }
@@ -10452,12 +11448,12 @@ var CMSService = class {
10452
11448
  * @param locale - Optional locale for i18n slug matching
10453
11449
  * @returns CMSRouteMatch if matched, null otherwise
10454
11450
  */
10455
- async matchRoute(path3, locale) {
11451
+ async matchRoute(path5, locale) {
10456
11452
  if (!this.provider) {
10457
11453
  return null;
10458
11454
  }
10459
11455
  for (const route of this.routePatterns) {
10460
- const match = path3.match(route.regex);
11456
+ const match = path5.match(route.regex);
10461
11457
  if (match) {
10462
11458
  const urlSlug = match[route.slugGroup];
10463
11459
  if (urlSlug === void 0) continue;
@@ -10566,11 +11562,11 @@ var CMSService = class {
10566
11562
  if (options?.excludeDrafts && isItemDraftForLocale(item, locale)) continue;
10567
11563
  const localizedSlug = slugValue[locale];
10568
11564
  if (localizedSlug) {
10569
- const path3 = urlPattern.replace("{{slug}}", localizedSlug);
11565
+ const path5 = urlPattern.replace("{{slug}}", localizedSlug);
10570
11566
  if (locale === defaultLocale) {
10571
- urls.push(path3);
11567
+ urls.push(path5);
10572
11568
  } else {
10573
- urls.push(`/${locale}${path3}`);
11569
+ urls.push(`/${locale}${path5}`);
10574
11570
  }
10575
11571
  }
10576
11572
  }
@@ -10822,7 +11818,7 @@ var CMSService = class {
10822
11818
  };
10823
11819
 
10824
11820
  // lib/server/providers/fileSystemCMSProvider.ts
10825
- import { existsSync as existsSync7, readdirSync as readdirSync4, mkdirSync as mkdirSync3 } from "node:fs";
11821
+ import { existsSync as existsSync8, readdirSync as readdirSync5, mkdirSync as mkdirSync3 } from "node:fs";
10826
11822
  import { join as join6 } from "node:path";
10827
11823
  var log6 = createLogger("FileSystemCMSProvider");
10828
11824
  var DRAFT_FILE_SUFFIX = `${CMS_DRAFT_SUFFIX}.json`;
@@ -10895,10 +11891,10 @@ var FileSystemCMSProvider = class {
10895
11891
  return this.schemaCache;
10896
11892
  }
10897
11893
  const schemas = /* @__PURE__ */ new Map();
10898
- if (!existsSync7(this.templatesDir)) {
11894
+ if (!existsSync8(this.templatesDir)) {
10899
11895
  return schemas;
10900
11896
  }
10901
- const files = readdirSync4(this.templatesDir);
11897
+ const files = readdirSync5(this.templatesDir);
10902
11898
  const jsonFiles = files.filter((f) => f.endsWith(".json"));
10903
11899
  const results = await Promise.all(
10904
11900
  jsonFiles.map(async (file) => {
@@ -10928,10 +11924,10 @@ var FileSystemCMSProvider = class {
10928
11924
  async getItems(collection) {
10929
11925
  this.validateCollection(collection);
10930
11926
  const collectionDir = join6(this.cmsDir, collection);
10931
- if (!existsSync7(collectionDir)) {
11927
+ if (!existsSync8(collectionDir)) {
10932
11928
  return [];
10933
11929
  }
10934
- const files = readdirSync4(collectionDir);
11930
+ const files = readdirSync5(collectionDir);
10935
11931
  const jsonFiles = files.filter((f) => f.endsWith(".json") && !f.endsWith(DRAFT_FILE_SUFFIX));
10936
11932
  const results = await Promise.all(
10937
11933
  jsonFiles.map(async (file) => {
@@ -11021,7 +12017,7 @@ var FileSystemCMSProvider = class {
11021
12017
  }
11022
12018
  this.validateFilename(filename);
11023
12019
  const collectionDir = join6(this.cmsDir, collection);
11024
- if (!existsSync7(collectionDir)) {
12020
+ if (!existsSync8(collectionDir)) {
11025
12021
  mkdirSync3(collectionDir, { recursive: true });
11026
12022
  }
11027
12023
  const itemData = stripTransient(item);
@@ -11036,11 +12032,11 @@ var FileSystemCMSProvider = class {
11036
12032
  this.validateFilename(filename);
11037
12033
  const { unlink } = await import("node:fs/promises");
11038
12034
  const publishedPath = join6(this.cmsDir, collection, `${filename}.json`);
11039
- if (existsSync7(publishedPath)) {
12035
+ if (existsSync8(publishedPath)) {
11040
12036
  await unlink(publishedPath);
11041
12037
  }
11042
12038
  const draftPath = this.draftPath(collection, filename);
11043
- if (existsSync7(draftPath)) {
12039
+ if (existsSync8(draftPath)) {
11044
12040
  await unlink(draftPath);
11045
12041
  }
11046
12042
  }
@@ -11074,8 +12070,8 @@ var FileSystemCMSProvider = class {
11074
12070
  async getAllDrafts(collection) {
11075
12071
  this.validateCollection(collection);
11076
12072
  const collectionDir = join6(this.cmsDir, collection);
11077
- if (!existsSync7(collectionDir)) return [];
11078
- const files = readdirSync4(collectionDir).filter((f) => f.endsWith(DRAFT_FILE_SUFFIX));
12073
+ if (!existsSync8(collectionDir)) return [];
12074
+ const files = readdirSync5(collectionDir).filter((f) => f.endsWith(DRAFT_FILE_SUFFIX));
11079
12075
  const results = await Promise.all(
11080
12076
  files.map(async (file) => {
11081
12077
  const filePath = join6(collectionDir, file);
@@ -11100,7 +12096,7 @@ var FileSystemCMSProvider = class {
11100
12096
  async hasDraft(collection, filename) {
11101
12097
  this.validateCollection(collection);
11102
12098
  this.validateFilename(filename);
11103
- return existsSync7(this.draftPath(collection, filename));
12099
+ return existsSync8(this.draftPath(collection, filename));
11104
12100
  }
11105
12101
  /**
11106
12102
  * Save the draft version of an item. Loose validation — drafts may have
@@ -11135,7 +12131,7 @@ var FileSystemCMSProvider = class {
11135
12131
  }
11136
12132
  this.validateFilename(filename);
11137
12133
  const collectionDir = join6(this.cmsDir, collection);
11138
- if (!existsSync7(collectionDir)) {
12134
+ if (!existsSync8(collectionDir)) {
11139
12135
  mkdirSync3(collectionDir, { recursive: true });
11140
12136
  }
11141
12137
  const itemData = stripTransient(item);
@@ -11155,7 +12151,7 @@ var FileSystemCMSProvider = class {
11155
12151
  this.validateFilename(filename);
11156
12152
  const { unlink } = await import("node:fs/promises");
11157
12153
  const filePath = this.draftPath(collection, filename);
11158
- if (existsSync7(filePath)) {
12154
+ if (existsSync8(filePath)) {
11159
12155
  await unlink(filePath);
11160
12156
  }
11161
12157
  }
@@ -11184,13 +12180,13 @@ var FileSystemCMSProvider = class {
11184
12180
  throw new Error(`Cannot publish invalid draft: ${messages}`);
11185
12181
  }
11186
12182
  const collectionDir = join6(this.cmsDir, collection);
11187
- if (!existsSync7(collectionDir)) {
12183
+ if (!existsSync8(collectionDir)) {
11188
12184
  mkdirSync3(collectionDir, { recursive: true });
11189
12185
  }
11190
12186
  const itemData = stripTransient(validation.data);
11191
12187
  const publishedPath = join6(collectionDir, `${filename}.json`);
11192
12188
  await writeFile3(publishedPath, JSON.stringify(itemData, null, 2), "utf-8");
11193
- if (existsSync7(draftFilePath)) {
12189
+ if (existsSync8(draftFilePath)) {
11194
12190
  await unlink(draftFilePath);
11195
12191
  }
11196
12192
  return normalizeItem(itemData, filename);
@@ -11211,18 +12207,18 @@ var FileSystemCMSProvider = class {
11211
12207
  );
11212
12208
  }
11213
12209
  const { writeFile: writeFile3, mkdir } = await import("node:fs/promises");
11214
- if (!existsSync7(this.templatesDir)) {
12210
+ if (!existsSync8(this.templatesDir)) {
11215
12211
  await mkdir(this.templatesDir, { recursive: true });
11216
12212
  }
11217
12213
  const pageFilePath = join6(this.templatesDir, `${collectionId}.json`);
11218
- if (existsSync7(pageFilePath)) {
12214
+ if (existsSync8(pageFilePath)) {
11219
12215
  throw new Error(`Page file already exists: templates/${collectionId}.json`);
11220
12216
  }
11221
12217
  await writeFile3(pageFilePath, JSON.stringify(pageData, null, 2), "utf-8");
11222
12218
  const schemaSource = pageData?.meta?.cms?.source;
11223
12219
  if (schemaSource !== "sanity") {
11224
12220
  const collectionDir = join6(this.cmsDir, collectionId);
11225
- if (!existsSync7(collectionDir)) {
12221
+ if (!existsSync8(collectionDir)) {
11226
12222
  await mkdir(collectionDir, { recursive: true });
11227
12223
  }
11228
12224
  }
@@ -11238,12 +12234,12 @@ var FileSystemCMSProvider = class {
11238
12234
  `Invalid collection ID: "${collectionId}". Collection IDs must contain only letters, numbers, hyphens, and underscores.`
11239
12235
  );
11240
12236
  }
11241
- const { readFile: readFile2, writeFile: writeFile3 } = await import("node:fs/promises");
12237
+ const { readFile: readFile3, writeFile: writeFile3 } = await import("node:fs/promises");
11242
12238
  const pageFilePath = join6(this.templatesDir, `${collectionId}.json`);
11243
- if (!existsSync7(pageFilePath)) {
12239
+ if (!existsSync8(pageFilePath)) {
11244
12240
  throw new Error(`Collection not found: ${collectionId}`);
11245
12241
  }
11246
- const content = await readFile2(pageFilePath, "utf-8");
12242
+ const content = await readFile3(pageFilePath, "utf-8");
11247
12243
  const pageData = JSON.parse(content);
11248
12244
  pageData.meta.cms = {
11249
12245
  ...pageData.meta.cms,
@@ -11262,26 +12258,26 @@ var PageCache = class {
11262
12258
  /**
11263
12259
  * Store page content and line map in cache
11264
12260
  */
11265
- set(path3, content, lineMap) {
11266
- this.cache.set(path3, { content, lineMap });
12261
+ set(path5, content, lineMap) {
12262
+ this.cache.set(path5, { content, lineMap });
11267
12263
  }
11268
12264
  /**
11269
12265
  * Get cached page data
11270
12266
  */
11271
- get(path3) {
11272
- return this.cache.get(path3);
12267
+ get(path5) {
12268
+ return this.cache.get(path5);
11273
12269
  }
11274
12270
  /**
11275
12271
  * Get page content from cache
11276
12272
  */
11277
- getContent(path3) {
11278
- return this.cache.get(path3)?.content;
12273
+ getContent(path5) {
12274
+ return this.cache.get(path5)?.content;
11279
12275
  }
11280
12276
  /**
11281
12277
  * Get line map from cache
11282
12278
  */
11283
- getLineMap(path3) {
11284
- return this.cache.get(path3)?.lineMap;
12279
+ getLineMap(path5) {
12280
+ return this.cache.get(path5)?.lineMap;
11285
12281
  }
11286
12282
  /**
11287
12283
  * Get line range for a specific element path
@@ -11292,14 +12288,14 @@ var PageCache = class {
11292
12288
  /**
11293
12289
  * Delete page from cache
11294
12290
  */
11295
- delete(path3) {
11296
- this.cache.delete(path3);
12291
+ delete(path5) {
12292
+ this.cache.delete(path5);
11297
12293
  }
11298
12294
  /**
11299
12295
  * Check if page exists in cache
11300
12296
  */
11301
- has(path3) {
11302
- return this.cache.has(path3);
12297
+ has(path5) {
12298
+ return this.cache.has(path5);
11303
12299
  }
11304
12300
  /**
11305
12301
  * Get all page paths
@@ -11338,21 +12334,21 @@ var DraftPageStore = class {
11338
12334
  * Store a draft for a page path. The value is the raw JSON string the
11339
12335
  * SSR pipeline expects, matching PageService.getPage()'s contract.
11340
12336
  */
11341
- set(path3, content) {
11342
- this.drafts.set(path3, content);
12337
+ set(path5, content) {
12338
+ this.drafts.set(path5, content);
11343
12339
  }
11344
12340
  /**
11345
12341
  * Get the current draft string for a page path, if any.
11346
12342
  */
11347
- get(path3) {
11348
- return this.drafts.get(path3);
12343
+ get(path5) {
12344
+ return this.drafts.get(path5);
11349
12345
  }
11350
12346
  /**
11351
12347
  * Drop the draft for a specific path. Called when the page is saved to
11352
12348
  * disk so subsequent renders read the persisted version.
11353
12349
  */
11354
- clear(path3) {
11355
- this.drafts.delete(path3);
12350
+ clear(path5) {
12351
+ this.drafts.delete(path5);
11356
12352
  }
11357
12353
  /**
11358
12354
  * Drop every draft. Used on shutdown / project switch.
@@ -11360,14 +12356,19 @@ var DraftPageStore = class {
11360
12356
  clearAll() {
11361
12357
  this.drafts.clear();
11362
12358
  }
11363
- has(path3) {
11364
- return this.drafts.has(path3);
12359
+ has(path5) {
12360
+ return this.drafts.has(path5);
11365
12361
  }
11366
12362
  };
11367
12363
 
11368
12364
  // 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";
12365
+ import { watch, existsSync as existsSync9, statSync as statSync2, readdirSync as readdirSync6 } from "node:fs";
12366
+ import { basename as basename2, dirname as dirname2, join as join7, relative as relative2 } from "node:path";
12367
+
12368
+ // lib/shared/utils/fileUtils.ts
12369
+ var collapseFolderIndex = (name) => name.endsWith("/index") ? name.slice(0, -"/index".length) : name;
12370
+
12371
+ // lib/server/fileWatcher.ts
11371
12372
  function watchRecursive(root, listener) {
11372
12373
  const nested = [];
11373
12374
  const watchedDirs = /* @__PURE__ */ new Set();
@@ -11375,17 +12376,17 @@ function watchRecursive(root, listener) {
11375
12376
  const coverNewDir = (abs) => {
11376
12377
  let isDir = false;
11377
12378
  try {
11378
- isDir = statSync(abs).isDirectory();
12379
+ isDir = statSync2(abs).isDirectory();
11379
12380
  } catch {
11380
12381
  return;
11381
12382
  }
11382
12383
  if (!isDir || watchedDirs.has(abs)) return;
11383
12384
  attachDir(abs);
11384
12385
  try {
11385
- for (const e of readdirSync5(abs, { withFileTypes: true })) {
12386
+ for (const e of readdirSync6(abs, { withFileTypes: true })) {
11386
12387
  const child = join7(abs, e.name);
11387
12388
  if (e.isDirectory()) coverNewDir(child);
11388
- else listener("rename", relative(root, child));
12389
+ else listener("rename", relative2(root, child));
11389
12390
  }
11390
12391
  } catch {
11391
12392
  }
@@ -11398,7 +12399,7 @@ function watchRecursive(root, listener) {
11398
12399
  w = watch(absDir, { recursive: true }, (event, filename) => {
11399
12400
  if (!filename) return;
11400
12401
  const abs = join7(absDir, filename);
11401
- listener(event, relative(root, abs));
12402
+ listener(event, relative2(root, abs));
11402
12403
  coverNewDir(abs);
11403
12404
  });
11404
12405
  } catch {
@@ -11423,19 +12424,19 @@ function watchRecursive(root, listener) {
11423
12424
  return handle;
11424
12425
  }
11425
12426
  function attachWhenDirExists(dirPath, attach, setWatcher) {
11426
- if (existsSync8(dirPath)) {
12427
+ if (existsSync9(dirPath)) {
11427
12428
  setWatcher(attach());
11428
12429
  return;
11429
12430
  }
11430
12431
  const parentDir = dirname2(dirPath);
11431
12432
  const targetName = basename2(dirPath);
11432
- if (!existsSync8(parentDir)) {
12433
+ if (!existsSync9(parentDir)) {
11433
12434
  return;
11434
12435
  }
11435
12436
  let parentWatcher = null;
11436
12437
  parentWatcher = watch(parentDir, (_event, filename) => {
11437
12438
  if (filename !== targetName) return;
11438
- if (!existsSync8(dirPath)) return;
12439
+ if (!existsSync9(dirPath)) return;
11439
12440
  parentWatcher?.close();
11440
12441
  parentWatcher = null;
11441
12442
  setWatcher(attach());
@@ -11465,7 +12466,7 @@ var FileWatcher = class {
11465
12466
  * Watches both .json and .js files to detect component definition and JavaScript changes
11466
12467
  */
11467
12468
  watchComponents(dirPath = projectPaths.components()) {
11468
- if (!existsSync8(dirPath)) {
12469
+ if (!existsSync9(dirPath)) {
11469
12470
  return;
11470
12471
  }
11471
12472
  this.componentsWatcher = watchRecursive(dirPath, async (_event, filename) => {
@@ -11481,7 +12482,7 @@ var FileWatcher = class {
11481
12482
  * Start watching pages directory
11482
12483
  */
11483
12484
  watchPages(dirPath = projectPaths.pages()) {
11484
- if (!existsSync8(dirPath)) {
12485
+ if (!existsSync9(dirPath)) {
11485
12486
  return;
11486
12487
  }
11487
12488
  this.pagesWatcher = watchRecursive(dirPath, async (_event, filename) => {
@@ -11521,7 +12522,7 @@ var FileWatcher = class {
11521
12522
  * Start watching colors.json file
11522
12523
  */
11523
12524
  watchColors(filePath = projectPaths.colors()) {
11524
- if (!existsSync8(filePath)) {
12525
+ if (!existsSync9(filePath)) {
11525
12526
  return;
11526
12527
  }
11527
12528
  const dirPath = getProjectRoot();
@@ -11578,7 +12579,7 @@ var FileWatcher = class {
11578
12579
  * Start watching images directory for new source images
11579
12580
  */
11580
12581
  watchImages(dirPath = projectPaths.images()) {
11581
- if (!existsSync8(dirPath)) {
12582
+ if (!existsSync9(dirPath)) {
11582
12583
  return;
11583
12584
  }
11584
12585
  this.imagesWatcher = watch(dirPath, { recursive: false }, async (event, filename) => {
@@ -11605,7 +12606,7 @@ var FileWatcher = class {
11605
12606
  * Start watching libraries directory for CSS/JS changes
11606
12607
  */
11607
12608
  watchLibraries(dirPath = projectPaths.libraries()) {
11608
- if (!existsSync8(dirPath)) {
12609
+ if (!existsSync9(dirPath)) {
11609
12610
  return;
11610
12611
  }
11611
12612
  this.librariesWatcher = watchRecursive(dirPath, async (_event, filename) => {
@@ -11638,7 +12639,8 @@ var FileWatcher = class {
11638
12639
  if (this.callbacks.onCMSTemplateChange) await this.callbacks.onCMSTemplateChange();
11639
12640
  return;
11640
12641
  }
11641
- const pagePath = mapPageNameToPath(filename.replace(/\.astro$/, ""));
12642
+ const pageName = collapseFolderIndex(filename.replace(/\.astro$/, ""));
12643
+ const pagePath = mapPageNameToPath(pageName);
11642
12644
  if (this.callbacks.onPageChange) await this.callbacks.onPageChange(pagePath);
11643
12645
  }),
11644
12646
  (w) => {
@@ -11806,9 +12808,9 @@ var FileWatcherService = class {
11806
12808
  this.refreshSchemasTimer = null;
11807
12809
  await cmsService.refreshSchemas();
11808
12810
  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);
12811
+ for (const path5 of pageService.getAllPagePaths()) {
12812
+ if (path5.startsWith("/templates/") && !ids.has(path5.slice("/templates/".length))) {
12813
+ await pageService.deletePage(path5);
11812
12814
  }
11813
12815
  }
11814
12816
  for (const id of ids) {
@@ -11862,20 +12864,20 @@ var FileWatcherService = class {
11862
12864
  };
11863
12865
 
11864
12866
  // lib/server/migrateTemplates.ts
11865
- import { existsSync as existsSync9 } from "node:fs";
12867
+ import { existsSync as existsSync10 } from "node:fs";
11866
12868
  import { rename } from "node:fs/promises";
11867
12869
  import { join as join8 } from "node:path";
11868
12870
  async function migrateTemplatesDirectory() {
11869
12871
  const oldDir = join8(projectPaths.pages(), "templates");
11870
12872
  const newDir = projectPaths.templates();
11871
- if (!existsSync9(oldDir)) return;
11872
- if (existsSync9(newDir)) return;
12873
+ if (!existsSync10(oldDir)) return;
12874
+ if (existsSync10(newDir)) return;
11873
12875
  await rename(oldDir, newDir);
11874
12876
  console.log("Migrated CMS templates: pages/templates/ \u2192 templates/");
11875
12877
  }
11876
12878
 
11877
12879
  // build-astro.ts
11878
- import { existsSync as existsSync10, readdirSync as readdirSync6, mkdirSync as mkdirSync4, rmSync, statSync as statSync2, copyFileSync, writeFileSync } from "node:fs";
12880
+ import { existsSync as existsSync11, readdirSync as readdirSync7, mkdirSync as mkdirSync4, rmSync, statSync as statSync3, copyFileSync, writeFileSync } from "node:fs";
11879
12881
  import { writeFile as writeFile2, readFile } from "node:fs/promises";
11880
12882
  import { join as join9 } from "node:path";
11881
12883
  import { createHash } from "node:crypto";
@@ -12961,8 +13963,8 @@ function injectInlineStyle(styleAttr, extraCss) {
12961
13963
  if (dynLiteralMatch) {
12962
13964
  const existing = dynLiteralMatch[1] ?? "";
12963
13965
  const trimmed = existing.trimEnd();
12964
- const sep = trimmed.length > 0 && !trimmed.endsWith(";") ? ";" : "";
12965
- return ` style={\`${existing}${sep}${extraCss}\`}`;
13966
+ const sep2 = trimmed.length > 0 && !trimmed.endsWith(";") ? ";" : "";
13967
+ return ` style={\`${existing}${sep2}${extraCss}\`}`;
12966
13968
  }
12967
13969
  const dynExprMatch = styleAttr.match(/^ style=\{([\s\S]*)\}$/);
12968
13970
  if (dynExprMatch) {
@@ -12970,8 +13972,8 @@ function injectInlineStyle(styleAttr, extraCss) {
12970
13972
  }
12971
13973
  return styleAttr.replace(/style="([^"]*)"/, (_, existing) => {
12972
13974
  const trimmed = existing.trimEnd();
12973
- const sep = trimmed.length > 0 && !trimmed.endsWith(";") ? ";" : "";
12974
- return `style="${existing}${sep}${extraCss}"`;
13975
+ const sep2 = trimmed.length > 0 && !trimmed.endsWith(";") ? ";" : "";
13976
+ return `style="${existing}${sep2}${extraCss}"`;
12975
13977
  });
12976
13978
  }
12977
13979
  var PICTURE_WIDTHS = [500, 800, 1080, 1600, 2400];
@@ -13697,7 +14699,7 @@ function emitLocaleListNode(node, ctx) {
13697
14699
  const code = localeConfig.code;
13698
14700
  const isCurrent = code === currentLocale;
13699
14701
  if (!showCurrent && isCurrent) continue;
13700
- const path3 = slugMap[code] || "/";
14702
+ const path5 = slugMap[code] || "/";
13701
14703
  const classes = isCurrent ? activeItemClasses : itemClasses;
13702
14704
  const classAttr = classes.length > 0 ? ` class="${classes.join(" ")}"` : "";
13703
14705
  const currentAttr = isCurrent ? ' data-current="true"' : ' data-current="false"';
@@ -13722,7 +14724,7 @@ function emitLocaleListNode(node, ctx) {
13722
14724
  }
13723
14725
  linkContent += `<div>${escapeJSX(displayText)}</div>`;
13724
14726
  links.push(
13725
- `${ind(ctx)} <a href="${escapeJSX(path3)}"${hreflangAttr}${currentAttr} data-locale="${escapeJSX(code)}"${classAttr}>${linkContent}</a>`
14727
+ `${ind(ctx)} <a href="${escapeJSX(path5)}"${hreflangAttr}${currentAttr} data-locale="${escapeJSX(code)}"${classAttr}>${linkContent}</a>`
13726
14728
  );
13727
14729
  }
13728
14730
  let linksContent;
@@ -14216,8 +15218,8 @@ function emitAstroPage(options) {
14216
15218
  }
14217
15219
  const componentImports = Array.from(ctx.imports).sort();
14218
15220
  for (const comp of componentImports) {
14219
- const path3 = componentImportPath(fileDepth, comp);
14220
- importLines.push(`import ${astroComponentName(comp)} from '${path3}';`);
15221
+ const path5 = componentImportPath(fileDepth, comp);
15222
+ importLines.push(`import ${astroComponentName(comp)} from '${path5}';`);
14221
15223
  }
14222
15224
  const scriptsArrayLiteral = scriptPaths.length > 0 ? `[${scriptPaths.map((s) => `"${s}"`).join(", ")}]` : "[]";
14223
15225
  const libraryTagsLiteral = `{ headCSS: \`${escapeTemplateLiteral2(libraryTags.headCSS || "")}\`, headJS: \`${escapeTemplateLiteral2(libraryTags.headJS || "")}\`, bodyEndJS: \`${escapeTemplateLiteral2(libraryTags.bodyEndJS || "")}\` }`;
@@ -14556,8 +15558,8 @@ function emitCMSPage(options) {
14556
15558
  }
14557
15559
  const componentImports = Array.from(ctx.imports).sort();
14558
15560
  for (const comp of componentImports) {
14559
- const path3 = componentImportPath2(fileDepth, comp);
14560
- importLines.push(`import ${astroComponentName(comp)} from '${path3}';`);
15561
+ const path5 = componentImportPath2(fileDepth, comp);
15562
+ importLines.push(`import ${astroComponentName(comp)} from '${path5}';`);
14561
15563
  }
14562
15564
  const staticPaths = buildGetStaticPaths(cmsSchema, isMultiLocale, i18nConfig, locale);
14563
15565
  const scriptsArrayLiteral = scriptPaths.length > 0 ? `[${scriptPaths.map((s) => `"${s}"`).join(", ")}]` : "[]";
@@ -14730,24 +15732,24 @@ function writePageScript(javascript, scriptsDir) {
14730
15732
  if (!javascript) return [];
14731
15733
  const hash = hashContent3(javascript);
14732
15734
  const scriptFile = `${hash}.js`;
14733
- if (!existsSync10(scriptsDir)) {
15735
+ if (!existsSync11(scriptsDir)) {
14734
15736
  mkdirSync4(scriptsDir, { recursive: true });
14735
15737
  }
14736
15738
  const fullScriptPath = join9(scriptsDir, scriptFile);
14737
- if (!existsSync10(fullScriptPath)) {
15739
+ if (!existsSync11(fullScriptPath)) {
14738
15740
  writeFileSync(fullScriptPath, javascript, "utf-8");
14739
15741
  }
14740
15742
  return [`/_scripts/${scriptFile}`];
14741
15743
  }
14742
15744
  function copyDirectory(src, dest, filter) {
14743
- if (!existsSync10(src)) return;
14744
- if (!existsSync10(dest)) mkdirSync4(dest, { recursive: true });
14745
- const files = readdirSync6(src);
15745
+ if (!existsSync11(src)) return;
15746
+ if (!existsSync11(dest)) mkdirSync4(dest, { recursive: true });
15747
+ const files = readdirSync7(src);
14746
15748
  for (const file of files) {
14747
15749
  if (filter && !filter(file)) continue;
14748
15750
  const srcPath = join9(src, file);
14749
15751
  const destPath = join9(dest, file);
14750
- const stat = statSync2(srcPath);
15752
+ const stat = statSync3(srcPath);
14751
15753
  if (stat.isDirectory()) copyDirectory(srcPath, destPath, filter);
14752
15754
  else copyFileSync(srcPath, destPath);
14753
15755
  }
@@ -14763,8 +15765,8 @@ function isCMSPage(pageData) {
14763
15765
  }
14764
15766
  function scanJSONFiles(dir, prefix = "") {
14765
15767
  const results = [];
14766
- if (!existsSync10(dir)) return results;
14767
- const entries = readdirSync6(dir, { withFileTypes: true });
15768
+ if (!existsSync11(dir)) return results;
15769
+ const entries = readdirSync7(dir, { withFileTypes: true });
14768
15770
  for (const entry of entries) {
14769
15771
  if (entry.isFile() && entry.name.endsWith(".json")) {
14770
15772
  results.push(prefix ? `${prefix}/${entry.name}` : entry.name);
@@ -14936,7 +15938,7 @@ async function buildAstroProject(_projectRoot, outputDir) {
14936
15938
  const componentLibraries = collectComponentLibraries(globalComponents);
14937
15939
  const imageMetadataMap = await buildImageMetadataMap();
14938
15940
  const outDir = outputDir || join9(projectPaths.project, "astro-export");
14939
- if (existsSync10(outDir)) {
15941
+ if (existsSync11(outDir)) {
14940
15942
  rmSync(outDir, { recursive: true, force: true });
14941
15943
  }
14942
15944
  mkdirSync4(outDir, { recursive: true });
@@ -14951,7 +15953,7 @@ async function buildAstroProject(_projectRoot, outputDir) {
14951
15953
  mkdirSync4(d, { recursive: true });
14952
15954
  }
14953
15955
  const pagesDir = projectPaths.pages();
14954
- if (!existsSync10(pagesDir)) {
15956
+ if (!existsSync11(pagesDir)) {
14955
15957
  console.error("Pages directory not found!");
14956
15958
  return { pages: 0, cmsPages: 0, collections: 0, errors: 1 };
14957
15959
  }
@@ -15082,7 +16084,7 @@ async function buildAstroProject(_projectRoot, outputDir) {
15082
16084
  const shouldInline = css.inline !== false;
15083
16085
  const relPath = css.url.slice(1);
15084
16086
  const srcPath = join9(projectPaths.project, relPath);
15085
- if (!existsSync10(srcPath)) continue;
16087
+ if (!existsSync11(srcPath)) continue;
15086
16088
  if (shouldInline) {
15087
16089
  try {
15088
16090
  inlineContents.set(css.url, await readFile(srcPath, "utf-8"));
@@ -15096,7 +16098,7 @@ async function buildAstroProject(_projectRoot, outputDir) {
15096
16098
  for (const js of buildLibraries.js || []) {
15097
16099
  if (js.url.startsWith("/")) {
15098
16100
  const relPath = js.url.slice(1);
15099
- if (existsSync10(join9(projectPaths.project, relPath))) {
16101
+ if (existsSync11(join9(projectPaths.project, relPath))) {
15100
16102
  localLibsToCopy.push(relPath);
15101
16103
  }
15102
16104
  }
@@ -15117,8 +16119,8 @@ async function buildAstroProject(_projectRoot, outputDir) {
15117
16119
  const cmsConsumerComponents = computeCmsConsumerComponents(globalComponents);
15118
16120
  const collectionUrlExpr = /* @__PURE__ */ new Map();
15119
16121
  const mergedRichTextFields = /* @__PURE__ */ new Set();
15120
- if (existsSync10(templatesDir)) {
15121
- for (const file of readdirSync6(templatesDir).filter((f) => f.endsWith(".json"))) {
16122
+ if (existsSync11(templatesDir)) {
16123
+ for (const file of readdirSync7(templatesDir).filter((f) => f.endsWith(".json"))) {
15122
16124
  const tc = await loadJSONFile(join9(templatesDir, file));
15123
16125
  if (!tc) continue;
15124
16126
  try {
@@ -15133,8 +16135,8 @@ async function buildAstroProject(_projectRoot, outputDir) {
15133
16135
  }
15134
16136
  }
15135
16137
  }
15136
- if (existsSync10(templatesDir)) {
15137
- const templateFiles = readdirSync6(templatesDir).filter((f) => f.endsWith(".json"));
16138
+ if (existsSync11(templatesDir)) {
16139
+ const templateFiles = readdirSync7(templatesDir).filter((f) => f.endsWith(".json"));
15138
16140
  for (const file of templateFiles) {
15139
16141
  const templateContent = await loadJSONFile(join9(templatesDir, file));
15140
16142
  if (!templateContent) continue;
@@ -15227,7 +16229,7 @@ async function buildAstroProject(_projectRoot, outputDir) {
15227
16229
  });
15228
16230
  const astroFileFull = join9(pagesOutDir, astroFilePath);
15229
16231
  const astroFileDir = astroFileFull.substring(0, astroFileFull.lastIndexOf("/"));
15230
- if (!existsSync10(astroFileDir)) {
16232
+ if (!existsSync11(astroFileDir)) {
15231
16233
  mkdirSync4(astroFileDir, { recursive: true });
15232
16234
  }
15233
16235
  await writeFile2(astroFileFull, astroContent, "utf-8");
@@ -15378,7 +16380,7 @@ const { title, meta = '', scripts = [], locale = 'en', theme = '${themeConfig.de
15378
16380
  }
15379
16381
  const astroFileFull = join9(pagesOutDir, result.astroFilePath);
15380
16382
  const astroFileDir = astroFileFull.substring(0, astroFileFull.lastIndexOf("/"));
15381
- if (!existsSync10(astroFileDir)) {
16383
+ if (!existsSync11(astroFileDir)) {
15382
16384
  mkdirSync4(astroFileDir, { recursive: true });
15383
16385
  }
15384
16386
  await writeFile2(astroFileFull, astroContent, "utf-8");
@@ -15410,9 +16412,9 @@ export const GET: APIRoute = () => {
15410
16412
  mkdirSync4(collectionDir, { recursive: true });
15411
16413
  const richTextFieldNames = Object.entries(schema.fields || {}).filter(([, fd]) => fd.type === "rich-text").map(([fn]) => fn);
15412
16414
  const cmsItemsDir = join9(projectPaths.cms(), schema.id);
15413
- if (existsSync10(cmsItemsDir)) {
16415
+ if (existsSync11(cmsItemsDir)) {
15414
16416
  const isDevBuild = process.env.MENO_DEV_BUILD === "true";
15415
- const itemFiles = readdirSync6(cmsItemsDir).filter(
16417
+ const itemFiles = readdirSync7(cmsItemsDir).filter(
15416
16418
  (f) => f.endsWith(".json") && (isDevBuild || !f.endsWith(`${CMS_DRAFT_SUFFIX}.json`))
15417
16419
  );
15418
16420
  for (const itemFile of itemFiles) {
@@ -15459,26 +16461,26 @@ export { collections };
15459
16461
  await writeFile2(join9(srcDir, "content.config.ts"), configContent, "utf-8");
15460
16462
  }
15461
16463
  const imagesSrcDir = join9(projectPaths.project, "images");
15462
- if (existsSync10(imagesSrcDir)) {
16464
+ if (existsSync11(imagesSrcDir)) {
15463
16465
  copyDirectory(imagesSrcDir, join9(srcDir, "assets", "images"), shouldCopyImageForAstro);
15464
16466
  copyDirectory(imagesSrcDir, join9(publicDir, "images"));
15465
16467
  }
15466
16468
  const publicAssetDirs = ["fonts", "icons", "videos", "assets"];
15467
16469
  for (const dir of publicAssetDirs) {
15468
16470
  const srcAssetDir = join9(projectPaths.project, dir);
15469
- if (existsSync10(srcAssetDir)) {
16471
+ if (existsSync11(srcAssetDir)) {
15470
16472
  copyDirectory(srcAssetDir, join9(publicDir, dir));
15471
16473
  }
15472
16474
  }
15473
16475
  const librariesDir = join9(projectPaths.project, "libraries");
15474
- if (existsSync10(librariesDir)) {
16476
+ if (existsSync11(librariesDir)) {
15475
16477
  copyDirectory(librariesDir, join9(publicDir, "libraries"));
15476
16478
  }
15477
16479
  for (const relPath of localLibsToCopy) {
15478
16480
  const srcPath = join9(projectPaths.project, relPath);
15479
16481
  const destPath = join9(publicDir, relPath);
15480
16482
  const destDir = destPath.substring(0, destPath.lastIndexOf("/"));
15481
- if (destDir && !existsSync10(destDir)) mkdirSync4(destDir, { recursive: true });
16483
+ if (destDir && !existsSync11(destDir)) mkdirSync4(destDir, { recursive: true });
15482
16484
  copyFileSync(srcPath, destPath);
15483
16485
  }
15484
16486
  const packageJson = {
@@ -15548,6 +16550,32 @@ export default defineConfig({${siteUrl ? `
15548
16550
 
15549
16551
  // lib/server/index.ts
15550
16552
  init_constants();
16553
+
16554
+ // lib/server/webflow/templateWrapper.ts
16555
+ import { readFile as readFile2 } from "node:fs/promises";
16556
+ var cachedTemplate = null;
16557
+ async function getWebflowTemplate(appName) {
16558
+ if (cachedTemplate) return cachedTemplate;
16559
+ const url = `https://webflow-ext.com/template/v2?name=${encodeURIComponent(appName)}`;
16560
+ const res = await fetch(url);
16561
+ if (!res.ok) throw new Error(`Failed to fetch Webflow template: ${res.status}`);
16562
+ cachedTemplate = await res.text();
16563
+ return cachedTemplate;
16564
+ }
16565
+ async function wrapInWebflowTemplate(html, manifestPath) {
16566
+ try {
16567
+ const manifest = JSON.parse(await readFile2(manifestPath, "utf-8"));
16568
+ const template = await getWebflowTemplate(manifest.name || "Export to Meno");
16569
+ const headMatch = html.match(/<head[^>]*>([\s\S]*?)<\/head>/i);
16570
+ const bodyMatch = html.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
16571
+ const headContent = headMatch?.[1] ?? "";
16572
+ const bodyContent = bodyMatch?.[1] ?? "";
16573
+ return template.replace("{{ui}}", headContent + bodyContent);
16574
+ } catch (err) {
16575
+ console.warn("Could not fetch Webflow wrapper template:", err.message);
16576
+ return html;
16577
+ }
16578
+ }
15551
16579
  export {
15552
16580
  CMSService,
15553
16581
  ColorService,
@@ -15567,6 +16595,7 @@ export {
15567
16595
  SERVE_PORT,
15568
16596
  VariableService,
15569
16597
  WebSocketManager,
16598
+ auditStylesheet,
15570
16599
  buildAstroProject,
15571
16600
  buildAttributes,
15572
16601
  buildComponentHTML,
@@ -15574,6 +16603,7 @@ export {
15574
16603
  buildLineMap,
15575
16604
  bundleFile,
15576
16605
  clearJSValidationCache,
16606
+ collectClassesFromPageData,
15577
16607
  collectComponentCSS,
15578
16608
  collectComponentJavaScript,
15579
16609
  colorService,
@@ -15588,6 +16618,7 @@ export {
15588
16618
  errorResponse,
15589
16619
  escapeHtml,
15590
16620
  extractPageMeta,
16621
+ extractRules,
15591
16622
  fileExists,
15592
16623
  formHandlerScript,
15593
16624
  generateBuildErrorPage,
@@ -15634,12 +16665,14 @@ export {
15634
16665
  readJsonFile,
15635
16666
  readTextFile,
15636
16667
  regenerateThemeCss,
16668
+ renderAuditMarkdown,
15637
16669
  renderPageSSR,
15638
16670
  resetFontConfig,
15639
16671
  resolvePackagePath,
15640
16672
  resolveProjectPath,
15641
16673
  saveColors,
15642
16674
  saveVariables,
16675
+ sectionPrefix,
15643
16676
  serializeThemeCss,
15644
16677
  serveFile,
15645
16678
  setProjectRoot,
@@ -15651,6 +16684,7 @@ export {
15651
16684
  watchRecursive,
15652
16685
  withErrorHandling,
15653
16686
  withLogging,
16687
+ wrapInWebflowTemplate,
15654
16688
  writeFile
15655
16689
  };
15656
16690
  //# sourceMappingURL=index.js.map