From 389629258b505b3c284e814db1e06e5973912c1a Mon Sep 17 00:00:00 2001 From: Mihai Parparita Date: Tue, 26 Jul 2022 15:45:52 -0700 Subject: [PATCH] cmd/tsconnect: switch to TypeScript Continues to use esbuild for development mode and building. Also includes a `yarn lint` script that uses tsc to do full type checking. Fixes #5138 Signed-off-by: Mihai Parparita --- cmd/tsconnect/common.go | 2 +- cmd/tsconnect/package.json | 6 ++ cmd/tsconnect/serve.go | 2 +- cmd/tsconnect/src/esbuild.d.ts | 15 ++++ cmd/tsconnect/src/{index.js => index.ts} | 2 +- .../{js-state-store.js => js-state-store.ts} | 6 +- cmd/tsconnect/src/{login.js => login.ts} | 15 ++-- .../src/{notifier.js => notifier.ts} | 36 +++----- cmd/tsconnect/src/{ssh.js => ssh.ts} | 10 +-- cmd/tsconnect/src/wasm_js.ts | 82 +++++++++++++++++++ cmd/tsconnect/tsconfig.json | 13 +++ cmd/tsconnect/wasm/wasm_js.go | 9 +- cmd/tsconnect/yarn.lock | 22 +++++ 13 files changed, 173 insertions(+), 47 deletions(-) create mode 100644 cmd/tsconnect/src/esbuild.d.ts rename cmd/tsconnect/src/{index.js => index.ts} (97%) rename cmd/tsconnect/src/{js-state-store.js => js-state-store.ts} (72%) rename cmd/tsconnect/src/{login.js => login.ts} (78%) rename cmd/tsconnect/src/{notifier.js => notifier.ts} (65%) rename cmd/tsconnect/src/{ssh.js => ssh.ts} (85%) create mode 100644 cmd/tsconnect/src/wasm_js.ts create mode 100644 cmd/tsconnect/tsconfig.json diff --git a/cmd/tsconnect/common.go b/cmd/tsconnect/common.go index c085996fe..eb47a0a5a 100644 --- a/cmd/tsconnect/common.go +++ b/cmd/tsconnect/common.go @@ -36,7 +36,7 @@ func commonSetup(dev bool) (*esbuild.BuildOptions, error) { } return &esbuild.BuildOptions{ - EntryPoints: []string{"src/index.js", "src/index.css"}, + EntryPoints: []string{"src/index.ts", "src/index.css"}, Loader: map[string]esbuild.Loader{".wasm": esbuild.LoaderFile}, Outdir: *distDir, Bundle: true, diff --git a/cmd/tsconnect/package.json b/cmd/tsconnect/package.json index 15151ad6c..57eb6f132 100644 --- a/cmd/tsconnect/package.json +++ b/cmd/tsconnect/package.json @@ -2,9 +2,15 @@ "name": "@tailscale/ssh", "version": "0.0.1", "devDependencies": { + "@types/golang-wasm-exec": "^1.15.0", + "@types/qrcode": "^1.4.2", "qrcode": "^1.5.0", + "typescript": "^4.7.4", "xterm": "^4.18.0" }, + "scripts": { + "lint": "tsc --noEmit" + }, "prettier": { "semi": false, "printWidth": 80 diff --git a/cmd/tsconnect/serve.go b/cmd/tsconnect/serve.go index 65f14c267..da9878bf5 100644 --- a/cmd/tsconnect/serve.go +++ b/cmd/tsconnect/serve.go @@ -102,7 +102,7 @@ func generateServeIndex(distFS fs.FS) ([]byte, error) { var entryPointsToDefaultDistPaths = map[string]string{ "src/index.css": "dist/index.css", - "src/index.js": "dist/index.js", + "src/index.ts": "dist/index.js", } func handleServeDist(w http.ResponseWriter, r *http.Request, distFS fs.FS) { diff --git a/cmd/tsconnect/src/esbuild.d.ts b/cmd/tsconnect/src/esbuild.d.ts new file mode 100644 index 000000000..c245918b5 --- /dev/null +++ b/cmd/tsconnect/src/esbuild.d.ts @@ -0,0 +1,15 @@ +// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/** + * @fileoverview Type definitions for types generated by the esbuild build + * process. + */ + +declare module "*.wasm" { + const path: string + export default path +} + +declare const DEBUG: boolean diff --git a/cmd/tsconnect/src/index.js b/cmd/tsconnect/src/index.ts similarity index 97% rename from cmd/tsconnect/src/index.js rename to cmd/tsconnect/src/index.ts index f5095f873..ad8f125f3 100644 --- a/cmd/tsconnect/src/index.js +++ b/cmd/tsconnect/src/index.ts @@ -7,7 +7,7 @@ import wasmUrl from "./main.wasm" import { notifyState, notifyNetMap, notifyBrowseToURL } from "./notifier" import { sessionStateStorage } from "./js-state-store" -const go = new window.Go() +const go = new Go() WebAssembly.instantiateStreaming( fetch(`./dist/${wasmUrl}`), go.importObject diff --git a/cmd/tsconnect/src/js-state-store.js b/cmd/tsconnect/src/js-state-store.ts similarity index 72% rename from cmd/tsconnect/src/js-state-store.js rename to cmd/tsconnect/src/js-state-store.ts index c0b509d2b..98b5aef37 100644 --- a/cmd/tsconnect/src/js-state-store.js +++ b/cmd/tsconnect/src/js-state-store.ts @@ -2,11 +2,9 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -/** - * @fileoverview Callbacks used by jsStateStore to persist IPN state. - */ +/** @fileoverview Callbacks used by jsStateStore to persist IPN state. */ -export const sessionStateStorage = { +export const sessionStateStorage: IPNStateStorage = { setState(id, value) { window.sessionStorage[`ipn-state-${id}`] = value }, diff --git a/cmd/tsconnect/src/login.js b/cmd/tsconnect/src/login.ts similarity index 78% rename from cmd/tsconnect/src/login.js rename to cmd/tsconnect/src/login.ts index fe6901914..a17c8bd08 100644 --- a/cmd/tsconnect/src/login.js +++ b/cmd/tsconnect/src/login.ts @@ -2,9 +2,9 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -import QRCode from "qrcode" +import * as qrcode from "qrcode" -export async function showLoginURL(url) { +export async function showLoginURL(url: string) { if (loginNode) { loginNode.remove() } @@ -16,7 +16,7 @@ export async function showLoginURL(url) { loginNode.appendChild(linkNode) try { - const dataURL = await QRCode.toDataURL(url, { width: 512 }) + const dataURL = await qrcode.toDataURL(url, { width: 512 }) const imageNode = document.createElement("img") imageNode.src = dataURL imageNode.width = 256 @@ -41,9 +41,9 @@ export function hideLoginURL() { loginNode = undefined } -let loginNode +let loginNode: HTMLDivElement | undefined -export function showLogoutButton(ipn) { +export function showLogoutButton(ipn: IPN) { if (logoutButtonNode) { logoutButtonNode.remove() } @@ -57,7 +57,8 @@ export function showLogoutButton(ipn) { }, { once: true } ) - document.getElementById("header").appendChild(logoutButtonNode) + const headerNode = document.getElementById("header") as HTMLDivElement + headerNode.appendChild(logoutButtonNode) } export function hideLogoutButton() { @@ -68,4 +69,4 @@ export function hideLogoutButton() { logoutButtonNode = undefined } -let logoutButtonNode +let logoutButtonNode: HTMLButtonElement | undefined diff --git a/cmd/tsconnect/src/notifier.js b/cmd/tsconnect/src/notifier.ts similarity index 65% rename from cmd/tsconnect/src/notifier.js rename to cmd/tsconnect/src/notifier.ts index 71317f01e..fe215c35d 100644 --- a/cmd/tsconnect/src/notifier.js +++ b/cmd/tsconnect/src/notifier.ts @@ -9,60 +9,50 @@ import { hideLogoutButton, } from "./login" import { showSSHPeers, hideSSHPeers } from "./ssh" +import { IPNState } from "./wasm_js" /** * @fileoverview Notification callback functions (bridged from ipn.Notify) */ -/** Mirrors values from ipn/backend.go */ -const State = { - NoState: 0, - InUseOtherUser: 1, - NeedsLogin: 2, - NeedsMachineAuth: 3, - Stopped: 4, - Starting: 5, - Running: 6, -} - -export function notifyState(ipn, state) { +export function notifyState(ipn: IPN, state: IPNState) { let stateLabel switch (state) { - case State.NoState: + case IPNState.NoState: stateLabel = "Initializing…" break - case State.InUseOtherUser: + case IPNState.InUseOtherUser: stateLabel = "In-use by another user" break - case State.NeedsLogin: + case IPNState.NeedsLogin: stateLabel = "Needs Login" hideLogoutButton() hideSSHPeers() ipn.login() break - case State.NeedsMachineAuth: + case IPNState.NeedsMachineAuth: stateLabel = "Needs authorization" break - case State.Stopped: + case IPNState.Stopped: stateLabel = "Stopped" hideLogoutButton() hideSSHPeers() break - case State.Starting: + case IPNState.Starting: stateLabel = "Starting…" break - case State.Running: + case IPNState.Running: stateLabel = "Running" hideLoginURL() showLogoutButton(ipn) break } - const stateNode = document.getElementById("state") + const stateNode = document.getElementById("state") as HTMLDivElement stateNode.textContent = stateLabel ?? "" } -export function notifyNetMap(ipn, netMapStr) { - const netMap = JSON.parse(netMapStr) +export function notifyNetMap(ipn: IPN, netMapStr: string) { + const netMap = JSON.parse(netMapStr) as IPNNetMap if (DEBUG) { console.log("Received net map: " + JSON.stringify(netMap, null, 2)) } @@ -70,6 +60,6 @@ export function notifyNetMap(ipn, netMapStr) { showSSHPeers(netMap.peers, ipn) } -export function notifyBrowseToURL(ipn, url) { +export function notifyBrowseToURL(ipn: IPN, url: string) { showLoginURL(url) } diff --git a/cmd/tsconnect/src/ssh.js b/cmd/tsconnect/src/ssh.ts similarity index 85% rename from cmd/tsconnect/src/ssh.js rename to cmd/tsconnect/src/ssh.ts index 7604f4a17..0daab3d6c 100644 --- a/cmd/tsconnect/src/ssh.js +++ b/cmd/tsconnect/src/ssh.ts @@ -4,8 +4,8 @@ import { Terminal } from "xterm" -export function showSSHPeers(peers, ipn) { - const peersNode = document.getElementById("peers") +export function showSSHPeers(peers: IPNNetMapPeerNode[], ipn: IPN) { + const peersNode = document.getElementById("peers") as HTMLDivElement peersNode.innerHTML = "" const sshPeers = peers.filter((p) => p.tailscaleSSHEnabled) @@ -35,11 +35,11 @@ export function showSSHPeers(peers, ipn) { } export function hideSSHPeers() { - const peersNode = document.getElementById("peers") + const peersNode = document.getElementById("peers") as HTMLDivElement peersNode.innerHTML = "" } -function ssh(hostname, ipn) { +function ssh(hostname: string, ipn: IPN) { const termContainerNode = document.createElement("div") termContainerNode.className = "term-container" document.body.appendChild(termContainerNode) @@ -56,7 +56,7 @@ function ssh(hostname, ipn) { } }) - let onDataHook + let onDataHook: ((data: string) => void) | undefined term.onData((e) => { onDataHook?.(e) }) diff --git a/cmd/tsconnect/src/wasm_js.ts b/cmd/tsconnect/src/wasm_js.ts new file mode 100644 index 000000000..9cf18df98 --- /dev/null +++ b/cmd/tsconnect/src/wasm_js.ts @@ -0,0 +1,82 @@ +// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/** + * @fileoverview Type definitions for types exported by the wasm_js.go Go + * module. Not actually a .d.ts file so that we can use enums from it in + * esbuild's simplified TypeScript compiler (see https://github.com/evanw/esbuild/issues/2298#issuecomment-1146378367) + */ + +declare global { + function newIPN(config: IPNConfig): IPN + + interface IPN { + run(callbacks: IPNCallbacks): void + login(): void + logout(): void + ssh( + host: string, + writeFn: (data: string) => void, + setReadFn: (readFn: (data: string) => void) => void, + rows: number, + cols: number, + onDone: () => void + ): void + } + + interface IPNStateStorage { + setState(id: string, value: string): void + getState(id: string): string + } + + type IPNConfig = { + stateStorage?: IPNStateStorage + } + + type IPNCallbacks = { + notifyState: (state: IPNState) => void + notifyNetMap: (netMapStr: string) => void + notifyBrowseToURL: (url: string) => void + } + + type IPNNetMap = { + self: IPNNetMapSelfNode + peers: IPNNetMapPeerNode[] + } + + type IPNNetMapNode = { + name: string + addresses: string[] + machineKey: string + nodeKey: string + } + + type IPNNetMapSelfNode = IPNNetMapNode & { + machineStatus: IPNMachineStatus + } + + type IPNNetMapPeerNode = IPNNetMapNode & { + online: boolean + tailscaleSSHEnabled: boolean + } +} + +/** Mirrors values from ipn/backend.go */ +export const enum IPNState { + NoState = 0, + InUseOtherUser = 1, + NeedsLogin = 2, + NeedsMachineAuth = 3, + Stopped = 4, + Starting = 5, + Running = 6, +} + +/** Mirrors values from MachineStatus in tailcfg.go */ +export const enum IPNMachineStatus { + MachineUnknown = 0, + MachineUnauthorized = 1, + MachineAuthorized = 2, + MachineInvalid = 3, +} diff --git a/cmd/tsconnect/tsconfig.json b/cmd/tsconnect/tsconfig.json new file mode 100644 index 000000000..9a3ccb290 --- /dev/null +++ b/cmd/tsconnect/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2017", + "module": "ES2020", + "moduleResolution": "node", + "isolatedModules": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} diff --git a/cmd/tsconnect/wasm/wasm_js.go b/cmd/tsconnect/wasm/wasm_js.go index b28b618c6..ee835ffb6 100644 --- a/cmd/tsconnect/wasm/wasm_js.go +++ b/cmd/tsconnect/wasm/wasm_js.go @@ -339,11 +339,10 @@ type jsNetMap struct { } type jsNetMapNode struct { - Name string `json:"name"` - Addresses []string `json:"addresses"` - MachineStatus int `json:"machineStatus"` - MachineKey string `json:"machineKey"` - NodeKey string `json:"nodeKey"` + Name string `json:"name"` + Addresses []string `json:"addresses"` + MachineKey string `json:"machineKey"` + NodeKey string `json:"nodeKey"` } type jsNetMapSelfNode struct { diff --git a/cmd/tsconnect/yarn.lock b/cmd/tsconnect/yarn.lock index 8315985be..39d5caff4 100644 --- a/cmd/tsconnect/yarn.lock +++ b/cmd/tsconnect/yarn.lock @@ -2,6 +2,23 @@ # yarn lockfile v1 +"@types/golang-wasm-exec@^1.15.0": + version "1.15.0" + resolved "https://registry.yarnpkg.com/@types/golang-wasm-exec/-/golang-wasm-exec-1.15.0.tgz#d0aafbb2b0dc07eaf45dfb83bfb6cdd5b2b3c55c" + integrity sha512-FrL97mp7WW8LqNinVkzTVKOIQKuYjQqgucnh41+1vRQ+bf1LT8uh++KRf9otZPXsa6H1p8ruIGz1BmCGttOL6Q== + +"@types/node@*": + version "18.6.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.6.1.tgz#828e4785ccca13f44e2fb6852ae0ef11e3e20ba5" + integrity sha512-z+2vB6yDt1fNwKOeGbckpmirO+VBDuQqecXkgeIqDlaOtmKn6hPR/viQ8cxCfqLU4fTlvM3+YjM367TukWdxpg== + +"@types/qrcode@^1.4.2": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@types/qrcode/-/qrcode-1.4.2.tgz#7d7142d6fa9921f195db342ed08b539181546c74" + integrity sha512-7uNT9L4WQTNJejHTSTdaJhfBSCN73xtXaHFyBJ8TSwiLhe4PRuTue7Iph0s2nG9R/ifUaSnGhLUOZavlBEqDWQ== + dependencies: + "@types/node" "*" + ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" @@ -155,6 +172,11 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1: dependencies: ansi-regex "^5.0.1" +typescript@^4.7.4: + version "4.7.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" + integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== + which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"