@messagevisor/catalog 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/LICENSE +21 -0
- package/README.md +7 -0
- package/dist/assets/index-CfGbXx4X.css +1 -0
- package/dist/assets/index-r8ugP5JL.js +73 -0
- package/dist/favicon.png +0 -0
- package/dist/index.html +14 -0
- package/dist/logo-text.png +0 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +18 -0
- package/lib/index.js.map +1 -0
- package/lib/node/formatExamplePreview.d.ts +10 -0
- package/lib/node/formatExamplePreview.js +79 -0
- package/lib/node/formatExamplePreview.js.map +1 -0
- package/lib/node/index.d.ts +191 -0
- package/lib/node/index.js +1645 -0
- package/lib/node/index.js.map +1 -0
- package/package.json +59 -13
- package/src/App.tsx +73 -0
- package/src/api.spec.ts +42 -0
- package/src/api.ts +87 -0
- package/src/catalogBrandAssets.ts +8 -0
- package/src/components/details/ConditionTree.tsx +146 -0
- package/src/components/details/FieldGrid.tsx +16 -0
- package/src/components/details/GroupSegmentTree.tsx +73 -0
- package/src/components/details/MarkdownContent.tsx +23 -0
- package/src/components/details/TranslationsTable.tsx +263 -0
- package/src/components/details/UsageLinks.tsx +29 -0
- package/src/components/history/HistoryTimeline.tsx +122 -0
- package/src/components/layout/AppShell.tsx +338 -0
- package/src/components/layout/PageHeader.tsx +13 -0
- package/src/components/layout/Tabs.tsx +35 -0
- package/src/components/lists/EntityList.tsx +451 -0
- package/src/components/ui/Badge.tsx +21 -0
- package/src/components/ui/Button.tsx +12 -0
- package/src/components/ui/Card.tsx +9 -0
- package/src/components/ui/CodeBlock.tsx +7 -0
- package/src/components/ui/EmptyState.tsx +8 -0
- package/src/components/ui/Input.tsx +12 -0
- package/src/components/ui/LabelValueBadge.tsx +55 -0
- package/src/config.ts +2 -0
- package/src/context/CatalogContext.tsx +50 -0
- package/src/entityTypes.ts +49 -0
- package/src/index.ts +1 -0
- package/src/main.tsx +28 -0
- package/src/node/formatExamplePreview.ts +85 -0
- package/src/node/index.spec.ts +713 -0
- package/src/node/index.ts +2007 -0
- package/src/pages/EntityDetailPage.tsx +3345 -0
- package/src/pages/HistoryPage.tsx +26 -0
- package/src/pages/HomePage.tsx +21 -0
- package/src/pages/ListPage.tsx +59 -0
- package/src/styles.css +95 -0
- package/src/theme.ts +36 -0
- package/src/types.ts +127 -0
- package/src/utils/formatCatalogTimestamp.ts +77 -0
- package/src/utils/hashTranslationValue.spec.ts +20 -0
- package/src/utils/hashTranslationValue.ts +22 -0
- package/src/utils/searchQuery.ts +46 -0
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import * as childProcess from "child_process";
|
|
5
|
+
|
|
6
|
+
import { mergeFormats, resolveFormats } from "../../../core/src/builder";
|
|
7
|
+
import { getProjectConfig } from "../../../core/src/config";
|
|
8
|
+
import { Datasource } from "../../../core/src/datasource";
|
|
9
|
+
import { resolveExamples } from "../../../core/src/examples";
|
|
10
|
+
import { findDuplicateTranslations } from "../../../core/src/find-duplicates";
|
|
11
|
+
import { getProjectSetExecutions } from "../../../core/src/sets";
|
|
12
|
+
import { createCatalogApi, createCatalogPlugin, type CatalogRuntime } from "./index";
|
|
13
|
+
|
|
14
|
+
const catalogApi = createCatalogApi({
|
|
15
|
+
mergeFormats,
|
|
16
|
+
resolveFormats,
|
|
17
|
+
getProjectSetExecutions,
|
|
18
|
+
resolveExamples,
|
|
19
|
+
findDuplicateTranslations,
|
|
20
|
+
});
|
|
21
|
+
const catalogRuntime: CatalogRuntime = {
|
|
22
|
+
mergeFormats,
|
|
23
|
+
resolveFormats,
|
|
24
|
+
getProjectSetExecutions,
|
|
25
|
+
resolveExamples,
|
|
26
|
+
findDuplicateTranslations,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
async function writeFile(root: string, relativePath: string, content: string) {
|
|
30
|
+
const filePath = path.join(root, relativePath);
|
|
31
|
+
|
|
32
|
+
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
33
|
+
await fs.promises.writeFile(filePath, content);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function readJson<T>(root: string, relativePath: string): Promise<T> {
|
|
37
|
+
return JSON.parse(await fs.promises.readFile(path.join(root, relativePath), "utf8"));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function createProject() {
|
|
41
|
+
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-catalog-"));
|
|
42
|
+
const interpolationModulePath = path.join(
|
|
43
|
+
path.resolve(__dirname, "../../../.."),
|
|
44
|
+
"packages/module-interpolation/src/index.ts",
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
await writeFile(
|
|
48
|
+
root,
|
|
49
|
+
"messagevisor.config.js",
|
|
50
|
+
[
|
|
51
|
+
`const { createInterpolationModule } = require(${JSON.stringify(interpolationModulePath)});`,
|
|
52
|
+
"module.exports = {",
|
|
53
|
+
" modules: [createInterpolationModule()],",
|
|
54
|
+
"};",
|
|
55
|
+
"",
|
|
56
|
+
].join("\n"),
|
|
57
|
+
);
|
|
58
|
+
await writeFile(
|
|
59
|
+
root,
|
|
60
|
+
"locales/en.yml",
|
|
61
|
+
[
|
|
62
|
+
"description: English",
|
|
63
|
+
"direction: ltr",
|
|
64
|
+
"formats:",
|
|
65
|
+
" number:",
|
|
66
|
+
" money:",
|
|
67
|
+
" style: currency",
|
|
68
|
+
" currency: USD",
|
|
69
|
+
"examples:",
|
|
70
|
+
" - description: Simple text",
|
|
71
|
+
" rawMessage: Hello, world!",
|
|
72
|
+
" - description: Welcome on pro plan",
|
|
73
|
+
" message: common.welcome",
|
|
74
|
+
" context:",
|
|
75
|
+
" plan: pro",
|
|
76
|
+
"",
|
|
77
|
+
].join("\n"),
|
|
78
|
+
);
|
|
79
|
+
await writeFile(
|
|
80
|
+
root,
|
|
81
|
+
"locales/en-US.yml",
|
|
82
|
+
[
|
|
83
|
+
"description: English US",
|
|
84
|
+
"direction: ltr",
|
|
85
|
+
"promotable: false",
|
|
86
|
+
"inheritFormatsFrom: en",
|
|
87
|
+
"inheritTranslationsFrom: en",
|
|
88
|
+
"mergeExamplesFrom: en",
|
|
89
|
+
"formats:",
|
|
90
|
+
" number:",
|
|
91
|
+
" money:",
|
|
92
|
+
" currencyDisplay: code",
|
|
93
|
+
"examples:",
|
|
94
|
+
" - matrix:",
|
|
95
|
+
" name: [Taylor, Sam]",
|
|
96
|
+
" description: Welcome ${{ name }}",
|
|
97
|
+
" rawMessage: Hello, {name}!",
|
|
98
|
+
" values:",
|
|
99
|
+
" name: ${{ name }}",
|
|
100
|
+
"",
|
|
101
|
+
].join("\n"),
|
|
102
|
+
);
|
|
103
|
+
await writeFile(root, "locales/nl.yml", "description: Dutch\n");
|
|
104
|
+
await writeFile(root, "attributes/plan.yml", "description: Plan\ntype: string\n");
|
|
105
|
+
await writeFile(
|
|
106
|
+
root,
|
|
107
|
+
"segments/pro.yml",
|
|
108
|
+
"description: Pro\nconditions:\n attribute: plan\n operator: equals\n value: pro\n",
|
|
109
|
+
);
|
|
110
|
+
await writeFile(
|
|
111
|
+
root,
|
|
112
|
+
"messages/common/welcome.yml",
|
|
113
|
+
[
|
|
114
|
+
"description: Welcome",
|
|
115
|
+
"promotable: false",
|
|
116
|
+
"examples:",
|
|
117
|
+
" - description: Default welcome",
|
|
118
|
+
" locale: en",
|
|
119
|
+
" - matrix:",
|
|
120
|
+
" locale: [en, en-US]",
|
|
121
|
+
" plan: [free, pro]",
|
|
122
|
+
" description: Welcome for ${{ locale }} plan ${{ plan }}",
|
|
123
|
+
" locale: ${{ locale }}",
|
|
124
|
+
" context:",
|
|
125
|
+
" plan: ${{ plan }}",
|
|
126
|
+
"translations:",
|
|
127
|
+
" en: Welcome",
|
|
128
|
+
"overrides:",
|
|
129
|
+
" - key: pro",
|
|
130
|
+
" segments: pro",
|
|
131
|
+
" translations:",
|
|
132
|
+
" en: Welcome Pro",
|
|
133
|
+
"",
|
|
134
|
+
].join("\n"),
|
|
135
|
+
);
|
|
136
|
+
await writeFile(
|
|
137
|
+
root,
|
|
138
|
+
"messages/common/draft.yml",
|
|
139
|
+
"description: Draft\ntranslations:\n en: Welcome\n",
|
|
140
|
+
);
|
|
141
|
+
await writeFile(
|
|
142
|
+
root,
|
|
143
|
+
"targets/web.yml",
|
|
144
|
+
[
|
|
145
|
+
"includeMessages:",
|
|
146
|
+
" - common*",
|
|
147
|
+
"locales:",
|
|
148
|
+
" - en-US",
|
|
149
|
+
"formats:",
|
|
150
|
+
" en-US:",
|
|
151
|
+
" number:",
|
|
152
|
+
" money:",
|
|
153
|
+
" minimumFractionDigits: 2",
|
|
154
|
+
"",
|
|
155
|
+
].join("\n"),
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
return root;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function git(root: string, args: string[]) {
|
|
162
|
+
childProcess.execFileSync("git", ["-C", root, ...args], {
|
|
163
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
describe("catalog", function () {
|
|
168
|
+
const roots: string[] = [];
|
|
169
|
+
let consoleLogSpy: jest.SpyInstance;
|
|
170
|
+
|
|
171
|
+
beforeEach(function () {
|
|
172
|
+
consoleLogSpy = jest.spyOn(console, "log").mockImplementation(function () {
|
|
173
|
+
// Keep catalog unit tests focused on generated data.
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
afterEach(async function () {
|
|
178
|
+
consoleLogSpy.mockRestore();
|
|
179
|
+
|
|
180
|
+
for (const root of roots) {
|
|
181
|
+
await fs.promises.rm(root, { recursive: true, force: true });
|
|
182
|
+
}
|
|
183
|
+
roots.length = 0;
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("exports regular project catalog data with relationships, status, and computed formats", async function () {
|
|
187
|
+
const root = await createProject();
|
|
188
|
+
roots.push(root);
|
|
189
|
+
const projectConfig = getProjectConfig(root);
|
|
190
|
+
const datasource = new Datasource(projectConfig, root);
|
|
191
|
+
|
|
192
|
+
const result = await catalogApi.exportCatalog(root, projectConfig, datasource, {
|
|
193
|
+
outDir: "catalog-out",
|
|
194
|
+
copyAssets: false,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
expect(result.outputDirectoryPath).toBe(path.join(root, "catalog-out"));
|
|
198
|
+
|
|
199
|
+
const manifest = await readJson<any>(root, "catalog-out/data/manifest.json");
|
|
200
|
+
const index = await readJson<any>(root, "catalog-out/data/root/index.json");
|
|
201
|
+
const locale = await readJson<any>(root, "catalog-out/data/root/entities/locale/en-US.json");
|
|
202
|
+
const localeDuplicates = await readJson<any>(
|
|
203
|
+
root,
|
|
204
|
+
"catalog-out/data/root/duplicates/locales/en-US.json",
|
|
205
|
+
);
|
|
206
|
+
const emptyLocaleDuplicates = await readJson<any>(
|
|
207
|
+
root,
|
|
208
|
+
"catalog-out/data/root/duplicates/locales/nl.json",
|
|
209
|
+
);
|
|
210
|
+
const message = await readJson<any>(
|
|
211
|
+
root,
|
|
212
|
+
"catalog-out/data/root/entities/message/common.welcome.json",
|
|
213
|
+
);
|
|
214
|
+
const attribute = await readJson<any>(
|
|
215
|
+
root,
|
|
216
|
+
"catalog-out/data/root/entities/attribute/plan.json",
|
|
217
|
+
);
|
|
218
|
+
const segment = await readJson<any>(root, "catalog-out/data/root/entities/segment/pro.json");
|
|
219
|
+
const target = await readJson<any>(root, "catalog-out/data/root/entities/target/web.json");
|
|
220
|
+
const history = await readJson<any>(root, "catalog-out/data/project/history/page-1.json");
|
|
221
|
+
|
|
222
|
+
expect(manifest.sets).toBe(false);
|
|
223
|
+
expect(manifest.router).toBe("browser");
|
|
224
|
+
expect(manifest.dev).toBeUndefined();
|
|
225
|
+
expect(manifest.paths.root).toBe("data/root/index.json");
|
|
226
|
+
expect(index.counts.message).toBe(2);
|
|
227
|
+
expect(
|
|
228
|
+
index.entities.message.find((entry: any) => entry.key === "common.welcome").targets,
|
|
229
|
+
).toEqual(["web"]);
|
|
230
|
+
expect(index.entities.locale.find((entry: any) => entry.key === "en-US").targets).toEqual([
|
|
231
|
+
"web",
|
|
232
|
+
]);
|
|
233
|
+
expect(index.entities.attribute.find((entry: any) => entry.key === "plan").targets).toEqual([
|
|
234
|
+
"web",
|
|
235
|
+
]);
|
|
236
|
+
expect(index.entities.segment.find((entry: any) => entry.key === "pro").targets).toEqual([
|
|
237
|
+
"web",
|
|
238
|
+
]);
|
|
239
|
+
expect(index.entities.target.find((entry: any) => entry.key === "web").messageCount).toBe(2);
|
|
240
|
+
expect(locale.computedFormats.number.money).toEqual({
|
|
241
|
+
style: "currency",
|
|
242
|
+
currency: "USD",
|
|
243
|
+
currencyDisplay: "code",
|
|
244
|
+
});
|
|
245
|
+
expect(locale.entity.examples).toHaveLength(1);
|
|
246
|
+
expect(locale.entity.direction).toBe("ltr");
|
|
247
|
+
expect(locale.entity.promotable).toBe(false);
|
|
248
|
+
expect(locale.formatRows).toEqual(
|
|
249
|
+
expect.arrayContaining([
|
|
250
|
+
expect.objectContaining({
|
|
251
|
+
path: "number.money.style",
|
|
252
|
+
value: "currency",
|
|
253
|
+
source: "inherited",
|
|
254
|
+
from: "en",
|
|
255
|
+
examplePreview: expect.any(String),
|
|
256
|
+
}),
|
|
257
|
+
expect.objectContaining({
|
|
258
|
+
path: "number.money.currencyDisplay",
|
|
259
|
+
value: "code",
|
|
260
|
+
source: "direct",
|
|
261
|
+
examplePreview: expect.any(String),
|
|
262
|
+
}),
|
|
263
|
+
]),
|
|
264
|
+
);
|
|
265
|
+
expect(locale.evaluatedExamples).toEqual(
|
|
266
|
+
expect.arrayContaining([
|
|
267
|
+
expect.objectContaining({
|
|
268
|
+
locale: "en-US",
|
|
269
|
+
sourceLocale: "en",
|
|
270
|
+
description: "Simple text",
|
|
271
|
+
rawMessage: "Hello, world!",
|
|
272
|
+
evaluatedTranslation: "Hello, world!",
|
|
273
|
+
}),
|
|
274
|
+
expect.objectContaining({
|
|
275
|
+
locale: "en-US",
|
|
276
|
+
sourceLocale: "en",
|
|
277
|
+
description: "Welcome on pro plan",
|
|
278
|
+
message: "common.welcome",
|
|
279
|
+
originalTranslation: "Welcome",
|
|
280
|
+
evaluatedTranslation: "Welcome Pro",
|
|
281
|
+
}),
|
|
282
|
+
expect.objectContaining({
|
|
283
|
+
locale: "en-US",
|
|
284
|
+
sourceLocale: "en-US",
|
|
285
|
+
description: "Welcome Taylor",
|
|
286
|
+
rawMessage: "Hello, {name}!",
|
|
287
|
+
evaluatedTranslation: "Hello, Taylor!",
|
|
288
|
+
}),
|
|
289
|
+
]),
|
|
290
|
+
);
|
|
291
|
+
expect(locale.targetFormats.web.number.money.minimumFractionDigits).toBe(2);
|
|
292
|
+
expect(localeDuplicates).toEqual({
|
|
293
|
+
locale: "en-US",
|
|
294
|
+
summary: {
|
|
295
|
+
duplicateValues: 1,
|
|
296
|
+
duplicateMessageKeys: 2,
|
|
297
|
+
},
|
|
298
|
+
duplicateValues: [
|
|
299
|
+
{
|
|
300
|
+
value: "Welcome",
|
|
301
|
+
messageKeys: ["common.draft", "common.welcome"],
|
|
302
|
+
sources: [
|
|
303
|
+
{ messageKey: "common.draft", locale: "en" },
|
|
304
|
+
{ messageKey: "common.welcome", locale: "en" },
|
|
305
|
+
],
|
|
306
|
+
},
|
|
307
|
+
],
|
|
308
|
+
});
|
|
309
|
+
expect(emptyLocaleDuplicates).toEqual({
|
|
310
|
+
locale: "nl",
|
|
311
|
+
summary: {
|
|
312
|
+
duplicateValues: 0,
|
|
313
|
+
duplicateMessageKeys: 0,
|
|
314
|
+
},
|
|
315
|
+
duplicateValues: [],
|
|
316
|
+
});
|
|
317
|
+
expect(target.formatRowsByLocale["en-US"]).toEqual(
|
|
318
|
+
expect.arrayContaining([
|
|
319
|
+
expect.objectContaining({
|
|
320
|
+
path: "number.money.minimumFractionDigits",
|
|
321
|
+
value: 2,
|
|
322
|
+
source: "target",
|
|
323
|
+
from: "target",
|
|
324
|
+
examplePreview: expect.any(String),
|
|
325
|
+
}),
|
|
326
|
+
expect.objectContaining({
|
|
327
|
+
path: "number.money.style",
|
|
328
|
+
value: "currency",
|
|
329
|
+
source: "inherited",
|
|
330
|
+
from: "en",
|
|
331
|
+
examplePreview: expect.any(String),
|
|
332
|
+
}),
|
|
333
|
+
]),
|
|
334
|
+
);
|
|
335
|
+
expect(message.targets).toEqual(["web"]);
|
|
336
|
+
expect(message.editLinks).toBeUndefined();
|
|
337
|
+
expect(message.entity.promotable).toBe(false);
|
|
338
|
+
expect(message.entity.examples).toHaveLength(2);
|
|
339
|
+
expect(message.translations).toEqual(
|
|
340
|
+
expect.arrayContaining([
|
|
341
|
+
{
|
|
342
|
+
locale: "en-US",
|
|
343
|
+
value: "Welcome",
|
|
344
|
+
source: "inherited",
|
|
345
|
+
from: "en",
|
|
346
|
+
},
|
|
347
|
+
]),
|
|
348
|
+
);
|
|
349
|
+
expect(message.localeDirections).toEqual({
|
|
350
|
+
en: "ltr",
|
|
351
|
+
"en-US": "ltr",
|
|
352
|
+
});
|
|
353
|
+
expect(message.evaluatedExamples).toEqual(
|
|
354
|
+
expect.arrayContaining([
|
|
355
|
+
expect.objectContaining({
|
|
356
|
+
locale: "en",
|
|
357
|
+
description: "Default welcome",
|
|
358
|
+
evaluatedTranslation: "Welcome",
|
|
359
|
+
}),
|
|
360
|
+
expect.objectContaining({
|
|
361
|
+
locale: "en-US",
|
|
362
|
+
description: "Welcome for en-US plan pro",
|
|
363
|
+
evaluatedTranslation: "Welcome Pro",
|
|
364
|
+
}),
|
|
365
|
+
]),
|
|
366
|
+
);
|
|
367
|
+
expect(message.entity.overrides[0].usedSegments).toEqual(["pro"]);
|
|
368
|
+
expect(attribute.usage.segments).toEqual(["pro"]);
|
|
369
|
+
expect(segment.usage.messages).toEqual(["common.welcome"]);
|
|
370
|
+
expect(target.messages).toEqual(["common.draft", "common.welcome"]);
|
|
371
|
+
expect(history.entries).toEqual([]);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("exports branch-aware repository links and hash router mode when requested", async function () {
|
|
375
|
+
const root = await createProject();
|
|
376
|
+
roots.push(root);
|
|
377
|
+
git(root, ["init"]);
|
|
378
|
+
git(root, ["checkout", "-b", "catalog-test"]);
|
|
379
|
+
git(root, ["remote", "add", "origin", "git@github.com:messagevisor/messagevisor.git"]);
|
|
380
|
+
const projectConfig = getProjectConfig(root);
|
|
381
|
+
const datasource = new Datasource(projectConfig, root);
|
|
382
|
+
|
|
383
|
+
await catalogApi.exportCatalog(root, projectConfig, datasource, {
|
|
384
|
+
outDir: "catalog-out",
|
|
385
|
+
copyAssets: false,
|
|
386
|
+
browserRouter: false,
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const manifest = await readJson<any>(root, "catalog-out/data/manifest.json");
|
|
390
|
+
|
|
391
|
+
expect(manifest.router).toBe("hash");
|
|
392
|
+
expect(manifest.links).toMatchObject({
|
|
393
|
+
provider: "github",
|
|
394
|
+
repository: "https://github.com/messagevisor/messagevisor",
|
|
395
|
+
source: "https://github.com/messagevisor/messagevisor/blob/catalog-test/{{path}}",
|
|
396
|
+
commit: "https://github.com/messagevisor/messagevisor/commit/{{hash}}",
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("exports GitLab and Bitbucket repository links for known providers", async function () {
|
|
401
|
+
const providers = [
|
|
402
|
+
{
|
|
403
|
+
remote: "git@gitlab.com:messagevisor/messagevisor.git",
|
|
404
|
+
expected: {
|
|
405
|
+
provider: "gitlab",
|
|
406
|
+
repository: "https://gitlab.com/messagevisor/messagevisor",
|
|
407
|
+
source: "https://gitlab.com/messagevisor/messagevisor/-/blob/catalog-test/{{path}}",
|
|
408
|
+
commit: "https://gitlab.com/messagevisor/messagevisor/-/commit/{{hash}}",
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
remote: "git@bitbucket.org:messagevisor/messagevisor.git",
|
|
413
|
+
expected: {
|
|
414
|
+
provider: "bitbucket",
|
|
415
|
+
repository: "https://bitbucket.org/messagevisor/messagevisor",
|
|
416
|
+
source: "https://bitbucket.org/messagevisor/messagevisor/src/catalog-test/{{path}}",
|
|
417
|
+
commit: "https://bitbucket.org/messagevisor/messagevisor/commits/{{hash}}",
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
];
|
|
421
|
+
|
|
422
|
+
for (const provider of providers) {
|
|
423
|
+
const root = await createProject();
|
|
424
|
+
roots.push(root);
|
|
425
|
+
git(root, ["init"]);
|
|
426
|
+
git(root, ["checkout", "-b", "catalog-test"]);
|
|
427
|
+
git(root, ["remote", "add", "origin", provider.remote]);
|
|
428
|
+
const projectConfig = getProjectConfig(root);
|
|
429
|
+
const datasource = new Datasource(projectConfig, root);
|
|
430
|
+
|
|
431
|
+
await catalogApi.exportCatalog(root, projectConfig, datasource, {
|
|
432
|
+
outDir: "catalog-out",
|
|
433
|
+
copyAssets: false,
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
const manifest = await readJson<any>(root, "catalog-out/data/manifest.json");
|
|
437
|
+
|
|
438
|
+
expect(manifest.links).toMatchObject(provider.expected);
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it("omits repository links for unknown Git providers", async function () {
|
|
443
|
+
const root = await createProject();
|
|
444
|
+
roots.push(root);
|
|
445
|
+
git(root, ["init"]);
|
|
446
|
+
git(root, ["checkout", "-b", "catalog-test"]);
|
|
447
|
+
git(root, ["remote", "add", "origin", "git@example.com:messagevisor/messagevisor.git"]);
|
|
448
|
+
const projectConfig = getProjectConfig(root);
|
|
449
|
+
const datasource = new Datasource(projectConfig, root);
|
|
450
|
+
|
|
451
|
+
await catalogApi.exportCatalog(root, projectConfig, datasource, {
|
|
452
|
+
outDir: "catalog-out",
|
|
453
|
+
copyAssets: false,
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
const manifest = await readJson<any>(root, "catalog-out/data/manifest.json");
|
|
457
|
+
|
|
458
|
+
expect(manifest.links).toBeUndefined();
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it("exports dev-only editor metadata and entity editor links when requested", async function () {
|
|
462
|
+
const root = await createProject();
|
|
463
|
+
roots.push(root);
|
|
464
|
+
const projectConfig = getProjectConfig(root);
|
|
465
|
+
const datasource = new Datasource(projectConfig, root);
|
|
466
|
+
|
|
467
|
+
await catalogApi.exportCatalog(root, projectConfig, datasource, {
|
|
468
|
+
outDir: "catalog-out",
|
|
469
|
+
copyAssets: false,
|
|
470
|
+
dev: true,
|
|
471
|
+
devEditors: [{ id: "cursor", label: "Cursor", icon: "cursor" }],
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
const manifest = await readJson<any>(root, "catalog-out/data/manifest.json");
|
|
475
|
+
const message = await readJson<any>(
|
|
476
|
+
root,
|
|
477
|
+
"catalog-out/data/root/entities/message/common.welcome.json",
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
expect(manifest.links).toBeUndefined();
|
|
481
|
+
expect(manifest.dev).toEqual({
|
|
482
|
+
editors: [{ id: "cursor", label: "Cursor", icon: "cursor" }],
|
|
483
|
+
});
|
|
484
|
+
expect(message.editLinks).toEqual({
|
|
485
|
+
cursor: expect.stringMatching(/^cursor:\/\/file\/.+messages\/common\/welcome\.yml$/),
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it("exports repo-relative source paths for nested projects", async function () {
|
|
490
|
+
const repositoryRoot = await fs.promises.mkdtemp(
|
|
491
|
+
path.join(os.tmpdir(), "messagevisor-catalog-repo-"),
|
|
492
|
+
);
|
|
493
|
+
roots.push(repositoryRoot);
|
|
494
|
+
const projectRoot = path.join(repositoryRoot, "projects", "shop");
|
|
495
|
+
|
|
496
|
+
await fs.promises.mkdir(projectRoot, { recursive: true });
|
|
497
|
+
await writeFile(projectRoot, "messagevisor.config.js", "module.exports = {};\n");
|
|
498
|
+
await writeFile(projectRoot, "locales/en.yml", "description: English\n");
|
|
499
|
+
await writeFile(
|
|
500
|
+
projectRoot,
|
|
501
|
+
"messages/common/welcome.yml",
|
|
502
|
+
"description: Welcome\ntranslations:\n en: Welcome\n",
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
git(repositoryRoot, ["init"]);
|
|
506
|
+
git(repositoryRoot, ["checkout", "-b", "nested-catalog-test"]);
|
|
507
|
+
git(repositoryRoot, [
|
|
508
|
+
"remote",
|
|
509
|
+
"add",
|
|
510
|
+
"origin",
|
|
511
|
+
"git@github.com:messagevisor/messagevisor.git",
|
|
512
|
+
]);
|
|
513
|
+
|
|
514
|
+
const projectConfig = getProjectConfig(projectRoot);
|
|
515
|
+
const datasource = new Datasource(projectConfig, projectRoot);
|
|
516
|
+
|
|
517
|
+
await catalogApi.exportCatalog(projectRoot, projectConfig, datasource, {
|
|
518
|
+
outDir: "catalog-out",
|
|
519
|
+
copyAssets: false,
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
const message = await readJson<any>(
|
|
523
|
+
projectRoot,
|
|
524
|
+
"catalog-out/data/root/entities/message/common.welcome.json",
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
expect(message.sourcePath).toBe("projects/shop/messages/common/welcome.yml");
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it("exports set project catalog data independently for each set", async function () {
|
|
531
|
+
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-catalog-"));
|
|
532
|
+
roots.push(root);
|
|
533
|
+
|
|
534
|
+
await writeFile(root, "messagevisor.config.js", "module.exports = { sets: true };\n");
|
|
535
|
+
|
|
536
|
+
for (const set of ["storefront", "admin"]) {
|
|
537
|
+
await writeFile(root, `sets/${set}/locales/en.yml`, "description: English\n");
|
|
538
|
+
await writeFile(
|
|
539
|
+
root,
|
|
540
|
+
`sets/${set}/messages/common/welcome.yml`,
|
|
541
|
+
`description: Welcome\ntranslations:\n en: ${set}\n`,
|
|
542
|
+
);
|
|
543
|
+
await writeFile(
|
|
544
|
+
root,
|
|
545
|
+
`sets/${set}/messages/common/duplicate.yml`,
|
|
546
|
+
`description: Duplicate\ntranslations:\n en: ${set}\n`,
|
|
547
|
+
);
|
|
548
|
+
await writeFile(
|
|
549
|
+
root,
|
|
550
|
+
`sets/${set}/targets/web.yml`,
|
|
551
|
+
"includeMessages:\n - common*\nlocales:\n - en\n",
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const projectConfig = getProjectConfig(root);
|
|
556
|
+
const datasource = new Datasource(projectConfig, root);
|
|
557
|
+
|
|
558
|
+
await catalogApi.exportCatalog(root, projectConfig, datasource, {
|
|
559
|
+
outDir: "catalog-out",
|
|
560
|
+
copyAssets: false,
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
const manifest = await readJson<any>(root, "catalog-out/data/manifest.json");
|
|
564
|
+
const storefront = await readJson<any>(root, "catalog-out/data/sets/storefront/index.json");
|
|
565
|
+
const admin = await readJson<any>(root, "catalog-out/data/sets/admin/index.json");
|
|
566
|
+
|
|
567
|
+
expect(manifest.sets).toBe(true);
|
|
568
|
+
expect(manifest.setKeys).toEqual(["admin", "storefront"]);
|
|
569
|
+
const storefrontDuplicates = await readJson<any>(
|
|
570
|
+
root,
|
|
571
|
+
"catalog-out/data/sets/storefront/duplicates/locales/en.json",
|
|
572
|
+
);
|
|
573
|
+
const adminDuplicates = await readJson<any>(
|
|
574
|
+
root,
|
|
575
|
+
"catalog-out/data/sets/admin/duplicates/locales/en.json",
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
expect(storefront.counts.message).toBe(2);
|
|
579
|
+
expect(admin.counts.message).toBe(2);
|
|
580
|
+
expect(storefrontDuplicates.duplicateValues).toEqual([
|
|
581
|
+
{
|
|
582
|
+
value: "storefront",
|
|
583
|
+
messageKeys: ["common.duplicate", "common.welcome"],
|
|
584
|
+
sources: [
|
|
585
|
+
{ messageKey: "common.duplicate", locale: "en" },
|
|
586
|
+
{ messageKey: "common.welcome", locale: "en" },
|
|
587
|
+
],
|
|
588
|
+
},
|
|
589
|
+
]);
|
|
590
|
+
expect(adminDuplicates.duplicateValues).toEqual([
|
|
591
|
+
{
|
|
592
|
+
value: "admin",
|
|
593
|
+
messageKeys: ["common.duplicate", "common.welcome"],
|
|
594
|
+
sources: [
|
|
595
|
+
{ messageKey: "common.duplicate", locale: "en" },
|
|
596
|
+
{ messageKey: "common.welcome", locale: "en" },
|
|
597
|
+
],
|
|
598
|
+
},
|
|
599
|
+
]);
|
|
600
|
+
await expect(
|
|
601
|
+
readJson<any>(root, "catalog-out/data/sets/storefront/entities/message/common.welcome.json"),
|
|
602
|
+
).resolves.toMatchObject({
|
|
603
|
+
key: "common.welcome",
|
|
604
|
+
entity: { translations: { en: "storefront" } },
|
|
605
|
+
});
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it("uses root-relative asset paths for browser-router refresh safety", async function () {
|
|
609
|
+
const viteConfigSource = await fs.promises.readFile(
|
|
610
|
+
path.join(__dirname, "../../vite.config.ts"),
|
|
611
|
+
"utf8",
|
|
612
|
+
);
|
|
613
|
+
|
|
614
|
+
expect(viteConfigSource).toContain('base: "/"');
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
describe("catalog plugin", function () {
|
|
619
|
+
let exportMock: jest.Mock;
|
|
620
|
+
let serveMock: jest.Mock;
|
|
621
|
+
|
|
622
|
+
beforeEach(function () {
|
|
623
|
+
jest.useFakeTimers();
|
|
624
|
+
exportMock = jest.fn().mockResolvedValue({
|
|
625
|
+
outputDirectoryPath: "/tmp/catalog",
|
|
626
|
+
manifest: {},
|
|
627
|
+
});
|
|
628
|
+
serveMock = jest.fn().mockResolvedValue({
|
|
629
|
+
close: jest.fn(),
|
|
630
|
+
triggerReload: jest.fn(),
|
|
631
|
+
});
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
afterEach(function () {
|
|
635
|
+
jest.clearAllTimers();
|
|
636
|
+
jest.useRealTimers();
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
function createPlugin() {
|
|
640
|
+
const plugin = createCatalogPlugin(catalogRuntime, {
|
|
641
|
+
exportCatalog: exportMock,
|
|
642
|
+
serveCatalog: serveMock,
|
|
643
|
+
});
|
|
644
|
+
const rootDirectoryPath = "/tmp/messagevisor-project";
|
|
645
|
+
const projectConfig = {
|
|
646
|
+
catalogDirectoryPath: path.join(rootDirectoryPath, "catalog"),
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
return {
|
|
650
|
+
plugin,
|
|
651
|
+
handler: (parsed: Record<string, unknown>) =>
|
|
652
|
+
plugin.handler({
|
|
653
|
+
rootDirectoryPath,
|
|
654
|
+
projectConfig,
|
|
655
|
+
datasource: {},
|
|
656
|
+
parsed: parsed as any,
|
|
657
|
+
}),
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
it("forwards long and short port options for dev catalog mode", async function () {
|
|
662
|
+
const { handler } = createPlugin();
|
|
663
|
+
|
|
664
|
+
await handler({ _: ["catalog"], port: 3101 });
|
|
665
|
+
expect(serveMock).toHaveBeenLastCalledWith(
|
|
666
|
+
expect.any(String),
|
|
667
|
+
expect.any(Object),
|
|
668
|
+
expect.any(Object),
|
|
669
|
+
expect.objectContaining({ port: 3101, liveReload: true }),
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
await handler({ _: ["catalog"], p: 3102 });
|
|
673
|
+
expect(serveMock).toHaveBeenLastCalledWith(
|
|
674
|
+
expect.any(String),
|
|
675
|
+
expect.any(Object),
|
|
676
|
+
expect.any(Object),
|
|
677
|
+
expect.objectContaining({ port: 3102, liveReload: true }),
|
|
678
|
+
);
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
it("forwards long and short port options for serve subcommand", async function () {
|
|
682
|
+
const { handler } = createPlugin();
|
|
683
|
+
|
|
684
|
+
await handler({ _: ["catalog", "serve"], subcommand: "serve", port: 3103 });
|
|
685
|
+
expect(serveMock).toHaveBeenLastCalledWith(
|
|
686
|
+
expect.any(String),
|
|
687
|
+
expect.any(Object),
|
|
688
|
+
expect.any(Object),
|
|
689
|
+
expect.objectContaining({ port: 3103 }),
|
|
690
|
+
);
|
|
691
|
+
|
|
692
|
+
await handler({ _: ["catalog", "serve"], subcommand: "serve", p: 3104 });
|
|
693
|
+
expect(serveMock).toHaveBeenLastCalledWith(
|
|
694
|
+
expect.any(String),
|
|
695
|
+
expect.any(Object),
|
|
696
|
+
expect.any(Object),
|
|
697
|
+
expect.objectContaining({ port: 3104 }),
|
|
698
|
+
);
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
it("lets serveCatalog apply its default port when no port option is provided", async function () {
|
|
702
|
+
const { handler } = createPlugin();
|
|
703
|
+
|
|
704
|
+
await handler({ _: ["catalog", "serve"], subcommand: "serve" });
|
|
705
|
+
|
|
706
|
+
expect(serveMock).toHaveBeenLastCalledWith(
|
|
707
|
+
expect.any(String),
|
|
708
|
+
expect.any(Object),
|
|
709
|
+
expect.any(Object),
|
|
710
|
+
expect.objectContaining({ port: undefined }),
|
|
711
|
+
);
|
|
712
|
+
});
|
|
713
|
+
});
|