meno-core 1.1.2 → 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 (57) hide show
  1. package/dist/chunks/{chunk-J4IPTP5X.js → chunk-UOF4MCAD.js} +92 -4
  2. package/dist/chunks/chunk-UOF4MCAD.js.map +7 -0
  3. package/dist/chunks/{chunk-7ZLF4NE5.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 +1287 -236
  7. package/dist/lib/server/index.js.map +4 -4
  8. package/dist/lib/shared/index.js +5 -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/scripts/formHandler.ts +17 -0
  13. package/lib/client/theme.test.ts +2 -2
  14. package/lib/client/theme.ts +28 -26
  15. package/lib/server/createServer.ts +5 -1
  16. package/lib/server/cssAudit.test.ts +270 -0
  17. package/lib/server/cssAudit.ts +815 -0
  18. package/lib/server/deMirror.test.ts +61 -0
  19. package/lib/server/deMirror.ts +317 -0
  20. package/lib/server/fileWatcher.test.ts +23 -0
  21. package/lib/server/fileWatcher.ts +6 -1
  22. package/lib/server/index.ts +19 -0
  23. package/lib/server/middleware/cors.test.ts +1 -1
  24. package/lib/server/middleware/cors.ts +1 -1
  25. package/lib/server/routes/api/core-routes.ts +18 -0
  26. package/lib/server/routes/api/cssAudit.test.ts +101 -0
  27. package/lib/server/routes/api/cssAudit.ts +138 -0
  28. package/lib/server/routes/api/deMirror.test.ts +105 -0
  29. package/lib/server/routes/api/deMirror.ts +127 -0
  30. package/lib/server/routes/api/functions.ts +2 -2
  31. package/lib/server/routes/api/pages.ts +2 -2
  32. package/lib/server/services/componentService.test.ts +43 -0
  33. package/lib/server/services/componentService.ts +49 -12
  34. package/lib/server/services/pageService.test.ts +15 -0
  35. package/lib/server/services/pageService.ts +44 -22
  36. package/lib/server/ssr/htmlGenerator.test.ts +29 -0
  37. package/lib/server/ssr/htmlGenerator.ts +83 -3
  38. package/lib/server/themeCssCodec.test.ts +67 -0
  39. package/lib/server/themeCssCodec.ts +67 -5
  40. package/lib/server/webflow/templateWrapper.ts +54 -0
  41. package/lib/shared/cssGeneration.test.ts +28 -0
  42. package/lib/shared/interfaces/contentProvider.ts +9 -0
  43. package/lib/shared/nodeUtils.ts +18 -0
  44. package/lib/shared/types/cms.ts +1 -0
  45. package/lib/shared/utilityClassConfig.ts +2 -0
  46. package/lib/shared/utilityClassMapper.test.ts +63 -0
  47. package/lib/shared/utilityClassMapper.ts +9 -1
  48. package/lib/shared/utilityClassNames.ts +74 -0
  49. package/lib/shared/utils/fileUtils.ts +11 -0
  50. package/lib/shared/validation/schemas.test.ts +78 -0
  51. package/lib/shared/validation/schemas.ts +54 -5
  52. package/lib/shared/viewportUnits.test.ts +34 -1
  53. package/lib/shared/viewportUnits.ts +43 -0
  54. package/package.json +3 -1
  55. package/vite.config.ts +4 -1
  56. package/dist/chunks/chunk-J4IPTP5X.js.map +0 -7
  57. /package/dist/chunks/{chunk-7ZLF4NE5.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-7ZLF4NE5.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-J4IPTP5X.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
  /**
@@ -284,7 +285,7 @@ init_constants();
284
285
  // lib/server/middleware/cors.ts
285
286
  var DEFAULT_OPTIONS = {
286
287
  origin: "*",
287
- methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
288
+ methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
288
289
  allowedHeaders: ["Content-Type", "Authorization"],
289
290
  credentials: false
290
291
  };
@@ -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)) {
@@ -2950,7 +3845,7 @@ async function handleFunctionsRoute(req, url) {
2950
3845
  const response = await handler(context);
2951
3846
  const headers = new Headers(response.headers);
2952
3847
  headers.set("Access-Control-Allow-Origin", "*");
2953
- headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
3848
+ headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS");
2954
3849
  headers.set("Access-Control-Allow-Headers", "Content-Type");
2955
3850
  return new Response(response.body, {
2956
3851
  status: response.status,
@@ -2976,7 +3871,7 @@ function handleFunctionsPreflight(req, url) {
2976
3871
  status: 204,
2977
3872
  headers: {
2978
3873
  "Access-Control-Allow-Origin": "*",
2979
- "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
3874
+ "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
2980
3875
  "Access-Control-Allow-Headers": "Content-Type",
2981
3876
  "Access-Control-Max-Age": "86400"
2982
3877
  }
@@ -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
  }
@@ -5279,7 +6186,23 @@ var formHandlerScript = `
5279
6186
  // Find all forms with fetch handler
5280
6187
  const forms = document.querySelectorAll('form[data-submit-handler="fetch"]');
5281
6188
 
6189
+ // Spam protection (server-enforced at /api/send-email): inject a hidden honeypot
6190
+ // field bots fill but humans never see, and stamp a clock-skew-free dwell time on
6191
+ // submit. Fields named with a leading "_" are control/meta and are not emailed.
6192
+ const loadedAt = Date.now();
6193
+
5282
6194
  forms.forEach(function(form) {
6195
+ if (!form.querySelector('input[name="_honey"]')) {
6196
+ const hp = document.createElement('input');
6197
+ hp.type = 'text';
6198
+ hp.name = '_honey';
6199
+ hp.tabIndex = -1;
6200
+ hp.setAttribute('autocomplete', 'off');
6201
+ hp.setAttribute('aria-hidden', 'true');
6202
+ hp.style.cssText = 'position:absolute;left:-9999px;width:1px;height:1px;opacity:0';
6203
+ form.appendChild(hp);
6204
+ }
6205
+
5283
6206
  form.addEventListener('submit', async function(e) {
5284
6207
  e.preventDefault();
5285
6208
 
@@ -5314,6 +6237,7 @@ var formHandlerScript = `
5314
6237
 
5315
6238
  try {
5316
6239
  const formData = new FormData(form);
6240
+ formData.append('_elapsed', String(Date.now() - loadedAt));
5317
6241
 
5318
6242
  const response = await fetch(action, {
5319
6243
  method: method.toUpperCase(),
@@ -5437,6 +6361,7 @@ function generateAllInlineDataScripts(collections) {
5437
6361
  }
5438
6362
 
5439
6363
  // lib/server/ssr/htmlGenerator.ts
6364
+ import { posix } from "node:path";
5440
6365
  function generateImagePreloadTags(preloadImages) {
5441
6366
  if (preloadImages.length === 0) return "";
5442
6367
  return preloadImages.map(
@@ -5447,6 +6372,43 @@ function minifyCSS(code) {
5447
6372
  if (!code.trim()) return code;
5448
6373
  return code.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\s*([{};:,>~])\s*/g, "$1").replace(/\s+/g, " ").replace(/\{\s+/g, "{").replace(/\s+\}/g, "}").replace(/;}/g, "}").trim();
5449
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
+ }
5450
6412
  async function generateSSRHTML(pageDataOrOptions, globalComponents = {}, pagePath = "/", baseUrl = "", useBuiltBundle = false, locale, slugMappings, cmsContext, cmsService, externalScriptPath) {
5451
6413
  let options;
5452
6414
  if ("pageData" in pageDataOrOptions) {
@@ -5468,7 +6430,7 @@ async function generateSSRHTML(pageDataOrOptions, globalComponents = {}, pagePat
5468
6430
  const {
5469
6431
  pageData,
5470
6432
  globalComponents: components = {},
5471
- pagePath: path3 = "/",
6433
+ pagePath: path5 = "/",
5472
6434
  baseUrl: base = "",
5473
6435
  useBuiltBundle: useBundled = false,
5474
6436
  locale: loc,
@@ -5492,7 +6454,7 @@ async function generateSSRHTML(pageDataOrOptions, globalComponents = {}, pagePat
5492
6454
  const rendered = await renderPageSSR(
5493
6455
  pageData,
5494
6456
  components,
5495
- path3,
6457
+ path5,
5496
6458
  base,
5497
6459
  loc,
5498
6460
  void 0,
@@ -5679,7 +6641,7 @@ picture {
5679
6641
  line-height: normal;
5680
6642
  }`;
5681
6643
  const combinedCSS = [fontCSS, themeColorVariablesCSS, variablesCSS, baseCSS, componentCSS, utilityCSS, interactiveCSS].filter(Boolean).join("\n");
5682
- const cssWithStableViewport = rewriteViewportUnits(combinedCSS);
6644
+ const cssWithStableViewport = rewriteViewportUnitsInStylesheet(combinedCSS);
5683
6645
  const finalCSS = useBundled ? minifyCSS(cssWithStableViewport) : cssWithStableViewport;
5684
6646
  const prefetchConfig = await loadPrefetchConfig();
5685
6647
  const menoConfig = prefetchConfig.enabled ? { prefetch: prefetchConfig } : {};
@@ -5708,6 +6670,7 @@ ${externalJavaScript}`;
5708
6670
  const styleContent = useBundled ? finalCSS : `
5709
6671
  ${combinedCSS.split("\n").join("\n ")}
5710
6672
  `;
6673
+ const pageCssTags = await collectPageCssImports(pageData._frontmatter, pagePath, nonceAttr);
5711
6674
  const htmlDocument = `<!DOCTYPE html>
5712
6675
  <html lang="${rendered.locale}" theme="${themeConfig.default}">
5713
6676
  <head>
@@ -5721,7 +6684,8 @@ ${externalJavaScript}`;
5721
6684
  ` : ""}${libraryTags.headCSS ? `${libraryTags.headCSS}
5722
6685
  ` : ""}${libraryTags.headJS ? `${libraryTags.headJS}
5723
6686
  ` : ""}${rendered.meta}
5724
- ${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 ? `
5725
6689
  ${mergedCustomCode.head}` : ""}
5726
6690
  </head>
5727
6691
  <body>${mergedCustomCode.bodyStart ? `
@@ -7129,8 +8093,8 @@ function singularizeWord(word) {
7129
8093
  if (word.endsWith("s")) return word.slice(0, -1);
7130
8094
  return word;
7131
8095
  }
7132
- function getNestedValue2(obj, path3) {
7133
- return path3.split(".").reduce((current, key) => {
8096
+ function getNestedValue2(obj, path5) {
8097
+ return path5.split(".").reduce((current, key) => {
7134
8098
  return current && typeof current === "object" ? current[key] : void 0;
7135
8099
  }, obj);
7136
8100
  }
@@ -8611,7 +9575,11 @@ async function createServer(config) {
8611
9575
  try {
8612
9576
  const server = await createRuntimeServer({
8613
9577
  port: currentPort,
8614
- 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",
8615
9583
  wsPath: HMR_ROUTE,
8616
9584
  async fetch(req, upgradeWebSocket) {
8617
9585
  const url = new URL(req.url);
@@ -9006,8 +9974,8 @@ ${plainTextErrors}`;
9006
9974
  }
9007
9975
 
9008
9976
  // lib/server/services/pageService.ts
9009
- import { existsSync as existsSync5, readdirSync as readdirSync2, mkdirSync, rmdirSync } from "node:fs";
9010
- 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";
9011
9979
 
9012
9980
  // lib/server/utils/jsonLineMapper.ts
9013
9981
  function buildLineMap(jsonText) {
@@ -9051,13 +10019,13 @@ function buildLineMap(jsonText) {
9051
10019
  pos++;
9052
10020
  }
9053
10021
  }
9054
- function parseValue(path3, trackChildren) {
10022
+ function parseValue(path5, trackChildren) {
9055
10023
  skipWhitespace();
9056
10024
  const startPos = pos;
9057
10025
  if (jsonText[pos] === "{") {
9058
- parseObject(path3, trackChildren);
10026
+ parseObject(path5, trackChildren);
9059
10027
  } else if (jsonText[pos] === "[") {
9060
- parseArray(path3, trackChildren);
10028
+ parseArray(path5, trackChildren);
9061
10029
  } else if (jsonText[pos] === '"') {
9062
10030
  parseString();
9063
10031
  } else if (jsonText[pos] === "t") {
@@ -9071,14 +10039,14 @@ function buildLineMap(jsonText) {
9071
10039
  }
9072
10040
  return { start: startPos, end: pos };
9073
10041
  }
9074
- function recordTrackedRoot(path3) {
9075
- const { start, end } = parseValue(path3, true);
10042
+ function recordTrackedRoot(path5) {
10043
+ const { start, end } = parseValue(path5, true);
9076
10044
  lineMap.set("", {
9077
10045
  startLine: charToLine[start] ?? 0,
9078
10046
  endLine: charToLine[end - 1] || charToLine[start] || 0
9079
10047
  });
9080
10048
  }
9081
- function parseObject(path3, trackChildren) {
10049
+ function parseObject(path5, trackChildren) {
9082
10050
  pos++;
9083
10051
  skipWhitespace();
9084
10052
  while (pos < jsonText.length && jsonText[pos] !== "}") {
@@ -9087,14 +10055,14 @@ function buildLineMap(jsonText) {
9087
10055
  skipWhitespace();
9088
10056
  pos++;
9089
10057
  skipWhitespace();
9090
- if (key === "root" && path3.length === 0) {
9091
- recordTrackedRoot(path3);
9092
- } else if (key === "component" && path3.length === 0) {
9093
- parseComponentWrapper(path3);
10058
+ if (key === "root" && path5.length === 0) {
10059
+ recordTrackedRoot(path5);
10060
+ } else if (key === "component" && path5.length === 0) {
10061
+ parseComponentWrapper(path5);
9094
10062
  } else if (key === "children" && trackChildren) {
9095
- parseValue(path3, true);
10063
+ parseValue(path5, true);
9096
10064
  } else {
9097
- parseValue(path3, false);
10065
+ parseValue(path5, false);
9098
10066
  }
9099
10067
  skipWhitespace();
9100
10068
  if (jsonText[pos] === ",") pos++;
@@ -9102,9 +10070,9 @@ function buildLineMap(jsonText) {
9102
10070
  }
9103
10071
  pos++;
9104
10072
  }
9105
- function parseComponentWrapper(path3) {
10073
+ function parseComponentWrapper(path5) {
9106
10074
  if (jsonText[pos] !== "{") {
9107
- parseValue(path3, false);
10075
+ parseValue(path5, false);
9108
10076
  return;
9109
10077
  }
9110
10078
  pos++;
@@ -9116,9 +10084,9 @@ function buildLineMap(jsonText) {
9116
10084
  pos++;
9117
10085
  skipWhitespace();
9118
10086
  if (key === "structure") {
9119
- recordTrackedRoot(path3);
10087
+ recordTrackedRoot(path5);
9120
10088
  } else {
9121
- parseValue(path3, false);
10089
+ parseValue(path5, false);
9122
10090
  }
9123
10091
  skipWhitespace();
9124
10092
  if (jsonText[pos] === ",") pos++;
@@ -9126,12 +10094,12 @@ function buildLineMap(jsonText) {
9126
10094
  }
9127
10095
  pos++;
9128
10096
  }
9129
- function parseArray(path3, trackChildren) {
10097
+ function parseArray(path5, trackChildren) {
9130
10098
  pos++;
9131
10099
  skipWhitespace();
9132
10100
  let index = 0;
9133
10101
  while (pos < jsonText.length && jsonText[pos] !== "]") {
9134
- const childPath = [...path3, index];
10102
+ const childPath = [...path5, index];
9135
10103
  const { start, end } = parseValue(childPath, trackChildren);
9136
10104
  if (trackChildren) {
9137
10105
  const pathStr = childPath.join(",");
@@ -9204,9 +10172,12 @@ var PageService = class {
9204
10172
  return;
9205
10173
  }
9206
10174
  const pages = await this.provider.loadAll();
9207
- for (const [path3, content] of pages) {
10175
+ for (const [path5, content] of pages) {
9208
10176
  const lineMap = buildLineMap(content);
9209
- 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);
9210
10181
  }
9211
10182
  }
9212
10183
  /**
@@ -9223,8 +10194,8 @@ var PageService = class {
9223
10194
  * }
9224
10195
  * ```
9225
10196
  */
9226
- getPage(path3) {
9227
- return this.pageCache.getContent(path3);
10197
+ getPage(path5) {
10198
+ return this.pageCache.getContent(path5);
9228
10199
  }
9229
10200
  /**
9230
10201
  * Get parsed page data
@@ -9243,8 +10214,8 @@ var PageService = class {
9243
10214
  * }
9244
10215
  * ```
9245
10216
  */
9246
- getPageData(path3) {
9247
- const content = this.pageCache.getContent(path3);
10217
+ getPageData(path5) {
10218
+ const content = this.pageCache.getContent(path5);
9248
10219
  if (!content) {
9249
10220
  return null;
9250
10221
  }
@@ -9274,15 +10245,15 @@ var PageService = class {
9274
10245
  * });
9275
10246
  * ```
9276
10247
  */
9277
- async savePage(path3, data) {
10248
+ async savePage(path5, data) {
9278
10249
  if (!this.provider) {
9279
10250
  throw new Error("PageProvider not set");
9280
10251
  }
9281
10252
  const { _lineMap, ...dataToSave } = data;
9282
10253
  const content = JSON.stringify(dataToSave, null, 2);
9283
- await this.provider.save(path3, content);
10254
+ await this.provider.save(path5, content);
9284
10255
  const lineMap = buildLineMap(content);
9285
- this.pageCache.set(path3, content, lineMap);
10256
+ this.pageCache.set(path5, content, lineMap);
9286
10257
  }
9287
10258
  /**
9288
10259
  * Re-read a single page from disk via the provider and refresh the cache.
@@ -9296,22 +10267,22 @@ var PageService = class {
9296
10267
  * partial mid-write or parse error), in which case the cache is left intact and
9297
10268
  * the next watcher event retries.
9298
10269
  */
9299
- async reloadPageFromDisk(path3) {
10270
+ async reloadPageFromDisk(path5) {
9300
10271
  if (!this.provider) return false;
9301
10272
  let content = null;
9302
10273
  try {
9303
- content = await this.provider.get(path3);
10274
+ content = await this.provider.get(path5);
9304
10275
  } catch (error) {
9305
- logRuntimeError("pageService.reloadPageFromDisk", error, { path: path3 });
10276
+ logRuntimeError("pageService.reloadPageFromDisk", error, { path: path5 });
9306
10277
  return false;
9307
10278
  }
9308
10279
  if (content) {
9309
- this.pageCache.set(path3, content, buildLineMap(content));
10280
+ this.pageCache.set(path5, content, buildLineMap(content));
9310
10281
  return true;
9311
10282
  }
9312
10283
  try {
9313
- if (!await this.provider.exists(path3)) {
9314
- this.pageCache.delete(path3);
10284
+ if (!await this.provider.exists(path5)) {
10285
+ this.pageCache.delete(path5);
9315
10286
  return true;
9316
10287
  }
9317
10288
  } catch {
@@ -9334,10 +10305,10 @@ var PageService = class {
9334
10305
  * await pageService.deletePage('/old-page', true);
9335
10306
  * ```
9336
10307
  */
9337
- async deletePage(path3, deleteFromStorage = false) {
9338
- this.pageCache.delete(path3);
10308
+ async deletePage(path5, deleteFromStorage = false) {
10309
+ this.pageCache.delete(path5);
9339
10310
  if (deleteFromStorage && this.provider) {
9340
- await this.provider.delete(path3);
10311
+ await this.provider.delete(path5);
9341
10312
  }
9342
10313
  }
9343
10314
  /**
@@ -9365,26 +10336,34 @@ var PageService = class {
9365
10336
  *
9366
10337
  * @example
9367
10338
  * ```typescript
9368
- * const pages = pageService.getAllPagesWithInfo();
10339
+ * const pages = await pageService.getAllPagesWithInfo();
9369
10340
  * // [{ path: "/", isDraft: false }, { path: "/about", isDraft: true }]
9370
10341
  * ```
9371
10342
  */
9372
- getAllPagesWithInfo() {
10343
+ async getAllPagesWithInfo() {
9373
10344
  const paths = this.pageCache.keys().filter((p) => !p.startsWith("/_sketch/"));
9374
- return paths.map((path3) => {
9375
- const pageData = this.getPageData(path3);
9376
- const pathWithoutSlash = path3 === "/" ? "index" : path3.substring(1);
9377
- const slashIndex = pathWithoutSlash.lastIndexOf("/");
9378
- const folder = slashIndex > 0 ? pathWithoutSlash.substring(0, slashIndex) : void 0;
9379
- const metaSource = pageData?.meta?.source;
9380
- const source = metaSource === "cms" || metaSource === "ssr" ? metaSource : void 0;
9381
- return {
9382
- path: path3,
9383
- isDraft: pageData?.meta?.draft === true,
9384
- ...folder ? { folder } : {},
9385
- ...source ? { source } : {}
9386
- };
9387
- });
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
+ );
9388
10367
  }
9389
10368
  /**
9390
10369
  * Get line map for a page
@@ -9394,8 +10373,8 @@ var PageService = class {
9394
10373
  * @param path - Page path (e.g., "/" or "/about")
9395
10374
  * @returns LineMap object or undefined if page not found
9396
10375
  */
9397
- getLineMap(path3) {
9398
- return this.pageCache.getLineMap(path3);
10376
+ getLineMap(path5) {
10377
+ return this.pageCache.getLineMap(path5);
9399
10378
  }
9400
10379
  /**
9401
10380
  * Pages base directory — provider-aware (e.g. `src/pages` for astro projects),
@@ -9419,12 +10398,12 @@ var PageService = class {
9419
10398
  */
9420
10399
  getAllFolders() {
9421
10400
  const pagesDir = this.pagesBaseDir();
9422
- if (!existsSync5(pagesDir)) {
10401
+ if (!existsSync6(pagesDir)) {
9423
10402
  return [];
9424
10403
  }
9425
10404
  const collectFolders = (dir, prefix) => {
9426
10405
  try {
9427
- const entries = readdirSync2(dir, { withFileTypes: true });
10406
+ const entries = readdirSync3(dir, { withFileTypes: true });
9428
10407
  const result = [];
9429
10408
  for (const entry of entries) {
9430
10409
  if (!entry.isDirectory()) continue;
@@ -9462,7 +10441,7 @@ var PageService = class {
9462
10441
  }
9463
10442
  const pagesDir = this.pagesBaseDir();
9464
10443
  const folderPath = join4(pagesDir, trimmed);
9465
- if (existsSync5(folderPath)) {
10444
+ if (existsSync6(folderPath)) {
9466
10445
  throw new Error("Folder already exists");
9467
10446
  }
9468
10447
  mkdirSync(folderPath, { recursive: true });
@@ -9490,11 +10469,11 @@ var PageService = class {
9490
10469
  }
9491
10470
  if (newFolder) {
9492
10471
  const targetDir = join4(pagesDir, newFolder);
9493
- if (!existsSync5(targetDir)) {
10472
+ if (!existsSync6(targetDir)) {
9494
10473
  mkdirSync(targetDir, { recursive: true });
9495
10474
  }
9496
10475
  }
9497
- if (!existsSync5(sourceFile)) {
10476
+ if (!existsSync6(sourceFile)) {
9498
10477
  throw new Error(`Page not found: ${pagePath}`);
9499
10478
  }
9500
10479
  await rename2(sourceFile, targetFile);
@@ -9509,7 +10488,7 @@ var PageService = class {
9509
10488
  const sourceDir = slashIndex >= 0 ? join4(pagesDir, pageName.substring(0, slashIndex)) : null;
9510
10489
  if (sourceDir && sourceDir !== pagesDir) {
9511
10490
  try {
9512
- const remaining = readdirSync2(sourceDir);
10491
+ const remaining = readdirSync3(sourceDir);
9513
10492
  if (remaining.length === 0) {
9514
10493
  rmdirSync(sourceDir);
9515
10494
  }
@@ -9557,10 +10536,10 @@ var PageService = class {
9557
10536
  const ext = this.pageExt();
9558
10537
  const sourceFile = join4(pagesDir, `${oldName}${ext}`);
9559
10538
  const targetFile = join4(pagesDir, `${newName}${ext}`);
9560
- if (!existsSync5(sourceFile)) {
10539
+ if (!existsSync6(sourceFile)) {
9561
10540
  throw new Error(`Page not found: ${oldPath}`);
9562
10541
  }
9563
- if (existsSync5(targetFile)) {
10542
+ if (existsSync6(targetFile)) {
9564
10543
  throw new Error(`Page already exists: ${newPath}`);
9565
10544
  }
9566
10545
  await rename2(sourceFile, targetFile);
@@ -9588,17 +10567,17 @@ var PageService = class {
9588
10567
  getSlugMappings() {
9589
10568
  const mappings = [];
9590
10569
  const paths = this.pageCache.keys();
9591
- for (const path3 of paths) {
9592
- const pageData = this.getPageData(path3);
10570
+ for (const path5 of paths) {
10571
+ const pageData = this.getPageData(path5);
9593
10572
  if (pageData?.meta?.slugs) {
9594
- const pageId = path3 === "/" ? "index" : path3.substring(1);
10573
+ const pageId = path5 === "/" ? "index" : path5.substring(1);
9595
10574
  mappings.push({
9596
10575
  pageId,
9597
10576
  slugs: pageData.meta.slugs
9598
10577
  });
9599
10578
  } else {
9600
- const pageId = path3 === "/" ? "index" : path3.substring(1);
9601
- const defaultSlug = path3 === "/" ? "" : path3.substring(1);
10579
+ const pageId = path5 === "/" ? "index" : path5.substring(1);
10580
+ const defaultSlug = path5 === "/" ? "" : path5.substring(1);
9602
10581
  mappings.push({
9603
10582
  pageId,
9604
10583
  slugs: { _default: defaultSlug }
@@ -9611,7 +10590,7 @@ var PageService = class {
9611
10590
 
9612
10591
  // lib/server/services/componentService.ts
9613
10592
  import { join as join5 } from "node:path";
9614
- 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";
9615
10594
  var ComponentService = class {
9616
10595
  components = /* @__PURE__ */ new Map();
9617
10596
  componentCategories = /* @__PURE__ */ new Map();
@@ -9620,6 +10599,36 @@ var ComponentService = class {
9620
10599
  writer;
9621
10600
  loadErrors = /* @__PURE__ */ new Map();
9622
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
+ }
9623
10632
  /**
9624
10633
  * Creates a new ComponentService instance
9625
10634
  *
@@ -9874,12 +10883,12 @@ var ComponentService = class {
9874
10883
  */
9875
10884
  findComponentCategoryOnDisk(name) {
9876
10885
  const componentsDir = this.componentsBaseDir();
9877
- if (!existsSync6(componentsDir)) return void 0;
10886
+ if (!existsSync7(componentsDir)) return void 0;
9878
10887
  const ext = this.writer ? null : ".json";
9879
10888
  const existsAt = (dir) => {
9880
- if (ext) return existsSync6(join5(dir, `${name}${ext}`));
10889
+ if (ext) return existsSync7(join5(dir, `${name}${ext}`));
9881
10890
  try {
9882
- return readdirSync3(dir, { withFileTypes: true }).some(
10891
+ return readdirSync4(dir, { withFileTypes: true }).some(
9883
10892
  (e) => e.isFile() && (e.name === `${name}.json` || e.name === `${name}.astro`)
9884
10893
  );
9885
10894
  } catch {
@@ -9888,7 +10897,7 @@ var ComponentService = class {
9888
10897
  };
9889
10898
  if (existsAt(componentsDir)) return void 0;
9890
10899
  try {
9891
- for (const entry of readdirSync3(componentsDir, { withFileTypes: true })) {
10900
+ for (const entry of readdirSync4(componentsDir, { withFileTypes: true })) {
9892
10901
  if (entry.isDirectory() && existsAt(join5(componentsDir, entry.name))) {
9893
10902
  return entry.name;
9894
10903
  }
@@ -9969,7 +10978,7 @@ var ComponentService = class {
9969
10978
  await this.writer.writeComponent(componentDir, name, dataWithJS);
9970
10979
  } else {
9971
10980
  const writeFile3 = this.fs ? this.fs.writeFile.bind(this.fs) : (await import("node:fs/promises")).writeFile;
9972
- if (targetCategory && !existsSync6(componentDir)) {
10981
+ if (targetCategory && !existsSync7(componentDir)) {
9973
10982
  mkdirSync2(componentDir, { recursive: true });
9974
10983
  }
9975
10984
  const filePath = join5(componentDir, `${name}.json`);
@@ -9998,10 +11007,12 @@ var ComponentService = class {
9998
11007
  async saveComponentJavaScript(name, javascript) {
9999
11008
  const componentDir = this.getComponentDir(name);
10000
11009
  if (this.writer) {
10001
- const current = this.components.get(name);
10002
- await this.writer.writeJavaScript(componentDir, name, javascript || "", current);
10003
- const next = current ? { ...current, component: { ...current.component, javascript: javascript || "" } } : { component: { javascript: javascript || "" } };
10004
- 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
+ });
10005
11016
  return;
10006
11017
  }
10007
11018
  const writeFile3 = this.fs ? this.fs.writeFile.bind(this.fs) : (await import("node:fs/promises")).writeFile;
@@ -10044,10 +11055,12 @@ var ComponentService = class {
10044
11055
  async saveComponentCSS(name, css) {
10045
11056
  const componentDir = this.getComponentDir(name);
10046
11057
  if (this.writer) {
10047
- const current = this.components.get(name);
10048
- await this.writer.writeCSS(componentDir, name, css || "", current);
10049
- const next = current ? { ...current, component: { ...current.component, css: css || "" } } : { component: { css: css || "" } };
10050
- 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
+ });
10051
11064
  return;
10052
11065
  }
10053
11066
  const writeFile3 = this.fs ? this.fs.writeFile.bind(this.fs) : (await import("node:fs/promises")).writeFile;
@@ -10086,11 +11099,11 @@ var ComponentService = class {
10086
11099
  */
10087
11100
  getAllFolders() {
10088
11101
  const componentsDir = this.componentsBaseDir();
10089
- if (!existsSync6(componentsDir)) {
11102
+ if (!existsSync7(componentsDir)) {
10090
11103
  return [];
10091
11104
  }
10092
11105
  try {
10093
- const entries = readdirSync3(componentsDir, { withFileTypes: true });
11106
+ const entries = readdirSync4(componentsDir, { withFileTypes: true });
10094
11107
  return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
10095
11108
  } catch {
10096
11109
  return [];
@@ -10122,7 +11135,7 @@ var ComponentService = class {
10122
11135
  }
10123
11136
  const componentsDir = this.componentsBaseDir();
10124
11137
  const folderPath = join5(componentsDir, folderName);
10125
- if (existsSync6(folderPath)) {
11138
+ if (existsSync7(folderPath)) {
10126
11139
  throw new Error("Folder already exists");
10127
11140
  }
10128
11141
  mkdirSync2(folderPath, { recursive: true });
@@ -10160,7 +11173,7 @@ var ComponentService = class {
10160
11173
  if (this.writer) {
10161
11174
  await this.writer.moveComponent(sourceDir, targetDir, name);
10162
11175
  } else {
10163
- if (targetCategory && !existsSync6(targetDir)) {
11176
+ if (targetCategory && !existsSync7(targetDir)) {
10164
11177
  mkdirSync2(targetDir, { recursive: true });
10165
11178
  }
10166
11179
  const extensions = [".json", ".js", ".css"];
@@ -10168,7 +11181,7 @@ var ComponentService = class {
10168
11181
  for (const ext of extensions) {
10169
11182
  const sourcePath = join5(sourceDir, `${name}${ext}`);
10170
11183
  const targetPath = join5(targetDir, `${name}${ext}`);
10171
- if (existsSync6(sourcePath)) {
11184
+ if (existsSync7(sourcePath)) {
10172
11185
  await rename2(sourcePath, targetPath);
10173
11186
  }
10174
11187
  }
@@ -10176,7 +11189,7 @@ var ComponentService = class {
10176
11189
  this.componentCategories.set(name, targetCategory);
10177
11190
  if (currentCategory) {
10178
11191
  try {
10179
- const remaining = readdirSync3(sourceDir);
11192
+ const remaining = readdirSync4(sourceDir);
10180
11193
  if (remaining.length === 0) {
10181
11194
  rmdirSync2(sourceDir);
10182
11195
  }
@@ -10231,7 +11244,7 @@ var ComponentService = class {
10231
11244
  await this.writer.moveComponent(dir, dir, oldName, trimmedNew);
10232
11245
  } else {
10233
11246
  for (const ext of [".json", ".js", ".css"]) {
10234
- if (existsSync6(join5(dir, `${trimmedNew}${ext}`))) {
11247
+ if (existsSync7(join5(dir, `${trimmedNew}${ext}`))) {
10235
11248
  throw new Error(`File "${trimmedNew}${ext}" already exists in components/${category ?? ""}`);
10236
11249
  }
10237
11250
  }
@@ -10239,7 +11252,7 @@ var ComponentService = class {
10239
11252
  for (const ext of [".json", ".js", ".css"]) {
10240
11253
  const src = join5(dir, `${oldName}${ext}`);
10241
11254
  const dst = join5(dir, `${trimmedNew}${ext}`);
10242
- if (existsSync6(src)) {
11255
+ if (existsSync7(src)) {
10243
11256
  await rename2(src, dst);
10244
11257
  }
10245
11258
  }
@@ -10273,11 +11286,11 @@ var ComponentService = class {
10273
11286
  let pageRefs = 0;
10274
11287
  if (pageService) {
10275
11288
  const pagePaths = pageService.getAllPagePaths();
10276
- for (const path3 of pagePaths) {
10277
- const pageData = pageService.getPageData(path3);
11289
+ for (const path5 of pagePaths) {
11290
+ const pageData = pageService.getPageData(path5);
10278
11291
  if (!pageData || !("root" in pageData) || !pageData.root) continue;
10279
11292
  if (rewriteComponentRefs(pageData.root, oldName, trimmedNew)) {
10280
- await pageService.savePage(path3, pageData);
11293
+ await pageService.savePage(path5, pageData);
10281
11294
  pageRefs++;
10282
11295
  }
10283
11296
  }
@@ -10435,12 +11448,12 @@ var CMSService = class {
10435
11448
  * @param locale - Optional locale for i18n slug matching
10436
11449
  * @returns CMSRouteMatch if matched, null otherwise
10437
11450
  */
10438
- async matchRoute(path3, locale) {
11451
+ async matchRoute(path5, locale) {
10439
11452
  if (!this.provider) {
10440
11453
  return null;
10441
11454
  }
10442
11455
  for (const route of this.routePatterns) {
10443
- const match = path3.match(route.regex);
11456
+ const match = path5.match(route.regex);
10444
11457
  if (match) {
10445
11458
  const urlSlug = match[route.slugGroup];
10446
11459
  if (urlSlug === void 0) continue;
@@ -10549,11 +11562,11 @@ var CMSService = class {
10549
11562
  if (options?.excludeDrafts && isItemDraftForLocale(item, locale)) continue;
10550
11563
  const localizedSlug = slugValue[locale];
10551
11564
  if (localizedSlug) {
10552
- const path3 = urlPattern.replace("{{slug}}", localizedSlug);
11565
+ const path5 = urlPattern.replace("{{slug}}", localizedSlug);
10553
11566
  if (locale === defaultLocale) {
10554
- urls.push(path3);
11567
+ urls.push(path5);
10555
11568
  } else {
10556
- urls.push(`/${locale}${path3}`);
11569
+ urls.push(`/${locale}${path5}`);
10557
11570
  }
10558
11571
  }
10559
11572
  }
@@ -10805,7 +11818,7 @@ var CMSService = class {
10805
11818
  };
10806
11819
 
10807
11820
  // lib/server/providers/fileSystemCMSProvider.ts
10808
- 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";
10809
11822
  import { join as join6 } from "node:path";
10810
11823
  var log6 = createLogger("FileSystemCMSProvider");
10811
11824
  var DRAFT_FILE_SUFFIX = `${CMS_DRAFT_SUFFIX}.json`;
@@ -10878,10 +11891,10 @@ var FileSystemCMSProvider = class {
10878
11891
  return this.schemaCache;
10879
11892
  }
10880
11893
  const schemas = /* @__PURE__ */ new Map();
10881
- if (!existsSync7(this.templatesDir)) {
11894
+ if (!existsSync8(this.templatesDir)) {
10882
11895
  return schemas;
10883
11896
  }
10884
- const files = readdirSync4(this.templatesDir);
11897
+ const files = readdirSync5(this.templatesDir);
10885
11898
  const jsonFiles = files.filter((f) => f.endsWith(".json"));
10886
11899
  const results = await Promise.all(
10887
11900
  jsonFiles.map(async (file) => {
@@ -10911,10 +11924,10 @@ var FileSystemCMSProvider = class {
10911
11924
  async getItems(collection) {
10912
11925
  this.validateCollection(collection);
10913
11926
  const collectionDir = join6(this.cmsDir, collection);
10914
- if (!existsSync7(collectionDir)) {
11927
+ if (!existsSync8(collectionDir)) {
10915
11928
  return [];
10916
11929
  }
10917
- const files = readdirSync4(collectionDir);
11930
+ const files = readdirSync5(collectionDir);
10918
11931
  const jsonFiles = files.filter((f) => f.endsWith(".json") && !f.endsWith(DRAFT_FILE_SUFFIX));
10919
11932
  const results = await Promise.all(
10920
11933
  jsonFiles.map(async (file) => {
@@ -11004,7 +12017,7 @@ var FileSystemCMSProvider = class {
11004
12017
  }
11005
12018
  this.validateFilename(filename);
11006
12019
  const collectionDir = join6(this.cmsDir, collection);
11007
- if (!existsSync7(collectionDir)) {
12020
+ if (!existsSync8(collectionDir)) {
11008
12021
  mkdirSync3(collectionDir, { recursive: true });
11009
12022
  }
11010
12023
  const itemData = stripTransient(item);
@@ -11019,11 +12032,11 @@ var FileSystemCMSProvider = class {
11019
12032
  this.validateFilename(filename);
11020
12033
  const { unlink } = await import("node:fs/promises");
11021
12034
  const publishedPath = join6(this.cmsDir, collection, `${filename}.json`);
11022
- if (existsSync7(publishedPath)) {
12035
+ if (existsSync8(publishedPath)) {
11023
12036
  await unlink(publishedPath);
11024
12037
  }
11025
12038
  const draftPath = this.draftPath(collection, filename);
11026
- if (existsSync7(draftPath)) {
12039
+ if (existsSync8(draftPath)) {
11027
12040
  await unlink(draftPath);
11028
12041
  }
11029
12042
  }
@@ -11057,8 +12070,8 @@ var FileSystemCMSProvider = class {
11057
12070
  async getAllDrafts(collection) {
11058
12071
  this.validateCollection(collection);
11059
12072
  const collectionDir = join6(this.cmsDir, collection);
11060
- if (!existsSync7(collectionDir)) return [];
11061
- 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));
11062
12075
  const results = await Promise.all(
11063
12076
  files.map(async (file) => {
11064
12077
  const filePath = join6(collectionDir, file);
@@ -11083,7 +12096,7 @@ var FileSystemCMSProvider = class {
11083
12096
  async hasDraft(collection, filename) {
11084
12097
  this.validateCollection(collection);
11085
12098
  this.validateFilename(filename);
11086
- return existsSync7(this.draftPath(collection, filename));
12099
+ return existsSync8(this.draftPath(collection, filename));
11087
12100
  }
11088
12101
  /**
11089
12102
  * Save the draft version of an item. Loose validation — drafts may have
@@ -11118,7 +12131,7 @@ var FileSystemCMSProvider = class {
11118
12131
  }
11119
12132
  this.validateFilename(filename);
11120
12133
  const collectionDir = join6(this.cmsDir, collection);
11121
- if (!existsSync7(collectionDir)) {
12134
+ if (!existsSync8(collectionDir)) {
11122
12135
  mkdirSync3(collectionDir, { recursive: true });
11123
12136
  }
11124
12137
  const itemData = stripTransient(item);
@@ -11138,7 +12151,7 @@ var FileSystemCMSProvider = class {
11138
12151
  this.validateFilename(filename);
11139
12152
  const { unlink } = await import("node:fs/promises");
11140
12153
  const filePath = this.draftPath(collection, filename);
11141
- if (existsSync7(filePath)) {
12154
+ if (existsSync8(filePath)) {
11142
12155
  await unlink(filePath);
11143
12156
  }
11144
12157
  }
@@ -11167,13 +12180,13 @@ var FileSystemCMSProvider = class {
11167
12180
  throw new Error(`Cannot publish invalid draft: ${messages}`);
11168
12181
  }
11169
12182
  const collectionDir = join6(this.cmsDir, collection);
11170
- if (!existsSync7(collectionDir)) {
12183
+ if (!existsSync8(collectionDir)) {
11171
12184
  mkdirSync3(collectionDir, { recursive: true });
11172
12185
  }
11173
12186
  const itemData = stripTransient(validation.data);
11174
12187
  const publishedPath = join6(collectionDir, `${filename}.json`);
11175
12188
  await writeFile3(publishedPath, JSON.stringify(itemData, null, 2), "utf-8");
11176
- if (existsSync7(draftFilePath)) {
12189
+ if (existsSync8(draftFilePath)) {
11177
12190
  await unlink(draftFilePath);
11178
12191
  }
11179
12192
  return normalizeItem(itemData, filename);
@@ -11194,18 +12207,18 @@ var FileSystemCMSProvider = class {
11194
12207
  );
11195
12208
  }
11196
12209
  const { writeFile: writeFile3, mkdir } = await import("node:fs/promises");
11197
- if (!existsSync7(this.templatesDir)) {
12210
+ if (!existsSync8(this.templatesDir)) {
11198
12211
  await mkdir(this.templatesDir, { recursive: true });
11199
12212
  }
11200
12213
  const pageFilePath = join6(this.templatesDir, `${collectionId}.json`);
11201
- if (existsSync7(pageFilePath)) {
12214
+ if (existsSync8(pageFilePath)) {
11202
12215
  throw new Error(`Page file already exists: templates/${collectionId}.json`);
11203
12216
  }
11204
12217
  await writeFile3(pageFilePath, JSON.stringify(pageData, null, 2), "utf-8");
11205
12218
  const schemaSource = pageData?.meta?.cms?.source;
11206
12219
  if (schemaSource !== "sanity") {
11207
12220
  const collectionDir = join6(this.cmsDir, collectionId);
11208
- if (!existsSync7(collectionDir)) {
12221
+ if (!existsSync8(collectionDir)) {
11209
12222
  await mkdir(collectionDir, { recursive: true });
11210
12223
  }
11211
12224
  }
@@ -11221,12 +12234,12 @@ var FileSystemCMSProvider = class {
11221
12234
  `Invalid collection ID: "${collectionId}". Collection IDs must contain only letters, numbers, hyphens, and underscores.`
11222
12235
  );
11223
12236
  }
11224
- const { readFile: readFile2, writeFile: writeFile3 } = await import("node:fs/promises");
12237
+ const { readFile: readFile3, writeFile: writeFile3 } = await import("node:fs/promises");
11225
12238
  const pageFilePath = join6(this.templatesDir, `${collectionId}.json`);
11226
- if (!existsSync7(pageFilePath)) {
12239
+ if (!existsSync8(pageFilePath)) {
11227
12240
  throw new Error(`Collection not found: ${collectionId}`);
11228
12241
  }
11229
- const content = await readFile2(pageFilePath, "utf-8");
12242
+ const content = await readFile3(pageFilePath, "utf-8");
11230
12243
  const pageData = JSON.parse(content);
11231
12244
  pageData.meta.cms = {
11232
12245
  ...pageData.meta.cms,
@@ -11245,26 +12258,26 @@ var PageCache = class {
11245
12258
  /**
11246
12259
  * Store page content and line map in cache
11247
12260
  */
11248
- set(path3, content, lineMap) {
11249
- this.cache.set(path3, { content, lineMap });
12261
+ set(path5, content, lineMap) {
12262
+ this.cache.set(path5, { content, lineMap });
11250
12263
  }
11251
12264
  /**
11252
12265
  * Get cached page data
11253
12266
  */
11254
- get(path3) {
11255
- return this.cache.get(path3);
12267
+ get(path5) {
12268
+ return this.cache.get(path5);
11256
12269
  }
11257
12270
  /**
11258
12271
  * Get page content from cache
11259
12272
  */
11260
- getContent(path3) {
11261
- return this.cache.get(path3)?.content;
12273
+ getContent(path5) {
12274
+ return this.cache.get(path5)?.content;
11262
12275
  }
11263
12276
  /**
11264
12277
  * Get line map from cache
11265
12278
  */
11266
- getLineMap(path3) {
11267
- return this.cache.get(path3)?.lineMap;
12279
+ getLineMap(path5) {
12280
+ return this.cache.get(path5)?.lineMap;
11268
12281
  }
11269
12282
  /**
11270
12283
  * Get line range for a specific element path
@@ -11275,14 +12288,14 @@ var PageCache = class {
11275
12288
  /**
11276
12289
  * Delete page from cache
11277
12290
  */
11278
- delete(path3) {
11279
- this.cache.delete(path3);
12291
+ delete(path5) {
12292
+ this.cache.delete(path5);
11280
12293
  }
11281
12294
  /**
11282
12295
  * Check if page exists in cache
11283
12296
  */
11284
- has(path3) {
11285
- return this.cache.has(path3);
12297
+ has(path5) {
12298
+ return this.cache.has(path5);
11286
12299
  }
11287
12300
  /**
11288
12301
  * Get all page paths
@@ -11321,21 +12334,21 @@ var DraftPageStore = class {
11321
12334
  * Store a draft for a page path. The value is the raw JSON string the
11322
12335
  * SSR pipeline expects, matching PageService.getPage()'s contract.
11323
12336
  */
11324
- set(path3, content) {
11325
- this.drafts.set(path3, content);
12337
+ set(path5, content) {
12338
+ this.drafts.set(path5, content);
11326
12339
  }
11327
12340
  /**
11328
12341
  * Get the current draft string for a page path, if any.
11329
12342
  */
11330
- get(path3) {
11331
- return this.drafts.get(path3);
12343
+ get(path5) {
12344
+ return this.drafts.get(path5);
11332
12345
  }
11333
12346
  /**
11334
12347
  * Drop the draft for a specific path. Called when the page is saved to
11335
12348
  * disk so subsequent renders read the persisted version.
11336
12349
  */
11337
- clear(path3) {
11338
- this.drafts.delete(path3);
12350
+ clear(path5) {
12351
+ this.drafts.delete(path5);
11339
12352
  }
11340
12353
  /**
11341
12354
  * Drop every draft. Used on shutdown / project switch.
@@ -11343,14 +12356,19 @@ var DraftPageStore = class {
11343
12356
  clearAll() {
11344
12357
  this.drafts.clear();
11345
12358
  }
11346
- has(path3) {
11347
- return this.drafts.has(path3);
12359
+ has(path5) {
12360
+ return this.drafts.has(path5);
11348
12361
  }
11349
12362
  };
11350
12363
 
11351
12364
  // lib/server/fileWatcher.ts
11352
- import { watch, existsSync as existsSync8, statSync, readdirSync as readdirSync5 } from "node:fs";
11353
- 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
11354
12372
  function watchRecursive(root, listener) {
11355
12373
  const nested = [];
11356
12374
  const watchedDirs = /* @__PURE__ */ new Set();
@@ -11358,17 +12376,17 @@ function watchRecursive(root, listener) {
11358
12376
  const coverNewDir = (abs) => {
11359
12377
  let isDir = false;
11360
12378
  try {
11361
- isDir = statSync(abs).isDirectory();
12379
+ isDir = statSync2(abs).isDirectory();
11362
12380
  } catch {
11363
12381
  return;
11364
12382
  }
11365
12383
  if (!isDir || watchedDirs.has(abs)) return;
11366
12384
  attachDir(abs);
11367
12385
  try {
11368
- for (const e of readdirSync5(abs, { withFileTypes: true })) {
12386
+ for (const e of readdirSync6(abs, { withFileTypes: true })) {
11369
12387
  const child = join7(abs, e.name);
11370
12388
  if (e.isDirectory()) coverNewDir(child);
11371
- else listener("rename", relative(root, child));
12389
+ else listener("rename", relative2(root, child));
11372
12390
  }
11373
12391
  } catch {
11374
12392
  }
@@ -11381,7 +12399,7 @@ function watchRecursive(root, listener) {
11381
12399
  w = watch(absDir, { recursive: true }, (event, filename) => {
11382
12400
  if (!filename) return;
11383
12401
  const abs = join7(absDir, filename);
11384
- listener(event, relative(root, abs));
12402
+ listener(event, relative2(root, abs));
11385
12403
  coverNewDir(abs);
11386
12404
  });
11387
12405
  } catch {
@@ -11406,19 +12424,19 @@ function watchRecursive(root, listener) {
11406
12424
  return handle;
11407
12425
  }
11408
12426
  function attachWhenDirExists(dirPath, attach, setWatcher) {
11409
- if (existsSync8(dirPath)) {
12427
+ if (existsSync9(dirPath)) {
11410
12428
  setWatcher(attach());
11411
12429
  return;
11412
12430
  }
11413
12431
  const parentDir = dirname2(dirPath);
11414
12432
  const targetName = basename2(dirPath);
11415
- if (!existsSync8(parentDir)) {
12433
+ if (!existsSync9(parentDir)) {
11416
12434
  return;
11417
12435
  }
11418
12436
  let parentWatcher = null;
11419
12437
  parentWatcher = watch(parentDir, (_event, filename) => {
11420
12438
  if (filename !== targetName) return;
11421
- if (!existsSync8(dirPath)) return;
12439
+ if (!existsSync9(dirPath)) return;
11422
12440
  parentWatcher?.close();
11423
12441
  parentWatcher = null;
11424
12442
  setWatcher(attach());
@@ -11448,7 +12466,7 @@ var FileWatcher = class {
11448
12466
  * Watches both .json and .js files to detect component definition and JavaScript changes
11449
12467
  */
11450
12468
  watchComponents(dirPath = projectPaths.components()) {
11451
- if (!existsSync8(dirPath)) {
12469
+ if (!existsSync9(dirPath)) {
11452
12470
  return;
11453
12471
  }
11454
12472
  this.componentsWatcher = watchRecursive(dirPath, async (_event, filename) => {
@@ -11464,7 +12482,7 @@ var FileWatcher = class {
11464
12482
  * Start watching pages directory
11465
12483
  */
11466
12484
  watchPages(dirPath = projectPaths.pages()) {
11467
- if (!existsSync8(dirPath)) {
12485
+ if (!existsSync9(dirPath)) {
11468
12486
  return;
11469
12487
  }
11470
12488
  this.pagesWatcher = watchRecursive(dirPath, async (_event, filename) => {
@@ -11504,7 +12522,7 @@ var FileWatcher = class {
11504
12522
  * Start watching colors.json file
11505
12523
  */
11506
12524
  watchColors(filePath = projectPaths.colors()) {
11507
- if (!existsSync8(filePath)) {
12525
+ if (!existsSync9(filePath)) {
11508
12526
  return;
11509
12527
  }
11510
12528
  const dirPath = getProjectRoot();
@@ -11561,7 +12579,7 @@ var FileWatcher = class {
11561
12579
  * Start watching images directory for new source images
11562
12580
  */
11563
12581
  watchImages(dirPath = projectPaths.images()) {
11564
- if (!existsSync8(dirPath)) {
12582
+ if (!existsSync9(dirPath)) {
11565
12583
  return;
11566
12584
  }
11567
12585
  this.imagesWatcher = watch(dirPath, { recursive: false }, async (event, filename) => {
@@ -11588,7 +12606,7 @@ var FileWatcher = class {
11588
12606
  * Start watching libraries directory for CSS/JS changes
11589
12607
  */
11590
12608
  watchLibraries(dirPath = projectPaths.libraries()) {
11591
- if (!existsSync8(dirPath)) {
12609
+ if (!existsSync9(dirPath)) {
11592
12610
  return;
11593
12611
  }
11594
12612
  this.librariesWatcher = watchRecursive(dirPath, async (_event, filename) => {
@@ -11621,7 +12639,8 @@ var FileWatcher = class {
11621
12639
  if (this.callbacks.onCMSTemplateChange) await this.callbacks.onCMSTemplateChange();
11622
12640
  return;
11623
12641
  }
11624
- const pagePath = mapPageNameToPath(filename.replace(/\.astro$/, ""));
12642
+ const pageName = collapseFolderIndex(filename.replace(/\.astro$/, ""));
12643
+ const pagePath = mapPageNameToPath(pageName);
11625
12644
  if (this.callbacks.onPageChange) await this.callbacks.onPageChange(pagePath);
11626
12645
  }),
11627
12646
  (w) => {
@@ -11789,9 +12808,9 @@ var FileWatcherService = class {
11789
12808
  this.refreshSchemasTimer = null;
11790
12809
  await cmsService.refreshSchemas();
11791
12810
  const ids = new Set(cmsService.getAllSchemas().keys());
11792
- for (const path3 of pageService.getAllPagePaths()) {
11793
- if (path3.startsWith("/templates/") && !ids.has(path3.slice("/templates/".length))) {
11794
- 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);
11795
12814
  }
11796
12815
  }
11797
12816
  for (const id of ids) {
@@ -11845,20 +12864,20 @@ var FileWatcherService = class {
11845
12864
  };
11846
12865
 
11847
12866
  // lib/server/migrateTemplates.ts
11848
- import { existsSync as existsSync9 } from "node:fs";
12867
+ import { existsSync as existsSync10 } from "node:fs";
11849
12868
  import { rename } from "node:fs/promises";
11850
12869
  import { join as join8 } from "node:path";
11851
12870
  async function migrateTemplatesDirectory() {
11852
12871
  const oldDir = join8(projectPaths.pages(), "templates");
11853
12872
  const newDir = projectPaths.templates();
11854
- if (!existsSync9(oldDir)) return;
11855
- if (existsSync9(newDir)) return;
12873
+ if (!existsSync10(oldDir)) return;
12874
+ if (existsSync10(newDir)) return;
11856
12875
  await rename(oldDir, newDir);
11857
12876
  console.log("Migrated CMS templates: pages/templates/ \u2192 templates/");
11858
12877
  }
11859
12878
 
11860
12879
  // build-astro.ts
11861
- 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";
11862
12881
  import { writeFile as writeFile2, readFile } from "node:fs/promises";
11863
12882
  import { join as join9 } from "node:path";
11864
12883
  import { createHash } from "node:crypto";
@@ -12944,8 +13963,8 @@ function injectInlineStyle(styleAttr, extraCss) {
12944
13963
  if (dynLiteralMatch) {
12945
13964
  const existing = dynLiteralMatch[1] ?? "";
12946
13965
  const trimmed = existing.trimEnd();
12947
- const sep = trimmed.length > 0 && !trimmed.endsWith(";") ? ";" : "";
12948
- return ` style={\`${existing}${sep}${extraCss}\`}`;
13966
+ const sep2 = trimmed.length > 0 && !trimmed.endsWith(";") ? ";" : "";
13967
+ return ` style={\`${existing}${sep2}${extraCss}\`}`;
12949
13968
  }
12950
13969
  const dynExprMatch = styleAttr.match(/^ style=\{([\s\S]*)\}$/);
12951
13970
  if (dynExprMatch) {
@@ -12953,8 +13972,8 @@ function injectInlineStyle(styleAttr, extraCss) {
12953
13972
  }
12954
13973
  return styleAttr.replace(/style="([^"]*)"/, (_, existing) => {
12955
13974
  const trimmed = existing.trimEnd();
12956
- const sep = trimmed.length > 0 && !trimmed.endsWith(";") ? ";" : "";
12957
- return `style="${existing}${sep}${extraCss}"`;
13975
+ const sep2 = trimmed.length > 0 && !trimmed.endsWith(";") ? ";" : "";
13976
+ return `style="${existing}${sep2}${extraCss}"`;
12958
13977
  });
12959
13978
  }
12960
13979
  var PICTURE_WIDTHS = [500, 800, 1080, 1600, 2400];
@@ -13680,7 +14699,7 @@ function emitLocaleListNode(node, ctx) {
13680
14699
  const code = localeConfig.code;
13681
14700
  const isCurrent = code === currentLocale;
13682
14701
  if (!showCurrent && isCurrent) continue;
13683
- const path3 = slugMap[code] || "/";
14702
+ const path5 = slugMap[code] || "/";
13684
14703
  const classes = isCurrent ? activeItemClasses : itemClasses;
13685
14704
  const classAttr = classes.length > 0 ? ` class="${classes.join(" ")}"` : "";
13686
14705
  const currentAttr = isCurrent ? ' data-current="true"' : ' data-current="false"';
@@ -13705,7 +14724,7 @@ function emitLocaleListNode(node, ctx) {
13705
14724
  }
13706
14725
  linkContent += `<div>${escapeJSX(displayText)}</div>`;
13707
14726
  links.push(
13708
- `${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>`
13709
14728
  );
13710
14729
  }
13711
14730
  let linksContent;
@@ -14199,8 +15218,8 @@ function emitAstroPage(options) {
14199
15218
  }
14200
15219
  const componentImports = Array.from(ctx.imports).sort();
14201
15220
  for (const comp of componentImports) {
14202
- const path3 = componentImportPath(fileDepth, comp);
14203
- importLines.push(`import ${astroComponentName(comp)} from '${path3}';`);
15221
+ const path5 = componentImportPath(fileDepth, comp);
15222
+ importLines.push(`import ${astroComponentName(comp)} from '${path5}';`);
14204
15223
  }
14205
15224
  const scriptsArrayLiteral = scriptPaths.length > 0 ? `[${scriptPaths.map((s) => `"${s}"`).join(", ")}]` : "[]";
14206
15225
  const libraryTagsLiteral = `{ headCSS: \`${escapeTemplateLiteral2(libraryTags.headCSS || "")}\`, headJS: \`${escapeTemplateLiteral2(libraryTags.headJS || "")}\`, bodyEndJS: \`${escapeTemplateLiteral2(libraryTags.bodyEndJS || "")}\` }`;
@@ -14539,8 +15558,8 @@ function emitCMSPage(options) {
14539
15558
  }
14540
15559
  const componentImports = Array.from(ctx.imports).sort();
14541
15560
  for (const comp of componentImports) {
14542
- const path3 = componentImportPath2(fileDepth, comp);
14543
- importLines.push(`import ${astroComponentName(comp)} from '${path3}';`);
15561
+ const path5 = componentImportPath2(fileDepth, comp);
15562
+ importLines.push(`import ${astroComponentName(comp)} from '${path5}';`);
14544
15563
  }
14545
15564
  const staticPaths = buildGetStaticPaths(cmsSchema, isMultiLocale, i18nConfig, locale);
14546
15565
  const scriptsArrayLiteral = scriptPaths.length > 0 ? `[${scriptPaths.map((s) => `"${s}"`).join(", ")}]` : "[]";
@@ -14713,24 +15732,24 @@ function writePageScript(javascript, scriptsDir) {
14713
15732
  if (!javascript) return [];
14714
15733
  const hash = hashContent3(javascript);
14715
15734
  const scriptFile = `${hash}.js`;
14716
- if (!existsSync10(scriptsDir)) {
15735
+ if (!existsSync11(scriptsDir)) {
14717
15736
  mkdirSync4(scriptsDir, { recursive: true });
14718
15737
  }
14719
15738
  const fullScriptPath = join9(scriptsDir, scriptFile);
14720
- if (!existsSync10(fullScriptPath)) {
15739
+ if (!existsSync11(fullScriptPath)) {
14721
15740
  writeFileSync(fullScriptPath, javascript, "utf-8");
14722
15741
  }
14723
15742
  return [`/_scripts/${scriptFile}`];
14724
15743
  }
14725
15744
  function copyDirectory(src, dest, filter) {
14726
- if (!existsSync10(src)) return;
14727
- if (!existsSync10(dest)) mkdirSync4(dest, { recursive: true });
14728
- const files = readdirSync6(src);
15745
+ if (!existsSync11(src)) return;
15746
+ if (!existsSync11(dest)) mkdirSync4(dest, { recursive: true });
15747
+ const files = readdirSync7(src);
14729
15748
  for (const file of files) {
14730
15749
  if (filter && !filter(file)) continue;
14731
15750
  const srcPath = join9(src, file);
14732
15751
  const destPath = join9(dest, file);
14733
- const stat = statSync2(srcPath);
15752
+ const stat = statSync3(srcPath);
14734
15753
  if (stat.isDirectory()) copyDirectory(srcPath, destPath, filter);
14735
15754
  else copyFileSync(srcPath, destPath);
14736
15755
  }
@@ -14746,8 +15765,8 @@ function isCMSPage(pageData) {
14746
15765
  }
14747
15766
  function scanJSONFiles(dir, prefix = "") {
14748
15767
  const results = [];
14749
- if (!existsSync10(dir)) return results;
14750
- const entries = readdirSync6(dir, { withFileTypes: true });
15768
+ if (!existsSync11(dir)) return results;
15769
+ const entries = readdirSync7(dir, { withFileTypes: true });
14751
15770
  for (const entry of entries) {
14752
15771
  if (entry.isFile() && entry.name.endsWith(".json")) {
14753
15772
  results.push(prefix ? `${prefix}/${entry.name}` : entry.name);
@@ -14919,7 +15938,7 @@ async function buildAstroProject(_projectRoot, outputDir) {
14919
15938
  const componentLibraries = collectComponentLibraries(globalComponents);
14920
15939
  const imageMetadataMap = await buildImageMetadataMap();
14921
15940
  const outDir = outputDir || join9(projectPaths.project, "astro-export");
14922
- if (existsSync10(outDir)) {
15941
+ if (existsSync11(outDir)) {
14923
15942
  rmSync(outDir, { recursive: true, force: true });
14924
15943
  }
14925
15944
  mkdirSync4(outDir, { recursive: true });
@@ -14934,7 +15953,7 @@ async function buildAstroProject(_projectRoot, outputDir) {
14934
15953
  mkdirSync4(d, { recursive: true });
14935
15954
  }
14936
15955
  const pagesDir = projectPaths.pages();
14937
- if (!existsSync10(pagesDir)) {
15956
+ if (!existsSync11(pagesDir)) {
14938
15957
  console.error("Pages directory not found!");
14939
15958
  return { pages: 0, cmsPages: 0, collections: 0, errors: 1 };
14940
15959
  }
@@ -15065,7 +16084,7 @@ async function buildAstroProject(_projectRoot, outputDir) {
15065
16084
  const shouldInline = css.inline !== false;
15066
16085
  const relPath = css.url.slice(1);
15067
16086
  const srcPath = join9(projectPaths.project, relPath);
15068
- if (!existsSync10(srcPath)) continue;
16087
+ if (!existsSync11(srcPath)) continue;
15069
16088
  if (shouldInline) {
15070
16089
  try {
15071
16090
  inlineContents.set(css.url, await readFile(srcPath, "utf-8"));
@@ -15079,7 +16098,7 @@ async function buildAstroProject(_projectRoot, outputDir) {
15079
16098
  for (const js of buildLibraries.js || []) {
15080
16099
  if (js.url.startsWith("/")) {
15081
16100
  const relPath = js.url.slice(1);
15082
- if (existsSync10(join9(projectPaths.project, relPath))) {
16101
+ if (existsSync11(join9(projectPaths.project, relPath))) {
15083
16102
  localLibsToCopy.push(relPath);
15084
16103
  }
15085
16104
  }
@@ -15100,8 +16119,8 @@ async function buildAstroProject(_projectRoot, outputDir) {
15100
16119
  const cmsConsumerComponents = computeCmsConsumerComponents(globalComponents);
15101
16120
  const collectionUrlExpr = /* @__PURE__ */ new Map();
15102
16121
  const mergedRichTextFields = /* @__PURE__ */ new Set();
15103
- if (existsSync10(templatesDir)) {
15104
- 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"))) {
15105
16124
  const tc = await loadJSONFile(join9(templatesDir, file));
15106
16125
  if (!tc) continue;
15107
16126
  try {
@@ -15116,8 +16135,8 @@ async function buildAstroProject(_projectRoot, outputDir) {
15116
16135
  }
15117
16136
  }
15118
16137
  }
15119
- if (existsSync10(templatesDir)) {
15120
- const templateFiles = readdirSync6(templatesDir).filter((f) => f.endsWith(".json"));
16138
+ if (existsSync11(templatesDir)) {
16139
+ const templateFiles = readdirSync7(templatesDir).filter((f) => f.endsWith(".json"));
15121
16140
  for (const file of templateFiles) {
15122
16141
  const templateContent = await loadJSONFile(join9(templatesDir, file));
15123
16142
  if (!templateContent) continue;
@@ -15210,7 +16229,7 @@ async function buildAstroProject(_projectRoot, outputDir) {
15210
16229
  });
15211
16230
  const astroFileFull = join9(pagesOutDir, astroFilePath);
15212
16231
  const astroFileDir = astroFileFull.substring(0, astroFileFull.lastIndexOf("/"));
15213
- if (!existsSync10(astroFileDir)) {
16232
+ if (!existsSync11(astroFileDir)) {
15214
16233
  mkdirSync4(astroFileDir, { recursive: true });
15215
16234
  }
15216
16235
  await writeFile2(astroFileFull, astroContent, "utf-8");
@@ -15361,7 +16380,7 @@ const { title, meta = '', scripts = [], locale = 'en', theme = '${themeConfig.de
15361
16380
  }
15362
16381
  const astroFileFull = join9(pagesOutDir, result.astroFilePath);
15363
16382
  const astroFileDir = astroFileFull.substring(0, astroFileFull.lastIndexOf("/"));
15364
- if (!existsSync10(astroFileDir)) {
16383
+ if (!existsSync11(astroFileDir)) {
15365
16384
  mkdirSync4(astroFileDir, { recursive: true });
15366
16385
  }
15367
16386
  await writeFile2(astroFileFull, astroContent, "utf-8");
@@ -15393,9 +16412,9 @@ export const GET: APIRoute = () => {
15393
16412
  mkdirSync4(collectionDir, { recursive: true });
15394
16413
  const richTextFieldNames = Object.entries(schema.fields || {}).filter(([, fd]) => fd.type === "rich-text").map(([fn]) => fn);
15395
16414
  const cmsItemsDir = join9(projectPaths.cms(), schema.id);
15396
- if (existsSync10(cmsItemsDir)) {
16415
+ if (existsSync11(cmsItemsDir)) {
15397
16416
  const isDevBuild = process.env.MENO_DEV_BUILD === "true";
15398
- const itemFiles = readdirSync6(cmsItemsDir).filter(
16417
+ const itemFiles = readdirSync7(cmsItemsDir).filter(
15399
16418
  (f) => f.endsWith(".json") && (isDevBuild || !f.endsWith(`${CMS_DRAFT_SUFFIX}.json`))
15400
16419
  );
15401
16420
  for (const itemFile of itemFiles) {
@@ -15442,26 +16461,26 @@ export { collections };
15442
16461
  await writeFile2(join9(srcDir, "content.config.ts"), configContent, "utf-8");
15443
16462
  }
15444
16463
  const imagesSrcDir = join9(projectPaths.project, "images");
15445
- if (existsSync10(imagesSrcDir)) {
16464
+ if (existsSync11(imagesSrcDir)) {
15446
16465
  copyDirectory(imagesSrcDir, join9(srcDir, "assets", "images"), shouldCopyImageForAstro);
15447
16466
  copyDirectory(imagesSrcDir, join9(publicDir, "images"));
15448
16467
  }
15449
16468
  const publicAssetDirs = ["fonts", "icons", "videos", "assets"];
15450
16469
  for (const dir of publicAssetDirs) {
15451
16470
  const srcAssetDir = join9(projectPaths.project, dir);
15452
- if (existsSync10(srcAssetDir)) {
16471
+ if (existsSync11(srcAssetDir)) {
15453
16472
  copyDirectory(srcAssetDir, join9(publicDir, dir));
15454
16473
  }
15455
16474
  }
15456
16475
  const librariesDir = join9(projectPaths.project, "libraries");
15457
- if (existsSync10(librariesDir)) {
16476
+ if (existsSync11(librariesDir)) {
15458
16477
  copyDirectory(librariesDir, join9(publicDir, "libraries"));
15459
16478
  }
15460
16479
  for (const relPath of localLibsToCopy) {
15461
16480
  const srcPath = join9(projectPaths.project, relPath);
15462
16481
  const destPath = join9(publicDir, relPath);
15463
16482
  const destDir = destPath.substring(0, destPath.lastIndexOf("/"));
15464
- if (destDir && !existsSync10(destDir)) mkdirSync4(destDir, { recursive: true });
16483
+ if (destDir && !existsSync11(destDir)) mkdirSync4(destDir, { recursive: true });
15465
16484
  copyFileSync(srcPath, destPath);
15466
16485
  }
15467
16486
  const packageJson = {
@@ -15531,6 +16550,32 @@ export default defineConfig({${siteUrl ? `
15531
16550
 
15532
16551
  // lib/server/index.ts
15533
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
+ }
15534
16579
  export {
15535
16580
  CMSService,
15536
16581
  ColorService,
@@ -15550,6 +16595,7 @@ export {
15550
16595
  SERVE_PORT,
15551
16596
  VariableService,
15552
16597
  WebSocketManager,
16598
+ auditStylesheet,
15553
16599
  buildAstroProject,
15554
16600
  buildAttributes,
15555
16601
  buildComponentHTML,
@@ -15557,6 +16603,7 @@ export {
15557
16603
  buildLineMap,
15558
16604
  bundleFile,
15559
16605
  clearJSValidationCache,
16606
+ collectClassesFromPageData,
15560
16607
  collectComponentCSS,
15561
16608
  collectComponentJavaScript,
15562
16609
  colorService,
@@ -15571,6 +16618,7 @@ export {
15571
16618
  errorResponse,
15572
16619
  escapeHtml,
15573
16620
  extractPageMeta,
16621
+ extractRules,
15574
16622
  fileExists,
15575
16623
  formHandlerScript,
15576
16624
  generateBuildErrorPage,
@@ -15617,12 +16665,14 @@ export {
15617
16665
  readJsonFile,
15618
16666
  readTextFile,
15619
16667
  regenerateThemeCss,
16668
+ renderAuditMarkdown,
15620
16669
  renderPageSSR,
15621
16670
  resetFontConfig,
15622
16671
  resolvePackagePath,
15623
16672
  resolveProjectPath,
15624
16673
  saveColors,
15625
16674
  saveVariables,
16675
+ sectionPrefix,
15626
16676
  serializeThemeCss,
15627
16677
  serveFile,
15628
16678
  setProjectRoot,
@@ -15634,6 +16684,7 @@ export {
15634
16684
  watchRecursive,
15635
16685
  withErrorHandling,
15636
16686
  withLogging,
16687
+ wrapInWebflowTemplate,
15637
16688
  writeFile
15638
16689
  };
15639
16690
  //# sourceMappingURL=index.js.map