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

WIP: add dev-server-hmr #685

Merged
merged 34 commits into from
Nov 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
e97c564
add dev-server-hmr wip
43081j Oct 4, 2020
fd2e173
add a simple dependency tree
43081j Oct 4, 2020
fcb76dd
add some comments
43081j Oct 4, 2020
7536a08
build a better dep tree
43081j Oct 4, 2020
b4bcabb
clear dependencies when files are served
43081j Oct 4, 2020
e983a59
add debug logging
43081j Oct 4, 2020
bd4a326
fix up message typing
43081j Oct 10, 2020
42d7bad
add client script
43081j Oct 11, 2020
416707a
add client implementation
43081j Oct 11, 2020
82c97f6
remove unused imports
43081j Oct 11, 2020
1b9921a
resolve imports correctly
43081j Oct 11, 2020
b9582da
run format
43081j Oct 11, 2020
b0b93e4
hmr enum
43081j Oct 11, 2020
38c5118
remove unused code
43081j Oct 17, 2020
f7fa4fe
use shorthand content type
43081j Oct 18, 2020
65b1cfa
reuse existing websocket
43081j Oct 18, 2020
878b9a8
revert protocol option
43081j Oct 18, 2020
68d77f2
drop unused append
43081j Oct 18, 2020
0c27d8c
use parse5-utils
43081j Oct 18, 2020
533834d
add tests for hmr plugin
43081j Oct 24, 2020
cb8cb62
simplify update calls
43081j Oct 24, 2020
e8b39ca
drop unused import
43081j Oct 24, 2020
2a8cb7d
relocate the client path
43081j Oct 24, 2020
f4ef737
run format
43081j Oct 24, 2020
039ea1c
add parse5-utils as a dependency
43081j Oct 24, 2020
1df871e
add some minimal docs
43081j Oct 26, 2020
f77207f
add examples
43081j Oct 26, 2020
377e656
update docs
43081j Oct 26, 2020
b430e05
add demo
43081j Oct 31, 2020
6aa7bdb
enable watch mode
43081j Oct 31, 2020
ff3a6b9
revert watch change
43081j Oct 31, 2020
e03c0c4
fix: reuse existing hmr clients
43081j Oct 31, 2020
d690232
fix tests
43081j Oct 31, 2020
363b3ee
use a regex instead
43081j Oct 31, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 194 additions & 0 deletions docs/docs/dev-server/plugins/hmr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
# Dev Server >> Plugins >> Hot Module Replacement ||7

> **Warning:** this plugin is still experimental and may change until it
> reaches a stable release.

Plugin for introducing HMR (hot module replacement) support.

Modules can be written to consume the provided HMR API at development
time, allowing for them to update without reloading the page.

## Installation

Install the package:

```
npm i --save-dev @web/dev-server-hmr
```

Add the plugin to your `web-dev-server-config.mjs` or `web-test-runner.config.js`:

```ts
import { hmrPlugin } from '@web/dev-server-hmr';

export default {
plugins: [hmrPlugin()],
};
```

## Basic usage

When the plugin is loaded, any HMR-compatible module will have the HMR API
made available to it via `import.meta.hot`.

By default, it will effectively do nothing until you have written code
which consumes this API.

For example, take the following module:

```ts
/** Adds two numbers */
export let add = (a, b) => {
return a + b;
};
```

In its current state, it will _not_ be HMR-compatible as it does not reference
the HMR API. This means if our `add` module changes, the HMR plugin will
trigger a full page reload.

To make it compatible, we must use the HMR API via `import.meta.hot`:

```ts
/** Adds two numbers */
export let add = (a, b) => {
return a + b;
};

if (import.meta.hot) {
import.meta.hot.accept(({ module }) => {
add = module.add;
});
}
```

The plugin will detect that your module uses the HMR API and will make the
`import.meta.hot` object available.

Do note that in our example we wrapped this in an `if` statement. The reason
for this is to account for if the plugin has not been loaded.

## Note about production

In production it is highly recommended you remove any of these HMR related
blocks of code as they will effectively be dead code.

## Use with libraries/frameworks

This plugin exists primarily to serve as a base to other
framework/ecosystem-specific implementations of HMR.

It can be consumed directly as-is, but in future should usually be
used via another higher level plugin layered on top of this.

## API

### `import.meta.hot.accept()`

Calling `accept` without a callback will notify the plugin that your module
accepts updates, but will not deal with the updates.

This is only really useful if your module is one which has side-effects
and does not need mutating on update (i.e. has no exports).

Example:

```ts
// will be set each time the module updates as a side-effect
window.someGlobal = 303;
import.meta.hot.accept();
```

### `import.meta.hot.accept(({ module }) => { ... })`

If you pass a callback to `accept`, it will be passed the updated module
any time an update occurs.

At this point, you should usually update any exports to be those on the
new module.

Example:

```ts
export let foo = 808;
import.meta.hot.accept(({ module }) => {
foo = module.foo;
});
```

### `import.meta.hot.accept(['./dep1.js', './dep2.js'], ({ deps, module }) => { ... })`

If you specify a list of dependencies as well as a callback, your callback
will be provided with the up-to-date version of each of those modules.

This can be useful if your updates require access to dependencies of the
current module.

Example:

```ts
import { add } from './add.js';

export let foo = add(10, 10);

import.meta.hot.accept(['./add.js'], ({ deps, module }) => {
foo = deps[0].add(10, 10);
});
```

### `import.meta.hot.invalidate()`

Immediately invalidates the current module which will then lead to reloading
the page.

Example:

```ts
export let foo = 303;

import.meta.hot.accept(({ module }) => {
if (!module.foo) {
import.meta.hot.invalidate();
} else {
foo = module.foo;
}
});
```

### `import.meta.hot.decline()`

Notifies the server that you do not support updates, meaning any updates
will result in a full page reload.

This may be useful when your module makes global changes (side effects) which
cannot be re-done.

Example:

```ts
doSomethingGlobal();

import.meta.hot.decline();
```

### `import.meta.dispose(() => { ... })`

Specifies a callback to be called when the current module is disposed of,
before the new module is loaded and passed to `accept()`.

Example:

```ts
export let foo = new Server();

foo.start();

import.meta.hot.accept(({ module }) => {
foo = module.foo;
foo.start();
});

import.meta.dispose(() => {
foo.stop();
});
```
1 change: 1 addition & 0 deletions docs/docs/dev-server/plugins/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ See
- [rollup](./rollup.md)
- [import-maps](./import-maps.md)
- [legacy](./legacy.md)
- [hmr](./hmr.md)

If you have more specific needs it's best to [write your own plugin](../writing-plugins/overview.md).
1 change: 1 addition & 0 deletions packages/dev-server-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
],
"dependencies": {
"@types/ws": "^7.2.6",
"@web/parse5-utils": "^1.0.0",
"chokidar": "^3.4.0",
"clone": "^2.1.2",
"es-module-lexer": "^0.3.24",
Expand Down
2 changes: 1 addition & 1 deletion packages/dev-server-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export { WebSocket };
export { DevServer } from './server/DevServer';
export { Plugin, ServerStartParams } from './Plugin';
export { DevServerCoreConfig, MimeTypeMappings } from './DevServerCoreConfig';
export { WebSocketsManager } from './web-sockets/WebSocketsManager';
export { WebSocketsManager, WebSocketData } from './web-sockets/WebSocketsManager';
export {
getRequestBrowserPath,
getRequestFilePath,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { EventEmitter } from './EventEmitter';

export const NAME_WEB_SOCKET_IMPORT = '/__web-dev-server__web-socket.js';

type WebSocketData = { type: string } & Record<string, unknown>;
export type WebSocketData = { type: string } & Record<string, unknown>;

export interface Events {
message: { webSocket: WebSocket; data: WebSocketData };
Expand Down
16 changes: 2 additions & 14 deletions packages/dev-server-core/src/web-sockets/webSocketsPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { parse as parseHtml } from 'parse5';
import { query, predicates } from '../dom5';
import { Plugin } from '../Plugin';
import { NAME_WEB_SOCKET_IMPORT } from './WebSocketsManager';
import { appendToDocument } from '@web/parse5-utils';

export const webSocketScript = `<!-- injected by web-dev-server -->
<script type="module" src="${NAME_WEB_SOCKET_IMPORT}"></script>`;
Expand Down Expand Up @@ -90,18 +89,7 @@ if (webSocket) {

async transform(context) {
if (context.response.is('html')) {
const documentAst = parseHtml(context.body, { sourceCodeLocationInfo: true });
const htmlNode = query(documentAst, predicates.hasTagName('html'));
const bodyNode = query(documentAst, predicates.hasTagName('body'));
if (!htmlNode?.sourceCodeLocation || !bodyNode?.sourceCodeLocation) {
// if html or body tag does not have a source code location it was generated
return;
}

const { startOffset } = bodyNode.sourceCodeLocation.endTag;
const start = context.body.substring(0, startOffset);
const end = context.body.substring(startOffset);
return `${start}\n\n${webSocketScript}\n\n${end}`;
return appendToDocument(context.body, webSocketScript);
}
},
};
Expand Down
5 changes: 5 additions & 0 deletions packages/dev-server-hmr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Dev server HMR

Plugin for using HMR (hot module reload) in the dev server.

See [our website](https://modern-web.dev/docs/dev-server/plugins/hmr/) for full documentation.
48 changes: 48 additions & 0 deletions packages/dev-server-hmr/demo/component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const renderedComponents = new Set();

export class MyComponent extends HTMLElement {
static styles = `
h1 { color: hotpink; }
p { color: olivegreen; }
`;

static template = `
<h1>Foo</h1>
<p>Bar</p>
`;

constructor() {
super();
this.attachShadow({ mode: 'open' });
}

connectedCallback() {
renderedComponents.add(this);
this.render();
}

disconnectedCallback() {
renderedComponents.delete(this);
}

render() {
this.shadowRoot.innerHTML = `
<style>${MyComponent.styles}</style>
${MyComponent.template}
`;
}
}

if (import.meta.hot) {
import.meta.hot.accept(({ module }) => {
MyComponent.styles = module.MyComponent.styles;
MyComponent.template = module.MyComponent.template;
for (const component of renderedComponents) {
component.render();
}
});
}

if (!window.customElements.get('my-component')) {
window.customElements.define('my-component', MyComponent);
}
8 changes: 8 additions & 0 deletions packages/dev-server-hmr/demo/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<html>
<head></head>

<body>
<my-component></my-component>
<script type="module" src="./component.js"></script>
</body>
</html>
6 changes: 6 additions & 0 deletions packages/dev-server-hmr/demo/server.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const { hmrPlugin } = require('../dist/index');

module.exports = {
rootDir: '.',
plugins: [hmrPlugin()],
};
2 changes: 2 additions & 0 deletions packages/dev-server-hmr/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// this file is autogenerated with the generate-mjs-dts-entrypoints script
export * from './dist/index';
6 changes: 6 additions & 0 deletions packages/dev-server-hmr/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// this file is autogenerated with the generate-mjs-dts-entrypoints script
import cjsEntrypoint from './dist/index.js';

const { hmrPlugin } = cjsEntrypoint;

export { hmrPlugin };
43 changes: 43 additions & 0 deletions packages/dev-server-hmr/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"name": "@web/dev-server-hmr",
"version": "0.0.1",
"publishConfig": {
"access": "public"
},
"description": "Plugin for using HMR in @web/dev-server",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/modernweb-dev/web.git",
"directory": "packages/dev-server-hmr"
},
"author": "modern-web",
"homepage": "https://github.com/modernweb-dev/web/tree/master/packages/dev-server-hmr",
"main": "dist/index.js",
"engines": {
"node": ">=10.0.0"
},
"scripts": {
"build": "tsc",
"start:demo": "web-dev-server --config demo/server.config.js",
"test": "mocha \"test/**/*.test.ts\" --require ts-node/register --reporter dot",
"test:watch": "mocha \"test/**/*.test.ts\" --require ts-node/register --watch --watch-files src,test --reporter dot"
},
"files": [
"*.d.ts",
"*.js",
"*.mjs",
"dist",
"src"
],
"dependencies": {
"@web/dev-server-core": "^0.2.2"
},
"devDependencies": {},
"exports": {
".": {
"import": "./index.mjs",
"require": "./dist/index.js"
}
}
}
Loading