@lovable.dev/vite-tanstack-config 2.12.1 → 2.13.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.
@@ -0,0 +1,350 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ //#region src/hmr-gate/index.ts
4
+ const statusCorsOriginRe = /^(?:https:\/\/(?:[^/]+\.)?lovable\.dev|https?:\/\/(?:(?:[^/:]+\.)?localhost|127\.0\.0\.1|\[::1\]))(?::\d+)?$/;
5
+ function requestPathname(req) {
6
+ const url = req.url ?? "";
7
+ const queryStart = url.indexOf("?");
8
+ return queryStart === -1 ? url : url.slice(0, queryStart);
9
+ }
10
+ function gateStatusHeaders(req) {
11
+ const origin = req.headers.origin;
12
+ const headers = {
13
+ "Content-Type": "application/json",
14
+ Vary: "Origin"
15
+ };
16
+ if (typeof origin === "string" && statusCorsOriginRe.test(origin)) headers["Access-Control-Allow-Origin"] = origin;
17
+ return headers;
18
+ }
19
+ const DEV_SERVER_MODE_EVENT = "lovable:dev-server-mode";
20
+ const EDIT_FREE_RELOAD_MARKER = "edit-free";
21
+ /** Host-controlled default for `fullReload`; an explicit option still wins. */
22
+ const FULL_RELOAD_ENV_VAR = "LOVABLE_HMR_FULL_RELOAD";
23
+ function hmrGatePlugin(options = {}) {
24
+ const fullReload = options.fullReload ?? process.env["LOVABLE_HMR_FULL_RELOAD"] !== "false";
25
+ const gatedEvents = new Set(options.events ?? [
26
+ "change",
27
+ "add",
28
+ "unlink"
29
+ ]);
30
+ const passthroughPatterns = options.passthrough ?? [];
31
+ const pending = /* @__PURE__ */ new Map();
32
+ const lastFlushedHashes = /* @__PURE__ */ new Map();
33
+ let originalEmit = null;
34
+ let server = null;
35
+ let classicFailOpenTimer = null;
36
+ let transportMode = false;
37
+ let swallowedUpdates = 0;
38
+ let heldReload = false;
39
+ let passthroughReloads = 0;
40
+ const wrappedSends = [];
41
+ const drainMs = options.drainMs ?? 1500;
42
+ const maxHoldMs = options.maxHoldMs ?? 24e4;
43
+ const startupGraceMs = options.startupGraceMs ?? 5e3;
44
+ let startupGraceUntil = 0;
45
+ const patchPathRe = /^\/hmr_patch_\d+\.js(\?|$)/;
46
+ const parked = [];
47
+ let drainUntil = 0;
48
+ let restartTimer = null;
49
+ let reloadNeededListener = null;
50
+ const bufferedReloadNeeded = /* @__PURE__ */ new Map();
51
+ const pendingReplay = /* @__PURE__ */ new Map();
52
+ let reloadFailOpenTimer = null;
53
+ function replayBufferedReloadNeeded() {
54
+ if (reloadNeededListener === null || bufferedReloadNeeded.size === 0) return;
55
+ const entries = Array.from(bufferedReloadNeeded.entries());
56
+ bufferedReloadNeeded.clear();
57
+ for (const [client, payload] of entries) reloadNeededListener(payload, client);
58
+ }
59
+ function releaseParked() {
60
+ const count = parked.length;
61
+ for (const entry of parked.splice(0)) entry.release();
62
+ return count;
63
+ }
64
+ function initialBuildPending() {
65
+ const bundled = server?.environments?.client?.bundledDev;
66
+ return bundled !== void 0 && bundled.initialBuildCompleted !== true;
67
+ }
68
+ function gateSendTarget(target) {
69
+ const original = target.send.bind(target);
70
+ wrappedSends.push({
71
+ target,
72
+ original
73
+ });
74
+ target.send = (...args) => {
75
+ const frame = args[0];
76
+ const type = frame?.type;
77
+ if (type === "update") {
78
+ swallowedUpdates++;
79
+ return;
80
+ }
81
+ if (type === "full-reload") {
82
+ if (frame?.ifFallback === true || frame?.lovable?.hmrGate === EDIT_FREE_RELOAD_MARKER || Date.now() < startupGraceUntil || initialBuildPending()) {
83
+ passthroughReloads++;
84
+ return original(...args);
85
+ }
86
+ heldReload = true;
87
+ return;
88
+ }
89
+ return original(...args);
90
+ };
91
+ }
92
+ function fileContentHash(filePath) {
93
+ try {
94
+ return createHash("sha1").update(readFileSync(filePath)).digest("hex");
95
+ } catch {
96
+ return null;
97
+ }
98
+ }
99
+ function isPassthrough(filePath) {
100
+ if (typeof filePath !== "string") return true;
101
+ if (filePath.includes("node_modules/.vite")) return true;
102
+ return passthroughPatterns.some((p) => filePath.includes(p));
103
+ }
104
+ return {
105
+ name: "hmr-gate",
106
+ apply: "serve",
107
+ configResolved(config) {
108
+ transportMode = config.experimental?.bundledDev === true;
109
+ },
110
+ configureServer(srv) {
111
+ server = srv;
112
+ srv.ws.on?.("connection", (socket) => {
113
+ try {
114
+ socket.send(JSON.stringify({
115
+ type: "custom",
116
+ event: DEV_SERVER_MODE_EVENT,
117
+ data: { mode: transportMode ? "bundled" : "classic" }
118
+ }));
119
+ } catch {}
120
+ });
121
+ if (transportMode) {
122
+ const startedAtMs = Date.now();
123
+ startupGraceUntil = startedAtMs + startupGraceMs;
124
+ const ws = srv.ws;
125
+ const hot = srv.environments?.client?.hot;
126
+ const target = typeof ws?.send === "function" ? ws : hot;
127
+ if (target && typeof target.send === "function") gateSendTarget(target);
128
+ const hotChannel = hot;
129
+ if (hotChannel && typeof hotChannel.on === "function") {
130
+ const originalOn = hotChannel.on.bind(hotChannel);
131
+ hotChannel.on = (event, listener) => {
132
+ if (event !== "vite:bundled-dev:reload-needed") return originalOn(event, listener);
133
+ reloadNeededListener = listener;
134
+ originalOn(event, (payload, client) => {
135
+ if (Date.now() < startupGraceUntil || initialBuildPending()) return listener(payload, client);
136
+ bufferedReloadNeeded.set(client, payload);
137
+ if (reloadFailOpenTimer === null) {
138
+ reloadFailOpenTimer = setTimeout(() => {
139
+ reloadFailOpenTimer = null;
140
+ replayBufferedReloadNeeded();
141
+ }, maxHoldMs);
142
+ reloadFailOpenTimer.unref?.();
143
+ }
144
+ });
145
+ };
146
+ }
147
+ srv.middlewares.use((req, _res, next) => {
148
+ if (req.method === "GET" && patchPathRe.test(req.url ?? "") && Date.now() >= drainUntil) {
149
+ let released = false;
150
+ const release = () => {
151
+ if (released) return;
152
+ released = true;
153
+ clearTimeout(timer);
154
+ const index = parked.indexOf(entry);
155
+ if (index !== -1) parked.splice(index, 1);
156
+ next();
157
+ };
158
+ const timer = setTimeout(release, maxHoldMs);
159
+ timer.unref?.();
160
+ const entry = { release };
161
+ parked.push(entry);
162
+ return;
163
+ }
164
+ next();
165
+ });
166
+ srv.middlewares.use((req, res, next) => {
167
+ if (req.method !== "POST" || requestPathname(req) !== "/__hmr_flush") return next();
168
+ const priorDrainUntil = drainUntil;
169
+ const released = releaseParked();
170
+ drainUntil = Date.now() + drainMs;
171
+ const updates = swallowedUpdates;
172
+ swallowedUpdates = 0;
173
+ const broadcastHeld = heldReload;
174
+ heldReload = false;
175
+ const reload = broadcastHeld || bufferedReloadNeeded.size > 0;
176
+ let flushRestarts = false;
177
+ if (reload) {
178
+ const releaseDelayMs = Math.max(released > 0 ? drainMs : 0, priorDrainUntil - Date.now(), 0);
179
+ const restart = srv.restart;
180
+ if (restartTimer !== null) clearTimeout(restartTimer);
181
+ if (reloadFailOpenTimer !== null) {
182
+ clearTimeout(reloadFailOpenTimer);
183
+ reloadFailOpenTimer = null;
184
+ }
185
+ if (!broadcastHeld && bufferedReloadNeeded.size > 0 && reloadNeededListener !== null) {
186
+ for (const [client, payload] of bufferedReloadNeeded) pendingReplay.set(client, payload);
187
+ bufferedReloadNeeded.clear();
188
+ restartTimer = setTimeout(() => {
189
+ restartTimer = null;
190
+ const entries = Array.from(pendingReplay.entries());
191
+ pendingReplay.clear();
192
+ for (const [client, payload] of entries) reloadNeededListener?.(payload, client);
193
+ }, releaseDelayMs);
194
+ restartTimer.unref?.();
195
+ } else if (typeof restart === "function") {
196
+ flushRestarts = true;
197
+ bufferedReloadNeeded.clear();
198
+ pendingReplay.clear();
199
+ restartTimer = setTimeout(() => {
200
+ restartTimer = null;
201
+ restart.call(srv).catch(() => {
202
+ if (wrappedSends.length > 0) wrappedSends[0].original({ type: "full-reload" });
203
+ });
204
+ }, releaseDelayMs);
205
+ restartTimer.unref?.();
206
+ } else if (wrappedSends.length > 0) {
207
+ bufferedReloadNeeded.clear();
208
+ pendingReplay.clear();
209
+ wrappedSends[0].original({ type: "full-reload" });
210
+ }
211
+ }
212
+ const body = JSON.stringify({
213
+ flushed: [],
214
+ count: released + updates,
215
+ released,
216
+ reload,
217
+ restartsOnReload: flushRestarts,
218
+ mode: "transport-patch-hold"
219
+ });
220
+ res.writeHead(200, { "Content-Type": "application/json" });
221
+ res.end(body);
222
+ });
223
+ srv.middlewares.use((req, res, next) => {
224
+ if (req.method !== "GET" || requestPathname(req) !== "/__hmr_gate") return next();
225
+ const body = JSON.stringify({
226
+ enabled: true,
227
+ pending: {},
228
+ count: parked.length + swallowedUpdates + (heldReload || bufferedReloadNeeded.size > 0 ? 1 : 0),
229
+ heldReload: heldReload || bufferedReloadNeeded.size > 0,
230
+ restartsOnReload: reloadNeededListener === null || heldReload,
231
+ startedAt: startedAtMs,
232
+ passthroughReloads,
233
+ mode: "transport-patch-hold"
234
+ });
235
+ res.writeHead(200, gateStatusHeaders(req));
236
+ res.end(body);
237
+ });
238
+ return;
239
+ }
240
+ const _emit = srv.watcher.emit.bind(srv.watcher);
241
+ originalEmit = _emit;
242
+ const flushPending = () => {
243
+ if (classicFailOpenTimer !== null) {
244
+ clearTimeout(classicFailOpenTimer);
245
+ classicFailOpenTimer = null;
246
+ }
247
+ const entries = Array.from(pending.entries());
248
+ pending.clear();
249
+ for (const [f] of entries) srv.moduleGraph.onFileChange(f);
250
+ if (entries.length > 0) {
251
+ for (const [f, events] of entries) {
252
+ for (const event of events) _emit(event, f);
253
+ const hash = fileContentHash(f);
254
+ if (hash === null) lastFlushedHashes.delete(f);
255
+ else lastFlushedHashes.set(f, hash);
256
+ }
257
+ if (fullReload) srv.ws.send({ type: "full-reload" });
258
+ }
259
+ return entries;
260
+ };
261
+ srv.watcher.emit = ((event, ...args) => {
262
+ if (typeof event === "string" && gatedEvents.has(event) && !isPassthrough(args[0])) {
263
+ const filePath = args[0];
264
+ if (event === "change" || event === "add") {
265
+ const hash = fileContentHash(filePath);
266
+ if (hash !== null && hash === lastFlushedHashes.get(filePath)) {
267
+ pending.delete(filePath);
268
+ if (pending.size === 0 && classicFailOpenTimer !== null) {
269
+ clearTimeout(classicFailOpenTimer);
270
+ classicFailOpenTimer = null;
271
+ }
272
+ return false;
273
+ }
274
+ }
275
+ let events = pending.get(filePath);
276
+ if (!events) {
277
+ events = /* @__PURE__ */ new Set();
278
+ pending.set(filePath, events);
279
+ }
280
+ events.add(event);
281
+ if (classicFailOpenTimer === null) {
282
+ classicFailOpenTimer = setTimeout(() => {
283
+ classicFailOpenTimer = null;
284
+ flushPending();
285
+ }, maxHoldMs);
286
+ classicFailOpenTimer.unref?.();
287
+ }
288
+ return false;
289
+ }
290
+ return _emit(event, ...args);
291
+ });
292
+ srv.middlewares.use((req, res, next) => {
293
+ if (req.method !== "POST" || requestPathname(req) !== "/__hmr_flush") return next();
294
+ const entries = flushPending();
295
+ const files = entries.map(([f]) => f);
296
+ const body = JSON.stringify({
297
+ flushed: files,
298
+ count: entries.length,
299
+ mode: fullReload ? "full-reload" : "granular"
300
+ });
301
+ res.writeHead(200, { "Content-Type": "application/json" });
302
+ res.end(body);
303
+ });
304
+ srv.middlewares.use((req, res, next) => {
305
+ if (req.method !== "GET" || requestPathname(req) !== "/__hmr_gate") return next();
306
+ const serialized = {};
307
+ for (const [f, events] of pending) serialized[f] = Array.from(events);
308
+ const body = JSON.stringify({
309
+ enabled: true,
310
+ pending: serialized,
311
+ count: pending.size,
312
+ mode: fullReload ? "full-reload" : "granular"
313
+ });
314
+ res.writeHead(200, gateStatusHeaders(req));
315
+ res.end(body);
316
+ });
317
+ },
318
+ closeWatcher() {
319
+ if (server && originalEmit) server.watcher.emit = originalEmit;
320
+ for (const { target, original } of wrappedSends) target.send = original;
321
+ wrappedSends.length = 0;
322
+ if (restartTimer !== null) {
323
+ clearTimeout(restartTimer);
324
+ restartTimer = null;
325
+ }
326
+ releaseParked();
327
+ if (reloadFailOpenTimer !== null) {
328
+ clearTimeout(reloadFailOpenTimer);
329
+ reloadFailOpenTimer = null;
330
+ }
331
+ if (classicFailOpenTimer !== null) {
332
+ clearTimeout(classicFailOpenTimer);
333
+ classicFailOpenTimer = null;
334
+ }
335
+ bufferedReloadNeeded.clear();
336
+ pendingReplay.clear();
337
+ reloadNeededListener = null;
338
+ swallowedUpdates = 0;
339
+ heldReload = false;
340
+ passthroughReloads = 0;
341
+ drainUntil = 0;
342
+ pending.clear();
343
+ lastFlushedHashes.clear();
344
+ server = null;
345
+ originalEmit = null;
346
+ }
347
+ };
348
+ }
349
+ //#endregion
350
+ export { DEV_SERVER_MODE_EVENT, FULL_RELOAD_ENV_VAR, hmrGatePlugin };
package/dist/index.cjs CHANGED
@@ -717,13 +717,13 @@ function defineConfig(configOrOptions = {}) {
717
717
  if (command === "serve") {
718
718
  const hmrGateOpt = options.hmrGate;
719
719
  if (isSandbox ? hmrGateOpt !== false : !!hmrGateOpt) {
720
- const { hmrGatePlugin } = await import("@lovable.dev/vite-plugin-hmr-gate");
720
+ const { hmrGatePlugin } = await Promise.resolve().then(() => require("./hmr-gate.cjs"));
721
721
  const hmrGateOptions = typeof hmrGateOpt === "object" ? hmrGateOpt : {};
722
722
  internalPlugins.push(hmrGatePlugin(hmrGateOptions));
723
723
  }
724
724
  }
725
725
  if (command === "serve" && isSandbox) {
726
- const { devServerBridgePlugin } = await import("@lovable.dev/vite-plugin-dev-server-bridge");
726
+ const { devServerBridgePlugin } = await Promise.resolve().then(() => require("./dev-server-bridge.cjs"));
727
727
  internalPlugins.push(devServerBridgePlugin());
728
728
  }
729
729
  if (command === "serve") internalPlugins.push(lovableAssetsProxyPlugin());
package/dist/index.d.cts CHANGED
@@ -58,11 +58,7 @@ interface LovableViteTanstackOptions {
58
58
  react?: Record<string, unknown>;
59
59
  /** Set to false to disable automatic VITE_* env define injection. Default: true. */
60
60
  envDefine?: boolean;
61
- /**
62
- * Options for the HMR gate plugin (@lovable.dev/vite-plugin-hmr-gate).
63
- * Enabled by default in sandbox mode. Set to false to disable, or pass options
64
- * to customise behaviour. Set to true or an options object to enable outside sandbox.
65
- */
61
+ /** hmrGatePlugin() options. Default on in sandbox (false disables); true or an options object enables elsewhere. */
66
62
  hmrGate?: boolean | Record<string, unknown>;
67
63
  }
68
64
  declare function defineConfig(config: UserConfig): (env: ConfigEnv) => Promise<UserConfig>;
package/dist/index.d.ts CHANGED
@@ -58,11 +58,7 @@ interface LovableViteTanstackOptions {
58
58
  react?: Record<string, unknown>;
59
59
  /** Set to false to disable automatic VITE_* env define injection. Default: true. */
60
60
  envDefine?: boolean;
61
- /**
62
- * Options for the HMR gate plugin (@lovable.dev/vite-plugin-hmr-gate).
63
- * Enabled by default in sandbox mode. Set to false to disable, or pass options
64
- * to customise behaviour. Set to true or an options object to enable outside sandbox.
65
- */
61
+ /** hmrGatePlugin() options. Default on in sandbox (false disables); true or an options object enables elsewhere. */
66
62
  hmrGate?: boolean | Record<string, unknown>;
67
63
  }
68
64
  declare function defineConfig(config: UserConfig): (env: ConfigEnv) => Promise<UserConfig>;
package/dist/index.js CHANGED
@@ -693,13 +693,13 @@ function defineConfig(configOrOptions = {}) {
693
693
  if (command === "serve") {
694
694
  const hmrGateOpt = options.hmrGate;
695
695
  if (isSandbox ? hmrGateOpt !== false : !!hmrGateOpt) {
696
- const { hmrGatePlugin } = await import("@lovable.dev/vite-plugin-hmr-gate");
696
+ const { hmrGatePlugin } = await import("./hmr-gate.js");
697
697
  const hmrGateOptions = typeof hmrGateOpt === "object" ? hmrGateOpt : {};
698
698
  internalPlugins.push(hmrGatePlugin(hmrGateOptions));
699
699
  }
700
700
  }
701
701
  if (command === "serve" && isSandbox) {
702
- const { devServerBridgePlugin } = await import("@lovable.dev/vite-plugin-dev-server-bridge");
702
+ const { devServerBridgePlugin } = await import("./dev-server-bridge.js");
703
703
  internalPlugins.push(devServerBridgePlugin());
704
704
  }
705
705
  if (command === "serve") internalPlugins.push(lovableAssetsProxyPlugin());
package/package.json CHANGED
@@ -1,11 +1,53 @@
1
1
  {
2
2
  "name": "@lovable.dev/vite-tanstack-config",
3
- "version": "2.12.1",
3
+ "version": "2.13.1",
4
4
  "description": "Vite config wrapper for Lovable TanStack Start projects",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
8
8
  "type": "module",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ },
20
+ "./hmr-gate": {
21
+ "import": {
22
+ "types": "./dist/hmr-gate.d.ts",
23
+ "default": "./dist/hmr-gate.js"
24
+ },
25
+ "require": {
26
+ "types": "./dist/hmr-gate.d.cts",
27
+ "default": "./dist/hmr-gate.cjs"
28
+ }
29
+ },
30
+ "./dev-server-bridge": {
31
+ "import": {
32
+ "types": "./dist/dev-server-bridge.d.ts",
33
+ "default": "./dist/dev-server-bridge.js"
34
+ },
35
+ "require": {
36
+ "types": "./dist/dev-server-bridge.d.cts",
37
+ "default": "./dist/dev-server-bridge.cjs"
38
+ }
39
+ }
40
+ },
41
+ "typesVersions": {
42
+ "*": {
43
+ "hmr-gate": [
44
+ "./dist/hmr-gate.d.ts"
45
+ ],
46
+ "dev-server-bridge": [
47
+ "./dist/dev-server-bridge.d.ts"
48
+ ]
49
+ }
50
+ },
9
51
  "files": [
10
52
  "dist",
11
53
  "runtime"
@@ -18,8 +60,6 @@
18
60
  ],
19
61
  "license": "MIT",
20
62
  "dependencies": {
21
- "@lovable.dev/vite-plugin-dev-server-bridge": "^1.2.1",
22
- "@lovable.dev/vite-plugin-hmr-gate": "^1.6.1",
23
63
  "@tanstack/devtools-vite": "^0.8.1",
24
64
  "lightningcss": "^1.30.0"
25
65
  },