@dialexjs/cli 0.7.2 → 0.9.2
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 +73 -32
- package/dist/api.d.mts.map +1 -1
- package/dist/api.mjs +2 -2
- package/dist/index.mjs +566 -328
- package/dist/index.mjs.map +1 -1
- package/dist/{project-Dz0FbydQ.mjs → project-BTpZGNjh.mjs} +625 -324
- package/dist/project-BTpZGNjh.mjs.map +1 -0
- package/package.json +2 -2
- package/dist/project-Dz0FbydQ.mjs.map +0 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
1
|
import path from "node:path";
|
|
3
2
|
import pc from "picocolors";
|
|
4
|
-
import
|
|
3
|
+
import fs from "node:fs";
|
|
5
4
|
import { generateCode, parseExpression, parseModule, writeFile } from "magicast";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
6
|
import fg from "fast-glob";
|
|
7
7
|
import ignore from "ignore";
|
|
8
8
|
import { argumentSignature, findPlurals, getArguments, isIcuStructured, parseMessage } from "dialexjs/icu";
|
|
@@ -10,60 +10,68 @@ 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/dictionary
|
|
13
|
+
//#region src/utils/dictionary/types.ts
|
|
14
14
|
const TODO_PREFIX = "[TODO] ";
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/utils/ast.ts
|
|
17
|
+
/** Strips `as`, `satisfies` and parentheses around an expression. */
|
|
18
|
+
function unwrapNode(node) {
|
|
17
19
|
while (node && (node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression" || node.type === "ParenthesizedExpression")) node = node.expression;
|
|
18
20
|
return node;
|
|
19
21
|
}
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
+
const isObjectNode = (node) => node?.type === "ObjectExpression";
|
|
23
|
+
/** The static name of a property key, or `undefined` when it is computed. */
|
|
24
|
+
function propertyKeyName(prop) {
|
|
22
25
|
const key = prop?.key;
|
|
23
26
|
if (!key || prop.computed) return void 0;
|
|
24
27
|
if (key.type === "Identifier") return key.name;
|
|
25
28
|
if (key.type === "StringLiteral" || key.type === "Literal" && typeof key.value === "string") return key.value;
|
|
26
29
|
if (key.type === "NumericLiteral") return String(key.value);
|
|
27
30
|
}
|
|
28
|
-
const isPropLike = (p) => p?.type === "ObjectProperty" || p?.type === "Property" || p?.type === "ObjectMethod";
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
const isPropLike$1 = (p) => p?.type === "ObjectProperty" || p?.type === "Property" || p?.type === "ObjectMethod";
|
|
32
|
+
/** The first property of `obj` with the static name `key`. */
|
|
33
|
+
function findProperty(obj, key) {
|
|
34
|
+
return obj.properties.find((p) => isPropLike$1(p) && propertyKeyName(p) === key);
|
|
31
35
|
}
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/utils/dictionary/ast-helpers.ts
|
|
38
|
+
const IDENTIFIER$1 = /^[A-Za-z_$][\w$]*$/;
|
|
39
|
+
const isPropLike = (p) => p?.type === "ObjectProperty" || p?.type === "Property" || p?.type === "ObjectMethod";
|
|
32
40
|
function stringValue(node) {
|
|
33
|
-
node =
|
|
41
|
+
node = unwrapNode(node);
|
|
34
42
|
if (node?.type === "StringLiteral") return node.value;
|
|
35
43
|
if (node?.type === "Literal" && typeof node.value === "string") return node.value;
|
|
36
44
|
if (node?.type === "TemplateLiteral" && node.expressions.length === 0) return node.quasis[0]?.value?.cooked ?? void 0;
|
|
37
45
|
}
|
|
38
46
|
function leafKind(node) {
|
|
39
|
-
node =
|
|
47
|
+
node = unwrapNode(node);
|
|
40
48
|
if (stringValue(node) !== void 0) return "string";
|
|
41
49
|
if (node?.type === "ArrowFunctionExpression" || node?.type === "FunctionExpression" || node?.type === "ObjectMethod") return "function";
|
|
42
50
|
return "other";
|
|
43
51
|
}
|
|
44
52
|
function dictionaryProp(obj) {
|
|
45
|
-
const value =
|
|
46
|
-
return
|
|
53
|
+
const value = unwrapNode(findProperty(obj, "dictionary")?.value);
|
|
54
|
+
return isObjectNode(value) ? value : void 0;
|
|
47
55
|
}
|
|
48
56
|
function locateName(declaration) {
|
|
49
|
-
const decl =
|
|
57
|
+
const decl = unwrapNode(declaration);
|
|
50
58
|
if (decl?.type === "CallExpression") {
|
|
51
|
-
const first =
|
|
52
|
-
if (
|
|
59
|
+
const first = unwrapNode(decl.arguments[0]);
|
|
60
|
+
if (isObjectNode(first)) return stringValue(findProperty(first, "name")?.value);
|
|
53
61
|
return stringValue(first);
|
|
54
62
|
}
|
|
55
|
-
if (
|
|
63
|
+
if (isObjectNode(decl)) return stringValue(findProperty(decl, "name")?.value);
|
|
56
64
|
}
|
|
57
65
|
function locateLocalesObject(declaration) {
|
|
58
|
-
const decl =
|
|
66
|
+
const decl = unwrapNode(declaration);
|
|
59
67
|
if (decl?.type === "CallExpression") {
|
|
60
|
-
const first =
|
|
61
|
-
const second =
|
|
62
|
-
if (
|
|
63
|
-
if (
|
|
68
|
+
const first = unwrapNode(decl.arguments[0]);
|
|
69
|
+
const second = unwrapNode(decl.arguments[1]);
|
|
70
|
+
if (isObjectNode(second)) return second;
|
|
71
|
+
if (isObjectNode(first)) return dictionaryProp(first);
|
|
64
72
|
return;
|
|
65
73
|
}
|
|
66
|
-
if (
|
|
74
|
+
if (isObjectNode(decl)) return dictionaryProp(decl);
|
|
67
75
|
}
|
|
68
76
|
/** Quotes a property key only when it is not a valid identifier. */
|
|
69
77
|
function keyCode(key) {
|
|
@@ -82,17 +90,19 @@ function makeStringNode(value) {
|
|
|
82
90
|
*/
|
|
83
91
|
function isStaticallyComplete(localesObj) {
|
|
84
92
|
const walk = (obj) => obj.properties.every((prop) => {
|
|
85
|
-
if (!isPropLike(prop) ||
|
|
93
|
+
if (!isPropLike(prop) || propertyKeyName(prop) === void 0) return false;
|
|
86
94
|
if (prop.type === "ObjectMethod") return true;
|
|
87
|
-
const value =
|
|
88
|
-
return
|
|
95
|
+
const value = unwrapNode(prop.value);
|
|
96
|
+
return isObjectNode(value) ? walk(value) : true;
|
|
89
97
|
});
|
|
90
98
|
return localesObj.properties.every((prop) => {
|
|
91
|
-
if (!isPropLike(prop) ||
|
|
92
|
-
const value =
|
|
93
|
-
return
|
|
99
|
+
if (!isPropLike(prop) || propertyKeyName(prop) === void 0 || prop.type === "ObjectMethod") return false;
|
|
100
|
+
const value = unwrapNode(prop.value);
|
|
101
|
+
return isObjectNode(value) && walk(value);
|
|
94
102
|
});
|
|
95
103
|
}
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region src/utils/dictionary/io.ts
|
|
96
106
|
/**
|
|
97
107
|
* Parses dictionary source text (for example an unsaved editor buffer). Returns `undefined` when
|
|
98
108
|
* the file does not have an editable shape; throws on syntax errors.
|
|
@@ -128,12 +138,14 @@ async function saveDictionaryFile(df) {
|
|
|
128
138
|
if (!text.endsWith("\n")) fs.writeFileSync(df.file, text + "\n", "utf-8");
|
|
129
139
|
}
|
|
130
140
|
}
|
|
141
|
+
//#endregion
|
|
142
|
+
//#region src/utils/dictionary/read.ts
|
|
131
143
|
function listLocales(df) {
|
|
132
|
-
return df.localesObj.properties.filter(isPropLike).map(
|
|
144
|
+
return df.localesObj.properties.filter(isPropLike).map(propertyKeyName).filter((k) => k !== void 0);
|
|
133
145
|
}
|
|
134
146
|
function localeObject(df, locale) {
|
|
135
|
-
const value =
|
|
136
|
-
return
|
|
147
|
+
const value = unwrapNode(findProperty(df.localesObj, locale)?.value);
|
|
148
|
+
return isObjectNode(value) ? value : void 0;
|
|
137
149
|
}
|
|
138
150
|
/** Lists every leaf (string, function, or other value) of a locale, in source order. */
|
|
139
151
|
function listLeaves(df, locale) {
|
|
@@ -143,11 +155,11 @@ function listLeaves(df, locale) {
|
|
|
143
155
|
const walk = (obj, prefix) => {
|
|
144
156
|
for (const prop of obj.properties) {
|
|
145
157
|
if (!isPropLike(prop)) continue;
|
|
146
|
-
const name =
|
|
158
|
+
const name = propertyKeyName(prop);
|
|
147
159
|
if (name === void 0) continue;
|
|
148
160
|
const path = [...prefix, name];
|
|
149
|
-
const value = prop.type === "ObjectMethod" ? prop :
|
|
150
|
-
if (
|
|
161
|
+
const value = prop.type === "ObjectMethod" ? prop : unwrapNode(prop.value);
|
|
162
|
+
if (isObjectNode(value)) walk(value, path);
|
|
151
163
|
else {
|
|
152
164
|
const kind = leafKind(value);
|
|
153
165
|
leaves.push({
|
|
@@ -164,11 +176,11 @@ function listLeaves(df, locale) {
|
|
|
164
176
|
function getProp(df, locale, path) {
|
|
165
177
|
let obj = localeObject(df, locale);
|
|
166
178
|
for (let i = 0; obj && i < path.length; i++) {
|
|
167
|
-
const prop =
|
|
179
|
+
const prop = findProperty(obj, path[i]);
|
|
168
180
|
if (!prop) return void 0;
|
|
169
181
|
if (i === path.length - 1) return prop;
|
|
170
|
-
const next =
|
|
171
|
-
obj =
|
|
182
|
+
const next = unwrapNode(prop.value);
|
|
183
|
+
obj = isObjectNode(next) ? next : void 0;
|
|
172
184
|
}
|
|
173
185
|
}
|
|
174
186
|
function hasPath(df, locale, path) {
|
|
@@ -178,6 +190,13 @@ function getString(df, locale, path) {
|
|
|
178
190
|
const prop = getProp(df, locale, path);
|
|
179
191
|
return prop && prop.type !== "ObjectMethod" ? stringValue(prop.value) : void 0;
|
|
180
192
|
}
|
|
193
|
+
/** Leaf paths of a runtime dictionary value (plain objects recurse; everything else is a leaf). */
|
|
194
|
+
function flattenPaths(value, prefix = []) {
|
|
195
|
+
if (value && typeof value === "object" && !Array.isArray(value)) return Object.entries(value).flatMap(([k, v]) => flattenPaths(v, [...prefix, k]));
|
|
196
|
+
return prefix.length > 0 ? [prefix] : [];
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region src/utils/dictionary/write.ts
|
|
181
200
|
function ensureLocale(df, locale) {
|
|
182
201
|
let obj = localeObject(df, locale);
|
|
183
202
|
if (!obj) {
|
|
@@ -191,13 +210,13 @@ function ensureLocale(df, locale) {
|
|
|
191
210
|
function ensureParent(df, locale, path) {
|
|
192
211
|
let obj = ensureLocale(df, locale);
|
|
193
212
|
for (const segment of path.slice(0, -1)) {
|
|
194
|
-
let prop =
|
|
213
|
+
let prop = findProperty(obj, segment);
|
|
195
214
|
if (!prop) {
|
|
196
215
|
prop = makeProp(segment, "{}");
|
|
197
216
|
obj.properties.push(prop);
|
|
198
217
|
}
|
|
199
|
-
const next =
|
|
200
|
-
if (!
|
|
218
|
+
const next = unwrapNode(prop.value);
|
|
219
|
+
if (!isObjectNode(next)) return void 0;
|
|
201
220
|
obj = next;
|
|
202
221
|
}
|
|
203
222
|
return obj;
|
|
@@ -210,7 +229,7 @@ function setString(df, locale, path, value) {
|
|
|
210
229
|
const parent = ensureParent(df, locale, path);
|
|
211
230
|
if (!parent) return "skipped";
|
|
212
231
|
const last = path[path.length - 1];
|
|
213
|
-
const existing =
|
|
232
|
+
const existing = findProperty(parent, last);
|
|
214
233
|
if (existing) {
|
|
215
234
|
if (existing.type === "ObjectMethod" || leafKind(existing.value) !== "string") return "skipped";
|
|
216
235
|
existing.value = makeStringNode(value);
|
|
@@ -239,11 +258,8 @@ function copyLeaf(df, fromLocale, toLocale, path, mapString) {
|
|
|
239
258
|
}
|
|
240
259
|
return true;
|
|
241
260
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (value && typeof value === "object" && !Array.isArray(value)) return Object.entries(value).flatMap(([k, v]) => flattenPaths(v, [...prefix, k]));
|
|
245
|
-
return prefix.length > 0 ? [prefix] : [];
|
|
246
|
-
}
|
|
261
|
+
//#endregion
|
|
262
|
+
//#region src/utils/dictionary/locations.ts
|
|
247
263
|
const toRange = (loc) => loc ? {
|
|
248
264
|
start: {
|
|
249
265
|
line: loc.start.line - 1,
|
|
@@ -259,7 +275,7 @@ const toRange = (loc) => loc ? {
|
|
|
259
275
|
* it is a good place to attach a diagnostic or jump to.
|
|
260
276
|
*/
|
|
261
277
|
function locationOf(df, locale, path = []) {
|
|
262
|
-
const prop = path.length === 0 ?
|
|
278
|
+
const prop = path.length === 0 ? findProperty(df.localesObj, locale) : getProp(df, locale, path);
|
|
263
279
|
return toRange((prop?.key ?? prop)?.loc);
|
|
264
280
|
}
|
|
265
281
|
/** The source text of the dictionary object itself (the object whose keys are locales). */
|
|
@@ -273,6 +289,153 @@ function getLeafSource(df, locale, path) {
|
|
|
273
289
|
return generateCode(prop.type === "ObjectMethod" ? prop : prop.value).code;
|
|
274
290
|
}
|
|
275
291
|
//#endregion
|
|
292
|
+
//#region src/analysis/cache.ts
|
|
293
|
+
function createAnalysisCache() {
|
|
294
|
+
return {
|
|
295
|
+
dictionaries: /* @__PURE__ */ new Map(),
|
|
296
|
+
sources: /* @__PURE__ */ new Map(),
|
|
297
|
+
gitignore: /* @__PURE__ */ new Map()
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
/** Forget the cached `.gitignore` rules, for example after a `.gitignore` file changed. */
|
|
301
|
+
function clearGitignore(cache) {
|
|
302
|
+
cache.gitignore.clear();
|
|
303
|
+
}
|
|
304
|
+
function stampOf(file) {
|
|
305
|
+
try {
|
|
306
|
+
const stat = fs.statSync(file);
|
|
307
|
+
return `${stat.mtimeMs}:${stat.size}`;
|
|
308
|
+
} catch {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
//#endregion
|
|
313
|
+
//#region src/analysis/load-dictionary.ts
|
|
314
|
+
function fromSyntaxTree(df) {
|
|
315
|
+
const locales = listLocales(df);
|
|
316
|
+
const leaves = /* @__PURE__ */ new Map();
|
|
317
|
+
for (const locale of locales) {
|
|
318
|
+
const entries = /* @__PURE__ */ new Map();
|
|
319
|
+
for (const leaf of listLeaves(df, locale)) entries.set(leaf.path.join("."), {
|
|
320
|
+
kind: leaf.kind,
|
|
321
|
+
value: leaf.value
|
|
322
|
+
});
|
|
323
|
+
leaves.set(locale, entries);
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
name: df.name,
|
|
327
|
+
locales,
|
|
328
|
+
leaves,
|
|
329
|
+
df
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
async function fromRuntime(file) {
|
|
333
|
+
try {
|
|
334
|
+
const mod = await import(`${pathToFileURL(file).href}?t=${Date.now()}_${Math.random()}`);
|
|
335
|
+
const definition = mod.default || mod;
|
|
336
|
+
if (!definition || typeof definition !== "object" || !definition.dictionary) return {
|
|
337
|
+
ok: false,
|
|
338
|
+
level: "error",
|
|
339
|
+
code: "invalid-dictionary",
|
|
340
|
+
message: "Default export does not appear to be a valid defineDictionary output."
|
|
341
|
+
};
|
|
342
|
+
const dictionary = definition.dictionary;
|
|
343
|
+
const leaves = /* @__PURE__ */ new Map();
|
|
344
|
+
const invalid = [];
|
|
345
|
+
for (const [locale, content] of Object.entries(dictionary)) {
|
|
346
|
+
if (!content || typeof content !== "object") {
|
|
347
|
+
invalid.push(locale);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
const entries = /* @__PURE__ */ new Map();
|
|
351
|
+
for (const leafPath of flattenPaths(content)) {
|
|
352
|
+
let value = content;
|
|
353
|
+
for (const segment of leafPath) value = value?.[segment];
|
|
354
|
+
entries.set(leafPath.join("."), typeof value === "string" ? {
|
|
355
|
+
kind: "string",
|
|
356
|
+
value
|
|
357
|
+
} : { kind: typeof value === "function" ? "function" : "other" });
|
|
358
|
+
}
|
|
359
|
+
leaves.set(locale, entries);
|
|
360
|
+
}
|
|
361
|
+
if (invalid.length > 0) return {
|
|
362
|
+
ok: false,
|
|
363
|
+
level: "error",
|
|
364
|
+
code: "invalid-locale",
|
|
365
|
+
message: invalid.map((l) => `Locale "${l}" content is empty or invalid.`).join("\n")
|
|
366
|
+
};
|
|
367
|
+
return {
|
|
368
|
+
ok: true,
|
|
369
|
+
data: {
|
|
370
|
+
name: String(definition.name ?? ""),
|
|
371
|
+
locales: [...leaves.keys()],
|
|
372
|
+
leaves
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
} catch (error) {
|
|
376
|
+
return {
|
|
377
|
+
ok: false,
|
|
378
|
+
level: "error",
|
|
379
|
+
code: "invalid-dictionary",
|
|
380
|
+
message: `Failed to import dictionary: ${error.message || String(error)}`
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function fromParsed(parsed) {
|
|
385
|
+
try {
|
|
386
|
+
const df = parsed();
|
|
387
|
+
if (df?.complete && df.name) return {
|
|
388
|
+
ok: true,
|
|
389
|
+
data: fromSyntaxTree(df)
|
|
390
|
+
};
|
|
391
|
+
} catch (error) {
|
|
392
|
+
return {
|
|
393
|
+
ok: false,
|
|
394
|
+
level: "error",
|
|
395
|
+
code: "invalid-dictionary",
|
|
396
|
+
message: `Failed to parse dictionary: ${error.message}`
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const UNANALYZABLE = {
|
|
401
|
+
ok: false,
|
|
402
|
+
level: "info",
|
|
403
|
+
code: "unanalyzable",
|
|
404
|
+
message: "This dictionary uses spreads, computed keys, imported values, or a computed name, so it cannot be analyzed statically."
|
|
405
|
+
};
|
|
406
|
+
/** Reads a dictionary from its syntax tree. Never runs project code. */
|
|
407
|
+
function loadDictionaryDataStatic(file) {
|
|
408
|
+
return fromParsed(() => parseDictionaryText(file, fs.readFileSync(file, "utf-8"))) ?? UNANALYZABLE;
|
|
409
|
+
}
|
|
410
|
+
async function loadDictionaryData(file, runtime) {
|
|
411
|
+
const parsed = fromParsed(() => parseDictionaryText(file, fs.readFileSync(file, "utf-8")));
|
|
412
|
+
if (parsed?.ok) return parsed;
|
|
413
|
+
if (runtime) return fromRuntime(file);
|
|
414
|
+
return parsed ?? UNANALYZABLE;
|
|
415
|
+
}
|
|
416
|
+
async function cachedDictionary(file, runtime, cache) {
|
|
417
|
+
const stamp = cache ? stampOf(file) : void 0;
|
|
418
|
+
const hit = stamp ? cache.dictionaries.get(file) : void 0;
|
|
419
|
+
if (hit && hit.stamp === stamp) return hit.loaded;
|
|
420
|
+
const loaded = await loadDictionaryData(file, runtime);
|
|
421
|
+
if (cache && stamp) cache.dictionaries.set(file, {
|
|
422
|
+
stamp,
|
|
423
|
+
loaded
|
|
424
|
+
});
|
|
425
|
+
return loaded;
|
|
426
|
+
}
|
|
427
|
+
function cachedDictionaryStatic(file, cache) {
|
|
428
|
+
const stamp = cache ? stampOf(file) : void 0;
|
|
429
|
+
const hit = stamp ? cache.dictionaries.get(file) : void 0;
|
|
430
|
+
if (hit && hit.stamp === stamp) return hit.loaded;
|
|
431
|
+
const loaded = loadDictionaryDataStatic(file);
|
|
432
|
+
if (cache && stamp) cache.dictionaries.set(file, {
|
|
433
|
+
stamp,
|
|
434
|
+
loaded
|
|
435
|
+
});
|
|
436
|
+
return loaded;
|
|
437
|
+
}
|
|
438
|
+
//#endregion
|
|
276
439
|
//#region src/utils/files.ts
|
|
277
440
|
/** Folders that never hold project dictionaries or sources, in every scanner. */
|
|
278
441
|
const BUILTIN_IGNORE = [
|
|
@@ -517,29 +680,121 @@ function lockFor(root, sourceLocale) {
|
|
|
517
680
|
};
|
|
518
681
|
}
|
|
519
682
|
//#endregion
|
|
520
|
-
//#region src/analysis.ts
|
|
521
|
-
function createAnalysisCache() {
|
|
522
|
-
return {
|
|
523
|
-
dictionaries: /* @__PURE__ */ new Map(),
|
|
524
|
-
sources: /* @__PURE__ */ new Map(),
|
|
525
|
-
gitignore: /* @__PURE__ */ new Map()
|
|
526
|
-
};
|
|
527
|
-
}
|
|
528
|
-
/** Forget the cached `.gitignore` rules, for example after a `.gitignore` file changed. */
|
|
529
|
-
function clearGitignore(cache) {
|
|
530
|
-
cache.gitignore.clear();
|
|
531
|
-
}
|
|
683
|
+
//#region src/analysis/source-scan.ts
|
|
532
684
|
const DEFAULT_SOURCE_GLOB = "**/*.{ts,tsx,js,jsx,mjs,cjs,vue,svelte,astro,mdx}";
|
|
533
685
|
/** `getDictionary("x")`, `useDictionary("x")`, `@DialexDictionary("x")` */
|
|
534
686
|
const DICTIONARY_CALL = /\b(?:getDictionary|useDictionary|DialexDictionary)\(\s*(["'`])([^"'`$\n]+)\1/dg;
|
|
535
687
|
/** `t("dictionary.path.to.key")` */
|
|
536
688
|
const T_CALL = /(?<![\w$.])t\(\s*(["'`])([\w-]+(?:\.[\w-]+)+)\1/dg;
|
|
537
|
-
/**
|
|
538
|
-
|
|
689
|
+
/**
|
|
690
|
+
* Offsets of `//`, `/* *\/` and `<!-- -->` comments. Quoted strings are skipped so `"http://x"`
|
|
691
|
+
* is not a comment; `'` and `"` end at a newline, template literals do not.
|
|
692
|
+
*/
|
|
693
|
+
function commentRanges(text) {
|
|
694
|
+
const ranges = [];
|
|
695
|
+
const length = text.length;
|
|
696
|
+
let i = 0;
|
|
697
|
+
while (i < length) {
|
|
698
|
+
const c = text[i];
|
|
699
|
+
if (c === "\"" || c === "'" || c === "`") {
|
|
700
|
+
i++;
|
|
701
|
+
while (i < length && text[i] !== c && (c === "`" || text[i] !== "\n")) {
|
|
702
|
+
if (text[i] === "\\") i++;
|
|
703
|
+
i++;
|
|
704
|
+
}
|
|
705
|
+
i++;
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
let close;
|
|
709
|
+
if (c === "/" && text[i + 1] === "/") close = "\n";
|
|
710
|
+
else if (c === "/" && text[i + 1] === "*") close = "*/";
|
|
711
|
+
else if (c === "<" && text.startsWith("<!--", i)) close = "-->";
|
|
712
|
+
if (close === void 0) {
|
|
713
|
+
i++;
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
const found = text.indexOf(close, i + 2);
|
|
717
|
+
const end = found === -1 ? length : close === "\n" ? found : found + close.length;
|
|
718
|
+
ranges.push([i, end]);
|
|
719
|
+
i = end;
|
|
720
|
+
}
|
|
721
|
+
return ranges;
|
|
722
|
+
}
|
|
723
|
+
const IGNORE_DIRECTIVE = /dialex-ignore(-next-line)?(?![\w-])/;
|
|
724
|
+
/** `dictionaries: { name: ..., "other-name": ..., shorthand }`, up to the first key. */
|
|
725
|
+
const DICTIONARIES_OBJECT = /\bdictionaries\s*:\s*\{/g;
|
|
726
|
+
const OBJECT_KEY = /\s*(?:([A-Za-z_$][\w$-]*)|(["'])([^"'\n]+)\2)\s*(?=[:,}])/y;
|
|
727
|
+
/** Replaces comments with spaces of the same length, keeping offsets and line breaks. */
|
|
728
|
+
function maskComments(text, comments) {
|
|
729
|
+
if (comments.length === 0) return text;
|
|
730
|
+
let out = "";
|
|
731
|
+
let at = 0;
|
|
732
|
+
for (const [from, to] of comments) {
|
|
733
|
+
out += text.slice(at, from) + text.slice(from, to).replace(/[^\n]/g, " ");
|
|
734
|
+
at = to;
|
|
735
|
+
}
|
|
736
|
+
return out + text.slice(at);
|
|
737
|
+
}
|
|
738
|
+
/** Names that an object literal `dictionaries: { ... }` registers inline. */
|
|
739
|
+
function inlineDictionaryNames(masked) {
|
|
740
|
+
const names = /* @__PURE__ */ new Set();
|
|
741
|
+
for (const match of masked.matchAll(DICTIONARIES_OBJECT)) {
|
|
742
|
+
let i = match.index + match[0].length;
|
|
743
|
+
let depth = 1;
|
|
744
|
+
let expectKey = true;
|
|
745
|
+
while (i < masked.length && depth > 0) {
|
|
746
|
+
if (expectKey && depth === 1) {
|
|
747
|
+
OBJECT_KEY.lastIndex = i;
|
|
748
|
+
const key = OBJECT_KEY.exec(masked);
|
|
749
|
+
if (key) {
|
|
750
|
+
names.add(key[1] ?? key[3]);
|
|
751
|
+
i = OBJECT_KEY.lastIndex;
|
|
752
|
+
}
|
|
753
|
+
expectKey = false;
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
const c = masked[i];
|
|
757
|
+
if (c === "\"" || c === "'" || c === "`") {
|
|
758
|
+
i++;
|
|
759
|
+
while (i < masked.length && masked[i] !== c && (c === "`" || masked[i] !== "\n")) {
|
|
760
|
+
if (masked[i] === "\\") i++;
|
|
761
|
+
i++;
|
|
762
|
+
}
|
|
763
|
+
} else if (c === "{" || c === "[" || c === "(") depth++;
|
|
764
|
+
else if (c === "}" || c === "]" || c === ")") depth--;
|
|
765
|
+
else if (c === "," && depth === 1) expectKey = true;
|
|
766
|
+
i++;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return [...names];
|
|
770
|
+
}
|
|
771
|
+
/** Lines that a `dialex-ignore` / `dialex-ignore-next-line` comment excludes from checking. */
|
|
772
|
+
function ignoredLines(text, comments) {
|
|
773
|
+
const lines = /* @__PURE__ */ new Set();
|
|
774
|
+
if (!text.includes("dialex-ignore")) return lines;
|
|
775
|
+
const starts = lineStarts(text);
|
|
776
|
+
for (const [from, to] of comments) {
|
|
777
|
+
const directive = IGNORE_DIRECTIVE.exec(text.slice(from, to));
|
|
778
|
+
if (!directive) continue;
|
|
779
|
+
const first = positionAt(starts, from).line;
|
|
780
|
+
const last = positionAt(starts, Math.max(from, to - 1)).line;
|
|
781
|
+
if (directive[1]) lines.add(last + 1);
|
|
782
|
+
else for (let line = first; line <= last; line++) lines.add(line);
|
|
783
|
+
}
|
|
784
|
+
return lines;
|
|
785
|
+
}
|
|
786
|
+
/** References and inline dictionary names found in one source file. */
|
|
787
|
+
function scanText(text) {
|
|
788
|
+
const comments = commentRanges(text);
|
|
789
|
+
const ignored = ignoredLines(text, comments);
|
|
790
|
+
const starts = ignored.size > 0 ? lineStarts(text) : [];
|
|
791
|
+
const inComment = (offset) => comments.some(([from, to]) => offset >= from && offset < to);
|
|
539
792
|
const references = [];
|
|
540
793
|
const collect = (pattern, kind) => {
|
|
541
794
|
for (const match of text.matchAll(pattern)) {
|
|
542
795
|
const [start, end] = match.indices[2];
|
|
796
|
+
if (comments.length > 0 && inComment(start)) continue;
|
|
797
|
+
if (ignored.size > 0 && ignored.has(positionAt(starts, start).line)) continue;
|
|
543
798
|
references.push({
|
|
544
799
|
kind,
|
|
545
800
|
value: match[2],
|
|
@@ -550,7 +805,18 @@ function scanReferences(text) {
|
|
|
550
805
|
};
|
|
551
806
|
collect(DICTIONARY_CALL, "dictionary");
|
|
552
807
|
collect(T_CALL, "path");
|
|
553
|
-
|
|
808
|
+
references.sort((a, b) => a.start - b.start);
|
|
809
|
+
return {
|
|
810
|
+
references,
|
|
811
|
+
inline: text.includes("dictionaries") ? inlineDictionaryNames(maskComments(text, comments)) : []
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Finds `getDictionary("x")`-style and `t("x.y.z")` references in source text. References inside
|
|
816
|
+
* comments, and on lines a `dialex-ignore` comment covers, are skipped.
|
|
817
|
+
*/
|
|
818
|
+
function scanReferences(text) {
|
|
819
|
+
return scanText(text).references;
|
|
554
820
|
}
|
|
555
821
|
function lineStarts(text) {
|
|
556
822
|
const starts = [0];
|
|
@@ -570,134 +836,34 @@ function positionAt(starts, offset) {
|
|
|
570
836
|
column: offset - starts[low]
|
|
571
837
|
};
|
|
572
838
|
}
|
|
573
|
-
function fromSyntaxTree(df) {
|
|
574
|
-
const locales = listLocales(df);
|
|
575
|
-
const leaves = /* @__PURE__ */ new Map();
|
|
576
|
-
for (const locale of locales) {
|
|
577
|
-
const entries = /* @__PURE__ */ new Map();
|
|
578
|
-
for (const leaf of listLeaves(df, locale)) entries.set(leaf.path.join("."), {
|
|
579
|
-
kind: leaf.kind,
|
|
580
|
-
value: leaf.value
|
|
581
|
-
});
|
|
582
|
-
leaves.set(locale, entries);
|
|
583
|
-
}
|
|
584
|
-
return {
|
|
585
|
-
name: df.name,
|
|
586
|
-
locales,
|
|
587
|
-
leaves,
|
|
588
|
-
df
|
|
589
|
-
};
|
|
590
|
-
}
|
|
591
|
-
async function fromRuntime(file) {
|
|
592
|
-
try {
|
|
593
|
-
const mod = await import(`${pathToFileURL(file).href}?t=${Date.now()}_${Math.random()}`);
|
|
594
|
-
const definition = mod.default || mod;
|
|
595
|
-
if (!definition || typeof definition !== "object" || !definition.dictionary) return {
|
|
596
|
-
ok: false,
|
|
597
|
-
level: "error",
|
|
598
|
-
code: "invalid-dictionary",
|
|
599
|
-
message: "Default export does not appear to be a valid defineDictionary output."
|
|
600
|
-
};
|
|
601
|
-
const dictionary = definition.dictionary;
|
|
602
|
-
const leaves = /* @__PURE__ */ new Map();
|
|
603
|
-
const invalid = [];
|
|
604
|
-
for (const [locale, content] of Object.entries(dictionary)) {
|
|
605
|
-
if (!content || typeof content !== "object") {
|
|
606
|
-
invalid.push(locale);
|
|
607
|
-
continue;
|
|
608
|
-
}
|
|
609
|
-
const entries = /* @__PURE__ */ new Map();
|
|
610
|
-
for (const leafPath of flattenPaths(content)) {
|
|
611
|
-
let value = content;
|
|
612
|
-
for (const segment of leafPath) value = value?.[segment];
|
|
613
|
-
entries.set(leafPath.join("."), typeof value === "string" ? {
|
|
614
|
-
kind: "string",
|
|
615
|
-
value
|
|
616
|
-
} : { kind: typeof value === "function" ? "function" : "other" });
|
|
617
|
-
}
|
|
618
|
-
leaves.set(locale, entries);
|
|
619
|
-
}
|
|
620
|
-
if (invalid.length > 0) return {
|
|
621
|
-
ok: false,
|
|
622
|
-
level: "error",
|
|
623
|
-
code: "invalid-locale",
|
|
624
|
-
message: invalid.map((l) => `Locale "${l}" content is empty or invalid.`).join("\n")
|
|
625
|
-
};
|
|
626
|
-
return {
|
|
627
|
-
ok: true,
|
|
628
|
-
data: {
|
|
629
|
-
name: String(definition.name ?? ""),
|
|
630
|
-
locales: [...leaves.keys()],
|
|
631
|
-
leaves
|
|
632
|
-
}
|
|
633
|
-
};
|
|
634
|
-
} catch (error) {
|
|
635
|
-
return {
|
|
636
|
-
ok: false,
|
|
637
|
-
level: "error",
|
|
638
|
-
code: "invalid-dictionary",
|
|
639
|
-
message: `Failed to import dictionary: ${error.message || String(error)}`
|
|
640
|
-
};
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
async function loadDictionaryData(file, runtime) {
|
|
644
|
-
let astError;
|
|
645
|
-
try {
|
|
646
|
-
const df = await loadDictionaryFile(file);
|
|
647
|
-
if (df?.complete && df.name) return {
|
|
648
|
-
ok: true,
|
|
649
|
-
data: fromSyntaxTree(df)
|
|
650
|
-
};
|
|
651
|
-
} catch (error) {
|
|
652
|
-
astError = error;
|
|
653
|
-
}
|
|
654
|
-
if (runtime) return fromRuntime(file);
|
|
655
|
-
return astError ? {
|
|
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
|
-
};
|
|
666
|
-
}
|
|
667
|
-
function stampOf(file) {
|
|
668
|
-
try {
|
|
669
|
-
const stat = fs.statSync(file);
|
|
670
|
-
return `${stat.mtimeMs}:${stat.size}`;
|
|
671
|
-
} catch {
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
async function cachedDictionary(file, runtime, cache) {
|
|
676
|
-
const stamp = cache ? stampOf(file) : void 0;
|
|
677
|
-
const hit = stamp ? cache.dictionaries.get(file) : void 0;
|
|
678
|
-
if (hit && hit.stamp === stamp) return hit.loaded;
|
|
679
|
-
const loaded = await loadDictionaryData(file, runtime);
|
|
680
|
-
if (cache && stamp) cache.dictionaries.set(file, {
|
|
681
|
-
stamp,
|
|
682
|
-
loaded
|
|
683
|
-
});
|
|
684
|
-
return loaded;
|
|
685
|
-
}
|
|
686
839
|
function scanSource(text) {
|
|
687
840
|
const starts = lineStarts(text);
|
|
688
841
|
const rangeOf = (start, end) => ({
|
|
689
842
|
start: positionAt(starts, start),
|
|
690
843
|
end: positionAt(starts, end)
|
|
691
844
|
});
|
|
845
|
+
const { references, inline } = scanText(text);
|
|
692
846
|
return {
|
|
693
847
|
tokens: [...new Set(text.match(/[\w$-]+/g) ?? [])],
|
|
694
|
-
refs:
|
|
848
|
+
refs: references.map((ref) => ({
|
|
695
849
|
ref,
|
|
696
850
|
range: rangeOf(ref.start, ref.end)
|
|
697
|
-
}))
|
|
851
|
+
})),
|
|
852
|
+
inline
|
|
698
853
|
};
|
|
699
854
|
}
|
|
700
|
-
|
|
855
|
+
//#endregion
|
|
856
|
+
//#region src/analysis/steps.ts
|
|
857
|
+
function lintLevel(setting) {
|
|
858
|
+
const level = Array.isArray(setting) ? setting[0] : setting;
|
|
859
|
+
return level === "off" || level === "warn" || level === "error" ? level : void 0;
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* The analysis itself, written once. It yields a `LoadRequest` for every dictionary so that a
|
|
863
|
+
* synchronous driver (`analyzeProjectSync`) and an asynchronous one (`analyzeProject`, which may
|
|
864
|
+
* import dictionaries at runtime) can share it.
|
|
865
|
+
*/
|
|
866
|
+
function* analysisSteps(options) {
|
|
701
867
|
const { root, config } = options;
|
|
702
868
|
const include = config.include || "**/*.content.ts";
|
|
703
869
|
const configuredLocales = config.locales || [];
|
|
@@ -718,7 +884,12 @@ async function analyzeProject(options) {
|
|
|
718
884
|
const dictionaries = [];
|
|
719
885
|
const lock = options.lock === false ? void 0 : readLock(root);
|
|
720
886
|
for (const file of files) {
|
|
721
|
-
const loaded =
|
|
887
|
+
const loaded = yield {
|
|
888
|
+
file,
|
|
889
|
+
runtime: options.runtime === true,
|
|
890
|
+
cache,
|
|
891
|
+
text: options.contents?.get(file)
|
|
892
|
+
};
|
|
722
893
|
if (!loaded.ok) {
|
|
723
894
|
for (const message of loaded.message.split("\n")) issues.push({
|
|
724
895
|
file,
|
|
@@ -831,8 +1002,10 @@ async function analyzeProject(options) {
|
|
|
831
1002
|
ignore: extraIgnore,
|
|
832
1003
|
gitignoreFilter: gitignore
|
|
833
1004
|
}).filter((f) => !dictionaryFiles.has(f));
|
|
1005
|
+
const inline = /* @__PURE__ */ new Set();
|
|
834
1006
|
if (sourceFiles.length > 0) {
|
|
835
1007
|
const tokens = /* @__PURE__ */ new Set();
|
|
1008
|
+
const scans = [];
|
|
836
1009
|
for (const file of sourceFiles) {
|
|
837
1010
|
const stamp = cache ? stampOf(file) : void 0;
|
|
838
1011
|
let scan = stamp ? cache.sources.get(file) : void 0;
|
|
@@ -850,32 +1023,37 @@ async function analyzeProject(options) {
|
|
|
850
1023
|
if (cache && stamp) cache.sources.set(file, scan);
|
|
851
1024
|
}
|
|
852
1025
|
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({
|
|
1026
|
+
scans.push({
|
|
1027
|
+
file,
|
|
1028
|
+
scan: scan.scan
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
for (const { scan } of scans) for (const name of scan.inline) inline.add(name);
|
|
1032
|
+
for (const { file, scan } of scans) for (const { ref, range } of scan.refs) {
|
|
1033
|
+
if (ref.kind === "dictionary") {
|
|
1034
|
+
if (!known.has(ref.value) && !inline.has(ref.value)) issues.push({
|
|
870
1035
|
file,
|
|
871
1036
|
level: "error",
|
|
872
|
-
code: "unknown-
|
|
873
|
-
message: `Unknown
|
|
1037
|
+
code: "unknown-dictionary",
|
|
1038
|
+
message: `Unknown dictionary "${ref.value}"`,
|
|
874
1039
|
range,
|
|
875
|
-
dictionary:
|
|
876
|
-
path: rest
|
|
1040
|
+
dictionary: ref.value
|
|
877
1041
|
});
|
|
1042
|
+
continue;
|
|
878
1043
|
}
|
|
1044
|
+
const [name, ...rest] = ref.value.split(".");
|
|
1045
|
+
const dictionary = known.get(name);
|
|
1046
|
+
if (!dictionary) continue;
|
|
1047
|
+
const keyPath = rest.join(".");
|
|
1048
|
+
if (!dictionary.paths.some((p) => p === keyPath || p.startsWith(`${keyPath}.`))) issues.push({
|
|
1049
|
+
file,
|
|
1050
|
+
level: "error",
|
|
1051
|
+
code: "unknown-path",
|
|
1052
|
+
message: `Unknown translation path "${ref.value}"`,
|
|
1053
|
+
range,
|
|
1054
|
+
dictionary: name,
|
|
1055
|
+
path: rest
|
|
1056
|
+
});
|
|
879
1057
|
}
|
|
880
1058
|
if (options.unused !== false) for (const dictionary of dictionaries) {
|
|
881
1059
|
if (!tokens.has(dictionary.name)) {
|
|
@@ -909,13 +1087,49 @@ async function analyzeProject(options) {
|
|
|
909
1087
|
const live = /* @__PURE__ */ new Set([...files, ...sourceFiles]);
|
|
910
1088
|
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
1089
|
}
|
|
1090
|
+
const rules = config.lint?.rules;
|
|
1091
|
+
if (rules && Object.keys(rules).length > 0) for (let i = issues.length - 1; i >= 0; i--) {
|
|
1092
|
+
const level = lintLevel(rules[issues[i].code]);
|
|
1093
|
+
if (!level) continue;
|
|
1094
|
+
if (level === "off") issues.splice(i, 1);
|
|
1095
|
+
else issues[i].level = level === "error" ? "error" : "warning";
|
|
1096
|
+
}
|
|
912
1097
|
return {
|
|
913
1098
|
issues,
|
|
914
1099
|
dictionaries,
|
|
915
1100
|
files,
|
|
916
|
-
sourceFilesScanned: sourceFiles.length
|
|
1101
|
+
sourceFilesScanned: sourceFiles.length,
|
|
1102
|
+
inlineDictionaries: [...inline]
|
|
917
1103
|
};
|
|
918
1104
|
}
|
|
1105
|
+
//#endregion
|
|
1106
|
+
//#region src/analysis/index.ts
|
|
1107
|
+
/** Analyzes a project. With `runtime: true`, dictionaries the syntax tree cannot describe are imported. */
|
|
1108
|
+
async function analyzeProject(options) {
|
|
1109
|
+
const steps = analysisSteps(options);
|
|
1110
|
+
let step = steps.next();
|
|
1111
|
+
while (!step.done) {
|
|
1112
|
+
const { file, runtime, cache, text } = step.value;
|
|
1113
|
+
step = steps.next(text === void 0 ? await cachedDictionary(file, runtime, cache) : fromParsed(() => parseDictionaryText(file, text)) ?? UNANALYZABLE);
|
|
1114
|
+
}
|
|
1115
|
+
return step.value;
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* `analyzeProject` for callers that cannot await, such as lint rules. Reads dictionaries from the
|
|
1119
|
+
* syntax tree only: `runtime` is ignored, so project code is never executed.
|
|
1120
|
+
*/
|
|
1121
|
+
function analyzeProjectSync(options) {
|
|
1122
|
+
const steps = analysisSteps({
|
|
1123
|
+
...options,
|
|
1124
|
+
runtime: false
|
|
1125
|
+
});
|
|
1126
|
+
let step = steps.next();
|
|
1127
|
+
while (!step.done) {
|
|
1128
|
+
const { file, cache, text } = step.value;
|
|
1129
|
+
step = steps.next(text === void 0 ? cachedDictionaryStatic(file, cache) : fromParsed(() => parseDictionaryText(file, text)) ?? UNANALYZABLE);
|
|
1130
|
+
}
|
|
1131
|
+
return step.value;
|
|
1132
|
+
}
|
|
919
1133
|
/** Project root-relative path with forward slashes, for display. */
|
|
920
1134
|
function relativeFile(root, file) {
|
|
921
1135
|
return path.relative(root, file);
|
|
@@ -930,44 +1144,48 @@ const logger = {
|
|
|
930
1144
|
log: (msg) => console.log(msg)
|
|
931
1145
|
};
|
|
932
1146
|
//#endregion
|
|
1147
|
+
//#region src/utils/load-config.ts
|
|
1148
|
+
/**
|
|
1149
|
+
* Runs the first config file that exists (`dialex.config.*`, then `i18n.config.*`, or
|
|
1150
|
+
* `configFile`) with `unconfig` and returns what it exports. **This executes project code.**
|
|
1151
|
+
* Returns `undefined` when the project has no config file; throws what the file throws.
|
|
1152
|
+
*/
|
|
1153
|
+
function loadConfigObject(root, configFile) {
|
|
1154
|
+
const sources = [];
|
|
1155
|
+
if (configFile) sources.push({ files: configFile });
|
|
1156
|
+
for (const name of ["dialex.config", "i18n.config"]) sources.push({
|
|
1157
|
+
files: name,
|
|
1158
|
+
extensions: [
|
|
1159
|
+
"ts",
|
|
1160
|
+
"mts",
|
|
1161
|
+
"cts",
|
|
1162
|
+
"js",
|
|
1163
|
+
"mjs",
|
|
1164
|
+
"cjs",
|
|
1165
|
+
"json",
|
|
1166
|
+
""
|
|
1167
|
+
]
|
|
1168
|
+
});
|
|
1169
|
+
const result = loadConfigSync({
|
|
1170
|
+
sources,
|
|
1171
|
+
merge: false,
|
|
1172
|
+
defaults: {},
|
|
1173
|
+
cwd: root
|
|
1174
|
+
});
|
|
1175
|
+
const file = result?.sources?.[0];
|
|
1176
|
+
if (!file) return void 0;
|
|
1177
|
+
return {
|
|
1178
|
+
config: result.config && typeof result.config === "object" ? result.config : {},
|
|
1179
|
+
file
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
//#endregion
|
|
933
1183
|
//#region src/utils/scanner.ts
|
|
934
1184
|
function resolveDialexConfig(root, inlineConfig = {}) {
|
|
935
1185
|
let loadedConfig = {};
|
|
936
1186
|
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;
|
|
1187
|
+
const loaded = loadConfigObject(root, inlineConfig.configFile);
|
|
1188
|
+
if (loaded) loadedConfig = loaded.config;
|
|
971
1189
|
} catch {}
|
|
972
1190
|
return {
|
|
973
1191
|
...DEFAULT_CONFIG,
|
|
@@ -990,7 +1208,15 @@ const REGISTRY_ADAPTERS = [
|
|
|
990
1208
|
"vue",
|
|
991
1209
|
"web",
|
|
992
1210
|
"h3",
|
|
993
|
-
"angular"
|
|
1211
|
+
"angular",
|
|
1212
|
+
"svelte",
|
|
1213
|
+
"solid",
|
|
1214
|
+
"preact",
|
|
1215
|
+
"qwik",
|
|
1216
|
+
"lit",
|
|
1217
|
+
"hapi",
|
|
1218
|
+
"adonis",
|
|
1219
|
+
"trpc"
|
|
994
1220
|
];
|
|
995
1221
|
/** Dependencies that tell which `dialexjs/*` entry points a project can import. */
|
|
996
1222
|
const ADAPTER_DEPENDENCIES = {
|
|
@@ -1011,6 +1237,17 @@ const ADAPTER_DEPENDENCIES = {
|
|
|
1011
1237
|
h3: ["h3"],
|
|
1012
1238
|
nitropack: ["h3"],
|
|
1013
1239
|
"@angular/core": ["angular"],
|
|
1240
|
+
svelte: ["svelte"],
|
|
1241
|
+
"solid-js": ["solid"],
|
|
1242
|
+
preact: ["preact"],
|
|
1243
|
+
"@builder.io/qwik": ["qwik"],
|
|
1244
|
+
lit: ["lit"],
|
|
1245
|
+
"@hapi/hapi": ["hapi"],
|
|
1246
|
+
"@adonisjs/core": ["adonis"],
|
|
1247
|
+
"@adonisjs/http-server": ["adonis"],
|
|
1248
|
+
"@trpc/server": ["trpc"],
|
|
1249
|
+
"react-native": ["react"],
|
|
1250
|
+
expo: ["react"],
|
|
1014
1251
|
"react-router": ["web"],
|
|
1015
1252
|
"react-router-dom": ["web"],
|
|
1016
1253
|
"@remix-run/react": ["web"],
|
|
@@ -1041,9 +1278,9 @@ function detectAdapters(root) {
|
|
|
1041
1278
|
* The `dialex-env.d.ts` a project should have: its path and contents, or `undefined` when there
|
|
1042
1279
|
* are no dictionary files. Nothing is written.
|
|
1043
1280
|
*/
|
|
1044
|
-
function renderDts(root, files, locales, messageArguments = {}) {
|
|
1281
|
+
function renderDts(root, files, locales, messageArguments = {}, directory) {
|
|
1045
1282
|
if (files.length === 0) return void 0;
|
|
1046
|
-
const dtsDir = fs.existsSync(path.join(root, "src")) ? path.join(root, "src") : root;
|
|
1283
|
+
const dtsDir = directory ?? (fs.existsSync(path.join(root, "src")) ? path.join(root, "src") : root);
|
|
1047
1284
|
const imports = [];
|
|
1048
1285
|
const records = [];
|
|
1049
1286
|
files.forEach((file, index) => {
|
|
@@ -1226,9 +1463,9 @@ function splitLocales(df) {
|
|
|
1226
1463
|
const bound = topLevelNames(df);
|
|
1227
1464
|
const slices = [];
|
|
1228
1465
|
for (const prop of df.localesObj.properties) {
|
|
1229
|
-
const locale =
|
|
1230
|
-
const value =
|
|
1231
|
-
if (locale === void 0 || !
|
|
1466
|
+
const locale = propertyKeyName(prop);
|
|
1467
|
+
const value = unwrapNode(prop.value);
|
|
1468
|
+
if (locale === void 0 || !isObjectNode(value)) return {
|
|
1232
1469
|
ok: false,
|
|
1233
1470
|
reason: "a locale is not an object literal"
|
|
1234
1471
|
};
|
|
@@ -1253,8 +1490,10 @@ function splitLocales(df) {
|
|
|
1253
1490
|
}
|
|
1254
1491
|
//#endregion
|
|
1255
1492
|
//#region src/utils/static-config.ts
|
|
1493
|
+
/** The names a config file can have, without extension, in order of preference. */
|
|
1256
1494
|
const CONFIG_NAMES = ["dialex.config", "i18n.config"];
|
|
1257
|
-
|
|
1495
|
+
/** The extensions tried for each name, in order. */
|
|
1496
|
+
const CONFIG_EXTENSIONS = [
|
|
1258
1497
|
"ts",
|
|
1259
1498
|
"mts",
|
|
1260
1499
|
"cts",
|
|
@@ -1263,6 +1502,22 @@ const EXTENSIONS = [
|
|
|
1263
1502
|
"cjs",
|
|
1264
1503
|
"json"
|
|
1265
1504
|
];
|
|
1505
|
+
/** The keys of the config that tooling reads. */
|
|
1506
|
+
const CONFIG_KEYS = [
|
|
1507
|
+
"defaultLocale",
|
|
1508
|
+
"locales",
|
|
1509
|
+
"include",
|
|
1510
|
+
"exclude",
|
|
1511
|
+
"output",
|
|
1512
|
+
"fallbacks",
|
|
1513
|
+
"prefixDefault",
|
|
1514
|
+
"lazy",
|
|
1515
|
+
"lint"
|
|
1516
|
+
];
|
|
1517
|
+
/** The paths `readStaticConfig` and `resolveConfig` try, in order, for a project root. */
|
|
1518
|
+
function configCandidates(root, configFile) {
|
|
1519
|
+
return configFile ? [path.resolve(root, configFile)] : CONFIG_NAMES.flatMap((name) => CONFIG_EXTENSIONS.map((ext) => path.join(root, `${name}.${ext}`)));
|
|
1520
|
+
}
|
|
1266
1521
|
const DEFAULT_STATIC_CONFIG = {
|
|
1267
1522
|
defaultLocale: "en",
|
|
1268
1523
|
locales: [],
|
|
@@ -1270,11 +1525,12 @@ const DEFAULT_STATIC_CONFIG = {
|
|
|
1270
1525
|
exclude: [],
|
|
1271
1526
|
fallbacks: {},
|
|
1272
1527
|
prefixDefault: true,
|
|
1273
|
-
lazy: false
|
|
1528
|
+
lazy: false,
|
|
1529
|
+
lint: { rules: {} }
|
|
1274
1530
|
};
|
|
1275
1531
|
const NOT_LITERAL = Symbol("not literal");
|
|
1276
1532
|
function literal(node) {
|
|
1277
|
-
node =
|
|
1533
|
+
node = unwrapNode(node);
|
|
1278
1534
|
switch (node?.type) {
|
|
1279
1535
|
case "StringLiteral":
|
|
1280
1536
|
case "NumericLiteral":
|
|
@@ -1288,7 +1544,7 @@ function literal(node) {
|
|
|
1288
1544
|
case "ObjectExpression": {
|
|
1289
1545
|
const result = {};
|
|
1290
1546
|
for (const prop of node.properties) {
|
|
1291
|
-
const key =
|
|
1547
|
+
const key = propertyKeyName(prop);
|
|
1292
1548
|
const value = key === void 0 ? NOT_LITERAL : literal(prop.value);
|
|
1293
1549
|
if (value === NOT_LITERAL) return NOT_LITERAL;
|
|
1294
1550
|
result[key] = value;
|
|
@@ -1300,12 +1556,36 @@ function literal(node) {
|
|
|
1300
1556
|
}
|
|
1301
1557
|
/** The object literal passed to `defineConfig({...})`, or exported directly. */
|
|
1302
1558
|
function findConfigObject(declaration) {
|
|
1303
|
-
const node =
|
|
1559
|
+
const node = unwrapNode(declaration);
|
|
1304
1560
|
if (node?.type === "CallExpression") return findConfigObject(node.arguments[0]);
|
|
1305
|
-
return
|
|
1561
|
+
return isObjectNode(node) ? node : void 0;
|
|
1306
1562
|
}
|
|
1307
1563
|
const isStringArray = (value) => Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
1308
|
-
|
|
1564
|
+
const LINT_LEVELS = [
|
|
1565
|
+
"off",
|
|
1566
|
+
"warn",
|
|
1567
|
+
"error"
|
|
1568
|
+
];
|
|
1569
|
+
/** `lint: { rules: { code: "warn" | ["warn", {...}] } }`, or `undefined` when it is not that shape. */
|
|
1570
|
+
function lintRules(value) {
|
|
1571
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
1572
|
+
const rules = value.rules;
|
|
1573
|
+
if (rules === void 0) return {};
|
|
1574
|
+
if (typeof rules !== "object" || rules === null || Array.isArray(rules)) return void 0;
|
|
1575
|
+
const result = {};
|
|
1576
|
+
for (const [code, setting] of Object.entries(rules)) {
|
|
1577
|
+
const [level, options] = Array.isArray(setting) ? setting : [setting];
|
|
1578
|
+
if (typeof level !== "string" || !LINT_LEVELS.includes(level)) return void 0;
|
|
1579
|
+
if (options !== void 0 && (typeof options !== "object" || options === null)) return void 0;
|
|
1580
|
+
result[code] = setting;
|
|
1581
|
+
}
|
|
1582
|
+
return result;
|
|
1583
|
+
}
|
|
1584
|
+
/**
|
|
1585
|
+
* Applies the values of a config object (read from a syntax tree or from a loaded module) over
|
|
1586
|
+
* `config`, with the same checks and notes either way.
|
|
1587
|
+
*/
|
|
1588
|
+
function applyConfigValues(object, config, notes) {
|
|
1309
1589
|
const accept = (key, valid, expected) => {
|
|
1310
1590
|
if (!(key in object)) return;
|
|
1311
1591
|
const value = object[key];
|
|
@@ -1316,25 +1596,33 @@ function apply(object, config, notes) {
|
|
|
1316
1596
|
accept("locales", isStringArray, "an array of string literals");
|
|
1317
1597
|
accept("include", (v) => typeof v === "string" || isStringArray(v), "a string or array of strings");
|
|
1318
1598
|
accept("exclude", isStringArray, "an array of string literals");
|
|
1599
|
+
accept("output", (v) => typeof v === "string" && v !== "", "a string literal");
|
|
1319
1600
|
accept("fallbacks", (v) => typeof v === "object" && v !== null && Object.values(v).every((list) => isStringArray(list)), "an object of string arrays");
|
|
1320
1601
|
accept("prefixDefault", (v) => typeof v === "boolean", "true or false");
|
|
1321
1602
|
accept("lazy", (v) => typeof v === "boolean" || v === "locale", "true, false or \"locale\"");
|
|
1603
|
+
if ("lint" in object) {
|
|
1604
|
+
const rules = lintRules(object.lint);
|
|
1605
|
+
if (rules) config.lint = { rules };
|
|
1606
|
+
else notes.push("\"lint\" could not be read statically (expected { rules: { code: \"off\" | \"warn\" | \"error\" | [level, options] } }).");
|
|
1607
|
+
}
|
|
1322
1608
|
}
|
|
1323
1609
|
/** Reads a config file's source text. Exposed for tests and for unsaved buffers. */
|
|
1324
1610
|
function parseStaticConfig(text, file = "dialex.config.ts") {
|
|
1325
1611
|
const notes = [];
|
|
1326
1612
|
const config = { ...DEFAULT_STATIC_CONFIG };
|
|
1613
|
+
let opaque = false;
|
|
1327
1614
|
if (file.endsWith(".json")) {
|
|
1328
1615
|
try {
|
|
1329
1616
|
const data = JSON.parse(text);
|
|
1330
|
-
if (data && typeof data === "object")
|
|
1617
|
+
if (data && typeof data === "object") applyConfigValues(data, config, notes);
|
|
1331
1618
|
} catch (error) {
|
|
1332
1619
|
notes.push(`Could not parse ${path.basename(file)}: ${error.message}`);
|
|
1333
1620
|
}
|
|
1334
1621
|
return {
|
|
1335
1622
|
config,
|
|
1336
1623
|
file,
|
|
1337
|
-
notes
|
|
1624
|
+
notes,
|
|
1625
|
+
opaque
|
|
1338
1626
|
};
|
|
1339
1627
|
}
|
|
1340
1628
|
try {
|
|
@@ -1343,22 +1631,15 @@ function parseStaticConfig(text, file = "dialex.config.ts") {
|
|
|
1343
1631
|
if (!object) notes.push(`${path.basename(file)} does not export a config object literal.`);
|
|
1344
1632
|
else {
|
|
1345
1633
|
const values = {};
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
"include",
|
|
1350
|
-
"exclude",
|
|
1351
|
-
"fallbacks",
|
|
1352
|
-
"prefixDefault",
|
|
1353
|
-
"lazy"
|
|
1354
|
-
]) {
|
|
1355
|
-
const prop = findProp(object, key);
|
|
1634
|
+
opaque = object.properties.some((p) => p.type === "SpreadElement" || p.type === "SpreadProperty" || p.computed === true);
|
|
1635
|
+
for (const key of CONFIG_KEYS) {
|
|
1636
|
+
const prop = findProperty(object, key);
|
|
1356
1637
|
if (!prop) continue;
|
|
1357
1638
|
const value = literal(prop.value);
|
|
1358
1639
|
if (value === NOT_LITERAL) notes.push(`"${key}" could not be read statically (not a literal).`);
|
|
1359
1640
|
else values[key] = value;
|
|
1360
1641
|
}
|
|
1361
|
-
|
|
1642
|
+
applyConfigValues(values, config, notes);
|
|
1362
1643
|
}
|
|
1363
1644
|
} catch (error) {
|
|
1364
1645
|
notes.push(`Could not parse ${path.basename(file)}: ${error.message}`);
|
|
@@ -1366,16 +1647,17 @@ function parseStaticConfig(text, file = "dialex.config.ts") {
|
|
|
1366
1647
|
return {
|
|
1367
1648
|
config,
|
|
1368
1649
|
file,
|
|
1369
|
-
notes
|
|
1650
|
+
notes,
|
|
1651
|
+
opaque
|
|
1370
1652
|
};
|
|
1371
1653
|
}
|
|
1372
1654
|
/** Reads `dialex.config.*` (or `i18n.config.*`) from `root`, or from `configFile` if given. */
|
|
1373
1655
|
function readStaticConfig(root, configFile) {
|
|
1374
|
-
const
|
|
1375
|
-
for (const file of candidates) if (fs.existsSync(file)) return parseStaticConfig(fs.readFileSync(file, "utf-8"), file);
|
|
1656
|
+
for (const file of configCandidates(root, configFile)) if (fs.existsSync(file)) return parseStaticConfig(fs.readFileSync(file, "utf-8"), file);
|
|
1376
1657
|
return {
|
|
1377
1658
|
config: { ...DEFAULT_STATIC_CONFIG },
|
|
1378
|
-
notes: []
|
|
1659
|
+
notes: [],
|
|
1660
|
+
opaque: false
|
|
1379
1661
|
};
|
|
1380
1662
|
}
|
|
1381
1663
|
//#endregion
|
|
@@ -1408,7 +1690,15 @@ function configFile(from) {
|
|
|
1408
1690
|
if (fs.existsSync(from) && fs.statSync(from).isFile()) return from;
|
|
1409
1691
|
for (const candidate of [`${from}.json`, path.join(from, "tsconfig.json")]) if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate;
|
|
1410
1692
|
}
|
|
1411
|
-
/**
|
|
1693
|
+
/** A package in `extends` (`@adonisjs/tsconfig/tsconfig.app.json`), looked up from `dir` upwards. */
|
|
1694
|
+
function packageConfig(from, specifier) {
|
|
1695
|
+
for (let dir = from;; dir = path.dirname(dir)) {
|
|
1696
|
+
const found = configFile(path.join(dir, "node_modules", specifier));
|
|
1697
|
+
if (found) return found;
|
|
1698
|
+
if (path.dirname(dir) === dir) return void 0;
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
/** `moduleResolution` and `include` of a tsconfig, following `extends` (relative or a package). */
|
|
1412
1702
|
function read(file, depth = 0) {
|
|
1413
1703
|
const result = { references: [] };
|
|
1414
1704
|
let json;
|
|
@@ -1418,8 +1708,12 @@ function read(file, depth = 0) {
|
|
|
1418
1708
|
return result;
|
|
1419
1709
|
}
|
|
1420
1710
|
const dir = path.dirname(file);
|
|
1421
|
-
const
|
|
1422
|
-
|
|
1711
|
+
const parents = Array.isArray(json.extends) ? json.extends : [json.extends];
|
|
1712
|
+
for (const extendsFrom of parents) {
|
|
1713
|
+
if (typeof extendsFrom !== "string" || depth >= 5) continue;
|
|
1714
|
+
const parent = extendsFrom.startsWith(".") ? configFile(path.resolve(dir, extendsFrom)) : packageConfig(dir, extendsFrom);
|
|
1715
|
+
if (parent) Object.assign(result, read(parent, depth + 1));
|
|
1716
|
+
}
|
|
1423
1717
|
const options = json.compilerOptions ?? {};
|
|
1424
1718
|
const module = typeof options.module === "string" ? options.module.toLowerCase() : void 0;
|
|
1425
1719
|
const resolution = typeof options.moduleResolution === "string" ? options.moduleResolution.toLowerCase() : module === "node16" || module === "nodenext" ? module : void 0;
|
|
@@ -1445,7 +1739,7 @@ function relativeImportExtension(root) {
|
|
|
1445
1739
|
return resolution === "node16" || resolution === "nodenext" ? ".js" : "";
|
|
1446
1740
|
}
|
|
1447
1741
|
//#endregion
|
|
1448
|
-
//#region src/commands/generate.ts
|
|
1742
|
+
//#region src/commands/generate/render.ts
|
|
1449
1743
|
/** The folder, next to `dialex.generated.ts`, that holds the per-locale modules. */
|
|
1450
1744
|
const LOCALES_DIR = "dialex.locales";
|
|
1451
1745
|
const safeName = (text) => text.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
@@ -1462,7 +1756,8 @@ function renderGenerated(root, options = {}) {
|
|
|
1462
1756
|
const config = options.static ? staticRead.config : resolveDialexConfig(root, inlineConfig);
|
|
1463
1757
|
const found = scanFiles(root, config.include || DEFAULT_CONFIG.include, { exclude: config.exclude }).sort();
|
|
1464
1758
|
const srcDir = fs.existsSync(path.join(root, "src")) ? path.join(root, "src") : root;
|
|
1465
|
-
const
|
|
1759
|
+
const outputOption = options.output ?? config.output;
|
|
1760
|
+
const outputPath = outputOption ? path.resolve(root, outputOption) : path.join(srcDir, "dialex.generated.ts");
|
|
1466
1761
|
const outputDir = path.dirname(outputPath);
|
|
1467
1762
|
const files = found.filter((file) => file !== outputPath && !file.endsWith(".d.ts") && path.basename(file) !== "dialex.generated.ts");
|
|
1468
1763
|
const parsed = files.map((file) => parseDictionaryText(file, fs.readFileSync(file, "utf-8")));
|
|
@@ -1552,18 +1847,70 @@ export const dialex = { dictionaries, config };
|
|
|
1552
1847
|
export default dictionaries;
|
|
1553
1848
|
`;
|
|
1554
1849
|
}
|
|
1555
|
-
const dts = renderDts(root, files, locales, messageArguments(parsed, clientConfig.defaultLocale));
|
|
1850
|
+
const dts = renderDts(root, files, locales, messageArguments(parsed, clientConfig.defaultLocale), outputOption ? outputDir : void 0);
|
|
1556
1851
|
return {
|
|
1557
1852
|
files,
|
|
1558
1853
|
outputPath,
|
|
1559
1854
|
content,
|
|
1560
|
-
dtsPath: dts?.path ?? path.join(srcDir, "dialex-env.d.ts"),
|
|
1855
|
+
dtsPath: dts?.path ?? path.join(outputOption ? outputDir : srcDir, "dialex-env.d.ts"),
|
|
1561
1856
|
dtsContent: dts?.content,
|
|
1562
1857
|
configFile: staticRead.file,
|
|
1563
1858
|
extraFiles,
|
|
1564
1859
|
notes
|
|
1565
1860
|
};
|
|
1566
1861
|
}
|
|
1862
|
+
//#endregion
|
|
1863
|
+
//#region src/commands/generate/watch.ts
|
|
1864
|
+
const WATCH_IGNORED = /(^|[\\/])(node_modules|dist|\.next|\.nuxt|\.output|\.turbo|\.git|dialex\.locales)([\\/]|$)/;
|
|
1865
|
+
const CONFIG_FILE = /(^|[\\/])(dialex|i18n)\.config\.[cm]?[jt]s(on)?$/;
|
|
1866
|
+
/**
|
|
1867
|
+
* Regenerates when a dictionary or config file is added, edited or removed. chokidar 4+ no longer
|
|
1868
|
+
* understands globs, so this watches the project directory and decides for itself which events
|
|
1869
|
+
* matter, using the same `include` glob as `generate`.
|
|
1870
|
+
*/
|
|
1871
|
+
function startGenerateWatcher(root, options, regenerate, delay = 100, log = (message) => logger.info(message)) {
|
|
1872
|
+
const dictionaryFiles = () => new Set(renderGenerated(root, options).files);
|
|
1873
|
+
let known = dictionaryFiles();
|
|
1874
|
+
let outputs = /* @__PURE__ */ new Set();
|
|
1875
|
+
let timer;
|
|
1876
|
+
const trigger = () => {
|
|
1877
|
+
if (timer) clearTimeout(timer);
|
|
1878
|
+
timer = setTimeout(() => {
|
|
1879
|
+
regenerate();
|
|
1880
|
+
try {
|
|
1881
|
+
known = dictionaryFiles();
|
|
1882
|
+
const rendered = renderGenerated(root, options);
|
|
1883
|
+
outputs = /* @__PURE__ */ new Set([rendered.outputPath, rendered.dtsPath]);
|
|
1884
|
+
} catch {}
|
|
1885
|
+
}, delay);
|
|
1886
|
+
};
|
|
1887
|
+
const gitIgnored = createGitignoreFilter(root);
|
|
1888
|
+
const watcher = chokidar.watch(root, {
|
|
1889
|
+
ignoreInitial: true,
|
|
1890
|
+
ignored: (file) => WATCH_IGNORED.test(file) || gitIgnored(path.resolve(root, file))
|
|
1891
|
+
});
|
|
1892
|
+
watcher.on("all", (event, filePath) => {
|
|
1893
|
+
const file = path.resolve(root, filePath);
|
|
1894
|
+
if (outputs.has(file)) return;
|
|
1895
|
+
const isConfig = CONFIG_FILE.test(file);
|
|
1896
|
+
if (event === "change") {
|
|
1897
|
+
if (!known.has(file) && !isConfig) return;
|
|
1898
|
+
} else if (event === "add" || event === "unlink") {
|
|
1899
|
+
let now;
|
|
1900
|
+
try {
|
|
1901
|
+
now = dictionaryFiles();
|
|
1902
|
+
} catch {
|
|
1903
|
+
return;
|
|
1904
|
+
}
|
|
1905
|
+
if (!(now.size !== known.size || [...now].some((f) => !known.has(f))) && !isConfig) return;
|
|
1906
|
+
} else return;
|
|
1907
|
+
log(`[${event}] ${path.relative(root, file)} changed, regenerating...`);
|
|
1908
|
+
trigger();
|
|
1909
|
+
});
|
|
1910
|
+
return watcher;
|
|
1911
|
+
}
|
|
1912
|
+
//#endregion
|
|
1913
|
+
//#region src/commands/generate/write.ts
|
|
1567
1914
|
function writeIfChanged(file, content) {
|
|
1568
1915
|
if ((fs.existsSync(file) ? fs.readFileSync(file, "utf-8") : "") === content) return;
|
|
1569
1916
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
@@ -1591,6 +1938,8 @@ function generateDictionaries(root, options = {}) {
|
|
|
1591
1938
|
notes: rendered.notes
|
|
1592
1939
|
};
|
|
1593
1940
|
}
|
|
1941
|
+
//#endregion
|
|
1942
|
+
//#region src/commands/generate/index.ts
|
|
1594
1943
|
async function runGenerate(options = {}) {
|
|
1595
1944
|
const root = options.cwd || process.cwd();
|
|
1596
1945
|
const quiet = options.json === true;
|
|
@@ -1627,54 +1976,6 @@ async function runGenerate(options = {}) {
|
|
|
1627
1976
|
startGenerateWatcher(root, options, doGenerate);
|
|
1628
1977
|
}
|
|
1629
1978
|
}
|
|
1630
|
-
const WATCH_IGNORED = /(^|[\\/])(node_modules|dist|\.next|\.nuxt|\.output|\.turbo|\.git|dialex\.locales)([\\/]|$)/;
|
|
1631
|
-
const CONFIG_FILE = /(^|[\\/])(dialex|i18n)\.config\.[cm]?[jt]s(on)?$/;
|
|
1632
|
-
/**
|
|
1633
|
-
* Regenerates when a dictionary or config file is added, edited or removed. chokidar 4+ no longer
|
|
1634
|
-
* understands globs, so this watches the project directory and decides for itself which events
|
|
1635
|
-
* matter, using the same `include` glob as `generate`.
|
|
1636
|
-
*/
|
|
1637
|
-
function startGenerateWatcher(root, options, regenerate, delay = 100, log = (message) => logger.info(message)) {
|
|
1638
|
-
const dictionaryFiles = () => new Set(renderGenerated(root, options).files);
|
|
1639
|
-
let known = dictionaryFiles();
|
|
1640
|
-
let outputs = /* @__PURE__ */ new Set();
|
|
1641
|
-
let timer;
|
|
1642
|
-
const trigger = () => {
|
|
1643
|
-
if (timer) clearTimeout(timer);
|
|
1644
|
-
timer = setTimeout(() => {
|
|
1645
|
-
regenerate();
|
|
1646
|
-
try {
|
|
1647
|
-
known = dictionaryFiles();
|
|
1648
|
-
const rendered = renderGenerated(root, options);
|
|
1649
|
-
outputs = /* @__PURE__ */ new Set([rendered.outputPath, rendered.dtsPath]);
|
|
1650
|
-
} catch {}
|
|
1651
|
-
}, delay);
|
|
1652
|
-
};
|
|
1653
|
-
const gitIgnored = createGitignoreFilter(root);
|
|
1654
|
-
const watcher = chokidar.watch(root, {
|
|
1655
|
-
ignoreInitial: true,
|
|
1656
|
-
ignored: (file) => WATCH_IGNORED.test(file) || gitIgnored(path.resolve(root, file))
|
|
1657
|
-
});
|
|
1658
|
-
watcher.on("all", (event, filePath) => {
|
|
1659
|
-
const file = path.resolve(root, filePath);
|
|
1660
|
-
if (outputs.has(file)) return;
|
|
1661
|
-
const isConfig = CONFIG_FILE.test(file);
|
|
1662
|
-
if (event === "change") {
|
|
1663
|
-
if (!known.has(file) && !isConfig) return;
|
|
1664
|
-
} else if (event === "add" || event === "unlink") {
|
|
1665
|
-
let now;
|
|
1666
|
-
try {
|
|
1667
|
-
now = dictionaryFiles();
|
|
1668
|
-
} catch {
|
|
1669
|
-
return;
|
|
1670
|
-
}
|
|
1671
|
-
if (!(now.size !== known.size || [...now].some((f) => !known.has(f))) && !isConfig) return;
|
|
1672
|
-
} else return;
|
|
1673
|
-
log(`[${event}] ${path.relative(root, file)} changed, regenerating...`);
|
|
1674
|
-
trigger();
|
|
1675
|
-
});
|
|
1676
|
-
return watcher;
|
|
1677
|
-
}
|
|
1678
1979
|
//#endregion
|
|
1679
1980
|
//#region src/utils/project.ts
|
|
1680
1981
|
async function loadDictionaries(root, config) {
|
|
@@ -1729,6 +2030,6 @@ async function loadStaticProject(root, configFile) {
|
|
|
1729
2030
|
/** `home` + `["nav", "about"]` -> `home.nav.about` */
|
|
1730
2031
|
const toKey = (dictionary, keyPath) => [dictionary, ...keyPath].join(".");
|
|
1731
2032
|
//#endregion
|
|
1732
|
-
export { scanFiles as A,
|
|
2033
|
+
export { scanFiles as A, listLeaves as B, lockPath as C, BUILTIN_IGNORE as D, writeLock as E, locationOf as F, saveDictionaryFile as G, loadDictionaryFile as H, copyLeaf as I, TODO_PREFIX as K, setString as L, createAnalysisCache as M, dictionaryLocation as N, SOURCE_BUILTIN_IGNORE as O, getLeafSource as P, getString as R, lockFor as S, record as T, parseDictionaryText as U, listLocales as V, renderDictionaryFile as W, scanReferences as _, generateDictionaries as a, hashSource as b, DEFAULT_STATIC_CONFIG as c, resolveDialexConfig as d, logger as f, DEFAULT_SOURCE_GLOB as g, relativeFile as h, runGenerate as i, clearGitignore as j, createGitignoreFilter as k, parseStaticConfig as l, analyzeProjectSync as m, loadStaticProject as n, startGenerateWatcher as o, analyzeProject as p, toKey as r, renderGenerated as s, loadProject as t, readStaticConfig as u, scanText as v, readLock as w, isStale as x, LOCK_FILE as y, hasPath as z };
|
|
1733
2034
|
|
|
1734
|
-
//# sourceMappingURL=project-
|
|
2035
|
+
//# sourceMappingURL=project-BTpZGNjh.mjs.map
|