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 <mihai@tailscale.com>
pull/5172/head
Mihai Parparita 2 years ago committed by Mihai Parparita
parent 0a6aa75a2d
commit 389629258b

@ -36,7 +36,7 @@ func commonSetup(dev bool) (*esbuild.BuildOptions, error) {
} }
return &esbuild.BuildOptions{ 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}, Loader: map[string]esbuild.Loader{".wasm": esbuild.LoaderFile},
Outdir: *distDir, Outdir: *distDir,
Bundle: true, Bundle: true,

@ -2,9 +2,15 @@
"name": "@tailscale/ssh", "name": "@tailscale/ssh",
"version": "0.0.1", "version": "0.0.1",
"devDependencies": { "devDependencies": {
"@types/golang-wasm-exec": "^1.15.0",
"@types/qrcode": "^1.4.2",
"qrcode": "^1.5.0", "qrcode": "^1.5.0",
"typescript": "^4.7.4",
"xterm": "^4.18.0" "xterm": "^4.18.0"
}, },
"scripts": {
"lint": "tsc --noEmit"
},
"prettier": { "prettier": {
"semi": false, "semi": false,
"printWidth": 80 "printWidth": 80

@ -102,7 +102,7 @@ func generateServeIndex(distFS fs.FS) ([]byte, error) {
var entryPointsToDefaultDistPaths = map[string]string{ var entryPointsToDefaultDistPaths = map[string]string{
"src/index.css": "dist/index.css", "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) { func handleServeDist(w http.ResponseWriter, r *http.Request, distFS fs.FS) {

@ -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

@ -7,7 +7,7 @@ import wasmUrl from "./main.wasm"
import { notifyState, notifyNetMap, notifyBrowseToURL } from "./notifier" import { notifyState, notifyNetMap, notifyBrowseToURL } from "./notifier"
import { sessionStateStorage } from "./js-state-store" import { sessionStateStorage } from "./js-state-store"
const go = new window.Go() const go = new Go()
WebAssembly.instantiateStreaming( WebAssembly.instantiateStreaming(
fetch(`./dist/${wasmUrl}`), fetch(`./dist/${wasmUrl}`),
go.importObject go.importObject

@ -2,11 +2,9 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // 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) { setState(id, value) {
window.sessionStorage[`ipn-state-${id}`] = value window.sessionStorage[`ipn-state-${id}`] = value
}, },

@ -2,9 +2,9 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // 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) { if (loginNode) {
loginNode.remove() loginNode.remove()
} }
@ -16,7 +16,7 @@ export async function showLoginURL(url) {
loginNode.appendChild(linkNode) loginNode.appendChild(linkNode)
try { try {
const dataURL = await QRCode.toDataURL(url, { width: 512 }) const dataURL = await qrcode.toDataURL(url, { width: 512 })
const imageNode = document.createElement("img") const imageNode = document.createElement("img")
imageNode.src = dataURL imageNode.src = dataURL
imageNode.width = 256 imageNode.width = 256
@ -41,9 +41,9 @@ export function hideLoginURL() {
loginNode = undefined loginNode = undefined
} }
let loginNode let loginNode: HTMLDivElement | undefined
export function showLogoutButton(ipn) { export function showLogoutButton(ipn: IPN) {
if (logoutButtonNode) { if (logoutButtonNode) {
logoutButtonNode.remove() logoutButtonNode.remove()
} }
@ -57,7 +57,8 @@ export function showLogoutButton(ipn) {
}, },
{ once: true } { once: true }
) )
document.getElementById("header").appendChild(logoutButtonNode) const headerNode = document.getElementById("header") as HTMLDivElement
headerNode.appendChild(logoutButtonNode)
} }
export function hideLogoutButton() { export function hideLogoutButton() {
@ -68,4 +69,4 @@ export function hideLogoutButton() {
logoutButtonNode = undefined logoutButtonNode = undefined
} }
let logoutButtonNode let logoutButtonNode: HTMLButtonElement | undefined

@ -9,60 +9,50 @@ import {
hideLogoutButton, hideLogoutButton,
} from "./login" } from "./login"
import { showSSHPeers, hideSSHPeers } from "./ssh" import { showSSHPeers, hideSSHPeers } from "./ssh"
import { IPNState } from "./wasm_js"
/** /**
* @fileoverview Notification callback functions (bridged from ipn.Notify) * @fileoverview Notification callback functions (bridged from ipn.Notify)
*/ */
/** Mirrors values from ipn/backend.go */ export function notifyState(ipn: IPN, state: IPNState) {
const State = {
NoState: 0,
InUseOtherUser: 1,
NeedsLogin: 2,
NeedsMachineAuth: 3,
Stopped: 4,
Starting: 5,
Running: 6,
}
export function notifyState(ipn, state) {
let stateLabel let stateLabel
switch (state) { switch (state) {
case State.NoState: case IPNState.NoState:
stateLabel = "Initializing…" stateLabel = "Initializing…"
break break
case State.InUseOtherUser: case IPNState.InUseOtherUser:
stateLabel = "In-use by another user" stateLabel = "In-use by another user"
break break
case State.NeedsLogin: case IPNState.NeedsLogin:
stateLabel = "Needs Login" stateLabel = "Needs Login"
hideLogoutButton() hideLogoutButton()
hideSSHPeers() hideSSHPeers()
ipn.login() ipn.login()
break break
case State.NeedsMachineAuth: case IPNState.NeedsMachineAuth:
stateLabel = "Needs authorization" stateLabel = "Needs authorization"
break break
case State.Stopped: case IPNState.Stopped:
stateLabel = "Stopped" stateLabel = "Stopped"
hideLogoutButton() hideLogoutButton()
hideSSHPeers() hideSSHPeers()
break break
case State.Starting: case IPNState.Starting:
stateLabel = "Starting…" stateLabel = "Starting…"
break break
case State.Running: case IPNState.Running:
stateLabel = "Running" stateLabel = "Running"
hideLoginURL() hideLoginURL()
showLogoutButton(ipn) showLogoutButton(ipn)
break break
} }
const stateNode = document.getElementById("state") const stateNode = document.getElementById("state") as HTMLDivElement
stateNode.textContent = stateLabel ?? "" stateNode.textContent = stateLabel ?? ""
} }
export function notifyNetMap(ipn, netMapStr) { export function notifyNetMap(ipn: IPN, netMapStr: string) {
const netMap = JSON.parse(netMapStr) const netMap = JSON.parse(netMapStr) as IPNNetMap
if (DEBUG) { if (DEBUG) {
console.log("Received net map: " + JSON.stringify(netMap, null, 2)) console.log("Received net map: " + JSON.stringify(netMap, null, 2))
} }
@ -70,6 +60,6 @@ export function notifyNetMap(ipn, netMapStr) {
showSSHPeers(netMap.peers, ipn) showSSHPeers(netMap.peers, ipn)
} }
export function notifyBrowseToURL(ipn, url) { export function notifyBrowseToURL(ipn: IPN, url: string) {
showLoginURL(url) showLoginURL(url)
} }

@ -4,8 +4,8 @@
import { Terminal } from "xterm" import { Terminal } from "xterm"
export function showSSHPeers(peers, ipn) { export function showSSHPeers(peers: IPNNetMapPeerNode[], ipn: IPN) {
const peersNode = document.getElementById("peers") const peersNode = document.getElementById("peers") as HTMLDivElement
peersNode.innerHTML = "" peersNode.innerHTML = ""
const sshPeers = peers.filter((p) => p.tailscaleSSHEnabled) const sshPeers = peers.filter((p) => p.tailscaleSSHEnabled)
@ -35,11 +35,11 @@ export function showSSHPeers(peers, ipn) {
} }
export function hideSSHPeers() { export function hideSSHPeers() {
const peersNode = document.getElementById("peers") const peersNode = document.getElementById("peers") as HTMLDivElement
peersNode.innerHTML = "" peersNode.innerHTML = ""
} }
function ssh(hostname, ipn) { function ssh(hostname: string, ipn: IPN) {
const termContainerNode = document.createElement("div") const termContainerNode = document.createElement("div")
termContainerNode.className = "term-container" termContainerNode.className = "term-container"
document.body.appendChild(termContainerNode) document.body.appendChild(termContainerNode)
@ -56,7 +56,7 @@ function ssh(hostname, ipn) {
} }
}) })
let onDataHook let onDataHook: ((data: string) => void) | undefined
term.onData((e) => { term.onData((e) => {
onDataHook?.(e) onDataHook?.(e)
}) })

@ -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,
}

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2017",
"module": "ES2020",
"moduleResolution": "node",
"isolatedModules": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}

@ -339,11 +339,10 @@ type jsNetMap struct {
} }
type jsNetMapNode struct { type jsNetMapNode struct {
Name string `json:"name"` Name string `json:"name"`
Addresses []string `json:"addresses"` Addresses []string `json:"addresses"`
MachineStatus int `json:"machineStatus"` MachineKey string `json:"machineKey"`
MachineKey string `json:"machineKey"` NodeKey string `json:"nodeKey"`
NodeKey string `json:"nodeKey"`
} }
type jsNetMapSelfNode struct { type jsNetMapSelfNode struct {

@ -2,6 +2,23 @@
# yarn lockfile v1 # 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: ansi-regex@^5.0.1:
version "5.0.1" version "5.0.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" 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: dependencies:
ansi-regex "^5.0.1" 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: which-module@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"

Loading…
Cancel
Save