diff --git a/client-src/index.js b/client-src/index.js index 6f361db83b..dee427ae29 100644 --- a/client-src/index.js +++ b/client-src/index.js @@ -15,7 +15,7 @@ import createSocketURL from "./utils/createSocketURL.js"; * @property {boolean} hot * @property {boolean} liveReload * @property {boolean} progress - * @property {boolean | { warnings?: boolean, errors?: boolean }} overlay + * @property {boolean | { warnings?: boolean, errors?: boolean, policyName?: string }} overlay * @property {string} [logging] * @property {number} [reconnect] */ @@ -230,7 +230,9 @@ const onSocketMessage = { : options.overlay && options.overlay.warnings; if (needShowOverlayForWarnings) { - show("warning", warnings); + const policyName = + typeof options.overlay === "object" && options.overlay.policyName; + show("warning", warnings, policyName || null); } if (params && params.preventReloading) { @@ -263,7 +265,9 @@ const onSocketMessage = { : options.overlay && options.overlay.errors; if (needShowOverlayForErrors) { - show("error", errors); + const policyName = + typeof options.overlay === "object" && options.overlay.policyName; + show("error", errors, policyName || null); } }, /** diff --git a/client-src/overlay.js b/client-src/overlay.js index 5c074702ff..1c671b9243 100644 --- a/client-src/overlay.js +++ b/client-src/overlay.js @@ -23,10 +23,25 @@ let iframeContainerElement; let containerElement; /** @type {Array<(element: HTMLDivElement) => void>} */ let onLoadQueue = []; +/** @type {any} */ +let overlayTrustedTypesPolicy; ansiHTML.setColors(colors); -function createContainer() { +/** + * @param {string | null} policyName + */ +function createContainer(policyName) { + // Enable Trusted Types if they are available in the current browser. + if (window.trustedTypes) { + overlayTrustedTypesPolicy = window.trustedTypes.createPolicy( + policyName || "webpack-dev-server#overlay", + { + createHTML: (value) => value, + } + ); + } + iframeContainerElement = document.createElement("iframe"); iframeContainerElement.id = "webpack-dev-server-client-overlay"; iframeContainerElement.src = "about:blank"; @@ -109,8 +124,9 @@ function createContainer() { /** * @param {(element: HTMLDivElement) => void} callback + * @param {string | null} policyName */ -function ensureOverlayExists(callback) { +function ensureOverlayExists(callback, policyName) { if (containerElement) { // Everything is ready, call the callback right away. callback(containerElement); @@ -124,7 +140,7 @@ function ensureOverlayExists(callback) { return; } - createContainer(); + createContainer(policyName); } // Successful compilation. @@ -178,8 +194,9 @@ function formatProblem(type, item) { /** * @param {string} type * @param {Array} messages + * @param {string | null} policyName */ -function show(type, messages) { +function show(type, messages, policyName) { ensureOverlayExists(() => { messages.forEach((message) => { const entryElement = document.createElement("div"); @@ -193,7 +210,9 @@ function show(type, messages) { const text = ansiHTML(encode(body)); const messageTextNode = document.createElement("div"); - messageTextNode.innerHTML = text; + messageTextNode.innerHTML = overlayTrustedTypesPolicy + ? overlayTrustedTypesPolicy.createHTML(text) + : text; entryElement.appendChild(typeElement); entryElement.appendChild(document.createElement("br")); @@ -205,7 +224,7 @@ function show(type, messages) { /** @type {HTMLDivElement} */ (containerElement).appendChild(entryElement); }); - }); + }, policyName); } export { formatProblem, show, hide }; diff --git a/examples/client/trusted-types-overlay/README.md b/examples/client/trusted-types-overlay/README.md new file mode 100644 index 0000000000..b6cbd447b7 --- /dev/null +++ b/examples/client/trusted-types-overlay/README.md @@ -0,0 +1,36 @@ +# client.overlay.policyName option + +**webpack.config.js** + +```js +module.exports = { + // ... + output: { + trustedTypes: { policyName: "webpack" }, + }, + devServer: { + client: { + overlay: { + policyName: "overlay-policy", + }, + }, + }, +}; +``` + +Usage via CLI: + +```shell +npx webpack serve --open +``` + +## What Should Happen + +1. The script should open `http://localhost:8080/` in your default browser. +2. You should see an overlay in browser for compilation errors. +3. Modify `devServer.client.overlay.policyName` in webpack.config.js to `disallowed-policy` and save. +4. Restart the command and you should not see an overlay at all. In the console you should see the following error: + +``` +Refused to create a TrustedTypePolicy named 'disallowed-policy' because it violates the following Content Security Policy directive: "trusted-types webpack overlay-policy". +``` diff --git a/examples/client/trusted-types-overlay/app.js b/examples/client/trusted-types-overlay/app.js new file mode 100644 index 0000000000..0c5dbd0635 --- /dev/null +++ b/examples/client/trusted-types-overlay/app.js @@ -0,0 +1,6 @@ +"use strict"; + +const target = document.querySelector("#target"); + +target.classList.add("pass"); +target.textContent = "Success!"; diff --git a/examples/client/trusted-types-overlay/layout.html b/examples/client/trusted-types-overlay/layout.html new file mode 100644 index 0000000000..6787082976 --- /dev/null +++ b/examples/client/trusted-types-overlay/layout.html @@ -0,0 +1,38 @@ + + + + + + + + WDS ▻ <%= htmlWebpackPlugin.options.title %> + + + + + + + +
+
+

+ + webpack-dev-server +

+
+
+

<%= htmlWebpackPlugin.options.title %>

+
+
+
+ + diff --git a/examples/client/trusted-types-overlay/webpack.config.js b/examples/client/trusted-types-overlay/webpack.config.js new file mode 100644 index 0000000000..17a3ef73a0 --- /dev/null +++ b/examples/client/trusted-types-overlay/webpack.config.js @@ -0,0 +1,32 @@ +"use strict"; + +const path = require("path"); +const HtmlWebpackPlugin = require("html-webpack-plugin"); +// our setup function adds behind-the-scenes bits to the config that all of our +// examples need +const { setup } = require("../../util"); + +const config = setup({ + context: __dirname, + // create error for overlay + entry: "./invalid.js", + output: { + trustedTypes: { policyName: "webpack" }, + }, + devServer: { + client: { + overlay: { + policyName: "overlay-policy", + }, + }, + }, +}); + +// overwrite the index.html with our own to enable Trusted Types +config.plugins[0] = new HtmlWebpackPlugin({ + filename: "index.html", + template: path.join(__dirname, "./layout.html"), + title: "trusted types overlay", +}); + +module.exports = config; diff --git a/lib/options.json b/lib/options.json index 32b1c84ca1..84c0a57626 100644 --- a/lib/options.json +++ b/lib/options.json @@ -110,6 +110,10 @@ "cli": { "negatedDescription": "Disables the full-screen overlay in the browser when there are compiler warnings." } + }, + "policyName": { + "description": "The name of a Trusted Types policy for the overlay. Defaults to 'webpack-dev-server#overlay'.", + "type": "string" } } } diff --git a/test/e2e/__snapshots__/overlay.test.js.snap.webpack5 b/test/e2e/__snapshots__/overlay.test.js.snap.webpack5 index c8a56d4012..7c4fdc318b 100644 --- a/test/e2e/__snapshots__/overlay.test.js.snap.webpack5 +++ b/test/e2e/__snapshots__/overlay.test.js.snap.webpack5 @@ -364,6 +364,14 @@ exports[`overlay should not show initially, then show on an error, then show oth " `; +exports[`overlay should not show overlay when Trusted Types are enabled, but policy is not allowed: page html 1`] = ` +" +

webpack-dev-server is running...

+ + +" +`; + exports[`overlay should show a warning after invalidation: overlay html 1`] = ` "
" `; + +exports[`overlay should show overlay when Trusted Types are enabled: overlay html 1`] = ` +" +
+ Compiled with problems:

+
+ ERROR

+
Error from compilation. Can't find 'test' module.
+

+
+
+ +" +`; + +exports[`overlay should show overlay when Trusted Types are enabled: page html 1`] = ` +" +

webpack-dev-server is running...

+ + + + +" +`; diff --git a/test/e2e/overlay.test.js b/test/e2e/overlay.test.js index c9e91865e9..fc3c41371f 100644 --- a/test/e2e/overlay.test.js +++ b/test/e2e/overlay.test.js @@ -6,6 +6,7 @@ const prettier = require("prettier"); const webpack = require("webpack"); const Server = require("../../lib/Server"); const config = require("../fixtures/overlay-config/webpack.config"); +const trustedTypesConfig = require("../fixtures/overlay-config/trusted-types.webpack.config"); const runBrowser = require("../helpers/run-browser"); const port = require("../ports-map").overlay; @@ -772,6 +773,81 @@ describe("overlay", () => { await server.stop(); }); + it("should show overlay when Trusted Types are enabled", async () => { + const compiler = webpack(trustedTypesConfig); + + new ErrorPlugin().apply(compiler); + + const devServerOptions = { + port, + client: { + overlay: { + policyName: "overlay-policy", + }, + }, + }; + const server = new Server(devServerOptions, compiler); + + await server.start(); + + const { page, browser } = await runBrowser(); + + await page.goto(`http://localhost:${port}/`, { + waitUntil: "networkidle0", + }); + + const pageHtml = await page.evaluate(() => document.body.outerHTML); + const overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + const overlayFrame = await overlayHandle.contentFrame(); + const overlayHtml = await overlayFrame.evaluate( + () => document.body.outerHTML + ); + + expect(prettier.format(pageHtml, { parser: "html" })).toMatchSnapshot( + "page html" + ); + expect(prettier.format(overlayHtml, { parser: "html" })).toMatchSnapshot( + "overlay html" + ); + + await browser.close(); + await server.stop(); + }); + + it("should not show overlay when Trusted Types are enabled, but policy is not allowed", async () => { + const compiler = webpack(trustedTypesConfig); + + new ErrorPlugin().apply(compiler); + + const devServerOptions = { + port, + client: { + overlay: { + policyName: "disallowed-policy", + }, + }, + }; + const server = new Server(devServerOptions, compiler); + + await server.start(); + + const { page, browser } = await runBrowser(); + + await page.goto(`http://localhost:${port}/`, { + waitUntil: "networkidle0", + }); + + const pageHtml = await page.evaluate(() => document.body.outerHTML); + const overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + expect(overlayHandle).toBe(null); + expect(prettier.format(pageHtml, { parser: "html" })).toMatchSnapshot( + "page html" + ); + + await browser.close(); + await server.stop(); + }); + it('should show an error when "client.overlay.errors" is "true"', async () => { const compiler = webpack(config); diff --git a/test/fixtures/overlay-config/trusted-types.webpack.config.js b/test/fixtures/overlay-config/trusted-types.webpack.config.js new file mode 100644 index 0000000000..ca53618cfc --- /dev/null +++ b/test/fixtures/overlay-config/trusted-types.webpack.config.js @@ -0,0 +1,28 @@ +"use strict"; + +const webpack = require("webpack"); +const HTMLGeneratorPlugin = require("../../helpers/trusted-types-html-generator-plugin"); + +const isWebpack5 = webpack.version.startsWith("5"); + +module.exports = { + mode: "development", + context: __dirname, + stats: "none", + entry: "./foo.js", + output: { + path: "/", + trustedTypes: { policyName: "webpack" }, + }, + infrastructureLogging: isWebpack5 + ? { + level: "info", + stream: { + write: () => {}, + }, + } + : { + level: "info", + }, + plugins: [new HTMLGeneratorPlugin()], +}; diff --git a/test/helpers/trusted-types-html-generator-plugin.js b/test/helpers/trusted-types-html-generator-plugin.js new file mode 100644 index 0000000000..3a3ee6d189 --- /dev/null +++ b/test/helpers/trusted-types-html-generator-plugin.js @@ -0,0 +1,82 @@ +"use strict"; + +const HTMLContentForIndex = ` + + + + + + webpack-dev-server + + +

webpack-dev-server is running...

+ + + +`; + +const HTMLContentForTest = ` + + + + + + test + + +

Created via HTMLGeneratorPlugin

+ + +`; + +module.exports = class HTMLGeneratorPlugin { + // eslint-disable-next-line class-methods-use-this + apply(compiler) { + const pluginName = "html-generator-plugin"; + + compiler.hooks.thisCompilation.tap(pluginName, (compilation) => { + if (compiler.webpack) { + const { RawSource } = compiler.webpack.sources; + + compilation.hooks.processAssets.tap( + { + name: pluginName, + stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL, + }, + () => { + const indexSource = new RawSource(HTMLContentForIndex); + const testSource = new RawSource(HTMLContentForTest); + + compilation.emitAsset("index.html", indexSource); + compilation.emitAsset("test.html", testSource); + } + ); + } else { + compilation.hooks.additionalAssets.tap(pluginName, () => { + compilation.emitAsset("index.html", { + source() { + return HTMLContentForIndex; + }, + size() { + return HTMLContentForIndex.length; + }, + }); + compilation.emitAsset("test.html", { + source() { + return HTMLContentForTest; + }, + size() { + return HTMLContentForTest.length; + }, + }); + }); + } + }); + } +}; diff --git a/types/lib/Server.d.ts b/types/lib/Server.d.ts index 82272d80c8..4ea229a77c 100644 --- a/types/lib/Server.d.ts +++ b/types/lib/Server.d.ts @@ -1610,6 +1610,10 @@ declare class Server { negatedDescription: string; }; }; + policyName: { + description: string; + type: string; + }; }; description?: undefined; link?: undefined; @@ -1625,93 +1629,96 @@ declare class Server { negatedDescription: string; }; }; + /** + * @typedef {{ implementation: WebSocketServer, clients: ClientConnection[] }} WebSocketServerImplementation + */ + /** + * @callback ByPass + * @param {Request} req + * @param {Response} res + * @param {ProxyConfigArrayItem} proxyConfig + */ + /** + * @typedef {{ path?: HttpProxyMiddlewareOptionsFilter | undefined, context?: HttpProxyMiddlewareOptionsFilter | undefined } & { bypass?: ByPass } & HttpProxyMiddlewareOptions } ProxyConfigArrayItem + */ + /** + * @typedef {(ProxyConfigArrayItem | ((req?: Request | undefined, res?: Response | undefined, next?: NextFunction | undefined) => ProxyConfigArrayItem))[]} ProxyConfigArray + */ + /** + * @typedef {{ [url: string]: string | ProxyConfigArrayItem }} ProxyConfigMap + */ + /** + * @typedef {Object} OpenApp + * @property {string} [name] + * @property {string[]} [arguments] + */ + /** + * @typedef {Object} Open + * @property {string | string[] | OpenApp} [app] + * @property {string | string[]} [target] + */ + /** + * @typedef {Object} NormalizedOpen + * @property {string} target + * @property {import("open").Options} options + */ + /** + * @typedef {Object} WebSocketURL + * @property {string} [hostname] + * @property {string} [password] + * @property {string} [pathname] + * @property {number | string} [port] + * @property {string} [protocol] + * @property {string} [username] + */ + /** + * @typedef {Object} ClientConfiguration + * @property {"log" | "info" | "warn" | "error" | "none" | "verbose"} [logging] + * @property {boolean | { warnings?: boolean, errors?: boolean }} [overlay] + * @property {boolean} [progress] + * @property {boolean | number} [reconnect] + * @property {"ws" | "sockjs" | string} [webSocketTransport] + * @property {string | WebSocketURL} [webSocketURL] + */ + /** + * @typedef {Array<{ key: string; value: string }> | Record} Headers + */ + /** + * @typedef {{ name?: string, path?: string, middleware: ExpressRequestHandler | ExpressErrorRequestHandler } | ExpressRequestHandler | ExpressErrorRequestHandler} Middleware + */ + /** + * @typedef {Object} Configuration + * @property {boolean | string} [ipc] + * @property {Host} [host] + * @property {Port} [port] + * @property {boolean | "only"} [hot] + * @property {boolean} [liveReload] + * @property {DevMiddlewareOptions} [devMiddleware] + * @property {boolean} [compress] + * @property {boolean} [magicHtml] + * @property {"auto" | "all" | string | string[]} [allowedHosts] + * @property {boolean | ConnectHistoryApiFallbackOptions} [historyApiFallback] + * @property {boolean} [setupExitSignals] + * @property {boolean | Record | BonjourOptions} [bonjour] + * @property {string | string[] | WatchFiles | Array} [watchFiles] + * @property {boolean | string | Static | Array} [static] + * @property {boolean | ServerOptions} [https] + * @property {boolean} [http2] + * @property {"http" | "https" | "spdy" | string | ServerConfiguration} [server] + * @property {boolean | "sockjs" | "ws" | string | WebSocketServerConfiguration} [webSocketServer] + * @property {ProxyConfigMap | ProxyConfigArrayItem | ProxyConfigArray} [proxy] + * @property {boolean | string | Open | Array} [open] + * @property {boolean} [setupExitSignals] + * @property {boolean | ClientConfiguration} [client] + * @property {Headers | ((req: Request, res: Response, context: DevMiddlewareContext) => Headers)} [headers] + * @property {(devServer: Server) => void} [onAfterSetupMiddleware] + * @property {(devServer: Server) => void} [onBeforeSetupMiddleware] + * @property {(devServer: Server) => void} [onListening] + * @property {(middlewares: Middleware[], devServer: Server) => Middleware[]} [setupMiddlewares] + */ ClientReconnect: { description: string; link: string; - /** - * @callback ByPass - * @param {Request} req - * @param {Response} res - * @param {ProxyConfigArrayItem} proxyConfig - */ - /** - * @typedef {{ path?: HttpProxyMiddlewareOptionsFilter | undefined, context?: HttpProxyMiddlewareOptionsFilter | undefined } & { bypass?: ByPass } & HttpProxyMiddlewareOptions } ProxyConfigArrayItem - */ - /** - * @typedef {(ProxyConfigArrayItem | ((req?: Request | undefined, res?: Response | undefined, next?: NextFunction | undefined) => ProxyConfigArrayItem))[]} ProxyConfigArray - */ - /** - * @typedef {{ [url: string]: string | ProxyConfigArrayItem }} ProxyConfigMap - */ - /** - * @typedef {Object} OpenApp - * @property {string} [name] - * @property {string[]} [arguments] - */ - /** - * @typedef {Object} Open - * @property {string | string[] | OpenApp} [app] - * @property {string | string[]} [target] - */ - /** - * @typedef {Object} NormalizedOpen - * @property {string} target - * @property {import("open").Options} options - */ - /** - * @typedef {Object} WebSocketURL - * @property {string} [hostname] - * @property {string} [password] - * @property {string} [pathname] - * @property {number | string} [port] - * @property {string} [protocol] - * @property {string} [username] - */ - /** - * @typedef {Object} ClientConfiguration - * @property {"log" | "info" | "warn" | "error" | "none" | "verbose"} [logging] - * @property {boolean | { warnings?: boolean, errors?: boolean }} [overlay] - * @property {boolean} [progress] - * @property {boolean | number} [reconnect] - * @property {"ws" | "sockjs" | string} [webSocketTransport] - * @property {string | WebSocketURL} [webSocketURL] - */ - /** - * @typedef {Array<{ key: string; value: string }> | Record} Headers - */ - /** - * @typedef {{ name?: string, path?: string, middleware: ExpressRequestHandler | ExpressErrorRequestHandler } | ExpressRequestHandler | ExpressErrorRequestHandler} Middleware - */ - /** - * @typedef {Object} Configuration - * @property {boolean | string} [ipc] - * @property {Host} [host] - * @property {Port} [port] - * @property {boolean | "only"} [hot] - * @property {boolean} [liveReload] - * @property {DevMiddlewareOptions} [devMiddleware] - * @property {boolean} [compress] - * @property {boolean} [magicHtml] - * @property {"auto" | "all" | string | string[]} [allowedHosts] - * @property {boolean | ConnectHistoryApiFallbackOptions} [historyApiFallback] - * @property {boolean} [setupExitSignals] - * @property {boolean | Record | BonjourOptions} [bonjour] - * @property {string | string[] | WatchFiles | Array} [watchFiles] - * @property {boolean | string | Static | Array} [static] - * @property {boolean | ServerOptions} [https] - * @property {boolean} [http2] - * @property {"http" | "https" | "spdy" | string | ServerConfiguration} [server] - * @property {boolean | "sockjs" | "ws" | string | WebSocketServerConfiguration} [webSocketServer] - * @property {ProxyConfigMap | ProxyConfigArrayItem | ProxyConfigArray} [proxy] - * @property {boolean | string | Open | Array} [open] - * @property {boolean} [setupExitSignals] - * @property {boolean | ClientConfiguration} [client] - * @property {Headers | ((req: Request, res: Response, context: DevMiddlewareContext) => Headers)} [headers] - * @property {(devServer: Server) => void} [onAfterSetupMiddleware] - * @property {(devServer: Server) => void} [onBeforeSetupMiddleware] - * @property {(devServer: Server) => void} [onListening] - * @property {(middlewares: Middleware[], devServer: Server) => Middleware[]} [setupMiddlewares] - */ anyOf: ( | { type: string; @@ -1764,9 +1771,6 @@ declare class Server { description: string; type: string; }; - /** - * @typedef {Array<{ key: string; value: string }> | Record} Headers - */ password: { description: string; type: string; @@ -1880,10 +1884,6 @@ declare class Server { } | { instanceof: string; - /** - * @private - * @type {RequestHandler[]} - */ type?: undefined; items?: undefined; } @@ -2043,9 +2043,6 @@ declare class Server { instanceof?: undefined; } )[]; - /** - * @returns {string} - */ }; instanceof?: undefined; } @@ -2075,6 +2072,9 @@ declare class Server { properties: { key: { description: string; + /** + * @type {string[]} + */ type: string; }; value: { @@ -2086,9 +2086,6 @@ declare class Server { exclude: boolean; }; }; - /** - * @type {string[]} - */ Headers: { anyOf: ( | { @@ -2120,20 +2117,20 @@ declare class Server { | { type: string; cli: { - negatedDescription: string; + negatedDescription: string /** @type {{ type: WebSocketServerConfiguration["type"], options: NonNullable }} */; }; description?: undefined; link?: undefined; } | { type: string; - /** @type {ClientConfiguration} */ description: string; + description: string; link: string; cli?: undefined /** @typedef {import("express").Request} Request */; } )[]; - description: string; - link: string; + /** @type {string} */ description: string; + link: string /** @type {ServerConfiguration} */; }; Host: { description: string; @@ -2167,7 +2164,7 @@ declare class Server { } )[]; description: string; - /** @type {string} */ link: string; + link: string; }; IPC: { anyOf: ( @@ -2316,6 +2313,7 @@ declare class Server { type: string; minLength: number; }; + /** @type {any} */ Port: { anyOf: ( | { @@ -2326,7 +2324,6 @@ declare class Server { enum?: undefined; } | { - /** @type {any} */ type: string; minLength: number; minimum?: undefined; @@ -2356,6 +2353,10 @@ declare class Server { anyOf: ( | { type: string; + /** + * @private + * @returns {Compiler["options"]} + */ instanceof?: undefined; } | { @@ -2382,7 +2383,7 @@ declare class Server { ServerEnum: { enum: string[]; cli: { - exclude: boolean /** @type {MultiCompiler} */; + exclude: boolean; }; }; ServerString: { @@ -2404,7 +2405,7 @@ declare class Server { $ref: string; }; }; - additionalProperties: boolean; + additionalProperties: boolean /** @type {Compiler} */; }; ServerOptions: { type: string; @@ -2782,7 +2783,7 @@ declare class Server { | { type: string; minLength: number; - /** @type {ServerConfiguration} */ items?: undefined; + items?: undefined; } )[]; description: string; @@ -2804,7 +2805,7 @@ declare class Server { anyOf: { $ref: string; }[]; - /** @type {ServerOptions} */ description: string; + description: string; link: string; }; WebSocketServerType: { @@ -2827,13 +2828,17 @@ declare class Server { )[]; cli: { description: string; - }; + } /** @type {any} */; }; WebSocketServerFunction: { instanceof: string; }; WebSocketServerObject: { type: string; + /** + * @param {string | Buffer | undefined} item + * @returns {string | Buffer | undefined} + */ properties: { type: { anyOf: { @@ -2869,6 +2874,7 @@ declare class Server { compress: { $ref: string; }; + /** @type {ServerOptions} */ devMiddleware: { $ref: string; };