mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-10-12 02:16:43 +00:00
feat(frontend): make webv2 the default through call sites
Select webv2 for launches, frontend tooling, wheels, containers and dependency installation while preserving both source paths. Retain explicit legacy fallback, shared API artifact ownership and deployment-scoped browser worker cleanup.
This commit is contained in:
@@ -28,3 +28,4 @@
|
||||
/invokeai/frontend/merge @lstein @blessedcoolant
|
||||
/invokeai/frontend/training @lstein @blessedcoolant
|
||||
/invokeai/frontend/web @blessedcoolant @lstein @dunkeroni @Pfannkuchensack @jpphoto
|
||||
/invokeai/frontend/webv2 @blessedcoolant @lstein @dunkeroni @Pfannkuchensack @jpphoto
|
||||
|
||||
@@ -4,14 +4,14 @@ inputs:
|
||||
working-directory:
|
||||
description: Directory to install dependencies in
|
||||
required: false
|
||||
default: invokeai/frontend/web
|
||||
default: invokeai/frontend/webv2
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: setup node 22
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22'
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: setup pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
|
||||
@@ -11,6 +11,7 @@ python:
|
||||
- all-globs-to-any-file:
|
||||
- 'invokeai/**'
|
||||
- '!invokeai/frontend/web/**'
|
||||
- '!invokeai/frontend/webv2/**'
|
||||
|
||||
python-tests:
|
||||
- changed-files:
|
||||
@@ -56,4 +57,6 @@ frontend-deps:
|
||||
|
||||
frontend:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: 'invokeai/frontend/web/**'
|
||||
- any-glob-to-any-file:
|
||||
- 'invokeai/frontend/web/**'
|
||||
- 'invokeai/frontend/webv2/**'
|
||||
|
||||
@@ -62,12 +62,15 @@ jobs:
|
||||
files_yaml: |
|
||||
frontend:
|
||||
- '.github/workflows/frontend-checks.yml'
|
||||
- '.nvmrc'
|
||||
- '.github/actions/install-frontend-deps/**'
|
||||
- 'invokeai/frontend/web/**'
|
||||
|
||||
- name: install dependencies
|
||||
if: ${{ steps.changed-files.outputs.frontend_any_changed == 'true' || inputs.always_run == true }}
|
||||
uses: ./.github/actions/install-frontend-deps
|
||||
with:
|
||||
working-directory: invokeai/frontend/web
|
||||
|
||||
- name: tsc
|
||||
if: ${{ steps.changed-files.outputs.frontend_any_changed == 'true' || inputs.always_run == true }}
|
||||
@@ -111,6 +114,7 @@ jobs:
|
||||
files_yaml: |
|
||||
webv2:
|
||||
- '.github/workflows/frontend-checks.yml'
|
||||
- '.nvmrc'
|
||||
- '.github/actions/install-frontend-deps/**'
|
||||
- 'invokeai/frontend/webv2/**'
|
||||
|
||||
|
||||
@@ -62,12 +62,15 @@ jobs:
|
||||
files_yaml: |
|
||||
frontend:
|
||||
- '.github/workflows/frontend-tests.yml'
|
||||
- '.nvmrc'
|
||||
- '.github/actions/install-frontend-deps/**'
|
||||
- 'invokeai/frontend/web/**'
|
||||
|
||||
- name: install dependencies
|
||||
if: ${{ steps.changed-files.outputs.frontend_any_changed == 'true' || inputs.always_run == true }}
|
||||
uses: ./.github/actions/install-frontend-deps
|
||||
with:
|
||||
working-directory: invokeai/frontend/web
|
||||
|
||||
- name: vitest
|
||||
if: ${{ steps.changed-files.outputs.frontend_any_changed == 'true' || inputs.always_run == true }}
|
||||
@@ -91,6 +94,7 @@ jobs:
|
||||
files_yaml: |
|
||||
webv2:
|
||||
- '.github/workflows/frontend-tests.yml'
|
||||
- '.nvmrc'
|
||||
- '.github/actions/install-frontend-deps/**'
|
||||
- 'invokeai/frontend/webv2/**'
|
||||
|
||||
|
||||
@@ -79,6 +79,8 @@ jobs:
|
||||
src:
|
||||
- 'pyproject.toml'
|
||||
- '.github/workflows/openapi-checks.yml'
|
||||
- '.nvmrc'
|
||||
- '.github/actions/install-frontend-deps/**'
|
||||
- 'scripts/generate_openapi_schema.py'
|
||||
- 'invokeai/**'
|
||||
# The schema is generated from Python and compared against legacy web's artifacts.
|
||||
@@ -107,6 +109,8 @@ jobs:
|
||||
- name: install frontend dependencies
|
||||
if: ${{ steps.changed-files.outputs.src_any_changed == 'true' || inputs.always_run == true }}
|
||||
uses: ./.github/actions/install-frontend-deps
|
||||
with:
|
||||
working-directory: invokeai/frontend/web
|
||||
|
||||
- name: copy schema
|
||||
if: ${{ steps.changed-files.outputs.src_any_changed == 'true' || inputs.always_run == true }}
|
||||
|
||||
@@ -79,6 +79,8 @@ jobs:
|
||||
src:
|
||||
- 'pyproject.toml'
|
||||
- '.github/workflows/typegen-checks.yml'
|
||||
- '.nvmrc'
|
||||
- '.github/actions/install-frontend-deps/**'
|
||||
- 'scripts/generate_openapi_schema.py'
|
||||
- 'invokeai/**'
|
||||
# The schema is generated from Python and compared against legacy web's artifacts.
|
||||
@@ -107,6 +109,8 @@ jobs:
|
||||
- name: install frontend dependencies
|
||||
if: ${{ steps.changed-files.outputs.src_any_changed == 'true' || inputs.always_run == true }}
|
||||
uses: ./.github/actions/install-frontend-deps
|
||||
with:
|
||||
working-directory: invokeai/frontend/web
|
||||
|
||||
- name: copy schema
|
||||
if: ${{ steps.changed-files.outputs.src_any_changed == 'true' || inputs.always_run == true }}
|
||||
@@ -115,7 +119,7 @@ jobs:
|
||||
|
||||
- name: generate schema
|
||||
if: ${{ steps.changed-files.outputs.src_any_changed == 'true' || inputs.always_run == true }}
|
||||
run: cd invokeai/frontend/web && uv run ../../../scripts/generate_openapi_schema.py | pnpm typegen
|
||||
run: set -o pipefail; cd invokeai/frontend/web && uv run ../../../scripts/generate_openapi_schema.py | pnpm typegen
|
||||
shell: bash
|
||||
|
||||
- name: compare files
|
||||
|
||||
@@ -19,7 +19,7 @@ Before editing, read `AGENTS.md` along each target path; scoped rules supplement
|
||||
| CI and contribution tooling | [.github/AGENTS.md](.github/AGENTS.md) |
|
||||
| Documentation | `docs/README.md` and `docs/package.json`; keep documentation about the implemented product current |
|
||||
|
||||
Ordinary frontend work targets **webv2**, served with `--webv2`; default launches select legacy web. Verify build/launch targets for UI investigations. Legacy web owns generated OpenAPI/type artifacts.
|
||||
Ordinary frontend work targets **webv2**, served by default; `--webv2` remains a compatibility alias and `--web-legacy` selects legacy web. Verify build/launch targets for UI investigations. Legacy web owns generated OpenAPI/type artifacts.
|
||||
|
||||
## Engineering standard
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# simple Makefile with scripts that are otherwise hard to remember
|
||||
# to use, run from the repo root `make <command>`
|
||||
|
||||
SHELL := /bin/bash
|
||||
|
||||
default: help
|
||||
|
||||
help:
|
||||
@@ -13,11 +15,11 @@ help:
|
||||
@echo "test Run the unit tests."
|
||||
@echo "frontend-install Install the pnpm modules needed for the frontend"
|
||||
@echo "frontend-build Build the frontend for localhost:9090"
|
||||
@echo "frontendv2-install Install the pnpm modules needed for the webv2 frontend"
|
||||
@echo "frontendv2-build Build the webv2 frontend for localhost:9090"
|
||||
@echo "frontend-legacy-install Install legacy frontend dependencies"
|
||||
@echo "frontend-legacy-build Build the legacy frontend for --web-legacy"
|
||||
@echo "frontend-test Run the frontend test suite once"
|
||||
@echo "frontend-dev Run the frontend in developer mode on localhost:5173"
|
||||
@echo "frontendv2-dev Run the webv2 frontend in developer mode on localhost:5173"
|
||||
@echo "frontend-legacy-dev Run the legacy frontend development server"
|
||||
@echo "frontend-openapi Generate the OpenAPI schema"
|
||||
@echo "frontend-typegen Generate types for the frontend from the OpenAPI schema"
|
||||
@echo "frontend-lint Run frontend checks and fixable lint/format steps"
|
||||
@@ -53,33 +55,39 @@ test:
|
||||
|
||||
# Install the pnpm modules needed for the front end
|
||||
frontend-install:
|
||||
rm -rf invokeai/frontend/web/node_modules
|
||||
cd invokeai/frontend/web && pnpm install
|
||||
|
||||
# Build the frontend
|
||||
frontend-build:
|
||||
cd invokeai/frontend/web && pnpm build
|
||||
|
||||
# Install the pnpm modules needed for the webv2 front end
|
||||
frontendv2-install:
|
||||
rm -rf invokeai/frontend/webv2/node_modules
|
||||
cd invokeai/frontend/webv2 && pnpm install
|
||||
|
||||
# Build the webv2 frontend
|
||||
frontendv2-build:
|
||||
# Build the frontend
|
||||
frontend-build:
|
||||
cd invokeai/frontend/webv2 && pnpm build
|
||||
|
||||
# Compatibility aliases and explicit legacy commands
|
||||
frontendv2-install: frontend-install
|
||||
|
||||
frontend-legacy-install:
|
||||
rm -rf invokeai/frontend/web/node_modules
|
||||
cd invokeai/frontend/web && pnpm install
|
||||
|
||||
# Build the legacy frontend
|
||||
frontendv2-build: frontend-build
|
||||
|
||||
frontend-legacy-build:
|
||||
cd invokeai/frontend/web && pnpm build
|
||||
|
||||
# Run the frontend test suite once
|
||||
frontend-test:
|
||||
cd invokeai/frontend/web && pnpm run test:run
|
||||
cd invokeai/frontend/webv2 && pnpm test
|
||||
|
||||
# Run the frontend in dev mode
|
||||
frontend-dev:
|
||||
cd invokeai/frontend/web && pnpm dev
|
||||
cd invokeai/frontend/webv2 && pnpm dev
|
||||
|
||||
# Run the webv2 frontend in dev mode
|
||||
frontendv2-dev:
|
||||
cd invokeai/frontend/webv2 && pnpm run dev
|
||||
# Run the legacy frontend in dev mode
|
||||
frontendv2-dev: frontend-dev
|
||||
|
||||
frontend-legacy-dev:
|
||||
cd invokeai/frontend/web && pnpm run dev
|
||||
|
||||
# Generate the OpenAPI Schema for the app
|
||||
frontend-openapi:
|
||||
@@ -88,14 +96,10 @@ frontend-openapi:
|
||||
pnpm prettier --write openapi.json
|
||||
|
||||
frontend-typegen:
|
||||
cd invokeai/frontend/web && python ../../../scripts/generate_openapi_schema.py | pnpm typegen
|
||||
set -o pipefail; cd invokeai/frontend/web && python ../../../scripts/generate_openapi_schema.py | pnpm typegen
|
||||
|
||||
frontend-lint:
|
||||
cd invokeai/frontend/web/src && \
|
||||
pnpm lint:tsc && \
|
||||
pnpm lint:dpdm && \
|
||||
pnpm lint:eslint --fix && \
|
||||
pnpm lint:prettier --write
|
||||
pnpm -C invokeai/frontend/webv2 lint
|
||||
|
||||
# Tag the release
|
||||
wheel:
|
||||
|
||||
+10
-2
@@ -8,10 +8,17 @@ ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack use [email protected] && corepack enable
|
||||
|
||||
WORKDIR /build
|
||||
COPY invokeai/frontend/webv2/ ./
|
||||
RUN --mount=type=cache,target=/pnpm/store \
|
||||
pnpm install --frozen-lockfile
|
||||
RUN pnpm exec vite build
|
||||
|
||||
FROM web-builder AS legacy-web-builder
|
||||
WORKDIR /legacy-build
|
||||
COPY invokeai/frontend/web/ ./
|
||||
RUN --mount=type=cache,target=/pnpm/store \
|
||||
pnpm install --frozen-lockfile
|
||||
RUN npx vite build
|
||||
RUN pnpm exec vite build
|
||||
|
||||
## Backend ---------------------------------------
|
||||
|
||||
@@ -95,7 +102,8 @@ ENTRYPOINT ["/opt/invokeai/docker-entrypoint.sh"]
|
||||
CMD ["invokeai-web"]
|
||||
|
||||
# --link requires buldkit w/ dockerfile syntax 1.4, does not work with podman
|
||||
COPY --link --from=web-builder /build/dist ${INVOKEAI_SRC}/invokeai/frontend/web/dist
|
||||
COPY --link --from=web-builder /build/dist ${INVOKEAI_SRC}/invokeai/frontend/webv2/dist
|
||||
COPY --link --from=legacy-web-builder /legacy-build/dist ${INVOKEAI_SRC}/invokeai/frontend/web/dist
|
||||
|
||||
# add sources last to minimize image changes on code changes
|
||||
COPY invokeai ${INVOKEAI_SRC}/invokeai
|
||||
|
||||
@@ -7,28 +7,32 @@ Invoke's UI is made possible by many contributors and open-source libraries. Tha
|
||||
|
||||
## Dev environment
|
||||
|
||||
Follow the [dev environment](/development/setup/dev-environment/) guide to get set up. Run the UI using `pnpm dev`.
|
||||
Follow the [dev environment](/development/setup/dev-environment/) guide to get set up. The default UI lives in `invokeai/frontend/webv2`. Run `make frontend-install`, then `make frontend-dev`; `make frontend-build` builds the bundle served by `invokeai-web`. The existing `frontendv2-*` targets remain aliases.
|
||||
|
||||
`invokeai/frontend/web` is the legacy frontend. Build it with `make frontend-legacy-build` and select it with `invokeai-web --web-legacy`. `--webv2` remains a compatibility alias for the default UI. Close other editor tabs before switching frontends on the same origin. Existing browser storage and project recovery data retain their names and formats; switching frontends does not migrate or erase them.
|
||||
|
||||
## Package scripts
|
||||
|
||||
- `dev`: run the frontend in dev mode, enabling hot reloading
|
||||
- `build`: run all checks (dpdm, eslint, prettier, tsc, knip) and then build the frontend
|
||||
- `lint:dpdm`: check circular dependencies
|
||||
- `lint:eslint`: check code quality
|
||||
- `lint:prettier`: check code formatting
|
||||
- `lint:tsc`: check type issues
|
||||
- `lint:knip`: check for unused exports or objects
|
||||
- `lint`: run all checks concurrently
|
||||
- `fix`: run `eslint` and `prettier`, fixing fixable issues
|
||||
- `test:ui`: run `vitest` with the fancy web UI
|
||||
Run these in `invokeai/frontend/webv2`:
|
||||
|
||||
- `dev`: run the frontend with hot reloading
|
||||
- `build`: run formatting, lint, types, and architecture checks, then build
|
||||
- `lint`: run formatting, Oxc lint, TypeScript, and architecture checks
|
||||
- `fix`: fix supported lint and formatting issues
|
||||
- `test`: run the unit suite
|
||||
- `test:browser`: run Chromium interaction tests
|
||||
- `check:release`: run the complete release gates, including performance, project-file, and accessibility journeys
|
||||
|
||||
The legacy package has its own scripts and lockfile. See each package's `AGENTS.md` for its commands and ownership rules.
|
||||
|
||||
## Type generation
|
||||
|
||||
We use [openapi-typescript] to generate types from the app's OpenAPI schema. The generated types are committed to the repo in [schema.ts].
|
||||
The legacy `web` package owns generation, including shared backend contracts consumed by webv2. We use [openapi-typescript] to generate types from the app's OpenAPI schema. The generated types are committed to the repo in [schema.ts].
|
||||
|
||||
If you make backend changes, it's important to regenerate the frontend types:
|
||||
|
||||
```sh
|
||||
set -o pipefail
|
||||
cd invokeai/frontend/web && python ../../../scripts/generate_openapi_schema.py | pnpm typegen
|
||||
```
|
||||
|
||||
@@ -53,7 +57,7 @@ Only the English source strings (i.e. `en.json`) should be changed on this repo.
|
||||
"request": "launch",
|
||||
"name": "Invoke UI",
|
||||
"url": "http://localhost:5173",
|
||||
"webRoot": "${workspaceFolder}/invokeai/frontend/web"
|
||||
"webRoot": "${workspaceFolder}/invokeai/frontend/webv2"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -85,11 +89,7 @@ Ping `@psychedelicious` on [discord] in the `#frontend-dev` channel or in the fe
|
||||
|
||||
## Code conventions
|
||||
|
||||
- This is a fairly complex app with a deep component tree. Please use memoization (`useCallback`, `useMemo`, `memo`) with enthusiasm.
|
||||
- If you need to add some global, ephemeral state, please use [nanostores] if possible.
|
||||
- Be careful with your redux selectors. If they need to be parameterized, consider creating them inside a `useMemo`.
|
||||
- Feel free to use `lodash` (via `lodash-es`) to make the intent of your code clear.
|
||||
- Please add comments describing the "why", not the "how" (unless it is really arcane).
|
||||
Follow `invokeai/frontend/webv2/AGENTS.md` and its `ARCHITECTURE.md` for ownership, state, React, persistence, and product-quality rules. The linked Redux and control-layer guides describe the legacy frontend.
|
||||
|
||||
## Commit format
|
||||
|
||||
@@ -102,11 +102,7 @@ Please use the [conventional commits] spec for the web UI, with a scope of "ui":
|
||||
|
||||
## Tests
|
||||
|
||||
We don't do any UI testing at this time, but consider adding tests for sensitive logic.
|
||||
|
||||
We use `vitest`, and tests should be next to the file they are testing. If the logic is in `something.ts`, the tests should be in `something.test.ts`.
|
||||
|
||||
In some situations, we may want to test types. For example, if you use `zod` to create a schema that should match a generated type, it's best to add a test to confirm that the types match. Use `tsafe`'s assert for this.
|
||||
Colocate unit tests and Chromium browser tests with the owning code. Use real browser storage and interaction where mocks cannot establish correctness. Run `pnpm check:release` before milestone readiness; browser screenshots and interaction review complement automated gates.
|
||||
|
||||
## Submitting a PR
|
||||
|
||||
|
||||
@@ -489,7 +489,7 @@ clients automatically:
|
||||
|
||||
## Frontend behavior
|
||||
|
||||
### webv2 (`--webv2`)
|
||||
### webv2 (default UI)
|
||||
|
||||
The update lands in the **Generate** panel of the project that was active when
|
||||
the event arrived, and shows a notification summarizing how many fields were
|
||||
|
||||
@@ -7,35 +7,28 @@ import { Tabs, TabItem } from '@astrojs/starlight/components';
|
||||
|
||||
## Frontend Tests
|
||||
|
||||
We use `vitest` to run the frontend tests. (See [vite.config.ts](https://github.com/invoke-ai/InvokeAI/blob/main/invokeai/frontend/web/vite.config.mts) for the default `vitest` options.)
|
||||
|
||||
Below are some common frontend test commands:
|
||||
The default frontend is `webv2`, with unit and Chromium suites. Run commands from the repository root:
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Make">
|
||||
```bash
|
||||
# From the repository root, run the frontend test suite once.
|
||||
# Run the default frontend unit suite once.
|
||||
make frontend-test
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="pnpm">
|
||||
```bash
|
||||
# From the repository root, run the full frontend test suite in watch mode.
|
||||
pnpm --dir invokeai/frontend/web test
|
||||
|
||||
# Run the frontend test suite once without watch mode.
|
||||
pnpm --dir invokeai/frontend/web test:no-watch
|
||||
|
||||
# Equivalent one-shot command using the underlying Vitest subcommand.
|
||||
pnpm --dir invokeai/frontend/web test:run
|
||||
|
||||
# Open the Vitest UI and collect coverage data while running tests.
|
||||
pnpm --dir invokeai/frontend/web test:ui
|
||||
pnpm -C invokeai/frontend/webv2 test
|
||||
pnpm -C invokeai/frontend/webv2 test:browser
|
||||
# Complete milestone gates, including performance, project files, and accessibility.
|
||||
pnpm -C invokeai/frontend/webv2 check:release
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
If you are already working from `invokeai/frontend/web`, you can omit the `--dir` flag and run the same `pnpm` scripts directly from that directory.
|
||||
Install Chromium with `pnpm -C invokeai/frontend/webv2 exec playwright install chromium` if needed. Read `webv2/AGENTS.md` for test ownership and browser verification rules.
|
||||
|
||||
The legacy frontend remains in `invokeai/frontend/web`. Run its separate suite with `pnpm -C invokeai/frontend/web test:no-watch` and its checks with `pnpm -C invokeai/frontend/web lint`.
|
||||
|
||||
## Backend Tests
|
||||
|
||||
|
||||
@@ -75,14 +75,14 @@ import SystemRequirementsLink from '@components/SystemRequirmentsLink.astro'
|
||||
|
||||
7. Install the frontend dev toolchain, paying attention to versions:
|
||||
|
||||
- [`nodejs`](https://nodejs.org/) (tested on LTS, v22)
|
||||
- [`nodejs`](https://nodejs.org/) (use the version in `.nvmrc`)
|
||||
- [`pnpm`](https://pnpm.io/installation) (tested on v10)
|
||||
|
||||
8. Do a production build of the frontend:
|
||||
|
||||
```sh
|
||||
cd <PATH_TO_INVOKEAI_REPO>/invokeai/frontend/web
|
||||
pnpm i
|
||||
cd <PATH_TO_INVOKEAI_REPO>/invokeai/frontend/webv2
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm build
|
||||
```
|
||||
|
||||
@@ -105,12 +105,14 @@ See the [Tests](../tests) documentation for information about running and writin
|
||||
|
||||
## Frontend Development
|
||||
|
||||
The default frontend is `webv2`. To use legacy `web`, build it with `make frontend-legacy-build` and launch `invokeai-web --web-legacy`.
|
||||
|
||||
You'll need to run `pnpm build` every time you pull in new changes to the frontend.
|
||||
|
||||
Another option is to skip the build and instead run the UI in dev mode:
|
||||
|
||||
```sh
|
||||
cd invokeai/frontend/web
|
||||
cd invokeai/frontend/webv2
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
from pathlib import Path
|
||||
|
||||
from starlette.applications import Starlette
|
||||
|
||||
from invokeai.app.api.no_cache_staticfiles import NoCacheStaticFiles
|
||||
|
||||
|
||||
def mount_frontend(app: Starlette, frontend_root: Path, *, legacy: bool = False) -> None:
|
||||
"""Mount shared documentation assets before the selected UI's catch-all SPA route."""
|
||||
ui_root = frontend_root.parent / "web" if legacy else frontend_root
|
||||
app.mount("/static", NoCacheStaticFiles(directory=frontend_root.parent / "web" / "static"), name="static")
|
||||
app.mount("/", NoCacheStaticFiles(directory=ui_root / "dist", html=True), name="ui")
|
||||
+6
-12
@@ -19,9 +19,9 @@ from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoin
|
||||
from starlette.middleware.gzip import GZipMiddleware, GZipResponder, IdentityResponder
|
||||
from starlette.types import ASGIApp, Message, Receive, Scope, Send
|
||||
|
||||
import invokeai.frontend.web as web_dir
|
||||
import invokeai.frontend.webv2 as webv2_dir
|
||||
from invokeai.app.api.dependencies import ApiDependencies
|
||||
from invokeai.app.api.no_cache_staticfiles import NoCacheStaticFiles
|
||||
from invokeai.app.api.frontend import mount_frontend
|
||||
from invokeai.app.api.routers import (
|
||||
app_info,
|
||||
auth,
|
||||
@@ -788,10 +788,7 @@ def overridden_redoc(request: Request) -> HTMLResponse:
|
||||
)
|
||||
|
||||
|
||||
web_root_path = Path(list(web_dir.__path__)[0])
|
||||
ui_root_path = web_root_path
|
||||
if InvokeAIArgs.did_parse and getattr(InvokeAIArgs.args, "webv2", False):
|
||||
ui_root_path = web_root_path.parent / "webv2"
|
||||
web_root_path = Path(list(webv2_dir.__path__)[0])
|
||||
|
||||
if app_config.unsafe_disable_picklescan:
|
||||
logger.warning(
|
||||
@@ -800,9 +797,6 @@ if app_config.unsafe_disable_picklescan:
|
||||
)
|
||||
|
||||
try:
|
||||
app.mount("/", NoCacheStaticFiles(directory=Path(ui_root_path, "dist"), html=True), name="ui")
|
||||
except RuntimeError:
|
||||
logger.warning(f"No UI found at {ui_root_path}/dist, skipping UI mount")
|
||||
app.mount(
|
||||
"/static", NoCacheStaticFiles(directory=Path(web_root_path, "static/")), name="static"
|
||||
) # docs favicon is in here
|
||||
mount_frontend(app, web_root_path, legacy=getattr(InvokeAIArgs.args, "web_legacy", False))
|
||||
except RuntimeError as error:
|
||||
logger.warning(f"No UI found, skipping UI mount: {error}")
|
||||
|
||||
@@ -13,8 +13,11 @@ _config_file_help = r"""Path to the invokeai.yaml configuration file. If omitted
|
||||
_parser = ArgumentParser(description="Invoke Studio", formatter_class=RawTextHelpFormatter)
|
||||
_parser.add_argument("--root", type=str, help=_root_help)
|
||||
_parser.add_argument("--config", dest="config_file", type=str, help=_config_file_help)
|
||||
_webv2_help = r"""Serve the webv2 frontend (invokeai/frontend/webv2) instead of the legacy web frontend. The webv2 frontend must be built first (make frontendv2-build)."""
|
||||
_parser.add_argument("--webv2", action="store_true", help=_webv2_help)
|
||||
_frontend_group = _parser.add_mutually_exclusive_group()
|
||||
_frontend_group.add_argument("--webv2", action="store_true", help="Serve the default frontend (compatibility alias).")
|
||||
_frontend_group.add_argument(
|
||||
"--web-legacy", action="store_true", help="Serve the legacy frontend instead of the default UI."
|
||||
)
|
||||
_parser.add_argument("--version", action="version", version=__version__, help="Displays the version and exits.")
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
> This is the legacy frontend. InvokeAI serves `../webv2/` by default; use `--web-legacy` to serve this bundle. Build it with `make frontend-legacy-build`. Generated OpenAPI and TypeScript API artifacts remain owned here.
|
||||
|
||||
# Invoke UI
|
||||
|
||||
<https://invoke.ai/development/front-end/>
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { retireDefaultServiceWorker } from './retireDefaultServiceWorker';
|
||||
|
||||
afterEach(() => vi.unstubAllGlobals());
|
||||
|
||||
describe('legacy frontend service-worker handoff', () => {
|
||||
const base = 'https://example.com/invoke';
|
||||
|
||||
const setup = (scriptURL: string, controlled: boolean) => {
|
||||
const unregister = vi.fn(() => Promise.resolve(true));
|
||||
const getRegistration = vi.fn(() => Promise.resolve({ active: { scriptURL }, unregister }));
|
||||
const reload = vi.fn();
|
||||
const entries = new Map([
|
||||
['invokeai-shell-old', new Set([`${base}/index.html`, 'https://example.com/other/index.html'])],
|
||||
['invokeai-assets', new Set([`${base}/assets/main.js`, 'https://example.com/other/assets/main.js'])],
|
||||
['invokeai-runtime', new Set([`${base}/locales/en.json`, 'https://example.com/other/locales/en.json'])],
|
||||
['another-app', new Set([`${base}/index.html`])],
|
||||
]);
|
||||
const cacheStorage = {
|
||||
keys: () => Promise.resolve([...entries.keys()]),
|
||||
open: (name: string) =>
|
||||
Promise.resolve({
|
||||
keys: () => Promise.resolve([...entries.get(name)!].map((url) => new Request(url))),
|
||||
delete: (request: Request) => Promise.resolve(entries.get(name)!.delete(request.url)),
|
||||
}),
|
||||
};
|
||||
vi.stubGlobal('navigator', { serviceWorker: { getRegistration, controller: controlled ? { scriptURL } : null } });
|
||||
vi.stubGlobal('window', { caches: cacheStorage, location: { reload } });
|
||||
vi.stubGlobal('caches', cacheStorage);
|
||||
return { unregister, getRegistration, reload, entries };
|
||||
};
|
||||
|
||||
it('releases the deployment worker and only disposable Invoke caches before reloading', async () => {
|
||||
const { unregister, getRegistration, reload, entries } = setup(`${base}/sw.js`, true);
|
||||
expect(await retireDefaultServiceWorker(base)).toBe(true);
|
||||
expect(getRegistration).toHaveBeenCalledWith(`${base}/`);
|
||||
expect(unregister).toHaveBeenCalledOnce();
|
||||
expect(entries).toEqual(
|
||||
new Map([
|
||||
['invokeai-shell-old', new Set(['https://example.com/other/index.html'])],
|
||||
['invokeai-assets', new Set(['https://example.com/other/assets/main.js'])],
|
||||
['invokeai-runtime', new Set(['https://example.com/other/locales/en.json'])],
|
||||
['another-app', new Set([`${base}/index.html`])],
|
||||
])
|
||||
);
|
||||
expect(reload).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('continues boot without a reload when the document is already uncontrolled', async () => {
|
||||
const { reload } = setup(`${base}/sw.js`, false);
|
||||
expect(await retireDefaultServiceWorker(base)).toBe(false);
|
||||
expect(reload).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('leaves a different application worker and its caches alone', async () => {
|
||||
const { unregister, reload, entries } = setup('https://example.com/sw.js', true);
|
||||
expect(await retireDefaultServiceWorker(base)).toBe(false);
|
||||
expect(unregister).not.toHaveBeenCalled();
|
||||
expect(reload).not.toHaveBeenCalled();
|
||||
expect(entries.get('invokeai-assets')).toEqual(
|
||||
new Set([`${base}/assets/main.js`, 'https://example.com/other/assets/main.js'])
|
||||
);
|
||||
});
|
||||
|
||||
it('boots normally when service workers are unavailable', async () => {
|
||||
vi.stubGlobal('navigator', {});
|
||||
expect(await retireDefaultServiceWorker(base)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
/** Release the default UI's worker before legacy loads its differently shaped locale files. */
|
||||
export const retireDefaultServiceWorker = async (deploymentBaseUrl: string): Promise<boolean> => {
|
||||
if (!('serviceWorker' in navigator)) {
|
||||
return false;
|
||||
}
|
||||
const scriptUrl = `${deploymentBaseUrl}/sw.js`;
|
||||
const registration = await navigator.serviceWorker.getRegistration(`${deploymentBaseUrl}/`);
|
||||
if (registration?.active?.scriptURL !== scriptUrl) {
|
||||
return false;
|
||||
}
|
||||
await registration.unregister();
|
||||
// Asset caches are disposable. Never clear localStorage or IndexedDB recovery drafts.
|
||||
if ('caches' in window) {
|
||||
const names = await caches.keys();
|
||||
const indexUrl = `${deploymentBaseUrl}/index.html`;
|
||||
const assetPrefix = `${deploymentBaseUrl}/assets/`;
|
||||
const localePrefix = `${deploymentBaseUrl}/locales/`;
|
||||
await Promise.all(
|
||||
names
|
||||
.filter(
|
||||
(name) => name.startsWith('invokeai-shell-') || name === 'invokeai-assets' || name === 'invokeai-runtime'
|
||||
)
|
||||
.map(async (name) => {
|
||||
const cache = await caches.open(name);
|
||||
const requests = await cache.keys();
|
||||
// Cache names are origin-wide, so another deployment may share each bucket.
|
||||
await Promise.all(
|
||||
requests
|
||||
.filter(({ url }) => url === indexUrl || url.startsWith(assetPrefix) || url.startsWith(localePrefix))
|
||||
.map((request) => cache.delete(request))
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
// Unregistration leaves the current document controlled until its next navigation.
|
||||
if (navigator.serviceWorker.controller?.scriptURL === scriptUrl) {
|
||||
window.location.reload();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@@ -1,5 +1,23 @@
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
import InvokeAIUI from './app/components/InvokeAIUI';
|
||||
import { getDeploymentBaseUrl } from './common/util/baseUrl';
|
||||
import { retireDefaultServiceWorker } from './common/util/retireDefaultServiceWorker';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<InvokeAIUI />);
|
||||
const boot = async () => {
|
||||
if (await retireDefaultServiceWorker(getDeploymentBaseUrl())) {
|
||||
return;
|
||||
}
|
||||
const { default: InvokeAIUI } = await import('./app/components/InvokeAIUI');
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<InvokeAIUI />);
|
||||
};
|
||||
|
||||
void boot().catch(() => {
|
||||
const root = document.getElementById('root');
|
||||
if (root) {
|
||||
root.textContent = 'Unable to start the legacy frontend. Reload to retry.';
|
||||
const retry = document.createElement('button');
|
||||
retry.textContent = 'Reload';
|
||||
retry.addEventListener('click', () => window.location.reload());
|
||||
root.appendChild(retry);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Webv2
|
||||
|
||||
Read [ARCHITECTURE.md](ARCHITECTURE.md) for ownership/interface changes and affected owner-local READMEs for lifecycle/persistence. Stack: React 19, Vite, Chakra UI 3, TanStack Query/Router, @dnd-kit; legacy web differs.
|
||||
Read [ARCHITECTURE.md](ARCHITECTURE.md) for ownership/interface changes and affected owner-local READMEs for lifecycle/persistence. Stack: React 19, Vite, Chakra UI 3, TanStack Query/Router, @dnd-kit; legacy web differs. Webv2 is served by default; `--webv2` is a compatibility alias and `--web-legacy` selects `../web/`.
|
||||
|
||||
## Ownership and state
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""Default InvokeAI frontend package."""
|
||||
@@ -7,7 +7,7 @@
|
||||
"#architecture/source-analysis": "./src/architecture/tsSourceAnalysis.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"architecture:check": "vitest run --config vitest.config.mts src/architecture/dependencyPolicy.test.ts src/architecture/workbenchOwnership.test.ts src/architecture/architectureWorkflow.test.ts src/architecture/canvasSeamOwnership.test.ts src/architecture/tsSourceAnalysis.test.ts src/architecture/secureContextApis.test.ts src/architecture/axeAudits.test.ts && node scripts/write-workbench-ownership-inventory.mjs && node scripts/write-contract-caller-matrix.mjs && node scripts/write-canvas-import-matrix.mjs",
|
||||
"architecture:check": "vitest run --config vitest.config.mts src/architecture/dependencyPolicy.test.ts src/architecture/workbenchOwnership.test.ts src/architecture/architectureWorkflow.test.ts src/architecture/canvasSeamOwnership.test.ts src/architecture/tsSourceAnalysis.test.ts src/architecture/secureContextApis.test.ts src/architecture/axeAudits.test.ts && node --experimental-strip-types scripts/write-workbench-ownership-inventory.mjs && node --experimental-strip-types scripts/write-contract-caller-matrix.mjs && node --experimental-strip-types scripts/write-canvas-import-matrix.mjs",
|
||||
"build": "pnpm run lint && vite build",
|
||||
"check:architecture": "pnpm run lint && pnpm run test:all && pnpm run test:performance:architecture && pnpm run test:project-files:browser",
|
||||
"check:release": "pnpm run check:architecture && pnpm run test:accessibility:browser",
|
||||
@@ -34,8 +34,8 @@
|
||||
"test:performance:architecture": "pnpm run test:performance:build && pnpm run test:performance:browser",
|
||||
"test:performance:browser": "node scripts/measure-architecture-performance.mjs",
|
||||
"test:performance:browser:update-baseline": "node scripts/measure-architecture-performance.mjs --update-baseline",
|
||||
"test:performance:build": "pnpm run test:performance:contracts && vite build && node scripts/check-architecture-performance.mjs",
|
||||
"test:performance:build:update-baseline": "pnpm run test:performance:contracts && vite build && node scripts/check-architecture-performance.mjs --update-baseline",
|
||||
"test:performance:build": "pnpm run test:performance:contracts && vite build && node --experimental-strip-types scripts/check-architecture-performance.mjs",
|
||||
"test:performance:build:update-baseline": "pnpm run test:performance:contracts && vite build && node --experimental-strip-types scripts/check-architecture-performance.mjs --update-baseline",
|
||||
"test:performance:contracts": "node --test scripts/performance-budgets.test.mjs",
|
||||
"test:project-files": "vite build && pnpm run test:project-files:browser",
|
||||
"test:project-files:browser": "node scripts/run-project-file-journey.mjs"
|
||||
|
||||
@@ -41,4 +41,63 @@ describe('architecture workflow', () => {
|
||||
expect(completion.run).toBe('pnpm check:release');
|
||||
expect(completion.env.WEBV2_PERF_REFERENCE_DIR).toContain('steps.perf-ref.outputs.cache-matched-key');
|
||||
});
|
||||
|
||||
it('installs dependencies in the package that owns each frontend and generation gate', () => {
|
||||
const sources = import.meta.glob(
|
||||
[
|
||||
'../../../../../.github/workflows/{frontend-checks,frontend-tests,openapi-checks,typegen-checks}.yml',
|
||||
'../../../../../.github/actions/install-frontend-deps/action.yml',
|
||||
],
|
||||
{ eager: true, import: 'default', query: '?raw' }
|
||||
) as Record<string, string>;
|
||||
const action = parse(Object.entries(sources).find(([path]) => path.endsWith('/action.yml'))![1]);
|
||||
expect(action.inputs['working-directory'].default).toBe('invokeai/frontend/webv2');
|
||||
for (const [path, source] of Object.entries(sources).filter(([path]) => path.includes('/workflows/'))) {
|
||||
const jobs = parse(source).jobs;
|
||||
for (const [id, job] of Object.entries(jobs) as [
|
||||
string,
|
||||
{
|
||||
defaults?: { run?: { 'working-directory'?: string } };
|
||||
steps: { uses?: string; with?: { 'working-directory'?: string } }[];
|
||||
},
|
||||
][]) {
|
||||
const install = job.steps.find((step) => step.uses === './.github/actions/install-frontend-deps');
|
||||
if (!install) {
|
||||
continue;
|
||||
}
|
||||
const expected = id.startsWith('frontend-webv2-') ? 'invokeai/frontend/webv2' : 'invokeai/frontend/web';
|
||||
expect(install.with?.['working-directory'] ?? action.inputs['working-directory'].default, path).toBe(expected);
|
||||
if (job.defaults?.run) {
|
||||
expect(job.defaults.run['working-directory'], path).toBe(expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('runs every frontend-dependent gate when the shared Node version changes', () => {
|
||||
const sources = import.meta.glob(
|
||||
[
|
||||
'../../../../../.github/workflows/{frontend-checks,frontend-tests,openapi-checks,typegen-checks}.yml',
|
||||
'../../../../../.github/actions/install-frontend-deps/action.yml',
|
||||
],
|
||||
{ eager: true, import: 'default', query: '?raw' }
|
||||
) as Record<string, string>;
|
||||
const action = parse(Object.entries(sources).find(([path]) => path.endsWith('/action.yml'))![1]);
|
||||
const nodeVersionFile = action.runs.steps.find((step: { uses?: string }) =>
|
||||
step.uses?.startsWith('actions/setup-node@')
|
||||
).with['node-version-file'];
|
||||
const workflows = Object.entries(sources).filter(([path]) => path.includes('/workflows/'));
|
||||
expect(workflows).toHaveLength(4);
|
||||
for (const [path, source] of workflows) {
|
||||
const jobs = parse(source).jobs as Record<string, { steps: { uses?: string; with?: { files_yaml?: string } }[] }>;
|
||||
for (const job of Object.values(jobs)) {
|
||||
if (!job.steps.some((step) => step.uses === './.github/actions/install-frontend-deps')) {
|
||||
continue;
|
||||
}
|
||||
const filter = job.steps.find((step) => step.with?.files_yaml)?.with?.files_yaml ?? '';
|
||||
const patterns = Object.values(parse(filter) as Record<string, string[]>).flat();
|
||||
expect(patterns, path).toContain(nodeVersionFile);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -328,6 +328,7 @@ version = { attr = "invokeai.version.__version__" }
|
||||
"invokeai.backend" = ["**.png", "**/*.icc"]
|
||||
"invokeai.configs" = ["*.example", "**/*.yaml", "*.txt"]
|
||||
"invokeai.frontend.web.dist" = ["**"]
|
||||
"invokeai.frontend.webv2.dist" = ["**"]
|
||||
"invokeai.frontend.web.static" = ["**"]
|
||||
"invokeai.app.invocations" = ["**"]
|
||||
|
||||
@@ -370,6 +371,7 @@ exclude = [
|
||||
"build",
|
||||
"dist",
|
||||
"invokeai/frontend/web/node_modules/",
|
||||
"invokeai/frontend/webv2/node_modules/",
|
||||
".venv*",
|
||||
"*.ipynb",
|
||||
"invokeai/backend/image_util/mediapipe_face/", # External code
|
||||
|
||||
+13
-17
@@ -46,22 +46,18 @@ echo
|
||||
|
||||
# ---------------------- FRONTEND ----------------------
|
||||
|
||||
pushd ../invokeai/frontend/web >/dev/null
|
||||
echo "Installing frontend dependencies..."
|
||||
echo
|
||||
pnpm i --frozen-lockfile
|
||||
echo
|
||||
if [[ ! -z ${CI} ]]; then
|
||||
echo "Building frontend without checks..."
|
||||
# In CI, we have already done the frontend checks and can just build
|
||||
pnpm vite build
|
||||
else
|
||||
echo "Running checks and building frontend..."
|
||||
# This runs all the frontend checks and builds
|
||||
pnpm build
|
||||
fi
|
||||
echo
|
||||
popd
|
||||
for frontend in webv2 web; do
|
||||
pushd "../invokeai/frontend/$frontend" >/dev/null
|
||||
echo "Installing $frontend dependencies..."
|
||||
pnpm install --frozen-lockfile
|
||||
if [[ ! -z ${CI} ]]; then
|
||||
# CI runs each package's completion gates before building distributions.
|
||||
pnpm exec vite build
|
||||
else
|
||||
pnpm build
|
||||
fi
|
||||
popd
|
||||
done
|
||||
|
||||
# ---------------------- BACKEND ----------------------
|
||||
|
||||
@@ -85,7 +81,7 @@ echo -e "${BGREEN}Built PyPi distribution: ./dist${RESET}"
|
||||
if [[ -z ${CI} ]]; then
|
||||
echo
|
||||
echo "Cleaning up intermediate build files..."
|
||||
rm -rf InvokeAI-Installer tmp ../invokeai/frontend/web/dist/
|
||||
rm -rf InvokeAI-Installer tmp ../invokeai/frontend/webv2/dist/ ../invokeai/frontend/web/dist/
|
||||
fi
|
||||
|
||||
if [[ ! -z ${CI} ]]; then
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from starlette.applications import Starlette
|
||||
from starlette.responses import PlainTextResponse
|
||||
from starlette.routing import Mount, Route
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from invokeai.app.api.frontend import mount_frontend
|
||||
from invokeai.frontend.cli.arg_parser import InvokeAIArgs
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def frontend_root(tmp_path: Path) -> Path:
|
||||
for name, marker in [("webv2", "default frontend"), ("web", "legacy frontend")]:
|
||||
bundle = tmp_path / name / "dist"
|
||||
(bundle / "assets").mkdir(parents=True)
|
||||
(bundle / "index.html").write_text(marker)
|
||||
(bundle / "assets" / "main.js").write_text(f"console.log('{marker}')")
|
||||
root = tmp_path / "web"
|
||||
(root / "static" / "docs").mkdir(parents=True)
|
||||
(root / "static" / "docs" / "invoke-favicon-docs.svg").write_text("<svg>docs favicon</svg>")
|
||||
return tmp_path / "webv2"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("prefix", ["", "/invoke"])
|
||||
@pytest.mark.parametrize(
|
||||
("flags", "marker"),
|
||||
[([], "default frontend"), (["--webv2"], "default frontend"), (["--web-legacy"], "legacy frontend")],
|
||||
)
|
||||
def test_launch_serves_selected_bundle_and_preserves_api_and_docs(
|
||||
frontend_root: Path, monkeypatch: pytest.MonkeyPatch, flags: list[str], marker: str, prefix: str
|
||||
) -> None:
|
||||
monkeypatch.setattr(sys, "argv", ["invokeai-web", *flags])
|
||||
monkeypatch.setattr(InvokeAIArgs, "args", None)
|
||||
monkeypatch.setattr(InvokeAIArgs, "did_parse", False)
|
||||
args = InvokeAIArgs.parse_args()
|
||||
app = Starlette(routes=[Route("/api/ping", lambda request: PlainTextResponse("backend"))])
|
||||
mount_frontend(app, frontend_root, legacy=getattr(args, "web_legacy", False))
|
||||
host = Starlette(routes=[Mount(prefix, app=app)]) if prefix else app
|
||||
with TestClient(host) as client:
|
||||
for path in ["/", "/projects/example"]:
|
||||
response = client.get(f"{prefix}{path}")
|
||||
assert response.status_code == 200
|
||||
assert response.text == marker
|
||||
assert "no-store" in response.headers["cache-control"]
|
||||
assert client.get(f"{prefix}/assets/main.js").text == f"console.log('{marker}')"
|
||||
favicon = client.get(f"{prefix}/static/docs/invoke-favicon-docs.svg")
|
||||
assert favicon.status_code == 200
|
||||
assert favicon.text == "<svg>docs favicon</svg>"
|
||||
assert client.get(f"{prefix}/static/missing.svg").status_code == 404
|
||||
assert client.get(f"{prefix}/api/ping").text == "backend"
|
||||
|
||||
|
||||
def test_conflicting_frontend_flags_are_rejected(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(sys, "argv", ["invokeai-web", "--webv2", "--web-legacy"])
|
||||
with pytest.raises(SystemExit, match="2"):
|
||||
InvokeAIArgs.parse_args()
|
||||
|
||||
|
||||
def test_missing_legacy_bundle_does_not_silently_serve_default(frontend_root: Path) -> None:
|
||||
(frontend_root.parent / "web" / "dist").rename(frontend_root.parent / "old-dist")
|
||||
with pytest.raises(RuntimeError, match="web"):
|
||||
mount_frontend(Starlette(), frontend_root, legacy=True)
|
||||
|
||||
|
||||
def test_missing_default_bundle_preserves_docs_without_falling_back_to_legacy(frontend_root: Path) -> None:
|
||||
(frontend_root / "dist").rename(frontend_root.parent / "old-dist")
|
||||
app = Starlette()
|
||||
with pytest.raises(RuntimeError, match="webv2"):
|
||||
mount_frontend(app, frontend_root)
|
||||
with TestClient(app) as client:
|
||||
assert client.get("/").status_code == 404
|
||||
assert client.get("/static/docs/invoke-favicon-docs.svg").text == "<svg>docs favicon</svg>"
|
||||
Reference in New Issue
Block a user