mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-25 04:54:08 -03:00
feat: support reverse-proxy URL subpaths (llama-swap, SwarmUI) (#1122)
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import { describe, it, expect, afterEach } from 'vitest';
|
||||
|
||||
import { getBasePath, withBasePath } from '../../../static/js/utils/basePath.js';
|
||||
|
||||
describe('static/js/utils/basePath.js', () => {
|
||||
afterEach(() => {
|
||||
delete window.LM_BASE_PATH;
|
||||
});
|
||||
|
||||
it('returns empty base path when bootstrap did not set one', () => {
|
||||
expect(getBasePath()).toBe('');
|
||||
expect(withBasePath('/loras')).toBe('/loras');
|
||||
});
|
||||
|
||||
it('prepends the detected base path', () => {
|
||||
window.LM_BASE_PATH = '/comfyui';
|
||||
expect(getBasePath()).toBe('/comfyui');
|
||||
expect(withBasePath('/loras')).toBe('/comfyui/loras');
|
||||
expect(withBasePath('/loras/recipes')).toBe('/comfyui/loras/recipes');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,217 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
|
||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
|
||||
|
||||
const extractBootstrapScript = () => {
|
||||
const html = readFileSync(
|
||||
resolve(repoRoot, 'templates/components/base_path_bootstrap.html'),
|
||||
'utf8',
|
||||
);
|
||||
const match = html.match(/<script>([\s\S]*?)<\/script>/);
|
||||
if (!match) {
|
||||
throw new Error('bootstrap <script> block not found');
|
||||
}
|
||||
return match[1];
|
||||
};
|
||||
|
||||
const PATCHED_PROTOS = () => [
|
||||
[Element.prototype, ['innerHTML', 'outerHTML']],
|
||||
[HTMLImageElement.prototype, ['src']],
|
||||
[HTMLMediaElement.prototype, ['src']],
|
||||
[HTMLSourceElement.prototype, ['src']],
|
||||
[HTMLVideoElement.prototype, ['poster']],
|
||||
[HTMLAnchorElement.prototype, ['href']],
|
||||
[HTMLScriptElement.prototype, ['src']],
|
||||
[HTMLLinkElement.prototype, ['href']],
|
||||
];
|
||||
|
||||
describe('base_path_bootstrap.html', () => {
|
||||
let savedGlobals;
|
||||
let savedDescriptors;
|
||||
let savedMethods;
|
||||
|
||||
beforeEach(() => {
|
||||
savedGlobals = {
|
||||
fetch: window.fetch,
|
||||
WebSocket: window.WebSocket,
|
||||
};
|
||||
savedDescriptors = PATCHED_PROTOS().flatMap(([proto, props]) =>
|
||||
props.map((prop) => [proto, prop, Object.getOwnPropertyDescriptor(proto, prop)]),
|
||||
);
|
||||
savedMethods = {
|
||||
xhrOpen: XMLHttpRequest.prototype.open,
|
||||
insertAdjacentHTML: Element.prototype.insertAdjacentHTML,
|
||||
setAttribute: Element.prototype.setAttribute,
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.fetch = savedGlobals.fetch;
|
||||
window.WebSocket = savedGlobals.WebSocket;
|
||||
for (const [proto, prop, desc] of savedDescriptors) {
|
||||
if (desc) {
|
||||
Object.defineProperty(proto, prop, desc);
|
||||
}
|
||||
}
|
||||
XMLHttpRequest.prototype.open = savedMethods.xhrOpen;
|
||||
Element.prototype.insertAdjacentHTML = savedMethods.insertAdjacentHTML;
|
||||
Element.prototype.setAttribute = savedMethods.setAttribute;
|
||||
delete window.LM_BASE_PATH;
|
||||
delete window.lmWithBasePath;
|
||||
window.history.replaceState({}, '', '/');
|
||||
});
|
||||
|
||||
const runBootstrap = (pathname) => {
|
||||
window.history.replaceState({}, '', pathname);
|
||||
(0, eval)(extractBootstrapScript());
|
||||
};
|
||||
|
||||
it('detects no prefix for root-mounted pages and patches nothing', () => {
|
||||
const nativeFetch = window.fetch;
|
||||
runBootstrap('/loras');
|
||||
expect(window.LM_BASE_PATH).toBe('');
|
||||
expect(window.fetch).toBe(nativeFetch);
|
||||
|
||||
runBootstrap('/');
|
||||
expect(window.LM_BASE_PATH).toBe('');
|
||||
});
|
||||
|
||||
it.each([
|
||||
['/comfyui/loras', '/comfyui'],
|
||||
['/comfyui/loras/', '/comfyui'],
|
||||
['/comfyui/loras/recipes', '/comfyui'],
|
||||
['/comfyui/checkpoints', '/comfyui'],
|
||||
['/comfyui/embeddings', '/comfyui'],
|
||||
['/comfyui/other', '/comfyui'],
|
||||
['/comfyui/statistics', '/comfyui'],
|
||||
['/ComfyBackendDirect/loras', '/ComfyBackendDirect'],
|
||||
['/proxy/nested/loras', '/proxy/nested'],
|
||||
])('detects prefix for %s', (pathname, expected) => {
|
||||
runBootstrap(pathname);
|
||||
expect(window.LM_BASE_PATH).toBe(expected);
|
||||
});
|
||||
|
||||
it('does not mistake similar paths for manager pages', () => {
|
||||
runBootstrap('/comfyui/lorasgallery');
|
||||
expect(window.LM_BASE_PATH).toBe('');
|
||||
runBootstrap('/comfyui/foo-loras');
|
||||
expect(window.LM_BASE_PATH).toBe('');
|
||||
});
|
||||
|
||||
it('prefixes root-absolute fetch URLs only', async () => {
|
||||
const fetchSpy = vi.fn().mockResolvedValue({ ok: true });
|
||||
window.fetch = fetchSpy;
|
||||
runBootstrap('/comfyui/loras');
|
||||
|
||||
await window.fetch('/api/lm/loras/list');
|
||||
await window.fetch('/loras_static/images/no-preview.png');
|
||||
await window.fetch('https://civitai.com/api/v1/models');
|
||||
await window.fetch('//cdn.example.com/x.js');
|
||||
await window.fetch('relative/path');
|
||||
|
||||
expect(fetchSpy.mock.calls.map((call) => call[0])).toEqual([
|
||||
'/comfyui/api/lm/loras/list',
|
||||
'/comfyui/loras_static/images/no-preview.png',
|
||||
'https://civitai.com/api/v1/models',
|
||||
'//cdn.example.com/x.js',
|
||||
'relative/path',
|
||||
]);
|
||||
});
|
||||
|
||||
it('prefixes same-origin absolute fetch URLs', async () => {
|
||||
const fetchSpy = vi.fn().mockResolvedValue({ ok: true });
|
||||
window.fetch = fetchSpy;
|
||||
runBootstrap('/comfyui/loras');
|
||||
|
||||
const absolute = `${window.location.origin}/api/lm/init-status`;
|
||||
await window.fetch(absolute);
|
||||
expect(fetchSpy).toHaveBeenCalledWith(
|
||||
`${window.location.origin}/comfyui/api/lm/init-status`,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('prefixes fetch() called with a URL object', async () => {
|
||||
const fetchSpy = vi.fn().mockResolvedValue({ ok: true });
|
||||
window.fetch = fetchSpy;
|
||||
runBootstrap('/comfyui/loras');
|
||||
|
||||
await window.fetch(new URL('/api/lm/base-models', window.location.origin));
|
||||
expect(fetchSpy).toHaveBeenCalledWith(
|
||||
`${window.location.origin}/comfyui/api/lm/base-models`,
|
||||
undefined,
|
||||
);
|
||||
|
||||
await window.fetch(new URL('https://civitai.com/api/v1/models'));
|
||||
expect(fetchSpy).toHaveBeenLastCalledWith('https://civitai.com/api/v1/models', undefined);
|
||||
});
|
||||
|
||||
it('prefixes WebSocket URLs', () => {
|
||||
const constructed = [];
|
||||
class FakeWebSocket {
|
||||
constructor(url, protocols) {
|
||||
constructed.push([url, protocols]);
|
||||
}
|
||||
}
|
||||
FakeWebSocket.CONNECTING = 0;
|
||||
FakeWebSocket.OPEN = 1;
|
||||
FakeWebSocket.CLOSING = 2;
|
||||
FakeWebSocket.CLOSED = 3;
|
||||
window.WebSocket = FakeWebSocket;
|
||||
|
||||
runBootstrap('/comfyui/loras');
|
||||
|
||||
new window.WebSocket('/ws/fetch-progress');
|
||||
new window.WebSocket('wss://other.example.com/socket', ['a']);
|
||||
expect(constructed).toEqual([
|
||||
['/comfyui/ws/fetch-progress', undefined],
|
||||
['wss://other.example.com/socket', ['a']],
|
||||
]);
|
||||
});
|
||||
|
||||
it('rewrites root-absolute URLs inside innerHTML markup', () => {
|
||||
runBootstrap('/comfyui/loras');
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = `<img src="/api/lm/previews?path=x" onerror="this.src='/loras_static/images/no-preview.png'">`
|
||||
+ `<a href="/api/lm/download-model/1">dl</a>`
|
||||
+ `<video poster="/loras_static/p.png"><source src="/example_images_static/a/b.mp4"></video>`;
|
||||
|
||||
const html = container.innerHTML;
|
||||
expect(html).toContain('src="/comfyui/api/lm/previews?path=x"');
|
||||
expect(html).toContain("this.src='/comfyui/loras_static/images/no-preview.png'");
|
||||
expect(html).toContain('href="/comfyui/api/lm/download-model/1"');
|
||||
expect(html).toContain('poster="/comfyui/loras_static/p.png"');
|
||||
expect(html).toContain('src="/comfyui/example_images_static/a/b.mp4"');
|
||||
});
|
||||
|
||||
it('does not double-prefix markup that already carries the prefix', () => {
|
||||
runBootstrap('/comfyui/loras');
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<img src="/api/lm/previews?path=x">';
|
||||
const once = container.innerHTML;
|
||||
container.innerHTML = once;
|
||||
expect(container.innerHTML).toBe(once);
|
||||
expect(once).not.toContain('/comfyui/comfyui/');
|
||||
});
|
||||
|
||||
it('prefixes direct DOM URL assignments', () => {
|
||||
runBootstrap('/comfyui/loras');
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = '/loras_static/images/no-preview.png';
|
||||
expect(img.getAttribute('src')).toBe('/comfyui/loras_static/images/no-preview.png');
|
||||
|
||||
const anchor = document.createElement('a');
|
||||
anchor.setAttribute('href', '/api/lm/download-model/1');
|
||||
expect(anchor.getAttribute('href')).toBe('/comfyui/api/lm/download-model/1');
|
||||
|
||||
const video = document.createElement('video');
|
||||
video.poster = '/loras_static/p.png';
|
||||
expect(video.getAttribute('poster')).toBe('/comfyui/loras_static/p.png');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { describe, it, expect, afterEach } from 'vitest';
|
||||
|
||||
import { getComfyUIBasePath, lmUrl } from '../../../web/comfyui/base_path.js';
|
||||
|
||||
describe('web/comfyui/base_path.js', () => {
|
||||
afterEach(() => {
|
||||
window.history.replaceState({}, '', '/');
|
||||
});
|
||||
|
||||
it.each([
|
||||
['/', ''],
|
||||
['/comfyui/', '/comfyui'],
|
||||
['/comfyui', '/comfyui'],
|
||||
['/ComfyBackendDirect/', '/ComfyBackendDirect'],
|
||||
])('maps %s to base path %s', (pathname, expected) => {
|
||||
window.history.replaceState({}, '', pathname);
|
||||
expect(getComfyUIBasePath()).toBe(expected);
|
||||
});
|
||||
|
||||
it('builds prefixed URLs', () => {
|
||||
window.history.replaceState({}, '', '/comfyui/');
|
||||
expect(lmUrl('/api/lm/version-info')).toBe('/comfyui/api/lm/version-info');
|
||||
expect(lmUrl('/loras')).toBe('/comfyui/loras');
|
||||
|
||||
window.history.replaceState({}, '', '/');
|
||||
expect(lmUrl('/api/lm/version-info')).toBe('/api/lm/version-info');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user