Skip to content
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

Feature: ESLint plugin 🐼 #2016

Closed
wants to merge 6 commits into from
Closed
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
22 changes: 22 additions & 0 deletions .changeset/small-dogs-jump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
'@pandacss/eslint-plugin': minor
---

Add Panda Eslint Plugin.

Install the package:

```bash
pnpm add -D @pandacss/eslint-plugin
```

Add it to your `.eslintrc.json` file, then configure the rules you want to use under the rules section:

```json
{
"plugins": ["@pandacss"],
"rules": {
"@pandacss/no-shorthand-prop": "warn"
}
}
```
5 changes: 5 additions & 0 deletions .changeset/smart-dolphins-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@pandacss/shared': patch
---

Update `getArbitraryValue` so it works for values that start on a new line
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"build:playground": "pnpm --filter {./playground}... build",
"prepare": "husky install && pnpm build-fast",
"dev": "pnpm --parallel --filter=./packages/** dev",
"build-fast": "pnpm -r --parallel --filter=./packages/** build-fast",
"build-fast": "pnpm -r --filter=./packages/** build-fast",
"build": "pnpm -r --filter=./packages/** build",
"check": "pnpm build && pnpm typecheck && pnpm lint && pnpm test run",
"clean": "pnpm -r --parallel exec rimraf dist .turbo *.log",
Expand Down Expand Up @@ -56,6 +56,7 @@
"@types/node": "20.4.5",
"@typescript-eslint/eslint-plugin": "6.2.1",
"@typescript-eslint/parser": "6.2.1",
"@typescript-eslint/rule-tester": "^6.19.1",
"eslint": "^8.54.0",
"eslint-config-prettier": "^8.9.0",
"prettier": "^2.8.8"
Expand Down
43 changes: 43 additions & 0 deletions packages/eslint-plugin/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"name": "@pandacss/eslint-plugin",
"version": "0.0.1",
"description": "Eslint plugin for Panda CSS",
"author": "Abraham Aremu <[email protected]>",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"sideEffects": false,
"publishConfig": {
"access": "public"
},
"exports": {
".": {
"source": "./src/index.ts",
"types": "./dist/index.d.ts",
"require": "./dist/index.js",
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
}
},
"./package.json": "./package.json"
},
"scripts": {
"build": "tsup",
"dev": "tsup --watch"
},
"files": [
"dist"
],
"dependencies": {
"@pandacss/config": "workspace:*",
"@pandacss/core": "workspace:*",
"@pandacss/node": "workspace:*",
"@pandacss/shared": "workspace:*",
"@pandacss/types": "workspace:*",
"synckit": "^0.9.0"
},
"peerDependencies": {
"eslint": "*"
}
}
19 changes: 19 additions & 0 deletions packages/eslint-plugin/src/configs/all.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { rules } from '../rules'
import { RULE_NAME as FileNotIncluded } from '../rules/file-not-included'
import { RULE_NAME as NoConfigunctionInSource } from '../rules/no-config-function-in-source'
import { RULE_NAME as NoInvalidTokenPaths } from '../rules/no-invalid-token-paths'

const errorRules = [FileNotIncluded, NoConfigunctionInSource, NoInvalidTokenPaths]

const allRules = Object.fromEntries(
Object.entries(rules).map(([name]) => {
return [`@pandacss/${name}`, errorRules.includes(name) ? 'error' : 'warn']
}),
)

export default {
parser: '@typescript-eslint/parser',
parserOptions: { sourceType: 'module' },
plugins: ['@pandacss'],
rules: allRules,
}
12 changes: 12 additions & 0 deletions packages/eslint-plugin/src/configs/recommended.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default {
parser: '@typescript-eslint/parser',
parserOptions: { sourceType: 'module' },
plugins: ['@pandacss'],
rules: {
'@pandacss/file-not-included': 'error',
'@pandacss/no-config-function-in-source': 'error',
'@pandacss/no-debug': 'warn',
'@pandacss/no-dynamic-styling': 'warn',
'@pandacss/no-invalid-token-paths': 'error',
},
}
20 changes: 20 additions & 0 deletions packages/eslint-plugin/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { name, version } from '../package.json'

import all from './configs/all'
import recommended from './configs/recommended'

import { rules } from './rules'

const plugin = {
meta: {
name,
version,
},
rules,
configs: {
all,
recommended,
},
}

module.exports = plugin
31 changes: 31 additions & 0 deletions packages/eslint-plugin/src/rules/file-not-included.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { tester } from '../../test-utils'
import rule, { RULE_NAME } from './file-not-included'

const code = `import { css } from './panda/css'
import { Circle } from './panda/jsx'
`

tester.run(RULE_NAME, rule as any, {
valid: [
{
code,
filename: './src/valid.tsx',
},
],
invalid: [
{
code,
filename: './src/invalid.tsx',
errors: [
{
messageId: 'include',
suggestions: null,
},
{
messageId: 'include',
suggestions: null,
},
],
},
],
})
35 changes: 35 additions & 0 deletions packages/eslint-plugin/src/rules/file-not-included.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { type Rule, createRule } from '../utils'
import { isPandaImport, isValidFile } from '../utils/helpers'

export const RULE_NAME = 'file-not-included'

const rule: Rule = createRule({
name: RULE_NAME,
meta: {
docs: {
description:
'Disallow the use of panda css in files that are not included in the specified panda `include` config.',
},
messages: {
include: 'The use of Panda CSS is not allowed in this file. Please check the specified `include` config.',
},
type: 'suggestion',
schema: [],
},
defaultOptions: [],
create(context) {
return {
ImportDeclaration(node) {
if (!isPandaImport(node, context)) return
if (isValidFile(context)) return

context.report({
node,
messageId: 'include',
})
},
}
},
})

export default rule
23 changes: 23 additions & 0 deletions packages/eslint-plugin/src/rules/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import fileNotIncluded, { RULE_NAME as FileNotIncluded } from './file-not-included'
import noConfigunctionInSource, { RULE_NAME as NoConfigunctionInSource } from './no-config-function-in-source'
import noDebug, { RULE_NAME as NoDebug } from './no-debug'
import noDynamicStyling, { RULE_NAME as NoDynamicStyling } from './no-dynamic-styling'
import noEscapeHatch, { RULE_NAME as NoEscapeHatch } from './no-escape-hatch'
import noHardCodedColor, { RULE_NAME as NoHardCodedColor } from './no-hardcoded-color'
import noInvalidTokenPaths, { RULE_NAME as NoInvalidTokenPaths } from './no-invalid-token-paths'
import noShorthandProp, { RULE_NAME as NoShorthandProp } from './no-shorthand-prop'
import noUnsafeTokenUsage, { RULE_NAME as NoUnsafeTokenUsage } from './no-unsafe-token-fn-usage'
import preferAtomicProperties, { RULE_NAME as PreferAtomicProperties } from './prefer-atomic-properties'

export const rules = {
[FileNotIncluded]: fileNotIncluded,
[NoConfigunctionInSource]: noConfigunctionInSource,
[NoDebug]: noDebug,
[NoDynamicStyling]: noDynamicStyling,
[NoEscapeHatch]: noEscapeHatch,
[NoHardCodedColor]: noHardCodedColor,
[NoInvalidTokenPaths]: noInvalidTokenPaths,
[NoShorthandProp]: noShorthandProp,
[NoUnsafeTokenUsage]: noUnsafeTokenUsage,
[PreferAtomicProperties]: preferAtomicProperties,
} as any
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { tester } from '../../test-utils'
import rule, { RULE_NAME } from './no-config-function-in-source'

const imports = `import { defineKeyframes } from '@pandacss/dev';`
const code = `const keyframes = defineKeyframes({
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
})
`

tester.run(RULE_NAME, rule as any, {
valid: [
{
code: imports + code.trim(),
filename: './panda.config.ts',
},
],
invalid: [
{
code: imports + code.trim(),
filename: './src/valid.tsx',
errors: [
{
messageId: 'configFunction',
suggestions: null,
},
],
output: imports,
},
],
})
62 changes: 62 additions & 0 deletions packages/eslint-plugin/src/rules/no-config-function-in-source.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { isIdentifier, isVariableDeclaration } from '../utils/nodes'
import { type Rule, createRule } from '../utils'
import { getAncestor, isValidFile } from '../utils/helpers'

export const RULE_NAME = 'no-config-function-in-source'

const rule: Rule = createRule({
name: RULE_NAME,
meta: {
docs: {
description: 'Prohibit the use of config functions outside the Panda config.',
},
messages: {
configFunction: 'Remove `{{name}}` usage. Config functions should only be used in panda config',
},
type: 'suggestion',
fixable: 'code',
schema: [],
},
defaultOptions: [],
create(context) {
return {
CallExpression(node) {
if (!isIdentifier(node.callee)) return
if (!CONFIG_FUNCTIONS.includes(node.callee.name)) return

if (!isValidFile(context)) return

context.report({
node,
messageId: 'configFunction',
data: {
name: node.callee.name,
},
fix(fixer) {
const declaration = getAncestor(isVariableDeclaration, node)
return fixer.remove(declaration ?? node)
},
})
},
}
},
})

export default rule

const CONFIG_FUNCTIONS = [
'defineConfig',
'defineRecipe',
'defineSlotRecipe',
'defineParts',
'definePattern',
'definePreset',
'defineKeyframes',
'defineGlobalStyles',
'defineUtility',
'defineTextStyles',
'defineLayerStyles',
'defineStyles',
'defineTokens',
'defineSemanticTokens',
]
58 changes: 58 additions & 0 deletions packages/eslint-plugin/src/rules/no-debug.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import rule, { RULE_NAME } from './no-debug'
import { tester } from '../../test-utils'

const imports = `import { css } from './panda/css';
import { styled, Circle } from './panda/jsx';`

const valids = [
'const styles = { debug: true }',
'const styles = css({ bg: "red" })',
'const styles = css.raw({ bg: "red" })',
'const randomFunc = f({ debug: true })',
'<NonPandaComponent debug={true} />',
'<NonPandaComponent debug={true}>content</NonPandaComponent>',
`const a = 1; const PandaComp = styled(div); <PandaComp someProp={{ debug: true }} />`,
]

const invalids = [
{ code: 'const styles = css({ bg: "red", debug: true })', output: 'const styles = css({ bg: "red", })' },
{
code: 'const styles = css.raw({ bg: "red", debug: true })',
output: 'const styles = css.raw({ bg: "red", })',
},
{
code: 'const styles = css({ bg: "red", "&:hover": { debug: true } })',
output: 'const styles = css({ bg: "red", "&:hover": { } })',
},
{
code: 'const styles = css({ bg: "red", "&:hover": { "&:disabled": { debug: true } } })',
output: 'const styles = css({ bg: "red", "&:hover": { "&:disabled": { } } })',
},
{ code: '<Circle debug />', output: '<Circle />' },
{ code: '<Circle debug={true} />', output: '<Circle />' },
{ code: '<Circle css={{ debug: true }} />', output: '<Circle css={{ }} />' },
{ code: '<Circle css={{ "&:hover": { debug: true } }} />', output: '<Circle css={{ "&:hover": { } }} />' },
{ code: '<styled.div _hover={{ debug: true }} />', output: '<styled.div _hover={{ }} />' },
{
code: `const PandaComp = styled(div); <PandaComp css={{ debug: true }} />`,
output: 'const PandaComp = styled(div); <PandaComp css={{ }} />',
},
]

tester.run(RULE_NAME, rule as any, {
valid: valids.map((code) => ({
code: imports + code,
filename: './src/valid.tsx',
})),
invalid: invalids.map(({ code, output }) => ({
code: imports + code,
filename: './src/invalid.tsx',
errors: [
{
messageId: 'debug',
suggestions: null,
},
],
output: imports + output,
})),
})
Loading