@dialexjs/cli 0.7.2 → 0.9.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.
- package/README.md +20 -1
- package/dist/api.d.mts +40 -2
- package/dist/api.d.mts.map +1 -1
- package/dist/api.mjs +2 -2
- package/dist/index.mjs +335 -31
- package/dist/index.mjs.map +1 -1
- package/dist/{project-Dz0FbydQ.mjs → project-B27699T2.mjs} +436 -159
- package/dist/project-B27699T2.mjs.map +1 -0
- package/package.json +2 -2
- package/dist/project-Dz0FbydQ.mjs.map +0 -1
|
@@ -10,60 +10,66 @@ import { createHash } from "node:crypto";
|
|
|
10
10
|
import { DEFAULT_CONFIG } from "dialexjs";
|
|
11
11
|
import { loadConfigSync } from "unconfig";
|
|
12
12
|
import chokidar from "chokidar";
|
|
13
|
-
//#region src/utils/
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
function unwrap(node) {
|
|
13
|
+
//#region src/utils/ast.ts
|
|
14
|
+
/** Strips `as`, `satisfies` and parentheses around an expression. */
|
|
15
|
+
function unwrapNode(node) {
|
|
17
16
|
while (node && (node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression" || node.type === "ParenthesizedExpression")) node = node.expression;
|
|
18
17
|
return node;
|
|
19
18
|
}
|
|
20
|
-
const
|
|
21
|
-
|
|
19
|
+
const isObjectNode = (node) => node?.type === "ObjectExpression";
|
|
20
|
+
/** The static name of a property key, or `undefined` when it is computed. */
|
|
21
|
+
function propertyKeyName(prop) {
|
|
22
22
|
const key = prop?.key;
|
|
23
23
|
if (!key || prop.computed) return void 0;
|
|
24
24
|
if (key.type === "Identifier") return key.name;
|
|
25
25
|
if (key.type === "StringLiteral" || key.type === "Literal" && typeof key.value === "string") return key.value;
|
|
26
26
|
if (key.type === "NumericLiteral") return String(key.value);
|
|
27
27
|
}
|
|
28
|
-
const isPropLike = (p) => p?.type === "ObjectProperty" || p?.type === "Property" || p?.type === "ObjectMethod";
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
const isPropLike$1 = (p) => p?.type === "ObjectProperty" || p?.type === "Property" || p?.type === "ObjectMethod";
|
|
29
|
+
/** The first property of `obj` with the static name `key`. */
|
|
30
|
+
function findProperty(obj, key) {
|
|
31
|
+
return obj.properties.find((p) => isPropLike$1(p) && propertyKeyName(p) === key);
|
|
31
32
|
}
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/utils/dictionary-edit.ts
|
|
35
|
+
const TODO_PREFIX = "[TODO] ";
|
|
36
|
+
const IDENTIFIER$1 = /^[A-Za-z_$][\w$]*$/;
|
|
37
|
+
const isPropLike = (p) => p?.type === "ObjectProperty" || p?.type === "Property" || p?.type === "ObjectMethod";
|
|
32
38
|
function stringValue(node) {
|
|
33
|
-
node =
|
|
39
|
+
node = unwrapNode(node);
|
|
34
40
|
if (node?.type === "StringLiteral") return node.value;
|
|
35
41
|
if (node?.type === "Literal" && typeof node.value === "string") return node.value;
|
|
36
42
|
if (node?.type === "TemplateLiteral" && node.expressions.length === 0) return node.quasis[0]?.value?.cooked ?? void 0;
|
|
37
43
|
}
|
|
38
44
|
function leafKind(node) {
|
|
39
|
-
node =
|
|
45
|
+
node = unwrapNode(node);
|
|
40
46
|
if (stringValue(node) !== void 0) return "string";
|
|
41
47
|
if (node?.type === "ArrowFunctionExpression" || node?.type === "FunctionExpression" || node?.type === "ObjectMethod") return "function";
|
|
42
48
|
return "other";
|
|
43
49
|
}
|
|
44
50
|
function dictionaryProp(obj) {
|
|
45
|
-
const value =
|
|
46
|
-
return
|
|
51
|
+
const value = unwrapNode(findProperty(obj, "dictionary")?.value);
|
|
52
|
+
return isObjectNode(value) ? value : void 0;
|
|
47
53
|
}
|
|
48
54
|
function locateName(declaration) {
|
|
49
|
-
const decl =
|
|
55
|
+
const decl = unwrapNode(declaration);
|
|
50
56
|
if (decl?.type === "CallExpression") {
|
|
51
|
-
const first =
|
|
52
|
-
if (
|
|
57
|
+
const first = unwrapNode(decl.arguments[0]);
|
|
58
|
+
if (isObjectNode(first)) return stringValue(findProperty(first, "name")?.value);
|
|
53
59
|
return stringValue(first);
|
|
54
60
|
}
|
|
55
|
-
if (
|
|
61
|
+
if (isObjectNode(decl)) return stringValue(findProperty(decl, "name")?.value);
|
|
56
62
|
}
|
|
57
63
|
function locateLocalesObject(declaration) {
|
|
58
|
-
const decl =
|
|
64
|
+
const decl = unwrapNode(declaration);
|
|
59
65
|
if (decl?.type === "CallExpression") {
|
|
60
|
-
const first =
|
|
61
|
-
const second =
|
|
62
|
-
if (
|
|
63
|
-
if (
|
|
66
|
+
const first = unwrapNode(decl.arguments[0]);
|
|
67
|
+
const second = unwrapNode(decl.arguments[1]);
|
|
68
|
+
if (isObjectNode(second)) return second;
|
|
69
|
+
if (isObjectNode(first)) return dictionaryProp(first);
|
|
64
70
|
return;
|
|
65
71
|
}
|
|
66
|
-
if (
|
|
72
|
+
if (isObjectNode(decl)) return dictionaryProp(decl);
|
|
67
73
|
}
|
|
68
74
|
/** Quotes a property key only when it is not a valid identifier. */
|
|
69
75
|
function keyCode(key) {
|
|
@@ -82,15 +88,15 @@ function makeStringNode(value) {
|
|
|
82
88
|
*/
|
|
83
89
|
function isStaticallyComplete(localesObj) {
|
|
84
90
|
const walk = (obj) => obj.properties.every((prop) => {
|
|
85
|
-
if (!isPropLike(prop) ||
|
|
91
|
+
if (!isPropLike(prop) || propertyKeyName(prop) === void 0) return false;
|
|
86
92
|
if (prop.type === "ObjectMethod") return true;
|
|
87
|
-
const value =
|
|
88
|
-
return
|
|
93
|
+
const value = unwrapNode(prop.value);
|
|
94
|
+
return isObjectNode(value) ? walk(value) : true;
|
|
89
95
|
});
|
|
90
96
|
return localesObj.properties.every((prop) => {
|
|
91
|
-
if (!isPropLike(prop) ||
|
|
92
|
-
const value =
|
|
93
|
-
return
|
|
97
|
+
if (!isPropLike(prop) || propertyKeyName(prop) === void 0 || prop.type === "ObjectMethod") return false;
|
|
98
|
+
const value = unwrapNode(prop.value);
|
|
99
|
+
return isObjectNode(value) && walk(value);
|
|
94
100
|
});
|
|
95
101
|
}
|
|
96
102
|
/**
|
|
@@ -129,11 +135,11 @@ async function saveDictionaryFile(df) {
|
|
|
129
135
|
}
|
|
130
136
|
}
|
|
131
137
|
function listLocales(df) {
|
|
132
|
-
return df.localesObj.properties.filter(isPropLike).map(
|
|
138
|
+
return df.localesObj.properties.filter(isPropLike).map(propertyKeyName).filter((k) => k !== void 0);
|
|
133
139
|
}
|
|
134
140
|
function localeObject(df, locale) {
|
|
135
|
-
const value =
|
|
136
|
-
return
|
|
141
|
+
const value = unwrapNode(findProperty(df.localesObj, locale)?.value);
|
|
142
|
+
return isObjectNode(value) ? value : void 0;
|
|
137
143
|
}
|
|
138
144
|
/** Lists every leaf (string, function, or other value) of a locale, in source order. */
|
|
139
145
|
function listLeaves(df, locale) {
|
|
@@ -143,11 +149,11 @@ function listLeaves(df, locale) {
|
|
|
143
149
|
const walk = (obj, prefix) => {
|
|
144
150
|
for (const prop of obj.properties) {
|
|
145
151
|
if (!isPropLike(prop)) continue;
|
|
146
|
-
const name =
|
|
152
|
+
const name = propertyKeyName(prop);
|
|
147
153
|
if (name === void 0) continue;
|
|
148
154
|
const path = [...prefix, name];
|
|
149
|
-
const value = prop.type === "ObjectMethod" ? prop :
|
|
150
|
-
if (
|
|
155
|
+
const value = prop.type === "ObjectMethod" ? prop : unwrapNode(prop.value);
|
|
156
|
+
if (isObjectNode(value)) walk(value, path);
|
|
151
157
|
else {
|
|
152
158
|
const kind = leafKind(value);
|
|
153
159
|
leaves.push({
|
|
@@ -164,11 +170,11 @@ function listLeaves(df, locale) {
|
|
|
164
170
|
function getProp(df, locale, path) {
|
|
165
171
|
let obj = localeObject(df, locale);
|
|
166
172
|
for (let i = 0; obj && i < path.length; i++) {
|
|
167
|
-
const prop =
|
|
173
|
+
const prop = findProperty(obj, path[i]);
|
|
168
174
|
if (!prop) return void 0;
|
|
169
175
|
if (i === path.length - 1) return prop;
|
|
170
|
-
const next =
|
|
171
|
-
obj =
|
|
176
|
+
const next = unwrapNode(prop.value);
|
|
177
|
+
obj = isObjectNode(next) ? next : void 0;
|
|
172
178
|
}
|
|
173
179
|
}
|
|
174
180
|
function hasPath(df, locale, path) {
|
|
@@ -191,13 +197,13 @@ function ensureLocale(df, locale) {
|
|
|
191
197
|
function ensureParent(df, locale, path) {
|
|
192
198
|
let obj = ensureLocale(df, locale);
|
|
193
199
|
for (const segment of path.slice(0, -1)) {
|
|
194
|
-
let prop =
|
|
200
|
+
let prop = findProperty(obj, segment);
|
|
195
201
|
if (!prop) {
|
|
196
202
|
prop = makeProp(segment, "{}");
|
|
197
203
|
obj.properties.push(prop);
|
|
198
204
|
}
|
|
199
|
-
const next =
|
|
200
|
-
if (!
|
|
205
|
+
const next = unwrapNode(prop.value);
|
|
206
|
+
if (!isObjectNode(next)) return void 0;
|
|
201
207
|
obj = next;
|
|
202
208
|
}
|
|
203
209
|
return obj;
|
|
@@ -210,7 +216,7 @@ function setString(df, locale, path, value) {
|
|
|
210
216
|
const parent = ensureParent(df, locale, path);
|
|
211
217
|
if (!parent) return "skipped";
|
|
212
218
|
const last = path[path.length - 1];
|
|
213
|
-
const existing =
|
|
219
|
+
const existing = findProperty(parent, last);
|
|
214
220
|
if (existing) {
|
|
215
221
|
if (existing.type === "ObjectMethod" || leafKind(existing.value) !== "string") return "skipped";
|
|
216
222
|
existing.value = makeStringNode(value);
|
|
@@ -259,7 +265,7 @@ const toRange = (loc) => loc ? {
|
|
|
259
265
|
* it is a good place to attach a diagnostic or jump to.
|
|
260
266
|
*/
|
|
261
267
|
function locationOf(df, locale, path = []) {
|
|
262
|
-
const prop = path.length === 0 ?
|
|
268
|
+
const prop = path.length === 0 ? findProperty(df.localesObj, locale) : getProp(df, locale, path);
|
|
263
269
|
return toRange((prop?.key ?? prop)?.loc);
|
|
264
270
|
}
|
|
265
271
|
/** The source text of the dictionary object itself (the object whose keys are locales). */
|
|
@@ -518,6 +524,10 @@ function lockFor(root, sourceLocale) {
|
|
|
518
524
|
}
|
|
519
525
|
//#endregion
|
|
520
526
|
//#region src/analysis.ts
|
|
527
|
+
function lintLevel(setting) {
|
|
528
|
+
const level = Array.isArray(setting) ? setting[0] : setting;
|
|
529
|
+
return level === "off" || level === "warn" || level === "error" ? level : void 0;
|
|
530
|
+
}
|
|
521
531
|
function createAnalysisCache() {
|
|
522
532
|
return {
|
|
523
533
|
dictionaries: /* @__PURE__ */ new Map(),
|
|
@@ -534,12 +544,115 @@ const DEFAULT_SOURCE_GLOB = "**/*.{ts,tsx,js,jsx,mjs,cjs,vue,svelte,astro,mdx}";
|
|
|
534
544
|
const DICTIONARY_CALL = /\b(?:getDictionary|useDictionary|DialexDictionary)\(\s*(["'`])([^"'`$\n]+)\1/dg;
|
|
535
545
|
/** `t("dictionary.path.to.key")` */
|
|
536
546
|
const T_CALL = /(?<![\w$.])t\(\s*(["'`])([\w-]+(?:\.[\w-]+)+)\1/dg;
|
|
537
|
-
/**
|
|
538
|
-
|
|
547
|
+
/**
|
|
548
|
+
* Offsets of `//`, `/* *\/` and `<!-- -->` comments. Quoted strings are skipped so `"http://x"`
|
|
549
|
+
* is not a comment; `'` and `"` end at a newline, template literals do not.
|
|
550
|
+
*/
|
|
551
|
+
function commentRanges(text) {
|
|
552
|
+
const ranges = [];
|
|
553
|
+
const length = text.length;
|
|
554
|
+
let i = 0;
|
|
555
|
+
while (i < length) {
|
|
556
|
+
const c = text[i];
|
|
557
|
+
if (c === "\"" || c === "'" || c === "`") {
|
|
558
|
+
i++;
|
|
559
|
+
while (i < length && text[i] !== c && (c === "`" || text[i] !== "\n")) {
|
|
560
|
+
if (text[i] === "\\") i++;
|
|
561
|
+
i++;
|
|
562
|
+
}
|
|
563
|
+
i++;
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
let close;
|
|
567
|
+
if (c === "/" && text[i + 1] === "/") close = "\n";
|
|
568
|
+
else if (c === "/" && text[i + 1] === "*") close = "*/";
|
|
569
|
+
else if (c === "<" && text.startsWith("<!--", i)) close = "-->";
|
|
570
|
+
if (close === void 0) {
|
|
571
|
+
i++;
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
const found = text.indexOf(close, i + 2);
|
|
575
|
+
const end = found === -1 ? length : close === "\n" ? found : found + close.length;
|
|
576
|
+
ranges.push([i, end]);
|
|
577
|
+
i = end;
|
|
578
|
+
}
|
|
579
|
+
return ranges;
|
|
580
|
+
}
|
|
581
|
+
const IGNORE_DIRECTIVE = /dialex-ignore(-next-line)?(?![\w-])/;
|
|
582
|
+
/** `dictionaries: { name: ..., "other-name": ..., shorthand }`, up to the first key. */
|
|
583
|
+
const DICTIONARIES_OBJECT = /\bdictionaries\s*:\s*\{/g;
|
|
584
|
+
const OBJECT_KEY = /\s*(?:([A-Za-z_$][\w$-]*)|(["'])([^"'\n]+)\2)\s*(?=[:,}])/y;
|
|
585
|
+
/** Replaces comments with spaces of the same length, keeping offsets and line breaks. */
|
|
586
|
+
function maskComments(text, comments) {
|
|
587
|
+
if (comments.length === 0) return text;
|
|
588
|
+
let out = "";
|
|
589
|
+
let at = 0;
|
|
590
|
+
for (const [from, to] of comments) {
|
|
591
|
+
out += text.slice(at, from) + text.slice(from, to).replace(/[^\n]/g, " ");
|
|
592
|
+
at = to;
|
|
593
|
+
}
|
|
594
|
+
return out + text.slice(at);
|
|
595
|
+
}
|
|
596
|
+
/** Names that an object literal `dictionaries: { ... }` registers inline. */
|
|
597
|
+
function inlineDictionaryNames(masked) {
|
|
598
|
+
const names = /* @__PURE__ */ new Set();
|
|
599
|
+
for (const match of masked.matchAll(DICTIONARIES_OBJECT)) {
|
|
600
|
+
let i = match.index + match[0].length;
|
|
601
|
+
let depth = 1;
|
|
602
|
+
let expectKey = true;
|
|
603
|
+
while (i < masked.length && depth > 0) {
|
|
604
|
+
if (expectKey && depth === 1) {
|
|
605
|
+
OBJECT_KEY.lastIndex = i;
|
|
606
|
+
const key = OBJECT_KEY.exec(masked);
|
|
607
|
+
if (key) {
|
|
608
|
+
names.add(key[1] ?? key[3]);
|
|
609
|
+
i = OBJECT_KEY.lastIndex;
|
|
610
|
+
}
|
|
611
|
+
expectKey = false;
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
const c = masked[i];
|
|
615
|
+
if (c === "\"" || c === "'" || c === "`") {
|
|
616
|
+
i++;
|
|
617
|
+
while (i < masked.length && masked[i] !== c && (c === "`" || masked[i] !== "\n")) {
|
|
618
|
+
if (masked[i] === "\\") i++;
|
|
619
|
+
i++;
|
|
620
|
+
}
|
|
621
|
+
} else if (c === "{" || c === "[" || c === "(") depth++;
|
|
622
|
+
else if (c === "}" || c === "]" || c === ")") depth--;
|
|
623
|
+
else if (c === "," && depth === 1) expectKey = true;
|
|
624
|
+
i++;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return [...names];
|
|
628
|
+
}
|
|
629
|
+
/** Lines that a `dialex-ignore` / `dialex-ignore-next-line` comment excludes from checking. */
|
|
630
|
+
function ignoredLines(text, comments) {
|
|
631
|
+
const lines = /* @__PURE__ */ new Set();
|
|
632
|
+
if (!text.includes("dialex-ignore")) return lines;
|
|
633
|
+
const starts = lineStarts(text);
|
|
634
|
+
for (const [from, to] of comments) {
|
|
635
|
+
const directive = IGNORE_DIRECTIVE.exec(text.slice(from, to));
|
|
636
|
+
if (!directive) continue;
|
|
637
|
+
const first = positionAt(starts, from).line;
|
|
638
|
+
const last = positionAt(starts, Math.max(from, to - 1)).line;
|
|
639
|
+
if (directive[1]) lines.add(last + 1);
|
|
640
|
+
else for (let line = first; line <= last; line++) lines.add(line);
|
|
641
|
+
}
|
|
642
|
+
return lines;
|
|
643
|
+
}
|
|
644
|
+
/** References and inline dictionary names found in one source file. */
|
|
645
|
+
function scanText(text) {
|
|
646
|
+
const comments = commentRanges(text);
|
|
647
|
+
const ignored = ignoredLines(text, comments);
|
|
648
|
+
const starts = ignored.size > 0 ? lineStarts(text) : [];
|
|
649
|
+
const inComment = (offset) => comments.some(([from, to]) => offset >= from && offset < to);
|
|
539
650
|
const references = [];
|
|
540
651
|
const collect = (pattern, kind) => {
|
|
541
652
|
for (const match of text.matchAll(pattern)) {
|
|
542
653
|
const [start, end] = match.indices[2];
|
|
654
|
+
if (comments.length > 0 && inComment(start)) continue;
|
|
655
|
+
if (ignored.size > 0 && ignored.has(positionAt(starts, start).line)) continue;
|
|
543
656
|
references.push({
|
|
544
657
|
kind,
|
|
545
658
|
value: match[2],
|
|
@@ -550,7 +663,18 @@ function scanReferences(text) {
|
|
|
550
663
|
};
|
|
551
664
|
collect(DICTIONARY_CALL, "dictionary");
|
|
552
665
|
collect(T_CALL, "path");
|
|
553
|
-
|
|
666
|
+
references.sort((a, b) => a.start - b.start);
|
|
667
|
+
return {
|
|
668
|
+
references,
|
|
669
|
+
inline: text.includes("dictionaries") ? inlineDictionaryNames(maskComments(text, comments)) : []
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Finds `getDictionary("x")`-style and `t("x.y.z")` references in source text. References inside
|
|
674
|
+
* comments, and on lines a `dialex-ignore` comment covers, are skipped.
|
|
675
|
+
*/
|
|
676
|
+
function scanReferences(text) {
|
|
677
|
+
return scanText(text).references;
|
|
554
678
|
}
|
|
555
679
|
function lineStarts(text) {
|
|
556
680
|
const starts = [0];
|
|
@@ -640,29 +764,37 @@ async function fromRuntime(file) {
|
|
|
640
764
|
};
|
|
641
765
|
}
|
|
642
766
|
}
|
|
643
|
-
|
|
644
|
-
let astError;
|
|
767
|
+
function fromParsed(parsed) {
|
|
645
768
|
try {
|
|
646
|
-
const df =
|
|
769
|
+
const df = parsed();
|
|
647
770
|
if (df?.complete && df.name) return {
|
|
648
771
|
ok: true,
|
|
649
772
|
data: fromSyntaxTree(df)
|
|
650
773
|
};
|
|
651
774
|
} catch (error) {
|
|
652
|
-
|
|
775
|
+
return {
|
|
776
|
+
ok: false,
|
|
777
|
+
level: "error",
|
|
778
|
+
code: "invalid-dictionary",
|
|
779
|
+
message: `Failed to parse dictionary: ${error.message}`
|
|
780
|
+
};
|
|
653
781
|
}
|
|
782
|
+
}
|
|
783
|
+
const UNANALYZABLE = {
|
|
784
|
+
ok: false,
|
|
785
|
+
level: "info",
|
|
786
|
+
code: "unanalyzable",
|
|
787
|
+
message: "This dictionary uses spreads, computed keys, imported values, or a computed name, so it cannot be analyzed statically."
|
|
788
|
+
};
|
|
789
|
+
/** Reads a dictionary from its syntax tree. Never runs project code. */
|
|
790
|
+
function loadDictionaryDataStatic(file) {
|
|
791
|
+
return fromParsed(() => parseDictionaryText(file, fs.readFileSync(file, "utf-8"))) ?? UNANALYZABLE;
|
|
792
|
+
}
|
|
793
|
+
async function loadDictionaryData(file, runtime) {
|
|
794
|
+
const parsed = fromParsed(() => parseDictionaryText(file, fs.readFileSync(file, "utf-8")));
|
|
795
|
+
if (parsed?.ok) return parsed;
|
|
654
796
|
if (runtime) return fromRuntime(file);
|
|
655
|
-
return
|
|
656
|
-
ok: false,
|
|
657
|
-
level: "error",
|
|
658
|
-
code: "invalid-dictionary",
|
|
659
|
-
message: `Failed to parse dictionary: ${astError.message}`
|
|
660
|
-
} : {
|
|
661
|
-
ok: false,
|
|
662
|
-
level: "info",
|
|
663
|
-
code: "unanalyzable",
|
|
664
|
-
message: "This dictionary uses spreads, computed keys, imported values, or a computed name, so it cannot be analyzed statically."
|
|
665
|
-
};
|
|
797
|
+
return parsed ?? UNANALYZABLE;
|
|
666
798
|
}
|
|
667
799
|
function stampOf(file) {
|
|
668
800
|
try {
|
|
@@ -683,21 +815,39 @@ async function cachedDictionary(file, runtime, cache) {
|
|
|
683
815
|
});
|
|
684
816
|
return loaded;
|
|
685
817
|
}
|
|
818
|
+
function cachedDictionaryStatic(file, cache) {
|
|
819
|
+
const stamp = cache ? stampOf(file) : void 0;
|
|
820
|
+
const hit = stamp ? cache.dictionaries.get(file) : void 0;
|
|
821
|
+
if (hit && hit.stamp === stamp) return hit.loaded;
|
|
822
|
+
const loaded = loadDictionaryDataStatic(file);
|
|
823
|
+
if (cache && stamp) cache.dictionaries.set(file, {
|
|
824
|
+
stamp,
|
|
825
|
+
loaded
|
|
826
|
+
});
|
|
827
|
+
return loaded;
|
|
828
|
+
}
|
|
686
829
|
function scanSource(text) {
|
|
687
830
|
const starts = lineStarts(text);
|
|
688
831
|
const rangeOf = (start, end) => ({
|
|
689
832
|
start: positionAt(starts, start),
|
|
690
833
|
end: positionAt(starts, end)
|
|
691
834
|
});
|
|
835
|
+
const { references, inline } = scanText(text);
|
|
692
836
|
return {
|
|
693
837
|
tokens: [...new Set(text.match(/[\w$-]+/g) ?? [])],
|
|
694
|
-
refs:
|
|
838
|
+
refs: references.map((ref) => ({
|
|
695
839
|
ref,
|
|
696
840
|
range: rangeOf(ref.start, ref.end)
|
|
697
|
-
}))
|
|
841
|
+
})),
|
|
842
|
+
inline
|
|
698
843
|
};
|
|
699
844
|
}
|
|
700
|
-
|
|
845
|
+
/**
|
|
846
|
+
* The analysis itself, written once. It yields a `LoadRequest` for every dictionary so that a
|
|
847
|
+
* synchronous driver (`analyzeProjectSync`) and an asynchronous one (`analyzeProject`, which may
|
|
848
|
+
* import dictionaries at runtime) can share it.
|
|
849
|
+
*/
|
|
850
|
+
function* analysisSteps(options) {
|
|
701
851
|
const { root, config } = options;
|
|
702
852
|
const include = config.include || "**/*.content.ts";
|
|
703
853
|
const configuredLocales = config.locales || [];
|
|
@@ -718,7 +868,12 @@ async function analyzeProject(options) {
|
|
|
718
868
|
const dictionaries = [];
|
|
719
869
|
const lock = options.lock === false ? void 0 : readLock(root);
|
|
720
870
|
for (const file of files) {
|
|
721
|
-
const loaded =
|
|
871
|
+
const loaded = yield {
|
|
872
|
+
file,
|
|
873
|
+
runtime: options.runtime === true,
|
|
874
|
+
cache,
|
|
875
|
+
text: options.contents?.get(file)
|
|
876
|
+
};
|
|
722
877
|
if (!loaded.ok) {
|
|
723
878
|
for (const message of loaded.message.split("\n")) issues.push({
|
|
724
879
|
file,
|
|
@@ -831,8 +986,10 @@ async function analyzeProject(options) {
|
|
|
831
986
|
ignore: extraIgnore,
|
|
832
987
|
gitignoreFilter: gitignore
|
|
833
988
|
}).filter((f) => !dictionaryFiles.has(f));
|
|
989
|
+
const inline = /* @__PURE__ */ new Set();
|
|
834
990
|
if (sourceFiles.length > 0) {
|
|
835
991
|
const tokens = /* @__PURE__ */ new Set();
|
|
992
|
+
const scans = [];
|
|
836
993
|
for (const file of sourceFiles) {
|
|
837
994
|
const stamp = cache ? stampOf(file) : void 0;
|
|
838
995
|
let scan = stamp ? cache.sources.get(file) : void 0;
|
|
@@ -850,32 +1007,37 @@ async function analyzeProject(options) {
|
|
|
850
1007
|
if (cache && stamp) cache.sources.set(file, scan);
|
|
851
1008
|
}
|
|
852
1009
|
for (const token of scan.scan.tokens) tokens.add(token);
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
});
|
|
863
|
-
continue;
|
|
864
|
-
}
|
|
865
|
-
const [name, ...rest] = ref.value.split(".");
|
|
866
|
-
const dictionary = known.get(name);
|
|
867
|
-
if (!dictionary) continue;
|
|
868
|
-
const keyPath = rest.join(".");
|
|
869
|
-
if (!dictionary.paths.some((p) => p === keyPath || p.startsWith(`${keyPath}.`))) issues.push({
|
|
1010
|
+
scans.push({
|
|
1011
|
+
file,
|
|
1012
|
+
scan: scan.scan
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
for (const { scan } of scans) for (const name of scan.inline) inline.add(name);
|
|
1016
|
+
for (const { file, scan } of scans) for (const { ref, range } of scan.refs) {
|
|
1017
|
+
if (ref.kind === "dictionary") {
|
|
1018
|
+
if (!known.has(ref.value) && !inline.has(ref.value)) issues.push({
|
|
870
1019
|
file,
|
|
871
1020
|
level: "error",
|
|
872
|
-
code: "unknown-
|
|
873
|
-
message: `Unknown
|
|
1021
|
+
code: "unknown-dictionary",
|
|
1022
|
+
message: `Unknown dictionary "${ref.value}"`,
|
|
874
1023
|
range,
|
|
875
|
-
dictionary:
|
|
876
|
-
path: rest
|
|
1024
|
+
dictionary: ref.value
|
|
877
1025
|
});
|
|
1026
|
+
continue;
|
|
878
1027
|
}
|
|
1028
|
+
const [name, ...rest] = ref.value.split(".");
|
|
1029
|
+
const dictionary = known.get(name);
|
|
1030
|
+
if (!dictionary) continue;
|
|
1031
|
+
const keyPath = rest.join(".");
|
|
1032
|
+
if (!dictionary.paths.some((p) => p === keyPath || p.startsWith(`${keyPath}.`))) issues.push({
|
|
1033
|
+
file,
|
|
1034
|
+
level: "error",
|
|
1035
|
+
code: "unknown-path",
|
|
1036
|
+
message: `Unknown translation path "${ref.value}"`,
|
|
1037
|
+
range,
|
|
1038
|
+
dictionary: name,
|
|
1039
|
+
path: rest
|
|
1040
|
+
});
|
|
879
1041
|
}
|
|
880
1042
|
if (options.unused !== false) for (const dictionary of dictionaries) {
|
|
881
1043
|
if (!tokens.has(dictionary.name)) {
|
|
@@ -909,13 +1071,47 @@ async function analyzeProject(options) {
|
|
|
909
1071
|
const live = /* @__PURE__ */ new Set([...files, ...sourceFiles]);
|
|
910
1072
|
for (const map of [cache.dictionaries, cache.sources]) for (const key of map.keys()) if (key.startsWith(root) && !live.has(key)) map.delete(key);
|
|
911
1073
|
}
|
|
1074
|
+
const rules = config.lint?.rules;
|
|
1075
|
+
if (rules && Object.keys(rules).length > 0) for (let i = issues.length - 1; i >= 0; i--) {
|
|
1076
|
+
const level = lintLevel(rules[issues[i].code]);
|
|
1077
|
+
if (!level) continue;
|
|
1078
|
+
if (level === "off") issues.splice(i, 1);
|
|
1079
|
+
else issues[i].level = level === "error" ? "error" : "warning";
|
|
1080
|
+
}
|
|
912
1081
|
return {
|
|
913
1082
|
issues,
|
|
914
1083
|
dictionaries,
|
|
915
1084
|
files,
|
|
916
|
-
sourceFilesScanned: sourceFiles.length
|
|
1085
|
+
sourceFilesScanned: sourceFiles.length,
|
|
1086
|
+
inlineDictionaries: [...inline]
|
|
917
1087
|
};
|
|
918
1088
|
}
|
|
1089
|
+
/** Analyzes a project. With `runtime: true`, dictionaries the syntax tree cannot describe are imported. */
|
|
1090
|
+
async function analyzeProject(options) {
|
|
1091
|
+
const steps = analysisSteps(options);
|
|
1092
|
+
let step = steps.next();
|
|
1093
|
+
while (!step.done) {
|
|
1094
|
+
const { file, runtime, cache, text } = step.value;
|
|
1095
|
+
step = steps.next(text === void 0 ? await cachedDictionary(file, runtime, cache) : fromParsed(() => parseDictionaryText(file, text)) ?? UNANALYZABLE);
|
|
1096
|
+
}
|
|
1097
|
+
return step.value;
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* `analyzeProject` for callers that cannot await, such as lint rules. Reads dictionaries from the
|
|
1101
|
+
* syntax tree only: `runtime` is ignored, so project code is never executed.
|
|
1102
|
+
*/
|
|
1103
|
+
function analyzeProjectSync(options) {
|
|
1104
|
+
const steps = analysisSteps({
|
|
1105
|
+
...options,
|
|
1106
|
+
runtime: false
|
|
1107
|
+
});
|
|
1108
|
+
let step = steps.next();
|
|
1109
|
+
while (!step.done) {
|
|
1110
|
+
const { file, cache, text } = step.value;
|
|
1111
|
+
step = steps.next(text === void 0 ? cachedDictionaryStatic(file, cache) : fromParsed(() => parseDictionaryText(file, text)) ?? UNANALYZABLE);
|
|
1112
|
+
}
|
|
1113
|
+
return step.value;
|
|
1114
|
+
}
|
|
919
1115
|
/** Project root-relative path with forward slashes, for display. */
|
|
920
1116
|
function relativeFile(root, file) {
|
|
921
1117
|
return path.relative(root, file);
|
|
@@ -930,44 +1126,48 @@ const logger = {
|
|
|
930
1126
|
log: (msg) => console.log(msg)
|
|
931
1127
|
};
|
|
932
1128
|
//#endregion
|
|
1129
|
+
//#region src/utils/load-config.ts
|
|
1130
|
+
/**
|
|
1131
|
+
* Runs the first config file that exists (`dialex.config.*`, then `i18n.config.*`, or
|
|
1132
|
+
* `configFile`) with `unconfig` and returns what it exports. **This executes project code.**
|
|
1133
|
+
* Returns `undefined` when the project has no config file; throws what the file throws.
|
|
1134
|
+
*/
|
|
1135
|
+
function loadConfigObject(root, configFile) {
|
|
1136
|
+
const sources = [];
|
|
1137
|
+
if (configFile) sources.push({ files: configFile });
|
|
1138
|
+
for (const name of ["dialex.config", "i18n.config"]) sources.push({
|
|
1139
|
+
files: name,
|
|
1140
|
+
extensions: [
|
|
1141
|
+
"ts",
|
|
1142
|
+
"mts",
|
|
1143
|
+
"cts",
|
|
1144
|
+
"js",
|
|
1145
|
+
"mjs",
|
|
1146
|
+
"cjs",
|
|
1147
|
+
"json",
|
|
1148
|
+
""
|
|
1149
|
+
]
|
|
1150
|
+
});
|
|
1151
|
+
const result = loadConfigSync({
|
|
1152
|
+
sources,
|
|
1153
|
+
merge: false,
|
|
1154
|
+
defaults: {},
|
|
1155
|
+
cwd: root
|
|
1156
|
+
});
|
|
1157
|
+
const file = result?.sources?.[0];
|
|
1158
|
+
if (!file) return void 0;
|
|
1159
|
+
return {
|
|
1160
|
+
config: result.config && typeof result.config === "object" ? result.config : {},
|
|
1161
|
+
file
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
//#endregion
|
|
933
1165
|
//#region src/utils/scanner.ts
|
|
934
1166
|
function resolveDialexConfig(root, inlineConfig = {}) {
|
|
935
1167
|
let loadedConfig = {};
|
|
936
1168
|
try {
|
|
937
|
-
const
|
|
938
|
-
if (
|
|
939
|
-
sources.push({
|
|
940
|
-
files: "dialex.config",
|
|
941
|
-
extensions: [
|
|
942
|
-
"ts",
|
|
943
|
-
"mts",
|
|
944
|
-
"cts",
|
|
945
|
-
"js",
|
|
946
|
-
"mjs",
|
|
947
|
-
"cjs",
|
|
948
|
-
"json",
|
|
949
|
-
""
|
|
950
|
-
]
|
|
951
|
-
}, {
|
|
952
|
-
files: "i18n.config",
|
|
953
|
-
extensions: [
|
|
954
|
-
"ts",
|
|
955
|
-
"mts",
|
|
956
|
-
"cts",
|
|
957
|
-
"js",
|
|
958
|
-
"mjs",
|
|
959
|
-
"cjs",
|
|
960
|
-
"json",
|
|
961
|
-
""
|
|
962
|
-
]
|
|
963
|
-
});
|
|
964
|
-
const result = loadConfigSync({
|
|
965
|
-
sources,
|
|
966
|
-
merge: false,
|
|
967
|
-
defaults: {},
|
|
968
|
-
cwd: root
|
|
969
|
-
});
|
|
970
|
-
if (result && result.config) loadedConfig = result.config;
|
|
1169
|
+
const loaded = loadConfigObject(root, inlineConfig.configFile);
|
|
1170
|
+
if (loaded) loadedConfig = loaded.config;
|
|
971
1171
|
} catch {}
|
|
972
1172
|
return {
|
|
973
1173
|
...DEFAULT_CONFIG,
|
|
@@ -990,7 +1190,15 @@ const REGISTRY_ADAPTERS = [
|
|
|
990
1190
|
"vue",
|
|
991
1191
|
"web",
|
|
992
1192
|
"h3",
|
|
993
|
-
"angular"
|
|
1193
|
+
"angular",
|
|
1194
|
+
"svelte",
|
|
1195
|
+
"solid",
|
|
1196
|
+
"preact",
|
|
1197
|
+
"qwik",
|
|
1198
|
+
"lit",
|
|
1199
|
+
"hapi",
|
|
1200
|
+
"adonis",
|
|
1201
|
+
"trpc"
|
|
994
1202
|
];
|
|
995
1203
|
/** Dependencies that tell which `dialexjs/*` entry points a project can import. */
|
|
996
1204
|
const ADAPTER_DEPENDENCIES = {
|
|
@@ -1011,6 +1219,17 @@ const ADAPTER_DEPENDENCIES = {
|
|
|
1011
1219
|
h3: ["h3"],
|
|
1012
1220
|
nitropack: ["h3"],
|
|
1013
1221
|
"@angular/core": ["angular"],
|
|
1222
|
+
svelte: ["svelte"],
|
|
1223
|
+
"solid-js": ["solid"],
|
|
1224
|
+
preact: ["preact"],
|
|
1225
|
+
"@builder.io/qwik": ["qwik"],
|
|
1226
|
+
lit: ["lit"],
|
|
1227
|
+
"@hapi/hapi": ["hapi"],
|
|
1228
|
+
"@adonisjs/core": ["adonis"],
|
|
1229
|
+
"@adonisjs/http-server": ["adonis"],
|
|
1230
|
+
"@trpc/server": ["trpc"],
|
|
1231
|
+
"react-native": ["react"],
|
|
1232
|
+
expo: ["react"],
|
|
1014
1233
|
"react-router": ["web"],
|
|
1015
1234
|
"react-router-dom": ["web"],
|
|
1016
1235
|
"@remix-run/react": ["web"],
|
|
@@ -1041,9 +1260,9 @@ function detectAdapters(root) {
|
|
|
1041
1260
|
* The `dialex-env.d.ts` a project should have: its path and contents, or `undefined` when there
|
|
1042
1261
|
* are no dictionary files. Nothing is written.
|
|
1043
1262
|
*/
|
|
1044
|
-
function renderDts(root, files, locales, messageArguments = {}) {
|
|
1263
|
+
function renderDts(root, files, locales, messageArguments = {}, directory) {
|
|
1045
1264
|
if (files.length === 0) return void 0;
|
|
1046
|
-
const dtsDir = fs.existsSync(path.join(root, "src")) ? path.join(root, "src") : root;
|
|
1265
|
+
const dtsDir = directory ?? (fs.existsSync(path.join(root, "src")) ? path.join(root, "src") : root);
|
|
1047
1266
|
const imports = [];
|
|
1048
1267
|
const records = [];
|
|
1049
1268
|
files.forEach((file, index) => {
|
|
@@ -1226,9 +1445,9 @@ function splitLocales(df) {
|
|
|
1226
1445
|
const bound = topLevelNames(df);
|
|
1227
1446
|
const slices = [];
|
|
1228
1447
|
for (const prop of df.localesObj.properties) {
|
|
1229
|
-
const locale =
|
|
1230
|
-
const value =
|
|
1231
|
-
if (locale === void 0 || !
|
|
1448
|
+
const locale = propertyKeyName(prop);
|
|
1449
|
+
const value = unwrapNode(prop.value);
|
|
1450
|
+
if (locale === void 0 || !isObjectNode(value)) return {
|
|
1232
1451
|
ok: false,
|
|
1233
1452
|
reason: "a locale is not an object literal"
|
|
1234
1453
|
};
|
|
@@ -1253,8 +1472,10 @@ function splitLocales(df) {
|
|
|
1253
1472
|
}
|
|
1254
1473
|
//#endregion
|
|
1255
1474
|
//#region src/utils/static-config.ts
|
|
1475
|
+
/** The names a config file can have, without extension, in order of preference. */
|
|
1256
1476
|
const CONFIG_NAMES = ["dialex.config", "i18n.config"];
|
|
1257
|
-
|
|
1477
|
+
/** The extensions tried for each name, in order. */
|
|
1478
|
+
const CONFIG_EXTENSIONS = [
|
|
1258
1479
|
"ts",
|
|
1259
1480
|
"mts",
|
|
1260
1481
|
"cts",
|
|
@@ -1263,6 +1484,22 @@ const EXTENSIONS = [
|
|
|
1263
1484
|
"cjs",
|
|
1264
1485
|
"json"
|
|
1265
1486
|
];
|
|
1487
|
+
/** The keys of the config that tooling reads. */
|
|
1488
|
+
const CONFIG_KEYS = [
|
|
1489
|
+
"defaultLocale",
|
|
1490
|
+
"locales",
|
|
1491
|
+
"include",
|
|
1492
|
+
"exclude",
|
|
1493
|
+
"output",
|
|
1494
|
+
"fallbacks",
|
|
1495
|
+
"prefixDefault",
|
|
1496
|
+
"lazy",
|
|
1497
|
+
"lint"
|
|
1498
|
+
];
|
|
1499
|
+
/** The paths `readStaticConfig` and `resolveConfig` try, in order, for a project root. */
|
|
1500
|
+
function configCandidates(root, configFile) {
|
|
1501
|
+
return configFile ? [path.resolve(root, configFile)] : CONFIG_NAMES.flatMap((name) => CONFIG_EXTENSIONS.map((ext) => path.join(root, `${name}.${ext}`)));
|
|
1502
|
+
}
|
|
1266
1503
|
const DEFAULT_STATIC_CONFIG = {
|
|
1267
1504
|
defaultLocale: "en",
|
|
1268
1505
|
locales: [],
|
|
@@ -1270,11 +1507,12 @@ const DEFAULT_STATIC_CONFIG = {
|
|
|
1270
1507
|
exclude: [],
|
|
1271
1508
|
fallbacks: {},
|
|
1272
1509
|
prefixDefault: true,
|
|
1273
|
-
lazy: false
|
|
1510
|
+
lazy: false,
|
|
1511
|
+
lint: { rules: {} }
|
|
1274
1512
|
};
|
|
1275
1513
|
const NOT_LITERAL = Symbol("not literal");
|
|
1276
1514
|
function literal(node) {
|
|
1277
|
-
node =
|
|
1515
|
+
node = unwrapNode(node);
|
|
1278
1516
|
switch (node?.type) {
|
|
1279
1517
|
case "StringLiteral":
|
|
1280
1518
|
case "NumericLiteral":
|
|
@@ -1288,7 +1526,7 @@ function literal(node) {
|
|
|
1288
1526
|
case "ObjectExpression": {
|
|
1289
1527
|
const result = {};
|
|
1290
1528
|
for (const prop of node.properties) {
|
|
1291
|
-
const key =
|
|
1529
|
+
const key = propertyKeyName(prop);
|
|
1292
1530
|
const value = key === void 0 ? NOT_LITERAL : literal(prop.value);
|
|
1293
1531
|
if (value === NOT_LITERAL) return NOT_LITERAL;
|
|
1294
1532
|
result[key] = value;
|
|
@@ -1300,12 +1538,36 @@ function literal(node) {
|
|
|
1300
1538
|
}
|
|
1301
1539
|
/** The object literal passed to `defineConfig({...})`, or exported directly. */
|
|
1302
1540
|
function findConfigObject(declaration) {
|
|
1303
|
-
const node =
|
|
1541
|
+
const node = unwrapNode(declaration);
|
|
1304
1542
|
if (node?.type === "CallExpression") return findConfigObject(node.arguments[0]);
|
|
1305
|
-
return
|
|
1543
|
+
return isObjectNode(node) ? node : void 0;
|
|
1306
1544
|
}
|
|
1307
1545
|
const isStringArray = (value) => Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
1308
|
-
|
|
1546
|
+
const LINT_LEVELS = [
|
|
1547
|
+
"off",
|
|
1548
|
+
"warn",
|
|
1549
|
+
"error"
|
|
1550
|
+
];
|
|
1551
|
+
/** `lint: { rules: { code: "warn" | ["warn", {...}] } }`, or `undefined` when it is not that shape. */
|
|
1552
|
+
function lintRules(value) {
|
|
1553
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
1554
|
+
const rules = value.rules;
|
|
1555
|
+
if (rules === void 0) return {};
|
|
1556
|
+
if (typeof rules !== "object" || rules === null || Array.isArray(rules)) return void 0;
|
|
1557
|
+
const result = {};
|
|
1558
|
+
for (const [code, setting] of Object.entries(rules)) {
|
|
1559
|
+
const [level, options] = Array.isArray(setting) ? setting : [setting];
|
|
1560
|
+
if (typeof level !== "string" || !LINT_LEVELS.includes(level)) return void 0;
|
|
1561
|
+
if (options !== void 0 && (typeof options !== "object" || options === null)) return void 0;
|
|
1562
|
+
result[code] = setting;
|
|
1563
|
+
}
|
|
1564
|
+
return result;
|
|
1565
|
+
}
|
|
1566
|
+
/**
|
|
1567
|
+
* Applies the values of a config object (read from a syntax tree or from a loaded module) over
|
|
1568
|
+
* `config`, with the same checks and notes either way.
|
|
1569
|
+
*/
|
|
1570
|
+
function applyConfigValues(object, config, notes) {
|
|
1309
1571
|
const accept = (key, valid, expected) => {
|
|
1310
1572
|
if (!(key in object)) return;
|
|
1311
1573
|
const value = object[key];
|
|
@@ -1316,25 +1578,33 @@ function apply(object, config, notes) {
|
|
|
1316
1578
|
accept("locales", isStringArray, "an array of string literals");
|
|
1317
1579
|
accept("include", (v) => typeof v === "string" || isStringArray(v), "a string or array of strings");
|
|
1318
1580
|
accept("exclude", isStringArray, "an array of string literals");
|
|
1581
|
+
accept("output", (v) => typeof v === "string" && v !== "", "a string literal");
|
|
1319
1582
|
accept("fallbacks", (v) => typeof v === "object" && v !== null && Object.values(v).every((list) => isStringArray(list)), "an object of string arrays");
|
|
1320
1583
|
accept("prefixDefault", (v) => typeof v === "boolean", "true or false");
|
|
1321
1584
|
accept("lazy", (v) => typeof v === "boolean" || v === "locale", "true, false or \"locale\"");
|
|
1585
|
+
if ("lint" in object) {
|
|
1586
|
+
const rules = lintRules(object.lint);
|
|
1587
|
+
if (rules) config.lint = { rules };
|
|
1588
|
+
else notes.push("\"lint\" could not be read statically (expected { rules: { code: \"off\" | \"warn\" | \"error\" | [level, options] } }).");
|
|
1589
|
+
}
|
|
1322
1590
|
}
|
|
1323
1591
|
/** Reads a config file's source text. Exposed for tests and for unsaved buffers. */
|
|
1324
1592
|
function parseStaticConfig(text, file = "dialex.config.ts") {
|
|
1325
1593
|
const notes = [];
|
|
1326
1594
|
const config = { ...DEFAULT_STATIC_CONFIG };
|
|
1595
|
+
let opaque = false;
|
|
1327
1596
|
if (file.endsWith(".json")) {
|
|
1328
1597
|
try {
|
|
1329
1598
|
const data = JSON.parse(text);
|
|
1330
|
-
if (data && typeof data === "object")
|
|
1599
|
+
if (data && typeof data === "object") applyConfigValues(data, config, notes);
|
|
1331
1600
|
} catch (error) {
|
|
1332
1601
|
notes.push(`Could not parse ${path.basename(file)}: ${error.message}`);
|
|
1333
1602
|
}
|
|
1334
1603
|
return {
|
|
1335
1604
|
config,
|
|
1336
1605
|
file,
|
|
1337
|
-
notes
|
|
1606
|
+
notes,
|
|
1607
|
+
opaque
|
|
1338
1608
|
};
|
|
1339
1609
|
}
|
|
1340
1610
|
try {
|
|
@@ -1343,22 +1613,15 @@ function parseStaticConfig(text, file = "dialex.config.ts") {
|
|
|
1343
1613
|
if (!object) notes.push(`${path.basename(file)} does not export a config object literal.`);
|
|
1344
1614
|
else {
|
|
1345
1615
|
const values = {};
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
"include",
|
|
1350
|
-
"exclude",
|
|
1351
|
-
"fallbacks",
|
|
1352
|
-
"prefixDefault",
|
|
1353
|
-
"lazy"
|
|
1354
|
-
]) {
|
|
1355
|
-
const prop = findProp(object, key);
|
|
1616
|
+
opaque = object.properties.some((p) => p.type === "SpreadElement" || p.type === "SpreadProperty" || p.computed === true);
|
|
1617
|
+
for (const key of CONFIG_KEYS) {
|
|
1618
|
+
const prop = findProperty(object, key);
|
|
1356
1619
|
if (!prop) continue;
|
|
1357
1620
|
const value = literal(prop.value);
|
|
1358
1621
|
if (value === NOT_LITERAL) notes.push(`"${key}" could not be read statically (not a literal).`);
|
|
1359
1622
|
else values[key] = value;
|
|
1360
1623
|
}
|
|
1361
|
-
|
|
1624
|
+
applyConfigValues(values, config, notes);
|
|
1362
1625
|
}
|
|
1363
1626
|
} catch (error) {
|
|
1364
1627
|
notes.push(`Could not parse ${path.basename(file)}: ${error.message}`);
|
|
@@ -1366,16 +1629,17 @@ function parseStaticConfig(text, file = "dialex.config.ts") {
|
|
|
1366
1629
|
return {
|
|
1367
1630
|
config,
|
|
1368
1631
|
file,
|
|
1369
|
-
notes
|
|
1632
|
+
notes,
|
|
1633
|
+
opaque
|
|
1370
1634
|
};
|
|
1371
1635
|
}
|
|
1372
1636
|
/** Reads `dialex.config.*` (or `i18n.config.*`) from `root`, or from `configFile` if given. */
|
|
1373
1637
|
function readStaticConfig(root, configFile) {
|
|
1374
|
-
const
|
|
1375
|
-
for (const file of candidates) if (fs.existsSync(file)) return parseStaticConfig(fs.readFileSync(file, "utf-8"), file);
|
|
1638
|
+
for (const file of configCandidates(root, configFile)) if (fs.existsSync(file)) return parseStaticConfig(fs.readFileSync(file, "utf-8"), file);
|
|
1376
1639
|
return {
|
|
1377
1640
|
config: { ...DEFAULT_STATIC_CONFIG },
|
|
1378
|
-
notes: []
|
|
1641
|
+
notes: [],
|
|
1642
|
+
opaque: false
|
|
1379
1643
|
};
|
|
1380
1644
|
}
|
|
1381
1645
|
//#endregion
|
|
@@ -1408,7 +1672,15 @@ function configFile(from) {
|
|
|
1408
1672
|
if (fs.existsSync(from) && fs.statSync(from).isFile()) return from;
|
|
1409
1673
|
for (const candidate of [`${from}.json`, path.join(from, "tsconfig.json")]) if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate;
|
|
1410
1674
|
}
|
|
1411
|
-
/**
|
|
1675
|
+
/** A package in `extends` (`@adonisjs/tsconfig/tsconfig.app.json`), looked up from `dir` upwards. */
|
|
1676
|
+
function packageConfig(from, specifier) {
|
|
1677
|
+
for (let dir = from;; dir = path.dirname(dir)) {
|
|
1678
|
+
const found = configFile(path.join(dir, "node_modules", specifier));
|
|
1679
|
+
if (found) return found;
|
|
1680
|
+
if (path.dirname(dir) === dir) return void 0;
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
/** `moduleResolution` and `include` of a tsconfig, following `extends` (relative or a package). */
|
|
1412
1684
|
function read(file, depth = 0) {
|
|
1413
1685
|
const result = { references: [] };
|
|
1414
1686
|
let json;
|
|
@@ -1418,8 +1690,12 @@ function read(file, depth = 0) {
|
|
|
1418
1690
|
return result;
|
|
1419
1691
|
}
|
|
1420
1692
|
const dir = path.dirname(file);
|
|
1421
|
-
const
|
|
1422
|
-
|
|
1693
|
+
const parents = Array.isArray(json.extends) ? json.extends : [json.extends];
|
|
1694
|
+
for (const extendsFrom of parents) {
|
|
1695
|
+
if (typeof extendsFrom !== "string" || depth >= 5) continue;
|
|
1696
|
+
const parent = extendsFrom.startsWith(".") ? configFile(path.resolve(dir, extendsFrom)) : packageConfig(dir, extendsFrom);
|
|
1697
|
+
if (parent) Object.assign(result, read(parent, depth + 1));
|
|
1698
|
+
}
|
|
1423
1699
|
const options = json.compilerOptions ?? {};
|
|
1424
1700
|
const module = typeof options.module === "string" ? options.module.toLowerCase() : void 0;
|
|
1425
1701
|
const resolution = typeof options.moduleResolution === "string" ? options.moduleResolution.toLowerCase() : module === "node16" || module === "nodenext" ? module : void 0;
|
|
@@ -1462,7 +1738,8 @@ function renderGenerated(root, options = {}) {
|
|
|
1462
1738
|
const config = options.static ? staticRead.config : resolveDialexConfig(root, inlineConfig);
|
|
1463
1739
|
const found = scanFiles(root, config.include || DEFAULT_CONFIG.include, { exclude: config.exclude }).sort();
|
|
1464
1740
|
const srcDir = fs.existsSync(path.join(root, "src")) ? path.join(root, "src") : root;
|
|
1465
|
-
const
|
|
1741
|
+
const outputOption = options.output ?? config.output;
|
|
1742
|
+
const outputPath = outputOption ? path.resolve(root, outputOption) : path.join(srcDir, "dialex.generated.ts");
|
|
1466
1743
|
const outputDir = path.dirname(outputPath);
|
|
1467
1744
|
const files = found.filter((file) => file !== outputPath && !file.endsWith(".d.ts") && path.basename(file) !== "dialex.generated.ts");
|
|
1468
1745
|
const parsed = files.map((file) => parseDictionaryText(file, fs.readFileSync(file, "utf-8")));
|
|
@@ -1552,12 +1829,12 @@ export const dialex = { dictionaries, config };
|
|
|
1552
1829
|
export default dictionaries;
|
|
1553
1830
|
`;
|
|
1554
1831
|
}
|
|
1555
|
-
const dts = renderDts(root, files, locales, messageArguments(parsed, clientConfig.defaultLocale));
|
|
1832
|
+
const dts = renderDts(root, files, locales, messageArguments(parsed, clientConfig.defaultLocale), outputOption ? outputDir : void 0);
|
|
1556
1833
|
return {
|
|
1557
1834
|
files,
|
|
1558
1835
|
outputPath,
|
|
1559
1836
|
content,
|
|
1560
|
-
dtsPath: dts?.path ?? path.join(srcDir, "dialex-env.d.ts"),
|
|
1837
|
+
dtsPath: dts?.path ?? path.join(outputOption ? outputDir : srcDir, "dialex-env.d.ts"),
|
|
1561
1838
|
dtsContent: dts?.content,
|
|
1562
1839
|
configFile: staticRead.file,
|
|
1563
1840
|
extraFiles,
|
|
@@ -1729,6 +2006,6 @@ async function loadStaticProject(root, configFile) {
|
|
|
1729
2006
|
/** `home` + `["nav", "about"]` -> `home.nav.about` */
|
|
1730
2007
|
const toKey = (dictionary, keyPath) => [dictionary, ...keyPath].join(".");
|
|
1731
2008
|
//#endregion
|
|
1732
|
-
export {
|
|
2009
|
+
export { SOURCE_BUILTIN_IGNORE as A, listLocales as B, isStale as C, record as D, readLock as E, dictionaryLocation as F, saveDictionaryFile as G, locationOf as H, getLeafSource as I, setString as K, getString as L, scanFiles as M, TODO_PREFIX as N, writeLock as O, copyLeaf as P, hasPath as R, hashSource as S, lockPath as T, parseDictionaryText as U, loadDictionaryFile as V, renderDictionaryFile as W, createAnalysisCache as _, renderGenerated as a, scanText as b, DEFAULT_STATIC_CONFIG as c, resolveDialexConfig as d, logger as f, clearGitignore as g, analyzeProjectSync as h, generateDictionaries as i, createGitignoreFilter as j, BUILTIN_IGNORE as k, parseStaticConfig as l, analyzeProject as m, loadStaticProject as n, runGenerate as o, DEFAULT_SOURCE_GLOB as p, toKey as r, startGenerateWatcher as s, loadProject as t, readStaticConfig as u, relativeFile as v, lockFor as w, LOCK_FILE as x, scanReferences as y, listLeaves as z };
|
|
1733
2010
|
|
|
1734
|
-
//# sourceMappingURL=project-
|
|
2011
|
+
//# sourceMappingURL=project-B27699T2.mjs.map
|