diff --git a/.changeset/thin-pumpkins-sleep.md b/.changeset/thin-pumpkins-sleep.md
new file mode 100644
index 00000000..b1bed86d
--- /dev/null
+++ b/.changeset/thin-pumpkins-sleep.md
@@ -0,0 +1,5 @@
+---
+'@open-wc/dev-server-wc-hmr': patch
+---
+
+first release
diff --git a/.eslintignore b/.eslintignore
index 14a152e5..dd3671b2 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -6,3 +6,4 @@ stats.html
/packages/**/test-node/**/snapshots
/packages/demoing-storybook/storybook-static/**/*
/packages/**/demo/**/*
+/packages/dev-server-hmr/src/patches/**/*
\ No newline at end of file
diff --git a/.eslintrc.js b/.eslintrc.js
index a3d47ecf..0567c338 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -2,6 +2,8 @@ module.exports = {
extends: ['./packages/eslint-config/index.js', require.resolve('eslint-config-prettier')],
rules: {
'lit/no-useless-template-literals': 'off',
+ 'consistent-return': 'off',
+ 'max-classes-per-file': 'off',
},
overrides: [
{
diff --git a/docs/docs/development/hot-module-replacement.md b/docs/docs/development/hot-module-replacement.md
new file mode 100644
index 00000000..818d561e
--- /dev/null
+++ b/docs/docs/development/hot-module-replacement.md
@@ -0,0 +1,240 @@
+# Development >> Hot Module Replacement ||50
+
+> This project is currently experimental. Try it out and let us know what you think!
+
+Plugin for "hot module replacement" or "fast refresh" with web components.
+
+Keeps track of web component definitions in your code, and updates them at runtime on change. This is faster than a full page reload and preserves the page's state.
+
+HMR requires the web component base class to implement a `hotReplaceCallback`.
+
+## Installation
+
+Install the package:
+
+```
+npm i --save-dev @web/dev-server-hmr
+```
+
+Add the plugin to your `web-dev-server-config.mjs`:
+
+```js
+import { hmrPlugin } from '@open-wc/dev-server-hmr';
+
+export default {
+ plugins: [
+ hmrPlugin({
+ include: ['src/**/*'],
+ }),
+ ],
+};
+```
+
+## Implementations
+
+### Vanilla
+
+For vanilla web component projects that don't implement any base class or library this plugin should detect your components correctly. Read more below on how to implement the `hotReplaceCallback`.
+
+### LitElement
+
+LitElement v2 supports HMR with a small code patch included in the preset. There is no support yet for the v3 prerelease.
+
+```js
+import { hmrPlugin, presets } from '@open-wc/dev-server-hmr';
+
+export default {
+ plugins: [
+ hmrPlugin({
+ include: ['src/**/*'],
+ presets: [presets.litElement],
+ }),
+ ],
+};
+```
+
+### FAST Element
+
+We have experimental support for FAST element using a small code patch included in the preset. This might not cover all use cases yet, let us know if you run into any issues!
+
+```js
+import { hmrPlugin, presets } from '@open-wc/dev-server-hmr';
+
+export default {
+ plugins: [
+ hmrPlugin({
+ include: ['src/**/*'],
+ presets: [presets.fastElement],
+ }),
+ ],
+};
+```
+
+### Other libraries
+
+If you know any other libraries that work correctly with HMR we can add presets for them here. Presets help by configuring the detection of base classes, decorators, and/or runtime code patches.
+
+## Detecting web components
+
+To "hot replace" an edited web component we have to be able to detect component definitions in your code. By default we look for usage of `customElements.define` and extending from `HTMLElement`.
+
+For other use cases, you can specify base classes or decorators to indicate component definitions. We have some presets shown above which configure this for libraries that we know to work with HMR.
+
+### Base classes
+
+The base class option detects web components that extend a specific base class. The base class can be matched by name, or as an import from a specific module.
+
+```js
+hmrPlugin({
+ include: ['src/**/*'],
+ baseClasses: [
+ // anything that extends a class called MyElement
+ { name: 'MyElement' },
+ // anything that extends a class called MyElement imported from 'my-element'
+ { name: 'MyElement', import: 'my-element' },
+ // anything that extends a class called MyElement imported from './src/MyElement.js' (relative to current working directory)
+ { name: 'MyElement', import: './src/MyElement.js' },
+ // anything that extends a default importeded class from 'my-element'
+ { name: 'default', import: 'my-element' },
+ ],
+});
+```
+
+### Decorator
+
+The plugin can also detect web components defined using decorators. The decorators can be matched by name, or as an import from a specific module.
+
+```js
+hmrPlugin({
+ include: ['src/**/*'],
+ decorators: [
+ // any class that uses a decorator called customElement
+ { name: 'customElement' },
+ // any class that uses a decorator called customElement imported from 'my-element'
+ { name: 'customElement', import: 'my-element' },
+ // any class that uses a decorator called customElement imported from './src/MyElement.js' (relative to current working directory)
+ { name: 'customElement', import: './src/MyElement.js' },
+ // any class that uses a decorator default imported from 'my-element'
+ { name: 'default', import: 'my-element' },
+ ],
+});
+```
+
+### Functions
+
+We don't currently support function based web components. Let us know if you have ideas on how this could work!
+
+## Limitations
+
+HMR workflows are not perfect. We're overwriting and moving around code at runtime. It breaks assumptions you normally make about your code. We recommended periodically doing a full refresh of the page, especially when you encounter strange behavior.
+
+The following limitations should be kept in mind when working with open-wc HMR:
+
+- Modules containing web components are re-imported under a new name and only the web component class is replaced. Side effects are triggered again but exported symbols are not updated.
+- Constructors for already created elements are not re-run when a class is replaced. Otherwise it would reset the properties of your element. This does mean that newly added properties don't show up.
+- Instance class fields act like properties defined in a constructor, and newly added or changed class fields are not hot replaced.
+- Newly created elements do use the new constructors and class fields.
+
+> Did you run into other limitations? Let us know so we can improve this list.
+
+## Implementing HMR
+
+When hot replacing a web component class we can't replace the actual class. The custom element registry doesn't allow re-registration and we want to preserve the state of already rendered components. Instead, we patch the initial class with the properties from the updates class.
+
+This updating logic can be different for each base class, and it can be implemented using the `hotReplaceCallback`.
+
+This is the default implementation:
+
+```js
+function updateObjectMembers(currentObj, newObj) {
+ const currentProperties = new Set(Object.getOwnPropertyNames(hmrClass));
+ const newProperties = new Set(Object.getOwnPropertyNames(newClass));
+
+ // add new and overwrite existing properties/methods
+ for (const prop of Object.getOwnPropertyNames(newClass)) {
+ const descriptor = Object.getOwnPropertyDescriptor(newClass, prop);
+ if (descriptor && descriptor.configurable) {
+ Object.defineProperty(hmrClass, prop, descriptor);
+ }
+ }
+
+ // delete removed properties
+ for (const existingProp of currentProperties) {
+ if (!newProperties.has(existingProp)) {
+ try {
+ delete hmrClass[existingProp];
+ } catch {}
+ }
+ }
+}
+
+class MyElement extends HTMLElement {
+ // static callback, called once when a class updates
+ static hotReplaceCallback(newClass) {
+ updateObjectMembers(this, newClass);
+ updateObjectMembers(this.prototype, newClass.prototype);
+ }
+
+ // instance callback, called for each connected element
+ hotReplaceCallback() {
+ // this should kick off re-rendering
+ this.update();
+ }
+}
+```
+
+### Static callback
+
+The static `hotReplaceCallback` callback is called once for each replacement on the initial class of the component. This is where you can copy over properties from the new class to the existing class.
+
+Implementing this callback is not mandatory, by default we copy over properties of the new class to the existing class. If this is not sufficient, you can customize this logic.
+
+### Instance callback
+
+The instance callback is called on each connected element implementing the replaced class. Implementing this is necessary to do some work at the instance level, such as trigger a re-render or style update.
+
+When the instance callback is called, all the class members (properties, methods, etc.) have already been updated. So it could be as simple as kicking off the regular updating/rendering pipeline.
+
+### Patching
+
+If you don't want to include the HMR code in your production code, you could patch in the callbacks externally:
+
+```js
+import { MyElement } from 'my-element';
+
+MyElement.hotReplaceCallback = function hotReplaceCallback(newClass) {
+ // code for the static callback
+};
+
+MyElement.prototype.hotReplaceCallback = function hotReplaceCallback(newClass) {
+ // code for the instance callback
+};
+```
+
+Make sure this code is loaded before any of your components are loaded. You could also do this using the `patch` option in the config:
+
+```js
+import { hmrPlugin } from '@open-wc/dev-server-hmr';
+
+const myElementPatch = `
+import { MyElement } from 'my-element';
+
+MyElement.hotReplaceCallback = function hotReplaceCallback(newClass) {
+ // code for the static callback
+};
+
+MyElement.prototype.hotReplaceCallback = function hotReplaceCallback(newClass) {
+ // code for the instance callback
+};
+`;
+
+export default {
+ plugins: [
+ hmrPlugin({
+ include: ['src/**/*'],
+ baseClasses: [{ name: 'MyElement', import: 'my-element' }],
+ patches: [myElementPatch],
+ }),
+ ],
+};
+```
diff --git a/package.json b/package.json
index 3e62e384..5561a65e 100644
--- a/package.json
+++ b/package.json
@@ -44,7 +44,7 @@
"@types/eslint": "^7.2.4",
"@types/estree": "^0.0.45",
"@types/parse5-htmlparser2-tree-adapter": "^5.0.1",
- "@web/dev-server": "^0.0.19",
+ "@web/dev-server": "^0.0.24",
"@web/test-runner": "^0.9.5",
"@web/test-runner-playwright": "^0.6.4",
"babel-eslint": "^10.0.3",
diff --git a/packages/dev-server-hmr/README.md b/packages/dev-server-hmr/README.md
new file mode 100644
index 00000000..5e120ba8
--- /dev/null
+++ b/packages/dev-server-hmr/README.md
@@ -0,0 +1,5 @@
+# Demoing via storybook
+
+Plugin for "hot module replacement" or "fast refresh" with web components.
+
+See [our website](https://open-wc.org/docs/development/hot-module-replacement/) for full documentation or view the docs [on github](../../docs/docs/development/hot-module-replacement.md)
diff --git a/packages/dev-server-hmr/demo/fast-element/index.html b/packages/dev-server-hmr/demo/fast-element/index.html
new file mode 100644
index 00000000..44c4ef03
--- /dev/null
+++ b/packages/dev-server-hmr/demo/fast-element/index.html
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/packages/dev-server-hmr/demo/fast-element/server.config.mjs b/packages/dev-server-hmr/demo/fast-element/server.config.mjs
new file mode 100644
index 00000000..334ad9cf
--- /dev/null
+++ b/packages/dev-server-hmr/demo/fast-element/server.config.mjs
@@ -0,0 +1,15 @@
+import { esbuildPlugin } from '@web/dev-server-esbuild';
+import { hmrPlugin, presets } from '../../index.mjs';
+
+export default {
+ open: 'packages/dev-server-hmr/demo/fast-element/',
+ rootDir: '../..',
+ nodeResolve: true,
+ plugins: [
+ esbuildPlugin({ ts: true }),
+ hmrPlugin({
+ exclude: ['**/*/node_modules/**/*'],
+ presets: [presets.fastElement],
+ }),
+ ],
+};
diff --git a/packages/dev-server-hmr/demo/fast-element/src/sharedStyles.ts b/packages/dev-server-hmr/demo/fast-element/src/sharedStyles.ts
new file mode 100644
index 00000000..406b6b1f
--- /dev/null
+++ b/packages/dev-server-hmr/demo/fast-element/src/sharedStyles.ts
@@ -0,0 +1,7 @@
+import { css } from '@microsoft/fast-element';
+
+export const sharedStyles = css`
+ .shared-template {
+ color: green;
+ }
+`;
diff --git a/packages/dev-server-hmr/demo/fast-element/src/sharedTemplate.ts b/packages/dev-server-hmr/demo/fast-element/src/sharedTemplate.ts
new file mode 100644
index 00000000..7de0fd05
--- /dev/null
+++ b/packages/dev-server-hmr/demo/fast-element/src/sharedTemplate.ts
@@ -0,0 +1,5 @@
+import { html } from '@microsoft/fast-element';
+
+export const sharedTemplate = html`
+ Shared template
+`;
diff --git a/packages/dev-server-hmr/demo/fast-element/src/todo-app.ts b/packages/dev-server-hmr/demo/fast-element/src/todo-app.ts
new file mode 100644
index 00000000..20a173d9
--- /dev/null
+++ b/packages/dev-server-hmr/demo/fast-element/src/todo-app.ts
@@ -0,0 +1,15 @@
+import { FASTElement, customElement, html } from '@microsoft/fast-element';
+import './todo-list.js';
+
+const template = html`
+ Todo app
+
+`;
+
+@customElement({
+ name: 'todo-app',
+ template,
+})
+class TodoApp extends FASTElement {
+ static definition = { name: 'todo-app', template };
+}
diff --git a/packages/dev-server-hmr/demo/fast-element/src/todo-item.ts b/packages/dev-server-hmr/demo/fast-element/src/todo-item.ts
new file mode 100644
index 00000000..2e748656
--- /dev/null
+++ b/packages/dev-server-hmr/demo/fast-element/src/todo-item.ts
@@ -0,0 +1,50 @@
+import { FASTElement, customElement, observable, html, css } from '@microsoft/fast-element';
+
+const styles = css`
+ :host {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ .message {
+ color: blue;
+ }
+
+ .delete {
+ border: none;
+ width: auto;
+ background: transparent;
+ font-size: 12px;
+ padding: 6px 4px;
+ }
+`;
+
+const template = html`
+
+ x.checked}
+ @change=${(x, c) => x._onCheckedChanged(c.event)}
+ />
+ ${x => x.message}
+
+ x._onDelete()}>❌
+`;
+
+@customElement({ name: 'todo-item', template, styles })
+class TodoItem extends FASTElement {
+ @observable checked = false;
+ @observable message = '';
+
+ _onCheckedChanged(e: Event) {
+ this.dispatchEvent(
+ new CustomEvent('checked-changed', { detail: (e.target as HTMLInputElement).checked }),
+ );
+ }
+
+ _onDelete() {
+ this.dispatchEvent(new Event('delete'));
+ }
+}
diff --git a/packages/dev-server-hmr/demo/fast-element/src/todo-list.ts b/packages/dev-server-hmr/demo/fast-element/src/todo-list.ts
new file mode 100644
index 00000000..3aca607a
--- /dev/null
+++ b/packages/dev-server-hmr/demo/fast-element/src/todo-list.ts
@@ -0,0 +1,94 @@
+import { FASTElement, customElement, observable, repeat, html, css } from '@microsoft/fast-element';
+import { sharedTemplate } from './sharedTemplate';
+import { sharedStyles } from './sharedStyles';
+import './todo-item.js';
+
+interface TodoItem {
+ message: string;
+ checked: boolean;
+}
+
+const styles = [
+ sharedStyles,
+ css`
+ ul {
+ list-style: none;
+ padding-inline-start: 0;
+ max-width: 200px;
+ }
+
+ .add {
+ display: flex;
+ }
+
+ .add > * {
+ margin-right: 6px;
+ }
+ `,
+];
+
+const template = html`
+
+ ${repeat(
+ x => x.items,
+ html`
+
+ x.message}
+ :checked=${x => x.checked}
+ data-i=${(_, c) => c.index}
+ @checked-changed=${(_, c) => c.parent._onCheckedChanged(c.event as any)}
+ @delete=${(_, c) => c.parent._onDelete(c.event)}
+ >
+
+ `,
+ )}
+
+
+
+
+ x._addTodo()}>
+ Add
+
+
+
+ ${sharedTemplate}
+`;
+
+@customElement({ name: 'todo-list', template, styles })
+class TodoList extends FASTElement {
+ @observable items: TodoItem[] = [
+ { message: 'Do A', checked: true },
+ { message: 'Do B', checked: false },
+ { message: 'Do C', checked: true },
+ { message: 'Do D', checked: false },
+ { message: 'Do E', checked: false },
+ ];
+
+ _onCheckedChanged(e: Event & { detail: boolean }) {
+ const { i: iString } = (e.target as HTMLElement).dataset;
+ const i = Number(iString);
+ const item = this.items[i];
+ const newItems = this.items.slice();
+ newItems.splice(i, 1, { ...item, checked: e.detail });
+ this.items = newItems;
+ }
+
+ _onDelete(e: Event) {
+ const { i: iString } = (e.target as HTMLElement).dataset;
+ const i = Number(iString);
+ const newItems = this.items.slice();
+ newItems.splice(i, 1);
+ this.items = newItems;
+ }
+
+ _addTodo() {
+ const input = this.shadowRoot!.getElementById('input') as HTMLInputElement;
+ const message = input.value;
+ if (!message) {
+ return;
+ }
+ input.value = '';
+ this.items = [...this.items, { message, checked: false }];
+ }
+}
diff --git a/packages/dev-server-hmr/demo/fast-element/tsconfig.json b/packages/dev-server-hmr/demo/fast-element/tsconfig.json
new file mode 100644
index 00000000..9ed3d198
--- /dev/null
+++ b/packages/dev-server-hmr/demo/fast-element/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "include": ["src"],
+ "compilerOptions": {
+ "target": "ESNEXT" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
+ "module":"ESNext",
+ "strict": true /* Enable all strict type-checking options. */,
+ /* Module Resolution Options */
+ "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
+ "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
+ "experimentalDecorators": true
+ }
+}
diff --git a/packages/dev-server-hmr/demo/lit-element-ts/index.html b/packages/dev-server-hmr/demo/lit-element-ts/index.html
new file mode 100644
index 00000000..44c4ef03
--- /dev/null
+++ b/packages/dev-server-hmr/demo/lit-element-ts/index.html
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/packages/dev-server-hmr/demo/lit-element-ts/server.config.mjs b/packages/dev-server-hmr/demo/lit-element-ts/server.config.mjs
new file mode 100644
index 00000000..e057306a
--- /dev/null
+++ b/packages/dev-server-hmr/demo/lit-element-ts/server.config.mjs
@@ -0,0 +1,15 @@
+import { esbuildPlugin } from '@web/dev-server-esbuild';
+import { hmrPlugin, presets } from '../../index.mjs';
+
+export default {
+ open: 'packages/dev-server-hmr/demo/lit-element-ts/',
+ rootDir: '../..',
+ nodeResolve: true,
+ plugins: [
+ esbuildPlugin({ ts: true }),
+ hmrPlugin({
+ exclude: ['**/*/node_modules/**/*'],
+ presets: [presets.litElement],
+ }),
+ ],
+};
diff --git a/packages/dev-server-hmr/demo/lit-element-ts/src/sharedStyles.ts b/packages/dev-server-hmr/demo/lit-element-ts/src/sharedStyles.ts
new file mode 100644
index 00000000..9880f235
--- /dev/null
+++ b/packages/dev-server-hmr/demo/lit-element-ts/src/sharedStyles.ts
@@ -0,0 +1,7 @@
+import { css } from 'lit-element';
+
+export const sharedStyles = css`
+ .shared-template {
+ color: green;
+ }
+`;
diff --git a/packages/dev-server-hmr/demo/lit-element-ts/src/sharedTemplate.ts b/packages/dev-server-hmr/demo/lit-element-ts/src/sharedTemplate.ts
new file mode 100644
index 00000000..8cdd63a0
--- /dev/null
+++ b/packages/dev-server-hmr/demo/lit-element-ts/src/sharedTemplate.ts
@@ -0,0 +1,3 @@
+import { html } from 'lit-element';
+
+export const sharedTemplate = html`Shared template
`;
diff --git a/packages/dev-server-hmr/demo/lit-element-ts/src/todo-app.ts b/packages/dev-server-hmr/demo/lit-element-ts/src/todo-app.ts
new file mode 100644
index 00000000..8bb53f23
--- /dev/null
+++ b/packages/dev-server-hmr/demo/lit-element-ts/src/todo-app.ts
@@ -0,0 +1,12 @@
+import { LitElement, customElement, html } from 'lit-element';
+import './todo-list.js';
+
+@customElement('todo-app')
+class TodoApp extends LitElement {
+ render() {
+ return html`
+ Todo app
+
+ `;
+ }
+}
diff --git a/packages/dev-server-hmr/demo/lit-element-ts/src/todo-item.ts b/packages/dev-server-hmr/demo/lit-element-ts/src/todo-item.ts
new file mode 100644
index 00000000..55277691
--- /dev/null
+++ b/packages/dev-server-hmr/demo/lit-element-ts/src/todo-item.ts
@@ -0,0 +1,55 @@
+import { LitElement, property, customElement, html, css } from 'lit-element';
+
+@customElement('todo-item')
+class TodoItem extends LitElement {
+ static get styles() {
+ return css`
+ :host {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ .message {
+ color: blue;
+ }
+
+ .delete {
+ border: none;
+ width: auto;
+ background: transparent;
+ font-size: 12px;
+ padding: 6px 4px;
+ }
+ `;
+ }
+
+ @property({ type: Boolean })
+ checked = false;
+ @property({ type: String })
+ message?: string;
+
+ render() {
+ return html`
+
+
+ ${this.message}
+
+
+ ❌
+ `;
+ }
+
+ _onCheckedChanged(e: any) {
+ this.dispatchEvent(new CustomEvent('checked-changed', { detail: e.target.checked }));
+ }
+
+ _onDelete() {
+ this.dispatchEvent(new Event('delete'));
+ }
+}
diff --git a/packages/dev-server-hmr/demo/lit-element-ts/src/todo-list.ts b/packages/dev-server-hmr/demo/lit-element-ts/src/todo-list.ts
new file mode 100644
index 00000000..4c63fff8
--- /dev/null
+++ b/packages/dev-server-hmr/demo/lit-element-ts/src/todo-list.ts
@@ -0,0 +1,93 @@
+import { LitElement, customElement, property, html, css } from 'lit-element';
+import { sharedTemplate } from './sharedTemplate';
+import { sharedStyles } from './sharedStyles';
+import './todo-item.js';
+
+@customElement('todo-list')
+class TodoList extends LitElement {
+ static get styles() {
+ return [
+ sharedStyles,
+ css`
+ ul {
+ list-style: none;
+ padding-inline-start: 0;
+ max-width: 200px;
+ }
+
+ .add {
+ display: flex;
+ }
+
+ .add > * {
+ margin-right: 6px;
+ }
+ `,
+ ];
+ }
+
+ @property({ type: Array })
+ items = [
+ { message: 'Do A', checked: true },
+ { message: 'Do B', checked: false },
+ { message: 'Do C', checked: true },
+ { message: 'Do D', checked: false },
+ { message: 'Do E', checked: false },
+ ];
+
+ render() {
+ return html`
+
+ ${this.items.map(
+ (item, i) =>
+ html`
+
+
+
+ `,
+ )}
+
+
+
+
+ Add
+
+
+
+ ${sharedTemplate}
+ `;
+ }
+
+ _onCheckedChanged(e: any) {
+ const { i } = e.target.dataset;
+ const item = this.items[i];
+ const newItems = this.items.slice();
+ newItems.splice(i, 1, { ...item, checked: e.detail });
+ this.items = newItems;
+ }
+
+ _onDelete(e) {
+ const { i } = e.target.dataset;
+ const newItems = this.items.slice();
+ newItems.splice(i, 1);
+ this.items = newItems;
+ }
+
+ _addTodo() {
+ const input = this.shadowRoot!.getElementById('input') as HTMLInputElement;
+ const message = input.value;
+ if (!message) {
+ return;
+ }
+ input.value = '';
+ this.items = [...this.items, { message, checked: false }];
+ }
+}
diff --git a/packages/dev-server-hmr/demo/lit-element-ts/tsconfig.json b/packages/dev-server-hmr/demo/lit-element-ts/tsconfig.json
new file mode 100644
index 00000000..9ed3d198
--- /dev/null
+++ b/packages/dev-server-hmr/demo/lit-element-ts/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "include": ["src"],
+ "compilerOptions": {
+ "target": "ESNEXT" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
+ "module":"ESNext",
+ "strict": true /* Enable all strict type-checking options. */,
+ /* Module Resolution Options */
+ "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
+ "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
+ "experimentalDecorators": true
+ }
+}
diff --git a/packages/dev-server-hmr/demo/lit-element/index.html b/packages/dev-server-hmr/demo/lit-element/index.html
new file mode 100644
index 00000000..a572a034
--- /dev/null
+++ b/packages/dev-server-hmr/demo/lit-element/index.html
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/packages/dev-server-hmr/demo/lit-element/server.config.mjs b/packages/dev-server-hmr/demo/lit-element/server.config.mjs
new file mode 100644
index 00000000..af048620
--- /dev/null
+++ b/packages/dev-server-hmr/demo/lit-element/server.config.mjs
@@ -0,0 +1,13 @@
+import { hmrPlugin, presets } from '../../index.mjs';
+
+export default {
+ open: 'packages/dev-server-hmr/demo/lit-element/',
+ rootDir: '../..',
+ nodeResolve: true,
+ plugins: [
+ hmrPlugin({
+ exclude: ['**/*/node_modules/**/*'],
+ presets: [presets.litElement],
+ }),
+ ],
+};
diff --git a/packages/dev-server-hmr/demo/lit-element/src/sharedStyles.js b/packages/dev-server-hmr/demo/lit-element/src/sharedStyles.js
new file mode 100644
index 00000000..9880f235
--- /dev/null
+++ b/packages/dev-server-hmr/demo/lit-element/src/sharedStyles.js
@@ -0,0 +1,7 @@
+import { css } from 'lit-element';
+
+export const sharedStyles = css`
+ .shared-template {
+ color: green;
+ }
+`;
diff --git a/packages/dev-server-hmr/demo/lit-element/src/sharedTemplate.js b/packages/dev-server-hmr/demo/lit-element/src/sharedTemplate.js
new file mode 100644
index 00000000..8cdd63a0
--- /dev/null
+++ b/packages/dev-server-hmr/demo/lit-element/src/sharedTemplate.js
@@ -0,0 +1,3 @@
+import { html } from 'lit-element';
+
+export const sharedTemplate = html`Shared template
`;
diff --git a/packages/dev-server-hmr/demo/lit-element/src/todo-app.js b/packages/dev-server-hmr/demo/lit-element/src/todo-app.js
new file mode 100644
index 00000000..9bafd634
--- /dev/null
+++ b/packages/dev-server-hmr/demo/lit-element/src/todo-app.js
@@ -0,0 +1,13 @@
+import { LitElement, html } from 'lit-element';
+import './todo-list.js';
+
+class TodoApp extends LitElement {
+ render() {
+ return html`
+ Todo app
+
+ `;
+ }
+}
+
+customElements.define('todo-app', TodoApp);
diff --git a/packages/dev-server-hmr/demo/lit-element/src/todo-item.js b/packages/dev-server-hmr/demo/lit-element/src/todo-item.js
new file mode 100644
index 00000000..b76fbd5d
--- /dev/null
+++ b/packages/dev-server-hmr/demo/lit-element/src/todo-item.js
@@ -0,0 +1,57 @@
+import { LitElement, html, css } from 'lit-element';
+
+class TodoItem extends LitElement {
+ static get styles() {
+ return css`
+ :host {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ .message {
+ color: blue;
+ }
+
+ .delete {
+ border: none;
+ width: auto;
+ background: transparent;
+ font-size: 12px;
+ padding: 6px 4px;
+ }
+ `;
+ }
+
+ static get properties() {
+ return {
+ checked: { type: Boolean },
+ message: { type: String },
+ };
+ }
+
+ render() {
+ return html`
+
+
+ ${this.message}
+
+ ❌
+ `;
+ }
+
+ _onCheckedChanged(e) {
+ this.dispatchEvent(new CustomEvent('checked-changed', { detail: e.target.checked }));
+ }
+
+ _onDelete() {
+ this.dispatchEvent(new Event('delete'));
+ }
+}
+
+customElements.define('todo-item', TodoItem);
diff --git a/packages/dev-server-hmr/demo/lit-element/src/todo-list.js b/packages/dev-server-hmr/demo/lit-element/src/todo-list.js
new file mode 100644
index 00000000..657a8a42
--- /dev/null
+++ b/packages/dev-server-hmr/demo/lit-element/src/todo-list.js
@@ -0,0 +1,100 @@
+import { LitElement, html, css } from 'lit-element';
+import { sharedTemplate } from './sharedTemplate.js';
+import { sharedStyles } from './sharedStyles.js';
+import './todo-item.js';
+
+class TodoList extends LitElement {
+ static get styles() {
+ return [
+ sharedStyles,
+ css`
+ ul {
+ list-style: none;
+ padding-inline-start: 0;
+ max-width: 200px;
+ }
+
+ .add {
+ display: flex;
+ }
+
+ .add > * {
+ margin-right: 6px;
+ }
+ `,
+ ];
+ }
+
+ static get properties() {
+ return { items: { type: Object } };
+ }
+
+ constructor() {
+ super();
+ this.items = [
+ { message: 'Do A', checked: true },
+ { message: 'Do B', checked: false },
+ { message: 'Do C', checked: true },
+ { message: 'Do D', checked: false },
+ { message: 'Do E', checked: false },
+ ];
+ }
+
+ render() {
+ return html`
+
+ ${this.items.map(
+ (item, i) =>
+ html`
+
+
+
+ `,
+ )}
+
+
+
+
+ Add
+
+
+
+ ${sharedTemplate}
+ `;
+ }
+
+ _onCheckedChanged(e) {
+ const { i } = e.target.dataset;
+ const item = this.items[i];
+ const newItems = this.items.slice();
+ newItems.splice(i, 1, { ...item, checked: e.detail });
+ this.items = newItems;
+ }
+
+ _onDelete(e) {
+ const { i } = e.target.dataset;
+ const newItems = this.items.slice();
+ newItems.splice(i, 1);
+ this.items = newItems;
+ }
+
+ _addTodo() {
+ const input = this.shadowRoot.getElementById('input');
+ const message = input.value;
+ if (!message) {
+ return;
+ }
+ input.value = '';
+ this.items = [...this.items, { message, checked: false }];
+ }
+}
+
+customElements.define('todo-list', TodoList);
diff --git a/packages/dev-server-hmr/demo/vanilla/index.html b/packages/dev-server-hmr/demo/vanilla/index.html
new file mode 100644
index 00000000..7ba3a181
--- /dev/null
+++ b/packages/dev-server-hmr/demo/vanilla/index.html
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/packages/dev-server-hmr/demo/vanilla/server.config.mjs b/packages/dev-server-hmr/demo/vanilla/server.config.mjs
new file mode 100644
index 00000000..08f4b10a
--- /dev/null
+++ b/packages/dev-server-hmr/demo/vanilla/server.config.mjs
@@ -0,0 +1,18 @@
+import path from 'path';
+import { fileURLToPath } from 'url';
+import { hmrPlugin } from '../../index.mjs';
+
+const dirname = path.dirname(fileURLToPath(import.meta.url));
+
+export default {
+ open: 'demo/vanilla/',
+ nodeResolve: true,
+ plugins: [
+ hmrPlugin({
+ exclude: ['**/*/node_modules/**/*'],
+ baseClasses: [
+ { name: 'SharedElement', import: path.resolve(dirname, './src/SharedElement.js') },
+ ],
+ }),
+ ],
+};
diff --git a/packages/dev-server-hmr/demo/vanilla/src/BaseClass.js b/packages/dev-server-hmr/demo/vanilla/src/BaseClass.js
new file mode 100644
index 00000000..99bf94af
--- /dev/null
+++ b/packages/dev-server-hmr/demo/vanilla/src/BaseClass.js
@@ -0,0 +1,24 @@
+export class BaseClass extends HTMLElement {
+ constructor() {
+ super();
+ this.attachShadow({ mode: 'open' });
+ }
+
+ connectedCallback() {
+ if (super.connectedCallback) {
+ super.connectedCallback();
+ }
+ this.update();
+ }
+
+ hotReplaceCallback() {
+ this.update();
+ }
+
+ update() {
+ this.shadowRoot.innerHTML = `
+
+ ${this.render()}
+ `;
+ }
+}
diff --git a/packages/dev-server-hmr/demo/vanilla/src/VanillaElementBase.js b/packages/dev-server-hmr/demo/vanilla/src/VanillaElementBase.js
new file mode 100644
index 00000000..98a1840f
--- /dev/null
+++ b/packages/dev-server-hmr/demo/vanilla/src/VanillaElementBase.js
@@ -0,0 +1,17 @@
+import { BaseClass } from './BaseClass.js';
+
+export class VanillaElementBase extends BaseClass {
+ styles() {
+ return `
+ .base {
+ color: blue;
+ }
+ `;
+ }
+
+ render() {
+ return `
+ Vanilla element base
+ `;
+ }
+}
diff --git a/packages/dev-server-hmr/demo/vanilla/src/sharedStyles.js b/packages/dev-server-hmr/demo/vanilla/src/sharedStyles.js
new file mode 100644
index 00000000..94181a42
--- /dev/null
+++ b/packages/dev-server-hmr/demo/vanilla/src/sharedStyles.js
@@ -0,0 +1,5 @@
+export const sharedStyles = `
+ .shared-template {
+ color: green;
+ }
+`;
diff --git a/packages/dev-server-hmr/demo/vanilla/src/sharedTemplate.js b/packages/dev-server-hmr/demo/vanilla/src/sharedTemplate.js
new file mode 100644
index 00000000..0ff642d3
--- /dev/null
+++ b/packages/dev-server-hmr/demo/vanilla/src/sharedTemplate.js
@@ -0,0 +1 @@
+export const sharedTemplate = 'Shared template
';
diff --git a/packages/dev-server-hmr/demo/vanilla/src/vanilla-app.js b/packages/dev-server-hmr/demo/vanilla/src/vanilla-app.js
new file mode 100644
index 00000000..04866a60
--- /dev/null
+++ b/packages/dev-server-hmr/demo/vanilla/src/vanilla-app.js
@@ -0,0 +1,32 @@
+import { BaseClass } from './BaseClass.js';
+import { sharedTemplate } from './sharedTemplate.js';
+import { sharedStyles } from './sharedStyles.js';
+import './vanilla-message.js';
+import './vanilla-element.js';
+
+class VanillaApp extends BaseClass {
+ styles() {
+ return `
+ ${sharedStyles}
+
+ h1 {
+ color: blue;
+ }
+ `;
+ }
+
+ render() {
+ return `
+ Vanilla app
+ ${sharedTemplate}
+
+
+
+
+
+
+ `;
+ }
+}
+
+customElements.define('vanilla-app', VanillaApp);
diff --git a/packages/dev-server-hmr/demo/vanilla/src/vanilla-element.js b/packages/dev-server-hmr/demo/vanilla/src/vanilla-element.js
new file mode 100644
index 00000000..5efc077e
--- /dev/null
+++ b/packages/dev-server-hmr/demo/vanilla/src/vanilla-element.js
@@ -0,0 +1,22 @@
+import { VanillaElementBase } from './VanillaElementBase.js';
+
+class VanillaElement extends VanillaElementBase {
+ styles() {
+ return `
+ ${super.styles()}
+
+ .element {
+ color: red;
+ }
+ `;
+ }
+
+ render() {
+ return `
+ ${super.render()}
+ Vanilla element
+ `;
+ }
+}
+
+customElements.define('vanilla-element', VanillaElement);
diff --git a/packages/dev-server-hmr/demo/vanilla/src/vanilla-message.js b/packages/dev-server-hmr/demo/vanilla/src/vanilla-message.js
new file mode 100644
index 00000000..0842f5a0
--- /dev/null
+++ b/packages/dev-server-hmr/demo/vanilla/src/vanilla-message.js
@@ -0,0 +1,39 @@
+import { BaseClass } from './BaseClass.js';
+
+class VanillaMessage extends BaseClass {
+ static observedAttributes = ['message'];
+
+ constructor() {
+ super();
+ this.message = '';
+ }
+
+ attributeChangedCallback(name, oldValue, newValue) {
+ this[name] = newValue;
+ }
+
+ set message(value) {
+ this._message = value;
+ this.update();
+ }
+
+ get message() {
+ return this._message;
+ }
+
+ styles() {
+ return `
+ p {
+ color: blue;
+ }
+ `;
+ }
+
+ render() {
+ return `
+ Message: ${this.message}
+ `;
+ }
+}
+
+customElements.define('vanilla-message', VanillaMessage);
diff --git a/packages/dev-server-hmr/index.mjs b/packages/dev-server-hmr/index.mjs
new file mode 100644
index 00000000..0229639b
--- /dev/null
+++ b/packages/dev-server-hmr/index.mjs
@@ -0,0 +1,5 @@
+import module from './src/index.js';
+
+const { hmrPlugin, presets, WC_HMR_MODULE_RUNTIME } = module;
+
+export { hmrPlugin, presets, WC_HMR_MODULE_RUNTIME };
diff --git a/packages/dev-server-hmr/package.json b/packages/dev-server-hmr/package.json
new file mode 100644
index 00000000..c38a38e2
--- /dev/null
+++ b/packages/dev-server-hmr/package.json
@@ -0,0 +1,63 @@
+{
+ "name": "@open-wc/dev-server-wc-hmr",
+ "version": "0.0.0",
+ "publishConfig": {
+ "access": "public"
+ },
+ "description": "Plugin for HMR with web components",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/open-wc/open-wc.git",
+ "directory": "packages/dev-server-wc-hmr"
+ },
+ "author": "open-wc",
+ "homepage": "https://github.com/open-wc/open-wc/packages/dev-server-wc-hmr",
+ "main": "dist/index.js",
+ "scripts": {
+ "start:fast": "wds --config demo/fast-element/server.config.mjs",
+ "start:lit": "wds --config demo/lit-element/server.config.mjs",
+ "start:lit-ts": "wds --config demo/lit-element-ts/server.config.mjs",
+ "start:vanilla": "wds --config demo/vanilla/server.config.mjs",
+ "test": "npm run test:node",
+ "test:node": "mocha test-node --recursive",
+ "test:watch": "npm run test:node -- --watch --watchfiles test"
+ },
+ "files": [
+ "dist",
+ "src"
+ ],
+ "keywords": [
+ "web",
+ "dev server",
+ "hmr",
+ "hot",
+ "module",
+ "replacement",
+ "reload",
+ "web",
+ "components"
+ ],
+ "dependencies": {
+ "@babel/core": "^7.12.3",
+ "@babel/plugin-proposal-class-properties": "^7.12.1",
+ "@babel/plugin-syntax-import-assertions": "^7.12.1",
+ "@babel/plugin-syntax-top-level-await": "^7.12.1",
+ "@web/dev-server-core": "^0.2.18",
+ "@web/dev-server-hmr": "^0.1.6",
+ "picomatch": "^2.2.2"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.12.3",
+ "@microsoft/fast-element": "^0.19.1",
+ "@types/babel__core": "^7.1.12",
+ "@types/babel__generator": "^7.6.2",
+ "@types/babel__parser": "^7.1.1",
+ "@types/babel__traverse": "^7.0.15",
+ "@types/picomatch": "^2.2.1",
+ "@web/dev-server": "^0.0.24",
+ "@web/dev-server-esbuild": "^0.2.8",
+ "lit-element": "^2.4.0",
+ "mocha": "^8.2.1"
+ }
+}
diff --git a/packages/dev-server-hmr/src/babel/babelPluginWcHmr.js b/packages/dev-server-hmr/src/babel/babelPluginWcHmr.js
new file mode 100644
index 00000000..da0b408b
--- /dev/null
+++ b/packages/dev-server-hmr/src/babel/babelPluginWcHmr.js
@@ -0,0 +1,191 @@
+/** @typedef {import('@babel/core').PluginObj} PluginObj */
+/** @template T @typedef {import('@babel/core').NodePath} NodePath */
+
+/**
+ * @typedef {object} BaseClass
+ * @property {string} name
+ * @property {string} [import]
+ */
+
+/**
+ * @typedef {object} Decorator
+ * @property {string} name
+ * @property {string} [import]
+ */
+
+/**
+ * @typedef {object} BabelPluginWcHmrOptions
+ * @property {string} rootDir
+ * @property {BaseClass[]} [baseClasses]
+ * @property {Decorator[]} [decorators]
+ * @property {string[]} [patches]
+ */
+
+const { types } = require('@babel/core');
+const path = require('path');
+
+const { findDefinedCustomElement } = require('./customElementsDefine');
+const { findDecoratedCustomElement } = require('./decorators');
+const { injectRegisterClass, injectRuntime } = require('./inject');
+const { parseOptions, singlePath, addToSet } = require('./utils');
+const { getImportedVariableNames } = require('./getImportedVariableNames');
+const { implementsBaseClass } = require('./class');
+const { createError } = require('../utils');
+
+/** @returns {PluginObj} */
+function babelPluginWcHmr() {
+ return {
+ visitor: {
+ Program(program) {
+ if (!this.filename) throw createError('Missing filename');
+ const resolvedFilename = path.resolve(this.filename);
+ const opts = parseOptions(/** @type {BabelPluginWcHmrOptions} */ (this.opts));
+ const baseClasses = opts.baseClasses || [];
+ const decorators = opts.decorators || [];
+
+ /** @type {Set} */
+ const baseClassNames = new Set();
+ /** @type {Set} */
+ const decoratorNames = new Set();
+ /** @type {Set} */
+ const injectedClassNames = new Set();
+ let injectedRegister = false;
+
+ /**
+ * @param {NodePath} nodePath
+ * @param {string} name
+ * @param {boolean} insertAfter
+ */
+ function maybeInjectRegister(nodePath, name, insertAfter = false) {
+ if (injectedClassNames.has(name)) {
+ return;
+ }
+ injectRegisterClass(nodePath, name, insertAfter);
+ injectedRegister = true;
+ injectedClassNames.add(name);
+ }
+
+ // add decorators that don't require their import to be checked
+ addToSet(
+ baseClassNames,
+ baseClasses.filter(b => !b.import).map(b => b.name),
+ );
+ addToSet(
+ decoratorNames,
+ decorators.filter(c => !c.import).map(d => d.name),
+ );
+
+ program.traverse({
+ ImportDeclaration(importDecl) {
+ // find all base classes and decorators that match this import
+ const result = getImportedVariableNames(
+ baseClasses,
+ decorators,
+ importDecl,
+ resolvedFilename,
+ opts.rootDir,
+ );
+ addToSet(baseClassNames, result.baseClassNames);
+ addToSet(decoratorNames, result.decoratorNames);
+ },
+ });
+
+ program.traverse({
+ CallExpression(callExpr) {
+ const callee = callExpr.get('callee');
+ const args = callExpr.get('arguments');
+ if (!singlePath(callee) || !Array.isArray(args)) {
+ return;
+ }
+
+ if (callee.isMemberExpression()) {
+ // this might be a customElements.define call
+ const definedCustomElement = findDefinedCustomElement(callee, args);
+ if (!definedCustomElement) {
+ return;
+ }
+
+ if (definedCustomElement.isIdentifier()) {
+ maybeInjectRegister(callExpr, definedCustomElement.node.name);
+ }
+
+ if (definedCustomElement.isClassExpression()) {
+ // take inline class expression out of the define so that it can be registered
+ const id = callExpr.scope.generateUidIdentifierBasedOnNode(
+ definedCustomElement.node,
+ );
+ const { name } = id;
+
+ if (!injectedClassNames.has(name)) {
+ callExpr.insertBefore(
+ types.variableDeclaration('const', [
+ types.variableDeclarator(id, definedCustomElement.node),
+ ]),
+ );
+ definedCustomElement.replaceWith(id);
+ maybeInjectRegister(callExpr, name);
+ }
+ }
+ return;
+ }
+
+ if (decoratorNames.size > 0) {
+ if (callee.isIdentifier()) {
+ // this might be a decorated class
+ const decoratedCustomElement = findDecoratedCustomElement(
+ decoratorNames,
+ callee,
+ args,
+ );
+ if (!decoratedCustomElement) {
+ return;
+ }
+
+ if (decoratedCustomElement.isIdentifier()) {
+ maybeInjectRegister(callExpr.parentPath, decoratedCustomElement.node.name);
+ }
+ }
+ }
+ },
+
+ ClassDeclaration(classDecl) {
+ // this is a class declaration like class A extends HTMLElement {}
+ if (implementsBaseClass(classDecl, baseClassNames)) {
+ maybeInjectRegister(classDecl, classDecl.node.id.name, true);
+ }
+ },
+
+ ClassExpression(classExpr) {
+ const { parent, parentPath: varDeclarator } = classExpr;
+ if (!parent || varDeclarator.isVariableDeclarator()) {
+ return;
+ }
+ if (!varDeclarator || varDeclarator.parentPath.isVariableDeclaration()) {
+ return;
+ }
+ const id = varDeclarator.get('id');
+ if (!singlePath(id) || !id.isIdentifier()) {
+ return;
+ }
+
+ const injectScope = varDeclarator.parentPath;
+ if (!injectScope) {
+ return;
+ }
+
+ // this is a class expression assignment like const A = class B extends HTMLElement {}
+ if (implementsBaseClass(classExpr, baseClassNames)) {
+ maybeInjectRegister(classExpr, id.node.name, true);
+ }
+ },
+ });
+
+ if (injectedRegister) {
+ injectRuntime(opts, program);
+ }
+ },
+ },
+ };
+}
+
+module.exports = babelPluginWcHmr;
diff --git a/packages/dev-server-hmr/src/babel/babelTransform.js b/packages/dev-server-hmr/src/babel/babelTransform.js
new file mode 100644
index 00000000..7fc8ef9c
--- /dev/null
+++ b/packages/dev-server-hmr/src/babel/babelTransform.js
@@ -0,0 +1,40 @@
+const { transformAsync } = require('@babel/core');
+
+const babelPluginWcHmr = require('./babelPluginWcHmr');
+const { createError } = require('../utils');
+
+/** @typedef {import('./babelPluginWcHmr').BabelPluginWcHmrOptions} BabelPluginWcHmrOptions */
+
+/**
+ * @param {string} code
+ * @param {string} filename
+ * @param {BabelPluginWcHmrOptions} options
+ * @returns {Promise}
+ */
+async function babelTransform(code, filename, options) {
+ const largeFile = code.length > 100000;
+ const result = await transformAsync(code, {
+ caller: {
+ name: '@open-wc/dev-server-hmr',
+ supportsStaticESM: true,
+ },
+ plugins: [
+ [babelPluginWcHmr, options],
+ require.resolve('@babel/plugin-syntax-class-properties'),
+ require.resolve('@babel/plugin-syntax-import-assertions'),
+ require.resolve('@babel/plugin-syntax-top-level-await'),
+ ],
+ filename,
+ babelrc: false,
+ configFile: false,
+ compact: largeFile,
+ sourceType: 'module',
+ });
+
+ if (!result?.code) {
+ throw createError(`Failed to babel transform ${filename}`);
+ }
+ return result.code;
+}
+
+module.exports = { babelTransform };
diff --git a/packages/dev-server-hmr/src/babel/class.js b/packages/dev-server-hmr/src/babel/class.js
new file mode 100644
index 00000000..a9406e99
--- /dev/null
+++ b/packages/dev-server-hmr/src/babel/class.js
@@ -0,0 +1,37 @@
+/** @typedef {import('@babel/types').ClassDeclaration} ClassDeclaration */
+/** @typedef {import('@babel/types').ClassExpression} ClassExpression */
+/** @template T @typedef {import('@babel/core').NodePath} NodePath */
+
+const { singlePath } = require('./utils');
+
+/**
+ * @param {NodePath | NodePath} classDeclOrExpr
+ * @returns {NodePath | undefined}
+ */
+function walkClassMixins(classDeclOrExpr) {
+ let el = classDeclOrExpr.get('superClass');
+ // walk possible mixin functions
+ while (singlePath(el) && el.isCallExpression()) {
+ const result = el.get('arguments');
+ if (Array.isArray(result)) {
+ [el] = result;
+ }
+ }
+
+ return singlePath(el) ? el : undefined;
+}
+
+/**
+ * @param {NodePath | NodePath} classDeclOrExpr
+ * @param {Set} baseClassNames
+ */
+function implementsBaseClass(classDeclOrExpr, baseClassNames) {
+ const el = walkClassMixins(classDeclOrExpr);
+
+ if (el && el.isIdentifier()) {
+ const { name } = el.node;
+ return baseClassNames.has(name);
+ }
+}
+
+module.exports = { implementsBaseClass };
diff --git a/packages/dev-server-hmr/src/babel/customElementsDefine.js b/packages/dev-server-hmr/src/babel/customElementsDefine.js
new file mode 100644
index 00000000..7cdeca3c
--- /dev/null
+++ b/packages/dev-server-hmr/src/babel/customElementsDefine.js
@@ -0,0 +1,85 @@
+/** @typedef {import('@babel/types').MemberExpression} MemberExpression */
+/** @typedef {import('@babel/types').CallExpression} CallExpression */
+/** @template T @typedef {import('@babel/core').NodePath} NodePath */
+
+const { resolvePath, singlePath } = require('./utils');
+
+const GLOBALS = ['window', 'self', 'globalThis'];
+
+/**
+ * @param {NodePath} callee
+ * @param {NodePath[]} args
+ */
+function isDefineCall(callee, args) {
+ const property = callee.get('property');
+
+ if (!singlePath(property) || !property.isIdentifier() || property.node.name !== 'define') {
+ return false;
+ }
+
+ if (!args || !Array.isArray(args)) {
+ return false;
+ }
+
+ return args.length >= 2 && (args[1].isIdentifier() || args[1].isClassExpression());
+}
+
+/** @param {NodePath} memberExpr */
+function isCallOnCustomElementObject(memberExpr) {
+ const object = memberExpr.get('object');
+ if (!singlePath(object)) {
+ return false;
+ }
+
+ if (object.isIdentifier()) {
+ // we are dealing with .define(), check if references global customElements
+ const resolvedIdPath = resolvePath(object);
+ if (!resolvedIdPath || !resolvedIdPath.isIdentifier()) {
+ return false;
+ }
+ return resolvedIdPath.node.name === 'customElements';
+ }
+
+ if (object.isMemberExpression()) {
+ const property = object.get('property');
+ if (
+ !singlePath(property) ||
+ !property.isIdentifier() ||
+ property.node.name !== 'customElements'
+ ) {
+ return false;
+ }
+ // we are dealing with .customElements.define, check if is the global scope
+
+ const subObject = object.get('object');
+ if (!singlePath(subObject) || !subObject.isIdentifier()) {
+ return false;
+ }
+ const resolvedIdPath = resolvePath(subObject);
+ return (
+ resolvedIdPath && resolvedIdPath.isIdentifier() && GLOBALS.includes(resolvedIdPath.node.name)
+ );
+ }
+
+ return false;
+}
+
+/** @param {NodePath[]} args */
+function getDefinedClass(args) {
+ if (!args || !Array.isArray(args)) {
+ return;
+ }
+ return args[1];
+}
+
+/**
+ * @param {NodePath} memberExpr
+ * @param {NodePath[]} args
+ */
+function findDefinedCustomElement(memberExpr, args) {
+ if (isDefineCall(memberExpr, args) && isCallOnCustomElementObject(memberExpr)) {
+ return getDefinedClass(args);
+ }
+}
+
+module.exports = { findDefinedCustomElement };
diff --git a/packages/dev-server-hmr/src/babel/decorators.js b/packages/dev-server-hmr/src/babel/decorators.js
new file mode 100644
index 00000000..2e6f86b3
--- /dev/null
+++ b/packages/dev-server-hmr/src/babel/decorators.js
@@ -0,0 +1,61 @@
+/** @typedef {import('@babel/types').Identifier} Identifier */
+/** @typedef {import('./babelPluginWcHmr').Decorator} Decorator */
+/** @template T @typedef {import('@babel/core').NodePath} NodePath */
+
+const { singlePath } = require('./utils');
+
+/**
+ * @param {Set} decoratorNames
+ * @param {NodePath} callee
+ * @param {NodePath[]} args
+ */
+function findCompiledTsDecoratedCustomElement(decoratorNames, callee, args) {
+ // is this a decorator helper function?
+ if (callee.node.name !== '__decorate') {
+ return;
+ }
+
+ const [arrayExpr, decoratedClass] = args;
+ // are we decorating an identifier (of a class)
+ if (!singlePath(decoratedClass) || !decoratedClass.isIdentifier()) {
+ return;
+ }
+ // is the first parameter an array of decorator functions?
+ if (!singlePath(arrayExpr) || !arrayExpr.isArrayExpression()) {
+ return;
+ }
+ const elements = arrayExpr.get('elements');
+ if (!Array.isArray(elements)) {
+ return;
+ }
+
+ // find a decorator function called customElement
+ const decoratorCall = elements.find(e => {
+ if (!e.isCallExpression()) {
+ return false;
+ }
+ const eCallee = e.get('callee');
+ if (!singlePath(eCallee) || !eCallee.isIdentifier()) {
+ return false;
+ }
+ return decoratorNames.has(eCallee.node.name);
+ });
+
+ if (!decoratorCall) {
+ return;
+ }
+ return decoratedClass;
+}
+
+/**
+ * @param {Set} decoratorNames
+ * @param {NodePath} callee
+ * @param {NodePath[]} args
+ * @returns {NodePath | undefined}
+ */
+function findDecoratedCustomElement(decoratorNames, callee, args) {
+ // TODO: add non-compiled decorator when it becomes stage 3 and properly supported by babel
+ return findCompiledTsDecoratedCustomElement(decoratorNames, callee, args);
+}
+
+module.exports = { findDecoratedCustomElement };
diff --git a/packages/dev-server-hmr/src/babel/getImportedVariableNames.js b/packages/dev-server-hmr/src/babel/getImportedVariableNames.js
new file mode 100644
index 00000000..f6c1d139
--- /dev/null
+++ b/packages/dev-server-hmr/src/babel/getImportedVariableNames.js
@@ -0,0 +1,100 @@
+/** @typedef {import('@babel/types').ImportDefaultSpecifier} ImportDefaultSpecifier */
+/** @typedef {import('@babel/types').ImportNamespaceSpecifier} ImportNamespaceSpecifier */
+/** @typedef {import('@babel/types').ImportSpecifier} ImportSpecifier */
+/** @typedef {import('@babel/types').ImportDeclaration} ImportDeclaration */
+/** @typedef {import('@babel/types').StringLiteral} StringLiteral */
+/** @template T @typedef {import('@babel/core').NodePath} NodePath */
+/** @typedef {import('./babelPluginWcHmr').BaseClass} BaseClass */
+/** @typedef {import('./babelPluginWcHmr').Decorator} Decorator */
+
+const path = require('path');
+const { singlePath } = require('./utils');
+
+/**
+ * @param {string} importSpecifier
+ * @param {string} importPath
+ * @param {string} filename
+ * @param {string} rootDir
+ * @returns {boolean}
+ */
+function isMatchingImport(importSpecifier, importPath, filename, rootDir) {
+ // is this a non-bare import
+ if (
+ importSpecifier.startsWith('../') ||
+ importSpecifier.startsWith('./') ||
+ importSpecifier.startsWith('/')
+ ) {
+ const partialImportFilePath = importSpecifier.split('/').join(path.sep);
+ const joinBase = importSpecifier.startsWith('/') ? rootDir : path.dirname(filename);
+ const importFilePath = path.join(joinBase, partialImportFilePath);
+
+ return importFilePath === importPath || `${importFilePath}.js` === importPath;
+ }
+
+ // this is a bare import
+ return importPath === importSpecifier || `${importPath}.js` === importSpecifier;
+}
+
+/**
+ * @param {BaseClass[]} baseClasses
+ * @param {Decorator[]} decorators
+ * @param {string} importSpecifier
+ * @param {string} filename
+ * @param {string} rootDir
+ */
+function getMatchesForImport(baseClasses, decorators, importSpecifier, filename, rootDir) {
+ return {
+ baseClasses: baseClasses.filter(
+ baseClass =>
+ baseClass.import && isMatchingImport(importSpecifier, baseClass.import, filename, rootDir),
+ ),
+ decorators: decorators.filter(
+ decorator =>
+ decorator.import && isMatchingImport(importSpecifier, decorator.import, filename, rootDir),
+ ),
+ };
+}
+
+/**
+ * @param {BaseClass[]} baseClasses
+ * @param {Decorator[]} decorators
+ * @param {NodePath} importDeclaration
+ * @param {string} filename
+ * @param {string} rootDir
+ */
+function getImportedVariableNames(baseClasses, decorators, importDeclaration, filename, rootDir) {
+ const importSpecifier = importDeclaration.node.source.value;
+ const matches = getMatchesForImport(baseClasses, decorators, importSpecifier, filename, rootDir);
+ /** @type {string[]} */
+ const baseClassNames = [];
+ const decoratorNames = [];
+
+ if (matches.baseClasses.length || matches.decorators.length) {
+ for (const specifier of importDeclaration.get('specifiers')) {
+ if (specifier.isImportDefaultSpecifier()) {
+ if (matches.baseClasses.some(cl => cl.name === 'default')) {
+ baseClassNames.push(specifier.node.local.name);
+ }
+ if (matches.decorators.some(cl => cl.name === 'default')) {
+ decoratorNames.push(specifier.node.local.name);
+ }
+ } else if (specifier.isImportSpecifier()) {
+ const imported = specifier.get('imported');
+ if (singlePath(imported)) {
+ const importedName = imported.isIdentifier()
+ ? imported.node.name
+ : /** @type {StringLiteral} */ (imported.node).value;
+ if (matches.baseClasses.some(cl => cl.name === importedName)) {
+ baseClassNames.push(specifier.node.local.name);
+ }
+ if (matches.decorators.some(cl => cl.name === importedName)) {
+ decoratorNames.push(specifier.node.local.name);
+ }
+ }
+ }
+ }
+ }
+ return { baseClassNames, decoratorNames };
+}
+
+module.exports = { getImportedVariableNames };
diff --git a/packages/dev-server-hmr/src/babel/inject.js b/packages/dev-server-hmr/src/babel/inject.js
new file mode 100644
index 00000000..f4d8b4f0
--- /dev/null
+++ b/packages/dev-server-hmr/src/babel/inject.js
@@ -0,0 +1,40 @@
+/** @typedef {import('@babel/types').Program} Program */
+/** @typedef {import('./babelPluginWcHmr').BabelPluginWcHmrOptions} BabelPluginWcHmrOptions */
+/** @template T @typedef {import('@babel/core').NodePath} NodePath */
+
+const { parse } = require('@babel/core');
+const { WC_HMR_NAMESPACE, WC_HMR_MODULE_PATCH, WC_HMR_MODULE_RUNTIME } = require('../constants');
+
+/**
+ * @template T
+ * @param {NodePath} nodePath
+ * @param {string} name
+ * @param {boolean} insertAfter
+ */
+function injectRegisterClass(nodePath, name, insertAfter) {
+ const toInject = parse(`${WC_HMR_NAMESPACE}.register(import.meta.url, ${name})`);
+ if (!toInject) throw new TypeError('Failed to parse');
+ if (insertAfter) {
+ nodePath.insertAfter(toInject);
+ } else {
+ nodePath.insertBefore(toInject);
+ }
+}
+
+/**
+ * @param {BabelPluginWcHmrOptions} options
+ * @param {NodePath} program
+ */
+function injectRuntime(options, program) {
+ const patch =
+ options.patches && options.patches.length > 0 ? `import '${WC_HMR_MODULE_PATCH}'; ` : '';
+ const toInject = parse(
+ `${patch}import * as ${WC_HMR_NAMESPACE} from '${WC_HMR_MODULE_RUNTIME}'; if(import.meta.hot) { import.meta.hot.accept(); }`,
+ );
+
+ if (toInject) {
+ program.node.body.unshift(/** @type {any} */ (toInject));
+ }
+}
+
+module.exports = { injectRegisterClass, injectRuntime };
diff --git a/packages/dev-server-hmr/src/babel/utils.js b/packages/dev-server-hmr/src/babel/utils.js
new file mode 100644
index 00000000..4fe72e31
--- /dev/null
+++ b/packages/dev-server-hmr/src/babel/utils.js
@@ -0,0 +1,45 @@
+/** @typedef {import('./babelPluginWcHmr').BabelPluginWcHmrOptions} BabelPluginWcHmrOptions */
+/** @template T @typedef {import('@babel/core').NodePath} NodePath */
+
+const { createError } = require('../utils');
+
+/**
+ * @param {BabelPluginWcHmrOptions} options
+ * @returns {BabelPluginWcHmrOptions}
+ */
+function parseOptions(options) {
+ if (!options) throw createError('Missing babel plugin options');
+ if (!options.rootDir) throw createError('Missing rootDir in babel plugin options');
+ return options;
+}
+
+/**
+ * @template T
+ * @param {NodePath} nodePath
+ */
+function resolvePath(nodePath) {
+ const pathCast = /** @type {any} */ (nodePath);
+ return /** @type {NodePath} */ (pathCast.resolve());
+}
+
+/**
+ * @template T
+ * @param {NodePath | NodePath[]} nodePath
+ * @returns {nodePath is NodePath}
+ */
+function singlePath(nodePath) {
+ return nodePath && !Array.isArray(nodePath);
+}
+
+/**
+ * @template T
+ * @param {Set} set
+ * @param {T[]} elements
+ */
+function addToSet(set, elements) {
+ for (const e of elements) {
+ set.add(e);
+ }
+}
+
+module.exports = { parseOptions, resolvePath, singlePath, addToSet };
diff --git a/packages/dev-server-hmr/src/constants.js b/packages/dev-server-hmr/src/constants.js
new file mode 100644
index 00000000..6c6bc7d5
--- /dev/null
+++ b/packages/dev-server-hmr/src/constants.js
@@ -0,0 +1,11 @@
+const WC_HMR_NAMESPACE = '__$wc_hmr$__';
+const WC_HMR_MODULE_PREFIX = '/__web-dev-server__/wc-hmr/';
+const WC_HMR_MODULE_PATCH = `${WC_HMR_MODULE_PREFIX}patch.js`;
+const WC_HMR_MODULE_RUNTIME = `${WC_HMR_MODULE_PREFIX}runtime.js`;
+
+module.exports = {
+ WC_HMR_NAMESPACE,
+ WC_HMR_MODULE_PREFIX,
+ WC_HMR_MODULE_PATCH,
+ WC_HMR_MODULE_RUNTIME,
+};
diff --git a/packages/dev-server-hmr/src/hmrPlugin.js b/packages/dev-server-hmr/src/hmrPlugin.js
new file mode 100644
index 00000000..805ee070
--- /dev/null
+++ b/packages/dev-server-hmr/src/hmrPlugin.js
@@ -0,0 +1,149 @@
+/** @typedef {import('@web/dev-server-core').Plugin} DevServerPlugin */
+/** @typedef {import('./utils').Matcher} Matcher */
+
+/**
+ * @typedef {object} BaseClass
+ * @property {string} name
+ * @property {string} [import]
+ */
+
+/**
+ * @typedef {object} Decorator
+ * @property {string} name
+ * @property {string} [import]
+ */
+
+/**
+ * @typedef {object} Preset
+ * @property {BaseClass[]} baseClasses
+ * @property {Decorator[]} decorators
+ * @property {string} patch
+ */
+
+/**
+ * @typedef {object} WcHmrPluginConfig
+ * @property {string[]} [include]
+ * @property {string[]} [exclude]
+ * @property {Preset[]} [presets]
+ * @property {BaseClass[]} [baseClasses]
+ * @property {Decorator[]} [decorators]
+ * @property {string[]} [patches]
+ */
+
+const { getRequestFilePath, PluginSyntaxError } = require('@web/dev-server-core');
+const { hmrPlugin: createBaseHmrPlugin } = require('@web/dev-server-hmr');
+
+const { WC_HMR_MODULE_PREFIX, WC_HMR_MODULE_RUNTIME, WC_HMR_MODULE_PATCH } = require('./constants');
+const { parseConfig, createMatchers, createError } = require('./utils');
+const { babelTransform } = require('./babel/babelTransform');
+const { wcHmrRuntime } = require('./wcHmrRuntime');
+
+/**
+ * @param {WcHmrPluginConfig} pluginConfig
+ * @returns {DevServerPlugin}
+ */
+function hmrPlugin(pluginConfig) {
+ const baseHmrPlugin = createBaseHmrPlugin();
+ const parsedPluginConfig = parseConfig(pluginConfig);
+
+ /** @type {string} */
+ let rootDir;
+ /** @type {Matcher} */
+ let matchInclude = () => true;
+ /** @type {Matcher} */
+ let matchExclude = () => false;
+
+ return {
+ name: 'wc-hmr',
+ injectWebSocket: true,
+
+ resolveImport(...args) {
+ const { source } = args[0];
+ if (source.startsWith(WC_HMR_MODULE_PREFIX)) {
+ return source;
+ }
+
+ return baseHmrPlugin.resolveImport?.(...args);
+ },
+
+ serve(...args) {
+ const context = args[0];
+ if (context.path === WC_HMR_MODULE_RUNTIME) {
+ return wcHmrRuntime;
+ }
+
+ if (context.path === WC_HMR_MODULE_PATCH) {
+ return pluginConfig.patches?.join('\n') || '';
+ }
+
+ return baseHmrPlugin.serve?.(...args);
+ },
+
+ serverStart(...args) {
+ if (args[0].config.plugins?.find(pl => pl.name === 'hmr')) {
+ throw createError(
+ `Cannot include both @web/dev-server-hmr and @open-wc/dev-server-hmr plugins.`,
+ );
+ }
+
+ rootDir = args[0].config.rootDir;
+ if (parsedPluginConfig.include) {
+ matchInclude = createMatchers(rootDir, parsedPluginConfig.include);
+ }
+
+ if (parsedPluginConfig.exclude) {
+ matchExclude = createMatchers(rootDir, parsedPluginConfig.exclude);
+ }
+
+ return baseHmrPlugin.serverStart?.(...args);
+ },
+
+ async transform(...args) {
+ const context = args[0];
+ if (!context.response.is('js')) {
+ return;
+ }
+
+ const filePath = getRequestFilePath(context, rootDir);
+ if (
+ matchInclude(filePath) &&
+ !matchExclude(filePath) &&
+ !filePath.startsWith('__web-dev-server__')
+ ) {
+ try {
+ context.body = await babelTransform(context.body, filePath, {
+ baseClasses: parsedPluginConfig.baseClasses,
+ decorators: parsedPluginConfig.decorators,
+ patches: parsedPluginConfig.patches,
+ rootDir,
+ });
+ } catch (error) {
+ if (error.name === 'SyntaxError') {
+ // forward babel error to dev server
+ const strippedMsg = error.message.replace(new RegExp(`${filePath} ?:? ?`, 'g'), '');
+ throw new PluginSyntaxError(strippedMsg, filePath, error.code, error.loc, error.pos);
+ }
+ throw error;
+ }
+ }
+
+ return baseHmrPlugin.transform?.(...args);
+ },
+
+ // forward all other plugin hooks
+ serverStop(...args) {
+ return baseHmrPlugin.serverStop?.(...args);
+ },
+ transformCacheKey(...args) {
+ return baseHmrPlugin.transformCacheKey?.(...args);
+ },
+ transformImport(...args) {
+ return baseHmrPlugin.transformImport?.(...args);
+ },
+ resolveMimeType(...args) {
+ return baseHmrPlugin.resolveMimeType?.(...args);
+ },
+ };
+}
+
+module.exports = { hmrPlugin };
diff --git a/packages/dev-server-hmr/src/index.js b/packages/dev-server-hmr/src/index.js
new file mode 100644
index 00000000..d9bf1788
--- /dev/null
+++ b/packages/dev-server-hmr/src/index.js
@@ -0,0 +1,12 @@
+const { hmrPlugin } = require('./hmrPlugin');
+const { litElement } = require('./presets/litElement');
+const { fastElement } = require('./presets/fastElement');
+const { WC_HMR_MODULE_RUNTIME } = require('./constants');
+
+const presets = { litElement, fastElement };
+
+module.exports = {
+ hmrPlugin,
+ presets,
+ WC_HMR_MODULE_RUNTIME,
+};
diff --git a/packages/dev-server-hmr/src/presets/fastElement.js b/packages/dev-server-hmr/src/presets/fastElement.js
new file mode 100644
index 00000000..8c5ef03f
--- /dev/null
+++ b/packages/dev-server-hmr/src/presets/fastElement.js
@@ -0,0 +1,20 @@
+const { WC_HMR_MODULE_RUNTIME } = require('../constants');
+
+const patch = `import { FASTElement, FASTElementDefinition } from '@microsoft/fast-element';
+import { updateClassMembers } from '${WC_HMR_MODULE_RUNTIME}';
+
+FASTElement.prototype.hotReplaceCallback = function hotReplaceCallback(newClass) {
+ const newDefinition = FASTElementDefinition.forType(newClass);
+ if (newDefinition) {
+ this.$fastController.styles = newDefinition.styles;
+ this.$fastController.template = newDefinition.template;
+ }
+};`;
+
+const fastElement = {
+ decorators: [{ name: 'customElement', import: '@microsoft/fast-element' }],
+ baseClasses: [{ name: 'FASTElement', import: '@microsoft/fast-element' }],
+ patch,
+};
+
+module.exports = { fastElement };
diff --git a/packages/dev-server-hmr/src/presets/litElement.js b/packages/dev-server-hmr/src/presets/litElement.js
new file mode 100644
index 00000000..f8bb0a2e
--- /dev/null
+++ b/packages/dev-server-hmr/src/presets/litElement.js
@@ -0,0 +1,41 @@
+const { WC_HMR_MODULE_RUNTIME } = require('../constants');
+
+const patch = `import { LitElement } from 'lit-element';
+import { updateClassMembers } from '${WC_HMR_MODULE_RUNTIME}';
+const supportsAdoptingStyleSheets = (window.ShadowRoot) &&
+ (window.ShadyCSS === undefined || window.ShadyCSS.nativeShadow) &&
+ ('adoptedStyleSheets' in Document.prototype) &&
+ ('replace' in CSSStyleSheet.prototype);
+
+// static callback
+LitElement.hotReplaceCallback = function hotReplaceCallback(newClass) {
+ newClass.finalize();
+ updateClassMembers(this, newClass);
+ this.finalize();
+};
+
+// instance callback
+LitElement.prototype.hotReplaceCallback = function hotReplaceCallback() {
+ if (!supportsAdoptingStyleSheets) {
+ const nodes = Array.from(this.renderRoot.children);
+ for (const node of nodes) {
+ if (node.tagName.toLowerCase() === 'style') {
+ node.remove();
+ }
+ }
+ }
+
+ this.constructor._getUniqueStyles();
+ if (window.ShadowRoot && this.renderRoot instanceof window.ShadowRoot) {
+ this.adoptStyles();
+ }
+ this.requestUpdate();
+};`;
+
+const litElement = {
+ decorators: [{ name: 'customElement', import: 'lit-element' }],
+ baseClasses: [{ name: 'LitElement', import: 'lit-element' }],
+ patch,
+};
+
+module.exports = { litElement };
diff --git a/packages/dev-server-hmr/src/utils.js b/packages/dev-server-hmr/src/utils.js
new file mode 100644
index 00000000..2eb1ecbf
--- /dev/null
+++ b/packages/dev-server-hmr/src/utils.js
@@ -0,0 +1,87 @@
+/* eslint-disable no-param-reassign */
+const picoMatch = require('picomatch');
+const { isAbsolute, posix, sep } = require('path');
+
+/** @typedef {(path: string) => boolean} Matcher */
+/** @typedef {import('./hmrPlugin').WcHmrPluginConfig} WcHmrPluginConfig */
+
+/**
+ * @param {string} msg
+ */
+function createError(msg) {
+ return new Error(`[@open-wc/dev-server-hmr] ${msg}`);
+}
+
+/**
+ * @param {WcHmrPluginConfig} config
+ * @param {keyof WcHmrPluginConfig} prop
+ */
+function assertOptionalArray(config, prop) {
+ if (config[prop] != null && !Array.isArray(config[prop])) {
+ throw createError(`Option ${prop} must be an array`);
+ }
+}
+
+/**
+ * @param {WcHmrPluginConfig} config
+ */
+function parseConfig(config) {
+ if (!Array.isArray(config.include) && !Array.isArray(config.exclude)) {
+ throw createError('Must provide either an "include" or "exclude" pattern in config.');
+ }
+ assertOptionalArray(config, 'include');
+ assertOptionalArray(config, 'exclude');
+ assertOptionalArray(config, 'patches');
+ assertOptionalArray(config, 'baseClasses');
+ assertOptionalArray(config, 'decorators');
+ assertOptionalArray(config, 'presets');
+
+ config.baseClasses = config.baseClasses || [];
+ config.baseClasses.push({ name: 'HTMLElement' });
+
+ if (config.presets) {
+ for (const preset of config.presets) {
+ if (preset.patch) {
+ config.patches = config.patches || [];
+ config.patches.push(preset.patch);
+ }
+ if (preset.baseClasses) {
+ config.baseClasses.push(...preset.baseClasses);
+ }
+ if (preset.decorators) {
+ config.decorators = config.decorators || [];
+ config.decorators.push(...preset.decorators);
+ }
+ }
+ }
+
+ return { ...config };
+}
+
+/**
+ * @param {string} rootDir
+ * @param {string} pattern
+ * @returns {Matcher}
+ */
+function createMatcher(rootDir, pattern) {
+ const matcherRootDir = rootDir.split(sep).join('/');
+ const resolvedPattern =
+ !isAbsolute(pattern) && !pattern.startsWith('*')
+ ? posix.join(matcherRootDir, pattern)
+ : pattern;
+ return picoMatch(resolvedPattern);
+}
+
+/**
+ * @param {string} rootDir
+ * @param {string[]} patterns
+ * @returns {Matcher}
+ */
+function createMatchers(rootDir, patterns) {
+ const matchers = patterns.map(p => createMatcher(rootDir, p));
+ return function matcher(path) {
+ return matchers.some(m => m(path));
+ };
+}
+
+module.exports = { createMatcher, createMatchers, parseConfig, createError };
diff --git a/packages/dev-server-hmr/src/wcHmrRuntime.js b/packages/dev-server-hmr/src/wcHmrRuntime.js
new file mode 100644
index 00000000..8eca1f8a
--- /dev/null
+++ b/packages/dev-server-hmr/src/wcHmrRuntime.js
@@ -0,0 +1,96 @@
+const wcHmrRuntime = `
+// override global define to allow double registrations
+const originalDefine = window.customElements.define;
+window.customElements.define = (name, ...rest) => {
+ if (!window.customElements.get(name)) {
+ originalDefine.call(window.customElements, name, ...rest);
+ }
+};
+
+const registry = new Map();
+
+function createClassKey(importMetaUrl, className) {
+ const modulePath = new URL(importMetaUrl).pathname;
+ return \`\${modulePath}:\${className}\`;
+}
+
+export function register(importMetaUrl, hmrClass) {
+ const key = createClassKey(importMetaUrl, hmrClass.name);
+ const hotReplaceCallback = registry.get(key);
+ if (hotReplaceCallback) {
+ // class is already registered, call the replace function registered below
+ hotReplaceCallback(hmrClass);
+ return;
+ }
+ // class is not yet registered, set it up and register an update callback
+ const connectedElements = trackConnectedElements(hmrClass);
+
+ // register a callback for this class later patch in updates to the class
+ registry.set(key, async newClass => {
+ // wait 1 microtask to allow the class definition to propagate
+ await 0;
+
+ if (hmrClass.hotReplaceCallback) {
+ // class has implemented a callback, use that
+ hmrClass.hotReplaceCallback(newClass);
+ } else {
+ // otherwise apply default update
+ updateClassMembers(hmrClass, newClass);
+ }
+
+ for (const element of connectedElements) {
+ if (element.hotReplaceCallback) {
+ element.hotReplaceCallback(newClass);
+ }
+ }
+ });
+}
+
+function trackConnectedElements(hmrClass) {
+ const connectedElements = new Set();
+ const originalCb = hmrClass.prototype.connectedCallback;
+ hmrClass.prototype.connectedCallback = function connectedCallback(...args) {
+ if (originalCb) {
+ originalCb.call(this, ...args);
+ }
+ connectedElements.add(this);
+ };
+
+ const originalDcb = hmrClass.prototype.disconnectedCallback;
+ hmrClass.prototype.disconnectedCallback = function disconnectedCallback(...args) {
+ if (originalDcb) {
+ originalDcb.call(this, ...args);
+ }
+ connectedElements.delete(this);
+ };
+ return connectedElements;
+}
+
+const preserved = ['connectedCallback', 'disconnectedCallback', 'observedAttributes'];
+
+export function updateClassMembers(hmrClass, newClass) {
+ updateObjectMembers(hmrClass, newClass);
+ updateObjectMembers(hmrClass.prototype, newClass.prototype);
+}
+
+export function updateObjectMembers(hmrClass, newClass) {
+ const currentProperties = new Set(Object.getOwnPropertyNames(hmrClass));
+ const newProperties = new Set(Object.getOwnPropertyNames(newClass));
+
+ for (const prop of Object.getOwnPropertyNames(newClass)) {
+ const descriptor = Object.getOwnPropertyDescriptor(newClass, prop);
+ if (descriptor && descriptor.configurable) {
+ Object.defineProperty(hmrClass, prop, descriptor);
+ }
+ }
+
+ for (const existingProp of currentProperties) {
+ if (!preserved.includes(existingProp) && !newProperties.has(existingProp)) {
+ try {
+ delete hmrClass[existingProp];
+ } catch {}
+ }
+ }
+}`;
+
+module.exports = { wcHmrRuntime };
diff --git a/packages/dev-server-hmr/test-node/babel/base-class.test.js b/packages/dev-server-hmr/test-node/babel/base-class.test.js
new file mode 100644
index 00000000..70d155f2
--- /dev/null
+++ b/packages/dev-server-hmr/test-node/babel/base-class.test.js
@@ -0,0 +1,126 @@
+/* eslint-disable import/no-extraneous-dependencies */
+const { expect } = require('chai');
+const path = require('path');
+const { banner, transform, rootDir } = require('./utils');
+
+describe('babelPluginWcHmr - detecting base class', () => {
+ it('global base class', () => {
+ const code = `class Foo extends MyElement {}`;
+ const result = transform(code, { baseClasses: [{ name: 'MyElement' }] });
+
+ expect(result).to.equal(`${banner}
+class Foo extends MyElement {}
+
+__$wc_hmr$__.register(import.meta.url, Foo);`);
+ });
+
+ it('named import', () => {
+ const code = `import { MyElement } from 'my-element'; class Foo extends MyElement {}`;
+ const result = transform(code, { baseClasses: [{ name: 'MyElement', import: 'my-element' }] });
+
+ expect(result).to.equal(`${banner}
+import { MyElement } from 'my-element';
+
+class Foo extends MyElement {}
+
+__$wc_hmr$__.register(import.meta.url, Foo);`);
+ });
+
+ it('unmatched import', () => {
+ const code = `import { MyElement } from 'not-my-element'; class Foo extends MyElement {}`;
+ const result = transform(code, { baseClasses: [{ name: 'MyElement', import: 'my-element' }] });
+
+ expect(result).to.equal(`import { MyElement } from 'not-my-element';
+
+class Foo extends MyElement {}`);
+ });
+
+ it('default import', () => {
+ const code = `import BaseElement from 'base-element'; class Foo extends BaseElement {}`;
+ const result = transform(code, { baseClasses: [{ name: 'default', import: 'base-element' }] });
+ expect(result).to.equal(`${banner}
+import BaseElement from 'base-element';
+
+class Foo extends BaseElement {}
+
+__$wc_hmr$__.register(import.meta.url, Foo);`);
+ });
+
+ it('mixins', () => {
+ const code = `import { MyElement } from 'my-element'; class Foo extends MixA(MixB(MyElement)) {}`;
+ const result = transform(code, { baseClasses: [{ name: 'MyElement', import: 'my-element' }] });
+
+ expect(result).to.equal(`${banner}
+import { MyElement } from 'my-element';
+
+class Foo extends MixA(MixB(MyElement)) {}
+
+__$wc_hmr$__.register(import.meta.url, Foo);`);
+ });
+
+ it('multiple classes', () => {
+ const code = `import { MyElement } from 'my-element';
+class A extends MyElement {}
+class B {}
+class C extends HTMLElement {}
+class D extends MyElement {}`;
+ const result = transform(code, { baseClasses: [{ name: 'MyElement', import: 'my-element' }] });
+
+ expect(result).to.equal(`${banner}
+import { MyElement } from 'my-element';
+
+class A extends MyElement {}
+
+__$wc_hmr$__.register(import.meta.url, A);
+
+class B {}
+
+class C extends HTMLElement {}
+
+class D extends MyElement {}
+
+__$wc_hmr$__.register(import.meta.url, D);`);
+ });
+
+ it('multiple base class definitions', () => {
+ const code = `import { ElementA } from 'element-a';
+import ElementB from 'element-b';
+class A extends ElementA {}
+class B extends ElementB {}
+class C extends HTMLElement {}`;
+ const result = transform(code, {
+ baseClasses: [
+ { name: 'ElementA', import: 'element-a' },
+ { name: 'default', import: 'element-b' },
+ ],
+ });
+
+ expect(result).to.equal(`${banner}
+import { ElementA } from 'element-a';
+import ElementB from 'element-b';
+
+class A extends ElementA {}
+
+__$wc_hmr$__.register(import.meta.url, A);
+
+class B extends ElementB {}
+
+__$wc_hmr$__.register(import.meta.url, B);
+
+class C extends HTMLElement {}`);
+ });
+
+ it('base class with a specific import path', () => {
+ const code = "import { MyElement } from '../MyElement.js'; class Foo extends MyElement {}";
+ const result = transform(code, {
+ baseClasses: [{ name: 'MyElement', import: path.join(rootDir, 'MyElement.js') }],
+ });
+
+ expect(result).to.equal(`${banner}
+import { MyElement } from '../MyElement.js';
+
+class Foo extends MyElement {}
+
+__$wc_hmr$__.register(import.meta.url, Foo);`);
+ });
+});
diff --git a/packages/dev-server-hmr/test-node/babel/customElements.test.js b/packages/dev-server-hmr/test-node/babel/customElements.test.js
new file mode 100644
index 00000000..88d54188
--- /dev/null
+++ b/packages/dev-server-hmr/test-node/babel/customElements.test.js
@@ -0,0 +1,138 @@
+/* eslint-disable import/no-extraneous-dependencies */
+const { expect } = require('chai');
+const { banner, transform } = require('./utils');
+
+describe('babelPluginWcHmr - detecting customElements.define', () => {
+ it('injects registration when detecting a customElements.define', () => {
+ const code = `class Foo extends HTMLElement {}\ncustomElements.define('x-foo', Foo);`;
+ const result = transform(code);
+ expect(result).to.equal(
+ `${banner}
+class Foo extends HTMLElement {}
+
+__$wc_hmr$__.register(import.meta.url, Foo);
+
+customElements.define('x-foo', Foo);`,
+ );
+ });
+
+ it('injects registration when detecting a window.customElements.define', () => {
+ const code = `class Foo extends HTMLElement {}\nwindow.customElements.define('x-foo', Foo);`;
+ const result = transform(code);
+ expect(result).to.equal(
+ `${banner}
+class Foo extends HTMLElement {}
+
+__$wc_hmr$__.register(import.meta.url, Foo);
+
+window.customElements.define('x-foo', Foo);`,
+ );
+ });
+
+ it('injects registration when detecting a globalThis.customElements.define', () => {
+ const code = `class Foo extends HTMLElement {}\nglobalThis.customElements.define('x-foo', Foo);`;
+ const result = transform(code);
+ expect(result).to.equal(
+ `${banner}
+class Foo extends HTMLElement {}
+
+__$wc_hmr$__.register(import.meta.url, Foo);
+
+globalThis.customElements.define('x-foo', Foo);`,
+ );
+ });
+
+ it('injects multiple registrations', () => {
+ const code =
+ "class Foo extends HTMLElement {}\ncustomElements.define('x-foo', Foo);\n" +
+ "class Bar extends HTMLElement {}\ncustomElements.define('x-bar', Bar);" +
+ "class Baz extends HTMLElement {}\ncustomElements.define('x-baz', Baz);";
+ // console.log(code);
+ const result = transform(code);
+ expect(result).to.equal(
+ `${banner}
+class Foo extends HTMLElement {}
+
+__$wc_hmr$__.register(import.meta.url, Foo);
+
+customElements.define('x-foo', Foo);
+
+class Bar extends HTMLElement {}
+
+__$wc_hmr$__.register(import.meta.url, Bar);
+
+customElements.define('x-bar', Bar);
+
+class Baz extends HTMLElement {}
+
+__$wc_hmr$__.register(import.meta.url, Baz);
+
+customElements.define('x-baz', Baz);`,
+ );
+ });
+
+ it('can configure a patch to be injected', () => {
+ const code = `class Foo extends HTMLElement {}\ncustomElements.define('x-foo', Foo);`;
+ const result = transform(code, { patches: ['x.js'] });
+ expect(result).to.equal(
+ `import '/__web-dev-server__/wc-hmr/patch.js';
+${banner}
+class Foo extends HTMLElement {}
+
+__$wc_hmr$__.register(import.meta.url, Foo);
+
+customElements.define('x-foo', Foo);`,
+ );
+ });
+
+ it('injects registration when detecting an inline class', () => {
+ const code = `customElements.define('x-foo', class Foo extends HTMLElement {});`;
+ const result = transform(code);
+ expect(result).to.equal(`${banner}
+const _Foo = class Foo extends HTMLElement {};
+
+__$wc_hmr$__.register(import.meta.url, _Foo);
+
+customElements.define('x-foo', _Foo);`);
+ });
+
+ it('injects registration when detecting an anonymous inline class', () => {
+ const code = `customElements.define('x-foo', class extends HTMLElement {});`;
+ const result = transform(code);
+ expect(result).to.equal(`${banner}
+const _ref = class extends HTMLElement {};
+
+__$wc_hmr$__.register(import.meta.url, _ref);
+
+customElements.define('x-foo', _ref);`);
+ });
+
+ it('deconflicts variable names', () => {
+ const code = `const Foo = 1; const _Foo = 2; customElements.define('x-foo', class Foo extends HTMLElement {});`;
+ const result = transform(code);
+ expect(result).to.equal(`${banner}
+const Foo = 1;
+const _Foo = 2;
+
+const _Foo2 = class Foo extends HTMLElement {};
+
+__$wc_hmr$__.register(import.meta.url, _Foo2);
+
+customElements.define('x-foo', _Foo2);`);
+ });
+
+ it('respects the custom element define scope', () => {
+ const code = `console.log("x"); (function () { customElements.define('x-foo', class extends HTMLElement {}); })();`;
+ const result = transform(code);
+ expect(result).to.equal(`${banner}
+console.log("x");
+
+(function () {
+ const _ref = class extends HTMLElement {};
+
+ __$wc_hmr$__.register(import.meta.url, _ref);
+
+ customElements.define('x-foo', _ref);
+})();`);
+ });
+});
diff --git a/packages/dev-server-hmr/test-node/babel/decorators.test.js b/packages/dev-server-hmr/test-node/babel/decorators.test.js
new file mode 100644
index 00000000..1593b173
--- /dev/null
+++ b/packages/dev-server-hmr/test-node/babel/decorators.test.js
@@ -0,0 +1,115 @@
+/* eslint-disable import/no-extraneous-dependencies */
+const { expect } = require('chai');
+const path = require('path');
+const { banner, transform, rootDir } = require('./utils');
+
+describe('babelPluginWcHmr - detecting decorators', () => {
+ it('compiled decorator with decorator name', () => {
+ const code = `
+function __decorate() {}
+let Foo = class extends HTMLElement {};
+Foo = __decorate([customElement('x-foo')], Foo);`;
+ const result = transform(code, { decorators: [{ name: 'customElement' }] });
+
+ expect(result).to.equal(`${banner}
+function __decorate() {}
+
+let Foo = class extends HTMLElement {};
+
+__$wc_hmr$__.register(import.meta.url, Foo);
+
+Foo = __decorate([customElement('x-foo')], Foo);`);
+ });
+
+ it('handles multiple decorators', () => {
+ const code = `
+function __decorate() {}
+let Foo = class extends HTMLElement {};
+Foo = __decorate([x(), y(), customElement('x-foo')], Foo);`;
+ const result = transform(code, { decorators: [{ name: 'customElement' }] });
+
+ expect(result).to.equal(`${banner}
+function __decorate() {}
+
+let Foo = class extends HTMLElement {};
+
+__$wc_hmr$__.register(import.meta.url, Foo);
+
+Foo = __decorate([x(), y(), customElement('x-foo')], Foo);`);
+ });
+
+ it('does not inject for a decorator with a different name', () => {
+ const code = `function __decorate() {}
+
+let Foo = class extends HTMLElement {};
+Foo = __decorate([notCustomElement('x-foo')], Foo);`;
+ const result = transform(code, { decorators: [{ name: 'customElement' }] });
+ expect(result).to.equal(code);
+ });
+
+ it('compiled decorator with a bare import', () => {
+ const code = `
+import {customElement} from 'my-package';
+function __decorate() {}
+let Foo = class extends HTMLElement {};
+Foo = __decorate([customElement('x-foo')], Foo);`;
+ const result = transform(code, {
+ decorators: [{ name: 'customElement', import: 'my-package' }],
+ });
+
+ expect(result).to.equal(`${banner}
+import { customElement } from 'my-package';
+
+function __decorate() {}
+
+let Foo = class extends HTMLElement {};
+
+__$wc_hmr$__.register(import.meta.url, Foo);
+
+Foo = __decorate([customElement('x-foo')], Foo);`);
+ });
+
+ it('compiled decorator with a default import', () => {
+ const code = `
+import customElement from 'my-package';
+function __decorate() {}
+let Foo = class extends HTMLElement {};
+Foo = __decorate([customElement('x-foo')], Foo);`;
+ const result = transform(code, {
+ decorators: [{ name: 'default', import: 'my-package' }],
+ });
+
+ expect(result).to.equal(`${banner}
+import customElement from 'my-package';
+
+function __decorate() {}
+
+let Foo = class extends HTMLElement {};
+
+__$wc_hmr$__.register(import.meta.url, Foo);
+
+Foo = __decorate([customElement('x-foo')], Foo);`);
+ });
+
+ it('compiled decorator with a specific import path', () => {
+ const code = `
+import { defineElement } from '../defineElement.js';
+function __decorate() {}
+let Foo = class extends HTMLElement {};
+Foo = __decorate([defineElement('x-foo')], Foo);`;
+ const result = transform(code, {
+ decorators: [{ name: 'defineElement', import: path.join(rootDir, 'defineElement.js') }],
+ });
+
+ expect(result).to.equal(`${banner}
+import { defineElement } from '../defineElement.js';
+
+function __decorate() {}
+
+let Foo = class extends HTMLElement {};
+
+__$wc_hmr$__.register(import.meta.url, Foo);
+
+Foo = __decorate([defineElement('x-foo')], Foo);`);
+ });
+});
diff --git a/packages/dev-server-hmr/test-node/babel/utils.js b/packages/dev-server-hmr/test-node/babel/utils.js
new file mode 100644
index 00000000..c7fb25fe
--- /dev/null
+++ b/packages/dev-server-hmr/test-node/babel/utils.js
@@ -0,0 +1,27 @@
+const { transformSync } = require('@babel/core');
+const path = require('path');
+const babelPluginWcHmr = require('../../src/babel/babelPluginWcHmr');
+
+const banner = `import * as __$wc_hmr$__ from '/__web-dev-server__/wc-hmr/runtime.js';
+
+if (import.meta.hot) {
+ import.meta.hot.accept();
+}
+`;
+
+const rootDir = path.join(process.cwd(), 'virtual-project');
+
+/**
+ * @param {string} code
+ * @returns {string}
+ */
+function transform(code, extraOptions) {
+ return transformSync(code, {
+ babelrc: false,
+ configFile: false,
+ filename: path.join(rootDir, 'src', 'foo.js'),
+ plugins: [[babelPluginWcHmr, { rootDir, ...extraOptions }]],
+ }).code;
+}
+
+module.exports = { banner, transform, rootDir };
diff --git a/packages/dev-server-hmr/tsconfig.json b/packages/dev-server-hmr/tsconfig.json
new file mode 100644
index 00000000..8cb3cc39
--- /dev/null
+++ b/packages/dev-server-hmr/tsconfig.json
@@ -0,0 +1,24 @@
+// Don't edit this file directly. It is generated by /scripts/update-package-configs.ts
+
+{
+ "extends": "../../tsconfig.node-base.json",
+ "compilerOptions": {
+ "module": "commonjs",
+ "outDir": "./types",
+ "rootDir": ".",
+ "composite": true,
+ "allowJs": true,
+ "strict": true,
+ "checkJs": true,
+ "emitDeclarationOnly": true
+ },
+ "references": [],
+ "include": [
+ "src",
+ "*.js"
+ ],
+ "exclude": [
+ "dist",
+ "types"
+ ]
+}
\ No newline at end of file
diff --git a/packages/storybook-addon-markdown-docs/package.json b/packages/storybook-addon-markdown-docs/package.json
index 4a4584ab..1bb958c7 100644
--- a/packages/storybook-addon-markdown-docs/package.json
+++ b/packages/storybook-addon-markdown-docs/package.json
@@ -48,7 +48,7 @@
"@types/babel__core": "^7.1.3",
"@types/chai": "^4.2.11",
"@types/mocha": "^5.0.0",
- "@web/dev-server": "^0.0.19",
+ "@web/dev-server": "^0.0.24",
"@web/dev-server-storybook": "^0.1.3",
"babel-loader": "^8.0.0",
"mocha-chai-snapshot": "^1.0.0"
diff --git a/scripts/generate-ts-configs.mjs b/scripts/generate-ts-configs.mjs
index 9dea44f3..678842a2 100644
--- a/scripts/generate-ts-configs.mjs
+++ b/scripts/generate-ts-configs.mjs
@@ -80,6 +80,7 @@ packageDirnameMap.forEach((packageDirname, packageName) => {
rootDir: '.',
composite: true,
allowJs: true,
+ strict: pkg.strict,
checkJs: pkg.type === 'js' ? true : undefined,
emitDeclarationOnly: pkg.type === 'js' ? true : undefined,
},
diff --git a/tsconfig.json b/tsconfig.json
index d7851e17..105a57d5 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -10,6 +10,9 @@
{
"path": "./packages/polyfills-loader/tsconfig.json"
},
+ {
+ "path": "./packages/dev-server-hmr/tsconfig.json"
+ },
{
"path": "./packages/import-maps-resolve/tsconfig.json"
},
diff --git a/workspace-packages.mjs b/workspace-packages.mjs
index b9af15d0..437d6795 100644
--- a/workspace-packages.mjs
+++ b/workspace-packages.mjs
@@ -1,5 +1,6 @@
const packages = [
{ name: 'polyfills-loader', type: 'js', environment: 'node' },
+ { name: 'dev-server-hmr', type: 'js', environment: 'node', strict: true },
{ name: 'import-maps-resolve', type: 'js', environment: 'node' },
{ name: 'scoped-elements', type: 'js', environment: 'browser' },
{ name: 'testing-helpers', type: 'js', environment: 'browser' },
diff --git a/yarn.lock b/yarn.lock
index d24b9a0d..d2578271 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -361,16 +361,16 @@
chalk "^2.0.0"
js-tokens "^4.0.0"
+"@babel/parser@*", "@babel/parser@^7.12.5":
+ version "7.12.5"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.5.tgz#b4af32ddd473c0bfa643bd7ff0728b8e71b81ea0"
+ integrity sha512-FVM6RZQ0mn2KCf1VUED7KepYeUWoVShczewOCfm3nzoBybaih51h+sYVVGthW9M6lPByEPTQf+xm27PBdlpwmQ==
+
"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.1.6", "@babel/parser@^7.10.4", "@babel/parser@^7.11.5", "@babel/parser@^7.12.0", "@babel/parser@^7.12.1", "@babel/parser@^7.12.3", "@babel/parser@^7.4.2", "@babel/parser@^7.4.3", "@babel/parser@^7.6.0", "@babel/parser@^7.7.0", "@babel/parser@^7.9.6":
version "7.12.3"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.3.tgz#a305415ebe7a6c7023b40b5122a0662d928334cd"
integrity sha512-kFsOS0IbsuhO5ojF8Hc8z/8vEIOkylVBrjiZUbLTE3XFe0Qi+uu6HjzQixkFaqr0ZPAMZcBVxEwmsnsLPZ2Xsw==
-"@babel/parser@^7.12.5":
- version "7.12.5"
- resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.5.tgz#b4af32ddd473c0bfa643bd7ff0728b8e71b81ea0"
- integrity sha512-FVM6RZQ0mn2KCf1VUED7KepYeUWoVShczewOCfm3nzoBybaih51h+sYVVGthW9M6lPByEPTQf+xm27PBdlpwmQ==
-
"@babel/plugin-proposal-async-generator-functions@^7.12.1":
version "7.12.1"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.12.1.tgz#dc6c1170e27d8aca99ff65f4925bd06b1c90550e"
@@ -522,6 +522,13 @@
dependencies:
"@babel/helper-plugin-utils" "^7.10.4"
+"@babel/plugin-syntax-import-assertions@^7.12.1":
+ version "7.12.1"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.12.1.tgz#fd96f8f7bb681fa7dacfe470fa41add68f7f241f"
+ integrity sha512-eyHdg2/DBrQfPgT7URyQacH0BuR2yJoKPkc5yQwRSh6yfA+77A5JRRGxwSOWHpsitWxtimHfPqJ+sZA2DuDJLg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.10.4"
+
"@babel/plugin-syntax-import-meta@^7.10.4", "@babel/plugin-syntax-import-meta@^7.2.0":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51"
@@ -1735,6 +1742,13 @@
dependencies:
extend "3.0.2"
+"@mdn/browser-compat-data@^2.0.7":
+ version "2.0.7"
+ resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-2.0.7.tgz#72ec37b9c1e00ce0b4e0309d753be18e2da12ee3"
+ integrity sha512-GeeM827DlzFFidn1eKkMBiqXFD2oLsnZbaiGhByPl0vcapsRzUL+t9hDoov1swc9rB2jw64R+ihtzC8qOE9wXw==
+ dependencies:
+ extend "3.0.2"
+
"@mdx-js/loader@^1.5.1":
version "1.6.19"
resolved "https://registry.yarnpkg.com/@mdx-js/loader/-/loader-1.6.19.tgz#2b90dee54b6f959539f310776f18fe24e1f15cc5"
@@ -1809,6 +1823,11 @@
resolved "https://registry.yarnpkg.com/@mdx-js/util/-/util-1.6.21.tgz#25f97a0a1b76e78c16ae5d98c6c73e1be8d89e39"
integrity sha512-6sANhqfEHu6gdHZSrzDjN18Y48mIon8f2Os6J+IFmMHN0IhNG/0PUIIsI07kA1sZ9t6vgZNBloVmcDa5WOSe6A==
+"@microsoft/fast-element@^0.19.1":
+ version "0.19.1"
+ resolved "https://registry.yarnpkg.com/@microsoft/fast-element/-/fast-element-0.19.1.tgz#7b7cddc844168eb168f75e41ea63d23d942667cb"
+ integrity sha512-qfEqj4s7E3B0qsNRoiQyKF682fdpEbCINV/GS6tE/ZOFC/B0CKpcZjk3mWyw8XAfsLA50E4YuR9Hn8SuMc2sFg==
+
"@mrmlnc/readdir-enhanced@^2.2.1":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
@@ -3141,6 +3160,17 @@
resolved "https://registry.yarnpkg.com/@types/babel__code-frame/-/babel__code-frame-7.0.2.tgz#e0c0f1648cbc09a9d4e5b4ed2ae9a6f7c8f5aeb0"
integrity sha512-imO+jT/yjOKOAS5GQZ8SDtwiIloAGGr6OaZDKB0V5JVaSfGZLat5K5/ZRtyKW6R60XHV3RHYPTFfhYb+wDKyKg==
+"@types/babel__core@^7.1.12":
+ version "7.1.12"
+ resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.12.tgz#4d8e9e51eb265552a7e4f1ff2219ab6133bdfb2d"
+ integrity sha512-wMTHiiTiBAAPebqaPiPDLFA4LYPKr6Ph0Xq/6rq1Ur3v66HXyG+clfR9CNETkD7MQS8ZHvpQOtA53DLws5WAEQ==
+ dependencies:
+ "@babel/parser" "^7.1.0"
+ "@babel/types" "^7.0.0"
+ "@types/babel__generator" "*"
+ "@types/babel__template" "*"
+ "@types/babel__traverse" "*"
+
"@types/babel__core@^7.1.3":
version "7.1.10"
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.10.tgz#ca58fc195dd9734e77e57c6f2df565623636ab40"
@@ -3152,13 +3182,20 @@
"@types/babel__template" "*"
"@types/babel__traverse" "*"
-"@types/babel__generator@*":
+"@types/babel__generator@*", "@types/babel__generator@^7.6.2":
version "7.6.2"
resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.2.tgz#f3d71178e187858f7c45e30380f8f1b7415a12d8"
integrity sha512-MdSJnBjl+bdwkLskZ3NGFp9YcXGx5ggLpQQPqtgakVhsWK0hTtNYhjpZLlWQTviGTvF8at+Bvli3jV7faPdgeQ==
dependencies:
"@babel/types" "^7.0.0"
+"@types/babel__parser@^7.1.1":
+ version "7.1.1"
+ resolved "https://registry.yarnpkg.com/@types/babel__parser/-/babel__parser-7.1.1.tgz#39b311e54697636b0b98127700b5f01f5e1e85f4"
+ integrity sha512-baSzIb0QQOUQSglfR9gwXVSbHH91YvY00C9Zjq6E7sPdnp8oyPyUsonIj3SF4wUl0s96vR/kyWeVv30gmM/xZw==
+ dependencies:
+ "@babel/parser" "*"
+
"@types/babel__template@*":
version "7.0.3"
resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.0.3.tgz#b8aaeba0a45caca7b56a5de9459872dde3727214"
@@ -3167,7 +3204,7 @@
"@babel/parser" "^7.1.0"
"@babel/types" "^7.0.0"
-"@types/babel__traverse@*":
+"@types/babel__traverse@*", "@types/babel__traverse@^7.0.15":
version "7.0.15"
resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.15.tgz#db9e4238931eb69ef8aab0ad6523d4d4caa39d03"
integrity sha512-Pzh9O3sTK8V6I1olsXpCfj2k/ygO2q1X0vhhnDrEQyYLHZesWz+zMZMVcwXLCYf0U36EtmyYaFGPfXlTtDHe3A==
@@ -3627,6 +3664,11 @@
resolved "https://registry.yarnpkg.com/@types/path-is-inside/-/path-is-inside-1.0.0.tgz#02d6ff38975d684bdec96204494baf9f29f0e17f"
integrity sha512-hfnXRGugz+McgX2jxyy5qz9sB21LRzlGn24zlwN2KEgoPtEvjzNRrLtUkOOebPDPZl3Rq7ywKxYvylVcEZDnEw==
+"@types/picomatch@^2.2.1":
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/@types/picomatch/-/picomatch-2.2.1.tgz#f9e5a5e6ad03996832975ab7eadfa35791ca2a8f"
+ integrity sha512-26/tQcDmJXYHiaWAAIjnTVL5nwrT+IVaqFZIbBImAuKk/r/j1r/1hmZ7uaOzG6IknqP3QHcNNQ6QO8Vp28lUoA==
+
"@types/prop-types@*":
version "15.7.3"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"
@@ -3896,6 +3938,11 @@
resolved "https://registry.yarnpkg.com/@ungap/from-entries/-/from-entries-0.2.1.tgz#7e86196b8b2e99d73106a8f25c2a068326346354"
integrity sha512-CAqefTFAfnUPwYqsWHXpOxHaq1Zo5UQ3m9Zm2p09LggGe57rqHoBn3c++xcoomzXKynAUuiBMDUCQvKMnXjUpA==
+"@ungap/promise-all-settled@1.1.2":
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44"
+ integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==
+
"@vue/compiler-core@3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.0.2.tgz#7790b7a1fcbba5ace4d81a70ce59096fa5c95734"
@@ -4023,6 +4070,29 @@
picomatch "^2.2.2"
ws "^7.3.1"
+"@web/dev-server-core@^0.2.17", "@web/dev-server-core@^0.2.18":
+ version "0.2.18"
+ resolved "https://registry.yarnpkg.com/@web/dev-server-core/-/dev-server-core-0.2.18.tgz#c96bd373a9aa10429eceb13387202fd5ded06f4d"
+ integrity sha512-cgZuB0eG6i1wYZydVEd+g5J8Qa0EyRcZcPk3977+E298kOZ/2XPpM1tattDBQHu9J1afl9gX9bW80OH0iuTMhQ==
+ dependencies:
+ "@types/koa" "^2.11.6"
+ "@types/ws" "^7.2.6"
+ "@web/parse5-utils" "^1.0.0"
+ chokidar "^3.4.3"
+ clone "^2.1.2"
+ es-module-lexer "^0.3.26"
+ get-stream "^6.0.0"
+ is-stream "^2.0.0"
+ isbinaryfile "^4.0.6"
+ koa "^2.13.0"
+ koa-etag "^3.0.0"
+ koa-static "^5.0.0"
+ lru-cache "^5.1.1"
+ mime-types "^2.1.27"
+ parse5 "^6.0.1"
+ picomatch "^2.2.2"
+ ws "^7.3.1"
+
"@web/dev-server-esbuild@^0.2.4":
version "0.2.6"
resolved "https://registry.yarnpkg.com/@web/dev-server-esbuild/-/dev-server-esbuild-0.2.6.tgz#b9a73a0bcbe6bb6ede4e3409b8e87ac781c92d5d"
@@ -4034,15 +4104,33 @@
parse5 "^6.0.1"
ua-parser-js "^0.7.21"
-"@web/dev-server-rollup@^0.2.10":
- version "0.2.10"
- resolved "https://registry.yarnpkg.com/@web/dev-server-rollup/-/dev-server-rollup-0.2.10.tgz#d0e902b997f9cc445ec80fc2db5e93e27ffbdcae"
- integrity sha512-jJHxHiircfj7SngFmwa8Rlc2n4o2MB+mLY8y4JyIXJG00ifgxNU6kSjbV/5shPAzzN6MD/H60EkE7n7kYfuAig==
+"@web/dev-server-esbuild@^0.2.8":
+ version "0.2.8"
+ resolved "https://registry.yarnpkg.com/@web/dev-server-esbuild/-/dev-server-esbuild-0.2.8.tgz#1ce7f83c670f36e0e98ee2ac88ba1fec53f8fae1"
+ integrity sha512-7HNIcoUVgVI7L/XGx/cSPGUqsIok41ucgxfIB+JYCAg+R0M9Jt8N7WQMfQFlHpoDLmqNpXuH+MYbY393u6hd6Q==
dependencies:
- "@web/dev-server-core" "^0.2.11"
+ "@mdn/browser-compat-data" "^2.0.7"
+ "@web/dev-server-core" "^0.2.2"
+ esbuild "^0.8.12"
+ parse5 "^6.0.1"
+ ua-parser-js "^0.7.22"
+
+"@web/dev-server-hmr@^0.1.6":
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/@web/dev-server-hmr/-/dev-server-hmr-0.1.6.tgz#5a8e8b2c02dcdf9392d001b11cb010acd4f92ce0"
+ integrity sha512-J3wQ46OeWMA5I48fyPNygbgbCLUIxvysAOanXMCVkuYen3ybWQ+T60CKqwYXGEOaRK3aWqxiyNFyHHvJzjeSIA==
+ dependencies:
+ "@web/dev-server-core" "^0.2.18"
+
+"@web/dev-server-rollup@^0.2.11":
+ version "0.2.11"
+ resolved "https://registry.yarnpkg.com/@web/dev-server-rollup/-/dev-server-rollup-0.2.11.tgz#daa65b9e2f0eaeaa9d7c6d39d0943d494eda8bb7"
+ integrity sha512-cqeXC93BElaKIV0Bel05M86Or5HQKem7FuBjfoMMlgp0irt6ZYCovYe7l2rG0Hw+/MCYaNs+uxjMK5UYLuWMBw==
+ dependencies:
+ "@web/dev-server-core" "^0.2.17"
chalk "^4.1.0"
parse5 "^6.0.1"
- rollup "^2.20.0"
+ rollup "^2.33.2"
whatwg-url "^8.1.0"
"@web/dev-server-rollup@^0.2.5", "@web/dev-server-rollup@^0.2.9":
@@ -4103,27 +4191,26 @@
open "^7.1.0"
portfinder "^1.0.27"
-"@web/dev-server@^0.0.19":
- version "0.0.19"
- resolved "https://registry.yarnpkg.com/@web/dev-server/-/dev-server-0.0.19.tgz#cf473079269fa2fdd6eee728f7a67a0648ec5e30"
- integrity sha512-e+mFEaXB/Fax8OcFT9+b/Z/4NqG/qr6edITgQdk3WxW5O+HYF0ramsZ6grSCwS9tly+/QfTlCnLJCMPOcibqYA==
+"@web/dev-server@^0.0.24":
+ version "0.0.24"
+ resolved "https://registry.yarnpkg.com/@web/dev-server/-/dev-server-0.0.24.tgz#891de7da5497a538d74b50b7c99a492a1f5661b3"
+ integrity sha512-vBvI6Q3h7oiTq2G9j6Xlj4LkCpTulSycEDvaFCrHwadDvKLoQLPFqm1wv4A8mvjU0zm6Mjs5X5eZndNJU0naIA==
dependencies:
"@babel/code-frame" "^7.10.4"
"@rollup/plugin-node-resolve" "^8.4.0"
"@types/command-line-args" "^5.0.0"
"@web/config-loader" "^0.1.1"
- "@web/dev-server-cli" "^0.0.3"
- "@web/dev-server-core" "^0.2.15"
- "@web/dev-server-rollup" "^0.2.10"
- camelcase "^6.0.0"
+ "@web/dev-server-core" "^0.2.18"
+ "@web/dev-server-rollup" "^0.2.11"
+ camelcase "^6.2.0"
chalk "^4.1.0"
command-line-args "^5.1.1"
- command-line-usage "^6.1.0"
+ command-line-usage "^6.1.1"
debounce "^1.2.0"
deepmerge "^4.2.2"
ip "^1.1.5"
- open "^7.1.0"
- portfinder "^1.0.27"
+ open "^7.3.0"
+ portfinder "^1.0.28"
"@web/parse5-utils@^1.0.0", "@web/parse5-utils@^1.1.2":
version "1.1.2"
@@ -4662,16 +4749,16 @@ ansi-colors@3.2.3:
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.3.tgz#57d35b8686e851e2cc04c403f1c00203976a1813"
integrity sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==
+ansi-colors@4.1.1, ansi-colors@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
+ integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==
+
ansi-colors@^3.0.0:
version "3.2.4"
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf"
integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==
-ansi-colors@^4.1.1:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
- integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==
-
ansi-escapes@^3.1.0, ansi-escapes@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b"
@@ -6310,6 +6397,11 @@ camelcase@^6.0.0:
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.1.0.tgz#27dc176173725fb0adf8a48b647f4d7871944d78"
integrity sha512-WCMml9ivU60+8rEJgELlFp1gxFcEGxwYleE3bziHEDeqsqAWGHdimB7beBFGjLzVNgPGyDsfgXLQEYMpmIFnVQ==
+camelcase@^6.2.0:
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809"
+ integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==
+
can-use-dom@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/can-use-dom/-/can-use-dom-0.1.0.tgz#22cc4a34a0abc43950f42c6411024a3f6366b45a"
@@ -6527,6 +6619,21 @@ cheerio@^1.0.0-rc.3:
lodash "^4.15.0"
parse5 "^3.0.1"
+chokidar@3.4.3, chokidar@^3.0.0, chokidar@^3.0.2, chokidar@^3.3.0, chokidar@^3.4.0, chokidar@^3.4.1, chokidar@^3.4.3:
+ version "3.4.3"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.3.tgz#c1df38231448e45ca4ac588e6c79573ba6a57d5b"
+ integrity sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ==
+ dependencies:
+ anymatch "~3.1.1"
+ braces "~3.0.2"
+ glob-parent "~5.1.0"
+ is-binary-path "~2.1.0"
+ is-glob "~4.0.1"
+ normalize-path "~3.0.0"
+ readdirp "~3.5.0"
+ optionalDependencies:
+ fsevents "~2.1.2"
+
chokidar@^2.0.4, chokidar@^2.1.8:
version "2.1.8"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
@@ -6546,21 +6653,6 @@ chokidar@^2.0.4, chokidar@^2.1.8:
optionalDependencies:
fsevents "^1.2.7"
-chokidar@^3.0.0, chokidar@^3.0.2, chokidar@^3.3.0, chokidar@^3.4.0, chokidar@^3.4.1:
- version "3.4.3"
- resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.3.tgz#c1df38231448e45ca4ac588e6c79573ba6a57d5b"
- integrity sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ==
- dependencies:
- anymatch "~3.1.1"
- braces "~3.0.2"
- glob-parent "~5.1.0"
- is-binary-path "~2.1.0"
- is-glob "~4.0.1"
- normalize-path "~3.0.0"
- readdirp "~3.5.0"
- optionalDependencies:
- fsevents "~2.1.2"
-
chownr@^1.1.1, chownr@^1.1.2:
version "1.1.4"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
@@ -6960,6 +7052,16 @@ command-line-usage@^6.1.0:
table-layout "^1.0.0"
typical "^5.2.0"
+command-line-usage@^6.1.1:
+ version "6.1.1"
+ resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-6.1.1.tgz#c908e28686108917758a49f45efb4f02f76bc03f"
+ integrity sha512-F59pEuAR9o1SF/bD0dQBDluhpT4jJQNWUHEuVBqpDmCUo6gPjCi+m9fCWnWZVR/oG6cMTUms4h+3NPl74wGXvA==
+ dependencies:
+ array-back "^4.0.1"
+ chalk "^2.4.2"
+ table-layout "^1.0.1"
+ typical "^5.2.0"
+
commander@^2.19.0, commander@^2.2.0, commander@^2.20.0, commander@^2.20.3, commander@^2.3.0, commander@^2.8.1:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
@@ -7670,7 +7772,7 @@ debug@3.2.6, debug@^3.0.0, debug@^3.1.0, debug@^3.1.1, debug@^3.2.5:
dependencies:
ms "^2.1.1"
-debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
+debug@4, debug@4.2.0, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
version "4.2.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1"
integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==
@@ -7704,6 +7806,11 @@ decamelize@^1.0.0, decamelize@^1.1.0, decamelize@^1.1.2, decamelize@^1.2.0:
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
+decamelize@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837"
+ integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==
+
decimal.js@^10.2.0:
version "10.2.1"
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.2.1.tgz#238ae7b0f0c793d3e3cea410108b35a2c01426a3"
@@ -8102,7 +8209,7 @@ diff@3.5.0, diff@^3.5.0:
resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==
-diff@^4.0.2:
+diff@4.0.2, diff@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
@@ -8761,6 +8868,11 @@ es-module-lexer@^0.3.13, es-module-lexer@^0.3.24, es-module-lexer@^0.3.25:
resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.3.25.tgz#24a1abcb9c5dc96923a8e42be033b801f788de06"
integrity sha512-H9VoFD5H9zEfiOX2LeTWDwMvAbLqcAyA2PIb40TOAvGpScOjit02oTGWgIh+M0rx2eJOKyJVM9wtpKFVgnyC3A==
+es-module-lexer@^0.3.26:
+ version "0.3.26"
+ resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.3.26.tgz#7b507044e97d5b03b01d4392c74ffeb9c177a83b"
+ integrity sha512-Va0Q/xqtrss45hWzP8CZJwzGSZJjDM5/MJRE3IXXnUCcVLElR9BRaE9F62BopysASyc4nM3uwhSW7FFB9nlWAA==
+
es-module-shims@^0.4.6:
version "0.4.7"
resolved "https://registry.yarnpkg.com/es-module-shims/-/es-module-shims-0.4.7.tgz#1419b65bbd38dfe91ab8ea5d7b4b454561e44641"
@@ -8790,6 +8902,11 @@ esbuild@^0.7.15:
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.7.22.tgz#9149b903f8128b7c45a754046c24199d76bbe08e"
integrity sha512-B43SYg8LGWYTCv9Gs0RnuLNwjzpuWOoCaZHTWEDEf5AfrnuDMerPVMdCEu7xOdhFvQ+UqfP2MGU9lxEy0JzccA==
+esbuild@^0.8.12:
+ version "0.8.12"
+ resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.8.12.tgz#bea417dbb228a971a5df255cb1b8acf5ea96ef52"
+ integrity sha512-SWCUodYFOkKcu2Jbum12NbD98MoQq2EWWvHfQtpR2C9RkPzRr1BE4cavYjQpQV01hBR19UAftXl0TVLjka/i3Q==
+
escalade@^3.1.0, escalade@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
@@ -9614,6 +9731,14 @@ find-up@3.0.0, find-up@^3.0.0:
dependencies:
locate-path "^3.0.0"
+find-up@5.0.0, find-up@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
+ integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
+ dependencies:
+ locate-path "^6.0.0"
+ path-exists "^4.0.0"
+
find-up@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f"
@@ -9637,14 +9762,6 @@ find-up@^4.0.0, find-up@^4.1.0:
locate-path "^5.0.0"
path-exists "^4.0.0"
-find-up@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
- integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
- dependencies:
- locate-path "^6.0.0"
- path-exists "^4.0.0"
-
find-versions@^3.0.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/find-versions/-/find-versions-3.2.0.tgz#10297f98030a786829681690545ef659ed1d254e"
@@ -9676,6 +9793,11 @@ flat@^4.1.0:
dependencies:
is-buffer "~2.0.3"
+flat@^5.0.2:
+ version "5.0.2"
+ resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241"
+ integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==
+
flatted@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138"
@@ -10164,7 +10286,7 @@ glob@7.1.3:
once "^1.3.0"
path-is-absolute "^1.0.0"
-glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
+glob@7.1.6, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
version "7.1.6"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
@@ -12222,7 +12344,7 @@ js-yaml@3.13.1:
argparse "^1.0.7"
esprima "^4.0.0"
-js-yaml@^3.11.0, js-yaml@^3.13.0, js-yaml@^3.13.1, js-yaml@^3.14.0, js-yaml@^3.6.1:
+js-yaml@3.14.0, js-yaml@^3.11.0, js-yaml@^3.13.0, js-yaml@^3.13.1, js-yaml@^3.14.0, js-yaml@^3.6.1:
version "3.14.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482"
integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==
@@ -12816,7 +12938,7 @@ listr2@^2.6.0:
rxjs "^6.6.2"
through "^2.3.8"
-lit-element@^2.0.1, lit-element@^2.2.1, lit-element@^2.3.1, lit-element@~2.4.0:
+lit-element@^2.0.1, lit-element@^2.2.1, lit-element@^2.3.1, lit-element@^2.4.0, lit-element@~2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-2.4.0.tgz#b22607a037a8fc08f5a80736dddf7f3f5d401452"
integrity sha512-pBGLglxyhq/Prk2H91nA0KByq/hx/wssJBQFiYqXhGDvEnY31PRGYf1RglVzyLeRysu0IHm2K0P196uLLWmwFg==
@@ -13137,6 +13259,13 @@ log-symbols@2.2.0:
dependencies:
chalk "^2.0.1"
+log-symbols@4.0.0, log-symbols@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920"
+ integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==
+ dependencies:
+ chalk "^4.0.0"
+
log-symbols@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18"
@@ -13151,13 +13280,6 @@ log-symbols@^3.0.0:
dependencies:
chalk "^2.4.2"
-log-symbols@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920"
- integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==
- dependencies:
- chalk "^4.0.0"
-
log-update@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1"
@@ -13895,6 +14017,37 @@ mocha@^6.2.2:
yargs-parser "13.1.2"
yargs-unparser "1.6.0"
+mocha@^8.2.1:
+ version "8.2.1"
+ resolved "https://registry.yarnpkg.com/mocha/-/mocha-8.2.1.tgz#f2fa68817ed0e53343d989df65ccd358bc3a4b39"
+ integrity sha512-cuLBVfyFfFqbNR0uUKbDGXKGk+UDFe6aR4os78XIrMQpZl/nv7JYHcvP5MFIAb374b2zFXsdgEGwmzMtP0Xg8w==
+ dependencies:
+ "@ungap/promise-all-settled" "1.1.2"
+ ansi-colors "4.1.1"
+ browser-stdout "1.3.1"
+ chokidar "3.4.3"
+ debug "4.2.0"
+ diff "4.0.2"
+ escape-string-regexp "4.0.0"
+ find-up "5.0.0"
+ glob "7.1.6"
+ growl "1.10.5"
+ he "1.2.0"
+ js-yaml "3.14.0"
+ log-symbols "4.0.0"
+ minimatch "3.0.4"
+ ms "2.1.2"
+ nanoid "3.1.12"
+ serialize-javascript "5.0.1"
+ strip-json-comments "3.1.1"
+ supports-color "7.2.0"
+ which "2.0.2"
+ wide-align "1.1.3"
+ workerpool "6.0.2"
+ yargs "13.3.2"
+ yargs-parser "13.1.2"
+ yargs-unparser "2.0.0"
+
mock-require@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/mock-require/-/mock-require-3.0.3.tgz#ccd544d9eae81dd576b3f219f69ec867318a1946"
@@ -14010,6 +14163,11 @@ nan@^2.12.1:
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19"
integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==
+nanoid@3.1.12:
+ version "3.1.12"
+ resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.12.tgz#6f7736c62e8d39421601e4a0c77623a97ea69654"
+ integrity sha512-1qstj9z5+x491jfiC4Nelk+f8XBad7LN20PmyWINJEMRSf3wcAjAWysw1qaA8z6NSKe2sjq1hRSDpBH5paCb6A==
+
nanomatch@^1.2.9:
version "1.2.13"
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
@@ -14628,7 +14786,7 @@ open@^6.3.0:
dependencies:
is-wsl "^1.1.0"
-open@^7.0.0, open@^7.0.3, open@^7.0.4, open@^7.1.0:
+open@^7.0.0, open@^7.0.3, open@^7.0.4, open@^7.1.0, open@^7.3.0:
version "7.3.0"
resolved "https://registry.yarnpkg.com/open/-/open-7.3.0.tgz#45461fdee46444f3645b6e14eb3ca94b82e1be69"
integrity sha512-mgLwQIx2F/ye9SmbrUkurZCnkoXyXyu9EbHtJZrICjVAJfyMArdHp3KkixGdZx1ZHFPNIwl0DDM1dFFqXbTLZw==
@@ -15377,7 +15535,7 @@ popper.js@^1.14.4, popper.js@^1.14.7, popper.js@^1.15.0:
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b"
integrity sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==
-portfinder@^1.0.13, portfinder@^1.0.21, portfinder@^1.0.26, portfinder@^1.0.27:
+portfinder@^1.0.13, portfinder@^1.0.21, portfinder@^1.0.26, portfinder@^1.0.27, portfinder@^1.0.28:
version "1.0.28"
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778"
integrity sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==
@@ -17361,6 +17519,13 @@ rollup@^2.33.1:
optionalDependencies:
fsevents "~2.1.2"
+rollup@^2.33.2:
+ version "2.33.3"
+ resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.33.3.tgz#ae72ce31f992b09a580072951bfea76e9df17342"
+ integrity sha512-RpayhPTe4Gu/uFGCmk7Gp5Z9Qic2VsqZ040G+KZZvsZYdcuWaJg678JeDJJvJeEQXminu24a2au+y92CUWVd+w==
+ optionalDependencies:
+ fsevents "~2.1.2"
+
rsvp@^4.8.4:
version "4.8.5"
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
@@ -17645,6 +17810,13 @@ sentence-case@^3.0.3:
tslib "^1.10.0"
upper-case-first "^2.0.1"
+serialize-javascript@5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-5.0.1.tgz#7886ec848049a462467a97d3d918ebb2aaf934f4"
+ integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==
+ dependencies:
+ randombytes "^2.1.0"
+
serialize-javascript@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-3.1.0.tgz#8bf3a9170712664ef2561b44b691eafe399214ea"
@@ -18613,7 +18785,7 @@ strip-json-comments@2.0.1, strip-json-comments@~2.0.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
-strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
+strip-json-comments@3.1.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
@@ -18654,6 +18826,13 @@ supports-color@6.0.0:
dependencies:
has-flag "^3.0.0"
+supports-color@7.2.0, supports-color@^7.0.0, supports-color@^7.1.0:
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
+ integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
+ dependencies:
+ has-flag "^4.0.0"
+
supports-color@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-0.2.0.tgz#d92de2694eb3f67323973d7ae3d8b55b4c22190a"
@@ -18678,13 +18857,6 @@ supports-color@^6.1.0:
dependencies:
has-flag "^3.0.0"
-supports-color@^7.0.0, supports-color@^7.1.0:
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
- integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
- dependencies:
- has-flag "^4.0.0"
-
supports-hyperlinks@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-1.0.1.tgz#71daedf36cc1060ac5100c351bb3da48c29c0ef7"
@@ -18729,7 +18901,7 @@ systemjs@^6.3.1, systemjs@^6.7.1:
resolved "https://registry.yarnpkg.com/systemjs/-/systemjs-6.7.1.tgz#3db5036f450180a0701e078fbb5b434a690026f0"
integrity sha512-Q78H/SYy9ErC8PH8r9vA/FcQ3X+Hf33dOpx2JKP/Ma6f2gHuSScPFuCKZH+6CIj7EsIJlzODxSG4mMIpjOh5oA==
-table-layout@^1.0.0:
+table-layout@^1.0.0, table-layout@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-1.0.1.tgz#8411181ee951278ad0638aea2f779a9ce42894f9"
integrity sha512-dEquqYNJiGwY7iPfZ3wbXDI944iqanTSchrACLL2nOB+1r+h1Nzu2eH+DuPPvWvm5Ry7iAPeFlgEtP5bIp5U7Q==
@@ -19384,7 +19556,7 @@ typical@^5.0.0, typical@^5.2.0:
resolved "https://registry.yarnpkg.com/typical/-/typical-5.2.0.tgz#4daaac4f2b5315460804f0acf6cb69c52bb93066"
integrity sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==
-ua-parser-js@^0.7.18, ua-parser-js@^0.7.21:
+ua-parser-js@^0.7.18, ua-parser-js@^0.7.21, ua-parser-js@^0.7.22:
version "0.7.22"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.22.tgz#960df60a5f911ea8f1c818f3747b99c6e177eae3"
integrity sha512-YUxzMjJ5T71w6a8WWVcMGM6YWOTX27rCoIQgLXiWaxqXSx9D7DNjiGWn1aJIRSQ5qr0xuhra77bSIh6voR/46Q==
@@ -20255,7 +20427,7 @@ which@1.3.1, which@^1.2.9, which@^1.3.1:
dependencies:
isexe "^2.0.0"
-which@^2.0.1, which@^2.0.2:
+which@2.0.2, which@^2.0.1, which@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
@@ -20519,6 +20691,11 @@ worker-rpc@^0.1.0:
dependencies:
microevent.ts "~0.1.1"
+workerpool@6.0.2:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.0.2.tgz#e241b43d8d033f1beb52c7851069456039d1d438"
+ integrity sha512-DSNyvOpFKrNusaaUwk+ej6cBj1bmhLcBfj80elGk+ZIo5JSkq+unB1dLKEOcNfJDZgjGICfhQ0Q5TbP0PvF4+Q==
+
wrap-ansi@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-4.0.0.tgz#b3570d7c70156159a2d42be5cc942e957f7b1131"
@@ -20746,6 +20923,16 @@ yargs-unparser@1.6.0:
lodash "^4.17.15"
yargs "^13.3.0"
+yargs-unparser@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb"
+ integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==
+ dependencies:
+ camelcase "^6.0.0"
+ decamelize "^4.0.0"
+ flat "^5.0.2"
+ is-plain-obj "^2.1.0"
+
yargs@13.3.0:
version "13.3.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.0.tgz#4c657a55e07e5f2cf947f8a366567c04a0dedc83"