From 4da856fb6d0d48e3b46b6e8a18816593054634db Mon Sep 17 00:00:00 2001 From: InfinityPacer <160988576+InfinityPacer@users.noreply.github.com> Date: Mon, 20 Jul 2026 15:05:58 +0800 Subject: [PATCH] fix(plugin): resolve remote entry URLs from document base (#566) --- src/utils/__tests__/federationUrl.spec.ts | 47 +++++++++++++++++++++++ src/utils/federationLoader.ts | 14 +------ src/utils/federationUrl.ts | 11 ++++++ 3 files changed, 60 insertions(+), 12 deletions(-) create mode 100644 src/utils/__tests__/federationUrl.spec.ts create mode 100644 src/utils/federationUrl.ts diff --git a/src/utils/__tests__/federationUrl.spec.ts b/src/utils/__tests__/federationUrl.spec.ts new file mode 100644 index 00000000..848ed5a7 --- /dev/null +++ b/src/utils/__tests__/federationUrl.spec.ts @@ -0,0 +1,47 @@ +import { resolveFederationRemoteUrl } from '@/utils/federationUrl' +import { describe, expect, it } from 'vitest' + +describe('resolveFederationRemoteUrl', () => { + const remotePath = '/plugin/file/example/frontend/dist/assets/remoteEntry.js' + + it.each([ + { + name: 'Docker 根路径 HTTP 入口', + apiBaseUrl: 'api/v1/', + documentBaseUri: 'http://moviepilot.local/', + expected: 'http://moviepilot.local/api/v1/plugin/file/example/frontend/dist/assets/remoteEntry.js', + }, + { + name: 'Docker 或 local_setup 的无尾斜杠 fallback 入口', + apiBaseUrl: 'api/v1/', + documentBaseUri: 'http://moviepilot.local/dashboard', + expected: 'http://moviepilot.local/api/v1/plugin/file/example/frontend/dist/assets/remoteEntry.js', + }, + { + name: 'HTTPS 反向代理入口', + apiBaseUrl: 'api/v1/', + documentBaseUri: 'https://moviepilot.example.com/dashboard', + expected: 'https://moviepilot.example.com/api/v1/plugin/file/example/frontend/dist/assets/remoteEntry.js', + }, + { + name: '反向代理子路径入口', + apiBaseUrl: 'api/v1/', + documentBaseUri: 'https://moviepilot.example.com/mp/', + expected: 'https://moviepilot.example.com/mp/api/v1/plugin/file/example/frontend/dist/assets/remoteEntry.js', + }, + { + name: '根路径 API 配置', + apiBaseUrl: '/api/v1/', + documentBaseUri: 'https://moviepilot.example.com/mp/', + expected: 'https://moviepilot.example.com/api/v1/plugin/file/example/frontend/dist/assets/remoteEntry.js', + }, + { + name: '完整跨域 API 配置', + apiBaseUrl: 'https://api.example.com/api/v1', + documentBaseUri: 'http://moviepilot.local/dashboard', + expected: 'https://api.example.com/api/v1/plugin/file/example/frontend/dist/assets/remoteEntry.js', + }, + ])('$name', ({ apiBaseUrl, documentBaseUri, expected }) => { + expect(resolveFederationRemoteUrl(remotePath, apiBaseUrl, documentBaseUri)).toBe(expected) + }) +}) diff --git a/src/utils/federationLoader.ts b/src/utils/federationLoader.ts index 4b59543b..05e03443 100644 --- a/src/utils/federationLoader.ts +++ b/src/utils/federationLoader.ts @@ -1,4 +1,5 @@ import api from '@/api' +import { resolveFederationRemoteUrl } from '@/utils/federationUrl' import { __federation_method_setRemote, __federation_method_getRemote, @@ -144,18 +145,7 @@ async function fetchRemoteModules(): Promise { * @param modules 远程模块列表 */ export function injectRemoteModule(module: RemoteModule): void { - // 与 API 请求一致:使用 origin + pathname 作为前缀,子路径代理时 pathname 含 /mp 等 - const baseUrl = new URL(window.location.href) - const pathBase = baseUrl.pathname.replace(/\/$/, '') || '' - let apiBase = import.meta.env.VITE_API_BASE_URL - if (apiBase.startsWith('/')) { - apiBase = apiBase.slice(1) - } - if (apiBase.endsWith('/')) { - apiBase = apiBase.slice(0, -1) - } - const pathWithoutLeadingSlash = module.url.startsWith('/') ? module.url.slice(1) : module.url - const remoteEntryUrl = `${baseUrl.origin}${pathBase}/${apiBase}/${pathWithoutLeadingSlash}` + const remoteEntryUrl = resolveFederationRemoteUrl(module.url, import.meta.env.VITE_API_BASE_URL, document.baseURI) __federation_method_setRemote(module.id, { url: () => Promise.resolve(remoteEntryUrl), format: 'esm', diff --git a/src/utils/federationUrl.ts b/src/utils/federationUrl.ts new file mode 100644 index 00000000..d74a20e9 --- /dev/null +++ b/src/utils/federationUrl.ts @@ -0,0 +1,11 @@ +/** + * 按页面文档基址解析插件联邦入口,使其与相对 API 请求遵循相同的部署路径规则。 + * 相对 API 基址保留反向代理子路径,根路径或完整 URL 配置则按其显式语义解析。 + */ +export function resolveFederationRemoteUrl(remotePath: string, apiBaseUrl: string, documentBaseUri: string): string { + const normalizedApiBase = apiBaseUrl.endsWith('/') ? apiBaseUrl : `${apiBaseUrl}/` + const apiUrl = new URL(normalizedApiBase, documentBaseUri) + const normalizedRemotePath = remotePath.replace(/^\/+/, '') + + return new URL(normalizedRemotePath, apiUrl).href +}