@messagevisor/sdk 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/conditions.d.ts +10 -0
- package/lib/conditions.js +163 -0
- package/lib/conditions.js.map +1 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +19 -0
- package/lib/index.js.map +1 -0
- package/lib/instance.d.ts +218 -0
- package/lib/instance.js +1000 -0
- package/lib/instance.js.map +1 -0
- package/package.json +42 -13
- package/src/conditions.spec.ts +373 -0
- package/src/conditions.ts +207 -0
- package/src/conformance.spec.ts +553 -0
- package/src/evaluation-edges.spec.ts +294 -0
- package/src/formatters.spec.ts +295 -0
- package/src/index.ts +2 -0
- package/src/instance.spec.ts +2493 -0
- package/src/instance.ts +1615 -0
- package/src/lifecycle.spec.ts +268 -0
- package/tsconfig.cjs.json +14 -0
- package/tsconfig.typecheck.json +4 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import type { DatafileContent } from "@messagevisor/types";
|
|
2
|
+
|
|
3
|
+
import { createMessagevisor } from "./index";
|
|
4
|
+
import type { MessagevisorModule } from "./instance";
|
|
5
|
+
|
|
6
|
+
const datafile: DatafileContent = {
|
|
7
|
+
schemaVersion: "1",
|
|
8
|
+
messagevisorVersion: "0.0.1",
|
|
9
|
+
revision: "1",
|
|
10
|
+
target: "web",
|
|
11
|
+
locale: "en-US",
|
|
12
|
+
direction: "ltr",
|
|
13
|
+
formats: {},
|
|
14
|
+
segments: {},
|
|
15
|
+
messages: {
|
|
16
|
+
hello: {},
|
|
17
|
+
},
|
|
18
|
+
translations: {
|
|
19
|
+
hello: "Hello",
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
describe("SDK lifecycle invariants", function () {
|
|
24
|
+
let consoleInfoSpy: jest.SpyInstance;
|
|
25
|
+
let consoleErrorSpy: jest.SpyInstance;
|
|
26
|
+
|
|
27
|
+
beforeEach(function () {
|
|
28
|
+
consoleInfoSpy = jest.spyOn(console, "info").mockImplementation(function () {});
|
|
29
|
+
consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(function () {});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(function () {
|
|
33
|
+
consoleInfoSpy.mockRestore();
|
|
34
|
+
consoleErrorSpy.mockRestore();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("deduplicates listener registrations and unsubscribe is idempotent", function () {
|
|
38
|
+
const m = createMessagevisor({ datafile, logLevel: "fatal" });
|
|
39
|
+
const events: string[] = [];
|
|
40
|
+
const callback = () => events.push("change");
|
|
41
|
+
|
|
42
|
+
const unsubscribeA = m.subscribe(callback);
|
|
43
|
+
const unsubscribeB = m.subscribe(callback);
|
|
44
|
+
|
|
45
|
+
m.setContext({ plan: "pro" });
|
|
46
|
+
unsubscribeA();
|
|
47
|
+
unsubscribeA();
|
|
48
|
+
m.setCurrency("EUR");
|
|
49
|
+
unsubscribeB();
|
|
50
|
+
m.setTimeZone("UTC");
|
|
51
|
+
|
|
52
|
+
expect(events).toEqual(["change"]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("emits error events only for error-level diagnostics", function () {
|
|
56
|
+
const diagnostics: any[] = [];
|
|
57
|
+
const errors: any[] = [];
|
|
58
|
+
const m = createMessagevisor({
|
|
59
|
+
datafile: {
|
|
60
|
+
...datafile,
|
|
61
|
+
messages: {
|
|
62
|
+
hello: { deprecated: true, deprecationWarning: "Use greeting." },
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
logLevel: "warn",
|
|
66
|
+
onDiagnostic: (diagnostic) => diagnostics.push(diagnostic),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
m.on("error", (event) => errors.push(event));
|
|
70
|
+
|
|
71
|
+
expect(m.translate("hello")).toEqual("Hello");
|
|
72
|
+
expect(errors).toEqual([]);
|
|
73
|
+
|
|
74
|
+
expect(m.translate("missing")).toEqual("missing");
|
|
75
|
+
expect(errors).toHaveLength(1);
|
|
76
|
+
expect(errors[0].diagnostic).toEqual(
|
|
77
|
+
expect.objectContaining({ code: "missing_translation", level: "error" }),
|
|
78
|
+
);
|
|
79
|
+
expect(diagnostics.map((diagnostic) => diagnostic.code)).toEqual([
|
|
80
|
+
"deprecated_message",
|
|
81
|
+
"missing_translation",
|
|
82
|
+
]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("ignores state mutations and module registration after close", async function () {
|
|
86
|
+
const calls: string[] = [];
|
|
87
|
+
const module: MessagevisorModule = {
|
|
88
|
+
name: "closer",
|
|
89
|
+
transform() {
|
|
90
|
+
calls.push("transform");
|
|
91
|
+
},
|
|
92
|
+
close() {
|
|
93
|
+
calls.push("close");
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
const m = createMessagevisor({ datafile, modules: [module], logLevel: "fatal" });
|
|
97
|
+
|
|
98
|
+
expect(m.translate("hello")).toEqual("Hello");
|
|
99
|
+
expect(calls).toEqual(["transform"]);
|
|
100
|
+
|
|
101
|
+
await m.close();
|
|
102
|
+
m.addModule({
|
|
103
|
+
name: "late",
|
|
104
|
+
transform() {
|
|
105
|
+
calls.push("late-transform");
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
m.removeModule("closer");
|
|
109
|
+
const unsubscribe = m.subscribe(() => calls.push("change"));
|
|
110
|
+
unsubscribe();
|
|
111
|
+
|
|
112
|
+
m.setContext({ plan: "pro" });
|
|
113
|
+
expect(m.translate("hello")).toEqual("Hello");
|
|
114
|
+
expect(calls).toEqual(["transform", "close"]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("does not mutate caller-provided context objects when setting or snapshotting context", function () {
|
|
118
|
+
const context = { account: { plan: "free" }, roles: ["viewer"] };
|
|
119
|
+
const m = createMessagevisor({ datafile, context, logLevel: "fatal" });
|
|
120
|
+
|
|
121
|
+
const firstContext = m.getContext();
|
|
122
|
+
firstContext.account = { plan: "pro" } as any;
|
|
123
|
+
expect(m.getContext()).toEqual(context);
|
|
124
|
+
|
|
125
|
+
const nextContext = { account: { plan: "enterprise" }, roles: ["admin"] };
|
|
126
|
+
m.setContext(nextContext);
|
|
127
|
+
const snapshot = m.getSnapshot();
|
|
128
|
+
snapshot.context.account = { plan: "mutated" } as any;
|
|
129
|
+
|
|
130
|
+
expect(m.getContext()).toEqual(nextContext);
|
|
131
|
+
expect(context).toEqual({ account: { plan: "free" }, roles: ["viewer"] });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("reports invalid object and string datafiles without emitting state events", function () {
|
|
135
|
+
const diagnostics: any[] = [];
|
|
136
|
+
const events: string[] = [];
|
|
137
|
+
const m = createMessagevisor({
|
|
138
|
+
logLevel: "error",
|
|
139
|
+
onDiagnostic: (diagnostic) => diagnostics.push(diagnostic),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
m.on("datafile_set", () => events.push("datafile"));
|
|
143
|
+
m.setDatafile({ schemaVersion: "1" } as any);
|
|
144
|
+
m.setDatafile("{not json");
|
|
145
|
+
|
|
146
|
+
expect(m.getLocale()).toEqual(null);
|
|
147
|
+
expect(events).toEqual([]);
|
|
148
|
+
expect(diagnostics).toEqual([
|
|
149
|
+
expect.objectContaining({ code: "invalid_datafile", level: "error" }),
|
|
150
|
+
expect.objectContaining({ code: "invalid_datafile", level: "error" }),
|
|
151
|
+
]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("does not switch the active locale when setDatafile receives another locale later", function () {
|
|
155
|
+
const m = createMessagevisor({ datafile, logLevel: "fatal" });
|
|
156
|
+
const events: any[] = [];
|
|
157
|
+
|
|
158
|
+
m.on("datafile_set", (event) => events.push(event));
|
|
159
|
+
m.setDatafile({
|
|
160
|
+
...datafile,
|
|
161
|
+
revision: "2",
|
|
162
|
+
locale: "nl-NL",
|
|
163
|
+
translations: { hello: "Hallo" },
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(m.getLocale()).toEqual("en-US");
|
|
167
|
+
expect(m.translate("hello")).toEqual("Hello");
|
|
168
|
+
expect(m.getDatafile("nl-NL").translations.hello).toEqual("Hallo");
|
|
169
|
+
expect(events[0]).toEqual(
|
|
170
|
+
expect.objectContaining({
|
|
171
|
+
type: "datafile_set",
|
|
172
|
+
locale: "en-US",
|
|
173
|
+
previousLocale: "en-US",
|
|
174
|
+
}),
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("emits ordered change events for repeated state updates even when values are unchanged", function () {
|
|
179
|
+
const m = createMessagevisor({ datafile, logLevel: "fatal" });
|
|
180
|
+
const changeEvents: any[] = [];
|
|
181
|
+
const detailedEvents: any[] = [];
|
|
182
|
+
|
|
183
|
+
m.on("change", (event) => changeEvents.push(event));
|
|
184
|
+
m.on("currency_set", (event) => detailedEvents.push(event));
|
|
185
|
+
m.on("timeZone_set", (event) => detailedEvents.push(event));
|
|
186
|
+
m.on("context_set", (event) => detailedEvents.push(event));
|
|
187
|
+
m.setCurrency("EUR");
|
|
188
|
+
m.setCurrency("EUR");
|
|
189
|
+
m.setTimeZone("UTC");
|
|
190
|
+
m.setContext({ plan: "pro" });
|
|
191
|
+
|
|
192
|
+
expect(changeEvents.map((event) => event.type)).toEqual([
|
|
193
|
+
"change",
|
|
194
|
+
"change",
|
|
195
|
+
"change",
|
|
196
|
+
"change",
|
|
197
|
+
]);
|
|
198
|
+
expect(changeEvents.map((event) => event.version)).toEqual([2, 3, 4, 5]);
|
|
199
|
+
expect(detailedEvents.map((event) => event.type)).toEqual([
|
|
200
|
+
"currency_set",
|
|
201
|
+
"currency_set",
|
|
202
|
+
"timeZone_set",
|
|
203
|
+
"context_set",
|
|
204
|
+
]);
|
|
205
|
+
expect(detailedEvents.map((event) => event.version)).toEqual([2, 3, 4, 5]);
|
|
206
|
+
expect(detailedEvents[1].previousCurrency).toEqual("EUR");
|
|
207
|
+
expect(detailedEvents[3].previousSnapshot.context).toEqual({});
|
|
208
|
+
expect(detailedEvents[3].snapshot.context).toEqual({ plan: "pro" });
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("keeps constructor locale usable with defaults before a datafile arrives", function () {
|
|
212
|
+
const m = createMessagevisor({
|
|
213
|
+
locale: "en-US",
|
|
214
|
+
defaultTranslations: {
|
|
215
|
+
"en-US": {
|
|
216
|
+
hello: "Fallback hello",
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
defaultFormats: {
|
|
220
|
+
"en-US": {
|
|
221
|
+
number: {
|
|
222
|
+
precise: { minimumFractionDigits: 2, maximumFractionDigits: 2 },
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
logLevel: "fatal",
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
expect(m.getLocale()).toEqual("en-US");
|
|
230
|
+
expect(m.getDefaultTranslations()).toEqual({ hello: "Fallback hello" });
|
|
231
|
+
expect(m.getDefaultFormats()).toEqual({
|
|
232
|
+
number: { precise: { minimumFractionDigits: 2, maximumFractionDigits: 2 } },
|
|
233
|
+
});
|
|
234
|
+
expect(m.translate("hello")).toEqual("Fallback hello");
|
|
235
|
+
expect(m.formatNumber(12, "precise")).toEqual("12.00");
|
|
236
|
+
expect(() => m.getDatafile()).toThrow("Datafile not found for locale: en-US");
|
|
237
|
+
expect(() => m.setLocale("nl-NL")).toThrow("Datafile not found for locale: nl-NL");
|
|
238
|
+
|
|
239
|
+
m.setDatafile({
|
|
240
|
+
...datafile,
|
|
241
|
+
formats: {
|
|
242
|
+
number: {
|
|
243
|
+
precise: { minimumFractionDigits: 0, maximumFractionDigits: 0 },
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
expect(m.getDatafile().locale).toEqual("en-US");
|
|
249
|
+
expect(m.translate("hello")).toEqual("Hello");
|
|
250
|
+
expect(m.formatNumber(12.34, "precise")).toEqual("12");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("documents that context accessors isolate only top-level properties", function () {
|
|
254
|
+
const m = createMessagevisor({
|
|
255
|
+
datafile,
|
|
256
|
+
context: { account: { plan: "free" }, flags: ["a"] },
|
|
257
|
+
logLevel: "fatal",
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const context = m.getContext();
|
|
261
|
+
context.account = { plan: "pro" } as any;
|
|
262
|
+
expect(m.getContext()).toEqual({ account: { plan: "free" }, flags: ["a"] });
|
|
263
|
+
|
|
264
|
+
const nested = m.getContext();
|
|
265
|
+
(nested.account as any).plan = "enterprise";
|
|
266
|
+
expect(m.getContext()).toEqual({ account: { plan: "enterprise" }, flags: ["a"] });
|
|
267
|
+
});
|
|
268
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.cjs.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "./lib",
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"moduleResolution": "node",
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"target": "es2018",
|
|
9
|
+
"lib": ["es2021", "es2021.intl", "dom"],
|
|
10
|
+
"skipLibCheck": true
|
|
11
|
+
},
|
|
12
|
+
"include": ["./src/**/*.ts"],
|
|
13
|
+
"exclude": ["./src/**/*.spec.ts"]
|
|
14
|
+
}
|