この記事はtraP新歓ブログリレー2022 41日目(4/18)の記事です。
こんにちは、19Bの翠(sappi_red)です。
普段はSysAd班で活動しています。
この記事ではVite-likeなウェブフロントエンドツールをつくっていきながら、Viteの大まかな仕組みを説明していきます。
できる限り細かく手順を書いたので、ぜひ実際に追ってみてください!
実際に完成したものはこのリポジトリ(sapphi-red/micro-vite)にあります。名称は愚直にMicroViteにしました。
ある程度Viteに近い実装にはなっていますが、そこそこいろんな箇所が異なります
また、動作確認はNode.js v16.14.2で行っています。(がおそらくv12以降なら動くと思います)
準備
とりあえず動作確認用のごく単純なウェブアプリケーションを作ります。
動作確認用なのでplayground
というディレクトリを作って、その中にindex.html
、src/main.ts
、src/content.ts
を作成します。tsconfig.json
も用意しておきます。 (c194bbf
)
HTMLから直接.ts
ファイルを読み込んでいること以外は特に変哲もないものになっています。
また、playground
でnpm init
を実行します。すべてEnter連打で大丈夫です。もし、間違えてしまってもあとからpackage.json
を編集すれば問題ありません。(e215ad2
)
さて、MicroViteをつくるディレクトリを用意します。今度はplayground
と同じ階層にmicro-vite
というディレクトリを作ります。その中でnpm init
を実行します。今回はentry point: (index.js)
のときだけdist/index.js
を指定します。ほかはEnter連打で大丈夫です。(c89d11a
)
ここでMicroViteのソースコードをビルドできるように設定します。TypeScriptをビルド+バンドルできれば、何でも問題ないですがここではtsup
を利用します。
micro-vite
でnpm i -D tsup
を行ったのち、package.json
の'scripts'
を以下のように変更します。
"build": "tsup src/index.ts",
"watch": "tsup src/index.ts --watch"
また、tsconfig.json
も用意しておきます。これによってmicro-vite
でnpm run build
を行うことでmicro-vite/src/index.ts
に書いた内容がバンドルされて、micro-vite/dist/index.js
に出力されるようになりました。(88f7e76
)
npm run watch
を実行すると変更があった際に自動でバンドルし直されるので、実行しておくと便利です。
ここ以降ではnpm run build
を省略しますが、micro-vite/src
以下を編集した場合、npm run watch
を実行していなければ、実行することが必要です
最後にplayground
ディレクトリでnpm link -D ../micro-vite
を実行します。これでplayground
でmicro-vite
ディレクトリがパッケージとして利用できるようになります。(f024596
)
コマンドラインからのWebサーバー起動
まずは、コマンドラインから静的配信が行えるWebサーバーを起動できるようにするところまで実装していきます。
CLIの実装 (7e74bc5
)
ここではViteでも利用されているcac
というライブラリを利用します。micro-vite
ディレクトリでnpm i cac
を実行します。
micro-vite/src/index.ts
に実装をしていきます。ここでは、dev
とbuild
と--help
が実行できるようにしました。
import cac from 'cac'
const cli = cac()
cli.command('dev')
.action(() => {
console.log('dev server start')
})
cli.command('build')
.action(() => {
console.log('build start')
})
cli.help()
cli.parse()
ただ、このままではmicro-vite
のように呼び出しても実行されません。そのため、次のような設定が必要です。
package.json
に下を追加する"bin": { "micro-vite": "cli.js" },
micro-vite/cli.js
を作成して次の内容を書き込む#!/usr/bin/env node require('./dist/index.js')
これで、micro-vite
のように呼び出したときにmicro-vite/cli.js
が呼び出されてmicro-vite/src/index.ts
の内容が実行されるようになりました。
今度はplayground/package.json
のscripts
を以下のように変更します。
"scripts": {
"dev": "micro-vite dev",
"build": "micro-vite build"
},
その後、playground
ディレクトリでnpm link -D ../micro-vite
を再度実行します。
これでplayground
ディレクトリでnpm run dev
を実行した際に、以下のような出力が得られるようになったはずです!
$ npm run dev
> playground@1.0.0 dev
> micro-vite dev
dev server start
静的配信サーバーの実装 (0b8772c
)
ここでは、Viteでも利用されているconnect
とsirv
を利用します。利用するライブラリをインストールするためにmicro-vite
ディレクトリでnpm i connect connect-history-api-fallback sirv
とnpm i -D @types/connect @types/connect-history-api-fallback
を実行します。
micro-vite/src/dev.ts
に静的配信サーバーを実装します。
import connect from 'connect'
import historyApiFallback from 'connect-history-api-fallback'
import sirv from 'sirv'
export const startDev = () => {
const server = connect()
server.listen(3000, 'localhost')
server.use(
sirv(undefined, {
dev: true,
etag: true
})
)
server.use(historyApiFallback() as any) // ファイルが存在しなかったときにindex.htmlを返すようにするミドルウェア
console.log('dev server running at http://localhost:3000')
}
micro-vite/src/index.ts
から先ほどのstartDev
関数を呼ぶようにします。
import cac from 'cac'
import { startDev } from './dev' // 追加
const cli = cac()
cli.command('dev')
.action(() => {
startDev() // 書き換え
})
/* 以下略 */
これでplayground
ディレクトリでnpm run dev
を実行すると、localhost:3000
のレスポンスがかえってきます!
しかし、アクセスはできるものの画面は真っ白です。
tsファイルの読み込みエラーの対処 (76a33d7
)
先ほどの画面でブラウザの開発者ツールのコンソールを確認すると下のようなエラーが出ていることに気づきます。
Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of "video/mp2t". Strict MIME type checking is enforced for module scripts per HTML spec.
<script type="module">
を利用する際はMIMEがapplication/javascript
などでない場合に読み込まれないようになっています。また、.ts
の拡張子はTypeScriptだけでなく、MPEG2-TSでも利用されているため、拡張子からMIME Typeを付与する際にvideo/mp2t
が選ばれます。(仮にTypeScriptに専用のMIME Typeが付与されていたとしてもHTMLの仕様で許可されなければ同様に動きません)
そのため、サーバーで.ts
のMIME Typeがapplication/javascript
になるようにする必要があります。
sirv
でTypeScriptを返す際にMIME Typeを指定するようにします。
sirv(undefined, {
dev: true,
etag: true,
setHeaders(res, pathname) { // ここのメソッドを追加します
if (/\.[tj]s$/.test(pathname)) {
res.setHeader('Content-Type', 'application/javascript')
}
}
})
これで先ほどのエラーは解決できました。
オンデマンド・トランスパイル
まだブラウザの画面は真っ白のままですね。再度コンソールを確認するとこのようなエラーが出ています。
Uncaught SyntaxError: Unexpected token '!' (at main.ts:3:59)
このエラーは、単純にTypeScriptをトランスパイルしていないため発生しています。
今のところは静的配信しか行っていないので当然ですね。
さて、ここからViteの肝であるオンデマンド・トランスパイルを実装していきます。
transformMiddleware (0a89822
)
まずは特定の拡張子のファイルの応答を書き換えるミドルウェアの実装をしてみましょう。
micro-vite/src/transformMiddleware.ts
にそれを実装します。
import { NextHandleFunction } from 'connect'
export const transformMiddleware = (): NextHandleFunction => {
const transformRequest = async (pathname: string): Promise<{ mime: string, content: string } | null> => {
if (pathname.endsWith('.ts')) { // 拡張子が.tsだったとき
return {
mime: 'application/javascript',
content: `console.log('file: ${pathname}')`
// 例えば `console.log('file: /src/main.ts')` になる
}
}
return null
}
return async (req, res, next) => {
if (req.method !== 'GET') {
return next()
}
// req.urlのパース
let url: URL
try {
url = new URL(req.url!, 'http://example.com')
} catch (e) {
return next(e)
}
const pathname = url.pathname
try {
const result = await transformRequest(pathname)
if (result) {
res.statusCode = 200
res.setHeader('Content-Type', result.mime)
return res.end(result.content)
}
} catch (e) {
return next(e)
}
// transformRequestで処理されなかったものは次のミドルウェアに託す
// 例えば静的配信をするsirv
next()
}
}
micro-vite/src/dev.ts
から先ほどのtransformMiddleware
関数を呼ぶようにします。
import connect from 'connect'
import historyApiFallback from 'connect-history-api-fallback'
import sirv from 'sirv'
import { transformMiddleware } from './transformMiddleware' // 追加
export const startDev = () => {
const server = connect()
server.listen(3000, 'localhost')
server.use(transformMiddleware()) // 追加
server.use(
sirv(undefined, {
dev: true,
/* 以下略 */
これでplayground
ディレクトリでnpm run dev
を実行しlocalhost:3000/src/main.ts
にアクセスすると、内容がconsole.log('file: /src/main.ts')
に置き換えられていることがわかります。
pluginContainer (64b359f
)
単純な置き換えもできたので、あとはTypeScriptのトランスパイル処理を行うように書き換えると、動くようになります。
しかし、ここではViteに見習ってrollup.jsのプラグインが直接利用できるように実装をしていきます。
rollup.jsのプラグイン
rollupではプラグインがバンドル時に介入できるタイミングを「Build Hooks」と呼びます。
このBuild Hooksは、主にresolveId
、load
、transform
が存在し、これらは順番に呼ばれます。
resolveId
- 主にファイルパスの解決に介入するために利用されます
- 例として、エイリアスに利用できます
- 主にファイルパスの解決に介入するために利用されます
load
- 解決されたIdに対応する内容の取得に介入するために利用されます
- 例として、後述するバーチャルファイルの実現に利用できます
- 解決されたIdに対応する内容の取得に介入するために利用されます
transform
- 取得した内容の変換を行うために利用されます
- 例として、TypeScriptからJavaScriptへの変換に利用できます
- 取得した内容の変換を行うために利用されます
より詳細はrollup.jsのドキュメントのBuild Hooksをご覧ください。
ここではTypeScriptからの変換はrollup-plugin-esbuild
を利用します。micro-vite
ディレクトリでnpm i rollup-plugin-esbuild
とnpm i -D rollup
を実行します。
Viteではesbuildをつかったプラグインが独自で実装されています。
主な登場人物は以下の三つです。
plugin
: rollup.jsでも動作するプラグインpluginContainer
: pluginをrollup.jsと同じように実行するtransformMiddleware
: pluginContainerを利用して、来たリクエストを加工したものをレスポンスとして返す
最初にpluginを実装していきます。
micro-vite/src/plugins.ts
に使うプラグイン一覧を取得できる関数を用意しておきます。
import { resolve } from './resolvePlugin'
import esbuild from 'rollup-plugin-esbuild'
export const getPlugins = () => [
resolve(),
esbuild({
target: 'esnext',
minify: false
})
]
ここで新しく出てきたresolvePlugin
(micro-vite/src/resolvePlugin.ts
)ですがこれはこのように基本的なファイル解決を行うように実装します。
import type { Plugin } from 'rollup'
import * as path from 'node:path'
import * as fs from 'node:fs/promises'
const root = process.cwd()
export const resolve = (): Plugin => {
return {
name: 'micro-vite:resolve',
async resolveId(id: string) {
// idのファイルが存在すれば絶対パスのidを返します
const absolutePath = path.resolve(root, `.${id}`)
try {
const stat = await fs.stat(absolutePath)
if (stat.isFile()) {
return absolutePath
}
} catch {}
return null
},
async load(id: string) {
// パスのファイルを読み出します
try {
const res = await fs.readFile(id, 'utf-8')
return res
} catch {}
return null
}
}
}
そうしたら、今度はこれらのpluginを実行するPluginContainer(micro-vite/src/pluginContainer.ts
)の実装をします。
import type { Plugin, LoadResult, PartialResolvedId, SourceDescription } from 'rollup'
// 本来は他にもhookが存在しますがここではこの三つのみ対応します
export type PluginContainer = {
resolveId(id: string): Promise<PartialResolvedId | null>
load(id: string): Promise<LoadResult | null>
transform(code: string, id: string): Promise<SourceDescription | null>
}
export const createPluginContainer = (plugins: Plugin[]): PluginContainer => {
return {
async resolveId(id) {
for (const plugin of plugins) {
if (plugin.resolveId) {
// @ts-expect-error do not support rollup context
const newId = await plugin.resolveId(id, undefined, undefined)
if (newId) {
id = typeof newId === 'string' ? newId : newId.id
return { id }
}
}
}
return null
},
async load(id) {
for (const plugin of plugins) {
if (plugin.load) {
// @ts-expect-error do not support rollup context
const result = await plugin.load(id)
if (result) {
return result
}
}
}
return null
},
async transform(code, id) {
for (const plugin of plugins) {
if (plugin.transform) {
// @ts-expect-error do not support rollup context
const result = await plugin.transform(code, id)
if (!result) continue
if (typeof result === 'string') {
code = result
} else if (result.code) {
code = result.code
}
}
}
return { code }
}
}
}
micro-vite/src/transformMiddleware.ts
のtransformRequest
でPluginContainer
を利用するように以下のように書き換えます。rollup.jsでpluginが呼ばれるのを模したような実装になっています。
const transformRequest = async (pathname: string): Promise<{ mime?: string, content: string } | null> => {
const idResult = await pluginContainer.resolveId(pathname) || { id: pathname }
const loadResult = await pluginContainer.load(idResult.id)
if (!loadResult) {
return null
}
const code = typeof loadResult === 'string' ? loadResult : loadResult.code
const transformResult = await pluginContainer.transform(code, idResult.id)
if (!transformResult) {
return null
}
return {
mime: /\.[jt]s$/.test(idResult.id) ? 'application/javascript' : undefined,
content: transformResult.code
}
}
transformMiddleware
の返り値の関数も少し書き換える必要があります。
- res.setHeader('Content-Type', result.mime)
+ if (result.mime) {
+ res.setHeader('Content-Type', result.mime)
+ }
あとはmicro-vite/src/dev.ts
でこれらを組み合わせるようにすれば完了です。
/* 前略 */
import { createPluginContainer } from './pluginContainer' // 追加
import { getPlugins } from './plugins' // 追加
import { transformMiddleware } from './transformMiddleware'
export const startDev = () => {
const server = connect()
server.listen(3000, 'localhost')
const plugins = getPlugins() // 追加
const pluginContainer = createPluginContainer(plugins) // 追加
server.use(transformMiddleware(pluginContainer)) // 変更
/* 後略 */
これでplayground
ディレクトリでnpm run dev
を実行して、localhost:3000/src/main.ts
にアクセスするとJavaScriptに変換されていることがわかります。
拡張子の省略への対応 (6f0fb4f
)
先ほどの状態だとlocalhost:3000
にアクセスをしても、/src/content
が読み込めておらず画面が真っ白のままです。
これはsrc/content
ではなくsrc/content.ts
を読みにいくようにする必要があるためです。
micro-vite/src/resolvePlugin.ts
で拡張子がない場合の処理を追加しましょう。
const root = process.cwd()
const extensions = ['', '.ts', '.js'] // 追加
export const resolve = (): Plugin => {
return {
name: 'micro-vite:resolve',
async resolveId(id: string) {
for (const ext of extensions) { // 拡張子をつけてファイルがあるかチェックするように変更
const absolutePath = path.resolve(root, `.${id}${ext}`)
try {
const stat = await fs.stat(absolutePath)
if (stat.isFile()) {
return absolutePath
}
} catch {}
}
return null
},
これでplayground
ディレクトリでnpm run dev
を実行して、localhost:3000
にアクセスすると無事ちゃんとした画面が表示されるようになります!
ファイル変更時のリロード
今度はファイルを変更した際にブラウザが自動でリロードする機能を実装していきます。
Viteでは開発サーバーからWebSocketを通じてメッセージを送信することでこれを実現しています。ここでも同じ方法で実装していきます。
リロード用スクリプトの挿入 (cf8ec08
)
配信するHTMLにscriptタグを挿入してそのscriptタグで指定したURLでリロード用のスクリプトを配信するようにします。
HTMLの編集を行うためにここではnode-html-parser
を使うことにします。micro-vite
ディレクトリでnpm i node-html-parser
を実行します。
先に/
でアクセスした際も/index.html
にアクセスしたときと同じ動作がされるようにmicro-vite/src/resolvePlugin.ts
を以下のように変更しておきます。
// 追加
const fileExists = async (p: string) => {
try {
const stat = await fs.stat(p)
if (stat.isFile()) {
return true
}
} catch {}
return false
}
export const resolve = (): Plugin => {
return {
name: 'micro-vite:resolve',
async resolveId(id: string) {
for (const ext of extensions) {
const absolutePath = path.resolve(root, `.${id}${ext}`)
if (await fileExists(absolutePath)) { // fileExists関数への切り出し
return absolutePath
}
}
// 追加: /で終わっていたら/index.htmlがあるかチェックしてidを変更する
if (id.endsWith('/')) {
const absolutePath = path.resolve(root, `.${id}index.html`)
if (await fileExists(absolutePath)) {
return absolutePath
}
}
return null
},
/* 以下略 */
micro-vite/src/reloadPlugin.ts
にHTMLへのscriptタグの挿入とそのスクリプトの配信をします。ここでは前述したバーチャルファイルという方法をとります。
import type { Plugin } from 'rollup'
import { parse } from 'node-html-parser'
const virtualScriptId = '/@micro-vite:reload/script.js'
const virtualScript = `
console.log('bar')
` // とりあえずbarとコンソールに出力することにする
export const reload = (): Plugin => {
return {
name: 'micro-vite:reload',
async resolveId(id: string) {
// virtualScriptIdのものはそのまま解決できると指定する
if (id === virtualScriptId) return virtualScriptId
return null
},
async load(id: string) {
// virtualScriptIdの内容としてvirtualScriptを返す
if (id === virtualScriptId) {
return virtualScript
}
return null
},
async transform(code, id) {
if (!id.endsWith('.html')) return null
// HTMLのheadタグの末尾にvirtualScriptIdへのリンクのあるscriptタグを挿入する
const doc = parse(code)
doc
.querySelector('head')
?.insertAdjacentHTML('beforeend', `<script type="module" src="${virtualScriptId}">`)
return doc.toString()
}
}
}
そうしたら、micro-vite/src/plugins.ts
にreloadPlugin
を追加します。
import { resolve } from './resolvePlugin'
import { reload } from './reloadPlugin' // 追加
import esbuild from 'rollup-plugin-esbuild'
export const getPlugins = () => [
resolve(),
reload(), // 追加
esbuild({
target: 'esnext',
minify: false
})
]
これでplayground
ディレクトリでnpm run dev
を実行して、localhost:3000
にアクセスすると開発者ツールのコンソールにbar
と出力されるようになりました。
WebSocketでの通信の実装 (4e4bf0d
)
開発サーバーとの通信を実装していきます。
ここではViteでも利用されているws
を利用します。micro-vite
ディレクトリでnpm i ws
とnpm i -D @types/ws
を実行します。
micro-vite/src/reloadPlugin.ts
に送受信処理を実装します。JSONでシリアライズしてメッセージを送る単純な実装です。
import type { Plugin } from 'rollup'
import { parse } from 'node-html-parser'
import WebSocket, { WebSocketServer } from 'ws'; // 追加
const port = 24678
const virtualScriptId = '/@micro-vite:reload/script.js'
const virtualScript = `
const ws = new WebSocket('ws://localhost:${port}/')
ws.addEventListener('message', ({ data }) => {
const msg = JSON.parse(data)
if (msg.type === 'reload') {
location.reload() // リロードというメッセージが来たらページをリロードする
}
})
` // 変更
export const reload = (): Plugin => {
/* 省略 */
}
interface Data { // 追加
type: string
}
export const setupReloadServer = () => { // 追加
const wss = new WebSocketServer({
port,
host: 'localhost'
})
let ws: WebSocket
wss.on('connection', connectedWs => {
ws = connectedWs
})
return {
send(data: Data) {
if (!ws) return
ws.send(JSON.stringify(data))
}
}
}
あとはこれをmicro-vite/src/dev.ts
で利用するようにします。
import sirv from 'sirv'
import { createPluginContainer } from './pluginContainer'
import { getPlugins } from './plugins'
import { setupReloadServer as setupWsServer } from './reloadPlugin' // 追加
import { transformMiddleware } from './transformMiddleware'
export const startDev = () => {
const server = connect()
server.listen(3000, 'localhost')
const ws = setupWsServer() // 追加
/* 中略 */
console.log('dev server running at http://localhost:3000')
setTimeout(() => { // 追加
console.log('reload!')
ws.send({ type: 'reload' })
}, 1000 * 5) // とりあえず5秒後にリロードするようにする
}
これでplayground
ディレクトリでnpm run dev
を実行して、localhost:3000
にアクセスすると、コマンド実行後の5秒後にブラウザがリロードされるようになりました。
ファイルの監視 (98b34dd
)
さて、ファイルの監視を行ってファイルの変更があったときにブラウザのリロードを行うようにしましょう。
ここではViteでも利用されているchokidar
を利用します。micro-vite
ディレクトリでnpm i chokidar
を実行します。
micro-vite/src/fileWatcher.ts
にファイル監視処理を実装します。
import chokidar from 'chokidar'
export const createFileWatcher = (onChange: (eventName: string, path: string) => void) => {
const watcher = chokidar.watch('**/*', {
ignored: ['node_modules', '.git'],
ignoreInitial: true // リッスン開始時にイベントを発火しないようにする
})
watcher.on('all', (eventName, path) => {
onChange(eventName, path) // 何か変更が起きたらonChangeを呼び出す
})
}
そうしたらmicro-vite/src/dev.ts
でそれを利用するようにします。
import connect from 'connect'
import historyApiFallback from 'connect-history-api-fallback'
import sirv from 'sirv'
import { createFileWatcher } from './fileWatcher' // 追加
import { createPluginContainer } from './pluginContainer'
import { getPlugins } from './plugins'
/* 中略 */
console.log('dev server running at http://localhost:3000')
createFileWatcher((eventName, path) => { // setTimeoutから変更
console.log(`Detected file change (${eventName}) reloading!: ${path}`)
ws.send({ type: 'reload' })
})
}
これでファイルを変更した際に自動でブラウザがリロードされるようになりました!
バンドル
開発サーバーは起動するようになったので、今度はバンドルの実装をしていきます。
これに関してはほとんどrollup任せなので、実装量は少ないです。
index.html
の加工 (1108f8b
)
まず、バンドルして生成されたファイルをindex.htmlに挿入する処理を実装します。
micro-vite/src/build.ts
に実装していきます。
import * as path from 'node:path'
import * as fs from 'node:fs/promises'
import parse from 'node-html-parser'
const root = process.cwd()
const dist = path.resolve(root, './dist')
export const startBuild = async () => {
await fs.rm(dist, { recursive: true, force: true }).catch(() => {})
await fs.mkdir(dist)
const indexHtmlPath = path.resolve(root, './index.html')
const distIndexHtmlPath = path.resolve(dist, './index.html')
// index.htmlを加工してdist/index.htmlに出力する
await processHtml(indexHtmlPath, distIndexHtmlPath, async src => {
// スクリプトタグのsrc属性を変更する
return src + '?processed'
})
}
const processHtml = async (
htmlPath: string,
distHtmlPath: string,
bundleEntrypoint: (path: string) => Promise<string>
) => {
const htmlContent = await fs.readFile(htmlPath, 'utf-8')
const doc = parse(htmlContent)
const scriptTag = doc.querySelector('script') // only expect one entrypoint
if (scriptTag) {
const src = scriptTag.getAttribute('src')
if (src) {
const newSrc = await bundleEntrypoint(src)
scriptTag.setAttribute('src', newSrc) // scriptタグのsrc属性を変更する
}
}
await fs.writeFile(distHtmlPath, doc.toString(), 'utf-8')
}
micro-vite/src/index.ts
から先ほどのstartBuild
関数を呼ぶようにします。
import cac from 'cac'
import { startDev } from './dev'
import { startBuild } from './build' // 追加
const cli = cac()
cli.command('dev')
.action(() => {
startDev()
})
cli.command('build')
.action(() => {
startBuild() // 変更
})
cli.help()
cli.parse()
これでplayground
ディレクトリでnpm run build
を実行すると、index.html
のscriptタグのsrc属性に?processed
を追加したファイルがdist/index.html
に出力されます。
バンドルの実行
rollupを型だけでなく実行に利用するので、devDependencies
からdependencies
に移すためにnpm i -P rollup
を実行します。
開発時とバンドル時でプラグインの設定が変えられるようにmicro-vite/src/plugins.ts
のgetPlugins
を以下のように変更します。
export const getPlugins = (isDev: boolean) => [
...(isDev ? [resolve(), reload()] : []),
esbuild({
target: isDev ? 'esnext' : 'es2019',
minify: !isDev
})
]
あわせて、micro-vite/src/dev.ts
のconst plugins = getPlugins()
をconst plugins = getPlugins(true)
に変更します。
さて、micro-vite/src/build.ts
にバンドルの実行を実装していきます。
import * as path from 'node:path'
import * as fs from 'node:fs/promises'
import parse from 'node-html-parser'
import { rollup } from 'rollup' // 追加
import { getPlugins } from './plugins' // 追加
const root = process.cwd()
const dist = path.resolve(root, './dist')
export const startBuild = async () => {
const plugins = getPlugins(false) // 追加
await fs.rm(dist, { recursive: true, force: true }).catch(() => {})
await fs.mkdir(dist)
const indexHtmlPath = path.resolve(root, './index.html')
const distIndexHtmlPath = path.resolve(dist, './index.html')
await processHtml(indexHtmlPath, distIndexHtmlPath, async src => {
// この関数の中を変更
// rollupでバンドルする
const bundle = await rollup({
input: path.resolve(root, `.${src}`),
plugins
})
const { output } = await bundle.write({
dir: dist,
format: 'es',
entryFileNames: 'assets/[name].[hash].js',
chunkFileNames: 'assets/[name].[hash].js'
})
await bundle.close()
return `/${output[0].fileName}` // 出力されたファイル名をsrc属性にセットする
})
}
/* 以下略 */
これでplayground
ディレクトリでnpm run build
を実行すると、index.html
以外にバンドルされたJavaScriptファイルがdist
ディレクトリに出力されます!
バンドル結果でのプレビュー
さてバンドルはできましたが、バンドル結果が正常に動作しているか確認できないので、プレビュー環境を簡単に用意します。
playground
ディレクトリでnpm i -D sirv-cli
を実行します。
package.json
のscripts
に以下のようにpreview
を追加します。
"scripts": {
"dev": "micro-vite dev",
"build": "micro-vite build"
"build": "micro-vite build",
"preview": "sirv dist"
},
これでplayground
ディレクトリでnpm run preview
を実行することでプレビューが表示できるようになりました!
バンドルした結果も正常に動作していそうです。
最後に
だいぶ長くなってしまいましたが、Viteの仕組みがなんとなく伝わりましたでしょうか?
実際につくってみた感想としては、ライブラリを利用しつつ、機能をかなり絞れば意外と少ない行数で作れるんだなぁってなりました。
参考までに最終的な各ファイルの行数と合計の行数は以下の通りです。
$ wc -l $(find src -type f)
50 src/build.ts
39 src/dev.ts
11 src/fileWatcher.ts
19 src/index.ts
52 src/pluginContainer.ts
11 src/plugins.ts
64 src/reloadPlugin.ts
47 src/resolvePlugin.ts
54 src/transformMiddleware.ts
347 total
明日の担当者は@logicaです。お楽しみに!