@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 +1 -1
- package/dist/cli.js +156 -49
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +11 -2
- package/dist/index.js +141 -30
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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/
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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
|
-
|
|
664
|
-
|
|
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
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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
|
-
|
|
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,
|
|
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) {
|