puppeteer 13.4.0

puppeteer 13.4.0がリリースされました。

さっそくリリースノートを見ていきましょう。

Features
・add support for async waitForTarget
・export Frame._client through getter
・HTTPResponse: expose timing information
Bug Fixes
・change kill to signal the whole process group to terminate
・element screenshot issue in headful mode
・ensure dom binding is not called after detach
・use both __dirname and require.resolve to support different bundlers

https://github.com/puppeteer/puppeteer/releases/tag/v13.4.0

waitForTargetがasync対応したこと以外は特に大きな変更はないようです。

面白そうなものはHTTPResponseクラスにtiming()メソッドが追加されたことです。詳細はこちらを見てほしいのですが、HTTPレスポンスに要した様々な時間を見ることができます。通信や処理のボトルネックを探す際に役立つかもしれません。

ADSLの廃止

www.magisystem.netのサーバを配置していた固定ADSL回線を廃止して、光回線に移行しました。これを機会にサーバはさくらのレンタルサーバに移行したので、光回線は固定IPにしませんでした。

サーバ自体はADSLの前身であるテレーホーダイやINS64の時代(2000年)から運用していたのですが、IIJmioがADSLでの固定IPサービスを提供していることを知った2005年から17年も同じサービスを使い続けてきました。これで廃止となってしまうのは感慨深いです。今までありがとう。

ADSLサービス

puppeteer 13.3.2

puppeteer 13.3.2がリリースされました。

さっそくリリースノートを見ていきましょう。

Bug Fixes
・always use ENV executable path when present
・use require.resolve instead of __dirname

https://github.com/puppeteer/puppeteer/releases/tag/v13.3.2

今回はそこまで影響がある修正ではありませんでしたが、先日のCommon JS関連でもう1つだけ。

__dirname はCommon JSでのみ使える変数です。ES moduleではありません。同様に __filename も同じです。ES moduleで書く場合は使えないので気を付けましょう。

puppeteer 13.3.1

puppeteer 13.3.1がリリースされました。

さっそくリリースノートを見ていきましょう。

Bug Fixes
・puppeteer: revert: esm modules

https://github.com/puppeteer/puppeteer/releases/tag/v13.3.1

先日紹介した ES module に関する修正を巻き戻したとのことです。

ここでちょっとだけおさらいを。ES moduleではexportsで何をモジュールとして提供するかを明確に指定することができます(一応defaultsもあるのですが、今回は説明を割愛)。これは、モジュールで提供されている関数やクラスなどを後から上書きできないようする、いわばES moduleのセーフティ機構でもあるわけです。これが前回の修正では裏目に出てしまったようです。

Common JSではモジュールは結局のところただのオブジェクトです。そのためモジュールとして公開している意図はなかったとしても、オブジェクトからプロパティとして見えていたら使えてしまうわけです。この puppeteer の内部のプロパティを参照する使い方をしている NodeJS モジュールがあったのが問題だったわけです。 Common JS では問題なく見えていたのですが、 ES module 用に厳密なexportsを定義したところ、puppeteerチームは公開しているつもりがなかったプロパティがexportsされなくなってしまったのです。そして、このプロパティを参照してた NodeJS モジュールの方では値が取れなくなってしまったわけです。

JavaやC#、そしてTypeScriptであればprivate/publicなどで制御できたかもしれませんが、ゆるゆるなJavaScriptだと気付かないところでプロパティを直参照される・・・なんてことが起きやすいのです。そういった不慮の公開(?)を防ぐ上でも ES module としてexportsすることに効果はあると思います。

puppeteer 13.3.0

先日13.2.0が出たばかりなのですが、puppeteer 13.3.0がリリースされました。

さっそくリリースノートを見ていきましょう。

Features
・puppeteer: export esm modules in package.json

https://github.com/puppeteer/puppeteer/releases/tag/v13.3.0

今回の修正はesm modulesの対応でした。ところで Common JS と ES module の違いはご存知でしょうか?

Common JS

Common JS は従来より NodeJS のデフォルトとなっていたモジュールの管理方式です。 require 文を使ってモジュールを呼び出す書き方でよく見ると思います。実際、 puppeteer のサンプルも Common JS で書かれています。

Common JS では async/await を使うときに注意があります。グローバルスコープでは async/await を書くことができないのです。そのため、(async () => { ... })() のように async/await の処理を即時関数の中に閉じ込める必要があります。下にサンプルコードを示しておきます。13.2.0で追加されたiPhone 13のデバイス定義をさっそく使っています。

const puppeteer = require('puppeteer');

(async () => {
        const browser = await puppeteer.launch({
                headless: true
        });
        const page = await browser.newPage();
        await page.emulate(puppeteer.devices['iPhone 13']);
        await page.goto('https://www.magisystem.net/blog/', {
                waitUntil: 'networkidle0'
        })
        await page.screenshot({ path: 'example.png' });
        await browser.close();
})();

Common JSでは従来の .js 拡張子に加えて .cjs を使うことができます。これは .js だと Common JS か ES module かの区別が付き難いからです。明示的に Common JS と言いたいときは .cjs とするとよいでしょう。

ES module

一方で、 ES module はES2015 (ECMAScript 2015/ES6)で標準化されたJavaScriptの仕様です。 Common JS との最大の違いは、ES2015に準拠したブラウザでも使えるということです。つまり、ブラウザでもモジュールを使えるようにしたのがこの仕様です。 import 文と export 文を使ってモジュールを使用します。最近の npm モジュールもこの書き方が増えてきましたので、目にする機会もあると思います。

ES moduleの import の特徴は2つあります。

1つは require と異なり、 export した名前(クラス、定数、関数など)を後から上書きすることを防止しています。つまり、悪意のあるモジュールが別のモジュールの関数やクラスを上書きできないようにして、安全性を高めているわけです。

2つ目は import の処理が非同期であることです。 Common JS の require は同期処理です。そのため、 require しているモジュールが更にいくつものモジュールを読み込んでいたり、何か重たい初期化処理があると readFileSync() のように require の部分で待ちが発生します。 ES module の import は読み込み自体は非同期に行い、モジュールのクラスや関数を使うときにリゾルブして処理を始めるように動作します。よって、多くのモジュールを読み込む場合には速度メリットが得られるようになります。その反面、実行時まで解決されないので、トランスパイラや静的パーサーと相性が悪いようです。

さて、 ES module は import が非同期であるため、グローバルスコープで async/await をそのまま書くことができます。即時関数に閉じ込める必要がなくなるのは便利です。下にサンプルコードを示しておきます。

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({
        headless: true
});
const page = await browser.newPage();
await page.emulate(puppeteer.devices['iPhone 13']);
await page.goto('https://www.magisystem.net/blog/', {
        waitUntil: 'networkidle0'
})
await page.screenshot({ path: 'example.png' });
await browser.close();

ES moduleでは従来の .js 拡張子に加えて .mjs を使うことができます。明示的に Common JS と区別したい場合は .mjs とするとよいでしょう。 ES Lint などのパーサーも .mjs にしないと ES module と認識しないのか、グローバルスコープの async/await をエラーにする場合があります。

Common JS or ES module?

Common JS で書くか ES module で書くか…これは宗教戦争になってしまうので明言は避けておきますが、指針は出しておきたいと思います。

まず、 NodeJS でしか実行しない場合は Common JS でいいと思います。普及している npm モジュールも Common JS ですし、 ES module に対応していないものもまだあります。ただ、 async/await を標準として使って書く場合は、上記の理由から ES module にした方が可読性が上がります。また、 async/await 系の処理を使う npm モジュールは、大抵の場合は import にも対応しています。

次にブラウザと NodeJS で共通する処理を書く場合です。この場合は ES module で書くことを始めた方がよいでしょう。従来のように全てをグローバルスコープに読み込む方法から、モジュールを使う方法に変えていくのがよいです。ブラウザに関して言えば、Chromeはもちろん、Firefox、Opera、Safariもサポートしているので、現在のブラウザ環境では問題なく使えると言えます。なお、ブラウザで使う場合は拡張子を .js のままにしておくのがよいです。というのはウェブサーバは .js しかJavaScriptと扱わないことが多いからです。

おまけ: sandboxについて

puppeteerのノウハウには --no-sandbox を指定するように書いている記事が多く見られます。しかし、これはセキュリティ上からはお薦めしません。セキュリティに気を使うGoogleがデフォルトで用意しているsandboxの仕組みをあえて無効にする必要はありません。

[kusanagi@kusanagi8 pp]$ node index.cjs
/home/kusanagi/work/pp/node_modules/puppeteer/lib/cjs/puppeteer/node/BrowserRunner.js:229
            reject(new Error([
                   ^

Error: Failed to launch the browser process!
[0210/100340.343786:FATAL:zygote_host_impl_linux.cc(117)] No usable sandbox! Update your kernel or see https://chromium.googlesource.com/chromium/src/+/main/docs/linux/suid_sandbox_development.md for more information on developing with the SUID sandbox. If you want to live dangerously and need an immediate workaround, you can try using --no-sandbox.
#0 0x55c3695e7a59 base::debug::CollectStackTrace()
#1 0x55c36954ee93 base::debug::StackTrace::StackTrace()
#2 0x55c369561e60 logging::LogMessage::~LogMessage()
#3 0x55c3676715ab content::ZygoteHostImpl::Init()
#4 0x55c3690ffc8a content::ContentMainRunnerImpl::Initialize()
#5 0x55c3690fd6d6 content::RunContentProcess()
#6 0x55c3690fe0c7 content::ContentMain()
#7 0x55c369158f1a headless::(anonymous namespace)::RunContentMain()
#8 0x55c369158c25 headless::HeadlessShellMain()
#9 0x55c365e6633b ChromeMain
#10 0x7fa6c7054555 __libc_start_main
#11 0x55c365e6616a _start
(以下省略)

もしもCentOSの環境で上記のようなエラーが出た場合は、以下のコマンドを実行してみてください。

[kusanagi@kusanagi8 pp]$ sudo sysctl -w user.max_user_namespaces=28633

usernsを有効にすることでsandboxが使えるようになります。