From 19c3a2c647b94a01da9453b47ef50856ac02a2d5 Mon Sep 17 00:00:00 2001 From: Alexandre Stahmer <47224540+astahmer@users.noreply.github.com> Date: Wed, 19 Jun 2024 23:56:44 +0200 Subject: [PATCH] refactor(cli): panda analyze minor changes (#2679) * refactor(cli): rename `panda analyze` --json -> --outfile * feat(cli): rm box in favor of range for each report item + add lightningCss timings/css file size * chore: rename/restructure a bunch of things * chore: add componentName * chore: rename "name" to "componentName" * chore: lint & typecheck --- .changeset/famous-rockets-give.md | 8 + packages/cli/src/cli-main.ts | 10 +- packages/cli/src/types.ts | 2 +- packages/extractor/src/box.ts | 1 + packages/node/src/analyze-tokens.ts | 113 ++++-- packages/node/src/classify.ts | 351 +++++++++++------- packages/parser/__tests__/ast-dynamic.test.ts | 1 + packages/types/src/analyze-report.ts | 220 ++++++----- 8 files changed, 410 insertions(+), 296 deletions(-) create mode 100644 .changeset/famous-rockets-give.md diff --git a/.changeset/famous-rockets-give.md b/.changeset/famous-rockets-give.md new file mode 100644 index 000000000..f7c5f7961 --- /dev/null +++ b/.changeset/famous-rockets-give.md @@ -0,0 +1,8 @@ +--- +'@pandacss/extractor': patch +'@pandacss/types': patch +'@pandacss/node': patch +'@pandacss/dev': patch +--- + +Minor changes in the `panda analyze --output coverage.json` file diff --git a/packages/cli/src/cli-main.ts b/packages/cli/src/cli-main.ts index f1aaf2fbd..d49778f73 100644 --- a/packages/cli/src/cli-main.ts +++ b/packages/cli/src/cli-main.ts @@ -360,7 +360,7 @@ export async function main() { cli .command('analyze [glob]', 'Analyze design token usage in glob') - .option('--json [filepath]', 'Output analyze report in JSON') + .option('--outfile [filepath]', 'Output analyze report in JSON') .option('--silent', "Don't print any logs") .option('-c, --config ', 'Path to panda config file') .option('--cwd ', 'Current working directory', { default: cwd }) @@ -385,13 +385,13 @@ export async function main() { }, }) - if (flags?.json && typeof flags.json === 'string') { - await writeAnalyzeJSON(flags.json, result, ctx) - logger.info('cli', `JSON report saved to ${flags.json}`) + if (flags?.outfile && typeof flags.outfile === 'string') { + await writeAnalyzeJSON(flags.outfile, result, ctx) + logger.info('cli', `JSON report saved to ${resolve(flags.outfile)}`) return } - logger.info('cli', `Found ${result.details.byId.size} token used in ${result.details.byFilePathMaps.size} files`) + logger.info('cli', `Found ${result.propByIndex.size} token used in ${result.derived.byFilePathMaps.size} files`) }) cli diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts index 812e30772..ac132bc7c 100644 --- a/packages/cli/src/types.ts +++ b/packages/cli/src/types.ts @@ -39,7 +39,7 @@ export interface StudioCommandFlags extends Pick { export interface AnalyzeCommandFlags { silent?: boolean - json?: string + outfile?: string cwd?: string config?: string } diff --git a/packages/extractor/src/box.ts b/packages/extractor/src/box.ts index 8943ee338..7fbf549e2 100644 --- a/packages/extractor/src/box.ts +++ b/packages/extractor/src/box.ts @@ -46,6 +46,7 @@ export const box = { value: undefined, getNode: () => node.getNode(), getStack: () => node.getStack(), + getRange: () => node.getRange(), } }, /** diff --git a/packages/node/src/analyze-tokens.ts b/packages/node/src/analyze-tokens.ts index b22c72a6a..af5964c66 100644 --- a/packages/node/src/analyze-tokens.ts +++ b/packages/node/src/analyze-tokens.ts @@ -1,8 +1,10 @@ import { logger } from '@pandacss/logger' -import type { ParserResultInterface } from '@pandacss/types' +import type { ParserResultInterface, ReportSnapshot } from '@pandacss/types' import { filesize } from 'filesize' import { writeFile } from 'fs/promises' +import path from 'node:path' import zlib from 'zlib' +import { version } from '../package.json' import { classifyTokens } from './classify' import type { PandaContext } from './create-context' @@ -12,21 +14,26 @@ interface Options { onResult?: (file: string, result: ParserResultInterface) => void } -export function analyzeTokens(ctx: PandaContext, options: Options = {}) { +export function analyzeTokens(ctx: PandaContext, options: Options = {}): ReportSnapshot { const filesMap = new Map() const timesMap = new Map() const files = ctx.getFiles() + const sheet = ctx.createSheet() + ctx.appendLayerParams(sheet) + ctx.appendBaselineCss(sheet) + files.forEach((file) => { const start = performance.now() const result = ctx.project.parseSourceFile(file) const extractMs = performance.now() - start - timesMap.set(file, extractMs) + const relativePath = path.relative(ctx.config.cwd, file) + timesMap.set(relativePath, extractMs) logger.debug('analyze', `Parsed ${file} in ${extractMs}ms`) if (result) { - filesMap.set(file, result) + filesMap.set(relativePath, result) options.onResult?.(file, result) } }) @@ -34,28 +41,59 @@ export function analyzeTokens(ctx: PandaContext, options: Options = {}) { const totalMs = Array.from(timesMap.values()).reduce((a, b) => a + b, 0) logger.debug('analyze', `Analyzed ${files.length} files in ${totalMs.toFixed(2)}ms`) - const minify = ctx.config.minify + ctx.appendParserCss(sheet) - ctx.config.optimize = true + const cssStart = performance.now() ctx.config.minify = false - - // TODO - const css = '' - const minifiedCss = '' - - // restore minify config - ctx.config.minify = minify + const css = ctx.getCss(sheet) + const cssMs = performance.now() - cssStart + + const cssMinifyStart = performance.now() + ctx.config.minify = true + const minifiedCss = ctx.getCss(sheet) + const cssMinifyMs = performance.now() - cssMinifyStart + + let lightningCss = '' + let lightningCssMs: number | undefined + let lightningCssMinifiedCss = '' + let lightningCssMinifiedMs: number | undefined + + const isUsingLightningCss = ctx.config.lightningcss + if (!isUsingLightningCss) { + sheet['context'].lightningcss = true + + ctx.config.minify = false + const lightningcssStart = performance.now() + lightningCss = ctx.getCss(sheet) + lightningCssMs = performance.now() - lightningcssStart + + ctx.config.minify = true + const lightningcssMinifyStart = performance.now() + lightningCssMinifiedCss = ctx.getCss(sheet) + lightningCssMinifiedMs = performance.now() - lightningcssMinifyStart + } const start = performance.now() const analysis = classifyTokens(ctx, filesMap) const classifyMs = performance.now() - start - return Object.assign( + const details = Object.assign( { duration: { - extractTimeByFiles: Object.fromEntries(timesMap.entries()), - extractTotal: totalMs, classify: classifyMs, + // + cssMs, + cssMinifyMs, + // + ...(!isUsingLightningCss + ? { + lightningCssMs, + lightningCssMinifiedMs, + } + : {}), + // + extractTotal: totalMs, + extractTimeByFiles: Object.fromEntries(timesMap.entries()), }, fileSizes: { lineCount: css.split('\n').length, @@ -65,10 +103,27 @@ export function analyzeTokens(ctx: PandaContext, options: Options = {}) { normal: filesize(gzipSizeSync(css)), minified: filesize(gzipSizeSync(minifiedCss)), }, + lightningCss: !isUsingLightningCss + ? { + normal: filesize(Buffer.byteLength(lightningCss, 'utf-8')), + minified: filesize(Buffer.byteLength(lightningCssMinifiedCss, 'utf-8')), + } + : undefined, }, }, - analysis, - ) + analysis.details, + ) satisfies ReportSnapshot['details'] + + const { globalCss, ...config } = ctx.config + + return { + schemaVersion: version, + details, + propByIndex: analysis.propById, + componentByIndex: analysis.componentById, + derived: analysis.derived, + config, + } } const analyzeResultSerializer = (_key: string, value: any) => { @@ -83,27 +138,9 @@ const analyzeResultSerializer = (_key: string, value: any) => { return value } -export const writeAnalyzeJSON = (filePath: string, result: ReturnType, ctx: PandaContext) => { - // prevent writing twice the same BoxNode in the output (already serialized in the `byId` map) - result.details.byInstanceId.forEach((item) => { - item.box = item.box.toJSON() as any - }) - +export const writeAnalyzeJSON = (filePath: string, result: ReportSnapshot, ctx: PandaContext) => { const dirname = ctx.runtime.path.dirname(filePath) ctx.runtime.fs.ensureDirSync(dirname) - return writeFile( - filePath, - JSON.stringify( - Object.assign(result, { - cwd: ctx.config.cwd, - theme: ctx.config.theme, - utilities: ctx.config.utilities, - conditions: ctx.config.conditions, - shorthands: ctx.utility.shorthands, - }), - analyzeResultSerializer, - 2, - ), - ) + return writeFile(filePath, JSON.stringify(result, analyzeResultSerializer, 2)) } diff --git a/packages/node/src/classify.ts b/packages/node/src/classify.ts index 3a3ecb282..ee69e8426 100644 --- a/packages/node/src/classify.ts +++ b/packages/node/src/classify.ts @@ -1,59 +1,60 @@ -import type { ParserResultInterface, ReportInstanceItem, ReportItem } from '@pandacss/types' -import { box } from '@pandacss/extractor' +import type { + ParserResultInterface, + ComponentReportItem, + PropertyReportItem, + ReportDerivedMaps, + CssSemanticGroup, +} from '@pandacss/types' +import { BoxNodeMap, box } from '@pandacss/extractor' import type { PandaContext } from './create-context' import type { ResultItem } from '@pandacss/types' -type BoxNodeMap = ReportInstanceItem['box'] - const createReportMaps = () => { - const byInstanceOfKind = new Map<'function' | 'component', Set>() - const byPropertyName = new Map>() - const byCategory = new Map>() - const byConditionName = new Map>() - const byShorthand = new Map>() - const byTokenName = new Map>() - const byPropertyPath = new Map>() - const fromKind = new Map<'function' | 'component', Set>() - const byType = new Map>() - const byInstanceName = new Map>() - const colorsUsed = new Map>() + const byComponentOfKind = new Map<'function' | 'component', Set>() + const byPropertyName = new Map>() + const byTokenType = new Map>() + const byConditionName = new Map>() + const byShorthand = new Map>() + const byTokenName = new Map>() + const byPropertyPath = new Map>() + const fromKind = new Map<'function' | 'component', Set>() + const byType = new Map>() + const byComponentName = new Map>() + const colorsUsed = new Map>() return { - byInstanceOfKind, + byComponentOfKind, byPropertyName, - byCategory, + byTokenType, byConditionName, byShorthand, byTokenName, byPropertyPath, fromKind, byType, - byInstanceName, + byComponentName, colorsUsed, } } -const colorPropNames = new Set(['background', 'outline', 'border']) - -type ReportMaps = ReturnType - export const classifyTokens = (ctx: PandaContext, parserResultByFilepath: Map) => { - const byId = new Map() - const byInstanceId = new Map() + const byId = new Map() + const byComponentIndex = new Map() - const byFilepath = new Map>() - const byInstanceInFilepath = new Map>() + const byFilepath = new Map>() + const byComponentInFilepath = new Map>() const globalMaps = createReportMaps() - const byFilePathMaps = new Map() + const byFilePathMaps = new Map() const conditions = new Map(Object.entries(ctx.conditions.values)) + const { groupByProp } = getPropertyGroupMap(ctx) let id = 0, - instanceId = 0 + componentIndex = 0 - const isKnownUtility = (reportItem: ReportItem) => { - const { propName, type, value, from } = reportItem + const isKnownUtility = (reportItem: PropertyReportItem, componentReportItem: ComponentReportItem) => { + const { propName, value } = reportItem const utility = ctx.config.utilities?.[propName] if (utility) { @@ -64,8 +65,8 @@ export const classifyTokens = (ctx: PandaContext, parserResultByFilepath: Map { - const { from, type, kind } = reportInstanceItem + const processMap = (map: BoxNodeMap, current: string[], componentReportItem: ComponentReportItem) => { + const { reportItemType: type, kind } = componentReportItem + const name = componentReportItem.componentName map.value.forEach((attrNode, attrName) => { if (box.isLiteral(attrNode) || box.isEmptyInitializer(attrNode)) { const value = box.isLiteral(attrNode) ? (attrNode.value as string) : true - const reportItem = { - id: id++, - instanceId, - category: 'unknown', + const propReportItem = { + index: String(id++), + componentIndex: String(componentReportItem.componentIndex), + componentName: name, + tokenType: undefined, propName: attrName, - from, - type, + reportItemKind: 'utility', + reportItemType: type, kind, filepath, path: current.concat(attrName), value, - box: attrNode, - isKnown: false, - } as ReportItem // TODO satisfies - reportInstanceItem.contains.push(reportItem.id) + isKnownValue: false, + range: map.getRange(), + } as PropertyReportItem + componentReportItem.contains.push(propReportItem.index) if (conditions.has(attrName)) { - addTo(globalMaps.byConditionName, attrName, reportItem.id) - addTo(localMaps.byConditionName, attrName, reportItem.id) - reportItem.propName = current[0] ?? attrName - reportItem.isKnown = isKnownUtility(reportItem) - reportItem.conditionName = attrName + addTo(globalMaps.byConditionName, attrName, propReportItem.index) + addTo(localMaps.byConditionName, attrName, propReportItem.index) + propReportItem.propName = current[0] ?? attrName + propReportItem.isKnownValue = isKnownUtility(propReportItem, componentReportItem) + propReportItem.conditionName = attrName } else { if (current.length && conditions.has(current[0])) { - reportItem.conditionName = current[0] + propReportItem.conditionName = current[0] // TODO: when using nested conditions // should we add the reportItem.id for each of them or just the first one? // (currently just the first one) - addTo(globalMaps.byConditionName, current[0], reportItem.id) - addTo(localMaps.byConditionName, current[0], reportItem.id) + addTo(globalMaps.byConditionName, current[0], propReportItem.index) + addTo(localMaps.byConditionName, current[0], propReportItem.index) } const propName = ctx.utility.shorthands.get(attrName) ?? attrName - reportItem.propName = propName + propReportItem.propName = propName const utility = ctx.config.utilities?.[propName] - reportItem.isKnown = isKnownUtility(reportItem) + propReportItem.isKnownValue = isKnownUtility(propReportItem, componentReportItem) - const category = typeof utility?.values === 'string' ? utility?.values : 'unknown' - reportItem.category = category + const tokenType = typeof utility?.values === 'string' ? utility?.values : undefined + if (tokenType) { + propReportItem.reportItemKind = 'token' + propReportItem.tokenType = tokenType + } - addTo(globalMaps.byPropertyName, propName, reportItem.id) - addTo(localMaps.byPropertyName, propName, reportItem.id) + addTo(globalMaps.byPropertyName, propName, propReportItem.index) + addTo(localMaps.byPropertyName, propName, propReportItem.index) - addTo(globalMaps.byCategory, category, reportItem.id) - addTo(localMaps.byCategory, category, reportItem.id) + if (tokenType) { + addTo(globalMaps.byTokenType, tokenType, propReportItem.index) + addTo(localMaps.byTokenType, tokenType, propReportItem.index) + } - if (propName.toLowerCase().includes('color') || colorPropNames.has(propName)) { - addTo(globalMaps.colorsUsed, value as string, reportItem.id) - addTo(localMaps.colorsUsed, value as string, reportItem.id) + if ( + propName.toLowerCase().includes('color') || + groupByProp.get(propName) === 'Color' || + tokenType === 'colors' + ) { + addTo(globalMaps.colorsUsed, value as string, propReportItem.index) + addTo(localMaps.colorsUsed, value as string, propReportItem.index) } if (ctx.utility.shorthands.has(attrName)) { - addTo(globalMaps.byShorthand, attrName, reportItem.id) - addTo(localMaps.byShorthand, attrName, reportItem.id) + addTo(globalMaps.byShorthand, attrName, propReportItem.index) + addTo(localMaps.byShorthand, attrName, propReportItem.index) } } if (current.length) { - addTo(globalMaps.byPropertyPath, reportItem.path.join('.'), reportItem.id) - addTo(localMaps.byPropertyPath, reportItem.path.join('.'), reportItem.id) + addTo(globalMaps.byPropertyPath, propReportItem.path.join('.'), propReportItem.index) + addTo(localMaps.byPropertyPath, propReportItem.path.join('.'), propReportItem.index) } // - addTo(globalMaps.byTokenName, String(value), reportItem.id) - addTo(localMaps.byTokenName, String(value), reportItem.id) + addTo(globalMaps.byTokenName, String(value), propReportItem.index) + addTo(localMaps.byTokenName, String(value), propReportItem.index) // - addTo(globalMaps.byType, type, reportItem.id) - addTo(localMaps.byType, type, reportItem.id) + addTo(globalMaps.byType, type, propReportItem.index) + addTo(localMaps.byType, type, propReportItem.index) // - addTo(globalMaps.byInstanceName, from, reportItem.id) - addTo(localMaps.byInstanceName, from, reportItem.id) + addTo(globalMaps.byComponentName, name, propReportItem.index) + addTo(localMaps.byComponentName, name, propReportItem.index) // - addTo(globalMaps.fromKind, kind, reportItem.id) - addTo(localMaps.fromKind, kind, reportItem.id) + addTo(globalMaps.fromKind, kind, propReportItem.index) + addTo(localMaps.fromKind, kind, propReportItem.index) // - addTo(byFilepath, filepath, reportItem.id) - byId.set(reportItem.id, reportItem) + addTo(byFilepath, filepath, propReportItem.index) + byId.set(propReportItem.index, propReportItem) return } if (box.isMap(attrNode) && attrNode.value.size) { - return processMap(attrNode, current.concat(attrName), reportInstanceItem) + return processMap(attrNode, current.concat(attrName), componentReportItem) } }) } - const processResultItem = (item: ResultItem, kind: ReportItem['kind']) => { + const processResultItem = (item: ResultItem, kind: ComponentReportItem['kind']) => { if (!item.box || box.isUnresolvable(item.box)) { - // console.log('no box', item) + // TODO store that in the report (unresolved values) + // console.log('no box', filepath, item.name, item.type, item.box?.getRange()) return } if (!item.data) { - // console.log('no data', item) + console.log('no data', item) return } - const from = item.name! - const type = item.type as ReportItem['type'] - const reportInstanceItem = { - instanceId: instanceId++, - from, - type, + const componentReportItem = { + componentIndex: String(componentIndex++), + componentName: item.name!, + reportItemType: item.type!, kind, filepath, value: item.data, - box: item.box, + range: item.box.getRange(), contains: [], - } as ReportInstanceItem // TODO satisfies + } satisfies ComponentReportItem if (box.isArray(item.box)) { - addTo(byInstanceInFilepath, filepath, reportInstanceItem.instanceId) + addTo(byComponentInFilepath, filepath, componentReportItem.componentIndex) - return reportInstanceItem + return componentReportItem } if (box.isMap(item.box) && item.box.value.size) { - addTo(byInstanceInFilepath, filepath, reportInstanceItem.instanceId) + addTo(byComponentInFilepath, filepath, componentReportItem.componentIndex) - processMap(item.box, [], reportInstanceItem) - return reportInstanceItem + processMap(item.box, [], componentReportItem) + return componentReportItem } } const processComponentResultItem = (item: ResultItem) => { - const reportInstanceItem = processResultItem(item, 'component') - if (!reportInstanceItem) return + const componentReportItem = processResultItem(item, 'component') + if (!componentReportItem) return - addTo(globalMaps.byInstanceOfKind, 'component', reportInstanceItem.instanceId) - addTo(localMaps.byInstanceOfKind, 'component', reportInstanceItem.instanceId) + addTo(globalMaps.byComponentOfKind, 'component', componentReportItem.componentIndex) + addTo(localMaps.byComponentOfKind, 'component', componentReportItem.componentIndex) - byInstanceId.set(reportInstanceItem.instanceId, reportInstanceItem) + byComponentIndex.set(componentReportItem.componentIndex, componentReportItem) } const processFunctionResultItem = (item: ResultItem) => { - const reportInstanceItem = processResultItem(item, 'function') - if (!reportInstanceItem) return + const componentReportItem = processResultItem(item, 'function') + if (!componentReportItem) return - addTo(globalMaps.byInstanceOfKind, 'function', reportInstanceItem.instanceId) - addTo(localMaps.byInstanceOfKind, 'function', reportInstanceItem.instanceId) + addTo(globalMaps.byComponentOfKind, 'function', componentReportItem.componentIndex) + addTo(localMaps.byComponentOfKind, 'function', componentReportItem.componentIndex) - byInstanceId.set(reportInstanceItem.instanceId, reportInstanceItem) + byComponentIndex.set(componentReportItem.componentIndex, componentReportItem) } parserResult.jsx.forEach(processComponentResultItem) @@ -276,63 +287,55 @@ export const classifyTokens = (ctx: PandaContext, parserResultByFilepath: Map [filepath, list.size] as const) .sort((a, b) => b[1] - a[1]) .slice(0, pickCount), ) - const filesWithMostPropValueCombinations = Object.fromEntries( - Array.from(byFilepath.entries()) - .map(([token, list]) => [token, list.size] as const) - .sort((a, b) => b[1] - a[1]) - .slice(0, pickCount), - ) - return { - counts: { - filesWithTokens: byFilepath.size, - propNameUsed: globalMaps.byPropertyName.size, - tokenUsed: globalMaps.byTokenName.size, - shorthandUsed: globalMaps.byShorthand.size, - propertyPathUsed: globalMaps.byPropertyPath.size, - typeUsed: globalMaps.byType.size, - instanceNameUsed: globalMaps.byInstanceName.size, - kindUsed: globalMaps.fromKind.size, - instanceOfKindUsed: globalMaps.byInstanceOfKind.size, - colorsUsed: globalMaps.colorsUsed.size, - }, - stats: { - // - filesWithMostInstance, - filesWithMostPropValueCombinations, - // - mostUseds: getXMostUseds(globalMaps, 10), - }, + propById: byId, + componentById: byComponentIndex, details: { - byId, - byInstanceId, + counts: { + filesWithTokens: byFilepath.size, + propNameUsed: globalMaps.byPropertyName.size, + tokenUsed: globalMaps.byTokenName.size, + shorthandUsed: globalMaps.byShorthand.size, + propertyPathUsed: globalMaps.byPropertyPath.size, + typeUsed: globalMaps.byType.size, + componentNameUsed: globalMaps.byComponentName.size, + kindUsed: globalMaps.fromKind.size, + componentOfKindUsed: globalMaps.byComponentOfKind.size, + colorsUsed: globalMaps.colorsUsed.size, + }, + stats: { + filesWithMostComponent, + mostUseds: getXMostUseds(globalMaps, 10), + }, + }, + derived: { byFilepath, - byInstanceInFilepath, + byComponentInFilepath, globalMaps, byFilePathMaps, }, } } -const getXMostUseds = (globalMaps: ReportMaps, pickCount: number) => { +const getXMostUseds = (globalMaps: ReportDerivedMaps, pickCount: number) => { return { propNames: getMostUsedInMap(globalMaps.byPropertyName, pickCount), tokens: getMostUsedInMap(globalMaps.byTokenName, pickCount), shorthands: getMostUsedInMap(globalMaps.byShorthand, pickCount), conditions: getMostUsedInMap(globalMaps.byConditionName, pickCount), propertyPaths: getMostUsedInMap(globalMaps.byPropertyPath, pickCount), - categories: getMostUsedInMap(globalMaps.byCategory, pickCount), + categories: getMostUsedInMap(globalMaps.byTokenType, pickCount), types: getMostUsedInMap(globalMaps.byType, pickCount), - instanceNames: getMostUsedInMap(globalMaps.byInstanceName, pickCount), + componentNames: getMostUsedInMap(globalMaps.byComponentName, pickCount), fromKinds: getMostUsedInMap(globalMaps.fromKind, pickCount), - instanceOfKinds: getMostUsedInMap(globalMaps.byInstanceOfKind, pickCount), + componentOfKinds: getMostUsedInMap(globalMaps.byComponentOfKind, pickCount), colors: getMostUsedInMap(globalMaps.colorsUsed, pickCount), } } @@ -344,3 +347,73 @@ const getMostUsedInMap = (map: Map>, pickCount: number) => { .slice(0, pickCount) .map(([key, count]) => ({ key, count })) } + +const defaultGroupNames: CssSemanticGroup[] = [ + 'System', + 'Container', + 'Display', + 'Visibility', + 'Position', + 'Transform', + 'Flex Layout', + 'Grid Layout', + 'Layout', + 'Border', + 'Border Radius', + 'Width', + 'Height', + 'Margin', + 'Padding', + 'Color', + 'Typography', + 'Background', + 'Shadow', + 'Table', + 'List', + 'Scroll', + 'Interactivity', + 'Transition', + 'Effect', + 'Other', +] + +const getPropertyGroupMap = (context: PandaContext) => { + const groups = new Map>(defaultGroupNames.map((name) => [name, new Set()])) + const groupByProp = new Map() + const systemGroup = groups.get('System')! + systemGroup.add('base') + systemGroup.add('colorPalette') + + const otherStyleProps = groups.get('Other')! + + Object.entries(context.utility.config).map(([key, value]) => { + const group = value?.group + if (!group) { + otherStyleProps.add(key) + return + } + + if (!groups.has(group)) { + groups.set(group, new Set()) + } + + const set = groups.get(group)! + + if (value.shorthand) { + if (Array.isArray(value.shorthand)) { + value.shorthand.forEach((shorthand) => { + set.add(shorthand) + groupByProp.set(shorthand, group) + }) + } else { + set.add(value.shorthand) + groupByProp.set(value.shorthand, group) + } + } + + set.add(key) + groupByProp.set(key, group) + }) + + return { groups, groupByProp } +} diff --git a/packages/parser/__tests__/ast-dynamic.test.ts b/packages/parser/__tests__/ast-dynamic.test.ts index eea311af8..f02a1c56f 100644 --- a/packages/parser/__tests__/ast-dynamic.test.ts +++ b/packages/parser/__tests__/ast-dynamic.test.ts @@ -95,6 +95,7 @@ describe('[dynamic] ast parser', () => { { "box": { "getNode": [Function], + "getRange": [Function], "getStack": [Function], "value": undefined, }, diff --git a/packages/types/src/analyze-report.ts b/packages/types/src/analyze-report.ts index edec9f996..f11e6ba7e 100644 --- a/packages/types/src/analyze-report.ts +++ b/packages/types/src/analyze-report.ts @@ -1,45 +1,71 @@ -import type { BoxNodeEmptyInitializer, BoxNodeLiteral, BoxNodeMap } from '@pandacss/extractor' import type { Config } from './config' -export type ReportItemType = 'object' | 'cva' | 'pattern' | 'recipe' | 'jsx' | 'jsx-factory' -export interface ReportItem { - id: number - from: string - type: ReportItemType - filepath: string - kind: 'function' | 'component' +export type ReportItemType = + | 'css' + | 'cva' + | 'sva' + | 'pattern' + | 'recipe' + | 'jsx-factory' + | 'jsx-pattern' + | 'jsx-recipe' + | 'jsx' + +type ComponentKind = 'component' | 'function' + +type Range = { + startPosition: number + startLineNumber: number + startColumn: number + endPosition: number + endLineNumber: number + endColumn: number +} + +export interface PropertyReportItem { + index: string + componentIndex: ComponentReportItem['componentIndex'] + componentName: ComponentReportItem['componentName'] + reportItemKind: 'token' | 'utility' + path: string[] - propName: string conditionName?: string | undefined - value: string | number | true - category: string - isKnown: boolean - box: BoxNodeLiteral | BoxNodeEmptyInitializer + propName: string + value: string | number | boolean + + tokenType?: string + isKnownValue: boolean + + range: Range + filepath: string } /** - * An instance is either a component usage or a function usage - * @example an instance name could be 'Button', 'css', 'panda.div', 'vstack', ... + * An component is either a component usage or a function usage + * @example an component name could be 'Button', 'css', 'panda.div', 'vstack', ... */ -export interface ReportInstanceItem extends Pick { - instanceId: number - contains: Array +export interface ComponentReportItem extends Pick { + componentIndex: string + componentName: string + reportItemType: ReportItemType + kind: ComponentKind + contains: Array value: Record - box: BoxNodeMap + range: Range } -export interface ReportMaps { - byInstanceOfKind: Map<'function' | 'component', Set> - byPropertyName: Map> - byCategory: Map> - byConditionName: Map> - byShorthand: Map> - byTokenName: Map> - byPropertyPath: Map> - fromKind: Map<'function' | 'component', Set> - byType: Map> - byInstanceName: Map> - colorsUsed: Map> +export interface ReportDerivedMaps { + byComponentOfKind: Map> + byPropertyName: Map> + byTokenType: Map> + byConditionName: Map> + byShorthand: Map> + byTokenName: Map> + byPropertyPath: Map> + fromKind: Map> + byType: Map> + byComponentName: Map> + colorsUsed: Map> } export interface ReportCounts { @@ -49,9 +75,9 @@ export interface ReportCounts { shorthandUsed: number propertyPathUsed: number typeUsed: number - instanceNameUsed: number + componentNameUsed: number kindUsed: number - instanceOfKindUsed: number + componentOfKindUsed: number colorsUsed: number } @@ -60,8 +86,7 @@ export interface MostUsedItem { count: number } export interface ReportStats { - filesWithMostInstance: Record - filesWithMostPropValueCombinations: Record + filesWithMostComponent: Record mostUseds: { propNames: Array tokens: Array @@ -70,9 +95,9 @@ export interface ReportStats { conditions: Array propertyPaths: Array types: Array - instanceNames: Array + componentNames: Array fromKinds: Array - instanceOfKinds: Array + componentOfKinds: Array colors: Array } } @@ -80,13 +105,15 @@ export interface ReportStats { export interface ReportDetails { counts: ReportCounts stats: ReportStats - details: { - byId: Map - byInstanceId: Map - byFilepath: Map> - byInstanceInFilepath: Map> - globalMaps: ReportMaps - byFilePathMaps: Map + fileSizes: FileSizes + duration: { + classify: number + cssMs: number + cssMinifyMs: number + extractTotal: number + extractTimeByFiles: Record + lightningCssMs?: number + lightningCssMinifiedMs?: number } } @@ -97,83 +124,50 @@ interface FileSizes { normal: string minified: string } + lightningCss?: { + normal: string + minified: string + } } -export interface AnalysisReport extends ReportDetails { - fileSizes: FileSizes -} +export interface ReportSnapshot { + schemaVersion: string + details: ReportDetails + config: Omit -interface ReportMapsJSON { - byInstanceOfKind: Record<'function' | 'component', Array> - byPropertyName: Record> - byCategory: Record> - byConditionName: Record> - byShorthand: Record> - byTokenName: Record> - byPropertyPath: Record> - fromKind: Record<'function' | 'component', Array> - byType: Record> - byInstanceName: Record> - colorsUsed: Record> -} + propByIndex: Map + componentByIndex: Map -export interface ReportItemJSON { - id: number - from: string - type: ReportItemType - filepath: string - kind: 'function' | 'component' - path: string[] - propName: string - conditionName?: string | undefined - value: string | number | true - category: string - isKnown: boolean - box: { - type: 'literal' | 'empty-initializer' - value: string | number | boolean | undefined | null - node: string - stack: string[] - line: number - column: number + derived: { + byFilepath: Map> + byComponentInFilepath: Map> + globalMaps: ReportDerivedMaps + byFilePathMaps: Map } } -export interface ReportInstanceItemJSON extends Pick { - instanceId: number - contains: Array - value: Record - box: { type: 'map'; value: Record; node: string; stack: string[]; line: number; column: number } +interface ReportDerivedMapsJSON { + byComponentOfKind: Record> + byPropertyName: Record> + byTokenType: Record> + byConditionName: Record> + byShorthand: Record> + byTokenName: Record> + byPropertyPath: Record> + fromKind: Record> + byType: Record> + byComponentName: Record> + colorsUsed: Record> } -export interface AnalysisReportJSON { - counts: ReportCounts - stats: ReportStats - details: { - byId: Record - byInstanceId: Record - byFilepath: Record> - byInstanceInFilepath: Record> - globalMaps: ReportMapsJSON - byFilePathMaps: Record - } - fileSizes: FileSizes - cwd: Config['cwd'] - theme: Config['theme'] - utilities: Config['utilities'] - conditions: Config['conditions'] - shorthands: Record - // Generator["parserOptions""] - parserOptions: { - importMap: { - css: string - recipe: string - pattern: string - jsx: string - } - jsx: { - factory: string - nodes: Array<{ type: 'string'; name: string; props: string[]; baseName: string }> - } +export interface ReportSnapshotJSON extends Omit { + propByIndex: Record + componentByIndex: Record + + derived: { + byFilepath: Record> + byComponentInFilepath: Record> + globalMaps: ReportDerivedMapsJSON + byFilePathMaps: Record } }