@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,368 @@
1
+ import * as fs from "fs";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+
5
+ import { getProjectConfig } from "../config";
6
+ import { Datasource } from "../datasource";
7
+ import { evaluatePlugin } from "./cli";
8
+
9
+ async function writeFile(root: string, relativePath: string, content: string) {
10
+ const filePath = path.join(root, relativePath);
11
+ await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
12
+ await fs.promises.writeFile(filePath, content);
13
+ }
14
+
15
+ async function createProject(configContent = "module.exports = {};\n") {
16
+ const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-evaluate-"));
17
+
18
+ await writeFile(root, "messagevisor.config.js", configContent);
19
+ await writeFile(
20
+ root,
21
+ "locales/en.yml",
22
+ [
23
+ "description: English",
24
+ "formats:",
25
+ " number:",
26
+ " decimal:",
27
+ " minimumFractionDigits: 2",
28
+ " maximumFractionDigits: 2",
29
+ "",
30
+ ].join("\n"),
31
+ );
32
+ await writeFile(
33
+ root,
34
+ "targets/web.yml",
35
+ [
36
+ "description: Web",
37
+ "includeMessages:",
38
+ " - auth*",
39
+ "locales:",
40
+ " - en",
41
+ "formats:",
42
+ " en:",
43
+ " number:",
44
+ " money:",
45
+ " style: currency",
46
+ " currency: USD",
47
+ "",
48
+ ].join("\n"),
49
+ );
50
+ await writeFile(
51
+ root,
52
+ "messages/auth/signin.yml",
53
+ [
54
+ "description: Sign in",
55
+ "translations:",
56
+ " en: Sign in",
57
+ "overrides:",
58
+ " - key: adult",
59
+ " segments: adult",
60
+ " translations:",
61
+ " en: Adult sign in",
62
+ "",
63
+ ].join("\n"),
64
+ );
65
+ await writeFile(
66
+ root,
67
+ "segments/adult.yml",
68
+ [
69
+ "conditions:",
70
+ " - attribute: age",
71
+ " operator: greaterThanOrEquals",
72
+ " value: 18",
73
+ "",
74
+ ].join("\n"),
75
+ );
76
+
77
+ return root;
78
+ }
79
+
80
+ describe("evaluatePlugin", function () {
81
+ it("evaluates segments using context without requiring a locale", async function () {
82
+ const root = await createProject();
83
+ const projectConfig = getProjectConfig(root);
84
+ const datasource = new Datasource(projectConfig, root);
85
+ const logSpy = jest.spyOn(console, "log").mockImplementation(() => {});
86
+
87
+ await expect(
88
+ evaluatePlugin.handler({
89
+ projectConfig,
90
+ datasource,
91
+ parsed: {
92
+ segment: "adult",
93
+ context: JSON.stringify({ age: 21 }),
94
+ },
95
+ }),
96
+ ).resolves.toBeUndefined();
97
+
98
+ expect(logSpy).toHaveBeenCalledWith('Segment "adult" matched: true');
99
+ logSpy.mockRestore();
100
+ });
101
+
102
+ it("still requires locale for message evaluation", async function () {
103
+ const root = await createProject();
104
+ const projectConfig = getProjectConfig(root);
105
+ const datasource = new Datasource(projectConfig, root);
106
+ const errorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
107
+
108
+ try {
109
+ await expect(
110
+ evaluatePlugin.handler({
111
+ projectConfig,
112
+ datasource,
113
+ parsed: {
114
+ message: "auth.signin",
115
+ context: JSON.stringify({}),
116
+ },
117
+ }),
118
+ ).resolves.toEqual(false);
119
+
120
+ expect(errorSpy).toHaveBeenCalledWith("Pass --locale=<locale>");
121
+ } finally {
122
+ errorSpy.mockRestore();
123
+ }
124
+ });
125
+
126
+ it("evaluates messages with JSON values using project-configured modules", async function () {
127
+ const interpolationModulePath = path.join(
128
+ path.resolve(__dirname, "../../../.."),
129
+ "packages/module-interpolation/src/index.ts",
130
+ );
131
+ const root = await createProject(
132
+ [
133
+ `const { createInterpolationModule } = require(${JSON.stringify(interpolationModulePath)});`,
134
+ "module.exports = {",
135
+ " modules: [createInterpolationModule()],",
136
+ "};",
137
+ "",
138
+ ].join("\n"),
139
+ );
140
+
141
+ await writeFile(
142
+ root,
143
+ "messages/auth/signin.yml",
144
+ ["description: Sign in", "translations:", " en: Hello {name}", ""].join("\n"),
145
+ );
146
+
147
+ const projectConfig = getProjectConfig(root);
148
+ const datasource = new Datasource(projectConfig, root);
149
+ const logSpy = jest.spyOn(console, "log").mockImplementation(() => {});
150
+
151
+ await expect(
152
+ evaluatePlugin.handler({
153
+ projectConfig,
154
+ datasource,
155
+ parsed: {
156
+ message: "auth.signin",
157
+ locale: "en",
158
+ values: JSON.stringify({ name: "Ada" }),
159
+ },
160
+ }),
161
+ ).resolves.toBeUndefined();
162
+
163
+ expect(logSpy).toHaveBeenCalledWith("Hello Ada");
164
+ logSpy.mockRestore();
165
+ });
166
+
167
+ it("evaluates keyed messages with context and applies overrides without requiring a target", async function () {
168
+ const root = await createProject();
169
+ const projectConfig = getProjectConfig(root);
170
+ const datasource = new Datasource(projectConfig, root);
171
+ const logSpy = jest.spyOn(console, "log").mockImplementation(() => {});
172
+
173
+ await expect(
174
+ evaluatePlugin.handler({
175
+ projectConfig,
176
+ datasource,
177
+ parsed: {
178
+ message: "auth.signin",
179
+ locale: "en",
180
+ context: JSON.stringify({ age: 21 }),
181
+ },
182
+ }),
183
+ ).resolves.toBeUndefined();
184
+
185
+ expect(logSpy).toHaveBeenCalledWith("Adult sign in");
186
+ logSpy.mockRestore();
187
+ });
188
+
189
+ it("reports invalid values JSON clearly for message evaluation", async function () {
190
+ const root = await createProject();
191
+ const projectConfig = getProjectConfig(root);
192
+ const datasource = new Datasource(projectConfig, root);
193
+ const errorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
194
+
195
+ try {
196
+ await expect(
197
+ evaluatePlugin.handler({
198
+ projectConfig,
199
+ datasource,
200
+ parsed: {
201
+ message: "auth.signin",
202
+ locale: "en",
203
+ values: "{not-json}",
204
+ },
205
+ }),
206
+ ).resolves.toEqual(false);
207
+
208
+ expect(errorSpy).toHaveBeenCalledWith("Invalid --values: expected valid JSON");
209
+ } finally {
210
+ errorSpy.mockRestore();
211
+ }
212
+ });
213
+
214
+ it("evaluates raw messages with JSON values using project-configured modules", async function () {
215
+ const interpolationModulePath = path.join(
216
+ path.resolve(__dirname, "../../../.."),
217
+ "packages/module-interpolation/src/index.ts",
218
+ );
219
+ const root = await createProject(
220
+ [
221
+ `const { createInterpolationModule } = require(${JSON.stringify(interpolationModulePath)});`,
222
+ "module.exports = {",
223
+ " modules: [createInterpolationModule()],",
224
+ "};",
225
+ "",
226
+ ].join("\n"),
227
+ );
228
+ const projectConfig = getProjectConfig(root);
229
+ const datasource = new Datasource(projectConfig, root);
230
+ const logSpy = jest.spyOn(console, "log").mockImplementation(() => {});
231
+
232
+ await expect(
233
+ evaluatePlugin.handler({
234
+ projectConfig,
235
+ datasource,
236
+ parsed: {
237
+ rawMessage: "Hello {name}",
238
+ locale: "en",
239
+ values: JSON.stringify({ name: "Ada" }),
240
+ },
241
+ }),
242
+ ).resolves.toBeUndefined();
243
+
244
+ expect(logSpy).toHaveBeenCalledWith("Hello Ada");
245
+ logSpy.mockRestore();
246
+ });
247
+
248
+ it("evaluates raw messages without target while still using locale formats", async function () {
249
+ const icuModulePath = path.join(
250
+ path.resolve(__dirname, "../../../.."),
251
+ "packages/module-icu/src/index.ts",
252
+ );
253
+ const root = await createProject(
254
+ [
255
+ `const { createICUModule } = require(${JSON.stringify(icuModulePath)});`,
256
+ "module.exports = {",
257
+ " modules: [createICUModule()],",
258
+ "};",
259
+ "",
260
+ ].join("\n"),
261
+ );
262
+ const projectConfig = getProjectConfig(root);
263
+ const datasource = new Datasource(projectConfig, root);
264
+ const logSpy = jest.spyOn(console, "log").mockImplementation(() => {});
265
+
266
+ await expect(
267
+ evaluatePlugin.handler({
268
+ projectConfig,
269
+ datasource,
270
+ parsed: {
271
+ rawMessage: "Amount: {amount, number, decimal}",
272
+ locale: "en",
273
+ values: JSON.stringify({ amount: 12 }),
274
+ },
275
+ }),
276
+ ).resolves.toBeUndefined();
277
+
278
+ expect(logSpy).toHaveBeenCalledWith("Amount: 12.00");
279
+ logSpy.mockRestore();
280
+ });
281
+
282
+ it("evaluates raw messages with target using the target-specific datafile formats", async function () {
283
+ const icuModulePath = path.join(
284
+ path.resolve(__dirname, "../../../.."),
285
+ "packages/module-icu/src/index.ts",
286
+ );
287
+ const root = await createProject(
288
+ [
289
+ `const { createICUModule } = require(${JSON.stringify(icuModulePath)});`,
290
+ "module.exports = {",
291
+ " modules: [createICUModule()],",
292
+ "};",
293
+ "",
294
+ ].join("\n"),
295
+ );
296
+ const projectConfig = getProjectConfig(root);
297
+ const datasource = new Datasource(projectConfig, root);
298
+ const logSpy = jest.spyOn(console, "log").mockImplementation(() => {});
299
+
300
+ await expect(
301
+ evaluatePlugin.handler({
302
+ projectConfig,
303
+ datasource,
304
+ parsed: {
305
+ rawMessage: "Total: {amount, number, money}",
306
+ locale: "en",
307
+ target: "web",
308
+ values: JSON.stringify({ amount: 12 }),
309
+ },
310
+ }),
311
+ ).resolves.toBeUndefined();
312
+
313
+ expect(String(logSpy.mock.calls[0][0])).toContain("12");
314
+ expect(String(logSpy.mock.calls[0][0])).toContain("$");
315
+ logSpy.mockRestore();
316
+ });
317
+
318
+ it("still requires locale for raw message evaluation", async function () {
319
+ const root = await createProject();
320
+ const projectConfig = getProjectConfig(root);
321
+ const datasource = new Datasource(projectConfig, root);
322
+ const errorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
323
+
324
+ try {
325
+ await expect(
326
+ evaluatePlugin.handler({
327
+ projectConfig,
328
+ datasource,
329
+ parsed: {
330
+ rawMessage: "Hello {name}",
331
+ values: JSON.stringify({ name: "Ada" }),
332
+ },
333
+ }),
334
+ ).resolves.toEqual(false);
335
+
336
+ expect(errorSpy).toHaveBeenCalledWith("Pass --locale=<locale>");
337
+ } finally {
338
+ errorSpy.mockRestore();
339
+ }
340
+ });
341
+
342
+ it("rejects using message and rawMessage together", async function () {
343
+ const root = await createProject();
344
+ const projectConfig = getProjectConfig(root);
345
+ const datasource = new Datasource(projectConfig, root);
346
+ const errorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
347
+
348
+ try {
349
+ await expect(
350
+ evaluatePlugin.handler({
351
+ projectConfig,
352
+ datasource,
353
+ parsed: {
354
+ message: "auth.signin",
355
+ rawMessage: "Hello {name}",
356
+ locale: "en",
357
+ },
358
+ }),
359
+ ).resolves.toEqual(false);
360
+
361
+ expect(errorSpy).toHaveBeenCalledWith(
362
+ "Pass either --message=<key> or --rawMessage=<message>, not both",
363
+ );
364
+ } finally {
365
+ errorSpy.mockRestore();
366
+ }
367
+ });
368
+ });
@@ -0,0 +1,130 @@
1
+ /* eslint-disable @typescript-eslint/no-unused-vars */
2
+ import { createMessagevisor } from "@messagevisor/sdk";
3
+ import type { Segment } from "@messagevisor/types";
4
+
5
+ import { buildDatafile } from "../builder";
6
+ import { MessagevisorCLIError, printMessagevisorCLIError } from "../error";
7
+ import { getProjectSetExecutions } from "../sets";
8
+ import { evaluateSegment } from "./index";
9
+
10
+ async function readAllSegments(datasource: any) {
11
+ const segmentKeys = await datasource.listSegments();
12
+ const entries = await Promise.all(
13
+ segmentKeys.map(async (segmentKey: string) => {
14
+ const segment = (await datasource.readSegment(segmentKey)) as Segment;
15
+ return [segmentKey, segment] as const;
16
+ }),
17
+ );
18
+
19
+ return Object.fromEntries(entries);
20
+ }
21
+
22
+ function parseJsonOption(optionName: string, value: string) {
23
+ try {
24
+ return JSON.parse(value);
25
+ } catch (error) {
26
+ throw new MessagevisorCLIError(`Invalid ${optionName}: expected valid JSON`);
27
+ }
28
+ }
29
+
30
+ export const evaluatePlugin = {
31
+ command: "evaluate",
32
+ handler: async ({ projectConfig, datasource, parsed }: any) => {
33
+ try {
34
+ if (projectConfig.sets && !parsed.set) {
35
+ throw new MessagevisorCLIError("Pass --set=<set>");
36
+ }
37
+
38
+ if (projectConfig.sets) {
39
+ const [execution] = await getProjectSetExecutions(projectConfig, datasource, parsed.set);
40
+ projectConfig = execution.projectConfig;
41
+ datasource = execution.datasource;
42
+ }
43
+
44
+ const target = parsed.target;
45
+ const context = parsed.context ? parseJsonOption("--context", parsed.context) : {};
46
+ const values = parsed.values ? parseJsonOption("--values", parsed.values) : undefined;
47
+
48
+ if (parsed.message && parsed.rawMessage) {
49
+ throw new MessagevisorCLIError(
50
+ "Pass either --message=<key> or --rawMessage=<message>, not both",
51
+ );
52
+ }
53
+
54
+ if (parsed.segment) {
55
+ const segments = await readAllSegments(datasource);
56
+ const matched = evaluateSegment(parsed.segment, {
57
+ segments,
58
+ context,
59
+ });
60
+ const result = { segment: parsed.segment, matched };
61
+ console.log(
62
+ parsed.json
63
+ ? JSON.stringify(result, null, parsed.pretty ? 2 : 0)
64
+ : `Segment "${parsed.segment}" matched: ${matched}`,
65
+ );
66
+ return;
67
+ }
68
+
69
+ if (!parsed.locale) {
70
+ throw new MessagevisorCLIError("Pass --locale=<locale>");
71
+ }
72
+
73
+ const locale = parsed.locale;
74
+ const revision = await datasource.readRevision();
75
+ const datafile = await buildDatafile(projectConfig, datasource, target, locale, revision);
76
+ const messagevisor = createMessagevisor({
77
+ datafile,
78
+ context,
79
+ modules: projectConfig.modules || [],
80
+ logLevel: "warn",
81
+ });
82
+
83
+ if (parsed.message) {
84
+ const translation = messagevisor.translate(parsed.message, values, { context });
85
+ const result = { message: parsed.message, translation };
86
+ console.log(
87
+ parsed.json ? JSON.stringify(result, null, parsed.pretty ? 2 : 0) : translation,
88
+ );
89
+ return;
90
+ }
91
+
92
+ if (parsed.rawMessage) {
93
+ const translation = messagevisor.formatMessage(parsed.rawMessage, values);
94
+ const result = { rawMessage: parsed.rawMessage, translation };
95
+ console.log(
96
+ parsed.json ? JSON.stringify(result, null, parsed.pretty ? 2 : 0) : translation,
97
+ );
98
+ return;
99
+ }
100
+
101
+ throw new MessagevisorCLIError(
102
+ "Pass --message=<key>, --rawMessage=<message>, or --segment=<key>",
103
+ );
104
+ } catch (error) {
105
+ if (printMessagevisorCLIError(error)) {
106
+ return false;
107
+ }
108
+
109
+ throw error;
110
+ }
111
+ },
112
+ examples: [
113
+ {
114
+ command: "evaluate --message=auth.signin --locale=en-US",
115
+ description: "evaluate a message translation",
116
+ },
117
+ {
118
+ command: 'evaluate --message=dashboard.welcome --locale=en-US --values=\'{"name":"Ada"}\'',
119
+ description: "evaluate a message translation with JSON values",
120
+ },
121
+ {
122
+ command: 'evaluate --rawMessage="Hello {name}" --locale=en-US --values=\'{"name":"Ada"}\'',
123
+ description: "evaluate a raw message string with JSON values",
124
+ },
125
+ {
126
+ command: 'evaluate --segment=platform-web --context=\'{"platform":"web"}\'',
127
+ description: "evaluate a segment",
128
+ },
129
+ ],
130
+ };
@@ -0,0 +1,161 @@
1
+ import type { Condition, Context, GroupSegment, Segment } from "@messagevisor/types";
2
+
3
+ export interface EvaluateConditionOptions {
4
+ context?: Context;
5
+ segments?: Record<string, Segment>;
6
+ resolveFlag?: (featureKey: string, context?: Context) => boolean;
7
+ resolveVariation?: (experimentKey: string, context?: Context) => string;
8
+ }
9
+
10
+ function getContextValue(context: Context | undefined, attribute: string) {
11
+ if (!context) {
12
+ return undefined;
13
+ }
14
+
15
+ return attribute
16
+ .split(".")
17
+ .reduce((value: any, part) => (value ? value[part] : undefined), context as any);
18
+ }
19
+
20
+ function compareDate(value: unknown, expected: unknown, operator: "before" | "after") {
21
+ const valueTime = new Date(value as any).getTime();
22
+ const expectedTime = new Date(expected as any).getTime();
23
+
24
+ if (Number.isNaN(valueTime) || Number.isNaN(expectedTime)) {
25
+ return false;
26
+ }
27
+
28
+ return operator === "before" ? valueTime < expectedTime : valueTime > expectedTime;
29
+ }
30
+
31
+ export function evaluateCondition(
32
+ condition: Condition | Condition[] | "*" | undefined,
33
+ options: EvaluateConditionOptions = {},
34
+ ): boolean {
35
+ if (!condition || condition === "*") {
36
+ return true;
37
+ }
38
+
39
+ if (Array.isArray(condition)) {
40
+ return condition.every((item) => evaluateCondition(item, options));
41
+ }
42
+
43
+ if (typeof condition === "string") {
44
+ return evaluateSegment(condition, options);
45
+ }
46
+
47
+ if ("and" in condition) {
48
+ return condition.and.every((item) => evaluateCondition(item, options));
49
+ }
50
+
51
+ if ("or" in condition) {
52
+ return condition.or.some((item) => evaluateCondition(item, options));
53
+ }
54
+
55
+ if ("not" in condition) {
56
+ return !condition.not.every((item) => evaluateCondition(item, options));
57
+ }
58
+
59
+ if ("feature" in condition) {
60
+ const enabled = options.resolveFlag
61
+ ? options.resolveFlag(condition.feature, options.context)
62
+ : false;
63
+ return condition.operator === "isEnabled"
64
+ ? enabled
65
+ : condition.operator === "isDisabled"
66
+ ? !enabled
67
+ : false;
68
+ }
69
+
70
+ if ("experiment" in condition) {
71
+ const variation = options.resolveVariation
72
+ ? options.resolveVariation(condition.experiment, options.context)
73
+ : undefined;
74
+ return condition.operator === "hasVariation" ? variation === condition.value : false;
75
+ }
76
+
77
+ const value = getContextValue(options.context, condition.attribute);
78
+ const expected = condition.value;
79
+
80
+ switch (condition.operator) {
81
+ case "equals":
82
+ return value === expected;
83
+ case "notEquals":
84
+ return value !== expected;
85
+ case "exists":
86
+ return value !== undefined && value !== null;
87
+ case "notExists":
88
+ return value === undefined || value === null;
89
+ case "greaterThan":
90
+ return Number(value) > Number(expected);
91
+ case "greaterThanOrEquals":
92
+ return Number(value) >= Number(expected);
93
+ case "lessThan":
94
+ return Number(value) < Number(expected);
95
+ case "lessThanOrEquals":
96
+ return Number(value) <= Number(expected);
97
+ case "contains":
98
+ return String(value).includes(String(expected));
99
+ case "notContains":
100
+ return !String(value).includes(String(expected));
101
+ case "startsWith":
102
+ return String(value).startsWith(String(expected));
103
+ case "endsWith":
104
+ return String(value).endsWith(String(expected));
105
+ case "before":
106
+ return compareDate(value, expected, "before");
107
+ case "after":
108
+ return compareDate(value, expected, "after");
109
+ case "includes":
110
+ return Array.isArray(value) && value.includes(expected as any);
111
+ case "notIncludes":
112
+ return !Array.isArray(value) || !value.includes(expected as any);
113
+ case "in":
114
+ return Array.isArray(expected) && expected.includes(value as any);
115
+ case "notIn":
116
+ return !Array.isArray(expected) || !expected.includes(value as any);
117
+ default:
118
+ return false;
119
+ }
120
+ }
121
+
122
+ export function evaluateGroupSegment(
123
+ groupSegment: GroupSegment | GroupSegment[] | "*" | undefined,
124
+ options: EvaluateConditionOptions = {},
125
+ ): boolean {
126
+ if (!groupSegment || groupSegment === "*") {
127
+ return true;
128
+ }
129
+
130
+ if (Array.isArray(groupSegment)) {
131
+ return groupSegment.every((item) => evaluateGroupSegment(item, options));
132
+ }
133
+
134
+ if (typeof groupSegment === "string") {
135
+ return evaluateSegment(groupSegment, options);
136
+ }
137
+
138
+ if ("and" in groupSegment) {
139
+ return groupSegment.and.every((item) => evaluateGroupSegment(item, options));
140
+ }
141
+
142
+ if ("or" in groupSegment) {
143
+ return groupSegment.or.some((item) => evaluateGroupSegment(item, options));
144
+ }
145
+
146
+ if ("not" in groupSegment) {
147
+ return !groupSegment.not.every((item) => evaluateGroupSegment(item, options));
148
+ }
149
+
150
+ return false;
151
+ }
152
+
153
+ export function evaluateSegment(segmentKey: string, options: EvaluateConditionOptions = {}) {
154
+ const segment = options.segments ? options.segments[segmentKey] : undefined;
155
+
156
+ if (!segment || segment.archived) {
157
+ return false;
158
+ }
159
+
160
+ return evaluateCondition(segment.conditions, options);
161
+ }