fuzzyworld

dao-game / commit

Using esbuild to create JS bundle, converted game to TS (probably not entirely, but it's a start).

711f34cdddfbb0426b87aa067f627c81d9389491 · Pavel Safronov · 2022-03-04 00:44:17 -0800 · parents 112f91aeb91c

Using esbuild to create JS bundle, converted game to TS (probably not entirely, but it's a start).
 .gitignore               |   2 +
 .vscode/settings.json    |   7 +
 game.js => game.ts       | 157 +++------
 index.html               |  10 +-
 package-lock.json        | 820 +++++++++++++++++++++++++++++++++++++++++++++++
 package.json             |  30 ++
 readme.md                |   4 +
 scripts/esbuild-watch.js |   6 +
 scripts/esbuild.js       |   5 +
 9 files changed, 927 insertions(+), 114 deletions(-)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..41ee2c8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/node_modules/*
+/dist/*
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..768f2c5
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,7 @@
+{
+    "cSpell.words": [
+        "esbuild",
+        "outfile",
+        "Todos"
+    ]
+}
\ No newline at end of file
diff --git a/game.js b/game.ts
similarity index 78%
rename from game.js
rename to game.ts
index 6c292be..119341f 100644
--- a/game.js
+++ b/game.ts
@@ -1,55 +1,36 @@
 class Coord {
-    /**
-     * Creates an instance of Coord.
-     * @param {number} row
-     * @param {number} col
-     * @memberof Coord
-     */
-    constructor(row, col) {
-        /** @type {number} */
+    public row: number;
+    public col: number;
+
+    constructor(row: number, col: number) {
         this.row = row;
-        /** @type {number} */
         this.col = col;
     }
 
-    /**
-     * @param {Coord} coord
-     * @returns {Coord}
-     * @memberof Coord
-     */
-    add(coord) {
+    public add(coord: Coord): Coord {
         return new Coord(this.row + coord.row, this.col + coord.col);
     }
 
-    /**
-     * @param {Coord} coord
-     * @returns {Coord}
-     * @memberof Coord
-     */
-    sub(coord) {
+    sub(coord: Coord): Coord {
         return new Coord(this.row - coord.row, this.col - coord.col);
     }
 
-    /**
-     * @param {Coord} otherCoord
-     * @memberof Coord
-     */
-    equals(otherCoord) {
+    equals(otherCoord: Coord): boolean {
         const areEqual = (this.col === otherCoord.col && this.row === otherCoord.row);
         return areEqual;
     }
 
     // clock face directions
-    static get allDirections() {
+    static get allDirections(): Coord[] {
         const allDirections = [
-            { row: 1, col: 0 }, // up
-            { row: 1, col: 1 }, // up-right
-            { row: 0, col: 1 }, // right
-            { row: -1, col: 1 }, // right-down
-            { row: -1, col: 0 }, // down
-            { row: -1, col: -1 }, // down-left
-            { row: 0, col: -1 }, // left
-            { row: 1, col: -1 }, // left-up
+            new Coord(1, 0), // up
+            new Coord(1, 1), // up-right
+            new Coord(0, 1), // right
+            new Coord(-1, 1), // right-down
+            new Coord(-1, 0), // down
+            new Coord(-1, -1), // down-left
+            new Coord(0, -1), // left
+            new Coord(1, -1), // left-up
         ]
         return allDirections;
     }
@@ -78,19 +59,11 @@ const gameBoard = {
     //     null, 2, 3, null,
     //     6, null, null, 7
     // ],
-    /**
-     * @param {Coord} data
-     * @return {number} 
-     */
-    coordToBoardIndex: ({ row, col }) => {
+    coordToBoardIndex: ({ row, col }: Coord) => {
         const index = row * 4 + col;
         return index;
     },
-    /**
-     * @param {number} index
-     * @return {Coord} 
-     */
-    boardIndexToCoord: (index) => {
+    boardIndexToCoord: (index: number) => {
         const col = index % 4;
         const row = Math.floor(index / 4);
         return new Coord(row, col);
@@ -98,24 +71,18 @@ const gameBoard = {
 }
 
 class Move {
-    /**
-     * Creates an instance of Move.
-     * @param {number} sourceIndex
-     * @param {number} destIndex
-     * @memberof Move
-     */
-    constructor(sourceIndex, destIndex) {
-        /** @type {number} */
+    public sourceIndex: number;
+    public destIndex: number;
+    constructor(sourceIndex: number, destIndex: number) {
         this.sourceIndex = sourceIndex;
-        /** @type {number} */
         this.destIndex = destIndex;
     }
 }
-const player = {
+
+
+const player: { isRedTurn: boolean, selectedPieceId: number | undefined, moveHistory: Move[] } = {
     isRedTurn: false,
-    /** @type {number | undefined} */
     selectedPieceId: undefined,
-    /** @type {Move[]} */
     moveHistory: [],
 }
 
@@ -125,25 +92,23 @@ const dom = {
     defaultBluePiece: document.getElementById('default-blue-piece'),
     cells: () => Array.from(document.querySelectorAll("td")),
     allPieces: () => Array.from(document.querySelectorAll('.piece')),
-    playerPieces: (isRedTurn) => isRedTurn ?
+    playerPieces: (isRedTurn: boolean) => isRedTurn ?
         Array.from(document.querySelectorAll('.red-piece:not(#default-red-piece')) :
         Array.from(document.querySelectorAll('.blue-piece:not(#default-blue-piece')),
     redTurnText: () => Array.from(document.querySelectorAll(".red-turn-text")),
     blueTurnText: () => Array.from(document.querySelectorAll(".blue-turn-text")),
-    resetButton: document.getElementById('reset-button'),
-    undoButton: document.getElementById('undo-button'),
+    resetButton: document.getElementById('reset-button') as HTMLInputElement,
+    undoButton: document.getElementById('undo-button') as HTMLInputElement,
 }
 
-const listenerManager = {
-    /** @type {{element: Element, type: string, listener: (event: Event) => void}[]} */
+const listenerManager: {
+    listeners: { element: Element, type: string, listener: (event: Event) => void }[],
+    addListener: (element: Element, type: string, listener: (event: Event) => void) => void,
+    removeListeners: () => void,
+} = {
     listeners: [],
 
-    /**
-     * @param {Element} element
-     * @param {string} eventName
-     * @param {Event => void} callback
-     */
-    addListener: (element, type, listener) => {
+    addListener: (element: Element, type: string, listener: (event: Event) => void): void => {
         const { listeners } = listenerManager;
         element.addEventListener(type, listener);
         listeners.push({ element, type, listener });
@@ -179,14 +144,10 @@ const resetBoard = () => {
     listenerManager.removeListeners();
 };
 
-/**
- * @param {number} sourceIndex
- * @param {number} destIndex
- */
-const movePiece = async (sourceIndex, destIndex) => {
+const movePiece = async (sourceIndex: number, destIndex: number) => {
     const pieceId = gameBoard.board[sourceIndex];
     // find current piece
-    const piece = document.getElementById(pieceId);
+    const piece = document.getElementById(pieceId.toString());
     // unselect piece
     piece.classList.remove(constants.selectedPieceClassName);
 
@@ -219,21 +180,9 @@ const movePiece = async (sourceIndex, destIndex) => {
 
 const nextMove = () => {
     const setActivePlayerPieces = () => {
-        /** @param {Event} event */
-        const selectPiece = (event) => {
-            /**
-             * @param {number} pieceId
-             * @returns {number[]}
-             */
-            const getAllowedMoveIndicesForIndex = (boardIndex) => {
-                /**
-                 *
-                 *
-                 * @param {Coord} startCoord
-                 * @param {Coord} direction
-                 * @returns {Coord}
-                 */
-                const getFarthestCoord = (startCoord, direction) => {
+        const selectPiece = (event: Event) => {
+            const getAllowedMoveIndicesForIndex = (boardIndex: number): number[] => {
+                const getFarthestCoord = (startCoord: Coord, direction: Coord): Coord => {
                     let farthestPos = undefined;
                     let testPos = startCoord;
                     while (true) {
@@ -265,7 +214,7 @@ const nextMove = () => {
 
             const previousSelectedPieceId = player.selectedPieceId;
             if (previousSelectedPieceId !== undefined) {
-                const previousPiece = document.getElementById(previousSelectedPieceId);
+                const previousPiece = document.getElementById(previousSelectedPieceId.toString());
                 previousPiece.classList.remove(constants.selectedPieceClassName);
                 dom.cells().forEach(cell => {
                     cell.classList.remove(constants.blinkingClassName);
@@ -273,8 +222,7 @@ const nextMove = () => {
             }
 
             // read event data
-            /** @type {Element} */
-            const piece = event.target;
+            const piece = event.target as Element;
             const pieceId = parseInt(piece.id);
 
             // set as selected
@@ -344,16 +292,8 @@ const nextMove = () => {
 };
 
 const checkForWin = () => {
-    /**
-     * @param {Element[]} elements
-     * @return {Coord[]} 
-     */
-    const elementsToCoords = (elements) => {
-        /**
-         * @param {Element[]} elements
-         * @returns {number[]}
-         */
-        const elementsToBoardIndexes = (elements) => {
+    const elementsToCoords = (elements: Element[]): Coord[] => {
+        const elementsToBoardIndexes = (elements: Element[]): number[] => {
             const indices = elements.map(pp => {
                 const pieceId = Number.parseInt(pp.id);
                 const pieceIndex = gameBoard.board.indexOf(pieceId);
@@ -366,11 +306,7 @@ const checkForWin = () => {
         const coords = indices.map(pieceIndex => gameBoard.boardIndexToCoord(pieceIndex));
         return coords;
     };
-    /**
-     * @param {Coord[]} coords
-     * @returns {boolean}
-     */
-    const doCoordsFormSquare = (coords) => {
+    const doCoordsFormSquare = (coords: Coord[]): boolean => {
         const allRows = coords.map(c => c.row);
         const allCols = coords.map(c => c.col);
         const min = new Coord(Math.min(...allRows), Math.min(...allCols));
@@ -382,8 +318,7 @@ const checkForWin = () => {
 
     const playerCoords = elementsToCoords(dom.playerPieces(player.isRedTurn))
     const otherCoords = elementsToCoords(dom.playerPieces(!player.isRedTurn));
-    /** @type {Coord[]} */
-    const squareCoords = [
+    const squareCoords: Coord[] = [
         new Coord(0, 0),
         new Coord(3, 0),
         new Coord(3, 3),
@@ -444,8 +379,8 @@ const init = () => {
         if (value !== null) {
             const isRed = value < 4;
             const defaultPiece = isRed ? dom.defaultRedPiece : dom.defaultBluePiece;
-            const newPiece = defaultPiece.cloneNode(true);
-            newPiece.id = value;
+            const newPiece = defaultPiece.cloneNode(true) as HTMLParagraphElement;
+            newPiece.id = value.toString();
             dom.cells()[index].appendChild(newPiece);
         }
     });
diff --git a/index.html b/index.html
index e7285c8..8ab1737 100644
--- a/index.html
+++ b/index.html
@@ -7,7 +7,7 @@
         <meta name="viewport" content="width=device-width, initial-scale=1">
         <link rel="icon" href="/yin-yang.svg" type="image/svg+xml">
         <link rel="stylesheet" href="styles.css" />
-        <script defer type="module" src="game.js"></script>
+        <script defer src="dist/out.js"></script>
     </head>
 
     <body>
@@ -44,7 +44,8 @@
             <p>
                 Board game <a
                     href='https://en.wikipedia.org/wiki/Dao_(game)'
-                    target='_blank'>Dao</a>.
+                    target='_blank'
+                    rel="noopener">Dao</a>.
             </p>
         </div>
         <div class="info">
@@ -64,7 +65,10 @@
         <div class="info">
             <p>
                 Here is the
-                <a href='https://gitlab.com/fuzzy-rp/dao-game' target="_blank">source
+                <a
+                    href='https://gitlab.com/fuzzy-rp/dao-game'
+                    target="_blank"
+                    rel="noopener">source
                     code</a>
             </p>
         </div>
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..92f77e9
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,820 @@
+{
+  "name": "dao-game",
+  "version": "1.0.0",
+  "lockfileVersion": 2,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "dao-game",
+      "version": "1.0.0",
+      "license": "ISC",
+      "dependencies": {
+        "esbuild": "^0.14.25",
+        "react": "^17.0.2",
+        "react-dom": "^17.0.2"
+      },
+      "devDependencies": {
+        "rimraf": "^3.0.2"
+      }
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+      "dev": true
+    },
+    "node_modules/brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+      "dev": true
+    },
+    "node_modules/esbuild": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.25.tgz",
+      "integrity": "sha512-4JHEIOMNFvK09ziiL+iVmldIhLbn49V4NAVo888tcGFKedEZY/Y8YapfStJ6zSE23tzYPKxqKwQBnQoIO0BI/Q==",
+      "hasInstallScript": true,
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "optionalDependencies": {
+        "esbuild-android-64": "0.14.25",
+        "esbuild-android-arm64": "0.14.25",
+        "esbuild-darwin-64": "0.14.25",
+        "esbuild-darwin-arm64": "0.14.25",
+        "esbuild-freebsd-64": "0.14.25",
+        "esbuild-freebsd-arm64": "0.14.25",
+        "esbuild-linux-32": "0.14.25",
+        "esbuild-linux-64": "0.14.25",
+        "esbuild-linux-arm": "0.14.25",
+        "esbuild-linux-arm64": "0.14.25",
+        "esbuild-linux-mips64le": "0.14.25",
+        "esbuild-linux-ppc64le": "0.14.25",
+        "esbuild-linux-riscv64": "0.14.25",
+        "esbuild-linux-s390x": "0.14.25",
+        "esbuild-netbsd-64": "0.14.25",
+        "esbuild-openbsd-64": "0.14.25",
+        "esbuild-sunos-64": "0.14.25",
+        "esbuild-windows-32": "0.14.25",
+        "esbuild-windows-64": "0.14.25",
+        "esbuild-windows-arm64": "0.14.25"
+      }
+    },
+    "node_modules/esbuild-android-64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.25.tgz",
+      "integrity": "sha512-L5vCUk7TzFbBnoESNoXjU3x9+/+7TDIE/1mTfy/erAfvZAqC+S3sp/Qa9wkypFMcFvN9FzvESkTlpeQDolREtQ==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-android-arm64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.25.tgz",
+      "integrity": "sha512-4jv5xPjM/qNm27T5j3ZEck0PvjgQtoMHnz4FzwF5zNP56PvY2CT0WStcAIl6jNlsuDdN63rk2HRBIsO6xFbcFw==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-darwin-64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.25.tgz",
+      "integrity": "sha512-TGp8tuudIxOyWd1+8aYPxQmC1ZQyvij/AfNBa35RubixD0zJ1vkKHVAzo0Zao1zcG6pNqiSyzfPto8vmg0s7oA==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-darwin-arm64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.25.tgz",
+      "integrity": "sha512-oTcDgdm0MDVEmw2DWu8BV68pYuImpFgvWREPErBZmNA4MYKGuBRaCiJqq6jZmBR1x+3y1DWCjez+5uLtuAm6mw==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-freebsd-64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.25.tgz",
+      "integrity": "sha512-ueAqbnMZ8arnuLH8tHwTCQYeptnHOUV7vA6px6j4zjjQwDx7TdP7kACPf3TLZLdJQ3CAD1XCvQ2sPhX+8tacvQ==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-freebsd-arm64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.25.tgz",
+      "integrity": "sha512-+ZVWud2HKh+Ob6k/qiJWjBtUg4KmJGGmbvEXXW1SNKS7hW7HU+Zq2ZCcE1akFxOPkVB+EhOty/sSek30tkCYug==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-32": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.25.tgz",
+      "integrity": "sha512-3OP/lwV3kCzEz45tobH9nj+uE4ubhGsfx+tn0L26WAGtUbmmcRpqy7XRG/qK7h1mClZ+eguIANcQntYMdYklfw==",
+      "cpu": [
+        "ia32"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.25.tgz",
+      "integrity": "sha512-+aKHdHZmX9qwVlQmu5xYXh7GsBFf4TWrePgeJTalhXHOG7NNuUwoHmketGiZEoNsWyyqwH9rE5BC+iwcLY30Ug==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-arm": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.25.tgz",
+      "integrity": "sha512-aTLcE2VBoLydL943REcAcgnDi3bHtmULSXWLbjtBdtykRatJVSxKMjK9YlBXUZC4/YcNQfH7AxwVeQr9fNxPhw==",
+      "cpu": [
+        "arm"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-arm64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.25.tgz",
+      "integrity": "sha512-UxfenPx/wSZx55gScCImPtXekvZQLI2GW3qe5dtlmU7luiqhp5GWPzGeQEbD3yN3xg/pHc671m5bma5Ns7lBHw==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-mips64le": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.25.tgz",
+      "integrity": "sha512-wLWYyqVfYx9Ur6eU5RT92yJVsaBGi5RdkoWqRHOqcJ38Kn60QMlcghsKeWfe9jcYut8LangYZ98xO1LxIoSXrQ==",
+      "cpu": [
+        "mips64el"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-ppc64le": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.25.tgz",
+      "integrity": "sha512-0dR6Csl6Zas3g4p9ULckEl8Mo8IInJh33VCJ3eaV1hj9+MHGdmDOakYMN8MZP9/5nl+NU/0ygpd14cWgy8uqRw==",
+      "cpu": [
+        "ppc64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-riscv64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.25.tgz",
+      "integrity": "sha512-J4d20HDmTrgvhR0bdkDhvvJGaikH3LzXQnNaseo8rcw9Yqby9A90gKUmWpfwqLVNRILvNnAmKLfBjCKU9ajg8w==",
+      "cpu": [
+        "riscv64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-s390x": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.25.tgz",
+      "integrity": "sha512-YI2d5V6nTE73ZnhEKQD7MtsPs1EtUZJ3obS21oxQxGbbRw1G+PtJKjNyur+3t6nzHP9oTg6GHQ3S3hOLLmbDIQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-netbsd-64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.25.tgz",
+      "integrity": "sha512-TKIVgNWLUOkr+Exrye70XTEE1lJjdQXdM4tAXRzfHE9iBA7LXWcNtVIuSnphTqpanPzTDFarF0yqq4kpbC6miA==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-openbsd-64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.25.tgz",
+      "integrity": "sha512-QgFJ37A15D7NIXBTYEqz29+uw3nNBOIyog+3kFidANn6kjw0GHZ0lEYQn+cwjyzu94WobR+fes7cTl/ZYlHb1A==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-sunos-64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.25.tgz",
+      "integrity": "sha512-rmWfjUItYIVlqr5EnTH1+GCxXiBOC42WBZ3w++qh7n2cS9Xo0lO5pGSG2N+huOU2fX5L+6YUuJ78/vOYvefeFw==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-windows-32": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.25.tgz",
+      "integrity": "sha512-HGAxVUofl3iUIz9W10Y9XKtD0bNsK9fBXv1D55N/ljNvkrAYcGB8YCm0v7DjlwtyS6ws3dkdQyXadbxkbzaKOA==",
+      "cpu": [
+        "ia32"
+      ],
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-windows-64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.25.tgz",
+      "integrity": "sha512-TirEohRkfWU9hXLgoDxzhMQD1g8I2mOqvdQF2RS9E/wbkORTAqJHyh7wqGRCQAwNzdNXdg3JAyhQ9/177AadWA==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-windows-arm64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.25.tgz",
+      "integrity": "sha512-4ype9ERiI45rSh+R8qUoBtaj6kJvUOI7oVLhKqPEpcF4Pa5PpT3hm/mXAyotJHREkHpM87PAJcA442mLnbtlNA==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+      "dev": true
+    },
+    "node_modules/glob": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
+      "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
+      "dev": true,
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.0.4",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+      "dev": true,
+      "dependencies": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "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/js-tokens": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+    },
+    "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==",
+      "dependencies": {
+        "js-tokens": "^3.0.0 || ^4.0.0"
+      },
+      "bin": {
+        "loose-envify": "cli.js"
+      }
+    },
+    "node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "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=",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+      "dev": true,
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
+    "node_modules/path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/react": {
+      "version": "17.0.2",
+      "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
+      "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==",
+      "dependencies": {
+        "loose-envify": "^1.1.0",
+        "object-assign": "^4.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/react-dom": {
+      "version": "17.0.2",
+      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
+      "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==",
+      "dependencies": {
+        "loose-envify": "^1.1.0",
+        "object-assign": "^4.1.1",
+        "scheduler": "^0.20.2"
+      },
+      "peerDependencies": {
+        "react": "17.0.2"
+      }
+    },
+    "node_modules/rimraf": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+      "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+      "dev": true,
+      "dependencies": {
+        "glob": "^7.1.3"
+      },
+      "bin": {
+        "rimraf": "bin.js"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/scheduler": {
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz",
+      "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==",
+      "dependencies": {
+        "loose-envify": "^1.1.0",
+        "object-assign": "^4.1.1"
+      }
+    },
+    "node_modules/wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+      "dev": true
+    }
+  },
+  "dependencies": {
+    "balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+      "dev": true
+    },
+    "brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "requires": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+      "dev": true
+    },
+    "esbuild": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.25.tgz",
+      "integrity": "sha512-4JHEIOMNFvK09ziiL+iVmldIhLbn49V4NAVo888tcGFKedEZY/Y8YapfStJ6zSE23tzYPKxqKwQBnQoIO0BI/Q==",
+      "requires": {
+        "esbuild-android-64": "0.14.25",
+        "esbuild-android-arm64": "0.14.25",
+        "esbuild-darwin-64": "0.14.25",
+        "esbuild-darwin-arm64": "0.14.25",
+        "esbuild-freebsd-64": "0.14.25",
+        "esbuild-freebsd-arm64": "0.14.25",
+        "esbuild-linux-32": "0.14.25",
+        "esbuild-linux-64": "0.14.25",
+        "esbuild-linux-arm": "0.14.25",
+        "esbuild-linux-arm64": "0.14.25",
+        "esbuild-linux-mips64le": "0.14.25",
+        "esbuild-linux-ppc64le": "0.14.25",
+        "esbuild-linux-riscv64": "0.14.25",
+        "esbuild-linux-s390x": "0.14.25",
+        "esbuild-netbsd-64": "0.14.25",
+        "esbuild-openbsd-64": "0.14.25",
+        "esbuild-sunos-64": "0.14.25",
+        "esbuild-windows-32": "0.14.25",
+        "esbuild-windows-64": "0.14.25",
+        "esbuild-windows-arm64": "0.14.25"
+      }
+    },
+    "esbuild-android-64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.25.tgz",
+      "integrity": "sha512-L5vCUk7TzFbBnoESNoXjU3x9+/+7TDIE/1mTfy/erAfvZAqC+S3sp/Qa9wkypFMcFvN9FzvESkTlpeQDolREtQ==",
+      "optional": true
+    },
+    "esbuild-android-arm64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.25.tgz",
+      "integrity": "sha512-4jv5xPjM/qNm27T5j3ZEck0PvjgQtoMHnz4FzwF5zNP56PvY2CT0WStcAIl6jNlsuDdN63rk2HRBIsO6xFbcFw==",
+      "optional": true
+    },
+    "esbuild-darwin-64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.25.tgz",
+      "integrity": "sha512-TGp8tuudIxOyWd1+8aYPxQmC1ZQyvij/AfNBa35RubixD0zJ1vkKHVAzo0Zao1zcG6pNqiSyzfPto8vmg0s7oA==",
+      "optional": true
+    },
+    "esbuild-darwin-arm64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.25.tgz",
+      "integrity": "sha512-oTcDgdm0MDVEmw2DWu8BV68pYuImpFgvWREPErBZmNA4MYKGuBRaCiJqq6jZmBR1x+3y1DWCjez+5uLtuAm6mw==",
+      "optional": true
+    },
+    "esbuild-freebsd-64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.25.tgz",
+      "integrity": "sha512-ueAqbnMZ8arnuLH8tHwTCQYeptnHOUV7vA6px6j4zjjQwDx7TdP7kACPf3TLZLdJQ3CAD1XCvQ2sPhX+8tacvQ==",
+      "optional": true
+    },
+    "esbuild-freebsd-arm64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.25.tgz",
+      "integrity": "sha512-+ZVWud2HKh+Ob6k/qiJWjBtUg4KmJGGmbvEXXW1SNKS7hW7HU+Zq2ZCcE1akFxOPkVB+EhOty/sSek30tkCYug==",
+      "optional": true
+    },
+    "esbuild-linux-32": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.25.tgz",
+      "integrity": "sha512-3OP/lwV3kCzEz45tobH9nj+uE4ubhGsfx+tn0L26WAGtUbmmcRpqy7XRG/qK7h1mClZ+eguIANcQntYMdYklfw==",
+      "optional": true
+    },
+    "esbuild-linux-64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.25.tgz",
+      "integrity": "sha512-+aKHdHZmX9qwVlQmu5xYXh7GsBFf4TWrePgeJTalhXHOG7NNuUwoHmketGiZEoNsWyyqwH9rE5BC+iwcLY30Ug==",
+      "optional": true
+    },
+    "esbuild-linux-arm": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.25.tgz",
+      "integrity": "sha512-aTLcE2VBoLydL943REcAcgnDi3bHtmULSXWLbjtBdtykRatJVSxKMjK9YlBXUZC4/YcNQfH7AxwVeQr9fNxPhw==",
+      "optional": true
+    },
+    "esbuild-linux-arm64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.25.tgz",
+      "integrity": "sha512-UxfenPx/wSZx55gScCImPtXekvZQLI2GW3qe5dtlmU7luiqhp5GWPzGeQEbD3yN3xg/pHc671m5bma5Ns7lBHw==",
+      "optional": true
+    },
+    "esbuild-linux-mips64le": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.25.tgz",
+      "integrity": "sha512-wLWYyqVfYx9Ur6eU5RT92yJVsaBGi5RdkoWqRHOqcJ38Kn60QMlcghsKeWfe9jcYut8LangYZ98xO1LxIoSXrQ==",
+      "optional": true
+    },
+    "esbuild-linux-ppc64le": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.25.tgz",
+      "integrity": "sha512-0dR6Csl6Zas3g4p9ULckEl8Mo8IInJh33VCJ3eaV1hj9+MHGdmDOakYMN8MZP9/5nl+NU/0ygpd14cWgy8uqRw==",
+      "optional": true
+    },
+    "esbuild-linux-riscv64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.25.tgz",
+      "integrity": "sha512-J4d20HDmTrgvhR0bdkDhvvJGaikH3LzXQnNaseo8rcw9Yqby9A90gKUmWpfwqLVNRILvNnAmKLfBjCKU9ajg8w==",
+      "optional": true
+    },
+    "esbuild-linux-s390x": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.25.tgz",
+      "integrity": "sha512-YI2d5V6nTE73ZnhEKQD7MtsPs1EtUZJ3obS21oxQxGbbRw1G+PtJKjNyur+3t6nzHP9oTg6GHQ3S3hOLLmbDIQ==",
+      "optional": true
+    },
+    "esbuild-netbsd-64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.25.tgz",
+      "integrity": "sha512-TKIVgNWLUOkr+Exrye70XTEE1lJjdQXdM4tAXRzfHE9iBA7LXWcNtVIuSnphTqpanPzTDFarF0yqq4kpbC6miA==",
+      "optional": true
+    },
+    "esbuild-openbsd-64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.25.tgz",
+      "integrity": "sha512-QgFJ37A15D7NIXBTYEqz29+uw3nNBOIyog+3kFidANn6kjw0GHZ0lEYQn+cwjyzu94WobR+fes7cTl/ZYlHb1A==",
+      "optional": true
+    },
+    "esbuild-sunos-64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.25.tgz",
+      "integrity": "sha512-rmWfjUItYIVlqr5EnTH1+GCxXiBOC42WBZ3w++qh7n2cS9Xo0lO5pGSG2N+huOU2fX5L+6YUuJ78/vOYvefeFw==",
+      "optional": true
+    },
+    "esbuild-windows-32": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.25.tgz",
+      "integrity": "sha512-HGAxVUofl3iUIz9W10Y9XKtD0bNsK9fBXv1D55N/ljNvkrAYcGB8YCm0v7DjlwtyS6ws3dkdQyXadbxkbzaKOA==",
+      "optional": true
+    },
+    "esbuild-windows-64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.25.tgz",
+      "integrity": "sha512-TirEohRkfWU9hXLgoDxzhMQD1g8I2mOqvdQF2RS9E/wbkORTAqJHyh7wqGRCQAwNzdNXdg3JAyhQ9/177AadWA==",
+      "optional": true
+    },
+    "esbuild-windows-arm64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.25.tgz",
+      "integrity": "sha512-4ype9ERiI45rSh+R8qUoBtaj6kJvUOI7oVLhKqPEpcF4Pa5PpT3hm/mXAyotJHREkHpM87PAJcA442mLnbtlNA==",
+      "optional": true
+    },
+    "fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+      "dev": true
+    },
+    "glob": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
+      "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
+      "dev": true,
+      "requires": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.0.4",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      }
+    },
+    "inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+      "dev": true,
+      "requires": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "dev": true
+    },
+    "js-tokens": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+    },
+    "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==",
+      "requires": {
+        "js-tokens": "^3.0.0 || ^4.0.0"
+      }
+    },
+    "minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "requires": {
+        "brace-expansion": "^1.1.7"
+      }
+    },
+    "object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
+    },
+    "once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+      "dev": true,
+      "requires": {
+        "wrappy": "1"
+      }
+    },
+    "path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+      "dev": true
+    },
+    "react": {
+      "version": "17.0.2",
+      "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
+      "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==",
+      "requires": {
+        "loose-envify": "^1.1.0",
+        "object-assign": "^4.1.1"
+      }
+    },
+    "react-dom": {
+      "version": "17.0.2",
+      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
+      "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==",
+      "requires": {
+        "loose-envify": "^1.1.0",
+        "object-assign": "^4.1.1",
+        "scheduler": "^0.20.2"
+      }
+    },
+    "rimraf": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+      "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+      "dev": true,
+      "requires": {
+        "glob": "^7.1.3"
+      }
+    },
+    "scheduler": {
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz",
+      "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==",
+      "requires": {
+        "loose-envify": "^1.1.0",
+        "object-assign": "^4.1.1"
+      }
+    },
+    "wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+      "dev": true
+    }
+  }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..31e9984
--- /dev/null
+++ b/package.json
@@ -0,0 +1,30 @@
+{
+  "name": "dao-game",
+  "version": "1.0.0",
+  "description": "Dao (game)",
+  "main": "app.js",
+  "scripts": {
+    "clean": "rimraf dist",
+    "build": "node scripts/esbuild.js",
+    "build-watch": "node scripts/esbuild-watch.js",
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://gitlab.com/fuzzy-rp/dao-game.git"
+  },
+  "author": "",
+  "license": "ISC",
+  "bugs": {
+    "url": "https://gitlab.com/fuzzy-rp/dao-game/issues"
+  },
+  "homepage": "https://gitlab.com/fuzzy-rp/dao-game#readme",
+  "dependencies": {
+    "esbuild": "^0.14.25",
+    "react": "^17.0.2",
+    "react-dom": "^17.0.2"
+  },
+  "devDependencies": {
+    "rimraf": "^3.0.2"
+  }
+}
\ No newline at end of file
diff --git a/readme.md b/readme.md
index ef00c5b..260086c 100644
--- a/readme.md
+++ b/readme.md
@@ -10,6 +10,10 @@
 
 [Location](https://fuzzy-rp.gitlab.io/dao-game/)
 
+## Esbuild
+
+This project is using [esbuild](https://esbuild.github.io/getting-started/#build-scripts) for creating a bundle.
+
 ## Todos
 
 1. Use esbuild and rewrite app in TypeScript
diff --git a/scripts/esbuild-watch.js b/scripts/esbuild-watch.js
new file mode 100644
index 0000000..e60082f
--- /dev/null
+++ b/scripts/esbuild-watch.js
@@ -0,0 +1,6 @@
+require('esbuild').build({
+    entryPoints: ['game.ts'],
+    bundle: true,
+    outfile: 'dist/out.js',
+    watch: true
+}).catch(() => process.exit(1))
\ No newline at end of file
diff --git a/scripts/esbuild.js b/scripts/esbuild.js
new file mode 100644
index 0000000..d1ae731
--- /dev/null
+++ b/scripts/esbuild.js
@@ -0,0 +1,5 @@
+require('esbuild').build({
+    entryPoints: ['game.ts'],
+    bundle: true,
+    outfile: 'dist/out.js'
+}).catch(() => process.exit(1))
\ No newline at end of file