diff --git a/.changeset/early-lies-sell.md b/.changeset/early-lies-sell.md
new file mode 100644
index 00000000..7e58d62f
--- /dev/null
+++ b/.changeset/early-lies-sell.md
@@ -0,0 +1,5 @@
+---
+'@devup-ui/eslint-plugin': patch
+---
+
+Implement style order rule
diff --git a/packages/eslint-plugin/src/configs/__tests__/__snapshots__/recommended.test.ts.snap b/packages/eslint-plugin/src/configs/__tests__/__snapshots__/recommended.test.ts.snap
index bd5c0b2d..f1467256 100644
--- a/packages/eslint-plugin/src/configs/__tests__/__snapshots__/recommended.test.ts.snap
+++ b/packages/eslint-plugin/src/configs/__tests__/__snapshots__/recommended.test.ts.snap
@@ -73,6 +73,22 @@ exports[`recommended > export recommended config 1`] = `
},
"name": "no-useless-tailing-nulls",
},
+ "style-order-range": {
+ "create": [Function],
+ "defaultOptions": [],
+ "meta": {
+ "docs": {
+ "description": "Ensures styleOrder prop is within valid range (0 < value < 255).",
+ "url": "https://github.com/dev-five-git/devup-ui/tree/main/packages/eslint-plugin/src/rules/style-order-range",
+ },
+ "messages": {
+ "styleOrderRange": "styleOrder prop must be a number greater than 0 and less than 255.",
+ },
+ "schema": [],
+ "type": "problem",
+ },
+ "name": "style-order-range",
+ },
},
},
},
@@ -81,6 +97,7 @@ exports[`recommended > export recommended config 1`] = `
"@devup-ui/no-duplicate-value": "error",
"@devup-ui/no-useless-responsive": "error",
"@devup-ui/no-useless-tailing-nulls": "error",
+ "@devup-ui/style-order-range": "error",
},
},
]
diff --git a/packages/eslint-plugin/src/configs/recommended.ts b/packages/eslint-plugin/src/configs/recommended.ts
index 7bfef860..3827b73a 100644
--- a/packages/eslint-plugin/src/configs/recommended.ts
+++ b/packages/eslint-plugin/src/configs/recommended.ts
@@ -3,6 +3,7 @@ import {
noDuplicateValue,
noUselessResponsive,
noUselessTailingNulls,
+ styleOrderRange,
} from '../rules'
export default [
@@ -14,6 +15,7 @@ export default [
'css-utils-literal-only': cssUtilsLiteralOnly,
'no-duplicate-value': noDuplicateValue,
'no-useless-responsive': noUselessResponsive,
+ 'style-order-range': styleOrderRange,
},
},
},
@@ -22,6 +24,7 @@ export default [
'@devup-ui/css-utils-literal-only': 'error',
'@devup-ui/no-duplicate-value': 'error',
'@devup-ui/no-useless-responsive': 'error',
+ '@devup-ui/style-order-range': 'error',
},
},
]
diff --git a/packages/eslint-plugin/src/rules/__tests__/index.test.ts b/packages/eslint-plugin/src/rules/__tests__/index.test.ts
index 787c17c4..6c6900b3 100644
--- a/packages/eslint-plugin/src/rules/__tests__/index.test.ts
+++ b/packages/eslint-plugin/src/rules/__tests__/index.test.ts
@@ -6,6 +6,7 @@ describe('export index', () => {
cssUtilsLiteralOnly: expect.any(Object),
noDuplicateValue: expect.any(Object),
noUselessResponsive: expect.any(Object),
+ styleOrderRange: expect.any(Object),
})
})
})
diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts
index 2c7e3eb8..67184778 100644
--- a/packages/eslint-plugin/src/rules/index.ts
+++ b/packages/eslint-plugin/src/rules/index.ts
@@ -2,3 +2,4 @@ export * from './css-utils-literal-only'
export * from './no-duplicate-value'
export * from './no-useless-responsive'
export * from './no-useless-tailing-nulls'
+export * from './style-order-range'
diff --git a/packages/eslint-plugin/src/rules/style-order-range/README.md b/packages/eslint-plugin/src/rules/style-order-range/README.md
new file mode 100644
index 00000000..72c2da96
--- /dev/null
+++ b/packages/eslint-plugin/src/rules/style-order-range/README.md
@@ -0,0 +1,41 @@
+# style-order-range
+
+Ensures `styleOrder` prop is within valid range (0 < value < 255).
+
+## Rule Details
+
+This rule enforces that the `styleOrder` prop must be a number greater than 0 and less than 255.
+
+### Examples of **incorrect** code for this rule:
+
+```jsx
+// Zero and negative values
+
+
+
+
+// Values greater than or equal to 255
+
+
+
+
+// Non-numeric values
+
+
+```
+
+### Examples of **correct** code for this rule:
+
+```jsx
+// Valid range values (0 < value < 255)
+
+
+
+
+
+
+```
+
+## When Not To Use It
+
+If you don't use `styleOrder` props or want to allow any value range, you can disable this rule.
diff --git a/packages/eslint-plugin/src/rules/style-order-range/__tests__/index.test.ts b/packages/eslint-plugin/src/rules/style-order-range/__tests__/index.test.ts
new file mode 100644
index 00000000..2ebc5e97
--- /dev/null
+++ b/packages/eslint-plugin/src/rules/style-order-range/__tests__/index.test.ts
@@ -0,0 +1,349 @@
+import { RuleTester } from '@typescript-eslint/rule-tester'
+
+import { styleOrderRange } from '../index'
+
+describe('style-order-range rule', () => {
+ const ruleTester = new RuleTester({
+ languageOptions: {
+ ecmaVersion: 'latest',
+ parserOptions: {
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+ },
+ })
+
+ ruleTester.run('style-order-range rule', styleOrderRange, {
+ valid: [
+ {
+ code: 'import { Box } from "@devup-ui/react";\n',
+ filename: 'src/app/page.tsx',
+ },
+ {
+ code: 'import { Box } from "@devup-ui/react";\n',
+ filename: 'src/app/page.tsx',
+ },
+ {
+ code: 'import { Box } from "@devup-ui/react";\n',
+ filename: 'src/app/page.tsx',
+ },
+ {
+ code: 'import { Box } from "@devup-ui/react";\n',
+ filename: 'src/app/page.tsx',
+ },
+ {
+ code: 'import { Box } from "@devup-ui/react";\n',
+ filename: 'src/app/page.tsx',
+ },
+ {
+ code: 'import { Box } from "@devup-ui/react";\n',
+ filename: 'src/app/page.tsx',
+ },
+ {
+ code: 'import { Box } from "@devup-ui/react";\n',
+ filename: 'src/app/page.tsx',
+ },
+ {
+ code: 'import { Box } from "@devup-ui/react";\n',
+ filename: 'src/app/page.tsx',
+ },
+ {
+ code: 'import { Box } from "@devup-ui/react";\n',
+ filename: 'src/app/page.tsx',
+ },
+ {
+ code: 'import { Box } from "@devup-ui/react";\n',
+ filename: 'src/app/page.tsx',
+ },
+ {
+ code: 'import { Box } from "@devup-ui/react";\n',
+ filename: 'src/app/page.tsx',
+ },
+ {
+ code: 'import { css } from "@devup-ui/react";\ncss({styleOrder: 1})',
+ filename: 'src/app/page.tsx',
+ },
+ {
+ code: 'css({styleOrder: 1})',
+ filename: 'src/app/page.tsx',
+ },
+ {
+ code: '',
+ filename: 'src/app/page.tsx',
+ },
+ ],
+ invalid: [
+ {
+ code: 'import { Box } from "@devup-ui/react";\n',
+ filename: 'src/app/page.tsx',
+ errors: [
+ {
+ messageId: 'styleOrderRange',
+ },
+ ],
+ },
+ {
+ code: 'import { Box } from "@devup-ui/react";\n',
+ filename: 'src/app/page.tsx',
+ errors: [
+ {
+ messageId: 'styleOrderRange',
+ },
+ ],
+ },
+ {
+ code: 'import { Box } from "@devup-ui/react";\n',
+ filename: 'src/app/page.tsx',
+ errors: [
+ {
+ messageId: 'styleOrderRange',
+ },
+ ],
+ },
+ {
+ code: 'import { Box } from "@devup-ui/react";\n',
+ filename: 'src/app/page.tsx',
+ errors: [
+ {
+ messageId: 'styleOrderRange',
+ },
+ ],
+ },
+ {
+ code: 'import { Box } from "@devup-ui/react";\n',
+ filename: 'src/app/page.tsx',
+ errors: [
+ {
+ messageId: 'styleOrderRange',
+ },
+ ],
+ },
+ {
+ code: 'import { Box } from "@devup-ui/react";\n',
+ filename: 'src/app/page.tsx',
+ errors: [
+ {
+ messageId: 'styleOrderRange',
+ },
+ ],
+ },
+ {
+ code: 'import { Box } from "@devup-ui/react";\n',
+ filename: 'src/app/page.tsx',
+ errors: [
+ {
+ messageId: 'styleOrderRange',
+ },
+ ],
+ },
+ {
+ code: 'import { Box } from "@devup-ui/react";\n',
+ filename: 'src/app/page.tsx',
+ errors: [
+ {
+ messageId: 'styleOrderRange',
+ },
+ ],
+ },
+ {
+ code: 'import { Box } from "@devup-ui/react";\n',
+ filename: 'src/app/page.tsx',
+ errors: [
+ {
+ messageId: 'styleOrderRange',
+ },
+ ],
+ },
+ {
+ code: 'import { Box } from "@devup-ui/react";\n',
+ filename: 'src/app/page.tsx',
+ errors: [
+ {
+ messageId: 'styleOrderRange',
+ },
+ ],
+ },
+ {
+ code: 'import { Box } from "@devup-ui/react";\n',
+ filename: 'src/app/page.tsx',
+ errors: [
+ {
+ messageId: 'styleOrderRange',
+ },
+ ],
+ },
+ {
+ code: 'import { Box } from "@devup-ui/react";\n',
+ filename: 'src/app/page.tsx',
+ errors: [
+ {
+ messageId: 'styleOrderRange',
+ },
+ ],
+ },
+ {
+ code: 'import { Box } from "@devup-ui/react";\n',
+ filename: 'src/app/page.tsx',
+ errors: [
+ {
+ messageId: 'styleOrderRange',
+ },
+ ],
+ },
+ {
+ code: 'import { Box } from "@devup-ui/react";\n',
+ filename: 'src/app/page.tsx',
+ errors: [
+ {
+ messageId: 'styleOrderRange',
+ },
+ ],
+ },
+ {
+ code: 'import { Box } from "@devup-ui/react";\n',
+ filename: 'src/app/page.tsx',
+ errors: [
+ {
+ messageId: 'styleOrderRange',
+ },
+ ],
+ },
+ {
+ code: 'import { css } from "@devup-ui/react";\ncss({styleOrder: someVariable})',
+ filename: 'src/app/page.tsx',
+ errors: [
+ {
+ messageId: 'styleOrderRange',
+ },
+ ],
+ },
+ {
+ code: 'import { css } from "@devup-ui/react";\ncss({styleOrder: `someVariable`})',
+ filename: 'src/app/page.tsx',
+ errors: [
+ {
+ messageId: 'styleOrderRange',
+ },
+ ],
+ },
+ {
+ code: 'import { css } from "@devup-ui/react";\ncss({styleOrder: 1000})',
+ filename: 'src/app/page.tsx',
+ errors: [
+ {
+ messageId: 'styleOrderRange',
+ },
+ ],
+ },
+ {
+ code: 'import { css } from "@devup-ui/react";\ncss({styleOrder: -100})',
+ filename: 'src/app/page.tsx',
+ errors: [
+ {
+ messageId: 'styleOrderRange',
+ },
+ ],
+ },
+ {
+ code: 'import { css } from "@devup-ui/react";\ncss({styleOrder: "1000"})',
+ filename: 'src/app/page.tsx',
+ errors: [
+ {
+ messageId: 'styleOrderRange',
+ },
+ ],
+ },
+ {
+ code: 'import { css } from "@devup-ui/react";\ncss({styleOrder: `1000`})',
+ filename: 'src/app/page.tsx',
+ errors: [
+ {
+ messageId: 'styleOrderRange',
+ },
+ ],
+ },
+ {
+ code: 'import { css } from "@devup-ui/react";\ncss({styleOrder: `${someVariable}`})',
+ filename: 'src/app/page.tsx',
+ errors: [
+ {
+ messageId: 'styleOrderRange',
+ },
+ ],
+ },
+ {
+ code: 'import { css } from "@devup-ui/react";\ncss({styleOrder: +someVariable})',
+ filename: 'src/app/page.tsx',
+ errors: [
+ {
+ messageId: 'styleOrderRange',
+ },
+ ],
+ },
+ {
+ code: 'import { css } from "@devup-ui/react";\ncss({styleOrder: -someVariable})',
+ filename: 'src/app/page.tsx',
+ errors: [
+ {
+ messageId: 'styleOrderRange',
+ },
+ ],
+ },
+ {
+ code: 'import { css } from "@devup-ui/react";\ncss({styleOrder: typeof `100`})',
+ filename: 'src/app/page.tsx',
+ errors: [
+ {
+ messageId: 'styleOrderRange',
+ },
+ ],
+ },
+ {
+ code: 'import { css } from "@devup-ui/react";\ncss({styleOrder: void `100`})',
+ filename: 'src/app/page.tsx',
+ errors: [
+ {
+ messageId: 'styleOrderRange',
+ },
+ ],
+ },
+ {
+ code: 'import { css } from "@devup-ui/react";\ncss({styleOrder: delete `100`})',
+ filename: 'src/app/page.tsx',
+ errors: [
+ {
+ messageId: 'styleOrderRange',
+ },
+ ],
+ },
+ {
+ code: 'import { css } from "@devup-ui/react";\ncss({styleOrder: ~ `100`})',
+ filename: 'src/app/page.tsx',
+ errors: [
+ {
+ messageId: 'styleOrderRange',
+ },
+ ],
+ },
+ {
+ code: 'import { css } from "@devup-ui/react";\ncss({styleOrder: `100` + 100})',
+ filename: 'src/app/page.tsx',
+ errors: [
+ {
+ messageId: 'styleOrderRange',
+ },
+ ],
+ },
+ {
+ code: 'import { css } from "@devup-ui/react";\ncss({styleOrder: ~10})',
+ filename: 'src/app/page.tsx',
+ errors: [
+ {
+ messageId: 'styleOrderRange',
+ },
+ ],
+ },
+ ],
+ })
+})
diff --git a/packages/eslint-plugin/src/rules/style-order-range/index.ts b/packages/eslint-plugin/src/rules/style-order-range/index.ts
new file mode 100644
index 00000000..978d1136
--- /dev/null
+++ b/packages/eslint-plugin/src/rules/style-order-range/index.ts
@@ -0,0 +1,163 @@
+import {
+ AST_NODE_TYPES,
+ ESLintUtils,
+ type TSESTree,
+} from '@typescript-eslint/utils'
+import type { RuleContext } from '@typescript-eslint/utils/ts-eslint'
+
+import { ImportStorage } from '../../utils/import-storage'
+
+const createRule = ESLintUtils.RuleCreator(
+ (name) =>
+ `https://github.com/dev-five-git/devup-ui/tree/main/packages/eslint-plugin/src/rules/${name}`,
+)
+
+function checkStyleOrderRange>(
+ expression: TSESTree.Expression,
+ context: T,
+) {
+ let value: number | null = null
+
+ if (expression.type === AST_NODE_TYPES.Literal) {
+ if (typeof expression.value === 'number') {
+ value = expression.value
+ } else if (typeof expression.value === 'string') {
+ const parsed = parseInt(expression.value, 10)
+ if (!isNaN(parsed)) {
+ value = parsed
+ }
+ }
+ } else if (expression.type === AST_NODE_TYPES.UnaryExpression) {
+ if (
+ expression.argument.type === AST_NODE_TYPES.Literal &&
+ typeof expression.argument.value === 'number' &&
+ (expression.operator === '-' || expression.operator === '+')
+ ) {
+ value =
+ expression.operator === '-'
+ ? -expression.argument.value
+ : expression.argument.value
+ } else {
+ context.report({
+ node: expression,
+ messageId: 'styleOrderRange',
+ })
+ return
+ }
+ } else if (expression.type === AST_NODE_TYPES.TemplateLiteral) {
+ if (expression.expressions.length > 0) {
+ // error report
+ context.report({
+ node: expression,
+ messageId: 'styleOrderRange',
+ })
+ return
+ } else {
+ value = parseInt(expression.quasis[0].value.raw, 10)
+ if (isNaN(value)) {
+ // error report
+ context.report({
+ node: expression,
+ messageId: 'styleOrderRange',
+ })
+ return
+ }
+ }
+ }
+
+ if (value === null || value < 1 || value > 254) {
+ context.report({
+ node: expression,
+ messageId: 'styleOrderRange',
+ })
+ }
+}
+
+export const styleOrderRange = createRule({
+ name: 'style-order-range',
+ defaultOptions: [],
+ meta: {
+ schema: [],
+ messages: {
+ styleOrderRange:
+ 'styleOrder prop must be a number greater than 0 and less than 255.',
+ },
+ type: 'problem',
+ docs: {
+ description:
+ 'Ensures styleOrder prop is within valid range (0 < value < 255).',
+ },
+ },
+ create(context) {
+ const importStorage = new ImportStorage()
+ let devupContext:
+ | TSESTree.CallExpression
+ | TSESTree.JSXOpeningElement
+ | null = null
+
+ return {
+ ImportDeclaration(node) {
+ importStorage.addImportByDeclaration(node)
+ },
+ CallExpression(node) {
+ if (
+ importStorage.checkContextType(node) === 'UTIL' &&
+ node.arguments.length === 1 &&
+ node.arguments[0].type === AST_NODE_TYPES.ObjectExpression
+ ) {
+ devupContext = node
+ }
+ },
+ 'CallExpression:exit'(node) {
+ if (devupContext === node) {
+ devupContext = null
+ }
+ },
+ Property(node) {
+ if (!devupContext) return
+ if (
+ node.key.type === AST_NODE_TYPES.Identifier &&
+ node.key.name === 'styleOrder'
+ ) {
+ const value = node.value
+ if (
+ value.type !== AST_NODE_TYPES.AssignmentPattern &&
+ value.type !== AST_NODE_TYPES.TSEmptyBodyFunctionExpression
+ ) {
+ checkStyleOrderRange(value, context)
+ }
+ }
+ },
+ JSXOpeningElement(node) {
+ if (importStorage.checkContextType(node) === 'COMPONENT') {
+ devupContext = node
+ }
+ },
+ 'JSXOpeningElement:exit'(node) {
+ if (devupContext === node) {
+ devupContext = null
+ }
+ },
+ JSXAttribute(node) {
+ if (!devupContext) return
+ // styleOrder prop만 체크
+ if (
+ node.name.type !== AST_NODE_TYPES.JSXIdentifier ||
+ node.name.name !== 'styleOrder' ||
+ !node.value
+ ) {
+ return
+ }
+
+ if (node.value.type === AST_NODE_TYPES.JSXExpressionContainer) {
+ const expression = node.value.expression
+ if (expression.type !== AST_NODE_TYPES.JSXEmptyExpression) {
+ checkStyleOrderRange(expression, context)
+ }
+ } else if (node.value.type === AST_NODE_TYPES.Literal) {
+ checkStyleOrderRange(node.value, context)
+ }
+ },
+ }
+ },
+})