以下の内容はhttps://let.blog.jp/tag/Node.jsより取得しました。


Fastify で POST サイズの上限を無制限にできない
POST するときのボディのサイズの話
nginx も Fastify もデフォルトは 1MB なので 10MB まで許可にしたいときに両方を変更しないといけないです
二重に管理するのは無駄ですし 片方だけしか更新してなかったみたいな設定漏れが出るようにしか思わないです

なので外側の nginx で制御して Node.js 側は無制限にしようとしました
ですが Fastify ではサイズを無制限にできないようです

サーバー全体やルート単位で bodyLimit を設定できますが 0 以下や Number.isInteger が false になるものは不正な設定として弾かれます
なので 1 以上の整数にしかできず これと必ず比較するので無制限にならないです
0 は無制限というありそうな設定はないですし Infinity も設定できません
Number.MAX_SAFE_INTEGER を設定しておけばいいといえばいいのですが なんかしっくりこないです
リバースプロキシを配置することを推奨するなら こういうところを快適にしてほしいものです

nginx を置かずに使ってると Node.js 側での制限が必須なので特になんとも思わなかったですが nginx を置いてみて気付いた不満点です

直接自分で Fastify インスタンスを作るところならまだいいですが Fastify CLI を使うとなると コマンドラインオプションで bodyLimit を指定することになります
コマンドラインで Number.MAX_SAFE_INTEGER 的なものを指定って結構面倒です
完全に一致しなくてもいいので 適当に 1TB などにしておくにしても 0 の数が多くなると見づらいです
Fastify CLI で外部からプラグインを参照できない
Fastify CLI を使うと app.js というルートプラグインが各プラグインを autoload する作りになっています
app.js はこういうの

https://github.com/fastify/fastify-cli/blob/v6.1.1/templates/app-esm/app.js
import path from 'path'
import AutoLoad from '@fastify/autoload'
import { fileURLToPath } from 'url'

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)

// Pass --options via CLI arguments in command to enable these options.
export const options = {}

export default async function (fastify, opts) {
// Place here your custom code!

// Do not touch the following lines

// This loads all plugins defined in plugins
// those should be support plugins that are reused
// through your application
fastify.register(AutoLoad, {
dir: path.join(__dirname, 'plugins'),
options: Object.assign({}, opts)
})

// This loads all plugins defined in routes
// define your routes in one of these
fastify.register(AutoLoad, {
dir: path.join(__dirname, 'routes'),
options: Object.assign({}, opts)
})
}

基本は Fastify CLI から使われるのみですが この app.js をプラグインとして使うこともできます
主にテストなどで使われます
https://github.com/fastify/fastify-cli/blob/v6.1.1/templates/app-esm/test/helper.js

テスト以外でも app のインスタンスを作ってそこからグローバルのプラグインで追加したメソッドを使うと便利かなと思ったのですが

import helper from "fastify-cli/helper.js"

const app = await helper.build([path_to_app_js], {})

app.foo()

これで foo が見つからないエラーでした
foo は plugins フォルダ内のプラグインで decorate しています

原因は Fastify のインスタンスに app.js のプラグインを register するときにカプセル化を有効にしていることでした
カプセル化されると app.js の内部では参照できますが 外側からは参照できません

app.js のデフォルトでは上のコードの通り プラグインの関数は fastify-plugin でラップされていません
ここはあまりいじることを想定されてないみたいですし ラップしないのが推奨なようです
実際 他のアプリと混ぜて同じサーバーで動かしたいときなどでは プラグイン全体をカプセル化して特定のプレフィックス以下に配置したいユースケースはありそうですし ラップしないのは正しい気がします
ですが 今回みたいにラップして 外側から参照できるようにカプセル化しないようにしたい場合もあります

となると app.js を使う側の Fastify CLI で fastify-plugin を通してほしいです
オプションで切り替えれるのかなと探したのですが そういうのはないようでした
https://github.com/fastify/fastify-cli/blob/v6.1.1/start.js#L149

現状だと仕方ないので app.js 側で fastify-plugin を通すしかなさそうです
他アプリと混ぜて使う想定がないなら常に fastify-plugin を通しても問題なさそうですが 環境変数を見て変えるということもできそうです

[app.js]
// 前略

const App = async function (fastify, opts) {
// 略
}

if (!process.env.ENCAPSULATE_APP) {
App = fp(App)
}

export default App
Node.js の preload 用の引数
preload するスクリプト用のコマンドライン引数という珍しいものを見ました

node --import pre.js main.js foo=bar

みたいなもので foo=bar は pre.js 用という扱いでした
pre.js で処理した process.argv は main.js でも共通なので pre.js で先に引数を処理してしまうみたいです



[pre.js]
const args = process.argv.splice(2, 2)

console.log("pre", args)

[main.js]
console.log("main", process.argv)

root@fcb8f9998337:/mnt/t/1# node --import ./pre.js main.js a b c
pre [ 'a', 'b' ]
main [ '/usr/local/bin/node', '/mnt/tmp/1/main.js', 'c' ]

pre.js で splice を使って引数の最初の 2 個を取り出して main.js ではそれ以外が残ってる感じです
順番だけでなく pre.js 側で特定のプレフィックスのみを処理するとかもあると思います

preload をあまり使わないので気にしてませんでしたが 結構ありな考え方ですね
Node.js の readline のプロンプト
Node.js で 1 行標準入力から読み取ってなにかの処理をして を繰り返すとき よく使うのが標準モジュールの readline です
簡単に使えますが 単にループで読み取るだけだとプロンプトは自動で出力されません

import readline from "node:readline"

const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})

for await (const line of rl) {
console.log({ line })
}

これを実行して文字を入力すると エンターを押すたびに { line: "入力値" } のようなのが表示されます

user@DESKTOP03 ~> node rl.js
a
{ line: 'a' }
123
{ line: '123' }

入力するとき何も表示されていないので入力していい状態なのか分かりづらいです

createInterface の prompt の初期値は "> " になっているので紛らわしいのですが 手動で prompt メソッドを呼び出さないとプロンプトは出力されません
また output を標準出力に指定していないと prompt メソッドを呼び出しても画面に表示できません

こうすることで表示されます

import readline from "node:readline"

const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})

rl.prompt()
for await (const line of rl) {
console.log({ line })
rl.prompt()
}
user@DESKTOP03 ~> node rl.js
> 1
{ line: '1' }
> xx
{ line: 'xx' }
> ⏎

ただこれだと prompt の呼び出しが分かれますし なんか気持ち悪いです
自分でこれを書きたくないし 自動で内部的にやってほしいものなので asyncIterator を置き換えることにしました

[auto-prompt.js]
const org_async_iterator = Symbol("org-async-iterator")

async function* asyncIterator(...args) {
this.prompt()
for await (const item of this[org_async_iterator].call(this, ...args)) {
yield item
this.prompt()
}
}

const autoPrompt = (rl) => {
rl[org_async_iterator] = rl[Symbol.asyncIterator]
rl[Symbol.asyncIterator] = asyncIterator
}

export default autoPrompt
import readline from "node:readline"
import autoPrompt from "./auto-prompt.js"

const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})

autoPrompt(rl)

for await (const line of rl) {
console.log({ line })
}
user@DESKTOP03 ~> node rl.js
> foo
{ line: 'foo' }
> bar
{ line: 'bar' }
> ⏎

autoPrompt 側は仕方ないとして 普段使う部分ではスッキリと書けます

標準入力を使うならこの使い方で十分そうですが 全インスタンスに反映させるなら Interface の prototype の方を書き換えることもできます

autoPrompt(readline.Interface.prototype)
ウェブサーバーを起動するプロセスでやること
Fastify の CLI を使うと自分が書く部分はプラグインになります
プロセスのエントリポイント部分はライブラリ側で行われるのでウェブサーバーとしての処理しか書けないです

普段は Node.js のウェブサーバーをたてるときは ウェブサーバーとしての処理以外にもプロセス内でいろいろなことをしています
index.js ではそれらをセットアップして その中の一つとしてウェブサーバーもあるという感じです

ウェブサーバー以外のものは 例えば DB やログ等の定期的なクリア処理です
古いデータを消したりローテートしたりを 24 時間間隔などで行います
Node.js はタイマー処理がしやすいので cron 等に任せるより楽です

また ウェブサーバー以外で外部サービスからの通知類を受信したりすることもあります
Redis や PostgreSQL の通知の仕組みだったり 専用の TCP 通信だったりで外部と通信しています
受けたデータによってウェブサーバー側の処理を変更したりしますし DB からキャッシュを再取得するとかもあるので 同じプロセスの方が都合がいいです
別プロセスに分けると ウェブサーバーに API を設けて内部的にそこにアクセスして変更を伝えるようなことになり二度手間感があります

反対にウェブサーバーが受けたリクエストに応じて 通知を受信してる処理を変更するケースもあります
外部通信の ON/OFF の切り替えなどをするならやっぱり同じプロセスの方が扱いやすいです
分けることもあるのですが それでもウェブサーバー側が親で外部通信系が子となる関係で リクエストに応じて子プロセスを止めたり起動したりというのが多いです

systemd でそれぞれ別サービスにすることもできますが 分けすぎても扱いづらいのと ウェブサーバーのプロセスから外部通信プロセスを再起動したいみたいなときに systemd を通すと面倒なのですよね
以前は PM2 を通して systemd は PM2 を起動して ウェブサーバーは PM2 と通信して別プロセスを制御してましたが systemd だけで済ませられたほうが楽です
そうなると systemd がウェブサーバーを含むプロセスだけ起動して そこで全部をやるか一部をそのプロセスのサブプロセスにするかになります

他にも WebSocket サーバーも使うなら それらを統合する必要がありますし ロガーや DB のコネクションなどをウェブサーバーと WebSocket サーバーで共有するなら一括してそれらの外側で作って管理したいです

そんなわけで Fastify CLI って簡単なものでウェブサーバーだけならいいのですが 他にもやりたい場合に不便に感じます

でも PHP などはこういう状態になるわけですし 全部分けて別プロセスって割り切るほうが良いこともあるのでしょうか
Node.js はリクエスト間で変数の空間が分かれないので積極的にキャッシュしていけるのが魅力ですが プロセスが分かれるとキャッシュの更新ができずその利点が活かせなくなるのが困るところなんですよね
Node.js の前段に nginx を置くべき?
Node.js の前段に nginx を置いたほうがいいのか 新しくサーバー環境を作るたびに考えてる問題です
これまでは nginx は無しで Node.js で全部やることにしていました

Node.js 内で全部やるほうが柔軟にできますし サーバーの追加は面倒です
間に追加のレイヤーが増えるほうが複雑化しますし 処理が増える以上パフォーマンス面でも劣りますし 単純に管理するものも増えます
設定ミスやバグなどでうまく動かないとかが出る可能性はありますし シンプル化できるものはシンプルにしておきたいという考えです

ただ Fastify では 前段に nginx などを置くことを推奨しています
たまには置いてみようかな ということで試してました

やってみると たしかに nginx を使ったほうが 圧縮とかキャッシュとか証明書や TLS バージョン等の HTTPS 関係やアクセスログなどを Node.js 側でやらなくていいので Node.js 側は楽になりました
静的ファイルも nginx に任せてしまうと Node.js は完全に API だけに専念できるので いつも入れてるライブラリ類も結構減りました
Node.js 側をシンプルにするという意味では良さそうです

Node.js で全部やると柔軟にできるといっても フレームワークの都合で うまくやりたいように設定できなくて遠回りに自分で制御してたりしたので これでいいような気もしてきました
静的ファイルのサーブとルートの処理の優先順とか SPA に対応させるとか フォルダごとにキャッシュ設定変えたりとか こういうことって結構面倒だったりしますし

それにフレームワークを変えるコストも減ります
Node.js ですべてやると静的ファイルのサーブやレスポンスの圧縮などの方法はフレームワークごとに違いますし ミドルウェアやプラグインを自分で入れて設定しないといけないです
これが結構面倒で 新しいフレームワークを使うときのハードルでもありました
ですが この辺を nginx 側に移すと Node.js 側でやらなくていいのでフレームワークの変更が楽になります

もちろん nginx を別のリバースプロキシのウェブサーバーに変えることはありえるのですが Node.js 側のフレームワークに比べるとめったにないです
フレームワークはコードを実装するのに大きく関連するところなので良さそうなものがあれば積極的に変えたいですが nginx の部分は必要な機能が動いてればそんなに変えたいものでもないです
一応 その他のもので使われてるものは Fastify のページで紹介されてるものだと HAProxy で他のところでは Caddy や lighttpd があるようでした
特にこれらに置き換えたい理由もないですし Node.js のフレームワークよりも次から次に出てくるツールではないと思います

また nginx 側を見ると最初こそ設定ファイルの見づらさはありますが 慣れると結構シンプルで読みやすいと思います
server や location みたいなブロックがあってそれぞれのコンテキストの中に書ける設定が決まっています
location は一つだけにマッチするので ネストすることで全体的な設定と個別の設定を書きます
設定は 「key value;」 形式の行からなるものでドキュメントに一覧があります
Node.js でのフレームワークごとのミドルウェア・プラグインの設定を探すよりもわかりやすいと思います

パフォーマンス的には劣るかもですが そもそもそんなに速度に困ってはないのでわずかに遅くなったくらいで影響はないので その他のメリットが多ければ別にいいかなというところです
それに静的ファイルを nginx で処理すれば その部分は Node.js より速くなるはずです
残る API の処理はもともと少し時間がかかることが多いところなので nginx が増えることによる時間程度は本当に誤差です
静的ファイルの処理を nginx に移すことでユーザー情報が参照できなくて URL を知ってれば誰でもアクセスできてしまいますが PHP だってずっとそれなわけですし 頑張れば画面が見れるくらいで実際のデータは API の認証が必要になるので別にいい気がします

あとこれは Node.js を直接公開してる場合が前提になってます
クラウドを使うみたいなことがあれば別にロードバランサーがあったりすると思うので そうなると Node.js 用の nginx とかはなくしてそっちに任せるでいいと思います
ロードバランサーでは静的ファイルのサーブはしてくれないかもですが クラウドなら CDN を使うことが多いそうですし
そもそもクラウドになると Lambda や Cloud Functions みたいなのがあってあまりサーバー自体を用意しないのかもしれませんけど



簡単に試してただけだと nginx で特に不満は無いと思ってましたが 少し使ってるとちょっとした不満点がありました
圧縮フォーマットの brotli に標準で対応していません
もちろん最近の zstd もです
古い gzip しか使えません
Node.js でも標準で brotli に対応してたので その点効率が落ちるのは残念かも

一応外部モジュールで追加はできるらしいのですが dnf などで簡単には導入できないみたいです
RHEL 系は epel でインストールできる風に書いてるページもあったのですが AlmaLinux9 ではパッケージが見つからなかったです
そんなに手間を掛けてまで入れたいわけではないので gzip で妥協していますが 最近は安定していてあまり新しい機能は追加されてないようなので将来性考えるとどうなのかなと思い始めました

不満点はもう一つあって JavaScript の mime type が application/javascript です
application/javascript はすでに廃止されていて text/javascript が正式です
https://datatracker.ietf.org/doc/html/rfc9239#name-iana-considerations

なのに普段使わないような古い形式になっていて 設定では application/javascript と書かないといけないです
つい text/javascript と書いて動かなかったことがありました
設定変えるなんてめったにないし ほとんどコピペなのですが やっぱりこういうのは気になるのですよね
pino で 〇 を含むプロパティを伏せられない
pino の redact を見てると 「〇」 が使われてた
https://github.com/davidmarkclements/fast-redact/blob/v3.4.0/lib/validator.js

じゃあ〇を使うとエラーになる?と 思い付きで試してみました

pino と redact の使い方としてはこういうの

> require("pino")({ redact: ["key"] }).info({ key: "foo" })
{"level":30,"time":1710331939803,"pid":54,"hostname":"67cfbc893e4a","key":"[Redacted]"}
> require("pino")({ redact: ["あ"] }).info({ あ: "foo" })
{"level":30,"time":1710331978251,"pid":54,"hostname":"67cfbc893e4a","あ":"[Redacted]"}

ログするオブジェクトのパスを複数指定できて 指定したプロパティが伏せられます
〇 を入れてみると

> require("pino")({ redact: [`["〇"]`] })
Uncaught Error: pino – redact paths array contains an invalid path (["〇"])
at /mnt/x8uc1/node_modules/fast-redact/lib/validator.js:29:15
at Array.forEach (<anonymous>)
at validate (/mnt/x8uc1/node_modules/fast-redact/lib/validator.js:12:11)
at handle (/mnt/x8uc1/node_modules/pino/lib/redaction.js:107:5)
at redaction (/mnt/x8uc1/node_modules/pino/lib/redaction.js:16:29)
at pino (/mnt/x8uc1/node_modules/pino/pino.js:129:33)

確かにエラーになってます
ログをしなくてもインスタンスの作成時点でエラーです

少し特殊な

[`["〇"]`]

になってるのは記号の対処のためで 内部でプロパティをどう処理してるかというと

o${expr}

でコードを作ってそれを実行しています
eval みたいなものです

expr が .prop みたいになってます
foo.bar は有効ですが foo.1 や foo.! は無効です
JavaScript として有効なアクセス方法になるように [1] や ["!"] みたいにしないといけないです
そのため少し特殊な書き方になってます

記号などでも ["!"] の書き方にすると動作します

> require("pino")({ redact: ["!"] }).info({ "!": "a" })
Uncaught Error: pino – redact paths array contains an invalid path (!)
(略)

> require("pino")({ redact: [`["!"]`] }).info({ "!": "a" })
{"level":30,"time":1710352889569,"pid":350,"hostname":"8f9aa7f09ea2","!":"[Redacted]"}

〇 の記号はいくつかあって fast-redact に使われてるのは一番上のものです

〇 U+3007
◯ U+25EF
○ U+25CB

他の 〇 ならエラーになりません

> const fn = k => {
... console.log(k, k.charCodeAt().toString(16))
... require("pino")({ redact: [`["${k}"]`] }).info({ [k]: "a" })
... }

> fn("〇")
〇 3007
Uncaught Error: pino – redact paths array contains an invalid path (["〇"])
(略)

> fn("◯")
◯ 25ef
{"level":30,"time":1710416813795,"pid":3966,"hostname":"67cfbc893e4a","◯":"[Redacted]"}

> fn("○")
○ 25cb
{"level":30,"time":1710416820183,"pid":3966,"hostname":"67cfbc893e4a","○":"[Redacted]"}
fs で File URL をサポートしてほしい
Node.js を ESM にしてから不便に感じてる部分はやっぱりファイルパスの解決部分です

CJS のときはこんな感じで書けた部分ですが

[/tmp/a.js]
const path = require("path")

const file_path = path.join(__dirname, "./file.txt")
console.log(file_path)
// /tmp/file.txt

この長さになります

[/tmp/b.js]
import { fileURLToPath } from "node:url"
import path from "node:path"

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)

const file_path = path.join(__dirname, "./file.txt")
console.log(file_path)
// /tmp/file.txt

Node.js 20 なら import.meta に resolve があって __dirname を作らなくてもそのファイルからの相対パスでフルパスを取得できます
でも file:// から始まる URL 形式なので最後に fileURLToPath を通してローカルのパス形式にしないといけないです

[/tmp/c.js]
import { fileURLToPath } from "node:url"

const file_url = import.meta.resolve("./file.txt")
console.log(file_url)
// file:///tmp/file.txt

const file_path = fileURLToPath(file_url)
console.log(file_path)
// /tmp/file.txt

Node.js 21 なら import.meta.dirname があるので CJS と同じようなことができます
🔗 Node.js 21.2 で ESM に CJS の __dirname と __filename 相当の機能が追加された

[/tmp/d.js]
import path from "node:path"

const file_path = path.join(import.meta.dirname, "./file.txt")
console.log(file_path)
// /tmp/file.txt

でも現在の LTS は 20 です
21 に機能追加されて 3 ヶ月以上経っても 20 にバックポートされないのであまり期待できなそうです
使えるのは 22 の LTS からになるかもしれません

それに CJS と近い感じで書けるだけで __dirname が import.meta.dirname になって少し長いです

fs が URL 形式のパスもサポートしてくれるともっと楽なんですけどね
それなら同じフォルダのファイルを読み取るときはこれだけで済みます

fs.readFileSync(import.meta.resolve("./file.txt"))

そういう話がないのか探してみたのですが あまり積極的ではないようで放置されて issue がクローズされてました
https://github.com/nodejs/node/issues/48994

単に中で fileURLToPath を通すだけで file://foo/bar みたいなものを入れてもエラーにするでいいと思うのですけど
Fastify の printPlugins で表示される名前
Fastify では printRoutes や printPlugins で登録したルートやプラグインをわかりやすく表示できます
printPlugins ではプラグインの名前が表示されます

import Fastify from "fastify"
import sensible from "@fastify/sensible"

const fastify = Fastify()

await fastify.register(sensible)

console.log(fastify.printPlugins())
root -1 ms
├── bound _after 8 ms
├── @fastify/sensible 1 ms
└── bound _after 0 ms

ただ register のところで import を使うと問題が出ます

import Fastify from "fastify"

const fastify = Fastify()

await fastify.register(import("@fastify/sensible"))

console.log(fastify.printPlugins())
root -1 ms
├── bound _after 6 ms
├── [object Promise] 1 ms
└── bound _after 1 ms

import が返すのは Promise オブジェクトなので [object Promise] という表示になってました
register の引数のところで import を書くのは Fastify 側でサポートされていて ドキュメントでも見かける方法なのに対応できてないのは残念です

ただ import が終わって Promise が解決されないと中身の情報がわからないです
printPlugins が呼び出されるときに Promise が解決済みとも限らないですし仕方ないものなのかもしれません

名前をつけてみたら動くかなと試したら動きました

import Fastify from "fastify"

const importWithName = (specifier, name) =>
Object.assign(import(specifier), { name: name || specifier })

const fastify = Fastify()

await fastify.register(importWithName("@fastify/sensible", "sensible"))

console.log(fastify.printPlugins())
root -1 ms
├── bound _after 8 ms
├── sensible 1 ms
└── bound _after 0 ms

考えてみたら .name を参照してますが Promise だと .name はないはずです
どうやって作ってるのか気になったのでソースを見てみました

https://github.com/fastify/avvio/blob/v8.3.0/lib/get-plugin-name.js

いくつかパターンがあるみたいです
Promise のプロパティを追加しなくても単純にオプションとして name を渡すで良さそうです

import Fastify from "fastify"

const fastify = Fastify()

await fastify.register(import("@fastify/sensible"), { name: "sensible" })

console.log(fastify.printPlugins())
root -1 ms
├── bound _after 4 ms
├── sensible 1 ms
└── bound _after 0 ms

デバッグなど開発時向け機能の printPlugins のためだけに名前をつける必要があるのかはわかりませんけど 一時的にどれなのかわからないからつけるというときには良さそうです
module の JavaScript を動的にインポートするとき構文エラーの場所がわからない
Node.js を使っていて構文エラーがあったのですが原因の場所がわからなかったです
いつもならファイルと行番号を表示してくれるのですが なぜか表示されません
使ってるツールの都合なのかといろいろ試してたら 単純に Node.js を使うだけでも再現できました
mjs ファイルを動的にロードするとダメみたいです

拡張子によるものではないので type="module" でも同じですがここではわかりやすくするため cjs/mjs の拡張子にします
ロードされる mod.cjs / mod.mjs はどっちも構文エラーになるものです

[mod.cjs]
console.log(1

[mod.mjs]
console.log(1

これらをロードする main.cjs / main.mjs を作ります

[main.cjs]
try {
require("./mod.cjs")
} catch(err) {
console.log("cjs error:", { err })
}

import("./mod.mjs").catch(err => console.log("mjs error:", { err }))

[main.mjs]
await import("./mod.cjs").catch(err => console.log("cjs error:", { err }))
await import("./mod.mjs").catch(err => console.log("mjs error:", { err }))

main のそれぞれを実行します

root@e07f755b37f2:/opt# node main.cjs
cjs error: {
err: /opt/mod.cjs:1
console.log(1
^

SyntaxError: missing ) after argument list
at internalCompileFunction (node:internal/vm:77:18)
at wrapSafe (node:internal/modules/cjs/loader:1290:20)
at Module._compile (node:internal/modules/cjs/loader:1342:27)
at Module._extensions..js (node:internal/modules/cjs/loader:1437:10)
at Module.load (node:internal/modules/cjs/loader:1212:32)
at Module._load (node:internal/modules/cjs/loader:1028:12)
at Module.require (node:internal/modules/cjs/loader:1237:19)
at require (node:internal/modules/helpers:176:18)
at Object.<anonymous> (/opt/main.cjs:2:5)
at Module._compile (node:internal/modules/cjs/loader:1378:14)
}
mjs error: {
err: SyntaxError: missing ) after argument list
at ModuleLoader.moduleStrategy (node:internal/modules/esm/translators:168:18)
at callTranslator (node:internal/modules/esm/loader:279:14)
at ModuleLoader.moduleProvider (node:internal/modules/esm/loader:285:30)
at async link (node:internal/modules/esm/module_job:76:21)
}
root@e07f755b37f2:/opt# node main.mjs
cjs error: {
err: /opt/mod.cjs:1
console.log(1
^

SyntaxError: missing ) after argument list
at internalCompileFunction (node:internal/vm:77:18)
at wrapSafe (node:internal/modules/cjs/loader:1290:20)
at Module._compile (node:internal/modules/cjs/loader:1342:27)
at Module._extensions..js (node:internal/modules/cjs/loader:1437:10)
at Module.load (node:internal/modules/cjs/loader:1212:32)
at Module._load (node:internal/modules/cjs/loader:1028:12)
at cjsLoader (node:internal/modules/esm/translators:359:17)
at ModuleWrap.<anonymous> (node:internal/modules/esm/translators:308:7)
at ModuleJob.run (node:internal/modules/esm/module_job:218:25)
at async ModuleLoader.import (node:internal/modules/esm/loader:323:24)
}
mjs error: {
err: SyntaxError: missing ) after argument list
at ModuleLoader.moduleStrategy (node:internal/modules/esm/translators:168:18)
at callTranslator (node:internal/modules/esm/loader:279:14)
at ModuleLoader.moduleProvider (node:internal/modules/esm/loader:285:30)
at async link (node:internal/modules/esm/module_job:76:21)
}

どちらの場合でも mjs ファイルをロードしたときは構文エラーの場所が表示されていませんね
構文エラーならエディタ上ですぐにわかるのであまり困らないのですが 今回は高機能なエディタがない環境だったので苦戦しました

ちなみに「動的」なインポートでのみ起きます
静的な import の場合はちゃんとエラーの場所が表示されます

[main-s.mjs]
import "./mod.mjs"
root@e07f755b37f2:/opt# node main-s.mjs
file:///opt/mod.mjs:1
console.log(1
^

SyntaxError: missing ) after argument list
at ModuleLoader.moduleStrategy (node:internal/modules/esm/translators:168:18)
at callTranslator (node:internal/modules/esm/loader:279:14)
at ModuleLoader.moduleProvider (node:internal/modules/esm/loader:285:30)

Node.js バージョンは 18, 20, 21 で確認しましたがどれもこうなるようです
Windows の npx で NOENT エラーが出る
Windows 環境で 公式サイトからダウンロードしたインストーラーを使って Node.js をインストールした後に npx を使うとエラーが出ました

no such file or directory

Sandbox 下で Node.js だけをインストールして試すと再現します
バージョンは LTS の 20 系です

npm ERR! code ENOENT
npm ERR! syscall lstat
npm ERR! path C:\Users\WDAGUtilityAccount\AppData\Roaming\npm
npm ERR! errno -4058
npm ERR! enoent ENOENT: no such file or directory, lstat 'C:\Users\WDAGUtilityAccount\AppData\Roaming\npm'
npm ERR! enoent This is related to npm not being able to find a file.
npm ERR! enoent

メッセージのまま AppData\Roaming\npm が無いらしいのですが 無いなら作って欲しいのに作ってくれないみたいです
手動で作ってもいいですが適当になにかのパッケージを npm でグローバルインストールすると作られます

npm -g i (適当なパッケージ名)
Node.js 21.4 で Dirent に parentPath が追加された
Welcome to Node.js v21.4.0.
Type ".help" for more information.
> fs.writeFileSync("foo/bar/file", "text")
undefined
> await fs.promises.readdir("foo/bar")
[ 'file' ]
> await fs.promises.readdir("foo/bar", { withFileTypes: true })
[
Dirent {
name: 'file',
parentPath: 'foo/bar',
path: 'foo/bar',
[Symbol(type)]: 1
}
]

path と一緒ですね
どういう場合に違うんだろうといろいろ試しても違いがわからないのでドキュメントを読むとエイリアスだそうです

どうして全く同じものを?と思いましたが元々は path という名前でした
なのに実体は親のパスです
Dirent に対して path と言われたら name も含めたフルパスを期待するのに readdir の引数に渡されたパスになっています
これが紛らわしいので path を置き換える目的で追加されたようです
Chrome の devtools でエラーオブジェクトの cause の中身が見れない
情報がないエラーが出ていて困ったのですが cause が出ていないだけでした

console.log(new Error("error", { cause: new Error("inner") }))
// Error: error
// at <anonymous>:1:13

inner の情報が表示されません
クリックで開けるのかと思いましたがそういうこともできませんでした

cause に限らず AggregateError の errors プロパティも同様に表示されません

console.log で出ている変数なら右クリックから "Store object as global variable" でグローバル変数に持ってきてから自分で error.cause にアクセスすれば情報が見れるかと思ったのですが エラーオブジェクトはなぜかグローバル変数に持ってこれないみたいです

すでにログされたものはどうしようもなさそうです
対処方法はログ方法を変更して console.dir を使うことです
HTMLElement 等を XML 表示にせずオブジェクト表示させるのに使うものです
エラー表示もそれらと同じ扱いみたいで console.dir でオブジェクト表示を強制すると内側もプロパティも表示されるようになります

console.dir(new Error("error", { cause: new Error("inner") }))
// Error: error
// at <anonymous>:1:13
// cause: Error: inner at <anonymous>:1:41
// message: "inner"
// stack: "Error: inner\n at <anonymous>:1:41"
// [[Prototype]]: Object
// message: "error"
// stack: "Error: error\n at <anonymous>:1:13"
// [[Prototype]]: Object

調べてみると 2 年以上前から要望として issue はあるものの対応されてない状態みたいです
https://bugs.chromium.org/p/chromium/issues/detail?id=1211260

ちなみに Node.js の場合は inspect で内部プロパティを表示してくれるので console.dir にせず通常の console.log でいいです

> console.log(new Error("error", { cause: new Error("inner") }))
Error: error
at REPL47:1:13
at ContextifyScript.runInThisContext (node:vm:121:12)
... 7 lines matching cause stack trace ...
at [_line] [as _line] (node:internal/readline/interface:887:18) {
[cause]: Error: inner
at REPL47:1:41
at ContextifyScript.runInThisContext (node:vm:121:12)
at REPLServer.defaultEval (node:repl:599:22)
at bound (node:domain:432:15)
at REPLServer.runBound [as eval] (node:domain:443:12)
at REPLServer.onLine (node:repl:929:10)
at REPLServer.emit (node:events:526:35)
at REPLServer.emit (node:domain:488:12)
at [_onLine] [as _onLine] (node:internal/readline/interface:416:12)
at [_line] [as _line] (node:internal/readline/interface:887:18)
}
fetch のエラーが環境で違う
Node.js 21 で組み込みの fetch が安定しましたが 20 からたいして変更なさそうだしと 20 でも使ってました
それで動作が違うところがあり バージョンの違いで動作が違う部分があったのかと思ったのですが 20 で揃えても違ってました

違った部分はエラーが起きたときのエラーオブジェクトです
片方はよく見る感じのエラーです

fetch("http://localhost").catch(console.err)
Uncaught TypeError: fetch failed
at Object.fetch (node:internal/deps/undici/undici:11730:11)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
cause: Error: connect ECONNREFUSED 127.0.0.1:80
at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1595:16)
at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
errno: -111,
code: 'ECONNREFUSED',
syscall: 'connect',
address: '127.0.0.1',
port: 80
}
}

もう片方は cause の中が AggregateError になっていて 二つのエラーが含まれていました
同じエラーでなぜ 2 個分含まれているのか疑問でしたがよく見ると address が IPv6 と IPv4 で別になっていました
IPv6 が有効になっている環境で IPv6 で接続できないと自動で IPv4 でも試してくれてるみたいです

ちなみに OS は同じ AlmaLinux9 だったのですが IPv6 が有効な環境と無効な環境がありました
RHEL 系に NodeSource から Node.js をインストールする方法が変わってた
以前は shell script のファイルをダウンロードして bash にパイプする方法でした
実行すると環境に応じた repo ファイルがダウンロードされて /etc/yum.repos.d/ に配置される形でした

それがリポジトリを追加するためのパッケージをインストールする方法になっていました
rpm をインストールするとリポジトリが追加されるという形です
epel などと同じです
こっちの方が扱いやすくて良いですね

例えば Node.js 20 の場合は

dnf install https://rpm.nodesource.com/pub_20.x/nodistro/repo/nodesource-release-nodistro-1.noarch.rpm

という感じです
URL の 20 のところを 18 や 21 にすることで別バージョンに切り替えできます

これでリポジトリが追加されたので nodejs パッケージをインストールしようとすると 指定バージョンの Node.js がインストールできます
CJS を ESM に書き換え
関連
🔗 ESM のみのパッケージが不便
🔗 CJS を ESM に置き換えるのは難しい場合もあった

昔ながらのプロジェクトは相変わらず CJS ですが そろそろ ESM にしようと思って一部書き換えてます
CJS だと ESM のみのパッケージを使うときに 動的 import にするしかなくて非同期処理にせざるを得ないです
自分で Rollup で個別に変換してたときもありましたが 依存パッケージに ESM のみが増えていくとやってられないですし
要望が多くて CJS から同期処理でインポートする手段が提供されるかと思ったりもしてましたが 結局そういうのは入らなそうですし

ESM にしても関連のところに書いたような問題は出てくるのですが CJS から ESM パッケージをインポートするのよりはマシかなというところです

ブロックスコープ内でのエクスポート問題ですが これはひとつのモジュールに色々まとめ過ぎということもあったので ブロックごとに別モジュールにします
モジュールが少ないものだと 5 個が 10 個になるのは倍なので抵抗があったりもしましたが 全体が大きくなって数百もあれば 10 や 20 増えてもたいして気になりませんし 数行しかなくても別モジュールにわけて index.js 的な部分でまとめるようにします

ブロックが if 文なのは条件によってはエクスポートが undefined ということなので宣言的になるよう export const で条件演算子で分岐する形にします

動的な require が import になることで非同期になる問題があります
config ファイルを env の名前を使って読み込むときなどは名前が静的でないので動的 import にせざるを得ません
ですが今はトップレベル await があるので 実質同期的なように初期化できます

読み込み順が変わるので場合によっては問題になることもありますが ほとんどの場合は無視できます
詳しく書くとこういうケースです

/// index.js
import a from "./a.js"
import b from "./b.js"
console.log("I")

/// a.js
console.log("A")
await import("./a2.js")
export default "A"

/// a2.js
console.log("A2")
export default "A2"

/// b.js
console.log("B")
export default "B"

結果はこうなります

A
B
A2
I

これが require だと同期処理なので a.js を最初に読み込み a2.js も同期的に読み込まれます
そのあとに b.js が読み込まれるので順番は A → A2 → B → I です
import だと a.js と b.js は並列して取得してから順番に a と b を実行しますが a.js で非同期処理が入ると b.js に進みます
index.js のインポート部分が全部終わらないと index.js の本体の処理は始まりませんが index.js がインポートするモジュールは同時に処理されます

モジュール内で完結してるなら問題ないのですが トップレベルでグローバルに影響する処理をしたり 別モジュールの関数呼び出したりしていると 実行順で期待通りに動かないこともあります
トップレベルではできるかぎり関数等を定義するだけにして処理は行わないようにして 初期化処理が必要なら使う側で init みたいな関数を呼び出してもらい実行するようしたほうがいいかもですね

残る問題はたまにしか使わない機能なので動的に import する場合です
動的インポートなのでその関数が非同期になってしまいます
完全には避けられないので ロードする処理とモジュールを使う処理を分けて 後者の処理は同期処理に保つくらいしかできないです
ただ いつ最初に使うかわからないので 結局チェックしてロードする処理を挟む可能性が常にあって あまり意味がないです
その機能を呼び出す前の段階でその機能を有効にするようなフェーズがあるのなら そこでインポートしておくという使い方はできそうです

あとは 少し面倒な点で CJS のみ対応のライブラリのインポート時にプロパティを直接 named export とみなせません

/// module.cjs
module.exports = { foo: "bar" }

/// index.js
const { foo } from "./module.cjs"

これができません

const module from "./module.cjs"
const { foo } = module

という一手間が必要です
Node.js の組み込みモジュールはソースコード上は CJS なのにプロパティを直接参照できるのでなにか方法があるのかと思ったのですが なさそうでした
組み込みモジュールだからこそ特別な対応がされているのでしょうか
Node.js でメインモジュールを判定したい
Node.js でプログラムを実行した際に メインモジュールとして実行されたかを判断したいです
メインモジュールとして実行されたときだけ追加の処理をして それ以外のライブラリとして読み込まれたときは何もしないという感じに使います

よく Python で見る

if __name__ == "__main__":
...

をやりたいです

CJS の頃はシンプルな方法で実現できました

if (require.main === module) {
// ...
}

ドキュメントにも記載されている方法です
https://nodejs.org/docs/latest-v20.x/api/modules.html#accessing-the-main-module

しかし 現状の ESM だとこれを簡単に実現する方法はないです

Deno では import.meta.main に true/false でメインかどうかが入っています
同様の機能を実装する issue はあるのですが 2019 年からあるのに未だに実装されません
https://github.com/nodejs/node/issues/49440

__dirname や __filename に相当する機能は実装されたのでこれもそろそろ対応してほしいものです

現状でこれをやろうとするとコマンドラインの引数と比較するという気持ちの悪い方法に頼るしかないです
process.argv[1] と import.meta.url の比較になります
ただコマンドライン引数の場合 パスの解決が必要ですし コマンドラインで指定するものでは拡張子や index.js を省略できるなどもあり 簡単な === では済まないです

なのでこれをうまくやってくれるだけのパッケージが存在します
https://github.com/tschaub/es-main

現時点でスターが 71 で 週間ダウンロード数が 3 万以上です
これだけの需要があるのになぜ標準で実装しないのが疑問です
一部のメンバーが反対してるようなのですが 実装したところでデメリットなんて無いでしょうし CJS の頃からよく使われてるもので JavaScript 外でも使われるような方法なのに 何が気に入らないのでしょうね
Node.js 21.2 で ESM に CJS の __dirname と __filename 相当の機能が追加された
https://nodejs.org/api/esm.html#importmetadirname
https://github.com/nodejs/node/pull/48740

import.meta.filename
import.meta.dirname

で取得できます

root@cca30b68b828:/# cat /tmp/a.js
console.log(import.meta.filename)
console.log(import.meta.dirname)

root@cca30b68b828:/# node --experimental-detect-module /tmp/a.js
/tmp/a.js
/tmp

これまでは import.meta.url から自身のファイルのパスを file:// 形式で取得してローカルパス形式に変換する必要がありました

import path from "node:path"
import url from "node:url"

const __filename = url.fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)

これを毎回書くのが面倒だったのでかなり便利になりますね



ところで 18 や 20 でも使いたいなと思って Polyfill できないか考えてみました
こんなものを作ってみたのですが 期待通りには動かなかったです

import url from "node:url"
import path from "node:path"

if (!import.meta.filename) {
Object.defineProperties(import.meta, {
filename: {
get() {
return url.fileURLToPath(this.url)
},
},
dirname: {
get() {
return path.dirname(this.filename)
},
},
})
}

import.meta ってグローバルオブジェクト風に見えて 特殊なもので モジュールごとに別の実体があるのでどこかでプロパティを追加しても他のモジュールには影響しないです
プロトタイプのないオブジェクトなので プロトタイプの方を拡張することもできません

loader を使って全モジュールの最初に import.meta.filename などを追加する方法は取れなくもないですが ソースコードを暗黙的に書き換えるようなことはあまりしたくないですし 諦めてバックポートを待とうと思います(需要的に安定すればきっとされるはず)
Node.js 21 がリリースされた
Node.20 の LTS 化は来週みたい

Node.js 21 の新機能
https://github.com/nodejs/node/blob/main/doc/changelogs/CHANGELOG_V21.md#21.0.0

ブラウザ互換の WebSocket クライアントが追加されたみたいです
まだ実験的なのでフラグが必要です
Node.js をクライアントとして使いたいケースはパッと思いつかないですが テストには便利かも

fetch が stable になったみたいです
18 からフラグなしで使えてたのでもう普通に使ってましたが そういえばまだ experimental でしたね

glob 機能が追加されたみたいです
ただテスト関連の機能で内部的に使ってるだけみたいで公開されてないので fs モジュールをインポートしても直接使うことはできないようです
glob 機能だけでも使えるようになるといいですね

一番気になるところは --experimental-default-type オプションの追加
これでデフォルトを ESM に変更できます
これまではコマンド実行時に ESM と指定できず 拡張子を .mjs にしたり package.json を作って type に module を指定しないといけなくて面倒でした
これで ESM が使いやすくなる と思ったのですが これには問題もあるようです
ここのサイトで議論されている内容など詳しく説明されてました
https://jser.info/2023/10/18/node.js-roadmap-esm-by-default/

自分のコードの範囲だけ考えてましたが デフォルトが変わると node_modules のパッケージにも影響があります
古いパッケージは package.json に type が記載されていなくて CJS で書かれているので デフォルトが ESM になるとエラーになるということみたいです

たしかにそうですね
でも 現状で困っていてデフォルトを変えたいのは package.json を使わないスクリプトを実行するときです
npm パッケージは使わず 自分で書いたモジュールを読み込むときです
対象を package.json が無いところだけ にして node_modules の中の package.json があるところはこれまで通りで type を見て 無ければ CJS でいいと思うんです

package.json を作るところなら type: module の記載をするようにすればいいですし パッケージマネージャーなどが package.json を作るときにデフォルトで type: module になっていれば不便に思うところも無い気がします

package.json があって type が無いものまで ESM にするのだと 過去パッケージは使えないことにするしかないですし 中身を見て判断するというのはパフォーマンスが悪くなるなどが理由で .cjs と .mjs の拡張子に分けると決まったときに拒否されてたはずです
Node.js のバージョンアップでフロントのビルドが動かなくなった
Node.js ってけっこう互換性は保たれてるので メジャーアップデートしても問題なく動くことがほとんどでした
特にフロント側は Webpack や Parcel や Vite などでビルドするだけなのでバージョンの違いはほぼ影響ないです
なので フロント側のビルドではプロジェクトごとに Node.js バージョンの管理はしてないです
Windows に入れてる共通の Node.js でビルドしてます
新機能が使いたくなったら LTS の範囲で更新してるものです

最近はそこまで使いたい新機能もなかったので その環境では Node.js 16 が EOL になる少し前くらいに Node.js 18 にしました
それからしばらくして 1 年近くビルドしてなかったプロジェクトを少し更新してビルドしようとしたところエラーになってました

node_modules は前回のビルド以降更新してなくてどうしてエラーが起きるのか最初は原因がわからなかったのですが 調べてみると Node.js のバージョンが原因でした
Node.js 16 から Node.js 18 に上げると package.json の exports フィールドの動きに違いが出ます
Node.js 18 では exports で公開してるもの以外は直接 import/require できなくなります
このせいで一部のライブラリ間の読み込みで失敗していました
正確に書くと postcss 系です
postcss の exports に書かれていないファイルを postcss 系のパッケージのモジュールが直接読み込もうとしてエラーでした

バージョンアップで解決されるはずですが こういう内部で依存関係として自動的にインストールされているものって そこだけを更新し辛いのですよね
このプロジェクトは全体的に古いもので構成されていて できるだけ変更はしたくないです
いつもはとりあえず全部最新にしてるのであまり困らないですが 古いもので最低限の更新だけするというのはやりづらいです
lock ファイルで問題のパッケージのバージョンだけ書き換えることも考えましたが 依存関係が複雑なところだとおかしくなることもありそうで直接触るのは避けたいです

ビルドにしか使わないなら EOL でも別にいいので Node.js のバージョンを古いままにするのが正解なのかもですね
Docker を使って古いバージョンの Node.js でビルドしてみました
ビルドはファイルの読み取りが多い処理なのでファイルが Windows 側にあるとかなり遅いです
もしかすると コンテナ内でリポジトリをクローンしてビルドして push するほうが早いかもしれません
StackBlitz の Node.js のサーバーフレームワーク
StackBlitz で Backend 側テンプレートを見てると 見慣れないものがありました

Egg.js
https://github.com/eggjs/egg

Feathers
https://github.com/feathersjs/feathers

H3
https://github.com/unjs/h3

Nitro
https://github.com/unjs/nitro

ラインナップには Express や Koa はあるのに Hapi や Fastify はありません
それらよりもここにあるのは有名なのでしょうか?

ただ Egg.js と Feathers は言われてみると結構前にも見た覚えがなんとなくあります
2018 年か 2019 年くらいにフレームワークを探してたときだったかもです

たしか Egg.js は Koa を中で使ってて 中国でよく使われてるやつだったかと思います
Feathers はリアルタイム系らしいので WebSocket 系のようです
ソースコードを見てみると Socket.IO を使っていました

今でも StackBlitz のテンプレートに並ぶくらいには使われてるのでしょうか

H3 と Nitro は聞き覚えがないので新しいものかもしれません
リリース履歴をみるとどちらも去年からみたいなので新しいようです

H3 の方は Minimal H(TTP) framework らしく 小さめのライブラリみたいです
ミドルウェア系で Express/Koa に近い感じです
Router がついてるので Koa よりは高機能かもしれません
Express と Koa の違いみたいにハンドラで受け取るオブジェクトが異なっています
ただ Express と互換性のある形にもできるそうです
そうしないと Koa があまり流行らなかったみたいなことになりそうですからね

Nitro の方は Nuxt 関連のもののようです
Nuxt を強化できるらしいですが Nuxt なしでも使えるようで Nuxt を使う場合と使わない場合で設定の書き方が分かれてました

どれも今のところは特に使うことなさそうです
孫プロセスをバックグラウンドで実行するときにプロセスが残る
こういう JavaScript ファイルと ShellScript ファイルがあります

[job.sh]
#!/bin/bash

echo pre something something

./job.js &

echo post something something

[job.js]
#!/usr/bin/env node

console.log("Start job.js")

setTimeout(() => {
console.log("initialized")
}, 500)

// keep process
setInterval(() => {}, 100000)

job.js は常駐するプロセスです
ここでは setInterval でプロセスが終了しないようにしています
これを job.sh から起動します
job.sh は別の Node.js プロセスから起動します
job.sh を起動する側はこんな感じです

const cp = require("child_process")

const sub = cp.spawn("./job.sh")

sub.stdout.setEncoding("utf8")
sub.stdout.on("data", console.log)
sub.on("close", () => {
console.log("CLOSED")
})

job.sh や job.js の出力を受け取りたいので stdout の標準出力にリスナをつけています
job.js は常駐しますがすべてを受け取る必要はなくて 起動直後の内容だけでいいです
つまりは job.sh が終われば close イベントが起きてほしいです
しかし close イベントは起きません
子プロセスである job.sh は終了済みなのですが孫プロセスの job.js が生きていることで stdout や stderr が生きてるのでこれらが閉じられるまでは Node.js は子プロセスが終了したとみなしてくれないようです
放置でもいい気はしますがずっと残るわけなのでメモリリーク状態です

stdio が問題なので ignore にしてしまえば標準入出力を使わないようにできて これなら即終了とみなされ CLOSED が表示されます

const sub = cp.spawn("./job.sh", { stdio: "ignore" })

ただし問題があって標準出力を受け取れないです
ファイルに出力するようにして ファイルを読み取るということはできますが 回りくどくてイマイチです

起動する側もサーバーみたいな常駐するものだとメモリリークになるのを気にしないとですけど 一回限りのスクリプトでスクリプトが終了しないのが嫌という場合は unref で解決できます

const cp = require("child_process")

sub = cp.spawn("./job.sh")
sub.stdout.setEncoding("utf8")
sub.stdout.on("data", console.log)
sub.on("close", () => {
console.log("CLOSED")
})

setTimeout(() => {
sub.unref()
sub.stdout.unref()
sub.stderr.unref()
}, 1000)

必要な出力が出る程度に 1 秒待ってから unref しています
ちゃんとやるなら stdout の出力から必要なものを受け取れたら実行みたいにしたほうがいいです

まぁ終了させるなら unref していかなくても process.exit でもいいのですけどね

考えてみると常駐してるものから出力を受け取り続ける以上終了しないのが正常ですよね

job.sh で job.js を起動する部分で

./job.js > file 2>&1 &

みたいにしてしまったほうがいいかもしれませんね
TypeScript ユーザーってサーバーサイドで何使ってるんだろう
TypeScript が使われてるところが相変わらず増えてるなぁと思いながら ふとサーバーサイドって何使ってるんだろうと思いました
やっぱり Node.js なのでしょうか
Deno は TypeScript をサポートしているので TypeScript ユーザーは積極的に移行して Deno が流行るのかなと思ったこともありましたけど 最近はあまり流行ってないようです
そもそも Deno という名前を目にする機会がほぼないです

Deno が npm をサポートしてしまってからはどんどん注目されなくなりつつあるとか聞いたこともあります
たしかに私自身 npm をサポートしたあたりであまり興味がなくなってきましたし
npm を捨てて完全に新しいものという点で興味があったのに 結局 npm を使うなら Node.js でいいやと言う感じです
ライブラリ作者視点でも npm に登録しておけば Node.js からも Deno からも使えるならとりあえず npm に登録となって ユーザーも npm で使えるなら Node.js のままという感じでしょうか

私の場合は TypeScript を使わないので Node.js のままでも全然問題ないですが TypeScript を使う人の場合は変換のひと手間が必要になるので 新規に TypeScript で何かを作るときは Deno にしてもいいと思うのですけどね
TypeScript の普及に比べて Deno があまり普及してないように思うので 何を使ってるのかなと疑問に思いました

ただ TypeScript/JavaScript の記事を思い出してもサーバーサイドとして使ってるのをあまり見ない気もします
みんな基本はフロントのみで Vite や Next.js みたいな既存ツールを使うためだけに使っていて サーバーサイドにはそもそも TypeScript/JavaScript を使ってないのかもしれません
そういう用途だけなら Node.js でパッケージを入れて実行するだけでいいですし
Node.js でイベントループに残ってるタスク数を確認する
process.getActiveResourcesInfo() を使うとイベントループに残ってる一覧を文字列で見れる

Welcome to Node.js v18.17.1.
Type ".help" for more information.
> console.log(process.getActiveResourcesInfo())
[ 'TTYWrap', 'TTYWrap', 'TTYWrap' ]
> setTimeout(() => {}, 10000)
> console.log(process.getActiveResourcesInfo())
[ 'TTYWrap', 'TTYWrap', 'TTYWrap', 'Timeout' ]

setTimeout を登録すると Timeout が追加された
setTimeout を複数登録すると 配列の中の Timeout が複数になる
残ってる非同期処理の数がわかるのでどれくらい負荷があるかの参考にできる

console.log(0, process.getActiveResourcesInfo())
console.log(1, process.getActiveResourcesInfo())

setTimeout(() => {}, 100)

console.log(2, process.getActiveResourcesInfo())

const timeout = setTimeout(() => {}, 100)

console.log(3, process.getActiveResourcesInfo())

timeout.unref()

console.log(4, process.getActiveResourcesInfo())

require("fs/promises").readFile(__filename)

console.log(5, process.getActiveResourcesInfo())
0 []
1 [ 'TTYWrap', 'TTYWrap' ]
2 [ 'TTYWrap', 'TTYWrap', 'Timeout' ]
3 [ 'TTYWrap', 'TTYWrap', 'Timeout', 'Timeout' ]
4 [ 'TTYWrap', 'TTYWrap', 'Timeout' ]
5 [ 'FSReqPromise', 'TTYWrap', 'TTYWrap', 'Timeout' ]

ファイル指定で実行すると最初は何も入ってなくて 2 回目からデフォルトのものが含まれる
unref すると active 扱いされなくなって一覧から消える

最初から入ってるものは OS や CJS/ESM でも違うみたい

console.log(0, process.getActiveResourcesInfo())
console.log(1, process.getActiveResourcesInfo())

これを実行すると

Windows の場合

D:\t12>node a.cjs
0 []
1 [ 'TTYWrap', 'TTYWrap' ]

D:\t12>node a.mjs
0 [ 'CloseReq' ]
1 [ 'CloseReq', 'TTYWrap', 'TTYWrap' ]

Linux の場合

root@426b4d5c3e42:/opt# node a.cjs
0 []
1 [ 'TTYWrap' ]

root@426b4d5c3e42:/opt# node a.mjs
0 [ 'CloseReq' ]
1 [ 'CloseReq', 'TTYWrap' ]

バージョンはどっちも Node.js 18

Windows だと TTYWrap が 2 つ入ってる
Linux だと 1 つ

REPL だと Windows も Linux も TTYWrap がもう一つ増える

ESM だと CloseReq が最初から入ってる
CJS だと入ってない
Node.js に Symbol.dispose が増えてた
Node.js 20.4 で Explicit Resource Management の proposal をサポートしたそうです
試してみましたが using は構文エラーでした

リソースの解放が必要な Node.js の API で using に対応しただけで using そのものはまだ使えないみたいです
--experimental 系フラグを見てもそれらしいのはまだなかったです

using を使って解放可能なリソースのオブジェクトは Symbol.dispose や Symbol.asyncDispose プロパティを持っていて 解放処理の関数が入ってます
それらが追加されたということみたいです

対応前のバージョンだと

Welcome to Node.js v18.13.0.
Type ".help" for more information.
> Symbol.dispose
undefined
> Symbol.asyncDispose
undefined

ですが 対応後のバージョンだと

Welcome to Node.js v20.5.0.
Type ".help" for more information.
> Symbol.dispose
Symbol(nodejs.dispose)
> Symbol.asyncDispose
Symbol(nodejs.asyncDispose)
> fs.writeFileSync("a", "")
undefined
> const a = fs.createReadStream("a")
undefined
> a[Symbol.dispose]
undefined
> a[Symbol.asyncDispose]
[Function (anonymous)]

ファイルを読み取る stream では Symbol.asyncDispose に関数が入ってますね

シンボルが nodejs.dispose という名前になってますが V8 エンジン側でまだサポートされてないから Node.js で独自に用意してるだけで 正式に V8 側で対応したら独自のものじゃなくなるのだと思います
Node.js の stream とマルチバイト文字列
Node.js で stream を使うとき デフォルトだと Buffer として受け取ります

const stream = require("stream")
const timer = require("timers/promises")

const readable = new stream.Readable({
read() {},
})

readable.on("data", chunk => {
console.log(chunk)
})
readable.on("end", () => {
console.log("END")
})

const push = async () => {
await timer.setTimeout(1000)
readable.push(Buffer.from("abc"))
await timer.setTimeout(1000)
readable.push(Buffer.from("def"))
await timer.setTimeout(1000)
readable.push(null)
}
push()
<Buffer 61 62 63>
<Buffer 64 65 66>
END

文字列が欲しいときは Buffer の toString で変換できます
そういうコードを結構見かけます
データを受け取るたび toString した文字列を結合していって最終的な文字列を作ってたりです
でもこれが安全なのは英語圏だけです
日本語などのマルチバイト文字のときに問題が起きます

たとえば日本語の「あ」「い」「う」は UTF-8 ではこういう 3 バイトで表現されます

あ: 227, 129, 130
い: 227, 129, 132
う: 227, 129, 134

stream では基本的に複数のチャンクに分割されています
分割される場所が 1 文字の途中だった場合はこうなります

const stream = require("stream")
const timer = require("timers/promises")

const readable = new stream.Readable({
read() {},
})

readable.on("data", chunk => {
console.log(chunk, chunk.toString())
})
readable.on("end", () => {
console.log("END")
})

const push = async () => {
await timer.setTimeout(1000)
readable.push(Buffer.from([227, 129, 130, 227])) // い の 1 バイト目まで
await timer.setTimeout(1000)
readable.push(Buffer.from([129, 132, 227, 129, 134])) // い の 2 バイト目から
await timer.setTimeout(1000)
readable.push(null)
}
push()
<Buffer e3 81 82 e3> あ�
<Buffer 81 84 e3 81 86> ��う
END

不正なデータとなり 「い」 のそれぞれのバイトが 1 文字の�とされています
こういうことがあるので全部受け取ってから Buffer を結合後に文字列化したほうが安全です
でも streaming 的に処理したい場合は最後を待ってられないです
こういうときは encoding を utf-8 にしておくと 受け取るデータが最初から文字列になってるだけじゃなくてマルチバイト文字の切れ目もうまく扱ってくれます

const stream = require("stream")
const timer = require("timers/promises")

const readable = new stream.Readable({
encoding: "utf-8", // ←追加
read() {},
})

readable.on("data", chunk => {
console.log(chunk)
})
readable.on("end", () => {
console.log("END")
})

const push = async () => {
await timer.setTimeout(1000)
readable.push(Buffer.from([227, 129, 130, 227])) // い の 1 バイト目まで
await timer.setTimeout(1000)
readable.push(Buffer.from([129, 132, 227, 129, 134])) // い の 2 バイト目から
await timer.setTimeout(1000)
readable.push(null)
}
push()

いう
END
POST データが最後まで届かないと思ったら 受け取り切らずにレスポンスを返してるからだった
POST リクエストでペイロード部分をサーバー側でちゃんと受け取れてないケースがあって 原因調査のためにあれこれやってると再現したりしなかったり
余計なライブラリなしで 再現性がある程度あるものが用意できた と思ったら別原因で別の問題になってました

その時のコードがこんなの

[server.js]
require("http").createServer((req, res) => {
let size = 0
req.on("data", (chunk) => {
console.log("DATA", chunk.length, size += chunk.length)
})
req.on("end", () => {
console.log("END")
})
req.on("error", (err) => {
console.log("ERROR", err)
})
res.end("ok")
}).listen(8000)

[client.js]
const fs = require("fs")
const { request } = require("http")

const data = "abcd".repeat(1024 * 100)
fs.writeFileSync("tmp", data)

setInterval(() => {
const req = request({
hostname: "localhost",
port: 8000,
path: "/",
method: "POST",
headers: {
"Content-Type": "text/plain",
},
}, res => {
const buffers = []
res.on("data", (chunk) => {
buffers.push(chunk)
console.log("RES DATA", chunk.length)
})
res.on("end", () => {
const response = Buffer.concat(buffers).toString()
console.log("RES END", response)
})
})

req.on("finish", () => {
console.log("REQ FINISH")
})
req.on("error", (err) => {
console.log("REQ ERROR", err)
})

fs.createReadStream("tmp").pipe(req)
}, 1000 * 10)

fetch ではなく request を使っていたり 一旦ファイルに書き込んで readable stream を pipe しているのは 元の現象が発生したときに使っていたライブラリの内部実装に合わせたためです

出力は

[client]
> node .\client.js
REQ FINISH
RES DATA 2
RES END ok
RES DATA 2
RES END ok
RES DATA 2
RES END ok
RES DATA 2
RES END ok
RES DATA 2
RES END ok

[server]
> node .\server.js
DATA 65390 65390
DATA 146 65536
DATA 65381 130917
DATA 155 131072
DATA 65372 196444
DATA 164 196608
DATA 65363 261971
DATA 173 262144
DATA 65354 327498
DATA 182 327680
DATA 65345 393025
DATA 191 393216
DATA 16384 409600
END
DATA 65390 65390
DATA 146 65536
DATA 65381 130917
DATA 155 131072
DATA 65372 196444
DATA 164 196608
DATA 65390 65390
DATA 146 65536
DATA 65381 130917
DATA 155 131072
DATA 65372 196444
DATA 164 196608
DATA 65390 65390
DATA 146 65536
DATA 65381 130917
DATA 155 131072
DATA 65390 65390
DATA 146 65536
DATA 65381 130917
DATA 155 131072
DATA 65372 196444
DATA 164 196608

分かりづらいですが 5 回分のリクエストがあります
1 回目は正常なのですが 2 回目以降はサーバー側に途中までしかデータが届いていません
DATA の行の 2 つめの数字がそのリクエストの合計バイト数なので 前の行より減っているところからが次のリクエストです
クライアント側ではレスポンスは受け取れていますが finish イベントが起きなくなっています

なんどかプロセスを再起動してみても最初だけが成功します

接続したままになってるのかと思って socket を見てみると 途中までの場合でも毎回ちゃんとクローズしてるようです
サーバー側のリクエストハンドラにこういうコードを追加します

	req.socket.on("close", () => {
console.log("SOCK CLOSE")
})
(略)
DATA 16384 409600
END
SOCK CLOSE

(略)
DATA 164 196608
SOCK CLOSE

(略)
DATA 164 196608
SOCK CLOSE

原因不明で苦戦していたのですが すごく単純な理由でした
サーバー側のリクエストハンドラではペイロードを取得するためのイベントリスナを設定してはいますが レスポンスは同期的に即返しています

レスポンスを返してしまってるので もう残りのリクエストのペイロードを送信する必要はないと判断して途中で切断してしまっているみたいです

	res.end("ok")

を req の end イベントのハンドラ内に移すと この現象は発生しなくなりました
Node.js で事前処理をした状態で REPL を開く
REPL でデータを確認したりフィルタしたり変換したりと色々したいことがあります
そこで使うデータは別のファイルを読み取って変換したデータになるのですが REPL の中で毎回その準備をするのは大変です
1 回の REPL プロセス内で必要なデータの確認まで全てやって終わりならともかく変数等をクリアしたくてプロセスを終了して再度実行はけっこうあります
あれを見たいと思うたびに REPL のプロセスを起動してデータを読み取って準備しては手間です

「↑」キーで履歴呼び出しができるのですが 1 実行ごとの呼び出しなので初回準備が何行にも及ぶと再実行が面倒です
スクリプトにまとめておいてそれを require するだけにすれば 1 行になりますが REPL 操作部分が長くなると履歴からその require を探すのも一苦労だったりします
ということでいい方法を探したところ -r オプションでモジュールをロードするのが良さそうでした

仮にデータは 1 行 1 JSON 形式で保存されたテキストファイルで それをパースしてオブジェクトの配列として扱うとします

> cat data.txt
{"x":1,"y":2}
{"x":4,"y":0}
{"x":2,"y":10}

> cat pre.js
globalThis.items = require("fs").readFileSync("./data.txt").toString()
.split("\n").filter(x => x.trim()).map(x => JSON.parse(x))

この pre.js を -r で読み込むと REPL が実行される前にロードされるのでグローバル変数にデータを入れておけば参照できます

> node -r "./pre.js"
Welcome to Node.js v18.12.1.
Type ".help" for more information.
> items.filter(item => item.x === 1)
[ { x: 1, y: 2 } ]

データのファイルが複数あり ファイルを選択できるようにしたい場合はコマンドライン引数で渡すようにもできます

> cat pre.js
globalThis.items = require("fs").readFileSync(process.argv[2]).toString()
.split("\n").filter(x => x.trim()).map(x => JSON.parse(x))

> node -r "./pre.js" - data.txt
Welcome to Node.js v18.12.1.
Type ".help" for more information.
> items
[ { x: 1, y: 2 }, { x: 4, y: 0 }, { x: 2, y: 10 } ]

プロセスを終了して再実行するときには node コマンド内の REPL で実行したコマンドは関係なくシェル側の履歴から参照するので 1 回 「↑」 を押して再実行するだけです
Node.js でモジュールをフォルダにまとめるとき
(1) foo.js を読み込んで foo.js が foo/ 以下のモジュールを読み込む
foo.js
foo/file1.js
foo/file2.js

(2) foo/index.js を読み込んで foo/index.js が foo/ 以下のモジュールを読み込む
foo/index.js
foo/file1.js
foo/file2.js

CJS なら (2) が相性がいい
フォルダ名の foo を require すれば自動で index.js を補完してくれる
もとは foo.js 単体であとからモジュール分けするときに foo で require していれば読み込む側のコードは変更しなくていい
それに foo に関係するモジュールはすべて foo フォルダに入ってる

だけど ESM は CJS みたいな補完はなくて完全なファイル名が必要
どのモジュールをロードしているのかわかりやすくはあるけど import に foo/index.js のように書かないといけなくなる
foo.js からモジュール分けすると使うところの変更も必要
だから ESM なら (1) 形式にしたほうがいいのかなと最近思ってたりする
foo.js が foo/ を単純にインポートしてエクスポートするだけならまだいいけどここでも処理を入れると foo フォルダに入っていて欲しくなる
ESM のみのパッケージが不便
最近では ESM 版しか用意せず CJS 版がないパッケージもでてきているようです
CJS からでも import() を使えば ESM モジュールを読み込めますが 非同期処理になります
CJS ではトップレベルの await ができないので ESM のみのパッケージが入るだけであちこちで非同期処理が発生してとても使いづらいです

中にはこれまで CJS にしていたのにアップデートで ESM のみになるパッケージもあります
あるパッケージの最新の機能を使いたいのでアップデートしたら とても困ったことになりました
パッケージの読み込み等はすべて同期処理を前提として作っているものだったので まともに対応するなら大幅な作り直しが必要になります
まともに対応なんてやってられないので別の方法で回避することにしました

一つの方法は CJS 化すること
これまで CJS だったのですから こっちで CJS に変換してから使います
Rollup などを使います
ただこれには問題があって 対象が依存パッケージにも及びます

「MyPackage → A → B」 のような依存関係になっているとき A が ESM のみだと B も ESM のみの可能性があります
一つのプロジェクトでパッケージを分割してるような場合だと A の依存関係にも ESM のみパッケージが含まれるケースが多いです
自分のパッケージからの直接の依存関係である A だけを CJS 化しても A から B をインポートするときに require だとインポートできないというエラーになります
ESM パッケージすべての変換が必要ですが 多くなってくると 1 つ 1 つを CJS 化するのは大変です

手抜きをするなら A の CJS 変換時に依存関係も解決してバンドルすることです
A の変換済み CJS モジュールには A も B も含みます
楽にはなりますが B のパッケージが複数の A からインポートされる場合に A ごとに異なる B が存在することになります
同じパッケージを別パッケージとして複数回読み込むことになるのでムダが多いです
さらにキャッシュなどで状態を持つモジュールがあると 別モジュールとみなされることで動作に影響する場合もあります
class なら instanceof を使った判定を行うケースもあり 別モジュールだと実体が違うので別物とみなされ意図した動作にならない場合もあります
この点ではパッケージ 1 つ 1 つを CJS 化した方が良いです


別の方法では CJS 化せずそのまま使います
問題は import() によって読み込みが非同期化することなので 事前にすべての読み込みを終えてからメインの処理を実行することで非同期読み込みを不要にします

エントリポイントのスクリプトが index.js なら boot.js を追加してエントリポイントをこっちに変更します
boot.js はこんな感じにします

const esm_modules = require("./esm_modules.js")

!async function() {
const modules = [
"mod1",
"mod2",
// ...
]

const loaded = await Promise.all(modules.map(name => import(name)))

for (const [idx, name] of modules.entries()) {
esm_modules[name] = loaded[idx]
}

require("./index.js")
}()

esm_modules という空のモジュールを用意して ここを import 済み ESM モジュール置き場にします
ESM のパッケージをすべて ここで import() します
読み込みが終われば esm_modules のプロパティに保持します
これらの処理が終わってから require で本来のエントリポイントの index.js を実行します

その他モジュールでは ESM のみパッケージを require するところを esm_modules の require に置き換えます

const { mod1 } = require("./esm_modules.js")

index.js 以降の処理では ESM のみパッケージは変数に保持されているので非同期処理が不要になります
多少のコードの修正は必要ですが 非同期対応に比べれば遥かに簡単な修正で済みます
Node.js 18 のスナップショット機能に期待
Node.js 18 で最初にスナップショット機能が入ると聞いたときに調べたら Node.js のビルド時のもので その方法でビルドされた Node.js の実行ファイルはスナップショットの状態から起動するというものでした
そんな気軽に使えないものなら使わないかなと思っていましたが もっと簡単に使えるスナップショット機能も入ったようです

const foo = 1
globalThis.bar = foo

というスクリプトを用意して --build-snapshot で指定します

node --build-snapshot ss.js

すると snapshot.blob ファイルが作られるので これを --snapshot-blob に指定して実行します

root@d4572737f3a3:/tmp# node --snapshot-blob snapshot.blob
Welcome to Node.js v18.10.0.
Type ".help" for more information.
> foo
Uncaught ReferenceError: foo is not defined
> bar
1

ここでは実行するスクリプトを指定していないので REPL が起動しています
ここで foo と bar にアクセスしてみると bar の方は 1 が入ってます

グローバル変数以外のモジュールのトップレベルのスコープなどは引き継がないみたいで foo の方は見つかりませんでした
export の代わりに globalThis に入れておけば Node.js でスクリプトの実行時に最初から使えます
準備に時間がかかる処理があって スクリプトを繰り返し実行するときには便利そうですね

コマンドラインツールはもちろんですが ウェブサーバーやアプリケーションでも呼び出すと起動する関数をグローバル変数に配置しておいて 実行時のスクリプトはそれを呼び出すだけにすると高速で起動ができそうです

ただグローバル変数を使うのはあまりやりたくないです
モジュールにすればできるかなと思ったのですが require してもあるはずのモジュールが見つかりませんでした
require を見てみると

[Function: requireForUserSnapshot]

というもので通常の require とは別物になってるようです
基準パスが変わってたりするのかなと 絶対パスでモジュールを指定してみましたが これでもインポートできませんでした
調べてみるとドキュメントに記載がありました
https://nodejs.org/docs/latest-v18.x/api/cli.html#--build-snapshot

まだ 実験的なものなのでいくつか制限があって そのひとつでユーザーランドのモジュールはサポートしていないようです
バンドルして 1 ファイルにすれば動かせるそうです
ということはグローバル変数に置くしかなさそうですね
それにブラウザでならともかく Node.js でまでバンドルはしたくないので この辺がサポートされるの待ちです

完全に使えるようになると色々便利そうなので期待の機能です
Node.js でファイルを 1 行ずつ読み取る
ビルトインの readline モジュールはユーザーからの入力受付用かと思っていましたが ファイルの読み取りにも使えるみたいです

const fs = require("fs")
const readline = require("readline")

const fn = async () => {
fs.writeFileSync("test.txt", "foo\nbar\nbaz")

const rl = readline.createInterface({ input: fs.createReadStream("test.txt") })
for await (const line of rl) {
console.log({ line })
}
}

fn()
{ line: 'foo' }
{ line: 'bar' }
{ line: 'baz' }

readline で作成した Interface は async iterator を持ってるので for-await-of で使えます

const readline = require("readline")

const rl = readline.createInterface({
input: { on(){}, resume() {} }
})

console.log(rl[Symbol.iterator])
// undefined
console.log(rl[Symbol.asyncIterator])
// [Function (anonymous)]

Node.js 17 から readline に Promise API が追加されています
これは question みたいな関数がコールバックか Promise かの違いで 行ごとに読み取るだけなら 16 まででも使えます
18 以降でも変わりないです

注意しないといけないのがこういうケースです

const fs = require("fs")
const readline = require("readline")

const fn = async () => {
fs.writeFileSync("test.txt", "foo\nbar\nbaz")

const rl = readline.createInterface({ input: fs.createReadStream("test.txt") })

await new Promise(r => setTimeout(r, 100))

console.log(1)
for await (const line of rl) {
console.log({ line })
}
console.log(2)
}

fn()

readline の Interface を作ってから for-await-of で読むまでに非同期処理を挟む場合です
これを実行すると 1 だけが出力されます
for-await-of では待機したままデータなしになって 解決しない Promise という扱いで それ以降実行できるものがなくプロセスが終了します
その結果 2 を出力する console.log にたどり着かないので出力は 1 だけです

内部で stream の resume が呼び出されるので自動で読み進めてしまうみたいです
非同期処理を挟まず for-await-of を実行すると asyncIterator の作成処理で stream のイベントが起きる前にリスナを設定できます
その結果 正常に line イベントなどを受け取れて期待どおりに動作します
しかし 非同期処理を挟むと先にイベントが起きてしまって stream が close されたあとにリスナをつけることになるのでなんのイベントも起きず解決されない Promise になるということみたいです
JSON を POST するスクリプトは Node.js で良さそう
JSON を POST リクエストするちょっとしたスクリプトが必要なとき Node.js はあまり使いませんでした
標準の http モジュールだと辛すぎで node-fetch などなにかのパッケージが必要になります
ちょっとしたスクリプトなのに npm のパッケージを入れるのはなんかなーと思うところがあります
zx も同様

ただ他にいい方法もあまりないんですよね
単純なリクエストなら curl ですが JSON を送るとなると面倒です
jq コマンドを使って複雑なことをしてる例を見ましたが これなら他の言語でいいかなと思います

結局ベストと言える方法もなかったのですが Node.js が 18 で標準で fetch が使えるようになりました
トップレベル await もあるのですごくシンプルにかけます

const [param1, param2] = process.argv.slice(2)
const data = { param1, param2 }
const res = await fetch("url", { headers: { "content-type": "application/json" }, body: JSON.stringify(data) })
const result = await res.json()

console.log(res.status, result)

node <ファイル名> foo bar



{ "param1": "foo", "param2": "bar" }

を送信できます

ただし トップレベル await は CJS だと使えません
ESM とするには package.json に type を書く必要があります
package.json も作りたくないところでは .mjs 拡張子にする必要がありこれが微妙です
いっそ deno という手もありますが 追加インストールしないといけないのがネックです
コンテナを使うならどっちでもいいですが それなら zx にしてしまう方がいいのかもとも思ったりです
Node.js 16 の EOL が早まるみたい
https://nodejs.org/en/blog/announcements/nodejs16-eol/

もともと Node.js 16 で OpenSSL 3 に切り替える予定だったけど間に合わなくて 1.1.1 になってて このバージョンの OpenSSL の EOL は 2023/09/11 らしい
Node.js 16 の EOL はもっと先の予定だけど EOL を迎えたバージョンの OpenSSL を使い続けるのは脆弱性リスクがあるし OpenSSL 3 にすると互換性の問題もあるしということで Node.js 16 の EOL を OpenSSL 1.1.1 に合わせて早めることにしたみたい

本来の予定だと
https://nodejs.org/en/about/releases/

LTS バージョンごとの Active LTS 開始 / Maintenance LTS 開始 / EOL 日付は

v14  2020-10-27  2021-10-19  2023-04-30
v16 2021-10-26 2022-10-18 2024-04-30
v18 2022-10-25 2023-10-18 2025-04-30

いつも通りだけど毎年 10 月末に新しい LTS がリリースされる
約 1 年経つとメンテナンス LTS に切り替わる
それから約 1 年半後の 4 月末で EOL

Node.js 16 は 2024/04/30 が 2023/09/11 に変わるということみたい
14 の EOL から半年もないので結構短め

最近までは毎年の LTS が変わるたびに大きめな新機能が入ったりで毎年更新してたけど 16 からはそこまで大きく変わらないし しばらくは 16 でいいかななんて思ってたけど 今年も早めに 18 に切り替えて行く方がいいのかも
Node.js 18 のスナップショット機能が思ってたのと違った
Node.js 18 の新機能でスナップショット機能が入ると聞いて期待してたのですが思ってたのと違いました

思ってたのは好きなタイミングでメモリ状態をファイルに書き込むことができて Node.js の起動時にそのファイルを指定すれば同じ状態から起動できるというもの
サーバみたいに常駐するものならともかく コマンドラインで繰り返し実行する系のツールでは node_modules のモジュールを毎回ロードするので遅いですしムダに感じます
最初からモジュールがロード済みの状態で起動できて ユーザが入力したコマンドに応じた処理だけをすれば無駄なくいい感じになりそうです

ですが実際にはそんな便利なものではなく Node.js をビルドするときに .js ファイルを指定できるというもの
指定してビルドされた Node.js 実行ファイルを実行すると その .js ファイルを実行した状態でファイルを実行できるようです
自分でビルドしないといけないので結構面倒で気軽に使うものではなさそうです
また 実行時にスナップショットファイルを指定できるわけではないので スナップショットの数だけ Node.js の実行ファイルが作られます
ライブラリやツールごとに Node.js 実行ファイルがあるようなものです
すでに Electron はそういう状態でアプリごとに Electron 本体が含まれているので Electron アプリを色々入れるとそれだけでストレージが結構圧迫されます

そんななので自分で使う機会は特になさそうです
いくつかのツールはこれに移ってインストール方法が変わるかもしれませんね
try-catch を避けるために Sync 関数を使わない
何度も書いてる try-catch の扱いづらさ
ブロックスコープな上に try が文なせいで色々面倒になる
Node.js の Sync 系関数の場合 Promise を返す版にすれば結構楽になる
Web サーバとかじゃないスクリプトで並行に処理する必要もないときは Sync にしてたけど これを考えると Sync にしないほうが楽


// Sync の場合
let stat
try {
stat = fs.statSync(dir)
if (!stat.isDirectory()) {
return
}
} catch (err) {
return
}

// Promise の場合
const stat = await fs.promises.stat(dir).catch(err => null)
if (!stat || !stat.isDirectory()) {
return
}

エラーを見るなら

const stat = await fs.promises.stat(dir).catch(err => err)
if (stat instanceof Error) {
//
}
if (!stat.isDirectory()) {
return
}
Knex が 1.0 になってた
久々にみたら 1.0 になってた
https://github.com/knex/knex/blob/master/CHANGELOG.md

0.21 だったり 0.95 だったりでずっと 1 を超えなくて 0.X 系バージョニングを採用してるものとして取り上げられてたりもしたけど とうとう 1.0
大きく変わったことを期待したけど変化はそれほどなさそう
0.95 のときのほうが大きかった気がする
そういえば forever のときも大したことなかった気がするし 0.X 系プロジェクトの 1.0 にそこまで大きな意味はないのかも

影響しそうな破壊的変更はこれ
「Changed data structure from RETURNING operation to be consistent with SELECT」

以前書いたこともある returning を使った時の返り値の仕様

select メソッドだと .select("col1", "col2") のように可変長引数
returning は第 2 引数がオプションなので列名の指定は .returning(["col1", "col2"]) のように第 1 引数で配列形式
配列じゃなくて文字列単体で .returning("col1") とも書ける
この場合は ひとつだけなので返り値の配列の中の要素がオブジェクトではなく列の値になってる

await pg("table").insert([{ value: "a" }, { value: "b" }]).returning(["id"])
// [{ id: 1 }, { id: 2 }]

await pg("table").insert([{ value: "a" }, { value: "b" }]).returning("id")
// [1, 2]

慣れないと分かりづらいところはあるけど 引数の型が違うし 一つしか無いのにオブジェクトにする必要もなく合理的と思ってた
returning に id だけを指定してから id の配列にするために

const ids = rows.map(row => row.id)

というのはよく必要になるわけでこの手間をなくせる
だけど文字列でも "*" みたいなのが来ればオブジェクトになるし 気をつけないといけないポイントみたいになってた
それが 1.0 では変更されて常にオブジェクトになるようになった

await pg("table").insert([{ value: "a" }, { value: "b" }]).returning(["id"])
// [{ id: 1 }, { id: 2 }]

await pg("table").insert([{ value: "a" }, { value: "b" }]).returning("id")
// [{ id: 1 }, { id: 2 }]

id だけ指定して id の配列を受け取るようにしてる人は割といるんじゃないかと思うし 影響の大きな変更といえるかも
mysql/mariadb はこの機能がそもそもなかったと思うから影響受けるのは PostgreSQL/Oracle/MSSQL ユーザだけのはず
Node.js の mysql ライブラリの SQL インジェクションのやつ
https://blog.flatt.tech/entry/node_mysql_sqlinjection

話題になってた Node.js の mysql ライブラリで SQL インジェクションができるというやつ
Node.js と mysql の組み合わせは考えてみると使ったことがなくて どのパッケージがいいかもわかってないくらい
いつか使うかもしれないし 危険があるなら知っておくためにも読んでみるかと開いてみました

長いから全部は読まず最初の概要だけしか読んでないけど プレースホルダーのパラメータでオブジェクトを渡すと予期せぬ結果になって SQL インジェクションできるらしい

え?これって使う人が悪いだけじゃないの?
外部から受け取ったデータをチェックもせずに DB のクエリに入れるなんてありえないし
メールアドレスや郵便番号のフォーマットに沿ってるかみたいなところまでは見なくても 最低限型のチェックくらいはするのが当たり前だと思う

Node.js やこのライブラリが良くないみたいな反応も見かけたけど 使い方が悪いだけで問題として扱うようなものでもない気がする
チュートリアルの説明ではバリデーションがないけど そのライブラリの使い方として重要なところでもないからそういうのはスキップして書かれるのは普通だし 最低限の例をそのまま正式なところで使うようなのはありえないと思う
趣味レベルで外部に公開しないようなものならともかく 世界にサービスとして公開するような人がそんなレベルだとは思いたくない

それに プレースホルダーがどんな文字列になろうと SQL インジェクションにならない気がするけど それがありえるってことは mysql 側のプリペアドステートメント機能を使わずライブラリ側でエミュレートして文字列化して送信してるのかな
そういうのは DB 側に任せたほうが安全な気がするけど

最初は特定の文字列だと正しくエスケープされないバグみたいのがあるのかなとか思ってましたが 特に気にするほどでも無いものでした
ユーザの入力は信用しないという当たり前のことをしようという話ですね
PostgreSQL の正規表現が速い
前に SQL のクエリでわざと少し遅くしようとしたことがあり 遅くなるものといえば正規表現かなと試してみましたが 想像以上に速かったです
普通の正規表現だと遅いと言ってもそんなに変わらないので 遅くなるような正規表現を探したところシンプルなのだとこういうのが見つかりました

^(\d+)*$
.+a

まずは Node.js でどれくらいかかるのか調べると

/^(\d+)*$/.test("1234567890123456789012345678a")

が 17 秒ほど
最適化されるのか 3 回目くらいから 1 秒程度になりましたけど

/.+a/.test("1".repeat(100000))

が 9.8 秒ほど
repeat で 10 万文字もありますが 「.+a」 ではなく 「a.+」 だと 1 ミリ秒未満なので 文字数が多いせいというわけではないです

これを PostgreSQL で試したのですが

postgres=# \timing
Timing is on.
postgres=# select '1234567890123456789012345678a' ~ '^(\d+)*$';
?column?
----------
f
(1 row)

Time: 2.068 ms
postgres=# select '1234567890123456789012345678a' ~ '^(\d+)*$';
?column?
----------
f
(1 row)

Time: 0.538 ms
postgres=# select '1234567890123456789012345678a' ~ '^(\d+)*$';
?column?
----------
f
(1 row)

Time: 0.675 ms
postgres=# \timing
Timing is on.
postgres=# select repeat('1', 100000) ~ '.+a';
?column?
----------
f
(1 row)

Time: 5.444 ms
postgres=# select repeat('1', 100000) ~ '.+a';
?column?
----------
f
(1 row)

Time: 5.588 ms
postgres=# select repeat('1', 100000) ~ '.+a';
?column?
----------
f
(1 row)

Time: 5.080 ms

速すぎです
数ミリ秒程度です

遅い正規表現はエンジンの実装次第なところがあるらしいですが パフォーマンスには力を入れているはずの V8 でも遅いものをこれだけの速度で実行できるというのはすごいです
ユーザ入力で遅くなりうる正規表現を実行するなら SQL として PostgreSQL 側で実行するのもありかも?
Node.js の request ライブラリが deprecated になってた
https://github.com/request/request

1 年以上前の 2020/2/11 に完全に deprecated になったみたい
request はかなり昔からある古いもので重いから普段は使わないけど 最近の軽量系だと対応してないことをしたいときには使うことがあったし 使ってるライブラリが内部的に使ってたりもして このライブラリをインストールしてるプロジェクトはいくつかある

基本は node-fetch でいいんだけど ブラウザ互換だから足りない機能もあるし そういう場合に使うのは何にするか悩むところ
代替についての Issue は作られてる
https://github.com/request/request/issues/3143

axios は React の流行り始めのころにフロントエンドで使われてたらしいけど fetch のない IE サポート用だし 最近は積極的にメンテナンスされず放置気味らしいし ブラウザでも動くものを探してるわけじゃないので除外
superagent は昔からあるけどこれ使うなら request でいいってものだったから これも違う
その他はあまり聞かないのが多いし 機能やダウンロード数などを考えると got かな
PostgreSQL の日付型は now という文字列が自動で現在時刻に変換される
現在時刻を設定したいけど アプリケーションサーバのタイムスタンプじゃなくてデータベースサーバのタイムスタンプに統一したいところ
Node.js で knex を使っていて knex だと現在時刻を設定したいところは pg.fn.now() を入れておけば良い

そのとき update するためのオブジェクトを作るところは別モジュールで pg を参照できなくて とりあえず "now" という文字列で渡しておいて update を実行するところで置換すればいいやと思ってた
その後 pg.fn.now に置き換える処理を追加するのを忘れてたけどなぜか普通に動いてる
調べてみると "now" という文字列を日付型に保存しようとすると自動で現在時刻として扱われるみたい
knex 固有の機能というわけではなくて psql で SQL を書くときでも一緒
単にキャストすればそうなる

postgres=# select 'now'::timestamp;
timestamp
----------------------------
2021-09-18 06:33:46.890335
(1 row)

すごく便利
pg.fn.now() なんて長いの書く必要なかった
Node.js の http サーバで keep-alive を無効にする
リクエストは API のみで HTML のあとに CSS とか JavaScript とかをロードすることはないサーバ
同じクライアントからのリクエスト頻度はたまにあるくらい
keep-alive のメリットが特にないし keep-alive することで前書いたようなサーバを即時止めるために少し手間が必要になる
これは keep-alive を無効でいいかと思ったけどドキュメントを見ても無効化する方法が載ってない

server.keepAliveTimeout というのがあるけどこれはタイムアウトの設定だから 0 にしてもタイムアウトしなくなるだけ
たぶんクライアントが切断するまでつないだままになる

調べると レスポンスの HTTP ヘッダで 「Connection: close」 を送ればいいというのがあったけど 直接ヘッダ操作はあまりしたくない
ソースコードを見てると shouldKeepAlive というプロパティがあって これを見てヘッダの Connection を追加してる
https://github.com/nodejs/node/blob/v14.17.6/lib/_http_outgoing.js#L436

HTTP 1.0 だと false になってて HTTP バージョンによって自動で判断されてる
このプロパティを false にすれば無効にできそう

http.createServer((req, res) => {
res.shouldKeepAlive = false
res.end("ok")
})

これで試してみるとレスポンスは 「Connection: close」 になって ちゃんと無効化できてた
データベースでタイムスタンプの一致が検索できない
RDB のクエリで WHERE にタイムスタンプ型の列を対象にするとなぜか毎回何も一致しません
レコードが存在するのは確認していて SELECT で取得したデータをそのまま WHERE に入れても一致しません

これに結構苦戦させられたのですが 単純に精度の問題でした
環境は Node.js なのでタイムスタンプ型は Date 型として取得できます
node-pg でもそれをラップした knex 等でも同じです

Date 型はミリ秒までですが PostgreSQL で timestamp と指定したときの精度はマイクロ秒まであります
なので Node.js で受け取った段階でマイクロ秒は切り捨てられて その値で検索しても一致はしないわけです
大きな数値の ID などで数値型でも有り得そうな問題ですが 受け取ったタイムスタンプが見た目上問題なさそうなので原因特定に時間がかかりました

言語の都合でデータベースの定義を変更するのはどうかなという気はするものの その Node.js のアプリ専用なデータベースですし 実際にはミリ秒以下は必要になることはないので秒までにすることで対処しました
タイムスタンプ型のオプションで精度は変更できます
また CURRENT_TIMESTAMP などを使わず Node.js 側で現在時刻を取得して保存すればマイクロ秒は常に 0 になるので定義を変更せずに対処することもできます

もしマイクロ秒まで必要となると select 時にキャストして文字列型で取得するしかなさそうです

pg("table")
.select("id", pg.raw("xxx_timestamp::text"))
Node.js が acorn に依存してた
なんとなく Node.js のソースコードを見てると deps に acorn がありました
https://github.com/nodejs/node/tree/master/deps

どこに使ってるんだろうと探すと assert と repl のようです
追加されたタイミングは 10.0 でした
https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V10.md#10.0.0
PR: https://github.com/nodejs/node/pull/15566
Commit: https://github.com/nodejs/node/commit/eeab7bc068

Node.js の REPL ってトップレベル await 対応してたんですね
せっかくなので試してみると……

C:\>node
Welcome to Node.js v14.15.4.
Type ".help" for more information.
> await 1
await 1
^^^^^

Uncaught SyntaxError: await is only valid in async function

async function の中でしか使えないというエラーです
導入されたのは結構前なのに未だにフラグが必要みたいです

C:\>node --experimental-repl-await
Welcome to Node.js v14.15.4.
Type ".help" for more information.
> await 1
1
Node.js の GUI ライブラリ
Python だと GUI を作るライブラリで tkinter とか kivy とか聞くけど Node.js って NW.js や Electron くらいしか聞かない
ブラウザ組み込み系じゃなく GUI を作るライブラリってないのかなと調べてみた

● Node-Qt
http://documentup.com/arturadib/node-qt (Github)

C++ Qt bindings
この手のライブラリはラップしてるだけだと思うので C++ で Qt 使ったことある人には向いてるかも
2014 年から更新されてない
bindings だし安定してるのか放置されてるのか

● node-gtk
(Github)

GNOME Gtk+ bindings
こっちは最終更新は 2020 年末で最近
Windows だと使えなさそう

● NodeGui
https://docs.nodegui.org/ (Github)

一番人気そうな GUI ライブラリ
内部は Qt 使ってるらしい
React や Vue にも対応してるらしいけど そういうの使うなら Electron でいい気がする
React Native は Windows 版もあった気がするし

● Yue
https://libyue.com/ (Github)

リポジトリ内に C++, Lua, Node.js がまとめて入っていてドキュメントも同じサイトにまとまってる
Qt などの移植と違って元プロジェクト自体が 3 言語サポートしてるみたい
既存のライブラリに求めてるのがなかったから自作したらしい
内部的に何使ってるかまでわかってない
base フォルダを見るに一部 Chromium のコードを流用してる?
Windows 版は GDI+ で将来的に Direct2D に置き換え予定だとか
Node.js のログイン比較
比較用に生の Node.js といくつかの Node.js フレームワークでログイン処理を作ってみたもの
https://gitlab.com/nexpr/nodejs-login

◯ Node.js
◯ koa
◯ api
◯ fastify

の 4 種類

パスが /secret/ の内側のみ 通常のルートも静的ファイルもログインが必要
外側なら誰でも見える
/ が各ページへのリンクがあるインデックスページ

ログインは id/pw を POST して cookie に保存するスタンダードなもの
ログインできるユーザ情報は user1/PW で固定

それぞれにログイン周りが少し違う 2 パターンを用意 (1.js と 2.js)
違いはログインしてない状態でログイン必要がページを開いたときの動き

1.js では /secret/login にログイン画面を用意
ここにリダイレクトされて ログインに成功したら元のアクセスしたページにリダイレクト

2.js ではリダイレクトせずアクセスしたページの URL でログイン画面が出る
ファイル開くときのフラグのわかりやすい図
https://github.com/byrro/misc-public-files/blob/master/public/ExWNT-white-bg.png

あまり使わないから r,w,a 以外忘れてる

+ があると r+,w+,a+ 全部読み書きできるから 書き込み位置と今のファイルを消すかの違い

Node.js だとこれにまだ追加があって
w,a,w+,a+ に x 付きの wx,ax,wx+,ax+ がある
x があると ファイルが存在する場合はエラー

r+,a,a+ に s 付きの rs+,as,as+ がある
rs はないみたい
s 付きは synchronous mode
Node.js の Sync とは違って IO 処理の同期モードだから Sync 系関数にしないと JavaScript の処理は完了を待たずに次の処理に進む
ディスクへの書き込みが同期処理だから通常より書き込み完了は遅くなるはず
ドキュメントでもパフォーマンスに影響するから必要ない限り推奨しないって書かれてる
Lua で使える Node.js 「Luvit」
なんかすごいプロジェクトを見つけました
Lua ですが Node.js と同じように使えます

https://luvit.io/

トップページの例のコードなんて Node.js そのままです

local http = require('http')

http.createServer(function (req, res)
local body = "Hello world\n"
res:setHeader("Content-Type", "text/plain")
res:setHeader("Content-Length", #body)
res:finish(body)
end):listen(1337, '127.0.0.1')

print('Server running at http://127.0.0.1:1337/')

ドキュメントの感じまで Node.js です
https://luvit.io/api/

内部で libuv を使っているので API が一緒なだけじゃなく 非同期処理も Node.js と同じ感じで動くはずです

また npm にあたるパッケージマネージャの lit というのも用意されています
https://github.com/luvit/lit
fs.appendFile は順番保証されなかった
書き込みを待たずに次の処理に行きたかったのでこんなのを書いたら

const fs = require("fs")

const w = text => {
fs.appendFile("./log.txt", text + "\n", () => {})
}

w("A")
w("B")
w("C")
w("D")
w("E")
w("F")
w("G")
w("H")
w("I")
w("J")

順番がバラバラだった

D
A
C
E
F
H
B
I
G
J

非同期とは言え書き込み順は保証されてると思ったのにそんなことなかった

キューに入れて書き込み順は保証するようにした

const fs = require("fs")

const w = (() => {
const q = []
let running = false
const run = async () => {
running = true
let text
while(text = q.shift()) {
await fs.promises.appendFile("./log.txt", text + "\n")
}
running = false
}
return text => {
q.push(text)
if (!running) run()
}
})()

w("A")
w("B")
w("C")
w("D")
w("E")
w("F")
w("G")
w("H")
w("I")
w("J")



以上の内容はhttps://let.blog.jp/tag/Node.jsより取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14