@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.
- package/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +7 -0
- package/jest.config.js +8 -0
- package/lib/benchmark/index.d.ts +2 -0
- package/lib/benchmark/index.js +417 -0
- package/lib/benchmark/index.js.map +1 -0
- package/lib/builder/index.d.ts +70 -0
- package/lib/builder/index.js +831 -0
- package/lib/builder/index.js.map +1 -0
- package/lib/cli/index.d.ts +28 -0
- package/lib/cli/index.js +182 -0
- package/lib/cli/index.js.map +1 -0
- package/lib/config/index.d.ts +61 -0
- package/lib/config/index.js +255 -0
- package/lib/config/index.js.map +1 -0
- package/lib/create/index.d.ts +2 -0
- package/lib/create/index.js +405 -0
- package/lib/create/index.js.map +1 -0
- package/lib/datasource/filesystemAdapter.d.ts +44 -0
- package/lib/datasource/filesystemAdapter.js +424 -0
- package/lib/datasource/filesystemAdapter.js.map +1 -0
- package/lib/datasource/index.d.ts +39 -0
- package/lib/datasource/index.js +96 -0
- package/lib/datasource/index.js.map +1 -0
- package/lib/error.d.ts +6 -0
- package/lib/error.js +49 -0
- package/lib/error.js.map +1 -0
- package/lib/evaluate/cli.d.ts +8 -0
- package/lib/evaluate/cli.js +179 -0
- package/lib/evaluate/cli.js.map +1 -0
- package/lib/evaluate/index.d.ts +10 -0
- package/lib/evaluate/index.js +131 -0
- package/lib/evaluate/index.js.map +1 -0
- package/lib/examples/coerceExampleIsoDates.d.ts +12 -0
- package/lib/examples/coerceExampleIsoDates.js +81 -0
- package/lib/examples/coerceExampleIsoDates.js.map +1 -0
- package/lib/examples/index.d.ts +63 -0
- package/lib/examples/index.js +713 -0
- package/lib/examples/index.js.map +1 -0
- package/lib/exporter/index.d.ts +60 -0
- package/lib/exporter/index.js +610 -0
- package/lib/exporter/index.js.map +1 -0
- package/lib/find-duplicates/index.d.ts +41 -0
- package/lib/find-duplicates/index.js +297 -0
- package/lib/find-duplicates/index.js.map +1 -0
- package/lib/generate-code/index.d.ts +11 -0
- package/lib/generate-code/index.js +157 -0
- package/lib/generate-code/index.js.map +1 -0
- package/lib/generate-code/typescript.d.ts +14 -0
- package/lib/generate-code/typescript.js +307 -0
- package/lib/generate-code/typescript.js.map +1 -0
- package/lib/importer/index.d.ts +64 -0
- package/lib/importer/index.js +1092 -0
- package/lib/importer/index.js.map +1 -0
- package/lib/index.d.ts +18 -0
- package/lib/index.js +35 -0
- package/lib/index.js.map +1 -0
- package/lib/info/index.d.ts +17 -0
- package/lib/info/index.js +132 -0
- package/lib/info/index.js.map +1 -0
- package/lib/init/index.d.ts +30 -0
- package/lib/init/index.js +348 -0
- package/lib/init/index.js.map +1 -0
- package/lib/lint/index.d.ts +1 -0
- package/lib/lint/index.js +6 -0
- package/lib/lint/index.js.map +1 -0
- package/lib/linter/attributeSchema.d.ts +7 -0
- package/lib/linter/attributeSchema.js +36 -0
- package/lib/linter/attributeSchema.js.map +1 -0
- package/lib/linter/checkLocaleCircularDependency.d.ts +7 -0
- package/lib/linter/checkLocaleCircularDependency.js +42 -0
- package/lib/linter/checkLocaleCircularDependency.js.map +1 -0
- package/lib/linter/conditionSchema.d.ts +3 -0
- package/lib/linter/conditionSchema.js +283 -0
- package/lib/linter/conditionSchema.js.map +1 -0
- package/lib/linter/formatSchema.d.ts +325 -0
- package/lib/linter/formatSchema.js +165 -0
- package/lib/linter/formatSchema.js.map +1 -0
- package/lib/linter/icuStyleLint.d.ts +6 -0
- package/lib/linter/icuStyleLint.js +226 -0
- package/lib/linter/icuStyleLint.js.map +1 -0
- package/lib/linter/index.d.ts +34 -0
- package/lib/linter/index.js +557 -0
- package/lib/linter/index.js.map +1 -0
- package/lib/linter/localeSchema.d.ts +672 -0
- package/lib/linter/localeSchema.js +50 -0
- package/lib/linter/localeSchema.js.map +1 -0
- package/lib/linter/messageSchema.d.ts +35 -0
- package/lib/linter/messageSchema.js +115 -0
- package/lib/linter/messageSchema.js.map +1 -0
- package/lib/linter/printError.d.ts +8 -0
- package/lib/linter/printError.js +41 -0
- package/lib/linter/printError.js.map +1 -0
- package/lib/linter/schema.d.ts +33 -0
- package/lib/linter/schema.js +192 -0
- package/lib/linter/schema.js.map +1 -0
- package/lib/linter/segmentSchema.d.ts +8 -0
- package/lib/linter/segmentSchema.js +18 -0
- package/lib/linter/segmentSchema.js.map +1 -0
- package/lib/linter/targetSchema.d.ts +337 -0
- package/lib/linter/targetSchema.js +39 -0
- package/lib/linter/targetSchema.js.map +1 -0
- package/lib/linter/testSchema.d.ts +71 -0
- package/lib/linter/testSchema.js +165 -0
- package/lib/linter/testSchema.js.map +1 -0
- package/lib/linter/zodHelpers.d.ts +2 -0
- package/lib/linter/zodHelpers.js +15 -0
- package/lib/linter/zodHelpers.js.map +1 -0
- package/lib/list/index.d.ts +8 -0
- package/lib/list/index.js +524 -0
- package/lib/list/index.js.map +1 -0
- package/lib/matrix.d.ts +4 -0
- package/lib/matrix.js +66 -0
- package/lib/matrix.js.map +1 -0
- package/lib/promoter/index.d.ts +65 -0
- package/lib/promoter/index.js +1208 -0
- package/lib/promoter/index.js.map +1 -0
- package/lib/prune/index.d.ts +37 -0
- package/lib/prune/index.js +673 -0
- package/lib/prune/index.js.map +1 -0
- package/lib/sets.d.ts +10 -0
- package/lib/sets.js +120 -0
- package/lib/sets.js.map +1 -0
- package/lib/tester/cliFormat.d.ts +8 -0
- package/lib/tester/cliFormat.js +15 -0
- package/lib/tester/cliFormat.js.map +1 -0
- package/lib/tester/index.d.ts +35 -0
- package/lib/tester/index.js +713 -0
- package/lib/tester/index.js.map +1 -0
- package/lib/tester/matrix.d.ts +14 -0
- package/lib/tester/matrix.js +76 -0
- package/lib/tester/matrix.js.map +1 -0
- package/lib/tester/prettyDuration.d.ts +1 -0
- package/lib/tester/prettyDuration.js +30 -0
- package/lib/tester/prettyDuration.js.map +1 -0
- package/lib/tester/printTestResult.d.ts +2 -0
- package/lib/tester/printTestResult.js +32 -0
- package/lib/tester/printTestResult.js.map +1 -0
- package/lib/tester/types.d.ts +29 -0
- package/lib/tester/types.js +3 -0
- package/lib/tester/types.js.map +1 -0
- package/package.json +41 -13
- package/src/benchmark/index.spec.ts +375 -0
- package/src/benchmark/index.ts +433 -0
- package/src/builder/index.spec.ts +822 -0
- package/src/builder/index.ts +920 -0
- package/src/cli/index.spec.ts +54 -0
- package/src/cli/index.ts +150 -0
- package/src/config/index.spec.ts +70 -0
- package/src/config/index.ts +259 -0
- package/src/create/index.spec.ts +272 -0
- package/src/create/index.ts +295 -0
- package/src/datasource/filesystemAdapter.ts +313 -0
- package/src/datasource/index.ts +135 -0
- package/src/error.ts +33 -0
- package/src/evaluate/cli.spec.ts +368 -0
- package/src/evaluate/cli.ts +130 -0
- package/src/evaluate/index.ts +161 -0
- package/src/examples/coerceExampleIsoDates.spec.ts +81 -0
- package/src/examples/coerceExampleIsoDates.ts +98 -0
- package/src/examples/index.spec.ts +453 -0
- package/src/examples/index.ts +854 -0
- package/src/exporter/index.spec.ts +443 -0
- package/src/exporter/index.ts +643 -0
- package/src/find-duplicates/index.spec.ts +289 -0
- package/src/find-duplicates/index.ts +314 -0
- package/src/generate-code/index.ts +92 -0
- package/src/generate-code/typescript.spec.ts +241 -0
- package/src/generate-code/typescript.ts +284 -0
- package/src/importer/index.spec.ts +1101 -0
- package/src/importer/index.ts +1190 -0
- package/src/index.ts +18 -0
- package/src/info/index.ts +67 -0
- package/src/init/index.spec.ts +279 -0
- package/src/init/index.ts +292 -0
- package/src/lint/index.ts +1 -0
- package/src/linter/attributeSchema.ts +38 -0
- package/src/linter/checkLocaleCircularDependency.ts +51 -0
- package/src/linter/conditionSchema.ts +386 -0
- package/src/linter/formatSchema.ts +170 -0
- package/src/linter/icuStyleLint.ts +312 -0
- package/src/linter/index.spec.ts +824 -0
- package/src/linter/index.ts +460 -0
- package/src/linter/localeSchema.ts +70 -0
- package/src/linter/messageSchema.ts +152 -0
- package/src/linter/printError.ts +52 -0
- package/src/linter/schema.ts +230 -0
- package/src/linter/segmentSchema.ts +15 -0
- package/src/linter/targetSchema.ts +50 -0
- package/src/linter/testSchema.spec.ts +405 -0
- package/src/linter/testSchema.ts +239 -0
- package/src/linter/zodHelpers.ts +16 -0
- package/src/list/index.spec.ts +431 -0
- package/src/list/index.ts +463 -0
- package/src/matrix.ts +69 -0
- package/src/promoter/index.spec.ts +584 -0
- package/src/promoter/index.ts +1267 -0
- package/src/prune/index.spec.ts +418 -0
- package/src/prune/index.ts +693 -0
- package/src/sets.ts +74 -0
- package/src/tester/cliFormat.ts +11 -0
- package/src/tester/featurevisorIntegration.spec.ts +101 -0
- package/src/tester/index.spec.ts +577 -0
- package/src/tester/index.ts +679 -0
- package/src/tester/matrix.ts +106 -0
- package/src/tester/prettyDuration.ts +34 -0
- package/src/tester/printTestResult.ts +40 -0
- package/src/tester/types.ts +32 -0
- package/tsconfig.cjs.json +11 -0
- package/tsconfig.typecheck.json +4 -0
|
@@ -0,0 +1,1267 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
Attribute,
|
|
7
|
+
Condition,
|
|
8
|
+
Locale,
|
|
9
|
+
Message,
|
|
10
|
+
Target,
|
|
11
|
+
Segment,
|
|
12
|
+
Test,
|
|
13
|
+
} from "@messagevisor/types";
|
|
14
|
+
|
|
15
|
+
import type { ProjectConfig } from "../config";
|
|
16
|
+
import type { Datasource } from "../datasource";
|
|
17
|
+
import { MessagevisorCLIError, printMessagevisorCLIError } from "../error";
|
|
18
|
+
import { lintProject, type LintError } from "../linter";
|
|
19
|
+
import { CLI_FORMAT_BOLD, CLI_FORMAT_GREEN, colorize } from "../tester/cliFormat";
|
|
20
|
+
import { prettyDuration } from "../tester/prettyDuration";
|
|
21
|
+
|
|
22
|
+
type EntityType = "locale" | "attribute" | "segment" | "target" | "message" | "test";
|
|
23
|
+
type ConflictPolicy = "source" | "destination" | "fail";
|
|
24
|
+
type PromotionAuditFormat = "json" | "markdown";
|
|
25
|
+
|
|
26
|
+
type EntityValue = Locale | Attribute | Segment | Target | Message | Test;
|
|
27
|
+
|
|
28
|
+
function isPromotable(entity: { promotable?: boolean } | undefined) {
|
|
29
|
+
return entity?.promotable !== false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface PromotionConflict {
|
|
33
|
+
type: EntityType;
|
|
34
|
+
key: string;
|
|
35
|
+
path: string;
|
|
36
|
+
source: unknown;
|
|
37
|
+
destination: unknown;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface EntityPlan {
|
|
41
|
+
type: EntityType;
|
|
42
|
+
key: string;
|
|
43
|
+
source: EntityValue;
|
|
44
|
+
destination?: EntityValue;
|
|
45
|
+
merged: EntityValue;
|
|
46
|
+
conflicts: PromotionConflict[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface PromoteProjectSetsOptions {
|
|
50
|
+
from?: string;
|
|
51
|
+
to?: string;
|
|
52
|
+
target?: string | string[];
|
|
53
|
+
locale?: string | string[];
|
|
54
|
+
includeMessages?: string | string[];
|
|
55
|
+
excludeMessages?: string | string[];
|
|
56
|
+
excludeOverrides?: boolean;
|
|
57
|
+
conflicts?: ConflictPolicy;
|
|
58
|
+
allowEmpty?: boolean;
|
|
59
|
+
apply?: boolean;
|
|
60
|
+
audit?: boolean | PromotionAuditFormat;
|
|
61
|
+
showUnchanged?: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface PromoteProjectSetsResult {
|
|
65
|
+
from: string;
|
|
66
|
+
to: string;
|
|
67
|
+
apply: boolean;
|
|
68
|
+
duration: number;
|
|
69
|
+
filters: {
|
|
70
|
+
targets: string[];
|
|
71
|
+
locales: string[];
|
|
72
|
+
includeMessages: string[];
|
|
73
|
+
excludeMessages: string[];
|
|
74
|
+
excludeOverrides: boolean;
|
|
75
|
+
conflicts: ConflictPolicy;
|
|
76
|
+
};
|
|
77
|
+
dependencies: {
|
|
78
|
+
locales: number;
|
|
79
|
+
attributes: number;
|
|
80
|
+
segments: number;
|
|
81
|
+
targets: number;
|
|
82
|
+
messages: number;
|
|
83
|
+
tests: number;
|
|
84
|
+
};
|
|
85
|
+
files: {
|
|
86
|
+
created: string[];
|
|
87
|
+
updated: string[];
|
|
88
|
+
unchanged: string[];
|
|
89
|
+
};
|
|
90
|
+
conflicts: PromotionConflict[];
|
|
91
|
+
auditFilePath?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function assertAllowedPromotionFlow(projectConfig: ProjectConfig, from: string, to: string) {
|
|
95
|
+
const allowedFlows = projectConfig.promotionFlows;
|
|
96
|
+
|
|
97
|
+
if (typeof allowedFlows === "undefined") {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const isAllowed = allowedFlows.some((flow) => flow.from === from && flow.to === to);
|
|
102
|
+
|
|
103
|
+
if (isAllowed) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const allowedList = allowedFlows.map((flow) => `${flow.from} -> ${flow.to}`).join(", ") || "none";
|
|
108
|
+
|
|
109
|
+
throw new MessagevisorCLIError(
|
|
110
|
+
`Promotion from "${from}" to "${to}" is not allowed by this project's configured promotionFlows.\nAllowed flows: ${allowedList}.\nChoose one of the allowed promotion paths or update messagevisor.config.js if this flow should be permitted.`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function toArray(value: string | string[] | undefined): string[] {
|
|
115
|
+
if (typeof value === "undefined") {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return Array.isArray(value) ? value : [value];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
123
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function deepEqual(left: unknown, right: unknown): boolean {
|
|
127
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getArrayEntryIdentity(value: unknown): string | undefined {
|
|
131
|
+
if (!isPlainObject(value)) {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (typeof value.description === "string") {
|
|
136
|
+
return `description:${value.description}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const identity: Record<string, unknown> = {};
|
|
140
|
+
|
|
141
|
+
for (const key of [
|
|
142
|
+
"locale",
|
|
143
|
+
"target",
|
|
144
|
+
"segment",
|
|
145
|
+
"context",
|
|
146
|
+
"values",
|
|
147
|
+
"withFlags",
|
|
148
|
+
"withVariations",
|
|
149
|
+
"currency",
|
|
150
|
+
"timeZone",
|
|
151
|
+
]) {
|
|
152
|
+
if (typeof value[key] !== "undefined") {
|
|
153
|
+
identity[key] = value[key];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return Object.keys(identity).length > 0 ? JSON.stringify(identity) : undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function deepMerge(destination: unknown, source: unknown): unknown {
|
|
161
|
+
if (typeof destination === "undefined") return source;
|
|
162
|
+
if (typeof source === "undefined") return destination;
|
|
163
|
+
|
|
164
|
+
if (Array.isArray(destination) && Array.isArray(source)) {
|
|
165
|
+
const result = [...source];
|
|
166
|
+
const sourceIdentities = new Set(
|
|
167
|
+
source.map(getArrayEntryIdentity).filter((value): value is string => Boolean(value)),
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
for (const entry of destination) {
|
|
171
|
+
const identity = getArrayEntryIdentity(entry);
|
|
172
|
+
|
|
173
|
+
if (!identity && !result.some((item) => deepEqual(item, entry))) {
|
|
174
|
+
result.push(entry);
|
|
175
|
+
} else if (
|
|
176
|
+
identity &&
|
|
177
|
+
!sourceIdentities.has(identity) &&
|
|
178
|
+
!result.some((item) => getArrayEntryIdentity(item) === identity)
|
|
179
|
+
) {
|
|
180
|
+
result.push(entry);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (isPlainObject(destination) && isPlainObject(source)) {
|
|
188
|
+
const result: Record<string, unknown> = { ...destination };
|
|
189
|
+
|
|
190
|
+
for (const key of Object.keys(source)) {
|
|
191
|
+
result[key] = deepMerge(result[key], source[key]);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return source;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function deepMergeWithPolicy(
|
|
201
|
+
destination: unknown,
|
|
202
|
+
source: unknown,
|
|
203
|
+
policy: ConflictPolicy,
|
|
204
|
+
conflicts: Array<Omit<PromotionConflict, "type" | "key">>,
|
|
205
|
+
pathSegments: string[] = [],
|
|
206
|
+
): unknown {
|
|
207
|
+
if (typeof destination === "undefined") return source;
|
|
208
|
+
if (typeof source === "undefined") return destination;
|
|
209
|
+
|
|
210
|
+
const conflictPath = pathSegments.join(".") || "<root>";
|
|
211
|
+
|
|
212
|
+
if (Array.isArray(destination) && Array.isArray(source)) {
|
|
213
|
+
if (!deepEqual(destination, source)) {
|
|
214
|
+
conflicts.push({ path: conflictPath, source, destination });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (policy === "destination") {
|
|
218
|
+
const result = [...destination];
|
|
219
|
+
const destinationIdentities = new Set(
|
|
220
|
+
destination.map(getArrayEntryIdentity).filter((value): value is string => Boolean(value)),
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
for (const entry of source) {
|
|
224
|
+
const identity = getArrayEntryIdentity(entry);
|
|
225
|
+
|
|
226
|
+
if (!identity && !result.some((item) => deepEqual(item, entry))) {
|
|
227
|
+
result.push(entry);
|
|
228
|
+
} else if (
|
|
229
|
+
identity &&
|
|
230
|
+
!destinationIdentities.has(identity) &&
|
|
231
|
+
!result.some((item) => getArrayEntryIdentity(item) === identity)
|
|
232
|
+
) {
|
|
233
|
+
result.push(entry);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return result;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return deepMerge(destination, source);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (isPlainObject(destination) && isPlainObject(source)) {
|
|
244
|
+
const result: Record<string, unknown> = { ...destination };
|
|
245
|
+
|
|
246
|
+
for (const key of Object.keys(source)) {
|
|
247
|
+
result[key] = deepMergeWithPolicy(result[key], source[key], policy, conflicts, [
|
|
248
|
+
...pathSegments,
|
|
249
|
+
key,
|
|
250
|
+
]);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (!deepEqual(destination, source)) {
|
|
257
|
+
conflicts.push({ path: conflictPath, source, destination });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return policy === "destination" ? destination : source;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function matchesPattern(key: string, patterns: string[]) {
|
|
264
|
+
if (patterns.length === 0) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return patterns.some((pattern) => {
|
|
269
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
270
|
+
return new RegExp(`^${escaped}$`).test(key);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function withoutKey<T extends Record<string, unknown>>(entity: T): T {
|
|
275
|
+
const { key: _key, ...rest } = entity;
|
|
276
|
+
|
|
277
|
+
return rest as T;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function filterLocaleMap<T>(values: Record<string, T> | undefined, locales: Set<string>) {
|
|
281
|
+
if (!values || locales.size === 0) {
|
|
282
|
+
return values;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return Object.fromEntries(Object.entries(values).filter(([locale]) => locales.has(locale)));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function collectGroupSegmentKeys(value: any, result: Set<string>) {
|
|
289
|
+
if (!value || value === "*") return;
|
|
290
|
+
if (typeof value === "string") {
|
|
291
|
+
result.add(value);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (Array.isArray(value)) {
|
|
295
|
+
value.forEach((entry) => collectGroupSegmentKeys(entry, result));
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (value.and) value.and.forEach((entry: any) => collectGroupSegmentKeys(entry, result));
|
|
299
|
+
if (value.or) value.or.forEach((entry: any) => collectGroupSegmentKeys(entry, result));
|
|
300
|
+
if (value.not) value.not.forEach((entry: any) => collectGroupSegmentKeys(entry, result));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function collectConditionDependencies(
|
|
304
|
+
value: Condition | Condition[] | "*" | undefined,
|
|
305
|
+
segments: Set<string>,
|
|
306
|
+
attributes: Set<string>,
|
|
307
|
+
) {
|
|
308
|
+
if (!value || value === "*") return;
|
|
309
|
+
if (typeof value === "string") {
|
|
310
|
+
segments.add(value);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (Array.isArray(value)) {
|
|
314
|
+
value.forEach((entry) => collectConditionDependencies(entry, segments, attributes));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if ("attribute" in value) {
|
|
318
|
+
attributes.add(value.attribute.split(".")[0]);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if ("and" in value)
|
|
322
|
+
value.and.forEach((entry) => collectConditionDependencies(entry, segments, attributes));
|
|
323
|
+
if ("or" in value)
|
|
324
|
+
value.or.forEach((entry) => collectConditionDependencies(entry, segments, attributes));
|
|
325
|
+
if ("not" in value)
|
|
326
|
+
value.not.forEach((entry) => collectConditionDependencies(entry, segments, attributes));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function mergeMessage(
|
|
330
|
+
messageKey: string,
|
|
331
|
+
destination: Message | undefined,
|
|
332
|
+
source: Message,
|
|
333
|
+
policy: ConflictPolicy,
|
|
334
|
+
conflicts: PromotionConflict[],
|
|
335
|
+
): Message {
|
|
336
|
+
if (!destination) {
|
|
337
|
+
return source;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
validateMessageOverrideKeys(messageKey, destination);
|
|
341
|
+
|
|
342
|
+
const sourceOverrides = source.overrides || [];
|
|
343
|
+
const destinationOverrides = destination.overrides || [];
|
|
344
|
+
const mergedOverrideKeys = new Set<string>();
|
|
345
|
+
const overrides = sourceOverrides.map((sourceOverride) => {
|
|
346
|
+
const destinationOverride = destinationOverrides.find(
|
|
347
|
+
(override) => override.key === sourceOverride.key,
|
|
348
|
+
);
|
|
349
|
+
mergedOverrideKeys.add(sourceOverride.key);
|
|
350
|
+
|
|
351
|
+
const overrideConflicts: Array<Omit<PromotionConflict, "type" | "key">> = [];
|
|
352
|
+
const merged = deepMergeWithPolicy(
|
|
353
|
+
destinationOverride,
|
|
354
|
+
sourceOverride,
|
|
355
|
+
policy,
|
|
356
|
+
overrideConflicts,
|
|
357
|
+
["overrides", sourceOverride.key],
|
|
358
|
+
) as Message["overrides"][number];
|
|
359
|
+
|
|
360
|
+
conflicts.push(
|
|
361
|
+
...overrideConflicts.map((conflict) => ({
|
|
362
|
+
type: "message" as const,
|
|
363
|
+
key: messageKey,
|
|
364
|
+
...conflict,
|
|
365
|
+
})),
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
return merged;
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
for (const destinationOverride of destinationOverrides) {
|
|
372
|
+
if (!mergedOverrideKeys.has(destinationOverride.key)) {
|
|
373
|
+
overrides.push(destinationOverride);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const messageConflicts: Array<Omit<PromotionConflict, "type" | "key">> = [];
|
|
378
|
+
const mergedMessage = deepMergeWithPolicy(
|
|
379
|
+
destination,
|
|
380
|
+
{ ...source, overrides: undefined },
|
|
381
|
+
policy,
|
|
382
|
+
messageConflicts,
|
|
383
|
+
) as Message;
|
|
384
|
+
conflicts.push(
|
|
385
|
+
...messageConflicts.map((conflict) => ({
|
|
386
|
+
type: "message" as const,
|
|
387
|
+
key: messageKey,
|
|
388
|
+
...conflict,
|
|
389
|
+
})),
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
...mergedMessage,
|
|
394
|
+
overrides: overrides.length > 0 ? overrides : undefined,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function removeMessageOverrides(message: Message): Message {
|
|
399
|
+
const { overrides: _overrides, ...messageWithoutOverrides } = message;
|
|
400
|
+
|
|
401
|
+
return messageWithoutOverrides;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function filterMessageForLocales(message: Message, locales: Set<string>): Message {
|
|
405
|
+
if (locales.size === 0) {
|
|
406
|
+
return message;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const overrides = (message.overrides || [])
|
|
410
|
+
.map((override) => ({
|
|
411
|
+
...override,
|
|
412
|
+
translations: filterLocaleMap(override.translations, locales) || {},
|
|
413
|
+
}))
|
|
414
|
+
.filter((override) => Object.keys(override.translations).length > 0);
|
|
415
|
+
|
|
416
|
+
return {
|
|
417
|
+
...message,
|
|
418
|
+
translations: filterLocaleMap(message.translations, locales) || {},
|
|
419
|
+
overrides: overrides.length > 0 ? overrides : undefined,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function filterTargetForLocales(target: Target, locales: Set<string>) {
|
|
424
|
+
if (locales.size === 0) {
|
|
425
|
+
return target;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
...target,
|
|
430
|
+
locales: target.locales?.filter((locale) => locales.has(locale)),
|
|
431
|
+
formats: filterLocaleMap(target.formats, locales),
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function filterTestForLocales(test: any, locales: Set<string>) {
|
|
436
|
+
if (locales.size === 0 || !Array.isArray(test.assertions)) {
|
|
437
|
+
return test;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return {
|
|
441
|
+
...test,
|
|
442
|
+
assertions: test.assertions.filter(
|
|
443
|
+
(assertion: any) => !assertion.locale || locales.has(assertion.locale),
|
|
444
|
+
),
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function validateMessageOverrideKeys(messageKey: string, message: Message) {
|
|
449
|
+
const keys = new Set<string>();
|
|
450
|
+
|
|
451
|
+
for (let index = 0; index < (message.overrides || []).length; index++) {
|
|
452
|
+
const override = (message.overrides || [])[index];
|
|
453
|
+
|
|
454
|
+
if (!override.key) {
|
|
455
|
+
throw new Error(
|
|
456
|
+
`Message "${messageKey}" override at index ${index} must define a key before promotion.`,
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (keys.has(override.key)) {
|
|
461
|
+
throw new Error(`Message "${messageKey}" has duplicate override key "${override.key}".`);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
keys.add(override.key);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async function safeRead<T>(
|
|
469
|
+
keys: string[],
|
|
470
|
+
read: (key: string) => Promise<T>,
|
|
471
|
+
): Promise<Record<string, T>> {
|
|
472
|
+
const entries = await Promise.all(keys.map(async (key) => [key, await read(key)] as const));
|
|
473
|
+
return Object.fromEntries(entries);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async function readDestination<T>(
|
|
477
|
+
key: string,
|
|
478
|
+
read: (key: string) => Promise<T>,
|
|
479
|
+
): Promise<T | undefined> {
|
|
480
|
+
try {
|
|
481
|
+
return await read(key);
|
|
482
|
+
} catch {
|
|
483
|
+
return undefined;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function formatLintPreflightErrors(set: string, errors: LintError[]) {
|
|
488
|
+
const preview = errors
|
|
489
|
+
.slice(0, 5)
|
|
490
|
+
.map((error) => `${error.filePath}: ${error.message}`)
|
|
491
|
+
.join("\n");
|
|
492
|
+
const suffix = errors.length > 5 ? `\n...and ${errors.length - 5} more` : "";
|
|
493
|
+
|
|
494
|
+
return `Set "${set}" failed preflight lint with ${errors.length} error(s).\n${preview}${suffix}`;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async function assertSetLintsClean(set: string, datasource: Datasource) {
|
|
498
|
+
const result = await lintProject(datasource.getConfig(), datasource);
|
|
499
|
+
|
|
500
|
+
if (result.hasError) {
|
|
501
|
+
throw new Error(formatLintPreflightErrors(set, result.errors));
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function getEntityFilePath(projectConfig: ProjectConfig, type: EntityType, key: string) {
|
|
506
|
+
const directories: Record<EntityType, string> = {
|
|
507
|
+
locale: projectConfig.localesDirectoryPath,
|
|
508
|
+
attribute: projectConfig.attributesDirectoryPath,
|
|
509
|
+
segment: projectConfig.segmentsDirectoryPath,
|
|
510
|
+
target: projectConfig.targetsDirectoryPath,
|
|
511
|
+
message: projectConfig.messagesDirectoryPath,
|
|
512
|
+
test: projectConfig.testsDirectoryPath,
|
|
513
|
+
};
|
|
514
|
+
const extension = (projectConfig.parser as any).extension || "yml";
|
|
515
|
+
|
|
516
|
+
return (
|
|
517
|
+
path.join(directories[type], ...key.split(projectConfig.namespaceCharacter)) + `.${extension}`
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async function getPromotionPlan(
|
|
522
|
+
sourceDatasource: Datasource,
|
|
523
|
+
destinationDatasource: Datasource,
|
|
524
|
+
options: Required<
|
|
525
|
+
Pick<
|
|
526
|
+
PromoteProjectSetsOptions,
|
|
527
|
+
| "target"
|
|
528
|
+
| "locale"
|
|
529
|
+
| "includeMessages"
|
|
530
|
+
| "excludeMessages"
|
|
531
|
+
| "excludeOverrides"
|
|
532
|
+
| "allowEmpty"
|
|
533
|
+
| "conflicts"
|
|
534
|
+
>
|
|
535
|
+
>,
|
|
536
|
+
) {
|
|
537
|
+
const selectedTargets = new Set(toArray(options.target));
|
|
538
|
+
const requestedLocales = new Set(toArray(options.locale));
|
|
539
|
+
const includeMessages = toArray(options.includeMessages);
|
|
540
|
+
const excludeMessages = toArray(options.excludeMessages);
|
|
541
|
+
const hasNoFilters =
|
|
542
|
+
selectedTargets.size === 0 && includeMessages.length === 0 && requestedLocales.size === 0;
|
|
543
|
+
|
|
544
|
+
const [localeKeys, targetKeys, messageKeys, segmentKeys, attributeKeys, testKeys] =
|
|
545
|
+
await Promise.all([
|
|
546
|
+
sourceDatasource.listLocales(),
|
|
547
|
+
sourceDatasource.listTargets(),
|
|
548
|
+
sourceDatasource.listMessages(),
|
|
549
|
+
sourceDatasource.listSegments(),
|
|
550
|
+
sourceDatasource.listAttributes(),
|
|
551
|
+
sourceDatasource.listTests(),
|
|
552
|
+
]);
|
|
553
|
+
|
|
554
|
+
for (const locale of Array.from(requestedLocales)) {
|
|
555
|
+
if (!localeKeys.includes(locale)) {
|
|
556
|
+
throw new Error(
|
|
557
|
+
`Unknown source locale "${locale}". Available locales: ${localeKeys.join(", ") || "none"}.`,
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
const [locales, targets, messages, segments, attributes, tests] = await Promise.all([
|
|
562
|
+
safeRead<Locale>(localeKeys, (key) => sourceDatasource.readLocale(key)),
|
|
563
|
+
safeRead<Target>(targetKeys, (key) => sourceDatasource.readTarget(key)),
|
|
564
|
+
safeRead<Message>(messageKeys, (key) => sourceDatasource.readMessage(key)),
|
|
565
|
+
safeRead<Segment>(segmentKeys, (key) => sourceDatasource.readSegment(key)),
|
|
566
|
+
safeRead<Attribute>(attributeKeys, (key) => sourceDatasource.readAttribute(key)),
|
|
567
|
+
safeRead<Test>(testKeys, (key) => sourceDatasource.readTest(key)),
|
|
568
|
+
]);
|
|
569
|
+
|
|
570
|
+
function addLocaleWithAncestors(locale: string, result: Set<string>) {
|
|
571
|
+
if (!locales[locale] || result.has(locale)) return;
|
|
572
|
+
|
|
573
|
+
result.add(locale);
|
|
574
|
+
|
|
575
|
+
if (locales[locale].inheritFormatsFrom)
|
|
576
|
+
addLocaleWithAncestors(locales[locale].inheritFormatsFrom, result);
|
|
577
|
+
if (locales[locale].inheritTranslationsFrom)
|
|
578
|
+
addLocaleWithAncestors(locales[locale].inheritTranslationsFrom, result);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const promotedTargetKeys = new Set<string>();
|
|
582
|
+
const promotedMessageKeys = new Set<string>();
|
|
583
|
+
const explicitRuntimeLocales = new Set<string>();
|
|
584
|
+
|
|
585
|
+
if (hasNoFilters) {
|
|
586
|
+
targetKeys.forEach((key) => promotedTargetKeys.add(key));
|
|
587
|
+
messageKeys.forEach((key) => promotedMessageKeys.add(key));
|
|
588
|
+
localeKeys.forEach((key) => explicitRuntimeLocales.add(key));
|
|
589
|
+
} else {
|
|
590
|
+
selectedTargets.forEach((key) => {
|
|
591
|
+
if (!targets[key]) throw new Error(`Unknown source target "${key}".`);
|
|
592
|
+
promotedTargetKeys.add(key);
|
|
593
|
+
(targets[key].locales || localeKeys).forEach((locale) => explicitRuntimeLocales.add(locale));
|
|
594
|
+
|
|
595
|
+
for (const messageKey of messageKeys) {
|
|
596
|
+
const included = matchesPattern(
|
|
597
|
+
messageKey,
|
|
598
|
+
targets[key].includeMessages?.length ? targets[key].includeMessages : ["*"],
|
|
599
|
+
);
|
|
600
|
+
const excluded = matchesPattern(messageKey, targets[key].excludeMessages || []);
|
|
601
|
+
|
|
602
|
+
if (included && !excluded) {
|
|
603
|
+
promotedMessageKeys.add(messageKey);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
if (includeMessages.length > 0) {
|
|
609
|
+
let matchedMessageCount = 0;
|
|
610
|
+
|
|
611
|
+
for (const messageKey of messageKeys) {
|
|
612
|
+
if (
|
|
613
|
+
matchesPattern(messageKey, includeMessages) &&
|
|
614
|
+
!matchesPattern(messageKey, excludeMessages)
|
|
615
|
+
) {
|
|
616
|
+
promotedMessageKeys.add(messageKey);
|
|
617
|
+
matchedMessageCount++;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (matchedMessageCount === 0 && !options.allowEmpty) {
|
|
622
|
+
throw new Error(
|
|
623
|
+
`No source messages matched --includeMessages=${includeMessages.join(", ")}.`,
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (requestedLocales.size > 0) {
|
|
629
|
+
requestedLocales.forEach((locale) => explicitRuntimeLocales.add(locale));
|
|
630
|
+
if (selectedTargets.size === 0 && includeMessages.length === 0) {
|
|
631
|
+
targetKeys.forEach((key) => promotedTargetKeys.add(key));
|
|
632
|
+
messageKeys.forEach((key) => promotedMessageKeys.add(key));
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
for (const messageKey of Array.from(promotedMessageKeys)) {
|
|
638
|
+
if (matchesPattern(messageKey, excludeMessages)) {
|
|
639
|
+
promotedMessageKeys.delete(messageKey);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (selectedTargets.size === 0 && requestedLocales.size === 0 && includeMessages.length > 0) {
|
|
644
|
+
for (const messageKey of Array.from(promotedMessageKeys)) {
|
|
645
|
+
const message = messages[messageKey];
|
|
646
|
+
|
|
647
|
+
if (!message) continue;
|
|
648
|
+
|
|
649
|
+
Object.keys(message.translations || {}).forEach((locale) =>
|
|
650
|
+
explicitRuntimeLocales.add(locale),
|
|
651
|
+
);
|
|
652
|
+
if (!options.excludeOverrides) {
|
|
653
|
+
for (const override of message.overrides || []) {
|
|
654
|
+
Object.keys(override.translations || {}).forEach((locale) =>
|
|
655
|
+
explicitRuntimeLocales.add(locale),
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const promotedLocaleKeys = new Set<string>();
|
|
663
|
+
const localeFilterKeys = new Set<string>();
|
|
664
|
+
const localeRestricted = requestedLocales.size > 0 || selectedTargets.size > 0;
|
|
665
|
+
const localeSeeds = requestedLocales.size > 0 ? requestedLocales : explicitRuntimeLocales;
|
|
666
|
+
localeSeeds.forEach((locale) => addLocaleWithAncestors(locale, promotedLocaleKeys));
|
|
667
|
+
(requestedLocales.size > 0 ? requestedLocales : new Set<string>()).forEach((locale) =>
|
|
668
|
+
addLocaleWithAncestors(locale, localeFilterKeys),
|
|
669
|
+
);
|
|
670
|
+
|
|
671
|
+
if (hasNoFilters && !localeRestricted && promotedLocaleKeys.size === 0) {
|
|
672
|
+
localeKeys.forEach((key) => promotedLocaleKeys.add(key));
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const promotedSegmentKeys = new Set<string>();
|
|
676
|
+
const promotedAttributeKeys = new Set<string>();
|
|
677
|
+
|
|
678
|
+
for (const messageKey of Array.from(promotedMessageKeys)) {
|
|
679
|
+
const message = messages[messageKey];
|
|
680
|
+
if (!message) continue;
|
|
681
|
+
|
|
682
|
+
if (!options.excludeOverrides) {
|
|
683
|
+
validateMessageOverrideKeys(messageKey, message);
|
|
684
|
+
|
|
685
|
+
for (const override of message.overrides || []) {
|
|
686
|
+
collectConditionDependencies(
|
|
687
|
+
override.conditions,
|
|
688
|
+
promotedSegmentKeys,
|
|
689
|
+
promotedAttributeKeys,
|
|
690
|
+
);
|
|
691
|
+
collectGroupSegmentKeys(override.segments, promotedSegmentKeys);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const pendingSegments = Array.from(promotedSegmentKeys);
|
|
697
|
+
for (let index = 0; index < pendingSegments.length; index++) {
|
|
698
|
+
const segmentKey = pendingSegments[index];
|
|
699
|
+
const segment = segments[segmentKey];
|
|
700
|
+
|
|
701
|
+
if (!segment) continue;
|
|
702
|
+
|
|
703
|
+
const beforeSize = promotedSegmentKeys.size;
|
|
704
|
+
collectConditionDependencies(segment.conditions, promotedSegmentKeys, promotedAttributeKeys);
|
|
705
|
+
|
|
706
|
+
if (promotedSegmentKeys.size > beforeSize) {
|
|
707
|
+
pendingSegments.push(
|
|
708
|
+
...Array.from(promotedSegmentKeys).filter((key) => !pendingSegments.includes(key)),
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (hasNoFilters) {
|
|
714
|
+
segmentKeys.forEach((key) => promotedSegmentKeys.add(key));
|
|
715
|
+
attributeKeys.forEach((key) => promotedAttributeKeys.add(key));
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const promotedTestKeys = testKeys.filter((key) => {
|
|
719
|
+
const test = tests[key] as any;
|
|
720
|
+
return (
|
|
721
|
+
promotedMessageKeys.has(test.message) ||
|
|
722
|
+
promotedSegmentKeys.has(test.segment) ||
|
|
723
|
+
promotedTargetKeys.has(test.target) ||
|
|
724
|
+
promotedLocaleKeys.has(test.locale)
|
|
725
|
+
);
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
const plans: EntityPlan[] = [];
|
|
729
|
+
|
|
730
|
+
async function plan<T extends EntityValue>(
|
|
731
|
+
type: EntityType,
|
|
732
|
+
key: string,
|
|
733
|
+
source: T,
|
|
734
|
+
readDestinationEntity: (key: string) => Promise<T>,
|
|
735
|
+
merge: (destination: T | undefined, source: T, conflicts: PromotionConflict[]) => T = (
|
|
736
|
+
destination,
|
|
737
|
+
sourceValue,
|
|
738
|
+
conflicts,
|
|
739
|
+
) => {
|
|
740
|
+
const entityConflicts: Array<Omit<PromotionConflict, "type" | "key">> = [];
|
|
741
|
+
const merged = deepMergeWithPolicy(
|
|
742
|
+
destination,
|
|
743
|
+
sourceValue,
|
|
744
|
+
options.conflicts,
|
|
745
|
+
entityConflicts,
|
|
746
|
+
) as T;
|
|
747
|
+
conflicts.push(...entityConflicts.map((conflict) => ({ type, key, ...conflict })));
|
|
748
|
+
|
|
749
|
+
return merged;
|
|
750
|
+
},
|
|
751
|
+
) {
|
|
752
|
+
const cleanedSource = withoutKey(source as any) as T;
|
|
753
|
+
const destination = await readDestination<T>(key, readDestinationEntity);
|
|
754
|
+
const cleanedDestination = destination ? (withoutKey(destination as any) as T) : undefined;
|
|
755
|
+
|
|
756
|
+
if (cleanedDestination && (!isPromotable(cleanedSource) || !isPromotable(cleanedDestination))) {
|
|
757
|
+
plans.push({
|
|
758
|
+
type,
|
|
759
|
+
key,
|
|
760
|
+
source: cleanedSource,
|
|
761
|
+
destination: cleanedDestination,
|
|
762
|
+
merged: cleanedDestination,
|
|
763
|
+
conflicts: [],
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const conflicts: PromotionConflict[] = [];
|
|
770
|
+
const merged = merge(cleanedDestination, cleanedSource, conflicts);
|
|
771
|
+
|
|
772
|
+
plans.push({
|
|
773
|
+
type,
|
|
774
|
+
key,
|
|
775
|
+
source: cleanedSource,
|
|
776
|
+
destination: cleanedDestination,
|
|
777
|
+
merged,
|
|
778
|
+
conflicts,
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
for (const key of Array.from(promotedLocaleKeys).sort()) {
|
|
783
|
+
if (locales[key])
|
|
784
|
+
await plan("locale", key, locales[key], (entryKey) =>
|
|
785
|
+
destinationDatasource.readLocale(entryKey),
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
for (const key of Array.from(promotedAttributeKeys).sort()) {
|
|
790
|
+
if (attributes[key])
|
|
791
|
+
await plan("attribute", key, attributes[key], (entryKey) =>
|
|
792
|
+
destinationDatasource.readAttribute(entryKey),
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
for (const key of Array.from(promotedSegmentKeys).sort()) {
|
|
797
|
+
if (segments[key])
|
|
798
|
+
await plan("segment", key, segments[key], (entryKey) =>
|
|
799
|
+
destinationDatasource.readSegment(entryKey),
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const targetLocaleFilter =
|
|
804
|
+
requestedLocales.size > 0 ? new Set(Array.from(requestedLocales)) : new Set<string>();
|
|
805
|
+
for (const key of Array.from(promotedTargetKeys).sort()) {
|
|
806
|
+
if (targets[key]) {
|
|
807
|
+
await plan(
|
|
808
|
+
"target",
|
|
809
|
+
key,
|
|
810
|
+
filterTargetForLocales(targets[key], targetLocaleFilter),
|
|
811
|
+
(entryKey) => destinationDatasource.readTarget(entryKey),
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
const messageLocaleFilter = localeFilterKeys.size > 0 ? localeFilterKeys : new Set<string>();
|
|
817
|
+
for (const key of Array.from(promotedMessageKeys).sort()) {
|
|
818
|
+
if (messages[key]) {
|
|
819
|
+
const sourceMessage = options.excludeOverrides
|
|
820
|
+
? removeMessageOverrides(filterMessageForLocales(messages[key], messageLocaleFilter))
|
|
821
|
+
: filterMessageForLocales(messages[key], messageLocaleFilter);
|
|
822
|
+
await plan(
|
|
823
|
+
"message",
|
|
824
|
+
key,
|
|
825
|
+
sourceMessage,
|
|
826
|
+
(entryKey) => destinationDatasource.readMessage(entryKey),
|
|
827
|
+
(destination, source, conflicts) =>
|
|
828
|
+
mergeMessage(key, destination, source, options.conflicts, conflicts),
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
for (const key of promotedTestKeys.sort()) {
|
|
834
|
+
const test = filterTestForLocales(tests[key] as any, requestedLocales);
|
|
835
|
+
if (!Array.isArray((test as any).assertions) || (test as any).assertions.length > 0) {
|
|
836
|
+
await plan("test", key, test, (entryKey) => destinationDatasource.readTest(entryKey));
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
return plans;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
async function writePlan(destinationDatasource: Datasource, plans: EntityPlan[]) {
|
|
844
|
+
for (const plan of plans) {
|
|
845
|
+
if (deepEqual(plan.destination, plan.merged)) {
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if (plan.type === "locale")
|
|
850
|
+
await destinationDatasource.writeLocale(plan.key, plan.merged as Locale);
|
|
851
|
+
if (plan.type === "attribute")
|
|
852
|
+
await destinationDatasource.writeAttribute(plan.key, plan.merged as Attribute);
|
|
853
|
+
if (plan.type === "segment")
|
|
854
|
+
await destinationDatasource.writeSegment(plan.key, plan.merged as Segment);
|
|
855
|
+
if (plan.type === "target")
|
|
856
|
+
await destinationDatasource.writeTarget(plan.key, plan.merged as Target);
|
|
857
|
+
if (plan.type === "message")
|
|
858
|
+
await destinationDatasource.writeMessage(plan.key, plan.merged as Message);
|
|
859
|
+
if (plan.type === "test") await destinationDatasource.writeTest(plan.key, plan.merged as Test);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function normalizeConflictPolicy(value: unknown): ConflictPolicy {
|
|
864
|
+
if (typeof value === "undefined" || value === false) {
|
|
865
|
+
return "source";
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if (value === "source" || value === "destination" || value === "fail") {
|
|
869
|
+
return value;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
throw new MessagevisorCLIError(
|
|
873
|
+
`Invalid --conflicts value "${String(value)}". Use source, destination, or fail.`,
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function normalizeAuditFormat(value: unknown): PromotionAuditFormat | false {
|
|
878
|
+
if (typeof value === "undefined" || value === false || value === "false") {
|
|
879
|
+
return false;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
if (value === true || value === "true") {
|
|
883
|
+
return "json";
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (value === "json" || value === "markdown") {
|
|
887
|
+
return value;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
throw new MessagevisorCLIError(`Invalid --audit value "${String(value)}". Use json or markdown.`);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function getTimestamp() {
|
|
894
|
+
const date = new Date();
|
|
895
|
+
const pad = (value: number) => (value < 10 ? `0${value}` : String(value));
|
|
896
|
+
|
|
897
|
+
return [
|
|
898
|
+
date.getUTCFullYear(),
|
|
899
|
+
pad(date.getUTCMonth() + 1),
|
|
900
|
+
pad(date.getUTCDate()),
|
|
901
|
+
"T",
|
|
902
|
+
pad(date.getUTCHours()),
|
|
903
|
+
pad(date.getUTCMinutes()),
|
|
904
|
+
pad(date.getUTCSeconds()),
|
|
905
|
+
].join("");
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
async function getPromotionAuditFilePath(
|
|
909
|
+
projectConfig: ProjectConfig,
|
|
910
|
+
result: PromoteProjectSetsResult,
|
|
911
|
+
format: PromotionAuditFormat,
|
|
912
|
+
) {
|
|
913
|
+
const extension = format === "markdown" ? "md" : "json";
|
|
914
|
+
const baseFileName = `${getTimestamp()}-${result.from}-to-${result.to}`;
|
|
915
|
+
const directoryPath = path.join(projectConfig.stateDirectoryPath, "promotions");
|
|
916
|
+
let suffix = 0;
|
|
917
|
+
|
|
918
|
+
while (true) {
|
|
919
|
+
const fileName = `${baseFileName}${suffix === 0 ? "" : `-${suffix}`}.${extension}`;
|
|
920
|
+
const filePath = path.join(directoryPath, fileName);
|
|
921
|
+
|
|
922
|
+
if (!fs.existsSync(filePath)) {
|
|
923
|
+
return filePath;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
suffix++;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function getAuditPayload(result: PromoteProjectSetsResult) {
|
|
931
|
+
return {
|
|
932
|
+
from: result.from,
|
|
933
|
+
to: result.to,
|
|
934
|
+
apply: result.apply,
|
|
935
|
+
filters: result.filters,
|
|
936
|
+
dependencies: result.dependencies,
|
|
937
|
+
files: result.files,
|
|
938
|
+
conflicts: result.conflicts.map((conflict) => ({
|
|
939
|
+
type: conflict.type,
|
|
940
|
+
key: conflict.key,
|
|
941
|
+
path: conflict.path,
|
|
942
|
+
source: conflict.source,
|
|
943
|
+
destination: conflict.destination,
|
|
944
|
+
})),
|
|
945
|
+
duration: result.duration,
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function stringifyMarkdownAudit(result: PromoteProjectSetsResult) {
|
|
950
|
+
const lines = [
|
|
951
|
+
`# Messagevisor Promotion`,
|
|
952
|
+
"",
|
|
953
|
+
`- From: ${result.from}`,
|
|
954
|
+
`- To: ${result.to}`,
|
|
955
|
+
`- Mode: ${result.apply ? "apply" : "preview"}`,
|
|
956
|
+
`- Conflicts: ${result.filters.conflicts}`,
|
|
957
|
+
`- Exclude overrides: ${result.filters.excludeOverrides ? "true" : "false"}`,
|
|
958
|
+
`- Duration: ${prettyDuration(result.duration)}`,
|
|
959
|
+
"",
|
|
960
|
+
`## Dependencies`,
|
|
961
|
+
"",
|
|
962
|
+
`- Locales: ${result.dependencies.locales}`,
|
|
963
|
+
`- Attributes: ${result.dependencies.attributes}`,
|
|
964
|
+
`- Segments: ${result.dependencies.segments}`,
|
|
965
|
+
`- Targets: ${result.dependencies.targets}`,
|
|
966
|
+
`- Messages: ${result.dependencies.messages}`,
|
|
967
|
+
`- Tests: ${result.dependencies.tests}`,
|
|
968
|
+
"",
|
|
969
|
+
`## Files`,
|
|
970
|
+
"",
|
|
971
|
+
];
|
|
972
|
+
|
|
973
|
+
for (const [label, files] of [
|
|
974
|
+
["Created", result.files.created],
|
|
975
|
+
["Updated", result.files.updated],
|
|
976
|
+
["Unchanged", result.files.unchanged],
|
|
977
|
+
] as const) {
|
|
978
|
+
lines.push(`### ${label}`, "");
|
|
979
|
+
lines.push(...(files.length > 0 ? files.map((filePath) => `- ${filePath}`) : ["- None"]));
|
|
980
|
+
lines.push("");
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
if (result.conflicts.length > 0) {
|
|
984
|
+
lines.push(`## Conflicts`, "");
|
|
985
|
+
lines.push(
|
|
986
|
+
...result.conflicts.map(
|
|
987
|
+
(conflict) => `- ${conflict.type} ${conflict.key} at ${conflict.path}`,
|
|
988
|
+
),
|
|
989
|
+
);
|
|
990
|
+
lines.push("");
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
return `${lines.join("\n")}\n`;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
async function writePromotionAudit(
|
|
997
|
+
projectConfig: ProjectConfig,
|
|
998
|
+
result: PromoteProjectSetsResult,
|
|
999
|
+
format: PromotionAuditFormat,
|
|
1000
|
+
) {
|
|
1001
|
+
const filePath = await getPromotionAuditFilePath(projectConfig, result, format);
|
|
1002
|
+
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
1003
|
+
|
|
1004
|
+
const content =
|
|
1005
|
+
format === "markdown"
|
|
1006
|
+
? stringifyMarkdownAudit(result)
|
|
1007
|
+
: `${JSON.stringify(getAuditPayload(result), null, 2)}\n`;
|
|
1008
|
+
|
|
1009
|
+
await fs.promises.writeFile(filePath, content);
|
|
1010
|
+
|
|
1011
|
+
return path.relative(process.cwd(), filePath);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
export async function promoteProjectSets(
|
|
1015
|
+
projectConfig: ProjectConfig,
|
|
1016
|
+
datasource: Datasource,
|
|
1017
|
+
options: PromoteProjectSetsOptions,
|
|
1018
|
+
): Promise<PromoteProjectSetsResult> {
|
|
1019
|
+
const startTime = Date.now();
|
|
1020
|
+
const conflictPolicy = normalizeConflictPolicy(options.conflicts);
|
|
1021
|
+
const auditFormat = normalizeAuditFormat(options.audit);
|
|
1022
|
+
|
|
1023
|
+
if (!projectConfig.sets)
|
|
1024
|
+
throw new MessagevisorCLIError("Promotion is only available when `sets: true` is configured.");
|
|
1025
|
+
if (!options.from) throw new MessagevisorCLIError("Pass --from=<set>.");
|
|
1026
|
+
if (!options.to) throw new MessagevisorCLIError("Pass --to=<set>.");
|
|
1027
|
+
if (options.from === options.to)
|
|
1028
|
+
throw new MessagevisorCLIError("--from and --to must be different sets.");
|
|
1029
|
+
|
|
1030
|
+
const sets = await datasource.listSets();
|
|
1031
|
+
if (!sets.includes(options.from))
|
|
1032
|
+
throw new MessagevisorCLIError(
|
|
1033
|
+
`Unknown source set "${options.from}". Available sets: ${sets.join(", ") || "none"}.`,
|
|
1034
|
+
);
|
|
1035
|
+
if (!sets.includes(options.to))
|
|
1036
|
+
throw new MessagevisorCLIError(
|
|
1037
|
+
`Unknown destination set "${options.to}". Available sets: ${sets.join(", ") || "none"}.`,
|
|
1038
|
+
);
|
|
1039
|
+
|
|
1040
|
+
assertAllowedPromotionFlow(projectConfig, options.from, options.to);
|
|
1041
|
+
|
|
1042
|
+
const sourceDatasource = datasource.forSet(options.from);
|
|
1043
|
+
const destinationDatasource = datasource.forSet(options.to);
|
|
1044
|
+
|
|
1045
|
+
await assertSetLintsClean(options.from, sourceDatasource);
|
|
1046
|
+
await assertSetLintsClean(options.to, destinationDatasource);
|
|
1047
|
+
|
|
1048
|
+
const plans = await getPromotionPlan(sourceDatasource, destinationDatasource, {
|
|
1049
|
+
target: options.target || [],
|
|
1050
|
+
locale: options.locale || [],
|
|
1051
|
+
includeMessages: options.includeMessages || [],
|
|
1052
|
+
excludeMessages: options.excludeMessages || [],
|
|
1053
|
+
excludeOverrides: options.excludeOverrides === true,
|
|
1054
|
+
allowEmpty: options.allowEmpty === true,
|
|
1055
|
+
conflicts: conflictPolicy,
|
|
1056
|
+
});
|
|
1057
|
+
const conflicts = plans.flatMap((plan) => plan.conflicts);
|
|
1058
|
+
|
|
1059
|
+
if (conflictPolicy === "fail" && conflicts.length > 0) {
|
|
1060
|
+
const preview = conflicts
|
|
1061
|
+
.slice(0, 5)
|
|
1062
|
+
.map((conflict) => `${conflict.type} "${conflict.key}" at ${conflict.path}`)
|
|
1063
|
+
.join("\n");
|
|
1064
|
+
const suffix = conflicts.length > 5 ? `\n...and ${conflicts.length - 5} more` : "";
|
|
1065
|
+
|
|
1066
|
+
throw new MessagevisorCLIError(
|
|
1067
|
+
`Promotion has ${conflicts.length} conflict(s) and --conflicts=fail was used.\n${preview}${suffix}`,
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
if (options.apply === true) {
|
|
1072
|
+
await writePlan(destinationDatasource, plans);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
const created = plans
|
|
1076
|
+
.filter((plan) => !plan.destination)
|
|
1077
|
+
.map((plan) =>
|
|
1078
|
+
path.relative(
|
|
1079
|
+
process.cwd(),
|
|
1080
|
+
getEntityFilePath(destinationDatasource.getConfig(), plan.type, plan.key),
|
|
1081
|
+
),
|
|
1082
|
+
);
|
|
1083
|
+
const updated = plans
|
|
1084
|
+
.filter((plan) => plan.destination && !deepEqual(plan.destination, plan.merged))
|
|
1085
|
+
.map((plan) =>
|
|
1086
|
+
path.relative(
|
|
1087
|
+
process.cwd(),
|
|
1088
|
+
getEntityFilePath(destinationDatasource.getConfig(), plan.type, plan.key),
|
|
1089
|
+
),
|
|
1090
|
+
);
|
|
1091
|
+
const unchanged = plans
|
|
1092
|
+
.filter((plan) => plan.destination && deepEqual(plan.destination, plan.merged))
|
|
1093
|
+
.map((plan) =>
|
|
1094
|
+
path.relative(
|
|
1095
|
+
process.cwd(),
|
|
1096
|
+
getEntityFilePath(destinationDatasource.getConfig(), plan.type, plan.key),
|
|
1097
|
+
),
|
|
1098
|
+
);
|
|
1099
|
+
|
|
1100
|
+
const result: PromoteProjectSetsResult = {
|
|
1101
|
+
from: options.from,
|
|
1102
|
+
to: options.to,
|
|
1103
|
+
apply: options.apply === true,
|
|
1104
|
+
duration: Date.now() - startTime,
|
|
1105
|
+
filters: {
|
|
1106
|
+
targets: toArray(options.target),
|
|
1107
|
+
locales: toArray(options.locale),
|
|
1108
|
+
includeMessages: toArray(options.includeMessages),
|
|
1109
|
+
excludeMessages: toArray(options.excludeMessages),
|
|
1110
|
+
excludeOverrides: options.excludeOverrides === true,
|
|
1111
|
+
conflicts: conflictPolicy,
|
|
1112
|
+
},
|
|
1113
|
+
dependencies: {
|
|
1114
|
+
locales: plans.filter((plan) => plan.type === "locale").length,
|
|
1115
|
+
attributes: plans.filter((plan) => plan.type === "attribute").length,
|
|
1116
|
+
segments: plans.filter((plan) => plan.type === "segment").length,
|
|
1117
|
+
targets: plans.filter((plan) => plan.type === "target").length,
|
|
1118
|
+
messages: plans.filter((plan) => plan.type === "message").length,
|
|
1119
|
+
tests: plans.filter((plan) => plan.type === "test").length,
|
|
1120
|
+
},
|
|
1121
|
+
files: { created, updated, unchanged },
|
|
1122
|
+
conflicts,
|
|
1123
|
+
};
|
|
1124
|
+
|
|
1125
|
+
if (result.apply && auditFormat) {
|
|
1126
|
+
result.auditFilePath = await writePromotionAudit(projectConfig, result, auditFormat);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
return result;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function printPromoteResult(
|
|
1133
|
+
result: PromoteProjectSetsResult,
|
|
1134
|
+
options: { showUnchanged?: boolean } = {},
|
|
1135
|
+
) {
|
|
1136
|
+
console.log("");
|
|
1137
|
+
console.log(CLI_FORMAT_BOLD, `Promoting Messagevisor set translations`);
|
|
1138
|
+
console.log(` From: ${result.from}`);
|
|
1139
|
+
console.log(` To: ${result.to}`);
|
|
1140
|
+
console.log(` Mode: ${result.apply ? "apply" : "preview"}`);
|
|
1141
|
+
if (result.filters.targets.length > 0)
|
|
1142
|
+
console.log(` Targets: ${result.filters.targets.join(", ")}`);
|
|
1143
|
+
if (result.filters.locales.length > 0)
|
|
1144
|
+
console.log(` Locales: ${result.filters.locales.join(", ")}`);
|
|
1145
|
+
if (result.filters.includeMessages.length > 0)
|
|
1146
|
+
console.log(` Include messages: ${result.filters.includeMessages.join(", ")}`);
|
|
1147
|
+
if (result.filters.excludeMessages.length > 0)
|
|
1148
|
+
console.log(` Exclude messages: ${result.filters.excludeMessages.join(", ")}`);
|
|
1149
|
+
console.log(` Conflict policy: ${result.filters.conflicts}`);
|
|
1150
|
+
console.log(
|
|
1151
|
+
` Overrides: ${result.filters.excludeOverrides ? "excluded; existing destination overrides are preserved" : "included"}`,
|
|
1152
|
+
);
|
|
1153
|
+
console.log("");
|
|
1154
|
+
console.log(
|
|
1155
|
+
` Dependencies: ${result.dependencies.locales} locales, ${result.dependencies.attributes} attributes, ${result.dependencies.segments} segments, ${result.dependencies.targets} targets, ${result.dependencies.messages} messages, ${result.dependencies.tests} tests`,
|
|
1156
|
+
);
|
|
1157
|
+
console.log(` Created: ${result.files.created.length}`);
|
|
1158
|
+
console.log(` Updated: ${result.files.updated.length}`);
|
|
1159
|
+
console.log(` Unchanged: ${result.files.unchanged.length}`);
|
|
1160
|
+
console.log(` Conflicts: ${result.conflicts.length}`);
|
|
1161
|
+
console.log("");
|
|
1162
|
+
|
|
1163
|
+
printFileGroup("Created", result.files.created, 32);
|
|
1164
|
+
printFileGroup("Updated", result.files.updated, 33);
|
|
1165
|
+
if (options.showUnchanged === true) {
|
|
1166
|
+
printFileGroup("Unchanged", result.files.unchanged, 2);
|
|
1167
|
+
}
|
|
1168
|
+
printConflictPreview(result.conflicts);
|
|
1169
|
+
|
|
1170
|
+
if (result.auditFilePath) {
|
|
1171
|
+
console.log(` Audit: ${colorize(result.auditFilePath, 36)}`);
|
|
1172
|
+
console.log("");
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
console.log(CLI_FORMAT_GREEN, result.apply ? "Promotion applied" : "Promotion preview complete");
|
|
1176
|
+
console.log(CLI_FORMAT_BOLD, `Time: ${prettyDuration(result.duration)}`);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function printFileGroup(label: string, files: string[], color: number) {
|
|
1180
|
+
if (files.length === 0) {
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
console.log(CLI_FORMAT_BOLD, label);
|
|
1185
|
+
for (const filePath of files) {
|
|
1186
|
+
console.log(` ${colorize(filePath, color)}`);
|
|
1187
|
+
}
|
|
1188
|
+
console.log("");
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
function printConflictPreview(conflicts: PromotionConflict[]) {
|
|
1192
|
+
if (conflicts.length === 0) {
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
console.log(CLI_FORMAT_BOLD, "Conflicts");
|
|
1197
|
+
for (const conflict of conflicts.slice(0, 10)) {
|
|
1198
|
+
console.log(` ${colorize(conflict.type, 33)} ${conflict.key} ${colorize(conflict.path, 2)}`);
|
|
1199
|
+
}
|
|
1200
|
+
if (conflicts.length > 10) {
|
|
1201
|
+
console.log(` ${colorize(`...and ${conflicts.length - 10} more`, 2)}`);
|
|
1202
|
+
}
|
|
1203
|
+
console.log("");
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
export const promotePlugin = {
|
|
1207
|
+
command: "promote",
|
|
1208
|
+
handler: async ({ projectConfig, datasource, parsed }: any) => {
|
|
1209
|
+
let result;
|
|
1210
|
+
|
|
1211
|
+
try {
|
|
1212
|
+
result = await promoteProjectSets(projectConfig, datasource, {
|
|
1213
|
+
from: parsed.from,
|
|
1214
|
+
to: parsed.to,
|
|
1215
|
+
target: parsed.target,
|
|
1216
|
+
locale: parsed.locale,
|
|
1217
|
+
includeMessages: parsed.includeMessages,
|
|
1218
|
+
excludeMessages: parsed.excludeMessages,
|
|
1219
|
+
excludeOverrides: parsed.excludeOverrides,
|
|
1220
|
+
conflicts: parsed.conflicts,
|
|
1221
|
+
allowEmpty: parsed.allowEmpty,
|
|
1222
|
+
apply: parsed.apply === true || parsed.apply === "true",
|
|
1223
|
+
audit: parsed.audit,
|
|
1224
|
+
});
|
|
1225
|
+
} catch (error) {
|
|
1226
|
+
if (printMessagevisorCLIError(error)) {
|
|
1227
|
+
return false;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
throw error;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
printPromoteResult(result, {
|
|
1234
|
+
showUnchanged: parsed.showUnchanged === true || parsed.showUnchanged === "true",
|
|
1235
|
+
});
|
|
1236
|
+
},
|
|
1237
|
+
examples: [
|
|
1238
|
+
{
|
|
1239
|
+
command: "promote --from=dev --to=staging",
|
|
1240
|
+
description: "preview all translations that can be promoted from one set to another",
|
|
1241
|
+
},
|
|
1242
|
+
{
|
|
1243
|
+
command: "promote --from=dev --to=staging --target=web",
|
|
1244
|
+
description: "preview translations affecting a target",
|
|
1245
|
+
},
|
|
1246
|
+
{
|
|
1247
|
+
command: "promote --from=dev --to=staging --excludeOverrides",
|
|
1248
|
+
description: "preview messages without copying overrides",
|
|
1249
|
+
},
|
|
1250
|
+
{
|
|
1251
|
+
command: "promote --from=dev --to=staging --conflicts=fail",
|
|
1252
|
+
description: "fail instead of overwriting conflicting destination fields",
|
|
1253
|
+
},
|
|
1254
|
+
{
|
|
1255
|
+
command: "promote --from=dev --to=staging --apply",
|
|
1256
|
+
description: "apply a promotion and write destination files",
|
|
1257
|
+
},
|
|
1258
|
+
{
|
|
1259
|
+
command: "promote --from=dev --to=staging --apply --audit=markdown",
|
|
1260
|
+
description: "write a promotion audit file",
|
|
1261
|
+
},
|
|
1262
|
+
{
|
|
1263
|
+
command: "promote --from=dev --to=staging --showUnchanged",
|
|
1264
|
+
description: "preview a promotion and include unchanged entries",
|
|
1265
|
+
},
|
|
1266
|
+
],
|
|
1267
|
+
};
|