@lovable.dev/vite-tanstack-config 2.17.1 → 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/index.cjs +193 -14
- package/dist/index.d.cts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +193 -14
- package/package.json +1 -1
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:
|
|
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 =
|
|
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 =
|
|
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 ${
|
|
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:
|
|
876
|
-
serverDir:
|
|
877
|
-
publicDir:
|
|
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/
|
|
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,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:
|
|
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 =
|
|
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 =
|
|
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 ${
|
|
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:
|
|
852
|
-
serverDir:
|
|
853
|
-
publicDir:
|
|
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,
|