-
Notifications
You must be signed in to change notification settings - Fork 4.3k
/
Copy pathhead.ts
104 lines (95 loc) · 3.05 KB
/
head.ts
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
/**
* Helper to update only the necessary tags in the head.
*
* @async
* @param newHead The head elements of the new page.
*/
export const updateHead = async ( newHead: HTMLHeadElement[] ) => {
// Helper to get the tag id store in the cache.
const getTagId = ( tag: Element ) => tag.id || tag.outerHTML;
// Map incoming head tags by their content.
const newHeadMap = new Map< string, Element >();
for ( const child of newHead ) {
newHeadMap.set( getTagId( child ), child );
}
const toRemove: Element[] = [];
// Detect nodes that should be added or removed.
for ( const child of document.head.children ) {
const id = getTagId( child );
// Always remove styles and links as they might change.
if ( child.nodeName === 'LINK' || child.nodeName === 'STYLE' ) {
toRemove.push( child );
} else if ( newHeadMap.has( id ) ) {
newHeadMap.delete( id );
} else if ( child.nodeName !== 'SCRIPT' && child.nodeName !== 'META' ) {
toRemove.push( child );
}
}
// Prepare new assets.
const toAppend = [ ...newHeadMap.values() ];
// Apply the changes.
toRemove.forEach( ( n ) => n.remove() );
document.head.append( ...toAppend );
};
/**
* Fetches and processes head assets (stylesheets and scripts) from a specified document.
*
* @async
* @param doc The document from which to fetch head assets. It should support standard DOM querying methods.
* @param headElements A map of head elements to modify tracking the URLs of already processed assets to avoid duplicates.
* @param headElements.tag
* @param headElements.text
*
* @return Returns an array of HTML elements representing the head assets.
*/
export const fetchHeadAssets = async (
doc: Document,
headElements: Map< string, { tag: HTMLElement; text: string } >
): Promise< HTMLElement[] > => {
const headTags = [];
const assets = [
{
tagName: 'style',
selector: 'link[rel=stylesheet]',
attribute: 'href',
},
{ tagName: 'script', selector: 'script[src]', attribute: 'src' },
];
for ( const asset of assets ) {
const { tagName, selector, attribute } = asset;
const tags = doc.querySelectorAll<
HTMLScriptElement | HTMLStyleElement
>( selector );
// Use Promise.all to wait for fetch to complete
await Promise.all(
Array.from( tags ).map( async ( tag ) => {
const attributeValue = tag.getAttribute( attribute );
if ( ! headElements.has( attributeValue ) ) {
try {
const response = await fetch( attributeValue );
const text = await response.text();
headElements.set( attributeValue, {
tag,
text,
} );
} catch ( e ) {
// eslint-disable-next-line no-console
console.error( e );
}
}
const headElement = headElements.get( attributeValue );
const element = doc.createElement( tagName );
element.innerText = headElement.text;
for ( const attr of headElement.tag.attributes ) {
element.setAttribute( attr.name, attr.value );
}
headTags.push( element );
} )
);
}
return [
doc.querySelector( 'title' ),
...doc.querySelectorAll( 'style' ),
...headTags,
];
};