Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

minista v4 開発メモ #121

Open
23 of 34 tasks
qrac opened this issue Dec 29, 2023 · 18 comments
Open
23 of 34 tasks

minista v4 開発メモ #121

qrac opened this issue Dec 29, 2023 · 18 comments

Comments

@qrac
Copy link
Owner

qrac commented Dec 29, 2023

Releases

テーマ:モジュラー化

  • すべての機能をViteプラグインとしてリプレイス
  • 既存のHTMLをJSXで修正するプラグインを追加
  • 静的ページをStorybook風のUIで表示するプラグインを追加

Features

  • 本体の依存関係がほぼなくなり大幅に軽量化した
  • 本体無しでもViteとプラグインだけで動くようになった
  • DenoとBunで動くようになった
  • ViteをpeerDependenciesにすることで更新を効率化した
  • アウトプットにhashが使えるようになった
  • 自作プラグインでHTMLを処理できるようになった
  • Partial Hydrationの出力JSをページ毎に最適化
  • 画像生成にキャッシュを使えるようになった

Breaking Changes

  • minista.config.tsの内容はvite.config.tsになった
  • プラグインを適応しないと各機能を使えなくなった
  • Headなどの型定義はプラグインから参照するように変更
  • コンフィグで設定していたアセットエントリーを廃止
  • archiveで生成するzipを1つに制限
  • Partial Hydrationの動作するReactのバージョンを18以上に変更
  • Partial HydrationのusePreactオプションを削除

Plans

開発ブランチ: v4

  • 実務に最低限のアルファ版を投入して必要になった機能とプラグインを随時リリース
  • 予定している以下のパッケージすべてが完成した時点でv4として公開
  • ドキュメント(v3を配下に内包)とアートワークスの更新

Packages

  • create-minista
  • minista
  • minista-plugin-ssg
  • minista-plugin-enhance
  • minista-plugin-bundle
  • minista-plugin-entry
  • minista-plugin-mdx
  • minista-plugin-hydrate
  • minista-plugin-story
  • minista-plugin-image
  • minista-plugin-svg
  • minista-plugin-sprite
  • minista-plugin-search
  • minista-plugin-delivery
  • minista-plugin-beautify
  • minista-plugin-archive
  • minista-shared-head
  • minista-shared-utils

Tasks

  • serverのHMR・ホットリロード
  • Headのレンダリング処理
  • StoryApp Component
  • StoryAppのNavItem型を提供
  • StoryAppにローディングアニメーション追加
  • bundle buildのHTMLタグ挿入
  • 画像の相対パスに対応
  • entryにbaseを反映
  • Headにclassの型を追加
  • SsgPageのtypeの置き場検討
  • StoryAppのモバイル対応
  • StoryAppの目次自動生成
  • 画像生成のキャッシュ対応
@qrac qrac pinned this issue Dec 29, 2023
@qrac
Copy link
Owner Author

qrac commented Dec 29, 2023

初期メモ

テーマ:軽量化と互換性

  • すべての機能をViteプラグイン化
  • Deno・Bun互換

「ministaの機能はぜんぶViteプラグインになりました!」という感じにしたい。理由は以下の通り。

  • Viteで使える機能がministaで使えないときに改修が大変(ファイルにハッシュ付けるとか)
  • プロトタイプをministaで途中まで作ってViteのSPAに切り替えて納品したりしてる
  • Vite単体もよく使うしぶっちゃけツールを持ち替えるのが面倒
  • RemixもViteプラグインになったらしいからSSG ↔︎ SPA ↔︎ SSRの切り替えが楽になるのでは?

裏でむちゃくちゃやってる部分をViteプラグイン化できるかわからない部分もあるし、密結合な機能を分割できるかもわからないけど、試しに作ってみたら結構上手くいったので進めてみる。

  • minista本体はViteにSSRエクスポート+SSGビルドを指示するだけのラッパーにする
  • 依存関係を減らす
    • Viteは更新が早いのでpeerDependenciesにする
    • react-helmet-asyncは自作できた
    • react-router-domは不要だったので外した
  • 分割したパッケージの命名規則を決める
    • minista-plugin-core
      • import { Head } from "minista-plugin-core/client"
    • vite-plugin-minista-ssg
      • import { Head } from "vite-plugin-minista-ssg/client"
    • パッケージの中にViteプラグインとクライアント向けのJSXなど複数入っているので minista-* で良いのでは?
    • プラグインは minista-plugin-* 共通関数は minista-shared-*
    • minista-plugin-ssg
      • import { pluginSsg } from "minista-plugin-ssg"
      • import { Head } from "minista-plugin-ssg/client"
      • import { getHtmlPath } from "minista-shared-utils"
  • プラグインで共通している関数をどうするか決める
    • monorepo内で他のパッケージを依存させるとバージョンの更新が面倒
      • バージョン更新時に特定の依存も更新できるライブラリを自分で作っていたのを忘れていたのでそれを使う
    • プラグインごとに同じコードを書くのは冗長だけど依存を切り離すためにやむなく
    • tsupでビルド時にバンドルしてしまう
      • tsupだとoutbaseを保持できずesbuildだと特定のパッケージだけのバンドルはできず...
      • tsupのバンドルでuseContextのインスタンスが分離してしまう
      • tsupでもbundle falseしながら依存にない一部パッケージだけバンドルするのは厳しい(noExternal効かず)
  • 処理の途中に他のプラグインが介入できるようにする
  • 基本的なSSGプラグイン
    • CSSバンドルは別プラグインにする
      • Remixは分かれているが... これがないとemitなHTMLのみの時にビルドできない
      • console.log("") のみのスルーファイルを用意する
  • エンハンスに特化したSSGプラグイン
    • 年間の仕事の半分は既存ページの改修なのでHTMLを読み込んでJSXで改修部分だけ書いて差し替えたい
  • Next.jsみたいなexport+buildするプラグイン
    • ルーティング・ハイドレーションJSのページごと分割を実装すればそれっぽい?
    • これを作る場合 *-ssg-with-hydration な感じなので外すことになる基本プラグインを *-core にできない
    • 作れそうだけど実務で使うかわからないので後回し
    • minista-plugin-app

いくつかプラグインを作って命名規則がしっくりきたらブランチに上げる。

v3はsharpがBunで動かなくてコケてたけどリポジトリには改善されたと書いてあったのでもう一度試す。動かなくてもプラグインを分割するので問題を切り離せる。

@qrac
Copy link
Owner Author

qrac commented Dec 29, 2023

エンハンスプラグインの利用シーンイメージ

import html from "./index.html?raw"

function ComponentBox() {
  return (
    <div className="box">
      <p>component text</p>
    </div>
  )
}

export default function (): EnhancePage {
  return {
    html,
    commands: [
      {
        selector: "section#content3 > div",
        component: ComponentBox,
      },
    ],
  }
}

HTML内のルートパスをサーバー上の絶対パスに変更する処理も必要そう。

@qrac
Copy link
Owner Author

qrac commented Jan 7, 2024

エンハンスプラグインはReact componentをHTMLとして挿入できるだけでなく、HTMLの挿入、要素の削除、attributeの修正や正規表現を使った既存の値のreplaceなども行えるようにした。

export default function (): EnhancePage {
  return {
    html,
    commands: [
      {
        selector: "html",
        attr: "lang",
        value: "ja",
      },
      {
        selector: `link[href="/css/style2.css"]`,
        method: "remove",
      },
      {
        selectorAll: "script",
        attr: "src",
        pattern: /^\/(.*?)/,
        value: "https://example.com/$1",
      },
      {
        selector: "section#content1 > div",
        html: `<div class="box"><p>html text</p></div>`,
      },
    ],
  }
}

@qrac
Copy link
Owner Author

qrac commented Jan 7, 2024

minista-plugin-appの予定だったものはminista-plugin-mpaに変更。appだとディレクトリ名がassetsより上に来てしまう。

app
assets
components
enhance
layouts
pages
stories

↓

assets
components
enhance
layouts
mpa
pages
stories

@qrac
Copy link
Owner Author

qrac commented Jan 7, 2024

プラグインを同時に使用する場合は、ページパスの重複を避けるため拡張子を追加して同居させるのが望ましい。

assets
components
layouts
pages
├─ **/*.tsx
├─ **/*.mdx
├─ **/*.mpa.tsx
├─ **/*.enhance.tsx
├─ **/*.stories.tsx
└─ **/*.stories.mdx

@qrac
Copy link
Owner Author

qrac commented Jan 10, 2024

アルファ版をリリースした。実務で検証を開始。

@qrac
Copy link
Owner Author

qrac commented Jan 10, 2024

エンハンス用立ち上げインストール。

npm i -D minista@next minista-plugin-enhance@next vite react react-dom typescript @types/react @types/react-dom

@qrac
Copy link
Owner Author

qrac commented Jan 10, 2024

StoryApp用

npm i react react-dom
npm i -D minista@next vite typescript @types/node @types/react @types/react-dom
npm i -D  minista-plugin-enhance@next minista-plugin-entry@next minista-plugin-mdx@next minista-plugin-story@next minista-plugin-beautify@next minista-plugin-archive@next

@qrac
Copy link
Owner Author

qrac commented Jan 12, 2024

エンハンスプラグインで無茶苦茶編集すると最終的に何ができあがっているのかイメージしづらくなった。編集箇所を決めること、全体処理とコンポーネントの差し込みをメインにして、基本的な作業はコンポーネント側に渡す方が良さそう。全体リニューアルには向かない。

@qrac
Copy link
Owner Author

qrac commented Jan 15, 2024

StoryAppにローディングアニメーションを追加する。
開発中にいちいち出るのが鬱陶しかったので削除していたが再実装予定(かなり弱めにする)。

NetlifyにアップしたプロトタイプはTTFBがかなり遅くて長いHTMLのロードに数秒時間がかかっている。パスの先が無くてエラーを起こしているのかロード中なのか不明でストレス。

すぐにロードできている場合は出さない。2〜3秒かかった場合にローディングを出すようにしてみる。

同時にパスが見つかっていない場合の処理も追加する。現状だとindex.htmlが出てしまう。Appがindex.htmlのときにナビゲーションのパスを間違うとiframeが無限入れ子になってしまう。
開発中の問題。本番では404ページを作っておけば解決。

@qrac
Copy link
Owner Author

qrac commented Jan 18, 2024

minista-plugin-deliveryの機能は最近使ってないから一旦削除でいいかも。storyapp使って納品してる。ただ、実機スマホで見てもらうなどの用途の時にないと困る可能性はある。

@qrac
Copy link
Owner Author

qrac commented Feb 12, 2024

minista-plugin-island

babelのparse, traverse, generateを使ってIslandコンポーネント配下を検出しようとしたがesmでうまく動かず。

traverse is not a function
// defaultを使う形でも動かない?
import traverse from "@babel/traverse"

traverse.default(...)

@qrac
Copy link
Owner Author

qrac commented Jun 29, 2024

#122 の画像生成キャッシュを導入する。

@qrac
Copy link
Owner Author

qrac commented Jul 8, 2024

Partial Hydrationの動作するReactのバージョンを18以上に変更した。

  • バージョン検出の実装コードが長い
  • 検出が確実とは言えない
  • テストしづらい
  • 業務で18未満を使うシーンがなくなった

@qrac
Copy link
Owner Author

qrac commented Jul 8, 2024

Partial HydrationのusePreactオプションを削除した。

  • プラグインの影響範囲を超えて他のJavaScriptのビルドにも影響するため
  • 依存関係を減らしたい

今後は手動でpreact-compatをビルド全体に対して設定することにする。これによりプラグインの影響範囲を超えることを認識する。

@qrac
Copy link
Owner Author

qrac commented Jul 9, 2024

MPAプラグインは使わなそうなので予定から一旦削除。

@qrac
Copy link
Owner Author

qrac commented Jul 14, 2024

vituumを参考にSSGプラグインを新しい方法で作ってみたが、coreのHTMLプラグインの挙動をいじれない部分があり破棄。

このプラグインではHTMLファイルを実際に生成してViteのinputに渡す。既存のemitFileで生成結果を差し込む場合と異なり、srcやhrefでルートパスを書いたアセットを簡単にバンドルできる。

ただし、エントリーのスクリプトに不要な内容が含まれたり、エントリーしたCSSの名前やタグの順番が変わったりと実務に向かなかった。

import type { Plugin, UserConfig } from "vite"
import fs from "node:fs"
import path from "node:path"
import fg from "fast-glob"

import type { SsgPage } from "minista-shared-utils"
import {
  checkDeno,
  getCwd,
  getPluginName,
  getTempName,
  getRootDir,
  getTempDir,
  getHtmlPath,
  getBasedAssetPath,
} from "minista-shared-utils"

import type { ImportedLayouts, ImportedPages } from "../@types/node.js"
import type { PluginOptions } from "./option.js"
import { getGlobExportCode, getSsgExportCode } from "./code.js"
import { formatLayout, resolveLayout } from "./layout.js"
import { formatPages, resolvePages } from "./page.js"
import { transformHtml } from "./html.js"

export function pluginSsgBuild(opts: PluginOptions): Plugin {
  const isDeno = checkDeno()
  const cwd = getCwd(isDeno)
  const names = ["ssg", "build"]
  const pluginName = getPluginName(names)
  const tempName = getTempName(names)
  const regCwd = new RegExp("^" + cwd)

  let isSsr = false
  let base = "/"
  let rootDir = ""
  let tempDir = ""
  let globDir = ""
  let globFile = ""
  let ssrDir = ""
  let ssrFile = ""
  let ssgDir = ""
  let entries: { [key: string]: string } = {}
  let entryChanges: {
    beforeName: string
    afterName: string
  }[] = []

  return {
    name: pluginName,
    enforce: "post",
    apply: "build",
    config: async (config, { command }) => {
      isSsr = config.build?.ssr ? true : false
      base = config.base || base
      rootDir = getRootDir(cwd, config.root || "")
      tempDir = getTempDir(cwd, rootDir)
      globDir = path.join(tempDir, "glob")
      globFile = path.join(globDir, `${tempName}.js`)
      ssrDir = path.join(tempDir, "ssr")
      ssrFile = path.join(ssrDir, `${tempName}.mjs`)
      ssgDir = path.join(tempDir, "ssg")

      if (isSsr) {
        const code = getGlobExportCode(opts)
        await fs.promises.mkdir(globDir, { recursive: true })
        await fs.promises.writeFile(globFile, code, "utf8")

        return {
          build: {
            rollupOptions: {
              input: {
                [tempName]: globFile,
              },
              output: {
                chunkFileNames: "[name].mjs",
                entryFileNames: "[name].mjs",
              },
            },
            outDir: ssrDir,
          },
          ssr: {
            external: ["minista-shared-head"],
          },
        } as UserConfig
      }

      if (!isSsr) {
        const htmlFiles = await fg(path.join(ssgDir, `**/*.html`))
        const regRootDirSlash = new RegExp("^" + rootDir + "[/\\\\]")
        const redSsgDirSlash = new RegExp("^" + ssgDir + "[/\\\\]")

        for (const htmlFile of htmlFiles) {
          const htmlKey = htmlFile
            .replace(regCwd, "")
            .replace(/\//g, "_")
            .replace(/\\/g, "_")
            .replace(/\./g, "_")
          entries[htmlKey] = htmlFile

          const beforeName = htmlFile.replace(regRootDirSlash, "")
          const afterName = htmlFile.replace(redSsgDirSlash, "")

          entryChanges.push({ beforeName, afterName })
        }
        //console.log("entryChanges: ", entryChanges)
        return {
          build: {
            rollupOptions: {
              input: entries,
            },
          },
        }
      }
    },
    generateBundle(options, bundle) {
      if (!isSsr) {
        const diffDir = path.relative(ssgDir, rootDir)
        const upDirCount = diffDir
          .split(/[/\\]/)
          .filter((part) => part === "..").length
        const upDirString = "../".repeat(upDirCount)

        //console.log("generateBundle: ", bundle)
        for (const entryChange of entryChanges) {
          const { beforeName, afterName } = entryChange

          if (bundle[beforeName]) {
            bundle[beforeName].fileName = afterName
          }
        }
        //console.log("generateBundle: ", bundle)
      }
    },
    async writeBundle(options, bundle) {
      if (isSsr) {
        const { LAYOUTS, PAGES } = (await import(ssrFile)) as {
          LAYOUTS: ImportedLayouts
          PAGES: ImportedPages
        }
        const formatedLayout = formatLayout(LAYOUTS)
        const resolvedLayout = await resolveLayout(formatedLayout)
        const formatedPages = formatPages(PAGES, opts)
        const resolvedPages = await resolvePages(formatedPages)
        const ssgPages = resolvedPages.map((resolvedPage) => {
          const fileName = getHtmlPath(resolvedPage.path)
          const html = transformHtml({ resolvedLayout, resolvedPage })
          return { fileName, html }
        })
        await Promise.all(
          ssgPages.map(async (ssgPage) => {
            const filePath = path.join(ssgDir, ssgPage.fileName)
            const fileDir = path.dirname(filePath)
            const code = ssgPage.html
            await fs.promises.mkdir(fileDir, { recursive: true })
            await fs.promises.writeFile(filePath, code, "utf8")
          })
        )
      }
    },
  }
}

@qrac
Copy link
Owner Author

qrac commented Jul 14, 2024

plugin-bundleとplugin-entryのHTML処理が重複するためplugin-bundleに統合する。画像のsrcエントリーにも対応。plugin-ssgに含まれる画像の相対パスに関する修正もplugin-bundleに移動する。

  • テンプレートのルートパスから CSS・JS・画像をエントリー
  • テンプレートでインポートした CSS を結合して出力
  • テンプレートでインポートした画像を出力

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant