Skip to content

Commit 486a70a

Browse files
committed
Merge branch 'main' into release/reflex-0.8.0
2 parents 81f1cbf + 80942f6 commit 486a70a

File tree

3 files changed

+164
-2
lines changed

3 files changed

+164
-2
lines changed
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/* vite-plugin-safari-cachebust.js
2+
*
3+
* Rewrite modulepreload <link> tags and ESM imports to include a cache-busting
4+
* query parameter for Safari browser.
5+
*
6+
* https://github.com/remix-run/react-router/issues/12761
7+
*
8+
* The issue seems to be Safari over-aggressive caching of ESM imports (and modulepreload)
9+
* which does not respect the cache-control headers sent by the server. This approach
10+
* allows hot reload to work in Safari when adding routes or changing dependencies.
11+
*
12+
* No equivalent transformation is needed for production builds, as the
13+
* output already contains the file hash in the name.
14+
*/
15+
16+
/**
17+
* @typedef {import('vite').Plugin} Plugin
18+
* @typedef {import('vite').ViteDevServer} ViteDevServer
19+
* @typedef {import('http').IncomingMessage} IncomingMessage
20+
* @typedef {import('http').ServerResponse} ServerResponse
21+
* @typedef {import('connect').NextHandleFunction} NextHandleFunction
22+
*/
23+
24+
const pluginName = "vite-plugin-safari-cachebust";
25+
26+
/**
27+
* Creates a Vite plugin that adds cache-busting for Safari browsers
28+
* @returns {Plugin} The Vite plugin
29+
*/
30+
export default function safariCacheBustPlugin() {
31+
return {
32+
name: pluginName,
33+
/**
34+
* Configure the dev server with the Safari middleware
35+
* @param {ViteDevServer} server - The Vite dev server instance
36+
*/
37+
configureServer(server) {
38+
server.middlewares.use(createSafariMiddleware());
39+
},
40+
};
41+
}
42+
43+
/**
44+
* Determines if the user agent is Safari
45+
* @param {string} ua - The user agent string
46+
* @returns {boolean} True if the browser is Safari
47+
*/
48+
function isSafari(ua) {
49+
return /Safari/.test(ua) && !/Chrome/.test(ua);
50+
}
51+
52+
/**
53+
* Creates a middleware that adds cache-busting for Safari browsers
54+
* @returns {NextHandleFunction} The middleware function
55+
*/
56+
function createSafariMiddleware() {
57+
// Set when a log message for rewriting n links has been emitted.
58+
let _have_logged_n = -1;
59+
60+
/**
61+
* Rewrites module import links in HTML content with cache-busting parameters
62+
* @param {string} html - The HTML content to process
63+
* @returns {string} The processed HTML content
64+
*/
65+
function rewriteModuleImports(html) {
66+
const currentTimestamp = new Date().getTime();
67+
const parts = html.split(/(<link\s+rel="modulepreload"[^>]*>)/g);
68+
/** @type {[string, string][]} */
69+
const replacements = parts
70+
.map((chunk) => {
71+
const match = chunk.match(
72+
/<link\s+rel="modulepreload"\s+href="([^"]+)"(.*?)\/?>/,
73+
);
74+
if (!match) return;
75+
76+
const [fullMatch, href, rest] = match;
77+
if (/^(https?:)?\/\//.test(href)) return;
78+
79+
try {
80+
const newHref = href.includes("?")
81+
? `${href}&__reflex_ts=${currentTimestamp}`
82+
: `${href}?__reflex_ts=${currentTimestamp}`;
83+
return [href, newHref];
84+
} catch {
85+
// no worries;
86+
}
87+
})
88+
.filter(Boolean);
89+
if (replacements.length && _have_logged_n !== replacements.length) {
90+
_have_logged_n = replacements.length;
91+
console.debug(
92+
`[${pluginName}] Rewrote ${replacements.length} modulepreload links with __reflex_ts param.`,
93+
);
94+
}
95+
return replacements.reduce((accumulator, [target, replacement]) => {
96+
return accumulator.split(target).join(replacement);
97+
}, html);
98+
}
99+
100+
/**
101+
* Middleware function to handle Safari cache busting
102+
* @param {IncomingMessage} req - The incoming request
103+
* @param {ServerResponse} res - The server response
104+
* @param {(err?: any) => void} next - The next middleware function
105+
* @returns {void}
106+
*/
107+
return function safariCacheBustMiddleware(req, res, next) {
108+
const ua = req.headers["user-agent"] || "";
109+
// Remove our special cache bust query param to avoid affecting lower middleware layers.
110+
if (
111+
req.url &&
112+
(req.url.includes("?__reflex_ts=") || req.url.includes("&__reflex_ts="))
113+
) {
114+
req.url = req.url.replace(/(\?|&)__reflex_ts=\d+/, "");
115+
return next();
116+
}
117+
118+
// Only apply this middleware for Safari browsers.
119+
if (!isSafari(ua)) return next();
120+
121+
// Only transform requests that want HTML.
122+
const header_accept = req.headers["accept"] || "";
123+
if (
124+
typeof header_accept !== "string" ||
125+
!header_accept.includes("text/html")
126+
) {
127+
return next();
128+
}
129+
130+
let buffer = "";
131+
const _end = res.end.bind(res);
132+
133+
res.setHeader("x-modified-by", "vite-plugin-safari-cachebust");
134+
/**
135+
* Overridden write method to collect chunks
136+
* @param {any} chunk - The chunk to write
137+
* @param {...any} args - Additional arguments
138+
* @returns {boolean} Result of the write operation
139+
*/
140+
res.write = function (chunk, ...args) {
141+
buffer += chunk instanceof Buffer ? chunk.toString("utf-8") : chunk;
142+
return true;
143+
};
144+
145+
/**
146+
* Overridden end method to process and send the final response
147+
* @param {any} chunk - The final chunk to write
148+
* @param {...any} args - Additional arguments
149+
* @returns {ServerResponse<IncomingMessage>} The server response
150+
*/
151+
res.end = function (chunk, ...args) {
152+
if (chunk) {
153+
buffer += chunk instanceof Buffer ? chunk.toString("utf-8") : chunk;
154+
}
155+
buffer = rewriteModuleImports(buffer);
156+
return _end(buffer, ...args);
157+
};
158+
return next();
159+
};
160+
}

reflex/.templates/web/vite.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { fileURLToPath, URL } from "url";
22
import { reactRouter } from "@react-router/dev/vite";
33
import { defineConfig } from "vite";
4+
import safariCacheBustPlugin from "./vite-plugin-safari-cachebust";
45

56
export default defineConfig((config) => ({
6-
plugins: [reactRouter()],
7+
plugins: [reactRouter(), safariCacheBustPlugin()],
78
build: {
89
rollupOptions: {
910
jsx: {},

reflex/utils/processes.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from typing import Any, Literal, overload
1616

1717
import click
18+
import rich.markup
1819
from redis.exceptions import RedisError
1920
from rich.progress import Progress
2021

@@ -281,7 +282,7 @@ def stream_logs(
281282
return
282283
try:
283284
for line in process.stdout:
284-
console.debug(line, end="", progress=progress)
285+
console.debug(rich.markup.escape(line), end="", progress=progress)
285286
logs.append(line)
286287
yield line
287288
except ValueError:

0 commit comments

Comments
 (0)