@cellajs/cli 0.6.4 → 0.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -59,7 +59,7 @@ Per-command help: `pnpm cella <command> --help`.
59
59
 
60
60
  | Command | Useful options |
61
61
  |---------|----------------|
62
- | testbed | `stop`, `--fresh`, `-- <command>` |
62
+ | testbed | `stop`, `--fresh`, `--showcase`, `-- <command>` |
63
63
  | bench | `[scenario]`, `--all`, `--short`, `--isolated`, `--skip-seed` |
64
64
  | a11y | `report`, `decide`, `--states <ids>`, `--workers <n>`, `--email <address>`, `--publish`, `--draft`, `--file <path>` |
65
65
  | screenshots | `<ids...>`, `--out <dir>`, `--email <address>` |
@@ -91,6 +91,7 @@ database container.
91
91
  | `pnpm cella testbed` | Start a testbed and hold it |
92
92
  | `pnpm cella testbed -- <command>` | Run a command against it: the one this checkout holds, or one started for the command and stopped after. The command gets the testbed's ports (`DEV_PORT_OFFSET`) and database URLs in its environment |
93
93
  | `pnpm cella testbed --fresh` | Start from an empty database, seeded again |
94
+ | `pnpm cella testbed --showcase` | Start one on the app's showcase and hold it, for a screenshot review loop |
94
95
  | `pnpm cella testbed stop` | Stop the running testbed, or the servers one left behind after a crash |
95
96
 
96
97
  One testbed runs per app on a machine, since every checkout of the app shares its database: a second start, from
@@ -98,6 +99,11 @@ another worktree too, says which checkout holds it. The servers write to `testbe
98
99
  start that fails prints the end of that log. The testbed needs Docker and an app whose `backend/compose.yaml` has the
99
100
  `db_testbed` service.
100
101
 
102
+ The database holds either the app's seed or its showcase: the curated `backend/scripts/seeds/showcase.json` that the screenshots shoot,
103
+ seeded by `pnpm seed showcase`. A start that asks for the other one seeds a new database, and so does every showcase
104
+ start, since the showcase's times are relative to its seed. A running testbed serves only commands that want what it
105
+ holds.
106
+
101
107
  ## Tools and their adapters
102
108
 
103
109
  A tool's engine ships in this package, and what is app-specific sits in the app's `cella/` folder as an adapter: a base
@@ -194,8 +200,10 @@ A short script can open one state of the scope, signed in, to check a fix: `open
194
200
 
195
201
  `pnpm cella screenshots` shoots the app's marketing screenshots on a testbed, a light and a dark file each, over the
196
202
  files the app ships, and prints each file's pixel and byte size. Ids pick some shots (`pnpm cella screenshots settings`),
197
- `--out <dir>` writes a trial run elsewhere. On the testbed it also turns MFA on for the shot user and a share of their
198
- members, so the pages show both states; nobody can sign in with those factors. WebP is written lossless by the system
203
+ `--out <dir>` writes a trial run elsewhere. An app with a `backend/scripts/seeds/showcase.json` is shot on a testbed seeded with that
204
+ showcase, which no other tool writes to, as its admin; without one the testbed holds the app's seed, and the command turns
205
+ MFA on for the shot user and a share of their members through the backend's `showcase:mfa` script where the app has it.
206
+ Nobody can sign in with those factors. WebP is written lossless by the system
199
207
  `cwebp` encoder, checked before the testbed starts. `cella/screenshots.ts` lists the template's shots and devices, and
200
208
  `cella/screenshots.app.ts` the app's own. The cella template's `screenshots` skill is the rubric for judging them.
201
209
 
@@ -209,7 +217,7 @@ database and gets its URLs.
209
217
 
210
218
  ## Translations
211
219
 
212
- `pnpm cella i18n` reads `locales/<language>/<file>.json` the way the app loads them: `locales/namespaces.json` names the
220
+ `pnpm cella i18n` reads `shared/locales/<language>/<file>.json` the way the app loads them: `namespaces.json` there names the
213
221
  runtime namespaces that merge several files (`c` is `common.json` with `app.json` over it), and every other file is a
214
222
  namespace of its own. English holds every key, and each other language is measured against it.
215
223
 
package/dist/a11y.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/tools/drive.ts","../src/utils/app-facts.ts","../src/utils/env.ts","../src/tools/a11y/define.ts","../src/tools/adapters.ts","../src/tools/a11y/session.ts","../src/tools/a11y/scope.ts"],"sourcesContent":["import { spawnSync } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport process from 'node:process';\nimport type { Browser, BrowserContext, BrowserContextOptions, Page } from 'playwright';\nimport { importFromWorkspace } from '../utils/app-facts';\n\nexport type { Browser, BrowserContext, BrowserContextOptions, Page } from 'playwright';\n\nexport type Playwright = typeof import('playwright');\n\n/** A signed-in user of the app: where it answers and the cookie that signs a browser or a request in. */\nexport interface Session {\n email: string;\n frontendUrl: string;\n /** The API, a path under the frontend's origin. */\n backendUrl: string;\n /** The app's slug, read off the cookie name; the persisted UI store's key starts with it. */\n slug: string;\n /** In the shape `context.addCookies` takes. */\n cookie: { name: string; value: string; url: string; httpOnly: true; sameSite: 'Strict' };\n /** `name=value`, for a request's cookie header. */\n header: string;\n}\n\nexport interface MintOptions {\n /** The app's root; default the current directory. */\n forkPath?: string;\n /** The environment the mint script runs in: a testbed's names its database. Default this process's. */\n env?: NodeJS.ProcessEnv;\n /** Session lifetime; default 2. */\n hours?: number;\n}\n\n/** Playwright as the app's frontend installed it, so the CLI ships none. */\nexport function loadPlaywright(forkPath = process.cwd()): Promise<Playwright> {\n return importFromWorkspace<Playwright>(forkPath, 'frontend', 'playwright');\n}\n\n/**\n * Checks that Playwright has its Chromium, before anything slow starts.\n * @throws with the install command when it has none\n */\nexport async function requireChromium(forkPath = process.cwd()): Promise<void> {\n const { chromium } = await loadPlaywright(forkPath);\n if (!existsSync(chromium.executablePath())) {\n throw new Error('Playwright has no Chromium yet: run `pnpm --filter frontend exec playwright install chromium`');\n }\n}\n\n/**\n * Signs `email` in through the backend's `session:mint` script, which refuses outside development and test. The script\n * prints `<cookie name>=<signed value>` and a curl line with the API URL, both from the config of the environment it runs\n * in, so a testbed's shifted ports and a linked worktree's are written down nowhere.\n * @throws when the script prints no session, as for an email with no user\n */\nexport function mintSession(email: string, { forkPath = process.cwd(), env, hours = 2 }: MintOptions = {}): Session {\n const minted = spawnSync('pnpm', ['--silent', '--filter', 'backend', 'session:mint', email, String(hours)], {\n cwd: forkPath,\n env,\n encoding: 'utf8',\n });\n const cookie = /^((?:__Host-)?([\\w-]+)-session-v\\d+)=(\\S+)$/m.exec(minted.stdout);\n const api = /^curl (\\S+)\\/me /m.exec(minted.stdout);\n if (!cookie || !api) throw new Error(`could not mint a session for ${email}:\\n${minted.stdout}\\n${minted.stderr}`);\n const frontendUrl = new URL(api[1]).origin;\n return {\n email,\n frontendUrl,\n backendUrl: api[1],\n slug: cookie[2],\n cookie: { name: cookie[1], value: cookie[3], url: frontendUrl, httpOnly: true, sameSite: 'Strict' },\n header: `${cookie[1]}=${cookie[3]}`,\n };\n}\n\n/**\n * Signs a minted session out, so a finished run leaves no live session in the user's sessions list. A JSON content type\n * passes the CSRF check without an Origin header; a stack that is already down leaves the session to expire.\n */\nexport async function signOut(session: Session | null | undefined): Promise<void> {\n if (!session) return;\n const headers = { cookie: session.header, 'content-type': 'application/json' };\n await fetch(`${session.backendUrl}/auth/sign-out`, { method: 'POST', headers }).catch(() => {});\n}\n\nexport interface ContextOptions extends BrowserContextOptions {\n /** Signs the context in. */\n session?: Session;\n /**\n * Fields written into the app's persisted UI store (`<slug>-ui`) before any page script runs, so the first paint is\n * already in the mode they set. Merged into what the store holds: a list joins the stored one, any other value wins.\n */\n uiStore?: { slug: string; state: Record<string, unknown> };\n}\n\n/** A browser context, signed in and with its UI store seeded when asked. */\nexport async function newContext(\n browser: Browser,\n { session, uiStore, ...options }: ContextOptions = {},\n): Promise<BrowserContext> {\n const context = await browser.newContext(options);\n // tsx and esbuild's keepNames wrap named functions in an `__name` helper that a page script does not have\n await context.addInitScript({ content: 'window.__name = (fn) => fn;' });\n if (session) await context.addCookies([session.cookie]);\n if (uiStore) {\n await context.addInitScript(\n ([key, fields]) => {\n const stored = JSON.parse(localStorage.getItem(key) ?? '{}');\n const state = { ...stored.state };\n for (const [name, value] of Object.entries(fields)) {\n const before = state[name];\n state[name] = Array.isArray(value) && Array.isArray(before) ? [...new Set([...before, ...value])] : value;\n }\n // A fresh entry needs the store's own version, or zustand drops it as written by an older build\n localStorage.setItem(key, JSON.stringify({ version: 1, ...stored, state }));\n },\n [`${uiStore.slug}-ui`, uiStore.state] as const,\n );\n }\n return context;\n}\n\n/**\n * Resolves once the page has stopped adding or changing content for `quiet` ms, or after `cap` ms. Lazy sections, query\n * results and opening overlays all show up as DOM changes; the network never goes idle in development.\n */\nexport async function settle(page: Page, { quiet = 400, cap = 3000 } = {}): Promise<void> {\n const waited = page.evaluate(\n ([quietMs, capMs]) =>\n new Promise<void>((resolve) => {\n const done = () => {\n observer.disconnect();\n clearTimeout(timer);\n clearTimeout(limit);\n resolve();\n };\n let timer = setTimeout(done, quietMs);\n const limit = setTimeout(done, capMs);\n const observer = new MutationObserver(() => {\n clearTimeout(timer);\n timer = setTimeout(done, quietMs);\n });\n observer.observe(document, { subtree: true, childList: true, characterData: true });\n }),\n [quiet, cap] as const,\n );\n // A navigation during the wait tears down the page's script context\n await waited.catch(() => page.waitForTimeout(quiet));\n}\n\n/**\n * Opens `url` in a new page and settles once the route has rendered its content.\n * @throws when the route redirects: a check or a shot would judge another page under this one's name\n */\nexport async function openPage(context: BrowserContext, url: string, { timeout = 30_000 } = {}): Promise<Page> {\n const page = await context.newPage();\n await page.goto(url, { waitUntil: 'domcontentloaded', timeout });\n await page.locator('main, [role=\"main\"], #accessibility-content, form, h1, footer').first().waitFor({ timeout });\n await settle(page);\n const landed = new URL(page.url()).pathname;\n const asked = new URL(url).pathname;\n if (landed !== asked) throw new Error(`${asked} redirected to ${landed}`);\n return page;\n}\n","import { execFileSync, spawnSync } from 'node:child_process';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport { dirname, join, resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { z } from 'zod';\nimport { setEnv } from './env';\n\nconst appConfigSchema = z.object({\n frontendUrl: z.string(),\n backendUrl: z.string(),\n devPortOffset: z.number(),\n services: z.record(\n z.string(),\n z.object({ enabled: z.boolean(), port: z.number(), internalPort: z.number().optional() }),\n ),\n});\n\n/** The part of the app's development config the develop commands read. */\nexport type AppFacts = z.infer<typeof appConfigSchema>;\n\n/** The app's local database helpers (`shared/src/local-database.ts`), as the develop commands call them. */\nexport interface LocalDatabases {\n port: (database: string) => number;\n url: (database: string, role: 'postgres' | 'runtime_role' | 'admin_role') => string;\n /** The env `backend/compose.yaml` reads: the project name and each database's host port. */\n composeEnv: () => Record<string, string>;\n}\n\n/** Node strips the types of a file outside node_modules, so the app's own TS loads without a build or tsx. */\nexport async function importFromApp(forkPath: string, file: string): Promise<Record<string, unknown>> {\n const path = join(forkPath, file);\n if (!existsSync(path)) throw new Error(`not an app built from cella: ${path} is missing`);\n // The app's config reads the mode once, at its first import, and the dev port offset applies in development mode only\n setEnv('NODE_ENV', 'development');\n return import(pathToFileURL(path).href);\n}\n\n/**\n * The app's config in development mode, from its own `shared` package. In a linked worktree the import claims that\n * worktree's port slot, as `pnpm dev` would.\n */\nexport async function loadAppFacts(forkPath: string): Promise<AppFacts> {\n const parsed = appConfigSchema.safeParse((await importFromApp(forkPath, 'shared/index.ts')).appConfig);\n if (!parsed.success)\n throw new Error(`shared/index.ts exports no appConfig this CLI can read: ${parsed.error.message}`);\n return parsed.data;\n}\n\n/** The app's local database helpers. */\nexport async function loadLocalDatabases(forkPath: string): Promise<LocalDatabases> {\n const module = await importFromApp(forkPath, 'shared/src/local-database.ts');\n const { localDatabasePort, localDatabaseUrl, composeEnv } = module;\n if (\n typeof localDatabasePort !== 'function' ||\n typeof localDatabaseUrl !== 'function' ||\n typeof composeEnv !== 'function'\n ) {\n throw new Error('shared/src/local-database.ts lacks localDatabasePort, localDatabaseUrl or composeEnv');\n }\n return {\n port: (database) => localDatabasePort(database),\n url: (database, role) => localDatabaseUrl(database, role),\n composeEnv: () => composeEnv(),\n };\n}\n\n/**\n * A package as the app's `workspace` installed it, so a tool drives the app's own Playwright, TypeScript or Tailwind and\n * the CLI ships none. The package's ESM entry when it has one, else its CommonJS exports.\n * @throws when the workspace does not have the package installed\n */\nexport async function importFromWorkspace<T>(forkPath: string, workspace: string, name: string): Promise<T> {\n const require = createRequire(join(forkPath, workspace, 'package.json'));\n let manifestPath: string | undefined;\n try {\n manifestPath = require.resolve(`${name}/package.json`);\n } catch {\n // A package whose exports leave out package.json resolves by its main entry below\n }\n const root = manifestPath ? JSON.parse(readFileSync(manifestPath, 'utf8')).exports?.['.'] : undefined;\n const esm = typeof root?.import === 'string' ? root.import : root?.import?.default;\n if (manifestPath && typeof esm === 'string') return import(pathToFileURL(join(dirname(manifestPath), esm)).href);\n try {\n return require(name);\n } catch {\n throw new Error(`${name} is not installed in ${workspace}: run \\`pnpm --filter ${workspace} add -D ${name}\\``);\n }\n}\n\n/**\n * A Docker image as a tool runs it, pinned by digest (`name[:tag]@sha256:…`) so every run scans with the same code.\n * Pulls it on first use; the pull shows Docker's own progress.\n * @throws when the reference has no digest, Docker does not answer, or the pull fails\n */\nexport function resolveImage(reference: string): string {\n if (!/@sha256:[0-9a-f]{64}$/.test(reference)) throw new Error(`${reference} is not pinned by digest`);\n if (spawnSync('docker', ['info'], { stdio: 'ignore' }).status !== 0) {\n throw new Error('Docker does not answer: start Docker (or OrbStack) first');\n }\n if (spawnSync('docker', ['image', 'inspect', reference], { stdio: 'ignore' }).status === 0) return reference;\n if (spawnSync('docker', ['pull', reference], { stdio: ['ignore', 'inherit', 'inherit'] }).status !== 0) {\n throw new Error(`could not pull ${reference}`);\n }\n return reference;\n}\n\n/**\n * Where a tool keeps its results and state: `<git dir>/cella-<tool>/` of this checkout, or of the git directory every\n * worktree shares. Inside the git directory nothing shows in `git status`.\n */\nexport function toolDir(forkPath: string, tool: string, scope: 'checkout' | 'repository' = 'checkout'): string {\n const flag = scope === 'checkout' ? '--absolute-git-dir' : '--git-common-dir';\n const gitDir = execFileSync('git', ['rev-parse', flag], { cwd: forkPath, encoding: 'utf8' }).trim();\n return join(resolve(forkPath, gitDir), `cella-${tool}`);\n}\n","import process from 'node:process';\n\n/**\n * Centralized environment access for CLI/runtime integration points.\n */\nexport function getEnv(name: string): string | undefined {\n // biome-ignore lint/style/noProcessEnv: This module centralizes intentional CLI environment access.\n return process.env[name];\n}\n\n/** Whether the variable is set at all, even to an empty string. */\nexport function hasEnv(name: string): boolean {\n // biome-ignore lint/style/noProcessEnv: This module centralizes intentional CLI environment access.\n return name in process.env;\n}\n\n/** A mutable copy of the full environment, for passing to child processes. */\nexport function getEnvSnapshot(): NodeJS.ProcessEnv {\n // biome-ignore lint/style/noProcessEnv: This module centralizes intentional CLI environment access.\n return { ...process.env };\n}\n\n/** Set a variable for this process and the children it spawns from here on. */\nexport function setEnv(name: string, value: string): void {\n // biome-ignore lint/style/noProcessEnv: This module centralizes intentional CLI environment access.\n process.env[name] = value;\n}\n","import { z } from 'zod';\nimport { fn, type Placeholders, type Prepare, placeholdersSchema } from '../adapters';\nimport type { BrowserContextOptions, Page } from '../drive';\n\ntype Opener = (page: Page) => Promise<void>;\n\n/** A form probe a state runs on its page. */\nexport type FormCheck =\n /** Submitting the form empty identifies each error in text (3.3.1). */\n | 'required'\n /** Inputs for the user's own details declare their purpose (1.3.5). */\n | 'personal'\n /** Sign-in fields accept autofill and pasted text (3.3.8). */\n | 'sign-in';\n\n/** A page or state of the audit scope. */\nexport interface A11yState {\n id: string;\n /** Path on the frontend; a `{name}` placeholder takes the value its resolver returns. */\n path: string;\n auth: boolean;\n /** Brings the page into the state to audit, such as an open dialog. Runs after the page has loaded. */\n open?: Opener;\n /** `open` leaves an overlay (dialog, sheet, menu) open that holds focus until it closes. */\n overlay?: true;\n /** Browser settings of this state's visits, such as a viewport the state only exists at. */\n context?: BrowserContextOptions;\n forms?: FormCheck[];\n}\n\n/** `cella/a11y.ts`: the template's scope, synced with the UI it describes. */\nexport interface A11yBase {\n /** Brings the audit user into the state the pages expect, before the first visit. */\n prepare?: Prepare;\n /** Values for the `{name}` placeholders of the paths, read as the audit user before the first visit. */\n placeholders?: Placeholders;\n /** What the code checks search, as `git grep` pathspecs from the app's root. */\n sources: string[];\n /** The sample of pages and states the audit covers (WCAG-EM steps 2 and 3), in report order. */\n scope: A11yState[];\n}\n\n/** `cella/a11y.app.ts`, optional: what an app adds to the base, removes from it or overrides. */\nexport interface A11yApp {\n /** Runs after the base's. */\n prepare?: Prepare;\n /** Joins the base's; a name in both takes this one. */\n placeholders?: Placeholders;\n /** Base state ids the app does not have. */\n remove?: string[];\n /** States of the app's own; one with a base state's id replaces it in place. */\n add?: A11yState[];\n}\n\nconst stateSchema = z.strictObject({\n id: z.string().min(1),\n path: z.string().startsWith('/').or(z.string().startsWith('{')),\n auth: z.boolean(),\n open: fn<Opener>().optional(),\n overlay: z.literal(true).optional(),\n context: z.custom<BrowserContextOptions>((value) => typeof value === 'object' && value !== null).optional(),\n forms: z.array(z.enum(['required', 'personal', 'sign-in'])).optional(),\n}) satisfies z.ZodType<A11yState>;\n\nexport const baseSchema = z.strictObject({\n prepare: fn<Prepare>().optional(),\n placeholders: placeholdersSchema.optional(),\n sources: z.array(z.string()).min(1),\n scope: z.array(stateSchema).min(1),\n}) satisfies z.ZodType<A11yBase>;\n\nexport const appSchema = z.strictObject({\n prepare: fn<Prepare>().optional(),\n placeholders: placeholdersSchema.optional(),\n remove: z.array(z.string()).optional(),\n add: z.array(stateSchema).optional(),\n}) satisfies z.ZodType<A11yApp>;\n\n/**\n * The audit measures the app with increased contrast turned on, which is what raises the edge tokens to 3:1. It is a\n * user setting, not the default, so every report built from a run says so: the ledger records it and the report prints\n * it. `prepare` stores it for the audit user, since `/me` overwrites the seeded UI store on every authenticated page.\n */\nexport const auditContrast = 'more' as const;\n\nexport const defineA11yBase = (base: A11yBase): A11yBase => base;\n\nexport const defineA11yApp = (app: A11yApp): A11yApp => app;\n","import { existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { z } from 'zod';\nimport { importFromWorkspace } from '../utils/app-facts';\nimport type { Session } from './drive';\n\n/** What `prepare` and a placeholder resolver get: the signed-in tool user, to call the API as. */\nexport type SignedIn = Pick<Session, 'email' | 'frontendUrl' | 'backendUrl'> & {\n /** `name=value`: the cookie header that signs a request in. */\n cookie: string;\n};\n\nexport const signedIn = ({ email, frontendUrl, backendUrl, header }: Session): SignedIn => ({\n email,\n frontendUrl,\n backendUrl,\n cookie: header,\n});\n\n/** Brings the tool user into the state the pages expect, before the first visit. */\nexport type Prepare = (user: SignedIn) => Promise<void>;\n\n/**\n * Values for the `{name}` placeholders of the paths, by name. A resolver that finds nothing returns null, and a path\n * that needs its value then fails with a message naming it.\n */\nexport type Placeholders = Record<string, (user: SignedIn) => Promise<string | null>>;\n\n/** A zod schema that accepts any function; the adapter's types say which. */\nexport const fn = <T>() => z.custom<T>((value) => typeof value === 'function', 'expected a function');\n\nexport const placeholdersSchema = z.record(z.string(), fn<Placeholders[string]>());\n\n/** Fills the `{name}` placeholders of `path`. */\nexport function fillPath(path: string, values: Record<string, string | null>, file: string): string {\n return path.replace(/\\{(\\w+)\\}/g, (_, name: string) => {\n const value = values[name];\n if (!value) throw new Error(`no value for {${name}}: its resolver in ${file} found nothing for the signed-in user`);\n return value;\n });\n}\n\ninterface TsxApi {\n tsImport: (specifier: string, parentURL: string) => Promise<Record<string, unknown>>;\n}\n\n/**\n * Imports an adapter file under the app's own tsx. An adapter imports the app's SDK, whose generated files import each\n * other without an extension, and Node's own type stripping resolves no such import.\n */\nasync function importAdapter(forkPath: string, file: string): Promise<unknown> {\n const { tsImport } = await importFromWorkspace<TsxApi>(forkPath, '.', 'tsx/esm/api');\n const module = await tsImport(pathToFileURL(join(forkPath, file)).href, import.meta.url);\n return module.default;\n}\n\nfunction parsed<T>(file: string, schema: z.ZodType<T>, value: unknown): T {\n const result = schema.safeParse(value);\n if (result.success) return result.data;\n throw new Error(`${file} does not fit this version of the CLI:\\n${z.prettifyError(result.error)}`);\n}\n\n/**\n * A tool's adapter in the app's `cella/` folder: the base `<tool>.ts`, synced from the template, and the optional delta\n * `<tool>.app.ts`, pinned to the app, each checked against its schema. A missing delta means the base alone.\n * @throws when the base is missing or a file does not fit its schema\n */\nexport async function loadAdapter<Base, App>(\n forkPath: string,\n tool: string,\n baseSchema: z.ZodType<Base>,\n appSchema: z.ZodType<App>,\n): Promise<{ base: Base; app: App | undefined }> {\n const baseFile = `cella/${tool}.ts`;\n const appFile = `cella/${tool}.app.ts`;\n if (!existsSync(join(forkPath, baseFile))) {\n throw new Error(`${baseFile} is missing: upgrade the app from cella to get the ${tool} adapter`);\n }\n const base = parsed(baseFile, baseSchema, await importAdapter(forkPath, baseFile));\n const app = existsSync(join(forkPath, appFile))\n ? parsed(appFile, appSchema, await importAdapter(forkPath, appFile))\n : undefined;\n return { base, app };\n}\n\n/**\n * The base's entries with the delta applied: `remove` drops ids, and an added entry replaces the base entry of its id in\n * place or else comes last.\n * @throws when `remove` names an id the base lacks, so a renamed base entry is noticed\n */\nexport function applyDelta<T extends { id: string }>(\n base: T[],\n { remove = [], add = [] }: { remove?: string[]; add?: T[] },\n file: string,\n): T[] {\n const unknown = remove.filter((id) => !base.some((entry) => entry.id === id));\n if (unknown.length > 0) throw new Error(`${file} removes ${unknown.join(', ')}, which the base does not have`);\n const replaced = base\n .filter((entry) => !remove.includes(entry.id))\n .map((entry) => add.find((added) => added.id === entry.id) ?? entry);\n return [...replaced, ...add.filter((added) => !base.some((entry) => entry.id === added.id))];\n}\n","import process from 'node:process';\nimport type { Browser, BrowserContext, BrowserContextOptions, Page } from 'playwright';\nimport { getEnv } from '../../utils/env';\nimport { fillPath, signedIn } from '../adapters';\nimport { loadPlaywright, mintSession, newContext, openPage, type Session, settle, signOut } from '../drive';\nimport { type A11yState, auditContrast } from './define';\nimport { loadScope, type Scope } from './scope';\n\nexport type Mode = 'light' | 'dark';\n\ntype StorageState = Awaited<ReturnType<BrowserContext['storageState']>>;\n\n/** A signed-in audit: one browser for every visit, and what each visit starts from. */\nexport interface AuditSession {\n browser: Browser;\n user: Session;\n /** Cookies and local storage of a browser that has loaded the app once, as after a real sign-in. */\n storage: StorageState;\n /** Resolved `{name}` placeholders of the scope paths. */\n values: Record<string, string | null>;\n}\n\n/** Elements that exist only in development builds; scans and probes ignore them. */\nexport const devOnlySelectors = [\n '.TanStackRouterDevtools',\n '.tsqd-parent-container',\n '[aria-label=\"toggle debug toolbar\"]',\n];\n\nexport const defaultViewport = { width: 1280, height: 900 };\n\n/**\n * Signs the audit user in, runs the scope's `prepare` steps, resolves its placeholders and starts the browser.\n * `env` points the session mint at the database the app runs on.\n */\nexport async function startAudit(\n forkPath: string,\n scope: Scope,\n { email, env }: { email: string; env?: NodeJS.ProcessEnv },\n): Promise<AuditSession> {\n const user = mintSession(email, { forkPath, env, hours: 4 });\n try {\n for (const prepare of scope.prepare) await prepare(signedIn(user));\n const values: AuditSession['values'] = {};\n for (const [name, resolve] of Object.entries(scope.placeholders)) values[name] = await resolve(signedIn(user));\n\n const { chromium } = await loadPlaywright(forkPath);\n const browser = await chromium.launch();\n // Route guards read the user store, which a real sign-in fills before any deep link is opened\n const context = await newContext(browser, { session: user });\n const page = await context.newPage();\n await page.goto(`${user.frontendUrl}/home`, { waitUntil: 'domcontentloaded' });\n await page.locator('main').first().waitFor({ timeout: 60_000 });\n await settle(page);\n const storage = await context.storageState();\n await context.close();\n return { browser, user, storage, values };\n } catch (error) {\n await signOut(user);\n throw error;\n }\n}\n\n/** Closes the browser and signs the audit user out, so a finished audit leaves no live session behind. */\nexport async function endAudit(session: AuditSession): Promise<void> {\n await session.browser.close();\n await signOut(session.user);\n}\n\n/**\n * A fresh context for one visit: color mode, more contrast, reduced motion, the dev banner dismissed, and signed in\n * when the state needs it. `overrides` change the browser settings, such as the viewport or the motion preference.\n */\nexport function auditContext(\n session: AuditSession,\n { auth, mode }: { auth: boolean; mode: Mode },\n overrides: BrowserContextOptions = {},\n): Promise<BrowserContext> {\n return newContext(session.browser, {\n viewport: defaultViewport,\n colorScheme: mode,\n // Emulates `prefers-contrast`, which tailwind.css answers on its own, so public pages are covered like signed-in ones\n contrast: 'more',\n reducedMotion: 'reduce',\n serviceWorkers: 'block',\n storageState: auth ? session.storage : undefined,\n // `contrast` here is what the app's own `data-contrast` path reads; the context option above covers the media query\n uiStore: {\n slug: session.user.slug,\n state: { mode, theme: 'none', contrast: auditContrast, publicAlertsSeen: ['test-credentials'] },\n },\n ...overrides,\n });\n}\n\n/** Opens a state's path in `context`; its `open` step is the caller's. */\nexport function openStatePath(session: AuditSession, context: BrowserContext, state: A11yState): Promise<Page> {\n const path = fillPath(state.path, session.values, 'the a11y adapter');\n return openPage(context, `${session.user.frontendUrl}${path}`);\n}\n\ninterface OpenStateOptions {\n mode?: Mode;\n /** Over the state's own browser settings, such as a phone or `{ reducedMotion: 'no-preference' }`. */\n context?: BrowserContextOptions;\n /** Default `ADMIN_EMAIL`, which a testbed hands the command it runs. */\n email?: string;\n}\n\n/**\n * Opens a state of the app's audit scope by id, signed in: the start of a short script that checks a fix in the\n * running app. Run the script from the app's root through `pnpm cella testbed -- node <script>`; `close` ends it.\n * @throws when the scope has no state of that id, or no email is given and `ADMIN_EMAIL` is unset\n */\nexport async function openState(\n id: string,\n { mode = 'light', context: overrides, email = getEnv('ADMIN_EMAIL') }: OpenStateOptions = {},\n): Promise<{ page: Page; context: BrowserContext; close: () => Promise<void> }> {\n if (!email) throw new Error('no email: pass one, or run through `pnpm cella testbed --`, which sets ADMIN_EMAIL');\n const forkPath = process.cwd();\n const scope = await loadScope(forkPath);\n const state = scope.states.find((candidate) => candidate.id === id);\n if (!state) throw new Error(`no state \"${id}\" in the a11y scope`);\n const session = await startAudit(forkPath, scope, { email });\n try {\n const context = await auditContext(session, { auth: state.auth, mode }, { ...state.context, ...overrides });\n const page = await openStatePath(session, context, state);\n await state.open?.(page);\n return { page, context, close: () => endAudit(session) };\n } catch (error) {\n await endAudit(session);\n throw error;\n }\n}\n","import type { Page } from 'playwright';\nimport { z } from 'zod';\nimport { importFromApp } from '../../utils/app-facts';\nimport { setEnv } from '../../utils/env';\nimport { applyDelta, loadAdapter, type Placeholders, type Prepare } from '../adapters';\nimport { type A11yState, appSchema, baseSchema } from './define';\n\n/** The audit scope with the app's delta applied. */\nexport interface Scope {\n /** The base's, then the delta's. */\n prepare: Prepare[];\n placeholders: Placeholders;\n sources: string[];\n states: A11yState[];\n}\n\nexport async function loadScope(forkPath: string): Promise<Scope> {\n // The adapters read the app config, which takes the development ports and URLs in development mode only\n setEnv('NODE_ENV', 'development');\n const { base, app } = await loadAdapter(forkPath, 'a11y', baseSchema, appSchema);\n return {\n prepare: [base.prepare, app?.prepare].filter((step) => step !== undefined),\n placeholders: { ...base.placeholders, ...app?.placeholders },\n sources: base.sources,\n states: app ? applyDelta(base.scope, app, 'cella/a11y.app.ts') : base.scope,\n };\n}\n\nconst appConfigSchema = z.object({\n name: z.string(),\n slug: z.string(),\n description: z.string(),\n supportEmail: z.string(),\n frontendUrl: z.string(),\n company: z.object({ name: z.string() }),\n languages: z.array(z.string()).min(1),\n defaultLanguage: z.string(),\n enabledAuthStrategies: z.array(z.string()),\n});\n\n/** The part of the app's config the audit and the report read. */\nexport type AppConfig = z.infer<typeof appConfigSchema>;\n\n/** The app's config in `mode`: the report describes the production app, the audit runs against development builds. */\nexport async function loadAppConfig(forkPath: string, mode: 'development' | 'production'): Promise<AppConfig> {\n setEnv('NODE_ENV', mode);\n const parsed = appConfigSchema.safeParse((await importFromApp(forkPath, 'shared/index.ts')).appConfig);\n if (!parsed.success)\n throw new Error(`shared/index.ts exports no appConfig the audit can read: ${parsed.error.message}`);\n return parsed.data;\n}\n\n/** Overlays that hold focus while open. Toasts render as dialogs but never take focus. */\nexport const overlaySelector =\n '[role=\"dialog\"]:not([data-slot^=\"toast\"]), [role=\"alertdialog\"]:not([data-slot^=\"toast\"]), [role=\"menu\"], [role=\"listbox\"]';\n\n/** Opens the state's overlay again when a check (a resize, an Escape) closed it. */\nexport async function ensureOpen(page: Page, state: A11yState): Promise<void> {\n if (!state.overlay || !state.open || (await page.locator(overlaySelector).count())) return;\n await state.open(page);\n}\n"],"mappings":"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","names":["spawnSync","existsSync","process","require","process","process","spawnSync","resolve","z","existsSync","join","pathToFileURL","z","z","pathToFileURL","join","baseSchema","appSchema","existsSync","z","process","z","appConfigSchema","z","resolve","process"]}
1
+ {"version":3,"sources":["../src/tools/drive.ts","../src/utils/app-facts.ts","../src/utils/env.ts","../src/tools/a11y/define.ts","../src/tools/adapters.ts","../src/tools/a11y/session.ts","../src/tools/a11y/scope.ts"],"sourcesContent":["import { spawnSync } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport process from 'node:process';\nimport type { Browser, BrowserContext, BrowserContextOptions, Page } from 'playwright';\nimport { importFromWorkspace } from '../utils/app-facts';\n\nexport type { Browser, BrowserContext, BrowserContextOptions, Page } from 'playwright';\n\nexport type Playwright = typeof import('playwright');\n\n/** A signed-in user of the app: where it answers and the cookie that signs a browser or a request in. */\nexport interface Session {\n email: string;\n frontendUrl: string;\n /** The API, a path under the frontend's origin. */\n backendUrl: string;\n /** The app's slug, read off the cookie name; the persisted UI store's key starts with it. */\n slug: string;\n /** In the shape `context.addCookies` takes. */\n cookie: { name: string; value: string; url: string; httpOnly: true; sameSite: 'Strict' };\n /** `name=value`, for a request's cookie header. */\n header: string;\n}\n\nexport interface MintOptions {\n /** The app's root; default the current directory. */\n forkPath?: string;\n /** The environment the mint script runs in: a testbed's names its database. Default this process's. */\n env?: NodeJS.ProcessEnv;\n /** Session lifetime; default 2. */\n hours?: number;\n}\n\n/** Playwright as the app's frontend installed it, so the CLI ships none. */\nexport function loadPlaywright(forkPath = process.cwd()): Promise<Playwright> {\n return importFromWorkspace<Playwright>(forkPath, 'frontend', 'playwright');\n}\n\n/**\n * Checks that Playwright has its Chromium, before anything slow starts.\n * @throws with the install command when it has none\n */\nexport async function requireChromium(forkPath = process.cwd()): Promise<void> {\n const { chromium } = await loadPlaywright(forkPath);\n if (!existsSync(chromium.executablePath())) {\n throw new Error('Playwright has no Chromium yet: run `pnpm --filter frontend exec playwright install chromium`');\n }\n}\n\n/**\n * Signs `email` in through the backend's `session:mint` script, which refuses outside development and test. The script\n * prints `<cookie name>=<signed value>` and a curl line with the API URL, both from the config of the environment it runs\n * in, so a testbed's shifted ports and a linked worktree's are written down nowhere.\n * @throws when the script prints no session, as for an email with no user\n */\nexport function mintSession(email: string, { forkPath = process.cwd(), env, hours = 2 }: MintOptions = {}): Session {\n const minted = spawnSync('pnpm', ['--silent', '--filter', 'backend', 'session:mint', email, String(hours)], {\n cwd: forkPath,\n env,\n encoding: 'utf8',\n });\n const cookie = /^((?:__Host-)?([\\w-]+)-session-v\\d+)=(\\S+)$/m.exec(minted.stdout);\n const api = /^curl (\\S+)\\/me /m.exec(minted.stdout);\n if (!cookie || !api) throw new Error(`could not mint a session for ${email}:\\n${minted.stdout}\\n${minted.stderr}`);\n const frontendUrl = new URL(api[1]).origin;\n return {\n email,\n frontendUrl,\n backendUrl: api[1],\n slug: cookie[2],\n cookie: { name: cookie[1], value: cookie[3], url: frontendUrl, httpOnly: true, sameSite: 'Strict' },\n header: `${cookie[1]}=${cookie[3]}`,\n };\n}\n\n/**\n * Signs a minted session out, so a finished run leaves no live session in the user's sessions list. A JSON content type\n * passes the CSRF check without an Origin header; a stack that is already down leaves the session to expire.\n */\nexport async function signOut(session: Session | null | undefined): Promise<void> {\n if (!session) return;\n const headers = { cookie: session.header, 'content-type': 'application/json' };\n await fetch(`${session.backendUrl}/auth/sign-out`, { method: 'POST', headers }).catch(() => {});\n}\n\nexport interface ContextOptions extends BrowserContextOptions {\n /** Signs the context in. */\n session?: Session;\n /**\n * Fields written into the app's persisted UI store (`<slug>-ui`) before any page script runs, so the first paint is\n * already in the mode they set. Merged into what the store holds: a list joins the stored one, any other value wins.\n */\n uiStore?: { slug: string; state: Record<string, unknown> };\n}\n\n/** A browser context, signed in and with its UI store seeded when asked. */\nexport async function newContext(\n browser: Browser,\n { session, uiStore, ...options }: ContextOptions = {},\n): Promise<BrowserContext> {\n const context = await browser.newContext(options);\n // tsx and esbuild's keepNames wrap named functions in an `__name` helper that a page script does not have\n await context.addInitScript({ content: 'window.__name = (fn) => fn;' });\n if (session) await context.addCookies([session.cookie]);\n if (uiStore) {\n await context.addInitScript(\n ([key, fields]) => {\n const stored = JSON.parse(localStorage.getItem(key) ?? '{}');\n const state = { ...stored.state };\n for (const [name, value] of Object.entries(fields)) {\n const before = state[name];\n state[name] = Array.isArray(value) && Array.isArray(before) ? [...new Set([...before, ...value])] : value;\n }\n // A fresh entry needs the store's own version, or zustand drops it as written by an older build\n localStorage.setItem(key, JSON.stringify({ version: 1, ...stored, state }));\n },\n [`${uiStore.slug}-ui`, uiStore.state] as const,\n );\n }\n return context;\n}\n\n/**\n * Resolves once the page has stopped adding or changing content for `quiet` ms, or after `cap` ms. Lazy sections, query\n * results and opening overlays all show up as DOM changes; the network never goes idle in development.\n */\nexport async function settle(page: Page, { quiet = 400, cap = 3000 } = {}): Promise<void> {\n const waited = page.evaluate(\n ([quietMs, capMs]) =>\n new Promise<void>((resolve) => {\n const done = () => {\n observer.disconnect();\n clearTimeout(timer);\n clearTimeout(limit);\n resolve();\n };\n let timer = setTimeout(done, quietMs);\n const limit = setTimeout(done, capMs);\n const observer = new MutationObserver(() => {\n clearTimeout(timer);\n timer = setTimeout(done, quietMs);\n });\n observer.observe(document, { subtree: true, childList: true, characterData: true });\n }),\n [quiet, cap] as const,\n );\n // A navigation during the wait tears down the page's script context\n await waited.catch(() => page.waitForTimeout(quiet));\n}\n\n/**\n * Opens `url` in a new page and settles once the route has rendered its content.\n * @throws when the route redirects: a check or a shot would judge another page under this one's name\n */\nexport async function openPage(context: BrowserContext, url: string, { timeout = 30_000 } = {}): Promise<Page> {\n const page = await context.newPage();\n await page.goto(url, { waitUntil: 'domcontentloaded', timeout });\n await page.locator('main, [role=\"main\"], #accessibility-content, form, h1, footer').first().waitFor({ timeout });\n await settle(page);\n const landed = new URL(page.url()).pathname;\n const asked = new URL(url).pathname;\n if (landed !== asked) throw new Error(`${asked} redirected to ${landed}`);\n return page;\n}\n","import { execFileSync, spawnSync } from 'node:child_process';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport { dirname, join, resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { z } from 'zod';\nimport { setEnv } from './env';\n\nconst appConfigSchema = z.object({\n frontendUrl: z.string(),\n backendUrl: z.string(),\n devPortOffset: z.number(),\n services: z.record(\n z.string(),\n z.object({ enabled: z.boolean(), port: z.number(), internalPort: z.number().optional() }),\n ),\n});\n\n/** The part of the app's development config the develop commands read. */\nexport type AppFacts = z.infer<typeof appConfigSchema>;\n\n/** The app's local database helpers (`shared/src/local-database.ts`), as the develop commands call them. */\nexport interface LocalDatabases {\n port: (database: string) => number;\n url: (database: string, role: 'postgres' | 'runtime_role' | 'admin_role') => string;\n /** The env `backend/compose.yaml` reads: the project name and each database's host port. */\n composeEnv: () => Record<string, string>;\n}\n\n/** Node strips the types of a file outside node_modules, so the app's own TS loads without a build or tsx. */\nexport async function importFromApp(forkPath: string, file: string): Promise<Record<string, unknown>> {\n const path = join(forkPath, file);\n if (!existsSync(path)) throw new Error(`not an app built from cella: ${path} is missing`);\n // The app's config reads the mode once, at its first import, and the dev port offset applies in development mode only\n setEnv('NODE_ENV', 'development');\n return import(pathToFileURL(path).href);\n}\n\n/** An app path a template alignment moved: `current`, or `legacy` in an app that has only the old one. */\nexport const movedPath = (forkPath: string, current: string, legacy: string): string =>\n !existsSync(join(forkPath, current)) && existsSync(join(forkPath, legacy)) ? legacy : current;\n\n/**\n * The app's config in development mode, from its own `shared` package. In a linked worktree the import claims that\n * worktree's port slot, as `pnpm dev` would.\n */\nexport async function loadAppFacts(forkPath: string): Promise<AppFacts> {\n const parsed = appConfigSchema.safeParse((await importFromApp(forkPath, 'shared/index.ts')).appConfig);\n if (!parsed.success)\n throw new Error(`shared/index.ts exports no appConfig this CLI can read: ${parsed.error.message}`);\n return parsed.data;\n}\n\n/** The app's local database helpers. */\nexport async function loadLocalDatabases(forkPath: string): Promise<LocalDatabases> {\n const module = await importFromApp(forkPath, 'shared/src/local-database.ts');\n const { localDatabasePort, localDatabaseUrl, composeEnv } = module;\n if (\n typeof localDatabasePort !== 'function' ||\n typeof localDatabaseUrl !== 'function' ||\n typeof composeEnv !== 'function'\n ) {\n throw new Error('shared/src/local-database.ts lacks localDatabasePort, localDatabaseUrl or composeEnv');\n }\n return {\n port: (database) => localDatabasePort(database),\n url: (database, role) => localDatabaseUrl(database, role),\n composeEnv: () => composeEnv(),\n };\n}\n\n/**\n * A package as the app's `workspace` installed it, so a tool drives the app's own Playwright, TypeScript or Tailwind and\n * the CLI ships none. The package's ESM entry when it has one, else its CommonJS exports.\n * @throws when the workspace does not have the package installed\n */\nexport async function importFromWorkspace<T>(forkPath: string, workspace: string, name: string): Promise<T> {\n const require = createRequire(join(forkPath, workspace, 'package.json'));\n let manifestPath: string | undefined;\n try {\n manifestPath = require.resolve(`${name}/package.json`);\n } catch {\n // A package whose exports leave out package.json resolves by its main entry below\n }\n const root = manifestPath ? JSON.parse(readFileSync(manifestPath, 'utf8')).exports?.['.'] : undefined;\n const esm = typeof root?.import === 'string' ? root.import : root?.import?.default;\n if (manifestPath && typeof esm === 'string') return import(pathToFileURL(join(dirname(manifestPath), esm)).href);\n try {\n return require(name);\n } catch {\n throw new Error(`${name} is not installed in ${workspace}: run \\`pnpm --filter ${workspace} add -D ${name}\\``);\n }\n}\n\n/**\n * A Docker image as a tool runs it, pinned by digest (`name[:tag]@sha256:…`) so every run scans with the same code.\n * Pulls it on first use; the pull shows Docker's own progress.\n * @throws when the reference has no digest, Docker does not answer, or the pull fails\n */\nexport function resolveImage(reference: string): string {\n if (!/@sha256:[0-9a-f]{64}$/.test(reference)) throw new Error(`${reference} is not pinned by digest`);\n if (spawnSync('docker', ['info'], { stdio: 'ignore' }).status !== 0) {\n throw new Error('Docker does not answer: start Docker (or OrbStack) first');\n }\n if (spawnSync('docker', ['image', 'inspect', reference], { stdio: 'ignore' }).status === 0) return reference;\n if (spawnSync('docker', ['pull', reference], { stdio: ['ignore', 'inherit', 'inherit'] }).status !== 0) {\n throw new Error(`could not pull ${reference}`);\n }\n return reference;\n}\n\n/**\n * Where a tool keeps its results and state: `<git dir>/cella-<tool>/` of this checkout, or of the git directory every\n * worktree shares. Inside the git directory nothing shows in `git status`.\n */\nexport function toolDir(forkPath: string, tool: string, scope: 'checkout' | 'repository' = 'checkout'): string {\n const flag = scope === 'checkout' ? '--absolute-git-dir' : '--git-common-dir';\n const gitDir = execFileSync('git', ['rev-parse', flag], { cwd: forkPath, encoding: 'utf8' }).trim();\n return join(resolve(forkPath, gitDir), `cella-${tool}`);\n}\n","import process from 'node:process';\n\n/**\n * Centralized environment access for CLI/runtime integration points.\n */\nexport function getEnv(name: string): string | undefined {\n // biome-ignore lint/style/noProcessEnv: This module centralizes intentional CLI environment access.\n return process.env[name];\n}\n\n/** Whether the variable is set at all, even to an empty string. */\nexport function hasEnv(name: string): boolean {\n // biome-ignore lint/style/noProcessEnv: This module centralizes intentional CLI environment access.\n return name in process.env;\n}\n\n/** A mutable copy of the full environment, for passing to child processes. */\nexport function getEnvSnapshot(): NodeJS.ProcessEnv {\n // biome-ignore lint/style/noProcessEnv: This module centralizes intentional CLI environment access.\n return { ...process.env };\n}\n\n/** Set a variable for this process and the children it spawns from here on. */\nexport function setEnv(name: string, value: string): void {\n // biome-ignore lint/style/noProcessEnv: This module centralizes intentional CLI environment access.\n process.env[name] = value;\n}\n","import { z } from 'zod';\nimport { fn, type Placeholders, type Prepare, placeholdersSchema } from '../adapters';\nimport type { BrowserContextOptions, Page } from '../drive';\n\ntype Opener = (page: Page) => Promise<void>;\n\n/** A form probe a state runs on its page. */\nexport type FormCheck =\n /** Submitting the form empty identifies each error in text (3.3.1). */\n | 'required'\n /** Inputs for the user's own details declare their purpose (1.3.5). */\n | 'personal'\n /** Sign-in fields accept autofill and pasted text (3.3.8). */\n | 'sign-in';\n\n/** A page or state of the audit scope. */\nexport interface A11yState {\n id: string;\n /** Path on the frontend; a `{name}` placeholder takes the value its resolver returns. */\n path: string;\n auth: boolean;\n /** Brings the page into the state to audit, such as an open dialog. Runs after the page has loaded. */\n open?: Opener;\n /** `open` leaves an overlay (dialog, sheet, menu) open that holds focus until it closes. */\n overlay?: true;\n /** Browser settings of this state's visits, such as a viewport the state only exists at. */\n context?: BrowserContextOptions;\n forms?: FormCheck[];\n}\n\n/** `cella/a11y.ts`: the template's scope, synced with the UI it describes. */\nexport interface A11yBase {\n /** Brings the audit user into the state the pages expect, before the first visit. */\n prepare?: Prepare;\n /** Values for the `{name}` placeholders of the paths, read as the audit user before the first visit. */\n placeholders?: Placeholders;\n /** What the code checks search, as `git grep` pathspecs from the app's root. */\n sources: string[];\n /** The sample of pages and states the audit covers (WCAG-EM steps 2 and 3), in report order. */\n scope: A11yState[];\n}\n\n/** `cella/a11y.app.ts`, optional: what an app adds to the base, removes from it or overrides. */\nexport interface A11yApp {\n /** Runs after the base's. */\n prepare?: Prepare;\n /** Joins the base's; a name in both takes this one. */\n placeholders?: Placeholders;\n /** Base state ids the app does not have. */\n remove?: string[];\n /** States of the app's own; one with a base state's id replaces it in place. */\n add?: A11yState[];\n}\n\nconst stateSchema = z.strictObject({\n id: z.string().min(1),\n path: z.string().startsWith('/').or(z.string().startsWith('{')),\n auth: z.boolean(),\n open: fn<Opener>().optional(),\n overlay: z.literal(true).optional(),\n context: z.custom<BrowserContextOptions>((value) => typeof value === 'object' && value !== null).optional(),\n forms: z.array(z.enum(['required', 'personal', 'sign-in'])).optional(),\n}) satisfies z.ZodType<A11yState>;\n\nexport const baseSchema = z.strictObject({\n prepare: fn<Prepare>().optional(),\n placeholders: placeholdersSchema.optional(),\n sources: z.array(z.string()).min(1),\n scope: z.array(stateSchema).min(1),\n}) satisfies z.ZodType<A11yBase>;\n\nexport const appSchema = z.strictObject({\n prepare: fn<Prepare>().optional(),\n placeholders: placeholdersSchema.optional(),\n remove: z.array(z.string()).optional(),\n add: z.array(stateSchema).optional(),\n}) satisfies z.ZodType<A11yApp>;\n\n/**\n * The audit measures the app with increased contrast turned on, which is what raises the edge tokens to 3:1. It is a\n * user setting, not the default, so every report built from a run says so: the ledger records it and the report prints\n * it. `prepare` stores it for the audit user, since `/me` overwrites the seeded UI store on every authenticated page.\n */\nexport const auditContrast = 'more' as const;\n\nexport const defineA11yBase = (base: A11yBase): A11yBase => base;\n\nexport const defineA11yApp = (app: A11yApp): A11yApp => app;\n","import { existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { z } from 'zod';\nimport { importFromWorkspace } from '../utils/app-facts';\nimport type { Session } from './drive';\n\n/** What `prepare` and a placeholder resolver get: the signed-in tool user, to call the API as. */\nexport type SignedIn = Pick<Session, 'email' | 'frontendUrl' | 'backendUrl'> & {\n /** `name=value`: the cookie header that signs a request in. */\n cookie: string;\n};\n\nexport const signedIn = ({ email, frontendUrl, backendUrl, header }: Session): SignedIn => ({\n email,\n frontendUrl,\n backendUrl,\n cookie: header,\n});\n\n/** Brings the tool user into the state the pages expect, before the first visit. */\nexport type Prepare = (user: SignedIn) => Promise<void>;\n\n/**\n * Values for the `{name}` placeholders of the paths, by name. A resolver that finds nothing returns null, and a path\n * that needs its value then fails with a message naming it.\n */\nexport type Placeholders = Record<string, (user: SignedIn) => Promise<string | null>>;\n\n/** A zod schema that accepts any function; the adapter's types say which. */\nexport const fn = <T>() => z.custom<T>((value) => typeof value === 'function', 'expected a function');\n\nexport const placeholdersSchema = z.record(z.string(), fn<Placeholders[string]>());\n\n/** Fills the `{name}` placeholders of `path`. */\nexport function fillPath(path: string, values: Record<string, string | null>, file: string): string {\n return path.replace(/\\{(\\w+)\\}/g, (_, name: string) => {\n const value = values[name];\n if (!value) throw new Error(`no value for {${name}}: its resolver in ${file} found nothing for the signed-in user`);\n return value;\n });\n}\n\ninterface TsxApi {\n tsImport: (specifier: string, parentURL: string) => Promise<Record<string, unknown>>;\n}\n\n/**\n * Imports an adapter file under the app's own tsx. An adapter imports the app's SDK, whose generated files import each\n * other without an extension, and Node's own type stripping resolves no such import.\n */\nasync function importAdapter(forkPath: string, file: string): Promise<unknown> {\n const { tsImport } = await importFromWorkspace<TsxApi>(forkPath, '.', 'tsx/esm/api');\n const module = await tsImport(pathToFileURL(join(forkPath, file)).href, import.meta.url);\n return module.default;\n}\n\nfunction parsed<T>(file: string, schema: z.ZodType<T>, value: unknown): T {\n const result = schema.safeParse(value);\n if (result.success) return result.data;\n throw new Error(`${file} does not fit this version of the CLI:\\n${z.prettifyError(result.error)}`);\n}\n\n/**\n * A tool's adapter in the app's `cella/` folder: the base `<tool>.ts`, synced from the template, and the optional delta\n * `<tool>.app.ts`, pinned to the app, each checked against its schema. A missing delta means the base alone.\n * @throws when the base is missing or a file does not fit its schema\n */\nexport async function loadAdapter<Base, App>(\n forkPath: string,\n tool: string,\n baseSchema: z.ZodType<Base>,\n appSchema: z.ZodType<App>,\n): Promise<{ base: Base; app: App | undefined }> {\n const baseFile = `cella/${tool}.ts`;\n const appFile = `cella/${tool}.app.ts`;\n if (!existsSync(join(forkPath, baseFile))) {\n throw new Error(`${baseFile} is missing: upgrade the app from cella to get the ${tool} adapter`);\n }\n const base = parsed(baseFile, baseSchema, await importAdapter(forkPath, baseFile));\n const app = existsSync(join(forkPath, appFile))\n ? parsed(appFile, appSchema, await importAdapter(forkPath, appFile))\n : undefined;\n return { base, app };\n}\n\n/**\n * The base's entries with the delta applied: `remove` drops ids, and an added entry replaces the base entry of its id in\n * place or else comes last.\n * @throws when `remove` names an id the base lacks, so a renamed base entry is noticed\n */\nexport function applyDelta<T extends { id: string }>(\n base: T[],\n { remove = [], add = [] }: { remove?: string[]; add?: T[] },\n file: string,\n): T[] {\n const unknown = remove.filter((id) => !base.some((entry) => entry.id === id));\n if (unknown.length > 0) throw new Error(`${file} removes ${unknown.join(', ')}, which the base does not have`);\n const replaced = base\n .filter((entry) => !remove.includes(entry.id))\n .map((entry) => add.find((added) => added.id === entry.id) ?? entry);\n return [...replaced, ...add.filter((added) => !base.some((entry) => entry.id === added.id))];\n}\n","import process from 'node:process';\nimport type { Browser, BrowserContext, BrowserContextOptions, Page } from 'playwright';\nimport { getEnv } from '../../utils/env';\nimport { fillPath, signedIn } from '../adapters';\nimport { loadPlaywright, mintSession, newContext, openPage, type Session, settle, signOut } from '../drive';\nimport { type A11yState, auditContrast } from './define';\nimport { loadScope, type Scope } from './scope';\n\nexport type Mode = 'light' | 'dark';\n\ntype StorageState = Awaited<ReturnType<BrowserContext['storageState']>>;\n\n/** A signed-in audit: one browser for every visit, and what each visit starts from. */\nexport interface AuditSession {\n browser: Browser;\n user: Session;\n /** Cookies and local storage of a browser that has loaded the app once, as after a real sign-in. */\n storage: StorageState;\n /** Resolved `{name}` placeholders of the scope paths. */\n values: Record<string, string | null>;\n}\n\n/** Elements that exist only in development builds; scans and probes ignore them. */\nexport const devOnlySelectors = [\n '.TanStackRouterDevtools',\n '.tsqd-parent-container',\n '[aria-label=\"toggle debug toolbar\"]',\n];\n\nexport const defaultViewport = { width: 1280, height: 900 };\n\n/**\n * Signs the audit user in, runs the scope's `prepare` steps, resolves its placeholders and starts the browser.\n * `env` points the session mint at the database the app runs on.\n */\nexport async function startAudit(\n forkPath: string,\n scope: Scope,\n { email, env }: { email: string; env?: NodeJS.ProcessEnv },\n): Promise<AuditSession> {\n const user = mintSession(email, { forkPath, env, hours: 4 });\n try {\n for (const prepare of scope.prepare) await prepare(signedIn(user));\n const values: AuditSession['values'] = {};\n for (const [name, resolve] of Object.entries(scope.placeholders)) values[name] = await resolve(signedIn(user));\n\n const { chromium } = await loadPlaywright(forkPath);\n const browser = await chromium.launch();\n // Route guards read the user store, which a real sign-in fills before any deep link is opened\n const context = await newContext(browser, { session: user });\n const page = await context.newPage();\n await page.goto(`${user.frontendUrl}/home`, { waitUntil: 'domcontentloaded' });\n await page.locator('main').first().waitFor({ timeout: 60_000 });\n await settle(page);\n const storage = await context.storageState();\n await context.close();\n return { browser, user, storage, values };\n } catch (error) {\n await signOut(user);\n throw error;\n }\n}\n\n/** Closes the browser and signs the audit user out, so a finished audit leaves no live session behind. */\nexport async function endAudit(session: AuditSession): Promise<void> {\n await session.browser.close();\n await signOut(session.user);\n}\n\n/**\n * A fresh context for one visit: color mode, more contrast, reduced motion, the dev banner dismissed, and signed in\n * when the state needs it. `overrides` change the browser settings, such as the viewport or the motion preference.\n */\nexport function auditContext(\n session: AuditSession,\n { auth, mode }: { auth: boolean; mode: Mode },\n overrides: BrowserContextOptions = {},\n): Promise<BrowserContext> {\n return newContext(session.browser, {\n viewport: defaultViewport,\n colorScheme: mode,\n // Emulates `prefers-contrast`, which tailwind.css answers on its own, so public pages are covered like signed-in ones\n contrast: 'more',\n reducedMotion: 'reduce',\n serviceWorkers: 'block',\n storageState: auth ? session.storage : undefined,\n // `contrast` here is what the app's own `data-contrast` path reads; the context option above covers the media query\n uiStore: {\n slug: session.user.slug,\n state: { mode, theme: 'none', contrast: auditContrast, publicAlertsSeen: ['test-credentials'] },\n },\n ...overrides,\n });\n}\n\n/** Opens a state's path in `context`; its `open` step is the caller's. */\nexport function openStatePath(session: AuditSession, context: BrowserContext, state: A11yState): Promise<Page> {\n const path = fillPath(state.path, session.values, 'the a11y adapter');\n return openPage(context, `${session.user.frontendUrl}${path}`);\n}\n\ninterface OpenStateOptions {\n mode?: Mode;\n /** Over the state's own browser settings, such as a phone or `{ reducedMotion: 'no-preference' }`. */\n context?: BrowserContextOptions;\n /** Default `ADMIN_EMAIL`, which a testbed hands the command it runs. */\n email?: string;\n}\n\n/**\n * Opens a state of the app's audit scope by id, signed in: the start of a short script that checks a fix in the\n * running app. Run the script from the app's root through `pnpm cella testbed -- node <script>`; `close` ends it.\n * @throws when the scope has no state of that id, or no email is given and `ADMIN_EMAIL` is unset\n */\nexport async function openState(\n id: string,\n { mode = 'light', context: overrides, email = getEnv('ADMIN_EMAIL') }: OpenStateOptions = {},\n): Promise<{ page: Page; context: BrowserContext; close: () => Promise<void> }> {\n if (!email) throw new Error('no email: pass one, or run through `pnpm cella testbed --`, which sets ADMIN_EMAIL');\n const forkPath = process.cwd();\n const scope = await loadScope(forkPath);\n const state = scope.states.find((candidate) => candidate.id === id);\n if (!state) throw new Error(`no state \"${id}\" in the a11y scope`);\n const session = await startAudit(forkPath, scope, { email });\n try {\n const context = await auditContext(session, { auth: state.auth, mode }, { ...state.context, ...overrides });\n const page = await openStatePath(session, context, state);\n await state.open?.(page);\n return { page, context, close: () => endAudit(session) };\n } catch (error) {\n await endAudit(session);\n throw error;\n }\n}\n","import type { Page } from 'playwright';\nimport { z } from 'zod';\nimport { importFromApp } from '../../utils/app-facts';\nimport { setEnv } from '../../utils/env';\nimport { applyDelta, loadAdapter, type Placeholders, type Prepare } from '../adapters';\nimport { type A11yState, appSchema, baseSchema } from './define';\n\n/** The audit scope with the app's delta applied. */\nexport interface Scope {\n /** The base's, then the delta's. */\n prepare: Prepare[];\n placeholders: Placeholders;\n sources: string[];\n states: A11yState[];\n}\n\nexport async function loadScope(forkPath: string): Promise<Scope> {\n // The adapters read the app config, which takes the development ports and URLs in development mode only\n setEnv('NODE_ENV', 'development');\n const { base, app } = await loadAdapter(forkPath, 'a11y', baseSchema, appSchema);\n return {\n prepare: [base.prepare, app?.prepare].filter((step) => step !== undefined),\n placeholders: { ...base.placeholders, ...app?.placeholders },\n sources: base.sources,\n states: app ? applyDelta(base.scope, app, 'cella/a11y.app.ts') : base.scope,\n };\n}\n\nconst appConfigSchema = z.object({\n name: z.string(),\n slug: z.string(),\n description: z.string(),\n supportEmail: z.string(),\n frontendUrl: z.string(),\n company: z.object({ name: z.string() }),\n languages: z.array(z.string()).min(1),\n defaultLanguage: z.string(),\n enabledAuthStrategies: z.array(z.string()),\n});\n\n/** The part of the app's config the audit and the report read. */\nexport type AppConfig = z.infer<typeof appConfigSchema>;\n\n/** The app's config in `mode`: the report describes the production app, the audit runs against development builds. */\nexport async function loadAppConfig(forkPath: string, mode: 'development' | 'production'): Promise<AppConfig> {\n setEnv('NODE_ENV', mode);\n const parsed = appConfigSchema.safeParse((await importFromApp(forkPath, 'shared/index.ts')).appConfig);\n if (!parsed.success)\n throw new Error(`shared/index.ts exports no appConfig the audit can read: ${parsed.error.message}`);\n return parsed.data;\n}\n\n/** Overlays that hold focus while open. Toasts render as dialogs but never take focus. */\nexport const overlaySelector =\n '[role=\"dialog\"]:not([data-slot^=\"toast\"]), [role=\"alertdialog\"]:not([data-slot^=\"toast\"]), [role=\"menu\"], [role=\"listbox\"]';\n\n/** Opens the state's overlay again when a check (a resize, an Escape) closed it. */\nexport async function ensureOpen(page: Page, state: A11yState): Promise<void> {\n if (!state.overlay || !state.open || (await page.locator(overlaySelector).count())) return;\n await state.open(page);\n}\n"],"mappings":"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","names":["spawnSync","existsSync","process","require","process","process","spawnSync","resolve","z","existsSync","join","pathToFileURL","z","z","pathToFileURL","join","baseSchema","appSchema","existsSync","z","process","z","appConfigSchema","z","resolve","process"]}
package/dist/bench.js CHANGED
@@ -1067,7 +1067,7 @@ import process6 from "process";
1067
1067
  // package.json
1068
1068
  var package_default = {
1069
1069
  name: "@cellajs/cli",
1070
- version: "0.6.4",
1070
+ version: "0.6.6",
1071
1071
  private: false,
1072
1072
  license: "MIT",
1073
1073
  description: "CLI for managing your monorepo and upgrading it with upstream Cella.",