diff --git a/.env.example b/.env.example index 313024511..70ea742fb 100644 --- a/.env.example +++ b/.env.example @@ -31,6 +31,11 @@ ENABLE_PLUGINS=true # For production you should set this to match the proxy configuration (127.0.0.1) FORWARDED_ALLOW_IPS='*' +# Optional URL subpath prefix for reverse-proxy deployments (e.g. "/openwebui"). +# Must start with a leading slash and must not end with a trailing slash. +# Leave empty to serve at the root path. +WEBUI_SUBPATH="" + # DO NOT TRACK SCARF_NO_ANALYTICS=true DO_NOT_TRACK=true diff --git a/Dockerfile b/Dockerfile index 08d7a71ee..3130ad84f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,6 +29,7 @@ ARG BUILD_HASH ARG USE_SLIM ARG UID ARG GID +ARG WEBUI_SUBPATH="" # Set Node.js options (heap limit Allocation failed - JavaScript heap out of memory) # ENV NODE_OPTIONS="--max-old-space-size=4096" @@ -38,11 +39,15 @@ WORKDIR /app # to store git revision in build RUN apk add --no-cache git +# The SvelteKit/Vite production build OOMs at Node's ~2GB default heap. +ENV NODE_OPTIONS="--max-old-space-size=6144" + COPY package.json package-lock.json ./ RUN npm ci --force COPY . . ENV APP_BUILD_HASH=${BUILD_HASH} +ENV WEBUI_SUBPATH=${WEBUI_SUBPATH} RUN npm run build && \ if [ "$USE_SLIM" = "true" ]; then find build -type f -name '*.map' -delete; fi @@ -228,4 +233,9 @@ ARG BUILD_HASH ENV WEBUI_BUILD_VERSION=${BUILD_HASH} ENV DOCKER=true +# Default the backend runtime subpath to match the value the frontend was built +# with. Can still be overridden at runtime with `-e WEBUI_SUBPATH=...`. +ARG WEBUI_SUBPATH="" +ENV WEBUI_SUBPATH=${WEBUI_SUBPATH} + CMD [ "bash", "start.sh"] diff --git a/backend/open_webui/__init__.py b/backend/open_webui/__init__.py index e803cea46..ad490bb56 100644 --- a/backend/open_webui/__init__.py +++ b/backend/open_webui/__init__.py @@ -74,7 +74,11 @@ def serve( os.environ['LD_LIBRARY_PATH'] = ':'.join(LD_LIBRARY_PATH) import open_webui.main # noqa: F401 - from open_webui.env import UVICORN_WORKERS, UVICORN_WS_PER_MESSAGE_DEFLATE + from open_webui.env import ( + UVICORN_WORKERS, + UVICORN_WS_PER_MESSAGE_DEFLATE, + WEBUI_SUBPATH, + ) # Import the workers setting and web subpath # On Windows, uvicorn's default loop factory hardcodes ProactorEventLoop, # which is incompatible with psycopg v3 async. Setting loop='none' lets @@ -89,6 +93,7 @@ def serve( workers=UVICORN_WORKERS, ws_per_message_deflate=UVICORN_WS_PER_MESSAGE_DEFLATE, loop=loop, + root_path=WEBUI_SUBPATH, ) @@ -98,7 +103,7 @@ def dev( port: int = 8080, reload: bool = True, ): - from open_webui.env import UVICORN_WS_PER_MESSAGE_DEFLATE + from open_webui.env import UVICORN_WS_PER_MESSAGE_DEFLATE, WEBUI_SUBPATH uvicorn.run( 'open_webui.main:app', @@ -107,6 +112,7 @@ def dev( reload=reload, forwarded_allow_ips='*', ws_per_message_deflate=UVICORN_WS_PER_MESSAGE_DEFLATE, + root_path=WEBUI_SUBPATH, ) diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py index fe5f8efb2..542845535 100644 --- a/backend/open_webui/env.py +++ b/backend/open_webui/env.py @@ -958,6 +958,30 @@ if WEBUI_NAME != 'Open WebUI': # https://docs.openwebui.com/license. WEBUI_FAVICON_URL = 'https://openwebui.com/favicon.png' WEBUI_BUILD_HASH = os.getenv('WEBUI_BUILD_HASH', 'dev-build') +# Optional URL subpath prefix for reverse-proxy deployments (e.g. "/openwebui"). +# Leave empty to serve the app at the root path. +WEBUI_SUBPATH = os.getenv('WEBUI_SUBPATH', '') +# Surface the resolved subpath at startup so misconfigured deployments are easy +# to diagnose from the pod logs. +if WEBUI_SUBPATH == '': + log.info('WEBUI_SUBPATH is empty; serving Open WebUI at the root path "/".') +else: + log.info( + 'WEBUI_SUBPATH=%r; Open WebUI will be served under this prefix (FastAPI root_path).', + WEBUI_SUBPATH, + ) + if not WEBUI_SUBPATH.startswith('/'): + log.warning( + 'WEBUI_SUBPATH=%r does not start with "/"; a reverse-proxy subpath must look ' + 'like "/openwebui". This is almost certainly misconfigured.', + WEBUI_SUBPATH, + ) + if WEBUI_SUBPATH != '/' and WEBUI_SUBPATH.endswith('/'): + log.warning( + 'WEBUI_SUBPATH=%r ends with a trailing "/"; it should not (use "/openwebui", ' + 'not "/openwebui/"). This is almost certainly misconfigured.', + WEBUI_SUBPATH, + ) TRUSTED_SIGNATURE_KEY = os.getenv('TRUSTED_SIGNATURE_KEY', '') #################################### diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index e7c291513..a307871df 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -125,6 +125,7 @@ from open_webui.env import ( WEBUI_SECRET_KEY, WEBUI_SESSION_COOKIE_SAME_SITE, WEBUI_SESSION_COOKIE_SECURE, + WEBUI_SUBPATH, ) from open_webui.events import ( EVENTS, @@ -509,7 +510,9 @@ app = FastAPI( openapi_url='/openapi.json' if ENV == 'dev' else None, redoc_url=None, lifespan=lifespan, + root_path=WEBUI_SUBPATH, ) +log.info('FastAPI app initialized with root_path=%r (from WEBUI_SUBPATH).', app.root_path) @app.exception_handler(RecurrenceEvaluationTimeout) @@ -2312,6 +2315,7 @@ async def get_app_config(request: Request): 'status': True, 'name': app.state.WEBUI_NAME, 'version': VERSION, + 'subpath': WEBUI_SUBPATH, 'default_locale': str(DEFAULT_LOCALE), 'i18n': config.get('ui.i18n') or {}, 'oauth': { @@ -2889,7 +2893,7 @@ async def get_manifest_json(): 'name': app.state.WEBUI_NAME, 'short_name': app.state.WEBUI_NAME, 'description': f'{app.state.WEBUI_NAME} is an open, extensible, user-friendly interface for AI that adapts to your workflow.', - 'start_url': '/', + 'start_url': f'{WEBUI_SUBPATH}/', 'display': 'standalone', 'background_color': '#343541', 'icons': [ @@ -2897,20 +2901,20 @@ async def get_manifest_json(): # Do not alter, remove, obscure, or replace it except as LICENSE permits: # https://docs.openwebui.com/license. { - 'src': '/static/logo.png', + 'src': f'{WEBUI_SUBPATH}/static/logo.png', 'type': 'image/png', 'sizes': '500x500', 'purpose': 'any', }, { - 'src': '/static/logo.png', + 'src': f'{WEBUI_SUBPATH}/static/logo.png', 'type': 'image/png', 'sizes': '500x500', 'purpose': 'maskable', }, ], 'share_target': { - 'action': '/', + 'action': f'{WEBUI_SUBPATH}/', 'method': 'GET', 'params': {'text': 'shared'}, }, @@ -3055,9 +3059,9 @@ def swagger_ui_html(*args, **kwargs): return get_swagger_ui_html( *args, **kwargs, - swagger_js_url='/static/swagger-ui/swagger-ui-bundle.js', - swagger_css_url='/static/swagger-ui/swagger-ui.css', - swagger_favicon_url='/static/swagger-ui/favicon.png', + swagger_js_url=f'{WEBUI_SUBPATH}/static/swagger-ui/swagger-ui-bundle.js', + swagger_css_url=f'{WEBUI_SUBPATH}/static/swagger-ui/swagger-ui.css', + swagger_favicon_url=f'{WEBUI_SUBPATH}/static/swagger-ui/favicon.png', ) diff --git a/backend/open_webui/routers/models.py b/backend/open_webui/routers/models.py index e8426b1da..f2f699e0b 100644 --- a/backend/open_webui/routers/models.py +++ b/backend/open_webui/routers/models.py @@ -784,6 +784,10 @@ async def get_model_profile_image( bypass_access_control = BYPASS_MODEL_ACCESS_CONTROL or (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) + # Prefix static-asset redirects with the ASGI root_path so they keep the + # reverse-proxy subpath (a bare "/static/..." Location drops it → 404). + root_path = request.scope.get('root_path', '').rstrip('/') + # First, check the database for regular models model_meta = await Models.get_model_meta_by_id(id, db=db) if model_meta: @@ -838,7 +842,7 @@ async def get_model_profile_image( # Do not alter, remove, obscure, or replace it except as LICENSE permits: # https://docs.openwebui.com/license. return RedirectResponse( - url='/static/favicon.png', + url=f'{root_path}/static/favicon.png', status_code=status.HTTP_302_FOUND, ) @@ -860,7 +864,7 @@ async def get_model_profile_image( safe_static = _safe_static_redirect_path(profile_image_url) if safe_static: return RedirectResponse( - url=safe_static, + url=f'{root_path}{safe_static}', status_code=status.HTTP_302_FOUND, ) @@ -868,7 +872,7 @@ async def get_model_profile_image( # Do not alter, remove, obscure, or replace it except as LICENSE permits: # https://docs.openwebui.com/license. return RedirectResponse( - url='/static/favicon.png', + url=f'{root_path}/static/favicon.png', status_code=status.HTTP_302_FOUND, ) diff --git a/backend/open_webui/socket/main.py b/backend/open_webui/socket/main.py index 2e0969ab2..f33cc3cfa 100644 --- a/backend/open_webui/socket/main.py +++ b/backend/open_webui/socket/main.py @@ -31,6 +31,7 @@ from open_webui.env import ( WEBSOCKET_SERVER_LOGGING, WEBSOCKET_SERVER_PING_INTERVAL, WEBSOCKET_SERVER_PING_TIMEOUT, + WEBUI_SUBPATH, ) from open_webui.models.access_grants import AccessGrants from open_webui.models.channels import Channels @@ -308,9 +309,14 @@ async def periodic_usage_pool_cleanup(): await asyncio.sleep(retry_delay) +# engineio's ASGIApp matches the *full* request path (it does not subtract the +# ASGI root_path), so under a reverse-proxy subpath the browser hits +# `${WEBUI_SUBPATH}/ws/socket.io` and the handshake would otherwise 404 into a +# websocket-scope `not_found`. Prefix socketio_path with WEBUI_SUBPATH so it +# stays aligned with the frontend client (`path: ${base}/ws/socket.io`). app = socketio.ASGIApp( sio, - socketio_path='/ws/socket.io', + socketio_path=f'{WEBUI_SUBPATH}/ws/socket.io', ) diff --git a/backend/open_webui/utils/asgi_middleware.py b/backend/open_webui/utils/asgi_middleware.py index 2d9f4e190..7fcdce387 100644 --- a/backend/open_webui/utils/asgi_middleware.py +++ b/backend/open_webui/utils/asgi_middleware.py @@ -198,7 +198,8 @@ class AppHTTPMiddleware: redirect_params['q'] = text if redirect_params: - redirect_url = f'/?{urlencode(redirect_params)}' + root_path = (scope.get('root_path') or '').rstrip('/') + redirect_url = f'{root_path}/?{urlencode(redirect_params)}' response = RedirectResponse(url=redirect_url) await response(scope, receive, send) return True diff --git a/backend/tests/test_asgi_middleware.py b/backend/tests/test_asgi_middleware.py new file mode 100644 index 000000000..bd0db41ec --- /dev/null +++ b/backend/tests/test_asgi_middleware.py @@ -0,0 +1,34 @@ +from starlette.applications import Starlette +from starlette.responses import PlainTextResponse +from starlette.routing import Route +from starlette.testclient import TestClient + +from open_webui.utils.asgi_middleware import RedirectMiddleware + + +async def ok(_request): + return PlainTextResponse('ok') + + +def build_client(*, root_path: str = '') -> TestClient: + app = Starlette(routes=[Route('/', ok), Route('/watch', ok)]) + app.add_middleware(RedirectMiddleware) + return TestClient(app, root_path=root_path) + + +def test_redirect_middleware_keeps_root_path_for_watch_redirect(): + client = build_client(root_path='/openwebui') + + response = client.get('/watch?v=abc123', follow_redirects=False) + + assert response.status_code in (301, 302, 307, 308) + assert response.headers['location'] == '/openwebui/?youtube=abc123' + + +def test_redirect_middleware_uses_root_when_no_root_path_is_set(): + client = build_client() + + response = client.get('/watch?v=abc123', follow_redirects=False) + + assert response.status_code in (301, 302, 307, 308) + assert response.headers['location'] == '/?youtube=abc123' diff --git a/backend/tests/test_env.py b/backend/tests/test_env.py new file mode 100644 index 000000000..0920a357e --- /dev/null +++ b/backend/tests/test_env.py @@ -0,0 +1,11 @@ +import importlib + + +def test_env_exports_webui_subpath(monkeypatch): + monkeypatch.setenv('WEBUI_SECRET_KEY', 'test-secret') + monkeypatch.setenv('WEBUI_SUBPATH', '/openwebui') + + env_module = importlib.import_module('open_webui.env') + env_module = importlib.reload(env_module) + + assert env_module.WEBUI_SUBPATH == '/openwebui' diff --git a/src/app.html b/src/app.html index 667808e01..b711d8aa8 100644 --- a/src/app.html +++ b/src/app.html @@ -6,37 +6,51 @@ visual, textual, symbolic identifiers, metadata, and surrounding UI. Do not alter, remove, obscure, or replace it except as LICENSE permits: https://docs.openwebui.com/license. --> - + + - - + + - - + + + +
+
+ {$i18n.t('Model Usage')} + {totalMessages} {$i18n.t('messages')} +
+
+ +
+ {#if loading} +
+ +
+ {/if} + + {#if !modelStats.length && !loading} +
{$i18n.t('No data found')}
+ {:else if modelStats.length} + + + + + + + + + + + {#each sortedModels as model, idx (model.model_id)} + + + + + + + {/each} + +
# toggleSort('name')} + > +
+ {$i18n.t('Model')} + {#if orderBy === 'name'} + {#if direction === 'asc'}{:else}{/if} + {:else} + + {/if} +
+
toggleSort('count')} + > +
+ {$i18n.t('Messages')} + {#if orderBy === 'count'} + {#if direction === 'asc'}{:else}{/if} + {:else} + + {/if} +
+
toggleSort('percentage')} + > +
+ {$i18n.t('Share')} + {#if orderBy === 'percentage'} + {#if direction === 'asc'}{:else}{/if} + {:else} + + {/if} +
+
+ {idx + 1} + +
+ {model.name} { + e.target.src = `${base}/static/favicon.png`; + }} + /> + {model.name} +
+
+ {model.count.toLocaleString()} + + {((model.count / totalMessages) * 100).toFixed(1)}% +
+ {/if} +
+ +
+
+ ⓘ {$i18n.t('Message counts are based on assistant responses.')} +
+
diff --git a/src/lib/components/admin/Evaluations.svelte b/src/lib/components/admin/Evaluations.svelte index 5a7bb7965..d2112a708 100644 --- a/src/lib/components/admin/Evaluations.svelte +++ b/src/lib/components/admin/Evaluations.svelte @@ -1,5 +1,7 @@ diff --git a/src/lib/components/channel/Thread.svelte b/src/lib/components/channel/Thread.svelte index 454c0ea89..2d5157d12 100644 --- a/src/lib/components/channel/Thread.svelte +++ b/src/lib/components/channel/Thread.svelte @@ -1,4 +1,5 @@ diff --git a/src/routes/(app)/admin/analytics/+page.svelte b/src/routes/(app)/admin/analytics/+page.svelte index 302f4c426..2bda342d2 100644 --- a/src/routes/(app)/admin/analytics/+page.svelte +++ b/src/routes/(app)/admin/analytics/+page.svelte @@ -1,13 +1,14 @@ diff --git a/src/routes/(app)/admin/analytics/[tab]/+page.svelte b/src/routes/(app)/admin/analytics/[tab]/+page.svelte index 302f4c426..2bda342d2 100644 --- a/src/routes/(app)/admin/analytics/[tab]/+page.svelte +++ b/src/routes/(app)/admin/analytics/[tab]/+page.svelte @@ -1,13 +1,14 @@ diff --git a/src/routes/(app)/admin/evaluations/+page.svelte b/src/routes/(app)/admin/evaluations/+page.svelte index 8913071b7..bd205f5e1 100644 --- a/src/routes/(app)/admin/evaluations/+page.svelte +++ b/src/routes/(app)/admin/evaluations/+page.svelte @@ -1,8 +1,9 @@ diff --git a/src/routes/(app)/admin/functions/create/+page.svelte b/src/routes/(app)/admin/functions/create/+page.svelte index 553d67dce..aee34328e 100644 --- a/src/routes/(app)/admin/functions/create/+page.svelte +++ b/src/routes/(app)/admin/functions/create/+page.svelte @@ -1,4 +1,5 @@ diff --git a/src/routes/(app)/admin/users/+page.svelte b/src/routes/(app)/admin/users/+page.svelte index ebf29a7ab..bc56c4f47 100644 --- a/src/routes/(app)/admin/users/+page.svelte +++ b/src/routes/(app)/admin/users/+page.svelte @@ -1,8 +1,9 @@ diff --git a/src/routes/(app)/automations/+page.svelte b/src/routes/(app)/automations/+page.svelte index 9dd9ffbdb..bbe93ec75 100644 --- a/src/routes/(app)/automations/+page.svelte +++ b/src/routes/(app)/automations/+page.svelte @@ -1,4 +1,5 @@ diff --git a/src/routes/(app)/calendar/+page.svelte b/src/routes/(app)/calendar/+page.svelte index b057c3a77..44ff634e4 100644 --- a/src/routes/(app)/calendar/+page.svelte +++ b/src/routes/(app)/calendar/+page.svelte @@ -2,6 +2,7 @@ import { onMount, getContext, tick } from 'svelte'; import { toast } from 'svelte-sonner'; import { goto } from '$app/navigation'; + import { base } from '$app/paths'; import { WEBUI_NAME, mobile, showSidebar, user } from '$lib/stores'; import { getCalendars, @@ -138,9 +139,9 @@ const evt = e.detail; if (evt.meta?.automation_id) { if (evt.meta?.chat_id) { - goto(`/c/${evt.meta.chat_id}`); + goto(`${base}/c/${evt.meta.chat_id}`); } else { - goto(`/automations/${evt.meta.automation_id}`); + goto(`${base}/automations/${evt.meta.automation_id}`); } return; } diff --git a/src/routes/(app)/home/+layout.svelte b/src/routes/(app)/home/+layout.svelte index 311a54900..6792dda19 100644 --- a/src/routes/(app)/home/+layout.svelte +++ b/src/routes/(app)/home/+layout.svelte @@ -1,4 +1,5 @@ diff --git a/src/routes/(app)/playground/+layout.svelte b/src/routes/(app)/playground/+layout.svelte index 4550ebe88..183fe148d 100644 --- a/src/routes/(app)/playground/+layout.svelte +++ b/src/routes/(app)/playground/+layout.svelte @@ -1,4 +1,5 @@ diff --git a/src/routes/(app)/workspace/functions/create/+page.svelte b/src/routes/(app)/workspace/functions/create/+page.svelte index 932d28804..d3da353d9 100644 --- a/src/routes/(app)/workspace/functions/create/+page.svelte +++ b/src/routes/(app)/workspace/functions/create/+page.svelte @@ -1,8 +1,9 @@ diff --git a/src/routes/(app)/workspace/knowledge/create/+page.svelte b/src/routes/(app)/workspace/knowledge/create/+page.svelte index b5e1b33ef..d93a9b8c5 100644 --- a/src/routes/(app)/workspace/knowledge/create/+page.svelte +++ b/src/routes/(app)/workspace/knowledge/create/+page.svelte @@ -1,5 +1,6 @@ - + diff --git a/src/routes/(app)/workspace/models/create/+page.svelte b/src/routes/(app)/workspace/models/create/+page.svelte index 08fae9b00..dc88737c7 100644 --- a/src/routes/(app)/workspace/models/create/+page.svelte +++ b/src/routes/(app)/workspace/models/create/+page.svelte @@ -1,4 +1,5 @@ diff --git a/src/routes/(app)/workspace/prompts/create/+page.svelte b/src/routes/(app)/workspace/prompts/create/+page.svelte index c6c80aed7..9a6c400ba 100644 --- a/src/routes/(app)/workspace/prompts/create/+page.svelte +++ b/src/routes/(app)/workspace/prompts/create/+page.svelte @@ -1,5 +1,6 @@ - + diff --git a/src/routes/(app)/workspace/skills/create/+page.svelte b/src/routes/(app)/workspace/skills/create/+page.svelte index 3985cea95..7e05ad669 100644 --- a/src/routes/(app)/workspace/skills/create/+page.svelte +++ b/src/routes/(app)/workspace/skills/create/+page.svelte @@ -1,4 +1,5 @@ diff --git a/src/routes/(app)/workspace/tools/create/+page.svelte b/src/routes/(app)/workspace/tools/create/+page.svelte index 8bcc66ce0..b58effed7 100644 --- a/src/routes/(app)/workspace/tools/create/+page.svelte +++ b/src/routes/(app)/workspace/tools/create/+page.svelte @@ -1,4 +1,5 @@ diff --git a/src/routes/watch/+page.svelte b/src/routes/watch/+page.svelte index 78eab5be3..b727a8c1a 100644 --- a/src/routes/watch/+page.svelte +++ b/src/routes/watch/+page.svelte @@ -1,4 +1,5 @@ diff --git a/svelte.config.js b/svelte.config.js index ffb774e72..4a4b3570a 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -3,6 +3,11 @@ import * as child_process from 'node:child_process'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; import fs from 'node:fs'; +// Optional URL subpath prefix for reverse-proxy deployments (e.g. "/openwebui"). +// Must start with a leading slash and must not end with a trailing slash. +// Leave empty to serve at the root path. +const WEBUI_BASE_PATH = process.env.WEBUI_SUBPATH || ''; + /** @type {import('@sveltejs/kit').Config} */ const config = { // Consult https://kit.svelte.dev/docs/integrations#preprocessors @@ -17,6 +22,16 @@ const config = { assets: 'build', fallback: 'index.html' }), + paths: { + base: WEBUI_BASE_PATH, + // Bake absolute, base-prefixed asset URLs instead of runtime-relative ones. + // With the default (relative: true) SvelteKit derives `base` from the + // document URL and emits relative `%sveltekit.assets%` paths, so static + // refs in app.html (e.g. /static/favicon.png) and nested-route asset loads + // drop the subpath. A fixed reverse-proxy subpath is always served from + // `base`, so absolute paths are correct and route-independent. + relative: false + }, // poll for new version name every 60 seconds (to trigger reload mechanic in +layout.svelte) version: { name: (() => { diff --git a/vite.config.ts b/vite.config.ts index 631ce5972..aa6d25889 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,6 +5,11 @@ import { viteStaticCopy } from 'vite-plugin-static-copy'; const backendTarget = process.env.WEBUI_BACKEND_URL || 'http://localhost:8080'; +// Optional URL subpath prefix for reverse-proxy deployments (e.g. "/openwebui"). +// Must start with a leading slash and must not end with a trailing slash. +// Leave empty to serve at the root path. +const WEBUI_BASE_PATH = process.env.WEBUI_SUBPATH || ''; + export default defineConfig({ resolve: { conditions: ['onnxruntime-web-use-extern-wasm'] @@ -25,6 +30,7 @@ export default defineConfig({ APP_VERSION: JSON.stringify(process.env.npm_package_version), APP_BUILD_HASH: JSON.stringify(process.env.APP_BUILD_HASH || 'dev-build') }, + base: WEBUI_BASE_PATH || '/', build: { sourcemap: true },