deno + SolidJS の設定で試行錯誤

最近、SolidJSに興味を持ちました。
JSXを使うことができるけど、React系とはアプローチが異なり、仮想DOMを使わないことで効率が良いらしい。

公式ページの情報

公式ページの情報でも、deno + SolidJS については書いてあります。

Quick start - Solid Docs
Start building with Solid quickly. Try the playground, create projects with templates, and get your first Solid app runn...
deno init --npm solid

とすれば、いろいろと開発環境を整えてくれるらしいのですが、今回これは使いません。
この形式は私の好みじゃないのです。

私は deno + preact でいろいろ作ってきたので、同じように deno + SolidJS で頑張ってみようと思いました。

……のですが、結構大変でした。その話を書き残しておこうと思います。

最初のチャレンジ

deno + SolidJSなので、最初に deno の設定です。
deno.json を次のようにします。

{
    "compilerOptions": {
        "jsx": "preserve",
        "jsxImportSource": "npm:solid-js",
        "lib": [
            "dom",
            "deno.ns"
        ]
    },
    "imports": {
        "solid-js": "npm:solid-js"
    }
}

次に簡単なソースとして index.tsx を作ってみます。

import { render } from "solid-js/web";

document.addEventListener("DOMContentLoaded", () => {
    render(() => <div>Hello World</div>, document.body);
})

renderの書き方がpreactと少し違います。

vscodeの補完機能では、

(alias) render(code: () => JSX.Element, element: MountableElement): () => void
import render

と表示されます。第1引数がpreactと違いますね。
(preactなら、 render(<div>Hello World</div>, document.body)) でよい。)

最後にJavaScriptに変換。

deno bundle index.tsx --output index.js

できた index.js を読み込むように、適当な HTML を用意して開いてみると……

Uncaught Error 
    at notSup (c:\Users\purple\Documents\vscode\TypeScript\TestSolidJS\index.js:1038:9)
    at <anonymous> (c:\Users\purple\Documents\vscode\TypeScript\TestSolidJS\index.js:1043:3)

というエラーが出ました。

最初のチャレンジ失敗!

deno + SolidJS + esbuild

変換された index.js を調べると、次のようになっていました。

// index.tsx
document.addEventListener("DOMContentLoaded", () => {
  notSup(() => /* @__PURE__ */ React.createElement("div", null, "Hello World"), document.body);
});

deno.jsonの書き方(compilerOptionsとか)が悪いのかと、いろいろ試行錯誤したのですが解決しません。

deno bundle は preactには対応しているのですが、 SolidJSには対応していないので、notSup()となっているようです。

ここは deno bundle での変換をあきらめて、esbuildで変換を試みることにします。

esbuildを使うために、deno.json を変更します。

{
    "compilerOptions": {
        "jsx": "preserve",
        "jsxImportSource": "npm:solid-js",
        "lib": [
            "dom",
            "deno.ns"
        ]
    },
    "nodeModulesDir": "auto",
    "imports": {
        "solid-js": "npm:solid-js",
        "esbuild": "npm:esbuild"
    }
}

imports に esbuild を追加しました。

また、 nodeModulesDir を追加しました。
(これを設定しないと、esbuild が import を解決できずに、途中でエラー終了してしまいます。)

そして、変換用の build.ts を作ります。

import * as esbuild from "esbuild";

esbuild.build({
    "entryPoints": ["./index.tsx"],
    "outfile": "./index.js",
    "bundle": true,
    "platform": "browser",
    "format": "iife",
    "target": "esnext",
})

次のコマンドで変換します。

deno -A build.ts

index.jsはできたものの、HTMLを開いても、やはり何も表示されません。

デバッグコンソールにエラーが出ていました。

Uncaught Error 
    at <anonymous> (c:\Users\purple\Documents\vscode\TypeScript\TestSolidJS\index.js:757:40)
    at <anonymous> (c:\Users\purple\Documents\vscode\TypeScript\TestSolidJS\index.js:612:55)
    at updateFn (c:\Users\purple\Documents\vscode\TypeScript\TestSolidJS\index.js:48:40)
    at runUpdates (c:\Users\purple\Documents\vscode\TypeScript\TestSolidJS\index.js:322:19)
    at createRoot (c:\Users\purple\Documents\vscode\TypeScript\TestSolidJS\index.js:52:14)
    at render (c:\Users\purple\Documents\vscode\TypeScript\TestSolidJS\index.js:610:5)
    at <anonymous> (c:\Users\purple\Documents\vscode\TypeScript\TestSolidJS\index.js:757:5)

またも、チャレンジ失敗!!

deno + SolidJS + esbuild + esbuild-plugin-solid

変換された index.js を調べると、次のようになっていました。

  // index.tsx
  document.addEventListener("DOMContentLoaded", () => {
    render(() => /* @__PURE__ */ React.createElement("div", null, "Hello World"), document.body);
  });

notSup() はなくなりました。
でも、React.createElementとなっているのがあやしいです。
(だって、SolidJSなんだから)

自力では解決できなかったのでネット検索してみると、esbuildが SolidJS に対応できていないので、専用のプラグインを追加する必要がありました。

というわけで、esbuild-plugin-solid を deno.json の imports に追加します。

    "imports": {
        "solid-js": "npm:solid-js",
        "esbuild": "npm:esbuild",
        "esbuild-plugin-solid": "npm:esbuild-plugin-solid"
    }

build.ts も esbuild-plugin-solid を追加します。

import * as esbuild from "esbuild";
import { solidPlugin } from "esbuild-plugin-solid";

esbuild.build({
    "plugins": [
        solidPlugin(),
    ],
    "entryPoints": ["./index.tsx"],
    "outfile": "./index.js",
    "bundle": true,
    "platform": "browser",
    "format": "iife",
    "target": "esnext",
})

deno -A build.ts して、HTMLを開いてみます。

すると……今回は成功!!!

HTMLを開くと「Hello World」の文字が出てくれます。

index.js を確認すると、下のようになっていました。

  // index.tsx
  var _tmpl$ = /* @__PURE__ */ template(`<div>Hello World`);
  document.addEventListener("DOMContentLoaded", () => {
    render(() => _tmpl$(), document.body);
  });

今までとは全然違いますね。この形が SolidJS を使ったときの JavaScript になるようです。

node_modules を削除するチャレンジ

変換して、動かすことができるようになったものの、 node_modules ができてしまうのが、私にとっては気に入りません。
できれば排除したいです。

そのためには、まず deno.json を編集します。

{
    "compilerOptions": {
        "jsx": "preserve",
        "jsxImportSource": "npm:solid-js",
        "lib": [
            "dom",
            "deno.ns"
        ]
    },
    "imports": {
        "solid-js": "npm:solid-js",
        "esbuild": "npm:esbuild",
        "esbuild-plugin-solid": "npm:esbuild-plugin-solid"
    }
}

"nodeModulesDir": "auto",を削除しました。

これで node_modules/ が自動で作成されなくなります。

既に作成されている node_modules を削除した後、 deno -A build.ts を実行すると、次のようなエラーが出ます。

✘ [ERROR] Could not resolve "solid-js/web"

    index.tsx:1:39:
      1 │ import { template as _$template } from "solid-js/web";
        ╵                                        ~~~~~~~~~~~~~~

  You can mark the path "solid-js/web" as external to exclude it from the bundle, which will remove
  this error and leave the unresolved path in the bundle.

error: Uncaught (in promise) Error: Build failed with 1 error:
index.tsx:1:39: ERROR: Could not resolve "solid-js/web"
  let error = new Error(text);

index.tsx で import している "solid-js/web" が解決しないので、なんとかしろ!といっているようです。

その解決のために、"nodeModulesDir": "auto",を使っていたのです。それを削除したから当然の結果です。

まず、deno は nodeModulesDir がないときは、専用の場所に npm パッケージをダウンロードしています。

"solid-js/web"の場所は以下のコマンドを実行すればわかります。

deno eval 'console.log(import.meta.resolve("solid-js/web"))'

私の環境では、次のようになりました。

cyan:/mnt/c/Users/purple/Documents/vscode/TypeScript/TestSolidJS> deno eval 'console.log(import.meta.resolve("solid-js/web"))'
file:///home/purple/.cache/deno/npm/registry.npmjs.org/solid-js/1.9.11/web/dist/server.js

(WSL 環境のdenoなので、こんなディレクトリ名ですが、Windows版だと別のフォルダ名になるはず。)

そこで、build.ts に alias を追加して、"solid-js/web" と上の場所を関連付けます。

import * as esbuild from "esbuild";
import { solidPlugin } from "esbuild-plugin-solid";

esbuild.build({
    "plugins": [
        solidPlugin(),
    ],
    "entryPoints": ["./index.tsx"],
    "outfile": "./index.js",
    "bundle": true,
    "platform": "browser",
    "format": "iife",
    "target": "esnext",
    "alias": {
        "solid-js": "/home/purple/.cache/deno/npm/registry.npmjs.org/solid-js/1.9.11/dist/server.js",
        "solid-js/web": "/home/purple/.cache/deno/npm/registry.npmjs.org/solid-js/1.9.11/web/dist/server.js",
    }
})

"solid-js/web" のなかで "solid-js" を import しているので、それも alias しています。

これで deno -A build.ts をしてみると……

    ../../../../../../../../home/purple/.cache/deno/npm/registry.npmjs.org/solid-js/1.9.11/web/dist/server.js:3:61:
      3 │ import { Feature, Serializer, getCrossReferenceHeader } from 'seroval';
        ╵                                                              ~~~~~~~~~

のような、新たなエラーが出てきます。

このあと、エラーが出てくるパッケージ名を同じように alias に追加してみたものの、うまくいきませんでした。

うまくいかない理由と改良

うまくいかない理由は、"/home/purple/.cache/deno/npm/registry.npmjs.org/solid-js/1.9.11/web/dist/server.js"が正しくないからでした。

この server.js は、serverモード用のパッケージらしいです。

deno eval 'console.log(import.meta.resolve("solid-js/web"))'

でパッケージの場所を探していましたが、

denoを使っている。
→ ブラウザ用ではなく、deno(のserver)用として探し出す。
→ 結果 serverモード用の server.js を見つけてしまう。

という動作になっているようです。

/home/purple/.cache/deno/npm/registry.npmjs.org/solid-js/1.9.11/packaege.json を確認すると……

    "./web": {
      "worker": {
        "types": "./web/types/index.d.ts",
        "import": "./web/dist/server.js",
        "require": "./web/dist/server.cjs"
      },
      "browser": {
        "development": {
          "types": "./web/types/index.d.ts",
          "import": "./web/dist/dev.js",
          "require": "./web/dist/dev.cjs"
        },
        "types": "./web/types/index.d.ts",
        "import": "./web/dist/web.js",
        "require": "./web/dist/web.cjs"
      },
      "deno": {
        "types": "./web/types/index.d.ts",
        "import": "./web/dist/server.js",
        "require": "./web/dist/server.cjs"
      },

となっています。

  • deno用としては "./web/dist/server.js"
  • browser用としては "./web/dist/web.js"

にすると正しく解決できるようです。

そして、"solid-js" のファイルも同じように違っていました。

そこで、build.ts の alias を次のように修正します。

    "alias": {
        "solid-js": "/home/purple/.cache/deno/npm/registry.npmjs.org/solid-js/1.9.11/dist/solid.js",
        "solid-js/web": "/home/purple/.cache/deno/npm/registry.npmjs.org/solid-js/1.9.11/web/dist/web.js",
    }

deno -A build.ts を実行すると、エラーが出なくなりました。

変換された JavaScript も node_modules があるときと同じで、HTMLを開くと「Hello World」が表示されました。

なお、上の書き方もあまりよくないようで、最終的な build.ts は次のようになりました。

import * as esbuild from "esbuild";
import { solidPlugin } from "esbuild-plugin-solid";

esbuild.build({
    "plugins": [
        solidPlugin(),
    ],
    "entryPoints": ["./index.tsx"],
    "outfile": "./index.js",
    "bundle": true,
    "platform": "browser",
    "format": "iife",
    "target": "esnext",
    "nodePaths": [
        "/home/purple/.cache/deno/npm/registry.npmjs.org/"
    ],
    "alias": {
        "solid-js": "solid-js/1.9.11/dist/solid.js",
        "solid-js/web": "solid-js/1.9.11/web/dist/web.js",
    }
})

コメント