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
@@ -378,6 +378,66 @@ function negateCssValue(root: string, value: string): string | null {
378
378
  return /^\d/.test(value) ? `-${value}` : null;
379
379
  }
380
380
 
381
+ /**
382
+ * Border-width/style shorthand family — Tailwind's `border`, `border-2`, `border-t`,
383
+ * `border-b-4`, `border-solid`, … Meno has no Preflight to supply a default border-style,
384
+ * so a bare `border` (width only) would render invisible; these instead expand to a
385
+ * `<width> solid` shorthand — visible exactly as the class reads, with no smuggled design
386
+ * value (`1px` is the class's literal meaning). The color is left OFF: an omitted border
387
+ * color defaults to `currentColor` (inherits the text color), and — crucially — the per-side
388
+ * forms then emit no color longhand, so a separate `border-<token>` / `border-[#hex]` class
389
+ * controls the color with no cascade fight. Style is overridable via `border-dashed` etc.
390
+ * `border-x` / `border-y` use the logical `border-inline` / `border-block` shorthands.
391
+ *
392
+ * Colors (`border-primary`, `border-[#fff]`, `border-(--x)`) and arbitrary shorthands
393
+ * (`border-[1px_solid_red]`) are handled by the color-token / arbitrary / var branches — this
394
+ * only absorbs the bare width/style idioms that were previously dropped.
395
+ */
396
+ const BORDER_SIDE_PROPERTY: Record<string, string> = {
397
+ border: 'border',
398
+ 'border-t': 'border-top',
399
+ 'border-r': 'border-right',
400
+ 'border-b': 'border-bottom',
401
+ 'border-l': 'border-left',
402
+ 'border-x': 'border-inline',
403
+ 'border-y': 'border-block',
404
+ };
405
+ const BORDER_STYLE_KEYWORDS: ReadonlySet<string> = new Set(['solid', 'dashed', 'dotted', 'double', 'none', 'hidden']);
406
+ /** Default style composed onto a bare/numeric border width so it renders on its own (color inherits). */
407
+ const BORDER_STYLE = 'solid';
408
+
409
+ /**
410
+ * Parse a border width/style class. `root` is `border` (all-side widths, bare sides, style
411
+ * keywords) or a side root like `border-t` (a sided width, `border-t-2`). Returns null for
412
+ * anything that isn't a bare width / style keyword (colors fall through to their own branch).
413
+ */
414
+ function parseBorderClass(root: string, rest: string): ParsedUtilityClass | null {
415
+ // Sided width: `border-t-2`, `border-b-4`, `border-x-8` (entered as the side root).
416
+ if (root !== 'border') {
417
+ const sideProp = BORDER_SIDE_PROPERTY[root];
418
+ if (sideProp && /^\d+$/.test(rest)) {
419
+ return { property: sideProp, value: `${rest}px ${BORDER_STYLE}`, root, kind: 'numeric' };
420
+ }
421
+ return null;
422
+ }
423
+ // Bare side: `border-t` … `border-y` arrive here as root `border`, rest = the side letter.
424
+ // Re-root to the side (`border-t`) so the CSS generator emits per-side longhands.
425
+ const sideRoot = `border-${rest}`;
426
+ const sideProp = BORDER_SIDE_PROPERTY[sideRoot];
427
+ if (sideProp) {
428
+ return { property: sideProp, value: `1px ${BORDER_STYLE}`, root: sideRoot, kind: 'keyword' };
429
+ }
430
+ // All-side width: `border-0`, `border-2`, `border-4` (bare `border` is handled upstream).
431
+ if (/^\d+$/.test(rest)) {
432
+ return { property: 'border', value: `${rest}px ${BORDER_STYLE}`, root: 'border', kind: 'numeric' };
433
+ }
434
+ // Border style on its own: `border-solid`, `border-dashed`, `border-none`, …
435
+ if (BORDER_STYLE_KEYWORDS.has(rest)) {
436
+ return { property: 'border-style', value: rest, root: 'border', kind: 'keyword' };
437
+ }
438
+ return null;
439
+ }
440
+
381
441
  /**
382
442
  * Parse a single (unprefixed) utility class name into its CSS property and
383
443
  * decoded value. Returns null for class names that aren't Meno utilities.
@@ -405,6 +465,12 @@ export function parseUtilityClass(className: string, knownTokens?: ReadonlySet<s
405
465
  };
406
466
  }
407
467
 
468
+ // Bare `border` → a visible 1px all-side border (see parseBorderClass). The dashed/sided/
469
+ // numeric forms flow through the root loop below into parseBorderClass.
470
+ if (className === 'border') {
471
+ return { property: 'border', value: `1px ${BORDER_STYLE}`, root: 'border', kind: 'keyword' };
472
+ }
473
+
408
474
  // Leading-`-` negative form (`-mt-4`, `-top-2`, `-translate-y-1/2`, `-rotate-3`): parse the
409
475
  // positive class, then negate its value when the root is negatable. Not a negatable root or a
410
476
  // non-numeric value → unrecognized (don't misread foreign `-foo` classes as utilities).
@@ -480,6 +546,14 @@ function parseRootValue(root: string, rest: string, knownTokens?: ReadonlySet<st
480
546
  return { property, value, root, kind: 'var' };
481
547
  }
482
548
 
549
+ // Border width/style family (`border-2`, `border-b`, `border-t-4`, `border-solid`, …).
550
+ // Placed after the arbitrary `[..]` and var `(--x)` escapes so those win; colors
551
+ // (`border-primary`) return null here and fall through to the color-token branch below.
552
+ if (root === 'border' || BORDER_SIDE_PROPERTY[root]) {
553
+ const border = parseBorderClass(root, rest);
554
+ if (border) return border;
555
+ }
556
+
483
557
  // Named keyword: w-full, m-auto, rounded-full
484
558
  const keyword = keywordValues[root]?.[rest];
485
559
  if (keyword !== undefined) {
@@ -37,6 +37,17 @@ export const getBaseName = (filePath: string): string => {
37
37
  return parts[parts.length - 1] || '';
38
38
  };
39
39
 
40
+ /**
41
+ * Collapse a folder-index route name to its directory route, mirroring Astro's routing:
42
+ * `blog/index` => `blog` (serves `/blog`), while the root `index` and non-index names are
43
+ * returned unchanged. Keep this in sync with the copy in astro's `astroPageProvider.ts`
44
+ * (duplicated across the package boundary to avoid a publish-ordering coupling).
45
+ * @example collapseFolderIndex('blog/index') => 'blog'
46
+ * @example collapseFolderIndex('index') => 'index'
47
+ */
48
+ export const collapseFolderIndex = (name: string): string =>
49
+ name.endsWith('/index') ? name.slice(0, -'/index'.length) : name;
50
+
40
51
  /**
41
52
  * Map page filename to route path
42
53
  * @example mapPageNameToPath('index') => '/'
@@ -201,6 +201,84 @@ describe('Schema Validation', () => {
201
201
  });
202
202
  });
203
203
 
204
+ describe('list prop authoring mistakes surface a clear, prop-named error', () => {
205
+ // Collect every message in a zod error tree (union branches included), so
206
+ // assertions don't depend on which branch zod happens to surface.
207
+ const allMessages = (error: import('zod').ZodError): string[] => {
208
+ const out: string[] = [];
209
+ const walk = (issues: import('zod').ZodIssue[]) => {
210
+ for (const issue of issues) {
211
+ out.push(issue.message);
212
+ if (issue.code === 'invalid_union') {
213
+ for (const ue of (issue as import('zod').ZodInvalidUnionIssue).unionErrors) walk(ue.errors);
214
+ }
215
+ }
216
+ };
217
+ walk(error.issues);
218
+ return out;
219
+ };
220
+
221
+ test('a bare-string `default` with no `itemSchema` is rejected with the list-prop requirement', () => {
222
+ // The single most common first guess. The codec round-trips it and
223
+ // `astro build` renders it, so the editor load path is the only guard.
224
+ const propDef = { type: 'list', default: ['First', 'Second'] };
225
+
226
+ const result = PropDefinitionSchema.safeParse(propDef);
227
+
228
+ expect(result.success).toBe(false);
229
+ if (!result.success) {
230
+ expect(allMessages(result.error).some((m) => m.includes('list prop requires `itemSchema`'))).toBe(true);
231
+ }
232
+ });
233
+
234
+ test('an object-array `default` still requires `itemSchema`', () => {
235
+ const propDef = { type: 'list', default: [{ label: 'First' }] };
236
+ const result = PropDefinitionSchema.safeParse(propDef);
237
+ expect(result.success).toBe(false);
238
+ });
239
+
240
+ test('the correct form (itemSchema + object-array default) passes', () => {
241
+ const propDef = {
242
+ type: 'list',
243
+ itemSchema: { label: { type: 'string', default: 'Item' } },
244
+ default: [{ label: 'First' }, { label: 'Second' }],
245
+ };
246
+ expect(PropDefinitionSchema.safeParse(propDef).success).toBe(true);
247
+ });
248
+
249
+ test('a component file with a bad list prop reports the prop error, NOT the misleading JSONPage refine', () => {
250
+ // Regression for the union-dirty-branch bug: a `{ component }` payload
251
+ // whose list prop is malformed used to surface "JSONPage must have at
252
+ // least one of: meta, components, or root" (the page branch's refine),
253
+ // which names nothing useful. It must surface the list-prop error instead.
254
+ const componentFile = {
255
+ component: {
256
+ interface: { items: { type: 'list', default: ['a', 'b'] } },
257
+ structure: { type: 'node', tag: 'div', children: [] },
258
+ },
259
+ };
260
+
261
+ const result = PageDataSchema.safeParse(componentFile);
262
+
263
+ expect(result.success).toBe(false);
264
+ if (!result.success) {
265
+ const messages = allMessages(result.error);
266
+ expect(messages.some((m) => m.includes('list prop requires `itemSchema`'))).toBe(true);
267
+ expect(messages.some((m) => m.includes('JSONPage must have'))).toBe(false);
268
+ }
269
+ });
270
+
271
+ test('an empty payload (a real page with no fields) still reports the JSONPage refine', () => {
272
+ // The guard only fires on a present `component` key — a genuinely empty
273
+ // page payload must keep its existing refine message.
274
+ const result = PageDataSchema.safeParse({});
275
+ expect(result.success).toBe(false);
276
+ if (!result.success) {
277
+ expect(allMessages(result.error).some((m) => m.includes('JSONPage must have'))).toBe(true);
278
+ }
279
+ });
280
+ });
281
+
204
282
  describe('Missing required fields handled', () => {
205
283
  test('prop definition without default', () => {
206
284
  const propDef = {
@@ -126,10 +126,47 @@ export const ListPropDefinitionSchema = z
126
126
  .passthrough();
127
127
 
128
128
  /**
129
- * Prop definition schema (union of base and list)
130
- * Validates prop definitions from component interfaces
129
+ * Prop definition schema discriminates on `type` instead of a plain
130
+ * `z.union([List, Base])`.
131
+ *
132
+ * Why not a bare union: a `type: "list"` prop with a bad shape (the single most
133
+ * common authoring mistake — a bare-string `default` and/or a missing
134
+ * `itemSchema`, e.g. `{ type: "list", default: ["a", "b"] }`) matches NEITHER
135
+ * union branch (List wants `itemSchema` + an object-array `default`; Base's
136
+ * `type` enum has no `"list"`). The union then reports a noisy pile of
137
+ * cross-branch issues (`type` not in the base enum, `default[0]` expected object,
138
+ * …) — and, worse, when this prop sits inside a component file the outer
139
+ * `PageDataSchema` union surfaces the *page* branch's refine instead (see
140
+ * `JSONPageSchema` / `PageDataSchema` below), so the real error reads
141
+ * "JSONPage must have …root". The codec round-trips it and `astro build`
142
+ * renders it, so the only place it ever surfaces is here.
143
+ *
144
+ * Discriminating on `type` lets a list prop be validated by ONLY
145
+ * `ListPropDefinitionSchema` and collapses its failure into one actionable
146
+ * message that names the requirement, at the prop's own path.
131
147
  */
132
- export const PropDefinitionSchema = z.union([ListPropDefinitionSchema, BasePropDefinitionSchema]);
148
+ export const PropDefinitionSchema = z
149
+ .object({ type: z.string() })
150
+ .passthrough()
151
+ .superRefine((val, ctx) => {
152
+ const branch = val.type === 'list' ? ListPropDefinitionSchema : BasePropDefinitionSchema;
153
+ const result = branch.safeParse(val);
154
+ if (result.success) return;
155
+ if (val.type === 'list') {
156
+ // Collapse the list-prop failure into one actionable message rather than
157
+ // replaying zod's per-field noise. `itemSchema` is required and `default`
158
+ // must be an array of OBJECTS (one `{ field: value }` record per item).
159
+ ctx.addIssue({
160
+ code: z.ZodIssueCode.custom,
161
+ message:
162
+ 'list prop requires `itemSchema` and an object-array `default` ' +
163
+ '(each item is a `{ field: value }` object, not a bare string)',
164
+ });
165
+ return;
166
+ }
167
+ // Non-list props keep their precise per-field errors.
168
+ for (const issue of result.error.issues) ctx.addIssue(issue);
169
+ });
133
170
 
134
171
  /**
135
172
  * Style mapping schema
@@ -722,6 +759,14 @@ export const JSONPageSchema = z
722
759
  // Marks a page whose source is outside the meno-astro dialect (read-only in the editor).
723
760
  // Explicit (not just .passthrough()) so it survives a future tightening of this schema.
724
761
  _unsupported: z.object({ reason: z.string() }).passthrough().optional(),
762
+ // A component payload (`{ component: {...} }`) must NEVER validate as a page.
763
+ // Without this guard the page branch parses structurally (it's `.passthrough()`)
764
+ // and only trips the refine below — so when both branches of `PageDataSchema`
765
+ // fail, zod treats this branch as the "closest" (dirty) one and surfaces
766
+ // "JSONPage must have …root", masking the real component error (e.g. a malformed
767
+ // list prop). Rejecting a present `component` key makes this branch hard-fail on
768
+ // component payloads, so `PageDataSchema` reports the component branch instead.
769
+ component: z.undefined().optional(),
725
770
  })
726
771
  .passthrough()
727
772
  .refine(
@@ -752,8 +797,11 @@ export const PageDataWithComponentSchema = z
752
797
  .passthrough();
753
798
 
754
799
  /**
755
- * Page data schema - union of JSONPage and PageDataWithComponent
756
- * PageDataWithComponent checked first because it's more specific
800
+ * Page data schema - union of JSONPage and PageDataWithComponent.
801
+ * PageDataWithComponent is checked first because it's more specific; the
802
+ * `component: z.undefined()` guard on `JSONPageSchema` keeps a component payload
803
+ * from being mis-reported as a page (see that schema's comment), so a malformed
804
+ * prop surfaces its own error instead of the page-branch refine.
757
805
  */
758
806
  export const PageDataSchema = z.union([PageDataWithComponentSchema, JSONPageSchema]);
759
807
 
@@ -791,6 +839,7 @@ export const CMSFieldDefinitionSchema = z
791
839
  accept: z.string().optional(),
792
840
  collection: z.string().optional(),
793
841
  multiple: z.boolean().optional(),
842
+ editor: z.enum(['basic', 'extended']).optional(), // For 'rich-text' type: selects the render helper (Basic → richText(); Extended → richTextWithComponents())
794
843
  })
795
844
  .passthrough();
796
845
 
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'bun:test';
2
- import { rewriteViewportUnits } from './viewportUnits';
2
+ import { rewriteViewportUnits, rewriteViewportUnitsInStylesheet } from './viewportUnits';
3
3
 
4
4
  describe('rewriteViewportUnits', () => {
5
5
  it('rewrites basic vh values in declarations', () => {
@@ -102,3 +102,36 @@ describe('rewriteViewportUnits', () => {
102
102
  expect(rewriteViewportUnits('top: 0vh;')).toBe('top: calc(var(--design-vh, 1vh) * 0);');
103
103
  });
104
104
  });
105
+
106
+ describe('rewriteViewportUnitsInStylesheet', () => {
107
+ it('preserves an escaped selector carrying a DECIMAL viewport unit', () => {
108
+ // `text-[clamp(56px,9.5vw,150px)]` escapes to `…9\.5vw…`; the value-level
109
+ // rewriter matches `.5vw` after the backslash and corrupts the selector, so
110
+ // the browser drops the rule. Stylesheet-scoped rewrite touches only bodies.
111
+ const rule = '.text-\\[clamp\\(56px\\,9\\.5vw\\,150px\\)\\] { font-size: clamp(56px,9.5vw,150px); }';
112
+ expect(rewriteViewportUnitsInStylesheet(rule)).toBe(
113
+ '.text-\\[clamp\\(56px\\,9\\.5vw\\,150px\\)\\] { font-size: clamp(56px,calc(var(--design-vw, 1vw) * 9.5),150px); }',
114
+ );
115
+ // Rationale guard: the raw value rewriter alone corrupts this selector.
116
+ expect(rewriteViewportUnits(rule).split('{')[0]).toContain('calc(');
117
+ });
118
+
119
+ it('preserves an escaped selector when a viewport unit follows a no-space comma', () => {
120
+ const rule = '.p-\\[80px_clamp\\(20px\\,5vw\\,64px\\)\\] { padding: 80px clamp(20px,5vw,64px); }';
121
+ expect(rewriteViewportUnitsInStylesheet(rule)).toBe(
122
+ '.p-\\[80px_clamp\\(20px\\,5vw\\,64px\\)\\] { padding: 80px clamp(20px,calc(var(--design-vw, 1vw) * 5),64px); }',
123
+ );
124
+ });
125
+
126
+ it('rewrites declaration values inside @media blocks while keeping nested selectors literal', () => {
127
+ const sheet = '@media (max-width: 767px) { .h-\\[50dvh\\] { height: 50dvh; } }';
128
+ expect(rewriteViewportUnitsInStylesheet(sheet)).toBe(
129
+ '@media (max-width: 767px) { .h-\\[50dvh\\] { height: calc(var(--design-dvh, 1dvh) * 50); } }',
130
+ );
131
+ });
132
+
133
+ it('is a no-op on empty / brace-free input', () => {
134
+ expect(rewriteViewportUnitsInStylesheet('')).toBe('');
135
+ expect(rewriteViewportUnitsInStylesheet('.a { color: red; }')).toBe('.a { color: red; }');
136
+ });
137
+ });
@@ -53,6 +53,49 @@ export function rewriteViewportUnits(input: string): string {
53
53
  });
54
54
  }
55
55
 
56
+ /**
57
+ * Apply {@link rewriteViewportUnits} to a whole stylesheet, rewriting ONLY the
58
+ * declaration bodies — never the selectors or at-rule preludes.
59
+ *
60
+ * {@link rewriteViewportUnits} is regex-based and can't reliably tell a
61
+ * declaration value's viewport unit from one buried in an *escaped*
62
+ * arbitrary-value class SELECTOR. The `VIEWPORT_UNIT_RE` lookbehind guards the
63
+ * common selector shapes (`.mh-100vh`, `.min-h-[100svh]`), but a class whose
64
+ * arbitrary value carries a viewport unit after an escaped `.` or `,` slips
65
+ * through — e.g. `text-[clamp(56px,9.5vw,150px)]` escapes to
66
+ * `.text-\[clamp\(56px\,9\.5vw\,150px\)\]`, where the `.5vw` sits right after a
67
+ * backslash and the rewriter injects `calc(...)` INTO the selector. The result
68
+ * is invalid CSS the browser drops entirely, so the rule — and the design-mode
69
+ * viewport pin it carries — silently vanishes and the element renders unstyled.
70
+ *
71
+ * Splitting selector text from declaration bodies at brace boundaries removes
72
+ * the ambiguity: every selector / prelude ends at a `{`, every declaration body
73
+ * ends at a `}`, so only body text is handed to the value rewriter. Correct
74
+ * through `@media` nesting too (a nested rule's selector still ends at its own
75
+ * `{`). Assumes braces only ever delimit blocks — true for the generated
76
+ * utility/interactive/theme sheets (none emit literal `{`/`}` inside a value or
77
+ * string). Mirror of meno-astro's `rewriteViewportUnitsInStylesheet` (keep in
78
+ * sync — see packages/astro/lib/integration/viewportUnits.ts).
79
+ */
80
+ export function rewriteViewportUnitsInStylesheet(css: string): string {
81
+ if (!css) return css;
82
+ let out = '';
83
+ let seg = '';
84
+ for (let i = 0; i < css.length; i++) {
85
+ const ch = css[i];
86
+ if (ch === '{') {
87
+ out += seg + ch; // `seg` is a selector / at-rule prelude — keep it literal
88
+ seg = '';
89
+ } else if (ch === '}') {
90
+ out += rewriteViewportUnits(seg) + ch; // `seg` is a declaration body — rewrite its values
91
+ seg = '';
92
+ } else {
93
+ seg += ch;
94
+ }
95
+ }
96
+ return out + seg; // trailing text outside any block — never a declaration value
97
+ }
98
+
56
99
  /**
57
100
  * Names of the CSS custom properties the runtime sets/clears to switch
58
101
  * between design-mode (stable px) and page-mode (no override, vh = actual
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meno-core",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "meno": "./dist/bin/cli.js"
@@ -23,6 +23,7 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "@emotion/css": "^11.13.5",
26
+ "css-tree": "^3.2.1",
26
27
  "esbuild": "^0.25.0",
27
28
  "isomorphic-dompurify": "^2.32.0",
28
29
  "jsep": "^1.4.0",
@@ -33,6 +34,7 @@
33
34
  "zod": "^3.22.4"
34
35
  },
35
36
  "devDependencies": {
37
+ "@types/css-tree": "^2.3.11",
36
38
  "@types/markdown-it": "^14.1.2",
37
39
  "@types/react": "^18.2.0",
38
40
  "@types/react-dom": "^18.2.0",
package/vite.config.ts CHANGED
@@ -19,7 +19,10 @@ export default defineConfig({
19
19
  'react',
20
20
  'react-dom',
21
21
  'bun',
22
- // Node.js built-ins (server code)
22
+ // Node.js built-ins (server code) — bare and node:-prefixed specifiers.
23
+ // The regex covers all `node:`-prefixed imports (e.g. `node:path`) so they
24
+ // stay external instead of being browser-stubbed by Vite.
25
+ /^node:/,
23
26
  'path',
24
27
  'fs',
25
28
  'fs/promises',