fimo-vite 0.21.0-experimental.1

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.
@@ -0,0 +1,73 @@
1
+ import { readFileSync } from 'fs';
2
+ import { resolveFimoConfigPath } from 'fimo/config';
3
+ import { getFimoPaths } from 'fimo/paths';
4
+ function readSeoUrl() {
5
+ try {
6
+ const raw = readFileSync(resolveFimoConfigPath(process.cwd()), 'utf-8');
7
+ const config = JSON.parse(raw);
8
+ return config.seo?.url;
9
+ }
10
+ catch {
11
+ return undefined;
12
+ }
13
+ }
14
+ function buildSitemapXml(baseUrl, paths) {
15
+ const urls = paths
16
+ .map((path) => {
17
+ try {
18
+ return new URL(path, baseUrl).toString();
19
+ }
20
+ catch {
21
+ return undefined;
22
+ }
23
+ })
24
+ .filter((url) => typeof url === 'string');
25
+ const body = urls.map((url) => ` <url><loc>${url}</loc></url>`).join('\n');
26
+ return [
27
+ '<?xml version="1.0" encoding="UTF-8"?>',
28
+ '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
29
+ body,
30
+ '</urlset>',
31
+ '',
32
+ ].join('\n');
33
+ }
34
+ async function generateSitemap() {
35
+ const baseUrl = readSeoUrl();
36
+ if (!baseUrl) {
37
+ return undefined;
38
+ }
39
+ const fimoPaths = await getFimoPaths();
40
+ const paths = fimoPaths.map((p) => p.path);
41
+ return buildSitemapXml(baseUrl, paths);
42
+ }
43
+ export default function sitemapPlugin() {
44
+ return {
45
+ name: 'fimo:sitemap',
46
+ configureServer(server) {
47
+ server.middlewares.use(async (req, res, next) => {
48
+ if (req.url?.split('?')[0] !== '/sitemap.xml') {
49
+ next();
50
+ return;
51
+ }
52
+ const xml = await generateSitemap();
53
+ if (!xml) {
54
+ next();
55
+ return;
56
+ }
57
+ res.setHeader('Content-Type', 'application/xml; charset=utf-8');
58
+ res.end(xml);
59
+ });
60
+ },
61
+ async generateBundle() {
62
+ const xml = await generateSitemap();
63
+ if (!xml) {
64
+ return;
65
+ }
66
+ this.emitFile({
67
+ type: 'asset',
68
+ fileName: 'sitemap.xml',
69
+ source: xml,
70
+ });
71
+ },
72
+ };
73
+ }
@@ -0,0 +1,11 @@
1
+ import type { Plugin } from 'vite';
2
+ interface LabelBundle {
3
+ dictionaries: Record<string, Record<string, string>>;
4
+ defaultLocale: string;
5
+ locales: string[];
6
+ updatedAt: string | null;
7
+ }
8
+ export default function translationsPlugin(): Plugin;
9
+ export declare function loadLabelBundle(rootDir: string): Promise<LabelBundle>;
10
+ export {};
11
+ //# sourceMappingURL=translations.d.ts.map
@@ -0,0 +1,600 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { createHash } from 'node:crypto';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { homedir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { getConfigServer, getFimoDefaultLocale, getFimoLocales, resolveFimoConfigPath } from 'fimo/config';
7
+ import { CURRENT_PROJECT_LAYOUT, LEGACY_PROJECT_LAYOUT } from 'fimo/project-layout';
8
+ const VIRTUAL_ID = 'virtual:translations';
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;
13
+ export default function translationsPlugin() {
14
+ let rootDir = process.cwd();
15
+ let bundle = {
16
+ dictionaries: { en: {} },
17
+ defaultLocale: 'en',
18
+ locales: ['en'],
19
+ updatedAt: null,
20
+ };
21
+ let command = 'build';
22
+ let devServer = null;
23
+ let eventSockets = [];
24
+ let reconnectTimer;
25
+ let refreshTimer;
26
+ let pollTimer;
27
+ let eventConnectionAttempt = 0;
28
+ let closed = false;
29
+ const refreshLabels = async () => {
30
+ bundle = await loadLabelBundle(rootDir);
31
+ return bundle;
32
+ };
33
+ const refreshLabelsAndInvalidateIfChanged = async () => {
34
+ const before = serializeBundle(bundle);
35
+ const next = await refreshLabels();
36
+ if (serializeBundle(next) !== before) {
37
+ invalidateLabels();
38
+ }
39
+ };
40
+ const invalidateLabels = () => {
41
+ if (!devServer) {
42
+ return;
43
+ }
44
+ for (const mod of findVirtualTranslationModules(devServer)) {
45
+ devServer.moduleGraph.invalidateModule(mod);
46
+ }
47
+ devServer.ws.send({
48
+ type: 'custom',
49
+ event: 'fimo:labels-updated',
50
+ data: labelUpdatePayload(bundle),
51
+ });
52
+ };
53
+ const emitContentUpdate = (payload) => {
54
+ if (!devServer) {
55
+ return;
56
+ }
57
+ devServer.ws.send({
58
+ type: 'custom',
59
+ event: 'fimo:content-updated',
60
+ data: payload,
61
+ });
62
+ };
63
+ const scheduleRefresh = () => {
64
+ if (closed) {
65
+ return;
66
+ }
67
+ if (refreshTimer) {
68
+ clearTimeout(refreshTimer);
69
+ }
70
+ refreshTimer = setTimeout(() => {
71
+ refreshTimer = undefined;
72
+ if (closed) {
73
+ return;
74
+ }
75
+ void refreshLabelsAndInvalidateIfChanged().catch(() => undefined);
76
+ }, 150);
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
+ };
149
+ return {
150
+ name: 'fimo-virtual-translations',
151
+ enforce: 'pre',
152
+ configResolved(config) {
153
+ rootDir = config.root;
154
+ command = config.command;
155
+ },
156
+ async buildStart() {
157
+ bundle = emptyLabelBundle(rootDir);
158
+ this.addWatchFile(resolveFimoConfigPath(rootDir));
159
+ this.addWatchFile(join(rootDir, '.env'));
160
+ this.addWatchFile(join(rootDir, '.env.local'));
161
+ if (command === 'build') {
162
+ await refreshLabels();
163
+ }
164
+ },
165
+ configureServer(server) {
166
+ devServer = server;
167
+ closed = false;
168
+ server.watcher.add([resolveFimoConfigPath(rootDir), join(rootDir, '.env'), join(rootDir, '.env.local')]);
169
+ void refreshLabelsAndInvalidateIfChanged().catch(() => undefined);
170
+ startFallbackPolling();
171
+ void connectEvents();
172
+ },
173
+ resolveId(id) {
174
+ if (id === VIRTUAL_ID) {
175
+ return RESOLVED_ID;
176
+ }
177
+ return null;
178
+ },
179
+ async load(id) {
180
+ if (id !== RESOLVED_ID) {
181
+ return null;
182
+ }
183
+ const current = command === 'serve' ? bundle : await refreshLabels();
184
+ const dictionaries = JSON.stringify(current.dictionaries);
185
+ const defaultLabels = JSON.stringify(current.dictionaries[current.defaultLocale] ?? {});
186
+ return [
187
+ `export const dictionaries = ${dictionaries};`,
188
+ `export const labelsByLocale = dictionaries;`,
189
+ `export const translations = ${defaultLabels};`,
190
+ `export const labels = translations;`,
191
+ `export const defaultLocale = ${JSON.stringify(current.defaultLocale)};`,
192
+ `export const locale = defaultLocale;`,
193
+ `export const locales = ${JSON.stringify(current.locales)};`,
194
+ `export const updatedAt = ${JSON.stringify(current.updatedAt)};`,
195
+ `export const signature = ${JSON.stringify(labelSignature(current))};`,
196
+ ].join('\n');
197
+ },
198
+ async handleHotUpdate(ctx) {
199
+ const watched = new Set([resolveFimoConfigPath(rootDir), join(rootDir, '.env'), join(rootDir, '.env.local')]);
200
+ if (!watched.has(ctx.file)) {
201
+ return;
202
+ }
203
+ await refreshLabels();
204
+ const modules = findVirtualTranslationModules(ctx.server);
205
+ if (modules.length > 0) {
206
+ for (const mod of modules) {
207
+ ctx.server.moduleGraph.invalidateModule(mod);
208
+ }
209
+ return modules;
210
+ }
211
+ return [];
212
+ },
213
+ closeBundle() {
214
+ closed = true;
215
+ eventConnectionAttempt += 1;
216
+ closeEventSockets();
217
+ devServer = null;
218
+ if (reconnectTimer) {
219
+ clearTimeout(reconnectTimer);
220
+ reconnectTimer = undefined;
221
+ }
222
+ if (refreshTimer) {
223
+ clearTimeout(refreshTimer);
224
+ refreshTimer = undefined;
225
+ }
226
+ stopFallbackPolling();
227
+ },
228
+ };
229
+ async function connectLabelEvents(root, onLabelsChanged, sockets, callbacks) {
230
+ const env = await readProjectEnv(root);
231
+ if (!callbacks.isActive()) {
232
+ return;
233
+ }
234
+ const apiUrl = env.VITE_API_URL;
235
+ if (!apiUrl) {
236
+ return;
237
+ }
238
+ const runtimeConfig = await loadRuntimeConfig(apiUrl);
239
+ if (!callbacks.isActive()) {
240
+ return;
241
+ }
242
+ const eventsUrl = runtimeConfig.eventsServerUrl ?? undefined;
243
+ if (!eventsUrl) {
244
+ return;
245
+ }
246
+ const settings = await readProjectSettings(root);
247
+ if (!callbacks.isActive()) {
248
+ return;
249
+ }
250
+ if (!settings.projectId) {
251
+ return;
252
+ }
253
+ const WebSocketCtor = globalThis.WebSocket;
254
+ if (!WebSocketCtor) {
255
+ return;
256
+ }
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));
264
+ return;
265
+ }
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()) {
271
+ return;
272
+ }
273
+ openSockets.add(socket);
274
+ if (openSockets.size === rooms.length) {
275
+ callbacks.onConnected();
276
+ }
277
+ };
278
+ socket.onmessage = (event) => {
279
+ if (!callbacks.isActive()) {
280
+ return;
281
+ }
282
+ const raw = typeof event.data === 'string' ? event.data : '';
283
+ if (!raw) {
284
+ return;
285
+ }
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
+ }
313
+ }
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();
327
+ }
328
+ }
329
+ function parseContentChangePayload(payload) {
330
+ if (!payload || typeof payload !== 'object') {
331
+ return { changes: [] };
332
+ }
333
+ const source = payload;
334
+ const changes = Array.isArray(source.changes)
335
+ ? source.changes
336
+ .filter((change) => Boolean(change) && typeof change === 'object')
337
+ .map((change) => ({
338
+ action: typeof change.action === 'string' ? change.action : undefined,
339
+ contentType: typeof change.contentType === 'string' ? change.contentType : undefined,
340
+ documentId: typeof change.documentId === 'string' ? change.documentId : undefined,
341
+ id: typeof change.id === 'string' ? change.id : undefined,
342
+ locale: typeof change.locale === 'string' ? change.locale : undefined,
343
+ }))
344
+ : [];
345
+ return {
346
+ changes,
347
+ updatedAt: typeof source.updatedAt === 'string' ? source.updatedAt : undefined,
348
+ };
349
+ }
350
+ function serializeBundle(bundle) {
351
+ return JSON.stringify({
352
+ dictionaries: bundle.dictionaries,
353
+ defaultLocale: bundle.defaultLocale,
354
+ locales: bundle.locales,
355
+ });
356
+ }
357
+ function emptyLabelBundle(rootDir) {
358
+ const config = getConfigServer(rootDir);
359
+ const defaultLocale = getFimoDefaultLocale(config);
360
+ const locales = getFimoLocales(config);
361
+ return {
362
+ dictionaries: Object.fromEntries(locales.map((locale) => [locale, {}])),
363
+ defaultLocale,
364
+ locales,
365
+ updatedAt: null,
366
+ };
367
+ }
368
+ function findVirtualTranslationModules(server) {
369
+ const direct = server.moduleGraph.getModuleById(RESOLVED_ID);
370
+ const matches = new Set(direct ? [direct] : []);
371
+ for (const mod of server.moduleGraph.idToModuleMap.values()) {
372
+ if (mod.id?.includes('virtual:translations')) {
373
+ matches.add(mod);
374
+ }
375
+ }
376
+ return [...matches];
377
+ }
378
+ function labelSignature(bundle) {
379
+ return Buffer.from(serializeBundle(bundle)).toString('base64');
380
+ }
381
+ function labelUpdatePayload(bundle) {
382
+ return {
383
+ ...bundle,
384
+ signature: labelSignature(bundle),
385
+ };
386
+ }
387
+ export async function loadLabelBundle(rootDir) {
388
+ const config = getConfigServer(rootDir);
389
+ const defaultLocale = getFimoDefaultLocale(config);
390
+ const locales = getFimoLocales(config);
391
+ const env = await readProjectEnv(rootDir);
392
+ const apiUrl = env.VITE_API_URL;
393
+ if (!apiUrl) {
394
+ return {
395
+ dictionaries: Object.fromEntries(locales.map((locale) => [locale, {}])),
396
+ defaultLocale,
397
+ locales,
398
+ updatedAt: null,
399
+ };
400
+ }
401
+ try {
402
+ const loaded = await Promise.all(locales.map((locale) => fetchLabelDictionary(apiUrl, locale).catch(() => ({ locale, labels: {}, updatedAt: null }))));
403
+ const updatedAtValues = loaded
404
+ .map((entry) => entry.updatedAt)
405
+ .filter((value) => typeof value === 'string')
406
+ .sort();
407
+ const updatedAt = updatedAtValues[updatedAtValues.length - 1];
408
+ return {
409
+ dictionaries: Object.fromEntries(loaded.map((entry) => [entry.locale, entry.labels])),
410
+ defaultLocale,
411
+ locales,
412
+ updatedAt: updatedAt ?? null,
413
+ };
414
+ }
415
+ catch {
416
+ return {
417
+ dictionaries: Object.fromEntries(locales.map((locale) => [locale, {}])),
418
+ defaultLocale,
419
+ locales,
420
+ updatedAt: null,
421
+ };
422
+ }
423
+ }
424
+ async function fetchLabelDictionary(apiUrl, locale) {
425
+ const url = new URL('/labels', apiUrl);
426
+ url.searchParams.set('locale', locale);
427
+ const response = await fetchWithTimeout(url, LABEL_FETCH_TIMEOUT_MS);
428
+ if (!response.ok) {
429
+ return { locale, labels: {}, updatedAt: null };
430
+ }
431
+ const body = (await response.json());
432
+ if (Array.isArray(body.data)) {
433
+ return {
434
+ locale,
435
+ labels: Object.fromEntries(body.data.map((row) => [row.key, row.value ?? row.defaultValue ?? ''])),
436
+ updatedAt: null,
437
+ };
438
+ }
439
+ return {
440
+ locale,
441
+ labels: body.data?.labels ?? {},
442
+ updatedAt: body.data?.updatedAt ?? null,
443
+ };
444
+ }
445
+ async function loadRuntimeConfig(apiUrl) {
446
+ try {
447
+ const response = await fetchWithTimeout(new URL('/runtime-config', apiUrl), 2000);
448
+ if (!response.ok) {
449
+ return {};
450
+ }
451
+ const body = await response.json();
452
+ return parseRuntimeConfig(body);
453
+ }
454
+ catch {
455
+ return {};
456
+ }
457
+ }
458
+ function parseRuntimeConfig(body) {
459
+ if (!body || typeof body !== 'object') {
460
+ return {};
461
+ }
462
+ const envelopeData = body.data;
463
+ const source = envelopeData && typeof envelopeData === 'object' ? envelopeData : body;
464
+ const eventsServerUrl = source.eventsServerUrl;
465
+ return {
466
+ eventsServerUrl: typeof eventsServerUrl === 'string' ? eventsServerUrl : undefined,
467
+ };
468
+ }
469
+ async function fetchWithTimeout(url, timeoutMs) {
470
+ const controller = new AbortController();
471
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
472
+ try {
473
+ return await fetch(url, { signal: controller.signal });
474
+ }
475
+ finally {
476
+ clearTimeout(timeout);
477
+ }
478
+ }
479
+ async function readProjectEnv(rootDir) {
480
+ const values = {
481
+ ...(await readEnvFile(join(rootDir, '.env'))),
482
+ ...(await readEnvFile(join(rootDir, '.env.local'))),
483
+ };
484
+ for (const key of ['VITE_API_URL']) {
485
+ if (process.env[key]) {
486
+ values[key] = process.env[key];
487
+ }
488
+ }
489
+ return values;
490
+ }
491
+ async function readEnvFile(filePath) {
492
+ try {
493
+ const content = await readFile(filePath, 'utf8');
494
+ const values = {};
495
+ for (const line of content.split(/\r?\n/)) {
496
+ const trimmed = line.trim();
497
+ if (!trimmed || trimmed.startsWith('#')) {
498
+ continue;
499
+ }
500
+ const index = trimmed.indexOf('=');
501
+ if (index === -1) {
502
+ continue;
503
+ }
504
+ const key = trimmed.slice(0, index).trim();
505
+ const value = trimmed
506
+ .slice(index + 1)
507
+ .trim()
508
+ .replace(/^['"]|['"]$/g, '');
509
+ values[key] = value;
510
+ }
511
+ return values;
512
+ }
513
+ catch {
514
+ return {};
515
+ }
516
+ }
517
+ async function readProjectSettings(rootDir) {
518
+ for (const relativePath of [CURRENT_PROJECT_LAYOUT.project, LEGACY_PROJECT_LAYOUT.project]) {
519
+ try {
520
+ const raw = await readFile(join(rootDir, relativePath), 'utf8');
521
+ return JSON.parse(raw);
522
+ }
523
+ catch {
524
+ continue;
525
+ }
526
+ }
527
+ return {};
528
+ }
529
+ function deriveEventsRooms(projectId, tenantApiUrl) {
530
+ const rooms = new Set([projectId]);
531
+ try {
532
+ const firstLabel = new URL(tenantApiUrl).hostname.split('.')[0];
533
+ if (firstLabel.startsWith(`${projectId}-`)) {
534
+ rooms.add(`${projectId}:${firstLabel.slice(projectId.length + 1)}`);
535
+ }
536
+ }
537
+ catch {
538
+ return [...rooms];
539
+ }
540
+ return [...rooms];
541
+ }
542
+ async function generateOneTimeToken(apiUrl) {
543
+ const bearer = process.env.FIMO_API_TOKEN?.trim() || (await readStoredToken(apiUrl));
544
+ if (!bearer) {
545
+ throw new Error('Not signed in to Fimo.');
546
+ }
547
+ const response = await fetch(new URL('/api/auth/one-time-token/generate', apiUrl), {
548
+ method: 'GET',
549
+ headers: {
550
+ Authorization: `Bearer ${bearer}`,
551
+ },
552
+ });
553
+ if (!response.ok) {
554
+ throw new Error(`Failed to generate events auth token: ${response.status}`);
555
+ }
556
+ const body = (await response.json());
557
+ const token = body.token ?? body.data?.token;
558
+ if (!token) {
559
+ throw new Error('Events auth token response did not include a token.');
560
+ }
561
+ return token;
562
+ }
563
+ async function readStoredToken(apiUrl) {
564
+ const configDir = join(process.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'fimo');
565
+ const credentialsFile = join(configDir, 'credentials', credentialsFilename(apiUrl));
566
+ try {
567
+ const raw = await readFile(credentialsFile, 'utf8');
568
+ const parsed = JSON.parse(raw);
569
+ return parsed.access_token ?? null;
570
+ }
571
+ catch {
572
+ return null;
573
+ }
574
+ }
575
+ function credentialsFilename(apiUrl) {
576
+ const normalized = normalizeApiUrl(apiUrl);
577
+ let hostLabel = 'unknown';
578
+ try {
579
+ hostLabel = new URL(normalized).host
580
+ .replace(/:/g, '-')
581
+ .replace(/[^a-zA-Z0-9.-]+/g, '-')
582
+ .replace(/^[.-]+|[.-]+$/g, '')
583
+ .toLowerCase();
584
+ }
585
+ catch {
586
+ hostLabel = 'unknown';
587
+ }
588
+ return `${hostLabel || 'unknown'}-${createHash('sha256').update(normalized).digest('hex').slice(0, 6)}.json`;
589
+ }
590
+ function normalizeApiUrl(input) {
591
+ const trimmed = input.trim();
592
+ try {
593
+ const url = new URL(trimmed);
594
+ const pathname = url.pathname.replace(/\/+$/, '');
595
+ return `${url.protocol}//${url.host}${pathname}`;
596
+ }
597
+ catch {
598
+ return trimmed.replace(/\/+$/, '');
599
+ }
600
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=overlay-hmr.d.ts.map