UNPKG

7.35 kBJavaScriptView Raw
1"use strict";
2var __importDefault = (this && this.__importDefault) || function (mod) {
3 return (mod && mod.__esModule) ? mod : { "default": mod };
4};
5Object.defineProperty(exports, "__esModule", { value: true });
6exports.EsmHmrEngine = void 0;
7const ws_1 = __importDefault(require("ws"));
8const strip_ansi_1 = __importDefault(require("strip-ansi"));
9const DEFAULT_CONNECT_DELAY = 2000;
10const DEFAULT_PORT = 12321;
11class EsmHmrEngine {
12 constructor(options) {
13 this.clients = new Set();
14 this.dependencyTree = new Map();
15 this.delay = 0;
16 this.currentBatch = [];
17 this.currentBatchTimeout = null;
18 this.cachedConnectErrors = new Set();
19 this.port = 0;
20 this.port = options.port || DEFAULT_PORT;
21 const wss = options.server
22 ? new ws_1.default.Server({ noServer: true })
23 : new ws_1.default.Server({ port: this.port });
24 if (options.delay) {
25 this.delay = options.delay;
26 }
27 if (options.server) {
28 options.server.on('upgrade', (req, socket, head) => {
29 // Only handle upgrades to ESM-HMR requests, ignore others.
30 if (req.headers['sec-websocket-protocol'] !== 'esm-hmr') {
31 return;
32 }
33 wss.handleUpgrade(req, socket, head, (client) => {
34 wss.emit('connection', client, req);
35 });
36 });
37 }
38 wss.on('connection', (client) => {
39 this.connectClient(client);
40 this.registerListener(client);
41 if (this.cachedConnectErrors.size > 0) {
42 this.dispatchMessage(Array.from(this.cachedConnectErrors), client);
43 }
44 });
45 wss.on('close', (client) => {
46 this.disconnectClient(client);
47 });
48 }
49 registerListener(client) {
50 client.on('message', (data) => {
51 const message = JSON.parse(data.toString());
52 if (message.type === 'hotAccept') {
53 const entry = this.getEntry(message.id, true);
54 entry.isHmrAccepted = true;
55 entry.isHmrEnabled = true;
56 }
57 });
58 }
59 createEntry(sourceUrl) {
60 const newEntry = {
61 dependencies: new Set(),
62 dependents: new Set(),
63 needsReplacement: false,
64 needsReplacementCount: 0,
65 isHmrEnabled: false,
66 isHmrAccepted: false,
67 };
68 this.dependencyTree.set(sourceUrl, newEntry);
69 return newEntry;
70 }
71 getEntry(sourceUrl, createIfNotFound = false) {
72 const result = this.dependencyTree.get(sourceUrl);
73 if (result) {
74 return result;
75 }
76 if (createIfNotFound) {
77 return this.createEntry(sourceUrl);
78 }
79 return null;
80 }
81 setEntry(sourceUrl, imports, isHmrEnabled = false) {
82 const result = this.getEntry(sourceUrl, true);
83 const outdatedDependencies = new Set(result.dependencies);
84 result.isHmrEnabled = isHmrEnabled;
85 for (const importUrl of imports) {
86 this.addRelationship(sourceUrl, importUrl);
87 outdatedDependencies.delete(importUrl);
88 }
89 for (const importUrl of outdatedDependencies) {
90 this.removeRelationship(sourceUrl, importUrl);
91 }
92 }
93 removeRelationship(sourceUrl, importUrl) {
94 let importResult = this.getEntry(importUrl);
95 importResult && importResult.dependents.delete(sourceUrl);
96 const sourceResult = this.getEntry(sourceUrl);
97 sourceResult && sourceResult.dependencies.delete(importUrl);
98 }
99 addRelationship(sourceUrl, importUrl) {
100 if (importUrl !== sourceUrl) {
101 let importResult = this.getEntry(importUrl, true);
102 importResult.dependents.add(sourceUrl);
103 const sourceResult = this.getEntry(sourceUrl, true);
104 sourceResult.dependencies.add(importUrl);
105 }
106 }
107 markEntryForReplacement(entry, state) {
108 if (state) {
109 entry.needsReplacementCount++;
110 }
111 else {
112 entry.needsReplacementCount--;
113 }
114 entry.needsReplacement = !!entry.needsReplacementCount;
115 }
116 broadcastMessage(data) {
117 // Special "error" event handling
118 if (data.type === 'error') {
119 // Clean: remove any console styling before we send to the browser
120 // NOTE(@fks): If another event ever needs this, okay to generalize.
121 data.title = data.title && strip_ansi_1.default(data.title);
122 data.errorMessage = data.errorMessage && strip_ansi_1.default(data.errorMessage);
123 data.fileLoc = data.fileLoc && strip_ansi_1.default(data.fileLoc);
124 data.errorStackTrace = data.errorStackTrace && strip_ansi_1.default(data.errorStackTrace);
125 // Cache: Cache errors in case an HMR client connects after the error (first page load).
126 if (Array.from(this.cachedConnectErrors).every((f) => JSON.stringify(f) !== JSON.stringify(data))) {
127 this.cachedConnectErrors.add(data);
128 setTimeout(() => {
129 this.cachedConnectErrors.delete(data);
130 }, DEFAULT_CONNECT_DELAY);
131 }
132 }
133 if (this.delay > 0) {
134 if (this.currentBatchTimeout) {
135 clearTimeout(this.currentBatchTimeout);
136 }
137 this.currentBatch.push(data);
138 this.currentBatchTimeout = setTimeout(() => this.dispatchBatch(), this.delay || 100);
139 }
140 else {
141 this.dispatchMessage([data]);
142 }
143 }
144 dispatchBatch() {
145 if (this.currentBatchTimeout) {
146 clearTimeout(this.currentBatchTimeout);
147 }
148 if (this.currentBatch.length > 0) {
149 this.dispatchMessage(this.currentBatch);
150 this.currentBatch = [];
151 }
152 }
153 /**
154 * This is shared logic to dispatch messages to the clients. The public methods
155 * `broadcastMessage` and `dispatchBatch` manage the delay then use this,
156 * internally when it's time to actually send the data.
157 */
158 dispatchMessage(messageBatch, singleClient) {
159 if (messageBatch.length === 0) {
160 return;
161 }
162 const clientRecipientList = singleClient ? [singleClient] : this.clients;
163 let singleSummaryMessage = messageBatch.find((message) => message.type === 'reload') || null;
164 clientRecipientList.forEach((client) => {
165 if (client.readyState === ws_1.default.OPEN) {
166 if (singleSummaryMessage) {
167 client.send(JSON.stringify(singleSummaryMessage));
168 }
169 else {
170 messageBatch.forEach((data) => {
171 client.send(JSON.stringify(data));
172 });
173 }
174 }
175 else {
176 this.disconnectClient(client);
177 }
178 });
179 }
180 connectClient(client) {
181 this.clients.add(client);
182 }
183 disconnectClient(client) {
184 client.terminate();
185 this.clients.delete(client);
186 }
187 disconnectAllClients() {
188 for (const client of this.clients) {
189 this.disconnectClient(client);
190 }
191 }
192}
193exports.EsmHmrEngine = EsmHmrEngine;