Custom components support

This commit is contained in:
4eb0da
2023-10-17 15:23:59 +03:00
parent 6b318459a8
commit 439ed43aec
39 changed files with 1660 additions and 50 deletions
+23
View File
@@ -11835,6 +11835,25 @@
"client/ios/codegen_DivKit.sh":"divkit/public/client/ios/codegen_DivKit.sh",
"client/ios/codegen_Tests.sh":"divkit/public/client/ios/codegen_Tests.sh",
"client/web/divkit-examples/README.md":"divkit/public/client/web/divkit-examples/README.md",
"client/web/divkit-examples/custom-container/.npmrc":"divkit/public/client/web/divkit-examples/custom-container/.npmrc",
"client/web/divkit-examples/custom-container/README.md":"divkit/public/client/web/divkit-examples/custom-container/README.md",
"client/web/divkit-examples/custom-container/customElement.js":"divkit/public/client/web/divkit-examples/custom-container/customElement.js",
"client/web/divkit-examples/custom-container/index.html":"divkit/public/client/web/divkit-examples/custom-container/index.html",
"client/web/divkit-examples/custom-container/package-lock.json":"divkit/public/client/web/divkit-examples/custom-container/package-lock.json",
"client/web/divkit-examples/custom-container/package.json":"divkit/public/client/web/divkit-examples/custom-container/package.json",
"client/web/divkit-examples/custom-simple/.npmrc":"divkit/public/client/web/divkit-examples/custom-simple/.npmrc",
"client/web/divkit-examples/custom-simple/README.md":"divkit/public/client/web/divkit-examples/custom-simple/README.md",
"client/web/divkit-examples/custom-simple/customElement.js":"divkit/public/client/web/divkit-examples/custom-simple/customElement.js",
"client/web/divkit-examples/custom-simple/index.html":"divkit/public/client/web/divkit-examples/custom-simple/index.html",
"client/web/divkit-examples/custom-simple/package-lock.json":"divkit/public/client/web/divkit-examples/custom-simple/package-lock.json",
"client/web/divkit-examples/custom-simple/package.json":"divkit/public/client/web/divkit-examples/custom-simple/package.json",
"client/web/divkit-examples/custom-ssr/.npmrc":"divkit/public/client/web/divkit-examples/custom-ssr/.npmrc",
"client/web/divkit-examples/custom-ssr/README.md":"divkit/public/client/web/divkit-examples/custom-ssr/README.md",
"client/web/divkit-examples/custom-ssr/customElement.js":"divkit/public/client/web/divkit-examples/custom-ssr/customElement.js",
"client/web/divkit-examples/custom-ssr/package-lock.json":"divkit/public/client/web/divkit-examples/custom-ssr/package-lock.json",
"client/web/divkit-examples/custom-ssr/package.json":"divkit/public/client/web/divkit-examples/custom-ssr/package.json",
"client/web/divkit-examples/custom-ssr/server.js":"divkit/public/client/web/divkit-examples/custom-ssr/server.js",
"client/web/divkit-examples/custom-ssr/template.html":"divkit/public/client/web/divkit-examples/custom-ssr/template.html",
"client/web/divkit-examples/divkit-react/.npmrc":"divkit/public/client/web/divkit-examples/divkit-react/.npmrc",
"client/web/divkit-examples/divkit-react/README.md":"divkit/public/client/web/divkit-examples/divkit-react/README.md",
"client/web/divkit-examples/divkit-react/index.html":"divkit/public/client/web/divkit-examples/divkit-react/index.html",
@@ -11925,6 +11944,7 @@
"client/web/divkit/src/components/container/Container.svelte":"divkit/public/client/web/divkit/src/components/container/Container.svelte",
"client/web/divkit/src/components/container/ContainerSeparator.module.css":"divkit/public/client/web/divkit/src/components/container/ContainerSeparator.module.css",
"client/web/divkit/src/components/container/ContainerSeparators.svelte":"divkit/public/client/web/divkit/src/components/container/ContainerSeparators.svelte",
"client/web/divkit/src/components/custom/Custom.svelte":"divkit/public/client/web/divkit/src/components/custom/Custom.svelte",
"client/web/divkit/src/components/gallery/Gallery.module.css":"divkit/public/client/web/divkit/src/components/gallery/Gallery.module.css",
"client/web/divkit/src/components/gallery/Gallery.svelte":"divkit/public/client/web/divkit/src/components/gallery/Gallery.svelte",
"client/web/divkit/src/components/grid/Grid.module.css":"divkit/public/client/web/divkit/src/components/grid/Grid.module.css",
@@ -11969,6 +11989,7 @@
"client/web/divkit/src/context/state.ts":"divkit/public/client/web/divkit/src/context/state.ts",
"client/web/divkit/src/dev.html":"divkit/public/client/web/divkit/src/dev.html",
"client/web/divkit/src/dev.ts":"divkit/public/client/web/divkit/src/dev.ts",
"client/web/divkit/src/devCustomComponents.ts":"divkit/public/client/web/divkit/src/devCustomComponents.ts",
"client/web/divkit/src/expressions/ast.d.ts":"divkit/public/client/web/divkit/src/expressions/ast.d.ts",
"client/web/divkit/src/expressions/bigint.ts":"divkit/public/client/web/divkit/src/expressions/bigint.ts",
"client/web/divkit/src/expressions/const.ts":"divkit/public/client/web/divkit/src/expressions/const.ts",
@@ -12003,6 +12024,7 @@
"client/web/divkit/src/types/base.d.ts":"divkit/public/client/web/divkit/src/types/base.d.ts",
"client/web/divkit/src/types/border.d.ts":"divkit/public/client/web/divkit/src/types/border.d.ts",
"client/web/divkit/src/types/container.d.ts":"divkit/public/client/web/divkit/src/types/container.d.ts",
"client/web/divkit/src/types/custom.d.ts":"divkit/public/client/web/divkit/src/types/custom.d.ts",
"client/web/divkit/src/types/drawable.d.ts":"divkit/public/client/web/divkit/src/types/drawable.d.ts",
"client/web/divkit/src/types/edgeInserts.d.ts":"divkit/public/client/web/divkit/src/types/edgeInserts.d.ts",
"client/web/divkit/src/types/filter.d.ts":"divkit/public/client/web/divkit/src/types/filter.d.ts",
@@ -14164,6 +14186,7 @@
"client/web/divkit/typings/client-hydratable.d.ts":"divkit/public/client/web/divkit/typings/client-hydratable.d.ts",
"client/web/divkit/typings/client.d.ts":"divkit/public/client/web/divkit/typings/client.d.ts",
"client/web/divkit/typings/common.d.ts":"divkit/public/client/web/divkit/typings/common.d.ts",
"client/web/divkit/typings/custom.d.ts":"divkit/public/client/web/divkit/typings/custom.d.ts",
"client/web/divkit/typings/extensions.d.ts":"divkit/public/client/web/divkit/typings/extensions.d.ts",
"client/web/divkit/typings/server.d.ts":"divkit/public/client/web/divkit/typings/server.d.ts",
"client/web/divkit/typings/variables.d.ts":"divkit/public/client/web/divkit/typings/variables.d.ts",
@@ -0,0 +1 @@
save-exact=true
@@ -0,0 +1,42 @@
## Using DivKit components inside your Custom Component
[General notes about custom components](../../divkit/README.md#customComponents)
[Previous example](../custom-simple)
Install the packages first:
```
npm ci
```
And then open `index.html` file with any http server to see the result (the browser may block loading of external modules into the file:// pages).
### How can support a DivKitComponents inside my Custom Component?
First of all, lets look at the markup:
```js
// DivJson
"type": "custom",
"custom_type": "custom_container",
"items": [
...
]
```
These `items` are the DivKit markup and will be rendered by DivKit itself.
Now we will provide a place (`slot`) for these `items`:
```js
// JS code
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = '<slot></slot>';
```
This `<slot>` can be located at any depth of your element, not just a direct child of ShadowRoot.
### How can I render Custom DivKit components on the Server Side?
See [next example](../custom-ssr).
@@ -0,0 +1,9 @@
class CustomContainer extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = '<slot></slot>';
}
}
customElements.define('custom-container', CustomContainer);
@@ -0,0 +1,86 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<link rel="stylesheet" type="text/css" href="node_modules/@divkitframework/divkit/dist/client.css">
</head>
<body>
<div id="root"></div>
<script type="module">
import { render } from './node_modules/@divkitframework/divkit/dist/esm/client.mjs';
import './customElement.js';
render({
id: 'test',
target: document.querySelector('#root'),
customComponents: new Map([
['custom_container', {
element: 'custom-container'
}]
]),
json: {
"templates": {
"news_item_template": {
"type": "text",
"font_size": 16,
"line_height": 20,
"text_color": "#333",
"$text": "news_item_text",
"paddings": {
"left": 12,
"right": 12,
"top": 8
}
}
},
"card": {
"log_id": "custom_card",
"states": [
{
"state_id": 0,
"div": {
"type": "container",
"margins": {
"left": 16,
"top": 16,
"right": 16,
"bottom": 16
},
"items": [
{
"type": "custom",
"custom_type": "custom_container",
"items": [
{
"type": "news_item_template",
"news_item_text": "This is div-text item 1"
},
{
"type": "news_item_template",
"news_item_text": "This is div-text item 2"
},
{
"type": "news_item_template",
"news_item_text": "This is div-text item 3"
}
]
}
]
}
}
]
}
}
});
</script>
</body>
</html>
@@ -0,0 +1,22 @@
{
"name": "custom-simple",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "custom-simple",
"version": "1.0.0",
"license": "UNLICENSED",
"devDependencies": {
"@divkitframework/divkit": "28.6.0-canary-f1eee8005aa682b1f02f9f9caec744f889bc1fdb"
}
},
"node_modules/@divkitframework/divkit": {
"version": "28.6.0-canary-f1eee8005aa682b1f02f9f9caec744f889bc1fdb",
"resolved": "https://registry.npmjs.org/@divkitframework/divkit/-/divkit-28.6.0-canary-f1eee8005aa682b1f02f9f9caec744f889bc1fdb.tgz",
"integrity": "sha512-EEqRHNF77NYZhVsYGxQLEku9q5tYniAjFJ0UzsxM2TMwt6lLmJ96hOZbMro6wM7Fh6XuwC3Kjkc6PQdPTUuIXw==",
"dev": true
}
}
}
@@ -0,0 +1,14 @@
{
"name": "custom-simple",
"private": true,
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {},
"keywords": [],
"author": "",
"license": "UNLICENSED",
"devDependencies": {
"@divkitframework/divkit": "28.6.0-canary-f1eee8005aa682b1f02f9f9caec744f889bc1fdb"
}
}
@@ -0,0 +1 @@
save-exact=true
@@ -0,0 +1,94 @@
## Using custom components
[General notes about custom components](../../divkit/README.md#customComponents)
Install the packages first:
```
npm ci
```
And then open `index.html` file with any http server to see the result (the browser may block loading of external modules into the file:// pages).
### What's the idea?
In json markup you can provide a special component:
```json
// DivJson
{
"type": "custom",
"custom_type": "custom_card"
}
```
Without any help, DivKit will not know about this special component. This can be done using the `customComponents` map:
```js
// JS code
render({
id: 'test',
target: document.querySelector('#root'),
customComponents: new Map([
['custom_card', {
element: 'custom-card'
}]
]),
json: {
...
}
});
```
Now we need to create our `custom-card` component. This can be done using the power of Custom Elements:
```js
// JS Code
class CustomCard extends HTMLElement {
constructor() {
super();
// some logic
}
}
customElements.define('custom-card', CustomCard);
```
This code defines a custom element inside the browser. So, any `custom` component inside DivKit should be a Custom Element in terms of HTML.
### How can I provide additional data for a component?
`custom` components in DivKit can accept an additional field `custom_props`. This is an object, and all its properties are directly mapped as attributes of your Custom Element:
```js
// DivJson
"custom_props": {
"start": 15
}
// ->
// HTML
<custom-card start="15"></custom-card>
```
Keep in mind that there are no integer attributes here, all additional properties will be rendered as regular html attributes.
Next, your Custom Element should read these properties:
```js
// JS code
connectedCallback() {
const start = this.getAttribute('start');
}
```
Keep in mind that these additional attributes will only be available in the `connectedCallback`, and not in the `constructor` of the Custom Element.
### What about browser support?
Custom elements have been supported in all modern browsers for a while. There are some nuances that will be covered in the following examples, but basic support for Custom Elements v1 will be a good start for your components.
### How can I wrap other DivKit components with my Custom Component?
See [next example](../custom-container).
@@ -0,0 +1,56 @@
class CustomCard extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const style = document.createElement('style');
style.textContent = `
.component {
padding: 30px;
font-size: 20px;
background: linear-gradient(85deg, #fff, #068200, #1eea00, #FF0000, #fcfa1e, #ccd49b, #7af719);
}
`;
const div = this._text = document.createElement('div');
div.className = 'component';
div.textContent = '00:00';
shadow.appendChild(style);
shadow.appendChild(div);
}
_updateText() {
const minutes = Math.floor(this._counter / 60);
const seconds = this._counter % 60;
this._text.textContent = String(minutes).padStart(2, '0') + ':' + String(seconds).padStart(2, '0');
}
_tick() {
this._timeout = window.setTimeout(() => {
++this._counter;
this._updateText();
this._tick();
}, 1000);
}
connectedCallback() {
const start = this.getAttribute('start');
this._counter = Number(start) || 0;
this._timeout = 0;
this._updateText();
this._tick();
}
disconnectedCallback() {
if (this._timeout) {
clearTimeout(this._timeout);
this._timeout = 0;
}
}
}
customElements.define('custom-card', CustomCard);
@@ -0,0 +1,52 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<link rel="stylesheet" type="text/css" href="node_modules/@divkitframework/divkit/dist/client.css">
</head>
<body>
<div id="root"></div>
<script type="module">
import { render } from './node_modules/@divkitframework/divkit/dist/esm/client.mjs';
import './customElement.js';
render({
id: 'test',
target: document.querySelector('#root'),
customComponents: new Map([
['custom_card', {
element: 'custom-card'
}]
]),
json: {
"templates": {
},
"card": {
"log_id": "snapshot_test_card",
"states": [
{
"state_id": 0,
"div": {
"type": "custom",
"custom_type": "custom_card",
"custom_props": {
"start": 15
}
}
}
]
}
}
});
</script>
</body>
</html>
@@ -0,0 +1,22 @@
{
"name": "custom-simple",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "custom-simple",
"version": "1.0.0",
"license": "UNLICENSED",
"devDependencies": {
"@divkitframework/divkit": "28.6.0-canary-f1eee8005aa682b1f02f9f9caec744f889bc1fdb"
}
},
"node_modules/@divkitframework/divkit": {
"version": "28.6.0-canary-f1eee8005aa682b1f02f9f9caec744f889bc1fdb",
"resolved": "https://registry.npmjs.org/@divkitframework/divkit/-/divkit-28.6.0-canary-f1eee8005aa682b1f02f9f9caec744f889bc1fdb.tgz",
"integrity": "sha512-EEqRHNF77NYZhVsYGxQLEku9q5tYniAjFJ0UzsxM2TMwt6lLmJ96hOZbMro6wM7Fh6XuwC3Kjkc6PQdPTUuIXw==",
"dev": true
}
}
}
@@ -0,0 +1,14 @@
{
"name": "custom-simple",
"private": true,
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {},
"keywords": [],
"author": "",
"license": "UNLICENSED",
"devDependencies": {
"@divkitframework/divkit": "28.6.0-canary-f1eee8005aa682b1f02f9f9caec744f889bc1fdb"
}
}
@@ -0,0 +1 @@
save-exact=true
@@ -0,0 +1,131 @@
## Using DivKit components inside your Custom Component
[General notes about custom components](../../divkit/README.md#customComponents)
[Previous example with a simple component](../custom-simple)
[Previous example with container component](../custom-container)
[General info about SSR in DivKit](../ssr)
This example assumes that you are familiar with the ideas from the previous examples.
Install the packages first:
```
npm ci
```
### Run sample
```shell
npm start
```
### How does it work without items?
The server code contains a similar `customComponents` map and will produce something like this:
```html
// HTML
<custom-container></custom-container>
```
On the client side, this markup will be hydrated. So, what about markup inside the Custom Element?
Lets assume that our component has some markup, for example, `Hello custom container`:
```html
// HTML
<custom-container>
<!-- rendered on client -->
Hello custom container
<!-- rendered on client -->
</custom-container>
```
Custom elements is defined on the client side, and their markup is created in the browser. This means that if we don't do anything, our glorious SSR (Server-side rendering) will produce incomplete markup, which will be changed after the Custom Element is defined. The component will work as expected, but the user may notice a layout shift and a markup change.
Declarative shadow dom comes to the rescue! If our `customComponents` contain not only the tag name of the Custom Element, but also a template itself, DivKit will produce a `<template>` for this component for us:
```js
// JS code
{
element: 'custom-container',
template: 'Hello custom container'
}
// HTML
<custom-container>
<template shadowrootmode="open">
Hello custom container
</template>
</custom-container>
```
This markup will be parsed in the browser directly into the shadow dom:
```html
<custom-container>
#shadow-root (open)
Hello custom container
</custom-container>
```
In this case, the browser will be able to show the exact page even without running JS on the client side, which means that there will be no layout shift on our page and it will be persisted during the entire page load.
Don't forget to change your custom element code:
```js
// JS Code
if (this.shadowRoot) {
// Declarative shadow dom is supported and parsed by the browser
} else {
// Create a shadow root as usual
const shadow = this.attachShadow({ mode: 'open' });
// ...
}
```
### How it works with items?
Literally the same. Don't forget to include `<slot></slot>` in your template, and everything will be fine:
```js
// JS code
{
element: 'custom-container',
template: 'Hello custom container<slot></slot>'
}
// HTML Server response
<custom-container>
<template shadowrootmode="open">
Hello custom container
</template>
<div>
<!--some DivKit markup-->
</div>
</custom-container>
// In the browser parsed HTML
<custom-container>
#shadow-root (open)
Hello custom container
<slot>
<div>
<!--some DivKit markup-->
</div>
</slot>
</custom-container>
```
### What about browser support?
Declarative Shadow DOM is currently supported only in Chromium-based browsers and in Safari. Firefox currently does not support this, but work on supporting this [will begin soon](https://github.com/mozilla/standards-positions/issues/335#issuecomment-1714182997).
### What about old browsers that will receive markup using that feature?
This will create a `<template>` element, but this template will not create shadow root automatically, so your component should do it the classic way. In conclusion, your Custom Component will work, but SSR will be "incomplete" in this browser, and the layout will be changed after JS execution.
@@ -0,0 +1,11 @@
class CustomContainer extends HTMLElement {
constructor() {
super();
if (!this.shadowRoot) {
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = 'Hello custom container<slot></slot>';
}
}
}
customElements.define('custom-container', CustomContainer);
+666
View File
@@ -0,0 +1,666 @@
{
"name": "custom-ssr",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "custom-ssr",
"version": "1.0.0",
"license": "UNLICENSED",
"devDependencies": {
"@divkitframework/divkit": "28.6.0-canary-f1eee8005aa682b1f02f9f9caec744f889bc1fdb",
"express": "4.18.2"
}
},
"node_modules/@divkitframework/divkit": {
"version": "28.6.0-canary-f1eee8005aa682b1f02f9f9caec744f889bc1fdb",
"resolved": "https://registry.npmjs.org/@divkitframework/divkit/-/divkit-28.6.0-canary-f1eee8005aa682b1f02f9f9caec744f889bc1fdb.tgz",
"integrity": "sha512-EEqRHNF77NYZhVsYGxQLEku9q5tYniAjFJ0UzsxM2TMwt6lLmJ96hOZbMro6wM7Fh6XuwC3Kjkc6PQdPTUuIXw==",
"dev": true
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"dev": true,
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"dev": true
},
"node_modules/body-parser": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
"integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
"dev": true,
"dependencies": {
"bytes": "3.1.2",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.11.0",
"raw-body": "2.5.1",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"dev": true,
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.1",
"get-intrinsic": "^1.0.2"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"dev": true,
"dependencies": {
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"dev": true,
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
"dev": true,
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"dev": true
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"dev": true,
"engines": {
"node": ">= 0.8"
}
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"dev": true,
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"dev": true
},
"node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"dev": true,
"engines": {
"node": ">= 0.8"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"dev": true
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"dev": true,
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express": {
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
"dev": true,
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.1",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.5.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.2.0",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.7",
"qs": "6.11.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.18.0",
"serve-static": "1.15.0",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.10.0"
}
},
"node_modules/finalhandler": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
"dev": true,
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"statuses": "2.0.1",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"dev": true,
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"dev": true,
"engines": {
"node": ">= 0.6"
}
},
"node_modules/function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
},
"node_modules/get-intrinsic": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
"integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz",
"integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==",
"dev": true,
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/has-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
"integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"dev": true,
"dependencies": {
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dev": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"dev": true,
"engines": {
"node": ">= 0.10"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"dev": true,
"engines": {
"node": ">= 0.6"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==",
"dev": true
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"dev": true,
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"dev": true,
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"dev": true,
"engines": {
"node": ">= 0.6"
}
},
"node_modules/object-inspect": {
"version": "1.12.3",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
"integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"dev": true,
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"dev": true,
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==",
"dev": true
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"dev": true,
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"dev": true,
"dependencies": {
"side-channel": "^1.0.4"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"dev": true,
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
"integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
"dev": true,
"dependencies": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true
},
"node_modules/send": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
"dev": true,
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "2.4.1",
"range-parser": "~1.2.1",
"statuses": "2.0.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true
},
"node_modules/serve-static": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
"dev": true,
"dependencies": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.18.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"dev": true
},
"node_modules/side-channel": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.0",
"get-intrinsic": "^1.0.2",
"object-inspect": "^1.9.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"dev": true,
"engines": {
"node": ">= 0.8"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"dev": true,
"engines": {
"node": ">=0.6"
}
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"dev": true,
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"dev": true,
"engines": {
"node": ">= 0.8"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"dev": true,
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"dev": true,
"engines": {
"node": ">= 0.8"
}
}
}
}
@@ -0,0 +1,15 @@
{
"name": "custom-ssr",
"version": "1.0.0",
"private": "true",
"keywords": [],
"scripts": {
"start": "node server.js"
},
"author": "",
"license": "UNLICENSED",
"devDependencies": {
"@divkitframework/divkit": "28.6.0-canary-f1eee8005aa682b1f02f9f9caec744f889bc1fdb",
"express": "4.18.2"
}
}
@@ -0,0 +1,107 @@
const path = require('path');
const fs = require('fs');
const express = require('express');
const { render } = require('@divkitframework/divkit/server');
const PORT = 3123;
const template = fs.readFileSync(path.resolve(__dirname, 'template.html'), 'utf-8');
function jsonFilter(str) {
return String(str).replace(/[<>&\u2028\u2029]/g, function(match) {
switch (match) {
case '<':
return '\\u003c';
case '>':
return '\\u003e';
case '&':
return '\\u0026';
case '\u2028':
return '\\u2028';
case '\u2029':
return '\\u2029';
}
return '';
});
}
const app = express();
const json = {
"templates": {
"news_item_template": {
"type": "text",
"font_size": 16,
"line_height": 20,
"text_color": "#333",
"$text": "news_item_text",
"paddings": {
"left": 12,
"right": 12,
"top": 8
}
}
},
"card": {
"log_id": "custom_card",
"states": [
{
"state_id": 0,
"div": {
"type": "container",
"margins": {
"left": 16,
"top": 16,
"right": 16,
"bottom": 16
},
"items": [
{
"type": "custom",
"custom_type": "custom_container",
"items": [
{
"type": "news_item_template",
"news_item_text": "This is div-text item 1"
},
{
"type": "news_item_template",
"news_item_text": "This is div-text item 2"
},
{
"type": "news_item_template",
"news_item_text": "This is div-text item 3"
}
]
}
]
}
}
]
}
};
app.get('/', (req, res, _next) => {
const html = render({
id: 'test',
json,
customComponents: new Map([
['custom_container', {
element: 'custom-container',
template: 'Hello custom container<slot></slot>'
}]
])
});
res.send(
template
.replace('[% html %]', () => html)
.replace('[% json %]', () => jsonFilter(JSON.stringify(json)))
);
});
app.use(express.static(__dirname));
app.listen(PORT, () => {
console.log(`Server started on http://localhost:${PORT}/`);
});
@@ -0,0 +1,36 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<link rel="stylesheet" type="text/css" href="node_modules/@divkitframework/divkit/dist/client.css">
</head>
<body>
<div id="root">[% html %]</div>
<script type="text/json" id="json">
[% json %]
</script>
<script type="module">
import { render } from './node_modules/@divkitframework/divkit/dist/esm/client-hydratable.mjs';
import './customElement.js';
const json = JSON.parse(document.querySelector('#json').textContent);
render({
id: 'test',
target: document.querySelector('#root'),
hydrate: true,
json,
customComponents: new Map([
['custom_container', {
element: 'custom-container'
}]
])
});
</script>
</body>
</html>
+16
View File
@@ -335,6 +335,22 @@ Also be aware of new releases (even minor and patch versions). Built-in extensio
[Building of a new extension](../divkit-examples/extensions-handmade/)
### customComponents
`Map<string, CustomComponentDescription>`, optional.
Custom components can be provided using the `customComponents` map. This structure maps names from json markup to html custom elements (or web components).
These components can provide any additional features that your application needs, such as map widgets, integration into social networks, YouTube videos, and so on.
Custom components is supported not only on the client side, but also on the server side. See the examples below for more detailed information:
[Simple custom component](../divkit-examples/custom-simple/)
[Custom component with the DivKit items](../divkit-examples/custom-container/)
[Custom component and SSR](../divkit-examples/custom-ssr/)
#### theme (EXPERIMENTAL)
`system` | `light` | `dark`
+7 -20
View File
@@ -13,6 +13,7 @@ import type {
TypefaceProvider
} from '../typings/common';
import type { GlobalVariablesController } from './expressions/globalVariablesController';
import type { CustomComponentDescription } from '../typings/custom';
export function render(opts: {
target: HTMLElement;
@@ -33,28 +34,14 @@ export function render(opts: {
theme?: Theme;
fetchInit?: FetchInit;
tooltipRoot?: HTMLElement;
customComponents?: Map<string, CustomComponentDescription> | undefined;
}) {
const { target, hydrate, ...rest } = opts;
return new Root({
target: opts.target,
props: {
id: opts.id,
json: opts.json,
globalVariablesController: opts.globalVariablesController,
mix: opts.mix,
customization: opts.customization,
builtinProtocols: opts.builtinProtocols,
extensions: opts.extensions,
onStat: opts.onStat,
onCustomAction: opts.onCustomAction,
onError: opts.onError,
onComponent: opts.onComponent,
typefaceProvider: opts.typefaceProvider,
platform: opts.platform,
theme: opts.theme,
fetchInit: opts.fetchInit,
tooltipRoot: opts.tooltipRoot
},
hydrate: opts.hydrate
target: target,
props: rest,
hydrate: hydrate
});
}
+2 -1
View File
@@ -13,5 +13,6 @@ module.exports.COMPONENTS = [
'slider',
'input',
'select',
'video'
'video',
'custom'
];
+3 -1
View File
@@ -39,6 +39,7 @@
DisappearAction,
FetchInit
} from '../../typings/common';
import type { CustomComponentDescription } from '../../typings/custom';
import type { AppearanceTransition, DivBaseData, Tooltip, TransitionChange } from '../types/base';
import type { SwitchElements, Overflow } from '../types/switch-elements';
import type { TintMode } from '../types/image';
@@ -70,7 +71,6 @@
} from '../expressions/globalVariablesController';
import { getUrlSchema, isBuiltinSchema } from '../utils/url';
import { TimersController } from '../utils/timers';
import TooltipView from './tooltip/Tooltip.svelte';
export let id: string;
export let json: Partial<DivJson> = {};
@@ -88,6 +88,7 @@
export let typefaceProvider: TypefaceProvider = _fontFamily => '';
export let fetchInit: FetchInit = {};
export let tooltipRoot: HTMLElement | undefined = undefined;
export let customComponents: Map<string, CustomComponentDescription> | undefined = undefined;
let isDesktop = writable(platform === 'desktop');
if (platform === 'auto' && typeof matchMedia !== 'undefined') {
@@ -972,6 +973,7 @@
typefaceProvider,
isDesktop,
isPointerFocus,
customComponents,
registerComponent: process.env.DEVTOOL ? registerComponentReal : undefined,
unregisterComponent: process.env.DEVTOOL ? unregisterComponentReal : undefined
});
@@ -0,0 +1,98 @@
<script lang="ts">
import { getContext } from 'svelte';
import type { LayoutParams } from '../../types/layoutParams';
import type { DivBase, TemplateContext } from '../../../typings/common';
import type { DivCustomData } from '../../types/custom';
import type { DivBaseData } from '../../types/base';
import type { CustomComponentDescription } from '../../../typings/custom';
import Unknown from '../utilities/Unknown.svelte';
import Outer from '../utilities/Outer.svelte';
import { ROOT_CTX, RootCtxValue } from '../../context/root';
import { wrapError } from '../../utils/wrapError';
export let json: Partial<DivCustomData> = {};
export let templateContext: TemplateContext;
export let origJson: DivBase | undefined = undefined;
export let layoutParams: LayoutParams | undefined = undefined;
const rootCtx = getContext<RootCtxValue>(ROOT_CTX);
let desc: CustomComponentDescription | null = null;
let templateContent = '';
// shadowrootmode is an unknown attribute in TS :(
let templateAttrs: any = {};
$: if (typeof json.custom_type === 'string' && json.custom_type && rootCtx.customComponents?.has(json.custom_type)) {
desc = rootCtx.customComponents.get(json.custom_type)!;
templateContent = desc.template || '';
templateAttrs = {
shadowrootmode: desc.shadowRootMode || 'open'
};
} else {
desc = null;
templateContent = ';';
rootCtx.logError(wrapError(new Error('Unknown or incorrect "custom_type" prop for div "custom"')));
}
let hasItemsError = false;
$: jsonItems = json.items;
$: {
if (jsonItems !== undefined && !Array.isArray(jsonItems)) {
hasItemsError = true;
rootCtx.logError(wrapError(new Error('Incorrect "items" prop for div "custom"')));
} else {
hasItemsError = false;
}
}
$: items = (!hasItemsError && jsonItems || []).map(item => {
let childJson: DivBaseData = item as DivBaseData;
let childContext: TemplateContext = templateContext;
({
templateContext: childContext,
json: childJson
} = rootCtx.processTemplate(childJson, childContext));
return {
json: childJson,
templateContext: childContext,
origJson: item
};
});
$: childLayoutParams = {
fakeElement: layoutParams?.fakeElement
};
</script>
{#if desc}
<Outer
{json}
{origJson}
{templateContext}
{layoutParams}
>
<svelte:element this={desc.element} {...(json.custom_props || {})}>
{#if templateContent}
<template {...templateAttrs}>
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html templateContent}
</template>
{/if}
{#if !hasItemsError && jsonItems}
{#key jsonItems}
{#each items as item}
<Unknown
layoutParams={childLayoutParams}
div={item.json}
templateContext={item.templateContext}
origJson={item.origJson}
/>
{/each}
{/key}
{/if}
</svelte:element>
</Outer>
{/if}
@@ -13,6 +13,7 @@ import Slider from './slider/Slider.svelte';
import Input from './input/Input.svelte';
import Select from './select/Select.svelte';
import Video from './video/Video.svelte';
import Custom from './custom/Custom.svelte';
export const TYPE_MAP: Record<string, ComponentType> = {};
@@ -75,3 +76,7 @@ if (process.env.ENABLE_COMPONENT_SELECT || process.env.ENABLE_COMPONENT_SELECT =
if (process.env.ENABLE_COMPONENT_VIDEO || process.env.ENABLE_COMPONENT_VIDEO === undefined) {
TYPE_MAP.video = Video;
}
if (process.env.ENABLE_COMPONENT_CUSTOM || process.env.ENABLE_COMPONENT_CUSTOM === undefined) {
TYPE_MAP.custom = Custom;
}
+2
View File
@@ -6,6 +6,7 @@ import type { MaybeMissing } from '../expressions/json';
import type { Variable, VariableType } from '../expressions/variable';
import type { TintMode } from '../types/image';
import type { Customization } from '../../typings/common';
import type { CustomComponentDescription } from '../../typings/custom';
export const ROOT_CTX = Symbol('root');
@@ -51,6 +52,7 @@ export interface RootCtxValue {
isPointerFocus: Readable<boolean>;
typefaceProvider: TypefaceProvider;
isDesktop: Readable<boolean>;
customComponents: Map<string, CustomComponentDescription> | undefined;
// Devtool
registerComponent?({
+18
View File
@@ -3,6 +3,7 @@ import { SizeProvider } from './extensions/sizeProvider';
import { lottieExtensionBuilder } from './extensions/lottie';
import type { DivExtensionClass } from '../typings/common';
import Lottie from 'lottie-web/build/player/lottie';
import './devCustomComponents';
const json = {
"templates": {},
@@ -32,6 +33,23 @@ window.root = new Root({
extensions: new Map<string, DivExtensionClass>([
['size_provider', SizeProvider],
['lottie', lottieExtensionBuilder(Lottie.loadAnimation)],
]),
customComponents: new Map([
['old_custom_card_1', {
element: 'old-custom-card1'
}],
['old_custom_card_2', {
element: 'old-custom-card2'
}],
['new_custom_card_1', {
element: 'new-custom-card'
}],
['new_custom_card_2', {
element: 'new-custom-card'
}],
['new_custom_container_1', {
element: 'new-custom-container'
}]
])
}
});
@@ -0,0 +1,80 @@
class OldCustomCard1 extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.textContent = 'hi, i\'m old card';
}
}
class OldCustomCard2 extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.textContent = 'and i\'m old as well';
}
}
class NewCustomCard extends HTMLElement {
private _counter: number;
private _timeout: number;
private _text: HTMLElement;
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const style = document.createElement('style');
style.textContent = `
.component {
padding: 30px;
font-size: 20px;
background: linear-gradient(85deg, #000400, #068200, #1eea00, #FF0000, #fcfa1e, #ccd49b, #7af719);
}
`;
const div = this._text = document.createElement('div');
div.className = 'component';
div.textContent = '00:00';
shadow.appendChild(style);
shadow.appendChild(div);
this._counter = 0;
this._timeout = 0;
this._tick();
}
_tick() {
this._timeout = window.setTimeout(() => {
++this._counter;
const minutes = Math.floor(this._counter / 60);
const seconds = this._counter % 60;
this._text.textContent = String(minutes).padStart(2, '0') + ':' + String(seconds).padStart(2, '0');
this._tick();
}, 1000);
}
disconnectedCallback() {
if (this._timeout) {
clearTimeout(this._timeout);
this._timeout = 0;
}
}
}
class NewCustomContainer extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = '<slot></slot>';
}
}
customElements.define('old-custom-card1', OldCustomCard1);
customElements.define('old-custom-card2', OldCustomCard2);
customElements.define('new-custom-card', NewCustomCard);
customElements.define('new-custom-container', NewCustomContainer);
+3 -11
View File
@@ -1,6 +1,7 @@
import Root from './components/Root.svelte';
import type { Platform, TypefaceProvider } from '../typings/common';
import type { GlobalVariablesController } from './expressions/globalVariablesController';
import type { CustomComponentDescription } from '../typings/custom';
export function render(opts: {
json: unknown;
@@ -12,19 +13,10 @@ export function render(opts: {
onError?: unknown;
typefaceProvider?: TypefaceProvider;
platform?: Platform;
customComponents?: Map<string, CustomComponentDescription> | undefined;
}) {
// Root has client-side typings, not a server one
return (Root as any).render({
id: opts.id,
json: opts.json,
globalVariablesController: opts.globalVariablesController,
mix: opts.mix,
customization: opts.customization,
builtinProtocols: opts.builtinProtocols,
onError: opts.onError,
typefaceProvider: opts.typefaceProvider,
platform: opts.platform
}).html;
return (Root as any).render(opts).html;
}
export {
+9
View File
@@ -0,0 +1,9 @@
import type { DivBaseData } from './base';
export interface DivCustomData extends DivBaseData {
type: 'custom';
items?: DivBaseData[];
custom_type: string;
custom_props?: object;
}
+2
View File
@@ -14,6 +14,7 @@ import type {
} from './common';
import type { GlobalVariablesController, Variable } from './variables';
import type { WrappedError } from './common';
import type { CustomComponentDescription } from './custom';
export interface DivkitDebugInstance extends DivkitInstance {
getDebugVariables(): Map<string, Variable>;
@@ -39,6 +40,7 @@ export function render(opts: {
theme?: Theme;
fetchInit?: FetchInit;
tooltipRoot?: HTMLElement;
customComponents?: Map<string, CustomComponentDescription> | undefined;
}): DivkitDebugInstance;
export { createVariable, createGlobalVariablesController } from './variables';
+2
View File
@@ -11,6 +11,7 @@ import type {
TypefaceProvider,
FetchInit
} from './common';
import type { CustomComponentDescription } from './custom';
import type { GlobalVariablesController } from './variables';
export function render(opts: {
@@ -32,6 +33,7 @@ export function render(opts: {
theme?: Theme;
fetchInit?: FetchInit;
tooltipRoot?: HTMLElement;
customComponents?: Map<string, CustomComponentDescription> | undefined;
}): DivkitInstance;
export { createVariable, createGlobalVariablesController } from './variables';
+2
View File
@@ -11,6 +11,7 @@ import type {
TypefaceProvider,
FetchInit
} from './common';
import type { CustomComponentDescription } from './custom';
import type { GlobalVariablesController } from './variables';
export function render(opts: {
@@ -31,6 +32,7 @@ export function render(opts: {
theme?: Theme;
fetchInit?: FetchInit;
tooltipRoot?: HTMLElement;
customComponents?: Map<string, CustomComponentDescription> | undefined;
}): DivkitInstance;
export { createVariable, createGlobalVariablesController } from './variables';
+5
View File
@@ -0,0 +1,5 @@
export interface CustomComponentDescription {
element: string;
shadowRootMode?: 'open' | 'close';
template?: string;
}
+2
View File
@@ -5,6 +5,7 @@ import type {
Customization,
TypefaceProvider
} from './common';
import type { CustomComponentDescription } from './custom';
export function render(opts: {
json: DivJson;
@@ -15,4 +16,5 @@ export function render(opts: {
builtinProtocols?: string[];
onError?: ErrorCallback;
typefaceProvider?: TypefaceProvider;
customComponents?: Map<string, CustomComponentDescription> | undefined;
}): string;
-4
View File
@@ -1,10 +1,6 @@
{
"java_extends": "DivBlockWithId",
"$description": "translations.json#/div_custom",
"platforms": [
"android",
"ios"
],
"allOf": [
{
"$ref": "div-base.json"
+1 -5
View File
@@ -44,11 +44,7 @@
},
{
"$ref": "div-custom.json",
"$description": "translations.json#/div_custom_short",
"platforms": [
"android",
"ios"
]
"$description": "translations.json#/div_custom_short"
},
{
"$ref": "div-indicator.json",
@@ -1564,18 +1564,10 @@
},
{
"title": "Custom container",
"platforms": [
"android",
"ios"
],
"file": "custom-container.json"
},
{
"title": "Custom card",
"platforms": [
"android",
"ios"
],
"file": "custom-card.json"
},
{