Open qrac opened 8 months ago
テーマ:軽量化と互換性
「ministaの機能はぜんぶViteプラグインになりました!」という感じにしたい。理由は以下の通り。
裏でむちゃくちゃやってる部分をViteプラグイン化できるかわからない部分もあるし、密結合な機能を分割できるかもわからないけど、試しに作ってみたら結構上手くいったので進めてみる。
minista-plugin-core
import { Head } from "minista-plugin-core/client"
vite-plugin-minista-ssg
import { Head } from "vite-plugin-minista-ssg/client"
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"
console.log("")
のみのスルーファイルを用意する*-ssg-with-hydration
な感じなので外すことになる基本プラグインを *-core
にできないminista-plugin-app
いくつかプラグインを作って命名規則がしっくりきたらブランチに上げる。
v3はsharpがBunで動かなくてコケてたけどリポジトリには改善されたと書いてあったのでもう一度試す。動かなくてもプラグインを分割するので問題を切り離せる。
エンハンスプラグインの利用シーンイメージ
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内のルートパスをサーバー上の絶対パスに変更する処理も必要そう。
エンハンスプラグインは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>`,
},
],
}
}
minista-plugin-appの予定だったものはminista-plugin-mpaに変更。appだとディレクトリ名がassetsより上に来てしまう。
app
assets
components
enhance
layouts
pages
stories
↓
assets
components
enhance
layouts
mpa
pages
stories
プラグインを同時に使用する場合は、ページパスの重複を避けるため拡張子を追加して同居させるのが望ましい。
assets
components
layouts
pages
├─ **/*.tsx
├─ **/*.mdx
├─ **/*.mpa.tsx
├─ **/*.enhance.tsx
├─ **/*.stories.tsx
└─ **/*.stories.mdx
アルファ版をリリースした。実務で検証を開始。
エンハンス用立ち上げインストール。
npm i -D minista@next minista-plugin-enhance@next vite react react-dom typescript @types/react @types/react-dom
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
エンハンスプラグインで無茶苦茶編集すると最終的に何ができあがっているのかイメージしづらくなった。編集箇所を決めること、全体処理とコンポーネントの差し込みをメインにして、基本的な作業はコンポーネント側に渡す方が良さそう。全体リニューアルには向かない。
StoryAppにローディングアニメーションを追加する。 開発中にいちいち出るのが鬱陶しかったので削除していたが再実装予定(かなり弱めにする)。
NetlifyにアップしたプロトタイプはTTFBがかなり遅くて長いHTMLのロードに数秒時間がかかっている。パスの先が無くてエラーを起こしているのかロード中なのか不明でストレス。
すぐにロードできている場合は出さない。2〜3秒かかった場合にローディングを出すようにしてみる。
同時にパスが見つかっていない場合の処理も追加する。現状だとindex.htmlが出てしまう。Appがindex.htmlのときにナビゲーションのパスを間違うとiframeが無限入れ子になってしまう。
開発中の問題。本番では404ページを作っておけば解決。
minista-plugin-deliveryの機能は最近使ってないから一旦削除でいいかも。storyapp使って納品してる。ただ、実機スマホで見てもらうなどの用途の時にないと困る可能性はある。
minista-plugin-island
babelのparse, traverse, generateを使ってIslandコンポーネント配下を検出しようとしたがesmでうまく動かず。
traverse is not a function
// defaultを使う形でも動かない?
import traverse from "@babel/traverse"
traverse.default(...)
Partial Hydrationの動作するReactのバージョンを18以上に変更した。
Partial HydrationのusePreactオプションを削除した。
今後は手動でpreact-compatをビルド全体に対して設定することにする。これによりプラグインの影響範囲を超えることを認識する。
MPAプラグインは使わなそうなので予定から一旦削除。
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")
})
)
}
},
}
}
plugin-bundleとplugin-entryのHTML処理が重複するためplugin-bundleに統合する。画像のsrcエントリーにも対応。plugin-ssgに含まれる画像の相対パスに関する修正もplugin-bundleに移動する。
v3のSVGスプライトの変換にはスタイルタグのインライン化機能が入っていないので svgo
などを使う。
svg-sprite
に svgo
を使うオプションがあるので、それを利用する。
ついでに、既存のスプライトへの追加も業務で必要だったので実装する。
Image
コンポーネントのloadingなどのデフォルト設定をオプションに作る。lazy
はGoogleの点数は上がるが体感的にはスクロール中に画像が遅れてうんざりする場合あり。
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
型が取得できない。 tsconfig.jsonに"moduleResolution": "Bundler"
書いていないだけだった。
import type { StaticData, PageProps } from "minista-plugin-ssg/client"
Image・PictureのpropsからoutNameを削除する。使わない割に画像生成の仕組みやキャッシュ生成の分岐が複雑になるため。AstroのImageやAstro Imagetoolsにもないので一般的に望まれている機能ではないとも思う。
v3の画像最適化はHTMLのパース・画像生成・HTMLタグの書き換えを同時におこなっていたが、v4でViteプラグインにする場合は工程を分離させる必要がある。
参考にしているvite-plugin-svgrのimport方法が変わっていたので準じて変更予定。ファイルが出力されてしまう問題も解決している?SVGOの最適化どうしていたかも確認する。
// Before
import { ReactComponent as Logo } from "./logo.svg";
// After
import Logo from "./logo.svg?react";
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を引き継げるようにしたので、この内容で公開する。
search, delivery, storyの自動で全体のコンテンツを取得する機能を実装するには、事前に1箇所にSSGした共通フォーマットのファイルを用意しなければならない。
本番ビルドは一方通行なので簡単だが、開発モードの時に少エネで動作させ続けるには?ページレンダリング時に毎回生成するのではなく機能にアクセスした時に生成するとか?機能の補助をssgやenhanceなどに持たせずsearchのみで完結させる?
Releases
テーマ:モジュラー化
Features
Breaking Changes
Plans
開発ブランチ: v4
Packages
Tasks