@verbaly/compiler 0.18.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 +157 -49
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +16 -3
- package/dist/index.js +158 -36
- 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")}
|
|
@@ -156,6 +156,7 @@ ${lines.join("\n")}
|
|
|
156
156
|
|
|
157
157
|
export const sourceLocale: string;
|
|
158
158
|
export const locales: string[];
|
|
159
|
+
export function loadMessages(locale: string): Promise<Record<string, string>>;
|
|
159
160
|
export function createInstance(
|
|
160
161
|
options?: import('verbaly').VerbalyOptions<VerbalyKey>,
|
|
161
162
|
): import('verbaly').Verbaly<VerbalyKey>;
|
|
@@ -322,7 +323,7 @@ function resolveConfig(config = {}) {
|
|
|
322
323
|
dir,
|
|
323
324
|
sourceLocale,
|
|
324
325
|
locales: [...locales],
|
|
325
|
-
include: config.include ?? ["src/**/*.{js,jsx,ts,tsx,mjs,mts}"],
|
|
326
|
+
include: config.include ?? ["{src,app}/**/*.{js,jsx,ts,tsx,mjs,mts,svelte,vue}"],
|
|
326
327
|
exclude: config.exclude ?? ["**/node_modules/**", "**/dist/**"],
|
|
327
328
|
translate: config.translate ?? {},
|
|
328
329
|
render: config.render ?? {}
|
|
@@ -378,6 +379,38 @@ function definedOnly(config) {
|
|
|
378
379
|
return Object.fromEntries(Object.entries(config).filter(([, value]) => value !== void 0));
|
|
379
380
|
}
|
|
380
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
|
|
381
414
|
//#region src/key.ts
|
|
382
415
|
function stableKey(message) {
|
|
383
416
|
return createHash("sha256").update(message).digest("base64url").slice(0, 8);
|
|
@@ -391,7 +424,9 @@ const SKIP_KEYS = /* @__PURE__ */ new Set([
|
|
|
391
424
|
"innerComments",
|
|
392
425
|
"extra"
|
|
393
426
|
]);
|
|
394
|
-
|
|
427
|
+
const DEFAULT_T_NAMES = ["t"];
|
|
428
|
+
function analyze(code, file, options = {}) {
|
|
429
|
+
const names = new Set(options.tNames ?? DEFAULT_T_NAMES);
|
|
395
430
|
const ast = parse$1(code, {
|
|
396
431
|
sourceType: "module",
|
|
397
432
|
errorRecovery: true,
|
|
@@ -402,10 +437,10 @@ function analyze(code, file) {
|
|
|
402
437
|
walk(ast.program, (node) => {
|
|
403
438
|
if (node.type === "TaggedTemplateExpression") {
|
|
404
439
|
const tag = node.tag;
|
|
405
|
-
const explicit = explicitId(tag);
|
|
406
|
-
if (!explicit && !isTReference(tag)) return;
|
|
440
|
+
const explicit = explicitId(tag, names);
|
|
441
|
+
if (!explicit && !isTReference(tag, names)) return;
|
|
407
442
|
const quasi = node.quasi;
|
|
408
|
-
const message = buildMessage(code, quasi);
|
|
443
|
+
const message = buildMessage(code, quasi, names);
|
|
409
444
|
if (!message) return;
|
|
410
445
|
tagged.push({
|
|
411
446
|
key: explicit ? explicit.key : stableKey(message.text),
|
|
@@ -419,20 +454,20 @@ function analyze(code, file) {
|
|
|
419
454
|
});
|
|
420
455
|
} else if (node.type === "CallExpression") {
|
|
421
456
|
const callee = node.callee;
|
|
422
|
-
if (!isTReference(callee)) return;
|
|
457
|
+
if (!isTReference(callee, names)) return;
|
|
423
458
|
const first = node.arguments[0];
|
|
424
459
|
if (first?.type === "StringLiteral") usedKeys.push({
|
|
425
460
|
key: first.value,
|
|
426
461
|
file
|
|
427
462
|
});
|
|
428
|
-
} else if (node.type === "JSXElement") handleTrans(code, node, file, tagged, usedKeys);
|
|
463
|
+
} else if (node.type === "JSXElement") handleTrans(code, node, file, tagged, usedKeys, names);
|
|
429
464
|
});
|
|
430
465
|
return {
|
|
431
466
|
tagged,
|
|
432
467
|
usedKeys
|
|
433
468
|
};
|
|
434
469
|
}
|
|
435
|
-
function handleTrans(code, node, file, tagged, usedKeys) {
|
|
470
|
+
function handleTrans(code, node, file, tagged, usedKeys, names) {
|
|
436
471
|
const opening = node.openingElement;
|
|
437
472
|
const nameNode = opening.name;
|
|
438
473
|
if (nameNode.type !== "JSXIdentifier" || nameNode.name !== "Trans") return;
|
|
@@ -444,7 +479,7 @@ function handleTrans(code, node, file, tagged, usedKeys) {
|
|
|
444
479
|
if (value?.type !== "StringLiteral") return;
|
|
445
480
|
const id = value.value;
|
|
446
481
|
if (attrs.length === 1 && children?.length) {
|
|
447
|
-
const built = buildTransMessage(code, children);
|
|
482
|
+
const built = buildTransMessage(code, children, names);
|
|
448
483
|
if (built?.text.trim()) {
|
|
449
484
|
tagged.push({
|
|
450
485
|
key: id,
|
|
@@ -471,7 +506,7 @@ function handleTrans(code, node, file, tagged, usedKeys) {
|
|
|
471
506
|
}
|
|
472
507
|
if (attrs.length > 0) return;
|
|
473
508
|
if (!children?.length) return;
|
|
474
|
-
const built = buildTransMessage(code, children);
|
|
509
|
+
const built = buildTransMessage(code, children, names);
|
|
475
510
|
if (!built || !built.text.trim()) return;
|
|
476
511
|
tagged.push({
|
|
477
512
|
key: stableKey(built.text),
|
|
@@ -488,14 +523,14 @@ function handleTrans(code, node, file, tagged, usedKeys) {
|
|
|
488
523
|
}
|
|
489
524
|
});
|
|
490
525
|
}
|
|
491
|
-
function explicitId(tag) {
|
|
526
|
+
function explicitId(tag, names) {
|
|
492
527
|
if (tag.type !== "CallExpression") return void 0;
|
|
493
528
|
const callee = tag.callee;
|
|
494
529
|
if (callee.type !== "MemberExpression" || callee.computed) return void 0;
|
|
495
530
|
const prop = callee.property;
|
|
496
531
|
if (prop.type !== "Identifier" || prop.name !== "id") return void 0;
|
|
497
532
|
const obj = callee.object;
|
|
498
|
-
if (!isTReference(obj)) return void 0;
|
|
533
|
+
if (!isTReference(obj, names)) return void 0;
|
|
499
534
|
const args = tag.arguments;
|
|
500
535
|
const first = args[0];
|
|
501
536
|
if (args.length !== 1 || first?.type !== "StringLiteral") return void 0;
|
|
@@ -505,7 +540,7 @@ function explicitId(tag) {
|
|
|
505
540
|
refEnd: obj.end
|
|
506
541
|
};
|
|
507
542
|
}
|
|
508
|
-
function buildTransMessage(code, children) {
|
|
543
|
+
function buildTransMessage(code, children, names) {
|
|
509
544
|
const params = [];
|
|
510
545
|
const components = [];
|
|
511
546
|
const takenParams = /* @__PURE__ */ new Map();
|
|
@@ -520,7 +555,7 @@ function buildTransMessage(code, children) {
|
|
|
520
555
|
text += escapeText(expr.value);
|
|
521
556
|
continue;
|
|
522
557
|
}
|
|
523
|
-
if (containsTaggedT(expr)) return void 0;
|
|
558
|
+
if (containsTaggedT(expr, names)) return void 0;
|
|
524
559
|
const source = code.slice(expr.start, expr.end);
|
|
525
560
|
const name = uniqueName(deriveName(expr, params.length), source, takenParams);
|
|
526
561
|
params.push({
|
|
@@ -573,24 +608,24 @@ function selfClosedSource(code, opening) {
|
|
|
573
608
|
const src = code.slice(opening.start, opening.end);
|
|
574
609
|
return src.endsWith("/>") ? src : `${src.slice(0, -1).trimEnd()} />`;
|
|
575
610
|
}
|
|
576
|
-
function containsTaggedT(node) {
|
|
611
|
+
function containsTaggedT(node, names) {
|
|
577
612
|
let found = false;
|
|
578
613
|
walk(node, (n) => {
|
|
579
614
|
if (n.type !== "TaggedTemplateExpression") return;
|
|
580
615
|
const tag = n.tag;
|
|
581
|
-
if (isTReference(tag) || explicitId(tag)) found = true;
|
|
616
|
+
if (isTReference(tag, names) || explicitId(tag, names)) found = true;
|
|
582
617
|
});
|
|
583
618
|
return found;
|
|
584
619
|
}
|
|
585
|
-
function isTReference(node) {
|
|
586
|
-
if (node.type === "Identifier") return node.name
|
|
620
|
+
function isTReference(node, names) {
|
|
621
|
+
if (node.type === "Identifier") return names.has(node.name);
|
|
587
622
|
if (node.type === "MemberExpression" && !node.computed) {
|
|
588
623
|
const prop = node.property;
|
|
589
|
-
return prop.type === "Identifier" && prop.name
|
|
624
|
+
return prop.type === "Identifier" && names.has(prop.name);
|
|
590
625
|
}
|
|
591
626
|
return false;
|
|
592
627
|
}
|
|
593
|
-
function buildMessage(code, quasi) {
|
|
628
|
+
function buildMessage(code, quasi, names) {
|
|
594
629
|
const quasis = quasi.quasis;
|
|
595
630
|
const expressions = quasi.expressions;
|
|
596
631
|
const params = [];
|
|
@@ -599,7 +634,7 @@ function buildMessage(code, quasi) {
|
|
|
599
634
|
for (let i = 0; i < expressions.length; i++) {
|
|
600
635
|
const expr = expressions[i];
|
|
601
636
|
if (!expr) return void 0;
|
|
602
|
-
if (containsTaggedT(expr)) return void 0;
|
|
637
|
+
if (containsTaggedT(expr, names)) return void 0;
|
|
603
638
|
const source = code.slice(expr.start, expr.end);
|
|
604
639
|
const name = uniqueName(deriveName(expr, i), source, taken);
|
|
605
640
|
params.push({
|
|
@@ -653,37 +688,110 @@ function isNode(value) {
|
|
|
653
688
|
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
654
689
|
}
|
|
655
690
|
//#endregion
|
|
656
|
-
//#region src/
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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);
|
|
661
710
|
}
|
|
662
|
-
|
|
663
|
-
|
|
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;
|
|
664
721
|
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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);
|
|
676
742
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
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();
|
|
683
782
|
}
|
|
684
|
-
|
|
783
|
+
i += 1;
|
|
784
|
+
if (stack.length === 0) return i;
|
|
685
785
|
}
|
|
686
|
-
}
|
|
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
|
+
}
|
|
687
795
|
//#endregion
|
|
688
796
|
//#region src/extract.ts
|
|
689
797
|
async function extractProject(cfg) {
|
|
@@ -693,7 +801,7 @@ async function extractProject(cfg) {
|
|
|
693
801
|
absolute: true
|
|
694
802
|
});
|
|
695
803
|
const registry = new MessageRegistry();
|
|
696
|
-
for (const file of files) registry.update(file,
|
|
804
|
+
for (const file of files) registry.update(file, analyzeFile(readFileSync(file, "utf8"), file));
|
|
697
805
|
return registry;
|
|
698
806
|
}
|
|
699
807
|
function syncCatalogs(cfg, catalogs, registry) {
|