@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,272 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { Readable } from "stream";
|
|
5
|
+
|
|
6
|
+
import { getProjectConfig } from "../config";
|
|
7
|
+
import { Datasource } from "../datasource";
|
|
8
|
+
import { lintProject } from "../linter";
|
|
9
|
+
import { createPlugin } from "./index";
|
|
10
|
+
|
|
11
|
+
async function writeFile(root: string, relativePath: string, content: string) {
|
|
12
|
+
const filePath = path.join(root, relativePath);
|
|
13
|
+
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
14
|
+
await fs.promises.writeFile(filePath, content);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function createProject(configContent = "module.exports = {};\n") {
|
|
18
|
+
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-create-"));
|
|
19
|
+
await writeFile(root, "messagevisor.config.js", configContent);
|
|
20
|
+
return root;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function createSetsProject() {
|
|
24
|
+
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-create-sets-"));
|
|
25
|
+
await writeFile(root, "messagevisor.config.js", "module.exports = { sets: true };\n");
|
|
26
|
+
await fs.promises.mkdir(path.join(root, "sets/dev"), { recursive: true });
|
|
27
|
+
await fs.promises.mkdir(path.join(root, "sets/production"), { recursive: true });
|
|
28
|
+
return root;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function mockStdin(content: string) {
|
|
32
|
+
const originalDescriptor = Object.getOwnPropertyDescriptor(process, "stdin");
|
|
33
|
+
const stdin = Readable.from([content]) as NodeJS.ReadableStream & { isTTY?: boolean };
|
|
34
|
+
stdin.isTTY = false;
|
|
35
|
+
|
|
36
|
+
Object.defineProperty(process, "stdin", {
|
|
37
|
+
configurable: true,
|
|
38
|
+
value: stdin,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return () => {
|
|
42
|
+
if (originalDescriptor) {
|
|
43
|
+
Object.defineProperty(process, "stdin", originalDescriptor);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
delete (process as any).stdin;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function pluginOptions(projectConfig: any, datasource: any, parsed: Record<string, unknown>) {
|
|
52
|
+
return {
|
|
53
|
+
rootDirectoryPath: "",
|
|
54
|
+
projectConfig,
|
|
55
|
+
datasource,
|
|
56
|
+
parsed: {
|
|
57
|
+
_: [],
|
|
58
|
+
...parsed,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe("createPlugin", function () {
|
|
64
|
+
it("creates missing message definitions from --keys and respects namespace paths", async function () {
|
|
65
|
+
const root = await createProject();
|
|
66
|
+
const projectConfig = getProjectConfig(root);
|
|
67
|
+
const datasource = new Datasource(projectConfig, root);
|
|
68
|
+
|
|
69
|
+
await createPlugin.handler(
|
|
70
|
+
pluginOptions(projectConfig, datasource, {
|
|
71
|
+
locales: true,
|
|
72
|
+
keys: "en\n",
|
|
73
|
+
}),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
await createPlugin.handler(
|
|
77
|
+
pluginOptions(projectConfig, datasource, {
|
|
78
|
+
messages: true,
|
|
79
|
+
keys: "auth.signin\nauth.signout\n",
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
expect(fs.existsSync(path.join(root, "messages/auth/signin.yml"))).toBe(true);
|
|
84
|
+
expect(fs.existsSync(path.join(root, "messages/auth/signout.yml"))).toBe(true);
|
|
85
|
+
|
|
86
|
+
const lintResult = await lintProject(projectConfig, datasource);
|
|
87
|
+
expect(lintResult.hasError).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("creates missing locale, target, attribute, and segment definitions with lint-valid shells", async function () {
|
|
91
|
+
const root = await createProject();
|
|
92
|
+
const projectConfig = getProjectConfig(root);
|
|
93
|
+
const datasource = new Datasource(projectConfig, root);
|
|
94
|
+
|
|
95
|
+
await createPlugin.handler(
|
|
96
|
+
pluginOptions(projectConfig, datasource, {
|
|
97
|
+
locales: true,
|
|
98
|
+
keys: "en\nnl\n",
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
await createPlugin.handler(
|
|
102
|
+
pluginOptions(projectConfig, datasource, {
|
|
103
|
+
targets: true,
|
|
104
|
+
keys: "web\n",
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
await createPlugin.handler(
|
|
108
|
+
pluginOptions(projectConfig, datasource, {
|
|
109
|
+
attributes: true,
|
|
110
|
+
keys: "plan\n",
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
113
|
+
await createPlugin.handler(
|
|
114
|
+
pluginOptions(projectConfig, datasource, {
|
|
115
|
+
segments: true,
|
|
116
|
+
keys: "everyone\n",
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const lintResult = await lintProject(projectConfig, datasource);
|
|
121
|
+
expect(lintResult.hasError).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("reads keys from stdin, trims whitespace, ignores blank lines, and dedupes duplicates", async function () {
|
|
125
|
+
const root = await createProject();
|
|
126
|
+
const projectConfig = getProjectConfig(root);
|
|
127
|
+
const datasource = new Datasource(projectConfig, root);
|
|
128
|
+
|
|
129
|
+
await createPlugin.handler(
|
|
130
|
+
pluginOptions(projectConfig, datasource, {
|
|
131
|
+
locales: true,
|
|
132
|
+
keys: "en\n",
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const restoreStdin = mockStdin(" auth.signin \n\nauth.signin\naccount.profile\n");
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
await createPlugin.handler(
|
|
140
|
+
pluginOptions(projectConfig, datasource, {
|
|
141
|
+
messages: true,
|
|
142
|
+
}),
|
|
143
|
+
);
|
|
144
|
+
} finally {
|
|
145
|
+
restoreStdin();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
expect((await datasource.listMessages()).sort()).toEqual(["account.profile", "auth.signin"]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("skips existing files and reports a JSON summary", async function () {
|
|
152
|
+
const root = await createProject();
|
|
153
|
+
const projectConfig = getProjectConfig(root);
|
|
154
|
+
const datasource = new Datasource(projectConfig, root);
|
|
155
|
+
const logSpy = jest.spyOn(console, "log").mockImplementation(() => {});
|
|
156
|
+
|
|
157
|
+
await createPlugin.handler(
|
|
158
|
+
pluginOptions(projectConfig, datasource, {
|
|
159
|
+
locales: true,
|
|
160
|
+
keys: "en\n",
|
|
161
|
+
}),
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
await createPlugin.handler(
|
|
165
|
+
pluginOptions(projectConfig, datasource, {
|
|
166
|
+
messages: true,
|
|
167
|
+
keys: "auth.signin\n",
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
await createPlugin.handler(
|
|
172
|
+
pluginOptions(projectConfig, datasource, {
|
|
173
|
+
messages: true,
|
|
174
|
+
keys: "auth.signin\nauth.signout\n",
|
|
175
|
+
json: true,
|
|
176
|
+
}),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const summary = JSON.parse(String(logSpy.mock.calls[logSpy.mock.calls.length - 1][0]));
|
|
180
|
+
expect(summary.entityType).toEqual("messages");
|
|
181
|
+
expect(summary.requestedKeys).toEqual(["auth.signin", "auth.signout"]);
|
|
182
|
+
expect(summary.createdKeys).toEqual(["auth.signout"]);
|
|
183
|
+
expect(summary.skippedKeys).toEqual(["auth.signin"]);
|
|
184
|
+
expect(summary.createdFilePaths).toEqual([
|
|
185
|
+
path.relative(process.cwd(), path.join(root, "messages/auth/signout.yml")),
|
|
186
|
+
]);
|
|
187
|
+
logSpy.mockRestore();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("errors when creating messages without any locales", async function () {
|
|
191
|
+
const root = await createProject();
|
|
192
|
+
const projectConfig = getProjectConfig(root);
|
|
193
|
+
const datasource = new Datasource(projectConfig, root);
|
|
194
|
+
const errorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
await expect(
|
|
198
|
+
createPlugin.handler(
|
|
199
|
+
pluginOptions(projectConfig, datasource, {
|
|
200
|
+
messages: true,
|
|
201
|
+
keys: "auth.signin\n",
|
|
202
|
+
}),
|
|
203
|
+
),
|
|
204
|
+
).resolves.toEqual(false);
|
|
205
|
+
|
|
206
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
207
|
+
"Cannot create messages without at least one locale. Create a locale first.",
|
|
208
|
+
);
|
|
209
|
+
} finally {
|
|
210
|
+
errorSpy.mockRestore();
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("requires exactly one selector flag", async function () {
|
|
215
|
+
const root = await createProject();
|
|
216
|
+
const projectConfig = getProjectConfig(root);
|
|
217
|
+
const datasource = new Datasource(projectConfig, root);
|
|
218
|
+
const errorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
await expect(
|
|
222
|
+
createPlugin.handler(
|
|
223
|
+
pluginOptions(projectConfig, datasource, {
|
|
224
|
+
keys: "auth.signin\n",
|
|
225
|
+
}),
|
|
226
|
+
),
|
|
227
|
+
).resolves.toEqual(false);
|
|
228
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
229
|
+
"Nothing to create. Pass exactly one of --messages, --locales, --targets, --attributes, or --segments.",
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
errorSpy.mockClear();
|
|
233
|
+
|
|
234
|
+
await expect(
|
|
235
|
+
createPlugin.handler(
|
|
236
|
+
pluginOptions(projectConfig, datasource, {
|
|
237
|
+
messages: true,
|
|
238
|
+
locales: true,
|
|
239
|
+
keys: "auth.signin\n",
|
|
240
|
+
}),
|
|
241
|
+
),
|
|
242
|
+
).resolves.toEqual(false);
|
|
243
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
244
|
+
"Pass exactly one of --messages, --locales, --targets, --attributes, or --segments.",
|
|
245
|
+
);
|
|
246
|
+
} finally {
|
|
247
|
+
errorSpy.mockRestore();
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("requires --set in sets-enabled projects", async function () {
|
|
252
|
+
const root = await createSetsProject();
|
|
253
|
+
const projectConfig = getProjectConfig(root);
|
|
254
|
+
const datasource = new Datasource(projectConfig, root);
|
|
255
|
+
const errorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
await expect(
|
|
259
|
+
createPlugin.handler(
|
|
260
|
+
pluginOptions(projectConfig, datasource, {
|
|
261
|
+
messages: true,
|
|
262
|
+
keys: "auth.signin\n",
|
|
263
|
+
}),
|
|
264
|
+
),
|
|
265
|
+
).resolves.toEqual(false);
|
|
266
|
+
|
|
267
|
+
expect(errorSpy).toHaveBeenCalledWith("Pass --set=<set>");
|
|
268
|
+
} finally {
|
|
269
|
+
errorSpy.mockRestore();
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
});
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
|
|
3
|
+
import type { Attribute, Locale, Message, Target, Segment } from "@messagevisor/types";
|
|
4
|
+
|
|
5
|
+
import type { Plugin } from "../cli";
|
|
6
|
+
import { MessagevisorCLIError, printMessagevisorCLIError } from "../error";
|
|
7
|
+
import { getProjectSetExecutions } from "../sets";
|
|
8
|
+
|
|
9
|
+
type CreateEntityType = "messages" | "locales" | "targets" | "attributes" | "segments";
|
|
10
|
+
|
|
11
|
+
interface CreateSummary {
|
|
12
|
+
entityType: CreateEntityType;
|
|
13
|
+
requestedKeys: string[];
|
|
14
|
+
createdKeys: string[];
|
|
15
|
+
skippedKeys: string[];
|
|
16
|
+
createdFilePaths: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getSelectedEntityType(parsed: Record<string, unknown>): CreateEntityType {
|
|
20
|
+
const selected = (
|
|
21
|
+
["messages", "locales", "targets", "attributes", "segments"] as CreateEntityType[]
|
|
22
|
+
).filter((entityType) => Boolean(parsed[entityType]));
|
|
23
|
+
|
|
24
|
+
if (selected.length === 0) {
|
|
25
|
+
throw new MessagevisorCLIError(
|
|
26
|
+
"Nothing to create. Pass exactly one of --messages, --locales, --targets, --attributes, or --segments.",
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (selected.length > 1) {
|
|
31
|
+
throw new MessagevisorCLIError(
|
|
32
|
+
"Pass exactly one of --messages, --locales, --targets, --attributes, or --segments.",
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return selected[0];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function readInputKeys(parsed: Record<string, unknown>) {
|
|
40
|
+
if (typeof parsed.keys === "string") {
|
|
41
|
+
return parsed.keys;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (process.stdin.isTTY) {
|
|
45
|
+
throw new MessagevisorCLIError(
|
|
46
|
+
"Pass --keys=<multiline string> or provide newline-separated keys via stdin.",
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const chunks: string[] = [];
|
|
51
|
+
|
|
52
|
+
for await (const chunk of process.stdin) {
|
|
53
|
+
chunks.push(String(chunk));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return chunks.join("");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function parseKeys(input: string) {
|
|
60
|
+
const seen = new Set<string>();
|
|
61
|
+
const keys: string[] = [];
|
|
62
|
+
|
|
63
|
+
for (const rawLine of input.split(/\r?\n/g)) {
|
|
64
|
+
const key = rawLine.trim();
|
|
65
|
+
|
|
66
|
+
if (!key || seen.has(key)) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
seen.add(key);
|
|
71
|
+
keys.push(key);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (keys.length === 0) {
|
|
75
|
+
throw new MessagevisorCLIError("No keys found. Pass at least one newline-separated key.");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return keys;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getParserExtension(projectConfig: any) {
|
|
82
|
+
return (projectConfig.parser as any).extension || "yml";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getEntityDirectoryPath(projectConfig: any, entityType: CreateEntityType) {
|
|
86
|
+
if (entityType === "messages") {
|
|
87
|
+
return projectConfig.messagesDirectoryPath;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (entityType === "locales") {
|
|
91
|
+
return projectConfig.localesDirectoryPath;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (entityType === "targets") {
|
|
95
|
+
return projectConfig.targetsDirectoryPath;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (entityType === "attributes") {
|
|
99
|
+
return projectConfig.attributesDirectoryPath;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return projectConfig.segmentsDirectoryPath;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getEntityFilePath(projectConfig: any, entityType: CreateEntityType, key: string) {
|
|
106
|
+
return (
|
|
107
|
+
path.join(
|
|
108
|
+
getEntityDirectoryPath(projectConfig, entityType),
|
|
109
|
+
...key.split(projectConfig.namespaceCharacter),
|
|
110
|
+
) + `.${getParserExtension(projectConfig)}`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function createEntityShell(
|
|
115
|
+
datasource: any,
|
|
116
|
+
entityType: CreateEntityType,
|
|
117
|
+
): Promise<Message | Locale | Target | Attribute | Segment> {
|
|
118
|
+
if (entityType === "messages") {
|
|
119
|
+
const localeKeys = await datasource.listLocales();
|
|
120
|
+
|
|
121
|
+
if (localeKeys.length === 0) {
|
|
122
|
+
throw new MessagevisorCLIError(
|
|
123
|
+
"Cannot create messages without at least one locale. Create a locale first.",
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
description: "",
|
|
129
|
+
translations: {
|
|
130
|
+
[localeKeys[0]]: "",
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (entityType === "locales") {
|
|
136
|
+
return {
|
|
137
|
+
description: "",
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (entityType === "targets") {
|
|
142
|
+
return {
|
|
143
|
+
description: "",
|
|
144
|
+
includeMessages: ["*"],
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (entityType === "attributes") {
|
|
149
|
+
return {
|
|
150
|
+
description: "",
|
|
151
|
+
type: "string",
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
description: "",
|
|
157
|
+
conditions: "*",
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function listExistingKeys(datasource: any, entityType: CreateEntityType) {
|
|
162
|
+
if (entityType === "messages") {
|
|
163
|
+
return datasource.listMessages();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (entityType === "locales") {
|
|
167
|
+
return datasource.listLocales();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (entityType === "targets") {
|
|
171
|
+
return datasource.listTargets();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (entityType === "attributes") {
|
|
175
|
+
return datasource.listAttributes();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return datasource.listSegments();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function writeEntity(datasource: any, entityType: CreateEntityType, key: string) {
|
|
182
|
+
const entity = await createEntityShell(datasource, entityType);
|
|
183
|
+
|
|
184
|
+
if (entityType === "messages") {
|
|
185
|
+
await datasource.writeMessage(key, entity as Message);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (entityType === "locales") {
|
|
190
|
+
await datasource.writeLocale(key, entity as Locale);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (entityType === "targets") {
|
|
195
|
+
await datasource.writeTarget(key, entity as Target);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (entityType === "attributes") {
|
|
200
|
+
await datasource.writeAttribute(key, entity as Attribute);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
await datasource.writeSegment(key, entity as Segment);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function createDefinitions(
|
|
208
|
+
projectConfig: any,
|
|
209
|
+
datasource: any,
|
|
210
|
+
entityType: CreateEntityType,
|
|
211
|
+
requestedKeys: string[],
|
|
212
|
+
): Promise<CreateSummary> {
|
|
213
|
+
const existingKeys = new Set(await listExistingKeys(datasource, entityType));
|
|
214
|
+
const createdKeys: string[] = [];
|
|
215
|
+
const skippedKeys: string[] = [];
|
|
216
|
+
const createdFilePaths: string[] = [];
|
|
217
|
+
|
|
218
|
+
for (const key of requestedKeys) {
|
|
219
|
+
if (existingKeys.has(key)) {
|
|
220
|
+
skippedKeys.push(key);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
await writeEntity(datasource, entityType, key);
|
|
225
|
+
createdKeys.push(key);
|
|
226
|
+
createdFilePaths.push(
|
|
227
|
+
path.relative(process.cwd(), getEntityFilePath(projectConfig, entityType, key)),
|
|
228
|
+
);
|
|
229
|
+
existingKeys.add(key);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
entityType,
|
|
234
|
+
requestedKeys,
|
|
235
|
+
createdKeys,
|
|
236
|
+
skippedKeys,
|
|
237
|
+
createdFilePaths,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function printSummary(summary: CreateSummary) {
|
|
242
|
+
console.log("");
|
|
243
|
+
console.log(`Entity type : ${summary.entityType}`);
|
|
244
|
+
console.log(`Requested : ${summary.requestedKeys.length}`);
|
|
245
|
+
console.log(`Created : ${summary.createdKeys.length}`);
|
|
246
|
+
console.log(`Skipped existing : ${summary.skippedKeys.length}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export const createPlugin: Plugin = {
|
|
250
|
+
command: "create",
|
|
251
|
+
handler: async ({ projectConfig, datasource, parsed }) => {
|
|
252
|
+
try {
|
|
253
|
+
if (projectConfig.sets && !parsed.set) {
|
|
254
|
+
throw new MessagevisorCLIError("Pass --set=<set>");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (projectConfig.sets) {
|
|
258
|
+
const [execution] = await getProjectSetExecutions(projectConfig, datasource, parsed.set);
|
|
259
|
+
projectConfig = execution.projectConfig;
|
|
260
|
+
datasource = execution.datasource;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const entityType = getSelectedEntityType(parsed);
|
|
264
|
+
const requestedKeys = parseKeys(await readInputKeys(parsed));
|
|
265
|
+
const summary = await createDefinitions(projectConfig, datasource, entityType, requestedKeys);
|
|
266
|
+
|
|
267
|
+
if (parsed.json) {
|
|
268
|
+
console.log(parsed.pretty ? JSON.stringify(summary, null, 2) : JSON.stringify(summary));
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
printSummary(summary);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
if (printMessagevisorCLIError(error)) {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
examples: [
|
|
282
|
+
{
|
|
283
|
+
command: `create --messages --keys=$'auth.signin\\nauth.signout'`,
|
|
284
|
+
description: "create missing message definitions from newline-separated keys",
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
command: `create --locales --keys=$'en\\nnl'`,
|
|
288
|
+
description: "create missing locale definitions",
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
command: `create --attributes --keys=$'plan\\nplatform'`,
|
|
292
|
+
description: "create missing attribute definitions",
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
};
|