From 15724ef8b9c5bdd3a98a7e8ae6dce5819b4c5795 Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Fri, 22 Apr 2022 21:12:33 -0400 Subject: [PATCH 1/5] example of intercepting a custom element definition --- src/runtime.js | 26 ++++++++++++++++++++++++++ ssr.js | 3 +++ www/components/test.js | 16 ++++++++++++++++ www/pages/index.js | 3 +++ 4 files changed, 48 insertions(+) create mode 100644 src/runtime.js create mode 100644 www/components/test.js diff --git a/src/runtime.js b/src/runtime.js new file mode 100644 index 0000000..f861727 --- /dev/null +++ b/src/runtime.js @@ -0,0 +1,26 @@ +// 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 +// }); + +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/ssr.js b/ssr.js index d5694e8..da66ea6 100644 --- a/ssr.js +++ b/ssr.js @@ -49,6 +49,9 @@ app.get('/*', async (request, reply) => { WCC - SSR + + + ${ eagerJs.map(script => { return ``; diff --git a/www/components/test.js b/www/components/test.js new file mode 100644 index 0000000..41d7a9b --- /dev/null +++ b/www/components/test.js @@ -0,0 +1,16 @@ +class TestComponent extends HTMLElement { + constructor() { + super() + + // TODO have wcc skip over mode / attachShadow, innerHTML, connectedCallback, etc + this.attachShadow({ mode: 'open' }); + } + + static __secret() { + console.debug('sssshhh! this is a secret :)') + } +} + +export { TestComponent } + +customElements.define('wcc-test', TestComponent) \ No newline at end of file diff --git a/www/pages/index.js b/www/pages/index.js index 8de843d..bfac4c5 100644 --- a/www/pages/index.js +++ b/www/pages/index.js @@ -1,6 +1,7 @@ import '../components/counter.js'; import '../components/footer.js'; import '../components/header.js'; +import '../components/test.js'; export default class HomePage extends HTMLElement { constructor() { @@ -29,6 +30,8 @@ export default class HomePage extends HTMLElement { + +

Home Page

From f144430841299f342529a2612e350b66110163a7 Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Wed, 27 Apr 2022 07:41:53 -0400 Subject: [PATCH 2/5] linting --- src/runtime.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/runtime.js b/src/runtime.js index f861727..f00b6d8 100644 --- a/src/runtime.js +++ b/src/runtime.js @@ -12,15 +12,16 @@ // 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) { + if (BaseClass.__secret) { console.debug('hmmmm... wonder what could we do here????'); - BaseClass.__secret(); + BaseClass.__secret(); } backupDefine(tagName, BaseClass); -} \ No newline at end of file +}; \ No newline at end of file From 287168ddeea17bcdbab3381aa04dbd9af05eb3fe Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Wed, 27 Apr 2022 08:50:37 -0400 Subject: [PATCH 3/5] WIP of compiling the hydrate functionality --- src/runtime.js | 4 ++-- src/wcc.js | 33 ++++++++++++++++++++++++++++++++- ssr.js | 19 ++++++++++++++++--- www/components/test.js | 5 ++--- 4 files changed, 52 insertions(+), 9 deletions(-) diff --git a/src/runtime.js b/src/runtime.js index f00b6d8..ab84cc8 100644 --- a/src/runtime.js +++ b/src/runtime.js @@ -18,9 +18,9 @@ const backupDefine = customElements.define.bind(window.customElements); window.customElements.define = (tagName, BaseClass) => { console.debug('intercepted customElement.define', { tagName, BaseClass }); - if (BaseClass.__secret) { + if (BaseClass.__secret) { console.debug('hmmmm... wonder what could we do here????'); - BaseClass.__secret(); + BaseClass.__secret(); } backupDefine(tagName, BaseClass); diff --git a/src/wcc.js b/src/wcc.js index 53c7804..dda553d 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,37 @@ 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 +130,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].replace(/\n/g, '') + } + }; + } + return { html: elementInstance.getInnerHTML({ includeShadowRoots: fragment }), assets: deps diff --git a/ssr.js b/ssr.js index da66ea6..9cb6b2a 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); } @@ -50,7 +53,17 @@ app.get('/*', async (request, reply) => { WCC - SSR - + + + ${ + hydrateJs.map(f => { + return ` + + ` + }) + } ${ eagerJs.map(script => { diff --git a/www/components/test.js b/www/components/test.js index 41d7a9b..9548bef 100644 --- a/www/components/test.js +++ b/www/components/test.js @@ -2,12 +2,11 @@ class TestComponent extends HTMLElement { constructor() { super() - // TODO have wcc skip over mode / attachShadow, innerHTML, connectedCallback, etc this.attachShadow({ mode: 'open' }); } - static __secret() { - console.debug('sssshhh! this is a secret :)') + static __hydrate__() { + console.debug('special __hydrate__ function from TestComponent :)') } } From 740cd3de37a307e286f97032554857426e1febad Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Wed, 27 Apr 2022 20:51:18 -0400 Subject: [PATCH 4/5] execute special hydrate function at runtime --- src/wcc.js | 3 ++- ssr.js | 1 + www/components/test.js | 4 +++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/wcc.js b/src/wcc.js index dda553d..d477a96 100644 --- a/src/wcc.js +++ b/src/wcc.js @@ -65,7 +65,8 @@ async function registerDependencies(moduleURL) { 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__', ''); + + hydrateFunctions[name] = `(${innerFunction.replace('static __hydrate__()', '() => ')})()`; } }) } diff --git a/ssr.js b/ssr.js index 9cb6b2a..9048b71 100644 --- a/ssr.js +++ b/ssr.js @@ -60,6 +60,7 @@ app.get('/*', async (request, reply) => { return ` ` }) diff --git a/www/components/test.js b/www/components/test.js index 9548bef..82ec787 100644 --- a/www/components/test.js +++ b/www/components/test.js @@ -3,10 +3,12 @@ class TestComponent extends HTMLElement { super() this.attachShadow({ mode: 'open' }); + this.shadowRoot.innerHTML = '

This is a test

'; } static __hydrate__() { - console.debug('special __hydrate__ function from TestComponent :)') + console.debug('special __hydrate__ function from TestComponent :)'); + alert('special __hydrate__ function from TestComponent :)'); } } From ae275400e6af2b529087ad4d5e4905705eb82a99 Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Wed, 27 Apr 2022 21:16:59 -0400 Subject: [PATCH 5/5] animated the test component --- src/wcc.js | 2 +- ssr.js | 1 - www/components/test.js | 68 +++++++++++++++++++++++++++++++++++++++--- www/pages/index.js | 4 +-- 4 files changed, 67 insertions(+), 8 deletions(-) diff --git a/src/wcc.js b/src/wcc.js index d477a96..a1c772f 100644 --- a/src/wcc.js +++ b/src/wcc.js @@ -135,7 +135,7 @@ async function renderToString(elementURL, fragment = true) { for(const f in hydrateFunctions) { for(const d in deps) { if(f === deps[d].instanceName) { - deps[d].__hydrate__ = hydrateFunctions[f].replace(/\n/g, '') + deps[d].__hydrate__ = hydrateFunctions[f]; } }; } diff --git a/ssr.js b/ssr.js index 9048b71..6d3e4e0 100644 --- a/ssr.js +++ b/ssr.js @@ -59,7 +59,6 @@ app.get('/*', async (request, reply) => { hydrateJs.map(f => { return ` ` diff --git a/www/components/test.js b/www/components/test.js index 82ec787..acf1391 100644 --- a/www/components/test.js +++ b/www/components/test.js @@ -1,14 +1,74 @@ +const template = document.createElement('template'); + +template.innerHTML = ` + + +
This is a test
+`; + class TestComponent extends HTMLElement { - constructor() { - super() + connectedCallback() { + if (!this.shadowRoot) { + this.attachShadow({ mode: 'open' }); + this.shadowRoot.appendChild(template.content.cloneNode(true)); + } else { + const header = this.shadowRoot.querySelector('h6'); - this.attachShadow({ mode: 'open' }); - this.shadowRoot.innerHTML = '

This is a test

'; + header.style.color = this.getAttribute('color'); + header.classList.add('hydrated'); + } } static __hydrate__() { console.debug('special __hydrate__ function from TestComponent :)'); alert('special __hydrate__ function from TestComponent :)'); + let initialized = false; + + window.addEventListener('load', () => { + let options = { + root: null, + rootMargin: '20px', + threshold: 1.0 + }; + + let callback = (entries, observer) => { + entries.forEach(entry => { + console.debug({ entry }) + if(!initialized && entry.isIntersecting) { + alert('Intersected wcc-test, time to hydrate!!!'); + initialized = true; + document.querySelector('wcc-test').setAttribute('color', 'green'); + import(new URL('./www/components/test.js', import.meta.url)); + } + }); + }; + + let observer = new IntersectionObserver(callback, options); + let target = document.querySelector('wcc-test'); + + observer.observe(target); + }) } } diff --git a/www/pages/index.js b/www/pages/index.js index bfac4c5..608ca43 100644 --- a/www/pages/index.js +++ b/www/pages/index.js @@ -30,14 +30,14 @@ export default class HomePage extends HTMLElement { - -

Home Page

+ + `; }