@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,460 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+
4
+ import type { Attribute, Locale, Message } from "@messagevisor/types";
5
+ import type { ZodError, ZodTypeAny } from "zod";
6
+
7
+ import type { ProjectConfig } from "../config";
8
+ import type { Datasource } from "../datasource";
9
+ import { getAttributeZodSchema } from "./attributeSchema";
10
+ import { checkLocaleCircularDependency } from "./checkLocaleCircularDependency";
11
+ import { getConditionsZodSchema } from "./conditionSchema";
12
+ import { lintMessageIcuFormatStyles } from "./icuStyleLint";
13
+ import { getLocaleZodSchema } from "./localeSchema";
14
+ import { getMessageZodSchema } from "./messageSchema";
15
+ import { getLintIssuesFromZodError } from "./printError";
16
+ import { getTargetZodSchema } from "./targetSchema";
17
+ import { getSegmentZodSchema } from "./segmentSchema";
18
+ import { getTestZodSchema } from "./testSchema";
19
+ import {
20
+ assertProjectSetJsonSelection,
21
+ getProjectSetExecutions,
22
+ getProjectSetRelativeFilePath,
23
+ } from "../sets";
24
+ import { CLI_FORMAT_BOLD, CLI_FORMAT_GREEN, CLI_FORMAT_RED, colorize } from "../tester/cliFormat";
25
+ import { prettyDuration } from "../tester/prettyDuration";
26
+
27
+ export type LintEntityType =
28
+ | "locale"
29
+ | "attribute"
30
+ | "segment"
31
+ | "message"
32
+ | "target"
33
+ | "test"
34
+ | "project";
35
+
36
+ export interface LintProjectOptions {
37
+ keyPattern?: string;
38
+ entityType?: string;
39
+ set?: string;
40
+ json?: boolean;
41
+ pretty?: boolean;
42
+ }
43
+
44
+ export interface LintError {
45
+ level: "error";
46
+ filePath: string;
47
+ entityType: LintEntityType;
48
+ entityKey: string;
49
+ message: string;
50
+ path: (string | number)[];
51
+ code?: string;
52
+ value?: unknown;
53
+ }
54
+
55
+ export interface LintResult {
56
+ hasError: boolean;
57
+ errors: LintError[];
58
+ duration: number;
59
+ }
60
+
61
+ const ENTITY_NAME_REGEX_ERROR = "Names must be alphanumeric and can contain _, -, /, and .";
62
+
63
+ function isValidEntityKey(projectConfig: ProjectConfig, key: string) {
64
+ return /^[a-zA-Z0-9_\-/]+$/.test(key.split(projectConfig.namespaceCharacter).join(""));
65
+ }
66
+
67
+ function getParserExtension(projectConfig: ProjectConfig) {
68
+ return (projectConfig.parser as any).extension || "yml";
69
+ }
70
+
71
+ function getFullPathFromKey(projectConfig: ProjectConfig, entityType: LintEntityType, key: string) {
72
+ const fileName = `${key.split(projectConfig.namespaceCharacter).join(path.sep)}.${getParserExtension(projectConfig)}`;
73
+
74
+ if (entityType === "locale") {
75
+ return path.join(projectConfig.localesDirectoryPath, fileName);
76
+ }
77
+
78
+ if (entityType === "attribute") {
79
+ return path.join(projectConfig.attributesDirectoryPath, fileName);
80
+ }
81
+
82
+ if (entityType === "segment") {
83
+ return path.join(projectConfig.segmentsDirectoryPath, fileName);
84
+ }
85
+
86
+ if (entityType === "message") {
87
+ return path.join(projectConfig.messagesDirectoryPath, fileName);
88
+ }
89
+
90
+ if (entityType === "target") {
91
+ return path.join(projectConfig.targetsDirectoryPath, fileName);
92
+ }
93
+
94
+ if (entityType === "test") {
95
+ const specFileName = `${key
96
+ .split(projectConfig.namespaceCharacter)
97
+ .join(path.sep)}.spec.${getParserExtension(projectConfig)}`;
98
+ const specFilePath = path.join(projectConfig.testsDirectoryPath, specFileName);
99
+
100
+ return fs.existsSync(specFilePath)
101
+ ? specFilePath
102
+ : path.join(projectConfig.testsDirectoryPath, fileName);
103
+ }
104
+
105
+ return path.join(process.cwd(), "messagevisor.config.js");
106
+ }
107
+
108
+ async function readAll<T>(
109
+ keys: string[],
110
+ read: (key: string) => Promise<T>,
111
+ ): Promise<Record<string, T>> {
112
+ const entries: [string, T][] = [];
113
+
114
+ for (const key of keys) {
115
+ try {
116
+ entries.push([key, await read(key)]);
117
+ } catch {
118
+ // Parse/read errors are reported during entity validation.
119
+ }
120
+ }
121
+
122
+ return Object.fromEntries(entries);
123
+ }
124
+
125
+ export async function lintProject(
126
+ projectConfig: ProjectConfig,
127
+ datasource: Datasource,
128
+ options: LintProjectOptions = {},
129
+ ): Promise<LintResult> {
130
+ const startTime = Date.now();
131
+ const errors: LintError[] = [];
132
+ const keyPattern = options.keyPattern ? new RegExp(options.keyPattern) : null;
133
+
134
+ function shouldLintKey(key: string) {
135
+ return !keyPattern || keyPattern.test(key);
136
+ }
137
+
138
+ function recordError(error: Omit<LintError, "level">) {
139
+ errors.push({ level: "error", ...error });
140
+ }
141
+
142
+ function reportZodError(
143
+ entityType: LintEntityType,
144
+ key: string,
145
+ fullPath: string,
146
+ error: ZodError,
147
+ ) {
148
+ for (const issue of getLintIssuesFromZodError(error)) {
149
+ recordError({
150
+ filePath: path.relative(process.cwd(), fullPath),
151
+ entityType,
152
+ entityKey: key,
153
+ message: issue.message,
154
+ path: issue.path,
155
+ code: issue.code,
156
+ value: issue.value,
157
+ });
158
+ }
159
+ }
160
+
161
+ async function lintEntity(
162
+ entityType: Exclude<LintEntityType, "project">,
163
+ key: string,
164
+ schema: ZodTypeAny,
165
+ read: (key: string) => Promise<unknown>,
166
+ ) {
167
+ const fullPath = getFullPathFromKey(projectConfig, entityType, key);
168
+
169
+ if (!isValidEntityKey(projectConfig, key)) {
170
+ recordError({
171
+ filePath: path.relative(process.cwd(), fullPath),
172
+ entityType,
173
+ entityKey: key,
174
+ message: `Invalid name: "${key}". ${ENTITY_NAME_REGEX_ERROR}`,
175
+ path: [],
176
+ code: "invalid_name",
177
+ });
178
+ }
179
+
180
+ try {
181
+ const parsed = await read(key);
182
+ const result = schema.safeParse(parsed);
183
+
184
+ if (!result.success) {
185
+ reportZodError(entityType, key, fullPath, result.error);
186
+ }
187
+ } catch (error) {
188
+ recordError({
189
+ filePath: path.relative(process.cwd(), fullPath),
190
+ entityType,
191
+ entityKey: key,
192
+ message: error instanceof Error ? error.message : String(error),
193
+ path: [],
194
+ code: error instanceof Error ? error.name : "error",
195
+ });
196
+ }
197
+ }
198
+
199
+ const [localeKeys, attributeKeys, segmentKeys, messageKeys, targetKeys, testKeys] =
200
+ await Promise.all([
201
+ datasource.listLocales(),
202
+ datasource.listAttributes(),
203
+ datasource.listSegments(),
204
+ datasource.listMessages(),
205
+ datasource.listTargets(),
206
+ datasource.listTests(),
207
+ ]);
208
+
209
+ if (localeKeys.length === 0) {
210
+ recordError({
211
+ filePath: "messagevisor.config.js",
212
+ entityType: "project",
213
+ entityKey: "messagevisor.config.js",
214
+ message: "At least one locale is required",
215
+ path: ["locales"],
216
+ code: "missing_locale",
217
+ });
218
+ }
219
+
220
+ const localesByKey = await readAll<Locale>(localeKeys, (key) => datasource.readLocale(key));
221
+ const attributesByKey = await readAll<Attribute>(attributeKeys, (key) =>
222
+ datasource.readAttribute(key),
223
+ );
224
+ const messagesByKey = await readAll<Message>(messageKeys, (key) => datasource.readMessage(key));
225
+
226
+ const localeZodSchema = getLocaleZodSchema(localeKeys, messageKeys);
227
+ const attributeZodSchema = getAttributeZodSchema();
228
+ const conditionsZodSchema = getConditionsZodSchema(attributesByKey);
229
+ const segmentZodSchema = getSegmentZodSchema(conditionsZodSchema);
230
+ const messageZodSchema = getMessageZodSchema(localeKeys, segmentKeys, attributesByKey, {
231
+ namespaceCharacter: projectConfig.namespaceCharacter,
232
+ exportOverrideKeySeparator: projectConfig.exportOverrideKeySeparator,
233
+ });
234
+ const targetZodSchema = getTargetZodSchema(localeKeys);
235
+ const testZodSchema = getTestZodSchema(messageKeys, segmentKeys, localeKeys, targetKeys);
236
+
237
+ if (!options.entityType || options.entityType === "locale") {
238
+ for (const key of localeKeys.filter(shouldLintKey)) {
239
+ await lintEntity("locale", key, localeZodSchema, (entityKey) =>
240
+ datasource.readLocale(entityKey),
241
+ );
242
+ }
243
+ }
244
+
245
+ for (const field of [
246
+ "inheritFormatsFrom",
247
+ "inheritTranslationsFrom",
248
+ "mergeExamplesFrom",
249
+ ] as const) {
250
+ for (const circularDependency of checkLocaleCircularDependency(localesByKey, field)) {
251
+ const key = circularDependency.cycle[0];
252
+ const fullPath = getFullPathFromKey(projectConfig, "locale", key);
253
+
254
+ recordError({
255
+ filePath: path.relative(process.cwd(), fullPath),
256
+ entityType: "locale",
257
+ entityKey: key,
258
+ message: `Circular locale dependency detected for ${field}: ${circularDependency.cycle.join(" -> ")}`,
259
+ path: [field],
260
+ code: "circular_locale_dependency",
261
+ });
262
+ }
263
+ }
264
+
265
+ if (!options.entityType || options.entityType === "attribute") {
266
+ for (const key of attributeKeys.filter(shouldLintKey)) {
267
+ await lintEntity("attribute", key, attributeZodSchema, (entityKey) =>
268
+ datasource.readAttribute(entityKey),
269
+ );
270
+ }
271
+ }
272
+
273
+ if (!options.entityType || options.entityType === "segment") {
274
+ for (const key of segmentKeys.filter(shouldLintKey)) {
275
+ await lintEntity("segment", key, segmentZodSchema, (entityKey) =>
276
+ datasource.readSegment(entityKey),
277
+ );
278
+ }
279
+ }
280
+
281
+ if (!options.entityType || options.entityType === "message") {
282
+ for (const key of messageKeys.filter(shouldLintKey)) {
283
+ await lintEntity("message", key, messageZodSchema, (entityKey) =>
284
+ datasource.readMessage(entityKey),
285
+ );
286
+ }
287
+
288
+ errors.push(
289
+ ...lintMessageIcuFormatStyles(
290
+ Object.fromEntries(Object.entries(messagesByKey).filter(([key]) => shouldLintKey(key))),
291
+ localesByKey,
292
+ (key) => path.relative(process.cwd(), getFullPathFromKey(projectConfig, "message", key)),
293
+ { icuSkeleton: projectConfig.icuSkeleton },
294
+ ),
295
+ );
296
+ }
297
+
298
+ if (!options.entityType || options.entityType === "target") {
299
+ for (const key of targetKeys.filter(shouldLintKey)) {
300
+ await lintEntity("target", key, targetZodSchema, (entityKey) =>
301
+ datasource.readTarget(entityKey),
302
+ );
303
+ }
304
+ }
305
+
306
+ if (!options.entityType || options.entityType === "test") {
307
+ for (const key of testKeys.filter(shouldLintKey)) {
308
+ await lintEntity("test", key, testZodSchema, (entityKey) => datasource.readTest(entityKey));
309
+ }
310
+ }
311
+
312
+ return {
313
+ hasError: errors.length > 0,
314
+ errors,
315
+ duration: Date.now() - startTime,
316
+ };
317
+ }
318
+
319
+ function formatPath(errorPath: (string | number)[]) {
320
+ return errorPath.reduce<string>((pathSegments, entry) => {
321
+ if (typeof entry === "number") {
322
+ return `${pathSegments}[${entry}]`;
323
+ }
324
+
325
+ return pathSegments ? `${pathSegments}.${entry}` : entry;
326
+ }, "");
327
+ }
328
+
329
+ function getLintOptionsLabel(parsed: any) {
330
+ const labels = [];
331
+
332
+ if (parsed.set) {
333
+ labels.push(`set: ${parsed.set}`);
334
+ }
335
+
336
+ if (parsed.entityType) {
337
+ labels.push(`entity: ${parsed.entityType}`);
338
+ }
339
+
340
+ if (parsed.keyPattern) {
341
+ labels.push(`keyPattern: ${parsed.keyPattern}`);
342
+ }
343
+
344
+ return labels.length > 0 ? labels.join(", ") : "all definitions";
345
+ }
346
+
347
+ async function lintProjectSets(
348
+ projectConfig: ProjectConfig,
349
+ datasource: Datasource,
350
+ options: LintProjectOptions = {},
351
+ ): Promise<LintResult> {
352
+ const startTime = Date.now();
353
+ const errors: LintError[] = [];
354
+ const setExecutions = await getProjectSetExecutions(projectConfig, datasource, options.set);
355
+
356
+ for (const execution of setExecutions) {
357
+ const result = await lintProject(execution.projectConfig, execution.datasource, options);
358
+
359
+ errors.push(
360
+ ...result.errors.map((error) => ({
361
+ ...error,
362
+ filePath: projectConfig.sets
363
+ ? getProjectSetRelativeFilePath(projectConfig, execution.set, error.filePath)
364
+ : error.filePath,
365
+ entityKey: projectConfig.sets ? `${execution.set}/${error.entityKey}` : error.entityKey,
366
+ })),
367
+ );
368
+ }
369
+
370
+ return {
371
+ hasError: errors.length > 0,
372
+ errors,
373
+ duration: Date.now() - startTime,
374
+ };
375
+ }
376
+
377
+ function groupLintErrors(errors: LintError[]) {
378
+ const groups: Array<{ label: string; filePath: string; errors: LintError[] }> = [];
379
+ const groupIndexes = new Map<string, number>();
380
+
381
+ for (const error of errors) {
382
+ const label = `${error.entityType} "${error.entityKey}"`;
383
+ const groupKey = `${label}:${error.filePath}`;
384
+ let groupIndex = groupIndexes.get(groupKey);
385
+
386
+ if (typeof groupIndex === "undefined") {
387
+ groupIndex = groups.length;
388
+ groupIndexes.set(groupKey, groupIndex);
389
+ groups.push({ label, filePath: error.filePath, errors: [] });
390
+ }
391
+
392
+ groups[groupIndex].errors.push(error);
393
+ }
394
+
395
+ return groups;
396
+ }
397
+
398
+ function printLintResult(result: LintResult, parsed: any) {
399
+ console.log("");
400
+ console.log(CLI_FORMAT_BOLD, "Linting Messagevisor definitions");
401
+ console.log(` ${colorize("Target", 36)}: ${getLintOptionsLabel(parsed)}`);
402
+ console.log("");
403
+
404
+ if (!result.hasError) {
405
+ console.log(CLI_FORMAT_GREEN, "✔ No lint errors found");
406
+ console.log(CLI_FORMAT_BOLD, `Time: ${prettyDuration(result.duration)}`);
407
+ return;
408
+ }
409
+
410
+ console.log(CLI_FORMAT_RED, `✘ ${result.errors.length} lint error(s) found`);
411
+ console.log("");
412
+
413
+ for (const group of groupLintErrors(result.errors)) {
414
+ console.log(CLI_FORMAT_BOLD, group.label);
415
+ console.log(` ${colorize(group.filePath, 36)}`);
416
+
417
+ for (const error of group.errors) {
418
+ const errorPath = formatPath(error.path);
419
+ const code = error.code ? colorize(error.code, 33) : colorize("lint_error", 33);
420
+ const pathSuffix = errorPath ? ` ${colorize(errorPath, 2)}` : "";
421
+
422
+ console.error(` ${colorize("✘", 31)} [${code}]${pathSuffix}`);
423
+ console.error(` ${error.message}`);
424
+ }
425
+
426
+ console.log("");
427
+ }
428
+
429
+ console.log(CLI_FORMAT_RED, `Errors: ${result.errors.length} failed`);
430
+ console.log(CLI_FORMAT_BOLD, `Time: ${prettyDuration(result.duration)}`);
431
+ }
432
+
433
+ export const lintPlugin = {
434
+ command: "lint",
435
+ handler: async ({ projectConfig, datasource, parsed }: any) => {
436
+ assertProjectSetJsonSelection(projectConfig, parsed.set, parsed.json);
437
+
438
+ const result = await lintProjectSets(projectConfig, datasource, {
439
+ set: parsed.set,
440
+ entityType: parsed.entityType,
441
+ keyPattern: parsed.keyPattern,
442
+ json: parsed.json,
443
+ pretty: parsed.pretty,
444
+ });
445
+
446
+ if (parsed.json) {
447
+ console.log(
448
+ parsed.pretty ? JSON.stringify(result.errors, null, 2) : JSON.stringify(result.errors),
449
+ );
450
+ } else {
451
+ printLintResult(result, parsed);
452
+ }
453
+
454
+ return !result.hasError;
455
+ },
456
+ examples: [
457
+ { command: "lint", description: "lint Messagevisor project definitions" },
458
+ { command: "lint --json --pretty", description: "print lint results as JSON" },
459
+ ],
460
+ };
@@ -0,0 +1,70 @@
1
+ import { z } from "zod";
2
+
3
+ import { formatPresetsZodSchema } from "./formatSchema";
4
+ import { refineWithMessage } from "./zodHelpers";
5
+
6
+ export function getLocaleZodSchema(localeKeys: string[], messageKeys: string[]) {
7
+ const matrixZodSchema = z.record(
8
+ z.string(),
9
+ z.array(z.union([z.string(), z.number(), z.boolean(), z.null()])),
10
+ );
11
+
12
+ const localeExampleSchema = z
13
+ .object({
14
+ matrix: matrixZodSchema.optional(),
15
+ index: z.number().int().min(0).optional(),
16
+ description: z.string().optional(),
17
+ values: z.record(z.string(), z.unknown()).optional(),
18
+ context: z.record(z.string(), z.unknown()).optional(),
19
+ formats: formatPresetsZodSchema.optional(),
20
+ timeZone: z.string().optional(),
21
+ currency: z.string().optional(),
22
+ rawMessage: z.string().optional(),
23
+ message: refineWithMessage(
24
+ z.string(),
25
+ (value) => messageKeys.includes(value),
26
+ (value) => `Unknown message "${value}"`,
27
+ ).optional(),
28
+ })
29
+ .strict()
30
+ .superRefine((data, ctx) => {
31
+ const hasRawMessage = typeof data.rawMessage !== "undefined";
32
+ const hasMessage = typeof data.message !== "undefined";
33
+
34
+ if (hasRawMessage === hasMessage) {
35
+ ctx.addIssue({
36
+ code: z.ZodIssueCode.custom,
37
+ message: "Example must define exactly one of `rawMessage` or `message`.",
38
+ path: ["rawMessage"],
39
+ });
40
+ }
41
+ });
42
+
43
+ return z
44
+ .object({
45
+ key: z.string().optional(),
46
+ promotable: z.boolean().optional(),
47
+ description: z.string({
48
+ error: (issue) => (issue.input === undefined ? "Required" : undefined),
49
+ }),
50
+ direction: z.enum(["ltr", "rtl"]).optional(),
51
+ inheritFormatsFrom: refineWithMessage(
52
+ z.string(),
53
+ (value) => localeKeys.includes(value),
54
+ (value) => `Unknown locale "${value}"`,
55
+ ).optional(),
56
+ inheritTranslationsFrom: refineWithMessage(
57
+ z.string(),
58
+ (value) => localeKeys.includes(value),
59
+ (value) => `Unknown locale "${value}"`,
60
+ ).optional(),
61
+ mergeExamplesFrom: refineWithMessage(
62
+ z.string(),
63
+ (value) => localeKeys.includes(value),
64
+ (value) => `Unknown locale "${value}"`,
65
+ ).optional(),
66
+ formats: formatPresetsZodSchema.optional(),
67
+ examples: z.array(localeExampleSchema).optional(),
68
+ })
69
+ .strict();
70
+ }
@@ -0,0 +1,152 @@
1
+ import { z } from "zod";
2
+
3
+ import { getConditionsZodSchema } from "./conditionSchema";
4
+ import { valueZodSchema } from "./schema";
5
+ import { refineWithMessage } from "./zodHelpers";
6
+
7
+ export function getGroupSegmentZodSchema(segmentKeys: string[]) {
8
+ type GroupSegmentInput = any;
9
+
10
+ const groupSegmentZodSchema: z.ZodType<GroupSegmentInput> = z.lazy(() =>
11
+ z.union([
12
+ refineWithMessage(
13
+ z.string(),
14
+ (value) => segmentKeys.includes(value),
15
+ (value) => `Unknown segment "${value}"`,
16
+ ),
17
+ z.object({ and: z.array(groupSegmentZodSchema).min(1) }).strict(),
18
+ z.object({ or: z.array(groupSegmentZodSchema).min(1) }).strict(),
19
+ z.object({ not: z.array(groupSegmentZodSchema).min(1) }).strict(),
20
+ ]),
21
+ );
22
+
23
+ return z.union([z.literal("*"), groupSegmentZodSchema, z.array(groupSegmentZodSchema).min(1)]);
24
+ }
25
+
26
+ export function getMessageZodSchema(
27
+ localeKeys: string[],
28
+ segmentKeys: string[],
29
+ attributesByKey: any,
30
+ options: {
31
+ namespaceCharacter: string;
32
+ exportOverrideKeySeparator: string;
33
+ },
34
+ ) {
35
+ const matrixZodSchema = z.record(
36
+ z.string(),
37
+ z.array(z.union([z.string(), z.number(), z.boolean(), z.null()])),
38
+ );
39
+
40
+ const messageExampleLocaleSchema = refineWithMessage(
41
+ z.string(),
42
+ (value) => localeKeys.includes(value) || /^\$\{\{.+\}\}$/.test(value),
43
+ (value) => `Unknown locale "${value}"`,
44
+ );
45
+
46
+ const messageExampleZodSchema = z
47
+ .object({
48
+ matrix: matrixZodSchema.optional(),
49
+ index: z.number().int().min(0).optional(),
50
+ description: z.string().optional(),
51
+ locale: messageExampleLocaleSchema,
52
+ values: z.record(z.string(), z.unknown()).optional(),
53
+ context: z.record(z.string(), z.unknown()).optional(),
54
+ formats: z.record(z.string(), z.unknown()).optional(),
55
+ timeZone: z.string().optional(),
56
+ currency: z.string().optional(),
57
+ })
58
+ .strict();
59
+
60
+ const localeTranslations = z
61
+ .record(
62
+ refineWithMessage(
63
+ z.string(),
64
+ (value) => localeKeys.includes(value),
65
+ (value) => `Unknown locale "${value}"`,
66
+ ),
67
+ z.string(),
68
+ )
69
+ .refine((value) => Object.keys(value).length > 0, {
70
+ message: "At least one translation is required",
71
+ });
72
+
73
+ const conditionsZodSchema = getConditionsZodSchema(attributesByKey);
74
+ const groupSegmentZodSchema = getGroupSegmentZodSchema(segmentKeys);
75
+
76
+ const overrideZodSchema = z
77
+ .object({
78
+ key: z.string().min(1),
79
+ description: z.string().optional(),
80
+ summary: z.string().optional(),
81
+ conditions: conditionsZodSchema.optional(),
82
+ segments: groupSegmentZodSchema.optional(),
83
+ translations: localeTranslations,
84
+ })
85
+ .strict()
86
+ .superRefine((data, ctx) => {
87
+ if (!data.conditions && !data.segments) {
88
+ ctx.addIssue({
89
+ code: z.ZodIssueCode.custom,
90
+ message: "Override must define either `conditions` or `segments`.",
91
+ path: ["conditions"],
92
+ });
93
+ }
94
+ });
95
+
96
+ return z
97
+ .object({
98
+ key: z.string().optional(),
99
+ archived: z.boolean().optional(),
100
+ promotable: z.boolean().optional(),
101
+ deprecated: z.boolean().optional(),
102
+ deprecationWarning: z.string().optional(),
103
+ description: z.string({
104
+ error: (issue) => (issue.input === undefined ? "Required" : undefined),
105
+ }),
106
+ summary: z.string().optional(),
107
+ meta: z.record(z.string(), valueZodSchema).optional(),
108
+ examples: z.array(messageExampleZodSchema).optional(),
109
+ translations: localeTranslations,
110
+ overrides: z.array(overrideZodSchema).optional(),
111
+ })
112
+ .strict()
113
+ .superRefine((data, ctx) => {
114
+ const overrideKeys = new Set<string>();
115
+
116
+ for (let index = 0; index < (data.overrides || []).length; index++) {
117
+ const override = (data.overrides || [])[index];
118
+ const disallowedCharacters = [
119
+ ["namespaceCharacter", options.namespaceCharacter],
120
+ ["exportOverrideKeySeparator", options.exportOverrideKeySeparator],
121
+ ];
122
+
123
+ for (const [label, character] of disallowedCharacters) {
124
+ if (override.key.includes(character)) {
125
+ ctx.addIssue({
126
+ code: z.ZodIssueCode.custom,
127
+ message: `Override key "${override.key}" must not include ${label} "${character}".`,
128
+ path: ["overrides", index, "key"],
129
+ });
130
+ }
131
+ }
132
+
133
+ if (overrideKeys.has(override.key)) {
134
+ ctx.addIssue({
135
+ code: z.ZodIssueCode.custom,
136
+ message: `Duplicate override key "${override.key}". Override keys must be unique within a message.`,
137
+ path: ["overrides", index, "key"],
138
+ });
139
+ }
140
+
141
+ overrideKeys.add(override.key);
142
+ }
143
+
144
+ if (data.deprecationWarning && !data.deprecated) {
145
+ ctx.addIssue({
146
+ code: z.ZodIssueCode.custom,
147
+ message: "`deprecationWarning` can only be set when `deprecated` is true.",
148
+ path: ["deprecationWarning"],
149
+ });
150
+ }
151
+ });
152
+ }