Closed21

denoとesbuildでnode無しフロントエンドができるか?

node/npmを一切使わずにフロントエンドの開発環境作れるんじゃないか?と思いついたので試してみる。

なおesbuildは多少環境構築したことあるがdenoは触ったことない。

denoは適当にインストールする。思いつきネタなのでバージョンがどうこうとかは言わない。

https://deno.land/#installation

githubのreleasesでバイナリ取ってくればそれだけで動く。

とりあえず動かす

https://deno.land/#getting-started

適当な作業ディレクトリを掘って、 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を動かす。対象はこれか。

https://deno.land/x/esbuild@v0.11.23

どう見ても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自体は動いたので次は適当にビルドしてみる。作業ディレクトリ直下に適当にファイルを置く。

main.ts
import {var} from "./lib"

console.log("Hello!");
console.log(`var is ${var}`);
lib.ts
export var = 5;

で、ビルドスクリプトを書いてみる。公式を参考に https://esbuild.github.io/api/#build-api

build.ts
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();
  1. bundleフラグ入れてなかった。そのままやったらimport文がそのまま残ってた。
  2. awaitしたらプロセスが終了しなくなった。ので適当にDeno.exit()と明示的に入れてみた。本来これでいいのかは全然知らない。

main/lib

lib.ts
export const variable = 5;
main.ts
import {variable} from "./lib";

console.log("Hello!");
console.log(`variable is ${variable}`);
  1. 雑に決めた変数名varが思いっきり予約語だった。何やってんだ。
  2. 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リストより、それっぽいやつを見繕う。

https://github.com/dalcib/esbuild-plugin-cache
で、こいつもdenoにある。
https://deno.land/x/esbuild_plugin_cache@v0.2.6

READMEを参考にしつつ、

build.ts
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のコードを見る。

https://deno.land/x/importmap@0.2.0
バージョンを紐解くと、どうもresolve0.2.0で削除されているもよう。0.1.4にはあった。

esbuild_plugin_cacheがimportmapのバージョンを固定していないために壊れたらしい。

どうすんだこれ。

仕方ないのでforkする。

https://github.com/cumet04/esbuild-plugin-cache
そして該当のimportのバージョンを固定する。
https://github.com/cumet04/esbuild-plugin-cache/commit/fdb7365e1b4da9405f61a3b3ab21debe85c57782

で、読み込む

build.ts
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なものをビルドする。

main.tsx
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')
);

※スレッド冒頭のは色々と雑すぎたのでちょっと直してる

index.html
<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ディレクトリは消した状態

てかこれだけで動いてるのすごいな。

そしてそろそろリポジトリを生やす。

https://github.com/cumet04/no-node-frontend

no-codeに見間違えそうになるが、no-nodeである。node.jsフリー。

整理した。

https://github.com/cumet04/no-node-frontend/tree/9e16321604a7437d58ce4e227ae5802def1896e9
$ tree
.
├── LICENSE
├── bin
│   ├── build.sh
│   └── build.ts
├── public
│   └── index.html
└── src
    └── main.tsx

4 directories, 7 files

build.tsはentryPointsとoutfileを調整した。成果物はdistディレクトリ配下に吐く想定。

またdenoのオプションが多いのでスクリプトを用意。

bin/build.sh
#!/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に機能があるので使う。

bin/dev.js
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なしでいけるのかは興味深いところ。

このスクラップは4ヶ月前にクローズされました
ログインするとコメントできます