Skip to content

Commit 592c963

Browse files
authored
perf(worker): avoid accessing panda context to scan all files (#291)
2 parents 76c59c4 + 4ad93d1 commit 592c963

File tree

8 files changed

+111
-39
lines changed

8 files changed

+111
-39
lines changed

.changeset/ten-phones-explode.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@pandacss/eslint-plugin': minor
3+
---
4+
5+
Improve performance

fixture/src/create-context.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { PandaContext } from '@pandacss/node'
44
import { stringifyJson, parseJson } from '@pandacss/shared'
55
import type { Config, LoadConfigResult, UserConfig } from '@pandacss/types'
66
import { fixturePreset } from './config'
7+
export { default as v9Config } from '../../sandbox/v9/panda.config'
78
import v9Config from '../../sandbox/v9/panda.config'
89

910
const config: UserConfig = {
@@ -28,7 +29,7 @@ export const fixtureDefaults = {
2829

2930
export const createGeneratorContext = (userConfig?: Config) => {
3031
const resolvedConfig = (
31-
userConfig ? mergeConfigs([userConfig, fixtureDefaults.config]) : fixtureDefaults.config
32+
userConfig ? mergeConfigs([fixtureDefaults.config, userConfig]) : fixtureDefaults.config
3233
) as UserConfig
3334

3435
return new Generator({ ...fixtureDefaults, config: resolvedConfig })

fixture/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './config'
22
export * from './create-context'
3+
export { v9Config } from './create-context'
34
export * from './layers'
45
export * from './recipes'
56
export * from './semantic-tokens'

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@
2727
"dependencies": {
2828
"@changesets/changelog-github": "^0.5.0",
2929
"@changesets/cli": "^2.27.1",
30+
"@types/micromatch": "^4.0.10",
3031
"@typescript-eslint/utils": "^8.21.0",
3132
"esbuild": "0.25.0",
33+
"micromatch": "^4.0.8",
3234
"tsup": "^8.0.1",
3335
"typescript": "^5.3.3"
3436
},

plugin/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,14 @@
3939
"@pandacss/shared": "^0.53.2",
4040
"@typescript-eslint/utils": "^8.21.0",
4141
"hookable": "^5.5.3",
42+
"micromatch": "^4.0.8",
4243
"synckit": "^0.9.0"
4344
},
4445
"peerDependencies": {
4546
"eslint": "*"
4647
},
4748
"devDependencies": {
49+
"@types/micromatch": "^4.0.10",
4850
"typescript": "^5.7.2"
4951
}
5052
}

plugin/src/utils/helpers.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ const isValidStyledProp = <T extends Node>(node: T, context: RuleContext<any, an
9797
export const isPandaIsh = (name: string, context: RuleContext<any, any>) => {
9898
const imports = getImports(context)
9999
if (imports.length === 0) return false
100+
// Check if the name is the jsx factory
101+
const jsxFactory = syncAction('getJsxFactory', getSyncOpts(context))
102+
if (jsxFactory && name === jsxFactory) {
103+
// Check if the jsx factory is imported
104+
return imports.some((imp) => imp.name === name || imp.alias === name)
105+
}
100106
return syncAction('matchFile', getSyncOpts(context), name, imports)
101107
}
102108

@@ -159,10 +165,14 @@ export const isPandaProp = (node: TSESTree.JSXAttribute, context: RuleContext<an
159165
const prop = node.name.name
160166

161167
// Ensure component is a panda component
162-
if (!isPandaIsh(name, context) && !isLocalStyledFactory(jsxAncestor, context)) return
168+
const isPandaComponent = isPandaIsh(name, context) || isLocalStyledFactory(jsxAncestor, context)
169+
if (!isPandaComponent) return
163170

164171
// Ensure prop is a styled prop
165-
if (typeof prop !== 'string' || !isValidProperty(prop, context, name)) return
172+
// For jsx factory components (e.g., styled.div), pass undefined as the pattern name
173+
// so that only global property validation is performed
174+
const patternName = isJSXMemberExpression(jsxAncestor.name) ? undefined : name
175+
if (typeof prop !== 'string' || !isValidProperty(prop, context, patternName)) return
166176

167177
return true
168178
}

plugin/src/utils/worker.ts

Lines changed: 61 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,56 @@
1-
import { PandaContext, loadConfigAndCreateContext } from '@pandacss/node'
1+
import { Generator } from '@pandacss/generator'
22
import { runAsWorker } from 'synckit'
3-
import { createContext } from 'fixture'
3+
import { createGeneratorContext, v9Config } from 'fixture'
44
import { resolveTsPathPattern } from '@pandacss/config/ts-path'
5-
import { findConfig } from '@pandacss/config'
5+
import { findConfig, loadConfig } from '@pandacss/config'
66
import path from 'path'
7+
import micromatch from 'micromatch'
78
import type { ImportResult } from '.'
89

910
type Opts = {
1011
currentFile: string
1112
configPath?: string
1213
}
1314

14-
const contextCache: { [configPath: string]: Promise<PandaContext> } = {}
15+
const contextCache: { [configPath: string]: Promise<Generator> } = {}
1516

1617
async function _getContext(configPath: string | undefined) {
1718
if (!configPath) throw new Error('Invalid config path')
1819

1920
const cwd = path.dirname(configPath)
2021

21-
const ctx = await loadConfigAndCreateContext({ configPath, cwd })
22+
const conf = await loadConfig({ file: configPath, cwd })
23+
const ctx = new Generator(conf)
2224
return ctx
2325
}
2426

2527
export async function getContext(opts: Opts) {
2628
if (process.env.NODE_ENV === 'test') {
27-
const ctx = createContext() as unknown as PandaContext
28-
ctx.getFiles = () => ['App.tsx']
29+
const ctx = createGeneratorContext({
30+
...v9Config,
31+
include: ['**/*'],
32+
exclude: ['**/Invalid.tsx', '**/panda.config.ts'],
33+
importMap: './panda',
34+
jsxFactory: 'styled',
35+
})
2936
return ctx
3037
} else {
3138
const configPath = findConfig({ cwd: opts.configPath ?? opts.currentFile })
39+
const cwd = path.dirname(configPath)
3240

3341
// The context cache ensures we don't reload the same config multiple times
3442
if (!contextCache[configPath]) {
3543
contextCache[configPath] = _getContext(configPath)
3644
}
3745

38-
return await contextCache[configPath]
46+
return contextCache[configPath]
3947
}
4048
}
4149

42-
async function filterInvalidTokens(ctx: PandaContext, paths: string[]): Promise<string[]> {
43-
return paths.filter((path) => !ctx.utility.tokens.view.get(path))
50+
async function filterInvalidTokens(ctx: Generator, paths: string[]): Promise<string[]> {
51+
const invalid = paths.filter((path) => !ctx.utility.tokens.view.get(path))
52+
console.error('filterInvalidTokens', { paths, invalid })
53+
return invalid
4454
}
4555

4656
export type DeprecatedToken =
@@ -50,67 +60,77 @@ export type DeprecatedToken =
5060
value: string
5161
}
5262

53-
async function filterDeprecatedTokens(ctx: PandaContext, tokens: DeprecatedToken[]): Promise<DeprecatedToken[]> {
63+
async function filterDeprecatedTokens(ctx: Generator, tokens: DeprecatedToken[]): Promise<DeprecatedToken[]> {
5464
return tokens.filter((token) => {
5565
const value = typeof token === 'string' ? token : token.category + '.' + token.value
5666
return ctx.utility.tokens.isDeprecated(value)
5767
})
5868
}
5969

60-
async function isColorToken(ctx: PandaContext, value: string): Promise<boolean> {
70+
async function isColorToken(ctx: Generator, value: string): Promise<boolean> {
6171
return !!ctx.utility.tokens.view.categoryMap.get('colors')?.get(value)
6272
}
6373

64-
async function getPropCategory(ctx: PandaContext, _attr: string) {
74+
async function getPropCategory(ctx: Generator, _attr: string) {
6575
const longhand = await resolveLongHand(ctx, _attr)
6676
const attr = longhand || _attr
6777
const attrConfig = ctx.utility.config[attr]
6878
return typeof attrConfig?.values === 'string' ? attrConfig.values : undefined
6979
}
7080

71-
async function isColorAttribute(ctx: PandaContext, _attr: string): Promise<boolean> {
81+
async function isColorAttribute(ctx: Generator, _attr: string): Promise<boolean> {
7282
const category = await getPropCategory(ctx, _attr)
7383
return category === 'colors'
7484
}
7585

76-
const arePathsEqual = (path1: string, path2: string) => {
77-
const normalizedPath1 = path.resolve(path1)
78-
const normalizedPath2 = path.resolve(path2)
86+
async function isValidFile(ctx: Generator, fileName: string): Promise<boolean> {
87+
const { include, exclude } = ctx.config
88+
const cwd = ctx.config.cwd || process.cwd()
7989

80-
return normalizedPath1 === normalizedPath2
81-
}
90+
const relativePath = path.isAbsolute(fileName) ? path.relative(cwd, fileName) : fileName
8291

83-
async function isValidFile(ctx: PandaContext, fileName: string): Promise<boolean> {
84-
return ctx.getFiles().some((file) => arePathsEqual(file, fileName))
92+
return micromatch.isMatch(relativePath, include, { ignore: exclude, dot: true })
8593
}
8694

87-
async function resolveShorthands(ctx: PandaContext, name: string): Promise<string[] | undefined> {
95+
async function resolveShorthands(ctx: Generator, name: string): Promise<string[] | undefined> {
8896
return ctx.utility.getPropShorthandsMap().get(name)
8997
}
9098

91-
async function resolveLongHand(ctx: PandaContext, name: string): Promise<string | undefined> {
99+
async function resolveLongHand(ctx: Generator, name: string): Promise<string | undefined> {
92100
const reverseShorthandsMap = new Map()
93101

94-
for (const [key, values] of ctx.utility.getPropShorthandsMap()) {
102+
const shorthands = ctx.utility.getPropShorthandsMap()
103+
104+
for (const [key, values] of shorthands) {
95105
for (const value of values) {
96106
reverseShorthandsMap.set(value, key)
97107
}
98108
}
99109

100-
return reverseShorthandsMap.get(name)
110+
const result = reverseShorthandsMap.get(name)
111+
return result
101112
}
102113

103-
async function isValidProperty(ctx: PandaContext, name: string, patternName?: string) {
104-
if (ctx.isValidProperty(name)) return true
105-
if (!patternName) return
114+
async function isValidProperty(ctx: Generator, name: string, patternName?: string) {
115+
const isValid = ctx.isValidProperty(name)
116+
if (isValid) return true
117+
if (!patternName) return false
118+
119+
// If the pattern name is the jsxFactory (e.g., 'styled'), we should accept
120+
// any property that is valid according to the global property check
121+
// Since styled components are generic wrappers, we don't need pattern-specific checks
122+
if (patternName === ctx.config.jsxFactory) {
123+
// Already checked globally above, so return false if we got here
124+
return false
125+
}
106126

107127
const pattern = ctx.patterns.details.find((p) => p.baseName === patternName || p.jsx.includes(patternName))?.config
108128
.properties
109-
if (!pattern) return
129+
if (!pattern) return false
110130
return Object.keys(pattern).includes(name)
111131
}
112132

113-
async function matchFile(ctx: PandaContext, name: string, imports: ImportResult[]) {
133+
async function matchFile(ctx: Generator, name: string, imports: ImportResult[]) {
114134
const file = ctx.imports.file(imports)
115135

116136
return file.match(name)
@@ -121,12 +141,17 @@ type MatchImportResult = {
121141
alias: string
122142
mod: string
123143
}
124-
async function matchImports(ctx: PandaContext, result: MatchImportResult) {
125-
return ctx.imports.match(result, (mod) => {
144+
async function matchImports(ctx: Generator, result: MatchImportResult) {
145+
const isMatch = ctx.imports.match(result, (mod) => {
126146
const { tsOptions } = ctx.parserOptions
127147
if (!tsOptions?.pathMappings) return
128148
return resolveTsPathPattern(tsOptions.pathMappings, mod)
129149
})
150+
return isMatch
151+
}
152+
153+
async function getJsxFactory(ctx: Generator) {
154+
return ctx.config.jsxFactory
130155
}
131156

132157
export function runAsync(action: 'filterInvalidTokens', opts: Opts, paths: string[]): Promise<string[]>
@@ -139,6 +164,7 @@ export function runAsync(action: 'isValidProperty', opts: Opts, name: string, pa
139164
export function runAsync(action: 'matchFile', opts: Opts, name: string, imports: ImportResult[]): Promise<boolean>
140165
export function runAsync(action: 'matchImports', opts: Opts, result: MatchImportResult): Promise<boolean>
141166
export function runAsync(action: 'getPropCategory', opts: Opts, prop: string): Promise<string>
167+
export function runAsync(action: 'getJsxFactory', opts: Opts): Promise<string | undefined>
142168
export function runAsync(
143169
action: 'filterDeprecatedTokens',
144170
opts: Opts,
@@ -177,6 +203,8 @@ export async function runAsync(action: string, opts: Opts, ...args: any): Promis
177203
case 'getPropCategory':
178204
// @ts-expect-error cast
179205
return getPropCategory(ctx, ...args)
206+
case 'getJsxFactory':
207+
return getJsxFactory(ctx)
180208
case 'filterDeprecatedTokens':
181209
// @ts-expect-error cast
182210
return filterDeprecatedTokens(ctx, ...args)
@@ -193,6 +221,7 @@ export function run(action: 'isValidProperty', opts: Opts, name: string, pattern
193221
export function run(action: 'matchFile', opts: Opts, name: string, imports: ImportResult[]): boolean
194222
export function run(action: 'matchImports', opts: Opts, result: MatchImportResult): boolean
195223
export function run(action: 'getPropCategory', opts: Opts, prop: string): string
224+
export function run(action: 'getJsxFactory', opts: Opts): string | undefined
196225
export function run(action: 'filterDeprecatedTokens', opts: Opts, tokens: DeprecatedToken[]): DeprecatedToken[]
197226
export function run(action: string, opts: Opts, ...args: any[]): any {
198227
// @ts-expect-error cast

pnpm-lock.yaml

Lines changed: 26 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)