├── .changeset └── config.json ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github ├── CODEOWNERS ├── PULL_REQUEST_TEMPLATE.md ├── detect-add.yml └── workflows │ ├── add-assignee.yml │ ├── ci.yaml │ ├── detect-changed-packages.yml │ └── release.yaml ├── .gitignore ├── .husky └── pre-commit ├── .markdownlint.json ├── .markdownlint.jsonc ├── .markdownlintignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── .size-limit.js ├── LICENSE ├── README.md ├── package.json ├── packages ├── commit-helper │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── __test__ │ │ └── cli.test.js │ ├── bin │ │ ├── cli.ts │ │ └── index.ts │ ├── package.json │ ├── src │ │ ├── constant.ts │ │ └── schema.json │ └── tsconfig.json ├── fmb │ ├── CHANGELOG.md │ ├── README.md │ ├── index.js │ └── package.json └── publint │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ ├── fixtures │ │ ├── cjs-typescript-no-require-types │ │ │ ├── package.json │ │ │ └── tsconfig.json │ │ ├── dual-package-invalid-extensions │ │ │ └── package.json │ │ ├── dual-package-no-module │ │ │ └── package.json │ │ ├── dual-package-valid-extensions │ │ │ └── package.json │ │ ├── dual-typescript-missing-types │ │ │ ├── package.json │ │ │ └── tsconfig.json │ │ ├── esm-typescript-no-import-types │ │ │ ├── package.json │ │ │ └── tsconfig.json │ │ ├── invalid-cjs-typescript-types-extension │ │ │ ├── package.json │ │ │ └── tsconfig.json │ │ ├── invalid-dual-typescript-import-types-extension │ │ │ ├── package.json │ │ │ └── tsconfig.json │ │ ├── invalid-dual-typescript-require-types-extension │ │ │ ├── package.json │ │ │ └── tsconfig.json │ │ ├── invalid-esm-typescript-types-extension │ │ │ ├── package.json │ │ │ └── tsconfig.json │ │ ├── invalid-module-extension │ │ │ └── package.json │ │ ├── invalid-types-field-extension │ │ │ ├── package.json │ │ │ └── tsconfig.json │ │ ├── no-exports-field │ │ │ └── package.json │ │ ├── no-files-field │ │ │ └── package.json │ │ ├── no-main-field │ │ │ └── package.json │ │ ├── no-package-json-exports │ │ │ └── package.json │ │ ├── no-side-effects-field │ │ │ └── package.json │ │ ├── typescript-no-types │ │ │ ├── package.json │ │ │ └── tsconfig.json │ │ ├── valid-cjs-typescript-package │ │ │ ├── package.json │ │ │ └── tsconfig.json │ │ ├── valid-dual-typescript-package │ │ │ ├── package.json │ │ │ └── tsconfig.json │ │ ├── valid-esm-typescript-package │ │ │ ├── package.json │ │ │ └── tsconfig.json │ │ ├── valid-package │ │ │ └── package.json │ │ ├── valid-types-field │ │ │ ├── package.json │ │ │ └── tsconfig.json │ │ └── valid-typescript-package │ │ │ ├── package.json │ │ │ └── tsconfig.json │ └── verifyPackageJSON.test.ts │ ├── bin │ └── cli.ts │ ├── package.json │ ├── src │ ├── errors.ts │ ├── index.ts │ ├── types │ │ └── packageJSON.ts │ └── verify │ │ ├── exports.ts │ │ ├── outputPaths.ts │ │ ├── packageStructure.ts │ │ ├── packageType.ts │ │ ├── requiredFields.ts │ │ └── typescriptPackage.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── turbo.json /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{js,ts,tsx}] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | max_line_length = 120 10 | trim_trailing_whitespace = true 11 | 12 | [*.{json,yml,yaml}] 13 | charset = utf-8 14 | end_of_line = lf 15 | indent_style = space 16 | indent_size = 4 17 | insert_final_newline = true 18 | trim_trailing_whitespace = true 19 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | pnpm-lock.yaml 39 | 40 | # Optional npm cache directory 41 | .npm 42 | 43 | # Optional eslint cache 44 | .eslintcache 45 | 46 | # Optional REPL history 47 | .node_repl_history 48 | 49 | # Output of 'npm pack' 50 | *.tgz 51 | 52 | # Yarn Integrity file 53 | .yarn-integrity 54 | 55 | # dotenv environment variables file 56 | .env 57 | 58 | # next.js build output 59 | .next 60 | 61 | dist 62 | 63 | apps/docs/docs 64 | 65 | .changeset/* 66 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@naverpay/eslint-config" 3 | } 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @NaverPayDev/frontend 2 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Related Issue 2 | 3 | - # 4 | 5 | ## Describe your changes 6 | 7 | - 8 | 9 | ## Request 10 | 11 | - 12 | -------------------------------------------------------------------------------- /.github/detect-add.yml: -------------------------------------------------------------------------------- 1 | name: detect changed packages 2 | 3 | on: 4 | pull_request: 5 | branches: ['**'] 6 | types: [opened, reopened, labeled, unlabeled, synchronize] 7 | 8 | concurrency: 9 | group: detect-${{ github.event.pull_request.number }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | detect: 14 | runs-on: ubuntu-latest 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.ACTION_TOKEN }} 17 | steps: 18 | - name: Checkout Repo 19 | uses: actions/checkout@v3 20 | with: 21 | token: ${{ secrets.ACTION_TOKEN }} 22 | fetch-depth: 0 23 | 24 | - uses: actions/checkout@v4 25 | - uses: pnpm/action-setup@v4 26 | - uses: actions/setup-node@v4 27 | with: 28 | node-version: '20' 29 | cache: 'pnpm' 30 | 31 | - name: install dependencies 32 | run: pnpm install --frozen-lockfile 33 | 34 | - name: 'detect changed packages' 35 | uses: NaverPayDev/changeset-actions/detect-add@main 36 | with: 37 | github_token: ${{ secrets.ACTION_TOKEN }} 38 | packages_dir: . 39 | skip_label: skip-detect-change 40 | skip_branches: main 41 | formatting_script: pnpm run markdownlint:fix 42 | -------------------------------------------------------------------------------- /.github/workflows/add-assignee.yml: -------------------------------------------------------------------------------- 1 | name: 'add assignee to pull request automatically' 2 | on: 3 | pull_request: 4 | types: [ opened, ready_for_review, synchronize, reopened ] 5 | branches: 6 | - '**' 7 | - '!main' 8 | jobs: 9 | ADD_ASSIGNEE_TO_PULL_REQUEST: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Add Assignee to pr 13 | uses: actions/github-script@v3 14 | with: 15 | script: | 16 | try { 17 | const result = await github.pulls.get({ 18 | owner: context.repo.owner, 19 | repo: context.repo.repo, 20 | pull_number: context.payload.number, 21 | }) 22 | 23 | console.log(result) 24 | 25 | if (result.data.assignee === null) { 26 | await github.issues.addAssignees({ 27 | owner: context.repo.owner, 28 | repo: context.repo.repo, 29 | issue_number: context.issue.number, 30 | assignees: context.actor, 31 | }) 32 | } 33 | } catch (err) { 34 | console.error(`Check Pull Request Error ${err}`) 35 | } 36 | 37 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | branches: 5 | - '**' 6 | 7 | jobs: 8 | PrettierAndLint: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: pnpm/action-setup@v3 13 | with: 14 | version: 8 15 | run_install: true 16 | - run: pnpm run prettier 17 | - run: pnpm run lint 18 | - run: pnpm run markdownlint 19 | 20 | Test: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v3 24 | - uses: pnpm/action-setup@v3 25 | with: 26 | version: 8 27 | run_install: true 28 | - run: pnpm run test 29 | -------------------------------------------------------------------------------- /.github/workflows/detect-changed-packages.yml: -------------------------------------------------------------------------------- 1 | name: detect changed packages 2 | 3 | on: 4 | pull_request: 5 | branches: ['**'] 6 | types: [opened, reopened, labeled, unlabeled, synchronize] 7 | 8 | concurrency: 9 | group: detect-${{ github.event.pull_request.number }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | detect: 14 | runs-on: ubuntu-latest 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.ACTION_TOKEN }} 17 | steps: 18 | - name: Checkout Repo 19 | uses: actions/checkout@v3 20 | with: 21 | token: ${{ secrets.ACTION_TOKEN }} 22 | fetch-depth: 0 23 | 24 | - uses: pnpm/action-setup@v4 25 | - name: Use Node.js 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: '22' 29 | cache: 'pnpm' 30 | - name: Install dependencies 31 | run: pnpm install --frozen-lockfile 32 | 33 | - name: 'detect changed packages' 34 | uses: NaverPayDev/changeset-actions/detect-add@main 35 | with: 36 | github_token: ${{ secrets.ACTION_TOKEN }} 37 | packages_dir: packages,share 38 | skip_label: skip-detect-change 39 | skip_branches: main 40 | formatting_script: pnpm run markdownlint:fix 41 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: release packages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | release: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout Repo 15 | uses: actions/checkout@v3 16 | with: 17 | token: ${{ secrets.ACTION_TOKEN }} 18 | fetch-depth: 0 19 | 20 | - name: Use Node.js 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: '20.x' 24 | 25 | - name: Install Corepack 26 | run: npm install -g corepack@0.31.0 27 | 28 | - name: Enable Corepack 29 | run: corepack enable 30 | 31 | - name: Install dependencies 32 | run: pnpm install --frozen-lockfile 33 | 34 | - name: Build 35 | run: pnpm build 36 | 37 | - name: Create Release Pull Request or Publish to npm 38 | id: changesets 39 | uses: changesets/action@v1 40 | with: 41 | title: '🚀 version changed packages' 42 | commit: '📦 bump changed packages version' 43 | publish: pnpm release 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.ACTION_TOKEN }} 46 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # etc 2 | .idea 3 | .vscode 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | .pnpm-debug.log* 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | /coverage 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # Optional npm cache directory 42 | .npm 43 | 44 | # Optional eslint cache 45 | .eslintcache 46 | 47 | # Optional REPL history 48 | .node_repl_history 49 | 50 | # Output of 'npm pack' 51 | *.tgz 52 | 53 | # Yarn Integrity file 54 | .yarn-integrity 55 | 56 | # dotenv environment variables file 57 | .env 58 | .env.local 59 | 60 | # production 61 | build 62 | 63 | # misc 64 | .DS_Store 65 | *.pem 66 | 67 | # vercel 68 | .vercel 69 | 70 | # typescript 71 | *.tsbuildinfo 72 | next-env.d.ts 73 | 74 | # next.js build output 75 | .next/ 76 | dist/ 77 | out/ 78 | 79 | # turbo 80 | .turbo 81 | 82 | # telemetry disable 설정 릴리즈시점에 커밋에 섞여들어옴. 이를 방지하기 위함 83 | apps/web/cache/ -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@naverpay/markdown-lint" 3 | } 4 | -------------------------------------------------------------------------------- /.markdownlint.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@naverpay/markdown-lint" 3 | } 4 | -------------------------------------------------------------------------------- /.markdownlintignore: -------------------------------------------------------------------------------- 1 | .changeset 2 | **/CHANGELOG.md 3 | apps/docs/**/*.md 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | .changeset 3 | .husky -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # changeset 제외 2 | .changeset 3 | 4 | # markdown 포맷팅은 markdown-lint에게 맡깁니다. 5 | **/*.md 6 | 7 | # pnpm lock yaml 제외 8 | pnpm-lock.yaml 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | "@naverpay/prettier-config" 2 | -------------------------------------------------------------------------------- /.size-limit.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const glob = require('glob') 3 | const fs = require('fs') 4 | 5 | const packageJsonList = glob 6 | .globSync('**/package.json', { 7 | cwd: path.join(process.cwd(), 'packages'), 8 | }) 9 | .map((filePath) => { 10 | const {name, main} = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'packages', filePath), 'utf8')) 11 | const packageName = name.split('/')[1] 12 | return { 13 | name: packageName, 14 | path: `packages/${packageName}${main.slice(1)}`, 15 | } 16 | }) 17 | 18 | module.exports = packageJsonList 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 NaverPayDev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## @naverpay/cli 2 | 3 | This repository is a monorepo that manages a collection of Command Line Interface (CLI) tools designed to improve the development experience. 4 | > 📖 For detailed usage instructions, please refer to the README file in each individual package. 5 | 6 | ### Packages 7 | 8 | - [@naverpay/commit-helper](https://github.com/NaverPayDev/cli/blob/main/packages/commit-helper/README.md) 9 | - Provides various tools which assist your commit. 10 | - [@naverpay/fmb](https://github.com/NaverPayDev/cli/blob/main/packages/fmb/README.md) 11 | - Deletes branches that have already been merged into the main branch. 12 | - [@naverpay/publint](https://github.com/NaverPayDev/cli/blob/main/packages/publint/README.md) 13 | - Checks repository configurations and dependency configurations to help maintain consistency and quality. 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@naverpay/cli", 3 | "author": "@NaverPayDev/frontend", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/NaverPayDev/cli.git" 7 | }, 8 | "version": "0.0.0", 9 | "scripts": { 10 | "prepare": "husky install", 11 | "start": "turbo run start", 12 | "build": "turbo run build", 13 | "test": "turbo run test", 14 | "lint": "eslint '**/*.{js,jsx,ts,tsx}'", 15 | "lint:fix": "pnpm run lint --fix", 16 | "prettier": "prettier --check '**/*.{json,yaml,md,ts,tsx,js,jsx}'", 17 | "prettier:fix": "prettier --write '**/*.{json,yaml,md,ts,tsx,js,jsx}'", 18 | "markdownlint": "markdownlint '**/*.md' '#.changeset' '#**/CHANGELOG.md'", 19 | "markdownlint:fix": "markdownlint --fix '**/*.md' '#.changeset' '#**/CHANGELOG.md'", 20 | "clean": "turbo run clean && rm -rf ./node_modules && pnpm i", 21 | "release:canary": "pnpm run build && changeset publish --no-git-tag", 22 | "release": "pnpm run build && changeset publish" 23 | }, 24 | "lint-staged": { 25 | "**/*.{json,yaml,md,ts,tsx,js,jsx}": "prettier --check", 26 | "**/*.{ts,tsx,js,jsx}": "eslint" 27 | }, 28 | "devDependencies": { 29 | "@changesets/cli": "^2.26.2", 30 | "@naverpay/eslint-config": "^0.2.0", 31 | "@naverpay/markdown-lint": "^0.0.2", 32 | "@naverpay/prettier-config": "^0.0.2", 33 | "@size-limit/preset-big-lib": "^11.0.2", 34 | "@types/node": "^20.12.7", 35 | "glob": "^9.3.4", 36 | "husky": "^8.0.3", 37 | "lint-staged": "^15.0.1", 38 | "size-limit": "^11.0.2", 39 | "turbo": "^1.10.16", 40 | "typescript": "^5.2.2" 41 | }, 42 | "packageManager": "pnpm@8.14.3" 43 | } 44 | -------------------------------------------------------------------------------- /packages/commit-helper/.gitignore: -------------------------------------------------------------------------------- 1 | bin/**/*.js 2 | src/**/*.js 3 | bin/**/*.d.ts 4 | src/**/*.d.ts 5 | -------------------------------------------------------------------------------- /packages/commit-helper/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @naverpay/commit-helper 2 | 3 | ## 1.2.0 4 | 5 | ### Minor Changes 6 | 7 | - e7acbf3: 🔧 fix: fallback to default protected branches if no custom patterns are provided 8 | 9 | PR: [🔧 fix: fallback to default protected branches if no custom patterns are provided](https://github.com/NaverPayDev/cli/pull/29) 10 | 11 | ### Patch Changes 12 | 13 | - 9f491fe: protect 설정값의 undefined 에 대한 예외처리 추가 14 | 15 | PR: [protect 설정값의 undefined 에 대한 예외처리 추가](https://github.com/NaverPayDev/cli/pull/31) 16 | 17 | ## 1.1.0 18 | 19 | ### Minor Changes 20 | 21 | - c214be8: extends 문법 지원 22 | 23 | ## 1.0.0 24 | 25 | ### Major Changes 26 | 27 | - b7f6303: @naverpay/commit-helper 패키지를 제공합니다 28 | -------------------------------------------------------------------------------- /packages/commit-helper/README.md: -------------------------------------------------------------------------------- 1 | # @naverpay/commit-helper 2 | 3 | This CLI provides various tools which assist your commit based on [husky](https://typicode.github.io/husky/) `commit-msg` hook. 4 | 5 | ## How to use 6 | 7 | ### .husky/commit-msg 8 | 9 | ``` 10 | npx --yes @naverpay/commit-helper@latest $1 11 | ``` 12 | 13 | > `@latest` is not necessary but this option always provides latest version of commit-helper. 14 | 15 | ## what it does 16 | 17 | ### Tag related issue 18 | 19 | > Automatically Add your related github issue number at your commit message through your branch name 20 | 21 | ```shell 22 | ➜ your-repo git:(feature/1) git add . && git commit -m ":memo: test" 23 | ℹ No staged files match any configured task. 24 | $ git branch --show-current 25 | feature/1 26 | [feature/1 1e70c244f] [#1] :memo: test 27 | 1 file changed, 1 insertion(+) 28 | ``` 29 | 30 | Your issue number is automatically tagged base on your setting (`.commithelperrc.json`) 31 | 32 | ### Blocking commit 33 | 34 | - Blocks direct commit toward `main`, `develop` `master` branch by throwing error on commit attempt. 35 | - To block specific branches, add at `protect` field on `commithelperrc`. 36 | 37 | #### Customization 38 | 39 | If you need to add customized commit rule, use [cosmiconfig](https://github.com/cosmiconfig/cosmiconfig) rules as `commithelper`. `cosmiconfig` is a library widely used in tools like `eslint` and `prettier` to read `rc` configuration files. `commithelper` also uses `cosmiconfig`. If there is any conflict with predefined rules, `cosmicconfig` takes precedence. 40 | 41 | ### commithelperrc 42 | 43 | This is Basic rule of `.commithelperrc`. 44 | 45 | ```json 46 | { 47 | "protect": ["main", "master", "develop"], 48 | "rules": { 49 | "feature": null, 50 | "qa": "your-org/your-repo" 51 | } 52 | } 53 | ``` 54 | 55 | #### rules 56 | 57 | - Key of rules field means branch prefix. By `feature` key, this rule is applied to branches named using the `feature/***` pattern. 58 | - Value represents the repository to be tagged. For example, rule with value 'your-org/your-repo' tags 'your-org/your-repo#1'. 59 | - A rule with a `null` value tags repository itself. 60 | 61 | #### protect 62 | 63 | - Defines branch prefixes that are blocked from committing. `main`, `master`, `develop` branch is blocked by default. 64 | 65 | ### Example 66 | 67 | ```json 68 | // commithelperrc.json 69 | { 70 | "protect": ["epic"], 71 | "rules": { 72 | "feature": null, 73 | "qa": "your-org/your-repo" 74 | } 75 | } 76 | ``` 77 | 78 | > For example as above, 79 | > 80 | > - commit on `feature/1` branch will be tagged as `[#1]`. 81 | > - commit on `qa/1` branch will be tagged as `[your-org/your-repo#1]`. 82 | > - direct commit attempt toward `main`, `master`, `develop`, `epic/***` branch will be blocked 83 | 84 | ## Q&A 85 | 86 | - What happens if commit has already tagged issue like `[your-org/your-repo#1]`? 87 | - `commithelper` do not works. Already tagged issue remains unchanged 88 | - How does commit-helper behaves on `feature/1_xxx` `feature/1-xxx` patterned branch name? 89 | - It works same as `feature/1` branch. 90 | -------------------------------------------------------------------------------- /packages/commit-helper/__test__/cli.test.js: -------------------------------------------------------------------------------- 1 | import {getCommitMessageByBranchName, isStringMatchingPatterns} from '../bin/cli.js' 2 | import {ISSUE_TAGGING_REGEX, BRANCH_ISSUE_TAGGING_REGEX} from '../src/constant.js' 3 | 4 | describe('커밋 메시지내 이슈를 찾는 정규식 테스트', () => { 5 | it.each([ 6 | ['일반적인 메시지', false], 7 | ['naverpay/cli#1', false], 8 | ['naverpay/cli#1]', false], 9 | ['naverpay/cli#1] 결제는 네이버페이로', false], 10 | ['[naverpay/cli#1', false], 11 | ['[naverpay/cli#1 결제는 네이버페이로', false], 12 | ['[naverpay/cli#1]', true], 13 | ['[naverpay/cli#1] 결제는 네이버페이로', true], 14 | ['[naverpay/cli#12345] 결제는 네이버페이로', true], 15 | ['[naverpay/cli#123456] 결제는 네이버페이로', false], 16 | ['[#12345] 결제는 네이버페이로', true], 17 | ['[#123] 결제는 네이버페이로', true], 18 | ['[#123456] 결제는 네이버페이로', false], 19 | ])('커밋 메시지 "%s"는 내부에 이슈 태깅이 있다 => (%s)', (message, result) => { 20 | const output = message.match(ISSUE_TAGGING_REGEX) !== null 21 | expect(output).toBe(result) 22 | }) 23 | }) 24 | 25 | describe('브랜치가 commithelper 형식에 맞는지 확인하는 정규식', () => { 26 | it.each([ 27 | ['main', false], 28 | ['master', false], 29 | ['develop', false], 30 | ['main', false], 31 | ['repo', false], 32 | ['cli', false], 33 | ['cli/123', true], 34 | ['cli/123456', false], 35 | ['cli/123_fix', true], 36 | ['cli/123_test', true], 37 | ['cli/123_wtf_bug_fix', true], 38 | ['cli/123_FIX_BUG', true], 39 | ['cli/123_FIX_BUG_TEST_OH_YEAH', true], 40 | ['cli/123fixbug', true], 41 | ])('브랜치 명 "%s"는 태깅할 수 있는 브랜치다 => (%s)', (message, result) => { 42 | const output = message.match(BRANCH_ISSUE_TAGGING_REGEX) !== null 43 | expect(output).toBe(result) 44 | }) 45 | }) 46 | 47 | /** 48 | * @description 이 테스트는 ISSUE_TAGGING_MAP 와 강결합되어 있습니다. 49 | */ 50 | describe('원하는 대로 커밋메시지를 잘 변조하는지 확인.', () => { 51 | it.each([ 52 | ['repo/123', '안녕하세요.', '[#123] 안녕하세요.'], 53 | ['repo/123_fix', '안녕하세요.', '[#123] 안녕하세요.'], 54 | ['repo/123-fix', '안녕하세요.', '[#123] 안녕하세요.'], 55 | ['repo/123-fix_bug', '안녕하세요.', '[#123] 안녕하세요.'], 56 | ['feature/123', '안녕하세요.', '[#123] 안녕하세요.'], 57 | /** 58 | * @description ISSUE_TAGGING_MAP 에 정의되지 않은 브랜치명은 그대로 반환한다. 59 | */ 60 | ['epic/123', '안녕하세요.', '안녕하세요.'], 61 | ['develop', '안녕하세요.', '안녕하세요.'], 62 | ])('브랜치명 [%s] 에서 커밋메시지 "%s"는 "%s"다.', (branch, commitMessage, finalCommitMessage) => { 63 | const output = getCommitMessageByBranchName(branch, commitMessage) 64 | expect(output).toBe(finalCommitMessage) 65 | }) 66 | }) 67 | 68 | describe('', () => { 69 | it.each([ 70 | [['main'], 'main', true], 71 | [['master'], 'master', true], 72 | [['develop'], 'develop', true], 73 | [['release/*'], 'release/123456', true], 74 | [['release/*'], 'release/123456', true], 75 | [['release/*'], 'release', false], 76 | [['release/*'], 'release_123456', false], 77 | [['epic/*'], 'epic/naverpay', true], 78 | [['epic/*'], 'epic/naverpay_1234', true], 79 | [['epic/*'], 'epic/naverpay_yes', true], 80 | [['epic/*'], 'epic_naverpay_yes', false], 81 | [['epic/*'], 'epic', false], 82 | ])('브랜치명 패턴 %s 에 "%s" 브랜치 매칭 여부 %s', (patterns, branchName, result) => { 83 | const output = isStringMatchingPatterns(branchName, patterns) 84 | expect(output).toBe(result) 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /packages/commit-helper/bin/cli.ts: -------------------------------------------------------------------------------- 1 | import {exec} from 'child_process' 2 | import fs from 'fs/promises' 3 | import {join} from 'path' 4 | import process from 'process' 5 | 6 | import {cosmiconfigSync} from 'cosmiconfig' 7 | import meow from 'meow' 8 | 9 | import { 10 | ISSUE_TAGGING_MAP, 11 | BRANCH_ISSUE_TAGGING_REGEX, 12 | PURE_BRANCH_ISSUE_TAGGING_REGEX, 13 | DEFAULT_PROTECTED_BRANCHES, 14 | ISSUE_TAGGING_REGEX, 15 | } from '../src/constant.js' 16 | 17 | async function getCurrentBranchName(): Promise { 18 | return new Promise((resolve, reject) => { 19 | exec('git branch --show-current', (err, stdout, stderr) => { 20 | if (err) { 21 | reject(err) 22 | return 23 | } 24 | if (stderr) { 25 | reject(new Error(stderr)) 26 | return 27 | } 28 | resolve(stdout.trim() as string) 29 | }) 30 | }) 31 | } 32 | 33 | export function getCommitMessageByBranchName( 34 | branchName: string, 35 | originCommitMessage: string, 36 | externalConfig?: Record, 37 | ) { 38 | let finalCommitMessage = '' 39 | 40 | // 커밋 메시지 내부에 이슈 태깅이 되어 있을 경우, 브랜치명을 확인하지 않고 그냥 보낸다. 41 | if (ISSUE_TAGGING_REGEX.test(originCommitMessage)) { 42 | finalCommitMessage = originCommitMessage 43 | } 44 | // 현재 브랜치명에서 이슈 태깅을 찾아서 커밋 메시지에 추가한다. 45 | else { 46 | const foundedIssueTagging = branchName.match(BRANCH_ISSUE_TAGGING_REGEX) 47 | 48 | // 브랜치 명이 정규식과 일치한다면 49 | if (foundedIssueTagging && foundedIssueTagging.length > 0) { 50 | // 브랜치명에는 _fx 와 같은 추가정보가 있을 수 있으므로, 서비스 명을 추가로 뽑아낸다. 51 | const foundBranch = foundedIssueTagging[0].match(PURE_BRANCH_ISSUE_TAGGING_REGEX) 52 | 53 | if (!foundBranch) { 54 | // rebase 등으로 브랜치 없이 작업할 수 있음 55 | return originCommitMessage 56 | } 57 | 58 | const branchInfo = foundBranch[0] 59 | 60 | const serviceName = branchInfo.split('/')[0].toLowerCase() 61 | const issueNumber = branchInfo.split('/')[1] 62 | 63 | /** 64 | * @description 내부 상수를 후순위. 사용자 설정이 덮어쓴다. 65 | */ 66 | const issueMap = { 67 | ...ISSUE_TAGGING_MAP, 68 | ...externalConfig, 69 | } as Record 70 | 71 | // 태깅 맵 객체에 맞는게 있는지 확인한다. 72 | const repoName = issueMap[serviceName] 73 | 74 | // 사용자 설정에 있거나 내부상수에 정의된 경우 75 | if (repoName) { 76 | finalCommitMessage = `[${repoName}#${issueNumber}] ${originCommitMessage}` 77 | } 78 | // null 로 명시되었다는 것은 자기자신 (#123) 으로 태깅하겠다는 뜻 79 | else if (repoName === null) { 80 | finalCommitMessage = `[#${issueNumber}] ${originCommitMessage}` 81 | } 82 | // undefined 는 못찾았다는 뜻. 커밋메시지를 그냥 돌려보낸다. 83 | else { 84 | finalCommitMessage = originCommitMessage 85 | } 86 | } 87 | // 맞는게 없다면 그냥 기존 메시지로 보낸다 88 | else { 89 | finalCommitMessage = originCommitMessage 90 | } 91 | } 92 | return finalCommitMessage 93 | } 94 | 95 | interface Config { 96 | rules: Record 97 | protect?: string[] 98 | } 99 | 100 | export async function readExternalConfig(): Promise { 101 | const explorerSync = cosmiconfigSync('commithelper') 102 | const searchedFor = explorerSync.search() 103 | 104 | if (!searchedFor) { 105 | return {rules: {}} 106 | } 107 | 108 | const localConfig = searchedFor.config as Partial 109 | 110 | const mergedRules: Record = {...(localConfig.rules || {})} 111 | let mergedProtect: string[] = Array.isArray(localConfig.protect) ? [...localConfig.protect] : [] 112 | 113 | const extendsUrl = localConfig.extends 114 | if (typeof extendsUrl === 'string' && /^(http|https):\/\//.test(extendsUrl)) { 115 | try { 116 | const response = await fetch(extendsUrl) 117 | if (!response.ok) { 118 | throw new Error(`Failed to fetch extends config: ${response.status} ${response.statusText}`) 119 | } 120 | 121 | const extendsConfig = (await response.json()) as Partial 122 | 123 | if (extendsConfig.rules && typeof extendsConfig.rules === 'object') { 124 | Object.assign(mergedRules, extendsConfig.rules) 125 | } 126 | 127 | if (Array.isArray(extendsConfig.protect)) { 128 | mergedProtect = [...extendsConfig.protect, ...mergedProtect] 129 | } 130 | } catch (e) { 131 | throw new Error(`Failed to load external config from "${extendsUrl}": ${(e as Error).message}`) 132 | } 133 | } 134 | 135 | const result: Config = { 136 | rules: mergedRules, 137 | } 138 | 139 | if (mergedProtect.length > 0) { 140 | result.protect = [...new Set(mergedProtect)] 141 | } 142 | 143 | return result 144 | } 145 | 146 | export function isStringMatchingPatterns(stringToCheck: string, patternsArray?: string[]) { 147 | const patterns = patternsArray || DEFAULT_PROTECTED_BRANCHES 148 | 149 | return patterns.some((pattern) => { 150 | if (pattern.endsWith('/*')) { 151 | const basePattern = pattern.slice(0, -1) 152 | const regex = new RegExp(`^${basePattern}.+$`) 153 | return regex.test(stringToCheck) 154 | } else { 155 | return pattern === stringToCheck 156 | } 157 | }) 158 | } 159 | 160 | export async function run() { 161 | const cli = meow(`Tag issues to commit messages based on your current branch names.`, { 162 | importMeta: import.meta, 163 | flags: {help: {type: 'boolean', shortFlag: 'h'}, show: {type: 'boolean', shortFlag: 's'}}, 164 | }) 165 | 166 | const currentBranchName = (await getCurrentBranchName()).toLowerCase() 167 | 168 | const {rules = {}, protect} = await readExternalConfig() 169 | 170 | if (cli.flags.show) { 171 | /** 172 | * @description 내부 상수를 후순위. 사용자 설정이 덮어쓴다. 173 | */ 174 | const issueMap = { 175 | ...ISSUE_TAGGING_MAP, 176 | ...rules, 177 | } as Record 178 | 179 | // eslint-disable-next-line no-console 180 | console.log(JSON.stringify(issueMap, null, 2)) 181 | return 182 | } 183 | 184 | const commitMessagePath = cli.input[0] 185 | const commitFilePath = join(process.cwd(), commitMessagePath) 186 | const commitMessage = await fs.readFile(commitFilePath, 'utf8') 187 | 188 | if (!commitMessage) { 189 | throw new Error('Commit message is required.') 190 | } 191 | 192 | const isProtectedBranch = isStringMatchingPatterns(currentBranchName, protect) 193 | 194 | if (isProtectedBranch) { 195 | throw new Error(`You can't commit on this branch: ${currentBranchName}`) 196 | } 197 | 198 | const result = getCommitMessageByBranchName(currentBranchName, commitMessage, rules) 199 | 200 | await fs.writeFile(commitFilePath, result, {encoding: 'utf8'}) 201 | } 202 | -------------------------------------------------------------------------------- /packages/commit-helper/bin/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import {run} from './cli.js' 4 | 5 | run() 6 | -------------------------------------------------------------------------------- /packages/commit-helper/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@naverpay/commit-helper", 3 | "version": "1.2.0", 4 | "description": "help your commit in git", 5 | "main": "./dist/index.js", 6 | "bin": { 7 | "commit-helper": "./dist/index.js" 8 | }, 9 | "scripts": { 10 | "build": "ncc build ./bin/index.ts -o dist --no-cache", 11 | "test": "pnpm tsc -d -p tsconfig.json && node --experimental-vm-modules node_modules/jest/bin/jest.js" 12 | }, 13 | "type": "module", 14 | "keywords": [], 15 | "author": "", 16 | "license": "ISC", 17 | "devDependencies": { 18 | "jest": "^29.7.0", 19 | "@vercel/ncc": "^0.38.1", 20 | "typescript": "^5.2.2" 21 | }, 22 | "engines": { 23 | "node": ">=18.0.0" 24 | }, 25 | "dependencies": { 26 | "cosmiconfig": "^8.3.6", 27 | "meow": "^12.1.1" 28 | }, 29 | "peerDependencies": { 30 | "husky": "^8.0.3" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/commit-helper/src/constant.ts: -------------------------------------------------------------------------------- 1 | export const ISSUE_TAGGING_MAP = { 2 | repo: null, 3 | feature: null, 4 | } as const 5 | export const ISSUE_TAGGING_REGEX = /\[(?:[A-Za-z-_]+\/)?[A-Za-z-_]*#\d{1,5}\]/ 6 | export const BRANCH_ISSUE_TAGGING_REGEX = /[A-Za-z-]+\/\d{1,5}([-_a-zA-Z]+[0-9]*)*$/ 7 | export const PURE_BRANCH_ISSUE_TAGGING_REGEX = /[A-Za-z-]+\/\d{1,5}/ 8 | export const DEFAULT_PROTECTED_BRANCHES: Readonly = ['main', 'master', 'develop'] 9 | -------------------------------------------------------------------------------- /packages/commit-helper/src/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "https://github.com/NaverPayDev/cli/packages/commit-helper/src/schema.json", 4 | "title": "Schema for .commithelperrc.json", 5 | "type": "object", 6 | "properties": { 7 | "extends": { 8 | "type": "string", 9 | "pattern": "^(http|https)://", 10 | "description": "Extends remote URL" 11 | }, 12 | "protect": { 13 | "type": "array", 14 | "items": { 15 | "type": "string" 16 | }, 17 | "minItems": 1 18 | }, 19 | "rules": { 20 | "type": "object", 21 | "properties": { 22 | "repo": { 23 | "type": "string" 24 | }, 25 | "feature": { 26 | "type": "string" 27 | }, 28 | "qa": { 29 | "type": "string" 30 | } 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/commit-helper/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "Node16", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true 9 | }, 10 | "include": ["**/*.ts"], 11 | "exclude": ["node_modules"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/fmb/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # fmb 2 | 3 | ## 0.0.1 4 | 5 | ### Patch Changes 6 | 7 | - d9a61f5: 🚀 fmb 8 | 9 | ## 0.0.1 10 | 11 | ### Patch Changes 12 | 13 | - 080c511: 🚀 fmb 14 | -------------------------------------------------------------------------------- /packages/fmb/README.md: -------------------------------------------------------------------------------- 1 | # fmb - Fire (remove) Merged Branches 2 | 3 | `fmb` is a simple CLI tool for removing Git branches that have already been merged into `origin/main`. 4 | 5 | ## Features 6 | 7 | - Deletes merged branches automatically. 8 | - Supports excluding specific branches. 9 | - Supports branch filtering with glob patterns. 10 | - Interactive confirmation before deletion. 11 | - Automatic deletion without confirmation using the `--all` flag. 12 | 13 | ## Usage 14 | 15 | ```bash 16 | npx fmb [excluded-branches|glob-pattern] [--all] 17 | ``` 18 | 19 | ### Examples 20 | 21 | 1. **Delete all merged branches except `main` and `master`:** 22 | 23 | ```bash 24 | npx fmb 25 | ``` 26 | 27 | 2. **Exclude custom branches:** 28 | 29 | ```bash 30 | npx fmb develop,canary 31 | ``` 32 | 33 | 3. **Use Glob Patterns:** 34 | 35 | ```bash 36 | npx fmb 'feature/*' 37 | ``` 38 | 39 | 4. **Automatic Deletion Without Confirmation:** 40 | 41 | ```bash 42 | npx fmb --all 43 | ``` 44 | 45 | 5. **Interactive Confirmation:** 46 | 47 | - You will be prompted to confirm each branch deletion unless `--all` is specified. 48 | 49 | ## How It Works 50 | 51 | 1. Fetches updates from the remote repository and prunes deleted branches. 52 | 2. Lists all branches that are merged into `origin/main`. 53 | 3. Excludes specified branches or matches them using glob patterns. 54 | 4. Deletes branches automatically or prompts for confirmation based on the `--all` flag. 55 | 56 | ## License 57 | 58 | MIT License © 2024 Naver Financial 59 | -------------------------------------------------------------------------------- /packages/fmb/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* eslint-disable no-console */ 4 | import chalk from 'chalk' 5 | import {minimatch} from 'minimatch' 6 | import {$, question} from 'zx' 7 | 8 | const args = process.argv.slice(2) 9 | const shouldDeleteAll = args.includes('--all') 10 | 11 | const defaultExclusions = ['main', 'master', 'develop'] 12 | 13 | function getExcludedBranches() { 14 | const exclusions = args 15 | .filter((arg) => arg !== '--all') 16 | .join(',') 17 | .split(',') 18 | .map((branch) => branch.trim()) 19 | .filter(Boolean) 20 | return [...new Set([...defaultExclusions, ...exclusions])] 21 | } 22 | 23 | async function fetchAndPrune() { 24 | console.log(chalk.cyan('Fetching and pruning...')) 25 | await $`git fetch --prune` 26 | } 27 | 28 | async function getMergedBranches() { 29 | try { 30 | const mergeCommands = [] 31 | 32 | for (const branch of defaultExclusions) { 33 | const branchExists = await $`git show-ref --verify refs/remotes/origin/${branch}`.quiet().catch(() => false) 34 | if (branchExists) { 35 | mergeCommands.push(['git', 'branch', '--merged', `origin/${branch}`]) 36 | } 37 | } 38 | 39 | if (mergeCommands.length === 0) { 40 | console.error(chalk.red('None of the default branches exist on origin.')) 41 | return [] 42 | } 43 | 44 | const branches = (await Promise.all(mergeCommands.map((cmd) => $`${cmd}`.quiet()))) 45 | .flatMap((output) => output.stdout.split('\n')) 46 | .map((branch) => branch.trim()) 47 | .filter(Boolean) 48 | .filter((branch) => !getExcludedBranches().includes(branch)) 49 | 50 | return branches 51 | } catch (error) { 52 | console.error(chalk.red('Error fetching merged branches:'), error) 53 | return [] 54 | } 55 | } 56 | 57 | function filterBranches(branches, patterns) { 58 | return branches.filter((branch) => !patterns.some((pattern) => minimatch(branch, pattern))) 59 | } 60 | 61 | async function deleteBranches(branches) { 62 | if (!branches.length) { 63 | console.log(chalk.yellow('No branches to delete.')) 64 | return 65 | } 66 | 67 | const deletedBranches = [] 68 | 69 | for (const branch of branches) { 70 | try { 71 | const cleanedBranch = branch.trim().replace(/'/g, "'") 72 | if (shouldDeleteAll) { 73 | await $`git branch -d ${cleanedBranch}` 74 | deletedBranches.push(cleanedBranch) 75 | } else { 76 | const confirmation = (await question(chalk.magenta(`Delete branch ${cleanedBranch}? (y/N): `))) 77 | .trim() 78 | .toLowerCase() 79 | if (confirmation === 'y') { 80 | await $`git branch -d ${cleanedBranch}` 81 | deletedBranches.push(cleanedBranch) 82 | } else { 83 | console.log(chalk.blue(`Skipped branch: ${cleanedBranch}`)) 84 | } 85 | } 86 | } catch (error) { 87 | console.error(chalk.red(`Error deleting branch ${branch.trim()}:`), error) 88 | } 89 | } 90 | 91 | if (deletedBranches.length) { 92 | console.log(chalk.green('Deleted branches:')) 93 | deletedBranches.forEach((branch) => console.log(` - ${chalk.bold(branch)}`)) 94 | } else { 95 | console.log(chalk.yellow('No branches were deleted.')) 96 | } 97 | } 98 | 99 | async function main() { 100 | const excludedPatterns = getExcludedBranches() 101 | await fetchAndPrune() 102 | const branches = await getMergedBranches() 103 | const branchesToDelete = filterBranches(branches, excludedPatterns) 104 | 105 | if (!branchesToDelete.length) { 106 | console.log(chalk.yellow('No branches to delete. All branches are either excluded or not merged.')) 107 | return 108 | } 109 | 110 | await deleteBranches(branchesToDelete) 111 | } 112 | 113 | main() 114 | -------------------------------------------------------------------------------- /packages/fmb/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fmb", 3 | "version": "0.0.1", 4 | "description": "CLI tool to delete merged Git branches.", 5 | "main": "index.js", 6 | "type": "module", 7 | "bin": { 8 | "fmb": "index.js" 9 | }, 10 | "scripts": { 11 | "start": "node index.js" 12 | }, 13 | "keywords": [ 14 | "git", 15 | "cli", 16 | "branches", 17 | "delete", 18 | "prune" 19 | ], 20 | "author": "yc.effort@navercorp.com", 21 | "license": "MIT", 22 | "dependencies": { 23 | "chalk": "^5.3.0", 24 | "minimatch": "^10.0.1", 25 | "zx": "^8.2.4" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/publint/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @naverpay/publint 2 | 3 | ## 0.0.6 4 | 5 | ### Patch Changes 6 | 7 | - 8701371: 8 | - 에러 메시지 글로벌화 9 | - 에러 로깅 제거 10 | 11 | ## 0.0.5 12 | 13 | ### Patch Changes 14 | 15 | - 31fd379: export import, require 없을 때 type 검사 추가 16 | 17 | ## 0.0.4 18 | 19 | ### Patch Changes 20 | 21 | - 283c503: [#21] exports 필드 검사 조건 수정 22 | 23 | ## 0.0.3 24 | 25 | ### Patch Changes 26 | 27 | - 23af1c6: main 경로 수정 28 | 29 | ## 0.0.2 30 | 31 | ### Patch Changes 32 | 33 | - f4f8222: 🚀 init @naverpay/publint 34 | -------------------------------------------------------------------------------- /packages/publint/README.md: -------------------------------------------------------------------------------- 1 | # @naverpay/publint 2 | 3 | @naverpay/publint is a specialized tool for verifying and linting npm package structure and `package.json` files, tailored specifically for NaverPay frontend developers. It ensures that packages meet both general npm standards and NaverPay's internal best practices for modern JavaScript and TypeScript projects. 4 | 5 | ## Features 6 | 7 | - Validates the structure and content of `package.json` 8 | - Enforces NaverPay-specific frontend development rules 9 | - Supports TypeScript projects 10 | - Verifies package types (regular, module, or dual) 11 | - Checks the `exports` field 12 | - Ensures presence of required fields 13 | - Validates output paths 14 | - Provides a user-friendly CLI interface 15 | 16 | ## NaverPay Frontend Guidelines 17 | 18 | This tool incorporates NaverPay's internal frontend development guidelines, ensuring that all packages published by the team maintain consistent quality and structure. Some of the NaverPay-specific checks include: 19 | 20 | - Adherence to NaverPay's naming conventions 21 | - Verification of required NaverPay-specific fields in `package.json` 22 | - Checks for compliance with NaverPay's code structuring rules 23 | - Validation of dependencies against NaverPay's approved list 24 | 25 | By using @naverpay/publint, developers can ensure their packages are compliant with team standards before publication. 26 | 27 | ## Installation 28 | 29 | You can install @naverpay/publint globally: 30 | 31 | ```bash 32 | npm install -g @naverpay/publint 33 | ``` 34 | 35 | Or use it directly with npx without installing: 36 | 37 | ```bash 38 | npx @naverpay/publint 39 | ``` 40 | 41 | ## Usage 42 | 43 | If installed globally, you can use publint directly in your project directory: 44 | 45 | ```bash 46 | publint 47 | ``` 48 | 49 | Or specify a custom directory: 50 | 51 | ```bash 52 | publint ./my-project 53 | ``` 54 | 55 | Using npx (without global installation): 56 | 57 | ```bash 58 | npx @naverpay/publint 59 | ``` 60 | 61 | Or with a custom directory: 62 | 63 | ```bash 64 | npx @naverpay/publint ./my-project 65 | ``` 66 | 67 | ## What it checks 68 | 69 | - Presence and validity of `package.json` 70 | - Correct structure of the `exports` field 71 | - Presence of required fields in `package.json`, including NaverPay-specific fields 72 | - Package type (regular, module, or dual) and corresponding structure 73 | - TypeScript configuration and type definition files (if applicable) 74 | - Validity of output paths defined in the `exports` field 75 | - Compliance with NaverPay frontend development guidelines 76 | 77 | ## Output 78 | 79 | The tool will provide detailed feedback on the verification process, including any errors or warnings encountered during the checks. It will specifically highlight any deviations from NaverPay's internal standards. 80 | 81 | ## Contributing 82 | 83 | Contributions are welcome from NaverPay team members! Please ensure you're familiar with our internal development guidelines before submitting a Pull Request. 84 | 85 | ## License 86 | 87 | [MIT License](LICENSE) 88 | 89 | ## Acknowledgements 90 | 91 | This project was inspired by [publint](https://github.com/bluwy/publint), created by Bjorn Lu. We appreciate their work in improving the npm package ecosystem. @naverpay/publint builds upon this foundation to meet the specific needs of the NaverPay frontend development team. 92 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/cjs-typescript-no-require-types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cjs-typescript-no-require-types", 3 | "version": "1.0.0", 4 | "files": [ 5 | "index.js", 6 | "index.d.ts" 7 | ], 8 | "main": "./index.js", 9 | "types": "./index.d.ts", 10 | "exports": { 11 | ".": { 12 | "require": "./index.js" 13 | }, 14 | "./package.json": "./package.json" 15 | }, 16 | "sideEffects": false 17 | } 18 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/cjs-typescript-no-require-types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig" 3 | } 4 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/dual-package-invalid-extensions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dual-package-invalid-extensions", 3 | "version": "1.0.0", 4 | "files": [ 5 | "index.js" 6 | ], 7 | "main": "./index.js", 8 | "exports": { 9 | ".": { 10 | "require": { 11 | "default": "./index.js", 12 | "types": "./index.d.ts" 13 | }, 14 | "import": { 15 | "default": "./index.js", 16 | "types": "./index.d.ts" 17 | } 18 | }, 19 | "./package.json": "./package.json" 20 | }, 21 | "sideEffects": false, 22 | "module": "./index.js", 23 | "types": "./index.d.ts" 24 | } 25 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/dual-package-no-module/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dual-package-no-module", 3 | "version": "1.0.0", 4 | "files": [ 5 | "index.js", 6 | "index.mjs" 7 | ], 8 | "main": "./index.js", 9 | "exports": { 10 | ".": { 11 | "require": "./index.js", 12 | "import": "./index.mjs" 13 | }, 14 | "./package.json": "./package.json" 15 | }, 16 | "sideEffects": false, 17 | "types": "./index.d.ts" 18 | } 19 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/dual-package-valid-extensions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dual-package-invalid-extensions", 3 | "version": "1.0.0", 4 | "files": [ 5 | "index.js" 6 | ], 7 | "main": "./index.js", 8 | "exports": { 9 | ".": { 10 | "require": { 11 | "default": "./index.js", 12 | "types": "./index.d.ts" 13 | }, 14 | "import": { 15 | "default": "./index.mjs", 16 | "types": "./index.d.ts" 17 | } 18 | }, 19 | "./package.json": "./package.json" 20 | }, 21 | "sideEffects": false, 22 | "module": "./index.mjs", 23 | "types": "./index.d.ts" 24 | } 25 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/dual-typescript-missing-types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dual-typescript-missing-types", 3 | "version": "1.0.0", 4 | "files": [ 5 | "index.js", 6 | "index.mjs", 7 | "index.d.ts" 8 | ], 9 | "main": "./index.js", 10 | "module": "./index.mjs", 11 | "types": "./index.d.ts", 12 | "exports": { 13 | ".": { 14 | "require": "./index.js", 15 | "import": "./index.mjs" 16 | }, 17 | "./package.json": "./package.json" 18 | }, 19 | "sideEffects": false 20 | } 21 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/dual-typescript-missing-types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig" 3 | } 4 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/esm-typescript-no-import-types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esm-typescript-no-import-types", 3 | "version": "1.0.0", 4 | "files": [ 5 | "index.mjs" 6 | ], 7 | "main": "./index.mjs", 8 | "exports": { 9 | ".": { 10 | "import": "./index.mjs", 11 | "require": { 12 | "types": "./index.d.ts", 13 | "default": "./index.js" 14 | } 15 | }, 16 | "./package.json": "./package.json" 17 | }, 18 | "sideEffects": false, 19 | "module": "./index.mjs", 20 | "types": "./index.d.ts" 21 | } 22 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/esm-typescript-no-import-types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig" 3 | } 4 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/invalid-cjs-typescript-types-extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "invalid-cjs-typescript-types-extension", 3 | "version": "1.0.0", 4 | "main": "./index.js", 5 | "types": "./index.d.ts", 6 | "exports": { 7 | ".": { 8 | "require": { 9 | "types": "./index.ts", 10 | "default": "./index.js" 11 | } 12 | }, 13 | "./package.json": "./package.json" 14 | }, 15 | "files": [ 16 | "index.js" 17 | ], 18 | "sideEffects": false 19 | } 20 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/invalid-cjs-typescript-types-extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig" 3 | } 4 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/invalid-dual-typescript-import-types-extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "invalid-dual-typescript-import-types-extension", 3 | "version": "1.0.0", 4 | "main": "./index.js", 5 | "module": "./index.mjs", 6 | "types": "./index.d.ts", 7 | "exports": { 8 | ".": { 9 | "require": { 10 | "types": "./index.d.ts", 11 | "default": "./index.js" 12 | }, 13 | "import": { 14 | "types": "./index.ts", 15 | "default": "./index.mjs" 16 | } 17 | }, 18 | "./package.json": "./package.json" 19 | }, 20 | "files": [ 21 | "index.js" 22 | ], 23 | "sideEffects": false 24 | } 25 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/invalid-dual-typescript-import-types-extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig" 3 | } 4 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/invalid-dual-typescript-require-types-extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "invalid-dual-typescript-require-types-extension", 3 | "version": "1.0.0", 4 | "main": "./index.js", 5 | "files": [ 6 | "index.js" 7 | ], 8 | "module": "./index.mjs", 9 | "types": "./index.d.ts", 10 | "exports": { 11 | ".": { 12 | "require": { 13 | "types": "./index.ts", 14 | "default": "./index.js" 15 | }, 16 | "import": { 17 | "types": "./index.d.ts", 18 | "default": "./index.mjs" 19 | } 20 | }, 21 | "./package.json": "./package.json" 22 | }, 23 | "sideEffects": false 24 | } 25 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/invalid-dual-typescript-require-types-extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig" 3 | } 4 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/invalid-esm-typescript-types-extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "invalid-esm-typescript-types-extension", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "main": "./index.js", 6 | "types": "./index.d.ts", 7 | "exports": { 8 | ".": { 9 | "import": { 10 | "types": "./index.ts", 11 | "default": "./index.js" 12 | } 13 | }, 14 | "./package.json": "./package.json" 15 | }, 16 | "files": [ 17 | "index.js" 18 | ], 19 | "sideEffects": false 20 | } 21 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/invalid-esm-typescript-types-extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig" 3 | } 4 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/invalid-module-extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "invalid-module-extension", 3 | "version": "1.0.0", 4 | "main": "./index.js", 5 | "module": "./index.cjs", 6 | "files": [ 7 | "index.cjs", 8 | "index.mjs" 9 | ], 10 | "exports": { 11 | ".": { 12 | "require": { 13 | "default": "./index.cjs", 14 | "types": "./index.d.ts" 15 | }, 16 | "import": { 17 | "default": "./index.mjs", 18 | "types": "./index.d.ts" 19 | } 20 | }, 21 | "./package.json": "./package.json" 22 | }, 23 | "sideEffects": false, 24 | "types": "./index.d.ts" 25 | } 26 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/invalid-types-field-extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "invalid-types-field-extension", 3 | "version": "1.0.0", 4 | "main": "./index.js", 5 | "files": [ 6 | "index.js" 7 | ], 8 | "sideEffects": false, 9 | "types": "index.ts", 10 | "exports": { 11 | ".": { 12 | "require": { 13 | "types": "./index.d.ts", 14 | "default": "./index.js" 15 | } 16 | }, 17 | "./package.json": "./package.json" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/invalid-types-field-extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig" 3 | } 4 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/no-exports-field/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "no-exports-field", 3 | "version": "1.0.0", 4 | "files": [ 5 | "index.js" 6 | ], 7 | "main": "./index.js", 8 | "sideEffects": false 9 | } 10 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/no-files-field/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "no-files-field", 3 | "version": "1.0.0", 4 | "main": "./index.js", 5 | "exports": { 6 | ".": "./index.js", 7 | "./package.json": "./package.json" 8 | }, 9 | "sideEffects": false 10 | } 11 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/no-main-field/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "no-main-field", 3 | "version": "1.0.0", 4 | "files": [ 5 | "index.js" 6 | ], 7 | "exports": { 8 | ".": "./index.js", 9 | "./package.json": "./package.json" 10 | }, 11 | "sideEffects": false 12 | } 13 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/no-package-json-exports/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "valid-package", 3 | "version": "1.0.0", 4 | "files": [ 5 | "index.js" 6 | ], 7 | "main": "./index.js", 8 | "exports": { 9 | ".": { 10 | "require": { 11 | "default": "./index.js", 12 | "types": "./index.d.ts" 13 | }, 14 | "import": { 15 | "default": "./index.mjs", 16 | "types": "./index.d.ts" 17 | } 18 | } 19 | }, 20 | "sideEffects": false, 21 | "module": "./index.mjs", 22 | "types": "./index.d.ts" 23 | } 24 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/no-side-effects-field/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "no-side-effects-field", 3 | "version": "1.0.0", 4 | "files": [ 5 | "index.js" 6 | ], 7 | "main": "./index.js", 8 | "exports": { 9 | ".": "./index.js", 10 | "./package.json": "./package.json" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/typescript-no-types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-no-types", 3 | "version": "1.0.0", 4 | "files": [ 5 | "index.js" 6 | ], 7 | "main": "./index.js", 8 | "exports": { 9 | ".": { 10 | "require": "./index.js", 11 | "import": "./index.mjs" 12 | }, 13 | "./package.json": "./package.json" 14 | }, 15 | "sideEffects": false, 16 | "module": "./index.mjs" 17 | } 18 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/typescript-no-types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig" 3 | } 4 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/valid-cjs-typescript-package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "valid-cjs-typescript-package", 3 | "version": "1.0.0", 4 | "main": "./index.js", 5 | "types": "./index.d.ts", 6 | "exports": { 7 | ".": { 8 | "require": { 9 | "types": "./index.d.ts", 10 | "default": "./index.js" 11 | } 12 | }, 13 | "./package.json": "./package.json" 14 | }, 15 | "files": [ 16 | "index.js" 17 | ], 18 | "sideEffects": false 19 | } 20 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/valid-cjs-typescript-package/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig" 3 | } 4 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/valid-dual-typescript-package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "valid-dual-typescript-package", 3 | "version": "1.0.0", 4 | "main": "./index.js", 5 | "module": "./index.mjs", 6 | "types": "./index.d.ts", 7 | "exports": { 8 | ".": { 9 | "require": { 10 | "types": "./index.d.ts", 11 | "default": "./index.js" 12 | }, 13 | "import": { 14 | "types": "./index.d.ts", 15 | "default": "./index.mjs" 16 | } 17 | }, 18 | "./package.json": "./package.json" 19 | }, 20 | "files": [ 21 | "index.js" 22 | ], 23 | "sideEffects": false 24 | } 25 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/valid-dual-typescript-package/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig" 3 | } 4 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/valid-esm-typescript-package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "valid-esm-typescript-package", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "main": "./index.js", 6 | "types": "./index.d.ts", 7 | "exports": { 8 | ".": { 9 | "import": { 10 | "types": "./index.d.ts", 11 | "default": "./index.js" 12 | } 13 | }, 14 | "./package.json": "./package.json" 15 | }, 16 | "files": [ 17 | "index.js" 18 | ], 19 | "sideEffects": false 20 | } 21 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/valid-esm-typescript-package/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig" 3 | } 4 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/valid-package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "valid-package", 3 | "version": "1.0.0", 4 | "files": [ 5 | "index.js" 6 | ], 7 | "main": "./index.js", 8 | "exports": { 9 | ".": { 10 | "require": { 11 | "default": "./index.js", 12 | "types": "./index.d.ts" 13 | }, 14 | "import": { 15 | "default": "./index.mjs", 16 | "types": "./index.d.ts" 17 | } 18 | }, 19 | "./package.json": "./package.json" 20 | }, 21 | "sideEffects": false, 22 | "module": "./index.mjs", 23 | "types": "./index.d.ts" 24 | } 25 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/valid-types-field/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "valid-types-field", 3 | "version": "1.0.0", 4 | "main": "./index.js", 5 | "types": "./index.d.ts", 6 | "exports": { 7 | ".": { 8 | "require": { 9 | "types": "./index.d.ts", 10 | "default": "./index.js" 11 | } 12 | }, 13 | "./package.json": "./package.json" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/valid-types-field/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig" 3 | } 4 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/valid-typescript-package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "valid-typescript-package", 3 | "version": "1.0.0", 4 | "files": [ 5 | "index.js", 6 | "index.mjs", 7 | "index.d.ts" 8 | ], 9 | "main": "./index.js", 10 | "module": "./index.mjs", 11 | "types": "./index.d.ts", 12 | "exports": { 13 | ".": { 14 | "require": { 15 | "types": "./index.d.ts", 16 | "default": "./index.js" 17 | }, 18 | "import": { 19 | "types": "./index.d.ts", 20 | "default": "./index.mjs" 21 | } 22 | }, 23 | "./package.json": "./package.json" 24 | }, 25 | "sideEffects": false 26 | } 27 | -------------------------------------------------------------------------------- /packages/publint/__tests__/fixtures/valid-typescript-package/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig" 3 | } 4 | -------------------------------------------------------------------------------- /packages/publint/__tests__/verifyPackageJSON.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | import {vi, describe, test, expect, beforeEach, afterEach} from 'vitest' 4 | 5 | import { 6 | NoFilesFieldError, 7 | NoMainFieldError, 8 | NoExportsFieldError, 9 | NoSideEffectsFieldError, 10 | DualPackageModuleFieldError, 11 | TypescriptTypesFieldError, 12 | TypescriptExportMapError, 13 | InvalidFileExtensionError, 14 | InvalidModuleExtensionError, 15 | InvalidTypesFileError, 16 | PackageJsonError, 17 | MissingPackageJsonExportError, 18 | } from '../src/errors.js' 19 | import {verifyPackageJSON} from '../src/index.js' 20 | 21 | /* eslint-disable no-console */ 22 | describe('verifyPackageJSON 함수 테스트', () => { 23 | const fixturesDir = path.join(__dirname, 'fixtures') 24 | const originalConsoleError = console.error 25 | 26 | beforeEach(() => { 27 | console.error = vi.fn() 28 | }) 29 | 30 | afterEach(() => { 31 | console.error = originalConsoleError 32 | }) 33 | 34 | const runTest = (fixtureName: string, ErrorType?: typeof PackageJsonError) => { 35 | const fixturePath = path.join(fixturesDir, fixtureName) 36 | 37 | if (ErrorType) { 38 | expect(() => verifyPackageJSON(fixturePath)).toThrow(ErrorType) 39 | } else { 40 | expect(() => verifyPackageJSON(fixturePath)).not.toThrow() 41 | } 42 | } 43 | 44 | const testCases = [ 45 | {name: '유효한 package.json은 검증을 통과해야 함', fixture: 'valid-package'}, 46 | { 47 | name: 'files 필드가 없으면 NoFilesFieldError를 발생시켜야 함', 48 | fixture: 'no-files-field', 49 | error: NoFilesFieldError, 50 | }, 51 | { 52 | name: 'main 필드가 없으면 NoMainFieldError를 발생시켜야 함', 53 | fixture: 'no-main-field', 54 | error: NoMainFieldError, 55 | }, 56 | { 57 | name: 'exports 필드가 없으면 NoExportsFieldError를 발생시켜야 함', 58 | fixture: 'no-exports-field', 59 | error: NoExportsFieldError, 60 | }, 61 | { 62 | name: 'sideEffects 필드가 없으면 NoSideEffectsFieldError를 발생시켜야 함', 63 | fixture: 'no-side-effects-field', 64 | error: NoSideEffectsFieldError, 65 | }, 66 | { 67 | name: 'dual 패키지에서 module 필드가 없으면 DualPackageModuleFieldError를 발생시켜야 함', 68 | fixture: 'dual-package-no-module', 69 | error: DualPackageModuleFieldError, 70 | }, 71 | { 72 | name: 'TypeScript 패키지에서 types 필드가 없으면 TypescriptTypesFieldError를 발생시켜야 함', 73 | fixture: 'typescript-no-types', 74 | error: TypescriptTypesFieldError, 75 | }, 76 | { 77 | name: 'Dual 패키지에서 CJS와 ESM 출력 파일이 모두 .js 확장자를 사용하면 InvalidFileExtensionError를 발생시켜야 함', 78 | fixture: 'dual-package-invalid-extensions', 79 | error: InvalidFileExtensionError, 80 | }, 81 | { 82 | name: 'Dual 패키지에서 CJS와 ESM 출력 파일이 서로 다른 확장자를 사용하면 에러가 발생하지 않아야 함', 83 | fixture: 'dual-package-valid-extensions', 84 | }, 85 | { 86 | name: 'CJS TypeScript 패키지에서 require.types가 없으면 TypescriptExportMapError를 발생시켜야 함', 87 | fixture: 'cjs-typescript-no-require-types', 88 | error: TypescriptExportMapError, 89 | }, 90 | { 91 | name: 'ESM TypeScript 패키지에서 import.types가 없으면 TypescriptExportMapError를 발생시켜야 함', 92 | fixture: 'esm-typescript-no-import-types', 93 | error: TypescriptExportMapError, 94 | }, 95 | { 96 | name: 'Dual TypeScript 패키지에서 require.types 또는 import.types가 없으면 TypescriptExportMapError를 발생시켜야 함', 97 | fixture: 'dual-typescript-missing-types', 98 | error: TypescriptExportMapError, 99 | }, 100 | {name: '올바른 TypeScript 패키지 구성에서는 에러가 발생하지 않아야 함', fixture: 'valid-typescript-package'}, 101 | { 102 | name: 'module 필드에 .cjs 확장자가 사용되면 InvalidModuleExtensionError를 발생시켜야 함', 103 | fixture: 'invalid-module-extension', 104 | error: InvalidModuleExtensionError, 105 | }, 106 | { 107 | name: '유효하지 않은 types 필드 확장자는 InvalidTypesFileError를 발생시켜야 함', 108 | fixture: 'invalid-types-field-extension', 109 | error: InvalidTypesFileError, 110 | }, 111 | { 112 | name: 'CJS TypeScript 패키지에서 유효한 require.types는 검증을 통과해야 함', 113 | fixture: 'valid-cjs-typescript-package', 114 | }, 115 | { 116 | name: 'CJS TypeScript 패키지에서 유효하지 않은 require.types 확장자는 InvalidTypesFileError를 발생시켜야 함', 117 | fixture: 'invalid-cjs-typescript-types-extension', 118 | error: InvalidTypesFileError, 119 | }, 120 | { 121 | name: 'ESM TypeScript 패키지에서 유효한 import.types는 검증을 통과해야 함', 122 | fixture: 'valid-esm-typescript-package', 123 | }, 124 | { 125 | name: 'ESM TypeScript 패키지에서 유효하지 않은 import.types 확장자는 InvalidTypesFileError를 발생시켜야 함', 126 | fixture: 'invalid-esm-typescript-types-extension', 127 | error: InvalidTypesFileError, 128 | }, 129 | { 130 | name: 'Dual TypeScript 패키지에서 유효한 require.types와 import.types는 검증을 통과해야 함', 131 | fixture: 'valid-dual-typescript-package', 132 | }, 133 | { 134 | name: 'Dual TypeScript 패키지에서 유효하지 않은 require.types 확장자는 InvalidTypesFileError를 발생시켜야 함', 135 | fixture: 'invalid-dual-typescript-require-types-extension', 136 | error: InvalidTypesFileError, 137 | }, 138 | { 139 | name: 'Dual TypeScript 패키지에서 유효하지 않은 import.types 확장자는 InvalidTypesFileError를 발생시켜야 함', 140 | fixture: 'invalid-dual-typescript-import-types-extension', 141 | error: InvalidTypesFileError, 142 | }, 143 | { 144 | name: 'exports 에 ./package.json 이 없으면 에러가 발생함', 145 | fixture: 'no-package-json-exports', 146 | error: MissingPackageJsonExportError, 147 | }, 148 | ] 149 | 150 | testCases.forEach(({name, fixture, error}) => { 151 | test(name, () => { 152 | runTest(fixture, error) 153 | }) 154 | }) 155 | }) 156 | -------------------------------------------------------------------------------- /packages/publint/bin/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import meow from 'meow' 4 | 5 | import {verifyPackageJSON} from '../src/index.js' 6 | 7 | const cli = meow( 8 | ` 9 | Usage 10 | $ @naverpay/publint [directory] 11 | 12 | Examples 13 | $ @naverpay/publint 14 | $ @naverpay/publint ./my-project 15 | `, 16 | { 17 | importMeta: import.meta, 18 | flags: {}, 19 | }, 20 | ) 21 | 22 | const directory = cli.input[0] || '.' 23 | 24 | try { 25 | const result = verifyPackageJSON(directory) 26 | // eslint-disable-next-line no-console 27 | console.log('Package verification successful!') 28 | // eslint-disable-next-line no-console 29 | console.log('Verification results:') 30 | // eslint-disable-next-line no-console 31 | console.log(JSON.stringify(result, null, 2)) 32 | } catch (error) { 33 | if (error instanceof Error) { 34 | // eslint-disable-next-line no-console 35 | console.error(`Verification failed: ${error.message}`) 36 | process.exit(1) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/publint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@naverpay/publint", 3 | "version": "0.0.6", 4 | "description": "", 5 | "main": "dist/src/index.js", 6 | "keywords": [], 7 | "author": "yceffort ", 8 | "type": "module", 9 | "license": "ISC", 10 | "sideEffects": false, 11 | "bin": { 12 | "publint": "./dist/bin/cli.js" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/NaverPayDev/cli" 17 | }, 18 | "files": [ 19 | "dist" 20 | ], 21 | "exports": { 22 | ".": { 23 | "import": "./dist/src/index.js", 24 | "require": "./dist/src/index.js", 25 | "types": "./dist/src/index.d.ts" 26 | }, 27 | "./package.json": "./package.json" 28 | }, 29 | "scripts": { 30 | "build": "tsup", 31 | "test": "vitest run ./__tests__/verifyPackageJSON.test.ts" 32 | }, 33 | "dependencies": { 34 | "meow": "^13.2.0" 35 | }, 36 | "devDependencies": { 37 | "@types/node": "^20.12.12", 38 | "tsup": "^8.2.4", 39 | "vitest": "^2.0.5" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/publint/src/errors.ts: -------------------------------------------------------------------------------- 1 | export class PackageJsonError extends Error { 2 | constructor(message: string) { 3 | super(message) 4 | this.name = 'PackageJsonError' 5 | } 6 | } 7 | 8 | export class NoPackageJsonError extends PackageJsonError { 9 | constructor() { 10 | super(`Please add a 'package.json'`) 11 | this.name = 'NoPackageJsonError' 12 | } 13 | } 14 | 15 | export class NoFilesFieldError extends PackageJsonError { 16 | constructor() { 17 | super(`The 'files' in 'package.json' is required to specify published files`) 18 | this.name = 'NoFilesFieldError' 19 | } 20 | } 21 | 22 | export class NoMainFieldError extends PackageJsonError { 23 | constructor() { 24 | super(`Please add the 'main' to 'package.json'`) 25 | this.name = 'NoMainFieldError' 26 | } 27 | } 28 | 29 | export class NoExportsFieldError extends PackageJsonError { 30 | constructor() { 31 | super(`For better package development, please define an 'exports' in package.json`) 32 | this.name = 'NoExportsFieldError' 33 | } 34 | } 35 | 36 | export class NoSideEffectsFieldError extends PackageJsonError { 37 | constructor() { 38 | super(`For tree shaking, set the 'sideEffects' to 'false' or an array`) 39 | this.name = 'NoSideEffectsFieldError' 40 | } 41 | } 42 | 43 | export class DualPackageModuleFieldError extends PackageJsonError { 44 | constructor() { 45 | super(`In a dual package, please add the 'module' to 'package.json'`) 46 | this.name = 'DualPackageModuleFieldError' 47 | } 48 | } 49 | 50 | export class TypescriptTypesFieldError extends PackageJsonError { 51 | constructor() { 52 | super(`A 'tsconfig' is present. Please add the 'types' to 'package.json'`) 53 | this.name = 'TypescriptTypesFieldError' 54 | } 55 | } 56 | 57 | export class TypescriptExportMapError extends PackageJsonError { 58 | constructor(message: string) { 59 | super(message) 60 | this.name = 'TypescriptExportMapError' 61 | } 62 | } 63 | 64 | export class MissingExportPathError extends PackageJsonError { 65 | constructor(type: string) { 66 | super(`Please add the path for ${type} files in the 'exports'`) 67 | this.name = 'MissingExportPathError' 68 | } 69 | } 70 | 71 | export class InvalidFileExtensionError extends PackageJsonError { 72 | constructor(message: string) { 73 | super(message) 74 | this.name = 'InvalidFileExtensionError' 75 | } 76 | } 77 | 78 | export class InvalidModuleExtensionError extends Error { 79 | constructor() { 80 | super(`The extension of 'module' cannot be '.cjs'`) 81 | this.name = 'InvalidModuleExtensionError' 82 | } 83 | } 84 | 85 | export class InvalidTypesFileError extends Error { 86 | constructor(field: string) { 87 | super(`${field} must have a '.d.ts' extension`) 88 | this.name = 'InvalidTypesFileError' 89 | } 90 | } 91 | 92 | export class InvalidExportError extends Error { 93 | constructor(message: string) { 94 | super(`Invalid export configuration: ${message}`) 95 | this.name = 'InvalidExportError' 96 | } 97 | } 98 | 99 | export class InvalidModuleTypeError extends PackageJsonError { 100 | constructor(message: string) { 101 | super(message) 102 | this.name = 'InvalidModuleTypeError' 103 | } 104 | } 105 | 106 | export class InvalidPathError extends PackageJsonError { 107 | constructor(path: string) { 108 | super(`The file path must start with './': ${path}`) 109 | this.name = 'InvalidPathError' 110 | } 111 | } 112 | 113 | export class MissingPackageJsonExportError extends Error { 114 | constructor() { 115 | super( 116 | `The 'exports' is missing {"./package.json": "./package.json"}. This is required for external access to the package metadata`, 117 | ) 118 | this.name = 'MissingPackageJsonExportError' 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /packages/publint/src/index.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | import {NoPackageJsonError} from './errors.js' 5 | import {IPackageJson} from './types/packageJSON.js' 6 | import {verifyExports} from './verify/exports.js' 7 | import {verifyOutputPaths} from './verify/outputPaths.js' 8 | import {verifyPackageStructure} from './verify/packageStructure.js' 9 | import {verifyPackageType} from './verify/packageType.js' 10 | import {verifyRequiredFields} from './verify/requiredFields.js' 11 | import {verifyTypescriptPackage} from './verify/typescriptPackage.js' 12 | 13 | const PACKAGE_JSON = 'package.json' 14 | const TSCONFIG_JSON = 'tsconfig.json' 15 | 16 | interface PackageVerificationResult { 17 | writtenByTypescript: boolean 18 | packageJSON: IPackageJson 19 | isDualPackage: boolean 20 | } 21 | 22 | export function verifyPackageJSON(packageDir: string): PackageVerificationResult { 23 | // package.json 존재 여부 확인 24 | const packageJSONPath = path.join(packageDir, PACKAGE_JSON) 25 | if (!fs.existsSync(packageJSONPath)) { 26 | throw new NoPackageJsonError() 27 | } 28 | 29 | const packageJSON = JSON.parse(fs.readFileSync(packageJSONPath, 'utf-8')) as IPackageJson 30 | 31 | // exports 필드 검증 32 | verifyExports(packageJSON.exports) 33 | 34 | // 필수 필드 확인 35 | verifyRequiredFields(packageJSON) 36 | 37 | // 패키지 타입 확인 후 각 타입에 맞는 구조를 가지고 있는지 확인 38 | const packageType = verifyPackageType(packageJSON) 39 | 40 | // dual package 면 `module` 필드를 갖도록 선언 41 | verifyPackageStructure(packageJSON, packageType) 42 | 43 | // TypeScript 패키지인 경우 타입 정의 파일이 있는지 확인 44 | const tsConfigPath = path.join(packageDir, TSCONFIG_JSON) 45 | const writtenByTypescript = fs.existsSync(tsConfigPath) 46 | 47 | if (writtenByTypescript) { 48 | verifyTypescriptPackage(packageJSON, packageType) 49 | } 50 | 51 | // exports 필드에 정의된 출력 경로가 올바른지 확인 52 | verifyOutputPaths(packageJSON, packageType, writtenByTypescript) 53 | 54 | return { 55 | writtenByTypescript, 56 | packageJSON, 57 | isDualPackage: packageType === 'dual', 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/publint/src/types/packageJSON.ts: -------------------------------------------------------------------------------- 1 | export interface ExportMap { 2 | require?: string | ConditionalExport 3 | import?: string | ConditionalExport 4 | default?: string 5 | } 6 | 7 | interface ConditionalExport { 8 | types?: string 9 | default?: string 10 | } 11 | 12 | export interface IPackageJson { 13 | name: string 14 | version: string 15 | description?: string 16 | keywords?: string[] 17 | homepage?: string 18 | bugs?: { 19 | url?: string 20 | email?: string 21 | } 22 | license?: string 23 | author?: 24 | | string 25 | | { 26 | name: string 27 | email?: string 28 | url?: string 29 | } 30 | contributors?: ( 31 | | string 32 | | { 33 | name: string 34 | email?: string 35 | url?: string 36 | } 37 | )[] 38 | files?: string[] 39 | main?: string 40 | browser?: string 41 | bin?: Record 42 | man?: string | string[] 43 | directories?: { 44 | lib?: string 45 | bin?: string 46 | man?: string 47 | doc?: string 48 | example?: string 49 | test?: string 50 | } 51 | repository?: { 52 | type?: string 53 | url?: string 54 | directory?: string 55 | } 56 | scripts?: Record 57 | config?: Record 58 | dependencies?: Record 59 | devDependencies?: Record 60 | peerDependencies?: Record 61 | optionalDependencies?: Record 62 | bundledDependencies?: string[] 63 | engines?: Record 64 | os?: string[] 65 | cpu?: string[] 66 | private?: boolean 67 | publishConfig?: Record 68 | workspaces?: string[] | {packages?: string[]; nohoist?: string[]} 69 | types?: string 70 | module?: string 71 | exports?: Record 72 | type?: 'commonjs' | 'module' 73 | sideEffects?: boolean | string[] 74 | } 75 | 76 | export type PackageType = 'cjs' | 'esm' | 'dual' 77 | -------------------------------------------------------------------------------- /packages/publint/src/verify/exports.ts: -------------------------------------------------------------------------------- 1 | import {NoExportsFieldError, InvalidExportError, MissingPackageJsonExportError} from '../errors.js' 2 | import {IPackageJson} from '../types/packageJSON.js' 3 | 4 | // 새로운 에러 타입 추가 5 | 6 | export function verifyExports(exports: IPackageJson['exports']): void { 7 | if (typeof exports !== 'object' || exports === null) { 8 | throw new NoExportsFieldError() 9 | } 10 | 11 | // package.json 엔트리 확인 12 | if (!exports['./package.json'] || exports['./package.json'] !== './package.json') { 13 | throw new MissingPackageJsonExportError() 14 | } 15 | 16 | Object.entries(exports).forEach(([key, value]) => { 17 | if (typeof value === 'string') { 18 | return 19 | } 20 | 21 | if (typeof value !== 'object' || value === null) { 22 | throw new InvalidExportError(`Export for the '${key}' is invalid. It must be an 'object' or a 'string'`) 23 | } 24 | 25 | if (!value.import && !value.require && !value.default) { 26 | throw new InvalidExportError( 27 | `Export for the '${key}' must include at least one of 'import', 'require', or 'default'`, 28 | ) 29 | } 30 | 31 | if (value.import && typeof value.import !== 'string' && typeof value.import !== 'object') { 32 | throw new InvalidExportError(`The 'import' for '${key}' is invalid`) 33 | } 34 | 35 | if (value.require && typeof value.require !== 'string' && typeof value.require !== 'object') { 36 | throw new InvalidExportError(`The 'require' for '${key}' is invalid`) 37 | } 38 | 39 | if (typeof value.import === 'object') { 40 | if (!value.import.default || typeof value.import.default !== 'string') { 41 | throw new InvalidExportError(`The 'import.default' for '${key}' is invalid`) 42 | } 43 | if (value.import.types && typeof value.import.types !== 'string') { 44 | throw new InvalidExportError(`The 'import.types' for '${key}' is invalid`) 45 | } 46 | } 47 | 48 | if (typeof value.require === 'object') { 49 | if (!value.require.default || typeof value.require.default !== 'string') { 50 | throw new InvalidExportError(`The 'require.default' for '${key}' is invalid`) 51 | } 52 | if (value.require.types && typeof value.require.types !== 'string') { 53 | throw new InvalidExportError(`The 'require.types' for '${key}' is invalid`) 54 | } 55 | } 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /packages/publint/src/verify/outputPaths.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | import { 4 | InvalidFileExtensionError, 5 | MissingExportPathError, 6 | TypescriptExportMapError, 7 | InvalidModuleTypeError, 8 | InvalidPathError, 9 | InvalidModuleExtensionError, 10 | } from '../errors.js' 11 | import {IPackageJson, PackageType} from '../types/packageJSON.js' 12 | 13 | interface ExportPath { 14 | default?: string 15 | types?: string 16 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 17 | [key: string]: any 18 | } 19 | 20 | function validatePath(filePath: string): string { 21 | if (!filePath.startsWith('./')) { 22 | throw new InvalidPathError(filePath) 23 | } 24 | return filePath 25 | } 26 | 27 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 28 | function isExportPath(value: any): value is ExportPath { 29 | return typeof value === 'object' && value !== null 30 | } 31 | 32 | export function verifyOutputPaths( 33 | packageJSON: IPackageJson, 34 | packageType: PackageType, 35 | writtenByTypescript: boolean, 36 | ): void { 37 | const {exports, type, module} = packageJSON 38 | 39 | let cjsOutputPath: string | undefined 40 | let esmOutputPath: string | undefined 41 | let cjsTypes: string | undefined 42 | let esmTypes: string | undefined 43 | 44 | const isESM = type === 'module' 45 | 46 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 47 | function traverseExports(obj: any) { 48 | for (const [key, value] of Object.entries(obj)) { 49 | if (typeof value === 'string') { 50 | const validatedPath = validatePath(value) 51 | if (key === 'require') { 52 | cjsOutputPath = cjsOutputPath || validatedPath 53 | } else if (key === 'import') { 54 | esmOutputPath = esmOutputPath || validatedPath 55 | } else if (key === 'types' || key === 'typings') { 56 | cjsTypes = cjsTypes || validatedPath 57 | esmTypes = esmTypes || validatedPath 58 | } else if (!key.startsWith('.')) { 59 | if (isESM) { 60 | esmOutputPath = esmOutputPath || validatedPath 61 | } else { 62 | cjsOutputPath = cjsOutputPath || validatedPath 63 | } 64 | } 65 | } else if (isExportPath(value)) { 66 | if (key === 'require') { 67 | cjsOutputPath = cjsOutputPath || (value.default ? validatePath(value.default) : undefined) 68 | cjsTypes = cjsTypes || (value.types ? validatePath(value.types) : undefined) 69 | } else if (key === 'import') { 70 | esmOutputPath = esmOutputPath || (value.default ? validatePath(value.default) : undefined) 71 | esmTypes = esmTypes || (value.types ? validatePath(value.types) : undefined) 72 | } else { 73 | traverseExports(value) 74 | } 75 | } 76 | } 77 | } 78 | 79 | if (module) { 80 | if (path.extname(module) === '.cjs') { 81 | throw new InvalidModuleExtensionError() 82 | } 83 | esmOutputPath = validatePath(module) 84 | } 85 | 86 | if (typeof exports === 'string') { 87 | esmOutputPath = esmOutputPath || validatePath(exports) 88 | } else if (typeof exports === 'object' && exports !== null) { 89 | traverseExports(exports) 90 | } 91 | 92 | if (isESM && cjsOutputPath && !esmOutputPath) { 93 | throw new InvalidModuleTypeError('Package type is "module" but only CommonJS output is specified') 94 | } 95 | if (!isESM && esmOutputPath && !cjsOutputPath) { 96 | throw new InvalidModuleTypeError('Package type is "commonjs" but only ES module output is specified') 97 | } 98 | 99 | if (packageType === 'dual' && cjsOutputPath && esmOutputPath) { 100 | const cjsExt = path.extname(cjsOutputPath) 101 | const esmExt = path.extname(esmOutputPath) 102 | if (cjsExt === '.js' && esmExt === '.js') { 103 | throw new InvalidFileExtensionError('Dual package CJS and ESM output files must have different extensions') 104 | } 105 | } 106 | 107 | if ((packageType === 'cjs' || packageType === 'dual') && !cjsOutputPath) { 108 | throw new MissingExportPathError('CJS') 109 | } 110 | if ((packageType === 'esm' || packageType === 'dual') && !esmOutputPath) { 111 | throw new MissingExportPathError('ESM') 112 | } 113 | 114 | if (writtenByTypescript) { 115 | if ((packageType === 'cjs' || packageType === 'dual') && !cjsTypes) { 116 | throw new TypescriptExportMapError(`CJS TypeScript packages must include 'require.types' in the 'exports'`) 117 | } 118 | if ((packageType === 'esm' || packageType === 'dual') && !esmTypes) { 119 | throw new TypescriptExportMapError(`ESM TypeScript packages must include 'import.types' in the 'exports'`) 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /packages/publint/src/verify/packageStructure.ts: -------------------------------------------------------------------------------- 1 | import {DualPackageModuleFieldError} from '../errors.js' 2 | import {IPackageJson, PackageType} from '../types/packageJSON.js' 3 | 4 | export function verifyPackageStructure(packageJSON: IPackageJson, packageType: PackageType): void { 5 | const {module} = packageJSON 6 | 7 | if (packageType === 'dual') { 8 | if (!module) { 9 | throw new DualPackageModuleFieldError() 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/publint/src/verify/packageType.ts: -------------------------------------------------------------------------------- 1 | import {NoExportsFieldError} from '../errors.js' 2 | import {IPackageJson, PackageType} from '../types/packageJSON.js' 3 | 4 | export function verifyPackageType({exports, type}: IPackageJson): PackageType { 5 | let hasCjs = false 6 | let hasEsm = false 7 | 8 | if (!exports) { 9 | throw new NoExportsFieldError() 10 | } 11 | 12 | Object.values(exports).forEach((exp) => { 13 | if (typeof exp === 'object' && exp !== null) { 14 | if (exp.require) { 15 | hasCjs = true 16 | } 17 | if (exp.import) { 18 | hasEsm = true 19 | } 20 | if (!exp.import && !exp.require && type) { 21 | type === 'module' ? (hasEsm = true) : (hasCjs = true) 22 | } 23 | } 24 | }) 25 | 26 | if (hasCjs && hasEsm) { 27 | return 'dual' 28 | } 29 | if (hasCjs) { 30 | return 'cjs' 31 | } 32 | if (hasEsm) { 33 | return 'esm' 34 | } 35 | 36 | throw new NoExportsFieldError() 37 | } 38 | -------------------------------------------------------------------------------- /packages/publint/src/verify/requiredFields.ts: -------------------------------------------------------------------------------- 1 | import { 2 | NoFilesFieldError, 3 | NoMainFieldError, 4 | NoExportsFieldError, 5 | NoSideEffectsFieldError, 6 | InvalidPathError, 7 | } from '../errors.js' 8 | import {IPackageJson} from '../types/packageJSON.js' 9 | 10 | function validatePath(filePath: string) { 11 | if (!filePath.startsWith('./')) { 12 | throw new InvalidPathError(filePath) 13 | } 14 | } 15 | 16 | export function verifyRequiredFields(packageJSON: IPackageJson): void { 17 | if (!packageJSON.files) { 18 | throw new NoFilesFieldError() 19 | } 20 | if (!packageJSON.main) { 21 | throw new NoMainFieldError() 22 | } else { 23 | validatePath(packageJSON.main) 24 | } 25 | if (!packageJSON.exports) { 26 | throw new NoExportsFieldError() 27 | } 28 | 29 | if (packageJSON.sideEffects === undefined) { 30 | throw new NoSideEffectsFieldError() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/publint/src/verify/typescriptPackage.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TypescriptTypesFieldError, 3 | TypescriptExportMapError, 4 | InvalidTypesFileError, 5 | InvalidPathError, 6 | } from '../errors.js' 7 | import {IPackageJson, PackageType} from '../types/packageJSON.js' 8 | 9 | function endsWithDTs(filePath: string): boolean { 10 | return filePath.endsWith('.d.ts') || filePath.endsWith('.d.mts') || filePath.endsWith('.d.cts') 11 | } 12 | 13 | function validatePath(filePath: string) { 14 | if (!filePath.startsWith('./')) { 15 | throw new InvalidPathError(filePath) 16 | } 17 | } 18 | 19 | export function verifyTypescriptPackage(packageJSON: IPackageJson, packageType: PackageType): void { 20 | if (!packageJSON.types) { 21 | throw new TypescriptTypesFieldError() 22 | } 23 | 24 | if (!endsWithDTs(packageJSON.types)) { 25 | throw new InvalidTypesFileError(`'types'`) 26 | } 27 | 28 | validatePath(packageJSON.types) 29 | 30 | const mainExport = packageJSON.exports?.['.'] 31 | if (typeof mainExport === 'object') { 32 | let cjsTypes: string | undefined 33 | let esmTypes: string | undefined 34 | 35 | if (typeof mainExport.require === 'string') { 36 | cjsTypes = packageJSON.types 37 | } else if (typeof mainExport.require === 'object') { 38 | cjsTypes = mainExport.require.types 39 | } 40 | 41 | if (typeof mainExport.import === 'string') { 42 | esmTypes = packageJSON.types 43 | } else if (typeof mainExport.import === 'object') { 44 | esmTypes = mainExport.import.types 45 | } 46 | 47 | // 만약 require나 import에 types가 명시되어 있지 않다면, 최상위 types 필드를 사용 48 | cjsTypes = cjsTypes || packageJSON.types 49 | esmTypes = esmTypes || packageJSON.types 50 | 51 | if (cjsTypes && !endsWithDTs(cjsTypes)) { 52 | throw new InvalidTypesFileError(`'exports["."].require.types' or the 'types'`) 53 | } 54 | if (esmTypes && !endsWithDTs(esmTypes)) { 55 | throw new InvalidTypesFileError(`'exports["."].import.types' or the 'types'`) 56 | } 57 | 58 | switch (packageType) { 59 | case 'cjs': 60 | if (!cjsTypes) { 61 | throw new TypescriptExportMapError(`The 'types' for CJS is required`) 62 | } 63 | break 64 | case 'esm': 65 | if (!esmTypes) { 66 | throw new TypescriptExportMapError(`The 'types' for ESM is required`) 67 | } 68 | break 69 | case 'dual': 70 | if (!(cjsTypes && esmTypes)) { 71 | throw new TypescriptExportMapError(`The 'types' for both CJS and ESM is required`) 72 | } 73 | break 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /packages/publint/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "target": "es2021", 5 | "jsx": "react", 6 | "lib": ["dom", "es2021"], 7 | "strict": true, 8 | "allowJs": true, 9 | "allowSyntheticDefaultImports": true, 10 | "skipLibCheck": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "isolatedModules": true, 14 | "preserveConstEnums": true, 15 | "sourceMap": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "resolveJsonModule": true, 18 | "baseUrl": ".", 19 | "declaration": true, 20 | "emitDeclarationOnly": false, 21 | "outDir": "./dist", 22 | "declarationDir": "./dist", 23 | "module": "NodeNext", 24 | "moduleResolution": "NodeNext", 25 | "removeComments": false, 26 | "typeRoots": ["./node_modules/@types"], 27 | "types": ["node"], 28 | "esModuleInterop": true 29 | }, 30 | "include": ["./src", "__tests__", "./bin"], 31 | "exclude": ["node_modules", "dist"] 32 | } 33 | -------------------------------------------------------------------------------- /packages/publint/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts', 'bin/cli.ts'], 5 | format: ['esm'], 6 | dts: true, 7 | splitting: false, 8 | sourcemap: true, 9 | clean: true, 10 | minify: true, 11 | target: 'node16', 12 | outDir: 'dist', 13 | }) 14 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'apps/*' 3 | - 'packages/*' 4 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turborepo.org/schema.json", 3 | "pipeline": { 4 | "build": {"cache": false, "outputs": ["dist/**"]}, 5 | "lint": {"cache": false}, 6 | "lint:fix": {"cache": false}, 7 | "prettier": {"cache": false}, 8 | "prettier:fix": {"cache": false}, 9 | "test": {"cache": false}, 10 | "clean": {"cache": false} 11 | } 12 | } 13 | --------------------------------------------------------------------------------