@lovable.dev/vite-tanstack-config 2.18.0 → 2.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -152,7 +152,7 @@ function bundledDevCssUrlShim() {
152
152
  //#endregion
153
153
  //#region package.json
154
154
  var name = "@lovable.dev/vite-tanstack-config";
155
- var version = "2.18.0";
155
+ var version = "2.19.1";
156
156
  //#endregion
157
157
  //#region src/logging.ts
158
158
  const PACKAGE_NAME = "@lovable.dev/vite-tanstack-config";
@@ -198,15 +198,17 @@ function normalizeBasePath(raw) {
198
198
  return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
199
199
  }
200
200
  function withoutBasePath(route, basePath) {
201
- if (route === basePath) return "/";
202
201
  if (basePath === "") return route;
203
- return route.startsWith(`${basePath}/`) ? route.slice(basePath.length) : route;
202
+ if (route === basePath) return "/";
203
+ if (route.startsWith(`${basePath}/`)) return route.slice(basePath.length);
204
+ return route.startsWith(basePath) ? void 0 : route;
204
205
  }
205
206
  function documentEntryForPage(page, basePath = "") {
206
207
  if (typeof page.prerender?.outputPath === "string") return void 0;
207
208
  const pagePath = normalizeRoute(page.path);
208
209
  if (pagePath === void 0) return void 0;
209
210
  const inner = withoutBasePath(pagePath, basePath);
211
+ if (inner === void 0) return void 0;
210
212
  const route = normalizeRoute(`${basePath}${inner}`);
211
213
  if (route === void 0) return void 0;
212
214
  const file = inner === "/" ? "index.html" : `${inner.slice(1)}/index.html`;
@@ -253,13 +255,24 @@ function serializeDocumentSet(entries, generator) {
253
255
  });
254
256
  }
255
257
  const EXISTENCE_BATCH_SIZE = 64;
258
+ async function inBatches(items, run) {
259
+ const results = [];
260
+ for (let i = 0; i < items.length; i += EXISTENCE_BATCH_SIZE) results.push(...await Promise.all(items.slice(i, i + EXISTENCE_BATCH_SIZE).map(run)));
261
+ return results;
262
+ }
256
263
  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;
264
+ return inBatches(paths, (path) => access(path).then(() => true, () => false));
265
+ }
266
+ async function whichMatch(pairs, readFile) {
267
+ const read = (path) => readFile(path).catch((error) => {
268
+ if (error.code !== "ENOENT") logWarning(`Could not read ${path} while checking prerender collisions: ${String(error)}`);
269
+ });
270
+ return inBatches(pairs, ([left, right]) => Promise.all([read(left), read(right)]).then(([a, b]) => a !== void 0 && b !== void 0 && a.equals(b)));
271
+ }
272
+ /** How the error names the public dir, which vite lets a project point anywhere. */
273
+ function publicDirLabel(root, publicDir, nodePath) {
274
+ const relative = nodePath.relative(root, publicDir);
275
+ return relative !== "" && !relative.startsWith("..") ? relative.split(nodePath.sep).join("/") : publicDir;
263
276
  }
264
277
  function createDocumentSet(routerBasePath) {
265
278
  const basePath = normalizeBasePath(routerBasePath);
@@ -268,6 +281,7 @@ function createDocumentSet(routerBasePath) {
268
281
  const documents = /* @__PURE__ */ new Map();
269
282
  let root = process.cwd();
270
283
  let clientOutputDir = CLIENT_OUTPUT_DIR;
284
+ let publicDir = "";
271
285
  const plugin = {
272
286
  name: DOCUMENT_SET_PLUGIN_NAME,
273
287
  apply: "build",
@@ -279,11 +293,12 @@ function createDocumentSet(routerBasePath) {
279
293
  const ours = names.indexOf(DOCUMENT_SET_PLUGIN_NAME);
280
294
  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
295
  clientOutputDir = config.environments?.client?.build?.outDir ?? CLIENT_OUTPUT_DIR;
296
+ publicDir = (config.environments?.client?.build?.copyPublicDir ?? config.build?.copyPublicDir) !== false && typeof config.publicDir === "string" ? config.publicDir : "";
282
297
  },
283
298
  buildApp: {
284
299
  order: "post",
285
300
  async handler() {
286
- const { access, mkdir, rm, writeFile } = await import("node:fs/promises");
301
+ const { access, mkdir, readFile, rm, writeFile } = await import("node:fs/promises");
287
302
  const nodePath = await import("node:path");
288
303
  const artifact = nodePath.join(root, PUBLISH_OUTPUT_DIR, DOCUMENT_SET_DIR, DOCUMENT_SET_FILE);
289
304
  await rm(artifact, { force: true }).catch((error) => {
@@ -291,6 +306,29 @@ function createDocumentSet(routerBasePath) {
291
306
  });
292
307
  if (documents.size === 0) return;
293
308
  const clientDir = nodePath.resolve(root, clientOutputDir);
309
+ const label = publicDirLabel(root, publicDir, nodePath);
310
+ const collisions = [];
311
+ if (publicDir !== "") {
312
+ const candidates = [...documents].map(([route, file]) => ({
313
+ route,
314
+ file
315
+ }));
316
+ const committed = await whichExist(candidates.map(({ file }) => nodePath.join(publicDir, file)), access);
317
+ const suspects = candidates.filter((_candidate, index) => committed[index]);
318
+ const survived = await whichMatch(suspects.map(({ file }) => [nodePath.join(publicDir, file), nodePath.resolve(clientDir, file)]), readFile);
319
+ for (const [index, suspect] of suspects.entries()) {
320
+ if (!survived[index]) {
321
+ collisions.push(suspect);
322
+ continue;
323
+ }
324
+ documents.delete(suspect.route);
325
+ logWarning(`Not publishing prerendered route ${suspect.route}: it would serve the committed ${label}/${suspect.file}, which the prerender pass left untouched.`);
326
+ }
327
+ }
328
+ if (collisions.length > 0) {
329
+ const pairs = collisions.sort(compareRoutes).map(({ route, file }) => ` ${route} overwrites ${label}/${file}`);
330
+ 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\`).`);
331
+ }
294
332
  const entries = [...documents].map(([route, file]) => ({
295
333
  route,
296
334
  file
package/dist/index.js CHANGED
@@ -128,7 +128,7 @@ function bundledDevCssUrlShim() {
128
128
  //#endregion
129
129
  //#region package.json
130
130
  var name = "@lovable.dev/vite-tanstack-config";
131
- var version = "2.18.0";
131
+ var version = "2.19.1";
132
132
  //#endregion
133
133
  //#region src/logging.ts
134
134
  const PACKAGE_NAME = "@lovable.dev/vite-tanstack-config";
@@ -174,15 +174,17 @@ function normalizeBasePath(raw) {
174
174
  return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
175
175
  }
176
176
  function withoutBasePath(route, basePath) {
177
- if (route === basePath) return "/";
178
177
  if (basePath === "") return route;
179
- return route.startsWith(`${basePath}/`) ? route.slice(basePath.length) : route;
178
+ if (route === basePath) return "/";
179
+ if (route.startsWith(`${basePath}/`)) return route.slice(basePath.length);
180
+ return route.startsWith(basePath) ? void 0 : route;
180
181
  }
181
182
  function documentEntryForPage(page, basePath = "") {
182
183
  if (typeof page.prerender?.outputPath === "string") return void 0;
183
184
  const pagePath = normalizeRoute(page.path);
184
185
  if (pagePath === void 0) return void 0;
185
186
  const inner = withoutBasePath(pagePath, basePath);
187
+ if (inner === void 0) return void 0;
186
188
  const route = normalizeRoute(`${basePath}${inner}`);
187
189
  if (route === void 0) return void 0;
188
190
  const file = inner === "/" ? "index.html" : `${inner.slice(1)}/index.html`;
@@ -229,13 +231,24 @@ function serializeDocumentSet(entries, generator) {
229
231
  });
230
232
  }
231
233
  const EXISTENCE_BATCH_SIZE = 64;
234
+ async function inBatches(items, run) {
235
+ const results = [];
236
+ for (let i = 0; i < items.length; i += EXISTENCE_BATCH_SIZE) results.push(...await Promise.all(items.slice(i, i + EXISTENCE_BATCH_SIZE).map(run)));
237
+ return results;
238
+ }
232
239
  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;
240
+ return inBatches(paths, (path) => access(path).then(() => true, () => false));
241
+ }
242
+ async function whichMatch(pairs, readFile) {
243
+ const read = (path) => readFile(path).catch((error) => {
244
+ if (error.code !== "ENOENT") logWarning(`Could not read ${path} while checking prerender collisions: ${String(error)}`);
245
+ });
246
+ return inBatches(pairs, ([left, right]) => Promise.all([read(left), read(right)]).then(([a, b]) => a !== void 0 && b !== void 0 && a.equals(b)));
247
+ }
248
+ /** How the error names the public dir, which vite lets a project point anywhere. */
249
+ function publicDirLabel(root, publicDir, nodePath) {
250
+ const relative = nodePath.relative(root, publicDir);
251
+ return relative !== "" && !relative.startsWith("..") ? relative.split(nodePath.sep).join("/") : publicDir;
239
252
  }
240
253
  function createDocumentSet(routerBasePath) {
241
254
  const basePath = normalizeBasePath(routerBasePath);
@@ -244,6 +257,7 @@ function createDocumentSet(routerBasePath) {
244
257
  const documents = /* @__PURE__ */ new Map();
245
258
  let root = process.cwd();
246
259
  let clientOutputDir = CLIENT_OUTPUT_DIR;
260
+ let publicDir = "";
247
261
  const plugin = {
248
262
  name: DOCUMENT_SET_PLUGIN_NAME,
249
263
  apply: "build",
@@ -255,11 +269,12 @@ function createDocumentSet(routerBasePath) {
255
269
  const ours = names.indexOf(DOCUMENT_SET_PLUGIN_NAME);
256
270
  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
271
  clientOutputDir = config.environments?.client?.build?.outDir ?? CLIENT_OUTPUT_DIR;
272
+ publicDir = (config.environments?.client?.build?.copyPublicDir ?? config.build?.copyPublicDir) !== false && typeof config.publicDir === "string" ? config.publicDir : "";
258
273
  },
259
274
  buildApp: {
260
275
  order: "post",
261
276
  async handler() {
262
- const { access, mkdir, rm, writeFile } = await import("node:fs/promises");
277
+ const { access, mkdir, readFile, rm, writeFile } = await import("node:fs/promises");
263
278
  const nodePath = await import("node:path");
264
279
  const artifact = nodePath.join(root, PUBLISH_OUTPUT_DIR, DOCUMENT_SET_DIR, DOCUMENT_SET_FILE);
265
280
  await rm(artifact, { force: true }).catch((error) => {
@@ -267,6 +282,29 @@ function createDocumentSet(routerBasePath) {
267
282
  });
268
283
  if (documents.size === 0) return;
269
284
  const clientDir = nodePath.resolve(root, clientOutputDir);
285
+ const label = publicDirLabel(root, publicDir, nodePath);
286
+ const collisions = [];
287
+ if (publicDir !== "") {
288
+ const candidates = [...documents].map(([route, file]) => ({
289
+ route,
290
+ file
291
+ }));
292
+ const committed = await whichExist(candidates.map(({ file }) => nodePath.join(publicDir, file)), access);
293
+ const suspects = candidates.filter((_candidate, index) => committed[index]);
294
+ const survived = await whichMatch(suspects.map(({ file }) => [nodePath.join(publicDir, file), nodePath.resolve(clientDir, file)]), readFile);
295
+ for (const [index, suspect] of suspects.entries()) {
296
+ if (!survived[index]) {
297
+ collisions.push(suspect);
298
+ continue;
299
+ }
300
+ documents.delete(suspect.route);
301
+ logWarning(`Not publishing prerendered route ${suspect.route}: it would serve the committed ${label}/${suspect.file}, which the prerender pass left untouched.`);
302
+ }
303
+ }
304
+ if (collisions.length > 0) {
305
+ const pairs = collisions.sort(compareRoutes).map(({ route, file }) => ` ${route} overwrites ${label}/${file}`);
306
+ 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\`).`);
307
+ }
270
308
  const entries = [...documents].map(([route, file]) => ({
271
309
  route,
272
310
  file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovable.dev/vite-tanstack-config",
3
- "version": "2.18.0",
3
+ "version": "2.19.1",
4
4
  "description": "Vite config wrapper for Lovable TanStack Start projects",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",