fimo-vite 2.4.0-staging.2 → 2.4.0-staging.4
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.
|
@@ -5,7 +5,19 @@ interface LabelBundle {
|
|
|
5
5
|
locales: string[];
|
|
6
6
|
updatedAt: string | null;
|
|
7
7
|
}
|
|
8
|
+
interface LabelBundleRead {
|
|
9
|
+
bundle: LabelBundle;
|
|
10
|
+
ok: boolean;
|
|
11
|
+
}
|
|
8
12
|
export default function translationsPlugin(): Plugin;
|
|
9
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Reads every configured locale dictionary from the tenant API.
|
|
15
|
+
*
|
|
16
|
+
* `ok` is true only when every locale request returned an authoritative
|
|
17
|
+
* dictionary — an empty dictionary from a 200 response counts, a rejected or
|
|
18
|
+
* non-ok read does not. Failed locales keep the dictionary from `previous`,
|
|
19
|
+
* so a transient outage never replaces valid labels with empty data.
|
|
20
|
+
*/
|
|
21
|
+
export declare function loadLabelBundle(rootDir: string, previous?: LabelBundle): Promise<LabelBundleRead>;
|
|
10
22
|
export {};
|
|
11
23
|
//# sourceMappingURL=translations.d.ts.map
|
|
@@ -25,17 +25,50 @@ export default function translationsPlugin() {
|
|
|
25
25
|
let refreshTimer;
|
|
26
26
|
let pollTimer;
|
|
27
27
|
let eventConnectionAttempt = 0;
|
|
28
|
+
let subscribedAttempt = null;
|
|
28
29
|
let closed = false;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
let refreshQueue = Promise.resolve();
|
|
31
|
+
// `closeBundle` also fires between build-watch rebuilds, which must keep
|
|
32
|
+
// reading labels; only a torn-down dev server retires the queue.
|
|
33
|
+
const retired = () => closed && command === 'serve';
|
|
34
|
+
// Reads run one at a time: a read started before an event still resolves
|
|
35
|
+
// first, so it can never overwrite the fresher bundle the event asked for.
|
|
36
|
+
const refreshLabels = (invalidateIfChanged = false) => {
|
|
37
|
+
const refresh = refreshQueue.then(async () => {
|
|
38
|
+
if (retired()) {
|
|
39
|
+
return { bundle, ok: false };
|
|
40
|
+
}
|
|
41
|
+
const before = serializeBundle(bundle);
|
|
42
|
+
const next = await loadLabelBundle(rootDir, bundle);
|
|
43
|
+
if (retired()) {
|
|
44
|
+
return { bundle, ok: false };
|
|
45
|
+
}
|
|
46
|
+
bundle = next.bundle;
|
|
47
|
+
if (invalidateIfChanged && serializeBundle(bundle) !== before) {
|
|
48
|
+
invalidateLabels();
|
|
49
|
+
}
|
|
50
|
+
return next;
|
|
51
|
+
});
|
|
52
|
+
refreshQueue = refresh.then(() => undefined, () => undefined);
|
|
53
|
+
return refresh;
|
|
32
54
|
};
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
55
|
+
// A failed read must not retire the poll: it can hide a label change
|
|
56
|
+
// committed while the subscription was still connecting. Only a snapshot
|
|
57
|
+
// every locale reported as authoritative, applied while the attempt that
|
|
58
|
+
// asked for it is still the subscribed one, proves events now cover
|
|
59
|
+
// further changes.
|
|
60
|
+
const reconcileLabels = (attempt) => {
|
|
61
|
+
void refreshLabels(true)
|
|
62
|
+
.then((next) => {
|
|
63
|
+
if (next.ok &&
|
|
64
|
+
!closed &&
|
|
65
|
+
attempt !== null &&
|
|
66
|
+
attempt === eventConnectionAttempt &&
|
|
67
|
+
attempt === subscribedAttempt) {
|
|
68
|
+
stopFallbackPolling();
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
.catch(() => undefined);
|
|
39
72
|
};
|
|
40
73
|
const invalidateLabels = () => {
|
|
41
74
|
if (!devServer) {
|
|
@@ -72,7 +105,7 @@ export default function translationsPlugin() {
|
|
|
72
105
|
if (closed) {
|
|
73
106
|
return;
|
|
74
107
|
}
|
|
75
|
-
void
|
|
108
|
+
void refreshLabels(true).catch(() => undefined);
|
|
76
109
|
}, 150);
|
|
77
110
|
};
|
|
78
111
|
const startFallbackPolling = () => {
|
|
@@ -80,7 +113,7 @@ export default function translationsPlugin() {
|
|
|
80
113
|
return;
|
|
81
114
|
}
|
|
82
115
|
pollTimer = setInterval(() => {
|
|
83
|
-
|
|
116
|
+
reconcileLabels(subscribedAttempt);
|
|
84
117
|
}, FALLBACK_POLL_INTERVAL_MS);
|
|
85
118
|
};
|
|
86
119
|
const stopFallbackPolling = () => {
|
|
@@ -115,19 +148,21 @@ export default function translationsPlugin() {
|
|
|
115
148
|
await connectLabelEvents(rootDir, scheduleRefresh, sockets, {
|
|
116
149
|
isActive: () => !closed && attempt === eventConnectionAttempt,
|
|
117
150
|
onConnected: () => {
|
|
118
|
-
if (closed || attempt !== eventConnectionAttempt) {
|
|
151
|
+
if (closed || attempt !== eventConnectionAttempt || subscribedAttempt === attempt) {
|
|
119
152
|
return;
|
|
120
153
|
}
|
|
154
|
+
subscribedAttempt = attempt;
|
|
121
155
|
if (reconnectTimer) {
|
|
122
156
|
clearTimeout(reconnectTimer);
|
|
123
157
|
reconnectTimer = undefined;
|
|
124
158
|
}
|
|
125
|
-
|
|
159
|
+
reconcileLabels(attempt);
|
|
126
160
|
},
|
|
127
161
|
onDisconnected: () => {
|
|
128
162
|
if (closed || attempt !== eventConnectionAttempt) {
|
|
129
163
|
return;
|
|
130
164
|
}
|
|
165
|
+
subscribedAttempt = null;
|
|
131
166
|
scheduleReconnect();
|
|
132
167
|
},
|
|
133
168
|
});
|
|
@@ -166,7 +201,7 @@ export default function translationsPlugin() {
|
|
|
166
201
|
devServer = server;
|
|
167
202
|
closed = false;
|
|
168
203
|
server.watcher.add([resolveFimoConfigPath(rootDir), join(rootDir, '.env'), join(rootDir, '.env.local')]);
|
|
169
|
-
void
|
|
204
|
+
void refreshLabels(true).catch(() => undefined);
|
|
170
205
|
startFallbackPolling();
|
|
171
206
|
void connectEvents();
|
|
172
207
|
},
|
|
@@ -180,7 +215,7 @@ export default function translationsPlugin() {
|
|
|
180
215
|
if (id !== RESOLVED_ID) {
|
|
181
216
|
return null;
|
|
182
217
|
}
|
|
183
|
-
const current = command === 'serve' ? bundle : await refreshLabels();
|
|
218
|
+
const current = command === 'serve' ? bundle : (await refreshLabels()).bundle;
|
|
184
219
|
const dictionaries = JSON.stringify(current.dictionaries);
|
|
185
220
|
const defaultLabels = JSON.stringify(current.dictionaries[current.defaultLocale] ?? {});
|
|
186
221
|
return [
|
|
@@ -384,7 +419,15 @@ function labelUpdatePayload(bundle) {
|
|
|
384
419
|
signature: labelSignature(bundle),
|
|
385
420
|
};
|
|
386
421
|
}
|
|
387
|
-
|
|
422
|
+
/**
|
|
423
|
+
* Reads every configured locale dictionary from the tenant API.
|
|
424
|
+
*
|
|
425
|
+
* `ok` is true only when every locale request returned an authoritative
|
|
426
|
+
* dictionary — an empty dictionary from a 200 response counts, a rejected or
|
|
427
|
+
* non-ok read does not. Failed locales keep the dictionary from `previous`,
|
|
428
|
+
* so a transient outage never replaces valid labels with empty data.
|
|
429
|
+
*/
|
|
430
|
+
export async function loadLabelBundle(rootDir, previous) {
|
|
388
431
|
const config = getConfigServer(rootDir);
|
|
389
432
|
const defaultLocale = getFimoDefaultLocale(config);
|
|
390
433
|
const locales = getFimoLocales(config);
|
|
@@ -392,52 +435,54 @@ export async function loadLabelBundle(rootDir) {
|
|
|
392
435
|
const apiUrl = env.VITE_API_URL;
|
|
393
436
|
if (!apiUrl) {
|
|
394
437
|
return {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
438
|
+
bundle: {
|
|
439
|
+
dictionaries: Object.fromEntries(locales.map((locale) => [locale, {}])),
|
|
440
|
+
defaultLocale,
|
|
441
|
+
locales,
|
|
442
|
+
updatedAt: null,
|
|
443
|
+
},
|
|
444
|
+
ok: true,
|
|
399
445
|
};
|
|
400
446
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
};
|
|
447
|
+
const loaded = await Promise.all(locales.map(async (locale) => {
|
|
448
|
+
try {
|
|
449
|
+
return { locale, ok: true, ...(await fetchLabelDictionary(apiUrl, locale)) };
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
return { locale, ok: false };
|
|
453
|
+
}
|
|
454
|
+
}));
|
|
455
|
+
const ok = loaded.every((entry) => entry.ok);
|
|
456
|
+
const updatedAtValues = loaded.flatMap((entry) => entry.ok && typeof entry.updatedAt === 'string' ? [entry.updatedAt] : []);
|
|
457
|
+
if (!ok && typeof previous?.updatedAt === 'string') {
|
|
458
|
+
updatedAtValues.push(previous.updatedAt);
|
|
414
459
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
460
|
+
updatedAtValues.sort();
|
|
461
|
+
return {
|
|
462
|
+
bundle: {
|
|
463
|
+
dictionaries: Object.fromEntries(loaded.map((entry) => [entry.locale, entry.ok ? entry.labels : (previous?.dictionaries[entry.locale] ?? {})])),
|
|
418
464
|
defaultLocale,
|
|
419
465
|
locales,
|
|
420
|
-
updatedAt: null,
|
|
421
|
-
}
|
|
422
|
-
|
|
466
|
+
updatedAt: updatedAtValues[updatedAtValues.length - 1] ?? null,
|
|
467
|
+
},
|
|
468
|
+
ok,
|
|
469
|
+
};
|
|
423
470
|
}
|
|
424
471
|
async function fetchLabelDictionary(apiUrl, locale) {
|
|
425
472
|
const url = new URL('/labels', apiUrl);
|
|
426
473
|
url.searchParams.set('locale', locale);
|
|
427
474
|
const response = await fetchWithTimeout(url, LABEL_FETCH_TIMEOUT_MS);
|
|
428
475
|
if (!response.ok) {
|
|
429
|
-
|
|
476
|
+
throw new Error(`Failed to load labels for locale "${locale}": ${response.status}`);
|
|
430
477
|
}
|
|
431
478
|
const body = (await response.json());
|
|
432
479
|
if (Array.isArray(body.data)) {
|
|
433
480
|
return {
|
|
434
|
-
locale,
|
|
435
481
|
labels: Object.fromEntries(body.data.map((row) => [row.key, row.value ?? row.defaultValue ?? ''])),
|
|
436
482
|
updatedAt: null,
|
|
437
483
|
};
|
|
438
484
|
}
|
|
439
485
|
return {
|
|
440
|
-
locale,
|
|
441
486
|
labels: body.data?.labels ?? {},
|
|
442
487
|
updatedAt: body.data?.updatedAt ?? null,
|
|
443
488
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fimo-vite",
|
|
3
|
-
"version": "2.4.0-staging.
|
|
3
|
+
"version": "2.4.0-staging.4",
|
|
4
4
|
"description": "Framework-neutral Fimo Vite plugin. Pairs with the fimo package at the same version.",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist/",
|
|
@@ -31,15 +31,15 @@
|
|
|
31
31
|
"oxc-parser": "^0.118.0"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@fimo/tsconfig": "2.4.0-staging.
|
|
34
|
+
"@fimo/tsconfig": "2.4.0-staging.4",
|
|
35
35
|
"@types/node": "22.18.6",
|
|
36
|
-
"fimo": "2.4.0-staging.
|
|
36
|
+
"fimo": "2.4.0-staging.4",
|
|
37
37
|
"typescript": "7.0.2",
|
|
38
38
|
"vite": "8.2.2",
|
|
39
39
|
"vitest": "5.0.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"fimo": "2.4.0-staging.
|
|
42
|
+
"fimo": "2.4.0-staging.4",
|
|
43
43
|
"vite": "^7.0.0 || ^8.0.0"
|
|
44
44
|
},
|
|
45
45
|
"engines": {
|