cmd/tsconnect: extract NPM package for reusing in other projects

`src/` is broken up into several subdirectories:
- `lib/` and `types`/ for shared code and type definitions (more code
  will be moved here)
- `app/` for the existing Preact-app
- `pkg/` for the new NPM package

A new `build-pkg` esbuild-based command is added to generate the files
for the NPM package. To generate type definitions (something that esbuild
does not do), we set up `dts-bundle-generator`.

Includes additional cleanups to the Wasm type definitions (we switch to
string literals for enums, since exported const enums are hard to use
via packages).

Also allows the control URL to be set a runtime (in addition to the
current build option), so that we don't have to rebuild the package
for dev vs. prod use.

Updates #5415

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
This commit is contained in:
Mihai Parparita
2022-08-23 18:05:23 -07:00
committed by Mihai Parparita
parent 472529af38
commit 1a093ef482
24 changed files with 304 additions and 83 deletions

View File

@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
import { render, Component } from "preact"
import { IPNState } from "./wasm_js"
import { URLDisplay } from "./url-display"
import { Header } from "./header"
import { GoPanicDisplay } from "./go-panic-display"
@@ -18,7 +17,7 @@ type AppState = {
}
class App extends Component<{}, AppState> {
state: AppState = { ipnState: IPNState.NoState }
state: AppState = { ipnState: "NoState" }
#goPanicTimeout?: number
render() {
@@ -37,7 +36,7 @@ class App extends Component<{}, AppState> {
}
let machineAuthInstructions
if (ipnState === IPNState.NeedsMachineAuth) {
if (ipnState === "NeedsMachineAuth") {
machineAuthInstructions = (
<div class="container mx-auto px-4 text-center">
An administrator needs to authorize this device.
@@ -46,7 +45,7 @@ class App extends Component<{}, AppState> {
}
let ssh
if (ipn && ipnState === IPNState.Running && netMap) {
if (ipn && ipnState === "Running" && netMap) {
ssh = <SSH netMap={netMap} ipn={ipn} />
}
@@ -77,9 +76,9 @@ class App extends Component<{}, AppState> {
handleIPNState = (state: IPNState) => {
const { ipn } = this.state
this.setState({ ipnState: state })
if (state == IPNState.NeedsLogin) {
if (state === "NeedsLogin") {
ipn?.login()
} else if ([IPNState.Running, IPNState.NeedsMachineAuth].includes(state)) {
} else if (["Running", "NeedsMachineAuth"].includes(state)) {
this.setState({ browseToURL: undefined })
}
}

View File

@@ -2,13 +2,11 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
import { IPNState } from "./wasm_js"
export function Header({ state, ipn }: { state: IPNState; ipn?: IPN }) {
const stateText = STATE_LABELS[state]
let logoutButton
if (state === IPNState.Running) {
if (state === "Running") {
logoutButton = (
<button
class="button bg-gray-500 border-gray-500 text-white hover:bg-gray-600 hover:border-gray-600 ml-2 font-bold"
@@ -30,11 +28,11 @@ export function Header({ state, ipn }: { state: IPNState; ipn?: IPN }) {
}
const STATE_LABELS = {
[IPNState.NoState]: "Initializing…",
[IPNState.InUseOtherUser]: "In-use by another user",
[IPNState.NeedsLogin]: "Needs login",
[IPNState.NeedsMachineAuth]: "Needs authorization",
[IPNState.Stopped]: "Stopped",
[IPNState.Starting]: "Starting…",
[IPNState.Running]: "Running",
NoState: "Initializing…",
InUseOtherUser: "In-use by another user",
NeedsLogin: "Needs login",
NeedsMachineAuth: "Needs authorization",
Stopped: "Stopped",
Starting: "Starting…",
Running: "Running",
} as const

View File

@@ -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 "./wasm_exec"
import "../wasm_exec"
import wasmUrl from "./main.wasm"
import { sessionStateStorage } from "./js-state-store"
import { sessionStateStorage } from "../lib/js-state-store"
import { renderApp } from "./app"
async function main() {

View File

@@ -0,0 +1,9 @@
/* 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. */
@import "xterm/css/xterm.css";
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -0,0 +1,39 @@
// 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.
// Type definitions need to be manually imported for dts-bundle-generator to
// discover them.
/// <reference path="../types/esbuild.d.ts" />
/// <reference path="../types/wasm_js.d.ts" />
import "../wasm_exec"
import wasmURL from "./main.wasm"
/**
* Superset of the IPNConfig type, with additional configuration that is
* needed for the package to function.
*/
type IPNPackageConfig = IPNConfig & {
// Auth key used to intitialize the Tailscale client (required)
authKey: string
// URL of the main.wasm file that is included in the page, if it is not
// accessible via a relative URL.
wasmURL?: string
// Funtion invoked if the Go process panics or unexpectedly exits.
panicHandler: (err: string) => void
}
export async function createIPN(config: IPNPackageConfig): Promise<IPN> {
const go = new Go()
const wasmInstance = await WebAssembly.instantiateStreaming(
fetch(config.wasmURL ?? wasmURL),
go.importObject
)
// The Go process should never exit, if it does then it's an unhandled panic.
go.run(wasmInstance.instance).then(() =>
config.panicHandler("Unexpected shutdown")
)
return newIPN(config)
}

View File

@@ -4,8 +4,7 @@
/**
* @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)
* module.
*/
declare global {
@@ -26,9 +25,7 @@ declare global {
onDone: () => void
}
): IPNSSHSession
fetch(
url: string
): Promise<{
fetch(url: string): Promise<{
status: number
statusText: string
text: () => Promise<string>
@@ -48,6 +45,7 @@ declare global {
type IPNConfig = {
stateStorage?: IPNStateStorage
authKey?: string
controlURL?: string
}
type IPNCallbacks = {
@@ -77,23 +75,23 @@ declare global {
online?: boolean
tailscaleSSHEnabled: boolean
}
/** Mirrors values from ipn/backend.go */
type IPNState =
| "NoState"
| "InUseOtherUser"
| "NeedsLogin"
| "NeedsMachineAuth"
| "Stopped"
| "Starting"
| "Running"
/** Mirrors values from MachineStatus in tailcfg.go */
type IPNMachineStatus =
| "MachineUnknown"
| "MachineUnauthorized"
| "MachineAuthorized"
| "MachineInvalid"
}
/** 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,
}
export {}