|
| 1 | +import { Connect } from "vite"; |
| 2 | +import getEtag from "etag"; |
| 3 | + |
| 4 | +// Modifies the vite HMR client to support various web extension features including: |
| 5 | +// Exporting a function to add HMR style injection targets |
| 6 | +// Tweaks to support running in a service worker context |
| 7 | +const viteClientModifier: Connect.NextHandleFunction = (req, res, next) => { |
| 8 | + const _originalEnd = res.end; |
| 9 | + |
| 10 | + // @ts-ignore |
| 11 | + res.end = function end(chunk, ...otherArgs) { |
| 12 | + if (req.url === "/@vite/client" && typeof chunk === "string") { |
| 13 | + chunk = addCustomStyleFunctionality(chunk); |
| 14 | + chunk = addServiceWorkerSupport(chunk); |
| 15 | + |
| 16 | + res.setHeader("Etag", getEtag(chunk, { weak: true })); |
| 17 | + } |
| 18 | + |
| 19 | + // @ts-ignore |
| 20 | + return _originalEnd.call(this, chunk, ...otherArgs); |
| 21 | + }; |
| 22 | + |
| 23 | + next(); |
| 24 | +}; |
| 25 | + |
| 26 | +function addCustomStyleFunctionality(source: string): string { |
| 27 | + if ( |
| 28 | + !/const sheetsMap/.test(source) || |
| 29 | + !/document\.head\.appendChild\(style\)/.test(source) || |
| 30 | + !/document\.head\.removeChild\(style\)/.test(source) || |
| 31 | + (!/style\.textContent = content/.test(source) && |
| 32 | + !/style\.innerHTML = content/.test(source)) |
| 33 | + ) { |
| 34 | + console.error( |
| 35 | + "Web extension HMR style support disabled -- failed to update vite client" |
| 36 | + ); |
| 37 | + |
| 38 | + return source; |
| 39 | + } |
| 40 | + |
| 41 | + source = source.replace( |
| 42 | + "const sheetsMap", |
| 43 | + "const styleTargets = new Set(); const styleTargetsStyleMap = new Map(); const sheetsMap" |
| 44 | + ); |
| 45 | + source = source.replace("export {", "export { addStyleTarget, "); |
| 46 | + source = source.replace( |
| 47 | + "document.head.appendChild(style)", |
| 48 | + "styleTargets.size ? styleTargets.forEach(target => addStyleToTarget(style, target)) : document.head.appendChild(style)" |
| 49 | + ); |
| 50 | + source = source.replace( |
| 51 | + "document.head.removeChild(style)", |
| 52 | + "styleTargetsStyleMap.get(style) ? styleTargetsStyleMap.get(style).forEach(style => style.parentNode.removeChild(style)) : document.head.removeChild(style)" |
| 53 | + ); |
| 54 | + |
| 55 | + const styleProperty = /style\.textContent = content/.test(source) |
| 56 | + ? "style.textContent" |
| 57 | + : "style.innerHTML"; |
| 58 | + |
| 59 | + const lastStyleInnerHtml = source.lastIndexOf(`${styleProperty} = content`); |
| 60 | + |
| 61 | + source = |
| 62 | + source.slice(0, lastStyleInnerHtml) + |
| 63 | + source |
| 64 | + .slice(lastStyleInnerHtml) |
| 65 | + .replace( |
| 66 | + `${styleProperty} = content`, |
| 67 | + `${styleProperty} = content; styleTargetsStyleMap.get(style)?.forEach(style => ${styleProperty} = content)` |
| 68 | + ); |
| 69 | + |
| 70 | + source += ` |
| 71 | + function addStyleTarget(newStyleTarget) { |
| 72 | + for (const [, style] of sheetsMap.entries()) { |
| 73 | + addStyleToTarget(style, newStyleTarget, styleTargets.size !== 0); |
| 74 | + } |
| 75 | +
|
| 76 | + styleTargets.add(newStyleTarget); |
| 77 | + } |
| 78 | +
|
| 79 | + function addStyleToTarget(style, target, cloneStyle = true) { |
| 80 | + const addedStyle = cloneStyle ? style.cloneNode(true) : style; |
| 81 | + target.appendChild(addedStyle); |
| 82 | +
|
| 83 | + styleTargetsStyleMap.set(style, [...(styleTargetsStyleMap.get(style) ?? []), addedStyle]); |
| 84 | + } |
| 85 | + `; |
| 86 | + |
| 87 | + return source; |
| 88 | +} |
| 89 | + |
| 90 | +function guardDocumentUsageWithDefault( |
| 91 | + source: string, |
| 92 | + documentUsage: string, |
| 93 | + defaultValue: string |
| 94 | +): string { |
| 95 | + return source.replace( |
| 96 | + documentUsage, |
| 97 | + `('document' in globalThis ? ${documentUsage} : ${defaultValue})` |
| 98 | + ); |
| 99 | +} |
| 100 | + |
| 101 | +function addServiceWorkerSupport(source: string): string { |
| 102 | + // update location.reload usages |
| 103 | + source = source.replaceAll( |
| 104 | + /(window\.)?location.reload\(\)/g, |
| 105 | + "(location.reload?.() ?? (typeof chrome !== 'undefined' ? chrome.runtime?.reload?.() : ''))" |
| 106 | + ); |
| 107 | + |
| 108 | + // add document guards |
| 109 | + source = guardDocumentUsageWithDefault( |
| 110 | + source, |
| 111 | + "document.querySelectorAll(overlayId).length", |
| 112 | + "false" |
| 113 | + ); |
| 114 | + |
| 115 | + source = guardDocumentUsageWithDefault( |
| 116 | + source, |
| 117 | + "document.visibilityState", |
| 118 | + `"visible"` |
| 119 | + ); |
| 120 | + |
| 121 | + source = guardDocumentUsageWithDefault( |
| 122 | + source, |
| 123 | + `document.querySelectorAll('link')`, |
| 124 | + "[]" |
| 125 | + ); |
| 126 | + |
| 127 | + source = source.replace( |
| 128 | + "const enableOverlay =", |
| 129 | + `const enableOverlay = ('document' in globalThis) &&` |
| 130 | + ); |
| 131 | + |
| 132 | + return source; |
| 133 | +} |
| 134 | + |
| 135 | +export default viteClientModifier; |
0 commit comments