diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 00000000..e01b3e62
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,12 @@
+# These are supported funding model platforms
+
+github: [rmorshea]
+patreon: # Replace with a single Patreon username
+open_collective: # Replace with a single Open Collective username
+ko_fi: # Replace with a single Ko-fi username
+tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
+community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
+liberapay: # Replace with a single Liberapay username
+issuehunt: # Replace with a single IssueHunt username
+otechie: # Replace with a single Otechie username
+custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 00000000..037e3c2f
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,8 @@
+blank_issues_enabled: false
+contact_links:
+ - name: Documentation
+ url: https://idom-docs.herokuapp.com/
+ about: Refer to the documentation before starting a discussion
+ - name: Community Support
+ url: https://github.com/idom-team/idom/discussions
+ about: Report issues, request features, and ask questions
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 00000000..d3097b33
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,35 @@
+name: Test
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches:
+ - main
+ schedule:
+ - cron: "0 0 * * *"
+
+jobs:
+ test-python-versions:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: [3.7, 3.8, 3.9]
+ steps:
+ - uses: actions/checkout@v2
+ - uses: nanasess/setup-chromedriver@master
+ - uses: actions/setup-node@v2-beta
+ with:
+ node-version: "14"
+ - name: Use Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v2
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Install Python Dependencies
+ run: pip install -r requirements/test-run.txt
+ - name: Run Tests
+ run: |
+ npm install -g npm@latest
+ npm --version
+ nox -s test -- --headless
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..434278f2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,128 @@
+# Django #
+logs
+*.log
+*.pot
+*.pyc
+.dccachea
+__pycache__
+db.sqlite3
+media
+cache
+static-deploy
+data
+settings.json
+
+# Backup files #
+*.bak
+
+# If you are using PyCharm #
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/dictionaries
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.xml
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/gradle.xml
+.idea/**/libraries
+*.iws /out/
+
+# Python #
+*.py[cod]
+*$py.class
+
+# Distribution / packaging
+.Python build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+.pytest_cache/
+nosetests.xml
+coverage.xml
+*.cover
+.hypothesis/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# pyenv
+.python-version
+
+# celery
+celerybeat-schedule.*
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+
+# Sublime Text #
+*.tmlanguage.cache
+*.tmPreferences.cache
+*.stTheme.cache
+*.sublime-workspace
+*.sublime-project
+
+# sftp configuration file
+sftp-config.json
+
+# Package control specific files Package
+Control.last-run
+Control.ca-list
+Control.ca-bundle
+Control.system-ca-bundle
+GitHub.sublime-settings
+
+# Visual Studio Code #
+.vscode
+.vscode/*
+.vscode/settings.json
+.vscode/tasks.json
+.vscode/launch.json
+.vscode/extensions.json
+.history
+%SystemDrive%
+
+# Mac file system
+.DS_Store
diff --git a/README.md b/README.md
index 2444be03..943a2aed 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,52 @@
# Django IDOM
-Support for IDOM in Django
+
+
+
+
+
+
+
+
+
+
+A package for building highly interactive user interfaces in pure Python inspired by
+[ReactJS](https://reactjs.org/).
+
+**Be sure to [read the IDOM Documentation](https://idom-docs.herokuapp.com)!**
+
+If you have ideas or find a bug, be sure to post an
+[issue](https://github.com/idom-team/django-idom/issues)
+or create a
+[pull request](https://github.com/idom-team/django-idom/pulls). Thanks in advance!
+
+
+
+Click the badge above to get started! It will take you to a [Jupyter Notebooks](https://jupyter.org/)
+hosted by [Binder](https://mybinder.org/) with some great examples.
+
+### Or Install it Now
+
+```bash
+pip install django-idom
+```
+
+# Django Integration
+
+This version of IDOM can be directly integrated into Django. For example
+
+```python
+# Example code goes here
+```
+
+For examples on how to use IDOM, [read the IDOM Documentation](https://idom-docs.herokuapp.com).
diff --git a/noxfile.py b/noxfile.py
new file mode 100644
index 00000000..4a943345
--- /dev/null
+++ b/noxfile.py
@@ -0,0 +1,78 @@
+from __future__ import annotations
+
+import os
+import re
+import subprocess
+from pathlib import Path
+from typing import List, Tuple
+
+import nox
+from nox.sessions import Session
+
+
+HERE = Path(__file__).parent
+POSARGS_PATTERN = re.compile(r"^(\w+)\[(.+)\]$")
+
+
+@nox.session(reuse_venv=True)
+def manage(session: Session) -> None:
+ session.install("-r", "requirements.txt")
+ session.install("idom[stable]")
+ session.install("-e", ".")
+ session.chdir("tests")
+
+ build_js_on_commands = ["runserver"]
+ if set(session.posargs).intersection(build_js_on_commands):
+ session.run("python", "manage.py", "build_js")
+
+ session.run("python", "manage.py", *session.posargs)
+
+
+@nox.session(reuse_venv=True)
+def format(session: Session) -> None:
+ install_requirements_file(session, "check-style")
+ session.run("black", ".")
+ session.run("isort", ".")
+
+
+@nox.session
+def test(session: Session) -> None:
+ """Run the complete test suite"""
+ session.install("--upgrade", "pip", "setuptools", "wheel")
+ session.notify("test_suite", posargs=session.posargs)
+ session.notify("test_style")
+
+
+@nox.session
+def test_suite(session: Session) -> None:
+ """Run the Python-based test suite"""
+ install_requirements_file(session, "test-env")
+ session.install(".[all]")
+
+ session.chdir(HERE / "tests")
+ session.env["IDOM_DEBUG_MODE"] = "1"
+ session.env["SELENIUM_HEADLESS"] = str(int("--headless" in session.posargs))
+ session.run("python", "manage.py", "build_js")
+ session.run("python", "manage.py", "test")
+
+
+@nox.session
+def test_style(session: Session) -> None:
+ """Check that style guidelines are being followed"""
+ install_requirements_file(session, "check-style")
+ session.run("flake8", "src/django_idom", "tests")
+ black_default_exclude = r"\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|\.svn|_build|buck-out|build|dist"
+ session.run(
+ "black",
+ ".",
+ "--check",
+ "--exclude",
+ rf"/({black_default_exclude}|venv|node_modules)/",
+ )
+ session.run("isort", ".", "--check-only")
+
+
+def install_requirements_file(session: Session, name: str) -> None:
+ file_path = HERE / "requirements" / (name + ".txt")
+ assert file_path.exists(), f"requirements file {file_path} does not exist"
+ session.install("-r", str(file_path))
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000..18a77f5a
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,12 @@
+[build-system]
+requires = ["setuptools>=42", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[tool.isort]
+multi_line_output = 3
+force_grid_wrap = 0
+use_parentheses = "True"
+ensure_newline_before_comments = "True"
+include_trailing_comma = "True"
+line_length = 88
+lines_after_imports = 2
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 00000000..22728910
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,4 @@
+-r requirements/pkg-deps.txt
+-r requirements/check-style.txt
+-r requirements/test-env.txt
+-r requirements/test-run.txt
diff --git a/requirements/check-style.txt b/requirements/check-style.txt
new file mode 100644
index 00000000..5e5647ac
--- /dev/null
+++ b/requirements/check-style.txt
@@ -0,0 +1,3 @@
+black
+flake8
+isort
diff --git a/requirements/pkg-deps.txt b/requirements/pkg-deps.txt
new file mode 100644
index 00000000..b823d656
--- /dev/null
+++ b/requirements/pkg-deps.txt
@@ -0,0 +1,2 @@
+channels<4.0.0 # Django websocket features
+idom<1.0.0 # Python React
diff --git a/requirements/test-env.txt b/requirements/test-env.txt
new file mode 100644
index 00000000..c100c316
--- /dev/null
+++ b/requirements/test-env.txt
@@ -0,0 +1,6 @@
+django
+selenium
+
+# required due issue with channels:
+# https://github.com/django/channels/issues/1639#issuecomment-817994671
+twisted<21
diff --git a/requirements/test-run.txt b/requirements/test-run.txt
new file mode 100644
index 00000000..816817c6
--- /dev/null
+++ b/requirements/test-run.txt
@@ -0,0 +1 @@
+nox
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 00000000..998619dd
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,11 @@
+[bdist_wheel]
+universal=1
+
+[flake8]
+ignore = E203, E266, E501, W503, F811, N802
+max-line-length = 88
+max-complexity = 18
+select = B,C,E,F,W,T4,B9,N,ROH
+exclude =
+ .eggs/*
+ .nox/*
diff --git a/setup.py b/setup.py
new file mode 100644
index 00000000..dbdbbf67
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,96 @@
+import os
+import sys
+from pathlib import Path
+
+from setuptools import find_packages, setup
+
+
+# the name of the project
+name = "django_idom"
+
+# basic paths used to gather files
+root_dir = Path(__file__).parent
+src_dir = root_dir / "src"
+package_dir = src_dir / name
+
+
+# -----------------------------------------------------------------------------
+# Package Definition
+# -----------------------------------------------------------------------------
+
+
+package = {
+ "name": name,
+ "python_requires": ">=3.7",
+ "packages": find_packages(str(src_dir)),
+ "package_dir": {"": "src"},
+ "description": "Control the web with Python",
+ "author": "Ryan Morshead",
+ "author_email": "ryan.morshead@gmail.com",
+ "url": "https://github.com/idom-team/django-idom",
+ "license": "MIT",
+ "platforms": "Linux, Mac OS X, Windows",
+ "keywords": ["interactive", "widgets", "DOM", "React"],
+ "zip_safe": False,
+ "classifiers": [
+ "Framework :: Django",
+ "Framework :: Django :: 3.1",
+ "Framework :: Django :: 3.2",
+ "Operating System :: OS Independent",
+ "Intended Audience :: Developers",
+ "Intended Audience :: Science/Research",
+ "Topic :: Multimedia :: Graphics",
+ "Programming Language :: Python :: 3.7",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Environment :: Web Environment",
+ ],
+}
+
+
+# -----------------------------------------------------------------------------
+# Library Version
+# -----------------------------------------------------------------------------
+
+with open(os.path.join(package_dir, "__init__.py")) as f:
+ for line in f.read().split("\n"):
+ if line.startswith("__version__ = "):
+ package["version"] = eval(line.split("=", 1)[1])
+ break
+ else:
+ print("No version found in %s/__init__.py" % package_dir)
+ sys.exit(1)
+
+
+# -----------------------------------------------------------------------------
+# Requirements
+# -----------------------------------------------------------------------------
+
+
+requirements = []
+with (root_dir / "requirements" / "pkg-deps.txt").open() as f:
+ for line in map(str.strip, f):
+ if not line.startswith("#"):
+ requirements.append(line)
+package["install_requires"] = requirements
+
+
+# -----------------------------------------------------------------------------
+# Library Description
+# -----------------------------------------------------------------------------
+
+
+with (root_dir / "README.md").open() as f:
+ long_description = f.read()
+
+package["long_description"] = long_description
+package["long_description_content_type"] = "text/markdown"
+
+
+# -----------------------------------------------------------------------------
+# Install It
+# -----------------------------------------------------------------------------
+
+
+if __name__ == "__main__":
+ setup(**package)
diff --git a/src/django_idom/__init__.py b/src/django_idom/__init__.py
new file mode 100644
index 00000000..60932c7a
--- /dev/null
+++ b/src/django_idom/__init__.py
@@ -0,0 +1,5 @@
+from .websocket_consumer import IdomAsyncWebSocketConsumer
+
+
+__version__ = "0.0.1"
+__all__ = ["IdomAsyncWebSocketConsumer"]
diff --git a/src/django_idom/websocket_consumer.py b/src/django_idom/websocket_consumer.py
new file mode 100644
index 00000000..2fae2869
--- /dev/null
+++ b/src/django_idom/websocket_consumer.py
@@ -0,0 +1,44 @@
+"""Anything used to construct a websocket endpoint"""
+import asyncio
+from typing import Any
+
+from channels.generic.websocket import AsyncJsonWebsocketConsumer
+from idom.core.dispatcher import dispatch_single_view
+from idom.core.layout import Layout, LayoutEvent
+from idom.core.proto import ComponentConstructor
+
+
+class IdomAsyncWebSocketConsumer(AsyncJsonWebsocketConsumer):
+ """Communicates with the browser to perform actions on-demand."""
+
+ def __init__(
+ self, component: ComponentConstructor, *args: Any, **kwargs: Any
+ ) -> None:
+ self._idom_component_constructor = component
+ super().__init__(*args, **kwargs)
+
+ async def connect(self) -> None:
+ await super().connect()
+ self._idom_dispatcher_future = asyncio.ensure_future(self._run_dispatch_loop())
+
+ async def disconnect(self, code: int) -> None:
+ if self._idom_dispatcher_future.done():
+ await self._idom_dispatcher_future
+ else:
+ self._idom_dispatcher_future.cancel()
+ await super().disconnect(code)
+
+ async def receive_json(self, content: Any, **kwargs: Any) -> None:
+ await self._idom_recv_queue.put(LayoutEvent(**content))
+
+ async def _run_dispatch_loop(self):
+ self._idom_recv_queue = recv_queue = asyncio.Queue()
+ try:
+ await dispatch_single_view(
+ Layout(self._idom_component_constructor()),
+ self.send_json,
+ recv_queue.get,
+ )
+ except Exception:
+ await self.close()
+ raise
diff --git a/tests/.gitignore b/tests/.gitignore
new file mode 100644
index 00000000..bf4a6d58
--- /dev/null
+++ b/tests/.gitignore
@@ -0,0 +1 @@
+test_app/static/build.js
diff --git a/tests/js/.gitignore b/tests/js/.gitignore
new file mode 100644
index 00000000..5980a333
--- /dev/null
+++ b/tests/js/.gitignore
@@ -0,0 +1,2 @@
+# Javascript
+node_modules
diff --git a/tests/js/package-lock.json b/tests/js/package-lock.json
new file mode 100644
index 00000000..6639bf96
--- /dev/null
+++ b/tests/js/package-lock.json
@@ -0,0 +1,603 @@
+{
+ "name": "tests",
+ "version": "1.0.0",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "tests",
+ "version": "1.0.0",
+ "dependencies": {
+ "idom-client-react": "^0.8.2"
+ },
+ "devDependencies": {
+ "prettier": "^2.2.1",
+ "rollup": "^2.35.1",
+ "rollup-plugin-commonjs": "^10.1.0",
+ "rollup-plugin-node-resolve": "^5.2.0",
+ "rollup-plugin-replace": "^2.2.0"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "0.0.48",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.48.tgz",
+ "integrity": "sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew==",
+ "dev": true
+ },
+ "node_modules/@types/node": {
+ "version": "15.12.2",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz",
+ "integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww==",
+ "dev": true
+ },
+ "node_modules/@types/resolve": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz",
+ "integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/builtin-modules": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz",
+ "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz",
+ "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==",
+ "dev": true
+ },
+ "node_modules/fast-json-patch": {
+ "version": "3.0.0-1",
+ "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.0.0-1.tgz",
+ "integrity": "sha512-6pdFb07cknxvPzCeLsFHStEy+MysPJPgZQ9LbQ/2O67unQF93SNqfdSqnPPl71YMHX+AD8gbl7iuoGFzHEdDuw=="
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "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/has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/htm": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/htm/-/htm-3.0.4.tgz",
+ "integrity": "sha512-VRdvxX3tmrXuT/Ovt59NMp/ORMFi4bceFMDjos1PV4E0mV+5votuID8R60egR9A4U8nLt238R/snlJGz3UYiTQ=="
+ },
+ "node_modules/idom-client-react": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/idom-client-react/-/idom-client-react-0.8.2.tgz",
+ "integrity": "sha512-pK4FjyfVIaOVA/R0sj6Ulvpo3FATFU11TbnoqgzGbXjjY7kYPuR3x2pa/M6MY/Ot9yUb2Nsz9Gr1Vj8QPJ6GwA==",
+ "dependencies": {
+ "fast-json-patch": "^3.0.0-1",
+ "htm": "^3.0.3"
+ },
+ "peerDependencies": {
+ "react": "^16.13.1",
+ "react-dom": "^16.13.1"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz",
+ "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==",
+ "dev": true,
+ "dependencies": {
+ "has": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-module": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
+ "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=",
+ "dev": true
+ },
+ "node_modules/is-reference": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
+ "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree": "*"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "peer": true
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "peer": true,
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.25.7",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
+ "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==",
+ "dev": true,
+ "dependencies": {
+ "sourcemap-codec": "^1.4.4"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
+ "peer": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true
+ },
+ "node_modules/prettier": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.1.tgz",
+ "integrity": "sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA==",
+ "dev": true,
+ "bin": {
+ "prettier": "bin-prettier.js"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/prop-types": {
+ "version": "15.7.2",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
+ "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
+ "peer": true,
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.8.1"
+ }
+ },
+ "node_modules/react": {
+ "version": "16.14.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz",
+ "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==",
+ "peer": true,
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "object-assign": "^4.1.1",
+ "prop-types": "^15.6.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "16.14.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz",
+ "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==",
+ "peer": true,
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "object-assign": "^4.1.1",
+ "prop-types": "^15.6.2",
+ "scheduler": "^0.19.1"
+ },
+ "peerDependencies": {
+ "react": "^16.14.0"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "peer": true
+ },
+ "node_modules/resolve": {
+ "version": "1.20.0",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
+ "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
+ "dev": true,
+ "dependencies": {
+ "is-core-module": "^2.2.0",
+ "path-parse": "^1.0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "2.51.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.51.2.tgz",
+ "integrity": "sha512-ReV2eGEadA7hmXSzjxdDKs10neqH2QURf2RxJ6ayAlq93ugy6qIvXMmbc5cWMGCDh1h5T4thuWO1e2VNbMq8FA==",
+ "dev": true,
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.1"
+ }
+ },
+ "node_modules/rollup-plugin-commonjs": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-10.1.0.tgz",
+ "integrity": "sha512-jlXbjZSQg8EIeAAvepNwhJj++qJWNJw1Cl0YnOqKtP5Djx+fFGkp3WRh+W0ASCaFG5w1jhmzDxgu3SJuVxPF4Q==",
+ "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-commonjs.",
+ "dev": true,
+ "dependencies": {
+ "estree-walker": "^0.6.1",
+ "is-reference": "^1.1.2",
+ "magic-string": "^0.25.2",
+ "resolve": "^1.11.0",
+ "rollup-pluginutils": "^2.8.1"
+ },
+ "peerDependencies": {
+ "rollup": ">=1.12.0"
+ }
+ },
+ "node_modules/rollup-plugin-node-resolve": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-5.2.0.tgz",
+ "integrity": "sha512-jUlyaDXts7TW2CqQ4GaO5VJ4PwwaV8VUGA7+km3n6k6xtOEacf61u0VXwN80phY/evMcaS+9eIeJ9MOyDxt5Zw==",
+ "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-node-resolve.",
+ "dev": true,
+ "dependencies": {
+ "@types/resolve": "0.0.8",
+ "builtin-modules": "^3.1.0",
+ "is-module": "^1.0.0",
+ "resolve": "^1.11.1",
+ "rollup-pluginutils": "^2.8.1"
+ },
+ "peerDependencies": {
+ "rollup": ">=1.11.0"
+ }
+ },
+ "node_modules/rollup-plugin-replace": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/rollup-plugin-replace/-/rollup-plugin-replace-2.2.0.tgz",
+ "integrity": "sha512-/5bxtUPkDHyBJAKketb4NfaeZjL5yLZdeUihSfbF2PQMz+rSTEb8ARKoOl3UBT4m7/X+QOXJo3sLTcq+yMMYTA==",
+ "deprecated": "This module has moved and is now available at @rollup/plugin-replace. Please update your dependencies. This version is no longer maintained.",
+ "dev": true,
+ "dependencies": {
+ "magic-string": "^0.25.2",
+ "rollup-pluginutils": "^2.6.0"
+ }
+ },
+ "node_modules/rollup-pluginutils": {
+ "version": "2.8.2",
+ "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz",
+ "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==",
+ "dev": true,
+ "dependencies": {
+ "estree-walker": "^0.6.1"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz",
+ "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==",
+ "peer": true,
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "object-assign": "^4.1.1"
+ }
+ },
+ "node_modules/sourcemap-codec": {
+ "version": "1.4.8",
+ "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
+ "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
+ "dev": true
+ }
+ },
+ "dependencies": {
+ "@types/estree": {
+ "version": "0.0.48",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.48.tgz",
+ "integrity": "sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew==",
+ "dev": true
+ },
+ "@types/node": {
+ "version": "15.12.2",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz",
+ "integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww==",
+ "dev": true
+ },
+ "@types/resolve": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz",
+ "integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
+ "builtin-modules": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz",
+ "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==",
+ "dev": true
+ },
+ "estree-walker": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz",
+ "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==",
+ "dev": true
+ },
+ "fast-json-patch": {
+ "version": "3.0.0-1",
+ "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.0.0-1.tgz",
+ "integrity": "sha512-6pdFb07cknxvPzCeLsFHStEy+MysPJPgZQ9LbQ/2O67unQF93SNqfdSqnPPl71YMHX+AD8gbl7iuoGFzHEdDuw=="
+ },
+ "fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "optional": true
+ },
+ "function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+ "dev": true
+ },
+ "has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "dev": true,
+ "requires": {
+ "function-bind": "^1.1.1"
+ }
+ },
+ "htm": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/htm/-/htm-3.0.4.tgz",
+ "integrity": "sha512-VRdvxX3tmrXuT/Ovt59NMp/ORMFi4bceFMDjos1PV4E0mV+5votuID8R60egR9A4U8nLt238R/snlJGz3UYiTQ=="
+ },
+ "idom-client-react": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/idom-client-react/-/idom-client-react-0.8.2.tgz",
+ "integrity": "sha512-pK4FjyfVIaOVA/R0sj6Ulvpo3FATFU11TbnoqgzGbXjjY7kYPuR3x2pa/M6MY/Ot9yUb2Nsz9Gr1Vj8QPJ6GwA==",
+ "requires": {
+ "fast-json-patch": "^3.0.0-1",
+ "htm": "^3.0.3"
+ }
+ },
+ "is-core-module": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz",
+ "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==",
+ "dev": true,
+ "requires": {
+ "has": "^1.0.3"
+ }
+ },
+ "is-module": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
+ "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=",
+ "dev": true
+ },
+ "is-reference": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
+ "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
+ "dev": true,
+ "requires": {
+ "@types/estree": "*"
+ }
+ },
+ "js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "peer": true
+ },
+ "loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "peer": true,
+ "requires": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ }
+ },
+ "magic-string": {
+ "version": "0.25.7",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
+ "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==",
+ "dev": true,
+ "requires": {
+ "sourcemap-codec": "^1.4.4"
+ }
+ },
+ "object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
+ "peer": true
+ },
+ "path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true
+ },
+ "prettier": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.1.tgz",
+ "integrity": "sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA==",
+ "dev": true
+ },
+ "prop-types": {
+ "version": "15.7.2",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
+ "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
+ "peer": true,
+ "requires": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.8.1"
+ }
+ },
+ "react": {
+ "version": "16.14.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz",
+ "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==",
+ "peer": true,
+ "requires": {
+ "loose-envify": "^1.1.0",
+ "object-assign": "^4.1.1",
+ "prop-types": "^15.6.2"
+ }
+ },
+ "react-dom": {
+ "version": "16.14.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz",
+ "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==",
+ "peer": true,
+ "requires": {
+ "loose-envify": "^1.1.0",
+ "object-assign": "^4.1.1",
+ "prop-types": "^15.6.2",
+ "scheduler": "^0.19.1"
+ }
+ },
+ "react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "peer": true
+ },
+ "resolve": {
+ "version": "1.20.0",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
+ "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
+ "dev": true,
+ "requires": {
+ "is-core-module": "^2.2.0",
+ "path-parse": "^1.0.6"
+ }
+ },
+ "rollup": {
+ "version": "2.51.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.51.2.tgz",
+ "integrity": "sha512-ReV2eGEadA7hmXSzjxdDKs10neqH2QURf2RxJ6ayAlq93ugy6qIvXMmbc5cWMGCDh1h5T4thuWO1e2VNbMq8FA==",
+ "dev": true,
+ "requires": {
+ "fsevents": "~2.3.1"
+ }
+ },
+ "rollup-plugin-commonjs": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-10.1.0.tgz",
+ "integrity": "sha512-jlXbjZSQg8EIeAAvepNwhJj++qJWNJw1Cl0YnOqKtP5Djx+fFGkp3WRh+W0ASCaFG5w1jhmzDxgu3SJuVxPF4Q==",
+ "dev": true,
+ "requires": {
+ "estree-walker": "^0.6.1",
+ "is-reference": "^1.1.2",
+ "magic-string": "^0.25.2",
+ "resolve": "^1.11.0",
+ "rollup-pluginutils": "^2.8.1"
+ }
+ },
+ "rollup-plugin-node-resolve": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-5.2.0.tgz",
+ "integrity": "sha512-jUlyaDXts7TW2CqQ4GaO5VJ4PwwaV8VUGA7+km3n6k6xtOEacf61u0VXwN80phY/evMcaS+9eIeJ9MOyDxt5Zw==",
+ "dev": true,
+ "requires": {
+ "@types/resolve": "0.0.8",
+ "builtin-modules": "^3.1.0",
+ "is-module": "^1.0.0",
+ "resolve": "^1.11.1",
+ "rollup-pluginutils": "^2.8.1"
+ }
+ },
+ "rollup-plugin-replace": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/rollup-plugin-replace/-/rollup-plugin-replace-2.2.0.tgz",
+ "integrity": "sha512-/5bxtUPkDHyBJAKketb4NfaeZjL5yLZdeUihSfbF2PQMz+rSTEb8ARKoOl3UBT4m7/X+QOXJo3sLTcq+yMMYTA==",
+ "dev": true,
+ "requires": {
+ "magic-string": "^0.25.2",
+ "rollup-pluginutils": "^2.6.0"
+ }
+ },
+ "rollup-pluginutils": {
+ "version": "2.8.2",
+ "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz",
+ "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==",
+ "dev": true,
+ "requires": {
+ "estree-walker": "^0.6.1"
+ }
+ },
+ "scheduler": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz",
+ "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==",
+ "peer": true,
+ "requires": {
+ "loose-envify": "^1.1.0",
+ "object-assign": "^4.1.1"
+ }
+ },
+ "sourcemap-codec": {
+ "version": "1.4.8",
+ "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
+ "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
+ "dev": true
+ }
+ }
+}
diff --git a/tests/js/package.json b/tests/js/package.json
new file mode 100644
index 00000000..3844cf32
--- /dev/null
+++ b/tests/js/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "tests",
+ "version": "1.0.0",
+ "description": "test app for idom_django websocket server",
+ "main": "src/index.js",
+ "files": [
+ "src/**/*.js"
+ ],
+ "scripts": {
+ "build": "rollup --config",
+ "format": "prettier --ignore-path .gitignore --write ."
+ },
+ "dependencies": {
+ "idom-client-react": "^0.8.2"
+ },
+ "devDependencies": {
+ "prettier": "^2.2.1",
+ "rollup": "^2.35.1",
+ "rollup-plugin-commonjs": "^10.1.0",
+ "rollup-plugin-node-resolve": "^5.2.0",
+ "rollup-plugin-replace": "^2.2.0"
+ }
+}
diff --git a/tests/js/rollup.config.js b/tests/js/rollup.config.js
new file mode 100644
index 00000000..ad597a61
--- /dev/null
+++ b/tests/js/rollup.config.js
@@ -0,0 +1,33 @@
+import resolve from "rollup-plugin-node-resolve";
+import commonjs from "rollup-plugin-commonjs";
+import replace from "rollup-plugin-replace";
+
+const { PRODUCTION } = process.env;
+
+export default {
+ input: "src/index.js",
+ output: {
+ file: "../test_app/static/build.js",
+ format: "esm",
+ },
+ plugins: [
+ resolve(),
+ commonjs(),
+ replace({
+ "process.env.NODE_ENV": JSON.stringify(
+ PRODUCTION ? "production" : "development"
+ ),
+ }),
+ ],
+ onwarn: function (warning) {
+ // Skip certain warnings
+
+ // should intercept ... but doesn't in some rollup versions
+ if (warning.code === "THIS_IS_UNDEFINED") {
+ return;
+ }
+
+ // console.warn everything else
+ console.warn(warning.message);
+ },
+};
diff --git a/tests/js/src/index.js b/tests/js/src/index.js
new file mode 100644
index 00000000..613515dc
--- /dev/null
+++ b/tests/js/src/index.js
@@ -0,0 +1,13 @@
+import { mountLayoutWithWebSocket } from "idom-client-react";
+
+// Set up a websocket at the base endpoint
+let LOCATION = window.location;
+let WS_PROTOCOL = "";
+if (LOCATION.protocol == "https:") {
+ WS_PROTOCOL = "wss://";
+} else {
+ WS_PROTOCOL = "ws://";
+}
+let WS_ENDPOINT_URL = WS_PROTOCOL + LOCATION.host;
+
+mountLayoutWithWebSocket(document.getElementById("mount"), WS_ENDPOINT_URL);
diff --git a/tests/manage.py b/tests/manage.py
new file mode 100644
index 00000000..234cb618
--- /dev/null
+++ b/tests/manage.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+import os
+import sys
+
+
+def main():
+ """Run administrative tasks."""
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_app.settings")
+ try:
+ from django.core.management import execute_from_command_line
+ except ImportError as exc:
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ ) from exc
+ execute_from_command_line(sys.argv)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tests/test_app/__init__.py b/tests/test_app/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/test_app/asgi.py b/tests/test_app/asgi.py
new file mode 100644
index 00000000..113b147f
--- /dev/null
+++ b/tests/test_app/asgi.py
@@ -0,0 +1,35 @@
+"""
+ASGI config for test_app project.
+
+It exposes the ASGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
+"""
+
+import os
+
+from django.conf.urls import url
+from django.core.asgi import get_asgi_application
+
+from django_idom import IdomAsyncWebSocketConsumer # noqa: E402
+
+from .views import Root
+
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_app.settings")
+
+# Fetch ASGI application before importing dependencies that require ORM models.
+http_asgi_app = get_asgi_application()
+
+from channels.routing import ProtocolTypeRouter, URLRouter # noqa: E402
+
+
+application = ProtocolTypeRouter(
+ {
+ "http": http_asgi_app,
+ "websocket": URLRouter(
+ [url("", IdomAsyncWebSocketConsumer.as_asgi(component=Root))]
+ ),
+ }
+)
diff --git a/tests/test_app/management/__init__.py b/tests/test_app/management/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/test_app/management/commands/__init__.py b/tests/test_app/management/commands/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/test_app/management/commands/build_js.py b/tests/test_app/management/commands/build_js.py
new file mode 100644
index 00000000..61af5ed9
--- /dev/null
+++ b/tests/test_app/management/commands/build_js.py
@@ -0,0 +1,16 @@
+import subprocess
+from pathlib import Path
+
+from django.core.management.base import BaseCommand
+
+
+HERE = Path(__file__).parent
+JS_DIR = HERE.parent.parent.parent / "js"
+
+
+class Command(BaseCommand):
+ help = "Build javascript source for test app"
+
+ def handle(self, *args, **options):
+ subprocess.run(["npm", "install"], cwd=JS_DIR, check=True)
+ subprocess.run(["npm", "run", "build"], cwd=JS_DIR, check=True)
diff --git a/tests/test_app/migrations/__init__.py b/tests/test_app/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/test_app/settings.py b/tests/test_app/settings.py
new file mode 100644
index 00000000..73eda0fa
--- /dev/null
+++ b/tests/test_app/settings.py
@@ -0,0 +1,122 @@
+"""
+Django settings for test_app project.
+
+Generated by 'django-admin startproject' using Django 3.2.3.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.2/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/3.2/ref/settings/
+"""
+import os
+import sys
+from pathlib import Path
+
+
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+BASE_DIR = Path(__file__).resolve().parent.parent
+SRC_DIR = BASE_DIR.parent / "src"
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = "django-insecure-n!bd1#+7ufw5#9ipayu9k(lyu@za$c2ajbro7es(v8_7w1$=&c"
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+ALLOWED_HOSTS = []
+
+# Application definition
+INSTALLED_APPS = [
+ "django.contrib.admin",
+ "django.contrib.auth",
+ "django.contrib.contenttypes",
+ "django.contrib.sessions",
+ "django.contrib.messages",
+ "django.contrib.staticfiles",
+ "channels", # Websocket library
+ "test_app", # This test application
+]
+MIDDLEWARE = [
+ "django.middleware.security.SecurityMiddleware",
+ "django.contrib.sessions.middleware.SessionMiddleware",
+ "django.middleware.common.CommonMiddleware",
+ "django.middleware.csrf.CsrfViewMiddleware",
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
+ "django.contrib.messages.middleware.MessageMiddleware",
+ "django.middleware.clickjacking.XFrameOptionsMiddleware",
+]
+ROOT_URLCONF = "test_app.urls"
+TEMPLATES = [
+ {
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
+ "DIRS": [os.path.join(BASE_DIR, "test_app", "templates")],
+ "APP_DIRS": True,
+ "OPTIONS": {
+ "context_processors": [
+ "django.template.context_processors.debug",
+ "django.template.context_processors.request",
+ "django.contrib.auth.context_processors.auth",
+ "django.contrib.messages.context_processors.messages",
+ ],
+ },
+ },
+]
+ASGI_APPLICATION = "test_app.asgi.application"
+sys.path.append(str(SRC_DIR))
+
+# Database
+# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
+DATABASES = {
+ "default": {
+ "ENGINE": "django.db.backends.sqlite3",
+ "NAME": os.path.join(BASE_DIR, "db.sqlite3"),
+ "TEST": {
+ "NAME": os.path.join(BASE_DIR, "db_test.sqlite3"),
+ },
+ },
+}
+
+# Password validation
+# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
+ },
+ {
+ "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
+ },
+ {
+ "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
+ },
+ {
+ "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
+ },
+]
+
+# Internationalization
+# https://docs.djangoproject.com/en/3.2/topics/i18n/
+LANGUAGE_CODE = "en-us"
+TIME_ZONE = "UTC"
+USE_I18N = True
+USE_L10N = True
+USE_TZ = True
+
+
+# Default primary key field type
+# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
+DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
+STATIC_ROOT = os.path.join(BASE_DIR, "static-deploy")
+
+# Static Files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/3.2/howto/static-files/
+STATIC_URL = "/static/"
+STATICFILES_DIRS = [
+ os.path.join(BASE_DIR, "test_app", "static"),
+]
+STATICFILES_FINDERS = [
+ "django.contrib.staticfiles.finders.FileSystemFinder",
+ "django.contrib.staticfiles.finders.AppDirectoriesFinder",
+]
diff --git a/tests/test_app/static/favicon.ico b/tests/test_app/static/favicon.ico
new file mode 100644
index 00000000..7005d293
Binary files /dev/null and b/tests/test_app/static/favicon.ico differ
diff --git a/tests/test_app/templates/base.html b/tests/test_app/templates/base.html
new file mode 100644
index 00000000..d2a88607
--- /dev/null
+++ b/tests/test_app/templates/base.html
@@ -0,0 +1,21 @@
+{% load static %}
+
+
+
+
+
+
+
+ IDOM
+
+
+
+ IDOM Test Page
+
+
+
+
diff --git a/tests/test_app/tests.py b/tests/test_app/tests.py
new file mode 100644
index 00000000..1a80ee5a
--- /dev/null
+++ b/tests/test_app/tests.py
@@ -0,0 +1,37 @@
+import os
+
+from channels.testing import ChannelsLiveServerTestCase
+from selenium import webdriver
+from selenium.webdriver.support.ui import WebDriverWait
+
+
+class TestIdomCapabilities(ChannelsLiveServerTestCase):
+ def setUp(self):
+ self.driver = make_driver(5, 5)
+ self.driver.get(self.live_server_url)
+
+ def tearDown(self) -> None:
+ self.driver.quit()
+
+ def wait_until(self, condition, timeout=5):
+ WebDriverWait(self.driver, timeout).until(lambda driver: condition())
+
+ def test_hello_world(self):
+ self.driver.find_element_by_id("hello-world")
+
+ def test_counter(self):
+ button = self.driver.find_element_by_id("counter-inc")
+ count = self.driver.find_element_by_id("counter-num")
+
+ for i in range(5):
+ self.wait_until(lambda: count.get_attribute("data-count") == str(i))
+ button.click()
+
+
+def make_driver(page_load_timeout, implicit_wait_timeout):
+ options = webdriver.ChromeOptions()
+ options.headless = bool(int(os.environ.get("SELENIUM_HEADLESS", 0)))
+ driver = webdriver.Chrome(options=options)
+ driver.set_page_load_timeout(page_load_timeout)
+ driver.implicitly_wait(implicit_wait_timeout)
+ return driver
diff --git a/tests/test_app/urls.py b/tests/test_app/urls.py
new file mode 100644
index 00000000..4449e447
--- /dev/null
+++ b/tests/test_app/urls.py
@@ -0,0 +1,25 @@
+"""test_app URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/3.2/topics/http/urls/
+
+Examples:
+
+Function views
+ 1. Add an import: from my_app import views
+ 2. Add a URL to urlpatterns: path('', views.home, name='home')
+
+Class-based views
+ 1. Add an import: from other_app.views import Home
+ 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
+
+Including another URLconf
+ 1. Import the include() function: from django.urls import include, path
+ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
+"""
+from django.urls import path
+
+from .views import base_template
+
+
+urlpatterns = [path("", base_template)]
diff --git a/tests/test_app/views.py b/tests/test_app/views.py
new file mode 100644
index 00000000..a996eb1e
--- /dev/null
+++ b/tests/test_app/views.py
@@ -0,0 +1,34 @@
+import idom
+from django.http import HttpResponse
+from django.template import loader
+
+
+def base_template(request):
+ template = loader.get_template("base.html")
+ context = {}
+ return HttpResponse(template.render(context, request))
+
+
+@idom.component
+def Root():
+ return idom.html.div(HelloWorld(), Counter())
+
+
+@idom.component
+def HelloWorld():
+ return idom.html.h1({"id": "hello-world"}, "Hello World!")
+
+
+@idom.component
+def Counter():
+ count, set_count = idom.hooks.use_state(0)
+ return idom.html.div(
+ idom.html.button(
+ {"id": "counter-inc", "onClick": lambda event: set_count(count + 1)},
+ "Click me!",
+ ),
+ idom.html.p(
+ {"id": "counter-num", "data-count": count},
+ f"Current count is: {count}",
+ ),
+ )