diff --git a/src/runtime.js b/src/runtime.js new file mode 100644 index 0000000..ab84cc8 --- /dev/null +++ b/src/runtime.js @@ -0,0 +1,27 @@ +// Proxy ? +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy + +// A +// function customDefine(tagName, BaseClass) { +// console.debug('intercepted customElement.define', { tagName, BaseClass }); +// } + +// new Proxy(customElements.__proto__.define, customDefine) + +// new Proxy(customElements.__proto__, { +// define: customDefine +// }); + +/* eslint-disable no-underscore-dangle */ +const backupDefine = customElements.define.bind(window.customElements); + +window.customElements.define = (tagName, BaseClass) => { + console.debug('intercepted customElement.define', { tagName, BaseClass }); + + if (BaseClass.__secret) { + console.debug('hmmmm... wonder what could we do here????'); + BaseClass.__secret(); + } + + backupDefine(tagName, BaseClass); +}; \ No newline at end of file diff --git a/src/wcc.js b/src/wcc.js index 53c7804..a1c772f 100644 --- a/src/wcc.js +++ b/src/wcc.js @@ -7,7 +7,9 @@ import { parseFragment, serialize } from 'parse5'; import fs from 'node:fs/promises'; +// TODO better data structure for deps and hydrate function? const deps = []; +const hydrateFunctions = new Map(); async function renderComponentRoots(tree) { for (const node of tree.childNodes) { @@ -52,17 +54,38 @@ async function registerDependencies(moduleURL) { ecmaVersion: 'latest', sourceType: 'module' }), { + + // walk custom element class for internal methods to expose at runtime + // for supporting hydration and lazy loading strategies + async ClassDeclaration(node) { + if (node.superClass.name === 'HTMLElement') { + const name = node.id.name; + + // find __hydrate__ method + node.body.body.forEach((n) => { + if(n.type === 'MethodDefinition' && n.static && n.key.name === '__hydrate__') { + const innerFunction = moduleContents.slice(n.start, n.end); + + hydrateFunctions[name] = `(${innerFunction.replace('static __hydrate__()', '() => ')})()`; + } + }) + } + }, + + // walk import statements to find other custom element definitions async ImportDeclaration(node) { const dependencyModuleURL = new URL(node.source.value, moduleURL); await registerDependencies(dependencyModuleURL); }, + + // find customElement.define calls and track relevant metadata async ExpressionStatement(node) { const { expression } = node; // TODO don't need to update if it already exists if (expression.type === 'CallExpression' && expression.callee && expression.callee.object - && expression.callee.property && expression.callee.object.name === 'customElements' + && expression.callee.property && expression.callee.object.name === 'customElements' && expression.callee.property.name === 'define') { const tagName = node.expression.arguments[0].value; @@ -108,6 +131,15 @@ async function renderToString(elementURL, fragment = true) { elementInstance.shadowRoot.innerHTML = serialize(finalTree); + // link custom element definitions with their __hydrate__ function + for(const f in hydrateFunctions) { + for(const d in deps) { + if(f === deps[d].instanceName) { + deps[d].__hydrate__ = hydrateFunctions[f]; + } + }; + } + return { html: elementInstance.getInnerHTML({ includeShadowRoots: fragment }), assets: deps diff --git a/ssr.js b/ssr.js index d5694e8..6d3e4e0 100644 --- a/ssr.js +++ b/ssr.js @@ -10,8 +10,8 @@ app.register(fastifyStatic, { prefix: '/www' }); app.register(fastifyStatic, { - root: new URL('./lib', import.meta.url).pathname, - prefix: '/lib', + root: new URL('./src', import.meta.url).pathname, + prefix: '/src', decorateReply: false }); @@ -27,6 +27,7 @@ app.get('/*', async (request, reply) => { const { html, assets } = await renderToString(new URL(entryPoint, import.meta.url), false); const lazyJs = []; const eagerJs = []; + const hydrateJs = []; for (const asset in assets) { const a = assets[asset]; @@ -36,6 +37,8 @@ app.get('/*', async (request, reply) => { if (a.moduleURL.href.endsWith('.js')) { if (a.hydrate === 'lazy') { lazyJs.push(a); + } else if(a.__hydrate__) { + hydrateJs.push(a.__hydrate__); } else { eagerJs.push(a); } @@ -49,6 +52,19 @@ app.get('/*', async (request, reply) => {