Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support Trusted Types for client overlay #4404

Merged
merged 9 commits into from May 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
248 changes: 119 additions & 129 deletions README.md

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions bin/cli-flags.js
Expand Up @@ -121,6 +121,21 @@ module.exports = {
simpleType: "boolean",
multiple: false,
},
"client-overlay-trusted-types-policy-name": {
configs: [
{
description:
"The name of a Trusted Types policy for the overlay. Defaults to 'webpack-dev-server#overlay'.",
multiple: false,
path: "client.overlay.trustedTypesPolicyName",
type: "string",
},
],
description:
"The name of a Trusted Types policy for the overlay. Defaults to 'webpack-dev-server#overlay'.",
multiple: false,
simpleType: "string",
},
"client-overlay-warnings": {
configs: [
{
Expand Down
12 changes: 9 additions & 3 deletions client-src/index.js
Expand Up @@ -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, trustedTypesPolicyName?: string }} overlay
* @property {string} [logging]
* @property {number} [reconnect]
*/
Expand Down Expand Up @@ -230,7 +230,10 @@ const onSocketMessage = {
: options.overlay && options.overlay.warnings;

if (needShowOverlayForWarnings) {
show("warning", warnings);
const trustedTypesPolicyName =
typeof options.overlay === "object" &&
options.overlay.trustedTypesPolicyName;
show("warning", warnings, trustedTypesPolicyName || null);
}

if (params && params.preventReloading) {
Expand Down Expand Up @@ -263,7 +266,10 @@ const onSocketMessage = {
: options.overlay && options.overlay.errors;

if (needShowOverlayForErrors) {
show("error", errors);
const trustedTypesPolicyName =
typeof options.overlay === "object" &&
options.overlay.trustedTypesPolicyName;
show("error", errors, trustedTypesPolicyName || null);
}
},
/**
Expand Down
31 changes: 25 additions & 6 deletions client-src/overlay.js
Expand Up @@ -23,10 +23,25 @@ let iframeContainerElement;
let containerElement;
/** @type {Array<(element: HTMLDivElement) => void>} */
let onLoadQueue = [];
/** @type {TrustedTypePolicy | undefined} */
let overlayTrustedTypesPolicy;

ansiHTML.setColors(colors);

function createContainer() {
/**
* @param {string | null} trustedTypesPolicyName
*/
function createContainer(trustedTypesPolicyName) {
// Enable Trusted Types if they are available in the current browser.
if (window.trustedTypes) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could optionally check whether the policyName is not null. However, I think using trustedTypes whenever they are available is better because:

  1. It is still noop and backwards compatible change for applications which do not enforce Trusted Types
  2. This enables Trusted Types integration without the need to change anything in webpack configuration (which can be hidden, such as in CRA). This means that the only change the application needs to do is whitelist the default policy name.

overlayTrustedTypesPolicy = window.trustedTypes.createPolicy(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case there is an error creating a Trusted Types policy the overlay is not shown at all.

A different approach would be to capture the exception outside this function (same place we define overlayTrustedTypesPolicy) and show an overlay without using innerHTML showing that the policy creation failed.

However I think having an error in the console is sufficient, since this is a development only feature so we can assume the console is opened anyways.

trustedTypesPolicyName || "webpack-dev-server#overlay",
{
createHTML: (value) => value,
}
);
}

iframeContainerElement = document.createElement("iframe");
iframeContainerElement.id = "webpack-dev-server-client-overlay";
iframeContainerElement.src = "about:blank";
Expand Down Expand Up @@ -109,8 +124,9 @@ function createContainer() {

/**
* @param {(element: HTMLDivElement) => void} callback
* @param {string | null} trustedTypesPolicyName
*/
function ensureOverlayExists(callback) {
function ensureOverlayExists(callback, trustedTypesPolicyName) {
if (containerElement) {
// Everything is ready, call the callback right away.
callback(containerElement);
Expand All @@ -124,7 +140,7 @@ function ensureOverlayExists(callback) {
return;
}

createContainer();
createContainer(trustedTypesPolicyName);
}

// Successful compilation.
Expand Down Expand Up @@ -178,8 +194,9 @@ function formatProblem(type, item) {
/**
* @param {string} type
* @param {Array<string | { file?: string, moduleName?: string, loc?: string, message?: string }>} messages
* @param {string | null} trustedTypesPolicyName
*/
function show(type, messages) {
function show(type, messages, trustedTypesPolicyName) {
ensureOverlayExists(() => {
messages.forEach((message) => {
const entryElement = document.createElement("div");
Expand All @@ -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"));
Expand All @@ -205,7 +224,7 @@ function show(type, messages) {
/** @type {HTMLDivElement} */
(containerElement).appendChild(entryElement);
});
});
}, trustedTypesPolicyName);
}

export { formatProblem, show, hide };
36 changes: 36 additions & 0 deletions examples/client/trusted-types-overlay/README.md
@@ -0,0 +1,36 @@
# client.overlay.trustedTypesPolicyName option

**webpack.config.js**

```js
module.exports = {
// ...
output: {
trustedTypes: { policyName: "webpack" },
},
devServer: {
client: {
overlay: {
trustedTypesPolicyName: "webpack#dev-overlay",
},
},
},
};
```

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.trustedTypesPolicyName` 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 webpack#dev-overlay".
```
6 changes: 6 additions & 0 deletions 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!";
38 changes: 38 additions & 0 deletions examples/client/trusted-types-overlay/layout.html
@@ -0,0 +1,38 @@
<!-- Originally copied from "../../.assets/layout.html" -->
<!DOCTYPE html>
<html>
<head>
<!-- Enable Trusted Types -->
<meta
http-equiv="Content-Security-Policy"
content="require-trusted-types-for 'script'; trusted-types webpack webpack#dev-overlay;"
/>

<title>WDS ▻ <%= htmlWebpackPlugin.options.title %></title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="shortcut icon" href=".assets/favicon.ico" />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Source+Code+Pro:400,600|Source+Sans+Pro:400,400i,500,600"
/>
<link rel="stylesheet" href=".assets/style.css" />
</head>
<body>
<main>
<header>
<h1>
<img
src=".assets/icon-square.svg"
style="width: 35px; height: 35px"
/>
webpack-dev-server
</h1>
</header>
<section>
<h2><%= htmlWebpackPlugin.options.title %></h2>
<div id="target"></div>
</section>
</main>
</body>
</html>
32 changes: 32 additions & 0 deletions 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: {
trustedTypesPolicyName: "webpack#dev-overlay",
},
},
},
});

// 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;
4 changes: 4 additions & 0 deletions lib/options.json
Expand Up @@ -110,6 +110,10 @@
"cli": {
"negatedDescription": "Disables the full-screen overlay in the browser when there are compiler warnings."
}
},
"trustedTypesPolicyName": {
"description": "The name of a Trusted Types policy for the overlay. Defaults to 'webpack-dev-server#overlay'.",
"type": "string"
}
}
}
Expand Down
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Expand Up @@ -78,6 +78,7 @@
"@types/default-gateway": "^3.0.1",
"@types/rimraf": "^3.0.2",
"@types/sockjs-client": "^1.5.1",
"@types/trusted-types": "^2.0.2",
"acorn": "^8.2.4",
"babel-jest": "^27.5.1",
"babel-loader": "^8.2.4",
Expand Down
6 changes: 3 additions & 3 deletions test/__snapshots__/validate-options.test.js.snap.webpack5
Expand Up @@ -90,19 +90,19 @@ exports[`options validate should throw an error on the "client" option with '{"o
-> Read more at https://webpack.js.org/configuration/dev-server/#devserverclient
Details:
* options.client.overlay should be one of these:
boolean | object { errors?, warnings? }
boolean | object { errors?, warnings?, trustedTypesPolicyName? }
Details:
* options.client.overlay should be a boolean.
-> Enables a full-screen overlay in the browser when there are compiler errors or warnings.
-> Read more at https://webpack.js.org/configuration/dev-server/#overlay
* options.client.overlay should be an object:
object { errors?, warnings? }"
object { errors?, warnings?, trustedTypesPolicyName? }"
`;

exports[`options validate should throw an error on the "client" option with '{"overlay":{"arbitrary":""}}' value 1`] = `
"ValidationError: Invalid options object. Dev Server has been initialized using an options object that does not match the API schema.
- options.client.overlay has an unknown property 'arbitrary'. These properties are valid:
object { errors?, warnings? }"
object { errors?, warnings?, trustedTypesPolicyName? }"
`;

exports[`options validate should throw an error on the "client" option with '{"overlay":{"errors":""}}' value 1`] = `
Expand Down