@3o3/eslint-config 0.2.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,6 +22,8 @@ eslint-plugin-react, react-hooks, import, @emotion, jsdoc, tsdoc, prettier)은
22
22
  | `all` | `base + react + prettier` 번들 | "전부 켜기" |
23
23
  | `prettier` | `eslint-config-prettier`(포매팅 충돌 규칙 off) | 항상 **마지막**에 스프레드 |
24
24
 
25
+ `base` 는 자체 규칙 `@3o3/no-app-event-on-window`(error)를 켠다. 앱 이벤트(`flutter_*`)를 `window.addEventListener`·`removeEventListener` 에 문자열 리터럴로 직접 넘기는 코드를 막는다. `onBackPressed` 같은 등록형 이벤트는 직접 붙이면 앱에 등록되지 않아 동작하지 않으므로, 앱 이벤트 전체를 `@3o3/app-bridge` 의 `webView` 로 모은다. `eslint --fix` 가 `webView.addEventListener`·`removeEventListener` 로 바꾸고 import 를 채운다. `webView` 라는 이름을 이미 다른 값에 쓰는 파일은 알리기만 한다.
26
+
25
27
  `react` 는 `base` 위에 얹는 누적 델타다. 각 영역의 3o3 규칙을 이미 포함한다.
26
28
  전역 무시(dist/esm/비 ts·tsx)와 프로젝트 고유 규칙은 소비하는 쪽 config 에서 직접 둔다.
27
29
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@3o3/eslint-config",
3
- "version": "0.2.0",
3
+ "version": "1.1.0",
4
4
  "description": "3o3 공유 ESLint 설정 — 사용처에 따라 레이어(base/react/docs/all)를 골라 쓴다.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -14,7 +14,8 @@
14
14
  "./package.json": "./package.json"
15
15
  },
16
16
  "files": [
17
- "src"
17
+ "src",
18
+ "!src/**/__tests__"
18
19
  ],
19
20
  "repository": {
20
21
  "type": "git",
@@ -35,6 +36,12 @@
35
36
  "globals": "^15.15.0",
36
37
  "typescript-eslint": "^8.60.0"
37
38
  },
39
+ "devDependencies": {
40
+ "eslint": "^9.39.4",
41
+ "typescript": "5.9.3",
42
+ "vitest": "4.1.9",
43
+ "@3o3/test-config": "0.1.0"
44
+ },
38
45
  "peerDependencies": {
39
46
  "eslint": ">=9",
40
47
  "typescript": ">=5"
@@ -46,5 +53,8 @@
46
53
  },
47
54
  "publishConfig": {
48
55
  "access": "public"
56
+ },
57
+ "scripts": {
58
+ "test": "vitest run"
49
59
  }
50
60
  }
package/src/base.js CHANGED
@@ -2,6 +2,7 @@ import js from '@eslint/js'
2
2
  import tseslint from 'typescript-eslint'
3
3
  import importPlugin from 'eslint-plugin-import'
4
4
  import globals from 'globals'
5
+ import plugin from './plugin.js'
5
6
 
6
7
  /**
7
8
  * base 레이어 — js + ts 공통.
@@ -33,7 +34,7 @@ const base = tseslint.config({
33
34
  globals: {
34
35
  ...globals.node,
35
36
  ...globals.browser,
36
- ...globals.jest,
37
+ ...globals.vitest,
37
38
  ...globals.es2021,
38
39
  },
39
40
  parserOptions: {
@@ -43,6 +44,7 @@ const base = tseslint.config({
43
44
  settings: {
44
45
  'import/resolver': { typescript: {} },
45
46
  },
47
+ plugins: { '@3o3': plugin },
46
48
  rules: {
47
49
  'no-implicit-coercion': 'error',
48
50
  'no-warning-comments': [
@@ -54,6 +56,8 @@ const base = tseslint.config({
54
56
  ],
55
57
  curly: ['error', 'all'],
56
58
  eqeqeq: ['error', 'always', { null: 'ignore' }],
59
+ // window 에 직접 붙인 등록형 앱 이벤트는 앱에 등록되지 않는다
60
+ '@3o3/no-app-event-on-window': 'error',
57
61
 
58
62
  // eslint-plugin-import recommended preset에서 신규 적용 — 기존 config는 import 규칙을 켜지 않았으므로 동작 보존 위해 off
59
63
  // (import/export는 TS type re-export에 대한 오탐, 실제 중복 export는 tsc typecheck가 잡음)
package/src/docs.js CHANGED
@@ -28,7 +28,6 @@ const disabledJsdocRules = {
28
28
  // ── ESLint 9에서 작동 불가: 플러그인이 직접 "이 룰 끄고 processor 쓰라"고 출력 ──
29
29
  'jsdoc/check-examples': 'off',
30
30
  'jsdoc/require-template': 'off',
31
- 'jsdoc/require-example': 'off',
32
31
  }
33
32
 
34
33
  export function docs({ files = ['**/src/**/*.{ts,tsx}'] } = {}) {
@@ -53,7 +52,16 @@ export function docs({ files = ['**/src/**/*.{ts,tsx}'] } = {}) {
53
52
  rules: {
54
53
  ...disabledJsdocRules,
55
54
  'jsdoc/informative-docs': 'off',
56
-
55
+ 'jsdoc/require-example': [
56
+ 'error',
57
+ {
58
+ enableFixer: false,
59
+ contexts: [
60
+ 'ExportNamedDeclaration > FunctionDeclaration',
61
+ 'ExportNamedDeclaration > VariableDeclaration > VariableDeclarator > ArrowFunctionExpression',
62
+ ],
63
+ },
64
+ ],
57
65
  'jsdoc/require-jsdoc': [
58
66
  'error',
59
67
  {
package/src/index.js CHANGED
@@ -1,49 +1,26 @@
1
1
  import base from './base.js'
2
2
  import reactLayer from './react.js'
3
+ import testsLayer from './tests.js'
3
4
  import all from './all.js'
4
5
  import { docs } from './docs.js'
5
6
  import eslintConfigPrettier from 'eslint-config-prettier'
7
+ import plugin from './plugin.js'
6
8
 
7
- /**
8
- * `@3o3/eslint-config` — 사용처별 레이어를 골라 쓰는 공유 ESLint 설정(ESLint 9 flat).
9
- *
10
- * 두 가지 소비 방식을 모두 지원한다.
11
- *
12
- * 1) 배열 스프레드(named export):
13
- * ```js
14
- * import { base, react } from '@3o3/eslint-config'
15
- * export default [...base, ...react]
16
- * ```
17
- *
18
- * 2) ESLint 가 권장하는 `extends` 스타일(default export + `defineConfig`):
19
- * ```js
20
- * import { defineConfig } from 'eslint/config'
21
- * import threeo3 from '@3o3/eslint-config'
22
- * export default defineConfig([
23
- * { plugins: { '@3o3': threeo3 }, extends: ['@3o3/base', '@3o3/react'] },
24
- * ])
25
- * ```
26
- *
27
- * default export 는 `configs` 맵을 가진 플러그인 형태(typescript-eslint /
28
- * eslint-plugin-react 와 동일한 패턴)라 `extends: ['@3o3/<layer>']` 문자열로
29
- * 참조된다. `<layer>` 는 base / react / all.
30
- */
31
- const plugin = {
32
- meta: { name: '@3o3/eslint-config' },
33
- // `extends: ['@3o3/<layer>']` 문자열 참조용(typescript-eslint / react 패턴).
34
- configs: {
35
- base,
36
- react: reactLayer,
37
- all,
38
- },
39
- docs,
40
- prettier: eslintConfigPrettier,
41
- }
9
+ // base 와 plugin 이 서로를 참조하므로 레이어는 여기서 채운다.
10
+ // docs·prettier 도 여기서 붙여, base 만 가져오는 소비처가 문서용 플러그인까지 불러오지 않게 한다
11
+ Object.assign(plugin.configs, {
12
+ base,
13
+ react: reactLayer,
14
+ tests: testsLayer,
15
+ all,
16
+ })
17
+ Object.assign(plugin, { docs, prettier: eslintConfigPrettier })
42
18
 
43
19
  export default plugin
44
20
 
45
21
  // 배열 스프레드용 named export (기존 방식 유지)
46
22
  export { base, all }
47
23
  export { reactLayer as react }
24
+ export { testsLayer as tests }
48
25
  export { docs }
49
26
  export { eslintConfigPrettier as prettier }
package/src/plugin.js ADDED
@@ -0,0 +1,37 @@
1
+ import noAppEventOnWindow from './rules/no-app-event-on-window.js'
2
+
3
+ /**
4
+ * `@3o3/eslint-config` — 사용처별 레이어를 골라 쓰는 공유 ESLint 설정(ESLint 9 flat).
5
+ *
6
+ * 두 가지 소비 방식을 모두 지원한다.
7
+ *
8
+ * 1) 배열 스프레드(named export):
9
+ * ```js
10
+ * import { base, react } from '@3o3/eslint-config'
11
+ * export default [...base, ...react]
12
+ * ```
13
+ *
14
+ * 2) ESLint 가 권장하는 `extends` 스타일(default export + `defineConfig`):
15
+ * ```js
16
+ * import { defineConfig } from 'eslint/config'
17
+ * import threeo3 from '@3o3/eslint-config'
18
+ * export default defineConfig([
19
+ * { plugins: { '@3o3': threeo3 }, extends: ['@3o3/base', '@3o3/react'] },
20
+ * ])
21
+ * ```
22
+ *
23
+ * default export 는 `configs` 맵을 가진 플러그인 형태(typescript-eslint /
24
+ * eslint-plugin-react 와 동일한 패턴)라 `extends: ['@3o3/<layer>']` 문자열로
25
+ * 참조된다. `<layer>` 는 base / react / tests / all.
26
+ *
27
+ * base 가 이 객체를 `@3o3` 로 등록한다. 소비처가 다시 등록해도 같은 객체라 중복 정의가 아니다.
28
+ */
29
+ const plugin = {
30
+ meta: { name: '@3o3/eslint-config' },
31
+ rules: {
32
+ 'no-app-event-on-window': noAppEventOnWindow,
33
+ },
34
+ configs: {},
35
+ }
36
+
37
+ export default plugin
@@ -0,0 +1,123 @@
1
+ const APP_EVENT_PREFIX = 'flutter_'
2
+ const WINDOW_NAMES = ['window', 'globalThis', 'self']
3
+ const METHODS = ['addEventListener', 'removeEventListener']
4
+ const APP_BRIDGE = '@3o3/app-bridge'
5
+
6
+ const findVariable = (scope, name) => {
7
+ for (let current = scope; current; current = current.upper) {
8
+ const variable = current.set.get(name)
9
+ if (variable) {
10
+ return variable
11
+ }
12
+ }
13
+ return undefined
14
+ }
15
+
16
+ /**
17
+ * 앱 이벤트(flutter_*)를 window 에 직접 붙이는 코드를 막는다. 이벤트 이름이 문자열 리터럴인 호출만 잡는다.
18
+ * 등록형 이벤트(onBackPressed)는 window 에 직접 붙이면 앱에 등록되지 않는다.
19
+ */
20
+ const rule = {
21
+ meta: {
22
+ type: 'problem',
23
+ fixable: 'code',
24
+ docs: {
25
+ description:
26
+ '앱 이벤트(flutter_*)는 window 대신 @3o3/app-bridge 의 webView 로 구독한다',
27
+ },
28
+ schema: [],
29
+ messages: {
30
+ useWebView:
31
+ "앱 이벤트 '{{event}}' 는 window 에 직접 붙이지 말고 @3o3/app-bridge 의 webView.{{method}} 를 쓰세요. 등록형 이벤트는 직접 붙이면 앱에 등록되지 않습니다.",
32
+ },
33
+ },
34
+ create(context) {
35
+ const { sourceCode } = context
36
+
37
+ return {
38
+ CallExpression(node) {
39
+ const { callee } = node
40
+ const [eventName] = node.arguments
41
+ if (
42
+ callee.type !== 'MemberExpression' ||
43
+ callee.computed ||
44
+ callee.object.type !== 'Identifier' ||
45
+ !WINDOW_NAMES.includes(callee.object.name) ||
46
+ callee.property.type !== 'Identifier' ||
47
+ !METHODS.includes(callee.property.name) ||
48
+ eventName?.type !== 'Literal' ||
49
+ typeof eventName.value !== 'string' ||
50
+ !eventName.value.startsWith(APP_EVENT_PREFIX)
51
+ ) {
52
+ return
53
+ }
54
+
55
+ const program = sourceCode.ast
56
+ const appBridgeImports = program.body.filter(
57
+ (statement) =>
58
+ statement.type === 'ImportDeclaration' &&
59
+ statement.source.value === APP_BRIDGE &&
60
+ statement.importKind !== 'type'
61
+ )
62
+ const webViewSpecifier = appBridgeImports
63
+ .flatMap((declaration) => declaration.specifiers)
64
+ .find(
65
+ (specifier) =>
66
+ specifier.type === 'ImportSpecifier' &&
67
+ specifier.imported.name === 'webView' &&
68
+ specifier.importKind !== 'type'
69
+ )
70
+ const localName = webViewSpecifier?.local.name ?? 'webView'
71
+
72
+ // webView 가 다른 값을 가리키면 고치지 않는다
73
+ const variable = findVariable(sourceCode.getScope(node), localName)
74
+ const canFix = webViewSpecifier
75
+ ? variable?.defs[0]?.node === webViewSpecifier
76
+ : !variable
77
+
78
+ const importFixes = (fixer) => {
79
+ if (webViewSpecifier) {
80
+ return []
81
+ }
82
+
83
+ const namedImport = appBridgeImports.find((declaration) =>
84
+ declaration.specifiers.some(
85
+ (specifier) => specifier.type === 'ImportSpecifier'
86
+ )
87
+ )
88
+ if (namedImport) {
89
+ return [
90
+ fixer.insertTextAfter(namedImport.specifiers.at(-1), ', webView'),
91
+ ]
92
+ }
93
+
94
+ // 'use client' 같은 지시어는 맨 위에 남아야 한다
95
+ const anchor =
96
+ program.body.find(
97
+ (statement) => statement.type === 'ImportDeclaration'
98
+ ) ?? program.body.find((statement) => !statement.directive)
99
+ return [
100
+ fixer.insertTextBefore(
101
+ anchor,
102
+ `import { webView } from '${APP_BRIDGE}'\n`
103
+ ),
104
+ ]
105
+ }
106
+
107
+ context.report({
108
+ node,
109
+ messageId: 'useWebView',
110
+ data: { event: eventName.value, method: callee.property.name },
111
+ fix: canFix
112
+ ? (fixer) => [
113
+ fixer.replaceText(callee.object, localName),
114
+ ...importFixes(fixer),
115
+ ]
116
+ : null,
117
+ })
118
+ },
119
+ }
120
+ },
121
+ }
122
+
123
+ export default rule
package/src/tests.js ADDED
@@ -0,0 +1,30 @@
1
+ import tseslint from 'typescript-eslint'
2
+
3
+ /**
4
+ * tests 레이어 — 테스트 파일에만 적용하는 완화 규칙.
5
+ *
6
+ * 대상: `**\/__tests__/**\/*.{ts,tsx}` 와 `**\/*.spec.{ts,tsx}` (레포 테스트 컨벤션).
7
+ *
8
+ * 테스트는 목·스텁·DOM 노드 접근 때문에 non-null 어설션(`!`)을 자연스럽게 쓰고,
9
+ * 임시 픽스처 배열 타입도 `T[]` 로 두는 편이 읽기 쉽다. 프로덕션 코드에는
10
+ * `no-non-null-assertion`(warn)·`array-type`(array-simple) 을 유지하되, 테스트
11
+ * 파일에서는 off 한다. base/react 레이어 뒤에 스프레드해야 오버라이드가 적용된다.
12
+ *
13
+ * @example
14
+ * ```js
15
+ * import { defineConfig } from 'eslint/config'
16
+ * import threeo3 from '@3o3/eslint-config'
17
+ * export default defineConfig([
18
+ * { plugins: { '@3o3': threeo3 }, extends: ['@3o3/base', '@3o3/react', '@3o3/tests'] },
19
+ * ])
20
+ * ```
21
+ */
22
+ const tests = tseslint.config({
23
+ files: ['**/__tests__/**/*.{ts,tsx}', '**/*.spec.{ts,tsx}'],
24
+ rules: {
25
+ '@typescript-eslint/no-non-null-assertion': 'off',
26
+ '@typescript-eslint/array-type': 'off',
27
+ },
28
+ })
29
+
30
+ export default tests