@dialexjs/cli 0.5.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,15 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { renderDts, resolveDialexConfig } from "dialexjs/scanner";
4
- import fg from "fast-glob";
5
3
  import pc from "picocolors";
6
4
  import { pathToFileURL } from "node:url";
7
5
  import { generateCode, parseExpression, parseModule, writeFile } from "magicast";
6
+ import fg from "fast-glob";
7
+ import ignore from "ignore";
8
8
  import { argumentSignature, findPlurals, getArguments, isIcuStructured, parseMessage } from "dialexjs/icu";
9
9
  import { createHash } from "node:crypto";
10
- import chokidar from "chokidar";
11
10
  import { DEFAULT_CONFIG } from "dialexjs";
11
+ import { loadConfigSync } from "unconfig";
12
+ import chokidar from "chokidar";
12
13
  //#region src/utils/dictionary-edit.ts
13
14
  const TODO_PREFIX = "[TODO] ";
14
15
  const IDENTIFIER$1 = /^[A-Za-z_$][\w$]*$/;
@@ -272,6 +273,100 @@ function getLeafSource(df, locale, path) {
272
273
  return generateCode(prop.type === "ObjectMethod" ? prop : prop.value).code;
273
274
  }
274
275
  //#endregion
276
+ //#region src/utils/files.ts
277
+ /** Folders that never hold project dictionaries or sources, in every scanner. */
278
+ const BUILTIN_IGNORE = [
279
+ "**/node_modules/**",
280
+ "**/dist/**",
281
+ "**/.next/**",
282
+ "**/dialex.locales/**"
283
+ ];
284
+ /** What the source scan skips on top: other tools' output, declarations, generated and config files. */
285
+ const SOURCE_BUILTIN_IGNORE = [
286
+ ...BUILTIN_IGNORE,
287
+ "**/.nuxt/**",
288
+ "**/.output/**",
289
+ "**/.svelte-kit/**",
290
+ "**/.astro/**",
291
+ "**/.turbo/**",
292
+ "**/coverage/**",
293
+ "**/graphify-out/**",
294
+ "**/*.d.ts",
295
+ "**/dialex.generated.*",
296
+ "**/dialex.config.*",
297
+ "**/i18n.config.*"
298
+ ];
299
+ /** The nearest directory at or above `root` that contains `.git`, or `root` itself. */
300
+ function repositoryRoot(root) {
301
+ for (let dir = root;; dir = path.dirname(dir)) {
302
+ if (fs.existsSync(path.join(dir, ".git"))) return dir;
303
+ if (path.dirname(dir) === dir) return root;
304
+ }
305
+ }
306
+ function readScope(dir) {
307
+ try {
308
+ const text = fs.readFileSync(path.join(dir, ".gitignore"), "utf-8");
309
+ return {
310
+ dir,
311
+ matcher: ignore().add(text)
312
+ };
313
+ } catch {
314
+ return;
315
+ }
316
+ }
317
+ /**
318
+ * A function that says whether an absolute path is ignored by Git. It reads the `.gitignore`
319
+ * files of the project folder, of every folder above it up to the repository root (monorepo
320
+ * packages inherit the root's rules), and of nested folders.
321
+ */
322
+ function createGitignoreFilter(root) {
323
+ const base = path.resolve(root);
324
+ const top = repositoryRoot(base);
325
+ const scopes = [];
326
+ for (let dir = base;; dir = path.dirname(dir)) {
327
+ const scope = readScope(dir);
328
+ if (scope) scopes.push(scope);
329
+ if (dir === top || path.dirname(dir) === dir) break;
330
+ }
331
+ const nested = fg.sync("**/.gitignore", {
332
+ cwd: base,
333
+ absolute: true,
334
+ dot: true,
335
+ ignore: ["**/node_modules/**", "**/.git/**"]
336
+ });
337
+ for (const file of nested) {
338
+ const dir = path.dirname(file);
339
+ if (dir === base) continue;
340
+ const scope = readScope(dir);
341
+ if (scope) scopes.push(scope);
342
+ }
343
+ if (scopes.length === 0) return () => false;
344
+ return (absolutePath) => {
345
+ for (const { dir, matcher } of scopes) {
346
+ const relative = path.relative(dir, absolutePath);
347
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) continue;
348
+ if (matcher.ignores(relative.split(path.sep).join("/"))) return true;
349
+ }
350
+ return false;
351
+ };
352
+ }
353
+ /** Finds files under `root`, skipping build output, configured excludes and Git-ignored paths. */
354
+ function scanFiles(root, patterns, options = {}) {
355
+ const builtin = options.kind === "source" ? SOURCE_BUILTIN_IGNORE : BUILTIN_IGNORE;
356
+ const files = fg.sync(patterns, {
357
+ cwd: root,
358
+ absolute: true,
359
+ ignore: [
360
+ ...builtin,
361
+ ...options.exclude ?? [],
362
+ ...options.ignore ?? []
363
+ ]
364
+ });
365
+ if (options.gitignore === false) return files;
366
+ const ignored = createGitignoreFilter(root);
367
+ return files.filter((file) => !ignored(file));
368
+ }
369
+ //#endregion
275
370
  //#region src/utils/icu-check.ts
276
371
  /** CLDR order. `resolvedOptions().pluralCategories` order differs between Node versions. */
277
372
  const CATEGORY_ORDER = [
@@ -423,26 +518,7 @@ function lockFor(root, sourceLocale) {
423
518
  }
424
519
  //#endregion
425
520
  //#region src/analysis.ts
426
- const DICTIONARY_IGNORE$1 = [
427
- "**/node_modules/**",
428
- "**/dist/**",
429
- "**/.next/**",
430
- "**/dialex.locales/**"
431
- ];
432
521
  const DEFAULT_SOURCE_GLOB = "**/*.{ts,tsx,js,jsx,mjs,cjs,vue,svelte,astro,mdx}";
433
- const SOURCE_IGNORE = [
434
- ...DICTIONARY_IGNORE$1,
435
- "**/.nuxt/**",
436
- "**/.output/**",
437
- "**/.svelte-kit/**",
438
- "**/.astro/**",
439
- "**/coverage/**",
440
- "**/graphify-out/**",
441
- "**/*.d.ts",
442
- "**/dialex.generated.*",
443
- "**/dialex.config.*",
444
- "**/i18n.config.*"
445
- ];
446
522
  /** `getDictionary("x")`, `useDictionary("x")`, `@DialexDictionary("x")` */
447
523
  const DICTIONARY_CALL = /\b(?:getDictionary|useDictionary|DialexDictionary)\(\s*(["'`])([^"'`$\n]+)\1/dg;
448
524
  /** `t("dictionary.path.to.key")` */
@@ -583,10 +659,9 @@ async function analyzeProject(options) {
583
659
  const configuredLocales = config.locales || [];
584
660
  const defaultLocale = config.defaultLocale || "en";
585
661
  const extraIgnore = options.ignore ?? [];
586
- const files = fg.sync(include, {
587
- cwd: root,
588
- absolute: true,
589
- ignore: [...DICTIONARY_IGNORE$1, ...extraIgnore]
662
+ const files = scanFiles(root, include, {
663
+ exclude: config.exclude,
664
+ ignore: extraIgnore
590
665
  });
591
666
  const issues = [];
592
667
  const dictionaries = [];
@@ -699,10 +774,10 @@ async function analyzeProject(options) {
699
774
  const known = new Map(dictionaries.map((d) => [d.name, d]));
700
775
  const dictionaryFiles = new Set(files);
701
776
  const sourceGlobs = options.src ?? ["**/*.{ts,tsx,js,jsx,mjs,cjs,vue,svelte,astro,mdx}"];
702
- const sourceFiles = sourceGlobs.length === 0 ? [] : fg.sync(sourceGlobs, {
703
- cwd: root,
704
- absolute: true,
705
- ignore: [...SOURCE_IGNORE, ...extraIgnore]
777
+ const sourceFiles = sourceGlobs.length === 0 ? [] : scanFiles(root, sourceGlobs, {
778
+ kind: "source",
779
+ exclude: config.exclude,
780
+ ignore: extraIgnore
706
781
  }).filter((f) => !dictionaryFiles.has(f));
707
782
  if (sourceFiles.length > 0) {
708
783
  const tokens = /* @__PURE__ */ new Set();
@@ -795,6 +870,157 @@ const logger = {
795
870
  log: (msg) => console.log(msg)
796
871
  };
797
872
  //#endregion
873
+ //#region src/utils/scanner.ts
874
+ function resolveDialexConfig(root, inlineConfig = {}) {
875
+ let loadedConfig = {};
876
+ try {
877
+ const sources = [];
878
+ if (inlineConfig.configFile) sources.push({ files: inlineConfig.configFile });
879
+ sources.push({
880
+ files: "dialex.config",
881
+ extensions: [
882
+ "ts",
883
+ "mts",
884
+ "cts",
885
+ "js",
886
+ "mjs",
887
+ "cjs",
888
+ "json",
889
+ ""
890
+ ]
891
+ }, {
892
+ files: "i18n.config",
893
+ extensions: [
894
+ "ts",
895
+ "mts",
896
+ "cts",
897
+ "js",
898
+ "mjs",
899
+ "cjs",
900
+ "json",
901
+ ""
902
+ ]
903
+ });
904
+ const result = loadConfigSync({
905
+ sources,
906
+ merge: false,
907
+ defaults: {},
908
+ cwd: root
909
+ });
910
+ if (result && result.config) loadedConfig = result.config;
911
+ } catch {}
912
+ return {
913
+ ...DEFAULT_CONFIG,
914
+ ...loadedConfig,
915
+ ...inlineConfig
916
+ };
917
+ }
918
+ /** Every adapter entry point that has its own `DictionaryRegistry` to augment. */
919
+ const REGISTRY_ADAPTERS = [
920
+ "react",
921
+ "server",
922
+ "hono",
923
+ "express",
924
+ "fastify",
925
+ "koa",
926
+ "nestjs",
927
+ "elysia",
928
+ "sveltekit",
929
+ "astro",
930
+ "vue",
931
+ "web",
932
+ "h3",
933
+ "angular"
934
+ ];
935
+ /** Dependencies that tell which `dialexjs/*` entry points a project can import. */
936
+ const ADAPTER_DEPENDENCIES = {
937
+ react: ["react"],
938
+ "react-dom": ["react"],
939
+ next: ["react", "server"],
940
+ hono: ["hono"],
941
+ express: ["express"],
942
+ fastify: ["fastify"],
943
+ koa: ["koa"],
944
+ "@nestjs/core": ["nestjs"],
945
+ "@nestjs/common": ["nestjs"],
946
+ elysia: ["elysia"],
947
+ "@sveltejs/kit": ["sveltekit"],
948
+ astro: ["astro"],
949
+ vue: ["vue"],
950
+ nuxt: ["vue", "h3"],
951
+ h3: ["h3"],
952
+ nitropack: ["h3"],
953
+ "@angular/core": ["angular"],
954
+ "react-router": ["web"],
955
+ "react-router-dom": ["web"],
956
+ "@remix-run/react": ["web"],
957
+ "@remix-run/node": ["web"],
958
+ "@tanstack/react-start": ["web"],
959
+ "@solidjs/start": ["web"],
960
+ wrangler: ["web"]
961
+ };
962
+ /**
963
+ * The adapter entry points to augment for a project, read from its `package.json`. Falls back to
964
+ * all of them when nothing is recognised, so types never silently go missing.
965
+ */
966
+ function detectAdapters(root) {
967
+ try {
968
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf-8"));
969
+ const deps = {
970
+ ...pkg.dependencies,
971
+ ...pkg.devDependencies,
972
+ ...pkg.peerDependencies
973
+ };
974
+ const found = /* @__PURE__ */ new Set();
975
+ for (const name of Object.keys(deps)) for (const adapter of ADAPTER_DEPENDENCIES[name] ?? []) found.add(adapter);
976
+ if (found.size > 0) return REGISTRY_ADAPTERS.filter((adapter) => found.has(adapter));
977
+ } catch {}
978
+ return [...REGISTRY_ADAPTERS];
979
+ }
980
+ /**
981
+ * The `dialex-env.d.ts` a project should have: its path and contents, or `undefined` when there
982
+ * are no dictionary files. Nothing is written.
983
+ */
984
+ function renderDts(root, files, locales, messageArguments = {}) {
985
+ if (files.length === 0) return void 0;
986
+ const dtsDir = fs.existsSync(path.join(root, "src")) ? path.join(root, "src") : root;
987
+ const imports = [];
988
+ const records = [];
989
+ files.forEach((file, index) => {
990
+ let relPath = path.relative(dtsDir, file).replace(/\\/g, "/");
991
+ relPath = relPath.replace(/\.ts$/, "");
992
+ if (!relPath.startsWith(".")) relPath = "./" + relPath;
993
+ imports.push(`import dict${index} from '${relPath}';`);
994
+ records.push(`Record<typeof dict${index}['name'], typeof dict${index}['dictionary'][keyof typeof dict${index}['dictionary']]>`);
995
+ });
996
+ const localesType = locales && locales.length > 0 ? locales.map((l) => `"${l}"`).join(" | ") : "string";
997
+ const registry = records.join(",\n ");
998
+ const messageEntries = Object.entries(messageArguments).sort(([a], [b]) => a.localeCompare(b)).map(([key, type]) => ` ${JSON.stringify(key)}: ${type};`).join("\n");
999
+ const adapterBlocks = detectAdapters(root).map((adapter) => `
1000
+ declare module 'dialexjs/${adapter}' {
1001
+ export interface DictionaryRegistry extends ${registry} {}
1002
+ }
1003
+ `).join("");
1004
+ const dtsContent = `/* eslint-disable */
1005
+ // @ts-nocheck
1006
+ // Generated by dialex
1007
+ ${imports.join("\n")}
1008
+
1009
+ declare module 'dialexjs' {
1010
+ export interface Register {
1011
+ locales: ${localesType};
1012
+ }
1013
+ export interface DictionaryRegistry extends ${records.join(",\n ")} {}
1014
+ export interface MessageArguments {${messageEntries ? `\n${messageEntries}\n ` : ""}}
1015
+ }
1016
+ ${adapterBlocks}
1017
+ `;
1018
+ return {
1019
+ path: path.join(dtsDir, "dialex-env.d.ts"),
1020
+ content: dtsContent
1021
+ };
1022
+ }
1023
+ //#endregion
798
1024
  //#region src/utils/message-arguments.ts
799
1025
  const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
800
1026
  /**
@@ -981,6 +1207,7 @@ const DEFAULT_STATIC_CONFIG = {
981
1207
  defaultLocale: "en",
982
1208
  locales: [],
983
1209
  include: "**/*.content.ts",
1210
+ exclude: [],
984
1211
  fallbacks: {},
985
1212
  prefixDefault: true,
986
1213
  lazy: false
@@ -1028,6 +1255,7 @@ function apply(object, config, notes) {
1028
1255
  accept("defaultLocale", (v) => typeof v === "string" && v !== "", "a string literal");
1029
1256
  accept("locales", isStringArray, "an array of string literals");
1030
1257
  accept("include", (v) => typeof v === "string" || isStringArray(v), "a string or array of strings");
1258
+ accept("exclude", isStringArray, "an array of string literals");
1031
1259
  accept("fallbacks", (v) => typeof v === "object" && v !== null && Object.values(v).every((list) => isStringArray(list)), "an object of string arrays");
1032
1260
  accept("prefixDefault", (v) => typeof v === "boolean", "true or false");
1033
1261
  accept("lazy", (v) => typeof v === "boolean" || v === "locale", "true, false or \"locale\"");
@@ -1059,6 +1287,7 @@ function parseStaticConfig(text, file = "dialex.config.ts") {
1059
1287
  "defaultLocale",
1060
1288
  "locales",
1061
1289
  "include",
1290
+ "exclude",
1062
1291
  "fallbacks",
1063
1292
  "prefixDefault",
1064
1293
  "lazy"
@@ -1157,12 +1386,6 @@ function relativeImportExtension(root) {
1157
1386
  }
1158
1387
  //#endregion
1159
1388
  //#region src/commands/generate.ts
1160
- const DICTIONARY_IGNORE = [
1161
- "**/node_modules/**",
1162
- "**/dist/**",
1163
- "**/.next/**",
1164
- "**/dialex.locales/**"
1165
- ];
1166
1389
  /** The folder, next to `dialex.generated.ts`, that holds the per-locale modules. */
1167
1390
  const LOCALES_DIR = "dialex.locales";
1168
1391
  const safeName = (text) => text.replace(/[^A-Za-z0-9._-]/g, "_");
@@ -1177,12 +1400,7 @@ function renderGenerated(root, options = {}) {
1177
1400
  const inlineConfig = options.config ? { configFile: options.config } : {};
1178
1401
  const staticRead = readStaticConfig(root, options.config);
1179
1402
  const config = options.static ? staticRead.config : resolveDialexConfig(root, inlineConfig);
1180
- const include = config.include || DEFAULT_CONFIG.include;
1181
- const found = fg.sync(include, {
1182
- cwd: root,
1183
- absolute: true,
1184
- ignore: DICTIONARY_IGNORE
1185
- }).sort();
1403
+ const found = scanFiles(root, config.include || DEFAULT_CONFIG.include, { exclude: config.exclude }).sort();
1186
1404
  const srcDir = fs.existsSync(path.join(root, "src")) ? path.join(root, "src") : root;
1187
1405
  const outputPath = options.output ? path.resolve(root, options.output) : path.join(srcDir, "dialex.generated.ts");
1188
1406
  const outputDir = path.dirname(outputPath);
@@ -1372,9 +1590,10 @@ function startGenerateWatcher(root, options, regenerate, delay = 100) {
1372
1590
  } catch {}
1373
1591
  }, delay);
1374
1592
  };
1593
+ const gitIgnored = createGitignoreFilter(root);
1375
1594
  const watcher = chokidar.watch(root, {
1376
1595
  ignoreInitial: true,
1377
- ignored: (file) => WATCH_IGNORED.test(file)
1596
+ ignored: (file) => WATCH_IGNORED.test(file) || gitIgnored(path.resolve(root, file))
1378
1597
  });
1379
1598
  watcher.on("all", (event, filePath) => {
1380
1599
  const file = path.resolve(root, filePath);
@@ -1399,17 +1618,7 @@ function startGenerateWatcher(root, options, regenerate, delay = 100) {
1399
1618
  //#endregion
1400
1619
  //#region src/utils/project.ts
1401
1620
  async function loadDictionaries(root, config) {
1402
- const include = config.include || "**/*.content.ts";
1403
- const files = fg.sync(include, {
1404
- cwd: root,
1405
- absolute: true,
1406
- ignore: [
1407
- "**/node_modules/**",
1408
- "**/dist/**",
1409
- "**/.next/**",
1410
- "**/dialex.locales/**"
1411
- ]
1412
- });
1621
+ const files = scanFiles(root, config.include || "**/*.content.ts", { exclude: config.exclude });
1413
1622
  const dictionaries = [];
1414
1623
  const unsupported = [];
1415
1624
  for (const file of files) {
@@ -1460,6 +1669,6 @@ async function loadStaticProject(root, configFile) {
1460
1669
  /** `home` + `["nav", "about"]` -> `home.nav.about` */
1461
1670
  const toKey = (dictionary, keyPath) => [dictionary, ...keyPath].join(".");
1462
1671
  //#endregion
1463
- export { listLocales as A, TODO_PREFIX as C, getString as D, getLeafSource as E, saveDictionaryFile as F, setString as I, locationOf as M, parseDictionaryText as N, hasPath as O, renderDictionaryFile as P, writeLock as S, dictionaryLocation as T, isStale as _, renderGenerated as a, readLock as b, parseStaticConfig as c, DEFAULT_SOURCE_GLOB as d, analyzeProject as f, hashSource as g, LOCK_FILE as h, generateDictionaries as i, loadDictionaryFile as j, listLeaves as k, readStaticConfig as l, scanReferences as m, loadStaticProject as n, runGenerate as o, relativeFile as p, toKey as r, DEFAULT_STATIC_CONFIG as s, loadProject as t, logger as u, lockFor as v, copyLeaf as w, record as x, lockPath as y };
1672
+ export { dictionaryLocation as A, saveDictionaryFile as B, writeLock as C, scanFiles as D, createGitignoreFilter as E, listLocales as F, loadDictionaryFile as I, locationOf as L, getString as M, hasPath as N, TODO_PREFIX as O, listLeaves as P, parseDictionaryText as R, record as S, SOURCE_BUILTIN_IGNORE as T, setString as V, hashSource as _, renderGenerated as a, lockPath as b, parseStaticConfig as c, logger as d, DEFAULT_SOURCE_GLOB as f, LOCK_FILE as g, scanReferences as h, generateDictionaries as i, getLeafSource as j, copyLeaf as k, readStaticConfig as l, relativeFile as m, loadStaticProject as n, runGenerate as o, analyzeProject as p, toKey as r, DEFAULT_STATIC_CONFIG as s, loadProject as t, resolveDialexConfig as u, isStale as v, BUILTIN_IGNORE as w, readLock as x, lockFor as y, renderDictionaryFile as z };
1464
1673
 
1465
- //# sourceMappingURL=project-BJYmnKiU.mjs.map
1674
+ //# sourceMappingURL=project-DBVeKLa3.mjs.map