@saastro/studio 0.8.0 → 0.10.0

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 (41) hide show
  1. package/dist/{chunk-FGYTDBZU.js → chunk-3VK5GQVH.js} +1323 -508
  2. package/dist/chunk-3VK5GQVH.js.map +1 -0
  3. package/dist/{chunk-PKDMKMD6.js → chunk-7V3SGN3T.js} +19 -2
  4. package/dist/chunk-7V3SGN3T.js.map +1 -0
  5. package/dist/{chunk-7U2GMTVW.js → chunk-IKMPDZYE.js} +17 -4
  6. package/dist/chunk-IKMPDZYE.js.map +1 -0
  7. package/dist/{chunk-JMSDBWL2.js → chunk-N2PHBHNZ.js} +2 -2
  8. package/dist/chunk-OI6N7H4Y.js +24 -0
  9. package/dist/chunk-OI6N7H4Y.js.map +1 -0
  10. package/dist/chunk-TCKD2I2B.js +443 -0
  11. package/dist/chunk-TCKD2I2B.js.map +1 -0
  12. package/dist/doctor.d.ts +35 -3
  13. package/dist/doctor.js +96 -10
  14. package/dist/doctor.js.map +1 -1
  15. package/dist/editable-array-item-Cko68alJ.d.ts +33 -0
  16. package/dist/index.d.ts +4 -32
  17. package/dist/index.js +9 -22
  18. package/dist/index.js.map +1 -1
  19. package/dist/integration/index.js +4 -4
  20. package/dist/markers.d.ts +20 -0
  21. package/dist/markers.js +23 -0
  22. package/dist/markers.js.map +1 -0
  23. package/dist/middleware/index.js +3 -1
  24. package/dist/middleware/index.js.map +1 -1
  25. package/dist/richtext/index.d.ts +96 -17
  26. package/dist/richtext/index.js +23 -1
  27. package/dist/vite/index.js +3 -3
  28. package/package.json +9 -5
  29. package/src/RichText.astro +45 -22
  30. package/dist/chunk-4L5C226B.js +0 -71
  31. package/dist/chunk-4L5C226B.js.map +0 -1
  32. package/dist/chunk-5QZN4IGF.js +0 -71
  33. package/dist/chunk-5QZN4IGF.js.map +0 -1
  34. package/dist/chunk-7U2GMTVW.js.map +0 -1
  35. package/dist/chunk-FGYTDBZU.js.map +0 -1
  36. package/dist/chunk-K7DTYPRE.js +0 -1348
  37. package/dist/chunk-K7DTYPRE.js.map +0 -1
  38. package/dist/chunk-PKDMKMD6.js.map +0 -1
  39. package/dist/chunk-S2IKJ3QM.js +0 -28
  40. package/dist/chunk-S2IKJ3QM.js.map +0 -1
  41. /package/dist/{chunk-JMSDBWL2.js.map → chunk-N2PHBHNZ.js.map} +0 -0
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  saastro
3
- } from "../chunk-JMSDBWL2.js";
4
- import "../chunk-K7DTYPRE.js";
5
- import "../chunk-4L5C226B.js";
6
- import "../chunk-PKDMKMD6.js";
3
+ } from "../chunk-N2PHBHNZ.js";
4
+ import "../chunk-IKMPDZYE.js";
5
+ import "../chunk-TCKD2I2B.js";
6
+ import "../chunk-7V3SGN3T.js";
7
7
  import "../chunk-PZ5AY32C.js";
8
8
  export {
9
9
  saastro as default
@@ -0,0 +1,20 @@
1
+ export { b as editableArrayItem, e as editableSection, a as editableSlot } from './editable-array-item-Cko68alJ.js';
2
+ import { E as EditableAttrs } from './editable-image-B4UdcCKi.js';
3
+ export { e as editableImage } from './editable-image-B4UdcCKi.js';
4
+
5
+ /**
6
+ * Field marker — drop on the element that renders a single editable value
7
+ * inside a marked section (`data-saastro-field="<path>"`). The path is
8
+ * RELATIVE to the section's fieldPrefix and uses bracket notation for array
9
+ * items (`title`, `stats[0].value`).
10
+ *
11
+ * In `.astro` sections the Vite plugin injects these automatically; React
12
+ * islands are never auto-instrumented, so island code writes them via this
13
+ * helper instead of hand-typed attribute strings (greppable, lintable, and
14
+ * the doctor can require it).
15
+ *
16
+ * Returns empty object when the path is falsy — safe to spread unconditionally.
17
+ */
18
+ declare function editableField(fieldPath: string | undefined): EditableAttrs;
19
+
20
+ export { EditableAttrs, editableField };
@@ -0,0 +1,23 @@
1
+ import {
2
+ editableArrayItem,
3
+ editableSection,
4
+ editableSlot
5
+ } from "./chunk-OI6N7H4Y.js";
6
+ import {
7
+ editableImage
8
+ } from "./chunk-FLFH5RVH.js";
9
+ import "./chunk-PZ5AY32C.js";
10
+
11
+ // src/helpers/editable-field.ts
12
+ function editableField(fieldPath) {
13
+ if (!fieldPath) return {};
14
+ return { "data-saastro-field": fieldPath };
15
+ }
16
+ export {
17
+ editableArrayItem,
18
+ editableField,
19
+ editableImage,
20
+ editableSection,
21
+ editableSlot
22
+ };
23
+ //# sourceMappingURL=markers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/helpers/editable-field.ts"],"sourcesContent":["import type { EditableAttrs } from './types.js'\n\n/**\n * Field marker — drop on the element that renders a single editable value\n * inside a marked section (`data-saastro-field=\"<path>\"`). The path is\n * RELATIVE to the section's fieldPrefix and uses bracket notation for array\n * items (`title`, `stats[0].value`).\n *\n * In `.astro` sections the Vite plugin injects these automatically; React\n * islands are never auto-instrumented, so island code writes them via this\n * helper instead of hand-typed attribute strings (greppable, lintable, and\n * the doctor can require it).\n *\n * Returns empty object when the path is falsy — safe to spread unconditionally.\n */\nexport function editableField(fieldPath: string | undefined): EditableAttrs {\n if (!fieldPath) return {}\n return { 'data-saastro-field': fieldPath }\n}\n"],"mappings":";;;;;;;;;;;AAeO,SAAS,cAAc,WAA8C;AAC1E,MAAI,CAAC,UAAW,QAAO,CAAC;AACxB,SAAO,EAAE,sBAAsB,UAAU;AAC3C;","names":[]}
@@ -1,7 +1,8 @@
1
1
  import {
2
+ injectThemeStyle,
2
3
  injectVersionMetaTag,
3
4
  injectVisibilityStyle
4
- } from "../chunk-4L5C226B.js";
5
+ } from "../chunk-TCKD2I2B.js";
5
6
  import "../chunk-PZ5AY32C.js";
6
7
 
7
8
  // src/middleware/index.ts
@@ -13,6 +14,7 @@ var onRequest = async (_context, next) => {
13
14
  const html = await response.text();
14
15
  let patched = injectVersionMetaTag(html);
15
16
  patched = injectVisibilityStyle(patched);
17
+ patched = injectThemeStyle(patched);
16
18
  if (patched === html) {
17
19
  return makeHtmlResponse(html, response);
18
20
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/middleware/index.ts"],"sourcesContent":["/**\n * Astro middleware that injects the Saastro Studio meta tags into every\n * HTML response. Companion to the Vite plugin's `transformIndexHtml`\n * hook, which only fires for static HTML served by Vite's dev server —\n * SSR routes (`output: 'server'` or `'hybrid'`) bypass it entirely.\n *\n * Registered by the integration via\n * `addMiddleware({ entrypoint: '@saastro/studio/middleware', order: 'pre' })`\n * so it runs before any user middleware.\n *\n * Behavior:\n * - Non-HTML responses (CSS, JS, JSON, redirects, images) pass through\n * untouched — we never buffer their bodies.\n * - HTML responses get their `<head>` patched with two meta tags. The\n * patch is idempotent: a response that already has\n * `saastro:studio-version` is returned as-is.\n * - Response status, statusText, and all headers (minus `content-length`\n * and `etag`, which are invalidated by the body rewrite) are\n * preserved.\n */\n\nimport { injectVersionMetaTag } from '../vite/meta-tag.js'\nimport { injectVisibilityStyle } from '../visibility-css.js'\n\ninterface MiddlewareContext {\n request: Request\n url?: URL\n}\n\ntype Next = () => Promise<Response>\n\nconst HTML_RE = /text\\/html/i\n\nexport const onRequest = async (\n _context: MiddlewareContext,\n next: Next,\n): Promise<Response> => {\n const response = await next()\n const contentType = response.headers.get('content-type') ?? ''\n if (!HTML_RE.test(contentType)) return response\n\n const html = await response.text()\n // Layered injection: meta tags first (for the Hub overlay), then the\n // `studio.config.json`-driven visibility CSS. Both helpers are\n // idempotent so a response that already carries either tag is left\n // untouched in that pass.\n let patched = injectVersionMetaTag(html)\n // `injectVisibilityStyle` defaults to `'.'` (resolved relative to\n // the current working directory) when no project root is passed —\n // the middleware can't easily determine Vite's resolved root, but\n // the CWD is the project root in every realistic deployment.\n patched = injectVisibilityStyle(patched)\n if (patched === html) {\n // Nothing changed, but `text()` already consumed the original\n // stream. Re-emit with a fresh body so downstream adapters get a\n // readable response.\n return makeHtmlResponse(html, response)\n }\n return makeHtmlResponse(patched, response)\n}\n\nfunction makeHtmlResponse(body: string, source: Response): Response {\n const headers = new Headers(source.headers)\n // The body length changed (or we re-emitted). The original\n // `content-length` / `etag` are invalid — drop them and let the\n // runtime/proxy recompute or ignore.\n headers.delete('content-length')\n headers.delete('etag')\n return new Response(body, {\n status: source.status,\n statusText: source.statusText,\n headers,\n })\n}\n"],"mappings":";;;;;;;AA+BA,IAAM,UAAU;AAET,IAAM,YAAY,OACvB,UACA,SACsB;AACtB,QAAM,WAAW,MAAM,KAAK;AAC5B,QAAM,cAAc,SAAS,QAAQ,IAAI,cAAc,KAAK;AAC5D,MAAI,CAAC,QAAQ,KAAK,WAAW,EAAG,QAAO;AAEvC,QAAM,OAAO,MAAM,SAAS,KAAK;AAKjC,MAAI,UAAU,qBAAqB,IAAI;AAKvC,YAAU,sBAAsB,OAAO;AACvC,MAAI,YAAY,MAAM;AAIpB,WAAO,iBAAiB,MAAM,QAAQ;AAAA,EACxC;AACA,SAAO,iBAAiB,SAAS,QAAQ;AAC3C;AAEA,SAAS,iBAAiB,MAAc,QAA4B;AAClE,QAAM,UAAU,IAAI,QAAQ,OAAO,OAAO;AAI1C,UAAQ,OAAO,gBAAgB;AAC/B,UAAQ,OAAO,MAAM;AACrB,SAAO,IAAI,SAAS,MAAM;AAAA,IACxB,QAAQ,OAAO;AAAA,IACf,YAAY,OAAO;AAAA,IACnB;AAAA,EACF,CAAC;AACH;","names":[]}
1
+ {"version":3,"sources":["../../src/middleware/index.ts"],"sourcesContent":["/**\n * Astro middleware that injects the Saastro Studio meta tags into every\n * HTML response. Companion to the Vite plugin's `transformIndexHtml`\n * hook, which only fires for static HTML served by Vite's dev server —\n * SSR routes (`output: 'server'` or `'hybrid'`) bypass it entirely.\n *\n * Registered by the integration via\n * `addMiddleware({ entrypoint: '@saastro/studio/middleware', order: 'pre' })`\n * so it runs before any user middleware.\n *\n * Behavior:\n * - Non-HTML responses (CSS, JS, JSON, redirects, images) pass through\n * untouched — we never buffer their bodies.\n * - HTML responses get their `<head>` patched with two meta tags. The\n * patch is idempotent: a response that already has\n * `saastro:studio-version` is returned as-is.\n * - Response status, statusText, and all headers (minus `content-length`\n * and `etag`, which are invalidated by the body rewrite) are\n * preserved.\n */\n\nimport { injectVersionMetaTag } from '../vite/meta-tag.js'\nimport { injectVisibilityStyle } from '../visibility-css.js'\nimport { injectThemeStyle } from '../theme-css.js'\n\ninterface MiddlewareContext {\n request: Request\n url?: URL\n}\n\ntype Next = () => Promise<Response>\n\nconst HTML_RE = /text\\/html/i\n\nexport const onRequest = async (\n _context: MiddlewareContext,\n next: Next,\n): Promise<Response> => {\n const response = await next()\n const contentType = response.headers.get('content-type') ?? ''\n if (!HTML_RE.test(contentType)) return response\n\n const html = await response.text()\n // Layered injection: meta tags first (for the Hub overlay), then the\n // `studio.config.json`-driven visibility CSS. Both helpers are\n // idempotent so a response that already carries either tag is left\n // untouched in that pass.\n let patched = injectVersionMetaTag(html)\n // `injectVisibilityStyle` defaults to `'.'` (resolved relative to\n // the current working directory) when no project root is passed —\n // the middleware can't easily determine Vite's resolved root, but\n // the CWD is the project root in every realistic deployment.\n patched = injectVisibilityStyle(patched)\n // Theme override LAST so its `:root` vars cascade over the bundled CSS.\n patched = injectThemeStyle(patched)\n if (patched === html) {\n // Nothing changed, but `text()` already consumed the original\n // stream. Re-emit with a fresh body so downstream adapters get a\n // readable response.\n return makeHtmlResponse(html, response)\n }\n return makeHtmlResponse(patched, response)\n}\n\nfunction makeHtmlResponse(body: string, source: Response): Response {\n const headers = new Headers(source.headers)\n // The body length changed (or we re-emitted). The original\n // `content-length` / `etag` are invalid — drop them and let the\n // runtime/proxy recompute or ignore.\n headers.delete('content-length')\n headers.delete('etag')\n return new Response(body, {\n status: source.status,\n statusText: source.statusText,\n headers,\n })\n}\n"],"mappings":";;;;;;;;AAgCA,IAAM,UAAU;AAET,IAAM,YAAY,OACvB,UACA,SACsB;AACtB,QAAM,WAAW,MAAM,KAAK;AAC5B,QAAM,cAAc,SAAS,QAAQ,IAAI,cAAc,KAAK;AAC5D,MAAI,CAAC,QAAQ,KAAK,WAAW,EAAG,QAAO;AAEvC,QAAM,OAAO,MAAM,SAAS,KAAK;AAKjC,MAAI,UAAU,qBAAqB,IAAI;AAKvC,YAAU,sBAAsB,OAAO;AAEvC,YAAU,iBAAiB,OAAO;AAClC,MAAI,YAAY,MAAM;AAIpB,WAAO,iBAAiB,MAAM,QAAQ;AAAA,EACxC;AACA,SAAO,iBAAiB,SAAS,QAAQ;AAC3C;AAEA,SAAS,iBAAiB,MAAc,QAA4B;AAClE,QAAM,UAAU,IAAI,QAAQ,OAAO,OAAO;AAI1C,UAAQ,OAAO,gBAAgB;AAC/B,UAAQ,OAAO,MAAM;AACrB,SAAO,IAAI,SAAS,MAAM;AAAA,IACxB,QAAQ,OAAO;AAAA,IACf,YAAY,OAAO;AAAA,IACnB;AAAA,EACF,CAAC;AACH;","names":[]}
@@ -24,32 +24,67 @@ import { z } from 'zod';
24
24
  * (atomic migration; enforced by the spans doctor check).
25
25
  */
26
26
 
27
- /** Closed whitelist. Owner decision 2026-07-06: accent + bold only. */
28
- declare const SPAN_MARKS: readonly ["accent", "bold"];
29
- type SpanMark = (typeof SPAN_MARKS)[number];
27
+ /**
28
+ * Simple (parameterless) marks. Owner decisions 2026-07-06: accent + bold
29
+ * first; italic + tokenized color/size/font added the same day (marks v2).
30
+ */
31
+ declare const SPAN_MARKS: readonly ["accent", "bold", "italic"];
32
+ type SimpleSpanMark = (typeof SPAN_MARKS)[number];
33
+ /**
34
+ * Tokenized marks (marks v2) — the value part is always a SEMANTIC name,
35
+ * never a raw CSS value:
36
+ *
37
+ * c:<token> color — the site's own CSS custom property name (without
38
+ * the leading --), e.g. `c:accent-300`, `c:brand-jcenlo`.
39
+ * Renders as `color: var(--<token>)`, so it resolves against
40
+ * whatever the site's palette declares.
41
+ * s:sm|lg|xl font size — closed 3-step scale relative to the element
42
+ * (0.85em / 1.15em / 1.35em). No arbitrary sizes.
43
+ * f:display|body|code font family — the site's semantic font slots
44
+ * (`var(--font-display)` / `var(--font-body)` / `var(--code)`).
45
+ * Arbitrary families stay theme-editor territory.
46
+ */
47
+ declare const COLOR_MARK_RE: RegExp;
48
+ declare const SIZE_MARK_RE: RegExp;
49
+ declare const FONT_MARK_RE: RegExp;
50
+ type SpanMark = string;
51
+ declare function isValidMark(mark: unknown): mark is SpanMark;
52
+ declare const spanMarkSchema: z.ZodString & z.ZodType<string, string, z.core.$ZodTypeInternals<string, string>>;
30
53
  declare const richSpanSchema: z.ZodObject<{
31
54
  text: z.ZodString;
32
- marks: z.ZodOptional<z.ZodArray<z.ZodEnum<{
33
- accent: "accent";
34
- bold: "bold";
35
- }>>>;
55
+ marks: z.ZodOptional<z.ZodArray<z.ZodString & z.ZodType<string, string, z.core.$ZodTypeInternals<string, string>>>>;
36
56
  }, z.core.$strip>;
37
57
  type RichSpan = z.infer<typeof richSpanSchema>;
38
58
  /** A spans value is a non-empty array of spans. */
39
59
  declare const richSpansValueSchema: z.ZodArray<z.ZodObject<{
40
60
  text: z.ZodString;
41
- marks: z.ZodOptional<z.ZodArray<z.ZodEnum<{
42
- accent: "accent";
43
- bold: "bold";
44
- }>>>;
61
+ marks: z.ZodOptional<z.ZodArray<z.ZodString & z.ZodType<string, string, z.core.$ZodTypeInternals<string, string>>>>;
45
62
  }, z.core.$strip>>;
46
63
  type RichSpansValue = z.infer<typeof richSpansValueSchema>;
47
64
  /**
48
- * Structural guard for the spans shape. Deliberately strict: every element
49
- * must be a plain object whose `text` is a string and whose `marks`, if
50
- * present, only contains whitelisted marks. Anything else (generic i18n
51
- * arrays of strings/objects, mixed arrays) is NOT spans and must keep
52
- * flowing through the generic array code paths.
65
+ * Structural guard for the spans GRAMMAR only. Deliberately strict: every
66
+ * element must be a plain object whose `text` is a string and whose
67
+ * `marks`, if present, only contains whitelisted marks. It does NOT decide
68
+ * whether the value IS rich spans — see `isSpansValue`. Editors use this
69
+ * to keep round-tripping a value mid-edit (a run whose last mark was just
70
+ * toggled off is still grammar-valid), and message validators use it to
71
+ * accept an unmark action crossing the postMessage boundary.
72
+ */
73
+ declare function isSpansShape(value: unknown): value is RichSpansValue;
74
+ /** True when at least one span carries a non-empty mark list. */
75
+ declare function spansHasMarks(value: RichSpansValue): boolean;
76
+ /**
77
+ * "Spans iff marked" — the canonical rich-spans detector (audit E2 #28).
78
+ *
79
+ * A value is rich spans ONLY when it satisfies the grammar AND at least one
80
+ * span actually carries a mark. A `[{text}, {text}]` array with no marks
81
+ * anywhere is indistinguishable from a legitimate generic content array
82
+ * (testimonials/quotes/ticker items with a single `text` key), so it must
83
+ * keep flowing through the generic array code paths — the stega encoder
84
+ * registers its items per-path, the panel renders a FieldArray, etc.
85
+ * Writers enforce the complementary invariant: a spans value whose last
86
+ * mark is removed is normalised back to the plain string
87
+ * (`spansToPlainText`) before staging, so unmarked spans never persist.
53
88
  */
54
89
  declare function isSpansValue(value: unknown): value is RichSpansValue;
55
90
  /** Flatten a spans value to its visible plain text (marks dropped). */
@@ -59,5 +94,49 @@ declare function spansToPlainText(value: RichSpansValue): string;
59
94
  * (summaries, stega visible text, graceful degradation).
60
95
  */
61
96
  declare function leafToPlainText(value: string | RichSpansValue): string;
97
+ declare const SIZE_MARK_EM: Record<string, string>;
98
+ declare const FONT_MARK_VAR: Record<string, string>;
99
+ /**
100
+ * CSS declarations for the tokenized marks of a span (color/size/font).
101
+ * Returns '' when none apply. `accent`/`bold`/`italic` are NOT covered
102
+ * here — they map to class/element (`.ac`, <strong>, <em>) renderer-side.
103
+ */
104
+ declare function marksToInlineStyle(marks: readonly SpanMark[] | undefined): string;
105
+ /** Serialized form for the lossless round-trip attribute (data-saastro-marks). */
106
+ declare function marksToAttr(marks: readonly SpanMark[] | undefined): string;
107
+ /** Parse the round-trip attribute back to a validated, sorted mark list. */
108
+ declare function marksFromAttr(attr: string | null | undefined): SpanMark[];
109
+ /**
110
+ * Exclusivity group of a mark. Tokenized marks are EXCLUSIVE within their
111
+ * group — one color, one size, one font per run — and `accent` shares the
112
+ * color group with `c:<token>` (both paint the run's text color).
113
+ */
114
+ declare function markGroupPrefix(mark: SpanMark): 'color' | 'size' | 'font' | null;
115
+ /** True when both marks belong to the same (non-null) exclusivity group. */
116
+ declare function sameMarkGroup(a: SpanMark, b: SpanMark): boolean;
117
+ /** Normalise: sort marks, merge adjacent equal-mark spans, drop empties, never []. */
118
+ declare function normalizeSpans(spans: readonly RichSpan[]): RichSpansValue;
119
+ /**
120
+ * Toggle `mark` across the [start, end) character range of the flattened
121
+ * text — the familiar bold-button semantics: if every character in the
122
+ * range already carries the mark it is removed, otherwise it is added.
123
+ * Adding a grouped mark (color/size/font — see `markGroupPrefix`) evicts
124
+ * its group siblings from the range so a run never accumulates e.g.
125
+ * `accent` + `c:<token>` (audit E2 #38/#40 — this eviction is canonical;
126
+ * the overlay's `toggleSpansMark` mirrors it).
127
+ */
128
+ declare function toggleMarkOnRange(spans: RichSpansValue, start: number, end: number, mark: SpanMark): RichSpansValue;
129
+ /**
130
+ * Deep-flatten every spans-SHAPED leaf of a translations tree to its
131
+ * concatenated plain text. This is the universal degradation a site's
132
+ * `getTranslations` applies so every `{t.x.y}` interpolation keeps
133
+ * rendering strings — never `[object Object]` (Astro) or a hooks crash
134
+ * (React islands). Rich rendering opts in per call site via <RichText>
135
+ * fed from the UN-flattened tree (`getRichTranslations`).
136
+ *
137
+ * Uses `isSpansShape` (structure only): unmarked spans-shaped arrays
138
+ * flatten too — content-wise they ARE plain text.
139
+ */
140
+ declare function flattenSpansDeep<T>(value: T): T;
62
141
 
63
- export { type RichSpan, type RichSpansValue, SPAN_MARKS, type SpanMark, isSpansValue, leafToPlainText, richSpanSchema, richSpansValueSchema, spansToPlainText };
142
+ export { COLOR_MARK_RE, FONT_MARK_RE, FONT_MARK_VAR, type RichSpan, type RichSpansValue, SIZE_MARK_EM, SIZE_MARK_RE, SPAN_MARKS, type SimpleSpanMark, type SpanMark, flattenSpansDeep, isSpansShape, isSpansValue, isValidMark, leafToPlainText, markGroupPrefix, marksFromAttr, marksToAttr, marksToInlineStyle, normalizeSpans, richSpanSchema, richSpansValueSchema, sameMarkGroup, spanMarkSchema, spansHasMarks, spansToPlainText, toggleMarkOnRange };
@@ -1,16 +1,38 @@
1
1
  import {
2
+ COLOR_MARK_RE,
3
+ FONT_MARK_RE,
4
+ FONT_MARK_VAR,
5
+ SIZE_MARK_EM,
6
+ SIZE_MARK_RE,
2
7
  SPAN_MARKS,
8
+ flattenSpansDeep,
9
+ isSpansShape,
3
10
  isSpansValue,
11
+ isValidMark,
4
12
  leafToPlainText,
13
+ marksFromAttr,
14
+ marksToAttr,
15
+ marksToInlineStyle,
5
16
  richSpanSchema,
6
17
  richSpansValueSchema,
7
18
  spansToPlainText
8
- } from "../chunk-FGYTDBZU.js";
19
+ } from "../chunk-3VK5GQVH.js";
9
20
  import "../chunk-PZ5AY32C.js";
10
21
  export {
22
+ COLOR_MARK_RE,
23
+ FONT_MARK_RE,
24
+ FONT_MARK_VAR,
25
+ SIZE_MARK_EM,
26
+ SIZE_MARK_RE,
11
27
  SPAN_MARKS,
28
+ flattenSpansDeep,
29
+ isSpansShape,
12
30
  isSpansValue,
31
+ isValidMark,
13
32
  leafToPlainText,
33
+ marksFromAttr,
34
+ marksToAttr,
35
+ marksToInlineStyle,
14
36
  richSpanSchema,
15
37
  richSpansValueSchema,
16
38
  spansToPlainText
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  studioAutoWrap
3
- } from "../chunk-K7DTYPRE.js";
4
- import "../chunk-4L5C226B.js";
5
- import "../chunk-PKDMKMD6.js";
3
+ } from "../chunk-IKMPDZYE.js";
4
+ import "../chunk-TCKD2I2B.js";
5
+ import "../chunk-7V3SGN3T.js";
6
6
  import "../chunk-PZ5AY32C.js";
7
7
  export {
8
8
  studioAutoWrap as default
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@saastro/studio",
3
- "version": "0.8.0",
4
- "description": "Build-time instrumentation for Saastro Studio — Vite plugin + editable helpers for Astro sites managed by Saastro Hub",
3
+ "version": "0.10.0",
4
+ "description": "Build-time instrumentation for Saastro Studio \u2014 Vite plugin + editable helpers for Astro sites managed by Saastro Hub",
5
5
  "license": "MIT",
6
6
  "author": "SAASTRO",
7
7
  "keywords": [
@@ -50,6 +50,10 @@
50
50
  "types": "./dist/richtext/index.d.ts",
51
51
  "import": "./dist/richtext/index.js"
52
52
  },
53
+ "./markers": {
54
+ "types": "./dist/markers.d.ts",
55
+ "import": "./dist/markers.js"
56
+ },
53
57
  "./Img.astro": "./src/Img.astro",
54
58
  "./RichText.astro": "./src/RichText.astro"
55
59
  },
@@ -82,8 +86,8 @@
82
86
  "devDependencies": {
83
87
  "@saastro/studio-core": "workspace:*",
84
88
  "tsup": "^8.5.1",
85
- "typescript": "^5.8.3",
86
- "vite": "^7.0.0",
87
- "vitest": "^3.2.4"
89
+ "typescript": "^5.9.3",
90
+ "vite": "^7.3.6",
91
+ "vitest": "^3.2.7"
88
92
  }
89
93
  }
@@ -7,23 +7,29 @@
7
7
  * <h2 data-saastro-field="title"><RichText value={t.hero.title} /></h2>
8
8
  *
9
9
  * - Plain string → emitted as-is (text interpolation, auto-escaped).
10
- * - Rich spans (`[{text, marks?}]`, whitelist accent|bold) → each span
11
- * renders through Astro's template interpolation (never set:html):
12
- * accent → <span class="ac" data-saastro-mark="accent"> (the theme's
13
- * semantic accent class; override via `accentClass`)
14
- * bold → <strong>
10
+ * - Rich spans (`[{text, marks?}]`) → per span:
11
+ * accent → class `ac` (the theme's semantic accent class;
12
+ * override via `accentClass`)
13
+ * bold / italic → nested <strong> / <em>
14
+ * c:<token> → `color: var(--<token>)` (site palette token)
15
+ * s:sm|lg|xl → relative font-size step
16
+ * f:display|body|code → the site's semantic font-slot vars
17
+ * Marked spans carry `data-saastro-marks` so the Studio canvas can
18
+ * serialize the element back to the exact spans value (lossless
19
+ * round-trip). All presentation derives from the SHARED mapping in
20
+ * @saastro/studio/richtext — token names are regex-validated, raw CSS
21
+ * values never travel in content, and set:html is never used.
15
22
  * - Anything else (a malformed value) degrades to its concatenated plain
16
23
  * text when possible, never `[object Object]`.
17
24
  *
18
- * Put the `data-saastro-field` marker on the WRAPPING element (as above):
19
- * the Studio overlay re-renders the wrapper's children on live edits and
20
- * recognises its own inline marks.
21
- *
22
25
  * Ships with @saastro/studio (consumed via the `./RichText.astro` export).
23
- * Helpers come from the bundled `@saastro/studio/richtext` subpath — same
24
- * distribution pattern as Img.astro + `@saastro/studio/image`.
25
26
  */
26
- import { isSpansValue, leafToPlainText } from '@saastro/studio/richtext'
27
+ import {
28
+ isSpansValue,
29
+ leafToPlainText,
30
+ marksToAttr,
31
+ marksToInlineStyle,
32
+ } from '@saastro/studio/richtext'
27
33
 
28
34
  interface Props {
29
35
  /** The i18n leaf: plain string or rich spans array. */
@@ -35,11 +41,18 @@ interface Props {
35
41
  const { value, accentClass = 'ac' } = Astro.props
36
42
 
37
43
  const spans = isSpansValue(value)
38
- ? value.map(s => ({
39
- text: s.text,
40
- bold: (s.marks ?? []).includes('bold'),
41
- accent: (s.marks ?? []).includes('accent'),
42
- }))
44
+ ? value.map(s => {
45
+ const marks = s.marks ?? []
46
+ const style = marksToInlineStyle(marks)
47
+ return {
48
+ text: s.text,
49
+ bold: marks.includes('bold'),
50
+ italic: marks.includes('italic'),
51
+ accent: marks.includes('accent'),
52
+ style: style.length > 0 ? style : undefined,
53
+ attr: marksToAttr(marks),
54
+ }
55
+ })
43
56
  : null
44
57
 
45
58
  const plain = spans
@@ -56,12 +69,22 @@ const plain = spans
56
69
  {
57
70
  spans
58
71
  ? spans.map(s =>
59
- s.accent ? (
60
- <span class={accentClass} data-saastro-mark="accent">
61
- {s.bold ? <strong>{s.text}</strong> : s.text}
72
+ s.attr ? (
73
+ <span
74
+ class={s.accent ? accentClass : undefined}
75
+ style={s.style}
76
+ data-saastro-marks={s.attr}
77
+ >
78
+ {s.bold && s.italic ? (
79
+ <strong><em>{s.text}</em></strong>
80
+ ) : s.bold ? (
81
+ <strong>{s.text}</strong>
82
+ ) : s.italic ? (
83
+ <em>{s.text}</em>
84
+ ) : (
85
+ s.text
86
+ )}
62
87
  </span>
63
- ) : s.bold ? (
64
- <strong>{s.text}</strong>
65
88
  ) : (
66
89
  s.text
67
90
  ),
@@ -1,71 +0,0 @@
1
- // src/vite/meta-tag.ts
2
- var STUDIO_PROTOCOL_VERSION = "2";
3
- var PACKAGE_VERSION = "0.8.0".length > 0 ? "0.8.0" : "0.0.0+unknown";
4
- var META_TAGS = [
5
- `<meta name="saastro:studio-version" content="${STUDIO_PROTOCOL_VERSION}">`,
6
- `<meta name="saastro:studio-package" content="${PACKAGE_VERSION}">`
7
- ].join("\n ");
8
- function injectVersionMetaTag(html) {
9
- if (html.includes("saastro:studio-version")) return html;
10
- return html.replace(/<head([^>]*)>/i, `<head$1>
11
- ${META_TAGS}`);
12
- }
13
-
14
- // src/visibility-css.ts
15
- var CONFIG_FILENAME = "studio.config.json";
16
- var STYLE_ID = "saastro-studio-hidden";
17
- function readVisibilityConfigSync(projectRoot = ".") {
18
- try {
19
- const req = globalThis.require;
20
- if (typeof req !== "function") return {};
21
- const fs = req("node:fs");
22
- const path = req("node:path");
23
- const raw = fs.readFileSync(path.resolve(projectRoot, CONFIG_FILENAME), "utf8");
24
- if (!raw.trim()) return {};
25
- const parsed = JSON.parse(raw);
26
- if (parsed && typeof parsed === "object") return parsed;
27
- return {};
28
- } catch {
29
- return {};
30
- }
31
- }
32
- function collectHiddenFieldPrefixes(config) {
33
- const set = /* @__PURE__ */ new Set();
34
- for (const prefixes of Object.values(config.hidden ?? {})) {
35
- if (!Array.isArray(prefixes)) continue;
36
- for (const p of prefixes) {
37
- if (typeof p === "string" && p.length > 0) set.add(p);
38
- }
39
- }
40
- return Array.from(set).sort();
41
- }
42
- function buildVisibilityStyleTag(config) {
43
- const prefixes = collectHiddenFieldPrefixes(config);
44
- if (prefixes.length === 0) return "";
45
- const rules = prefixes.map((fp) => {
46
- const safe = `sec:${fp}`.replace(/["\\]/g, "\\$&");
47
- return `[data-saastro="${safe}"]{display:none !important}`;
48
- }).join("");
49
- return `<style id="${STYLE_ID}">${rules}</style>`;
50
- }
51
- function injectVisibilityStyle(html, _projectRoot = ".") {
52
- if (html.includes(`id="${STYLE_ID}"`)) return html;
53
- let tag = "";
54
- try {
55
- if (typeof __SAASTRO_STUDIO_HIDDEN_TAG__ === "string") {
56
- tag = __SAASTRO_STUDIO_HIDDEN_TAG__;
57
- }
58
- } catch {
59
- }
60
- if (!tag) return html;
61
- return html.replace(/<head([^>]*)>/i, `<head$1>
62
- ${tag}`);
63
- }
64
-
65
- export {
66
- injectVersionMetaTag,
67
- readVisibilityConfigSync,
68
- buildVisibilityStyleTag,
69
- injectVisibilityStyle
70
- };
71
- //# sourceMappingURL=chunk-4L5C226B.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/vite/meta-tag.ts","../src/visibility-css.ts"],"sourcesContent":["/**\n * Injects two meta tags into the HTML <head>:\n * - <meta name=\"saastro:studio-version\" content=\"2\"> — protocol version\n * - <meta name=\"saastro:studio-package\" content=\"X.Y.Z\"> — npm package version\n *\n * B8 — spec v5.3 §2 bumps the postMessage protocol number to \"2\". The\n * `studio-version` field reads as the PROTOCOL version (the contract Hub\n * relies on when wiring overlay <-> host messages), not the npm package\n * version of `@saastro/studio`. The package version still ships in a\n * separate `saastro:studio-package` tag for telemetry.\n *\n * The package version comes from a tsup `define` substitution\n * (`__SAASTRO_STUDIO_VERSION__`) injected at build time. The previous\n * implementation used `createRequire(import.meta.url)` to read\n * `package.json` at runtime, but tsup's chunked output places dist\n * files at `dist/chunk-XXX.js` and `../../package.json` resolves\n * OUTSIDE the package — the require always threw and the catch fell\n * back to a hardcoded `'0.1.0'`. Build-time substitution is bullet-\n * proof regardless of how tsup chunks the bundle.\n *\n * This is separate from the auto-wrap transform hook —\n * transformIndexHtml runs on rendered HTML output, not on .astro\n * source files.\n */\n\n/**\n * postMessage protocol version. Bumped to \"2\" in spec v5.3 to add\n * `studio:sections:enumerate` + `studio:select:section` exchanges.\n * Increment when adding/changing the iframe⇄host wire format.\n */\nconst STUDIO_PROTOCOL_VERSION = '2'\n\ndeclare const __SAASTRO_STUDIO_VERSION__: string | undefined\n\nconst PACKAGE_VERSION =\n typeof __SAASTRO_STUDIO_VERSION__ === 'string' && __SAASTRO_STUDIO_VERSION__.length > 0\n ? __SAASTRO_STUDIO_VERSION__\n : '0.0.0+unknown'\n\nconst META_TAGS = [\n `<meta name=\"saastro:studio-version\" content=\"${STUDIO_PROTOCOL_VERSION}\">`,\n `<meta name=\"saastro:studio-package\" content=\"${PACKAGE_VERSION}\">`,\n].join('\\n ')\n\n/**\n * Inject the two studio meta tags into the HTML head. Idempotent —\n * skips injection if the protocol-version tag is already present\n * (matches both the new shape and the legacy single-tag shape, so\n * customer sites with old metadata aren't double-tagged on rebuild).\n */\nexport function injectVersionMetaTag(html: string): string {\n if (html.includes('saastro:studio-version')) return html\n return html.replace(/<head([^>]*)>/i, `<head$1>\\n ${META_TAGS}`)\n}\n","/**\n * Read `studio.config.json` from a project root and build the\n * `<style>` block that hides sections flagged as `hidden`.\n *\n * The config shape is the one written by the Studio Hub at publish time:\n *\n * {\n * \"hidden\": {\n * \"<pageFile>\": [\"<fieldPrefix>\", ...]\n * }\n * }\n *\n * The CSS targets the `data-saastro=\"sec:<fieldPrefix>\"` markers the\n * auto-wrap plugin emits — so hiding is \"exactly the section root,\n * nothing more\". Selectors are flattened across all `pageFile` entries:\n * Studio's `fieldPrefix` naming convention is `<sectionName>.<pageName>`\n * which keeps them unique site-wide, so this loses no precision.\n *\n * Both the Vite `transformIndexHtml` hook (static-build sites) and the\n * Astro middleware (SSR routes) call into the same helpers so the\n * injected CSS is identical regardless of render mode.\n */\n\n// **CRITICAL — DO NOT add static node:fs / node:path imports here.**\n// This module is loaded by `@saastro/studio/middleware`, which runs\n// inside Cloudflare Workers at SSR boot. CF Workers does not support\n// `node:fs` even with the `nodejs_compat` flag (sync fs I/O is\n// fundamentally incompatible with the worker model) — a top-level\n// `import { readFileSync } from 'node:fs'` causes the module to fail\n// to load with `Error: No such module \"node:fs\"`, which breaks the\n// entire middleware chain and makes Astro return 500 on every SSR\n// route.\n//\n// The `node:fs` fallback (used by `astro dev` in Node) loads via\n// dynamic import inside `readVisibilityConfig`, wrapped in try/catch.\n// Workers never reaches that path because the build-time `define`\n// (`__SAASTRO_STUDIO_HIDDEN_TAG__`) short-circuits the readFileSync\n// call entirely.\n\nconst CONFIG_FILENAME = 'studio.config.json'\nconst STYLE_ID = 'saastro-studio-hidden'\n\n/**\n * Compile-time constant injected by the `@saastro/studio` Vite plugin\n * (see `config()` hook in `vite/index.ts`). esbuild substitutes the\n * identifier at customer-bundle time with a JSON string of the\n * full `<style>` tag (or an empty string when nothing is hidden), so\n * the middleware doesn't need filesystem access at runtime — a\n * hard requirement on Cloudflare Workers, where `node:fs` is a no-op.\n *\n * Declared as a free variable on the global scope so esbuild's\n * `define` substitution recognises it as a literal-replacement\n * target. When the plugin isn't installed, the identifier remains\n * undefined and the middleware falls back to the readFileSync path\n * (works in node-based deployments).\n */\ndeclare const __SAASTRO_STUDIO_HIDDEN_TAG__: string | undefined\n\ninterface StudioConfig {\n hidden?: Record<string, string[]>\n}\n\n/**\n * Read studio.config.json from `projectRoot`. Returns an empty config\n * if the file is missing or malformed — a publish that hasn't written\n * the config yet behaves like \"nothing hidden\".\n *\n * Defaulting `projectRoot` to `'.'` lets callers omit it when they\n * cannot easily determine the resolved Vite root — `node:path.resolve`\n * resolves relative to the current working directory, which is the\n * project root in every realistic deployment of the middleware.\n */\n/**\n * Build-time-only helper. Imports `node:fs` lazily so this module\n * stays loadable on CF Workers (where `node:fs` is not available).\n * The Vite plugin (`vite/index.ts`, runs in Node) calls this during\n * `config()` to inline the visibility tag as a `define` constant.\n * The runtime middleware (`middleware/index.ts`, runs in Workers)\n * never calls it — it reads the inlined define directly.\n *\n * Returns `{}` when the file isn't there, isn't readable, or when\n * `node:fs` simply isn't available (e.g. bundled into a non-Node\n * runtime that somehow reached this branch).\n */\nexport async function readVisibilityConfig(\n projectRoot: string = '.',\n): Promise<StudioConfig> {\n try {\n const [{ readFileSync }, { resolve }] = await Promise.all([\n import('node:fs'),\n import('node:path'),\n ])\n const raw = readFileSync(resolve(projectRoot, CONFIG_FILENAME), 'utf8')\n if (!raw.trim()) return {}\n const parsed = JSON.parse(raw) as unknown\n if (parsed && typeof parsed === 'object') return parsed as StudioConfig\n return {}\n } catch {\n return {}\n }\n}\n\n/**\n * Synchronous variant kept for back-compat with callers that can't\n * await (e.g. test fixtures, legacy plugin hooks). Same lazy node:fs\n * import via `require` — works in Node CJS contexts, returns `{}`\n * on CF Workers / pure-ESM environments where `require` is undefined.\n *\n * The dynamic `globalThis.require` lookup avoids esbuild bundling the\n * module reference, which means CF Workers never throws\n * \"No such module 'node:fs'\" at import time.\n */\nexport function readVisibilityConfigSync(projectRoot: string = '.'): StudioConfig {\n try {\n const req = (globalThis as { require?: (id: string) => unknown }).require\n if (typeof req !== 'function') return {}\n const fs = req('node:fs') as { readFileSync: (p: string, e: string) => string }\n const path = req('node:path') as { resolve: (...parts: string[]) => string }\n const raw = fs.readFileSync(path.resolve(projectRoot, CONFIG_FILENAME), 'utf8')\n if (!raw.trim()) return {}\n const parsed = JSON.parse(raw) as unknown\n if (parsed && typeof parsed === 'object') return parsed as StudioConfig\n return {}\n } catch {\n return {}\n }\n}\n\n/**\n * Collect every fieldPrefix flagged as hidden across all pageFile\n * entries. Deduplicates so a prefix that appears under multiple\n * pageFiles only emits one selector.\n */\nexport function collectHiddenFieldPrefixes(config: StudioConfig): string[] {\n const set = new Set<string>()\n for (const prefixes of Object.values(config.hidden ?? {})) {\n if (!Array.isArray(prefixes)) continue\n for (const p of prefixes) {\n if (typeof p === 'string' && p.length > 0) set.add(p)\n }\n }\n return Array.from(set).sort()\n}\n\n/**\n * Build the `<style>` block. Returns an empty string when there's\n * nothing to hide so callers can no-op without conditional branching.\n */\nexport function buildVisibilityStyleTag(config: StudioConfig): string {\n const prefixes = collectHiddenFieldPrefixes(config)\n if (prefixes.length === 0) return ''\n const rules = prefixes\n .map(fp => {\n // CSS attribute-selector grammar: only `\\` and `\"` need escaping\n // inside `[attr=\"...\"]`. fieldPrefix is conventionally\n // `[a-zA-Z0-9._-]+` so this is belt-and-braces.\n const safe = `sec:${fp}`.replace(/[\"\\\\]/g, '\\\\$&')\n return `[data-saastro=\"${safe}\"]{display:none !important}`\n })\n .join('')\n return `<style id=\"${STYLE_ID}\">${rules}</style>`\n}\n\n/**\n * Inject the visibility `<style>` into an HTML document's `<head>`.\n * Idempotent: a page that already carries a `${STYLE_ID}` style is\n * returned unchanged.\n */\nexport function injectVisibilityStyle(html: string, _projectRoot: string = '.'): string {\n if (html.includes(`id=\"${STYLE_ID}\"`)) return html\n // Prefer the build-time-injected tag when the customer's Vite build\n // populated `__SAASTRO_STUDIO_HIDDEN_TAG__` via the plugin's `config`\n // hook. This is the only path that works on Cloudflare Workers, where\n // `node:fs` is a no-op. The readFileSync fallback handles node-based\n // deployments (or local `astro dev` before the plugin's define lands).\n let tag = ''\n try {\n if (typeof __SAASTRO_STUDIO_HIDDEN_TAG__ === 'string') {\n tag = __SAASTRO_STUDIO_HIDDEN_TAG__\n }\n } catch {\n // ReferenceError swallowed — identifier wasn't substituted.\n }\n // No `readFileSync` fallback in this runtime path — that branch\n // only worked on Node and breaks CF Workers (`node:fs` import\n // throws \"No such module\"). When the build-time define isn't\n // populated (e.g. someone uses the middleware without the Vite\n // plugin), the visibility CSS just isn't injected — graceful\n // degradation, the meta tag still works.\n if (!tag) return html\n return html.replace(/<head([^>]*)>/i, `<head$1>\\n ${tag}`)\n}\n"],"mappings":";AA8BA,IAAM,0BAA0B;AAIhC,IAAM,kBAC8C,QAA2B,SAAS,IAClF,UACA;AAEN,IAAM,YAAY;AAAA,EAChB,gDAAgD,uBAAuB;AAAA,EACvE,gDAAgD,eAAe;AACjE,EAAE,KAAK,MAAM;AAQN,SAAS,qBAAqB,MAAsB;AACzD,MAAI,KAAK,SAAS,wBAAwB,EAAG,QAAO;AACpD,SAAO,KAAK,QAAQ,kBAAkB;AAAA,IAAe,SAAS,EAAE;AAClE;;;ACdA,IAAM,kBAAkB;AACxB,IAAM,WAAW;AAwEV,SAAS,yBAAyB,cAAsB,KAAmB;AAChF,MAAI;AACF,UAAM,MAAO,WAAqD;AAClE,QAAI,OAAO,QAAQ,WAAY,QAAO,CAAC;AACvC,UAAM,KAAK,IAAI,SAAS;AACxB,UAAM,OAAO,IAAI,WAAW;AAC5B,UAAM,MAAM,GAAG,aAAa,KAAK,QAAQ,aAAa,eAAe,GAAG,MAAM;AAC9E,QAAI,CAAC,IAAI,KAAK,EAAG,QAAO,CAAC;AACzB,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,QAAI,UAAU,OAAO,WAAW,SAAU,QAAO;AACjD,WAAO,CAAC;AAAA,EACV,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAOO,SAAS,2BAA2B,QAAgC;AACzE,QAAM,MAAM,oBAAI,IAAY;AAC5B,aAAW,YAAY,OAAO,OAAO,OAAO,UAAU,CAAC,CAAC,GAAG;AACzD,QAAI,CAAC,MAAM,QAAQ,QAAQ,EAAG;AAC9B,eAAW,KAAK,UAAU;AACxB,UAAI,OAAO,MAAM,YAAY,EAAE,SAAS,EAAG,KAAI,IAAI,CAAC;AAAA,IACtD;AAAA,EACF;AACA,SAAO,MAAM,KAAK,GAAG,EAAE,KAAK;AAC9B;AAMO,SAAS,wBAAwB,QAA8B;AACpE,QAAM,WAAW,2BAA2B,MAAM;AAClD,MAAI,SAAS,WAAW,EAAG,QAAO;AAClC,QAAM,QAAQ,SACX,IAAI,QAAM;AAIT,UAAM,OAAO,OAAO,EAAE,GAAG,QAAQ,UAAU,MAAM;AACjD,WAAO,kBAAkB,IAAI;AAAA,EAC/B,CAAC,EACA,KAAK,EAAE;AACV,SAAO,cAAc,QAAQ,KAAK,KAAK;AACzC;AAOO,SAAS,sBAAsB,MAAc,eAAuB,KAAa;AACtF,MAAI,KAAK,SAAS,OAAO,QAAQ,GAAG,EAAG,QAAO;AAM9C,MAAI,MAAM;AACV,MAAI;AACF,QAAI,OAAO,kCAAkC,UAAU;AACrD,YAAM;AAAA,IACR;AAAA,EACF,QAAQ;AAAA,EAER;AAOA,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO,KAAK,QAAQ,kBAAkB;AAAA,IAAe,GAAG,EAAE;AAC5D;","names":[]}
@@ -1,71 +0,0 @@
1
- // src/vite/meta-tag.ts
2
- var STUDIO_PROTOCOL_VERSION = "2";
3
- var PACKAGE_VERSION = "0.7.1".length > 0 ? "0.7.1" : "0.0.0+unknown";
4
- var META_TAGS = [
5
- `<meta name="saastro:studio-version" content="${STUDIO_PROTOCOL_VERSION}">`,
6
- `<meta name="saastro:studio-package" content="${PACKAGE_VERSION}">`
7
- ].join("\n ");
8
- function injectVersionMetaTag(html) {
9
- if (html.includes("saastro:studio-version")) return html;
10
- return html.replace(/<head([^>]*)>/i, `<head$1>
11
- ${META_TAGS}`);
12
- }
13
-
14
- // src/visibility-css.ts
15
- var CONFIG_FILENAME = "studio.config.json";
16
- var STYLE_ID = "saastro-studio-hidden";
17
- function readVisibilityConfigSync(projectRoot = ".") {
18
- try {
19
- const req = globalThis.require;
20
- if (typeof req !== "function") return {};
21
- const fs = req("node:fs");
22
- const path = req("node:path");
23
- const raw = fs.readFileSync(path.resolve(projectRoot, CONFIG_FILENAME), "utf8");
24
- if (!raw.trim()) return {};
25
- const parsed = JSON.parse(raw);
26
- if (parsed && typeof parsed === "object") return parsed;
27
- return {};
28
- } catch {
29
- return {};
30
- }
31
- }
32
- function collectHiddenFieldPrefixes(config) {
33
- const set = /* @__PURE__ */ new Set();
34
- for (const prefixes of Object.values(config.hidden ?? {})) {
35
- if (!Array.isArray(prefixes)) continue;
36
- for (const p of prefixes) {
37
- if (typeof p === "string" && p.length > 0) set.add(p);
38
- }
39
- }
40
- return Array.from(set).sort();
41
- }
42
- function buildVisibilityStyleTag(config) {
43
- const prefixes = collectHiddenFieldPrefixes(config);
44
- if (prefixes.length === 0) return "";
45
- const rules = prefixes.map((fp) => {
46
- const safe = `sec:${fp}`.replace(/["\\]/g, "\\$&");
47
- return `[data-saastro="${safe}"]{display:none !important}`;
48
- }).join("");
49
- return `<style id="${STYLE_ID}">${rules}</style>`;
50
- }
51
- function injectVisibilityStyle(html, _projectRoot = ".") {
52
- if (html.includes(`id="${STYLE_ID}"`)) return html;
53
- let tag = "";
54
- try {
55
- if (typeof __SAASTRO_STUDIO_HIDDEN_TAG__ === "string") {
56
- tag = __SAASTRO_STUDIO_HIDDEN_TAG__;
57
- }
58
- } catch {
59
- }
60
- if (!tag) return html;
61
- return html.replace(/<head([^>]*)>/i, `<head$1>
62
- ${tag}`);
63
- }
64
-
65
- export {
66
- injectVersionMetaTag,
67
- readVisibilityConfigSync,
68
- buildVisibilityStyleTag,
69
- injectVisibilityStyle
70
- };
71
- //# sourceMappingURL=chunk-5QZN4IGF.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/vite/meta-tag.ts","../src/visibility-css.ts"],"sourcesContent":["/**\n * Injects two meta tags into the HTML <head>:\n * - <meta name=\"saastro:studio-version\" content=\"2\"> — protocol version\n * - <meta name=\"saastro:studio-package\" content=\"X.Y.Z\"> — npm package version\n *\n * B8 — spec v5.3 §2 bumps the postMessage protocol number to \"2\". The\n * `studio-version` field reads as the PROTOCOL version (the contract Hub\n * relies on when wiring overlay <-> host messages), not the npm package\n * version of `@saastro/studio`. The package version still ships in a\n * separate `saastro:studio-package` tag for telemetry.\n *\n * The package version comes from a tsup `define` substitution\n * (`__SAASTRO_STUDIO_VERSION__`) injected at build time. The previous\n * implementation used `createRequire(import.meta.url)` to read\n * `package.json` at runtime, but tsup's chunked output places dist\n * files at `dist/chunk-XXX.js` and `../../package.json` resolves\n * OUTSIDE the package — the require always threw and the catch fell\n * back to a hardcoded `'0.1.0'`. Build-time substitution is bullet-\n * proof regardless of how tsup chunks the bundle.\n *\n * This is separate from the auto-wrap transform hook —\n * transformIndexHtml runs on rendered HTML output, not on .astro\n * source files.\n */\n\n/**\n * postMessage protocol version. Bumped to \"2\" in spec v5.3 to add\n * `studio:sections:enumerate` + `studio:select:section` exchanges.\n * Increment when adding/changing the iframe⇄host wire format.\n */\nconst STUDIO_PROTOCOL_VERSION = '2'\n\ndeclare const __SAASTRO_STUDIO_VERSION__: string | undefined\n\nconst PACKAGE_VERSION =\n typeof __SAASTRO_STUDIO_VERSION__ === 'string' && __SAASTRO_STUDIO_VERSION__.length > 0\n ? __SAASTRO_STUDIO_VERSION__\n : '0.0.0+unknown'\n\nconst META_TAGS = [\n `<meta name=\"saastro:studio-version\" content=\"${STUDIO_PROTOCOL_VERSION}\">`,\n `<meta name=\"saastro:studio-package\" content=\"${PACKAGE_VERSION}\">`,\n].join('\\n ')\n\n/**\n * Inject the two studio meta tags into the HTML head. Idempotent —\n * skips injection if the protocol-version tag is already present\n * (matches both the new shape and the legacy single-tag shape, so\n * customer sites with old metadata aren't double-tagged on rebuild).\n */\nexport function injectVersionMetaTag(html: string): string {\n if (html.includes('saastro:studio-version')) return html\n return html.replace(/<head([^>]*)>/i, `<head$1>\\n ${META_TAGS}`)\n}\n","/**\n * Read `studio.config.json` from a project root and build the\n * `<style>` block that hides sections flagged as `hidden`.\n *\n * The config shape is the one written by the Studio Hub at publish time:\n *\n * {\n * \"hidden\": {\n * \"<pageFile>\": [\"<fieldPrefix>\", ...]\n * }\n * }\n *\n * The CSS targets the `data-saastro=\"sec:<fieldPrefix>\"` markers the\n * auto-wrap plugin emits — so hiding is \"exactly the section root,\n * nothing more\". Selectors are flattened across all `pageFile` entries:\n * Studio's `fieldPrefix` naming convention is `<sectionName>.<pageName>`\n * which keeps them unique site-wide, so this loses no precision.\n *\n * Both the Vite `transformIndexHtml` hook (static-build sites) and the\n * Astro middleware (SSR routes) call into the same helpers so the\n * injected CSS is identical regardless of render mode.\n */\n\n// **CRITICAL — DO NOT add static node:fs / node:path imports here.**\n// This module is loaded by `@saastro/studio/middleware`, which runs\n// inside Cloudflare Workers at SSR boot. CF Workers does not support\n// `node:fs` even with the `nodejs_compat` flag (sync fs I/O is\n// fundamentally incompatible with the worker model) — a top-level\n// `import { readFileSync } from 'node:fs'` causes the module to fail\n// to load with `Error: No such module \"node:fs\"`, which breaks the\n// entire middleware chain and makes Astro return 500 on every SSR\n// route.\n//\n// The `node:fs` fallback (used by `astro dev` in Node) loads via\n// dynamic import inside `readVisibilityConfig`, wrapped in try/catch.\n// Workers never reaches that path because the build-time `define`\n// (`__SAASTRO_STUDIO_HIDDEN_TAG__`) short-circuits the readFileSync\n// call entirely.\n\nconst CONFIG_FILENAME = 'studio.config.json'\nconst STYLE_ID = 'saastro-studio-hidden'\n\n/**\n * Compile-time constant injected by the `@saastro/studio` Vite plugin\n * (see `config()` hook in `vite/index.ts`). esbuild substitutes the\n * identifier at customer-bundle time with a JSON string of the\n * full `<style>` tag (or an empty string when nothing is hidden), so\n * the middleware doesn't need filesystem access at runtime — a\n * hard requirement on Cloudflare Workers, where `node:fs` is a no-op.\n *\n * Declared as a free variable on the global scope so esbuild's\n * `define` substitution recognises it as a literal-replacement\n * target. When the plugin isn't installed, the identifier remains\n * undefined and the middleware falls back to the readFileSync path\n * (works in node-based deployments).\n */\ndeclare const __SAASTRO_STUDIO_HIDDEN_TAG__: string | undefined\n\ninterface StudioConfig {\n hidden?: Record<string, string[]>\n}\n\n/**\n * Read studio.config.json from `projectRoot`. Returns an empty config\n * if the file is missing or malformed — a publish that hasn't written\n * the config yet behaves like \"nothing hidden\".\n *\n * Defaulting `projectRoot` to `'.'` lets callers omit it when they\n * cannot easily determine the resolved Vite root — `node:path.resolve`\n * resolves relative to the current working directory, which is the\n * project root in every realistic deployment of the middleware.\n */\n/**\n * Build-time-only helper. Imports `node:fs` lazily so this module\n * stays loadable on CF Workers (where `node:fs` is not available).\n * The Vite plugin (`vite/index.ts`, runs in Node) calls this during\n * `config()` to inline the visibility tag as a `define` constant.\n * The runtime middleware (`middleware/index.ts`, runs in Workers)\n * never calls it — it reads the inlined define directly.\n *\n * Returns `{}` when the file isn't there, isn't readable, or when\n * `node:fs` simply isn't available (e.g. bundled into a non-Node\n * runtime that somehow reached this branch).\n */\nexport async function readVisibilityConfig(\n projectRoot: string = '.',\n): Promise<StudioConfig> {\n try {\n const [{ readFileSync }, { resolve }] = await Promise.all([\n import('node:fs'),\n import('node:path'),\n ])\n const raw = readFileSync(resolve(projectRoot, CONFIG_FILENAME), 'utf8')\n if (!raw.trim()) return {}\n const parsed = JSON.parse(raw) as unknown\n if (parsed && typeof parsed === 'object') return parsed as StudioConfig\n return {}\n } catch {\n return {}\n }\n}\n\n/**\n * Synchronous variant kept for back-compat with callers that can't\n * await (e.g. test fixtures, legacy plugin hooks). Same lazy node:fs\n * import via `require` — works in Node CJS contexts, returns `{}`\n * on CF Workers / pure-ESM environments where `require` is undefined.\n *\n * The dynamic `globalThis.require` lookup avoids esbuild bundling the\n * module reference, which means CF Workers never throws\n * \"No such module 'node:fs'\" at import time.\n */\nexport function readVisibilityConfigSync(projectRoot: string = '.'): StudioConfig {\n try {\n const req = (globalThis as { require?: (id: string) => unknown }).require\n if (typeof req !== 'function') return {}\n const fs = req('node:fs') as { readFileSync: (p: string, e: string) => string }\n const path = req('node:path') as { resolve: (...parts: string[]) => string }\n const raw = fs.readFileSync(path.resolve(projectRoot, CONFIG_FILENAME), 'utf8')\n if (!raw.trim()) return {}\n const parsed = JSON.parse(raw) as unknown\n if (parsed && typeof parsed === 'object') return parsed as StudioConfig\n return {}\n } catch {\n return {}\n }\n}\n\n/**\n * Collect every fieldPrefix flagged as hidden across all pageFile\n * entries. Deduplicates so a prefix that appears under multiple\n * pageFiles only emits one selector.\n */\nexport function collectHiddenFieldPrefixes(config: StudioConfig): string[] {\n const set = new Set<string>()\n for (const prefixes of Object.values(config.hidden ?? {})) {\n if (!Array.isArray(prefixes)) continue\n for (const p of prefixes) {\n if (typeof p === 'string' && p.length > 0) set.add(p)\n }\n }\n return Array.from(set).sort()\n}\n\n/**\n * Build the `<style>` block. Returns an empty string when there's\n * nothing to hide so callers can no-op without conditional branching.\n */\nexport function buildVisibilityStyleTag(config: StudioConfig): string {\n const prefixes = collectHiddenFieldPrefixes(config)\n if (prefixes.length === 0) return ''\n const rules = prefixes\n .map(fp => {\n // CSS attribute-selector grammar: only `\\` and `\"` need escaping\n // inside `[attr=\"...\"]`. fieldPrefix is conventionally\n // `[a-zA-Z0-9._-]+` so this is belt-and-braces.\n const safe = `sec:${fp}`.replace(/[\"\\\\]/g, '\\\\$&')\n return `[data-saastro=\"${safe}\"]{display:none !important}`\n })\n .join('')\n return `<style id=\"${STYLE_ID}\">${rules}</style>`\n}\n\n/**\n * Inject the visibility `<style>` into an HTML document's `<head>`.\n * Idempotent: a page that already carries a `${STYLE_ID}` style is\n * returned unchanged.\n */\nexport function injectVisibilityStyle(html: string, _projectRoot: string = '.'): string {\n if (html.includes(`id=\"${STYLE_ID}\"`)) return html\n // Prefer the build-time-injected tag when the customer's Vite build\n // populated `__SAASTRO_STUDIO_HIDDEN_TAG__` via the plugin's `config`\n // hook. This is the only path that works on Cloudflare Workers, where\n // `node:fs` is a no-op. The readFileSync fallback handles node-based\n // deployments (or local `astro dev` before the plugin's define lands).\n let tag = ''\n try {\n if (typeof __SAASTRO_STUDIO_HIDDEN_TAG__ === 'string') {\n tag = __SAASTRO_STUDIO_HIDDEN_TAG__\n }\n } catch {\n // ReferenceError swallowed — identifier wasn't substituted.\n }\n // No `readFileSync` fallback in this runtime path — that branch\n // only worked on Node and breaks CF Workers (`node:fs` import\n // throws \"No such module\"). When the build-time define isn't\n // populated (e.g. someone uses the middleware without the Vite\n // plugin), the visibility CSS just isn't injected — graceful\n // degradation, the meta tag still works.\n if (!tag) return html\n return html.replace(/<head([^>]*)>/i, `<head$1>\\n ${tag}`)\n}\n"],"mappings":";AA8BA,IAAM,0BAA0B;AAIhC,IAAM,kBAC8C,QAA2B,SAAS,IAClF,UACA;AAEN,IAAM,YAAY;AAAA,EAChB,gDAAgD,uBAAuB;AAAA,EACvE,gDAAgD,eAAe;AACjE,EAAE,KAAK,MAAM;AAQN,SAAS,qBAAqB,MAAsB;AACzD,MAAI,KAAK,SAAS,wBAAwB,EAAG,QAAO;AACpD,SAAO,KAAK,QAAQ,kBAAkB;AAAA,IAAe,SAAS,EAAE;AAClE;;;ACdA,IAAM,kBAAkB;AACxB,IAAM,WAAW;AAwEV,SAAS,yBAAyB,cAAsB,KAAmB;AAChF,MAAI;AACF,UAAM,MAAO,WAAqD;AAClE,QAAI,OAAO,QAAQ,WAAY,QAAO,CAAC;AACvC,UAAM,KAAK,IAAI,SAAS;AACxB,UAAM,OAAO,IAAI,WAAW;AAC5B,UAAM,MAAM,GAAG,aAAa,KAAK,QAAQ,aAAa,eAAe,GAAG,MAAM;AAC9E,QAAI,CAAC,IAAI,KAAK,EAAG,QAAO,CAAC;AACzB,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,QAAI,UAAU,OAAO,WAAW,SAAU,QAAO;AACjD,WAAO,CAAC;AAAA,EACV,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAOO,SAAS,2BAA2B,QAAgC;AACzE,QAAM,MAAM,oBAAI,IAAY;AAC5B,aAAW,YAAY,OAAO,OAAO,OAAO,UAAU,CAAC,CAAC,GAAG;AACzD,QAAI,CAAC,MAAM,QAAQ,QAAQ,EAAG;AAC9B,eAAW,KAAK,UAAU;AACxB,UAAI,OAAO,MAAM,YAAY,EAAE,SAAS,EAAG,KAAI,IAAI,CAAC;AAAA,IACtD;AAAA,EACF;AACA,SAAO,MAAM,KAAK,GAAG,EAAE,KAAK;AAC9B;AAMO,SAAS,wBAAwB,QAA8B;AACpE,QAAM,WAAW,2BAA2B,MAAM;AAClD,MAAI,SAAS,WAAW,EAAG,QAAO;AAClC,QAAM,QAAQ,SACX,IAAI,QAAM;AAIT,UAAM,OAAO,OAAO,EAAE,GAAG,QAAQ,UAAU,MAAM;AACjD,WAAO,kBAAkB,IAAI;AAAA,EAC/B,CAAC,EACA,KAAK,EAAE;AACV,SAAO,cAAc,QAAQ,KAAK,KAAK;AACzC;AAOO,SAAS,sBAAsB,MAAc,eAAuB,KAAa;AACtF,MAAI,KAAK,SAAS,OAAO,QAAQ,GAAG,EAAG,QAAO;AAM9C,MAAI,MAAM;AACV,MAAI;AACF,QAAI,OAAO,kCAAkC,UAAU;AACrD,YAAM;AAAA,IACR;AAAA,EACF,QAAQ;AAAA,EAER;AAOA,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO,KAAK,QAAQ,kBAAkB;AAAA,IAAe,GAAG,EAAE;AAC5D;","names":[]}