fimo 0.10.0 → 0.10.1-staging.2
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/assets/skills/fimo-cli/SKILL.md +3 -1
- package/assets/skills/fimo-cli/references/content.md +25 -6
- package/assets/skills/fimo-cli/references/history.md +40 -0
- package/assets/skills/fimo-cli/references/schedules.md +45 -22
- package/assets/skills/fimo-code/references/content.md +1 -1
- package/assets/skills/fimo-studio/SKILL.md +4 -2
- package/assets/skills/fimo-studio/references/cms.md +8 -8
- package/assets/skills/fimo-studio/references/schedules.md +43 -0
- package/assets/skills/fimo-studio/references/version-history.md +9 -9
- package/dist/build/vite/plugins/translations.js +176 -58
- package/dist/cli/bundle.json +1 -1
- package/dist/cli/index.js +80 -67
- package/dist/runtime/content/client/createContentClient.js +1 -1
- package/package.json +2 -1
- package/release.json +2 -2
- package/templates/react-router/package.json +1 -1
|
@@ -7,15 +7,25 @@ import { getFimoDefaultLocale, getFimoLocales } from '../../../runtime/i18n/loca
|
|
|
7
7
|
import { getConfigServer } from '../../../runtime/shared/fimo-config.server.js';
|
|
8
8
|
const VIRTUAL_ID = 'virtual:translations';
|
|
9
9
|
const RESOLVED_ID = '\0' + VIRTUAL_ID;
|
|
10
|
+
const EVENT_RECONNECT_DELAY_MS = 2000;
|
|
11
|
+
const FALLBACK_POLL_INTERVAL_MS = 30_000;
|
|
12
|
+
const LABEL_FETCH_TIMEOUT_MS = 30_000;
|
|
10
13
|
export default function translationsPlugin() {
|
|
11
14
|
let rootDir = process.cwd();
|
|
12
|
-
let bundle = {
|
|
15
|
+
let bundle = {
|
|
16
|
+
dictionaries: { en: {} },
|
|
17
|
+
defaultLocale: 'en',
|
|
18
|
+
locales: ['en'],
|
|
19
|
+
updatedAt: null,
|
|
20
|
+
};
|
|
13
21
|
let command = 'build';
|
|
14
22
|
let devServer = null;
|
|
15
23
|
let eventSockets = [];
|
|
16
24
|
let reconnectTimer;
|
|
17
25
|
let refreshTimer;
|
|
18
26
|
let pollTimer;
|
|
27
|
+
let eventConnectionAttempt = 0;
|
|
28
|
+
let closed = false;
|
|
19
29
|
const refreshLabels = async () => {
|
|
20
30
|
bundle = await loadLabelBundle(rootDir);
|
|
21
31
|
return bundle;
|
|
@@ -51,13 +61,91 @@ export default function translationsPlugin() {
|
|
|
51
61
|
});
|
|
52
62
|
};
|
|
53
63
|
const scheduleRefresh = () => {
|
|
64
|
+
if (closed) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
54
67
|
if (refreshTimer) {
|
|
55
68
|
clearTimeout(refreshTimer);
|
|
56
69
|
}
|
|
57
70
|
refreshTimer = setTimeout(() => {
|
|
71
|
+
refreshTimer = undefined;
|
|
72
|
+
if (closed) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
58
75
|
void refreshLabelsAndInvalidateIfChanged().catch(() => undefined);
|
|
59
76
|
}, 150);
|
|
60
77
|
};
|
|
78
|
+
const startFallbackPolling = () => {
|
|
79
|
+
if (closed || pollTimer) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
pollTimer = setInterval(() => {
|
|
83
|
+
void refreshLabelsAndInvalidateIfChanged().catch(() => undefined);
|
|
84
|
+
}, FALLBACK_POLL_INTERVAL_MS);
|
|
85
|
+
};
|
|
86
|
+
const stopFallbackPolling = () => {
|
|
87
|
+
if (!pollTimer) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
clearInterval(pollTimer);
|
|
91
|
+
pollTimer = undefined;
|
|
92
|
+
};
|
|
93
|
+
const closeEventSockets = () => {
|
|
94
|
+
closeSockets(eventSockets.splice(0));
|
|
95
|
+
};
|
|
96
|
+
const scheduleReconnect = (delayMs = EVENT_RECONNECT_DELAY_MS) => {
|
|
97
|
+
startFallbackPolling();
|
|
98
|
+
if (closed || reconnectTimer) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
reconnectTimer = setTimeout(() => {
|
|
102
|
+
reconnectTimer = undefined;
|
|
103
|
+
closeEventSockets();
|
|
104
|
+
void connectEvents();
|
|
105
|
+
}, delayMs);
|
|
106
|
+
};
|
|
107
|
+
const connectEvents = async () => {
|
|
108
|
+
if (closed) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const attempt = ++eventConnectionAttempt;
|
|
112
|
+
const sockets = [];
|
|
113
|
+
eventSockets = sockets;
|
|
114
|
+
try {
|
|
115
|
+
await connectLabelEvents(rootDir, scheduleRefresh, sockets, {
|
|
116
|
+
isActive: () => !closed && attempt === eventConnectionAttempt,
|
|
117
|
+
onConnected: () => {
|
|
118
|
+
if (closed || attempt !== eventConnectionAttempt) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (reconnectTimer) {
|
|
122
|
+
clearTimeout(reconnectTimer);
|
|
123
|
+
reconnectTimer = undefined;
|
|
124
|
+
}
|
|
125
|
+
stopFallbackPolling();
|
|
126
|
+
},
|
|
127
|
+
onDisconnected: () => {
|
|
128
|
+
if (closed || attempt !== eventConnectionAttempt) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
scheduleReconnect();
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
if (closed || attempt !== eventConnectionAttempt) {
|
|
135
|
+
closeSockets(sockets.splice(0));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (sockets.length === 0) {
|
|
139
|
+
scheduleReconnect(FALLBACK_POLL_INTERVAL_MS);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
if (!closed && attempt === eventConnectionAttempt) {
|
|
144
|
+
closeEventSockets();
|
|
145
|
+
scheduleReconnect(FALLBACK_POLL_INTERVAL_MS);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
61
149
|
return {
|
|
62
150
|
name: 'fimo-virtual-translations',
|
|
63
151
|
enforce: 'pre',
|
|
@@ -76,16 +164,11 @@ export default function translationsPlugin() {
|
|
|
76
164
|
},
|
|
77
165
|
configureServer(server) {
|
|
78
166
|
devServer = server;
|
|
167
|
+
closed = false;
|
|
79
168
|
server.watcher.add([join(rootDir, 'fimo-config.json'), join(rootDir, '.env'), join(rootDir, '.env.local')]);
|
|
80
169
|
void refreshLabelsAndInvalidateIfChanged().catch(() => undefined);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
eventSockets = sockets;
|
|
84
|
-
})
|
|
85
|
-
.catch(() => undefined);
|
|
86
|
-
pollTimer = setInterval(() => {
|
|
87
|
-
void refreshLabelsAndInvalidateIfChanged().catch(() => undefined);
|
|
88
|
-
}, 2000);
|
|
170
|
+
startFallbackPolling();
|
|
171
|
+
void connectEvents();
|
|
89
172
|
},
|
|
90
173
|
resolveId(id) {
|
|
91
174
|
if (id === VIRTUAL_ID) {
|
|
@@ -128,84 +211,119 @@ export default function translationsPlugin() {
|
|
|
128
211
|
return [];
|
|
129
212
|
},
|
|
130
213
|
closeBundle() {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
214
|
+
closed = true;
|
|
215
|
+
eventConnectionAttempt += 1;
|
|
216
|
+
closeEventSockets();
|
|
217
|
+
devServer = null;
|
|
135
218
|
if (reconnectTimer) {
|
|
136
219
|
clearTimeout(reconnectTimer);
|
|
220
|
+
reconnectTimer = undefined;
|
|
137
221
|
}
|
|
138
222
|
if (refreshTimer) {
|
|
139
223
|
clearTimeout(refreshTimer);
|
|
224
|
+
refreshTimer = undefined;
|
|
140
225
|
}
|
|
141
|
-
|
|
142
|
-
clearInterval(pollTimer);
|
|
143
|
-
}
|
|
226
|
+
stopFallbackPolling();
|
|
144
227
|
},
|
|
145
228
|
};
|
|
146
|
-
async function connectLabelEvents(root, onLabelsChanged) {
|
|
229
|
+
async function connectLabelEvents(root, onLabelsChanged, sockets, callbacks) {
|
|
147
230
|
const env = await readProjectEnv(root);
|
|
231
|
+
if (!callbacks.isActive()) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
148
234
|
const apiUrl = env.VITE_API_URL;
|
|
149
235
|
if (!apiUrl) {
|
|
150
|
-
return
|
|
236
|
+
return;
|
|
151
237
|
}
|
|
152
238
|
const runtimeConfig = await loadRuntimeConfig(apiUrl);
|
|
239
|
+
if (!callbacks.isActive()) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
153
242
|
const eventsUrl = runtimeConfig.eventsServerUrl ?? undefined;
|
|
154
243
|
if (!eventsUrl) {
|
|
155
|
-
return
|
|
244
|
+
return;
|
|
156
245
|
}
|
|
157
246
|
const settings = await readProjectSettings(root);
|
|
247
|
+
if (!callbacks.isActive()) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
158
250
|
if (!settings.projectId) {
|
|
159
|
-
return
|
|
251
|
+
return;
|
|
160
252
|
}
|
|
161
253
|
const WebSocketCtor = globalThis.WebSocket;
|
|
162
254
|
if (!WebSocketCtor) {
|
|
163
|
-
return
|
|
255
|
+
return;
|
|
164
256
|
}
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const raw = typeof event.data === 'string' ? event.data : '';
|
|
173
|
-
if (!raw) {
|
|
257
|
+
const rooms = deriveEventsRooms(settings.projectId, apiUrl);
|
|
258
|
+
const openSockets = new Set();
|
|
259
|
+
try {
|
|
260
|
+
for (const room of rooms) {
|
|
261
|
+
const token = await generateOneTimeToken(settings.apiUrl ?? process.env.FIMO_API_URL ?? 'http://localhost:3000');
|
|
262
|
+
if (!callbacks.isActive()) {
|
|
263
|
+
closeSockets(sockets.splice(0));
|
|
174
264
|
return;
|
|
175
265
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
266
|
+
const wsUrl = new URL(`/parties/events/${room}`, eventsUrl.replace(/^http/, 'ws'));
|
|
267
|
+
wsUrl.searchParams.set('authToken', token);
|
|
268
|
+
const socket = new WebSocketCtor(wsUrl.toString());
|
|
269
|
+
socket.onopen = () => {
|
|
270
|
+
if (!callbacks.isActive()) {
|
|
180
271
|
return;
|
|
181
272
|
}
|
|
182
|
-
|
|
273
|
+
openSockets.add(socket);
|
|
274
|
+
if (openSockets.size === rooms.length) {
|
|
275
|
+
callbacks.onConnected();
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
socket.onmessage = (event) => {
|
|
279
|
+
if (!callbacks.isActive()) {
|
|
183
280
|
return;
|
|
184
281
|
}
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (changedLocales.length === 0 || changedLocales.some((locale) => bundle.locales.includes(locale))) {
|
|
189
|
-
onLabelsChanged();
|
|
282
|
+
const raw = typeof event.data === 'string' ? event.data : '';
|
|
283
|
+
if (!raw) {
|
|
284
|
+
return;
|
|
190
285
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
286
|
+
try {
|
|
287
|
+
const message = JSON.parse(raw);
|
|
288
|
+
if (message.type === 'content:changed') {
|
|
289
|
+
emitContentUpdate(parseContentChangePayload(message.payload));
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (message.type !== 'labels:changed') {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const changedLocales = message.payload?.changes
|
|
296
|
+
?.map((change) => change.locale)
|
|
297
|
+
.filter((locale) => typeof locale === 'string') ?? [];
|
|
298
|
+
if (changedLocales.length === 0 || changedLocales.some((locale) => bundle.locales.includes(locale))) {
|
|
299
|
+
onLabelsChanged();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
socket.onclose = () => {
|
|
307
|
+
openSockets.delete(socket);
|
|
308
|
+
callbacks.onDisconnected();
|
|
309
|
+
};
|
|
310
|
+
socket.onerror = callbacks.onDisconnected;
|
|
311
|
+
sockets.push(socket);
|
|
312
|
+
}
|
|
207
313
|
}
|
|
208
|
-
|
|
314
|
+
catch (error) {
|
|
315
|
+
closeSockets(sockets.splice(0));
|
|
316
|
+
throw error;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
function closeSockets(sockets) {
|
|
321
|
+
for (const socket of sockets) {
|
|
322
|
+
socket.onopen = null;
|
|
323
|
+
socket.onmessage = null;
|
|
324
|
+
socket.onclose = null;
|
|
325
|
+
socket.onerror = null;
|
|
326
|
+
socket.close();
|
|
209
327
|
}
|
|
210
328
|
}
|
|
211
329
|
function parseContentChangePayload(payload) {
|
|
@@ -306,7 +424,7 @@ export async function loadLabelBundle(rootDir) {
|
|
|
306
424
|
async function fetchLabelDictionary(apiUrl, locale) {
|
|
307
425
|
const url = new URL('/labels', apiUrl);
|
|
308
426
|
url.searchParams.set('locale', locale);
|
|
309
|
-
const response = await fetchWithTimeout(url,
|
|
427
|
+
const response = await fetchWithTimeout(url, LABEL_FETCH_TIMEOUT_MS);
|
|
310
428
|
if (!response.ok) {
|
|
311
429
|
return { locale, labels: {}, updatedAt: null };
|
|
312
430
|
}
|