@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,241 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
|
|
5
|
+
import { getProjectConfig } from "../config";
|
|
6
|
+
import { Datasource } from "../datasource";
|
|
7
|
+
import { generateCodeForProject, generateCodePlugin } from "./index";
|
|
8
|
+
|
|
9
|
+
async function writeFile(root: string, relativePath: string, content: string) {
|
|
10
|
+
const filePath = path.join(root, relativePath);
|
|
11
|
+
|
|
12
|
+
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
13
|
+
await fs.promises.writeFile(filePath, content);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function createProject() {
|
|
17
|
+
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-codegen-"));
|
|
18
|
+
|
|
19
|
+
await writeFile(root, "messagevisor.config.js", "module.exports = {};\n");
|
|
20
|
+
await writeFile(root, "locales/en-US.yml", "description: English\n");
|
|
21
|
+
await writeFile(
|
|
22
|
+
root,
|
|
23
|
+
"targets/web.yml",
|
|
24
|
+
"description: Web\nincludeMessages:\n - common*\n - checkout*\nexcludeMessages:\n - common.hidden\nlocales:\n - en-US\n",
|
|
25
|
+
);
|
|
26
|
+
await writeFile(
|
|
27
|
+
root,
|
|
28
|
+
"messages/common/welcome.yml",
|
|
29
|
+
"description: Welcome\ntranslations:\n en-US: Welcome\n",
|
|
30
|
+
);
|
|
31
|
+
await writeFile(
|
|
32
|
+
root,
|
|
33
|
+
"messages/checkout/total.yml",
|
|
34
|
+
"description: Total\ntranslations:\n en-US: Total\n",
|
|
35
|
+
);
|
|
36
|
+
await writeFile(
|
|
37
|
+
root,
|
|
38
|
+
"messages/common/hidden.yml",
|
|
39
|
+
"description: Hidden\ntranslations:\n en-US: Hidden\n",
|
|
40
|
+
);
|
|
41
|
+
await writeFile(
|
|
42
|
+
root,
|
|
43
|
+
"messages/common/draft.yml",
|
|
44
|
+
"description: Draft\ntranslations:\n en-US: Draft\n",
|
|
45
|
+
);
|
|
46
|
+
await writeFile(
|
|
47
|
+
root,
|
|
48
|
+
"messages/common/archived.yml",
|
|
49
|
+
"description: Archived\narchived: true\ntranslations:\n en-US: Archived\n",
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
return root;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function generate(root: string, options: Record<string, unknown>) {
|
|
56
|
+
const projectConfig = getProjectConfig(root);
|
|
57
|
+
const datasource = new Datasource(projectConfig, root);
|
|
58
|
+
|
|
59
|
+
return generateCodeForProject(projectConfig, datasource, root, {
|
|
60
|
+
language: "typescript",
|
|
61
|
+
outDir: "generated",
|
|
62
|
+
...options,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function readGenerated(root: string, fileName: string) {
|
|
67
|
+
return fs.promises.readFile(path.join(root, "generated", fileName), "utf8");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe("generate-code/typescript", function () {
|
|
71
|
+
let roots: string[] = [];
|
|
72
|
+
let consoleLogSpy: jest.SpyInstance;
|
|
73
|
+
|
|
74
|
+
beforeEach(function () {
|
|
75
|
+
consoleLogSpy = jest.spyOn(console, "log").mockImplementation(function () {
|
|
76
|
+
// Keep generator unit tests focused on generated files.
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
afterEach(async function () {
|
|
81
|
+
consoleLogSpy.mockRestore();
|
|
82
|
+
|
|
83
|
+
for (const root of roots) {
|
|
84
|
+
await fs.promises.rm(root, { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
roots = [];
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("generates typed SDK helpers without runtime key metadata", async function () {
|
|
91
|
+
const root = await createProject();
|
|
92
|
+
roots.push(root);
|
|
93
|
+
|
|
94
|
+
const result = await generate(root, {});
|
|
95
|
+
const messages = await readGenerated(root, "messages.ts");
|
|
96
|
+
const sdk = await readGenerated(root, "sdk.ts");
|
|
97
|
+
const index = await readGenerated(root, "index.ts");
|
|
98
|
+
|
|
99
|
+
expect(result.messageKeys).toEqual([
|
|
100
|
+
"checkout.total",
|
|
101
|
+
"common.draft",
|
|
102
|
+
"common.hidden",
|
|
103
|
+
"common.welcome",
|
|
104
|
+
]);
|
|
105
|
+
expect(messages).toEqual(
|
|
106
|
+
[
|
|
107
|
+
"export type MessagevisorMessageKey =",
|
|
108
|
+
' | "checkout.total"',
|
|
109
|
+
' | "common.draft"',
|
|
110
|
+
' | "common.hidden"',
|
|
111
|
+
' | "common.welcome";',
|
|
112
|
+
"",
|
|
113
|
+
].join("\n"),
|
|
114
|
+
);
|
|
115
|
+
expect(messages).not.toContain("const");
|
|
116
|
+
expect(messages).not.toContain("[");
|
|
117
|
+
expect(messages).not.toContain("{");
|
|
118
|
+
expect(sdk).toContain("let instance: Messagevisor | undefined;");
|
|
119
|
+
expect(sdk).toContain("export function setInstance(messagevisor: Messagevisor)");
|
|
120
|
+
expect(sdk).toContain("export function getInstance()");
|
|
121
|
+
expect(sdk).toContain("export function translate(");
|
|
122
|
+
expect(sdk).toContain("export const t = translate;");
|
|
123
|
+
expect(index).toContain('export type * from "./messages";');
|
|
124
|
+
expect(index).toContain('export * from "./sdk";');
|
|
125
|
+
expect(index).not.toContain("./react");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("prints expected plugin option errors without throwing", async function () {
|
|
129
|
+
const root = await createProject();
|
|
130
|
+
roots.push(root);
|
|
131
|
+
const projectConfig = getProjectConfig(root);
|
|
132
|
+
const datasource = new Datasource(projectConfig, root);
|
|
133
|
+
const errorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
await expect(
|
|
137
|
+
generateCodePlugin.handler({
|
|
138
|
+
rootDirectoryPath: root,
|
|
139
|
+
projectConfig,
|
|
140
|
+
datasource,
|
|
141
|
+
parsed: {
|
|
142
|
+
outDir: "generated",
|
|
143
|
+
},
|
|
144
|
+
} as any),
|
|
145
|
+
).resolves.toEqual(false);
|
|
146
|
+
|
|
147
|
+
expect(errorSpy).toHaveBeenCalledWith("Option --language is required.");
|
|
148
|
+
} finally {
|
|
149
|
+
errorSpy.mockRestore();
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("generates React helpers when requested", async function () {
|
|
154
|
+
const root = await createProject();
|
|
155
|
+
roots.push(root);
|
|
156
|
+
|
|
157
|
+
await generate(root, { react: true });
|
|
158
|
+
|
|
159
|
+
const react = await readGenerated(root, "react.ts");
|
|
160
|
+
const index = await readGenerated(root, "index.ts");
|
|
161
|
+
|
|
162
|
+
expect(react).toContain("useBaseTranslation");
|
|
163
|
+
expect(react).toContain("export function useTranslation(");
|
|
164
|
+
expect(react).toContain("export function useMessagevisor()");
|
|
165
|
+
expect(react).toContain("t: messagevisor.t as typeof useTranslation");
|
|
166
|
+
expect(index).toContain('export * from "./react";');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("supports target and include/exclude message filters", async function () {
|
|
170
|
+
const root = await createProject();
|
|
171
|
+
roots.push(root);
|
|
172
|
+
|
|
173
|
+
await generate(root, {
|
|
174
|
+
target: "web",
|
|
175
|
+
includeMessages: "common*",
|
|
176
|
+
excludeMessages: "common.hidden",
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const messages = await readGenerated(root, "messages.ts");
|
|
180
|
+
|
|
181
|
+
expect(messages).toContain('"common.draft"');
|
|
182
|
+
expect(messages).toContain('"common.welcome"');
|
|
183
|
+
expect(messages).not.toContain("checkout.total");
|
|
184
|
+
expect(messages).not.toContain("common.hidden");
|
|
185
|
+
expect(messages).not.toContain("common.archived");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("generates never when no messages match", async function () {
|
|
189
|
+
const root = await createProject();
|
|
190
|
+
roots.push(root);
|
|
191
|
+
|
|
192
|
+
await generate(root, {
|
|
193
|
+
includeMessages: "missing*",
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
await expect(readGenerated(root, "messages.ts")).resolves.toEqual(
|
|
197
|
+
"export type MessagevisorMessageKey = never;\n",
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("generates union across sets by default and selected set with --set", async function () {
|
|
202
|
+
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-codegen-"));
|
|
203
|
+
roots.push(root);
|
|
204
|
+
|
|
205
|
+
await writeFile(root, "messagevisor.config.js", "module.exports = { sets: true };\n");
|
|
206
|
+
|
|
207
|
+
for (const set of ["storefront", "admin"]) {
|
|
208
|
+
await writeFile(root, `sets/${set}/locales/en-US.yml`, "description: English\n");
|
|
209
|
+
await writeFile(
|
|
210
|
+
root,
|
|
211
|
+
`sets/${set}/messages/common/welcome.yml`,
|
|
212
|
+
`description: Welcome\ntranslations:\n en-US: ${set} welcome\n`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
await writeFile(
|
|
217
|
+
root,
|
|
218
|
+
"sets/storefront/messages/storefront/cart.yml",
|
|
219
|
+
"description: Cart\ntranslations:\n en-US: Cart\n",
|
|
220
|
+
);
|
|
221
|
+
await writeFile(
|
|
222
|
+
root,
|
|
223
|
+
"sets/admin/messages/admin/dashboard.yml",
|
|
224
|
+
"description: Dashboard\ntranslations:\n en-US: Dashboard\n",
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
await generate(root, {});
|
|
228
|
+
expect(await readGenerated(root, "messages.ts")).toContain('"admin.dashboard"');
|
|
229
|
+
expect(await readGenerated(root, "messages.ts")).toContain('"storefront.cart"');
|
|
230
|
+
expect(await readGenerated(root, "messages.ts")).toContain('"common.welcome"');
|
|
231
|
+
|
|
232
|
+
await fs.promises.rm(path.join(root, "generated"), { recursive: true, force: true });
|
|
233
|
+
|
|
234
|
+
await generate(root, { set: "storefront" });
|
|
235
|
+
const messages = await readGenerated(root, "messages.ts");
|
|
236
|
+
|
|
237
|
+
expect(messages).not.toContain("admin.dashboard");
|
|
238
|
+
expect(messages).toContain('"storefront.cart"');
|
|
239
|
+
expect(messages).toContain('"common.welcome"');
|
|
240
|
+
});
|
|
241
|
+
});
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
|
|
4
|
+
import type { Message, Target } from "@messagevisor/types";
|
|
5
|
+
|
|
6
|
+
import type { ProjectConfig } from "../config";
|
|
7
|
+
import type { Datasource } from "../datasource";
|
|
8
|
+
import { MessagevisorCLIError } from "../error";
|
|
9
|
+
import { getProjectSetExecutions } from "../sets";
|
|
10
|
+
|
|
11
|
+
export interface TypeScriptCodeGenerationOptions {
|
|
12
|
+
set?: string | string[];
|
|
13
|
+
target?: string | string[];
|
|
14
|
+
includeMessages?: string | string[];
|
|
15
|
+
excludeMessages?: string | string[];
|
|
16
|
+
react?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface TypeScriptCodeGenerationResult {
|
|
20
|
+
messageKeys: string[];
|
|
21
|
+
files: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function toArray(value?: string | string[]): string[] {
|
|
25
|
+
if (typeof value === "undefined") return [];
|
|
26
|
+
return Array.isArray(value) ? value : [value];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function matchesPattern(key: string, patterns?: string[]) {
|
|
30
|
+
if (!patterns || patterns.length === 0) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return patterns.some((pattern) => {
|
|
35
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
36
|
+
return new RegExp(`^${escaped}$`).test(key);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isAvailable(message: Message) {
|
|
41
|
+
return !message.archived;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function sortUnique(values: string[]) {
|
|
45
|
+
return Array.from(new Set(values)).sort();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveSelectedSet(projectConfig: ProjectConfig, value?: string | string[]) {
|
|
49
|
+
const sets = toArray(value);
|
|
50
|
+
|
|
51
|
+
if (!projectConfig.sets && sets.length > 0) {
|
|
52
|
+
throw new MessagevisorCLIError("Option --set can only be used when project sets are enabled.");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (sets.length > 1) {
|
|
56
|
+
throw new MessagevisorCLIError("Only one --set value can be used for code generation.");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return sets[0];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function collectTargetMessageKeys(
|
|
63
|
+
datasource: Datasource,
|
|
64
|
+
targetKeys: string[],
|
|
65
|
+
allMessageKeys: string[],
|
|
66
|
+
) {
|
|
67
|
+
if (targetKeys.length === 0) {
|
|
68
|
+
return allMessageKeys;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const selected: string[] = [];
|
|
72
|
+
|
|
73
|
+
for (const targetKey of targetKeys) {
|
|
74
|
+
const target = (await datasource.readTarget(targetKey)) as Target;
|
|
75
|
+
const includeMessages = target.includeMessages?.length ? target.includeMessages : ["*"];
|
|
76
|
+
const excludeMessages = target.excludeMessages || [];
|
|
77
|
+
|
|
78
|
+
for (const messageKey of allMessageKeys) {
|
|
79
|
+
if (
|
|
80
|
+
matchesPattern(messageKey, includeMessages) &&
|
|
81
|
+
!matchesPattern(messageKey, excludeMessages)
|
|
82
|
+
) {
|
|
83
|
+
selected.push(messageKey);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return sortUnique(selected);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function collectMessageKeysForDatasource(
|
|
92
|
+
datasource: Datasource,
|
|
93
|
+
options: TypeScriptCodeGenerationOptions,
|
|
94
|
+
) {
|
|
95
|
+
const allMessageKeys = await datasource.listMessages();
|
|
96
|
+
const messages: Record<string, Message> = {};
|
|
97
|
+
|
|
98
|
+
for (const messageKey of allMessageKeys) {
|
|
99
|
+
messages[messageKey] = await datasource.readMessage(messageKey);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let messageKeys = await collectTargetMessageKeys(
|
|
103
|
+
datasource,
|
|
104
|
+
toArray(options.target),
|
|
105
|
+
allMessageKeys,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const includeMessages = toArray(options.includeMessages);
|
|
109
|
+
const excludeMessages = toArray(options.excludeMessages);
|
|
110
|
+
|
|
111
|
+
if (includeMessages.length > 0) {
|
|
112
|
+
messageKeys = messageKeys.filter((messageKey) => matchesPattern(messageKey, includeMessages));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (excludeMessages.length > 0) {
|
|
116
|
+
messageKeys = messageKeys.filter((messageKey) => !matchesPattern(messageKey, excludeMessages));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return messageKeys.filter((messageKey) => isAvailable(messages[messageKey]));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function collectMessageKeys(
|
|
123
|
+
projectConfig: ProjectConfig,
|
|
124
|
+
datasource: Datasource,
|
|
125
|
+
options: TypeScriptCodeGenerationOptions,
|
|
126
|
+
) {
|
|
127
|
+
const selectedSet = resolveSelectedSet(projectConfig, options.set);
|
|
128
|
+
const executions = await getProjectSetExecutions(projectConfig, datasource, selectedSet);
|
|
129
|
+
const messageKeys: string[] = [];
|
|
130
|
+
|
|
131
|
+
for (const execution of executions) {
|
|
132
|
+
messageKeys.push(...(await collectMessageKeysForDatasource(execution.datasource, options)));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return sortUnique(messageKeys);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function renderMessageKeyType(messageKeys: string[]) {
|
|
139
|
+
if (messageKeys.length === 0) {
|
|
140
|
+
return "export type MessagevisorMessageKey = never;\n";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return [
|
|
144
|
+
"export type MessagevisorMessageKey =",
|
|
145
|
+
...messageKeys.map((messageKey, index) => {
|
|
146
|
+
const suffix = index === messageKeys.length - 1 ? ";" : "";
|
|
147
|
+
|
|
148
|
+
return ` | ${JSON.stringify(messageKey)}${suffix}`;
|
|
149
|
+
}),
|
|
150
|
+
"",
|
|
151
|
+
].join("\n");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function renderSdkFile() {
|
|
155
|
+
return `import type {
|
|
156
|
+
TranslateOptions,
|
|
157
|
+
MessageFormatResult,
|
|
158
|
+
MessagePrimitiveValue,
|
|
159
|
+
MessageValues,
|
|
160
|
+
Messagevisor,
|
|
161
|
+
} from "@messagevisor/sdk";
|
|
162
|
+
import type { MessagevisorMessageKey } from "./messages";
|
|
163
|
+
|
|
164
|
+
let instance: Messagevisor | undefined;
|
|
165
|
+
|
|
166
|
+
export function setInstance(messagevisor: Messagevisor) {
|
|
167
|
+
instance = messagevisor;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function getInstance() {
|
|
171
|
+
if (!instance) {
|
|
172
|
+
throw new Error("Messagevisor instance is not set. Call setInstance(instance) first.");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return instance;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function translate(
|
|
179
|
+
messageKey: MessagevisorMessageKey,
|
|
180
|
+
values?: Record<string, MessagePrimitiveValue>,
|
|
181
|
+
options?: TranslateOptions,
|
|
182
|
+
): string;
|
|
183
|
+
export function translate<T>(
|
|
184
|
+
messageKey: MessagevisorMessageKey,
|
|
185
|
+
values: MessageValues<T>,
|
|
186
|
+
options?: TranslateOptions,
|
|
187
|
+
): MessageFormatResult<T>;
|
|
188
|
+
export function translate<T>(
|
|
189
|
+
messageKey: MessagevisorMessageKey,
|
|
190
|
+
values?: MessageValues<T>,
|
|
191
|
+
options?: TranslateOptions,
|
|
192
|
+
) {
|
|
193
|
+
return getInstance().translate(messageKey, values, options);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export const t = translate;
|
|
197
|
+
`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function renderReactFile() {
|
|
201
|
+
return `import type * as React from "react";
|
|
202
|
+
import type {
|
|
203
|
+
TranslateOptions,
|
|
204
|
+
MessageFormatResult,
|
|
205
|
+
MessagePrimitiveValue,
|
|
206
|
+
MessageValues,
|
|
207
|
+
} from "@messagevisor/sdk";
|
|
208
|
+
import {
|
|
209
|
+
useMessagevisor as useBaseMessagevisor,
|
|
210
|
+
useTranslation as useBaseTranslation,
|
|
211
|
+
} from "@messagevisor/react";
|
|
212
|
+
import type { MessagevisorMessageKey } from "./messages";
|
|
213
|
+
|
|
214
|
+
export function useTranslation(
|
|
215
|
+
messageKey: MessagevisorMessageKey,
|
|
216
|
+
values?: Record<string, MessagePrimitiveValue>,
|
|
217
|
+
options?: TranslateOptions,
|
|
218
|
+
): string;
|
|
219
|
+
export function useTranslation<T>(
|
|
220
|
+
messageKey: MessagevisorMessageKey,
|
|
221
|
+
values: MessageValues<T>,
|
|
222
|
+
options?: TranslateOptions,
|
|
223
|
+
): MessageFormatResult<T> | React.ReactNode;
|
|
224
|
+
export function useTranslation<T>(
|
|
225
|
+
messageKey: MessagevisorMessageKey,
|
|
226
|
+
values?: MessageValues<T>,
|
|
227
|
+
options?: TranslateOptions,
|
|
228
|
+
) {
|
|
229
|
+
return useBaseTranslation(messageKey, values as any, options);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function useMessagevisor() {
|
|
233
|
+
const messagevisor = useBaseMessagevisor();
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
...messagevisor,
|
|
237
|
+
t: messagevisor.t as typeof useTranslation,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function renderIndexFile(react?: boolean) {
|
|
244
|
+
return [
|
|
245
|
+
'export type * from "./messages";',
|
|
246
|
+
'export * from "./sdk";',
|
|
247
|
+
react ? 'export * from "./react";' : "",
|
|
248
|
+
"",
|
|
249
|
+
]
|
|
250
|
+
.filter((line) => line !== "")
|
|
251
|
+
.join("\n");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function writeGeneratedFile(outDir: string, fileName: string, content: string) {
|
|
255
|
+
const filePath = path.join(outDir, fileName);
|
|
256
|
+
|
|
257
|
+
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
258
|
+
await fs.promises.writeFile(filePath, content.endsWith("\n") ? content : `${content}\n`);
|
|
259
|
+
|
|
260
|
+
return filePath;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export async function generateTypeScriptCodeForProject(
|
|
264
|
+
projectConfig: ProjectConfig,
|
|
265
|
+
datasource: Datasource,
|
|
266
|
+
outDir: string,
|
|
267
|
+
options: TypeScriptCodeGenerationOptions = {},
|
|
268
|
+
): Promise<TypeScriptCodeGenerationResult> {
|
|
269
|
+
const messageKeys = await collectMessageKeys(projectConfig, datasource, options);
|
|
270
|
+
const files = [
|
|
271
|
+
await writeGeneratedFile(outDir, "messages.ts", renderMessageKeyType(messageKeys)),
|
|
272
|
+
await writeGeneratedFile(outDir, "sdk.ts", renderSdkFile()),
|
|
273
|
+
await writeGeneratedFile(outDir, "index.ts", renderIndexFile(options.react)),
|
|
274
|
+
];
|
|
275
|
+
|
|
276
|
+
if (options.react) {
|
|
277
|
+
files.push(await writeGeneratedFile(outDir, "react.ts", renderReactFile()));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
messageKeys,
|
|
282
|
+
files,
|
|
283
|
+
};
|
|
284
|
+
}
|