@saastro/studio 0.4.0 → 0.5.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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  studioAutoWrap
3
- } from "./chunk-5EXFBVC2.js";
3
+ } from "./chunk-WTHLK3XQ.js";
4
4
 
5
5
  // src/integration/index.ts
6
6
  function saastro(options = {}) {
@@ -25,4 +25,4 @@ function saastro(options = {}) {
25
25
  export {
26
26
  saastro
27
27
  };
28
- //# sourceMappingURL=chunk-S5K6H6MF.js.map
28
+ //# sourceMappingURL=chunk-HEBSQKPT.js.map
@@ -1,6 +1,6 @@
1
1
  // src/vite/meta-tag.ts
2
2
  var STUDIO_PROTOCOL_VERSION = "2";
3
- var PACKAGE_VERSION = "0.4.0".length > 0 ? "0.4.0" : "0.0.0+unknown";
3
+ var PACKAGE_VERSION = "0.5.0".length > 0 ? "0.5.0" : "0.0.0+unknown";
4
4
  var META_TAGS = [
5
5
  `<meta name="saastro:studio-version" content="${STUDIO_PROTOCOL_VERSION}">`,
6
6
  `<meta name="saastro:studio-package" content="${PACKAGE_VERSION}">`
@@ -68,4 +68,4 @@ export {
68
68
  buildVisibilityStyleTag,
69
69
  injectVisibilityStyle
70
70
  };
71
- //# sourceMappingURL=chunk-IPVMSMB4.js.map
71
+ //# sourceMappingURL=chunk-MMUEPPBK.js.map
@@ -3,7 +3,7 @@ import {
3
3
  injectVersionMetaTag,
4
4
  injectVisibilityStyle,
5
5
  readVisibilityConfigSync
6
- } from "./chunk-IPVMSMB4.js";
6
+ } from "./chunk-MMUEPPBK.js";
7
7
 
8
8
  // src/vite/index.ts
9
9
  import { promises as fs } from "node:fs";
@@ -20,6 +20,7 @@ var STUDIO_DIRECTIVE_KINDS = /* @__PURE__ */ new Set([
20
20
  "enum",
21
21
  "number",
22
22
  "boolean",
23
+ "date",
23
24
  "array",
24
25
  "object",
25
26
  "json"
@@ -27,6 +28,7 @@ var STUDIO_DIRECTIVE_KINDS = /* @__PURE__ */ new Set([
27
28
  var STUDIO_DIRECTIVE_RE = /@studio:([A-Za-z][A-Za-z0-9-]*)/g;
28
29
  var IMAGE_NAME_HINTS = /(^|_)(image|img|avatar|logo|icon|thumbnail|photo|picture|cover|background)($|_)/i;
29
30
  var IMAGE_SUFFIX_HINTS = /_(url|src)$/i;
31
+ var DATE_NAME_HINTS = /(^|_)date($|_)|date$|_at$|At$|^(published|updated|created|expires?|start|end|due)(date|at)?$/i;
30
32
  var FRONTMATTER_RE = /^\s*---\s*\n([\s\S]*?)\n---/;
31
33
  function extractFrontmatter(astroSource) {
32
34
  const m = FRONTMATTER_RE.exec(astroSource);
@@ -219,7 +221,7 @@ function describeProperty(prop, parent) {
219
221
  const type = prop.getTypeAtLocation(propsSourceFile);
220
222
  const optional = isQuestionToken || typeIncludes(type, (t) => t.isUndefined());
221
223
  const directive = readStudioDirective(decl);
222
- if (type.isBoolean() || stripUndefined(type).isBoolean()) {
224
+ if (isBooleanType(type)) {
223
225
  return applyDirective({ name, kind: "boolean", optional }, directive, type);
224
226
  }
225
227
  const nonNullishParts = type.isUnion() ? type.getUnionTypes().filter((t) => !t.isUndefined() && !t.isNull()) : [type];
@@ -302,12 +304,16 @@ function isDirectiveCompatible(directive, type) {
302
304
  case "textarea":
303
305
  case "image-url":
304
306
  return nonNullishParts.every((t) => t.isString() || t.isStringLiteral());
307
+ case "date":
308
+ return nonNullishParts.every(
309
+ (t) => t.isString() || t.isStringLiteral() || (t.getSymbol() ?? t.getAliasSymbol())?.getName() === "Date"
310
+ );
305
311
  case "enum":
306
312
  return nonNullishParts.every((t) => t.isString() || t.isStringLiteral());
307
313
  case "number":
308
314
  return nonNullishParts.every((t) => t.isNumber() || t.isNumberLiteral());
309
315
  case "boolean":
310
- return type.isBoolean() || stripped.isBoolean();
316
+ return isBooleanType(type);
311
317
  case "array":
312
318
  return nonNullishParts.every((t) => t.isArray());
313
319
  case "object":
@@ -354,7 +360,10 @@ function describeType(name, type, depth = 0) {
354
360
  return { name, kind: pickStringKind(name), optional: false };
355
361
  }
356
362
  if (type.isNumber()) return { name, kind: "number", optional: false };
357
- if (type.isBoolean()) return { name, kind: "boolean", optional: false };
363
+ if (isBooleanType(type)) return { name, kind: "boolean", optional: false };
364
+ if ((type.getSymbol() ?? type.getAliasSymbol())?.getName() === "Date") {
365
+ return { name, kind: "date", optional: false };
366
+ }
358
367
  if (depth >= MAX_DESCENT_DEPTH) {
359
368
  return {
360
369
  name,
@@ -401,6 +410,7 @@ function describeType(name, type, depth = 0) {
401
410
  }
402
411
  function describeAnyType(name, type, depth = 0) {
403
412
  const optional = typeIncludes(type, (t) => t.isUndefined());
413
+ if (isBooleanType(type)) return { name, kind: "boolean", optional };
404
414
  const nonNullishParts = type.isUnion() ? type.getUnionTypes().filter((t) => !t.isUndefined() && !t.isNull()) : [type];
405
415
  if (nonNullishParts.length >= 1 && nonNullishParts.every((t) => t.isStringLiteral())) {
406
416
  if (nonNullishParts.length === 1) {
@@ -428,6 +438,9 @@ function pickStringKind(name) {
428
438
  if (IMAGE_NAME_HINTS.test(name) || IMAGE_SUFFIX_HINTS.test(name)) {
429
439
  return "image-url";
430
440
  }
441
+ if (DATE_NAME_HINTS.test(name)) {
442
+ return "date";
443
+ }
431
444
  if (/description|body|content|copy|summary/i.test(name)) {
432
445
  return "textarea";
433
446
  }
@@ -445,6 +458,11 @@ function stripUndefined(type) {
445
458
  const remaining = type.getUnionTypes().filter((t) => !t.isUndefined() && !t.isNull());
446
459
  return remaining[0] ?? type;
447
460
  }
461
+ function isBooleanType(type) {
462
+ if (type.isBoolean() || type.isBooleanLiteral()) return true;
463
+ const parts = type.isUnion() ? type.getUnionTypes().filter((t) => !t.isUndefined() && !t.isNull()) : [type];
464
+ return parts.length > 0 && parts.every((t) => t.isBoolean() || t.isBooleanLiteral());
465
+ }
448
466
  function applyDefaultsFromDestructuring(frontmatter, fields) {
449
467
  const match = /const\s*\{([\s\S]*?)\}\s*=\s*Astro\.props/.exec(frontmatter);
450
468
  if (!match) return;
@@ -493,23 +511,176 @@ function parseLiteral(rhs) {
493
511
  var FRONTMATTER_RE2 = /^\s*---\s*\n([\s\S]*?)\n---\s*\n/;
494
512
  var SAASTRO_ATTR_RE = /data-saastro/;
495
513
  var FIELD_PREFIX_REFERENCE_RE = /\bfieldPrefix\b/;
496
- var FIRST_OPEN_TAG_RE = /<([a-zA-Z][a-zA-Z0-9]*)\b([^<>]*?)\s*(\/?)>/;
514
+ var OPEN_TAG_NAME_RE = /^<([a-zA-Z][a-zA-Z0-9]*)/;
497
515
  var INJECTION = ' {...(fieldPrefix ? { "data-saastro": `sec:${fieldPrefix}`, draggable: "true" } : {})}';
516
+ function findTagEnd(s, start) {
517
+ let i = start + 1;
518
+ let quote = "";
519
+ let brace = 0;
520
+ while (i < s.length) {
521
+ const c = s[i];
522
+ if (quote) {
523
+ if (c === quote) quote = "";
524
+ } else if (c === '"' || c === "'" || c === "`") {
525
+ quote = c;
526
+ } else if (c === "{") {
527
+ brace++;
528
+ } else if (c === "}") {
529
+ if (brace > 0) brace--;
530
+ } else if (c === ">" && brace === 0) {
531
+ return i;
532
+ }
533
+ i++;
534
+ }
535
+ return -1;
536
+ }
537
+ function injectIntoTag(template, tagStart, tagName, tagEnd, injection) {
538
+ const between = template.slice(tagStart + 1 + tagName.length, tagEnd);
539
+ const selfCloseMatch = /\s*\/\s*$/.exec(between);
540
+ let attrs = between;
541
+ let selfClose = false;
542
+ if (selfCloseMatch) {
543
+ selfClose = true;
544
+ attrs = between.slice(0, selfCloseMatch.index);
545
+ }
546
+ attrs = attrs.replace(/\s+$/, "");
547
+ const rebuilt = `<${tagName}${attrs}${injection}${selfClose ? " />" : ">"}`;
548
+ return template.slice(0, tagStart) + rebuilt + template.slice(tagEnd + 1);
549
+ }
550
+ function firstTagStart(template) {
551
+ return template.search(/<[a-zA-Z]/);
552
+ }
498
553
  function transformAstroSource(source) {
499
554
  const fm = FRONTMATTER_RE2.exec(source);
500
555
  if (!fm) return source;
501
556
  if (!FIELD_PREFIX_REFERENCE_RE.test(fm[1])) return source;
502
- const template = source.slice(fm[0].length);
503
- const tagMatch = FIRST_OPEN_TAG_RE.exec(template);
504
- if (!tagMatch) return source;
505
- const tagName = tagMatch[1];
557
+ const fmLen = fm[0].length;
558
+ const template = source.slice(fmLen);
559
+ const start = firstTagStart(template);
560
+ if (start === -1) return source;
561
+ const nameMatch = OPEN_TAG_NAME_RE.exec(template.slice(start));
562
+ if (!nameMatch) return source;
563
+ const tagName = nameMatch[1];
506
564
  if (tagName[0] !== tagName[0].toLowerCase()) return source;
507
- const existingAttrs = tagMatch[2];
508
- if (SAASTRO_ATTR_RE.test(existingAttrs)) return source;
509
- const selfClose = tagMatch[3];
510
- const replacement = `<${tagName}${existingAttrs}${INJECTION}${selfClose ? " " + selfClose : ""}>`;
511
- const templateInjected = template.slice(0, tagMatch.index) + replacement + template.slice(tagMatch.index + tagMatch[0].length);
512
- return source.slice(0, fm[0].length) + templateInjected;
565
+ const tagEnd = findTagEnd(template, start);
566
+ if (tagEnd === -1) return source;
567
+ const between = template.slice(start + 1 + tagName.length, tagEnd);
568
+ if (SAASTRO_ATTR_RE.test(between)) return source;
569
+ return source.slice(0, fmLen) + injectIntoTag(template, start, tagName, tagEnd, INJECTION);
570
+ }
571
+ var SKIP_BODY_TAGS = /* @__PURE__ */ new Set(["script", "style", "noscript", "template", "title"]);
572
+ var SKIP_STRUCTURAL_TAGS = /* @__PURE__ */ new Set(["html", "head", "body", "link", "meta", "base", "slot"]);
573
+ var PAGE_MARKER_RE = /data-saastro\s*=|["']data-saastro["']\s*:/;
574
+ var T_USAGE_RE = /(?:Astro\.locals\.)?\bt\.([A-Za-z_$][\w$]*)/g;
575
+ function indexAfterCloseTag(s, lower, from) {
576
+ const re = new RegExp(`</\\s*${lower}\\s*>`, "i");
577
+ const m = re.exec(s.slice(from));
578
+ return m ? from + m.index + m[0].length : -1;
579
+ }
580
+ function findFirstContentTag(template) {
581
+ const n = template.length;
582
+ let i = 0;
583
+ let brace = 0;
584
+ while (i < n) {
585
+ const c = template[i];
586
+ if (c === "{") {
587
+ brace++;
588
+ i++;
589
+ continue;
590
+ }
591
+ if (c === "}") {
592
+ if (brace > 0) brace--;
593
+ i++;
594
+ continue;
595
+ }
596
+ if (c !== "<") {
597
+ i++;
598
+ continue;
599
+ }
600
+ if (template.startsWith("<!--", i)) {
601
+ const end = template.indexOf("-->", i + 4);
602
+ i = end === -1 ? n : end + 3;
603
+ continue;
604
+ }
605
+ const nameMatch = OPEN_TAG_NAME_RE.exec(template.slice(i));
606
+ if (!nameMatch) {
607
+ i++;
608
+ continue;
609
+ }
610
+ const tagName = nameMatch[1];
611
+ const tagEnd = findTagEnd(template, i);
612
+ if (tagEnd === -1) return null;
613
+ const lower = tagName.toLowerCase();
614
+ if (SKIP_BODY_TAGS.has(lower)) {
615
+ const after = indexAfterCloseTag(template, lower, tagEnd + 1);
616
+ i = after === -1 ? n : after;
617
+ continue;
618
+ }
619
+ const isComponent = tagName[0] !== tagName[0].toLowerCase();
620
+ if (isComponent || SKIP_STRUCTURAL_TAGS.has(lower) || brace > 0) {
621
+ i = tagEnd + 1;
622
+ continue;
623
+ }
624
+ return { tagStart: i, tagName, tagEnd };
625
+ }
626
+ return null;
627
+ }
628
+ function normalizeKey(s) {
629
+ return s.toLowerCase().replace(/[-_\s.]/g, "");
630
+ }
631
+ function routeBaseName(path) {
632
+ const m = /(?:^|\/)src\/pages\/(.+\.astro)$/.exec(path);
633
+ if (!m) return null;
634
+ const rel = m[1].replace(/\.astro$/, "");
635
+ const segments = rel.split("/").filter(Boolean);
636
+ const isDynamic = (s) => s.includes("[") || s.includes("]");
637
+ const isLocale = (s) => /^[a-z]{2}(-[a-z]{2})?$/i.test(s);
638
+ let last = segments.pop();
639
+ if (!last || isDynamic(last)) return null;
640
+ if (last === "index") {
641
+ let parent;
642
+ while (segments.length) {
643
+ const s = segments.pop();
644
+ if (!isDynamic(s) && !isLocale(s)) {
645
+ parent = s;
646
+ break;
647
+ }
648
+ }
649
+ last = parent ?? "home";
650
+ }
651
+ return last;
652
+ }
653
+ function scanTUsage(source) {
654
+ const out = /* @__PURE__ */ new Set();
655
+ T_USAGE_RE.lastIndex = 0;
656
+ let m;
657
+ while ((m = T_USAGE_RE.exec(source)) !== null) out.add(m[1]);
658
+ return out;
659
+ }
660
+ function resolvePageKey(source, path) {
661
+ const base = routeBaseName(path);
662
+ if (!base) return null;
663
+ const target = normalizeKey(base);
664
+ for (const ns of scanTUsage(source)) {
665
+ if (normalizeKey(ns) === target) return ns;
666
+ }
667
+ return null;
668
+ }
669
+ function transformAstroPage(source, key) {
670
+ const fm = FRONTMATTER_RE2.exec(source);
671
+ const fmLen = fm ? fm[0].length : 0;
672
+ const template = source.slice(fmLen);
673
+ if (PAGE_MARKER_RE.test(template)) return source;
674
+ const hit = findFirstContentTag(template);
675
+ if (!hit) return source;
676
+ const injected = injectIntoTag(
677
+ template,
678
+ hit.tagStart,
679
+ hit.tagName,
680
+ hit.tagEnd,
681
+ ` data-saastro="sec:${key}"`
682
+ );
683
+ return source.slice(0, fmLen) + injected;
513
684
  }
514
685
 
515
686
  // src/vite/extract-props-schema.ts
@@ -544,7 +715,7 @@ function extractPropsSchema(source, options = {}) {
544
715
  // src/vite/inject-schema.ts
545
716
  var FRONTMATTER_RE3 = /^\s*---\s*\n([\s\S]*?)\n---\s*\n/;
546
717
  var SCHEMA_SCRIPT_MARKER = 'type="application/saastro-schema"';
547
- var FIRST_OPEN_TAG_RE2 = /<([a-zA-Z][a-zA-Z0-9]*)\b([^<>]*?)\s*(\/?)>/;
718
+ var FIRST_OPEN_TAG_RE = /<([a-zA-Z][a-zA-Z0-9]*)\b([^<>]*?)\s*(\/?)>/;
548
719
  function extractFieldPrefixDefault(frontmatter) {
549
720
  const destruct = /const\s*\{([\s\S]*?)\}\s*=\s*Astro\.props/.exec(frontmatter);
550
721
  if (!destruct) return null;
@@ -589,7 +760,7 @@ function injectSchema(source, schema, options = {}) {
589
760
  const frontmatter = fm[1] ?? "";
590
761
  const templateStart = fm[0].length;
591
762
  const template = source.slice(templateStart);
592
- const tagMatch = FIRST_OPEN_TAG_RE2.exec(template);
763
+ const tagMatch = FIRST_OPEN_TAG_RE.exec(template);
593
764
  if (!tagMatch) return source;
594
765
  const tagName = tagMatch[1];
595
766
  if (tagName[0] !== tagName[0].toLowerCase()) return source;
@@ -1206,6 +1377,7 @@ function studioAutoWrap(opts = {}) {
1206
1377
  const enabled = opts.autoWrap === true;
1207
1378
  const include = opts.include ?? defaultInclude;
1208
1379
  const instrumentTsx = opts.instrumentTsx !== false;
1380
+ const autoWrapPages = opts.autoWrapPages === true;
1209
1381
  let projectRoot;
1210
1382
  return {
1211
1383
  name: "@saastro/studio:auto-wrap",
@@ -1227,7 +1399,7 @@ function studioAutoWrap(opts = {}) {
1227
1399
  const { path } = splitId(id);
1228
1400
  if (isMainAstroModule(id)) {
1229
1401
  if (!include(path)) return null;
1230
- return loadAstro(path);
1402
+ return loadAstro(path, autoWrapPages);
1231
1403
  }
1232
1404
  if (instrumentTsx && isReactComponentModule(id)) {
1233
1405
  if (!include(path)) return null;
@@ -1247,14 +1419,23 @@ var DEFAULT_INCLUDE_EXTS = /\.(astro|tsx|jsx)$/;
1247
1419
  function defaultInclude(id) {
1248
1420
  return DEFAULT_INCLUDE_EXTS.test(id);
1249
1421
  }
1250
- async function loadAstro(path) {
1422
+ async function loadAstro(path, autoWrapPages) {
1251
1423
  let code;
1252
1424
  try {
1253
1425
  code = await fs.readFile(path, "utf8");
1254
1426
  } catch {
1255
1427
  return null;
1256
1428
  }
1257
- if (!hasFieldPrefixReference(code)) return null;
1429
+ if (!hasFieldPrefixReference(code)) {
1430
+ if (autoWrapPages) {
1431
+ const key = resolvePageKey(code, path);
1432
+ if (key) {
1433
+ const pageWrapped = transformAstroPage(code, key);
1434
+ if (pageWrapped !== code) return { code: pageWrapped, map: null };
1435
+ }
1436
+ }
1437
+ return null;
1438
+ }
1258
1439
  const wrapped = transformAstroSource(code);
1259
1440
  const schema = extractPropsSchema(code, { componentPath: path });
1260
1441
  let next = wrapped;
@@ -1284,4 +1465,4 @@ async function loadTsx(path) {
1284
1465
  export {
1285
1466
  studioAutoWrap
1286
1467
  };
1287
- //# sourceMappingURL=chunk-5EXFBVC2.js.map
1468
+ //# sourceMappingURL=chunk-WTHLK3XQ.js.map