@saastro/studio 0.1.1 → 0.1.3

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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  studioAutoWrap
3
- } from "./chunk-IBV2VRJU.js";
3
+ } from "./chunk-XVVAAIXK.js";
4
4
 
5
5
  // src/integration/index.ts
6
6
  function saastro(options = {}) {
@@ -21,4 +21,4 @@ function saastro(options = {}) {
21
21
  export {
22
22
  saastro
23
23
  };
24
- //# sourceMappingURL=chunk-AW6UON7J.js.map
24
+ //# sourceMappingURL=chunk-IPUVRBJI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/integration/index.ts"],"sourcesContent":["import type { AstroIntegration } from 'astro'\nimport studioAutoWrap from '../vite/index.js'\nimport type { StudioAutoWrapOptions } from '../vite/types.js'\n\nexport type SaastroStudioOptions = StudioAutoWrapOptions\n\n/**\n * Astro Integration for Saastro Studio.\n *\n * import saastro from '@saastro/studio'\n * export default defineConfig({\n * integrations: [saastro({ autoWrap: true })],\n * })\n *\n * This integration is sugar: it registers the underlying Vite plugin\n * (`@saastro/studio/vite`) so users only need one import. The plugin's\n * ordering correctness comes from using a `load` hook (which always runs\n * before any `transform`, including Astro's `.astro` compiler) — NOT\n * from where the integration slots itself in. See the comment block in\n * `../vite/index.ts` for the full rationale.\n *\n * The `@saastro/studio/vite` entry remains exported for sites that wired\n * the bare Vite plugin directly.\n */\nexport default function saastro(options: SaastroStudioOptions = {}): AstroIntegration {\n return {\n name: '@saastro/studio',\n hooks: {\n 'astro:config:setup': ({ updateConfig }) => {\n updateConfig({\n vite: {\n plugins: [studioAutoWrap(options)],\n },\n })\n },\n },\n }\n}\n"],"mappings":";;;;;AAwBe,SAAR,QAAyB,UAAgC,CAAC,GAAqB;AACpF,SAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO;AAAA,MACL,sBAAsB,CAAC,EAAE,aAAa,MAAM;AAC1C,qBAAa;AAAA,UACX,MAAM;AAAA,YACJ,SAAS,CAAC,eAAe,OAAO,CAAC;AAAA,UACnC;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
@@ -1,3 +1,6 @@
1
+ // src/vite/index.ts
2
+ import { promises as fs } from "fs";
3
+
1
4
  // src/vite/auto-wrap.ts
2
5
  var FRONTMATTER_RE = /^\s*---\s*\n([\s\S]*?)\n---\s*\n/;
3
6
  var SAASTRO_ATTR_RE = /data-saastro/;
@@ -528,14 +531,9 @@ function transformArrayField(template, field) {
528
531
  } else {
529
532
  result += transformedJsx;
530
533
  }
531
- const bodyAbsEnd = arrowEnd + body.bodyOffset + body.bodyText.length;
534
+ const afterBody = arrowEnd + body.bodyCloseRel;
532
535
  const callCloseAbsEnd = arrowEnd + body.totalEndRel;
533
- if (body.parens) {
534
- const afterBodyParen = bodyAbsEnd + 1;
535
- result += template.slice(afterBodyParen, callCloseAbsEnd);
536
- } else {
537
- result += template.slice(bodyAbsEnd, callCloseAbsEnd);
538
- }
536
+ result += template.slice(afterBody, callCloseAbsEnd);
539
537
  cursor = callCloseAbsEnd;
540
538
  re.lastIndex = cursor;
541
539
  }
@@ -567,6 +565,7 @@ function readArrowBody(template, start) {
567
565
  parens,
568
566
  bodyOffset: bodyStart - start,
569
567
  bodyText: template.slice(bodyStart, bodyEnd),
568
+ bodyCloseRel: inner + 1 - start,
570
569
  totalEndRel: j - start
571
570
  };
572
571
  }
@@ -584,6 +583,7 @@ function readArrowBody(template, start) {
584
583
  parens,
585
584
  bodyOffset: bodyStart - start,
586
585
  bodyText: template.slice(bodyStart, bodyEnd),
586
+ bodyCloseRel: bodyEnd - start,
587
587
  totalEndRel: j - start
588
588
  };
589
589
  }
@@ -915,7 +915,7 @@ function isOnlyMeaningfulExpression(body, relStart, relEnd) {
915
915
 
916
916
  // src/vite/meta-tag.ts
917
917
  var STUDIO_PROTOCOL_VERSION = "2";
918
- var PACKAGE_VERSION = "0.1.1".length > 0 ? "0.1.1" : "0.0.0+unknown";
918
+ var PACKAGE_VERSION = "0.1.2".length > 0 ? "0.1.2" : "0.0.0+unknown";
919
919
  var META_TAGS = [
920
920
  `<meta name="saastro:studio-version" content="${STUDIO_PROTOCOL_VERSION}">`,
921
921
  `<meta name="saastro:studio-package" content="${PACKAGE_VERSION}">`
@@ -931,22 +931,40 @@ var FRONTMATTER_FIELD_PREFIX_RE = /^\s*---\s*\n[\s\S]*?\bfieldPrefix\b[\s\S]*?\n
931
931
  function hasFieldPrefixReference(source) {
932
932
  return FRONTMATTER_FIELD_PREFIX_RE.test(source);
933
933
  }
934
+ function splitId(id) {
935
+ const idx = id.indexOf("?");
936
+ return idx === -1 ? { path: id, query: "" } : { path: id.slice(0, idx), query: id.slice(idx) };
937
+ }
938
+ function isMainAstroModule(id) {
939
+ const { path, query } = splitId(id);
940
+ if (!path.endsWith(".astro")) return false;
941
+ if (query && /[?&]astro([&=]|$)/.test(query)) return false;
942
+ return true;
943
+ }
934
944
  function studioAutoWrap(opts = {}) {
935
945
  const enabled = opts.autoWrap === true;
936
946
  const include = opts.include ?? ((id) => id.endsWith(".astro"));
937
947
  return {
938
948
  name: "@saastro/studio:auto-wrap",
939
949
  enforce: "pre",
940
- transform(code, id) {
950
+ async load(id) {
941
951
  if (!enabled) return null;
942
- if (!include(id)) return null;
952
+ if (!isMainAstroModule(id)) return null;
953
+ const { path } = splitId(id);
954
+ if (!include(path)) return null;
955
+ let code;
956
+ try {
957
+ code = await fs.readFile(path, "utf8");
958
+ } catch {
959
+ return null;
960
+ }
943
961
  if (!hasFieldPrefixReference(code)) return null;
944
962
  const wrapped = transformAstroSource(code);
945
- const schema = extractPropsSchema(code, { componentPath: id });
963
+ const schema = extractPropsSchema(code, { componentPath: path });
946
964
  let next = wrapped;
947
965
  if (schema) {
948
966
  next = injectSchema(next, schema);
949
- next = instrumentFields(next, schema, { componentPath: id });
967
+ next = instrumentFields(next, schema, { componentPath: path });
950
968
  }
951
969
  if (next === code) return null;
952
970
  return { code: next, map: null };
@@ -961,4 +979,4 @@ function studioAutoWrap(opts = {}) {
961
979
  export {
962
980
  studioAutoWrap
963
981
  };
964
- //# sourceMappingURL=chunk-IBV2VRJU.js.map
982
+ //# sourceMappingURL=chunk-XVVAAIXK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/vite/index.ts","../src/vite/auto-wrap.ts","../../studio-core/src/component-schema/extract.ts","../../studio-core/src/component-schema/extract-bindings.ts","../src/vite/extract-props-schema.ts","../src/vite/inject-schema.ts","../src/vite/instrument-fields.ts","../src/vite/meta-tag.ts"],"sourcesContent":["import { promises as fs } from 'node:fs'\nimport type { Plugin } from 'vite'\nimport { transformAstroSource } from './auto-wrap.js'\nimport { extractPropsSchema } from './extract-props-schema.js'\nimport { injectSchema } from './inject-schema.js'\nimport { instrumentFields } from './instrument-fields.js'\nimport { injectVersionMetaTag } from './meta-tag.js'\nimport type { StudioAutoWrapOptions } from './types.js'\n\nexport type { StudioAutoWrapOptions } from './types.js'\n\n/**\n * Quick check: does this file declare `fieldPrefix` in its frontmatter\n * Props interface? Used as the entry-gate for the whole transform —\n * we only want to spin up ts-morph + run the regex pipeline on files\n * that are actually Studio-instrumentable. Anything else is skipped\n * with a cheap regex test.\n *\n * Conservative match: anchored on the `---` frontmatter fences and a\n * word-boundaried `fieldPrefix`. Comments mentioning `fieldPrefix`\n * trigger a false positive, but the downstream passes are no-ops on\n * files that don't actually use it.\n */\nconst FRONTMATTER_FIELD_PREFIX_RE = /^\\s*---\\s*\\n[\\s\\S]*?\\bfieldPrefix\\b[\\s\\S]*?\\n---\\s*\\n/\n\nfunction hasFieldPrefixReference(source: string): boolean {\n return FRONTMATTER_FIELD_PREFIX_RE.test(source)\n}\n\n/**\n * Splits an id into its file-path component and the query string (if any).\n * Vite ids look like `/abs/path/to/Hero.astro` or\n * `/abs/path/to/Hero.astro?astro&type=script&index=0&lang.ts`.\n */\nfunction splitId(id: string): { path: string; query: string } {\n const idx = id.indexOf('?')\n return idx === -1 ? { path: id, query: '' } : { path: id.slice(0, idx), query: id.slice(idx) }\n}\n\n/**\n * Whether this id is the main `.astro` module (NOT an Astro-virtual\n * sub-request like `?astro&type=style&index=0`). We only transform main\n * modules — sub-requests are emitted by Astro's own load hook with\n * already-compiled content and must not be touched.\n */\nfunction isMainAstroModule(id: string): boolean {\n const { path, query } = splitId(id)\n if (!path.endsWith('.astro')) return false\n if (query && /[?&]astro([&=]|$)/.test(query)) return false\n return true\n}\n\n/**\n * Vite plugin factory for Saastro Studio instrumentation.\n *\n * Drop into `vite.plugins` in `astro.config.mjs`:\n *\n * import studioAutoWrap from '@saastro/studio/vite';\n * export default defineConfig({\n * vite: { plugins: [studioAutoWrap({ autoWrap: true })] },\n * });\n *\n * ## Why `load` and not `transform`?\n *\n * Astro 6's `astro:build` plugin compiles `.astro` source → JS inside its\n * own `transform` hook (`vite-plugin-astro/index.js`, line ~204). Both\n * plugins use `enforce: 'pre'`, so ordering falls back to insertion\n * order — and Astro's plugin is registered before any user\n * integration's plugins, so its transform always runs first. The result:\n * by the time our transform fires, `code` is already compiled JS and our\n * regex-on-frontmatter pipeline can't match anything.\n *\n * Vite runs all `load` hooks BEFORE any `transform`, regardless of\n * `enforce`. Astro's own `load` filters strictly to virtual sub-requests\n * (`/[?&]astro([&=]|$)/`), so the main `.astro` module falls through to\n * the default fs loader. We slot ourselves in there: read the file,\n * apply the regex pipeline to raw `.astro` source (with frontmatter\n * intact), return the modified raw source. Astro's transform then\n * compiles our edited source as if it were the original on disk.\n *\n * What the plugin does:\n * 1. (load) Scans `.astro` source for `fieldPrefix` prop and injects\n * `data-saastro=\"sec:${fieldPrefix}\"` on the root element.\n * 2. (load) Extracts a `ComponentSchema` from the file's `interface Props`\n * (via ts-morph in `@saastro/studio-core`) and bakes it into a\n * `<script type=\"application/saastro-schema\" data-prefix=\"…\">` placed\n * just before the section's root closing tag. The overlay reads it\n * at runtime so the Studio panel doesn't need a server round-trip.\n * 3. (load) Walks the template and stamps each rendered `Props` field\n * with `data-saastro-field=\"<propPath>\"` so the Studio overlay can\n * mutate the exact DOM node by name.\n * 4. (transformIndexHtml) Injects <meta name=\"saastro:studio-version\">\n * into every HTML page's <head> so the Hub can detect protocol version.\n */\nexport default function studioAutoWrap(opts: StudioAutoWrapOptions = {}): Plugin {\n const enabled = opts.autoWrap === true\n const include = opts.include ?? ((id: string) => id.endsWith('.astro'))\n\n return {\n name: '@saastro/studio:auto-wrap',\n enforce: 'pre',\n\n async load(id) {\n if (!enabled) return null\n if (!isMainAstroModule(id)) return null\n const { path } = splitId(id)\n if (!include(path)) return null\n\n let code: string\n try {\n code = await fs.readFile(path, 'utf8')\n } catch {\n // Let the default Vite loader produce its own error.\n return null\n }\n\n // Trigger: the file declares a `fieldPrefix` prop. This catches\n // BOTH the auto-wrap path (we'll inject `data-saastro=\"sec:...\"`)\n // AND the hand-written path (the developer already put the marker\n // on the root tag — we still need to run schema injection +\n // per-field instrumentation, which transformAstroSource skips).\n if (!hasFieldPrefixReference(code)) return null\n\n const wrapped = transformAstroSource(code)\n const schema = extractPropsSchema(code, { componentPath: path })\n let next = wrapped\n // Schema-driven steps need the parsed Props interface. When\n // ts-morph can't parse it (no `interface Props`, or trickier\n // shapes than we support), we still keep the auto-wrap edit so\n // the section marker lands — the runtime catalog fallback on the\n // Hub picks up the slack.\n if (schema) {\n next = injectSchema(next, schema)\n next = instrumentFields(next, schema, { componentPath: path })\n }\n if (next === code) return null\n return { code: next, map: null }\n },\n\n transformIndexHtml(html) {\n if (!enabled) return html\n return injectVersionMetaTag(html)\n },\n }\n}\n","/**\n * Studio auto-wrap transform — Studio v5.2 §M1.2.\n *\n * Scans `.astro` files for a `fieldPrefix` prop and injects\n * `data-saastro=\"sec:${fieldPrefix}\"` on the template's root element.\n *\n * Approach: regex-based source transformation (enforce: 'pre', runs before\n * Astro compiles the template). Covers 95%+ of real-world Astro components.\n *\n * ## Limitations\n * - Component roots (PascalCase tags) are skipped — the marker must land\n * on a rendered HTML element so the overlay can hit-test it.\n * - Top-level branching (`{cond ? <A/> : <B/>}`) is skipped; users can\n * opt-in manually with `editableSection()`.\n */\n\n// Matches the frontmatter block at the very top of the file.\nconst FRONTMATTER_RE = /^\\s*---\\s*\\n([\\s\\S]*?)\\n---\\s*\\n/\n\n// Matches both HTML attribute form (`data-saastro=`) and the JSX spread\n// form we inject (`\"data-saastro\":`). Plain substring check is enough —\n// `data-saastro` is unique to this instrumentation.\nconst SAASTRO_ATTR_RE = /data-saastro/\n\n/**\n * Detects any mention of `fieldPrefix` in the frontmatter.\n * Conservative: a comment mentioning `fieldPrefix` is a false-positive,\n * but the runtime guard (`fieldPrefix ? ... : {}`) makes it safe.\n */\nconst FIELD_PREFIX_REFERENCE_RE = /\\bfieldPrefix\\b/\n\n/**\n * Matches the first tag opening after the frontmatter. Group 1 = tag name\n * (either HTML lowercase or component PascalCase), group 2 = existing\n * attributes, group 3 = self-closing slash. The lowercase check in\n * `transformAstroSource` decides whether to inject or skip.\n */\nconst FIRST_OPEN_TAG_RE = /<([a-zA-Z][a-zA-Z0-9]*)\\b([^<>]*?)\\s*(\\/?)>/\n\n/**\n * Attribute spread injected on the root element. Uses conditional form\n * so no `data-saastro` attribute appears in production HTML when the\n * prop is falsy (avoids `data-saastro=\"sec:\"` partial matches).\n *\n * H9 — also emit `draggable=\"true\"` so HTML5 drag-and-drop fires\n * `dragstart` on the section root. Spec v5.2 §M1.6 promised section-level\n * drag reorder; the overlay's drag listeners (overlay-script.ts) were\n * wired but never received events because the wrappers weren't draggable.\n */\nconst INJECTION =\n ' {...(fieldPrefix ? { \"data-saastro\": `sec:${fieldPrefix}`, draggable: \"true\" } : {})}'\n\n/**\n * Transform a single .astro source string. Exported for unit tests;\n * the plugin wraps this with file-system + Vite metadata.\n */\nexport function transformAstroSource(source: string): string {\n const fm = FRONTMATTER_RE.exec(source)\n if (!fm) return source\n\n if (!FIELD_PREFIX_REFERENCE_RE.test(fm[1]!)) return source\n\n const template = source.slice(fm[0].length)\n const tagMatch = FIRST_OPEN_TAG_RE.exec(template)\n if (!tagMatch) return source\n\n const tagName = tagMatch[1]!\n if (tagName[0]! !== tagName[0]!.toLowerCase()) return source\n\n const existingAttrs = tagMatch[2]!\n if (SAASTRO_ATTR_RE.test(existingAttrs)) return source\n\n const selfClose = tagMatch[3]!\n const replacement = `<${tagName}${existingAttrs}${INJECTION}${selfClose ? ' ' + selfClose : ''}>`\n const templateInjected =\n template.slice(0, tagMatch.index) +\n replacement +\n template.slice(tagMatch.index + tagMatch[0]!.length)\n\n return source.slice(0, fm[0].length) + templateInjected\n}\n","/**\n * Extract a `ComponentSchema` from a single `.astro` source file.\n *\n * The shape we care about lives in the frontmatter — the block between\n * the opening `---` and closing `---` lines. We feed the frontmatter into\n * ts-morph (TypeScript's AST library) so we get real type resolution\n * instead of the brittle regex matching the Vite plugin uses today for\n * the `fieldPrefix` marker injection.\n *\n * Why ts-morph and not a regex:\n * - The customer's `interface Props` may import types from sibling files\n * (`import type { StatItem } from './types'`).\n * - Property types may be unions of string literals (`'left' | 'center'`).\n * - Optional/required, nullable, default-bearing properties all need to\n * round-trip into the panel correctly.\n *\n * What we deliberately don't do here:\n * - Parse the Astro template body — we only care about the type contract.\n * Per-prop DOM markers (`data-saastro-field`) are a separate transform\n * in the Vite plugin (Phase 2 of the rollout plan).\n * - Run any I/O. The caller (Vite plugin or runtime fallback) is in\n * charge of locating the source and importing related .ts files into\n * the same in-memory ts-morph project.\n */\n\nimport {\n Project,\n SourceFile,\n Type,\n type InterfaceDeclaration,\n type JSDocableNode,\n type Node,\n type Symbol as TsSymbol,\n type TypeAliasDeclaration,\n} from 'ts-morph'\nimport type {\n ComponentField,\n ComponentSchema,\n FieldKind,\n} from './types.js'\n\n/**\n * Allowed kinds in `@studio:<kind>` JSDoc directives. The customer can\n * sprinkle these on individual `Props` properties to override the\n * name-based heuristic used by `pickStringKind` and friends. Compatibility\n * with the resolved type is enforced in `applyDirective` — a directive\n * that conflicts with the type is silently ignored (easier to debug than\n * a build-time throw, and the panel still shows something sensible).\n *\n * Shorthand: `@studio:image` is accepted as an alias for `image-url`,\n * since \"image\" reads more naturally on a property declaration.\n */\nconst STUDIO_DIRECTIVE_KINDS: ReadonlySet<FieldKind> = new Set<FieldKind>([\n 'string',\n 'textarea',\n 'image-url',\n 'enum',\n 'number',\n 'boolean',\n 'array',\n 'object',\n 'json',\n])\n\nconst STUDIO_DIRECTIVE_RE = /@studio:([A-Za-z][A-Za-z0-9-]*)/g\n\n/**\n * Property-name patterns we treat as image-shaped strings. The list is\n * intentionally conservative — adding \"hero\" here once caused EVERY\n * field on a hero section to flip to the image-url widget. Hints\n * describe the FIELD's role, not the section it lives in.\n */\nconst IMAGE_NAME_HINTS = /(^|_)(image|img|avatar|logo|icon|thumbnail|photo|picture|cover|background)($|_)/i\nconst IMAGE_SUFFIX_HINTS = /_(url|src)$/i\n\nconst FRONTMATTER_RE = /^\\s*---\\s*\\n([\\s\\S]*?)\\n---/\n\nexport interface ExtractOptions {\n /**\n * Path of the .astro file relative to the project root. Used only for\n * diagnostics and to populate `ComponentSchema.componentPath`.\n */\n componentPath?: string\n /**\n * Base directory the source file lives in. Affects how ts-morph\n * resolves relative imports (`import type { Foo } from './foo'`).\n * Defaults to the in-memory virtual root, which means imports won't\n * resolve unless the caller pre-loaded those files into the same\n * Project.\n */\n baseDir?: string\n /**\n * Caller-supplied ts-morph Project. Reusing one across extractions in\n * the same plugin run is cheaper because TypeScript can share the\n * type-checker cache. If omitted we spin up a fresh in-memory one.\n */\n project?: Project\n}\n\n/**\n * Pull the frontmatter out of a .astro source string. Returns null when\n * the file has no frontmatter block (a pure-template Astro component).\n */\nexport function extractFrontmatter(astroSource: string): string | null {\n const m = FRONTMATTER_RE.exec(astroSource)\n return m ? (m[1] ?? '') : null\n}\n\n/**\n * Main entry point. Takes the raw `.astro` source, returns a schema\n * (or null when the file has no `interface Props`). Errors during\n * parsing are caught and returned as null — the caller decides whether\n * to fall back to the catalog.\n */\nexport function extractComponentSchema(\n astroSource: string,\n options: ExtractOptions = {},\n): ComponentSchema | null {\n const frontmatter = extractFrontmatter(astroSource)\n if (frontmatter == null) return null\n\n const project = options.project ?? createProject()\n // Virtual filename — ts-morph ignores extensions for lookup, but using\n // .ts here keeps any error message readable.\n const virtualName = options.componentPath\n ? `${options.componentPath}.frontmatter.ts`\n : 'component.frontmatter.ts'\n\n let sourceFile: SourceFile\n try {\n sourceFile = project.createSourceFile(virtualName, frontmatter, {\n overwrite: true,\n })\n } catch {\n return null\n }\n\n let propsDecl: InterfaceDeclaration | TypeAliasDeclaration | undefined =\n sourceFile.getInterface('Props')\n if (!propsDecl) {\n propsDecl = sourceFile.getTypeAlias('Props')\n }\n if (!propsDecl) {\n sourceFile.delete()\n return null\n }\n\n const propsType = propsDecl.getType()\n const fields: ComponentField[] = []\n for (const prop of propsType.getProperties()) {\n const field = describeProperty(prop, propsDecl)\n if (field) fields.push(field)\n }\n\n // Parse default values from the destructuring assignment if present.\n // Pattern matched: `const { title = 'foo', stats = [] } = Astro.props`.\n applyDefaultsFromDestructuring(frontmatter, fields)\n\n sourceFile.delete()\n\n return {\n fields,\n componentPath: options.componentPath,\n version: 1,\n }\n}\n\nfunction createProject(): Project {\n return new Project({\n useInMemoryFileSystem: true,\n skipFileDependencyResolution: true,\n compilerOptions: {\n strict: true,\n // Match the customer's typical Astro tsconfig — ESNext + node16\n // resolution covers most setups. Misses are non-fatal: unresolved\n // imports surface as `'json'` kind with a description.\n target: 99,\n module: 199,\n moduleResolution: 100,\n allowSyntheticDefaultImports: true,\n esModuleInterop: true,\n skipLibCheck: true,\n },\n })\n}\n\ninterface PropertyParent {\n getSourceFile(): SourceFile\n}\n\nfunction describeProperty(\n prop: TsSymbol,\n parent: PropertyParent,\n): ComponentField | null {\n const name = prop.getName()\n const declarations = prop.getDeclarations()\n const decl = declarations[0]\n if (!decl) return null\n\n // Optional flag: `prop?: T` OR `prop: T | undefined`\n const isQuestionToken = (\n decl as unknown as { hasQuestionToken?: () => boolean }\n ).hasQuestionToken?.() ?? false\n\n const propsSourceFile = parent.getSourceFile()\n const type = prop.getTypeAtLocation(propsSourceFile)\n\n const optional =\n isQuestionToken || typeIncludes(type, t => t.isUndefined())\n\n const directive = readStudioDirective(decl)\n\n // ts-morph models `boolean` as the union `true | false`, so the\n // generic union path below would mis-classify it as heterogeneous.\n // Short-circuit primitives that survive a `T | undefined` wrapper.\n if (type.isBoolean() || stripUndefined(type).isBoolean()) {\n return applyDirective({ name, kind: 'boolean', optional }, directive, type)\n }\n\n // Strip undefined / null while preserving the rest of the union so\n // string-literal enums like `'left' | 'center' | undefined` keep all\n // their literals.\n const nonNullishParts = type.isUnion()\n ? type.getUnionTypes().filter(t => !t.isUndefined() && !t.isNull())\n : [type]\n\n // 1. String literal union → enum\n if (\n nonNullishParts.length >= 1 &&\n nonNullishParts.every(t => t.isStringLiteral())\n ) {\n const enumValues = nonNullishParts.map(t =>\n String(t.getLiteralValue()),\n )\n if (enumValues.length === 1) {\n // Single literal isn't an enum — describe as a normal string.\n const f = describeType(name, nonNullishParts[0]!)\n return applyDirective(\n { ...f, optional: f.optional || optional },\n directive,\n type,\n )\n }\n return applyDirective(\n { name, kind: 'enum', optional, enumValues },\n directive,\n type,\n )\n }\n\n // 2. Single non-nullish branch → describe that branch.\n if (nonNullishParts.length === 1) {\n const f = describeType(name, nonNullishParts[0]!)\n return applyDirective(\n { ...f, optional: f.optional || optional },\n directive,\n type,\n )\n }\n\n // 3. Heterogeneous union (string | number, etc.) — too ambiguous to\n // pick a control. Fall back to json with the raw type text.\n return applyDirective(\n {\n name,\n kind: 'json',\n optional,\n description: `unparsed: ${type.getText()}`,\n },\n directive,\n type,\n )\n}\n\n/**\n * Read the first `@studio:<kind>` JSDoc directive on a node. Returns\n * the canonicalised `FieldKind` or null if none/unknown. Walks ts-morph\n * `getJsDocs()` plus the leading-comment fallback (older ts-morph\n * versions don't always thread JSDoc onto every PropertySignature).\n */\nfunction readStudioDirective(decl: Node): FieldKind | null {\n const sources: string[] = []\n\n const jsDocable = decl as Node & Partial<JSDocableNode>\n if (typeof jsDocable.getJsDocs === 'function') {\n for (const doc of jsDocable.getJsDocs()) {\n sources.push(doc.getText())\n }\n }\n\n // Fallback: scan leading trivia in case JSDoc isn't attached as a\n // first-class node (happens for some property signature shapes).\n if (sources.length === 0) {\n const ranges =\n (decl as unknown as {\n getLeadingCommentRanges?: () => { getText(): string }[]\n }).getLeadingCommentRanges?.() ?? []\n for (const range of ranges) {\n const text = range.getText()\n if (text.includes('@studio:')) sources.push(text)\n }\n }\n\n for (const source of sources) {\n STUDIO_DIRECTIVE_RE.lastIndex = 0\n let match: RegExpExecArray | null\n while ((match = STUDIO_DIRECTIVE_RE.exec(source)) !== null) {\n const raw = match[1]\n if (!raw) continue\n // Shorthand: `@studio:image` → `image-url`.\n const kind = (raw === 'image' ? 'image-url' : raw) as FieldKind\n if (STUDIO_DIRECTIVE_KINDS.has(kind)) return kind\n // Unknown kind — skip and keep looking. First valid match wins.\n }\n }\n\n return null\n}\n\n/**\n * Override `field.kind` when the directive is compatible with the\n * already-resolved type. Incompatible directives are dropped silently —\n * the panel renders the heuristic-picked control instead, which is far\n * easier to debug than a build-time throw.\n */\nfunction applyDirective(\n field: ComponentField,\n directive: FieldKind | null,\n type: Type,\n): ComponentField {\n if (!directive) return field\n if (!isDirectiveCompatible(directive, type)) return field\n if (field.kind === directive) return field\n return { ...field, kind: directive }\n}\n\nfunction isDirectiveCompatible(directive: FieldKind, type: Type): boolean {\n // `json` is the universal escape hatch.\n if (directive === 'json') return true\n\n const stripped = stripUndefined(type)\n const nonNullishParts = type.isUnion()\n ? type.getUnionTypes().filter(t => !t.isUndefined() && !t.isNull())\n : [stripped]\n\n switch (directive) {\n case 'string':\n case 'textarea':\n case 'image-url':\n return nonNullishParts.every(t => t.isString() || t.isStringLiteral())\n case 'enum':\n return nonNullishParts.every(t => t.isString() || t.isStringLiteral())\n case 'number':\n return nonNullishParts.every(t => t.isNumber() || t.isNumberLiteral())\n case 'boolean':\n return type.isBoolean() || stripped.isBoolean()\n case 'array':\n return nonNullishParts.every(t => t.isArray())\n case 'object':\n return nonNullishParts.every(t => isDescendableObject(t))\n default:\n return false\n }\n}\n\n/**\n * Built-in classes / brand types we never want to descend into. They\n * have member properties (e.g. RegExp.lastIndex, Date.getTime) that\n * would confuse the panel, so we surface them as a json fallback\n * instead of an object form.\n */\nconst NON_DESCENDABLE_TYPE_NAMES = new Set([\n 'RegExp',\n 'Date',\n 'Map',\n 'Set',\n 'WeakMap',\n 'WeakSet',\n 'Promise',\n 'Error',\n 'URL',\n 'URLSearchParams',\n 'ArrayBuffer',\n 'Uint8Array',\n])\n\nfunction isCallable(type: Type): boolean {\n return type.getCallSignatures().length > 0\n}\n\nfunction isDescendableObject(type: Type): boolean {\n if (!type.isObject() && !type.isInterface()) return false\n if (isCallable(type)) return false\n const symbol = type.getSymbol() ?? type.getAliasSymbol()\n const symbolName = symbol?.getName()\n if (symbolName && NON_DESCENDABLE_TYPE_NAMES.has(symbolName)) return false\n return type.getProperties().length > 0\n}\n\nfunction describeType(name: string, type: Type): ComponentField {\n // Order matters: literal first (single literal kept as enum-of-1 might\n // be useful but we treat as string for sanity), then primitives,\n // arrays, descendable objects, fallback.\n\n if (type.isStringLiteral()) {\n return { name, kind: 'string', optional: false }\n }\n\n if (\n type.isUnion() &&\n type.getUnionTypes().every(t => t.isStringLiteral())\n ) {\n const values = type\n .getUnionTypes()\n .map(t => String(t.getLiteralValue()))\n return { name, kind: 'enum', optional: false, enumValues: values }\n }\n\n if (type.isString()) {\n return { name, kind: pickStringKind(name), optional: false }\n }\n if (type.isNumber()) return { name, kind: 'number', optional: false }\n if (type.isBoolean()) return { name, kind: 'boolean', optional: false }\n\n if (type.isArray()) {\n const elementType = type.getArrayElementType()\n if (elementType) {\n const itemDescriptor = describeAnyType('__item__', elementType)\n return { name, kind: 'array', optional: false, items: itemDescriptor }\n }\n return { name, kind: 'array', optional: false }\n }\n\n if (isDescendableObject(type)) {\n const fields: ComponentField[] = []\n for (const sub of type.getProperties()) {\n const subDecl = sub.getDeclarations()[0]\n if (!subDecl) continue\n const subType = sub.getTypeAtLocation(subDecl)\n const subOptional =\n ((subDecl as unknown as { hasQuestionToken?: () => boolean })\n .hasQuestionToken?.() ?? false) ||\n typeIncludes(subType, t => t.isUndefined())\n const sf = describeAnyType(sub.getName(), subType)\n const subDirective = readStudioDirective(subDecl)\n fields.push(\n applyDirective(\n { ...sf, optional: sf.optional || subOptional },\n subDirective,\n subType,\n ),\n )\n }\n if (fields.length > 0) {\n return { name, kind: 'object', optional: false, fields }\n }\n }\n\n return {\n name,\n kind: 'json',\n optional: false,\n description: `unparsed: ${type.getText()}`,\n }\n}\n\n/**\n * Like `describeType` but also handles the optional / null-stripping\n * pre-pass — used when descending into array items or nested object\n * properties (where we don't have a question-token to look at).\n */\nfunction describeAnyType(name: string, type: Type): ComponentField {\n const optional = typeIncludes(type, t => t.isUndefined())\n const nonNullishParts = type.isUnion()\n ? type.getUnionTypes().filter(t => !t.isUndefined() && !t.isNull())\n : [type]\n\n if (\n nonNullishParts.length >= 1 &&\n nonNullishParts.every(t => t.isStringLiteral())\n ) {\n if (nonNullishParts.length === 1) {\n return { ...describeType(name, nonNullishParts[0]!), optional }\n }\n return {\n name,\n kind: 'enum',\n optional,\n enumValues: nonNullishParts.map(t => String(t.getLiteralValue())),\n }\n }\n\n if (nonNullishParts.length === 1) {\n const f = describeType(name, nonNullishParts[0]!)\n return { ...f, optional: f.optional || optional }\n }\n\n return {\n name,\n kind: 'json',\n optional,\n description: `unparsed: ${type.getText()}`,\n }\n}\n\nfunction pickStringKind(name: string): FieldKind {\n if (IMAGE_NAME_HINTS.test(name) || IMAGE_SUFFIX_HINTS.test(name)) {\n return 'image-url'\n }\n // Long-form heuristic — only the name is available without runtime\n // length info. Matches the existing zod-introspect behavior.\n if (/description|body|content|copy|summary/i.test(name)) {\n return 'textarea'\n }\n return 'string'\n}\n\nfunction typeIncludes(type: Type, predicate: (t: Type) => boolean): boolean {\n if (predicate(type)) return true\n if (type.isUnion()) {\n return type.getUnionTypes().some(predicate)\n }\n return false\n}\n\nfunction stripUndefined(type: Type): Type {\n if (!type.isUnion()) return type\n const remaining = type\n .getUnionTypes()\n .filter(t => !t.isUndefined() && !t.isNull())\n // We just need a representative type for primitive checks — return any\n // of the surviving members. The full extraction path uses\n // `nonNullishParts` (an array) for completeness.\n return remaining[0] ?? type\n}\n\n/**\n * Best-effort: parse `const { foo = X, bar = Y } = Astro.props` from the\n * frontmatter and stamp `defaultValue` on matching fields. Pure regex —\n * we don't try to evaluate the RHS, just record it as a string for the\n * panel's placeholder.\n */\nfunction applyDefaultsFromDestructuring(\n frontmatter: string,\n fields: ComponentField[],\n): void {\n const match = /const\\s*\\{([\\s\\S]*?)\\}\\s*=\\s*Astro\\.props/.exec(frontmatter)\n if (!match) return\n const body = match[1] ?? ''\n // Naive split on top-level commas — sufficient for `name = 'foo'`,\n // `count = 0`, `flag = false`. Object/array defaults will fall through.\n const entries = splitTopLevel(body, ',')\n for (const entry of entries) {\n const eq = entry.indexOf('=')\n if (eq < 0) continue\n const lhs = entry.slice(0, eq).trim()\n const rhs = entry.slice(eq + 1).trim()\n if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(lhs)) continue\n const target = fields.find(f => f.name === lhs)\n if (!target) continue\n target.defaultValue = parseLiteral(rhs)\n }\n}\n\nfunction splitTopLevel(s: string, sep: string): string[] {\n const out: string[] = []\n let depth = 0\n let current = ''\n for (const ch of s) {\n if (ch === '{' || ch === '[' || ch === '(') depth++\n else if (ch === '}' || ch === ']' || ch === ')') depth--\n if (ch === sep && depth === 0) {\n out.push(current)\n current = ''\n } else {\n current += ch\n }\n }\n if (current.trim()) out.push(current)\n return out\n}\n\nfunction parseLiteral(rhs: string): unknown {\n const trimmed = rhs.trim()\n if (trimmed === 'true') return true\n if (trimmed === 'false') return false\n if (trimmed === 'null') return null\n if (/^-?\\d+(\\.\\d+)?$/.test(trimmed)) return Number(trimmed)\n // Strip wrapping quotes.\n const quoted = /^(['\"`])([\\s\\S]*)\\1$/.exec(trimmed)\n if (quoted) return quoted[2]\n // Anything else (object, array, identifier reference) — keep the raw\n // source text so the panel can show it as a placeholder string.\n return trimmed\n}\n","/**\n * Extract per-prop call-site bindings from an Astro PAGE file.\n *\n * Where component-schema (./extract.ts) parses the COMPONENT file\n * (`Hero.astro`) for its `interface Props`, this extractor parses the\n * PAGE that uses the component (`src/pages/[locale]/index.astro`) and\n * inspects each attribute on the JSX-like element marked with\n * `fieldPrefix=\"<prefix>\"`.\n *\n * The output is what the panel needs to decide which change op to stage\n * when the user edits a field:\n *\n * <Hero\n * title={hero.title} ← i18n: keyPath = 'hero.home.title'\n * image=\"/img/hero.jpg\" ← literal\n * stats={getStats()} ← unknown\n * fieldPrefix=\"hero.home\"\n * />\n *\n * Frontmatter trace:\n * const { t } = Astro.locals;\n * const hero = t.hero.home;\n *\n * The trace builds a map `hero → 'hero.home'`, then the attribute\n * `title={hero.title}` resolves to keyPath `hero.home.title`.\n *\n * Supported call-site shapes:\n * prop=\"literal\"\n * prop={'literal'}\n * prop={`literal`} (no expressions)\n * prop={IDENT} (where IDENT was assigned to t.<path>)\n * prop={IDENT.sub.path}\n * prop={IDENT['sub']['path']}\n * prop={t.foo.bar}\n * prop={t('foo.bar')}\n * prop={t['foo.bar']}\n *\n * Unsupported (graded as `unknown`):\n * prop={fn()}, prop={a + b}, prop={cond ? x : y}, etc.\n *\n * The extractor is intentionally conservative — anything ambiguous lands\n * as `unknown` rather than silently misclassifying. The panel's behaviour\n * for `unknown` is the same as before this extractor existed\n * (`section_props_update`), so misclassification is degrade-only.\n */\n\nimport { Project } from 'ts-morph'\nimport type { PropBinding } from './types.js'\n\nconst FRONTMATTER_RE = /^\\s*---\\s*\\n([\\s\\S]*?)\\n---/\n\n/**\n * Strict identifier (TS rules — letters, digits, _, $; cannot start with\n * digit). Conservative on purpose: looser parsing can match attribute\n * fragments inside HTML strings.\n */\nconst IDENT_RE = '[A-Za-z_$][A-Za-z0-9_$]*'\n\nexport interface ExtractBindingsOptions {\n /** Path of the page file relative to repo root — included in the result for debugging. */\n pageFile?: string\n}\n\nexport interface ExtractBindingsResult {\n /**\n * Map from prop name → binding info. Props whose call-site value we\n * couldn't classify are still included as `kind: 'unknown'` so the\n * panel can render them (and stage as `section_props_update`).\n *\n * Props missing from the call site entirely (e.g. relying on a\n * component-side default) are absent from this map.\n */\n bindings: Record<string, PropBinding>\n /** Echo of pageFile from options, for debugging. */\n pageFile?: string\n}\n\nexport function extractCallSiteBindings(\n pageSource: string,\n fieldPrefix: string,\n options: ExtractBindingsOptions = {},\n): ExtractBindingsResult {\n const empty: ExtractBindingsResult = { bindings: {}, pageFile: options.pageFile }\n\n const fmMatch = pageSource.match(FRONTMATTER_RE)\n const frontmatter = fmMatch?.[1] ?? ''\n const template = fmMatch ? pageSource.slice(fmMatch[0].length) : pageSource\n\n // 1. Build identifier → keyPath map from the frontmatter. Only\n // top-level `const X = t.<path>` (or `let`/`var`) declarations\n // count. Nested scopes are out of scope (rare in Astro pages).\n const identMap = buildIdentifierMap(frontmatter)\n\n // 2. Locate the JSX-like element with the matching fieldPrefix and\n // pull out its other attributes.\n const attrs = findElementAttributes(template, fieldPrefix)\n if (!attrs) return empty\n\n // 3. Classify each attribute.\n const bindings: Record<string, PropBinding> = {}\n for (const [name, raw] of Object.entries(attrs)) {\n if (name === 'fieldPrefix') continue\n bindings[name] = classifyAttributeValue(raw, identMap)\n }\n\n return { bindings, pageFile: options.pageFile }\n}\n\n/* -------------------------------------------------------------------------- */\n/* Frontmatter: identifier → key path */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Walk the page's frontmatter and build a map from identifier name to\n * the translation key path it was assigned. Examples:\n *\n * const hero = t.hero.home; → hero → 'hero.home'\n * const homeAbout = t.homeAbout; → homeAbout → 'homeAbout'\n * const x = t['foo']['bar']; → x → 'foo.bar'\n *\n * Only direct assignments rooted at `t` are tracked — chained traces\n * through other identifiers (`const a = hero.foo`) are ignored to keep\n * this conservative. dorjoiers does flat one-level binding so this\n * covers the canonical case.\n */\nfunction buildIdentifierMap(frontmatter: string): Map<string, string> {\n const map = new Map<string, string>()\n if (!frontmatter) return map\n\n const project = new Project({\n useInMemoryFileSystem: true,\n skipAddingFilesFromTsConfig: true,\n skipFileDependencyResolution: true,\n skipLoadingLibFiles: true,\n compilerOptions: {\n allowJs: true,\n noResolve: true,\n jsx: 4 /* Preserve */,\n noEmit: true,\n },\n })\n let sf\n try {\n sf = project.createSourceFile('__page-frontmatter.ts', frontmatter)\n } catch {\n return map\n }\n\n for (const v of sf.getVariableDeclarations()) {\n const name = v.getName()\n const init = v.getInitializer()\n if (!init) continue\n const path = traceTPath(init.getText())\n if (path !== null) map.set(name, path)\n }\n return map\n}\n\n/**\n * Convert a member-access chain rooted at `t` into a dotted key path.\n * Returns `null` if the expression isn't a recognized t.<...> chain.\n */\nfunction traceTPath(expr: string): string | null {\n // Strip outer parens (rare): `(t.hero.home)` → `t.hero.home`.\n let e = expr.trim()\n while (e.startsWith('(') && e.endsWith(')')) {\n e = e.slice(1, -1).trim()\n }\n if (!/^t([.[(]|$)/.test(e)) return null\n\n // Tokenize: split into dot-segments and ['x']/[\"x\"] segments.\n const segments: string[] = []\n let i = 1 // skip leading 't'\n while (i < e.length) {\n const ch = e[i]\n if (ch === '.') {\n const m = e.slice(i + 1).match(new RegExp('^' + IDENT_RE))\n if (!m) return null\n segments.push(m[0])\n i += 1 + m[0].length\n } else if (ch === '[') {\n const close = e.indexOf(']', i + 1)\n if (close === -1) return null\n const inner = e.slice(i + 1, close).trim()\n const litMatch = inner.match(/^(['\"])(.+)\\1$/)\n if (!litMatch || litMatch[2] === undefined) return null\n segments.push(litMatch[2])\n i = close + 1\n } else if (ch === '(') {\n // t('foo.bar') — only valid as the very first segment.\n if (segments.length !== 0) return null\n const close = matchingClose(e, i)\n if (close === -1) return null\n const inner = e.slice(i + 1, close).trim()\n const litMatch = inner.match(/^(['\"`])(.+)\\1$/)\n if (!litMatch || litMatch[2] === undefined) return null\n // Split the dotted key inside the call.\n for (const seg of litMatch[2].split('.')) {\n if (seg) segments.push(seg)\n }\n i = close + 1\n // After a call, only a member access may follow (rare). Bail to\n // keep this deterministic.\n if (i < e.length) return null\n } else {\n return null\n }\n }\n return segments.length === 0 ? null : segments.join('.')\n}\n\nfunction matchingClose(s: string, openIdx: number): number {\n let depth = 0\n let inStr: string | null = null\n for (let i = openIdx; i < s.length; i++) {\n const ch = s[i]\n if (inStr) {\n if (ch === '\\\\') { i++; continue }\n if (ch === inStr) inStr = null\n continue\n }\n if (ch === '\"' || ch === \"'\" || ch === '`') {\n inStr = ch\n continue\n }\n if (ch === '(') depth++\n else if (ch === ')') {\n depth--\n if (depth === 0) return i\n }\n }\n return -1\n}\n\n/* -------------------------------------------------------------------------- */\n/* Template: locate element with fieldPrefix and extract attributes */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Find the JSX-like element in the template that has\n * `fieldPrefix=\"<prefix>\"` (or `fieldPrefix={`<prefix>`}` /\n * `fieldPrefix={'<prefix>'}`) and return ALL its attributes as a\n * `name → rawValueText` map. RawValueText preserves the original\n * surrounding form (\"...\" / '...' / {...}) so the classifier can\n * distinguish literals from expressions.\n *\n * Returns `null` if no matching element is found.\n *\n * The scanner is purpose-built for Astro/JSX-style attribute lists. It\n * tracks balanced braces inside expression values and respects\n * string/template literals to avoid being fooled by `{` or `}` inside\n * string content.\n */\nfunction findElementAttributes(\n template: string,\n fieldPrefix: string,\n): Record<string, string> | null {\n const escaped = escapeRegExp(fieldPrefix)\n // Match `<Tag` … `fieldPrefix=\"...\"` … `>` or `/>`. We don't insist on\n // tag identity — Studio prefixes are unique so the attribute alone\n // pins us. We DO need to know where the tag begins so we can scan\n // its full attribute list both before and after fieldPrefix.\n const fpRegexes = [\n new RegExp(`fieldPrefix\\\\s*=\\\\s*\"${escaped}\"`),\n new RegExp(`fieldPrefix\\\\s*=\\\\s*'${escaped}'`),\n new RegExp(`fieldPrefix\\\\s*=\\\\s*\\\\{\\\\s*\\`${escaped}\\`\\\\s*\\\\}`),\n new RegExp(`fieldPrefix\\\\s*=\\\\s*\\\\{\\\\s*'${escaped}'\\\\s*\\\\}`),\n new RegExp(`fieldPrefix\\\\s*=\\\\s*\\\\{\\\\s*\"${escaped}\"\\\\s*\\\\}`),\n ]\n let fpIdx = -1\n for (const re of fpRegexes) {\n const m = re.exec(template)\n if (m) { fpIdx = m.index; break }\n }\n if (fpIdx === -1) return null\n\n // Walk backwards to the opening `<Tag` of the element containing\n // this attribute. We bail if we cross a closing `>` or another `<`,\n // which would mean fieldPrefix lives outside the tag we expected.\n const tagStart = findTagStartBefore(template, fpIdx)\n if (tagStart === -1) return null\n\n const tagEnd = findTagEndAfter(template, fpIdx)\n if (tagEnd === -1) return null\n\n const tagBody = template.slice(tagStart, tagEnd + 1)\n return parseAttributeList(tagBody)\n}\n\nfunction findTagStartBefore(s: string, idx: number): number {\n // Scan back looking for `<` that starts a JSX tag (component or HTML).\n // We assume tag content has no embedded `<` outside expressions; an\n // unbalanced `>` before our `<` invalidates the parse.\n let depth = 0\n for (let i = idx; i >= 0; i--) {\n const ch = s[i]\n if (ch === '}') depth++\n else if (ch === '{') {\n if (depth > 0) depth--\n } else if (ch === '<' && depth === 0) {\n return i\n } else if (ch === '>' && depth === 0) {\n // Crossed a tag boundary — fieldPrefix isn't inside an open tag.\n return -1\n }\n }\n return -1\n}\n\nfunction findTagEndAfter(s: string, idx: number): number {\n let depth = 0\n let inStr: string | null = null\n for (let i = idx; i < s.length; i++) {\n const ch = s[i]\n if (inStr) {\n if (ch === '\\\\') { i++; continue }\n if (ch === inStr) inStr = null\n continue\n }\n if (ch === '\"' || ch === \"'\" || ch === '`') {\n inStr = ch\n continue\n }\n if (ch === '{') depth++\n else if (ch === '}') {\n if (depth > 0) depth--\n } else if (ch === '>' && depth === 0) {\n return i\n }\n }\n return -1\n}\n\n/**\n * Parse the attributes of a single tag body (e.g. `<Hero a=\"1\" b={x} c />`).\n * Returns `name → rawValueText` (without the surrounding quotes/braces\n * stripped — the classifier needs the syntax).\n *\n * Boolean attributes (no `=`) get value `'true'` (literal `'true'`,\n * not the boolean primitive — the classifier treats it as a literal\n * to avoid surprising behaviour).\n */\nfunction parseAttributeList(tagBody: string): Record<string, string> {\n const out: Record<string, string> = {}\n // Skip the leading `<TagName` (or `<TagName>`).\n const tagNameMatch = tagBody.match(new RegExp(`^<${IDENT_RE}(?:\\\\.${IDENT_RE})*`))\n if (!tagNameMatch) return out\n let i = tagNameMatch[0].length\n\n while (i < tagBody.length) {\n // Skip whitespace.\n while (i < tagBody.length && /\\s/.test(tagBody[i] ?? '')) i++\n if (i >= tagBody.length) break\n const ch = tagBody[i]\n if (ch === '/' || ch === '>') break\n\n // Read attribute name. Astro accepts hyphens (`data-foo`) but\n // Component prop names are TS identifiers — we stick to that.\n const nameMatch = tagBody.slice(i).match(new RegExp(`^${IDENT_RE}`))\n if (!nameMatch) {\n // Not a name we can parse — skip to next whitespace-or-end.\n i++\n continue\n }\n const name = nameMatch[0]\n i += name.length\n\n // Skip whitespace before potential `=`.\n while (i < tagBody.length && /\\s/.test(tagBody[i] ?? '')) i++\n\n if (tagBody[i] !== '=') {\n // Boolean attribute (no `=`). Quote it so the classifier\n // resolves it as a string literal `\"true\"`.\n out[name] = '\"true\"'\n continue\n }\n i++ // consume `=`\n while (i < tagBody.length && /\\s/.test(tagBody[i] ?? '')) i++\n\n const valStart = i\n const after = tagBody[i]\n if (after === '\"' || after === \"'\") {\n const close = findStringClose(tagBody, i)\n if (close === -1) break\n out[name] = tagBody.slice(valStart, close + 1)\n i = close + 1\n } else if (after === '{') {\n const close = findExprClose(tagBody, i)\n if (close === -1) break\n out[name] = tagBody.slice(valStart, close + 1)\n i = close + 1\n } else {\n // Unquoted value — uncommon in JSX; consume to whitespace.\n const m = tagBody.slice(i).match(/^[^\\s>]+/)\n if (!m) break\n out[name] = m[0]\n i += m[0].length\n }\n }\n return out\n}\n\nfunction findStringClose(s: string, openIdx: number): number {\n const quote = s[openIdx]\n for (let i = openIdx + 1; i < s.length; i++) {\n const ch = s[i]\n if (ch === '\\\\') { i++; continue }\n if (ch === quote) return i\n }\n return -1\n}\n\nfunction findExprClose(s: string, openIdx: number): number {\n let depth = 0\n let inStr: string | null = null\n for (let i = openIdx; i < s.length; i++) {\n const ch = s[i]\n if (inStr) {\n if (ch === '\\\\') { i++; continue }\n if (ch === inStr) inStr = null\n continue\n }\n if (ch === '\"' || ch === \"'\" || ch === '`') {\n inStr = ch\n continue\n }\n if (ch === '{') depth++\n else if (ch === '}') {\n depth--\n if (depth === 0) return i\n }\n }\n return -1\n}\n\n/* -------------------------------------------------------------------------- */\n/* Classification */\n/* -------------------------------------------------------------------------- */\n\nfunction classifyAttributeValue(\n raw: string,\n identMap: Map<string, string>,\n): PropBinding {\n if (!raw) return { kind: 'unknown' }\n const trimmed = raw.trim()\n\n // String literal: \"...\" or '...'\n const strLit = matchStringLiteral(trimmed)\n if (strLit !== null) return { kind: 'literal', value: strLit }\n\n // Expression: { ... }\n if (trimmed.startsWith('{') && trimmed.endsWith('}')) {\n const inner = trimmed.slice(1, -1).trim()\n\n // Literal-inside-expression: {\"foo\"} / {'foo'} / {`foo`} (no $)\n const innerStr = matchStringLiteral(inner)\n if (innerStr !== null) return { kind: 'literal', value: innerStr }\n const tplLit = matchTemplateLiteralNoExpr(inner)\n if (tplLit !== null) return { kind: 'literal', value: tplLit }\n\n // Member access: hero.title / homeAbout.eyebrow / hero['title']\n const path = traceMemberPath(inner, identMap)\n if (path !== null) return { kind: 'i18n', keyPath: path }\n\n return { kind: 'unknown', expression: inner }\n }\n\n return { kind: 'unknown', expression: trimmed }\n}\n\nfunction matchStringLiteral(s: string): string | null {\n if (s.length < 2) return null\n const q = s[0]\n if ((q !== '\"' && q !== \"'\") || s[s.length - 1] !== q) return null\n // Reject anything with an unescaped quote in the middle.\n for (let i = 1; i < s.length - 1; i++) {\n if (s[i] === '\\\\') { i++; continue }\n if (s[i] === q) return null\n }\n // Unescape minimally — \\\" \\' \\\\ \\n \\t.\n return s.slice(1, -1).replace(/\\\\(.)/g, (_, c) => {\n if (c === 'n') return '\\n'\n if (c === 't') return '\\t'\n return c\n })\n}\n\nfunction matchTemplateLiteralNoExpr(s: string): string | null {\n if (s.length < 2 || s[0] !== '`' || s[s.length - 1] !== '`') return null\n if (s.includes('${')) return null\n return s.slice(1, -1)\n}\n\n/**\n * Trace a member-access expression rooted at a known identifier or `t`.\n * Returns dotted key path or `null`.\n *\n * Examples (with identMap = { hero: 'hero.home' }):\n * hero.title → 'hero.home.title'\n * hero['title'] → 'hero.home.title'\n * t.hero.home.title → 'hero.home.title'\n * t('hero.home.title') → 'hero.home.title'\n * t.hero['home'].title → 'hero.home.title'\n */\nfunction traceMemberPath(\n expr: string,\n identMap: Map<string, string>,\n): string | null {\n const e = expr.trim()\n // Direct t(...) / t.<...>\n const direct = traceTPath(e)\n if (direct !== null) return direct\n\n // Identifier-rooted\n const m = e.match(new RegExp(`^${IDENT_RE}`))\n if (!m) return null\n const root = m[0]\n const prefix = identMap.get(root)\n if (!prefix) return null\n\n let i = root.length\n const segments: string[] = []\n while (i < e.length) {\n const ch = e[i]\n if (ch === '.') {\n const sub = e.slice(i + 1).match(new RegExp(`^${IDENT_RE}`))\n if (!sub) return null\n segments.push(sub[0])\n i += 1 + sub[0].length\n } else if (ch === '[') {\n const close = e.indexOf(']', i + 1)\n if (close === -1) return null\n const inner = e.slice(i + 1, close).trim()\n const lit = inner.match(/^(['\"])(.+)\\1$/)\n if (!lit || lit[2] === undefined) return null\n segments.push(lit[2])\n i = close + 1\n } else {\n return null\n }\n }\n if (segments.length === 0) return prefix\n return prefix + '.' + segments.join('.')\n}\n\nfunction escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n","/**\n * Studio component-schema extractor — Studio v5.3 Phase 1.2.\n *\n * Thin wrapper around `extractComponentSchema` from `@saastro/studio-core`\n * that memoises results keyed by source-file content. Each `.astro` file\n * is parsed at most once per content hash; HMR replaces a hash with a new\n * one on every edit, which naturally evicts stale entries.\n *\n * The extractor itself is heavy (spins up an in-memory ts-morph project),\n * so caching matters even for small sites — `pnpm dev` restarts and full\n * production builds otherwise re-extract every section on every transform\n * pass.\n */\n\nimport {\n extractComponentSchema,\n type ComponentSchema,\n} from '@saastro/studio-core/component-schema'\n\n/**\n * Cache keyed by `${componentPath} ${hash(source)}`. The componentPath\n * is part of the key because the same source can legitimately live at\n * two paths during dev (a copied file rename), and we want the cached\n * schema to carry the right `componentPath` field.\n *\n * LRU cap so long-running `pnpm dev` sessions don't leak memory: every\n * HMR edit inserts a new (path, hash) entry — without a bound the Map\n * grows monotonically and never frees the old schemas. JS Map preserves\n * insertion order, so we evict the oldest key when we hit the ceiling\n * and re-insert on hits so the recency heuristic survives.\n */\nconst CACHE_MAX = 500\nconst cache = new Map<string, ComponentSchema | null>()\n\n/**\n * djb2-style content hash. Plenty of entropy for a per-file cache key —\n * collisions just mean a stale schema for one transform pass, which the\n * next edit will refresh. Avoids pulling in `node:crypto` here so the\n * module stays lean and bundles cleanly under tsup.\n */\nfunction hashSource(source: string): string {\n let h = 5381\n for (let i = 0; i < source.length; i++) {\n h = ((h << 5) + h + source.charCodeAt(i)) | 0\n }\n // Force unsigned hex so the key doesn't carry a leading `-`.\n return (h >>> 0).toString(16)\n}\n\nexport interface ExtractPropsSchemaOptions {\n /** Path used for diagnostics + populated on the returned schema. */\n componentPath?: string\n}\n\n/**\n * Memoised wrapper around `extractComponentSchema`. Returns null when the\n * file has no `interface Props` (or the parser bails) — same contract as\n * the underlying extractor.\n */\nexport function extractPropsSchema(\n source: string,\n options: ExtractPropsSchemaOptions = {},\n): ComponentSchema | null {\n const key = `${options.componentPath ?? ''} ${hashSource(source)}`\n if (cache.has(key)) {\n // LRU bump: re-insert so the entry moves to the back of the\n // insertion-order list and isn't the next eviction victim.\n const cached = cache.get(key) ?? null\n cache.delete(key)\n cache.set(key, cached)\n return cached\n }\n const schema = extractComponentSchema(source, {\n componentPath: options.componentPath,\n })\n if (cache.size >= CACHE_MAX) {\n // Evict the oldest (first inserted) key — Map iteration is in\n // insertion order, so `.keys().next().value` is the LRU.\n const oldest = cache.keys().next().value\n if (oldest !== undefined) cache.delete(oldest)\n }\n cache.set(key, schema)\n return schema\n}\n\n/**\n * Test-only escape hatch — clears the in-memory cache. Production code\n * should never need to call this; the content-hash key already handles\n * HMR by producing a new key per edit.\n */\nexport function __resetPropsSchemaCache(): void {\n cache.clear()\n}\n\nexport type { ComponentSchema }\n","/**\n * Inject a `<script type=\"application/saastro-schema\">` next to an\n * instrumented section root.\n *\n * Runs AFTER `transformAstroSource` (the auto-wrap pass). Given an\n * `.astro` source whose root element already carries the\n * `data-saastro=\"sec:${fieldPrefix}\"` marker, this transform inserts a\n * single inline `<script>` carrying the JSON-encoded `ComponentSchema`\n * just before the root's closing tag. The overlay (`overlay-script.ts`\n * in hub-react) reads that script at runtime to discover prop fields\n * without making an extra network round-trip.\n *\n * ## Output shape\n *\n * For an auto-wrapped section with prop `fieldPrefix`:\n *\n * ```html\n * <section ... data-saastro=\"sec:...\" ...>\n * ...original body...\n * <script type=\"application/saastro-schema\" is:inline data-prefix={fieldPrefix}>\n * {\"version\":1,\"fields\":[…]}\n * </script>\n * </section>\n * ```\n *\n * The script body is a JSON-string literal (NOT an Astro JS expression)\n * because Astro's compiler must treat it as text — anything wrapped in\n * `{}` would be evaluated at SSR time and produce a runtime expression.\n * Astro preserves `<script is:inline>` content verbatim regardless of\n * `{` characters, so plain JSON works.\n *\n * ## Constraints\n *\n * - Idempotent — re-running on output that already has a\n * `<script type=\"application/saastro-schema\">` is a no-op.\n * - Self-closing roots (`<img />`, `<input />`) are skipped: there is no\n * place to put a child node. The overlay's document-wide fallback\n * query covers those if/when we add a sibling-emission path later.\n * - Non-HTML roots (PascalCase components) are skipped — `auto-wrap`\n * already declines to instrument those.\n * - Escapes any `</` inside the JSON body to `<\\/` so a default value\n * containing the literal `</script>` cannot break the script tag.\n */\n\nimport type { ComponentSchema } from '@saastro/studio-core/component-schema'\n\nconst FRONTMATTER_RE = /^\\s*---\\s*\\n([\\s\\S]*?)\\n---\\s*\\n/\n\n/** Substring check is enough — the marker is unique to our instrumentation. */\nconst SCHEMA_SCRIPT_MARKER = 'type=\"application/saastro-schema\"'\n\n/**\n * Match the first opening tag after the frontmatter. Mirrors the regex\n * `auto-wrap` uses but with a simpler attribute capture (we don't care\n * about the existing attribute text here, only positions).\n */\nconst FIRST_OPEN_TAG_RE = /<([a-zA-Z][a-zA-Z0-9]*)\\b([^<>]*?)\\s*(\\/?)>/\n\n/**\n * Extract the literal default for `fieldPrefix` from a destructuring\n * assignment, if any. Examples that yield `'hero.home'`:\n *\n * - `const { fieldPrefix = 'hero.home' } = Astro.props`\n * - `const { fieldPrefix = \"hero.home\", title } = Astro.props`\n *\n * Returns null when the default is missing, non-string, or too complex\n * to parse with a single regex (object literal, function call, etc.).\n */\nfunction extractFieldPrefixDefault(frontmatter: string): string | null {\n const destruct = /const\\s*\\{([\\s\\S]*?)\\}\\s*=\\s*Astro\\.props/.exec(frontmatter)\n if (!destruct) return null\n const body = destruct[1] ?? ''\n // Find `fieldPrefix = <literal>` — accept single, double, or backtick\n // quotes. `[^'\"\\n]` keeps the match conservative; templated strings\n // (`\\`hero.${section}\\``) deliberately do NOT match — we'd lose the\n // expression by treating it as a literal.\n const m = /\\bfieldPrefix\\s*=\\s*(['\"])([^'\"\\n]*)\\1/.exec(body)\n return m ? (m[2] ?? null) : null\n}\n\n/**\n * Detect the literal default embedded in a `data-saastro` attribute that\n * was hand-written rather than auto-wrap-injected. Pattern matched:\n *\n * data-saastro={`sec:${fieldPrefix ?? 'hero.home'}`}\n *\n * Used as a fallback when the destructuring regex above misses (the\n * dorjoiers Hero, for example, destructures `fieldPrefix` with NO\n * default and folds the fallback into the JSX expression).\n */\nfunction extractFieldPrefixFromDataAttr(template: string): string | null {\n const m = /data-saastro=\\{\\s*`\\s*sec:\\$\\{\\s*fieldPrefix\\s*\\?\\?\\s*(['\"])([^'\"`]*)\\1\\s*\\}/.exec(\n template,\n )\n return m ? (m[2] ?? null) : null\n}\n\n/**\n * Find the position of the matching closing tag for the root element at\n * `openTagEnd`. Naive bracket counting on `<TAG` openers and `</TAG>`\n * closers — sufficient for well-formed Astro templates where the root\n * is a single HTML element (the only shape `auto-wrap` instruments).\n *\n * HTML comments (`<!-- ... -->`) are skipped so a commented-out\n * `<section>` inside the root doesn't desynchronise the depth counter.\n * Astro JS expression comments (`{/* ... *​/}`) don't carry HTML tags so\n * they never confuse the bracket walker; we leave them alone.\n *\n * Returns the index of the `<` in the closing tag, or -1 when no match\n * is found (in which case the caller bails out without emitting).\n */\nfunction findClosingTag(\n haystack: string,\n tagName: string,\n searchFrom: number,\n): number {\n // Match either an HTML comment OR an opener / closer of `tagName`.\n // Putting comments in the same alternation guarantees we skip them\n // atomically — a tag inside a comment can't sneak into the depth\n // counter.\n const re = new RegExp(\n `<!--[\\\\s\\\\S]*?-->|<(/?)${tagName}\\\\b[^<>]*?(/?)>`,\n 'gi',\n )\n re.lastIndex = searchFrom\n let depth = 1\n while (true) {\n const m = re.exec(haystack)\n if (!m) return -1\n // Comments don't have capture groups for the opener/closer marks,\n // so `m[1]` and `m[2]` are undefined for the comment branch. Skip.\n if (m[0]!.startsWith('<!--')) continue\n const isClose = m[1] === '/'\n const isSelfClose = m[2] === '/'\n if (isClose) {\n depth--\n if (depth === 0) return m.index\n } else if (!isSelfClose) {\n depth++\n }\n }\n}\n\n/**\n * JSON-encode a schema and escape any character sequences that could\n * accidentally close the host `<script>` tag. The replacement is safe\n * for `JSON.parse` to round-trip because `\\/` is a valid JSON escape.\n */\nfunction encodeSchemaBody(schema: ComponentSchema): string {\n return JSON.stringify(schema).replace(/<\\//g, '<\\\\/')\n}\n\nexport interface InjectSchemaOptions {\n /**\n * Override for the `data-prefix` attribute. The plugin normally lets\n * `injectSchema` derive this from the frontmatter; tests use this to\n * pin the value explicitly.\n */\n prefixOverride?: string\n}\n\n/**\n * Inject the schema script into `source`. Returns the source unchanged\n * when the file lacks a frontmatter / lacks an instrumented root /\n * already has a schema script.\n */\nexport function injectSchema(\n source: string,\n schema: ComponentSchema,\n options: InjectSchemaOptions = {},\n): string {\n // Idempotency — never emit a second script tag.\n if (source.includes(SCHEMA_SCRIPT_MARKER)) return source\n\n const fm = FRONTMATTER_RE.exec(source)\n if (!fm) return source\n\n const frontmatter = fm[1] ?? ''\n const templateStart = fm[0].length\n const template = source.slice(templateStart)\n\n const tagMatch = FIRST_OPEN_TAG_RE.exec(template)\n if (!tagMatch) return source\n const tagName = tagMatch[1]!\n // Component (PascalCase) roots are out of scope — auto-wrap skips them\n // for the same reason: the marker has to land on a real DOM element.\n if (tagName[0]! !== tagName[0]!.toLowerCase()) return source\n\n const isSelfClose = tagMatch[3] === '/'\n if (isSelfClose) return source\n\n // Where does the opening tag END (just past the `>`)?\n const openTagEnd = tagMatch.index + tagMatch[0].length\n // Find the matching closing tag.\n const closeIdx = findClosingTag(template, tagName, openTagEnd)\n if (closeIdx < 0) return source\n\n // Determine the data-prefix expression. Priority order:\n // 1. Caller-provided override (tests).\n // 2. Default literal from destructuring (`{ fieldPrefix = 'x' } = ...`).\n // 3. Default literal from a hand-written data-saastro attribute.\n // 4. Bare `{fieldPrefix}` reference — overlay falls back to the\n // sibling-by-data-prefix lookup.\n const prefixDefault =\n options.prefixOverride ??\n extractFieldPrefixDefault(frontmatter) ??\n extractFieldPrefixFromDataAttr(template)\n\n const dataPrefixAttr =\n prefixDefault != null\n ? `data-prefix={fieldPrefix ?? ${JSON.stringify(prefixDefault)}}`\n : `data-prefix={fieldPrefix}`\n\n const scriptTag =\n `<script type=\"application/saastro-schema\" is:inline ${dataPrefixAttr}>` +\n encodeSchemaBody(schema) +\n `</script>`\n\n const newTemplate =\n template.slice(0, closeIdx) + scriptTag + template.slice(closeIdx)\n\n return source.slice(0, templateStart) + newTemplate\n}\n","/**\n * Per-prop DOM instrumentation transform — Phase 2.\n *\n * Walks the Astro template and adds `data-saastro-field=\"<propPath>\"`\n * markers on the elements that render each `Props` field, so the\n * Studio overlay's `applySectionProps` can mutate the right node by\n * exact match instead of guessing via role-based heuristics.\n *\n * Markers we emit:\n *\n * <h1 data-saastro-field=\"title\">{title}</h1>\n * Welcome <span data-saastro-field=\"appName\">{appName}</span>!\n * <img data-saastro-field=\"image\" src={image} alt={imageAlt} />\n * {stats.map((s, __i) => (\n * <div data-saastro-field={`stats[${__i}]`}>\n * <p data-saastro-field={`stats[${__i}].value`}>{s.value}</p>\n * </div>\n * ))}\n *\n * Implementation choice: regex / string-walk, NOT `@astrojs/compiler`.\n * We deliberately keep the dependency surface light because:\n *\n * - This package ships into customer Astro builds; pulling in a\n * ~3 MB compiler dep is hard to justify for a small set of patterns.\n * - The patterns we need to handle are constrained to plain prop\n * references (`{ident}`, `{ident.member}`) and one well-known\n * iterator shape (`{ident.map((x[, i]) => <jsx/>)}`).\n * - The existing `auto-wrap.ts` is regex-based and ships fine.\n * - Anything we cannot recognize is left untouched — the bridge has a\n * heuristic fallback for unmarked nodes, so partial coverage is\n * never wrong, only suboptimal.\n *\n * Idempotency:\n *\n * - If an element already has `data-saastro-field`, we leave it alone\n * (developer's manual marker wins).\n * - The schema `<script type=\"application/saastro-schema\">` body is\n * skipped — its content is JSON, not template.\n * - The section root carries `data-saastro=\"sec:…\"` and we never\n * stamp it again.\n */\n\nimport type {\n ComponentField,\n ComponentSchema,\n} from '@saastro/studio-core/component-schema/types'\n\nexport interface InstrumentFieldsOptions {\n /** Component file path — used for diagnostics only. */\n componentPath?: string\n}\n\nconst FRONTMATTER_RE = /^\\s*---\\s*\\n[\\s\\S]*?\\n---\\s*\\n/\n\n/**\n * Pure transform. Takes auto-wrapped Astro source + the schema (so we\n * know which identifiers are Props fields and what their nested shapes\n * are) and returns the source with `data-saastro-field` markers\n * injected.\n *\n * No-op when:\n * - schema has no fields (nothing to instrument)\n * - source has no `data-saastro=\"sec:` marker (auto-wrap didn't\n * instrument this file)\n */\nexport function instrumentFields(\n source: string,\n schema: ComponentSchema,\n _options: InstrumentFieldsOptions = {},\n): string {\n if (!schema.fields.length) return source\n if (!source.includes('data-saastro=')) return source\n\n const fmMatch = FRONTMATTER_RE.exec(source)\n if (!fmMatch) return source\n\n const templateStart = fmMatch[0].length\n const template = source.slice(templateStart)\n\n // Carve out the schema script body so we never poke around inside\n // its JSON. The simplest and safest approach is to split on the\n // marker, transform the head only, and rejoin.\n const schemaScriptStart = template.indexOf(\n '<script type=\"application/saastro-schema\"',\n )\n\n let head: string\n let tail: string\n if (schemaScriptStart >= 0) {\n head = template.slice(0, schemaScriptStart)\n tail = template.slice(schemaScriptStart)\n } else {\n head = template\n tail = ''\n }\n\n const transformed = transformTemplate(head, schema.fields)\n return source.slice(0, templateStart) + transformed + tail\n}\n\n/**\n * Top-level transform of the template body (everything between\n * frontmatter and the schema script). Order matters:\n *\n * 1. Array fields first — their `.map()` block is rewritten as a\n * whole, after which the inner JSX gets per-item markers.\n * 2. Plain (non-array) fields — `{ident}` references in the\n * remaining template.\n */\nfunction transformTemplate(\n template: string,\n fields: readonly ComponentField[],\n): string {\n let out = template\n\n // Step 1 — arrays with `.map()`.\n for (const field of fields) {\n if (field.kind !== 'array') continue\n out = transformArrayField(out, field)\n }\n\n // Step 2 — scalar / object / enum / etc. references.\n for (const field of fields) {\n if (field.kind === 'array') continue\n out = transformScalarField(out, field.name)\n }\n\n return out\n}\n\n/* ─────────────────────── Array .map() rewriting ─────────────────────── */\n\n/**\n * Rewrite `{<name>.map((item[, i]) => <jsx/>)}` so:\n *\n * - The iterator gains a second parameter `__i` (or reuses an existing\n * one, renaming it to `__i`).\n * - The first JSX element inside the body gets\n * `data-saastro-field={\\`<name>[\\${__i}]\\`}`.\n * - Each `{item.<sub>}` inside gets a wrapper / parent marker of\n * `<name>[\\${__i}].<sub>` (depending on single-child / multi-child).\n */\nfunction transformArrayField(\n template: string,\n field: ComponentField,\n): string {\n const name = field.name\n const subFields = field.items?.fields ?? []\n\n // Anchor on `<name> . map ( ( <param> [, <idx>] ) =>`. We deliberately\n // do NOT require a leading `{` here — the iterator is sometimes guarded\n // by a conditional shortcut (e.g. `{xs && xs.map(...)}`) and matching\n // the bare method call is enough to find every site we care about.\n const re = new RegExp(\n `\\\\b${escapeReg(name)}\\\\s*\\\\.\\\\s*map\\\\s*\\\\(\\\\s*\\\\(\\\\s*([A-Za-z_$][\\\\w$]*)\\\\s*(?:,\\\\s*([A-Za-z_$][\\\\w$]*)\\\\s*)?\\\\)\\\\s*=>\\\\s*`,\n 'g',\n )\n\n let result = ''\n let cursor = 0\n let m: RegExpExecArray | null\n while ((m = re.exec(template)) !== null) {\n const headerStart = m.index\n const paramName = m[1]!\n const existingIdx = m[2] ?? null\n const arrowEnd = headerStart + m[0].length\n\n const body = readArrowBody(template, arrowEnd)\n if (!body) continue\n\n let innerJsx = body.bodyText\n if (existingIdx && existingIdx !== '__i') {\n const renameRe = new RegExp(`\\\\b${escapeReg(existingIdx)}\\\\b`, 'g')\n innerJsx = innerJsx.replace(renameRe, '__i')\n }\n const transformedJsx = stampArrayItem(innerJsx, name, paramName, subFields)\n\n const headerNew = `${name}.map((${paramName}, __i) => `\n\n result += template.slice(cursor, headerStart) + headerNew\n if (body.parens) {\n result += '(' + transformedJsx + ')'\n } else {\n result += transformedJsx\n }\n // Preserve text from the end of the body (or its closing paren) up\n // to the close of the `.map(` call — that range carries any\n // user-authored whitespace / comments. Note we use `bodyCloseRel`\n // (which always points PAST the body's terminator, including the\n // wrapping `)` when present) rather than `bodyOffset + bodyText.length`\n // because the latter is whitespace-trimmed and would leak the\n // body's close paren back into the slice.\n const afterBody = arrowEnd + body.bodyCloseRel\n const callCloseAbsEnd = arrowEnd + body.totalEndRel\n result += template.slice(afterBody, callCloseAbsEnd)\n\n cursor = callCloseAbsEnd\n re.lastIndex = cursor\n }\n result += template.slice(cursor)\n return result\n}\n\n/**\n * Read the arrow function body starting at `start` (immediately after\n * `=>`). Two shapes supported:\n *\n * a) `( <jsx> )` — paren-wrapped expression. Walk balanced parens.\n * b) `<jsx />` — unwrapped JSX. Walk one element.\n *\n * Returns positional info so callers can slice and rebuild.\n */\nfunction readArrowBody(\n template: string,\n start: number,\n): null | {\n parens: boolean\n bodyOffset: number // start of bodyText relative to `start`\n bodyText: string\n /**\n * Offset past the close of the arrow body itself, relative to `start`.\n * For `parens: true` this is one past the `)` that wraps the body.\n * For `parens: false` this equals `bodyOffset + bodyText.length`.\n */\n bodyCloseRel: number\n totalEndRel: number // offset past the closing `)` of the .map call, relative to `start`\n} {\n // Skip whitespace\n let i = start\n while (i < template.length && /\\s/.test(template[i]!)) i++\n if (i >= template.length) return null\n\n let bodyStart: number\n let bodyEnd: number\n let parens: boolean\n\n if (template[i] === '(') {\n parens = true\n // Walk balanced parens to find the body's wrapping `(...)`.\n const inner = readBalanced(template, i, '(', ')')\n if (inner == null) return null\n bodyStart = i + 1\n bodyEnd = inner // index of matching ')'\n // Trim whitespace inside the parens.\n while (bodyStart < bodyEnd && /\\s/.test(template[bodyStart]!)) bodyStart++\n let trimEnd = bodyEnd\n while (trimEnd > bodyStart && /\\s/.test(template[trimEnd - 1]!)) trimEnd--\n bodyEnd = trimEnd\n\n // After the close paren, find the closing `)` of the `.map(` call.\n let j = inner + 1\n while (j < template.length && /\\s/.test(template[j]!)) j++\n if (template[j] !== ')') return null\n j++\n\n return {\n parens,\n bodyOffset: bodyStart - start,\n bodyText: template.slice(bodyStart, bodyEnd),\n bodyCloseRel: inner + 1 - start,\n totalEndRel: j - start,\n }\n }\n\n // Unwrapped JSX — read one element.\n if (template[i] === '<') {\n parens = false\n const elEnd = readJsxElement(template, i)\n if (elEnd == null) return null\n bodyStart = i\n bodyEnd = elEnd\n\n let j = bodyEnd\n while (j < template.length && /\\s/.test(template[j]!)) j++\n if (template[j] !== ')') return null\n j++\n\n return {\n parens,\n bodyOffset: bodyStart - start,\n bodyText: template.slice(bodyStart, bodyEnd),\n bodyCloseRel: bodyEnd - start,\n totalEndRel: j - start,\n }\n }\n\n return null\n}\n\n/**\n * Walk balanced delimiters starting at `i` (which points at `open`).\n * Returns the index of the matching close, or null if unbalanced.\n *\n * Aware of strings (single, double, template) and JSX expression braces.\n */\nfunction readBalanced(\n s: string,\n i: number,\n open: string,\n close: string,\n): number | null {\n if (s[i] !== open) return null\n let depth = 0\n let j = i\n while (j < s.length) {\n const ch = s[j]!\n if (ch === '\"' || ch === \"'\" || ch === '`') {\n j = skipString(s, j) + 1\n continue\n }\n if (ch === open) depth++\n else if (ch === close) {\n depth--\n if (depth === 0) return j\n }\n j++\n }\n return null\n}\n\nfunction skipString(s: string, i: number): number {\n const quote = s[i]!\n let j = i + 1\n while (j < s.length) {\n const ch = s[j]!\n if (ch === '\\\\') {\n j += 2\n continue\n }\n // Template literals can hold `${...}` — walk through them as balanced\n // braces. Strings inside the expression are themselves handled\n // recursively by the outer walker, but for our simple needs the\n // greedy approach is enough.\n if (quote === '`' && ch === '$' && s[j + 1] === '{') {\n const close = readBalanced(s, j + 1, '{', '}')\n if (close == null) return s.length - 1\n j = close + 1\n continue\n }\n if (ch === quote) return j\n j++\n }\n return s.length - 1\n}\n\n/**\n * Read one JSX element starting at `<`. Returns the index just past the\n * matching close tag (or self-close). Handles nested same-named tags.\n *\n * This is conservative — anything tricky causes a return of null, which\n * makes the caller skip the rewrite and leave the original text alone.\n */\nfunction readJsxElement(s: string, start: number): number | null {\n if (s[start] !== '<') return null\n // Read tag name\n let i = start + 1\n const nameMatch = /^[A-Za-z][A-Za-z0-9]*/.exec(s.slice(i))\n if (!nameMatch) return null\n const tag = nameMatch[0]\n i += tag.length\n\n // Walk attributes until we hit `>` or `/>`\n let inString: string | null = null\n let braceDepth = 0\n while (i < s.length) {\n const ch = s[i]!\n if (inString) {\n if (ch === '\\\\') {\n i += 2\n continue\n }\n if (inString === '`' && ch === '$' && s[i + 1] === '{') {\n const close = readBalanced(s, i + 1, '{', '}')\n if (close == null) return null\n i = close + 1\n continue\n }\n if (ch === inString) inString = null\n i++\n continue\n }\n if (braceDepth > 0) {\n if (ch === '{') braceDepth++\n else if (ch === '}') braceDepth--\n else if (ch === '\"' || ch === \"'\" || ch === '`') inString = ch\n i++\n continue\n }\n if (ch === '\"' || ch === \"'\" || ch === '`') {\n inString = ch\n i++\n continue\n }\n if (ch === '{') {\n braceDepth = 1\n i++\n continue\n }\n if (ch === '/' && s[i + 1] === '>') {\n // Self-closing — done.\n return i + 2\n }\n if (ch === '>') {\n i++\n // Now walk children until matching `</tag>`.\n let depth = 1\n const re = new RegExp(`<\\\\/?${tag}\\\\b`, 'g')\n re.lastIndex = i\n while (true) {\n const m = re.exec(s)\n if (!m) return null\n const isClose = s[m.index + 1] === '/'\n // Find the `>` that ends this tag, accounting for attributes.\n let k = m.index + (isClose ? 2 + tag.length : 1 + tag.length)\n let str: string | null = null\n let bd = 0\n while (k < s.length) {\n const c = s[k]!\n if (str) {\n if (c === '\\\\') {\n k += 2\n continue\n }\n if (c === str) str = null\n k++\n continue\n }\n if (bd > 0) {\n if (c === '{') bd++\n else if (c === '}') bd--\n k++\n continue\n }\n if (c === '\"' || c === \"'\" || c === '`') {\n str = c\n k++\n continue\n }\n if (c === '{') {\n bd = 1\n k++\n continue\n }\n if (c === '/' && s[k + 1] === '>') {\n k += 2\n break\n }\n if (c === '>') {\n k++\n break\n }\n k++\n }\n if (isClose) {\n depth--\n if (depth === 0) return k\n } else {\n // Self-closing siblings don't increment depth; check explicitly.\n // Heuristic — if `s[k - 2] === '/'` it was self-closing.\n if (s[k - 2] !== '/') depth++\n }\n re.lastIndex = k\n }\n }\n i++\n }\n return null\n}\n\n/**\n * Stamp markers inside the body of an array `.map()` callback:\n *\n * - The outer iteration element gets `data-saastro-field={\\`name[\\${__i}]\\`}`\n * - For each sub-field declared in the schema, find `{paramName.<sub>}`\n * and stamp `name[\\${__i}].<sub>` on the appropriate element (parent\n * for single-child, span wrap for multi-child).\n */\nfunction stampArrayItem(\n jsx: string,\n fieldName: string,\n paramName: string,\n subFields: readonly ComponentField[],\n): string {\n // Stamp the outer element first.\n let out = stampOuterElement(\n jsx,\n `{\\`${fieldName}[\\${__i}]\\`}`,\n )\n\n // Then per-sub-field references. The attribute VALUE we emit is a\n // JSX expression: `{`<path>[${__i}].<sub>`}` — caller passes the\n // full wrapped value so `injectAttr` can paste it as-is. The value\n // is always a literal `{…}` expression here (never a JSON-quoted\n // string) because we need to interpolate `${__i}` per iteration.\n for (const sub of subFields) {\n const propPath = `{\\`${fieldName}[\\${__i}].${sub.name}\\`}`\n out = transformDottedReference(out, paramName, sub.name, propPath)\n }\n return out\n}\n\n/**\n * Inject `data-saastro-field={…}` on the FIRST element of a JSX\n * fragment — used for the `.map()` iteration root. Skips elements that\n * already have a marker.\n */\nfunction stampOuterElement(jsx: string, valueExpr: string): string {\n // Find the first `<tag` that isn't a closing tag.\n const m = /<([A-Za-z][A-Za-z0-9]*)\\b([^<>]*?)(\\/?)>/.exec(jsx)\n if (!m) return jsx\n if (/data-saastro-field/.test(m[2]!)) return jsx\n const tag = m[1]\n const attrs = m[2]\n const selfClose = m[3]\n const replacement = `<${tag}${attrs} data-saastro-field=${valueExpr}${selfClose}>`\n return jsx.slice(0, m.index) + replacement + jsx.slice(m.index + m[0].length)\n}\n\n/* ─────────────────────── Scalar prop rewriting ─────────────────────── */\n\n/**\n * Find `{name}` references in the template and stamp the appropriate\n * element. Skips:\n *\n * - Compound expressions (`{name.foo}`, `{name || 'x'}`, `{name + 'y'}`)\n * - References inside attributes (those are handled separately for img)\n * - References on elements that already have `data-saastro-field`\n */\nfunction transformScalarField(template: string, name: string): string {\n return transformDottedReference(template, name, null, JSON.stringify(name))\n}\n\n/**\n * Generic per-reference rewriter shared between scalar props and array\n * sub-props.\n *\n * `valueAttr` is whatever string the caller wants pasted after the `=`\n * sign of the emitted attribute. The caller decides the quoting style:\n *\n * - scalar/static field name → caller passes `JSON.stringify(name)`,\n * yielding `data-saastro-field=\"title\"`.\n * - array sub-resource with interpolated index → caller passes a JSX\n * expression like `{`stats[${__i}].value`}`, yielding\n * `data-saastro-field={`stats[${__i}].value`}`.\n *\n * Both shapes are valid Astro template literals, so a single pasting\n * path covers both cases.\n *\n * @param paramOrName the identifier whose reference we look for. If\n * `subProp` is given, we look for `paramOrName.subProp`.\n * If `subProp` is null, we look for the bare\n * identifier `paramOrName`.\n * @param valueAttr the attribute value to emit (caller-quoted).\n */\nfunction transformDottedReference(\n template: string,\n paramOrName: string,\n subProp: string | null,\n valueAttr: string,\n): string {\n const refPattern = subProp\n ? new RegExp(\n `\\\\{\\\\s*${escapeReg(paramOrName)}\\\\s*\\\\.\\\\s*${escapeReg(subProp)}\\\\s*\\\\}`,\n 'g',\n )\n : new RegExp(`\\\\{\\\\s*${escapeReg(paramOrName)}\\\\s*\\\\}`, 'g')\n\n // Special-case: img tags with `src={name}` get marked on the img itself.\n // We handle this before the text-position pass so the image attribute\n // case wins over a hypothetical surrounding-element pass.\n let out = template\n if (!subProp) {\n out = stampImgSrc(out, paramOrName, valueAttr)\n }\n\n // Walk every plain `{ident}` (or `{param.sub}`) reference and stamp\n // the parent element.\n let result = ''\n let cursor = 0\n let m: RegExpExecArray | null\n refPattern.lastIndex = 0\n while ((m = refPattern.exec(out)) !== null) {\n const refStart = m.index\n const refEnd = refStart + m[0].length\n\n // Skip if this `{ident}` is inside an attribute value (e.g. src={image}).\n // Heuristic: walk backwards from refStart to the most recent `<` or `>`.\n // If we hit `<` first WITHOUT crossing a `>`, we're inside a tag.\n if (isInsideTag(out, refStart)) {\n result += out.slice(cursor, refEnd)\n cursor = refEnd\n continue\n }\n\n // Skip if `{name}` is preceded by `.` or followed by `.`/`(` —\n // signals a member access or call (e.g. `{name.toUpperCase()}`).\n // The regex itself captures only `{ \\s* name \\s* }` so this is\n // already guarded, but we double-check defensively.\n if (out[refEnd] === '.' || out[refStart - 1] === '.') {\n result += out.slice(cursor, refEnd)\n cursor = refEnd\n continue\n }\n\n // Find the enclosing element for this expression.\n const parent = findEnclosingElement(out, refStart)\n if (!parent) {\n result += out.slice(cursor, refEnd)\n cursor = refEnd\n continue\n }\n\n // Skip the section root — already has `data-saastro=\"sec:…\"`.\n if (parent.attrs.includes('data-saastro=')) {\n result += out.slice(cursor, refEnd)\n cursor = refEnd\n continue\n }\n // Skip if already marked.\n if (parent.attrs.includes('data-saastro-field')) {\n result += out.slice(cursor, refEnd)\n cursor = refEnd\n continue\n }\n\n // Single-child vs multi-child: count meaningful \"tokens\" of children.\n const childContent = out.slice(parent.bodyStart, parent.bodyEnd)\n const onlyChild = isOnlyMeaningfulExpression(childContent, refStart - parent.bodyStart, refEnd - parent.bodyStart)\n\n if (onlyChild) {\n // Inject `data-saastro-field=...` into parent's opening tag.\n const newOpen = injectAttr(parent.openTag, valueAttr)\n const replaced = out.slice(0, parent.openStart) + newOpen + out.slice(parent.openEnd)\n // Recompute cursor / refEnd offsets — easiest is to advance based\n // on length delta and continue from after the original ref.\n const delta = newOpen.length - parent.openTag.length\n result += replaced.slice(cursor, refEnd + delta)\n cursor = refEnd + delta\n out = replaced\n refPattern.lastIndex = cursor\n } else {\n // Wrap in `<span data-saastro-field=…>{name}</span>`.\n const wrapped = `<span data-saastro-field=${valueAttr}>${m[0]}</span>`\n result += out.slice(cursor, refStart) + wrapped\n cursor = refEnd\n // Update refPattern.lastIndex relative to current `out`.\n refPattern.lastIndex = cursor\n }\n }\n result += out.slice(cursor)\n return result\n}\n\n/**\n * Stamp `<img>` tags whose `src={name}` (or templated `src={\\`…${name}…\\`}`)\n * matches our prop name. The marker goes ON the img element itself.\n */\nfunction stampImgSrc(\n template: string,\n name: string,\n valueAttr: string,\n): string {\n // Match an <img …> tag with `src={...name...}` — we keep this loose so\n // helper-wrapped src values still get caught (`src={imageUrl(image)}`).\n const re = /<img\\b([^<>]*?)\\/?>/g\n let result = ''\n let cursor = 0\n let m: RegExpExecArray | null\n while ((m = re.exec(template)) !== null) {\n const attrs = m[1]!\n if (/data-saastro-field/.test(attrs)) continue\n const srcMatch = /\\bsrc\\s*=\\s*\\{([\\s\\S]*?)\\}/.exec(attrs)\n if (!srcMatch) continue\n const srcExpr = srcMatch[1]!\n if (!new RegExp(`\\\\b${escapeReg(name)}\\\\b`).test(srcExpr)) continue\n // Insert the marker.\n const newOpen = injectAttr(m[0], valueAttr)\n result += template.slice(cursor, m.index) + newOpen\n cursor = m.index + m[0].length\n re.lastIndex = cursor\n }\n result += template.slice(cursor)\n return result\n}\n\n/** Escape a string for use inside a RegExp. */\nfunction escapeReg(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\n/**\n * Inject a `data-saastro-field=…` attribute into an opening tag.\n * Idempotent (returns the tag unchanged when the attribute is already\n * present). `valueAttr` is pasted verbatim — the caller chooses the\n * quoting style (JSON-quoted string vs JSX expression) before passing.\n */\nfunction injectAttr(tag: string, valueAttr: string): string {\n if (/data-saastro-field/.test(tag)) return tag\n // We want the attribute right after the tag name for stable test diffs.\n const m = /^<([A-Za-z][A-Za-z0-9]*)/.exec(tag)\n if (!m) return tag\n const insertAt = m[0].length\n return tag.slice(0, insertAt) + ` data-saastro-field=${valueAttr}` + tag.slice(insertAt)\n}\n\n/** True when the position is inside a tag (between `<` and the next `>`). */\nfunction isInsideTag(s: string, pos: number): boolean {\n for (let i = pos - 1; i >= 0; i--) {\n const ch = s[i]!\n if (ch === '>') return false\n if (ch === '<') return true\n }\n return false\n}\n\n/**\n * Find the smallest element that fully contains `pos`. Returns the\n * positions of the open tag, the body, and the open tag's attribute\n * string. Returns null when no enclosing element can be located (the\n * caller leaves the reference unmarked in that case).\n */\nfunction findEnclosingElement(\n s: string,\n pos: number,\n): null | {\n openStart: number\n openEnd: number\n openTag: string\n attrs: string\n bodyStart: number\n bodyEnd: number\n} {\n // Walk backwards to find the closest `<tag …>` that opens before pos\n // and whose matching close is after pos. We do a linear scan: collect\n // every open/close tag before pos, then pop matching pairs.\n // For practical templates this is fast enough.\n const tagRe = /<(\\/?)([A-Za-z][A-Za-z0-9]*)\\b([^<>]*?)(\\/?)>/g\n type TagInfo = {\n isClose: boolean\n selfClose: boolean\n tagName: string\n attrs: string\n start: number\n end: number\n }\n const tags: TagInfo[] = []\n let m: RegExpExecArray | null\n while ((m = tagRe.exec(s)) !== null) {\n tags.push({\n isClose: m[1] === '/',\n selfClose: m[4] === '/',\n tagName: m[2]!,\n attrs: m[3]!,\n start: m.index,\n end: m.index + m[0].length,\n })\n }\n\n // Find the open tag whose body covers `pos`. Strategy: walk tags in\n // order; maintain a stack of open tags. When a close tag pops, if\n // pos is between the popped open's end and the close's start, we\n // have our parent — but we want the SMALLEST such element, so keep\n // a candidate and replace it whenever a deeper one is found.\n const stack: TagInfo[] = []\n let bestOpen: TagInfo | null = null\n let bestClose: TagInfo | null = null\n for (const t of tags) {\n if (t.selfClose) continue\n if (t.isClose) {\n // pop matching\n for (let i = stack.length - 1; i >= 0; i--) {\n if (stack[i]!.tagName === t.tagName) {\n const opener = stack[i]!\n // Drop everything above this index (mismatched, skip).\n stack.splice(i, stack.length - i)\n // Element runs from opener.end .. t.start.\n if (opener.end <= pos && pos < t.start) {\n // smallest: prefer the one with latest opener.end / earliest t.start\n if (\n !bestOpen ||\n opener.end > bestOpen.end ||\n (opener.end === bestOpen.end && t.start < bestClose!.start)\n ) {\n bestOpen = opener\n bestClose = t\n }\n }\n break\n }\n }\n } else {\n stack.push(t)\n }\n }\n\n if (!bestOpen || !bestClose) return null\n\n return {\n openStart: bestOpen.start,\n openEnd: bestOpen.end,\n openTag: s.slice(bestOpen.start, bestOpen.end),\n attrs: bestOpen.attrs,\n bodyStart: bestOpen.end,\n bodyEnd: bestClose.start,\n }\n}\n\n/**\n * Within the BODY of an element, decide whether the `{name}` expression\n * (located at relStart..relEnd within `body`) is the only meaningful\n * content. Whitespace and comments don't count. Anything else (text,\n * other expressions, sibling elements) makes it a multi-child case.\n */\nfunction isOnlyMeaningfulExpression(\n body: string,\n relStart: number,\n relEnd: number,\n): boolean {\n const before = body.slice(0, relStart)\n const after = body.slice(relEnd)\n return /^\\s*$/.test(before) && /^\\s*$/.test(after)\n}\n","/**\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"],"mappings":"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","names":["Project","FRONTMATTER_RE","FRONTMATTER_RE","FIRST_OPEN_TAG_RE","FRONTMATTER_RE"]}
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  saastro
3
- } from "./chunk-AW6UON7J.js";
4
- import "./chunk-IBV2VRJU.js";
3
+ } from "./chunk-IPUVRBJI.js";
4
+ import "./chunk-XVVAAIXK.js";
5
5
 
6
6
  // src/helpers/editable-section.ts
7
7
  function editableSection(fieldPrefix) {
@@ -10,21 +10,15 @@ type SaastroStudioOptions = StudioAutoWrapOptions;
10
10
  * integrations: [saastro({ autoWrap: true })],
11
11
  * })
12
12
  *
13
- * Why an integration (vs adding the Vite plugin directly):
13
+ * This integration is sugar: it registers the underlying Vite plugin
14
+ * (`@saastro/studio/vite`) so users only need one import. The plugin's
15
+ * ordering correctness comes from using a `load` hook (which always runs
16
+ * before any `transform`, including Astro's `.astro` compiler) — NOT
17
+ * from where the integration slots itself in. See the comment block in
18
+ * `../vite/index.ts` for the full rationale.
14
19
  *
15
- * Astro 6 compiles `.astro` files via its own internal Vite plugin. A bare
16
- * `vite.plugins: [studioAutoWrap()]` entry races that internal plugin on cold
17
- * builds — sometimes our `enforce: 'pre'` runs first and sees raw frontmatter,
18
- * sometimes Astro's compiler runs first and we see compiled JSX-like output,
19
- * which the call-site scanner can't read.
20
- *
21
- * Registering through `astro:config:setup → updateConfig` lets Astro slot our
22
- * plugin into a deterministic position in the pipeline, ahead of its own
23
- * `.astro` transforms. That's the only fix we need — everything else stays in
24
- * the Vite plugin we already ship.
25
- *
26
- * The `@saastro/studio/vite` entry remains exported for back-compat with the
27
- * sites that wired it directly (e.g. enlolab/dorjoiers).
20
+ * The `@saastro/studio/vite` entry remains exported for sites that wired
21
+ * the bare Vite plugin directly.
28
22
  */
29
23
  declare function saastro(options?: SaastroStudioOptions): AstroIntegration;
30
24
 
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  saastro
3
- } from "../chunk-AW6UON7J.js";
4
- import "../chunk-IBV2VRJU.js";
3
+ } from "../chunk-IPUVRBJI.js";
4
+ import "../chunk-XVVAAIXK.js";
5
5
  export {
6
6
  saastro as default
7
7
  };
@@ -11,21 +11,37 @@ import { S as StudioAutoWrapOptions } from '../types-JUOAAaUW.js';
11
11
  * vite: { plugins: [studioAutoWrap({ autoWrap: true })] },
12
12
  * });
13
13
  *
14
+ * ## Why `load` and not `transform`?
15
+ *
16
+ * Astro 6's `astro:build` plugin compiles `.astro` source → JS inside its
17
+ * own `transform` hook (`vite-plugin-astro/index.js`, line ~204). Both
18
+ * plugins use `enforce: 'pre'`, so ordering falls back to insertion
19
+ * order — and Astro's plugin is registered before any user
20
+ * integration's plugins, so its transform always runs first. The result:
21
+ * by the time our transform fires, `code` is already compiled JS and our
22
+ * regex-on-frontmatter pipeline can't match anything.
23
+ *
24
+ * Vite runs all `load` hooks BEFORE any `transform`, regardless of
25
+ * `enforce`. Astro's own `load` filters strictly to virtual sub-requests
26
+ * (`/[?&]astro([&=]|$)/`), so the main `.astro` module falls through to
27
+ * the default fs loader. We slot ourselves in there: read the file,
28
+ * apply the regex pipeline to raw `.astro` source (with frontmatter
29
+ * intact), return the modified raw source. Astro's transform then
30
+ * compiles our edited source as if it were the original on disk.
31
+ *
14
32
  * What the plugin does:
15
- * 1. (enforce: 'pre') Scans .astro source for `fieldPrefix` prop and injects
33
+ * 1. (load) Scans `.astro` source for `fieldPrefix` prop and injects
16
34
  * `data-saastro="sec:${fieldPrefix}"` on the root element.
17
- * 2. (enforce: 'pre') When step 1 mutates the file, extracts a
18
- * `ComponentSchema` from the file's `interface Props` (via ts-morph in
19
- * `@saastro/studio-core`) and bakes it into a
35
+ * 2. (load) Extracts a `ComponentSchema` from the file's `interface Props`
36
+ * (via ts-morph in `@saastro/studio-core`) and bakes it into a
20
37
  * `<script type="application/saastro-schema" data-prefix="…">` placed
21
- * just before the section's root closing tag. The overlay reads it at
22
- * runtime so the Studio panel doesn't need a server round-trip.
23
- * 3. (enforce: 'pre') Walks the template and stamps each rendered
24
- * `Props` field with `data-saastro-field="<propPath>"` so the Studio
25
- * overlay can mutate the exact DOM node by name (no role heuristic).
26
- * Currently a pass-through stub; full AST traversal lands in Phase 2.
27
- * 4. (transformIndexHtml) Injects <meta name="saastro:studio-version"> into
28
- * every HTML page's <head> so the Hub can detect protocol version.
38
+ * just before the section's root closing tag. The overlay reads it
39
+ * at runtime so the Studio panel doesn't need a server round-trip.
40
+ * 3. (load) Walks the template and stamps each rendered `Props` field
41
+ * with `data-saastro-field="<propPath>"` so the Studio overlay can
42
+ * mutate the exact DOM node by name.
43
+ * 4. (transformIndexHtml) Injects <meta name="saastro:studio-version">
44
+ * into every HTML page's <head> so the Hub can detect protocol version.
29
45
  */
30
46
  declare function studioAutoWrap(opts?: StudioAutoWrapOptions): Plugin;
31
47
 
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  studioAutoWrap
3
- } from "../chunk-IBV2VRJU.js";
3
+ } from "../chunk-XVVAAIXK.js";
4
4
  export {
5
5
  studioAutoWrap as default
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saastro/studio",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Build-time instrumentation for Saastro Studio — Vite plugin + editable helpers for Astro sites managed by Saastro Hub",
5
5
  "license": "MIT",
6
6
  "author": "SAASTRO",
@@ -38,6 +38,15 @@
38
38
  "files": [
39
39
  "dist"
40
40
  ],
41
+ "scripts": {
42
+ "build": "tsup",
43
+ "dev": "tsup --watch",
44
+ "typecheck": "tsc --noEmit",
45
+ "test": "vitest run",
46
+ "test:watch": "vitest",
47
+ "clean": "rm -rf dist",
48
+ "prepublishOnly": "pnpm run build"
49
+ },
41
50
  "peerDependencies": {
42
51
  "astro": "^5.0.0 || ^6.0.0",
43
52
  "vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
@@ -51,18 +60,10 @@
51
60
  "ts-morph": "^25.0.1"
52
61
  },
53
62
  "devDependencies": {
63
+ "@saastro/studio-core": "workspace:*",
54
64
  "tsup": "^8.5.1",
55
65
  "typescript": "^5.8.3",
56
66
  "vite": "^7.0.0",
57
- "vitest": "^3.2.4",
58
- "@saastro/studio-core": "0.0.1"
59
- },
60
- "scripts": {
61
- "build": "tsup",
62
- "dev": "tsup --watch",
63
- "typecheck": "tsc --noEmit",
64
- "test": "vitest run",
65
- "test:watch": "vitest",
66
- "clean": "rm -rf dist"
67
+ "vitest": "^3.2.4"
67
68
  }
68
- }
69
+ }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/integration/index.ts"],"sourcesContent":["import type { AstroIntegration } from 'astro'\nimport studioAutoWrap from '../vite/index.js'\nimport type { StudioAutoWrapOptions } from '../vite/types.js'\n\nexport type SaastroStudioOptions = StudioAutoWrapOptions\n\n/**\n * Astro Integration for Saastro Studio.\n *\n * import saastro from '@saastro/studio'\n * export default defineConfig({\n * integrations: [saastro({ autoWrap: true })],\n * })\n *\n * Why an integration (vs adding the Vite plugin directly):\n *\n * Astro 6 compiles `.astro` files via its own internal Vite plugin. A bare\n * `vite.plugins: [studioAutoWrap()]` entry races that internal plugin on cold\n * builds — sometimes our `enforce: 'pre'` runs first and sees raw frontmatter,\n * sometimes Astro's compiler runs first and we see compiled JSX-like output,\n * which the call-site scanner can't read.\n *\n * Registering through `astro:config:setup → updateConfig` lets Astro slot our\n * plugin into a deterministic position in the pipeline, ahead of its own\n * `.astro` transforms. That's the only fix we need — everything else stays in\n * the Vite plugin we already ship.\n *\n * The `@saastro/studio/vite` entry remains exported for back-compat with the\n * sites that wired it directly (e.g. enlolab/dorjoiers).\n */\nexport default function saastro(options: SaastroStudioOptions = {}): AstroIntegration {\n return {\n name: '@saastro/studio',\n hooks: {\n 'astro:config:setup': ({ updateConfig }) => {\n updateConfig({\n vite: {\n plugins: [studioAutoWrap(options)],\n },\n })\n },\n },\n }\n}\n"],"mappings":";;;;;AA8Be,SAAR,QAAyB,UAAgC,CAAC,GAAqB;AACpF,SAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO;AAAA,MACL,sBAAsB,CAAC,EAAE,aAAa,MAAM;AAC1C,qBAAa;AAAA,UACX,MAAM;AAAA,YACJ,SAAS,CAAC,eAAe,OAAO,CAAC;AAAA,UACnC;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/vite/auto-wrap.ts","../../studio-core/src/component-schema/extract.ts","../../studio-core/src/component-schema/extract-bindings.ts","../src/vite/extract-props-schema.ts","../src/vite/inject-schema.ts","../src/vite/instrument-fields.ts","../src/vite/meta-tag.ts","../src/vite/index.ts"],"sourcesContent":["/**\n * Studio auto-wrap transform — Studio v5.2 §M1.2.\n *\n * Scans `.astro` files for a `fieldPrefix` prop and injects\n * `data-saastro=\"sec:${fieldPrefix}\"` on the template's root element.\n *\n * Approach: regex-based source transformation (enforce: 'pre', runs before\n * Astro compiles the template). Covers 95%+ of real-world Astro components.\n *\n * ## Limitations\n * - Component roots (PascalCase tags) are skipped — the marker must land\n * on a rendered HTML element so the overlay can hit-test it.\n * - Top-level branching (`{cond ? <A/> : <B/>}`) is skipped; users can\n * opt-in manually with `editableSection()`.\n */\n\n// Matches the frontmatter block at the very top of the file.\nconst FRONTMATTER_RE = /^\\s*---\\s*\\n([\\s\\S]*?)\\n---\\s*\\n/\n\n// Matches both HTML attribute form (`data-saastro=`) and the JSX spread\n// form we inject (`\"data-saastro\":`). Plain substring check is enough —\n// `data-saastro` is unique to this instrumentation.\nconst SAASTRO_ATTR_RE = /data-saastro/\n\n/**\n * Detects any mention of `fieldPrefix` in the frontmatter.\n * Conservative: a comment mentioning `fieldPrefix` is a false-positive,\n * but the runtime guard (`fieldPrefix ? ... : {}`) makes it safe.\n */\nconst FIELD_PREFIX_REFERENCE_RE = /\\bfieldPrefix\\b/\n\n/**\n * Matches the first tag opening after the frontmatter. Group 1 = tag name\n * (either HTML lowercase or component PascalCase), group 2 = existing\n * attributes, group 3 = self-closing slash. The lowercase check in\n * `transformAstroSource` decides whether to inject or skip.\n */\nconst FIRST_OPEN_TAG_RE = /<([a-zA-Z][a-zA-Z0-9]*)\\b([^<>]*?)\\s*(\\/?)>/\n\n/**\n * Attribute spread injected on the root element. Uses conditional form\n * so no `data-saastro` attribute appears in production HTML when the\n * prop is falsy (avoids `data-saastro=\"sec:\"` partial matches).\n *\n * H9 — also emit `draggable=\"true\"` so HTML5 drag-and-drop fires\n * `dragstart` on the section root. Spec v5.2 §M1.6 promised section-level\n * drag reorder; the overlay's drag listeners (overlay-script.ts) were\n * wired but never received events because the wrappers weren't draggable.\n */\nconst INJECTION =\n ' {...(fieldPrefix ? { \"data-saastro\": `sec:${fieldPrefix}`, draggable: \"true\" } : {})}'\n\n/**\n * Transform a single .astro source string. Exported for unit tests;\n * the plugin wraps this with file-system + Vite metadata.\n */\nexport function transformAstroSource(source: string): string {\n const fm = FRONTMATTER_RE.exec(source)\n if (!fm) return source\n\n if (!FIELD_PREFIX_REFERENCE_RE.test(fm[1]!)) return source\n\n const template = source.slice(fm[0].length)\n const tagMatch = FIRST_OPEN_TAG_RE.exec(template)\n if (!tagMatch) return source\n\n const tagName = tagMatch[1]!\n if (tagName[0]! !== tagName[0]!.toLowerCase()) return source\n\n const existingAttrs = tagMatch[2]!\n if (SAASTRO_ATTR_RE.test(existingAttrs)) return source\n\n const selfClose = tagMatch[3]!\n const replacement = `<${tagName}${existingAttrs}${INJECTION}${selfClose ? ' ' + selfClose : ''}>`\n const templateInjected =\n template.slice(0, tagMatch.index) +\n replacement +\n template.slice(tagMatch.index + tagMatch[0]!.length)\n\n return source.slice(0, fm[0].length) + templateInjected\n}\n","/**\n * Extract a `ComponentSchema` from a single `.astro` source file.\n *\n * The shape we care about lives in the frontmatter — the block between\n * the opening `---` and closing `---` lines. We feed the frontmatter into\n * ts-morph (TypeScript's AST library) so we get real type resolution\n * instead of the brittle regex matching the Vite plugin uses today for\n * the `fieldPrefix` marker injection.\n *\n * Why ts-morph and not a regex:\n * - The customer's `interface Props` may import types from sibling files\n * (`import type { StatItem } from './types'`).\n * - Property types may be unions of string literals (`'left' | 'center'`).\n * - Optional/required, nullable, default-bearing properties all need to\n * round-trip into the panel correctly.\n *\n * What we deliberately don't do here:\n * - Parse the Astro template body — we only care about the type contract.\n * Per-prop DOM markers (`data-saastro-field`) are a separate transform\n * in the Vite plugin (Phase 2 of the rollout plan).\n * - Run any I/O. The caller (Vite plugin or runtime fallback) is in\n * charge of locating the source and importing related .ts files into\n * the same in-memory ts-morph project.\n */\n\nimport {\n Project,\n SourceFile,\n Type,\n type InterfaceDeclaration,\n type JSDocableNode,\n type Node,\n type Symbol as TsSymbol,\n type TypeAliasDeclaration,\n} from 'ts-morph'\nimport type {\n ComponentField,\n ComponentSchema,\n FieldKind,\n} from './types.js'\n\n/**\n * Allowed kinds in `@studio:<kind>` JSDoc directives. The customer can\n * sprinkle these on individual `Props` properties to override the\n * name-based heuristic used by `pickStringKind` and friends. Compatibility\n * with the resolved type is enforced in `applyDirective` — a directive\n * that conflicts with the type is silently ignored (easier to debug than\n * a build-time throw, and the panel still shows something sensible).\n *\n * Shorthand: `@studio:image` is accepted as an alias for `image-url`,\n * since \"image\" reads more naturally on a property declaration.\n */\nconst STUDIO_DIRECTIVE_KINDS: ReadonlySet<FieldKind> = new Set<FieldKind>([\n 'string',\n 'textarea',\n 'image-url',\n 'enum',\n 'number',\n 'boolean',\n 'array',\n 'object',\n 'json',\n])\n\nconst STUDIO_DIRECTIVE_RE = /@studio:([A-Za-z][A-Za-z0-9-]*)/g\n\n/**\n * Property-name patterns we treat as image-shaped strings. The list is\n * intentionally conservative — adding \"hero\" here once caused EVERY\n * field on a hero section to flip to the image-url widget. Hints\n * describe the FIELD's role, not the section it lives in.\n */\nconst IMAGE_NAME_HINTS = /(^|_)(image|img|avatar|logo|icon|thumbnail|photo|picture|cover|background)($|_)/i\nconst IMAGE_SUFFIX_HINTS = /_(url|src)$/i\n\nconst FRONTMATTER_RE = /^\\s*---\\s*\\n([\\s\\S]*?)\\n---/\n\nexport interface ExtractOptions {\n /**\n * Path of the .astro file relative to the project root. Used only for\n * diagnostics and to populate `ComponentSchema.componentPath`.\n */\n componentPath?: string\n /**\n * Base directory the source file lives in. Affects how ts-morph\n * resolves relative imports (`import type { Foo } from './foo'`).\n * Defaults to the in-memory virtual root, which means imports won't\n * resolve unless the caller pre-loaded those files into the same\n * Project.\n */\n baseDir?: string\n /**\n * Caller-supplied ts-morph Project. Reusing one across extractions in\n * the same plugin run is cheaper because TypeScript can share the\n * type-checker cache. If omitted we spin up a fresh in-memory one.\n */\n project?: Project\n}\n\n/**\n * Pull the frontmatter out of a .astro source string. Returns null when\n * the file has no frontmatter block (a pure-template Astro component).\n */\nexport function extractFrontmatter(astroSource: string): string | null {\n const m = FRONTMATTER_RE.exec(astroSource)\n return m ? (m[1] ?? '') : null\n}\n\n/**\n * Main entry point. Takes the raw `.astro` source, returns a schema\n * (or null when the file has no `interface Props`). Errors during\n * parsing are caught and returned as null — the caller decides whether\n * to fall back to the catalog.\n */\nexport function extractComponentSchema(\n astroSource: string,\n options: ExtractOptions = {},\n): ComponentSchema | null {\n const frontmatter = extractFrontmatter(astroSource)\n if (frontmatter == null) return null\n\n const project = options.project ?? createProject()\n // Virtual filename — ts-morph ignores extensions for lookup, but using\n // .ts here keeps any error message readable.\n const virtualName = options.componentPath\n ? `${options.componentPath}.frontmatter.ts`\n : 'component.frontmatter.ts'\n\n let sourceFile: SourceFile\n try {\n sourceFile = project.createSourceFile(virtualName, frontmatter, {\n overwrite: true,\n })\n } catch {\n return null\n }\n\n let propsDecl: InterfaceDeclaration | TypeAliasDeclaration | undefined =\n sourceFile.getInterface('Props')\n if (!propsDecl) {\n propsDecl = sourceFile.getTypeAlias('Props')\n }\n if (!propsDecl) {\n sourceFile.delete()\n return null\n }\n\n const propsType = propsDecl.getType()\n const fields: ComponentField[] = []\n for (const prop of propsType.getProperties()) {\n const field = describeProperty(prop, propsDecl)\n if (field) fields.push(field)\n }\n\n // Parse default values from the destructuring assignment if present.\n // Pattern matched: `const { title = 'foo', stats = [] } = Astro.props`.\n applyDefaultsFromDestructuring(frontmatter, fields)\n\n sourceFile.delete()\n\n return {\n fields,\n componentPath: options.componentPath,\n version: 1,\n }\n}\n\nfunction createProject(): Project {\n return new Project({\n useInMemoryFileSystem: true,\n skipFileDependencyResolution: true,\n compilerOptions: {\n strict: true,\n // Match the customer's typical Astro tsconfig — ESNext + node16\n // resolution covers most setups. Misses are non-fatal: unresolved\n // imports surface as `'json'` kind with a description.\n target: 99,\n module: 199,\n moduleResolution: 100,\n allowSyntheticDefaultImports: true,\n esModuleInterop: true,\n skipLibCheck: true,\n },\n })\n}\n\ninterface PropertyParent {\n getSourceFile(): SourceFile\n}\n\nfunction describeProperty(\n prop: TsSymbol,\n parent: PropertyParent,\n): ComponentField | null {\n const name = prop.getName()\n const declarations = prop.getDeclarations()\n const decl = declarations[0]\n if (!decl) return null\n\n // Optional flag: `prop?: T` OR `prop: T | undefined`\n const isQuestionToken = (\n decl as unknown as { hasQuestionToken?: () => boolean }\n ).hasQuestionToken?.() ?? false\n\n const propsSourceFile = parent.getSourceFile()\n const type = prop.getTypeAtLocation(propsSourceFile)\n\n const optional =\n isQuestionToken || typeIncludes(type, t => t.isUndefined())\n\n const directive = readStudioDirective(decl)\n\n // ts-morph models `boolean` as the union `true | false`, so the\n // generic union path below would mis-classify it as heterogeneous.\n // Short-circuit primitives that survive a `T | undefined` wrapper.\n if (type.isBoolean() || stripUndefined(type).isBoolean()) {\n return applyDirective({ name, kind: 'boolean', optional }, directive, type)\n }\n\n // Strip undefined / null while preserving the rest of the union so\n // string-literal enums like `'left' | 'center' | undefined` keep all\n // their literals.\n const nonNullishParts = type.isUnion()\n ? type.getUnionTypes().filter(t => !t.isUndefined() && !t.isNull())\n : [type]\n\n // 1. String literal union → enum\n if (\n nonNullishParts.length >= 1 &&\n nonNullishParts.every(t => t.isStringLiteral())\n ) {\n const enumValues = nonNullishParts.map(t =>\n String(t.getLiteralValue()),\n )\n if (enumValues.length === 1) {\n // Single literal isn't an enum — describe as a normal string.\n const f = describeType(name, nonNullishParts[0]!)\n return applyDirective(\n { ...f, optional: f.optional || optional },\n directive,\n type,\n )\n }\n return applyDirective(\n { name, kind: 'enum', optional, enumValues },\n directive,\n type,\n )\n }\n\n // 2. Single non-nullish branch → describe that branch.\n if (nonNullishParts.length === 1) {\n const f = describeType(name, nonNullishParts[0]!)\n return applyDirective(\n { ...f, optional: f.optional || optional },\n directive,\n type,\n )\n }\n\n // 3. Heterogeneous union (string | number, etc.) — too ambiguous to\n // pick a control. Fall back to json with the raw type text.\n return applyDirective(\n {\n name,\n kind: 'json',\n optional,\n description: `unparsed: ${type.getText()}`,\n },\n directive,\n type,\n )\n}\n\n/**\n * Read the first `@studio:<kind>` JSDoc directive on a node. Returns\n * the canonicalised `FieldKind` or null if none/unknown. Walks ts-morph\n * `getJsDocs()` plus the leading-comment fallback (older ts-morph\n * versions don't always thread JSDoc onto every PropertySignature).\n */\nfunction readStudioDirective(decl: Node): FieldKind | null {\n const sources: string[] = []\n\n const jsDocable = decl as Node & Partial<JSDocableNode>\n if (typeof jsDocable.getJsDocs === 'function') {\n for (const doc of jsDocable.getJsDocs()) {\n sources.push(doc.getText())\n }\n }\n\n // Fallback: scan leading trivia in case JSDoc isn't attached as a\n // first-class node (happens for some property signature shapes).\n if (sources.length === 0) {\n const ranges =\n (decl as unknown as {\n getLeadingCommentRanges?: () => { getText(): string }[]\n }).getLeadingCommentRanges?.() ?? []\n for (const range of ranges) {\n const text = range.getText()\n if (text.includes('@studio:')) sources.push(text)\n }\n }\n\n for (const source of sources) {\n STUDIO_DIRECTIVE_RE.lastIndex = 0\n let match: RegExpExecArray | null\n while ((match = STUDIO_DIRECTIVE_RE.exec(source)) !== null) {\n const raw = match[1]\n if (!raw) continue\n // Shorthand: `@studio:image` → `image-url`.\n const kind = (raw === 'image' ? 'image-url' : raw) as FieldKind\n if (STUDIO_DIRECTIVE_KINDS.has(kind)) return kind\n // Unknown kind — skip and keep looking. First valid match wins.\n }\n }\n\n return null\n}\n\n/**\n * Override `field.kind` when the directive is compatible with the\n * already-resolved type. Incompatible directives are dropped silently —\n * the panel renders the heuristic-picked control instead, which is far\n * easier to debug than a build-time throw.\n */\nfunction applyDirective(\n field: ComponentField,\n directive: FieldKind | null,\n type: Type,\n): ComponentField {\n if (!directive) return field\n if (!isDirectiveCompatible(directive, type)) return field\n if (field.kind === directive) return field\n return { ...field, kind: directive }\n}\n\nfunction isDirectiveCompatible(directive: FieldKind, type: Type): boolean {\n // `json` is the universal escape hatch.\n if (directive === 'json') return true\n\n const stripped = stripUndefined(type)\n const nonNullishParts = type.isUnion()\n ? type.getUnionTypes().filter(t => !t.isUndefined() && !t.isNull())\n : [stripped]\n\n switch (directive) {\n case 'string':\n case 'textarea':\n case 'image-url':\n return nonNullishParts.every(t => t.isString() || t.isStringLiteral())\n case 'enum':\n return nonNullishParts.every(t => t.isString() || t.isStringLiteral())\n case 'number':\n return nonNullishParts.every(t => t.isNumber() || t.isNumberLiteral())\n case 'boolean':\n return type.isBoolean() || stripped.isBoolean()\n case 'array':\n return nonNullishParts.every(t => t.isArray())\n case 'object':\n return nonNullishParts.every(t => isDescendableObject(t))\n default:\n return false\n }\n}\n\n/**\n * Built-in classes / brand types we never want to descend into. They\n * have member properties (e.g. RegExp.lastIndex, Date.getTime) that\n * would confuse the panel, so we surface them as a json fallback\n * instead of an object form.\n */\nconst NON_DESCENDABLE_TYPE_NAMES = new Set([\n 'RegExp',\n 'Date',\n 'Map',\n 'Set',\n 'WeakMap',\n 'WeakSet',\n 'Promise',\n 'Error',\n 'URL',\n 'URLSearchParams',\n 'ArrayBuffer',\n 'Uint8Array',\n])\n\nfunction isCallable(type: Type): boolean {\n return type.getCallSignatures().length > 0\n}\n\nfunction isDescendableObject(type: Type): boolean {\n if (!type.isObject() && !type.isInterface()) return false\n if (isCallable(type)) return false\n const symbol = type.getSymbol() ?? type.getAliasSymbol()\n const symbolName = symbol?.getName()\n if (symbolName && NON_DESCENDABLE_TYPE_NAMES.has(symbolName)) return false\n return type.getProperties().length > 0\n}\n\nfunction describeType(name: string, type: Type): ComponentField {\n // Order matters: literal first (single literal kept as enum-of-1 might\n // be useful but we treat as string for sanity), then primitives,\n // arrays, descendable objects, fallback.\n\n if (type.isStringLiteral()) {\n return { name, kind: 'string', optional: false }\n }\n\n if (\n type.isUnion() &&\n type.getUnionTypes().every(t => t.isStringLiteral())\n ) {\n const values = type\n .getUnionTypes()\n .map(t => String(t.getLiteralValue()))\n return { name, kind: 'enum', optional: false, enumValues: values }\n }\n\n if (type.isString()) {\n return { name, kind: pickStringKind(name), optional: false }\n }\n if (type.isNumber()) return { name, kind: 'number', optional: false }\n if (type.isBoolean()) return { name, kind: 'boolean', optional: false }\n\n if (type.isArray()) {\n const elementType = type.getArrayElementType()\n if (elementType) {\n const itemDescriptor = describeAnyType('__item__', elementType)\n return { name, kind: 'array', optional: false, items: itemDescriptor }\n }\n return { name, kind: 'array', optional: false }\n }\n\n if (isDescendableObject(type)) {\n const fields: ComponentField[] = []\n for (const sub of type.getProperties()) {\n const subDecl = sub.getDeclarations()[0]\n if (!subDecl) continue\n const subType = sub.getTypeAtLocation(subDecl)\n const subOptional =\n ((subDecl as unknown as { hasQuestionToken?: () => boolean })\n .hasQuestionToken?.() ?? false) ||\n typeIncludes(subType, t => t.isUndefined())\n const sf = describeAnyType(sub.getName(), subType)\n const subDirective = readStudioDirective(subDecl)\n fields.push(\n applyDirective(\n { ...sf, optional: sf.optional || subOptional },\n subDirective,\n subType,\n ),\n )\n }\n if (fields.length > 0) {\n return { name, kind: 'object', optional: false, fields }\n }\n }\n\n return {\n name,\n kind: 'json',\n optional: false,\n description: `unparsed: ${type.getText()}`,\n }\n}\n\n/**\n * Like `describeType` but also handles the optional / null-stripping\n * pre-pass — used when descending into array items or nested object\n * properties (where we don't have a question-token to look at).\n */\nfunction describeAnyType(name: string, type: Type): ComponentField {\n const optional = typeIncludes(type, t => t.isUndefined())\n const nonNullishParts = type.isUnion()\n ? type.getUnionTypes().filter(t => !t.isUndefined() && !t.isNull())\n : [type]\n\n if (\n nonNullishParts.length >= 1 &&\n nonNullishParts.every(t => t.isStringLiteral())\n ) {\n if (nonNullishParts.length === 1) {\n return { ...describeType(name, nonNullishParts[0]!), optional }\n }\n return {\n name,\n kind: 'enum',\n optional,\n enumValues: nonNullishParts.map(t => String(t.getLiteralValue())),\n }\n }\n\n if (nonNullishParts.length === 1) {\n const f = describeType(name, nonNullishParts[0]!)\n return { ...f, optional: f.optional || optional }\n }\n\n return {\n name,\n kind: 'json',\n optional,\n description: `unparsed: ${type.getText()}`,\n }\n}\n\nfunction pickStringKind(name: string): FieldKind {\n if (IMAGE_NAME_HINTS.test(name) || IMAGE_SUFFIX_HINTS.test(name)) {\n return 'image-url'\n }\n // Long-form heuristic — only the name is available without runtime\n // length info. Matches the existing zod-introspect behavior.\n if (/description|body|content|copy|summary/i.test(name)) {\n return 'textarea'\n }\n return 'string'\n}\n\nfunction typeIncludes(type: Type, predicate: (t: Type) => boolean): boolean {\n if (predicate(type)) return true\n if (type.isUnion()) {\n return type.getUnionTypes().some(predicate)\n }\n return false\n}\n\nfunction stripUndefined(type: Type): Type {\n if (!type.isUnion()) return type\n const remaining = type\n .getUnionTypes()\n .filter(t => !t.isUndefined() && !t.isNull())\n // We just need a representative type for primitive checks — return any\n // of the surviving members. The full extraction path uses\n // `nonNullishParts` (an array) for completeness.\n return remaining[0] ?? type\n}\n\n/**\n * Best-effort: parse `const { foo = X, bar = Y } = Astro.props` from the\n * frontmatter and stamp `defaultValue` on matching fields. Pure regex —\n * we don't try to evaluate the RHS, just record it as a string for the\n * panel's placeholder.\n */\nfunction applyDefaultsFromDestructuring(\n frontmatter: string,\n fields: ComponentField[],\n): void {\n const match = /const\\s*\\{([\\s\\S]*?)\\}\\s*=\\s*Astro\\.props/.exec(frontmatter)\n if (!match) return\n const body = match[1] ?? ''\n // Naive split on top-level commas — sufficient for `name = 'foo'`,\n // `count = 0`, `flag = false`. Object/array defaults will fall through.\n const entries = splitTopLevel(body, ',')\n for (const entry of entries) {\n const eq = entry.indexOf('=')\n if (eq < 0) continue\n const lhs = entry.slice(0, eq).trim()\n const rhs = entry.slice(eq + 1).trim()\n if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(lhs)) continue\n const target = fields.find(f => f.name === lhs)\n if (!target) continue\n target.defaultValue = parseLiteral(rhs)\n }\n}\n\nfunction splitTopLevel(s: string, sep: string): string[] {\n const out: string[] = []\n let depth = 0\n let current = ''\n for (const ch of s) {\n if (ch === '{' || ch === '[' || ch === '(') depth++\n else if (ch === '}' || ch === ']' || ch === ')') depth--\n if (ch === sep && depth === 0) {\n out.push(current)\n current = ''\n } else {\n current += ch\n }\n }\n if (current.trim()) out.push(current)\n return out\n}\n\nfunction parseLiteral(rhs: string): unknown {\n const trimmed = rhs.trim()\n if (trimmed === 'true') return true\n if (trimmed === 'false') return false\n if (trimmed === 'null') return null\n if (/^-?\\d+(\\.\\d+)?$/.test(trimmed)) return Number(trimmed)\n // Strip wrapping quotes.\n const quoted = /^(['\"`])([\\s\\S]*)\\1$/.exec(trimmed)\n if (quoted) return quoted[2]\n // Anything else (object, array, identifier reference) — keep the raw\n // source text so the panel can show it as a placeholder string.\n return trimmed\n}\n","/**\n * Extract per-prop call-site bindings from an Astro PAGE file.\n *\n * Where component-schema (./extract.ts) parses the COMPONENT file\n * (`Hero.astro`) for its `interface Props`, this extractor parses the\n * PAGE that uses the component (`src/pages/[locale]/index.astro`) and\n * inspects each attribute on the JSX-like element marked with\n * `fieldPrefix=\"<prefix>\"`.\n *\n * The output is what the panel needs to decide which change op to stage\n * when the user edits a field:\n *\n * <Hero\n * title={hero.title} ← i18n: keyPath = 'hero.home.title'\n * image=\"/img/hero.jpg\" ← literal\n * stats={getStats()} ← unknown\n * fieldPrefix=\"hero.home\"\n * />\n *\n * Frontmatter trace:\n * const { t } = Astro.locals;\n * const hero = t.hero.home;\n *\n * The trace builds a map `hero → 'hero.home'`, then the attribute\n * `title={hero.title}` resolves to keyPath `hero.home.title`.\n *\n * Supported call-site shapes:\n * prop=\"literal\"\n * prop={'literal'}\n * prop={`literal`} (no expressions)\n * prop={IDENT} (where IDENT was assigned to t.<path>)\n * prop={IDENT.sub.path}\n * prop={IDENT['sub']['path']}\n * prop={t.foo.bar}\n * prop={t('foo.bar')}\n * prop={t['foo.bar']}\n *\n * Unsupported (graded as `unknown`):\n * prop={fn()}, prop={a + b}, prop={cond ? x : y}, etc.\n *\n * The extractor is intentionally conservative — anything ambiguous lands\n * as `unknown` rather than silently misclassifying. The panel's behaviour\n * for `unknown` is the same as before this extractor existed\n * (`section_props_update`), so misclassification is degrade-only.\n */\n\nimport { Project } from 'ts-morph'\nimport type { PropBinding } from './types.js'\n\nconst FRONTMATTER_RE = /^\\s*---\\s*\\n([\\s\\S]*?)\\n---/\n\n/**\n * Strict identifier (TS rules — letters, digits, _, $; cannot start with\n * digit). Conservative on purpose: looser parsing can match attribute\n * fragments inside HTML strings.\n */\nconst IDENT_RE = '[A-Za-z_$][A-Za-z0-9_$]*'\n\nexport interface ExtractBindingsOptions {\n /** Path of the page file relative to repo root — included in the result for debugging. */\n pageFile?: string\n}\n\nexport interface ExtractBindingsResult {\n /**\n * Map from prop name → binding info. Props whose call-site value we\n * couldn't classify are still included as `kind: 'unknown'` so the\n * panel can render them (and stage as `section_props_update`).\n *\n * Props missing from the call site entirely (e.g. relying on a\n * component-side default) are absent from this map.\n */\n bindings: Record<string, PropBinding>\n /** Echo of pageFile from options, for debugging. */\n pageFile?: string\n}\n\nexport function extractCallSiteBindings(\n pageSource: string,\n fieldPrefix: string,\n options: ExtractBindingsOptions = {},\n): ExtractBindingsResult {\n const empty: ExtractBindingsResult = { bindings: {}, pageFile: options.pageFile }\n\n const fmMatch = pageSource.match(FRONTMATTER_RE)\n const frontmatter = fmMatch?.[1] ?? ''\n const template = fmMatch ? pageSource.slice(fmMatch[0].length) : pageSource\n\n // 1. Build identifier → keyPath map from the frontmatter. Only\n // top-level `const X = t.<path>` (or `let`/`var`) declarations\n // count. Nested scopes are out of scope (rare in Astro pages).\n const identMap = buildIdentifierMap(frontmatter)\n\n // 2. Locate the JSX-like element with the matching fieldPrefix and\n // pull out its other attributes.\n const attrs = findElementAttributes(template, fieldPrefix)\n if (!attrs) return empty\n\n // 3. Classify each attribute.\n const bindings: Record<string, PropBinding> = {}\n for (const [name, raw] of Object.entries(attrs)) {\n if (name === 'fieldPrefix') continue\n bindings[name] = classifyAttributeValue(raw, identMap)\n }\n\n return { bindings, pageFile: options.pageFile }\n}\n\n/* -------------------------------------------------------------------------- */\n/* Frontmatter: identifier → key path */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Walk the page's frontmatter and build a map from identifier name to\n * the translation key path it was assigned. Examples:\n *\n * const hero = t.hero.home; → hero → 'hero.home'\n * const homeAbout = t.homeAbout; → homeAbout → 'homeAbout'\n * const x = t['foo']['bar']; → x → 'foo.bar'\n *\n * Only direct assignments rooted at `t` are tracked — chained traces\n * through other identifiers (`const a = hero.foo`) are ignored to keep\n * this conservative. dorjoiers does flat one-level binding so this\n * covers the canonical case.\n */\nfunction buildIdentifierMap(frontmatter: string): Map<string, string> {\n const map = new Map<string, string>()\n if (!frontmatter) return map\n\n const project = new Project({\n useInMemoryFileSystem: true,\n skipAddingFilesFromTsConfig: true,\n skipFileDependencyResolution: true,\n skipLoadingLibFiles: true,\n compilerOptions: {\n allowJs: true,\n noResolve: true,\n jsx: 4 /* Preserve */,\n noEmit: true,\n },\n })\n let sf\n try {\n sf = project.createSourceFile('__page-frontmatter.ts', frontmatter)\n } catch {\n return map\n }\n\n for (const v of sf.getVariableDeclarations()) {\n const name = v.getName()\n const init = v.getInitializer()\n if (!init) continue\n const path = traceTPath(init.getText())\n if (path !== null) map.set(name, path)\n }\n return map\n}\n\n/**\n * Convert a member-access chain rooted at `t` into a dotted key path.\n * Returns `null` if the expression isn't a recognized t.<...> chain.\n */\nfunction traceTPath(expr: string): string | null {\n // Strip outer parens (rare): `(t.hero.home)` → `t.hero.home`.\n let e = expr.trim()\n while (e.startsWith('(') && e.endsWith(')')) {\n e = e.slice(1, -1).trim()\n }\n if (!/^t([.[(]|$)/.test(e)) return null\n\n // Tokenize: split into dot-segments and ['x']/[\"x\"] segments.\n const segments: string[] = []\n let i = 1 // skip leading 't'\n while (i < e.length) {\n const ch = e[i]\n if (ch === '.') {\n const m = e.slice(i + 1).match(new RegExp('^' + IDENT_RE))\n if (!m) return null\n segments.push(m[0])\n i += 1 + m[0].length\n } else if (ch === '[') {\n const close = e.indexOf(']', i + 1)\n if (close === -1) return null\n const inner = e.slice(i + 1, close).trim()\n const litMatch = inner.match(/^(['\"])(.+)\\1$/)\n if (!litMatch || litMatch[2] === undefined) return null\n segments.push(litMatch[2])\n i = close + 1\n } else if (ch === '(') {\n // t('foo.bar') — only valid as the very first segment.\n if (segments.length !== 0) return null\n const close = matchingClose(e, i)\n if (close === -1) return null\n const inner = e.slice(i + 1, close).trim()\n const litMatch = inner.match(/^(['\"`])(.+)\\1$/)\n if (!litMatch || litMatch[2] === undefined) return null\n // Split the dotted key inside the call.\n for (const seg of litMatch[2].split('.')) {\n if (seg) segments.push(seg)\n }\n i = close + 1\n // After a call, only a member access may follow (rare). Bail to\n // keep this deterministic.\n if (i < e.length) return null\n } else {\n return null\n }\n }\n return segments.length === 0 ? null : segments.join('.')\n}\n\nfunction matchingClose(s: string, openIdx: number): number {\n let depth = 0\n let inStr: string | null = null\n for (let i = openIdx; i < s.length; i++) {\n const ch = s[i]\n if (inStr) {\n if (ch === '\\\\') { i++; continue }\n if (ch === inStr) inStr = null\n continue\n }\n if (ch === '\"' || ch === \"'\" || ch === '`') {\n inStr = ch\n continue\n }\n if (ch === '(') depth++\n else if (ch === ')') {\n depth--\n if (depth === 0) return i\n }\n }\n return -1\n}\n\n/* -------------------------------------------------------------------------- */\n/* Template: locate element with fieldPrefix and extract attributes */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Find the JSX-like element in the template that has\n * `fieldPrefix=\"<prefix>\"` (or `fieldPrefix={`<prefix>`}` /\n * `fieldPrefix={'<prefix>'}`) and return ALL its attributes as a\n * `name → rawValueText` map. RawValueText preserves the original\n * surrounding form (\"...\" / '...' / {...}) so the classifier can\n * distinguish literals from expressions.\n *\n * Returns `null` if no matching element is found.\n *\n * The scanner is purpose-built for Astro/JSX-style attribute lists. It\n * tracks balanced braces inside expression values and respects\n * string/template literals to avoid being fooled by `{` or `}` inside\n * string content.\n */\nfunction findElementAttributes(\n template: string,\n fieldPrefix: string,\n): Record<string, string> | null {\n const escaped = escapeRegExp(fieldPrefix)\n // Match `<Tag` … `fieldPrefix=\"...\"` … `>` or `/>`. We don't insist on\n // tag identity — Studio prefixes are unique so the attribute alone\n // pins us. We DO need to know where the tag begins so we can scan\n // its full attribute list both before and after fieldPrefix.\n const fpRegexes = [\n new RegExp(`fieldPrefix\\\\s*=\\\\s*\"${escaped}\"`),\n new RegExp(`fieldPrefix\\\\s*=\\\\s*'${escaped}'`),\n new RegExp(`fieldPrefix\\\\s*=\\\\s*\\\\{\\\\s*\\`${escaped}\\`\\\\s*\\\\}`),\n new RegExp(`fieldPrefix\\\\s*=\\\\s*\\\\{\\\\s*'${escaped}'\\\\s*\\\\}`),\n new RegExp(`fieldPrefix\\\\s*=\\\\s*\\\\{\\\\s*\"${escaped}\"\\\\s*\\\\}`),\n ]\n let fpIdx = -1\n for (const re of fpRegexes) {\n const m = re.exec(template)\n if (m) { fpIdx = m.index; break }\n }\n if (fpIdx === -1) return null\n\n // Walk backwards to the opening `<Tag` of the element containing\n // this attribute. We bail if we cross a closing `>` or another `<`,\n // which would mean fieldPrefix lives outside the tag we expected.\n const tagStart = findTagStartBefore(template, fpIdx)\n if (tagStart === -1) return null\n\n const tagEnd = findTagEndAfter(template, fpIdx)\n if (tagEnd === -1) return null\n\n const tagBody = template.slice(tagStart, tagEnd + 1)\n return parseAttributeList(tagBody)\n}\n\nfunction findTagStartBefore(s: string, idx: number): number {\n // Scan back looking for `<` that starts a JSX tag (component or HTML).\n // We assume tag content has no embedded `<` outside expressions; an\n // unbalanced `>` before our `<` invalidates the parse.\n let depth = 0\n for (let i = idx; i >= 0; i--) {\n const ch = s[i]\n if (ch === '}') depth++\n else if (ch === '{') {\n if (depth > 0) depth--\n } else if (ch === '<' && depth === 0) {\n return i\n } else if (ch === '>' && depth === 0) {\n // Crossed a tag boundary — fieldPrefix isn't inside an open tag.\n return -1\n }\n }\n return -1\n}\n\nfunction findTagEndAfter(s: string, idx: number): number {\n let depth = 0\n let inStr: string | null = null\n for (let i = idx; i < s.length; i++) {\n const ch = s[i]\n if (inStr) {\n if (ch === '\\\\') { i++; continue }\n if (ch === inStr) inStr = null\n continue\n }\n if (ch === '\"' || ch === \"'\" || ch === '`') {\n inStr = ch\n continue\n }\n if (ch === '{') depth++\n else if (ch === '}') {\n if (depth > 0) depth--\n } else if (ch === '>' && depth === 0) {\n return i\n }\n }\n return -1\n}\n\n/**\n * Parse the attributes of a single tag body (e.g. `<Hero a=\"1\" b={x} c />`).\n * Returns `name → rawValueText` (without the surrounding quotes/braces\n * stripped — the classifier needs the syntax).\n *\n * Boolean attributes (no `=`) get value `'true'` (literal `'true'`,\n * not the boolean primitive — the classifier treats it as a literal\n * to avoid surprising behaviour).\n */\nfunction parseAttributeList(tagBody: string): Record<string, string> {\n const out: Record<string, string> = {}\n // Skip the leading `<TagName` (or `<TagName>`).\n const tagNameMatch = tagBody.match(new RegExp(`^<${IDENT_RE}(?:\\\\.${IDENT_RE})*`))\n if (!tagNameMatch) return out\n let i = tagNameMatch[0].length\n\n while (i < tagBody.length) {\n // Skip whitespace.\n while (i < tagBody.length && /\\s/.test(tagBody[i] ?? '')) i++\n if (i >= tagBody.length) break\n const ch = tagBody[i]\n if (ch === '/' || ch === '>') break\n\n // Read attribute name. Astro accepts hyphens (`data-foo`) but\n // Component prop names are TS identifiers — we stick to that.\n const nameMatch = tagBody.slice(i).match(new RegExp(`^${IDENT_RE}`))\n if (!nameMatch) {\n // Not a name we can parse — skip to next whitespace-or-end.\n i++\n continue\n }\n const name = nameMatch[0]\n i += name.length\n\n // Skip whitespace before potential `=`.\n while (i < tagBody.length && /\\s/.test(tagBody[i] ?? '')) i++\n\n if (tagBody[i] !== '=') {\n // Boolean attribute (no `=`). Quote it so the classifier\n // resolves it as a string literal `\"true\"`.\n out[name] = '\"true\"'\n continue\n }\n i++ // consume `=`\n while (i < tagBody.length && /\\s/.test(tagBody[i] ?? '')) i++\n\n const valStart = i\n const after = tagBody[i]\n if (after === '\"' || after === \"'\") {\n const close = findStringClose(tagBody, i)\n if (close === -1) break\n out[name] = tagBody.slice(valStart, close + 1)\n i = close + 1\n } else if (after === '{') {\n const close = findExprClose(tagBody, i)\n if (close === -1) break\n out[name] = tagBody.slice(valStart, close + 1)\n i = close + 1\n } else {\n // Unquoted value — uncommon in JSX; consume to whitespace.\n const m = tagBody.slice(i).match(/^[^\\s>]+/)\n if (!m) break\n out[name] = m[0]\n i += m[0].length\n }\n }\n return out\n}\n\nfunction findStringClose(s: string, openIdx: number): number {\n const quote = s[openIdx]\n for (let i = openIdx + 1; i < s.length; i++) {\n const ch = s[i]\n if (ch === '\\\\') { i++; continue }\n if (ch === quote) return i\n }\n return -1\n}\n\nfunction findExprClose(s: string, openIdx: number): number {\n let depth = 0\n let inStr: string | null = null\n for (let i = openIdx; i < s.length; i++) {\n const ch = s[i]\n if (inStr) {\n if (ch === '\\\\') { i++; continue }\n if (ch === inStr) inStr = null\n continue\n }\n if (ch === '\"' || ch === \"'\" || ch === '`') {\n inStr = ch\n continue\n }\n if (ch === '{') depth++\n else if (ch === '}') {\n depth--\n if (depth === 0) return i\n }\n }\n return -1\n}\n\n/* -------------------------------------------------------------------------- */\n/* Classification */\n/* -------------------------------------------------------------------------- */\n\nfunction classifyAttributeValue(\n raw: string,\n identMap: Map<string, string>,\n): PropBinding {\n if (!raw) return { kind: 'unknown' }\n const trimmed = raw.trim()\n\n // String literal: \"...\" or '...'\n const strLit = matchStringLiteral(trimmed)\n if (strLit !== null) return { kind: 'literal', value: strLit }\n\n // Expression: { ... }\n if (trimmed.startsWith('{') && trimmed.endsWith('}')) {\n const inner = trimmed.slice(1, -1).trim()\n\n // Literal-inside-expression: {\"foo\"} / {'foo'} / {`foo`} (no $)\n const innerStr = matchStringLiteral(inner)\n if (innerStr !== null) return { kind: 'literal', value: innerStr }\n const tplLit = matchTemplateLiteralNoExpr(inner)\n if (tplLit !== null) return { kind: 'literal', value: tplLit }\n\n // Member access: hero.title / homeAbout.eyebrow / hero['title']\n const path = traceMemberPath(inner, identMap)\n if (path !== null) return { kind: 'i18n', keyPath: path }\n\n return { kind: 'unknown', expression: inner }\n }\n\n return { kind: 'unknown', expression: trimmed }\n}\n\nfunction matchStringLiteral(s: string): string | null {\n if (s.length < 2) return null\n const q = s[0]\n if ((q !== '\"' && q !== \"'\") || s[s.length - 1] !== q) return null\n // Reject anything with an unescaped quote in the middle.\n for (let i = 1; i < s.length - 1; i++) {\n if (s[i] === '\\\\') { i++; continue }\n if (s[i] === q) return null\n }\n // Unescape minimally — \\\" \\' \\\\ \\n \\t.\n return s.slice(1, -1).replace(/\\\\(.)/g, (_, c) => {\n if (c === 'n') return '\\n'\n if (c === 't') return '\\t'\n return c\n })\n}\n\nfunction matchTemplateLiteralNoExpr(s: string): string | null {\n if (s.length < 2 || s[0] !== '`' || s[s.length - 1] !== '`') return null\n if (s.includes('${')) return null\n return s.slice(1, -1)\n}\n\n/**\n * Trace a member-access expression rooted at a known identifier or `t`.\n * Returns dotted key path or `null`.\n *\n * Examples (with identMap = { hero: 'hero.home' }):\n * hero.title → 'hero.home.title'\n * hero['title'] → 'hero.home.title'\n * t.hero.home.title → 'hero.home.title'\n * t('hero.home.title') → 'hero.home.title'\n * t.hero['home'].title → 'hero.home.title'\n */\nfunction traceMemberPath(\n expr: string,\n identMap: Map<string, string>,\n): string | null {\n const e = expr.trim()\n // Direct t(...) / t.<...>\n const direct = traceTPath(e)\n if (direct !== null) return direct\n\n // Identifier-rooted\n const m = e.match(new RegExp(`^${IDENT_RE}`))\n if (!m) return null\n const root = m[0]\n const prefix = identMap.get(root)\n if (!prefix) return null\n\n let i = root.length\n const segments: string[] = []\n while (i < e.length) {\n const ch = e[i]\n if (ch === '.') {\n const sub = e.slice(i + 1).match(new RegExp(`^${IDENT_RE}`))\n if (!sub) return null\n segments.push(sub[0])\n i += 1 + sub[0].length\n } else if (ch === '[') {\n const close = e.indexOf(']', i + 1)\n if (close === -1) return null\n const inner = e.slice(i + 1, close).trim()\n const lit = inner.match(/^(['\"])(.+)\\1$/)\n if (!lit || lit[2] === undefined) return null\n segments.push(lit[2])\n i = close + 1\n } else {\n return null\n }\n }\n if (segments.length === 0) return prefix\n return prefix + '.' + segments.join('.')\n}\n\nfunction escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n","/**\n * Studio component-schema extractor — Studio v5.3 Phase 1.2.\n *\n * Thin wrapper around `extractComponentSchema` from `@saastro/studio-core`\n * that memoises results keyed by source-file content. Each `.astro` file\n * is parsed at most once per content hash; HMR replaces a hash with a new\n * one on every edit, which naturally evicts stale entries.\n *\n * The extractor itself is heavy (spins up an in-memory ts-morph project),\n * so caching matters even for small sites — `pnpm dev` restarts and full\n * production builds otherwise re-extract every section on every transform\n * pass.\n */\n\nimport {\n extractComponentSchema,\n type ComponentSchema,\n} from '@saastro/studio-core/component-schema'\n\n/**\n * Cache keyed by `${componentPath} ${hash(source)}`. The componentPath\n * is part of the key because the same source can legitimately live at\n * two paths during dev (a copied file rename), and we want the cached\n * schema to carry the right `componentPath` field.\n *\n * LRU cap so long-running `pnpm dev` sessions don't leak memory: every\n * HMR edit inserts a new (path, hash) entry — without a bound the Map\n * grows monotonically and never frees the old schemas. JS Map preserves\n * insertion order, so we evict the oldest key when we hit the ceiling\n * and re-insert on hits so the recency heuristic survives.\n */\nconst CACHE_MAX = 500\nconst cache = new Map<string, ComponentSchema | null>()\n\n/**\n * djb2-style content hash. Plenty of entropy for a per-file cache key —\n * collisions just mean a stale schema for one transform pass, which the\n * next edit will refresh. Avoids pulling in `node:crypto` here so the\n * module stays lean and bundles cleanly under tsup.\n */\nfunction hashSource(source: string): string {\n let h = 5381\n for (let i = 0; i < source.length; i++) {\n h = ((h << 5) + h + source.charCodeAt(i)) | 0\n }\n // Force unsigned hex so the key doesn't carry a leading `-`.\n return (h >>> 0).toString(16)\n}\n\nexport interface ExtractPropsSchemaOptions {\n /** Path used for diagnostics + populated on the returned schema. */\n componentPath?: string\n}\n\n/**\n * Memoised wrapper around `extractComponentSchema`. Returns null when the\n * file has no `interface Props` (or the parser bails) — same contract as\n * the underlying extractor.\n */\nexport function extractPropsSchema(\n source: string,\n options: ExtractPropsSchemaOptions = {},\n): ComponentSchema | null {\n const key = `${options.componentPath ?? ''} ${hashSource(source)}`\n if (cache.has(key)) {\n // LRU bump: re-insert so the entry moves to the back of the\n // insertion-order list and isn't the next eviction victim.\n const cached = cache.get(key) ?? null\n cache.delete(key)\n cache.set(key, cached)\n return cached\n }\n const schema = extractComponentSchema(source, {\n componentPath: options.componentPath,\n })\n if (cache.size >= CACHE_MAX) {\n // Evict the oldest (first inserted) key — Map iteration is in\n // insertion order, so `.keys().next().value` is the LRU.\n const oldest = cache.keys().next().value\n if (oldest !== undefined) cache.delete(oldest)\n }\n cache.set(key, schema)\n return schema\n}\n\n/**\n * Test-only escape hatch — clears the in-memory cache. Production code\n * should never need to call this; the content-hash key already handles\n * HMR by producing a new key per edit.\n */\nexport function __resetPropsSchemaCache(): void {\n cache.clear()\n}\n\nexport type { ComponentSchema }\n","/**\n * Inject a `<script type=\"application/saastro-schema\">` next to an\n * instrumented section root.\n *\n * Runs AFTER `transformAstroSource` (the auto-wrap pass). Given an\n * `.astro` source whose root element already carries the\n * `data-saastro=\"sec:${fieldPrefix}\"` marker, this transform inserts a\n * single inline `<script>` carrying the JSON-encoded `ComponentSchema`\n * just before the root's closing tag. The overlay (`overlay-script.ts`\n * in hub-react) reads that script at runtime to discover prop fields\n * without making an extra network round-trip.\n *\n * ## Output shape\n *\n * For an auto-wrapped section with prop `fieldPrefix`:\n *\n * ```html\n * <section ... data-saastro=\"sec:...\" ...>\n * ...original body...\n * <script type=\"application/saastro-schema\" is:inline data-prefix={fieldPrefix}>\n * {\"version\":1,\"fields\":[…]}\n * </script>\n * </section>\n * ```\n *\n * The script body is a JSON-string literal (NOT an Astro JS expression)\n * because Astro's compiler must treat it as text — anything wrapped in\n * `{}` would be evaluated at SSR time and produce a runtime expression.\n * Astro preserves `<script is:inline>` content verbatim regardless of\n * `{` characters, so plain JSON works.\n *\n * ## Constraints\n *\n * - Idempotent — re-running on output that already has a\n * `<script type=\"application/saastro-schema\">` is a no-op.\n * - Self-closing roots (`<img />`, `<input />`) are skipped: there is no\n * place to put a child node. The overlay's document-wide fallback\n * query covers those if/when we add a sibling-emission path later.\n * - Non-HTML roots (PascalCase components) are skipped — `auto-wrap`\n * already declines to instrument those.\n * - Escapes any `</` inside the JSON body to `<\\/` so a default value\n * containing the literal `</script>` cannot break the script tag.\n */\n\nimport type { ComponentSchema } from '@saastro/studio-core/component-schema'\n\nconst FRONTMATTER_RE = /^\\s*---\\s*\\n([\\s\\S]*?)\\n---\\s*\\n/\n\n/** Substring check is enough — the marker is unique to our instrumentation. */\nconst SCHEMA_SCRIPT_MARKER = 'type=\"application/saastro-schema\"'\n\n/**\n * Match the first opening tag after the frontmatter. Mirrors the regex\n * `auto-wrap` uses but with a simpler attribute capture (we don't care\n * about the existing attribute text here, only positions).\n */\nconst FIRST_OPEN_TAG_RE = /<([a-zA-Z][a-zA-Z0-9]*)\\b([^<>]*?)\\s*(\\/?)>/\n\n/**\n * Extract the literal default for `fieldPrefix` from a destructuring\n * assignment, if any. Examples that yield `'hero.home'`:\n *\n * - `const { fieldPrefix = 'hero.home' } = Astro.props`\n * - `const { fieldPrefix = \"hero.home\", title } = Astro.props`\n *\n * Returns null when the default is missing, non-string, or too complex\n * to parse with a single regex (object literal, function call, etc.).\n */\nfunction extractFieldPrefixDefault(frontmatter: string): string | null {\n const destruct = /const\\s*\\{([\\s\\S]*?)\\}\\s*=\\s*Astro\\.props/.exec(frontmatter)\n if (!destruct) return null\n const body = destruct[1] ?? ''\n // Find `fieldPrefix = <literal>` — accept single, double, or backtick\n // quotes. `[^'\"\\n]` keeps the match conservative; templated strings\n // (`\\`hero.${section}\\``) deliberately do NOT match — we'd lose the\n // expression by treating it as a literal.\n const m = /\\bfieldPrefix\\s*=\\s*(['\"])([^'\"\\n]*)\\1/.exec(body)\n return m ? (m[2] ?? null) : null\n}\n\n/**\n * Detect the literal default embedded in a `data-saastro` attribute that\n * was hand-written rather than auto-wrap-injected. Pattern matched:\n *\n * data-saastro={`sec:${fieldPrefix ?? 'hero.home'}`}\n *\n * Used as a fallback when the destructuring regex above misses (the\n * dorjoiers Hero, for example, destructures `fieldPrefix` with NO\n * default and folds the fallback into the JSX expression).\n */\nfunction extractFieldPrefixFromDataAttr(template: string): string | null {\n const m = /data-saastro=\\{\\s*`\\s*sec:\\$\\{\\s*fieldPrefix\\s*\\?\\?\\s*(['\"])([^'\"`]*)\\1\\s*\\}/.exec(\n template,\n )\n return m ? (m[2] ?? null) : null\n}\n\n/**\n * Find the position of the matching closing tag for the root element at\n * `openTagEnd`. Naive bracket counting on `<TAG` openers and `</TAG>`\n * closers — sufficient for well-formed Astro templates where the root\n * is a single HTML element (the only shape `auto-wrap` instruments).\n *\n * HTML comments (`<!-- ... -->`) are skipped so a commented-out\n * `<section>` inside the root doesn't desynchronise the depth counter.\n * Astro JS expression comments (`{/* ... *​/}`) don't carry HTML tags so\n * they never confuse the bracket walker; we leave them alone.\n *\n * Returns the index of the `<` in the closing tag, or -1 when no match\n * is found (in which case the caller bails out without emitting).\n */\nfunction findClosingTag(\n haystack: string,\n tagName: string,\n searchFrom: number,\n): number {\n // Match either an HTML comment OR an opener / closer of `tagName`.\n // Putting comments in the same alternation guarantees we skip them\n // atomically — a tag inside a comment can't sneak into the depth\n // counter.\n const re = new RegExp(\n `<!--[\\\\s\\\\S]*?-->|<(/?)${tagName}\\\\b[^<>]*?(/?)>`,\n 'gi',\n )\n re.lastIndex = searchFrom\n let depth = 1\n while (true) {\n const m = re.exec(haystack)\n if (!m) return -1\n // Comments don't have capture groups for the opener/closer marks,\n // so `m[1]` and `m[2]` are undefined for the comment branch. Skip.\n if (m[0]!.startsWith('<!--')) continue\n const isClose = m[1] === '/'\n const isSelfClose = m[2] === '/'\n if (isClose) {\n depth--\n if (depth === 0) return m.index\n } else if (!isSelfClose) {\n depth++\n }\n }\n}\n\n/**\n * JSON-encode a schema and escape any character sequences that could\n * accidentally close the host `<script>` tag. The replacement is safe\n * for `JSON.parse` to round-trip because `\\/` is a valid JSON escape.\n */\nfunction encodeSchemaBody(schema: ComponentSchema): string {\n return JSON.stringify(schema).replace(/<\\//g, '<\\\\/')\n}\n\nexport interface InjectSchemaOptions {\n /**\n * Override for the `data-prefix` attribute. The plugin normally lets\n * `injectSchema` derive this from the frontmatter; tests use this to\n * pin the value explicitly.\n */\n prefixOverride?: string\n}\n\n/**\n * Inject the schema script into `source`. Returns the source unchanged\n * when the file lacks a frontmatter / lacks an instrumented root /\n * already has a schema script.\n */\nexport function injectSchema(\n source: string,\n schema: ComponentSchema,\n options: InjectSchemaOptions = {},\n): string {\n // Idempotency — never emit a second script tag.\n if (source.includes(SCHEMA_SCRIPT_MARKER)) return source\n\n const fm = FRONTMATTER_RE.exec(source)\n if (!fm) return source\n\n const frontmatter = fm[1] ?? ''\n const templateStart = fm[0].length\n const template = source.slice(templateStart)\n\n const tagMatch = FIRST_OPEN_TAG_RE.exec(template)\n if (!tagMatch) return source\n const tagName = tagMatch[1]!\n // Component (PascalCase) roots are out of scope — auto-wrap skips them\n // for the same reason: the marker has to land on a real DOM element.\n if (tagName[0]! !== tagName[0]!.toLowerCase()) return source\n\n const isSelfClose = tagMatch[3] === '/'\n if (isSelfClose) return source\n\n // Where does the opening tag END (just past the `>`)?\n const openTagEnd = tagMatch.index + tagMatch[0].length\n // Find the matching closing tag.\n const closeIdx = findClosingTag(template, tagName, openTagEnd)\n if (closeIdx < 0) return source\n\n // Determine the data-prefix expression. Priority order:\n // 1. Caller-provided override (tests).\n // 2. Default literal from destructuring (`{ fieldPrefix = 'x' } = ...`).\n // 3. Default literal from a hand-written data-saastro attribute.\n // 4. Bare `{fieldPrefix}` reference — overlay falls back to the\n // sibling-by-data-prefix lookup.\n const prefixDefault =\n options.prefixOverride ??\n extractFieldPrefixDefault(frontmatter) ??\n extractFieldPrefixFromDataAttr(template)\n\n const dataPrefixAttr =\n prefixDefault != null\n ? `data-prefix={fieldPrefix ?? ${JSON.stringify(prefixDefault)}}`\n : `data-prefix={fieldPrefix}`\n\n const scriptTag =\n `<script type=\"application/saastro-schema\" is:inline ${dataPrefixAttr}>` +\n encodeSchemaBody(schema) +\n `</script>`\n\n const newTemplate =\n template.slice(0, closeIdx) + scriptTag + template.slice(closeIdx)\n\n return source.slice(0, templateStart) + newTemplate\n}\n","/**\n * Per-prop DOM instrumentation transform — Phase 2.\n *\n * Walks the Astro template and adds `data-saastro-field=\"<propPath>\"`\n * markers on the elements that render each `Props` field, so the\n * Studio overlay's `applySectionProps` can mutate the right node by\n * exact match instead of guessing via role-based heuristics.\n *\n * Markers we emit:\n *\n * <h1 data-saastro-field=\"title\">{title}</h1>\n * Welcome <span data-saastro-field=\"appName\">{appName}</span>!\n * <img data-saastro-field=\"image\" src={image} alt={imageAlt} />\n * {stats.map((s, __i) => (\n * <div data-saastro-field={`stats[${__i}]`}>\n * <p data-saastro-field={`stats[${__i}].value`}>{s.value}</p>\n * </div>\n * ))}\n *\n * Implementation choice: regex / string-walk, NOT `@astrojs/compiler`.\n * We deliberately keep the dependency surface light because:\n *\n * - This package ships into customer Astro builds; pulling in a\n * ~3 MB compiler dep is hard to justify for a small set of patterns.\n * - The patterns we need to handle are constrained to plain prop\n * references (`{ident}`, `{ident.member}`) and one well-known\n * iterator shape (`{ident.map((x[, i]) => <jsx/>)}`).\n * - The existing `auto-wrap.ts` is regex-based and ships fine.\n * - Anything we cannot recognize is left untouched — the bridge has a\n * heuristic fallback for unmarked nodes, so partial coverage is\n * never wrong, only suboptimal.\n *\n * Idempotency:\n *\n * - If an element already has `data-saastro-field`, we leave it alone\n * (developer's manual marker wins).\n * - The schema `<script type=\"application/saastro-schema\">` body is\n * skipped — its content is JSON, not template.\n * - The section root carries `data-saastro=\"sec:…\"` and we never\n * stamp it again.\n */\n\nimport type {\n ComponentField,\n ComponentSchema,\n} from '@saastro/studio-core/component-schema/types'\n\nexport interface InstrumentFieldsOptions {\n /** Component file path — used for diagnostics only. */\n componentPath?: string\n}\n\nconst FRONTMATTER_RE = /^\\s*---\\s*\\n[\\s\\S]*?\\n---\\s*\\n/\n\n/**\n * Pure transform. Takes auto-wrapped Astro source + the schema (so we\n * know which identifiers are Props fields and what their nested shapes\n * are) and returns the source with `data-saastro-field` markers\n * injected.\n *\n * No-op when:\n * - schema has no fields (nothing to instrument)\n * - source has no `data-saastro=\"sec:` marker (auto-wrap didn't\n * instrument this file)\n */\nexport function instrumentFields(\n source: string,\n schema: ComponentSchema,\n _options: InstrumentFieldsOptions = {},\n): string {\n if (!schema.fields.length) return source\n if (!source.includes('data-saastro=')) return source\n\n const fmMatch = FRONTMATTER_RE.exec(source)\n if (!fmMatch) return source\n\n const templateStart = fmMatch[0].length\n const template = source.slice(templateStart)\n\n // Carve out the schema script body so we never poke around inside\n // its JSON. The simplest and safest approach is to split on the\n // marker, transform the head only, and rejoin.\n const schemaScriptStart = template.indexOf(\n '<script type=\"application/saastro-schema\"',\n )\n\n let head: string\n let tail: string\n if (schemaScriptStart >= 0) {\n head = template.slice(0, schemaScriptStart)\n tail = template.slice(schemaScriptStart)\n } else {\n head = template\n tail = ''\n }\n\n const transformed = transformTemplate(head, schema.fields)\n return source.slice(0, templateStart) + transformed + tail\n}\n\n/**\n * Top-level transform of the template body (everything between\n * frontmatter and the schema script). Order matters:\n *\n * 1. Array fields first — their `.map()` block is rewritten as a\n * whole, after which the inner JSX gets per-item markers.\n * 2. Plain (non-array) fields — `{ident}` references in the\n * remaining template.\n */\nfunction transformTemplate(\n template: string,\n fields: readonly ComponentField[],\n): string {\n let out = template\n\n // Step 1 — arrays with `.map()`.\n for (const field of fields) {\n if (field.kind !== 'array') continue\n out = transformArrayField(out, field)\n }\n\n // Step 2 — scalar / object / enum / etc. references.\n for (const field of fields) {\n if (field.kind === 'array') continue\n out = transformScalarField(out, field.name)\n }\n\n return out\n}\n\n/* ─────────────────────── Array .map() rewriting ─────────────────────── */\n\n/**\n * Rewrite `{<name>.map((item[, i]) => <jsx/>)}` so:\n *\n * - The iterator gains a second parameter `__i` (or reuses an existing\n * one, renaming it to `__i`).\n * - The first JSX element inside the body gets\n * `data-saastro-field={\\`<name>[\\${__i}]\\`}`.\n * - Each `{item.<sub>}` inside gets a wrapper / parent marker of\n * `<name>[\\${__i}].<sub>` (depending on single-child / multi-child).\n */\nfunction transformArrayField(\n template: string,\n field: ComponentField,\n): string {\n const name = field.name\n const subFields = field.items?.fields ?? []\n\n // Anchor on `<name> . map ( ( <param> [, <idx>] ) =>`. We deliberately\n // do NOT require a leading `{` here — the iterator is sometimes guarded\n // by a conditional shortcut (e.g. `{xs && xs.map(...)}`) and matching\n // the bare method call is enough to find every site we care about.\n const re = new RegExp(\n `\\\\b${escapeReg(name)}\\\\s*\\\\.\\\\s*map\\\\s*\\\\(\\\\s*\\\\(\\\\s*([A-Za-z_$][\\\\w$]*)\\\\s*(?:,\\\\s*([A-Za-z_$][\\\\w$]*)\\\\s*)?\\\\)\\\\s*=>\\\\s*`,\n 'g',\n )\n\n let result = ''\n let cursor = 0\n let m: RegExpExecArray | null\n while ((m = re.exec(template)) !== null) {\n const headerStart = m.index\n const paramName = m[1]!\n const existingIdx = m[2] ?? null\n const arrowEnd = headerStart + m[0].length\n\n const body = readArrowBody(template, arrowEnd)\n if (!body) continue\n\n let innerJsx = body.bodyText\n if (existingIdx && existingIdx !== '__i') {\n const renameRe = new RegExp(`\\\\b${escapeReg(existingIdx)}\\\\b`, 'g')\n innerJsx = innerJsx.replace(renameRe, '__i')\n }\n const transformedJsx = stampArrayItem(innerJsx, name, paramName, subFields)\n\n const headerNew = `${name}.map((${paramName}, __i) => `\n\n result += template.slice(cursor, headerStart) + headerNew\n if (body.parens) {\n result += '(' + transformedJsx + ')'\n } else {\n result += transformedJsx\n }\n // Preserve text from the end of the body (or its closing paren) up\n // to the close of the `.map(` call — that range carries any\n // user-authored whitespace / comments.\n const bodyAbsEnd = arrowEnd + body.bodyOffset + body.bodyText.length\n const callCloseAbsEnd = arrowEnd + body.totalEndRel\n if (body.parens) {\n // The original `(`...`)` already accounted for; jump past the\n // close paren of the body in source.\n const afterBodyParen = bodyAbsEnd + 1 // skip ')'\n result += template.slice(afterBodyParen, callCloseAbsEnd)\n } else {\n result += template.slice(bodyAbsEnd, callCloseAbsEnd)\n }\n\n cursor = callCloseAbsEnd\n re.lastIndex = cursor\n }\n result += template.slice(cursor)\n return result\n}\n\n/**\n * Read the arrow function body starting at `start` (immediately after\n * `=>`). Two shapes supported:\n *\n * a) `( <jsx> )` — paren-wrapped expression. Walk balanced parens.\n * b) `<jsx />` — unwrapped JSX. Walk one element.\n *\n * Returns positional info so callers can slice and rebuild.\n */\nfunction readArrowBody(\n template: string,\n start: number,\n): null | {\n parens: boolean\n bodyOffset: number // start of bodyText relative to `start`\n bodyText: string\n totalEndRel: number // offset past the closing `)` of the .map call, relative to `start`\n} {\n // Skip whitespace\n let i = start\n while (i < template.length && /\\s/.test(template[i]!)) i++\n if (i >= template.length) return null\n\n let bodyStart: number\n let bodyEnd: number\n let parens: boolean\n\n if (template[i] === '(') {\n parens = true\n // Walk balanced parens to find the body's wrapping `(...)`.\n const inner = readBalanced(template, i, '(', ')')\n if (inner == null) return null\n bodyStart = i + 1\n bodyEnd = inner // index of matching ')'\n // Trim whitespace inside the parens.\n while (bodyStart < bodyEnd && /\\s/.test(template[bodyStart]!)) bodyStart++\n let trimEnd = bodyEnd\n while (trimEnd > bodyStart && /\\s/.test(template[trimEnd - 1]!)) trimEnd--\n bodyEnd = trimEnd\n\n // After the close paren, find the closing `)` of the `.map(` call.\n let j = inner + 1\n while (j < template.length && /\\s/.test(template[j]!)) j++\n if (template[j] !== ')') return null\n j++\n\n return {\n parens,\n bodyOffset: bodyStart - start,\n bodyText: template.slice(bodyStart, bodyEnd),\n totalEndRel: j - start,\n }\n }\n\n // Unwrapped JSX — read one element.\n if (template[i] === '<') {\n parens = false\n const elEnd = readJsxElement(template, i)\n if (elEnd == null) return null\n bodyStart = i\n bodyEnd = elEnd\n\n let j = bodyEnd\n while (j < template.length && /\\s/.test(template[j]!)) j++\n if (template[j] !== ')') return null\n j++\n\n return {\n parens,\n bodyOffset: bodyStart - start,\n bodyText: template.slice(bodyStart, bodyEnd),\n totalEndRel: j - start,\n }\n }\n\n return null\n}\n\n/**\n * Walk balanced delimiters starting at `i` (which points at `open`).\n * Returns the index of the matching close, or null if unbalanced.\n *\n * Aware of strings (single, double, template) and JSX expression braces.\n */\nfunction readBalanced(\n s: string,\n i: number,\n open: string,\n close: string,\n): number | null {\n if (s[i] !== open) return null\n let depth = 0\n let j = i\n while (j < s.length) {\n const ch = s[j]!\n if (ch === '\"' || ch === \"'\" || ch === '`') {\n j = skipString(s, j) + 1\n continue\n }\n if (ch === open) depth++\n else if (ch === close) {\n depth--\n if (depth === 0) return j\n }\n j++\n }\n return null\n}\n\nfunction skipString(s: string, i: number): number {\n const quote = s[i]!\n let j = i + 1\n while (j < s.length) {\n const ch = s[j]!\n if (ch === '\\\\') {\n j += 2\n continue\n }\n // Template literals can hold `${...}` — walk through them as balanced\n // braces. Strings inside the expression are themselves handled\n // recursively by the outer walker, but for our simple needs the\n // greedy approach is enough.\n if (quote === '`' && ch === '$' && s[j + 1] === '{') {\n const close = readBalanced(s, j + 1, '{', '}')\n if (close == null) return s.length - 1\n j = close + 1\n continue\n }\n if (ch === quote) return j\n j++\n }\n return s.length - 1\n}\n\n/**\n * Read one JSX element starting at `<`. Returns the index just past the\n * matching close tag (or self-close). Handles nested same-named tags.\n *\n * This is conservative — anything tricky causes a return of null, which\n * makes the caller skip the rewrite and leave the original text alone.\n */\nfunction readJsxElement(s: string, start: number): number | null {\n if (s[start] !== '<') return null\n // Read tag name\n let i = start + 1\n const nameMatch = /^[A-Za-z][A-Za-z0-9]*/.exec(s.slice(i))\n if (!nameMatch) return null\n const tag = nameMatch[0]\n i += tag.length\n\n // Walk attributes until we hit `>` or `/>`\n let inString: string | null = null\n let braceDepth = 0\n while (i < s.length) {\n const ch = s[i]!\n if (inString) {\n if (ch === '\\\\') {\n i += 2\n continue\n }\n if (inString === '`' && ch === '$' && s[i + 1] === '{') {\n const close = readBalanced(s, i + 1, '{', '}')\n if (close == null) return null\n i = close + 1\n continue\n }\n if (ch === inString) inString = null\n i++\n continue\n }\n if (braceDepth > 0) {\n if (ch === '{') braceDepth++\n else if (ch === '}') braceDepth--\n else if (ch === '\"' || ch === \"'\" || ch === '`') inString = ch\n i++\n continue\n }\n if (ch === '\"' || ch === \"'\" || ch === '`') {\n inString = ch\n i++\n continue\n }\n if (ch === '{') {\n braceDepth = 1\n i++\n continue\n }\n if (ch === '/' && s[i + 1] === '>') {\n // Self-closing — done.\n return i + 2\n }\n if (ch === '>') {\n i++\n // Now walk children until matching `</tag>`.\n let depth = 1\n const re = new RegExp(`<\\\\/?${tag}\\\\b`, 'g')\n re.lastIndex = i\n while (true) {\n const m = re.exec(s)\n if (!m) return null\n const isClose = s[m.index + 1] === '/'\n // Find the `>` that ends this tag, accounting for attributes.\n let k = m.index + (isClose ? 2 + tag.length : 1 + tag.length)\n let str: string | null = null\n let bd = 0\n while (k < s.length) {\n const c = s[k]!\n if (str) {\n if (c === '\\\\') {\n k += 2\n continue\n }\n if (c === str) str = null\n k++\n continue\n }\n if (bd > 0) {\n if (c === '{') bd++\n else if (c === '}') bd--\n k++\n continue\n }\n if (c === '\"' || c === \"'\" || c === '`') {\n str = c\n k++\n continue\n }\n if (c === '{') {\n bd = 1\n k++\n continue\n }\n if (c === '/' && s[k + 1] === '>') {\n k += 2\n break\n }\n if (c === '>') {\n k++\n break\n }\n k++\n }\n if (isClose) {\n depth--\n if (depth === 0) return k\n } else {\n // Self-closing siblings don't increment depth; check explicitly.\n // Heuristic — if `s[k - 2] === '/'` it was self-closing.\n if (s[k - 2] !== '/') depth++\n }\n re.lastIndex = k\n }\n }\n i++\n }\n return null\n}\n\n/**\n * Stamp markers inside the body of an array `.map()` callback:\n *\n * - The outer iteration element gets `data-saastro-field={\\`name[\\${__i}]\\`}`\n * - For each sub-field declared in the schema, find `{paramName.<sub>}`\n * and stamp `name[\\${__i}].<sub>` on the appropriate element (parent\n * for single-child, span wrap for multi-child).\n */\nfunction stampArrayItem(\n jsx: string,\n fieldName: string,\n paramName: string,\n subFields: readonly ComponentField[],\n): string {\n // Stamp the outer element first.\n let out = stampOuterElement(\n jsx,\n `{\\`${fieldName}[\\${__i}]\\`}`,\n )\n\n // Then per-sub-field references. The attribute VALUE we emit is a\n // JSX expression: `{`<path>[${__i}].<sub>`}` — caller passes the\n // full wrapped value so `injectAttr` can paste it as-is. The value\n // is always a literal `{…}` expression here (never a JSON-quoted\n // string) because we need to interpolate `${__i}` per iteration.\n for (const sub of subFields) {\n const propPath = `{\\`${fieldName}[\\${__i}].${sub.name}\\`}`\n out = transformDottedReference(out, paramName, sub.name, propPath)\n }\n return out\n}\n\n/**\n * Inject `data-saastro-field={…}` on the FIRST element of a JSX\n * fragment — used for the `.map()` iteration root. Skips elements that\n * already have a marker.\n */\nfunction stampOuterElement(jsx: string, valueExpr: string): string {\n // Find the first `<tag` that isn't a closing tag.\n const m = /<([A-Za-z][A-Za-z0-9]*)\\b([^<>]*?)(\\/?)>/.exec(jsx)\n if (!m) return jsx\n if (/data-saastro-field/.test(m[2]!)) return jsx\n const tag = m[1]\n const attrs = m[2]\n const selfClose = m[3]\n const replacement = `<${tag}${attrs} data-saastro-field=${valueExpr}${selfClose}>`\n return jsx.slice(0, m.index) + replacement + jsx.slice(m.index + m[0].length)\n}\n\n/* ─────────────────────── Scalar prop rewriting ─────────────────────── */\n\n/**\n * Find `{name}` references in the template and stamp the appropriate\n * element. Skips:\n *\n * - Compound expressions (`{name.foo}`, `{name || 'x'}`, `{name + 'y'}`)\n * - References inside attributes (those are handled separately for img)\n * - References on elements that already have `data-saastro-field`\n */\nfunction transformScalarField(template: string, name: string): string {\n return transformDottedReference(template, name, null, JSON.stringify(name))\n}\n\n/**\n * Generic per-reference rewriter shared between scalar props and array\n * sub-props.\n *\n * `valueAttr` is whatever string the caller wants pasted after the `=`\n * sign of the emitted attribute. The caller decides the quoting style:\n *\n * - scalar/static field name → caller passes `JSON.stringify(name)`,\n * yielding `data-saastro-field=\"title\"`.\n * - array sub-resource with interpolated index → caller passes a JSX\n * expression like `{`stats[${__i}].value`}`, yielding\n * `data-saastro-field={`stats[${__i}].value`}`.\n *\n * Both shapes are valid Astro template literals, so a single pasting\n * path covers both cases.\n *\n * @param paramOrName the identifier whose reference we look for. If\n * `subProp` is given, we look for `paramOrName.subProp`.\n * If `subProp` is null, we look for the bare\n * identifier `paramOrName`.\n * @param valueAttr the attribute value to emit (caller-quoted).\n */\nfunction transformDottedReference(\n template: string,\n paramOrName: string,\n subProp: string | null,\n valueAttr: string,\n): string {\n const refPattern = subProp\n ? new RegExp(\n `\\\\{\\\\s*${escapeReg(paramOrName)}\\\\s*\\\\.\\\\s*${escapeReg(subProp)}\\\\s*\\\\}`,\n 'g',\n )\n : new RegExp(`\\\\{\\\\s*${escapeReg(paramOrName)}\\\\s*\\\\}`, 'g')\n\n // Special-case: img tags with `src={name}` get marked on the img itself.\n // We handle this before the text-position pass so the image attribute\n // case wins over a hypothetical surrounding-element pass.\n let out = template\n if (!subProp) {\n out = stampImgSrc(out, paramOrName, valueAttr)\n }\n\n // Walk every plain `{ident}` (or `{param.sub}`) reference and stamp\n // the parent element.\n let result = ''\n let cursor = 0\n let m: RegExpExecArray | null\n refPattern.lastIndex = 0\n while ((m = refPattern.exec(out)) !== null) {\n const refStart = m.index\n const refEnd = refStart + m[0].length\n\n // Skip if this `{ident}` is inside an attribute value (e.g. src={image}).\n // Heuristic: walk backwards from refStart to the most recent `<` or `>`.\n // If we hit `<` first WITHOUT crossing a `>`, we're inside a tag.\n if (isInsideTag(out, refStart)) {\n result += out.slice(cursor, refEnd)\n cursor = refEnd\n continue\n }\n\n // Skip if `{name}` is preceded by `.` or followed by `.`/`(` —\n // signals a member access or call (e.g. `{name.toUpperCase()}`).\n // The regex itself captures only `{ \\s* name \\s* }` so this is\n // already guarded, but we double-check defensively.\n if (out[refEnd] === '.' || out[refStart - 1] === '.') {\n result += out.slice(cursor, refEnd)\n cursor = refEnd\n continue\n }\n\n // Find the enclosing element for this expression.\n const parent = findEnclosingElement(out, refStart)\n if (!parent) {\n result += out.slice(cursor, refEnd)\n cursor = refEnd\n continue\n }\n\n // Skip the section root — already has `data-saastro=\"sec:…\"`.\n if (parent.attrs.includes('data-saastro=')) {\n result += out.slice(cursor, refEnd)\n cursor = refEnd\n continue\n }\n // Skip if already marked.\n if (parent.attrs.includes('data-saastro-field')) {\n result += out.slice(cursor, refEnd)\n cursor = refEnd\n continue\n }\n\n // Single-child vs multi-child: count meaningful \"tokens\" of children.\n const childContent = out.slice(parent.bodyStart, parent.bodyEnd)\n const onlyChild = isOnlyMeaningfulExpression(childContent, refStart - parent.bodyStart, refEnd - parent.bodyStart)\n\n if (onlyChild) {\n // Inject `data-saastro-field=...` into parent's opening tag.\n const newOpen = injectAttr(parent.openTag, valueAttr)\n const replaced = out.slice(0, parent.openStart) + newOpen + out.slice(parent.openEnd)\n // Recompute cursor / refEnd offsets — easiest is to advance based\n // on length delta and continue from after the original ref.\n const delta = newOpen.length - parent.openTag.length\n result += replaced.slice(cursor, refEnd + delta)\n cursor = refEnd + delta\n out = replaced\n refPattern.lastIndex = cursor\n } else {\n // Wrap in `<span data-saastro-field=…>{name}</span>`.\n const wrapped = `<span data-saastro-field=${valueAttr}>${m[0]}</span>`\n result += out.slice(cursor, refStart) + wrapped\n cursor = refEnd\n // Update refPattern.lastIndex relative to current `out`.\n refPattern.lastIndex = cursor\n }\n }\n result += out.slice(cursor)\n return result\n}\n\n/**\n * Stamp `<img>` tags whose `src={name}` (or templated `src={\\`…${name}…\\`}`)\n * matches our prop name. The marker goes ON the img element itself.\n */\nfunction stampImgSrc(\n template: string,\n name: string,\n valueAttr: string,\n): string {\n // Match an <img …> tag with `src={...name...}` — we keep this loose so\n // helper-wrapped src values still get caught (`src={imageUrl(image)}`).\n const re = /<img\\b([^<>]*?)\\/?>/g\n let result = ''\n let cursor = 0\n let m: RegExpExecArray | null\n while ((m = re.exec(template)) !== null) {\n const attrs = m[1]!\n if (/data-saastro-field/.test(attrs)) continue\n const srcMatch = /\\bsrc\\s*=\\s*\\{([\\s\\S]*?)\\}/.exec(attrs)\n if (!srcMatch) continue\n const srcExpr = srcMatch[1]!\n if (!new RegExp(`\\\\b${escapeReg(name)}\\\\b`).test(srcExpr)) continue\n // Insert the marker.\n const newOpen = injectAttr(m[0], valueAttr)\n result += template.slice(cursor, m.index) + newOpen\n cursor = m.index + m[0].length\n re.lastIndex = cursor\n }\n result += template.slice(cursor)\n return result\n}\n\n/** Escape a string for use inside a RegExp. */\nfunction escapeReg(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\n/**\n * Inject a `data-saastro-field=…` attribute into an opening tag.\n * Idempotent (returns the tag unchanged when the attribute is already\n * present). `valueAttr` is pasted verbatim — the caller chooses the\n * quoting style (JSON-quoted string vs JSX expression) before passing.\n */\nfunction injectAttr(tag: string, valueAttr: string): string {\n if (/data-saastro-field/.test(tag)) return tag\n // We want the attribute right after the tag name for stable test diffs.\n const m = /^<([A-Za-z][A-Za-z0-9]*)/.exec(tag)\n if (!m) return tag\n const insertAt = m[0].length\n return tag.slice(0, insertAt) + ` data-saastro-field=${valueAttr}` + tag.slice(insertAt)\n}\n\n/** True when the position is inside a tag (between `<` and the next `>`). */\nfunction isInsideTag(s: string, pos: number): boolean {\n for (let i = pos - 1; i >= 0; i--) {\n const ch = s[i]!\n if (ch === '>') return false\n if (ch === '<') return true\n }\n return false\n}\n\n/**\n * Find the smallest element that fully contains `pos`. Returns the\n * positions of the open tag, the body, and the open tag's attribute\n * string. Returns null when no enclosing element can be located (the\n * caller leaves the reference unmarked in that case).\n */\nfunction findEnclosingElement(\n s: string,\n pos: number,\n): null | {\n openStart: number\n openEnd: number\n openTag: string\n attrs: string\n bodyStart: number\n bodyEnd: number\n} {\n // Walk backwards to find the closest `<tag …>` that opens before pos\n // and whose matching close is after pos. We do a linear scan: collect\n // every open/close tag before pos, then pop matching pairs.\n // For practical templates this is fast enough.\n const tagRe = /<(\\/?)([A-Za-z][A-Za-z0-9]*)\\b([^<>]*?)(\\/?)>/g\n type TagInfo = {\n isClose: boolean\n selfClose: boolean\n tagName: string\n attrs: string\n start: number\n end: number\n }\n const tags: TagInfo[] = []\n let m: RegExpExecArray | null\n while ((m = tagRe.exec(s)) !== null) {\n tags.push({\n isClose: m[1] === '/',\n selfClose: m[4] === '/',\n tagName: m[2]!,\n attrs: m[3]!,\n start: m.index,\n end: m.index + m[0].length,\n })\n }\n\n // Find the open tag whose body covers `pos`. Strategy: walk tags in\n // order; maintain a stack of open tags. When a close tag pops, if\n // pos is between the popped open's end and the close's start, we\n // have our parent — but we want the SMALLEST such element, so keep\n // a candidate and replace it whenever a deeper one is found.\n const stack: TagInfo[] = []\n let bestOpen: TagInfo | null = null\n let bestClose: TagInfo | null = null\n for (const t of tags) {\n if (t.selfClose) continue\n if (t.isClose) {\n // pop matching\n for (let i = stack.length - 1; i >= 0; i--) {\n if (stack[i]!.tagName === t.tagName) {\n const opener = stack[i]!\n // Drop everything above this index (mismatched, skip).\n stack.splice(i, stack.length - i)\n // Element runs from opener.end .. t.start.\n if (opener.end <= pos && pos < t.start) {\n // smallest: prefer the one with latest opener.end / earliest t.start\n if (\n !bestOpen ||\n opener.end > bestOpen.end ||\n (opener.end === bestOpen.end && t.start < bestClose!.start)\n ) {\n bestOpen = opener\n bestClose = t\n }\n }\n break\n }\n }\n } else {\n stack.push(t)\n }\n }\n\n if (!bestOpen || !bestClose) return null\n\n return {\n openStart: bestOpen.start,\n openEnd: bestOpen.end,\n openTag: s.slice(bestOpen.start, bestOpen.end),\n attrs: bestOpen.attrs,\n bodyStart: bestOpen.end,\n bodyEnd: bestClose.start,\n }\n}\n\n/**\n * Within the BODY of an element, decide whether the `{name}` expression\n * (located at relStart..relEnd within `body`) is the only meaningful\n * content. Whitespace and comments don't count. Anything else (text,\n * other expressions, sibling elements) makes it a multi-child case.\n */\nfunction isOnlyMeaningfulExpression(\n body: string,\n relStart: number,\n relEnd: number,\n): boolean {\n const before = body.slice(0, relStart)\n const after = body.slice(relEnd)\n return /^\\s*$/.test(before) && /^\\s*$/.test(after)\n}\n","/**\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","import type { Plugin } from 'vite'\nimport { transformAstroSource } from './auto-wrap.js'\nimport { extractPropsSchema } from './extract-props-schema.js'\nimport { injectSchema } from './inject-schema.js'\nimport { instrumentFields } from './instrument-fields.js'\nimport { injectVersionMetaTag } from './meta-tag.js'\nimport type { StudioAutoWrapOptions } from './types.js'\n\nexport type { StudioAutoWrapOptions } from './types.js'\n\n/**\n * Quick check: does this file declare `fieldPrefix` in its frontmatter\n * Props interface? Used as the entry-gate for the whole transform —\n * we only want to spin up ts-morph + run the regex pipeline on files\n * that are actually Studio-instrumentable. Anything else is skipped\n * with a cheap regex test.\n *\n * Conservative match: anchored on the `---` frontmatter fences and a\n * word-boundaried `fieldPrefix`. Comments mentioning `fieldPrefix`\n * trigger a false positive, but the downstream passes are no-ops on\n * files that don't actually use it.\n */\nconst FRONTMATTER_FIELD_PREFIX_RE = /^\\s*---\\s*\\n[\\s\\S]*?\\bfieldPrefix\\b[\\s\\S]*?\\n---\\s*\\n/\n\nfunction hasFieldPrefixReference(source: string): boolean {\n return FRONTMATTER_FIELD_PREFIX_RE.test(source)\n}\n\n/**\n * Vite plugin factory for Saastro Studio instrumentation.\n *\n * Drop into `vite.plugins` in `astro.config.mjs`:\n *\n * import studioAutoWrap from '@saastro/studio/vite';\n * export default defineConfig({\n * vite: { plugins: [studioAutoWrap({ autoWrap: true })] },\n * });\n *\n * What the plugin does:\n * 1. (enforce: 'pre') Scans .astro source for `fieldPrefix` prop and injects\n * `data-saastro=\"sec:${fieldPrefix}\"` on the root element.\n * 2. (enforce: 'pre') When step 1 mutates the file, extracts a\n * `ComponentSchema` from the file's `interface Props` (via ts-morph in\n * `@saastro/studio-core`) and bakes it into a\n * `<script type=\"application/saastro-schema\" data-prefix=\"…\">` placed\n * just before the section's root closing tag. The overlay reads it at\n * runtime so the Studio panel doesn't need a server round-trip.\n * 3. (enforce: 'pre') Walks the template and stamps each rendered\n * `Props` field with `data-saastro-field=\"<propPath>\"` so the Studio\n * overlay can mutate the exact DOM node by name (no role heuristic).\n * Currently a pass-through stub; full AST traversal lands in Phase 2.\n * 4. (transformIndexHtml) Injects <meta name=\"saastro:studio-version\"> into\n * every HTML page's <head> so the Hub can detect protocol version.\n */\nexport default function studioAutoWrap(opts: StudioAutoWrapOptions = {}): Plugin {\n const enabled = opts.autoWrap === true\n const include = opts.include ?? ((id: string) => id.endsWith('.astro'))\n\n return {\n name: '@saastro/studio:auto-wrap',\n enforce: 'pre',\n\n transform(code, id) {\n if (!enabled) return null\n if (!include(id)) return null\n\n // Trigger: the file declares a `fieldPrefix` prop. This catches\n // BOTH the auto-wrap path (we'll inject `data-saastro=\"sec:...\"`)\n // AND the hand-written path (the developer already put the\n // marker on the root tag — we still need to run schema injection\n // + per-field instrumentation, which transformAstroSource skips).\n //\n // Previous bug: the plugin used \"auto-wrap modified the file\" as\n // the trigger. When the developer had already hand-written\n // `data-saastro={`sec:${fieldPrefix}`}` on the root, auto-wrap\n // short-circuited → the pipeline never ran → `<img>` and\n // per-prop markers never reached the bundle. This shape is\n // common in instrumented codebases (dorjoiers etc.).\n if (!hasFieldPrefixReference(code)) return null\n\n const wrapped = transformAstroSource(code)\n const schema = extractPropsSchema(code, { componentPath: id })\n let next = wrapped\n // Schema-driven steps need the parsed Props interface. When\n // ts-morph can't parse it (no `interface Props`, or trickier\n // shapes than we support), we still keep the auto-wrap edit so\n // the section marker lands — the runtime catalog fallback on the\n // Hub picks up the slack.\n if (schema) {\n next = injectSchema(next, schema)\n next = instrumentFields(next, schema, { componentPath: id })\n }\n if (next === code) return null\n return { code: next, map: null }\n },\n\n transformIndexHtml(html) {\n if (!enabled) return html\n return injectVersionMetaTag(html)\n },\n }\n}\n"],"mappings":"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","names":["Project","FRONTMATTER_RE","FRONTMATTER_RE","FIRST_OPEN_TAG_RE","FRONTMATTER_RE"]}