feat: integration hooks system (#54)

* refactor: modularize event-handler transform functions

* feat: add plugin hooks from mcfly config
This commit is contained in:
Ayo Ayco 2025-01-08 20:03:19 +01:00 committed by GitHub
parent 4c69aa6cc0
commit f08ce4c043
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 458 additions and 403 deletions

View file

@ -29,6 +29,7 @@ async function _build(args) {
...(mcflyConfig.nitro ?? {}),
...(nitroConfig ?? {}),
})
nitro.options.runtimeConfig.mcfly = mcflyConfig
await prepare(nitro)
await copyPublicAssets(nitro)
await prerender(nitro)

View file

@ -46,6 +46,7 @@ async function serve(args) {
*/
let nitro
const reload = async () => {
// close existing nitro
if (nitro) {
consola.info('Restarting dev server...')
if ('unwatch' in nitro.options._c12) {
@ -53,6 +54,8 @@ async function serve(args) {
}
await nitro.close()
}
// create new nitro
nitro = await createNitro(
{
extends: '@mcflyjs/config',
@ -87,6 +90,7 @@ async function serve(args) {
}
)
nitro.hooks.hookOnce('restart', reload)
nitro.options.runtimeConfig.mcfly = mcflyConfig
const server = createDevServer(nitro)
// const listenOptions = parseArgs(args)
await server.listen(1234)

View file

@ -0,0 +1,3 @@
export default {
components: 'js',
}

View file

@ -0,0 +1,162 @@
import { ELEMENT_NODE, parse, renderSync, walkSync } from 'ultrahtml'
import { parseScript } from 'esprima'
import { consola } from 'consola'
/**
* @typedef {import('estree').BaseNode} JsNode
*/
/**
* McFly HTML Template parser
* @param {*} _html
* @returns {string}
*/
export function evaluateServerScripts(_html) {
let html = evaluateServerScript(_html)
html = deleteServerScripts(html)
return html
}
/**
* Evaluates server:setup script and replaces all variables used in the HTML
* @param {string} html
* @returns {string}
*/
function evaluateServerScript(html) {
const ast = parse(html)
const serverScripts = []
walkSync(ast, (node) => {
const { attributes } = node
const attributeKeys = Object.keys(attributes ?? {})
const isServerScript = attributeKeys.some((key) => key.includes('server:'))
if (
node.type === ELEMENT_NODE &&
node.name === 'script' &&
isServerScript
) {
const scripts = node.children.map((child) => child.value)
const script = cleanScript(scripts)
serverScripts.push(script)
}
})
const setupMap = {}
serverScripts.forEach((script) => {
const { body } = parseScript(script)
const keys = body
.filter((n) => n.type === 'VariableDeclaration')
.map((n) => n['declarations'][0].id.name)
const constructor = `(function(){}.constructor)(\`${script}; return {${keys.join(
','
)}}\`);`
const evalStore = eval(constructor)
Object.assign(setupMap, new evalStore())
})
const regex = /\{\{(.*?)\}\}/g
let match
while ((match = regex.exec(html))) {
let [key, rawValue] = match
const value = rawValue.replace(/\s/g, '')
const keys = value.split('.')
let finalValue = ''
let setupCopy = setupMap
// if not in the server script, it could be a js expression
if (!(keys[0] in setupMap)) {
try {
finalValue = eval(rawValue)
} catch (e) {
consola.error('[ERR]: Failed to evaluate expression', e)
}
}
// nested objects
keys.forEach((key) => {
if (key in setupCopy) {
finalValue = setupCopy[key]
setupCopy = finalValue
}
})
html = html.replace(key, finalValue ?? '')
regex.lastIndex = -1
}
return html
}
/**
* Removes any instance of server:setup script in the HTML
* @param {string} html
* @returns {string}
*/
function deleteServerScripts(html) {
const ast = parse(html)
walkSync(ast, (node) => {
const { attributes } = node
const attributeKeys = Object.keys(attributes ?? {})
const isServerScript = attributeKeys.some((key) => key.includes('server:'))
if (isServerScript && !!node.parent) {
node.parent.children.splice(node.parent.children.indexOf(node), 1)
}
})
return renderSync(ast)
}
/**
* Cleans a JS string for save evaluation
* @param {Array<string>} scripts
* @returns {string}
*/
function cleanScript(scripts) {
let script = scripts.map((s) => s.trim()).join(' ')
script = removeComments(script)
return script //.replace(/\n/g, '').replace(/\s+/g, ' ')
}
/**
* Removes all instances of comments in a JS string
* @param {string} script
* @returns {string}
*/
function removeComments(script) {
const entries = []
parseScript(script, { comment: true }, function (node, meta) {
if (isComment(node)) {
entries.push({
start: meta.start.offset,
end: meta.end.offset,
})
}
})
entries
.sort((a, b) => {
return b.end - a.end
})
.forEach((n) => {
script = script.slice(0, n.start) + script.slice(n.end)
})
return script
}
/**
* Checks if given node of a JS script is a comment
* @param {JsNode} node
* @returns {boolean}
*/
function isComment(node) {
return (
node.type === 'Line' ||
node.type === 'Block' ||
node.type === 'BlockComment' ||
node.type === 'LineComment'
)
}

View file

@ -1,16 +1,17 @@
import { ELEMENT_NODE, parse, render, renderSync, walkSync } from 'ultrahtml'
import { consola } from 'consola'
import { parseScript } from 'esprima'
import { loadConfig } from 'c12'
import { eventHandler } from 'h3'
import { resolve } from 'pathe'
import { createHooks } from 'hookable'
import { hooks as mcflyHooks } from './hooks.js'
import { evaluateServerScripts } from './evaluate-scripts.js'
import { injectHtmlFragments } from './inject-fragments.js'
import { injectCustomElements } from './inject-elements.js'
import defaultMcflyConfig from './default-mcfly-config.js'
import { loadConfig } from 'c12'
/**
* @typedef {import('../config').McFlyConfig} Config
* @typedef {import('unstorage').Storage} Storage
* @typedef {import('unstorage').StorageValue} StorageValue
* @typedef {import('ultrahtml').Node} HtmlNode
* @typedef {import('estree').BaseNode} JsNode
* @typedef {import('h3').EventHandler} EventHandler
*/
@ -22,35 +23,58 @@ import { resolve } from 'pathe'
* @returns {EventHandler}
*/
export function useMcFlyRoute({ storage }) {
const hooks = createHooks()
Object.keys(mcflyHooks).forEach((hookName) => hooks.addHooks(hookName))
return eventHandler(async (event) => {
const { path } = event
const rootDir = resolve('.')
const loadedConfig = await loadConfig({
name: 'mcfly',
configFile: 'mcfly.config',
cwd: rootDir,
})
const config = {
components: 'js', // work around for c12.loadConfig not working on Netlify function
...loadedConfig.config,
let { config } = await loadConfig({ name: 'mcfly' })
if (!config || Object.keys(config).length === 0) {
config = defaultMcflyConfig
consola.warn(`[WARN]: McFly configuration not loaded, using defaults...`)
}
const plugins = config.plugins
plugins.forEach((plugin) => {
const pluginHooks = Object.keys(plugin)
pluginHooks.forEach((pluginHook) => {
hooks.hook(pluginHook, plugin[pluginHook])
})
})
const { components: componentType } = config
let html = await getHtml(path, storage)
consola.info('[INFO]: Config found\n', config)
consola.info('>>> Current Working Directory: ', rootDir)
if (html) {
const transforms = [evaluateServerScript, deleteServerScripts]
for (const transform of transforms) {
html = transform(html.toString())
}
html = await useFragments(html.toString(), storage)
const transforms = [
{
fn: evaluateServerScripts,
args: [''],
hook: mcflyHooks.serverScriptsEvaluated,
},
{
fn: injectHtmlFragments,
args: [storage],
hook: mcflyHooks.fragmentsInjected,
},
{
fn: injectCustomElements,
args: [componentType, storage],
hook: mcflyHooks.customElementsInjected,
},
]
if (!!componentType && !!html) {
html = await insertRegistry(html.toString(), componentType, storage)
for (const transform of transforms) {
html = await transform.fn(html.toString(), ...transform.args)
// call hook
if (transform.hook) {
hooks.callHook(transform.hook)
}
}
} else {
consola.error('[ERR]: Failed to insert registry', {
componentType: !componentType ? 'missing' : 'okay',
@ -59,6 +83,10 @@ export function useMcFlyRoute({ storage }) {
}
}
if (html) {
hooks.callHook(mcflyHooks.pageRendered)
}
return (
html ??
new Response(
@ -72,7 +100,6 @@ export function useMcFlyRoute({ storage }) {
function getPurePath(path) {
return path.split('?')[0]
}
/**
* Gets the correct HTML depending on the path requested
* @param {string} path
@ -101,343 +128,3 @@ async function getHtml(path, storage) {
function getPath(filename) {
return `assets:pages${filename}`
}
/**
* Returns transformed HTML with custom elements registry in the head
* @param {string} html
* @param {Config['components']} type
* @param {Storage} storage
* @returns {Promise<string>}
*/
async function insertRegistry(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
}
/**
* Evaluates server:setup script and replaces all variables used in the HTML
* @param {string} html
* @returns {string}
*/
function evaluateServerScript(html) {
const ast = parse(html)
const serverScripts = []
walkSync(ast, (node) => {
const { attributes } = node
const attributeKeys = Object.keys(attributes ?? {})
const isServerScript = attributeKeys.some((key) => key.includes('server:'))
if (
node.type === ELEMENT_NODE &&
node.name === 'script' &&
isServerScript
) {
const scripts = node.children.map((child) => child.value)
const script = cleanScript(scripts)
serverScripts.push(script)
}
})
const setupMap = {}
serverScripts.forEach((script) => {
const { body } = parseScript(script)
const keys = body
.filter((n) => n.type === 'VariableDeclaration')
.map((n) => n['declarations'][0].id.name)
const constructor = `(function(){}.constructor)(\`${script}; return {${keys.join(
','
)}}\`);`
const evalStore = eval(constructor)
Object.assign(setupMap, new evalStore())
})
const regex = /\{\{(.*?)\}\}/g
let match
while ((match = regex.exec(html))) {
let [key, rawValue] = match
const value = rawValue.replace(/\s/g, '')
const keys = value.split('.')
let finalValue = ''
let setupCopy = setupMap
// if not in the server script, it could be a js expression
if (!(keys[0] in setupMap)) {
try {
finalValue = eval(rawValue)
} catch (e) {
consola.error('[ERR]: Failed to evaluate expression', e)
}
}
// nested objects
keys.forEach((key) => {
if (key in setupCopy) {
finalValue = setupCopy[key]
setupCopy = finalValue
}
})
html = html.replace(key, finalValue ?? '')
regex.lastIndex = -1
}
return html
}
/**
* Removes any instance of server:setup script in the HTML
* @param {string} html
* @returns {string}
*/
function deleteServerScripts(html) {
const ast = parse(html)
walkSync(ast, (node) => {
const { attributes } = node
const attributeKeys = Object.keys(attributes ?? {})
const isServerScript = attributeKeys.some((key) => key.includes('server:'))
if (isServerScript && !!node.parent) {
node.parent.children.splice(node.parent.children.indexOf(node), 1)
}
})
return renderSync(ast)
}
/**
* Cleans a JS string for save evaluation
* @param {Array<string>} scripts
* @returns {string}
*/
function cleanScript(scripts) {
let script = scripts.map((s) => s.trim()).join(' ')
script = removeComments(script)
return script //.replace(/\n/g, '').replace(/\s+/g, ' ')
}
/**
* Checks if given node of a JS script is a comment
* @param {JsNode} node
* @returns {boolean}
*/
function isComment(node) {
return (
node.type === 'Line' ||
node.type === 'Block' ||
node.type === 'BlockComment' ||
node.type === 'LineComment'
)
}
/**
* Removes all instances of comments in a JS string
* @param {string} script
* @returns {string}
*/
function removeComments(script) {
const entries = []
parseScript(script, { comment: true }, function (node, meta) {
if (isComment(node)) {
entries.push({
start: meta.start.offset,
end: meta.end.offset,
})
}
})
entries
.sort((a, b) => {
return b.end - a.end
})
.forEach((n) => {
script = script.slice(0, n.start) + script.slice(n.end)
})
return script
}
/**
* Return HTML with all fragments replaced with the correct content in the storage
* @param {string} html
* @param {Storage} storage
* @returns {Promise<string>}
*/
async function useFragments(html, storage) {
const fragmentFiles = await getFiles('html', storage)
const availableFragments = fragmentFiles.reduce((acc, key) => {
return {
...acc,
[key.replace('.html', '')]: '',
}
}, {})
const ast = parse(html)
for (const key in availableFragments) {
/**
* @type string | null
*/
let text = await storage.getItem('assets:components:' + key + '.html')
if (!text) continue
availableFragments[key] = text.replace(/\n/g, '').replace(/\s+/g, ' ')
}
walkSync(ast, (node) => {
const selector = Object.keys(availableFragments).find(
(name) => name === node.name
)
if (node.type === ELEMENT_NODE && !!selector) {
const index = node.parent.children.indexOf(node)
/**
* @type {HtmlNode}
*/
const fragmentNode = parse(availableFragments[selector])
replaceSlots(fragmentNode, node)
node.parent.children[index] = fragmentNode
}
})
return render(ast)
}
/**
* Replace a slot in a fragmentNode with given node
* @param {HtmlNode} fragmentNode
* @param {HtmlNode} node
* @returns {void}
*/
function replaceSlots(fragmentNode, node) {
let slotted = []
const containsAll = (arr, target) => target.every((v) => arr.includes(v))
walkSync(fragmentNode, (n) => {
if (n.type === ELEMENT_NODE && n.name === 'slot') {
// find node child with same name attribute
const currentSlotName = n.attributes?.['name'] ?? null
let nodeChildren = []
if (currentSlotName === null) {
nodeChildren = node.children.filter(
(child) => !child.attributes?.['slot']
)
} else {
nodeChildren = node.children.filter((child) => {
const childSlotName = child.attributes?.['slot']
return childSlotName === currentSlotName
})
}
if (nodeChildren.length > 0 && !containsAll(slotted, nodeChildren)) {
slotted = [...slotted, ...nodeChildren]
const index = n.parent.children.indexOf(n)
n.parent.children.splice(index, 1, ...nodeChildren)
}
}
})
}
/**
* Get all files from the storage given a type
* @param {string} type
* @param {Storage} storage
* @returns {Promise<string[]>}
*/
async function getFiles(type, storage) {
return (await storage.getKeys('assets:components'))
.map((key) => key.replace('assets:components:', ''))
.filter((key) => key.includes(type))
}

View file

@ -0,0 +1,11 @@
/**
* Get all files from the storage given a type
* @param {string} type
* @param {Storage} storage
* @returns {Promise<string[]>}
*/
export async function getFiles(type, storage) {
return (await storage.getKeys('assets:components'))
.map((key) => key.replace('assets:components:', ''))
.filter((key) => key.includes(type))
}

6
packages/core/hooks.js Normal file
View file

@ -0,0 +1,6 @@
export const hooks = {
pageRendered: 'mcfly:page:rendered',
serverScriptsEvaluated: 'mcfly:scripts:evaluated',
fragmentsInjected: 'mcfly:fragments:injected',
customElementsInjected: 'mcfly:elements:injected',
}

View file

@ -0,0 +1,108 @@
import { ELEMENT_NODE, parse, render, walkSync } from 'ultrahtml'
import { getFiles } from './get-files.js'
/**
* @typedef {import('../config').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
}

View file

@ -0,0 +1,88 @@
import { ELEMENT_NODE, parse, render, walkSync } from 'ultrahtml'
import { getFiles } from './get-files.js'
/**
* @typedef {import('ultrahtml').Node} HtmlNode
*/
/**
* Return HTML with all fragments replaced with the correct content in the storage
* @param {string} html
* @param {Storage} storage
* @returns {Promise<string>}
*/
export async function injectHtmlFragments(html, storage) {
const fragmentFiles = await getFiles('html', storage)
const availableFragments = fragmentFiles.reduce((acc, key) => {
return {
...acc,
[key.replace('.html', '')]: '',
}
}, {})
const ast = parse(html)
for (const key in availableFragments) {
/**
* @type string | null
*/
let text = await storage.getItem('assets:components:' + key + '.html')
if (!text) continue
availableFragments[key] = text.replace(/\n/g, '').replace(/\s+/g, ' ')
}
walkSync(ast, (node) => {
const selector = Object.keys(availableFragments).find(
(name) => name === node.name
)
if (node.type === ELEMENT_NODE && !!selector) {
const index = node.parent.children.indexOf(node)
/**
* @type {HtmlNode}
*/
const fragmentNode = parse(availableFragments[selector])
replaceSlots(fragmentNode, node)
node.parent.children[index] = fragmentNode
}
})
return render(ast)
}
/**
* Replace a slot in a fragmentNode with given node
* @param {HtmlNode} fragmentNode
* @param {HtmlNode} node
* @returns {void}
*/
function replaceSlots(fragmentNode, node) {
let slotted = []
const containsAll = (arr, target) => target.every((v) => arr.includes(v))
walkSync(fragmentNode, (n) => {
if (n.type === ELEMENT_NODE && n.name === 'slot') {
// find node child with same name attribute
const currentSlotName = n.attributes?.['name'] ?? null
let nodeChildren = []
if (currentSlotName === null) {
nodeChildren = node.children.filter(
(child) => !child.attributes?.['slot']
)
} else {
nodeChildren = node.children.filter((child) => {
const childSlotName = child.attributes?.['slot']
return childSlotName === currentSlotName
})
}
if (nodeChildren.length > 0 && !containsAll(slotted, nodeChildren)) {
slotted = [...slotted, ...nodeChildren]
const index = n.parent.children.indexOf(n)
n.parent.children.splice(index, 1, ...nodeChildren)
}
}
})
}

View file

@ -25,12 +25,13 @@
"consola": "^3.3.3",
"esbuild": "^0.24.2",
"esprima": "^4.0.1",
"h3": "^1.8.2",
"nitropack": "~2.10.4",
"h3": "^1.13.0",
"nitropack": "latest",
"pathe": "^1.1.2",
"ultrahtml": "^1.5.2"
"ultrahtml": "^1.5.3"
},
"devDependencies": {
"hookable": "^5.5.3",
"unstorage": "^1.14.4"
}
}

View file

@ -95,18 +95,21 @@ importers:
specifier: ^4.0.1
version: 4.0.1
h3:
specifier: ^1.8.2
specifier: ^1.13.0
version: 1.13.0
nitropack:
specifier: ~2.10.4
specifier: latest
version: 2.10.4(typescript@5.7.2)
pathe:
specifier: ^1.1.2
version: 1.1.2
ultrahtml:
specifier: ^1.5.2
specifier: ^1.5.3
version: 1.5.3
devDependencies:
hookable:
specifier: ^5.5.3
version: 5.5.3
unstorage:
specifier: ^1.14.4
version: 1.14.4(db0@0.2.1)(ioredis@5.4.2)
@ -131,18 +134,15 @@ importers:
'@mcflyjs/core':
specifier: workspace:*
version: link:../packages/core
nitropack:
specifier: ~2.10.4
version: 2.10.4(typescript@5.7.2)
templates/basic:
dependencies:
'@mcflyjs/cli':
specifier: ^0.1.1
version: 0.1.1(magicast@0.3.5)
version: link:../../packages/cli
'@mcflyjs/config':
specifier: ^0.2.1
version: 0.2.1
version: link:../../packages/config
'@mcflyjs/core':
specifier: ^0.6.1
version: 0.6.1(magicast@0.3.5)(typescript@5.7.2)
@ -622,13 +622,6 @@ packages:
engines: {node: '>=18'}
hasBin: true
'@mcflyjs/cli@0.1.1':
resolution: {integrity: sha512-YT0XVwbUO9SscfoCrNY1Tz92SwLrCKbvHYrPUWjC8UAVaaX8tekdfRDF7DiIr8PX5me3YIZUBnQw0/loFNPhvQ==}
hasBin: true
'@mcflyjs/config@0.2.1':
resolution: {integrity: sha512-u+4iKQtId3P8qvopl9p04gX2a/ahSmmrLx/uFp9bvg6UA6Y9tIhPG8l713QH7LVNr4iSIybukeuYbHxYHLiHYQ==}
'@mcflyjs/core@0.6.1':
resolution: {integrity: sha512-ozgpIRpNix7gyuFlB8ofEm+iENMvxQsgyzxMTbIrOM69QYiEV1GsjxWyUqH3QuCwA4UB1KHdyWS+PNaCWSepOQ==}
@ -3139,22 +3132,6 @@ snapshots:
- encoding
- supports-color
'@mcflyjs/cli@0.1.1(magicast@0.3.5)':
dependencies:
c12: 2.0.1(magicast@0.3.5)
citty: 0.1.6
consola: 3.3.3
pathe: 1.1.2
transitivePeerDependencies:
- magicast
'@mcflyjs/config@0.2.1':
dependencies:
h3: 1.13.0
web-component-base: 2.1.2
transitivePeerDependencies:
- uWebSockets.js
'@mcflyjs/core@0.6.1(magicast@0.3.5)(typescript@5.7.2)':
dependencies:
c12: 2.0.1(magicast@0.3.5)

View file

@ -5,6 +5,14 @@ export default defineMcFlyConfig({
server: {
logs: true,
},
plugins: [
{
'mcfly:page:rendered': () => console.log('>>> page rendered'),
'mcfly:scripts:evaluated': () => console.log('>>> scripts evaluated'),
'mcfly:fragments:injected': () => console.log('>>> fragments injected'),
'mcfly:elements:injected': () => console.log('>>> elements injected'),
},
],
nitro: {
devServer: {
watch: ['../packages'],

View file

@ -13,8 +13,7 @@
"dependencies": {
"@mcflyjs/cli": "workspace:*",
"@mcflyjs/config": "workspace:*",
"@mcflyjs/core": "workspace:*",
"nitropack": "~2.10.4"
"@mcflyjs/core": "workspace:*"
},
"version": "0.0.1",
"main": "index.js",