
* 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
162 lines
3.8 KiB
JavaScript
162 lines
3.8 KiB
JavaScript
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'
|
|
)
|
|
}
|