@lovable.dev/vite-tanstack-config 2.17.0 → 2.18.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/dist/hmr-gate.cjs CHANGED
@@ -8,6 +8,24 @@ function requestPathname(req) {
8
8
  const queryStart = url.indexOf("?");
9
9
  return queryStart === -1 ? url : url.slice(0, queryStart);
10
10
  }
11
+ const nonNavigationPrefixes = [
12
+ "/@vite/",
13
+ "/@fs/",
14
+ "/@id/",
15
+ "/@react-refresh",
16
+ "/api/",
17
+ "/go-api/",
18
+ "/assets/",
19
+ "/src/",
20
+ "/_",
21
+ "/favicon"
22
+ ];
23
+ function isHtmlNavigationRequest(req) {
24
+ if (req.method !== "GET") return false;
25
+ const url = req.url;
26
+ if (!url || nonNavigationPrefixes.some((prefix) => url.startsWith(prefix))) return false;
27
+ return (req.headers.accept ?? "").includes("text/html");
28
+ }
11
29
  function gateStatusHeaders(req) {
12
30
  const origin = req.headers.origin;
13
31
  const headers = {
@@ -233,6 +251,7 @@ function hmrGatePlugin(options = {}) {
233
251
  lastUpdateKey = "";
234
252
  }
235
253
  let classicFailOpenTimer = null;
254
+ let errorServedSinceFlush = false;
236
255
  let transportMode = false;
237
256
  let swallowedUpdates = 0;
238
257
  let heldReload = false;
@@ -420,6 +439,12 @@ function hmrGatePlugin(options = {}) {
420
439
  });
421
440
  srv.middlewares.use((req, res, next) => {
422
441
  if (req.method !== "POST" || requestPathname(req) !== "/__hmr_flush") return next();
442
+ const flushOrigin = req.headers.origin;
443
+ if (typeof flushOrigin === "string" && !statusCorsOriginRe.test(flushOrigin)) {
444
+ res.writeHead(403, { "Content-Type": "application/json" });
445
+ res.end(JSON.stringify({ error: "origin not allowed" }));
446
+ return;
447
+ }
423
448
  const priorDrainUntil = drainUntil;
424
449
  const released = releaseParked();
425
450
  drainUntil = Date.now() + drainMs;
@@ -494,6 +519,23 @@ function hmrGatePlugin(options = {}) {
494
519
  });
495
520
  return;
496
521
  }
522
+ srv.middlewares.use((req, res, next) => {
523
+ const originalEnd = res.end.bind(res);
524
+ res.end = ((...args) => {
525
+ if (res.statusCode >= 500 && isHtmlNavigationRequest(req)) {
526
+ errorServedSinceFlush = true;
527
+ if (classicFailOpenTimer === null) {
528
+ classicFailOpenTimer = setTimeout(() => {
529
+ classicFailOpenTimer = null;
530
+ flushPending();
531
+ }, maxHoldMs);
532
+ classicFailOpenTimer.unref?.();
533
+ }
534
+ }
535
+ return originalEnd(...args);
536
+ });
537
+ next();
538
+ });
497
539
  const _emit = srv.watcher.emit.bind(srv.watcher);
498
540
  originalEmit = _emit;
499
541
  const flushPending = () => {
@@ -502,6 +544,9 @@ function hmrGatePlugin(options = {}) {
502
544
  classicFailOpenTimer = null;
503
545
  }
504
546
  if (pending.size > 0) noteUpdateFlush();
547
+ const errorServed = errorServedSinceFlush;
548
+ errorServedSinceFlush = false;
549
+ if (errorServed) srv.moduleGraph.invalidateAll();
505
550
  const entries = Array.from(pending.entries());
506
551
  pending.clear();
507
552
  for (const [f] of entries) srv.moduleGraph.onFileChange(f);
@@ -514,7 +559,11 @@ function hmrGatePlugin(options = {}) {
514
559
  }
515
560
  if (fullReload) srv.ws.send({ type: "full-reload" });
516
561
  }
517
- return entries;
562
+ if (errorServed && (entries.length === 0 || !fullReload)) srv.ws.send({ type: "full-reload" });
563
+ return {
564
+ entries,
565
+ errorRecovery: errorServed
566
+ };
518
567
  };
519
568
  srv.watcher.emit = ((event, ...args) => {
520
569
  if (typeof event === "string" && gatedEvents.has(event) && !isPassthrough(args[0])) {
@@ -523,7 +572,7 @@ function hmrGatePlugin(options = {}) {
523
572
  const hash = fileContentHash(filePath);
524
573
  if (hash !== null && hash === lastFlushedHashes.get(filePath)) {
525
574
  pending.delete(filePath);
526
- if (pending.size === 0 && classicFailOpenTimer !== null) {
575
+ if (pending.size === 0 && !errorServedSinceFlush && classicFailOpenTimer !== null) {
527
576
  clearTimeout(classicFailOpenTimer);
528
577
  classicFailOpenTimer = null;
529
578
  }
@@ -550,11 +599,18 @@ function hmrGatePlugin(options = {}) {
550
599
  });
551
600
  srv.middlewares.use((req, res, next) => {
552
601
  if (req.method !== "POST" || requestPathname(req) !== "/__hmr_flush") return next();
553
- const entries = flushPending();
602
+ const origin = req.headers.origin;
603
+ if (typeof origin === "string" && !statusCorsOriginRe.test(origin)) {
604
+ res.writeHead(403, { "Content-Type": "application/json" });
605
+ res.end(JSON.stringify({ error: "origin not allowed" }));
606
+ return;
607
+ }
608
+ const { entries, errorRecovery } = flushPending();
554
609
  const files = entries.map(([f]) => f);
555
610
  const body = JSON.stringify({
556
611
  flushed: files,
557
612
  count: entries.length,
613
+ errorRecovery,
558
614
  mode: fullReload ? "full-reload" : "granular"
559
615
  });
560
616
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -567,7 +623,8 @@ function hmrGatePlugin(options = {}) {
567
623
  const body = JSON.stringify({
568
624
  enabled: true,
569
625
  pending: serialized,
570
- count: pending.size,
626
+ count: pending.size + (errorServedSinceFlush ? 1 : 0),
627
+ errorRecovery: errorServedSinceFlush,
571
628
  mode: fullReload ? "full-reload" : "granular"
572
629
  });
573
630
  res.writeHead(200, gateStatusHeaders(req));
@@ -609,6 +666,7 @@ function hmrGatePlugin(options = {}) {
609
666
  drainUntil = 0;
610
667
  pending.clear();
611
668
  lastFlushedHashes.clear();
669
+ errorServedSinceFlush = false;
612
670
  server = null;
613
671
  originalEmit = null;
614
672
  }
package/dist/hmr-gate.js CHANGED
@@ -7,6 +7,24 @@ function requestPathname(req) {
7
7
  const queryStart = url.indexOf("?");
8
8
  return queryStart === -1 ? url : url.slice(0, queryStart);
9
9
  }
10
+ const nonNavigationPrefixes = [
11
+ "/@vite/",
12
+ "/@fs/",
13
+ "/@id/",
14
+ "/@react-refresh",
15
+ "/api/",
16
+ "/go-api/",
17
+ "/assets/",
18
+ "/src/",
19
+ "/_",
20
+ "/favicon"
21
+ ];
22
+ function isHtmlNavigationRequest(req) {
23
+ if (req.method !== "GET") return false;
24
+ const url = req.url;
25
+ if (!url || nonNavigationPrefixes.some((prefix) => url.startsWith(prefix))) return false;
26
+ return (req.headers.accept ?? "").includes("text/html");
27
+ }
10
28
  function gateStatusHeaders(req) {
11
29
  const origin = req.headers.origin;
12
30
  const headers = {
@@ -232,6 +250,7 @@ function hmrGatePlugin(options = {}) {
232
250
  lastUpdateKey = "";
233
251
  }
234
252
  let classicFailOpenTimer = null;
253
+ let errorServedSinceFlush = false;
235
254
  let transportMode = false;
236
255
  let swallowedUpdates = 0;
237
256
  let heldReload = false;
@@ -419,6 +438,12 @@ function hmrGatePlugin(options = {}) {
419
438
  });
420
439
  srv.middlewares.use((req, res, next) => {
421
440
  if (req.method !== "POST" || requestPathname(req) !== "/__hmr_flush") return next();
441
+ const flushOrigin = req.headers.origin;
442
+ if (typeof flushOrigin === "string" && !statusCorsOriginRe.test(flushOrigin)) {
443
+ res.writeHead(403, { "Content-Type": "application/json" });
444
+ res.end(JSON.stringify({ error: "origin not allowed" }));
445
+ return;
446
+ }
422
447
  const priorDrainUntil = drainUntil;
423
448
  const released = releaseParked();
424
449
  drainUntil = Date.now() + drainMs;
@@ -493,6 +518,23 @@ function hmrGatePlugin(options = {}) {
493
518
  });
494
519
  return;
495
520
  }
521
+ srv.middlewares.use((req, res, next) => {
522
+ const originalEnd = res.end.bind(res);
523
+ res.end = ((...args) => {
524
+ if (res.statusCode >= 500 && isHtmlNavigationRequest(req)) {
525
+ errorServedSinceFlush = true;
526
+ if (classicFailOpenTimer === null) {
527
+ classicFailOpenTimer = setTimeout(() => {
528
+ classicFailOpenTimer = null;
529
+ flushPending();
530
+ }, maxHoldMs);
531
+ classicFailOpenTimer.unref?.();
532
+ }
533
+ }
534
+ return originalEnd(...args);
535
+ });
536
+ next();
537
+ });
496
538
  const _emit = srv.watcher.emit.bind(srv.watcher);
497
539
  originalEmit = _emit;
498
540
  const flushPending = () => {
@@ -501,6 +543,9 @@ function hmrGatePlugin(options = {}) {
501
543
  classicFailOpenTimer = null;
502
544
  }
503
545
  if (pending.size > 0) noteUpdateFlush();
546
+ const errorServed = errorServedSinceFlush;
547
+ errorServedSinceFlush = false;
548
+ if (errorServed) srv.moduleGraph.invalidateAll();
504
549
  const entries = Array.from(pending.entries());
505
550
  pending.clear();
506
551
  for (const [f] of entries) srv.moduleGraph.onFileChange(f);
@@ -513,7 +558,11 @@ function hmrGatePlugin(options = {}) {
513
558
  }
514
559
  if (fullReload) srv.ws.send({ type: "full-reload" });
515
560
  }
516
- return entries;
561
+ if (errorServed && (entries.length === 0 || !fullReload)) srv.ws.send({ type: "full-reload" });
562
+ return {
563
+ entries,
564
+ errorRecovery: errorServed
565
+ };
517
566
  };
518
567
  srv.watcher.emit = ((event, ...args) => {
519
568
  if (typeof event === "string" && gatedEvents.has(event) && !isPassthrough(args[0])) {
@@ -522,7 +571,7 @@ function hmrGatePlugin(options = {}) {
522
571
  const hash = fileContentHash(filePath);
523
572
  if (hash !== null && hash === lastFlushedHashes.get(filePath)) {
524
573
  pending.delete(filePath);
525
- if (pending.size === 0 && classicFailOpenTimer !== null) {
574
+ if (pending.size === 0 && !errorServedSinceFlush && classicFailOpenTimer !== null) {
526
575
  clearTimeout(classicFailOpenTimer);
527
576
  classicFailOpenTimer = null;
528
577
  }
@@ -549,11 +598,18 @@ function hmrGatePlugin(options = {}) {
549
598
  });
550
599
  srv.middlewares.use((req, res, next) => {
551
600
  if (req.method !== "POST" || requestPathname(req) !== "/__hmr_flush") return next();
552
- const entries = flushPending();
601
+ const origin = req.headers.origin;
602
+ if (typeof origin === "string" && !statusCorsOriginRe.test(origin)) {
603
+ res.writeHead(403, { "Content-Type": "application/json" });
604
+ res.end(JSON.stringify({ error: "origin not allowed" }));
605
+ return;
606
+ }
607
+ const { entries, errorRecovery } = flushPending();
553
608
  const files = entries.map(([f]) => f);
554
609
  const body = JSON.stringify({
555
610
  flushed: files,
556
611
  count: entries.length,
612
+ errorRecovery,
557
613
  mode: fullReload ? "full-reload" : "granular"
558
614
  });
559
615
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -566,7 +622,8 @@ function hmrGatePlugin(options = {}) {
566
622
  const body = JSON.stringify({
567
623
  enabled: true,
568
624
  pending: serialized,
569
- count: pending.size,
625
+ count: pending.size + (errorServedSinceFlush ? 1 : 0),
626
+ errorRecovery: errorServedSinceFlush,
570
627
  mode: fullReload ? "full-reload" : "granular"
571
628
  });
572
629
  res.writeHead(200, gateStatusHeaders(req));
@@ -608,6 +665,7 @@ function hmrGatePlugin(options = {}) {
608
665
  drainUntil = 0;
609
666
  pending.clear();
610
667
  lastFlushedHashes.clear();
668
+ errorServedSinceFlush = false;
611
669
  server = null;
612
670
  originalEmit = null;
613
671
  }
package/dist/index.cjs CHANGED
@@ -150,6 +150,186 @@ function bundledDevCssUrlShim() {
150
150
  };
151
151
  }
152
152
  //#endregion
153
+ //#region package.json
154
+ var name = "@lovable.dev/vite-tanstack-config";
155
+ var version = "2.18.0";
156
+ //#endregion
157
+ //#region src/logging.ts
158
+ const PACKAGE_NAME = "@lovable.dev/vite-tanstack-config";
159
+ function logWarning(message) {
160
+ console.warn(`[${PACKAGE_NAME}] ${message}`);
161
+ }
162
+ //#endregion
163
+ //#region src/outputPaths.ts
164
+ const PUBLISH_OUTPUT_DIR = "dist";
165
+ const SERVER_OUTPUT_DIR = `${PUBLISH_OUTPUT_DIR}/server`;
166
+ const CLIENT_OUTPUT_DIR = `${PUBLISH_OUTPUT_DIR}/client`;
167
+ //#endregion
168
+ //#region src/documents.ts
169
+ const DOCUMENT_SET_VERSION = 1;
170
+ const GENERATOR = `${name}@${version}`;
171
+ const DOCUMENT_SET_DIR = ".lovable";
172
+ const DOCUMENT_SET_FILE = "output.json";
173
+ const DOCUMENT_SET_MAX_ENTRIES = 1e4;
174
+ const DOCUMENT_SET_MAX_BYTES = 4e6;
175
+ const DOCUMENT_SET_PLUGIN_NAME = "lovable:document-set";
176
+ const TANSTACK_POST_BUILD_PLUGIN = "tanstack-start-core:post-build";
177
+ const DOT_SEGMENT = /(^|\/)\.(\/|$)/;
178
+ function hasControlChars(value) {
179
+ for (let i = 0; i < value.length; i++) {
180
+ const code = value.charCodeAt(i);
181
+ if (code < 32 || code === 127) return true;
182
+ }
183
+ return false;
184
+ }
185
+ function normalizeRoute(raw) {
186
+ if (typeof raw !== "string") return void 0;
187
+ const pathname = raw.split(/[?#]/)[0] ?? "";
188
+ if (!pathname.startsWith("/")) return void 0;
189
+ const route = pathname.replace(/\/+$/, "") || "/";
190
+ if (route.includes("//") || route.includes("..") || route.includes("\\") || DOT_SEGMENT.test(route) || hasControlChars(route)) return;
191
+ return route;
192
+ }
193
+ /** Upstream's `joinURL("/", router.basepath)`: leading slash, no trailing slash, "" when unset. */
194
+ function normalizeBasePath(raw) {
195
+ if (typeof raw !== "string") return "";
196
+ const trimmed = raw.trim().replace(/\/+$/, "");
197
+ if (trimmed === "") return "";
198
+ return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
199
+ }
200
+ function withoutBasePath(route, basePath) {
201
+ if (route === basePath) return "/";
202
+ if (basePath === "") return route;
203
+ return route.startsWith(`${basePath}/`) ? route.slice(basePath.length) : route;
204
+ }
205
+ function documentEntryForPage(page, basePath = "") {
206
+ if (typeof page.prerender?.outputPath === "string") return void 0;
207
+ const pagePath = normalizeRoute(page.path);
208
+ if (pagePath === void 0) return void 0;
209
+ const inner = withoutBasePath(pagePath, basePath);
210
+ const route = normalizeRoute(`${basePath}${inner}`);
211
+ if (route === void 0) return void 0;
212
+ const file = inner === "/" ? "index.html" : `${inner.slice(1)}/index.html`;
213
+ if (file !== file.trim()) return void 0;
214
+ return {
215
+ route,
216
+ file
217
+ };
218
+ }
219
+ function compareRoutes(a, b) {
220
+ if (a.route < b.route) return -1;
221
+ if (a.route > b.route) return 1;
222
+ return 0;
223
+ }
224
+ /** The artifact bytes, or undefined when there is nothing to publish. */
225
+ function serializeDocumentSet(entries, generator) {
226
+ const sorted = [...entries].sort(compareRoutes);
227
+ const selected = [];
228
+ let cappedBy;
229
+ let length = JSON.stringify({
230
+ version: DOCUMENT_SET_VERSION,
231
+ generator,
232
+ documents: []
233
+ }).length;
234
+ for (const entry of sorted) {
235
+ if (selected.length === DOCUMENT_SET_MAX_ENTRIES) {
236
+ cappedBy = "entry limit";
237
+ break;
238
+ }
239
+ const added = JSON.stringify(entry).length + 1;
240
+ if (length + added > DOCUMENT_SET_MAX_BYTES) {
241
+ cappedBy = "byte limit";
242
+ break;
243
+ }
244
+ length += added;
245
+ selected.push(entry);
246
+ }
247
+ if (cappedBy !== void 0) logWarning(`Publishing only ${selected.length} of ${sorted.length} publishable routes; the document set hit its ${cappedBy}.`);
248
+ if (selected.length === 0) return void 0;
249
+ return JSON.stringify({
250
+ version: DOCUMENT_SET_VERSION,
251
+ generator,
252
+ documents: selected
253
+ });
254
+ }
255
+ const EXISTENCE_BATCH_SIZE = 64;
256
+ async function whichExist(paths, access) {
257
+ const exists = [];
258
+ for (let i = 0; i < paths.length; i += EXISTENCE_BATCH_SIZE) {
259
+ const batch = paths.slice(i, i + EXISTENCE_BATCH_SIZE).map((path) => access(path).then(() => true, () => false));
260
+ exists.push(...await Promise.all(batch));
261
+ }
262
+ return exists;
263
+ }
264
+ function createDocumentSet(routerBasePath) {
265
+ const basePath = normalizeBasePath(routerBasePath);
266
+ const basePathServable = basePath === "" || normalizeRoute(basePath) === basePath;
267
+ if (!basePathServable) logWarning(`Not publishing any prerendered documents: router.basepath "${basePath}" is not a servable route.`);
268
+ const documents = /* @__PURE__ */ new Map();
269
+ let root = process.cwd();
270
+ let clientOutputDir = CLIENT_OUTPUT_DIR;
271
+ const plugin = {
272
+ name: DOCUMENT_SET_PLUGIN_NAME,
273
+ apply: "build",
274
+ enforce: "post",
275
+ configResolved(config) {
276
+ root = config.root;
277
+ const names = config.plugins?.map((candidate) => candidate.name) ?? [];
278
+ const theirs = names.indexOf(TANSTACK_POST_BUILD_PLUGIN);
279
+ const ours = names.indexOf(DOCUMENT_SET_PLUGIN_NAME);
280
+ if (theirs !== -1 && ours !== -1 && ours < theirs) logWarning(`Prerendered routes will fall back to SSR: ${DOCUMENT_SET_PLUGIN_NAME} is registered before ${TANSTACK_POST_BUILD_PLUGIN}, so the document set is written before the prerender pass runs.`);
281
+ clientOutputDir = config.environments?.client?.build?.outDir ?? CLIENT_OUTPUT_DIR;
282
+ },
283
+ buildApp: {
284
+ order: "post",
285
+ async handler() {
286
+ const { access, mkdir, rm, writeFile } = await import("node:fs/promises");
287
+ const nodePath = await import("node:path");
288
+ const artifact = nodePath.join(root, PUBLISH_OUTPUT_DIR, DOCUMENT_SET_DIR, DOCUMENT_SET_FILE);
289
+ await rm(artifact, { force: true }).catch((error) => {
290
+ logWarning(`Could not remove a previous build's document set: ${String(error)}`);
291
+ });
292
+ if (documents.size === 0) return;
293
+ const clientDir = nodePath.resolve(root, clientOutputDir);
294
+ const entries = [...documents].map(([route, file]) => ({
295
+ route,
296
+ file
297
+ }));
298
+ const exists = await whichExist(entries.map(({ file }) => nodePath.resolve(clientDir, file)), access);
299
+ const present = [];
300
+ for (const [index, entry] of entries.entries()) {
301
+ if (exists[index]) {
302
+ present.push(entry);
303
+ continue;
304
+ }
305
+ logWarning(`Not publishing prerendered route ${entry.route}: it produced no ${entry.file}.`);
306
+ }
307
+ const payload = serializeDocumentSet(present, GENERATOR);
308
+ if (payload === void 0) return;
309
+ try {
310
+ await mkdir(nodePath.dirname(artifact), { recursive: true });
311
+ await writeFile(artifact, payload);
312
+ } catch (error) {
313
+ logWarning(`Could not write the document set; prerendered routes fall back to SSR: ${String(error)}`);
314
+ }
315
+ }
316
+ }
317
+ };
318
+ const record = (page) => {
319
+ if (!basePathServable) return;
320
+ const entry = documentEntryForPage(page, basePath);
321
+ if (entry === void 0) {
322
+ if (typeof page.prerender?.outputPath !== "string") logWarning(`Not publishing prerendered route ${String(page.path)}: its path cannot be a document key (dot segments, "//", "..", backslashes, control characters, and leading/trailing whitespace are rejected).`);
323
+ return;
324
+ }
325
+ documents.set(entry.route, entry.file);
326
+ };
327
+ return {
328
+ record,
329
+ plugin
330
+ };
331
+ }
332
+ //#endregion
153
333
  //#region src/index.ts
154
334
  const SANDBOX_ENV_VAR = "DEV_SERVER__PROJECT_PATH";
155
335
  const LOVABLE_SANDBOX_ENV_VAR = "LOVABLE_SANDBOX";
@@ -163,9 +343,6 @@ const TOOL_MANAGED_WATCH_IGNORES = [
163
343
  "**/.lovable/**",
164
344
  "**/.tanstack/tmp/**"
165
345
  ];
166
- function logWarning(message) {
167
- console.warn(`[@lovable.dev/vite-tanstack-config] ${message}`);
168
- }
169
346
  const NITRO_DEFAULT_PRESET_MIN = [
170
347
  3,
171
348
  0,
@@ -214,6 +391,7 @@ function pinPrerenderOptions(source, pins, label) {
214
391
  function pinPrerenderForLovableBuild(userOptions) {
215
392
  const attempted = /* @__PURE__ */ new Set();
216
393
  const rendered = /* @__PURE__ */ new Set();
394
+ const collector = createDocumentSet(userOptions.router?.basepath);
217
395
  const userPrerender = userOptions.prerender;
218
396
  const prerender = pinPrerenderOptions(userPrerender, ROOT_PRERENDER_PINS, "prerender");
219
397
  const userFilter = userPrerender?.filter;
@@ -224,6 +402,7 @@ function pinPrerenderForLovableBuild(userOptions) {
224
402
  };
225
403
  const withRenderTracking = (userOnSuccess) => (ctx) => {
226
404
  rendered.add(ctx.page.path);
405
+ collector.record(ctx.page);
227
406
  return typeof userOnSuccess === "function" ? userOnSuccess(ctx) : void 0;
228
407
  };
229
408
  prerender.onSuccess = withRenderTracking(userPrerender?.onSuccess);
@@ -256,7 +435,8 @@ function pinPrerenderForLovableBuild(userOptions) {
256
435
  logWarning(`Prerender skipped ${skipped.length} route(s) that failed to render; they fall back to SSR: ${skipped.join(", ")}`);
257
436
  }
258
437
  }
259
- }
438
+ },
439
+ documentSet: collector.plugin
260
440
  };
261
441
  }
262
442
  function lovableFetchBundlePreset() {
@@ -266,7 +446,7 @@ function lovableFetchBundlePreset() {
266
446
  noExternals: true,
267
447
  inlineDynamicImports: true,
268
448
  output: {
269
- dir: "dist",
449
+ dir: PUBLISH_OUTPUT_DIR,
270
450
  serverDir: "{{ output.dir }}/server",
271
451
  publicDir: "{{ output.dir }}/client"
272
452
  }
@@ -516,7 +696,7 @@ function lovableBuildErrorDiagnostics() {
516
696
  };
517
697
  }
518
698
  const NODEJS_COMPAT_DEFAULT_ON_DATE = "2026-08-04";
519
- const SANDBOX_WRANGLER_CONFIG_PATH = "dist/server/wrangler.json";
699
+ const SANDBOX_WRANGLER_CONFIG_PATH = `${SERVER_OUTPUT_DIR}/wrangler.json`;
520
700
  function stripRedundantNodejsCompatFlag() {
521
701
  let root = process.cwd();
522
702
  return {
@@ -553,7 +733,6 @@ function stripRedundantNodejsCompatFlag() {
553
733
  }
554
734
  const NITRO_SERVER_ENTRY = "index.mjs";
555
735
  const NITRO_WRANGLER_CONFIG = "wrangler.json";
556
- const DEFAULT_SERVER_OUTPUT_DIR = "dist/server";
557
736
  const DEFAULT_SERVER_ENTRY_NAME = "server";
558
737
  const PRERENDER_PREVIEW_HOST = "127.0.0.1";
559
738
  const TSS_PRERENDERING_ENV = "TSS_PRERENDERING";
@@ -577,7 +756,7 @@ export default {
577
756
  }
578
757
  function prerenderPreviewShim(state) {
579
758
  let root = process.cwd();
580
- let serverOutDir = DEFAULT_SERVER_OUTPUT_DIR;
759
+ let serverOutDir = SERVER_OUTPUT_DIR;
581
760
  let serverEntryName = DEFAULT_SERVER_ENTRY_NAME;
582
761
  return {
583
762
  name: "lovable-prerender-preview-shim",
@@ -608,7 +787,7 @@ function prerenderPreviewShim(state) {
608
787
  if (!await exists(nodePath.join(outDir, NITRO_SERVER_ENTRY))) return;
609
788
  const basename = nodePath.basename(serverEntryName, nodePath.extname(serverEntryName));
610
789
  if (!basename || basename === "index") {
611
- logWarning(`Cannot prerender: the server entry "${serverEntryName}" would need a shim at ${DEFAULT_SERVER_OUTPUT_DIR}/index.js, which outranks nitro's own entry when publishing. Rename the entry to anything but \`index\`.`);
790
+ logWarning(`Cannot prerender: the server entry "${serverEntryName}" would need a shim at ${SERVER_OUTPUT_DIR}/index.js, which outranks nitro's own entry when publishing. Rename the entry to anything but \`index\`.`);
612
791
  return;
613
792
  }
614
793
  const target = nodePath.join(outDir, `${basename}.js`);
@@ -833,8 +1012,8 @@ function defineConfig(configOrOptions = {}) {
833
1012
  } }, options.tanstackStart ?? {});
834
1013
  const prerenderEnabled = prerenderIsEnabled(tanstackStartOptions);
835
1014
  if (command === "build" && isSandbox) {
836
- const { startOptions, skipReporter } = pinPrerenderForLovableBuild(tanstackStartOptions);
837
- internalPlugins.push(tanstackStart(startOptions), skipReporter);
1015
+ const { startOptions, skipReporter, documentSet } = pinPrerenderForLovableBuild(tanstackStartOptions);
1016
+ internalPlugins.push(tanstackStart(startOptions), skipReporter, documentSet);
838
1017
  } else internalPlugins.push(tanstackStart(tanstackStartOptions));
839
1018
  const explicitNitro = options.nitro === true || typeof options.nitro === "object" && options.nitro !== null;
840
1019
  if (options.nitro !== false && command === "build") {
@@ -872,9 +1051,9 @@ function defineConfig(configOrOptions = {}) {
872
1051
  delete nitroOpts.defaultPreset;
873
1052
  nitroOpts.preset = "cloudflare-module";
874
1053
  nitroOpts.output = {
875
- dir: "dist",
876
- serverDir: "dist/server",
877
- publicDir: "dist/client"
1054
+ dir: PUBLISH_OUTPUT_DIR,
1055
+ serverDir: SERVER_OUTPUT_DIR,
1056
+ publicDir: CLIENT_OUTPUT_DIR
878
1057
  };
879
1058
  nitroOpts.cloudflare = {
880
1059
  nodeCompat: true,
package/dist/index.d.cts CHANGED
@@ -11,11 +11,13 @@ import { ConfigEnv, Plugin, PluginOption, UserConfig, UserConfigFn, UserConfigFn
11
11
  */
12
12
  declare function lovableAssetsProxyPlugin(): Plugin;
13
13
  //#endregion
14
- //#region src/index.d.ts
14
+ //#region src/documents.d.ts
15
15
  type PrerenderPage = {
16
16
  path: string;
17
17
  prerender?: Record<string, unknown>;
18
18
  };
19
+ //#endregion
20
+ //#region src/index.d.ts
19
21
  declare function stripRedundantNodejsCompatFlag(): Plugin;
20
22
  interface LovableViteTanstackOptions {
21
23
  plugins?: PluginOption[];
@@ -72,4 +74,4 @@ declare function defineConfig(config: UserConfigFnPromise): (env: ConfigEnv) =>
72
74
  declare function defineConfig(config: UserConfigFn): (env: ConfigEnv) => Promise<UserConfig>;
73
75
  declare function defineConfig(options?: LovableViteTanstackOptions): (env: ConfigEnv) => Promise<UserConfig>;
74
76
  //#endregion
75
- export { LovableViteTanstackOptions, PrerenderPage, defineConfig, lovableAssetsProxyPlugin, stripRedundantNodejsCompatFlag };
77
+ export { LovableViteTanstackOptions, type PrerenderPage, defineConfig, lovableAssetsProxyPlugin, stripRedundantNodejsCompatFlag };
package/dist/index.d.ts CHANGED
@@ -11,11 +11,13 @@ import { ConfigEnv, Plugin, PluginOption, UserConfig, UserConfigFn, UserConfigFn
11
11
  */
12
12
  declare function lovableAssetsProxyPlugin(): Plugin;
13
13
  //#endregion
14
- //#region src/index.d.ts
14
+ //#region src/documents.d.ts
15
15
  type PrerenderPage = {
16
16
  path: string;
17
17
  prerender?: Record<string, unknown>;
18
18
  };
19
+ //#endregion
20
+ //#region src/index.d.ts
19
21
  declare function stripRedundantNodejsCompatFlag(): Plugin;
20
22
  interface LovableViteTanstackOptions {
21
23
  plugins?: PluginOption[];
@@ -72,4 +74,4 @@ declare function defineConfig(config: UserConfigFnPromise): (env: ConfigEnv) =>
72
74
  declare function defineConfig(config: UserConfigFn): (env: ConfigEnv) => Promise<UserConfig>;
73
75
  declare function defineConfig(options?: LovableViteTanstackOptions): (env: ConfigEnv) => Promise<UserConfig>;
74
76
  //#endregion
75
- export { LovableViteTanstackOptions, PrerenderPage, defineConfig, lovableAssetsProxyPlugin, stripRedundantNodejsCompatFlag };
77
+ export { LovableViteTanstackOptions, type PrerenderPage, defineConfig, lovableAssetsProxyPlugin, stripRedundantNodejsCompatFlag };
package/dist/index.js CHANGED
@@ -126,6 +126,186 @@ function bundledDevCssUrlShim() {
126
126
  };
127
127
  }
128
128
  //#endregion
129
+ //#region package.json
130
+ var name = "@lovable.dev/vite-tanstack-config";
131
+ var version = "2.18.0";
132
+ //#endregion
133
+ //#region src/logging.ts
134
+ const PACKAGE_NAME = "@lovable.dev/vite-tanstack-config";
135
+ function logWarning(message) {
136
+ console.warn(`[${PACKAGE_NAME}] ${message}`);
137
+ }
138
+ //#endregion
139
+ //#region src/outputPaths.ts
140
+ const PUBLISH_OUTPUT_DIR = "dist";
141
+ const SERVER_OUTPUT_DIR = `${PUBLISH_OUTPUT_DIR}/server`;
142
+ const CLIENT_OUTPUT_DIR = `${PUBLISH_OUTPUT_DIR}/client`;
143
+ //#endregion
144
+ //#region src/documents.ts
145
+ const DOCUMENT_SET_VERSION = 1;
146
+ const GENERATOR = `${name}@${version}`;
147
+ const DOCUMENT_SET_DIR = ".lovable";
148
+ const DOCUMENT_SET_FILE = "output.json";
149
+ const DOCUMENT_SET_MAX_ENTRIES = 1e4;
150
+ const DOCUMENT_SET_MAX_BYTES = 4e6;
151
+ const DOCUMENT_SET_PLUGIN_NAME = "lovable:document-set";
152
+ const TANSTACK_POST_BUILD_PLUGIN = "tanstack-start-core:post-build";
153
+ const DOT_SEGMENT = /(^|\/)\.(\/|$)/;
154
+ function hasControlChars(value) {
155
+ for (let i = 0; i < value.length; i++) {
156
+ const code = value.charCodeAt(i);
157
+ if (code < 32 || code === 127) return true;
158
+ }
159
+ return false;
160
+ }
161
+ function normalizeRoute(raw) {
162
+ if (typeof raw !== "string") return void 0;
163
+ const pathname = raw.split(/[?#]/)[0] ?? "";
164
+ if (!pathname.startsWith("/")) return void 0;
165
+ const route = pathname.replace(/\/+$/, "") || "/";
166
+ if (route.includes("//") || route.includes("..") || route.includes("\\") || DOT_SEGMENT.test(route) || hasControlChars(route)) return;
167
+ return route;
168
+ }
169
+ /** Upstream's `joinURL("/", router.basepath)`: leading slash, no trailing slash, "" when unset. */
170
+ function normalizeBasePath(raw) {
171
+ if (typeof raw !== "string") return "";
172
+ const trimmed = raw.trim().replace(/\/+$/, "");
173
+ if (trimmed === "") return "";
174
+ return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
175
+ }
176
+ function withoutBasePath(route, basePath) {
177
+ if (route === basePath) return "/";
178
+ if (basePath === "") return route;
179
+ return route.startsWith(`${basePath}/`) ? route.slice(basePath.length) : route;
180
+ }
181
+ function documentEntryForPage(page, basePath = "") {
182
+ if (typeof page.prerender?.outputPath === "string") return void 0;
183
+ const pagePath = normalizeRoute(page.path);
184
+ if (pagePath === void 0) return void 0;
185
+ const inner = withoutBasePath(pagePath, basePath);
186
+ const route = normalizeRoute(`${basePath}${inner}`);
187
+ if (route === void 0) return void 0;
188
+ const file = inner === "/" ? "index.html" : `${inner.slice(1)}/index.html`;
189
+ if (file !== file.trim()) return void 0;
190
+ return {
191
+ route,
192
+ file
193
+ };
194
+ }
195
+ function compareRoutes(a, b) {
196
+ if (a.route < b.route) return -1;
197
+ if (a.route > b.route) return 1;
198
+ return 0;
199
+ }
200
+ /** The artifact bytes, or undefined when there is nothing to publish. */
201
+ function serializeDocumentSet(entries, generator) {
202
+ const sorted = [...entries].sort(compareRoutes);
203
+ const selected = [];
204
+ let cappedBy;
205
+ let length = JSON.stringify({
206
+ version: DOCUMENT_SET_VERSION,
207
+ generator,
208
+ documents: []
209
+ }).length;
210
+ for (const entry of sorted) {
211
+ if (selected.length === DOCUMENT_SET_MAX_ENTRIES) {
212
+ cappedBy = "entry limit";
213
+ break;
214
+ }
215
+ const added = JSON.stringify(entry).length + 1;
216
+ if (length + added > DOCUMENT_SET_MAX_BYTES) {
217
+ cappedBy = "byte limit";
218
+ break;
219
+ }
220
+ length += added;
221
+ selected.push(entry);
222
+ }
223
+ if (cappedBy !== void 0) logWarning(`Publishing only ${selected.length} of ${sorted.length} publishable routes; the document set hit its ${cappedBy}.`);
224
+ if (selected.length === 0) return void 0;
225
+ return JSON.stringify({
226
+ version: DOCUMENT_SET_VERSION,
227
+ generator,
228
+ documents: selected
229
+ });
230
+ }
231
+ const EXISTENCE_BATCH_SIZE = 64;
232
+ async function whichExist(paths, access) {
233
+ const exists = [];
234
+ for (let i = 0; i < paths.length; i += EXISTENCE_BATCH_SIZE) {
235
+ const batch = paths.slice(i, i + EXISTENCE_BATCH_SIZE).map((path) => access(path).then(() => true, () => false));
236
+ exists.push(...await Promise.all(batch));
237
+ }
238
+ return exists;
239
+ }
240
+ function createDocumentSet(routerBasePath) {
241
+ const basePath = normalizeBasePath(routerBasePath);
242
+ const basePathServable = basePath === "" || normalizeRoute(basePath) === basePath;
243
+ if (!basePathServable) logWarning(`Not publishing any prerendered documents: router.basepath "${basePath}" is not a servable route.`);
244
+ const documents = /* @__PURE__ */ new Map();
245
+ let root = process.cwd();
246
+ let clientOutputDir = CLIENT_OUTPUT_DIR;
247
+ const plugin = {
248
+ name: DOCUMENT_SET_PLUGIN_NAME,
249
+ apply: "build",
250
+ enforce: "post",
251
+ configResolved(config) {
252
+ root = config.root;
253
+ const names = config.plugins?.map((candidate) => candidate.name) ?? [];
254
+ const theirs = names.indexOf(TANSTACK_POST_BUILD_PLUGIN);
255
+ const ours = names.indexOf(DOCUMENT_SET_PLUGIN_NAME);
256
+ if (theirs !== -1 && ours !== -1 && ours < theirs) logWarning(`Prerendered routes will fall back to SSR: ${DOCUMENT_SET_PLUGIN_NAME} is registered before ${TANSTACK_POST_BUILD_PLUGIN}, so the document set is written before the prerender pass runs.`);
257
+ clientOutputDir = config.environments?.client?.build?.outDir ?? CLIENT_OUTPUT_DIR;
258
+ },
259
+ buildApp: {
260
+ order: "post",
261
+ async handler() {
262
+ const { access, mkdir, rm, writeFile } = await import("node:fs/promises");
263
+ const nodePath = await import("node:path");
264
+ const artifact = nodePath.join(root, PUBLISH_OUTPUT_DIR, DOCUMENT_SET_DIR, DOCUMENT_SET_FILE);
265
+ await rm(artifact, { force: true }).catch((error) => {
266
+ logWarning(`Could not remove a previous build's document set: ${String(error)}`);
267
+ });
268
+ if (documents.size === 0) return;
269
+ const clientDir = nodePath.resolve(root, clientOutputDir);
270
+ const entries = [...documents].map(([route, file]) => ({
271
+ route,
272
+ file
273
+ }));
274
+ const exists = await whichExist(entries.map(({ file }) => nodePath.resolve(clientDir, file)), access);
275
+ const present = [];
276
+ for (const [index, entry] of entries.entries()) {
277
+ if (exists[index]) {
278
+ present.push(entry);
279
+ continue;
280
+ }
281
+ logWarning(`Not publishing prerendered route ${entry.route}: it produced no ${entry.file}.`);
282
+ }
283
+ const payload = serializeDocumentSet(present, GENERATOR);
284
+ if (payload === void 0) return;
285
+ try {
286
+ await mkdir(nodePath.dirname(artifact), { recursive: true });
287
+ await writeFile(artifact, payload);
288
+ } catch (error) {
289
+ logWarning(`Could not write the document set; prerendered routes fall back to SSR: ${String(error)}`);
290
+ }
291
+ }
292
+ }
293
+ };
294
+ const record = (page) => {
295
+ if (!basePathServable) return;
296
+ const entry = documentEntryForPage(page, basePath);
297
+ if (entry === void 0) {
298
+ if (typeof page.prerender?.outputPath !== "string") logWarning(`Not publishing prerendered route ${String(page.path)}: its path cannot be a document key (dot segments, "//", "..", backslashes, control characters, and leading/trailing whitespace are rejected).`);
299
+ return;
300
+ }
301
+ documents.set(entry.route, entry.file);
302
+ };
303
+ return {
304
+ record,
305
+ plugin
306
+ };
307
+ }
308
+ //#endregion
129
309
  //#region src/index.ts
130
310
  const SANDBOX_ENV_VAR = "DEV_SERVER__PROJECT_PATH";
131
311
  const LOVABLE_SANDBOX_ENV_VAR = "LOVABLE_SANDBOX";
@@ -139,9 +319,6 @@ const TOOL_MANAGED_WATCH_IGNORES = [
139
319
  "**/.lovable/**",
140
320
  "**/.tanstack/tmp/**"
141
321
  ];
142
- function logWarning(message) {
143
- console.warn(`[@lovable.dev/vite-tanstack-config] ${message}`);
144
- }
145
322
  const NITRO_DEFAULT_PRESET_MIN = [
146
323
  3,
147
324
  0,
@@ -190,6 +367,7 @@ function pinPrerenderOptions(source, pins, label) {
190
367
  function pinPrerenderForLovableBuild(userOptions) {
191
368
  const attempted = /* @__PURE__ */ new Set();
192
369
  const rendered = /* @__PURE__ */ new Set();
370
+ const collector = createDocumentSet(userOptions.router?.basepath);
193
371
  const userPrerender = userOptions.prerender;
194
372
  const prerender = pinPrerenderOptions(userPrerender, ROOT_PRERENDER_PINS, "prerender");
195
373
  const userFilter = userPrerender?.filter;
@@ -200,6 +378,7 @@ function pinPrerenderForLovableBuild(userOptions) {
200
378
  };
201
379
  const withRenderTracking = (userOnSuccess) => (ctx) => {
202
380
  rendered.add(ctx.page.path);
381
+ collector.record(ctx.page);
203
382
  return typeof userOnSuccess === "function" ? userOnSuccess(ctx) : void 0;
204
383
  };
205
384
  prerender.onSuccess = withRenderTracking(userPrerender?.onSuccess);
@@ -232,7 +411,8 @@ function pinPrerenderForLovableBuild(userOptions) {
232
411
  logWarning(`Prerender skipped ${skipped.length} route(s) that failed to render; they fall back to SSR: ${skipped.join(", ")}`);
233
412
  }
234
413
  }
235
- }
414
+ },
415
+ documentSet: collector.plugin
236
416
  };
237
417
  }
238
418
  function lovableFetchBundlePreset() {
@@ -242,7 +422,7 @@ function lovableFetchBundlePreset() {
242
422
  noExternals: true,
243
423
  inlineDynamicImports: true,
244
424
  output: {
245
- dir: "dist",
425
+ dir: PUBLISH_OUTPUT_DIR,
246
426
  serverDir: "{{ output.dir }}/server",
247
427
  publicDir: "{{ output.dir }}/client"
248
428
  }
@@ -492,7 +672,7 @@ function lovableBuildErrorDiagnostics() {
492
672
  };
493
673
  }
494
674
  const NODEJS_COMPAT_DEFAULT_ON_DATE = "2026-08-04";
495
- const SANDBOX_WRANGLER_CONFIG_PATH = "dist/server/wrangler.json";
675
+ const SANDBOX_WRANGLER_CONFIG_PATH = `${SERVER_OUTPUT_DIR}/wrangler.json`;
496
676
  function stripRedundantNodejsCompatFlag() {
497
677
  let root = process.cwd();
498
678
  return {
@@ -529,7 +709,6 @@ function stripRedundantNodejsCompatFlag() {
529
709
  }
530
710
  const NITRO_SERVER_ENTRY = "index.mjs";
531
711
  const NITRO_WRANGLER_CONFIG = "wrangler.json";
532
- const DEFAULT_SERVER_OUTPUT_DIR = "dist/server";
533
712
  const DEFAULT_SERVER_ENTRY_NAME = "server";
534
713
  const PRERENDER_PREVIEW_HOST = "127.0.0.1";
535
714
  const TSS_PRERENDERING_ENV = "TSS_PRERENDERING";
@@ -553,7 +732,7 @@ export default {
553
732
  }
554
733
  function prerenderPreviewShim(state) {
555
734
  let root = process.cwd();
556
- let serverOutDir = DEFAULT_SERVER_OUTPUT_DIR;
735
+ let serverOutDir = SERVER_OUTPUT_DIR;
557
736
  let serverEntryName = DEFAULT_SERVER_ENTRY_NAME;
558
737
  return {
559
738
  name: "lovable-prerender-preview-shim",
@@ -584,7 +763,7 @@ function prerenderPreviewShim(state) {
584
763
  if (!await exists(nodePath.join(outDir, NITRO_SERVER_ENTRY))) return;
585
764
  const basename = nodePath.basename(serverEntryName, nodePath.extname(serverEntryName));
586
765
  if (!basename || basename === "index") {
587
- logWarning(`Cannot prerender: the server entry "${serverEntryName}" would need a shim at ${DEFAULT_SERVER_OUTPUT_DIR}/index.js, which outranks nitro's own entry when publishing. Rename the entry to anything but \`index\`.`);
766
+ logWarning(`Cannot prerender: the server entry "${serverEntryName}" would need a shim at ${SERVER_OUTPUT_DIR}/index.js, which outranks nitro's own entry when publishing. Rename the entry to anything but \`index\`.`);
588
767
  return;
589
768
  }
590
769
  const target = nodePath.join(outDir, `${basename}.js`);
@@ -809,8 +988,8 @@ function defineConfig(configOrOptions = {}) {
809
988
  } }, options.tanstackStart ?? {});
810
989
  const prerenderEnabled = prerenderIsEnabled(tanstackStartOptions);
811
990
  if (command === "build" && isSandbox) {
812
- const { startOptions, skipReporter } = pinPrerenderForLovableBuild(tanstackStartOptions);
813
- internalPlugins.push(tanstackStart(startOptions), skipReporter);
991
+ const { startOptions, skipReporter, documentSet } = pinPrerenderForLovableBuild(tanstackStartOptions);
992
+ internalPlugins.push(tanstackStart(startOptions), skipReporter, documentSet);
814
993
  } else internalPlugins.push(tanstackStart(tanstackStartOptions));
815
994
  const explicitNitro = options.nitro === true || typeof options.nitro === "object" && options.nitro !== null;
816
995
  if (options.nitro !== false && command === "build") {
@@ -848,9 +1027,9 @@ function defineConfig(configOrOptions = {}) {
848
1027
  delete nitroOpts.defaultPreset;
849
1028
  nitroOpts.preset = "cloudflare-module";
850
1029
  nitroOpts.output = {
851
- dir: "dist",
852
- serverDir: "dist/server",
853
- publicDir: "dist/client"
1030
+ dir: PUBLISH_OUTPUT_DIR,
1031
+ serverDir: SERVER_OUTPUT_DIR,
1032
+ publicDir: CLIENT_OUTPUT_DIR
854
1033
  };
855
1034
  nitroOpts.cloudflare = {
856
1035
  nodeCompat: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovable.dev/vite-tanstack-config",
3
- "version": "2.17.0",
3
+ "version": "2.18.0",
4
4
  "description": "Vite config wrapper for Lovable TanStack Start projects",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",