mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-02 21:03:34 +03:00
Links a self-hosted instance to a SaaS team over an ordinary redirect, and leaves the admin's browser holding a Stirling session at the same time. ## The problem A self-hosted server needs a device credential bound to a SaaS team, and the admin's Supabase JWT must never reach the instance backend. Three things ruled out the obvious approaches: - **A customer hostname can never be in Supabase's redirect allow-list**, so the sign-in cannot happen on the instance's own origin. That is why SSO and sign-up did not work for linking at all. - **A device credential identifies a server, not a person.** Every attended portal read (Usage, Billing, Documents, Infrastructure) goes through `getPortalSaasToken()` and needs a *user* session, so a credential-only link left all of them asking for a second sign-in. - **The previous design relayed a JWT** from the browser into the instance, which is the thing we wanted to avoid. That path is deleted here. ## The solution Redirect and nonce, modelled on desktop's `authService.loginWithSelfHostedOAuth`: mint a nonce, hand the browser off, accept only a callback carrying that nonce back. Desktop has the OS route the reply; self-hosted has no OS hop, so our own approval page performs it. That is the point — the human half happens on an origin we control. ``` instance SaaS admin's browser | POST connect/request | | | (name, callback, nonce, | | | claim-secret hash) | | |-------------------------->| | | <- requestId + authorizeUrl | | | GET /link?request=... | | |<-------------------------------| | | sign in (SSO works here), | | | see ACCOUNT + ORIGIN, approve | | |------------------------------->| | | 302 callback#nonce+session | | POST connect/claim | | | (requestId, claim secret)| | |-------------------------->| | | <- device credential | | ``` Four properties carry the safety, and each is stated in the code because each is easy to lose in a refactor: - **The redirect target is never caller-supplied.** Validated once at creation, then read back from the stored row, so nothing in the approval page's URL can steer the token elsewhere. - **Approval and minting are separate.** Approval records the team and hands out nothing usable; the credential is minted only on claim, authenticated by a secret that never entered a browser. - **A re-authentication cannot move a server between teams.** The team is pinned at creation from the credential only that instance holds, so an approver from another team gets `WRONG_TEAM` instead of a rebind. - **The approver has to confirm what they are binding.** The page shows the address and the signed-in account, with a way to switch, and a checkbox naming the address gates the approve button. The name the server reports is deliberately not shown: the requester picks it on an unauthenticated endpoint, and its honest value is the hostname already in the address. The session rides the URL fragment, so it stays out of access logs and `Referer`, and is stripped before anything awaits. The claim is row-locked, so one approval mints once. A request lives 30 minutes; a settled one is not offered again, since approving it fails server-side. Signing in mid-flow no longer loses the request. The id is kept on the SaaS origin and resumed after any sign-in, which is what makes creating an account work: the confirmation email opens a new tab, where the `next` parameter is gone. Reading it does not consume it — the request may be open in two tabs — and only a recorded decision retires it. The result lands as a modal over the portal the admin started from, and the portal re-reads its link status so the page behind agrees with the modal. Plaintext `http://` callbacks are accepted rather than refused, because many self-hosted instances legitimately run plain HTTP on a private network; the address carries a warning icon explaining the risk, derived server-side so a requester cannot suppress it. Hard-refusing `http://` to a public IP literal is a reasonable follow-up; a bare hostname can't be classified without a DNS lookup, so the warning stays the general mechanism. ## Configuration Four surfaces. Placeholders below, not values. **SaaS backend** | Setting | Needed | Why | |---|---|---| | `stirling.billing.account-link.enabled` | Yes, `true` | The connect controller and service are `@ConditionalOnProperty` with no default, so without it the endpoints do not exist. | | `system.frontendUrl` | Only when the approval page is not on the API's own origin | Where the approver is sent. Must include the app's base path if it is served under one, or the redirect misses `/link`. | **SaaS frontend** | Setting | Needed | Why | |---|---|---| | `VITE_SUPABASE_URL`, `VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY` | Yes | Its own sign-in. Must be the project the SaaS backend validates tokens against. | | `RUN_SUBPATH` | Only if served under a subpath | Moves the approval page to `<base>/<subpath>/link`, so `system.frontendUrl` has to agree. | **Self-hosted backend** | Setting | Needed | Why | |---|---|---| | `stirling.billing.account-link.enabled` | Yes, `true` | Defaults to `false`. | | `stirling.billing.account-link.saas-base-url` | Yes | Origin of the SaaS API it links to. Not the SaaS frontend. | | `system.frontendUrl` | Optional | Externally reachable base URL for the callback. Otherwise derived from the request's `Origin`, which is right for ordinary deployments and wrong behind a rewriting proxy. | **Self-hosted frontend** | Setting | Needed | Why | |---|---|---| | `VITE_SUPABASE_URL`, `VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY` | Yes | Accepts the session handed over in the callback fragment. | | `VITE_SAAS_API_URL` | For Usage and Billing | Attended reads go to the SaaS API with the admin's token. Absent, those surfaces stay on the mock. | | `VITE_INCLUDE_PORTAL` | Production builds | Dev builds include the portal automatically; without it there is no link UI and no callback route. | Two things worth stating because neither fails loudly: - **Both frontends must use the URL *and* key of the same Supabase project**, and the same one the SaaS backend validates against. A key from one project with a URL from another is accepted by the browser and rejected by Supabase, which surfaces much later as "session expired" on Usage rather than as an error at hand-over. - **The Supabase redirect allow-list must contain the SaaS app's `/auth/callback`**, since a confirmation email returns through it. Entries are matched exactly. - **`system.frontendUrl` is the existing setting for this**, not a new one, so each side reads its own value and there is nothing extra to configure. It also gates share links, so on a stack with storage and sharing already on, setting it here turns those on too. The self-hosted side deliberately does **not** configure where the approval page lives — SaaS answers that in the connect-request reply, being the only party that knows. Also here, because testing this needs two stacks side by side: `linked:staging` / `linked:dev` (which derive `system.frontendUrl` and `RUN_SUBPATH` themselves), the missing `frontend:staging:saas`, and a per-mode vite `cacheDir` — two dev servers in different modes otherwise re-optimise over one shared dep cache. ## How to test Automated and green: `task frontend:check:all` plus both backend modules. `ConnectRequestServiceTest` covers callback validation, the per-IP cap, single-use approval, claim outcomes, expiry, `WRONG_TEAM` and reauth confirming without minting; `ConnectServiceTest` covers callback-resolution precedence including a foreign-origin callback being discarded; `ConnectControllerTest` covers the authorize URL, including the forwarded-header path and only the first hop being trusted; `ConnectCallback.test.tsx` covers the fragment being stripped synchronously and malformed fragments refused; `LinkAccountModal.test.tsx` covers link and reauth hitting different endpoints. Manual walkthrough: 1. `task linked:staging` — added here; brings up a SaaS stack and a self-hosted instance pointed at it, on discovered ports, and prints the four addresses. 2. Open the link-account modal in the self-hosted portal and continue. Expect the SaaS approval page at `/link?request=<id>`. 3. Sign in as a team leader, or create an account and confirm the email. Either way you should come back to the approval page. 4. Tick the acknowledgement and approve. Expect the fragment gone from the address bar immediately, a result modal over the portal, the portal showing linked without a reload, and attended reads (Usage, Billing) working without a second sign-in. 5. Repeat, approving as a member of a different team. Expect a refusal, not a rebind. ## Outstanding - #7415 to be reworked against this design once this lands. - **No SaaS-side UI to disconnect a server.** `GET /account-link/instances` and `POST /account-link/instances/{id}/revoke` are already team-scoped and leader-gated, and the portal has a panel that uses them, but `portal-saas/components/settings/accountLinkSettings.tsx` exports `null` on the reasoning that "SaaS has no account-link concept". That held when linking was a self-hosted admin managing their own instance; here a leader approves a server they may not administer, and has no way to withdraw it. The seam to fill is that one file. Expected to land with the CTA work in #7415. --------- Co-authored-by: James Brunton <jbrunton96@gmail.com>
598 lines
18 KiB
YAML
598 lines
18 KiB
YAML
version: '3'
|
|
|
|
# Tasks operate from the workspace root (frontend/). Editor commands pass
|
|
# `editor` as the vite project root (positional after `build` / before the
|
|
# mode flag) or use `--project editor/...` for tsc — so the editor lives
|
|
# under frontend/editor/ without each task needing a cd.
|
|
|
|
vars:
|
|
# Dev-only browser-tab label so concurrent worktrees are distinguishable. Only
|
|
# the worktree folder basename (e.g. "wt1") is exposed — never the full path,
|
|
# hostname, or user. Dropped from production builds.
|
|
DEV_LABEL:
|
|
sh: >-
|
|
{{if eq OS "windows"}}powershell -NoProfile -Command '$root = git rev-parse --show-toplevel 2>$null; if (-not $root) { $root = (Get-Location).Path }; Split-Path -Leaf $root'{{else}}basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)"{{end}}
|
|
|
|
tasks:
|
|
install:
|
|
desc: "Install dependencies"
|
|
run: once
|
|
cmds:
|
|
- '{{ if eq .CI "true" }}npm ci{{ else }}npm install{{ end }}'
|
|
sources:
|
|
- package-lock.json
|
|
- package.json
|
|
status:
|
|
- test -d node_modules
|
|
env:
|
|
CI: '{{ .CI | default "false" }}'
|
|
|
|
prepare:env:
|
|
internal: true
|
|
run: when_changed
|
|
deps: [install]
|
|
vars:
|
|
MODE: '{{.MODE | default ""}}'
|
|
cmds:
|
|
- npx tsx editor/scripts/setup-env.mts{{if .MODE}} --{{.MODE}}{{end}}
|
|
sources:
|
|
- editor/scripts/setup-env.mts
|
|
generates:
|
|
- editor/.env.local
|
|
- editor/.env{{if .MODE}}.{{.MODE}}{{end}}.local
|
|
|
|
prepare:icons:
|
|
internal: true
|
|
run: once
|
|
deps: [install]
|
|
cmds:
|
|
- node editor/scripts/generate-icons.js
|
|
|
|
prepare:og:
|
|
internal: true
|
|
run: when_changed
|
|
desc: "Regenerate OG/social-preview metadata from the tool registry"
|
|
cmds:
|
|
- node editor/scripts/generate-og-metadata.mjs
|
|
sources:
|
|
- editor/src/core/types/toolId.ts
|
|
- editor/src/core/utils/urlMapping.ts
|
|
- editor/src/core/data/useTranslatedToolRegistry.tsx
|
|
- editor/public/og_images/*.png
|
|
generates:
|
|
- editor/src/core/data/ogImageMap.json
|
|
- editor/public/og-metadata.json
|
|
|
|
prepare:
|
|
desc: "Set up dev environment"
|
|
run: when_changed
|
|
vars:
|
|
MODE: '{{.MODE | default ""}}'
|
|
deps:
|
|
- task: prepare:env
|
|
vars: { MODE: '{{.MODE}}' }
|
|
- prepare:icons
|
|
- prepare:og
|
|
|
|
# ============================================================
|
|
# Development
|
|
# ============================================================
|
|
|
|
dev:_run:
|
|
internal: true
|
|
ignore_error: true
|
|
vars:
|
|
MODE: '{{.MODE}}'
|
|
PORT: '{{.PORT | default "5173"}}'
|
|
BACKEND_URL: '{{.BACKEND_URL | default "http://localhost:8080"}}'
|
|
OPEN: '{{.OPEN | default ""}}'
|
|
env:
|
|
BACKEND_URL: '{{.BACKEND_URL}}'
|
|
STIRLING_DEV_LABEL: '{{.DEV_LABEL}}'
|
|
cmds:
|
|
- npx vite editor --mode {{.MODE}} --port {{.PORT}}{{if .OPEN}} --open{{end}}
|
|
|
|
# Separate from dev:_run rather than a flag on it: Task sets an `env:` key even
|
|
# when its value resolves to empty, and Vite treats an empty process.env VITE_* as
|
|
# authoritative over the committed editor/.env, so folding these in blanks Supabase
|
|
# config for the core, proprietary and desktop dev servers.
|
|
dev:_run:saas:
|
|
internal: true
|
|
ignore_error: true
|
|
# The backend's own env files, so both halves target one project. Paths are
|
|
# relative to this taskfile's dir, `frontend`.
|
|
dotenv: ['../app/.env.saas.local', '../app/.env.saas']
|
|
vars:
|
|
PORT: '{{.PORT | default "5173"}}'
|
|
BACKEND_URL: '{{.BACKEND_URL | default "http://localhost:8080"}}'
|
|
OPEN: '{{.OPEN | default ""}}'
|
|
SAAS_ENV: '{{.SAAS_ENV | default "dev"}}'
|
|
env:
|
|
BACKEND_URL: '{{.BACKEND_URL}}'
|
|
STIRLING_DEV_LABEL: '{{.DEV_LABEL}}'
|
|
SAAS_ENV: '{{.SAAS_ENV}}'
|
|
# A real process.env VITE_* beats a committed .env in Vite (loadEnv applies
|
|
# process.env last), which is what lets this override editor/.env.
|
|
#
|
|
# These must stay `sh:`, not Go templates: dotenv values are visible to Task's
|
|
# embedded shell but not to templates, where {{.SAAS_DEV_PROJECT_REF}} is
|
|
# always empty.
|
|
VITE_SUPABASE_URL:
|
|
sh: |
|
|
case "${SAAS_ENV:-dev}" in
|
|
staging) ref="${SAAS_STAGING_PROJECT_REF:?set it in app/.env.saas.local}" ;;
|
|
*) ref="${SAAS_DEV_PROJECT_REF:?set it in app/.env.saas.local, or pass SAAS_ENV=staging}" ;;
|
|
esac
|
|
echo "https://${ref}.supabase.co"
|
|
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY:
|
|
sh: |
|
|
case "${SAAS_ENV:-dev}" in
|
|
staging) echo "${SAAS_STAGING_PUBLISHABLE_KEY:?set it in app/.env.saas.local}" ;;
|
|
*) echo "${SAAS_DEV_PUBLISHABLE_KEY:?set it in app/.env.saas.local, or pass SAAS_ENV=staging}" ;;
|
|
esac
|
|
cmds:
|
|
- 'echo ">> frontend {{.SAAS_ENV}}: Supabase $VITE_SUPABASE_URL, backend $BACKEND_URL"'
|
|
- npx vite editor --mode saas --port {{.PORT}}{{if .OPEN}} --open{{end}}
|
|
|
|
dev:
|
|
desc: "Start frontend dev server"
|
|
cmds:
|
|
- task: dev:proprietary
|
|
vars: { PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
|
|
|
|
dev:core:
|
|
desc: "Start frontend dev server in core mode"
|
|
deps: [prepare]
|
|
cmds:
|
|
- task: dev:_run
|
|
vars: { MODE: core, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
|
|
|
|
dev:proprietary:
|
|
desc: "Start frontend dev server in proprietary mode"
|
|
deps: [prepare]
|
|
cmds:
|
|
- task: dev:_run
|
|
vars: { MODE: proprietary, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
|
|
|
|
dev:saas:
|
|
desc: "Start frontend dev server in SaaS mode (SAAS_ENV=dev|staging|prod)"
|
|
deps:
|
|
- task: prepare
|
|
vars: { MODE: saas }
|
|
vars:
|
|
SAAS_ENV: '{{.SAAS_ENV | default "dev"}}'
|
|
# prod routes to the plain runner, which sets no VITE_SUPABASE_* and so leaves
|
|
# the committed editor/.env alone.
|
|
RUNNER: '{{if eq .SAAS_ENV "prod"}}dev:_run{{else}}dev:_run:saas{{end}}'
|
|
cmds:
|
|
- task: '{{.RUNNER}}'
|
|
vars:
|
|
MODE: saas
|
|
PORT: '{{.PORT}}'
|
|
BACKEND_URL: '{{.BACKEND_URL}}'
|
|
OPEN: '{{.OPEN}}'
|
|
SAAS_ENV: '{{.SAAS_ENV}}'
|
|
|
|
staging:saas:
|
|
desc: "Start frontend dev server against the shared v3 staging project"
|
|
cmds:
|
|
- task: dev:saas
|
|
vars:
|
|
SAAS_ENV: staging
|
|
PORT: '{{.PORT}}'
|
|
BACKEND_URL: '{{.BACKEND_URL}}'
|
|
OPEN: '{{.OPEN}}'
|
|
|
|
dev:desktop:
|
|
desc: "Start frontend dev server in desktop mode"
|
|
deps:
|
|
- task: prepare
|
|
vars: { MODE: desktop }
|
|
cmds:
|
|
- task: dev:_run
|
|
vars: { MODE: desktop, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
|
|
|
|
dev:prototypes:
|
|
desc: "Start frontend dev server in prototypes mode"
|
|
deps: [prepare]
|
|
cmds:
|
|
- task: dev:_run
|
|
vars: { MODE: prototypes, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
|
|
|
|
# ============================================================
|
|
# Build
|
|
# ============================================================
|
|
|
|
build:
|
|
desc: "Production build (default mode)"
|
|
deps: [prepare]
|
|
cmds:
|
|
- npx vite build editor
|
|
|
|
build:core:
|
|
desc: "Build for core mode"
|
|
deps: [prepare]
|
|
cmds:
|
|
- npx vite build editor --mode core
|
|
|
|
build:proprietary:
|
|
desc: "Build for proprietary mode"
|
|
deps: [prepare]
|
|
vars:
|
|
PREVIEW: '{{.PREVIEW | default ""}}'
|
|
cmds:
|
|
- '{{if .PREVIEW}}VITE_BUILD_FOR_PREVIEW=1 {{end}}npx vite build editor --mode proprietary'
|
|
|
|
build:saas:
|
|
desc: "Build for SaaS mode"
|
|
deps:
|
|
- task: prepare
|
|
vars: { MODE: saas }
|
|
cmds:
|
|
- npx vite build editor --mode saas
|
|
|
|
build:desktop:
|
|
desc: "Build for desktop mode"
|
|
deps:
|
|
- task: prepare
|
|
vars: { MODE: desktop }
|
|
cmds:
|
|
- npx vite build editor --mode desktop
|
|
|
|
build:prototypes:
|
|
desc: "Build for prototypes mode"
|
|
deps: [prepare]
|
|
cmds:
|
|
- npx vite build editor --mode prototypes
|
|
|
|
|
|
storybook:
|
|
desc: "Start Storybook dev server"
|
|
deps: [prepare]
|
|
cmds:
|
|
- npx storybook dev -p 6006 {{.CLI_ARGS}}
|
|
|
|
storybook:build:
|
|
desc: "Build static Storybook"
|
|
deps: [prepare]
|
|
cmds:
|
|
- npx storybook build {{.CLI_ARGS}}
|
|
|
|
storybook:browser:
|
|
internal: true
|
|
desc: "Install the Chromium build the story scan runs in"
|
|
run: once
|
|
deps: [install]
|
|
cmds:
|
|
- npx playwright install chromium
|
|
|
|
storybook:test:
|
|
desc: "Scan every story in real Chromium: it must render and pass axe"
|
|
deps: [prepare, storybook:browser]
|
|
cmds:
|
|
# Runs each story as a browser test. Pass a filter through, e.g.
|
|
# task frontend:storybook:test -- Button
|
|
- npx vitest run --config .storybook/vitest.config.ts {{.CLI_ARGS}}
|
|
|
|
storybook:a11y:light:
|
|
desc: "a11y gate over every story in light mode"
|
|
deps: [prepare, storybook:browser]
|
|
cmds:
|
|
- node .storybook/a11y-scan.mjs {{.CLI_ARGS}}
|
|
- node .storybook/a11y-check.mjs --in .a11y-scan --manifest .a11y-scan/manifest.txt
|
|
|
|
storybook:a11y:dark:
|
|
desc: "a11y gate over every story in dark mode"
|
|
deps: [prepare, storybook:browser]
|
|
cmds:
|
|
- SCAN_THEME=dark node .storybook/a11y-scan.mjs {{.CLI_ARGS}}
|
|
- node .storybook/a11y-check.mjs --in .a11y-scan --manifest .a11y-scan/manifest.txt --baseline .storybook/a11y-baseline.dark.json
|
|
|
|
storybook:a11y:
|
|
desc: "a11y gate over every story, light and dark"
|
|
cmds:
|
|
- task: storybook:a11y:light
|
|
- task: storybook:a11y:dark
|
|
|
|
storybook:a11y:changed:
|
|
desc: "a11y gate over the stories this branch affects (default base origin/main)"
|
|
summary: |
|
|
Scans the stories a branch affects, which is what pull requests run — a
|
|
full scan takes ~30 minutes, far too long to sit in front of every merge.
|
|
A story is affected if its file changed, or if a same-named sibling
|
|
source file changed (editing Button.tsx or Button.css re-scans
|
|
Button.stories.tsx — the story renders the live component, so a component
|
|
edit changes what the story shows without touching the story file).
|
|
Changes that ripple further than a component's own stories are covered by
|
|
the nightly full sweep.
|
|
|
|
Pass a base ref through CLI_ARGS, e.g.
|
|
task frontend:storybook:a11y:changed -- origin/release
|
|
vars:
|
|
BASE: '{{.CLI_ARGS | default "origin/main"}}'
|
|
CHANGED:
|
|
sh: node .storybook/a11y-changed.mjs {{.CLI_ARGS | default "origin/main"}}
|
|
cmds:
|
|
- cmd: |
|
|
if [ -z '{{.CHANGED}}' ]; then
|
|
echo "a11y: no story files affected vs {{.BASE}} — nothing to check"
|
|
exit 0
|
|
fi
|
|
rc=0
|
|
task frontend:storybook:a11y:light -- {{.CHANGED}} || rc=1
|
|
task frontend:storybook:a11y:dark -- {{.CHANGED}} || rc=1
|
|
exit $rc
|
|
|
|
storybook:a11y:record:
|
|
desc: "Re-record both a11y baselines (run after intentionally fixing/adding violations)"
|
|
deps: [prepare, storybook:browser]
|
|
cmds:
|
|
- node .storybook/a11y-scan.mjs
|
|
- node .storybook/a11y-check.mjs --in .a11y-scan --manifest .a11y-scan/manifest.txt --record
|
|
- SCAN_THEME=dark node .storybook/a11y-scan.mjs
|
|
- node .storybook/a11y-check.mjs --in .a11y-scan --manifest .a11y-scan/manifest.txt --record --baseline .storybook/a11y-baseline.dark.json
|
|
|
|
# ============================================================
|
|
# Code quality
|
|
# ============================================================
|
|
|
|
lint:
|
|
desc: "Run linting"
|
|
deps: [install]
|
|
cmds:
|
|
- task: lint:oxlint
|
|
- task: lint:colors
|
|
- task: lint:css
|
|
|
|
lint:css:
|
|
desc: "Lint stylesheets for duplicate selectors"
|
|
deps: [install]
|
|
cmds:
|
|
# Covers the whole editor tree, including the portal/processor layer and
|
|
# public/css. Vendored CSS and build output are excluded via ignoreFiles
|
|
# in stylelint.config.mjs.
|
|
- npx stylelint "editor/**/*.css"
|
|
|
|
lint:colors:
|
|
desc: "Enforce theme tokens — no hardcoded colours or raw primitives in components"
|
|
aliases: [lint:colours]
|
|
deps: [install]
|
|
cmds:
|
|
- node editor/scripts/lint/theme-lint.mjs
|
|
- node editor/scripts/lint/theme-lint.mjs css-colors
|
|
- node editor/scripts/lint/theme-lint.mjs code-colors
|
|
- node editor/scripts/lint/theme-lint.mjs no-primitives
|
|
|
|
contrast:
|
|
desc: "Report low-contrast theme token pairs (warning only, never blocks)"
|
|
deps: [install]
|
|
cmds:
|
|
- node editor/scripts/lint/theme-lint.mjs contrast
|
|
|
|
lint:oxlint:
|
|
desc: "Run oxlint linting"
|
|
deps: [install]
|
|
cmds:
|
|
- npx oxlint --config oxlint.config.ts --max-warnings=0
|
|
|
|
lint:fix:
|
|
desc: "Auto-fix lint issues"
|
|
deps: [install]
|
|
cmds:
|
|
- npx oxlint --config oxlint.config.ts --fix
|
|
|
|
format:
|
|
desc: "Auto-fix code formatting"
|
|
deps: [install]
|
|
cmds:
|
|
- npx oxfmt --write .
|
|
|
|
format:check:
|
|
desc: "Check code formatting"
|
|
deps: [install]
|
|
cmds:
|
|
- npx oxfmt --check .
|
|
|
|
fix:
|
|
desc: "Auto-fix lint and format"
|
|
cmds:
|
|
- task: format
|
|
- task: lint:fix
|
|
|
|
typecheck:
|
|
desc: "Typecheck default build of the app"
|
|
cmds:
|
|
- task: typecheck:proprietary
|
|
|
|
typecheck:_run:
|
|
internal: true
|
|
cmds:
|
|
- 'npx tsc --noEmit --project {{.PROJECT}}'
|
|
|
|
typecheck:core:
|
|
desc: "Typecheck core build variant"
|
|
deps: [prepare]
|
|
cmds:
|
|
- task: typecheck:_run
|
|
vars: { PROJECT: editor/src/core/tsconfig.json }
|
|
|
|
typecheck:proprietary:
|
|
desc: "Typecheck proprietary build variant"
|
|
deps: [prepare]
|
|
cmds:
|
|
- task: typecheck:_run
|
|
vars: { PROJECT: editor/src/proprietary/tsconfig.json }
|
|
|
|
typecheck:saas:
|
|
desc: "Typecheck SaaS build variant"
|
|
deps:
|
|
- task: prepare
|
|
vars: { MODE: saas }
|
|
cmds:
|
|
- task: typecheck:_run
|
|
vars: { PROJECT: editor/src/saas/tsconfig.json }
|
|
|
|
typecheck:desktop:
|
|
desc: "Typecheck desktop build variant"
|
|
deps:
|
|
- task: prepare
|
|
vars: { MODE: desktop }
|
|
cmds:
|
|
- task: typecheck:_run
|
|
vars: { PROJECT: editor/src/desktop/tsconfig.json }
|
|
|
|
typecheck:cloud:
|
|
desc: "Typecheck cloud shared layer (standalone)"
|
|
deps: [prepare]
|
|
cmds:
|
|
- task: typecheck:_run
|
|
vars: { PROJECT: editor/src/cloud/tsconfig.json }
|
|
|
|
typecheck:scripts:
|
|
desc: "Typecheck scripts"
|
|
deps: [prepare]
|
|
cmds:
|
|
- task: typecheck:_run
|
|
vars: { PROJECT: editor/scripts/tsconfig.json }
|
|
|
|
typecheck:prototypes:
|
|
desc: "Typecheck prototypes build variant"
|
|
deps: [prepare]
|
|
cmds:
|
|
- task: typecheck:_run
|
|
vars: { PROJECT: editor/src/prototypes/tsconfig.json }
|
|
|
|
typecheck:portal:
|
|
desc: "Typecheck developer portal build variant"
|
|
deps: [install]
|
|
cmds:
|
|
- task: typecheck:_run
|
|
vars: { PROJECT: editor/src/portal/tsconfig.json }
|
|
|
|
typecheck:storybook:
|
|
desc: "Typecheck Storybook config and stories"
|
|
deps: [prepare]
|
|
cmds:
|
|
- task: typecheck:_run
|
|
vars: { PROJECT: .storybook/tsconfig.json }
|
|
|
|
typecheck:all:
|
|
desc: "Typecheck all build variants"
|
|
cmds:
|
|
- task: typecheck:core
|
|
- task: typecheck:proprietary
|
|
- task: typecheck:saas
|
|
- task: typecheck:desktop
|
|
- task: typecheck:cloud
|
|
- task: typecheck:scripts
|
|
- task: typecheck:prototypes
|
|
- task: typecheck:portal
|
|
- task: typecheck:storybook
|
|
|
|
# ============================================================
|
|
# Quality Gate
|
|
# ============================================================
|
|
|
|
check:
|
|
desc: "Quick quality gate for local development"
|
|
cmds:
|
|
- task: typecheck
|
|
- task: lint
|
|
- task: format:check
|
|
- task: test
|
|
|
|
og:check:
|
|
desc: "Fail if committed OG/social-preview metadata is out of date"
|
|
cmds:
|
|
- node editor/scripts/generate-og-metadata.mjs --check
|
|
|
|
check:all:
|
|
desc: "Full CI quality gate"
|
|
cmds:
|
|
# Runs first, before prepare regenerates: guards the committed og-metadata.json /
|
|
# ogImageMap.json that the Cloudflare Pages (plain `vite build`) deploy relies on.
|
|
- task: og:check
|
|
- task: typecheck:all
|
|
- task: lint
|
|
- task: format:check
|
|
- task: build
|
|
- task: test
|
|
- task: storybook:build
|
|
|
|
# ============================================================
|
|
# Test
|
|
# ============================================================
|
|
|
|
test:
|
|
desc: "Run tests"
|
|
cmds:
|
|
- task: test:editor
|
|
|
|
test:editor:
|
|
desc: "Run editor tests"
|
|
deps: [prepare]
|
|
vars:
|
|
COVERAGE: '{{.COVERAGE | default .CI | default "false"}}'
|
|
cmds:
|
|
- >
|
|
npx vitest run --root editor
|
|
{{if eq .COVERAGE "true"}}--coverage
|
|
--coverage.provider=v8
|
|
--coverage.reporter=text-summary
|
|
--coverage.reporter=json-summary
|
|
--coverage.reporter=html
|
|
--coverage.reportsDirectory=./coverage{{end}}
|
|
|
|
test:watch:
|
|
desc: "Run tests in watch mode"
|
|
deps: [prepare]
|
|
cmds:
|
|
- npx vitest --watch --root editor
|
|
|
|
test:coverage:
|
|
desc: "Run tests with coverage (one-shot; CI-friendly)."
|
|
cmds:
|
|
- task: test:editor
|
|
vars: { COVERAGE: "true" }
|
|
|
|
# ============================================================
|
|
# Code Generation
|
|
# ============================================================
|
|
|
|
tool-models:
|
|
desc: "Generate tool API types from the Java OpenAPI spec"
|
|
deps: [install, ":backend:swagger"]
|
|
cmds:
|
|
- npx tsx editor/scripts/generate-tool-api-types.mts --spec ../SwaggerDoc.json --output editor/src/core/types/toolApiTypes.ts --io-output editor/src/core/types/toolIO.ts
|
|
- task: format
|
|
sources:
|
|
- editor/scripts/generate-tool-api-types.mts
|
|
- ../SwaggerDoc.json
|
|
generates:
|
|
- editor/src/core/types/toolApiTypes.ts
|
|
- editor/src/core/types/toolIO.ts
|
|
|
|
tool-models:check:
|
|
desc: "Fail if committed tool API types are out of date"
|
|
cmds:
|
|
- task: tool-models
|
|
- git diff --exit-code -- editor/src/core/types/toolApiTypes.ts editor/src/core/types/toolIO.ts
|
|
|
|
licenses:generate:
|
|
desc: "Generate frontend license report"
|
|
deps: [install]
|
|
cmds:
|
|
- node editor/scripts/generate-licenses.js
|
|
|
|
# ============================================================
|
|
# Clean
|
|
# ============================================================
|
|
|
|
clean:
|
|
desc: "Clean build artifacts and caches"
|
|
cmds:
|
|
- cmd: powershell rm -Recurse -Force -ErrorAction SilentlyContinue node_modules/.vite, editor/dist, dist
|
|
platforms: [windows]
|
|
- cmd: rm -rf node_modules/.vite editor/dist dist
|
|
platforms: [linux, darwin]
|