@messagevisor/core 0.0.1 → 0.1.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.
Files changed (211) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/LICENSE +21 -0
  3. package/README.md +7 -0
  4. package/jest.config.js +8 -0
  5. package/lib/benchmark/index.d.ts +2 -0
  6. package/lib/benchmark/index.js +417 -0
  7. package/lib/benchmark/index.js.map +1 -0
  8. package/lib/builder/index.d.ts +70 -0
  9. package/lib/builder/index.js +831 -0
  10. package/lib/builder/index.js.map +1 -0
  11. package/lib/cli/index.d.ts +28 -0
  12. package/lib/cli/index.js +182 -0
  13. package/lib/cli/index.js.map +1 -0
  14. package/lib/config/index.d.ts +61 -0
  15. package/lib/config/index.js +255 -0
  16. package/lib/config/index.js.map +1 -0
  17. package/lib/create/index.d.ts +2 -0
  18. package/lib/create/index.js +405 -0
  19. package/lib/create/index.js.map +1 -0
  20. package/lib/datasource/filesystemAdapter.d.ts +44 -0
  21. package/lib/datasource/filesystemAdapter.js +424 -0
  22. package/lib/datasource/filesystemAdapter.js.map +1 -0
  23. package/lib/datasource/index.d.ts +39 -0
  24. package/lib/datasource/index.js +96 -0
  25. package/lib/datasource/index.js.map +1 -0
  26. package/lib/error.d.ts +6 -0
  27. package/lib/error.js +49 -0
  28. package/lib/error.js.map +1 -0
  29. package/lib/evaluate/cli.d.ts +8 -0
  30. package/lib/evaluate/cli.js +179 -0
  31. package/lib/evaluate/cli.js.map +1 -0
  32. package/lib/evaluate/index.d.ts +10 -0
  33. package/lib/evaluate/index.js +131 -0
  34. package/lib/evaluate/index.js.map +1 -0
  35. package/lib/examples/coerceExampleIsoDates.d.ts +12 -0
  36. package/lib/examples/coerceExampleIsoDates.js +81 -0
  37. package/lib/examples/coerceExampleIsoDates.js.map +1 -0
  38. package/lib/examples/index.d.ts +63 -0
  39. package/lib/examples/index.js +713 -0
  40. package/lib/examples/index.js.map +1 -0
  41. package/lib/exporter/index.d.ts +60 -0
  42. package/lib/exporter/index.js +610 -0
  43. package/lib/exporter/index.js.map +1 -0
  44. package/lib/find-duplicates/index.d.ts +41 -0
  45. package/lib/find-duplicates/index.js +297 -0
  46. package/lib/find-duplicates/index.js.map +1 -0
  47. package/lib/generate-code/index.d.ts +11 -0
  48. package/lib/generate-code/index.js +157 -0
  49. package/lib/generate-code/index.js.map +1 -0
  50. package/lib/generate-code/typescript.d.ts +14 -0
  51. package/lib/generate-code/typescript.js +307 -0
  52. package/lib/generate-code/typescript.js.map +1 -0
  53. package/lib/importer/index.d.ts +64 -0
  54. package/lib/importer/index.js +1092 -0
  55. package/lib/importer/index.js.map +1 -0
  56. package/lib/index.d.ts +18 -0
  57. package/lib/index.js +35 -0
  58. package/lib/index.js.map +1 -0
  59. package/lib/info/index.d.ts +17 -0
  60. package/lib/info/index.js +132 -0
  61. package/lib/info/index.js.map +1 -0
  62. package/lib/init/index.d.ts +30 -0
  63. package/lib/init/index.js +348 -0
  64. package/lib/init/index.js.map +1 -0
  65. package/lib/lint/index.d.ts +1 -0
  66. package/lib/lint/index.js +6 -0
  67. package/lib/lint/index.js.map +1 -0
  68. package/lib/linter/attributeSchema.d.ts +7 -0
  69. package/lib/linter/attributeSchema.js +36 -0
  70. package/lib/linter/attributeSchema.js.map +1 -0
  71. package/lib/linter/checkLocaleCircularDependency.d.ts +7 -0
  72. package/lib/linter/checkLocaleCircularDependency.js +42 -0
  73. package/lib/linter/checkLocaleCircularDependency.js.map +1 -0
  74. package/lib/linter/conditionSchema.d.ts +3 -0
  75. package/lib/linter/conditionSchema.js +283 -0
  76. package/lib/linter/conditionSchema.js.map +1 -0
  77. package/lib/linter/formatSchema.d.ts +325 -0
  78. package/lib/linter/formatSchema.js +165 -0
  79. package/lib/linter/formatSchema.js.map +1 -0
  80. package/lib/linter/icuStyleLint.d.ts +6 -0
  81. package/lib/linter/icuStyleLint.js +226 -0
  82. package/lib/linter/icuStyleLint.js.map +1 -0
  83. package/lib/linter/index.d.ts +34 -0
  84. package/lib/linter/index.js +557 -0
  85. package/lib/linter/index.js.map +1 -0
  86. package/lib/linter/localeSchema.d.ts +672 -0
  87. package/lib/linter/localeSchema.js +50 -0
  88. package/lib/linter/localeSchema.js.map +1 -0
  89. package/lib/linter/messageSchema.d.ts +35 -0
  90. package/lib/linter/messageSchema.js +115 -0
  91. package/lib/linter/messageSchema.js.map +1 -0
  92. package/lib/linter/printError.d.ts +8 -0
  93. package/lib/linter/printError.js +41 -0
  94. package/lib/linter/printError.js.map +1 -0
  95. package/lib/linter/schema.d.ts +33 -0
  96. package/lib/linter/schema.js +192 -0
  97. package/lib/linter/schema.js.map +1 -0
  98. package/lib/linter/segmentSchema.d.ts +8 -0
  99. package/lib/linter/segmentSchema.js +18 -0
  100. package/lib/linter/segmentSchema.js.map +1 -0
  101. package/lib/linter/targetSchema.d.ts +337 -0
  102. package/lib/linter/targetSchema.js +39 -0
  103. package/lib/linter/targetSchema.js.map +1 -0
  104. package/lib/linter/testSchema.d.ts +71 -0
  105. package/lib/linter/testSchema.js +165 -0
  106. package/lib/linter/testSchema.js.map +1 -0
  107. package/lib/linter/zodHelpers.d.ts +2 -0
  108. package/lib/linter/zodHelpers.js +15 -0
  109. package/lib/linter/zodHelpers.js.map +1 -0
  110. package/lib/list/index.d.ts +8 -0
  111. package/lib/list/index.js +524 -0
  112. package/lib/list/index.js.map +1 -0
  113. package/lib/matrix.d.ts +4 -0
  114. package/lib/matrix.js +66 -0
  115. package/lib/matrix.js.map +1 -0
  116. package/lib/promoter/index.d.ts +65 -0
  117. package/lib/promoter/index.js +1208 -0
  118. package/lib/promoter/index.js.map +1 -0
  119. package/lib/prune/index.d.ts +37 -0
  120. package/lib/prune/index.js +673 -0
  121. package/lib/prune/index.js.map +1 -0
  122. package/lib/sets.d.ts +10 -0
  123. package/lib/sets.js +120 -0
  124. package/lib/sets.js.map +1 -0
  125. package/lib/tester/cliFormat.d.ts +8 -0
  126. package/lib/tester/cliFormat.js +15 -0
  127. package/lib/tester/cliFormat.js.map +1 -0
  128. package/lib/tester/index.d.ts +35 -0
  129. package/lib/tester/index.js +713 -0
  130. package/lib/tester/index.js.map +1 -0
  131. package/lib/tester/matrix.d.ts +14 -0
  132. package/lib/tester/matrix.js +76 -0
  133. package/lib/tester/matrix.js.map +1 -0
  134. package/lib/tester/prettyDuration.d.ts +1 -0
  135. package/lib/tester/prettyDuration.js +30 -0
  136. package/lib/tester/prettyDuration.js.map +1 -0
  137. package/lib/tester/printTestResult.d.ts +2 -0
  138. package/lib/tester/printTestResult.js +32 -0
  139. package/lib/tester/printTestResult.js.map +1 -0
  140. package/lib/tester/types.d.ts +29 -0
  141. package/lib/tester/types.js +3 -0
  142. package/lib/tester/types.js.map +1 -0
  143. package/package.json +41 -13
  144. package/src/benchmark/index.spec.ts +375 -0
  145. package/src/benchmark/index.ts +433 -0
  146. package/src/builder/index.spec.ts +822 -0
  147. package/src/builder/index.ts +920 -0
  148. package/src/cli/index.spec.ts +54 -0
  149. package/src/cli/index.ts +150 -0
  150. package/src/config/index.spec.ts +70 -0
  151. package/src/config/index.ts +259 -0
  152. package/src/create/index.spec.ts +272 -0
  153. package/src/create/index.ts +295 -0
  154. package/src/datasource/filesystemAdapter.ts +313 -0
  155. package/src/datasource/index.ts +135 -0
  156. package/src/error.ts +33 -0
  157. package/src/evaluate/cli.spec.ts +368 -0
  158. package/src/evaluate/cli.ts +130 -0
  159. package/src/evaluate/index.ts +161 -0
  160. package/src/examples/coerceExampleIsoDates.spec.ts +81 -0
  161. package/src/examples/coerceExampleIsoDates.ts +98 -0
  162. package/src/examples/index.spec.ts +453 -0
  163. package/src/examples/index.ts +854 -0
  164. package/src/exporter/index.spec.ts +443 -0
  165. package/src/exporter/index.ts +643 -0
  166. package/src/find-duplicates/index.spec.ts +289 -0
  167. package/src/find-duplicates/index.ts +314 -0
  168. package/src/generate-code/index.ts +92 -0
  169. package/src/generate-code/typescript.spec.ts +241 -0
  170. package/src/generate-code/typescript.ts +284 -0
  171. package/src/importer/index.spec.ts +1101 -0
  172. package/src/importer/index.ts +1190 -0
  173. package/src/index.ts +18 -0
  174. package/src/info/index.ts +67 -0
  175. package/src/init/index.spec.ts +279 -0
  176. package/src/init/index.ts +292 -0
  177. package/src/lint/index.ts +1 -0
  178. package/src/linter/attributeSchema.ts +38 -0
  179. package/src/linter/checkLocaleCircularDependency.ts +51 -0
  180. package/src/linter/conditionSchema.ts +386 -0
  181. package/src/linter/formatSchema.ts +170 -0
  182. package/src/linter/icuStyleLint.ts +312 -0
  183. package/src/linter/index.spec.ts +824 -0
  184. package/src/linter/index.ts +460 -0
  185. package/src/linter/localeSchema.ts +70 -0
  186. package/src/linter/messageSchema.ts +152 -0
  187. package/src/linter/printError.ts +52 -0
  188. package/src/linter/schema.ts +230 -0
  189. package/src/linter/segmentSchema.ts +15 -0
  190. package/src/linter/targetSchema.ts +50 -0
  191. package/src/linter/testSchema.spec.ts +405 -0
  192. package/src/linter/testSchema.ts +239 -0
  193. package/src/linter/zodHelpers.ts +16 -0
  194. package/src/list/index.spec.ts +431 -0
  195. package/src/list/index.ts +463 -0
  196. package/src/matrix.ts +69 -0
  197. package/src/promoter/index.spec.ts +584 -0
  198. package/src/promoter/index.ts +1267 -0
  199. package/src/prune/index.spec.ts +418 -0
  200. package/src/prune/index.ts +693 -0
  201. package/src/sets.ts +74 -0
  202. package/src/tester/cliFormat.ts +11 -0
  203. package/src/tester/featurevisorIntegration.spec.ts +101 -0
  204. package/src/tester/index.spec.ts +577 -0
  205. package/src/tester/index.ts +679 -0
  206. package/src/tester/matrix.ts +106 -0
  207. package/src/tester/prettyDuration.ts +34 -0
  208. package/src/tester/printTestResult.ts +40 -0
  209. package/src/tester/types.ts +32 -0
  210. package/tsconfig.cjs.json +11 -0
  211. package/tsconfig.typecheck.json +4 -0
@@ -0,0 +1,54 @@
1
+ import { MessagevisorCLIError } from "../error";
2
+
3
+ import { getCLIErrorOutput, runCLI } from "./index";
4
+
5
+ describe("getCLIErrorOutput", function () {
6
+ it("returns friendly CLI text for MessagevisorCLIError instances", function () {
7
+ const error = new MessagevisorCLIError(
8
+ [
9
+ 'Promotion from "dev" to "production" is not allowed by this project\'s configured promotionFlows.',
10
+ "Allowed flows: dev -> staging, staging -> production.",
11
+ "Choose one of the allowed promotion paths or update messagevisor.config.js if this flow should be permitted.",
12
+ ].join("\n"),
13
+ );
14
+
15
+ expect(getCLIErrorOutput(error)).toEqual(error.cliMessage);
16
+ });
17
+
18
+ it("returns regular Error instances unchanged", function () {
19
+ const error = new Error("Boom");
20
+
21
+ expect(getCLIErrorOutput(error)).toBe(error);
22
+ expect(getCLIErrorOutput("plain string")).toEqual("plain string");
23
+ });
24
+ });
25
+
26
+ describe("runCLI", function () {
27
+ const originalArgv = process.argv;
28
+
29
+ afterEach(function () {
30
+ process.argv = originalArgv;
31
+ jest.restoreAllMocks();
32
+ });
33
+
34
+ it("exits with a failure code when a command handler returns false", async function () {
35
+ process.argv = ["node", "messagevisor", "expected-failure"];
36
+ const exitSpy = jest.spyOn(process, "exit").mockImplementation(() => undefined as never);
37
+
38
+ await runCLI({
39
+ rootDirectoryPath: "/tmp/messagevisor-test",
40
+ projectConfig: {
41
+ plugins: [
42
+ {
43
+ command: "expected-failure",
44
+ handler: async () => false,
45
+ examples: [],
46
+ },
47
+ ],
48
+ } as any,
49
+ datasource: {} as any,
50
+ });
51
+
52
+ expect(exitSpy).toHaveBeenCalledWith(1);
53
+ });
54
+ });
@@ -0,0 +1,150 @@
1
+ import { createCatalogPlugin } from "@messagevisor/catalog";
2
+
3
+ import { ProjectConfig, getProjectConfig } from "../config";
4
+ import { Datasource } from "../datasource";
5
+
6
+ import { buildPlugin } from "../builder";
7
+ import { benchmarkPlugin } from "../benchmark";
8
+ import { mergeFormats, resolveFormats } from "../builder";
9
+ import { configPlugin } from "../config";
10
+ import { createPlugin } from "../create";
11
+ import { prunePlugin } from "../prune";
12
+ import { examplesPlugin, resolveExamples } from "../examples";
13
+ import { evaluatePlugin } from "../evaluate/cli";
14
+ import { exportPlugin } from "../exporter";
15
+ import { findDuplicateTranslations, findDuplicatesPlugin } from "../find-duplicates";
16
+ import { generateCodePlugin } from "../generate-code";
17
+ import { importPlugin } from "../importer";
18
+ import { infoPlugin } from "../info";
19
+ import { initPlugin } from "../init";
20
+ import { lintPlugin } from "../linter";
21
+ import { listPlugin } from "../list";
22
+ import { promotePlugin } from "../promoter";
23
+ import { getProjectSetExecutions } from "../sets";
24
+ import { testPlugin } from "../tester";
25
+ import { getMessagevisorCLIErrorMessage } from "../error";
26
+
27
+ export interface ParsedOptions {
28
+ _: string[];
29
+ [key: string]: any;
30
+ }
31
+
32
+ export interface PluginHandlerOptions {
33
+ rootDirectoryPath: string;
34
+ projectConfig: ProjectConfig;
35
+ datasource: Datasource;
36
+ parsed: ParsedOptions;
37
+ }
38
+
39
+ export interface Plugin {
40
+ command: string;
41
+ handler: (options: PluginHandlerOptions) => Promise<void | boolean>;
42
+ examples: {
43
+ command: string;
44
+ description: string;
45
+ }[];
46
+ }
47
+
48
+ export interface RunnerOptions {
49
+ rootDirectoryPath: string;
50
+ projectConfig?: ProjectConfig;
51
+ datasource?: Datasource;
52
+ }
53
+
54
+ export function getCLIErrorOutput(error: unknown) {
55
+ const cliMessage = getMessagevisorCLIErrorMessage(error);
56
+
57
+ if (typeof cliMessage !== "undefined") {
58
+ return cliMessage;
59
+ }
60
+
61
+ return error;
62
+ }
63
+
64
+ const projectBasedPlugins = [
65
+ configPlugin,
66
+ createPlugin,
67
+ prunePlugin,
68
+ examplesPlugin,
69
+ createCatalogPlugin({
70
+ mergeFormats,
71
+ resolveFormats,
72
+ getProjectSetExecutions,
73
+ resolveExamples,
74
+ findDuplicateTranslations,
75
+ }),
76
+ benchmarkPlugin,
77
+ lintPlugin,
78
+ listPlugin,
79
+ findDuplicatesPlugin,
80
+ buildPlugin,
81
+ testPlugin,
82
+ infoPlugin,
83
+ evaluatePlugin,
84
+ exportPlugin,
85
+ importPlugin,
86
+ generateCodePlugin,
87
+ promotePlugin,
88
+ ];
89
+ const nonProjectPlugins = [initPlugin];
90
+
91
+ export async function runCLI(runnerOptions: RunnerOptions) {
92
+ const yargs = require("yargs");
93
+ let y = yargs(process.argv.slice(2)).usage("Usage: $0 <command> [options]");
94
+ const registeredSubcommands: string[] = [];
95
+ const { rootDirectoryPath, projectConfig, datasource } = runnerOptions;
96
+
97
+ function registerPlugin(plugin: Plugin) {
98
+ const subcommand = plugin.command.split(" ")[0];
99
+
100
+ if (registeredSubcommands.includes(subcommand)) {
101
+ return;
102
+ }
103
+
104
+ y = y.command({
105
+ command: plugin.command,
106
+ handler: async function (parsed: ParsedOptions) {
107
+ try {
108
+ const result = await plugin.handler({
109
+ rootDirectoryPath,
110
+ projectConfig,
111
+ datasource,
112
+ parsed,
113
+ } as PluginHandlerOptions);
114
+
115
+ if (result === false) {
116
+ process.exit(1);
117
+ }
118
+ } catch (error) {
119
+ console.error(getCLIErrorOutput(error));
120
+ process.exit(1);
121
+ }
122
+ },
123
+ });
124
+
125
+ for (const example of plugin.examples) {
126
+ y = y.example(`$0 ${example.command}`, example.description);
127
+ }
128
+
129
+ registeredSubcommands.push(subcommand);
130
+ }
131
+
132
+ if (projectConfig && datasource) {
133
+ for (const plugin of [...projectBasedPlugins, ...(projectConfig.plugins || [])]) {
134
+ registerPlugin(plugin);
135
+ }
136
+ } else {
137
+ for (const plugin of nonProjectPlugins) {
138
+ registerPlugin(plugin);
139
+ }
140
+ }
141
+
142
+ y.command({
143
+ command: "*",
144
+ handler() {
145
+ y.showHelp();
146
+ },
147
+ }).argv;
148
+ }
149
+
150
+ export { getProjectConfig, Datasource };
@@ -0,0 +1,70 @@
1
+ import * as fs from "fs";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+
5
+ import { getProjectConfig } from "./index";
6
+
7
+ async function createProject(configContent: string) {
8
+ const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-config-"));
9
+ await fs.promises.writeFile(path.join(root, "messagevisor.config.js"), configContent);
10
+ return root;
11
+ }
12
+
13
+ describe("getProjectConfig", function () {
14
+ it("accepts valid promotionFlows object rules", async function () {
15
+ const root = await createProject(
16
+ [
17
+ "module.exports = {",
18
+ " sets: true,",
19
+ " promotionFlows: [",
20
+ ' { from: "dev", to: "staging" },',
21
+ ' { from: "staging", to: "production" },',
22
+ " ],",
23
+ "};",
24
+ "",
25
+ ].join("\n"),
26
+ );
27
+
28
+ const projectConfig = getProjectConfig(root);
29
+
30
+ expect(projectConfig.promotionFlows).toEqual([
31
+ { from: "dev", to: "staging" },
32
+ { from: "staging", to: "production" },
33
+ ]);
34
+ });
35
+
36
+ it("rejects invalid promotionFlows shapes", async function () {
37
+ const invalidConfigs = [
38
+ {
39
+ config: "module.exports = { promotionFlows: true };\n",
40
+ message: "Invalid promotionFlows: true. It must be an array.",
41
+ },
42
+ {
43
+ config: 'module.exports = { promotionFlows: ["dev"] };\n',
44
+ message:
45
+ 'Invalid promotionFlows[0]: dev. Each entry must be an object with exactly "from" and "to" string fields.',
46
+ },
47
+ {
48
+ config: 'module.exports = { promotionFlows: [{ from: "dev" }] };\n',
49
+ message:
50
+ 'Invalid promotionFlows[0]: {"from":"dev"}. Each entry must contain exactly "from" and "to".',
51
+ },
52
+ {
53
+ config:
54
+ 'module.exports = { promotionFlows: [{ from: "dev", to: "staging", note: true }] };\n',
55
+ message:
56
+ 'Invalid promotionFlows[0]: {"from":"dev","to":"staging","note":true}. Each entry must contain exactly "from" and "to".',
57
+ },
58
+ {
59
+ config: 'module.exports = { promotionFlows: [{ from: "dev", to: 1 }] };\n',
60
+ message:
61
+ 'Invalid promotionFlows[0]: {"from":"dev","to":1}. "from" and "to" must be strings.',
62
+ },
63
+ ];
64
+
65
+ for (const invalid of invalidConfigs) {
66
+ const root = await createProject(invalid.config);
67
+ expect(() => getProjectConfig(root)).toThrow(invalid.message);
68
+ }
69
+ });
70
+ });
@@ -0,0 +1,259 @@
1
+ import * as path from "path";
2
+ import * as util from "util";
3
+
4
+ import { Parser, parsers } from "@featurevisor/parsers";
5
+ import type { MessagevisorModule } from "@messagevisor/sdk";
6
+
7
+ import { FilesystemAdapter } from "../datasource/filesystemAdapter";
8
+ import type { Plugin } from "../cli";
9
+
10
+ export const LOCALES_DIRECTORY_NAME = "locales";
11
+ export const MESSAGES_DIRECTORY_NAME = "messages";
12
+ export const SEGMENTS_DIRECTORY_NAME = "segments";
13
+ export const ATTRIBUTES_DIRECTORY_NAME = "attributes";
14
+ export const TARGETS_DIRECTORY_NAME = "targets";
15
+ export const TESTS_DIRECTORY_NAME = "tests";
16
+ export const SETS_DIRECTORY_NAME = "sets";
17
+ export const STATE_DIRECTORY_NAME = ".messagevisor";
18
+ export const DATAFILES_DIRECTORY_NAME = "datafiles";
19
+ export const EXPORTS_DIRECTORY_NAME = "exports";
20
+ export const CATALOG_DIRECTORY_NAME = "catalog";
21
+ export const DATAFILE_NAME_PATTERN = "messagevisor-%s.json";
22
+ export const REVISION_FILE_NAME = "REVISION";
23
+ export const CONFIG_MODULE_NAME = "messagevisor.config.js";
24
+ export const ROOT_DIR_PLACEHOLDER = "<rootDir>";
25
+ export const DEFAULT_NAMESPACE_CHARACTER = ".";
26
+ export const DEFAULT_EXPORT_OVERRIDE_KEY_SEPARATOR = ":";
27
+ export const DEFAULT_PARSER: Parser = "yml";
28
+ export const DEFAULT_ICU_SKELETON = false;
29
+ export const DEFAULT_SETS = false;
30
+ export const SCHEMA_VERSION = "1";
31
+
32
+ export interface ProjectConfig {
33
+ promotionFlows?: Array<{
34
+ from: string;
35
+ to: string;
36
+ }>;
37
+ namespaceCharacter: string;
38
+ exportOverrideKeySeparator: string;
39
+ icuSkeleton: boolean;
40
+ modules: MessagevisorModule[];
41
+ sets: boolean;
42
+ setsDirectoryPath: string;
43
+ localesDirectoryPath: string;
44
+ messagesDirectoryPath: string;
45
+ segmentsDirectoryPath: string;
46
+ attributesDirectoryPath: string;
47
+ targetsDirectoryPath: string;
48
+ testsDirectoryPath: string;
49
+ stateDirectoryPath: string;
50
+ datafilesDirectoryPath: string;
51
+ exportsDirectoryPath: string;
52
+ catalogDirectoryPath: string;
53
+ datafileNamePattern: string;
54
+ revisionFileName: string;
55
+ adapter: any;
56
+ plugins: Plugin[];
57
+ parser: Parser;
58
+ }
59
+
60
+ export function getProjectConfig(rootDirectoryPath: string): ProjectConfig {
61
+ const baseConfig: ProjectConfig = {
62
+ namespaceCharacter: DEFAULT_NAMESPACE_CHARACTER,
63
+ exportOverrideKeySeparator: DEFAULT_EXPORT_OVERRIDE_KEY_SEPARATOR,
64
+ icuSkeleton: DEFAULT_ICU_SKELETON,
65
+ modules: [],
66
+ sets: DEFAULT_SETS,
67
+ promotionFlows: undefined,
68
+ parser: DEFAULT_PARSER,
69
+ adapter: FilesystemAdapter,
70
+ plugins: [],
71
+ setsDirectoryPath: path.join(rootDirectoryPath, SETS_DIRECTORY_NAME),
72
+ localesDirectoryPath: path.join(rootDirectoryPath, LOCALES_DIRECTORY_NAME),
73
+ messagesDirectoryPath: path.join(rootDirectoryPath, MESSAGES_DIRECTORY_NAME),
74
+ segmentsDirectoryPath: path.join(rootDirectoryPath, SEGMENTS_DIRECTORY_NAME),
75
+ attributesDirectoryPath: path.join(rootDirectoryPath, ATTRIBUTES_DIRECTORY_NAME),
76
+ targetsDirectoryPath: path.join(rootDirectoryPath, TARGETS_DIRECTORY_NAME),
77
+ testsDirectoryPath: path.join(rootDirectoryPath, TESTS_DIRECTORY_NAME),
78
+ stateDirectoryPath: path.join(rootDirectoryPath, STATE_DIRECTORY_NAME),
79
+ datafilesDirectoryPath: path.join(rootDirectoryPath, DATAFILES_DIRECTORY_NAME),
80
+ exportsDirectoryPath: path.join(rootDirectoryPath, EXPORTS_DIRECTORY_NAME),
81
+ catalogDirectoryPath: path.join(rootDirectoryPath, CATALOG_DIRECTORY_NAME),
82
+ datafileNamePattern: DATAFILE_NAME_PATTERN,
83
+ revisionFileName: REVISION_FILE_NAME,
84
+ };
85
+
86
+ const customConfig = require(path.join(rootDirectoryPath, CONFIG_MODULE_NAME));
87
+ const mergedConfig: Record<string, any> = {};
88
+
89
+ Object.keys(baseConfig).forEach((key) => {
90
+ mergedConfig[key] =
91
+ typeof customConfig[key] !== "undefined" ? customConfig[key] : (baseConfig as any)[key];
92
+
93
+ if (key.endsWith("Path") && mergedConfig[key].indexOf(ROOT_DIR_PLACEHOLDER) !== -1) {
94
+ mergedConfig[key] = mergedConfig[key].replace(ROOT_DIR_PLACEHOLDER, rootDirectoryPath);
95
+ }
96
+ });
97
+
98
+ const finalConfig = mergedConfig as ProjectConfig;
99
+
100
+ if (
101
+ typeof finalConfig.namespaceCharacter !== "string" ||
102
+ finalConfig.namespaceCharacter.length === 0 ||
103
+ finalConfig.namespaceCharacter === "/" ||
104
+ finalConfig.namespaceCharacter === "\\"
105
+ ) {
106
+ throw new Error(
107
+ `Invalid namespaceCharacter: ${finalConfig.namespaceCharacter}. It must be a non-empty string and cannot be a path separator.`,
108
+ );
109
+ }
110
+
111
+ if (
112
+ typeof finalConfig.exportOverrideKeySeparator !== "string" ||
113
+ finalConfig.exportOverrideKeySeparator.length === 0 ||
114
+ finalConfig.exportOverrideKeySeparator === "/" ||
115
+ finalConfig.exportOverrideKeySeparator === "\\"
116
+ ) {
117
+ throw new Error(
118
+ `Invalid exportOverrideKeySeparator: ${finalConfig.exportOverrideKeySeparator}. It must be a non-empty string and cannot be a path separator.`,
119
+ );
120
+ }
121
+
122
+ if (finalConfig.exportOverrideKeySeparator === finalConfig.namespaceCharacter) {
123
+ throw new Error(
124
+ `Invalid exportOverrideKeySeparator: it cannot be the same as namespaceCharacter "${finalConfig.namespaceCharacter}".`,
125
+ );
126
+ }
127
+
128
+ if (typeof finalConfig.icuSkeleton !== "boolean") {
129
+ throw new Error(`Invalid icuSkeleton: ${finalConfig.icuSkeleton}. It must be a boolean.`);
130
+ }
131
+
132
+ if (!Array.isArray(finalConfig.modules)) {
133
+ throw new Error(`Invalid modules: ${finalConfig.modules}. It must be an array.`);
134
+ }
135
+
136
+ if (typeof finalConfig.sets !== "boolean") {
137
+ throw new Error(`Invalid sets: ${finalConfig.sets}. It must be a boolean.`);
138
+ }
139
+
140
+ if (typeof finalConfig.promotionFlows !== "undefined") {
141
+ if (!Array.isArray(finalConfig.promotionFlows)) {
142
+ throw new Error(
143
+ `Invalid promotionFlows: ${finalConfig.promotionFlows}. It must be an array.`,
144
+ );
145
+ }
146
+
147
+ finalConfig.promotionFlows.forEach((entry: any, index: number) => {
148
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
149
+ throw new Error(
150
+ `Invalid promotionFlows[${index}]: ${entry}. Each entry must be an object with exactly "from" and "to" string fields.`,
151
+ );
152
+ }
153
+
154
+ const keys = Object.keys(entry).sort();
155
+
156
+ if (keys.length !== 2 || keys[0] !== "from" || keys[1] !== "to") {
157
+ throw new Error(
158
+ `Invalid promotionFlows[${index}]: ${JSON.stringify(entry)}. Each entry must contain exactly "from" and "to".`,
159
+ );
160
+ }
161
+
162
+ if (typeof entry.from !== "string" || typeof entry.to !== "string") {
163
+ throw new Error(
164
+ `Invalid promotionFlows[${index}]: ${JSON.stringify(entry)}. "from" and "to" must be strings.`,
165
+ );
166
+ }
167
+ });
168
+ }
169
+
170
+ if (typeof finalConfig.parser === "string") {
171
+ const allowedParsers = Object.keys(parsers);
172
+
173
+ if (allowedParsers.indexOf(finalConfig.parser) === -1) {
174
+ throw new Error(`Invalid parser: ${finalConfig.parser}`);
175
+ }
176
+
177
+ finalConfig.parser = parsers[finalConfig.parser];
178
+ }
179
+
180
+ return finalConfig;
181
+ }
182
+
183
+ export function getProjectConfigForSet(projectConfig: ProjectConfig, set: string): ProjectConfig {
184
+ const setRootDirectoryPath = path.join(projectConfig.setsDirectoryPath, set);
185
+
186
+ return {
187
+ ...projectConfig,
188
+ localesDirectoryPath: path.join(setRootDirectoryPath, LOCALES_DIRECTORY_NAME),
189
+ messagesDirectoryPath: path.join(setRootDirectoryPath, MESSAGES_DIRECTORY_NAME),
190
+ segmentsDirectoryPath: path.join(setRootDirectoryPath, SEGMENTS_DIRECTORY_NAME),
191
+ attributesDirectoryPath: path.join(setRootDirectoryPath, ATTRIBUTES_DIRECTORY_NAME),
192
+ targetsDirectoryPath: path.join(setRootDirectoryPath, TARGETS_DIRECTORY_NAME),
193
+ testsDirectoryPath: path.join(setRootDirectoryPath, TESTS_DIRECTORY_NAME),
194
+ stateDirectoryPath: path.join(projectConfig.stateDirectoryPath, SETS_DIRECTORY_NAME, set),
195
+ datafilesDirectoryPath: path.join(projectConfig.datafilesDirectoryPath, set),
196
+ };
197
+ }
198
+
199
+ export function formatDatafileName(projectConfig: ProjectConfig, parts: string[]) {
200
+ return util.format(projectConfig.datafileNamePattern, parts.join("-"));
201
+ }
202
+
203
+ export function formatDatafilePath(
204
+ projectConfig: ProjectConfig,
205
+ targetKey: string,
206
+ localeKey: string,
207
+ ) {
208
+ const targetPathSegments = targetKey.split(projectConfig.namespaceCharacter);
209
+ const targetFileKey = targetPathSegments.pop() || targetKey;
210
+
211
+ return path.join(
212
+ ...targetPathSegments,
213
+ formatDatafileName(projectConfig, [targetFileKey, localeKey]),
214
+ );
215
+ }
216
+
217
+ export interface ShowProjectConfigOptions {
218
+ json?: boolean;
219
+ pretty?: boolean;
220
+ }
221
+
222
+ export function showProjectConfig(
223
+ projectConfig: ProjectConfig,
224
+ options: ShowProjectConfigOptions = {},
225
+ ) {
226
+ if (options.json) {
227
+ console.log(
228
+ options.pretty ? JSON.stringify(projectConfig, null, 2) : JSON.stringify(projectConfig),
229
+ );
230
+ return;
231
+ }
232
+
233
+ console.log("\nProject configuration:\n");
234
+
235
+ for (const key of Object.keys(projectConfig)) {
236
+ if (["adapter", "parser", "plugins", "modules"].includes(key)) {
237
+ continue;
238
+ }
239
+
240
+ console.log(` - ${key}: ${(projectConfig as any)[key]}`);
241
+ }
242
+ }
243
+
244
+ export const configPlugin: Plugin = {
245
+ command: "config",
246
+ handler: async ({ projectConfig, parsed }) => {
247
+ showProjectConfig(projectConfig, {
248
+ json: parsed.json,
249
+ pretty: parsed.pretty,
250
+ });
251
+ },
252
+ examples: [
253
+ { command: "config", description: "show the project configuration" },
254
+ {
255
+ command: "config --json --pretty",
256
+ description: "show the project configuration as pretty JSON",
257
+ },
258
+ ],
259
+ };