Skip to content

Commit fea78c7

Browse files
feat: token and recipe analysis (#3035)
* wip * refactor: code and mv to node pkg * docs: add changeset * refactor: create reporter package * fix: recipe and token detection * refactor: classify pattern * refactor: add support for output format * refactor: format * refactor: support scope and classify global css * refactor: update tokens * refactor: slice most used * refactor: update * chore: rm unused * docs: update changeset * refactor: expose token maps * refactor: fix typo --------- Co-authored-by: Alexandre Stahmer <[email protected]>
1 parent 552b01c commit fea78c7

28 files changed

+6852
-5634
lines changed

Diff for: .changeset/nervous-pans-enjoy.md

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@pandacss/types': minor
3+
'@pandacss/node': minor
4+
'@pandacss/dev': minor
5+
'@pandacss/reporter': minor
6+
---
7+
8+
Adds support for static analysis of used tokens and recipe variants. It helps to get a birds-eye view of how your design
9+
system is used and answers the following questions:
10+
11+
- What tokens are most used?
12+
- What recipe variants are most used?
13+
- How many hardcoded values vs tokens do we have?
14+
15+
```sh
16+
panda analyze --scope=<token|recipe>
17+
```
18+
19+
> Still work in progress but we're excited to get your feedback!

Diff for: packages/cli/src/cli-main.ts

+17-10
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { findConfig } from '@pandacss/config'
22
import { colors, logger } from '@pandacss/logger'
33
import {
44
PandaContext,
5-
analyzeTokens,
5+
analyze,
66
buildInfo,
77
codegen,
88
cssgen,
@@ -14,7 +14,6 @@ import {
1414
setupGitIgnore,
1515
setupPostcss,
1616
startProfiling,
17-
writeAnalyzeJSON,
1817
type CssGenOptions,
1918
} from '@pandacss/node'
2019
import { PandaError, compact } from '@pandacss/shared'
@@ -361,10 +360,14 @@ export async function main() {
361360
.command('analyze [glob]', 'Analyze design token usage in glob')
362361
.option('--outfile [filepath]', 'Output analyze report in JSON')
363362
.option('--silent', "Don't print any logs")
363+
.option('--scope <type>', 'Select analysis scope (token or recipe)')
364364
.option('-c, --config <path>', 'Path to panda config file')
365365
.option('--cwd <cwd>', 'Current working directory', { default: cwd })
366366
.action(async (maybeGlob?: string, flags: AnalyzeCommandFlags = {}) => {
367-
const { silent, config: configPath } = flags
367+
const { silent, config: configPath, scope } = flags
368+
369+
const tokenScope = scope == null || scope === 'token'
370+
const recipeScope = scope == null || scope === 'recipe'
368371

369372
const cwd = resolve(flags.cwd!)
370373

@@ -378,19 +381,23 @@ export async function main() {
378381
configPath,
379382
})
380383

381-
const result = analyzeTokens(ctx, {
382-
onResult(file) {
383-
logger.info('cli', `Analyzed ${colors.bold(file)}`)
384-
},
385-
})
384+
const result = analyze(ctx)
386385

387386
if (flags?.outfile && typeof flags.outfile === 'string') {
388-
await writeAnalyzeJSON(flags.outfile, result, ctx)
387+
await result.writeReport(flags.outfile)
389388
logger.info('cli', `JSON report saved to ${resolve(flags.outfile)}`)
390389
return
391390
}
392391

393-
logger.info('cli', `Found ${result.propByIndex.size} token used in ${result.derived.byFilePathMaps.size} files`)
392+
if (tokenScope && !ctx.tokens.isEmpty) {
393+
const tokenAnalysis = result.getTokenReport()
394+
logger.info('analyze:tokens', `Token usage report 🎨 \n${tokenAnalysis.formatted}`)
395+
}
396+
397+
if (recipeScope && !ctx.recipes.isEmpty()) {
398+
const recipeAnalysis = result.getRecipeReport()
399+
logger.info('analyze:recipes', `Recipe usage report 🎛️ \n${recipeAnalysis.formatted}`)
400+
}
394401
})
395402

396403
cli

Diff for: packages/cli/src/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export interface AnalyzeCommandFlags {
4242
outfile?: string
4343
cwd?: string
4444
config?: string
45+
scope?: 'token' | 'recipe'
4546
}
4647

4748
export interface DebugCommandFlags {

Diff for: packages/core/src/context.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,10 @@ export class Context {
204204
recipes: this.recipes,
205205
patterns: this.patterns,
206206
jsx: this.jsx,
207-
syntax: config.syntax,
207+
config: this.config,
208+
tokens: this.tokens,
209+
conditions: this.conditions,
210+
utility: this.utility,
208211
encoder: this.encoder,
209212
tsOptions: this.conf.tsOptions,
210213
join: (...paths: string[]) => paths.join('/'),

Diff for: packages/core/src/recipes.ts

+4
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ export class Recipes {
5353

5454
private context!: SerializeContext
5555

56+
get config() {
57+
return this.recipes
58+
}
59+
5660
constructor(private recipes: RecipeRecord = {}) {
5761
this.prune()
5862
}

Diff for: packages/core/src/types.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { TokenDictionary } from '@pandacss/token-dictionary'
12
import type {
23
Config,
34
Dict,
@@ -9,13 +10,15 @@ import type {
910
TSConfig,
1011
UserConfig,
1112
} from '@pandacss/types'
13+
import type { Conditions } from './conditions'
14+
import type { Context } from './context'
1215
import type { ImportMap } from './import-map'
1316
import type { JsxEngine } from './jsx'
1417
import type { Layers } from './layers'
1518
import type { Patterns } from './patterns'
1619
import type { Recipes } from './recipes'
1720
import type { StyleEncoder } from './style-encoder'
18-
import type { Context } from './context'
21+
import type { Utility } from './utility'
1922

2023
export interface TransformResult {
2124
layer?: string
@@ -114,9 +117,12 @@ export interface ParserOptions {
114117
hash: HashOptions
115118
imports: ImportMap
116119
jsx: JsxEngine
117-
syntax: Config['syntax']
120+
config: Config
118121
recipes: Recipes
122+
tokens: TokenDictionary
119123
patterns: Patterns
124+
utility: Utility
125+
conditions: Conditions
120126
encoder: StyleEncoder
121127
join: (...paths: string[]) => string
122128
compilerOptions: TSConfig['compilerOptions']

Diff for: packages/core/src/utility.ts

+26-5
Original file line numberDiff line numberDiff line change
@@ -413,17 +413,24 @@ export class Utility {
413413
return this
414414
}
415415

416-
private getTransformArgs = (raw: string): TransformArgs => {
417-
const token = Object.assign(this.getToken.bind(this), {
416+
private getTokenFn = () => {
417+
return Object.assign(this.getToken.bind(this), {
418418
raw: (path: string) => this.tokens.getByName(path),
419419
})
420+
}
420421

421-
const _colorMix = (value: string) => colorMix(value, token)
422+
resolveColorMix = (value: string) => {
423+
const token = this.getTokenFn()
424+
return colorMix(value, token)
425+
}
422426

427+
private getTransformArgs = (raw: string): TransformArgs => {
423428
return {
424-
token,
429+
token: this.getTokenFn(),
425430
raw,
426-
utils: { colorMix: _colorMix },
431+
utils: {
432+
colorMix: this.resolveColorMix.bind(this),
433+
},
427434
}
428435
}
429436

@@ -548,4 +555,18 @@ export class Utility {
548555
isDeprecated = (prop: string) => {
549556
return this.deprecated.has(prop)
550557
}
558+
559+
/**
560+
* Returns the token type for a given property
561+
*/
562+
getTokenType = (prop: string) => {
563+
const set = this.types.get(prop)
564+
if (!set) return
565+
for (const type of set) {
566+
const match = type.match(TOKEN_TYPE_PATTERN)
567+
if (match) return match[1]
568+
}
569+
}
551570
}
571+
572+
const TOKEN_TYPE_PATTERN = /type:Tokens\["([^"]+)"\]/

Diff for: packages/extractor/src/box.ts

+9
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ export const box = {
3131
return new BoxNodeConditional({ type: 'conditional', whenTrue, whenFalse, node, stack })
3232
},
3333
from: toBoxNode,
34+
objectToMap: (value: Record<string, unknown>, node: Node, stack: Node[]) => {
35+
const map = new Map(
36+
Object.entries(value).map(([k, v]: [string, any]) => {
37+
const boxed = box.from(v, node, stack)
38+
return [k, boxed || null]
39+
}),
40+
)
41+
return new BoxNodeMap({ type: 'map', value: map, node, stack })
42+
},
3443
//
3544
emptyObject: (node: Node, stack: Node[]) => {
3645
return new BoxNodeObject({ type: 'object', value: {}, isEmpty: true, node, stack })

Diff for: packages/node/package.json

+1-3
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@
3939
"dependencies": {
4040
"@pandacss/config": "workspace:*",
4141
"@pandacss/core": "workspace:*",
42-
"@pandacss/extractor": "workspace:*",
4342
"@pandacss/generator": "workspace:*",
43+
"@pandacss/reporter": "workspace:*",
4444
"@pandacss/logger": "workspace:*",
4545
"@pandacss/parser": "workspace:*",
4646
"@pandacss/shared": "workspace:*",
@@ -49,8 +49,6 @@
4949
"browserslist": "4.23.3",
5050
"chokidar": "3.6.0",
5151
"fast-glob": "3.3.2",
52-
"file-size": "1.0.0",
53-
"filesize": "10.1.6",
5452
"fs-extra": "11.2.0",
5553
"glob-parent": "6.0.2",
5654
"is-glob": "4.0.3",

Diff for: packages/node/src/analyze-tokens.ts

-146
This file was deleted.

0 commit comments

Comments
 (0)