@lovable.dev/vite-tanstack-config 2.20.0 → 2.22.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 CHANGED
@@ -12,3 +12,7 @@ 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.
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.20.0";
156
+ var version = "2.22.0";
157
157
  //#endregion
158
158
  //#region src/logging.ts
159
159
  const PACKAGE_NAME = "@lovable.dev/vite-tanstack-config";
@@ -950,6 +950,86 @@ function prerenderPreviewShimCleanup(state) {
950
950
  }
951
951
  };
952
952
  }
953
+ const BUILD_EXIT_REPORT_MARKER_OPEN = "[lovable-build-exit-report]";
954
+ const BUILD_EXIT_REPORT_MARKER_CLOSE = "[/lovable-build-exit-report]";
955
+ const EXIT_WATCHDOG_ENV_VAR = "LOVABLE_PRERENDER_EXIT_WATCHDOG";
956
+ const EXIT_GRACE_ENV_VAR = "LOVABLE_PRERENDER_EXIT_GRACE_MS";
957
+ const DEFAULT_EXIT_GRACE_MS = 6e4;
958
+ const MIN_EXIT_GRACE_MS = 5e3;
959
+ const MAX_EXIT_GRACE_MS = 6e5;
960
+ const MAX_REPORTED_HANDLE_TYPES = 20;
961
+ const MAX_REPORTED_HANDLE_TYPE_BYTES = 64;
962
+ const IGNORED_HANDLE_TYPES = /* @__PURE__ */ new Set(["PipeWrap"]);
963
+ function clampGraceMs(value) {
964
+ return Math.min(MAX_EXIT_GRACE_MS, Math.max(MIN_EXIT_GRACE_MS, Math.floor(value)));
965
+ }
966
+ function resolveExitWatchdog(config) {
967
+ const enabled = process.env[EXIT_WATCHDOG_ENV_VAR] === "off" ? false : config?.enabled !== false;
968
+ let graceMs = DEFAULT_EXIT_GRACE_MS;
969
+ if (config?.graceMs !== void 0) if (typeof config.graceMs === "number" && Number.isFinite(config.graceMs) && config.graceMs > 0) graceMs = clampGraceMs(config.graceMs);
970
+ else logWarning(`Ignoring buildExitWatchdog.graceMs=${String(config.graceMs)}: expected a positive number of ms.`);
971
+ const envGrace = process.env[EXIT_GRACE_ENV_VAR];
972
+ if (envGrace) if (/^\d+$/.test(envGrace) && Number(envGrace) > 0) graceMs = clampGraceMs(Number(envGrace));
973
+ else logWarning(`Ignoring ${EXIT_GRACE_ENV_VAR}=${envGrace}: expected a positive integer of ms.`);
974
+ return {
975
+ enabled,
976
+ graceMs
977
+ };
978
+ }
979
+ function cutUtf8(value, maxBytes) {
980
+ let out = "";
981
+ let bytes = 0;
982
+ for (const char of value) {
983
+ bytes += Buffer.byteLength(char);
984
+ if (bytes > maxBytes) break;
985
+ out += char;
986
+ }
987
+ return out;
988
+ }
989
+ function collectOpenHandles() {
990
+ const counts = /* @__PURE__ */ new Map();
991
+ let resources = [];
992
+ try {
993
+ resources = process.getActiveResourcesInfo();
994
+ } catch {}
995
+ for (const resource of resources) {
996
+ if (IGNORED_HANDLE_TYPES.has(resource)) continue;
997
+ const type = cutUtf8(resource, MAX_REPORTED_HANDLE_TYPE_BYTES);
998
+ counts.set(type, (counts.get(type) ?? 0) + 1);
999
+ }
1000
+ const ranked = [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, MAX_REPORTED_HANDLE_TYPES);
1001
+ return Object.fromEntries(ranked);
1002
+ }
1003
+ function emitBuildExitReport(graceMs, exitCode, openHandles) {
1004
+ const payload = JSON.stringify({
1005
+ grace_ms: graceMs,
1006
+ exit_code: exitCode,
1007
+ open_handles: openHandles
1008
+ });
1009
+ try {
1010
+ process.stderr.write(`\n${BUILD_EXIT_REPORT_MARKER_OPEN}\n${payload}\n${BUILD_EXIT_REPORT_MARKER_CLOSE}\n`);
1011
+ } catch {}
1012
+ }
1013
+ function prerenderExitWatchdog(graceMs) {
1014
+ return {
1015
+ name: "lovable-prerender-exit-watchdog",
1016
+ apply: "build",
1017
+ enforce: "post",
1018
+ buildApp: {
1019
+ order: "post",
1020
+ handler: async () => {
1021
+ setTimeout(() => {
1022
+ const exitCode = Number(process.exitCode ?? 0) || 0;
1023
+ const openHandles = collectOpenHandles();
1024
+ emitBuildExitReport(graceMs, exitCode, openHandles);
1025
+ const types = Object.entries(openHandles).map(([type, count]) => `${type} x${count}`);
1026
+ 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.`);
1027
+ process.exit(exitCode);
1028
+ }, graceMs).unref();
1029
+ }
1030
+ }
1031
+ };
1032
+ }
953
1033
  function prerenderIsEnabled(startOptions) {
954
1034
  const spa = startOptions.spa;
955
1035
  if (spa && (spa.enabled ?? true)) return true;
@@ -1108,9 +1188,10 @@ function defineConfig(configOrOptions = {}) {
1108
1188
  optionObject.nitro = false;
1109
1189
  logWarning("`cloudflare: false` is deprecated in v2 — migrated to `nitro: false`. Update your config to drop the warning.");
1110
1190
  } 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 };
1191
+ 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
1192
  }
1113
1193
  const internalPlugins = [];
1194
+ let exitWatchdog;
1114
1195
  if (mode === "development") {
1115
1196
  const { devtools } = await import("@tanstack/devtools-vite");
1116
1197
  internalPlugins.push(devtools({
@@ -1195,6 +1276,10 @@ function defineConfig(configOrOptions = {}) {
1195
1276
  const shimState = {};
1196
1277
  internalPlugins.push(prerenderPreviewShim(shimState, nitroDirs), prerenderPreviewShimCleanup(shimState));
1197
1278
  }
1279
+ if (isSandbox && prerenderEnabled) {
1280
+ const watchdog = resolveExitWatchdog(options.buildExitWatchdog);
1281
+ if (watchdog.enabled) exitWatchdog = prerenderExitWatchdog(watchdog.graceMs);
1282
+ }
1198
1283
  if (isSandbox) internalPlugins.push(stripRedundantNodejsCompatFlag(nitroDirs));
1199
1284
  }
1200
1285
  }
@@ -1274,6 +1359,7 @@ function defineConfig(configOrOptions = {}) {
1274
1359
  host: PRERENDER_PREVIEW_HOST,
1275
1360
  port: 0
1276
1361
  } }, config);
1362
+ if (exitWatchdog) config = (0, vite.mergeConfig)(config, { plugins: [exitWatchdog] });
1277
1363
  if (isSandbox) return config;
1278
1364
  return applyWatchDebounceDefaults(config);
1279
1365
  };
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.20.0";
132
+ var version = "2.22.0";
133
133
  //#endregion
134
134
  //#region src/logging.ts
135
135
  const PACKAGE_NAME = "@lovable.dev/vite-tanstack-config";
@@ -926,6 +926,86 @@ function prerenderPreviewShimCleanup(state) {
926
926
  }
927
927
  };
928
928
  }
929
+ const BUILD_EXIT_REPORT_MARKER_OPEN = "[lovable-build-exit-report]";
930
+ const BUILD_EXIT_REPORT_MARKER_CLOSE = "[/lovable-build-exit-report]";
931
+ const EXIT_WATCHDOG_ENV_VAR = "LOVABLE_PRERENDER_EXIT_WATCHDOG";
932
+ const EXIT_GRACE_ENV_VAR = "LOVABLE_PRERENDER_EXIT_GRACE_MS";
933
+ const DEFAULT_EXIT_GRACE_MS = 6e4;
934
+ const MIN_EXIT_GRACE_MS = 5e3;
935
+ const MAX_EXIT_GRACE_MS = 6e5;
936
+ const MAX_REPORTED_HANDLE_TYPES = 20;
937
+ const MAX_REPORTED_HANDLE_TYPE_BYTES = 64;
938
+ const IGNORED_HANDLE_TYPES = /* @__PURE__ */ new Set(["PipeWrap"]);
939
+ function clampGraceMs(value) {
940
+ return Math.min(MAX_EXIT_GRACE_MS, Math.max(MIN_EXIT_GRACE_MS, Math.floor(value)));
941
+ }
942
+ function resolveExitWatchdog(config) {
943
+ const enabled = process.env[EXIT_WATCHDOG_ENV_VAR] === "off" ? false : config?.enabled !== false;
944
+ let graceMs = DEFAULT_EXIT_GRACE_MS;
945
+ if (config?.graceMs !== void 0) if (typeof config.graceMs === "number" && Number.isFinite(config.graceMs) && config.graceMs > 0) graceMs = clampGraceMs(config.graceMs);
946
+ else logWarning(`Ignoring buildExitWatchdog.graceMs=${String(config.graceMs)}: expected a positive number of ms.`);
947
+ const envGrace = process.env[EXIT_GRACE_ENV_VAR];
948
+ if (envGrace) if (/^\d+$/.test(envGrace) && Number(envGrace) > 0) graceMs = clampGraceMs(Number(envGrace));
949
+ else logWarning(`Ignoring ${EXIT_GRACE_ENV_VAR}=${envGrace}: expected a positive integer of ms.`);
950
+ return {
951
+ enabled,
952
+ graceMs
953
+ };
954
+ }
955
+ function cutUtf8(value, maxBytes) {
956
+ let out = "";
957
+ let bytes = 0;
958
+ for (const char of value) {
959
+ bytes += Buffer.byteLength(char);
960
+ if (bytes > maxBytes) break;
961
+ out += char;
962
+ }
963
+ return out;
964
+ }
965
+ function collectOpenHandles() {
966
+ const counts = /* @__PURE__ */ new Map();
967
+ let resources = [];
968
+ try {
969
+ resources = process.getActiveResourcesInfo();
970
+ } catch {}
971
+ for (const resource of resources) {
972
+ if (IGNORED_HANDLE_TYPES.has(resource)) continue;
973
+ const type = cutUtf8(resource, MAX_REPORTED_HANDLE_TYPE_BYTES);
974
+ counts.set(type, (counts.get(type) ?? 0) + 1);
975
+ }
976
+ const ranked = [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, MAX_REPORTED_HANDLE_TYPES);
977
+ return Object.fromEntries(ranked);
978
+ }
979
+ function emitBuildExitReport(graceMs, exitCode, openHandles) {
980
+ const payload = JSON.stringify({
981
+ grace_ms: graceMs,
982
+ exit_code: exitCode,
983
+ open_handles: openHandles
984
+ });
985
+ try {
986
+ process.stderr.write(`\n${BUILD_EXIT_REPORT_MARKER_OPEN}\n${payload}\n${BUILD_EXIT_REPORT_MARKER_CLOSE}\n`);
987
+ } catch {}
988
+ }
989
+ function prerenderExitWatchdog(graceMs) {
990
+ return {
991
+ name: "lovable-prerender-exit-watchdog",
992
+ apply: "build",
993
+ enforce: "post",
994
+ buildApp: {
995
+ order: "post",
996
+ handler: async () => {
997
+ setTimeout(() => {
998
+ const exitCode = Number(process.exitCode ?? 0) || 0;
999
+ const openHandles = collectOpenHandles();
1000
+ emitBuildExitReport(graceMs, exitCode, openHandles);
1001
+ const types = Object.entries(openHandles).map(([type, count]) => `${type} x${count}`);
1002
+ 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.`);
1003
+ process.exit(exitCode);
1004
+ }, graceMs).unref();
1005
+ }
1006
+ }
1007
+ };
1008
+ }
929
1009
  function prerenderIsEnabled(startOptions) {
930
1010
  const spa = startOptions.spa;
931
1011
  if (spa && (spa.enabled ?? true)) return true;
@@ -1084,9 +1164,10 @@ function defineConfig(configOrOptions = {}) {
1084
1164
  optionObject.nitro = false;
1085
1165
  logWarning("`cloudflare: false` is deprecated in v2 — migrated to `nitro: false`. Update your config to drop the warning.");
1086
1166
  } 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 };
1167
+ 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
1168
  }
1089
1169
  const internalPlugins = [];
1170
+ let exitWatchdog;
1090
1171
  if (mode === "development") {
1091
1172
  const { devtools } = await import("@tanstack/devtools-vite");
1092
1173
  internalPlugins.push(devtools({
@@ -1171,6 +1252,10 @@ function defineConfig(configOrOptions = {}) {
1171
1252
  const shimState = {};
1172
1253
  internalPlugins.push(prerenderPreviewShim(shimState, nitroDirs), prerenderPreviewShimCleanup(shimState));
1173
1254
  }
1255
+ if (isSandbox && prerenderEnabled) {
1256
+ const watchdog = resolveExitWatchdog(options.buildExitWatchdog);
1257
+ if (watchdog.enabled) exitWatchdog = prerenderExitWatchdog(watchdog.graceMs);
1258
+ }
1174
1259
  if (isSandbox) internalPlugins.push(stripRedundantNodejsCompatFlag(nitroDirs));
1175
1260
  }
1176
1261
  }
@@ -1250,6 +1335,7 @@ function defineConfig(configOrOptions = {}) {
1250
1335
  host: PRERENDER_PREVIEW_HOST,
1251
1336
  port: 0
1252
1337
  } }, config);
1338
+ if (exitWatchdog) config = mergeConfig(config, { plugins: [exitWatchdog] });
1253
1339
  if (isSandbox) return config;
1254
1340
  return applyWatchDebounceDefaults(config);
1255
1341
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovable.dev/vite-tanstack-config",
3
- "version": "2.20.0",
3
+ "version": "2.22.0",
4
4
  "description": "Vite config wrapper for Lovable TanStack Start projects",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",