Skip to content

Commit 85953a6

Browse files
authored
Shared Storage (#39)
1 parent 521b178 commit 85953a6

File tree

21 files changed

+756
-15
lines changed

21 files changed

+756
-15
lines changed

.prettierrc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"proseWrap": "always"
3+
}

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
"postinstall": "cd packages/vscode-host && yarn",
55
"watch": "pnpm watch --filter ./packages",
66
"build": "pnpm build --filter ./packages && cd packages/vscode-host && yarn build",
7-
"serve": "cd packages/vscode-host && yarn serve",
7+
"serve": "run-p serve:**",
8+
"serve:host": "cd packages/vscode-host && yarn serve",
9+
"serve:entrypoint": "pnpm serve --filter ./packages/entrypoint",
810
"copy-and-serve": "cd packages/vscode-host && yarn copy-and-serve",
911
"lint": "eslint --ext .ts ./packages/*/src/**/*.ts",
1012
"lint:fix": "pnpm lint -- --fix",
@@ -31,6 +33,7 @@
3133
"eslint-plugin-unused-imports": "^1.1.5",
3234
"jsdom": "^19.0.0",
3335
"mocha": "^9.1.3",
36+
"npm-run-all": "^4.1.5",
3437
"process": "^0.11.10",
3538
"serve": "^13.0.2",
3639
"ts-loader": "^9.2.5",

packages/entrypoint/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
The website hosted on convenience URL entry points powering the "simple domain
2+
change" use case.
3+
4+
( To open Ethereum Code Viewer from a [supported blockchain
5+
explorer][explorers], change the TLD (i.e. `.io`, `.com`) to `.deth.net`. )
6+
7+
[explorers]: ../../docs/supported-explorers.md
8+
9+
![domains and iframes](https://user-images.githubusercontent.com/15332326/147769185-649bfacf-8a5c-4aa7-89e1-325441f7633f.png)
10+
11+
## Contributing
12+
13+
### Setup
14+
15+
- Generate HTTPS certificates using `mkcert` as describe in
16+
[root README.md](../../README.md)
17+
18+
### Scripts
19+
20+
- **`pnpm serve`** - Starts HTTP server with the entrypoint website.
21+
22+
- **`pnpm build`** - Builds the website to `./dist` directory, using localhost
23+
as the iframe `src`.
24+
25+
- **`pnpm build:production`** - Builds the website to `./dist` directory, using
26+
*https://ecv.deth.net* as the iframe `src`.
27+
28+
- **`pnpm dev`** - runs `build` and `serve`.

packages/entrypoint/package.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "@dethcrypto/ethereum-viewer-app-entrypoint",
3+
"description": "The website hosted on convenience URL entry points powering the 'simple domain change' use case.",
4+
"private": true,
5+
"scripts": {
6+
"serve": "serve --cors -l 5000 --ssl-cert ../../certs/localhost.pem --ssl-key ../../certs/localhost-key.pem ./dist -c ../../vscode-host/serve.json",
7+
"build": "node scripts/build.js",
8+
"build:production": "node scripts/build.js --production",
9+
"dev": "pnpm build && pnpm serve"
10+
},
11+
"devDependencies": {
12+
"fs-extra": "^10.0.0",
13+
"serve": "^13.0.2",
14+
"ts-essentials": "^9.0.0",
15+
"typescript": "^4.5.3",
16+
"esbuild": "^0.14.10"
17+
}
18+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
<title>Ethereum Code Viewer</title>
6+
7+
<!-- Disable pinch zooming -->
8+
<meta
9+
name="viewport"
10+
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"
11+
/>
12+
13+
<style>
14+
body,
15+
html {
16+
height: 100%;
17+
}
18+
body {
19+
margin: 0;
20+
}
21+
iframe {
22+
width: 100%;
23+
height: 100%;
24+
border: none;
25+
}
26+
</style>
27+
</head>
28+
29+
<body>
30+
<iframe
31+
src="{{APPLICATION_URL}}"
32+
id="frame"
33+
sandbox="allow-scripts allow-popups allow-same-origin"
34+
></iframe>
35+
</body>
36+
37+
<script src="./index.js"></script>
38+
</html>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../vercel.json
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
exports.argv = {
2+
production:
3+
process.env.NODE_ENV === "production" || saysTrue(readArg("production")),
4+
};
5+
6+
function saysTrue(value) {
7+
if (typeof value === "string") value = value.toLowerCase();
8+
9+
return [1, true, "1", "true", "yes"].includes(value);
10+
}
11+
12+
/**
13+
* @param {string} name
14+
*/
15+
function readArg(name) {
16+
if (process.argv.includes(`--${name}`)) return true;
17+
18+
const argWithValuePrefix = `--${name}=`;
19+
const withValue = process.argv.find((arg) =>
20+
arg.startsWith(argWithValuePrefix)
21+
);
22+
23+
if (withValue) {
24+
return withValue.replace(argWithValuePrefix, "");
25+
}
26+
27+
const fromEnv = process.env[name.toUpperCase()];
28+
if (fromEnv) return fromEnv;
29+
30+
return false;
31+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// @ts-check
2+
3+
const esbuild = require("esbuild");
4+
const { copySync, readFileSync, writeFileSync, rmSync } = require("fs-extra");
5+
6+
const { argv } = require("./argv");
7+
8+
const PRODUCTION_URL = "https://ecv.deth.net";
9+
const DEVELOPMENT_URL = "https://localhost:5001";
10+
11+
rmSync("./dist", { recursive: true, force: true });
12+
copySync("./public", "./dist");
13+
14+
// Replace templates in index.html
15+
let indexHtml = readFileSync("./dist/index.html", "utf8");
16+
indexHtml = indexHtml.replace(
17+
"{{APPLICATION_URL}}",
18+
argv.production ? PRODUCTION_URL : DEVELOPMENT_URL
19+
);
20+
writeFileSync("./dist/index.html", indexHtml, { encoding: "utf8" });
21+
22+
esbuild.build({
23+
entryPoints: ["./src/index.ts"],
24+
outfile: "./dist/index.js",
25+
sourcemap: true,
26+
});

packages/entrypoint/src/index.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import type { UnionToIntersection } from "ts-essentials";
2+
3+
import type { ExposedFunctions } from "../../vscode-host/src/deth/in-iframe/lib";
4+
5+
const log = console.log.bind(console, "\x1b[32m [ecv:top]");
6+
7+
// on load, we copy pathname and search params to the iframe
8+
const iframe = document.getElementById("frame")! as HTMLIFrameElement;
9+
const url = new URL(iframe.getAttribute("src")!);
10+
const { hostname, pathname, search } = window.location;
11+
12+
url.pathname = pathname;
13+
url.search = search;
14+
15+
// prefix of the hostname is passed to `explorer` search param
16+
// @see vscode-host/src/deth/commands/ethViewerCommands.ts
17+
if (hostname.endsWith(".deth.net") && !url.searchParams.get("explorer")) {
18+
url.searchParams.set("explorer", hostname.slice(0, -9));
19+
}
20+
21+
log("setting iframe src:", url.href);
22+
iframe.setAttribute("src", url.href);
23+
// @todo in the future, we should also listen for postMessage events when
24+
// the ECV app inside of the iframe updates the URL, but it's not happening
25+
// right now
26+
27+
void (function exposeFunctions() {
28+
let channel: MessageChannel;
29+
30+
const exposedFunctions: ExposedFunctions.Async = {
31+
/**
32+
* see VSCode's BrowserKeyboardMapperFactoryBase._getBrowserKeyMapping
33+
*/
34+
async getLayoutMap() {
35+
const keyboard = (navigator as any).keyboard as NavigatorKeyboard | null;
36+
37+
if (!keyboard) {
38+
throw new Error(
39+
'"navigator.keyboard" is not available — it should not be called from the editor'
40+
);
41+
}
42+
43+
const keyboardLayoutMap = await keyboard.getLayoutMap();
44+
// KeyboardLayoutMap can't be cloned, so it can't be sent with postMessage
45+
return new Map(keyboardLayoutMap);
46+
},
47+
async setTitle(title: string) {
48+
document.title = title;
49+
},
50+
};
51+
52+
type ExposedFunctionCall = {
53+
[P in keyof ExposedFunctions]: {
54+
type: P;
55+
args: Parameters<ExposedFunctions[P]>;
56+
};
57+
}[keyof ExposedFunctions];
58+
59+
iframe.addEventListener("load", function onLoad() {
60+
iframe.removeEventListener("load", onLoad);
61+
62+
channel = new MessageChannel();
63+
const iframeWindow = iframe.contentWindow!;
64+
65+
log("iframe loaded, passing channel port");
66+
// The code responsible for interaction with this port lies in
67+
// vscode-host/src/deth/in-iframe.ts
68+
iframeWindow.postMessage({ type: "port-open" }, "*", [channel.port2]);
69+
70+
channel.port1.start();
71+
channel.port1.onmessage = (event) => {
72+
if (event.data && "type" in event.data) {
73+
const data = event.data as ExposedFunctionCall;
74+
const fun = exposedFunctions[data.type];
75+
76+
// This isn't very pretty, but it's a way to preserve a semblance of
77+
// and avoid a huge switch case with this command-dispatch pattern
78+
type Fun = UnionToIntersection<typeof fun>;
79+
type Args = Parameters<Fun>;
80+
81+
void (fun as Fun)(...(data.args as Args)).then((res) => {
82+
log(`returned from ${data.type}:`, res, event.data);
83+
84+
channel.port1.postMessage({ type: "result", fun: data.type, res });
85+
});
86+
} else {
87+
log("received unexpected message from iframe:", event.data);
88+
}
89+
};
90+
});
91+
})();
92+
93+
interface NavigatorKeyboard {
94+
getLayoutMap(): Promise<KeyboardLayoutMap>;
95+
}
96+
/** https://developer.mozilla.org/en-US/docs/Web/API/KeyboardLayoutMap */
97+
interface KeyboardLayoutMap
98+
extends Iterable<
99+
[label: string, key: string] /* example: ['BracketRight', ']'] */
100+
> {}

packages/entrypoint/tsconfig.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"compilerOptions": {
3+
"strict": true,
4+
"lib": ["DOM", "ESNext"],
5+
"target": "ESNext",
6+
"moduleResolution": "Node",
7+
"skipLibCheck": true
8+
},
9+
"include": ["src/**/*.ts"]
10+
}

0 commit comments

Comments
 (0)