@cellajs/cli 0.6.5 → 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 +3 -3
- package/dist/a11y.js.map +1 -1
- package/dist/bench.js +1 -1
- package/dist/bench.js.map +1 -1
- package/dist/drive.js.map +1 -1
- package/dist/index.js +154 -130
- package/dist/index.js.map +1 -1
- package/dist/screenshots.js.map +1 -1
- package/dist/style.js.map +1 -1
- package/package.json +1 -1
package/dist/screenshots.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/tools/drive.ts","../src/utils/app-facts.ts","../src/utils/env.ts","../src/tools/screenshots/define.ts","../src/tools/adapters.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 { Page } from '../drive';\n\ntype Opener = (page: Page) => Promise<void>;\n\n/** A viewport and its pixel ratio: scale 2 draws a 1280px-wide app at 2560px. */\nexport interface Device {\n width: number;\n height: number;\n scale: number;\n}\n\n/** UI that shows once per user, marked seen in the persisted UI store before the first page. */\nexport interface Suppress {\n /** `publicAlertsSeen` ids, such as the dev banner. */\n alerts?: string[];\n /** `hintsSeen` ids, such as a first-visit menu hint. */\n hints?: string[];\n}\n\n/** One screenshot, written in both color modes as `<out>.<format>` and `<out>-dark.<format>`. */\nexport interface Shot {\n id: string;\n /** A key of `devices`. */\n device: string;\n /** Route on the frontend; a `{name}` placeholder takes the value its resolver returns. */\n path: string;\n /** From the app's root, without the extension. */\n out: string;\n /** Brings the page into the state the shot wants, after the route has rendered and settled. */\n open?: Opener;\n}\n\n/** `cella/screenshots.ts`: the template's shots, synced with the UI they show. */\nexport interface ScreenshotsBase {\n /** The file type of every shot. WebP is written lossless by the system `cwebp` encoder. */\n format: 'webp' | 'png';\n devices: Record<string, Device>;\n suppress?: Suppress;\n /** Brings the shot user into the state the pages expect, before the first shot. */\n prepare?: Prepare;\n placeholders?: Placeholders;\n shots: Shot[];\n}\n\n/** `cella/screenshots.app.ts`, optional: what an app adds to the base, removes from it or overrides. */\nexport interface ScreenshotsApp {\n format?: ScreenshotsBase['format'];\n /** Join the base's; a name in both takes this one. */\n devices?: Record<string, Device>;\n /** Joins the base's lists. */\n suppress?: Suppress;\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 shot ids the app does not ship. */\n remove?: string[];\n /** Shots of the app's own; one with a base shot's id replaces it in place. */\n add?: Shot[];\n}\n\nconst formatSchema = z.enum(['webp', 'png']);\n\nconst deviceSchema = z.strictObject({\n width: z.number().int().positive(),\n height: z.number().int().positive(),\n scale: z.number().positive(),\n});\n\nconst suppressSchema = z.strictObject({\n alerts: z.array(z.string()).optional(),\n hints: z.array(z.string()).optional(),\n});\n\nconst shotSchema = z.strictObject({\n id: z.string().min(1),\n device: z.string().min(1),\n path: z.string().startsWith('/').or(z.string().startsWith('{')),\n out: z.string().min(1),\n open: fn<Opener>().optional(),\n}) satisfies z.ZodType<Shot>;\n\nexport const baseSchema = z.strictObject({\n format: formatSchema,\n devices: z.record(z.string(), deviceSchema),\n suppress: suppressSchema.optional(),\n prepare: fn<Prepare>().optional(),\n placeholders: placeholdersSchema.optional(),\n shots: z.array(shotSchema).min(1),\n}) satisfies z.ZodType<ScreenshotsBase>;\n\nexport const appSchema = z.strictObject({\n format: formatSchema.optional(),\n devices: z.record(z.string(), deviceSchema).optional(),\n suppress: suppressSchema.optional(),\n prepare: fn<Prepare>().optional(),\n placeholders: placeholdersSchema.optional(),\n remove: z.array(z.string()).optional(),\n add: z.array(shotSchema).optional(),\n}) satisfies z.ZodType<ScreenshotsApp>;\n\nexport const defineScreenshotsBase = (base: ScreenshotsBase): ScreenshotsBase => base;\n\nexport const defineScreenshotsApp = (app: ScreenshotsApp): ScreenshotsApp => 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"],"mappings":"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","names":["spawnSync","existsSync","process","resolve","z","existsSync","join","pathToFileURL","z","z","z"]}
|
|
1
|
+
{"version":3,"sources":["../src/tools/drive.ts","../src/utils/app-facts.ts","../src/utils/env.ts","../src/tools/screenshots/define.ts","../src/tools/adapters.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 { Page } from '../drive';\n\ntype Opener = (page: Page) => Promise<void>;\n\n/** A viewport and its pixel ratio: scale 2 draws a 1280px-wide app at 2560px. */\nexport interface Device {\n width: number;\n height: number;\n scale: number;\n}\n\n/** UI that shows once per user, marked seen in the persisted UI store before the first page. */\nexport interface Suppress {\n /** `publicAlertsSeen` ids, such as the dev banner. */\n alerts?: string[];\n /** `hintsSeen` ids, such as a first-visit menu hint. */\n hints?: string[];\n}\n\n/** One screenshot, written in both color modes as `<out>.<format>` and `<out>-dark.<format>`. */\nexport interface Shot {\n id: string;\n /** A key of `devices`. */\n device: string;\n /** Route on the frontend; a `{name}` placeholder takes the value its resolver returns. */\n path: string;\n /** From the app's root, without the extension. */\n out: string;\n /** Brings the page into the state the shot wants, after the route has rendered and settled. */\n open?: Opener;\n}\n\n/** `cella/screenshots.ts`: the template's shots, synced with the UI they show. */\nexport interface ScreenshotsBase {\n /** The file type of every shot. WebP is written lossless by the system `cwebp` encoder. */\n format: 'webp' | 'png';\n devices: Record<string, Device>;\n suppress?: Suppress;\n /** Brings the shot user into the state the pages expect, before the first shot. */\n prepare?: Prepare;\n placeholders?: Placeholders;\n shots: Shot[];\n}\n\n/** `cella/screenshots.app.ts`, optional: what an app adds to the base, removes from it or overrides. */\nexport interface ScreenshotsApp {\n format?: ScreenshotsBase['format'];\n /** Join the base's; a name in both takes this one. */\n devices?: Record<string, Device>;\n /** Joins the base's lists. */\n suppress?: Suppress;\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 shot ids the app does not ship. */\n remove?: string[];\n /** Shots of the app's own; one with a base shot's id replaces it in place. */\n add?: Shot[];\n}\n\nconst formatSchema = z.enum(['webp', 'png']);\n\nconst deviceSchema = z.strictObject({\n width: z.number().int().positive(),\n height: z.number().int().positive(),\n scale: z.number().positive(),\n});\n\nconst suppressSchema = z.strictObject({\n alerts: z.array(z.string()).optional(),\n hints: z.array(z.string()).optional(),\n});\n\nconst shotSchema = z.strictObject({\n id: z.string().min(1),\n device: z.string().min(1),\n path: z.string().startsWith('/').or(z.string().startsWith('{')),\n out: z.string().min(1),\n open: fn<Opener>().optional(),\n}) satisfies z.ZodType<Shot>;\n\nexport const baseSchema = z.strictObject({\n format: formatSchema,\n devices: z.record(z.string(), deviceSchema),\n suppress: suppressSchema.optional(),\n prepare: fn<Prepare>().optional(),\n placeholders: placeholdersSchema.optional(),\n shots: z.array(shotSchema).min(1),\n}) satisfies z.ZodType<ScreenshotsBase>;\n\nexport const appSchema = z.strictObject({\n format: formatSchema.optional(),\n devices: z.record(z.string(), deviceSchema).optional(),\n suppress: suppressSchema.optional(),\n prepare: fn<Prepare>().optional(),\n placeholders: placeholdersSchema.optional(),\n remove: z.array(z.string()).optional(),\n add: z.array(shotSchema).optional(),\n}) satisfies z.ZodType<ScreenshotsApp>;\n\nexport const defineScreenshotsBase = (base: ScreenshotsBase): ScreenshotsBase => base;\n\nexport const defineScreenshotsApp = (app: ScreenshotsApp): ScreenshotsApp => 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"],"mappings":"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","names":["spawnSync","existsSync","process","resolve","z","existsSync","join","pathToFileURL","z","z","z"]}
|
package/dist/style.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/tools/style/repo.ts","../src/utils/app-facts.ts","../src/utils/env.ts","../src/tools/style/class-tokens.ts","../src/tools/style/source-comments.ts","../src/tools/style/tailwind-class-rules.ts"],"sourcesContent":["import { execFileSync } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type TypeScript from 'typescript';\nimport { importFromWorkspace } from '../../utils/app-facts';\n\n/** Absolute path of the repository a run checks; every repo-relative path here resolves against it. */\nexport let repoRoot = '';\n\n/** The TypeScript the app's root package installed, so the CLI ships none. */\nexport let ts: typeof TypeScript;\n\n/** The node types the rules name as `ts.<Type>`: a namespace of types merges with the binding above. */\nexport declare namespace ts {\n type ArrowFunction = TypeScript.ArrowFunction;\n type BindingName = TypeScript.BindingName;\n type CallExpression = TypeScript.CallExpression;\n type CommentRange = TypeScript.CommentRange;\n type Expression = TypeScript.Expression;\n type FunctionExpression = TypeScript.FunctionExpression;\n type FunctionLikeDeclaration = TypeScript.FunctionLikeDeclaration;\n type Node = TypeScript.Node;\n type ObjectLiteralExpression = TypeScript.ObjectLiteralExpression;\n type PropertyName = TypeScript.PropertyName;\n type SourceFile = TypeScript.SourceFile;\n type StringLiteralLike = TypeScript.StringLiteralLike;\n type TemplateExpression = TypeScript.TemplateExpression;\n type TemplateLiteralLikeNode = TypeScript.TemplateLiteralLikeNode;\n type VariableStatement = TypeScript.VariableStatement;\n}\n\n/**\n * Points the rules at the repository in `root` and loads its TypeScript. Call it before any other export of this\n * folder: they read `repoRoot` and `ts`.\n * @throws when the root package has no `typescript` installed\n */\nexport async function openRepository(root: string): Promise<void> {\n ts = await importFromWorkspace<typeof TypeScript>(root, '.', 'typescript');\n repoRoot = root;\n}\n\n/** One style finding. */\nexport interface Finding {\n file: string;\n /** 1-based, as is `column`; absent when the finding is about the path itself. */\n line?: number;\n column?: number;\n rule: string;\n /** The matched text, printed in quotes before the message; absent when the finding names no single term. */\n term?: string;\n message: string;\n}\n\n/** Repo-relative files git tracks, plus untracked ones it does not ignore, that exist on disk, in `git ls-files` order. */\nexport function repoFiles(): string[] {\n return execFileSync('git', ['ls-files', '-co', '--exclude-standard'], { cwd: repoRoot, encoding: 'utf8' })\n .split('\\n')\n .filter((file) => file && existsSync(join(repoRoot, file)));\n}\n\n/** Whether `file` is the path `entry` or inside it; `entry` may end in `/`. */\nexport function isUnder(file: string, entry: string): boolean {\n return file === entry || file.startsWith(entry.endsWith('/') ? entry : `${entry}/`);\n}\n\n/** Whether `file` is one of the requested roots or inside one; no roots requests every file. */\nexport function isRequested(file: string, roots: string[]): boolean {\n return roots.length === 0 || roots.some((root) => isUnder(file, root.replace(/^\\.\\//, '').replace(/\\/$/, '')));\n}\n\n/** App sources under frontend/src, outside generated files, content, stories and tests: the files the frontend convention rules read. */\nexport function isFrontendSource(file: string): boolean {\n return (\n /^frontend\\/src\\/.*\\.tsx?$/.test(file) &&\n !file.includes('.gen.') &&\n !file.includes('/content/') &&\n !file.includes('/stories/') &&\n !/\\.(?:stories|test)\\.tsx?$/.test(file)\n );\n}\n\n/** `text` with every character but a line break turned into a space, so offsets and line numbers into it still hold. */\nexport function blankOut(text: string): string {\n return text.replace(/[^\\n]/g, ' ');\n}\n\n/** 1-based line and column of `offset`, counting `\\n` line breaks only. */\nexport function lineColumn(source: string, offset: number): { line: number; column: number } {\n const lines = source.slice(0, offset).split('\\n');\n return { line: lines.length, column: lines.at(-1)!.length + 1 };\n}\n\n/** `file:line:column [rule] \"term\": message`; a finding about the path itself prints the path alone. */\nexport function formatFinding({ file, line, column, rule, term, message }: Finding): string {\n const location = line === undefined ? file : `${file}:${line}:${column}`;\n return `${location} [${rule}] ${term === undefined ? '' : `\"${term}\": `}${message}`;\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 { ts } from './repo';\n\n/** A whitespace-separated token of a string and its offset in the source file. */\nexport type Token = { token: string; offset: number };\n\n/**\n * The variants and the utility of a class token, without the important marker: `md:hover:x!` gives `['md', 'hover']`\n * and `x`. A `:` inside `[]` or `()` belongs to an arbitrary value or variant, as in `has-[a:hover]:x` or `text-[length:1em]`.\n */\nexport function splitToken(token: string): { variants: string[]; base: string } {\n const text = token.replace(/^!|!$/g, '');\n const variants: string[] = [];\n let depth = 0;\n let start = 0;\n for (let index = 0; index < text.length; index++) {\n const char = text[index];\n if (char === '[' || char === '(') depth++;\n else if (char === ']' || char === ')') depth--;\n else if (char === ':' && depth === 0) {\n variants.push(text.slice(start, index));\n start = index + 1;\n }\n }\n return { variants, base: text.slice(start).replace(/^!/, '') };\n}\n\n/** Whitespace-separated tokens of every string and template piece in `node` and under it, with their source offsets. */\nexport function stringTokens(sourceFile: ts.SourceFile, node: ts.Node): Token[] {\n const found: Token[] = [];\n (function visit(child: ts.Node) {\n if (\n ts.isStringLiteralLike(child) ||\n ts.isTemplateHead(child) ||\n ts.isTemplateMiddle(child) ||\n ts.isTemplateTail(child)\n ) {\n const start = child.getStart(sourceFile) + 1;\n for (const match of child.text.matchAll(/\\S+/g)) found.push({ token: match[0], offset: start + match.index });\n }\n child.forEachChild(visit);\n })(node);\n return found;\n}\n","import { extname } from 'node:path';\nimport { ts } from './repo';\n\n/** One comment of a source file, with its delimiters. */\nexport interface Comment {\n /** Offset of the comment's first character in the file. */\n offset: number;\n /** Offset just past its last character. */\n end: number;\n text: string;\n}\n\n/** Extensions of the script files, whose comments come from the TypeScript parse. */\nexport const scriptExtensions = new Set(['.cjs', '.js', '.jsx', '.mjs', '.ts', '.tsx']);\n\nlet lastParsed: ts.SourceFile | undefined;\n\n/** The TypeScript parse of a script file; the checks of one file share it. */\nexport function parseSource(file: string, source: string): ts.SourceFile {\n if (lastParsed?.fileName === file && lastParsed.text === source) return lastParsed;\n const kind = ['.tsx', '.jsx'].includes(extname(file)) ? ts.ScriptKind.TSX : ts.ScriptKind.TS;\n lastParsed = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, kind);\n return lastParsed;\n}\n\n/**\n * Comments at every token boundary, which reaches the ones after an opening bracket, in an empty JSX expression and\n * before a `.` that starts a line too: every comment trails a token on its line or leads the token after a line break.\n */\nfunction scriptComments(file: string, source: string): Comment[] {\n const sourceFile = parseSource(file, source);\n const comments = new Map<number, Comment>();\n const add = (ranges: readonly ts.CommentRange[] | undefined) => {\n for (const { pos, end } of ranges ?? []) comments.set(pos, { offset: pos, end, text: source.slice(pos, end) });\n };\n const visit = (node: ts.Node) => {\n if (ts.isJSDoc(node)) return;\n const children = node.getChildren(sourceFile);\n if (children.length === 0) {\n add(ts.getLeadingCommentRanges(source, node.pos));\n add(ts.getTrailingCommentRanges(source, node.end));\n }\n for (const child of children) visit(child);\n };\n visit(sourceFile);\n return [...comments.values()].sort((a, b) => a.offset - b.offset);\n}\n\nfunction jsoncComments(source: string): Comment[] {\n const comments: Comment[] = [];\n for (let i = 0; i < source.length; i++) {\n if (source[i] === '\"') {\n for (i++; i < source.length; i++) {\n if (source[i] === '\\\\') i++;\n else if (source[i] === '\"') break;\n }\n continue;\n }\n if (source[i] !== '/' || (source[i + 1] !== '/' && source[i + 1] !== '*')) continue;\n\n const start = i;\n if (source[++i] === '/') {\n while (i + 1 < source.length && source[i + 1] !== '\\n') i++;\n } else {\n while (i + 1 < source.length && !(source[i] === '*' && source[i + 1] === '/')) i++;\n if (i + 1 < source.length) i++;\n }\n comments.push({ offset: start, end: i + 1, text: source.slice(start, i + 1) });\n }\n return comments;\n}\n\n/** Index of the `#` that opens a comment on a YAML line, or -1; `#` inside a quoted scalar or a word is text. */\nfunction commentHash(line: string): number {\n let quote = '';\n for (let i = 0; i < line.length; i++) {\n const char = line[i];\n if (quote) {\n if (char === '\\\\' && quote === '\"') i++;\n else if (char === quote && quote === \"'\" && line[i + 1] === \"'\") i++;\n else if (char === quote) quote = '';\n } else if ((char === '\"' || char === \"'\") && /^$|[\\s[{,]$/.test(line.slice(0, i))) {\n quote = char;\n } else if (char === '#' && (i === 0 || line[i - 1] === ' ' || line[i - 1] === '\\t')) {\n return i;\n }\n }\n return -1;\n}\n\n/** Comments after a YAML value, outside block scalars, whose lines hold text in another language. */\nfunction yamlTrailingComments(source: string): Comment[] {\n const comments: Comment[] = [];\n let offset = 0;\n let blockIndent = -1;\n for (const line of source.split('\\n')) {\n const indent = line.length - line.trimStart().length;\n if (blockIndent < 0 || (line.trim() && indent <= blockIndent)) {\n const hash = commentHash(line);\n const value = hash < 0 ? line : line.slice(0, hash);\n if (hash >= 0 && value.trim()) {\n comments.push({ offset: offset + hash, end: offset + line.length, text: line.slice(hash) });\n }\n blockIndent = value.trim() && /(?:^|\\s)[|>][0-9+-]*\\s*$/.test(value) ? indent : -1;\n }\n offset += line.length + 1;\n }\n return comments;\n}\n\n/**\n * The comments of a source file: the TypeScript parse for scripts, a string-aware scanner for JSONC, block comments for\n * CSS and SCSS, block then line comments for SQL, and `#` lines for YAML (with the comments after a value, in document\n * order), Dockerfile, Caddyfile and env files. JSON has none.\n */\nexport function sourceComments(file: string, source: string): Comment[] {\n const extension = extname(file);\n if (scriptExtensions.has(extension)) return scriptComments(file, source);\n if (extension === '.jsonc') return jsoncComments(source);\n if (extension === '.json') return [];\n const patterns =\n extension === '.css' || extension === '.scss'\n ? [/\\/\\*[\\s\\S]*?\\*\\//g]\n : extension === '.sql'\n ? [/\\/\\*[\\s\\S]*?\\*\\//g, /--[^\\n]*/g]\n : [/^[\\t ]*#[^\\n]*/gm];\n const comments = patterns.flatMap((pattern) =>\n [...source.matchAll(pattern)].map((match) => ({\n offset: match.index,\n end: match.index + match[0].length,\n text: match[0],\n })),\n );\n if (extension !== '.yaml' && extension !== '.yml') return comments;\n return [...comments, ...yamlTrailingComments(source)].sort((a, b) => a.offset - b.offset);\n}\n","import { readFileSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport { dirname, join, relative } from 'node:path';\nimport { importFromWorkspace } from '../../utils/app-facts';\nimport { splitToken, type Token } from './class-tokens';\nimport { type Finding, lineColumn, repoRoot, ts } from './repo';\nimport { parseSource } from './source-comments';\n\n/** Class names no frontend stylesheet, script or class string selects, each with the reason it stays. */\nconst markerClasses: Record<string, string> = {\n 'is-selected': 'BoardPanel state for apps whose board selects items (hasSelection), a hook for their styles',\n};\n\n/** Third-party class prefixes: the library's own stylesheet or DOM code reads them. */\nconst libraryPrefixes: Record<string, string> = {\n 'bn-': 'BlockNote editor parts',\n rdg: 'data grid rows, cells and states',\n};\n\n/** Calls whose arguments are class values; object keys are class names and object values conditions, as in clsx. */\nconst classFunctions = new Set(['clsx', 'cn', 'cx', 'tw', 'twJoin', 'twMerge']);\nconst classNamePattern =\n /^(?:class|classes|className|classNames|classList)$|[a-z](?:Class|Classes|Class[Nn]ames?|ClassList)$/;\nconst groupPattern = /^(?:group|peer)(?:\\/[\\w-]+)?$/;\nconst tailwindImport = /@import\\s+['\"]tailwindcss(?:\\/[\\w.-]+)?['\"]/;\n\ninterface DesignSystem {\n candidatesToCss(classes: string[]): (string | null)[];\n}\n\ninterface TailwindNode {\n __unstable__loadDesignSystem(css: string, options: { base: string }): Promise<DesignSystem>;\n}\n\n/** The loaded design system plus the class names that stylesheets, scripts and arbitrary selectors use. */\nexport interface TailwindContext {\n /** Repo-relative stylesheet that imports Tailwind. */\n entry: string;\n /** Absent when loading failed, as on an `@apply` of an unknown class; `loadError` then holds Tailwind's message. */\n designSystem?: DesignSystem;\n loadError?: string;\n /** Class names a stylesheet, script or arbitrary selector uses, and the marker classes. */\n knownClasses: Set<string>;\n /** Every candidate sent to Tailwind so far, and whether it compiled to CSS. */\n compiles: Map<string, boolean>;\n}\n\ntype Mode = 'class' | 'clsx';\n\n/** Text of one string or template piece in a class position; `open*` marks a side that touches a `${...}` placeholder. */\ninterface ClassText {\n /** Offset of `raw` in the source file. */\n start: number;\n /** The value, escapes resolved. */\n text: string;\n /** The source text of the value, which `start` locates. */\n raw: string;\n openStart: boolean;\n openEnd: boolean;\n}\n\nfunction isFrontendScript(file: string): boolean {\n return /^frontend\\/src\\/.*\\.tsx?$/.test(file) && !file.endsWith('.d.ts') && !file.includes('.gen.');\n}\n\nfunction isFrontendStylesheet(file: string): boolean {\n return /^frontend\\/src\\/.*\\.css$/.test(file);\n}\n\n/** Test files pass made-up class names to check prop forwarding and merging. */\nexport function isClassSource(file: string): boolean {\n return isFrontendScript(file) && !/\\.test\\.tsx?$/.test(file);\n}\n\n/** `@tailwindcss/node` from the frontend package, directly or through `@tailwindcss/vite`, which depends on it. */\nexport async function loadTailwindNode(): Promise<TailwindNode> {\n try {\n return await importFromWorkspace<TailwindNode>(repoRoot, 'frontend', '@tailwindcss/node');\n } catch {\n const plugin = createRequire(join(repoRoot, 'frontend/package.json')).resolve('@tailwindcss/vite');\n return importFromWorkspace<TailwindNode>(repoRoot, relative(repoRoot, dirname(plugin)), '@tailwindcss/node');\n }\n}\n\nfunction addMatches(target: Set<string>, text: string, pattern: RegExp): void {\n for (const match of text.matchAll(pattern)) target.add(match[1]);\n}\n\n/** Class names a stylesheet selects, a script queries or toggles, or an arbitrary selector such as `group-[.x]` names. */\nfunction usedClassNames(files: string[]): Set<string> {\n const names = new Set<string>();\n for (const file of files.filter(isFrontendStylesheet)) {\n const css = readFileSync(join(repoRoot, file), 'utf8').replace(/\\/\\*[\\s\\S]*?\\*\\//g, '');\n addMatches(names, css, /(?<!\\\\)\\.(-?[_a-zA-Z][\\w-]*)/g);\n }\n for (const file of files.filter(isFrontendScript)) {\n const source = readFileSync(join(repoRoot, file), 'utf8');\n // Arbitrary selectors (`group-[.x]`, `[&_.x]`); a bracket after a name, `)`, `]` or `?.` indexes code.\n for (const [chunk] of source.matchAll(/(?<![\\w$)\\]?.])\\[[^\\s'\"`]*/g))\n addMatches(names, chunk, /(?<![a-zA-Z0-9$.)\\]?!])\\.(-?[_a-zA-Z][\\w-]*)/g);\n // Selector strings for querySelector, closest and the like.\n for (const [, , text] of source.matchAll(/(['\"`])((?:\\\\.|(?!\\1)[^\\\\\\n])*)\\1/g)) {\n if (/^[\\w\\s.#>+~,:()[\\]=\"*-]+$/.test(text)) addMatches(names, text, /(?:^|[\\s>+~,(])\\.(-?[_a-zA-Z][\\w-]*)/g);\n }\n for (const [, args] of source.matchAll(\n /(?:classList\\.(?:add|contains|remove|replace|toggle)|getElementsByClassName)\\(([^)]*)\\)/g,\n )) {\n for (const [, text] of args.matchAll(/['\"`]([^'\"`]+)['\"`]/g))\n for (const name of text.split(/\\s+/)) names.add(name);\n }\n }\n return names;\n}\n\n/** The design system from the frontend stylesheet that imports Tailwind; undefined in a repo without one. */\nexport async function tailwindContext(\n files: string[],\n appMarkerClasses: Record<string, string> = {},\n): Promise<TailwindContext | undefined> {\n const entry = files\n .filter(isFrontendStylesheet)\n .find((file) => tailwindImport.test(readFileSync(join(repoRoot, file), 'utf8')));\n if (!entry) return undefined;\n const path = join(repoRoot, entry);\n const knownClasses = usedClassNames(files);\n for (const name of Object.keys({ ...markerClasses, ...appMarkerClasses })) knownClasses.add(name);\n const context: TailwindContext = { entry, knownClasses, compiles: new Map() };\n try {\n context.designSystem = await (await loadTailwindNode()).__unstable__loadDesignSystem(readFileSync(path, 'utf8'), {\n base: dirname(path),\n });\n } catch (error) {\n context.loadError = error instanceof Error ? error.message : String(error);\n }\n return context;\n}\n\nfunction propertyName(name: ts.PropertyName | ts.BindingName | undefined): string | undefined {\n return name && (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isPrivateIdentifier(name))\n ? name.text\n : undefined;\n}\n\nfunction isClassNamed(...names: (string | undefined)[]): boolean {\n return names.some((name) => name !== undefined && classNamePattern.test(name));\n}\n\n/** Strings in class positions: class attributes, class helper calls, cva, `tw` templates and class-named values. */\nexport function classTexts(sourceFile: ts.SourceFile): ClassText[] {\n const texts: ClassText[] = [];\n const add = (node: ts.StringLiteralLike | ts.TemplateLiteralLikeNode, openStart = false, openEnd = false) => {\n const start = node.getStart(sourceFile) + 1;\n const end = node.end - (ts.isTemplateHead(node) || ts.isTemplateMiddle(node) ? 2 : 1);\n texts.push({ start, text: node.text, raw: sourceFile.text.slice(start, end), openStart, openEnd });\n };\n const addKey = (name: ts.Node) => {\n if (ts.isStringLiteral(name)) add(name);\n else if (ts.isIdentifier(name))\n texts.push({\n start: name.getStart(sourceFile),\n text: name.text,\n raw: name.text,\n openStart: false,\n openEnd: false,\n });\n else walk(name);\n };\n\n function collectTemplate(node: ts.TemplateExpression): void {\n add(node.head, false, true);\n let before = node.head.text;\n for (const [index, span] of node.templateSpans.entries()) {\n const last = index === node.templateSpans.length - 1;\n // A placeholder holds whole classes only when whitespace or the string edge separates it from the text around it.\n const leftClosed = /\\s$/.test(before) || (index === 0 && before === '');\n const rightClosed = /^\\s/.test(span.literal.text) || (last && span.literal.text === '');\n if (leftClosed && rightClosed) collect(span.expression, 'class');\n else walk(span.expression);\n add(span.literal, true, !last);\n before = span.literal.text;\n }\n }\n\n function collectObject(node: ts.ObjectLiteralExpression, mode: Mode): void {\n for (const property of node.properties) {\n if (mode === 'clsx' && (ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property))) {\n addKey(property.name);\n if (ts.isPropertyAssignment(property)) walk(property.initializer);\n } else if (ts.isPropertyAssignment(property)) {\n walk(property.name);\n collect(property.initializer, 'class');\n } else walk(property);\n }\n }\n\n function collectCva(node: ts.CallExpression): void {\n const [base, config, ...rest] = node.arguments;\n collect(base, 'class');\n for (const argument of rest) walk(argument);\n if (!config || !ts.isObjectLiteralExpression(config)) {\n walk(config);\n return;\n }\n for (const property of config.properties) {\n if (\n !ts.isPropertyAssignment(property) ||\n propertyName(property.name) !== 'variants' ||\n !ts.isObjectLiteralExpression(property.initializer)\n ) {\n walk(property);\n continue;\n }\n for (const variant of property.initializer.properties) {\n if (ts.isPropertyAssignment(variant) && ts.isObjectLiteralExpression(variant.initializer))\n collectObject(variant.initializer, 'class');\n else walk(variant);\n }\n }\n }\n\n function calleeName(node: ts.CallExpression): string | undefined {\n const callee = node.expression;\n return ts.isIdentifier(callee) ? callee.text : ts.isPropertyAccessExpression(callee) ? callee.name.text : undefined;\n }\n\n /** Whether `node` starts a class position, collecting it when it does. */\n function collectCall(node: ts.CallExpression, mode?: Mode): boolean {\n const name = calleeName(node);\n if (name === 'cva') collectCva(node);\n else if (name && classFunctions.has(name)) {\n walk(node.expression);\n for (const argument of node.arguments) collect(argument, 'clsx');\n } else if (mode && (name === 'join' || name === 'filter') && ts.isPropertyAccessExpression(node.expression)) {\n collect(node.expression.expression, mode);\n for (const argument of node.arguments) walk(argument);\n } else return false;\n return true;\n }\n\n function collectReturns(node: ts.FunctionLikeDeclaration, mode: Mode): void {\n for (const parameter of node.parameters) walk(parameter);\n if (node.body && !ts.isBlock(node.body)) collect(node.body, mode);\n else walk(node.body, mode);\n }\n\n function collect(node: ts.Node | undefined, mode: Mode): void {\n if (!node) return;\n if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) add(node);\n else if (ts.isTemplateExpression(node)) collectTemplate(node);\n else if (\n ts.isParenthesizedExpression(node) ||\n ts.isAsExpression(node) ||\n ts.isSatisfiesExpression(node) ||\n ts.isNonNullExpression(node) ||\n ts.isSpreadElement(node) ||\n ts.isJsxExpression(node)\n )\n collect(node.expression, mode);\n else if (ts.isConditionalExpression(node)) {\n walk(node.condition);\n collect(node.whenTrue, mode);\n collect(node.whenFalse, mode);\n } else if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {\n walk(node.left);\n collect(node.right, mode);\n } else if (\n ts.isBinaryExpression(node) &&\n [ts.SyntaxKind.BarBarToken, ts.SyntaxKind.QuestionQuestionToken].includes(node.operatorToken.kind)\n ) {\n collect(node.left, mode);\n collect(node.right, mode);\n } else if (ts.isArrayLiteralExpression(node)) {\n for (const element of node.elements) collect(element, mode);\n } else if (ts.isObjectLiteralExpression(node)) collectObject(node, mode);\n else if (ts.isElementAccessExpression(node)) {\n collect(node.expression, mode);\n walk(node.argumentExpression);\n } else if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) collectReturns(node, mode);\n else if (!ts.isCallExpression(node) || !collectCall(node, mode)) walk(node);\n }\n\n /** Collects `node` when it starts a class position; `returns` is set inside a class-named function. */\n function collectStart(node: ts.Node, returns?: Mode): boolean {\n if (ts.isJsxAttribute(node) && ts.isIdentifier(node.name) && isClassNamed(node.name.text))\n collect(node.initializer, 'class');\n else if (ts.isCallExpression(node)) return collectCall(node);\n else if (ts.isTaggedTemplateExpression(node) && ts.isIdentifier(node.tag) && node.tag.text === 'tw')\n collect(node.template, 'class');\n else if (ts.isReturnStatement(node) && returns) collect(node.expression, returns);\n // Storybook argTypes describe controls (`className: { control: 'text' }`), never classes.\n else if (ts.isPropertyAssignment(node) && propertyName(node.name) === 'argTypes') return true;\n else if (\n (ts.isVariableDeclaration(node) ||\n ts.isPropertyAssignment(node) ||\n ts.isPropertyDeclaration(node) ||\n ts.isParameter(node)) &&\n node.initializer &&\n isClassNamed(propertyName(node.name))\n ) {\n walk(node.name);\n collect(node.initializer, 'class');\n } else if (\n ts.isBindingElement(node) &&\n node.initializer &&\n isClassNamed(propertyName(node.name), propertyName(node.propertyName))\n ) {\n collect(node.initializer, 'class');\n } else if (\n (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) &&\n isClassNamed(propertyName(node.name))\n ) {\n collectReturns(node, 'class');\n } else return false;\n return true;\n }\n\n /** Visits `node` and its children looking for class positions. */\n function walk(node: ts.Node | undefined, returns?: Mode): void {\n if (!node || ts.isTypeNode(node) || collectStart(node, returns)) return;\n const inner = ts.isFunctionLike(node) ? undefined : returns;\n ts.forEachChild(node, (child) => walk(child, inner));\n }\n\n walk(sourceFile);\n return texts;\n}\n\n/** Whole tokens of `text` with their offsets; a token touching an open side is part of a dynamic class and skipped. */\nexport function classTokens({ start, text, raw, openStart, openEnd }: ClassText): Token[] {\n const result: Token[] = [];\n for (const match of text.matchAll(/\\S+/g)) {\n const end = match.index + match[0].length;\n if ((openStart && match.index === 0) || (openEnd && end === text.length)) continue;\n result.push({ token: match[0], offset: start + (raw === text ? match.index : Math.max(0, raw.indexOf(match[0]))) });\n }\n return result;\n}\n\n/** Compiles the candidates `context.compiles` does not hold yet, in one call, and records each result there. */\nfunction compileCandidates(context: TailwindContext, designSystem: DesignSystem, candidates: string[]): void {\n const unknown = [...new Set(candidates)].filter((candidate) => !context.compiles.has(candidate));\n const css = designSystem.candidatesToCss(unknown);\n for (const [index, candidate] of unknown.entries()) context.compiles.set(candidate, css[index] !== null);\n}\n\n/** Why `token` produces nothing, or undefined when Tailwind compiles it or it is a known non-Tailwind class. */\nfunction problem(context: TailwindContext, token: string): string | undefined {\n if (context.compiles.get(token) || groupPattern.test(token) || context.knownClasses.has(token)) return undefined;\n if (Object.keys(libraryPrefixes).some((prefix) => token.startsWith(prefix))) return undefined;\n const { base } = splitToken(token);\n if (base !== token && (context.knownClasses.has(base) || groupPattern.test(base))) {\n return `${base} is a plain CSS class, so variants and ! do not apply to it; define it with @utility`;\n }\n return 'Tailwind compiles no CSS for it and no stylesheet or script selects it; fix the name, or list it under markerClasses in shared/config/vocabulary-allowlist.ts';\n}\n\n/**\n * Class names in one frontend file that compile to no CSS; `context` comes from `tailwindContext`. Tailwind itself\n * rejects an `@apply` of an unknown class while loading, which this reports on the entry stylesheet.\n */\nexport function tailwindFindings(file: string, source: string, context: TailwindContext | undefined): Finding[] {\n if (!context) return [];\n const { designSystem, loadError } = context;\n if (!designSystem)\n return file === context.entry\n ? [{ file, rule: 'tailwind-class', message: `Tailwind cannot load this stylesheet: ${loadError}` }]\n : [];\n if (!isClassSource(file)) return [];\n const sourceFile = parseSource(file, source);\n const found = classTexts(sourceFile).flatMap(classTokens);\n compileCandidates(\n context,\n designSystem,\n found.map(({ token }) => token),\n );\n return found.flatMap(({ token, offset }) => {\n const message = problem(context, token);\n if (!message) return [];\n return [{ file, ...lineColumn(source, offset), rule: 'tailwind-class', term: token, message }];\n });\n}\n"],"mappings":"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","names":["execFileSync","existsSync","join","require","execFileSync","existsSync","join","readFileSync","createRequire","dirname","join","createRequire","join","dirname"]}
|
|
1
|
+
{"version":3,"sources":["../src/tools/style/repo.ts","../src/utils/app-facts.ts","../src/utils/env.ts","../src/tools/style/class-tokens.ts","../src/tools/style/source-comments.ts","../src/tools/style/tailwind-class-rules.ts"],"sourcesContent":["import { execFileSync } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type TypeScript from 'typescript';\nimport { importFromWorkspace } from '../../utils/app-facts';\n\n/** Absolute path of the repository a run checks; every repo-relative path here resolves against it. */\nexport let repoRoot = '';\n\n/** The TypeScript the app's root package installed, so the CLI ships none. */\nexport let ts: typeof TypeScript;\n\n/** The node types the rules name as `ts.<Type>`: a namespace of types merges with the binding above. */\nexport declare namespace ts {\n type ArrowFunction = TypeScript.ArrowFunction;\n type BindingName = TypeScript.BindingName;\n type CallExpression = TypeScript.CallExpression;\n type CommentRange = TypeScript.CommentRange;\n type Expression = TypeScript.Expression;\n type FunctionExpression = TypeScript.FunctionExpression;\n type FunctionLikeDeclaration = TypeScript.FunctionLikeDeclaration;\n type Node = TypeScript.Node;\n type ObjectLiteralExpression = TypeScript.ObjectLiteralExpression;\n type PropertyName = TypeScript.PropertyName;\n type SourceFile = TypeScript.SourceFile;\n type StringLiteralLike = TypeScript.StringLiteralLike;\n type TemplateExpression = TypeScript.TemplateExpression;\n type TemplateLiteralLikeNode = TypeScript.TemplateLiteralLikeNode;\n type VariableStatement = TypeScript.VariableStatement;\n}\n\n/**\n * Points the rules at the repository in `root` and loads its TypeScript. Call it before any other export of this\n * folder: they read `repoRoot` and `ts`.\n * @throws when the root package has no `typescript` installed\n */\nexport async function openRepository(root: string): Promise<void> {\n ts = await importFromWorkspace<typeof TypeScript>(root, '.', 'typescript');\n repoRoot = root;\n}\n\n/** One style finding. */\nexport interface Finding {\n file: string;\n /** 1-based, as is `column`; absent when the finding is about the path itself. */\n line?: number;\n column?: number;\n rule: string;\n /** The matched text, printed in quotes before the message; absent when the finding names no single term. */\n term?: string;\n message: string;\n}\n\n/** Repo-relative files git tracks, plus untracked ones it does not ignore, that exist on disk, in `git ls-files` order. */\nexport function repoFiles(): string[] {\n return execFileSync('git', ['ls-files', '-co', '--exclude-standard'], { cwd: repoRoot, encoding: 'utf8' })\n .split('\\n')\n .filter((file) => file && existsSync(join(repoRoot, file)));\n}\n\n/** Whether `file` is the path `entry` or inside it; `entry` may end in `/`. */\nexport function isUnder(file: string, entry: string): boolean {\n return file === entry || file.startsWith(entry.endsWith('/') ? entry : `${entry}/`);\n}\n\n/** Whether `file` is one of the requested roots or inside one; no roots requests every file. */\nexport function isRequested(file: string, roots: string[]): boolean {\n return roots.length === 0 || roots.some((root) => isUnder(file, root.replace(/^\\.\\//, '').replace(/\\/$/, '')));\n}\n\n/** App sources under frontend/src, outside generated files, content, stories and tests: the files the frontend convention rules read. */\nexport function isFrontendSource(file: string): boolean {\n return (\n /^frontend\\/src\\/.*\\.tsx?$/.test(file) &&\n !file.includes('.gen.') &&\n !file.includes('/content/') &&\n !file.includes('/stories/') &&\n !/\\.(?:stories|test)\\.tsx?$/.test(file)\n );\n}\n\n/** `text` with every character but a line break turned into a space, so offsets and line numbers into it still hold. */\nexport function blankOut(text: string): string {\n return text.replace(/[^\\n]/g, ' ');\n}\n\n/** 1-based line and column of `offset`, counting `\\n` line breaks only. */\nexport function lineColumn(source: string, offset: number): { line: number; column: number } {\n const lines = source.slice(0, offset).split('\\n');\n return { line: lines.length, column: lines.at(-1)!.length + 1 };\n}\n\n/** `file:line:column [rule] \"term\": message`; a finding about the path itself prints the path alone. */\nexport function formatFinding({ file, line, column, rule, term, message }: Finding): string {\n const location = line === undefined ? file : `${file}:${line}:${column}`;\n return `${location} [${rule}] ${term === undefined ? '' : `\"${term}\": `}${message}`;\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 { ts } from './repo';\n\n/** A whitespace-separated token of a string and its offset in the source file. */\nexport type Token = { token: string; offset: number };\n\n/**\n * The variants and the utility of a class token, without the important marker: `md:hover:x!` gives `['md', 'hover']`\n * and `x`. A `:` inside `[]` or `()` belongs to an arbitrary value or variant, as in `has-[a:hover]:x` or `text-[length:1em]`.\n */\nexport function splitToken(token: string): { variants: string[]; base: string } {\n const text = token.replace(/^!|!$/g, '');\n const variants: string[] = [];\n let depth = 0;\n let start = 0;\n for (let index = 0; index < text.length; index++) {\n const char = text[index];\n if (char === '[' || char === '(') depth++;\n else if (char === ']' || char === ')') depth--;\n else if (char === ':' && depth === 0) {\n variants.push(text.slice(start, index));\n start = index + 1;\n }\n }\n return { variants, base: text.slice(start).replace(/^!/, '') };\n}\n\n/** Whitespace-separated tokens of every string and template piece in `node` and under it, with their source offsets. */\nexport function stringTokens(sourceFile: ts.SourceFile, node: ts.Node): Token[] {\n const found: Token[] = [];\n (function visit(child: ts.Node) {\n if (\n ts.isStringLiteralLike(child) ||\n ts.isTemplateHead(child) ||\n ts.isTemplateMiddle(child) ||\n ts.isTemplateTail(child)\n ) {\n const start = child.getStart(sourceFile) + 1;\n for (const match of child.text.matchAll(/\\S+/g)) found.push({ token: match[0], offset: start + match.index });\n }\n child.forEachChild(visit);\n })(node);\n return found;\n}\n","import { extname } from 'node:path';\nimport { ts } from './repo';\n\n/** One comment of a source file, with its delimiters. */\nexport interface Comment {\n /** Offset of the comment's first character in the file. */\n offset: number;\n /** Offset just past its last character. */\n end: number;\n text: string;\n}\n\n/** Extensions of the script files, whose comments come from the TypeScript parse. */\nexport const scriptExtensions = new Set(['.cjs', '.js', '.jsx', '.mjs', '.ts', '.tsx']);\n\nlet lastParsed: ts.SourceFile | undefined;\n\n/** The TypeScript parse of a script file; the checks of one file share it. */\nexport function parseSource(file: string, source: string): ts.SourceFile {\n if (lastParsed?.fileName === file && lastParsed.text === source) return lastParsed;\n const kind = ['.tsx', '.jsx'].includes(extname(file)) ? ts.ScriptKind.TSX : ts.ScriptKind.TS;\n lastParsed = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, kind);\n return lastParsed;\n}\n\n/**\n * Comments at every token boundary, which reaches the ones after an opening bracket, in an empty JSX expression and\n * before a `.` that starts a line too: every comment trails a token on its line or leads the token after a line break.\n */\nfunction scriptComments(file: string, source: string): Comment[] {\n const sourceFile = parseSource(file, source);\n const comments = new Map<number, Comment>();\n const add = (ranges: readonly ts.CommentRange[] | undefined) => {\n for (const { pos, end } of ranges ?? []) comments.set(pos, { offset: pos, end, text: source.slice(pos, end) });\n };\n const visit = (node: ts.Node) => {\n if (ts.isJSDoc(node)) return;\n const children = node.getChildren(sourceFile);\n if (children.length === 0) {\n add(ts.getLeadingCommentRanges(source, node.pos));\n add(ts.getTrailingCommentRanges(source, node.end));\n }\n for (const child of children) visit(child);\n };\n visit(sourceFile);\n return [...comments.values()].sort((a, b) => a.offset - b.offset);\n}\n\nfunction jsoncComments(source: string): Comment[] {\n const comments: Comment[] = [];\n for (let i = 0; i < source.length; i++) {\n if (source[i] === '\"') {\n for (i++; i < source.length; i++) {\n if (source[i] === '\\\\') i++;\n else if (source[i] === '\"') break;\n }\n continue;\n }\n if (source[i] !== '/' || (source[i + 1] !== '/' && source[i + 1] !== '*')) continue;\n\n const start = i;\n if (source[++i] === '/') {\n while (i + 1 < source.length && source[i + 1] !== '\\n') i++;\n } else {\n while (i + 1 < source.length && !(source[i] === '*' && source[i + 1] === '/')) i++;\n if (i + 1 < source.length) i++;\n }\n comments.push({ offset: start, end: i + 1, text: source.slice(start, i + 1) });\n }\n return comments;\n}\n\n/** Index of the `#` that opens a comment on a YAML line, or -1; `#` inside a quoted scalar or a word is text. */\nfunction commentHash(line: string): number {\n let quote = '';\n for (let i = 0; i < line.length; i++) {\n const char = line[i];\n if (quote) {\n if (char === '\\\\' && quote === '\"') i++;\n else if (char === quote && quote === \"'\" && line[i + 1] === \"'\") i++;\n else if (char === quote) quote = '';\n } else if ((char === '\"' || char === \"'\") && /^$|[\\s[{,]$/.test(line.slice(0, i))) {\n quote = char;\n } else if (char === '#' && (i === 0 || line[i - 1] === ' ' || line[i - 1] === '\\t')) {\n return i;\n }\n }\n return -1;\n}\n\n/** Comments after a YAML value, outside block scalars, whose lines hold text in another language. */\nfunction yamlTrailingComments(source: string): Comment[] {\n const comments: Comment[] = [];\n let offset = 0;\n let blockIndent = -1;\n for (const line of source.split('\\n')) {\n const indent = line.length - line.trimStart().length;\n if (blockIndent < 0 || (line.trim() && indent <= blockIndent)) {\n const hash = commentHash(line);\n const value = hash < 0 ? line : line.slice(0, hash);\n if (hash >= 0 && value.trim()) {\n comments.push({ offset: offset + hash, end: offset + line.length, text: line.slice(hash) });\n }\n blockIndent = value.trim() && /(?:^|\\s)[|>][0-9+-]*\\s*$/.test(value) ? indent : -1;\n }\n offset += line.length + 1;\n }\n return comments;\n}\n\n/**\n * The comments of a source file: the TypeScript parse for scripts, a string-aware scanner for JSONC, block comments for\n * CSS and SCSS, block then line comments for SQL, and `#` lines for YAML (with the comments after a value, in document\n * order), Dockerfile, Caddyfile and env files. JSON has none.\n */\nexport function sourceComments(file: string, source: string): Comment[] {\n const extension = extname(file);\n if (scriptExtensions.has(extension)) return scriptComments(file, source);\n if (extension === '.jsonc') return jsoncComments(source);\n if (extension === '.json') return [];\n const patterns =\n extension === '.css' || extension === '.scss'\n ? [/\\/\\*[\\s\\S]*?\\*\\//g]\n : extension === '.sql'\n ? [/\\/\\*[\\s\\S]*?\\*\\//g, /--[^\\n]*/g]\n : [/^[\\t ]*#[^\\n]*/gm];\n const comments = patterns.flatMap((pattern) =>\n [...source.matchAll(pattern)].map((match) => ({\n offset: match.index,\n end: match.index + match[0].length,\n text: match[0],\n })),\n );\n if (extension !== '.yaml' && extension !== '.yml') return comments;\n return [...comments, ...yamlTrailingComments(source)].sort((a, b) => a.offset - b.offset);\n}\n","import { readFileSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport { dirname, join, relative } from 'node:path';\nimport { importFromWorkspace } from '../../utils/app-facts';\nimport { splitToken, type Token } from './class-tokens';\nimport { type Finding, lineColumn, repoRoot, ts } from './repo';\nimport { parseSource } from './source-comments';\n\n/** Class names no frontend stylesheet, script or class string selects, each with the reason it stays. */\nconst markerClasses: Record<string, string> = {\n 'is-selected': 'BoardPanel state for apps whose board selects items (hasSelection), a hook for their styles',\n};\n\n/** Third-party class prefixes: the library's own stylesheet or DOM code reads them. */\nconst libraryPrefixes: Record<string, string> = {\n 'bn-': 'BlockNote editor parts',\n rdg: 'data grid rows, cells and states',\n};\n\n/** Calls whose arguments are class values; object keys are class names and object values conditions, as in clsx. */\nconst classFunctions = new Set(['clsx', 'cn', 'cx', 'tw', 'twJoin', 'twMerge']);\nconst classNamePattern =\n /^(?:class|classes|className|classNames|classList)$|[a-z](?:Class|Classes|Class[Nn]ames?|ClassList)$/;\nconst groupPattern = /^(?:group|peer)(?:\\/[\\w-]+)?$/;\nconst tailwindImport = /@import\\s+['\"]tailwindcss(?:\\/[\\w.-]+)?['\"]/;\n\ninterface DesignSystem {\n candidatesToCss(classes: string[]): (string | null)[];\n}\n\ninterface TailwindNode {\n __unstable__loadDesignSystem(css: string, options: { base: string }): Promise<DesignSystem>;\n}\n\n/** The loaded design system plus the class names that stylesheets, scripts and arbitrary selectors use. */\nexport interface TailwindContext {\n /** Repo-relative stylesheet that imports Tailwind. */\n entry: string;\n /** Absent when loading failed, as on an `@apply` of an unknown class; `loadError` then holds Tailwind's message. */\n designSystem?: DesignSystem;\n loadError?: string;\n /** Class names a stylesheet, script or arbitrary selector uses, and the marker classes. */\n knownClasses: Set<string>;\n /** Every candidate sent to Tailwind so far, and whether it compiled to CSS. */\n compiles: Map<string, boolean>;\n}\n\ntype Mode = 'class' | 'clsx';\n\n/** Text of one string or template piece in a class position; `open*` marks a side that touches a `${...}` placeholder. */\ninterface ClassText {\n /** Offset of `raw` in the source file. */\n start: number;\n /** The value, escapes resolved. */\n text: string;\n /** The source text of the value, which `start` locates. */\n raw: string;\n openStart: boolean;\n openEnd: boolean;\n}\n\nfunction isFrontendScript(file: string): boolean {\n return /^frontend\\/src\\/.*\\.tsx?$/.test(file) && !file.endsWith('.d.ts') && !file.includes('.gen.');\n}\n\nfunction isFrontendStylesheet(file: string): boolean {\n return /^frontend\\/src\\/.*\\.css$/.test(file);\n}\n\n/** Test files pass made-up class names to check prop forwarding and merging. */\nexport function isClassSource(file: string): boolean {\n return isFrontendScript(file) && !/\\.test\\.tsx?$/.test(file);\n}\n\n/** `@tailwindcss/node` from the frontend package, directly or through `@tailwindcss/vite`, which depends on it. */\nexport async function loadTailwindNode(): Promise<TailwindNode> {\n try {\n return await importFromWorkspace<TailwindNode>(repoRoot, 'frontend', '@tailwindcss/node');\n } catch {\n const plugin = createRequire(join(repoRoot, 'frontend/package.json')).resolve('@tailwindcss/vite');\n return importFromWorkspace<TailwindNode>(repoRoot, relative(repoRoot, dirname(plugin)), '@tailwindcss/node');\n }\n}\n\nfunction addMatches(target: Set<string>, text: string, pattern: RegExp): void {\n for (const match of text.matchAll(pattern)) target.add(match[1]);\n}\n\n/** Class names a stylesheet selects, a script queries or toggles, or an arbitrary selector such as `group-[.x]` names. */\nfunction usedClassNames(files: string[]): Set<string> {\n const names = new Set<string>();\n for (const file of files.filter(isFrontendStylesheet)) {\n const css = readFileSync(join(repoRoot, file), 'utf8').replace(/\\/\\*[\\s\\S]*?\\*\\//g, '');\n addMatches(names, css, /(?<!\\\\)\\.(-?[_a-zA-Z][\\w-]*)/g);\n }\n for (const file of files.filter(isFrontendScript)) {\n const source = readFileSync(join(repoRoot, file), 'utf8');\n // Arbitrary selectors (`group-[.x]`, `[&_.x]`); a bracket after a name, `)`, `]` or `?.` indexes code.\n for (const [chunk] of source.matchAll(/(?<![\\w$)\\]?.])\\[[^\\s'\"`]*/g))\n addMatches(names, chunk, /(?<![a-zA-Z0-9$.)\\]?!])\\.(-?[_a-zA-Z][\\w-]*)/g);\n // Selector strings for querySelector, closest and the like.\n for (const [, , text] of source.matchAll(/(['\"`])((?:\\\\.|(?!\\1)[^\\\\\\n])*)\\1/g)) {\n if (/^[\\w\\s.#>+~,:()[\\]=\"*-]+$/.test(text)) addMatches(names, text, /(?:^|[\\s>+~,(])\\.(-?[_a-zA-Z][\\w-]*)/g);\n }\n for (const [, args] of source.matchAll(\n /(?:classList\\.(?:add|contains|remove|replace|toggle)|getElementsByClassName)\\(([^)]*)\\)/g,\n )) {\n for (const [, text] of args.matchAll(/['\"`]([^'\"`]+)['\"`]/g))\n for (const name of text.split(/\\s+/)) names.add(name);\n }\n }\n return names;\n}\n\n/** The design system from the frontend stylesheet that imports Tailwind; undefined in a repo without one. */\nexport async function tailwindContext(\n files: string[],\n appMarkerClasses: Record<string, string> = {},\n): Promise<TailwindContext | undefined> {\n const entry = files\n .filter(isFrontendStylesheet)\n .find((file) => tailwindImport.test(readFileSync(join(repoRoot, file), 'utf8')));\n if (!entry) return undefined;\n const path = join(repoRoot, entry);\n const knownClasses = usedClassNames(files);\n for (const name of Object.keys({ ...markerClasses, ...appMarkerClasses })) knownClasses.add(name);\n const context: TailwindContext = { entry, knownClasses, compiles: new Map() };\n try {\n context.designSystem = await (await loadTailwindNode()).__unstable__loadDesignSystem(readFileSync(path, 'utf8'), {\n base: dirname(path),\n });\n } catch (error) {\n context.loadError = error instanceof Error ? error.message : String(error);\n }\n return context;\n}\n\nfunction propertyName(name: ts.PropertyName | ts.BindingName | undefined): string | undefined {\n return name && (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isPrivateIdentifier(name))\n ? name.text\n : undefined;\n}\n\nfunction isClassNamed(...names: (string | undefined)[]): boolean {\n return names.some((name) => name !== undefined && classNamePattern.test(name));\n}\n\n/** Strings in class positions: class attributes, class helper calls, cva, `tw` templates and class-named values. */\nexport function classTexts(sourceFile: ts.SourceFile): ClassText[] {\n const texts: ClassText[] = [];\n const add = (node: ts.StringLiteralLike | ts.TemplateLiteralLikeNode, openStart = false, openEnd = false) => {\n const start = node.getStart(sourceFile) + 1;\n const end = node.end - (ts.isTemplateHead(node) || ts.isTemplateMiddle(node) ? 2 : 1);\n texts.push({ start, text: node.text, raw: sourceFile.text.slice(start, end), openStart, openEnd });\n };\n const addKey = (name: ts.Node) => {\n if (ts.isStringLiteral(name)) add(name);\n else if (ts.isIdentifier(name))\n texts.push({\n start: name.getStart(sourceFile),\n text: name.text,\n raw: name.text,\n openStart: false,\n openEnd: false,\n });\n else walk(name);\n };\n\n function collectTemplate(node: ts.TemplateExpression): void {\n add(node.head, false, true);\n let before = node.head.text;\n for (const [index, span] of node.templateSpans.entries()) {\n const last = index === node.templateSpans.length - 1;\n // A placeholder holds whole classes only when whitespace or the string edge separates it from the text around it.\n const leftClosed = /\\s$/.test(before) || (index === 0 && before === '');\n const rightClosed = /^\\s/.test(span.literal.text) || (last && span.literal.text === '');\n if (leftClosed && rightClosed) collect(span.expression, 'class');\n else walk(span.expression);\n add(span.literal, true, !last);\n before = span.literal.text;\n }\n }\n\n function collectObject(node: ts.ObjectLiteralExpression, mode: Mode): void {\n for (const property of node.properties) {\n if (mode === 'clsx' && (ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property))) {\n addKey(property.name);\n if (ts.isPropertyAssignment(property)) walk(property.initializer);\n } else if (ts.isPropertyAssignment(property)) {\n walk(property.name);\n collect(property.initializer, 'class');\n } else walk(property);\n }\n }\n\n function collectCva(node: ts.CallExpression): void {\n const [base, config, ...rest] = node.arguments;\n collect(base, 'class');\n for (const argument of rest) walk(argument);\n if (!config || !ts.isObjectLiteralExpression(config)) {\n walk(config);\n return;\n }\n for (const property of config.properties) {\n if (\n !ts.isPropertyAssignment(property) ||\n propertyName(property.name) !== 'variants' ||\n !ts.isObjectLiteralExpression(property.initializer)\n ) {\n walk(property);\n continue;\n }\n for (const variant of property.initializer.properties) {\n if (ts.isPropertyAssignment(variant) && ts.isObjectLiteralExpression(variant.initializer))\n collectObject(variant.initializer, 'class');\n else walk(variant);\n }\n }\n }\n\n function calleeName(node: ts.CallExpression): string | undefined {\n const callee = node.expression;\n return ts.isIdentifier(callee) ? callee.text : ts.isPropertyAccessExpression(callee) ? callee.name.text : undefined;\n }\n\n /** Whether `node` starts a class position, collecting it when it does. */\n function collectCall(node: ts.CallExpression, mode?: Mode): boolean {\n const name = calleeName(node);\n if (name === 'cva') collectCva(node);\n else if (name && classFunctions.has(name)) {\n walk(node.expression);\n for (const argument of node.arguments) collect(argument, 'clsx');\n } else if (mode && (name === 'join' || name === 'filter') && ts.isPropertyAccessExpression(node.expression)) {\n collect(node.expression.expression, mode);\n for (const argument of node.arguments) walk(argument);\n } else return false;\n return true;\n }\n\n function collectReturns(node: ts.FunctionLikeDeclaration, mode: Mode): void {\n for (const parameter of node.parameters) walk(parameter);\n if (node.body && !ts.isBlock(node.body)) collect(node.body, mode);\n else walk(node.body, mode);\n }\n\n function collect(node: ts.Node | undefined, mode: Mode): void {\n if (!node) return;\n if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) add(node);\n else if (ts.isTemplateExpression(node)) collectTemplate(node);\n else if (\n ts.isParenthesizedExpression(node) ||\n ts.isAsExpression(node) ||\n ts.isSatisfiesExpression(node) ||\n ts.isNonNullExpression(node) ||\n ts.isSpreadElement(node) ||\n ts.isJsxExpression(node)\n )\n collect(node.expression, mode);\n else if (ts.isConditionalExpression(node)) {\n walk(node.condition);\n collect(node.whenTrue, mode);\n collect(node.whenFalse, mode);\n } else if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {\n walk(node.left);\n collect(node.right, mode);\n } else if (\n ts.isBinaryExpression(node) &&\n [ts.SyntaxKind.BarBarToken, ts.SyntaxKind.QuestionQuestionToken].includes(node.operatorToken.kind)\n ) {\n collect(node.left, mode);\n collect(node.right, mode);\n } else if (ts.isArrayLiteralExpression(node)) {\n for (const element of node.elements) collect(element, mode);\n } else if (ts.isObjectLiteralExpression(node)) collectObject(node, mode);\n else if (ts.isElementAccessExpression(node)) {\n collect(node.expression, mode);\n walk(node.argumentExpression);\n } else if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) collectReturns(node, mode);\n else if (!ts.isCallExpression(node) || !collectCall(node, mode)) walk(node);\n }\n\n /** Collects `node` when it starts a class position; `returns` is set inside a class-named function. */\n function collectStart(node: ts.Node, returns?: Mode): boolean {\n if (ts.isJsxAttribute(node) && ts.isIdentifier(node.name) && isClassNamed(node.name.text))\n collect(node.initializer, 'class');\n else if (ts.isCallExpression(node)) return collectCall(node);\n else if (ts.isTaggedTemplateExpression(node) && ts.isIdentifier(node.tag) && node.tag.text === 'tw')\n collect(node.template, 'class');\n else if (ts.isReturnStatement(node) && returns) collect(node.expression, returns);\n // Storybook argTypes describe controls (`className: { control: 'text' }`), never classes.\n else if (ts.isPropertyAssignment(node) && propertyName(node.name) === 'argTypes') return true;\n else if (\n (ts.isVariableDeclaration(node) ||\n ts.isPropertyAssignment(node) ||\n ts.isPropertyDeclaration(node) ||\n ts.isParameter(node)) &&\n node.initializer &&\n isClassNamed(propertyName(node.name))\n ) {\n walk(node.name);\n collect(node.initializer, 'class');\n } else if (\n ts.isBindingElement(node) &&\n node.initializer &&\n isClassNamed(propertyName(node.name), propertyName(node.propertyName))\n ) {\n collect(node.initializer, 'class');\n } else if (\n (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) &&\n isClassNamed(propertyName(node.name))\n ) {\n collectReturns(node, 'class');\n } else return false;\n return true;\n }\n\n /** Visits `node` and its children looking for class positions. */\n function walk(node: ts.Node | undefined, returns?: Mode): void {\n if (!node || ts.isTypeNode(node) || collectStart(node, returns)) return;\n const inner = ts.isFunctionLike(node) ? undefined : returns;\n ts.forEachChild(node, (child) => walk(child, inner));\n }\n\n walk(sourceFile);\n return texts;\n}\n\n/** Whole tokens of `text` with their offsets; a token touching an open side is part of a dynamic class and skipped. */\nexport function classTokens({ start, text, raw, openStart, openEnd }: ClassText): Token[] {\n const result: Token[] = [];\n for (const match of text.matchAll(/\\S+/g)) {\n const end = match.index + match[0].length;\n if ((openStart && match.index === 0) || (openEnd && end === text.length)) continue;\n result.push({ token: match[0], offset: start + (raw === text ? match.index : Math.max(0, raw.indexOf(match[0]))) });\n }\n return result;\n}\n\n/** Compiles the candidates `context.compiles` does not hold yet, in one call, and records each result there. */\nfunction compileCandidates(context: TailwindContext, designSystem: DesignSystem, candidates: string[]): void {\n const unknown = [...new Set(candidates)].filter((candidate) => !context.compiles.has(candidate));\n const css = designSystem.candidatesToCss(unknown);\n for (const [index, candidate] of unknown.entries()) context.compiles.set(candidate, css[index] !== null);\n}\n\n/** Why `token` produces nothing, or undefined when Tailwind compiles it or it is a known non-Tailwind class. */\nfunction problem(context: TailwindContext, token: string): string | undefined {\n if (context.compiles.get(token) || groupPattern.test(token) || context.knownClasses.has(token)) return undefined;\n if (Object.keys(libraryPrefixes).some((prefix) => token.startsWith(prefix))) return undefined;\n const { base } = splitToken(token);\n if (base !== token && (context.knownClasses.has(base) || groupPattern.test(base))) {\n return `${base} is a plain CSS class, so variants and ! do not apply to it; define it with @utility`;\n }\n return 'Tailwind compiles no CSS for it and no stylesheet or script selects it; fix the name, or list it under markerClasses in shared/config/vocabulary-allowlist.ts';\n}\n\n/**\n * Class names in one frontend file that compile to no CSS; `context` comes from `tailwindContext`. Tailwind itself\n * rejects an `@apply` of an unknown class while loading, which this reports on the entry stylesheet.\n */\nexport function tailwindFindings(file: string, source: string, context: TailwindContext | undefined): Finding[] {\n if (!context) return [];\n const { designSystem, loadError } = context;\n if (!designSystem)\n return file === context.entry\n ? [{ file, rule: 'tailwind-class', message: `Tailwind cannot load this stylesheet: ${loadError}` }]\n : [];\n if (!isClassSource(file)) return [];\n const sourceFile = parseSource(file, source);\n const found = classTexts(sourceFile).flatMap(classTokens);\n compileCandidates(\n context,\n designSystem,\n found.map(({ token }) => token),\n );\n return found.flatMap(({ token, offset }) => {\n const message = problem(context, token);\n if (!message) return [];\n return [{ file, ...lineColumn(source, offset), rule: 'tailwind-class', term: token, message }];\n });\n}\n"],"mappings":"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","names":["execFileSync","existsSync","join","require","execFileSync","existsSync","join","readFileSync","createRequire","dirname","join","createRequire","join","dirname"]}
|