├── .cursor └── rules │ └── webpage.mdc ├── .cursorignore ├── .cursorrules ├── .gitattributes ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── monorepo.code-workspace ├── package.json ├── packages └── cli │ ├── .cursorignore │ ├── .gitignore │ ├── .npmrc │ ├── .prettierrc │ ├── CHANGELOG.md │ ├── README.ko.md │ ├── README.md │ ├── bin │ └── cli │ ├── eslint.config.mjs │ ├── nest-cli.json │ ├── package.json │ ├── src │ ├── app.module.ts │ ├── cli.ts │ ├── commands │ │ ├── abstract.command.ts │ │ ├── adsense │ │ │ ├── adsense.command.ts │ │ │ └── adsense.module.ts │ │ ├── auth │ │ │ ├── actions │ │ │ │ ├── idpw.ts │ │ │ │ ├── init.ts │ │ │ │ └── kakao.ts │ │ │ ├── auth.command.ts │ │ │ └── index.ts │ │ ├── channelio │ │ │ ├── channelio.command.ts │ │ │ └── channelio.module.ts │ │ ├── clarity │ │ │ ├── clarity.command.ts │ │ │ └── clarity.module.ts │ │ ├── create │ │ │ ├── create-app.ts │ │ │ ├── create.command.ts │ │ │ ├── create.module.ts │ │ │ ├── helpers │ │ │ │ ├── copy.ts │ │ │ │ ├── get-pkg-manager.ts │ │ │ │ ├── git.ts │ │ │ │ ├── install.ts │ │ │ │ ├── is-folder-empty.ts │ │ │ │ ├── is-online.ts │ │ │ │ ├── is-writeable.ts │ │ │ │ └── validate-pkg.ts │ │ │ └── templates │ │ │ │ ├── default │ │ │ │ ├── .cursor │ │ │ │ │ └── rules │ │ │ │ │ │ └── global.mdc │ │ │ │ ├── .env.example │ │ │ │ ├── README-template.md │ │ │ │ ├── components.json │ │ │ │ ├── cursorignore │ │ │ │ ├── eslint.config.mjs │ │ │ │ ├── gitignore │ │ │ │ ├── next-env.d.ts │ │ │ │ ├── next.config.ts │ │ │ │ ├── postcss.config.mjs │ │ │ │ ├── public │ │ │ │ │ └── easynext.png │ │ │ │ ├── src │ │ │ │ │ ├── app │ │ │ │ │ │ ├── favicon.ico │ │ │ │ │ │ ├── globals.css │ │ │ │ │ │ ├── layout.tsx │ │ │ │ │ │ ├── page.tsx │ │ │ │ │ │ └── providers.tsx │ │ │ │ │ ├── components │ │ │ │ │ │ └── ui │ │ │ │ │ │ │ ├── accordion.tsx │ │ │ │ │ │ │ ├── avatar.tsx │ │ │ │ │ │ │ ├── badge.tsx │ │ │ │ │ │ │ ├── button.tsx │ │ │ │ │ │ │ ├── card.tsx │ │ │ │ │ │ │ ├── checkbox.tsx │ │ │ │ │ │ │ ├── dropdown-menu.tsx │ │ │ │ │ │ │ ├── file-upload.tsx │ │ │ │ │ │ │ ├── form.tsx │ │ │ │ │ │ │ ├── input.tsx │ │ │ │ │ │ │ ├── label.tsx │ │ │ │ │ │ │ ├── select.tsx │ │ │ │ │ │ │ ├── separator.tsx │ │ │ │ │ │ │ ├── sheet.tsx │ │ │ │ │ │ │ ├── textarea.tsx │ │ │ │ │ │ │ ├── toast.tsx │ │ │ │ │ │ │ └── toaster.tsx │ │ │ │ │ ├── hooks │ │ │ │ │ │ └── use-toast.ts │ │ │ │ │ └── lib │ │ │ │ │ │ └── utils.ts │ │ │ │ ├── tailwind.config.ts │ │ │ │ └── tsconfig.json │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ ├── doctor │ │ │ ├── doctor.command.ts │ │ │ └── doctor.module.ts │ │ ├── gtag │ │ │ ├── gtag.command.ts │ │ │ └── gtag.module.ts │ │ ├── i18n │ │ │ ├── i18n.command.ts │ │ │ └── i18n.module.ts │ │ ├── index.ts │ │ ├── lang │ │ │ ├── lang.command.ts │ │ │ └── lang.module.ts │ │ ├── login │ │ │ └── login.command.ts │ │ ├── sentry │ │ │ ├── sentry.command.ts │ │ │ └── sentry.module.ts │ │ ├── sitemap │ │ │ ├── sitemap.command.ts │ │ │ └── sitemap.module.ts │ │ ├── supabase │ │ │ ├── actions │ │ │ │ └── init.ts │ │ │ ├── supabase.command.ts │ │ │ └── supabase.module.ts │ │ └── version │ │ │ ├── version.command.ts │ │ │ └── version.module.ts │ ├── global │ │ └── config │ │ │ ├── auth.config.ts │ │ │ ├── config.module.ts │ │ │ └── global.config.ts │ ├── output-manager.ts │ └── util │ │ ├── check-root.ts │ │ ├── config │ │ ├── files.ts │ │ ├── get-default.ts │ │ └── global-path.ts │ │ ├── emoji.ts │ │ ├── error-handler.ts │ │ ├── humanize-path.ts │ │ ├── i18n │ │ └── index.ts │ │ └── output │ │ ├── create-output.ts │ │ ├── erase-lines.ts │ │ ├── highlight.ts │ │ ├── index.ts │ │ ├── link.ts │ │ └── wait.ts │ ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json │ ├── tsconfig.build.json │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tsconfig.base.json ├── turbo.json └── webpage ├── .cursorignore ├── .gitignore ├── README.md ├── components.json ├── eslint.config.mjs ├── next.config.ts ├── package.json ├── postcss.config.mjs ├── public ├── channelio.png ├── cursormatfia.png ├── fastcampus.svg ├── file.svg ├── globe.svg ├── images │ ├── home │ │ ├── background │ │ │ ├── 0.png │ │ │ ├── 1.png │ │ │ ├── 10.png │ │ │ ├── 11.png │ │ │ ├── 12.webp │ │ │ ├── 13.webp │ │ │ ├── 14.webp │ │ │ ├── 15.webp │ │ │ ├── 16.webp │ │ │ ├── 17.webp │ │ │ ├── 18.webp │ │ │ ├── 2.png │ │ │ ├── 3.png │ │ │ ├── 4.png │ │ │ ├── 5.png │ │ │ ├── 6.png │ │ │ ├── 7.png │ │ │ ├── 8.png │ │ │ └── 9.png │ │ └── templates │ │ │ ├── community.png │ │ │ ├── landing.gif │ │ │ └── saas.gif │ └── logo │ │ ├── clarity.png │ │ ├── ga.svg │ │ ├── nextjs.svg │ │ ├── supabase.svg │ │ ├── tailwindcss.svg │ │ └── typescript.svg ├── logo.png ├── next.svg ├── shadcn.svg ├── vercel.svg └── window.svg ├── src ├── app │ ├── api │ │ ├── premium │ │ │ ├── cli-login │ │ │ │ └── route.ts │ │ │ ├── coupon │ │ │ │ └── route.ts │ │ │ └── template-url │ │ │ │ └── route.ts │ │ └── register-coupon │ │ │ └── route.ts │ ├── globals.css │ ├── icon.png │ ├── layout.tsx │ ├── page.tsx │ ├── premium │ │ ├── _CouponDialog.tsx │ │ ├── _PremiumDialog.tsx │ │ ├── guide │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── page.tsx │ └── providers.tsx ├── components │ ├── Footer.tsx │ ├── Header.tsx │ ├── TechStackCard.tsx │ ├── TechStackSection.tsx │ └── ui │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── checkbox.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── file-upload.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── textarea.tsx │ │ ├── toast.tsx │ │ └── toaster.tsx ├── hooks │ └── use-toast.ts └── lib │ ├── supabase │ ├── client.ts │ └── server.ts │ └── utils.ts ├── supabase ├── .gitignore ├── config.toml └── migrations │ ├── 0000_create_accounts_table.sql │ └── 0001_create_templates_table.sql ├── tailwind.config.ts └── tsconfig.json /.cursorignore: -------------------------------------------------------------------------------- 1 | /pnpm-lock.yaml -------------------------------------------------------------------------------- /.cursorrules: -------------------------------------------------------------------------------- 1 | # Senior Full-Stack Developer Guidelines 2 | 3 | ## Solution Process: 4 | 5 | 1. Rephrase Input: Transform to clear, professional prompt. 6 | 2. Analyze & Strategize: Identify issues, outline solutions, define output format. 7 | 3. Develop Solution: 8 | - "As a senior-level developer, I need to [rephrased prompt]. To accomplish this, I need to:" 9 | - List steps numerically. 10 | - "To resolve these steps, I need the following solutions:" 11 | - List solutions with bullet points. 12 | 4. Validate Solution: Review, refine, test against edge cases. 13 | 5. Evaluate Progress: 14 | - If incomplete: Pause, inform user, await input. 15 | - If satisfactory: Proceed to final output. 16 | 6. Prepare Final Output: 17 | - ASCII title 18 | - Problem summary and approach 19 | - Step-by-step solution with relevant code snippets 20 | - Format code changes: 21 | ```language:path/to/file 22 | // ... existing code ... 23 | function exampleFunction() { 24 | // Modified or new code here 25 | } 26 | // ... existing code ... 27 | ``` 28 | - Use appropriate formatting 29 | - Describe modifications 30 | - Conclude with potential improvements 31 | 32 | ## Key Mindsets: 33 | 34 | 1. Simplicity 35 | 2. Readability 36 | 3. Maintainability 37 | 4. Testability 38 | 5. Reusability 39 | 6. Functional Paradigm 40 | 7. Pragmatism 41 | 42 | ## Code Guidelines: 43 | 44 | 1. Early Returns 45 | 2. Conditional Classes over ternary 46 | 3. Descriptive Names 47 | 4. Constants > Functions 48 | 5. DRY 49 | 6. Functional & Immutable 50 | 7. Minimal Changes 51 | 8. Pure Functions 52 | 9. Composition over inheritance 53 | 54 | ## Functional Programming: 55 | 56 | - Avoid Mutation 57 | - Use Map, Filter, Reduce 58 | - Currying and Partial Application 59 | - Immutability 60 | 61 | ## Performance: 62 | 63 | - Avoid Premature Optimization 64 | - Profile Before Optimizing 65 | - Optimize Judiciously 66 | - Document Optimizations 67 | 68 | ## Comments & Documentation: 69 | 70 | - Comment function purpose 71 | - Use JSDoc for JS 72 | - Document "why" not "what" 73 | 74 | ## Function Ordering: 75 | 76 | - Higher-order functionality first 77 | - Group related functions 78 | 79 | ## Handling Bugs: 80 | 81 | - Use TODO: and FIXME: comments 82 | 83 | ## Error Handling: 84 | 85 | - Use appropriate techniques 86 | - Prefer returning errors over exceptions 87 | 88 | ## Testing: 89 | 90 | - Unit tests for core functionality 91 | - Consider integration and end-to-end tests 92 | 93 | You are a senior full-stack developer, one of those rare 10x devs. Your focus: clean, maintainable, high-quality code. 94 | Apply these principles judiciously, considering project and team needs. 95 | 96 | ## Commit Message: 97 | 98 | - Each commit message should include the following: 99 | 100 | ``` 101 | (): 102 | ``` 103 | 104 | - Write subject in Korean, type and scope in English. 105 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | 19 | - name: Set up Node.js 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: '20' 23 | 24 | - name: Cache pnpm store 25 | uses: actions/cache@v3 26 | with: 27 | path: ~/.pnpm-store 28 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 29 | restore-keys: | 30 | ${{ runner.os }}-pnpm-store- 31 | 32 | - name: Install pnpm 33 | run: npm install -g pnpm 34 | 35 | - name: Install dependencies 36 | run: pnpm install 37 | 38 | - name: Run lint 39 | run: pnpm run lint 40 | 41 | - name: Build project 42 | run: pnpm run build 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | # and https://github.com/github/gitignore for examples 3 | 4 | # local env files (followinf dotenv-flow / nextjs convention) 5 | 6 | .env.local 7 | .env.*.local 8 | 9 | # security: atlassian/changeset 10 | 11 | **/.netrc 12 | 13 | # dependencies 14 | node_modules 15 | .pnpm-store/ 16 | .pnp.* 17 | 18 | # testing 19 | /coverage 20 | .out/ 21 | 22 | # Turbo 23 | .turbo 24 | 25 | # Debug 26 | 27 | **/.debug 28 | 29 | # Build directories (next.js...) 30 | /.next/ 31 | /out/ 32 | /build 33 | /dist/ 34 | 35 | # Cache 36 | *.tsbuildinfo 37 | **/.eslintcache 38 | .cache/* 39 | .swc/ 40 | 41 | # Misc 42 | .DS_Store 43 | *.pem 44 | 45 | # Debug 46 | npm-debug.log* 47 | pnpm-debug.log* 48 | 49 | 50 | # IDE 51 | **/.idea/* 52 | !**/.idea/modules.xml 53 | !**/.idea/*.iml 54 | .project 55 | .classpath 56 | *.launch 57 | *.sublime-workspace 58 | 59 | .vscode/* 60 | !.vscode/settings.json 61 | !.vscode/tasks.json 62 | !.vscode/launch.json 63 | !.vscode/extensions.json 64 | !.vscode/*.code-snippets 65 | 66 | # Docker overrides 67 | 68 | ./docker-compose.override.yml 69 | 70 | # Deployment platforms 71 | 72 | .vercel 73 | 74 | # LocalStorage assets 75 | 76 | **/.assets -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | strict-peer-dependencies=false 3 | auto-install-peers=true 4 | lockfile=true 5 | # force use npmjs.org registry 6 | registry=https://registry.npmjs.org/ 7 | save-prefix='' -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## 1. Install dependencies 4 | 5 | ```bash 6 | pnpm install 7 | ``` 8 | 9 | ## 2. Run the CLI 10 | 11 | ```bash 12 | cd packages/cli 13 | 14 | pnpm build 15 | 16 | node dist/src/cli.js --help 17 | ``` 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2025 EasyNext 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ./packages/cli/README.md -------------------------------------------------------------------------------- /monorepo.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "name": "root", 5 | "path": "." 6 | }, 7 | { 8 | "name": "cli", 9 | "path": "packages/cli" 10 | } 11 | ], 12 | "extensions": { 13 | "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] 14 | }, 15 | "settings": { 16 | "editor.formatOnSave": true, 17 | "editor.defaultFormatter": "esbenp.prettier-vscode", 18 | "editor.codeActionsOnSave": { 19 | "source.fixAll.eslint": "explicit" 20 | }, 21 | // Disable vscode formatting for js,jsx,ts,tsx files 22 | // to allow dbaeumer.vscode-eslint to format them 23 | "[javascript]": { 24 | "editor.formatOnSave": false 25 | }, 26 | "eslint.alwaysShowStatus": true, 27 | // https://github.com/Microsoft/vscode-eslint#mono-repository-setup 28 | "eslint.workingDirectories": ["./packages/cli"], 29 | "[typescript]": { 30 | "editor.defaultFormatter": "esbenp.prettier-vscode" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "easynext", 3 | "version": "1.0.1", 4 | "main": "index.js", 5 | "scripts": { 6 | "dev": "turbo dev", 7 | "lint": "turbo lint", 8 | "build": "turbo build" 9 | }, 10 | "author": "", 11 | "license": "MIT", 12 | "description": "", 13 | "devDependencies": { 14 | "@vercel/style-guide": "6.0.0", 15 | "eslint": "^9.0.0", 16 | "eslint-config-prettier": "^9.0.0", 17 | "eslint-plugin-prettier": "^5.0.0", 18 | "prettier": "^3.0.0", 19 | "rimraf": "^6.0.1", 20 | "turbo": "2.3.3", 21 | "typescript": "^5.1.3", 22 | "typescript-eslint": "^8.18.2" 23 | }, 24 | "packageManager": "pnpm@9.15.1", 25 | "engines": { 26 | "node": ">=20" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/cli/.cursorignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml -------------------------------------------------------------------------------- /packages/cli/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | /build 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | pnpm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | lerna-debug.log* 14 | 15 | # OS 16 | .DS_Store 17 | 18 | # Tests 19 | /coverage 20 | /.nyc_output 21 | 22 | # IDEs and editors 23 | /.idea 24 | .project 25 | .classpath 26 | .c9/ 27 | *.launch 28 | .settings/ 29 | *.sublime-workspace 30 | 31 | # IDE - VSCode 32 | .vscode/* 33 | !.vscode/settings.json 34 | !.vscode/tasks.json 35 | !.vscode/launch.json 36 | !.vscode/extensions.json 37 | 38 | # dotenv environment variable files 39 | .env 40 | .env.development.local 41 | .env.test.local 42 | .env.production.local 43 | .env.local 44 | 45 | # temp directory 46 | .temp 47 | .tmp 48 | 49 | # Runtime data 50 | pids 51 | *.pid 52 | *.seed 53 | *.pid.lock 54 | 55 | # Diagnostic reports (https://nodejs.org/api/report.html) 56 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 57 | -------------------------------------------------------------------------------- /packages/cli/.npmrc: -------------------------------------------------------------------------------- 1 | message="chore: :rocket: release %s" 2 | -------------------------------------------------------------------------------- /packages/cli/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /packages/cli/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.0.7 4 | 5 | - add supabase command 6 | 7 | ## 0.0.6 8 | 9 | - add version command 10 | 11 | ## 0.0.5 12 | 13 | - add create command 14 | 15 | ## 0.0.2 16 | 17 | - add lang command 18 | 19 | ## 0.0.1 20 | 21 | - add doctor command 22 | -------------------------------------------------------------------------------- /packages/cli/README.ko.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | EasyNext 로고 5 | 6 | 7 |

EasyNext

8 | 9 | NPM 버전 10 | 라이선스 11 | 12 |
13 | 14 | > Cursor, Vercel, shadcn-ui, tailwindcss, Supabase 사용을 가정합니다. 15 | 16 | 가장 쉽게 시작할 수 있는 Next.js ⚡️ 17 | 18 | ### EasyNext의 장점 19 | 20 | Next.js 프로젝트에 필요한 모든 설정을 쉽게 할 수 있습니다.
21 | 모두 한국어로 사용할 수 있어요! 22 | 23 | - 필수 세팅이 포함된 프로젝트 생성 24 | - 구글/애플/카카오/네이버 인증 설정 25 | - Supabase 설정 및 연동 26 | - Google Sheet 모듈 설정 27 | - 더 많은 기능 추가 예정 👀 28 | 29 | ## 설치 30 | 31 | ```bash 32 | # (권장) npm 사용 시 33 | $ npm install -g @easynext/cli 34 | 35 | # 혹시라도 yarn, pnpm을 사용하고 있다면 36 | $ yarn global add @easynext/cli 37 | $ pnpm add -g @easynext/cli 38 | ``` 39 | 40 | ## 사용 가능한 명령어 41 | 42 | ```bash 43 | # 설치된 툴 확인 44 | $ easynext doctor 45 | 46 | # 언어 설정 47 | $ easynext lang <'ko' | 'en'> 48 | 49 | # 새 프로젝트 생성 50 | $ easynext create 51 | 52 | # supabase 사용 설정 53 | $ easynext supabase 54 | ``` 55 | 56 | ## 곧 추가될 명령어 57 | 58 | ```bash 59 | # 로그인 기능 추가 (구글, 애플, 카카오, 네이버) 60 | $ easynext auth google|apple|kakao|naver 61 | 62 | # next-ui 사용 설정 63 | $ easynext next-ui 64 | 65 | # Google Sheet 연동 설정 66 | $ easynext google-sheet 67 | ``` 68 | 69 | ## 라이선스 70 | 71 | [MIT](https://github.com/easynextjs/easynext/blob/main/LICENSE). 72 | -------------------------------------------------------------------------------- /packages/cli/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | EasyNext logo 5 | 6 | 7 |

EasyNext

8 | 9 | NPM version 10 | License 11 | 12 |
13 | 14 | > Depends on Cursor, Vercel, shadcn-ui, tailwindcss, Supabase 15 | 16 | Easiest way to start Next.js project ⚡️ [한글](https://github.com/easynextjs/easynext/blob/main/packages/cli/README.ko.md) 17 | 18 | ### With EasyNext, You can ... 19 | 20 | Do all the things you need to do to start a Next.js project,
21 | in your own language! (English, Korean) 22 | 23 | - Create a Ready-to-use project 24 | - Set up Google/Apple/Kakao/Naver Auth 25 | - Set up and link Supabase 26 | - Set up Google Sheet modules 27 | - More to come... 👀 28 | 29 | ## Prerequisites 30 | 31 | - Node.js 20+ 32 | 33 | ## Installiation 34 | 35 | ```bash 36 | # (recommended) use npm 37 | $ npm install -g @easynext/cli 38 | 39 | # if you are using yarn, pnpm 40 | $ yarn global add @easynext/cli 41 | $ pnpm add -g @easynext/cli 42 | ``` 43 | 44 | ## Available commands 45 | 46 | ```bash 47 | # Check tools installation 48 | $ easynext doctor 49 | 50 | # Set language 51 | $ easynext lang <'ko' | 'en'> 52 | 53 | # Create a new project 54 | $ easynext create 55 | 56 | # Init supabase 57 | $ easynext supabase 58 | 59 | # Init next-auth 60 | $ easynext auth 61 | or 62 | $ easynext auth init 63 | 64 | # Set oauth provider 65 | $ easynext auth <'kakao' | 'idpw'> 66 | 67 | # Set Up Google Analytics 68 | $ easynext gtag 69 | 70 | # Set Up Microsoft Clarity 71 | $ easynext clarity 72 | 73 | # Set Up ChannelTalk 74 | $ easynext channeltalk 75 | 76 | # Set Up Sentry 77 | $ easynext sentry 78 | ``` 79 | 80 | ## Coming Soon Commands 81 | 82 | ```bash 83 | 84 | 85 | # Init next-ui 86 | $ easynext next-ui 87 | 88 | # Add Google Sheet modules 89 | $ easynext google-sheet 90 | ``` 91 | 92 | ## License 93 | 94 | [MIT](https://github.com/easynextjs/easynext/blob/main/LICENSE). 95 | -------------------------------------------------------------------------------- /packages/cli/bin/cli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../dist/src/cli.js') -------------------------------------------------------------------------------- /packages/cli/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslint from '@eslint/js'; 2 | import tseslint from 'typescript-eslint'; 3 | import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; 4 | import globals from 'globals'; 5 | 6 | /** @type {import('typescript-eslint').Config} */ 7 | const config = tseslint.config( 8 | { 9 | ignores: ['eslint.config.mjs', 'src/commands/create/templates/default/'], 10 | }, 11 | eslint.configs.recommended, 12 | ...tseslint.configs.recommendedTypeChecked, 13 | eslintPluginPrettierRecommended, 14 | { 15 | languageOptions: { 16 | globals: { 17 | ...globals.node, 18 | ...globals.jest, 19 | }, 20 | ecmaVersion: 5, 21 | sourceType: 'module', 22 | parserOptions: { 23 | projectService: true, 24 | tsconfigRootDir: import.meta.dirname, 25 | }, 26 | }, 27 | }, 28 | { 29 | rules: { 30 | '@typescript-eslint/no-explicit-any': 'off', 31 | '@typescript-eslint/no-floating-promises': 'off', 32 | '@typescript-eslint/no-unsafe-argument': 'warn', 33 | '@typescript-eslint/require-await': 'off', 34 | '@typescript-eslint/no-unsafe-assignment': 'off', 35 | '@typescript-eslint/no-unsafe-call': 'off', 36 | '@typescript-eslint/no-unsafe-member-access': 'off', 37 | '@typescript-eslint/no-unsafe-return': 'off', 38 | }, 39 | }, 40 | ); 41 | 42 | export default config; 43 | -------------------------------------------------------------------------------- /packages/cli/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | }, 8 | "entryFile": "cli" 9 | } 10 | -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@easynext/cli", 3 | "version": "0.1.35", 4 | "description": "", 5 | "author": "", 6 | "publishConfig": { 7 | "access": "public" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/easynextjs/easynext.git" 12 | }, 13 | "license": "MIT", 14 | "bugs": { 15 | "url": "https://github.com/easynextjs/easynext/issues" 16 | }, 17 | "homepage": "https://github.com/easynextjs/easynext#readme", 18 | "main": "dist/src/cli.js", 19 | "types": "dist/src/cli.d.ts", 20 | "bin": { 21 | "easynext": "./bin/cli" 22 | }, 23 | "files": [ 24 | "dist" 25 | ], 26 | "scripts": { 27 | "prebuild": "rimraf dist", 28 | "build": "nest build", 29 | "postbuild": "copyfiles -u 4 -a \"./src/commands/create/templates/**/*\" dist/src/commands/create/templates", 30 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 31 | "cli": "ts-node src/cli.ts", 32 | "start": "nest start", 33 | "dev": "nest start --watch", 34 | "start:debug": "nest start --debug --watch", 35 | "start:prod": "node dist/main", 36 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 37 | "test": "jest", 38 | "test:watch": "jest --watch", 39 | "test:cov": "jest --coverage", 40 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 41 | "test:e2e": "jest --config ./test/jest-e2e.json", 42 | "prepublish:npm": "pnpm build", 43 | "publish:npm": "pnpm npm publish" 44 | }, 45 | "dependencies": { 46 | "@nestjs/common": "^10.0.0", 47 | "@nestjs/core": "^10.0.0", 48 | "@supabase/supabase-js": "^2.39.0", 49 | "@vercel/error-utils": "2.0.3", 50 | "adm-zip": "0.5.16", 51 | "ansi-escapes": "4.3.2", 52 | "arg": "5.0.2", 53 | "async-sema": "3.1.1", 54 | "axios": "^1.6.2", 55 | "chalk": "4", 56 | "cross-spawn": "7.0.6", 57 | "es-toolkit": "1.31.0", 58 | "fast-glob": "3.3.2", 59 | "fs-extra": "11.2.0", 60 | "load-json-file": "3.0.0", 61 | "nest-commander": "^3.15.0", 62 | "ora": "3.4.0", 63 | "reflect-metadata": "^0.2.0", 64 | "rxjs": "^7.8.1", 65 | "semver": "^7.6.3", 66 | "supports-hyperlinks": "3.1.0", 67 | "tar": "^6.2.0", 68 | "validate-npm-package-name": "6.0.0", 69 | "write-json-file": "2.2.0", 70 | "xdg-app-paths": "5.1.0" 71 | }, 72 | "devDependencies": { 73 | "@eslint/eslintrc": "^3.2.0", 74 | "@nestjs/cli": "^10.0.0", 75 | "@nestjs/schematics": "^10.0.0", 76 | "@nestjs/testing": "^10.0.0", 77 | "@types/fs-extra": "11.0.4", 78 | "@types/jest": "^29.5.2", 79 | "@types/load-json-file": "2.0.7", 80 | "@types/node": "^20.3.1", 81 | "@types/semver": "^7.5.8", 82 | "@types/supertest": "^6.0.0", 83 | "@types/write-json-file": "2.2.1", 84 | "@typescript-eslint/eslint-plugin": "^8.0.0", 85 | "@typescript-eslint/parser": "^8.0.0", 86 | "copyfiles": "2.4.1", 87 | "globals": "^15.14.0", 88 | "jest": "^29.5.0", 89 | "source-map-support": "^0.5.21", 90 | "supertest": "^7.0.0", 91 | "ts-jest": "^29.1.0", 92 | "ts-loader": "^9.4.3", 93 | "ts-node": "^10.9.1", 94 | "tsconfig-paths": "^4.2.0", 95 | "typescript": "5.7.3" 96 | }, 97 | "jest": { 98 | "moduleFileExtensions": [ 99 | "js", 100 | "json", 101 | "ts" 102 | ], 103 | "rootDir": "src", 104 | "testRegex": ".*\\.spec\\.ts$", 105 | "transform": { 106 | "^.+\\.(t|j)s$": "ts-jest" 107 | }, 108 | "collectCoverageFrom": [ 109 | "**/*.(t|j)s" 110 | ], 111 | "coverageDirectory": "../coverage", 112 | "testEnvironment": "node" 113 | }, 114 | "engines": { 115 | "node": ">=20" 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /packages/cli/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { commandModules } from './commands'; 3 | import { ConfigModule } from './global/config/config.module'; 4 | 5 | @Module({ 6 | imports: [...commandModules, ConfigModule.forRootAsync()], 7 | }) 8 | export class AppModule {} 9 | -------------------------------------------------------------------------------- /packages/cli/src/cli.ts: -------------------------------------------------------------------------------- 1 | import { isError } from '@vercel/error-utils'; 2 | 3 | try { 4 | // Test to see if cwd has been deleted before 5 | // importing 3rd party packages that might need cwd. 6 | process.cwd(); 7 | } catch (err: unknown) { 8 | if (isError(err) && err.message.includes('uv_cwd')) { 9 | console.error('Error: The current working directory does not exist.'); 10 | process.exit(1); 11 | } 12 | } 13 | 14 | import { CommandFactory } from 'nest-commander'; 15 | import { AppModule } from './app.module'; 16 | import * as eh from './util/error-handler'; 17 | 18 | import { version as PACKAGE_VERSION } from '../package.json'; 19 | import output from './output-manager'; 20 | 21 | async function main() { 22 | output.info(`EasyNext CLI v${PACKAGE_VERSION}`); 23 | 24 | await CommandFactory.run(AppModule, output); 25 | 26 | return 0; 27 | } 28 | 29 | process.on('unhandledRejection', eh.handleRejection); 30 | process.on('uncaughtException', eh.handleUnexpected); 31 | 32 | const handleSigTerm = () => process.exit(0); 33 | 34 | process.on('SIGINT', handleSigTerm); 35 | process.on('SIGTERM', handleSigTerm); 36 | 37 | main().then((code) => { 38 | process.exitCode = code; 39 | }); 40 | -------------------------------------------------------------------------------- /packages/cli/src/commands/abstract.command.ts: -------------------------------------------------------------------------------- 1 | import { CommandRunner } from 'nest-commander'; 2 | 3 | export abstract class AbstractCommand extends CommandRunner { 4 | abstract run(passedParam: string[]): Promise; 5 | } 6 | -------------------------------------------------------------------------------- /packages/cli/src/commands/adsense/adsense.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AdsenseCommand } from './adsense.command'; 3 | 4 | @Module({ 5 | providers: [AdsenseCommand], 6 | }) 7 | export class AdsenseModule {} 8 | -------------------------------------------------------------------------------- /packages/cli/src/commands/auth/auth.command.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandRunner } from 'nest-commander'; 2 | import output from '../../output-manager'; 3 | import { initAuth } from './actions/init'; 4 | import { addIdpw } from './actions/idpw'; 5 | import { addKakao } from './actions/kakao'; 6 | import { assertRoot } from '@/util/check-root'; 7 | 8 | @Command({ 9 | name: 'auth', 10 | description: 'Next-Auth 인증 설정', 11 | arguments: '[action]', 12 | }) 13 | export class AuthCommand extends CommandRunner { 14 | async run(passedParam: string[]): Promise { 15 | assertRoot(); 16 | 17 | const action = passedParam[0] || 'init'; 18 | 19 | switch (action) { 20 | case 'init': 21 | await initAuth(); 22 | break; 23 | case 'idpw': 24 | await addIdpw(); 25 | break; 26 | case 'kakao': 27 | await addKakao(); 28 | break; 29 | default: 30 | output.error(`알 수 없는 액션: ${action}`); 31 | output.info('사용 가능한 액션: init, idpw, kakao'); 32 | process.exit(1); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/cli/src/commands/auth/index.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AuthCommand } from './auth.command'; 3 | import { ConfigModule } from '@/global/config/config.module'; 4 | 5 | @Module({ 6 | imports: [ConfigModule], 7 | providers: [AuthCommand], 8 | exports: [AuthCommand], 9 | }) 10 | export class AuthModule {} 11 | 12 | export * from './auth.command'; 13 | export * from './actions/kakao'; 14 | -------------------------------------------------------------------------------- /packages/cli/src/commands/channelio/channelio.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ChannelIOCommand } from './channelio.command'; 3 | 4 | @Module({ 5 | providers: [ChannelIOCommand], 6 | }) 7 | export class ChannelIOodule {} 8 | -------------------------------------------------------------------------------- /packages/cli/src/commands/clarity/clarity.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ClarityCommand } from './clarity.command'; 3 | 4 | @Module({ 5 | providers: [ClarityCommand], 6 | }) 7 | export class ClarityModule {} 8 | -------------------------------------------------------------------------------- /packages/cli/src/commands/create/create-app.ts: -------------------------------------------------------------------------------- 1 | import { mkdirSync } from 'node:fs'; 2 | import { basename, dirname, join, resolve } from 'node:path'; 3 | import { cyan, green } from 'chalk'; 4 | import type { PackageManager } from './helpers/get-pkg-manager'; 5 | import { tryGitInit } from './helpers/git'; 6 | import { isFolderEmpty } from './helpers/is-folder-empty'; 7 | import { getOnline } from './helpers/is-online'; 8 | import { isWriteable } from './helpers/is-writeable'; 9 | import i18n from '@/util/i18n'; 10 | 11 | import type { TemplateType } from './templates'; 12 | import { installTemplate } from './templates'; 13 | 14 | export class DownloadError extends Error {} 15 | 16 | export async function createApp({ 17 | appPath, 18 | packageManager, 19 | skipInstall, 20 | turbopack, 21 | disableGit, 22 | }: { 23 | appPath: string; 24 | packageManager: PackageManager; 25 | skipInstall: boolean; 26 | turbopack: boolean; 27 | disableGit?: boolean; 28 | }): Promise { 29 | const template: TemplateType = 'default'; 30 | 31 | const root = resolve(appPath); 32 | 33 | if (!(await isWriteable(dirname(root)))) { 34 | console.error(i18n.t('create.not_writable')); 35 | console.error(i18n.t('create.no_permissions')); 36 | process.exit(1); 37 | } 38 | 39 | const appName = basename(root); 40 | 41 | mkdirSync(root, { recursive: true }); 42 | if (!isFolderEmpty(root, appName)) { 43 | process.exit(1); 44 | } 45 | 46 | const useYarn = packageManager === 'yarn'; 47 | const isOnline = !useYarn || (await getOnline()); 48 | const originalDirectory = process.cwd(); 49 | 50 | console.log(`${i18n.t('create.creating_app')} ${green(root)}.`); 51 | console.log(); 52 | 53 | process.chdir(root); 54 | 55 | const hasPackageJson = false; 56 | 57 | await installTemplate({ 58 | appName, 59 | root, 60 | template, 61 | packageManager, 62 | isOnline, 63 | skipInstall, 64 | turbopack, 65 | }); 66 | 67 | if (disableGit) { 68 | console.log(i18n.t('create.skip_git')); 69 | console.log(); 70 | } else if (tryGitInit(root)) { 71 | console.log(i18n.t('create.git_initialized')); 72 | console.log(); 73 | } 74 | 75 | let cdpath: string; 76 | if (join(originalDirectory, appName) === appPath) { 77 | cdpath = appName; 78 | } else { 79 | cdpath = appPath; 80 | } 81 | 82 | console.log( 83 | `${green(i18n.t('info.success'))} ${i18n.t('create.success_created')} ${appName} ${i18n.t('create.success_at')} ${appPath}`, 84 | ); 85 | 86 | if (hasPackageJson) { 87 | console.log(i18n.t('create.run_commands')); 88 | console.log(); 89 | console.log(cyan(` ${packageManager} ${useYarn ? '' : 'run '}dev`)); 90 | console.log(` ${i18n.t('create.run_dev')}`); 91 | console.log(); 92 | console.log(cyan(` ${packageManager} ${useYarn ? '' : 'run '}build`)); 93 | console.log(` ${i18n.t('create.run_build')}`); 94 | console.log(); 95 | console.log(cyan(` ${packageManager} start`)); 96 | console.log(` ${i18n.t('create.run_start')}`); 97 | console.log(); 98 | console.log(i18n.t('create.suggest_begin')); 99 | console.log(); 100 | console.log(cyan(' cd'), cdpath); 101 | console.log(` ${cyan(`${packageManager} ${useYarn ? '' : 'run '}dev`)}`); 102 | } 103 | console.log(); 104 | } 105 | -------------------------------------------------------------------------------- /packages/cli/src/commands/create/create.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { CreateCommand } from './create.command'; 3 | 4 | @Module({ 5 | providers: [CreateCommand], 6 | }) 7 | export class CreateModule {} 8 | -------------------------------------------------------------------------------- /packages/cli/src/commands/create/helpers/copy.ts: -------------------------------------------------------------------------------- 1 | import { resolve, dirname, basename, join } from 'node:path'; 2 | import { copyFile, mkdir } from 'node:fs/promises'; 3 | import { async as glob } from 'fast-glob'; 4 | 5 | interface CopyOption { 6 | cwd?: string; 7 | rename?: (basename: string) => string; 8 | parents?: boolean; 9 | } 10 | 11 | const identity = (x: string) => x; 12 | 13 | export const copy = async ( 14 | src: string | string[], 15 | dest: string, 16 | { cwd, rename = identity, parents = true }: CopyOption = {}, 17 | ) => { 18 | const source = typeof src === 'string' ? [src] : src; 19 | 20 | if (source.length === 0 || !dest) { 21 | throw new TypeError('`src` and `dest` are required'); 22 | } 23 | 24 | const sourceFiles = await glob(source, { 25 | cwd, 26 | dot: true, 27 | absolute: false, 28 | stats: false, 29 | }); 30 | 31 | const destRelativeToCwd = cwd ? resolve(cwd, dest) : dest; 32 | 33 | return Promise.all( 34 | sourceFiles.map(async (p: string) => { 35 | const dirName = dirname(p); 36 | const baseName = rename(basename(p)); 37 | 38 | const from = cwd ? resolve(cwd, p) : p; 39 | const to = parents 40 | ? join(destRelativeToCwd, dirName, baseName) 41 | : join(destRelativeToCwd, baseName); 42 | 43 | // Ensure the destination directory exists 44 | await mkdir(dirname(to), { recursive: true }); 45 | 46 | return copyFile(from, to); 47 | }), 48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /packages/cli/src/commands/create/helpers/get-pkg-manager.ts: -------------------------------------------------------------------------------- 1 | export type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun'; 2 | 3 | export function getPkgManager(): PackageManager { 4 | const userAgent = process.env.npm_config_user_agent || ''; 5 | 6 | if (userAgent.startsWith('yarn')) { 7 | return 'yarn'; 8 | } 9 | 10 | if (userAgent.startsWith('pnpm')) { 11 | return 'pnpm'; 12 | } 13 | 14 | if (userAgent.startsWith('bun')) { 15 | return 'bun'; 16 | } 17 | 18 | return 'npm'; 19 | } 20 | -------------------------------------------------------------------------------- /packages/cli/src/commands/create/helpers/git.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'node:child_process'; 2 | import { join } from 'node:path'; 3 | import { rmSync } from 'node:fs'; 4 | import { noop } from 'es-toolkit'; 5 | 6 | function isInGitRepository(): boolean { 7 | try { 8 | execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' }); 9 | return true; 10 | } catch { 11 | return false; 12 | } 13 | } 14 | 15 | function isInMercurialRepository(): boolean { 16 | try { 17 | execSync('hg --cwd . root', { stdio: 'ignore' }); 18 | return true; 19 | } catch { 20 | return false; 21 | } 22 | } 23 | 24 | function isDefaultBranchSet(): boolean { 25 | try { 26 | execSync('git config init.defaultBranch', { stdio: 'ignore' }); 27 | return true; 28 | } catch { 29 | return false; 30 | } 31 | } 32 | 33 | export function tryGitInit(root: string): boolean { 34 | let didInit = false; 35 | try { 36 | execSync('git --version', { stdio: 'ignore' }); 37 | if (isInGitRepository() || isInMercurialRepository()) { 38 | return false; 39 | } 40 | 41 | execSync('git init', { stdio: 'ignore' }); 42 | didInit = true; 43 | 44 | if (!isDefaultBranchSet()) { 45 | execSync('git checkout -b main', { stdio: 'ignore' }); 46 | } 47 | 48 | execSync('git add -A', { stdio: 'ignore' }); 49 | execSync('git commit -m "Initial commit from Create Next App"', { 50 | stdio: 'ignore', 51 | }); 52 | return true; 53 | } catch { 54 | if (didInit) { 55 | try { 56 | rmSync(join(root, '.git'), { recursive: true, force: true }); 57 | } catch { 58 | noop(); 59 | } 60 | } 61 | return false; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/cli/src/commands/create/helpers/install.ts: -------------------------------------------------------------------------------- 1 | import { yellow } from 'chalk'; 2 | import spawn from 'cross-spawn'; 3 | import type { PackageManager } from './get-pkg-manager'; 4 | 5 | /** 6 | * Spawn a package manager installation based on user preference. 7 | * 8 | * @returns A Promise that resolves once the installation is finished. 9 | */ 10 | export async function install( 11 | /** Indicate which package manager to use. */ 12 | packageManager: PackageManager, 13 | /** Indicate whether there is an active Internet connection.*/ 14 | isOnline: boolean, 15 | ): Promise { 16 | const args: string[] = ['install']; 17 | if (!isOnline) { 18 | console.log( 19 | yellow('You appear to be offline.\nFalling back to the local cache.'), 20 | ); 21 | args.push('--offline'); 22 | } 23 | /** 24 | * Return a Promise that resolves once the installation is finished. 25 | */ 26 | return new Promise((resolve, reject) => { 27 | /** 28 | * Spawn the installation process. 29 | */ 30 | const child = spawn(packageManager, args, { 31 | stdio: 'inherit', 32 | env: { 33 | ...process.env, 34 | ADBLOCK: '1', 35 | // we set NODE_ENV to development as pnpm skips dev 36 | // dependencies when production 37 | NODE_ENV: 'development', 38 | DISABLE_OPENCOLLECTIVE: '1', 39 | }, 40 | }); 41 | child.on('close', (code) => { 42 | if (code !== 0) { 43 | reject( 44 | new Error( 45 | `${packageManager} ${args.join(' ')} failed with code ${code}`, 46 | ), 47 | ); 48 | return; 49 | } 50 | resolve(); 51 | }); 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /packages/cli/src/commands/create/helpers/is-folder-empty.ts: -------------------------------------------------------------------------------- 1 | import { lstatSync, readdirSync } from 'node:fs'; 2 | import { join } from 'node:path'; 3 | import { green, blue } from 'chalk'; 4 | import i18n from '@/util/i18n'; 5 | 6 | export function isFolderEmpty(root: string, name: string): boolean { 7 | const validFiles = [ 8 | '.DS_Store', 9 | '.git', 10 | '.gitattributes', 11 | '.gitignore', 12 | '.gitlab-ci.yml', 13 | '.hg', 14 | '.hgcheck', 15 | '.hgignore', 16 | '.idea', 17 | '.npmignore', 18 | '.travis.yml', 19 | 'LICENSE', 20 | 'Thumbs.db', 21 | 'docs', 22 | 'mkdocs.yml', 23 | 'npm-debug.log', 24 | 'yarn-debug.log', 25 | 'yarn-error.log', 26 | 'yarnrc.yml', 27 | '.yarn', 28 | ]; 29 | 30 | const conflicts = readdirSync(root).filter( 31 | (file) => 32 | !validFiles.includes(file) && 33 | // Support IntelliJ IDEA-based editors 34 | !/\.iml$/.test(file), 35 | ); 36 | 37 | if (conflicts.length > 0) { 38 | console.log(i18n.t('create.folder_conflict', green(name))); 39 | console.log(); 40 | for (const file of conflicts) { 41 | try { 42 | const stats = lstatSync(join(root, file)); 43 | if (stats.isDirectory()) { 44 | console.log(` ${blue(file)}/`); 45 | } else { 46 | console.log(` ${file}`); 47 | } 48 | } catch { 49 | console.log(` ${file}`); 50 | } 51 | } 52 | console.log(); 53 | console.log(i18n.t('create.folder_conflict_solution')); 54 | console.log(); 55 | return false; 56 | } 57 | 58 | return true; 59 | } 60 | -------------------------------------------------------------------------------- /packages/cli/src/commands/create/helpers/is-online.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'node:child_process'; 2 | import { lookup } from 'node:dns/promises'; 3 | import url from 'node:url'; 4 | 5 | function getProxy(): string | undefined { 6 | if (process.env.https_proxy) { 7 | return process.env.https_proxy; 8 | } 9 | 10 | try { 11 | const httpsProxy = execSync('npm config get https-proxy').toString().trim(); 12 | return httpsProxy !== 'null' ? httpsProxy : undefined; 13 | } catch { 14 | return; 15 | } 16 | } 17 | 18 | export async function getOnline(): Promise { 19 | try { 20 | await lookup('registry.yarnpkg.com'); 21 | // If DNS lookup succeeds, we are online 22 | return true; 23 | } catch { 24 | // The DNS lookup failed, but we are still fine as long as a proxy has been set 25 | const proxy = getProxy(); 26 | if (!proxy) { 27 | return false; 28 | } 29 | 30 | const { hostname } = url.parse(proxy); 31 | if (!hostname) { 32 | // Invalid proxy URL 33 | return false; 34 | } 35 | 36 | try { 37 | await lookup(hostname); 38 | // If DNS lookup succeeds for the proxy server, we are online 39 | return true; 40 | } catch { 41 | // The DNS lookup for the proxy server also failed, so we are offline 42 | return false; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/cli/src/commands/create/helpers/is-writeable.ts: -------------------------------------------------------------------------------- 1 | import { W_OK } from 'node:constants'; 2 | import { access } from 'node:fs/promises'; 3 | 4 | export async function isWriteable(directory: string): Promise { 5 | try { 6 | await access(directory, W_OK); 7 | return true; 8 | } catch { 9 | return false; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/cli/src/commands/create/helpers/validate-pkg.ts: -------------------------------------------------------------------------------- 1 | import validateProjectName from 'validate-npm-package-name'; 2 | 3 | type ValidateNpmNameResult = 4 | | { 5 | valid: true; 6 | } 7 | | { 8 | valid: false; 9 | problems: string[]; 10 | }; 11 | 12 | export function validateNpmName(name: string): ValidateNpmNameResult { 13 | const nameValidation = validateProjectName(name); 14 | if (nameValidation.validForNewPackages) { 15 | return { valid: true }; 16 | } 17 | 18 | return { 19 | valid: false, 20 | problems: [ 21 | ...(nameValidation.errors || []), 22 | ...(nameValidation.warnings || []), 23 | ], 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /packages/cli/src/commands/create/templates/default/.env.example: -------------------------------------------------------------------------------- 1 | # Rename this file to `.env.local` to use environment variables locally with `next dev` 2 | # https://nextjs.org/docs/app/building-your-application/configuring/environment-variables 3 | MY_HOST="example.com" 4 | -------------------------------------------------------------------------------- /packages/cli/src/commands/create/templates/default/README-template.md: -------------------------------------------------------------------------------- 1 | 이 프로젝트는 [`EasyNext`](https://github.com/easynext/easynext)를 사용해 생성된 [Next.js](https://nextjs.org) 프로젝트입니다. 2 | 3 | ## Getting Started 4 | 5 | 개발 서버를 실행합니다.
6 | 환경에 따른 명령어를 사용해주세요. 7 | 8 | ```bash 9 | npm run dev 10 | # or 11 | yarn dev 12 | # or 13 | pnpm dev 14 | # or 15 | bun dev 16 | ``` 17 | 18 | 브라우저에서 [http://localhost:3000](http://localhost:3000)을 열어 결과를 확인할 수 있습니다. 19 | 20 | `app/page.tsx` 파일을 수정하여 페이지를 편집할 수 있습니다. 파일을 수정하면 자동으로 페이지가 업데이트됩니다. 21 | 22 | ## 기본 포함 라이브러리 23 | 24 | - [Next.js](https://nextjs.org) 25 | - [React](https://react.dev) 26 | - [Tailwind CSS](https://tailwindcss.com) 27 | - [TypeScript](https://www.typescriptlang.org) 28 | - [ESLint](https://eslint.org) 29 | - [Prettier](https://prettier.io) 30 | - [Shadcn UI](https://ui.shadcn.com) 31 | - [Lucide Icon](https://lucide.dev) 32 | - [date-fns](https://date-fns.org) 33 | - [react-use](https://github.com/streamich/react-use) 34 | - [es-toolkit](https://github.com/toss/es-toolkit) 35 | - [Zod](https://zod.dev) 36 | - [React Query](https://tanstack.com/query/latest) 37 | - [React Hook Form](https://react-hook-form.com) 38 | - [TS Pattern](https://github.com/gvergnaud/ts-pattern) 39 | 40 | ## 사용 가능한 명령어 41 | 42 | 한글버전 사용 43 | 44 | ```sh 45 | easynext lang ko 46 | ``` 47 | 48 | 최신버전으로 업데이트 49 | 50 | ```sh 51 | npm i -g @easynext/cli@latest 52 | # or 53 | yarn add -g @easynext/cli@latest 54 | # or 55 | pnpm add -g @easynext/cli@latest 56 | ``` 57 | 58 | Supabase 설정 59 | 60 | ```sh 61 | easynext supabase 62 | ``` 63 | 64 | Next-Auth 설정 65 | 66 | ```sh 67 | easynext auth 68 | 69 | # ID,PW 로그인 70 | easynext auth idpw 71 | # 카카오 로그인 72 | easynext auth kakao 73 | ``` 74 | 75 | 유용한 서비스 연동 76 | 77 | ```sh 78 | # Google Analytics 79 | easynext gtag 80 | 81 | # Microsoft Clarity 82 | easynext clarity 83 | 84 | # ChannelIO 85 | easynext channelio 86 | 87 | # Sentry 88 | easynext sentry 89 | 90 | # Google Adsense 91 | easynext adsense 92 | ``` 93 | -------------------------------------------------------------------------------- /packages/cli/src/commands/create/templates/default/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/cli/src/commands/create/templates/default/cursorignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | yarn.lock 3 | package-lock.json -------------------------------------------------------------------------------- /packages/cli/src/commands/create/templates/default/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | import { FlatCompat } from '@eslint/eslintrc'; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends('next/core-web-vitals', 'next/typescript'), 14 | { 15 | rules: { 16 | '@typescript-eslint/no-empty-object-type': 'off', 17 | '@typescript-eslint/no-explicit-any': 'off', 18 | '@typescript-eslint/no-unused-vars': 'off', 19 | }, 20 | }, 21 | ]; 22 | 23 | export default eslintConfig; 24 | -------------------------------------------------------------------------------- /packages/cli/src/commands/create/templates/default/gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # EasyNext 40 | .easynext 41 | 42 | # typescript 43 | *.tsbuildinfo 44 | next-env.d.ts 45 | -------------------------------------------------------------------------------- /packages/cli/src/commands/create/templates/default/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /packages/cli/src/commands/create/templates/default/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from 'next'; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | eslint: { 6 | ignoreDuringBuilds: true, 7 | }, 8 | images: { 9 | remotePatterns: [ 10 | { 11 | hostname: '**', 12 | }, 13 | ], 14 | }, 15 | }; 16 | 17 | export default nextConfig; 18 | -------------------------------------------------------------------------------- /packages/cli/src/commands/create/templates/default/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | }, 7 | }; 8 | 9 | export default config; 10 | -------------------------------------------------------------------------------- /packages/cli/src/commands/create/templates/default/public/easynext.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easynextjs/easynext/0e179fce6d5dbc3fe06484749cc63ed0912fac17/packages/cli/src/commands/create/templates/default/public/easynext.png -------------------------------------------------------------------------------- /packages/cli/src/commands/create/templates/default/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easynextjs/easynext/0e179fce6d5dbc3fe06484749cc63ed0912fac17/packages/cli/src/commands/create/templates/default/src/app/favicon.ico -------------------------------------------------------------------------------- /packages/cli/src/commands/create/templates/default/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 0 0% 3.9%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 0 0% 3.9%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 0 0% 3.9%; 13 | --primary: 0 0% 9%; 14 | --primary-foreground: 0 0% 98%; 15 | --secondary: 0 0% 96.1%; 16 | --secondary-foreground: 0 0% 9%; 17 | --muted: 0 0% 96.1%; 18 | --muted-foreground: 0 0% 45.1%; 19 | --accent: 0 0% 96.1%; 20 | --accent-foreground: 0 0% 9%; 21 | --destructive: 0 84.2% 60.2%; 22 | --destructive-foreground: 0 0% 98%; 23 | --border: 0 0% 89.8%; 24 | --input: 0 0% 89.8%; 25 | --ring: 0 0% 3.9%; 26 | --radius: 0.5rem; 27 | --chart-1: 12 76% 61%; 28 | --chart-2: 173 58% 39%; 29 | --chart-3: 197 37% 24%; 30 | --chart-4: 43 74% 66%; 31 | --chart-5: 27 87% 67%; 32 | } 33 | 34 | .dark { 35 | --background: 0 0% 3.9%; 36 | --foreground: 0 0% 98%; 37 | --card: 0 0% 3.9%; 38 | --card-foreground: 0 0% 98%; 39 | --popover: 0 0% 3.9%; 40 | --popover-foreground: 0 0% 98%; 41 | --primary: 0 0% 98%; 42 | --primary-foreground: 0 0% 9%; 43 | --secondary: 0 0% 14.9%; 44 | --secondary-foreground: 0 0% 98%; 45 | --muted: 0 0% 14.9%; 46 | --muted-foreground: 0 0% 63.9%; 47 | --accent: 0 0% 14.9%; 48 | --accent-foreground: 0 0% 98%; 49 | --destructive: 0 62.8% 30.6%; 50 | --destructive-foreground: 0 0% 98%; 51 | --border: 0 0% 14.9%; 52 | --input: 0 0% 14.9%; 53 | --ring: 0 0% 83.1%; 54 | --chart-1: 220 70% 50%; 55 | --chart-2: 160 60% 45%; 56 | --chart-3: 30 80% 55%; 57 | --chart-4: 280 65% 60%; 58 | --chart-5: 340 75% 55%; 59 | } 60 | } 61 | 62 | @layer base { 63 | * { 64 | @apply border-border; 65 | } 66 | body { 67 | @apply bg-background text-foreground; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/cli/src/commands/create/templates/default/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import { Geist, Geist_Mono } from 'next/font/google'; 3 | import './globals.css'; 4 | import Providers from './providers'; 5 | 6 | const geistSans = Geist({ 7 | variable: '--font-geist-sans', 8 | subsets: ['latin'], 9 | }); 10 | 11 | const geistMono = Geist_Mono({ 12 | variable: '--font-geist-mono', 13 | subsets: ['latin'], 14 | }); 15 | 16 | export const metadata: Metadata = { 17 | title: 'Create Next App', 18 | description: 'Generated by create next app', 19 | }; 20 | 21 | export default function RootLayout({ 22 | children, 23 | }: Readonly<{ 24 | children: React.ReactNode; 25 | }>) { 26 | return ( 27 | 28 | 31 | {children} 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /packages/cli/src/commands/create/templates/default/src/app/providers.tsx: -------------------------------------------------------------------------------- 1 | // In Next.js, this file would be called: app/providers.tsx 2 | 'use client'; 3 | 4 | // Since QueryClientProvider relies on useContext under the hood, we have to put 'use client' on top 5 | import { 6 | isServer, 7 | QueryClient, 8 | QueryClientProvider, 9 | } from '@tanstack/react-query'; 10 | import { ThemeProvider } from 'next-themes'; 11 | 12 | function makeQueryClient() { 13 | return new QueryClient({ 14 | defaultOptions: { 15 | queries: { 16 | // With SSR, we usually want to set some default staleTime 17 | // above 0 to avoid refetching immediately on the client 18 | staleTime: 60 * 1000, 19 | }, 20 | }, 21 | }); 22 | } 23 | 24 | let browserQueryClient: QueryClient | undefined = undefined; 25 | 26 | function getQueryClient() { 27 | if (isServer) { 28 | // Server: always make a new query client 29 | return makeQueryClient(); 30 | } else { 31 | // Browser: make a new query client if we don't already have one 32 | // This is very important, so we don't re-make a new client if React 33 | // suspends during the initial render. This may not be needed if we 34 | // have a suspense boundary BELOW the creation of the query client 35 | if (!browserQueryClient) browserQueryClient = makeQueryClient(); 36 | return browserQueryClient; 37 | } 38 | } 39 | 40 | export default function Providers({ children }: { children: React.ReactNode }) { 41 | // NOTE: Avoid useState when initializing the query client if you don't 42 | // have a suspense boundary between this and the code that may 43 | // suspend because React will throw away the client on the initial 44 | // render if it suspends and there is no boundary 45 | const queryClient = getQueryClient(); 46 | 47 | return ( 48 | 54 | {children} 55 | 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /packages/cli/src/commands/create/templates/default/src/components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AccordionPrimitive from "@radix-ui/react-accordion" 5 | import { ChevronDown } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Accordion = AccordionPrimitive.Root 10 | 11 | const AccordionItem = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 20 | )) 21 | AccordionItem.displayName = "AccordionItem" 22 | 23 | const AccordionTrigger = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, children, ...props }, ref) => ( 27 | 28 | svg]:rotate-180", 32 | className 33 | )} 34 | {...props} 35 | > 36 | {children} 37 | 38 | 39 | 40 | )) 41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName 42 | 43 | const AccordionContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, children, ...props }, ref) => ( 47 | 52 |
{children}
53 |
54 | )) 55 | 56 | AccordionContent.displayName = AccordionPrimitive.Content.displayName 57 | 58 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } 59 | -------------------------------------------------------------------------------- /packages/cli/src/commands/create/templates/default/src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /packages/cli/src/commands/create/templates/default/src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | }, 24 | ); 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ); 34 | } 35 | 36 | export { Badge, badgeVariants }; 37 | -------------------------------------------------------------------------------- /packages/cli/src/commands/create/templates/default/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | }, 34 | ); 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean; 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button"; 45 | return ( 46 | 51 | ); 52 | }, 53 | ); 54 | Button.displayName = "Button"; 55 | 56 | export { Button, buttonVariants }; 57 | -------------------------------------------------------------------------------- /packages/cli/src/commands/create/templates/default/src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLDivElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |
44 | )) 45 | CardTitle.displayName = "CardTitle" 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLDivElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
56 | )) 57 | CardDescription.displayName = "CardDescription" 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |
64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /packages/cli/src/commands/create/templates/default/src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; 5 | import { Check } from "lucide-react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const Checkbox = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 21 | 24 | 25 | 26 | 27 | )); 28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName; 29 | 30 | export { Checkbox }; 31 | -------------------------------------------------------------------------------- /packages/cli/src/commands/create/templates/default/src/components/ui/file-upload.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import { ChangeEvent, useRef } from "react"; 5 | 6 | interface FileUploadProps extends React.HTMLAttributes { 7 | onFileChange: (file: File) => void; 8 | accept?: string; 9 | } 10 | 11 | export function FileUpload({ 12 | className, 13 | onFileChange, 14 | accept = "image/*", 15 | children, 16 | ...props 17 | }: FileUploadProps) { 18 | const inputRef = useRef(null); 19 | 20 | const handleClick = () => { 21 | inputRef.current?.click(); 22 | }; 23 | 24 | const handleChange = (e: ChangeEvent) => { 25 | const file = e.target.files?.[0]; 26 | if (file) { 27 | onFileChange(file); 28 | } 29 | }; 30 | 31 | return ( 32 |
40 | 47 | {children} 48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /packages/cli/src/commands/create/templates/default/src/components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as LabelPrimitive from '@radix-ui/react-label'; 5 | import { Slot } from '@radix-ui/react-slot'; 6 | import { 7 | Controller, 8 | ControllerProps, 9 | FieldPath, 10 | FieldValues, 11 | FormProvider, 12 | useFormContext, 13 | } from 'react-hook-form'; 14 | 15 | import { cn } from '@/lib/utils'; 16 | import { Label } from '@/components/ui/label'; 17 | 18 | const Form = FormProvider; 19 | 20 | type FormFieldContextValue< 21 | TFieldValues extends FieldValues = FieldValues, 22 | TName extends FieldPath = FieldPath, 23 | > = { 24 | name: TName; 25 | }; 26 | 27 | const FormFieldContext = React.createContext( 28 | {} as FormFieldContextValue, 29 | ); 30 | 31 | const FormField = < 32 | TFieldValues extends FieldValues = FieldValues, 33 | TName extends FieldPath = FieldPath, 34 | >({ 35 | ...props 36 | }: ControllerProps) => { 37 | return ( 38 | 39 | 40 | 41 | ); 42 | }; 43 | 44 | const useFormField = () => { 45 | const fieldContext = React.useContext(FormFieldContext); 46 | const itemContext = React.useContext(FormItemContext); 47 | const { getFieldState, formState } = useFormContext(); 48 | 49 | const fieldState = getFieldState(fieldContext.name, formState); 50 | 51 | if (!fieldContext) { 52 | throw new Error('useFormField should be used within '); 53 | } 54 | 55 | const { id } = itemContext; 56 | 57 | return { 58 | id, 59 | name: fieldContext.name, 60 | formItemId: `${id}-form-item`, 61 | formDescriptionId: `${id}-form-item-description`, 62 | formMessageId: `${id}-form-item-message`, 63 | ...fieldState, 64 | }; 65 | }; 66 | 67 | type FormItemContextValue = { 68 | id: string; 69 | }; 70 | 71 | const FormItemContext = React.createContext( 72 | {} as FormItemContextValue, 73 | ); 74 | 75 | const FormItem = React.forwardRef< 76 | HTMLDivElement, 77 | React.HTMLAttributes 78 | >(({ className, ...props }, ref) => { 79 | const id = React.useId(); 80 | 81 | return ( 82 | 83 |
84 | 85 | ); 86 | }); 87 | FormItem.displayName = 'FormItem'; 88 | 89 | const FormLabel = React.forwardRef< 90 | React.ElementRef, 91 | React.ComponentPropsWithoutRef 92 | >(({ className, ...props }, ref) => { 93 | const { error, formItemId } = useFormField(); 94 | 95 | return ( 96 |