@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 +216 -14
- package/dist/index.d.cts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +216 -14
- package/package.json +1 -1
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:
|
|
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 =
|
|
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 =
|
|
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 ${
|
|
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:
|
|
876
|
-
serverDir:
|
|
877
|
-
publicDir:
|
|
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/
|
|
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/
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 ${
|
|
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:
|
|
852
|
-
serverDir:
|
|
853
|
-
publicDir:
|
|
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,
|