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 を出せばいいかなと思うので今後使うかというと使わないかもです
DevTools でプライベートプロパティが直接操作できる
いつのころからかコンソールでプライベートプロパティを直接操作できるようになってました

const x = new class { #p = 1 }
x.#p++
// 1
x.#p++
// 2
x.#p++
// 3

外部からのプライベートプロパティ操作なのでエラーになるはずなのにエラーが起きず読み書きできてます
最初はバグ?と驚きました
昔はコンソールでもちゃんとエラーになってたはずです

便利機能だとするといつ追加されたのか気になったので調べてみたら Chrome 111 らしいです
(Chrome 111 のリリースは去年の 3 月です)
https://developer.chrome.com/blog/new-in-devtools-111?hl=ja

「その他のハイライト」というところに

デバッグを容易にするため、DevTools でプライベート クラスメンバーを使用した式の評価がサポートされるようになりました。(1381806

という形で記載されてました
重要だと思うのですがひっそりとしてます

個人的にはプライベートプロパティはほぼ使わないので全然知らなかったです

この便利機能が使えるのはコンソール内のみで eval や script タグ経由になると無効です

eval("x.#p++")
// Uncaught SyntaxError: Private field '#p' must be declared in an enclosing class
window.x = x
const script = document.createElement("script")
script.innerHTML = `window.x.#p++`
document.head.append(script)
// Uncaught SyntaxError: Private field '#p' must be declared in an enclosing class

デバッグし辛いというプライベートプロパティの最大の問題が解決されたのでこれまでより使いやすくなったと思います
ですが 使う側で回避策や機能追加のために直接プライベートプロパティを扱いたいってことはあるのでライブラリレイヤーであまり使ってほしくないというのは変わらないです
シンボルをキーにしても完全には隠せないよ
ネットで見かけた記事でシンボルの使い方について プライベートプロパティとして使うみたいなのがありました

const sym = Symbol()
class X {
[sym] = 1
print() {
console.log(this[sym])
}
}
const x = new X()
console.log(x[sym])
// 1
x.print()
// 1

これで sym をエクスポートせず X だけエクスポートすると x[sym] ができないから外部から直接アクセスできないってやつですね
使い方としては別にいいと思うのですが こうすることで外部からは完全にアクセス不可能で更新できないみたいなことが書かれてました

マイナーですが Object.getOwnPropertySymbols でシンボルを取得することができます
それ以外の Object.keys や for-in ではシンボルは取ってこれないです

console.log(Object.keys(x))
// []

for (const key in x) {
console.log(key)
}
// (no output)

const syms = Object.getOwnPropertySymbols(x)
console.log(x[syms[0]])
// 1

x[syms[0]] = 10
x.print()
// 10

困ることがあるとすれば 作る側が Symbol に説明を付けてない場合です
Symbol 関数の引数に文字列を入れていれば description プロパティで参照できます
それがないと複数のシンボルがあるとき何番目がどのプロパティなのか分かりづらいです

また Object.getOwnPropertyDescriptors ではシンボルの情報も取れます
しかし キーがシンボルのオブジェクトを受け取るので結局これだけじゃアクセスできなかったりします

最近では プライベートプロパティを使ってもデバッグ用途だと不便は減りましたが 実行時にどうやってもアクセスできない不便さがあるので これくらいのゆるさのものが良いですね
DevTools が日本語だとコマンドが辛すぎる
もう結構前からですが Edge で DevTools を起動すると日本語になってます
Chrome では上の方に日本語にもできますよって通知が出るだけでデフォルトは英語です

以前は英語に戻してましたが 環境ごとに毎回設定変更は面倒なので最近はそのままのことも増えました
ボタンなどの場所は変わらないので ネットで見かけた設定等を試す時以外は日本語でも困ってなかったです……が コマンドを使うと辛かったです

Ctrl-Shift-P で出るやつです
VSCode と同じ感じ

ここも日本語になってるので コマンドを日本語で打たないといけないです
選択肢の候補すら日本語です
漢字に変換しないと出てこないですし ひらがなカタカナも区別されています

最終的なコマンドが日本語なのは別にいいとしても フィルタして選択肢を出すところでは 元の英語版のコマンドからも検索してほしいですし ひらがなでカタカナと漢字も検索してほしいです
Microsoft あるあるかもですが必要以上のローカライズって不便になるだけだと思うんですよね
過去バージョンの Chrome の動作を確認する
npx @puppeteer/browsers install chrome@115

みたいな感じで 指定バージョンの Chromium をインストールできる
現時点では 113 以降がインストールできる
112 や 111 は 404 エラー

Puppeteer が使用してるデータソースがこの辺で 113 以降しかデータがない
https://googlechromelabs.github.io/chrome-for-testing/latest-versions-per-milestone.json
https://googlechromelabs.github.io/chrome-for-testing/latest-patch-versions-per-build.json

最新が 123 だし 最新+過去 10 件だけ残して古いのは消されてるのかも

それ以前なら この辺からダウンロード
https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html

Windows 用
https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Win_x64/

ここでダウンロードする場合は Chrome のバージョンじゃなくて通しのビルド番号で指定する必要あり
バージョンとの対応の参考

{
"113": {
"milestone": "113",
"version": "113.0.5672.63",
"revision": "1121455"
},
"114": {
"milestone": "114",
"version": "114.0.5735.133",
"revision": "1135570"
},
"115": {
"milestone": "115",
"version": "115.0.5790.170",
"revision": "1148114"
},
"116": {
"milestone": "116",
"version": "116.0.5845.96",
"revision": "1160321"
},
"117": {
"milestone": "117",
"version": "117.0.5938.149",
"revision": "1181205"
},
"118": {
"milestone": "118",
"version": "118.0.5993.70",
"revision": "1192594"
},
"119": {
"milestone": "119",
"version": "119.0.6045.105",
"revision": "1204232"
},
"120": {
"milestone": "120",
"version": "120.0.6099.109",
"revision": "1217362"
},
"121": {
"milestone": "121",
"version": "121.0.6167.85",
"revision": "1233107"
},
"122": {
"milestone": "122",
"version": "122.0.6261.6",
"revision": "1250580"
},
"123": {
"milestone": "123",
"version": "123.0.6265.0",
"revision": "1252026"
}
}

これの revision の数値を使う

Chromium だと開発中バージョンで機能が全部入ってないケースがあるので どの Chrome バージョンで変更があったかを確認するのに注意が必要
例えば 110 で動かなくて 111 で動いたら 111 で追加された機能かと思うけど 試した 110 のバージョンより後の 110 のリリースで追加される場合もありえる
マイルストーンに記載されてる version と revision は その Chrome バージョンの最後のビルドになってるのでこのバージョンなら大丈夫かも
ただこの情報が取れない古いバージョンだと そのバージョンの最後のビルドの revision を探すのが大変
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()

プロパティの参照のみならオブジェクトが優れると聞いたのでオブジェクトにしてみたけど 速度はほぼ違いなかった