From efb2cec3f8acbbe5113aad20529e268c01ac29c2 Mon Sep 17 00:00:00 2001 From: Malcolm Kee Date: Thu, 13 Oct 2022 03:11:18 +1100 Subject: [PATCH] feat: open editor when clicking error on overlay (#4587) --- client-src/overlay.js | 12 ++++- examples/client/overlay/app.js | 4 ++ examples/client/overlay/webpack.config.js | 2 +- lib/Server.js | 13 +++++ package-lock.json | 38 ++++++++++++-- package.json | 2 + .../overlay.test.js.snap.webpack5 | 8 +++ test/e2e/overlay.test.js | 50 +++++++++++++++++++ test/helpers/run-browser.js | 16 ++++++ 9 files changed, 139 insertions(+), 6 deletions(-) diff --git a/client-src/overlay.js b/client-src/overlay.js index ca506c8ac8..4e7909b370 100644 --- a/client-src/overlay.js +++ b/client-src/overlay.js @@ -184,7 +184,7 @@ function formatProblem(type, item) { // Compilation with errors (e.g. syntax error or missing modules). /** * @param {string} type - * @param {Array} messages + * @param {Array} messages * @param {string | null} trustedTypesPolicyName */ function show(type, messages, trustedTypesPolicyName) { @@ -203,6 +203,16 @@ function show(type, messages, trustedTypesPolicyName) { typeElement.innerText = header; applyStyle(typeElement, msgTypeStyle); + if (message.moduleIdentifier) { + applyStyle(typeElement, { cursor: "pointer" }); + typeElement.dataset.canOpen = true; + typeElement.addEventListener("click", () => { + fetch( + `/webpack-dev-server/open-editor?fileName=${message.moduleIdentifier}` + ); + }); + } + // Make it look similar to our terminal. const text = ansiHTML(encode(body)); const messageTextNode = document.createElement("div"); diff --git a/examples/client/overlay/app.js b/examples/client/overlay/app.js index 51cf4a396b..77142167f0 100644 --- a/examples/client/overlay/app.js +++ b/examples/client/overlay/app.js @@ -2,5 +2,9 @@ const target = document.querySelector("#target"); +// eslint-disable-next-line import/no-unresolved, import/extensions +const invalid = require("./invalid.js"); + +console.log(invalid); target.classList.add("pass"); target.innerHTML = "Success!"; diff --git a/examples/client/overlay/webpack.config.js b/examples/client/overlay/webpack.config.js index eab79de138..43e883e183 100644 --- a/examples/client/overlay/webpack.config.js +++ b/examples/client/overlay/webpack.config.js @@ -7,7 +7,7 @@ const { setup } = require("../../util"); module.exports = setup({ context: __dirname, // create error for overlay - entry: "./invalid.js", + entry: "./app.js", devServer: { client: { overlay: true, diff --git a/lib/Server.js b/lib/Server.js index 36d00e5dcb..c8876acaab 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -2033,6 +2033,19 @@ class Server { } ); + /** @type {import("express").Application} */ + (app).get("/webpack-dev-server/open-editor", (req, res) => { + const fileName = req.query.fileName; + + if (typeof fileName === "string") { + // @ts-ignore + const launchEditor = require("launch-editor"); + launchEditor(fileName); + } + + res.end(); + }); + /** @type {import("express").Application} */ (app).get( "/webpack-dev-server", diff --git a/package-lock.json b/package-lock.json index b7662be1e9..99d9ea5967 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "html-entities": "^2.3.2", "http-proxy-middleware": "^2.0.3", "ipaddr.js": "^2.0.1", + "launch-editor": "^2.6.0", "open": "^8.0.9", "p-retry": "^4.5.0", "rimraf": "^3.0.2", @@ -94,6 +95,7 @@ "tcp-port-used": "^1.0.2", "typescript": "^4.7.2", "url-loader": "^4.1.1", + "wait-for-expect": "^3.0.2", "webpack": "^5.71.0", "webpack-cli": "^4.7.2", "webpack-merge": "^5.8.0" @@ -11030,6 +11032,15 @@ "node": ">= 8" } }, + "node_modules/launch-editor": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.0.tgz", + "integrity": "sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ==", + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.7.3" + } + }, "node_modules/less": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/less/-/less-4.1.3.tgz", @@ -13820,8 +13831,7 @@ "node_modules/shell-quote": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", - "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", - "dev": true + "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==" }, "node_modules/side-channel": { "version": "1.0.4", @@ -15264,6 +15274,12 @@ "node": ">=12" } }, + "node_modules/wait-for-expect": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/wait-for-expect/-/wait-for-expect-3.0.2.tgz", + "integrity": "sha512-cfS1+DZxuav1aBYbaO/kE06EOS8yRw7qOFoD3XtjTkYvCvh3zUvNST8DXK/nPaeqIzIv3P3kL3lRJn8iwOiSag==", + "dev": true + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -24043,6 +24059,15 @@ "integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==", "dev": true }, + "launch-editor": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.0.tgz", + "integrity": "sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ==", + "requires": { + "picocolors": "^1.0.0", + "shell-quote": "^1.7.3" + } + }, "less": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/less/-/less-4.1.3.tgz", @@ -26123,8 +26148,7 @@ "shell-quote": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", - "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", - "dev": true + "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==" }, "side-channel": { "version": "1.0.4", @@ -27187,6 +27211,12 @@ "xml-name-validator": "^4.0.0" } }, + "wait-for-expect": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/wait-for-expect/-/wait-for-expect-3.0.2.tgz", + "integrity": "sha512-cfS1+DZxuav1aBYbaO/kE06EOS8yRw7qOFoD3XtjTkYvCvh3zUvNST8DXK/nPaeqIzIv3P3kL3lRJn8iwOiSag==", + "dev": true + }, "walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", diff --git a/package.json b/package.json index e46204a8d3..7922c2631d 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "html-entities": "^2.3.2", "http-proxy-middleware": "^2.0.3", "ipaddr.js": "^2.0.1", + "launch-editor": "^2.6.0", "open": "^8.0.9", "p-retry": "^4.5.0", "rimraf": "^3.0.2", @@ -126,6 +127,7 @@ "tcp-port-used": "^1.0.2", "typescript": "^4.7.2", "url-loader": "^4.1.1", + "wait-for-expect": "^3.0.2", "webpack": "^5.71.0", "webpack-cli": "^4.7.2", "webpack-merge": "^5.8.0" diff --git a/test/e2e/__snapshots__/overlay.test.js.snap.webpack5 b/test/e2e/__snapshots__/overlay.test.js.snap.webpack5 index f7fcabc944..c85c4cf4cc 100644 --- a/test/e2e/__snapshots__/overlay.test.js.snap.webpack5 +++ b/test/e2e/__snapshots__/overlay.test.js.snap.webpack5 @@ -90,11 +90,13 @@ exports[`overlay should not show initially, then show on an error and allow to c \\" >
ERROR in ./foo.js 1:1 @@ -212,11 +214,13 @@ exports[`overlay should not show initially, then show on an error, then hide on \\" >
ERROR in ./foo.js 1:1 @@ -334,11 +338,13 @@ exports[`overlay should not show initially, then show on an error, then show oth \\" >
ERROR in ./foo.js 1:1 @@ -419,11 +425,13 @@ exports[`overlay should not show initially, then show on an error, then show oth \\" >
ERROR in ./foo.js 1:1 diff --git a/test/e2e/overlay.test.js b/test/e2e/overlay.test.js index 42a6adabaa..86e31bbf16 100644 --- a/test/e2e/overlay.test.js +++ b/test/e2e/overlay.test.js @@ -4,6 +4,7 @@ const path = require("path"); const fs = require("graceful-fs"); const prettier = require("prettier"); const webpack = require("webpack"); +const waitForExpect = require("wait-for-expect"); const Server = require("../../lib/Server"); const config = require("../fixtures/overlay-config/webpack.config"); const trustedTypesConfig = require("../fixtures/overlay-config/trusted-types.webpack.config"); @@ -478,6 +479,55 @@ describe("overlay", () => { await server.stop(); }); + (isWebpack5 ? it : it.skip)( + "should open editor when error with file info is clicked", + async () => { + const mockLaunchEditorCb = jest.fn(); + jest.mock("launch-editor", () => mockLaunchEditorCb); + + const compiler = webpack(config); + const devServerOptions = { + port, + }; + const server = new Server(devServerOptions, compiler); + + await server.start(); + + const { page, browser } = await runBrowser(); + + await page.goto(`http://localhost:${port}/`, { + waitUntil: "networkidle0", + }); + + const pathToFile = path.resolve( + __dirname, + "../fixtures/overlay-config/foo.js" + ); + const originalCode = fs.readFileSync(pathToFile); + + fs.writeFileSync(pathToFile, "`;"); + + await page.waitForSelector("#webpack-dev-server-client-overlay"); + + const frame = page + .frames() + .find((item) => item.name() === "webpack-dev-server-client-overlay"); + + const errorHandle = await frame.$("[data-can-open]"); + + await errorHandle.click(); + + await waitForExpect(() => { + expect(mockLaunchEditorCb).toHaveBeenCalledTimes(1); + }); + + fs.writeFileSync(pathToFile, originalCode); + + await browser.close(); + await server.stop(); + } + ); + it('should not show a warning when "client.overlay" is "false"', async () => { const compiler = webpack(config); diff --git a/test/helpers/run-browser.js b/test/helpers/run-browser.js index c4b332ed49..bbd9dbff58 100644 --- a/test/helpers/run-browser.js +++ b/test/helpers/run-browser.js @@ -3,6 +3,16 @@ const puppeteer = require("puppeteer"); const { puppeteerArgs } = require("./puppeteer-constants"); +/** + * @typedef {Object} RunBrowserResult + * @property {import('puppeteer').Page} page + * @property {import('puppeteer').Browser} browser + */ + +/** + * @param {Parameters[0]} config + * @returns {Promise} + */ function runBrowser(config) { const options = { viewport: { @@ -14,7 +24,13 @@ function runBrowser(config) { }; return new Promise((resolve, reject) => { + /** + * @type {import('puppeteer').Page} + */ let page; + /** + * @type {import('puppeteer').Browser} + */ let browser; puppeteer