@saastro/studio 0.7.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-S2IKJ3QM.js → chunk-3MKWTNQA.js} +2 -2
- package/dist/{chunk-7U2GMTVW.js → chunk-G3MUVKWX.js} +8 -3
- package/dist/chunk-G3MUVKWX.js.map +1 -0
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/chunk-PZ5AY32C.js.map +1 -0
- package/dist/chunk-T3ZZAM65.js +13881 -0
- package/dist/chunk-T3ZZAM65.js.map +1 -0
- package/dist/chunk-TU7VF6SY.js +443 -0
- package/dist/chunk-TU7VF6SY.js.map +1 -0
- package/dist/doctor.d.ts +31 -3
- package/dist/doctor.js +138 -2
- package/dist/doctor.js.map +1 -1
- package/dist/image/index.js +1 -0
- package/dist/image/index.js.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/integration/index.js +4 -3
- package/dist/middleware/index.js +4 -1
- package/dist/middleware/index.js.map +1 -1
- package/dist/richtext/index.d.ts +142 -0
- package/dist/richtext/index.js +40 -0
- package/dist/richtext/index.js.map +1 -0
- package/dist/vite/index.js +3 -2
- package/package.json +21 -14
- package/src/RichText.astro +93 -0
- package/dist/chunk-5QZN4IGF.js +0 -71
- package/dist/chunk-5QZN4IGF.js.map +0 -1
- package/dist/chunk-7U2GMTVW.js.map +0 -1
- /package/dist/{chunk-S2IKJ3QM.js.map → chunk-3MKWTNQA.js.map} +0 -0
package/dist/doctor.js
CHANGED
|
@@ -5,6 +5,13 @@ import {
|
|
|
5
5
|
resolvePageKey,
|
|
6
6
|
routeBaseName
|
|
7
7
|
} from "./chunk-PKDMKMD6.js";
|
|
8
|
+
import {
|
|
9
|
+
SPAN_MARKS,
|
|
10
|
+
isSpansShape,
|
|
11
|
+
isSpansValue,
|
|
12
|
+
isValidMark
|
|
13
|
+
} from "./chunk-T3ZZAM65.js";
|
|
14
|
+
import "./chunk-PZ5AY32C.js";
|
|
8
15
|
|
|
9
16
|
// ../studio-core/dist/chunk-CW5U4CZJ.js
|
|
10
17
|
function escapeReg(s) {
|
|
@@ -459,18 +466,145 @@ function checkAllowlistCompliance(files) {
|
|
|
459
466
|
function truncate(s) {
|
|
460
467
|
return s.length > 48 ? s.slice(0, 45) + "\u2026" : s;
|
|
461
468
|
}
|
|
469
|
+
function isSpansLike(v) {
|
|
470
|
+
return Array.isArray(v) && v.length > 0 && v.some(
|
|
471
|
+
(item) => item !== null && typeof item === "object" && !Array.isArray(item) && "text" in item
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
function spansDefect(v) {
|
|
475
|
+
for (let i = 0; i < v.length; i++) {
|
|
476
|
+
const item = v[i];
|
|
477
|
+
if (item === null || typeof item !== "object" || Array.isArray(item))
|
|
478
|
+
return `element ${i} is not a span object`;
|
|
479
|
+
const span = item;
|
|
480
|
+
if (typeof span.text !== "string") return `element ${i} has a non-string "text"`;
|
|
481
|
+
for (const k of Object.keys(span)) {
|
|
482
|
+
if (k !== "text" && k !== "marks") return `element ${i} has unknown key "${k}"`;
|
|
483
|
+
}
|
|
484
|
+
if (span.marks !== void 0) {
|
|
485
|
+
if (!Array.isArray(span.marks)) return `element ${i} has a non-array "marks"`;
|
|
486
|
+
for (const m of span.marks) {
|
|
487
|
+
if (!isValidMark(m))
|
|
488
|
+
return `element ${i} uses unknown mark "${String(m)}" (allowed: ${SPAN_MARKS.join(", ")}, c:<token>, s:sm|lg|xl, f:display|body|code)`;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return "unknown defect";
|
|
493
|
+
}
|
|
494
|
+
function classifyLeaves(obj, prefix, out) {
|
|
495
|
+
if (obj === null || typeof obj !== "object") {
|
|
496
|
+
if (prefix) out.set(prefix, typeof obj === "string" ? "string" : "other");
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
if (Array.isArray(obj)) {
|
|
500
|
+
if (isSpansValue(obj)) {
|
|
501
|
+
out.set(prefix, "spans");
|
|
502
|
+
} else if (isSpansShape(obj)) {
|
|
503
|
+
out.set(prefix, "spans-unmarked");
|
|
504
|
+
} else if (isSpansLike(obj)) {
|
|
505
|
+
out.set(prefix, "spans-malformed");
|
|
506
|
+
} else {
|
|
507
|
+
obj.forEach((item, i) => classifyLeaves(item, prefix ? `${prefix}.${i}` : String(i), out));
|
|
508
|
+
}
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
512
|
+
classifyLeaves(v, prefix ? `${prefix}.${k}` : k, out);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
function checkSpansIntegrity(locales) {
|
|
516
|
+
const findings = [];
|
|
517
|
+
const perLocale = locales.map((l) => {
|
|
518
|
+
const map = /* @__PURE__ */ new Map();
|
|
519
|
+
classifyLeaves(l.data, "", map);
|
|
520
|
+
return { ...l, map };
|
|
521
|
+
});
|
|
522
|
+
for (const l of perLocale) {
|
|
523
|
+
for (const [path, kind] of l.map) {
|
|
524
|
+
if (kind !== "spans-malformed") continue;
|
|
525
|
+
const raw = getAtDottedPath(l.data, path);
|
|
526
|
+
findings.push({
|
|
527
|
+
file: l.file,
|
|
528
|
+
status: "spans_malformed",
|
|
529
|
+
severity: "error",
|
|
530
|
+
reason: `Key "${path}" (${l.locale}) looks like rich spans but is malformed: ${Array.isArray(raw) ? spansDefect(raw) : "not an array"}.`,
|
|
531
|
+
remedy: `Fix the value to [{ text, marks?: [${SPAN_MARKS.map((m) => `'${m}'`).join(" | ")} | 'c:<token>' | 's:sm|lg|xl' | 'f:display|body|code'] }] or revert it to a plain string.`,
|
|
532
|
+
detail: path
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
const spansAnywhere = /* @__PURE__ */ new Set();
|
|
537
|
+
for (const l of perLocale) {
|
|
538
|
+
for (const [path, kind] of l.map) if (kind === "spans") spansAnywhere.add(path);
|
|
539
|
+
}
|
|
540
|
+
for (const path of spansAnywhere) {
|
|
541
|
+
for (const l of perLocale) {
|
|
542
|
+
if (l.map.get(path) !== "string") continue;
|
|
543
|
+
findings.push({
|
|
544
|
+
file: l.file,
|
|
545
|
+
status: "spans_shape_mismatch",
|
|
546
|
+
severity: "warn",
|
|
547
|
+
reason: `Key "${path}" is rich spans in another locale but a plain string in "${l.locale}" \u2014 spans migrations should cover all locales atomically.`,
|
|
548
|
+
remedy: `Convert "${path}" in ${l.file} to the same rich-spans shape (or revert every locale to a string).`,
|
|
549
|
+
detail: path
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return findings;
|
|
554
|
+
}
|
|
555
|
+
function getAtDottedPath(obj, dotted) {
|
|
556
|
+
let cursor = obj;
|
|
557
|
+
for (const seg of dotted.split(".")) {
|
|
558
|
+
if (cursor === null || typeof cursor !== "object") return void 0;
|
|
559
|
+
cursor = Array.isArray(cursor) ? cursor[Number(seg)] : cursor[seg];
|
|
560
|
+
}
|
|
561
|
+
return cursor;
|
|
562
|
+
}
|
|
563
|
+
function extractThemeBlock(css) {
|
|
564
|
+
const m = css.match(/@theme[^{]*\{([\s\S]*?)\}/);
|
|
565
|
+
return m ? m[1] : null;
|
|
566
|
+
}
|
|
567
|
+
function checkThemeFontOverridable(files) {
|
|
568
|
+
const findings = [];
|
|
569
|
+
for (const f of files) {
|
|
570
|
+
if (!f.path.endsWith(".css")) continue;
|
|
571
|
+
const block = extractThemeBlock(f.content);
|
|
572
|
+
if (block == null) continue;
|
|
573
|
+
for (const m of block.matchAll(/--font-(sans|heading)\s*:\s*([^;]+)/g)) {
|
|
574
|
+
const which = m[1];
|
|
575
|
+
const value = m[2].trim();
|
|
576
|
+
if (/^var\(\s*--font-(body|display)\b/.test(value)) continue;
|
|
577
|
+
if (/^var\(/.test(value)) continue;
|
|
578
|
+
findings.push({
|
|
579
|
+
file: f.path,
|
|
580
|
+
status: "theme_font_not_overridable",
|
|
581
|
+
severity: "warn",
|
|
582
|
+
reason: `--font-${which} is a literal family inside @theme \u2014 Studio font swaps won't apply (Tailwind inlines it at build).`,
|
|
583
|
+
remedy: "Move the family to a :root var and reference it: :root{--font-body:\u2026;--font-display:\u2026} + @theme{--font-sans:var(--font-body);--font-heading:var(--font-display)}.",
|
|
584
|
+
detail: `--font-${which}: ${truncate(value)}`
|
|
585
|
+
});
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return findings;
|
|
590
|
+
}
|
|
462
591
|
function runDoctorChecks(input) {
|
|
463
592
|
const findings = [
|
|
464
593
|
...checkI18nParity(input.locales),
|
|
465
594
|
...checkTokenCompliance(input.files),
|
|
466
595
|
...checkFormCompliance(input.files),
|
|
467
|
-
...checkAllowlistCompliance(input.files)
|
|
596
|
+
...checkAllowlistCompliance(input.files),
|
|
597
|
+
...checkSpansIntegrity(input.locales),
|
|
598
|
+
...checkThemeFontOverridable(input.files)
|
|
468
599
|
];
|
|
469
600
|
const byStatus = {
|
|
470
601
|
i18n_parity: 0,
|
|
471
602
|
hardcoded_hex: 0,
|
|
472
603
|
raw_form: 0,
|
|
473
|
-
allowlist_violation: 0
|
|
604
|
+
allowlist_violation: 0,
|
|
605
|
+
spans_malformed: 0,
|
|
606
|
+
spans_shape_mismatch: 0,
|
|
607
|
+
theme_font_not_overridable: 0
|
|
474
608
|
};
|
|
475
609
|
let needsAttention = 0;
|
|
476
610
|
let warnings = 0;
|
|
@@ -486,6 +620,8 @@ export {
|
|
|
486
620
|
checkAllowlistCompliance,
|
|
487
621
|
checkFormCompliance,
|
|
488
622
|
checkI18nParity,
|
|
623
|
+
checkSpansIntegrity,
|
|
624
|
+
checkThemeFontOverridable,
|
|
489
625
|
checkTokenCompliance,
|
|
490
626
|
detectAutoWrapPagesInConfig,
|
|
491
627
|
findAutoWrapGaps,
|
package/dist/doctor.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../studio-core/src/doctor-gaps.ts","../../studio-core/src/doctor.ts","../../studio-core/src/doctor-config.ts","../../studio-core/src/doctor-checks.ts"],"sourcesContent":["/**\n * autoWrap gap report (brownfield onboarding Fase 3). Pure + I/O-free.\n *\n * `@saastro/studio`'s autoWrap auto-instruments ~95% of a component's fields for\n * the visual editor. The 5% it can't: an ARRAY prop iterated through a DERIVED\n * variable — `const cols = items.map(...)` in the frontmatter, then `cols.map(…)`\n * in the template. autoWrap's template regex looks for `items.map(` in the body,\n * doesn't find it, and stamps no per-item markers, so those items aren't editable\n * (the #160 warning). This detector finds those props so the onboarding UI can\n * point the author at the hand-written `data-saastro-field` escape-hatch.\n *\n * Mirrors the trigger in `packages/studio/src/vite/instrument-fields.ts`\n * (`hasMapReference` + `hasHandWrittenFieldMarker`): a gap is a prop that is\n * `.map()`-ed in the frontmatter, NOT `.map()`-ed in the template (so autoWrap\n * misses it), and carries no hand-written marker yet.\n */\n\nexport interface AutoWrapGap {\n /** The prop whose items won't be editable until marked by hand. */\n field: string\n /** Why autoWrap can't reach it. */\n reason: 'derived_variable'\n /** A ready-to-paste escape-hatch marker. */\n snippet: string\n}\n\n/** Escape a string for use inside a RegExp (ported from instrument-fields.ts). */\nfunction escapeReg(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\n/** `<name>.map(` appears in `source` (the array is iterated). */\nfunction hasMapReference(source: string, name: string): boolean {\n return new RegExp('\\\\b' + escapeReg(name) + '\\\\s*\\\\.\\\\s*map\\\\s*\\\\(').test(source)\n}\n\n/** A hand-written `data-saastro-field={`…name[…`}` already instruments it. */\nfunction hasHandWrittenFieldMarker(template: string, name: string): boolean {\n return new RegExp(\n 'data-saastro(?:-field)?\\\\s*=\\\\s*\\\\{\\\\s*`[^`]*\\\\b' + escapeReg(name) + '[[.]',\n ).test(template)\n}\n\n/** Split a `.astro` file into its frontmatter fence body and template. */\nfunction splitFrontmatter(content: string): { frontmatter: string; template: string } {\n const open = /^\\uFEFF?[ \\t]*---[ \\t]*\\r?\\n/.exec(content)\n if (!open) return { frontmatter: '', template: content }\n const bodyStart = open[0].length\n const close = /\\r?\\n[ \\t]*---[ \\t]*(?:\\r?\\n|$)/g\n close.lastIndex = bodyStart\n const cm = close.exec(content)\n if (!cm) return { frontmatter: '', template: content }\n return {\n frontmatter: content.slice(bodyStart, cm.index),\n template: content.slice(cm.index + cm[0].length),\n }\n}\n\n/**\n * Extract the local names bound from `const { … } = Astro.props`. Handles\n * defaults (`a = 1` → `a`), renames (`a: b` → `b`, the name used in code), and\n * skips rest (`...x`).\n */\nfunction extractPropNames(frontmatter: string): string[] {\n const m = /const\\s*\\{([^}]*)\\}\\s*=\\s*Astro\\.props/.exec(frontmatter)\n if (!m) return []\n return m[1]!\n .split(',')\n .map(part => {\n const raw = part.trim()\n if (!raw || raw.startsWith('...')) return ''\n const noDefault = raw.split('=')[0]!.trim()\n const renamed = noDefault.includes(':')\n ? noDefault.split(':')[1]!.trim()\n : noDefault\n return /^[A-Za-z_$][\\w$]*$/.test(renamed) ? renamed : ''\n })\n .filter(Boolean)\n}\n\n/**\n * Find array props that autoWrap can't auto-instrument in one `.astro` file.\n * Pure: same input → same output. Never throws.\n */\nexport function findAutoWrapGaps(content: string): AutoWrapGap[] {\n const { frontmatter, template } = splitFrontmatter(content)\n const props = extractPropNames(frontmatter)\n const gaps: AutoWrapGap[] = []\n for (const name of props) {\n if (\n hasMapReference(frontmatter, name) &&\n !hasMapReference(template, name) &&\n !hasHandWrittenFieldMarker(template, name)\n ) {\n gaps.push({\n field: name,\n reason: 'derived_variable',\n snippet: `<img src={item.image} data-saastro-field={\\`${name}[\\${i}].image\\`} />`,\n })\n }\n }\n return gaps\n}\n","/**\n * Studio \"doctor\" — deterministic, I/O-free analyzer that classifies every\n * candidate section in a connected site repo by whether the Studio can edit it,\n * and (when it can't) names the precise missing convention + a ready-to-paste\n * snippet. NO LLM — pure regex + i18n-key set membership, so it's unit-testable\n * and identical run from the Hub or a CLI.\n *\n * It mirrors what the runtime resolver (`sections.ts` → `findOwnerFiles`) and the\n * overlay's marker (`data-saastro=\"sec:<key>\"`) actually do, so the report agrees\n * with the live editor:\n * - a section is editable via SCHEMA when a file declares its `fieldPrefix`\n * (call-site literal or `?? '<key>'` / destructuring default) AND has an\n * `interface *Props` (Hero-style),\n * - editable via I18N when it carries a `data-saastro=\"sec:<key>\"` marker (or a\n * fieldPrefix) whose `<key>` resolves to a translations namespace (the\n * platform i18n-forward path),\n * - a stranded section is one with a marker but neither schema nor i18n, or a\n * component that renders `t.<key>` content but carries no marker at all.\n *\n * The route (`doctor-route.ts`) does the I/O (list repo + read i18n keys) and\n * feeds this module; everything here is pure.\n */\n\nimport {\n hasFrontmatterFieldPrefix,\n hasPageContentAnchor,\n hasPageMarker,\n resolvePageKey,\n routeBaseName,\n} from './auto-wrap-core.js'\nimport { findAutoWrapGaps } from './doctor-gaps.js'\n\nexport type DoctorStatus =\n | 'editable_props' // owner file with fieldPrefix + interface Props → full schema editing\n | 'editable_i18n' // marker/fieldPrefix + matching i18n namespace → i18n-derived fields\n | 'no_schema_no_i18n' // marked but neither schema nor i18n resolves — the dead-end\n | 'missing_marker' // renders t.<key> content but has no marker/fieldPrefix → not enumerable\n | 'autowrap_gap' // editable, but a derived array prop won't auto-instrument (#160)\n\nexport interface DoctorSection {\n /** Repo-relative file the finding is anchored to. */\n file: string\n /** The section key (data-saastro / fieldPrefix / suggested i18n namespace). */\n sectionPrefix: string\n status: DoctorStatus\n /** One-line human explanation of why this status. */\n reason: string\n /** What to do about it (empty for the already-editable statuses). */\n remedy: string\n /** A ready-to-paste snippet, when there is a mechanical fix. */\n snippet?: string\n}\n\nexport interface DoctorReport {\n sections: DoctorSection[]\n stats: {\n files: number\n candidates: number\n editable: number\n needsAttention: number\n byStatus: Record<DoctorStatus, number>\n }\n}\n\n// ── Regexes (mirror sections.ts / the overlay marker) ───────────────────────\n\n/** Hand-written literal marker: `data-saastro=\"sec:<key>\"`. */\nconst MARKER_LITERAL_RE = /data-saastro\\s*=\\s*[\"']sec:([^\"']+)[\"']/g\n/** Hero-style template marker default: ``data-saastro={`sec:${fieldPrefix ?? 'hero.home'}`}``. */\nconst MARKER_DEFAULT_RE = /data-saastro\\s*=\\s*\\{\\s*`sec:\\$\\{[^}]*?\\?\\?\\s*[\"']([^\"']+)[\"']/g\n/** Call-site / destructuring literal: `fieldPrefix=\"x\"` or `fieldPrefix={`x`}` / `{\"x\"}`. */\nconst FIELDPREFIX_ATTR_RE = /fieldPrefix\\s*=\\s*(?:[\"']([^\"']+)[\"']|\\{\\s*[`\"']([^`\"'}]+)[`\"']\\s*\\})/g\n/** Component `?? '<key>'` fallback near a fieldPrefix usage. */\nconst FIELDPREFIX_NULLISH_RE = /fieldPrefix[^\\n]*?\\?\\?\\s*[\"'`]([^\"'`]+)[\"'`]/g\n/** `interface Props` / `interface <Name>Props` / `type *Props` — same as sections.ts. */\nconst PROPS_DECL_RE = /\\b(?:interface|type)\\s+\\w*Props\\b/\n/** `t.<key>` / `Astro.locals.t.<key>` usage — the i18n namespace a file reads. */\nconst T_USAGE_RE = /(?:Astro\\.locals\\.)?\\bt\\.([A-Za-z_$][\\w$]*)/g\n\nfunction matchAll(re: RegExp, s: string): string[] {\n const out: string[] = []\n re.lastIndex = 0\n let m: RegExpExecArray | null\n while ((m = re.exec(s)) !== null) {\n // Pick the first defined capture group (FIELDPREFIX_ATTR_RE has two).\n const v = m[1] ?? m[2]\n if (v) out.push(v)\n }\n return out\n}\n\n/** `src/components/HomeValues.astro` → `homeValues` (basename, lower-first). */\nfunction fileBaseCamel(path: string): string {\n const base = path.split('/').pop() ?? path\n const name = base.replace(/\\.(astro|tsx|jsx)$/, '')\n return name.charAt(0).toLowerCase() + name.slice(1)\n}\n\nfunction isPage(path: string): boolean {\n return /(^|\\/)src\\/pages\\//.test(path)\n}\n\nfunction isLayout(path: string): boolean {\n return /(^|\\/)src\\/layouts\\//.test(path) || /layout/i.test(path.split('/').pop() ?? '')\n}\n\n/** Does an i18n top-level key set resolve this (possibly dotted) section key? */\nfunction i18nResolves(keys: ReadonlySet<string>, prefix: string): boolean {\n if (keys.has(prefix)) return true\n if (prefix.includes('.')) {\n const head = prefix.split('.')[0]\n if (head && keys.has(head)) return true\n }\n return false\n}\n\ninterface FileFacts {\n path: string\n markerKeys: string[] // literal sec:<k> + template default\n ownerKeys: string[] // fieldPrefix attr / nullish default\n hasProps: boolean\n tKeys: string[] // referenced i18n namespaces\n isPage: boolean\n isLayout: boolean\n content: string\n}\n\nfunction readFacts(path: string, content: string): FileFacts {\n return {\n path,\n markerKeys: [...matchAll(MARKER_LITERAL_RE, content), ...matchAll(MARKER_DEFAULT_RE, content)],\n ownerKeys: [\n ...matchAll(FIELDPREFIX_ATTR_RE, content),\n ...matchAll(FIELDPREFIX_NULLISH_RE, content),\n ],\n hasProps: PROPS_DECL_RE.test(content),\n tKeys: [...new Set(matchAll(T_USAGE_RE, content))],\n isPage: isPage(path),\n isLayout: isLayout(path),\n content,\n }\n}\n\n/**\n * Classify every candidate section across the repo. Pure: same files + i18n keys\n * → same report.\n */\nexport function analyzeSections(\n files: ReadonlyArray<{ path: string; content: string }>,\n i18nKeys: ReadonlySet<string>,\n autoWrapPagesEnabled: boolean | 'unknown' = false,\n): DoctorReport {\n const facts = files.map(f => readFacts(f.path, f.content))\n\n // Cross-file maps. A section key is \"props-editable\" when SOME file declares\n // its fieldPrefix AND has an interface Props (mirrors matchesComponent). It is\n // \"instrumented\" when it has a literal marker OR a fieldPrefix anywhere (the\n // plugin injects the marker at runtime from the fieldPrefix).\n const propsEditableKeys = new Set<string>()\n const instrumentedKeys = new Set<string>()\n const keyToFile = new Map<string, string>() // representative file for a key\n\n // Attribution rank for a key's representative file: a schema owner (interface\n // Props) > a component DECLARING fieldPrefix in its frontmatter (i18n-only\n // owner) or carrying a hand-written marker > a mere call-site mention\n // (`<Hero fieldPrefix=\"x\" />` on a page) — so findings anchor to the\n // component that OWNS the section, not the page that happens to use it.\n // First file wins within the same rank.\n const keyRank = new Map<string, number>()\n const setKeyFile = (k: string, path: string, rank: number) => {\n if ((keyRank.get(k) ?? 0) >= rank) return\n keyRank.set(k, rank)\n keyToFile.set(k, path)\n }\n\n for (const f of facts) {\n for (const k of f.markerKeys) {\n instrumentedKeys.add(k)\n setKeyFile(k, f.path, 2)\n }\n const ownerRank = f.hasProps ? 3 : hasFrontmatterFieldPrefix(f.content) ? 2 : 1\n for (const k of f.ownerKeys) {\n instrumentedKeys.add(k)\n setKeyFile(k, f.path, ownerRank)\n if (f.hasProps) propsEditableKeys.add(k)\n }\n }\n\n const sections: DoctorSection[] = []\n const seen = new Set<string>() // `${status}:${key}:${file}` dedupe\n\n const push = (s: DoctorSection) => {\n const id = `${s.status}:${s.sectionPrefix}:${s.file}`\n if (seen.has(id)) return\n seen.add(id)\n sections.push(s)\n }\n\n // 1) Classify every instrumented candidate key.\n for (const key of instrumentedKeys) {\n const file = keyToFile.get(key) ?? ''\n if (propsEditableKeys.has(key)) {\n push({\n file,\n sectionPrefix: key,\n status: 'editable_props',\n reason: 'Has a fieldPrefix + interface Props — full schema editing.',\n remedy: '',\n })\n } else if (i18nResolves(i18nKeys, key)) {\n push({\n file,\n sectionPrefix: key,\n status: 'editable_i18n',\n reason: 'Marker + matching i18n namespace — editable via translation fields.',\n remedy: '',\n })\n } else {\n push({\n file,\n sectionPrefix: key,\n status: 'no_schema_no_i18n',\n reason:\n 'Marked section but no interface Props and no matching i18n namespace — nothing to edit.',\n remedy: `Add an i18n namespace \"${key}\" to your translations, or give the component an interface Props (+ fieldPrefix=\"${key}\").`,\n })\n }\n }\n\n // 2) missing_marker — a component that renders t.<key> content (key ∈ i18n)\n // but has NO marker and NO fieldPrefix at all → the overlay can't enumerate\n // it. (Pages are handled by step 2b below; skip layouts.) Pick the t.<key>\n // that best matches the file basename.\n for (const f of facts) {\n if (f.markerKeys.length > 0 || f.ownerKeys.length > 0 || f.isPage || f.isLayout) continue\n const i18nRefs = f.tKeys.filter(k => i18nKeys.has(k) && !instrumentedKeys.has(k))\n if (i18nRefs.length === 0) continue\n const base = fileBaseCamel(f.path)\n const key = i18nRefs.includes(base) ? base : i18nRefs[0]!\n push({\n file: f.path,\n sectionPrefix: key,\n status: 'missing_marker',\n reason: `Renders i18n content (t.${key}) but has no data-saastro marker — the editor can't see it.`,\n remedy: `Add a marker to the section's root element so the Studio enumerates it (fields then come from t.${key}).`,\n snippet: `data-saastro=\"sec:${key}\"`,\n })\n }\n\n // 2b) Route pages (src/pages/**). Mirror the @saastro/studio autoWrapPages\n // decision using the SHARED parser (resolvePageKey / hasPageContentAnchor /\n // hasFrontmatterFieldPrefix / hasPageMarker), so the verdict equals what the\n // plugin actually stamps — no re-implementation, no drift. The plugin stamps\n // iff: not a frontmatter-fieldPrefix section, no existing marker, the route\n // matches a referenced i18n namespace, there's a content element to anchor,\n // AND autoWrapPages is enabled in the site's astro.config.\n for (const f of facts) {\n // Gate ONLY on src/pages membership (mirrors the plugin, which has no\n // name/path filter beyond routeBaseName). NOT on isLayout: a route page\n // named like a layout (e.g. src/pages/Layout.astro) is still a real route\n // the plugin stamps — routeBaseName already rejects non-src/pages files.\n if (!f.isPage) continue\n if (hasFrontmatterFieldPrefix(f.content)) continue // section path (transformAstroSource), not a page wrap\n if (hasPageMarker(f.content)) continue // already carries a real marker\n if (!routeBaseName(f.path)) continue // dynamic / non-keyable route\n const routeBase = routeBaseName(f.path)!\n const key = resolvePageKey(f.content, f.path)\n const anchor = hasPageContentAnchor(f.content)\n\n if (key && anchor) {\n // Already covered by child sections: the page mounts sub-namespace\n // sections (e.g. colaboradores.astro renders <ColabHero\n // fieldPrefix=\"colaboradores.hero\" />, .benefits, .dark). The plugin does\n // NOT auto-wrap such a page — it has fieldPrefix props — and it doesn't\n // need to: the sub-sections are individually editable. Flagging a missing\n // `sec:colaboradores` marker is a false positive.\n const coveredBySubsections = Array.from(instrumentedKeys).some(k =>\n k.startsWith(`${key}.`),\n )\n if (coveredBySubsections) continue\n\n if (autoWrapPagesEnabled === true) {\n push({\n file: f.path,\n sectionPrefix: key,\n status: 'editable_i18n',\n reason: `Route page rendering t.${key} on a content element matching its path — auto-instrumented as sec:${key} (autoWrapPages is on).`,\n remedy: '',\n })\n } else if (autoWrapPagesEnabled === 'unknown') {\n push({\n file: f.path,\n sectionPrefix: key,\n status: 'missing_marker',\n reason: `Route page renders t.${key} on a content element matching its path, but astro.config sets autoWrap/autoWrapPages via a variable — the doctor can't verify pages auto-instrument.`,\n remedy: `Use literal flags — saastroStudio({ autoWrap: true, autoWrapPages: true }) — so the doctor can verify it, or add the marker by hand.`,\n snippet: `data-saastro=\"sec:${key}\"`,\n })\n } else {\n push({\n file: f.path,\n sectionPrefix: key,\n status: 'missing_marker',\n reason: `Route page renders t.${key} on a content element matching its path, but autoWrapPages is OFF so no marker ships.`,\n remedy: `Enable it once — saastroStudio({ autoWrap: true, autoWrapPages: true }) in astro.config — or add the marker by hand.`,\n snippet: `data-saastro=\"sec:${key}\"`,\n })\n }\n continue\n }\n if (key && !anchor) {\n push({\n file: f.path,\n sectionPrefix: key,\n status: 'missing_marker',\n reason: `Route page renders t.${key} only through child components (no HTML element to anchor) — autoWrapPages can't mark it here.`,\n remedy: `Mark the component that renders the content (give it fieldPrefix or data-saastro=\"sec:${key}\"), or wrap the page body in a plain element.`,\n snippet: `data-saastro=\"sec:${key}\"`,\n })\n continue\n }\n // No namespace matches the route. If it renders some resolvable i18n on an\n // element, the route name just doesn't match → suggest a hand marker.\n if (anchor) {\n const refs = f.tKeys.filter(k => i18nResolves(i18nKeys, k) && !instrumentedKeys.has(k))\n if (refs.length > 0) {\n const k2 = refs[0]!\n push({\n file: f.path,\n sectionPrefix: k2,\n status: 'missing_marker',\n reason: `Route page renders i18n content (t.${k2}) that doesn't match its path \"${routeBase}\", and has no marker.`,\n remedy: `Add a marker so the Studio enumerates it (fields then come from t.${k2}).`,\n snippet: `data-saastro=\"sec:${k2}\"`,\n })\n }\n }\n }\n\n // 3) autowrap_gap — additive warnings for derived-array props that won't\n // auto-instrument (#160), on .astro files only.\n for (const f of facts) {\n if (!f.path.endsWith('.astro')) continue\n const gaps = findAutoWrapGaps(f.content)\n if (gaps.length === 0) continue\n const key = f.markerKeys[0] ?? f.ownerKeys[0] ?? fileBaseCamel(f.path)\n for (const g of gaps) {\n push({\n file: f.path,\n sectionPrefix: key,\n status: 'autowrap_gap',\n reason: `Array prop \"${g.field}\" is iterated via a derived variable — autoWrap can't instrument its items.`,\n remedy: 'Add a hand-written data-saastro-field marker on the iterated element.',\n snippet: g.snippet,\n })\n }\n }\n\n const byStatus: Record<DoctorStatus, number> = {\n editable_props: 0,\n editable_i18n: 0,\n no_schema_no_i18n: 0,\n missing_marker: 0,\n autowrap_gap: 0,\n }\n for (const s of sections) byStatus[s.status]++\n const editable = byStatus.editable_props + byStatus.editable_i18n\n const needsAttention =\n byStatus.no_schema_no_i18n + byStatus.missing_marker + byStatus.autowrap_gap\n\n return {\n sections,\n stats: {\n files: files.length,\n candidates: instrumentedKeys.size,\n editable,\n needsAttention,\n byStatus,\n },\n }\n}\n","/**\n * astro.config flag detection for the Studio doctor. Pure + I/O-free.\n *\n * Decides whether a site's astro.config ACTIVELY enables page auto-wrapping, so\n * the doctor's verdict equals what the `@saastro/studio` plugin actually stamps.\n * Shared by the Hub's doctor route and the theme's `studio:check` CLI so both\n * read the config the same way — no drift.\n */\n\n/** Doctor's view of the astro.config flags: on / off / present-but-not-verifiable. */\nexport type AutoWrapConfigState = boolean | 'unknown'\n\n/** Strip JS comments + string literals so a flag mentioned in a comment / JSDoc\n * @example / string doesn't read as enabled. Best-effort (we test for presence,\n * not evaluate), so over-stripping is fine.\n *\n * Single-pass scanner — NOT sequential regex replaces. The old version stripped\n * strings before line-comments, so an apostrophe inside a `//` comment\n * (`// ...which doesn't exist`) opened a bogus single-quoted span that ate real\n * code — including the `autoWrap: true` flag — making the doctor report\n * autoWrapPages OFF on a config that literally enables it. Stripping comments\n * first instead would break `//` inside string literals (e.g. `'http://…'`).\n * Tracking context in one pass handles both. Regex literals are left as-is\n * (copied verbatim); they don't carry flag names so it's harmless. */\nexport function stripCommentsAndStrings(s: string): string {\n let out = ''\n let i = 0\n const n = s.length\n while (i < n) {\n const ch = s[i]\n const nx = s[i + 1]\n // line comment → blank to EOL\n if (ch === '/' && nx === '/') {\n i += 2\n while (i < n && s[i] !== '\\n') i++\n out += ' '\n continue\n }\n // block comment → blank through the close\n if (ch === '/' && nx === '*') {\n i += 2\n while (i < n && !(s[i] === '*' && s[i + 1] === '/')) i++\n i += 2\n out += ' '\n continue\n }\n // string literal (', \", `) → keep the delimiters, drop the body\n if (ch === '\"' || ch === \"'\" || ch === '`') {\n const q = ch\n out += q\n i++\n while (i < n && s[i] !== q) {\n if (s[i] === '\\\\') i++ // skip the escaped char\n i++\n }\n out += q // closing delimiter (or EOF)\n i++\n continue\n }\n out += ch\n i++\n }\n return out\n}\n\n/** One flag's literal state in stripped config source. */\nfunction flagState(c: string, name: string): 'true' | 'false' | 'absent' | 'unknown' {\n if (new RegExp(`\\\\b${name}\\\\s*:\\\\s*true\\\\b`).test(c)) return 'true'\n if (new RegExp(`\\\\b${name}\\\\s*:\\\\s*false\\\\b`).test(c)) return 'false'\n // Mentioned but not with a literal value — shorthand (`{ autoWrapPages }`)\n // or a variable (`autoWrapPages: flags.pages`). We can't evaluate it.\n if (new RegExp(`\\\\b${name}\\\\b`).test(c)) return 'unknown'\n return 'absent'\n}\n\n/**\n * Does the astro.config ACTIVELY enable page auto-wrapping? The plugin is inert\n * unless `autoWrap: true` AND only wraps pages when `autoWrapPages: true`\n * (vite/index.ts: `enabled = opts.autoWrap === true`, `load()` bails when not\n * enabled). We require BOTH, on comment/string-stripped source so a documented-\n * but-not-active flag doesn't flip it on. Best-effort source scan, NOT a JS\n * eval — but instead of silently reporting OFF when a flag is set via a\n * variable/shorthand, we return `'unknown'` so the doctor can say so. A flag\n * that's absent entirely (e.g. the whole options object is spread in) still\n * reads as OFF, matching the plugin's default. Pure + exported for unit tests.\n */\nexport function detectAutoWrapPagesInConfig(source: string): AutoWrapConfigState {\n const c = stripCommentsAndStrings(source)\n const wrap = flagState(c, 'autoWrap')\n const pages = flagState(c, 'autoWrapPages')\n if (wrap === 'false' || pages === 'false') return false\n if (wrap === 'absent' || pages === 'absent') return false\n if (wrap === 'true' && pages === 'true') return true\n return 'unknown'\n}\n","/**\n * Studio doctor — extra repo checks (H2). Pure + I/O-free, so the Hub's Setup\n * doctor and the theme's `studio:check` CLI run the SAME rules (no drift).\n *\n * Covers the gate items the section classifier (`analyzeSections`, doctor.ts)\n * doesn't:\n * - i18n parity — every translation key present across all locales\n * - token compliance — no hardcoded hex colors in .astro (oklch / var(--*))\n * - form compliance — no raw <form>; forms go through <HubForm>\n * - allowlist — editable copy lives in i18n, not hardcoded in a section\n *\n * (The playbook's \"section registered in saastrocms.config / studio.config\" gate\n * is intentionally NOT implemented: section keys are per-project examples, not a\n * closed canon — the doctor validates internal coherence, not list membership.)\n *\n * Each finding carries a severity: `error` fails the gate (counts toward\n * `needsAttention`), `warn` is advisory. `runDoctorChecks` aggregates them.\n */\n\nimport { stripCommentsAndStrings } from './doctor-config.js'\n\nexport type CheckStatus =\n | 'i18n_parity' // a key present in one locale, missing in another\n | 'hardcoded_hex' // hex color literal where a token belongs\n | 'raw_form' // <form> element instead of <HubForm>\n | 'allowlist_violation' // hardcoded editable copy in a marked section\n\nexport type CheckSeverity = 'error' | 'warn'\n\nexport interface CheckFinding {\n file: string\n status: CheckStatus\n severity: CheckSeverity\n /** One-line human explanation. */\n reason: string\n /** What to do about it. */\n remedy: string\n /** The offending token (key, hex, snippet) for display. */\n detail?: string\n}\n\nexport interface ChecksReport {\n findings: CheckFinding[]\n stats: {\n /** error-severity findings — these fail the gate. */\n needsAttention: number\n /** advisory findings. */\n warnings: number\n byStatus: Record<CheckStatus, number>\n }\n}\n\n// ── 1) i18n parity ──────────────────────────────────────────────────────────\n\n/** A parsed translations file for one locale. */\nexport interface LocaleTranslations {\n /** Locale code, e.g. 'en'. */\n locale: string\n /** Translations file path (anchors findings). */\n file: string\n /** Parsed JSON/YAML object (arrays + scalars treated as leaves). */\n data: unknown\n}\n\n/** Flatten a nested translations object to dotted leaf-key paths. Arrays are\n * leaves (their shape can legitimately differ per locale). */\nfunction flattenKeys(obj: unknown, prefix = ''): string[] {\n if (obj == null || typeof obj !== 'object' || Array.isArray(obj)) {\n return prefix ? [prefix] : []\n }\n const out: string[] = []\n for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {\n const path = prefix ? `${prefix}.${k}` : k\n if (v != null && typeof v === 'object' && !Array.isArray(v)) {\n out.push(...flattenKeys(v, path))\n } else {\n out.push(path)\n }\n }\n return out\n}\n\n/** Report keys missing in any locale relative to the union across all locales. */\nexport function checkI18nParity(locales: ReadonlyArray<LocaleTranslations>): CheckFinding[] {\n if (locales.length < 2) return []\n const withKeys = locales.map(l => ({ ...l, keys: new Set(flattenKeys(l.data)) }))\n const union = new Set<string>()\n for (const l of withKeys) for (const k of l.keys) union.add(k)\n const findings: CheckFinding[] = []\n for (const l of withKeys) {\n for (const key of union) {\n if (l.keys.has(key)) continue\n findings.push({\n file: l.file,\n status: 'i18n_parity',\n severity: 'error',\n reason: `Translation key \"${key}\" is missing in locale \"${l.locale}\".`,\n remedy: `Add \"${key}\" to ${l.file} so every locale has parity.`,\n detail: key,\n })\n }\n }\n return findings\n}\n\n// ── 2) token compliance ─────────────────────────────────────────────────────\n\nconst HEX_RE = /#([0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{4}|[0-9a-fA-F]{3})\\b/g\n\n/** Collect hex color literals from a color-bearing fragment. */\nfunction collectHex(fragment: string, into: Set<string>): void {\n for (const m of fragment.matchAll(HEX_RE)) into.add('#' + m[1]!.toLowerCase())\n}\n\n/** Hex colors that leak into a component instead of a token. Scans the places a\n * brand color realistically lands: `style=` attrs, `<style>` blocks, and\n * Tailwind arbitrary values `[#fff]`. Anchor ids / hrefs (`#section`) never look\n * like hex, so they don't false-positive. Inline `<svg>` is skipped: SVG paint\n * (icons, OG-image generators) legitimately uses literal colors — oklch tokens\n * don't apply to a standalone graphic rendered to an image. */\nexport function checkTokenCompliance(\n files: ReadonlyArray<{ path: string; content: string }>,\n): CheckFinding[] {\n const findings: CheckFinding[] = []\n for (const f of files) {\n if (!f.path.endsWith('.astro')) continue\n // Drop inline SVG graphics — their fills/gradients aren't themeable UI.\n const content = f.content.replace(/<svg[\\s\\S]*?<\\/svg>/gi, ' ')\n const hits = new Set<string>()\n // Tailwind arbitrary values: bg-[#fff], text-[#abc123], [#fff]\n for (const m of content.matchAll(/\\[#([0-9a-fA-F]{3,8})\\]/g)) hits.add('#' + m[1]!.toLowerCase())\n // String style attributes: style=\"color:#fff\"\n for (const m of content.matchAll(/style\\s*=\\s*[\"']([^\"']*)[\"']/g)) collectHex(m[1]!, hits)\n // JSX object style: style={{ color: '#fff' }}\n for (const m of content.matchAll(/style\\s*=\\s*\\{\\{([^}]*)\\}\\}/g)) collectHex(m[1]!, hits)\n // <style> blocks\n for (const m of content.matchAll(/<style[^>]*>([\\s\\S]*?)<\\/style>/g)) collectHex(m[1]!, hits)\n for (const hex of hits) {\n findings.push({\n file: f.path,\n status: 'hardcoded_hex',\n severity: 'error',\n reason: `Hardcoded hex color \"${hex}\" — use a design token (oklch var) instead.`,\n remedy: `Replace ${hex} with a token: var(--primary)/--accent or add --brand in src/styles/global.css.`,\n detail: hex,\n })\n }\n }\n return findings\n}\n\n// ── 3) form compliance ──────────────────────────────────────────────────────\n\nconst RAW_FORM_RE = /<form[\\s/>]/\n\n/** A raw lowercase <form> element (HubForm renders <HubForm …>, never a bare\n * <form>). Strips comments + strings first so a `<form>` mentioned in a comment\n * or string literal doesn't trip it. */\nexport function checkFormCompliance(\n files: ReadonlyArray<{ path: string; content: string }>,\n): CheckFinding[] {\n const findings: CheckFinding[] = []\n for (const f of files) {\n if (!/\\.(astro|tsx|jsx)$/.test(f.path)) continue\n if (!RAW_FORM_RE.test(stripCommentsAndStrings(f.content))) continue\n findings.push({\n file: f.path,\n status: 'raw_form',\n severity: 'error',\n reason: 'Raw <form> element — forms must go through <HubForm> (@saastro/forms).',\n remedy: 'Replace the <form> with <HubForm> so submits hit submit.saastro.io with validation/captcha.',\n detail: '<form>',\n })\n }\n return findings\n}\n\n// ── 4) allowlist compliance ─────────────────────────────────────────────────\n\n/** Publishable paths the Hub writes back. Editable copy must live here, never\n * hardcoded in a component (which the Hub freezes at connect time). */\nconst ALLOWLIST_PREFIXES = ['src/pages/', 'src/content/', 'src/i18n/']\nconst ALLOWLIST_FILES = ['studio.config.json']\n\nfunction isAllowlisted(path: string): boolean {\n if (ALLOWLIST_FILES.some(f => path.endsWith(f))) return true\n if (path.endsWith('.md') || path.endsWith('.mdx')) return true\n return ALLOWLIST_PREFIXES.some(p => path.includes(p))\n}\n\n/** Hardcoded sentence-like text content (≥2 words) directly rendered as the\n * child of an element — i.e. NOT an `{interpolation}`. Best-effort. */\nconst LITERAL_TEXT_RE = />\\s*([A-Z][A-Za-z0-9'’,.!?-]*(?:\\s+[A-Za-z0-9'’,.!?-]+)+)\\s*</g\n\n/** Editable copy hardcoded inside a marked (`data-saastro`) section that lives\n * OUTSIDE the publish allowlist → the client can't edit it and it's lost on the\n * next Hub publish. Advisory (best-effort heuristic): real i18n-driven sections\n * render `{t.…}`/`{prop}`, not literal sentences. */\nexport function checkAllowlistCompliance(\n files: ReadonlyArray<{ path: string; content: string }>,\n): CheckFinding[] {\n const findings: CheckFinding[] = []\n for (const f of files) {\n if (!f.path.endsWith('.astro')) continue\n if (isAllowlisted(f.path)) continue // pages/content/i18n are publishable\n if (!/data-saastro\\s*=/.test(f.content)) continue // only marked (editable) sections\n const literals = new Set<string>()\n for (const m of f.content.matchAll(LITERAL_TEXT_RE)) {\n const text = m[1]!.trim()\n if (text.length >= 6) literals.add(text)\n }\n for (const text of literals) {\n findings.push({\n file: f.path,\n status: 'allowlist_violation',\n severity: 'warn',\n reason: `Hardcoded copy \"${truncate(text)}\" in a marked section — not editable via the Hub (outside the i18n allowlist).`,\n remedy: 'Move the text into an i18n namespace and render it via {t.<key>.<field>}.',\n detail: text,\n })\n }\n }\n return findings\n}\n\nfunction truncate(s: string): string {\n return s.length > 48 ? s.slice(0, 45) + '…' : s\n}\n\n// ── aggregator ───────────────────────────────────────────────────────────────\n\nexport interface DoctorChecksInput {\n files: ReadonlyArray<{ path: string; content: string }>\n locales: ReadonlyArray<LocaleTranslations>\n}\n\n/** Run all four checks and tally them (error count = needsAttention). */\nexport function runDoctorChecks(input: DoctorChecksInput): ChecksReport {\n const findings: CheckFinding[] = [\n ...checkI18nParity(input.locales),\n ...checkTokenCompliance(input.files),\n ...checkFormCompliance(input.files),\n ...checkAllowlistCompliance(input.files),\n ]\n const byStatus: Record<CheckStatus, number> = {\n i18n_parity: 0,\n hardcoded_hex: 0,\n raw_form: 0,\n allowlist_violation: 0,\n }\n let needsAttention = 0\n let warnings = 0\n for (const f of findings) {\n byStatus[f.status]++\n if (f.severity === 'error') needsAttention++\n else warnings++\n }\n return { findings, stats: { needsAttention, warnings, byStatus } }\n}\n"],"mappings":";;;;;;;;;AA2BA,SAAS,UAAU,GAAmB;AACpC,SAAO,EAAE,QAAQ,uBAAuB,MAAM;AAChD;AAGA,SAAS,gBAAgB,QAAgB,MAAuB;AAC9D,SAAO,IAAI,OAAO,QAAQ,UAAU,IAAI,IAAI,uBAAuB,EAAE,KAAK,MAAM;AAClF;AAGA,SAAS,0BAA0B,UAAkB,MAAuB;AAC1E,SAAO,IAAI;IACT,qDAAqD,UAAU,IAAI,IAAI;EACzE,EAAE,KAAK,QAAQ;AACjB;AAGA,SAAS,iBAAiB,SAA4D;AACpF,QAAM,OAAO,+BAA+B,KAAK,OAAO;AACxD,MAAI,CAAC,KAAM,QAAO,EAAE,aAAa,IAAI,UAAU,QAAQ;AACvD,QAAM,YAAY,KAAK,CAAC,EAAE;AAC1B,QAAM,QAAQ;AACd,QAAM,YAAY;AAClB,QAAM,KAAK,MAAM,KAAK,OAAO;AAC7B,MAAI,CAAC,GAAI,QAAO,EAAE,aAAa,IAAI,UAAU,QAAQ;AACrD,SAAO;IACL,aAAa,QAAQ,MAAM,WAAW,GAAG,KAAK;IAC9C,UAAU,QAAQ,MAAM,GAAG,QAAQ,GAAG,CAAC,EAAE,MAAM;EACjD;AACF;AAOA,SAAS,iBAAiB,aAA+B;AACvD,QAAM,IAAI,yCAAyC,KAAK,WAAW;AACnE,MAAI,CAAC,EAAG,QAAO,CAAC;AAChB,SAAO,EAAE,CAAC,EACP,MAAM,GAAG,EACT,IAAI,CAAA,SAAQ;AACX,UAAM,MAAM,KAAK,KAAK;AACtB,QAAI,CAAC,OAAO,IAAI,WAAW,KAAK,EAAG,QAAO;AAC1C,UAAM,YAAY,IAAI,MAAM,GAAG,EAAE,CAAC,EAAG,KAAK;AAC1C,UAAM,UAAU,UAAU,SAAS,GAAG,IAClC,UAAU,MAAM,GAAG,EAAE,CAAC,EAAG,KAAK,IAC9B;AACJ,WAAO,qBAAqB,KAAK,OAAO,IAAI,UAAU;EACxD,CAAC,EACA,OAAO,OAAO;AACnB;AAMO,SAAS,iBAAiB,SAAgC;AAC/D,QAAM,EAAE,aAAa,SAAS,IAAI,iBAAiB,OAAO;AAC1D,QAAM,QAAQ,iBAAiB,WAAW;AAC1C,QAAM,OAAsB,CAAC;AAC7B,aAAW,QAAQ,OAAO;AACxB,QACE,gBAAgB,aAAa,IAAI,KACjC,CAAC,gBAAgB,UAAU,IAAI,KAC/B,CAAC,0BAA0B,UAAU,IAAI,GACzC;AACA,WAAK,KAAK;QACR,OAAO;QACP,QAAQ;QACR,SAAS,+CAA+C,IAAI;MAC9D,CAAC;IACH;EACF;AACA,SAAO;AACT;;;ACnCA,IAAM,oBAAoB;AAE1B,IAAM,oBAAoB;AAE1B,IAAM,sBAAsB;AAE5B,IAAM,yBAAyB;AAE/B,IAAM,gBAAgB;AAEtB,IAAM,aAAa;AAEnB,SAAS,SAAS,IAAY,GAAqB;AACjD,QAAM,MAAgB,CAAC;AACvB,KAAG,YAAY;AACf,MAAI;AACJ,UAAQ,IAAI,GAAG,KAAK,CAAC,OAAO,MAAM;AAEhC,UAAM,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC;AACrB,QAAI,EAAG,KAAI,KAAK,CAAC;EACnB;AACA,SAAO;AACT;AAGA,SAAS,cAAc,MAAsB;AAC3C,QAAM,OAAO,KAAK,MAAM,GAAG,EAAE,IAAI,KAAK;AACtC,QAAM,OAAO,KAAK,QAAQ,sBAAsB,EAAE;AAClD,SAAO,KAAK,OAAO,CAAC,EAAE,YAAY,IAAI,KAAK,MAAM,CAAC;AACpD;AAEA,SAAS,OAAO,MAAuB;AACrC,SAAO,qBAAqB,KAAK,IAAI;AACvC;AAEA,SAAS,SAAS,MAAuB;AACvC,SAAO,uBAAuB,KAAK,IAAI,KAAK,UAAU,KAAK,KAAK,MAAM,GAAG,EAAE,IAAI,KAAK,EAAE;AACxF;AAGA,SAAS,aAAa,MAA2B,QAAyB;AACxE,MAAI,KAAK,IAAI,MAAM,EAAG,QAAO;AAC7B,MAAI,OAAO,SAAS,GAAG,GAAG;AACxB,UAAM,OAAO,OAAO,MAAM,GAAG,EAAE,CAAC;AAChC,QAAI,QAAQ,KAAK,IAAI,IAAI,EAAG,QAAO;EACrC;AACA,SAAO;AACT;AAaA,SAAS,UAAU,MAAc,SAA4B;AAC3D,SAAO;IACL;IACA,YAAY,CAAC,GAAG,SAAS,mBAAmB,OAAO,GAAG,GAAG,SAAS,mBAAmB,OAAO,CAAC;IAC7F,WAAW;MACT,GAAG,SAAS,qBAAqB,OAAO;MACxC,GAAG,SAAS,wBAAwB,OAAO;IAC7C;IACA,UAAU,cAAc,KAAK,OAAO;IACpC,OAAO,CAAC,GAAG,IAAI,IAAI,SAAS,YAAY,OAAO,CAAC,CAAC;IACjD,QAAQ,OAAO,IAAI;IACnB,UAAU,SAAS,IAAI;IACvB;EACF;AACF;AAMO,SAAS,gBACd,OACA,UACA,uBAA4C,OAC9B;AACd,QAAM,QAAQ,MAAM,IAAI,CAAA,MAAK,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC;AAMzD,QAAM,oBAAoB,oBAAI,IAAY;AAC1C,QAAM,mBAAmB,oBAAI,IAAY;AACzC,QAAM,YAAY,oBAAI,IAAoB;AAQ1C,QAAM,UAAU,oBAAI,IAAoB;AACxC,QAAM,aAAa,CAAC,GAAW,MAAc,SAAiB;AAC5D,SAAK,QAAQ,IAAI,CAAC,KAAK,MAAM,KAAM;AACnC,YAAQ,IAAI,GAAG,IAAI;AACnB,cAAU,IAAI,GAAG,IAAI;EACvB;AAEA,aAAW,KAAK,OAAO;AACrB,eAAW,KAAK,EAAE,YAAY;AAC5B,uBAAiB,IAAI,CAAC;AACtB,iBAAW,GAAG,EAAE,MAAM,CAAC;IACzB;AACA,UAAM,YAAY,EAAE,WAAW,IAAI,0BAA0B,EAAE,OAAO,IAAI,IAAI;AAC9E,eAAW,KAAK,EAAE,WAAW;AAC3B,uBAAiB,IAAI,CAAC;AACtB,iBAAW,GAAG,EAAE,MAAM,SAAS;AAC/B,UAAI,EAAE,SAAU,mBAAkB,IAAI,CAAC;IACzC;EACF;AAEA,QAAM,WAA4B,CAAC;AACnC,QAAM,OAAO,oBAAI,IAAY;AAE7B,QAAM,OAAO,CAAC,MAAqB;AACjC,UAAM,KAAK,GAAG,EAAE,MAAM,IAAI,EAAE,aAAa,IAAI,EAAE,IAAI;AACnD,QAAI,KAAK,IAAI,EAAE,EAAG;AAClB,SAAK,IAAI,EAAE;AACX,aAAS,KAAK,CAAC;EACjB;AAGA,aAAW,OAAO,kBAAkB;AAClC,UAAM,OAAO,UAAU,IAAI,GAAG,KAAK;AACnC,QAAI,kBAAkB,IAAI,GAAG,GAAG;AAC9B,WAAK;QACH;QACA,eAAe;QACf,QAAQ;QACR,QAAQ;QACR,QAAQ;MACV,CAAC;IACH,WAAW,aAAa,UAAU,GAAG,GAAG;AACtC,WAAK;QACH;QACA,eAAe;QACf,QAAQ;QACR,QAAQ;QACR,QAAQ;MACV,CAAC;IACH,OAAO;AACL,WAAK;QACH;QACA,eAAe;QACf,QAAQ;QACR,QACE;QACF,QAAQ,0BAA0B,GAAG,oFAAoF,GAAG;MAC9H,CAAC;IACH;EACF;AAMA,aAAW,KAAK,OAAO;AACrB,QAAI,EAAE,WAAW,SAAS,KAAK,EAAE,UAAU,SAAS,KAAK,EAAE,UAAU,EAAE,SAAU;AACjF,UAAM,WAAW,EAAE,MAAM,OAAO,CAAA,MAAK,SAAS,IAAI,CAAC,KAAK,CAAC,iBAAiB,IAAI,CAAC,CAAC;AAChF,QAAI,SAAS,WAAW,EAAG;AAC3B,UAAM,OAAO,cAAc,EAAE,IAAI;AACjC,UAAM,MAAM,SAAS,SAAS,IAAI,IAAI,OAAO,SAAS,CAAC;AACvD,SAAK;MACH,MAAM,EAAE;MACR,eAAe;MACf,QAAQ;MACR,QAAQ,2BAA2B,GAAG;MACtC,QAAQ,mGAAmG,GAAG;MAC9G,SAAS,qBAAqB,GAAG;IACnC,CAAC;EACH;AASA,aAAW,KAAK,OAAO;AAKrB,QAAI,CAAC,EAAE,OAAQ;AACf,QAAI,0BAA0B,EAAE,OAAO,EAAG;AAC1C,QAAI,cAAc,EAAE,OAAO,EAAG;AAC9B,QAAI,CAAC,cAAc,EAAE,IAAI,EAAG;AAC5B,UAAM,YAAY,cAAc,EAAE,IAAI;AACtC,UAAM,MAAM,eAAe,EAAE,SAAS,EAAE,IAAI;AAC5C,UAAM,SAAS,qBAAqB,EAAE,OAAO;AAE7C,QAAI,OAAO,QAAQ;AAOjB,YAAM,uBAAuB,MAAM,KAAK,gBAAgB,EAAE;QAAK,CAAA,MAC7D,EAAE,WAAW,GAAG,GAAG,GAAG;MACxB;AACA,UAAI,qBAAsB;AAE1B,UAAI,yBAAyB,MAAM;AACjC,aAAK;UACH,MAAM,EAAE;UACR,eAAe;UACf,QAAQ;UACR,QAAQ,0BAA0B,GAAG,2EAAsE,GAAG;UAC9G,QAAQ;QACV,CAAC;MACH,WAAW,yBAAyB,WAAW;AAC7C,aAAK;UACH,MAAM,EAAE;UACR,eAAe;UACf,QAAQ;UACR,QAAQ,wBAAwB,GAAG;UACnC,QAAQ;UACR,SAAS,qBAAqB,GAAG;QACnC,CAAC;MACH,OAAO;AACL,aAAK;UACH,MAAM,EAAE;UACR,eAAe;UACf,QAAQ;UACR,QAAQ,wBAAwB,GAAG;UACnC,QAAQ;UACR,SAAS,qBAAqB,GAAG;QACnC,CAAC;MACH;AACA;IACF;AACA,QAAI,OAAO,CAAC,QAAQ;AAClB,WAAK;QACH,MAAM,EAAE;QACR,eAAe;QACf,QAAQ;QACR,QAAQ,wBAAwB,GAAG;QACnC,QAAQ,yFAAyF,GAAG;QACpG,SAAS,qBAAqB,GAAG;MACnC,CAAC;AACD;IACF;AAGA,QAAI,QAAQ;AACV,YAAM,OAAO,EAAE,MAAM,OAAO,CAAA,MAAK,aAAa,UAAU,CAAC,KAAK,CAAC,iBAAiB,IAAI,CAAC,CAAC;AACtF,UAAI,KAAK,SAAS,GAAG;AACnB,cAAM,KAAK,KAAK,CAAC;AACjB,aAAK;UACH,MAAM,EAAE;UACR,eAAe;UACf,QAAQ;UACR,QAAQ,sCAAsC,EAAE,kCAAkC,SAAS;UAC3F,QAAQ,qEAAqE,EAAE;UAC/E,SAAS,qBAAqB,EAAE;QAClC,CAAC;MACH;IACF;EACF;AAIA,aAAW,KAAK,OAAO;AACrB,QAAI,CAAC,EAAE,KAAK,SAAS,QAAQ,EAAG;AAChC,UAAM,OAAO,iBAAiB,EAAE,OAAO;AACvC,QAAI,KAAK,WAAW,EAAG;AACvB,UAAM,MAAM,EAAE,WAAW,CAAC,KAAK,EAAE,UAAU,CAAC,KAAK,cAAc,EAAE,IAAI;AACrE,eAAW,KAAK,MAAM;AACpB,WAAK;QACH,MAAM,EAAE;QACR,eAAe;QACf,QAAQ;QACR,QAAQ,eAAe,EAAE,KAAK;QAC9B,QAAQ;QACR,SAAS,EAAE;MACb,CAAC;IACH;EACF;AAEA,QAAM,WAAyC;IAC7C,gBAAgB;IAChB,eAAe;IACf,mBAAmB;IACnB,gBAAgB;IAChB,cAAc;EAChB;AACA,aAAW,KAAK,SAAU,UAAS,EAAE,MAAM;AAC3C,QAAM,WAAW,SAAS,iBAAiB,SAAS;AACpD,QAAM,iBACJ,SAAS,oBAAoB,SAAS,iBAAiB,SAAS;AAElE,SAAO;IACL;IACA,OAAO;MACL,OAAO,MAAM;MACb,YAAY,iBAAiB;MAC7B;MACA;MACA;IACF;EACF;AACF;;;ACpWO,SAAS,wBAAwB,GAAmB;AACzD,MAAI,MAAM;AACV,MAAI,IAAI;AACR,QAAM,IAAI,EAAE;AACZ,SAAO,IAAI,GAAG;AACZ,UAAM,KAAK,EAAE,CAAC;AACd,UAAM,KAAK,EAAE,IAAI,CAAC;AAElB,QAAI,OAAO,OAAO,OAAO,KAAK;AAC5B,WAAK;AACL,aAAO,IAAI,KAAK,EAAE,CAAC,MAAM,KAAM;AAC/B,aAAO;AACP;IACF;AAEA,QAAI,OAAO,OAAO,OAAO,KAAK;AAC5B,WAAK;AACL,aAAO,IAAI,KAAK,EAAE,EAAE,CAAC,MAAM,OAAO,EAAE,IAAI,CAAC,MAAM,KAAM;AACrD,WAAK;AACL,aAAO;AACP;IACF;AAEA,QAAI,OAAO,OAAO,OAAO,OAAO,OAAO,KAAK;AAC1C,YAAM,IAAI;AACV,aAAO;AACP;AACA,aAAO,IAAI,KAAK,EAAE,CAAC,MAAM,GAAG;AAC1B,YAAI,EAAE,CAAC,MAAM,KAAM;AACnB;MACF;AACA,aAAO;AACP;AACA;IACF;AACA,WAAO;AACP;EACF;AACA,SAAO;AACT;AAGA,SAAS,UAAU,GAAW,MAAuD;AACnF,MAAI,IAAI,OAAO,MAAM,IAAI,kBAAkB,EAAE,KAAK,CAAC,EAAG,QAAO;AAC7D,MAAI,IAAI,OAAO,MAAM,IAAI,mBAAmB,EAAE,KAAK,CAAC,EAAG,QAAO;AAG9D,MAAI,IAAI,OAAO,MAAM,IAAI,KAAK,EAAE,KAAK,CAAC,EAAG,QAAO;AAChD,SAAO;AACT;AAaO,SAAS,4BAA4B,QAAqC;AAC/E,QAAM,IAAI,wBAAwB,MAAM;AACxC,QAAM,OAAO,UAAU,GAAG,UAAU;AACpC,QAAM,QAAQ,UAAU,GAAG,eAAe;AAC1C,MAAI,SAAS,WAAW,UAAU,QAAS,QAAO;AAClD,MAAI,SAAS,YAAY,UAAU,SAAU,QAAO;AACpD,MAAI,SAAS,UAAU,UAAU,OAAQ,QAAO;AAChD,SAAO;AACT;;;AC5BA,SAAS,YAAY,KAAc,SAAS,IAAc;AACxD,MAAI,OAAO,QAAQ,OAAO,QAAQ,YAAY,MAAM,QAAQ,GAAG,GAAG;AAChE,WAAO,SAAS,CAAC,MAAM,IAAI,CAAC;EAC9B;AACA,QAAM,MAAgB,CAAC;AACvB,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,GAA8B,GAAG;AACnE,UAAM,OAAO,SAAS,GAAG,MAAM,IAAI,CAAC,KAAK;AACzC,QAAI,KAAK,QAAQ,OAAO,MAAM,YAAY,CAAC,MAAM,QAAQ,CAAC,GAAG;AAC3D,UAAI,KAAK,GAAG,YAAY,GAAG,IAAI,CAAC;IAClC,OAAO;AACL,UAAI,KAAK,IAAI;IACf;EACF;AACA,SAAO;AACT;AAGO,SAAS,gBAAgB,SAA4D;AAC1F,MAAI,QAAQ,SAAS,EAAG,QAAO,CAAC;AAChC,QAAM,WAAW,QAAQ,IAAI,CAAA,OAAM,EAAE,GAAG,GAAG,MAAM,IAAI,IAAI,YAAY,EAAE,IAAI,CAAC,EAAE,EAAE;AAChF,QAAM,QAAQ,oBAAI,IAAY;AAC9B,aAAW,KAAK,SAAU,YAAW,KAAK,EAAE,KAAM,OAAM,IAAI,CAAC;AAC7D,QAAM,WAA2B,CAAC;AAClC,aAAW,KAAK,UAAU;AACxB,eAAW,OAAO,OAAO;AACvB,UAAI,EAAE,KAAK,IAAI,GAAG,EAAG;AACrB,eAAS,KAAK;QACZ,MAAM,EAAE;QACR,QAAQ;QACR,UAAU;QACV,QAAQ,oBAAoB,GAAG,2BAA2B,EAAE,MAAM;QAClE,QAAQ,QAAQ,GAAG,QAAQ,EAAE,IAAI;QACjC,QAAQ;MACV,CAAC;IACH;EACF;AACA,SAAO;AACT;AAIA,IAAM,SAAS;AAGf,SAAS,WAAW,UAAkB,MAAyB;AAC7D,aAAW,KAAK,SAAS,SAAS,MAAM,EAAG,MAAK,IAAI,MAAM,EAAE,CAAC,EAAG,YAAY,CAAC;AAC/E;AAQO,SAAS,qBACd,OACgB;AAChB,QAAM,WAA2B,CAAC;AAClC,aAAW,KAAK,OAAO;AACrB,QAAI,CAAC,EAAE,KAAK,SAAS,QAAQ,EAAG;AAEhC,UAAM,UAAU,EAAE,QAAQ,QAAQ,yBAAyB,GAAG;AAC9D,UAAM,OAAO,oBAAI,IAAY;AAE7B,eAAW,KAAK,QAAQ,SAAS,0BAA0B,EAAG,MAAK,IAAI,MAAM,EAAE,CAAC,EAAG,YAAY,CAAC;AAEhG,eAAW,KAAK,QAAQ,SAAS,+BAA+B,EAAG,YAAW,EAAE,CAAC,GAAI,IAAI;AAEzF,eAAW,KAAK,QAAQ,SAAS,8BAA8B,EAAG,YAAW,EAAE,CAAC,GAAI,IAAI;AAExF,eAAW,KAAK,QAAQ,SAAS,kCAAkC,EAAG,YAAW,EAAE,CAAC,GAAI,IAAI;AAC5F,eAAW,OAAO,MAAM;AACtB,eAAS,KAAK;QACZ,MAAM,EAAE;QACR,QAAQ;QACR,UAAU;QACV,QAAQ,wBAAwB,GAAG;QACnC,QAAQ,WAAW,GAAG;QACtB,QAAQ;MACV,CAAC;IACH;EACF;AACA,SAAO;AACT;AAIA,IAAM,cAAc;AAKb,SAAS,oBACd,OACgB;AAChB,QAAM,WAA2B,CAAC;AAClC,aAAW,KAAK,OAAO;AACrB,QAAI,CAAC,qBAAqB,KAAK,EAAE,IAAI,EAAG;AACxC,QAAI,CAAC,YAAY,KAAK,wBAAwB,EAAE,OAAO,CAAC,EAAG;AAC3D,aAAS,KAAK;MACZ,MAAM,EAAE;MACR,QAAQ;MACR,UAAU;MACV,QAAQ;MACR,QAAQ;MACR,QAAQ;IACV,CAAC;EACH;AACA,SAAO;AACT;AAMA,IAAM,qBAAqB,CAAC,cAAc,gBAAgB,WAAW;AACrE,IAAM,kBAAkB,CAAC,oBAAoB;AAE7C,SAAS,cAAc,MAAuB;AAC5C,MAAI,gBAAgB,KAAK,CAAA,MAAK,KAAK,SAAS,CAAC,CAAC,EAAG,QAAO;AACxD,MAAI,KAAK,SAAS,KAAK,KAAK,KAAK,SAAS,MAAM,EAAG,QAAO;AAC1D,SAAO,mBAAmB,KAAK,CAAA,MAAK,KAAK,SAAS,CAAC,CAAC;AACtD;AAIA,IAAM,kBAAkB;AAMjB,SAAS,yBACd,OACgB;AAChB,QAAM,WAA2B,CAAC;AAClC,aAAW,KAAK,OAAO;AACrB,QAAI,CAAC,EAAE,KAAK,SAAS,QAAQ,EAAG;AAChC,QAAI,cAAc,EAAE,IAAI,EAAG;AAC3B,QAAI,CAAC,mBAAmB,KAAK,EAAE,OAAO,EAAG;AACzC,UAAM,WAAW,oBAAI,IAAY;AACjC,eAAW,KAAK,EAAE,QAAQ,SAAS,eAAe,GAAG;AACnD,YAAM,OAAO,EAAE,CAAC,EAAG,KAAK;AACxB,UAAI,KAAK,UAAU,EAAG,UAAS,IAAI,IAAI;IACzC;AACA,eAAW,QAAQ,UAAU;AAC3B,eAAS,KAAK;QACZ,MAAM,EAAE;QACR,QAAQ;QACR,UAAU;QACV,QAAQ,mBAAmB,SAAS,IAAI,CAAC;QACzC,QAAQ;QACR,QAAQ;MACV,CAAC;IACH;EACF;AACA,SAAO;AACT;AAEA,SAAS,SAAS,GAAmB;AACnC,SAAO,EAAE,SAAS,KAAK,EAAE,MAAM,GAAG,EAAE,IAAI,WAAM;AAChD;AAUO,SAAS,gBAAgB,OAAwC;AACtE,QAAM,WAA2B;IAC/B,GAAG,gBAAgB,MAAM,OAAO;IAChC,GAAG,qBAAqB,MAAM,KAAK;IACnC,GAAG,oBAAoB,MAAM,KAAK;IAClC,GAAG,yBAAyB,MAAM,KAAK;EACzC;AACA,QAAM,WAAwC;IAC5C,aAAa;IACb,eAAe;IACf,UAAU;IACV,qBAAqB;EACvB;AACA,MAAI,iBAAiB;AACrB,MAAI,WAAW;AACf,aAAW,KAAK,UAAU;AACxB,aAAS,EAAE,MAAM;AACjB,QAAI,EAAE,aAAa,QAAS;QACvB;EACP;AACA,SAAO,EAAE,UAAU,OAAO,EAAE,gBAAgB,UAAU,SAAS,EAAE;AACnE;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../studio-core/src/doctor-gaps.ts","../../studio-core/src/doctor.ts","../../studio-core/src/doctor-config.ts","../../studio-core/src/doctor-checks.ts"],"sourcesContent":["/**\n * autoWrap gap report (brownfield onboarding Fase 3). Pure + I/O-free.\n *\n * `@saastro/studio`'s autoWrap auto-instruments ~95% of a component's fields for\n * the visual editor. The 5% it can't: an ARRAY prop iterated through a DERIVED\n * variable — `const cols = items.map(...)` in the frontmatter, then `cols.map(…)`\n * in the template. autoWrap's template regex looks for `items.map(` in the body,\n * doesn't find it, and stamps no per-item markers, so those items aren't editable\n * (the #160 warning). This detector finds those props so the onboarding UI can\n * point the author at the hand-written `data-saastro-field` escape-hatch.\n *\n * Mirrors the trigger in `packages/studio/src/vite/instrument-fields.ts`\n * (`hasMapReference` + `hasHandWrittenFieldMarker`): a gap is a prop that is\n * `.map()`-ed in the frontmatter, NOT `.map()`-ed in the template (so autoWrap\n * misses it), and carries no hand-written marker yet.\n */\n\nexport interface AutoWrapGap {\n /** The prop whose items won't be editable until marked by hand. */\n field: string\n /** Why autoWrap can't reach it. */\n reason: 'derived_variable'\n /** A ready-to-paste escape-hatch marker. */\n snippet: string\n}\n\n/** Escape a string for use inside a RegExp (ported from instrument-fields.ts). */\nfunction escapeReg(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\n/** `<name>.map(` appears in `source` (the array is iterated). */\nfunction hasMapReference(source: string, name: string): boolean {\n return new RegExp('\\\\b' + escapeReg(name) + '\\\\s*\\\\.\\\\s*map\\\\s*\\\\(').test(source)\n}\n\n/** A hand-written `data-saastro-field={`…name[…`}` already instruments it. */\nfunction hasHandWrittenFieldMarker(template: string, name: string): boolean {\n return new RegExp(\n 'data-saastro(?:-field)?\\\\s*=\\\\s*\\\\{\\\\s*`[^`]*\\\\b' + escapeReg(name) + '[[.]',\n ).test(template)\n}\n\n/** Split a `.astro` file into its frontmatter fence body and template. */\nfunction splitFrontmatter(content: string): { frontmatter: string; template: string } {\n const open = /^\\uFEFF?[ \\t]*---[ \\t]*\\r?\\n/.exec(content)\n if (!open) return { frontmatter: '', template: content }\n const bodyStart = open[0].length\n const close = /\\r?\\n[ \\t]*---[ \\t]*(?:\\r?\\n|$)/g\n close.lastIndex = bodyStart\n const cm = close.exec(content)\n if (!cm) return { frontmatter: '', template: content }\n return {\n frontmatter: content.slice(bodyStart, cm.index),\n template: content.slice(cm.index + cm[0].length),\n }\n}\n\n/**\n * Extract the local names bound from `const { … } = Astro.props`. Handles\n * defaults (`a = 1` → `a`), renames (`a: b` → `b`, the name used in code), and\n * skips rest (`...x`).\n */\nfunction extractPropNames(frontmatter: string): string[] {\n const m = /const\\s*\\{([^}]*)\\}\\s*=\\s*Astro\\.props/.exec(frontmatter)\n if (!m) return []\n return m[1]!\n .split(',')\n .map(part => {\n const raw = part.trim()\n if (!raw || raw.startsWith('...')) return ''\n const noDefault = raw.split('=')[0]!.trim()\n const renamed = noDefault.includes(':')\n ? noDefault.split(':')[1]!.trim()\n : noDefault\n return /^[A-Za-z_$][\\w$]*$/.test(renamed) ? renamed : ''\n })\n .filter(Boolean)\n}\n\n/**\n * Find array props that autoWrap can't auto-instrument in one `.astro` file.\n * Pure: same input → same output. Never throws.\n */\nexport function findAutoWrapGaps(content: string): AutoWrapGap[] {\n const { frontmatter, template } = splitFrontmatter(content)\n const props = extractPropNames(frontmatter)\n const gaps: AutoWrapGap[] = []\n for (const name of props) {\n if (\n hasMapReference(frontmatter, name) &&\n !hasMapReference(template, name) &&\n !hasHandWrittenFieldMarker(template, name)\n ) {\n gaps.push({\n field: name,\n reason: 'derived_variable',\n snippet: `<img src={item.image} data-saastro-field={\\`${name}[\\${i}].image\\`} />`,\n })\n }\n }\n return gaps\n}\n","/**\n * Studio \"doctor\" — deterministic, I/O-free analyzer that classifies every\n * candidate section in a connected site repo by whether the Studio can edit it,\n * and (when it can't) names the precise missing convention + a ready-to-paste\n * snippet. NO LLM — pure regex + i18n-key set membership, so it's unit-testable\n * and identical run from the Hub or a CLI.\n *\n * It mirrors what the runtime resolver (`sections.ts` → `findOwnerFiles`) and the\n * overlay's marker (`data-saastro=\"sec:<key>\"`) actually do, so the report agrees\n * with the live editor:\n * - a section is editable via SCHEMA when a file declares its `fieldPrefix`\n * (call-site literal or `?? '<key>'` / destructuring default) AND has an\n * `interface *Props` (Hero-style),\n * - editable via I18N when it carries a `data-saastro=\"sec:<key>\"` marker (or a\n * fieldPrefix) whose `<key>` resolves to a translations namespace (the\n * platform i18n-forward path),\n * - a stranded section is one with a marker but neither schema nor i18n, or a\n * component that renders `t.<key>` content but carries no marker at all.\n *\n * The route (`doctor-route.ts`) does the I/O (list repo + read i18n keys) and\n * feeds this module; everything here is pure.\n */\n\nimport {\n hasFrontmatterFieldPrefix,\n hasPageContentAnchor,\n hasPageMarker,\n resolvePageKey,\n routeBaseName,\n} from './auto-wrap-core.js'\nimport { findAutoWrapGaps } from './doctor-gaps.js'\n\nexport type DoctorStatus =\n | 'editable_props' // owner file with fieldPrefix + interface Props → full schema editing\n | 'editable_i18n' // marker/fieldPrefix + matching i18n namespace → i18n-derived fields\n | 'no_schema_no_i18n' // marked but neither schema nor i18n resolves — the dead-end\n | 'missing_marker' // renders t.<key> content but has no marker/fieldPrefix → not enumerable\n | 'autowrap_gap' // editable, but a derived array prop won't auto-instrument (#160)\n\nexport interface DoctorSection {\n /** Repo-relative file the finding is anchored to. */\n file: string\n /** The section key (data-saastro / fieldPrefix / suggested i18n namespace). */\n sectionPrefix: string\n status: DoctorStatus\n /** One-line human explanation of why this status. */\n reason: string\n /** What to do about it (empty for the already-editable statuses). */\n remedy: string\n /** A ready-to-paste snippet, when there is a mechanical fix. */\n snippet?: string\n}\n\nexport interface DoctorReport {\n sections: DoctorSection[]\n stats: {\n files: number\n candidates: number\n editable: number\n needsAttention: number\n byStatus: Record<DoctorStatus, number>\n }\n}\n\n// ── Regexes (mirror sections.ts / the overlay marker) ───────────────────────\n\n/** Hand-written literal marker: `data-saastro=\"sec:<key>\"`. */\nconst MARKER_LITERAL_RE = /data-saastro\\s*=\\s*[\"']sec:([^\"']+)[\"']/g\n/** Hero-style template marker default: ``data-saastro={`sec:${fieldPrefix ?? 'hero.home'}`}``. */\nconst MARKER_DEFAULT_RE = /data-saastro\\s*=\\s*\\{\\s*`sec:\\$\\{[^}]*?\\?\\?\\s*[\"']([^\"']+)[\"']/g\n/** Call-site / destructuring literal: `fieldPrefix=\"x\"` or `fieldPrefix={`x`}` / `{\"x\"}`. */\nconst FIELDPREFIX_ATTR_RE = /fieldPrefix\\s*=\\s*(?:[\"']([^\"']+)[\"']|\\{\\s*[`\"']([^`\"'}]+)[`\"']\\s*\\})/g\n/** Component `?? '<key>'` fallback near a fieldPrefix usage. */\nconst FIELDPREFIX_NULLISH_RE = /fieldPrefix[^\\n]*?\\?\\?\\s*[\"'`]([^\"'`]+)[\"'`]/g\n/** `interface Props` / `interface <Name>Props` / `type *Props` — same as sections.ts. */\nconst PROPS_DECL_RE = /\\b(?:interface|type)\\s+\\w*Props\\b/\n/** `t.<key>` / `Astro.locals.t.<key>` usage — the i18n namespace a file reads. */\nconst T_USAGE_RE = /(?:Astro\\.locals\\.)?\\bt\\.([A-Za-z_$][\\w$]*)/g\n\nfunction matchAll(re: RegExp, s: string): string[] {\n const out: string[] = []\n re.lastIndex = 0\n let m: RegExpExecArray | null\n while ((m = re.exec(s)) !== null) {\n // Pick the first defined capture group (FIELDPREFIX_ATTR_RE has two).\n const v = m[1] ?? m[2]\n if (v) out.push(v)\n }\n return out\n}\n\n/** `src/components/HomeValues.astro` → `homeValues` (basename, lower-first). */\nfunction fileBaseCamel(path: string): string {\n const base = path.split('/').pop() ?? path\n const name = base.replace(/\\.(astro|tsx|jsx)$/, '')\n return name.charAt(0).toLowerCase() + name.slice(1)\n}\n\nfunction isPage(path: string): boolean {\n return /(^|\\/)src\\/pages\\//.test(path)\n}\n\nfunction isLayout(path: string): boolean {\n return /(^|\\/)src\\/layouts\\//.test(path) || /layout/i.test(path.split('/').pop() ?? '')\n}\n\n/** Does an i18n top-level key set resolve this (possibly dotted) section key? */\nfunction i18nResolves(keys: ReadonlySet<string>, prefix: string): boolean {\n if (keys.has(prefix)) return true\n if (prefix.includes('.')) {\n const head = prefix.split('.')[0]\n if (head && keys.has(head)) return true\n }\n return false\n}\n\ninterface FileFacts {\n path: string\n markerKeys: string[] // literal sec:<k> + template default\n ownerKeys: string[] // fieldPrefix attr / nullish default\n hasProps: boolean\n tKeys: string[] // referenced i18n namespaces\n isPage: boolean\n isLayout: boolean\n content: string\n}\n\nfunction readFacts(path: string, content: string): FileFacts {\n return {\n path,\n markerKeys: [...matchAll(MARKER_LITERAL_RE, content), ...matchAll(MARKER_DEFAULT_RE, content)],\n ownerKeys: [\n ...matchAll(FIELDPREFIX_ATTR_RE, content),\n ...matchAll(FIELDPREFIX_NULLISH_RE, content),\n ],\n hasProps: PROPS_DECL_RE.test(content),\n tKeys: [...new Set(matchAll(T_USAGE_RE, content))],\n isPage: isPage(path),\n isLayout: isLayout(path),\n content,\n }\n}\n\n/**\n * Classify every candidate section across the repo. Pure: same files + i18n keys\n * → same report.\n */\nexport function analyzeSections(\n files: ReadonlyArray<{ path: string; content: string }>,\n i18nKeys: ReadonlySet<string>,\n autoWrapPagesEnabled: boolean | 'unknown' = false,\n): DoctorReport {\n const facts = files.map(f => readFacts(f.path, f.content))\n\n // Cross-file maps. A section key is \"props-editable\" when SOME file declares\n // its fieldPrefix AND has an interface Props (mirrors matchesComponent). It is\n // \"instrumented\" when it has a literal marker OR a fieldPrefix anywhere (the\n // plugin injects the marker at runtime from the fieldPrefix).\n const propsEditableKeys = new Set<string>()\n const instrumentedKeys = new Set<string>()\n const keyToFile = new Map<string, string>() // representative file for a key\n\n // Attribution rank for a key's representative file: a schema owner (interface\n // Props) > a component DECLARING fieldPrefix in its frontmatter (i18n-only\n // owner) or carrying a hand-written marker > a mere call-site mention\n // (`<Hero fieldPrefix=\"x\" />` on a page) — so findings anchor to the\n // component that OWNS the section, not the page that happens to use it.\n // First file wins within the same rank.\n const keyRank = new Map<string, number>()\n const setKeyFile = (k: string, path: string, rank: number) => {\n if ((keyRank.get(k) ?? 0) >= rank) return\n keyRank.set(k, rank)\n keyToFile.set(k, path)\n }\n\n for (const f of facts) {\n for (const k of f.markerKeys) {\n instrumentedKeys.add(k)\n setKeyFile(k, f.path, 2)\n }\n const ownerRank = f.hasProps ? 3 : hasFrontmatterFieldPrefix(f.content) ? 2 : 1\n for (const k of f.ownerKeys) {\n instrumentedKeys.add(k)\n setKeyFile(k, f.path, ownerRank)\n if (f.hasProps) propsEditableKeys.add(k)\n }\n }\n\n const sections: DoctorSection[] = []\n const seen = new Set<string>() // `${status}:${key}:${file}` dedupe\n\n const push = (s: DoctorSection) => {\n const id = `${s.status}:${s.sectionPrefix}:${s.file}`\n if (seen.has(id)) return\n seen.add(id)\n sections.push(s)\n }\n\n // 1) Classify every instrumented candidate key.\n for (const key of instrumentedKeys) {\n const file = keyToFile.get(key) ?? ''\n if (propsEditableKeys.has(key)) {\n push({\n file,\n sectionPrefix: key,\n status: 'editable_props',\n reason: 'Has a fieldPrefix + interface Props — full schema editing.',\n remedy: '',\n })\n } else if (i18nResolves(i18nKeys, key)) {\n push({\n file,\n sectionPrefix: key,\n status: 'editable_i18n',\n reason: 'Marker + matching i18n namespace — editable via translation fields.',\n remedy: '',\n })\n } else {\n push({\n file,\n sectionPrefix: key,\n status: 'no_schema_no_i18n',\n reason:\n 'Marked section but no interface Props and no matching i18n namespace — nothing to edit.',\n remedy: `Add an i18n namespace \"${key}\" to your translations, or give the component an interface Props (+ fieldPrefix=\"${key}\").`,\n })\n }\n }\n\n // 2) missing_marker — a component that renders t.<key> content (key ∈ i18n)\n // but has NO marker and NO fieldPrefix at all → the overlay can't enumerate\n // it. (Pages are handled by step 2b below; skip layouts.) Pick the t.<key>\n // that best matches the file basename.\n for (const f of facts) {\n if (f.markerKeys.length > 0 || f.ownerKeys.length > 0 || f.isPage || f.isLayout) continue\n const i18nRefs = f.tKeys.filter(k => i18nKeys.has(k) && !instrumentedKeys.has(k))\n if (i18nRefs.length === 0) continue\n const base = fileBaseCamel(f.path)\n const key = i18nRefs.includes(base) ? base : i18nRefs[0]!\n push({\n file: f.path,\n sectionPrefix: key,\n status: 'missing_marker',\n reason: `Renders i18n content (t.${key}) but has no data-saastro marker — the editor can't see it.`,\n remedy: `Add a marker to the section's root element so the Studio enumerates it (fields then come from t.${key}).`,\n snippet: `data-saastro=\"sec:${key}\"`,\n })\n }\n\n // 2b) Route pages (src/pages/**). Mirror the @saastro/studio autoWrapPages\n // decision using the SHARED parser (resolvePageKey / hasPageContentAnchor /\n // hasFrontmatterFieldPrefix / hasPageMarker), so the verdict equals what the\n // plugin actually stamps — no re-implementation, no drift. The plugin stamps\n // iff: not a frontmatter-fieldPrefix section, no existing marker, the route\n // matches a referenced i18n namespace, there's a content element to anchor,\n // AND autoWrapPages is enabled in the site's astro.config.\n for (const f of facts) {\n // Gate ONLY on src/pages membership (mirrors the plugin, which has no\n // name/path filter beyond routeBaseName). NOT on isLayout: a route page\n // named like a layout (e.g. src/pages/Layout.astro) is still a real route\n // the plugin stamps — routeBaseName already rejects non-src/pages files.\n if (!f.isPage) continue\n if (hasFrontmatterFieldPrefix(f.content)) continue // section path (transformAstroSource), not a page wrap\n if (hasPageMarker(f.content)) continue // already carries a real marker\n if (!routeBaseName(f.path)) continue // dynamic / non-keyable route\n const routeBase = routeBaseName(f.path)!\n const key = resolvePageKey(f.content, f.path)\n const anchor = hasPageContentAnchor(f.content)\n\n if (key && anchor) {\n // Already covered by child sections: the page mounts sub-namespace\n // sections (e.g. colaboradores.astro renders <ColabHero\n // fieldPrefix=\"colaboradores.hero\" />, .benefits, .dark). The plugin does\n // NOT auto-wrap such a page — it has fieldPrefix props — and it doesn't\n // need to: the sub-sections are individually editable. Flagging a missing\n // `sec:colaboradores` marker is a false positive.\n const coveredBySubsections = Array.from(instrumentedKeys).some(k =>\n k.startsWith(`${key}.`),\n )\n if (coveredBySubsections) continue\n\n if (autoWrapPagesEnabled === true) {\n push({\n file: f.path,\n sectionPrefix: key,\n status: 'editable_i18n',\n reason: `Route page rendering t.${key} on a content element matching its path — auto-instrumented as sec:${key} (autoWrapPages is on).`,\n remedy: '',\n })\n } else if (autoWrapPagesEnabled === 'unknown') {\n push({\n file: f.path,\n sectionPrefix: key,\n status: 'missing_marker',\n reason: `Route page renders t.${key} on a content element matching its path, but astro.config sets autoWrap/autoWrapPages via a variable — the doctor can't verify pages auto-instrument.`,\n remedy: `Use literal flags — saastroStudio({ autoWrap: true, autoWrapPages: true }) — so the doctor can verify it, or add the marker by hand.`,\n snippet: `data-saastro=\"sec:${key}\"`,\n })\n } else {\n push({\n file: f.path,\n sectionPrefix: key,\n status: 'missing_marker',\n reason: `Route page renders t.${key} on a content element matching its path, but autoWrapPages is OFF so no marker ships.`,\n remedy: `Enable it once — saastroStudio({ autoWrap: true, autoWrapPages: true }) in astro.config — or add the marker by hand.`,\n snippet: `data-saastro=\"sec:${key}\"`,\n })\n }\n continue\n }\n if (key && !anchor) {\n push({\n file: f.path,\n sectionPrefix: key,\n status: 'missing_marker',\n reason: `Route page renders t.${key} only through child components (no HTML element to anchor) — autoWrapPages can't mark it here.`,\n remedy: `Mark the component that renders the content (give it fieldPrefix or data-saastro=\"sec:${key}\"), or wrap the page body in a plain element.`,\n snippet: `data-saastro=\"sec:${key}\"`,\n })\n continue\n }\n // No namespace matches the route. If it renders some resolvable i18n on an\n // element, the route name just doesn't match → suggest a hand marker.\n if (anchor) {\n const refs = f.tKeys.filter(k => i18nResolves(i18nKeys, k) && !instrumentedKeys.has(k))\n if (refs.length > 0) {\n const k2 = refs[0]!\n push({\n file: f.path,\n sectionPrefix: k2,\n status: 'missing_marker',\n reason: `Route page renders i18n content (t.${k2}) that doesn't match its path \"${routeBase}\", and has no marker.`,\n remedy: `Add a marker so the Studio enumerates it (fields then come from t.${k2}).`,\n snippet: `data-saastro=\"sec:${k2}\"`,\n })\n }\n }\n }\n\n // 3) autowrap_gap — additive warnings for derived-array props that won't\n // auto-instrument (#160), on .astro files only.\n for (const f of facts) {\n if (!f.path.endsWith('.astro')) continue\n const gaps = findAutoWrapGaps(f.content)\n if (gaps.length === 0) continue\n const key = f.markerKeys[0] ?? f.ownerKeys[0] ?? fileBaseCamel(f.path)\n for (const g of gaps) {\n push({\n file: f.path,\n sectionPrefix: key,\n status: 'autowrap_gap',\n reason: `Array prop \"${g.field}\" is iterated via a derived variable — autoWrap can't instrument its items.`,\n remedy: 'Add a hand-written data-saastro-field marker on the iterated element.',\n snippet: g.snippet,\n })\n }\n }\n\n const byStatus: Record<DoctorStatus, number> = {\n editable_props: 0,\n editable_i18n: 0,\n no_schema_no_i18n: 0,\n missing_marker: 0,\n autowrap_gap: 0,\n }\n for (const s of sections) byStatus[s.status]++\n const editable = byStatus.editable_props + byStatus.editable_i18n\n const needsAttention =\n byStatus.no_schema_no_i18n + byStatus.missing_marker + byStatus.autowrap_gap\n\n return {\n sections,\n stats: {\n files: files.length,\n candidates: instrumentedKeys.size,\n editable,\n needsAttention,\n byStatus,\n },\n }\n}\n","/**\n * astro.config flag detection for the Studio doctor. Pure + I/O-free.\n *\n * Decides whether a site's astro.config ACTIVELY enables page auto-wrapping, so\n * the doctor's verdict equals what the `@saastro/studio` plugin actually stamps.\n * Shared by the Hub's doctor route and the theme's `studio:check` CLI so both\n * read the config the same way — no drift.\n */\n\n/** Doctor's view of the astro.config flags: on / off / present-but-not-verifiable. */\nexport type AutoWrapConfigState = boolean | 'unknown'\n\n/** Strip JS comments + string literals so a flag mentioned in a comment / JSDoc\n * @example / string doesn't read as enabled. Best-effort (we test for presence,\n * not evaluate), so over-stripping is fine.\n *\n * Single-pass scanner — NOT sequential regex replaces. The old version stripped\n * strings before line-comments, so an apostrophe inside a `//` comment\n * (`// ...which doesn't exist`) opened a bogus single-quoted span that ate real\n * code — including the `autoWrap: true` flag — making the doctor report\n * autoWrapPages OFF on a config that literally enables it. Stripping comments\n * first instead would break `//` inside string literals (e.g. `'http://…'`).\n * Tracking context in one pass handles both. Regex literals are left as-is\n * (copied verbatim); they don't carry flag names so it's harmless. */\nexport function stripCommentsAndStrings(s: string): string {\n let out = ''\n let i = 0\n const n = s.length\n while (i < n) {\n const ch = s[i]\n const nx = s[i + 1]\n // line comment → blank to EOL\n if (ch === '/' && nx === '/') {\n i += 2\n while (i < n && s[i] !== '\\n') i++\n out += ' '\n continue\n }\n // block comment → blank through the close\n if (ch === '/' && nx === '*') {\n i += 2\n while (i < n && !(s[i] === '*' && s[i + 1] === '/')) i++\n i += 2\n out += ' '\n continue\n }\n // string literal (', \", `) → keep the delimiters, drop the body\n if (ch === '\"' || ch === \"'\" || ch === '`') {\n const q = ch\n out += q\n i++\n while (i < n && s[i] !== q) {\n if (s[i] === '\\\\') i++ // skip the escaped char\n i++\n }\n out += q // closing delimiter (or EOF)\n i++\n continue\n }\n out += ch\n i++\n }\n return out\n}\n\n/** One flag's literal state in stripped config source. */\nfunction flagState(c: string, name: string): 'true' | 'false' | 'absent' | 'unknown' {\n if (new RegExp(`\\\\b${name}\\\\s*:\\\\s*true\\\\b`).test(c)) return 'true'\n if (new RegExp(`\\\\b${name}\\\\s*:\\\\s*false\\\\b`).test(c)) return 'false'\n // Mentioned but not with a literal value — shorthand (`{ autoWrapPages }`)\n // or a variable (`autoWrapPages: flags.pages`). We can't evaluate it.\n if (new RegExp(`\\\\b${name}\\\\b`).test(c)) return 'unknown'\n return 'absent'\n}\n\n/**\n * Does the astro.config ACTIVELY enable page auto-wrapping? The plugin is inert\n * unless `autoWrap: true` AND only wraps pages when `autoWrapPages: true`\n * (vite/index.ts: `enabled = opts.autoWrap === true`, `load()` bails when not\n * enabled). We require BOTH, on comment/string-stripped source so a documented-\n * but-not-active flag doesn't flip it on. Best-effort source scan, NOT a JS\n * eval — but instead of silently reporting OFF when a flag is set via a\n * variable/shorthand, we return `'unknown'` so the doctor can say so. A flag\n * that's absent entirely (e.g. the whole options object is spread in) still\n * reads as OFF, matching the plugin's default. Pure + exported for unit tests.\n */\nexport function detectAutoWrapPagesInConfig(source: string): AutoWrapConfigState {\n const c = stripCommentsAndStrings(source)\n const wrap = flagState(c, 'autoWrap')\n const pages = flagState(c, 'autoWrapPages')\n if (wrap === 'false' || pages === 'false') return false\n if (wrap === 'absent' || pages === 'absent') return false\n if (wrap === 'true' && pages === 'true') return true\n return 'unknown'\n}\n","/**\n * Studio doctor — extra repo checks (H2). Pure + I/O-free, so the Hub's Setup\n * doctor and the theme's `studio:check` CLI run the SAME rules (no drift).\n *\n * Covers the gate items the section classifier (`analyzeSections`, doctor.ts)\n * doesn't:\n * - i18n parity — every translation key present across all locales\n * - token compliance — no hardcoded hex colors in .astro (oklch / var(--*))\n * - form compliance — no raw <form>; forms go through <HubForm>\n * - allowlist — editable copy lives in i18n, not hardcoded in a section\n * - spans integrity — rich-spans leaves are well-formed + locale-consistent\n *\n * (The playbook's \"section registered in saastrocms.config / studio.config\" gate\n * is intentionally NOT implemented: section keys are per-project examples, not a\n * closed canon — the doctor validates internal coherence, not list membership.)\n *\n * Each finding carries a severity: `error` fails the gate (counts toward\n * `needsAttention`), `warn` is advisory. `runDoctorChecks` aggregates them.\n */\n\nimport { stripCommentsAndStrings } from './doctor-config.js'\nimport { SPAN_MARKS, isSpansShape, isSpansValue, isValidMark } from './spans.js'\n\nexport type CheckStatus =\n | 'i18n_parity' // a key present in one locale, missing in another\n | 'hardcoded_hex' // hex color literal where a token belongs\n | 'raw_form' // <form> element instead of <HubForm>\n | 'allowlist_violation' // hardcoded editable copy in a marked section\n | 'spans_malformed' // spans-like array that violates the rich-spans shape\n | 'spans_shape_mismatch' // key is rich spans in one locale, plain string in another\n | 'theme_font_not_overridable' // @theme inlines a literal font family → Studio font swaps are no-ops\n\nexport type CheckSeverity = 'error' | 'warn'\n\nexport interface CheckFinding {\n file: string\n status: CheckStatus\n severity: CheckSeverity\n /** One-line human explanation. */\n reason: string\n /** What to do about it. */\n remedy: string\n /** The offending token (key, hex, snippet) for display. */\n detail?: string\n}\n\nexport interface ChecksReport {\n findings: CheckFinding[]\n stats: {\n /** error-severity findings — these fail the gate. */\n needsAttention: number\n /** advisory findings. */\n warnings: number\n byStatus: Record<CheckStatus, number>\n }\n}\n\n// ── 1) i18n parity ──────────────────────────────────────────────────────────\n\n/** A parsed translations file for one locale. */\nexport interface LocaleTranslations {\n /** Locale code, e.g. 'en'. */\n locale: string\n /** Translations file path (anchors findings). */\n file: string\n /** Parsed JSON/YAML object (arrays + scalars treated as leaves). */\n data: unknown\n}\n\n/** Flatten a nested translations object to dotted leaf-key paths. Arrays are\n * leaves (their shape can legitimately differ per locale). */\nfunction flattenKeys(obj: unknown, prefix = ''): string[] {\n if (obj == null || typeof obj !== 'object' || Array.isArray(obj)) {\n return prefix ? [prefix] : []\n }\n const out: string[] = []\n for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {\n const path = prefix ? `${prefix}.${k}` : k\n if (v != null && typeof v === 'object' && !Array.isArray(v)) {\n out.push(...flattenKeys(v, path))\n } else {\n out.push(path)\n }\n }\n return out\n}\n\n/** Report keys missing in any locale relative to the union across all locales. */\nexport function checkI18nParity(locales: ReadonlyArray<LocaleTranslations>): CheckFinding[] {\n if (locales.length < 2) return []\n const withKeys = locales.map(l => ({ ...l, keys: new Set(flattenKeys(l.data)) }))\n const union = new Set<string>()\n for (const l of withKeys) for (const k of l.keys) union.add(k)\n const findings: CheckFinding[] = []\n for (const l of withKeys) {\n for (const key of union) {\n if (l.keys.has(key)) continue\n findings.push({\n file: l.file,\n status: 'i18n_parity',\n severity: 'error',\n reason: `Translation key \"${key}\" is missing in locale \"${l.locale}\".`,\n remedy: `Add \"${key}\" to ${l.file} so every locale has parity.`,\n detail: key,\n })\n }\n }\n return findings\n}\n\n// ── 2) token compliance ─────────────────────────────────────────────────────\n\nconst HEX_RE = /#([0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{4}|[0-9a-fA-F]{3})\\b/g\n\n/** Collect hex color literals from a color-bearing fragment. */\nfunction collectHex(fragment: string, into: Set<string>): void {\n for (const m of fragment.matchAll(HEX_RE)) into.add('#' + m[1]!.toLowerCase())\n}\n\n/** Hex colors that leak into a component instead of a token. Scans the places a\n * brand color realistically lands: `style=` attrs, `<style>` blocks, and\n * Tailwind arbitrary values `[#fff]`. Anchor ids / hrefs (`#section`) never look\n * like hex, so they don't false-positive. Inline `<svg>` is skipped: SVG paint\n * (icons, OG-image generators) legitimately uses literal colors — oklch tokens\n * don't apply to a standalone graphic rendered to an image. */\nexport function checkTokenCompliance(\n files: ReadonlyArray<{ path: string; content: string }>,\n): CheckFinding[] {\n const findings: CheckFinding[] = []\n for (const f of files) {\n if (!f.path.endsWith('.astro')) continue\n // Drop inline SVG graphics — their fills/gradients aren't themeable UI.\n const content = f.content.replace(/<svg[\\s\\S]*?<\\/svg>/gi, ' ')\n const hits = new Set<string>()\n // Tailwind arbitrary values: bg-[#fff], text-[#abc123], [#fff]\n for (const m of content.matchAll(/\\[#([0-9a-fA-F]{3,8})\\]/g)) hits.add('#' + m[1]!.toLowerCase())\n // String style attributes: style=\"color:#fff\"\n for (const m of content.matchAll(/style\\s*=\\s*[\"']([^\"']*)[\"']/g)) collectHex(m[1]!, hits)\n // JSX object style: style={{ color: '#fff' }}\n for (const m of content.matchAll(/style\\s*=\\s*\\{\\{([^}]*)\\}\\}/g)) collectHex(m[1]!, hits)\n // <style> blocks\n for (const m of content.matchAll(/<style[^>]*>([\\s\\S]*?)<\\/style>/g)) collectHex(m[1]!, hits)\n for (const hex of hits) {\n findings.push({\n file: f.path,\n status: 'hardcoded_hex',\n severity: 'error',\n reason: `Hardcoded hex color \"${hex}\" — use a design token (oklch var) instead.`,\n remedy: `Replace ${hex} with a token: var(--primary)/--accent or add --brand in src/styles/global.css.`,\n detail: hex,\n })\n }\n }\n return findings\n}\n\n// ── 3) form compliance ──────────────────────────────────────────────────────\n\nconst RAW_FORM_RE = /<form[\\s/>]/\n\n/** A raw lowercase <form> element (HubForm renders <HubForm …>, never a bare\n * <form>). Strips comments + strings first so a `<form>` mentioned in a comment\n * or string literal doesn't trip it. */\nexport function checkFormCompliance(\n files: ReadonlyArray<{ path: string; content: string }>,\n): CheckFinding[] {\n const findings: CheckFinding[] = []\n for (const f of files) {\n if (!/\\.(astro|tsx|jsx)$/.test(f.path)) continue\n if (!RAW_FORM_RE.test(stripCommentsAndStrings(f.content))) continue\n findings.push({\n file: f.path,\n status: 'raw_form',\n severity: 'error',\n reason: 'Raw <form> element — forms must go through <HubForm> (@saastro/forms).',\n remedy: 'Replace the <form> with <HubForm> so submits hit submit.saastro.io with validation/captcha.',\n detail: '<form>',\n })\n }\n return findings\n}\n\n// ── 4) allowlist compliance ─────────────────────────────────────────────────\n\n/** Publishable paths the Hub writes back. Editable copy must live here, never\n * hardcoded in a component (which the Hub freezes at connect time). */\nconst ALLOWLIST_PREFIXES = ['src/pages/', 'src/content/', 'src/i18n/']\nconst ALLOWLIST_FILES = ['studio.config.json']\n\nfunction isAllowlisted(path: string): boolean {\n if (ALLOWLIST_FILES.some(f => path.endsWith(f))) return true\n if (path.endsWith('.md') || path.endsWith('.mdx')) return true\n return ALLOWLIST_PREFIXES.some(p => path.includes(p))\n}\n\n/** Hardcoded sentence-like text content (≥2 words) directly rendered as the\n * child of an element — i.e. NOT an `{interpolation}`. Best-effort. */\nconst LITERAL_TEXT_RE = />\\s*([A-Z][A-Za-z0-9'’,.!?-]*(?:\\s+[A-Za-z0-9'’,.!?-]+)+)\\s*</g\n\n/** Editable copy hardcoded inside a marked (`data-saastro`) section that lives\n * OUTSIDE the publish allowlist → the client can't edit it and it's lost on the\n * next Hub publish. Advisory (best-effort heuristic): real i18n-driven sections\n * render `{t.…}`/`{prop}`, not literal sentences. */\nexport function checkAllowlistCompliance(\n files: ReadonlyArray<{ path: string; content: string }>,\n): CheckFinding[] {\n const findings: CheckFinding[] = []\n for (const f of files) {\n if (!f.path.endsWith('.astro')) continue\n if (isAllowlisted(f.path)) continue // pages/content/i18n are publishable\n if (!/data-saastro\\s*=/.test(f.content)) continue // only marked (editable) sections\n const literals = new Set<string>()\n for (const m of f.content.matchAll(LITERAL_TEXT_RE)) {\n const text = m[1]!.trim()\n if (text.length >= 6) literals.add(text)\n }\n for (const text of literals) {\n findings.push({\n file: f.path,\n status: 'allowlist_violation',\n severity: 'warn',\n reason: `Hardcoded copy \"${truncate(text)}\" in a marked section — not editable via the Hub (outside the i18n allowlist).`,\n remedy: 'Move the text into an i18n namespace and render it via {t.<key>.<field>}.',\n detail: text,\n })\n }\n }\n return findings\n}\n\nfunction truncate(s: string): string {\n return s.length > 48 ? s.slice(0, 45) + '…' : s\n}\n\n// ── 5) rich-spans integrity ─────────────────────────────────────────────────\n\ntype SpansLeafKind = 'spans' | 'spans-unmarked' | 'spans-malformed' | 'string' | 'other'\n\n/** Looks like an ATTEMPT at rich spans: array of objects where some element\n * carries a `text` key. Generic i18n arrays (strings, `{label, href}` items)\n * don't match, so they stay out of this check entirely. */\nfunction isSpansLike(v: unknown): v is unknown[] {\n return (\n Array.isArray(v) &&\n v.length > 0 &&\n v.some(\n item =>\n item !== null &&\n typeof item === 'object' &&\n !Array.isArray(item) &&\n 'text' in (item as Record<string, unknown>),\n )\n )\n}\n\n/** Human-readable reason a spans-like array fails `isSpansValue`. */\nfunction spansDefect(v: unknown[]): string {\n for (let i = 0; i < v.length; i++) {\n const item = v[i]\n if (item === null || typeof item !== 'object' || Array.isArray(item))\n return `element ${i} is not a span object`\n const span = item as Record<string, unknown>\n if (typeof span.text !== 'string') return `element ${i} has a non-string \"text\"`\n for (const k of Object.keys(span)) {\n if (k !== 'text' && k !== 'marks') return `element ${i} has unknown key \"${k}\"`\n }\n if (span.marks !== undefined) {\n if (!Array.isArray(span.marks)) return `element ${i} has a non-array \"marks\"`\n for (const m of span.marks) {\n if (!isValidMark(m))\n return `element ${i} uses unknown mark \"${String(m)}\" (allowed: ${SPAN_MARKS.join(', ')}, c:<token>, s:sm|lg|xl, f:display|body|code)`\n }\n }\n }\n return 'unknown defect'\n}\n\n/** Flatten one locale's tree to leaf paths classified for the spans check. */\nfunction classifyLeaves(obj: unknown, prefix: string, out: Map<string, SpansLeafKind>): void {\n if (obj === null || typeof obj !== 'object') {\n if (prefix) out.set(prefix, typeof obj === 'string' ? 'string' : 'other')\n return\n }\n if (Array.isArray(obj)) {\n if (isSpansValue(obj)) {\n out.set(prefix, 'spans')\n } else if (isSpansShape(obj)) {\n // Grammar-valid but no marks anywhere: under the marked-only grammar\n // (audit E2 #28) this is legitimate generic content (`[{text}, …]`\n // testimonial/quote arrays), not an attempt at rich spans. No finding —\n // and it must not count as 'spans' for the cross-locale mismatch either.\n out.set(prefix, 'spans-unmarked')\n } else if (isSpansLike(obj)) {\n out.set(prefix, 'spans-malformed')\n } else {\n // Generic array: recurse — spans can sit inside collection items.\n obj.forEach((item, i) => classifyLeaves(item, prefix ? `${prefix}.${i}` : String(i), out))\n }\n return\n }\n for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {\n classifyLeaves(v, prefix ? `${prefix}.${k}` : k, out)\n }\n}\n\n/**\n * Rich-spans integrity:\n * - `spans_malformed` (error): a spans-like array that violates the shape\n * (unknown mark, non-string text, stray keys). Published as-is it renders\n * `[object Object]` on themes without the spans-aware renderer.\n * - `spans_shape_mismatch` (warn): a key that is rich spans in one locale\n * but a plain string in another — the dual renderer degrades fine, but\n * the spans migration is meant to be locale-atomic (deepMerge leaks the\n * default locale's array into locales missing the key).\n */\nexport function checkSpansIntegrity(\n locales: ReadonlyArray<LocaleTranslations>,\n): CheckFinding[] {\n const findings: CheckFinding[] = []\n const perLocale = locales.map(l => {\n const map = new Map<string, SpansLeafKind>()\n classifyLeaves(l.data, '', map)\n return { ...l, map }\n })\n\n for (const l of perLocale) {\n for (const [path, kind] of l.map) {\n if (kind !== 'spans-malformed') continue\n const raw = getAtDottedPath(l.data, path)\n findings.push({\n file: l.file,\n status: 'spans_malformed',\n severity: 'error',\n reason: `Key \"${path}\" (${l.locale}) looks like rich spans but is malformed: ${\n Array.isArray(raw) ? spansDefect(raw) : 'not an array'\n }.`,\n remedy: `Fix the value to [{ text, marks?: [${SPAN_MARKS.map(m => `'${m}'`).join(' | ')} | 'c:<token>' | 's:sm|lg|xl' | 'f:display|body|code'] }] or revert it to a plain string.`,\n detail: path,\n })\n }\n }\n\n const spansAnywhere = new Set<string>()\n for (const l of perLocale) {\n for (const [path, kind] of l.map) if (kind === 'spans') spansAnywhere.add(path)\n }\n for (const path of spansAnywhere) {\n for (const l of perLocale) {\n if (l.map.get(path) !== 'string') continue\n findings.push({\n file: l.file,\n status: 'spans_shape_mismatch',\n severity: 'warn',\n reason: `Key \"${path}\" is rich spans in another locale but a plain string in \"${l.locale}\" — spans migrations should cover all locales atomically.`,\n remedy: `Convert \"${path}\" in ${l.file} to the same rich-spans shape (or revert every locale to a string).`,\n detail: path,\n })\n }\n }\n return findings\n}\n\nfunction getAtDottedPath(obj: unknown, dotted: string): unknown {\n let cursor: unknown = obj\n for (const seg of dotted.split('.')) {\n if (cursor === null || typeof cursor !== 'object') return undefined\n cursor = Array.isArray(cursor)\n ? cursor[Number(seg)]\n : (cursor as Record<string, unknown>)[seg]\n }\n return cursor\n}\n\n// ── 6) theme font overridability ─────────────────────────────────────────────\n\n/** Pull the body of the first `@theme` / `@theme inline` block. */\nfunction extractThemeBlock(css: string): string | null {\n // Declarations only (no nested braces) → match to the first closing brace.\n const m = css.match(/@theme[^{]*\\{([\\s\\S]*?)\\}/)\n return m ? m[1]! : null\n}\n\n/**\n * Tailwind 4 gotcha: a font family written as a LITERAL inside `@theme\n * inline` (`--font-sans: 'Geist', sans-serif`) gets baked into the\n * `.font-sans` utility at build time, so a runtime `:root` override of\n * that var is a no-op — the Studio theme editor can't swap the font.\n * The fix is indirection: keep the family in a `:root` var\n * (`--font-body`/`--font-display`) and reference it from `@theme`\n * (`--font-sans: var(--font-body)`).\n *\n * Advisory (`warn`): colors already use this indirection in saastro-theme,\n * so color edits work regardless; only font swaps are affected.\n */\nexport function checkThemeFontOverridable(\n files: ReadonlyArray<{ path: string; content: string }>,\n): CheckFinding[] {\n const findings: CheckFinding[] = []\n for (const f of files) {\n if (!f.path.endsWith('.css')) continue\n const block = extractThemeBlock(f.content)\n if (block == null) continue\n // The two vars Tailwind utilities resolve for body/heading text.\n for (const m of block.matchAll(/--font-(sans|heading)\\s*:\\s*([^;]+)/g)) {\n const which = m[1]!\n const value = m[2]!.trim()\n if (/^var\\(\\s*--font-(body|display)\\b/.test(value)) continue // already indirected\n if (/^var\\(/.test(value)) continue // references some other var — assume intentional\n findings.push({\n file: f.path,\n status: 'theme_font_not_overridable',\n severity: 'warn',\n reason: `--font-${which} is a literal family inside @theme — Studio font swaps won't apply (Tailwind inlines it at build).`,\n remedy:\n 'Move the family to a :root var and reference it: ' +\n ':root{--font-body:…;--font-display:…} + @theme{--font-sans:var(--font-body);--font-heading:var(--font-display)}.',\n detail: `--font-${which}: ${truncate(value)}`,\n })\n break // one finding per file is enough signal\n }\n }\n return findings\n}\n\n// ── aggregator ───────────────────────────────────────────────────────────────\n\nexport interface DoctorChecksInput {\n files: ReadonlyArray<{ path: string; content: string }>\n locales: ReadonlyArray<LocaleTranslations>\n}\n\n/** Run all five checks and tally them (error count = needsAttention). */\nexport function runDoctorChecks(input: DoctorChecksInput): ChecksReport {\n const findings: CheckFinding[] = [\n ...checkI18nParity(input.locales),\n ...checkTokenCompliance(input.files),\n ...checkFormCompliance(input.files),\n ...checkAllowlistCompliance(input.files),\n ...checkSpansIntegrity(input.locales),\n ...checkThemeFontOverridable(input.files),\n ]\n const byStatus: Record<CheckStatus, number> = {\n i18n_parity: 0,\n hardcoded_hex: 0,\n raw_form: 0,\n allowlist_violation: 0,\n spans_malformed: 0,\n spans_shape_mismatch: 0,\n theme_font_not_overridable: 0,\n }\n let needsAttention = 0\n let warnings = 0\n for (const f of findings) {\n byStatus[f.status]++\n if (f.severity === 'error') needsAttention++\n else warnings++\n }\n return { findings, stats: { needsAttention, warnings, byStatus } }\n}\n"],"mappings":"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","names":[]}
|
package/dist/image/index.js
CHANGED
package/dist/image/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/image/cf-image.ts"],"sourcesContent":["import type {\n SrcKind,\n CfTransformOpts,\n BuildImageInput,\n BuildImageResult,\n CfImageUrlOpts,\n} from './types.js'\n\n/** Canonical Saastro media (R2 custom domain). Override per-tenant via `mediaHost`. */\nexport const DEFAULT_MEDIA_HOST = 'media.saastro.io'\nexport const DEFAULT_WIDTHS: readonly number[] = [480, 768, 1024, 1536, 2048]\nexport const DEFAULT_QUALITY = 80\n\n/** Vector/animated/already-optimal formats CF can't meaningfully resize/recode → serve raw. */\nconst NO_TRANSFORM_EXT = /\\.(svg|gif|ico|avif)$/i\n\n/**\n * Classify a content image string by where its bytes live:\n * - 'repo' → a `public/` asset referenced root-relative (`/images/...`)\n * - 'r2' → a Saastro media-library upload on the media host\n * - 'remote' → any other absolute URL (third-party) → never transformed\n */\nexport function classifySrc(src: string, mediaHost: string = DEFAULT_MEDIA_HOST): SrcKind {\n if (src.startsWith('/')) return 'repo'\n try {\n const u = new URL(src)\n return u.host === mediaHost ? 'r2' : 'remote'\n } catch {\n // relative path without a leading slash → treat as a repo asset\n return 'repo'\n }\n}\n\nexport function isTransformable(src: string): boolean {\n const path = src.split(/[?#]/)[0] ?? src\n return !NO_TRANSFORM_EXT.test(path)\n}\n\nfunction cfOptions(o: CfTransformOpts): string {\n const parts = [\n `width=${o.width}`,\n `format=${o.format ?? 'auto'}`,\n `quality=${o.quality ?? DEFAULT_QUALITY}`,\n ]\n if (o.fit) parts.push(`fit=${o.fit}`)\n return parts.join(',')\n}\n\n/** Repo asset → root-relative; CF intercepts `/cdn-cgi/image/*` at the zone edge. */\nfunction cfRepoUrl(src: string, o: CfTransformOpts): string {\n return `/cdn-cgi/image/${cfOptions(o)}${src}`\n}\n\n/** R2 media → transform on the media host where the object + R2 binding live. */\nfunction cfR2Url(src: string, o: CfTransformOpts): string {\n const u = new URL(src)\n return `https://${u.host}/cdn-cgi/image/${cfOptions(o)}${u.pathname}`\n}\n\n/**\n * Build the element-level `src` plus a transformed `srcset` for a content image.\n * When `optimize` is false (dev / no CF edge) or the source isn't transformable\n * (svg/gif/remote), returns the raw src with no srcset so the caller renders a\n * plain `<img>` — identical to today's behavior, nothing breaks.\n */\nexport function buildImage(input: BuildImageInput): BuildImageResult {\n const mediaHost = input.mediaHost ?? DEFAULT_MEDIA_HOST\n const kind = classifySrc(input.src, mediaHost)\n const canTransform = input.optimize && kind !== 'remote' && isTransformable(input.src)\n\n if (!canTransform) {\n return { rawSrc: input.src, srcset: null, optimizedSrc: null, kind }\n }\n\n const quality = input.quality ?? DEFAULT_QUALITY\n const widths = input.widths.length > 0 ? input.widths : [...DEFAULT_WIDTHS]\n const toUrl = (w: number): string => {\n const o: CfTransformOpts = { width: w, quality, fit: input.fit }\n return kind === 'r2' ? cfR2Url(input.src, o) : cfRepoUrl(input.src, o)\n }\n\n const srcset = widths.map((w) => `${toUrl(w)} ${w}w`).join(', ')\n const largest = widths[widths.length - 1]!\n const optimizedSrc = toUrl(largest)\n return { rawSrc: input.src, srcset, optimizedSrc, kind }\n}\n\n/**\n * Single optimized URL for ONE image — for CSS backgrounds\n * (`background-image: url(...)`), inline styles, `<meta>` og:image, etc. —\n * anywhere a `srcset` doesn't apply. Returns the raw src unchanged for\n * non-transformable sources (svg/gif/remote) or when `optimize` is false, so\n * it's always safe to drop in.\n *\n * Dev-gating note: this is a PURE helper — it can't read `import.meta.env.DEV`\n * (it runs from the compiled runtime, not the consumer's Vite context). In an\n * `.astro` file, gate it yourself so local dev serves the raw bytes:\n * const bg = cfImageUrl(src, { width: 1920, optimize: !import.meta.env.DEV })\n */\nexport function cfImageUrl(src: string, opts: CfImageUrlOpts): string {\n if (opts.optimize === false) return src\n const mediaHost = opts.mediaHost ?? DEFAULT_MEDIA_HOST\n const kind = classifySrc(src, mediaHost)\n if (kind === 'remote' || !isTransformable(src)) return src\n const o: CfTransformOpts = { width: opts.width, quality: opts.quality, fit: opts.fit }\n return kind === 'r2' ? cfR2Url(src, o) : cfRepoUrl(src, o)\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../src/image/cf-image.ts"],"sourcesContent":["import type {\n SrcKind,\n CfTransformOpts,\n BuildImageInput,\n BuildImageResult,\n CfImageUrlOpts,\n} from './types.js'\n\n/** Canonical Saastro media (R2 custom domain). Override per-tenant via `mediaHost`. */\nexport const DEFAULT_MEDIA_HOST = 'media.saastro.io'\nexport const DEFAULT_WIDTHS: readonly number[] = [480, 768, 1024, 1536, 2048]\nexport const DEFAULT_QUALITY = 80\n\n/** Vector/animated/already-optimal formats CF can't meaningfully resize/recode → serve raw. */\nconst NO_TRANSFORM_EXT = /\\.(svg|gif|ico|avif)$/i\n\n/**\n * Classify a content image string by where its bytes live:\n * - 'repo' → a `public/` asset referenced root-relative (`/images/...`)\n * - 'r2' → a Saastro media-library upload on the media host\n * - 'remote' → any other absolute URL (third-party) → never transformed\n */\nexport function classifySrc(src: string, mediaHost: string = DEFAULT_MEDIA_HOST): SrcKind {\n if (src.startsWith('/')) return 'repo'\n try {\n const u = new URL(src)\n return u.host === mediaHost ? 'r2' : 'remote'\n } catch {\n // relative path without a leading slash → treat as a repo asset\n return 'repo'\n }\n}\n\nexport function isTransformable(src: string): boolean {\n const path = src.split(/[?#]/)[0] ?? src\n return !NO_TRANSFORM_EXT.test(path)\n}\n\nfunction cfOptions(o: CfTransformOpts): string {\n const parts = [\n `width=${o.width}`,\n `format=${o.format ?? 'auto'}`,\n `quality=${o.quality ?? DEFAULT_QUALITY}`,\n ]\n if (o.fit) parts.push(`fit=${o.fit}`)\n return parts.join(',')\n}\n\n/** Repo asset → root-relative; CF intercepts `/cdn-cgi/image/*` at the zone edge. */\nfunction cfRepoUrl(src: string, o: CfTransformOpts): string {\n return `/cdn-cgi/image/${cfOptions(o)}${src}`\n}\n\n/** R2 media → transform on the media host where the object + R2 binding live. */\nfunction cfR2Url(src: string, o: CfTransformOpts): string {\n const u = new URL(src)\n return `https://${u.host}/cdn-cgi/image/${cfOptions(o)}${u.pathname}`\n}\n\n/**\n * Build the element-level `src` plus a transformed `srcset` for a content image.\n * When `optimize` is false (dev / no CF edge) or the source isn't transformable\n * (svg/gif/remote), returns the raw src with no srcset so the caller renders a\n * plain `<img>` — identical to today's behavior, nothing breaks.\n */\nexport function buildImage(input: BuildImageInput): BuildImageResult {\n const mediaHost = input.mediaHost ?? DEFAULT_MEDIA_HOST\n const kind = classifySrc(input.src, mediaHost)\n const canTransform = input.optimize && kind !== 'remote' && isTransformable(input.src)\n\n if (!canTransform) {\n return { rawSrc: input.src, srcset: null, optimizedSrc: null, kind }\n }\n\n const quality = input.quality ?? DEFAULT_QUALITY\n const widths = input.widths.length > 0 ? input.widths : [...DEFAULT_WIDTHS]\n const toUrl = (w: number): string => {\n const o: CfTransformOpts = { width: w, quality, fit: input.fit }\n return kind === 'r2' ? cfR2Url(input.src, o) : cfRepoUrl(input.src, o)\n }\n\n const srcset = widths.map((w) => `${toUrl(w)} ${w}w`).join(', ')\n const largest = widths[widths.length - 1]!\n const optimizedSrc = toUrl(largest)\n return { rawSrc: input.src, srcset, optimizedSrc, kind }\n}\n\n/**\n * Single optimized URL for ONE image — for CSS backgrounds\n * (`background-image: url(...)`), inline styles, `<meta>` og:image, etc. —\n * anywhere a `srcset` doesn't apply. Returns the raw src unchanged for\n * non-transformable sources (svg/gif/remote) or when `optimize` is false, so\n * it's always safe to drop in.\n *\n * Dev-gating note: this is a PURE helper — it can't read `import.meta.env.DEV`\n * (it runs from the compiled runtime, not the consumer's Vite context). In an\n * `.astro` file, gate it yourself so local dev serves the raw bytes:\n * const bg = cfImageUrl(src, { width: 1920, optimize: !import.meta.env.DEV })\n */\nexport function cfImageUrl(src: string, opts: CfImageUrlOpts): string {\n if (opts.optimize === false) return src\n const mediaHost = opts.mediaHost ?? DEFAULT_MEDIA_HOST\n const kind = classifySrc(src, mediaHost)\n if (kind === 'remote' || !isTransformable(src)) return src\n const o: CfTransformOpts = { width: opts.width, quality: opts.quality, fit: opts.fit }\n return kind === 'r2' ? cfR2Url(src, o) : cfRepoUrl(src, o)\n}\n"],"mappings":";;;;;;AASO,IAAM,qBAAqB;AAC3B,IAAM,iBAAoC,CAAC,KAAK,KAAK,MAAM,MAAM,IAAI;AACrE,IAAM,kBAAkB;AAG/B,IAAM,mBAAmB;AAQlB,SAAS,YAAY,KAAa,YAAoB,oBAA6B;AACxF,MAAI,IAAI,WAAW,GAAG,EAAG,QAAO;AAChC,MAAI;AACF,UAAM,IAAI,IAAI,IAAI,GAAG;AACrB,WAAO,EAAE,SAAS,YAAY,OAAO;AAAA,EACvC,QAAQ;AAEN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,gBAAgB,KAAsB;AACpD,QAAM,OAAO,IAAI,MAAM,MAAM,EAAE,CAAC,KAAK;AACrC,SAAO,CAAC,iBAAiB,KAAK,IAAI;AACpC;AAEA,SAAS,UAAU,GAA4B;AAC7C,QAAM,QAAQ;AAAA,IACZ,SAAS,EAAE,KAAK;AAAA,IAChB,UAAU,EAAE,UAAU,MAAM;AAAA,IAC5B,WAAW,EAAE,WAAW,eAAe;AAAA,EACzC;AACA,MAAI,EAAE,IAAK,OAAM,KAAK,OAAO,EAAE,GAAG,EAAE;AACpC,SAAO,MAAM,KAAK,GAAG;AACvB;AAGA,SAAS,UAAU,KAAa,GAA4B;AAC1D,SAAO,kBAAkB,UAAU,CAAC,CAAC,GAAG,GAAG;AAC7C;AAGA,SAAS,QAAQ,KAAa,GAA4B;AACxD,QAAM,IAAI,IAAI,IAAI,GAAG;AACrB,SAAO,WAAW,EAAE,IAAI,kBAAkB,UAAU,CAAC,CAAC,GAAG,EAAE,QAAQ;AACrE;AAQO,SAAS,WAAW,OAA0C;AACnE,QAAM,YAAY,MAAM,aAAa;AACrC,QAAM,OAAO,YAAY,MAAM,KAAK,SAAS;AAC7C,QAAM,eAAe,MAAM,YAAY,SAAS,YAAY,gBAAgB,MAAM,GAAG;AAErF,MAAI,CAAC,cAAc;AACjB,WAAO,EAAE,QAAQ,MAAM,KAAK,QAAQ,MAAM,cAAc,MAAM,KAAK;AAAA,EACrE;AAEA,QAAM,UAAU,MAAM,WAAW;AACjC,QAAM,SAAS,MAAM,OAAO,SAAS,IAAI,MAAM,SAAS,CAAC,GAAG,cAAc;AAC1E,QAAM,QAAQ,CAAC,MAAsB;AACnC,UAAM,IAAqB,EAAE,OAAO,GAAG,SAAS,KAAK,MAAM,IAAI;AAC/D,WAAO,SAAS,OAAO,QAAQ,MAAM,KAAK,CAAC,IAAI,UAAU,MAAM,KAAK,CAAC;AAAA,EACvE;AAEA,QAAM,SAAS,OAAO,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,IAAI;AAC/D,QAAM,UAAU,OAAO,OAAO,SAAS,CAAC;AACxC,QAAM,eAAe,MAAM,OAAO;AAClC,SAAO,EAAE,QAAQ,MAAM,KAAK,QAAQ,cAAc,KAAK;AACzD;AAcO,SAAS,WAAW,KAAa,MAA8B;AACpE,MAAI,KAAK,aAAa,MAAO,QAAO;AACpC,QAAM,YAAY,KAAK,aAAa;AACpC,QAAM,OAAO,YAAY,KAAK,SAAS;AACvC,MAAI,SAAS,YAAY,CAAC,gBAAgB,GAAG,EAAG,QAAO;AACvD,QAAM,IAAqB,EAAE,OAAO,KAAK,OAAO,SAAS,KAAK,SAAS,KAAK,KAAK,IAAI;AACrF,SAAO,SAAS,OAAO,QAAQ,KAAK,CAAC,IAAI,UAAU,KAAK,CAAC;AAC3D;","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
saastro
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-3MKWTNQA.js";
|
|
4
|
+
import "./chunk-G3MUVKWX.js";
|
|
5
|
+
import "./chunk-TU7VF6SY.js";
|
|
6
6
|
import {
|
|
7
7
|
editableImage
|
|
8
8
|
} from "./chunk-FLFH5RVH.js";
|
|
9
9
|
import "./chunk-PKDMKMD6.js";
|
|
10
|
+
import "./chunk-PZ5AY32C.js";
|
|
10
11
|
|
|
11
12
|
// src/helpers/editable-section.ts
|
|
12
13
|
function editableSection(fieldPrefix) {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/helpers/editable-section.ts","../src/helpers/editable-slot.ts","../src/helpers/editable-array-item.ts","../src/index.ts"],"sourcesContent":["import type { EditableAttrs } from './types.js'\n\n/**\n * Section marker — drop on the root element of a component that\n * receives `fieldPrefix`. Lets the Studio overlay hover/select/drag\n * the whole section.\n *\n * Returns empty object when fieldPrefix is falsy — safe to spread unconditionally.\n */\nexport function editableSection(fieldPrefix: string | undefined): EditableAttrs {\n if (!fieldPrefix) return {}\n return { 'data-saastro': `sec:${fieldPrefix}` }\n}\n","import type { EditableAttrs } from './types.js'\n\n/**\n * Slot marker — used for discrete named edits within a section\n * (e.g. a CTA label inside a hero). `fieldPrefix` scopes the slot\n * to its parent component; `slotName` is the local identifier.\n *\n * Returns empty object when either argument is falsy — safe to spread unconditionally.\n */\nexport function editableSlot(\n fieldPrefix: string | undefined,\n slotName: string,\n): EditableAttrs {\n if (!fieldPrefix || !slotName) return {}\n return { 'data-saastro': `slot:${fieldPrefix}.${slotName}` }\n}\n","import type { EditableAttrs } from './types.js'\n\n/**\n * Array-item marker — used on each rendered item inside a `.map(...)`\n * over repeatable content (pricing plans, testimonials, feature cards).\n * Studio uses this to select/duplicate/reorder individual items.\n *\n * Format: `arr:{arrPath}[{index}]` — brackets follow JS/TS convention\n * (lodash, JSON Pointer) and avoid ambiguity with numeric literal keys.\n *\n * Returns empty object when fieldPrefix is falsy or index is negative/non-finite.\n */\nexport function editableArrayItem(\n fieldPrefix: string | undefined,\n index: number,\n): EditableAttrs {\n if (!fieldPrefix || index < 0 || !Number.isFinite(index)) return {}\n return { 'data-saastro': `arr:${fieldPrefix}[${index}]` }\n}\n","/**\n * @saastro/studio — build-time instrumentation for Saastro Studio.\n *\n * This package provides:\n * - **Astro Integration** (default export): `saastro({ autoWrap: true })` —\n * registers the Vite plugin through Astro's own pipeline so ordering against\n * the `.astro` compiler is deterministic on cold builds.\n * - **Editable helpers**: `editableSection`, `editableImage`, `editableSlot`,\n * `editableArrayItem` + the `studioEditable` namespace.\n * - **Vite plugin** (separate entry, back-compat): `@saastro/studio/vite`.\n *\n * NOTE: This package is the *runtime instrumentation* installed in Astro\n * sites. The Studio editor UI lives at hub.saastro.io and is NOT part of this\n * package.\n *\n * @saastro/studio and @saastro/cms are peer packages — neither depends on the\n * other.\n */\n\nimport saastro from './integration/index.js'\n\nexport default saastro\nexport { saastro }\nexport type { SaastroStudioOptions } from './integration/index.js'\n\nexport type { EditableAttrs } from './helpers/types.js'\nexport { editableSection } from './helpers/editable-section.js'\nexport { editableImage } from './helpers/editable-image.js'\nexport { editableSlot } from './helpers/editable-slot.js'\nexport { editableArrayItem } from './helpers/editable-array-item.js'\n\nimport { editableSection } from './helpers/editable-section.js'\nimport { editableImage } from './helpers/editable-image.js'\nimport { editableSlot } from './helpers/editable-slot.js'\nimport { editableArrayItem } from './helpers/editable-array-item.js'\n\n/**\n * Convenience namespace — all four helpers grouped under one object.\n */\nexport const studioEditable = {\n section: editableSection,\n image: editableImage,\n slot: editableSlot,\n arrayItem: editableArrayItem,\n} as const\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../src/helpers/editable-section.ts","../src/helpers/editable-slot.ts","../src/helpers/editable-array-item.ts","../src/index.ts"],"sourcesContent":["import type { EditableAttrs } from './types.js'\n\n/**\n * Section marker — drop on the root element of a component that\n * receives `fieldPrefix`. Lets the Studio overlay hover/select/drag\n * the whole section.\n *\n * Returns empty object when fieldPrefix is falsy — safe to spread unconditionally.\n */\nexport function editableSection(fieldPrefix: string | undefined): EditableAttrs {\n if (!fieldPrefix) return {}\n return { 'data-saastro': `sec:${fieldPrefix}` }\n}\n","import type { EditableAttrs } from './types.js'\n\n/**\n * Slot marker — used for discrete named edits within a section\n * (e.g. a CTA label inside a hero). `fieldPrefix` scopes the slot\n * to its parent component; `slotName` is the local identifier.\n *\n * Returns empty object when either argument is falsy — safe to spread unconditionally.\n */\nexport function editableSlot(\n fieldPrefix: string | undefined,\n slotName: string,\n): EditableAttrs {\n if (!fieldPrefix || !slotName) return {}\n return { 'data-saastro': `slot:${fieldPrefix}.${slotName}` }\n}\n","import type { EditableAttrs } from './types.js'\n\n/**\n * Array-item marker — used on each rendered item inside a `.map(...)`\n * over repeatable content (pricing plans, testimonials, feature cards).\n * Studio uses this to select/duplicate/reorder individual items.\n *\n * Format: `arr:{arrPath}[{index}]` — brackets follow JS/TS convention\n * (lodash, JSON Pointer) and avoid ambiguity with numeric literal keys.\n *\n * Returns empty object when fieldPrefix is falsy or index is negative/non-finite.\n */\nexport function editableArrayItem(\n fieldPrefix: string | undefined,\n index: number,\n): EditableAttrs {\n if (!fieldPrefix || index < 0 || !Number.isFinite(index)) return {}\n return { 'data-saastro': `arr:${fieldPrefix}[${index}]` }\n}\n","/**\n * @saastro/studio — build-time instrumentation for Saastro Studio.\n *\n * This package provides:\n * - **Astro Integration** (default export): `saastro({ autoWrap: true })` —\n * registers the Vite plugin through Astro's own pipeline so ordering against\n * the `.astro` compiler is deterministic on cold builds.\n * - **Editable helpers**: `editableSection`, `editableImage`, `editableSlot`,\n * `editableArrayItem` + the `studioEditable` namespace.\n * - **Vite plugin** (separate entry, back-compat): `@saastro/studio/vite`.\n *\n * NOTE: This package is the *runtime instrumentation* installed in Astro\n * sites. The Studio editor UI lives at hub.saastro.io and is NOT part of this\n * package.\n *\n * @saastro/studio and @saastro/cms are peer packages — neither depends on the\n * other.\n */\n\nimport saastro from './integration/index.js'\n\nexport default saastro\nexport { saastro }\nexport type { SaastroStudioOptions } from './integration/index.js'\n\nexport type { EditableAttrs } from './helpers/types.js'\nexport { editableSection } from './helpers/editable-section.js'\nexport { editableImage } from './helpers/editable-image.js'\nexport { editableSlot } from './helpers/editable-slot.js'\nexport { editableArrayItem } from './helpers/editable-array-item.js'\n\nimport { editableSection } from './helpers/editable-section.js'\nimport { editableImage } from './helpers/editable-image.js'\nimport { editableSlot } from './helpers/editable-slot.js'\nimport { editableArrayItem } from './helpers/editable-array-item.js'\n\n/**\n * Convenience namespace — all four helpers grouped under one object.\n */\nexport const studioEditable = {\n section: editableSection,\n image: editableImage,\n slot: editableSlot,\n arrayItem: editableArrayItem,\n} as const\n"],"mappings":";;;;;;;;;;;;AASO,SAAS,gBAAgB,aAAgD;AAC9E,MAAI,CAAC,YAAa,QAAO,CAAC;AAC1B,SAAO,EAAE,gBAAgB,OAAO,WAAW,GAAG;AAChD;;;ACHO,SAAS,aACd,aACA,UACe;AACf,MAAI,CAAC,eAAe,CAAC,SAAU,QAAO,CAAC;AACvC,SAAO,EAAE,gBAAgB,QAAQ,WAAW,IAAI,QAAQ,GAAG;AAC7D;;;ACHO,SAAS,kBACd,aACA,OACe;AACf,MAAI,CAAC,eAAe,QAAQ,KAAK,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO,CAAC;AAClE,SAAO,EAAE,gBAAgB,OAAO,WAAW,IAAI,KAAK,IAAI;AAC1D;;;ACGA,IAAO,cAAQ;AAkBR,IAAM,iBAAiB;AAAA,EAC5B,SAAS;AAAA,EACT,OAAO;AAAA,EACP,MAAM;AAAA,EACN,WAAW;AACb;","names":[]}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
saastro
|
|
3
|
-
} from "../chunk-
|
|
4
|
-
import "../chunk-
|
|
5
|
-
import "../chunk-
|
|
3
|
+
} from "../chunk-3MKWTNQA.js";
|
|
4
|
+
import "../chunk-G3MUVKWX.js";
|
|
5
|
+
import "../chunk-TU7VF6SY.js";
|
|
6
6
|
import "../chunk-PKDMKMD6.js";
|
|
7
|
+
import "../chunk-PZ5AY32C.js";
|
|
7
8
|
export {
|
|
8
9
|
saastro as default
|
|
9
10
|
};
|
package/dist/middleware/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
+
injectThemeStyle,
|
|
2
3
|
injectVersionMetaTag,
|
|
3
4
|
injectVisibilityStyle
|
|
4
|
-
} from "../chunk-
|
|
5
|
+
} from "../chunk-TU7VF6SY.js";
|
|
6
|
+
import "../chunk-PZ5AY32C.js";
|
|
5
7
|
|
|
6
8
|
// src/middleware/index.ts
|
|
7
9
|
var HTML_RE = /text\/html/i;
|
|
@@ -12,6 +14,7 @@ var onRequest = async (_context, next) => {
|
|
|
12
14
|
const html = await response.text();
|
|
13
15
|
let patched = injectVersionMetaTag(html);
|
|
14
16
|
patched = injectVisibilityStyle(patched);
|
|
17
|
+
patched = injectThemeStyle(patched);
|
|
15
18
|
if (patched === html) {
|
|
16
19
|
return makeHtmlResponse(html, response);
|
|
17
20
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/middleware/index.ts"],"sourcesContent":["/**\n * Astro middleware that injects the Saastro Studio meta tags into every\n * HTML response. Companion to the Vite plugin's `transformIndexHtml`\n * hook, which only fires for static HTML served by Vite's dev server —\n * SSR routes (`output: 'server'` or `'hybrid'`) bypass it entirely.\n *\n * Registered by the integration via\n * `addMiddleware({ entrypoint: '@saastro/studio/middleware', order: 'pre' })`\n * so it runs before any user middleware.\n *\n * Behavior:\n * - Non-HTML responses (CSS, JS, JSON, redirects, images) pass through\n * untouched — we never buffer their bodies.\n * - HTML responses get their `<head>` patched with two meta tags. The\n * patch is idempotent: a response that already has\n * `saastro:studio-version` is returned as-is.\n * - Response status, statusText, and all headers (minus `content-length`\n * and `etag`, which are invalidated by the body rewrite) are\n * preserved.\n */\n\nimport { injectVersionMetaTag } from '../vite/meta-tag.js'\nimport { injectVisibilityStyle } from '../visibility-css.js'\n\ninterface MiddlewareContext {\n request: Request\n url?: URL\n}\n\ntype Next = () => Promise<Response>\n\nconst HTML_RE = /text\\/html/i\n\nexport const onRequest = async (\n _context: MiddlewareContext,\n next: Next,\n): Promise<Response> => {\n const response = await next()\n const contentType = response.headers.get('content-type') ?? ''\n if (!HTML_RE.test(contentType)) return response\n\n const html = await response.text()\n // Layered injection: meta tags first (for the Hub overlay), then the\n // `studio.config.json`-driven visibility CSS. Both helpers are\n // idempotent so a response that already carries either tag is left\n // untouched in that pass.\n let patched = injectVersionMetaTag(html)\n // `injectVisibilityStyle` defaults to `'.'` (resolved relative to\n // the current working directory) when no project root is passed —\n // the middleware can't easily determine Vite's resolved root, but\n // the CWD is the project root in every realistic deployment.\n patched = injectVisibilityStyle(patched)\n if (patched === html) {\n // Nothing changed, but `text()` already consumed the original\n // stream. Re-emit with a fresh body so downstream adapters get a\n // readable response.\n return makeHtmlResponse(html, response)\n }\n return makeHtmlResponse(patched, response)\n}\n\nfunction makeHtmlResponse(body: string, source: Response): Response {\n const headers = new Headers(source.headers)\n // The body length changed (or we re-emitted). The original\n // `content-length` / `etag` are invalid — drop them and let the\n // runtime/proxy recompute or ignore.\n headers.delete('content-length')\n headers.delete('etag')\n return new Response(body, {\n status: source.status,\n statusText: source.statusText,\n headers,\n })\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../src/middleware/index.ts"],"sourcesContent":["/**\n * Astro middleware that injects the Saastro Studio meta tags into every\n * HTML response. Companion to the Vite plugin's `transformIndexHtml`\n * hook, which only fires for static HTML served by Vite's dev server —\n * SSR routes (`output: 'server'` or `'hybrid'`) bypass it entirely.\n *\n * Registered by the integration via\n * `addMiddleware({ entrypoint: '@saastro/studio/middleware', order: 'pre' })`\n * so it runs before any user middleware.\n *\n * Behavior:\n * - Non-HTML responses (CSS, JS, JSON, redirects, images) pass through\n * untouched — we never buffer their bodies.\n * - HTML responses get their `<head>` patched with two meta tags. The\n * patch is idempotent: a response that already has\n * `saastro:studio-version` is returned as-is.\n * - Response status, statusText, and all headers (minus `content-length`\n * and `etag`, which are invalidated by the body rewrite) are\n * preserved.\n */\n\nimport { injectVersionMetaTag } from '../vite/meta-tag.js'\nimport { injectVisibilityStyle } from '../visibility-css.js'\nimport { injectThemeStyle } from '../theme-css.js'\n\ninterface MiddlewareContext {\n request: Request\n url?: URL\n}\n\ntype Next = () => Promise<Response>\n\nconst HTML_RE = /text\\/html/i\n\nexport const onRequest = async (\n _context: MiddlewareContext,\n next: Next,\n): Promise<Response> => {\n const response = await next()\n const contentType = response.headers.get('content-type') ?? ''\n if (!HTML_RE.test(contentType)) return response\n\n const html = await response.text()\n // Layered injection: meta tags first (for the Hub overlay), then the\n // `studio.config.json`-driven visibility CSS. Both helpers are\n // idempotent so a response that already carries either tag is left\n // untouched in that pass.\n let patched = injectVersionMetaTag(html)\n // `injectVisibilityStyle` defaults to `'.'` (resolved relative to\n // the current working directory) when no project root is passed —\n // the middleware can't easily determine Vite's resolved root, but\n // the CWD is the project root in every realistic deployment.\n patched = injectVisibilityStyle(patched)\n // Theme override LAST so its `:root` vars cascade over the bundled CSS.\n patched = injectThemeStyle(patched)\n if (patched === html) {\n // Nothing changed, but `text()` already consumed the original\n // stream. Re-emit with a fresh body so downstream adapters get a\n // readable response.\n return makeHtmlResponse(html, response)\n }\n return makeHtmlResponse(patched, response)\n}\n\nfunction makeHtmlResponse(body: string, source: Response): Response {\n const headers = new Headers(source.headers)\n // The body length changed (or we re-emitted). The original\n // `content-length` / `etag` are invalid — drop them and let the\n // runtime/proxy recompute or ignore.\n headers.delete('content-length')\n headers.delete('etag')\n return new Response(body, {\n status: source.status,\n statusText: source.statusText,\n headers,\n })\n}\n"],"mappings":";;;;;;;;AAgCA,IAAM,UAAU;AAET,IAAM,YAAY,OACvB,UACA,SACsB;AACtB,QAAM,WAAW,MAAM,KAAK;AAC5B,QAAM,cAAc,SAAS,QAAQ,IAAI,cAAc,KAAK;AAC5D,MAAI,CAAC,QAAQ,KAAK,WAAW,EAAG,QAAO;AAEvC,QAAM,OAAO,MAAM,SAAS,KAAK;AAKjC,MAAI,UAAU,qBAAqB,IAAI;AAKvC,YAAU,sBAAsB,OAAO;AAEvC,YAAU,iBAAiB,OAAO;AAClC,MAAI,YAAY,MAAM;AAIpB,WAAO,iBAAiB,MAAM,QAAQ;AAAA,EACxC;AACA,SAAO,iBAAiB,SAAS,QAAQ;AAC3C;AAEA,SAAS,iBAAiB,MAAc,QAA4B;AAClE,QAAM,UAAU,IAAI,QAAQ,OAAO,OAAO;AAI1C,UAAQ,OAAO,gBAAgB;AAC/B,UAAQ,OAAO,MAAM;AACrB,SAAO,IAAI,SAAS,MAAM;AAAA,IACxB,QAAQ,OAAO;AAAA,IACf,YAAY,OAAO;AAAA,IACnB;AAAA,EACF,CAAC;AACH;","names":[]}
|