@lovable.dev/vite-tanstack-config 2.16.0 → 2.17.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/hmr-gate.cjs +62 -4
- package/dist/hmr-gate.js +62 -4
- package/dist/index.cjs +110 -0
- package/dist/index.js +110 -0
- package/package.json +1 -1
package/dist/hmr-gate.cjs
CHANGED
|
@@ -8,6 +8,24 @@ function requestPathname(req) {
|
|
|
8
8
|
const queryStart = url.indexOf("?");
|
|
9
9
|
return queryStart === -1 ? url : url.slice(0, queryStart);
|
|
10
10
|
}
|
|
11
|
+
const nonNavigationPrefixes = [
|
|
12
|
+
"/@vite/",
|
|
13
|
+
"/@fs/",
|
|
14
|
+
"/@id/",
|
|
15
|
+
"/@react-refresh",
|
|
16
|
+
"/api/",
|
|
17
|
+
"/go-api/",
|
|
18
|
+
"/assets/",
|
|
19
|
+
"/src/",
|
|
20
|
+
"/_",
|
|
21
|
+
"/favicon"
|
|
22
|
+
];
|
|
23
|
+
function isHtmlNavigationRequest(req) {
|
|
24
|
+
if (req.method !== "GET") return false;
|
|
25
|
+
const url = req.url;
|
|
26
|
+
if (!url || nonNavigationPrefixes.some((prefix) => url.startsWith(prefix))) return false;
|
|
27
|
+
return (req.headers.accept ?? "").includes("text/html");
|
|
28
|
+
}
|
|
11
29
|
function gateStatusHeaders(req) {
|
|
12
30
|
const origin = req.headers.origin;
|
|
13
31
|
const headers = {
|
|
@@ -233,6 +251,7 @@ function hmrGatePlugin(options = {}) {
|
|
|
233
251
|
lastUpdateKey = "";
|
|
234
252
|
}
|
|
235
253
|
let classicFailOpenTimer = null;
|
|
254
|
+
let errorServedSinceFlush = false;
|
|
236
255
|
let transportMode = false;
|
|
237
256
|
let swallowedUpdates = 0;
|
|
238
257
|
let heldReload = false;
|
|
@@ -420,6 +439,12 @@ function hmrGatePlugin(options = {}) {
|
|
|
420
439
|
});
|
|
421
440
|
srv.middlewares.use((req, res, next) => {
|
|
422
441
|
if (req.method !== "POST" || requestPathname(req) !== "/__hmr_flush") return next();
|
|
442
|
+
const flushOrigin = req.headers.origin;
|
|
443
|
+
if (typeof flushOrigin === "string" && !statusCorsOriginRe.test(flushOrigin)) {
|
|
444
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
445
|
+
res.end(JSON.stringify({ error: "origin not allowed" }));
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
423
448
|
const priorDrainUntil = drainUntil;
|
|
424
449
|
const released = releaseParked();
|
|
425
450
|
drainUntil = Date.now() + drainMs;
|
|
@@ -494,6 +519,23 @@ function hmrGatePlugin(options = {}) {
|
|
|
494
519
|
});
|
|
495
520
|
return;
|
|
496
521
|
}
|
|
522
|
+
srv.middlewares.use((req, res, next) => {
|
|
523
|
+
const originalEnd = res.end.bind(res);
|
|
524
|
+
res.end = ((...args) => {
|
|
525
|
+
if (res.statusCode >= 500 && isHtmlNavigationRequest(req)) {
|
|
526
|
+
errorServedSinceFlush = true;
|
|
527
|
+
if (classicFailOpenTimer === null) {
|
|
528
|
+
classicFailOpenTimer = setTimeout(() => {
|
|
529
|
+
classicFailOpenTimer = null;
|
|
530
|
+
flushPending();
|
|
531
|
+
}, maxHoldMs);
|
|
532
|
+
classicFailOpenTimer.unref?.();
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return originalEnd(...args);
|
|
536
|
+
});
|
|
537
|
+
next();
|
|
538
|
+
});
|
|
497
539
|
const _emit = srv.watcher.emit.bind(srv.watcher);
|
|
498
540
|
originalEmit = _emit;
|
|
499
541
|
const flushPending = () => {
|
|
@@ -502,6 +544,9 @@ function hmrGatePlugin(options = {}) {
|
|
|
502
544
|
classicFailOpenTimer = null;
|
|
503
545
|
}
|
|
504
546
|
if (pending.size > 0) noteUpdateFlush();
|
|
547
|
+
const errorServed = errorServedSinceFlush;
|
|
548
|
+
errorServedSinceFlush = false;
|
|
549
|
+
if (errorServed) srv.moduleGraph.invalidateAll();
|
|
505
550
|
const entries = Array.from(pending.entries());
|
|
506
551
|
pending.clear();
|
|
507
552
|
for (const [f] of entries) srv.moduleGraph.onFileChange(f);
|
|
@@ -514,7 +559,11 @@ function hmrGatePlugin(options = {}) {
|
|
|
514
559
|
}
|
|
515
560
|
if (fullReload) srv.ws.send({ type: "full-reload" });
|
|
516
561
|
}
|
|
517
|
-
|
|
562
|
+
if (errorServed && (entries.length === 0 || !fullReload)) srv.ws.send({ type: "full-reload" });
|
|
563
|
+
return {
|
|
564
|
+
entries,
|
|
565
|
+
errorRecovery: errorServed
|
|
566
|
+
};
|
|
518
567
|
};
|
|
519
568
|
srv.watcher.emit = ((event, ...args) => {
|
|
520
569
|
if (typeof event === "string" && gatedEvents.has(event) && !isPassthrough(args[0])) {
|
|
@@ -523,7 +572,7 @@ function hmrGatePlugin(options = {}) {
|
|
|
523
572
|
const hash = fileContentHash(filePath);
|
|
524
573
|
if (hash !== null && hash === lastFlushedHashes.get(filePath)) {
|
|
525
574
|
pending.delete(filePath);
|
|
526
|
-
if (pending.size === 0 && classicFailOpenTimer !== null) {
|
|
575
|
+
if (pending.size === 0 && !errorServedSinceFlush && classicFailOpenTimer !== null) {
|
|
527
576
|
clearTimeout(classicFailOpenTimer);
|
|
528
577
|
classicFailOpenTimer = null;
|
|
529
578
|
}
|
|
@@ -550,11 +599,18 @@ function hmrGatePlugin(options = {}) {
|
|
|
550
599
|
});
|
|
551
600
|
srv.middlewares.use((req, res, next) => {
|
|
552
601
|
if (req.method !== "POST" || requestPathname(req) !== "/__hmr_flush") return next();
|
|
553
|
-
const
|
|
602
|
+
const origin = req.headers.origin;
|
|
603
|
+
if (typeof origin === "string" && !statusCorsOriginRe.test(origin)) {
|
|
604
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
605
|
+
res.end(JSON.stringify({ error: "origin not allowed" }));
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
const { entries, errorRecovery } = flushPending();
|
|
554
609
|
const files = entries.map(([f]) => f);
|
|
555
610
|
const body = JSON.stringify({
|
|
556
611
|
flushed: files,
|
|
557
612
|
count: entries.length,
|
|
613
|
+
errorRecovery,
|
|
558
614
|
mode: fullReload ? "full-reload" : "granular"
|
|
559
615
|
});
|
|
560
616
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -567,7 +623,8 @@ function hmrGatePlugin(options = {}) {
|
|
|
567
623
|
const body = JSON.stringify({
|
|
568
624
|
enabled: true,
|
|
569
625
|
pending: serialized,
|
|
570
|
-
count: pending.size,
|
|
626
|
+
count: pending.size + (errorServedSinceFlush ? 1 : 0),
|
|
627
|
+
errorRecovery: errorServedSinceFlush,
|
|
571
628
|
mode: fullReload ? "full-reload" : "granular"
|
|
572
629
|
});
|
|
573
630
|
res.writeHead(200, gateStatusHeaders(req));
|
|
@@ -609,6 +666,7 @@ function hmrGatePlugin(options = {}) {
|
|
|
609
666
|
drainUntil = 0;
|
|
610
667
|
pending.clear();
|
|
611
668
|
lastFlushedHashes.clear();
|
|
669
|
+
errorServedSinceFlush = false;
|
|
612
670
|
server = null;
|
|
613
671
|
originalEmit = null;
|
|
614
672
|
}
|
package/dist/hmr-gate.js
CHANGED
|
@@ -7,6 +7,24 @@ function requestPathname(req) {
|
|
|
7
7
|
const queryStart = url.indexOf("?");
|
|
8
8
|
return queryStart === -1 ? url : url.slice(0, queryStart);
|
|
9
9
|
}
|
|
10
|
+
const nonNavigationPrefixes = [
|
|
11
|
+
"/@vite/",
|
|
12
|
+
"/@fs/",
|
|
13
|
+
"/@id/",
|
|
14
|
+
"/@react-refresh",
|
|
15
|
+
"/api/",
|
|
16
|
+
"/go-api/",
|
|
17
|
+
"/assets/",
|
|
18
|
+
"/src/",
|
|
19
|
+
"/_",
|
|
20
|
+
"/favicon"
|
|
21
|
+
];
|
|
22
|
+
function isHtmlNavigationRequest(req) {
|
|
23
|
+
if (req.method !== "GET") return false;
|
|
24
|
+
const url = req.url;
|
|
25
|
+
if (!url || nonNavigationPrefixes.some((prefix) => url.startsWith(prefix))) return false;
|
|
26
|
+
return (req.headers.accept ?? "").includes("text/html");
|
|
27
|
+
}
|
|
10
28
|
function gateStatusHeaders(req) {
|
|
11
29
|
const origin = req.headers.origin;
|
|
12
30
|
const headers = {
|
|
@@ -232,6 +250,7 @@ function hmrGatePlugin(options = {}) {
|
|
|
232
250
|
lastUpdateKey = "";
|
|
233
251
|
}
|
|
234
252
|
let classicFailOpenTimer = null;
|
|
253
|
+
let errorServedSinceFlush = false;
|
|
235
254
|
let transportMode = false;
|
|
236
255
|
let swallowedUpdates = 0;
|
|
237
256
|
let heldReload = false;
|
|
@@ -419,6 +438,12 @@ function hmrGatePlugin(options = {}) {
|
|
|
419
438
|
});
|
|
420
439
|
srv.middlewares.use((req, res, next) => {
|
|
421
440
|
if (req.method !== "POST" || requestPathname(req) !== "/__hmr_flush") return next();
|
|
441
|
+
const flushOrigin = req.headers.origin;
|
|
442
|
+
if (typeof flushOrigin === "string" && !statusCorsOriginRe.test(flushOrigin)) {
|
|
443
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
444
|
+
res.end(JSON.stringify({ error: "origin not allowed" }));
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
422
447
|
const priorDrainUntil = drainUntil;
|
|
423
448
|
const released = releaseParked();
|
|
424
449
|
drainUntil = Date.now() + drainMs;
|
|
@@ -493,6 +518,23 @@ function hmrGatePlugin(options = {}) {
|
|
|
493
518
|
});
|
|
494
519
|
return;
|
|
495
520
|
}
|
|
521
|
+
srv.middlewares.use((req, res, next) => {
|
|
522
|
+
const originalEnd = res.end.bind(res);
|
|
523
|
+
res.end = ((...args) => {
|
|
524
|
+
if (res.statusCode >= 500 && isHtmlNavigationRequest(req)) {
|
|
525
|
+
errorServedSinceFlush = true;
|
|
526
|
+
if (classicFailOpenTimer === null) {
|
|
527
|
+
classicFailOpenTimer = setTimeout(() => {
|
|
528
|
+
classicFailOpenTimer = null;
|
|
529
|
+
flushPending();
|
|
530
|
+
}, maxHoldMs);
|
|
531
|
+
classicFailOpenTimer.unref?.();
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return originalEnd(...args);
|
|
535
|
+
});
|
|
536
|
+
next();
|
|
537
|
+
});
|
|
496
538
|
const _emit = srv.watcher.emit.bind(srv.watcher);
|
|
497
539
|
originalEmit = _emit;
|
|
498
540
|
const flushPending = () => {
|
|
@@ -501,6 +543,9 @@ function hmrGatePlugin(options = {}) {
|
|
|
501
543
|
classicFailOpenTimer = null;
|
|
502
544
|
}
|
|
503
545
|
if (pending.size > 0) noteUpdateFlush();
|
|
546
|
+
const errorServed = errorServedSinceFlush;
|
|
547
|
+
errorServedSinceFlush = false;
|
|
548
|
+
if (errorServed) srv.moduleGraph.invalidateAll();
|
|
504
549
|
const entries = Array.from(pending.entries());
|
|
505
550
|
pending.clear();
|
|
506
551
|
for (const [f] of entries) srv.moduleGraph.onFileChange(f);
|
|
@@ -513,7 +558,11 @@ function hmrGatePlugin(options = {}) {
|
|
|
513
558
|
}
|
|
514
559
|
if (fullReload) srv.ws.send({ type: "full-reload" });
|
|
515
560
|
}
|
|
516
|
-
|
|
561
|
+
if (errorServed && (entries.length === 0 || !fullReload)) srv.ws.send({ type: "full-reload" });
|
|
562
|
+
return {
|
|
563
|
+
entries,
|
|
564
|
+
errorRecovery: errorServed
|
|
565
|
+
};
|
|
517
566
|
};
|
|
518
567
|
srv.watcher.emit = ((event, ...args) => {
|
|
519
568
|
if (typeof event === "string" && gatedEvents.has(event) && !isPassthrough(args[0])) {
|
|
@@ -522,7 +571,7 @@ function hmrGatePlugin(options = {}) {
|
|
|
522
571
|
const hash = fileContentHash(filePath);
|
|
523
572
|
if (hash !== null && hash === lastFlushedHashes.get(filePath)) {
|
|
524
573
|
pending.delete(filePath);
|
|
525
|
-
if (pending.size === 0 && classicFailOpenTimer !== null) {
|
|
574
|
+
if (pending.size === 0 && !errorServedSinceFlush && classicFailOpenTimer !== null) {
|
|
526
575
|
clearTimeout(classicFailOpenTimer);
|
|
527
576
|
classicFailOpenTimer = null;
|
|
528
577
|
}
|
|
@@ -549,11 +598,18 @@ function hmrGatePlugin(options = {}) {
|
|
|
549
598
|
});
|
|
550
599
|
srv.middlewares.use((req, res, next) => {
|
|
551
600
|
if (req.method !== "POST" || requestPathname(req) !== "/__hmr_flush") return next();
|
|
552
|
-
const
|
|
601
|
+
const origin = req.headers.origin;
|
|
602
|
+
if (typeof origin === "string" && !statusCorsOriginRe.test(origin)) {
|
|
603
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
604
|
+
res.end(JSON.stringify({ error: "origin not allowed" }));
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
const { entries, errorRecovery } = flushPending();
|
|
553
608
|
const files = entries.map(([f]) => f);
|
|
554
609
|
const body = JSON.stringify({
|
|
555
610
|
flushed: files,
|
|
556
611
|
count: entries.length,
|
|
612
|
+
errorRecovery,
|
|
557
613
|
mode: fullReload ? "full-reload" : "granular"
|
|
558
614
|
});
|
|
559
615
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -566,7 +622,8 @@ function hmrGatePlugin(options = {}) {
|
|
|
566
622
|
const body = JSON.stringify({
|
|
567
623
|
enabled: true,
|
|
568
624
|
pending: serialized,
|
|
569
|
-
count: pending.size,
|
|
625
|
+
count: pending.size + (errorServedSinceFlush ? 1 : 0),
|
|
626
|
+
errorRecovery: errorServedSinceFlush,
|
|
570
627
|
mode: fullReload ? "full-reload" : "granular"
|
|
571
628
|
});
|
|
572
629
|
res.writeHead(200, gateStatusHeaders(req));
|
|
@@ -608,6 +665,7 @@ function hmrGatePlugin(options = {}) {
|
|
|
608
665
|
drainUntil = 0;
|
|
609
666
|
pending.clear();
|
|
610
667
|
lastFlushedHashes.clear();
|
|
668
|
+
errorServedSinceFlush = false;
|
|
611
669
|
server = null;
|
|
612
670
|
originalEmit = null;
|
|
613
671
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -551,6 +551,107 @@ function stripRedundantNodejsCompatFlag() {
|
|
|
551
551
|
}
|
|
552
552
|
};
|
|
553
553
|
}
|
|
554
|
+
const NITRO_SERVER_ENTRY = "index.mjs";
|
|
555
|
+
const NITRO_WRANGLER_CONFIG = "wrangler.json";
|
|
556
|
+
const DEFAULT_SERVER_OUTPUT_DIR = "dist/server";
|
|
557
|
+
const DEFAULT_SERVER_ENTRY_NAME = "server";
|
|
558
|
+
const PRERENDER_PREVIEW_HOST = "127.0.0.1";
|
|
559
|
+
const TSS_PRERENDERING_ENV = "TSS_PRERENDERING";
|
|
560
|
+
const SHIM_MARKER = "// Generated by @lovable.dev/vite-tanstack-config for the prerender preview server.";
|
|
561
|
+
function prerenderShimSource(vars) {
|
|
562
|
+
return `${SHIM_MARKER}
|
|
563
|
+
import server from "./${NITRO_SERVER_ENTRY}";
|
|
564
|
+
|
|
565
|
+
const env = ${JSON.stringify(vars)};
|
|
566
|
+
const ctx = { waitUntil() {}, passThroughOnException() {}, props: {} };
|
|
567
|
+
|
|
568
|
+
export default {
|
|
569
|
+
fetch(request) {
|
|
570
|
+
// srvx's NodeRequest exposes \`ip\` as a getter-only accessor and nitro's
|
|
571
|
+
// cloudflare module handler assigns to it; shadow it with a writable one.
|
|
572
|
+
Object.defineProperty(request, "ip", { value: undefined, writable: true, configurable: true });
|
|
573
|
+
return server.fetch(request, env, ctx);
|
|
574
|
+
},
|
|
575
|
+
};
|
|
576
|
+
`;
|
|
577
|
+
}
|
|
578
|
+
function prerenderPreviewShim(state) {
|
|
579
|
+
let root = process.cwd();
|
|
580
|
+
let serverOutDir = DEFAULT_SERVER_OUTPUT_DIR;
|
|
581
|
+
let serverEntryName = DEFAULT_SERVER_ENTRY_NAME;
|
|
582
|
+
return {
|
|
583
|
+
name: "lovable-prerender-preview-shim",
|
|
584
|
+
apply: "build",
|
|
585
|
+
config(userConfig) {
|
|
586
|
+
const ssrBuild = userConfig.environments?.ssr?.build;
|
|
587
|
+
if (ssrBuild?.outDir) serverOutDir = ssrBuild.outDir;
|
|
588
|
+
const input = ssrBuild?.rolldownOptions?.input ?? ssrBuild?.rollupOptions?.input;
|
|
589
|
+
if (typeof input === "string") serverEntryName = input;
|
|
590
|
+
},
|
|
591
|
+
configResolved(config) {
|
|
592
|
+
root = config.root;
|
|
593
|
+
},
|
|
594
|
+
buildApp: {
|
|
595
|
+
order: "post",
|
|
596
|
+
handler: async () => {
|
|
597
|
+
const { access, readFile, writeFile } = await import("node:fs/promises");
|
|
598
|
+
const nodePath = await import("node:path");
|
|
599
|
+
const exists = async (candidate) => {
|
|
600
|
+
try {
|
|
601
|
+
await access(candidate);
|
|
602
|
+
return true;
|
|
603
|
+
} catch {
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
const outDir = nodePath.resolve(root, serverOutDir);
|
|
608
|
+
if (!await exists(nodePath.join(outDir, NITRO_SERVER_ENTRY))) return;
|
|
609
|
+
const basename = nodePath.basename(serverEntryName, nodePath.extname(serverEntryName));
|
|
610
|
+
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\`.`);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
const target = nodePath.join(outDir, `${basename}.js`);
|
|
615
|
+
const occupant = await readFile(target, "utf8").catch(() => void 0);
|
|
616
|
+
if (occupant !== void 0 && !occupant.startsWith(SHIM_MARKER)) {
|
|
617
|
+
logWarning(`Cannot prerender: ${basename}.js already exists in ${serverOutDir} and was not written by this plugin.`);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
let vars = {};
|
|
621
|
+
try {
|
|
622
|
+
const candidate = JSON.parse(await readFile(nodePath.join(outDir, NITRO_WRANGLER_CONFIG), "utf8"))?.vars;
|
|
623
|
+
if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) vars = candidate;
|
|
624
|
+
} catch {}
|
|
625
|
+
await writeFile(target, prerenderShimSource(vars));
|
|
626
|
+
state.file = target;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
function prerenderPreviewShimCleanup(state) {
|
|
632
|
+
return {
|
|
633
|
+
name: "lovable-prerender-preview-shim-cleanup",
|
|
634
|
+
apply: "build",
|
|
635
|
+
enforce: "post",
|
|
636
|
+
buildApp: {
|
|
637
|
+
order: "post",
|
|
638
|
+
handler: async () => {
|
|
639
|
+
if (!state.file) return;
|
|
640
|
+
const { rm } = await import("node:fs/promises");
|
|
641
|
+
await rm(state.file, { force: true });
|
|
642
|
+
state.file = void 0;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
function prerenderIsEnabled(startOptions) {
|
|
648
|
+
const spa = startOptions.spa;
|
|
649
|
+
if (spa && (spa.enabled ?? true)) return true;
|
|
650
|
+
const prerender = startOptions.prerender;
|
|
651
|
+
if (prerender?.enabled === false) return false;
|
|
652
|
+
const pages = Array.isArray(startOptions.pages) ? startOptions.pages : [];
|
|
653
|
+
return !!(prerender?.enabled ?? pages.some((page) => !!page?.prerender?.enabled));
|
|
654
|
+
}
|
|
554
655
|
function devSsrErrorLogger() {
|
|
555
656
|
let lastCapture;
|
|
556
657
|
const CAPTURE_TTL_MS = 5e3;
|
|
@@ -730,6 +831,7 @@ function defineConfig(configOrOptions = {}) {
|
|
|
730
831
|
specifiers: ["server-only"]
|
|
731
832
|
}
|
|
732
833
|
} }, options.tanstackStart ?? {});
|
|
834
|
+
const prerenderEnabled = prerenderIsEnabled(tanstackStartOptions);
|
|
733
835
|
if (command === "build" && isSandbox) {
|
|
734
836
|
const { startOptions, skipReporter } = pinPrerenderForLovableBuild(tanstackStartOptions);
|
|
735
837
|
internalPlugins.push(tanstackStart(startOptions), skipReporter);
|
|
@@ -782,6 +884,10 @@ function defineConfig(configOrOptions = {}) {
|
|
|
782
884
|
}
|
|
783
885
|
} else if (!nitroOpts.preset && !await nitroSupportsDefaultPreset()) logWarning("The installed `nitro` predates the `defaultPreset` option (3.0.260603-beta), so the Cloudflare fallback is ignored and a zero-config build may target Node instead. Upgrade nitro to >=3.0.260603-beta, or set an explicit `nitro: { preset: \"cloudflare-module\" }`.");
|
|
784
886
|
internalPlugins.push(nitro(nitroOpts));
|
|
887
|
+
if (prerenderEnabled) {
|
|
888
|
+
const shimState = {};
|
|
889
|
+
internalPlugins.push(prerenderPreviewShim(shimState), prerenderPreviewShimCleanup(shimState));
|
|
890
|
+
}
|
|
785
891
|
if (isSandbox) internalPlugins.push(stripRedundantNodejsCompatFlag());
|
|
786
892
|
}
|
|
787
893
|
}
|
|
@@ -857,6 +963,10 @@ function defineConfig(configOrOptions = {}) {
|
|
|
857
963
|
host: "::",
|
|
858
964
|
port: 8080
|
|
859
965
|
} }, config);
|
|
966
|
+
if (env.isPreview && prerenderEnabled && process.env[TSS_PRERENDERING_ENV] === "true") config = (0, vite.mergeConfig)({ preview: {
|
|
967
|
+
host: PRERENDER_PREVIEW_HOST,
|
|
968
|
+
port: 0
|
|
969
|
+
} }, config);
|
|
860
970
|
if (isSandbox) return config;
|
|
861
971
|
return applyWatchDebounceDefaults(config);
|
|
862
972
|
};
|
package/dist/index.js
CHANGED
|
@@ -527,6 +527,107 @@ function stripRedundantNodejsCompatFlag() {
|
|
|
527
527
|
}
|
|
528
528
|
};
|
|
529
529
|
}
|
|
530
|
+
const NITRO_SERVER_ENTRY = "index.mjs";
|
|
531
|
+
const NITRO_WRANGLER_CONFIG = "wrangler.json";
|
|
532
|
+
const DEFAULT_SERVER_OUTPUT_DIR = "dist/server";
|
|
533
|
+
const DEFAULT_SERVER_ENTRY_NAME = "server";
|
|
534
|
+
const PRERENDER_PREVIEW_HOST = "127.0.0.1";
|
|
535
|
+
const TSS_PRERENDERING_ENV = "TSS_PRERENDERING";
|
|
536
|
+
const SHIM_MARKER = "// Generated by @lovable.dev/vite-tanstack-config for the prerender preview server.";
|
|
537
|
+
function prerenderShimSource(vars) {
|
|
538
|
+
return `${SHIM_MARKER}
|
|
539
|
+
import server from "./${NITRO_SERVER_ENTRY}";
|
|
540
|
+
|
|
541
|
+
const env = ${JSON.stringify(vars)};
|
|
542
|
+
const ctx = { waitUntil() {}, passThroughOnException() {}, props: {} };
|
|
543
|
+
|
|
544
|
+
export default {
|
|
545
|
+
fetch(request) {
|
|
546
|
+
// srvx's NodeRequest exposes \`ip\` as a getter-only accessor and nitro's
|
|
547
|
+
// cloudflare module handler assigns to it; shadow it with a writable one.
|
|
548
|
+
Object.defineProperty(request, "ip", { value: undefined, writable: true, configurable: true });
|
|
549
|
+
return server.fetch(request, env, ctx);
|
|
550
|
+
},
|
|
551
|
+
};
|
|
552
|
+
`;
|
|
553
|
+
}
|
|
554
|
+
function prerenderPreviewShim(state) {
|
|
555
|
+
let root = process.cwd();
|
|
556
|
+
let serverOutDir = DEFAULT_SERVER_OUTPUT_DIR;
|
|
557
|
+
let serverEntryName = DEFAULT_SERVER_ENTRY_NAME;
|
|
558
|
+
return {
|
|
559
|
+
name: "lovable-prerender-preview-shim",
|
|
560
|
+
apply: "build",
|
|
561
|
+
config(userConfig) {
|
|
562
|
+
const ssrBuild = userConfig.environments?.ssr?.build;
|
|
563
|
+
if (ssrBuild?.outDir) serverOutDir = ssrBuild.outDir;
|
|
564
|
+
const input = ssrBuild?.rolldownOptions?.input ?? ssrBuild?.rollupOptions?.input;
|
|
565
|
+
if (typeof input === "string") serverEntryName = input;
|
|
566
|
+
},
|
|
567
|
+
configResolved(config) {
|
|
568
|
+
root = config.root;
|
|
569
|
+
},
|
|
570
|
+
buildApp: {
|
|
571
|
+
order: "post",
|
|
572
|
+
handler: async () => {
|
|
573
|
+
const { access, readFile, writeFile } = await import("node:fs/promises");
|
|
574
|
+
const nodePath = await import("node:path");
|
|
575
|
+
const exists = async (candidate) => {
|
|
576
|
+
try {
|
|
577
|
+
await access(candidate);
|
|
578
|
+
return true;
|
|
579
|
+
} catch {
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
const outDir = nodePath.resolve(root, serverOutDir);
|
|
584
|
+
if (!await exists(nodePath.join(outDir, NITRO_SERVER_ENTRY))) return;
|
|
585
|
+
const basename = nodePath.basename(serverEntryName, nodePath.extname(serverEntryName));
|
|
586
|
+
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\`.`);
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
const target = nodePath.join(outDir, `${basename}.js`);
|
|
591
|
+
const occupant = await readFile(target, "utf8").catch(() => void 0);
|
|
592
|
+
if (occupant !== void 0 && !occupant.startsWith(SHIM_MARKER)) {
|
|
593
|
+
logWarning(`Cannot prerender: ${basename}.js already exists in ${serverOutDir} and was not written by this plugin.`);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
let vars = {};
|
|
597
|
+
try {
|
|
598
|
+
const candidate = JSON.parse(await readFile(nodePath.join(outDir, NITRO_WRANGLER_CONFIG), "utf8"))?.vars;
|
|
599
|
+
if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) vars = candidate;
|
|
600
|
+
} catch {}
|
|
601
|
+
await writeFile(target, prerenderShimSource(vars));
|
|
602
|
+
state.file = target;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
function prerenderPreviewShimCleanup(state) {
|
|
608
|
+
return {
|
|
609
|
+
name: "lovable-prerender-preview-shim-cleanup",
|
|
610
|
+
apply: "build",
|
|
611
|
+
enforce: "post",
|
|
612
|
+
buildApp: {
|
|
613
|
+
order: "post",
|
|
614
|
+
handler: async () => {
|
|
615
|
+
if (!state.file) return;
|
|
616
|
+
const { rm } = await import("node:fs/promises");
|
|
617
|
+
await rm(state.file, { force: true });
|
|
618
|
+
state.file = void 0;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
function prerenderIsEnabled(startOptions) {
|
|
624
|
+
const spa = startOptions.spa;
|
|
625
|
+
if (spa && (spa.enabled ?? true)) return true;
|
|
626
|
+
const prerender = startOptions.prerender;
|
|
627
|
+
if (prerender?.enabled === false) return false;
|
|
628
|
+
const pages = Array.isArray(startOptions.pages) ? startOptions.pages : [];
|
|
629
|
+
return !!(prerender?.enabled ?? pages.some((page) => !!page?.prerender?.enabled));
|
|
630
|
+
}
|
|
530
631
|
function devSsrErrorLogger() {
|
|
531
632
|
let lastCapture;
|
|
532
633
|
const CAPTURE_TTL_MS = 5e3;
|
|
@@ -706,6 +807,7 @@ function defineConfig(configOrOptions = {}) {
|
|
|
706
807
|
specifiers: ["server-only"]
|
|
707
808
|
}
|
|
708
809
|
} }, options.tanstackStart ?? {});
|
|
810
|
+
const prerenderEnabled = prerenderIsEnabled(tanstackStartOptions);
|
|
709
811
|
if (command === "build" && isSandbox) {
|
|
710
812
|
const { startOptions, skipReporter } = pinPrerenderForLovableBuild(tanstackStartOptions);
|
|
711
813
|
internalPlugins.push(tanstackStart(startOptions), skipReporter);
|
|
@@ -758,6 +860,10 @@ function defineConfig(configOrOptions = {}) {
|
|
|
758
860
|
}
|
|
759
861
|
} else if (!nitroOpts.preset && !await nitroSupportsDefaultPreset()) logWarning("The installed `nitro` predates the `defaultPreset` option (3.0.260603-beta), so the Cloudflare fallback is ignored and a zero-config build may target Node instead. Upgrade nitro to >=3.0.260603-beta, or set an explicit `nitro: { preset: \"cloudflare-module\" }`.");
|
|
760
862
|
internalPlugins.push(nitro(nitroOpts));
|
|
863
|
+
if (prerenderEnabled) {
|
|
864
|
+
const shimState = {};
|
|
865
|
+
internalPlugins.push(prerenderPreviewShim(shimState), prerenderPreviewShimCleanup(shimState));
|
|
866
|
+
}
|
|
761
867
|
if (isSandbox) internalPlugins.push(stripRedundantNodejsCompatFlag());
|
|
762
868
|
}
|
|
763
869
|
}
|
|
@@ -833,6 +939,10 @@ function defineConfig(configOrOptions = {}) {
|
|
|
833
939
|
host: "::",
|
|
834
940
|
port: 8080
|
|
835
941
|
} }, config);
|
|
942
|
+
if (env.isPreview && prerenderEnabled && process.env[TSS_PRERENDERING_ENV] === "true") config = mergeConfig({ preview: {
|
|
943
|
+
host: PRERENDER_PREVIEW_HOST,
|
|
944
|
+
port: 0
|
|
945
|
+
} }, config);
|
|
836
946
|
if (isSandbox) return config;
|
|
837
947
|
return applyWatchDebounceDefaults(config);
|
|
838
948
|
};
|