diff --git a/.changeset/ten-phones-explode.md b/.changeset/ten-phones-explode.md new file mode 100644 index 0000000..dd125dd --- /dev/null +++ b/.changeset/ten-phones-explode.md @@ -0,0 +1,5 @@ +--- +'@pandacss/eslint-plugin': minor +--- + +Improve performance diff --git a/fixture/src/create-context.ts b/fixture/src/create-context.ts index 668cc54..5d3bc3e 100644 --- a/fixture/src/create-context.ts +++ b/fixture/src/create-context.ts @@ -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 = { @@ -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 }) diff --git a/fixture/src/index.ts b/fixture/src/index.ts index 2509731..761d177 100644 --- a/fixture/src/index.ts +++ b/fixture/src/index.ts @@ -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' diff --git a/package.json b/package.json index fa8b4e3..b41cb47 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/plugin/package.json b/plugin/package.json index 2ef2743..5c9e4cd 100644 --- a/plugin/package.json +++ b/plugin/package.json @@ -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" } } diff --git a/plugin/src/utils/helpers.ts b/plugin/src/utils/helpers.ts index 635e90b..f280d88 100644 --- a/plugin/src/utils/helpers.ts +++ b/plugin/src/utils/helpers.ts @@ -97,6 +97,12 @@ const isValidStyledProp = (node: T, context: RuleContext) => { 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) } @@ -159,10 +165,14 @@ export const isPandaProp = (node: TSESTree.JSXAttribute, context: RuleContext } = {} +const contextCache: { [configPath: string]: Promise } = {} 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, + include: ['**/*'], + exclude: ['**/Invalid.tsx', '**/panda.config.ts'], + importMap: './panda', + jsxFactory: 'styled', + }) 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 { - return paths.filter((path) => !ctx.utility.tokens.view.get(path)) +async function filterInvalidTokens(ctx: Generator, paths: string[]): Promise { + const invalid = paths.filter((path) => !ctx.utility.tokens.view.get(path)) + console.error('filterInvalidTokens', { paths, invalid }) + return invalid } export type DeprecatedToken = @@ -50,67 +60,77 @@ export type DeprecatedToken = value: string } -async function filterDeprecatedTokens(ctx: PandaContext, tokens: DeprecatedToken[]): Promise { +async function filterDeprecatedTokens(ctx: Generator, tokens: DeprecatedToken[]): Promise { 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 { +async function isColorToken(ctx: Generator, value: string): Promise { 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 { +async function isColorAttribute(ctx: Generator, _attr: string): Promise { 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 { + 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 { - 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 { +async function resolveShorthands(ctx: Generator, name: string): Promise { return ctx.utility.getPropShorthandsMap().get(name) } -async function resolveLongHand(ctx: PandaContext, name: string): Promise { +async function resolveLongHand(ctx: Generator, name: string): Promise { 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) @@ -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 @@ -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 export function runAsync(action: 'matchImports', opts: Opts, result: MatchImportResult): Promise export function runAsync(action: 'getPropCategory', opts: Opts, prop: string): Promise +export function runAsync(action: 'getJsxFactory', opts: Opts): Promise export function runAsync( action: 'filterDeprecatedTokens', opts: Opts, @@ -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) @@ -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 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 788514c..b1469d7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,15 +19,21 @@ importers: '@changesets/cli': specifier: ^2.27.1 version: 2.27.12 + '@types/micromatch': + specifier: ^4.0.10 + version: 4.0.10 '@typescript-eslint/utils': specifier: ^8.21.0 version: 8.24.0(eslint@8.57.1)(typescript@5.7.3) esbuild: specifier: 0.25.0 version: 0.25.0 + micromatch: + specifier: ^4.0.8 + version: 4.0.8 tsup: specifier: ^8.0.1 - version: 8.3.6(typescript@5.7.3) + version: 8.3.6(postcss@8.4.49)(typescript@5.7.3) typescript: specifier: ^5.3.3 version: 5.7.3 @@ -125,10 +131,16 @@ importers: hookable: specifier: ^5.5.3 version: 5.5.3 + micromatch: + specifier: ^4.0.8 + version: 4.0.8 synckit: specifier: ^0.9.0 version: 0.9.2 devDependencies: + '@types/micromatch': + specifier: ^4.0.10 + version: 4.0.10 typescript: specifier: ^5.7.2 version: 5.7.3 @@ -2553,6 +2565,9 @@ packages: '@babel/types': 7.26.8 dev: true + /@types/braces@3.0.5: + resolution: {integrity: sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w==} + /@types/eslint@8.56.12: resolution: {integrity: sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==} dependencies: @@ -2571,6 +2586,11 @@ packages: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: true + /@types/micromatch@4.0.10: + resolution: {integrity: sha512-5jOhFDElqr4DKTrTEbnW8DZ4Hz5LRUEmyrGpCMrD/NphYv3nUnaF08xmSLx1rGGnyEs/kFnhiw6dCgcDqMr5PQ==} + dependencies: + '@types/braces': 3.0.5 + /@types/node@12.20.55: resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} dev: false @@ -5300,7 +5320,7 @@ packages: dependencies: postcss: 8.4.49 - /postcss-load-config@6.0.1: + /postcss-load-config@6.0.1(postcss@8.4.49): resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} engines: {node: '>= 18'} peerDependencies: @@ -5319,6 +5339,7 @@ packages: optional: true dependencies: lilconfig: 3.1.3 + postcss: 8.4.49 dev: false /postcss-merge-rules@7.0.0(postcss@8.4.38): @@ -5974,7 +5995,7 @@ packages: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} dev: false - /tsup@8.3.6(typescript@5.7.3): + /tsup@8.3.6(postcss@8.4.49)(typescript@5.7.3): resolution: {integrity: sha512-XkVtlDV/58S9Ye0JxUUTcrQk4S+EqlOHKzg6Roa62rdjL1nGWNUstG0xgI4vanHdfIpjP448J8vlN0oK6XOJ5g==} engines: {node: '>=18'} hasBin: true @@ -6001,7 +6022,8 @@ packages: esbuild: 0.24.2 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1 + postcss: 8.4.49 + postcss-load-config: 6.0.1(postcss@8.4.49) resolve-from: 5.0.0 rollup: 4.34.6 source-map: 0.8.0-beta.0