お知らせ

現在サイトのリニューアル作業中のため、全体的にページの表示が乱れています。
投稿日:
ソフトウェア::VSCode言語::TypeScript

新しくセットアップしたTypeScriptのプロジェクトでauto-importが機能しなかったので原因を調べた。

ここ数年ちまちま起きて、そろそろイラついて我慢の限界を覚えたので…。

確認環境

Env Ver
VSCode 1.83.0
typescript 4.8.4

再現方法

再現用のサンプルリポジトリsrc/index.tsの下図コードに対してauto-importが発動する操作を行う。

auto-importに失敗し、利用可能なコードアクションはありませんと出る

発生条件

恐らく以下を全て満たすときにauto-import操作をしようとした時に発生する。地味にややこしい。

node_modules配下に存在し、@types/モジュールを持たないものでかつ、同一プロジェクト内でimportされておらず、package.jsondependenciesに書かれていない。

因みにhoge/piyo/fugaのようなモジュールはhogeだけがdependenciesにいてもauto-importが動かない。これを全部package.jsondependenciesに書いていくのは目眩がするので正直auto-importを諦めて自分でimportを調べて書くのが無難だと思う。

発生原因

TypeScript 4.0で実装されたSmarter Auto-Importsのせい。

node_modulesをクロールすると重すぎるので@types/だけ読み込んで、あとはpackage.jsondependenciesも見るようにしたよという内容らしい。

解消方法

package.jsondependenciesに全部のモジュールを書いていくというのが解決方法になるが、気が遠くなるので諦めた方がいい。初回だけは苦痛でもimportパスをどうにかして調べて手動で書いて、あとはauto-importされるのを期待するのが良いだろう(そんな何度もimportしないと思うが…)

取り敢えず基本npm iで入れて行き、スラッシュ区切りのやつは諦めるくらいがちょうどいいだろう。

再現用のサンプルリポジトリではpackage.jsondependencies"firebase/app": "^10.4.0"を追加することでsrc/index.tsinitializeAppに対しauto-importが発動する様になるはずだ。

あとがき

早い話、node_modules配下にあって@types/を持たないものはTypeScriptに対応する気がないんだな程度に思っておくのが良いだろうが、TS化で@types/を廃止したライブラリがある当たり、すごく微妙な感じがある。ちょっとなんとかしてほしい。

そもそもpackage.jsondependenciesはnpm packageを公開する時に動作するために必要な依存関係を登録する場所で、型の補助をする場所ではなかったはずだ。

少なくともnpmjsのdevDependenciesには以下の記述がある。つまりモジュールを配布する時に動作に不要な依存関係を含まないようにするためにdevDependenciesがあるということだ。つまりdependenciesには動作に必要なものだけを入れるべきで、モジュールを配布しない場合、このフィールドは不要になるはずである。

If someone is planning on downloading and using your module in their program, then they probably don't want or need to download and build the external test or documentation framework that you use.

In this case, it's best to map these additional items in a devDependencies object.

ただTypeScriptがdependenciesに書かないとauto-importが失敗すると言っているので使うものはdependenciesに入れるというのが良いのだろう。(配布しないモジュールだとしても)

ただそれにしてもauto-importを動かすためだけに、dependenciesに同一モジュールのスラッシュ違いを大量に入れていくのはバカバカしいと思う。

  "dependencies": {
    "firebase": "^10.4.0",
    "firebase/app": "^10.4.0",
    "firebase/database": "^10.4.0",
    "firebase/analytics": "^10.4.0",
    ...
  }

かといってinitializeAppfirebase/appにあるなんて知らんわけで、全部のパスに総当りしていくのも嫌だし、一々リファレンス漁るのも面倒だし、なんかもうちょっといい具合になって欲しい…。

確認環境

Env Ver
OS Ubuntu 20.04.4 LTS
PHP 8.0.29
nginx 1.18.0

手順

php-fpmの導入

sudo apt install php8.0-fpm
sudo sed -i -e 's/;listen.mode = 0660/listen.mode = 0666/' /etc/php/8.0/fpm/pool.d/www.conf
sudo service php8.0-fpm start

nginxの設定

設定ファイルを開きPHPを動かす設定を書く

location ~ ^/.*$ {
  root /path/to/www;
  fastcgi_pass unix:/run/php/php8.0-fpm.sock;
  fastcgi_index index.php;
  include fastcgi_params;
  fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
}

undefinedの判定方法が複数あるということでundefined判定の処理速度比較をしてみたのでその結果。

端的に言うと、hoge === undefinedtypeof hoge === 'undefined'の二方式がある。後者は原則考慮不要だが、言語仕様上存在しているので比較したが、現実的に見た場合、どちらで記述した場合でも処理速度に有意な差はないように感じた。

確認環境

Env Ver
Node.js 20.1.0
TypeScript 4.9.5
@swc/core 1.3.8

比較結果

hoge === undefinedの方が早く見えるが実行するタイミングで変わるので誤差の範疇だと思う。

方式 ms
hoge === undefined 4,514
typeof hoge === 'undefined' 4,515

確認コード

const tyof = (param?: string) => {
  return typeof param === 'undefined';
};

const undef = (param?: string) => {
  return param === undefined;
};

const tyStart = +new Date();
for (let i = 0; i < 10000000000; i++) {
  tyof();
}
console.log('typeof', +new Date() - tyStart);

const unStart = +new Date();
for (let i = 0; i < 10000000000; i++) {
  undef();
}
console.log('undefined', +new Date() - unStart);

TSから生成されたJS

"use strict";
Object.defineProperty(exports, "__esModule", {
    value: true
});
const tyof = (param)=>{
    return typeof param === 'undefined';
};
const undef = (param)=>{
    return param === undefined;
};
const tyStart = +new Date();
for(let i = 0; i < 10000000000; i++){
    tyof();
}
console.log('typeof', +new Date() - tyStart);
const unStart = +new Date();
for(let i = 0; i < 10000000000; i++){
    undef();
}
console.log('undefined', +new Date() - unStart);

あとがき

MDNを読む限りtypeof hoge === 'undefined'は該当変数が存在しない場合に有用なようであるが、TypeScriptで書いている場合、通常このようなコードが生まれることがなく、仮に起きるとした場合、次のようなコードになるため現実的に考慮する必要はない。なおMDNにも「こんなことはしないこと」と書いてあるので、一般的なコードでないことは客観的にも伺えるだろう。

(() => {
  const undefined = 123;
  const hoge = undefined;

  if (typeof hoge === 'undefined') {
    console.log('hoge is undefined');
  } else {
    console.log('hoge is not undefined');
  }
})();

上記コードの実行結果としてはhoge is not undefinedが出力される。

このコードの主な問題点

  1. const undefined = 123;というコードは予約語を変数名にしているため、混乱を招くコードであり、書かないことが好ましい
    1. MDNには予約語ではないとあるが、一般的には予約語の一つとして解釈して支障ないと考える
  2. このコードはESLintのeslint:recommendedで検知されるため、通常であれば書かれることはない
    1. no-shadow-restricted-namesに引っかかる

なお、このコードは例示のために即時実行関数形式で記述しているが、必要がない限りこの形式での実装は避けたほうが問題が少なくなると思う。これは不必要なネストが生まれたり、スコープの混乱を生むためである。

投稿日:
言語::MarkdownNode.js::Prettier

2023-07-05にPrettier 3.0がリリースされて大分経ったので個人的に気になったところのまとめ。Highlights~Breaking Changesの部分だけ書いてます。Other Changesまで書くと長すぎるので…(Other Changesはざっと見た感じ概ね可読性が向上する感じの内容だと思いましたが、行が動くので書き方によってはコードや、行ベースで動作しているCIの検査が壊れる可能性が場合によってはありそうです。)

変更点

気になったところしか書いてないのでちゃんと見たい人は本家のリリースノートを見てください

日本語と西洋文字の間に半角スペースを挿入しない様に

Stop inserting spaces between Chinese or Japanese and Western characters

恐らく日本人的にはこれが今回最大の変更点になると思います。かなり賛否両論があると思いますが、私は肯定派です。むしろ半角スペースが入るようになって以来、これが来る日をずっと待ちわびていた。

今までは全角文字と半角文字の間に半角スペースが挿入されていましたが、これがなくなります。これは可読性の観点では悪くない処理だとは思うのですが、ドキュメントとしてみた場合に検索性が非常に悪く個人的に好きではありませんでした。

<!-- 入力値 -->
漢字
Alphabetsひらがな12345カタカナ67890

<!-- Prettier 2.8 -->
漢字 Alphabets ひらがな 12345 カタカナ 67890

<!-- Prettier 3.0 -->
漢字Alphabetsひらがな12345カタカナ67890

なお、この変更には後方互換性がなく、今までのように半角スペースを挿入することはできなくなります。

また、新しい方式に既存のドキュメントを合わせる場合、Prettierが挿入したこの半角スペースを自動で綺麗に除去するのは難しいと思います。探せばあるかもしれませんが、恐らくtextlintの除去ルールでは不完全だと思います。私が試した限りでは見出しやリンクタイトル辺りでは上手く機能してくれませんでした。ただ、ある程度はtextlintが解決してくれるので、大まかにはtextlintを使い、細かい部分は手修正していくのが無難かなと思います。

設定ファイルでESM形式をサポート

https://prettier.io/blog/2023/07/05/3.0.0.html#support-config-files-in-esm-13130httpsgithubcomprettierprettierpull13130-by-fiskerhttpsgithubcomfisker

今まではCJS形式で書く必要があったconfigファイルでESM形式がサポートされたようです。但し拡張子が.jsである場合はpackage.json{"type": "module"}を指定する必要があります。

<!-- Prettier 2.8 -->
module.exports = {
  // ...
}

<!-- Prettier 3.0 -->
export default {
  // ...
};

trailingComma設定の初期値をes5からallに変更

https://prettier.io/blog/2023/07/05/3.0.0.html#change-the-default-value-for-trailingcomma-to-all-11479httpsgithubcomprettierprettierpull11479-by-fiskerhttpsgithubcomfisker-13143httpsgithubcomprettierprettierpull13143-by-sosukesuzukihttpsgithubcomsosukesuzuki

これは正直あまり良く解っていないのですが、Trailing Commasを読んだ感じ、恐らく関数の仮引数の最後にカンマが入るかどうかだと思われます。つまり以下のような感じです。

<!-- 入力値 -->
function HelloWorld(greeting = "hello", greeted = '"World"', silent = false, onMouseOver) {
}

<!-- trailingComma: es5 -->
function HelloWorld(
  greeting = "hello",
  greeted = '"World"',
  silent = false,
  onMouseOver // カンマが入らない
) {}

<!-- trailingComma: all -->
function HelloWorld(
  greeting = "hello",
  greeted = '"World"',
  silent = false,
  onMouseOver, // カンマが入る
) {}

上記の結果はこちらのPlaygroundで確認できます。(--trailing-commaの設定値をいじると比較できます)

個人的にはこの修正は余り歓迎できないですね…。というのも不要なカンマが入っていると引数の数を見間違えたり、ケアレスミスの原因になると思いますし、いたずらにコードが長くなるからです。

今まで深く考えたことがありませんでしたが、trailingCommanoneが例外がなく、統一感があり、コードが短くなり、ケアレスミスが減ると思うため無難だと思いました。noneにしている場合、単純に一番最後の要素にカンマが入らなくなります。

BabelパーサーからFlow構文のサポートを削除

https://prettier.io/blog/2023/07/05/3.0.0.html#remove-flow-syntax-support-from-babel-parser-14314httpsgithubcomprettierprettierpull14314-by-fiskerhttpsgithubcomfisker-thorn0httpsgithubcomthorn0

これによりPrettierの動作パフォーマンスが上がったようです。

デフォルトで.gitignoreのファイルを無視するように変更

https://prettier.io/blog/2023/07/05/3.0.0.html#ignore-gitignored-files-by-default-14731httpsgithubcomprettierprettierpull14731-by-fiskerhttpsgithubcomfisker

.prettierignoreではなく.gitignoreを見るようになったようです。これもなんとも微妙な変更だと思います…。どっちかといえば.eslintignoreを見てもらいたいような気もするような…。従前のようにしたい場合はCLIのオプションに--ignore-path=.prettierignoreを渡してやると行けるようです。

関連記事

Web開発をしていると取り敢えずHTTPリクエストが取れる雑なモックサーバーが欲しくなることがあるので、その作り方。

確認環境

Env Ver
OS Windows 11 Pro
WSL2 -
Distoribution Ubuntu 20.04.4 LTS
PHP 8.0.29
nginx 1.18.0

作り方

  1. Windows側の hosts127.0.0.1 mock-server.test を追加
  2. /etc/nginx/conf.d に以下の設定ファイルを置く
server {
  listen       80;
  client_max_body_size 100m;
  server_name  mock-server.test;
  access_log   /var/log/nginx/mock-server.access.log;
  error_log    /var/log/nginx/mock-server.error.log;


  location ~ ^/.*$ {
    rewrite ^/.*$ / break;
    # リクエストパスの確認用
    proxy_set_header X-Request-Path $request_uri;
    proxy_pass  http://127.0.0.1:8888;
  }
}
  1. sudo service nginx restart
  2. モックサーバーのコードを書いて適当な場所に置く
<?php

$server_json = json_encode($_SERVER);

file_put_contents('log', "$server_json\n", FILE_APPEND);
  1. php -S localhost:8888
  2. http://localhost:8888/hoge にアクセスしログファイルに追記されることを確認
  3. おわり

おまけ

$_SERVER の中身をフィルタしたい時

<?php

$ignoreKeys = [
    'DOCUMENT_ROOT',
    'REMOTE_ADDR',
    'REMOTE_PORT',
    'SERVER_SOFTWARE',
    'SERVER_PROTOCOL',
    'SERVER_NAME',
    'SERVER_PORT',
    'REQUEST_URI',
    'SCRIPT_NAME',
    'SCRIPT_FILENAME',
    'PHP_SELF',
    'HTTP_HOST',
    'HTTP_CONNECTION',
    'HTTP_UPGRADE_INSECURE_REQUESTS',
    'HTTP_USER_AGENT',
    'HTTP_ACCEPT',
    'HTTP_ACCEPT_ENCODING',
    'HTTP_ACCEPT_LANGUAGE',
    'HTTP_CACHE_CONTROL',
    'REQUEST_TIME_FLOAT',
    'REQUEST_TIME'
];

$buff = array_filter($_SERVER, function($key) use($ignoreKeys) {
    return !in_array($key, $ignoreKeys);
}, ARRAY_FILTER_USE_KEY);

$server_json = json_encode($buff);

file_put_contents('log', "$server_json\n", FILE_APPEND);