Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/ten-phones-explode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@pandacss/eslint-plugin': minor
---

Improve performance
3 changes: 2 additions & 1 deletion fixture/src/create-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { PandaContext } from '@pandacss/node'
import { stringifyJson, parseJson } from '@pandacss/shared'
import type { Config, LoadConfigResult, UserConfig } from '@pandacss/types'
import { fixturePreset } from './config'
export { default as v9Config } from '../../sandbox/v9/panda.config'
import v9Config from '../../sandbox/v9/panda.config'

const config: UserConfig = {
Expand All @@ -28,7 +29,7 @@ export const fixtureDefaults = {

export const createGeneratorContext = (userConfig?: Config) => {
const resolvedConfig = (
userConfig ? mergeConfigs([userConfig, fixtureDefaults.config]) : fixtureDefaults.config
userConfig ? mergeConfigs([fixtureDefaults.config, userConfig]) : fixtureDefaults.config
) as UserConfig

return new Generator({ ...fixtureDefaults, config: resolvedConfig })
Expand Down
1 change: 1 addition & 0 deletions fixture/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './config'
export * from './create-context'
export { v9Config } from './create-context'
export * from './layers'
export * from './recipes'
export * from './semantic-tokens'
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@
"dependencies": {
"@changesets/changelog-github": "^0.5.0",
"@changesets/cli": "^2.27.1",
"@types/micromatch": "^4.0.10",
"@typescript-eslint/utils": "^8.21.0",
"esbuild": "0.25.0",
"micromatch": "^4.0.8",
"tsup": "^8.0.1",
"typescript": "^5.3.3"
},
Expand Down
2 changes: 2 additions & 0 deletions plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,14 @@
"@pandacss/shared": "^0.53.2",
"@typescript-eslint/utils": "^8.21.0",
"hookable": "^5.5.3",
"micromatch": "^4.0.8",
"synckit": "^0.9.0"
},
"peerDependencies": {
"eslint": "*"
},
"devDependencies": {
"@types/micromatch": "^4.0.10",
"typescript": "^5.7.2"
}
}
14 changes: 12 additions & 2 deletions plugin/src/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ const isValidStyledProp = <T extends Node>(node: T, context: RuleContext<any, an
export const isPandaIsh = (name: string, context: RuleContext<any, any>) => {
const imports = getImports(context)
if (imports.length === 0) return false
// Check if the name is the jsx factory
const jsxFactory = syncAction('getJsxFactory', getSyncOpts(context))
if (jsxFactory && name === jsxFactory) {
// Check if the jsx factory is imported
return imports.some((imp) => imp.name === name || imp.alias === name)
}
return syncAction('matchFile', getSyncOpts(context), name, imports)
}

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

// Ensure component is a panda component
if (!isPandaIsh(name, context) && !isLocalStyledFactory(jsxAncestor, context)) return
const isPandaComponent = isPandaIsh(name, context) || isLocalStyledFactory(jsxAncestor, context)
if (!isPandaComponent) return

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

return true
}
Expand Down
93 changes: 61 additions & 32 deletions plugin/src/utils/worker.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,56 @@
import { PandaContext, loadConfigAndCreateContext } from '@pandacss/node'
import { Generator } from '@pandacss/generator'
import { runAsWorker } from 'synckit'
import { createContext } from 'fixture'
import { createGeneratorContext, v9Config } from 'fixture'
import { resolveTsPathPattern } from '@pandacss/config/ts-path'
import { findConfig } from '@pandacss/config'
import { findConfig, loadConfig } from '@pandacss/config'
import path from 'path'
import micromatch from 'micromatch'
import type { ImportResult } from '.'

type Opts = {
currentFile: string
configPath?: string
}

const contextCache: { [configPath: string]: Promise<PandaContext> } = {}
const contextCache: { [configPath: string]: Promise<Generator> } = {}

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

const cwd = path.dirname(configPath)

const ctx = await loadConfigAndCreateContext({ configPath, cwd })
const conf = await loadConfig({ file: configPath, cwd })
const ctx = new Generator(conf)
return ctx
}

export async function getContext(opts: Opts) {
if (process.env.NODE_ENV === 'test') {
const ctx = createContext() as unknown as PandaContext
ctx.getFiles = () => ['App.tsx']
const ctx = createGeneratorContext({
...v9Config,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the tests rely on the specific configuration (tokens, recipes, conditions) defined in
sandbox/v9/panda.config.ts

include: ['**/*'],
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensures that all files used in tests (e.g., App.tsx) are considered valid

exclude: ['**/Invalid.tsx', '**/panda.config.ts'],
importMap: './panda',
Copy link
Contributor Author

@farlock farlock Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test files import Panda artifacts from ./panda/... The generator needs this importMap to correctly identify these imports as belonging to Panda.

jsxFactory: 'styled',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Th sandbox/v9/panda.config.ts sets jsxFactory to 'panda', but the tests use styled (e.g., styled.div). Overriding this to 'styled' ensures that matchImports correctly identifies styled as the JSX factory, enabling rules like no-dynamic-styling to work correctly....

})
return ctx
} else {
const configPath = findConfig({ cwd: opts.configPath ?? opts.currentFile })
const cwd = path.dirname(configPath)

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

return await contextCache[configPath]
return contextCache[configPath]
}
}

async function filterInvalidTokens(ctx: PandaContext, paths: string[]): Promise<string[]> {
return paths.filter((path) => !ctx.utility.tokens.view.get(path))
async function filterInvalidTokens(ctx: Generator, paths: string[]): Promise<string[]> {
const invalid = paths.filter((path) => !ctx.utility.tokens.view.get(path))
console.error('filterInvalidTokens', { paths, invalid })
return invalid
}

export type DeprecatedToken =
Expand All @@ -50,67 +60,77 @@ export type DeprecatedToken =
value: string
}

async function filterDeprecatedTokens(ctx: PandaContext, tokens: DeprecatedToken[]): Promise<DeprecatedToken[]> {
async function filterDeprecatedTokens(ctx: Generator, tokens: DeprecatedToken[]): Promise<DeprecatedToken[]> {
return tokens.filter((token) => {
const value = typeof token === 'string' ? token : token.category + '.' + token.value
return ctx.utility.tokens.isDeprecated(value)
})
}

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

async function getPropCategory(ctx: PandaContext, _attr: string) {
async function getPropCategory(ctx: Generator, _attr: string) {
const longhand = await resolveLongHand(ctx, _attr)
const attr = longhand || _attr
const attrConfig = ctx.utility.config[attr]
return typeof attrConfig?.values === 'string' ? attrConfig.values : undefined
}

async function isColorAttribute(ctx: PandaContext, _attr: string): Promise<boolean> {
async function isColorAttribute(ctx: Generator, _attr: string): Promise<boolean> {
const category = await getPropCategory(ctx, _attr)
return category === 'colors'
}

const arePathsEqual = (path1: string, path2: string) => {
const normalizedPath1 = path.resolve(path1)
const normalizedPath2 = path.resolve(path2)
async function isValidFile(ctx: Generator, fileName: string): Promise<boolean> {
const { include, exclude } = ctx.config
const cwd = ctx.config.cwd || process.cwd()

return normalizedPath1 === normalizedPath2
}
const relativePath = path.isAbsolute(fileName) ? path.relative(cwd, fileName) : fileName

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

async function resolveShorthands(ctx: PandaContext, name: string): Promise<string[] | undefined> {
async function resolveShorthands(ctx: Generator, name: string): Promise<string[] | undefined> {
return ctx.utility.getPropShorthandsMap().get(name)
}

async function resolveLongHand(ctx: PandaContext, name: string): Promise<string | undefined> {
async function resolveLongHand(ctx: Generator, name: string): Promise<string | undefined> {
const reverseShorthandsMap = new Map()

for (const [key, values] of ctx.utility.getPropShorthandsMap()) {
const shorthands = ctx.utility.getPropShorthandsMap()

for (const [key, values] of shorthands) {
for (const value of values) {
reverseShorthandsMap.set(value, key)
}
}

return reverseShorthandsMap.get(name)
const result = reverseShorthandsMap.get(name)
return result
}

async function isValidProperty(ctx: PandaContext, name: string, patternName?: string) {
if (ctx.isValidProperty(name)) return true
if (!patternName) return
async function isValidProperty(ctx: Generator, name: string, patternName?: string) {
const isValid = ctx.isValidProperty(name)
if (isValid) return true
if (!patternName) return false

// If the pattern name is the jsxFactory (e.g., 'styled'), we should accept
// any property that is valid according to the global property check
// Since styled components are generic wrappers, we don't need pattern-specific checks
if (patternName === ctx.config.jsxFactory) {
// Already checked globally above, so return false if we got here
return false
}

const pattern = ctx.patterns.details.find((p) => p.baseName === patternName || p.jsx.includes(patternName))?.config
.properties
if (!pattern) return
if (!pattern) return false
return Object.keys(pattern).includes(name)
}

async function matchFile(ctx: PandaContext, name: string, imports: ImportResult[]) {
async function matchFile(ctx: Generator, name: string, imports: ImportResult[]) {
const file = ctx.imports.file(imports)

return file.match(name)
Expand All @@ -121,12 +141,17 @@ type MatchImportResult = {
alias: string
mod: string
}
async function matchImports(ctx: PandaContext, result: MatchImportResult) {
return ctx.imports.match(result, (mod) => {
async function matchImports(ctx: Generator, result: MatchImportResult) {
const isMatch = ctx.imports.match(result, (mod) => {
const { tsOptions } = ctx.parserOptions
if (!tsOptions?.pathMappings) return
return resolveTsPathPattern(tsOptions.pathMappings, mod)
})
return isMatch
}

async function getJsxFactory(ctx: Generator) {
return ctx.config.jsxFactory
}

export function runAsync(action: 'filterInvalidTokens', opts: Opts, paths: string[]): Promise<string[]>
Expand All @@ -139,6 +164,7 @@ export function runAsync(action: 'isValidProperty', opts: Opts, name: string, pa
export function runAsync(action: 'matchFile', opts: Opts, name: string, imports: ImportResult[]): Promise<boolean>
export function runAsync(action: 'matchImports', opts: Opts, result: MatchImportResult): Promise<boolean>
export function runAsync(action: 'getPropCategory', opts: Opts, prop: string): Promise<string>
export function runAsync(action: 'getJsxFactory', opts: Opts): Promise<string | undefined>
export function runAsync(
action: 'filterDeprecatedTokens',
opts: Opts,
Expand Down Expand Up @@ -177,6 +203,8 @@ export async function runAsync(action: string, opts: Opts, ...args: any): Promis
case 'getPropCategory':
// @ts-expect-error cast
return getPropCategory(ctx, ...args)
case 'getJsxFactory':
return getJsxFactory(ctx)
case 'filterDeprecatedTokens':
// @ts-expect-error cast
return filterDeprecatedTokens(ctx, ...args)
Expand All @@ -193,6 +221,7 @@ export function run(action: 'isValidProperty', opts: Opts, name: string, pattern
export function run(action: 'matchFile', opts: Opts, name: string, imports: ImportResult[]): boolean
export function run(action: 'matchImports', opts: Opts, result: MatchImportResult): boolean
export function run(action: 'getPropCategory', opts: Opts, prop: string): string
export function run(action: 'getJsxFactory', opts: Opts): string | undefined
export function run(action: 'filterDeprecatedTokens', opts: Opts, tokens: DeprecatedToken[]): DeprecatedToken[]
export function run(action: string, opts: Opts, ...args: any[]): any {
// @ts-expect-error cast
Expand Down
30 changes: 26 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.