Open38

個人用途の自前のdeno-bundleを作る

podhmopodhmo

https://github.com/podhmo/deno-bundle

目的

podhmopodhmo

初手deno initから始めるかsrcディレクトリを切るか迷った

podhmopodhmo

なぜbundleが欲しくなるのか?

あとふつうの人は

  • すぐにviteなどに行きbundleしてdeploy
  • 何かしらのstaticsite generatorを触る

とかになりそう。

自分がなぜそこには行かないのか?というのを少し考えてみると良いかもしれない。

デプロイしたいのではなく探索したい。そしてその過程を残しておきたい。依存は少なめにしておきたい。みたいな感じだけれどもう少し整理しておきたい。

podhmopodhmo

あと、github pagesでのhtmlを出力にした動作確認用という体が強いのもあるかもしれない。

この形にするせいで暗黙にいわゆるdeploy的な行為を除外して考えている。

あとは試行錯誤の結果のまとめみたいな形にしておきたい感じなので不要な依存を膨らませたくはないしbundleすらしたくない…

podhmopodhmo

初手の手触りをどうにか改善して継続可能性を少しでも維持したいみたいな発想はcliのあれと似た感じなのかもしれない。

そしてそれはいわゆるscaffoldには繋がらないのは成果物の完成を期待してるのではなくその行為の継続を期待してるから。

あとは最近ui的なものとしてスマホのためのものを作りたいみたいなことを含んでる。

podhmopodhmo

色々あれから進めて答えが出た

  • esbuildのプラグインでとりあえずは良い
    • esm.shへtrees-hakingをしようとするとネック
  • 可能ならhtmlを出力したい
podhmopodhmo

deno.json周りで気になること

podhmopodhmo

semverをどの範囲にしたら良いかあんまりわかんない

podhmopodhmo

deno/emitは1系以外のstd/pathを要求するみたい。

頑張って小さくなるようにstd/pathの参照を書くべきなんだろうか?(個別のimportだるくない?)

podhmopodhmo

language serverで触ったパッケージもdeno.lockに載るんだなー

(アプリはdeno.lockを使いライブラリはdeno.jsonの範囲でインストールするという感じだとしっくりくるかも?)

podhmopodhmo

そういえばこちらは明確に ツール なのでsemverとか使う必要もない気もする。はじめからバージョン指定にしてしまっても良いかも?

podhmopodhmo

実装のこと

podhmopodhmo

とりあえずテキトーに作る。あとでPromise.allを使うように書き換えよう

podhmopodhmo

そういえば読み込んだ設定をdumpして終了したい

podhmopodhmo

機能

  • ファイルを指定して標準出力にbundle結果を出力する
  • 複数のファイルをいっきにbundleする
  • .tsxの変換をサポートする
  • deno.jsonやtsconfig.jsonを渡して設定を読み込んでbundleする
podhmopodhmo

一度テキトーに触ってみた結果bundleとtranspileとの差が分かってきた。HMRとかを欲しくはならないしviteに行かない理由はUI側の結果を確認したいフェーズではないからかもしれない。

とはいえ、個別のcomponentの変換と全体がつながったhtmlないしはwebアプリケーションは別物かもしれない。

  • jsxのrender to string
  • 他のライブラリの利用
  • esmとして出力
  • 放置してたcard componentとそれ用のブラウザ拡張
podhmopodhmo

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をしているすべきかも?

https://docs.deno.com/runtime/reference/jsx/#jsx-automatic-runtime-(recommended)

podhmopodhmo

おすすめされてたのは↑のreact-jsxを使うことらしいのだけれど、どうやら上手く言っていないらしく、一昔前の方法の方が良いみたい。

  "compilerOptions": {
    "jsx": "react",
    "jsxFactory": "h",
    "jsxFragmentFactory": "Fragment",
    "jsxImportSource": "npm:preact"
  },
podhmopodhmo

あー、なるほど。deno/emitのほうが対応していないのか。

podhmopodhmo

これが必要になるのはどうなんだろう?あとlanguege serverのwarningを無視ししないとだめなのかな。 @jsx h みたいに記述する方法もあるらしい(これは別の話そう?)

これは使わないimportだからエラーの対象になるのはわかる。ただ抑制するにはどうすれば良いんだろう?

[deno-lint] h is never used
If this is intentional, alias it with an underscore like h as _h

import { h } from "npm:preact";
podhmopodhmo

こう書くのが無難か

// deno-lint-ignore no-unused-vars
import { h } from "npm:preact";
podhmopodhmo

いやなんか /** @jsx h */ 系のものを使えば良いらしい?

podhmopodhmo

なるほど。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>
        </>
    );
}
``
podhmopodhmo

ちなみにこんな感で出力される

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 };
podhmopodhmo

import先をnpmから変える事もできるんだろうか?型定義はそのままやらせたい。
html側のimport mapを使おうとするのも手ではあるけれど。

podhmopodhmo

あー、bundleだから以下のようにするとpreactの定義も含まれるのかな?

const result = await bundle(url, {
      importMap: {
        imports: {
          "npm:preact": "https://esm.sh/preact@10",
        },
      },
    });
podhmopodhmo

これはnpmにしても同様で

  • tsx側の定義で preact を参照するようにする
  • importMapで preact:npm を参照するようにする

とかしてしまうと、どうやら、しっかりbundle時に含めてbundleしてくれる(それはそう)。なのだけど省きたい。

podhmopodhmo

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"));
}
podhmopodhmo

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 };
podhmopodhmo

ちなみに 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)