個人用途の自前のdeno-bundleを作る
初手deno initから始めるかsrcディレクトリを切るか迷った
なぜbundleが欲しくなるのか?
あとふつうの人は
- すぐにviteなどに行きbundleしてdeploy
- 何かしらのstaticsite generatorを触る
とかになりそう。
自分がなぜそこには行かないのか?というのを少し考えてみると良いかもしれない。
デプロイしたいのではなく探索したい。そしてその過程を残しておきたい。依存は少なめにしておきたい。みたいな感じだけれどもう少し整理しておきたい。
deno emitはたぶんswcをwasm compileしたようなものでできてる
unstableやでって言われてる 🙄
deno/wasmbuildが使われてる
自分がなぜそこには行かないのか?というのを少し考えてみると良いかもしれない。
この記事とかと感覚は近いのかもしれない
あと、github pagesでのhtmlを出力にした動作確認用という体が強いのもあるかもしれない。
この形にするせいで暗黙にいわゆるdeploy的な行為を除外して考えている。
あとは試行錯誤の結果のまとめみたいな形にしておきたい感じなので不要な依存を膨らませたくはないしbundleすらしたくない…
初手の手触りをどうにか改善して継続可能性を少しでも維持したいみたいな発想はcliのあれと似た感じなのかもしれない。
そしてそれはいわゆるscaffoldには繋がらないのは成果物の完成を期待してるのではなくその行為の継続を期待してるから。
あとは最近ui的なものとしてスマホのためのものを作りたいみたいなことを含んでる。
色々あれから進めて答えが出た
- esbuildのプラグインでとりあえずは良い
- esm.shへtrees-hakingをしようとするとネック
- 可能ならhtmlを出力したい
deno.json周りで気になること
semverをどの範囲にしたら良いかあんまりわかんない
deno/emitは1系以外のstd/pathを要求するみたい。
頑張って小さくなるようにstd/pathの参照を書くべきなんだろうか?(個別のimportだるくない?)
language serverで触ったパッケージもdeno.lockに載るんだなー
(アプリはdeno.lockを使いライブラリはdeno.jsonの範囲でインストールするという感じだとしっくりくるかも?)
そういえばこちらは明確に ツール なのでsemverとか使う必要もない気もする。はじめからバージョン指定にしてしまっても良いかも?
実装のこと
機能
- ファイルを指定して標準出力にbundle結果を出力する
- 複数のファイルをいっきにbundleする
- .tsxの変換をサポートする
- deno.jsonやtsconfig.jsonを渡して設定を読み込んでbundleする
tsxを記述しているときのlanguage serverをまともにしたい
例えば、以下のようなファイルのときにjsxのタグ部分でエラーが出る。
[deno-ts] JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists.
import { h } from "npm:preact";
export function hello(name: string) {
return (
<p>Hello {name}</p>
)
}
まぁそれはそうで、deno.jsonなどにcompilerOptionsが設定されていない。以下のような感じで定義するといける。
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "npm:preact"
},
この説明などを見ると preact
を指定して後にimportをしているすべきかも?
おすすめされてたのは↑のreact-jsxを使うことらしいのだけれど、どうやら上手く言っていないらしく、一昔前の方法の方が良いみたい。
"compilerOptions": {
"jsx": "react",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment",
"jsxImportSource": "npm:preact"
},
あー、なるほど。deno/emitのほうが対応していないのか。
これが必要になるのはどうなんだろう?あとlanguege serverのwarningを無視ししないとだめなのかな。 @jsx h
みたいに記述する方法もあるらしい(これは別の話そう?)
これは使わないimportだからエラーの対象になるのはわかる。ただ抑制するにはどうすれば良いんだろう?
[deno-lint]
h
is never used
If this is intentional, alias it with an underscore likeh as _h
import { h } from "npm:preact";
こう書くのが無難か
// deno-lint-ignore no-unused-vars
import { h } from "npm:preact";
いやなんか /** @jsx h */
系のものを使えば良いらしい?
なるほど。bundle側で以下のような指定をする必要がない。
const result = await bundle(url, {
compilerOptions: {
jsx: "react", // react-jsx, react-jsxdevはエラーになる
jsxFactory: "h",
jsxFragmentFactory: "Fragment",
jsxImportSource: "npm:preact",
},
importMap: {
imports: {
// "preact": "https://esm.sh/preact@10",
},
},
bundle(url)
で十分。
代わりにこんな感じでコメントを書いてあげる必要がある。
/** @jsx h */
/** @jsxFrag Fragment */
import { h, Fragment } from "npm:preact";
export function Hello(name: string) {
return (
<p>Hello {name}</p>
)
}
/** fragment version */
export function Hello2() {
return (
<>
<p>Hello World</p>
</>
);
}
``
ちなみにこんな感で出力される
import { h, Fragment } from "npm:preact";
function Hello(name) {
return h("p", null, "Hello ", name);
}
function Hello2() {
return h(Fragment, null, h("p", null, "Hello World"));
}
export { Hello as Hello };
export { Hello2 as Hello2 };
import先をnpmから変える事もできるんだろうか?型定義はそのままやらせたい。
html側のimport mapを使おうとするのも手ではあるけれど。
あー、bundleだから以下のようにするとpreactの定義も含まれるのかな?
const result = await bundle(url, {
importMap: {
imports: {
"npm:preact": "https://esm.sh/preact@10",
},
},
});
これはnpmにしても同様で
- tsx側の定義で
preact
を参照するようにする - importMapで preact:npm を参照するようにする
とかしてしまうと、どうやら、しっかりbundle時に含めてbundleしてくれる(それはそう)。なのだけど省きたい。
transpileでこういう感じで書くと良いっぽいのだけれど、こちらでもimportMapを指定すると含んでしまうな。 npm:preactで扱いたいのだけれど.
import { transpile } from "@deno/emit";
import { parseArgs, buildUsage } from "@podhmo/with-help";
import { join as pathjoin, dirname, basename } from "jsr:@std/path"
// deno run -A main.ts testdata/hello.ts
async function main(args: string[]) {
const options = parseArgs(args, {
name: "bundle",
usageText: `${buildUsage({ name: "bundle" })} <filename>...`,
description: "bundle typescript file (deno/emit wrapper)",
string: ["dst"],
// string: ["config"], // TODO: loading tsconfig.json for something of jsxFactory option and so on.
} as const);
// TODO: concurrency
for (const filename of options._) {
const url = new URL(filename, import.meta.url);
const resultMap = await transpile(url, {
importMap: {
imports: {
"npm:preact": "https://cdn.skypack.dev/preact",
}
}
});
if (options.dst !== undefined) {
for (const [filename, code] of resultMap.entries()) {
// write to file
const writename = pathjoin(options.dst, basename(filename).replace(/\.tsx?$/, ".js"));
await Deno.mkdir(dirname(writename), { recursive: true });
console.log(`write to ${writename}`);
await Deno.writeTextFile(writename, code);
}
} else {
// write to stdout
for (const [filename, code] of resultMap.entries()) {
console.log(`// ---- ${filename} ----`);
console.log(code);
}
}
}
}
if (import.meta.main) {
await main(Deno.args);
}
importMapがないと
/** @jsx h */ /** @jsxFrag Fragment */ import { h, Fragment } from "npm:preact";
export function Hello(name) {
return /*#__PURE__*/ h("p", null, "Hello ", name);
}
/** fragment version */ export function Hello2() {
return /*#__PURE__*/ h(Fragment, null, /*#__PURE__*/ h("p", null, "Hello World"));
}
transpile()とbundle()の違い
こういうshared componentsというかdenoの普通のパッケージでいうmod.tsのようなものを用意してあげてこちらを見てあげると違いがわかる。
export * from "./hello-component.tsx";
たとえば、transpile()のときは以下のような感じになる。
// ---- file:///home/po/ghq/github.com/podhmo/deno-bundle/testdata/src/shared-components.tsx ----
export * from "./hello-component.tsx";
// ---- file:///home/po/ghq/github.com/podhmo/deno-bundle/testdata/src/hello-component.tsx ----
/** @jsx h */ /** @jsxFrag Fragment */ import { h, Fragment } from "npm:preact";
export function Hello(name) {
return /*#__PURE__*/ h("p", null, "Hello ", name);
}
/** fragment version */ export function Hello2() {
return /*#__PURE__*/ h(Fragment, null, /*#__PURE__*/ h("p", null, "Hello World"));
}
そしてbundleのときは以下のようになる
import { h, Fragment } from "npm:preact";
function Hello(name) {
return h("p", null, "Hello ", name);
}
function Hello2() {
return h(Fragment, null, h("p", null, "Hello World"));
}
export { Hello as Hello };
export { Hello2 as Hello2 };
ほしいのはtranspileかもしれない。。
試行錯誤用のPR
ちなみに npm:preactではなくpreactをimportしてdeno.jsonでimportMapを指定すると以下のようなエラーになる(transpile)
relative pathを期待したところで preact
だとだめっぽい。
$ deno run -A main.ts testdata/src/shared-components.tsx
error: Uncaught (in promise) Error: Relative import path "preact" not prefixed with / or ./ or ../: Relative import path "preact" not prefixed with / or ./ or ../: Relative import path "preact" not prefixed with / or ./ or ../
const ret = new Error(getStringFromWasm0(arg0, arg1));
^
at __wbg_new_28c511d9baebfa89 (https://jsr.io/@deno/emit/0.46.0/emit.generated.js:557:19)
at <anonymous> (wasm://wasm/010a1b62:1:3287273)
at <anonymous> (wasm://wasm/010a1b62:1:247524)
at <anonymous> (wasm://wasm/010a1b62:1:1820413)
at <anonymous> (wasm://wasm/010a1b62:1:2928941)
at __wbg_adapter_46 (https://jsr.io/@deno/emit/0.46.0/emit.generated.js:247:6)
at real (https://jsr.io/@deno/emit/0.46.0/emit.generated.js:231:14)
at ext:core/01_core.js:291:9
at eventLoopTick (ext:core/01_core.js:175:7)