@saastro/studio 0.6.0 → 0.7.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-3ZM2I4QZ.js → chunk-DKDGF6IQ.js} +12 -192
- package/dist/chunk-DKDGF6IQ.js.map +1 -0
- package/dist/{chunk-GENKOFWL.js → chunk-MN3J6PBW.js} +2 -2
- package/dist/chunk-PKDMKMD6.js +212 -0
- package/dist/chunk-PKDMKMD6.js.map +1 -0
- package/dist/{chunk-XIK7ELIY.js → chunk-ZHCSQE2N.js} +2 -2
- package/dist/doctor.d.ts +216 -0
- package/dist/doctor.js +495 -0
- package/dist/doctor.js.map +1 -0
- 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 +1 -1
- package/dist/vite/index.js +3 -2
- package/package.json +16 -13
- package/dist/chunk-3ZM2I4QZ.js.map +0 -1
- /package/dist/{chunk-GENKOFWL.js.map → chunk-MN3J6PBW.js.map} +0 -0
- /package/dist/{chunk-XIK7ELIY.js.map → chunk-ZHCSQE2N.js.map} +0 -0
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hasFrontmatterFieldPrefix,
|
|
3
|
+
hasPageContentAnchor,
|
|
4
|
+
hasPageMarker,
|
|
5
|
+
resolvePageKey,
|
|
6
|
+
routeBaseName
|
|
7
|
+
} from "./chunk-PKDMKMD6.js";
|
|
8
|
+
|
|
9
|
+
// ../studio-core/dist/chunk-CW5U4CZJ.js
|
|
10
|
+
function escapeReg(s) {
|
|
11
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
12
|
+
}
|
|
13
|
+
function hasMapReference(source, name) {
|
|
14
|
+
return new RegExp("\\b" + escapeReg(name) + "\\s*\\.\\s*map\\s*\\(").test(source);
|
|
15
|
+
}
|
|
16
|
+
function hasHandWrittenFieldMarker(template, name) {
|
|
17
|
+
return new RegExp(
|
|
18
|
+
"data-saastro(?:-field)?\\s*=\\s*\\{\\s*`[^`]*\\b" + escapeReg(name) + "[[.]"
|
|
19
|
+
).test(template);
|
|
20
|
+
}
|
|
21
|
+
function splitFrontmatter(content) {
|
|
22
|
+
const open = /^\uFEFF?[ \t]*---[ \t]*\r?\n/.exec(content);
|
|
23
|
+
if (!open) return { frontmatter: "", template: content };
|
|
24
|
+
const bodyStart = open[0].length;
|
|
25
|
+
const close = /\r?\n[ \t]*---[ \t]*(?:\r?\n|$)/g;
|
|
26
|
+
close.lastIndex = bodyStart;
|
|
27
|
+
const cm = close.exec(content);
|
|
28
|
+
if (!cm) return { frontmatter: "", template: content };
|
|
29
|
+
return {
|
|
30
|
+
frontmatter: content.slice(bodyStart, cm.index),
|
|
31
|
+
template: content.slice(cm.index + cm[0].length)
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function extractPropNames(frontmatter) {
|
|
35
|
+
const m = /const\s*\{([^}]*)\}\s*=\s*Astro\.props/.exec(frontmatter);
|
|
36
|
+
if (!m) return [];
|
|
37
|
+
return m[1].split(",").map((part) => {
|
|
38
|
+
const raw = part.trim();
|
|
39
|
+
if (!raw || raw.startsWith("...")) return "";
|
|
40
|
+
const noDefault = raw.split("=")[0].trim();
|
|
41
|
+
const renamed = noDefault.includes(":") ? noDefault.split(":")[1].trim() : noDefault;
|
|
42
|
+
return /^[A-Za-z_$][\w$]*$/.test(renamed) ? renamed : "";
|
|
43
|
+
}).filter(Boolean);
|
|
44
|
+
}
|
|
45
|
+
function findAutoWrapGaps(content) {
|
|
46
|
+
const { frontmatter, template } = splitFrontmatter(content);
|
|
47
|
+
const props = extractPropNames(frontmatter);
|
|
48
|
+
const gaps = [];
|
|
49
|
+
for (const name of props) {
|
|
50
|
+
if (hasMapReference(frontmatter, name) && !hasMapReference(template, name) && !hasHandWrittenFieldMarker(template, name)) {
|
|
51
|
+
gaps.push({
|
|
52
|
+
field: name,
|
|
53
|
+
reason: "derived_variable",
|
|
54
|
+
snippet: `<img src={item.image} data-saastro-field={\`${name}[\${i}].image\`} />`
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return gaps;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ../studio-core/dist/doctor.js
|
|
62
|
+
var MARKER_LITERAL_RE = /data-saastro\s*=\s*["']sec:([^"']+)["']/g;
|
|
63
|
+
var MARKER_DEFAULT_RE = /data-saastro\s*=\s*\{\s*`sec:\$\{[^}]*?\?\?\s*["']([^"']+)["']/g;
|
|
64
|
+
var FIELDPREFIX_ATTR_RE = /fieldPrefix\s*=\s*(?:["']([^"']+)["']|\{\s*[`"']([^`"'}]+)[`"']\s*\})/g;
|
|
65
|
+
var FIELDPREFIX_NULLISH_RE = /fieldPrefix[^\n]*?\?\?\s*["'`]([^"'`]+)["'`]/g;
|
|
66
|
+
var PROPS_DECL_RE = /\b(?:interface|type)\s+\w*Props\b/;
|
|
67
|
+
var T_USAGE_RE = /(?:Astro\.locals\.)?\bt\.([A-Za-z_$][\w$]*)/g;
|
|
68
|
+
function matchAll(re, s) {
|
|
69
|
+
const out = [];
|
|
70
|
+
re.lastIndex = 0;
|
|
71
|
+
let m;
|
|
72
|
+
while ((m = re.exec(s)) !== null) {
|
|
73
|
+
const v = m[1] ?? m[2];
|
|
74
|
+
if (v) out.push(v);
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
function fileBaseCamel(path) {
|
|
79
|
+
const base = path.split("/").pop() ?? path;
|
|
80
|
+
const name = base.replace(/\.(astro|tsx|jsx)$/, "");
|
|
81
|
+
return name.charAt(0).toLowerCase() + name.slice(1);
|
|
82
|
+
}
|
|
83
|
+
function isPage(path) {
|
|
84
|
+
return /(^|\/)src\/pages\//.test(path);
|
|
85
|
+
}
|
|
86
|
+
function isLayout(path) {
|
|
87
|
+
return /(^|\/)src\/layouts\//.test(path) || /layout/i.test(path.split("/").pop() ?? "");
|
|
88
|
+
}
|
|
89
|
+
function i18nResolves(keys, prefix) {
|
|
90
|
+
if (keys.has(prefix)) return true;
|
|
91
|
+
if (prefix.includes(".")) {
|
|
92
|
+
const head = prefix.split(".")[0];
|
|
93
|
+
if (head && keys.has(head)) return true;
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
function readFacts(path, content) {
|
|
98
|
+
return {
|
|
99
|
+
path,
|
|
100
|
+
markerKeys: [...matchAll(MARKER_LITERAL_RE, content), ...matchAll(MARKER_DEFAULT_RE, content)],
|
|
101
|
+
ownerKeys: [
|
|
102
|
+
...matchAll(FIELDPREFIX_ATTR_RE, content),
|
|
103
|
+
...matchAll(FIELDPREFIX_NULLISH_RE, content)
|
|
104
|
+
],
|
|
105
|
+
hasProps: PROPS_DECL_RE.test(content),
|
|
106
|
+
tKeys: [...new Set(matchAll(T_USAGE_RE, content))],
|
|
107
|
+
isPage: isPage(path),
|
|
108
|
+
isLayout: isLayout(path),
|
|
109
|
+
content
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function analyzeSections(files, i18nKeys, autoWrapPagesEnabled = false) {
|
|
113
|
+
const facts = files.map((f) => readFacts(f.path, f.content));
|
|
114
|
+
const propsEditableKeys = /* @__PURE__ */ new Set();
|
|
115
|
+
const instrumentedKeys = /* @__PURE__ */ new Set();
|
|
116
|
+
const keyToFile = /* @__PURE__ */ new Map();
|
|
117
|
+
const keyRank = /* @__PURE__ */ new Map();
|
|
118
|
+
const setKeyFile = (k, path, rank) => {
|
|
119
|
+
if ((keyRank.get(k) ?? 0) >= rank) return;
|
|
120
|
+
keyRank.set(k, rank);
|
|
121
|
+
keyToFile.set(k, path);
|
|
122
|
+
};
|
|
123
|
+
for (const f of facts) {
|
|
124
|
+
for (const k of f.markerKeys) {
|
|
125
|
+
instrumentedKeys.add(k);
|
|
126
|
+
setKeyFile(k, f.path, 2);
|
|
127
|
+
}
|
|
128
|
+
const ownerRank = f.hasProps ? 3 : hasFrontmatterFieldPrefix(f.content) ? 2 : 1;
|
|
129
|
+
for (const k of f.ownerKeys) {
|
|
130
|
+
instrumentedKeys.add(k);
|
|
131
|
+
setKeyFile(k, f.path, ownerRank);
|
|
132
|
+
if (f.hasProps) propsEditableKeys.add(k);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const sections = [];
|
|
136
|
+
const seen = /* @__PURE__ */ new Set();
|
|
137
|
+
const push = (s) => {
|
|
138
|
+
const id = `${s.status}:${s.sectionPrefix}:${s.file}`;
|
|
139
|
+
if (seen.has(id)) return;
|
|
140
|
+
seen.add(id);
|
|
141
|
+
sections.push(s);
|
|
142
|
+
};
|
|
143
|
+
for (const key of instrumentedKeys) {
|
|
144
|
+
const file = keyToFile.get(key) ?? "";
|
|
145
|
+
if (propsEditableKeys.has(key)) {
|
|
146
|
+
push({
|
|
147
|
+
file,
|
|
148
|
+
sectionPrefix: key,
|
|
149
|
+
status: "editable_props",
|
|
150
|
+
reason: "Has a fieldPrefix + interface Props \u2014 full schema editing.",
|
|
151
|
+
remedy: ""
|
|
152
|
+
});
|
|
153
|
+
} else if (i18nResolves(i18nKeys, key)) {
|
|
154
|
+
push({
|
|
155
|
+
file,
|
|
156
|
+
sectionPrefix: key,
|
|
157
|
+
status: "editable_i18n",
|
|
158
|
+
reason: "Marker + matching i18n namespace \u2014 editable via translation fields.",
|
|
159
|
+
remedy: ""
|
|
160
|
+
});
|
|
161
|
+
} else {
|
|
162
|
+
push({
|
|
163
|
+
file,
|
|
164
|
+
sectionPrefix: key,
|
|
165
|
+
status: "no_schema_no_i18n",
|
|
166
|
+
reason: "Marked section but no interface Props and no matching i18n namespace \u2014 nothing to edit.",
|
|
167
|
+
remedy: `Add an i18n namespace "${key}" to your translations, or give the component an interface Props (+ fieldPrefix="${key}").`
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
for (const f of facts) {
|
|
172
|
+
if (f.markerKeys.length > 0 || f.ownerKeys.length > 0 || f.isPage || f.isLayout) continue;
|
|
173
|
+
const i18nRefs = f.tKeys.filter((k) => i18nKeys.has(k) && !instrumentedKeys.has(k));
|
|
174
|
+
if (i18nRefs.length === 0) continue;
|
|
175
|
+
const base = fileBaseCamel(f.path);
|
|
176
|
+
const key = i18nRefs.includes(base) ? base : i18nRefs[0];
|
|
177
|
+
push({
|
|
178
|
+
file: f.path,
|
|
179
|
+
sectionPrefix: key,
|
|
180
|
+
status: "missing_marker",
|
|
181
|
+
reason: `Renders i18n content (t.${key}) but has no data-saastro marker \u2014 the editor can't see it.`,
|
|
182
|
+
remedy: `Add a marker to the section's root element so the Studio enumerates it (fields then come from t.${key}).`,
|
|
183
|
+
snippet: `data-saastro="sec:${key}"`
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
for (const f of facts) {
|
|
187
|
+
if (!f.isPage) continue;
|
|
188
|
+
if (hasFrontmatterFieldPrefix(f.content)) continue;
|
|
189
|
+
if (hasPageMarker(f.content)) continue;
|
|
190
|
+
if (!routeBaseName(f.path)) continue;
|
|
191
|
+
const routeBase = routeBaseName(f.path);
|
|
192
|
+
const key = resolvePageKey(f.content, f.path);
|
|
193
|
+
const anchor = hasPageContentAnchor(f.content);
|
|
194
|
+
if (key && anchor) {
|
|
195
|
+
const coveredBySubsections = Array.from(instrumentedKeys).some(
|
|
196
|
+
(k) => k.startsWith(`${key}.`)
|
|
197
|
+
);
|
|
198
|
+
if (coveredBySubsections) continue;
|
|
199
|
+
if (autoWrapPagesEnabled === true) {
|
|
200
|
+
push({
|
|
201
|
+
file: f.path,
|
|
202
|
+
sectionPrefix: key,
|
|
203
|
+
status: "editable_i18n",
|
|
204
|
+
reason: `Route page rendering t.${key} on a content element matching its path \u2014 auto-instrumented as sec:${key} (autoWrapPages is on).`,
|
|
205
|
+
remedy: ""
|
|
206
|
+
});
|
|
207
|
+
} else if (autoWrapPagesEnabled === "unknown") {
|
|
208
|
+
push({
|
|
209
|
+
file: f.path,
|
|
210
|
+
sectionPrefix: key,
|
|
211
|
+
status: "missing_marker",
|
|
212
|
+
reason: `Route page renders t.${key} on a content element matching its path, but astro.config sets autoWrap/autoWrapPages via a variable \u2014 the doctor can't verify pages auto-instrument.`,
|
|
213
|
+
remedy: `Use literal flags \u2014 saastroStudio({ autoWrap: true, autoWrapPages: true }) \u2014 so the doctor can verify it, or add the marker by hand.`,
|
|
214
|
+
snippet: `data-saastro="sec:${key}"`
|
|
215
|
+
});
|
|
216
|
+
} else {
|
|
217
|
+
push({
|
|
218
|
+
file: f.path,
|
|
219
|
+
sectionPrefix: key,
|
|
220
|
+
status: "missing_marker",
|
|
221
|
+
reason: `Route page renders t.${key} on a content element matching its path, but autoWrapPages is OFF so no marker ships.`,
|
|
222
|
+
remedy: `Enable it once \u2014 saastroStudio({ autoWrap: true, autoWrapPages: true }) in astro.config \u2014 or add the marker by hand.`,
|
|
223
|
+
snippet: `data-saastro="sec:${key}"`
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (key && !anchor) {
|
|
229
|
+
push({
|
|
230
|
+
file: f.path,
|
|
231
|
+
sectionPrefix: key,
|
|
232
|
+
status: "missing_marker",
|
|
233
|
+
reason: `Route page renders t.${key} only through child components (no HTML element to anchor) \u2014 autoWrapPages can't mark it here.`,
|
|
234
|
+
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.`,
|
|
235
|
+
snippet: `data-saastro="sec:${key}"`
|
|
236
|
+
});
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (anchor) {
|
|
240
|
+
const refs = f.tKeys.filter((k) => i18nResolves(i18nKeys, k) && !instrumentedKeys.has(k));
|
|
241
|
+
if (refs.length > 0) {
|
|
242
|
+
const k2 = refs[0];
|
|
243
|
+
push({
|
|
244
|
+
file: f.path,
|
|
245
|
+
sectionPrefix: k2,
|
|
246
|
+
status: "missing_marker",
|
|
247
|
+
reason: `Route page renders i18n content (t.${k2}) that doesn't match its path "${routeBase}", and has no marker.`,
|
|
248
|
+
remedy: `Add a marker so the Studio enumerates it (fields then come from t.${k2}).`,
|
|
249
|
+
snippet: `data-saastro="sec:${k2}"`
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
for (const f of facts) {
|
|
255
|
+
if (!f.path.endsWith(".astro")) continue;
|
|
256
|
+
const gaps = findAutoWrapGaps(f.content);
|
|
257
|
+
if (gaps.length === 0) continue;
|
|
258
|
+
const key = f.markerKeys[0] ?? f.ownerKeys[0] ?? fileBaseCamel(f.path);
|
|
259
|
+
for (const g of gaps) {
|
|
260
|
+
push({
|
|
261
|
+
file: f.path,
|
|
262
|
+
sectionPrefix: key,
|
|
263
|
+
status: "autowrap_gap",
|
|
264
|
+
reason: `Array prop "${g.field}" is iterated via a derived variable \u2014 autoWrap can't instrument its items.`,
|
|
265
|
+
remedy: "Add a hand-written data-saastro-field marker on the iterated element.",
|
|
266
|
+
snippet: g.snippet
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const byStatus = {
|
|
271
|
+
editable_props: 0,
|
|
272
|
+
editable_i18n: 0,
|
|
273
|
+
no_schema_no_i18n: 0,
|
|
274
|
+
missing_marker: 0,
|
|
275
|
+
autowrap_gap: 0
|
|
276
|
+
};
|
|
277
|
+
for (const s of sections) byStatus[s.status]++;
|
|
278
|
+
const editable = byStatus.editable_props + byStatus.editable_i18n;
|
|
279
|
+
const needsAttention = byStatus.no_schema_no_i18n + byStatus.missing_marker + byStatus.autowrap_gap;
|
|
280
|
+
return {
|
|
281
|
+
sections,
|
|
282
|
+
stats: {
|
|
283
|
+
files: files.length,
|
|
284
|
+
candidates: instrumentedKeys.size,
|
|
285
|
+
editable,
|
|
286
|
+
needsAttention,
|
|
287
|
+
byStatus
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ../studio-core/dist/chunk-TKNO3PXL.js
|
|
293
|
+
function stripCommentsAndStrings(s) {
|
|
294
|
+
let out = "";
|
|
295
|
+
let i = 0;
|
|
296
|
+
const n = s.length;
|
|
297
|
+
while (i < n) {
|
|
298
|
+
const ch = s[i];
|
|
299
|
+
const nx = s[i + 1];
|
|
300
|
+
if (ch === "/" && nx === "/") {
|
|
301
|
+
i += 2;
|
|
302
|
+
while (i < n && s[i] !== "\n") i++;
|
|
303
|
+
out += " ";
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (ch === "/" && nx === "*") {
|
|
307
|
+
i += 2;
|
|
308
|
+
while (i < n && !(s[i] === "*" && s[i + 1] === "/")) i++;
|
|
309
|
+
i += 2;
|
|
310
|
+
out += " ";
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
314
|
+
const q = ch;
|
|
315
|
+
out += q;
|
|
316
|
+
i++;
|
|
317
|
+
while (i < n && s[i] !== q) {
|
|
318
|
+
if (s[i] === "\\") i++;
|
|
319
|
+
i++;
|
|
320
|
+
}
|
|
321
|
+
out += q;
|
|
322
|
+
i++;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
out += ch;
|
|
326
|
+
i++;
|
|
327
|
+
}
|
|
328
|
+
return out;
|
|
329
|
+
}
|
|
330
|
+
function flagState(c, name) {
|
|
331
|
+
if (new RegExp(`\\b${name}\\s*:\\s*true\\b`).test(c)) return "true";
|
|
332
|
+
if (new RegExp(`\\b${name}\\s*:\\s*false\\b`).test(c)) return "false";
|
|
333
|
+
if (new RegExp(`\\b${name}\\b`).test(c)) return "unknown";
|
|
334
|
+
return "absent";
|
|
335
|
+
}
|
|
336
|
+
function detectAutoWrapPagesInConfig(source) {
|
|
337
|
+
const c = stripCommentsAndStrings(source);
|
|
338
|
+
const wrap = flagState(c, "autoWrap");
|
|
339
|
+
const pages = flagState(c, "autoWrapPages");
|
|
340
|
+
if (wrap === "false" || pages === "false") return false;
|
|
341
|
+
if (wrap === "absent" || pages === "absent") return false;
|
|
342
|
+
if (wrap === "true" && pages === "true") return true;
|
|
343
|
+
return "unknown";
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ../studio-core/dist/doctor-checks.js
|
|
347
|
+
function flattenKeys(obj, prefix = "") {
|
|
348
|
+
if (obj == null || typeof obj !== "object" || Array.isArray(obj)) {
|
|
349
|
+
return prefix ? [prefix] : [];
|
|
350
|
+
}
|
|
351
|
+
const out = [];
|
|
352
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
353
|
+
const path = prefix ? `${prefix}.${k}` : k;
|
|
354
|
+
if (v != null && typeof v === "object" && !Array.isArray(v)) {
|
|
355
|
+
out.push(...flattenKeys(v, path));
|
|
356
|
+
} else {
|
|
357
|
+
out.push(path);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return out;
|
|
361
|
+
}
|
|
362
|
+
function checkI18nParity(locales) {
|
|
363
|
+
if (locales.length < 2) return [];
|
|
364
|
+
const withKeys = locales.map((l) => ({ ...l, keys: new Set(flattenKeys(l.data)) }));
|
|
365
|
+
const union = /* @__PURE__ */ new Set();
|
|
366
|
+
for (const l of withKeys) for (const k of l.keys) union.add(k);
|
|
367
|
+
const findings = [];
|
|
368
|
+
for (const l of withKeys) {
|
|
369
|
+
for (const key of union) {
|
|
370
|
+
if (l.keys.has(key)) continue;
|
|
371
|
+
findings.push({
|
|
372
|
+
file: l.file,
|
|
373
|
+
status: "i18n_parity",
|
|
374
|
+
severity: "error",
|
|
375
|
+
reason: `Translation key "${key}" is missing in locale "${l.locale}".`,
|
|
376
|
+
remedy: `Add "${key}" to ${l.file} so every locale has parity.`,
|
|
377
|
+
detail: key
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return findings;
|
|
382
|
+
}
|
|
383
|
+
var HEX_RE = /#([0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{4}|[0-9a-fA-F]{3})\b/g;
|
|
384
|
+
function collectHex(fragment, into) {
|
|
385
|
+
for (const m of fragment.matchAll(HEX_RE)) into.add("#" + m[1].toLowerCase());
|
|
386
|
+
}
|
|
387
|
+
function checkTokenCompliance(files) {
|
|
388
|
+
const findings = [];
|
|
389
|
+
for (const f of files) {
|
|
390
|
+
if (!f.path.endsWith(".astro")) continue;
|
|
391
|
+
const content = f.content.replace(/<svg[\s\S]*?<\/svg>/gi, " ");
|
|
392
|
+
const hits = /* @__PURE__ */ new Set();
|
|
393
|
+
for (const m of content.matchAll(/\[#([0-9a-fA-F]{3,8})\]/g)) hits.add("#" + m[1].toLowerCase());
|
|
394
|
+
for (const m of content.matchAll(/style\s*=\s*["']([^"']*)["']/g)) collectHex(m[1], hits);
|
|
395
|
+
for (const m of content.matchAll(/style\s*=\s*\{\{([^}]*)\}\}/g)) collectHex(m[1], hits);
|
|
396
|
+
for (const m of content.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/g)) collectHex(m[1], hits);
|
|
397
|
+
for (const hex of hits) {
|
|
398
|
+
findings.push({
|
|
399
|
+
file: f.path,
|
|
400
|
+
status: "hardcoded_hex",
|
|
401
|
+
severity: "error",
|
|
402
|
+
reason: `Hardcoded hex color "${hex}" \u2014 use a design token (oklch var) instead.`,
|
|
403
|
+
remedy: `Replace ${hex} with a token: var(--primary)/--accent or add --brand in src/styles/global.css.`,
|
|
404
|
+
detail: hex
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return findings;
|
|
409
|
+
}
|
|
410
|
+
var RAW_FORM_RE = /<form[\s/>]/;
|
|
411
|
+
function checkFormCompliance(files) {
|
|
412
|
+
const findings = [];
|
|
413
|
+
for (const f of files) {
|
|
414
|
+
if (!/\.(astro|tsx|jsx)$/.test(f.path)) continue;
|
|
415
|
+
if (!RAW_FORM_RE.test(stripCommentsAndStrings(f.content))) continue;
|
|
416
|
+
findings.push({
|
|
417
|
+
file: f.path,
|
|
418
|
+
status: "raw_form",
|
|
419
|
+
severity: "error",
|
|
420
|
+
reason: "Raw <form> element \u2014 forms must go through <HubForm> (@saastro/forms).",
|
|
421
|
+
remedy: "Replace the <form> with <HubForm> so submits hit submit.saastro.io with validation/captcha.",
|
|
422
|
+
detail: "<form>"
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
return findings;
|
|
426
|
+
}
|
|
427
|
+
var ALLOWLIST_PREFIXES = ["src/pages/", "src/content/", "src/i18n/"];
|
|
428
|
+
var ALLOWLIST_FILES = ["studio.config.json"];
|
|
429
|
+
function isAllowlisted(path) {
|
|
430
|
+
if (ALLOWLIST_FILES.some((f) => path.endsWith(f))) return true;
|
|
431
|
+
if (path.endsWith(".md") || path.endsWith(".mdx")) return true;
|
|
432
|
+
return ALLOWLIST_PREFIXES.some((p) => path.includes(p));
|
|
433
|
+
}
|
|
434
|
+
var LITERAL_TEXT_RE = />\s*([A-Z][A-Za-z0-9'’,.!?-]*(?:\s+[A-Za-z0-9'’,.!?-]+)+)\s*</g;
|
|
435
|
+
function checkAllowlistCompliance(files) {
|
|
436
|
+
const findings = [];
|
|
437
|
+
for (const f of files) {
|
|
438
|
+
if (!f.path.endsWith(".astro")) continue;
|
|
439
|
+
if (isAllowlisted(f.path)) continue;
|
|
440
|
+
if (!/data-saastro\s*=/.test(f.content)) continue;
|
|
441
|
+
const literals = /* @__PURE__ */ new Set();
|
|
442
|
+
for (const m of f.content.matchAll(LITERAL_TEXT_RE)) {
|
|
443
|
+
const text = m[1].trim();
|
|
444
|
+
if (text.length >= 6) literals.add(text);
|
|
445
|
+
}
|
|
446
|
+
for (const text of literals) {
|
|
447
|
+
findings.push({
|
|
448
|
+
file: f.path,
|
|
449
|
+
status: "allowlist_violation",
|
|
450
|
+
severity: "warn",
|
|
451
|
+
reason: `Hardcoded copy "${truncate(text)}" in a marked section \u2014 not editable via the Hub (outside the i18n allowlist).`,
|
|
452
|
+
remedy: "Move the text into an i18n namespace and render it via {t.<key>.<field>}.",
|
|
453
|
+
detail: text
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return findings;
|
|
458
|
+
}
|
|
459
|
+
function truncate(s) {
|
|
460
|
+
return s.length > 48 ? s.slice(0, 45) + "\u2026" : s;
|
|
461
|
+
}
|
|
462
|
+
function runDoctorChecks(input) {
|
|
463
|
+
const findings = [
|
|
464
|
+
...checkI18nParity(input.locales),
|
|
465
|
+
...checkTokenCompliance(input.files),
|
|
466
|
+
...checkFormCompliance(input.files),
|
|
467
|
+
...checkAllowlistCompliance(input.files)
|
|
468
|
+
];
|
|
469
|
+
const byStatus = {
|
|
470
|
+
i18n_parity: 0,
|
|
471
|
+
hardcoded_hex: 0,
|
|
472
|
+
raw_form: 0,
|
|
473
|
+
allowlist_violation: 0
|
|
474
|
+
};
|
|
475
|
+
let needsAttention = 0;
|
|
476
|
+
let warnings = 0;
|
|
477
|
+
for (const f of findings) {
|
|
478
|
+
byStatus[f.status]++;
|
|
479
|
+
if (f.severity === "error") needsAttention++;
|
|
480
|
+
else warnings++;
|
|
481
|
+
}
|
|
482
|
+
return { findings, stats: { needsAttention, warnings, byStatus } };
|
|
483
|
+
}
|
|
484
|
+
export {
|
|
485
|
+
analyzeSections,
|
|
486
|
+
checkAllowlistCompliance,
|
|
487
|
+
checkFormCompliance,
|
|
488
|
+
checkI18nParity,
|
|
489
|
+
checkTokenCompliance,
|
|
490
|
+
detectAutoWrapPagesInConfig,
|
|
491
|
+
findAutoWrapGaps,
|
|
492
|
+
runDoctorChecks,
|
|
493
|
+
stripCommentsAndStrings
|
|
494
|
+
};
|
|
495
|
+
//# sourceMappingURL=doctor.js.map
|
|
@@ -0,0 +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":"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","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
saastro
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-ZHCSQE2N.js";
|
|
4
|
+
import "./chunk-DKDGF6IQ.js";
|
|
5
|
+
import "./chunk-MN3J6PBW.js";
|
|
6
6
|
import {
|
|
7
7
|
editableImage
|
|
8
8
|
} from "./chunk-FLFH5RVH.js";
|
|
9
|
+
import "./chunk-PKDMKMD6.js";
|
|
9
10
|
|
|
10
11
|
// src/helpers/editable-section.ts
|
|
11
12
|
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,8 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
saastro
|
|
3
|
-
} from "../chunk-
|
|
4
|
-
import "../chunk-
|
|
5
|
-
import "../chunk-
|
|
3
|
+
} from "../chunk-ZHCSQE2N.js";
|
|
4
|
+
import "../chunk-DKDGF6IQ.js";
|
|
5
|
+
import "../chunk-MN3J6PBW.js";
|
|
6
|
+
import "../chunk-PKDMKMD6.js";
|
|
6
7
|
export {
|
|
7
8
|
saastro as default
|
|
8
9
|
};
|