@saastro/studio 0.1.7 → 0.3.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.
@@ -0,0 +1,62 @@
1
+ // src/vite/meta-tag.ts
2
+ var STUDIO_PROTOCOL_VERSION = "2";
3
+ var PACKAGE_VERSION = "0.3.0".length > 0 ? "0.3.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
+ import { readFileSync } from "fs";
16
+ import { resolve } from "path";
17
+ var CONFIG_FILENAME = "studio.config.json";
18
+ var STYLE_ID = "saastro-studio-hidden";
19
+ function readVisibilityConfig(projectRoot = ".") {
20
+ try {
21
+ const raw = readFileSync(resolve(projectRoot, CONFIG_FILENAME), "utf8");
22
+ if (!raw.trim()) return {};
23
+ const parsed = JSON.parse(raw);
24
+ if (parsed && typeof parsed === "object") return parsed;
25
+ return {};
26
+ } catch {
27
+ return {};
28
+ }
29
+ }
30
+ function collectHiddenFieldPrefixes(config) {
31
+ const set = /* @__PURE__ */ new Set();
32
+ for (const prefixes of Object.values(config.hidden ?? {})) {
33
+ if (!Array.isArray(prefixes)) continue;
34
+ for (const p of prefixes) {
35
+ if (typeof p === "string" && p.length > 0) set.add(p);
36
+ }
37
+ }
38
+ return Array.from(set).sort();
39
+ }
40
+ function buildVisibilityStyleTag(config) {
41
+ const prefixes = collectHiddenFieldPrefixes(config);
42
+ if (prefixes.length === 0) return "";
43
+ const rules = prefixes.map((fp) => {
44
+ const safe = `sec:${fp}`.replace(/["\\]/g, "\\$&");
45
+ return `[data-saastro="${safe}"]{display:none !important}`;
46
+ }).join("");
47
+ return `<style id="${STYLE_ID}">${rules}</style>`;
48
+ }
49
+ function injectVisibilityStyle(html, projectRoot = ".") {
50
+ if (html.includes(`id="${STYLE_ID}"`)) return html;
51
+ const config = readVisibilityConfig(projectRoot);
52
+ const tag = buildVisibilityStyleTag(config);
53
+ if (!tag) return html;
54
+ return html.replace(/<head([^>]*)>/i, `<head$1>
55
+ ${tag}`);
56
+ }
57
+
58
+ export {
59
+ injectVersionMetaTag,
60
+ injectVisibilityStyle
61
+ };
62
+ //# sourceMappingURL=chunk-D7L6LF5V.js.map
@@ -0,0 +1 @@
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\nimport { readFileSync } from 'node:fs'\nimport { resolve } from 'node:path'\n\nconst CONFIG_FILENAME = 'studio.config.json'\nconst STYLE_ID = 'saastro-studio-hidden'\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 */\nexport function readVisibilityConfig(projectRoot: string = '.'): StudioConfig {\n try {\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 * 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 const config = readVisibilityConfig(projectRoot)\n const tag = buildVisibilityStyleTag(config)\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;;;AC9BA,SAAS,oBAAoB;AAC7B,SAAS,eAAe;AAExB,IAAM,kBAAkB;AACxB,IAAM,WAAW;AAgBV,SAAS,qBAAqB,cAAsB,KAAmB;AAC5E,MAAI;AACF,UAAM,MAAM,aAAa,QAAQ,aAAa,eAAe,GAAG,MAAM;AACtE,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,cAAsB,KAAa;AACrF,MAAI,KAAK,SAAS,OAAO,QAAQ,GAAG,EAAG,QAAO;AAC9C,QAAM,SAAS,qBAAqB,WAAW;AAC/C,QAAM,MAAM,wBAAwB,MAAM;AAC1C,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO,KAAK,QAAQ,kBAAkB;AAAA,IAAe,GAAG,EAAE;AAC5D;","names":[]}
@@ -1,33 +1,11 @@
1
1
  import {
2
- injectVersionMetaTag
3
- } from "./chunk-3LBSAZCN.js";
2
+ injectVersionMetaTag,
3
+ injectVisibilityStyle
4
+ } from "./chunk-D7L6LF5V.js";
4
5
 
5
6
  // src/vite/index.ts
6
7
  import { promises as fs } from "fs";
7
8
 
8
- // src/vite/auto-wrap.ts
9
- var FRONTMATTER_RE = /^\s*---\s*\n([\s\S]*?)\n---\s*\n/;
10
- var SAASTRO_ATTR_RE = /data-saastro/;
11
- var FIELD_PREFIX_REFERENCE_RE = /\bfieldPrefix\b/;
12
- var FIRST_OPEN_TAG_RE = /<([a-zA-Z][a-zA-Z0-9]*)\b([^<>]*?)\s*(\/?)>/;
13
- var INJECTION = ' {...(fieldPrefix ? { "data-saastro": `sec:${fieldPrefix}`, draggable: "true" } : {})}';
14
- function transformAstroSource(source) {
15
- const fm = FRONTMATTER_RE.exec(source);
16
- if (!fm) return source;
17
- if (!FIELD_PREFIX_REFERENCE_RE.test(fm[1])) return source;
18
- const template = source.slice(fm[0].length);
19
- const tagMatch = FIRST_OPEN_TAG_RE.exec(template);
20
- if (!tagMatch) return source;
21
- const tagName = tagMatch[1];
22
- if (tagName[0] !== tagName[0].toLowerCase()) return source;
23
- const existingAttrs = tagMatch[2];
24
- if (SAASTRO_ATTR_RE.test(existingAttrs)) return source;
25
- const selfClose = tagMatch[3];
26
- const replacement = `<${tagName}${existingAttrs}${INJECTION}${selfClose ? " " + selfClose : ""}>`;
27
- const templateInjected = template.slice(0, tagMatch.index) + replacement + template.slice(tagMatch.index + tagMatch[0].length);
28
- return source.slice(0, fm[0].length) + templateInjected;
29
- }
30
-
31
9
  // ../studio-core/dist/component-schema/index.js
32
10
  import {
33
11
  Project
@@ -47,11 +25,135 @@ var STUDIO_DIRECTIVE_KINDS = /* @__PURE__ */ new Set([
47
25
  var STUDIO_DIRECTIVE_RE = /@studio:([A-Za-z][A-Za-z0-9-]*)/g;
48
26
  var IMAGE_NAME_HINTS = /(^|_)(image|img|avatar|logo|icon|thumbnail|photo|picture|cover|background)($|_)/i;
49
27
  var IMAGE_SUFFIX_HINTS = /_(url|src)$/i;
50
- var FRONTMATTER_RE2 = /^\s*---\s*\n([\s\S]*?)\n---/;
28
+ var FRONTMATTER_RE = /^\s*---\s*\n([\s\S]*?)\n---/;
51
29
  function extractFrontmatter(astroSource) {
52
- const m = FRONTMATTER_RE2.exec(astroSource);
30
+ const m = FRONTMATTER_RE.exec(astroSource);
53
31
  return m ? m[1] ?? "" : null;
54
32
  }
33
+ function extractPropsTypeBlock(tsxSource) {
34
+ const interfaceRe = /\b(?:export\s+)?interface\s+([A-Za-z_$][\w$]*)\s*\{/g;
35
+ const typeRe = /\b(?:export\s+)?type\s+([A-Za-z_$][\w$]*)\s*=\s*\{/g;
36
+ let match;
37
+ while ((match = interfaceRe.exec(tsxSource)) !== null) {
38
+ const name = match[1];
39
+ if (name === "Props" || name.endsWith("Props")) {
40
+ const braceOpenIdx = tsxSource.indexOf("{", match.index);
41
+ if (braceOpenIdx === -1) continue;
42
+ const close = readBalancedBrace(tsxSource, braceOpenIdx);
43
+ if (close === null) continue;
44
+ const block = tsxSource.slice(match.index, close + 1);
45
+ return normalizeInterfaceName(block, name);
46
+ }
47
+ }
48
+ while ((match = typeRe.exec(tsxSource)) !== null) {
49
+ const name = match[1];
50
+ if (name === "Props" || name.endsWith("Props")) {
51
+ const braceOpenIdx = tsxSource.indexOf("{", match.index);
52
+ if (braceOpenIdx === -1) continue;
53
+ const close = readBalancedBrace(tsxSource, braceOpenIdx);
54
+ if (close === null) continue;
55
+ const block = tsxSource.slice(match.index, close + 1);
56
+ return normalizeTypeAliasName(block, name);
57
+ }
58
+ }
59
+ return null;
60
+ }
61
+ function normalizeInterfaceName(block, name) {
62
+ if (name === "Props") return block;
63
+ return block.replace(/^(\s*(?:export\s+)?interface\s+)[A-Za-z_$][\w$]*/, "$1Props");
64
+ }
65
+ function normalizeTypeAliasName(block, name) {
66
+ if (name === "Props") return block;
67
+ return block.replace(/^(\s*(?:export\s+)?type\s+)[A-Za-z_$][\w$]*/, "$1Props");
68
+ }
69
+ function readBalancedBrace(s, open) {
70
+ if (s[open] !== "{") return null;
71
+ let depth = 0;
72
+ let i = open;
73
+ while (i < s.length) {
74
+ const ch = s[i];
75
+ if (ch === '"' || ch === "'" || ch === "`") {
76
+ i = skipStringLiteral(s, i) + 1;
77
+ continue;
78
+ }
79
+ if (ch === "/" && s[i + 1] === "/") {
80
+ const eol = s.indexOf("\n", i + 2);
81
+ i = eol === -1 ? s.length : eol + 1;
82
+ continue;
83
+ }
84
+ if (ch === "/" && s[i + 1] === "*") {
85
+ const end = s.indexOf("*/", i + 2);
86
+ i = end === -1 ? s.length : end + 2;
87
+ continue;
88
+ }
89
+ if (ch === "{") depth++;
90
+ else if (ch === "}") {
91
+ depth--;
92
+ if (depth === 0) return i;
93
+ }
94
+ i++;
95
+ }
96
+ return null;
97
+ }
98
+ function skipStringLiteral(s, start) {
99
+ const quote = s[start];
100
+ let i = start + 1;
101
+ while (i < s.length) {
102
+ const ch = s[i];
103
+ if (ch === "\\") {
104
+ i += 2;
105
+ continue;
106
+ }
107
+ if (quote === "`" && ch === "$" && s[i + 1] === "{") {
108
+ const close = readBalancedBrace(s, i + 1);
109
+ if (close === null) return s.length - 1;
110
+ i = close + 1;
111
+ continue;
112
+ }
113
+ if (ch === quote) return i;
114
+ i++;
115
+ }
116
+ return s.length - 1;
117
+ }
118
+ function extractAllTypeBlocks(tsxSource) {
119
+ const blocks = [];
120
+ const seen = /* @__PURE__ */ new Set();
121
+ const decl = /\b(?:export\s+)?(?:interface|type)\s+[A-Za-z_$][\w$]*/g;
122
+ let m;
123
+ while ((m = decl.exec(tsxSource)) !== null) {
124
+ const start = m.index;
125
+ if (seen.has(start)) continue;
126
+ seen.add(start);
127
+ const isType = /\btype\b/.test(m[0]);
128
+ const braceOpen = tsxSource.indexOf("{", start);
129
+ if (braceOpen < 0) continue;
130
+ const between = tsxSource.slice(start + m[0].length, braceOpen);
131
+ if (!/^[\s=]*(?:extends\s+[^{]+)?$/.test(between)) continue;
132
+ const close = readBalancedBrace(tsxSource, braceOpen);
133
+ if (close === null) continue;
134
+ let block = tsxSource.slice(start, close + 1);
135
+ if (isType) {
136
+ if (!block.trimEnd().endsWith(";")) block = block + ";";
137
+ }
138
+ blocks.push(block);
139
+ decl.lastIndex = close + 1;
140
+ }
141
+ return blocks;
142
+ }
143
+ function extractComponentSchemaFromTsx(tsxSource, options = {}) {
144
+ const propsBlock = extractPropsTypeBlock(tsxSource);
145
+ if (!propsBlock) return null;
146
+ const allBlocks = extractAllTypeBlocks(tsxSource);
147
+ const siblings = allBlocks.filter((b) => !looksLikeProps(b));
148
+ const synthetic = `---
149
+ ` + [...siblings, propsBlock].join("\n\n") + `
150
+ ---
151
+ `;
152
+ return extractComponentSchema(synthetic, options);
153
+ }
154
+ function looksLikeProps(block) {
155
+ return /^\s*(?:export\s+)?(?:interface|type)\s+(?:Props\b|[A-Za-z_$][\w$]*Props\b)/.test(block);
156
+ }
55
157
  function extractComponentSchema(astroSource, options = {}) {
56
158
  const frontmatter = extractFrontmatter(astroSource);
57
159
  if (frontmatter == null) return null;
@@ -385,6 +487,29 @@ function parseLiteral(rhs) {
385
487
  return trimmed;
386
488
  }
387
489
 
490
+ // src/vite/auto-wrap.ts
491
+ var FRONTMATTER_RE2 = /^\s*---\s*\n([\s\S]*?)\n---\s*\n/;
492
+ var SAASTRO_ATTR_RE = /data-saastro/;
493
+ var FIELD_PREFIX_REFERENCE_RE = /\bfieldPrefix\b/;
494
+ var FIRST_OPEN_TAG_RE = /<([a-zA-Z][a-zA-Z0-9]*)\b([^<>]*?)\s*(\/?)>/;
495
+ var INJECTION = ' {...(fieldPrefix ? { "data-saastro": `sec:${fieldPrefix}`, draggable: "true" } : {})}';
496
+ function transformAstroSource(source) {
497
+ const fm = FRONTMATTER_RE2.exec(source);
498
+ if (!fm) return source;
499
+ if (!FIELD_PREFIX_REFERENCE_RE.test(fm[1])) return source;
500
+ const template = source.slice(fm[0].length);
501
+ const tagMatch = FIRST_OPEN_TAG_RE.exec(template);
502
+ if (!tagMatch) return source;
503
+ const tagName = tagMatch[1];
504
+ if (tagName[0] !== tagName[0].toLowerCase()) return source;
505
+ const existingAttrs = tagMatch[2];
506
+ if (SAASTRO_ATTR_RE.test(existingAttrs)) return source;
507
+ const selfClose = tagMatch[3];
508
+ const replacement = `<${tagName}${existingAttrs}${INJECTION}${selfClose ? " " + selfClose : ""}>`;
509
+ const templateInjected = template.slice(0, tagMatch.index) + replacement + template.slice(tagMatch.index + tagMatch[0].length);
510
+ return source.slice(0, fm[0].length) + templateInjected;
511
+ }
512
+
388
513
  // src/vite/extract-props-schema.ts
389
514
  var CACHE_MAX = 500;
390
515
  var cache = /* @__PURE__ */ new Map();
@@ -502,6 +627,11 @@ function instrumentFields(source, schema, _options = {}) {
502
627
  const transformed = transformTemplate(head, schema.fields);
503
628
  return source.slice(0, templateStart) + transformed + tail;
504
629
  }
630
+ function instrumentJsxFields(source, schema, _options = {}) {
631
+ if (!schema.fields.length) return source;
632
+ if (!/<[A-Za-z]/.test(source)) return source;
633
+ return transformTemplate(source, schema.fields);
634
+ }
505
635
  function transformTemplate(template, fields) {
506
636
  let out = template;
507
637
  for (const field of fields) {
@@ -761,14 +891,81 @@ function stampArrayItem(jsx, fieldName, paramName, subFields) {
761
891
  return out;
762
892
  }
763
893
  function stampOuterElement(jsx, valueExpr) {
764
- const m = /<([A-Za-z][A-Za-z0-9]*)\b([^<>]*?)(\/?)>/.exec(jsx);
765
- if (!m) return jsx;
766
- if (/data-saastro-field/.test(m[2])) return jsx;
767
- const tag = m[1];
768
- const attrs = m[2];
769
- const selfClose = m[3];
770
- const replacement = `<${tag}${attrs} data-saastro-field=${valueExpr}${selfClose}>`;
771
- return jsx.slice(0, m.index) + replacement + jsx.slice(m.index + m[0].length);
894
+ const start = jsx.search(/<[A-Za-z]/);
895
+ if (start < 0) return jsx;
896
+ const tagInfo = readOpeningTag(jsx, start);
897
+ if (!tagInfo) return jsx;
898
+ if (/data-saastro-field/.test(tagInfo.attrs)) return jsx;
899
+ const inject = ` data-saastro-field=${valueExpr}`;
900
+ const insertAt = start + 1 + tagInfo.tag.length;
901
+ return jsx.slice(0, insertAt) + inject + jsx.slice(insertAt);
902
+ }
903
+ function readOpeningTag(s, start) {
904
+ if (s[start] !== "<") return null;
905
+ const nameMatch = /^[A-Za-z][A-Za-z0-9]*/.exec(s.slice(start + 1));
906
+ if (!nameMatch) return null;
907
+ const tag = nameMatch[0];
908
+ let i = start + 1 + tag.length;
909
+ const attrsStart = i;
910
+ let inString = null;
911
+ let braceDepth = 0;
912
+ while (i < s.length) {
913
+ const ch = s[i];
914
+ if (inString) {
915
+ if (ch === "\\") {
916
+ i += 2;
917
+ continue;
918
+ }
919
+ if (inString === "`" && ch === "$" && s[i + 1] === "{") {
920
+ const close = readBalanced(s, i + 1, "{", "}");
921
+ if (close == null) return null;
922
+ i = close + 1;
923
+ continue;
924
+ }
925
+ if (ch === inString) inString = null;
926
+ i++;
927
+ continue;
928
+ }
929
+ if (braceDepth > 0) {
930
+ if (ch === '"' || ch === "'" || ch === "`") {
931
+ inString = ch;
932
+ i++;
933
+ continue;
934
+ }
935
+ if (ch === "{") braceDepth++;
936
+ else if (ch === "}") braceDepth--;
937
+ i++;
938
+ continue;
939
+ }
940
+ if (ch === '"' || ch === "'" || ch === "`") {
941
+ inString = ch;
942
+ i++;
943
+ continue;
944
+ }
945
+ if (ch === "{") {
946
+ braceDepth = 1;
947
+ i++;
948
+ continue;
949
+ }
950
+ if (ch === "/" && s[i + 1] === ">") {
951
+ return {
952
+ tag,
953
+ attrs: s.slice(attrsStart, i),
954
+ end: i + 2,
955
+ selfClose: true
956
+ };
957
+ }
958
+ if (ch === ">") {
959
+ return {
960
+ tag,
961
+ attrs: s.slice(attrsStart, i),
962
+ end: i + 1,
963
+ selfClose: false
964
+ };
965
+ }
966
+ i++;
967
+ }
968
+ return null;
772
969
  }
773
970
  function transformScalarField(template, name) {
774
971
  return transformDottedReference(template, name, null, JSON.stringify(name));
@@ -873,18 +1070,48 @@ function isInsideTag(s, pos) {
873
1070
  return false;
874
1071
  }
875
1072
  function findEnclosingElement(s, pos) {
876
- const tagRe = /<(\/?)([A-Za-z][A-Za-z0-9]*)\b([^<>]*?)(\/?)>/g;
877
1073
  const tags = [];
878
- let m;
879
- while ((m = tagRe.exec(s)) !== null) {
880
- tags.push({
881
- isClose: m[1] === "/",
882
- selfClose: m[4] === "/",
883
- tagName: m[2],
884
- attrs: m[3],
885
- start: m.index,
886
- end: m.index + m[0].length
887
- });
1074
+ for (let scan = 0; scan < s.length; ) {
1075
+ const lt = s.indexOf("<", scan);
1076
+ if (lt < 0) break;
1077
+ const next = s[lt + 1];
1078
+ if (next === "/") {
1079
+ const nm = /^[A-Za-z][A-Za-z0-9]*/.exec(s.slice(lt + 2));
1080
+ if (!nm) {
1081
+ scan = lt + 1;
1082
+ continue;
1083
+ }
1084
+ const gt = s.indexOf(">", lt + 2 + nm[0].length);
1085
+ if (gt < 0) break;
1086
+ tags.push({
1087
+ isClose: true,
1088
+ selfClose: false,
1089
+ tagName: nm[0],
1090
+ attrs: "",
1091
+ start: lt,
1092
+ end: gt + 1
1093
+ });
1094
+ scan = gt + 1;
1095
+ continue;
1096
+ }
1097
+ if (typeof next === "string" && /[A-Za-z]/.test(next)) {
1098
+ const ot = readOpeningTag(s, lt);
1099
+ if (!ot) {
1100
+ scan = lt + 1;
1101
+ continue;
1102
+ }
1103
+ tags.push({
1104
+ isClose: false,
1105
+ selfClose: ot.selfClose,
1106
+ tagName: ot.tag,
1107
+ attrs: ot.attrs,
1108
+ start: lt,
1109
+ end: ot.end
1110
+ });
1111
+ scan = ot.end;
1112
+ continue;
1113
+ }
1114
+ scan = lt + 1;
888
1115
  }
889
1116
  const stack = [];
890
1117
  let bestOpen = null;
@@ -940,42 +1167,84 @@ function isMainAstroModule(id) {
940
1167
  if (query && /[?&]astro([&=]|$)/.test(query)) return false;
941
1168
  return true;
942
1169
  }
1170
+ function isReactComponentModule(id) {
1171
+ const { path, query } = splitId(id);
1172
+ if (query) return false;
1173
+ if (!/\.(tsx|jsx)$/.test(path)) return false;
1174
+ if (/\.(test|spec|stories)\.[tj]sx$/.test(path)) return false;
1175
+ return true;
1176
+ }
943
1177
  function studioAutoWrap(opts = {}) {
944
1178
  const enabled = opts.autoWrap === true;
945
- const include = opts.include ?? ((id) => id.endsWith(".astro"));
1179
+ const include = opts.include ?? defaultInclude;
1180
+ const instrumentTsx = opts.instrumentTsx !== false;
1181
+ let projectRoot;
946
1182
  return {
947
1183
  name: "@saastro/studio:auto-wrap",
948
1184
  enforce: "pre",
1185
+ configResolved(config) {
1186
+ projectRoot = config.root;
1187
+ },
949
1188
  async load(id) {
950
1189
  if (!enabled) return null;
951
- if (!isMainAstroModule(id)) return null;
952
1190
  const { path } = splitId(id);
953
- if (!include(path)) return null;
954
- let code;
955
- try {
956
- code = await fs.readFile(path, "utf8");
957
- } catch {
958
- return null;
1191
+ if (isMainAstroModule(id)) {
1192
+ if (!include(path)) return null;
1193
+ return loadAstro(path);
959
1194
  }
960
- if (!hasFieldPrefixReference(code)) return null;
961
- const wrapped = transformAstroSource(code);
962
- const schema = extractPropsSchema(code, { componentPath: path });
963
- let next = wrapped;
964
- if (schema) {
965
- next = injectSchema(next, schema);
966
- next = instrumentFields(next, schema, { componentPath: path });
1195
+ if (instrumentTsx && isReactComponentModule(id)) {
1196
+ if (!include(path)) return null;
1197
+ return loadTsx(path);
967
1198
  }
968
- if (next === code) return null;
969
- return { code: next, map: null };
1199
+ return null;
970
1200
  },
971
1201
  transformIndexHtml(html) {
972
1202
  if (!enabled) return html;
973
- return injectVersionMetaTag(html);
1203
+ let next = injectVersionMetaTag(html);
1204
+ next = injectVisibilityStyle(next, projectRoot);
1205
+ return next;
974
1206
  }
975
1207
  };
976
1208
  }
1209
+ var DEFAULT_INCLUDE_EXTS = /\.(astro|tsx|jsx)$/;
1210
+ function defaultInclude(id) {
1211
+ return DEFAULT_INCLUDE_EXTS.test(id);
1212
+ }
1213
+ async function loadAstro(path) {
1214
+ let code;
1215
+ try {
1216
+ code = await fs.readFile(path, "utf8");
1217
+ } catch {
1218
+ return null;
1219
+ }
1220
+ if (!hasFieldPrefixReference(code)) return null;
1221
+ const wrapped = transformAstroSource(code);
1222
+ const schema = extractPropsSchema(code, { componentPath: path });
1223
+ let next = wrapped;
1224
+ if (schema) {
1225
+ next = injectSchema(next, schema);
1226
+ next = instrumentFields(next, schema, { componentPath: path });
1227
+ }
1228
+ if (next === code) return null;
1229
+ return { code: next, map: null };
1230
+ }
1231
+ async function loadTsx(path) {
1232
+ let code;
1233
+ try {
1234
+ code = await fs.readFile(path, "utf8");
1235
+ } catch {
1236
+ return null;
1237
+ }
1238
+ if (!code.includes(".map(")) return null;
1239
+ if (extractPropsTypeBlock(code) === null) return null;
1240
+ const schema = extractComponentSchemaFromTsx(code, { componentPath: path });
1241
+ if (!schema || !schema.fields.length) return null;
1242
+ const next = instrumentJsxFields(code, schema, { componentPath: path });
1243
+ if (next === code) return null;
1244
+ return { code: next, map: null };
1245
+ }
977
1246
 
978
1247
  export {
979
1248
  studioAutoWrap
980
1249
  };
981
- //# sourceMappingURL=chunk-5LJXT5GH.js.map
1250
+ //# sourceMappingURL=chunk-LKNB6JHI.js.map