「Node.js を Deno 的に使う」を考える

Deno の開発に大きなニュースが出ました。

Deno is joining Cloudflare

Deno team が、Cloudflare に参加することを発表。

We will support the Deno runtime for another year with monthly releases containing bug fixes and security updates. After that year we will end our development of the Deno runtime. Deno will remain open source, and we welcome others who want to continue its development.

Deno はOSSとして残り、開発を続けたい人々を歓迎としているものの、バグ修正やセキュリティアップデートを含む月次リリースで今後1年間はサポート。
その後、Deno ランタイムの開発は終了を発表。

どうなるかはわからないものの、今後の不安はどうしてもよぎるもの。

自分は、無設定でTSが動き、パーミッションバチバチに充てられて、Web Standard 準拠なのを中心にDenoを推していました。
今回は、そういう意図でDenoを使っていた前提で、Node.jsを使うならどうすればいいのかをまとめておきます。

参考

確認

TS 対応

Node.js は今や、特に設定無く TypeScript が動くようになっている。

index.ts
1
2
3
4
5
6
7
8
9
10
11
interface MyInterface {
name: string;
age: number;
}

const person: MyInterface = {
name: "John Doe",
age: 30
};

console.log(person);
1
2
$ node ./index.ts
{ name: 'John Doe', age: 30 }

が、Enumなどの一部構文は対応していない。

index.ts
1
2
3
4
5
6
7
8
enum Color {
Red,
Green,
Blue
}

const favoriteColor: Color = Color.Green;
console.log(favoriteColor); // will output: 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ node -v
v26.11.1

$ node ./index.ts
file:///workspace/index.ts:14

> enum Color {
Red,
Green,
Blue
> }

SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode
at parseTypeScript (node:internal/modules/typescript:57:40)
at processTypeScriptCode (node:internal/modules/typescript:129:37)
at Object.stripTypeScriptModuleTypes (node:internal/modules/typescript:172:22)
at Module._compile (node:internal/modules/cjs/loader:1922:32)
at Object..js (node:internal/modules/cjs/loader:2108:10)
at Module.load (node:internal/modules/cjs/loader:1690:32)
at Module._load (node:internal/modules/cjs/loader:1480:12)
at wrapModuleLoad (node:internal/modules/cjs/loader:261:19)
at Module.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:171:5)
at node:internal/main/run_main_module:33:47 {
code: 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX'
}

Node.js v26.11.1

先の実行できた範囲は、type stripping という型情報の簡易な削除対応でできる範囲のものとなり限定的。
フルサポートはサードパーティーパッケージの利用の指示が書いてある。

There are two ways to enable runtime TypeScript support in Node.js:

For full support of all of TypeScript’s syntax and features, including using any version of TypeScript, use a third-party package.

For lightweight support, you can use the built-in support for type stripping.

というわけで、ヌルっとTypeScriptを書けそうにない。

案内が書いてあり次のようにできる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ npm install -g npm@12.2.0

removed 8 packages, and changed 72 packages in 14s

$ npm install --save-dev tsx

up to date, audited 4 packages in 694ms

found 0 vulnerabilities

$ npx tsx ./index.ts
npm notice run npx
npm notice run 'tsx' ./index.ts
{ name: 'John Doe', age: 30 }
1

$ node --import=tsx ./index.ts
{ name: 'John Doe', age: 30 }
1

tsx を入れた上で、実行コマンドは2通りある。
もちろん、webプロジェクトならtsxではなくviteを入れてしまって始めるでもいいだろう。

パーミッション(node)

Node.jsには、パーミッションモデルが導入されている。
Denoと違い、後付けのパーミッション管理のため、デフォルトで安全のスタンスではない。
そのうえで使っていく。

index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import * as fs from "fs";

type MyInterface ={
name: string;
age: number;
}

const person: MyInterface = {
name: "John Doe",
age: 30
};

console.log(person);

enum Color {
Red,
Green,
Blue
}

const favoriteColor: Color = Color.Green;
console.log(favoriteColor);

const file = fs.readFileSync("./example.txt", "utf-8");
console.log(file);
1
2
3
4
$ node --import=tsx  ./index.ts
{ name: 'John Doe', age: 30 }
1
hogehoge

パーミッション機能をいれなければ実行できる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ node --import=tsx --permission ./index.ts
node:internal/modules/helpers:85
modulesBinding.readPackageJSON(jsonPath, isESM, base, specifier),
^

Error: Access to this API has been restricted. Use --allow-fs-read to manage permissions.
at readPackageJSON (node:internal/modules/helpers:85:20)
at Object.loaderMethodWrapper [as readPackageJSON] (node:internal/modules/helpers:123:12)
at Object.read (node:internal/modules/package_json_reader:129:32)
at packageResolve (node:internal/modules/esm/resolve:754:43)
at moduleResolve (node:internal/modules/esm/resolve:840:18)
at defaultResolve (node:internal/modules/esm/resolve:973:11)
at #cachedDefaultResolve (node:internal/modules/esm/loader:708:20)
at #resolveAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:728:38)
at ModuleLoader.resolveSync (node:internal/modules/esm/loader:766:56)
at #resolve (node:internal/modules/esm/loader:690:17) {
code: 'ERR_ACCESS_DENIED',
permission: 'FileSystemRead',
resource: '/workspace/node_modules/tsx/package.json'
}

Node.js v26.11.1

--permission で有効にすると、ファイル読み込みの制限がかかる。
これについて明示的に許可を与える必要がある。
--allow-fs-read=* を充てる。

1
2
3
4
$ node --import=tsx --permission --allow-fs-read=* ./index.ts
{ name: 'John Doe', age: 30 }
1
hogehoge

これで実行できる。

--permission-audit は監査モードとなる。
しかし、この機能単体でのログ書き出し機能は持っていない。
これはnode:diagnostics_channelを導入した個別実装の必要がある。

index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import * as fs from "fs";

// node:diagnostics_channelを導入
const diagnostics_channel = require('node:diagnostics_channel');

diagnostics_channel.channel('node:permission-model:fs').subscribe((msg) => {
console.log(`Permission denied: ${msg.permission} on ${msg.resource}`);
});


type MyInterface ={
name: string;
age: number;
}

const person: MyInterface = {
name: "John Doe",
age: 30
};

console.log(person);

enum Color {
Red,
Green,
Blue
}

const favoriteColor: Color = Color.Green;
console.log(favoriteColor);

const file = fs.readFileSync("./example.txt", "utf-8");
console.log(file);

以下のように導入して、監査ログを吐き出せる。
面白いのは同様コマンドでもログが変わった。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ node --import=tsx --permission-audit ./index.ts
{ name: 'John Doe', age: 30 }
1
Permission denied: FileSystemRead on ./example.txt
hogehoge
Permission denied: FileSystemRead on /tmp/tsx-0
Permission denied: FileSystemRead on /tmp/tsx

$ node --import=tsx --permission-audit ./index.ts
{ name: 'John Doe', age: 30 }
1
Permission denied: FileSystemRead on ./example.txt
hogehoge

これはファイル編集直後と2回目の差になる。見ると1回目は、/tmp/tsx-0 や /tmp/tsx へのアクセスも監査されているが、2回目はそれが出ていない。

実は、明示的なファイル読み込みが記述されていなくても、ts->jsにトランスパイルされる過程で一時ファイルが生成され、それに対するアクセスも監査されている様子。
これも踏まえると最低限で権限を絞るなら以下のように記述できる。

1
2
3
4
$ node --import=tsx --permission --allow-fs-read=./ --allow-fs-read=/tmp ./index.ts
{ name: 'John Doe', age: 30 }
1
hogehoge

パーミッション(npx)

先では、node コマンドで確認したが、npxも多用するので確認する。
結構開ける必要があった上で、現状警告も多め。

1
2
3
4
5
6
7
8
9
10
11
12
$ npx --node-options="--permission --allow-fs-read=./ --allow-fs-read=/tmp --allow-fs-write=/tmp --allow-net --allow-child-process" tsx ./index.ts
npm notice run npx
npm notice run 'tsx' ./index.ts
(node:1486) [PERM0002] SecurityWarning: The flag --allow-child-process must be used with extreme caution. It could invalidate the permission model.
(Use `node --trace-warnings ...` to show where the warning was created)
(node:1486) ExperimentalWarning: The flag --allow-net is under experimental phase.
(node:1496) [PERM0002] SecurityWarning: The flag --allow-child-process must be used with extreme caution. It could invalidate the permission model.
(Use `node --trace-warnings ...` to show where the warning was created)
(node:1496) ExperimentalWarning: The flag --allow-net is under experimental phase.
{ name: 'John Doe', age: 30 }
1
hogehoge

設定ファイル

Denoでパーミッション設定をdeno.json に記述できた。
実験的機能ではあるが、Node.jsでも、node.config.json にパーミッション設定を記述できる。

node.config.json
1
2
3
4
5
6
7
8
9
10
11
12
{
"permission": {
"allow-fs-read": ["./", "/tmp"],
"allow-fs-write": [],
"allow-child-process": false,
"allow-worker": false,
"allow-net": false,
"allow-addons": false,
"allow-ffi": false,
"allow-openssl-store": false
}
}

--experimental-default-config-file を記述すると、node.config.jsonを読み込む。

1
2
3
4
5
6
$ node --import=tsx --experimental-default-config-file ./index.ts
(node:1585) ExperimentalWarning: --experimental-config-file is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
{ name: 'John Doe', age: 30 }
1
hogehoge

npx で使うことを試みたが、これは今のところ許可されていない。

1
2
3
4
$ npx --node-options="--experimental-default-config-file" tsx ./index.ts
npm notice run npx
npm notice run 'tsx' ./index.ts
node: --experimental-default-config-file is not allowed in NODE_OPTIONS

そのうち使えるようにはなるだろうが、待つ必要はある。

とりあえず方針

cliでTSを記述するのであれば、--experimental-default-config-fileに記述し、node.config.jsonを使う。
そのうえで、package.json の scripts に記述して省略をする方向で考えたい。

以下の構成を最小構成で始められるはず。

node.config.json
1
2
3
4
5
6
7
8
9
10
11
12
{
"permission": {
"allow-fs-read": ["./", "/tmp"],
"allow-fs-write": [],
"allow-child-process": false,
"allow-worker": false,
"allow-net": false,
"allow-addons": false,
"allow-ffi": false,
"allow-openssl-store": false
}
}
package.json
1
2
3
4
5
6
7
8
9
10
11
{
"scripts": {
"dev": "node --import=tsx --experimental-default-config-file ./index.ts"
},
"devDependencies": {
"tsx": "^4.23.15"
},
"allowScripts": {
"esbuild@0.28.2": true
}
}

以下で実行。

1
$ npm run dev

viteでやるとどうなるだろうか

Webの開発なら、今ならViteを使うことになるだろうから、これも試しておく。
まず、Viteのプロジェクトを作成。

1
$ npm create vite@latest

node.config.json を作成する。

node.config.json
1
2
3
4
5
6
7
8
9
10
11
12
{
"permission": {
"allow-fs-read": ["./", "/tmp", "/pnpm-workspace.yaml", "/package.json", "/lerna.json", "/nx.json"],
"allow-fs-write": ["./", "/tmp"],
"allow-child-process": false,
"allow-worker": false,
"allow-net": true,
"allow-addons": true,
"allow-ffi": false,
"allow-openssl-store": false
}
}
package.jsonc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"name": "workspace",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
// "dev": "vite",
"dev": "node --experimental-default-config-file ./node_modules/vite/bin/vite.js",
// "build": "tsc && vite build",
"build": "node --experimental-default-config-file ./node_modules/vite/bin/vite.js build",
// "preview": "vite preview",
"preview": "node --experimental-default-config-file ./node_modules/vite/bin/vite.js preview"
},
"devDependencies": {
"typescript": "~6.0.2",
"vite": "^8.3.0"
}
}

permission を充てたうえで、Viteを起動できる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ npm run dev:node

> workspace@0.0.0 dev:node
> node --experimental-default-config-file ./node_modules/vite/bin/vite.js

(node:430) [PERM0001] SecurityWarning: The flag --allow-addons must be used with extreme caution. It could invalidate the permission model.
(Use `node --trace-warnings ...` to show where the warning was created)
(node:430) ExperimentalWarning: The flag --allow-net is under experimental phase.
(node:430) ExperimentalWarning: --experimental-config-file is an experimental feature and might change at any time

VITE v8.3.4 ready in 206 ms

➜ Local: http://localhost:5174/
➜ Network: http://172.22.0.3:5174/ eth0
➜ press h + enter to show help

以上、Deno を意識して Node.js 利用をする際の設定を確認した。
実際Deno ランタイムがどうなるのかはわからないが、乗り換える可能性は意識をしておく必要はある。
これから1年の間をよく見ていく必要があるのは間違いない。

とりあえず、このブログはDeno Deployで公開しているので移行先を考えることになってしまった。

では。