diff --git a/cli.js b/cli.js index 7e46d89..04be1ca 100644 --- a/cli.js +++ b/cli.js @@ -78,7 +78,7 @@ Arguments: close() { if(this.server) { - this.server.close(); + return this.server.close(); } } } diff --git a/cmd.js b/cmd.js index f808204..695b845 100755 --- a/cmd.js +++ b/cmd.js @@ -67,9 +67,9 @@ try { domDiff: argv.domdiff, }); - process.on("SIGINT", () => { - cli.close(); - process.exit(); + process.on("SIGINT", async () => { + await cli.close(); + process.exitCode = 0; }); } } catch (e) { diff --git a/server.js b/server.js index af79561..e2a1425 100644 --- a/server.js +++ b/server.js @@ -795,23 +795,47 @@ class EleventyDevServer { } } - close() { + // Helper for promisifying close methods with callbacks, like http.Server or ws.WebSocketServer. + _closeServer(server) { + return new Promise((resolve, reject) => { + server.close(err => { + if (err) { + reject(err); + } + resolve(); + }); + }); + } + + async close() { + // Prevent multiple invocations. + if (this?._isClosing) { + return; + } + this._isClosing = true; + // TODO would be awesome to set a delayed redirect when port changed to redirect to new _server_ this.sendUpdateNotification({ type: "eleventy.status", status: "disconnected", }); - if(this.server) { - this.server.close(); - } if(this.updateServer) { - this.updateServer.close(); + // Close all existing WS connections. + this.updateServer?.clients.forEach(socket => socket.close()); + await this._closeServer(this.updateServer); + } + + if(this._server?.listening) { + await this._closeServer(this.server); } + if(this._watcher) { - this._watcher.close(); + await this._watcher.close(); delete this._watcher; } + + delete this._isClosing; } sendError({ error }) { diff --git a/test/testServer.js b/test/testServer.js index 741211c..6489ed0 100644 --- a/test/testServer.js +++ b/test/testServer.js @@ -6,7 +6,7 @@ function testNormalizeFilePath(filepath) { return filepath.split("/").join(path.sep); } -test("Url mappings for resource/index.html", t => { +test("Url mappings for resource/index.html", async (t) => { let server = new EleventyDevServer("test-server", "./test/stubs/"); t.deepEqual(server.mapUrlToFilePath("/route1/"), { @@ -27,11 +27,11 @@ test("Url mappings for resource/index.html", t => { statusCode: 200, filepath: testNormalizeFilePath("test/stubs/route1/index.html") }); - - server.close(); + + await server.close(); }); -test("Url mappings for resource.html", t => { +test("Url mappings for resource.html", async (t) => { let server = new EleventyDevServer("test-server", "./test/stubs/"); t.deepEqual(server.mapUrlToFilePath("/route2/"), { @@ -53,10 +53,10 @@ test("Url mappings for resource.html", t => { filepath: testNormalizeFilePath("test/stubs/route2.html",) }); - server.close(); + await server.close(); }); -test("Url mappings for resource.html and resource/index.html", t => { +test("Url mappings for resource.html and resource/index.html", async (t) => { let server = new EleventyDevServer("test-server", "./test/stubs/"); // Production mismatch warning: Netlify 301 redirects to /route3 here @@ -80,10 +80,10 @@ test("Url mappings for resource.html and resource/index.html", t => { filepath: testNormalizeFilePath("test/stubs/route3.html",) }); - server.close(); + await server.close(); }); -test("Url mappings for missing resource", t => { +test("Url mappings for missing resource", async (t) => { let server = new EleventyDevServer("test-server", "./test/stubs/"); // 404s @@ -91,10 +91,10 @@ test("Url mappings for missing resource", t => { statusCode: 404 }); - server.close(); + await server.close(); }); -test("Url mapping for a filename with a space in it", t => { +test("Url mapping for a filename with a space in it", async (t) => { let server = new EleventyDevServer("test-server", "./test/stubs/"); t.deepEqual(server.mapUrlToFilePath("/route space.html"), { @@ -102,7 +102,7 @@ test("Url mapping for a filename with a space in it", t => { filepath: testNormalizeFilePath("test/stubs/route space.html",) }); - server.close(); + await server.close(); }); test("matchPassthroughAlias", async (t) => { @@ -131,6 +131,8 @@ test("matchPassthroughAlias", async (t) => { // Map entry exists, file exists t.is(server.matchPassthroughAlias("/elsewhere/index.css"), "./test/stubs/with-css/style.css"); + + await server.close(); }); @@ -155,7 +157,7 @@ test("pathPrefix matching", async (t) => { url: '/pathprefix/', }); - server.close(); + await server.close(); }); test("pathPrefix without leading slash", async (t) => { @@ -179,7 +181,7 @@ test("pathPrefix without leading slash", async (t) => { url: '/pathprefix/', }); - server.close(); + await server.close(); }); test("pathPrefix without trailing slash", async (t) => { @@ -203,7 +205,7 @@ test("pathPrefix without trailing slash", async (t) => { url: '/pathprefix/', }); - server.close(); + await server.close(); }); test("pathPrefix without leading or trailing slash", async (t) => { @@ -227,7 +229,7 @@ test("pathPrefix without leading or trailing slash", async (t) => { url: '/pathprefix/', }); - server.close(); + await server.close(); }); test("indexFileName option: serve custom index when provided", async (t) => { @@ -244,7 +246,7 @@ test("indexFileName option: serve custom index when provided", async (t) => { filepath: testNormalizeFilePath("test/stubs/route1/custom-index.html"), }); - server.close(); + await server.close(); }); test("indexFileName option: return 404 when custom index file doesn't exist", async (t) => { @@ -254,5 +256,5 @@ test("indexFileName option: return 404 when custom index file doesn't exist", as statusCode: 404, }); - server.close(); + await server.close(); }); diff --git a/test/testServerRequests.js b/test/testServerRequests.js index 132188c..4be4a34 100644 --- a/test/testServerRequests.js +++ b/test/testServerRequests.js @@ -75,7 +75,7 @@ async function fetchHeadersForRequest(t, server, path, extras) { }) } -test("Standard request", async t => { +test("Standard request", async (t) => { let server = new EleventyDevServer("test-server", "./test/stubs/", getOptions()); server.serve(8100); @@ -83,7 +83,7 @@ test("Standard request", async t => { t.true(data.includes("