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


Fastify CLI で外部からプラグインを参照できない
Fastify CLI を使うと app.js というルートプラグインが各プラグインを autoload する作りになっています
app.js はこういうの

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

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

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

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

// Do not touch the following lines

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

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

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

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

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

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

app.foo()

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

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

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

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

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

[app.js]
// 前略

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

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

export default App
Prettier の折り返し位置
指定の文字数には全然達してないのに 折り返されたり折り返されなかったりして意味がわからないと思っていたら インデントサイズが影響していました

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

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

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

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

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

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

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

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

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

それよりも タブを使ってるのにタブの幅を見て動作を変えるのはやめてほしいです
タブを使うのは各自がインデント幅を自分の見やすい形にしていて 固定のタブ幅なんていうものはないはずです
相変わらずタブに対する理解のない動きをするんですよね
Node.js の preload 用の引数
preload するスクリプト用のコマンドライン引数という珍しいものを見ました

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

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



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

console.log("pre", args)

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

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

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

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

import readline from "node:readline"

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

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

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

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

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

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

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

import readline from "node:readline"

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

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

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

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

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

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

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

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

autoPrompt(rl)

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

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

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

autoPrompt(readline.Interface.prototype)
React の ErrorBoundary でキャッチしたエラーがコンソールに残る
React でエラーの表示を個別にせずまとめてやりたいなと思って コンポーネントの中で個別に表示する代わりに throw して親の ErrorBoundary で受け取るようにしてみてました
動作的には期待するものだったのですが ErrorBoundary でエラーをハンドルしてるのにコンソールに Uncaught Error が出るのですよね

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

import React, { useState } from "react"

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

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

static getDerivedStateFromError(error) {
return { error }
}

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

return this.props.children
}
}

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

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

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

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

export default App

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

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

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

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

StrictMode やフック関係でも要望が多く上がっていても変えないスタンスなところはそのままになってたりしますしあまり期待できそうにないですね
関数が受け取る引数の数で動作変えるのやめてほしい
一部のライブラリでは コールバック関数として渡した関数が受け取る引数の数で動作が変わります
こういうの

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

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

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

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

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

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

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

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

// ↑を↓にすると

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

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

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

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

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

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

// after callback process
}

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

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

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

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

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

JavaScript では引数の数があってる必要はないので 関数の引数の数は気にせず扱ってほしいですね
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]"}
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 のためだけに名前をつける必要があるのかはわかりませんけど 一時的にどれなのかわからないからつけるというときには良さそうです
遅延初期化処理が非同期処理のときにうまく動かなくて困った話
うまく動かないところがあって 原因を見つけるのに苦戦しました
たまにしか使わない処理なので最初の使用時に初期化処理を行うようにしてるものです

イメージ

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 が同期処理だとそもそも発生しない問題ですし あちこちが非同期処理になると不便なところも多いです
ページがロードする JavaScript のサイズ
色々なページでロードされる JavaScript のサイズを計測して 数 MB や数十 MB もあって多すぎるという内容の記事
https://tonsky.me/blog/js-bloat/

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

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

それよりも右上のダークモードの切り替えの挙動が思ってたのと違ったのが印象的でした
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 オブジェクトを作るもので破壊的なものではないです
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 で特定パッケージの依存関係のみインストールってできるのでしょうか
ウェブサーバーや別の常駐アプリと共通部分のパッケージはインストールが必要ですけど フロント側で使うパッケージはいらないです
入ることで特別問題はないのですが フロント系はパッケージ数が増えて重くなるので不要なサーバーでまで入れたくないなと思います
値とそれを更新する関数の扱い
値とそれを更新するための関数を作る時よくやるやつ

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 ではシンボルの情報も取れます
しかし キーがシンボルのオブジェクトを受け取るので結局これだけじゃアクセスできなかったりします

最近では プライベートプロパティを使ってもデバッグ用途だと不便は減りましたが 実行時にどうやってもアクセスできない不便さがあるので これくらいのゆるさのものが良いですね
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}
ページ内部からヘッダーを更新したい
メインのレイアウトがこんな構造のとき

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
来年は 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 や動作が変わりそうなものでもないですし バージョンを固定してれば勝手に動かなくなってるわけでもないので 別にいいかなというところです
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()

プロパティの参照のみならオブジェクトが優れると聞いたのでオブジェクトにしてみたけど 速度はほぼ違いなかった
プリミティブは 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 の更新をみるしかなさそうです
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 なので入力開始時にはイベントが起きなくてそこだけは違います
イベントリスナに設定した関数を簡単に置き換えたい
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 外でも使われるような方法なのに 何が気に入らないのでしょうね
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 などを追加する方法は取れなくもないですが ソースコードを暗黙的に書き換えるようなことはあまりしたくないですし 諦めてバックポートを待とうと思います(需要的に安定すればきっとされるはず)
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 をインポートしなくていいように分割してくれると更新は減るのですが 実行時にロードするモジュール数が増えることになるので それも良いとは言えないです

ただ現状だと ファイルごとにハッシュ値つけずにビルド結果を配置するフォルダにタイムスタンプをつけるのと大差ないようなものになってるので 気持ち悪さが残ります
関数の配置場所とスコープ
コードを読むときにスコープが広いと頭の中で読み取るときに大変ですよね
なので極力スコープは小さくしたいのですが 関数内だけ使う関数ってどこにあるのが良いのでしょうか?

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

}

fn2()
}

const fn3 = () => {

}

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

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

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

そういうことを考えると

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

const fn2 = () => {

}

const fn3 = () => {

}

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

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

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

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

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

const fn2 = () => {

}

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

const fn3 = () => {

}

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

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

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

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

にせず

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

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

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

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

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

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

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

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

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

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

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

const ref = useRef(true)

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

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

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

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

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

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

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

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

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

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

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

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

yarn add -D typescript

SDK をインストールします

yarn dlx @yarnpkg/sdks vscode

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

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

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

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

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

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

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

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

ライブラリの API の補完機能が欲しいだけなのにすごく面倒ですよね
TypeScript を使うプロジェクトなら最初からインストールしてるわけなので別にいいかとも思えますが シンプルな JavaScript のプロジェクトでやりたいものではないです
WebComponents でコンテキスト的なことをしたい
WebComponents で作ったコンポーネントで現在のコンテキストを受け取りたいことがあります
コンテキストというのは React のコンテキストみたいなもので 親で持っているデータのことです

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

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

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

イメージ

const wmap = new WeakMap()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

const plus1 = x => x + 1

console.log(@plus1 10)
// 11

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

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

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

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

デコレーターで書けると

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

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

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

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

この記法ではないものの 拡張機能として将来的もっと色々な使い方が考えられているようです
便利というより複雑という印象が強いですけど
https://github.com/tc39/proposal-decorators/blob/master/EXTENSIONS.md
閉じずに端に移動できるダイアログ
ダイアログで入力するものがあるとき 入力の途中でダイアログの裏側にある画面をみたいことってありますよね
でも裏側を見るためにダイアログを一旦閉じたら入力中の内容が消える場合が多いです
対処として別タブで同じページを開くこともありますが 面倒です
手間なく簡単に見れるようにしたいです

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

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

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

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

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

その後 もう少し豪華なサンプルも用意しました
ダイアログを通して要素の追加・編集ができるので 実際に裏側のものをコピーして入力などができます
https://nexpr.gitlab.io/public-pages/floatable-dialog/rich-example.html



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

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