dnf5 速い
昔はそんなに気にならなかったのですが 最近って dnf がすごく重たく感じます
特に各コマンドの前に行われるリポジトリの更新に時間がかかってます
ダウンロードは仕方ないとして それの前後で固まってる時間があります
パッケージ数が少ない AlmaLinux だとそれほどかかりませんが fedora ではかなり待たされるときがあります

どうにかしたいと思っていたら dnf5 が使えるようです
fedora だと dnf で dnf5 というパッケージをインストールすれば使えます
今の最新の fedora は 39 ですが 38 でもインストールできました
まだ dnf5 は正式ではないようで別パッケージとしてインストールが必要という状況だからか AlmaLinux の方には dnf5 パッケージとしては提供されてないようです

少し使ってみた感じ 同じように使えました
そして高速です
体感で結構変わります
ただ出力の見た目が変わっていて 個人的には今の dnf のほうが見やすくて好きです

実際の処理時間を比較してみました

fedora38 の Docker コンテナ環境に git をインストールします

dnf install git -y
dnf5 install git -y

事前にキャッシュはそれぞれクリアしておきます

dnf clean all
dnf5 clean all

キャッシュの管理は別になってるようです

結果は

dnf
real    1m1.498s
user 0m38.245s
sys 0m2.827s

dnf5
real    0m32.621s
user 0m13.929s
sys 0m2.522s

実時間 (real) で約 2 倍の差です
これは早く dnf5 がデフォルトになって欲しいですね

一応詳細なコマンドの実行ログはここにおいてます
https://gist.github.com/61edfbf85d436a9ac941770499e96e34
VSCode でエディタを別ウィンドウで開けるようになった
以前から欲しかった機能がやっと使えるようになりました
フローティングウィンドウとか呼ばれる機能です

ブラウザみたいにタブをウィンドウの外に持っていったら独立したウィンドウで開きたいのですがそれができなかったのですよね
ワークスペースの複製みたいな特殊なことをすればできなくはなかったですが やっぱり特殊な操作が必要で手軽なものではなかったです

それが今回のアップデートからはブラウザと同じ感じでタブを移動できます
しかも 2 つのウィンドウで同じファイルを開くと 画面分割で同じファイルを開いてるときと同じようにリアルタイムに両方に反映されます
とてもいいですね

ただ実装されるまでが長すぎて画面分割で慣れてきてしまって 最近はあんまりウィンドウ分けたいなと思わなくなりつつあったりもしますけど
Nim の {}
Python でいう dict の {}
Nim でも同じかなと思って使ってみても思い通りに使えません

let value = {"foo":"bar"}

echo value["foo"]
echo value.foo

この echo はどちらもエラーです
エラーはアクセスのところなので {} の記法は構文エラーではないようです

チュートリアルなどドキュメントを検索しても基本的なところでは使われていないようで 唯一 set で使うというのがありました
ですが set は

let value = {"foo", "bar", "baz"}

という形式で少し違うものです

Dictionary で調べたほうが早いと思って探すと Nim では Table らしいです
作り方はこういうの

let table = to_table({"key": "value", "other": "things"})

to_table 関数を通すみたいです
じゃあ to_table に入れる前の {} だけは一体何なの?

検索してもそれらしいのがヒットしないので直接型を見てみました

import typetraits

let value = {"foo": 1, "bar": 2}
echo type(value)
# array[0..1, (string, int)]

タプルの配列になってるみたいです

let value2 = [("foo", 1), ("bar", 2)]

と同じということでしょうか

let value = {"foo": 1, "bar": 2}
let value2 = [("foo", 1), ("bar", 2)]
echo value == value2
# true

一致しました
この辺は Python とは違うみたいです
{} だけで Table を作成してくれたらいいのに

わかってから探すと意外と簡単にマニュアル中で見つかりました
https://nim-lang.org/docs/manual.html#statements-and-expressions-table-constructor

Table はよく使うデータ構造なので基本やチュートリアルのドキュメントで紹介しておいてほしいですね
filter は結構遅い
const create = (len) => {
return Array.from(Array(len).keys())
}

const m = create(10000)
const a = create(10000)
const b = create(10000)
const c = create(10000)
const d = create(10000)

console.time()
const result = m.map(id => {
return {
a: a.filter(x => x === id),
b: b.filter(x => x === id),
c: c.filter(x => x === id),
d: d.filter(x => x === id),
}
})
console.timeEnd()

m を基準に各要素に a, b, c, d を id 検索して一致するものを配列で保持する
これだと create ですべて [0, 1, ..., 9999] が入ってるのでフィルターの結果はすべて 1 件

毎回フィルターするのはムダそうに見えるけど
m の 1 要素あたりの処理で 1 万 x 4 = 4 万回の処理
それを m の要素 1 万回なので 4 億回

単に for ループで 4 億回ループしてカウントアップしても 800 ms くらいで 1 秒かからない

また実際には create で作られる a, b, c, d の要素には m の中には含まれない id が多数あって 0 件も多め
事前に a などを id ごとにグループ化できるけど 実際には filter のあとに変換処理の map もあって 使わない要素まで変換するのはムダになりそうという判断で m の map の中で都度フィルター
変換の方がフィルターよりも重たそうと思ってたけど実際は変換は大したことなくて フィルターでかなり遅くなってた

上のコードの実行時間は 21 秒くらい
単純な 4 億回ループの 800ms と比べるとかなり遅い

フィルターを使わずグループ化してみる
上のコードに合わせて フィルターした要素の変換はなし

const create = (len) => {
return Array.from(Array(len).keys())
}

const m = create(10000)
const a = create(10000)
const b = create(10000)
const c = create(10000)
const d = create(10000)

console.time()

const a_map = new Map()
for (const x of a) {
const arr = a_map.get(x)
if (arr) {
arr.push(x)
} else {
a_map.set(x, [x])
}
}

const b_map = new Map()
for (const x of b) {
const arr = b_map.get(x)
if (arr) {
arr.push(x)
} else {
b_map.set(x, [x])
}
}

const c_map = new Map()
for (const x of c) {
const arr = c_map.get(x)
if (arr) {
arr.push(x)
} else {
c_map.set(x, [x])
}
}

const d_map = new Map()
for (const x of d) {
const arr = d_map.get(x)
if (arr) {
arr.push(x)
} else {
d_map.set(x, [x])
}
}

const result = m.map(id => {
return {
a: a_map.get(id),
b: b_map.get(id),
c: c_map.get(id),
d: d_map.get(id),
}
})

console.timeEnd()

結果は 20ms くらいで 1000 倍くらいの高速化
単純な === が条件のフィルターってほぼ無視できるくらいに考えてたけど結構遅めだった

最近は自分でやらなくても groupBy があるので そっちにしてみるともっと短くかける

const create = (len) => {
return Array.from(Array(len).keys())
}

const m = create(10000)
const a = create(10000)
const b = create(10000)
const c = create(10000)
const d = create(10000)

console.time()

const a_group = Object.groupBy(a, x => x)
const b_group = Object.groupBy(b, x => x)
const c_group = Object.groupBy(c, x => x)
const d_group = Object.groupBy(d, x => x)

const result = m.map(id => {
return {
a: a_group[id],
b: b_group[id],
c: c_group[id],
d: d_group[id],
}
})

console.timeEnd()

プロパティの参照のみならオブジェクトが優れると聞いたのでオブジェクトにしてみたけど 速度はほぼ違いなかった
ホストのフォルダをマウントしてない Docker コンテナからデータをコピーしたい
なにかを試すときに一時的なコンテナを作ってそこで作業してることがよくあります
そこで作ったものを Windows 側で使いたいなんてことがときどきあります
Windows 側とまで行かなくてもホスト側の WSL に持ってきたいこともあります
また逆に Windows 側で用意したファイルをコンテナで使いたかったりします

それを見越して とりあえずで docker run するときにカレントディレクトリをコンテナの /mnt にマウントするようにしてるのですが 時々忘れます
そして忘れたときに限ってコピーしたくなったりするものです
コンテナを作り直してもいいのですが 色々パッケージをインストールしたり 環境の設定を変えたりしているともう一度やり直しは面倒です
その時点のコンテナをイメージ化してそこからコンテナを作るという方法もとれますが 一時的なもののためにイメージ化するのも面倒です
それならコンテナを作り直しでもいいかなと思うくらい

ただやっぱり面倒なのでいい方法がないかなと考えていてふと思いつきました
WSL で sshd サーバーを起動しよう
コンテナから WSL には通信できるので scp でコピーできます
やってみると簡単にできました

sudo apt install openssh-server
sudo service ssh start

という感じです
sshd が入ってなければ sshd のインストールとサービスの起動をします
あとはコンテナから

scp file.txt user@172.21.76.78:

みたいな感じに使います

user は WSL のユーザー名で 172.21.76.78 は WSL の IP アドレスです

WSL まで持ってきたら Windows からは \\wsl$\... のフォルダでアクセスできるので扱いやすいです



以前使ってた方法

◯ http

ウェブサーバーを起動して GET/POST で通信してました
ウェブサーバーは扱いやすいですし GET だけなら python3 がデフォルトで入ってるので http.server モジュールの起動だけで使えるなど楽でした
しかし POST で保存するとなると面倒ですし GET に揃えると送りたい側でサーバーを起動しないといけないです
また フォルダをコピーしたいときには zip 化するなど一手間が必要でした
コンテナ環境だと zip の操作コマンドも入ってなかったりしますし

◯ cifs mount

フォルダのコピーなら共有フォルダがあると便利です
ただ Linux の cifs マウントって結構面倒ですし 頻繁にセットアップ時にうまく行かなくてググってます
オプションも覚えづらくて毎回ググる必要があります

Windows と Docker コンテナ間が直接通信できたらこれでもいいのですが ネットワークが違うので Windows ←→ WSL と WSL ←→ コンテナでしか通信できないです
それなら ssh を通して scp 等のコピーのほうが手軽だと思います



書いた後で思い出しましたが そういえば docker コマンド内にも cp みたいなのがあった気がします
コンテナ内からのコマンドで実行できませんが もうひとつ WSL ウィンドウを開いてホスト側から操作する場合はこっちでもいいかもしれません
TypeScript コードを実行する
JavaScript 化されてない TypeScript のコードだけがある状態で このコードを動かしたいです

短い 1 ファイルなら公式の Playground を使うのが簡単です
https://www.typescriptlang.org/play?#code/Q

実行機能もあるので コピペして Run ボタンを押せば実行できます

複数ファイルの場合はローカルで実行することになります
公式の typescript パッケージで変換してもいいですが 実行するのが目的なら ts-node でいいと思います

npm -g i ts-node
ts-node /path/to/index.ts

npm でグローバルにインストールしています
yarn でもいいですが yarn の場合は Peer dependencies をインストールしません
ts-node は Peer dependencies に typescript や @types/node を持っています

  "peerDependencies": {
"@swc/core": ">=1.2.50",
"@swc/wasm": ">=1.2.50",
"@types/node": "*",
"typescript": ">=2.7"
},

実行にはこれらが必須なので yarn だと追加でインストールする手間がかかります
yarn dlx で一回限りで実行する場合はこういう書き方にしないといけないです

yarn dlx -p typescript -p @types/node -p ts-node ts-node a.ts

-p を使うとコマンド実行前にインストールするパッケージを複数指定できます
-p があると実行するコマンドはインストールされないので ts-node 自体も -p で記載が必要になります

実行するファイルが大きい場合 ts-node だと時間がかかるので高速化のために SWC を使うこともできます
その場合 --swc オプションを追加しますが @swc/core パッケージも追加する必要があります
swc 系パッケージは Peer dependencies ですが オプショナルとされているので npm でもデフォルトでは入りません
npm でも yarn でも手動で追加インストールが必要です

どうせインストールするなら deno のほうが扱いやすいかもしれません

curl -fsSL https://deno.land/x/install/install.sh | sh

deno run /path/to/index.ts
Developer Roadmaps
Developer Roadmaps というサイトを知りました
https://roadmap.sh/roadmaps

使われてる画像は以前どこかで見たことある気がしますが サイトとしては知らなかったと思います

フロントエンド開発者やバックエンド開発者などが使う技術がまとまってます
https://roadmap.sh/frontend
https://roadmap.sh/backend
特定機能のためのマイナーライブラリまでは無いですが 主なものは網羅的に記載されてるようです

フロントエンドみたいなまとまりだけではなく TypeScript や Python みたいな言語単位もあれば Node.js や Flutter みたいのもありますし React や Vue みたいなフレームワーク単位もあります
また AWS や Docker や SQL などもありかなり充実してます

中を見てみるとフロントエンドだと知ってるのも多いですが バックエンドだと知らないのが多いです
これらをすべて使いこなしている人たちはすごいですね
AI 補完結構良さそう
最近はコードの補完を AI がやってくれるサービスがあると聞きます
ただ有料だったりで気楽に使えなそうと使うことはなかったのですが VisualStudio 2022 で C# を書いていたら補完候補を出してくれました
for 文みたいな毎回同じパターンで書くところは手入力だと面倒なのでかなり楽です

関数名から中身を全部作るみたいのは あまり期待通りに動かなそうで良い印象はなかったですが こういう小さい範囲での補完なら扱いやすそうです
VSCode でも導入したくなりました
でも VSCode の拡張機能でこういうのに対応してるのはどれも有料なんですよね
VisualStudio みたいに標準機能として実装されると嬉しいです
プリミティブは thenable にできないみたい
オブジェクトじゃないしね

zx を使うとコマンドの実行をこう書けます

const path = "/tmp"

await $`cd ${path}`

$ は zx が提供する関数です
テンプレートリテラルのタグ関数として動作します

C# でも似たことをやってるコードを見かけました

var path = @"C:\tmp"

await $"cd {path}"

どうやってるのだろうと思いましたが C# の場合は $ は文字列中の埋め込みのために付けるもので 見た目は似てますが JavaScript とは違います
文字列の await で実行される処理を追加してるらしいです

それなら JavaScript もできるかもと思って

String.prototype.then = function() {
// なにか処理
}

await "echo 1"

のようにしてみましたが await では 文字列の then は呼び出されませんでした
thenable オブジェクトと呼ばれるだけあって オブジェクトでないとダメそうです
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 が有効な環境と無効な環境がありました
2023/11 の TC39 ミーティングでの変更点
そういえば 11 月末なので TC39 のミーティングがやってますね
まだ続いてるみたいですが いま時点で Stage が変更されたものだと

◯ Array Grouping が Stage4
◯ Promise.withResolvers が Stage4

になったようです

すでにブラウザで使えてたこともあって地味なところ

気になっていた Iterator Helpers のその後や Temporal がいつくらいに使えそうなのかや Import Attributes のその後はどうなんでしょうね
このへんは Stage だけみてもわからないので しばらくしてから各 issue の更新をみるしかなさそうです
異常なアクセス数
最近メインの方のブログで異常なアクセス数になってることがあります

many-access

サーバーはライブドアブログのものなので私にとってはどうでもいいことですが 1 人で同じページに数千回のアクセスってもう攻撃レベルですよね
自分でサーバーを管理する場合はこういうのに対応しないといけなくて大変そうなので やっぱりこういうサーバー管理は自分で行わないサービスがいいですね

にしてもここまでくれば IP をブロックしてもいいと思うのにこれだけのアクセス数が数えられてるってことはブロックせずレスポンスを返してたってことで緩めなんだなと思います
dnf install 時の GPG キーのインポートに失敗する
dnf でパッケージをインストールしようとしたらこんなエラーが出ました

Signature not supported. Hash algorithm SHA1 not available.

最近の AlmaLinux9 環境だと SHA1 を使った署名はサポートされてないようです
しかしパッケージは SHA1 なのでエラーになってインストールできません

完全なエラーはこんな感じ

Importing GPG key 0x9B1BE0B4:
Userid : "NSolid <nsolid-gpg@nodesource.com>"
Fingerprint: 6F71 F525 2828 41EE DAF8 51B4 2F59 B5F9 9B1B E0B4
From : /etc/pki/rpm-gpg/NODESOURCE-NSOLID-GPG-SIGNING-KEY-EL
Is this ok [y/N]: y
warning: Signature not supported. Hash algorithm SHA1 not available.
Key import failed (code 2). Failing package is: nodejs-2:20.10.0-1nodesource.x86_64
GPG Keys are configured as: file:///etc/pki/rpm-gpg/NODESOURCE-NSOLID-GPG-SIGNING-KEY-EL
The downloaded packages were saved in cache until the next successful transaction.
You can remove cached packages by executing 'dnf clean packages'.
Error: GPG check FAILED

とはいえインストールする必要があります
古いアルゴリズムも許可するには

update-crypto-policies --set LEGACY

を使えば良いみたいです
このあとで再度インストールしようとするとエラーがなく実行できます

もとに戻すにはこれです

update-crypto-policies --set DEFAULT

古いアルゴリズムを許可するのではなくそもそもチェック自体をスキップするという方法もあります
こっちは dnf install に 「--nogpgcheck」 オプションをつけるだけです
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 なのにプロパティを直接参照できるのでなにか方法があるのかと思ったのですが なさそうでした
組み込みモジュールだからこそ特別な対応がされているのでしょうか
React で外部から DOM 操作するとき
React 外で DOM を直接操作するとき React の更新を防がないとダメという話を以前聞いたような気がします
クラスコンポーネント時代でいう shouldComponentUpdate で false を返すみたいなことを memo を使ってやらないといけないのかなと思ったのですが 何もしなくても特に問題なかったです

実際の DOM を書き換えても React は実際の DOM は無視して前回のレンダリング時の仮想 DOM と新しい仮想 DOM を比較し差分のみを更新します
なので 再レンダリングで変化する部分以外なら直接更新すればそのままです

const Component = () => {
const ref = useRef()

useEffect(() => {
const instance = init(ref.current)
return () => {
instance.destroy()
}
}, [])

return (
<div ref={ref}></div>
)
}

React では内部で要素の参照を持っているので 要素の間に要素を追加したりしても期待通りに動きます
state で更新する要素をドキュメントから切り離しても 見えないところで要素の中身が更新されています
Skypack で壊れる
Skypack を使っていたところでエラーが起きるようになってました
エラーの内容はコンストラクタで this にアクセスする前に super() を呼び出さないといけないというものです

Must call super constructor in derived class before accessing 'this' or returning from derived constructor

Skypack 以外を通して使うと発生してないので元のソースコードには問題はないはずです

エラー箇所を見てみるとプライベートプロパティの変換によるものでした
プライベートプロパティがあると constructor の最初で初期化処理を行うように変換されていて それが this を使うのに super の呼び出しより先に行っているのでエラーでした

こんな感じ

constructor() {
_foo.set(this, void 0);
super();
}

Skypack というよりは Skypack が使ってる変換ツールの問題な気もしますが そもそもプライベートプロパティはもう ES2022 で標準化されているので変換が不要だと思います
しかし 自動で生成されるモジュールのパスを見ると es2019 の文字が入ってるので ES2019 相当で変換されてるようです

Skypack はもうメンテされてなさそうで そういう issue がいくつかできてますし 公式サイトにあるブログも 2021 年から更新されてません
他のサービスに移行したほうがいいのかもしれませんね

といってもどこがいいのでしょうか
Skypack はバンドルもしてくれるあたりがよかったのですが あまりそういうのって他で聞かない気がします

unpkg はよく使いますが 遅いですし ときどき数秒レベルで待たされます
デフォルトでは npm パッケージのままなので node_modules 用のインポートになってます
ブラウザでは解決されません
解決するには URL に ?module を付ける必要があります
解決されてもバンドルはされずにひとつひとつのファイルが個別に変換されるだけです
依存関係が多いとただでさえ遅いのがかなり遅くなって エラーで表示されないのかモジュールのロード待ちなのかわからないことも多々あります

最近は jsdelivr も npm パッケージや Github のリポジトリから直接指定できるようになったのでこっちを使ったりもしています
unpkg に比べると高速です
ESM でブラウザで解決できるようにするには URL の最後に 「/+esm」 をつけます
これをつけるとバンドルもされます
Rollup + Terser で変換してるとコメントに記載されてます
ただ問題があって コードが重複します
オプションで本体に同梱されてないプラグイン系のモジュールをロードすると プラグインごとに共通部分のコードが含まれます
ちゃんと動作確認までしてないですが これがあると別モジュールとして扱われたりしてうまく動かないケースがあるのであまり使いたくないです
Skypack ではバンドルはしてるのですが 適度に分割はされていて確認した限りはこの問題がなかったです
ちゃんとパッケージ全体を見た上で公開されているエントリポイントを基準に分割してるのでしょうか

こういうのがあるので 遅いのを承知で unpkg を module で使うか jsdelivr を変換なしにして importmap で使うかが多いです
importmap が自動で作られるといいのですが 自力でやると依存パッケージが多いと手に負えなくなるんですよね
node_modules フォルダ内の (フォルダ数 x 2) を手作業で記載するような形になりますし

最近は esm.sh を見かけることが増えているのでこれを試してみると いい感じに動きました
Skypack と近い感じです
バンドルされますが コードが重複しないようになってるようです
また ネストされた import で順番にファイルを取得すると遅くなるので エントリポイント部分でフラットに import を展開して並列でロードできるようにするなど高速化の工夫もされています
良さそうに思うのですが 新しいものに飛びついた結果が Skypack ですし もうしばらくは様子見したいところです
debounce と throttle
頻繁にイベントが起きるもので 実行回数を抑えたいときがあります
数行書けばできるので自分でタイマー制御してることがほとんどです
ただ 使うライブラリによってはそういう機能をユーティリティ関数として提供してる場合もあります
これのためにわざわざライブラリをインストールしようとは思わないですが すでに使ってるライブラリに機能があるならそれを使おうと思います

機能名は debounce と throttle という名前になってるのをよく見ます
lodash から有名になったと思いますが 元になった underscore.js でも実装されていたものです
実装の経緯を見ると underscore.js が最初ではなくすでに jQuery プラグインなどで存在した機能のようです
https://github.com/jashkenas/underscore/issues/66

2 種類あるのは知ってるのですが 普段使いしてないとどっちがどういう動きするのかわからなくなります

debounce
これは関数の実行を指定時間分遅延します
1 回呼び出すだけなら setTimeout と同じです
指定の時間後に 1 度実行されます

複数回呼び出す場合 遅延している間にもう一度呼び出すとタイマーをクリアして再セットします
遅延時間内に関数を呼び出し続けると永遠に実行されません
また必ず最後の呼び出しから遅延時間分は遅れて実行されます
実装によってはオプションで 連続で呼び出し続けても何秒に 1 回は実行されるようにする最大遅延時間が指定できたりします
また タイマーをキャンセルしたり即時実行したりできるものもあります

throttle
こっちは指定時間の間は実行されないようにするものです
1 回目の呼び出しでは即自に実行されます
それから指定の時間の間は呼び出しても実行されず 前回の実行から指定時間の経過後に実行されます
なので呼び出し続けても指定時間に 1 回は実行されます
また 次の実行は前回の実行から指定時間後なので 指定時間の経過直前で呼び出すと 遅延はするものの呼び出してからすぐに実行される場合もあります


lodash の実装の場合は debounce と throttle の実装は共通になっていて デフォルトのオプションが異なるだけです
オプションで 実行しない期間に入るタイミング (leading) と実行しない期間の終わりのタイミング (trailing) で関数を実行するかを選べるようになっています
両方とも実行しない場合は一切関数が実行されない挙動になります
debounce のデフォルトは trailing のみの実行です
throttle のデフォルトは leading と trailing の両方かつ 最大遅延時間を遅延時間と同じにしています

連続で呼び出し続けてる間は全く実行されなくていい場合は debounce
連続で呼び出し続けても適度な間隔で実行されて欲しいなら throttle
という使い分けでいいと思います



そういえば以前 input 要素が idle イベントと timeout イベントを起こすようにしました
🔗 input で idle イベントを起こす

入力が終わって指定時間(idle)後に idle イベントが発生します
入力を続けている間は起きません
入力をずっと続けている間に 指定時間(timeout)が経過すると timeout イベントが発生します

debounce は idle イベントにリスナを付けるのと同じ感じです
throttle は timeout イベントにリスナを付けるのと同じ感じです
ただ timeout なので入力開始時にはイベントが起きなくてそこだけは違います
word-break と overflow-wrap
テキストの折り返しをしたいときの CSS についてです

まずは普通にこんな HTML を書きます

<!DOCTYPE html>
<style>
div {
width: 160px;
background: powderblue;
font-family: monospace;
}
</style>
<div>
aaaaa bbbbb ccccc ddddd eeeee fffff
</div>

幅が 160px しかないので 入り切らない部分はスペースの位置で改行されてこうなります

aaaaa bbbbb ccccc
ddddd eeeee fffff

スペースがない場合は

<div>
aaaaabbbbbcccccdddddeeeeefffff
</div>

折り返しされず 親要素のエリアをはみ出してすべてが 1 行で表示されます

aaaaabbbbbcccccdddddeeeeefffff

これの対処のために使われるものの一つが word-break です

word-break: break-all;

これをつけると結果はこうなります

aaaaabbbbbcccccdddddee
eeefffff

入り切らなくなるところで折り返ししてくれます
しかし これを使うと最初のようなスペースがある場合に問題が起きます
スペースで折り返されず ギリギリまで詰め込んで無理やり改行することになります

aaaaa bbbbb ccccc dddd
d eeeee fffff

コンソールみたいなところだとこれでいいのですが 通常の文章ではあまり見た目が良くないです
これを期待どおりにするのが overflow-wrap です

overflow-wrap: break-word;

これを設定すると 基本は通常通りスペースで折り返してくれて スペースがないような場合は入り切らなくなるところで折り返ししてくれます

aaaaa bbbbb ccccc
ddddd eeeee fffff
aaaaabbbbbcccccdddddee
eeefffff

なので基本は overflow-wrap を使うで良いと思います

また 特殊なケースで 記号のみの場合は word-break で折り返しされないというのがあります

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

みたいなものは word-break だと折り返されずはみ出します
overflow-wrap だと折り返しができます

記号と言っても折り返されるものとそうでないものがあります

確認用ページ
https://nexpr.gitlab.io/public-pages/word-break-chars/example.html

クエリパラメータで追加の文字を指定できるようにしてます
?chars=abc のように書くと a と b と c が追加されます
Google にインデックス登録されてない
ググって出てくるページと出てこないページがあるので Search Console で見てみるとインデックスに登録されてないページもありました
1 ヶ月以上前の記事でも登録されてないのがあったりで どこかからリンクされない限り登録されなそうです

一応ライブドアブログには ping 機能があって Google などには通知されてるはず と思ったのですが Google は ping のサポートを終了したという情報も出てきます
https://developers.google.com/search/blog/2023/06/sitemaps-lastmod-ping?hl=ja

エンドポイントとしては 6 月から 6 ヶ月後なので今年中は動いてそうですが あくまでエンドポイントが生きてるだけでレスポンスはエラーにならず返ってくるもののすでに意味はないという状態なんでしょうか

ただ これはサイトマップの ping であり 個別記事のものではない気がします
ライブドアブログが送信先として設定しているのはこれです
http://blogsearch.google.com/ping/RPC2

ググるとこれはもう何年も前に終了してるという情報も見かけます
どっちにしても ping 機能は意味ないと考えて良さそうです

そうなると Google のクロールに任せるしかないのですが その結果登録されてないのですよね
ブログですし トップページから新着記事へのリンクはあるのですけど
調べていると 小さいサイト (ページ数が 500 以下) はサイトマップが不要だけどページ数が多いならサイトマップを登録したほうが良さそうという情報もあります
https://developers.google.com/search/docs/crawling-indexing/sitemaps/overview?hl=ja

このブログですら記事ページは 1000 近くありますし あったほうがいいのかもしれません

サイトマップは Atom や RSS で良いみたいです
ライブドアブログだと自動で作られていて atom.xml や index.rdf にアクセスすると見れます
でもこれらはトップページの HTML 内で参照されていますし自動認識されてるようにも思いますけど

<link rel="alternate" type="application/rss+xml" title="RSS" href="https://let.blog.jp/index.rdf" />
<link rel="alternate" type="application/atom+xml" title="Atom" href="https://let.blog.jp/atom.xml" />

ただ これは PC 版のみで モバイル版の HTML には含まれてませんでした
モバイルファーストでクロールしてるのなら Google はこれらの URL を知らないという可能性もあります
とりあえず Search Console からサイトマップとして登録してみました

atom.xml の方は Google が期待するフォーマットと一致しないみたいでエラーになりました
index.rdf だと問題なかったのでライブドアブログでサイトマップ登録するならこっちを使うと良さそうです
イベントリスナに設定した関数を簡単に置き換えたい
elem.addEventListener("event", (event) => {
//
})

で登録した関数を置き換えたいことがあります
事前に処理内容がわかってるなら関数はそのままで中の if 文で分岐でもいいのですが そうでない場合は関数ごと置き換えたいです
しかしイベントリスナって付け外しが少し面倒です

楽にしたいなと思って考えると リスナとして設定した関数は外部の関数を呼び出すだけにして 外部の関数を置き換えるという方法が考えられます

let fn = () => {}
elem.addEventListener("event", (event) => {
fn(event)
})

const changeHandler = (new_fn) => {
fn = new_fn
}

こうすると登録した関数はそのままで fn を置き換えればいいです
ローカル変数なのでそれを更新する関数 changeHandler を作って それを外部に公開することでリスナの関数を置き換えできます

もう少し扱いやすくしたいので オブジェクトにして関数を作る部分も共通処理としてみます

const createHandler = (ref) => (event) => ref.handler(event) 

const ref = { handler: () => {} }
elem.addEventListener("event", createHandler(ref))

これで ref オブジェクトを公開するか ref.handler を更新する関数を公開すれば良いです

しかし 実はこれとほぼ同じ機能が標準で用意されています
addEventListener にハンドラーとして渡すものは関数がほとんどですがオブジェクトを渡すことができます
その場合は handleEvent というプロパティの関数が実行されます
なので

const handler = { handleEvent: () => {} }
elem.addEventListener("event", handler)

とだけ書けば 上のと同じことができます
handler オブジェクトを公開したり handler.handleEvent を更新する関数を公開すれば良いです

あまり知られてないマイナー機能ですね
ただのプロパティの書き換えで済んで removeEventListener して addEventListener するよりも高速なので 再レンダリングするようなライブラリの内部処理で使われていたりします
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 外でも使われるような方法なのに 何が気に入らないのでしょうね
AI が変な方向ですごい
やりたいことを実現する良い方法が思いつかなくてググってみてもこれといったのがみつからないです
なんとなく AI にでも聞いてみるかと思って聞いてみました

回答は想像以上に求めているものが出てきました
そんなのがあったんだ!と驚いてその名前でググったのですがそれっぽいのは出てきません
ドキュメントの URL を出してって言ったら公式サイトらしい URL や Github 上のファイルの URL を出してくれました
ただ アクセスしても 404 エラー

実在しない機能みたいです
名前や機能はいかにもそれっぽくてありそうだったのに

あと URL はとてもそれらしい名前でした
そのサイトの命名規則に従っていて パッと見では正規のものにしかみえなかったですし
Github だとリポジトリ名やブランチ名は実在のもので パスもいかにもな感じでした

やっぱり正確性が足りないと 知らないことを聞くのには向いてないですね
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 などを追加する方法は取れなくもないですが ソースコードを暗黙的に書き換えるようなことはあまりしたくないですし 諦めてバックポートを待とうと思います(需要的に安定すればきっとされるはず)
C# 12 のコレクション式記法が良さそう
.NET 8 が出ましたね
合わせて C# 12 が使えるようになったようです
新記法が追加されたようで 少しスクリプト言語に近づきました

int[] nums = [1, 2];

という感じで [] のリテラルで配列を作れます
JavaScript や PHP や Python などの言語に近い感じで書けます

配列以外のリストやセットなどでもいいです

List<string> strs = ["foo", "bar"];
HashSet<int> hs = [1, 2];

ネストもできます

int[][] n = [[1, 2], [2, 3]];
List<List<List<int>>> ll = [[[1], [2]], [[3], [4]]];

展開もできます

int[] ii = [.. i, .. i];

この記法は JavaScript や PHP と似てるようで違って紛らわしいです
... じゃなくて .. ですし フォーマットすると変数名の間にスペースができます
Range のリテラルに使う .. を使いまわしてるみたいです

var r = 1..10;

展開自体はこれまでの new [] { 1, 2 } の記法のときからできたのかと思いましたが できないようでした
今でもエラーになります

var error = new[] { ..i };

色々できて便利ですが Dictionary は対応していないようです

Dictionary<string, bool> flags = [["foo", false], ["bar", true]];

これはエラーでした

KeyValuePair を入れる方法も試しましたがダメでした

var kvp = new KeyValuePair<string, bool>("foo", false);
Dictionary<string, bool> flags = [kvp];

'value' の必要なパラメーター 'Dictionary<string, bool>.Add(string, bool)' に対応する特定の引数がありません
って言われます
Dictionary の場合は Add で key と value の 2 つの引数を取る形になりますが この記法で書くと KeyValuePair の 1 つを渡すようになってしまって引数が足りてないとみなされているようです

なので Dictionary を使う場合は まだこれまでの記法で書く必要があります

var flags = new Dictionary<string,bool> { ["foo"] = false, ["bar"] = true };

一見便利な新機能ですが 個人的に不満もあって var が使えません
左辺に型を明示的に書く必要があります
右辺だけだと List なのか配列なのかわからないので仕方ないのですが C 系の 「型を文の最初に書く」 記法は読みづらいので好きになれません
変数宣言だとわかるように 文は var など固定の文字列で始めたいです
int 等のシンプルな型ならまだいいですが 長めの型になるとこれが変数宣言だとわかるまで少し時間がかかって読みづらいです

一応 右辺でキャストすれば左辺は var でも通るのですが 無理矢理感もあってこれでいいのか不安な感じもします
これまでの記法とあまり変わらないですし

var nums = (int[])[1, 2];
var ll = (List<List<List<int>>>)[[[1], [2]], [[3], [4]]];
py ランチャーでバージョン一覧表示
py -0

-0 数字の 0
-0p にすると場所が表示されます

Windows で Python を入れるとついてくる py ランチャー
複数の場所やインストール方法で入れた Python を起動できるので便利です

しばらく使ってなかった環境で どのバージョンが入ってるのか一覧を見ようとしたのですが ヘルプにはそれらしい機能がありません
ランチャーなら見れそうなのに

ぐぐってみると普通にコマンドがあるようです
しかし 認識されないコマンドのようでエラーでした
原因は単純にバージョンが古かったみたいです

Python を新しく入れても py ランチャーは更新されないみたいです
カスタムインストールにしても py ランチャーの項目は灰色になってインストールできなくなってました
管理者権限が必要みたいなことが書いてるので管理者権限でインストーラーを起動してみましたが同じでした
一旦手動でアンインストールが必要みたいです

アンインストール後に再度 Python をインストールすると 最新の py ランチャーが入りました
ヘルプに -0 が出ていますし -0 でインストール済みバージョンの一覧が見れます
systemd の StandardOutput と StandardError
以前の記事で書いたように systemd の機能で標準出力をファイルに追記するようにしてロガーはシンプルに標準出力に書き込むようにしています
console.log でいい感じに整形してくれるので楽なんですよね
depth 問題があって省略されて必要な部分が残らないこともありますけど

ひさびさにその設定を見ていたら ふと思ったのですが標準出力しか設定していないです
StandardOutput は指定しているのに StandardError は未指定です

自分が書き込むのはすべて標準出力に統一してるのですがライブラリや Node.js など実行する環境側で出力されるものは標準エラー出力になることもありそうです

そういう特殊なものはむしろ systemd 標準の journal に送ったほうがいいのかもと思うところもあったりしますが 時系列を考えると同じログファイルに混ざっていて欲しいところもあります

とりあえず同じファイルに出力しようとしたのですが シェルの 「2>&1」 相当の記法がわかりません
シェルを通して本来のプログラムを実行して 標準エラー出力をすべて標準出力にまとめるようにすることもできますが あまりスッキリしないのでできれば避けたいです

ドキュメントを見てると fd:stdout のような記法があるみたいなので 「StandardError=fd:stdout」 でいいのかなと思ったのですが 構文エラーのようで起動ができなくなりました

もう少し読んでみると StandardError では inherit を使えば 標準出力のファイルディスクリプタが複製されると書いています
https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#StandardError=

また StandardError のデフォルト値 DefaultStandardError は inherit になっています
ということで何も設定してないこの状態で期待通りの動きになっていました

StandardOutput=file:/tmp/output

とだけ書けば標準出力も標準エラー出力も /tmp/output に書き込まれます
一応動かしてみたところ期待通りに動作しました



ちなみに fd を試していたときに同名指定も試しました

StandardOutput=file:/tmp/output
StandardError=file:/tmp/output

append なら問題なさそうでも通常の書き込みだと問題が出そうに思います
例えばこういうの

node index.js > out 2> out

[index.js]
console.log("out1")
console.error("err1")
console.log("out2")
console.error("err2")
console.log("out3")
console.error("err3")

[out]
err1
err2
err3

append モードじゃないとファイルの書き込み位置が更新されないので同じ場所に書き込んで上書きされます

同じ文字数だと分かりづらいですが こうするとそれぞれが持ってる書き込み位置に書き込んでることがわかりやすいと思います

[index.js]
console.error("AB")
console.log("a")
console.log("bcdefghijk")
console.error("CDEFG")
a
bCDEFG
ijk

自動で追加される改行があるので少しわかりづらさはありますけど AB が a と改行で上書きされて cd.. は CD.. で上書きされます
G の次の改行で h が上書きされてそれ以降は上書きされないので ijk が残ります

同じ名前を指定すると こうなるんだろうなと思ったのですが StandardOutput と StandardError に同じものを指定してもこうはならず両方が出力されていました
中でうまく管理されているようですね
ただ基本は同じのを 2 回書かずに inherit でいいと思います
Prettier 3.1.0 で条件演算子のフォーマットが戻ったけどタブに対応してない
Prettier 3.1.0 で条件演算子が昔の動きに戻ったと聞いたり 新モードも追加されたと聞いて 試してました
ただ今回の記事の内容は新モードではなく通常モードについてです

Prettier の更新で条件演算子がフラットになる問題が起きて以降 Prettier のバージョンを固定して上げないようにしているプロジェクトがありました
この数年は触れることもなかったので 別フォーマッターに移行せず Prettier のままです
とりあえずこれを通常モードの 3.1.0 にしてみました

差分なしになるのが期待の動作だったのですが 結果は謎のスペースが入るというものでした

[元]
const value = aaaaaaaaaaaaaaaaaaaaaaa
? bbbbbbbbbbbbbbbbbbbbbb
: cccccccccccccccccccccc
? dddddddddddddddddddd
: eeeeeeeeeeeeeeeeeeee

[フォーマット後]
const value = aaaaaaaaaaaaaaaaaaaaaaa
? bbbbbbbbbbbbbbbbbbbbbb
: cccccccccccccccccccccc
? dddddddddddddddddddd
: eeeeeeeeeeeeeeeeeeee

インデントにはタブを使っています
元のコードは b, c の行は 1 つのタブで d, e の行は 2 つのタブです
このままが期待するものです

しかしフォーマット後は d, e の行は 1 つのタブとそれに続く 2 つのスペースです
一応 タブサイズは 4 にはしているのに 2 つのスペースのインデントが追加されました

以前 インデントをフラットにした理由の一つにネストが深くなるというのがあるので 浅くしたいというのはわからなくもないですが タブを使ってるときはタブに揃えてもらいたいです
ただ すでに似たような問題としてこういうのがあります

[元]
const value = aaaaaaaaaaaaaaaaaaaaaaa
? {
a: 1,
}
: {
a: 2,
}

[フォーマット後]
const value = aaaaaaaaaaaaaaaaaaaaaaa
? {
a: 1,
}
: {
a: 2,
}

これも元のコードが期待するものです
しかしフォーマットすると 「{」 と 「}」 の縦の位置をそろえようとしてスペースが入ります
タブを使用する設定なので 「}」 の前には 1 つのタブと 2 つのスペースです

タブのインデントは見た目を揃える以上に論理的な構造を視覚化するためのものだと思うのですが Prettier 開発者は考えが違うみたいなので仕方ないですね

……と諦めてましたが issues を探してみるとすでに存在して バグ とラベル付けされています
https://github.com/prettier/prettier/issues/15655

修正されるのでしょうか?
今回の変更は 「前の動きに戻す」 というものだったはずなのでそういう意味では意図したものではなさそうですし修正されるのかもしれません
とはいえ 2 つめの例のような問題が残るのなら もうどっちでもいいのですけどね

でもこれをバグとするなら タブを使うモードでテストしてないというわけで やっぱり Prettier 開発者はタブを重視してなさそうです
デフォルトをタブにするという issue にコメントが 450 以上ついてますが これも実現するのか怪しいところがありますね
https://github.com/prettier/prettier/issues/7475
ビルド時のハッシュ値の意味があまりなかった
フロント側でビルドするときに各モジュールのファイル名にハッシュ値がつきます
ファイルの中身が一緒なら同じファイル名で 違えば別のファイル名になります
変更がなければ以前のバージョンのキャッシュを使えるので 効率が良くなるのですが 思ってたほどじゃなかったです
というのも ほぼ毎回ハッシュ値が変わります

まず index.js があります
これがエントリポイントで ここから各モジュールをロードします
どこかのモジュールが変わればそれをインポートするときのファイル名が変わるので index.js は毎回変化し 新しいハッシュ値になります

index.js には静的にインポートされるファイルが全て含まれ 動的にインポートされるものが別ファイルという分割のされ方になります
動的にインポートされるモジュールが使うモジュールは index.js に含まれてるケースが多いので 各モジュールは index.js をインポートすることになります
ただ index.js は上で書いたように 毎回のようにハッシュ値が変わります
その index.js の名前をコード上に含むわけなので 内容を変更してないモジュールでもビルドの出力ファイルとしては変更があり モジュールのハッシュ値も変わります

結果としてほぼすべてのモジュールのハッシュ値が毎回変わってしまいます
動的にインポートするモジュールが index.js をインポートしなくていいように分割してくれると更新は減るのですが 実行時にロードするモジュール数が増えることになるので それも良いとは言えないです

ただ現状だと ファイルごとにハッシュ値つけずにビルド結果を配置するフォルダにタイムスタンプをつけるのと大差ないようなものになってるので 気持ち悪さが残ります
このブログの記事の開閉ボタンを使った人数
このブログのトップページやカテゴリページなど 記事が一覧表示されているところでは記事の開閉が行なえます
記事の右端でスクロールバーがありそうなエリアが開閉ボタンになっています
閉じてるときは青色の場所です
開くと黄色になります

個別ページに飛ばなくても一覧ページで中身も見れて便利機能ではあるのですが これって使ってる人どれくらいいるのでしょうか
気になったので調査してみました

直近の 1 ヶ月をみたところ

6 人

でした

かなり少ないですね
ここまでだとむしろなぜその 6 人は開いたの?って疑問に感じるくらいです

ただ考えてみるとこっちのサブブログはそもそもの訪問者数が少ないです
直近 1 週間のユニークユーザーから平均を求めると だいたい 70人/日 でした
この数字には bot 等も含まれます

リファラを見ると 9 割以上は Google 検索です
ググって出てきたページを見るときって軽い気持ちでいくつか開いてみて ほとんどは期待と違っていて閉じます
それを考えたら 中身が期待と一致していて さらにそれで終わらずブログのトップページに移動して 他の記事まで読もうと思って 横のボタンに気づく人なんてかなり少ないはずです
そう考えると 6 人は多い方なのかもしれません

ところで人数は日でリセットされてるかわからないので もしかすると別の日にアクセスしてるだけで実際は 1 人という可能性もあります
関数の配置場所とスコープ
コードを読むときにスコープが広いと頭の中で読み取るときに大変ですよね
なので極力スコープは小さくしたいのですが 関数内だけ使う関数ってどこにあるのが良いのでしょうか?

const fn1 = () => {
const fn2 = () => {

}

fn2()
}

const fn3 = () => {

}

こんな感じのもので fn2 は fn1 の中でしか使いません
fn1 でしか使わないので fn1 の中 つまり今の場所でいいように思います

しかし fn1 のコードが長くなり fn1 の中に fn2 相当の関数が 5 や 10 になってくると fn1 関数がとても長くなり見づらくなります
関数内関数を無視して fn1 だけを見ると数十行程度なのに fn1 全体としては 数百行とかいうケースもありえます

また fn1 内のローカル変数がいくつもあると fn2 などの関数はそれらすべてを見ることができます
fn2 は外側を一切見ない関数だとしてもスコープ的には見れるので 読むときにはそれらを参照するかもしれないとして読む必要があります

そういうことを考えると

const fn1 = () => {
fn2()
}

const fn2 = () => {

}

const fn3 = () => {

}

でもいいのではと思うのですよね
fn2 は fn1 内のローカル変数を見ることができません
見れるのはグローバルやモジュール内の変数のみです
fn1 のローカル変数とは切り離されているので 読みやすくなります
また fn1 の中は fn1 で直接行う処理のみなので fn1 の行数も減ってスッキリします

しかし fn2 がモジュールのトップレベルに出てくることで fn3 が fn2 を参照できるようになってしまいます
今度は fn3 が fn2 を使うかもという部分を考えないといけません

いずれも全体として短いこれくらいだとどっちでもいいレベルですが 長くなってくると読みづらくなるんですよね
ただ fn3 が fn2 を見れても関数呼び出しだけです
それに対して fn2 が fn1 のローカル変数を見れるのは値の書き換えができるので複雑度が上がります
なのでどっちかというと関数内関数を減らしたほうがいいのかなと思ったりはするものの 本当にベストなのか疑問が残ります

別の手段としてモジュールを分けてしまうのがいいのかとも思ったりはしてますが モジュール数が結構増えそうで 結局試してません
分けるとしたらこういう感じです

// sub.js
export const fn1 = () => {
fn2()
}

const fn2 = () => {

}

// main.js
import { fn1 } from "./module1.js"

const fn3 = () => {

}

fn1 が読みづらいくらい長くなるなら fn1 だけを別モジュールに切り出して fn1 と fn2 をトップレベルに置きます
エクスポートは fn1 だけにして fn3 からは fn2 を参照できなくします

ただこれも トップレベルじゃないとできないです
親スコープを参照したいから関数内関数にしてるところがあって その中で今回みたいなことをしたい場合にはモジュールに分けるということはできないです
関数で state を更新するときに更新後の値をその場で取得したい
state が更新されたら別の state も更新したいとき useEffect を使わず最初の state を更新するところで関連する state も更新したいです

const onClick = (event) => {
setState1(event.target.value)
}

useEffect(() => {
setState2(state1 + 1)
}, [state1])

にせず

const onClick = (event) => {
setState1(event.target.value)
setState2(event.target.value + 1)
}

(この例だと +1 するだけなので state にする必要ないし 処理が重たくても state1 から計算できるなら useMemo でいいけど これは説明を簡単にするためのものなのでそこは気にしないでください)

しかし 更新方法が setState の関数を使うタイプの場合に困りました

const onClick = useCallback((event) => {
setState1(prev => prev + 1)
// state1 の更新後の値がここではわからないので state2 を更新できない
}, [])

useEffect(() => {
// 仕方ないので再レンダリング時の useEffect で対処
setState2(state1 + 1)
}, [state1])

setState の関数の中で別の setState を呼び出すこともできますが なんか気持ち悪さがありますし それってどうなのと思います
そんな使い方を見た覚えもないですし 推奨される方法ではない気がします

完全に state1 と state2 の更新タイミングが揃うものならオブジェクトにまとめて 1 つの state にしてもいいのですが そうとも限らないです
それに state2 の更新としてるところが localStorage への書き込みだったり React の state とは関係ない処理の場合もありますし
useEffect が 2 回呼び出されて問題になるケース
最近は新しく環境を作るときに React 18 なので useEffect が 2 回呼び出される問題の影響が増えてきました
多くの場合はちゃんとクリーンアップ処理を書けば大丈夫なのですが そうもいかないケースがあります
例えば マウント時に state にデータを追加するような処理がある場合 2 回呼び出されるので 最初から 2 つの要素が存在することになります

const add = () => {
setState(state => [...state, { at: new Date(), value: Math.random() }])
}

useEffect(() => {
add()
const timer = setInterval(add, 1000 * 60)
return () => clearInterval(timer)
}, [])

あまり問題にはならないですが エラーがないと 1 つだけみたいなケースで 開発中は常に 2 つあることになって 実際のものと見た目が異なるので気持ち悪かったりします
また キーになる情報がタイムスタンプくらいだと useEffect が 2 回呼び出された場合はキーが同じになってしまうことがあります
開発時のみの都合で別のキーを自動生成したり index をキーにするのはあまり良い方法とも思えません

とりあえず最初の実行かどうかを ref を使って判断するのですが 追加で ref が増えますし 開発時の都合で特別なことをするのはあまり気持ちの良いものではないです

const ref = useRef(true)

useEffect(() => {
const is_first = ref.current
ref.current = false

if (is_first) {
add()
}
const timer = setInterval(add, 1000 * 60)
return () => clearInterval(timer)
}, [])

クリーンアップ関数を使ってできないかと思ってやってみたのはこれです

useEffect(() => {
const timer1 = setTimeout(add, 1)
const timer2 = setInterval(add, 1000 * 60)
return () => {
clearTimeout(timer1)
clearInterval(timer2)
}
}, [])

一瞬だけ遅延させます
そうすることで 2 回目の useEffect 呼び出しで 1 回目の処理をキャンセルできます
ref が不要で useEffect の中だけで完結するのが良いです
ただし僅かな遅延があるので 見た目上 ちらつき等になる可能性もあります

また 正常な動作として高速で 2 回呼び出された場合にキャンセルが発生します
それも気にするなら ref に頼ることになりそうです

最近は React で新しいものを作るたびこういう不満点を感じて別のフレームワークを探しては 不足点があってまだ使えないなぁを繰り返してます
Yarn PnP を使うと JavaScript でライブラリの補完が出なくなる
Yarn4 から PnP を使おうとしています
速度面では問題もあるものの WSL やネットワーク越しの環境でサーバーを起動するようなケースはむしろ速くなってあまりデメリットもなさそうです
フロント関係でも Vite は Yarn PnP をサポートしてるので 開発用サーバーを起動できますし HMR も使えています
これなら PnP を普段遣いにしてもいいかも と思ってました

しかし VSCode との相性に問題がありました
VSCode は Yarn PnP でインストールされたパッケージを認識しないので 補完機能が動かなくなります
使い慣れてるライブラリだと特に問題を感じてなかったのですが 使い慣れないものや機能が多いライブラリを使ったときに補完が出ないのが結構不便でした
VSCode では JavaScript でもライブラリの関数の引数などは情報を見ることができるようになっています
選択式のところは補完の候補から選んだりもできます
これがないと毎回ライブラリの API のページを見ないといけないのが不便です

対応することはできるのですが 補完機能には TypeScript の機能が使われているので 実際には TypeScript を使わないのにライブラリの補完のためだけに TypeScript をプロジェクトにインストールしないといけないです

やり方はこんな感じです (参考)

VSCode の拡張機能の ZipFS をインストールします
これは Yarn のチームでメンテしてるもののようです
汎用的なものではなく Yarn PnP のためだけのものみたいですね

次にプロジェクトに TypeScript をインストールします

yarn add -D typescript

SDK をインストールします

yarn dlx @yarnpkg/sdks vscode

これを実行すると今のプロジェクトで使われてるツールを自動で認識して必要なものをインストールしてくれるようです
TypeScript 以外にも eslint が使われていたら eslint の SDK がインストールされます

あとは VSCode のコマンドから TypeScript のバージョンを切り替えます

Ctrl-Shift-P
→ TypeScript: Select TypeScript Version...
→ Use Workspace Version

VSCode 組み込みのものからワークスペースのものに切り替えると TypeScript の機能が有効になります
TypeScript をインストールしていないとワークスペースのバージョンが存在しないので切り替えができません

これでライブラリの補完ができるようになりました

ちなみに SDK のインストールですが TypeScript のみの環境ではこうなりました
.vscode フォルダに extensions.json と settings.json が追加されました
extensions.json では推奨される拡張機能に ZipFS が記載されて settings.json では↓のような内容が追加されています

{
"search.exclude": {
"**/.yarn": true,
"**/.pnp.*": true
},
"typescript.tsdk": ".yarn/sdks/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}

ここのパスにも記載がある .yarn/sdks/typescript に TypeScript の SDK がインストールされています

ライブラリの API の補完機能が欲しいだけなのにすごく面倒ですよね
TypeScript を使うプロジェクトなら最初からインストールしてるわけなので別にいいかとも思えますが シンプルな JavaScript のプロジェクトでやりたいものではないです
ストア版 WSL が 2.0 になった
https://github.com/microsoft/WSL/releases/tag/2.0.9

9 月にプレリリースという形でリリースされていた 2.0 が正式リリースされました
2.0.9 です

メモリやストレージを自動で解放してくれたり 外部端末から IP 指定でアクセスしても WSL にアクセスできない問題を解決したり色々新機能がありますね
新機能がまとめられてる記事↓
https://www.publickey1.jp/blog/23/windows_subsystem_for_linuxwsllanwsl.html

特にネットワーク周りの問題は面倒が多かったですからね
ただ Docker 周りでまだ問題があるとかいう話も聞きます
仮想マシンという形で動作してる以上 仕方ない部分もありそうですね

ただ せっかく仮想マシンで分かれてるんだし ブリッジ接続にしてくれたほうが便利だと思ったりもします
複数の WSL ディストリビューションや WSL 内の Docker/Podman コンテナで同時にウェブサーバーを起動してることってけっこうあります
全部をホストの Windows で動作してるようにみせかけるとポートの競合が起きて面倒です
全部 3000 番に揃えることができず それぞれのポートを別にしないといけないです
その点ブリッジだとアクセスする IP アドレスが違うのでポートの競合は気にしなくていいです

WSL 内の Docker を Windows と同じ階層にあるように見せて↓みたいなことができるかは知らないですけど

Windows        192.168.10.11
WSL(1) 192.168.10.21
Docker(1) 192.168.10.22
Docker(2) 192.168.10.23
WSL(2) 192.168.10.31
Docker(1) 192.168.10.32
Docker(2) 192.168.10.33
WebComponents でコンテキスト的なことをしたい
WebComponents で作ったコンポーネントで現在のコンテキストを受け取りたいことがあります
コンテキストというのは React のコンテキストみたいなもので 親で持っているデータのことです

WebComponents は DOM なので親をたどるのが簡単です
connectedCallback で接続されたときに

const shadow_root = this.getRootNode()
const host = shadow_root.host

のようにすれば ShadowRoot や その ShadowRoot を保持するホスト要素にアクセスできます
これを繰り返し期待のコンテキストを保持する要素を見つけて 見つかったらそこからデータを受け取るといいです

イメージ

const wmap = new WeakMap()

// コンテキストを識別するためのオブジェクトを作る
// WeakMap のキーにする
// 使わないけどデバッグ時のわかりやすさのために name を持たせておく
const createCtx = (name) => {
return { name }
}

// 要素にコンテキストデータをセットする
// CustomElements のコンストラクタで実行を想定
const setCtxData = (elem, ctx, data) => {
if (!wmap.has(elem)) {
wmap.set(elem, new WeakMap())
}
const ctxs = wmap.get(elem)
if (!ctxs.has(ctx)) {
ctxs.set(ctx, { value: null })
}
const container = ctxs.get(ctx)
container.value = data
}

// 一致するコンテキストを持つ祖先を探して
// 見つかればコンテキストのデータを取得する
const findCtxData = (elem, ctx) => {
const host = elem.getRootNode().host
if (!host) return null
const container = wmap.get(elem)?.get(ctx)
if (container) return container.value
return findCtxData(host)
}

これで良さそうかなと思ったのですが slot を使う場合に問題がありました

<foo-elem>
<bar-elem></bar-elem>
</foo-elem>

こういう構造の場合 bar-elem は foo-elem の ShadowDOM の中のどこかで slot を使って表示されます
この場合に foo-elem がコンテキストデータを持っていれば bar-elem は foo-elem からコンテキストデータを受け取りたいです
しかし getRootNode を使ってたどると bar-elem が最初に見るホスト要素は foo-elem と bar-elem の両方のホストです
foo-elem のように同じ ShadowDOM に属する祖先要素はスキップされてしまいます

これに対処するいい方法はないかなと探してると Lit ではイベントを使って親にリクエストを送り 親がコールバック関数を使って値を返す方法を使ってるようでした
その方針にしてみます

const setCtxData = (elem, ctx, data) => {
elem.addEventListener("ctx", event => {
if (event.detail.ctx === ctx) {
event.stopImmediatePropagation()
event.detail.callback(data)
}
})
}

const findCtxData = (elem, ctx) => {
let data
const event = new CustomEvent("ctx", {
detail: {
ctx,
callback: (d) => {
data = d
},
},
bubbles: true,
composed: true,
})
this.dispatchEvent(event)
return data
}

これで良さそうですが 親でコンテキストを更新したことに気づけません
コールバックで渡す値を EventTarget オブジェクトにして 受け取ったらそこにリスナをつけるなどでしょうか

class Context extends EventTarget {
_value = null
get value() { return this._value }
set value(v) {
this._value = v
this.dispatchEvent(new Event("change"))
}
constructor(value) {
super()
this.value = value
}
}

const setCtxData = (elem, ctx, data) => {
const ctx_data = new ContextData(data)
elem.addEventListener("ctx", event => {
if (event.detail.ctx === ctx) {
event.stopImmediatePropagation()
event.detail.callback(ctx_data)
}
})
return ctx_data
}

const findCtxData = (elem, ctx) => {
let ctx_data
const event = new CustomEvent("ctx", {
detail: {
ctx,
callback: (d) => {
ctx_data = d
},
},
bubbles: true,
composed: true,
})
elem.dispatchEvent(event)
return ctx_data
}

const subscribeExample = (elem, ctx) => {
const ctx_data = findCtxData(elem, ctx)
ctx_data.addEventListener("change", () => {
console.log("changed", ctx_data.value)
})
}

動く例
https://nexpr.gitlab.io/public-pages/webcomponents-context/example.html

foo-elem がコンテキストデータとして { num: 1 } を持っています
子孫の bar-elem は foo-elem のコンテキストデータを受け取り num を表示します
ボタンを押すと num の数値が 1 ずつ増えて bar-elem に反映されます
Prettier の条件演算子のフォーマットが改善されたみたい
https://sosukesuzuki.dev/posts/prettier-curious-ternaries/

Prettier を使わなくなって結構経つので 最近どんな変化があったかは把握してなかったですが あの微妙な条件演算子のフォーマットが改善されたようです
まだ フラグ付きの実験的導入みたいですが 以前の全フラットよりは良さそうに見えますね

少し試してみようかな
React で maxlength 的なことをしたい
input 要素に maxlength 属性をつけると文字数制限ができます
ですが IME で変換している状態では 文字数制限を超えて入力できます
確定時に超えた部分は捨てられる挙動です

これができないと日本語入力だと不便です
例えば 2 文字入力できるところに「入力」と入力したいとします
ひらがなで 「にゅうりょく」 と入力してから変換するわけですが IME の変換中も 2 文字しか入らないと 「にゅ」 までしか入力できません
ローマ字入力だと 「nyu」 ですからこの時点でもオーバーしています

React などで input を制御するときは入力文字や文字数を制御して最初から入力できないようにすることがありますが そのときに maxlength の動きのように IME で変換中は入力不可の文字も一時的に入力できるようにしたいです

IME の状態の変化は compositionstart と compositionend イベントで取得できます
一応 input イベントの isComposing プロパティでもわかりますが これだとその入力が変換中のものかはわかりますが 確定されたことが伝わらないので compositionend イベントのほうがいいです

maxlength 相当なものを作ってみました

const App = () => {
const [text1, setText1] = useState("")
const [text2, setText2] = useState("")
return (
<div>
<Input1 value={text1} onChange={setText1} max={2} />
<p>{text1}</p>
<hr/>
<Input2 value={text2} onChange={setText2} max={2} />
<p>{text2}</p>
</div>
)
}

const Input1 = ({ value, onChange, max }) => {
const [composing, setComposing] = useState(false)
return (
<input
value={value}
onChange={(event) => {
if (composing) {
onChange(event.target.value)
} else {
onChange(event.target.value.slice(0, max))
}
}}
onCompositionStart={() => {
setComposing(true)
}}
onCompositionEnd={() => {
setComposing(false)
onChange(value.slice(0, max))
}}
/>
)
}

const Input2 = ({ value, onChange, max }) => {
const [local, setLocal] = useState("")
const [composing, setComposing] = useState(false)
useEffect(() => {
setLocal(value)
}, [value])
return (
<input
value={local}
onChange={(event) => {
const v = event.target.value
if (composing) {
setLocal(v)
} else {
const fixed = v.slice(0, max)
setLocal(fixed)
onChange(fixed)
}
}}
onCompositionStart={() => {
setComposing(true)
}}
onCompositionEnd={() => {
setComposing(false)
const fixed = local.slice(0, max)
setLocal(fixed)
onChange(fixed)
}}
/>
)
}

Input1 と Input2 があり どちらも IME の変換中は最大文字数を超えられます
Input1 は最大文字数を超えた状態でも onChange を呼び出します
input イベントに近い感じで 入力があれば不正状態でも親に伝えます
Input2 ではローカルステートを用意して IME の変換中は親には変更があったことを伝えず内部のステートのみ更新します

ここでは maxlength 相当の文字数処理しかしてませんが 電話番号入力欄で確定時に数字とハイフン以外を消すようにすれば 「でんわ」 から電話番号に変換できるよう IME に登録してるユーザーを考慮することができたりします

ちなみに React などで IME の変換中に文字数オーバーや使えない文字を消しても IME は内部で入力状態を持っているので 画面には見えないだけで変換候補はちゃんと入力したものとして出てきます
しかし 変換候補を切り替えるときに 見えている文字を置き換えるために元の文字数分を消して現在の変換候補の文字を入力するので その間で無理やり書き換えると関係ない文字が消えてしまい正しく入力できなくなります (Google IME で確認)

そもそも入力自体を不可にする処理っているのですかね?
JavaScript の Decorators はどんな感じになったんだろう
Lit のドキュメントを見てるとよく使われてるデコレーター
そういえば少し前の TypeScript 5.0 から Stage3 のデコレーターがフラグ無しで使えるようになったと聞きます
あまり使うつもりも無かったのと ステージが上がるのが長期的なものだったので詳しくは見てなかったですが Stage3 に上がって 1 年以上経ってるようですし TypeScript で標準でつかえるようになるということは現状の機能で JavaScript に来そうですし どんなものか見てみました

クラス専用機能みたいです
思ってたのと違う
なんか前もそんなことを言ったような気がします

クラス定義やメソッド定義の前にだけかけるようです
また 単純に関数で変換するだけでなく コンテキストオブジェクトも受け取って 初期化処理を追加するなど思ってたより複雑なものでした

const decorator = (method, context) => {
console.log(context)
context.addInitializer(function() {
console.log(this)
})
}

addInitializer はクラスのデコレーターだと クラス定義後すぐで メソッドだと各コンストラクタ内の処理として呼び出されるようです
また this を受け取るのでアロー関数ではない通常の関数を渡す必要があります

また デコレーターは値を返すことは必須ではなく 返さないとデコレーターの引数に渡されるものそのままがセットされるようです

期待してたのは もっと単純で任意の式の前におくことができてアロー関数にも適用できるものだったのですけどね
こんなこともできて 関数でラップする代わりの記法みたいなもの

const log = fn => (...a) => {
console.log("called", fn, a)
const result = fn(...a)
console.log("result", fn, result)
return result
}

const fn = @log (a, b) => a + b
fn(1, 2)
// called (a, b) => a + b [1, 2]
// result (a, b) => a + b 3

const plus1 = x => x + 1

console.log(@plus1 10)
// 11

関数でラップすればいいだけ といえばそうなのですが これができたほうが見やすく書けると思うのですよね
例えば setTimeout みたいに関数を渡すところ

setTimeout(
() => {
console.log(1)
},
1000
)

これを関数でラップすると

setTimeout(
deco(option1, option2, () => {
console.log(1)
}),
1000
)

デコレーターで書けると

setTimeout(
@deco(option1, option2)
() => {
console.log(1)
},
1000
)

明らかにこっちのほうがいいと思います
手前に書く記法でもいい感じです

foo(
value1,
value2,
@once (value) => {
console.log(value)
},
)

ただ 構文を考えると アロー関数の引数の () がデコレーターの once の関数呼び出しみたいになり 判断できなそうです
対処するなら関数を () で包まないとダメそうで そうなるなら求めてるものじゃないんですよね

この記法ではないものの 拡張機能として将来的もっと色々な使い方が考えられているようです
便利というより複雑という印象が強いですけど
https://github.com/tc39/proposal-decorators/blob/master/EXTENSIONS.md
ユーザーの概念があるところでは localStorage を暗号化
前の記事で書いたログアウト後に localStorage が残る問題
対処するならどうするのがいいのかなと考えたところ ユーザーの key で暗号化して保存するのが一番かなと思います
暗号化しておけば別ユーザーでログインしてアクセスしたときに復号に失敗するので無視して復元なしになります

単純にログアウト時にクリアということも考えられますが Cookie のセッションや時間切れによる自然ログアウトという可能性もありえます
この場合は localStorage にデータは残ったままです
ログイン時に前回のログインユーザーと違えばクリアすることもできますが ログインする前に直接 localStorage を見ることだってできます
その点では暗号化しておけば安心です

ただ 暗号化は少し重ための処理になるので 入力イベントのたびに localStorage に書き込んだり localStorage からデータを探したりするのは向かなくなります



思いつきでちょっとしたものを作ってみました
https://nexpr.gitlab.io/public-pages/encrypt-storage/example.html

ユーザーを選んでログインボタンを押すとそのユーザーでログインできます
ログインといっても JavaScript で cookieStore に直接書き込む擬似的なものです
ログイン後は textarea が出るのでここに書き込むと内容が自動で localStorage に保存されます
ログアウトして別ユーザーでログインしても復元されません
同じユーザーでログインすると復元されます
localStorage のキーはひとつだけなので 別ユーザーでなにか入力すると上書きされて前のユーザーの入力情報は消えます

ここでは暗号化のキーはユーザー ID を元に適当に作ってますが ちゃんとやる場合は他ユーザーのキーを予想できないようにサーバーでランダムに作って ログイン中のユーザー情報と一緒に受け取るとかが必要です
全体を display:none にしておくとページ遷移のときにちらつきが減る
最近は CSR のものばかりで最初から HTML があるものはあまり作らないので気にしてなかったのですが 久々にそういうページを作ったら module のロード中に初期化前の状態が見えていまいちなものになりました
CSS のロードも ESM でやると適用されるのが後からになりますからね
また CSS を同期的にしても JavaScript で初期状態のクラスを設定する場合 module が全部ロードされてから処理が行われるので それまでに違う状態の画面が表示されてしまいます
ほぼ一瞬ですが チラつきになりますし あまり気持ちのいいものではないです

一瞬関係ない画面が映るよりは display: none をつけて何も表示されないほうがマシかなと思って display: none にしてみると 思ってた以上に良い動作になりました
一瞬の真っ白な画面が挟まりません
前のページの画面が維持されて display: none が消えて表示される状態になって始めて画面が更新されました

ただ 500ms くらいが境目のようで 600ms とか 1s ほどの遅延になると一瞬真っ白な画面が挟まります
それ未満の 300ms などだと 真っ白な画面は発生しません
display: none を外してみると 一瞬関係ない画面は出るので ブラウザがいい感じにしてくれてるようです

注意点として body の中身が全く表示されない状態の必要があります
メインのコンテンツの div を display: none にしてもその外に表示される要素があったら それだけが表示されます
body ごと display: none にしておくのがいいかもですね

試せるページ↓

遅延300ms+display:noneあり
遅延300ms+display:noneなし
遅延800ms+display:noneあり
遅延800ms+display:noneなし

下の方にあるリンクでメインとサブのページを交互に移動できます
URL の delay と hide で遅延させる時間や display: none の有無を変更できます
詳細はページ内のクエリパラメータの説明参照です
ログアウトとローカルデータ
ブラウザ内の一時保存で良いデータを localStorage に保存することがありますが 考えてみるとログアウトして別ユーザーでログインしたら別ユーザーにも引き継がれますよね
テーマとかサイドバーの開閉状態とかそれくらいなら別にいい気もしますが 例えばフォームの入力情報みたいのは他ユーザーが見れるとまずい気もします
例えばクラッシュに備えた自動保存で 投稿に成功したり入力を破棄しない限り再度開いたときにストレージから復元するような機能です
こういうのがあると ログインするものでは localStorage を気軽に使いづらいですね

似たようなもので長期キャッシュも問題になるかもしれません
max-age をつけてリクエスト不要のキャッシュにすると ログアウト後でも見れてしまいます
そもそも静的ファイルは隠す必要はないのかもしれませんけど
昔ながらの Apache + PHP だと 静的ファイルはアプリの処理に入る前に Apache で返すので公開されてますし
それに基本は .js か .css か画像系なので履歴に URL は残らなくてログアウト状態だとアクセスする URL はわからないはずです

問題になるのはログ系のファイルでしょうか
月や日付などが URL に含まれるようなもので更新されないデータです
更新されないんだからキャッシュさせたくなりますが ユーザーの情報が入ってるのでログアウト後に履歴から開いてみれるのは良くないです
こういうのがあると max-age に任せるだけじゃなくて E-Tag に対応させて 304 を返すような機能も入れておいたほうがいいかもですね
WeasyPrint はまだ日本語に使うのはいまいち
印刷向けの CSS 機能がブラウザより整っていたりして良いツールなのですが 日本語で使おうとすると問題点があり 将来性の期待はあるものの実用はしてない状況でした
久々に使って 色々更新されてるし問題点も対応されてるかなと思ったのですが まだ対応されてませんでした
フォント関係なので CJK のフォントを使わないならほとんど関係なさそうですし 優先度は低そうです
内容的に将来的にも対応されるかもわからないです

問題のひとつは CID キー付きフォントに対応してないことです
https://github.com/Kozea/WeasyPrint/issues/1593

日本語のフォントで有名な

源ノ角ゴシック (Source han sans)
源ノ明朝 (Source han serif)

など のフォントは CID キー付きフォントになってるようです
効率的に保存できるらしいので 多くの文字を保持しているフォントはたぶんほとんどこれなのでしょう
これらのフォントを WeasyPrint で使うと問題なく PDF は出力できるのですが 一部の環境では CJK の文字が表示されません
Edge などブラウザでは問題ないようなのですが PDF である以上 一般的なビューワー全てで見れないというのは使えない場合があります

ところで 源ノ明朝って 「げんのみんちょう」 って読むらしいです
「みなもと」 だと思ってたのに
(みなもとのよりともって呼んでる人多いよね?)



そういう問題があるので別のフォントを探して見つかったのが IPA フォント
これは CID キー付きフォントではないようです
しかしこれは別の問題がありました
太字・斜体にできません

最近の可変フォントを除けば 太字や斜体って別にフォントが用意されているものです
regular とか medium など太さがフォントの名前についてたりします
そうでないフォントは フォントとして太字のデータはないので 表示する側で擬似的に太字にしています
自動でやる分 ちゃんとフォントが別にあるものより汚いみたいです
WeasyPrint はこの疑似的に太字にしたり斜体にしたりする機能をサポートしていません
太字や斜体にしたいなら それに対応したフォントを使うか b や strong タグのスタイルに別の太字・斜体フォントを指定してという方針みたいです
しかし IPA フォントを始め多くのフォントって太さの違いを別フォントにしてないです
これのせいで太字が使えません

Windows にインストールされている標準のフォントを見ると MS ゴシックや MS 明朝はフォントが太さのフォントは用意されていないようです
メイリオや游ゴシックはフォントが用意されていました
しかし WeasyPrint を実行するのは Linux 環境です
個人で使う程度なら気にしなくてもいいのかもですが ライセンス的にはアウトだと思います
それにこれらのフォントが CID キー付きフォントでないかは未確認です



そんなわけで WeasyPrint で使える良い日本語フォントがなくて使えない状況です
マイナーフォントを探せば CID キー付きではなく太さごとにフォントファイルが用意されたフォントもあるかもしれないですが あまり変わった書体ではなく よくある一般的なものにしたいのですよね

将来的に WeasyPrint がこれらをサポートしてくれるといいのですが あまり期待もできません
太さの問題は過去バージョンは対応していたのに 内部的に PDF 生成やフォントを扱うツールを変えてから対応しなくなったようです
しばらく修正されないままですし 使うツールの都合上対応が難しそうなことも書かれています
https://github.com/Kozea/WeasyPrint/issues/1470
太さが違うフォントを指定すればいいという回避策もある以上期待できないです

CID キー付きフォントの方は調べてもいまいちよくわからないものです
レガシーという記述があったりなかったり
CID フォントと言っても種類があるようで sfnt wrapped という形式の CID フォントがレガシーらしいです
OpenType のものはそれとは違って問題ないように書いてるページもありました
https://ccjktype.fonts.adobe.com/wp-content/uploads/2018/12/cjkv2e-pp387-393.pdf
逆に CID キー付きフォントは この sfnt wrapped の CID を含むレガシーフォーマットと言う記載もあります
https://github.com/fontforge/fontforge/wiki/Community-guidelines#d6-cid-keyed-fonts

よくわからないですが フォント系を扱う有名なツールの fontforge がレガシーだから積極的にサポートしないといってるくらいです
WeasyPrint が対応する期待はできません

ブラウザ自体がもっと印刷や PDF 対応を頑張ってくれるといいのですが 印刷需要が少ないのであまり期待はできないですね
Dockerfile とパス
久々に使うと忘れてるので

「docker build」 で指定するのは Dockerfile ファイルじゃなくて Dockerfile があるフォルダ
コマンドを実行したカレントディレクトリを問わずに そのフォルダがカレントディレクトリとしてビルド処理が実行される
なので COPY コマンドの from 側の相対パスは指定した Dockerfile のあるフォルダになる

もっと正確にいうと 選択したフォルダがビルドのコンテキストになる
デフォルトではコンテキストのルートにある Dockerfile が使われる
コンテキストの中にあるファイルしか使えないので COPY で 「../」 を使って外側を参照できない

例えば複数の Dockerfile で共通のデータを使いたくてこういうフォルダ構造になってる場合

- common/common_file.txt
- image1/Dockerfile
- image2/Dockerfile

image1 をコンテキストにしてしまうと common の中身を参照できない
コンテキストを common や image1 があるフォルダにして 使用する Dockerfile のパスを別に指定することで解決できる
-f オプションを使って コンテキストのルートからの相対パスで Dockerfile の場所を指定する
ファイル名も指定するので Dockerfile という名前以外も可能

docker build -f image1/Dockerfile .

この場合 image1 のビルド時に image2 もコンテキストに含まれる
コンテキスト内のすべてのデータはビルド時にコピーされるので image2 のデータのコピーはムダになる
.dockerignore ファイルを用意することでコンテキストからファイルを除外できる

- image1/Dockerfile.dockerignore
- image2/Dockerfile.dockerignore

を作ってそれぞれのビルド時にコンテキストから除外したいファイルのパターンを指定する
基本は .gitignore みたいな記法
記法の詳細
https://docs.docker.com/build/building/context/#syntax

・ 最初に / があってもなくてもコンテキストのルートからのパスとして扱われる
・ ? や * のワイルドカードが使える
・ */foo でルートの任意のサブディレクトリの foo
・ **/foo で任意の階層の foo
・ # でコメント
・ ! で除外指定
CSS の zoom プロパティは存続するみたい
以前なくなりそうと書いたけど結局どうなったんだろうと調べてみました

https://groups.google.com/a/chromium.org/g/blink-dev/c/V7q43bgutbo/m/-7jneTl8CQAJ

の最後のコメントを見る感じ CSSWG で正式仕様にするよう決まったみたいで 削除はされないそうです
リンク先にすごく長い議論の本文がありますが 長すぎてそこまで読んでないです

標準の仕様化するなら今の一貫性のない挙動じゃなくて もっと見直してほしいと思いますが 標準化する理由が互換性の問題なので今の動作に合わせた形にしかならない気がします
また歴史的経緯で変な仕様が増えるのか
Grid でヘッダーとフッターとサイドバーのレイアウトを作るとき
3 x 2 のグリッドにしてこんな感じにしてる例をよく見ます

1 2
3 4
5 6

Header: 1, 2
Main: 3
Sidebar: 4
Footer: 5, 6

こうするとわざわざ ヘッダーとフッターは横 2 マス分って設定しないといけないです
サイドバーとメインコンテンツの区切り位置をヘッダーやフッターにも反映したいならともかく それらと関係ないなら一つのグリッドにまとめなくてもいいと思います
単純に縦に 3 分割して その中の 2 のところを横に 2 分割するで十分です
HTML 構造的にヘッダーもメインもサイドバーもフラットになって欲しいならありかもですが こういう場合は構造的にボディ部分は一つにまとまってその中で分割してほしいです
Grid ってフラットになる分 画面とコードの位置関係の対応が HTML だけだと分かりづらくなります
無理にフラットにしないほうがいいと思います
保存用出力フォーマットは何がいいのかな
海外サービスってログをずっと残しておいてくれるのが多いと思います
Amazon だと購入履歴がかなり昔のものも見れます
それに対して日本の銀行みたいなところってある程度の期間の情報しか残ってません
まぁ 全ユーザーの過去ログを保存してるとストレージ使用量も多くなりますし 検索負荷も高くなりますからね
消えるのは仕方ないにしても ユーザーは手元に記録を残しておきたいのでエクスポートできるようになってるものが多いはずです

そんなときのエクスポートするフォーマットって何がいいんでしょう
よく見るのはやっぱり PDF
印刷が前提ならとりあえず PDF となります
でもわざわざ印刷するような人って今の時代少ないですよね

それだとあまり PDF のメリットはないです
簡単に編集できないので誤って変更してしまうことはないですが 環境によっては扱いづらいです
あとデータを抽出して扱いたい時に PDF は相性が悪すぎます

そういうことを考えると XLSX
簡単に編集できますが 一応編集できないよう保護することもできます
保護を外せますが 外すなら意図的に変更してるのでそこは気にしてなくもいいと思います
データを抽出して扱いたい時は別のブックにデータをコピペしてそのままエクセル上で処理できます
ただ エクセルが入ってることが前提になります
最近は Office を入れてない人もいます
一応 OneDrive に保存してオンラインでエクセルを操作することはできますけど 扱いやすいとはいえないです

意外といいかもと思ったのは HTML
ダブルクリックすればブラウザで見れます
CDN のライブラリを使わなければ HTML 単体でネットに繋がっていなくても見れます
エクセルや PDF よりリッチな見た目にもできます
テキストエディタで開かないと編集はできないので誤って変更することはなさそうです
HTML 構造からデータを取り出せますし 内部でデータを JSON で持ってそれから DOM を作るようにしておけば JSON を取り出すこともできます
ただ エクセルよりは専門的な知識がいるので誰でもできるものじゃないです
閉じずに端に移動できるダイアログ
ダイアログで入力するものがあるとき 入力の途中でダイアログの裏側にある画面をみたいことってありますよね
でも裏側を見るためにダイアログを一旦閉じたら入力中の内容が消える場合が多いです
対処として別タブで同じページを開くこともありますが 面倒です
手間なく簡単に見れるようにしたいです

jQuery 時代によく見かけたものだと Windows のウィンドウみたいな感じでヘッダーをドラッグして動かせるものがありました
悪くはないですが ダイアログは自由な位置じゃなくて決まった位置にいて欲しい気持ちがあります
また 動かせても背景が暗いままだと裏側の文字が見づらいです

ということで 思ったのが小さくして端の方に持っていきたいというものです
Youtube の動画で右下で小さくして再生できたりしますが あんな感じ
その状態で必要な情報を見たりコピーしたりして またダイアログを表示させて入力します

固定で右下に持ってきたら 見たいものが右下にあると困るので 一応端に持ってきた状態ならドラッグで動かせたほうがいいかもしれません
そんな感じで試しに作ってみたのがこれです
https://nexpr.gitlab.io/public-pages/floatable-dialog/example.html

下の方にあるボタンを押すと ダイアログが開きます
ダイアログヘッダーの右側の小さくしそうなアイコンのクリックで縮めます
小さくしたらドラッグで動かせます
広がりそうなアイコンをクリックしたらダイアログを復元します

試しに くらいのつもりで 2, 3 時間くらいで簡単に作ったものなので 画面外までドラッグできたり色々問題もありますが 思ったよりいいかもしれません

その後 もう少し豪華なサンプルも用意しました
ダイアログを通して要素の追加・編集ができるので 実際に裏側のものをコピーして入力などができます
https://nexpr.gitlab.io/public-pages/floatable-dialog/rich-example.html
HHVM は正式リリースを作らなくなったみたい
去年末からリリース報告の更新がなくなって その後はセキュリティパッチの情報が数回あったくらいでした
どうなったんだろうと思ってましたが 先週新しい記事が投稿されてました

https://hhvm.com/blog/2023/10/27/oss-update.html

色々変更するそうです
GCC をやめて Clang にするみたいです
Clang のほうが新しいし 自然なものかなと思ってたら理由は Meta の投資が減って余裕がないからだとか
そんな良くない状況なんだ
たしかに 以前は PHP 代替として話題性もあったのに最近は全然聞かなくなりましたからね
PHP 互換を捨てると発表した頃から個人的には少し期待してましたが ついてこないユーザーばかりだったようですね

公式リリースが去年から公開されてませんでしたが 今後も作成しないようです
メンテする余裕もないみたい

一応 OSS として公開を続けるみたいですが ほぼ Meta の内部使用のためのものを公開してるだけみたいですね

この方向だとこれ以上 流行るように思えないですし 公開されてるドキュメント等も放置されたままになりそうですし もう使うことはないかも