Explorar o código

fix(web): opt panel pages out of Cloudflare Rocket Loader

Behind Cloudflare with Rocket Loader enabled, the panel's entry bundles
were rewritten and executed through Rocket Loader's own loader instead
of as native ES modules (a reporter's network capture shows the main
bundle initiated by rocket-loader.min.js). That breaks module semantics
and script ordering, leaving a blank page after login even though every
asset returns 200 - most visibly with a custom URI path, where the
injected base path must be set before the bundle boots.

Stamp data-cfasync="false" - Cloudflare's documented per-script opt-out
- on the built entry script tags via a build-time transformIndexHtml
hook (Vite regenerates entry tags, so a source-HTML attribute would be
stripped), and on the runtime-injected base-path/version inline script
in serveDistPage.

Closes #5868
MHSanaei hai 1 día
pai
achega
e6bef229ae
Modificáronse 2 ficheiros con 14 adicións e 2 borrados
  1. 13 1
      frontend/vite.config.js
  2. 1 1
      internal/web/controller/dist.go

+ 13 - 1
frontend/vite.config.js

@@ -77,6 +77,18 @@ function injectBasePathPlugin() {
   };
   };
 }
 }
 
 
+// Cloudflare Rocket Loader rewrites script tags and runs bundles through its
+// own loader, breaking ES-module semantics; data-cfasync="false" opts out.
+function rocketLoaderOptOutPlugin() {
+  return {
+    name: 'xui-rocket-loader-opt-out',
+    apply: 'build',
+    transformIndexHtml(html) {
+      return html.replaceAll('<script ', '<script data-cfasync="false" ');
+    },
+  };
+}
+
 function bypassMigratedRoute(req) {
 function bypassMigratedRoute(req) {
   if (req.method !== 'GET') return undefined;
   if (req.method !== 'GET') return undefined;
   const url = req.url.split('?')[0];
   const url = req.url.split('?')[0];
@@ -140,7 +152,7 @@ function makeBackendProxy(target) {
 }
 }
 
 
 export default defineConfig({
 export default defineConfig({
-  plugins: [react(), injectBasePathPlugin()],
+  plugins: [react(), injectBasePathPlugin(), rocketLoaderOptOutPlugin()],
   resolve: {
   resolve: {
     alias: {
     alias: {
       '@': path.resolve(__dirname, 'src'),
       '@': path.resolve(__dirname, 'src'),

+ 1 - 1
internal/web/controller/dist.go

@@ -110,7 +110,7 @@ func serveDistPage(c *gin.Context, name string) {
 	if nonce := c.GetString("csp_nonce"); nonce != "" {
 	if nonce := c.GetString("csp_nonce"); nonce != "" {
 		nonceAttr = ` nonce="` + htmlpkg.EscapeString(nonce) + `"`
 		nonceAttr = ` nonce="` + htmlpkg.EscapeString(nonce) + `"`
 	}
 	}
-	script := `<script` + nonceAttr + `>window.X_UI_BASE_PATH="` + escapedBase + `"`
+	script := `<script data-cfasync="false"` + nonceAttr + `>window.X_UI_BASE_PATH="` + escapedBase + `"`
 	if name != "login.html" {
 	if name != "login.html" {
 		escapedVer := jsEscape.Replace(config.GetPanelVersion())
 		escapedVer := jsEscape.Replace(config.GetPanelVersion())
 		script += `;window.X_UI_CUR_VER="` + escapedVer + `"`
 		script += `;window.X_UI_CUR_VER="` + escapedVer + `"`