@lovable.dev/vite-tanstack-config 2.12.1 → 2.13.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.
@@ -0,0 +1,548 @@
1
+ import { appendFile, mkdir, writeFile } from "node:fs/promises";
2
+ import { dirname, resolve } from "node:path";
3
+ //#region src/dev-server-bridge/error-collector.ts
4
+ const DEFAULT_ENDPOINT = "/__lovable/error-collector";
5
+ const DEFAULT_OUTPUT_FILE = "/tmp/runtime-errors.log";
6
+ const DEFAULT_MAX_BODY_BYTES = 64 * 1024;
7
+ function isViteClientCarrier(id) {
8
+ return id.includes("/vite/dist/client/client.mjs");
9
+ }
10
+ function validateEndpoint(endpoint) {
11
+ if (!endpoint.startsWith("/") || endpoint.startsWith("//") || endpoint.includes("?") || endpoint.includes("#")) throw new Error(`errorCollectorPlugin endpoint must be a root-relative path, received ${JSON.stringify(endpoint)}`);
12
+ }
13
+ function validateMaxBodyBytes(maxBodyBytes) {
14
+ if (!Number.isSafeInteger(maxBodyBytes) || maxBodyBytes <= 0) throw new Error("errorCollectorPlugin maxBodyBytes must be a positive safe integer");
15
+ }
16
+ function readJsonBody$1(req, maxBodyBytes) {
17
+ return new Promise((resolveBody) => {
18
+ let receivedBytes = 0;
19
+ let settled = false;
20
+ const chunks = [];
21
+ const settle = (result) => {
22
+ if (settled) return;
23
+ settled = true;
24
+ resolveBody(result);
25
+ };
26
+ req.on("data", (chunk) => {
27
+ if (settled) return;
28
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
29
+ receivedBytes += buffer.length;
30
+ if (receivedBytes > maxBodyBytes) {
31
+ settle({
32
+ ok: false,
33
+ reason: "too_large"
34
+ });
35
+ return;
36
+ }
37
+ chunks.push(buffer);
38
+ });
39
+ req.on("end", () => {
40
+ if (settled) return;
41
+ if (chunks.length === 0) {
42
+ settle({
43
+ ok: false,
44
+ reason: "empty"
45
+ });
46
+ return;
47
+ }
48
+ try {
49
+ settle({
50
+ ok: true,
51
+ value: JSON.parse(Buffer.concat(chunks).toString("utf8"))
52
+ });
53
+ } catch {
54
+ settle({
55
+ ok: false,
56
+ reason: "invalid_json"
57
+ });
58
+ }
59
+ });
60
+ req.on("error", () => settle({
61
+ ok: false,
62
+ reason: "read_error"
63
+ }));
64
+ });
65
+ }
66
+ function writeJson$1(res, statusCode, value) {
67
+ res.writeHead(statusCode, { "content-type": "application/json; charset=utf-8" });
68
+ res.end(JSON.stringify(value));
69
+ }
70
+ function browserClientScript(endpoint) {
71
+ return `
72
+ (() => {
73
+ if (globalThis.__lovableErrorCollectorInstalled) return;
74
+ globalThis.__lovableErrorCollectorInstalled = true;
75
+ const endpoint = ${JSON.stringify(endpoint).replaceAll("<", "\\u003c")};
76
+ const clip = (value) => String(value ?? "").slice(0, 10000);
77
+ const describe = (value, fallback) => {
78
+ if (value && typeof value === "object") {
79
+ const message = "message" in value ? clip(value.message) : "";
80
+ const stack = "stack" in value ? clip(value.stack) : "";
81
+ const name = "name" in value ? clip(value.name) : "";
82
+ if (message || stack || name) return { name: name || undefined, message: message || fallback, stack: stack || undefined };
83
+ try {
84
+ const seen = new WeakSet();
85
+ const json = JSON.stringify(value, (_key, nested) => {
86
+ if (typeof nested === "bigint") return String(nested);
87
+ if (nested && typeof nested === "object") {
88
+ if (seen.has(nested)) return "[Circular]";
89
+ seen.add(nested);
90
+ }
91
+ return nested;
92
+ });
93
+ if (json) return { message: clip(json) };
94
+ } catch {}
95
+ }
96
+ return { message: value == null ? fallback : clip(value) };
97
+ };
98
+ const post = (report) => {
99
+ try {
100
+ const body = JSON.stringify({
101
+ ...report,
102
+ timestamp: new Date().toISOString(),
103
+ pageUrl: location.href,
104
+ userAgent: navigator.userAgent,
105
+ });
106
+ void fetch(endpoint, {
107
+ method: "POST",
108
+ headers: { "content-type": "application/json" },
109
+ body,
110
+ keepalive: true,
111
+ }).catch(() => {});
112
+ } catch {}
113
+ };
114
+ addEventListener("error", (event) => {
115
+ const target = event.target;
116
+ if (target && target !== globalThis && typeof target === "object" && "tagName" in target) {
117
+ const tagName = clip(target.tagName).toLowerCase();
118
+ const resourceUrl = clip(target.currentSrc || target.src || target.href || "");
119
+ post({
120
+ type: "resource-error",
121
+ message: resourceUrl ? \`Failed to load <\${tagName}>: \${resourceUrl}\` : \`Failed to load <\${tagName}>\`,
122
+ tagName,
123
+ resourceUrl: resourceUrl || undefined,
124
+ });
125
+ return;
126
+ }
127
+ post({
128
+ type: "runtime-error",
129
+ ...describe(event.error || event.message, "Unknown runtime error"),
130
+ filename: clip(event.filename) || undefined,
131
+ line: Number.isFinite(event.lineno) ? event.lineno : undefined,
132
+ column: Number.isFinite(event.colno) ? event.colno : undefined,
133
+ });
134
+ }, true);
135
+ addEventListener("unhandledrejection", (event) => {
136
+ post({
137
+ type: "unhandled-rejection",
138
+ ...describe(event.reason, "Unhandled promise rejection"),
139
+ });
140
+ });
141
+ })();
142
+ `;
143
+ }
144
+ function createErrorCollectorHooks(options = {}) {
145
+ const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;
146
+ const outputFile = options.outputFile ?? DEFAULT_OUTPUT_FILE;
147
+ const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
148
+ const resetOnStart = options.resetOnStart ?? true;
149
+ validateEndpoint(endpoint);
150
+ validateMaxBodyBytes(maxBodyBytes);
151
+ const clientScript = browserClientScript(endpoint);
152
+ let root = process.cwd();
153
+ let outputPath = resolve(root, outputFile);
154
+ return {
155
+ configResolved: (config) => {
156
+ root = config.root;
157
+ outputPath = resolve(root, outputFile);
158
+ },
159
+ configureServer: (server) => {
160
+ Promise.resolve(server.watcher.unwatch(outputPath)).catch(() => {});
161
+ const initialize = mkdir(dirname(outputPath), { recursive: true }).then(async () => {
162
+ if (resetOnStart) await writeFile(outputPath, "", "utf8");
163
+ });
164
+ initialize.catch(() => {});
165
+ let writeQueue = Promise.resolve();
166
+ server.middlewares.use((req, res, next) => {
167
+ const path = (req.url ?? "").split("?", 1)[0];
168
+ if (req.method !== "POST" || path !== endpoint) return next();
169
+ readJsonBody$1(req, maxBodyBytes).then(async (parsed) => {
170
+ if (!parsed.ok) {
171
+ writeJson$1(res, parsed.reason === "too_large" ? 413 : 400, { error: parsed.reason });
172
+ return;
173
+ }
174
+ if (!parsed.value || typeof parsed.value !== "object" || Array.isArray(parsed.value)) {
175
+ writeJson$1(res, 400, { error: "body_must_be_an_object" });
176
+ return;
177
+ }
178
+ const line = `${JSON.stringify({
179
+ ...parsed.value,
180
+ receivedAt: (/* @__PURE__ */ new Date()).toISOString()
181
+ })}\n`;
182
+ const write = writeQueue.then(async () => {
183
+ await initialize;
184
+ await appendFile(outputPath, line, "utf8");
185
+ });
186
+ writeQueue = write.catch(() => {});
187
+ try {
188
+ await write;
189
+ res.writeHead(204);
190
+ res.end();
191
+ } catch (error) {
192
+ server.config.logger.error(`Failed to write browser error to ${outputPath}: ${String(error)}`);
193
+ writeJson$1(res, 500, { error: "write_failed" });
194
+ }
195
+ });
196
+ });
197
+ },
198
+ transform: (code, id, transformOptions) => {
199
+ if (transformOptions?.ssr || !isViteClientCarrier(id)) return null;
200
+ return {
201
+ code: `${code}\n${clientScript}\n`,
202
+ map: null
203
+ };
204
+ },
205
+ transformIndexHtml: () => {
206
+ return [{
207
+ tag: "script",
208
+ children: clientScript,
209
+ injectTo: "head-prepend"
210
+ }];
211
+ }
212
+ };
213
+ }
214
+ function errorCollectorPlugin(options = {}) {
215
+ const hooks = createErrorCollectorHooks(options);
216
+ return {
217
+ name: "error-collector",
218
+ apply: "serve",
219
+ configResolved(config) {
220
+ hooks.configResolved(config);
221
+ },
222
+ configureServer(server) {
223
+ hooks.configureServer(server);
224
+ },
225
+ transform(code, id, transformOptions) {
226
+ return hooks.transform(code, id, transformOptions);
227
+ },
228
+ transformIndexHtml() {
229
+ return hooks.transformIndexHtml();
230
+ }
231
+ };
232
+ }
233
+ function createErrorReportAppender(options = {}) {
234
+ const outputPath = resolve(process.cwd(), options.outputFile ?? DEFAULT_OUTPUT_FILE);
235
+ let initialized;
236
+ let writeQueue = Promise.resolve();
237
+ return (report) => {
238
+ initialized ??= mkdir(dirname(outputPath), { recursive: true }).then(() => void 0);
239
+ const write = writeQueue.then(async () => {
240
+ const line = `${JSON.stringify({
241
+ ...report,
242
+ receivedAt: (/* @__PURE__ */ new Date()).toISOString()
243
+ })}\n`;
244
+ await initialized;
245
+ await appendFile(outputPath, line, "utf8");
246
+ }).catch(() => void 0);
247
+ writeQueue = write;
248
+ return write;
249
+ };
250
+ }
251
+ //#endregion
252
+ //#region src/dev-server-bridge/index.ts
253
+ const RESTART_ANNOUNCE_PATH = "/__lovable/restart-announce";
254
+ const RESTART_EVENT = "lovable:dev-server-restarting";
255
+ const PREVIEW_EXECUTE_PATH = "/_sandbox/preview/execute";
256
+ const PREVIEW_VIEWERS_PATH = "/_sandbox/preview/viewers";
257
+ const PREVIEW_EXECUTE_EVENT = "lovable:preview-execute";
258
+ const PREVIEW_EXECUTE_RESULT_EVENT = "lovable:preview-execute-result";
259
+ const PREVIEW_EXECUTE_CLIENT_MODULE_ID = "virtual:lovable-preview-execute-client";
260
+ const RESOLVED_PREVIEW_EXECUTE_CLIENT_MODULE_ID = "\0virtual:lovable-preview-execute-client";
261
+ const RESTART_MAX_BODY_BYTES = 1024;
262
+ const PREVIEW_EXECUTE_MAX_BODY_BYTES = 64 * 1024;
263
+ const PREVIEW_EXECUTE_MAX_CODE_LENGTH = 50 * 1024;
264
+ const PREVIEW_EXECUTE_DEFAULT_TIMEOUT_MS = 1500;
265
+ const PREVIEW_EXECUTE_MAX_TIMEOUT_MS = 5e3;
266
+ let requestSequence = 0;
267
+ function nextRequestID() {
268
+ requestSequence += 1;
269
+ return `preview-exec-${Date.now().toString(36)}-${requestSequence.toString(36)}`;
270
+ }
271
+ function readJsonBody(req, maxBodyBytes) {
272
+ return new Promise((resolve) => {
273
+ let received = 0;
274
+ const chunks = [];
275
+ let aborted = false;
276
+ req.on("data", (chunk) => {
277
+ if (aborted) return;
278
+ received += chunk.length;
279
+ if (received > maxBodyBytes) {
280
+ aborted = true;
281
+ resolve({
282
+ ok: false,
283
+ reason: "too_large"
284
+ });
285
+ return;
286
+ }
287
+ chunks.push(chunk);
288
+ });
289
+ req.on("end", () => {
290
+ if (aborted) return;
291
+ if (chunks.length === 0) {
292
+ resolve({
293
+ ok: false,
294
+ reason: "empty"
295
+ });
296
+ return;
297
+ }
298
+ try {
299
+ resolve({
300
+ ok: true,
301
+ value: JSON.parse(Buffer.concat(chunks).toString("utf8"))
302
+ });
303
+ } catch {
304
+ resolve({
305
+ ok: false,
306
+ reason: "invalid_json"
307
+ });
308
+ }
309
+ });
310
+ req.on("error", () => resolve({
311
+ ok: false,
312
+ reason: "read_error"
313
+ }));
314
+ });
315
+ }
316
+ function writeJson(res, statusCode, value) {
317
+ res.writeHead(statusCode, { "content-type": "application/json; charset=utf-8" });
318
+ res.end(JSON.stringify(value));
319
+ }
320
+ function clampTimeoutMs(value) {
321
+ if (typeof value !== "number" || !Number.isFinite(value)) return PREVIEW_EXECUTE_DEFAULT_TIMEOUT_MS;
322
+ return Math.max(1, Math.min(PREVIEW_EXECUTE_MAX_TIMEOUT_MS, Math.trunc(value)));
323
+ }
324
+ function parsePreviewExecuteRequest(value) {
325
+ if (!value || typeof value !== "object") return "body must be a JSON object";
326
+ const code = value.code;
327
+ if (typeof code !== "string" || code.trim() === "") return "code must be a non-empty string";
328
+ if (code.length > PREVIEW_EXECUTE_MAX_CODE_LENGTH) return `code must be at most ${PREVIEW_EXECUTE_MAX_CODE_LENGTH} characters`;
329
+ return {
330
+ code,
331
+ timeout_ms: clampTimeoutMs(value.timeout_ms)
332
+ };
333
+ }
334
+ function isPreviewExecuteResult(value) {
335
+ if (!value || typeof value !== "object") return false;
336
+ const result = value;
337
+ return typeof result.request_id === "string" && typeof result.viewer_id === "string" && typeof result.ok === "boolean" && typeof result.duration_ms === "number";
338
+ }
339
+ function executeInViewers(ws, request, pendingRequests) {
340
+ const requestID = nextRequestID();
341
+ const viewerCount = ws.clients?.size ?? 0;
342
+ const results = /* @__PURE__ */ new Map();
343
+ pendingRequests.set(requestID, results);
344
+ if (viewerCount === 0) {
345
+ pendingRequests.delete(requestID);
346
+ return Promise.resolve({
347
+ request_id: requestID,
348
+ viewer_count: 0,
349
+ responded_count: 0,
350
+ timed_out_count: 0,
351
+ results: []
352
+ });
353
+ }
354
+ ws.send({
355
+ type: "custom",
356
+ event: PREVIEW_EXECUTE_EVENT,
357
+ data: {
358
+ request_id: requestID,
359
+ code: request.code
360
+ }
361
+ });
362
+ return new Promise((resolve) => {
363
+ let done = false;
364
+ let timeout;
365
+ let checkComplete;
366
+ const finish = () => {
367
+ if (done) return;
368
+ done = true;
369
+ clearTimeout(timeout);
370
+ clearInterval(checkComplete);
371
+ pendingRequests.delete(requestID);
372
+ resolve({
373
+ request_id: requestID,
374
+ viewer_count: viewerCount,
375
+ responded_count: results.size,
376
+ timed_out_count: Math.max(0, viewerCount - results.size),
377
+ results: [...results.values()]
378
+ });
379
+ };
380
+ timeout = setTimeout(finish, request.timeout_ms);
381
+ checkComplete = setInterval(() => {
382
+ if (results.size >= viewerCount) finish();
383
+ }, 10);
384
+ });
385
+ }
386
+ function previewExecuteClientScript() {
387
+ return `
388
+ if (import.meta.hot) {
389
+ // Two carriers can instantiate this module in one page, and Vite's stale-listener
390
+ // cleanup in createHotContext can silently drop an earlier instance's listeners —
391
+ // so every instance registers, and requests dedupe through globalThis state.
392
+ // Served untranspiled: keep the syntax at ES2020 (no ??=).
393
+ const viewerID =
394
+ globalThis.__lovablePreviewExecuteViewerID ||
395
+ (globalThis.__lovablePreviewExecuteViewerID =
396
+ globalThis.crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2));
397
+ const handled =
398
+ globalThis.__lovablePreviewExecuteHandled || (globalThis.__lovablePreviewExecuteHandled = new Set());
399
+ const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
400
+ const serialize = (value) => {
401
+ if (value === undefined) return { type: "undefined" };
402
+ try {
403
+ return structuredClone(value);
404
+ } catch {
405
+ try {
406
+ return JSON.parse(JSON.stringify(value));
407
+ } catch {
408
+ return String(value);
409
+ }
410
+ }
411
+ };
412
+ import.meta.hot.on("${PREVIEW_EXECUTE_EVENT}", async (payload) => {
413
+ const started = performance.now();
414
+ const requestID = payload && typeof payload.request_id === "string" ? payload.request_id : "";
415
+ const code = payload && typeof payload.code === "string" ? payload.code : "";
416
+ if (!requestID || !code || handled.has(requestID)) return;
417
+ // Request ids embed a timestamp + sequence and are never reused, so clearing
418
+ // at the cap cannot resurrect an old request; it only bounds realm lifetime growth.
419
+ if (handled.size >= 10000) handled.clear();
420
+ handled.add(requestID);
421
+ try {
422
+ const value = await new AsyncFunction(code)();
423
+ import.meta.hot.send("${PREVIEW_EXECUTE_RESULT_EVENT}", {
424
+ request_id: requestID,
425
+ viewer_id: viewerID,
426
+ ok: true,
427
+ value: serialize(value),
428
+ duration_ms: Math.round(performance.now() - started),
429
+ });
430
+ } catch (err) {
431
+ import.meta.hot.send("${PREVIEW_EXECUTE_RESULT_EVENT}", {
432
+ request_id: requestID,
433
+ viewer_id: viewerID,
434
+ ok: false,
435
+ error: {
436
+ name: err && typeof err === "object" && "name" in err ? String(err.name) : undefined,
437
+ message: err instanceof Error ? err.message : String(err),
438
+ stack: err instanceof Error ? err.stack : undefined,
439
+ },
440
+ duration_ms: Math.round(performance.now() - started),
441
+ });
442
+ }
443
+ });
444
+ }
445
+ `;
446
+ }
447
+ // @vitejs/plugin-react and vitejs/devtools use — see vitejs/vite#22864. Gated to
448
+ function isPreviewExecuteClientCarrier(id) {
449
+ return id.includes("/vite/dist/client/client.mjs");
450
+ }
451
+ function devServerBridgePlugin(options = {}) {
452
+ let isBundledDev = false;
453
+ const errorCollector = options.errorCollector === false ? void 0 : createErrorCollectorHooks(options.errorCollector);
454
+ const injectClientCarriers = () => {
455
+ const tags = errorCollector?.transformIndexHtml() ?? [];
456
+ if (isBundledDev) tags.push({
457
+ tag: "script",
458
+ attrs: { type: "module" },
459
+ children: `import ${JSON.stringify(PREVIEW_EXECUTE_CLIENT_MODULE_ID)};`,
460
+ injectTo: "head"
461
+ });
462
+ return tags.length > 0 ? tags : void 0;
463
+ };
464
+ return {
465
+ name: "lovable-dev-server-bridge",
466
+ apply: "serve",
467
+ configResolved(config) {
468
+ errorCollector?.configResolved(config);
469
+ isBundledDev = config.experimental?.bundledDev === true;
470
+ },
471
+ configureServer(srv) {
472
+ errorCollector?.configureServer(srv);
473
+ const pendingPreviewExecuteRequests = /* @__PURE__ */ new Map();
474
+ const ws = srv.ws;
475
+ let connectionSeq = 0;
476
+ ws.on?.("connection", () => {
477
+ connectionSeq += 1;
478
+ });
479
+ ws.on?.(PREVIEW_EXECUTE_RESULT_EVENT, (data, client) => {
480
+ if (!isPreviewExecuteResult(data)) return;
481
+ const results = pendingPreviewExecuteRequests.get(data.request_id);
482
+ if (!results) return;
483
+ results.set(client ?? data.viewer_id, data);
484
+ });
485
+ srv.middlewares.use((req, res, next) => {
486
+ const path = (req.url ?? "").split("?", 1)[0];
487
+ if (req.method === "GET" && path === PREVIEW_VIEWERS_PATH) {
488
+ writeJson(res, 200, {
489
+ viewer_count: ws.clients?.size ?? 0,
490
+ connection_seq: connectionSeq
491
+ });
492
+ return;
493
+ }
494
+ if (req.method !== "POST") return next();
495
+ if (path !== RESTART_ANNOUNCE_PATH && path !== PREVIEW_EXECUTE_PATH) return next();
496
+ readJsonBody(req, path === PREVIEW_EXECUTE_PATH ? PREVIEW_EXECUTE_MAX_BODY_BYTES : RESTART_MAX_BODY_BYTES).then((parsed) => {
497
+ if (path === PREVIEW_EXECUTE_PATH) {
498
+ if (!parsed.ok) {
499
+ writeJson(res, parsed.reason === "too_large" ? 413 : 400, { error: parsed.reason });
500
+ return;
501
+ }
502
+ const previewRequest = parsePreviewExecuteRequest(parsed.value);
503
+ if (typeof previewRequest === "string") {
504
+ writeJson(res, 400, { error: previewRequest });
505
+ return;
506
+ }
507
+ executeInViewers(ws, previewRequest, pendingPreviewExecuteRequests).then((response) => {
508
+ writeJson(res, 200, response);
509
+ });
510
+ return;
511
+ }
512
+ const reason = parsed.ok && parsed.value && typeof parsed.value === "object" && "reason" in parsed.value && typeof parsed.value.reason === "string" ? parsed.value.reason : void 0;
513
+ srv.ws.send({
514
+ type: "custom",
515
+ event: RESTART_EVENT,
516
+ data: reason ? { reason } : {}
517
+ });
518
+ res.writeHead(204);
519
+ res.end();
520
+ });
521
+ });
522
+ },
523
+ resolveId(id) {
524
+ if (id === PREVIEW_EXECUTE_CLIENT_MODULE_ID) return RESOLVED_PREVIEW_EXECUTE_CLIENT_MODULE_ID;
525
+ return null;
526
+ },
527
+ load(id) {
528
+ if (id === RESOLVED_PREVIEW_EXECUTE_CLIENT_MODULE_ID) return previewExecuteClientScript();
529
+ return null;
530
+ },
531
+ transform(code, id, options) {
532
+ const errorCollectorTransform = errorCollector?.transform(code, id, options);
533
+ if (options?.ssr || !isPreviewExecuteClientCarrier(id)) return errorCollectorTransform ?? null;
534
+ return {
535
+ code: `${errorCollectorTransform?.code ?? code}\nimport(${JSON.stringify(PREVIEW_EXECUTE_CLIENT_MODULE_ID)});\n`,
536
+ map: null
537
+ };
538
+ },
539
+ transformIndexHtml: {
540
+ order: "pre",
541
+ handler: injectClientCarriers,
542
+ enforce: "pre",
543
+ transform: injectClientCarriers
544
+ }
545
+ };
546
+ }
547
+ //#endregion
548
+ export { createErrorReportAppender, devServerBridgePlugin, errorCollectorPlugin };