|
3 | 3 | const os = require("node:os"); |
4 | 4 | const path = require("node:path"); |
5 | 5 | const webpack = require("webpack"); |
| 6 | +const WebSocket = require("ws"); |
6 | 7 | const Server = require("../../lib/Server"); |
7 | 8 | const config = require("../fixtures/client-config/webpack.config"); |
8 | 9 | const multiCompilerConfig = require("../fixtures/multi-compiler-two-configurations/webpack.config"); |
@@ -133,6 +134,110 @@ describe("API (plugin)", () => { |
133 | 134 | }); |
134 | 135 | }); |
135 | 136 |
|
| 137 | + it("should send 'invalid' to WebSocket clients when recompilation is triggered", async () => { |
| 138 | + const compiler = webpack(config); |
| 139 | + const server = new Server({ port }); |
| 140 | + server.apply(compiler); |
| 141 | + |
| 142 | + const { watching } = await compile(compiler, port); |
| 143 | + |
| 144 | + const sawInvalid = await new Promise((resolve, reject) => { |
| 145 | + let initialOkSeen = false; |
| 146 | + const ws = new WebSocket(`ws://127.0.0.1:${port}/ws`, { |
| 147 | + headers: { |
| 148 | + host: `127.0.0.1:${port}`, |
| 149 | + origin: `http://127.0.0.1:${port}`, |
| 150 | + }, |
| 151 | + }); |
| 152 | + |
| 153 | + ws.on("error", reject); |
| 154 | + ws.on("message", (raw) => { |
| 155 | + const { type } = JSON.parse(raw.toString()); |
| 156 | + // Wait for the initial "ok" (sent right after the WS handshake), |
| 157 | + // then trigger an invalidation. The server's `compiler.hooks.invalid` |
| 158 | + // tap should push an "invalid" message before the next compile |
| 159 | + // finishes. |
| 160 | + if (!initialOkSeen && type === "ok") { |
| 161 | + initialOkSeen = true; |
| 162 | + watching.invalidate(); |
| 163 | + return; |
| 164 | + } |
| 165 | + if (type === "invalid") { |
| 166 | + ws.close(); |
| 167 | + resolve(true); |
| 168 | + } |
| 169 | + }); |
| 170 | + }); |
| 171 | + |
| 172 | + expect(sawInvalid).toBe(true); |
| 173 | + |
| 174 | + await new Promise((resolve) => { |
| 175 | + compiler.close(resolve); |
| 176 | + }); |
| 177 | + }); |
| 178 | + |
| 179 | + it("should use constructor options instead of compiler.options.devServer", async () => { |
| 180 | + // Plugin reads its options from its constructor argument; values on |
| 181 | + // `compiler.options.devServer` are intentionally ignored. This protects |
| 182 | + // the documented contract. |
| 183 | + const compiler = webpack({ |
| 184 | + ...config, |
| 185 | + // Pretend an unrelated `devServer` block exists in the user's config. |
| 186 | + // The plugin must not pick `port: portB` from it. |
| 187 | + devServer: { port: portB, host: "0.0.0.0" }, |
| 188 | + }); |
| 189 | + const server = new Server({ port: portA }); |
| 190 | + server.apply(compiler); |
| 191 | + |
| 192 | + await compile(compiler, portA); |
| 193 | + |
| 194 | + const responseA = await fetch(`http://127.0.0.1:${portA}/`); |
| 195 | + expect(responseA.status).toBe(200); |
| 196 | + |
| 197 | + let portBReachable = true; |
| 198 | + try { |
| 199 | + await fetch(`http://127.0.0.1:${portB}/`); |
| 200 | + } catch { |
| 201 | + portBReachable = false; |
| 202 | + } |
| 203 | + expect(portBReachable).toBe(false); |
| 204 | + |
| 205 | + await new Promise((resolve) => { |
| 206 | + compiler.close(resolve); |
| 207 | + }); |
| 208 | + }); |
| 209 | + |
| 210 | + it("should propagate setup errors via the watch callback", async () => { |
| 211 | + const compiler = webpack(config); |
| 212 | + // Using a URL as `static.directory` throws inside `normalizeOptions` |
| 213 | + // during `setup()`. The rejection should bubble out through the |
| 214 | + // `beforeCompile.tapPromise` handler and reach `compiler.watch()`'s |
| 215 | + // user callback as an error. |
| 216 | + const server = new Server({ |
| 217 | + port, |
| 218 | + static: "https://absolute-url.example/some/path", |
| 219 | + }); |
| 220 | + server.apply(compiler); |
| 221 | + |
| 222 | + const error = await new Promise((resolve, reject) => { |
| 223 | + compiler.watch({}, (err) => { |
| 224 | + if (err) { |
| 225 | + resolve(err); |
| 226 | + } else { |
| 227 | + reject(new Error("expected setup to fail")); |
| 228 | + } |
| 229 | + }); |
| 230 | + }); |
| 231 | + |
| 232 | + expect(error.message).toMatch( |
| 233 | + /Using a URL as static.directory is not supported/, |
| 234 | + ); |
| 235 | + |
| 236 | + await new Promise((resolve) => { |
| 237 | + compiler.close(resolve); |
| 238 | + }); |
| 239 | + }); |
| 240 | + |
136 | 241 | describe("plugin in webpack config", () => { |
137 | 242 | it("should work when added to webpack config plugins array", async () => { |
138 | 243 | const server = new Server({ port }); |
|
0 commit comments