はじめに
TypeScript には プロジェクト参照(Project References)
という、別プロジェクトのソースコードを読み込んで使用できる仕組みがあります。
ただ、このプロジェクト参照は、以下のように 1 つの tsconfig.json
を持つ大きなプロジェクトがほぼ前提になっているようです。調べると大体この構成です。
. ├── projectA # 共通プロジェクト ├── projectB # projectAを参照 ├── projectC # projectAを参照 └── tsconfig.json
ただ、自分がやりたかったのは、以下のように各プロジェクトが tsconfig.json
を持つ独立した構成にしたかったのですよね。
. ├── projectA # 共通プロジェクト │ └── tsconfig.json ├── projectB # projectAを参照 │ └── tsconfig.json └── projectC # projectAを参照 └── tsconfig.json
この構成になると、途端に情報がなくなるのはなぜでしょうか。
公開したくないライブラリを参照する開発って、いくらでもある気がしますが。。
この記事では、以下の内容を扱います。
- TypeScript で外部プロジェクトを参照する方法
- TypeScript で外部プロジェクトにエイリアス(別名)をつける方法
なお、分量が多くなってしまうので、適宜折りたたみを入れています。
- はじめに
- 前提条件
- システム構成
- 初期設定
- TypeScript のプロジェクト参照を設定する
- TypeScript のプロジェクト参照にエイリアスをつける方法
- jest でプロジェクト参照のエイリアスを使用する
- おわりに
前提条件
この記事は以下を前提条件とします。
- Node.js や TypeScript はインストール済み
- パッケージマネージャーは npm を使用
- Mac での手順になるので Windows の方は読み替えが必要
システム構成
この記事で扱う内容は、以下のシステム構成になります。
. ├── common-entity # 参照先プロジェクト │ ├── dist # ビルド後ファイル │ ├── src # ソース(TyepeScript) │ └── tsconfig.json # 設定ファイル └── reference-proj # 参照元プロジェクト ├── dist # ビルド後ファイル ├── src # ソース(TyepeScript) └── tsconfig.json # 設定ファイル
また、ビルドは以下のように行います。
- common-entity: TypeScript の
tsc --build
- reference-proj: babel + webpack で
npm run build
初期設定
プロジェクトの作成
下記のように、 common-entity
と reference-proj
のプロジェクトを作成します。
プロジェクトの作成
# common-entity の作成 $ mkdir common-entity $ cd common-entity $ mkdir dist $ mkdir src $ mkdir test $ npm init # 回答はデフォルトで OK $ tsc --init # reference-proj の作成 $ mkdir reference-proj $ cd reference-proj $ mkdir dist $ mkdir src $ mkdir test $ npm init # 回答はデフォルトで OK $ tsc --init
パッケージのインストール(common-entity, reference-proj 共通)
common-entity
と reference-proj
の両方のプロジェクトに以下のパッケージをインストールします。
パッケージのインストール(common-entity, reference-proj 共通)
# ESLint, Prettier $ npm install --save-dev eslint $ npm install --save-dev prettier eslint-config-prettier $ npm install --save-dev @typescript-eslint/eslint-plugin $ npm install --save-dev eslint-config-standard eslint-plugin-import eslint-plugin-n eslint-plugin-promise
パッケージのインストール(reference-proj のみ)
reference-proj
にのみ、以下のパッケージをインストールします。
パッケージのインストール(reference-proj のみ)
# jest $ npm install --save-dev jest ts-jest @types/jest $ npm install --save-dev eslint-plugin-jest # babel $ npm install --save-dev babel-jest @babel/core @babel/preset-env $ npm install --save-dev @babel/preset-typescript $ npm install --save-dev @babel/core @babel/preset-typescript babel-loader # webpack $ npm install --save-dev webpack webpack-cli $ npm install --save node-polyfill-webpack-plugin $ npm install --save tsconfig-paths-webpack-plugin
プロジェクトの設定(common-entity, reference-proj 共通)
common-entity
と reference-proj
の両方のプロジェクトに以下の設定を行います。
プロジェクトの設定(common-entity, reference-proj 共通)
# シングルクォーテーションを強制する $ touch .prettierrc.json $ vim .prettierrc.json # 以下の内容を設定 { "singleQuote": true } # Lint の設定 $ touch .eslintrc.js $ vim .eslintrc.js # 以下の内容を設定 module.exports = { env: { browser: true, es2021: true, "jest/globals": true, }, extends: [ "standard", "prettier", // 他の設定の上書きを行うために、必ず最後に配置する。 ], parser: "@typescript-eslint/parser", parserOptions: { ecmaVersion: "latest", sourceType: "module", }, plugins: ["@typescript-eslint", "jest"], rules: {}, }; # TypeScript の設定 $ vim tsconfig.json # 以下の内容を設定 { "compilerOptions": { "target": "es6", "outDir": "./dist", "rootDir": "./src", "module": "commonjs", "downlevelIteration": true, "strict": true, "noUnusedLocals": true, "esModuleInterop": true, "moduleResolution": "node" }, "include": ["./src/**/*"], "exclude": ["node_modules", "**/*.test.ts"] } # git $ git init $ touch .gitignore $ vim .gitignore # 以下の内容を設定 node_modules/ coverage/ .DS_Store dist/
プロジェクトの設定(reference-proj のみ)
reference-proj
にのみ、以下のプロジェクト設定を行います。
プロジェクトの設定(reference-proj のみ)
# bablel $ touch babel.config.js $ vim babel.config.js # 以下の内容を設定 module.exports = { presets: [ ["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-typescript", ], }; # babel $ touch .babelrc $ vim .babelrc # 以下の内容を設定 { "presets": ["@babel/preset-typescript"] } # jest $ touch jest.config.js $ vim jest.config.js # 以下の内容を設定 module.exports = { testMatch: ["<rootDir>/test/**/*.test.ts?(x)"], }; $ mkdir .vscode $ touch .vscode/settings.json $ vim .vscode/settings.json { "jest.jestCommandLine": "npm run test --", "jest.autoRun": { "watch": true } } # webpack $ touch webpack.config.js $ vim webpack.config.js # 以下の内容を設定 const path = require('path'); const NodePolyfillPlugin = require('node-polyfill-webpack-plugin'); const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); module.exports = { mode: 'development', devtool: false, context: __dirname, entry: './src/index.ts', output: { path: path.join(__dirname, 'dist'), filename: 'index.js', }, resolve: { extensions: ['.ts', '.js', '.json'], fallback: { fs: false, readline: false, perf_hooks: false, child_process: false, http2: false, net: false, tls: false, async_hooks: false, dgram: false, cluster: false, url: false, module: false, }, plugins: [new TsconfigPathsPlugin({})], }, module: { rules: [ { test: /\.[tj]s$/, exclude: /node_modules/, loader: 'babel-loader', }, ], }, plugins: [new NodePolyfillPlugin()], }; # TypeScript を1つの JavaScript にまとめる設定 $ touch src/index.ts $ vim src/index.ts # 以下の内容を設定 /** * 1つのJavaScripにまとめたいTypeScriptを定義する */ // 例 // import sum from './sum'; // globalの型定義 declare const global: { [x: string]: unknown; }; // 公開したい関数を定義 // global.sum = sum; # package.json を修正 $ vim package.json # 以下の内容を設定 "scripts": { "build": "webpack", "test": "jest" },
動作確認
まずは、reference-proj
で jest
を含め正しく動作するか確認します。
コードがいかにも Java っぽいですが。。
src/entity/InnerEntity.ts
を作成します。
/** * プロジェクト内部のエンティティ */ export class InnerEntity { public name: string; /** * コンストラクタ * * @param name 名前 */ constructor(name: string) { this.name = name; } }
src/util/JsonUtil.ts
を作成します。
/** * JSONのユーティリティ */ export class JsonUtil { /** * JSON.stringifyした結果を返す * * @param object オブジェクト * @returns JSON.stringifyした結果を返す */ public static stringify(object?: any): string { return JSON.stringify(object); }
jest
で JsonUtil
のテストクラスを、 test/util/JsonUtil.test.ts
で作成します。
import { describe, test, expect } from '@jest/globals'; import { InnerEntity } from '../../src/entity/InnerEntity'; import { JsonUtil } from '../../src/util/JsonUtil'; describe('JsonUtilのテスト', () => { test('stringifyで想定した結果が返ってくる', () => { // 準備 const expected = '{"name":"hoge"}'; const entity = new InnerEntity('hoge'); // 実行 const actual = JsonUtil.stringify(entity); // 検証 expect(actual).toEqual(expected); }); });
テストを実行してみると、問題なく実行されることが分かります。
$ npm run test PASS test/util/JsonUtil.test.ts JsonUtilのテスト ✓ stringifyで想定した結果が返ってくる (1 ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 2.303 s
TypeScript のプロジェクト参照を設定する
参照先プロジェクトの設定(common-entity)
tsconfig.json
をプロジェクト参照で参照可能になるよう編集します。
$ vim tsconfig.json { "compilerOptions": { // プロジェクト参照の参照先は以下の3行を設定する "declaration": true, "declarationMap": true, "composite": true } }
また、src/entity/OuterEntity.ts
も作成しておきます。
/** * プロジェクト外部のエンティティ */ export class OuterEntity { public age: number; /** * コンストラクタ * * @param age: 年齢 */ constructor(age: number) { this.age = age; } }
TypeScript のビルドを行い、参照元プロジェクトで参照できるようにします。
$ tsc --build
参照元プロジェクトの設定(reference-proj)
tsconfig.json
をプロジェクト参照で参照可能になるよう編集します。
$ vim tsconfig.json { "compilerOptions": { // プロジェクト参照の参照元は以下の2行を追加する "baseUrl": ".", "allowJs": true, } // 参照先のプロジェクトの相対パスを追加する "references": [ { "path": "../common-entity" } ] }
では、先ほどの JsonUtil.test.ts
に、参照先の common-entity
にある OuterEntity
を使用したテストを追加してみましょう。
import { describe, test, expect } from '@jest/globals'; import { InnerEntity } from '../../src/entity/InnerEntity'; import { JsonUtil } from '../../src/util/JsonUtil'; import { OuterEntity } from '../../../common-entity/dist/entity/OuterEntity'; describe('JsonUtilのテスト', () => { // 追加 test('プロジェクト参照先のインスタンスをが想定した結果が返ってくる', () => { // 準備 const expected = '{"age":1}'; const entity = new OuterEntity(1); // 実行 const actual = JsonUtil.stringify(entity); // 検証 expect(actual).toEqual(expected); }); });
テストを実行してみると、問題なく使用することができていますね。
$ npm run test PASS test/util/JsonUtil.test.ts JsonUtilのテスト ✓ stringifyで想定した結果が返ってくる (1 ms) ✓ プロジェクト参照先のインスタンスをが想定した結果が返ってくる (1 ms) Test Suites: 1 passed, 1 total Tests: 2 passed, 2 total Snapshots: 0 total Time: 0.362 s, estimated 1 s
これで OK と言いたいところですが、import
文の相対パスがうっとうしいですね。
import { InnerEntity } from '../../src/entity/InnerEntity'; import { JsonUtil } from '../../src/util/JsonUtil'; import { OuterEntity } from '../../../common-entity/dist/entity/OuterEntity';
うっとうしいだけではなく、上記のプロジェクト参照で dist
ディレクトリを指していますが、これを間違えて src
を指定してしまうとコンパイルは通るのに実行時エラーとなり、かなり原因追究に時間がかかります。ですので、相対パスにエイリアスをつけて簡単に参照できるようにしましょう。
TypeScript のプロジェクト参照にエイリアスをつける方法
エイリアスの設定は、参照元プロジェクトの reference-proj
で行います。
今回は、参照先プロジェクトの common-entity
に @common-entity
、同じプロジェクトの src
に @src
のエイリアスをつけてみましょう。
なお、単純にエイリアスの設定を行うと jest
で動作しないので、その課題も含めエイリアスの設定を行います。
tsconfig.json
を以下のように修正します。
なお、common-enitity
の設定で dist
ディレクトリまで指定していることにご注目ください。これで参照先のディレクトリの指定間違いがなくなります。
{ "compilerOptions": { // tsc --build が通らなくなるので修正 // "rootDir": "./src", "rootDir": ".", // エイリアスの設定を相対パスで指定 "paths": { "@common-entity/*": ["../common-entity/dist/*"], "@src/*": ["src/*"] } }, // include と exclude を修正 // "include": ["./src/**/*"], // "exclude": ["node_modules", "**/*.test.ts"], "include": ["./src/**/*", "./test/**/*"], "exclude": ["node_modules"], }
なお、この設定だと、TypeScript でビルドした場合、テストコードがビルドに含まれていまいます。
TypeScript のビルドでテストコードを除外する方法は、下記記事が詳しいです。
webpack
でビルドした場合はテストコードは含まれないようです。
とは言え、念のために webpack
でテストコードの除外設定をして、プロジェクト参照とエイリアスの設定も追加します。
webpack.config.js
を以下のように修正します。
module.exports = { resolve: { // エイリアスの設定を追加 alias: { '@common-entity': path.resolve(__dirname, '../common-entity/dist'), '@src': path.resolve(__dirname, './src'), }, }, module: { rules: [ { test: /\.[tj]s$/, // exclude にテストも追加 // exclude: /node_modules/, exclude: /(node_modules | test)/, loader: 'babel-loader', // include を追加 include: [__dirname, path.resolve(__dirname, '../common-entity/dist')], }, ], }, }
jest.config.js
を以下のように修正します。
module.exports = { testMatch: ['<rootDir>/test/**/*.test.ts?(x)'], // エイリアスの設定を追加する moduleNameMapper: { '^@common-entity(.*)$': '<rootDir>/../common-entity/dist$1', '^@src/(.*)$': '<rootDir>/src/$1', }, };
下記コマンドを使用して、webpack
でビルドを行なってみると問題なくビルドされていますね。
$ npm run build > reference-proj@1.0.0 build > webpack asset index.js 4.08 KiB [compared for emit] (name: main) runtime modules 891 bytes 4 modules cacheable modules 535 bytes ./src/index.ts 256 bytes [built] [code generated] ./src/util/JsonUtil.ts 279 bytes [built] [code generated] webpack 5.74.0 compiled successfully in 404 ms
jest でプロジェクト参照のエイリアスを使用する
現在、JsonUtil.test.ts
の import
は以下のようになっています。相対パスが多く見づらいですね。
import { describe, test, expect } from '@jest/globals'; import { InnerEntity } from '../../src/entity/InnerEntity'; import { JsonUtil } from '../../src/util/JsonUtil'; import { OuterEntity } from '../../../common-entity/dist/entity/OuterEntity';
これをエイリアスで指定します。
import { describe, test, expect } from '@jest/globals'; import { InnerEntity } from '@src/entity/InnerEntity'; import { JsonUtil } from '@src/util/JsonUtil'; import { OuterEntity } from '@common-entity/entity/OuterEntity';
テストを実行します。
$ npm run test PASS test/util/JsonUtil.test.ts JsonUtilのテスト ✓ stringifyで想定した結果が返ってくる (3 ms) ✓ プロジェクト参照先のインスタンスをが想定した結果が返ってくる Test Suites: 1 passed, 1 total Tests: 2 passed, 2 total Snapshots: 0 total Time: 1.801 s
無事に jest
のテストを実行することができました。
おわりに
TypeScript で外部のプロジェクト参照とエイリアス設定は使う頻度が多そうなのに、情報が本当になくてかなり試行錯誤することになってしまいました。
着想から解決まで、2ヶ月半くらいかかっているかもしれません。
なお、ソースコードは、GitHub に上げましたので参考にしてみてください。