@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.
- package/dist/{chunk-FGYTDBZU.js → chunk-3VK5GQVH.js} +1323 -508
- package/dist/chunk-3VK5GQVH.js.map +1 -0
- package/dist/{chunk-PKDMKMD6.js → chunk-7V3SGN3T.js} +19 -2
- package/dist/chunk-7V3SGN3T.js.map +1 -0
- package/dist/{chunk-7U2GMTVW.js → chunk-IKMPDZYE.js} +17 -4
- package/dist/chunk-IKMPDZYE.js.map +1 -0
- package/dist/{chunk-JMSDBWL2.js → chunk-N2PHBHNZ.js} +2 -2
- package/dist/chunk-OI6N7H4Y.js +24 -0
- package/dist/chunk-OI6N7H4Y.js.map +1 -0
- package/dist/chunk-TCKD2I2B.js +443 -0
- package/dist/chunk-TCKD2I2B.js.map +1 -0
- package/dist/doctor.d.ts +35 -3
- package/dist/doctor.js +96 -10
- package/dist/doctor.js.map +1 -1
- package/dist/editable-array-item-Cko68alJ.d.ts +33 -0
- package/dist/index.d.ts +4 -32
- package/dist/index.js +9 -22
- package/dist/index.js.map +1 -1
- package/dist/integration/index.js +4 -4
- package/dist/markers.d.ts +20 -0
- package/dist/markers.js +23 -0
- package/dist/markers.js.map +1 -0
- package/dist/middleware/index.js +3 -1
- package/dist/middleware/index.js.map +1 -1
- package/dist/richtext/index.d.ts +96 -17
- package/dist/richtext/index.js +23 -1
- package/dist/vite/index.js +3 -3
- package/package.json +9 -5
- package/src/RichText.astro +45 -22
- package/dist/chunk-4L5C226B.js +0 -71
- package/dist/chunk-4L5C226B.js.map +0 -1
- package/dist/chunk-5QZN4IGF.js +0 -71
- package/dist/chunk-5QZN4IGF.js.map +0 -1
- package/dist/chunk-7U2GMTVW.js.map +0 -1
- package/dist/chunk-FGYTDBZU.js.map +0 -1
- package/dist/chunk-K7DTYPRE.js +0 -1348
- package/dist/chunk-K7DTYPRE.js.map +0 -1
- package/dist/chunk-PKDMKMD6.js.map +0 -1
- package/dist/chunk-S2IKJ3QM.js +0 -28
- package/dist/chunk-S2IKJ3QM.js.map +0 -1
- /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-
|
|
4
|
-
import "../chunk-
|
|
5
|
-
import "../chunk-
|
|
6
|
-
import "../chunk-
|
|
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 };
|
package/dist/markers.js
ADDED
|
@@ -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":[]}
|
package/dist/middleware/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
+
injectThemeStyle,
|
|
2
3
|
injectVersionMetaTag,
|
|
3
4
|
injectVisibilityStyle
|
|
4
|
-
} from "../chunk-
|
|
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":"
|
|
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":[]}
|
package/dist/richtext/index.d.ts
CHANGED
|
@@ -24,32 +24,67 @@ import { z } from 'zod';
|
|
|
24
24
|
* (atomic migration; enforced by the spans doctor check).
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
/**
|
|
28
|
-
|
|
29
|
-
|
|
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.
|
|
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.
|
|
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
|
|
49
|
-
* must be a plain object whose `text` is a string and whose
|
|
50
|
-
* present, only contains whitelisted marks.
|
|
51
|
-
*
|
|
52
|
-
*
|
|
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 };
|
package/dist/richtext/index.js
CHANGED
|
@@ -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-
|
|
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
|
package/dist/vite/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
studioAutoWrap
|
|
3
|
-
} from "../chunk-
|
|
4
|
-
import "../chunk-
|
|
5
|
-
import "../chunk-
|
|
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.
|
|
4
|
-
"description": "Build-time instrumentation for Saastro Studio
|
|
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.
|
|
86
|
-
"vite": "^7.
|
|
87
|
-
"vitest": "^3.2.
|
|
89
|
+
"typescript": "^5.9.3",
|
|
90
|
+
"vite": "^7.3.6",
|
|
91
|
+
"vitest": "^3.2.7"
|
|
88
92
|
}
|
|
89
93
|
}
|
package/src/RichText.astro
CHANGED
|
@@ -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?}]
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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 {
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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.
|
|
60
|
-
<span
|
|
61
|
-
{s.
|
|
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
|
),
|
package/dist/chunk-4L5C226B.js
DELETED
|
@@ -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":[]}
|
package/dist/chunk-5QZN4IGF.js
DELETED
|
@@ -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":[]}
|