@saastro/studio 0.4.0 → 0.5.1

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.
@@ -3,7 +3,7 @@ import {
3
3
  injectVersionMetaTag,
4
4
  injectVisibilityStyle,
5
5
  readVisibilityConfigSync
6
- } from "./chunk-IPVMSMB4.js";
6
+ } from "./chunk-VH3YYJEZ.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;
@@ -489,27 +507,190 @@ function parseLiteral(rhs) {
489
507
  return trimmed;
490
508
  }
491
509
 
492
- // src/vite/auto-wrap.ts
510
+ // ../studio-core/dist/auto-wrap-core.js
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
+ function stripCommentsAndStrings(s) {
515
+ return s.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/`(?:[^`\\]|\\.)*`/g, "``").replace(/'(?:[^'\\]|\\.)*'/g, "''").replace(/"(?:[^"\\]|\\.)*"/g, '""').replace(/\/\/[^\n]*/g, " ");
516
+ }
517
+ function frontmatterDeclaresFieldPrefix(fmBody) {
518
+ return FIELD_PREFIX_REFERENCE_RE.test(stripCommentsAndStrings(fmBody));
519
+ }
520
+ function hasFrontmatterFieldPrefix(source) {
521
+ const fm = FRONTMATTER_RE2.exec(source);
522
+ return fm ? frontmatterDeclaresFieldPrefix(fm[1]) : false;
523
+ }
524
+ var OPEN_TAG_NAME_RE = /^<([a-zA-Z][a-zA-Z0-9]*)/;
497
525
  var INJECTION = ' {...(fieldPrefix ? { "data-saastro": `sec:${fieldPrefix}`, draggable: "true" } : {})}';
526
+ function findTagEnd(s, start) {
527
+ let i = start + 1;
528
+ let quote = "";
529
+ let brace = 0;
530
+ while (i < s.length) {
531
+ const c = s[i];
532
+ if (quote) {
533
+ if (c === quote) quote = "";
534
+ } else if (c === '"' || c === "'" || c === "`") {
535
+ quote = c;
536
+ } else if (c === "{") {
537
+ brace++;
538
+ } else if (c === "}") {
539
+ if (brace > 0) brace--;
540
+ } else if (c === ">" && brace === 0) {
541
+ return i;
542
+ }
543
+ i++;
544
+ }
545
+ return -1;
546
+ }
547
+ function injectIntoTag(template, tagStart, tagName, tagEnd, injection) {
548
+ const between = template.slice(tagStart + 1 + tagName.length, tagEnd);
549
+ const selfCloseMatch = /\s*\/\s*$/.exec(between);
550
+ let attrs = between;
551
+ let selfClose = false;
552
+ if (selfCloseMatch) {
553
+ selfClose = true;
554
+ attrs = between.slice(0, selfCloseMatch.index);
555
+ }
556
+ attrs = attrs.replace(/\s+$/, "");
557
+ const rebuilt = `<${tagName}${attrs}${injection}${selfClose ? " />" : ">"}`;
558
+ return template.slice(0, tagStart) + rebuilt + template.slice(tagEnd + 1);
559
+ }
560
+ function firstTagStart(template) {
561
+ return template.search(/<[a-zA-Z]/);
562
+ }
498
563
  function transformAstroSource(source) {
499
564
  const fm = FRONTMATTER_RE2.exec(source);
500
565
  if (!fm) return source;
501
- 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];
566
+ if (!frontmatterDeclaresFieldPrefix(fm[1])) return source;
567
+ const fmLen = fm[0].length;
568
+ const template = source.slice(fmLen);
569
+ const start = firstTagStart(template);
570
+ if (start === -1) return source;
571
+ const nameMatch = OPEN_TAG_NAME_RE.exec(template.slice(start));
572
+ if (!nameMatch) return source;
573
+ const tagName = nameMatch[1];
506
574
  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;
575
+ const tagEnd = findTagEnd(template, start);
576
+ if (tagEnd === -1) return source;
577
+ const between = template.slice(start + 1 + tagName.length, tagEnd);
578
+ if (SAASTRO_ATTR_RE.test(between)) return source;
579
+ return source.slice(0, fmLen) + injectIntoTag(template, start, tagName, tagEnd, INJECTION);
580
+ }
581
+ var SKIP_BODY_TAGS = /* @__PURE__ */ new Set(["script", "style", "noscript", "template", "title"]);
582
+ var SKIP_STRUCTURAL_TAGS = /* @__PURE__ */ new Set(["html", "head", "body", "link", "meta", "base", "slot"]);
583
+ var PAGE_MARKER_RE = /data-saastro\s*=|["']data-saastro["']\s*:/;
584
+ var T_USAGE_RE = /(?:Astro\.locals\.)?\bt\.([A-Za-z_$][\w$]*)/g;
585
+ function indexAfterCloseTag(s, lower, from) {
586
+ const re = new RegExp(`</\\s*${lower}\\s*>`, "i");
587
+ const m = re.exec(s.slice(from));
588
+ return m ? from + m.index + m[0].length : -1;
589
+ }
590
+ function findFirstContentTag(template) {
591
+ const n = template.length;
592
+ let i = 0;
593
+ let brace = 0;
594
+ while (i < n) {
595
+ const c = template[i];
596
+ if (c === "{") {
597
+ brace++;
598
+ i++;
599
+ continue;
600
+ }
601
+ if (c === "}") {
602
+ if (brace > 0) brace--;
603
+ i++;
604
+ continue;
605
+ }
606
+ if (c !== "<") {
607
+ i++;
608
+ continue;
609
+ }
610
+ if (template.startsWith("<!--", i)) {
611
+ const end = template.indexOf("-->", i + 4);
612
+ i = end === -1 ? n : end + 3;
613
+ continue;
614
+ }
615
+ const nameMatch = OPEN_TAG_NAME_RE.exec(template.slice(i));
616
+ if (!nameMatch) {
617
+ i++;
618
+ continue;
619
+ }
620
+ const tagName = nameMatch[1];
621
+ const tagEnd = findTagEnd(template, i);
622
+ if (tagEnd === -1) return null;
623
+ const lower = tagName.toLowerCase();
624
+ if (SKIP_BODY_TAGS.has(lower)) {
625
+ const after = indexAfterCloseTag(template, lower, tagEnd + 1);
626
+ i = after === -1 ? n : after;
627
+ continue;
628
+ }
629
+ const isComponent = tagName[0] !== tagName[0].toLowerCase();
630
+ if (isComponent || SKIP_STRUCTURAL_TAGS.has(lower) || brace > 0) {
631
+ i = tagEnd + 1;
632
+ continue;
633
+ }
634
+ return { tagStart: i, tagName, tagEnd };
635
+ }
636
+ return null;
637
+ }
638
+ function normalizeKey(s) {
639
+ return s.toLowerCase().replace(/[-_\s.]/g, "");
640
+ }
641
+ function routeBaseName(path) {
642
+ const m = /(?:^|\/)src\/pages\/(.+\.astro)$/.exec(path);
643
+ if (!m) return null;
644
+ const rel = m[1].replace(/\.astro$/, "");
645
+ const segments = rel.split("/").filter(Boolean);
646
+ const isDynamic = (s) => s.includes("[") || s.includes("]");
647
+ const isLocale = (s) => /^[a-z]{2}(-[a-z]{2})?$/i.test(s);
648
+ let last = segments.pop();
649
+ if (!last || isDynamic(last)) return null;
650
+ if (last === "index") {
651
+ let parent;
652
+ while (segments.length) {
653
+ const s = segments.pop();
654
+ if (!isDynamic(s) && !isLocale(s)) {
655
+ parent = s;
656
+ break;
657
+ }
658
+ }
659
+ last = parent ?? "home";
660
+ }
661
+ return last;
662
+ }
663
+ function scanTUsage(source) {
664
+ const out = /* @__PURE__ */ new Set();
665
+ T_USAGE_RE.lastIndex = 0;
666
+ let m;
667
+ while ((m = T_USAGE_RE.exec(source)) !== null) out.add(m[1]);
668
+ return out;
669
+ }
670
+ function resolvePageKey(source, path) {
671
+ const base = routeBaseName(path);
672
+ if (!base) return null;
673
+ const target = normalizeKey(base);
674
+ for (const ns of scanTUsage(source)) {
675
+ if (normalizeKey(ns) === target) return ns;
676
+ }
677
+ return null;
678
+ }
679
+ function transformAstroPage(source, key) {
680
+ const fm = FRONTMATTER_RE2.exec(source);
681
+ const fmLen = fm ? fm[0].length : 0;
682
+ const template = source.slice(fmLen);
683
+ if (PAGE_MARKER_RE.test(template)) return source;
684
+ const hit = findFirstContentTag(template);
685
+ if (!hit) return source;
686
+ const injected = injectIntoTag(
687
+ template,
688
+ hit.tagStart,
689
+ hit.tagName,
690
+ hit.tagEnd,
691
+ ` data-saastro="sec:${key}"`
692
+ );
693
+ return source.slice(0, fmLen) + injected;
513
694
  }
514
695
 
515
696
  // src/vite/extract-props-schema.ts
@@ -544,7 +725,7 @@ function extractPropsSchema(source, options = {}) {
544
725
  // src/vite/inject-schema.ts
545
726
  var FRONTMATTER_RE3 = /^\s*---\s*\n([\s\S]*?)\n---\s*\n/;
546
727
  var SCHEMA_SCRIPT_MARKER = 'type="application/saastro-schema"';
547
- var FIRST_OPEN_TAG_RE2 = /<([a-zA-Z][a-zA-Z0-9]*)\b([^<>]*?)\s*(\/?)>/;
728
+ var FIRST_OPEN_TAG_RE = /<([a-zA-Z][a-zA-Z0-9]*)\b([^<>]*?)\s*(\/?)>/;
548
729
  function extractFieldPrefixDefault(frontmatter) {
549
730
  const destruct = /const\s*\{([\s\S]*?)\}\s*=\s*Astro\.props/.exec(frontmatter);
550
731
  if (!destruct) return null;
@@ -589,7 +770,7 @@ function injectSchema(source, schema, options = {}) {
589
770
  const frontmatter = fm[1] ?? "";
590
771
  const templateStart = fm[0].length;
591
772
  const template = source.slice(templateStart);
592
- const tagMatch = FIRST_OPEN_TAG_RE2.exec(template);
773
+ const tagMatch = FIRST_OPEN_TAG_RE.exec(template);
593
774
  if (!tagMatch) return source;
594
775
  const tagName = tagMatch[1];
595
776
  if (tagName[0] !== tagName[0].toLowerCase()) return source;
@@ -1181,10 +1362,6 @@ function isOnlyMeaningfulExpression(body, relStart, relEnd) {
1181
1362
  }
1182
1363
 
1183
1364
  // src/vite/index.ts
1184
- var FRONTMATTER_FIELD_PREFIX_RE = /^\s*---\s*\n[\s\S]*?\bfieldPrefix\b[\s\S]*?\n---\s*\n/;
1185
- function hasFieldPrefixReference(source) {
1186
- return FRONTMATTER_FIELD_PREFIX_RE.test(source);
1187
- }
1188
1365
  function splitId(id) {
1189
1366
  const idx = id.indexOf("?");
1190
1367
  return idx === -1 ? { path: id, query: "" } : { path: id.slice(0, idx), query: id.slice(idx) };
@@ -1206,6 +1383,7 @@ function studioAutoWrap(opts = {}) {
1206
1383
  const enabled = opts.autoWrap === true;
1207
1384
  const include = opts.include ?? defaultInclude;
1208
1385
  const instrumentTsx = opts.instrumentTsx !== false;
1386
+ const autoWrapPages = opts.autoWrapPages === true;
1209
1387
  let projectRoot;
1210
1388
  return {
1211
1389
  name: "@saastro/studio:auto-wrap",
@@ -1227,7 +1405,7 @@ function studioAutoWrap(opts = {}) {
1227
1405
  const { path } = splitId(id);
1228
1406
  if (isMainAstroModule(id)) {
1229
1407
  if (!include(path)) return null;
1230
- return loadAstro(path);
1408
+ return loadAstro(path, autoWrapPages);
1231
1409
  }
1232
1410
  if (instrumentTsx && isReactComponentModule(id)) {
1233
1411
  if (!include(path)) return null;
@@ -1247,14 +1425,23 @@ var DEFAULT_INCLUDE_EXTS = /\.(astro|tsx|jsx)$/;
1247
1425
  function defaultInclude(id) {
1248
1426
  return DEFAULT_INCLUDE_EXTS.test(id);
1249
1427
  }
1250
- async function loadAstro(path) {
1428
+ async function loadAstro(path, autoWrapPages) {
1251
1429
  let code;
1252
1430
  try {
1253
1431
  code = await fs.readFile(path, "utf8");
1254
1432
  } catch {
1255
1433
  return null;
1256
1434
  }
1257
- if (!hasFieldPrefixReference(code)) return null;
1435
+ if (!hasFrontmatterFieldPrefix(code)) {
1436
+ if (autoWrapPages) {
1437
+ const key = resolvePageKey(code, path);
1438
+ if (key) {
1439
+ const pageWrapped = transformAstroPage(code, key);
1440
+ if (pageWrapped !== code) return { code: pageWrapped, map: null };
1441
+ }
1442
+ }
1443
+ return null;
1444
+ }
1258
1445
  const wrapped = transformAstroSource(code);
1259
1446
  const schema = extractPropsSchema(code, { componentPath: path });
1260
1447
  let next = wrapped;
@@ -1284,4 +1471,4 @@ async function loadTsx(path) {
1284
1471
  export {
1285
1472
  studioAutoWrap
1286
1473
  };
1287
- //# sourceMappingURL=chunk-5EXFBVC2.js.map
1474
+ //# sourceMappingURL=chunk-C2QUBB5U.js.map