summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2022-12-06 09:21:17 -0300
committerSebastian <sebasjm@gmail.com>2022-12-06 09:21:17 -0300
commite382b022030db96b8282337b304ec5e599a5f405 (patch)
tree6ba9afb72a1b3e5ce35f37a987d74286b0e45c92
parent219e48f35196e54361dc17d6ca7de6e095c46eeb (diff)
downloadwallet-core-e382b022030db96b8282337b304ec5e599a5f405.tar.gz
wallet-core-e382b022030db96b8282337b304ec5e599a5f405.tar.bz2
wallet-core-e382b022030db96b8282337b304ec5e599a5f405.zip
web-util: utils for developing webapps
-rw-r--r--packages/web-util/README0
-rwxr-xr-xpackages/web-util/bin/taler-web-cli.mjs19
-rwxr-xr-xpackages/web-util/build.mjs103
-rw-r--r--packages/web-util/create_certificate.sh48
-rw-r--r--packages/web-util/package.json40
-rw-r--r--packages/web-util/src/cli.ts59
-rw-r--r--packages/web-util/src/custom.d.ts12
-rw-r--r--packages/web-util/src/index.browser.ts38
-rw-r--r--packages/web-util/src/index.node.ts3
-rw-r--r--packages/web-util/src/index.ts4
-rw-r--r--packages/web-util/src/keys/ca.crt14
-rw-r--r--packages/web-util/src/keys/ca.key16
-rw-r--r--packages/web-util/src/keys/ca.srl1
-rw-r--r--packages/web-util/src/keys/localhost.crt15
-rw-r--r--packages/web-util/src/keys/localhost.csr10
-rw-r--r--packages/web-util/src/keys/localhost.key16
-rw-r--r--packages/web-util/src/live-reload.ts52
-rw-r--r--packages/web-util/src/serve.ts108
-rw-r--r--packages/web-util/src/stories.html17
-rw-r--r--packages/web-util/src/stories.tsx580
-rw-r--r--packages/web-util/tsconfig.json34
21 files changed, 1189 insertions, 0 deletions
diff --git a/packages/web-util/README b/packages/web-util/README
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/packages/web-util/README
diff --git a/packages/web-util/bin/taler-web-cli.mjs b/packages/web-util/bin/taler-web-cli.mjs
new file mode 100755
index 000000000..4e89cf46d
--- /dev/null
+++ b/packages/web-util/bin/taler-web-cli.mjs
@@ -0,0 +1,19 @@
+#!/usr/bin/env node
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { main } from '../lib/cli.cjs';
+main();
diff --git a/packages/web-util/build.mjs b/packages/web-util/build.mjs
new file mode 100755
index 000000000..ba277b666
--- /dev/null
+++ b/packages/web-util/build.mjs
@@ -0,0 +1,103 @@
+#!/usr/bin/env node
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import esbuild from 'esbuild'
+import path from "path"
+import fs from "fs"
+
+// eslint-disable-next-line no-undef
+const BASE = process.cwd()
+
+let GIT_ROOT = BASE
+while (!fs.existsSync(path.join(GIT_ROOT, '.git')) && GIT_ROOT !== '/') {
+ GIT_ROOT = path.join(GIT_ROOT, '../')
+}
+if (GIT_ROOT === '/') {
+ // eslint-disable-next-line no-undef
+ console.log("not found")
+ // eslint-disable-next-line no-undef
+ process.exit(1);
+}
+const GIT_HASH = GIT_ROOT === '/' ? undefined : git_hash()
+
+
+let _package = JSON.parse(fs.readFileSync(path.join(BASE, 'package.json')));
+
+function git_hash() {
+ const rev = fs.readFileSync(path.join(GIT_ROOT, '.git', 'HEAD')).toString().trim().split(/.*[: ]/).slice(-1)[0];
+ if (rev.indexOf('/') === -1) {
+ return rev;
+ } else {
+ return fs.readFileSync(path.join(GIT_ROOT, '.git', rev)).toString().trim();
+ }
+}
+
+const buildConfigBase = {
+ outdir: "lib",
+ bundle: true,
+ minify: false,
+ target: [
+ 'es6'
+ ],
+ loader: {
+ '.key': 'text',
+ '.crt': 'text',
+ '.html': 'text',
+ },
+ sourcemap: true,
+ define: {
+ '__VERSION__': `"${_package.version}"`,
+ '__GIT_HASH__': `"${GIT_HASH}"`,
+ },
+}
+
+const buildConfigNode = {
+ ...buildConfigBase,
+ entryPoints: ["src/index.node.ts", "src/cli.ts"],
+ outExtension: {
+ '.js': '.cjs'
+ },
+ format: 'cjs',
+ platform: 'node',
+ external: ["preact"],
+};
+
+const buildConfigBrowser = {
+ ...buildConfigBase,
+ entryPoints: ["src/index.browser.ts", "src/live-reload.ts", 'src/stories.tsx'],
+ outExtension: {
+ '.js': '.mjs'
+ },
+ format: 'esm',
+ platform: 'browser',
+ external: ["preact", "@gnu-taler/taler-util", "jed"],
+ jsxFactory: 'h',
+ jsxFragment: 'Fragment',
+};
+
+[buildConfigNode, buildConfigBrowser].forEach((config) => {
+ esbuild
+ .build(config)
+ .catch((e) => {
+ // eslint-disable-next-line no-undef
+ console.log(e)
+ // eslint-disable-next-line no-undef
+ process.exit(1)
+ });
+
+})
+
diff --git a/packages/web-util/create_certificate.sh b/packages/web-util/create_certificate.sh
new file mode 100644
index 000000000..980aaf642
--- /dev/null
+++ b/packages/web-util/create_certificate.sh
@@ -0,0 +1,48 @@
+#!/usr/bin/env bash
+set -eu
+org=localhost-ca
+domain=localhost
+
+rm -rf keys
+mkdir keys
+cd keys
+
+openssl genpkey -algorithm RSA -out ca.key
+openssl req -x509 -key ca.key -out ca.crt \
+ -subj "/CN=$org/O=$org"
+
+openssl genpkey -algorithm RSA -out "$domain".key
+openssl req -new -key "$domain".key -out "$domain".csr \
+ -subj "/CN=$domain/O=$org"
+
+openssl x509 -req -in "$domain".csr -days 365 -out "$domain".crt \
+ -CA ca.crt -CAkey ca.key -CAcreateserial \
+ -extfile <(cat <<END
+basicConstraints = CA:FALSE
+subjectKeyIdentifier = hash
+authorityKeyIdentifier = keyid,issuer
+subjectAltName = DNS:$domain
+END
+ )
+
+sudo cp ca.crt /usr/local/share/ca-certificates/testing.crt
+sudo update-ca-certificates
+
+
+echo '
+## Chrome
+1. go to chrome://settings/certificates
+2. tab "authorities"
+3. button "import"
+4. choose "ca.crt"
+5. trust for identify websites
+
+## Firefox
+1. go to about:preferences#privacy
+2. button "view certificates"
+3. button "import"
+4. choose "ca.crt"
+5. trust for identify websites
+'
+
+echo done!
diff --git a/packages/web-util/package.json b/packages/web-util/package.json
new file mode 100644
index 000000000..969215f9f
--- /dev/null
+++ b/packages/web-util/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "@gnu-taler/web-util",
+ "version": "0.9.0",
+ "description": "Generic helper functionality for GNU Taler Web Apps",
+ "type": "module",
+ "types": "./lib/index.node.d.ts",
+ "main": "./dist/taler-web-cli.cjs",
+ "bin": {
+ "taler-wallet-cli": "./bin/taler-web-cli.cjs"
+ },
+ "author": "Sebastian Marchano",
+ "license": "AGPL-3.0-or-later",
+ "private": false,
+ "exports": {
+ "./lib/index.browser": "./lib/index.browser.mjs",
+ "./lib/index.node": "./lib/index.node.cjs"
+ },
+ "scripts": {
+ "prepare": "tsc",
+ "compile": "./build.mjs",
+ "clean": "rimraf dist lib tsconfig.tsbuildinfo",
+ "pretty": "prettier --write src"
+ },
+ "devDependencies": {
+ "@gnu-taler/taler-util": "workspace:*",
+ "@types/express": "^4.17.14",
+ "@types/node": "^18.11.9",
+ "@types/web": "^0.0.82",
+ "@types/ws": "^8.5.3",
+ "chokidar": "^3.5.3",
+ "esbuild": "^0.14.21",
+ "express": "^4.18.2",
+ "preact": "10.11.3",
+ "prettier": "^2.5.1",
+ "rimraf": "^3.0.2",
+ "tslib": "^2.4.0",
+ "typescript": "^4.8.4",
+ "ws": "7.4.5"
+ }
+}
diff --git a/packages/web-util/src/cli.ts b/packages/web-util/src/cli.ts
new file mode 100644
index 000000000..59cfe9989
--- /dev/null
+++ b/packages/web-util/src/cli.ts
@@ -0,0 +1,59 @@
+
+import {
+ clk, setGlobalLogLevelFromString
+} from "@gnu-taler/taler-util";
+import { serve } from "./serve.js";
+
+
+export const walletCli = clk
+ .program("wallet", {
+ help: "Command line interface for the GNU Taler wallet.",
+ })
+ .maybeOption("log", ["-L", "--log"], clk.STRING, {
+ help: "configure log level (NONE, ..., TRACE)",
+ onPresentHandler: (x) => {
+ setGlobalLogLevelFromString(x);
+ },
+ })
+ .flag("version", ["-v", "--version"], {
+ onPresentHandler: printVersion,
+ })
+ .flag("verbose", ["-V", "--verbose"], {
+ help: "Enable verbose output.",
+ })
+
+walletCli
+ .subcommand("serve", "serve", { help: "Create a server." })
+ .maybeOption("folder", ["-F", "--folder"], clk.STRING, {
+ help: "should complete",
+ // default: "./dist"
+ })
+ .maybeOption("port", ["-P", "--port"], clk.INT, {
+ help: "should complete",
+ // default: 8000
+ })
+ .flag("development", ["-D", "--dev"], {
+ help: "should complete",
+ })
+ .action(async (args) => {
+ return serve({
+ folder: args.serve.folder || "./dist",
+ port: args.serve.port || 8000,
+ development: args.serve.development
+ })
+ }
+ );
+
+
+
+declare const __VERSION__: string;
+function printVersion(): void {
+ console.log(__VERSION__);
+ process.exit(0);
+}
+
+export function main(): void {
+ walletCli.run();
+}
+
+
diff --git a/packages/web-util/src/custom.d.ts b/packages/web-util/src/custom.d.ts
new file mode 100644
index 000000000..6049ac6a9
--- /dev/null
+++ b/packages/web-util/src/custom.d.ts
@@ -0,0 +1,12 @@
+declare module "*.crt" {
+ const content: string;
+ export default content;
+}
+declare module "*.key" {
+ const content: string;
+ export default content;
+}
+declare module "*.html" {
+ const content: string;
+ export default content;
+}
diff --git a/packages/web-util/src/index.browser.ts b/packages/web-util/src/index.browser.ts
new file mode 100644
index 000000000..514a2ec42
--- /dev/null
+++ b/packages/web-util/src/index.browser.ts
@@ -0,0 +1,38 @@
+
+//`ws://localhost:8003/socket`
+export function setupLiveReload(wsURL: string | undefined) {
+ if (!wsURL) return;
+ const ws = new WebSocket(wsURL);
+ ws.addEventListener('message', (message) => {
+ const event = JSON.parse(message.data);
+ if (event.type === "LOG") {
+ console.log(event.message);
+ }
+ if (event.type === "RELOAD") {
+ window.location.reload();
+ }
+ if (event.type === "UPDATE") {
+ const c = document.getElementById("container")
+ if (c) {
+ document.body.removeChild(c);
+ }
+ const d = document.createElement("div");
+ d.setAttribute("id", "container");
+ d.setAttribute("class", "app-container");
+ document.body.appendChild(d);
+ const s = document.createElement("script");
+ s.setAttribute("id", "code");
+ s.setAttribute("type", "application/javascript");
+ s.textContent = atob(event.content);
+ document.body.appendChild(s);
+ }
+ });
+ ws.onerror = (error) => {
+ console.error(error);
+ };
+ ws.onclose = (e) => {
+ setTimeout(setupLiveReload, 500);
+ };
+}
+
+export { renderStories, parseGroupImport } from "./stories.js"
diff --git a/packages/web-util/src/index.node.ts b/packages/web-util/src/index.node.ts
new file mode 100644
index 000000000..0ef65921b
--- /dev/null
+++ b/packages/web-util/src/index.node.ts
@@ -0,0 +1,3 @@
+export { serve } from "./serve.js"
+
+
diff --git a/packages/web-util/src/index.ts b/packages/web-util/src/index.ts
new file mode 100644
index 000000000..cf0c963ed
--- /dev/null
+++ b/packages/web-util/src/index.ts
@@ -0,0 +1,4 @@
+
+
+
+export default {} \ No newline at end of file
diff --git a/packages/web-util/src/keys/ca.crt b/packages/web-util/src/keys/ca.crt
new file mode 100644
index 000000000..d0fd544a6
--- /dev/null
+++ b/packages/web-util/src/keys/ca.crt
@@ -0,0 +1,14 @@
+-----BEGIN CERTIFICATE-----
+MIICODCCAaGgAwIBAgIUH8AY7kGN1yzGEwQOZKeL26ZOQHAwDQYJKoZIhvcNAQEL
+BQAwLjEVMBMGA1UEAwwMbG9jYWxob3N0LWNhMRUwEwYDVQQKDAxsb2NhbGhvc3Qt
+Y2EwHhcNMjIxMTMwMjIwNjAxWhcNMjIxMjMwMjIwNjAxWjAuMRUwEwYDVQQDDAxs
+b2NhbGhvc3QtY2ExFTATBgNVBAoMDGxvY2FsaG9zdC1jYTCBnzANBgkqhkiG9w0B
+AQEFAAOBjQAwgYkCgYEAo2gw/oYcKxrSeDbVTTFX8pZA8fojGMwcQlSmeYMUrhtn
++PkXEvCTyMWcreLg2Y4sgdOjvK0ZM7OXnf/jx4fDiMpGy5BHT2ZJRWPzSh6UmNUy
+kyeRAkDB3gCyQSHmmL1rEFOuwmq1yoT0FlIyTQ+mWrs5yg7QTe1rRyFWXHIt1TMC
+AwEAAaNTMFEwHQYDVR0OBBYEFO1Op1KRMkVkzadGy2TZFQlwG9FFMB8GA1UdIwQY
+MBaAFO1Op1KRMkVkzadGy2TZFQlwG9FFMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI
+hvcNAQELBQADgYEAIdePTdDsD8IBFfHze9YVU+VZg3aNO5F/6QJPy/8InejQU0V8
+9Cod19SEh3Kdlpa4QLvZH1cX+ac7bvhL0JaZg0dsz8UaZ8xrkEPx6JJAwgCiv/Ir
+YqhoRd4fv/c6/B0yqD4Dhoy/jGkxfvc8XDnAuAP0uRttGwvsvHS9cSkHYFo=
+-----END CERTIFICATE-----
diff --git a/packages/web-util/src/keys/ca.key b/packages/web-util/src/keys/ca.key
new file mode 100644
index 000000000..8699ccb10
--- /dev/null
+++ b/packages/web-util/src/keys/ca.key
@@ -0,0 +1,16 @@
+-----BEGIN PRIVATE KEY-----
+MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAKNoMP6GHCsa0ng2
+1U0xV/KWQPH6IxjMHEJUpnmDFK4bZ/j5FxLwk8jFnK3i4NmOLIHTo7ytGTOzl53/
+48eHw4jKRsuQR09mSUVj80oelJjVMpMnkQJAwd4AskEh5pi9axBTrsJqtcqE9BZS
+Mk0Pplq7OcoO0E3ta0chVlxyLdUzAgMBAAECgYABkiDWcYeXynw3d595TH4h8NvS
+96qatGuZH6MyC9aJDe5j8FEOd42UIoItEb9DmCBJZzVtvOQ/IPzWIf2Yj2+LvydI
+qEA6ucroa9F9KG9T9ywNJfqM8fNzARQEAzK4/PglbT+n27hkNIm35BOA8PIUuBiD
+pT6D0L0LHfNs6NkRAQJBAM9RS9ApnRmo4qV8kNJvysBJ/NO8PdLT47XIA2uPaAAT
+O9NjrxGHaP0is+PIuwgTi9T5lyprpQss2yS9O7rN5PMCQQDJx0CMjkPDbelbWeH2
+nOvyxLLCev69ae6zVrMPcE7vRPohlJTSK/kgouLr0F6lomK9HVugD7VgrQHuj9am
+UV7BAkBhCHnlejSvl95M+lqGRBCvo3GUYJzHGqmPoYgIRdy1fEsaC6QbHjfDkwSD
+bqYrh4qBKjjYf/2Fl38SWQelzUyFAkBoht27cl9MN/3xIsjZ1kSsiJUKBmk8ekn7
+gWhVERry/EqPZscJcVonO/pNqq29JDf+O90hN8IACN+9U6ogknqBAkAr3SowHLyD
+LfTrEDxeoAd2+K7gGKyrK3gyIISbuWtluONNPqenuFFHXxehwJ72VplNkpUZP4Bt
+TQcIW9zIYT5r
+-----END PRIVATE KEY-----
diff --git a/packages/web-util/src/keys/ca.srl b/packages/web-util/src/keys/ca.srl
new file mode 100644
index 000000000..a53ff9b36
--- /dev/null
+++ b/packages/web-util/src/keys/ca.srl
@@ -0,0 +1 @@
+7488FC4F9D5E2BB55DEA16CF051F4E99ACA25241
diff --git a/packages/web-util/src/keys/localhost.crt b/packages/web-util/src/keys/localhost.crt
new file mode 100644
index 000000000..e32f2e24a
--- /dev/null
+++ b/packages/web-util/src/keys/localhost.crt
@@ -0,0 +1,15 @@
+-----BEGIN CERTIFICATE-----
+MIICRTCCAa6gAwIBAgIUdIj8T51eK7Vd6hbPBR9OmayiUkEwDQYJKoZIhvcNAQEL
+BQAwLjEVMBMGA1UEAwwMbG9jYWxob3N0LWNhMRUwEwYDVQQKDAxsb2NhbGhvc3Qt
+Y2EwHhcNMjIxMTMwMjIwNjAyWhcNMjMxMTMwMjIwNjAyWjArMRIwEAYDVQQDDAls
+b2NhbGhvc3QxFTATBgNVBAoMDGxvY2FsaG9zdC1jYTCBnzANBgkqhkiG9w0BAQEF
+AAOBjQAwgYkCgYEAvir90pl9q6qUsBsBz7jjdw0r1DDPeViqkSyDDTt4Lw2zYJbn
+Z7kxcTvNHNRWdtsWSzY/43ERCJu6nX60kMiML3NV00ty2VpaYeW9J5ozXgNbb+5P
+esLHrIHmnOIUj46jyiHjDKs+hgrfcrFg7W7ndjW3dCAvkeAV+mncz59pFvkCAwEA
+AaNjMGEwCQYDVR0TBAIwADAdBgNVHQ4EFgQUXADNSPivlIUBpKyd/XirIcqxqFgw
+HwYDVR0jBBgwFoAU7U6nUpEyRWTNp0bLZNkVCXAb0UUwFAYDVR0RBA0wC4IJbG9j
+YWxob3N0MA0GCSqGSIb3DQEBCwUAA4GBAClcLuKFnRJjAgP8652jJscYMLWYEkv3
+j9kChErpKZNKiv+VlWKPiOvhZVAl+/YEsBOKXpRFX3CuLCdGtuv7b6NaH7yEXaZn
+9MVIrYMRub3k0gVAhu3z3VXuvHFXdTms3KRlGdPdQV2xgpQJczDNnd7idp/GyI4j
+KqBo0UxuWZBJ
+-----END CERTIFICATE-----
diff --git a/packages/web-util/src/keys/localhost.csr b/packages/web-util/src/keys/localhost.csr
new file mode 100644
index 000000000..5f821f8b5
--- /dev/null
+++ b/packages/web-util/src/keys/localhost.csr
@@ -0,0 +1,10 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIIBajCB1AIBADArMRIwEAYDVQQDDAlsb2NhbGhvc3QxFTATBgNVBAoMDGxvY2Fs
+aG9zdC1jYTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAvir90pl9q6qUsBsB
+z7jjdw0r1DDPeViqkSyDDTt4Lw2zYJbnZ7kxcTvNHNRWdtsWSzY/43ERCJu6nX60
+kMiML3NV00ty2VpaYeW9J5ozXgNbb+5PesLHrIHmnOIUj46jyiHjDKs+hgrfcrFg
+7W7ndjW3dCAvkeAV+mncz59pFvkCAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4GBADJW
+Ww+l4E///54fz82AE5x8U114Yk32EbB1qOfGLyXgoXySGyLuiNu40SXxioKa/Gpn
+Z92o5JIrMVWUroPzMKAMXdAsixkaBGrT5RYzR9ztfy59djxp0f7dlL3ZxDO8JHOw
+aTJXJxKEfYdv0oFhkx/u4ki6BsaqG9mQfsFXtlUp
+-----END CERTIFICATE REQUEST-----
diff --git a/packages/web-util/src/keys/localhost.key b/packages/web-util/src/keys/localhost.key
new file mode 100644
index 000000000..c9b1cb6c8
--- /dev/null
+++ b/packages/web-util/src/keys/localhost.key
@@ -0,0 +1,16 @@
+-----BEGIN PRIVATE KEY-----
+MIICeQIBADANBgkqhkiG9w0BAQEFAASCAmMwggJfAgEAAoGBAL4q/dKZfauqlLAb
+Ac+443cNK9Qwz3lYqpEsgw07eC8Ns2CW52e5MXE7zRzUVnbbFks2P+NxEQibup1+
+tJDIjC9zVdNLctlaWmHlvSeaM14DW2/uT3rCx6yB5pziFI+Oo8oh4wyrPoYK33Kx
+YO1u53Y1t3QgL5HgFfpp3M+faRb5AgMBAAECgYEAh1xgqdxZqKzWA3hl1K7dMmus
+q/BGbjCf0JAnhG61QID3EqS3eIxI1jnj6UZ3eUi/WK/3z/Q2VLNMpTiAXKJzrUP0
+8m7yO87AeUxhy0rvtWEVmd8NBQjJKD2iElgy6tR9QUsgTXer9xuQf0sHRQb1psNU
+11WsBnwdzeEEzquORVUCQQDtJx/HjHDVTDF02W5B23J4oqwuu1EDCVDqNJiYSDSt
+2Dh0IdvSKJyh9lXIoY+kbbEui8uPPnhPKM1LIRfiv7FHAkEAzUf1mvTBNUGCwjZu
+qy/oKDR7TlEbdyDJY1F0JPquyim/CenRtM8VAH22Tni8+bSSpnHknytvKfaC0YFb
+VN8VvwJBAKTdJgKbZ3Vg2qDY5wVxgUrMC9cQ8Wii+VVX6x0yVSzlu5lAUIjxIrKV
+hV1Ms4cjmqE5HfIfA5REUTOBdhF0IdECQQC/1lia19Ha7/6/eljP17RQJkN5O+i7
+2kL5crxkdnRz7rFeFUlpfAB3dgOxr7mCbZKCw3rQmKmJAJreKNHuLZBHAkEAwYZ4
+tc4mWjtw4AMDK59o8d8ANObyuVaIy6I54NZ0ogg+0nzrXii9LkZZhAWwVSN9BdXa
+TYVu0J5fGxDZVAm0zQ==
+-----END PRIVATE KEY-----
diff --git a/packages/web-util/src/live-reload.ts b/packages/web-util/src/live-reload.ts
new file mode 100644
index 000000000..bae0a5b84
--- /dev/null
+++ b/packages/web-util/src/live-reload.ts
@@ -0,0 +1,52 @@
+/* eslint-disable no-undef */
+
+function setupLiveReload(): void {
+ const ws = new WebSocket("wss://localhost:8080/ws");
+
+ ws.addEventListener("message", (message) => {
+ try {
+ const event = JSON.parse(message.data);
+ if (event.type === "file-updated-start") {
+ showReloadOverlay();
+ return;
+ }
+ if (event.type === "file-updated-done") {
+ window.location.reload();
+ return;
+ }
+ } catch (e) {
+ return
+ }
+ console.log("unsupported", event);
+ });
+
+ ws.addEventListener("error", (error) => {
+ console.error(error);
+ });
+ ws.addEventListener("close", (message) => {
+ setTimeout(setupLiveReload, 1500);
+ });
+}
+setupLiveReload();
+
+
+function showReloadOverlay(): void {
+ const d = document.createElement("div");
+ d.style.position = "absolute";
+ d.style.width = "100%";
+ d.style.height = "100%";
+ d.style.color = "white";
+ d.style.backgroundColor = "rgba(0,0,0,0.5)";
+ d.style.display = "flex";
+ d.style.justifyContent = "center";
+ const h = document.createElement("h1");
+ h.style.margin = "auto";
+ h.innerHTML = "reloading...";
+ d.appendChild(h);
+ if (document.body.firstChild) {
+ document.body.insertBefore(d, document.body.firstChild);
+ } else {
+ document.body.appendChild(d);
+ }
+}
+
diff --git a/packages/web-util/src/serve.ts b/packages/web-util/src/serve.ts
new file mode 100644
index 000000000..11cc6db39
--- /dev/null
+++ b/packages/web-util/src/serve.ts
@@ -0,0 +1,108 @@
+import {
+ Logger
+} from "@gnu-taler/taler-util";
+import chokidar from 'chokidar';
+import express from "express";
+import https from "https";
+import { parse } from 'url';
+import WebSocket, { Server } from 'ws';
+
+
+import locahostCrt from './keys/localhost.crt';
+import locahostKey from './keys/localhost.key';
+import storiesHtml from './stories.html';
+
+import path from "path";
+
+const httpServerOptions = {
+ key: locahostKey,
+ cert: locahostCrt
+};
+
+const logger = new Logger("serve.ts");
+
+const PATHS = {
+ WS: "/ws",
+ NOTIFY: "/notify",
+ EXAMPLE: "/examples",
+ APP: "/app",
+}
+
+export async function serve(opts: {
+ folder: string,
+ port: number,
+ source?: string,
+ development?: boolean,
+ examplesLocationJs?: string,
+ examplesLocationCss?: string,
+ onUpdate?: () => Promise<void>;
+}): Promise<void> {
+
+ const app = express()
+
+ app.use(PATHS.APP, express.static(opts.folder))
+ const server = https.createServer(httpServerOptions, app)
+ server.listen(opts.port);
+ logger.info(`serving ${opts.folder} on ${opts.port}`)
+ logger.info(` ${PATHS.APP}: application`)
+ logger.info(` ${PATHS.EXAMPLE}: examples`)
+ logger.info(` ${PATHS.WS}: websocket`)
+ logger.info(` ${PATHS.NOTIFY}: broadcast`)
+
+ if (opts.development) {
+
+ const wss = new Server({ noServer: true });
+
+ wss.on('connection', function connection(ws) {
+ ws.send('welcome');
+ });
+
+ server.on('upgrade', function upgrade(request, socket, head) {
+ const { pathname } = parse(request.url || "");
+ if (pathname === PATHS.WS) {
+ wss.handleUpgrade(request, socket, head, function done(ws) {
+ wss.emit('connection', ws, request);
+ });
+ } else {
+ socket.destroy();
+ }
+ });
+
+ const sendToAllClients = function (data: object): void {
+ wss.clients.forEach(function each(client) {
+ if (client.readyState === WebSocket.OPEN) {
+ client.send(JSON.stringify(data));
+ }
+ })
+ }
+ const watchingFolder = opts.source ?? opts.folder
+ logger.info(`watching ${watchingFolder} for change`)
+
+ chokidar.watch(watchingFolder).on('change', (path, stats) => {
+ logger.trace(`changed ${path}`)
+
+ sendToAllClients({ type: 'file-updated-start', data: { path } })
+ if (opts.onUpdate) {
+ opts.onUpdate().then(result => {
+ sendToAllClients({ type: 'file-updated-done', data: { path, result } })
+ })
+ } else {
+ sendToAllClients({ type: 'file-change-done', data: { path } })
+ }
+ })
+
+ app.get(PATHS.EXAMPLE, function (req: any, res: any) {
+ res.set('Content-Type', 'text/html')
+ res.send(storiesHtml
+ .replace('__EXAMPLES_JS_FILE_LOCATION__', opts.examplesLocationJs ?? `.${PATHS.APP}/stories.js`)
+ .replace('__EXAMPLES_CSS_FILE_LOCATION__', opts.examplesLocationCss ?? `.${PATHS.APP}/stories.css`))
+ })
+
+ app.get(PATHS.NOTIFY, function (req: any, res: any) {
+ res.send('ok')
+ })
+
+ }
+}
+
+
diff --git a/packages/web-util/src/stories.html b/packages/web-util/src/stories.html
new file mode 100644
index 000000000..4c16ad2ff
--- /dev/null
+++ b/packages/web-util/src/stories.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>WebUtils: Stories</title>
+ <meta charset="utf-8" />
+ <link rel="icon" href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" />
+ <link
+ rel="stylesheet"
+ type="text/css"
+ href="__EXAMPLES_CSS_FILE_LOCATION__"
+ />
+ <script type="module" src="__EXAMPLES_JS_FILE_LOCATION__"></script>
+ </head>
+ <body>
+ <taler-stories id="container"></taler-stories>
+ </body>
+</html>
diff --git a/packages/web-util/src/stories.tsx b/packages/web-util/src/stories.tsx
new file mode 100644
index 000000000..a8a9fdf77
--- /dev/null
+++ b/packages/web-util/src/stories.tsx
@@ -0,0 +1,580 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { setupI18n } from "@gnu-taler/taler-util";
+import e from "express";
+import {
+ ComponentChild,
+ ComponentChildren,
+ Fragment,
+ FunctionalComponent,
+ FunctionComponent,
+ h,
+ JSX,
+ render,
+ VNode,
+} from "preact";
+import { useEffect, useErrorBoundary, useState } from "preact/hooks";
+
+const Page: FunctionalComponent = ({ children }): VNode => {
+ return (
+ <div
+ style={{
+ fontFamily: "Arial, Helvetica, sans-serif",
+ width: "100%",
+ display: "flex",
+ flexDirection: "row",
+ }}
+ >
+ {children}
+ </div>
+ );
+};
+
+const SideBar: FunctionalComponent<{ width: number }> = ({
+ width,
+ children,
+}): VNode => {
+ return (
+ <div
+ style={{
+ minWidth: width,
+ height: "calc(100vh - 20px)",
+ overflowX: "hidden",
+ overflowY: "visible",
+ scrollBehavior: "smooth",
+ }}
+ >
+ {children}
+ </div>
+ );
+};
+
+const ResizeHandleDiv: FunctionalComponent<
+ JSX.HTMLAttributes<HTMLDivElement>
+> = ({ children, ...props }): VNode => {
+ return (
+ <div
+ {...props}
+ style={{
+ width: 10,
+ backgroundColor: "#ddd",
+ cursor: "ew-resize",
+ }}
+ >
+ {children}
+ </div>
+ );
+};
+
+const Content: FunctionalComponent = ({ children }): VNode => {
+ return (
+ <div
+ style={{
+ width: "100%",
+ padding: 20,
+ }}
+ >
+ {children}
+ </div>
+ );
+};
+
+function findByGroupComponentName(
+ allExamples: Group[],
+ group: string,
+ component: string,
+ name: string,
+): ExampleItem | undefined {
+ const gl = allExamples.filter((e) => e.title === group);
+ if (gl.length === 0) {
+ return undefined;
+ }
+ const cl = gl[0].list.filter((l) => l.name === component);
+ if (cl.length === 0) {
+ return undefined;
+ }
+ const el = cl[0].examples.filter((c) => c.name === name);
+ if (el.length === 0) {
+ return undefined;
+ }
+ return el[0];
+}
+
+function getContentForExample(
+ item: ExampleItem | undefined,
+ allExamples: Group[],
+): FunctionalComponent {
+ if (!item)
+ return function SelectExampleMessage() {
+ return <div>select example from the list on the left</div>;
+ };
+ const example = findByGroupComponentName(
+ allExamples,
+ item.group,
+ item.component,
+ item.name,
+ );
+ if (!example) {
+ return function ExampleNotFoundMessage() {
+ return <div>example not found</div>;
+ };
+ }
+ return () => example.render.component(example.render.props);
+}
+
+function ExampleList({
+ name,
+ list,
+ selected,
+ onSelectStory,
+}: {
+ name: string;
+ list: {
+ name: string;
+ examples: ExampleItem[];
+ }[];
+ selected: ExampleItem | undefined;
+ onSelectStory: (i: ExampleItem, id: string) => void;
+}): VNode {
+ const [isOpen, setOpen] = useState(selected && selected.group === name);
+ return (
+ <ol style={{ padding: 4, margin: 0 }}>
+ <div
+ style={{ backgroundColor: "lightcoral", cursor: "pointer" }}
+ onClick={() => setOpen(!isOpen)}
+ >
+ {name}
+ </div>
+ <div style={{ display: isOpen ? undefined : "none" }}>
+ {list.map((k) => (
+ <li key={k.name}>
+ <dl style={{ margin: 0 }}>
+ <dt>{k.name}</dt>
+ {k.examples.map((r, i) => {
+ const e = encodeURIComponent;
+ const eId = `${e(r.group)}-${e(r.component)}-${e(r.name)}`;
+ const isSelected =
+ selected &&
+ selected.component === r.component &&
+ selected.group === r.group &&
+ selected.name === r.name;
+ return (
+ <dd
+ id={eId}
+ key={r.name}
+ style={{
+ backgroundColor: isSelected
+ ? "green"
+ : i % 2
+ ? "lightgray"
+ : "lightblue",
+ marginLeft: "1em",
+ padding: 4,
+ cursor: "pointer",
+ borderRadius: 4,
+ marginBottom: 4,
+ }}
+ >
+ <a
+ href={`#${eId}`}
+ style={{ color: "black" }}
+ onClick={(e) => {
+ e.preventDefault();
+ location.hash = `#${eId}`;
+ onSelectStory(r, eId);
+ history.pushState({}, "", `#${eId}`);
+ }}
+ >
+ {r.name}
+ </a>
+ </dd>
+ );
+ })}
+ </dl>
+ </li>
+ ))}
+ </div>
+ </ol>
+ );
+}
+
+/**
+ * Prevents the UI from redirecting and inform the dev
+ * where the <a /> should have redirected
+ * @returns
+ */
+function PreventLinkNavigation({
+ children,
+}: {
+ children: ComponentChildren;
+}): VNode {
+ return (
+ <div
+ onClick={(e) => {
+ let t: any = e.target;
+ do {
+ if (t.localName === "a" && t.getAttribute("href")) {
+ alert(`should navigate to: ${t.attributes.href.value}`);
+ e.stopImmediatePropagation();
+ e.stopPropagation();
+ e.preventDefault();
+ return false;
+ }
+ } while ((t = t.parentNode));
+ return true;
+ }}
+ >
+ {children}
+ </div>
+ );
+}
+
+function ErrorReport({
+ children,
+ selected,
+}: {
+ children: ComponentChild;
+ selected: ExampleItem | undefined;
+}): VNode {
+ const [error, resetError] = useErrorBoundary();
+ //if there is an error, reset when unloading this component
+ useEffect(() => (error ? resetError : undefined));
+ if (error) {
+ return (
+ <div>
+ <p>Error was thrown trying to render</p>
+ {selected && (
+ <ul>
+ <li>
+ <b>group</b>: {selected.group}
+ </li>
+ <li>
+ <b>component</b>: {selected.component}
+ </li>
+ <li>
+ <b>example</b>: {selected.name}
+ </li>
+ <li>
+ <b>args</b>:{" "}
+ <pre>{JSON.stringify(selected.render.props, undefined, 2)}</pre>
+ </li>
+ </ul>
+ )}
+ <p>{error.message}</p>
+ <pre>{error.stack}</pre>
+ </div>
+ );
+ }
+ return <Fragment>{children}</Fragment>;
+}
+
+function getSelectionFromLocationHash(
+ hash: string,
+ allExamples: Group[],
+): ExampleItem | undefined {
+ if (!hash) return undefined;
+ const parts = hash.substring(1).split("-");
+ if (parts.length < 3) return undefined;
+ return findByGroupComponentName(
+ allExamples,
+ decodeURIComponent(parts[0]),
+ decodeURIComponent(parts[1]),
+ decodeURIComponent(parts[2]),
+ );
+}
+
+function parseExampleImport(
+ group: string,
+ componentName: string,
+ im: MaybeComponent,
+): ComponentItem {
+ const examples: ExampleItem[] = Object.entries(im)
+ .filter(([k]) => k !== "default")
+ .map(([exampleName, exampleValue]): ExampleItem => {
+ if (!exampleValue) {
+ throw Error(
+ `example "${exampleName}" from component "${componentName}" in group "${group}" is undefined`,
+ );
+ }
+
+ if (typeof exampleValue === "function") {
+ return {
+ group,
+ component: componentName,
+ name: exampleName,
+ render: {
+ component: exampleValue as FunctionComponent,
+ props: {},
+ },
+ };
+ }
+ const v: any = exampleValue;
+ if (
+ "component" in v &&
+ typeof v.component === "function" &&
+ "props" in v
+ ) {
+ return {
+ group,
+ component: componentName,
+ name: exampleName,
+ render: v,
+ };
+ }
+ throw Error(
+ `example "${exampleName}" from component "${componentName}" in group "${group}" doesn't follow one of the two ways of example`,
+ );
+ });
+ return {
+ name: componentName,
+ examples,
+ };
+}
+
+export function parseGroupImport(
+ groups: Record<string, ComponentOrFolder>,
+): Group[] {
+ return Object.entries(groups).map(([groupName, value]) => {
+ return {
+ title: groupName,
+ list: Object.entries(value).flatMap(([key, value]) =>
+ folder(groupName, value),
+ ),
+ };
+ });
+}
+
+export interface Group {
+ title: string;
+ list: ComponentItem[];
+}
+
+export interface ComponentItem {
+ name: string;
+ examples: ExampleItem[];
+}
+
+export interface ExampleItem {
+ group: string;
+ component: string;
+ name: string;
+ render: {
+ component: FunctionalComponent;
+ props: object;
+ };
+}
+
+type ComponentOrFolder = MaybeComponent | MaybeFolder;
+interface MaybeFolder {
+ default?: { title: string };
+ // [exampleName: string]: FunctionalComponent;
+}
+interface MaybeComponent {
+ // default?: undefined;
+ [exampleName: string]: undefined | object;
+}
+
+function folder(groupName: string, value: ComponentOrFolder): ComponentItem[] {
+ let title: string | undefined = undefined;
+ try {
+ title =
+ typeof value === "object" &&
+ typeof value.default === "object" &&
+ value.default !== undefined &&
+ "title" in value.default &&
+ typeof value.default.title === "string"
+ ? value.default.title
+ : undefined;
+ } catch (e) {
+ throw Error(
+ `Could not defined if it is component or folder ${groupName}: ${JSON.stringify(
+ value,
+ undefined,
+ 2,
+ )}`,
+ );
+ }
+ if (title) {
+ const c = parseExampleImport(groupName, title, value as MaybeComponent);
+ return [c];
+ }
+ return Object.entries(value).flatMap(([subkey, value]) =>
+ folder(groupName, value),
+ );
+}
+
+interface Props {
+ getWrapperForGroup: (name: string) => FunctionComponent;
+ examplesInGroups: Group[];
+ langs: Record<string, object>;
+}
+
+function Application({
+ langs,
+ examplesInGroups,
+ getWrapperForGroup,
+}: Props): VNode {
+ const initialSelection = getSelectionFromLocationHash(
+ location.hash,
+ examplesInGroups,
+ );
+
+ const url = new URL(window.location.href);
+ const currentLang = url.searchParams.get("lang") || "en";
+
+ if (!langs["en"]) {
+ langs["en"] = {};
+ }
+ setupI18n(currentLang, langs);
+
+ const [selected, updateSelected] = useState<ExampleItem | undefined>(
+ initialSelection,
+ );
+ const [sidebarWidth, setSidebarWidth] = useState(200);
+ useEffect(() => {
+ if (location.hash) {
+ const hash = location.hash.substring(1);
+ const found = document.getElementById(hash);
+ if (found) {
+ setTimeout(() => {
+ found.scrollIntoView({
+ block: "center",
+ });
+ }, 10);
+ }
+ }
+ }, []);
+
+ const GroupWrapper = getWrapperForGroup(selected?.group || "default");
+ const ExampleContent = getContentForExample(selected, examplesInGroups);
+
+ //style={{ "--with-size": `${sidebarWidth}px` }}
+ return (
+ <Page>
+ {/* <LiveReload /> */}
+ <SideBar width={sidebarWidth}>
+ <div>
+ Language:
+ <select
+ value={currentLang}
+ onChange={(e) => {
+ const url = new URL(window.location.href);
+ url.searchParams.set("lang", e.currentTarget.value);
+ window.location.href = url.href;
+ }}
+ >
+ {Object.keys(langs).map((l) => (
+ <option key={l}>{l}</option>
+ ))}
+ </select>
+ </div>
+ {examplesInGroups.map((group) => (
+ <ExampleList
+ key={group.title}
+ name={group.title}
+ list={group.list}
+ selected={selected}
+ onSelectStory={(item, htmlId) => {
+ document.getElementById(htmlId)?.scrollIntoView({
+ block: "center",
+ });
+ updateSelected(item);
+ }}
+ />
+ ))}
+ <hr />
+ </SideBar>
+ <ResizeHandle
+ onUpdate={(x) => {
+ setSidebarWidth((s) => s + x);
+ }}
+ />
+ <Content>
+ <ErrorReport selected={selected}>
+ <PreventLinkNavigation>
+ <GroupWrapper>
+ <ExampleContent />
+ </GroupWrapper>
+ </PreventLinkNavigation>
+ </ErrorReport>
+ </Content>
+ </Page>
+ );
+}
+
+export interface Options {
+ id?: string;
+ strings?: any;
+ getWrapperForGroup?: (name: string) => FunctionComponent;
+}
+
+export function renderStories(
+ groups: Record<string, ComponentOrFolder>,
+ options: Options = {},
+): void {
+ const examples = parseGroupImport(groups);
+
+ try {
+ const cid = options.id ?? "container";
+ const container = document.getElementById(cid);
+ if (!container) {
+ throw Error(
+ `container with id ${cid} not found, can't mount page contents`,
+ );
+ }
+ render(
+ <Application
+ examplesInGroups={examples}
+ getWrapperForGroup={options.getWrapperForGroup ?? (() => Fragment)}
+ langs={options.strings ?? { en: {} }}
+ />,
+ container,
+ );
+ } catch (e) {
+ console.error("got error", e);
+ if (e instanceof Error) {
+ document.body.innerText = `Fatal error: "${e.message}". Please report this bug at https://bugs.gnunet.org/.`;
+ }
+ }
+}
+
+function ResizeHandle({ onUpdate }: { onUpdate: (x: number) => void }): VNode {
+ const [start, setStart] = useState<number | undefined>(undefined);
+ return (
+ <ResizeHandleDiv
+ onMouseDown={(e: any) => {
+ setStart(e.pageX);
+ console.log("active", e.pageX);
+ return false;
+ }}
+ onMouseMove={(e: any) => {
+ if (start !== undefined) {
+ onUpdate(e.pageX - start);
+ }
+ return false;
+ }}
+ onMouseUp={() => {
+ setStart(undefined);
+ return false;
+ }}
+ />
+ );
+}
diff --git a/packages/web-util/tsconfig.json b/packages/web-util/tsconfig.json
new file mode 100644
index 000000000..aede0a0ac
--- /dev/null
+++ b/packages/web-util/tsconfig.json
@@ -0,0 +1,34 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "target": "ES6",
+ "module": "ESNext",
+ "jsx": "react",
+ "jsxFactory": "h",
+ "jsxFragmentFactory": "Fragment",
+ "moduleResolution": "Node",
+ "sourceMap": true,
+ "lib": [
+ "es6"
+ ],
+ "outDir": "lib",
+ "preserveSymlinks": true,
+ "skipLibCheck": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "strict": true,
+ "strictPropertyInitialization": false,
+ "noImplicitAny": true,
+ "noImplicitThis": true,
+ "incremental": true,
+ "esModuleInterop": true,
+ "importHelpers": true,
+ "rootDir": "./src",
+ "typeRoots": [
+ "./node_modules/@types"
+ ]
+ },
+ "include": [
+ "src/**/*"
+ ]
+} \ No newline at end of file