byuckchon-frontend-cli 1.9.5 → 1.9.7
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 +205 -270
- package/package.json +1 -1
- package/src/commands/adopt.js +37 -2
- package/src/constants/versions.js +1 -0
- package/src/generators/createApp.js +18 -11
- package/src/generators/createBaseFiles.js +6 -4
- package/src/generators/createFolders.js +5 -5
- package/src/generators/createMonorepo.js +4 -2
- package/src/generators/createPackageJson.js +9 -2
- package/src/generators/createProject.js +22 -19
- package/src/generators/createReadme.js +4 -4
- package/src/generators/install.js +1 -0
- package/src/generators/scaffoldReviewAutomation.js +105 -0
- package/templates/review-automation/github/workflows/eslint-convention-review.monorepo.yml +86 -0
- package/templates/review-automation/github/workflows/eslint-convention-review.single.yml +44 -0
- package/templates/review-automation/tools/eslint-rules/internal-blocking-conventions.js +1242 -0
- package/templates/review-automation/tools/eslint-rules/internal-plugin.cjs +8 -0
- package/templates/review-automation/tools/eslint-rules/internal-rdjson-formatter.js +60 -0
- package/templates/review-automation/tools/eslint-rules/internal-warning-conventions.js +57 -0
- package/templates/review-automation/tools/eslint-rules/package.json +3 -0
- package/templates/review-automation/tools/eslint-rules/review.config.mjs +22 -0
- package/templates/review-automation/tools/post-eslint-review-comments.cjs +309 -0
package/src/commands/adopt.js
CHANGED
|
@@ -11,13 +11,15 @@ import {
|
|
|
11
11
|
apiRootForFramework,
|
|
12
12
|
scaffoldApiConventionDoc,
|
|
13
13
|
} from '../generators/apiConventionDoc.js';
|
|
14
|
+
import { scaffoldReviewAutomation } from '../generators/scaffoldReviewAutomation.js';
|
|
15
|
+
import { detectMonorepoRoot } from '../context/monorepo.js';
|
|
14
16
|
import { ensureRequiredDependencies } from '../utils/ensureRequiredDependencies.js';
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* `bc adopt`
|
|
18
20
|
*
|
|
19
|
-
* 기존 프로젝트(현재 디렉터리)에 bc.config.json
|
|
20
|
-
* 기존 소스
|
|
21
|
+
* 기존 프로젝트(현재 디렉터리)에 bc.config.json, 필수 의존성, PR 리뷰 자동화를 추가한다.
|
|
22
|
+
* 기존 소스 코드와 이미 존재하는 tools/workflow는 건드리지 않는다.
|
|
21
23
|
*
|
|
22
24
|
* 흐름:
|
|
23
25
|
* 1) 자동 감지(framework, styling, language, routing 등) 결과를 보여주고
|
|
@@ -176,6 +178,39 @@ export async function adoptCommand(opts = {}) {
|
|
|
176
178
|
} catch {
|
|
177
179
|
/* 문서 스캐폴드 실패는 치명적이지 않음 */
|
|
178
180
|
}
|
|
181
|
+
|
|
182
|
+
// PR 리뷰 자동화는 모노레포 루트에 한 번만 둔다. 기존 tools/workflow는 덮어쓰지 않는다.
|
|
183
|
+
try {
|
|
184
|
+
const monorepoRoot = await detectMonorepoRoot(cwd);
|
|
185
|
+
const reviewProjectRoot = monorepoRoot ?? cwd;
|
|
186
|
+
const projectType = monorepoRoot ? 'monorepo' : 'single';
|
|
187
|
+
const reviewAutomation = await scaffoldReviewAutomation({
|
|
188
|
+
projectRoot: reviewProjectRoot,
|
|
189
|
+
projectType,
|
|
190
|
+
overwrite: false,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
if (reviewAutomation.workflows.length) {
|
|
194
|
+
console.log(
|
|
195
|
+
chalk.green(
|
|
196
|
+
` ✓ ESLint Convention Review 설정 추가: ${path.join(reviewProjectRoot, '.github', 'workflows')}`,
|
|
197
|
+
),
|
|
198
|
+
);
|
|
199
|
+
} else {
|
|
200
|
+
console.log(
|
|
201
|
+
chalk.dim(
|
|
202
|
+
` ESLint Convention Review 설정 유지: ${path.join(reviewProjectRoot, '.github', 'workflows')} (이미 존재)`,
|
|
203
|
+
),
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.log(
|
|
208
|
+
chalk.yellow(
|
|
209
|
+
` ⚠ ESLint Convention Review 설정을 추가하지 못했습니다: ${error.message}`,
|
|
210
|
+
),
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
179
214
|
console.log();
|
|
180
215
|
console.log(chalk.dim(' 다음:'));
|
|
181
216
|
if (!process.env.ANTHROPIC_API_KEY) {
|
|
@@ -27,7 +27,10 @@ export async function createApp({ appDir, config, scope }) {
|
|
|
27
27
|
|
|
28
28
|
await createFolders(appDir, config);
|
|
29
29
|
await createBaseFiles(appDir, config);
|
|
30
|
-
await scaffoldApiConventionDoc({
|
|
30
|
+
await scaffoldApiConventionDoc({
|
|
31
|
+
projectRoot: appDir,
|
|
32
|
+
framework: config.framework,
|
|
33
|
+
});
|
|
31
34
|
await createBcConfig(appDir, config);
|
|
32
35
|
|
|
33
36
|
await createAppPackageJson(appDir, config, scope);
|
|
@@ -43,8 +46,8 @@ async function rm(target) {
|
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
/**
|
|
46
|
-
* 워크스페이스 앱용 package.json.
|
|
47
|
-
*
|
|
49
|
+
* 워크스페이스 앱용 package.json.
|
|
50
|
+
* 단일 프로젝트와 동일하게 Vite React는 React 18, Next는 React 19를 사용한다.
|
|
48
51
|
*/
|
|
49
52
|
async function createAppPackageJson(appDir, config, scope) {
|
|
50
53
|
const isReact = config.framework === 'react';
|
|
@@ -77,13 +80,14 @@ async function createAppPackageJson(appDir, config, scope) {
|
|
|
77
80
|
type: 'module',
|
|
78
81
|
scripts,
|
|
79
82
|
dependencies: {
|
|
80
|
-
react: versions['next-react'],
|
|
81
|
-
'react-dom': versions['next-react-dom'],
|
|
83
|
+
react: isReact ? versions.react : versions['next-react'],
|
|
84
|
+
'react-dom': isReact ? versions['react-dom'] : versions['next-react-dom'],
|
|
82
85
|
...(isReact ? {} : { next: versions.next }),
|
|
83
86
|
zustand: versions.zustand,
|
|
84
87
|
...(isReact
|
|
85
88
|
? {
|
|
86
89
|
axios: versions.axios,
|
|
90
|
+
'react-router-dom': versions['react-router-dom'],
|
|
87
91
|
'@tanstack/react-query': versions['@tanstack/react-query'],
|
|
88
92
|
}
|
|
89
93
|
: {}),
|
|
@@ -118,7 +122,10 @@ async function createAppPackageJson(appDir, config, scope) {
|
|
|
118
122
|
},
|
|
119
123
|
};
|
|
120
124
|
|
|
121
|
-
await write(
|
|
125
|
+
await write(
|
|
126
|
+
path.join(appDir, 'package.json'),
|
|
127
|
+
JSON.stringify(pkg, null, 2) + '\n'
|
|
128
|
+
);
|
|
122
129
|
}
|
|
123
130
|
|
|
124
131
|
/**
|
|
@@ -154,7 +161,7 @@ export default [
|
|
|
154
161
|
},
|
|
155
162
|
},
|
|
156
163
|
];
|
|
157
|
-
|
|
164
|
+
`
|
|
158
165
|
);
|
|
159
166
|
|
|
160
167
|
if (isReact) {
|
|
@@ -182,8 +189,8 @@ export default [
|
|
|
182
189
|
include: ['src', 'vite.config.ts'],
|
|
183
190
|
},
|
|
184
191
|
null,
|
|
185
|
-
2
|
|
186
|
-
) + '\n'
|
|
192
|
+
2
|
|
193
|
+
) + '\n'
|
|
187
194
|
);
|
|
188
195
|
} else {
|
|
189
196
|
await write(
|
|
@@ -204,8 +211,8 @@ export default [
|
|
|
204
211
|
exclude: ['node_modules'],
|
|
205
212
|
},
|
|
206
213
|
null,
|
|
207
|
-
2
|
|
208
|
-
) + '\n'
|
|
214
|
+
2
|
|
215
|
+
) + '\n'
|
|
209
216
|
);
|
|
210
217
|
}
|
|
211
218
|
}
|
|
@@ -424,8 +424,9 @@ export default defineConfig({
|
|
|
424
424
|
// src/App.css
|
|
425
425
|
await write(
|
|
426
426
|
path.join(rootDir, "src/App.css"),
|
|
427
|
-
`@import
|
|
428
|
-
@import
|
|
427
|
+
`@import 'tailwindcss';
|
|
428
|
+
@import "@byuckchon-frontend/settings/motion";
|
|
429
|
+
@import "./tokens.css";
|
|
429
430
|
`
|
|
430
431
|
);
|
|
431
432
|
await write(path.join(rootDir, "src/tokens.css"), "");
|
|
@@ -593,8 +594,9 @@ export default config;
|
|
|
593
594
|
// src/app/globals.css
|
|
594
595
|
await write(
|
|
595
596
|
path.join(rootDir, "src/app/globals.css"),
|
|
596
|
-
`@import
|
|
597
|
-
@import
|
|
597
|
+
`@import 'tailwindcss';
|
|
598
|
+
@import "@byuckchon-frontend/settings/motion";
|
|
599
|
+
@import "../tokens.css";
|
|
598
600
|
`
|
|
599
601
|
);
|
|
600
602
|
await write(path.join(rootDir, "token.config.js"), TOKEN_CONFIG_JS);
|
|
@@ -3,7 +3,7 @@ import path from 'path';
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* React 폴더 구조
|
|
6
|
-
* lib → store → api → hooks → context → components →
|
|
6
|
+
* lib → store → api → hooks → context → components → layouts → pages
|
|
7
7
|
* assets는 모든 레이어에서 참조 가능
|
|
8
8
|
*/
|
|
9
9
|
const REACT_FOLDERS = [
|
|
@@ -17,8 +17,8 @@ const REACT_FOLDERS = [
|
|
|
17
17
|
'src/hooks',
|
|
18
18
|
'src/context',
|
|
19
19
|
'src/components',
|
|
20
|
-
'src/
|
|
21
|
-
'src/
|
|
20
|
+
'src/layouts',
|
|
21
|
+
'src/pages',
|
|
22
22
|
];
|
|
23
23
|
|
|
24
24
|
/**
|
|
@@ -58,8 +58,8 @@ export async function createFolders(rootDir, config) {
|
|
|
58
58
|
await writeFile(path.join(rootDir, 'src/store/index.ts'), '// Zustand 스토어\n');
|
|
59
59
|
await writeFile(path.join(rootDir, 'src/api/index.ts'), '// Axios API 호출\n');
|
|
60
60
|
await writeFile(path.join(rootDir, 'src/context/index.tsx'), '// React Context\n');
|
|
61
|
-
await writeFile(path.join(rootDir, 'src/
|
|
62
|
-
await writeFile(path.join(rootDir, 'src/
|
|
61
|
+
await writeFile(path.join(rootDir, 'src/layouts/index.tsx'), '// 레이아웃 컴포넌트\n');
|
|
62
|
+
await writeFile(path.join(rootDir, 'src/pages/index.tsx'), '// 페이지 컴포넌트\n');
|
|
63
63
|
} else {
|
|
64
64
|
await writeFile(path.join(rootDir, 'src/constant/index.ts'), '// 상수 정의\n');
|
|
65
65
|
await writeFile(path.join(rootDir, 'src/providers/index.tsx'), '// 전역 Provider\n');
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
4
|
import { createApp } from './createApp.js';
|
|
5
|
+
import { scaffoldReviewAutomation } from './scaffoldReviewAutomation.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* 새 pnpm 모노레포를 스캐폴드한다. (marketd-frontend 구조 참고)
|
|
@@ -32,6 +33,7 @@ export async function createMonorepo(config) {
|
|
|
32
33
|
await writeRootFiles(root, config, scope);
|
|
33
34
|
await writeConfigTypescript(root, scope);
|
|
34
35
|
await writeConfigEslint(root, scope);
|
|
36
|
+
await scaffoldReviewAutomation({ projectRoot: root, projectType: 'monorepo' });
|
|
35
37
|
|
|
36
38
|
// 최초 앱 하나만 생성. 이후 추가는 `bc add`.
|
|
37
39
|
const appName = config.appName;
|
|
@@ -71,9 +73,9 @@ async function writeRootFiles(root, config, scope) {
|
|
|
71
73
|
typecheck: 'turbo run typecheck',
|
|
72
74
|
'tokens:build': 'turbo run tokens:build',
|
|
73
75
|
format:
|
|
74
|
-
'prettier --write "**/*.{ts,tsx,js,jsx,json,md,yml,yaml}" --ignore-path .gitignore',
|
|
76
|
+
'prettier --write "**/*.{ts,tsx,js,jsx,cjs,json,md,yml,yaml}" --ignore-path .gitignore',
|
|
75
77
|
'format:check':
|
|
76
|
-
'prettier --check "**/*.{ts,tsx,js,jsx,json,md,yml,yaml}" --ignore-path .gitignore',
|
|
78
|
+
'prettier --check "**/*.{ts,tsx,js,jsx,cjs,json,md,yml,yaml}" --ignore-path .gitignore',
|
|
77
79
|
clean: 'turbo run clean && rm -rf node_modules .turbo',
|
|
78
80
|
preinstall: 'npx only-allow pnpm',
|
|
79
81
|
// 최초 앱 실행 단축키 (bc add 시 앱마다 추가됨).
|
|
@@ -18,7 +18,10 @@ export async function createPackageJson(rootDir, config) {
|
|
|
18
18
|
'tokens:build': 'style-dictionary build --config token.config.js',
|
|
19
19
|
lint: 'eslint . --ext ts,tsx',
|
|
20
20
|
preview: 'vite preview',
|
|
21
|
-
format:
|
|
21
|
+
format:
|
|
22
|
+
'prettier --write "src/**/*.{ts,tsx,css}" "tools/**/*.{js,cjs,json,md}"',
|
|
23
|
+
'format:check':
|
|
24
|
+
'prettier --check "src/**/*.{ts,tsx,css}" "tools/**/*.{js,cjs,json,md}"',
|
|
22
25
|
}
|
|
23
26
|
: {
|
|
24
27
|
dev: 'next dev',
|
|
@@ -26,7 +29,10 @@ export async function createPackageJson(rootDir, config) {
|
|
|
26
29
|
'tokens:build': 'style-dictionary build --config token.config.js',
|
|
27
30
|
start: 'next start',
|
|
28
31
|
lint: 'next lint',
|
|
29
|
-
format:
|
|
32
|
+
format:
|
|
33
|
+
'prettier --write "src/**/*.{ts,tsx,css}" "tools/**/*.{js,cjs,json,md}"',
|
|
34
|
+
'format:check':
|
|
35
|
+
'prettier --check "src/**/*.{ts,tsx,css}" "tools/**/*.{js,cjs,json,md}"',
|
|
30
36
|
},
|
|
31
37
|
dependencies: {
|
|
32
38
|
react: isReact ? versions.react : versions['next-react'],
|
|
@@ -37,6 +43,7 @@ export async function createPackageJson(rootDir, config) {
|
|
|
37
43
|
...(isReact
|
|
38
44
|
? {
|
|
39
45
|
axios: versions.axios,
|
|
46
|
+
'react-router-dom': versions['react-router-dom'],
|
|
40
47
|
}
|
|
41
48
|
: {}),
|
|
42
49
|
zod: versions.zod,
|
|
@@ -1,22 +1,18 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import path from
|
|
3
|
-
import { exec as execCallback } from
|
|
4
|
-
import { promisify } from
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { exec as execCallback } from "child_process";
|
|
4
|
+
import { promisify } from "util";
|
|
5
5
|
|
|
6
|
-
import { createBaseFiles } from
|
|
7
|
-
import { createBcConfig } from
|
|
8
|
-
import { createFolders } from
|
|
9
|
-
import { createPackageJson } from
|
|
10
|
-
import { createReadme } from
|
|
11
|
-
import { scaffoldApiConventionDoc } from
|
|
6
|
+
import { createBaseFiles } from "./createBaseFiles.js";
|
|
7
|
+
import { createBcConfig } from "./createBcConfig.js";
|
|
8
|
+
import { createFolders } from "./createFolders.js";
|
|
9
|
+
import { createPackageJson } from "./createPackageJson.js";
|
|
10
|
+
import { createReadme } from "./createReadme.js";
|
|
11
|
+
import { scaffoldApiConventionDoc } from "./apiConventionDoc.js";
|
|
12
|
+
import { scaffoldReviewAutomation } from "./scaffoldReviewAutomation.js";
|
|
13
|
+
import { BYUCKCHON_PACKAGES } from "./install.js";
|
|
12
14
|
|
|
13
15
|
const exec = promisify(execCallback);
|
|
14
|
-
const BYUCKCHON_PACKAGES = [
|
|
15
|
-
'@byuckchon-frontend/hooks',
|
|
16
|
-
'@byuckchon-frontend/utils',
|
|
17
|
-
'@byuckchon-frontend/basic-ui',
|
|
18
|
-
'@byuckchon-frontend/core',
|
|
19
|
-
];
|
|
20
16
|
|
|
21
17
|
export async function createProject(config) {
|
|
22
18
|
const rootDir = path.resolve(config.projectName);
|
|
@@ -27,10 +23,17 @@ export async function createProject(config) {
|
|
|
27
23
|
await createBaseFiles(rootDir, config);
|
|
28
24
|
await createReadme(rootDir, config);
|
|
29
25
|
// API 코드 컨벤션 .md 를 프레임워크에 맞는 API 루트(src/api | src/lib/api)에 깐다.
|
|
30
|
-
await scaffoldApiConventionDoc({
|
|
26
|
+
await scaffoldApiConventionDoc({
|
|
27
|
+
projectRoot: rootDir,
|
|
28
|
+
framework: config.framework,
|
|
29
|
+
});
|
|
31
30
|
await createBcConfig(rootDir, config);
|
|
31
|
+
await scaffoldReviewAutomation({
|
|
32
|
+
projectRoot: rootDir,
|
|
33
|
+
projectType: "single",
|
|
34
|
+
});
|
|
32
35
|
|
|
33
36
|
// 최신 버전(latest 포함) 의존성을 실제로 설치해 lockfile까지 생성
|
|
34
|
-
await exec(
|
|
35
|
-
await exec(`npm install ${BYUCKCHON_PACKAGES.join(
|
|
37
|
+
await exec("npm install", { cwd: rootDir });
|
|
38
|
+
await exec(`npm install ${BYUCKCHON_PACKAGES.join(" ")}`, { cwd: rootDir });
|
|
36
39
|
}
|
|
@@ -10,7 +10,7 @@ export async function createReadme(rootDir, config) {
|
|
|
10
10
|
|
|
11
11
|
| 항목 | 내용 |
|
|
12
12
|
|------|------|
|
|
13
|
-
| Framework | ${isReact ? 'React
|
|
13
|
+
| Framework | ${isReact ? 'React 18' : 'Next.js 15 (App Router)'} |
|
|
14
14
|
| Language | TypeScript |
|
|
15
15
|
| Build Tool | ${isReact ? 'Vite' : 'Next.js built-in'} |
|
|
16
16
|
| Styling | Tailwind CSS |
|
|
@@ -47,8 +47,8 @@ src/
|
|
|
47
47
|
├── components/ 재사용 가능한 UI 컴포넌트${
|
|
48
48
|
isReact
|
|
49
49
|
? `
|
|
50
|
-
├──
|
|
51
|
-
└──
|
|
50
|
+
├── layouts/ 레이아웃 컴포넌트
|
|
51
|
+
└── pages/ 페이지 컴포넌트`
|
|
52
52
|
: `
|
|
53
53
|
└── app/ Next.js App Router (layout, page 등)`
|
|
54
54
|
}
|
|
@@ -61,7 +61,7 @@ ${
|
|
|
61
61
|
각 레이어는 아래 방향으로만 import 해야 합니다.
|
|
62
62
|
|
|
63
63
|
\`\`\`
|
|
64
|
-
lib → store → api → hooks → context → components →
|
|
64
|
+
lib → store → api → hooks → context → components → layouts → pages
|
|
65
65
|
\`\`\`
|
|
66
66
|
|
|
67
67
|
\`assets\`는 모든 레이어에서 자유롭게 참조 가능합니다.`
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const TEMPLATE_ROOT = path.resolve(
|
|
6
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
7
|
+
'../../templates/review-automation',
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
const ESLINT_WORKFLOW_BY_PROJECT_TYPE = {
|
|
11
|
+
single: 'eslint-convention-review.single.yml',
|
|
12
|
+
monorepo: 'eslint-convention-review.monorepo.yml',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function shouldCopyTemplateFile(source) {
|
|
16
|
+
return path.basename(source) !== '.DS_Store';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function pathExists(target) {
|
|
20
|
+
try {
|
|
21
|
+
await fs.access(target);
|
|
22
|
+
return true;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function copyFileIfAllowed(source, target, overwrite) {
|
|
29
|
+
if (!overwrite && (await pathExists(target))) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
await fs.copyFile(source, target);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 프로젝트 루트에 PR 리뷰 자동화 파일을 생성한다.
|
|
39
|
+
*
|
|
40
|
+
* - tools/ 는 ESLint convention rule 및 PR 댓글 게시 스크립트를 제공한다.
|
|
41
|
+
* - .github/workflows/ 는 프로젝트 유형에 맞는 ESLint workflow 하나와
|
|
42
|
+
* 공통 workflow(AI Code Review, PR Check 등)를 제공한다.
|
|
43
|
+
*
|
|
44
|
+
* @param {object} args
|
|
45
|
+
* @param {string} args.projectRoot 새로 생성한 프로젝트의 절대 경로
|
|
46
|
+
* @param {'single' | 'monorepo'} args.projectType 생성할 프로젝트 유형
|
|
47
|
+
* @param {boolean} [args.overwrite=true] 기존 파일을 템플릿으로 덮어쓸지
|
|
48
|
+
* @returns {Promise<{ toolsDir: string, workflows: string[] }>}
|
|
49
|
+
*/
|
|
50
|
+
export async function scaffoldReviewAutomation({
|
|
51
|
+
projectRoot,
|
|
52
|
+
projectType,
|
|
53
|
+
overwrite = true,
|
|
54
|
+
}) {
|
|
55
|
+
const eslintWorkflow = ESLINT_WORKFLOW_BY_PROJECT_TYPE[projectType];
|
|
56
|
+
|
|
57
|
+
if (!eslintWorkflow) {
|
|
58
|
+
throw new Error(`지원하지 않는 프로젝트 유형입니다: ${projectType}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const toolsTemplateDir = path.join(TEMPLATE_ROOT, 'tools');
|
|
62
|
+
const workflowsTemplateDir = path.join(TEMPLATE_ROOT, 'github', 'workflows');
|
|
63
|
+
const toolsTargetDir = path.join(projectRoot, 'tools');
|
|
64
|
+
const workflowsTargetDir = path.join(projectRoot, '.github', 'workflows');
|
|
65
|
+
|
|
66
|
+
await fs.cp(toolsTemplateDir, toolsTargetDir, {
|
|
67
|
+
recursive: true,
|
|
68
|
+
force: overwrite,
|
|
69
|
+
errorOnExist: false,
|
|
70
|
+
filter: shouldCopyTemplateFile,
|
|
71
|
+
});
|
|
72
|
+
await fs.mkdir(workflowsTargetDir, { recursive: true });
|
|
73
|
+
|
|
74
|
+
const workflowFiles = await fs.readdir(workflowsTemplateDir);
|
|
75
|
+
const commonWorkflows = workflowFiles.filter(
|
|
76
|
+
(fileName) =>
|
|
77
|
+
fileName.endsWith('.yml') &&
|
|
78
|
+
!fileName.startsWith('eslint-convention-review.'),
|
|
79
|
+
);
|
|
80
|
+
const copiedWorkflows = [];
|
|
81
|
+
|
|
82
|
+
for (const fileName of commonWorkflows) {
|
|
83
|
+
const copied = await copyFileIfAllowed(
|
|
84
|
+
path.join(workflowsTemplateDir, fileName),
|
|
85
|
+
path.join(workflowsTargetDir, fileName),
|
|
86
|
+
overwrite,
|
|
87
|
+
);
|
|
88
|
+
if (copied) copiedWorkflows.push(fileName);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const eslintWorkflowTarget = path.join(
|
|
92
|
+
workflowsTargetDir,
|
|
93
|
+
'eslint-convention-review.yml',
|
|
94
|
+
);
|
|
95
|
+
const copiedEslintWorkflow = await copyFileIfAllowed(
|
|
96
|
+
path.join(workflowsTemplateDir, eslintWorkflow),
|
|
97
|
+
eslintWorkflowTarget,
|
|
98
|
+
overwrite,
|
|
99
|
+
);
|
|
100
|
+
if (copiedEslintWorkflow) {
|
|
101
|
+
copiedWorkflows.push('eslint-convention-review.yml');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { toolsDir: toolsTargetDir, workflows: copiedWorkflows };
|
|
105
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# ESLint 기반 기계적 컨벤션을 PR 인라인 댓글로 게시합니다.
|
|
2
|
+
# typecheck/lint/build 성공 여부는 PR Check workflow가 담당합니다.
|
|
3
|
+
#
|
|
4
|
+
# 각 패키지의 eslint.config.mjs에는 internal 플러그인이 없으므로,
|
|
5
|
+
# tools/eslint-rules/review.config.mjs 전용 config로 레포 루트에서
|
|
6
|
+
# PR 변경 파일만 한 번에 검사합니다.
|
|
7
|
+
|
|
8
|
+
name: ESLint Convention Review
|
|
9
|
+
|
|
10
|
+
on:
|
|
11
|
+
pull_request:
|
|
12
|
+
types: [opened, synchronize, reopened]
|
|
13
|
+
branches:
|
|
14
|
+
- dev
|
|
15
|
+
|
|
16
|
+
concurrency:
|
|
17
|
+
group: eslint-convention-review-${{ github.event.pull_request.number }}
|
|
18
|
+
cancel-in-progress: true
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
eslint-conventions:
|
|
22
|
+
name: ESLint Convention Review
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
permissions:
|
|
25
|
+
contents: read
|
|
26
|
+
pull-requests: write
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v4
|
|
29
|
+
|
|
30
|
+
- uses: pnpm/action-setup@v4
|
|
31
|
+
|
|
32
|
+
- uses: actions/setup-node@v4
|
|
33
|
+
with:
|
|
34
|
+
node-version-file: .nvmrc
|
|
35
|
+
cache: pnpm
|
|
36
|
+
|
|
37
|
+
- name: 의존성 설치
|
|
38
|
+
run: pnpm install --frozen-lockfile
|
|
39
|
+
|
|
40
|
+
- name: PR 변경 파일 조회
|
|
41
|
+
env:
|
|
42
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
43
|
+
run: |
|
|
44
|
+
gh api "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" \
|
|
45
|
+
--paginate \
|
|
46
|
+
--jq '.[] | select(.status != "removed") | .filename | select(test("\\.(ts|tsx)$"))' \
|
|
47
|
+
> /tmp/eslint-convention-files.txt
|
|
48
|
+
|
|
49
|
+
echo "변경된 TypeScript 파일:"
|
|
50
|
+
cat /tmp/eslint-convention-files.txt
|
|
51
|
+
|
|
52
|
+
- name: ESLint 컨벤션 검사
|
|
53
|
+
run: |
|
|
54
|
+
if [[ ! -s /tmp/eslint-convention-files.txt ]]; then
|
|
55
|
+
echo "변경된 TypeScript 파일이 없습니다."
|
|
56
|
+
printf '{"source":{"name":"eslint-conventions"},"diagnostics":[]}' \
|
|
57
|
+
> /tmp/eslint-conventions.rdjson
|
|
58
|
+
exit 0
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
mapfile -t changed_files < /tmp/eslint-convention-files.txt
|
|
62
|
+
|
|
63
|
+
set +e
|
|
64
|
+
pnpm exec eslint "${changed_files[@]}" \
|
|
65
|
+
--config tools/eslint-rules/review.config.mjs \
|
|
66
|
+
--no-error-on-unmatched-pattern \
|
|
67
|
+
-f tools/eslint-rules/internal-rdjson-formatter.js \
|
|
68
|
+
-o /tmp/eslint-conventions.rdjson
|
|
69
|
+
eslint_exit=$?
|
|
70
|
+
set -e
|
|
71
|
+
|
|
72
|
+
if [[ ! -f /tmp/eslint-conventions.rdjson ]]; then
|
|
73
|
+
echo "ESLint가 /tmp/eslint-conventions.rdjson 을 생성하지 못했습니다. (exit ${eslint_exit})"
|
|
74
|
+
exit "$eslint_exit"
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
- name: 리뷰 댓글 게시
|
|
78
|
+
env:
|
|
79
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
80
|
+
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
81
|
+
REPO: ${{ github.repository }}
|
|
82
|
+
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
83
|
+
run: |
|
|
84
|
+
node tools/post-eslint-review-comments.cjs \
|
|
85
|
+
/tmp/eslint-conventions.rdjson \
|
|
86
|
+
eslint-conventions
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
name: ESLint Convention Review
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types: [opened, synchronize, reopened]
|
|
6
|
+
branches:
|
|
7
|
+
- dev
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: eslint-convention-review-${{ github.event.pull_request.number }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
eslint-conventions:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
permissions:
|
|
17
|
+
contents: read
|
|
18
|
+
pull-requests: write
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
|
|
22
|
+
- uses: actions/setup-node@v4
|
|
23
|
+
with:
|
|
24
|
+
node-version: 22
|
|
25
|
+
cache: npm
|
|
26
|
+
|
|
27
|
+
- name: 의존성 설치
|
|
28
|
+
run: npm ci
|
|
29
|
+
|
|
30
|
+
- name: ESLint 컨벤션 리뷰 댓글
|
|
31
|
+
env:
|
|
32
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
33
|
+
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
34
|
+
REPO: ${{ github.repository }}
|
|
35
|
+
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
36
|
+
run: |
|
|
37
|
+
npx eslint . --ext ts,tsx --rulesdir tools/eslint-rules \
|
|
38
|
+
--rule 'internal-blocking-conventions:error' \
|
|
39
|
+
--rule 'internal-warning-conventions:warn' \
|
|
40
|
+
-f tools/eslint-rules/internal-rdjson-formatter.js \
|
|
41
|
+
-o /tmp/eslint-conventions.rdjson || true
|
|
42
|
+
node tools/post-eslint-review-comments.cjs \
|
|
43
|
+
/tmp/eslint-conventions.rdjson \
|
|
44
|
+
eslint-conventions
|