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