@verbaly/compiler 0.19.0 → 0.20.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/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
 
12
12
  ---
13
13
 
14
- The compiler behind [Verbaly](https://github.com/AronSoto/verbaly): AST extraction of `t\`...\``**and JSX`<Trans>` children** into stable hashed keys — or **readable keys** via `` t.id('inbox.title')`…` `` and `<Trans id="inbox.title">…</Trans>` — flat JSON catalog sync, and typed codegen. It also ships the **`verbaly` CLI**.
14
+ The compiler behind [Verbaly](https://github.com/AronSoto/verbaly): AST extraction of `t\`...\``**and JSX`<Trans>` children** into stable hashed keys — or **readable keys** via `` t.id('inbox.title')`…` `` and `<Trans id="inbox.title">…</Trans>` — flat JSON catalog sync, and typed codegen. It also ships the **`verbaly` CLI**. Extraction covers `.js/.ts/.jsx/.tsx` **and `.svelte`/`.vue` single-file components** (script blocks and markup, including Svelte's `$t` store form).
15
15
 
16
16
  > Most projects don't install this directly — [`@verbaly/vite`](https://www.npmjs.com/package/@verbaly/vite) wraps it with zero config. Reach for it when scripting extraction/checks yourself.
17
17
 
package/dist/cli.js CHANGED
@@ -145,7 +145,7 @@ function generateDts(entries) {
145
145
  lines.push(` ${JSON.stringify(key)}: { ${fields} };`);
146
146
  }
147
147
  }
148
- return `// generated by verbaly do not edit
148
+ return `// generated by verbaly: do not edit
149
149
  declare module 'virtual:verbaly' {
150
150
  export interface VerbalyMessages {
151
151
  ${lines.join("\n")}
@@ -323,7 +323,7 @@ function resolveConfig(config = {}) {
323
323
  dir,
324
324
  sourceLocale,
325
325
  locales: [...locales],
326
- include: config.include ?? ["src/**/*.{js,jsx,ts,tsx,mjs,mts}"],
326
+ include: config.include ?? ["{src,app}/**/*.{js,jsx,ts,tsx,mjs,mts,svelte,vue}"],
327
327
  exclude: config.exclude ?? ["**/node_modules/**", "**/dist/**"],
328
328
  translate: config.translate ?? {},
329
329
  render: config.render ?? {}
@@ -379,6 +379,38 @@ function definedOnly(config) {
379
379
  return Object.fromEntries(Object.entries(config).filter(([, value]) => value !== void 0));
380
380
  }
381
381
  //#endregion
382
+ //#region src/registry.ts
383
+ var MessageRegistry = class {
384
+ files = /* @__PURE__ */ new Map();
385
+ update(file, analysis) {
386
+ this.files.set(file, analysis);
387
+ }
388
+ remove(file) {
389
+ this.files.delete(file);
390
+ }
391
+ messages() {
392
+ const out = /* @__PURE__ */ new Map();
393
+ for (const analysis of this.files.values()) for (const msg of analysis.tagged) {
394
+ const existing = out.get(msg.key);
395
+ if (existing) {
396
+ if (existing.message !== msg.message) console.warn(`[verbaly] key collision "${msg.key}": ${JSON.stringify(existing.message)} vs ${JSON.stringify(msg.message)} — second dropped.`);
397
+ continue;
398
+ }
399
+ out.set(msg.key, msg);
400
+ }
401
+ return out;
402
+ }
403
+ usedKeys() {
404
+ const out = /* @__PURE__ */ new Map();
405
+ for (const analysis of this.files.values()) for (const used of analysis.usedKeys) {
406
+ const files = out.get(used.key) ?? [];
407
+ if (!files.includes(used.file)) files.push(used.file);
408
+ out.set(used.key, files);
409
+ }
410
+ return out;
411
+ }
412
+ };
413
+ //#endregion
382
414
  //#region src/key.ts
383
415
  function stableKey(message) {
384
416
  return createHash("sha256").update(message).digest("base64url").slice(0, 8);
@@ -392,7 +424,9 @@ const SKIP_KEYS = /* @__PURE__ */ new Set([
392
424
  "innerComments",
393
425
  "extra"
394
426
  ]);
395
- function analyze(code, file) {
427
+ const DEFAULT_T_NAMES = ["t"];
428
+ function analyze(code, file, options = {}) {
429
+ const names = new Set(options.tNames ?? DEFAULT_T_NAMES);
396
430
  const ast = parse$1(code, {
397
431
  sourceType: "module",
398
432
  errorRecovery: true,
@@ -403,10 +437,10 @@ function analyze(code, file) {
403
437
  walk(ast.program, (node) => {
404
438
  if (node.type === "TaggedTemplateExpression") {
405
439
  const tag = node.tag;
406
- const explicit = explicitId(tag);
407
- if (!explicit && !isTReference(tag)) return;
440
+ const explicit = explicitId(tag, names);
441
+ if (!explicit && !isTReference(tag, names)) return;
408
442
  const quasi = node.quasi;
409
- const message = buildMessage(code, quasi);
443
+ const message = buildMessage(code, quasi, names);
410
444
  if (!message) return;
411
445
  tagged.push({
412
446
  key: explicit ? explicit.key : stableKey(message.text),
@@ -420,20 +454,20 @@ function analyze(code, file) {
420
454
  });
421
455
  } else if (node.type === "CallExpression") {
422
456
  const callee = node.callee;
423
- if (!isTReference(callee)) return;
457
+ if (!isTReference(callee, names)) return;
424
458
  const first = node.arguments[0];
425
459
  if (first?.type === "StringLiteral") usedKeys.push({
426
460
  key: first.value,
427
461
  file
428
462
  });
429
- } else if (node.type === "JSXElement") handleTrans(code, node, file, tagged, usedKeys);
463
+ } else if (node.type === "JSXElement") handleTrans(code, node, file, tagged, usedKeys, names);
430
464
  });
431
465
  return {
432
466
  tagged,
433
467
  usedKeys
434
468
  };
435
469
  }
436
- function handleTrans(code, node, file, tagged, usedKeys) {
470
+ function handleTrans(code, node, file, tagged, usedKeys, names) {
437
471
  const opening = node.openingElement;
438
472
  const nameNode = opening.name;
439
473
  if (nameNode.type !== "JSXIdentifier" || nameNode.name !== "Trans") return;
@@ -445,7 +479,7 @@ function handleTrans(code, node, file, tagged, usedKeys) {
445
479
  if (value?.type !== "StringLiteral") return;
446
480
  const id = value.value;
447
481
  if (attrs.length === 1 && children?.length) {
448
- const built = buildTransMessage(code, children);
482
+ const built = buildTransMessage(code, children, names);
449
483
  if (built?.text.trim()) {
450
484
  tagged.push({
451
485
  key: id,
@@ -472,7 +506,7 @@ function handleTrans(code, node, file, tagged, usedKeys) {
472
506
  }
473
507
  if (attrs.length > 0) return;
474
508
  if (!children?.length) return;
475
- const built = buildTransMessage(code, children);
509
+ const built = buildTransMessage(code, children, names);
476
510
  if (!built || !built.text.trim()) return;
477
511
  tagged.push({
478
512
  key: stableKey(built.text),
@@ -489,14 +523,14 @@ function handleTrans(code, node, file, tagged, usedKeys) {
489
523
  }
490
524
  });
491
525
  }
492
- function explicitId(tag) {
526
+ function explicitId(tag, names) {
493
527
  if (tag.type !== "CallExpression") return void 0;
494
528
  const callee = tag.callee;
495
529
  if (callee.type !== "MemberExpression" || callee.computed) return void 0;
496
530
  const prop = callee.property;
497
531
  if (prop.type !== "Identifier" || prop.name !== "id") return void 0;
498
532
  const obj = callee.object;
499
- if (!isTReference(obj)) return void 0;
533
+ if (!isTReference(obj, names)) return void 0;
500
534
  const args = tag.arguments;
501
535
  const first = args[0];
502
536
  if (args.length !== 1 || first?.type !== "StringLiteral") return void 0;
@@ -506,7 +540,7 @@ function explicitId(tag) {
506
540
  refEnd: obj.end
507
541
  };
508
542
  }
509
- function buildTransMessage(code, children) {
543
+ function buildTransMessage(code, children, names) {
510
544
  const params = [];
511
545
  const components = [];
512
546
  const takenParams = /* @__PURE__ */ new Map();
@@ -521,7 +555,7 @@ function buildTransMessage(code, children) {
521
555
  text += escapeText(expr.value);
522
556
  continue;
523
557
  }
524
- if (containsTaggedT(expr)) return void 0;
558
+ if (containsTaggedT(expr, names)) return void 0;
525
559
  const source = code.slice(expr.start, expr.end);
526
560
  const name = uniqueName(deriveName(expr, params.length), source, takenParams);
527
561
  params.push({
@@ -574,24 +608,24 @@ function selfClosedSource(code, opening) {
574
608
  const src = code.slice(opening.start, opening.end);
575
609
  return src.endsWith("/>") ? src : `${src.slice(0, -1).trimEnd()} />`;
576
610
  }
577
- function containsTaggedT(node) {
611
+ function containsTaggedT(node, names) {
578
612
  let found = false;
579
613
  walk(node, (n) => {
580
614
  if (n.type !== "TaggedTemplateExpression") return;
581
615
  const tag = n.tag;
582
- if (isTReference(tag) || explicitId(tag)) found = true;
616
+ if (isTReference(tag, names) || explicitId(tag, names)) found = true;
583
617
  });
584
618
  return found;
585
619
  }
586
- function isTReference(node) {
587
- if (node.type === "Identifier") return node.name === "t";
620
+ function isTReference(node, names) {
621
+ if (node.type === "Identifier") return names.has(node.name);
588
622
  if (node.type === "MemberExpression" && !node.computed) {
589
623
  const prop = node.property;
590
- return prop.type === "Identifier" && prop.name === "t";
624
+ return prop.type === "Identifier" && names.has(prop.name);
591
625
  }
592
626
  return false;
593
627
  }
594
- function buildMessage(code, quasi) {
628
+ function buildMessage(code, quasi, names) {
595
629
  const quasis = quasi.quasis;
596
630
  const expressions = quasi.expressions;
597
631
  const params = [];
@@ -600,7 +634,7 @@ function buildMessage(code, quasi) {
600
634
  for (let i = 0; i < expressions.length; i++) {
601
635
  const expr = expressions[i];
602
636
  if (!expr) return void 0;
603
- if (containsTaggedT(expr)) return void 0;
637
+ if (containsTaggedT(expr, names)) return void 0;
604
638
  const source = code.slice(expr.start, expr.end);
605
639
  const name = uniqueName(deriveName(expr, i), source, taken);
606
640
  params.push({
@@ -654,37 +688,110 @@ function isNode(value) {
654
688
  return typeof value === "object" && value !== null && typeof value.type === "string";
655
689
  }
656
690
  //#endregion
657
- //#region src/registry.ts
658
- var MessageRegistry = class {
659
- files = /* @__PURE__ */ new Map();
660
- update(file, analysis) {
661
- this.files.set(file, analysis);
691
+ //#region src/sfc.ts
692
+ const SFC_FILE_RE = /\.(?:svelte|vue)$/;
693
+ function analyzeFile(code, file) {
694
+ return SFC_FILE_RE.test(file) ? analyzeSfc(code, file) : analyze(code, file);
695
+ }
696
+ const SCRIPT_RE = /(<script\b[^>]*>)([\s\S]*?)(<\/script\s*>)/gi;
697
+ const STYLE_RE = /<style\b[^>]*>[\s\S]*?<\/style\s*>/gi;
698
+ const COMMENT_RE = /<!--[\s\S]*?-->/g;
699
+ const CANDIDATE_SVELTE = /(?<![\w$])\$?t(?=[`(]|\.id\()/g;
700
+ const CANDIDATE_VUE = /(?<![\w$])t(?=[`(]|\.id\()/g;
701
+ function analyzeSfc(code, file) {
702
+ const svelte = file.endsWith(".svelte");
703
+ const options = svelte ? { tNames: ["t", "$t"] } : void 0;
704
+ const tagged = [];
705
+ const usedKeys = [];
706
+ for (const match of code.matchAll(SCRIPT_RE)) {
707
+ const content = match[2];
708
+ if (!content) continue;
709
+ merge(analyzeSegment(content, file, options), match.index + match[1].length, false);
662
710
  }
663
- remove(file) {
664
- this.files.delete(file);
711
+ const markup = blank(blank(blank(code, SCRIPT_RE), STYLE_RE), COMMENT_RE);
712
+ const candidates = svelte ? CANDIDATE_SVELTE : CANDIDATE_VUE;
713
+ let consumedTo = 0;
714
+ for (const match of markup.matchAll(candidates)) {
715
+ const start = match.index;
716
+ if (start < consumedTo) continue;
717
+ const end = expressionExtent(markup, start);
718
+ if (end === void 0) continue;
719
+ merge(analyzeSegment(code.slice(start, end), file, options), start, true);
720
+ consumedTo = end;
665
721
  }
666
- messages() {
667
- const out = /* @__PURE__ */ new Map();
668
- for (const analysis of this.files.values()) for (const msg of analysis.tagged) {
669
- const existing = out.get(msg.key);
670
- if (existing) {
671
- if (existing.message !== msg.message) console.warn(`[verbaly] key collision "${msg.key}": ${JSON.stringify(existing.message)} vs ${JSON.stringify(msg.message)} — second dropped.`);
672
- continue;
673
- }
674
- out.set(msg.key, msg);
675
- }
676
- return out;
722
+ return {
723
+ tagged,
724
+ usedKeys
725
+ };
726
+ function merge(analysis, offset, markupSegment) {
727
+ if (!analysis) return;
728
+ for (const msg of analysis.tagged) tagged.push({
729
+ ...msg,
730
+ start: msg.start + offset,
731
+ end: msg.end + offset,
732
+ tagStart: msg.tagStart + offset,
733
+ tagEnd: msg.tagEnd + offset,
734
+ params: msg.params.map((p) => ({
735
+ ...p,
736
+ start: p.start + offset,
737
+ end: p.end + offset
738
+ })),
739
+ ...markupSegment && { singleQuote: true }
740
+ });
741
+ usedKeys.push(...analysis.usedKeys);
677
742
  }
678
- usedKeys() {
679
- const out = /* @__PURE__ */ new Map();
680
- for (const analysis of this.files.values()) for (const used of analysis.usedKeys) {
681
- const files = out.get(used.key) ?? [];
682
- if (!files.includes(used.file)) files.push(used.file);
683
- out.set(used.key, files);
743
+ }
744
+ function analyzeSegment(code, file, options) {
745
+ try {
746
+ return analyze(code, file, options);
747
+ } catch {
748
+ return;
749
+ }
750
+ }
751
+ function blank(source, re) {
752
+ return source.replace(re, (m) => " ".repeat(m.length));
753
+ }
754
+ function expressionExtent(code, start) {
755
+ let i = start;
756
+ if (code[i] === "$") i += 1;
757
+ i += 1;
758
+ if (code.startsWith(".id(", i)) {
759
+ const close = balancedEnd(code, i + 3);
760
+ if (close === void 0 || code[close] !== "`") return void 0;
761
+ return balancedEnd(code, close);
762
+ }
763
+ if (code[i] === "`" || code[i] === "(") return balancedEnd(code, i);
764
+ }
765
+ function balancedEnd(code, open) {
766
+ const stack = [];
767
+ let i = open;
768
+ while (i < code.length) {
769
+ const ch = code[i];
770
+ if (stack[stack.length - 1] === "`") {
771
+ if (ch === "\\") i += 1;
772
+ else if (ch === "`") stack.pop();
773
+ else if (ch === "$" && code[i + 1] === "{") {
774
+ stack.push("{");
775
+ i += 1;
776
+ }
777
+ } else if (ch === "'" || ch === "\"") i = stringEnd(code, i);
778
+ else if (ch === "`" || ch === "(" || ch === "{") stack.push(ch);
779
+ else if (ch === ")" || ch === "}") {
780
+ if (stack[stack.length - 1] !== (ch === ")" ? "(" : "{")) return void 0;
781
+ stack.pop();
684
782
  }
685
- return out;
783
+ i += 1;
784
+ if (stack.length === 0) return i;
686
785
  }
687
- };
786
+ }
787
+ function stringEnd(code, start) {
788
+ const quote = code[start];
789
+ let i = start + 1;
790
+ while (i < code.length) if (code[i] === "\\") i += 2;
791
+ else if (code[i] === quote) return i;
792
+ else i += 1;
793
+ return code.length;
794
+ }
688
795
  //#endregion
689
796
  //#region src/extract.ts
690
797
  async function extractProject(cfg) {
@@ -694,7 +801,7 @@ async function extractProject(cfg) {
694
801
  absolute: true
695
802
  });
696
803
  const registry = new MessageRegistry();
697
- for (const file of files) registry.update(file, analyze(readFileSync(file, "utf8"), file));
804
+ for (const file of files) registry.update(file, analyzeFile(readFileSync(file, "utf8"), file));
698
805
  return registry;
699
806
  }
700
807
  function syncCatalogs(cfg, catalogs, registry) {