gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] 01/06: web-util: utils for developing webapps


From: gnunet
Subject: [taler-wallet-core] 01/06: web-util: utils for developing webapps
Date: Tue, 06 Dec 2022 16:13:36 +0100

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a commit to branch master
in repository wallet-core.

commit e382b022030db96b8282337b304ec5e599a5f405
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Tue Dec 6 09:21:17 2022 -0300

    web-util: utils for developing webapps
---
 packages/web-util/README                 |   0
 packages/web-util/bin/taler-web-cli.mjs  |  19 +
 packages/web-util/build.mjs              | 103 ++++++
 packages/web-util/create_certificate.sh  |  48 +++
 packages/web-util/package.json           |  40 +++
 packages/web-util/src/cli.ts             |  59 ++++
 packages/web-util/src/custom.d.ts        |  12 +
 packages/web-util/src/index.browser.ts   |  38 ++
 packages/web-util/src/index.node.ts      |   3 +
 packages/web-util/src/index.ts           |   4 +
 packages/web-util/src/keys/ca.crt        |  14 +
 packages/web-util/src/keys/ca.key        |  16 +
 packages/web-util/src/keys/ca.srl        |   1 +
 packages/web-util/src/keys/localhost.crt |  15 +
 packages/web-util/src/keys/localhost.csr |  10 +
 packages/web-util/src/keys/localhost.key |  16 +
 packages/web-util/src/live-reload.ts     |  52 +++
 packages/web-util/src/serve.ts           | 108 ++++++
 packages/web-util/src/stories.html       |  17 +
 packages/web-util/src/stories.tsx        | 580 +++++++++++++++++++++++++++++++
 packages/web-util/tsconfig.json          |  34 ++
 21 files changed, 1189 insertions(+)

diff --git a/packages/web-util/README b/packages/web-util/README
new file mode 100644
index 000000000..e69de29bb
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/V
 [...]
+    <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

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]