web-util: utils for developing webapps
This commit is contained in:
parent
219e48f351
commit
e382b02203
0
packages/web-util/README
Normal file
0
packages/web-util/README
Normal file
19
packages/web-util/bin/taler-web-cli.mjs
Executable file
19
packages/web-util/bin/taler-web-cli.mjs
Executable file
@ -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();
|
103
packages/web-util/build.mjs
Executable file
103
packages/web-util/build.mjs
Executable file
@ -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)
|
||||||
|
});
|
||||||
|
|
||||||
|
})
|
||||||
|
|
48
packages/web-util/create_certificate.sh
Normal file
48
packages/web-util/create_certificate.sh
Normal file
@ -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!
|
40
packages/web-util/package.json
Normal file
40
packages/web-util/package.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
59
packages/web-util/src/cli.ts
Normal file
59
packages/web-util/src/cli.ts
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
12
packages/web-util/src/custom.d.ts
vendored
Normal file
12
packages/web-util/src/custom.d.ts
vendored
Normal file
@ -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;
|
||||||
|
}
|
38
packages/web-util/src/index.browser.ts
Normal file
38
packages/web-util/src/index.browser.ts
Normal file
@ -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"
|
3
packages/web-util/src/index.node.ts
Normal file
3
packages/web-util/src/index.node.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export { serve } from "./serve.js"
|
||||||
|
|
||||||
|
|
4
packages/web-util/src/index.ts
Normal file
4
packages/web-util/src/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default {}
|
14
packages/web-util/src/keys/ca.crt
Normal file
14
packages/web-util/src/keys/ca.crt
Normal file
@ -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-----
|
16
packages/web-util/src/keys/ca.key
Normal file
16
packages/web-util/src/keys/ca.key
Normal file
@ -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-----
|
1
packages/web-util/src/keys/ca.srl
Normal file
1
packages/web-util/src/keys/ca.srl
Normal file
@ -0,0 +1 @@
|
|||||||
|
7488FC4F9D5E2BB55DEA16CF051F4E99ACA25241
|
15
packages/web-util/src/keys/localhost.crt
Normal file
15
packages/web-util/src/keys/localhost.crt
Normal file
@ -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-----
|
10
packages/web-util/src/keys/localhost.csr
Normal file
10
packages/web-util/src/keys/localhost.csr
Normal file
@ -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-----
|
16
packages/web-util/src/keys/localhost.key
Normal file
16
packages/web-util/src/keys/localhost.key
Normal file
@ -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-----
|
52
packages/web-util/src/live-reload.ts
Normal file
52
packages/web-util/src/live-reload.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
108
packages/web-util/src/serve.ts
Normal file
108
packages/web-util/src/serve.ts
Normal file
@ -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')
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
17
packages/web-util/src/stories.html
Normal file
17
packages/web-util/src/stories.html
Normal file
@ -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>
|
580
packages/web-util/src/stories.tsx
Normal file
580
packages/web-util/src/stories.tsx
Normal file
@ -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;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
34
packages/web-util/tsconfig.json
Normal file
34
packages/web-util/tsconfig.json
Normal file
@ -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/**/*"
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user