Skip to content

Commit

Permalink
refactor: token and recipe analysis output
Browse files Browse the repository at this point in the history
  • Loading branch information
segunadebayo committed Jan 1, 2025
1 parent 93ee02a commit 9c1327e
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 116 deletions.
38 changes: 38 additions & 0 deletions .changeset/clean-geese-grin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
'@pandacss/reporter': patch
'@pandacss/dev': patch
---

Redesigned the recipe report to be more readable and easier to understand. We simplified the `JSX` and `Function`
columns to be more concise.

**BEFORE**

```sh
╔════════════════════════╤══════════════════════╤═════════╤═══════╤════════════╤═══════════════════╤══════════╗
║ Recipe │ Variant Combinations │ Usage % │ JSX % │ Function % │ Most Used │ Found in
╟────────────────────────┼──────────────────────┼─────────┼───────┼────────────┼───────────────────┼──────────╢
║ someRecipe (1 variant) │ 1 / 1 │ 100% │ 100% │ 0% │ size.small │ 1 file ║
╟────────────────────────┼──────────────────────┼─────────┼───────┼────────────┼───────────────────┼──────────╢
║ button (4 variants) │ 7 / 9 │ 77.78% │ 63% │ 38% │ size.md, size.sm, │ 2 files ║
║ │ │ │ │ │ state.focused, │ ║
║ │ │ │ │ │ variant.danger, │ ║
║ │ │ │ │ │ variant.primary │ ║
╚════════════════════════╧══════════════════════╧═════════╧═══════╧════════════╧═══════════════════╧══════════╝
```

**AFTER**

```sh
╔════════════════════════╤════════════════╤═══════════════════╤═══════════════════╤══════════╤═══════════╗
║ Recipe │ Variant values │ Usage % │ Most used │ Found in │ Used as ║
╟────────────────────────┼────────────────┼───────────────────┼───────────────────┼──────────┼───────────╢
║ someRecipe (1 variant) │ 1 value │ 100% (1 value) │ size.small │ 1 file │ jsx: 100% ║
║ │ │ │ │ │ fn: 0% ║
╟────────────────────────┼────────────────┼───────────────────┼───────────────────┼──────────┼───────────╢
║ button (4 variants) │ 9 values │ 77.78% (7 values) │ size.md, size.sm, │ 2 files │ jsx: 63% ║
║ │ │ │ state.focused, │ │ fn: 38% ║
║ │ │ │ variant.danger, │ │ ║
║ │ │ │ variant.primary │ │ ║
╚════════════════════════╧════════════════╧═══════════════════╧═══════════════════╧══════════╧═══════════╝
```
20 changes: 14 additions & 6 deletions packages/cli/src/cli-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,14 +389,22 @@ export async function main() {
return
}

if (tokenScope && !ctx.tokens.isEmpty) {
const tokenAnalysis = result.getTokenReport()
logger.info('analyze:tokens', `Token usage report 🎨 \n${tokenAnalysis.formatted}`)
if (tokenScope) {
if (!ctx.tokens.isEmpty) {
const tokenAnalysis = result.getTokenReport()
logger.info('analyze:tokens', `Token usage report 🎨 \n${tokenAnalysis.formatted}`)
} else {
logger.info('analyze:tokens', 'No tokens found')
}
}

if (recipeScope && !ctx.recipes.isEmpty()) {
const recipeAnalysis = result.getRecipeReport()
logger.info('analyze:recipes', `Recipe usage report 🎛️ \n${recipeAnalysis.formatted}`)
if (recipeScope) {
if (!ctx.recipes.isEmpty()) {
const recipeAnalysis = result.getRecipeReport()
logger.info('analyze:recipes', `Config recipes usage report 🎛️ \n${recipeAnalysis.formatted}`)
} else {
logger.info('analyze:recipes', 'No config recipes found')
}
}
})

Expand Down
146 changes: 41 additions & 105 deletions packages/reporter/src/report-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,146 +12,82 @@ const plural = (count: number, singular: string) => {
return `${count} ${plural}`
}

const createWrapFn = (enabled: boolean) => (str: string) => (enabled ? Wordwrap.wrap(str, { width: 20 }) : str)

export function formatTokenReport(result: TokenReportEntry[], format: ReportFormat): string {
const headers = ['Token', 'Usage %', 'Most used', 'Hardcoded', 'Found in']

function getFormatted(entry: TokenReportEntry, wrap: boolean) {
const wrapFn = createWrapFn(wrap)
return [
`${entry.category} (${plural(entry.count, 'token')})`,
`${entry.percentUsed}% (${plural(entry.usedCount, 'token')})`,
wrapFn(entry.mostUsedNames.join(', ')),
entry.hardcoded.toString(),
`${plural(entry.usedInXFiles, 'file')}`,
]
}

switch (format) {
case 'json':
return JSON.stringify(result, null, 2)

case 'markdown': {
return markdownTable([
['Token', 'Usage %', 'Most Used', 'Unused %', 'Hardcoded', 'Found in'],
...result.map((entry) => [
`${entry.category} (${entry.count} tokens)`,
`${entry.percentUsed}% (${entry.usedCount} tokens)`,
entry.mostUsedNames.join(', '),
`${(100 - entry.percentUsed).toFixed(2)}%`,
entry.hardcoded.toString(),
plural(entry.usedInXFiles, 'file'),
]),
])
return markdownTable([headers, ...result.map((entry) => getFormatted(entry, true))])
}

case 'csv': {
return [
'Token,Usage %,Most Used,Unused %,Hardcoded,Found in',
...result.map((entry) =>
[
`${entry.category} (${entry.count} tokens)`,
`${entry.percentUsed}% (${entry.usedCount} tokens)`,
entry.mostUsedNames.join(', '),
`${(100 - entry.percentUsed).toFixed(2)}%`,
entry.hardcoded.toString(),
plural(entry.usedInXFiles, 'file'),
].join(','),
),
].join('\n')
return [headers.join(','), ...result.map((entry) => getFormatted(entry, false).join(','))].join('\n')
}

case 'table': {
return table([
['Token', 'Usage %', 'Most Used', 'Unused %', 'Hardcoded', 'Found in'],
...result.map((entry) => [
`${entry.category} (${entry.count} tokens)`,
`${entry.percentUsed}% (${entry.usedCount} tokens)`,
Wordwrap.wrap(entry.mostUsedNames.join(', '), { width: 20 }),
`${(100 - entry.percentUsed).toFixed(2)}%`,
entry.hardcoded,
plural(entry.usedInXFiles, 'file'),
]),
])
return table([headers, ...result.map((entry) => getFormatted(entry, true))])
}

case 'text':
default:
return result
.map((entry) =>
[
`Token: ${entry.category} (${entry.count} tokens)`,
`Usage: ${entry.percentUsed}% (${entry.usedCount} tokens)`,
`Most Used: ${entry.mostUsedNames}`,
`Unused: ${(100 - entry.percentUsed).toFixed(2)}%`,
`Hardcoded: ${entry.hardcoded}`,
`Found in: ${plural(entry.usedInXFiles, 'file')}`,
'',
].join('\n'),
)
.join('\n')
default: {
const formatted = result.map((entry) => getFormatted(entry, false))
return headers.map((header, index) => `${header}: ${formatted[index]}`).join('\n')
}
}
}

export function formatRecipeReport(result: RecipeReportEntry[], format: ReportFormat): string {
function getFormatted(entry: RecipeReportEntry, wrap: boolean) {
const wrapFn = createWrapFn(wrap)
return [
`${entry.recipeName} (${plural(entry.variantCount, 'variant')})`,
`${plural(entry.possibleCombinations.length, 'value')}`,
`${entry.percentUsed}% (${plural(entry.usedCombinations, 'value')})`,
wrapFn(entry.mostUsedCombinations.join(', ')),
`${plural(entry.usedInXFiles, 'file')}`,
`jsx: ${entry.jsxPercentUsed}%\nfn: ${entry.fnPercentUsed}%`,
]
}

const headers = ['Recipe', 'Variant values', 'Usage %', 'Most used', 'Found in', 'Used as']

switch (format) {
case 'json': {
return JSON.stringify(result, null, 2)
}

case 'markdown': {
return table([
['Recipe', 'Variant Combinations', 'Usage %', 'JSX %', 'Function %', 'Unused', 'Most Used', 'Found in'],
...result.map((entry) => [
`${entry.recipeName} (${entry.variantCount} variants)`,
`${entry.usedCombinations} / ${entry.possibleCombinations.length}`,
`${entry.percentUsed}%`,
`${entry.jsxPercentUsed}%`,
`${entry.fnPercentUsed}%`,
entry.unusedCombinations,
entry.mostUsedCombinations,
plural(entry.usedInXFiles, 'file'),
]),
])
return table([headers, ...result.map((entry) => getFormatted(entry, true))])
}

case 'csv': {
return [
'Recipe,Variant Combinations,Usage %,JSX %,Function %,Unused,Most Used,Found in',
...result.map((entry) =>
[
`"${entry.recipeName} (${entry.variantCount} variants)"`,
`${entry.usedCombinations} / ${entry.possibleCombinations.length}`,
`${entry.percentUsed}%`,
`${entry.jsxPercentUsed}%`,
`${entry.fnPercentUsed}%`,
`"${entry.unusedCombinations}"`,
`"${entry.mostUsedCombinations}"`,
`"${plural(entry.usedInXFiles, 'file')}"`,
].join(','),
),
].join('\n')
return [headers.join(','), ...result.map((entry) => getFormatted(entry, false).join(','))].join('\n')
}

case 'table': {
return table([
['Recipe', 'Variant Combinations', 'Usage %', 'JSX %', 'Function %', 'Unused', 'Most Used', 'Found in'],
...result.map((entry) => [
`${entry.recipeName} (${entry.variantCount} variants)`,
`${entry.usedCombinations} / ${entry.possibleCombinations.length}`,
`${entry.percentUsed}%`,
`${entry.jsxPercentUsed}%`,
`${entry.fnPercentUsed}%`,
entry.unusedCombinations,
Wordwrap.wrap(entry.mostUsedCombinations.join(', '), { width: 20 }),
plural(entry.usedInXFiles, 'file'),
]),
])
return table([headers, ...result.map((entry) => getFormatted(entry, true))])
}

case 'text':
default: {
return result
.map((entry) =>
[
`Recipe: ${entry.recipeName} (${entry.variantCount} variants)`,
`Variant Combinations: ${entry.usedCombinations} / ${entry.possibleCombinations.length}`,
`Usage: ${entry.percentUsed}%`,
`JSX Usage: ${entry.jsxPercentUsed}%`,
`Function Usage: ${entry.fnPercentUsed}%`,
`Unused: ${entry.unusedCombinations}`,
`Most Used: ${entry.mostUsedCombinations}`,
`Found in: ${plural(entry.usedInXFiles, 'file')}`,
'',
].join('\n'),
)
.join('\n')
const formatted = result.map((entry) => getFormatted(entry, false))
return headers.map((header, index) => `${header}: ${formatted[index]}`).join('\n')
}
}
}
12 changes: 7 additions & 5 deletions website/pages/docs/references/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -427,16 +427,18 @@ Base path of project

## `panda analyze`

Analyze design token usage in glob.
Analyze design token and recipe usage.

By default it will analyze the entire project depending on your include and exclude options from your config file.
By default, it will analyze your project based on the `include` and `exclude` config options.

```bash
pnpm panda analyze
# You can also analyze a specific file or folder
# using the optional glob argument

# analyze a specific file
pnpm panda analyze src/components/Button.tsx
pnpm panda analyze "./src/components/**"

# analyze a specific glob
pnpm panda analyze "src/components/**"
```

### Flags
Expand Down

0 comments on commit 9c1327e

Please sign in to comment.