-
Notifications
You must be signed in to change notification settings - Fork 8
perf(worker): avoid accessing panda context to scan all files #291
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@pandacss/eslint-plugin': minor | ||
| --- | ||
|
|
||
| Improve performance |
| 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, | ||
| include: ['**/*'], | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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', | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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', | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 = | ||
|
|
@@ -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) | ||
|
|
@@ -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[]> | ||
|
|
@@ -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, | ||
|
|
@@ -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 | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
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