@lovable.dev/vite-tanstack-config 2.17.1 → 2.19.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/index.cjs CHANGED
@@ -150,6 +150,209 @@ 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.19.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 (basePath === "") return route;
202
+ if (route === basePath) return "/";
203
+ if (route.startsWith(`${basePath}/`)) return route.slice(basePath.length);
204
+ return route.startsWith(basePath) ? void 0 : route;
205
+ }
206
+ function documentEntryForPage(page, basePath = "") {
207
+ if (typeof page.prerender?.outputPath === "string") return void 0;
208
+ const pagePath = normalizeRoute(page.path);
209
+ if (pagePath === void 0) return void 0;
210
+ const inner = withoutBasePath(pagePath, basePath);
211
+ if (inner === void 0) return void 0;
212
+ const route = normalizeRoute(`${basePath}${inner}`);
213
+ if (route === void 0) return void 0;
214
+ const file = inner === "/" ? "index.html" : `${inner.slice(1)}/index.html`;
215
+ if (file !== file.trim()) return void 0;
216
+ return {
217
+ route,
218
+ file
219
+ };
220
+ }
221
+ function compareRoutes(a, b) {
222
+ if (a.route < b.route) return -1;
223
+ if (a.route > b.route) return 1;
224
+ return 0;
225
+ }
226
+ /** The artifact bytes, or undefined when there is nothing to publish. */
227
+ function serializeDocumentSet(entries, generator) {
228
+ const sorted = [...entries].sort(compareRoutes);
229
+ const selected = [];
230
+ let cappedBy;
231
+ let length = JSON.stringify({
232
+ version: DOCUMENT_SET_VERSION,
233
+ generator,
234
+ documents: []
235
+ }).length;
236
+ for (const entry of sorted) {
237
+ if (selected.length === DOCUMENT_SET_MAX_ENTRIES) {
238
+ cappedBy = "entry limit";
239
+ break;
240
+ }
241
+ const added = JSON.stringify(entry).length + 1;
242
+ if (length + added > DOCUMENT_SET_MAX_BYTES) {
243
+ cappedBy = "byte limit";
244
+ break;
245
+ }
246
+ length += added;
247
+ selected.push(entry);
248
+ }
249
+ if (cappedBy !== void 0) logWarning(`Publishing only ${selected.length} of ${sorted.length} publishable routes; the document set hit its ${cappedBy}.`);
250
+ if (selected.length === 0) return void 0;
251
+ return JSON.stringify({
252
+ version: DOCUMENT_SET_VERSION,
253
+ generator,
254
+ documents: selected
255
+ });
256
+ }
257
+ const EXISTENCE_BATCH_SIZE = 64;
258
+ async function whichExist(paths, access) {
259
+ const exists = [];
260
+ for (let i = 0; i < paths.length; i += EXISTENCE_BATCH_SIZE) {
261
+ const batch = paths.slice(i, i + EXISTENCE_BATCH_SIZE).map((path) => access(path).then(() => true, () => false));
262
+ exists.push(...await Promise.all(batch));
263
+ }
264
+ return exists;
265
+ }
266
+ /** How the error names the public dir, which vite lets a project point anywhere. */
267
+ function publicDirLabel(root, publicDir, nodePath) {
268
+ const relative = nodePath.relative(root, publicDir);
269
+ return relative !== "" && !relative.startsWith("..") ? relative.split(nodePath.sep).join("/") : publicDir;
270
+ }
271
+ function createDocumentSet(routerBasePath) {
272
+ const basePath = normalizeBasePath(routerBasePath);
273
+ const basePathServable = basePath === "" || normalizeRoute(basePath) === basePath;
274
+ if (!basePathServable) logWarning(`Not publishing any prerendered documents: router.basepath "${basePath}" is not a servable route.`);
275
+ const documents = /* @__PURE__ */ new Map();
276
+ let root = process.cwd();
277
+ let clientOutputDir = CLIENT_OUTPUT_DIR;
278
+ let publicDir = "";
279
+ const plugin = {
280
+ name: DOCUMENT_SET_PLUGIN_NAME,
281
+ apply: "build",
282
+ enforce: "post",
283
+ configResolved(config) {
284
+ root = config.root;
285
+ const names = config.plugins?.map((candidate) => candidate.name) ?? [];
286
+ const theirs = names.indexOf(TANSTACK_POST_BUILD_PLUGIN);
287
+ const ours = names.indexOf(DOCUMENT_SET_PLUGIN_NAME);
288
+ 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.`);
289
+ clientOutputDir = config.environments?.client?.build?.outDir ?? CLIENT_OUTPUT_DIR;
290
+ publicDir = (config.environments?.client?.build?.copyPublicDir ?? config.build?.copyPublicDir) !== false && typeof config.publicDir === "string" ? config.publicDir : "";
291
+ },
292
+ buildApp: {
293
+ order: "post",
294
+ async handler() {
295
+ const { access, mkdir, rm, writeFile } = await import("node:fs/promises");
296
+ const nodePath = await import("node:path");
297
+ const artifact = nodePath.join(root, PUBLISH_OUTPUT_DIR, DOCUMENT_SET_DIR, DOCUMENT_SET_FILE);
298
+ await rm(artifact, { force: true }).catch((error) => {
299
+ logWarning(`Could not remove a previous build's document set: ${String(error)}`);
300
+ });
301
+ if (documents.size === 0) return;
302
+ const collisions = [];
303
+ if (publicDir !== "") {
304
+ const candidates = [...documents].map(([route, file]) => ({
305
+ route,
306
+ file
307
+ }));
308
+ const committed = await whichExist(candidates.map(({ file }) => nodePath.join(publicDir, file)), access);
309
+ for (const [index, candidate] of candidates.entries()) if (committed[index]) collisions.push(candidate);
310
+ }
311
+ if (collisions.length > 0) {
312
+ const label = publicDirLabel(root, publicDir, nodePath);
313
+ const pairs = collisions.sort(compareRoutes).map(({ route, file }) => ` ${route} overwrites ${label}/${file}`);
314
+ throw new Error(`[${PACKAGE_NAME}] Prerendering would overwrite ${collisions.length} committed file(s) under ${label}/:\n${pairs.join("\n")}\nEach route's prerendered HTML is written into the client build after vite copies ${label}/, so every committed file above is replaced by its rendered route and never ships.\nFix each line above in one of two ways: delete or rename the ${label}/ file, or stop prerendering the route (drop it from \`pages\` in vite.config, or return false for it from \`prerender.filter\`).`);
315
+ }
316
+ const clientDir = nodePath.resolve(root, clientOutputDir);
317
+ const entries = [...documents].map(([route, file]) => ({
318
+ route,
319
+ file
320
+ }));
321
+ const exists = await whichExist(entries.map(({ file }) => nodePath.resolve(clientDir, file)), access);
322
+ const present = [];
323
+ for (const [index, entry] of entries.entries()) {
324
+ if (exists[index]) {
325
+ present.push(entry);
326
+ continue;
327
+ }
328
+ logWarning(`Not publishing prerendered route ${entry.route}: it produced no ${entry.file}.`);
329
+ }
330
+ const payload = serializeDocumentSet(present, GENERATOR);
331
+ if (payload === void 0) return;
332
+ try {
333
+ await mkdir(nodePath.dirname(artifact), { recursive: true });
334
+ await writeFile(artifact, payload);
335
+ } catch (error) {
336
+ logWarning(`Could not write the document set; prerendered routes fall back to SSR: ${String(error)}`);
337
+ }
338
+ }
339
+ }
340
+ };
341
+ const record = (page) => {
342
+ if (!basePathServable) return;
343
+ const entry = documentEntryForPage(page, basePath);
344
+ if (entry === void 0) {
345
+ 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).`);
346
+ return;
347
+ }
348
+ documents.set(entry.route, entry.file);
349
+ };
350
+ return {
351
+ record,
352
+ plugin
353
+ };
354
+ }
355
+ //#endregion
153
356
  //#region src/index.ts
154
357
  const SANDBOX_ENV_VAR = "DEV_SERVER__PROJECT_PATH";
155
358
  const LOVABLE_SANDBOX_ENV_VAR = "LOVABLE_SANDBOX";
@@ -163,9 +366,6 @@ const TOOL_MANAGED_WATCH_IGNORES = [
163
366
  "**/.lovable/**",
164
367
  "**/.tanstack/tmp/**"
165
368
  ];
166
- function logWarning(message) {
167
- console.warn(`[@lovable.dev/vite-tanstack-config] ${message}`);
168
- }
169
369
  const NITRO_DEFAULT_PRESET_MIN = [
170
370
  3,
171
371
  0,
@@ -214,6 +414,7 @@ function pinPrerenderOptions(source, pins, label) {
214
414
  function pinPrerenderForLovableBuild(userOptions) {
215
415
  const attempted = /* @__PURE__ */ new Set();
216
416
  const rendered = /* @__PURE__ */ new Set();
417
+ const collector = createDocumentSet(userOptions.router?.basepath);
217
418
  const userPrerender = userOptions.prerender;
218
419
  const prerender = pinPrerenderOptions(userPrerender, ROOT_PRERENDER_PINS, "prerender");
219
420
  const userFilter = userPrerender?.filter;
@@ -224,6 +425,7 @@ function pinPrerenderForLovableBuild(userOptions) {
224
425
  };
225
426
  const withRenderTracking = (userOnSuccess) => (ctx) => {
226
427
  rendered.add(ctx.page.path);
428
+ collector.record(ctx.page);
227
429
  return typeof userOnSuccess === "function" ? userOnSuccess(ctx) : void 0;
228
430
  };
229
431
  prerender.onSuccess = withRenderTracking(userPrerender?.onSuccess);
@@ -256,7 +458,8 @@ function pinPrerenderForLovableBuild(userOptions) {
256
458
  logWarning(`Prerender skipped ${skipped.length} route(s) that failed to render; they fall back to SSR: ${skipped.join(", ")}`);
257
459
  }
258
460
  }
259
- }
461
+ },
462
+ documentSet: collector.plugin
260
463
  };
261
464
  }
262
465
  function lovableFetchBundlePreset() {
@@ -266,7 +469,7 @@ function lovableFetchBundlePreset() {
266
469
  noExternals: true,
267
470
  inlineDynamicImports: true,
268
471
  output: {
269
- dir: "dist",
472
+ dir: PUBLISH_OUTPUT_DIR,
270
473
  serverDir: "{{ output.dir }}/server",
271
474
  publicDir: "{{ output.dir }}/client"
272
475
  }
@@ -516,7 +719,7 @@ function lovableBuildErrorDiagnostics() {
516
719
  };
517
720
  }
518
721
  const NODEJS_COMPAT_DEFAULT_ON_DATE = "2026-08-04";
519
- const SANDBOX_WRANGLER_CONFIG_PATH = "dist/server/wrangler.json";
722
+ const SANDBOX_WRANGLER_CONFIG_PATH = `${SERVER_OUTPUT_DIR}/wrangler.json`;
520
723
  function stripRedundantNodejsCompatFlag() {
521
724
  let root = process.cwd();
522
725
  return {
@@ -553,7 +756,6 @@ function stripRedundantNodejsCompatFlag() {
553
756
  }
554
757
  const NITRO_SERVER_ENTRY = "index.mjs";
555
758
  const NITRO_WRANGLER_CONFIG = "wrangler.json";
556
- const DEFAULT_SERVER_OUTPUT_DIR = "dist/server";
557
759
  const DEFAULT_SERVER_ENTRY_NAME = "server";
558
760
  const PRERENDER_PREVIEW_HOST = "127.0.0.1";
559
761
  const TSS_PRERENDERING_ENV = "TSS_PRERENDERING";
@@ -577,7 +779,7 @@ export default {
577
779
  }
578
780
  function prerenderPreviewShim(state) {
579
781
  let root = process.cwd();
580
- let serverOutDir = DEFAULT_SERVER_OUTPUT_DIR;
782
+ let serverOutDir = SERVER_OUTPUT_DIR;
581
783
  let serverEntryName = DEFAULT_SERVER_ENTRY_NAME;
582
784
  return {
583
785
  name: "lovable-prerender-preview-shim",
@@ -608,7 +810,7 @@ function prerenderPreviewShim(state) {
608
810
  if (!await exists(nodePath.join(outDir, NITRO_SERVER_ENTRY))) return;
609
811
  const basename = nodePath.basename(serverEntryName, nodePath.extname(serverEntryName));
610
812
  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\`.`);
813
+ 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
814
  return;
613
815
  }
614
816
  const target = nodePath.join(outDir, `${basename}.js`);
@@ -833,8 +1035,8 @@ function defineConfig(configOrOptions = {}) {
833
1035
  } }, options.tanstackStart ?? {});
834
1036
  const prerenderEnabled = prerenderIsEnabled(tanstackStartOptions);
835
1037
  if (command === "build" && isSandbox) {
836
- const { startOptions, skipReporter } = pinPrerenderForLovableBuild(tanstackStartOptions);
837
- internalPlugins.push(tanstackStart(startOptions), skipReporter);
1038
+ const { startOptions, skipReporter, documentSet } = pinPrerenderForLovableBuild(tanstackStartOptions);
1039
+ internalPlugins.push(tanstackStart(startOptions), skipReporter, documentSet);
838
1040
  } else internalPlugins.push(tanstackStart(tanstackStartOptions));
839
1041
  const explicitNitro = options.nitro === true || typeof options.nitro === "object" && options.nitro !== null;
840
1042
  if (options.nitro !== false && command === "build") {
@@ -872,9 +1074,9 @@ function defineConfig(configOrOptions = {}) {
872
1074
  delete nitroOpts.defaultPreset;
873
1075
  nitroOpts.preset = "cloudflare-module";
874
1076
  nitroOpts.output = {
875
- dir: "dist",
876
- serverDir: "dist/server",
877
- publicDir: "dist/client"
1077
+ dir: PUBLISH_OUTPUT_DIR,
1078
+ serverDir: SERVER_OUTPUT_DIR,
1079
+ publicDir: CLIENT_OUTPUT_DIR
878
1080
  };
879
1081
  nitroOpts.cloudflare = {
880
1082
  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,209 @@ 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.19.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 (basePath === "") return route;
178
+ if (route === basePath) return "/";
179
+ if (route.startsWith(`${basePath}/`)) return route.slice(basePath.length);
180
+ return route.startsWith(basePath) ? void 0 : route;
181
+ }
182
+ function documentEntryForPage(page, basePath = "") {
183
+ if (typeof page.prerender?.outputPath === "string") return void 0;
184
+ const pagePath = normalizeRoute(page.path);
185
+ if (pagePath === void 0) return void 0;
186
+ const inner = withoutBasePath(pagePath, basePath);
187
+ if (inner === void 0) return void 0;
188
+ const route = normalizeRoute(`${basePath}${inner}`);
189
+ if (route === void 0) return void 0;
190
+ const file = inner === "/" ? "index.html" : `${inner.slice(1)}/index.html`;
191
+ if (file !== file.trim()) return void 0;
192
+ return {
193
+ route,
194
+ file
195
+ };
196
+ }
197
+ function compareRoutes(a, b) {
198
+ if (a.route < b.route) return -1;
199
+ if (a.route > b.route) return 1;
200
+ return 0;
201
+ }
202
+ /** The artifact bytes, or undefined when there is nothing to publish. */
203
+ function serializeDocumentSet(entries, generator) {
204
+ const sorted = [...entries].sort(compareRoutes);
205
+ const selected = [];
206
+ let cappedBy;
207
+ let length = JSON.stringify({
208
+ version: DOCUMENT_SET_VERSION,
209
+ generator,
210
+ documents: []
211
+ }).length;
212
+ for (const entry of sorted) {
213
+ if (selected.length === DOCUMENT_SET_MAX_ENTRIES) {
214
+ cappedBy = "entry limit";
215
+ break;
216
+ }
217
+ const added = JSON.stringify(entry).length + 1;
218
+ if (length + added > DOCUMENT_SET_MAX_BYTES) {
219
+ cappedBy = "byte limit";
220
+ break;
221
+ }
222
+ length += added;
223
+ selected.push(entry);
224
+ }
225
+ if (cappedBy !== void 0) logWarning(`Publishing only ${selected.length} of ${sorted.length} publishable routes; the document set hit its ${cappedBy}.`);
226
+ if (selected.length === 0) return void 0;
227
+ return JSON.stringify({
228
+ version: DOCUMENT_SET_VERSION,
229
+ generator,
230
+ documents: selected
231
+ });
232
+ }
233
+ const EXISTENCE_BATCH_SIZE = 64;
234
+ async function whichExist(paths, access) {
235
+ const exists = [];
236
+ for (let i = 0; i < paths.length; i += EXISTENCE_BATCH_SIZE) {
237
+ const batch = paths.slice(i, i + EXISTENCE_BATCH_SIZE).map((path) => access(path).then(() => true, () => false));
238
+ exists.push(...await Promise.all(batch));
239
+ }
240
+ return exists;
241
+ }
242
+ /** How the error names the public dir, which vite lets a project point anywhere. */
243
+ function publicDirLabel(root, publicDir, nodePath) {
244
+ const relative = nodePath.relative(root, publicDir);
245
+ return relative !== "" && !relative.startsWith("..") ? relative.split(nodePath.sep).join("/") : publicDir;
246
+ }
247
+ function createDocumentSet(routerBasePath) {
248
+ const basePath = normalizeBasePath(routerBasePath);
249
+ const basePathServable = basePath === "" || normalizeRoute(basePath) === basePath;
250
+ if (!basePathServable) logWarning(`Not publishing any prerendered documents: router.basepath "${basePath}" is not a servable route.`);
251
+ const documents = /* @__PURE__ */ new Map();
252
+ let root = process.cwd();
253
+ let clientOutputDir = CLIENT_OUTPUT_DIR;
254
+ let publicDir = "";
255
+ const plugin = {
256
+ name: DOCUMENT_SET_PLUGIN_NAME,
257
+ apply: "build",
258
+ enforce: "post",
259
+ configResolved(config) {
260
+ root = config.root;
261
+ const names = config.plugins?.map((candidate) => candidate.name) ?? [];
262
+ const theirs = names.indexOf(TANSTACK_POST_BUILD_PLUGIN);
263
+ const ours = names.indexOf(DOCUMENT_SET_PLUGIN_NAME);
264
+ 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.`);
265
+ clientOutputDir = config.environments?.client?.build?.outDir ?? CLIENT_OUTPUT_DIR;
266
+ publicDir = (config.environments?.client?.build?.copyPublicDir ?? config.build?.copyPublicDir) !== false && typeof config.publicDir === "string" ? config.publicDir : "";
267
+ },
268
+ buildApp: {
269
+ order: "post",
270
+ async handler() {
271
+ const { access, mkdir, rm, writeFile } = await import("node:fs/promises");
272
+ const nodePath = await import("node:path");
273
+ const artifact = nodePath.join(root, PUBLISH_OUTPUT_DIR, DOCUMENT_SET_DIR, DOCUMENT_SET_FILE);
274
+ await rm(artifact, { force: true }).catch((error) => {
275
+ logWarning(`Could not remove a previous build's document set: ${String(error)}`);
276
+ });
277
+ if (documents.size === 0) return;
278
+ const collisions = [];
279
+ if (publicDir !== "") {
280
+ const candidates = [...documents].map(([route, file]) => ({
281
+ route,
282
+ file
283
+ }));
284
+ const committed = await whichExist(candidates.map(({ file }) => nodePath.join(publicDir, file)), access);
285
+ for (const [index, candidate] of candidates.entries()) if (committed[index]) collisions.push(candidate);
286
+ }
287
+ if (collisions.length > 0) {
288
+ const label = publicDirLabel(root, publicDir, nodePath);
289
+ const pairs = collisions.sort(compareRoutes).map(({ route, file }) => ` ${route} overwrites ${label}/${file}`);
290
+ throw new Error(`[${PACKAGE_NAME}] Prerendering would overwrite ${collisions.length} committed file(s) under ${label}/:\n${pairs.join("\n")}\nEach route's prerendered HTML is written into the client build after vite copies ${label}/, so every committed file above is replaced by its rendered route and never ships.\nFix each line above in one of two ways: delete or rename the ${label}/ file, or stop prerendering the route (drop it from \`pages\` in vite.config, or return false for it from \`prerender.filter\`).`);
291
+ }
292
+ const clientDir = nodePath.resolve(root, clientOutputDir);
293
+ const entries = [...documents].map(([route, file]) => ({
294
+ route,
295
+ file
296
+ }));
297
+ const exists = await whichExist(entries.map(({ file }) => nodePath.resolve(clientDir, file)), access);
298
+ const present = [];
299
+ for (const [index, entry] of entries.entries()) {
300
+ if (exists[index]) {
301
+ present.push(entry);
302
+ continue;
303
+ }
304
+ logWarning(`Not publishing prerendered route ${entry.route}: it produced no ${entry.file}.`);
305
+ }
306
+ const payload = serializeDocumentSet(present, GENERATOR);
307
+ if (payload === void 0) return;
308
+ try {
309
+ await mkdir(nodePath.dirname(artifact), { recursive: true });
310
+ await writeFile(artifact, payload);
311
+ } catch (error) {
312
+ logWarning(`Could not write the document set; prerendered routes fall back to SSR: ${String(error)}`);
313
+ }
314
+ }
315
+ }
316
+ };
317
+ const record = (page) => {
318
+ if (!basePathServable) return;
319
+ const entry = documentEntryForPage(page, basePath);
320
+ if (entry === void 0) {
321
+ 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).`);
322
+ return;
323
+ }
324
+ documents.set(entry.route, entry.file);
325
+ };
326
+ return {
327
+ record,
328
+ plugin
329
+ };
330
+ }
331
+ //#endregion
129
332
  //#region src/index.ts
130
333
  const SANDBOX_ENV_VAR = "DEV_SERVER__PROJECT_PATH";
131
334
  const LOVABLE_SANDBOX_ENV_VAR = "LOVABLE_SANDBOX";
@@ -139,9 +342,6 @@ const TOOL_MANAGED_WATCH_IGNORES = [
139
342
  "**/.lovable/**",
140
343
  "**/.tanstack/tmp/**"
141
344
  ];
142
- function logWarning(message) {
143
- console.warn(`[@lovable.dev/vite-tanstack-config] ${message}`);
144
- }
145
345
  const NITRO_DEFAULT_PRESET_MIN = [
146
346
  3,
147
347
  0,
@@ -190,6 +390,7 @@ function pinPrerenderOptions(source, pins, label) {
190
390
  function pinPrerenderForLovableBuild(userOptions) {
191
391
  const attempted = /* @__PURE__ */ new Set();
192
392
  const rendered = /* @__PURE__ */ new Set();
393
+ const collector = createDocumentSet(userOptions.router?.basepath);
193
394
  const userPrerender = userOptions.prerender;
194
395
  const prerender = pinPrerenderOptions(userPrerender, ROOT_PRERENDER_PINS, "prerender");
195
396
  const userFilter = userPrerender?.filter;
@@ -200,6 +401,7 @@ function pinPrerenderForLovableBuild(userOptions) {
200
401
  };
201
402
  const withRenderTracking = (userOnSuccess) => (ctx) => {
202
403
  rendered.add(ctx.page.path);
404
+ collector.record(ctx.page);
203
405
  return typeof userOnSuccess === "function" ? userOnSuccess(ctx) : void 0;
204
406
  };
205
407
  prerender.onSuccess = withRenderTracking(userPrerender?.onSuccess);
@@ -232,7 +434,8 @@ function pinPrerenderForLovableBuild(userOptions) {
232
434
  logWarning(`Prerender skipped ${skipped.length} route(s) that failed to render; they fall back to SSR: ${skipped.join(", ")}`);
233
435
  }
234
436
  }
235
- }
437
+ },
438
+ documentSet: collector.plugin
236
439
  };
237
440
  }
238
441
  function lovableFetchBundlePreset() {
@@ -242,7 +445,7 @@ function lovableFetchBundlePreset() {
242
445
  noExternals: true,
243
446
  inlineDynamicImports: true,
244
447
  output: {
245
- dir: "dist",
448
+ dir: PUBLISH_OUTPUT_DIR,
246
449
  serverDir: "{{ output.dir }}/server",
247
450
  publicDir: "{{ output.dir }}/client"
248
451
  }
@@ -492,7 +695,7 @@ function lovableBuildErrorDiagnostics() {
492
695
  };
493
696
  }
494
697
  const NODEJS_COMPAT_DEFAULT_ON_DATE = "2026-08-04";
495
- const SANDBOX_WRANGLER_CONFIG_PATH = "dist/server/wrangler.json";
698
+ const SANDBOX_WRANGLER_CONFIG_PATH = `${SERVER_OUTPUT_DIR}/wrangler.json`;
496
699
  function stripRedundantNodejsCompatFlag() {
497
700
  let root = process.cwd();
498
701
  return {
@@ -529,7 +732,6 @@ function stripRedundantNodejsCompatFlag() {
529
732
  }
530
733
  const NITRO_SERVER_ENTRY = "index.mjs";
531
734
  const NITRO_WRANGLER_CONFIG = "wrangler.json";
532
- const DEFAULT_SERVER_OUTPUT_DIR = "dist/server";
533
735
  const DEFAULT_SERVER_ENTRY_NAME = "server";
534
736
  const PRERENDER_PREVIEW_HOST = "127.0.0.1";
535
737
  const TSS_PRERENDERING_ENV = "TSS_PRERENDERING";
@@ -553,7 +755,7 @@ export default {
553
755
  }
554
756
  function prerenderPreviewShim(state) {
555
757
  let root = process.cwd();
556
- let serverOutDir = DEFAULT_SERVER_OUTPUT_DIR;
758
+ let serverOutDir = SERVER_OUTPUT_DIR;
557
759
  let serverEntryName = DEFAULT_SERVER_ENTRY_NAME;
558
760
  return {
559
761
  name: "lovable-prerender-preview-shim",
@@ -584,7 +786,7 @@ function prerenderPreviewShim(state) {
584
786
  if (!await exists(nodePath.join(outDir, NITRO_SERVER_ENTRY))) return;
585
787
  const basename = nodePath.basename(serverEntryName, nodePath.extname(serverEntryName));
586
788
  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\`.`);
789
+ 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
790
  return;
589
791
  }
590
792
  const target = nodePath.join(outDir, `${basename}.js`);
@@ -809,8 +1011,8 @@ function defineConfig(configOrOptions = {}) {
809
1011
  } }, options.tanstackStart ?? {});
810
1012
  const prerenderEnabled = prerenderIsEnabled(tanstackStartOptions);
811
1013
  if (command === "build" && isSandbox) {
812
- const { startOptions, skipReporter } = pinPrerenderForLovableBuild(tanstackStartOptions);
813
- internalPlugins.push(tanstackStart(startOptions), skipReporter);
1014
+ const { startOptions, skipReporter, documentSet } = pinPrerenderForLovableBuild(tanstackStartOptions);
1015
+ internalPlugins.push(tanstackStart(startOptions), skipReporter, documentSet);
814
1016
  } else internalPlugins.push(tanstackStart(tanstackStartOptions));
815
1017
  const explicitNitro = options.nitro === true || typeof options.nitro === "object" && options.nitro !== null;
816
1018
  if (options.nitro !== false && command === "build") {
@@ -848,9 +1050,9 @@ function defineConfig(configOrOptions = {}) {
848
1050
  delete nitroOpts.defaultPreset;
849
1051
  nitroOpts.preset = "cloudflare-module";
850
1052
  nitroOpts.output = {
851
- dir: "dist",
852
- serverDir: "dist/server",
853
- publicDir: "dist/client"
1053
+ dir: PUBLISH_OUTPUT_DIR,
1054
+ serverDir: SERVER_OUTPUT_DIR,
1055
+ publicDir: CLIENT_OUTPUT_DIR
854
1056
  };
855
1057
  nitroOpts.cloudflare = {
856
1058
  nodeCompat: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovable.dev/vite-tanstack-config",
3
- "version": "2.17.1",
3
+ "version": "2.19.0",
4
4
  "description": "Vite config wrapper for Lovable TanStack Start projects",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",