POST するときのボディのサイズの話
nginx も Fastify もデフォルトは 1MB なので 10MB まで許可にしたいときに両方を変更しないといけないです
二重に管理するのは無駄ですし 片方だけしか更新してなかったみたいな設定漏れが出るようにしか思わないです
なので外側の nginx で制御して Node.js 側は無制限にしようとしました
ですが Fastify ではサイズを無制限にできないようです
サーバー全体やルート単位で bodyLimit を設定できますが 0 以下や Number.isInteger が false になるものは不正な設定として弾かれます
なので 1 以上の整数にしかできず これと必ず比較するので無制限にならないです
0 は無制限というありそうな設定はないですし Infinity も設定できません
Number.MAX_SAFE_INTEGER を設定しておけばいいといえばいいのですが なんかしっくりこないです
リバースプロキシを配置することを推奨するなら こういうところを快適にしてほしいものです
nginx を置かずに使ってると Node.js 側での制限が必須なので特になんとも思わなかったですが nginx を置いてみて気付いた不満点です
直接自分で Fastify インスタンスを作るところならまだいいですが Fastify CLI を使うとなると コマンドラインオプションで bodyLimit を指定することになります
コマンドラインで Number.MAX_SAFE_INTEGER 的なものを指定って結構面倒です
完全に一致しなくてもいいので 適当に 1TB などにしておくにしても 0 の数が多くなると見づらいです
Fastify CLI を使うと app.js というルートプラグインが各プラグインを autoload する作りになっています
app.js はこういうの
https://github.com/fastify/fastify-cli/blob/v6.1.1/templates/app-esm/app.js
基本は Fastify CLI から使われるのみですが この app.js をプラグインとして使うこともできます
主にテストなどで使われます
https://github.com/fastify/fastify-cli/blob/v6.1.1/templates/app-esm/test/helper.js
テスト以外でも app のインスタンスを作ってそこからグローバルのプラグインで追加したメソッドを使うと便利かなと思ったのですが
これで 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]
app.js はこういうの
https://github.com/fastify/fastify-cli/blob/v6.1.1/templates/app-esm/app.js
import path from 'path'
import AutoLoad from '@fastify/autoload'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
// Pass --options via CLI arguments in command to enable these options.
export const options = {}
export default async function (fastify, opts) {
// Place here your custom code!
// Do not touch the following lines
// This loads all plugins defined in plugins
// those should be support plugins that are reused
// through your application
fastify.register(AutoLoad, {
dir: path.join(__dirname, 'plugins'),
options: Object.assign({}, opts)
})
// This loads all plugins defined in routes
// define your routes in one of these
fastify.register(AutoLoad, {
dir: path.join(__dirname, 'routes'),
options: Object.assign({}, opts)
})
}
基本は Fastify CLI から使われるのみですが この app.js をプラグインとして使うこともできます
主にテストなどで使われます
https://github.com/fastify/fastify-cli/blob/v6.1.1/templates/app-esm/test/helper.js
テスト以外でも app のインスタンスを作ってそこからグローバルのプラグインで追加したメソッドを使うと便利かなと思ったのですが
import helper from "fastify-cli/helper.js"
const app = await helper.build([path_to_app_js], {})
app.foo()
これで foo が見つからないエラーでした
foo は plugins フォルダ内のプラグインで decorate しています
原因は Fastify のインスタンスに app.js のプラグインを register するときにカプセル化を有効にしていることでした
カプセル化されると app.js の内部では参照できますが 外側からは参照できません
app.js のデフォルトでは上のコードの通り プラグインの関数は fastify-plugin でラップされていません
ここはあまりいじることを想定されてないみたいですし ラップしないのが推奨なようです
実際 他のアプリと混ぜて同じサーバーで動かしたいときなどでは プラグイン全体をカプセル化して特定のプレフィックス以下に配置したいユースケースはありそうですし ラップしないのは正しい気がします
ですが 今回みたいにラップして 外側から参照できるようにカプセル化しないようにしたい場合もあります
となると app.js を使う側の Fastify CLI で fastify-plugin を通してほしいです
オプションで切り替えれるのかなと探したのですが そういうのはないようでした
https://github.com/fastify/fastify-cli/blob/v6.1.1/start.js#L149
現状だと仕方ないので app.js 側で fastify-plugin を通すしかなさそうです
他アプリと混ぜて使う想定がないなら常に fastify-plugin を通しても問題なさそうですが 環境変数を見て変えるということもできそうです
[app.js]
// 前略
const App = async function (fastify, opts) {
// 略
}
if (!process.env.ENCAPSULATE_APP) {
App = fp(App)
}
export default App
PowerShell を使ってたときに奇妙な動きをするところがありました
null の動作が一部だけ違います
代入してない変数は null で -eq $null すると True になります
-match するとマッチしないので False です
$null を代入した変数でも同じです
しかしコマンドの結果の場合は
-match のときに False が出ません
コマンドの実行結果なので空配列だったりするのかと思いましたが $x.gettype() は null のメソッド呼び出しとしてエラーになります
$x を表示しても空ですし -eq $null が True なので null のはずなのに
node コマンドは何も出力しないように -e "null" を指定しています
null を評価するだけでそれを出力しないので null が返ってくるわけではなく 標準出力に何も書き込まれないコマンドです
($x -match "^A") も null になっているのかと思って gettype() を呼び出してみるとなんと Object[] でした
意味がわからないのですがググっていると PowerShell の仕様として特殊な null が存在するようでした
https://learn.microsoft.com/ja-jp/powershell/scripting/learn/deep-dives/everything-about-null?view=powershell-7.4#empty-null
実行ファイルに限らず PowerShell 上の関数でも 何も返さないものの結果である null は特殊な null で「Empty null」と呼ばれるものみたいです
実体は System.Management.Automation.Internal.AutomationNull.Value の値らしいです
この値は null と比較したり値の評価が行われるときは null として扱われるようです
なので
ですし 通常の null か判断できません
判断するには特殊な方法で配列に入れて Count を見ればいいようです
普通の null なら 1 で Empty null なら 0 になります
すごく特殊ですし こういうのはやめてほしいです
変なことしていて無理やり都合を合わせてる感
PHP みを感じます
もっと一貫性があって例外的なものが少ないようしてほしいです
null の動作が一部だけ違います
代入してない変数は null で -eq $null すると True になります
-match するとマッチしないので False です
PS C:\> echo ($val -eq $null)
True
PS C:\> echo ($val -match "^A")
False
$null を代入した変数でも同じです
PS C:\> $v = $null
PS C:\> echo ($v -eq $null)
True
PS C:\> echo ($v -match "^A")
False
しかしコマンドの結果の場合は
PS C:\> $x = (node -e "null")
PS C:\> echo ($x -eq $null)
True
PS C:\> echo ($x -match "^A")
PS C:\>
-match のときに False が出ません
コマンドの実行結果なので空配列だったりするのかと思いましたが $x.gettype() は null のメソッド呼び出しとしてエラーになります
$x を表示しても空ですし -eq $null が True なので null のはずなのに
node コマンドは何も出力しないように -e "null" を指定しています
null を評価するだけでそれを出力しないので null が返ってくるわけではなく 標準出力に何も書き込まれないコマンドです
($x -match "^A") も null になっているのかと思って gettype() を呼び出してみるとなんと Object[] でした
PS C:\> ($x -match "^A").gettype()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
意味がわからないのですがググっていると PowerShell の仕様として特殊な null が存在するようでした
https://learn.microsoft.com/ja-jp/powershell/scripting/learn/deep-dives/everything-about-null?view=powershell-7.4#empty-null
実行ファイルに限らず PowerShell 上の関数でも 何も返さないものの結果である null は特殊な null で「Empty null」と呼ばれるものみたいです
実体は System.Management.Automation.Internal.AutomationNull.Value の値らしいです
この値は null と比較したり値の評価が行われるときは null として扱われるようです
なので
PS C:\> [System.Management.Automation.Internal.AutomationNull]::Value -eq $null
True
ですし 通常の null か判断できません
判断するには特殊な方法で配列に入れて Count を見ればいいようです
PS C:\> @($v).count
1
PS C:\> @($x).count
0
普通の null なら 1 で Empty null なら 0 になります
すごく特殊ですし こういうのはやめてほしいです
変なことしていて無理やり都合を合わせてる感
PHP みを感じます
もっと一貫性があって例外的なものが少ないようしてほしいです
指定の文字数には全然達してないのに 折り返されたり折り返されなかったりして意味がわからないと思っていたら インデントサイズが影響していました
このコードはフォーマットしてもそのままでしたが abcde にして 5 文字になると
のように折り返されました
別のところに持っていくと
abcd まででも折り返されました
printWidth は同じです
理由がわからずバージョンの違いかと思ってバージョンをあわせても同じ挙動
相変わらずの意味不明な挙動と思ってたら tabWidth の違いでした
ab のところが tabWidth を超えると折り返されてるようです
上のときは tabWidth が 4 なので 4 文字の abcd で折り返されず abcde の 5 文字で折り返されていました
下のときは tabWidth が 2 だったので 2 文字で折り返されず 3 文字で折り返されていました
ここの文字数を見る意味がよくわからないです
printWidth に収まってるのですから 気にせず 1 行にまとめてくれればいいのですけど
それよりも タブを使ってるのにタブの幅を見て動作を変えるのはやめてほしいです
タブを使うのは各自がインデント幅を自分の見やすい形にしていて 固定のタブ幅なんていうものはないはずです
相変わらずタブに対する理解のない動きをするんですよね
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 行にまとめてくれればいいのですけど
それよりも タブを使ってるのにタブの幅を見て動作を変えるのはやめてほしいです
タブを使うのは各自がインデント幅を自分の見やすい形にしていて 固定のタブ幅なんていうものはないはずです
相変わらずタブに対する理解のない動きをするんですよね
最近は IE も消えたので Sec-Fetch-Site が same-origin であることをチェックして CSRF 対策としています
トークンを送ったり 独自のヘッダーをつけたりに比べるとかなり楽です
ですが この方法 問題がありました
Sec-Fetch-Site ヘッダーはブラウザが自動で送信するものですが HTTPS サイト限定のようです
HTTP サイトには送信されません
これのせいで HTTP のページで使おうとすると必要なヘッダーが送信されないので全て不正なリクエストとみなされます
一般公開するようなもので CSRF 対策が必要なサービスなら基本 HTTPS でしょうけど そういったもの以外ではありえます
一般には公開せず ネットワーク内のみでアクセスできるサーバーだったり 開発環境だったり
開発環境でも localhost という名前なら特別で HTTP でも HTTPS 同様に Sec-Fetch-Site ヘッダーが送信されていました
ですが 別端末からアクセスするケースなど localhost 以外でアクセスするケースもあって そういうときに動かなくなります
開発中やネットワーク内のみならそもそも攻撃する人がいないはずなので Sec-Fetch-Site ヘッダーを見ないようにして対処してますが 開発中に動かさないと動作確認ができないですし なにかとスッキリしないです
HTTP でもとりあえず送ってくれたらいいのに
トークンを送ったり 独自のヘッダーをつけたりに比べるとかなり楽です
ですが この方法 問題がありました
Sec-Fetch-Site ヘッダーはブラウザが自動で送信するものですが HTTPS サイト限定のようです
HTTP サイトには送信されません
これのせいで HTTP のページで使おうとすると必要なヘッダーが送信されないので全て不正なリクエストとみなされます
一般公開するようなもので CSRF 対策が必要なサービスなら基本 HTTPS でしょうけど そういったもの以外ではありえます
一般には公開せず ネットワーク内のみでアクセスできるサーバーだったり 開発環境だったり
開発環境でも localhost という名前なら特別で HTTP でも HTTPS 同様に Sec-Fetch-Site ヘッダーが送信されていました
ですが 別端末からアクセスするケースなど localhost 以外でアクセスするケースもあって そういうときに動かなくなります
開発中やネットワーク内のみならそもそも攻撃する人がいないはずなので Sec-Fetch-Site ヘッダーを見ないようにして対処してますが 開発中に動かさないと動作確認ができないですし なにかとスッキリしないです
HTTP でもとりあえず送ってくれたらいいのに
yarn の v2 以降で pnp を使うときの問題です
berry はただでさえ使いづらいところが多いのに これのせいで特定の用途の場合は yarn v1 を使うようにしてます
適当に pnp のプロジェクトを作って パッケージを入れてそれを使うようにします
package.json はこんな感じです
[index.js]
package.json のあるフォルダで
を実行すると問題なく動きます
次に この package.json があるフォルダを dir1/foo/bar に配置して dir1 をカレントディレクトリとして実行しようとします
になりますが これだと依存パッケージを解決できずエラーです
カレントディレクトリを変更するために --cwd をつけて
としてもエラーです
scripts にして yarn run を使えば行けるかなと思って試してみました
しかしこれもダメでした
yarn run の場合は cwd なしだと scripts を認識できず start が見つからないエラーになるものです
それが 認識できて node index.js を実行してからエラーになってるので うまく動いてくれても良さそうなのですけど
エラーを見るとこんな感じです
気になるのは v1.22.22 になってるところです
--cwd を考慮せず 今のカレントディレクトリで package.json を探して packageManager から yarn のバージョンを決めていそうです
--cwd を処理する時点では 1.22.22 で固定された後なので 期待する 4.1.1 にならないという動きみたいです
そうなるとわざわざカレントディレクトリを移動させないといけないですし それをしたくないようなところでは使えません
なにかと yarn の pnp は使いづらいのですよね
yarn v1 で node_modules を使う場合は カレントディレクトリを気にせず実行する .js ファイルから node_modules を探すのでカレントディレクトリによる問題は起きないです
未だに yarn berry には完全移行できてなくて 問題点が多いので いっそ pnpm の方に移ったほうがいいのかなと最近思ってます
berry はただでさえ使いづらいところが多いのに これのせいで特定の用途の場合は yarn v1 を使うようにしてます
適当に pnp のプロジェクトを作って パッケージを入れてそれを使うようにします
yarn init -2
yarn add dayjs
package.json はこんな感じです
{
"name": "pkg",
"packageManager": "yarn@4.1.1",
"dependencies": {
"dayjs": "^1.11.10"
},
"type": "module"
}
[index.js]
import dayjs from "dayjs"
console.log(dayjs().format("YYYY-MM-DD"))
package.json のあるフォルダで
yarn node index.js
を実行すると問題なく動きます
次に この package.json があるフォルダを dir1/foo/bar に配置して dir1 をカレントディレクトリとして実行しようとします
yarn node foo/bar/index.js
になりますが これだと依存パッケージを解決できずエラーです
カレントディレクトリを変更するために --cwd をつけて
yarn --cwd foo/bar node index.js
としてもエラーです
scripts にして yarn run を使えば行けるかなと思って試してみました
{
"name": "pkg",
"packageManager": "yarn@4.1.1",
"dependencies": {
"dayjs": "^1.11.10"
},
"type": "module",
"scripts": {
"start": "node index.js"
}
}
yarn --cwd foo/bar run start
しかしこれもダメでした
yarn run の場合は cwd なしだと scripts を認識できず start が見つからないエラーになるものです
それが 認識できて node index.js を実行してからエラーになってるので うまく動いてくれても良さそうなのですけど
エラーを見るとこんな感じです
root@787bf525a896:/dir1# yarn --cwd foo/bar node index.js
yarn node v1.22.22
warning package.json: No license field
node:internal/modules/esm/resolve:854
throw new ERR_MODULE_NOT_FOUND(packageName, fileURLToPath(base), null);
^
気になるのは v1.22.22 になってるところです
--cwd を考慮せず 今のカレントディレクトリで package.json を探して packageManager から yarn のバージョンを決めていそうです
--cwd を処理する時点では 1.22.22 で固定された後なので 期待する 4.1.1 にならないという動きみたいです
そうなるとわざわざカレントディレクトリを移動させないといけないですし それをしたくないようなところでは使えません
なにかと yarn の pnp は使いづらいのですよね
yarn v1 で node_modules を使う場合は カレントディレクトリを気にせず実行する .js ファイルから node_modules を探すのでカレントディレクトリによる問題は起きないです
未だに yarn berry には完全移行できてなくて 問題点が多いので いっそ pnpm の方に移ったほうがいいのかなと最近思ってます
React でエラーの表示を個別にせずまとめてやりたいなと思って コンポーネントの中で個別に表示する代わりに throw して親の ErrorBoundary で受け取るようにしてみてました
動作的には期待するものだったのですが ErrorBoundary でエラーをハンドルしてるのにコンソールに Uncaught Error が出るのですよね
実装ミスでもあるのかなと思って探してもなさそうで ErrorBoundary 側に設定があるのかと探してみてもなさそうでした
単純なケースでもこういう動作になるので React 側でハンドルしてもさらに throw してるみたいです
↓のコードで「Uncaught Error: ERROR!」がコンソールに出ます
ハンドル済みで期待する動作なのにエラーがコンソールに出るのは気持ち悪いです
単純に catch されなかったエラーをログに出さないならこれでできます
しかしこうすると React の ErrorBoundary でハンドルしてないその他のエラーも非表示になって困ります
getDerivedStateFromError で Error オブジェクトにマークを付けて マークがついていれば preventDefault をしようかと考えたのですが window に自分でつけるハンドラのほうが先に処理が行われるようで うまくいかないです
先に React 側のエラーハンドルの処理をしたいので 中で queueMicrotask で遅延させると先にコンソールにエラーが表示されてしまいます
探してみると issue がありましたが 対応する予定はなさそうです
https://github.com/facebook/react/issues/15069
StrictMode やフック関係でも要望が多く上がっていても変えないスタンスなところはそのままになってたりしますしあまり期待できそうにないですね
動作的には期待するものだったのですが 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 やフック関係でも要望が多く上がっていても変えないスタンスなところはそのままになってたりしますしあまり期待できそうにないですね
リストとか木構造とかってオブジェクトに相互の参照が入ってることがよくあります
DOM でも children と parentElement だったり nextElementSibling と previousElementSibling だったりがあります
ミュータブルなものなら簡単に実現できますが イミュータブルなデータだと相互に参照を入れられない気がします
普通に JavaScript でやると
これがイミュータブルだとすると 参照をいれるところがこうなります
こうしたときに child2_new.prev に child1_new が入っていますが child1_new.next は古い child2 です
イミュータブルしかできない言語だとこういう構造は扱わないのでしょうか
直接参照をもたせず 間接的に持たせて自分で参照するならできそうですが 手間が多いです
更新することも考えるとあまりやりたい方法ではないです
イミュータブルな言語でやり方をググってみると 関数にして遅延評価することで実現してる例がありました
たしかに実行時に参照先が評価される言語なら関数を作る時点で参照できなくても大丈夫そうです
ただこれでも変更は簡単ではなく どこかを書き換えると参照先も変更が必要になり全体を作り直すことになります
なのでそういうデータ構造は持たず単一方向にするのが普通で なぜ循環する参照が必要になるのか聞かれてるような感じでした
DOM でも children と parentElement だったり nextElementSibling と previousElementSibling だったりがあります
ミュータブルなものなら簡単に実現できますが イミュータブルなデータだと相互に参照を入れられない気がします
普通に JavaScript でやると
const child1 = {
parent: null,
children: [],
prev: null,
next: null,
}
const child2 = {
parent: null,
children: [],
prev: null,
next: null,
}
const parent = {
parent: null,
children: [],
prev: null,
next: null,
}
child1.next = child2
child1.parent = parent
child2.prev = child1
child2.parent = parent
parent.children.push(child1, child2)
これがイミュータブルだとすると 参照をいれるところがこうなります
const child1_new = { ...child1, next: child2, parent }
const child2_new = { ...child2, prev: child1_new, parent }
こうしたときに child2_new.prev に child1_new が入っていますが child1_new.next は古い child2 です
イミュータブルしかできない言語だとこういう構造は扱わないのでしょうか
直接参照をもたせず 間接的に持たせて自分で参照するならできそうですが 手間が多いです
const parent_id = Math.random()
const child1_id = Math.random()
const child2_id = Math.random()
const parent = {
parent: null,
children: [child1_id, child2_id],
prev: null,
next: null,
}
const child1 = {
parent: parent_id,
children: [],
prev: null,
next: child2_id,
}
const child2 = {
parent: parent_id,
children: [],
prev: child1_id,
next: null,
}
const refs = {
[parent_id]: parent,
[child1_id]: child1,
[child2_id]: child2,
}
console.log(child1 === refs[refs[child1.next].prev])
// true
更新することも考えるとあまりやりたい方法ではないです
イミュータブルな言語でやり方をググってみると 関数にして遅延評価することで実現してる例がありました
const parent = {
parent: null,
children: [() => child1, () => child2],
prev: null,
next: null,
}
const child1 = {
parent: () => parent,
children: [],
prev: null,
next: () => child2,
}
const child2 = {
parent: () => parent,
children: [],
prev: () => child1,
next: null,
}
console.log(child1 === child1.next().prev())
// true
たしかに実行時に参照先が評価される言語なら関数を作る時点で参照できなくても大丈夫そうです
ただこれでも変更は簡単ではなく どこかを書き換えると参照先も変更が必要になり全体を作り直すことになります
なのでそういうデータ構造は持たず単一方向にするのが普通で なぜ循環する参照が必要になるのか聞かれてるような感じでした
ローカル
↓
サーバー A → サーバー C
↓
サーバー B
こんな感じで A を通して B や C に ssh で接続しています
ssh 接続したまましばらくすると自動で切断されてることがあります
broken pipe みたいなエラーが表示されるやつです
接続してることを完全に忘れてることもあるので長時間なら切断されることは別にいいのですが 予想外のところで問題がありました
ローカル → サーバー A
ここの接続は切れているのに
サーバー A → サーバー B
の接続は維持されてるようです
ps でプロセスを見ると ssh 接続されています
また サーバー B にログインした状態で less やプログラミング言語の REPL などインタラクティブなものを使っているとそのプロセスも残っています
psql や mysql コマンドを使ってると DB とのコネクションが残ります
これは色々と問題です
ファイルを開いたままになっていたり メモリをずっと確保したままになっていたり
とりあえず ps でプロセスリストを見て それらしいものを kill していきます
困るのがどれがどれかわからないのですが ps の STAT 列に + がついているプロセスはフォアグラウンドプロセスグループに属してるらしいのでこれを目印にできます
ログインしてユーザーが操作してコマンドを実行すればフォアグラウンドなので + がついてるもので現在ログインして操作中でなければ kill していいはずです
自分しかログインしない環境なら簡単です
でも根本的な解決としてプロセスが残ってほしくないんですよね
A の ssh 接続設定にタイムアウトを入れる感じでしょうか
tmux とか入れれるならそれを使えば セッションを復元できますが 踏み台用途のところはあまり色々インストールしたくないのと tmux 系は不便もあるのであまり使ってないんですよね
Node.js を使っていて構文エラーがあったのですが原因の場所がわからなかったです
いつもならファイルと行番号を表示してくれるのですが なぜか表示されません
使ってるツールの都合なのかといろいろ試してたら 単純に Node.js を使うだけでも再現できました
mjs ファイルを動的にロードするとダメみたいです
拡張子によるものではないので type="module" でも同じですがここではわかりやすくするため cjs/mjs の拡張子にします
ロードされる mod.cjs / mod.mjs はどっちも構文エラーになるものです
[mod.cjs]
[mod.mjs]
これらをロードする main.cjs / main.mjs を作ります
[main.cjs]
[main.mjs]
main のそれぞれを実行します
どちらの場合でも mjs ファイルをロードしたときは構文エラーの場所が表示されていませんね
構文エラーならエディタ上ですぐにわかるのであまり困らないのですが 今回は高機能なエディタがない環境だったので苦戦しました
ちなみに「動的」なインポートでのみ起きます
静的な import の場合はちゃんとエラーの場所が表示されます
[main-s.mjs]
Node.js バージョンは 18, 20, 21 で確認しましたがどれもこうなるようです
いつもならファイルと行番号を表示してくれるのですが なぜか表示されません
使ってるツールの都合なのかといろいろ試してたら 単純に 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 で確認しましたがどれもこうなるようです
キャッシュがなんかおかしいなーと思って調べたら 「/」 のページだけ長期キャッシュになってました
静的ファイル置き場のファイルはこんな感じ
SPA で 404 のときに index.html を返します
静的ファイルのサーブでは cache-control が設定されています
「/」 のときは静的ファイルのサーブとして index.html がサーブされて 「/foo」 などではエラーハンドラの処理として index.html がサーブされていました
結果 「/」 でだけキャッシュが有効になってました
index.html は静的ファイル置き場に置かないほうがいいのでしょうか
ファイルごとにヘッダー設定ができるものなら .js や .css みたいなリソースファイルの拡張子の場合だけキャッシュさせたり assets みたいなフォルダ単位でキャッシュさせるとかすればいいのですが 全体設定しかできないものだったので困りました
静的ファイル置き場のファイルはこんな感じ
index.html
assets/index-abcdef.js
assets/...
SPA で 404 のときに index.html を返します
静的ファイルのサーブでは cache-control が設定されています
「/」 のときは静的ファイルのサーブとして index.html がサーブされて 「/foo」 などではエラーハンドラの処理として index.html がサーブされていました
結果 「/」 でだけキャッシュが有効になってました
index.html は静的ファイル置き場に置かないほうがいいのでしょうか
ファイルごとにヘッダー設定ができるものなら .js や .css みたいなリソースファイルの拡張子の場合だけキャッシュさせたり assets みたいなフォルダ単位でキャッシュさせるとかすればいいのですが 全体設定しかできないものだったので困りました
久々に使った PC で AlmaLinux9 の WSL を起動したらこんなエラーメッセージが出て動かせませんでした
WSL の更新や再起動でも変わらないです
Ubuntu の方は問題なく動いてます
wsl コマンドだとどうかなと 「wsl -d almalinux9」 を実行すると動きました
Windows アプリのショートカットがおかしくなったみたいです
そのアプリから開く必要は特にないので ショートカットを新しく作りました
右クリックメニューのショートカットの作成で項目の場所に
ショートカット名を 「AlmaLinux9」
として作るだけです
ショートカットのダブルクリックで起動します
今回問題が起きた環境は WindowsTerminal が入ってないので確認できてないですが VSCode からディストリビューションを選択する方法だと問題なかったので アプリのショートカットを通さず ディストリビューション一覧を取得してディストリビューション名で起動するところは問題なさそうです
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 からディストリビューションを選択する方法だと問題なかったので アプリのショートカットを通さず ディストリビューション一覧を取得してディストリビューション名で起動するところは問題なさそうです
もう結構前からですが Edge で DevTools を起動すると日本語になってます
Chrome では上の方に日本語にもできますよって通知が出るだけでデフォルトは英語です
以前は英語に戻してましたが 環境ごとに毎回設定変更は面倒なので最近はそのままのことも増えました
ボタンなどの場所は変わらないので ネットで見かけた設定等を試す時以外は日本語でも困ってなかったです……が コマンドを使うと辛かったです
Ctrl-Shift-P で出るやつです
VSCode と同じ感じ
ここも日本語になってるので コマンドを日本語で打たないといけないです
選択肢の候補すら日本語です
漢字に変換しないと出てこないですし ひらがなカタカナも区別されています
最終的なコマンドが日本語なのは別にいいとしても フィルタして選択肢を出すところでは 元の英語版のコマンドからも検索してほしいですし ひらがなでカタカナと漢字も検索してほしいです
Microsoft あるあるかもですが必要以上のローカライズって不便になるだけだと思うんですよね
Chrome では上の方に日本語にもできますよって通知が出るだけでデフォルトは英語です
以前は英語に戻してましたが 環境ごとに毎回設定変更は面倒なので最近はそのままのことも増えました
ボタンなどの場所は変わらないので ネットで見かけた設定等を試す時以外は日本語でも困ってなかったです……が コマンドを使うと辛かったです
Ctrl-Shift-P で出るやつです
VSCode と同じ感じ
ここも日本語になってるので コマンドを日本語で打たないといけないです
選択肢の候補すら日本語です
漢字に変換しないと出てこないですし ひらがなカタカナも区別されています
最終的なコマンドが日本語なのは別にいいとしても フィルタして選択肢を出すところでは 元の英語版のコマンドからも検索してほしいですし ひらがなでカタカナと漢字も検索してほしいです
Microsoft あるあるかもですが必要以上のローカライズって不便になるだけだと思うんですよね
Windows 環境で 公式サイトからダウンロードしたインストーラーを使って Node.js をインストールした後に npx を使うとエラーが出ました
no such file or directory
Sandbox 下で Node.js だけをインストールして試すと再現します
バージョンは LTS の 20 系です
メッセージのまま AppData\Roaming\npm が無いらしいのですが 無いなら作って欲しいのに作ってくれないみたいです
手動で作ってもいいですが適当になにかのパッケージを npm でグローバルインストールすると作られます
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 (適当なパッケージ名)
普段あまり SVG 画像を使わないということもあってか flexbox と組み合わせたときに予想外な動きになって困りました
[00.svg]
[01.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 を追加したほうが良さそうです
<!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 を追加したほうが良さそうです
メインのレイアウトがこんな構造のとき
Outlet のコンポーネントがその時点のページを表示します
このページ内の処理で Header や Footer に表示するものを設定したいです
ページタイトルとかそういうの
やろうとすると Main で Context を提供し setHeader みたいな関数にアクセスできるようにして 各ページがマウント時に呼び出します
あまり気持ちの良い方法ではないです
以前は Outlet のような使い方をする Router を使っていなくて Outlet のところに Router を配置する感じでした
それだと Header などレイアウトを外に配置するメリットもあまりなかったので Page が Main コンポーネントを使うという構造でした
こっちのほうが自然な感じです
とはいえ全部のページで Main を使う必要がありますし Outlet のような機能を持つ Router を使う場合だと Router でマッチしたコンポーネントは外側のコンポーネントの一部として表示する形になります
ページのコンポーネントがレイアウトを選択するのではなく ルーター側でレイアウト内にページを配置するようにしてるので こういう作りにならないです
いい方法はないものなんでしょうか
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 に依存せず 引数やグローバルなデータから返り値が決まるものはメソッドという形にはしたくないです
たとえば
this を使うメソッド something から method を呼び出すのですが method は中で this を使わず引数だけで結果が決まります
こういう関数は A の中に入れたくないので外側に出して単純な関数にします
扱うデータが A と深い関係があるなら A の static メソッドにすることもありますが A だけに関係するわけじゃないなら別用途で使いたいときに A を通したくないので関数です
こういう感じ
いつもは特に問題もなかったのですが 今回は少し困ったことがありました
fn みたいな関数がいくつかあり 相互に呼び出していて その深い部分で this に入ってる関数を使いたいということがありました
これの getValue は A のメソッドを使いたいです
また 再起や循環した呼び出しがあり 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 的なことはできるのですが ブラウザでは使えないです
たとえば
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
パッケージを追加して rye sync を実行したらなぜかエラーになりました
原因がわからなくて困ってましたが パッケージとは関係なく src フォルダ内にデフォルトで用意されてるフォルダを消すとまずいようでした
foo という名前で rye init したら src フォルダの中はこんな感じになっています
自分で記述するファイルは単一ファイルでよかったのでこう変更してました
実行するときは
で直接実行してました
foo フォルダを消したのがまずかったようで 消したあとに rye sync をするとエラーになりました
パッケージを追加しなければ最初以降に rye sync することもなかったので気づかなかったです
エラーは production lockfile の生成中に subprocess-exited-with-error というもの
エラー全体はすごく長くて最後の方を見ても lock ファイルの書き込みに失敗したくらいしかわからないです
まだ 0.15 だし そういうこともある?
とか思いましたが 念のため新プロジェクトで同じパッケージを入れて rye sync してみると問題なく動いてました
違いというと src フォルダ内のフォルダ構成くらいなので ここを元に戻してみるとエラーが出なくなりました
このフォルダは必須だったみたいです
エラーを見返すと中間部分にそれらしいメッセージがありました
内部で使用している hatch というツールにビルド対象を設定する必要があってそこでエラーになっていたようです
tool.hatch.build.targets.wheel の設定が必要みたいです
設定ファイルに記載していないとプロジェクト名から推測してくれるようで プロジェクト名と同じフォルダあったので初期状態だと動作していたようです
main.py というファイルに置き換えていたので↓のような設定を pyproject.toml に追加すると foo フォルダを消しても動作しました
パッケージとして公開するつもりがなく パッケージをインストールするためだけに rye を使っていてもこういう設定が必要なのは少し面倒ですね
原因がわからなくて困ってましたが パッケージとは関係なく 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 を使っていてもこういう設定が必要なのは少し面倒ですね
PC でもモバイル端末でもアプリからブラウザを開く機能があります
アカウント設定画面とかそういう系でアプリ内に画面を用意してないのでウェブ側でやってねというあれです
このとき自動でログインしてくれるのとそうでないのがありますよね
自動でログインしてくれないと手動でログイン情報を入力しないといけなくてとても面倒です
アプリ側で認証済みなんだからログイン状態にしてって思います
中にはそれに対応してくれていてブラウザ側でログインしてなくてもアプリから開くとその時点で自動ログインされるものもあります
便利に思ってたのですがこれにも欠点がありました
アプリからブラウザを開くときって最後に操作したウィンドウで開かれます
ブラウザで複数のプロファイルを使い分けてると期待するのと違うプロファイルの方にログインされてしまって困ることがあります
永続するログインにせずそのタブでのみ有効なセッションとしてログイン状態なら別にいいかなと思いますが 完全なログインにするなら確認のワンクッションがほしいですね
アカウント設定画面とかそういう系でアプリ内に画面を用意してないのでウェブ側でやってねというあれです
このとき自動でログインしてくれるのとそうでないのがありますよね
自動でログインしてくれないと手動でログイン情報を入力しないといけなくてとても面倒です
アプリ側で認証済みなんだからログイン状態にしてって思います
中にはそれに対応してくれていてブラウザ側でログインしてなくてもアプリから開くとその時点で自動ログインされるものもあります
便利に思ってたのですがこれにも欠点がありました
アプリからブラウザを開くときって最後に操作したウィンドウで開かれます
ブラウザで複数のプロファイルを使い分けてると期待するのと違うプロファイルの方にログインされてしまって困ることがあります
永続するログインにせずそのタブでのみ有効なセッションとしてログイン状態なら別にいいかなと思いますが 完全なログインにするなら確認のワンクッションがほしいですね
情報がないエラーが出ていて困ったのですが cause が出ていないだけでした
inner の情報が表示されません
クリックで開けるのかと思いましたがそういうこともできませんでした
cause に限らず AggregateError の errors プロパティも同様に表示されません
console.log で出ている変数なら右クリックから "Store object as global variable" でグローバル変数に持ってきてから自分で error.cause にアクセスすれば情報が見れるかと思ったのですが エラーオブジェクトはなぜかグローバル変数に持ってこれないみたいです
すでにログされたものはどうしようもなさそうです
対処方法はログ方法を変更して console.dir を使うことです
HTMLElement 等を XML 表示にせずオブジェクト表示させるのに使うものです
エラー表示もそれらと同じ扱いみたいで console.dir でオブジェクト表示を強制すると内側もプロパティも表示されるようになります
調べてみると 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 <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)
}
Node.js 21 で組み込みの fetch が安定しましたが 20 からたいして変更なさそうだしと 20 でも使ってました
それで動作が違うところがあり バージョンの違いで動作が違う部分があったのかと思ったのですが 20 で揃えても違ってました
違った部分はエラーが起きたときのエラーオブジェクトです
片方はよく見る感じのエラーです
もう片方は cause の中が AggregateError になっていて 二つのエラーが含まれていました
同じエラーでなぜ 2 個分含まれているのか疑問でしたがよく見ると address が IPv6 と IPv4 で別になっていました
IPv6 が有効になっている環境で IPv6 で接続できないと自動で IPv4 でも試してくれてるみたいです
ちなみに OS は同じ AlmaLinux9 だったのですが IPv6 が有効な環境と無効な環境がありました
それで動作が違うところがあり バージョンの違いで動作が違う部分があったのかと思ったのですが 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 が有効な環境と無効な環境がありました
dnf でパッケージをインストールしようとしたらこんなエラーが出ました
Signature not supported. Hash algorithm SHA1 not available.
最近の AlmaLinux9 環境だと SHA1 を使った署名はサポートされてないようです
しかしパッケージは SHA1 なのでエラーになってインストールできません
完全なエラーはこんな感じ
とはいえインストールする必要があります
古いアルゴリズムも許可するには
を使えば良いみたいです
このあとで再度インストールしようとするとエラーがなく実行できます
もとに戻すにはこれです
古いアルゴリズムを許可するのではなくそもそもチェック自体をスキップするという方法もあります
こっちは dnf install に 「--nogpgcheck」 オプションをつけるだけです
Signature not supported. Hash algorithm SHA1 not available.
最近の AlmaLinux9 環境だと SHA1 を使った署名はサポートされてないようです
しかしパッケージは SHA1 なのでエラーになってインストールできません
完全なエラーはこんな感じ
Importing GPG key 0x9B1BE0B4:
Userid : "NSolid <nsolid-gpg@nodesource.com>"
Fingerprint: 6F71 F525 2828 41EE DAF8 51B4 2F59 B5F9 9B1B E0B4
From : /etc/pki/rpm-gpg/NODESOURCE-NSOLID-GPG-SIGNING-KEY-EL
Is this ok [y/N]: y
warning: Signature not supported. Hash algorithm SHA1 not available.
Key import failed (code 2). Failing package is: nodejs-2:20.10.0-1nodesource.x86_64
GPG Keys are configured as: file:///etc/pki/rpm-gpg/NODESOURCE-NSOLID-GPG-SIGNING-KEY-EL
The downloaded packages were saved in cache until the next successful transaction.
You can remove cached packages by executing 'dnf clean packages'.
Error: GPG check FAILED
とはいえインストールする必要があります
古いアルゴリズムも許可するには
update-crypto-policies --set LEGACY
を使えば良いみたいです
このあとで再度インストールしようとするとエラーがなく実行できます
もとに戻すにはこれです
update-crypto-policies --set DEFAULT
古いアルゴリズムを許可するのではなくそもそもチェック自体をスキップするという方法もあります
こっちは dnf install に 「--nogpgcheck」 オプションをつけるだけです
Skypack を使っていたところでエラーが起きるようになってました
エラーの内容はコンストラクタで this にアクセスする前に super() を呼び出さないといけないというものです
Must call super constructor in derived class before accessing 'this' or returning from derived constructor
Skypack 以外を通して使うと発生してないので元のソースコードには問題はないはずです
エラー箇所を見てみるとプライベートプロパティの変換によるものでした
プライベートプロパティがあると constructor の最初で初期化処理を行うように変換されていて それが this を使うのに 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 ですし もうしばらくは様子見したいところです
エラーの内容はコンストラクタで 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 ですし もうしばらくは様子見したいところです
ググって出てくるページと出てこないページがあるので Search Console で見てみるとインデックスに登録されてないページもありました
1 ヶ月以上前の記事でも登録されてないのがあったりで どこかからリンクされない限り登録されなそうです
一応ライブドアブログには ping 機能があって Google などには通知されてるはず と思ったのですが Google は ping のサポートを終了したという情報も出てきます
https://developers.google.com/search/blog/2023/06/sitemaps-lastmod-ping?hl=ja
エンドポイントとしては 6 月から 6 ヶ月後なので今年中は動いてそうですが あくまでエンドポイントが生きてるだけでレスポンスはエラーにならず返ってくるもののすでに意味はないという状態なんでしょうか
ただ これはサイトマップの ping であり 個別記事のものではない気がします
ライブドアブログが送信先として設定しているのはこれです
http://blogsearch.google.com/ping/RPC2
ググるとこれはもう何年も前に終了してるという情報も見かけます
どっちにしても ping 機能は意味ないと考えて良さそうです
そうなると Google のクロールに任せるしかないのですが その結果登録されてないのですよね
ブログですし トップページから新着記事へのリンクはあるのですけど
調べていると 小さいサイト (ページ数が 500 以下) はサイトマップが不要だけどページ数が多いならサイトマップを登録したほうが良さそうという情報もあります
https://developers.google.com/search/docs/crawling-indexing/sitemaps/overview?hl=ja
このブログですら記事ページは 1000 近くありますし あったほうがいいのかもしれません
サイトマップは Atom や RSS で良いみたいです
ライブドアブログだと自動で作られていて atom.xml や index.rdf にアクセスすると見れます
でもこれらはトップページの HTML 内で参照されていますし自動認識されてるようにも思いますけど
ただ これは PC 版のみで モバイル版の HTML には含まれてませんでした
モバイルファーストでクロールしてるのなら Google はこれらの URL を知らないという可能性もあります
とりあえず Search Console からサイトマップとして登録してみました
atom.xml の方は Google が期待するフォーマットと一致しないみたいでエラーになりました
index.rdf だと問題なかったのでライブドアブログでサイトマップ登録するならこっちを使うと良さそうです
1 ヶ月以上前の記事でも登録されてないのがあったりで どこかからリンクされない限り登録されなそうです
一応ライブドアブログには ping 機能があって Google などには通知されてるはず と思ったのですが Google は ping のサポートを終了したという情報も出てきます
https://developers.google.com/search/blog/2023/06/sitemaps-lastmod-ping?hl=ja
エンドポイントとしては 6 月から 6 ヶ月後なので今年中は動いてそうですが あくまでエンドポイントが生きてるだけでレスポンスはエラーにならず返ってくるもののすでに意味はないという状態なんでしょうか
ただ これはサイトマップの ping であり 個別記事のものではない気がします
ライブドアブログが送信先として設定しているのはこれです
http://blogsearch.google.com/ping/RPC2
ググるとこれはもう何年も前に終了してるという情報も見かけます
どっちにしても ping 機能は意味ないと考えて良さそうです
そうなると Google のクロールに任せるしかないのですが その結果登録されてないのですよね
ブログですし トップページから新着記事へのリンクはあるのですけど
調べていると 小さいサイト (ページ数が 500 以下) はサイトマップが不要だけどページ数が多いならサイトマップを登録したほうが良さそうという情報もあります
https://developers.google.com/search/docs/crawling-indexing/sitemaps/overview?hl=ja
このブログですら記事ページは 1000 近くありますし あったほうがいいのかもしれません
サイトマップは Atom や RSS で良いみたいです
ライブドアブログだと自動で作られていて atom.xml や index.rdf にアクセスすると見れます
でもこれらはトップページの HTML 内で参照されていますし自動認識されてるようにも思いますけど
<link rel="alternate" type="application/rss+xml" title="RSS" href="https://let.blog.jp/index.rdf" />
<link rel="alternate" type="application/atom+xml" title="Atom" href="https://let.blog.jp/atom.xml" />
ただ これは PC 版のみで モバイル版の HTML には含まれてませんでした
モバイルファーストでクロールしてるのなら Google はこれらの URL を知らないという可能性もあります
とりあえず Search Console からサイトマップとして登録してみました
atom.xml の方は Google が期待するフォーマットと一致しないみたいでエラーになりました
index.rdf だと問題なかったのでライブドアブログでサイトマップ登録するならこっちを使うと良さそうです
Node.js でプログラムを実行した際に メインモジュールとして実行されたかを判断したいです
メインモジュールとして実行されたときだけ追加の処理をして それ以外のライブラリとして読み込まれたときは何もしないという感じに使います
よく Python で見る
をやりたいです
CJS の頃はシンプルな方法で実現できました
ドキュメントにも記載されている方法です
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 外でも使われるような方法なのに 何が気に入らないのでしょうね
メインモジュールとして実行されたときだけ追加の処理をして それ以外のライブラリとして読み込まれたときは何もしないという感じに使います
よく 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 外でも使われるような方法なのに 何が気に入らないのでしょうね
py -0
-0 数字の 0
-0p にすると場所が表示されます
Windows で Python を入れるとついてくる py ランチャー
複数の場所やインストール方法で入れた Python を起動できるので便利です
しばらく使ってなかった環境で どのバージョンが入ってるのか一覧を見ようとしたのですが ヘルプにはそれらしい機能がありません
ランチャーなら見れそうなのに
ぐぐってみると普通にコマンドがあるようです
しかし 認識されないコマンドのようでエラーでした
原因は単純にバージョンが古かったみたいです
Python を新しく入れても py ランチャーは更新されないみたいです
カスタムインストールにしても py ランチャーの項目は灰色になってインストールできなくなってました
管理者権限が必要みたいなことが書いてるので管理者権限でインストーラーを起動してみましたが同じでした
一旦手動でアンインストールが必要みたいです
アンインストール後に再度 Python をインストールすると 最新の py ランチャーが入りました
ヘルプに -0 が出ていますし -0 でインストール済みバージョンの一覧が見れます
Prettier 3.1.0 で条件演算子が昔の動きに戻ったと聞いたり 新モードも追加されたと聞いて 試してました
ただ今回の記事の内容は新モードではなく通常モードについてです
Prettier の更新で条件演算子がフラットになる問題が起きて以降 Prettier のバージョンを固定して上げないようにしているプロジェクトがありました
この数年は触れることもなかったので 別フォーマッターに移行せず Prettier のままです
とりあえずこれを通常モードの 3.1.0 にしてみました
差分なしになるのが期待の動作だったのですが 結果は謎のスペースが入るというものでした
[元]
[フォーマット後]
インデントにはタブを使っています
元のコードは b, c の行は 1 つのタブで d, e の行は 2 つのタブです
このままが期待するものです
しかしフォーマット後は d, e の行は 1 つのタブとそれに続く 2 つのスペースです
一応 タブサイズは 4 にはしているのに 2 つのスペースのインデントが追加されました
以前 インデントをフラットにした理由の一つにネストが深くなるというのがあるので 浅くしたいというのはわからなくもないですが タブを使ってるときはタブに揃えてもらいたいです
ただ すでに似たような問題としてこういうのがあります
[元]
[フォーマット後]
これも元のコードが期待するものです
しかしフォーマットすると 「{」 と 「}」 の縦の位置をそろえようとしてスペースが入ります
タブを使用する設定なので 「}」 の前には 1 つのタブと 2 つのスペースです
タブのインデントは見た目を揃える以上に論理的な構造を視覚化するためのものだと思うのですが Prettier 開発者は考えが違うみたいなので仕方ないですね
……と諦めてましたが issues を探してみるとすでに存在して バグ とラベル付けされています
https://github.com/prettier/prettier/issues/15655
修正されるのでしょうか?
今回の変更は 「前の動きに戻す」 というものだったはずなのでそういう意味では意図したものではなさそうですし修正されるのかもしれません
とはいえ 2 つめの例のような問題が残るのなら もうどっちでもいいのですけどね
でもこれをバグとするなら タブを使うモードでテストしてないというわけで やっぱり Prettier 開発者はタブを重視してなさそうです
デフォルトをタブにするという issue にコメントが 450 以上ついてますが これも実現するのか怪しいところがありますね
https://github.com/prettier/prettier/issues/7475
ただ今回の記事の内容は新モードではなく通常モードについてです
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 をインポートしなくていいように分割してくれると更新は減るのですが 実行時にロードするモジュール数が増えることになるので それも良いとは言えないです
ただ現状だと ファイルごとにハッシュ値つけずにビルド結果を配置するフォルダにタイムスタンプをつけるのと大差ないようなものになってるので 気持ち悪さが残ります
ファイルの中身が一緒なら同じファイル名で 違えば別のファイル名になります
変更がなければ以前のバージョンのキャッシュを使えるので 効率が良くなるのですが 思ってたほどじゃなかったです
というのも ほぼ毎回ハッシュ値が変わります
まず index.js があります
これがエントリポイントで ここから各モジュールをロードします
どこかのモジュールが変わればそれをインポートするときのファイル名が変わるので index.js は毎回変化し 新しいハッシュ値になります
index.js には静的にインポートされるファイルが全て含まれ 動的にインポートされるものが別ファイルという分割のされ方になります
動的にインポートされるモジュールが使うモジュールは index.js に含まれてるケースが多いので 各モジュールは index.js をインポートすることになります
ただ index.js は上で書いたように 毎回のようにハッシュ値が変わります
その index.js の名前をコード上に含むわけなので 内容を変更してないモジュールでもビルドの出力ファイルとしては変更があり モジュールのハッシュ値も変わります
結果としてほぼすべてのモジュールのハッシュ値が毎回変わってしまいます
動的にインポートするモジュールが index.js をインポートしなくていいように分割してくれると更新は減るのですが 実行時にロードするモジュール数が増えることになるので それも良いとは言えないです
ただ現状だと ファイルごとにハッシュ値つけずにビルド結果を配置するフォルダにタイムスタンプをつけるのと大差ないようなものになってるので 気持ち悪さが残ります
コードを読むときにスコープが広いと頭の中で読み取るときに大変ですよね
なので極力スコープは小さくしたいのですが 関数内だけ使う関数ってどこにあるのが良いのでしょうか?
こんな感じのもので fn2 は fn1 の中でしか使いません
fn1 でしか使わないので fn1 の中 つまり今の場所でいいように思います
しかし fn1 のコードが長くなり fn1 の中に fn2 相当の関数が 5 や 10 になってくると fn1 関数がとても長くなり見づらくなります
関数内関数を無視して fn1 だけを見ると数十行程度なのに fn1 全体としては 数百行とかいうケースもありえます
また fn1 内のローカル変数がいくつもあると fn2 などの関数はそれらすべてを見ることができます
fn2 は外側を一切見ない関数だとしてもスコープ的には見れるので 読むときにはそれらを参照するかもしれないとして読む必要があります
そういうことを考えると
でもいいのではと思うのですよね
fn2 は fn1 内のローカル変数を見ることができません
見れるのはグローバルやモジュール内の変数のみです
fn1 のローカル変数とは切り離されているので 読みやすくなります
また fn1 の中は fn1 で直接行う処理のみなので fn1 の行数も減ってスッキリします
しかし fn2 がモジュールのトップレベルに出てくることで fn3 が fn2 を参照できるようになってしまいます
今度は fn3 が fn2 を使うかもという部分を考えないといけません
いずれも全体として短いこれくらいだとどっちでもいいレベルですが 長くなってくると読みづらくなるんですよね
ただ fn3 が fn2 を見れても関数呼び出しだけです
それに対して fn2 が fn1 のローカル変数を見れるのは値の書き換えができるので複雑度が上がります
なのでどっちかというと関数内関数を減らしたほうがいいのかなと思ったりはするものの 本当にベストなのか疑問が残ります
別の手段としてモジュールを分けてしまうのがいいのかとも思ったりはしてますが モジュール数が結構増えそうで 結局試してません
分けるとしたらこういう感じです
fn1 が読みづらいくらい長くなるなら fn1 だけを別モジュールに切り出して fn1 と fn2 をトップレベルに置きます
エクスポートは fn1 だけにして fn3 からは fn2 を参照できなくします
ただこれも トップレベルじゃないとできないです
親スコープを参照したいから関数内関数にしてるところがあって その中で今回みたいなことをしたい場合にはモジュールに分けるということはできないです
なので極力スコープは小さくしたいのですが 関数内だけ使う関数ってどこにあるのが良いのでしょうか?
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 も更新したいとき useEffect を使わず最初の state を更新するところで関連する state も更新したいです
にせず
(この例だと +1 するだけなので state にする必要ないし 処理が重たくても state1 から計算できるなら useMemo でいいけど これは説明を簡単にするためのものなのでそこは気にしないでください)
しかし 更新方法が setState の関数を使うタイプの場合に困りました
setState の関数の中で別の setState を呼び出すこともできますが なんか気持ち悪さがありますし それってどうなのと思います
そんな使い方を見た覚えもないですし 推奨される方法ではない気がします
完全に state1 と state2 の更新タイミングが揃うものならオブジェクトにまとめて 1 つの state にしてもいいのですが そうとも限らないです
それに state2 の更新としてるところが localStorage への書き込みだったり React の 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 とは関係ない処理の場合もありますし
最近は新しく環境を作るときに React 18 なので useEffect が 2 回呼び出される問題の影響が増えてきました
多くの場合はちゃんとクリーンアップ処理を書けば大丈夫なのですが そうもいかないケースがあります
例えば マウント時に state にデータを追加するような処理がある場合 2 回呼び出されるので 最初から 2 つの要素が存在することになります
あまり問題にはならないですが エラーがないと 1 つだけみたいなケースで 開発中は常に 2 つあることになって 実際のものと見た目が異なるので気持ち悪かったりします
また キーになる情報がタイムスタンプくらいだと useEffect が 2 回呼び出された場合はキーが同じになってしまうことがあります
開発時のみの都合で別のキーを自動生成したり index をキーにするのはあまり良い方法とも思えません
とりあえず最初の実行かどうかを ref を使って判断するのですが 追加で ref が増えますし 開発時の都合で特別なことをするのはあまり気持ちの良いものではないです
クリーンアップ関数を使ってできないかと思ってやってみたのはこれです
一瞬だけ遅延させます
そうすることで 2 回目の useEffect 呼び出しで 1 回目の処理をキャンセルできます
ref が不要で useEffect の中だけで完結するのが良いです
ただし僅かな遅延があるので 見た目上 ちらつき等になる可能性もあります
また 正常な動作として高速で 2 回呼び出された場合にキャンセルが発生します
それも気にするなら ref に頼ることになりそうです
最近は React で新しいものを作るたびこういう不満点を感じて別のフレームワークを探しては 不足点があってまだ使えないなぁを繰り返してます
多くの場合はちゃんとクリーンアップ処理を書けば大丈夫なのですが そうもいかないケースがあります
例えば マウント時に 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 で新しいものを作るたびこういう不満点を感じて別のフレームワークを探しては 不足点があってまだ使えないなぁを繰り返してます
Yarn4 から PnP を使おうとしています
速度面では問題もあるものの WSL やネットワーク越しの環境でサーバーを起動するようなケースはむしろ速くなってあまりデメリットもなさそうです
フロント関係でも Vite は Yarn PnP をサポートしてるので 開発用サーバーを起動できますし HMR も使えています
これなら PnP を普段遣いにしてもいいかも と思ってました
しかし VSCode との相性に問題がありました
VSCode は Yarn PnP でインストールされたパッケージを認識しないので 補完機能が動かなくなります
使い慣れてるライブラリだと特に問題を感じてなかったのですが 使い慣れないものや機能が多いライブラリを使ったときに補完が出ないのが結構不便でした
VSCode では JavaScript でもライブラリの関数の引数などは情報を見ることができるようになっています
選択式のところは補完の候補から選んだりもできます
これがないと毎回ライブラリの API のページを見ないといけないのが不便です
対応することはできるのですが 補完機能には TypeScript の機能が使われているので 実際には TypeScript を使わないのにライブラリの補完のためだけに TypeScript をプロジェクトにインストールしないといけないです
やり方はこんな感じです (参考)
VSCode の拡張機能の ZipFS をインストールします
これは Yarn のチームでメンテしてるもののようです
汎用的なものではなく Yarn PnP のためだけのものみたいですね
次にプロジェクトに TypeScript をインストールします
SDK をインストールします
これを実行すると今のプロジェクトで使われてるツールを自動で認識して必要なものをインストールしてくれるようです
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 では↓のような内容が追加されています
ここのパスにも記載がある .yarn/sdks/typescript に TypeScript の SDK がインストールされています
ライブラリの API の補完機能が欲しいだけなのにすごく面倒ですよね
TypeScript を使うプロジェクトなら最初からインストールしてるわけなので別にいいかとも思えますが シンプルな JavaScript のプロジェクトでやりたいものではないです
速度面では問題もあるものの 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 のプロジェクトでやりたいものではないです
印刷向けの CSS 機能がブラウザより整っていたりして良いツールなのですが 日本語で使おうとすると問題点があり 将来性の期待はあるものの実用はしてない状況でした
久々に使って 色々更新されてるし問題点も対応されてるかなと思ったのですが まだ対応されてませんでした
フォント関係なので CJK のフォントを使わないならほとんど関係なさそうですし 優先度は低そうです
内容的に将来的にも対応されるかもわからないです
問題のひとつは CID キー付きフォントに対応してないことです
https://github.com/Kozea/WeasyPrint/issues/1593
日本語のフォントで有名な
源ノ角ゴシック (Source han sans)
源ノ明朝 (Source han serif)
など のフォントは CID キー付きフォントになってるようです
効率的に保存できるらしいので 多くの文字を保持しているフォントはたぶんほとんどこれなのでしょう
これらのフォントを WeasyPrint で使うと問題なく PDF は出力できるのですが 一部の環境では CJK の文字が表示されません
Edge などブラウザでは問題ないようなのですが PDF である以上 一般的なビューワー全てで見れないというのは使えない場合があります
ところで 源ノ明朝って 「げんのみんちょう」 って読むらしいです
「みなもと」 だと思ってたのに
(みなもとのよりともって呼んでる人多いよね?)
そういう問題があるので別のフォントを探して見つかったのが IPA フォント
これは CID キー付きフォントではないようです
しかしこれは別の問題がありました
太字・斜体にできません
最近の可変フォントを除けば 太字や斜体って別にフォントが用意されているものです
regular とか medium など太さがフォントの名前についてたりします
そうでないフォントは フォントとして太字のデータはないので 表示する側で擬似的に太字にしています
自動でやる分 ちゃんとフォントが別にあるものより汚いみたいです
WeasyPrint はこの疑似的に太字にしたり斜体にしたりする機能をサポートしていません
太字や斜体にしたいなら それに対応したフォントを使うか b や strong タグのスタイルに別の太字・斜体フォントを指定してという方針みたいです
しかし IPA フォントを始め多くのフォントって太さの違いを別フォントにしてないです
これのせいで太字が使えません
Windows にインストールされている標準のフォントを見ると MS ゴシックや MS 明朝はフォントが太さのフォントは用意されていないようです
メイリオや游ゴシックはフォントが用意されていました
しかし WeasyPrint を実行するのは Linux 環境です
個人で使う程度なら気にしなくてもいいのかもですが ライセンス的にはアウトだと思います
それにこれらのフォントが CID キー付きフォントでないかは未確認です
そんなわけで WeasyPrint で使える良い日本語フォントがなくて使えない状況です
マイナーフォントを探せば CID キー付きではなく太さごとにフォントファイルが用意されたフォントもあるかもしれないですが あまり変わった書体ではなく よくある一般的なものにしたいのですよね
将来的に WeasyPrint がこれらをサポートしてくれるといいのですが あまり期待もできません
太さの問題は過去バージョンは対応していたのに 内部的に PDF 生成やフォントを扱うツールを変えてから対応しなくなったようです
しばらく修正されないままですし 使うツールの都合上対応が難しそうなことも書かれています
https://github.com/Kozea/WeasyPrint/issues/1470
太さが違うフォントを指定すればいいという回避策もある以上期待できないです
CID キー付きフォントの方は調べてもいまいちよくわからないものです
レガシーという記述があったりなかったり
CID フォントと言っても種類があるようで sfnt wrapped という形式の CID フォントがレガシーらしいです
OpenType のものはそれとは違って問題ないように書いてるページもありました
https://ccjktype.fonts.adobe.com/wp-content/uploads/2018/12/cjkv2e-pp387-393.pdf
逆に CID キー付きフォントは この sfnt wrapped の CID を含むレガシーフォーマットと言う記載もあります
https://github.com/fontforge/fontforge/wiki/Community-guidelines#d6-cid-keyed-fonts
よくわからないですが フォント系を扱う有名なツールの fontforge がレガシーだから積極的にサポートしないといってるくらいです
WeasyPrint が対応する期待はできません
ブラウザ自体がもっと印刷や PDF 対応を頑張ってくれるといいのですが 印刷需要が少ないのであまり期待はできないですね
久々に使って 色々更新されてるし問題点も対応されてるかなと思ったのですが まだ対応されてませんでした
フォント関係なので CJK のフォントを使わないならほとんど関係なさそうですし 優先度は低そうです
内容的に将来的にも対応されるかもわからないです
問題のひとつは CID キー付きフォントに対応してないことです
https://github.com/Kozea/WeasyPrint/issues/1593
日本語のフォントで有名な
源ノ角ゴシック (Source han sans)
源ノ明朝 (Source han serif)
など のフォントは CID キー付きフォントになってるようです
効率的に保存できるらしいので 多くの文字を保持しているフォントはたぶんほとんどこれなのでしょう
これらのフォントを WeasyPrint で使うと問題なく PDF は出力できるのですが 一部の環境では CJK の文字が表示されません
Edge などブラウザでは問題ないようなのですが PDF である以上 一般的なビューワー全てで見れないというのは使えない場合があります
ところで 源ノ明朝って 「げんのみんちょう」 って読むらしいです
「みなもと」 だと思ってたのに
(みなもとのよりともって呼んでる人多いよね?)
そういう問題があるので別のフォントを探して見つかったのが IPA フォント
これは CID キー付きフォントではないようです
しかしこれは別の問題がありました
太字・斜体にできません
最近の可変フォントを除けば 太字や斜体って別にフォントが用意されているものです
regular とか medium など太さがフォントの名前についてたりします
そうでないフォントは フォントとして太字のデータはないので 表示する側で擬似的に太字にしています
自動でやる分 ちゃんとフォントが別にあるものより汚いみたいです
WeasyPrint はこの疑似的に太字にしたり斜体にしたりする機能をサポートしていません
太字や斜体にしたいなら それに対応したフォントを使うか b や strong タグのスタイルに別の太字・斜体フォントを指定してという方針みたいです
しかし IPA フォントを始め多くのフォントって太さの違いを別フォントにしてないです
これのせいで太字が使えません
Windows にインストールされている標準のフォントを見ると MS ゴシックや MS 明朝はフォントが太さのフォントは用意されていないようです
メイリオや游ゴシックはフォントが用意されていました
しかし WeasyPrint を実行するのは Linux 環境です
個人で使う程度なら気にしなくてもいいのかもですが ライセンス的にはアウトだと思います
それにこれらのフォントが CID キー付きフォントでないかは未確認です
そんなわけで WeasyPrint で使える良い日本語フォントがなくて使えない状況です
マイナーフォントを探せば CID キー付きではなく太さごとにフォントファイルが用意されたフォントもあるかもしれないですが あまり変わった書体ではなく よくある一般的なものにしたいのですよね
将来的に WeasyPrint がこれらをサポートしてくれるといいのですが あまり期待もできません
太さの問題は過去バージョンは対応していたのに 内部的に PDF 生成やフォントを扱うツールを変えてから対応しなくなったようです
しばらく修正されないままですし 使うツールの都合上対応が難しそうなことも書かれています
https://github.com/Kozea/WeasyPrint/issues/1470
太さが違うフォントを指定すればいいという回避策もある以上期待できないです
CID キー付きフォントの方は調べてもいまいちよくわからないものです
レガシーという記述があったりなかったり
CID フォントと言っても種類があるようで sfnt wrapped という形式の CID フォントがレガシーらしいです
OpenType のものはそれとは違って問題ないように書いてるページもありました
https://ccjktype.fonts.adobe.com/wp-content/uploads/2018/12/cjkv2e-pp387-393.pdf
逆に CID キー付きフォントは この sfnt wrapped の CID を含むレガシーフォーマットと言う記載もあります
https://github.com/fontforge/fontforge/wiki/Community-guidelines#d6-cid-keyed-fonts
よくわからないですが フォント系を扱う有名なツールの fontforge がレガシーだから積極的にサポートしないといってるくらいです
WeasyPrint が対応する期待はできません
ブラウザ自体がもっと印刷や PDF 対応を頑張ってくれるといいのですが 印刷需要が少ないのであまり期待はできないですね
WebComonents を使ってると やっぱり標準に合わせたくなります
img や iframe などを見ると URL だけ渡せば後はやってくれます
データを取得するのもコンポーネントに任せたいところです
ただ取得もやると 1 コンポーネントが重たくなるなーと思ったり
またデータはすでに別の理由で手元にあるときがあり そういうときでも URL から再取得するのはムダです
取得と使用で分けようかなと思ったものの どうしようか
書きやすさ的に React で考えます
React ならとりあえずフックでしょうか
フックがデータを取得し それを渡します
コンポーネントとして考えると取得するコンポーネントの props に取得したデータを使用するコンポーネントを渡すとかでしょうか
DataViewer に追加の props も渡したいことを考えると render 関数として渡す方がよいかもしれないです
内側に表示するものなので見た目的に children として渡します
これらだとすでにデータを取得している場合は DataViewer だけを使うことができます
反対に DataViewer の方を外側に持ってきて FetchData を渡す方法も考えられます
DataViewer が内部で FetchData へ onData 関数を渡して これが呼び出されたら自身の state に受け取ったデータをセットする感じです
FetchData は中でデータを取得し 取得したら props で受け取った onData 関数を呼び出します
DatViewer の props に data も用意してこっちがあればそのデータを使い なければ FetchData を使ってデータを取得とできます
この場合はわざわざ FetchData をコンポーネントにする必要もないので fetch に関数を渡すだけでもいいと思います
ただこうするとイベント時の処理ではなく 自身で関数を呼び出すわけなので API を直接呼び出すのと大差ないです
useEffect とかが入ってきます
API を直接呼び出すのと比べて 受け取ったあとのフォーマットやエラー時の処理などをやらなくて済むくらいです
となると FetchData を外側に持ってくるものか フックを使うものです
子コンポーネントに渡すレンダー関数を渡すというのは React ならではのものなので フックの方でしょうか
フックも React 固有になるので実際にはただの関数として用意するようになりそうです
エラー表示を考えるとコンポーネントである FetchData の方が表示も管理できて便利かもしれません
ただエラー表示が固定であればです
使う側でカスタムするなら 結局エラー表示を制御するのでそれなら 関数にしてもいいかもです
ただデータの管理を親コンポーネントでしないといけなくなるので DataViewer がいっぱいあるところだと 使うほうが面倒にもなります
難しいですね
img や iframe などを見ると URL だけ渡せば後はやってくれます
データを取得するのもコンポーネントに任せたいところです
ただ取得もやると 1 コンポーネントが重たくなるなーと思ったり
またデータはすでに別の理由で手元にあるときがあり そういうときでも URL から再取得するのはムダです
取得と使用で分けようかなと思ったものの どうしようか
書きやすさ的に React で考えます
React ならとりあえずフックでしょうか
フックがデータを取得し それを渡します
const data = useData(url)
return data.state === "loaded"
? <DataViewer data={data.content} />
: <Loader />
コンポーネントとして考えると取得するコンポーネントの props に取得したデータを使用するコンポーネントを渡すとかでしょうか
<FetchData url={url} Viewer={DataViewer} />
DataViewer に追加の props も渡したいことを考えると render 関数として渡す方がよいかもしれないです
内側に表示するものなので見た目的に children として渡します
<FetchData url={url}>
{data => <DataViewer data={data} />}
</FetchData>
これらだとすでにデータを取得している場合は DataViewer だけを使うことができます
反対に DataViewer の方を外側に持ってきて FetchData を渡す方法も考えられます
<DataViewer FetchData={FetchData} />
DataViewer が内部で FetchData へ onData 関数を渡して これが呼び出されたら自身の state に受け取ったデータをセットする感じです
FetchData は中でデータを取得し 取得したら props で受け取った onData 関数を呼び出します
DatViewer の props に data も用意してこっちがあればそのデータを使い なければ FetchData を使ってデータを取得とできます
この場合はわざわざ FetchData をコンポーネントにする必要もないので fetch に関数を渡すだけでもいいと思います
<DataViewer fetch={fetchData} />
ただこうするとイベント時の処理ではなく 自身で関数を呼び出すわけなので API を直接呼び出すのと大差ないです
useEffect とかが入ってきます
API を直接呼び出すのと比べて 受け取ったあとのフォーマットやエラー時の処理などをやらなくて済むくらいです
となると FetchData を外側に持ってくるものか フックを使うものです
子コンポーネントに渡すレンダー関数を渡すというのは React ならではのものなので フックの方でしょうか
フックも React 固有になるので実際にはただの関数として用意するようになりそうです
エラー表示を考えるとコンポーネントである FetchData の方が表示も管理できて便利かもしれません
ただエラー表示が固定であればです
使う側でカスタムするなら 結局エラー表示を制御するのでそれなら 関数にしてもいいかもです
ただデータの管理を親コンポーネントでしないといけなくなるので DataViewer がいっぱいあるところだと 使うほうが面倒にもなります
難しいですね
Node.js ってけっこう互換性は保たれてるので メジャーアップデートしても問題なく動くことがほとんどでした
特にフロント側は Webpack や Parcel や Vite などでビルドするだけなのでバージョンの違いはほぼ影響ないです
なので フロント側のビルドではプロジェクトごとに Node.js バージョンの管理はしてないです
Windows に入れてる共通の Node.js でビルドしてます
新機能が使いたくなったら LTS の範囲で更新してるものです
最近はそこまで使いたい新機能もなかったので その環境では Node.js 16 が EOL になる少し前くらいに Node.js 18 にしました
それからしばらくして 1 年近くビルドしてなかったプロジェクトを少し更新してビルドしようとしたところエラーになってました
node_modules は前回のビルド以降更新してなくてどうしてエラーが起きるのか最初は原因がわからなかったのですが 調べてみると Node.js のバージョンが原因でした
Node.js 16 から Node.js 18 に上げると package.json の exports フィールドの動きに違いが出ます
Node.js 18 では exports で公開してるもの以外は直接 import/require できなくなります
このせいで一部のライブラリ間の読み込みで失敗していました
正確に書くと postcss 系です
postcss の exports に書かれていないファイルを postcss 系のパッケージのモジュールが直接読み込もうとしてエラーでした
バージョンアップで解決されるはずですが こういう内部で依存関係として自動的にインストールされているものって そこだけを更新し辛いのですよね
このプロジェクトは全体的に古いもので構成されていて できるだけ変更はしたくないです
いつもはとりあえず全部最新にしてるのであまり困らないですが 古いもので最低限の更新だけするというのはやりづらいです
lock ファイルで問題のパッケージのバージョンだけ書き換えることも考えましたが 依存関係が複雑なところだとおかしくなることもありそうで直接触るのは避けたいです
ビルドにしか使わないなら EOL でも別にいいので Node.js のバージョンを古いままにするのが正解なのかもですね
Docker を使って古いバージョンの Node.js でビルドしてみました
ビルドはファイルの読み取りが多い処理なのでファイルが Windows 側にあるとかなり遅いです
もしかすると コンテナ内でリポジトリをクローンしてビルドして push するほうが早いかもしれません
特にフロント側は Webpack や Parcel や Vite などでビルドするだけなのでバージョンの違いはほぼ影響ないです
なので フロント側のビルドではプロジェクトごとに Node.js バージョンの管理はしてないです
Windows に入れてる共通の Node.js でビルドしてます
新機能が使いたくなったら LTS の範囲で更新してるものです
最近はそこまで使いたい新機能もなかったので その環境では Node.js 16 が EOL になる少し前くらいに Node.js 18 にしました
それからしばらくして 1 年近くビルドしてなかったプロジェクトを少し更新してビルドしようとしたところエラーになってました
node_modules は前回のビルド以降更新してなくてどうしてエラーが起きるのか最初は原因がわからなかったのですが 調べてみると Node.js のバージョンが原因でした
Node.js 16 から Node.js 18 に上げると package.json の exports フィールドの動きに違いが出ます
Node.js 18 では exports で公開してるもの以外は直接 import/require できなくなります
このせいで一部のライブラリ間の読み込みで失敗していました
正確に書くと postcss 系です
postcss の exports に書かれていないファイルを postcss 系のパッケージのモジュールが直接読み込もうとしてエラーでした
バージョンアップで解決されるはずですが こういう内部で依存関係として自動的にインストールされているものって そこだけを更新し辛いのですよね
このプロジェクトは全体的に古いもので構成されていて できるだけ変更はしたくないです
いつもはとりあえず全部最新にしてるのであまり困らないですが 古いもので最低限の更新だけするというのはやりづらいです
lock ファイルで問題のパッケージのバージョンだけ書き換えることも考えましたが 依存関係が複雑なところだとおかしくなることもありそうで直接触るのは避けたいです
ビルドにしか使わないなら EOL でも別にいいので Node.js のバージョンを古いままにするのが正解なのかもですね
Docker を使って古いバージョンの Node.js でビルドしてみました
ビルドはファイルの読み取りが多い処理なのでファイルが Windows 側にあるとかなり遅いです
もしかすると コンテナ内でリポジトリをクローンしてビルドして push するほうが早いかもしれません
DOM 外に state としてフォームの入力を保持する系ライブラリを使うとき今でも迷うところ
input は基本 値を文字列で扱います
type が number とか date でも value は文字列です
一応 valueAsNumber とか valueAsDate とかありはしますが 扱いづらいところがあるのでこれらは使いません
state 側では数値や Date 型など文字列以外で持っていることも少なくないです
そういうときにどうするかです
ひとつは input の入出力の際に毎回変換することです
value に渡すときに文字列に変換して 変更のイベント時に文字列から state の型に変換して state を更新します
それぞれの input の受け渡しのコードが長くなるのと 都度変換が必要なところが微妙です
日付型を文字列にフォーマットするくらいなら パフォーマンスに影響することはほとんどないと思いますが ユーザーの入力のたびにやるのってとてもムダに思えます
また 一度の再レンダリングで全部の input 分の変換を行うので 数が多い場合はパフォーマンスに影響無いとも言い切れないです
それに input は input そのものじゃなくてコンポーネントになってることもあります
その場合は変換がもっと重い処理のこともあります
別のオブジェクトを参照して内部の値を照合して とか
ベストな方法とは思えないなと思いつつ仕方ないか でやってる方法です
別の方法は フォーム用に state を用意するというものです
編集中として 編集開始時に state をコピーします
そのときそのままのコピーではなく input 用の表現に変換しておきます
そうすれば編集中 state は input と同じ型なので 編集中に変換不要です
state の値を そのまま input にセットして 変更後の値をそのまま state にセットできます
保存ボタンなど編集を完了するイベントで 編集中 state を本来の state に変換して更新します
変換は編集開始と終了の最小限なので こっちのほうが良さそうに思ってます
ただ state が増えるのはデメリットもあります
編集中に元 state が変わったときの扱いが難しくなります
全部新しい state で置き換えるならいいですが state 内の変更があったプロパティだけとなると自分で差分を見つけてそこだけを編集中 state に反映しないといけないので面倒です
React だと state 全体を更新だとしても変更を検知するための useEffect が必要になるのでローカル state を増やすこと自体が気が進まないです
また 編集中 state は基本的にそのフォーム内だけのものです
入力途中の state を参照したいとしてもほとんどの場合はそのフォーム内部の話です
なので問題なさそうに見えるのですが 稀にフォーム外の場所にも反映したいことがあります
例えば画面テーマの色を選べるとします
選んだ色をフォーム内でプレビューとして表示することができますが 実際にヘッダーとかサイドバーとかをその色にして確認したいなんてこともあります
そういうフォーム外にも編集中 state を反映したい場合は面倒が増えます
常にどっちの方がいいとは言えないので 未だにどっちにするか迷うところです
一応今のところは React は 1 つめの方法で lit (WebComponents) は 2 つめの方法にしてることが多めです
他にはフォームの機能によって変えてたりです
保存ボタンが存在しないフォームだと state を分けても都度 編集中 state を本来の state に反映することになって分ける意味がないです
なので 1 つめの方法です
キャンセルやリセットボタンがあるフォームなら 初期 state と編集中 state の 2 つを保持する必要があります
なので 2 つめの方法です
リセット機能はなく 保存ボタンだけがあるフォームだとどっちでもいいのでやっぱり迷います
input は基本 値を文字列で扱います
type が number とか date でも value は文字列です
一応 valueAsNumber とか valueAsDate とかありはしますが 扱いづらいところがあるのでこれらは使いません
state 側では数値や Date 型など文字列以外で持っていることも少なくないです
そういうときにどうするかです
ひとつは input の入出力の際に毎回変換することです
value に渡すときに文字列に変換して 変更のイベント時に文字列から state の型に変換して state を更新します
それぞれの input の受け渡しのコードが長くなるのと 都度変換が必要なところが微妙です
日付型を文字列にフォーマットするくらいなら パフォーマンスに影響することはほとんどないと思いますが ユーザーの入力のたびにやるのってとてもムダに思えます
また 一度の再レンダリングで全部の input 分の変換を行うので 数が多い場合はパフォーマンスに影響無いとも言い切れないです
それに input は input そのものじゃなくてコンポーネントになってることもあります
その場合は変換がもっと重い処理のこともあります
別のオブジェクトを参照して内部の値を照合して とか
ベストな方法とは思えないなと思いつつ仕方ないか でやってる方法です
別の方法は フォーム用に state を用意するというものです
編集中として 編集開始時に state をコピーします
そのときそのままのコピーではなく input 用の表現に変換しておきます
そうすれば編集中 state は input と同じ型なので 編集中に変換不要です
state の値を そのまま input にセットして 変更後の値をそのまま state にセットできます
保存ボタンなど編集を完了するイベントで 編集中 state を本来の state に変換して更新します
変換は編集開始と終了の最小限なので こっちのほうが良さそうに思ってます
ただ state が増えるのはデメリットもあります
編集中に元 state が変わったときの扱いが難しくなります
全部新しい state で置き換えるならいいですが state 内の変更があったプロパティだけとなると自分で差分を見つけてそこだけを編集中 state に反映しないといけないので面倒です
React だと state 全体を更新だとしても変更を検知するための useEffect が必要になるのでローカル state を増やすこと自体が気が進まないです
また 編集中 state は基本的にそのフォーム内だけのものです
入力途中の state を参照したいとしてもほとんどの場合はそのフォーム内部の話です
なので問題なさそうに見えるのですが 稀にフォーム外の場所にも反映したいことがあります
例えば画面テーマの色を選べるとします
選んだ色をフォーム内でプレビューとして表示することができますが 実際にヘッダーとかサイドバーとかをその色にして確認したいなんてこともあります
そういうフォーム外にも編集中 state を反映したい場合は面倒が増えます
常にどっちの方がいいとは言えないので 未だにどっちにするか迷うところです
一応今のところは React は 1 つめの方法で lit (WebComponents) は 2 つめの方法にしてることが多めです
他にはフォームの機能によって変えてたりです
保存ボタンが存在しないフォームだと state を分けても都度 編集中 state を本来の state に反映することになって分ける意味がないです
なので 1 つめの方法です
キャンセルやリセットボタンがあるフォームなら 初期 state と編集中 state の 2 つを保持する必要があります
なので 2 つめの方法です
リセット機能はなく 保存ボタンだけがあるフォームだとどっちでもいいのでやっぱり迷います
コンポーネントの関数って props を受け取って JSX の要素を返します
入出力は普通の関数と同じで 違うのはコンポーネント関数は React が呼び出して中でフックが使えるということ
普通の関数でフックを使うこともできますが それはフック関数という扱いになってフックのルールに則って使う必要があります
フックを使わないならコンポーネントにする必要がないようにも思います
例えば
の代わりに
と書いても動きます
そう思ったきっかけは逆で コンポーネント内で処理をまとめるために関数化していた部分があって 引数は 1 つのオブジェクトで JSX の要素が返ってくるものでした
これならコンポーネント化しても良さそうと思ったのですが むしろコンポーネントにする必要があるのかと疑問に思いました
コンポーネントだと React.memo が使えて props が同じなら再レンダリングを避けれます
ですが React.memo ってそんなに使うことがなくて 明らかに遅いようなところだけです
フックも使わないような小さいコンポーネントなので基本は memo しないものです
扱いやすさ的には普通の関数のほうが好きです
ただ その利点は React に依存しないという点が大きいですが フックを中で使わないなら命名規則くらいの差しかありません
見やすさ的には 上の例みたいに {} の中で使うならどっちもどっちですが そうでないなら普通の関数は実行するために必ず {} が必要になり 読みづらくなります
コンポーネントのほうが HTML ライクな記法で書けて見やすいです
迷うところですが 将来的にフックを使いたくなるかもと考えればとりあえず最初からコンポーネントにするのが無難かもしれないです
開発時に限れば コンポーネントにすると strict モードで複数回レンダリングされます
マウントされるコンポーネントが増えるほど重たくなります
普通の関数だとこれがないので少しですが速いです
本番ビルドには影響しないものなので これを理由に選ぶのはどうかとは思いますけど
入出力は普通の関数と同じで 違うのはコンポーネント関数は React が呼び出して中でフックが使えるということ
普通の関数でフックを使うこともできますが それはフック関数という扱いになってフックのルールに則って使う必要があります
フックを使わないならコンポーネントにする必要がないようにも思います
例えば
{user && <UserInfo user={user} others={others}/>}
の代わりに
{user && UserInfo({ user, other }) }
と書いても動きます
そう思ったきっかけは逆で コンポーネント内で処理をまとめるために関数化していた部分があって 引数は 1 つのオブジェクトで JSX の要素が返ってくるものでした
これならコンポーネント化しても良さそうと思ったのですが むしろコンポーネントにする必要があるのかと疑問に思いました
コンポーネントだと React.memo が使えて props が同じなら再レンダリングを避けれます
ですが React.memo ってそんなに使うことがなくて 明らかに遅いようなところだけです
フックも使わないような小さいコンポーネントなので基本は memo しないものです
扱いやすさ的には普通の関数のほうが好きです
ただ その利点は React に依存しないという点が大きいですが フックを中で使わないなら命名規則くらいの差しかありません
見やすさ的には 上の例みたいに {} の中で使うならどっちもどっちですが そうでないなら普通の関数は実行するために必ず {} が必要になり 読みづらくなります
コンポーネントのほうが HTML ライクな記法で書けて見やすいです
迷うところですが 将来的にフックを使いたくなるかもと考えればとりあえず最初からコンポーネントにするのが無難かもしれないです
開発時に限れば コンポーネントにすると strict モードで複数回レンダリングされます
マウントされるコンポーネントが増えるほど重たくなります
普通の関数だとこれがないので少しですが速いです
本番ビルドには影響しないものなので これを理由に選ぶのはどうかとは思いますけど
display に contents を指定すると DOM のツリーとしては存在するのに存在しないように扱わせることができます
React の Fragment に近いイメージです
要素をまとめるための要素が存在するのに 無いものとして扱わせたいときに使います
例えば
div.row は display: flex なので横並びになりますが 2 つめの子要素は普通の div なのでここは縦並びになります
B と C を囲む div.wrap を無いものにしたいときは これに display: contents をつけます
これで
という並びにできます
flex だと div.wrap も flex にすればいいのであまり必要なかもですが grid だともっと役立ちます
そんな display: contents ですが 完全に無いものとしては扱えず不便なときもあります
例えば 子要素にスタイルを当てるとき
こういうケースでは div.contents にマージンは効かず この div を透過して B と C にマージンが設定されることもありません
A と D にだけマージンが設定されます
React などをつかっていると div.container の子要素はコンポーネントで生成していて イベントをまとめて受け取るためなどで display: contents の要素でトップレベルをラップしてるとかもありえます
そうなるとこういう子要素に共通でスタイルを当てたいというときにうまく動かないケースが出てくるのですよね
CSS セレクタの > や + などでも display: contents は透過してくれるといいのですけど
React の Fragment に近いイメージです
要素をまとめるための要素が存在するのに 無いものとして扱わせたいときに使います
例えば
<style>
.row {
display: flex;
}
</style>
<div class="row">
<div>A</div>
<div class="wrap">
<div>B</div>
<div>C</div>
</div>
<div>D</div>
</div>
div.row は display: flex なので横並びになりますが 2 つめの子要素は普通の div なのでここは縦並びになります
ABD
C
B と C を囲む div.wrap を無いものにしたいときは これに display: contents をつけます
これで
ABCD
という並びにできます
flex だと div.wrap も flex にすればいいのであまり必要なかもですが grid だともっと役立ちます
そんな display: contents ですが 完全に無いものとしては扱えず不便なときもあります
例えば 子要素にスタイルを当てるとき
<style>
.container > * {
margin-top: 10px;
}
.contents {
display: contents;
}
</style>
<div class="container">
<div>A</div>
<div class="contents">
<div>B</div>
<div>C</div>
</div>
<div>D</div>
</div>
こういうケースでは div.contents にマージンは効かず この div を透過して B と C にマージンが設定されることもありません
A と D にだけマージンが設定されます
React などをつかっていると div.container の子要素はコンポーネントで生成していて イベントをまとめて受け取るためなどで display: contents の要素でトップレベルをラップしてるとかもありえます
そうなるとこういう子要素に共通でスタイルを当てたいというときにうまく動かないケースが出てくるのですよね
CSS セレクタの > や + などでも display: contents は透過してくれるといいのですけど
今でもスッキリしない React での初期化処理の扱い方
Edit コンポーネントがあって何かを編集するものです
React なので value を渡して編集があれば onChange で受け取って親側で更新するという 親側で state を管理する作りです
これだと value の値を初期化やチェックして修正するときに親側でやらないといけなくなります
ですが そのロジックは Edit のものなので Edit でやってもらいたいです
そうなると ユーザーの操作なしで Edit のマウント時に onChange を呼び出して更新することになります
仕方ないのですがそれはなんか気持ち悪さがあります
親で state 管理しないものなら 初期値を渡すだけで修正した値は Edit の中で持っておいて 親が値を使いたいときに Edit から取り出します
これだと自然なのですよね
Edit コンポーネントのモジュールから関数もエクスポートすれば ロジックは Edit モジュールの中で持てます
しかし Edit を使うのに 初期値を作る部分とコンポーネント部分で 2 つの処理が必要になるのが面倒です
例えば API で選択肢を取得して初期値が null なら最初の選択肢を初期値にする場合 初期値のために関数で API を呼び出して コンポーネント内でも選択肢を作るために API を呼び出す必要があるので 関数とコンポーネントの両方で API を呼び出すことになります
それを避けるなら関数は初期値の他に選択肢も返して それもコンポーネントに渡すという作りになりますが やることが増えます
やりたいのは単純に Edit コンポーネントを使って props を渡すだけにしたいのですけど
onChange の場合でも困るところはあります
変更イベントを beforeunload などと関連付けていれば ユーザーは何も編集してないのに変更ありとして扱われます
初期化が null の場合に値を入れたり 不正文字を除去など繰り返し行っても問題ないものならいいですが そうでない場合は判断するのが難しいです
入力値を制御するライブラリでは onChange を即呼び出すものを見かけますが 組み合わせるライブラリの相性とかもあるのか ときどきうまく動いてなかったりするのですよね
レンダリング中に同期的に呼び出すと問題ありそうですし useEffect 通しても その他の useEffect との順序が影響しそうです
const Component = (props) => {
// ...
return (
<div>
<Edit
value={value}
onChange={onChange}
/>
</div>
)
}
Edit コンポーネントがあって何かを編集するものです
React なので value を渡して編集があれば onChange で受け取って親側で更新するという 親側で state を管理する作りです
これだと value の値を初期化やチェックして修正するときに親側でやらないといけなくなります
ですが そのロジックは Edit のものなので Edit でやってもらいたいです
そうなると ユーザーの操作なしで Edit のマウント時に onChange を呼び出して更新することになります
仕方ないのですがそれはなんか気持ち悪さがあります
親で state 管理しないものなら 初期値を渡すだけで修正した値は Edit の中で持っておいて 親が値を使いたいときに Edit から取り出します
これだと自然なのですよね
Edit コンポーネントのモジュールから関数もエクスポートすれば ロジックは Edit モジュールの中で持てます
しかし Edit を使うのに 初期値を作る部分とコンポーネント部分で 2 つの処理が必要になるのが面倒です
例えば API で選択肢を取得して初期値が null なら最初の選択肢を初期値にする場合 初期値のために関数で API を呼び出して コンポーネント内でも選択肢を作るために API を呼び出す必要があるので 関数とコンポーネントの両方で API を呼び出すことになります
それを避けるなら関数は初期値の他に選択肢も返して それもコンポーネントに渡すという作りになりますが やることが増えます
やりたいのは単純に Edit コンポーネントを使って props を渡すだけにしたいのですけど
onChange の場合でも困るところはあります
変更イベントを beforeunload などと関連付けていれば ユーザーは何も編集してないのに変更ありとして扱われます
初期化が null の場合に値を入れたり 不正文字を除去など繰り返し行っても問題ないものならいいですが そうでない場合は判断するのが難しいです
入力値を制御するライブラリでは onChange を即呼び出すものを見かけますが 組み合わせるライブラリの相性とかもあるのか ときどきうまく動いてなかったりするのですよね
レンダリング中に同期的に呼び出すと問題ありそうですし useEffect 通しても その他の useEffect との順序が影響しそうです
Chrome と同じ感じで接続
リモートデバッギングを有効にして起動
接続とページ遷移は Chrome と同じ感じでできる
ページ内でスクリプトの実行は Chrome だと↓で動く
でも Firefox だと context が null と言われてエラー
Chrome のドキュメントだとオプショナルだけど evaluate のパラメーターに contextId を渡せる
ExecutionContextId という型で実体は int
試しに 0 や 1 など渡してみても指定の id の context がみつからないというエラー
context を作る方法を探すと Page.createIsolatedWorld で isolateworld を作ると返り値として ExecutionContextId が受け取れるみたい
Page.createIsolatedWorld には frameId が必要になるけどこれは Page.navigate で受け取れる
試してみたけどフレームが見つからないと言われてエラー
やっぱり Firefox を直接 CDP で動かすのはやめたほうが良さそう
調べてると Selenium は Firefox の CDP サポートをやめる予定があるみたい
https://github.com/SeleniumHQ/selenium/issues/11736
理由として Firefox の CDP 対応が不完全で新しい BiDi の対応を重視してるみたい
https://w3c.github.io/webdriver-bidi/
BiDi が標準化されて普及するなら Playwright もこっちに切り替えていく方針なのかなと思って調べるとこんな QA が出てきた
https://github.com/microsoft/playwright/discussions/14014
動画内での発言で 全メジャーブラウザがサポートされない限り BiDi の実装予定はないみたい
サポートされれば検討するとか
現状では積極的に変えていくつもりはなく様子見みたい
リモートデバッギングを有効にして起動
firefox --remote-debugging-port 9222
接続とページ遷移は Chrome と同じ感じでできる
import CDP from "chrome-remote-interface"
const cdp = await CDP()
await cdp.Page.navigate({ url: "https://github.com/" })
ページ内でスクリプトの実行は Chrome だと↓で動く
const expression = `document.title`
await cdp.Runtime.evaluate({ expression, returnByValue: true })
でも Firefox だと context が null と言われてエラー
Chrome のドキュメントだとオプショナルだけど evaluate のパラメーターに contextId を渡せる
ExecutionContextId という型で実体は int
試しに 0 や 1 など渡してみても指定の id の context がみつからないというエラー
context を作る方法を探すと Page.createIsolatedWorld で isolateworld を作ると返り値として ExecutionContextId が受け取れるみたい
Page.createIsolatedWorld には frameId が必要になるけどこれは Page.navigate で受け取れる
試してみたけどフレームが見つからないと言われてエラー
やっぱり Firefox を直接 CDP で動かすのはやめたほうが良さそう
調べてると Selenium は Firefox の CDP サポートをやめる予定があるみたい
https://github.com/SeleniumHQ/selenium/issues/11736
理由として Firefox の CDP 対応が不完全で新しい BiDi の対応を重視してるみたい
https://w3c.github.io/webdriver-bidi/
BiDi が標準化されて普及するなら Playwright もこっちに切り替えていく方針なのかなと思って調べるとこんな QA が出てきた
https://github.com/microsoft/playwright/discussions/14014
動画内での発言で 全メジャーブラウザがサポートされない限り BiDi の実装予定はないみたい
サポートされれば検討するとか
現状では積極的に変えていくつもりはなく様子見みたい
localhost へのアクセスで単純な fetch なのに preflight リクエストが発生しました
追加ヘッダーもなく GET リクエストで
だけなのですけど
各ページで devtools を開いて コンソールから fetch を実行し ネットワークパネルで preflight リクエストの有無を確認します
リクエスト先は 「http://localhost:8000」 です
「https://let.blog.jp/」 → あり
「https://www.yahoo.co.jp/」 → あり
「https://www.google.com/」 → あり
「http://var.blog.jp/」 → なし
「http://localhost:7000」 → なし
外部からだからというわけではなく https のページから localhost へのアクセスだと発生するようです
localhost 以外からでも http のページからだと発生しません
localhost 関係なく https → http なら発生するのかと思って 試してみましたが発生しませんでした
https から localhost だけのようです
localhost の代わりに 127.0.0.1 としても同様ですが 192.168.1.11 など自身の IP アドレス指定にした場合は発生しません
関係ないですが Github や MDN のページから fetch を実行すると CSP でブロックされました
これがあるから CSP は嫌なんですよね
devtools や拡張機能からなら無視してほしいものです
追加ヘッダーもなく GET リクエストで
fetch(url)
だけなのですけど
各ページで devtools を開いて コンソールから fetch を実行し ネットワークパネルで preflight リクエストの有無を確認します
リクエスト先は 「http://localhost:8000」 です
「https://let.blog.jp/」 → あり
「https://www.yahoo.co.jp/」 → あり
「https://www.google.com/」 → あり
「http://var.blog.jp/」 → なし
「http://localhost:7000」 → なし
外部からだからというわけではなく https のページから localhost へのアクセスだと発生するようです
localhost 以外からでも http のページからだと発生しません
localhost 関係なく https → http なら発生するのかと思って 試してみましたが発生しませんでした
https から localhost だけのようです
localhost の代わりに 127.0.0.1 としても同様ですが 192.168.1.11 など自身の IP アドレス指定にした場合は発生しません
関係ないですが Github や MDN のページから fetch を実行すると CSP でブロックされました
これがあるから CSP は嫌なんですよね
devtools や拡張機能からなら無視してほしいものです
なにかを作ったときってだいたい画面で動作確認したりしてるので ちゃんと動いてるかの確認という意味ではあまりテストが必要と思ったことはないです
ときどきかなり複雑なロジックなものがあって 色々なケースを試すときに手作業よりはテストツールでやったほうが楽かなと思うくらいです
なので テストの目的は仕様を記載するか あれこれ変更したときに元の動作が維持できているかの確認という意図が強めです
一度作ったらその部分のコードにはできるだけ触れないという人もいますが 個人的には気になるところがあれば頻繁に修正加えていくタイプなのでリファクタリング時が一番の目的かもしれません
なのですが 大きく書き換えるとリファクタリング時に動作が変わっていないかのテストに使えないことが多いのですよね
小さい規模だと使えることが多いですが 期待するのは大きい規模での変更です
なのにそういうときにあまり使えないとなると 丁寧にテストしてもあまり意味ないかなぁという気持ちになりました
例えばモジュールまるごと置き換えるような場合
A モジュールがあってそれを使う B モジュールがあります
A はこういう感じです
B では基本 default export されたものを使います
場合によっては直接 logic1 などを使うかもしれませんが基本は default export のものです
複雑なロジックをテストするときって基本まとまった部分じゃなくて小さい単位で詳細にテストします
上の例だと logic1 ~ logic3 などです
default export する関数だと中でこれらを複数使ってるので この関数を使って logic1 の動きや logic2 の動きまで詳細にテストするのは大変です
例えば logic3 が重たい処理だった場合に logic1 部分のテストだけでも時間がかかってしまいますし
なので default export の関数自体のテストはシンプルで 中で呼び出す logic1 などに渡す引数が間違ってないかを確認する程度の目的です
この関数自体が十分にシンプルなら省略することもあります
リファクタリングで logic1 の中身だけ修正しようとなった場合は logic1 はちゃんとテストされているので 効果が高いです
しかし 大きな規模だと A モジュールをまるごと作り直すということもありえます
そうなると logic1 や logic2 という関数の分け方もなくなります
過去のテストは使い回せないです
B に影響させない範囲なら default export の関数のインターフェースはそのままですが この関数のテストは簡単なものしかありません
このテストが通っても最低限のパターンでしかなくて 複雑なケースが動くことは保証できません
これに対応しようとすると logic1 ~ logic3 の分のテストを default export の関数でもテストしておく必要が出てきます
それはテストケースがかなり増えて大変ですし 関数が何重にもなってくると現実的ではないです
B モジュールを使う C モジュールがあれば C モジュールでも A モジュールの中身までテストするということになります
C モジュールを使う D モジュール……と続いて Z モジュールまで行ったとき Z モジュールのテストで A モジュール内の logic1 の分岐全部をテストなんてやってられないですよね
そう考えるとリファクタリング時の安全を求めるという目的でのテストはあまり効率的ではなさそうです
ときどきかなり複雑なロジックなものがあって 色々なケースを試すときに手作業よりはテストツールでやったほうが楽かなと思うくらいです
なので テストの目的は仕様を記載するか あれこれ変更したときに元の動作が維持できているかの確認という意図が強めです
一度作ったらその部分のコードにはできるだけ触れないという人もいますが 個人的には気になるところがあれば頻繁に修正加えていくタイプなのでリファクタリング時が一番の目的かもしれません
なのですが 大きく書き換えるとリファクタリング時に動作が変わっていないかのテストに使えないことが多いのですよね
小さい規模だと使えることが多いですが 期待するのは大きい規模での変更です
なのにそういうときにあまり使えないとなると 丁寧にテストしてもあまり意味ないかなぁという気持ちになりました
例えばモジュールまるごと置き換えるような場合
A モジュールがあってそれを使う B モジュールがあります
A はこういう感じです
export const logic1 = () => {}
export const logic2 = () => {}
export const logic3 = () => {}
// ...
export default () => {
// ここで logic1 や logic2 などを使う
}
B では基本 default export されたものを使います
場合によっては直接 logic1 などを使うかもしれませんが基本は default export のものです
複雑なロジックをテストするときって基本まとまった部分じゃなくて小さい単位で詳細にテストします
上の例だと logic1 ~ logic3 などです
default export する関数だと中でこれらを複数使ってるので この関数を使って logic1 の動きや logic2 の動きまで詳細にテストするのは大変です
例えば logic3 が重たい処理だった場合に logic1 部分のテストだけでも時間がかかってしまいますし
なので default export の関数自体のテストはシンプルで 中で呼び出す logic1 などに渡す引数が間違ってないかを確認する程度の目的です
この関数自体が十分にシンプルなら省略することもあります
リファクタリングで logic1 の中身だけ修正しようとなった場合は logic1 はちゃんとテストされているので 効果が高いです
しかし 大きな規模だと A モジュールをまるごと作り直すということもありえます
そうなると logic1 や logic2 という関数の分け方もなくなります
過去のテストは使い回せないです
B に影響させない範囲なら default export の関数のインターフェースはそのままですが この関数のテストは簡単なものしかありません
このテストが通っても最低限のパターンでしかなくて 複雑なケースが動くことは保証できません
これに対応しようとすると logic1 ~ logic3 の分のテストを default export の関数でもテストしておく必要が出てきます
それはテストケースがかなり増えて大変ですし 関数が何重にもなってくると現実的ではないです
B モジュールを使う C モジュールがあれば C モジュールでも A モジュールの中身までテストするということになります
C モジュールを使う D モジュール……と続いて Z モジュールまで行ったとき Z モジュールのテストで A モジュール内の logic1 の分岐全部をテストなんてやってられないですよね
そう考えるとリファクタリング時の安全を求めるという目的でのテストはあまり効率的ではなさそうです
こういう JavaScript ファイルと ShellScript ファイルがあります
[job.sh]
[job.js]
job.js は常駐するプロセスです
ここでは setInterval でプロセスが終了しないようにしています
これを job.sh から起動します
job.sh は別の Node.js プロセスから起動します
job.sh を起動する側はこんな感じです
job.sh や job.js の出力を受け取りたいので stdout の標準出力にリスナをつけています
job.js は常駐しますがすべてを受け取る必要はなくて 起動直後の内容だけでいいです
つまりは job.sh が終われば close イベントが起きてほしいです
しかし close イベントは起きません
子プロセスである job.sh は終了済みなのですが孫プロセスの job.js が生きていることで stdout や stderr が生きてるのでこれらが閉じられるまでは Node.js は子プロセスが終了したとみなしてくれないようです
放置でもいい気はしますがずっと残るわけなのでメモリリーク状態です
stdio が問題なので ignore にしてしまえば標準入出力を使わないようにできて これなら即終了とみなされ CLOSED が表示されます
ただし問題があって標準出力を受け取れないです
ファイルに出力するようにして ファイルを読み取るということはできますが 回りくどくてイマイチです
起動する側もサーバーみたいな常駐するものだとメモリリークになるのを気にしないとですけど 一回限りのスクリプトでスクリプトが終了しないのが嫌という場合は unref で解決できます
必要な出力が出る程度に 1 秒待ってから unref しています
ちゃんとやるなら stdout の出力から必要なものを受け取れたら実行みたいにしたほうがいいです
まぁ終了させるなら unref していかなくても process.exit でもいいのですけどね
考えてみると常駐してるものから出力を受け取り続ける以上終了しないのが正常ですよね
job.sh で job.js を起動する部分で
みたいにしてしまったほうがいいかもしれませんね
[job.sh]
#!/bin/bash
echo pre something something
./job.js &
echo post something something
[job.js]
#!/usr/bin/env node
console.log("Start job.js")
setTimeout(() => {
console.log("initialized")
}, 500)
// keep process
setInterval(() => {}, 100000)
job.js は常駐するプロセスです
ここでは setInterval でプロセスが終了しないようにしています
これを job.sh から起動します
job.sh は別の Node.js プロセスから起動します
job.sh を起動する側はこんな感じです
const cp = require("child_process")
const sub = cp.spawn("./job.sh")
sub.stdout.setEncoding("utf8")
sub.stdout.on("data", console.log)
sub.on("close", () => {
console.log("CLOSED")
})
job.sh や job.js の出力を受け取りたいので stdout の標準出力にリスナをつけています
job.js は常駐しますがすべてを受け取る必要はなくて 起動直後の内容だけでいいです
つまりは job.sh が終われば close イベントが起きてほしいです
しかし close イベントは起きません
子プロセスである job.sh は終了済みなのですが孫プロセスの job.js が生きていることで stdout や stderr が生きてるのでこれらが閉じられるまでは Node.js は子プロセスが終了したとみなしてくれないようです
放置でもいい気はしますがずっと残るわけなのでメモリリーク状態です
stdio が問題なので ignore にしてしまえば標準入出力を使わないようにできて これなら即終了とみなされ CLOSED が表示されます
const sub = cp.spawn("./job.sh", { stdio: "ignore" })
ただし問題があって標準出力を受け取れないです
ファイルに出力するようにして ファイルを読み取るということはできますが 回りくどくてイマイチです
起動する側もサーバーみたいな常駐するものだとメモリリークになるのを気にしないとですけど 一回限りのスクリプトでスクリプトが終了しないのが嫌という場合は unref で解決できます
const cp = require("child_process")
sub = cp.spawn("./job.sh")
sub.stdout.setEncoding("utf8")
sub.stdout.on("data", console.log)
sub.on("close", () => {
console.log("CLOSED")
})
setTimeout(() => {
sub.unref()
sub.stdout.unref()
sub.stderr.unref()
}, 1000)
必要な出力が出る程度に 1 秒待ってから unref しています
ちゃんとやるなら stdout の出力から必要なものを受け取れたら実行みたいにしたほうがいいです
まぁ終了させるなら unref していかなくても process.exit でもいいのですけどね
考えてみると常駐してるものから出力を受け取り続ける以上終了しないのが正常ですよね
job.sh で job.js を起動する部分で
./job.js > file 2>&1 &
みたいにしてしまったほうがいいかもしれませんね
内部で pino が使われているものを使っていたとき ログが表示されるはずなのに出ていません
console.log を使っている出力は表示されています
VSCode でデバッグ実行していたので 一応ターミナルから直接実行もしてみると ちゃんと表示できています
なぜ?
pino 側の問題かなと思って直接 pino を使って簡単なコードを用意しました
これをデバッグ実行すると これでも CONSOLE LOG だけしか表示されないです
中でどういう出力をしてるのだろうと pino の中を見ると stream への書き込みを行っていました
stream を作成してるのはここです
https://github.com/pinojs/pino/blob/v8.15.1/lib/tools.js#L329
SonicBoom に { fd: 1 } を渡しています
ターミナルからでも VSCode のデバッグ実行からでも process.stdout.fd は 1 でした
ということは 普通に process.stdout.write しても出なさそうですね
を試してみましたが やはり CONSOLE LOG だけでした
VSCode のデバッグ実行だと console.log が置き換えられてそうです
となると process.stdout.write を置き換えて 中で console.log するしかない?
でもデバッグ実行のためだけにやりたくない変更です
それに stdout への直接書き込みは末尾に改行が自動追加されないですが console.log だと自動追加されて消せなかったはずですし
デバッグ実行してる場合の仕方ないものとして諦めつつも一応ググってみると解決策がありました
launch.json を作って設定に 「"outputCapture": "std"」 を追加すれば標準出力もキャプチャしてくれるようです
これで pino のログも表示されました
console.log を使っている出力は表示されています
VSCode でデバッグ実行していたので 一応ターミナルから直接実行もしてみると ちゃんと表示できています
なぜ?
pino 側の問題かなと思って直接 pino を使って簡単なコードを用意しました
const pino = require("pino")
const logger = pino({ level: "info" })
logger.info("PINO LOG")
console.log("CONSOLE LOG")
これをデバッグ実行すると これでも CONSOLE LOG だけしか表示されないです
中でどういう出力をしてるのだろうと pino の中を見ると stream への書き込みを行っていました
stream を作成してるのはここです
https://github.com/pinojs/pino/blob/v8.15.1/lib/tools.js#L329
stream = buildSafeSonicBoom({ fd: process.stdout.fd || 1 })
SonicBoom に { fd: 1 } を渡しています
ターミナルからでも VSCode のデバッグ実行からでも process.stdout.fd は 1 でした
ということは 普通に process.stdout.write しても出なさそうですね
process.stdout.write("STDOUT LOG\n")
console.log("CONSOLE LOG")
を試してみましたが やはり CONSOLE LOG だけでした
VSCode のデバッグ実行だと console.log が置き換えられてそうです
となると process.stdout.write を置き換えて 中で console.log するしかない?
でもデバッグ実行のためだけにやりたくない変更です
それに stdout への直接書き込みは末尾に改行が自動追加されないですが console.log だと自動追加されて消せなかったはずですし
デバッグ実行してる場合の仕方ないものとして諦めつつも一応ググってみると解決策がありました
launch.json を作って設定に 「"outputCapture": "std"」 を追加すれば標準出力もキャプチャしてくれるようです
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
// 略
"outputCapture": "std"
}
]
}
これで pino のログも表示されました
キャッシュ設定を変更して動作確認していたら max-age を設定しているのに毎回サーバーに問い合わせが発生して困っていました
devtools の Network タブを見たときに (disk cache) や (memory cache) となっていてほしいのですがなってくれません
リクエストが発生して 304 が返ってきています
そのレスポンスのヘッダーを見るとちゃんと max-age が付いているのですけどリロードするとまたリクエストが発生します
色々試したところ max-age でキャッシュからロードしてくれるのはリソース扱いのときだけのようです
ブラウザで直接 HTML ファイルや JavaScript ファイルを開いた場合 これらのファイルはキャッシュを見ずにリクエストが発生するようです
HTML から script タグで JavaScript をロードしたり fetch で HTML ファイルを取得するようなケースではキャッシュが使われています
これのせいで無駄に時間を使うことになりました
前からこんな動きでしたっけ
というかこれは共通の仕様? Chrome の独自仕様?
MDN を軽く見た感じではアクセスするコンテキストで動作の違いとかはなさそうでした
◯ 確認用
「http://localhost:8000」 にアクセスして devtools を開いてリロードします
a.js のほうだけキャッシュが使われてるはずです
「fetch("/")」で / にアクセスすると HTML ファイルもキャッシュが使われます
こういう動きだと HTML もキャッシュさせたい場合は E-Tag や LastModified を使って 304 を返せるようにしておいたほうが良さそうですね
SPA を作るときは HTML ページだけはエントリポイントとして更新するために max-age 付けたらダメなので例外にする工夫が必要だったのですが それがいらないという点では便利ですね
リロードで確認すると max-age が無視されるみたいです
https://blog.jxck.io/entries/2023-11-05/reload-and-cache.html
リンクを用意して移動してきた場合はページを開くリクエストでもキャッシュが使われてました
ページを開くリクエストでキャッシュが無効化されるなら SPA のエントリポイントなどの HTML もまとめて max-age をつけておいても大丈夫かもと思ったのに そうはいかなそうです
HTML にも max-age をつけると外部から移動してきたときだけキャッシュが使われて最新版にならない問題が起きてしまいます
devtools の Network タブを見たときに (disk cache) や (memory cache) となっていてほしいのですがなってくれません
リクエストが発生して 304 が返ってきています
そのレスポンスのヘッダーを見るとちゃんと max-age が付いているのですけどリロードするとまたリクエストが発生します
色々試したところ max-age でキャッシュからロードしてくれるのはリソース扱いのときだけのようです
ブラウザで直接 HTML ファイルや JavaScript ファイルを開いた場合 これらのファイルはキャッシュを見ずにリクエストが発生するようです
HTML から script タグで JavaScript をロードしたり fetch で HTML ファイルを取得するようなケースではキャッシュが使われています
これのせいで無駄に時間を使うことになりました
前からこんな動きでしたっけ
というかこれは共通の仕様? Chrome の独自仕様?
MDN を軽く見た感じではアクセスするコンテキストで動作の違いとかはなさそうでした
◯ 確認用
require("http").createServer((req, res) => {
if (req.url === "/") {
res.writeHead(200, {
"Content-type": "text/html",
"Cache-Control": "max-age=3600",
})
res.end(`<script src="/a.js"></script><h1>HTML</h1>`)
return
}
if (req.url === "/a.js") {
res.writeHead(200, {
"Content-type": "text/javascript",
"Cache-Control": "max-age=3600",
})
res.end(`console.log(1)`)
return
}
res.writeHead(404)
res.end(`NOT FOUND`)
}).listen(8000)
「http://localhost:8000」 にアクセスして devtools を開いてリロードします
a.js のほうだけキャッシュが使われてるはずです
「fetch("/")」で / にアクセスすると HTML ファイルもキャッシュが使われます
こういう動きだと HTML もキャッシュさせたい場合は E-Tag や LastModified を使って 304 を返せるようにしておいたほうが良さそうですね
SPA を作るときは HTML ページだけはエントリポイントとして更新するために max-age 付けたらダメなので例外にする工夫が必要だったのですが それがいらないという点では便利ですね
リロードで確認すると max-age が無視されるみたいです
https://blog.jxck.io/entries/2023-11-05/reload-and-cache.html
リンクを用意して移動してきた場合はページを開くリクエストでもキャッシュが使われてました
ページを開くリクエストでキャッシュが無効化されるなら SPA のエントリポイントなどの HTML もまとめて max-age をつけておいても大丈夫かもと思ったのに そうはいかなそうです
HTML にも max-age をつけると外部から移動してきたときだけキャッシュが使われて最新版にならない問題が起きてしまいます
hr タグを書いているのに表示されていないところがありました
他のところは表示されています
devtools の Elements タブで見ても hr タグには特別なスタイルは当たっていないようです
原因は親要素が display: flex かつ縦並びになっていたことでした
こういう HTML で再現できます
hr に width: 100% を指定することで hr を表示できます
ですが align-items のデフォルトは stretch なので width: 100% を指定しなくても横幅いっぱいになるはずです
実際の値を見てみても stretch になっていました
なぜ width 指定なしでうまく表示されないのでしょうか
もう少しあれこれ見てみると原因はユーザーエージェントのデフォルトスタイルシートでした
Chrome 系ブラウザでは hr のマージン関係のデフォルトがこうなっています
margin-inline が左右なのでこれが auto ということは中身のサイズに合わせて左右に自動でマージンが付きます
hr は中身が無いので幅が 0 になって表示されてないということでした
デフォルトは auto じゃなくて 0 でいいと思うのですけど
他のところは表示されています
devtools の Elements タブで見ても hr タグには特別なスタイルは当たっていないようです
原因は親要素が display: flex かつ縦並びになっていたことでした
<div style="
display: flex;
flex-direction: column;
">
<p>a</p>
<hr>
<p>b</p>
</div>
こういう HTML で再現できます
hr に width: 100% を指定することで hr を表示できます
ですが align-items のデフォルトは stretch なので width: 100% を指定しなくても横幅いっぱいになるはずです
実際の値を見てみても stretch になっていました
なぜ width 指定なしでうまく表示されないのでしょうか
もう少しあれこれ見てみると原因はユーザーエージェントのデフォルトスタイルシートでした
Chrome 系ブラウザでは hr のマージン関係のデフォルトがこうなっています
margin-block-start: 0.5em;
margin-block-end: 0.5em;
margin-inline-start: auto;
margin-inline-end: auto;
margin-inline が左右なのでこれが auto ということは中身のサイズに合わせて左右に自動でマージンが付きます
hr は中身が無いので幅が 0 になって表示されてないということでした
デフォルトは auto じゃなくて 0 でいいと思うのですけど
VSCode の全ウィンドウを閉じた状態で VSCode を起動すると最後に開いていた全部のウィンドウが起動します
「File > Exit」でまとめて終了すると全部が起動します
それぞれのウィンドウが結構な量のメモリを消費するので それらのワークスペースを開かない限りは復元せずに何もない空の状態のウィンドウが起動してほしいです
設定を探すと window.restoreWindows というのがありました
none にすると良いようです
これで解決かと思ったのですが 複数のウィンドウが開くときがあります
フォルダを開いておらず未保存のファイルが残っているとそのウィンドウは必ず自動で復元されるようです
フォルダを開いていない場合は ウィンドウを復元しなければ未保存のファイルにアクセスする方法が残ってないからだと思います
フォルダを開いていないウィンドウはワークスペースとして管理して ワークスペースを開くから一覧出してくれるとかして くれるといいのですけどね
そうすれば一覧からいつでも開けるので 未保存があっても復元せずに済みますし
「File > Exit」でまとめて終了すると全部が起動します
それぞれのウィンドウが結構な量のメモリを消費するので それらのワークスペースを開かない限りは復元せずに何もない空の状態のウィンドウが起動してほしいです
設定を探すと window.restoreWindows というのがありました
none にすると良いようです
"window.restoreWindows": "none"
これで解決かと思ったのですが 複数のウィンドウが開くときがあります
フォルダを開いておらず未保存のファイルが残っているとそのウィンドウは必ず自動で復元されるようです
フォルダを開いていない場合は ウィンドウを復元しなければ未保存のファイルにアクセスする方法が残ってないからだと思います
フォルダを開いていないウィンドウはワークスペースとして管理して ワークスペースを開くから一覧出してくれるとかして くれるといいのですけどね
そうすれば一覧からいつでも開けるので 未保存があっても復元せずに済みますし
package.json に
と書けば 「npm run c1」 で command1 というコマンドを実行できます
この command1 に当たるコマンドは通常 npm などでインストールしたパッケージに含まれるコマンドです
推奨される方法ではないですが 直接 node_modules フォルダ内に .bin/command1 というファイルを配置すれば実行できます
という中身にします
Windows の場合は command1.cmd のように cmd 拡張子をつけて 中身は cmd ファイル (bat ファイル) にします
.js ファイルを実行するなら node コマンドを呼び出します
Linux の場合はファイルの実行権限が必要なので chmod で追加します
これで 「npm run c1」 を実行すると
のように実行されます
ここで command1 は存在してパスの通っているコマンドやシェルのビルトインと同名でも大丈夫です
例えば
として node_modules/.bin/ls にファイルをおけば この ls が実行されます
ファイルの一覧が表示されたりはしません
しかし command というコマンドにした場合 なぜかシェルビルトインが優先されるような動きで node_modules/.bin/command が使用されないです
123 が表示されていません
npm でも yarn でも同じ結果です
Windows では command.cmd を実行するからか問題ないです
たぶん Linux 側の仕様によるものな気がしますが これにハマって時間をとられました
{
....
"scripts": {
"c1": "command1"
},
....
}
と書けば 「npm run c1」 で command1 というコマンドを実行できます
この command1 に当たるコマンドは通常 npm などでインストールしたパッケージに含まれるコマンドです
推奨される方法ではないですが 直接 node_modules フォルダ内に .bin/command1 というファイルを配置すれば実行できます
#!/usr/bin/env node
console.log(123)
という中身にします
Windows の場合は command1.cmd のように cmd 拡張子をつけて 中身は cmd ファイル (bat ファイル) にします
.js ファイルを実行するなら node コマンドを呼び出します
Linux の場合はファイルの実行権限が必要なので chmod で追加します
これで 「npm run c1」 を実行すると
root@abc0c5fc3c92:/mnt/88# npm run c1
> 88@1.0.0 c1
> command1
123
のように実行されます
ここで command1 は存在してパスの通っているコマンドやシェルのビルトインと同名でも大丈夫です
例えば
{
....
"scripts": {
"c1": "ls"
},
....
}
として node_modules/.bin/ls にファイルをおけば この ls が実行されます
ファイルの一覧が表示されたりはしません
しかし command というコマンドにした場合 なぜかシェルビルトインが優先されるような動きで node_modules/.bin/command が使用されないです
root@abc0c5fc3c92:/mnt/88# npm run c1
> 88@1.0.0 c1
> ls
123
root@abc0c5fc3c92:/mnt/88# npm run c1
> 88@1.0.0 c1
> command
123 が表示されていません
npm でも yarn でも同じ結果です
Windows では command.cmd を実行するからか問題ないです
たぶん Linux 側の仕様によるものな気がしますが これにハマって時間をとられました
Parcel2 にしてからずっと起きていて困っていた問題
Parcel ではビルドで出力されるリソースファイルのファイル名にハッシュ値がつけられます
同じソースに対して複数回ビルドしても同じ出力になることを期待しますが ハッシュ値が時々かわります
対象は PNG ファイルです
ハッシュ値だけの問題だと思っていましたが ファイルを保存しておき比べてみるとファイルそのものも異なっていました
ファイルサイズも違います
Parcel では最適化による圧縮が行われるので 出力されるファイルは元ファイルとは異なるものです
同じファイルを入力すると圧縮されたファイルは毎回同じになってほしいのですが そうならないようです
ただし 確認している限りでは無数のパターンがあるわけではなく 2 パターンでそのどちらかになるような挙動です
バイナリデータ的に違いが出ているのは PNG ファイルの画像本体部分です
圧縮時に使用するアルゴリズムを選ぶとき どっちでもいい場合は再現性のない選び方をしているとかでしょうか?
PNG の最適化は中で oxipng が使われているので Parcel というよりはこっちの問題の可能性が高いです
もう少し調べてみようかなと思い 再現する環境を準備でしていました
しかし なぜか再現しません
バージョンは変わっていません
ですが PC を変えてみると再現します
発生しない PC だと WSL の Linux 環境でも Windows 環境でも発生していません
最適化の処理は Rust で書かれたネイティブな部分ですし CPU の種類が影響したりするのでしょうか?
たしかに発生している PC はいずれも Intel 製で Windows11 にできないくらいは古いもので 発生していないのはそれよりは新しい AMD 製です
ただ ARM ならともかく Intel と AMD ならこんな問題起きないように思いますけど
中で並列処理をしていて 実行速度によってタイミングの問題で発生する場合があったりでしょうか
どうにかできそうなものじゃなさそうですし 修正される期待もあまりできないので Parcel を使うのをやめたほうが早そうです
最近は Vite 一択になりつつありますし Parcel 使ってるのもできるだけ Vite に移していきたいですね
ただ Vite は最近の更新で Parcel で使ってる lightningcss をサポートしたようです
その流れで画像の最適化も始めて Parcel と同じ問題を持ってくるというのが心配です
Parcel ではビルドで出力されるリソースファイルのファイル名にハッシュ値がつけられます
同じソースに対して複数回ビルドしても同じ出力になることを期待しますが ハッシュ値が時々かわります
対象は PNG ファイルです
ハッシュ値だけの問題だと思っていましたが ファイルを保存しておき比べてみるとファイルそのものも異なっていました
ファイルサイズも違います
Parcel では最適化による圧縮が行われるので 出力されるファイルは元ファイルとは異なるものです
同じファイルを入力すると圧縮されたファイルは毎回同じになってほしいのですが そうならないようです
ただし 確認している限りでは無数のパターンがあるわけではなく 2 パターンでそのどちらかになるような挙動です
バイナリデータ的に違いが出ているのは PNG ファイルの画像本体部分です
圧縮時に使用するアルゴリズムを選ぶとき どっちでもいい場合は再現性のない選び方をしているとかでしょうか?
PNG の最適化は中で oxipng が使われているので Parcel というよりはこっちの問題の可能性が高いです
もう少し調べてみようかなと思い 再現する環境を準備でしていました
しかし なぜか再現しません
バージョンは変わっていません
ですが PC を変えてみると再現します
発生しない PC だと WSL の Linux 環境でも Windows 環境でも発生していません
最適化の処理は Rust で書かれたネイティブな部分ですし CPU の種類が影響したりするのでしょうか?
たしかに発生している PC はいずれも Intel 製で Windows11 にできないくらいは古いもので 発生していないのはそれよりは新しい AMD 製です
ただ ARM ならともかく Intel と AMD ならこんな問題起きないように思いますけど
中で並列処理をしていて 実行速度によってタイミングの問題で発生する場合があったりでしょうか
どうにかできそうなものじゃなさそうですし 修正される期待もあまりできないので Parcel を使うのをやめたほうが早そうです
最近は Vite 一択になりつつありますし Parcel 使ってるのもできるだけ Vite に移していきたいですね
ただ Vite は最近の更新で Parcel で使ってる lightningcss をサポートしたようです
その流れで画像の最適化も始めて Parcel と同じ問題を持ってくるというのが心配です
今年の頭くらいにEyeDropper を使ったときは特別な条件なく コンソールから実行したらカラーピッカーが表示されていたと思います
こういうのの実行は基本 devtools のコンソールで行ってますし そこで制限があったら注意書きを書いてそうですけど特に書いてませんし
ついさっき Chrome 116 で使おうとしたらこんなエラーが出ました
キャンセルはしてないのですけど
ウィンドウがアクティブじゃないからかもと予想して時間差で実行するようにして その間にウィンドウをアクティブにするようにしました
その場限りの使い捨てですし 時間よりクリックの方が急いだり待ったりしなくていいかもしれません
これだとカラーピッカーが表示されました
ウィンドウが非アクティブ時はカラーピッカーが表示されずキャンセル扱いになるみたいですね
この機能はブラウザ外部にも影響するものなので ブラウザ以外のところを操作中に急にカラーピッカーが表示されて混乱するようなことを避けるためでしょうか
仕方ないとはいえ コンソールから簡単にすぐ使えるメリットがなくなったのは残念です
こういうのの実行は基本 devtools のコンソールで行ってますし そこで制限があったら注意書きを書いてそうですけど特に書いてませんし
ついさっき Chrome 116 で使おうとしたらこんなエラーが出ました
DOMException: Failed to execute 'open' on 'EyeDropper': The user canceled the selection.
キャンセルはしてないのですけど
ウィンドウがアクティブじゃないからかもと予想して時間差で実行するようにして その間にウィンドウをアクティブにするようにしました
setTimeout(
() => new EyeDropper().open().then(console.log),
3000
)
その場限りの使い捨てですし 時間よりクリックの方が急いだり待ったりしなくていいかもしれません
window.onclick = () => new EyeDropper().open().then(console.log)
これだとカラーピッカーが表示されました
ウィンドウが非アクティブ時はカラーピッカーが表示されずキャンセル扱いになるみたいですね
この機能はブラウザ外部にも影響するものなので ブラウザ以外のところを操作中に急にカラーピッカーが表示されて混乱するようなことを避けるためでしょうか
仕方ないとはいえ コンソールから簡単にすぐ使えるメリットがなくなったのは残念です