s's-nook:

npm workspaceを用いて共通処理をpackageとして切り出す

以下のような構成でモノレポではあるものの、それぞれ独立したnodeプロジェクトで運用していた状態から、npm workspaceを用いて共通処理置き場(sharedプロジェクト)を作成したのでその方法を紹介します。

. ├── backend │ ├── package.json │ └── package-lock.json ├── frontend │ ├── next.config.ts │ ├── package.json │ └── package-lock.json └── supabase ├── migrations └── schema.sql

workspace移行後のプロジェクト構成

. ├── backend │ └── package.json ├── frontend │ ├── next.config.ts │ └── package.json ├── node_modules ├── package-lock.json ├── package.json ├── shared │ └── package.json └── supabase ├── migrations └── schema.sql

そもそもnpm workspaceとはという方は以下の記事がすごくわかりやすかったのでおすすめです。

npm workspacesとモノレポ探検記
suinさんのスクラップ
zenn.dev
npm workspacesとモノレポ探検記

やったこと

プロジェクトルートにworkspace用のpackage.json作成

{ "name": "your-app", "private": true, "workspaces": [ "frontend", "backend", "shared" ], "scripts": { "build": "npm run build --workspaces" }, "devDependencies": { "@types/node": "^22.13.1", "typescript": "^5.7.3" } }

全てのパッケージで利用するようなdependenciesはルートに指定しておくことができます。

nodeのモジュール解決ではホイスティング(hoisting, 巻き上げ)という機能があり、自分のnode_modulesに該当モジュールがない場合は親のnode_modulesに該当モジュールを探しに行くという機能による恩恵です。

sharedプロジェクト作成

プロジェクト構成

. ├── dist ├── package.json ├── src │ ├── date.ts │ └── index.ts └── tsconfig.json

shared/package.json

{ "name": "@your-app/shared", "private": true, "version": "1.0.0", "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { "build": "tsc", "watch": "tsc -w" } }

shared/tsconfig.json

{ "compilerOptions": { "target": "es2018", "module": "commonjs", "declaration": true, "outDir": "./dist", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["src"], "exclude": ["node_modules", "dist", "**/*.test.ts"] }

shared/index.ts

export * from "./date";

shared/date.ts

export const getDateStringBeforeN = (n: number) => { const today = new Date(); const targetDate = new Date(today); targetDate.setDate(today.getDate() - n); const year = targetDate.getFullYear(); const month = String(targetDate.getMonth() + 1).padStart(2, "0"); const day = String(targetDate.getDate()).padStart(2, "0"); return `${year}-${month}-${day}`; };

プロジェクトをbuildしておく

npm run build -w shared

frontend側の修正

sharedをfrontend側の依存に追加する

npm i @your-app/shared -w @you-app/frontend

sharedの機能を利用する

import { getDateStringBeforeN } from "@your-app/shared";

注意点

元々存在していたnode_moduleとpackage-lock.jsonを手動で削除してからルートにて npm install を実行する必要があります。これを実施しないと以下のようなエラーで共通処理を正しくimportできないと思います。

Module not found: Can't resolve '@your-app/shared'

最後にbuildコマンドでsharedプロジェクトを一緒にbuildされるように修正します。

"scripts": { "build": "npm --workspace=@you-app/shared run build && next build" },

最後に

全てのメソッドがshared/index.tsにまとまって一括でimportする構成はなんか微妙な気がする場合はexportを利用する手もあると思います。

package.jsonのexportsフィールドについて
zenn.dev
package.jsonのexportsフィールドについて