From 7a7fd16aa857691ff9b054387b19afd5a42e2ab0 Mon Sep 17 00:00:00 2001 From: p-iknow Date: Tue, 12 May 2026 15:35:13 +0900 Subject: [PATCH 1/7] feat: introduce react-hook-kit package --- .gitignore | 13 +- .oxfmtrc.json | 2 + .oxlintrc.json | 8 + apps/next-sample/next-env.d.ts | 7 - apps/next-sample/next.config.ts | 7 - apps/next-sample/package.json | 26 - apps/next-sample/src/app/layout.tsx | 16 - apps/next-sample/src/app/page.tsx | 19 - apps/next-sample/tsconfig.json | 16 - apps/tanstack-sample/package.json | 37 - apps/tanstack-sample/src/routeTree.gen.ts | 68 - apps/tanstack-sample/src/router.tsx | 18 - apps/tanstack-sample/src/routes/__root.tsx | 40 - apps/tanstack-sample/src/routes/index.tsx | 17 - apps/tanstack-sample/src/styles/app.css | 1 - apps/tanstack-sample/tsconfig.json | 12 - apps/tanstack-sample/vite.config.ts | 15 - apps/tanstack-sample/vitest.config.ts | 12 - apps/tanstack-sample/vitest.setup.ts | 2 - docs/01-pnpm-workspace.md | 125 - docs/02-module-resolution.md | 170 - docs/03-live-types.md | 219 - docs/04-turbo-and-tsconfig-references.md | 187 - docs/README.md | 46 - knip.json | 28 +- package.json | 13 +- packages/react-hook-kit/package.json | 102 + packages/react-hook-kit/src/hooks.test.ts | 110 + packages/react-hook-kit/src/index.ts | 9 + packages/react-hook-kit/src/use-boolean.ts | 33 + packages/react-hook-kit/src/use-counter.ts | 67 + packages/react-hook-kit/src/use-debounce.ts | 17 + .../react-hook-kit/src/use-local-storage.ts | 96 + packages/react-hook-kit/src/use-toggle.ts | 21 + packages/react-hook-kit/tsconfig.json | 12 + packages/react-hook-kit/tsdown.config.ts | 48 + packages/react-hook-kit/vitest.config.ts | 10 + packages/sample/package.json | 19 - packages/sample/src/index.test.ts | 8 - packages/sample/src/index.ts | 1 - packages/sample/tsconfig.json | 15 - pnpm-lock.yaml | 5969 ++++++++++++++--- pnpm-workspace.yaml | 15 +- scripts/init.mts | 26 - scripts/remove-all.mts | 142 - scripts/remove-app.mts | 190 - sheriff.config.ts | 22 +- tsconfig.base.json | 3 +- tsconfig.json | 7 +- tsconfig.scripts.json | 13 - turbo.json | 4 +- vitest.workspace.ts | 7 +- 52 files changed, 5483 insertions(+), 2607 deletions(-) delete mode 100644 apps/next-sample/next-env.d.ts delete mode 100644 apps/next-sample/next.config.ts delete mode 100644 apps/next-sample/package.json delete mode 100644 apps/next-sample/src/app/layout.tsx delete mode 100644 apps/next-sample/src/app/page.tsx delete mode 100644 apps/next-sample/tsconfig.json delete mode 100644 apps/tanstack-sample/package.json delete mode 100644 apps/tanstack-sample/src/routeTree.gen.ts delete mode 100644 apps/tanstack-sample/src/router.tsx delete mode 100644 apps/tanstack-sample/src/routes/__root.tsx delete mode 100644 apps/tanstack-sample/src/routes/index.tsx delete mode 100644 apps/tanstack-sample/src/styles/app.css delete mode 100644 apps/tanstack-sample/tsconfig.json delete mode 100644 apps/tanstack-sample/vite.config.ts delete mode 100644 apps/tanstack-sample/vitest.config.ts delete mode 100644 apps/tanstack-sample/vitest.setup.ts delete mode 100644 docs/01-pnpm-workspace.md delete mode 100644 docs/02-module-resolution.md delete mode 100644 docs/03-live-types.md delete mode 100644 docs/04-turbo-and-tsconfig-references.md delete mode 100644 docs/README.md create mode 100644 packages/react-hook-kit/package.json create mode 100644 packages/react-hook-kit/src/hooks.test.ts create mode 100644 packages/react-hook-kit/src/index.ts create mode 100644 packages/react-hook-kit/src/use-boolean.ts create mode 100644 packages/react-hook-kit/src/use-counter.ts create mode 100644 packages/react-hook-kit/src/use-debounce.ts create mode 100644 packages/react-hook-kit/src/use-local-storage.ts create mode 100644 packages/react-hook-kit/src/use-toggle.ts create mode 100644 packages/react-hook-kit/tsconfig.json create mode 100644 packages/react-hook-kit/tsdown.config.ts create mode 100644 packages/react-hook-kit/vitest.config.ts delete mode 100644 packages/sample/package.json delete mode 100644 packages/sample/src/index.test.ts delete mode 100644 packages/sample/src/index.ts delete mode 100644 packages/sample/tsconfig.json delete mode 100644 scripts/init.mts delete mode 100644 scripts/remove-all.mts delete mode 100644 scripts/remove-app.mts delete mode 100644 tsconfig.scripts.json diff --git a/.gitignore b/.gitignore index cb78985..64c0d41 100644 --- a/.gitignore +++ b/.gitignore @@ -4,21 +4,10 @@ node_modules # Build outputs dist .output +.astro tmp *.tsbuildinfo -# TanStack Start -# routeTree.gen.ts is checked in so `typecheck` works without a prior build; -# `vite dev`/`vite build` regenerates it on every run. -.tanstack -.vinxi -.nitro - -# Next.js -.next/ -next-env.d.ts -!apps/*/next-env.d.ts - # Environment & secrets .env .env.* diff --git a/.oxfmtrc.json b/.oxfmtrc.json index b17e7b9..3fbca39 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -13,6 +13,8 @@ "dist", ".output", ".next", + ".astro", + "**/.astro", ".omc", "pnpm-lock.yaml", "*.md", diff --git a/.oxlintrc.json b/.oxlintrc.json index 897ce8e..8329a69 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -17,6 +17,12 @@ "rules": { "no-console": "off" } + }, + { + "files": ["**/*.astro"], + "rules": { + "import/no-unassigned-import": "off" + } } ], "env": { @@ -29,6 +35,8 @@ "dist", ".output", ".next", + ".astro", + "**/.astro", ".omc", "*.gen.ts", "routeTree.gen.ts" diff --git a/apps/next-sample/next-env.d.ts b/apps/next-sample/next-env.d.ts deleted file mode 100644 index 3dfcfa3..0000000 --- a/apps/next-sample/next-env.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -/// -/// -// oxlint-disable-next-line import/no-unassigned-import -- includes Next.js generated route types -import "./.next/types/routes.d.ts"; - -// NOTE: This file should not be edited -// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/next-sample/next.config.ts b/apps/next-sample/next.config.ts deleted file mode 100644 index 0760261..0000000 --- a/apps/next-sample/next.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { NextConfig } from 'next' - -const nextConfig: NextConfig = { - transpilePackages: ['@repo/sample'], -} - -export default nextConfig diff --git a/apps/next-sample/package.json b/apps/next-sample/package.json deleted file mode 100644 index 0b583bb..0000000 --- a/apps/next-sample/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "@repo/next-sample", - "version": "0.0.0", - "private": true, - "type": "module", - "scripts": { - "dev": "next dev -p 3002", - "build": "next build", - "start": "next start -p 3002", - "typecheck": "tsc --noEmit", - "test": "vitest run --passWithNoTests" - }, - "dependencies": { - "@repo/sample": "workspace:*", - "next": "catalog:", - "react": "catalog:", - "react-dom": "catalog:" - }, - "devDependencies": { - "@types/node": "catalog:", - "@types/react": "catalog:", - "@types/react-dom": "catalog:", - "typescript": "catalog:", - "vitest": "catalog:" - } -} diff --git a/apps/next-sample/src/app/layout.tsx b/apps/next-sample/src/app/layout.tsx deleted file mode 100644 index 345f925..0000000 --- a/apps/next-sample/src/app/layout.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import type { Metadata } from 'next' -import type { ReactNode } from 'react' - -export const metadata: Metadata = { - title: 'next-sample', -} - -export default function RootLayout({ children }: Readonly<{ children: ReactNode }>) { - return ( - - - {children} - - - ) -} diff --git a/apps/next-sample/src/app/page.tsx b/apps/next-sample/src/app/page.tsx deleted file mode 100644 index e4f9680..0000000 --- a/apps/next-sample/src/app/page.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { greet } from '@repo/sample' - -export default function Page() { - return ( -
-
-

next-sample

-

{greet('Next.js')}

-
-
- ) -} diff --git a/apps/next-sample/tsconfig.json b/apps/next-sample/tsconfig.json deleted file mode 100644 index 14572b4..0000000 --- a/apps/next-sample/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "jsx": "preserve", - "lib": ["DOM", "DOM.Iterable", "ES2022"], - "plugins": [{ "name": "next" }], - "paths": { - "~/*": ["./src/*"], - "@/*": ["./src/*"] - }, - "incremental": true - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"], - "references": [{ "path": "../../packages/sample" }] -} diff --git a/apps/tanstack-sample/package.json b/apps/tanstack-sample/package.json deleted file mode 100644 index 980d6c7..0000000 --- a/apps/tanstack-sample/package.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "@repo/tanstack-sample", - "version": "0.0.0", - "private": true, - "type": "module", - "sideEffects": false, - "scripts": { - "dev": "vite dev", - "build": "vite build", - "preview": "vite preview", - "start": "node .output/server/index.mjs", - "typecheck": "tsc --noEmit", - "test": "vitest run --passWithNoTests" - }, - "dependencies": { - "@repo/sample": "workspace:*", - "@tanstack/react-router": "catalog:", - "@tanstack/react-router-devtools": "catalog:", - "@tanstack/react-start": "catalog:", - "react": "catalog:", - "react-dom": "catalog:" - }, - "devDependencies": { - "@tailwindcss/vite": "catalog:", - "@testing-library/jest-dom": "catalog:", - "@testing-library/react": "catalog:", - "@types/react": "catalog:", - "@types/react-dom": "catalog:", - "@vitejs/plugin-react": "catalog:", - "jsdom": "catalog:", - "tailwindcss": "catalog:", - "typescript": "catalog:", - "vite": "catalog:", - "vite-tsconfig-paths": "catalog:", - "vitest": "catalog:" - } -} diff --git a/apps/tanstack-sample/src/routeTree.gen.ts b/apps/tanstack-sample/src/routeTree.gen.ts deleted file mode 100644 index dceedff..0000000 --- a/apps/tanstack-sample/src/routeTree.gen.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* eslint-disable */ - -// @ts-nocheck - -// noinspection JSUnusedGlobalSymbols - -// This file was automatically generated by TanStack Router. -// You should NOT make any changes in this file as it will be overwritten. -// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. - -import { Route as rootRouteImport } from './routes/__root' -import { Route as IndexRouteImport } from './routes/index' - -const IndexRoute = IndexRouteImport.update({ - id: '/', - path: '/', - getParentRoute: () => rootRouteImport, -} as any) - -export interface FileRoutesByFullPath { - '/': typeof IndexRoute -} -export interface FileRoutesByTo { - '/': typeof IndexRoute -} -export interface FileRoutesById { - __root__: typeof rootRouteImport - '/': typeof IndexRoute -} -export interface FileRouteTypes { - fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' - fileRoutesByTo: FileRoutesByTo - to: '/' - id: '__root__' | '/' - fileRoutesById: FileRoutesById -} -export interface RootRouteChildren { - IndexRoute: typeof IndexRoute -} - -declare module '@tanstack/react-router' { - interface FileRoutesByPath { - '/': { - id: '/' - path: '/' - fullPath: '/' - preLoaderRoute: typeof IndexRouteImport - parentRoute: typeof rootRouteImport - } - } -} - -const rootRouteChildren: RootRouteChildren = { - IndexRoute: IndexRoute, -} -export const routeTree = rootRouteImport - ._addFileChildren(rootRouteChildren) - ._addFileTypes() - -import type { getRouter } from './router.tsx' -import type { createStart } from '@tanstack/react-start' -declare module '@tanstack/react-start' { - interface Register { - ssr: true - router: Awaited> - } -} diff --git a/apps/tanstack-sample/src/router.tsx b/apps/tanstack-sample/src/router.tsx deleted file mode 100644 index 6544aaf..0000000 --- a/apps/tanstack-sample/src/router.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { createRouter } from '@tanstack/react-router' -import { routeTree } from './routeTree.gen' - -export function getRouter() { - const router = createRouter({ - routeTree, - defaultPreload: 'intent', - scrollRestoration: true, - }) - - return router -} - -declare module '@tanstack/react-router' { - interface Register { - router: ReturnType - } -} diff --git a/apps/tanstack-sample/src/routes/__root.tsx b/apps/tanstack-sample/src/routes/__root.tsx deleted file mode 100644 index bc68344..0000000 --- a/apps/tanstack-sample/src/routes/__root.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/// -import type { ReactNode } from 'react' -import { Outlet, createRootRoute, HeadContent, Scripts } from '@tanstack/react-router' -import { TanStackRouterDevtools } from '@tanstack/react-router-devtools' -import appCss from '~/styles/app.css?url' - -export const Route = createRootRoute({ - head: () => ({ - meta: [ - { charSet: 'utf-8' }, - { name: 'viewport', content: 'width=device-width, initial-scale=1' }, - { title: 'tanstack-sample' }, - ], - links: [{ rel: 'stylesheet', href: appCss }], - }), - component: RootComponent, -}) - -function RootComponent() { - return ( - - - - ) -} - -function RootDocument({ children }: Readonly<{ children: ReactNode }>) { - return ( - - - - - - {children} - - - - - ) -} diff --git a/apps/tanstack-sample/src/routes/index.tsx b/apps/tanstack-sample/src/routes/index.tsx deleted file mode 100644 index 3a1a0cd..0000000 --- a/apps/tanstack-sample/src/routes/index.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router' -import { greet } from '@repo/sample' - -export const Route = createFileRoute('/')({ - component: Home, -}) - -function Home() { - return ( -
-
-

tanstack-sample

-

{greet('TanStack Start')}

-
-
- ) -} diff --git a/apps/tanstack-sample/src/styles/app.css b/apps/tanstack-sample/src/styles/app.css deleted file mode 100644 index d4b5078..0000000 --- a/apps/tanstack-sample/src/styles/app.css +++ /dev/null @@ -1 +0,0 @@ -@import 'tailwindcss'; diff --git a/apps/tanstack-sample/tsconfig.json b/apps/tanstack-sample/tsconfig.json deleted file mode 100644 index f210e05..0000000 --- a/apps/tanstack-sample/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "include": ["**/*.ts", "**/*.tsx"], - "compilerOptions": { - "jsx": "react-jsx", - "lib": ["DOM", "DOM.Iterable", "ES2022"], - "paths": { - "~/*": ["./src/*"] - } - }, - "references": [{ "path": "../../packages/sample" }] -} diff --git a/apps/tanstack-sample/vite.config.ts b/apps/tanstack-sample/vite.config.ts deleted file mode 100644 index 7be316c..0000000 --- a/apps/tanstack-sample/vite.config.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { defineConfig } from 'vite' -import { tanstackStart } from '@tanstack/react-start/plugin/vite' -import viteReact from '@vitejs/plugin-react' -import tsConfigPaths from 'vite-tsconfig-paths' -import tailwindcss from '@tailwindcss/vite' - -export default defineConfig({ - server: { - port: 3001, - }, - resolve: { - conditions: ['@repo/source'], - }, - plugins: [tailwindcss(), tsConfigPaths(), tanstackStart(), viteReact()], -}) diff --git a/apps/tanstack-sample/vitest.config.ts b/apps/tanstack-sample/vitest.config.ts deleted file mode 100644 index a55c7d4..0000000 --- a/apps/tanstack-sample/vitest.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { defineConfig } from 'vitest/config' -import viteReact from '@vitejs/plugin-react' -import tsConfigPaths from 'vite-tsconfig-paths' - -export default defineConfig({ - plugins: [tsConfigPaths(), viteReact()], - test: { - environment: 'jsdom', - setupFiles: ['./vitest.setup.ts'], - globals: true, - }, -}) diff --git a/apps/tanstack-sample/vitest.setup.ts b/apps/tanstack-sample/vitest.setup.ts deleted file mode 100644 index 1973856..0000000 --- a/apps/tanstack-sample/vitest.setup.ts +++ /dev/null @@ -1,2 +0,0 @@ -// oxlint-disable-next-line import/no-unassigned-import -- registers jest-dom matchers for Vitest -import '@testing-library/jest-dom/vitest' diff --git a/docs/01-pnpm-workspace.md b/docs/01-pnpm-workspace.md deleted file mode 100644 index 327e940..0000000 --- a/docs/01-pnpm-workspace.md +++ /dev/null @@ -1,125 +0,0 @@ -# 01. pnpm 워크스페이스 - -이 템플릿은 모노레포 도구로 `pnpm` 워크스페이스를 사용한다. Turborepo도 함께 쓰지만 Turbo는 **태스크 러너 / 캐시**일 뿐, 패키지의 위치와 의존 관계는 전적으로 `pnpm`이 관리한다 (Turbo의 역할은 [04 문서](./04-turbo-and-tsconfig-references.md) 참조). - -## 워크스페이스 선언 - -```yaml -# pnpm-workspace.yaml -packages: - - 'apps/*' - - 'packages/*' - -catalog: - typescript: ~6.0.3 - vitest: ^4.1.4 - react: ^19.2.5 - react-dom: ^19.2.5 - next: ^16.0.0 - # … -``` - -`packages` 글롭이 매칭하는 모든 디렉토리(현재는 `apps/tanstack-sample`, `apps/next-sample`, `packages/sample`)가 워크스페이스 멤버가 된다. 각 디렉토리의 `package.json#name`이 그 워크스페이스의 식별자다. - -## `workspace:*` 프로토콜과 심볼릭 링크 - -워크스페이스 내부에서 다른 멤버를 의존성으로 쓸 때는 `workspace:*`로 선언한다. - -```jsonc -// apps/tanstack-sample/package.json -{ - "dependencies": { - "@repo/sample": "workspace:*" - } -} -``` - -`pnpm install` 시점에 pnpm은 이 선언을 보고 다음과 같은 **심볼릭 링크**를 만든다. - -``` -apps/tanstack-sample/node_modules/@repo/sample - → ../../packages/sample -``` - -링크가 생기면 TypeScript도, Vite도, Next.js도 `@repo/sample`을 일반 npm 패키지처럼 취급한다. `node_modules/@repo/sample/package.json`을 읽고 `exports` 필드를 평가한다 — 패키지 해석 알고리즘이 워크스페이스 멤버라는 사실을 따로 알 필요가 없다. - -## `.npmrc` — 왜 `hoist=false`인가 - -```ini -# .npmrc -shamefully-hoist=false -strict-peer-dependencies=false -auto-install-peers=true -hoist=false -``` - -`hoist=false`가 핵심이다. pnpm의 기본은 의존성을 워크스페이스 루트의 `node_modules`로 부분 호이스팅하지만, 이 옵션을 끄면 **각 워크스페이스가 자기 `node_modules`만 본다**. - -``` -apps/tanstack-sample/node_modules/ - @repo/sample → symlink - @tanstack/react-router → 실제 패키지 (자신의 dependencies에 선언했음) - react → 실제 패키지 - -apps/next-sample/node_modules/ - @repo/sample → symlink - next → 실제 패키지 - react → 실제 패키지 (별도 인스턴스) -``` - -이 구조가 보장하는 것: - -- **Phantom dependency 차단** — `apps/next-sample`이 `package.json`에 적지 않은 `@tanstack/react-router`를 코드에서 우연히 import해도, 자기 `node_modules`에 없으므로 즉시 깨진다. 다른 워크스페이스 덕분에 잠깐 굴러가는 import가 생기지 않는다. -- **모듈 해석의 단일 진실 공급원** — 각 워크스페이스의 `package.json` + 자기 `node_modules`만 보면 된다. 상위 디렉토리를 추적할 일이 없다. - -`auto-install-peers=true`는 peer 의존성을 자동 설치하도록 두는 보조 설정이다. `strict-peer-dependencies=false`는 peer 충돌을 에러가 아닌 경고로 낮춰 워크스페이스의 일상 작업을 막지 않게 한다. - -## `catalog` — 버전 단일화 - -`pnpm-workspace.yaml`의 `catalog:` 블록에 버전을 한 번 적어두고, 각 워크스페이스의 `package.json`에서는 `catalog:` 키워드로 참조한다. - -```jsonc -// apps/tanstack-sample/package.json -{ - "dependencies": { - "react": "catalog:", - "react-dom": "catalog:" - }, - "devDependencies": { - "vite": "catalog:", - "typescript": "catalog:" - } -} -``` - -`pnpm install`이 `catalog:`를 카탈로그 선언으로 치환하므로, 모든 워크스페이스가 같은 React/TypeScript/Vitest 버전을 쓴다는 것이 구조적으로 보장된다. 버전 업그레이드는 `pnpm-workspace.yaml` 한 곳만 수정하면 된다. - -카탈로그는 모듈 해석 규칙에는 직접 영향을 주지 않는다. 하지만 모든 워크스페이스가 같은 TypeScript 버전을 쓴다는 보장이 있어야 [02 문서](./02-module-resolution.md)에서 다루는 `moduleResolution: "Bundler"`와 `customConditions` 같은 설정이 일관되게 동작한다. - -## `pnpm.onlyBuiltDependencies` — 설치 시 postinstall 허용 목록 - -```jsonc -// package.json (root) -{ - "pnpm": { - "onlyBuiltDependencies": [ - "esbuild", - "turbo" - ] - } -} -``` - -pnpm 10부터는 보안상 기본값으로 모든 의존성의 `postinstall` 스크립트 실행을 차단한다. 네이티브 바이너리를 받아야 하는 `esbuild`(Vite·Next 빌드에 필요)와 `turbo`(태스크 러너 바이너리)만 명시적으로 허용한다. 새로운 의존성이 빌드 단계에서 native binary를 필요로 한다면 이 목록에 추가해야 한다. - -## 정리 - -| 설정 | 보장하는 것 | -|------|-------------| -| `pnpm-workspace.yaml` `packages` | 어느 디렉토리가 워크스페이스 멤버인지 | -| `workspace:*` | 내부 패키지가 심볼릭 링크로 연결됨 → 일반 npm 패키지처럼 해석 가능 | -| `hoist=false` | 각 워크스페이스의 `node_modules`가 모듈 해석의 유일한 출처 → phantom dependency 차단 | -| `catalog:` | 모든 워크스페이스의 공통 의존성 버전이 한 곳에서 관리됨 | -| `onlyBuiltDependencies` | 신뢰하는 패키지만 설치 시 스크립트 실행 | - -다음: [02. TypeScript 모듈 해석](./02-module-resolution.md) diff --git a/docs/02-module-resolution.md b/docs/02-module-resolution.md deleted file mode 100644 index 3ec0edf..0000000 --- a/docs/02-module-resolution.md +++ /dev/null @@ -1,170 +0,0 @@ -# 02. TypeScript 모듈 해석 - -`import { greet } from "@repo/sample"`라고 썼을 때, TypeScript와 런타임(Vite, Next.js)이 각각 어떤 알고리즘으로 실제 파일을 찾는지 정리한다. - -## 이 템플릿의 설정 - -```jsonc -// tsconfig.base.json -{ - "compilerOptions": { - "module": "ESNext", - "moduleResolution": "Bundler", - "customConditions": ["@repo/source"], - "noEmit": true, - // … - } -} -``` - -세 가지가 핵심이다. - -- `module: "ESNext"` — 컴파일러가 ES 모듈 문법(import/export)을 그대로 출력. 번들러 환경의 표준값. -- `moduleResolution: "Bundler"` — 번들러(Vite, Next.js, esbuild 등)의 해석 규칙을 그대로 따라간다. TypeScript 5.0에서 도입. -- `customConditions: ["@repo/source"]` — `package.json#exports`의 커스텀 조건을 인식하게 한다. 본격적인 사용 사례는 [03 문서](./03-live-types.md)에서 다룬다. - -## 왜 `moduleResolution: "Bundler"`인가 - -이 템플릿의 두 앱은 모두 번들러를 거친다. - -- `apps/tanstack-sample` — Vite (dev 서버, 빌드, 프로덕션 SSR 모두 Vite) -- `apps/next-sample` — Next.js (내부적으로 Turbopack/webpack) - -번들러는 Node.js의 ESM보다 훨씬 관대하게 모듈을 해석한다. - -| 동작 | Node16 / NodeNext | Bundler | -|------|-------------------|---------| -| 확장자 없는 상대 import | ❌ `ERR_MODULE_NOT_FOUND` | ✅ `.ts`, `.tsx`, `index.ts` 자동 탐색 | -| `package.json#exports` 인식 | ✅ | ✅ | -| `main` / `types` fallback | ❌ (`exports`가 있으면 무시) | ✅ | -| `customConditions` | ✅ | ✅ | - -**판단 기준**: "런타임에 번들러를 거치는가?" 가 yes면 Bundler. 이 템플릿의 답은 yes다. (Node.js 서버를 번들러 없이 직접 실행하는 패키지가 추가된다면 그 패키지만 별도로 `Node16`을 쓰면 된다.) - -`Node16`을 쓰지 않는 부수 효과로, **소스에 `.js` 확장자를 적지 않아도 된다**. - -```typescript -// Bundler 모드 — 이 템플릿 -import { greet } from './utils' // ✅ - -// Node16 모드라면 -import { greet } from './utils.js' // 소스에는 .ts지만 .js로 적어야 함 -``` - -## `package.json#exports` 한 줄 요약 - -`exports`는 패키지의 **공개 API 경계**를 선언하는 필드다. 이 필드가 있으면 외부에서 import할 수 있는 경로가 거기에 적힌 것으로 한정된다. - -```jsonc -// packages/sample/package.json -{ - "name": "@repo/sample", - "type": "module", - "exports": { - ".": { - "types": "./src/index.ts", - "default": "./src/index.ts" - } - } -} -``` - -해석되는 모습: - -| import 문 | 해석 결과 | -|-----------|-----------| -| `import { greet } from "@repo/sample"` | `./packages/sample/src/index.ts` | -| `import { greet } from "@repo/sample/src/index"` | ❌ `exports`에 없음 | - -`type: "module"`은 이 패키지가 ESM이라는 선언이다. `.js` 산출물이 생기는 시점에 Node.js와 번들러가 그 파일을 ESM으로 해석하게 한다. - -## 조건(condition)의 우선순위 - -`exports` 안의 객체 키는 **위에서 아래로 순회하며 첫 매칭을 사용**한다. Bundler 모드의 TypeScript가 인식하는 기본 조건은 다음과 같다. - -| 조건 | 의미 | -|------|------| -| `types` | TypeScript가 타입 해석할 때 — 항상 첫 번째에 둬야 한다 | -| `import` | ESM `import` 문 | -| `require` | CJS `require()` | -| `default` | 매칭되는 것이 없을 때의 폴백 | - -여기에 `customConditions`로 사용자가 정의한 키를 추가할 수 있다. 이 템플릿은 `@repo/source` 한 가지를 추가했다 ([03 문서](./03-live-types.md)). - -### 순서 규칙: types를 가장 위에 - -```jsonc -// ✅ 올바른 순서 -{ - "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "default": "./dist/index.js" -} - -// ❌ types가 뒤에 있으면 import가 먼저 매칭되어 .d.ts를 못 찾을 수 있음 -{ - "import": "./dist/index.mjs", - "types": "./dist/index.d.ts" -} -``` - -커스텀 조건을 추가할 때는 그 조건을 **`types`보다도 위**에 둔다. 그래야 로컬 환경에서 커스텀 조건이 가장 먼저 매칭된다. - -```jsonc -// 03 문서에서 다룰 패턴 -{ - "@repo/source": "./src/index.ts", // 1순위 - "types": "./dist/index.d.ts", // 2순위 - "default": "./dist/index.js" // 3순위 -} -``` - -## `tsconfig.json#paths` — 이 템플릿에서의 위치 - -`paths`는 TypeScript 6에서 `baseUrl`이 deprecated된 이후로 **각 워크스페이스 내부의 별칭** 용도에만 쓰는 것이 권장 패턴이다. 이 템플릿도 그렇게 쓴다. - -```jsonc -// apps/tanstack-sample/tsconfig.json -{ - "compilerOptions": { - "paths": { "~/*": ["./src/*"] } - } -} - -// apps/next-sample/tsconfig.json -{ - "compilerOptions": { - "paths": { "~/*": ["./src/*"], "@/*": ["./src/*"] } - } -} - -// packages/sample/tsconfig.json -{ - "compilerOptions": { - "paths": { "@/*": ["./src/*"] } - } -} -``` - -**이 템플릿은 `baseUrl`을 쓰지 않는다.** TypeScript 6 기준 `paths`는 `baseUrl` 없이 동작하므로, `paths` 값에 직접 `./src/*`처럼 적으면 된다. - -워크스페이스 **간** 경로(`@repo/sample` 등)는 절대로 `paths`로 매핑하지 않는다. 그건 `pnpm`의 `workspace:*`와 `package.json#exports`가 표현하는 영역이다 — `paths`로 끌어오면 TypeScript는 통과하지만 런타임이 못 찾는 불일치가 생긴다. - -런타임 측 매핑은 다음 두 곳이 처리한다. - -- Vite: `apps/tanstack-sample/vite.config.ts`의 `vite-tsconfig-paths` 플러그인이 `tsconfig`의 `paths`를 그대로 읽는다. -- Next.js: 내장 webpack/Turbopack이 `tsconfig`의 `paths`를 자동으로 인식한다. - -그래서 `~/components/Header`처럼 쓴 import가 TypeScript와 런타임 모두에서 같은 파일로 해석된다. - -## 정리 - -| 결정 | 이 템플릿의 선택 | -|------|------------------| -| `moduleResolution` | `"Bundler"` — 두 앱이 모두 번들러 기반 | -| `module` | `"ESNext"` | -| 워크스페이스 간 import 표현 | `package.json#exports` (커스텀 조건은 03에서) | -| 워크스페이스 내부 별칭 | `tsconfig#paths` + `vite-tsconfig-paths` (Vite) / Next.js 자동 인식 | -| `baseUrl` | 사용하지 않음 | - -다음: [03. Live Types — 소스 직접 참조와 빌드 산출물의 분기](./03-live-types.md) diff --git a/docs/03-live-types.md b/docs/03-live-types.md deleted file mode 100644 index e6489b2..0000000 --- a/docs/03-live-types.md +++ /dev/null @@ -1,219 +0,0 @@ -# 03. Live Types — 소스 직접 참조와 빌드 산출물의 분기 - -이 템플릿의 핵심 결정. **`packages/sample`의 코드를 수정하면 `apps/*`의 타입과 런타임이 빌드 단계 없이 즉시 반영된다.** 이 문서는 그 메커니즘을 정리하고, 향후 패키지에 빌드 산출물(`dist/`)이 필요해졌을 때 어떻게 자연스럽게 확장할 수 있는지를 보여준다. - -## 두 시점, 하나의 `exports` - -이 문서는 [README](./README.md#핵심-용어)에서 정의한 두 시점을 일관되게 사용한다. - -| 용어 | 의미 | -|------|------| -| **로컬 실행** | 워크스페이스 안에서 `pnpm dev`, `tsc -b`, 에디터가 동작하는 시점. 워크스페이스끼리 `.ts` 소스를 직접 본다. | -| **번들 산출물** | 패키지가 `dist/`로 빌드되어 외부(npm 소비자, 다른 도구)에서 쓰이는 시점. | - -목표: **하나의 `package.json#exports`로 두 시점을 동시에 만족**시킨다. 로컬에서는 소스를, 외부에서는 산출물을 가리키게 한다. 도구는 **커스텀 export 조건**이다. - -## 지금: source-level exports만 사용 - -`@repo/sample`은 npm에 배포하지 않는 internal 패키지(`"private": true`)다. 외부 소비자가 없으므로 산출물도 필요 없다. 그래서 `exports`가 `.ts` 소스를 그대로 가리킨다. - -```jsonc -// packages/sample/package.json -{ - "name": "@repo/sample", - "private": true, - "type": "module", - "exports": { - ".": { - "types": "./src/index.ts", - "default": "./src/index.ts" - } - } -} -``` - -해석 흐름은 단순하다. - -``` -import { greet } from "@repo/sample" - -→ TypeScript (Bundler 모드) - → exports["."] 에서 "types" 매칭 - → packages/sample/src/index.ts 의 타입을 읽음 - -→ Vite (apps/tanstack-sample) - → exports["."] 에서 "default" 매칭 - → packages/sample/src/index.ts 를 직접 트랜스파일 - -→ Next.js (apps/next-sample) - → next.config.ts 의 transpilePackages: ["@repo/sample"] 설정으로 - 워크스페이스 .ts 소스를 자체 번들에 포함 - → packages/sample/src/index.ts 를 직접 컴파일 -``` - -`packages/sample/src/index.ts`를 수정하면, 위 세 도구가 모두 그 파일을 다시 읽는다. **`packages/sample`을 빌드하는 단계 자체가 존재하지 않는다.** - -### Next.js의 `transpilePackages` - -```typescript -// apps/next-sample/next.config.ts -const nextConfig: NextConfig = { - transpilePackages: ['@repo/sample'], -} -``` - -Next.js는 기본적으로 `node_modules` 안의 패키지를 **이미 빌드된 `.js`라고 가정**한다. 워크스페이스 심볼릭 링크 너머가 `.ts` 소스라는 것을 알리려면 `transpilePackages`에 명시적으로 추가해야 한다. 새로운 source-level 패키지를 만들 때마다 이 배열에 추가한다. - -Vite는 별도 설정이 필요 없다. `.ts` 소스를 만나면 자체 esbuild 파이프라인이 처리한다. - -## 미리 깔린 무대장치: `@repo/source` 커스텀 조건 - -`packages/sample`은 지금 커스텀 조건을 사용하지 않는다. 그런데 이 템플릿은 두 곳에 이미 `@repo/source`를 등록해 두었다. - -```jsonc -// tsconfig.base.json -{ - "compilerOptions": { - "customConditions": ["@repo/source"] - } -} -``` - -```typescript -// apps/tanstack-sample/vite.config.ts -export default defineConfig({ - resolve: { - conditions: ['@repo/source'], - }, - // … -}) -``` - -이 두 줄은 **빌드 산출물이 있는 패키지를 추가했을 때** 비로소 의미가 생긴다. 즉, 지금 당장 동작하는 것은 없지만 그쪽으로 자연스럽게 확장할 수 있도록 준비되어 있다. - -## 패키지를 "build 시점"으로 키울 때: 커스텀 조건 패턴 - -`packages/sample`을 npm에 배포하기로 했다고 가정한다. 그러면 **외부 소비자에게는 `dist/index.js` + `dist/index.d.ts`**를 제공해야 하지만, **로컬 워크스페이스는 여전히 `src/index.ts`를 봐야 한다** (그래야 Live Types가 유지된다). - -`exports`를 다음과 같이 확장한다. - -```jsonc -// packages/sample/package.json (가상의 미래 모습) -{ - "name": "@repo/sample", - "type": "module", - "exports": { - ".": { - "@repo/source": "./src/index.ts", // 1순위 — 로컬 실행 시점 - "types": "./dist/index.d.ts", // 2순위 — 외부 소비자의 타입 - "default": "./dist/index.js" // 3순위 — 외부 소비자의 런타임 - } - }, - "scripts": { - "build": "tsc -b", // dist/ 생성 (또는 tsup, unbuild 등) - "typecheck": "tsc -b", - "test": "vitest run --passWithNoTests" - } -} -``` - -해석 흐름이 시점별로 갈라진다. - -| 소비자 | 인식하는 조건 | 매칭 결과 | 읽히는 파일 | -|--------|---------------|-----------|-------------| -| 워크스페이스 내부 TypeScript | `customConditions: ["@repo/source"]` | `@repo/source` | `./src/index.ts` | -| 워크스페이스 내부 Vite | `resolve.conditions: ["@repo/source"]` | `@repo/source` | `./src/index.ts` | -| 워크스페이스 내부 Next.js | (커스텀 조건 미등록 — 아래 주의 참조) | `default` | `./dist/index.js` | -| 외부 npm 소비자 | (커스텀 조건 모름) | `types` → `default` | `./dist/index.d.ts` + `./dist/index.js` | - -### 핵심 포인트 - -1. **순서가 결정한다.** 객체 키는 위에서 아래로 첫 매칭이 이긴다. 커스텀 조건이 `types`보다 위에 있어야 로컬 도구가 산출물 대신 소스를 선택한다. -2. **커스텀 조건의 이름은 scoped로**. `"source"` 같은 일반 이름은 외부 패키지가 같은 키를 쓰면 충돌해서 외부 패키지의 미빌드 소스까지 읽히는 사고가 난다. `@repo/source`처럼 워크스페이스 스코프와 같은 prefix가 안전하다. -3. **로컬 도구만 조건을 등록한다.** 외부 소비자는 `customConditions`를 모르기 때문에 자동으로 `types` → `default` 폴백 경로를 탄다. 한 `package.json`이 두 시점을 동시에 만족시킨다. - -### Next.js를 같은 패턴에 합류시키려면 - -위 표에서 Next.js만 `dist/index.js`를 본다. Next는 dev/build 양쪽에서 커스텀 조건을 직접 노출하는 옵션이 일관되게 제공되지 않으므로, 보통 두 가지 중 하나를 선택한다. - -- **(A) 산출물을 보게 두기** — 그 패키지에 한해 빌드를 거치게 한다. `dist/`가 항상 최신이도록 Turbo의 `dependsOn: ["^build"]`를 활용 ([04 문서](./04-turbo-and-tsconfig-references.md)). -- **(B) 소스를 보게 하기** — `transpilePackages: ['@repo/sample']`로 Next에 소스 트랜스파일을 맡긴다. 단, Next 빌드의 webpack/Turbopack 설정에서 `resolve.conditions`를 추가해야 `@repo/source`가 매칭된다. (현재 Next.js 16 기준 `next.config.ts`의 `webpack` 콜백 또는 `turbopack.resolveAlias`를 통해 가능.) - -이 템플릿의 현재 `next-sample`은 (B)의 소스 트랜스파일만 켜져 있고 source-level exports를 쓰는 패키지(`@repo/sample`)에 대해서만 동작한다. `dist`를 가진 패키지를 추가하면 그 패키지에 맞춰 결정을 내려야 한다. - -## `composite: true`는 런타임과 무관하다 - -`packages/sample/tsconfig.json`은 `composite: true`, `outDir: "dist"`, `declaration: true`를 가진다. - -```jsonc -// packages/sample/tsconfig.json -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "composite": true, - "declaration": true, - "declarationMap": true, - "outDir": "dist", - "rootDir": "src", - "noEmit": false, - "paths": { "@/*": ["./src/*"] } - }, - "include": ["src/**/*.ts"] -} -``` - -여기서 만들어지는 `dist/`는 **런타임에 절대로 사용되지 않는다.** `exports`가 `src/index.ts`를 가리키고 있고 `apps/*`의 어떤 import도 `dist/`를 거치지 않는다. - -이 `dist/`의 진짜 용도는 **TypeScript의 project references**다. `composite: true` 프로젝트는 반드시 선언 파일을 emit해야 다른 프로젝트가 `references`로 참조할 수 있다. 그래서 `noEmit: true`(`tsconfig.base.json`에서 상속)를 명시적으로 `false`로 덮어쓴다. project references의 의미와 그것을 Turbo가 어떻게 활용하는지는 [04 문서](./04-turbo-and-tsconfig-references.md)에서 다룬다. - -요약하자면: - -| 메커니즘 | 영향 범위 | 비고 | -|----------|-----------|------| -| `package.json#exports` | 런타임 + TypeScript 타입 해석 | Live Types를 만든다 | -| `tsconfig.json#references` + `composite` | `tsc -b`의 incremental typecheck | 런타임 import와 무관 | - -이 둘을 혼동하면 "왜 `dist/`가 있는데 수정이 즉시 반영되지?"라거나, 반대로 "`dist/`를 지웠더니 typecheck가 이상해진다"는 혼선이 생긴다. - -## 새 패키지를 추가할 때의 결정 트리 - -``` -새 패키지가 npm에 배포되거나 외부 소비자가 있는가? -├── No (internal 전용) -│ └── source-level exports -│ package.json: -│ "exports": { -│ ".": { -│ "types": "./src/index.ts", -│ "default": "./src/index.ts" -│ } -│ } -│ Next.js로 import한다면 transpilePackages에 추가 -│ -└── Yes (또는 미래에 그럴 수 있음) - └── 커스텀 조건 + dist 폴백 - package.json: - "exports": { - ".": { - "@repo/source": "./src/index.ts", - "types": "./dist/index.d.ts", - "default": "./dist/index.js" - } - } - + 빌드 스크립트 - + Next.js로 import한다면 위의 (A)/(B) 결정 -``` - -두 경우 모두 **로컬 워크스페이스는 항상 `.ts` 소스를 본다**는 불변식을 지킨다. 빌드 산출물은 외부 경계에서만 등장한다. - -## 정리 - -| 결정 | 이유 | -|------|------| -| `@repo/sample`은 source-level exports만 사용 | internal 전용. dist가 필요 없으므로 가장 단순한 형태로 충분 | -| `tsconfig.base.json`에 `customConditions: ["@repo/source"]` 미리 등록 | 빌드 산출물이 있는 패키지가 추가될 때, 워크스페이스 측에 추가 설정 없이 바로 매칭되도록 | -| `vite.config.ts`에 `resolve.conditions` 미리 등록 | 같은 이유. Vite 측 무대장치 | -| Next.js는 `transpilePackages`로 source-level 패키지 처리 | Next의 기본 가정(`node_modules`는 빌드 완료)을 우회 | -| `packages/sample/dist/`는 project references 용도 | 런타임과 무관. 04 문서 참조 | - -다음: [04. Turborepo + tsconfig references](./04-turbo-and-tsconfig-references.md) diff --git a/docs/04-turbo-and-tsconfig-references.md b/docs/04-turbo-and-tsconfig-references.md deleted file mode 100644 index dfc11aa..0000000 --- a/docs/04-turbo-and-tsconfig-references.md +++ /dev/null @@ -1,187 +0,0 @@ -# 04. Turborepo + tsconfig references - -이 템플릿은 두 가지 의존 그래프를 명시적으로 관리한다. - -- **TypeScript의 그래프** — `tsconfig.json#references`로 표현. `tsc -b`와 에디터의 incremental typecheck를 위한 것. -- **태스크의 그래프** — `turbo.json#tasks.*.dependsOn`으로 표현. `pnpm typecheck`, `pnpm build` 같은 태스크가 어느 패키지부터 어느 순서로 실행되어야 하는지를 결정한다. - -두 그래프는 같은 워크스페이스 의존 관계를 다른 도구의 언어로 두 번 적은 것이다. 굳이 두 번 적는 이유가 이 문서의 주제다. - -## tsconfig references — `tsc`의 의존 그래프 - -### 각 워크스페이스의 references - -```jsonc -// apps/tanstack-sample/tsconfig.json -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { "jsx": "react-jsx" }, - "references": [{ "path": "../../packages/sample" }] -} -``` - -```jsonc -// apps/next-sample/tsconfig.json -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { "jsx": "preserve" }, - "references": [{ "path": "../../packages/sample" }] -} -``` - -`references`는 "이 프로젝트는 `packages/sample`에 의존한다"고 TypeScript에 알리는 선언이다. - -1. **Incremental typecheck** — `tsc -b`가 각 프로젝트의 `.tsbuildinfo`를 캐시한다. -2. **순서 보장** — `tsc -b apps/tanstack-sample`을 실행하면 `packages/sample`의 typecheck가 먼저 끝난 뒤 앱이 검사된다. - -### `composite: true`가 references의 전제 - -`references`로 가리켜지는 쪽 프로젝트는 반드시 `composite: true`여야 한다. - -```jsonc -// packages/sample/tsconfig.json -{ - "compilerOptions": { - "composite": true, - "declaration": true, - "declarationMap": true, - "outDir": "dist", - "rootDir": "src", - "noEmit": false - } -} -``` - -`composite`가 켜지면 TypeScript는 그 프로젝트의 선언 파일을 emit해야 한다. 다른 프로젝트가 이 패키지를 typecheck할 때 소스를 다시 파싱하지 않고 `.d.ts`를 읽도록 하기 위한 최적화다. - -> 주의: 여기서 만들어지는 `dist/`는 런타임에 쓰이지 않는다. 런타임/타입 import는 `package.json#exports`로 따로 결정되며 ([03 문서](./03-live-types.md) 참조), 현재는 `src/index.ts`를 가리킨다. - -### 루트 `tsconfig.json` — 그래프 전체의 진입점 - -```jsonc -// tsconfig.json (root) -{ - "extends": "./tsconfig.base.json", - "compilerOptions": {}, - "files": [], - "references": [ - { "path": "apps/tanstack-sample" }, - { "path": "apps/next-sample" }, - { "path": "packages/sample" } - ] -} -``` - -`files: []`로 자기 자신은 아무 소스도 가지지 않고, `references`로 모든 워크스페이스를 가리킨다. - -- `tsc -b`만 실행해도 모든 프로젝트가 의존 순서대로 빌드된다. -- 에디터가 그래프 전체를 알게 되어 cross-package "Go to Definition", "Find All References"가 정확해진다. - -## Turborepo — 태스크의 의존 그래프 - -### `turbo.json`의 핵심 - -```jsonc -// turbo.json -{ - "tasks": { - "build": { - "dependsOn": ["^build"], - "outputs": [".next/**", "!.next/cache/**", ".output/**", "dist/**"] - }, - "typecheck": { - "dependsOn": ["^typecheck"], - "outputs": [] - }, - "test": { - "dependsOn": ["^test"], - "outputs": [] - }, - "dev": { - "cache": false, - "persistent": true - } - } -} -``` - -세 가지를 보장한다. - -1. **`^` prefix = 의존 패키지의 같은 타깃 먼저** — `build` 또는 `typecheck`를 앱에 대해 실행하면 Turbo가 먼저 `packages/sample`의 같은 타깃을 실행한다. -2. **캐시** — 입력이 같으면 결과를 다시 계산하지 않는다. 이 템플릿의 `typecheck`/`test`는 워크스페이스별 출력물이 일정하지 않아 `outputs: []`로 둔다. -3. **`dev`는 캐시 안 함** — 장시간 실행되는 워치 모드는 캐시 대상이 아니다. - -Turbo는 패키지 그래프를 각 워크스페이스의 `package.json#dependencies`에서 읽는다. 그래서 `apps/*/package.json`의 `"@repo/sample": "workspace:*"`가 태스크 순서에도 영향을 준다. - -### Turbo가 references 위에 더하는 것 - -Turbo와 `tsconfig#references`는 둘 다 "의존 패키지를 먼저 처리한다"는 같은 아이디어를 표현한다. 차이는 적용 범위다. - -| 도구 | 무엇을 순서 짓는가 | 캐시 | -|------|-------------------|------| -| `tsc -b` (references 기반) | TypeScript 컴파일/typecheck만 | `.tsbuildinfo` 파일 단위, TypeScript 한정 | -| Turbo | 임의의 npm 스크립트 (`build`, `typecheck`, `test`, …) | content-hash 기반, 도구 무관 | - -같은 그래프지만 Turbo는 `pnpm build`에도, `pnpm test`에도, 향후 추가될 어떤 태스크에도 적용된다. `tsc`만 알았다면 `test` 같은 태스크에 대해서는 따로 의존 순서를 관리해야 했을 것이다. - -### 루트 `package.json`의 진입점 - -```jsonc -// package.json (root) -{ - "scripts": { - "build": "turbo run build", - "dev": "turbo run dev", - "typecheck": "turbo run typecheck", - "test": "turbo run test", - "lint": "oxlint -c .oxlintrc.json", - "check": "pnpm lint && pnpm format:check && pnpm typecheck && pnpm test && pnpm sheriff && pnpm knip" - } -} -``` - -`pnpm typecheck` 한 줄이 다음을 보장한다. - -```text -turbo run typecheck - ↓ tasks.typecheck.dependsOn: ["^typecheck"] 적용 - ↓ package.json dependencies로 의존 그래프 추출 - ↓ packages/sample 먼저, apps/* 나중에 - ↓ 각 패키지의 package.json#scripts.typecheck 실행 - ↓ 입력/출력 해시가 같으면 캐시 hit -``` - -각 워크스페이스의 `typecheck` 스크립트는 자기 도구를 자유롭게 고른다. - -| 워크스페이스 | `typecheck` 스크립트 | 사용하는 도구 | -|-------------|----------------------|---------------| -| `packages/sample` | `tsc -b` | composite + project references | -| `apps/tanstack-sample` | `tsc --noEmit` | references는 자체적으로 따라감 | -| `apps/next-sample` | `tsc --noEmit` | 동일 | - -## 두 그래프를 동기화하는 책임 - -같은 의존 관계가 세 군데에 적힌다. - -1. `package.json#dependencies`의 `"@repo/sample": "workspace:*"` -2. 그 패키지를 쓰는 워크스페이스의 `tsconfig.json#references` -3. 루트 `tsconfig.json#references` (전체 목록) - -세 곳이 일치해야 한다. - -- (1) 누락 → pnpm이 심볼릭 링크를 만들지 않음 → import 자체가 깨짐 -- (2) 누락 → typecheck는 통과하지만 `tsc -b`의 incremental 캐시가 부정확해짐 -- (3) 누락 → 에디터가 cross-package 기능에서 그 패키지를 못 찾음 - -## 정리 - -| 도구 | 무엇을 책임지는가 | -|------|-------------------| -| `pnpm` (`workspace:*`) | 패키지가 서로 import 가능한 상태를 만든다 | -| `package.json#exports` | import가 어느 파일로 해석되는지 결정 (런타임 + TypeScript 모두) | -| `tsconfig#references` + `composite` | TypeScript 한정의 incremental typecheck와 빌드 순서 | -| 루트 `tsconfig.json` | 그래프 전체의 진입점. 에디터가 cross-package 기능을 정확히 제공하도록 | -| `turbo.json#tasks.*.dependsOn` | 임의의 npm 스크립트에 같은 의존 순서를 적용. 캐시 | -| `pnpm