qrac / minista

Static site generator with 100% static export from React and Vite.
https://minista.qranoko.jp
165 stars 13 forks source link

minista v4 開発メモ #121

Open qrac opened 8 months ago

qrac commented 8 months ago

Releases

テーマ:モジュラー化

Features

Breaking Changes

Plans

開発ブランチ: v4

Packages

Tasks

qrac commented 8 months ago

初期メモ

テーマ:軽量化と互換性

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

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

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

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

qrac commented 8 months ago

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

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 commented 8 months ago

エンハンスプラグインは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 commented 8 months ago

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

app
assets
components
enhance
layouts
pages
stories

↓

assets
components
enhance
layouts
mpa
pages
stories
qrac commented 8 months ago

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

assets
components
layouts
pages
├─ **/*.tsx
├─ **/*.mdx
├─ **/*.mpa.tsx
├─ **/*.enhance.tsx
├─ **/*.stories.tsx
└─ **/*.stories.mdx
qrac commented 8 months ago

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

qrac commented 8 months ago

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

npm i -D minista@next minista-plugin-enhance@next vite react react-dom typescript @types/react @types/react-dom
qrac commented 8 months ago

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 commented 8 months ago

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

qrac commented 8 months ago

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

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

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

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

qrac commented 7 months ago

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

qrac commented 7 months ago

minista-plugin-island

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

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

traverse.default(...)
qrac commented 2 months ago

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

qrac commented 2 months ago

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

qrac commented 2 months ago

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

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

qrac commented 2 months ago

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

qrac commented 2 months ago

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 commented 2 months ago

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

qrac commented 1 month ago

v3のSVGスプライトの変換にはスタイルタグのインライン化機能が入っていないので svgo などを使う。 svg-spritesvgo を使うオプションがあるので、それを利用する。 ついでに、既存のスプライトへの追加も業務で必要だったので実装する。

qrac commented 1 month ago

Image コンポーネントのloadingなどのデフォルト設定をオプションに作る。lazy はGoogleの点数は上がるが体感的にはスクロール中に画像が遅れてうんざりする場合あり。

qrac commented 1 month ago

fetchはalpha-8で問題なく動作することを確認。node 18に含まれているundiciのfetchを自動的にimport不要で使用。

> build
> minista build

vite v5.3.5 building SSR bundle for production...
✓ 3 modules transformed.
node_modules/.minista/ssr/__minista_ssg_build.mjs  1.96 kB
✓ built in 117ms
vite v5.3.5 building for production...
✓ 1 modules transformed.
dist/index.html       0.45 kB │ gzip: 0.36 kB
dist/issues/115.html  0.55 kB │ gzip: 0.65 kB
dist/issues/114.html  0.60 kB │ gzip: 0.50 kB
dist/issues/113.html  0.61 kB │ gzip: 0.53 kB
dist/issues/118.html  1.64 kB │ gzip: 1.78 kB
dist/issues/121.html  1.92 kB │ gzip: 1.53 kB
✓ built in 45ms
qrac commented 1 month ago

型が取得できない。 tsconfig.jsonに"moduleResolution": "Bundler" 書いていないだけだった。

import type { StaticData, PageProps } from "minista-plugin-ssg/client"
qrac commented 1 month ago

Image・PictureのpropsからoutNameを削除する。使わない割に画像生成の仕組みやキャッシュ生成の分岐が複雑になるため。AstroのImageやAstro Imagetoolsにもないので一般的に望まれている機能ではないとも思う。

qrac commented 1 month ago

v3の画像最適化はHTMLのパース・画像生成・HTMLタグの書き換えを同時におこなっていたが、v4でViteプラグインにする場合は工程を分離させる必要がある。

qrac commented 1 month ago

参考にしているvite-plugin-svgrのimport方法が変わっていたので準じて変更予定。ファイルが出力されてしまう問題も解決している?SVGOの最適化どうしていたかも確認する。

// Before
import { ReactComponent as Logo } from "./logo.svg";

// After
import Logo from "./logo.svg?react";
qrac commented 1 month ago

SVGRの実装、importしないでルートパスから裏で読み取ればファイルも出力されないから良いのでは?

import { Svg } from "minista-plugin-svg"

export default function () {
  return <Svg src="/src/assets/logo.svg" />
} 

SVGRはJSXの状態にしないとpropsを受け取れなくなる?

とりあえず、svg系のattrsとtitleタグ、viewBoxを引き継げるようにしたので、この内容で公開する。

qrac commented 1 month ago

search, delivery, storyの自動で全体のコンテンツを取得する機能を実装するには、事前に1箇所にSSGした共通フォーマットのファイルを用意しなければならない。

本番ビルドは一方通行なので簡単だが、開発モードの時に少エネで動作させ続けるには?ページレンダリング時に毎回生成するのではなく機能にアクセスした時に生成するとか?機能の補助をssgやenhanceなどに持たせずsearchのみで完結させる?