【nodejsアプリ開発】SvelteKitでポータブルなバイナリアプリを作れるか(しかし現状では失敗)


※ 当ページには【広告/PR】を含む場合があります。
2023/02/09
【nodejsアプリ開発】pkg/Express.js/Svelteでポータブルなバイナリ起動のウェブブラウザアプリを作る
SvelteKitとAWS Lambda@Edgeで始めるサーバーレスなハイブリット(SSR/SSG)ウェブページを楽々作成する
蛸壺の技術ブログ|pkg/SvelteKitでポータブルなバイナリ・ウェブブラウザアプリを作る

SvelteKitのアダプター(adapter)を使うと、ビルドされたアプリをデプロイ先に合わせた最適な生成物を出力することができます。

例えば公式では以下のアダプターが用意されています。

            
            + Cloudflare (Pages): @sveltejs/adapter-cloudflare
+ Cloudflare (Workers): @sveltejs/adapter-cloudflare-workers
+ Netlify: @sveltejs/adapter-netlify
+ Node.js(サーバーサイド): @sveltejs/adapter-node
+ SSG(Static Site Generation)用: @sveltejs/adapter-static
+ Vercel: @sveltejs/adapter-vercel
        
他にもAWSなどのSaaSプラットフォーム向け公開されてるサードパーティ製アダプターも存在しています。

前回は単体のコアな
「Svelte」を使ってnodejs/Expressバイナリアプリの作成方法を解説していました。

合同会社タコスキングダム|蛸壺の技術ブログ
【nodejsアプリ開発】pkg/Express.js/Svelteでポータブルなバイナリ起動のウェブブラウザアプリを作る

nodejsでvercel/pkg&Express.jsを使ってJavascriptフレームワークからビルドしたSPAをローカル起動できるようなバイナリプログラムを作成する手順を紹介します。

他方でSvelteKitでnodejsバイナリアプリをビルドしたいと思っても、SvelteKitではフルスタックの次世代の開発フレームワークとして注目されている
「Vite」が採用してあり、esbuildによるビルドが非常に高速です。

SvelteKit自体は面倒なセットアップなしにViteによる開発環境がインストール一発で整うことが魅力ですが、余りに“いたれりつくせり”であるため、nodejsバイナリアプリを生成するには実は向いていない...というのが現状です。

現状でSvelteKitからNodejsバイナリアプリを生成する方法を検証した結果を紹介します。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート】nodejsをこれから学びたい人のためのオススメ書籍&教材特集

SvelteKitへadapter-nodeを導入する

nodejsアプリを出力ターゲットとするため、今回利用するアダプターはadapter-nodeになります。

以下のコマンドでnpmパッケージをインストールしましょう。

            
            $ yarn add @sveltejs/adapter-node -D
        
インストール後に、もともとSvelteKitで標準になっているアダプター(adapter-auto)からadapter-nodeに切り替えるように、svelte.config.jsを以下のように編集します。

            
            //👇コメントアウト
//import adapter from '@sveltejs/adapter-auto';

//👇adapter-nodeに置換
import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/kit/vite';

/** @type {import('@sveltejs/kit').Config} */
const config = {
    preprocess: [vitePreprocess()],
    kit: {
        adapter: adapter()
    }
};

export default config;
        
とするだけで準備が完了です。

あとはいつもの要領でビルドするだけです。

            
            $ yarn build
        
これだけでVite無しにnodeで動くアプリに出力してくれます。すこぶる簡単ですね♪

合同会社タコスキングダム|蛸壺の技術ブログ

デフォルトでは
buildというフォルダに出力されるようです。

この時点で、nodeで動かせることを確認しましょう。

            
            $ node build/index.js
        
ブラウザから起動したサーバーにアクセスさせてみると、

合同会社タコスキングダム|蛸壺の技術ブログ

というようにブラウザから見れていればOKです。

またここではポートはデフォルトの3000から別の番号に変えていますが、出力先のフォルダを変えたい場合や環境変数のプリフィックスを設定したいときには以下の公式のようにアダプタに設定を行ってみてください。

Node サーバー Option |adapter-node

このようにnodeコマンド単体で動かすのであれば、
adapter-nodeは非常に重宝することができます。

他方で、ここからnodejsバイナリアプリを生成しようとすると色々と制約が足かせになってきます。

以降では、どこでバイナリ化がつまずくのか、「pkg」と「nexe」のケースを個別に紹介します。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート】nodejsをこれから学びたい人のためのオススメ書籍&教材特集

pkgでSvelteKitアプリのバイナリ化

まずは、「pkg」によるバイナリ化を試してみましょう。

ずばりいうと、
現状ではSvelteKitのnodeアダプターとpkgの相性がとても噛み合わないのでおそらく上手く起動しない、というのが結論です。

というのは、

            
            - pkgは現状でcommonjsしか使えない
- Vite(SvelteKit-nodeアダプター)はesmしか出力対応していない
        
という、Javascriptの深刻なテーマである「CommonJS vs. ES Module」の問題に行き着いてしまうからです。

ということで、もうこうなるとダメ元で
babelを使ってesmからcommonjsに変換できるように無理やりトランスパイルをかけてみるしか良い用法もなさそうです。

前回の内容の繰り返しになりますが、「pkg」プロジェクトを作成し、SvelteKitで出力したリソースをターゲットにバイナリ化してみます。

pkgアプリ用に適当なプロジェクトフォルダを作って、先程生成したSvelteKitの生成物(buildフォルダ)をそこにコピーして、
package.jsonを新規追加します。

            
            $ mkdir sk-binary
$ cp build sk-binary/
$ cd sk-binary
$ touch package.json
        
以下のようなpkgバイナリアプリ向けにpackage.jsonを使いますが、詳しい設定の話は前回の記事でダイジェストにまとめましたのでここでは省略します。

            
            {
    "name": "pkg-sveltekit",
    "version": "0.0.1",
    "description": "How to make a pkg app with SvelteKit.",
    "bin": "lib/index.js",
    "scripts": {
        "pkg": "pkg",
        "convert": "babel --plugins @babel/plugin-transform-modules-commonjs build -d lib",
        "build": "yarn pkg . --debug"
    },
    "pkg": {
        "targets": ["latest-linux-x64"],
        "outputPath": "dist"
    },
    "devDependencies": {
        "@babel/cli": "^7.18.10",
        "@babel/core": "^7.18.10",
        "@babel/plugin-transform-modules-commonjs": "^7.20.11",
        "@babel/preset-env": "^7.18.10",
        "nexe": "^4.0.0-rc.2",
        "pkg": "^5.8.0"
    }
}
        
ファイルの準備が出来たらパッケージをインストール&babelトランスパイル、pkgビルドしてみます。

            
            $ yarn install
$ yarn convert
$ yarn build
        
とりあえずCommonjsにすれば動くかというとそうではありません。

まずコンパイルエラーとなるのが、

            
            SyntaxError: await is only valid in async functions and the top level bodies of modules
    at new Script (node:vm:102:7)
    at Socket.<anonymous> ([eval]:18:19)
    at Socket.emit (node:events:537:28)
    at addChunk (node:internal/streams/readable:324:12)
    at readableAddChunk (node:internal/streams/readable:297:9)
    at Readable.push (node:internal/streams/readable:234:10)
    at Pipe.onStreamRead (node:internal/stream_base_commons:190:23)
        
というnodeで良く問題として見られるトップレベルawaitのエラーです。

これは自動ではBabelもどうしてくれるものでもないので、試しに該当の箇所を手直ししてみます。

            
            //👇トップレベルawait = 以下のようにグローバルスコープにawaitがあるもの
await server.init({ env: process.env });

//↓↓↓↓↓↓↓↓

//👇asyncの即時関数で囲う
(async() => {
    await server.init({ env: process.env });
})();
        
これでエラー自体はなくなります。

次にエラーで生じる問題は以下のようなものです。

            
            SyntaxError: Cannot use 'import.meta' outside a module
    at new Script (node:vm:102:7)
    at Socket.<anonymous> ([eval]:18:19)
    at Socket.emit (node:events:537:28)
    at addChunk (node:internal/streams/readable:324:12)
    at readableAddChunk (node:internal/streams/readable:297:9)
    at Readable.push (node:internal/streams/readable:234:10)
    at Pipe.onStreamRead (node:internal/stream_base_commons:190:23)
        
このimport.metaはES Module(ES2020)から使えるようになったグローバルのメタデータを引き出すことのできる構文でCommonJS自体は当然受け付けないものです。

これをCommonJS風に例えば
import.meta.urlにあたるものに書き直すと、

            
            //👇ESModuleで利用されるメタデータの引き出し
const dir = path.dirname(fileURLToPath(import.meta.url));

//↓↓↓↓↓↓↓↓

//👇CommonJS風に書き直し
const import_meta_url = require("url").pathToFileURL(__filename);
const dir = path.dirname(fileURLToPath(import_meta_url));
        
ここまでやってみるとエラーが一旦なくなりビルドが通ります。

これでようやく動くかなとバイナリを早速起動してみてると...

合同会社タコスキングダム|蛸壺の技術ブログ

現状ははっきりとしない内部エラーでページのルーティングに失敗してしまうようです。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート】nodejsをこれから学びたい人のためのオススメ書籍&教材特集

nexeでSvelteKitアプリのバイナリ化

もう一つせっかくですので、nodejsアプリバイナリの雄・「nexe」を試してみます。

nexeの最大の魅力は
「ES Module対応」しているところです。

これで先程のpkgでクリティカルであったCommonJSかどうかはあまり意識しなくても良くなります。

ですが、依然として「Nexe」の最新バージョンは
「node14」相当です。

Nexeの公式を見てもv16まで引き上げるのにいまだ四苦八苦されているようで中々開発が難航しています。

残念ながらこちらも結論から言えばまだ満足にSvelteKitアプリをそのままバイナリ化は出来ません。

こちらもNodejsの悩ましい問題である、
「node-fetch(旧式)とfetch API(v18以降)の互換性」がボトルネックになっています。

とりあえず先程と同じプロジェクトからpkgはやめてnexeに鞍替えします。

            
            {
    "name": "pkg-sveltekit",
    "version": "0.0.1",
    "private": true,
    "description": "How to make a pkg app with SvelteKit.",
    "type": "module",
    "scripts": {
        "nexe": "nexe build/index.js --target=linux-x64-14.15.3"
    },
    "devDependencies": {
        "nexe": "^4.0.0-rc.2"
    }
}
        
これだけでnexeのバイナリ化が可能です。

            
            $ yarn install
$ yarn nexe
        
でバイナリパッケージが生成出来ました。

それから起動してみると...

            
            $./sk-binary
file:///********/sk-binary/dist/handler.js:12725
          this.cookies ??= [];
                       ^^^

SyntaxError: Unexpected token '??='
    at Loader.moduleStrategy (internal/modules/esm/translators.js:145:18)
        
というエラーが出てきます。

ここでエラーとなっている
「??」Null合体演算子なのに、「??=」が使えないのは盲点でした。

ちなみにnodev14から
「??」が利用できるようになっていますが、さらに砕けた構文の「??=」はそのまま使えないようです。

            
            this.cookies ??= [];

//↓↓↓↓↓↓↓↓

this.cookies = this.cookies ?? [];
        
再度ビルドすると、以下のようなエラーが出るようになります。

            
            internal/process/esm_loader.js:74
    internalBinding('errors').triggerUncaughtException(
                              ^

Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'stream' imported from ******/handler.js
    at packageResolve (internal/modules/esm/resolve.js:655:9)
    at moduleResolve (internal/modules/esm/resolve.js:696:18)
    at Loader.defaultResolve [as _resolve] (internal/modules/esm/resolve.js:810:11)
    at Loader.resolve (internal/modules/esm/loader.js:86:40)
    at Loader.getModuleJob (internal/modules/esm/loader.js:230:28)
    at ModuleWrap.<anonymous> (internal/modules/esm/module_job.js:56:40)
    at link (internal/modules/esm/module_job.js:55:36) {
  code: 'ERR_MODULE_NOT_FOUND'
        
これはnode-fetchと標準となったfetch APIの互換性エラーによるもので、node18以降に引き上げれば解決しそうではあるのですが、Nexeだとそうもいきません...。

こうなってくると自前で修正するとなるとかなり作業が厳しくなってきますし、せっかくのSvelteKit/Viteの良さとも言えるクライアント・サーバー側を区別無く動作できるFetch APIの実装が台無しになります。

事情を鑑みてもここはNexeのアップグレードも待った方が賢明と言えます。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート】nodejsをこれから学びたい人のためのオススメ書籍&教材特集

まとめ

SvelteKitはサクッと高機能なVite開発環境が設定なしに使えるようになる一方で、より古いJavascriptやnodejsの後方互換性には柔軟に対応することは出来ないようです。

あくまでも現状での話ですが、Svelteアプリをバイナリ化したい場合には素直にコアなSvelteをそのまま使う方が良いようです。

ただし、CommonJSもやがてはESMへと吸収されていく流れは止められないと想像できるため、あまり古いnodejs環境を使い続けることに固執すべきでないことも頭に入れておきましょう。

参考サイト

Adapters | Sveltekit

記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。

合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート】nodejsをこれから学びたい人のためのオススメ書籍&教材特集