@cellajs/cli 0.6.2 → 0.6.4

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/dist/style.js ADDED
@@ -0,0 +1,268 @@
1
+ // src/tools/style/repo.ts
2
+ import { execFileSync as execFileSync2 } from "child_process";
3
+ import { existsSync as existsSync2 } from "fs";
4
+ import { join as join2 } from "path";
5
+
6
+ // src/utils/app-facts.ts
7
+ import { execFileSync, spawnSync } from "child_process";
8
+ import { existsSync, readFileSync } from "fs";
9
+ import { createRequire } from "module";
10
+ import { dirname, join, resolve } from "path";
11
+ import { pathToFileURL } from "url";
12
+ import { z } from "zod";
13
+
14
+ // src/utils/env.ts
15
+ import process from "process";
16
+
17
+ // src/utils/app-facts.ts
18
+ var appConfigSchema = z.object({
19
+ frontendUrl: z.string(),
20
+ backendUrl: z.string(),
21
+ devPortOffset: z.number(),
22
+ services: z.record(
23
+ z.string(),
24
+ z.object({ enabled: z.boolean(), port: z.number(), internalPort: z.number().optional() })
25
+ )
26
+ });
27
+ async function importFromWorkspace(forkPath, workspace, name) {
28
+ const require2 = createRequire(join(forkPath, workspace, "package.json"));
29
+ let manifestPath;
30
+ try {
31
+ manifestPath = require2.resolve(`${name}/package.json`);
32
+ } catch {
33
+ }
34
+ const root = manifestPath ? JSON.parse(readFileSync(manifestPath, "utf8")).exports?.["."] : void 0;
35
+ const esm = typeof root?.import === "string" ? root.import : root?.import?.default;
36
+ if (manifestPath && typeof esm === "string") return import(pathToFileURL(join(dirname(manifestPath), esm)).href);
37
+ try {
38
+ return require2(name);
39
+ } catch {
40
+ throw new Error(`${name} is not installed in ${workspace}: run \`pnpm --filter ${workspace} add -D ${name}\``);
41
+ }
42
+ }
43
+
44
+ // src/tools/style/repo.ts
45
+ var repoRoot = "";
46
+ var ts;
47
+ async function openRepository(root) {
48
+ ts = await importFromWorkspace(root, ".", "typescript");
49
+ repoRoot = root;
50
+ }
51
+ function repoFiles() {
52
+ return execFileSync2("git", ["ls-files", "-co", "--exclude-standard"], { cwd: repoRoot, encoding: "utf8" }).split("\n").filter((file) => file && existsSync2(join2(repoRoot, file)));
53
+ }
54
+ function lineColumn(source, offset) {
55
+ const lines = source.slice(0, offset).split("\n");
56
+ return { line: lines.length, column: lines.at(-1).length + 1 };
57
+ }
58
+
59
+ // src/tools/style/class-tokens.ts
60
+ function splitToken(token) {
61
+ const text = token.replace(/^!|!$/g, "");
62
+ const variants = [];
63
+ let depth = 0;
64
+ let start = 0;
65
+ for (let index = 0; index < text.length; index++) {
66
+ const char = text[index];
67
+ if (char === "[" || char === "(") depth++;
68
+ else if (char === "]" || char === ")") depth--;
69
+ else if (char === ":" && depth === 0) {
70
+ variants.push(text.slice(start, index));
71
+ start = index + 1;
72
+ }
73
+ }
74
+ return { variants, base: text.slice(start).replace(/^!/, "") };
75
+ }
76
+
77
+ // src/tools/style/source-comments.ts
78
+ import { extname } from "path";
79
+ var lastParsed;
80
+ function parseSource(file, source) {
81
+ if (lastParsed?.fileName === file && lastParsed.text === source) return lastParsed;
82
+ const kind = [".tsx", ".jsx"].includes(extname(file)) ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
83
+ lastParsed = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, kind);
84
+ return lastParsed;
85
+ }
86
+
87
+ // src/tools/style/tailwind-class-rules.ts
88
+ import { readFileSync as readFileSync2 } from "fs";
89
+ import { createRequire as createRequire2 } from "module";
90
+ import { dirname as dirname2, join as join3, relative } from "path";
91
+ var classFunctions = /* @__PURE__ */ new Set(["clsx", "cn", "cx", "tw", "twJoin", "twMerge"]);
92
+ var classNamePattern = /^(?:class|classes|className|classNames|classList)$|[a-z](?:Class|Classes|Class[Nn]ames?|ClassList)$/;
93
+ function isFrontendScript(file) {
94
+ return /^frontend\/src\/.*\.tsx?$/.test(file) && !file.endsWith(".d.ts") && !file.includes(".gen.");
95
+ }
96
+ function isClassSource(file) {
97
+ return isFrontendScript(file) && !/\.test\.tsx?$/.test(file);
98
+ }
99
+ async function loadTailwindNode() {
100
+ try {
101
+ return await importFromWorkspace(repoRoot, "frontend", "@tailwindcss/node");
102
+ } catch {
103
+ const plugin = createRequire2(join3(repoRoot, "frontend/package.json")).resolve("@tailwindcss/vite");
104
+ return importFromWorkspace(repoRoot, relative(repoRoot, dirname2(plugin)), "@tailwindcss/node");
105
+ }
106
+ }
107
+ function propertyName(name) {
108
+ return name && (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isPrivateIdentifier(name)) ? name.text : void 0;
109
+ }
110
+ function isClassNamed(...names) {
111
+ return names.some((name) => name !== void 0 && classNamePattern.test(name));
112
+ }
113
+ function classTexts(sourceFile) {
114
+ const texts = [];
115
+ const add = (node, openStart = false, openEnd = false) => {
116
+ const start = node.getStart(sourceFile) + 1;
117
+ const end = node.end - (ts.isTemplateHead(node) || ts.isTemplateMiddle(node) ? 2 : 1);
118
+ texts.push({ start, text: node.text, raw: sourceFile.text.slice(start, end), openStart, openEnd });
119
+ };
120
+ const addKey = (name) => {
121
+ if (ts.isStringLiteral(name)) add(name);
122
+ else if (ts.isIdentifier(name))
123
+ texts.push({
124
+ start: name.getStart(sourceFile),
125
+ text: name.text,
126
+ raw: name.text,
127
+ openStart: false,
128
+ openEnd: false
129
+ });
130
+ else walk(name);
131
+ };
132
+ function collectTemplate(node) {
133
+ add(node.head, false, true);
134
+ let before = node.head.text;
135
+ for (const [index, span] of node.templateSpans.entries()) {
136
+ const last = index === node.templateSpans.length - 1;
137
+ const leftClosed = /\s$/.test(before) || index === 0 && before === "";
138
+ const rightClosed = /^\s/.test(span.literal.text) || last && span.literal.text === "";
139
+ if (leftClosed && rightClosed) collect(span.expression, "class");
140
+ else walk(span.expression);
141
+ add(span.literal, true, !last);
142
+ before = span.literal.text;
143
+ }
144
+ }
145
+ function collectObject(node, mode) {
146
+ for (const property of node.properties) {
147
+ if (mode === "clsx" && (ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property))) {
148
+ addKey(property.name);
149
+ if (ts.isPropertyAssignment(property)) walk(property.initializer);
150
+ } else if (ts.isPropertyAssignment(property)) {
151
+ walk(property.name);
152
+ collect(property.initializer, "class");
153
+ } else walk(property);
154
+ }
155
+ }
156
+ function collectCva(node) {
157
+ const [base, config, ...rest] = node.arguments;
158
+ collect(base, "class");
159
+ for (const argument of rest) walk(argument);
160
+ if (!config || !ts.isObjectLiteralExpression(config)) {
161
+ walk(config);
162
+ return;
163
+ }
164
+ for (const property of config.properties) {
165
+ if (!ts.isPropertyAssignment(property) || propertyName(property.name) !== "variants" || !ts.isObjectLiteralExpression(property.initializer)) {
166
+ walk(property);
167
+ continue;
168
+ }
169
+ for (const variant of property.initializer.properties) {
170
+ if (ts.isPropertyAssignment(variant) && ts.isObjectLiteralExpression(variant.initializer))
171
+ collectObject(variant.initializer, "class");
172
+ else walk(variant);
173
+ }
174
+ }
175
+ }
176
+ function calleeName(node) {
177
+ const callee = node.expression;
178
+ return ts.isIdentifier(callee) ? callee.text : ts.isPropertyAccessExpression(callee) ? callee.name.text : void 0;
179
+ }
180
+ function collectCall(node, mode) {
181
+ const name = calleeName(node);
182
+ if (name === "cva") collectCva(node);
183
+ else if (name && classFunctions.has(name)) {
184
+ walk(node.expression);
185
+ for (const argument of node.arguments) collect(argument, "clsx");
186
+ } else if (mode && (name === "join" || name === "filter") && ts.isPropertyAccessExpression(node.expression)) {
187
+ collect(node.expression.expression, mode);
188
+ for (const argument of node.arguments) walk(argument);
189
+ } else return false;
190
+ return true;
191
+ }
192
+ function collectReturns(node, mode) {
193
+ for (const parameter of node.parameters) walk(parameter);
194
+ if (node.body && !ts.isBlock(node.body)) collect(node.body, mode);
195
+ else walk(node.body, mode);
196
+ }
197
+ function collect(node, mode) {
198
+ if (!node) return;
199
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) add(node);
200
+ else if (ts.isTemplateExpression(node)) collectTemplate(node);
201
+ else if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isSatisfiesExpression(node) || ts.isNonNullExpression(node) || ts.isSpreadElement(node) || ts.isJsxExpression(node))
202
+ collect(node.expression, mode);
203
+ else if (ts.isConditionalExpression(node)) {
204
+ walk(node.condition);
205
+ collect(node.whenTrue, mode);
206
+ collect(node.whenFalse, mode);
207
+ } else if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
208
+ walk(node.left);
209
+ collect(node.right, mode);
210
+ } else if (ts.isBinaryExpression(node) && [ts.SyntaxKind.BarBarToken, ts.SyntaxKind.QuestionQuestionToken].includes(node.operatorToken.kind)) {
211
+ collect(node.left, mode);
212
+ collect(node.right, mode);
213
+ } else if (ts.isArrayLiteralExpression(node)) {
214
+ for (const element of node.elements) collect(element, mode);
215
+ } else if (ts.isObjectLiteralExpression(node)) collectObject(node, mode);
216
+ else if (ts.isElementAccessExpression(node)) {
217
+ collect(node.expression, mode);
218
+ walk(node.argumentExpression);
219
+ } else if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) collectReturns(node, mode);
220
+ else if (!ts.isCallExpression(node) || !collectCall(node, mode)) walk(node);
221
+ }
222
+ function collectStart(node, returns) {
223
+ if (ts.isJsxAttribute(node) && ts.isIdentifier(node.name) && isClassNamed(node.name.text))
224
+ collect(node.initializer, "class");
225
+ else if (ts.isCallExpression(node)) return collectCall(node);
226
+ else if (ts.isTaggedTemplateExpression(node) && ts.isIdentifier(node.tag) && node.tag.text === "tw")
227
+ collect(node.template, "class");
228
+ else if (ts.isReturnStatement(node) && returns) collect(node.expression, returns);
229
+ else if (ts.isPropertyAssignment(node) && propertyName(node.name) === "argTypes") return true;
230
+ else if ((ts.isVariableDeclaration(node) || ts.isPropertyAssignment(node) || ts.isPropertyDeclaration(node) || ts.isParameter(node)) && node.initializer && isClassNamed(propertyName(node.name))) {
231
+ walk(node.name);
232
+ collect(node.initializer, "class");
233
+ } else if (ts.isBindingElement(node) && node.initializer && isClassNamed(propertyName(node.name), propertyName(node.propertyName))) {
234
+ collect(node.initializer, "class");
235
+ } else if ((ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) && isClassNamed(propertyName(node.name))) {
236
+ collectReturns(node, "class");
237
+ } else return false;
238
+ return true;
239
+ }
240
+ function walk(node, returns) {
241
+ if (!node || ts.isTypeNode(node) || collectStart(node, returns)) return;
242
+ const inner = ts.isFunctionLike(node) ? void 0 : returns;
243
+ ts.forEachChild(node, (child) => walk(child, inner));
244
+ }
245
+ walk(sourceFile);
246
+ return texts;
247
+ }
248
+ function classTokens({ start, text, raw, openStart, openEnd }) {
249
+ const result = [];
250
+ for (const match of text.matchAll(/\S+/g)) {
251
+ const end = match.index + match[0].length;
252
+ if (openStart && match.index === 0 || openEnd && end === text.length) continue;
253
+ result.push({ token: match[0], offset: start + (raw === text ? match.index : Math.max(0, raw.indexOf(match[0]))) });
254
+ }
255
+ return result;
256
+ }
257
+ export {
258
+ classTexts,
259
+ classTokens,
260
+ isClassSource,
261
+ lineColumn,
262
+ loadTailwindNode,
263
+ openRepository,
264
+ parseSource,
265
+ repoFiles,
266
+ splitToken
267
+ };
268
+ //# sourceMappingURL=style.js.map
@@ -0,0 +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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cellajs/cli",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "description": "CLI for managing your monorepo and upgrading it with upstream Cella.",
@@ -21,6 +21,30 @@
21
21
  "types": "./dist/config.d.ts",
22
22
  "import": "./dist/config.js"
23
23
  },
24
+ "./bench": {
25
+ "types": "./dist/bench.d.ts",
26
+ "import": "./dist/bench.js"
27
+ },
28
+ "./style": {
29
+ "types": "./dist/style.d.ts",
30
+ "import": "./dist/style.js"
31
+ },
32
+ "./drive": {
33
+ "types": "./dist/drive.d.ts",
34
+ "import": "./dist/drive.js"
35
+ },
36
+ "./a11y": {
37
+ "types": "./dist/a11y.d.ts",
38
+ "import": "./dist/a11y.js"
39
+ },
40
+ "./screenshots": {
41
+ "types": "./dist/screenshots.d.ts",
42
+ "import": "./dist/screenshots.js"
43
+ },
44
+ "./pentest": {
45
+ "types": "./dist/pentest.d.ts",
46
+ "import": "./dist/pentest.js"
47
+ },
24
48
  "./package.json": "./package.json"
25
49
  },
26
50
  "files": [
@@ -38,6 +62,7 @@
38
62
  "@inquirer/core": "^12.0.3",
39
63
  "@inquirer/prompts": "^8.7.2",
40
64
  "@pierre/diffs": "^1.5.1",
65
+ "axe-core": "^4.14.0",
41
66
  "commander": "^15.0.0",
42
67
  "ts-morph": "^28.0.0",
43
68
  "yaml": "^2.9.1",
@@ -48,9 +73,12 @@
48
73
  "@commitlint/cli": "^21.2.3",
49
74
  "@commitlint/config-conventional": "^21.2.3",
50
75
  "@evilmartians/lefthook": "^2.1.15",
76
+ "@tailwindcss/node": "^4.3.3",
51
77
  "@types/node": "26.6.3",
52
78
  "@typescript/native-preview": "7.0.0-dev.20260707.2",
79
+ "playwright": "^1.64.1",
53
80
  "rimraf": "^6.1.3",
81
+ "tailwindcss": "^4.3.3",
54
82
  "tsup": "^8.5.1",
55
83
  "tsx": "^4.23.15",
56
84
  "typescript": "6.0.3",