Open20

Fresh Pluginの勉強

hashrockhashrock

まずはBASIC認証するmiddlewareを書いてみる。

https://github.com/denoland/fresh/discussions/738

ここにあるCustom Handlerの例をmiddlewareに書き換える。

// routes/greet/_middleware.ts
import { MiddlewareHandlerContext } from "$fresh/server.ts";
const USER = "alice"
const PASSWORD = "secret"

export async function handler(
  req: Request,
  ctx: MiddlewareHandlerContext,
) {
  if (req.headers.get("Authorization") !== `Basic ${btoa(`${USER}:${PASSWORD}`)}`) {
    const headers = new Headers({
      "WWW-Authenticate": 'Basic realm="Fake Realm"',
    });
    return new Response("Unauthorized", { status: 401, headers });
  }
  return await ctx.next();
}

これで、greet以下がBASIC認証で保護された。

hashrockhashrock

見様見真似でプラグインにできた。

// plugins/basic.ts
import { MiddlewareHandlerContext, Plugin } from "$fresh/server.ts";

export default function basicAuthPlugin(path: string): Plugin {
  const USER = Deno.env.get("BASIC_AUTH_USER");
  const PASSWORD = Deno.env.get("BASIC_AUTH_PASSWORD");
  const REALM = Deno.env.get("BASIC_AUTH_REALM");
  if (!USER || !PASSWORD) {
    throw new Error("BASIC_AUTH_USER and BASIC_AUTH_PASSWORD must be set");
  }

  async function handler(
    req: Request,
    ctx: MiddlewareHandlerContext,
  ) {
    if (
      req.headers.get("Authorization") !==
        `Basic ${btoa(`${USER}:${PASSWORD}`)}`
    ) {
      const headers = new Headers({
        "WWW-Authenticate": `Basic realm="${REALM || "Fake Realm"}"`,
      });
      return new Response("Unauthorized", { status: 401, headers });
    }
    return await ctx.next();
  }

  return {
    name: "basicAuthPlugin",
    middlewares: [
      {
        middleware: {
          handler,
        },
        path,
      },
    ],
  };
}

呼ぶときはこんな感じ。

// main.ts
import basicAuthPlugin from "./plugins/basic.ts";

await start(manifest, {
  plugins: [
    basicAuthPlugin("/greet/"),
    twindPlugin(twindConfig),
  ],
});
// .env
BASIC_AUTH_USER=alice
BASIC_AUTH_PASSWORD=secret
BASIC_AUTH_REALM=MyRealm
hashrockhashrock
  • middlewareに渡すパスは先頭と末尾にスラッシュ必須っぽい
  • 型の設定ちょっと面倒なのでinitスクリプトあってもいいのかもしれない
hashrockhashrock

無事publishもできたので、こういう感じで使えるようになりましたっと。

import basicAuthPlugin from "https://deno.land/x/hashrock_fresh_plugins@1.0.0/basic.ts";

await start(manifest, {
  plugins: [
    basicAuthPlugin("/greet/"),
    twindPlugin(twindConfig),
  ],
});
hashrockhashrock

次。pluginからrouteを生やす。

とは言ってもカスタムハンドラあんまり詳しくないのでblogのpluginを見ながら色々試行錯誤。

KVビューアを作りたかったが、とりあえずなにか表示するところまで。

// plugns/kv.ts
import {
  Plugin,
} from "$fresh/server.ts";
import Blog from "../routes/blog.tsx";

export default function kvViewerPlugin(): Plugin {
  return {
    name: "kvViewerPlugin",
    routes: [
      {
        path: "/kv",
        component: Blog,
        async handler(_req, ctx) {
          return await ctx.render("Hello");
        },
      },
    ],
  };
}

こんな感じでroutesからRouteコンポーネントを取ってきて、データを渡してRenderできることがわかった。

hashrockhashrock

ここまで来たら、ほとんどできないことはないんじゃないかな…?

islandsのみ、ファイルの実体が islandsディレクトリ下にあることで fresh.gen.tsの生成を行っているはずだから、pluginからは干渉できないはず。

となると、例えばAdminページをpluginから差し込むような場合には、動的なコンポーネントを画面内で使うのは難しいのかもしれない。レガシーなWebアプリみたいに、CRUD生やしてformから登録削除する、みたいなところまでは行けそう。

認証プラグインは普通に作れるのでは。

hashrockhashrock

pluginからislandsを追加することについて。

blog pluginを作ったdeerさんがまずislandsを追加できるIssueとPRを作成。

https://github.com/denoland/fresh/issues/1471

しかし、islands等を個別対応するよりは、プロジェクト全体をPluginとして扱えるようにしよっか、という包括的な提案がなされる。

https://github.com/denoland/fresh/issues/1602

細かい議論にはついていけてないけど、なんか良さそうですね。

とはいえislands以外のことはだいたいできることに変わりはないので、最小限の機能を持つプラグインをもっとぽこぽこ作ってみんなをハッピーにしようみたいなのはどんどんやって良さそう。

hashrockhashrock

さて、最新版でPluginからislandsを作れるようになった(deerさんのPR)

https://github.com/denoland/fresh/pull/1472

これを試してみたい。予想が正しければ、プラグインからインタラクティブなUIを生やすことが可能になる。

hashrockhashrock

まずはfreshをmainから取ってくるようにする。

  "imports": {
    "$fresh/": "https://raw.githubusercontent.com/denoland/fresh/81e71adad60c0fa83bc430677d2910785d0184fc/",
hashrockhashrock

ドキュメントはここ。

https://fresh.deno.dev/docs/canary/concepts/plugins#islands

ここからexampleがリンクされている。

https://github.com/denoland/fresh/blob/main/tests/fixture_plugin/utils/route-plugin.ts#L58

こんな感じでislandsのパスを指定している。

    islands: {
      baseLocation: import.meta.url,
      paths: ["./sample_islands/IslandFromPlugin.tsx"],
    },

baseLocationはなんのために必要なんだろう?
あと違うドメインに置いてたりしても取ってこれるのかな?

hashrockhashrock

そもそもRoute componentもリモートから取ってこれるのか。

import {
  Plugin,
} from "$fresh/server.ts";
// import { Hello } from "../components/Hello.tsx";
import Home from "https://raw.githubusercontent.com/hashrock/fresh-badminton-site/main/routes/member.tsx"

export default function helloPlugin(): Plugin {
  return {
    name: "helloPlugin",
    routes: [
      {
        path: "/hello",
        component: Home,
        async handler(_req, ctx) {
          return await ctx.render("Hello");
        },
      },
    ],
  };
}

あー、動くなこれ。ただ未キャッシュの状態だと起動が遅くなったりするのかもしれん(fetchが挟まりそう)

hashrockhashrock

ただし、import mapsは手元のが使われるので、外部URLからのインポートを前提としたコンポーネントはimportを相対パスで記述する必要がある($fresh/server.tsに関してはローカルのdeno.jsonと定義が一緒なので動いたっぽい)

hashrockhashrock

動いたは動いたなぁ。

// plugins/hello.ts
import {
  Plugin,
} from "$fresh/server.ts";
import { Hello } from "../components/Hello.tsx";

export default function helloPlugin(): Plugin {
  return {
    name: "helloPlugin",
    routes: [
      {
        path: "/hello",
        component: Hello,
        async handler(_req, ctx) {
          return await ctx.render("Hello");
        }
      },
    ],
    islands: {
      baseLocation: "https://raw.githubusercontent.com/hashrock/fresh-components-beta/main/components/Map.tsx",
      paths: [
        "Map.tsx",
      ]
    }
  };
}
// components/Hello.tsx
import { JSX } from "preact";
import Map from "https://raw.githubusercontent.com/hashrock/fresh-components-beta/main/components/Map.tsx"


export function Hello(props: JSX.HTMLAttributes<HTMLDivElement>) {
  return (
    <div {...props}>
      <h1>Hello Component</h1>
      <Map current="" />
    </div>
  );
}

この使い方であってるのか…?

hashrockhashrock

気になる点。

baseLocationは何なんだろう?サンプルだとimport.meta.urlを指定することになってるんだが、pluginは同じフォルダ内にコピーして使うことを想定してるのかな?

https://github.com/denoland/fresh/commit/72edafde4616d1f9100c78f573564d065027fbe9#diff-e111e17a3b884f9d179c2038ed323caf6b160128438076ca07e4a1127b8fc30cR253

ここでdirnameを取っているので、ディレクトリを指定してしまうとその親ディレクトリが取れてしまう(ので、前述の利用方法だとわざわざ Map.tsxまで指定している)

とりあえず外部のroutesやislandsを特別な設定なしに利用できるようになったんだけど推奨する構成がよくわからない。ローカルだけで使うようなツールをinjectするツールなら特に心配なく使っていけるかな