
* refactor: move cli to core * feat: move cli to core - use route-middleware in serve - eliminate need for `routes` dir in app * feat: use route-middleware in build * chore: update test gh action
108 lines
2.8 KiB
JavaScript
108 lines
2.8 KiB
JavaScript
import { ELEMENT_NODE, parse, render, walkSync } from 'ultrahtml'
|
|
import { getFiles } from './get-files.mjs'
|
|
|
|
/**
|
|
* @typedef {import('../config/index.js').McFlyConfig} Config
|
|
*/
|
|
|
|
/**
|
|
* Returns transformed HTML with custom elements registry in the head
|
|
* @param {string} html
|
|
* @param {Config['components']} type
|
|
* @param {Storage} storage
|
|
* @returns {Promise<string>}
|
|
*/
|
|
export async function injectCustomElements(html, type, storage) {
|
|
const ast = parse(html)
|
|
const componentFiles = await getFiles(type, storage)
|
|
const availableComponents = componentFiles.map((key) =>
|
|
key.replace(`.${type}`, '')
|
|
)
|
|
|
|
const usedCustomElements = []
|
|
|
|
walkSync(ast, (node) => {
|
|
const usedElement = availableComponents.find((name) => name === node.name)
|
|
|
|
if (node.type === ELEMENT_NODE && !!usedElement) {
|
|
usedCustomElements.push(usedElement)
|
|
}
|
|
})
|
|
|
|
// insert registry script to head
|
|
if (usedCustomElements.length > 0) {
|
|
const registryScript = await buildRegistry(
|
|
usedCustomElements,
|
|
type,
|
|
storage
|
|
)
|
|
walkSync(ast, (node) => {
|
|
if (node.type === ELEMENT_NODE && node.name === 'head') {
|
|
node.children.push(parse(registryScript))
|
|
}
|
|
})
|
|
}
|
|
|
|
return render(ast)
|
|
}
|
|
|
|
/**
|
|
* Builds the string containing all custom elements definition
|
|
* @param {Array<string>} usedCustomElements
|
|
* @param {Config['components']} type
|
|
* @param {Storage} storage
|
|
* @returns {Promise<string>}
|
|
*/
|
|
async function buildRegistry(usedCustomElements, type, storage) {
|
|
let registryScript = `<script type='module'>`
|
|
let isBaseClassImported = false
|
|
let classesImported = []
|
|
|
|
for (const name of usedCustomElements) {
|
|
const content = await storage.getItem(`assets:components:${name}.${type}`)
|
|
if (!content) continue
|
|
const evalStore = eval(
|
|
`class WebComponent {}; class HTMLElement {}; (${content.toString()})`
|
|
)
|
|
|
|
if (isConstructor(evalStore)) {
|
|
const className = new evalStore().constructor.name
|
|
|
|
if (!classesImported.includes(className)) {
|
|
if (
|
|
!isBaseClassImported &&
|
|
content.toString().includes('extends WebComponent')
|
|
) {
|
|
const baseClassImport = `import { WebComponent, html, attachEffect } from "https://unpkg.com/web-component-base@2.0.6/index.js";`
|
|
registryScript += baseClassImport
|
|
isBaseClassImported = true
|
|
}
|
|
|
|
registryScript += content
|
|
|
|
registryScript += `customElements.define("${name}", ${className});`
|
|
classesImported.push(className)
|
|
}
|
|
}
|
|
}
|
|
|
|
registryScript += '</script>'
|
|
|
|
return registryScript
|
|
}
|
|
|
|
/**
|
|
* Check if function is a constructor
|
|
* @param {function} f
|
|
* @returns boolean
|
|
*/
|
|
function isConstructor(f) {
|
|
try {
|
|
new f()
|
|
// eslint-disable-next-line no-unused-vars
|
|
} catch (err) {
|
|
// TODO: verify err is the expected error and then
|
|
return false
|
|
}
|
|
return true
|
|
}
|