Monorepo Turborepo + pnpm + tsup
Goal
Goal : 모노레포로 라이브러리 및 프로젝트를 관리한다.
- Tools : turbo, pnpm, tsup, storybook, typescript, eslint, jest
Tasks
- turbo repo 시작하기
- pnpm 명령어 정리 및 의존성 확인하기
- tsup 라이브러리 배포 환경 및 turbo 명령어로 개발환경 구축
Ref
- Vercel Examples : https://github.com/vercel/turborepo/blob/main/examples
📌 pnpm + monorepo
pnpm 명령어 정리
// 1.설치
pnpm i
// 1.1 -r 
// -r은 모든 워크스페이스를 순환시켜주는 명령어 이다. 아래처럼 모든 워크스페이스 마다 pnpm i 명령어 실행
// 하지만 특별히 모노레포의 경우, 루트에서 pnpm i으로만 충분
pnpm i -r
// 2.모노레포의 의존성 link
// 2.1 로컬 의존성 설치하는 방법  
pnpm add package-name  
pnpm add package-name -D  
// 2.2 여러 레포에 한번에 의존성 링크하기  
pnpm add @org/ui --filter apps/web  
// 2.3 수동으로 설치하기
- "@org/ui":"workspace:*" -> pnpm i  
// 2.4 의존성 링크 확인  
pnpm list  
// 참고
pnpm link package-name  // 의존성 링크 만들기 
pnpm unlink package-name // 의존성 링크 제거  
pnpm list // 현재 의존성 리스트  
// 3.글로벌 의존성 만들기
// 로컬의 의존성을 다른 레포에서 테스트 할수 있다.  
pnpm link --global              // (로컬모듈) 글로벌 링크 만들기
pnpm link package-name --global // (test) 글로벌의 로컬모듈을 test 레포에 설치(하드 링크 방식)   
//? pnpm list --global              // (test) 링크 확인  
pnpm unlink --global            // (로컬모듈) 글로벌 링크 제거
// 캐시 클리어 
pnpm store prune 
// 워크스페이스 명령어
pnpm install -r  
// 모든 워크스페이스에서 node_modules 제거하기  
pnpm -r exec rm -rf node_mouldes  
// 의존성 제거, 업데이트
pnpm remove 
pnpm update 
// script 수행 
pnpm run script-name  
workspace
모노레포는 node 진영에서는 workspace라는 개념으로 통한다.
- pnpm-workspace.yaml 파일 생성
packages:
  - "packages/*"
  - "apps/*"
- turbo.json 파일 생성
turbo 주요 명령어
turbo run <task>: 지정된 작업을 실행합니다.
turbo run dev --filter=reco-common-ui --ui
turbo dev: 개발 모드에서 작업을 실행합니다.
turbo build: 빌드 작업을 실행합니다.
turbo lint: 린트 작업을 실행합니다.
turbo test: 테스트 작업을 실행합니다.
turbo prune: 사용되지 않는 패키지를 제거합니다.
turbo clean: 캐시와 빌드 아티팩트를 정리합니다.
Setup tsup
export const tsup: Options = {
  entry: ['src/**/*.tsx', 'src/**/*.ts', 'src/**/*.css'],
  outDir: 'dist',
  format: ['cjs', 'esm'],
  target: 'es2015',
  dts: true,
  bundle: true,
  splitting: false,
  clean: true,
  skipNodeModulesBundle: true,
  sourcemap: true,
  minify:false,
  watch: env === 'development',
};
---
package.json
  "type": "module",
  "main": "dist/index.cjs",
  "module": "dist/index.js",
  "types": "dist/index.d.ts",
  "exports": {
    ".": {
      "require": {
        "types": "./dist/index.d.cts",
        "default": "./dist/index.cjs"
      },
      "import": {
        "types": "./dist/index.d.ts",
        "default": "./dist/index.js"
      }
    }
  },
Setup tsup (Single Entry)
// 0.src/index.ts  
// 배럴 파일에서는 이름이 겹칠 수 있다. renamed & re-export 하는 방법을 사용한다.
// export * from './calcuator/index.js';
// export * from './calcuator-v2/index.js';
import * as CalculatorV1 from './calcuator/index.js';
import * as CalculatorV2 from './calcuator-v2/index.js';
export { CalculatorV1, CalculatorV2 };
// 1.
// tsup.config.ts
import { defineConfig } from 'tsup';
const env = process.env.NODE_ENV;
export default defineConfig({
  // entry 는 포함할 파일 목록이다. 모듈노출은 package.json 에서 진행한다.   
  entry: ['src/**/*.ts'], // glob 패턴 사용
  outDir: 'dist', // 출력 디렉토리
  format: ['cjs', 'esm'], // CommonJS와 ESM 형식 출력
  dts: true, // 타입 선언 파일 생성
  sourcemap: env === 'development', // 소스맵 생성
  target: 'es2015',
  /* optimization */
  minify: true, // 코드 최소화 여부
  bundle: true,
  splitting: false,
  skipNodeModulesBundle: true,
  /* dev */
  clean: true, // 빌드 전에 디렉토리 정리
  watch: env === 'development',
});
---
// 2.
// package.json
{
  "name": "@dodo/blocks",
  "version": "0.0.1",
  "description": "",
  "scripts": {
    "dev": "NODE_ENV=development tsup",
    "build": "NODE_ENV=production tsup",
    "npm-publihs": ""
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "type": "module",
  "exports": {
    "./calcuator": {
      "require": {
        "types": "./dist/calcuator/index.d.cts",
        "default": "./dist/calcuator/index.cjs"
      },
      "import": {
        "types": "./dist/calcuator/index.d.ts",
        "default": "./dist/calcuator/index.js"
      }
    },
    "./calcuator-v2": {
      "require": {
        "types": "./dist/calcuator-v2/index.d.cts",
        "default": "./dist/calcuator-v2/index.cjs"
      },
      "import": {
        "types": "./dist/calcuator-v2/index.d.ts",
        "default": "./dist/calcuator-v2/index.js"
      }
    }
  },
  "devDependencies": {
    "@repo/eslint-config": "workspace:*",
    "@repo/typescript-config": "workspace:*",
    "@turbo/gen": "^1.12.4",
    "@types/node": "^20.11.24",
    "@types/eslint": "^8.56.5",
    "@types/react": "^18.2.61",
    "@types/react-dom": "^18.2.19",
    "eslint": "^8.57.0",
    "react": "^18.2.0",
    "typescript": "5.5.4",
    "tsup": "^8.3.5"
  }
}
//3.tsconfig.json
{
  "extends": "@repo/typescript-config/react-library.json",
  "compilerOptions": {
    "outDir": "dist"
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}
Setup tsup (Multiple Entry)
- 단점 : 모듈이 늘어날때마다 베이스 코드작업을 해야한다.
// 1.src/index.ts  
export * from './calcuator/index.js';
export * from './calcuator-v2/index.js';
// 2.
// package.json
{
  "name": "@dodo/blocks",
  "version": "0.0.1",
  "description": "",
  "scripts": {
    "dev": "NODE_ENV=development tsup",
    "build": "NODE_ENV=production tsup",
    "npm-publihs": ""
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "type": "module",
  "exports": {
    "./calcuator": {
      "require": {
        "types": "./dist/calcuator/index.d.cts",
        "default": "./dist/calcuator/index.cjs"
      },
      "import": {
        "types": "./dist/calcuator/index.d.ts",
        "default": "./dist/calcuator/index.js"
      }
    },
    "./calcuator-v2": {
      "require": {
        "types": "./dist/calcuator-v2/index.d.cts",
        "default": "./dist/calcuator-v2/index.cjs"
      },
      "import": {
        "types": "./dist/calcuator-v2/index.d.ts",
        "default": "./dist/calcuator-v2/index.js"
      }
    }
  },
  "devDependencies": {
    "@repo/eslint-config": "workspace:*",
    "@repo/typescript-config": "workspace:*",
    "@turbo/gen": "^1.12.4",
    "@types/node": "^20.11.24",
    "@types/eslint": "^8.56.5",
    "@types/react": "^18.2.61",
    "@types/react-dom": "^18.2.19",
    "eslint": "^8.57.0",
    "react": "^18.2.0",
    "typescript": "5.5.4",
    "tsup": "^8.3.5"
  }
}
Setup tsup with bin script (npx cli)
// 1.
// src/scripts/indext.ts
#!/usr/bin/env node
import { Command } from 'commander';
const program = new Command();
program
  .name('@dodolabs/blocks')
  .description('CLI tool for math operations')
  .version('1.0.0');
// `adder` 명령어 정의
program
  .command('adder')
  .description('Add two numbers')
  .option('--a <number>', 'First number', '0')
  .option('--b <number>', 'Second number', '0')
  .action((options) => {
    const a = parseFloat(options.a);
    const b = parseFloat(options.b);
    console.log(`Result: ${a + b}`);
  });
// 명령어 파싱
program.parse(process.argv);
// 2.package.json
// add bin field  
  "bin": {
    "@dodolabs/blocks": "./dist/scripts/index.js"
  },
// 3.build
// "build": "NODE_ENV=production tsup",
// 4.Local Test in inside Module 
node ./dist/scripts/index.js
// 📕 현재 선언된 모듈이 type:"module" 이므로 ESM 스크립으를 구동한다.  
// 5.Local test in outside Module  
// pnpm link --global  
// pnpm link @dodolabs/blocks --global  
// npx @dodolabs/blocks adder --a=1 --b=2  
// pnpm unlink @dodolabs/blocks --global    
// 6.publish  
    "npm-publish": "pnpm build && npm publish"
Local test
Test in another project
// 1.
// pnpm setup 명령어를 실행하여 pnpm이 자동으로 전역 바이너리 디렉토리를 설정하도록 합니다.
pnpm setup
source ~/.zshrc 
// 2.
// 로컬모듈에서 글로벌 링크 만들기  
pnpm link --global
// 3.
// 테스트 레포로 가서 방금 만든 로컬 모듈 연결하기  
// pnpm install 은 필요없다.  
pnpm link --global @dodo/blocks
//확인해보기 (잘안된다.?)
pnpm list
// 4. 테스트 후 링크제거
pnpm unlink --global @dodo/blocks
cjs test
- change package.json type to commonjs
const { adder } = require("@dodo/blocks/calcuator-v2");
console.log(typeof module !== "undefined" ? "CommonJS" : "ES Module"); // CommonJS
console.log(adder(1, 2));
mjs test
- change package.json type to module
// ---
import { adder } from "@dodo/blocks/calcuator-v2";
// module 이라는 전역 객체가 존재한다.
console.log(typeof module !== "undefined" ? "CommonJS" : "ES Module"); // ES Module
console.log(adder(1, 2));
publish to npm regitry
// 1.
아래와 같이 내 계정이름과 다른 접두사를 가진 패키지를 배포하려면 조직을 만들어야 한다.  
- @dodolabs/blocks
- npm registry에서 조직을 만들자.   
// 2.
npm login  
// 3.
npm publish
download from npm registry
pnpm i @dodolabs/blocks