denoとesbuildでnode無しフロントエンドができるか?
node/npmを一切使わずにフロントエンドの開発環境作れるんじゃないか?と思いついたので試してみる。
なおesbuildは多少環境構築したことあるがdenoは触ったことない。
denoは適当にインストールする。思いつきネタなのでバージョンがどうこうとかは言わない。
githubのreleasesでバイナリ取ってくればそれだけで動く。
とりあえず動かす
適当な作業ディレクトリを掘って、 vim test.ts
とかして、サンプルコードをそのままコピーする。
import { serve } from "https://deno.land/std@0.96.0/http/server.ts";
const s = serve({ port: 8000 });
console.log("http://localhost:8000/");
for await (const req of s) {
req.respond({ body: "Hello World\n" });
}
deno run {script-file}
で動くっぽいので
$ deno run test.ts
Download https://deno.land/std@0.96.0/http/server.ts
...略...
Download https://deno.land/std@0.96.0/fmt/colors.ts
Check file:///home/medalhkr/workdir/no-node-frontend/test.ts
error: Uncaught PermissionDenied: Requires net access to "0.0.0.0:8000", run again with the --allow-net flag
そういえばdenoは明示的にシステムリソースへのアクセス許可を与えないといけないんだった。
$ deno run --allow-net test.ts
http://localhost:8000/
なんか動いた。
ちなみにdeno実行時にDownloadされる依存ソース群は当然キャッシュされるのだが、キャッシュに使うディレクトリは公式マニュアルによると
- On Linux/Redox: $XDG_CACHE_HOME/deno or $HOME/.cache/deno
- On Windows: %LOCALAPPDATA%/deno (%LOCALAPPDATA% = FOLDERID_LocalAppData)
- On macOS: $HOME/Library/Caches/deno
- If something fails, it falls back to $HOME/.deno
とのこと。手元のWSL環境では$HOME.cache/deno
に生えてた。
また
Deno caches remote imports in a special directory specified by the DENO_DIR environment variable. It defaults to the system's cache directory if DENO_DIR is not specified.
とのことなので、deno用にディレクトリを指定することもできるようだ。なんにせよ、勝手に$HOME
直下に置いて変更不可とかじゃなければok。
てか、適当にググってて気付いたけど、同じアイデアのビルドツールは存在するようだ。
まぁミニマル構成で遊ぶのが目的なので気にせずやってみる。
いや冷静に当たり前のように色々あるな。それはそうだわ。
ではdenoでesbuildを動かす。対象はこれか。
どう見てもjsファイルが一つしかなく、importするのはこれであろう。説明とか無いけど。
mod.js
の一番下を見ると、exportされている名前一覧がある。
export {
build,
buildSync,
formatMessages,
formatMessagesSync,
initialize,
serve,
stop,
transform,
transformSync,
version
};
version
とかいう動作確認にうってつけの名前を発見。
import {version} from "https://deno.land/x/esbuild@v0.11.23/mod.js";
console.log(version);
で
$ deno run --allow-read test.ts
0.11.23
動いた。
なおesbuild自体(mod.js)が--allow-read
を必要とするようだ。フラグ無しだと
error: Uncaught PermissionDenied: Requires read access to <CWD>, run again with the --allow-read flag
var defaultWD = Deno.cwd();
となるので、cwdが欲しいらしい。ビルドツールなのでファイルアクセスできないと何もできないので当然か。
esbuild自体は動いたので次は適当にビルドしてみる。作業ディレクトリ直下に適当にファイルを置く。
import {var} from "./lib"
console.log("Hello!");
console.log(`var is ${var}`);
export var = 5;
で、ビルドスクリプトを書いてみる。公式を参考に https://esbuild.github.io/api/#build-api
import {buildSync} from "https://deno.land/x/esbuild@v0.11.23/mod.js";
buildSync({
entryPoints: ['main.ts'],
outfile: 'out.js',
})
動かす。
$ deno run --allow-read build.ts
Check file:///home/medalhkr/workdir/no-node-frontend/build.ts
error: TS2554 [ERROR]: Expected 0 arguments, but got 1.
buildSync({
^
at file:///home/medalhkr/workdir/no-node-frontend/build.ts:3:11
え?
仕方ないのでimportしているmod.js
を見る。すると1500行目あたりに
var buildSync = () => {
throw new Error(`The "buildSync" API does not work in Deno`);
};
そっかー。じゃあasync版でやるか。
書き換える
import {build} from "https://deno.land/x/esbuild@v0.11.23/mod.js";
await build({
entryPoints: ['main.ts'],
outfile: 'out.js',
});
$ deno run --allow-read build.ts
Check file:///home/medalhkr/workdir/no-node-frontend/build.ts
error: Uncaught PermissionDenied: Requires env access to "ESBUILD_BINARY_PATH", run again with the --allow-env flag
const overridePath = Deno.env.get("ESBUILD_BINARY_PATH");
ん?何やら雲行きがあやしい。
環境変数の名前で検索するとmod.jsの1600行目くらいになんかある。
async function install() {
const overridePath = Deno.env.get("ESBUILD_BINARY_PATH");
if (overridePath)
return overridePath;
const platformKey = Deno.build.target;
const knownWindowsPackages = {
"x86_64-pc-windows-msvc": "esbuild-windows-64"
};
const knownUnixlikePackages = {
"aarch64-apple-darwin": "esbuild-darwin-arm64",
"x86_64-apple-darwin": "esbuild-darwin-64",
"x86_64-unknown-linux-gnu": "esbuild-linux-64"
};
if (platformKey in knownWindowsPackages) {
return await installFromNPM(knownWindowsPackages[platformKey], "esbuild.exe");
} else if (platformKey in knownUnixlikePackages) {
return await installFromNPM(knownUnixlikePackages[platformKey], "bin/esbuild");
} else {
throw new Error(`Unsupported platform: ${platformKey}`);
}
}
バイナリを取ってくるが、環境変数が指定されてればそれを使うみたい。
一旦allowを与えて取ってきてもらおうと思ったら、あれやこれやの間にこうなった。
deno run --allow-read --allow-env --allow-net --allow-write --allow-run build.ts
バイナリを自前で用意すればここまでにはならんのかも?後で試す
やってみたらesbuild自体は動いたが、最初に用意したコードが色々雑すぎてダメだったので直す。
build.ts
import {build} from "https://deno.land/x/esbuild@v0.11.23/mod.js";
await build({
entryPoints: ['main.ts'],
bundle: true,
outfile: 'out.js',
});
Deno.exit();
-
bundle
フラグ入れてなかった。そのままやったらimport文がそのまま残ってた。 - awaitしたらプロセスが終了しなくなった。ので適当に
Deno.exit()
と明示的に入れてみた。本来これでいいのかは全然知らない。
main/lib
export const variable = 5;
import {variable} from "./lib";
console.log("Hello!");
console.log(`variable is ${variable}`);
- 雑に決めた変数名
var
が思いっきり予約語だった。何やってんだ。 -
export
の後のconst
が抜けてた。ほんと何やってんだ...
で、deno run --allow-read --allow-env --allow-net --allow-write --allow-run build.ts
すると
$ cat out.js
(() => {
// lib.ts
var variable = 5;
// main.ts
console.log("Hello!");
console.log(`variable is ${variable}`);
})();
はい。なんかいい感じですね。
そのままdenoで動かします。
$ deno run out.js
Hello!
variable is 5
ok
では次にreactを動かす。パッケージはskypackから適当に取ってくる。https://www.skypack.dev/view/react
想定コードはこんな感じか?
import react from 'https://cdn.skypack.dev/react';
import reactDom from 'https://cdn.skypack.dev/react-dom';
function HelloMessage({ name }) {
return <div>Hello {name}</div>;
}
ReactDOM.render(
<HelloMessage name="Taylor" />,
document.getElementById('container')
);
そしてesbuildの素の状態ではこのimportを扱えないのでpluginを用意する必要がある。
公式からリンクがあるpluginリストより、それっぽいやつを見繕う。 で、こいつもdenoにある。
READMEを参考にしつつ、
import { build } from "https://deno.land/x/esbuild@v0.11.23/mod.js";
import { cache } from 'https://deno.land/x/esbuild_plugin_cache@v0.2.6/mod.ts'
const importmap = {
imports: {},
}
await build({
entryPoints: ['main.tsx'],
bundle: true,
outfile: 'out.js',
plugins: [cache({importmap, directory: './cache'})],
});
Deno.exit();
と用意する。ちなみにREADMEの最初のサンプルではcache
にオプションを何も渡していないが、tsだからなのか明示的に渡さないと型エラーだった。
で、自分で用意したより遥かにシンプルなサンプルコードをpluginのREADMEから拝借して
import React from 'https://cdn.skypack.dev/react@17.0.1'
console.log(React.version)
動かしてみる
$ deno run --allow-read --allow-env --allow-net --allow-write --allow-run build.ts
Check file:///home/medalhkr/workdir/no-node-frontend/build.ts
error: TS2305 [ERROR]: Module '"https://deno.land/x/importmap@0.2.0/mod.ts"' has no exported member 'resolve'.
import { resolve } from 'https://deno.land/x/importmap/mod.ts'
~~~~~~~
at https://deno.land/x/esbuild_plugin_cache@v0.2.6/mod.ts:3:10
はい。
importmapのコードを見る。resolve
は0.2.0
で削除されているもよう。0.1.4
にはあった。
esbuild_plugin_cache
がimportmapのバージョンを固定していないために壊れたらしい。
どうすんだこれ。
仕方ないのでforkする。
そして該当のimportのバージョンを固定する。で、読み込む
import { build } from "https://deno.land/x/esbuild@v0.11.23/mod.js";
import { cache } from "https://raw.githubusercontent.com/cumet04/esbuild-plugin-cache/master/deno/mod.ts";
...
ビルドして実行
$ deno run --allow-read --allow-env --allow-net --allow-write --allow-run build.ts
$ deno run out.js
17.0.1
おけ。
気を取り直してちゃんとwebなものをビルドする。
import React from 'https://cdn.skypack.dev/react@17.0.1';
import ReactDOM from 'https://cdn.skypack.dev/react-dom@17.0.1';
function HelloMessage({ name }) {
return <div>Hello {name}</div>;
}
ReactDOM.render(
<HelloMessage name="Taylor" />,
document.getElementById('root')
);
※スレッド冒頭のは色々と雑すぎたのでちょっと直してる
<html>
<head></head>
<body>
<div id="root"></div>
<script src="./out.js"></script>
</body>
</html>
で、これまでと同様にビルドする。
$ deno run --allow-read --allow-env --allow-net --allow-write --allow-run build.ts
$ tail out.js
}
react_dom_default.render(/* @__PURE__ */ react_default.createElement(HelloMessage, {
name: "Taylor"
}), document.getElementById("root"));
})();
/*
object-assign
(c) Sindre Sorhus
@license MIT
*/
なんかそれっぽいのが出ている。
ここで適当にpython3 -m http.server
とかなんとかしてブラウザで開く。
よさそう。
まともになってきたので、そろそろディレクトリ構成を整理したいところ。なお現在
$ tree
.
├── build.ts
├── index.html
├── main.tsx
0 directories, 4 files
※esbuild-plugin-cache
が作るcache
ディレクトリは消した状態
てかこれだけで動いてるのすごいな。
そしてそろそろリポジトリを生やす。
no-codeに見間違えそうになるが、no-nodeである。node.jsフリー。
整理した。
$ tree
.
├── LICENSE
├── bin
│ ├── build.sh
│ └── build.ts
├── public
│ └── index.html
└── src
└── main.tsx
4 directories, 7 files
build.ts
はentryPointsとoutfileを調整した。成果物はdist
ディレクトリ配下に吐く想定。
またdenoのオプションが多いのでスクリプトを用意。
#!/bin/bash
cd $(readlink -f $0 | xargs dirname)/..
mkdir -p dist
rm -r dist/*
cp -r public/* dist/
deno run --allow-read --allow-env --allow-net --allow-write --allow-run ./bin/build.ts
静的ファイルの配信が極めて雑であるが、本格的にプロジェクトやりたいわけではないので一旦こんなもんで。
せっかくなので簡易的にdev serverを用意する。というか、esbuildに機能があるので使う。
import { serve } from "https://deno.land/x/esbuild@v0.11.23/mod.js";
import { cache } from "https://raw.githubusercontent.com/cumet04/esbuild-plugin-cache/master/deno/mod.ts";
const importmap = {
imports: {},
}
await serve({
servedir: 'dist',
},
{
entryPoints: ['src/main.tsx'],
bundle: true,
outfile: 'dist/out.js',
plugins: [cache({importmap, directory: './.esbuild-plugin-cache'})],
}).then(server => {
console.log(`serve on port ${server.port}`);
});
ビルドオプションはbuildの場合と何も変わっていない。
これをいつものオプションでdeno run
すると、localhostの8000ポート(デフォルト値)で待ち受けるようになる。
こうするとビルドされたものが見え、かつsrcの方を編集・保存し、ブラウザをリロードすると反映される。
...ホットリロード?そんな贅沢なものはありません!
これで立派な開発環境の完成だね!!!
というわけで、node/npmを一切使わずにReactの開発ができ...ないこともない、ということがわかりました。
今後の発展としては
- srcの方の型チェック(esbuildなのでできてない)
- lint, format
- エディタでの補完
あたりですかね。現実的にどこまでがnodeなしでいけるのかは興味深いところ。