@x18n/node 0.0.0 → 0.0.3

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.
Files changed (2) hide show
  1. package/dist/index.mjs +258 -127
  2. package/package.json +3 -3
package/dist/index.mjs CHANGED
@@ -1,16 +1,16 @@
1
1
  import { createRequire } from "node:module";
2
- import { existsSync } from "node:fs";
2
+ import { existsSync, readFileSync } from "node:fs";
3
3
  import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
4
4
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
- import { pathToFileURL } from "node:url";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  import { compileYamlPlugin } from "@x18n/builtins/yaml-plugin";
7
7
  import { PluginFileSchema, X18nError, createProject, hashText, isX18nError, resolveConfig, validate } from "@x18n/core";
8
8
  import { createJiti } from "jiti";
9
+ import { glob } from "tinyglobby";
9
10
  import { parse, stringify } from "yaml";
10
11
  import process from "node:process";
11
12
  import { watch } from "chokidar";
12
13
  import ignore from "ignore";
13
- import { glob } from "tinyglobby";
14
14
  import { createBuiltins } from "@x18n/builtins";
15
15
  import { applyEdits, modify, parse as parse$1 } from "jsonc-parser";
16
16
  //#region src/config.ts
@@ -41,9 +41,17 @@ function findConfigUpward(cwd) {
41
41
  dir = parent;
42
42
  }
43
43
  }
44
+ const core = fileURLToPath(import.meta.resolve("@x18n/core"));
45
+ const presets = fileURLToPath(import.meta.resolve("@x18n/builtins/presets"));
44
46
  const jiti = createJiti(import.meta.url, {
45
47
  moduleCache: false,
46
- interopDefault: true
48
+ interopDefault: true,
49
+ alias: {
50
+ "@x18n/core": core,
51
+ "@x18n/cli/presets": presets,
52
+ "@x18n/cli": core,
53
+ "@x18n/builtins/presets": presets
54
+ }
47
55
  });
48
56
  /** Imports a JS or TS module (config, plugin, executed resource). */
49
57
  async function importModule(file) {
@@ -91,16 +99,36 @@ function createPluginLoader(dir) {
91
99
  }
92
100
  };
93
101
  }
94
- /** Paths from `root` up to the git root, relative to `root` (`..`, `../..`), whose manifests count for detection. */
95
- function manifestRootsOf(root, gitRoot) {
102
+ /** Workspace package directories of `root`, from `pnpm-workspace.yaml` or the `workspaces` field of `package.json`. */
103
+ async function workspacePackages(root) {
104
+ const pnpm = parse(await readFile(join(root, "pnpm-workspace.yaml"), "utf8").catch(() => "") || "{}");
105
+ const manifest = JSON.parse(await readFile(join(root, "package.json"), "utf8").catch(() => "{}"));
106
+ const field = (value, key) => typeof value === "object" && value !== null && key in value ? new Map(Object.entries(value)).get(key) : void 0;
107
+ const workspaces = field(manifest, "workspaces");
108
+ const patterns = [
109
+ field(pnpm, "packages"),
110
+ workspaces,
111
+ field(workspaces, "packages")
112
+ ].flatMap((list) => Array.isArray(list) ? list.filter((item) => typeof item === "string") : []);
113
+ if (patterns.length === 0) return [];
114
+ return (await glob(patterns.filter((p) => !p.startsWith("!")).map((p) => `${p.replace(/\/$/, "")}/package.json`), {
115
+ cwd: root,
116
+ ignore: ["**/node_modules/**", ...patterns.filter((p) => p.startsWith("!")).map((p) => p.slice(1))]
117
+ })).map((file) => dirname(file)).sort();
118
+ }
119
+ /**
120
+ * Directories, relative to `root`, whose manifests count for preset detection: the parents up to the
121
+ * git root (`..`, `../..`), and the workspace packages of a monorepo root, where the i18n library is often listed.
122
+ */
123
+ async function manifestRootsOf(root, gitRoot) {
96
124
  const out = [];
97
- if (!gitRoot) return out;
125
+ const top = gitRoot ?? resolve(root);
98
126
  let dir = resolve(root);
99
- while (dir !== gitRoot && dir.startsWith(gitRoot)) {
127
+ while (dir !== top && dir.startsWith(top)) {
100
128
  dir = dirname(dir);
101
129
  out.push(relative(root, dir).replace(/\\/g, "/"));
102
130
  }
103
- return out;
131
+ return [...out, ...await workspacePackages(root)];
104
132
  }
105
133
  //#endregion
106
134
  //#region src/host.ts
@@ -256,90 +284,6 @@ function createNodeHost(root, options = {}) {
256
284
  };
257
285
  }
258
286
  //#endregion
259
- //#region src/load.ts
260
- async function projectSpecs(rootConfigFile, cwd) {
261
- const configDir = rootConfigFile ? dirname(rootConfigFile) : resolve(cwd);
262
- const rootConfig = rootConfigFile ? await loadConfigFile(rootConfigFile) : {};
263
- const root = resolve(configDir, rootConfig.root ?? ".");
264
- if (!rootConfig.projects?.length) return {
265
- root,
266
- specs: [{
267
- root,
268
- config: rootConfig,
269
- parent: void 0,
270
- configFile: rootConfigFile
271
- }]
272
- };
273
- const { projects, ...parent } = rootConfig;
274
- const dirs = await glob([...projects], {
275
- cwd: root,
276
- onlyDirectories: true,
277
- ignore: ["**/node_modules/**"]
278
- });
279
- const specs = [];
280
- for (const dir of dirs.sort()) {
281
- const projectRoot = resolve(root, dir);
282
- const file = findConfigFile(projectRoot);
283
- const config = file ? await loadConfigFile(file) : {};
284
- specs.push({
285
- root: resolve(projectRoot, config.root ?? "."),
286
- config,
287
- parent,
288
- configFile: file
289
- });
290
- }
291
- return {
292
- root,
293
- specs
294
- };
295
- }
296
- /** Finds the config, resolves every project and waits until each is indexed. */
297
- async function loadWorkspace(options) {
298
- const configFile = options.configFile ? resolve(options.cwd, options.configFile) : options.upward === false ? findConfigFile(resolve(options.cwd)) : findConfigUpward(options.cwd);
299
- const { root, specs } = await projectSpecs(configFile, options.cwd);
300
- const builtins = options.builtins ?? createBuiltins();
301
- const gitRoot = findGitRoot(root);
302
- const loaded = [];
303
- for (const spec of specs) {
304
- const name = spec.config.name ?? (spec.root === root ? relative(dirname(root), root) : relative(root, spec.root)).replace(/\\/g, "/");
305
- if (options.project !== void 0 && options.project !== name) continue;
306
- const host = createNodeHost(spec.root, {
307
- cacheDir: options.cache === false ? false : void 0,
308
- logger: options.logger
309
- });
310
- const projectHost = options.wrapHost?.(host) ?? host;
311
- const setup = await resolveConfig({
312
- host: projectHost,
313
- config: spec.config,
314
- parent: spec.parent,
315
- builtins,
316
- name,
317
- runtime: "node",
318
- loadPlugin: createPluginLoader(spec.configFile ? dirname(spec.configFile) : spec.root),
319
- manifestRoots: manifestRootsOf(spec.root, gitRoot)
320
- });
321
- const project = createProject({
322
- host: projectHost,
323
- setup
324
- });
325
- await project.ready;
326
- await host.flush();
327
- loaded.push({
328
- name,
329
- root: spec.root,
330
- configFile: spec.configFile,
331
- host,
332
- project
333
- });
334
- }
335
- if (options.project !== void 0 && loaded.length === 0) throw new X18nError("X18N0003", options.project);
336
- return {
337
- root,
338
- configFile,
339
- projects: loaded
340
- };
341
- }
342
- //#endregion
343
287
  //#region src/migrate.ts
344
288
  const frameworkPresets = {
345
289
  "vue": "vue-i18n",
@@ -427,13 +371,30 @@ const handledByDefaults = [
427
371
  async function readText(path) {
428
372
  return readFile(path, "utf8").catch(() => void 0);
429
373
  }
374
+ /** The first of `paths` that exists; the i18n-ally-next name comes first. */
375
+ async function readFirst(root, paths) {
376
+ for (const path of paths) {
377
+ const text = await readText(join(root, path));
378
+ if (text !== void 0) return {
379
+ path,
380
+ text
381
+ };
382
+ }
383
+ }
430
384
  function isRecord(value) {
431
385
  return typeof value === "object" && value !== null && !Array.isArray(value);
432
386
  }
433
387
  const asStrings = (value) => (Array.isArray(value) ? value : value === void 0 ? [] : [value]).filter((v) => typeof v === "string");
434
- /** `i18n-ally.*` and legacy `vue-i18n-ally.*` keys from `.vscode/settings.json` and `*.code-workspace` files. */
388
+ /** Setting prefixes by priority: the i18n-ally-next fork renames `i18n-ally.*` keys, so its value is the newest. */
389
+ const settingPrefixes = [
390
+ "i18n-ally-next",
391
+ "i18n-ally",
392
+ "vue-i18n-ally"
393
+ ];
394
+ /** `i18n-ally-next.*`, `i18n-ally.*` and legacy `vue-i18n-ally.*` keys from `.vscode/settings.json` and `*.code-workspace` files. */
435
395
  async function readSettings(root) {
436
396
  const settings = /* @__PURE__ */ new Map();
397
+ const ranks = /* @__PURE__ */ new Map();
437
398
  const keys = [];
438
399
  const sources = [];
439
400
  const vscode = await readText(join(root, ".vscode/settings.json"));
@@ -445,10 +406,13 @@ async function readSettings(root) {
445
406
  for (const source of sources) {
446
407
  if (!isRecord(source)) continue;
447
408
  for (const [key, value] of Object.entries(source)) {
448
- const match = /^(?:vue-)?i18n-ally\.(.+)$/.exec(key);
449
- if (match && !settings.has(match[1])) {
450
- settings.set(match[1], value);
451
- keys.push(key);
409
+ const match = /^(i18n-ally-next|i18n-ally|vue-i18n-ally)\.(.+)$/.exec(key);
410
+ if (!match) continue;
411
+ keys.push(key);
412
+ const rank = settingPrefixes.indexOf(match[1]);
413
+ if (rank < (ranks.get(match[2]) ?? Infinity)) {
414
+ settings.set(match[2], value);
415
+ ranks.set(match[2], rank);
452
416
  }
453
417
  }
454
418
  }
@@ -521,7 +485,7 @@ function configFile(config, format) {
521
485
  };
522
486
  return {
523
487
  path: "x18n.config.ts",
524
- content: `import { defineConfig } from 'x18n'\n\n// Migrated from i18n-ally by \`x18n migrate i18n-ally\`.\nexport default defineConfig(${JSON.stringify(config, null, 2).replace(/"([a-z_$][\w$]*)":/gi, "$1:").replace(/'/g, "\\'").replace(/"/g, "'")})\n`
488
+ content: `import { defineConfig } from '@x18n/core'\n\n// Migrated from i18n-ally by \`x18n migrate i18n-ally\`.\nexport default defineConfig(${JSON.stringify(config, null, 2).replace(/"([a-z_$][\w$]*)":/gi, "$1:").replace(/'/g, "\\'").replace(/"/g, "'")})\n`
525
489
  };
526
490
  }
527
491
  function readerOf(settings) {
@@ -639,17 +603,16 @@ function mapConfig(settings, parts, notes) {
639
603
  ...Object.keys(extract).length ? { extract } : {}
640
604
  };
641
605
  }
642
- /** Converts i18n-ally settings, custom framework and reviews into x18n files. Never deletes i18n-ally files. */
643
- async function migrateI18nAlly(root, options = {}) {
606
+ /** The x18n config that a project's i18n-ally settings and custom framework mean, or undefined without any. */
607
+ async function readI18nAllyConfig(root) {
644
608
  const { settings, keys } = await readSettings(root);
645
- const customText = await readText(join(root, ".vscode/i18n-ally-custom-framework.yml"));
646
- const reviews = await readText(join(root, ".vscode/i18n-ally-reviews.yml"));
609
+ return configOf(root, settings, keys);
610
+ }
611
+ async function configOf(root, settings, keys) {
612
+ const custom = await readFirst(root, [".vscode/i18n-ally-next-custom-framework.yml", ".vscode/i18n-ally-custom-framework.yml"]);
613
+ const customText = custom?.text;
614
+ if (keys.length === 0 && customText === void 0) return void 0;
647
615
  const notes = [];
648
- if (keys.length === 0 && customText === void 0) return {
649
- files: [],
650
- notes: ["No i18n-ally settings found."],
651
- removable: []
652
- };
653
616
  const parts = {
654
617
  regex: [],
655
618
  scopes: void 0,
@@ -659,28 +622,43 @@ async function migrateI18nAlly(root, options = {}) {
659
622
  };
660
623
  if (customText !== void 0) customFramework(parse(customText), parts);
661
624
  const config = mapConfig(settings, parts, notes);
662
- const files = [];
663
- const plugin = {
664
- $schema: "../node_modules/@x18n/core/schema/plugin.json",
665
- name: "custom",
666
- ...parts.regex.length ? { scanners: { regex: parts.regex } } : {},
667
- ...parts.scopes ? { scopes: { regex: parts.scopes } } : {},
668
- ...parts.templates.length ? { extract: { templates: parts.templates } } : {},
669
- ...Object.keys(parts.localeInfo).length ? { localeInfo: parts.localeInfo } : {}
625
+ if (parts.exclusive) notes.push("The custom framework was exclusive (`monopoly`), so built-in presets are turned off (`presets: []`).");
626
+ return {
627
+ config,
628
+ plugin: parts.regex.length > 0 || parts.scopes !== void 0 || parts.templates.length > 0 || Object.keys(parts.localeInfo).length > 0 ? {
629
+ name: "custom",
630
+ ...parts.regex.length ? { scanners: { regex: parts.regex } } : {},
631
+ ...parts.scopes ? { scopes: { regex: parts.scopes } } : {},
632
+ ...parts.templates.length ? { extract: { templates: parts.templates } } : {},
633
+ ...Object.keys(parts.localeInfo).length ? { localeInfo: parts.localeInfo } : {}
634
+ } : void 0,
635
+ notes,
636
+ keys,
637
+ customFramework: custom?.path
670
638
  };
671
- const needsPlugin = parts.regex.length > 0 || parts.scopes !== void 0 || parts.templates.length > 0 || Object.keys(parts.localeInfo).length > 0;
672
- const finalConfig = needsPlugin ? {
639
+ }
640
+ /** Converts i18n-ally settings, custom framework and reviews into x18n files. Never deletes i18n-ally files. */
641
+ async function migrateI18nAlly(root, options = {}) {
642
+ const { settings, keys } = await readSettings(root);
643
+ const found = await configOf(root, settings, keys);
644
+ if (!found) return {
645
+ files: [],
646
+ notes: ["No i18n-ally settings found."],
647
+ removable: []
648
+ };
649
+ const { config, plugin } = found;
650
+ const notes = [...found.notes];
651
+ const reviews = (await readFirst(root, [".vscode/i18n-ally-next-reviews.yml", ".vscode/i18n-ally-reviews.yml"]))?.text;
652
+ const files = [];
653
+ const finalConfig = plugin !== void 0 ? {
673
654
  ...config,
674
655
  plugins: ["./x18n/custom.x18n-plugin.yml"]
675
656
  } : config;
676
657
  files.push(configFile(finalConfig, options.format ?? "ts"));
677
- if (needsPlugin) {
678
- const { $schema, ...rest } = plugin;
679
- files.push({
680
- path: "x18n/custom.x18n-plugin.yml",
681
- content: `# yaml-language-server: $schema=${$schema}\n${stringify(rest)}`
682
- });
683
- }
658
+ if (plugin) files.push({
659
+ path: "x18n/custom.x18n-plugin.yml",
660
+ content: `# yaml-language-server: $schema=../node_modules/@x18n/core/schema/plugin.json\n${stringify(plugin)}`
661
+ });
684
662
  let vscodeText = await readText(join(root, ".vscode/settings.json"));
685
663
  if (vscodeText !== void 0) {
686
664
  let changed = false;
@@ -703,11 +681,164 @@ async function migrateI18nAlly(root, options = {}) {
703
681
  });
704
682
  notes.push("Reviews were copied to `.x18n/reviews.yml`; x18n reads them once reviews ship (1.x).");
705
683
  }
706
- if (parts.exclusive) notes.push("The custom framework was exclusive (`monopoly`), so built-in presets are turned off (`presets: []`).");
707
684
  return {
708
685
  files,
709
686
  notes,
710
- removable: [...keys, ...customText !== void 0 ? [".vscode/i18n-ally-custom-framework.yml"] : []]
687
+ removable: [...keys, ...found.customFramework ? [found.customFramework] : []]
688
+ };
689
+ }
690
+ //#endregion
691
+ //#region src/load.ts
692
+ /** The nearest directory at or above `cwd`, up to the git root, whose `.vscode/settings.json` has i18n-ally settings. */
693
+ function findI18nAllyRoot(cwd, upward) {
694
+ let dir = resolve(cwd);
695
+ while (true) {
696
+ const settings = join(dir, ".vscode/settings.json");
697
+ if (existsSync(settings) && /"(?:i18n-ally-next|i18n-ally|vue-i18n-ally)\./.test(readFileSync(settings, "utf8"))) return dir;
698
+ const parent = dirname(dir);
699
+ if (!upward || parent === dir || existsSync(join(dir, ".git"))) return void 0;
700
+ dir = parent;
701
+ }
702
+ }
703
+ /**
704
+ * Without an x18n config, the project's i18n-ally settings act as defaults, so an i18n-ally project
705
+ * works as it did in the editor. A plugin applies them, so detection and the locale guess still fill gaps.
706
+ */
707
+ async function i18nAllyFallback(root) {
708
+ const found = await readI18nAllyConfig(root);
709
+ if (!found) return void 0;
710
+ const { presets, plugins: _plugins, ...defaults } = found.config;
711
+ const settingsPlugin = {
712
+ name: "x18n:i18n-ally-settings",
713
+ config: (draft) => applyDefaults(draft, defaults)
714
+ };
715
+ return {
716
+ ...presets ? { presets } : {},
717
+ plugins: [settingsPlugin, ...found.plugin ? [compileYamlPlugin(found.plugin)] : []]
718
+ };
719
+ }
720
+ function applyDefaults(draft, config) {
721
+ if (config.sourceLocale) draft.sourceLocale = config.sourceLocale;
722
+ if (config.ignoreLocales) draft.ignoreLocales = [...draft.ignoreLocales, ...config.ignoreLocales];
723
+ if (config.resources) draft.resources = [...draft.resources, ...config.resources];
724
+ if (config.formats) draft.formats = {
725
+ ...draft.formats,
726
+ ...config.formats
727
+ };
728
+ if (config.keyStyle) draft.keyStyle = config.keyStyle;
729
+ if (config.namespace?.default) draft.namespace = {
730
+ ...draft.namespace,
731
+ default: config.namespace.default
732
+ };
733
+ if (config.usage) draft.usage = {
734
+ ...draft.usage,
735
+ keysInUse: [...draft.usage.keysInUse, ...config.usage.keysInUse ?? []],
736
+ derivedKeys: [...draft.usage.derivedKeys, ...config.usage.derivedKeys ?? []],
737
+ exclude: [...draft.usage.exclude, ...config.usage.exclude ?? []]
738
+ };
739
+ if (config.write) draft.write = {
740
+ sort: config.write.sort ?? draft.write.sort,
741
+ indent: config.write.indent ?? draft.write.indent,
742
+ keepFilled: config.write.keepFilled ?? draft.write.keepFilled
743
+ };
744
+ if (config.extract) draft.extract = {
745
+ ...draft.extract,
746
+ ...config.extract
747
+ };
748
+ }
749
+ async function projectSpecs(rootConfigFile, cwd, upward) {
750
+ if (!rootConfigFile) {
751
+ const settingsRoot = findI18nAllyRoot(cwd, upward);
752
+ const fallback = settingsRoot && await i18nAllyFallback(settingsRoot);
753
+ if (settingsRoot && fallback) return {
754
+ root: settingsRoot,
755
+ specs: [{
756
+ root: settingsRoot,
757
+ config: fallback,
758
+ parent: void 0,
759
+ configFile: join(settingsRoot, ".vscode/settings.json")
760
+ }]
761
+ };
762
+ }
763
+ const configDir = rootConfigFile ? dirname(rootConfigFile) : resolve(cwd);
764
+ const rootConfig = rootConfigFile ? await loadConfigFile(rootConfigFile) : {};
765
+ const root = resolve(configDir, rootConfig.root ?? ".");
766
+ if (!rootConfig.projects?.length) return {
767
+ root,
768
+ specs: [{
769
+ root,
770
+ config: rootConfig,
771
+ parent: void 0,
772
+ configFile: rootConfigFile
773
+ }]
774
+ };
775
+ const { projects, ...parent } = rootConfig;
776
+ const dirs = await glob([...projects], {
777
+ cwd: root,
778
+ onlyDirectories: true,
779
+ ignore: ["**/node_modules/**"]
780
+ });
781
+ const specs = [];
782
+ for (const dir of dirs.sort()) {
783
+ const projectRoot = resolve(root, dir);
784
+ const file = findConfigFile(projectRoot);
785
+ const config = file ? await loadConfigFile(file) : {};
786
+ specs.push({
787
+ root: resolve(projectRoot, config.root ?? "."),
788
+ config,
789
+ parent,
790
+ configFile: file
791
+ });
792
+ }
793
+ return {
794
+ root,
795
+ specs
796
+ };
797
+ }
798
+ /** Finds the config, resolves every project and waits until each is indexed. */
799
+ async function loadWorkspace(options) {
800
+ const configFile = options.configFile ? resolve(options.cwd, options.configFile) : options.upward === false ? findConfigFile(resolve(options.cwd)) : findConfigUpward(options.cwd);
801
+ const { root, specs } = await projectSpecs(configFile, options.cwd, options.upward !== false);
802
+ const builtins = options.builtins ?? createBuiltins();
803
+ const gitRoot = findGitRoot(root);
804
+ const loaded = [];
805
+ for (const spec of specs) {
806
+ const name = spec.config.name ?? (spec.root === root ? relative(dirname(root), root) : relative(root, spec.root)).replace(/\\/g, "/");
807
+ if (options.project !== void 0 && options.project !== name) continue;
808
+ const host = createNodeHost(spec.root, {
809
+ cacheDir: options.cache === false ? false : void 0,
810
+ logger: options.logger
811
+ });
812
+ const projectHost = options.wrapHost?.(host) ?? host;
813
+ const setup = await resolveConfig({
814
+ host: projectHost,
815
+ config: spec.config,
816
+ parent: spec.parent,
817
+ builtins,
818
+ name,
819
+ runtime: "node",
820
+ loadPlugin: createPluginLoader(spec.configFile ? dirname(spec.configFile) : spec.root),
821
+ manifestRoots: await manifestRootsOf(spec.root, gitRoot)
822
+ });
823
+ const project = createProject({
824
+ host: projectHost,
825
+ setup
826
+ });
827
+ await project.ready;
828
+ await host.flush();
829
+ loaded.push({
830
+ name,
831
+ root: spec.root,
832
+ configFile: spec.configFile,
833
+ host,
834
+ project
835
+ });
836
+ }
837
+ if (options.project !== void 0 && loaded.length === 0) throw new X18nError("X18N0003", options.project);
838
+ return {
839
+ root,
840
+ configFile,
841
+ projects: loaded
711
842
  };
712
843
  }
713
844
  //#endregion
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@x18n/node",
3
3
  "type": "module",
4
- "version": "0.0.0",
4
+ "version": "0.0.3",
5
5
  "description": "Node.js host for x18n: file system access, config and plugin loading",
6
6
  "author": "Anthony Fu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",
@@ -25,8 +25,8 @@
25
25
  "node": ">=22"
26
26
  },
27
27
  "dependencies": {
28
- "@x18n/builtins": "0.0.0",
29
- "@x18n/core": "0.0.0",
28
+ "@x18n/builtins": "0.0.3",
29
+ "@x18n/core": "0.0.3",
30
30
  "chokidar": "^5.0.0",
31
31
  "ignore": "^7.0.12",
32
32
  "jiti": "^2.6.1",