Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(use-case/nodecli): ESModule対応 #1465

Merged
merged 20 commits into from
Sep 5, 2022
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 65 additions & 32 deletions source/use-case/nodecli/argument-parse/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,57 +73,75 @@ $ npm init --yes
`package.json`ファイルが用意できたら、`npm install`コマンドを使って`commander`パッケージをインストールします。
このコマンドの引数にはインストールするパッケージの名前とそのバージョンを`@`記号でつなげて指定できます。
バージョンを指定せずにインストールすれば、その時点での最新の安定版が自動的に選択されます。
次のコマンドを実行して、commanderのバージョン5.0をインストールします。[^1]
次のコマンドを実行して、commanderのバージョン9.0をインストールします。[^1]

```shell
$ npm install commander@5.0
$ npm install commander@9.0
```

インストールが完了すると、`package.json`ファイルは次のようになっています。

[import, title:"package.json"](src/package.json)
[import, title:"package.json"](src/package.install.json)

また、npmのバージョンが5以上であれば `package-lock.json`ファイルが生成されています。
このファイルはnpmがインストールしたパッケージの、実際のバージョンを記録するためのものです。
先ほどcommanderのバージョンを`5.0`としましたが、実際にインストールされるのは`5.0.x`に一致する最新のバージョンです。
先ほどcommanderのバージョンを`9.0`としましたが、実際にインストールされるのは`9.0.x`に一致する最新のバージョンです。
`package-lock.json`ファイルには実際にインストールされたバージョンが記録されています。
これによって、再び`npm install`を実行したときに、異なるバージョンがインストールされるのを防ぎます。

### CommonJSモジュール {#commonjs-module}

インストールした`commander`パッケージを使う前に、**CommonJSモジュール**のことを知っておきましょう。
[CommonJSモジュール][]とは、[Node.js][]環境で利用されているJavaScriptのモジュール化の仕組みです。
CommonJSモジュールは基本文法で学んだ[ECMAScriptモジュール][]の仕様が策定されるより前からNode.jsで使われています。
Node.jsの標準パッケージやnpmで配布されるパッケージは、CommonJSモジュールとして提供されていることがほとんどです。
先ほどインストールした`commander`パッケージも、CommonJSモジュールとして利用できます。
### ECMAScriptモジュールを使う {#esmodule}

今回のユースケースでは、インストールした`commander`パッケージを利用するにあたって、基本文法で学んだ[ECMAScriptモジュール][]を使います。
`commander`パッケージはECMAScriptモジュールに対応しているため、次のように`import`文を使って変数や関数などをインポートできます。

<!-- doctest:disable -->
```js
import { program } from "commander";
```

ただし、ECMAScriptモジュールのパッケージをインポートするには、インポート元のファイルもECMAScriptモジュールでなければなりません。
なぜなら、[Node.js][]は[CommonJSモジュール][]という別のモジュール形式もサポートしており、CommonJSモジュール形式では`import`文は利用できないためです。
そのため、これから実行するJavaScriptファイルがどちらの形式であるかをNode.jsに教える必要があります。

Node.jsはもっとも近い上位ディレクトリの `package.json` が持つ `type` フィールドの値によってJavaScriptファイルのモジュール形式を判別します。
`type`フィールドが `module` であればECMAScriptモジュールとして、`commonjs` であればCommonJSモジュールとして扱われます。[^2]
また、JavaScriptファイルの拡張子によって明示的に示すこともできます。拡張子が `.mjs` である場合はECMAScriptモジュールとして、`.cjs` である場合はCommonJSモジュールであると判別されます。

CommonJSモジュールはNode.jsのグローバル変数である`module`変数を使って変数や関数などをエクスポートします。
CommonJSモジュールでは`module.exports`プロパティに代入されたオブジェクトが、そのJavaScriptファイルからエクスポートされます。
複数の名前つきエクスポートが可能なES Moduleとは異なり、CommonJSでは`module.exports`プロパティの値だけがエクスポートの対象です。
今回は `main.js` を ECMAScriptモジュールとして判別させるために、次のように `package.json` に`type` フィールドを追加します。
lacolaco marked this conversation as resolved.
Show resolved Hide resolved

次の例では、`my-module.js`というファイルを作成し、`module.exports`でオブジェクトをエクスポートしています。
```shell
# npm pkg コマンドで type フィールドの値をセットする
$ npm pkg set type=module
```

[import, title:"my-module.js"](src/my-module.js)
[import, title:"package.json"](src/package.json)

このCommonJSモジュールをインポートするには、Node.js実行環境のグローバル関数である[require関数][]を使います。
次のように`require`関数にインポートしたいモジュールのファイルパスを渡し、返り値としてエクスポートされた値をインポートできます。
インポートするファイルパスに拡張子が必須なES Moduleとは異なり、CommonJSの`require`関数では拡張子である`.js`が省略可能です。
#### [コラム] CommonJSモジュール {#commonjs-module}

[import](src/cjs-import.js)
[CommonJSモジュール][]とは、Node.js環境で利用されているJavaScriptのモジュール化の仕組みです。
CommonJSモジュールは[ECMAScriptモジュール][]の仕様が策定されるより前からNode.jsで使われています。

また、`require`関数は相対パスや絶対パス以外にもnpmでインストールしたパッケージ名を指定できます
`npm install`コマンドでインストールされたパッケージは、`node_modules`というディレクトリの中に配置されています
`require`関数にインストールしたパッケージ名を指定することで、`node_modules`ディレクトリに配置されたパッケージを読み込めます。
現在はNode.jsでもECMAScriptモジュールがサポートされていますが、`fs` などの標準モジュールはCommonJSモジュールとして提供されています
また、サードパーティ製のライブラリや長く開発が続けられているプロジェクトのソースコードなどでも、CommonJSモジュールを利用する場面は少なくありません
そのため、この2つのモジュール形式が共存する場合には、開発者はモジュール形式間の相互運用性に注意する必要があります。[^3]

次の例では、先ほどインストールした`commander`パッケージを`node_modules`ディレクトリから読み込んでいます。
Node.jsはECMAScriptモジュールからCommonJSモジュールをインポートする方向の相互運用性をサポートしています。
たとえば、次のようにCommonJSモジュールで`exports`オブジェクトを使ってエクスポートされたオブジェクトは、ECMAScriptモジュールで`import`文を使ってインポートできます。
Node.jsの標準モジュールはECMAScriptモジュールのJavaScriptファイルからでも利用できますが、それはこの相互運用性によるものです。
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

これあんまり意識してなかったけど、確かにそうなってた。
https://github.com/nodejs/node/tree/main/lib

前提として、Node.jsの標準モジュール自体がCJSで書かれてることが抜けている感じがする。
なんか別の例でもいいのかなーとはちょっと思ったけど、思いついてない。

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

標準モジュールのことは125行目でさらっと書いてますね


<!-- doctest:disable -->
```js
const program = require("commander");
// lib.cjs
exports.key = "value";

// app.mjs
import { key } from "./lib.cjs";
```
lacolaco marked this conversation as resolved.
Show resolved Hide resolved

このユースケースで今後登場するモジュールはすべてCommonJSモジュールです。
Node.jsではES Moduleもサポートされる予定ですが、現在はまだ安定した機能としてサポートされていません。
一方で、CommonJSモジュールからECMAScriptモジュールをインポートする方向の相互運用性はサポートされていません。
もし既存のライブラリから提供されるモジュールがECMAScriptモジュールであれば、それを使うアプリケーションもECMAScriptモジュールで書かれている必要があります。
複数のパッケージを利用しながらNode.jsアプリケーションを開発する際には特に相互運用性に注意しておく必要があるでしょう。

### コマンドライン引数からファイルパスを取得する {#get-file-path}

Expand All @@ -134,13 +152,13 @@ Node.jsではES Moduleもサポートされる予定ですが、現在はまだ
$ node main.js ./sample.md
```

commanderでコマンドライン引数をパースするためには、`parse`メソッドにコマンドライン引数を渡します。
commanderでコマンドライン引数をパースするためには、インポートした`program`オブジェクトの`parse`メソッドにコマンドライン引数を渡します。

<!-- doctest:disable -->
```js
// commanderモジュールをprogramオブジェクトとしてインポートする
const program = require("commander");
// コマンドライン引数をパースする
// commanderモジュールからprogramオブジェクトをインポートする
import { program } from "commander";
// コマンドライン引数をcommanderでパースする
program.parse(process.argv);
```
lacolaco marked this conversation as resolved.
Show resolved Hide resolved

Expand All @@ -163,11 +181,25 @@ $ node main.js ./sample.md
このように、`process.argv`配列を直接扱うよりも、commanderのようなライブラリを使うことで宣言的にコマンドライン引数を定義して処理できます。
次のセクションではコマンドライン引数から取得したファイルパスを元に、ファイルを読み込む処理を追加していきます。

#### [エラー例] SyntaxError: Cannot use import statement outside a module {#syntax-error-import-statement}

「`import`文をECMAScriptモジュールの外で使うことはできません」というエラーが出ています。`main.js` の実行でこのエラーが出る場合は、Node.jsが`main.js`ファイルをECMAScriptモジュールだと判別できていないことを意味します。

<!-- doctest:disable -->
```shell
import { program } from "commander";
^^^^^^

SyntaxError: Cannot use import statement outside a module
```

[ECMAScriptモジュールを使う](#esmodule)で述べたように、`package.json`の`type`フィールドを`module`に設定しましょう。

## このセクションのチェックリスト {#section-checklist}

- `process.argv`配列に`node`コマンドのコマンドライン引数が格納されていることを確認した
- npmを使ってパッケージをインストールする方法を理解した
- `require`関数を使ってパッケージのモジュールを読み込めることを確認した
- ECMAScriptモジュールを使ってパッケージを読み込めることを確認した
- commanderを使ってコマンドライン引数をパースできることを確認した
- コマンドライン引数で渡されたファイルパスを取得してコンソールに出力できた

Expand All @@ -176,7 +208,8 @@ $ node main.js ./sample.md
[npmのGitHubリポジトリ]: https://github.com/npm/npm
[CommonJSモジュール]: https://nodejs.org/docs/latest/api/modules.html
[Node.js]: https://nodejs.org/ja/
[require関数]: https://nodejs.org/dist/latest-v14.x/docs/api/modules.html#modules_loading_from_node_modules_folders
[アプリケーション開発の準備]: ../../setup-local-env/README.md
[ECMAScriptモジュール]: ../../../basic/module/README.md
[^1]: --saveオプションをつけてインストールしたのと同じ意味。npm 5.0.0からは--saveがデフォルトオプションとなりました。
[^2]: [package.json and file extensions](https://nodejs.org/api/packages.html#packagejson-and-file-extensions)
[^3]: [Interoperability with CommonJS](https://nodejs.org/api/esm.html#interoperability-with-commonjs)
3 changes: 0 additions & 3 deletions source/use-case/nodecli/argument-parse/src/cjs-import.js

This file was deleted.

5 changes: 3 additions & 2 deletions source/use-case/nodecli/argument-parse/src/main-2.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// commanderモジュールをprogramとしてインポートする
const program = require("commander");
// commanderモジュールからprogramオブジェクトをインポートする
import { program } from "commander";

// コマンドライン引数をcommanderでパースする
program.parse(process.argv);

Expand Down
3 changes: 0 additions & 3 deletions source/use-case/nodecli/argument-parse/src/my-module.js

This file was deleted.

16 changes: 8 additions & 8 deletions source/use-case/nodecli/argument-parse/src/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions source/use-case/nodecli/argument-parse/src/package.install.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "nodecli",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"commander": "^9.0.0"
}
}
3 changes: 2 additions & 1 deletion source/use-case/nodecli/argument-parse/src/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
"version": "1.0.0",
"description": "",
"main": "main.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"commander": "^5.0.0"
"commander": "^9.0.0"
}
}
6 changes: 3 additions & 3 deletions source/use-case/nodecli/md-to-html/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ JavaScriptでMarkdownをHTMLへ変換するために、今回は[marked][]とい
markedのパッケージはnpmで配布されているので、commanderと同様に`npm install`コマンドでパッケージをインストールしましょう。

```shell
$ npm install [email protected].10
$ npm install [email protected]
```

インストールが完了したら、Node.jsのスクリプトから読み込みます。
前のセクションの最後で書いたスクリプトに、markedパッケージの読み込み処理を追加しましょう
前のセクションの最後で書いたスクリプトに、`marked`モジュールの読み込み処理を追加しましょう
次のように`main.js`を変更し、読み込んだMarkdownファイルをmarkedを使ってHTMLに変換します。
markedパッケージをインポートした`marked.parse`関数は、Markdown文字列を引数にとり、HTML文字列に変換して返します。
`marked`モジュールからインポートした`marked.parse`関数は、Markdown文字列を引数にとり、HTML文字列に変換して返します。

[import title:"main.js"](src/main-1.js)

Expand Down
8 changes: 4 additions & 4 deletions source/use-case/nodecli/md-to-html/src/main-1.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const program = require("commander");
const fs = require("fs/promises");
// markedモジュールをmarkedオブジェクトとしてインポートする
const marked = require("marked");
import { program } from "commander";
import * as fs from "node:fs/promises";
// markedモジュールからmarkedオブジェクトをインポートする
import { marked } from "marked";

program.parse(process.argv);
const filePath = program.args[0];
Expand Down
6 changes: 3 additions & 3 deletions source/use-case/nodecli/md-to-html/src/main-2.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const program = require("commander");
const fs = require("fs/promises");
const marked = require("marked");
import { program } from "commander";
import * as fs from "node:fs/promises";
import { marked } from "marked";

program.parse(process.argv);
const filePath = program.args[0];
Expand Down
6 changes: 3 additions & 3 deletions source/use-case/nodecli/md-to-html/src/main-3.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const program = require("commander");
const fs = require("fs/promises");
const marked = require("marked");
import { program } from "commander";
import * as fs from "node:fs/promises";
import { marked } from "marked";

// gfmオプションを定義する
program.option("--gfm", "GFMを有効にする");
Expand Down
6 changes: 3 additions & 3 deletions source/use-case/nodecli/md-to-html/src/main.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const program = require("commander");
const fs = require("fs/promises");
const marked = require("marked");
import { program } from "commander";
import * as fs from "node:fs/promises";
import { marked } from "marked";

// gfmオプションを定義する
program.option("--gfm", "GFMを有効にする");
Expand Down
18 changes: 9 additions & 9 deletions source/use-case/nodecli/md-to-html/src/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions source/use-case/nodecli/md-to-html/src/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
"version": "1.0.0",
"description": "",
"main": "main.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"commander": "^5.0.0",
"marked": "^4.0.10"
"commander": "^9.0.0",
"marked": "^4.0.0"
}
}
Loading