Skip to content

Commit

Permalink
chore: refactor all so we can publish as npm module
Browse files Browse the repository at this point in the history
* chore: (clearDOM) replace setTimeout with requestAnimationFrame
* chore: refactor all so we can publish module
* chore: rename to "custom-elements-hmr-polyfill"
* chore: refactor package function
* chore: update sample
* chore: add more scripts to package.json
* chore: update readme
* chore: update package.json - author
  • Loading branch information
vegarringdal authored Aug 26, 2019
1 parent 34db1e1 commit c0d54ae
Show file tree
Hide file tree
Showing 22 changed files with 222 additions and 226 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
node_modules
dist
dist
dev
.cache
.iml
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2019 Vegar Ringdal
Copyright (c) 2019 Vegar Ringdal & Aron Homberg

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# customElements-hmr
try and get fusebox hmr working with "customElements" API
# custom-elements-hmr-polyfill
Custom Element HMR polyfill

How to use
How to start sample:
* `npm install`
* `npm start`
* `npm start`

How to build:
* `npm build`
5 changes: 2 additions & 3 deletions fuse.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ class Context {
target: 'browser',
homeDir: './',
output: `dev`,
entry: `src/index.ts`,
entry: `src/sample/index.ts`,
webIndex: {
template: `src/index.html`
template: `src/sample/index.html`
},
logging: { level: 'verbose' },
dependencies: {
Expand All @@ -22,7 +22,6 @@ class Context {
watch: { ignored: ['dist', 'dev'] },
hmr: true,
devServer: true,

plugins: [
pluginTypeChecker({
basePath: '.',
Expand Down
16 changes: 8 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 12 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
{
"name": "customelements-hmr",
"version": "1.0.0",
"description": "try and get fusebox hmr working with \"customElements\" API",
"name": "custom-elements-hmr-polyfill",
"version": "1.0.0-alpha.1",
"description": "Custom Elements HMR polyfill",
"main": "index.js",
"scripts": {
"start": "node fuse"
"start": "node fuse",
"watch": "node fuse",
"build": "node ./node_modules/typescript/bin/tsc --build ./tsconfig-build.json",
"test": "todo"
},
"repository": {
"type": "git",
"url": "git+https://github.com/vegarringdal/customElements-hmr.git"
"url": "git+https://github.com/vegarringdal/custom-elements-hmr-polyfill.git"
},
"author": "",
"author": "Vegar Ringdal<[email protected]> & Aron Homberg<[email protected] >",
"license": "MIT",
"bugs": {
"url": "https://github.com/vegarringdal/customElements-hmr/issues"
"url": "https://github.com/vegarringdal/custom-elements-hmr-polyfill/issues"
},
"homepage": "https://github.com/vegarringdal/customElements-hmr#readme",
"homepage": "https://github.com/vegarringdal/custom-elements-hmr-polyfill#readme",
"devDependencies": {
"fuse-box": "^4.0.0-alpha.80",
"fuse-box": "^4.0.0-next.88",
"fuse-box-typechecker": "^3.0.0-next.13",
"husky": "^3.0.4",
"lint-staged": "^9.2.3",
Expand Down
8 changes: 0 additions & 8 deletions src/index.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export function DefineCustomElement(
export function defineCustomElement(
elementName: string,
elementDefinitionOptions?: ElementDefinitionOptions
) {
Expand Down
9 changes: 9 additions & 0 deletions src/package/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { initCache } from './polyfill/hmrCache';
import { overrideCustomElementDefine } from './polyfill/overrideCustomElementDefine';
export { clearDOM } from './utils/clearDOM';
export { defineCustomElement } from './decorator/defineCustomElement';

export function applyPolyfill() {
initCache();
overrideCustomElementDefine();
}
40 changes: 40 additions & 0 deletions src/package/polyfill/constructInstance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { getMostRecentImpl } from './hmrCache';

export const BLACKLISTED_PATCH_METHODS = [
'constructor',
'connectedCallback',
'disconnectedCallback',
'adoptedCallback',
'attributeChangedCallback'
];

export function constructInstance(elementName: string, args: any, newTarget: any) {
const mostRecentImpl = getMostRecentImpl(elementName);

// Constructed instance partly points to outdated impl details.
// This patch loop makes sure that the hook methods aren't overridden,
// the constructor stays intact but methods, getters, setters and fields
// are updated according to the most recent implementation:
const customElementInstance = Reflect.construct(mostRecentImpl, args, newTarget);
const ownPropertyNames = Object.getOwnPropertyNames(mostRecentImpl.prototype);

const whitelistedPropertyNames = ownPropertyNames.filter((propertyName: string) => {
return BLACKLISTED_PATCH_METHODS.indexOf(propertyName) === -1;
});

for (let i = 0; i < whitelistedPropertyNames.length; i++) {
const propertyDescriptor = Object.getOwnPropertyDescriptor(
mostRecentImpl.prototype,
whitelistedPropertyNames[i]
);

if (propertyDescriptor) {
Object.defineProperty(
customElementInstance,
whitelistedPropertyNames[i],
propertyDescriptor
);
}
}
return customElementInstance;
}
37 changes: 37 additions & 0 deletions src/package/polyfill/createHookClass.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { getMostRecentImpl } from './hmrCache';

export function createHookClass(elementName: string, originalImpl: any) {
return class extends originalImpl {
static get observedAttributes() {
return super.observedAttributes;
}

connectedCallback() {
const mostRecentImpl = getMostRecentImpl(elementName).prototype;
if (mostRecentImpl.connectedCallback) {
mostRecentImpl.connectedCallback.apply(this, arguments);
}
}

disconnectedCallback() {
const mostRecentImpl = getMostRecentImpl(elementName).prototype;
if (mostRecentImpl.disconnectedCallback) {
mostRecentImpl.disconnectedCallback.apply(this, arguments);
}
}

adoptedCallback() {
const mostRecentImpl = getMostRecentImpl(elementName).prototype;
if (mostRecentImpl.adoptedCallback) {
mostRecentImpl.adoptedCallback.apply(this, arguments);
}
}

attributeChangedCallback() {
const mostRecentImpl = getMostRecentImpl(elementName).prototype;
if (mostRecentImpl.attributeChangedCallback) {
mostRecentImpl.attributeChangedCallback.apply(this, arguments);
}
}
};
}
21 changes: 21 additions & 0 deletions src/package/polyfill/hmrCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export function initCache() {
if (!(<any>globalThis).hmrCache) {
(<any>globalThis).hmrCache = {};
}
}

export function getMostRecentImpl(elementName: string) {
return (<any>globalThis).hmrCache[elementName];
}

export function setMostRecentImpl(elementName: string, impl: any) {
(<any>globalThis).hmrCache[elementName] = impl;
}

export function isCacheInitialized() {
return (<any>globalThis).hmrCache.initialized;
}

export function setCacheAsInitialized() {
(<any>globalThis).hmrCache.initialized = true;
}
30 changes: 30 additions & 0 deletions src/package/polyfill/overrideCustomElementDefine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { setMostRecentImpl, isCacheInitialized, setCacheAsInitialized } from './hmrCache';
import { createHookClass } from './createHookClass';
import { constructInstance } from './constructInstance';

export function overrideCustomElementDefine() {
if (!isCacheInitialized()) {
// make sure the override happens only once
setCacheAsInitialized();

const originalDefineFn = CustomElementRegistry.prototype.define;

CustomElementRegistry.prototype.define = function(
elementName: string,
impl: any,
options: ElementDefinitionOptions
) {
const registeredCustomElement = customElements.get(elementName);

if (!registeredCustomElement) {
const hookClass = new Proxy(createHookClass(elementName, impl), {
construct: function(element, args, newTarget) {
return constructInstance(elementName, args, newTarget);
}
});
originalDefineFn.apply(this, [elementName, hookClass, options]);
}
setMostRecentImpl(elementName, impl);
};
}
}
8 changes: 4 additions & 4 deletions src/helper/ClearDOM.ts → src/package/utils/clearDOM.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
const clearDOM = () => {
export function clearDOM() {
if (document.body) {
setTimeout(() => {
requestAnimationFrame(() => {
// simulate a Virtual DOM re-render
// TODO: How to find out (after HMR) which elements actually changed?
// TODO: The whole optimization not to reload the page is dependent of being able to tell the VDOM
// TODO: Only to re-render those elements that changed?!
const oldBodyHtml = document.body.innerHTML;
document.body.innerHTML = '';
document.body.innerHTML = oldBodyHtml;
}, 100);
});
}
};
clearDOM();

Loading

0 comments on commit c0d54ae

Please sign in to comment.