@lovable.dev/vite-tanstack-config 2.21.0 → 2.23.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/README.md +8 -0
- package/dist/index.cjs +389 -4
- package/dist/index.d.cts +9 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +389 -4
- package/package.json +12 -2
package/README.md
CHANGED
|
@@ -12,3 +12,11 @@ Vite config wrapper for Lovable TanStack Start projects.
|
|
|
12
12
|
## Customization
|
|
13
13
|
|
|
14
14
|
Apps can pass through Vite options and adjust the React, TanStack Start, Nitro, HMR gate, and error-logging behavior when they need to.
|
|
15
|
+
|
|
16
|
+
## Build exit watchdog
|
|
17
|
+
|
|
18
|
+
Inside a Lovable build with prerender enabled, the build process is forced to exit if it is still alive 60 seconds after the last build hook has returned, and a `[lovable-build-exit-report]` block naming the open handle types is written to stderr. Adjust it with `defineConfig({ buildExitWatchdog: { graceMs, enabled } })`; the grace is clamped to 5 s–10 min. The `LOVABLE_PRERENDER_EXIT_GRACE_MS` and `LOVABLE_PRERENDER_EXIT_WATCHDOG=off` environment variables override the config.
|
|
19
|
+
|
|
20
|
+
## Route manifest
|
|
21
|
+
|
|
22
|
+
Inside a Lovable build, the route surface of the app is written to `dist/.lovable/routes.json` after the build: `{ version, generator, basepath?, serverEntry, requestMiddleware?, routes }`, where `routes` is the generated route tree's `fullPaths` union in TanStack path syntax (`/posts/$postId`, `/files/$`, `/docs/{-$version}`). Lovable's serving layer uses it to answer requests no route can match without starting the app. The file is enumerated with `@tanstack/router-generator` through an in-memory overlay, so the build never rewrites `routeTree.gen.ts`, and it is skipped — with a warning naming the reason — whenever the app can answer paths outside its route tree: `tanstackStart.spa`, `router.virtualRouteConfig`, `nitro.routeRules`/`handlers`, a project `nitro.config.*` / `.config/nitro.*` / `.nitrorc` file (Nitro loads it on its own, so the wrapper cannot see what it adds), a `rewrite` in the router file, a `basepath` there that `tanstackStart.router.basepath` does not declare (a computed or shorthand one included), or a router file (`router.entry`, resolved the way TanStack Start resolves it) that cannot be read to check either.
|
package/dist/index.cjs
CHANGED
|
@@ -153,7 +153,7 @@ function bundledDevCssUrlShim() {
|
|
|
153
153
|
//#endregion
|
|
154
154
|
//#region package.json
|
|
155
155
|
var name = "@lovable.dev/vite-tanstack-config";
|
|
156
|
-
var version = "2.
|
|
156
|
+
var version = "2.23.0";
|
|
157
157
|
//#endregion
|
|
158
158
|
//#region src/logging.ts
|
|
159
159
|
const PACKAGE_NAME = "@lovable.dev/vite-tanstack-config";
|
|
@@ -168,7 +168,7 @@ const CLIENT_OUTPUT_DIR = `${PUBLISH_OUTPUT_DIR}/client`;
|
|
|
168
168
|
//#endregion
|
|
169
169
|
//#region src/documents.ts
|
|
170
170
|
const DOCUMENT_SET_VERSION = 1;
|
|
171
|
-
const GENERATOR = `${name}@${version}`;
|
|
171
|
+
const GENERATOR$1 = `${name}@${version}`;
|
|
172
172
|
const DOCUMENT_SET_DIR = ".lovable";
|
|
173
173
|
const DOCUMENT_SET_FILE = "output.json";
|
|
174
174
|
const DOCUMENT_SET_PLUGIN_NAME = "lovable:document-set";
|
|
@@ -341,7 +341,7 @@ function createDocumentSet(routerBasePath) {
|
|
|
341
341
|
}
|
|
342
342
|
logWarning(`Not publishing prerendered route ${entry.route}: it produced no ${entry.file}.`);
|
|
343
343
|
}
|
|
344
|
-
const payload = serializeDocumentSet(present, GENERATOR);
|
|
344
|
+
const payload = serializeDocumentSet(present, GENERATOR$1);
|
|
345
345
|
if (payload === void 0) return;
|
|
346
346
|
try {
|
|
347
347
|
await mkdir(nodePath.dirname(artifact), { recursive: true });
|
|
@@ -367,6 +367,301 @@ function createDocumentSet(routerBasePath) {
|
|
|
367
367
|
};
|
|
368
368
|
}
|
|
369
369
|
//#endregion
|
|
370
|
+
//#region src/routes.ts
|
|
371
|
+
const ROUTE_MANIFEST_VERSION = 1;
|
|
372
|
+
const GENERATOR = `${name}@${version}`;
|
|
373
|
+
const ROUTE_MANIFEST_DIR = ".lovable";
|
|
374
|
+
const ROUTE_MANIFEST_FILE = "routes.json";
|
|
375
|
+
const ROUTE_MANIFEST_MAX_ROUTES = 1e3;
|
|
376
|
+
const ROUTE_MANIFEST_MAX_BYTES = 512 * 1024;
|
|
377
|
+
const ROUTE_MANIFEST_PLUGIN_NAME = "lovable:route-manifest";
|
|
378
|
+
const ROUTER_GENERATOR_PACKAGE = "@tanstack/router-generator";
|
|
379
|
+
const ENTRY_EXTENSIONS = [
|
|
380
|
+
"",
|
|
381
|
+
".ts",
|
|
382
|
+
".js",
|
|
383
|
+
".mts",
|
|
384
|
+
".mjs",
|
|
385
|
+
".tsx",
|
|
386
|
+
".jsx"
|
|
387
|
+
];
|
|
388
|
+
const NITRO_CONFIG_EXTENSIONS = [
|
|
389
|
+
".js",
|
|
390
|
+
".ts",
|
|
391
|
+
".mjs",
|
|
392
|
+
".cjs",
|
|
393
|
+
".mts",
|
|
394
|
+
".cts",
|
|
395
|
+
".json",
|
|
396
|
+
".jsonc",
|
|
397
|
+
".json5",
|
|
398
|
+
".yaml",
|
|
399
|
+
".yml",
|
|
400
|
+
".toml"
|
|
401
|
+
];
|
|
402
|
+
const NITRO_CONFIG_FILES = [
|
|
403
|
+
...NITRO_CONFIG_EXTENSIONS.map((extension) => `nitro.config${extension}`),
|
|
404
|
+
...NITRO_CONFIG_EXTENSIONS.flatMap((extension) => [`.config/nitro${extension}`, `.config/nitro.config${extension}`]),
|
|
405
|
+
".nitrorc"
|
|
406
|
+
];
|
|
407
|
+
function subOptions(options, key) {
|
|
408
|
+
const value = options[key];
|
|
409
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
410
|
+
}
|
|
411
|
+
function stringOption(options, key, fallback) {
|
|
412
|
+
const value = options[key];
|
|
413
|
+
return typeof value === "string" ? value : fallback;
|
|
414
|
+
}
|
|
415
|
+
/** Why no manifest is emitted: the app can answer paths outside its route tree. */
|
|
416
|
+
function routeManifestOptOut(inputs) {
|
|
417
|
+
const { tanstackStart } = inputs;
|
|
418
|
+
if (tanstackStart.spa != null && subOptions(tanstackStart, "spa").enabled !== false) return "tanstackStart.spa serves the shell for every path";
|
|
419
|
+
if (subOptions(tanstackStart, "router").virtualRouteConfig !== void 0) return "tanstackStart.router.virtualRouteConfig is set";
|
|
420
|
+
if (typeof inputs.nitro === "object" && inputs.nitro !== null) {
|
|
421
|
+
for (const key of [
|
|
422
|
+
"routeRules",
|
|
423
|
+
"handlers",
|
|
424
|
+
"devHandlers"
|
|
425
|
+
]) if (key in inputs.nitro) return `nitro.${key} is set`;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
function routeFullPaths(routeNodes, inferFullPath) {
|
|
429
|
+
const paths = /* @__PURE__ */ new Set();
|
|
430
|
+
for (const node of routeNodes) {
|
|
431
|
+
if (node.routePath === void 0 || node.isNonPath) continue;
|
|
432
|
+
paths.add(inferFullPath(node));
|
|
433
|
+
}
|
|
434
|
+
return [...paths].sort();
|
|
435
|
+
}
|
|
436
|
+
const COMMENTS = String.raw`(?:(?://[^\n]*\n|/\*[\s\S]*?\*/)\s*)*`;
|
|
437
|
+
const BASEPATH_PROPERTY = new RegExp(String.raw`(?<=[{,]\s*${COMMENTS})\bbasepath\b\s*(?::\s*(?:(['"\x60])([^'"\x60]*)\1)?|(?=${COMMENTS}[,}]))`, "g");
|
|
438
|
+
/**
|
|
439
|
+
* Whether an object-literal `basepath` property in the router file names anything but
|
|
440
|
+
* the declared basepath; a computed value or a shorthand cannot be checked and disagrees.
|
|
441
|
+
*/
|
|
442
|
+
function routerBasepathDisagrees(routerSource, declared) {
|
|
443
|
+
for (const property of routerSource.matchAll(BASEPATH_PROPERTY)) {
|
|
444
|
+
const literal = property[2];
|
|
445
|
+
if (literal === void 0 || normalizeBasePath(literal) !== declared) return true;
|
|
446
|
+
}
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
/** The artifact bytes, or undefined when the manifest would exceed the worker's caps. */
|
|
450
|
+
function serializeRouteManifest(manifest) {
|
|
451
|
+
if (manifest.routes.length > 1e3) {
|
|
452
|
+
logWarning(`Not publishing the route manifest: ${manifest.routes.length} routes exceed the ${ROUTE_MANIFEST_MAX_ROUTES}-route limit.`);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const payload = JSON.stringify(manifest);
|
|
456
|
+
const bytes = Buffer.byteLength(payload, "utf8");
|
|
457
|
+
if (bytes > 524288) {
|
|
458
|
+
logWarning(`Not publishing the route manifest: ${bytes} bytes exceed the ${ROUTE_MANIFEST_MAX_BYTES}-byte limit.`);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
return payload;
|
|
462
|
+
}
|
|
463
|
+
function createOverlayFs(fsp) {
|
|
464
|
+
const overlay = /* @__PURE__ */ new Map();
|
|
465
|
+
const memStat = () => ({
|
|
466
|
+
mtimeMs: BigInt(Date.now()) * 1000000n,
|
|
467
|
+
mode: 420,
|
|
468
|
+
uid: 0,
|
|
469
|
+
gid: 0
|
|
470
|
+
});
|
|
471
|
+
return {
|
|
472
|
+
async stat(filePath) {
|
|
473
|
+
if (overlay.has(filePath)) return memStat();
|
|
474
|
+
const stat = await fsp.stat(filePath, { bigint: true });
|
|
475
|
+
return {
|
|
476
|
+
mtimeMs: stat.mtimeMs,
|
|
477
|
+
mode: Number(stat.mode),
|
|
478
|
+
uid: Number(stat.uid),
|
|
479
|
+
gid: Number(stat.gid)
|
|
480
|
+
};
|
|
481
|
+
},
|
|
482
|
+
async readFile(filePath) {
|
|
483
|
+
const overlaid = overlay.get(filePath);
|
|
484
|
+
if (overlaid !== void 0) return {
|
|
485
|
+
stat: memStat(),
|
|
486
|
+
fileContent: overlaid
|
|
487
|
+
};
|
|
488
|
+
try {
|
|
489
|
+
const [stat, fileContent] = await Promise.all([fsp.stat(filePath, { bigint: true }), fsp.readFile(filePath, "utf8")]);
|
|
490
|
+
return {
|
|
491
|
+
stat: { mtimeMs: stat.mtimeMs },
|
|
492
|
+
fileContent
|
|
493
|
+
};
|
|
494
|
+
} catch (error) {
|
|
495
|
+
if (error.code === "ENOENT") return "file-not-existing";
|
|
496
|
+
throw error;
|
|
497
|
+
}
|
|
498
|
+
},
|
|
499
|
+
async writeFile(filePath, content) {
|
|
500
|
+
overlay.set(filePath, content);
|
|
501
|
+
},
|
|
502
|
+
async rename(oldPath, newPath) {
|
|
503
|
+
const content = overlay.get(oldPath);
|
|
504
|
+
if (content === void 0) return;
|
|
505
|
+
overlay.set(newPath, content);
|
|
506
|
+
overlay.delete(oldPath);
|
|
507
|
+
},
|
|
508
|
+
async chmod() {},
|
|
509
|
+
async chown() {}
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
async function loadRouterGenerator(root) {
|
|
513
|
+
const { createRequire } = await import("node:module");
|
|
514
|
+
const { pathToFileURL } = await import("node:url");
|
|
515
|
+
const nodePath = await import("node:path");
|
|
516
|
+
const { readFile } = await import("node:fs/promises");
|
|
517
|
+
let lastError;
|
|
518
|
+
for (const base of [nodePath.join(root, "package.json"), require("url").pathToFileURL(__filename).href]) try {
|
|
519
|
+
const packageJsonPath = createRequire(base).resolve(`${ROUTER_GENERATOR_PACKAGE}/package.json`);
|
|
520
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
|
|
521
|
+
const entry = packageJson.exports?.["."]?.import?.default ?? packageJson.module ?? packageJson.main;
|
|
522
|
+
if (entry === void 0) throw new Error(`${ROUTER_GENERATOR_PACKAGE} declares no entry point`);
|
|
523
|
+
return await import(pathToFileURL(nodePath.join(nodePath.dirname(packageJsonPath), entry)).href);
|
|
524
|
+
} catch (error) {
|
|
525
|
+
lastError = error;
|
|
526
|
+
}
|
|
527
|
+
throw lastError;
|
|
528
|
+
}
|
|
529
|
+
/** The first of `names` (relative to `dir`) that exists, as given. */
|
|
530
|
+
async function findExisting(dir, names) {
|
|
531
|
+
const { access } = await import("node:fs/promises");
|
|
532
|
+
const nodePath = await import("node:path");
|
|
533
|
+
for (const name of names) if (await access(nodePath.join(dir, name)).then(() => true, () => false)) return name;
|
|
534
|
+
}
|
|
535
|
+
async function resolveEntryFile(srcDirectory, entry) {
|
|
536
|
+
const { stat } = await import("node:fs/promises");
|
|
537
|
+
const base = (await import("node:path")).resolve(srcDirectory, entry.startsWith("./") ? entry : `./${entry}`);
|
|
538
|
+
for (const extension of ENTRY_EXTENSIONS) {
|
|
539
|
+
const file = `${base}${extension}`;
|
|
540
|
+
if (await stat(file).then((stats) => stats.isFile(), () => false)) return file;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
async function readSource(file) {
|
|
544
|
+
if (file === void 0) return void 0;
|
|
545
|
+
const { readFile } = await import("node:fs/promises");
|
|
546
|
+
return readFile(file, "utf8").catch(() => void 0);
|
|
547
|
+
}
|
|
548
|
+
async function enumerateRoutes(options) {
|
|
549
|
+
const fsp = await import("node:fs/promises");
|
|
550
|
+
const nodePath = await import("node:path");
|
|
551
|
+
const os = await import("node:os");
|
|
552
|
+
const generator = await loadRouterGenerator(options.root);
|
|
553
|
+
const { root, srcDirectory } = options;
|
|
554
|
+
const routesDirectory = nodePath.resolve(root, srcDirectory, stringOption(options.router, "routesDirectory", "routes"));
|
|
555
|
+
const generatedRouteTree = nodePath.resolve(root, srcDirectory, stringOption(options.router, "generatedRouteTree", "routeTree.gen.ts"));
|
|
556
|
+
const tmpDir = await fsp.mkdtemp(nodePath.join(os.tmpdir(), "lovable-route-manifest-"));
|
|
557
|
+
try {
|
|
558
|
+
const config = generator.getConfig({
|
|
559
|
+
...options.router,
|
|
560
|
+
routesDirectory,
|
|
561
|
+
generatedRouteTree,
|
|
562
|
+
tmpDir,
|
|
563
|
+
disableLogging: true,
|
|
564
|
+
enableRouteTreeFormatting: false
|
|
565
|
+
}, root);
|
|
566
|
+
const crawler = new generator.Generator({
|
|
567
|
+
config,
|
|
568
|
+
root,
|
|
569
|
+
fs: createOverlayFs(fsp)
|
|
570
|
+
});
|
|
571
|
+
await crawler.run();
|
|
572
|
+
const crawl = await crawler.getCrawlingResult();
|
|
573
|
+
if (crawl === void 0) throw new Error("the route generator produced no crawling result");
|
|
574
|
+
return routeFullPaths(crawl.acc.routeNodes, generator.inferFullPath);
|
|
575
|
+
} finally {
|
|
576
|
+
await fsp.rm(tmpDir, {
|
|
577
|
+
recursive: true,
|
|
578
|
+
force: true
|
|
579
|
+
}).catch(() => {});
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
function createRouteManifestPlugin(inputs) {
|
|
583
|
+
let root = process.cwd();
|
|
584
|
+
return {
|
|
585
|
+
name: ROUTE_MANIFEST_PLUGIN_NAME,
|
|
586
|
+
apply: "build",
|
|
587
|
+
enforce: "post",
|
|
588
|
+
configResolved(config) {
|
|
589
|
+
root = config.root;
|
|
590
|
+
},
|
|
591
|
+
buildApp: {
|
|
592
|
+
order: "post",
|
|
593
|
+
async handler() {
|
|
594
|
+
const { mkdir, rm, writeFile } = await import("node:fs/promises");
|
|
595
|
+
const nodePath = await import("node:path");
|
|
596
|
+
const artifact = nodePath.join(root, PUBLISH_OUTPUT_DIR, ROUTE_MANIFEST_DIR, ROUTE_MANIFEST_FILE);
|
|
597
|
+
await rm(artifact, { force: true }).catch((error) => {
|
|
598
|
+
logWarning(`Could not remove a previous build's route manifest: ${String(error)}`);
|
|
599
|
+
});
|
|
600
|
+
const optOut = routeManifestOptOut(inputs);
|
|
601
|
+
if (optOut !== void 0) {
|
|
602
|
+
logWarning(`Not publishing a route manifest: ${optOut}.`);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const nitroConfig = await findExisting(root, NITRO_CONFIG_FILES);
|
|
606
|
+
if (nitroConfig !== void 0) {
|
|
607
|
+
logWarning(`Not publishing a route manifest: nitro loads ${nitroConfig} on its own, and it may add routes.`);
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
const { tanstackStart } = inputs;
|
|
611
|
+
const srcDirectory = stringOption(tanstackStart, "srcDirectory", "src");
|
|
612
|
+
const router = subOptions(tanstackStart, "router");
|
|
613
|
+
const sourceDir = nodePath.resolve(root, srcDirectory);
|
|
614
|
+
const routerEntry = stringOption(router, "entry", "router");
|
|
615
|
+
const routerSource = await readSource(await resolveEntryFile(sourceDir, routerEntry));
|
|
616
|
+
if (routerSource === void 0) {
|
|
617
|
+
logWarning(`Not publishing a route manifest: could not read the router file (${routerEntry} under ${srcDirectory}) to check its settings.`);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
if (/\brewrite\s*:/.test(routerSource)) {
|
|
621
|
+
logWarning("Not publishing a route manifest: the router file sets `rewrite`.");
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const basepath = normalizeBasePath(router.basepath);
|
|
625
|
+
if (routerBasepathDisagrees(routerSource, basepath)) {
|
|
626
|
+
logWarning("Not publishing a route manifest: the router file sets a `basepath` that tanstackStart.router.basepath does not declare.");
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
let routes;
|
|
630
|
+
try {
|
|
631
|
+
routes = await enumerateRoutes({
|
|
632
|
+
root,
|
|
633
|
+
srcDirectory,
|
|
634
|
+
router
|
|
635
|
+
});
|
|
636
|
+
} catch (error) {
|
|
637
|
+
logWarning(`Not publishing a route manifest; could not enumerate the route tree: ${String(error)}`);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
if (routes.length === 0) {
|
|
641
|
+
logWarning("Not publishing a route manifest: the route tree has no routes.");
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
const startSource = await readSource(await resolveEntryFile(sourceDir, stringOption(subOptions(tanstackStart, "start"), "entry", "start")));
|
|
645
|
+
const payload = serializeRouteManifest({
|
|
646
|
+
version: ROUTE_MANIFEST_VERSION,
|
|
647
|
+
generator: GENERATOR,
|
|
648
|
+
...basepath === "" ? {} : { basepath },
|
|
649
|
+
serverEntry: typeof subOptions(tanstackStart, "server").entry === "string" ? "custom" : "default",
|
|
650
|
+
...startSource === void 0 ? {} : { requestMiddleware: /\brequestMiddleware\b/.test(startSource) },
|
|
651
|
+
routes
|
|
652
|
+
});
|
|
653
|
+
if (payload === void 0) return;
|
|
654
|
+
try {
|
|
655
|
+
await mkdir(nodePath.dirname(artifact), { recursive: true });
|
|
656
|
+
await writeFile(artifact, payload);
|
|
657
|
+
} catch (error) {
|
|
658
|
+
logWarning(`Could not write the route manifest; unknown paths fall back to SSR: ${String(error)}`);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
//#endregion
|
|
370
665
|
//#region src/index.ts
|
|
371
666
|
const SANDBOX_ENV_VAR = "DEV_SERVER__PROJECT_PATH";
|
|
372
667
|
const LOVABLE_SANDBOX_ENV_VAR = "LOVABLE_SANDBOX";
|
|
@@ -950,6 +1245,86 @@ function prerenderPreviewShimCleanup(state) {
|
|
|
950
1245
|
}
|
|
951
1246
|
};
|
|
952
1247
|
}
|
|
1248
|
+
const BUILD_EXIT_REPORT_MARKER_OPEN = "[lovable-build-exit-report]";
|
|
1249
|
+
const BUILD_EXIT_REPORT_MARKER_CLOSE = "[/lovable-build-exit-report]";
|
|
1250
|
+
const EXIT_WATCHDOG_ENV_VAR = "LOVABLE_PRERENDER_EXIT_WATCHDOG";
|
|
1251
|
+
const EXIT_GRACE_ENV_VAR = "LOVABLE_PRERENDER_EXIT_GRACE_MS";
|
|
1252
|
+
const DEFAULT_EXIT_GRACE_MS = 6e4;
|
|
1253
|
+
const MIN_EXIT_GRACE_MS = 5e3;
|
|
1254
|
+
const MAX_EXIT_GRACE_MS = 6e5;
|
|
1255
|
+
const MAX_REPORTED_HANDLE_TYPES = 20;
|
|
1256
|
+
const MAX_REPORTED_HANDLE_TYPE_BYTES = 64;
|
|
1257
|
+
const IGNORED_HANDLE_TYPES = /* @__PURE__ */ new Set(["PipeWrap"]);
|
|
1258
|
+
function clampGraceMs(value) {
|
|
1259
|
+
return Math.min(MAX_EXIT_GRACE_MS, Math.max(MIN_EXIT_GRACE_MS, Math.floor(value)));
|
|
1260
|
+
}
|
|
1261
|
+
function resolveExitWatchdog(config) {
|
|
1262
|
+
const enabled = process.env[EXIT_WATCHDOG_ENV_VAR] === "off" ? false : config?.enabled !== false;
|
|
1263
|
+
let graceMs = DEFAULT_EXIT_GRACE_MS;
|
|
1264
|
+
if (config?.graceMs !== void 0) if (typeof config.graceMs === "number" && Number.isFinite(config.graceMs) && config.graceMs > 0) graceMs = clampGraceMs(config.graceMs);
|
|
1265
|
+
else logWarning(`Ignoring buildExitWatchdog.graceMs=${String(config.graceMs)}: expected a positive number of ms.`);
|
|
1266
|
+
const envGrace = process.env[EXIT_GRACE_ENV_VAR];
|
|
1267
|
+
if (envGrace) if (/^\d+$/.test(envGrace) && Number(envGrace) > 0) graceMs = clampGraceMs(Number(envGrace));
|
|
1268
|
+
else logWarning(`Ignoring ${EXIT_GRACE_ENV_VAR}=${envGrace}: expected a positive integer of ms.`);
|
|
1269
|
+
return {
|
|
1270
|
+
enabled,
|
|
1271
|
+
graceMs
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
function cutUtf8(value, maxBytes) {
|
|
1275
|
+
let out = "";
|
|
1276
|
+
let bytes = 0;
|
|
1277
|
+
for (const char of value) {
|
|
1278
|
+
bytes += Buffer.byteLength(char);
|
|
1279
|
+
if (bytes > maxBytes) break;
|
|
1280
|
+
out += char;
|
|
1281
|
+
}
|
|
1282
|
+
return out;
|
|
1283
|
+
}
|
|
1284
|
+
function collectOpenHandles() {
|
|
1285
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1286
|
+
let resources = [];
|
|
1287
|
+
try {
|
|
1288
|
+
resources = process.getActiveResourcesInfo();
|
|
1289
|
+
} catch {}
|
|
1290
|
+
for (const resource of resources) {
|
|
1291
|
+
if (IGNORED_HANDLE_TYPES.has(resource)) continue;
|
|
1292
|
+
const type = cutUtf8(resource, MAX_REPORTED_HANDLE_TYPE_BYTES);
|
|
1293
|
+
counts.set(type, (counts.get(type) ?? 0) + 1);
|
|
1294
|
+
}
|
|
1295
|
+
const ranked = [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, MAX_REPORTED_HANDLE_TYPES);
|
|
1296
|
+
return Object.fromEntries(ranked);
|
|
1297
|
+
}
|
|
1298
|
+
function emitBuildExitReport(graceMs, exitCode, openHandles) {
|
|
1299
|
+
const payload = JSON.stringify({
|
|
1300
|
+
grace_ms: graceMs,
|
|
1301
|
+
exit_code: exitCode,
|
|
1302
|
+
open_handles: openHandles
|
|
1303
|
+
});
|
|
1304
|
+
try {
|
|
1305
|
+
process.stderr.write(`\n${BUILD_EXIT_REPORT_MARKER_OPEN}\n${payload}\n${BUILD_EXIT_REPORT_MARKER_CLOSE}\n`);
|
|
1306
|
+
} catch {}
|
|
1307
|
+
}
|
|
1308
|
+
function prerenderExitWatchdog(graceMs) {
|
|
1309
|
+
return {
|
|
1310
|
+
name: "lovable-prerender-exit-watchdog",
|
|
1311
|
+
apply: "build",
|
|
1312
|
+
enforce: "post",
|
|
1313
|
+
buildApp: {
|
|
1314
|
+
order: "post",
|
|
1315
|
+
handler: async () => {
|
|
1316
|
+
setTimeout(() => {
|
|
1317
|
+
const exitCode = Number(process.exitCode ?? 0) || 0;
|
|
1318
|
+
const openHandles = collectOpenHandles();
|
|
1319
|
+
emitBuildExitReport(graceMs, exitCode, openHandles);
|
|
1320
|
+
const types = Object.entries(openHandles).map(([type, count]) => `${type} x${count}`);
|
|
1321
|
+
logWarning(`Build output is complete but the process was still alive ${graceMs}ms after prerender (open handles: ${types.length > 0 ? types.join(", ") : "none reported"}); exiting with code ${exitCode}. Usual causes: TanStack Query gcTime timers armed during SSR, or module-scope timers and connection pools in server code.`);
|
|
1322
|
+
process.exit(exitCode);
|
|
1323
|
+
}, graceMs).unref();
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
953
1328
|
function prerenderIsEnabled(startOptions) {
|
|
954
1329
|
const spa = startOptions.spa;
|
|
955
1330
|
if (spa && (spa.enabled ?? true)) return true;
|
|
@@ -1108,9 +1483,10 @@ function defineConfig(configOrOptions = {}) {
|
|
|
1108
1483
|
optionObject.nitro = false;
|
|
1109
1484
|
logWarning("`cloudflare: false` is deprecated in v2 — migrated to `nitro: false`. Update your config to drop the warning.");
|
|
1110
1485
|
} else logWarning("The `cloudflare` option was removed in v2. Use `nitro: false` to disable the deploy plugin, or `nitro: { preset: '...' }` to configure it. The `cloudflare` setting is being ignored.");
|
|
1111
|
-
options = "vite" in optionObject || "serverFnErrorLogger" in optionObject || "ssrErrorLogger" in optionObject || "nitro" in optionObject || "tanstackStart" in optionObject || "react" in optionObject || "envDefine" in optionObject || "hmrGate" in optionObject ? optionObject : { vite: optionObject };
|
|
1486
|
+
options = "vite" in optionObject || "serverFnErrorLogger" in optionObject || "ssrErrorLogger" in optionObject || "nitro" in optionObject || "tanstackStart" in optionObject || "react" in optionObject || "envDefine" in optionObject || "hmrGate" in optionObject || "buildExitWatchdog" in optionObject ? optionObject : { vite: optionObject };
|
|
1112
1487
|
}
|
|
1113
1488
|
const internalPlugins = [];
|
|
1489
|
+
let exitWatchdog;
|
|
1114
1490
|
if (mode === "development") {
|
|
1115
1491
|
const { devtools } = await import("@tanstack/devtools-vite");
|
|
1116
1492
|
internalPlugins.push(devtools({
|
|
@@ -1141,6 +1517,10 @@ function defineConfig(configOrOptions = {}) {
|
|
|
1141
1517
|
if (command === "build" && isSandbox) {
|
|
1142
1518
|
const { startOptions, skipReporter, documentSet } = pinPrerenderForLovableBuild(tanstackStartOptions);
|
|
1143
1519
|
internalPlugins.push(tanstackStart(startOptions), skipReporter, documentSet);
|
|
1520
|
+
internalPlugins.push(createRouteManifestPlugin({
|
|
1521
|
+
tanstackStart: tanstackStartOptions,
|
|
1522
|
+
nitro: options.nitro
|
|
1523
|
+
}));
|
|
1144
1524
|
} else internalPlugins.push(tanstackStart(tanstackStartOptions));
|
|
1145
1525
|
const explicitNitro = options.nitro === true || typeof options.nitro === "object" && options.nitro !== null;
|
|
1146
1526
|
if (options.nitro !== false && command === "build") {
|
|
@@ -1195,6 +1575,10 @@ function defineConfig(configOrOptions = {}) {
|
|
|
1195
1575
|
const shimState = {};
|
|
1196
1576
|
internalPlugins.push(prerenderPreviewShim(shimState, nitroDirs), prerenderPreviewShimCleanup(shimState));
|
|
1197
1577
|
}
|
|
1578
|
+
if (isSandbox && prerenderEnabled) {
|
|
1579
|
+
const watchdog = resolveExitWatchdog(options.buildExitWatchdog);
|
|
1580
|
+
if (watchdog.enabled) exitWatchdog = prerenderExitWatchdog(watchdog.graceMs);
|
|
1581
|
+
}
|
|
1198
1582
|
if (isSandbox) internalPlugins.push(stripRedundantNodejsCompatFlag(nitroDirs));
|
|
1199
1583
|
}
|
|
1200
1584
|
}
|
|
@@ -1274,6 +1658,7 @@ function defineConfig(configOrOptions = {}) {
|
|
|
1274
1658
|
host: PRERENDER_PREVIEW_HOST,
|
|
1275
1659
|
port: 0
|
|
1276
1660
|
} }, config);
|
|
1661
|
+
if (exitWatchdog) config = (0, vite.mergeConfig)(config, { plugins: [exitWatchdog] });
|
|
1277
1662
|
if (isSandbox) return config;
|
|
1278
1663
|
return applyWatchDebounceDefaults(config);
|
|
1279
1664
|
};
|
package/dist/index.d.cts
CHANGED
|
@@ -30,6 +30,12 @@ type PrerenderShimDirs = {
|
|
|
30
30
|
/** Filled by nitro's `compiled` hook, which fires inside its own post-order `buildApp`. */
|
|
31
31
|
nitroOutput: NitroOutput;
|
|
32
32
|
};
|
|
33
|
+
interface BuildExitWatchdogOptions {
|
|
34
|
+
/** Grace after the post-build hooks before the process is forced to exit. Clamped to 5s-10min. Default 60_000. */
|
|
35
|
+
graceMs?: number;
|
|
36
|
+
/** Set to false to let a build that keeps handles open run until the publish limit. Default true. */
|
|
37
|
+
enabled?: boolean;
|
|
38
|
+
}
|
|
33
39
|
interface LovableViteTanstackOptions {
|
|
34
40
|
plugins?: PluginOption[];
|
|
35
41
|
vite?: UserConfig;
|
|
@@ -77,6 +83,8 @@ interface LovableViteTanstackOptions {
|
|
|
77
83
|
envDefine?: boolean;
|
|
78
84
|
/** hmrGatePlugin() options. Default on in sandbox (false disables); true or an options object enables elsewhere. */
|
|
79
85
|
hmrGate?: boolean | Record<string, unknown>;
|
|
86
|
+
/** Forced exit after a Lovable prerender build when the server bundle keeps handles open. Sandbox builds only. */
|
|
87
|
+
buildExitWatchdog?: BuildExitWatchdogOptions;
|
|
80
88
|
}
|
|
81
89
|
declare function defineConfig(config: UserConfig): (env: ConfigEnv) => Promise<UserConfig>;
|
|
82
90
|
declare function defineConfig(config: Promise<UserConfig>): (env: ConfigEnv) => Promise<UserConfig>;
|
|
@@ -85,4 +93,4 @@ declare function defineConfig(config: UserConfigFnPromise): (env: ConfigEnv) =>
|
|
|
85
93
|
declare function defineConfig(config: UserConfigFn): (env: ConfigEnv) => Promise<UserConfig>;
|
|
86
94
|
declare function defineConfig(options?: LovableViteTanstackOptions): (env: ConfigEnv) => Promise<UserConfig>;
|
|
87
95
|
//#endregion
|
|
88
|
-
export { LovableViteTanstackOptions, type PrerenderPage, defineConfig, lovableAssetsProxyPlugin, stripRedundantNodejsCompatFlag };
|
|
96
|
+
export { BuildExitWatchdogOptions, LovableViteTanstackOptions, type PrerenderPage, defineConfig, lovableAssetsProxyPlugin, stripRedundantNodejsCompatFlag };
|
package/dist/index.d.ts
CHANGED
|
@@ -30,6 +30,12 @@ type PrerenderShimDirs = {
|
|
|
30
30
|
/** Filled by nitro's `compiled` hook, which fires inside its own post-order `buildApp`. */
|
|
31
31
|
nitroOutput: NitroOutput;
|
|
32
32
|
};
|
|
33
|
+
interface BuildExitWatchdogOptions {
|
|
34
|
+
/** Grace after the post-build hooks before the process is forced to exit. Clamped to 5s-10min. Default 60_000. */
|
|
35
|
+
graceMs?: number;
|
|
36
|
+
/** Set to false to let a build that keeps handles open run until the publish limit. Default true. */
|
|
37
|
+
enabled?: boolean;
|
|
38
|
+
}
|
|
33
39
|
interface LovableViteTanstackOptions {
|
|
34
40
|
plugins?: PluginOption[];
|
|
35
41
|
vite?: UserConfig;
|
|
@@ -77,6 +83,8 @@ interface LovableViteTanstackOptions {
|
|
|
77
83
|
envDefine?: boolean;
|
|
78
84
|
/** hmrGatePlugin() options. Default on in sandbox (false disables); true or an options object enables elsewhere. */
|
|
79
85
|
hmrGate?: boolean | Record<string, unknown>;
|
|
86
|
+
/** Forced exit after a Lovable prerender build when the server bundle keeps handles open. Sandbox builds only. */
|
|
87
|
+
buildExitWatchdog?: BuildExitWatchdogOptions;
|
|
80
88
|
}
|
|
81
89
|
declare function defineConfig(config: UserConfig): (env: ConfigEnv) => Promise<UserConfig>;
|
|
82
90
|
declare function defineConfig(config: Promise<UserConfig>): (env: ConfigEnv) => Promise<UserConfig>;
|
|
@@ -85,4 +93,4 @@ declare function defineConfig(config: UserConfigFnPromise): (env: ConfigEnv) =>
|
|
|
85
93
|
declare function defineConfig(config: UserConfigFn): (env: ConfigEnv) => Promise<UserConfig>;
|
|
86
94
|
declare function defineConfig(options?: LovableViteTanstackOptions): (env: ConfigEnv) => Promise<UserConfig>;
|
|
87
95
|
//#endregion
|
|
88
|
-
export { LovableViteTanstackOptions, type PrerenderPage, defineConfig, lovableAssetsProxyPlugin, stripRedundantNodejsCompatFlag };
|
|
96
|
+
export { BuildExitWatchdogOptions, LovableViteTanstackOptions, type PrerenderPage, defineConfig, lovableAssetsProxyPlugin, stripRedundantNodejsCompatFlag };
|
package/dist/index.js
CHANGED
|
@@ -129,7 +129,7 @@ function bundledDevCssUrlShim() {
|
|
|
129
129
|
//#endregion
|
|
130
130
|
//#region package.json
|
|
131
131
|
var name = "@lovable.dev/vite-tanstack-config";
|
|
132
|
-
var version = "2.
|
|
132
|
+
var version = "2.23.0";
|
|
133
133
|
//#endregion
|
|
134
134
|
//#region src/logging.ts
|
|
135
135
|
const PACKAGE_NAME = "@lovable.dev/vite-tanstack-config";
|
|
@@ -144,7 +144,7 @@ const CLIENT_OUTPUT_DIR = `${PUBLISH_OUTPUT_DIR}/client`;
|
|
|
144
144
|
//#endregion
|
|
145
145
|
//#region src/documents.ts
|
|
146
146
|
const DOCUMENT_SET_VERSION = 1;
|
|
147
|
-
const GENERATOR = `${name}@${version}`;
|
|
147
|
+
const GENERATOR$1 = `${name}@${version}`;
|
|
148
148
|
const DOCUMENT_SET_DIR = ".lovable";
|
|
149
149
|
const DOCUMENT_SET_FILE = "output.json";
|
|
150
150
|
const DOCUMENT_SET_PLUGIN_NAME = "lovable:document-set";
|
|
@@ -317,7 +317,7 @@ function createDocumentSet(routerBasePath) {
|
|
|
317
317
|
}
|
|
318
318
|
logWarning(`Not publishing prerendered route ${entry.route}: it produced no ${entry.file}.`);
|
|
319
319
|
}
|
|
320
|
-
const payload = serializeDocumentSet(present, GENERATOR);
|
|
320
|
+
const payload = serializeDocumentSet(present, GENERATOR$1);
|
|
321
321
|
if (payload === void 0) return;
|
|
322
322
|
try {
|
|
323
323
|
await mkdir(nodePath.dirname(artifact), { recursive: true });
|
|
@@ -343,6 +343,301 @@ function createDocumentSet(routerBasePath) {
|
|
|
343
343
|
};
|
|
344
344
|
}
|
|
345
345
|
//#endregion
|
|
346
|
+
//#region src/routes.ts
|
|
347
|
+
const ROUTE_MANIFEST_VERSION = 1;
|
|
348
|
+
const GENERATOR = `${name}@${version}`;
|
|
349
|
+
const ROUTE_MANIFEST_DIR = ".lovable";
|
|
350
|
+
const ROUTE_MANIFEST_FILE = "routes.json";
|
|
351
|
+
const ROUTE_MANIFEST_MAX_ROUTES = 1e3;
|
|
352
|
+
const ROUTE_MANIFEST_MAX_BYTES = 512 * 1024;
|
|
353
|
+
const ROUTE_MANIFEST_PLUGIN_NAME = "lovable:route-manifest";
|
|
354
|
+
const ROUTER_GENERATOR_PACKAGE = "@tanstack/router-generator";
|
|
355
|
+
const ENTRY_EXTENSIONS = [
|
|
356
|
+
"",
|
|
357
|
+
".ts",
|
|
358
|
+
".js",
|
|
359
|
+
".mts",
|
|
360
|
+
".mjs",
|
|
361
|
+
".tsx",
|
|
362
|
+
".jsx"
|
|
363
|
+
];
|
|
364
|
+
const NITRO_CONFIG_EXTENSIONS = [
|
|
365
|
+
".js",
|
|
366
|
+
".ts",
|
|
367
|
+
".mjs",
|
|
368
|
+
".cjs",
|
|
369
|
+
".mts",
|
|
370
|
+
".cts",
|
|
371
|
+
".json",
|
|
372
|
+
".jsonc",
|
|
373
|
+
".json5",
|
|
374
|
+
".yaml",
|
|
375
|
+
".yml",
|
|
376
|
+
".toml"
|
|
377
|
+
];
|
|
378
|
+
const NITRO_CONFIG_FILES = [
|
|
379
|
+
...NITRO_CONFIG_EXTENSIONS.map((extension) => `nitro.config${extension}`),
|
|
380
|
+
...NITRO_CONFIG_EXTENSIONS.flatMap((extension) => [`.config/nitro${extension}`, `.config/nitro.config${extension}`]),
|
|
381
|
+
".nitrorc"
|
|
382
|
+
];
|
|
383
|
+
function subOptions(options, key) {
|
|
384
|
+
const value = options[key];
|
|
385
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
386
|
+
}
|
|
387
|
+
function stringOption(options, key, fallback) {
|
|
388
|
+
const value = options[key];
|
|
389
|
+
return typeof value === "string" ? value : fallback;
|
|
390
|
+
}
|
|
391
|
+
/** Why no manifest is emitted: the app can answer paths outside its route tree. */
|
|
392
|
+
function routeManifestOptOut(inputs) {
|
|
393
|
+
const { tanstackStart } = inputs;
|
|
394
|
+
if (tanstackStart.spa != null && subOptions(tanstackStart, "spa").enabled !== false) return "tanstackStart.spa serves the shell for every path";
|
|
395
|
+
if (subOptions(tanstackStart, "router").virtualRouteConfig !== void 0) return "tanstackStart.router.virtualRouteConfig is set";
|
|
396
|
+
if (typeof inputs.nitro === "object" && inputs.nitro !== null) {
|
|
397
|
+
for (const key of [
|
|
398
|
+
"routeRules",
|
|
399
|
+
"handlers",
|
|
400
|
+
"devHandlers"
|
|
401
|
+
]) if (key in inputs.nitro) return `nitro.${key} is set`;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
function routeFullPaths(routeNodes, inferFullPath) {
|
|
405
|
+
const paths = /* @__PURE__ */ new Set();
|
|
406
|
+
for (const node of routeNodes) {
|
|
407
|
+
if (node.routePath === void 0 || node.isNonPath) continue;
|
|
408
|
+
paths.add(inferFullPath(node));
|
|
409
|
+
}
|
|
410
|
+
return [...paths].sort();
|
|
411
|
+
}
|
|
412
|
+
const COMMENTS = String.raw`(?:(?://[^\n]*\n|/\*[\s\S]*?\*/)\s*)*`;
|
|
413
|
+
const BASEPATH_PROPERTY = new RegExp(String.raw`(?<=[{,]\s*${COMMENTS})\bbasepath\b\s*(?::\s*(?:(['"\x60])([^'"\x60]*)\1)?|(?=${COMMENTS}[,}]))`, "g");
|
|
414
|
+
/**
|
|
415
|
+
* Whether an object-literal `basepath` property in the router file names anything but
|
|
416
|
+
* the declared basepath; a computed value or a shorthand cannot be checked and disagrees.
|
|
417
|
+
*/
|
|
418
|
+
function routerBasepathDisagrees(routerSource, declared) {
|
|
419
|
+
for (const property of routerSource.matchAll(BASEPATH_PROPERTY)) {
|
|
420
|
+
const literal = property[2];
|
|
421
|
+
if (literal === void 0 || normalizeBasePath(literal) !== declared) return true;
|
|
422
|
+
}
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
/** The artifact bytes, or undefined when the manifest would exceed the worker's caps. */
|
|
426
|
+
function serializeRouteManifest(manifest) {
|
|
427
|
+
if (manifest.routes.length > 1e3) {
|
|
428
|
+
logWarning(`Not publishing the route manifest: ${manifest.routes.length} routes exceed the ${ROUTE_MANIFEST_MAX_ROUTES}-route limit.`);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const payload = JSON.stringify(manifest);
|
|
432
|
+
const bytes = Buffer.byteLength(payload, "utf8");
|
|
433
|
+
if (bytes > 524288) {
|
|
434
|
+
logWarning(`Not publishing the route manifest: ${bytes} bytes exceed the ${ROUTE_MANIFEST_MAX_BYTES}-byte limit.`);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
return payload;
|
|
438
|
+
}
|
|
439
|
+
function createOverlayFs(fsp) {
|
|
440
|
+
const overlay = /* @__PURE__ */ new Map();
|
|
441
|
+
const memStat = () => ({
|
|
442
|
+
mtimeMs: BigInt(Date.now()) * 1000000n,
|
|
443
|
+
mode: 420,
|
|
444
|
+
uid: 0,
|
|
445
|
+
gid: 0
|
|
446
|
+
});
|
|
447
|
+
return {
|
|
448
|
+
async stat(filePath) {
|
|
449
|
+
if (overlay.has(filePath)) return memStat();
|
|
450
|
+
const stat = await fsp.stat(filePath, { bigint: true });
|
|
451
|
+
return {
|
|
452
|
+
mtimeMs: stat.mtimeMs,
|
|
453
|
+
mode: Number(stat.mode),
|
|
454
|
+
uid: Number(stat.uid),
|
|
455
|
+
gid: Number(stat.gid)
|
|
456
|
+
};
|
|
457
|
+
},
|
|
458
|
+
async readFile(filePath) {
|
|
459
|
+
const overlaid = overlay.get(filePath);
|
|
460
|
+
if (overlaid !== void 0) return {
|
|
461
|
+
stat: memStat(),
|
|
462
|
+
fileContent: overlaid
|
|
463
|
+
};
|
|
464
|
+
try {
|
|
465
|
+
const [stat, fileContent] = await Promise.all([fsp.stat(filePath, { bigint: true }), fsp.readFile(filePath, "utf8")]);
|
|
466
|
+
return {
|
|
467
|
+
stat: { mtimeMs: stat.mtimeMs },
|
|
468
|
+
fileContent
|
|
469
|
+
};
|
|
470
|
+
} catch (error) {
|
|
471
|
+
if (error.code === "ENOENT") return "file-not-existing";
|
|
472
|
+
throw error;
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
async writeFile(filePath, content) {
|
|
476
|
+
overlay.set(filePath, content);
|
|
477
|
+
},
|
|
478
|
+
async rename(oldPath, newPath) {
|
|
479
|
+
const content = overlay.get(oldPath);
|
|
480
|
+
if (content === void 0) return;
|
|
481
|
+
overlay.set(newPath, content);
|
|
482
|
+
overlay.delete(oldPath);
|
|
483
|
+
},
|
|
484
|
+
async chmod() {},
|
|
485
|
+
async chown() {}
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
async function loadRouterGenerator(root) {
|
|
489
|
+
const { createRequire } = await import("node:module");
|
|
490
|
+
const { pathToFileURL } = await import("node:url");
|
|
491
|
+
const nodePath = await import("node:path");
|
|
492
|
+
const { readFile } = await import("node:fs/promises");
|
|
493
|
+
let lastError;
|
|
494
|
+
for (const base of [nodePath.join(root, "package.json"), import.meta.url]) try {
|
|
495
|
+
const packageJsonPath = createRequire(base).resolve(`${ROUTER_GENERATOR_PACKAGE}/package.json`);
|
|
496
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
|
|
497
|
+
const entry = packageJson.exports?.["."]?.import?.default ?? packageJson.module ?? packageJson.main;
|
|
498
|
+
if (entry === void 0) throw new Error(`${ROUTER_GENERATOR_PACKAGE} declares no entry point`);
|
|
499
|
+
return await import(pathToFileURL(nodePath.join(nodePath.dirname(packageJsonPath), entry)).href);
|
|
500
|
+
} catch (error) {
|
|
501
|
+
lastError = error;
|
|
502
|
+
}
|
|
503
|
+
throw lastError;
|
|
504
|
+
}
|
|
505
|
+
/** The first of `names` (relative to `dir`) that exists, as given. */
|
|
506
|
+
async function findExisting(dir, names) {
|
|
507
|
+
const { access } = await import("node:fs/promises");
|
|
508
|
+
const nodePath = await import("node:path");
|
|
509
|
+
for (const name of names) if (await access(nodePath.join(dir, name)).then(() => true, () => false)) return name;
|
|
510
|
+
}
|
|
511
|
+
async function resolveEntryFile(srcDirectory, entry) {
|
|
512
|
+
const { stat } = await import("node:fs/promises");
|
|
513
|
+
const base = (await import("node:path")).resolve(srcDirectory, entry.startsWith("./") ? entry : `./${entry}`);
|
|
514
|
+
for (const extension of ENTRY_EXTENSIONS) {
|
|
515
|
+
const file = `${base}${extension}`;
|
|
516
|
+
if (await stat(file).then((stats) => stats.isFile(), () => false)) return file;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
async function readSource(file) {
|
|
520
|
+
if (file === void 0) return void 0;
|
|
521
|
+
const { readFile } = await import("node:fs/promises");
|
|
522
|
+
return readFile(file, "utf8").catch(() => void 0);
|
|
523
|
+
}
|
|
524
|
+
async function enumerateRoutes(options) {
|
|
525
|
+
const fsp = await import("node:fs/promises");
|
|
526
|
+
const nodePath = await import("node:path");
|
|
527
|
+
const os = await import("node:os");
|
|
528
|
+
const generator = await loadRouterGenerator(options.root);
|
|
529
|
+
const { root, srcDirectory } = options;
|
|
530
|
+
const routesDirectory = nodePath.resolve(root, srcDirectory, stringOption(options.router, "routesDirectory", "routes"));
|
|
531
|
+
const generatedRouteTree = nodePath.resolve(root, srcDirectory, stringOption(options.router, "generatedRouteTree", "routeTree.gen.ts"));
|
|
532
|
+
const tmpDir = await fsp.mkdtemp(nodePath.join(os.tmpdir(), "lovable-route-manifest-"));
|
|
533
|
+
try {
|
|
534
|
+
const config = generator.getConfig({
|
|
535
|
+
...options.router,
|
|
536
|
+
routesDirectory,
|
|
537
|
+
generatedRouteTree,
|
|
538
|
+
tmpDir,
|
|
539
|
+
disableLogging: true,
|
|
540
|
+
enableRouteTreeFormatting: false
|
|
541
|
+
}, root);
|
|
542
|
+
const crawler = new generator.Generator({
|
|
543
|
+
config,
|
|
544
|
+
root,
|
|
545
|
+
fs: createOverlayFs(fsp)
|
|
546
|
+
});
|
|
547
|
+
await crawler.run();
|
|
548
|
+
const crawl = await crawler.getCrawlingResult();
|
|
549
|
+
if (crawl === void 0) throw new Error("the route generator produced no crawling result");
|
|
550
|
+
return routeFullPaths(crawl.acc.routeNodes, generator.inferFullPath);
|
|
551
|
+
} finally {
|
|
552
|
+
await fsp.rm(tmpDir, {
|
|
553
|
+
recursive: true,
|
|
554
|
+
force: true
|
|
555
|
+
}).catch(() => {});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
function createRouteManifestPlugin(inputs) {
|
|
559
|
+
let root = process.cwd();
|
|
560
|
+
return {
|
|
561
|
+
name: ROUTE_MANIFEST_PLUGIN_NAME,
|
|
562
|
+
apply: "build",
|
|
563
|
+
enforce: "post",
|
|
564
|
+
configResolved(config) {
|
|
565
|
+
root = config.root;
|
|
566
|
+
},
|
|
567
|
+
buildApp: {
|
|
568
|
+
order: "post",
|
|
569
|
+
async handler() {
|
|
570
|
+
const { mkdir, rm, writeFile } = await import("node:fs/promises");
|
|
571
|
+
const nodePath = await import("node:path");
|
|
572
|
+
const artifact = nodePath.join(root, PUBLISH_OUTPUT_DIR, ROUTE_MANIFEST_DIR, ROUTE_MANIFEST_FILE);
|
|
573
|
+
await rm(artifact, { force: true }).catch((error) => {
|
|
574
|
+
logWarning(`Could not remove a previous build's route manifest: ${String(error)}`);
|
|
575
|
+
});
|
|
576
|
+
const optOut = routeManifestOptOut(inputs);
|
|
577
|
+
if (optOut !== void 0) {
|
|
578
|
+
logWarning(`Not publishing a route manifest: ${optOut}.`);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
const nitroConfig = await findExisting(root, NITRO_CONFIG_FILES);
|
|
582
|
+
if (nitroConfig !== void 0) {
|
|
583
|
+
logWarning(`Not publishing a route manifest: nitro loads ${nitroConfig} on its own, and it may add routes.`);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
const { tanstackStart } = inputs;
|
|
587
|
+
const srcDirectory = stringOption(tanstackStart, "srcDirectory", "src");
|
|
588
|
+
const router = subOptions(tanstackStart, "router");
|
|
589
|
+
const sourceDir = nodePath.resolve(root, srcDirectory);
|
|
590
|
+
const routerEntry = stringOption(router, "entry", "router");
|
|
591
|
+
const routerSource = await readSource(await resolveEntryFile(sourceDir, routerEntry));
|
|
592
|
+
if (routerSource === void 0) {
|
|
593
|
+
logWarning(`Not publishing a route manifest: could not read the router file (${routerEntry} under ${srcDirectory}) to check its settings.`);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
if (/\brewrite\s*:/.test(routerSource)) {
|
|
597
|
+
logWarning("Not publishing a route manifest: the router file sets `rewrite`.");
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
const basepath = normalizeBasePath(router.basepath);
|
|
601
|
+
if (routerBasepathDisagrees(routerSource, basepath)) {
|
|
602
|
+
logWarning("Not publishing a route manifest: the router file sets a `basepath` that tanstackStart.router.basepath does not declare.");
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
let routes;
|
|
606
|
+
try {
|
|
607
|
+
routes = await enumerateRoutes({
|
|
608
|
+
root,
|
|
609
|
+
srcDirectory,
|
|
610
|
+
router
|
|
611
|
+
});
|
|
612
|
+
} catch (error) {
|
|
613
|
+
logWarning(`Not publishing a route manifest; could not enumerate the route tree: ${String(error)}`);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
if (routes.length === 0) {
|
|
617
|
+
logWarning("Not publishing a route manifest: the route tree has no routes.");
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
const startSource = await readSource(await resolveEntryFile(sourceDir, stringOption(subOptions(tanstackStart, "start"), "entry", "start")));
|
|
621
|
+
const payload = serializeRouteManifest({
|
|
622
|
+
version: ROUTE_MANIFEST_VERSION,
|
|
623
|
+
generator: GENERATOR,
|
|
624
|
+
...basepath === "" ? {} : { basepath },
|
|
625
|
+
serverEntry: typeof subOptions(tanstackStart, "server").entry === "string" ? "custom" : "default",
|
|
626
|
+
...startSource === void 0 ? {} : { requestMiddleware: /\brequestMiddleware\b/.test(startSource) },
|
|
627
|
+
routes
|
|
628
|
+
});
|
|
629
|
+
if (payload === void 0) return;
|
|
630
|
+
try {
|
|
631
|
+
await mkdir(nodePath.dirname(artifact), { recursive: true });
|
|
632
|
+
await writeFile(artifact, payload);
|
|
633
|
+
} catch (error) {
|
|
634
|
+
logWarning(`Could not write the route manifest; unknown paths fall back to SSR: ${String(error)}`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
//#endregion
|
|
346
641
|
//#region src/index.ts
|
|
347
642
|
const SANDBOX_ENV_VAR = "DEV_SERVER__PROJECT_PATH";
|
|
348
643
|
const LOVABLE_SANDBOX_ENV_VAR = "LOVABLE_SANDBOX";
|
|
@@ -926,6 +1221,86 @@ function prerenderPreviewShimCleanup(state) {
|
|
|
926
1221
|
}
|
|
927
1222
|
};
|
|
928
1223
|
}
|
|
1224
|
+
const BUILD_EXIT_REPORT_MARKER_OPEN = "[lovable-build-exit-report]";
|
|
1225
|
+
const BUILD_EXIT_REPORT_MARKER_CLOSE = "[/lovable-build-exit-report]";
|
|
1226
|
+
const EXIT_WATCHDOG_ENV_VAR = "LOVABLE_PRERENDER_EXIT_WATCHDOG";
|
|
1227
|
+
const EXIT_GRACE_ENV_VAR = "LOVABLE_PRERENDER_EXIT_GRACE_MS";
|
|
1228
|
+
const DEFAULT_EXIT_GRACE_MS = 6e4;
|
|
1229
|
+
const MIN_EXIT_GRACE_MS = 5e3;
|
|
1230
|
+
const MAX_EXIT_GRACE_MS = 6e5;
|
|
1231
|
+
const MAX_REPORTED_HANDLE_TYPES = 20;
|
|
1232
|
+
const MAX_REPORTED_HANDLE_TYPE_BYTES = 64;
|
|
1233
|
+
const IGNORED_HANDLE_TYPES = /* @__PURE__ */ new Set(["PipeWrap"]);
|
|
1234
|
+
function clampGraceMs(value) {
|
|
1235
|
+
return Math.min(MAX_EXIT_GRACE_MS, Math.max(MIN_EXIT_GRACE_MS, Math.floor(value)));
|
|
1236
|
+
}
|
|
1237
|
+
function resolveExitWatchdog(config) {
|
|
1238
|
+
const enabled = process.env[EXIT_WATCHDOG_ENV_VAR] === "off" ? false : config?.enabled !== false;
|
|
1239
|
+
let graceMs = DEFAULT_EXIT_GRACE_MS;
|
|
1240
|
+
if (config?.graceMs !== void 0) if (typeof config.graceMs === "number" && Number.isFinite(config.graceMs) && config.graceMs > 0) graceMs = clampGraceMs(config.graceMs);
|
|
1241
|
+
else logWarning(`Ignoring buildExitWatchdog.graceMs=${String(config.graceMs)}: expected a positive number of ms.`);
|
|
1242
|
+
const envGrace = process.env[EXIT_GRACE_ENV_VAR];
|
|
1243
|
+
if (envGrace) if (/^\d+$/.test(envGrace) && Number(envGrace) > 0) graceMs = clampGraceMs(Number(envGrace));
|
|
1244
|
+
else logWarning(`Ignoring ${EXIT_GRACE_ENV_VAR}=${envGrace}: expected a positive integer of ms.`);
|
|
1245
|
+
return {
|
|
1246
|
+
enabled,
|
|
1247
|
+
graceMs
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
function cutUtf8(value, maxBytes) {
|
|
1251
|
+
let out = "";
|
|
1252
|
+
let bytes = 0;
|
|
1253
|
+
for (const char of value) {
|
|
1254
|
+
bytes += Buffer.byteLength(char);
|
|
1255
|
+
if (bytes > maxBytes) break;
|
|
1256
|
+
out += char;
|
|
1257
|
+
}
|
|
1258
|
+
return out;
|
|
1259
|
+
}
|
|
1260
|
+
function collectOpenHandles() {
|
|
1261
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1262
|
+
let resources = [];
|
|
1263
|
+
try {
|
|
1264
|
+
resources = process.getActiveResourcesInfo();
|
|
1265
|
+
} catch {}
|
|
1266
|
+
for (const resource of resources) {
|
|
1267
|
+
if (IGNORED_HANDLE_TYPES.has(resource)) continue;
|
|
1268
|
+
const type = cutUtf8(resource, MAX_REPORTED_HANDLE_TYPE_BYTES);
|
|
1269
|
+
counts.set(type, (counts.get(type) ?? 0) + 1);
|
|
1270
|
+
}
|
|
1271
|
+
const ranked = [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, MAX_REPORTED_HANDLE_TYPES);
|
|
1272
|
+
return Object.fromEntries(ranked);
|
|
1273
|
+
}
|
|
1274
|
+
function emitBuildExitReport(graceMs, exitCode, openHandles) {
|
|
1275
|
+
const payload = JSON.stringify({
|
|
1276
|
+
grace_ms: graceMs,
|
|
1277
|
+
exit_code: exitCode,
|
|
1278
|
+
open_handles: openHandles
|
|
1279
|
+
});
|
|
1280
|
+
try {
|
|
1281
|
+
process.stderr.write(`\n${BUILD_EXIT_REPORT_MARKER_OPEN}\n${payload}\n${BUILD_EXIT_REPORT_MARKER_CLOSE}\n`);
|
|
1282
|
+
} catch {}
|
|
1283
|
+
}
|
|
1284
|
+
function prerenderExitWatchdog(graceMs) {
|
|
1285
|
+
return {
|
|
1286
|
+
name: "lovable-prerender-exit-watchdog",
|
|
1287
|
+
apply: "build",
|
|
1288
|
+
enforce: "post",
|
|
1289
|
+
buildApp: {
|
|
1290
|
+
order: "post",
|
|
1291
|
+
handler: async () => {
|
|
1292
|
+
setTimeout(() => {
|
|
1293
|
+
const exitCode = Number(process.exitCode ?? 0) || 0;
|
|
1294
|
+
const openHandles = collectOpenHandles();
|
|
1295
|
+
emitBuildExitReport(graceMs, exitCode, openHandles);
|
|
1296
|
+
const types = Object.entries(openHandles).map(([type, count]) => `${type} x${count}`);
|
|
1297
|
+
logWarning(`Build output is complete but the process was still alive ${graceMs}ms after prerender (open handles: ${types.length > 0 ? types.join(", ") : "none reported"}); exiting with code ${exitCode}. Usual causes: TanStack Query gcTime timers armed during SSR, or module-scope timers and connection pools in server code.`);
|
|
1298
|
+
process.exit(exitCode);
|
|
1299
|
+
}, graceMs).unref();
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
929
1304
|
function prerenderIsEnabled(startOptions) {
|
|
930
1305
|
const spa = startOptions.spa;
|
|
931
1306
|
if (spa && (spa.enabled ?? true)) return true;
|
|
@@ -1084,9 +1459,10 @@ function defineConfig(configOrOptions = {}) {
|
|
|
1084
1459
|
optionObject.nitro = false;
|
|
1085
1460
|
logWarning("`cloudflare: false` is deprecated in v2 — migrated to `nitro: false`. Update your config to drop the warning.");
|
|
1086
1461
|
} else logWarning("The `cloudflare` option was removed in v2. Use `nitro: false` to disable the deploy plugin, or `nitro: { preset: '...' }` to configure it. The `cloudflare` setting is being ignored.");
|
|
1087
|
-
options = "vite" in optionObject || "serverFnErrorLogger" in optionObject || "ssrErrorLogger" in optionObject || "nitro" in optionObject || "tanstackStart" in optionObject || "react" in optionObject || "envDefine" in optionObject || "hmrGate" in optionObject ? optionObject : { vite: optionObject };
|
|
1462
|
+
options = "vite" in optionObject || "serverFnErrorLogger" in optionObject || "ssrErrorLogger" in optionObject || "nitro" in optionObject || "tanstackStart" in optionObject || "react" in optionObject || "envDefine" in optionObject || "hmrGate" in optionObject || "buildExitWatchdog" in optionObject ? optionObject : { vite: optionObject };
|
|
1088
1463
|
}
|
|
1089
1464
|
const internalPlugins = [];
|
|
1465
|
+
let exitWatchdog;
|
|
1090
1466
|
if (mode === "development") {
|
|
1091
1467
|
const { devtools } = await import("@tanstack/devtools-vite");
|
|
1092
1468
|
internalPlugins.push(devtools({
|
|
@@ -1117,6 +1493,10 @@ function defineConfig(configOrOptions = {}) {
|
|
|
1117
1493
|
if (command === "build" && isSandbox) {
|
|
1118
1494
|
const { startOptions, skipReporter, documentSet } = pinPrerenderForLovableBuild(tanstackStartOptions);
|
|
1119
1495
|
internalPlugins.push(tanstackStart(startOptions), skipReporter, documentSet);
|
|
1496
|
+
internalPlugins.push(createRouteManifestPlugin({
|
|
1497
|
+
tanstackStart: tanstackStartOptions,
|
|
1498
|
+
nitro: options.nitro
|
|
1499
|
+
}));
|
|
1120
1500
|
} else internalPlugins.push(tanstackStart(tanstackStartOptions));
|
|
1121
1501
|
const explicitNitro = options.nitro === true || typeof options.nitro === "object" && options.nitro !== null;
|
|
1122
1502
|
if (options.nitro !== false && command === "build") {
|
|
@@ -1171,6 +1551,10 @@ function defineConfig(configOrOptions = {}) {
|
|
|
1171
1551
|
const shimState = {};
|
|
1172
1552
|
internalPlugins.push(prerenderPreviewShim(shimState, nitroDirs), prerenderPreviewShimCleanup(shimState));
|
|
1173
1553
|
}
|
|
1554
|
+
if (isSandbox && prerenderEnabled) {
|
|
1555
|
+
const watchdog = resolveExitWatchdog(options.buildExitWatchdog);
|
|
1556
|
+
if (watchdog.enabled) exitWatchdog = prerenderExitWatchdog(watchdog.graceMs);
|
|
1557
|
+
}
|
|
1174
1558
|
if (isSandbox) internalPlugins.push(stripRedundantNodejsCompatFlag(nitroDirs));
|
|
1175
1559
|
}
|
|
1176
1560
|
}
|
|
@@ -1250,6 +1634,7 @@ function defineConfig(configOrOptions = {}) {
|
|
|
1250
1634
|
host: PRERENDER_PREVIEW_HOST,
|
|
1251
1635
|
port: 0
|
|
1252
1636
|
} }, config);
|
|
1637
|
+
if (exitWatchdog) config = mergeConfig(config, { plugins: [exitWatchdog] });
|
|
1253
1638
|
if (isSandbox) return config;
|
|
1254
1639
|
return applyWatchDebounceDefaults(config);
|
|
1255
1640
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lovable.dev/vite-tanstack-config",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.23.0",
|
|
4
4
|
"description": "Vite config wrapper for Lovable TanStack Start projects",
|
|
5
5
|
"main": "./dist/index.cjs",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -69,19 +69,28 @@
|
|
|
69
69
|
"vite-tsconfig-paths": ">=6.0.0",
|
|
70
70
|
"nitro": ">=3.0.260603-beta",
|
|
71
71
|
"@tanstack/react-start": ">=1.100.0",
|
|
72
|
+
"@tanstack/router-generator": ">=1.121.0 <1.166.45 || >=1.166.49",
|
|
72
73
|
"@vitejs/plugin-react": ">=4.0.0"
|
|
73
74
|
},
|
|
74
75
|
"peerDependenciesMeta": {
|
|
75
76
|
"nitro": {
|
|
76
77
|
"optional": true
|
|
78
|
+
},
|
|
79
|
+
"@tanstack/router-generator": {
|
|
80
|
+
"optional": true
|
|
77
81
|
}
|
|
78
82
|
},
|
|
79
83
|
"devDependencies": {
|
|
80
84
|
"@tailwindcss/vite": "^4.3.0",
|
|
85
|
+
"@tanstack/react-query": "^5.102.8",
|
|
86
|
+
"@tanstack/react-router": "1.170.30",
|
|
81
87
|
"@tanstack/react-start": "^1.167.50",
|
|
88
|
+
"@tanstack/router-generator": "^1.167.31",
|
|
82
89
|
"@types/node": "22.13.13",
|
|
83
90
|
"@vitejs/plugin-react": "^5.2.0",
|
|
84
91
|
"nitro": "3.0.260603-beta",
|
|
92
|
+
"react": "19.3.0",
|
|
93
|
+
"react-dom": "19.3.0",
|
|
85
94
|
"rimraf": "^5.0.0",
|
|
86
95
|
"tsdown": "^0.22.5",
|
|
87
96
|
"typescript": "7.0.2",
|
|
@@ -100,7 +109,8 @@
|
|
|
100
109
|
"typecheck": "tsgo --noEmit",
|
|
101
110
|
"dev": "tsdown --watch",
|
|
102
111
|
"test": "vitest run",
|
|
103
|
-
"test:
|
|
112
|
+
"test:unit": "vitest run --project unit",
|
|
113
|
+
"test:watch": "vitest --project unit",
|
|
104
114
|
"clean": "rimraf dist",
|
|
105
115
|
"coverage": "vitest run --coverage"
|
|
106
116
|
}
|