byuckchon-frontend-cli 1.9.2 → 1.9.4

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,514 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { createApp } from './createApp.js';
5
+
6
+ /**
7
+ * 새 pnpm 모노레포를 스캐폴드한다. (marketd-frontend 구조 참고)
8
+ *
9
+ * 생성물:
10
+ * <root>/
11
+ * package.json (turbo 스크립트 · pnpm)
12
+ * pnpm-workspace.yaml
13
+ * turbo.json
14
+ * tsconfig.base.json / tsconfig.json
15
+ * .npmrc / .nvmrc / .gitignore
16
+ * prettier.config.mjs / eslint.config.mjs
17
+ * README.md
18
+ * packages/config-typescript/*
19
+ * packages/config-eslint/*
20
+ * apps/<initial-app>/ (사용자가 고른 React 또는 Next 하나)
21
+ *
22
+ * @param {object} config { projectName, framework, appName, scope, aiModel, figmaUrl, openapiUrl }
23
+ */
24
+ export async function createMonorepo(config) {
25
+ const root = path.resolve(config.projectName);
26
+ const scope = config.scope;
27
+
28
+ await fs.mkdir(root);
29
+ await fs.mkdir(path.join(root, 'apps'), { recursive: true });
30
+ await fs.mkdir(path.join(root, 'packages'), { recursive: true });
31
+
32
+ await writeRootFiles(root, config, scope);
33
+ await writeConfigTypescript(root, scope);
34
+ await writeConfigEslint(root, scope);
35
+
36
+ // 최초 앱 하나만 생성. 이후 추가는 `bc add`.
37
+ const appName = config.appName;
38
+ await createApp({
39
+ appDir: path.join(root, 'apps', appName),
40
+ config: { ...config, projectName: appName },
41
+ scope,
42
+ });
43
+
44
+ return { root, scope, appName };
45
+ }
46
+
47
+ async function write(filePath, content) {
48
+ await fs.writeFile(filePath, content, 'utf-8');
49
+ }
50
+
51
+ async function writeJson(filePath, obj) {
52
+ await write(filePath, JSON.stringify(obj, null, 2) + '\n');
53
+ }
54
+
55
+ async function writeRootFiles(root, config, scope) {
56
+ const name = config.projectName;
57
+
58
+ await writeJson(path.join(root, 'package.json'), {
59
+ name,
60
+ version: '0.0.0',
61
+ private: true,
62
+ description: `${name} monorepo`,
63
+ packageManager: 'pnpm@10.0.0',
64
+ engines: { node: '>=20.0.0', pnpm: '>=9.0.0' },
65
+ type: 'module',
66
+ scripts: {
67
+ build: 'turbo run build',
68
+ dev: 'turbo run dev',
69
+ lint: 'turbo run lint',
70
+ 'lint:fix': 'turbo run lint -- --fix',
71
+ typecheck: 'turbo run typecheck',
72
+ 'tokens:build': 'turbo run tokens:build',
73
+ format:
74
+ 'prettier --write "**/*.{ts,tsx,js,jsx,json,md,yml,yaml}" --ignore-path .gitignore',
75
+ 'format:check':
76
+ 'prettier --check "**/*.{ts,tsx,js,jsx,json,md,yml,yaml}" --ignore-path .gitignore',
77
+ clean: 'turbo run clean && rm -rf node_modules .turbo',
78
+ preinstall: 'npx only-allow pnpm',
79
+ // 최초 앱 실행 단축키 (bc add 시 앱마다 추가됨).
80
+ [config.appName]: `pnpm --filter @${scope}/${config.appName} dev`,
81
+ },
82
+ devDependencies: {
83
+ '@types/node': '^22.0.0',
84
+ eslint: '^9.18.0',
85
+ 'eslint-config-prettier': '^10.1.8',
86
+ prettier: '^3.3.0',
87
+ turbo: '^2.9.6',
88
+ typescript: '~5.7.0',
89
+ },
90
+ });
91
+
92
+ await write(
93
+ path.join(root, 'pnpm-workspace.yaml'),
94
+ `# pnpm workspace 정의
95
+ # - apps/* : 배포 대상 (React/Next 앱)
96
+ # - packages/* : 내부 공유 패키지 (@${scope}/*)
97
+ packages:
98
+ - "apps/*"
99
+ - "packages/*"
100
+ `,
101
+ );
102
+
103
+ await writeJson(path.join(root, 'turbo.json'), {
104
+ $schema: 'https://turborepo.org/schema.json',
105
+ ui: 'tui',
106
+ globalDependencies: ['tsconfig.base.json', '.env', '.env.*', '!.env*.local'],
107
+ globalEnv: ['NODE_ENV', 'CI'],
108
+ tasks: {
109
+ build: {
110
+ dependsOn: ['^build'],
111
+ outputs: ['dist/**', 'build/**', '.next/**', '!.next/cache/**', 'out/**'],
112
+ inputs: [
113
+ '$TURBO_DEFAULT$',
114
+ '!**/*.md',
115
+ '!**/*.test.ts',
116
+ '!**/*.test.tsx',
117
+ '!**/*.spec.ts',
118
+ '!**/*.spec.tsx',
119
+ ],
120
+ },
121
+ dev: { cache: false, persistent: true },
122
+ lint: { dependsOn: ['^build'], outputs: [] },
123
+ typecheck: { dependsOn: ['^build'], outputs: ['.tsbuildinfo', '**/*.tsbuildinfo'] },
124
+ 'tokens:build': { outputs: ['src/tokens.css'] },
125
+ clean: { cache: false },
126
+ },
127
+ });
128
+
129
+ await writeJson(path.join(root, 'tsconfig.base.json'), {
130
+ $schema: 'https://json.schemastore.org/tsconfig',
131
+ display: `${name} Base`,
132
+ compilerOptions: {
133
+ target: 'ES2022',
134
+ lib: ['ES2022'],
135
+ module: 'ESNext',
136
+ moduleResolution: 'Bundler',
137
+ strict: true,
138
+ noUncheckedIndexedAccess: true,
139
+ noImplicitOverride: true,
140
+ noFallthroughCasesInSwitch: true,
141
+ useUnknownInCatchVariables: true,
142
+ exactOptionalPropertyTypes: false,
143
+ esModuleInterop: true,
144
+ allowSyntheticDefaultImports: true,
145
+ forceConsistentCasingInFileNames: true,
146
+ resolveJsonModule: true,
147
+ isolatedModules: true,
148
+ verbatimModuleSyntax: false,
149
+ skipLibCheck: true,
150
+ incremental: true,
151
+ composite: false,
152
+ types: [],
153
+ },
154
+ exclude: ['node_modules', 'dist', 'build', '.turbo', '.next', 'coverage'],
155
+ });
156
+
157
+ await writeJson(path.join(root, 'tsconfig.json'), {
158
+ extends: './tsconfig.base.json',
159
+ files: [],
160
+ include: [],
161
+ });
162
+
163
+ await write(
164
+ path.join(root, '.npmrc'),
165
+ `# pnpm 동작 설정
166
+ # React 버전 통일 및 (향후 Expo/RN 도입 대비) hoisted linker 사용.
167
+ node-linker=hoisted
168
+
169
+ public-hoist-pattern[]=*react*
170
+ public-hoist-pattern[]=*@types/*
171
+ public-hoist-pattern[]=*eslint*
172
+ public-hoist-pattern[]=*prettier*
173
+
174
+ strict-peer-dependencies=false
175
+ auto-install-peers=true
176
+ save-exact=false
177
+ save-prefix=^
178
+ prefer-frozen-lockfile=true
179
+ `,
180
+ );
181
+
182
+ await write(path.join(root, '.nvmrc'), '20\n');
183
+
184
+ await write(
185
+ path.join(root, 'prettier.config.mjs'),
186
+ `/** @type {import("prettier").Config} */
187
+ export default {
188
+ semi: true,
189
+ singleQuote: true,
190
+ trailingComma: 'all',
191
+ tabWidth: 2,
192
+ printWidth: 100,
193
+ arrowParens: 'always',
194
+ endOfLine: 'lf',
195
+ bracketSpacing: true,
196
+ bracketSameLine: false,
197
+ jsxSingleQuote: false,
198
+ plugins: ['prettier-plugin-tailwindcss'],
199
+ overrides: [
200
+ {
201
+ files: ['*.json', '*.md', '*.yml', '*.yaml'],
202
+ options: { tabWidth: 2 },
203
+ },
204
+ ],
205
+ };
206
+ `,
207
+ );
208
+
209
+ await write(
210
+ path.join(root, 'eslint.config.mjs'),
211
+ `// 루트 레벨 ESLint config.
212
+ // 각 앱/패키지는 자체 eslint.config.mjs 를 가진다. 루트는 스크립트/설정 파일만 훑는다.
213
+ import { baseConfig } from '@${scope}/config-eslint/base';
214
+
215
+ export default [
216
+ ...baseConfig,
217
+ {
218
+ ignores: [
219
+ 'apps/**',
220
+ 'packages/**',
221
+ '**/node_modules/**',
222
+ '**/dist/**',
223
+ '**/build/**',
224
+ '**/.turbo/**',
225
+ '**/.next/**',
226
+ ],
227
+ },
228
+ ];
229
+ `,
230
+ );
231
+
232
+ await write(
233
+ path.join(root, '.gitignore'),
234
+ `# Dependencies
235
+ node_modules/
236
+ .pnpm-store/
237
+
238
+ # Build
239
+ dist/
240
+ build/
241
+ out/
242
+ .next/
243
+ .turbo/
244
+ *.tsbuildinfo
245
+
246
+ # Env
247
+ .env
248
+ .env.*
249
+ !.env.example
250
+
251
+ # OS / Editor
252
+ .DS_Store
253
+ Thumbs.db
254
+ .idea/
255
+
256
+ # Logs
257
+ npm-debug.log*
258
+ pnpm-debug.log*
259
+
260
+ # bc
261
+ .bc/
262
+ `,
263
+ );
264
+
265
+ await write(
266
+ path.join(root, 'README.md'),
267
+ `# ${name}
268
+
269
+ pnpm + Turborepo 기반 프론트엔드 모노레포. (byuckchon-frontend-cli 로 생성)
270
+
271
+ ## 구조
272
+
273
+ \`\`\`
274
+ apps/ # 배포 대상 (React / Next 앱)
275
+ ${config.appName}/
276
+ packages/ # 내부 공유 패키지 (@${scope}/*)
277
+ config-eslint/
278
+ config-typescript/
279
+ \`\`\`
280
+
281
+ ## 시작하기
282
+
283
+ \`\`\`bash
284
+ pnpm install # 전체 의존성 설치
285
+ pnpm dev # 모든 앱 dev (turbo)
286
+ pnpm ${config.appName} # ${config.appName} 앱만 실행
287
+ \`\`\`
288
+
289
+ ## 앱 추가
290
+
291
+ 새 React/Next 앱을 이 모노레포에 추가하려면 루트에서:
292
+
293
+ \`\`\`bash
294
+ bc add
295
+ \`\`\`
296
+
297
+ ## 스크립트
298
+
299
+ | 명령 | 설명 |
300
+ |------|------|
301
+ | \`pnpm dev\` | 전체 앱 개발 서버 (turbo) |
302
+ | \`pnpm build\` | 전체 빌드 |
303
+ | \`pnpm lint\` | 전체 lint |
304
+ | \`pnpm typecheck\` | 전체 타입체크 |
305
+ | \`pnpm format\` | Prettier 포맷 |
306
+ `,
307
+ );
308
+ }
309
+
310
+ async function writeConfigTypescript(root, scope) {
311
+ const dir = path.join(root, 'packages', 'config-typescript');
312
+ await fs.mkdir(dir, { recursive: true });
313
+
314
+ await writeJson(path.join(dir, 'package.json'), {
315
+ name: `@${scope}/config-typescript`,
316
+ version: '0.0.0',
317
+ private: true,
318
+ description: `Shared TypeScript config presets for @${scope} apps & packages`,
319
+ files: ['base.json', 'library.json', 'react.json', 'next.json', 'node.json'],
320
+ });
321
+
322
+ await writeJson(path.join(dir, 'base.json'), {
323
+ $schema: 'https://json.schemastore.org/tsconfig',
324
+ display: 'Base',
325
+ extends: '../../tsconfig.base.json',
326
+ });
327
+
328
+ await writeJson(path.join(dir, 'react.json'), {
329
+ $schema: 'https://json.schemastore.org/tsconfig',
330
+ display: 'React Web (Vite)',
331
+ extends: './base.json',
332
+ compilerOptions: {
333
+ lib: ['ES2022', 'DOM', 'DOM.Iterable'],
334
+ jsx: 'react-jsx',
335
+ moduleResolution: 'Bundler',
336
+ module: 'ESNext',
337
+ noEmit: true,
338
+ allowImportingTsExtensions: true,
339
+ useDefineForClassFields: true,
340
+ types: ['vite/client'],
341
+ noUncheckedIndexedAccess: false,
342
+ },
343
+ });
344
+
345
+ await writeJson(path.join(dir, 'next.json'), {
346
+ $schema: 'https://json.schemastore.org/tsconfig',
347
+ display: 'Next.js (App Router)',
348
+ extends: './base.json',
349
+ compilerOptions: {
350
+ lib: ['ES2022', 'DOM', 'DOM.Iterable'],
351
+ jsx: 'preserve',
352
+ module: 'ESNext',
353
+ moduleResolution: 'Bundler',
354
+ noEmit: true,
355
+ allowJs: true,
356
+ incremental: true,
357
+ plugins: [{ name: 'next' }],
358
+ noUncheckedIndexedAccess: false,
359
+ },
360
+ });
361
+
362
+ await writeJson(path.join(dir, 'library.json'), {
363
+ $schema: 'https://json.schemastore.org/tsconfig',
364
+ display: 'Library (packages/*)',
365
+ extends: './base.json',
366
+ compilerOptions: {
367
+ declaration: true,
368
+ declarationMap: true,
369
+ sourceMap: true,
370
+ outDir: 'dist',
371
+ rootDir: 'src',
372
+ composite: true,
373
+ },
374
+ });
375
+
376
+ await writeJson(path.join(dir, 'node.json'), {
377
+ $schema: 'https://json.schemastore.org/tsconfig',
378
+ display: 'Node (scripts)',
379
+ extends: './base.json',
380
+ compilerOptions: {
381
+ lib: ['ES2022'],
382
+ module: 'NodeNext',
383
+ moduleResolution: 'NodeNext',
384
+ types: ['node'],
385
+ },
386
+ });
387
+ }
388
+
389
+ async function writeConfigEslint(root, scope) {
390
+ const dir = path.join(root, 'packages', 'config-eslint');
391
+ await fs.mkdir(dir, { recursive: true });
392
+
393
+ await writeJson(path.join(dir, 'package.json'), {
394
+ name: `@${scope}/config-eslint`,
395
+ version: '0.0.0',
396
+ private: true,
397
+ type: 'module',
398
+ description: `Shared ESLint flat configs for @${scope} apps & packages`,
399
+ main: './base.js',
400
+ exports: {
401
+ '.': './base.js',
402
+ './base': './base.js',
403
+ './react': './react.js',
404
+ },
405
+ files: ['base.js', 'react.js'],
406
+ dependencies: {
407
+ '@eslint/js': '^9.18.0',
408
+ 'eslint-config-prettier': '^10.1.8',
409
+ 'eslint-plugin-import': '^2.32.0',
410
+ 'eslint-plugin-react': '^7.37.4',
411
+ 'eslint-plugin-react-hooks': '^5.1.0',
412
+ globals: '^15.14.0',
413
+ 'typescript-eslint': '^8.59.1',
414
+ },
415
+ peerDependencies: {
416
+ eslint: '^9.0.0',
417
+ typescript: '>=5.0.0',
418
+ },
419
+ });
420
+
421
+ await write(
422
+ path.join(dir, 'base.js'),
423
+ `// @ts-check
424
+ import js from '@eslint/js';
425
+ import tseslint from 'typescript-eslint';
426
+ import prettier from 'eslint-config-prettier';
427
+ import importPlugin from 'eslint-plugin-import';
428
+ import globals from 'globals';
429
+
430
+ /**
431
+ * 공통 ESLint flat config (TS 베이스).
432
+ * @type {import("eslint").Linter.Config[]}
433
+ */
434
+ export const baseConfig = [
435
+ {
436
+ ignores: [
437
+ '**/node_modules/**',
438
+ '**/dist/**',
439
+ '**/build/**',
440
+ '**/out/**',
441
+ '**/.next/**',
442
+ '**/.turbo/**',
443
+ '**/coverage/**',
444
+ '**/*.d.ts',
445
+ ],
446
+ },
447
+ js.configs.recommended,
448
+ ...tseslint.configs.recommended,
449
+ {
450
+ languageOptions: {
451
+ ecmaVersion: 2022,
452
+ sourceType: 'module',
453
+ globals: { ...globals.es2022 },
454
+ },
455
+ plugins: { import: importPlugin },
456
+ rules: {
457
+ 'no-console': ['warn', { allow: ['warn', 'error'] }],
458
+ '@typescript-eslint/consistent-type-imports': [
459
+ 'warn',
460
+ { prefer: 'type-imports', fixStyle: 'inline-type-imports' },
461
+ ],
462
+ '@typescript-eslint/no-unused-vars': [
463
+ 'warn',
464
+ { argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
465
+ ],
466
+ },
467
+ },
468
+ prettier,
469
+ ];
470
+
471
+ export default baseConfig;
472
+ `,
473
+ );
474
+
475
+ await write(
476
+ path.join(dir, 'react.js'),
477
+ `// @ts-check
478
+ import react from 'eslint-plugin-react';
479
+ import reactHooks from 'eslint-plugin-react-hooks';
480
+ import globals from 'globals';
481
+
482
+ import { baseConfig } from './base.js';
483
+
484
+ /**
485
+ * React (Vite/Next) 앱용 ESLint config.
486
+ * @type {import("eslint").Linter.Config[]}
487
+ */
488
+ export const reactConfig = [
489
+ ...baseConfig,
490
+ {
491
+ files: ['**/*.{js,jsx,ts,tsx}'],
492
+ languageOptions: {
493
+ globals: { ...globals.browser, ...globals.es2022 },
494
+ parserOptions: { ecmaFeatures: { jsx: true } },
495
+ },
496
+ plugins: { react, 'react-hooks': reactHooks },
497
+ settings: { react: { version: 'detect' } },
498
+ rules: {
499
+ ...react.configs.recommended.rules,
500
+ ...react.configs['jsx-runtime'].rules,
501
+ ...reactHooks.configs.recommended.rules,
502
+ 'react/prop-types': 'off',
503
+ 'react/react-in-jsx-scope': 'off',
504
+ 'react/no-unescaped-entities': 'off',
505
+ 'react-hooks/rules-of-hooks': 'warn',
506
+ '@typescript-eslint/no-explicit-any': 'off',
507
+ },
508
+ },
509
+ ];
510
+
511
+ export default reactConfig;
512
+ `,
513
+ );
514
+ }
@@ -26,7 +26,7 @@ export async function createProject(config) {
26
26
  await createPackageJson(rootDir, config);
27
27
  await createBaseFiles(rootDir, config);
28
28
  await createReadme(rootDir, config);
29
- // API 코드 컨벤션 .md 를 프레임워크에 맞는 API 루트(src/api | lib/api)에 깐다.
29
+ // API 코드 컨벤션 .md 를 프레임워크에 맞는 API 루트(src/api | src/lib/api)에 깐다.
30
30
  await scaffoldApiConventionDoc({ projectRoot: rootDir, framework: config.framework });
31
31
  await createBcConfig(rootDir, config);
32
32
 
@@ -0,0 +1,69 @@
1
+ import { exec as execCallback } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+
4
+ import chalk from 'chalk';
5
+
6
+ const exec = promisify(execCallback);
7
+
8
+ export const BYUCKCHON_PACKAGES = [
9
+ '@byuckchon-frontend/hooks',
10
+ '@byuckchon-frontend/utils',
11
+ '@byuckchon-frontend/basic-ui',
12
+ '@byuckchon-frontend/core',
13
+ ];
14
+
15
+ async function hasPnpm() {
16
+ try {
17
+ await exec('pnpm --version');
18
+ return true;
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ /**
25
+ * 모노레포 루트에서 pnpm install + 앱에 byuckchon 패키지 추가.
26
+ * 네트워크/pnpm 미설치 등으로 실패해도 스캐폴딩 자체는 성공으로 둔다(경고만).
27
+ *
28
+ * @param {object} args
29
+ * @param {string} args.root 모노레포 루트
30
+ * @param {string} args.appPkgName 앱 package.json name (예: @scope/web)
31
+ */
32
+ export async function installMonorepoDeps({ root, appPkgName }) {
33
+ if (!(await hasPnpm())) {
34
+ console.log(
35
+ chalk.yellow('\n ⚠ pnpm 이 없어 의존성 설치를 건너뜁니다.'),
36
+ );
37
+ console.log(chalk.dim(' npm i -g pnpm 후 루트에서 pnpm install 을 실행하세요.\n'));
38
+ return;
39
+ }
40
+
41
+ try {
42
+ console.log(chalk.dim('\n pnpm install 중... (잠시 걸릴 수 있어요)'));
43
+ await exec('pnpm install', { cwd: root });
44
+ } catch (err) {
45
+ console.log(chalk.yellow(' ⚠ pnpm install 실패 — 루트에서 직접 실행해주세요.'));
46
+ console.log(chalk.dim(` ${err?.message ?? err}\n`));
47
+ return;
48
+ }
49
+
50
+ if (appPkgName) {
51
+ try {
52
+ await exec(
53
+ `pnpm --filter ${appPkgName} add ${BYUCKCHON_PACKAGES.join(' ')}`,
54
+ { cwd: root },
55
+ );
56
+ } catch (err) {
57
+ console.log(
58
+ chalk.yellow(
59
+ ` ⚠ byuckchon 패키지 설치 실패 — 나중에 다음을 실행하세요:`,
60
+ ),
61
+ );
62
+ console.log(
63
+ chalk.dim(
64
+ ` pnpm --filter ${appPkgName} add ${BYUCKCHON_PACKAGES.join(' ')}\n`,
65
+ ),
66
+ );
67
+ }
68
+ }
69
+ }