-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Expand file tree
/
Copy pathcustom-element.js
More file actions
108 lines (85 loc) · 3.62 KB
/
custom-element.js
File metadata and controls
108 lines (85 loc) · 3.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
/* eslint-env browser */
export function customElement(elementKey, createElementClass) {
const elementRegistration = document.querySelector(`element-registration[element-key="${elementKey}"]`);
elementRegistration.elementConstructor = createElementClass(elementRegistration);
}
function addSourceMapToExtractedScript(scriptContent, originalFileUrl) {
const sourceMap = {
version: 3,
sources: [originalFileUrl],
names: [],
mappings: 'AAAA', // Simple mapping - entire script maps to original file
file: originalFileUrl.split('/').pop(),
sourceRoot: '',
sourcesContent: [scriptContent]
};
const base64Map = btoa(JSON.stringify(sourceMap));
const dataUrl = `data:application/json;charset=utf-8;base64,${base64Map}`;
// TODO: Figure out how to make setting breakpoints work
return scriptContent + `\n//# sourceMappingURL=${dataUrl}`;
}
function fixScript(script, url) {
const newScript = document.createElement('script');
Array.from(script.attributes).forEach(attr => newScript.setAttribute(attr.name, attr.value));
newScript.text = addSourceMapToExtractedScript(script.text, url);
return newScript;
}
function initDocument(elementRegistration, currentDocument) {
const moduleUrl = new URL(`./${elementRegistration.getAttribute('element-key')}.ce.js`, import.meta.url).toString();
const allowedElementNames = ['link', 'style', 'script'];
if (!currentDocument) {
throw new Error('Custom element must be registered within a <element-registration> element.');
}
const result = Promise.withResolvers();
Object.defineProperty(elementRegistration, 'elementConstructor', {
set(constructor) {
if (!constructor) {
throw new Error('Custom element constructor is required.');
}
const elementName = elementRegistration.getAttribute('element-name');
if (!elementName) {
throw new Error('Custom element must have a name.');
}
customElements.define(elementName, constructor, constructor.options);
result.resolve(constructor);
},
get() {
return customElement.get(elementRegistration.getAttribute('element-name'));
}
});
document.head.append(
...Array.from(currentDocument.head.children)
.filter(element => allowedElementNames.includes(element.localName))
.map(element => (element.localName === 'script' ? fixScript(element, moduleUrl) : element))
);
elementRegistration.append(
...Array.from(currentDocument.body.children).map(element =>
element.localName === 'script' ? fixScript(element, moduleUrl) : element
)
);
document.body.appendChild(elementRegistration);
return result.promise;
}
export function registerElements(...elementNames) {
const parser = new DOMParser();
const entries = elementNames.map(entry => (typeof entry === 'string' ? [entry, entry] : Object.entries(entry).at(0)));
const raceInit = (key, initPromise) =>
Promise.race([
new Promise((_resolve, reject) => {
setTimeout(
() => reject(new Error(`Could not initialize custom element "${key}". Did you call customElement()?`)),
5000
);
}),
initPromise
]);
return Promise.all(
entries.map(async ([key, elementName]) => {
const content = await fetch(new URL(`./${key}.ce`, import.meta.url)).then(response => response.text());
const elementRegistration = document.createElement('element-registration');
elementRegistration.setAttribute('element-key', key);
elementRegistration.setAttribute('element-name', elementName);
return raceInit(key, initDocument(elementRegistration, parser.parseFromString(content, 'text/html')));
})
);
}