0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CLI で TypeScript のプログラムを動かす

Last updated at Posted at 2024-04-04

前提

フロントエンドは要らないので、 Vue.js や Vite, turbopack のような高機能なフレームワークは使わない。

実装

最初のディレクトリ構成と各ファイルの内容

$ tree mycli-example
mycli-example
├── app
│   ├── package.json
│   └── src
│       └── index.ts
└── bin
    └── mycli
app/package.json
{
  "name": "mycli-example",
  "version": "1.0.0",
  "scripts": {
    "dev": "node src/index.ts"
  }
}
app/src/index.ts
console.log('Hello world!');

実行してみる。

$ npm run dev

> mycli-example@1.0.0 dev
> node src/index.ts

Hello world!

ちゃんと動く。

別ファイルにモジュールを分けて書く。

1ファイルにはまとめにくい、もう少し複雑なプログラムを想定する。

app/src/module.ts
export function hello(name: string) {
  console.log(`Hello, ${name}!`);
}

index.ts 側ではそれをインポートして使用する。

app/src/index.ts
import { hello } from './module.ts';

hello('world');

実行する。

$ npm run dev

> mycli-example@1.0.0 dev
> node src/index.ts

(node:13187) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
import { hello } from './module.ts';
^^^^^^

SyntaxError: Cannot use import statement outside a module

エラーになったので、エラー内容にあるように package.json に設定を追加してみる。

app/package.json
 {
   "name": "mycli-example",
   "version": "1.0.0",
+  "type": "module",
   "scripts": {
     "dev": "node src/index.ts"
   }
 }

実行するとエラー内容が変わった。

$ npm run dev

> mycli-example@1.0.0 dev
> node src/index.ts

node:internal/errors:496
    ErrorCaptureStackTrace(err);
    ^

TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for /path/to/mycli-example/app/src/index.ts

node では .ts の拡張子を持つファイルをインポートできないとのこと。

代わりに ts-node を使用してみる。

npm install -D ts-node

tsconfig.json ファイルを作成し、最小限の設定を追加する。

app/tsconfig.json
{
  "compilerOptions": {
    "module": "esnext"
  },
  "ts-node": {
    "esm": true,
    "experimentalSpecifierResolution": "node"
  }
}

まだエラーが出るが、エラー内容は変わった。

$ npm run dev

> mycli-example@1.0.0 dev
> ts-node src/index.ts

/path/to/mycli-example/app/node_modules/ts-node/src/index.ts:859
    return new TSError(diagnosticText, diagnosticCodes, diagnostics);
           ^
TSError: ⨯ Unable to compile TypeScript:
src/index.ts:1:23 - error TS5097: An import path can only end with a '.ts' extension when 'allowImportingTsExtensions' is enabled.

1 import { hello } from './module.ts';
                        ~~~~~~~~~~~~~

今度もエラー内容にあるように tsconfig.json に設定を追加してみる。

app/tsconfig.json
 {
   "compilerOptions": {
     "module": "esnext",
+    "allowImportingTsExtensions": true,
+    "noEmit": true
   },
   "ts-node": {
     "esm": true,
     "experimentalSpecifierResolution": "node"
   }
 }

実行。

$ npm run dev

> mycli-example@1.0.0 dev
> ts-node src/index.ts

Hello, world!

ようやく動いた!

今はまだコードが少ないのでこれでも問題ないが、規模が大きくなってくると毎回の実行に時間がかかるようになる。

開発時以外は、ビルド(トランスパイル)された JavaScript ファイルを実行したい。

以前は Webpack を使っていたが、開発が停止しているとのこと。

ひとまず tsc を使ってみる。

npm install -D typescript
package.json
 {
   "name": "mycli-example",
   "version": "1.0.0",
   "type": "module",
   "scripts": {
     "dev": "ts-node src/index.ts",
+    "build": "tsc"
   },
   "devDependencies": {
     "ts-node": "^10.9.2",
     "typescript": "^5.4.3"
   }
 }

ビルドしてみる。

npm run build
error TS2792: Cannot find module 'XXX'. Did you mean to set the 'moduleResolution' option to 'nodenext', or to add aliases to the 'paths' option?

"moduleResolution": "nodenext" を指定すると "module": "nodenext" とする必要がある。moduleResolution は node でも問題ないので、ひとまず以下のように tsconfig.json を修正する。

app/tsconfig.json
 {
   "compilerOptions": {
     "module": "esnext",
+    "moduleResolution": "node",
+    "outDir": "./dist",
     "allowImportingTsExtensions": true,
     "noEmit": true
   },
   "ts-node": {
     "esm": true,
     "experimentalSpecifierResolution": "node"
   }
 }
npm run build

動いた...が、ファイルが作成されない。

tsconfig.json に設定した "noEmit": true が原因。

ただこれを外すと "allowImportingTsExtensions": true が設定できなくなってしまう。

ちなみに noEmit を外した時の allowImportingTsExtensions が出すエラーメッセージには「noEmit か emitDeclarationOnly を設定する必要がある」となっているが、代わりに emitDeclarationOnly を設定しても js ファイルは作成されない(定義ファイルのみ出力されるようになる)。

tsc はあきらめて、 esbuild を使用する。

最近は tsx もいいらしい。

npm i -D esbuild

npx esbuild で実行することもできるが、オプションをいろいろ設定する必要があるため esbuild の API を使用する。

app/esbuild.js
import * as esbuild from 'esbuild';

await esbuild.build({
  entryPoints: ['./src/index.ts'],
  target: 'esnext',
  platform: 'node',
  format: 'esm',
  bundle: true,
  minify: true,
  outdir: 'dist',
});
package.json
 {
   "name": "mycli-example",
   "version": "1.0.0",
   "type": "module",
   "scripts": {
     "dev": "ts-node src/index.ts",
-    "build": "tsc"
+    "build": "node esbuild.js",
+    "exec": "node dist/index.js"
   },
   "devDependencies": {
+    "esbuild": "^0.20.2",
     "ts-node": "^10.9.2",
     "typescript": "^5.4.3"
   }
 }

ビルドしてみる。

npm run build

app/dist/ に index.js が生成された。

app/dist/index.js
function o(l){console.log(`Hello, ${l}!`)}o("world");

minify: true の設定によりソースコードの圧縮が行われている。

ビルドして作成されたのは JavaScript ファイルなので、 node で実行する。

$ npm run exec

> mycli-example@1.0.0 exec
> node dist/index.js

Hello, world!

ちゃんと動いた。

外部ライブラリを使う場合

適当に外部ライブラリを使うようにコードを修正してみる。

app/index.ts
+ import * as dotenv from 'dotenv';
  import { hello } from './module.ts';

+ dotenv.config();

  hello('world');

ビルドして再度実行してみる。

$ npm run build

> mycli-example@1.0.0 build
> node esbuild.js

$ npm run exec

> mycli-example@1.0.0 exec
> node dist/index.js

file:///home/mwatanabe/project/mycli-example/app/dist/index.js:1

Error: Dynamic require of "fs" is not supported

ESM では使えない CommonJS 特有の機能が正常に動いていない。

app/esbuild.js
 import * as esbuild from 'esbuild';

 await esbuild.build({
   entryPoints: ['./src/index.ts'],
   target: 'esnext',
   platform: 'node',
   format: 'esm',
   bundle: true,
   minify: true,
   outdir: 'dist',
+  banner: {
+    js: "import{createRequire}from'module';const require=createRequire(import.meta.url);",
+  },
 });

require が実行できるよう、 index.js の先頭にコードを差し込む設定を追加する。

こちらの記事に詳しく書かれていた。

ビルドしてみる。

npm run build
app/dist/index.js
+ import{createRequire}from'module';const require=createRequire(import.meta.url);
  var O=Object.create;var m=Object.defineProperty;...(省略)

ちゃんと追加されているので、実行してみる。

$ npm run exec

> mycli-example@1.0.0 exec
> node dist/index.js

Hello, world!

実行できた。

bin/mycli
#!/bin/sh -e

cd "$(dirname "$0")/../app/"

npm run exec "$@"
export PATH=$PATH:/path/to/mycli-example/bin
$ mycli

> mycli-example@1.0.0 exec
> node dist/index.js

Hello, world!

完成。

補足

ts-node ではなく tsx を使うと高速らしいので、プログラムの規模によってはそれでも良いかもしれない。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?