byuckchon-frontend-cli 1.9.6 → 1.10.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.
@@ -0,0 +1,115 @@
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 PROJECT_TYPES = ['single', 'monorepo'];
11
+
12
+ /**
13
+ * 워크플로 템플릿 이름 규칙
14
+ * `<이름>.single.yml` / `<이름>.monorepo.yml` → 유형이 맞는 쪽만 `<이름>.yml` 로 복사
15
+ * `<이름>.yml` → 유형과 무관하게 항상 복사
16
+ *
17
+ * @param {string[]} fileNames 템플릿 디렉터리의 파일 목록
18
+ * @param {'single' | 'monorepo'} projectType
19
+ * @returns {Array<{ source: string, target: string }>}
20
+ */
21
+ function selectWorkflows(fileNames, projectType, exclude = []) {
22
+ const selected = [];
23
+
24
+ for (const fileName of fileNames) {
25
+ if (!fileName.endsWith('.yml')) continue;
26
+
27
+ const withoutExt = fileName.slice(0, -'.yml'.length);
28
+ const suffix = PROJECT_TYPES.find((type) => withoutExt.endsWith(`.${type}`));
29
+ const baseName = suffix
30
+ ? withoutExt.slice(0, -(suffix.length + 1))
31
+ : withoutExt;
32
+
33
+ if (exclude.includes(baseName)) continue;
34
+ if (suffix && suffix !== projectType) continue;
35
+
36
+ selected.push({ source: fileName, target: `${baseName}.yml` });
37
+ }
38
+
39
+ return selected;
40
+ }
41
+
42
+ function shouldCopyTemplateFile(source) {
43
+ return path.basename(source) !== '.DS_Store';
44
+ }
45
+
46
+ async function pathExists(target) {
47
+ try {
48
+ await fs.access(target);
49
+ return true;
50
+ } catch {
51
+ return false;
52
+ }
53
+ }
54
+
55
+ async function copyFileIfAllowed(source, target, overwrite) {
56
+ if (!overwrite && (await pathExists(target))) {
57
+ return false;
58
+ }
59
+
60
+ await fs.copyFile(source, target);
61
+ return true;
62
+ }
63
+
64
+ /**
65
+ * 프로젝트 루트에 PR 리뷰 자동화 파일을 생성한다.
66
+ *
67
+ * - tools/ 는 ESLint convention rule 및 PR 댓글 게시 스크립트를 제공한다.
68
+ * - .github/workflows/ 는 프로젝트 유형에 맞는 워크플로를 제공한다.
69
+ * 템플릿 이름이 `<이름>.single.yml` / `<이름>.monorepo.yml` 이면 유형이 맞는 쪽만
70
+ * `<이름>.yml` 로 복사되고, 접미사가 없으면 유형과 무관하게 복사된다.
71
+ *
72
+ * @param {object} args
73
+ * @param {string} args.projectRoot 새로 생성한 프로젝트의 절대 경로
74
+ * @param {'single' | 'monorepo'} args.projectType 생성할 프로젝트 유형
75
+ * @param {boolean} [args.overwrite=true] 기존 파일을 템플릿으로 덮어쓸지
76
+ * @param {string[]} [args.exclude=[]] 제외할 워크플로 이름 (확장자·유형 접미사 제외)
77
+ * @returns {Promise<{ toolsDir: string, workflows: string[] }>}
78
+ */
79
+ export async function scaffoldReviewAutomation({
80
+ projectRoot,
81
+ projectType,
82
+ overwrite = true,
83
+ exclude = [],
84
+ }) {
85
+ if (!PROJECT_TYPES.includes(projectType)) {
86
+ throw new Error(`지원하지 않는 프로젝트 유형입니다: ${projectType}`);
87
+ }
88
+
89
+ const toolsTemplateDir = path.join(TEMPLATE_ROOT, 'tools');
90
+ const workflowsTemplateDir = path.join(TEMPLATE_ROOT, 'github', 'workflows');
91
+ const toolsTargetDir = path.join(projectRoot, 'tools');
92
+ const workflowsTargetDir = path.join(projectRoot, '.github', 'workflows');
93
+
94
+ await fs.cp(toolsTemplateDir, toolsTargetDir, {
95
+ recursive: true,
96
+ force: overwrite,
97
+ errorOnExist: false,
98
+ filter: shouldCopyTemplateFile,
99
+ });
100
+ await fs.mkdir(workflowsTargetDir, { recursive: true });
101
+
102
+ const workflowFiles = await fs.readdir(workflowsTemplateDir);
103
+ const copiedWorkflows = [];
104
+
105
+ for (const { source, target } of selectWorkflows(workflowFiles, projectType, exclude)) {
106
+ const copied = await copyFileIfAllowed(
107
+ path.join(workflowsTemplateDir, source),
108
+ path.join(workflowsTargetDir, target),
109
+ overwrite,
110
+ );
111
+ if (copied) copiedWorkflows.push(target);
112
+ }
113
+
114
+ return { toolsDir: toolsTargetDir, workflows: copiedWorkflows };
115
+ }
@@ -7,6 +7,13 @@ const execFile = promisify(execFileCallback);
7
7
 
8
8
  const COMMON_DEPENDENCIES = ['@tanstack/react-query', 'zod'];
9
9
 
10
+ /**
11
+ * 스캐폴딩되는 설정 파일들이 참조하는 패키지.
12
+ * tools/review.config.mjs, eslint.config, prettier.config, tsconfig 가 모두
13
+ * @byuckchon-frontend/settings 를 import/extends 하므로 없으면 lint·build 가 실패한다.
14
+ */
15
+ const COMMON_DEV_DEPENDENCIES = ['@byuckchon-frontend/settings'];
16
+
10
17
  const installCommands = {
11
18
  npm: ['npm', ['install']],
12
19
  pnpm: ['pnpm', ['add']],
@@ -14,6 +21,14 @@ const installCommands = {
14
21
  bun: ['bun', ['add']],
15
22
  };
16
23
 
24
+ /** devDependencies 로 설치할 때 붙이는 플래그 */
25
+ const devFlags = {
26
+ npm: '--save-dev',
27
+ pnpm: '-D',
28
+ yarn: '-D',
29
+ bun: '-d',
30
+ };
31
+
17
32
  export function hasDependency(pkg, packageName) {
18
33
  return Boolean(
19
34
  pkg?.dependencies?.[packageName] ||
@@ -36,11 +51,11 @@ export async function ensureRequiredDependencies({
36
51
  packageManager,
37
52
  run = execFile,
38
53
  }) {
39
- const missing = requiredDependenciesForFramework(framework).filter(
40
- (packageName) => !hasDependency(pkg, packageName),
41
- );
54
+ const notInstalled = (packageName) => !hasDependency(pkg, packageName);
55
+ const missing = requiredDependenciesForFramework(framework).filter(notInstalled);
56
+ const missingDev = COMMON_DEV_DEPENDENCIES.filter(notInstalled);
42
57
 
43
- if (!missing.length) {
58
+ if (!missing.length && !missingDev.length) {
44
59
  return { installed: [], packageManager };
45
60
  }
46
61
 
@@ -48,11 +63,22 @@ export async function ensureRequiredDependencies({
48
63
  ? packageManager
49
64
  : 'npm';
50
65
  const [command, baseArgs] = installCommands[selectedPackageManager];
51
- const packageSpecs = missing.map(
52
- (packageName) => `${packageName}@${versions[packageName]}`,
53
- );
66
+ const spec = (packageName) =>
67
+ versions[packageName] ? `${packageName}@${versions[packageName]}` : packageName;
54
68
 
55
- await run(command, [...baseArgs, ...packageSpecs], { cwd });
69
+ if (missing.length) {
70
+ await run(command, [...baseArgs, ...missing.map(spec)], { cwd });
71
+ }
72
+ if (missingDev.length) {
73
+ await run(
74
+ command,
75
+ [...baseArgs, devFlags[selectedPackageManager], ...missingDev.map(spec)],
76
+ { cwd },
77
+ );
78
+ }
56
79
 
57
- return { installed: missing, packageManager: selectedPackageManager };
80
+ return {
81
+ installed: [...missing, ...missingDev],
82
+ packageManager: selectedPackageManager,
83
+ };
58
84
  }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @byuckchon-frontend/settings 가 관리하는 "파일로 존재해야 하는" 설정을 읽는다.
3
+ *
4
+ * eslint / prettier / tsconfig 는 프로젝트가 import·extends 로 참조하므로 여기 없다.
5
+ * 아래 파일들은 참조 문법이 없어서 생성 시점에 내용을 복사해야 한다.
6
+ * 이후 settings 가 바뀌면 프로젝트에서 `npx byuckchon-settings-sync` 로 갱신한다.
7
+ */
8
+
9
+ import fs from 'node:fs/promises';
10
+ import { createRequire } from 'node:module';
11
+
12
+ const require = createRequire(import.meta.url);
13
+
14
+ /** settings 패키지 안의 파일 경로를 해석한다. */
15
+ export function resolveSettingsAsset(subpath) {
16
+ return require.resolve(`@byuckchon-frontend/settings/${subpath}`);
17
+ }
18
+
19
+ /** settings 가 들고 있는 파일 내용을 문자열로 읽는다. */
20
+ export async function readSettingsAsset(subpath) {
21
+ return fs.readFile(resolveSettingsAsset(subpath), 'utf8');
22
+ }
23
+
24
+ /** CLI 가 함께 배포되는 settings 의 실제 버전 (생성 프로젝트가 참조할 버전) */
25
+ export function settingsVersion() {
26
+ return require('@byuckchon-frontend/settings/package.json').version;
27
+ }
@@ -0,0 +1,88 @@
1
+ # ESLint 기반 기계적 컨벤션을 PR 인라인 댓글로 게시합니다.
2
+ # typecheck/lint/build 성공 여부는 PR Check workflow가 담당합니다.
3
+ #
4
+ # 각 패키지의 eslint.config에는 internal 플러그인이 없으므로,
5
+ # tools/review.config.mjs 전용 config로 레포 루트에서 PR 변경 파일만 검사합니다.
6
+ #
7
+ # 규칙과 게시 스크립트는 @byuckchon-frontend/settings 가 제공합니다.
8
+ # 프로젝트에 규칙 사본을 두지 않으므로, 규칙 변경은 settings 버전 업으로 반영됩니다.
9
+
10
+ name: ESLint Convention Review
11
+
12
+ on:
13
+ pull_request:
14
+ types: [opened, synchronize, reopened]
15
+ branches:
16
+ - dev
17
+
18
+ concurrency:
19
+ group: eslint-convention-review-${{ github.event.pull_request.number }}
20
+ cancel-in-progress: true
21
+
22
+ jobs:
23
+ eslint-conventions:
24
+ name: ESLint Convention Review
25
+ runs-on: ubuntu-latest
26
+ permissions:
27
+ contents: read
28
+ pull-requests: write
29
+ steps:
30
+ - uses: actions/checkout@v4
31
+
32
+ - uses: pnpm/action-setup@v4
33
+
34
+ - uses: actions/setup-node@v4
35
+ with:
36
+ node-version-file: .nvmrc
37
+ cache: pnpm
38
+
39
+ - name: 의존성 설치
40
+ run: pnpm install --frozen-lockfile
41
+
42
+ - name: PR 변경 파일 조회
43
+ env:
44
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45
+ run: |
46
+ gh api "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" \
47
+ --paginate \
48
+ --jq '.[] | select(.status != "removed") | .filename | select(test("\\.(ts|tsx)$"))' \
49
+ > /tmp/eslint-convention-files.txt
50
+
51
+ echo "변경된 TypeScript 파일:"
52
+ cat /tmp/eslint-convention-files.txt
53
+
54
+ - name: ESLint 컨벤션 검사
55
+ run: |
56
+ if [[ ! -s /tmp/eslint-convention-files.txt ]]; then
57
+ echo "변경된 TypeScript 파일이 없습니다."
58
+ printf '{"source":{"name":"eslint-conventions"},"diagnostics":[]}' \
59
+ > /tmp/eslint-conventions.rdjson
60
+ exit 0
61
+ fi
62
+
63
+ mapfile -t changed_files < /tmp/eslint-convention-files.txt
64
+
65
+ set +e
66
+ pnpm exec eslint "${changed_files[@]}" \
67
+ --config tools/review.config.mjs \
68
+ --no-error-on-unmatched-pattern \
69
+ -f node_modules/@byuckchon-frontend/settings/src/eslint/internal-rdjson-formatter.js \
70
+ -o /tmp/eslint-conventions.rdjson
71
+ eslint_exit=$?
72
+ set -e
73
+
74
+ if [[ ! -f /tmp/eslint-conventions.rdjson ]]; then
75
+ echo "ESLint가 /tmp/eslint-conventions.rdjson 을 생성하지 못했습니다. (exit ${eslint_exit})"
76
+ exit "$eslint_exit"
77
+ fi
78
+
79
+ - name: 리뷰 댓글 게시
80
+ env:
81
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
82
+ PR_NUMBER: ${{ github.event.pull_request.number }}
83
+ REPO: ${{ github.repository }}
84
+ HEAD_SHA: ${{ github.event.pull_request.head.sha }}
85
+ run: |
86
+ npx --no-install byuckchon-eslint-review-comments \
87
+ /tmp/eslint-conventions.rdjson \
88
+ 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-file: .nvmrc
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 . \
38
+ --config tools/review.config.mjs \
39
+ --no-error-on-unmatched-pattern \
40
+ -f node_modules/@byuckchon-frontend/settings/src/eslint/internal-rdjson-formatter.js \
41
+ -o /tmp/eslint-conventions.rdjson || true
42
+ npx --no-install byuckchon-eslint-review-comments \
43
+ /tmp/eslint-conventions.rdjson \
44
+ eslint-conventions
@@ -0,0 +1,51 @@
1
+ # PR Check — 모노레포
2
+ #
3
+ # 기계적인 컨벤션은 ESLint Convention Review workflow 가 인라인 댓글로 알려주고,
4
+ # 이 workflow 는 "머지해도 되는가"(lint / typecheck / build 성공 여부)를 판정합니다.
5
+ #
6
+ # turbo 가 변경된 패키지만 골라 실행하고 캐시를 재사용합니다.
7
+ # 프로젝트 사정에 맞게 자유롭게 수정하세요. 이 파일은 프로젝트 소유입니다.
8
+
9
+ name: PR Check
10
+
11
+ on:
12
+ pull_request:
13
+ types: [opened, synchronize, reopened]
14
+ branches:
15
+ - dev
16
+
17
+ concurrency:
18
+ group: pr-check-${{ github.event.pull_request.number }}
19
+ cancel-in-progress: true
20
+
21
+ jobs:
22
+ check:
23
+ name: Lint · Typecheck · Build
24
+ runs-on: ubuntu-latest
25
+ permissions:
26
+ contents: read
27
+
28
+ steps:
29
+ - uses: actions/checkout@v4
30
+ with:
31
+ # turbo 가 변경 범위를 계산할 수 있도록 히스토리를 가져옵니다.
32
+ fetch-depth: 0
33
+
34
+ - uses: pnpm/action-setup@v4
35
+
36
+ - uses: actions/setup-node@v4
37
+ with:
38
+ node-version-file: .nvmrc
39
+ cache: pnpm
40
+
41
+ - name: 의존성 설치
42
+ run: pnpm install --frozen-lockfile
43
+
44
+ - name: Lint
45
+ run: pnpm lint
46
+
47
+ - name: Typecheck
48
+ run: pnpm typecheck
49
+
50
+ - name: Build
51
+ run: pnpm build
@@ -0,0 +1,47 @@
1
+ # PR Check — 단일 프로젝트
2
+ #
3
+ # 기계적인 컨벤션은 ESLint Convention Review workflow 가 인라인 댓글로 알려주고,
4
+ # 이 workflow 는 "머지해도 되는가"(lint / typecheck / build 성공 여부)를 판정합니다.
5
+ #
6
+ # 프로젝트 사정에 맞게 자유롭게 수정하세요. 이 파일은 프로젝트 소유입니다.
7
+ # - 대상 브랜치가 dev 가 아니라면 아래 branches 를 바꾸세요.
8
+ # - 테스트가 있다면 마지막에 단계를 추가하세요.
9
+
10
+ name: PR Check
11
+
12
+ on:
13
+ pull_request:
14
+ types: [opened, synchronize, reopened]
15
+ branches:
16
+ - dev
17
+
18
+ concurrency:
19
+ group: pr-check-${{ github.event.pull_request.number }}
20
+ cancel-in-progress: true
21
+
22
+ jobs:
23
+ check:
24
+ name: Lint · Typecheck · Build
25
+ runs-on: ubuntu-latest
26
+ permissions:
27
+ contents: read
28
+
29
+ steps:
30
+ - uses: actions/checkout@v4
31
+
32
+ - uses: actions/setup-node@v4
33
+ with:
34
+ node-version-file: .nvmrc
35
+ cache: npm
36
+
37
+ - name: 의존성 설치
38
+ run: npm ci
39
+
40
+ - name: Lint
41
+ run: npm run lint
42
+
43
+ - name: Typecheck
44
+ run: npm run typecheck
45
+
46
+ - name: Build
47
+ run: npm run build
@@ -0,0 +1,9 @@
1
+ // ESLint Convention Review 워크플로 전용 config.
2
+ //
3
+ // 규칙 본체(internal 플러그인 + 컨벤션 규칙)는 @byuckchon-frontend/settings 가
4
+ // 관리합니다. 규칙이 바뀌면 settings 버전만 올리면 되고 이 파일은 그대로 둡니다.
5
+ //
6
+ // 이 프로젝트에만 해당하는 예외가 필요하면 아래에 이어붙이세요.
7
+ // import review from '@byuckchon-frontend/settings/eslint/review';
8
+ // export default [...review, { ignores: ['src/legacy/**'] }];
9
+ export { default } from '@byuckchon-frontend/settings/eslint/review';