Add asyncAppend and asyncReplace directives (#118)

* Add asyncAppend and asyncReplace directives
This commit is contained in:
Justin Fagnani 2017-12-05 14:06:00 -08:00 коммит произвёл GitHub
Родитель cac223cdd9
Коммит 78f4d51146
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
14 изменённых файлов: 608 добавлений и 31 удалений

Просмотреть файл

@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
* Performance improvements for template setup
* Internal code cleanup
* Support synchronous thenables
* Added the `asyncAppend` and `asyncReplace` directives to handle async iterable values in expressions.
## [0.7.0] - 2017-10-06

Просмотреть файл

@ -167,6 +167,16 @@ const render = () => html`
`;
```
### Promises
Promises are rendered when they resolve, leaving the previous value in place until they do. Races are handled, so that if an unresolved Promise is overwritten, it won't update the template when it finally resolves.
```javascript
const render = () => html`
The response is ${fetch('sample.txt').then((r) => r.text())}.
`;
```
### Directives
Directives are functions that can extend lit-html by directly interacting with the Part API.
@ -240,14 +250,51 @@ const render = () => html`
`;
```
### Promises
#### `asyncAppend(asyncIterable)` and `asyncReplace(asyncIterable)`
Promises are rendered when they resolve, leaving the previous value in place until they do. Races are handled, so that if an unresolved Promise is overwritten, it won't update the template when it finally resolves.
JavaScript asynchronous iterators provide a generic interface for asynchronous sequential access to data. Much like an iterator, a consumer requests the next data item with a a call to `next()`, but with asynchronous iterators `next()` returns a `Promise`, allowing the iterator to provide the item when it's ready.
lit-html offers two directives to consume asynchronous iterators:
* `asyncAppend` renders the values of an [async iterable](https://github.com/tc39/proposal-async-iteration),
appending each new value after the previous.
* `asyncReplace` renders the values of an [async iterable](https://github.com/tc39/proposal-async-iteration),
replacing the previous value with the new value.
Example:
```javascript
const render = () => html`
The response is ${fetch('sample.txt').then((r) => r.text())}.
`;
const wait = (t) => new Promise((resolve) => setTimeout(resolve, t));
/**
* Returns an async iterable that yields increasing integers.
*/
async function* countUp() {
let i = 0;
while (true) {
yield i++;
await wait(1000);
}
}
render(html`
Count: <span>${asyncReplace(countUp())}</span>.
`, document.body);
```
In the near future, `ReadableStream`s will be async iterables, enabling streaming `fetch()` directly into a template:
```javascript
// Endpoint that returns a billion digits of PI, streamed.
const url =
'https://cors-anywhere.herokuapp.com/http://stuff.mit.edu/afs/sipb/contrib/pi/pi-billion.txt';
const streamingResponse = (async () => {
const response = await fetch(url);
return response.body.getReader();
})();
render(html`π is: ${asyncAppend(streamingResponse)}`, document.body);
```
### Composability

93
src/lib/async-append.ts Normal file
Просмотреть файл

@ -0,0 +1,93 @@
/**
* @license
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
import {directive, NodePart} from '../lit-html.js';
/**
* A directive that renders the items of an async iterable[1], appending new
* values after previous values, similar to the built-in support for iterables.
*
* Async iterables are objects with a [Symbol.asyncIterator] method, which
* returns an iterator who's `next()` method returns a Promise. When a new
* value is available, the Promise resolves and the value is appended to the
* Part controlled by the directive. If another value other than this
* directive has been set on the Part, the iterable will no longer be listened
* to and new values won't be written to the Part.
*
* [1]: https://github.com/tc39/proposal-async-iteration
*
* @param value An async iterable
* @param mapper An optional function that maps from (value, index) to another
* value. Useful for generating templates for each item in the iterable.
*/
export const asyncAppend = <T>(
value: AsyncIterable<T>, mapper?: (v: T, index?: number) => any) =>
directive(async (part: NodePart) => {
// If we've already set up this particular iterable, we don't need
// to do anything.
if (value === part._previousValue) {
return;
}
part._previousValue = value;
// We keep track of item Parts across iterations, so that we can
// share marker nodes between consecutive Parts.
let itemPart;
let i = 0;
for await (let v of value) {
// When we get the first value, clear the part. This let's the previous
// value display until we can replace it.
if (i === 0) {
part.clear();
}
// Check to make sure that value is the still the current value of
// the part, and if not bail because a new value owns this part
if (part._previousValue !== value) {
break;
}
// As a convenience, because functional-programming-style
// transforms of iterables and async iterables requires a library,
// we accept a mapper function. This is especially convenient for
// rendering a template for each item.
if (mapper !== undefined) {
v = mapper(v, i);
}
// Like with sync iterables, each item induces a Part, so we need
// to keep track of start and end nodes for the Part.
// Note: Because these Parts are not updatable like with a sync
// iterable (if we render a new value, we always clear), it may
// be possible to optimize away the Parts and just re-use the
// Part.setValue() logic.
let itemStartNode = part.startNode;
// Check to see if we have a previous item and Part
if (itemPart !== undefined) {
// Create a new node to separate the previous and next Parts
itemStartNode = document.createTextNode('');
// itemPart is currently the Part for the previous item. Set
// it's endNode to the node we'll use for the next Part's
// startNode.
itemPart.endNode = itemStartNode;
part.endNode.parentNode!.insertBefore(itemStartNode, part.endNode);
}
itemPart = new NodePart(part.instance, itemStartNode, part.endNode);
itemPart.setValue(v);
i++;
}
});

77
src/lib/async-replace.ts Normal file
Просмотреть файл

@ -0,0 +1,77 @@
/**
* @license
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
import {directive, NodePart} from '../lit-html.js';
/**
* A directive that renders the items of an async iterable[1], replacing
* previous values with new values, so that only one value is ever rendered
* at a time.
*
* Async iterables are objects with a [Symbol.asyncIterator] method, which
* returns an iterator who's `next()` method returns a Promise. When a new
* value is available, the Promise resolves and the value is rendered to the
* Part controlled by the directive. If another value other than this
* directive has been set on the Part, the iterable will no longer be listened
* to and new values won't be written to the Part.
*
* [1]: https://github.com/tc39/proposal-async-iteration
*
* @param value An async iterable
* @param mapper An optional function that maps from (value, index) to another
* value. Useful for generating templates for each item in the iterable.
*/
export const asyncReplace =
<T>(value: AsyncIterable<T>, mapper?: (v: T, index?: number) => any) =>
directive(async (part: NodePart) => {
// If we've already set up this particular iterable, we don't need
// to do anything.
if (value === part._previousValue) {
return;
}
// We nest a new part to keep track of previous item values separately
// of the iterable as a value itself.
const itemPart =
new NodePart(part.instance, part.startNode, part.endNode);
part._previousValue = itemPart;
let i = 0;
for await (let v of value) {
// When we get the first value, clear the part. This let's the previous
// value display until we can replace it.
if (i === 0) {
part.clear();
}
// Check to make sure that value is the still the current value of
// the part, and if not bail because a new value owns this part
if (part._previousValue !== itemPart) {
break;
}
// As a convenience, because functional-programming-style
// transforms of iterables and async iterables requires a library,
// we accept a mapper function. This is especially convenient for
// rendering a template for each item.
if (mapper !== undefined) {
v = mapper(v, i);
}
itemPart.setValue(v);
i++;
}
});

Просмотреть файл

@ -21,8 +21,8 @@ import {directive, NodePart} from '../lit-html.js';
* sanitized or escaped, as it may lead to cross-site-scripting
* vulnerabilities.
*/
export const unsafeHTML = (value: any) => directive((_part: NodePart) => {
export const unsafeHTML = (value: any) => directive((part: NodePart) => {
const tmp = document.createElement('template');
tmp.innerHTML = value;
return document.importNode(tmp.content, true);
part.setValue(document.importNode(tmp.content, true));
});

Просмотреть файл

@ -20,5 +20,5 @@ import {directive, NodePart} from '../lit-html.js';
export const until = (promise: Promise<any>, defaultContent: any) =>
directive((part: NodePart) => {
part.setValue(defaultContent);
return promise;
part.setValue(promise);
});

Просмотреть файл

@ -354,29 +354,38 @@ export class Template {
}
}
/**
* Returns a value ready to be inserted into a Part from a user-provided value.
*
* If the user value is a directive, this invokes the directive with the given
* part. If the value is null, it's converted to undefined to work better
* with certain DOM APIs, like textContent.
*/
export const getValue = (part: Part, value: any) => {
// `null` as the value of a Text node will render the string 'null'
// so we convert it to undefined
if (value != null && value.__litDirective === true) {
if (isDirective(value)) {
value = value(part);
return directiveValue;
}
return value === null ? undefined : value;
};
export type DirectiveFn = (part: Part) => any;
export type DirectiveFn<P extends Part = Part> = (part: P) => any;
export const directive = <F extends DirectiveFn>(f: F): F => {
export const directive = <P extends Part = Part, F = DirectiveFn<P>>(f: F): F => {
(f as any).__litDirective = true;
return f;
};
const isDirective = (o: any) =>
typeof o === 'function' && o.__litDirective === true;
const directiveValue = {};
export interface Part {
instance: TemplateInstance;
size?: number;
// constructor(instance: TemplateInstance) {
// this.instance = instance;
// }
}
export interface SinglePart extends Part { setValue(value: any): void; }
@ -410,7 +419,7 @@ export class AttributePart implements MultiPart {
for (let i = 0; i < l; i++) {
text += strings[i];
const v = getValue(this, values[startIndex + i]);
if (v &&
if (v && v !== directiveValue &&
(Array.isArray(v) || typeof v !== 'string' && v[Symbol.iterator])) {
for (const t of v) {
// TODO: we need to recursively call getValue into iterables...
@ -433,7 +442,7 @@ export class NodePart implements SinglePart {
instance: TemplateInstance;
startNode: Node;
endNode: Node;
private _previousValue: any;
_previousValue: any;
constructor(instance: TemplateInstance, startNode: Node, endNode: Node) {
this.instance = instance;
@ -445,7 +454,9 @@ export class NodePart implements SinglePart {
setValue(value: any): void {
value = getValue(this, value);
if (value === directiveValue) {
return;
}
if (value === null ||
!(typeof value === 'object' || typeof value === 'function')) {
// Handle primitive values
@ -483,6 +494,7 @@ export class NodePart implements SinglePart {
private _setText(value: string): void {
const node = this.startNode.nextSibling!;
value = value === undefined ? '' : value;
if (node === this.endNode.previousSibling &&
node.nodeType === Node.TEXT_NODE) {
// If we only have a single text node between the markers, we can just
@ -491,7 +503,7 @@ export class NodePart implements SinglePart {
// primitive?
node.textContent = value;
} else {
this._setNode(document.createTextNode(value === undefined ? '' : value));
this._setNode(document.createTextNode(value));
}
this._previousValue = value;
}

Просмотреть файл

@ -0,0 +1,114 @@
/**
* @license
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
/// <reference path="../../../node_modules/@types/mocha/index.d.ts" />
/// <reference path="../../../node_modules/@types/chai/index.d.ts" />
import {asyncAppend} from '../../lib/async-append.js';
import {html, render} from '../../lit-html.js';
import {TestAsyncIterable} from './test-async-iterable.js';
const assert = chai.assert;
// Set Symbol.asyncIterator on browsers without it
if (typeof Symbol !== undefined && Symbol.asyncIterator === undefined) {
Object.defineProperty(Symbol, 'Symbol.asyncIterator', {value: Symbol()});
}
suite('asyncAppend', () => {
let container: HTMLDivElement;
let iterable: TestAsyncIterable<string>;
setup(() => {
container = document.createElement('div');
iterable = new TestAsyncIterable<string>();
});
test('appends content as the async iterable yields new values', async () => {
render(html`<div>${asyncAppend(iterable)}</div>`, container);
assert.equal(container.innerHTML, '<div><!----><!----></div>');
await iterable.push('foo');
assert.equal(container.innerHTML, '<div><!---->foo<!----></div>');
await iterable.push('bar');
assert.equal(container.innerHTML, '<div><!---->foobar<!----></div>');
});
test('appends nothing with a value is undefined', async () => {
render(html`<div>${asyncAppend(iterable)}</div>`, container);
assert.equal(container.innerHTML, '<div><!----><!----></div>');
await iterable.push('foo');
assert.equal(container.innerHTML, '<div><!---->foo<!----></div>');
await iterable.push(undefined);
assert.equal(container.innerHTML, '<div><!---->foo<!----></div>');
});
test('uses a mapper function', async () => {
render(
html`<div>${asyncAppend(iterable, (v, i) => html`${i}: ${v} `)}</div>`,
container);
assert.equal(container.innerHTML, '<div><!----><!----></div>');
await iterable.push('foo');
assert.equal(container.innerHTML, '<div><!----><!---->0: foo <!----></div>');
await iterable.push('bar');
assert.equal(container.innerHTML, '<div><!----><!---->0: foo <!---->1: bar <!----></div>');
});
test('renders new iterable over a pending iterable', async () => {
const t = (iterable: any) => html`<div>${asyncAppend(iterable)}</div>`;
render(t(iterable), container);
assert.equal(container.innerHTML, '<div><!----><!----></div>');
await iterable.push('foo');
assert.equal(container.innerHTML, '<div><!---->foo<!----></div>');
const iterable2 = new TestAsyncIterable<string>();
render(t(iterable2), container);
// The last value is preserved until we receive the first
// value from the new iterable
assert.equal(container.innerHTML, '<div><!---->foo<!----></div>');
await iterable2.push('hello');
assert.equal(container.innerHTML, '<div><!---->hello<!----></div>');
await iterable.push('bar');
assert.equal(container.innerHTML, '<div><!---->hello<!----></div>');
});
test('renders new value over a pending iterable', async () => {
const t = (v: any) => html`<div>${v}</div>`;
// This is a little bit of an odd usage of directives as values, but it
// is possible, and we check here that asyncAppend plays nice in this case
render(t(asyncAppend(iterable)), container);
assert.equal(container.innerHTML, '<div><!----><!----></div>');
await iterable.push('foo');
assert.equal(container.innerHTML, '<div><!---->foo<!----></div>');
render(t('hello'), container);
assert.equal(container.innerHTML, '<div><!---->hello<!----></div>');
await iterable.push('bar');
assert.equal(container.innerHTML, '<div><!---->hello<!----></div>');
});
});

Просмотреть файл

@ -0,0 +1,114 @@
/**
* @license
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
/// <reference path="../../../node_modules/@types/mocha/index.d.ts" />
/// <reference path="../../../node_modules/@types/chai/index.d.ts" />
import {asyncReplace} from '../../lib/async-replace.js';
import {html, render} from '../../lit-html.js';
import {TestAsyncIterable} from './test-async-iterable.js';
const assert = chai.assert;
// Set Symbol.asyncIterator on browsers without it
if (typeof Symbol !== undefined && Symbol.asyncIterator === undefined) {
Object.defineProperty(Symbol, 'Symbol.asyncIterator', {value: Symbol()});
}
suite('asyncReplace', () => {
let container: HTMLDivElement;
let iterable: TestAsyncIterable<string>;
setup(() => {
container = document.createElement('div');
iterable = new TestAsyncIterable<string>();
});
test('replaces content as the async iterable yields new values', async () => {
render(html`<div>${asyncReplace(iterable)}</div>`, container);
assert.equal(container.innerHTML, '<div><!----><!----></div>');
await iterable.push('foo');
assert.equal(container.innerHTML, '<div><!---->foo<!----></div>');
await iterable.push('bar');
assert.equal(container.innerHTML, '<div><!---->bar<!----></div>');
});
test('clears the Part when a value is undefined', async () => {
render(html`<div>${asyncReplace(iterable)}</div>`, container);
assert.equal(container.innerHTML, '<div><!----><!----></div>');
await iterable.push('foo');
assert.equal(container.innerHTML, '<div><!---->foo<!----></div>');
await iterable.push(undefined);
assert.equal(container.innerHTML, '<div><!----><!----></div>');
});
test('uses the mapper function', async () => {
render(
html`<div>${asyncReplace(iterable, (v, i) => html`${i}: ${v} `)}</div>`,
container);
assert.equal(container.innerHTML, '<div><!----><!----></div>');
await iterable.push('foo');
assert.equal(container.innerHTML, '<div><!----><!---->0: foo <!----></div>');
await iterable.push('bar');
assert.equal(container.innerHTML, '<div><!----><!---->1: bar <!----></div>');
});
test('renders new iterable over a pending iterable', async () => {
const t = (iterable: any) => html`<div>${asyncReplace(iterable)}</div>`;
render(t(iterable), container);
assert.equal(container.innerHTML, '<div><!----><!----></div>');
await iterable.push('foo');
assert.equal(container.innerHTML, '<div><!---->foo<!----></div>');
const iterable2 = new TestAsyncIterable<string>();
render(t(iterable2), container);
// The last value is preserved until we receive the first
// value from the new iterable
assert.equal(container.innerHTML, '<div><!---->foo<!----></div>');
await iterable2.push('hello');
assert.equal(container.innerHTML, '<div><!---->hello<!----></div>');
await iterable.push('bar');
assert.equal(container.innerHTML, '<div><!---->hello<!----></div>');
});
test('renders new value over a pending iterable', async () => {
const t = (v: any) => html`<div>${v}</div>`;
// This is a little bit of an odd usage of directives as values, but it
// is possible, and we check here that asyncReplace plays nice in this case
render(t(asyncReplace(iterable)), container);
assert.equal(container.innerHTML, '<div><!----><!----></div>');
await iterable.push('foo');
assert.equal(container.innerHTML, '<div><!---->foo<!----></div>');
render(t('hello'), container);
assert.equal(container.innerHTML, '<div><!---->hello<!----></div>');
await iterable.push('bar');
assert.equal(container.innerHTML, '<div><!---->hello<!----></div>');
});
});

30
src/test/lib/deferred.ts Normal file
Просмотреть файл

@ -0,0 +1,30 @@
/**
* @license
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
/**
* A helper for creating Promises that can be resolved or rejected after
* initial creation.
*/
export class Deferred<T> {
readonly promise: Promise<T>;
readonly resolve: (value: T) => void;
readonly reject: (error: Error) => void;
constructor() {
this.promise = new Promise<T>((res, rej) => {
this.resolve! = res;
this.reject! = rej;
});
}
}

Просмотреть файл

@ -0,0 +1,60 @@
/**
* @license
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
// Set Symbol.asyncIterator on browsers without it
if (typeof Symbol !== undefined && Symbol.asyncIterator === undefined) {
Object.defineProperty(Symbol, 'asyncIterator', {value: Symbol()});
}
/**
* An async iterable that can have values pushed into it for testing code
* that consumes async iterables. This iterable can only be safely consumed
* by one listener.
*/
export class TestAsyncIterable<T> implements AsyncIterable<T> {
/**
* A Promise that resolves with the next value to be returned by the
* async iterable returned from iterable()
*/
private _nextValue: Promise<T> =
new Promise((resolve, _) => this._resolveNextValue = resolve);
private _resolveNextValue: (value: T) => void;
async * [Symbol.asyncIterator]() {
while (true) {
yield await this._nextValue;
}
}
/**
* Pushes a new value and returns a Promise that resolves when the value
* has been emitted by the iterator. push() must not be called before
* a previous call has completed, so always await a push() call.
*/
async push(value: any): Promise<void> {
const currentValue = this._nextValue;
const currentResolveValue = this._resolveNextValue;
this._nextValue =
new Promise((resolve, _) => this._resolveNextValue = resolve);
// Resolves the previous value of _nextValue (now currentValue in this
// scope), making `yield await this._nextValue` go.
currentResolveValue(value);
// Waits for the value to be emitted
await currentValue;
// Need to wait for one more microtask for value to be rendered, but only
// when devtools is closed. Waiting for rAF might be more reliable, but
// this waits the minimum that seems reliable now.
await Promise.resolve();
}
}

Просмотреть файл

@ -17,26 +17,53 @@
import {until} from '../../lib/until.js';
import {html, render} from '../../lit-html.js';
import {Deferred} from './deferred.js';
const assert = chai.assert;
suite('until', () => {
let container: HTMLDivElement;
let deferred: Deferred<string>;
setup(() => {
container = document.createElement('div');
deferred = new Deferred<string>();
});
test('displays defaultContent immediately', () => {
const container = document.createElement('div');
let resolve: (v: any) => void;
const promise = new Promise((res, _) => {
resolve = res;
});
render(
html`<div>${until(promise, html`<span>loading...</span>`)}</div>`,
html
`<div>${until(deferred.promise, html`<span>loading...</span>`)}</div>`,
container);
assert.equal(container.innerHTML, '<div><!----><span>loading...</span><!----></div>');
resolve!('foo');
return promise.then(() => new Promise((r) => setTimeout(() => r())))
.then(() => {
assert.equal(container.innerHTML, '<div><!---->foo<!----></div>');
});
deferred.resolve('foo');
return deferred.promise
.then(() => new Promise((r) => setTimeout(() => r())))
.then(() => {
assert.equal(container.innerHTML, '<div><!---->foo<!----></div>');
});
});
test('renders new Promise over existing Promise', () => {
const t = (v: any) =>
html`<div>${until(v, html`<span>loading...</span>`)}</div>`;
render(t(deferred.promise), container);
assert.equal(container.innerHTML, '<div><!----><span>loading...</span><!----></div>');
const deferred2 = new Deferred<string>();
render(t(deferred2.promise), container);
assert.equal(container.innerHTML, '<div><!----><span>loading...</span><!----></div>');
deferred2.resolve('bar');
return deferred2.promise.then(() => {
assert.equal(container.innerHTML, '<div><!---->bar<!----></div>');
deferred.resolve('foo');
return deferred.promise.then(() => {
assert.equal(container.innerHTML, '<div><!---->bar<!----></div>');
});
});
});
});

Просмотреть файл

@ -11,5 +11,7 @@
<script type="module" src="./lib/until_test.js"></script>
<script type="module" src="./lib/lit-extended_test.js"></script>
<script type="module" src="./lib/unsafe-html_test.js"></script>
<script type="module" src="./lib/async-append_test.js"></script>
<script type="module" src="./lib/async-replace_test.js"></script>
</body>
</html>

Просмотреть файл

@ -2,7 +2,7 @@
"compilerOptions": {
"target": "es2017",
"module": "es2015",
"lib": ["es2017", "dom"],
"lib": ["es2017", "esnext.asynciterable", "dom"],
"declaration": true,
"sourceMap": true,
"inlineSources": true,