Windows の npx で NOENT エラーが出る
Windows 環境で 公式サイトからダウンロードしたインストーラーを使って Node.js をインストールした後に npx を使うとエラーが出ました

no such file or directory

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

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

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

npm -g i (適当なパッケージ名)
V8 の harmony フラグの一覧
久々に V8 の --harmony-xxx のフラグを使ったのですが 今って harmony フラグってどれくらいあるんでしょうか
ググって出てくるものって過去の時点のものばかりなので参考にできないです

Node.js だと --v8-options で V8 のオプション一覧が出せるので ここから harmony を含むものを取り出します

root@c3872dcf640f:/# node -v
v21.6.0

root@c3872dcf640f:/# node -p process.versions.v8
11.8.172.17-node.19

root@c3872dcf640f:/# node --v8-options | grep harmony
--harmony (enable all completed harmony features)
type: bool default: --no-harmony
--harmony-shipping (enable all shipped harmony features)
type: bool default: --harmony-shipping
--harmony-import-attributes (enable "harmony import attributes" (in progress / experimental))
type: bool default: --harmony-import-attributes
--harmony-weak-refs-with-cleanup-some (enable "harmony weak references with FinalizationRegistry.prototype.cleanupSome" (in progress / experimental))
type: bool default: --no-harmony-weak-refs-with-cleanup-some
--harmony-temporal (enable "Temporal" (in progress / experimental))
type: bool default: --no-harmony-temporal
--harmony-shadow-realm (enable "harmony ShadowRealm" (in progress / experimental))
type: bool default: --no-harmony-shadow-realm
--harmony-struct (enable "harmony structs, shared structs, and shared arrays" (in progress / experimental))
type: bool default: --no-harmony-struct
--harmony-array-from-async (enable "harmony Array.fromAsync" (in progress / experimental))
type: bool default: --no-harmony-array-from-async
--harmony-intl-best-fit-matcher (enable "Intl BestFitMatcher" (in progress / experimental))
type: bool default: --no-harmony-intl-best-fit-matcher
--harmony-remove-intl-locale-info-getters (enable "Remove Obsoleted Intl Locale Info getters" (in progress / experimental))
type: bool default: --no-harmony-remove-intl-locale-info-getters
--harmony-intl-locale-info-func (enable "Intl Locale Info API as functions" (in progress / experimental))
type: bool default: --no-harmony-intl-locale-info-func
--harmony-intl-duration-format (enable "Intl DurationFormat API" (in progress / experimental))
type: bool default: --no-harmony-intl-duration-format
--harmony-set-methods (enable "harmony Set Methods")
type: bool default: --no-harmony-set-methods
--harmony-iterator-helpers (enable "JavaScript iterator helpers")
type: bool default: --no-harmony-iterator-helpers
--harmony-import-assertions (enable "harmony import assertions")
type: bool default: --harmony-import-assertions
--harmony-change-array-by-copy (enable "harmony change-Array-by-copy")
type: bool default: --harmony-change-array-by-copy
--harmony-rab-gsab (enable "harmony ResizableArrayBuffer / GrowableSharedArrayBuffer")
type: bool default: --harmony-rab-gsab
--harmony-regexp-unicode-sets (enable "harmony RegExp Unicode Sets")
type: bool default: --harmony-regexp-unicode-sets
--harmony-json-parse-with-source (enable "harmony json parse with source")
type: bool default: --harmony-json-parse-with-source
--harmony-rab-gsab-transfer (enable "harmony ArrayBuffer.transfer")
type: bool default: --harmony-rab-gsab-transfer
--harmony-array-grouping (enable "harmony array grouping")
type: bool default: --harmony-array-grouping

結構多めですね
ただ array-grouping や change-array-by-copy など すでにリリース済み機能も入ってるようです
出たばかりはまだ安定してないかもなので無効にするためなんでしょうか

デフォルトで有効かどうかは default のところが --no- ではじまるかでわかります
--no- で始まってれば無効です
in progress / experimental とは一致してないみたいです

V8 のソースコード的にはこの辺で定義されてるようでした

https://github.com/v8/v8/blob/12.2.280/src/flags/flag-definitions.h#L247
https://github.com/v8/v8/blob/12.2.280/src/flags/flag-definitions.h#L292
Array.fromAsync
来週に stable リリースの Chrome 121 で使えるようになる機能です
https://chromestatus.com/feature/5069575759069184

Array.from みたいなものですが非同期用です
返ってくるのは配列ではなく配列をラップした Promise です

Array.fromAsync([1, 2])
// Promise {<fulfilled>: Array(2)}

await Array.fromAsync([1, 2])
// [1, 2]

同期処理も非同期処理も対応してますが結果は Promise になります

await Array.fromAsync(function*() {
let i = 0
while (i < 3) yield i++
}())
// [0, 1, 2]

await Array.fromAsync(async function*() {
let i = 0
while (i < 3) {
await new Promise(resolve => setTimeout(resolve, 500))
yield i++
}
}())
// [0, 1, 2]

Array.from と for-of みたいな関係で Array.fromAsync は for-await-of で配列に入れるのと同じです

const it = async function*() {
let i = 0
while (i < 3) {
await new Promise(resolve => setTimeout(resolve, 500))
yield i++
}
}()
for await (const item of it) {
console.log(item)
}
// 0
// 1
// 2

Promise の配列に使うと Promise.all と同じ感じです

const promises = [1, 2, 3].map(x => Promise.resolve(x))
await Array.fromAsync(promises)
// (3) [1, 2, 3]
await Promise.all(promises)
// (3) [1, 2, 3]

ただし順番に await されるので thenable オブジェクトで then の中の処理がある場合 実行タイミングがずれます
それぞれに sleep のような処理がある場合は順番にスリープしていくので遅くなります
Promise.all の場合は全部の then を一気に呼び出すので一番長いものの待ち時間で済みます

const thenables = [1, 2, 3].map(x => {
return {
then: (onFulfilled, onRejected) => {
console.log(Date.now() % 10000)
setTimeout(onFulfilled, x * 1000, x)
}
}
})

Array.fromAsync(thenables)
.then((values) => console.log(Date.now() % 10000, values))
// 3842
// 4852
// 6867
// 9868 [1, 2, 3]

Promise.all(thenables)
.then((values) => console.log(Date.now() % 10000, values))
// 4857
// 4858
// 4858
// 7867 [1, 2, 3]

これまでのジェネレーターと同じですが 無限に続くところで使うと終わらないので注意が必要です
同期処理だと画面が固まったり早い段階でエラーになったりして気づきやすかったですが非同期になると少し分かりづらくなります
画面が固まらないですし Promise が解決される間隔がある程度あれば CPU 負荷もそこまでじゃないです

for-await-of ならイベントリスナのかわりみたいな使い方もありかなと思うのですが Array.fromAsync だとそういう使い方もできませんし どこで使えるのかはあまりイメージできてないです
ダミーデータ用に配列を簡単に増やす
テスト用にその場限りで適当にデータを増やしたいことがあります

something(
value,
[
{ a: 1 },
{ a: 2 },
{ a: 3 },
],
)

みたいなのがあって配列の要素数を 30 くらいにしたいです
実際はもっと長いので範囲選択のコピペもちょっとめんどうだったりします

something(
value,
[
{ a: 1 },
{ a: 2 },
{ a: 3 },
].repeat(10),
)

みたいなことがしたいですが配列には repeat メソッドはありません
サクッとかける方法でメソッドで要素数を増やしたいです

ということで使ってる flatMap

something(
value,
[
{ a: 1 },
{ a: 2 },
{ a: 3 },
].flatMap(x => Array(10).fill(x)),
)

1, 2, 3 の繰り返しじゃなくて 1 が続いた後で 2 が続いて 3 が続くことになるのと 全部参照は同じという欠点はあるのですが 数が増えれば中身は気にしない場所ではこれでもよかったりします
引数追加でもウェブ互換壊れそう
Iterator Helpers はじめウェブ互換に影響してる変更を見ていてふと思ったけど引数の追加でも壊れそうですよね
JavaScript だと map や filter 等の関数ではコールバック関数の 2 つめに index が 3 つめには配列自身が渡されます

parseInt を map に使うとおかしくなるのは有名です

[1, 1, 1].map(parseInt)
// [1, NaN, 1]

これがあるのと this が壊れることがあるのでアロー関数を使うことが多かったりしますが 直接関数を入れるケースもありえます
多いのだと Boolean でフィルタです

[{a: 1}, null, {a: 2}].filter(Boolean)
// [{a: 1}, {a: 2}]

他にも btoa など受け取る引数が 1 つのものは直接コールバック関数に渡したりします

["abc", "xyz"].map(btoa)
// ['YWJj', 'eHl6']

こういった関数に引数が追加されると 意図せず 2 つめ以降の引数を渡してしまって動作が変わります
引数の追加って互換性あるように見えますが JavaScript みたいに引数の数が一致してなくても動く言語だとそうとも言えなそうですね
Iterator Helpers が Chrome 122 で復活予定
期待してた機能の Iterator Helpers ですが 使えるようになってすぐにウェブ互換問題で使えなくなりました
🔗 Iterator helpers が使えなくなった

Chrome 122 (現 dev) から再度使えるようになるようです
https://chromestatus.com/feature/5102502917177344

ウェブ互換問題に対応するため 少し特別な扱いをしてるようです
toStringTag に違いがあるみたいです

https://github.com/tc39/proposal-iterator-helpers/pull/287
https://github.com/tc39/test262/pull/3970/files

実際に getOwnPropertyDescriptor で他のクラスと比較してみると

Object.getOwnPropertyDescriptor(Map.prototype, Symbol.toStringTag)
// {value: 'Map', writable: false, enumerable: false, configurable: true}

Object.getOwnPropertyDescriptor(Iterator.prototype, Symbol.toStringTag)
// {enumerable: false, configurable: true, get: ƒ, set: ƒ}

ほかは Map みたいに writable が false で value に値が入ってます
Iterator では getter/setter になっています

通常利用では気にする必要ないかと思いますが 特殊なもののようです



気になったので prototype 構造がどうなってるか少し調べてみました

Iterator.prototype.__proto__ === Object.prototype
// true

const i1 = [].values()
const i2 = i1.take(1)
const i3 = (function*(){})()
const i4 = i3.take(1)
const i5 = i3.map(x => x)

// Iterator Helpers で得られるオブジェクトの prototype は
// 元やメソッドが違っても同じ

i2.__proto__ === i4.__proto__
// true

i2.__proto__ === i5.__proto__
// true

// Iterator Helpers とそれ以外は別

i1.__proto__ === i2.__proto__
// false

// いずれも Iterator を継承してる

i1.__proto__.__proto__ === Iterator.prototype
// true

i2.__proto__.__proto__ === Iterator.prototype
// true

i3.__proto__.__proto__.__proto__ === Iterator.prototype
// true

Iterator を継承したオブジェクトの toStringTag は他と同じで writable が false で value に値が入ってる

Object.getOwnPropertyDescriptor(i1.__proto__, Symbol.toStringTag)
// {value: 'Array Iterator', writable: false, enumerable: false, configurable: true}

Object.getOwnPropertyDescriptor(i2.__proto__, Symbol.toStringTag)
// {value: 'Iterator Helper', writable: false, enumerable: false, configurable: true}

Object.getOwnPropertyDescriptor(i3.__proto__.__proto__, Symbol.toStringTag)
// {value: 'Generator', writable: false, enumerable: false, configurable: true}
pyscript が script タグになってた
去年にブラウザで Python を動かせるとして出てきた pyscript
あれからも大きく更新されてたようです
https://jeff.glass/post/whats-new-pyscript-2024-1-1/

最初の頃は Python のコードを書く場所に py-script タグを使ってました
それが stable 版になった今だと py-script タグは推奨されず script タグを使って type に py や mpy を指定するのが推奨のようです
https://pyscript.github.io/docs/2024.1.1/user-guide/first-steps/
カスタムエレメントだと HTML として解析されてしまうことがあるからだとか

最初見たときにも script タグを使わないとエスケープとかおかしなことになりそうと思いましたが やっぱりそこが問題だったみたいです
ただすべての py-* タグが推奨されないわけではなく設定を書く <py-config> は使われてるようです

ところで script タグを使うときの type の文字列
HTML の仕様的には 今後追加されるものは module や importmap みたいに MIME type 以外の文字列で追加予定なのでデータブロックを表すために type に指定するのは有効な MIME type で JavaScript 用に予約されてるもの以外とされてたはずです
https://html.spec.whatwg.org/multipage/scripting.html#the-script-element
py や mpy を使ってるので pyscript ではそれには則ってないみたいですね
ネットで見かける Windows Sandbox を Home Edition で使う方法
Windows Sandbox を使いたいことがよくあるのですが その時使ってる PC が Pro じゃないこともあります
公式ドキュメントでは Home エディションはサポートしてないと書かれています
ただ ググると Home エディションでも有効化するという bat ファイルが色々出てきます
サイトはいくつも出てきますが bat ファイルの中身は同じみたいです

怪しげなファイルをインストールするものだったり 海賊版的な方法だったりするのかと不安もあるので中身を見てみました
長い割にほとんどが管理者権限で動かすための部分だったりで実質は以下の部分だけでした

dir /b %SystemRoot%\servicing\Packages\*Containers*.mum >sandbox.txt
for /f %%i in ('findstr /i . sandbox.txt 2^>nul') do dism /online /norestart /add-package:"%SystemRoot%\servicing\Packages\%%i"
Dism /online /enable-feature /featurename:Containers-DisposableClientVM /LimitAccess /ALL

パッケージの追加もパッケージ名はローカルにあるものから作ってますし外部からの取得はしてなさそうです
これだけで動くなら正規の方法のようです
ただ問題起きたら影響が大きそうなので事前に壊れてもいい PC で試したいので実行はまた今度にします



ちなみに 1, 2 行目でやってることは

C:\Windows\servicing\Packages\ 内の Containers を含む .mum ファイルすべてに対して

dism /online /norestart /add-package:***

を実行しています
*** のところに .mum ファイルのフルパスが入ります

一旦ファイルに出力してますが 特別な意味はなさそうです
findstr を使ってますが findstr は正規表現で検索するコマンドで 検索条件が「.」なので全件に一致します
空行は無視して 1 件ごと取得するために使ってそうです
作ったファイルもすぐに消しています

やってることは PowerShell のこれと同じはず

$items = dir C:\Windows\servicing\Packages\*Containers*.mum
foreach ($i in $items) {
dism /online /norestart /add-package:$i.FullName
}
dism /online /enable-feature /featurename:Containers-DisposableClientVM /LimitAccess /ALL

最終的に有効化するのが Containers-DisposableClientVM なら add-package するのは Containers を含む全てではなく Containers-DisposableClientVM を含むものだけでも良さそうに見えますが 他も必要になるのでしょうか

> dir C:\Windows\servicing\Packages\*Containers-DisposableClientVM*.mum | select name

Name
----
Containers-DisposableClientVM-merged-Package~31bf3856ad364e35~amd64~en-US~10.0.19041.1.mum
Containers-DisposableClientVM-merged-Package~31bf3856ad364e35~amd64~ja-JP~10.0.19041.1.mum
Containers-DisposableClientVM-merged-Package~31bf3856ad364e35~amd64~~10.0.19041.3636.mum
Containers-DisposableClientVM-Package~31bf3856ad364e35~amd64~en-US~10.0.19041.3636.mum
Containers-DisposableClientVM-Package~31bf3856ad364e35~amd64~ja-JP~10.0.19041.3636.mum
Containers-DisposableClientVM-Package~31bf3856ad364e35~amd64~~10.0.19041.3693.mum
Containers-DisposableClientVM-Package~31bf3856ad364e35~amd64~~10.0.19041.3803.mum
flexbox 内の SVG 画像のサイズ
普段あまり SVG 画像を使わないということもあってか flexbox と組み合わせたときに予想外な動きになって困りました

<!doctype html>

<style>
.row {
display: flex;
}
</style>

<div class="row">
<div><img src="00.svg"></div>
<div>00.svg</div>
</div>
<div class="row">
<div><img src="01.svg"></div>
<div>01.svg</div>
</div>

[00.svg]
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#ffc8aa" />
<circle cx="100" cy="100" r="80" fill="#b45829" />
<text x="100" y="125" font-size="60" text-anchor="middle" fill="white">SVG</text>
</svg>

[01.svg]
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#ffc8aa" />
<circle cx="100" cy="100" r="80" fill="#b45829" />
<text x="100" y="125" font-size="60" text-anchor="middle" fill="white">SVG</text>
</svg>

このページを表示すると 00.svg は期待通りに表示されますが 01.svg は表示されません
01.svg の img のサイズが 0x0 になって何も表示されないです

(確認用)

img に height: 100% を設定すると 高さを行の高さにでき それに合わせて幅も確保され表示されるようになりますが 親の div のサイズは幅が 0 なのは変わらず 文字と画像が重なります

画像は全部こうなのかと思いましたが PNG などではならなかったです
また SVG でも これが発生する SVG 画像と発生しない SVG 画像がありました
違いを探すと 00.svg と 01.svg のように viewBox か width+height かの違いでした
viewBox を使う場合は width と height は決まっていないので flexbox の中で自動で計算される場合は最小の 0x0 になるようです
width と height があればそれが画像サイズとなるのでちゃんと表示されます

SVG 画像側に width と height がないなら img タグの属性側に viewBox に合わせた width と height をつければ解決できます
ただしその場合 CSS で max-width を指定しても属性で指定しただけの height が確保されてしまうような違いが出ます
可能なら SVG ファイルの方に width と height を追加したほうが良さそうです
ページ内部からヘッダーを更新したい
メインのレイアウトがこんな構造のとき

const Main = () => {
return (
<div>
<Header/>
<Outlet/>
<Footer/>
</div>
)
}

Outlet のコンポーネントがその時点のページを表示します
このページ内の処理で Header や Footer に表示するものを設定したいです
ページタイトルとかそういうの

やろうとすると Main で Context を提供し setHeader みたいな関数にアクセスできるようにして 各ページがマウント時に呼び出します

const Page1 = () => {
const setHeader = useSetHeader()

useEffect(() => {
setHeader({
title: "Page1",
})
}, [])

return (<div>...</div>)
}

あまり気持ちの良い方法ではないです

以前は Outlet のような使い方をする Router を使っていなくて Outlet のところに Router を配置する感じでした
それだと Header などレイアウトを外に配置するメリットもあまりなかったので Page が Main コンポーネントを使うという構造でした

const Page1 = () => {
const header = {
title: "Page1",
}
return (
<Main header={header}>
<div>...</div>
</Main>
)
}

こっちのほうが自然な感じです
とはいえ全部のページで Main を使う必要がありますし Outlet のような機能を持つ Router を使う場合だと Router でマッチしたコンポーネントは外側のコンポーネントの一部として表示する形になります
ページのコンポーネントがレイアウトを選択するのではなく ルーター側でレイアウト内にページを配置するようにしてるので こういう作りにならないです
いい方法はないものなんでしょうか
できるだけクラス内に関数を入れたくない
クラスを作るときは基本 this を使って値を参照・更新するのが目的なので this に依存せず 引数やグローバルなデータから返り値が決まるものはメソッドという形にはしたくないです

たとえば

class A {
something() {
// ...
this.method(this.value)
// ...
}

method(value) {
// this 使わない
return !!value
}
}

this を使うメソッド something から method を呼び出すのですが method は中で this を使わず引数だけで結果が決まります
こういう関数は A の中に入れたくないので外側に出して単純な関数にします
扱うデータが A と深い関係があるなら A の static メソッドにすることもありますが A だけに関係するわけじゃないなら別用途で使いたいときに A を通したくないので関数です

こういう感じ

const fn = (value) => {
return !!value
}

class A {
something() {
// ...
fn(this.value)
// ...
}
}

いつもは特に問題もなかったのですが 今回は少し困ったことがありました
fn みたいな関数がいくつかあり 相互に呼び出していて その深い部分で this に入ってる関数を使いたいということがありました

const foo = () => {
//
}

const bar = () => {
//
}

const baz = () => {
//
const value = getValue()
//
}

class A {
something() {
// ...
foo(this.value)
// ...
}
}

これの getValue は A のメソッドを使いたいです
また 再起や循環した呼び出しがあり baz が呼び出されるまでが長いです

イメージ:
something -> foo -> bar -> foo -> bar -> bar -> baz

foo の第二引数に getValue として使いたい A のメソッドを渡して baz が呼び出されるまで foo や bar の呼び出しで常に引数として渡すことはできるのですが すごく面倒です
また baz が呼び出されるケースは少なく その中でも getValue は初期値が必要になったときだけ使うようなもの
それのためにあちこちの呼び出しで 内部で baz を使う可能性があれば引数として A のメソッドを渡していくのはとても面倒です
React などでいう Context があると助かるのですが そういうのはないただの関数呼び出しなので都度渡していくしかないです

回避するには foo, bar, baz すべてを A のメソッドにしてしまいます
baz も A のメソッドなので this を参照できます
ただ foo, bar は A に依存しないものなので気が引けます

Node.js だと AsyncLocalStorage で Context 的なことはできるのですが ブラウザでは使えないです

const { AsyncLocalStorage } = require("node:async_hooks")

const alstorage = new AsyncLocalStorage()

const foo = () => {
bar()
}

const bar = () => {
baz()
}

const baz = () => {
const getValue = alstorage.getStore()
console.log(getValue())
}

class A {
constructor(value) {
this.value = value
}
something() {
alstorage.run(() => this.getValue(), () => {
foo()
})
}
getValue() {
return this.value
}
}

new A(1).something()
new A(2).something()
1
2
C# は Task を積極的に使っていったほうがいいのかも
JavaScript の Promise みたいなものが C# でもあって Task です
await で待機して結果を受け取れるあたりも一緒です

ただ Promise だとネストしても自動で 1 段にしてくれますが Task はそういうことがなく(
設定次第) 値を取り出すために二重三重に await をしないといけなかったりで Promise よりも使いづらかったです
また非同期処理を呼び出すところが Node.js ほどないのであまり使ってなかったです

JavaScript だとシングルスレッドでしか動かないので 不要なところを Promise 化しても 長い処理の途中で別の処理を行えるだけで全体としての実行時間は変わりません
ですが Task はマルチスレッドで動いてくれます
JavaScript でいう Worker を使うようなことを手軽にできます
この利点を考えたら普段から積極的に使っていく方がいいのかもしれない と思いました

例えばこういう処理があったとします

var start = DateTime.Now;

var list = new List<int>();

for (var i = 0; i < 12; i++)
{
list.Add(i);
}

var middle = DateTime.Now;
System.Console.WriteLine(middle - start);

var result = list.Select(x =>
{
var n = 0;
for (var i = 0; i < 100000000; i++)
{
n++;
}
return n;
}).ToList();

System.Console.WriteLine(string.Join(",", result));
var end = DateTime.Now;
System.Console.WriteLine(end - middle);
00:00:00.0076682
100000000,100000000,100000000,100000000,100000000,100000000,100000000,100000000,100000000,100000000,100000000,100000000
00:00:02.4401087

リストの要素のそれぞれに重たい処理を行います
今回だと 1 億回インクリメントです
全体で 2 秒ほどかかってます

これを Task 化します
リストのそれぞれの処理を Task にして最後にまとめて待機します

var start = DateTime.Now;

var list = new List<int>();

for (var i = 0; i < 12; i++)
{
list.Add(i);
}

var middle = DateTime.Now;
System.Console.WriteLine(middle - start);

var tasks = list.Select(x =>
Task.Run(() =>
{
var n = 0;
for (var i = 0; i < 100000000; i++)
{
n++;
}
return n;
})
);

var result = await Task.WhenAll(tasks);

System.Console.WriteLine(string.Join(",", result));
var end = DateTime.Now;
System.Console.WriteLine(end - middle);
00:00:00.0075865
100000000,100000000,100000000,100000000,100000000,100000000,100000000,100000000,100000000,100000000,100000000,100000000
00:00:00.3334272

0.3 秒程度になりました

別スレッドになるので JavaScript の Worker みたいに変数共有の面倒さがあったりするのかと思いましたが 普通に Task の中から外側の変数にアクセスできました
参照のみでなく更新もできます
各 Task が結果を返すのではなく Task 外の List に結果を追加するようしてみるとこんな感じです

var start = DateTime.Now;

var list = new List<int>();

for (var i = 0; i < 12; i++)
{
list.Add(i);
}

var middle = DateTime.Now;
System.Console.WriteLine(middle - start);

var result = new List<int>();

var tasks = list.Select((x, i) =>
Task.Run(() =>
{
var n = i;
for (var i = 0; i < 100000000; i++)
{
n++;
}
result.Add(n);
})
);

await Task.WhenAll(tasks);

System.Console.WriteLine(string.Join(",", result));
var end = DateTime.Now;
System.Console.WriteLine(end - middle);
00:00:00.0069215
100000000,100000001,100000003,100000002,100000004,100000006,100000005,100000008,100000007,100000009,100000010,100000011
00:00:00.3152148

ただ並列処理になるのでこういうことをすると結果の順番はバラバラです
あとから元データと紐付けるのが面倒なので Add ではなくインデックスを使って指定の場所を更新するか Select にしてそれぞれが結果を返す形でよさそうです
Python の type hints は実行時チェックされない
Python の type hints はこれといって書いたことがなく記法とそういうのがあるということを知ってるくらいでした

type hints 付きで書かれてるコードをいじってたときのこと
実装を変えて型が変わったのに関数の引数と返り値の型はそのままだったのですがエラーはなく動きました

def foo(num: int) -> int:
return num * 2

foo("A")
# "AA"

みたいな感じでエラーはなく動いてます
PHP みたいに実行時にチェックが入って一致しないとエラーになるのかと思いましたが 調べてみるとツールでチェックするためのもので実行時には Python 側でチェックなどはしないようですね

一応実行時にチェックさせようとするツールもあるようですが 単純に Python ファイルをそれに渡すだけじゃダメのようです
デコレーターを使ったり継承したりソースコード自体をそのツールに合わせて書く必要があるようです
型チェックのツールを変えるのにソースコードを書き換えないといけないとなると Flow から TypeScript に移行するようなもので 使いやすそうにも思えません
なので実行前に静的解析するだけで TypeScript に近い扱いみたいです

そうなると 以前話題になってた TypeScript の型記述をコメントとして JavaScript に持ってくるみたいなのも意外と現実性があったりするんでしょうか
そういえば最近全然話題になりませんが どうなったんでしょうね
ただ これとは違って Python の場合は型情報を値として持っていて実行時に参照可能です
TypeScript 構文をコメントとみなすあれは本当にコメントなので 実行時に何も残らず JavaScript からすると紛らわしいだけの無意味なものです
最悪入るとしても Python みたいに実行時に参照できるようにはなって欲しいものです

もう少し調べてみたら Python の type hints は annotations という仕組みであって type hints だけのものじゃないようです
だからか型を書くところに書けるものは型に限らず任意の式となってました

なのでこういうのも有効です

>>> import random
>>> from datetime import datetime
>>>
>>> def foo(a: random.random(), b: datetime.now(), c: [1,2,3][1:]) -> 1 + 2:
... return 0
...
>>> foo(1, 2, 3)
0
>>> foo.__annotations__
{'a': 0.8245880245015794, 'b': datetime.datetime(2023, 12, 29, 10, 16, 38, 266420), 'c': [2, 3], 'return': 3}

関数を実行した結果だったり datetime みたいなオブジェクトだったり 足し算した結果だったり
もう型のようには見えません

annotations の中で type hints を書いてるのなら dict で type みたいなキーに対して書くほうが適してそうには思います

>>> def foo(a: { "type": int }) -> { "type": str }:
... return "A" * a

これなら他の情報も追加できます
でもほとんどが type hints のみで コードが長くなるので type hints を直接書く形なのでしょうか
他のものも記載したくなったらどうするのでしょうね
dict 以外なら dict 化して値を type が key の value として追加するとかでしょうか

こういうことができるものなので Python では型は実体のある値として実行時に扱えるものです
これはすごくいいですね
C++ や C# みたいな言語だとクラスは実体がないものであって 実行時に触れられるものでもないので扱いづらく好きになれなかったです
その点 JavaScript や Python はクラスそのものもオブジェクトとして実行時に変数に入れたりプロパティを見たりなどできるものです
こういう扱いになってるのが好きなので型情報も変数に格納されてる値であるのはいいところです

ただ 変数の型については評価されるので関数は実行されますが どこにも保存されず実行時に扱えないようでした

>>> foo: 10 = 1

>>> f = lambda: print("called")
>>> bar: f() = 1
called

annotations で指定している 10 は変数 foo の情報であって foo に入ってる値の 1 に対するものではありません
foo でアクセスできるのは変数に入ってる値であって変数そのものではないので変数の情報である変数の型が取れないのは仕方なくはありますね

またジェネリクスを表現する型パラメーターというのもあります

>>> def foo[T](a: T) -> T:
... return a
...
>>> foo.__type_params__
(T,)
>>> type(foo.__type_params__[0])
<class 'typing.TypeVar'>
>>> foo.__annotations__
{'a': T, 'return': T}

これは引数みたいな感じで関数やクラスの中では変数に入ってる値として扱えます

>>> def foo[X]():      
... print(X, type(X))
...
>>> foo()
X <class 'typing.TypeVar'>
>>> class C[T]:
... print(T)
...
T
TypeScript でラッパー関数を作るときの型定義
const fn = (a: number, b: string, c: boolean): { a: number; b: string; c: boolean } => {
return { a, b, c }
}

という関数があって JavaScript でいうこういうことをしたいとき

const fnw = (...args) => {
console.log(args)
return fn(...args)
}

引数と返り値をそのまま返してるのですが TypeScript だと fnw の関数にも型を書かないといけないです
複雑な型定義だと毎回書くのが面倒です
完全に引数と返り値が一致しているのなら typeof を左辺に書くことで解決できます

const fnw: typeof fn = (...args) => {
console.log(args)
return fn(...args)
}

fnw(1, "a", false)

返り値が異なるなど変更するところがあれば関数の型をその場で作ります

const fn = (a: number, b: string, c: boolean): { a: number; b: string; c: boolean } => {
return { a, b, c }
}

const fnw: (...args: Parameters<typeof fn>) => void =
(...args) => {
fn(...args)
}

返り値を推論に任せるなら引数のみ指定ですが この場合は右辺側に書くことになります

const fnw = (...args: Parameters<typeof fn>) => {
fn(...args)
}

この場合は ...args とまとめて受け取らないとダメです
分けようとすると 1 つ 1 つ Parameters<typeof fn>[0] のように書く事になって面倒です
別々に受け取りたい場合でも ...args にして関数内で

const [a, b, c] = args

で展開するほうが楽そうです
rye sync でエラーになる
パッケージを追加して rye sync を実行したらなぜかエラーになりました
原因がわからなくて困ってましたが パッケージとは関係なく src フォルダ内にデフォルトで用意されてるフォルダを消すとまずいようでした

foo という名前で rye init したら src フォルダの中はこんな感じになっています

src/
└── foo/
└── __init__.py

自分で記述するファイルは単一ファイルでよかったのでこう変更してました

src/
└── main.py

実行するときは

python3 src/main.py

で直接実行してました

foo フォルダを消したのがまずかったようで 消したあとに rye sync をするとエラーになりました
パッケージを追加しなければ最初以降に rye sync することもなかったので気づかなかったです

エラーは production lockfile の生成中に subprocess-exited-with-error というもの
エラー全体はすごく長くて最後の方を見ても lock ファイルの書き込みに失敗したくらいしかわからないです

[root@4728010fbd1b foo]# rye sync
Reusing already existing virtualenv
Generating production lockfile: /opt/foo/requirements.lock
error: subprocess-exited-with-error

× Preparing metadata (pyproject.toml) did not run successfully.
│ exit code: 1
╰─> [41 lines of output]
Traceback (most recent call last):
File "/root/.rye/pip-tools/cpython@3.12/lib/python3.12/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 353, in <module>
main()
File "/root/.rye/pip-tools/cpython@3.12/lib/python3.12/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 335, in main
json_out['return_val'] = hook(**hook_input['kwargs'])
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/root/.rye/pip-tools/cpython@3.12/lib/python3.12/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 152, in prepare_metadata_for_build_wheel
whl_basename = backend.build_wheel(metadata_directory, config_settings)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/tmp/pip-build-env-ok8nhepv/overlay/lib/python3.12/site-packages/hatchling/build.py", line 58, in build_wheel
return os.path.basename(next(builder.build(directory=wheel_directory, versions=['standard'])))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/tmp/pip-build-env-ok8nhepv/overlay/lib/python3.12/site-packages/hatchling/builders/plugin/interface.py", line 155, in build
artifact = version_api[version](directory, **build_data)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/tmp/pip-build-env-ok8nhepv/overlay/lib/python3.12/site-packages/hatchling/builders/wheel.py", line 412, in build_standard
for included_file in self.recurse_included_files():
File "/tmp/pip-build-env-ok8nhepv/overlay/lib/python3.12/site-packages/hatchling/builders/plugin/interface.py", line 176, in recurse_included_files
yield from self.recurse_selected_project_files()
File "/tmp/pip-build-env-ok8nhepv/overlay/lib/python3.12/site-packages/hatchling/builders/plugin/interface.py", line 180, in recurse_selected_project_files
if self.config.only_include:
^^^^^^^^^^^^^^^^^^^^^^^^
File "/tmp/pip-build-env-ok8nhepv/overlay/lib/python3.12/site-packages/hatchling/builders/config.py", line 781, in only_include
only_include = only_include_config.get('only-include', self.default_only_include()) or self.packages
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/tmp/pip-build-env-ok8nhepv/overlay/lib/python3.12/site-packages/hatchling/builders/wheel.py", line 231, in default_only_include
return self.default_file_selection_options.only_include
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/root/.rye/py/cpython@3.12.0/install/lib/python3.12/functools.py", line 995, in __get__
val = self.func(instance)
^^^^^^^^^^^^^^^^^^^
File "/tmp/pip-build-env-ok8nhepv/overlay/lib/python3.12/site-packages/hatchling/builders/wheel.py", line 219, in default_file_selection_options
raise ValueError(message)
ValueError: Unable to determine which files to ship inside the wheel using the following heuristics: https://hatch.pypa.io/latest/plugins/builder/wheel/#default-file-selection

At least one file selection option must be defined in the `tool.hatch.build.targets.wheel` table, see: https://hatch.pypa.io/latest/config/build/

As an example, if you intend to ship a directory named `foo` that resides within a `src` directory located at the root of your project, you can define the following:

[tool.hatch.build.targets.wheel]
packages = ["src/foo"]
[end of output]

note: This error originates from a subprocess, and is likely not a problem with pip.
Traceback (most recent call last):
File "/root/.rye/pip-tools/cpython@3.12/lib/python3.12/site-packages/pip/_internal/operations/build/metadata.py", line 35, in generate_metadata
distinfo_dir = backend.prepare_metadata_for_build_wheel(metadata_dir)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/root/.rye/pip-tools/cpython@3.12/lib/python3.12/site-packages/pip/_internal/utils/misc.py", line 772, in prepare_metadata_for_build_wheel
return super().prepare_metadata_for_build_wheel(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/root/.rye/pip-tools/cpython@3.12/lib/python3.12/site-packages/pip/_vendor/pyproject_hooks/_impl.py", line 186, in prepare_metadata_for_build_wheel
return self._call_hook('prepare_metadata_for_build_wheel', {
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/root/.rye/pip-tools/cpython@3.12/lib/python3.12/site-packages/pip/_vendor/pyproject_hooks/_impl.py", line 311, in _call_hook
self._subprocess_runner(
File "/root/.rye/pip-tools/cpython@3.12/lib/python3.12/site-packages/pip/_internal/utils/subprocess.py", line 252, in runner
call_subprocess(
File "/root/.rye/pip-tools/cpython@3.12/lib/python3.12/site-packages/pip/_internal/utils/subprocess.py", line 224, in call_subprocess
raise error
pip._internal.exceptions.InstallationSubprocessError: Preparing metadata (pyproject.toml) exited with 1

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
File "/root/.rye/pip-tools/cpython@3.12/bin/pip-compile", line 8, in <module>
sys.exit(cli())
^^^^^
File "/root/.rye/pip-tools/cpython@3.12/lib/python3.12/site-packages/click/core.py", line 1157, in __call__
return self.main(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/root/.rye/pip-tools/cpython@3.12/lib/python3.12/site-packages/click/core.py", line 1078, in main
rv = self.invoke(ctx)
^^^^^^^^^^^^^^^^
File "/root/.rye/pip-tools/cpython@3.12/lib/python3.12/site-packages/click/core.py", line 1434, in invoke
return ctx.invoke(self.callback, **ctx.params)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/root/.rye/pip-tools/cpython@3.12/lib/python3.12/site-packages/click/core.py", line 783, in invoke
return __callback(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/root/.rye/pip-tools/cpython@3.12/lib/python3.12/site-packages/click/decorators.py", line 33, in new_func
return f(get_current_context(), *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/root/.rye/pip-tools/cpython@3.12/lib/python3.12/site-packages/piptools/scripts/compile.py", line 592, in cli
results = resolver.resolve(max_rounds=max_rounds)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/root/.rye/pip-tools/cpython@3.12/lib/python3.12/site-packages/piptools/resolver.py", line 593, in resolve
is_resolved = self._do_resolve(
^^^^^^^^^^^^^^^^^
File "/root/.rye/pip-tools/cpython@3.12/lib/python3.12/site-packages/piptools/resolver.py", line 625, in _do_resolve
resolver.resolve(
File "/root/.rye/pip-tools/cpython@3.12/lib/python3.12/site-packages/pip/_internal/resolution/resolvelib/resolver.py", line 76, in resolve
collected = self.factory.collect_root_requirements(root_reqs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/root/.rye/pip-tools/cpython@3.12/lib/python3.12/site-packages/pip/_internal/resolution/resolvelib/factory.py", line 534, in collect_root_requirements
reqs = list(
^^^^^
File "/root/.rye/pip-tools/cpython@3.12/lib/python3.12/site-packages/pip/_internal/resolution/resolvelib/factory.py", line 490, in _make_requirements_from_install_req
cand = self._make_base_candidate_from_link(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/root/.rye/pip-tools/cpython@3.12/lib/python3.12/site-packages/pip/_internal/resolution/resolvelib/factory.py", line 207, in _make_base_candidate_from_link
self._editable_candidate_cache[link] = EditableCandidate(
^^^^^^^^^^^^^^^^^^
File "/root/.rye/pip-tools/cpython@3.12/lib/python3.12/site-packages/pip/_internal/resolution/resolvelib/candidates.py", line 318, in __init__
super().__init__(
File "/root/.rye/pip-tools/cpython@3.12/lib/python3.12/site-packages/pip/_internal/resolution/resolvelib/candidates.py", line 156, in __init__
self.dist = self._prepare()
^^^^^^^^^^^^^^^
File "/root/.rye/pip-tools/cpython@3.12/lib/python3.12/site-packages/pip/_internal/resolution/resolvelib/candidates.py", line 225, in _prepare
dist = self._prepare_distribution()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/root/.rye/pip-tools/cpython@3.12/lib/python3.12/site-packages/pip/_internal/resolution/resolvelib/candidates.py", line 328, in _prepare_distribution
return self._factory.preparer.prepare_editable_requirement(self._ireq)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/root/.rye/pip-tools/cpython@3.12/lib/python3.12/site-packages/pip/_internal/operations/prepare.py", line 696, in prepare_editable_requirement
dist = _get_prepared_distribution(
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/root/.rye/pip-tools/cpython@3.12/lib/python3.12/site-packages/pip/_internal/operations/prepare.py", line 71, in _get_prepared_distribution
abstract_dist.prepare_distribution_metadata(
File "/root/.rye/pip-tools/cpython@3.12/lib/python3.12/site-packages/pip/_internal/distributions/sdist.py", line 67, in prepare_distribution_metadata
self.req.prepare_metadata()
File "/root/.rye/pip-tools/cpython@3.12/lib/python3.12/site-packages/pip/_internal/req/req_install.py", line 577, in prepare_metadata
self.metadata_directory = generate_metadata(
^^^^^^^^^^^^^^^^^^
File "/root/.rye/pip-tools/cpython@3.12/lib/python3.12/site-packages/pip/_internal/operations/build/metadata.py", line 37, in generate_metadata
raise MetadataGenerationFailed(package_details=details) from error
pip._internal.exceptions.MetadataGenerationFailed: metadata generation failed
error: could not write production lockfile for project

Caused by:
failed to generate lockfile

まだ 0.15 だし そういうこともある?
とか思いましたが 念のため新プロジェクトで同じパッケージを入れて rye sync してみると問題なく動いてました
違いというと src フォルダ内のフォルダ構成くらいなので ここを元に戻してみるとエラーが出なくなりました
このフォルダは必須だったみたいです

エラーを見返すと中間部分にそれらしいメッセージがありました
内部で使用している hatch というツールにビルド対象を設定する必要があってそこでエラーになっていたようです
tool.hatch.build.targets.wheel の設定が必要みたいです
設定ファイルに記載していないとプロジェクト名から推測してくれるようで プロジェクト名と同じフォルダあったので初期状態だと動作していたようです

main.py というファイルに置き換えていたので↓のような設定を pyproject.toml に追加すると foo フォルダを消しても動作しました

[tool.hatch.build.targets.wheel]
packages = ["src/main"]

パッケージとして公開するつもりがなく パッケージをインストールするためだけに rye を使っていてもこういう設定が必要なのは少し面倒ですね
来年は Deno 使おうかな
なんとなく Deno のドキュメントを眺めてると思ったよりも標準ライブラリが充実していました
https://deno.land/std@0.209.0?doc

CSV/JSONC/YAML/TOML などのファイル形式を扱えます
HTML のエスケープや正規表現のエスケープ機能があります
日付のフォーマット機能があります(足し算や月末取得などはなくシンプルな機能のみみたい)

この辺は普段から JavaScript のデフォルト機能にあってほしいと思うものです
Deno の場合は標準ライブラリも URL を指定してダウンロードするわけですが 一応標準ライブラリという扱いで入ってるのは良いところだと思います

Deno が npm サポートや Node.js 互換に方針転換したくらいから興味を失ってましたが Node.js 用コードがほぼそのままで動くわけですし 便利機能が多いなら Deno の方を使うでもいいのかなと思いました
Node.js の新機能で Deno にあったから追加したというのも割りと見ます
Deno のほうが先に機能追加されてると言えます

TypeScript 関連でも Deno の対応が早いと感じてます
JavaScript に新規追加された機能で TypeScript 公式に型定義を提供してないので使うと型エラーになるというものがありましたが Deno では独自に対応してたりしました
動かすために Deno のコードから型定義をコピペしてきたこともありました

懸念は std の機能にまだ Unstable が多いところです
以前も全然安定してないし まだいいかと思って詳細は見なかった覚えがあります
でも結構経ってもまだ Unstable ですし 長期的に Unstable のままになってそうです
日付のフォーマットとか HTML のエスケープなどは API や動作が変わりそうなものでもないですし バージョンを固定してれば勝手に動かなくなってるわけでもないので 別にいいかなというところです
アプリからブラウザを開いて自動ログインされるやつ
PC でもモバイル端末でもアプリからブラウザを開く機能があります
アカウント設定画面とかそういう系でアプリ内に画面を用意してないのでウェブ側でやってねというあれです
このとき自動でログインしてくれるのとそうでないのがありますよね

自動でログインしてくれないと手動でログイン情報を入力しないといけなくてとても面倒です
アプリ側で認証済みなんだからログイン状態にしてって思います

中にはそれに対応してくれていてブラウザ側でログインしてなくてもアプリから開くとその時点で自動ログインされるものもあります
便利に思ってたのですがこれにも欠点がありました

アプリからブラウザを開くときって最後に操作したウィンドウで開かれます
ブラウザで複数のプロファイルを使い分けてると期待するのと違うプロファイルの方にログインされてしまって困ることがあります

永続するログインにせずそのタブでのみ有効なセッションとしてログイン状態なら別にいいかなと思いますが 完全なログインにするなら確認のワンクッションがほしいですね
Node.js 21.4 で Dirent に parentPath が追加された
Welcome to Node.js v21.4.0.
Type ".help" for more information.
> fs.writeFileSync("foo/bar/file", "text")
undefined
> await fs.promises.readdir("foo/bar")
[ 'file' ]
> await fs.promises.readdir("foo/bar", { withFileTypes: true })
[
Dirent {
name: 'file',
parentPath: 'foo/bar',
path: 'foo/bar',
[Symbol(type)]: 1
}
]

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

どうして全く同じものを?と思いましたが元々は path という名前でした
なのに実体は親のパスです
Dirent に対して path と言われたら name も含めたフルパスを期待するのに readdir の引数に渡されたパスになっています
これが紛らわしいので path を置き換える目的で追加されたようです
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 を参照できなくします

ただこれも トップレベルじゃないとできないです
親スコープを参照したいから関数内関数にしてるところがあって その中で今回みたいなことをしたい場合にはモジュールに分けるということはできないです