Skip to content

Instantly share code, notes, and snippets.

@yusukebe
Created April 28, 2024 10:47
Show Gist options
  • Save yusukebe/e2c805f1e33039d96a8f8ef2a61250e8 to your computer and use it in GitHub Desktop.
Save yusukebe/e2c805f1e33039d96a8f8ef2a61250e8 to your computer and use it in GitHub Desktop.
import { Plugin } from 'vite'
import path from 'path'
import { parse } from '@babel/parser'
import _traverse, { NodePath } from '@babel/traverse'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const traverse = (_traverse.default as typeof _traverse) ?? _traverse
import _generate from '@babel/generator'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const generate = (_generate.default as typeof _generate) ?? _generate
import {
jsxAttribute,
jsxClosingElement,
jsxElement,
jsxIdentifier,
jsxOpeningElement,
stringLiteral,
jsxExpressionContainer,
isJSXAttribute
} from '@babel/types'
export const COMPONENT_NAME = 'component-name'
export const COMPONENT_EXPORT_NAME = 'component-export-name'
export const DATA_SERIALIZED_PROPS = 'data-serialized-props'
const files = {}
const islandComponentPlugin = (): Plugin => {
return {
name: 'transform-island-components',
enforce: 'pre',
async resolveId(source, importer) {
if (!importer?.endsWith('.tsx')) {
return null
}
const resolution = await this.resolve(source, importer)
files[`${importer}-${source}`] = resolution.id
},
async transform(code, id) {
if (!id.endsWith('.tsx')) {
return null
}
const ast = parse(code, {
sourceType: 'module',
plugins: ['jsx', 'typescript']
})
const imports = {}
// 非同期で解決が必要なすべてのパスを先に解決します。
const resolutions = await Promise.all(
ast.program.body
.filter((node) => node.type === 'ImportDeclaration')
.map(async (node) => {
const sourcePath = node.source.value
const resolution = await this.resolve(sourcePath, id)
// 解決されたIDをプロジェクトのルートからの相対パスに変換
const relativePath = path.relative(process.cwd(), resolution.id)
return { sourcePath, resolution: `/${relativePath}` }
})
)
// 解決したパス情報を使ってimportsオブジェクトを構築
resolutions.forEach(({ sourcePath, resolution }) => {
files[`${id}-${sourcePath}`] = resolution
})
traverse(ast, {
ImportDeclaration(path) {
const sourcePath = path.node.source.value
path.node.specifiers.forEach((specifier) => {
const localName = specifier.local.name
const importedType = specifier.type === 'ImportDefaultSpecifier' ? 'default' : specifier.imported.name
const key = `${id}-${localName}`
imports[key] = { sourcePath, importedType, componentName: localName }
})
},
JSXElement(path) {
const openingElement = path.node.openingElement
const componentName = openingElement.name.name
const clientAttr = openingElement.attributes.find(
(attr) => isJSXAttribute(attr) && attr.name.name === '$client'
)
if (clientAttr) {
const key = `${id}-${componentName}` // 同じキーを使用してインポート情報を取得
if (imports[key]) {
const { sourcePath, importedType } = imports[key]
const fullpath = files[`${id}-${sourcePath}`]
console.log(`Component: ${componentName} is imported from ${fullpath} as ${importedType}`)
// コンポーネントに渡されている属性を取得してJSONでシリアライズする
// <Badge $client name="Hono" /> なら { name: 'Hono' } になる
const props = {}
openingElement.attributes.forEach((attr) => {
if (!isJSXAttribute(attr) || attr.name.name === '$client') return
const attrName = attr.name.name
const attrValue = attr.value.value || attr.value.expression.value // JSXExpressionContainerを考慮する
props[attrName] = attrValue
})
// honox-islandタグを外側に追加する
// それぞれにはファイルパスと属性が追加される
const islandOpening = jsxOpeningElement(
jsxIdentifier('honox-island'),
[
jsxAttribute(jsxIdentifier(COMPONENT_NAME), stringLiteral(fullpath)),
jsxAttribute(jsxIdentifier(COMPONENT_EXPORT_NAME), stringLiteral(importedType)),
jsxAttribute(
jsxIdentifier(DATA_SERIALIZED_PROPS),
jsxExpressionContainer(stringLiteral(JSON.stringify(props)))
)
],
false
)
const islandClosing = jsxClosingElement(jsxIdentifier('honox-island'))
const islandElement = jsxElement(islandOpening, islandClosing, [path.node], false)
path.replaceWith(islandElement)
path.skip()
}
}
}
})
return {
code: generate(ast).code,
map: null
}
}
}
}
export default islandComponentPlugin
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment