fresh v1.2がリリースされました。

この記事では主な変更点などについて解説します。

このリリースに合わせて、PreactのメンテナーであるMarvin Hagemeister氏がDeno社に入社されたことが発表されています。これからMarvin Hagemeister氏を中心に、フルタイムでFreshの開発が進められていくことが計画されているようです。

アップデートについて

freshはアップデート用のスクリプト(update.ts)を提供しています。

以下のコマンドを実行すると、v1.2へアップデートすることができます。

$ deno run -A -r https://fresh.deno.dev/update .

また、このバージョン以降からinit.ts (freshのプロジェクト初期化用のスクリプト)で作成したプロジェクトでは、deno task updateでもfreshをアップデートすることができます。

Islandコンポーネントに関する改善

props.childrenのサポート

Islandコンポーネントでprops.childrenがサポートされました。

import type { PageProps } from "$fresh/server.ts";

import Collapse from "../islands/Collapse.tsx";

function Content() {
  return <div>foobar</div>;
}

export default function Index(props: PageProps) {
  return (
    <Collapse>
      <Content />
    </Collapse>
  );
}

またIslandコンポーネントをネストすることもできます。

export default function Index(props: PageProps) {
  return (  
    <Collapse>
      <Collapse>
        <Content />
      </Collapse>
    </Collapse>
  );
}

制限として、children以外のpropsにコンポーネントを渡すことはまだサポートされていません。

propsにシグナル/Uint8Array/循環したオブジェクトを渡せるように

Preact SignalsのシグナルをIslandコンポーネントのpropsとして渡せるようになりました。

import type { PageProps } from "$fresh/server.ts";
import { useSignal } from "@preact/signals";

import Counter from "../islands/Counter.tsx";
import Double from "../islands/Double.tsx";

export default function Index(props: PageProps<string>) {
  const count = useSignal(0);
  return (
    <>
      <Counter count={count} />
      <Double count={count} />
    </>
  );
}

また、循環したオブジェクトやUint8Arrayもサポートされました。

例えば、以下のように循環したオブジェクトを渡すこともできます。

import Example from "../islands/Example.tsx";

export default function Page() {
  const data = { a: 1, b: { c: 2 } };
  data.d = data;
  return (
    <Example data={data} />;
  );
}

islandsのサブディレクトリのサポート

今までは、Islandコンポーネントはislandsディレクトリの直下に配置する必要がありました。(例: islands/Counter.tsx)

この仕様が拡張されて、islandsのサブディレクトリに配置したIslandコンポーネントもfreshによって認識されるようになりました (例: islands/sub_directory/Counter.tsx)

npmパッケージのサポート

Islandコンポーネントでnpm:が利用できるようになりました。

// islands/Example.tsx
import truncate from "npm:lodash.truncate@4.4.2";

interface Props {
  text: string;
}

export default function Example({ text }: Props) {
  return <span>{ truncate(text) }</span>;
}

注意点として、Deno Deployではまだnpm:がサポートされていません。

Deno Deployでのnpm:サポートについては、まもなくサポートされる計画があるようなので、もし気になる場合はDeno DeployのChangelogなどをウォッチしておくとよいかもしれません。

プラグインシステムでrenderAsyncフックがサポート

freshにはプラグインシステムがあります。

これはfreshのライフサイクルにおける様々なタイミングに対してフックを提供することで、freshを拡張できるようにするための仕組みです。

v1.1の時点では、SSRの実行前後のタイミングに対するフック(renderフック)のみがサポートされていました。

このrenderフックを活用することにより、SSRによって生成されたHTMLに基づいてTwindなどのCSSランタイムにスタイルシートを生成させることなどが出来ました。

実際にfreshの公式からこの機能を活用したTwind向けのプラグインが提供されています。

制限として、このrenderフックは同期的に動作することが想定されており、非同期処理を仕込むことができないという課題がありました。

そのため、UnoCSSなどの非同期に動作するCSSエンジンをプラグイン経由でサポートすることができない課題がありました。

この制限を解消するため、renderAsyncという新しいフックが追加されました。

実際にこのrenderAsyncフックを活用してUnoCSSプラグインを実装するPRがfreshのリポジトリに作成されています。

feat(plugins): add unocss plugin (WIP) #1303

これにより、近い将来、fresh公式からUnoCSSプラグインが提供される可能性もありそうです。

deno.jsonサポートの改善

importsによる依存管理がサポート

今まで、freshではimport_map.jsonというファイル(Import Maps)で依存関係を管理していました。

また、Deno v1.30のリリースで、deno.jsonでImport Mapsが定義できるようになりました。

Deno本体でのこの変更に合わせて、freshでもdeno.jsonimportsで定義されたImport Mapsを解釈してくれるようになりました。

今後、init.tsで新しく作成されたプロジェクトについては、このdeno.jsonでの依存管理がデフォルトになります。

また、アップデートスクリプトを使用してアップデートした際も、import_map.jsonからdeno.jsonへ自動で移行されます。

祖先ディレクトリからのdeno.jsonの探索がサポート

今まで、freshではカレントディレクトリ配下にdeno.jsonが存在することが想定されていました。

このリリースから、カレントディレクトリの祖先からもdeno.jsonが探索されるようになりました。

これは、freshのプロジェクトをサブディレクトリに作成しているようなケースで活用されることを想定しているようです。

例えば、以下は./webにfreshのプロジェクトが作成されていますが、このような場合、freshは./deno.jsonを読み込んでくれます。

.
├── deno.json
├── web
│   ├── routes
│   │  ├── index.ts
│   │  └── about.ts
│   └── fresh.gen.ts
└── README.md

HEADメソッドのカスタムハンドラーが定義できるように

freshはSSR実行時の挙動を調整したり、REST APIなどの実装をサポートすることなどを目的として、カスタムハンドラーという機能を提供しています。

このカスタムハンドラーでHEADメソッドを処理できるようになりました。

HEADメソッドがリクエストされた際に、handler.HEADが定義されていればそれが呼ばれます。

もしhandler.HEADが未定義であれば、そのHEADリクエストは代わりにhandler.GETで処理されます。

// routes/api/example.ts
import type { Handlers } from "$fresh/server.ts";

export const handler: Handlers = {
  HEAD() {
    return new Response(null, {
      headers: {
        "Content-Type": "text/plain;charset=UTF-8",
        "Content-Length": "13",
      },
    });
  },
  GET() {
    return new Response("Hello, world!", {
      headers: {
        "Content-Type": "text/plain;charset=UTF-8",
        "Content-Length": "13",
      },
    });
  },
};

createHandler()の追加

新しくcreateHandler()というAPIが追加されました。

主にテストでの使用が想定されているようですが、それ以外の用途でも活用できそうです。

// test.ts
import { assertEquals, assertStringIncludes } from "https://deno.land/std@0.190.0/testing/asserts.ts";

import manifest from "./fresh.gen.ts";
import { createHandler } from "$fresh/server.ts";

import twindv1 from "$fresh/plugins/twindv1.ts";
import twindConfig from "./twind.config.ts";

Deno.test("handlers", async (t) => {
  const handler = await createHandler(manifest, { plugins: [twindv1(twindConfig)] });
  await t.step("GET /", async () => {
    const res = await handler(new Request("http://localhost:8000/"), {
      localAddr: {
        hostname: "127.0.0.1",
        port: 8000,
        transport: "tcp",
      },
      remoteAddr: {
        hostname: "127.0.0.1",
        port: 50000,
        transport: "tcp",
      }
    });
    assertEquals(res.status, 200);
    assertStringIncludes(await res.text(), "Hello world!");
  });
});

ページのレンダリング時のステータスコードやレスポンスヘッダーのカスタマイズ

HandlerContext.renderoptions引数をサポートしました。

以下のオプションがサポートされているようです。

オプション説明
statusHTTPステータスコードを上書きできます。
statusTextHTTPステータスメッセージを上書きできます。
headersHTTPヘッダを上書きできます。

これにより、例えば、ページをレンダリングする際のレスポンスのステータスやヘッダーをカスタマイズできます。

import type { Handlers } from "$fresh/server.ts";

export const handler: Handlers = {
  async GET(req, ctx) {
    return ctx.render(null, {
      // カスタムヘッダーを設定
      headers: { "x-custom-header": "foo" },
    });
  },
};

export default function Index(props: PageProps) {
  ...
}

_app.tsxでパスパラメータなどを参照できるように

アプリケーションラッパーコンポーネント(_app.tsx)のpropsの型であるAppPropsPagePropsの内容も受け取るように拡張されました。

これにより、パスパラメータなどを取り扱うことができるようになりました。

// routes/_app.tsx
export default function App(props: AppProps) {
  const { name } = props.params;
  ...
}

起動ポートに関する改善

PORT環境変数によって起動ポートを変更できるようになりました。

$ PORT=3000 deno task start
  ...

 🍋 Fresh ready
    Local: http://localhost:3000/

それ以外にも、PORT環境変数やportオプションによってポートが指定されなかった際に、8000〜8020までの中から空いているポートが自動で選択されるように改善が行われています。

パフォーマンス改善

TBTTTIの改善のため、Islandコンポーネントのレンダリングがscheduler.postTaskを使用して実行されるように挙動が変更されました。

アプリケーションにおけるプラグインやIslandコンポーネントの数が増えれば増える程、TTIが伸びる問題を解消したいのが背景にあるようです。

もしpostTaskが利用できない場合は、setTimeoutにフォールバックされます。

それ以外にも、Freshの各種エントリポイントやIslandコンポーネントなどがlink[rel=modulepreload]によって事前読み込みされるように改善されました。

その他の改善

SSR時の問題を見つけやすくするため、サーバサイドでもpreact/debugが読み込まれるようになりました。

参考