Edge でスクロールバーの色が変わった
最近気づいたら変わってました
黒いです
以前は気にならなかったので たぶん変わったのは Edge 123 か 124 だと思います
Chrome は今のところ変わっていません

変わったのは トップレベル(ドキュメント全体)のスクロールバーの色です
自分で div に overflow: auto をつけてスクロールバーを出してもここは変わっていないようです

色はダークテーマかどうかを反映してそうです
ためしに html 要素のスタイルの color-schema を変更してみると light だと白く dark だと黒くなりました
OS 側でダークテーマを ON にしてるとそれを引き継いで黒くなるのでいつもと違って見えたということみたいです

ダークテーマを切替可能なページなら設定に応じて color-schema を切り替えてるので自然ですが ダークテーマに対応していないページだと color-schema は設定してないことも多く見た目はライトテーマなのにスクロールバーだけ OS の設定に合わせてダークテーマになっていて不自然です

後から出てきたブラウザ仕様に合わせてページを書き換えるのは嫌なところもありますが ダークテーマをサポートしないページでも

html {
color-schema: light;
}

だけはつけておいたほうがいいかもしれません
Windows で cmd.exe を通してコマンドを実行するときの脆弱性
https://forest.watch.impress.co.jp/docs/news/1584317.html

これの話題を聞いてなんか納得しました
cmd の仕様が意味不明すぎるし一貫性がないしで本当に扱いづらいです
なにかの問題に対応すると別のが動かなくなったり
PHP を使ってるときに近いストレスなんですよね

Rust では全てのケースを安全にエスケープできないからエラーにする場合もあるそうですが仕方ないなと思います
そういえばもう 5 年以上前ですが Rust で簡単なツールを作ったとき Windows でコマンド実行するときにかなり苦戦した覚えがあります
Stackoverflow では既知の難しい問題と言われていたり cmd.exe を通すのはやめたほうが良いとか言われてました

Windows は 11 で古いものを切り捨てたり UI 変更したりするなら先にこういうものをどうにかしてほしいんですよね
こういう内部的な問題を放置で上辺だけ綺麗にされても使いたいと思えないです
古いものの互換性は WSL や Sandbox みたいな技術で仮想的に古い Windows を動かすとかすればいいです

それと気になったのは Rust が完全に対応できないものを Node.js や PHP など他の言語ではちゃんと対応できてるのでしょうか



せっかくなので Node.js の発表も見てきました
Node.js も同じような対策みたいです

問題点は spawn 系で { shell: true } のオプションをつけていない場合でも エスケープ不全で任意コードが実行される可能性があるということです
.cmd や .bat ファイルを実行すると発生するので これらを実行しようとすると EINVAL エラーが出るように変更したようです
破壊的変更みたいですが --security-revert オプションでもとに戻すこともできるようです

.cmd や .bat を実行する場合は 自分でサニタイズして { shell: true } にして使えば良いらしいです
基本は禁止で使うならエスケープとかはユーザーに任せるようです
Chrome のインストーラーに Google に情報を送るかのフラグが含まれてる
インストーラーをダウンロードするところに統計情報や障害レポートの自動送信のチェックボックスがあります
インストールするとき exe を実行してから Chrome が起動するまでには選択肢が出ないです
ということは exe ファイルが別?と思ってインストーラーを比較してみました

ファイル名はファイルサイズは一緒ですが ハッシュ値が異なってました
ダウンロード URL もよくみると別になってました
言語などいろいろな情報がクエリ形式で含まれていて その中の usagestats という統計情報の送信の有効無効のフラグが 1 か 0 で異なってました

インストーラーの exe を使い回したり共有したりすると その確認がされないのはどうなのかなと思ったりはしますね
WSL のメモリを解放する
気づくと vmmem が数 GB になってる
WSL は割り当てメモリ上限を指定できるので小さめにしておけばいいのだけど ときどき大きめのメモリが必要になるので上限は余裕を持たせておきたいのですよね
通常時はメモリを使って無くても Linux 側でキャッシュとして確保されたものも Windows からすると使用中として扱われて使用メモリが増えます

root@WIN-NOTE:~# free -h
total used free shared buff/cache available
Mem: 6.1Gi 139Mi 4.8Gi 0.0Ki 1.2Gi 5.8Gi
Swap: 2.0Gi 0B 2.0Gi

buff/cache のところ

解放するために手動でキャッシュをクリアします

root@WIN-NOTE:~# echo 3 > /proc/sys/vm/drop_caches
root@WIN-NOTE:~# free -h
total used free shared buff/cache available
Mem: 6.1Gi 135Mi 5.9Gi 0.0Ki 110Mi 5.8Gi
Swap: 2.0Gi 0B 2.0Gi

/proc/sys/vm/drop_caches に 3 を書き込むとクリアしてくれます (root 権限が必要)

こうすると Windows 側の vmmem の使用量も小さくなります
一気に下がるんじゃなくて時間をおいて段階的に下がる感じ
完全に used + buff/cache の値まで下がらず多少余裕を持って確保されてるみたい
上の状況だと 1GB 以上あったものが 700MB くらいまで減少

以前 WSL2.0 の新機能で自動でキャッシュを解放して Windows に戻してくれる機能が追加されるとか話がありましたがまだデフォルトで有効にはなってないみたいですね
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
PowerShell に特殊な null がある
PowerShell を使ってたときに奇妙な動きをするところがありました
null の動作が一部だけ違います

代入してない変数は null で -eq $null すると True になります
-match するとマッチしないので False です

PS C:\> echo ($val -eq $null)
True
PS C:\> echo ($val -match "^A")
False

$null を代入した変数でも同じです

PS C:\> $v = $null
PS C:\> echo ($v -eq $null)
True
PS C:\> echo ($v -match "^A")
False

しかしコマンドの結果の場合は

PS C:\> $x = (node -e "null")
PS C:\> echo ($x -eq $null)
True
PS C:\> echo ($x -match "^A")
PS C:\>

-match のときに False が出ません
コマンドの実行結果なので空配列だったりするのかと思いましたが $x.gettype() は null のメソッド呼び出しとしてエラーになります
$x を表示しても空ですし -eq $null が True なので null のはずなのに

node コマンドは何も出力しないように -e "null" を指定しています
null を評価するだけでそれを出力しないので null が返ってくるわけではなく 標準出力に何も書き込まれないコマンドです

($x -match "^A") も null になっているのかと思って gettype() を呼び出してみるとなんと Object[] でした

PS C:\> ($x -match "^A").gettype()

IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array

意味がわからないのですがググっていると PowerShell の仕様として特殊な null が存在するようでした
https://learn.microsoft.com/ja-jp/powershell/scripting/learn/deep-dives/everything-about-null?view=powershell-7.4#empty-null

実行ファイルに限らず PowerShell 上の関数でも 何も返さないものの結果である null は特殊な null で「Empty null」と呼ばれるものみたいです
実体は System.Management.Automation.Internal.AutomationNull.Value の値らしいです
この値は null と比較したり値の評価が行われるときは null として扱われるようです
なので

PS C:\> [System.Management.Automation.Internal.AutomationNull]::Value -eq $null
True

ですし 通常の null か判断できません

判断するには特殊な方法で配列に入れて Count を見ればいいようです

PS C:\> @($v).count
1
PS C:\> @($x).count
0

普通の null なら 1 で Empty null なら 0 になります

すごく特殊ですし こういうのはやめてほしいです
変なことしていて無理やり都合を合わせてる感
PHP みを感じます
もっと一貫性があって例外的なものが少ないようしてほしいです
コンテナを使って並列にテストしたい
テストを実行するときって 並列にすると色々問題が出そうでとりあえずひとつずつ順番に実行するようにしてます
純粋な関数なら問題ないですが 実際にはファイルや DB など外部に副作用を与える物が多いですからね
こういう処理が入るものを並列にすると問題が起きます
1 つ目のプロセスでデータを作って それを確認する前に 別のプロセスで上書きしてしまったり

同じ場所を使わなければいいので 並列に処理する数だけ フォルダやサーバーを用意すればいいといえばそうなのですが 結構面倒です
処理ごとにフォルダを作ったり DB や通信相手のサーバーを作ったりは大変です

ですが コンテナなら自然と独立した環境になります
アプリ側の処理の中では普通にファイルを書き込んだりサーバーを起動してアクセスしたりするだけです

でもそういう機能のテストランナーってあるのでしょうか
特に聞いた覚えがないです

テストランナーの処理の中で並列にするところは個別にコンテナを起動とか考えると複雑そうですし対応してるのがないのかもしれません
並列にしたいところはファイルを分けて 手動でファイルごとにコンテナを作ってその中でテストランナーを実行するようにするのがいいのかもです
でもこれはこれで面倒な気がするのですよね
タグに親子関係をつけたい
ブログもそうですが その他のところでもタグ付けって結構あちこちにあります
で 思うのですがタグに親子関係のような関連性を持たせたいです

記事でいうと React の記事を書いたらタグには "React" をつけますが その他にも "JavaScript" や "Frontend" などもつけると思います
Fastify の記事であれば "Fastify" 以外に "JavaScript", "Node.js", "Backend" など

このタグなら他にこれらもついてくるみたいなのを記事側に全部つけなくてもタグ側でやってほしいのですよね
React だけつければ その記事は React タグで検索したとき以外に JavaScript や Frontend タグで検索したときも出てきてほしいです

あと class というタグがあったとき オブジェクト指向言語で使われるクラスなのか HTML 属性のクラスなのかわからないです
同じ名前で違う意味のものがあるとときに区別ができないです
この点でも ただの名前の文字列だけじゃなくて関連性を持つものなら オブジェクト指向と関連性をもつ class か HTML と関連性を持つ class かで分けられます

こういうタグシステムが普及してほしいのですが 現状はどこのサービスもただの文字列だけでしかないんですよね
Prettier の折り返し位置
指定の文字数には全然達してないのに 折り返されたり折り返されなかったりして意味がわからないと思っていたら インデントサイズが影響していました

abcd.method({
a: 1,
}).method()

このコードはフォーマットしてもそのままでしたが abcde にして 5 文字になると

abcde
.method({
a: 1,
})
.method()

のように折り返されました

別のところに持っていくと
abcd まででも折り返されました
printWidth は同じです

ab.method({
a: 1,
}).method()

abc
.method({
a: 1,
})
.method()

理由がわからずバージョンの違いかと思ってバージョンをあわせても同じ挙動
相変わらずの意味不明な挙動と思ってたら tabWidth の違いでした

ab のところが tabWidth を超えると折り返されてるようです
上のときは tabWidth が 4 なので 4 文字の abcd で折り返されず abcde の 5 文字で折り返されていました
下のときは tabWidth が 2 だったので 2 文字で折り返されず 3 文字で折り返されていました
ここの文字数を見る意味がよくわからないです
printWidth に収まってるのですから 気にせず 1 行にまとめてくれればいいのですけど

それよりも タブを使ってるのにタブの幅を見て動作を変えるのはやめてほしいです
タブを使うのは各自がインデント幅を自分の見やすい形にしていて 固定のタブ幅なんていうものはないはずです
相変わらずタブに対する理解のない動きをするんですよね
楽に何かを作れるライブラリ/フレームワークが欲しい
楽に作れる物が欲しいです
いつもなら細かくライブラリを厳選してそれらを組み合わせて使うほうが好きなので 全部入りで色々決まっていてカスタマイズの余地が少ないライブラリやフレームワークは好みではないです
色々と自分でできるほうが楽しいですし自分の好みに調整できます

ですが 時間を掛けずとりあえず動かしたいってときもたまにはあるんですよね
どう作るかよりも何を作るかに力を入れたいときです
そういうときはこだわりはなくコードがどうなっていようと言語がなんであろうと動けば何でもいいです
やりたいことが実現できないとかでなければですけど

AI がもう少し賢ければありかもと思うのですが 現状だと動かないコードばかりですし コードを書く量は減っても人がバグを見つけて修正するような作業をしないといけないです
AI のコードレビューはしたくないので AI はなしでフレームワークに頼りたいところです

といっても Node.js ってあまりそういうフレームワークがないです
Express や Koa はミドルウェア系で本体は最小限で 各機能は外部のミドルウェア任せです
Fastify は基本的なプラグインは公式が提供していますが 結局別パッケージに分かれていたり設定の自由度があるので楽かというとそうとも言いづらいです
普段使いのテンプレートみたいなのがあればいいですが そうでないと色々考えることが多いです
公式にあるプラグインは基本的なウェブサーバーとしては十分ですが アプリケーションとして考えると不足箇所が多く結局そこはサードパーティのプラグインを探すか自作が必要になります

マイナーどころを探せばそういうフレームワークもないわけではないですが こういうフレームワークはメジャーであって 情報が多いことが重要です
フレームワークの内部実装まで読んでコードを書いたりしたくないですし

そうなると 他言語で Ruby の RoR や PHP の Laravel になってくるのでしょうか
これらもあまり気軽に使えるようなものじゃない気がしますけど
CSRF 対策に Sec-Fetch-Site を使う場合の問題
最近は IE も消えたので Sec-Fetch-Site が same-origin であることをチェックして CSRF 対策としています
トークンを送ったり 独自のヘッダーをつけたりに比べるとかなり楽です

ですが この方法 問題がありました
Sec-Fetch-Site ヘッダーはブラウザが自動で送信するものですが HTTPS サイト限定のようです
HTTP サイトには送信されません

これのせいで HTTP のページで使おうとすると必要なヘッダーが送信されないので全て不正なリクエストとみなされます
一般公開するようなもので CSRF 対策が必要なサービスなら基本 HTTPS でしょうけど そういったもの以外ではありえます
一般には公開せず ネットワーク内のみでアクセスできるサーバーだったり 開発環境だったり

開発環境でも localhost という名前なら特別で HTTP でも HTTPS 同様に Sec-Fetch-Site ヘッダーが送信されていました
ですが 別端末からアクセスするケースなど localhost 以外でアクセスするケースもあって そういうときに動かなくなります

開発中やネットワーク内のみならそもそも攻撃する人がいないはずなので Sec-Fetch-Site ヘッダーを見ないようにして対処してますが 開発中に動かさないと動作確認ができないですし なにかとスッキリしないです
HTTP でもとりあえず送ってくれたらいいのに
依存パッケージの数を知りたい
◯ Yarn v1 の場合

yarn.lock のトップレベルキーの数
コメントの # 行は無視

const lines = fs.readFileSync("./yarn.lock").toString()
.split("\n")
.filter(x => x[0]?.trim() && x[0] !== "#")

console.log(lines.length)
console.log(lines.join("\n"))
42
accepts@^1.3.5:
cache-content-type@^1.0.0:
co@^4.6.0:
content-disposition@~0.5.2:
content-type@^1.0.4:
cookies@~0.9.0:
debug@^4.3.2:
deep-equal@~1.0.1:
delegates@^1.0.0:
depd@^2.0.0, depd@~2.0.0:
depd@~1.1.2:
...

◯ Yarn Berry

2 以降でも基本は同じ
だけど yarn.lock ファイルが YAML として有効なファイルになってるので YAML としてパースしてトップレベルキーの数を数える方法も取れる

2 以降の yarn.lock では自身もエントリとして存在する
「foo@workspace」 みたいなの
さらに __metadata という特殊なキーもあるので これらを除外する必要あり

const data = require("yaml").parse(fs.readFileSync("./yarn.lock").toString())

console.log(Object.keys(data).length - 2)
console.log(Object.keys(data).join("\n"))
42
"accepts@npm:^1.3.5":
"cache-content-type@npm:^1.0.0":
"co@npm:^4.6.0":
"content-disposition@npm:~0.5.2":
"content-type@npm:^1.0.4":
"cookies@npm:~0.9.0":
"debug@npm:^4.3.2":
"deep-equal@npm:~1.0.1":
"delegates@npm:^1.0.0":
"depd@npm:^2.0.0, depd@npm:~2.0.0":
"depd@npm:~1.1.2":
...

YAML ライブラリを入れなくても v1 と同じように行ごとの単純処理でも十分対応できる
YAML として有効なファイルになったことでキーは必ず 「"」 から始まってる
1 文字目が 「"」 という単純なカウントで済むので v1 より楽
この方法だと __metadata は含まれないので 自身のパッケージの数だけ除外すればいい

const lines = fs.readFileSync("./yarn.lock").toString()
.split("\n")
.filter(x => x[0] === '"')

console.log(lines.length - 1)
console.log(lines.join("\n"))
42
"accepts@npm:^1.3.5":
"cache-content-type@npm:^1.0.0":
"co@npm:^4.6.0":
"content-disposition@npm:~0.5.2":
"content-type@npm:^1.0.4":
"cookies@npm:~0.9.0":
"debug@npm:^4.3.2":
"deep-equal@npm:~1.0.1":
"delegates@npm:^1.0.0":
"depd@npm:^2.0.0, depd@npm:~2.0.0":
"depd@npm:~1.1.2":
...

Yarn Berry でゼロインストールにしてる場合は .yarn/cache にパッケージの zip ファイル一覧が並ぶのでこれを数えることもできる
.yarnrc.yml で enableGlobalCache: false になってると .yarn/cache にパッケージファイルが保存されてる
node_modules と違ってネストされないので同じ階層のファイル数で判断できる

ls .yarn/cache/ | wc -l
42

ここの方法だと自身は含まれないし __metadata キーもないので一番楽
一応 .gitignore が cache フォルダにあるけど隠しファイルなので ls に出てこない

◯ npm

package-lock.json の packages オブジェクトのキーの数
lock ファイルが JSON なので一番カンタンかも

トップレベルの packages プロパティにオブジェクトが入っていて このオブジェクトのキーがパッケージ一覧になってる
Yarn Berry 同様で自身(キーは 「""」)が含まれてるので それの除外が必要

const data = require("./package-lock.json")

console.log(Object.keys(data.packages).length - 1)
console.log(Object.keys(data.packages).join("\n"))
42
node_modules/accepts
node_modules/cache-content-type
node_modules/co
node_modules/content-disposition
node_modules/content-type
node_modules/cookies
node_modules/debug
node_modules/deep-equal
node_modules/delegates
node_modules/depd
...

◯ pnpm

pnpm-lock.yaml ファイルの packages オブジェクトのキーの数
npm に近いけど YAML なのでパースが面倒
単純な方法でやるなら 「  /」 から始まる行の数

const data = require("yaml").parse(fs.readFileSync("./pnpm-lock.yaml").toString())

console.log(Object.keys(data.packages).length - 1)
console.log(Object.keys(data.packages).join("\n"))
42
/accepts@1.3.8
/cache-content-type@1.0.1
/co@4.6.0
/content-disposition@0.5.4
/content-type@1.0.5
/cookies@0.9.1
/debug@4.3.4
/deep-equal@1.0.1
/delegates@1.0.0
/depd@1.1.2
/depd@2.0.0
/destroy@1.2.0
yarn berry はプロジェクト外部からの実行がつらい
yarn の v2 以降で pnp を使うときの問題です
berry はただでさえ使いづらいところが多いのに これのせいで特定の用途の場合は yarn v1 を使うようにしてます

適当に pnp のプロジェクトを作って パッケージを入れてそれを使うようにします

yarn init -2
yarn add dayjs

package.json はこんな感じです

{
"name": "pkg",
"packageManager": "yarn@4.1.1",
"dependencies": {
"dayjs": "^1.11.10"
},
"type": "module"
}

[index.js]
import dayjs from "dayjs"

console.log(dayjs().format("YYYY-MM-DD"))

package.json のあるフォルダで

yarn node index.js

を実行すると問題なく動きます

次に この package.json があるフォルダを dir1/foo/bar に配置して dir1 をカレントディレクトリとして実行しようとします

yarn node foo/bar/index.js

になりますが これだと依存パッケージを解決できずエラーです
カレントディレクトリを変更するために --cwd をつけて

yarn --cwd foo/bar node index.js

としてもエラーです

scripts にして yarn run を使えば行けるかなと思って試してみました

{
"name": "pkg",
"packageManager": "yarn@4.1.1",
"dependencies": {
"dayjs": "^1.11.10"
},
"type": "module",
"scripts": {
"start": "node index.js"
}
}

yarn --cwd foo/bar run start

しかしこれもダメでした
yarn run の場合は cwd なしだと scripts を認識できず start が見つからないエラーになるものです
それが 認識できて node index.js を実行してからエラーになってるので うまく動いてくれても良さそうなのですけど

エラーを見るとこんな感じです

root@787bf525a896:/dir1# yarn --cwd foo/bar node index.js
yarn node v1.22.22
warning package.json: No license field
node:internal/modules/esm/resolve:854
throw new ERR_MODULE_NOT_FOUND(packageName, fileURLToPath(base), null);
^

気になるのは v1.22.22 になってるところです
--cwd を考慮せず 今のカレントディレクトリで package.json を探して packageManager から yarn のバージョンを決めていそうです
--cwd を処理する時点では 1.22.22 で固定された後なので 期待する 4.1.1 にならないという動きみたいです
そうなるとわざわざカレントディレクトリを移動させないといけないですし それをしたくないようなところでは使えません

なにかと yarn の pnp は使いづらいのですよね
yarn v1 で node_modules を使う場合は カレントディレクトリを気にせず実行する .js ファイルから node_modules を探すのでカレントディレクトリによる問題は起きないです

未だに yarn berry には完全移行できてなくて 問題点が多いので いっそ pnpm の方に移ったほうがいいのかなと最近思ってます
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 と書いて動かなかったことがありました
設定変えるなんてめったにないし ほとんどコピペなのですが やっぱりこういうのは気になるのですよね
ログって 1 ファイルにまとめたほうがいいの?
Fastify の作りだとロガーはひとつです
fastify.log.info や request.log.info を使ってログするとすべて同じ場所への出力です
pino の設定で変えれはするものの レベルが◯◯以上はこのファイルにログする みたいな考え方なのでアクセスログとエラーログを分けるみたいな使い方ではなさそうです

いつもは アクセスログ / 操作系のログ / エラー・警告類 は別ファイルにしていましたが 1 ファイルに保存して 見るときにフィルタするほうがいいのでしょうか?
用途が違うものは分けたい派なのですけど
Fastify では decorate で標準のとは別に request.access_log みたいなものを作れますが pino との統合をやってくれないので推奨される方法ではない気がするのですよね
React の ErrorBoundary でキャッチしたエラーがコンソールに残る
React でエラーの表示を個別にせずまとめてやりたいなと思って コンポーネントの中で個別に表示する代わりに throw して親の ErrorBoundary で受け取るようにしてみてました
動作的には期待するものだったのですが ErrorBoundary でエラーをハンドルしてるのにコンソールに Uncaught Error が出るのですよね

実装ミスでもあるのかなと思って探してもなさそうで ErrorBoundary 側に設定があるのかと探してみてもなさそうでした
単純なケースでもこういう動作になるので React 側でハンドルしてもさらに throw してるみたいです
↓のコードで「Uncaught Error: ERROR!」がコンソールに出ます

import React, { useState } from "react"

const App = () => {
return (
<ErrorBoundary>
<Component />
</ErrorBoundary>
)
}

class ErrorBoundary extends React.Component {
constructor(props) {
super(props)
this.state = { error: null }
}

static getDerivedStateFromError(error) {
return { error }
}

render() {
if (this.state.error) {
return (
<div>
<h1>Error!!</h1>
<p>{this.state.error.stack}</p>
</div>
)
}

return this.props.children
}
}

const Component = () => {
const [error, setError] = useState(false)

if (error) {
throw new Error("ERROR!")
}

const onClick = () => {
setError(true)
}

return (
<button onClick={onClick}>click</button>
)
}

export default App

ハンドル済みで期待する動作なのにエラーがコンソールに出るのは気持ち悪いです
単純に catch されなかったエラーをログに出さないならこれでできます

window.addEventListener("error", (event) => {
event.preventDefault()
})

しかしこうすると React の ErrorBoundary でハンドルしてないその他のエラーも非表示になって困ります
getDerivedStateFromError で Error オブジェクトにマークを付けて マークがついていれば preventDefault をしようかと考えたのですが window に自分でつけるハンドラのほうが先に処理が行われるようで うまくいかないです
先に React 側のエラーハンドルの処理をしたいので 中で queueMicrotask で遅延させると先にコンソールにエラーが表示されてしまいます

探してみると issue がありましたが 対応する予定はなさそうです
https://github.com/facebook/react/issues/15069

StrictMode やフック関係でも要望が多く上がっていても変えないスタンスなところはそのままになってたりしますしあまり期待できそうにないですね
同じポートを同時にリッスンできる?
同じポートを複数同時にリッスンできました
いつもは すでにリッスン中なら

Error: listen EADDRINUSE: address already in use 0.0.0.0:3000

みたいなエラーになります
なのにそうならずにリッスンできるケースがありました

アクセスしてみると あとからリッスンした方に接続されました
そのプロセスを止めると先にリッスンしていた方に接続されました
これは結構便利そうと思ってどうなってるのか調べていると リッスンするときのホスト名が関係してるようでした

これができる条件を探してみると 片方が Node.js の生の http モジュールを使って もう片方がライブラリを使う場合の組み合わせでした
ライブラリの内部を見ると ホスト名に localhost を指定してました
普通に http.createServer().listen() をホスト名を省略して使うと 0.0.0.0 になります
ここが違うとエラーにならず両方リッスンできるようです

つまり この listen1.js と listen2.js を同時に実行すると両方とも成功します

[listen1.js]
require("http").createServer((req, res) => {
res.end("OK1")
}).listen({ port: 3000, host: "0.0.0.0" }, () => console.log("listen1"))
.on("error", console.error)

[listen2.js]
require("http").createServer((req, res) => {
res.end("OK2")
}).listen({ port: 3000, host: "localhost" }, () => console.log("listen2"))
.on("error", console.error)

後勝ちなのかなと思ったのですが順番ではなく localhost が優先されてるようでした
また 127.0.0.1 でアクセスすると 0.0.0.0 の方に届くようです

C:\>curl http://localhost:3000
OK2
C:\>curl http://127.0.0.1:3000
OK1

ホスト名を変えればいくつでもできるのかなと思って試したのですが 存在しないホスト名だとエラーでした
ですが hosts に追加して foo を 127.0.0.1 にすれば foo でもリッスンできました
それならこの方法でバーチャルホストみたいなことができるの?と思ったのですが bar も追加してリッスンするとここはエラーでした

0.0.0.0 と localhost とその他の 3 つまでみたいです
foo と bar を両方 127.0.0.1 にすると両方リッスンできないなら localhost も同じになりそうですが Windows の場合 localhost は WSL 関係の都合で特殊扱いされてるのもかもしれません

Linux だとどうなるのかとやってみると 0.0.0.0 と localhost の 2 つを同時にリッスンすることはできませんでした
Windows が特殊なのかもしれません
npm run で npm が出す出力を消したい
JSON 形式でログを出力するスクリプトがあります

const log = (v) => console.log(JSON.stringify(v))

log({ value: 1 })
log({ value: 2 })

yarn berry で yarn start で実行すると

[root@60303402bf2f tmp]# yarn start
{"value":1}
{"value":2}

余分な出力がありません

しかし npm に置き換えると

[root@60303402bf2f tmp]# npm run start

> tmp@1.0.0 start
> node x.js

{"value":1}
{"value":2}

パッケージ名や 実行されるコマンドが出力されます
デバッグ時にはいいのですが 本番実行時に JSON 以外がログに混ざるのは避けたいので出力してほしくないです

--silent (-s) で消せるようです

[root@60303402bf2f tmp]# npm run start -s
{"value":1}
{"value":2}

この辺も yarn のほうが使いやすいなと思いましたが yarn v1 だとバージョンなどもっと色々出てました

[root@60303402bf2f tmp]# yarn run start
yarn run v1.22.22
$ node x.js
{"value":1}
{"value":2}
Done in 0.11s.

ウェブサーバーなど常駐プロセスの起動ならいらないですが CLI ツールとして使うなら実行時間が表示されるのは少し嬉しいかもしれません
イミュータブル型で循環参照ってどうするんだろう
リストとか木構造とかってオブジェクトに相互の参照が入ってることがよくあります
DOM でも children と parentElement だったり nextElementSibling と previousElementSibling だったりがあります

ミュータブルなものなら簡単に実現できますが イミュータブルなデータだと相互に参照を入れられない気がします

普通に JavaScript でやると

const child1 = {
parent: null,
children: [],
prev: null,
next: null,
}
const child2 = {
parent: null,
children: [],
prev: null,
next: null,
}
const parent = {
parent: null,
children: [],
prev: null,
next: null,
}

child1.next = child2
child1.parent = parent

child2.prev = child1
child2.parent = parent

parent.children.push(child1, child2)

これがイミュータブルだとすると 参照をいれるところがこうなります

const child1_new = { ...child1, next: child2, parent }
const child2_new = { ...child2, prev: child1_new, parent }

こうしたときに child2_new.prev に child1_new が入っていますが child1_new.next は古い child2 です
イミュータブルしかできない言語だとこういう構造は扱わないのでしょうか

直接参照をもたせず 間接的に持たせて自分で参照するならできそうですが 手間が多いです

const parent_id = Math.random()
const child1_id = Math.random()
const child2_id = Math.random()

const parent = {
parent: null,
children: [child1_id, child2_id],
prev: null,
next: null,
}
const child1 = {
parent: parent_id,
children: [],
prev: null,
next: child2_id,
}
const child2 = {
parent: parent_id,
children: [],
prev: child1_id,
next: null,
}

const refs = {
[parent_id]: parent,
[child1_id]: child1,
[child2_id]: child2,
}

console.log(child1 === refs[refs[child1.next].prev])
// true

更新することも考えるとあまりやりたい方法ではないです



イミュータブルな言語でやり方をググってみると 関数にして遅延評価することで実現してる例がありました

const parent = {
parent: null,
children: [() => child1, () => child2],
prev: null,
next: null,
}
const child1 = {
parent: () => parent,
children: [],
prev: null,
next: () => child2,
}
const child2 = {
parent: () => parent,
children: [],
prev: () => child1,
next: null,
}

console.log(child1 === child1.next().prev())
// true

たしかに実行時に参照先が評価される言語なら関数を作る時点で参照できなくても大丈夫そうです
ただこれでも変更は簡単ではなく どこかを書き換えると参照先も変更が必要になり全体を作り直すことになります
なのでそういうデータ構造は持たず単一方向にするのが普通で なぜ循環する参照が必要になるのか聞かれてるような感じでした
base64 の複数ファイルから元ファイルを復元する Python コード
Linux 環境で そこへのデータ送信は文字列のコピーのみできるところ
ssh 等を通したファイルのコピーは不可
その環境からインターネット接続は不可
だけどそこにファイル持っていきたい

ファイルを zip にまとめて base64 文字列で複数回に分けて貼り付けてから Linux 環境で復元する
PHP や Node.js は入れられないけど Python はデフォルトで入ってるので使える
Python のコードは文字列なのでプログラム自体を持っていくのも問題なくできる

そのときの復元用コード

import argparse
import base64
from pathlib import Path

parser = argparse.ArgumentParser()
parser.add_argument("directory")
parser.add_argument("output")
args = parser.parse_args()

directory = Path(args.directory)
files = list(directory.glob("*.part"))
files = sorted(files, key=lambda x: x.name)

if len(files) == 0:
print("対象のファイルが見つかりません")
exit(1)
else:
print("対象ファイル:")
print([file.name for file in files])

b64str = ""

for file in files:
b64str += file.read_text()

result = base64.b64decode(b64str)

with open(args.output, "wb") as f:
f.write(result)

print("完了しました")

適当なフォルダ (dir1) に 01.part, 02.part, ... のようにファイルを作って 次のコマンドを実行

python3 b64restore.py dir1 output.zip

dir1 の中の .part のファイルをソートして結合して base64 デコードして output.zip に出力



考えてみると 全体をメモリに一旦乗せるならファイルに保存する必要ないし 標準入力に貼り付けるようにしました

import sys
import base64

text = sys.stdin.read()
result = base64.b64decode(text)
sys.stdout.buffer.write(result)
python3 b64restore.py > data.zip

Base64 文字列を全部貼り付けたら Ctrl-D で EOF を送って終了させます
ssh が切れても後段の ssh は残ってる
ローカル

サーバー A → サーバー C

サーバー B

こんな感じで A を通して B や C に ssh で接続しています

ssh 接続したまましばらくすると自動で切断されてることがあります
broken pipe みたいなエラーが表示されるやつです

接続してることを完全に忘れてることもあるので長時間なら切断されることは別にいいのですが 予想外のところで問題がありました

ローカル → サーバー A

ここの接続は切れているのに

サーバー A → サーバー B

の接続は維持されてるようです
ps でプロセスを見ると ssh 接続されています

また サーバー B にログインした状態で less やプログラミング言語の REPL などインタラクティブなものを使っているとそのプロセスも残っています
psql や mysql コマンドを使ってると DB とのコネクションが残ります
これは色々と問題です
ファイルを開いたままになっていたり メモリをずっと確保したままになっていたり

とりあえず ps でプロセスリストを見て それらしいものを kill していきます
困るのがどれがどれかわからないのですが ps の STAT 列に + がついているプロセスはフォアグラウンドプロセスグループに属してるらしいのでこれを目印にできます
ログインしてユーザーが操作してコマンドを実行すればフォアグラウンドなので + がついてるもので現在ログインして操作中でなければ kill していいはずです
自分しかログインしない環境なら簡単です

でも根本的な解決としてプロセスが残ってほしくないんですよね
A の ssh 接続設定にタイムアウトを入れる感じでしょうか
tmux とか入れれるならそれを使えば セッションを復元できますが 踏み台用途のところはあまり色々インストールしたくないのと tmux 系は不便もあるのであまり使ってないんですよね
関数が受け取る引数の数で動作変えるのやめてほしい
一部のライブラリでは コールバック関数として渡した関数が受け取る引数の数で動作が変わります
こういうの

fn(() => {})
fn((a) => {})
fn((a, b) => {})

fn 側ではこういう感じで引数の数を見ています

const fn = (callback) => {
switch (callback.length) {
case 0: {
console.log("0")
return callback()
}
case 1: {
console.log("1")
return callback(1)
}
case 2: {
console.log("2")
return callback(1, 2)
}
}
}

これすごく分かりづらく感じるのでやめてほしいです
関数の引数の数ってあまりあてにならないです

console.log(((a, b = 1, c) => {}).length)
// 1

console.log(((...args) => {}).length)
// 0

デフォルト引数が設定された最初の引数より前の引数の数しかカウントされません
可変長引数は 0 になります
関数をラップして引数をそのまま渡して追加処理をするようなケースは可変長引数で受け取ることが多いのでそこで問題がおきます

fn((a, b) => {})
// 1

// ↑を↓にすると

const wrap = org => (...args) => {
// beforeSomething()
const result = org(...args)
// afterSomething()
return result
}

fn(wrap((a, b) => {}))
// 0

使われどころはコールバック関数を受け取るかどうかで コールバックと Promise のどちらを使うか分岐するというのが多い気がします
コールバック関数を受け取らないなら Promise を返す関数で そうでないならコールバック関数を呼び出すことで終了を呼び出し元に伝えるので 両方に対応するならこういう方法になります

const fn = async (callback) => {
switch (callback.length) {
case 0: {
console.log("0")
await callback()
break
}
case 1: {
console.log("1")
await new Promise(resolve => callback(resolve))
break
}
}
// after callback process
}

引数の数を見ないでとりあえず コールバックの関数を渡しておいて 返り値の Promise または渡した関数が呼び出されたかで判断でしてくれるといいのですが これの場合も使う側が async 関数にしてコールバック関数を使おうとするとうまく動かなかったりするのですよね

const fn = async (callback) => {
const { promise, resolve } = Promise.withResolvers()
const ret = callback(resolve)
const promises = (ret instanceof Promise) ? [ret, promise] : [promise]
await Promise.any(promises)

// after callback process
}

fn(async (next) => {
await something1()
something2(next)
})

fn に渡すのが async 関数なので something2 を待たず resolve されてしまって ここで fn は next の呼び出しを待たずに終わったとみなしてしまいます
無理にまとめず別の関数に分けたほうがいいと思うのですけどね

他には なにかの機能を引数経由で提供していて 引数として受け取らないならその機能は使われないので準備するのをスキップするとかでしょうか

const fn = (callback) => {
switch (callback.length) {
case 0: {
console.log("0")
return callback()
}
case 1: {
console.log("1")
const util = new Utility()
return callback(util)
}
}
}

引数を受け取らないということは util を使うことはないので new Utility() をスキップできます

JavaScript では引数の数があってる必要はないので 関数の引数の数は気にせず扱ってほしいですね
git なしで yarn berry の init をエラーなく実行する
特に役立たない話

yarn init で berry の初期化をするとき git でリポジトリが自動で作られます
git が入っていないとエラーが起きます

[root@60303402bf2f foo]# yarn init -2
➤ YN0000: You don't seem to have Corepack enabled; we'll have to rely on yarnPath instead
➤ YN0000: Downloading https://repo.yarnpkg.com/4.1.1/packages/yarnpkg-cli/bin/yarn.js
➤ YN0000: Saving the new release in .yarn/releases/yarn-4.1.1.cjs
➤ YN0000: Done with warnings in 0s 140ms
➤ YN0000: · Yarn 4.1.1
➤ YN0000: ┌ Resolution step
➤ YN0000: └ Completed
➤ YN0000: ┌ Fetch step
➤ YN0000: └ Completed
➤ YN0000: ┌ Link step
➤ YN0000: └ Completed
➤ YN0000: · Done in 0s 127ms
Internal Error: Process git failed to spawn
at ChildProcess.<anonymous> (/foo/.yarn/releases/yarn-4.1.1.cjs:149:5568)
at ChildProcess.emit (node:events:518:28)
at ChildProcess._handle.onexit (node:internal/child_process:292:12)
at onErrorNT (node:internal/child_process:484:16)
at process.processTicksAndRejections (node:internal/process/task_queues:82:21)
error Command failed.
Exit code: 1
Command: /usr/bin/node
Arguments: /usr/local/bin/yarn init -2
Directory: /foo
Output:

info Visit https://yarnpkg.com/en/docs/cli/init for documentation about this command.

という感じ

git が入ってない環境で使うこともありますし git の初期化はスキップしたいです
ですが オプションに git の初期化をスキップする設定が見当たらないです

しかし .git フォルダがすでにあると初期化処理はスキップされるようになっています
https://github.com/yarnpkg/berry/blob/%40yarnpkg/cli/4.1.1/packages/plugin-init/sources/commands/init.ts#L252

つまり

mkdir .git
yarn init -2
rmdir .git

とすればエラーはなくなります

ただ上のリンクのソースコードを見ても分かる通り git の初期化処理は最後の処理です
エラーで途中終了しても init の処理は終わってるのでエラーを消しても特に何も変わりません

ログにエラーを出したくないということがあれば使えます
エラーが無い場合は Done の行までの出力になります
Promise をまとめる系関数の動きを確認できる画面
https://nexpr.gitlab.io/public-pages/promise-merge/

Promise.all とか Promise.any とか Promise をまとめる関数があります
よく使う all はいいのですが たまに使うものだとどれだっけ?ってなることがあります
また誰かに伝えるときに説明ってしづらかったりするのですよね

ボタンを押して resolve/reject して結果が見れるのがあるとわかりやすいかなと思って確認できる画面を作ってみました

使い方は見たままですが……
一番上のボタンで all/any/race/allSettled を選びます
固定で 3 つの Promise が作られて それぞれの resolve/reject ボタンを押せます
押すとその Promise の状態が更新されます
3 つの Promise を指定の方法 (all や race) でまとめた結果の Promise の状態が一番下に出ます
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]"}
CSSStyleSheet から CSS の文字列を取り出したい
const css = new CSSStyleSheet()
css.replaceSync(text)

これで作った CSSStyleSheet オブジェクトから CSS のテキストを取り出したいです

目的は text の変数に入ってる CSS を有効な形に修正することです
HTML や CSS は構文がおかしくてもブラウザ側で不正なものは適当に修正してくれるのでその結果がほしいです

HTML なら

const div = document.createElement("div")
div.innerHTML = `<div><p>aa`
div.innerHTML
// '<div><p>aa</p></div>'

というふうにできるのですが CSS ってこういうのがないです
cssRules にルールが入ってますが 色々分かれてしまっています
いい感じに取り出すのは難しそうかなと思ってたのですが 単純に個々の cssRules の cssText を取得して結合すればいい感じになりました

「@」 を使ったものやネストなどに不安がありましたが MDN にあるサンプルコードを適当にコピペして試した感じではほぼ完全に復元できました
以下が確認済みです

@font-face
@keyframes
@media
@page
@container
@layer
@namespace
@counter-style
@supports
@property
@scope

@import は replace / replaceSync で使えないので確認してません
@charset は出力されなかったですがパース後には元の文字コードを保持する必要がないのでそういうものなのだと思います

もしかするとマイナーなところで出力できない部分があるかもしれないですが 基本的なものでは問題なく使えるレベルだと思います

const formatCSS = (text) => {
const css = new CSSStyleSheet()
css.replaceSync(text)
return [...css.cssRules].map(rule => rule.cssText).join("\n")
}

少し気になったところでは ネストするとインデントがおかしくなります

console.log(formatCSS(`
.foo {
.bar {
.baz {
}
}
}
`))
.foo {
.bar {
.baz { }
}
}

読むことを目的としてないので実害はないですが ネストの対応はこの辺まで完璧じゃないようですね

ちなみにこの方法 自分で作った CSSStyleSheet 以外の style タグや link タグでも使えました
link タグの場合は取得する CSS ファイルが同じオリジンである必要があります

const sheetToCSS = (sheet) => {
return [...sheet.cssRules].map(rule => rule.cssText).join("\n")
}

console.log(sheetToCSS(document.querySelector("style").sheet))
console.log(sheetToCSS(document.querySelector(`link[rel="stylesheet"]`).sheet))
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 のためだけに名前をつける必要があるのかはわかりませんけど 一時的にどれなのかわからないからつけるというときには良さそうです
PowerShell でコマンドや変数の文字色を変える
ターミナルの黒画面に飽きて色を変えてみてますが コマンドの出力に色がついてるところだと 色が被ったり影響が出るのですよね
Windows Terminal だと設定の中にテーマを設定するところがあって そこで文字色や背景色を設定できます

wt-color

通常の文字色と背景色以外にも 黒や赤などの色も設定できます
背景色の上に表示してはっきりと文字が見えるように調整できます
色名は参考のものでしかないので 赤なのに実際の色は青みたいなこともできます
アプリケーション側で赤色って指定されたときに画面に出す色を設定する感じです
これのお陰で色を変えてもきれいに見れるように調整できます

これだけでもいいのですが PowerShell 側で色を付けてる部分は PowerShell の設定で変更できます
ユーザーが入力する部分で コマンドは黄色で変数は緑色みたいなのです

ps-color1

ここでどの色を出すかは PowerShell 側に設定があって Get-PSReadLineOption で取得できます

PS C:\Users\WDAGUtilityAccount> Get-PSReadLineOption

EditMode : Windows
AddToHistoryHandler :
HistoryNoDuplicates : True
HistorySavePath : C:\Users\WDAGUtilityAccount\AppData\Roaming\Microsoft\Windows\PowerShell\PSRea
dLine\ConsoleHost_history.txt
HistorySaveStyle : SaveIncrementally
HistorySearchCaseSensitive : False
HistorySearchCursorMovesToEnd : False
MaximumHistoryCount : 4096
ContinuationPrompt : >>
ExtraPromptLineCount : 0
PromptText : >
BellStyle : Audible
DingDuration : 50
DingTone : 1221
CommandsToValidateScriptBlockArguments : {ForEach-Object, %, Invoke-Command, icm...}
CommandValidationHandler :
CompletionQueryItems : 100
MaximumKillRingCount : 10
ShowToolTips : True
ViModeIndicator : None
WordDelimiters : ;:,.[]{}()/\|^&*-=+'"–—―
CommandColor : "$([char]0x1b)[93m"
CommentColor : "$([char]0x1b)[32m"
ContinuationPromptColor : "$([char]0x1b)[37m"
DefaultTokenColor : "$([char]0x1b)[37m"
EmphasisColor : "$([char]0x1b)[96m"
ErrorColor : "$([char]0x1b)[91m"
KeywordColor : "$([char]0x1b)[92m"
MemberColor : "$([char]0x1b)[97m"
NumberColor : "$([char]0x1b)[97m"
OperatorColor : "$([char]0x1b)[90m"
ParameterColor : "$([char]0x1b)[90m"
SelectionColor : "$([char]0x1b)[30;47m"
StringColor : "$([char]0x1b)[36m"
TypeColor : "$([char]0x1b)[37m"
VariableColor : "$([char]0x1b)[92m"

◯◯Color になってるところは実際の色で表示されています
コマンドの色を変更してみます

Set-PSReadLineOption -Colors @{ "Command"="$([char]0x1b)[33m" }

ps-color2

エスケープとして出力する文字が変わるものなので 過去ログは変わらず変更後の行だけ変わります
33 や 93 の数字が色に対応してます
エスケープの詳細は wikipedia 参照です
https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters

38 や 48 を使うと 8bit/24bit で色を絶対値指定できますが これを使うと色名を通さないのでユーザーが変更できないです
そうなると背景色等のカスタマイズと相性が悪いのであまり使わないほうが良さそうです



ちなみに上のは標準の PowerShell でしたが 新しい PowerShell だと少しプロパティが追加されていて エスケープの表記方法も変わっていました
「$([char]0x1b)」 → 「`e」 とシンプルになってます

PowerShell 7.4.1
PS C:\Users\WDAGUtilityAccount> Get-PSReadLineOption

EditMode : Windows
AddToHistoryHandler : System.Func`2[System.String,System.Object]
HistoryNoDuplicates : True
HistorySavePath : C:\Users\WDAGUtilityAccount\AppData\Roaming\Microsoft\Windows\PowerShell\PSRea
dLine\ConsoleHost_history.txt
HistorySaveStyle : SaveIncrementally
HistorySearchCaseSensitive : False
HistorySearchCursorMovesToEnd : False
MaximumHistoryCount : 4096
ContinuationPrompt : >>
ExtraPromptLineCount : 0
PromptText : {> }
BellStyle : Audible
DingDuration : 50
DingTone : 1221
CommandsToValidateScriptBlockArguments : {ForEach-Object, %, Invoke-Command, icm…}
CommandValidationHandler :
CompletionQueryItems : 100
MaximumKillRingCount : 10
ShowToolTips : True
ViModeIndicator : None
WordDelimiters : ;:,.[]{}()/\|!?^&*-=+'"–—―
AnsiEscapeTimeout : 100
PredictionSource : HistoryAndPlugin
PredictionViewStyle : InlineView
TerminateOrphanedConsoleApps : False
CommandColor : "`e[93m"
CommentColor : "`e[32m"
ContinuationPromptColor : "`e[37m"
DefaultTokenColor : "`e[37m"
EmphasisColor : "`e[96m"
ErrorColor : "`e[91m"
InlinePredictionColor : "`e[97;2;3m"
KeywordColor : "`e[92m"
ListPredictionColor : "`e[33m"
ListPredictionSelectedColor : "`e[48;5;238m"
ListPredictionTooltipColor : "`e[97;2;3m"
MemberColor : "`e[37m"
NumberColor : "`e[97m"
OperatorColor : "`e[90m"
ParameterColor : "`e[90m"
SelectionColor : "`e[30;47m"
StringColor : "`e[36m"
TypeColor : "`e[37m"
VariableColor : "`e[92m"

またスタイルに関連する情報が入っている変数 $PSStyle が存在します

PS C:\Users\WDAGUtilityAccount> $psstyle

Reset : `e[0m
BlinkOff : `e[25m
Blink : `e[5m
BoldOff : `e[22m
Bold : `e[1m
DimOff : `e[22m
Dim : `e[2m
Hidden : `e[8m
HiddenOff : `e[28m
Reverse : `e[7m
ReverseOff : `e[27m
ItalicOff : `e[23m
Italic : `e[3m
UnderlineOff : `e[24m
Underline : `e[4m
StrikethroughOff : `e[29m
Strikethrough : `e[9m
OutputRendering : Host
Formatting.FormatAccent : `e[32;1m
Formatting.ErrorAccent : `e[36;1m
Formatting.Error : `e[31;1m
Formatting.Warning : `e[33;1m
Formatting.Verbose : `e[33;1m
Formatting.Debug : `e[33;1m
Formatting.TableHeader : `e[32;1m
Formatting.CustomTableHeaderLabel : `e[32;1;3m
Formatting.FeedbackName : `e[33m
Formatting.FeedbackText : `e[96m
Formatting.FeedbackAction : `e[97m
Progress.Style : `e[33;1m
Progress.MaxWidth : 120
Progress.View : Minimal
Progress.UseOSCIndicator : False
FileInfo.Directory : `e[44;1m
FileInfo.SymbolicLink : `e[36;1m
FileInfo.Executable : `e[32;1m
FileInfo.Extension : .zip,.tgz,.gz,.tar,.nupkg,.cab,.7z,.ps1,.psd1,.psm1,.ps1xml
Foreground.Black : `e[30m
Foreground.BrightBlack : `e[90m
Foreground.White : `e[37m
Foreground.BrightWhite : `e[97m
Foreground.Red : `e[31m
Foreground.BrightRed : `e[91m
Foreground.Magenta : `e[35m
Foreground.BrightMagenta : `e[95m
Foreground.Blue : `e[34m
Foreground.BrightBlue : `e[94m
Foreground.Cyan : `e[36m
Foreground.BrightCyan : `e[96m
Foreground.Green : `e[32m
Foreground.BrightGreen : `e[92m
Foreground.Yellow : `e[33m
Foreground.BrightYellow : `e[93m
Background.Black : `e[40m
Background.BrightBlack : `e[100m
Background.White : `e[47m
Background.BrightWhite : `e[107m
Background.Red : `e[41m
Background.BrightRed : `e[101m
Background.Magenta : `e[45m
Background.BrightMagenta : `e[105m
Background.Blue : `e[44m
Background.BrightBlue : `e[104m
Background.Cyan : `e[46m
Background.BrightCyan : `e[106m
Background.Green : `e[42m
Background.BrightGreen : `e[102m
Background.Yellow : `e[43m
Background.BrightYellow : `e[103m

Set-PSReadLineOption で色を変更するときに色に対応する数字を覚えなくてもここから参照できて便利です

ここの変数を直接更新できないかなと試しましたが ReadOnly プロパティと言われてダメでした

PS C:\Users\WDAGUtilityAccount> $PSStyle.Background.Red = $PSStyle.Foreground.Red
InvalidOperation: 'Red' is a ReadOnly property.
PS C:\Users\WDAGUtilityAccount> $PSStyle | Get-Member background

TypeName: System.Management.Automation.PSStyle

Name MemberType Definition
---- ---------- ----------
Background Property System.Management.Automation.PSStyle+BackgroundColor Background {get;}

PS C:\Users\WDAGUtilityAccount> $PSStyle.Background | Get-Member black

TypeName: System.Management.Automation.PSStyle+BackgroundColor

Name MemberType Definition
---- ---------- ----------
Black Property string Black {get;}

getter しか用意されてないですね
PowerShell 起動時に実行されるスクリプト
これまで気にしたこと無かったですが PowerShell でも bash の .bashrc などのように起動時に実行するスクリプトが登録できるようです

ファイルの場所は決まっていて $profile 変数に入ってます

PS C:\Users\WDAGUtilityAccount> $PROFILE | select *

AllUsersAllHosts : C:\Program Files\PowerShell\7\profile.ps1
AllUsersCurrentHost : C:\Program Files\PowerShell\7\Microsoft.PowerShell_profile.ps1
CurrentUserAllHosts : C:\Users\WDAGUtilityAccount\Documents\PowerShell\profile.ps1
CurrentUserCurrentHost : C:\Users\WDAGUtilityAccount\Documents\PowerShell\Microsoft.PowerShell_profile.ps1
Length : 81

AllUsers だと PowerShell の実行ファイルと同じ場所で CurrentUser だとそのユーザーのドキュメントフォルダの中みたいです
ユーザーは Sandbox ユーザーのものです
ユーザーフォルダと違ってドキュメントフォルダというのが少し微妙な感じがします

インストールせずに zip 版を使うと AllUsers は展開先になります

PS C:\> $PROFILE | select *

AllUsersAllHosts : C:\Users\WDAGUtilityAccount\Desktop\PowerShell-7.4.1-win-x64\profile.ps1
AllUsersCurrentHost : C:\Users\WDAGUtilityAccount\Desktop\PowerShell-7.4.1-win-x64\Microsoft.PowerShell_profile.ps1
CurrentUserAllHosts : C:\Users\WDAGUtilityAccount\Documents\PowerShell\profile.ps1
CurrentUserCurrentHost : C:\Users\WDAGUtilityAccount\Documents\PowerShell\Microsoft.PowerShell_profile.ps1
Length : 81

デフォルトで入ってる古い PowerShell の場合はこうでした

PS C:\Users\WDAGUtilityAccount> $PROFILE | select *

AllUsersAllHosts : C:\Windows\System32\WindowsPowerShell\v1.0\profile.ps1
AllUsersCurrentHost : C:\Windows\System32\WindowsPowerShell\v1.0\Microsoft.PowerShell_profile.ps1
CurrentUserAllHosts : C:\Users\WDAGUtilityAccount\Documents\WindowsPowerShell\profile.ps1
CurrentUserCurrentHost : C:\Users\WDAGUtilityAccount\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1
Length : 88

CurrentUser の方がドキュメントフォルダの中なのは同じですが 最近の方だと PowerShell だったところが WindowsPowerShell になっていて別フォルダでした
遅延初期化処理が非同期処理のときにうまく動かなくて困った話
うまく動かないところがあって 原因を見つけるのに苦戦しました
たまにしか使わない処理なので最初の使用時に初期化処理を行うようにしてるものです

イメージ

const execute = async (args) => {
if (!instance) {
await setupInstance()
}

return instance.run(args)
}

初期化処理に非同期処理が含まれるので初期化処理全体が非同期処理になっています
2 回目の呼び出しが 1 回目の直後だと インスタンスはあるものの初期化処理が完全に終わってないので instance.run の実行はできるのに何も行われないみたいな動きになってました

インスタンスの初期化が終わらないと 内部のリスナが設定されていなくてイベントを起こしても何も起きないという状況でした
ここが undefined のプロパティ参照や非関数の実行など実行時にエラーになってくれていれば簡単に原因がわかったのですけどね

インスタンス側に ready プロパティを用意して初期化後に解決する Promise を入れておくなどの対処が必要でした

const execute = async (args) => {
if (!instance) {
await setupInstance()
}
await instance.ready

return instance.run(args)
}

ただ この最初の呼び出しで初期化する方法だと instance.run が同期的なのに execute の処理が非同期処理になってしまうのですよね
setupInstance が同期処理だとそもそも発生しない問題ですし あちこちが非同期処理になると不便なところも多いです
php -S はリクエストをひとつずつしか処理してなかった
久々に見かけた PHP の話題で PHP の組み込みサーバーは並列処理をしてくれないというのを見かけました
え そうだっけ?
たまにしか使わないものの あまり遅かった記憶がないです
あまり重たい処理をさせないのと ほとんどは静的ファイルのサーブ目的なので一つずつ処理しても遅いと感じてなかっただけなのかもしれません

せっかくので試してみました
PHP ファイルは sleep を入れて時間がかかるようにします

[a.php]
<?php

sleep(5);
echo 'ok';

Node.js を使って適当にリクエストを送ります
レスポンスが来たらリクエストの送信時刻(s)とレスポンスの受信時刻(e)をあわせて表示してます

> request()
{
s: 2024-03-08T14:59:55.767Z,
e: 2024-03-08T15:00:00.775Z
}
> request()
2024-03-08T15:00:03.111Z
{
s: 2024-03-08T15:00:03.111Z,
e: 2024-03-08T15:00:08.128Z
}
> request()
> request()
> request()
> request()
{
s: 2024-03-08T15:00:11.991Z,
e: 2024-03-08T15:00:16.997Z
}
{
s: 2024-03-08T15:00:12.783Z,
e: 2024-03-08T15:00:22.000Z
}
{
s: 2024-03-08T15:00:13.583Z,
e: 2024-03-08T15:00:27.000Z
}
{
s: 2024-03-08T15:00:14.222Z,
e: 2024-03-08T15:00:32.001Z
}

連続してリクエストしたときに 2 つめは 10 秒後というふうに 5 秒ずつ遅れてきてるので 1 つずつ処理されてるようですね
ちなみにサーバー側のログ

[Fri Mar  8 14:59:55 2024] 127.0.0.1:33850 Accepted
[Fri Mar 8 15:00:00 2024] 127.0.0.1:33850 [200]: GET /a.php
[Fri Mar 8 15:00:00 2024] 127.0.0.1:33850 Closing
[Fri Mar 8 15:00:03 2024] 127.0.0.1:33852 Accepted
[Fri Mar 8 15:00:08 2024] 127.0.0.1:33852 [200]: GET /a.php
[Fri Mar 8 15:00:08 2024] 127.0.0.1:33852 Closing
[Fri Mar 8 15:00:11 2024] 127.0.0.1:33854 Accepted
[Fri Mar 8 15:00:16 2024] 127.0.0.1:33854 [200]: GET /a.php
[Fri Mar 8 15:00:16 2024] 127.0.0.1:33854 Closing
[Fri Mar 8 15:00:16 2024] 127.0.0.1:33856 Accepted
[Fri Mar 8 15:00:16 2024] 127.0.0.1:33858 Accepted
[Fri Mar 8 15:00:21 2024] 127.0.0.1:33856 [200]: GET /a.php
[Fri Mar 8 15:00:21 2024] 127.0.0.1:33856 Closing
[Fri Mar 8 15:00:21 2024] 127.0.0.1:33860 Accepted
[Fri Mar 8 15:00:26 2024] 127.0.0.1:33858 [200]: GET /a.php
[Fri Mar 8 15:00:26 2024] 127.0.0.1:33858 Closing
[Fri Mar 8 15:00:31 2024] 127.0.0.1:33860 [200]: GET /a.php
[Fri Mar 8 15:00:31 2024] 127.0.0.1:33860 Closing

レスポンスを返して Closing した後に次のが Accepted になっています
本番向けじゃないと言う注意書きがありましたが こういう理由だったのですね
CSS の適用範囲を制限したい
こんな HTML があって

<h1>H1</h1>
<p>p</p>

<div class="article">
<h1>a.H1</h1>
<p>a.p</p>
</div>

.article の中にだけスタイルを当てたいです
ただしそのスタイルは自分で書くものではなくユーザー入力です
なのでセレクタで .article の中だけが対象になっている保証はないです

最近は CSS をネストできるので ユーザー CSS の構文が正しいことを確認したあとに

const style = `
.article {
${original_style}
}
`

みたいにすればいいかなと思ったりもしました
ですが

.article {
body:has(&) {
background: black;
}
}

とすれば body にスタイルを当てられます

ShadowDOM を使ったほうがいいのかなと思ったものの slot の中にはスタイルが当たりません

<script type="module">
const css = new CSSStyleSheet()
css.replaceSync(`
p { color: red; border: 1px solid pink; }
`)

const article = document.querySelector(".article")
const root = article.attachShadow({ mode: "open" })
root.adoptedStyleSheets = [css]
root.innerHTML = `<slot/>`
</script>

<h1>H1</h1>
<p>p</p>

<div class="article">
<h1>a.H1</h1>
<p>a.p</p>
</div>

slot を使わず ShadowDOM の中に .article の子要素を持ってきてしまえばスタイルは当たりますが あまり良いやり方に思えないんですよね

<script type="module">
const css = new CSSStyleSheet()
css.replaceSync(`
p { color: red; border: 1px solid pink; }
`)

const article = document.querySelector(".article")
const root = article.attachShadow({ mode: "open" })
root.adoptedStyleSheets = [css]
root.replaceChildren(...article.childNodes)
</script>

<h1>H1</h1>
<p>p</p>

<div class="article">
<h1>a.H1</h1>
<p>a.p</p>
</div>

(追記) (コメントありがとうございます)
@scope でもネストと同じで突破できそうと思ってましたが いろいろ試してみたところ大丈夫そうでした
& はスコープ開始のセレクタを書くのと同じらしいですが そもそもスコープ内しか対象にしない機能なので body などの外側の要素を選択しても効果がないです

あとは } が多くて @scope を抜けてしまうみたいのを許可しなければ大丈夫そうです
ネストのときは すぐにダメそうとわかって あまり考えてなかったですが CSS って HTML の innerHTML みたいな感じで CSSStyleSheet から完全な CSS 文字列を簡単に作れないんですよね
標準機能だけで構文エラーを修正したものを作れるといいのですけど
この辺はライブラリに任せたほうがいいかもしれません

続き → CSSStyleSheet から CSS の文字列を取り出したい
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 で確認しましたがどれもこうなるようです
「/」 だけキャッシュされる
キャッシュがなんかおかしいなーと思って調べたら 「/」 のページだけ長期キャッシュになってました
静的ファイル置き場のファイルはこんな感じ

index.html
assets/index-abcdef.js
assets/...

SPA で 404 のときに index.html を返します
静的ファイルのサーブでは cache-control が設定されています

「/」 のときは静的ファイルのサーブとして index.html がサーブされて 「/foo」 などではエラーハンドラの処理として index.html がサーブされていました
結果 「/」 でだけキャッシュが有効になってました

index.html は静的ファイル置き場に置かないほうがいいのでしょうか
ファイルごとにヘッダー設定ができるものなら .js や .css みたいなリソースファイルの拡張子の場合だけキャッシュさせたり assets みたいなフォルダ単位でキャッシュさせるとかすればいいのですが 全体設定しかできないものだったので困りました
iOS の PWA
ちょっと前の情報ですが最近おすすめで見かけて知ったので

https://gigazine.net/news/20240209-apple-pwa-support-remove/
https://gigazine.net/news/20240216-ios-17-4-removes-web-apps-pwa/
https://gigazine.net/news/20240302-apple-pwa-support-not-remove/

EU のみ関係ある話で iOS で PWA 機能が削除される予定だったけどやっぱり撤回となったようです

PWA なんて使わないし最近は全然見ないし 全体的に終わる傾向なのかなと思ったものの EU の法律都合の特殊なものだったみたいです
実際 PWA ってそんな使われてるのでしょうか
出てきた頃は結構色々なページでインストールしますか?とか通知が出てきて邪魔でしたが最近はそういうのは全然見ないです
オフラインで使えるのが利点ではありますが 今の時代オフラインなことってそうそうないです
田舎の方に行くときに道中電波が届かないところはありますが そのタイミングでウェブアプリを使うなんてめったに無いと思います
地図アプリとかならともかく一般のアプリだと要らない機能ですよね

スマホで PWA を削除するとオフラインでも使えるようにしたい場合は 独自のネイティブアプリにするしかなくなります
Apple 的にはそのほうがアプリ登録だったり 課金時の手数料で儲かるから ウェブよりもネイティブアプリに誘導するためにやるのかなと考えたりもしましたが EU の法律に合わせるのが大変だからという理由だったようです
あのへんは面倒そうですからね

結局撤回されたみたいなのでこれまでと変わらないようです
Google Sheets って Safari 対応してないんだ
iPad で Google Sheets のリンクを開いたら 「お使いのブラウザのバージョンはサポートが終了しました」 ってメッセージが出てきた
アプリ版を使えってことかもだけど Google でも Safari のサポートしないんだなーって思った
競合とはいえ サービスのユーザーを増やす機会なのに
Google Sheets みたいな複雑な機能になるとやっぱり Safari の対応はかなりつらいものなのかな
最近は以前より Safari で動かないが減ってきたように思うけど
ページがロードする JavaScript のサイズ
色々なページでロードされる JavaScript のサイズを計測して 数 MB や数十 MB もあって多すぎるという内容の記事
https://tonsky.me/blog/js-bloat/

たしかに重すぎますよね
SPA の全ページを最初にロードすることで数 MB なら それ以降のダウンロードは発生しないので許容範囲ですが 1 ページだけで数 MB もダウンロードしてほしくないですね
ライブラリやビルドツールがファイルサイズを考慮してないものが多いですからね
サイズの小ささにこだわるライブラリもあるにはあるのですが マイナー気味であまり使われてないですし

ライブラリを使うほどでもないところでもとりあえずライブラリ入れておくと言う考えの人も多いです
ライブラリは入れる数が増えるほど 将来的にバージョンの問題などでメンテが面倒になるので無くていいものは入れないに越したことはないです
ライブラリは色々な用途を考慮して作られるので本当にそこで必要なものに比べたら余計な機能が多くあります
1 人で 1 日もかからず作れる程度のものならライブラリに頼らず そこ専用に自作したほうがムダがなくていいと思います

それよりも右上のダークモードの切り替えの挙動が思ってたのと違ったのが印象的でした
フロントエンドの移り変わりの速さについて
今週ネットで見かけた記事がすごく同意できるなと思ったので
これ
https://zenn.dev/nekoya/articles/12ebf1fd916db2

サーバーサイドだと言語が PHP とか Java とか Ruby とか色々あるのが フロントエンドだと全部 JavaScript なので フロントエンドの全部を追おうとするのはサーバーサイドで全部の言語を対象にするようなものなんですよね
生の JavaScript, React, Vue などの選択を言語と考えればそこまで速くないと思います
React も Vue も Angular をやってさらに新しく出てきたフレームワークも試すというのを PHP も Java も Ruby もやってさらに Go や Rust みたいな言語も試すと考えたらフロントの方が簡単そうに思います

以前は JavaScript の言語自体の変化が大きかったですが 最近は変化が少なく退屈なくらいです
最近はほとんどフレームワーク周りの変化ですが それもほぼ React 一強です
以前は Vue を使っていた人も Vue3 から React に移ったという話が多いです
新しく出てきたのが勢力をのばすかなと思ってましたが特にそれらしい気配が感じられません
どこをみても React の Next.js ばかりです

あと 元記事にもある話ですが 未だにフロントをおまけ程度に考えてる人って結構多いのですよね
ウェブ関連のことなのでウェブ上に記事があふれるのは自然ですし そこの記事を真に受けて今は◯◯を使わないといけないんだとか考えるとついていくのが大変だとは思います
SNS とかチャットツールとか YouTube とかゲームみたいなサービスであれば JavaScript による処理が必要になってきますが 単なるウェブサイトには別に JavaScript はなくてもいいです
HTML と CSS だけで SPA にせずリンクだっていいわけです
静的なページであっても JavaScript で作るとかはやりたい人が勝手にやってるだけで みんながやる必要はないので やりたくないならやらなくていいと思います

jQuery はもう古いと言われて何年も経ってますが 最近 4.0beta が出て もうすぐ 4.0 が出そうで まだメンテされています
jQuery で不便を感じたことがないって規模のものに React などを無理して入れる必要はないと思います
むしろ入れても面倒ごとが増えるだけです

とはいえ 新しくフロント周りをやりたくて入ってくる人は古いものは使ったことない上に今更そんなのを使いたくないって人が多いと思います
jQuery ならほぼ生の DOM の操作のショートカット程度ですが Angular やそれ以前のフレームワークになると使える人がほとんどいなくて COBOL みたいな問題が出てきそうですね
Chrome122
Chrome122 がリリースされました!
以前も書きましたが Iterator Helpers が再度使えるようになりました

Array(5).keys().map(x => `(${x})`).drop(2).toArray()
// ['(2)', '(3)', '(4)']

色々なところで [...values] を使って一旦配列化する手間が減ります

また Set の追加メソッドも使えるようになりました
結構便利です

const a = new Set([1, 2])
const b = new Set([2, 3])
const c = a.union(b)

a // Set(2) {1, 2}
b // Set(2) {2, 3}
c // Set(3) {1, 2, 3}

他にもありますが基本的な Set のメソッドです

intersection → 重複する要素の Set
symmetricDifference → どちらかにある要素の Set
a.difference(b) → a から b を除外した要素の Set
a.isSupersetOf(b) → a が b の要素すべてを含むと true
a.isSubsetOf(b) → a の要素すべてが b に存在すると true
isDisjointFrom → 重複する要素がないと true

Set を返すメソッドはどれも新規に Set オブジェクトを作るもので破壊的なものではないです
WSL ディストリビューションが起動しなくなった
久々に使った PC で AlmaLinux9 の WSL を起動したらこんなエラーメッセージが出て動かせませんでした

Installing, this may take a few minutes...
WslRegisterDistribution failed with error: 0x80070050
Error: 0x80070050 ??????????

Press any key to continue...

WSL の更新や再起動でも変わらないです
Ubuntu の方は問題なく動いてます

wsl コマンドだとどうかなと 「wsl -d almalinux9」 を実行すると動きました
Windows アプリのショートカットがおかしくなったみたいです

そのアプリから開く必要は特にないので ショートカットを新しく作りました
右クリックメニューのショートカットの作成で項目の場所に

wsl -d almalinux9

ショートカット名を 「AlmaLinux9」
として作るだけです

ショートカットのダブルクリックで起動します

今回問題が起きた環境は WindowsTerminal が入ってないので確認できてないですが VSCode からディストリビューションを選択する方法だと問題なかったので アプリのショートカットを通さず ディストリビューション一覧を取得してディストリビューション名で起動するところは問題なさそうです
Workspace 機能使うか使わないか
フォルダ構造がこうなってるプロジェクトフォルダがあります

- project_root/
- package.json
- node_modules/...
- package1/
- package.json
- node_modules/...
- package2/
- package.json
- node_modules/...

ワークスぺース機能を使ってないので 個別に yarn install して node_modules が複数あります
ワークスペースを使ったほうがいいかなと思ったものの現状のメリットもあるので迷ってます

ワークスペースを使ったほうがいいところは node_modules がまとまってムダがないことです
ワークスペースを使ってないと共通のパッケージがあるとき各 node_modules に同じパッケージがインストールされます
また yarn install を個別にする必要もないです
パッケージ間の関係でも あるパッケージから別パッケージを参照する場合に楽です

逆にワークスペースにすると node_modules が 1 つになるので yarn install の処理が重たくなります
共通のパッケージがあまりない場合は パッケージ数が大きく増えますし デメリットが大きいです

あと ワークスペースにしたときにサーバー上での yarn install で特定パッケージの依存関係のみインストールってできるのでしょうか
ウェブサーバーや別の常駐アプリと共通部分のパッケージはインストールが必要ですけど フロント側で使うパッケージはいらないです
入ることで特別問題はないのですが フロント系はパッケージ数が増えて重くなるので不要なサーバーでまで入れたくないなと思います
AVIF を使おう
画像フォーマットの AVIF 形式ですが Edge 121 でやっと使えるようになったようです
https://caniuse.com/avif

Chrome では数年前から使えて Safari も 1 年以上前から使えたのですが Edge だけが使えなかったです
Chrome で表示できるのだから Edge でも表示できていいのに無効化されていました
それがとうとう使えるようになりました

前から JPEG はそろそろ捨てたいと思って WebP を使ったりもしましたが どうせなら更に新しい AVIF にしたいなと思って期待していたので待ちに待った機能です

ただし今はまだ表示できるだけ
Canvas からの出力で image/avif を指定しても対応してないです
WebP みたいにブラウザ上で作れるともっと身近になるのですけどね

あとライブドアブログは対応してないので 手元で AVIF 画像で管理していてもページに貼り付けるときは JPEG/PNG に変換するしかないです
WebP すら対応してないのであまり期待はできないです
この辺はそろそろどうにかしてほしいところです

ところで AVIF より新しい形式に JPEG XL というのもあるそうです
JPEG という名前の時点で悪いイメージしか無いですし使おうと思いません (一応性能的には AVIF よりも上という情報も見かけます)
AVIF のときは WebP より AVIF がいいなと思いましたが JPEG XL は別にいいかなってところです
それに Chrome は以前 JPEG XL を実験的にサポートしていたものの現在ではサポートをやめたそうです
AVIF で良さそうですね
ライブドアブログの記事情報一覧取得
Analytics でパラメータに記事の ID を入れても ID だけでどの記事なのか判断が難しいです
記事名も送っておけばいいのですが ID しか情報がないところでページ上で記事名まで取得して Analytics に送るのは違う気もします
とはいえ RDB のテーブルジョインみたいなことできなそうですし 見るときに自分で記事 ID から記事名を探してます

都度探すのも面倒なので記事一覧を手元に持っておきたいなと思って取得することにしました
エクスポートデータだとパースし辛いので別のところから取得します

ブログの一覧ページを使ってもいいのですが 今回は管理画面で内部的に使われている API を使うことにしました
一覧ページだとページ当たりの記事数がブログの設定依存になりますし ページ数が少ないと取得するページ数が多くなります
1000 記事あって 記事/ページ が 10 なら 100 回アクセスしないといけないです
あと HTML はパースが少し面倒です
管理画面で使われてるものだと JSON で受け取れますし 一度の取得数をパラメータで指定できるようでした
画面で設定可能な 200 が最大でしたが 200 あれば十分です
ただ内部的なものなので頻繁に仕様が変わってこのままだと使えなくなるかもしれません

記事一覧画面でコンソールを開いて↓のコードを貼り付けます

const request = (page) => {
const url = new URL(location.pathname + "api/articles_pager", location.origin)
url.searchParams.append("_", Date.now())
url.searchParams.append("keyword", "")
url.searchParams.append("attachment_id", "")
url.searchParams.append("category_id", "")
url.searchParams.append("monthly", "")
url.searchParams.append("entries_per_page", 200)
url.searchParams.append("rkey", window.rkey)
url.searchParams.append("p", page)

return fetch(url).then(res => res.json())
}

const pages = []
const updatePages = (entries) => {
for (const entry of entries) {
pages.push({
link: entry.permalink,
id: entry.id,
datetime: entry.regist_datetime,
title: entry.title,
})
}
}

let page = 1
while (true) {
const data = await request(page)
updatePages(data.entries)
if (data.pager.last_page <= page) break
page++
await new Promise(resolve => setTimeout(resolve, 100))
}
copy(pages)

非同期処理を挟むとコンソール上の copy が使えなくなるので copy だけ別に分けて実行します
値とそれを更新する関数の扱い
値とそれを更新するための関数を作る時よくやるやつ

const values = []
const append = (data) => {
if (data.type === 0) {
values.push(data.value)
}
}

変数と関数をフラットにおいてる

クラス好きな人ならクラス化しそう

class X {
values = []
append(data) {
if (data.type === 0) {
this.values.push(data.value)
}
}
}
const x = new X()

一回限りなので即時インスタンス化して

const x = new class {
values = []
append(data) {
if (data.type === 0) {
this.values.push(data.value)
}
}
}

こうすると append を渡すときに this のコンテキストが消えるので bind やアロー関数でラップが必要になる
使う側で気にしなくていいようにプロトタイプじゃなくてインスタンス自身に関数を入れる

const x = new class {
values = []
append = (data) => {
if (data.type === 0) {
this.values.push(data.value)
}
}
}

これならもうクラスの必要性がないのでただのオブジェクトにして

const x = {
values: [],
append: (data) => {
if (data.type === 0) {
x.values.push(data.value)
}
},
}
ウェブページ内で Chrome の DevTools を使う
Solid.js の Playground では ページ内に Chrome の DevTools が埋め込まれています(右下)
https://playground.solidjs.com/

どうなってるのと思ってソースコードを見てました
https://github.com/solidjs/solid-playground

chii というプロジェクトで DevTools の画面を作ってるようです
ウェブフロントエンドで動くようにパッチを当てていますが DevTools のソースコードが使われてます
また DevTools と接続するページ側では chobitsu というライブラリを使って DevTools との通信を管理してるようです
chobitsu は CDP の JavaScript 実装らしく CDP ライブラリなら他にも類似のものは色々ありそうですが chii との通信専用に独自の部分があるのかもです

Vue にしてもそうですが中国のライブラリって日本アニメの名前が使われる傾向があるみたいですね

Solid.js の Playground のページは Solid.js が前提になっていたり外部から更新できる画面になっていて複雑だったのでシンプルに最低限の機能で動くページを作ってみました
Solid.js の Playground のページから多くコードを流用してます
https://nexpr.gitlab.io/public-pages/chrome-devtools/

仕組みとしてはメインのページがあってその中に 2 つの iframe があります
片方が DevTools の画面で もう片方が DevTools と接続する画面です
それぞれの画面が親フレームにメッセージを送るようになっているので メインのページではメッセージを相手側に送信するようします

Elements タブで要素にマウスを乗せると対応する部分に色がついたり要素サイズが表示されたり ちゃんと DevTools の動きをしています
Console タブで JavaScript コードを実行できますし DOM を更新すればそれに対応して画面も変わります

ほとんどいつもの DevTools と変わらないですが 制限も色々あります
コンソールから Sources タブに飛んでエラー箇所を確認できなかったり
デバッグ実行ができなかったり
Elements タブで一部のスタイルが適用されているのに確認できなかったり(外部 CSS がだめなのかも)

すごいライブラリではあるのですが 制限があることで中途半端になりますし普通に DevTools を出せばいいかなと思うので今後使うかというと使わないかもです