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
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"]
|
||||||
|
}
|
|
@ -341,7 +341,6 @@ 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"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -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…
Reference in New Issue