byuckchon-frontend-cli 1.9.7 → 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.
Files changed (24) hide show
  1. package/README.md +39 -5
  2. package/package.json +8 -2
  3. package/src/commands/adopt.js +12 -3
  4. package/src/constants/versions.js +12 -46
  5. package/src/generators/createApp.js +13 -29
  6. package/src/generators/createBaseFiles.js +79 -311
  7. package/src/generators/createFolders.js +17 -1
  8. package/src/generators/createMonorepo.js +49 -236
  9. package/src/generators/createPackageJson.js +10 -11
  10. package/src/generators/scaffoldReviewAutomation.js +41 -31
  11. package/src/utils/ensureRequiredDependencies.js +35 -9
  12. package/src/utils/settingsAssets.js +27 -0
  13. package/templates/review-automation/github/workflows/eslint-convention-review.monorepo.yml +8 -6
  14. package/templates/review-automation/github/workflows/eslint-convention-review.single.yml +6 -6
  15. package/templates/review-automation/github/workflows/pr-check.monorepo.yml +51 -0
  16. package/templates/review-automation/github/workflows/pr-check.single.yml +47 -0
  17. package/templates/review-automation/tools/review.config.mjs +9 -0
  18. package/templates/review-automation/tools/eslint-rules/internal-blocking-conventions.js +0 -1242
  19. package/templates/review-automation/tools/eslint-rules/internal-plugin.cjs +0 -8
  20. package/templates/review-automation/tools/eslint-rules/internal-rdjson-formatter.js +0 -60
  21. package/templates/review-automation/tools/eslint-rules/internal-warning-conventions.js +0 -57
  22. package/templates/review-automation/tools/eslint-rules/package.json +0 -3
  23. package/templates/review-automation/tools/eslint-rules/review.config.mjs +0 -22
  24. package/templates/review-automation/tools/post-eslint-review-comments.cjs +0 -309
@@ -1,6 +1,8 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
 
4
+ import { versions } from '../constants/versions.js';
5
+ import { readSettingsAsset } from '../utils/settingsAssets.js';
4
6
  import { createApp } from './createApp.js';
5
7
  import { scaffoldReviewAutomation } from './scaffoldReviewAutomation.js';
6
8
 
@@ -82,6 +84,8 @@ async function writeRootFiles(root, config, scope) {
82
84
  [config.appName]: `pnpm --filter @${scope}/${config.appName} dev`,
83
85
  },
84
86
  devDependencies: {
87
+ // 루트의 prettier.config.mjs 와 tsconfig.base.json 이 직접 참조한다.
88
+ '@byuckchon-frontend/settings': versions['@byuckchon-frontend/settings'],
85
89
  '@types/node': '^22.0.0',
86
90
  eslint: '^9.18.0',
87
91
  'eslint-config-prettier': '^10.1.8',
@@ -102,58 +106,22 @@ packages:
102
106
  `,
103
107
  );
104
108
 
105
- await writeJson(path.join(root, 'turbo.json'), {
106
- $schema: 'https://turborepo.org/schema.json',
107
- ui: 'tui',
108
- globalDependencies: ['tsconfig.base.json', '.env', '.env.*', '!.env*.local'],
109
- globalEnv: ['NODE_ENV', 'CI'],
110
- tasks: {
111
- build: {
112
- dependsOn: ['^build'],
113
- outputs: ['dist/**', 'build/**', '.next/**', '!.next/cache/**', 'out/**'],
114
- inputs: [
115
- '$TURBO_DEFAULT$',
116
- '!**/*.md',
117
- '!**/*.test.ts',
118
- '!**/*.test.tsx',
119
- '!**/*.spec.ts',
120
- '!**/*.spec.tsx',
121
- ],
122
- },
123
- dev: { cache: false, persistent: true },
124
- lint: { dependsOn: ['^build'], outputs: [] },
125
- typecheck: { dependsOn: ['^build'], outputs: ['.tsbuildinfo', '**/*.tsbuildinfo'] },
126
- 'tokens:build': { outputs: ['src/tokens.css'] },
127
- clean: { cache: false },
128
- },
129
- });
109
+ // turbo / .npmrc / .nvmrc 는 참조 문법이 없어서 내용을 복사한다.
110
+ // 이후 settings 가 바뀌면 `npx byuckchon-settings-sync` 로 갱신한다.
111
+ await write(path.join(root, 'turbo.json'), await readSettingsAsset('project/turbo.json'));
130
112
 
113
+ // 공통 옵션은 @byuckchon-frontend/settings 가 관리한다.
114
+ // 이 파일은 모노레포 전용 예외를 얹는 자리로만 남긴다.
131
115
  await writeJson(path.join(root, 'tsconfig.base.json'), {
132
116
  $schema: 'https://json.schemastore.org/tsconfig',
133
- display: `${name} Base`,
117
+ display: `${config.projectName} Base`,
118
+ extends: '@byuckchon-frontend/settings/tsconfig/base.json',
134
119
  compilerOptions: {
135
- target: 'ES2022',
136
- lib: ['ES2022'],
137
- module: 'ESNext',
138
- moduleResolution: 'Bundler',
139
- strict: true,
140
120
  noUncheckedIndexedAccess: true,
141
121
  noImplicitOverride: true,
142
- noFallthroughCasesInSwitch: true,
143
- useUnknownInCatchVariables: true,
144
- exactOptionalPropertyTypes: false,
145
- esModuleInterop: true,
146
122
  allowSyntheticDefaultImports: true,
147
- forceConsistentCasingInFileNames: true,
148
- resolveJsonModule: true,
149
- isolatedModules: true,
150
- verbatimModuleSyntax: false,
151
- skipLibCheck: true,
152
123
  incremental: true,
153
- composite: false,
154
- types: [],
155
124
  },
156
- exclude: ['node_modules', 'dist', 'build', '.turbo', '.next', 'coverage'],
157
125
  });
158
126
 
159
127
  await writeJson(path.join(root, 'tsconfig.json'), {
@@ -162,42 +130,19 @@ packages:
162
130
  include: [],
163
131
  });
164
132
 
165
- await write(
166
- path.join(root, '.npmrc'),
167
- `# pnpm 동작 설정
168
- # React 버전 통일 및 (향후 Expo/RN 도입 대비) hoisted linker 사용.
169
- node-linker=hoisted
170
-
171
- public-hoist-pattern[]=*react*
172
- public-hoist-pattern[]=*@types/*
173
- public-hoist-pattern[]=*eslint*
174
- public-hoist-pattern[]=*prettier*
175
-
176
- strict-peer-dependencies=false
177
- auto-install-peers=true
178
- save-exact=false
179
- save-prefix=^
180
- prefer-frozen-lockfile=true
181
- `,
182
- );
133
+ await write(path.join(root, '.npmrc'), await readSettingsAsset('project/npmrc'));
183
134
 
184
- await write(path.join(root, '.nvmrc'), '20\n');
135
+ await write(path.join(root, '.nvmrc'), await readSettingsAsset('project/nvmrc'));
185
136
 
186
137
  await write(
187
138
  path.join(root, 'prettier.config.mjs'),
188
- `/** @type {import("prettier").Config} */
139
+ `// 포맷 규칙은 @byuckchon-frontend/settings 관리합니다.
140
+ // 이 모노레포 전용 예외가 필요하면 펼쳐서 덮어쓰세요.
141
+ import byuckchon from '@byuckchon-frontend/settings/prettier';
142
+
143
+ /** @type {import("prettier").Config} */
189
144
  export default {
190
- semi: true,
191
- singleQuote: true,
192
- trailingComma: 'all',
193
- tabWidth: 2,
194
- printWidth: 100,
195
- arrowParens: 'always',
196
- endOfLine: 'lf',
197
- bracketSpacing: true,
198
- bracketSameLine: false,
199
- jsxSingleQuote: false,
200
- plugins: ['prettier-plugin-tailwindcss'],
145
+ ...byuckchon,
201
146
  overrides: [
202
147
  {
203
148
  files: ['*.json', '*.md', '*.yml', '*.yaml'],
@@ -205,7 +150,7 @@ export default {
205
150
  },
206
151
  ],
207
152
  };
208
- `,
153
+ `
209
154
  );
210
155
 
211
156
  await write(
@@ -313,106 +258,54 @@ async function writeConfigTypescript(root, scope) {
313
258
  const dir = path.join(root, 'packages', 'config-typescript');
314
259
  await fs.mkdir(dir, { recursive: true });
315
260
 
261
+ // 프리셋 본체는 @byuckchon-frontend/settings 가 관리한다.
262
+ // 이 패키지는 모노레포 전용 예외를 얹을 자리로만 남긴다.
316
263
  await writeJson(path.join(dir, 'package.json'), {
317
264
  name: `@${scope}/config-typescript`,
318
265
  version: '0.0.0',
319
266
  private: true,
320
- description: `Shared TypeScript config presets for @${scope} apps & packages`,
267
+ description: 'settings TypeScript 프리셋을 모노레포용으로 감싼 패키지',
321
268
  files: ['base.json', 'library.json', 'react.json', 'next.json', 'node.json'],
322
- });
323
-
324
- await writeJson(path.join(dir, 'base.json'), {
325
- $schema: 'https://json.schemastore.org/tsconfig',
326
- display: 'Base',
327
- extends: '../../tsconfig.base.json',
328
- });
329
-
330
- await writeJson(path.join(dir, 'react.json'), {
331
- $schema: 'https://json.schemastore.org/tsconfig',
332
- display: 'React Web (Vite)',
333
- extends: './base.json',
334
- compilerOptions: {
335
- lib: ['ES2022', 'DOM', 'DOM.Iterable'],
336
- jsx: 'react-jsx',
337
- moduleResolution: 'Bundler',
338
- module: 'ESNext',
339
- noEmit: true,
340
- allowImportingTsExtensions: true,
341
- useDefineForClassFields: true,
342
- types: ['vite/client'],
343
- noUncheckedIndexedAccess: false,
344
- },
345
- });
346
-
347
- await writeJson(path.join(dir, 'next.json'), {
348
- $schema: 'https://json.schemastore.org/tsconfig',
349
- display: 'Next.js (App Router)',
350
- extends: './base.json',
351
- compilerOptions: {
352
- lib: ['ES2022', 'DOM', 'DOM.Iterable'],
353
- jsx: 'preserve',
354
- module: 'ESNext',
355
- moduleResolution: 'Bundler',
356
- noEmit: true,
357
- allowJs: true,
358
- incremental: true,
359
- plugins: [{ name: 'next' }],
360
- noUncheckedIndexedAccess: false,
269
+ dependencies: {
270
+ '@byuckchon-frontend/settings': versions['@byuckchon-frontend/settings'],
361
271
  },
362
272
  });
363
273
 
364
- await writeJson(path.join(dir, 'library.json'), {
274
+ const preset = (display, name) => ({
365
275
  $schema: 'https://json.schemastore.org/tsconfig',
366
- display: 'Library (packages/*)',
367
- extends: './base.json',
368
- compilerOptions: {
369
- declaration: true,
370
- declarationMap: true,
371
- sourceMap: true,
372
- outDir: 'dist',
373
- rootDir: 'src',
374
- composite: true,
375
- },
276
+ display,
277
+ extends: `@byuckchon-frontend/settings/tsconfig/${name}.json`,
376
278
  });
377
279
 
378
- await writeJson(path.join(dir, 'node.json'), {
379
- $schema: 'https://json.schemastore.org/tsconfig',
380
- display: 'Node (scripts)',
381
- extends: './base.json',
382
- compilerOptions: {
383
- lib: ['ES2022'],
384
- module: 'NodeNext',
385
- moduleResolution: 'NodeNext',
386
- types: ['node'],
387
- },
388
- });
280
+ await writeJson(path.join(dir, 'base.json'), preset('Base', 'base'));
281
+ await writeJson(path.join(dir, 'react.json'), preset('React Web (Vite)', 'react'));
282
+ await writeJson(path.join(dir, 'next.json'), preset('Next.js (App Router)', 'next'));
283
+ await writeJson(path.join(dir, 'library.json'), preset('Library (packages/*)', 'library'));
284
+ await writeJson(path.join(dir, 'node.json'), preset('Node (scripts)', 'node'));
389
285
  }
390
286
 
391
287
  async function writeConfigEslint(root, scope) {
392
288
  const dir = path.join(root, 'packages', 'config-eslint');
393
289
  await fs.mkdir(dir, { recursive: true });
394
290
 
291
+ // 규칙 본체는 @byuckchon-frontend/settings 가 관리한다.
292
+ // 이 패키지는 모노레포 전용 예외를 얹을 자리로만 남긴다.
395
293
  await writeJson(path.join(dir, 'package.json'), {
396
294
  name: `@${scope}/config-eslint`,
397
295
  version: '0.0.0',
398
296
  private: true,
399
297
  type: 'module',
400
- description: `Shared ESLint flat configs for @${scope} apps & packages`,
298
+ description: 'settings ESLint 프리셋을 모노레포용으로 감싼 패키지',
401
299
  main: './base.js',
402
300
  exports: {
403
301
  '.': './base.js',
404
302
  './base': './base.js',
405
303
  './react': './react.js',
304
+ './next': './next.js',
406
305
  },
407
- files: ['base.js', 'react.js'],
306
+ files: ['base.js', 'react.js', 'next.js'],
408
307
  dependencies: {
409
- '@eslint/js': '^9.18.0',
410
- 'eslint-config-prettier': '^10.1.8',
411
- 'eslint-plugin-import': '^2.32.0',
412
- 'eslint-plugin-react': '^7.37.4',
413
- 'eslint-plugin-react-hooks': '^5.1.0',
414
- globals: '^15.14.0',
415
- 'typescript-eslint': '^8.59.1',
308
+ '@byuckchon-frontend/settings': versions['@byuckchon-frontend/settings'],
416
309
  },
417
310
  peerDependencies: {
418
311
  eslint: '^9.0.0',
@@ -420,97 +313,17 @@ async function writeConfigEslint(root, scope) {
420
313
  },
421
314
  });
422
315
 
423
- await write(
424
- path.join(dir, 'base.js'),
425
- `// @ts-check
426
- import js from '@eslint/js';
427
- import tseslint from 'typescript-eslint';
428
- import prettier from 'eslint-config-prettier';
429
- import importPlugin from 'eslint-plugin-import';
430
- import globals from 'globals';
316
+ const reexport = (name, named) => `// 규칙 본체는 @byuckchon-frontend/settings 가 관리합니다.
317
+ // 이 모노레포에만 해당하는 예외는 아래 배열에 이어붙이세요.
318
+ import byuckchon from '@byuckchon-frontend/settings/eslint/${name}';
431
319
 
432
- /**
433
- * 공통 ESLint flat config (TS 베이스).
434
- * @type {import("eslint").Linter.Config[]}
435
- */
436
- export const baseConfig = [
437
- {
438
- ignores: [
439
- '**/node_modules/**',
440
- '**/dist/**',
441
- '**/build/**',
442
- '**/out/**',
443
- '**/.next/**',
444
- '**/.turbo/**',
445
- '**/coverage/**',
446
- '**/*.d.ts',
447
- ],
448
- },
449
- js.configs.recommended,
450
- ...tseslint.configs.recommended,
451
- {
452
- languageOptions: {
453
- ecmaVersion: 2022,
454
- sourceType: 'module',
455
- globals: { ...globals.es2022 },
456
- },
457
- plugins: { import: importPlugin },
458
- rules: {
459
- 'no-console': ['warn', { allow: ['warn', 'error'] }],
460
- '@typescript-eslint/consistent-type-imports': [
461
- 'warn',
462
- { prefer: 'type-imports', fixStyle: 'inline-type-imports' },
463
- ],
464
- '@typescript-eslint/no-unused-vars': [
465
- 'warn',
466
- { argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
467
- ],
468
- },
469
- },
470
- prettier,
471
- ];
472
-
473
- export default baseConfig;
474
- `,
475
- );
476
-
477
- await write(
478
- path.join(dir, 'react.js'),
479
- `// @ts-check
480
- import react from 'eslint-plugin-react';
481
- import reactHooks from 'eslint-plugin-react-hooks';
482
- import globals from 'globals';
320
+ /** @type {import("eslint").Linter.Config[]} */
321
+ export const ${named} = [...byuckchon];
483
322
 
484
- import { baseConfig } from './base.js';
323
+ export default ${named};
324
+ `;
485
325
 
486
- /**
487
- * React (Vite/Next) 앱용 ESLint config.
488
- * @type {import("eslint").Linter.Config[]}
489
- */
490
- export const reactConfig = [
491
- ...baseConfig,
492
- {
493
- files: ['**/*.{js,jsx,ts,tsx}'],
494
- languageOptions: {
495
- globals: { ...globals.browser, ...globals.es2022 },
496
- parserOptions: { ecmaFeatures: { jsx: true } },
497
- },
498
- plugins: { react, 'react-hooks': reactHooks },
499
- settings: { react: { version: 'detect' } },
500
- rules: {
501
- ...react.configs.recommended.rules,
502
- ...react.configs['jsx-runtime'].rules,
503
- ...reactHooks.configs.recommended.rules,
504
- 'react/prop-types': 'off',
505
- 'react/react-in-jsx-scope': 'off',
506
- 'react/no-unescaped-entities': 'off',
507
- 'react-hooks/rules-of-hooks': 'warn',
508
- '@typescript-eslint/no-explicit-any': 'off',
509
- },
510
- },
511
- ];
512
-
513
- export default reactConfig;
514
- `,
515
- );
326
+ await write(path.join(dir, 'base.js'), reexport('base', 'baseConfig'));
327
+ await write(path.join(dir, 'react.js'), reexport('react', 'reactConfig'));
328
+ await write(path.join(dir, 'next.js'), reexport('next', 'nextConfig'));
516
329
  }
@@ -16,6 +16,8 @@ export async function createPackageJson(rootDir, config) {
16
16
  dev: 'vite',
17
17
  build: 'tsc -b && vite build',
18
18
  'tokens:build': 'style-dictionary build --config token.config.js',
19
+ // PR Check workflow 가 lint / typecheck / build 를 각각 호출한다.
20
+ typecheck: 'tsc -b',
19
21
  lint: 'eslint . --ext ts,tsx',
20
22
  preview: 'vite preview',
21
23
  format:
@@ -27,8 +29,9 @@ export async function createPackageJson(rootDir, config) {
27
29
  dev: 'next dev',
28
30
  build: 'next build',
29
31
  'tokens:build': 'style-dictionary build --config token.config.js',
32
+ typecheck: 'tsc --noEmit',
30
33
  start: 'next start',
31
- lint: 'next lint',
34
+ lint: 'eslint .',
32
35
  format:
33
36
  'prettier --write "src/**/*.{ts,tsx,css}" "tools/**/*.{js,cjs,json,md}"',
34
37
  'format:check':
@@ -47,6 +50,8 @@ export async function createPackageJson(rootDir, config) {
47
50
  }
48
51
  : {}),
49
52
  zod: versions.zod,
53
+ clsx: versions.clsx,
54
+ 'tailwind-merge': versions['tailwind-merge'],
50
55
  },
51
56
  devDependencies: {
52
57
  '@types/react': versions['@types/react'],
@@ -54,14 +59,10 @@ export async function createPackageJson(rootDir, config) {
54
59
  '@types/node': versions['@types/node'],
55
60
  '@trivago/prettier-plugin-sort-imports':
56
61
  versions['@trivago/prettier-plugin-sort-imports'],
62
+ // eslint/prettier/tsconfig/tokens 설정과 motion CSS 가 모두 이 패키지를 참조한다.
63
+ // (ESLint 플러그인도 이 패키지가 의존성으로 들고 온다)
64
+ '@byuckchon-frontend/settings': versions['@byuckchon-frontend/settings'],
57
65
  eslint: versions.eslint,
58
- 'eslint-config-expo': versions['eslint-config-expo'],
59
- 'eslint-import-resolver-typescript':
60
- versions['eslint-import-resolver-typescript'],
61
- 'eslint-plugin-import': versions['eslint-plugin-import'],
62
- 'eslint-plugin-react': versions['eslint-plugin-react'],
63
- 'eslint-plugin-react-hooks': versions['eslint-plugin-react-hooks'],
64
- 'eslint-plugin-unused-imports': versions['eslint-plugin-unused-imports'],
65
66
  prettier: versions.prettier,
66
67
  'prettier-plugin-tailwindcss': versions['prettier-plugin-tailwindcss'],
67
68
  'style-dictionary': versions['style-dictionary'],
@@ -70,9 +71,6 @@ export async function createPackageJson(rootDir, config) {
70
71
  ...(isReact
71
72
  ? {
72
73
  '@tailwindcss/vite': versions['@tailwindcss/vite'],
73
- '@typescript-eslint/eslint-plugin':
74
- versions['@typescript-eslint/eslint-plugin'],
75
- '@typescript-eslint/parser': versions['@typescript-eslint/parser'],
76
74
  '@vitejs/plugin-react': versions['@vitejs/plugin-react'],
77
75
  vite: versions.vite,
78
76
  'vite-plugin-svgr': versions['vite-plugin-svgr'],
@@ -81,6 +79,7 @@ export async function createPackageJson(rootDir, config) {
81
79
  '@tailwindcss/postcss': versions['@tailwindcss/postcss'],
82
80
  '@svgr/webpack': versions['@svgr/webpack'],
83
81
  'eslint-config-next': versions['eslint-config-next'],
82
+ '@eslint/eslintrc': versions['@eslint/eslintrc'],
84
83
  }),
85
84
  },
86
85
  };
@@ -7,10 +7,37 @@ const TEMPLATE_ROOT = path.resolve(
7
7
  '../../templates/review-automation',
8
8
  );
9
9
 
10
- const ESLINT_WORKFLOW_BY_PROJECT_TYPE = {
11
- single: 'eslint-convention-review.single.yml',
12
- monorepo: 'eslint-convention-review.monorepo.yml',
13
- };
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
+ }
14
41
 
15
42
  function shouldCopyTemplateFile(source) {
16
43
  return path.basename(source) !== '.DS_Store';
@@ -38,23 +65,24 @@ async function copyFileIfAllowed(source, target, overwrite) {
38
65
  * 프로젝트 루트에 PR 리뷰 자동화 파일을 생성한다.
39
66
  *
40
67
  * - tools/ 는 ESLint convention rule 및 PR 댓글 게시 스크립트를 제공한다.
41
- * - .github/workflows/ 는 프로젝트 유형에 맞는 ESLint workflow 하나와
42
- * 공통 workflow(AI Code Review, PR Check 등)를 제공한다.
68
+ * - .github/workflows/ 는 프로젝트 유형에 맞는 워크플로를 제공한다.
69
+ * 템플릿 이름이 `<이름>.single.yml` / `<이름>.monorepo.yml` 이면 유형이 맞는 쪽만
70
+ * `<이름>.yml` 로 복사되고, 접미사가 없으면 유형과 무관하게 복사된다.
43
71
  *
44
72
  * @param {object} args
45
73
  * @param {string} args.projectRoot 새로 생성한 프로젝트의 절대 경로
46
74
  * @param {'single' | 'monorepo'} args.projectType 생성할 프로젝트 유형
47
75
  * @param {boolean} [args.overwrite=true] 기존 파일을 템플릿으로 덮어쓸지
76
+ * @param {string[]} [args.exclude=[]] 제외할 워크플로 이름 (확장자·유형 접미사 제외)
48
77
  * @returns {Promise<{ toolsDir: string, workflows: string[] }>}
49
78
  */
50
79
  export async function scaffoldReviewAutomation({
51
80
  projectRoot,
52
81
  projectType,
53
82
  overwrite = true,
83
+ exclude = [],
54
84
  }) {
55
- const eslintWorkflow = ESLINT_WORKFLOW_BY_PROJECT_TYPE[projectType];
56
-
57
- if (!eslintWorkflow) {
85
+ if (!PROJECT_TYPES.includes(projectType)) {
58
86
  throw new Error(`지원하지 않는 프로젝트 유형입니다: ${projectType}`);
59
87
  }
60
88
 
@@ -72,33 +100,15 @@ export async function scaffoldReviewAutomation({
72
100
  await fs.mkdir(workflowsTargetDir, { recursive: true });
73
101
 
74
102
  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
103
  const copiedWorkflows = [];
81
104
 
82
- for (const fileName of commonWorkflows) {
105
+ for (const { source, target } of selectWorkflows(workflowFiles, projectType, exclude)) {
83
106
  const copied = await copyFileIfAllowed(
84
- path.join(workflowsTemplateDir, fileName),
85
- path.join(workflowsTargetDir, fileName),
107
+ path.join(workflowsTemplateDir, source),
108
+ path.join(workflowsTargetDir, target),
86
109
  overwrite,
87
110
  );
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');
111
+ if (copied) copiedWorkflows.push(target);
102
112
  }
103
113
 
104
114
  return { toolsDir: toolsTargetDir, workflows: copiedWorkflows };
@@ -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
+ }