1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | const isWindowDefined = typeof window !== 'undefined';
|
7 |
|
8 | function log(...args) {
|
9 | console.log('[ESM-HMR]', ...args);
|
10 | }
|
11 | function reload() {
|
12 | if (!isWindowDefined) {
|
13 | return;
|
14 | }
|
15 | location.reload(true);
|
16 | }
|
17 |
|
18 | function clearErrorOverlay() {
|
19 | if (!isWindowDefined) {
|
20 | return;
|
21 | }
|
22 | document.querySelectorAll('hmr-error-overlay').forEach((el) => el.remove());
|
23 | }
|
24 |
|
25 | function createNewErrorOverlay(data) {
|
26 | if (!isWindowDefined) {
|
27 | return;
|
28 | }
|
29 | const HmrErrorOverlay = customElements.get('hmr-error-overlay');
|
30 | if (HmrErrorOverlay) {
|
31 | const overlay = new HmrErrorOverlay(data);
|
32 | clearErrorOverlay();
|
33 | document.body.appendChild(overlay);
|
34 | }
|
35 | }
|
36 |
|
37 | let SOCKET_MESSAGE_QUEUE = [];
|
38 | function _sendSocketMessage(msg) {
|
39 | socket.send(JSON.stringify(msg));
|
40 | }
|
41 | function sendSocketMessage(msg) {
|
42 | if (socket.readyState !== socket.OPEN) {
|
43 | SOCKET_MESSAGE_QUEUE.push(msg);
|
44 | } else {
|
45 | _sendSocketMessage(msg);
|
46 | }
|
47 | }
|
48 |
|
49 | let socketURL = isWindowDefined && window.HMR_WEBSOCKET_URL;
|
50 | if (!socketURL) {
|
51 | const socketHost =
|
52 | isWindowDefined && window.HMR_WEBSOCKET_PORT
|
53 | ? `${location.hostname}:${window.HMR_WEBSOCKET_PORT}`
|
54 | : location.host;
|
55 | socketURL = (location.protocol === 'http:' ? 'ws://' : 'wss://') + socketHost + '/';
|
56 | }
|
57 |
|
58 | const socket = new WebSocket(socketURL, 'esm-hmr');
|
59 | socket.addEventListener('open', () => {
|
60 | SOCKET_MESSAGE_QUEUE.forEach(_sendSocketMessage);
|
61 | SOCKET_MESSAGE_QUEUE = [];
|
62 | });
|
63 | const REGISTERED_MODULES = {};
|
64 | class HotModuleState {
|
65 | constructor(id) {
|
66 | this.data = {};
|
67 | this.isLocked = false;
|
68 | this.isDeclined = false;
|
69 | this.isAccepted = false;
|
70 | this.acceptCallbacks = [];
|
71 | this.disposeCallbacks = [];
|
72 | this.id = id;
|
73 | }
|
74 | lock() {
|
75 | this.isLocked = true;
|
76 | }
|
77 | dispose(callback) {
|
78 | this.disposeCallbacks.push(callback);
|
79 | }
|
80 | invalidate() {
|
81 | reload();
|
82 | }
|
83 | decline() {
|
84 | this.isDeclined = true;
|
85 | }
|
86 | accept(_deps, callback = true) {
|
87 | if (this.isLocked) {
|
88 | return;
|
89 | }
|
90 | if (!this.isAccepted) {
|
91 | sendSocketMessage({id: this.id, type: 'hotAccept'});
|
92 | this.isAccepted = true;
|
93 | }
|
94 | if (!Array.isArray(_deps)) {
|
95 | callback = _deps || callback;
|
96 | _deps = [];
|
97 | }
|
98 | if (callback === true) {
|
99 | callback = () => {};
|
100 | }
|
101 | const deps = _deps.map((dep) => {
|
102 | const ext = dep.split('.').pop();
|
103 | if (!ext) {
|
104 | dep += '.js';
|
105 | } else if (ext !== 'js') {
|
106 | dep += '.proxy.js';
|
107 | }
|
108 | return new URL(dep, `${window.location.origin}${this.id}`).pathname;
|
109 | });
|
110 | this.acceptCallbacks.push({
|
111 | deps,
|
112 | callback,
|
113 | });
|
114 | }
|
115 | }
|
116 | export function createHotContext(fullUrl) {
|
117 | const id = new URL(fullUrl).pathname;
|
118 | const existing = REGISTERED_MODULES[id];
|
119 | if (existing) {
|
120 | existing.lock();
|
121 | runModuleDispose(id);
|
122 | return existing;
|
123 | }
|
124 | const state = new HotModuleState(id);
|
125 | REGISTERED_MODULES[id] = state;
|
126 | return state;
|
127 | }
|
128 |
|
129 |
|
130 | async function runCssStyleAccept({url: id}) {
|
131 | const nonce = Date.now();
|
132 | const oldLinkEl =
|
133 | document.head.querySelector(`link[data-hmr="${id}"]`) ||
|
134 | document.head.querySelector(`link[href="${id}"]`);
|
135 | if (!oldLinkEl) {
|
136 | return true;
|
137 | }
|
138 | const linkEl = oldLinkEl.cloneNode(false);
|
139 | linkEl.dataset.hmr = id;
|
140 | linkEl.type = 'text/css';
|
141 | linkEl.rel = 'stylesheet';
|
142 | linkEl.href = id + '?mtime=' + nonce;
|
143 | linkEl.addEventListener(
|
144 | 'load',
|
145 |
|
146 | () => setTimeout(() => document.head.removeChild(oldLinkEl), 30),
|
147 | false,
|
148 | );
|
149 | oldLinkEl.parentNode.insertBefore(linkEl, oldLinkEl)
|
150 | return true;
|
151 | }
|
152 |
|
153 |
|
154 | async function runJsModuleAccept({url: id, bubbled}) {
|
155 | const state = REGISTERED_MODULES[id];
|
156 | if (!state) {
|
157 | return false;
|
158 | }
|
159 | if (state.isDeclined) {
|
160 | return false;
|
161 | }
|
162 | const acceptCallbacks = state.acceptCallbacks;
|
163 | const updateID = Date.now();
|
164 | for (const {deps, callback: acceptCallback} of acceptCallbacks) {
|
165 | const [module, ...depModules] = await Promise.all([
|
166 | import(id + `?mtime=${updateID}`),
|
167 | ...deps.map((d) => import(d + `?mtime=${updateID}`)),
|
168 | ]);
|
169 | acceptCallback({module, bubbled, deps: depModules});
|
170 | }
|
171 | return true;
|
172 | }
|
173 |
|
174 |
|
175 | async function runModuleDispose(id) {
|
176 | const state = REGISTERED_MODULES[id];
|
177 | if (!state) {
|
178 | return false;
|
179 | }
|
180 | if (state.isDeclined) {
|
181 | return false;
|
182 | }
|
183 | const disposeCallbacks = state.disposeCallbacks;
|
184 | state.disposeCallbacks = [];
|
185 | state.data = {};
|
186 | disposeCallbacks.map((callback) => callback());
|
187 | return true;
|
188 | }
|
189 | socket.addEventListener('message', ({data: _data}) => {
|
190 | if (!_data) {
|
191 | return;
|
192 | }
|
193 | const data = JSON.parse(_data);
|
194 | if (data.type === 'reload') {
|
195 | log('message: reload');
|
196 | reload();
|
197 | return;
|
198 | }
|
199 | if (data.type === 'error') {
|
200 | console.error(
|
201 | `[ESM-HMR] ${data.fileLoc ? data.fileLoc + '\n' : ''}`,
|
202 | data.title + '\n' + data.errorMessage,
|
203 | );
|
204 | createNewErrorOverlay(data);
|
205 | return;
|
206 | }
|
207 | if (data.type === 'update') {
|
208 | log('message: update', data);
|
209 | (data.url.endsWith('.css') ? runCssStyleAccept(data) : runJsModuleAccept(data))
|
210 | .then((ok) => {
|
211 | if (ok) {
|
212 | clearErrorOverlay();
|
213 | } else {
|
214 | reload();
|
215 | }
|
216 | })
|
217 | .catch((err) => {
|
218 | console.error('[ESM-HMR] Hot Update Error', err);
|
219 |
|
220 |
|
221 |
|
222 | if (err instanceof SyntaxError) {
|
223 | createNewErrorOverlay({
|
224 | title: 'Hot Update Error',
|
225 | fileLoc: data.url,
|
226 | errorMessage: err.message,
|
227 | errorStackTrace: err.stack,
|
228 | });
|
229 | }
|
230 | });
|
231 | return;
|
232 | }
|
233 | log('message: unknown', data);
|
234 | });
|
235 | log('listening for file changes...');
|
236 |
|
237 |
|
238 | isWindowDefined && window.addEventListener('error', function (event) {
|
239 |
|
240 | let fileLoc;
|
241 | if (event.filename) {
|
242 | fileLoc = event.filename;
|
243 | if (event.lineno !== undefined) {
|
244 | fileLoc += ` [:${event.lineno}`;
|
245 | if (event.colno !== undefined) {
|
246 | fileLoc += `:${event.colno}`;
|
247 | }
|
248 | fileLoc += `]`;
|
249 | }
|
250 | }
|
251 | createNewErrorOverlay({
|
252 | title: 'Unhandled Runtime Error',
|
253 | fileLoc,
|
254 | errorMessage: event.message,
|
255 | errorStackTrace: event.error ? event.error.stack : undefined,
|
256 | });
|
257 | });
|