html: update lint rules

This commit is contained in:
Shuanglei Tao
2019-06-23 11:05:09 +08:00
parent ae837910da
commit 86123ca7bc
11 changed files with 1106 additions and 432 deletions

View File

@@ -1,11 +1,11 @@
root = true
[*]
indent_style = space
indent_size = 4
charset = utf-8
trim_trailing_whitespace = true
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false

View File

@@ -13,34 +13,44 @@
"prestart": "gulp clean",
"start": "webpack-dev-server",
"build": "NODE_ENV=production webpack && gulp",
"lint": "tslint --project ."
"check": "gts check",
"fix": "gts fix"
},
"eslintConfig": {
"extends": "eslint-config-aerian"
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"src/**/*.ts": [
"gts fix",
"git add"
],
"src/**/*.scss": [
"scssfmt",
"git add"
]
},
"eslintIgnore": [
"build/*"
],
"devDependencies": {
"copy-webpack-plugin": "^5.0.3",
"css-loader": "^1.0.1",
"gts": "^1.0.0",
"gulp": "^4.0.2",
"gulp-clean": "^0.4.0",
"gulp-inline-source": "^4.0.0",
"html-webpack-plugin": "^3.2.0",
"husky": "^2.4.1",
"lint-staged": "^8.2.1",
"mini-css-extract-plugin": "^0.6.0",
"node-sass": "^4.12.0",
"optimize-css-assets-webpack-plugin": "^5.0.1",
"sass-loader": "^7.1.0",
"scssfmt": "^1.0.7",
"style-loader": "^0.23.1",
"terser-webpack-plugin": "^1.3.0",
"ts-loader": "^6.0.1",
"tslint": "^5.16.0",
"tslint-config-prettier": "^1.18.0",
"tslint-consistent-codestyle": "^1.15.1",
"tslint-eslint-rules": "^5.4.0",
"tslint-loader": "^3.5.4",
"tslint-react": "^4.0.0",
"typescript": "^3.4.5",
"webpack": "^4.32.2",
"webpack-cli": "^3.3.2",

View File

@@ -1,46 +1,51 @@
import { Component, h } from 'preact';
import { ITerminalOptions, ITheme } from 'xterm';
import Xterm from './terminal';
import { Xterm } from './terminal';
if ((module as any).hot) {
// tslint:disable-next-line:no-var-requires
require('preact/debug');
// tslint:disable-next-line:no-var-requires
require('preact/debug');
}
const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
const wsPath = window.location.pathname.endsWith('/') ? 'ws' : '/ws';
const url = [protocol, window.location.host, window.location.pathname, wsPath, window.location.search].join('');
const url = [
protocol,
window.location.host,
window.location.pathname,
wsPath,
window.location.search,
].join('');
const termOptions = {
fontSize: 13,
fontFamily: 'Menlo For Powerline,Consolas,Liberation Mono,Menlo,Courier,monospace',
theme: {
foreground: '#d2d2d2',
background: '#2b2b2b',
cursor: '#adadad',
black: '#000000',
red: '#d81e00',
green: '#5ea702',
yellow: '#cfae00',
blue: '#427ab3',
magenta: '#89658e',
cyan: '#00a7aa',
white: '#dbded8',
brightBlack: '#686a66',
brightRed: '#f54235',
brightGreen: '#99e343',
brightYellow: '#fdeb61',
brightBlue: '#84b0d8',
brightMagenta: '#bc94b7',
brightCyan: '#37e6e8',
brightWhite: '#f1f1f0'
} as ITheme
fontSize: 13,
fontFamily:
'Menlo For Powerline,Consolas,Liberation Mono,Menlo,Courier,monospace',
theme: {
foreground: '#d2d2d2',
background: '#2b2b2b',
cursor: '#adadad',
black: '#000000',
red: '#d81e00',
green: '#5ea702',
yellow: '#cfae00',
blue: '#427ab3',
magenta: '#89658e',
cyan: '#00a7aa',
white: '#dbded8',
brightBlack: '#686a66',
brightRed: '#f54235',
brightGreen: '#99e343',
brightYellow: '#fdeb61',
brightBlue: '#84b0d8',
brightMagenta: '#bc94b7',
brightCyan: '#37e6e8',
brightWhite: '#f1f1f0',
} as ITheme,
} as ITerminalOptions;
export default class App extends Component {
public render() {
return (
<Xterm id='terminal-container' url={url} options={termOptions} />
);
}
export class App extends Component {
render() {
return <Xterm id="terminal-container" url={url} options={termOptions} />;
}
}

View File

@@ -7,198 +7,201 @@ import { OverlayAddon } from './overlay';
import 'xterm/dist/xterm.css';
export interface IWindowWithTerminal extends Window {
term: Terminal;
tty_auth_token?: string;
export interface WindowExtended extends Window {
term: Terminal;
tty_auth_token?: string;
}
declare let window: IWindowWithTerminal;
declare let window: WindowExtended;
const enum Command {
// server side
OUTPUT = '0',
SET_WINDOW_TITLE = '1',
SET_PREFERENCES = '2',
SET_RECONNECT = '3',
// server side
OUTPUT = '0',
SET_WINDOW_TITLE = '1',
SET_PREFERENCES = '2',
SET_RECONNECT = '3',
// client side
INPUT = '0',
RESIZE_TERMINAL = '1'
// client side
INPUT = '0',
RESIZE_TERMINAL = '1',
}
interface Props {
id: string;
url: string;
options: ITerminalOptions;
id: string;
url: string;
options: ITerminalOptions;
}
export default class Xterm extends Component<Props> {
private textEncoder: TextEncoder;
private textDecoder: TextDecoder;
private container: HTMLElement;
private terminal: Terminal;
private fitAddon: FitAddon;
private overlayAddon: OverlayAddon;
private socket: WebSocket;
private title: string;
private autoReconnect: number;
private resizeTimeout: number;
export class Xterm extends Component<Props> {
private textEncoder: TextEncoder;
private textDecoder: TextDecoder;
private container: HTMLElement;
private terminal: Terminal;
private fitAddon: FitAddon;
private overlayAddon: OverlayAddon;
private socket: WebSocket;
private title: string;
private autoReconnect: number;
private resizeTimeout: number;
constructor(props) {
super(props);
constructor(props) {
super(props);
this.textEncoder = new TextEncoder();
this.textDecoder = new TextDecoder();
this.fitAddon = new FitAddon();
this.overlayAddon = new OverlayAddon();
this.textEncoder = new TextEncoder();
this.textDecoder = new TextDecoder();
this.fitAddon = new FitAddon();
this.overlayAddon = new OverlayAddon();
}
componentDidMount() {
this.openTerminal();
}
componentWillUnmount() {
this.socket.close();
this.terminal.dispose();
window.removeEventListener('resize', this.onWindowResize);
window.removeEventListener('beforeunload', this.onWindowUnload);
}
render({ id }: Props) {
return <div id={id} ref={c => (this.container = c)} />;
}
@bind
private onWindowResize() {
const { fitAddon } = this;
clearTimeout(this.resizeTimeout);
this.resizeTimeout = setTimeout(() => fitAddon.fit(), 250) as any;
}
private onWindowUnload(event: BeforeUnloadEvent): string {
const message = 'Close terminal? this will also terminate the command.';
event.returnValue = message;
return message;
}
@bind
private openTerminal() {
if (this.terminal) {
this.terminal.dispose();
}
public componentDidMount() {
this.openTerminal();
this.socket = new WebSocket(this.props.url, ['tty']);
this.terminal = new Terminal(this.props.options);
const { socket, terminal, container, fitAddon, overlayAddon } = this;
window.term = terminal;
socket.binaryType = 'arraybuffer';
socket.onopen = this.onSocketOpen;
socket.onmessage = this.onSocketData;
socket.onclose = this.onSocketClose;
terminal.loadAddon(fitAddon);
terminal.loadAddon(overlayAddon);
terminal.loadAddon(new WebLinksAddon());
terminal.onTitleChange(data => {
if (data && data !== '') {
document.title = data + ' | ' + this.title;
}
});
terminal.onData(this.onTerminalData);
terminal.onResize(this.onTerminalResize);
if (
document.queryCommandSupported &&
document.queryCommandSupported('copy')
) {
terminal.onSelectionChange(() => {
overlayAddon.showOverlay('\u2702', 200);
document.execCommand('copy');
});
}
terminal.open(container);
terminal.focus();
public componentWillUnmount() {
this.socket.close();
this.terminal.dispose();
window.addEventListener('resize', this.onWindowResize);
window.addEventListener('beforeunload', this.onWindowUnload);
}
window.removeEventListener('resize', this.onWindowResize);
window.removeEventListener('beforeunload', this.onWindowUnload);
@bind
private onSocketOpen() {
console.log('[ttyd] Websocket connection opened');
const { socket, textEncoder, fitAddon } = this;
const authToken = window.tty_auth_token;
socket.send(textEncoder.encode(JSON.stringify({ AuthToken: authToken })));
fitAddon.fit();
}
@bind
private onSocketClose(event: CloseEvent) {
console.log(`[ttyd] websocket connection closed with code: ${event.code}`);
const { overlayAddon, openTerminal, autoReconnect } = this;
overlayAddon.showOverlay('Connection Closed', null);
window.removeEventListener('beforeunload', this.onWindowUnload);
// 1008: POLICY_VIOLATION - Auth failure
if (event.code === 1008) {
window.location.reload();
}
public render({ id }: Props) {
return (
<div id={id} ref={(c) => this.container = c} />
);
// 1000: CLOSE_NORMAL
if (event.code !== 1000 && autoReconnect > 0) {
setTimeout(openTerminal, autoReconnect * 1000);
}
}
@bind
private onWindowResize() {
const { fitAddon } = this;
clearTimeout(this.resizeTimeout);
this.resizeTimeout = setTimeout(() => fitAddon.fit(), 250) as any;
}
@bind
private onSocketData(event: MessageEvent) {
const { terminal, textDecoder } = this;
private onWindowUnload(event: BeforeUnloadEvent): string {
const message = 'Close terminal? this will also terminate the command.';
event.returnValue = message;
return message;
}
const rawData = new Uint8Array(event.data);
const cmd = String.fromCharCode(rawData[0]);
const data = rawData.slice(1);
@bind
private openTerminal() {
if (this.terminal) {
this.terminal.dispose();
}
this.socket = new WebSocket(this.props.url, ['tty']);
this.terminal = new Terminal(this.props.options);
const { socket, terminal, container, fitAddon, overlayAddon } = this;
window.term = terminal;
socket.binaryType = 'arraybuffer';
socket.onopen = this.onSocketOpen;
socket.onmessage = this.onSocketData;
socket.onclose = this.onSocketClose;
terminal.loadAddon(fitAddon);
terminal.loadAddon(overlayAddon);
terminal.loadAddon(new WebLinksAddon());
terminal.onTitleChange((data) => {
if (data && data !== '') {
document.title = (data + ' | ' + this.title);
}
switch (cmd) {
case Command.OUTPUT:
terminal.writeUtf8(data);
break;
case Command.SET_WINDOW_TITLE:
this.title = textDecoder.decode(data.buffer);
document.title = this.title;
break;
case Command.SET_PREFERENCES:
const preferences = JSON.parse(textDecoder.decode(data.buffer));
Object.keys(preferences).forEach(key => {
console.log(`[ttyd] setting ${key}: ${preferences[key]}`);
terminal.setOption(key, preferences[key]);
});
terminal.onData(this.onTerminalData);
terminal.onResize(this.onTerminalResize);
if (document.queryCommandSupported && document.queryCommandSupported('copy')) {
terminal.onSelectionChange(() => {
overlayAddon.showOverlay('\u2702', 200);
document.execCommand('copy');
});
}
terminal.open(container);
terminal.focus();
window.addEventListener('resize', this.onWindowResize);
window.addEventListener('beforeunload', this.onWindowUnload);
break;
case Command.SET_RECONNECT:
this.autoReconnect = Number(textDecoder.decode(data.buffer));
console.log(`[ttyd] enabling reconnect: ${this.autoReconnect} seconds`);
break;
default:
console.warn(`[ttyd] unknown command: ${cmd}`);
break;
}
}
@bind
private onSocketOpen() {
console.log('[ttyd] Websocket connection opened');
const { socket, textEncoder, fitAddon } = this;
const authToken = window.tty_auth_token;
socket.send(textEncoder.encode(JSON.stringify({AuthToken: authToken})));
fitAddon.fit();
@bind
private onTerminalResize(size: { cols: number; rows: number }) {
const { overlayAddon, socket, textEncoder } = this;
if (socket.readyState === WebSocket.OPEN) {
const msg = JSON.stringify({ columns: size.cols, rows: size.rows });
socket.send(textEncoder.encode(Command.RESIZE_TERMINAL + msg));
}
setTimeout(() => {
overlayAddon.showOverlay(`${size.cols}x${size.rows}`);
}, 500);
}
@bind
private onSocketClose(event: CloseEvent) {
console.log(`[ttyd] websocket connection closed with code: ${event.code}`);
const { overlayAddon, openTerminal, autoReconnect } = this;
overlayAddon.showOverlay('Connection Closed', null);
window.removeEventListener('beforeunload', this.onWindowUnload);
// 1008: POLICY_VIOLATION - Auth failure
if (event.code === 1008) {
window.location.reload();
}
// 1000: CLOSE_NORMAL
if (event.code !== 1000 && autoReconnect > 0) {
setTimeout(openTerminal, autoReconnect * 1000);
}
}
@bind
private onSocketData(event: MessageEvent) {
const { terminal, textDecoder } = this;
const rawData = new Uint8Array(event.data);
const cmd = String.fromCharCode(rawData[0]);
const data = rawData.slice(1);
switch(cmd) {
case Command.OUTPUT:
terminal.writeUtf8(data);
break;
case Command.SET_WINDOW_TITLE:
this.title = textDecoder.decode(data.buffer);
document.title = this.title;
break;
case Command.SET_PREFERENCES:
const preferences = JSON.parse(textDecoder.decode(data.buffer));
Object.keys(preferences).forEach((key) => {
console.log(`[ttyd] setting ${key}: ${preferences[key]}`);
terminal.setOption(key, preferences[key]);
});
break;
case Command.SET_RECONNECT:
this.autoReconnect = parseInt(textDecoder.decode(data.buffer));
console.log(`[ttyd] enabling reconnect: ${this.autoReconnect} seconds`);
break;
default:
console.warn(`[ttyd] unknown command: ${cmd}`);
break;
}
}
@bind
private onTerminalResize(size: {cols: number, rows: number}) {
const { overlayAddon, socket, textEncoder } = this;
if (socket.readyState === WebSocket.OPEN) {
const msg = JSON.stringify({columns: size.cols, rows: size.rows});
socket.send(textEncoder.encode(Command.RESIZE_TERMINAL + msg));
}
setTimeout(() => {overlayAddon.showOverlay(`${size.cols}x${size.rows}`)}, 500);
}
@bind
private onTerminalData(data: string) {
const { socket, textEncoder } = this;
if (socket.readyState === WebSocket.OPEN) {
socket.send(textEncoder.encode(Command.INPUT + data));
}
@bind
private onTerminalData(data: string) {
const { socket, textEncoder } = this;
if (socket.readyState === WebSocket.OPEN) {
socket.send(textEncoder.encode(Command.INPUT + data));
}
}
}

View File

@@ -3,13 +3,13 @@
import { ITerminalAddon, Terminal } from 'xterm';
export class OverlayAddon implements ITerminalAddon {
private terminal: Terminal | undefined;
private overlayNode: HTMLElement | null;
private overlayTimeout: number | null;
private terminal: Terminal | undefined;
private overlayNode: HTMLElement | null;
private overlayTimeout: number | null;
constructor() {
this.overlayNode = document.createElement('div');
this.overlayNode.style.cssText = `border-radius: 15px;
constructor() {
this.overlayNode = document.createElement('div');
this.overlayNode.style.cssText = `border-radius: 15px;
font-size: xx-large;
opacity: 0.75;
padding: 0.2em 0.5em 0.2em 0.5em;
@@ -19,53 +19,57 @@ position: absolute;
-moz-user-select: none;
-moz-transition: opacity 180ms ease-in;`;
this.overlayNode.addEventListener('mousedown', (e) => {
e.preventDefault();
e.stopPropagation();
}, true);
this.overlayNode.addEventListener(
'mousedown',
e => {
e.preventDefault();
e.stopPropagation();
},
true
);
}
activate(terminal: Terminal): void {
this.terminal = terminal;
}
dispose(): void {}
showOverlay(msg: string, timeout?: number): void {
const { terminal, overlayNode } = this;
overlayNode.style.color = '#101010';
overlayNode.style.backgroundColor = '#f0f0f0';
overlayNode.textContent = msg;
overlayNode.style.opacity = '0.75';
if (!overlayNode.parentNode) {
terminal.element.appendChild(overlayNode);
}
public activate(terminal: Terminal): void {
this.terminal = terminal;
const divSize = terminal.element.getBoundingClientRect();
const overlaySize = overlayNode.getBoundingClientRect();
overlayNode.style.top = (divSize.height - overlaySize.height) / 2 + 'px';
overlayNode.style.left = (divSize.width - overlaySize.width) / 2 + 'px';
if (this.overlayTimeout) {
clearTimeout(this.overlayTimeout);
}
if (timeout === null) {
return;
}
public dispose(): void {}
public showOverlay(msg: string, timeout?: number): void {
const {terminal, overlayNode } = this;
overlayNode.style.color = '#101010';
overlayNode.style.backgroundColor = '#f0f0f0';
overlayNode.textContent = msg;
const self = this;
self.overlayTimeout = setTimeout(() => {
overlayNode.style.opacity = '0';
self.overlayTimeout = setTimeout(() => {
if (overlayNode.parentNode) {
overlayNode.parentNode.removeChild(overlayNode);
}
self.overlayTimeout = null;
overlayNode.style.opacity = '0.75';
if (!overlayNode.parentNode) {
terminal.element.appendChild(overlayNode);
}
const divSize = terminal.element.getBoundingClientRect();
const overlaySize = overlayNode.getBoundingClientRect();
overlayNode.style.top = (divSize.height - overlaySize.height) / 2 + 'px';
overlayNode.style.left = (divSize.width - overlaySize.width) / 2 + 'px';
if (this.overlayTimeout) {
clearTimeout(this.overlayTimeout);
}
if (timeout === null) {
return;
}
const self = this;
self.overlayTimeout = <number><any>setTimeout(() => {
overlayNode.style.opacity = '0';
self.overlayTimeout = <number><any>setTimeout(() => {
if (overlayNode.parentNode) {
overlayNode.parentNode.removeChild(overlayNode);
}
self.overlayTimeout = null;
overlayNode.style.opacity = '0.75';
}, 200);
}, timeout || 1500);
}
}, 200) as any;
}, timeout || 1500) as any;
}
}

View File

@@ -1,5 +1,5 @@
import { h, render } from 'preact';
import App from './components/app';
import { App } from './components/app';
import './style/index.scss';
render(<App />, document.body);

View File

@@ -1,17 +1,18 @@
html, body {
height: 100%;
min-height: 100%;
margin: 0;
overflow: hidden;
html,
body {
height: 100%;
min-height: 100%;
margin: 0;
overflow: hidden;
}
#terminal-container {
width: auto;
height: 100%;
margin: 0 auto;
padding: 0;
background-color: #2b2b2b;
.terminal {
padding: 5px;
}
width: auto;
height: 100%;
margin: 0 auto;
padding: 0;
background-color: #2b2b2b;
.terminal {
padding: 5px;
}
}

View File

@@ -1,11 +1,10 @@
{
"compilerOptions": {
"target": "ES5",
"module": "ESNext",
"target": "esnext",
"module": "es6",
"lib": [
"dom",
"es5",
"es2015.promise"
"es2017",
"dom"
],
"allowJs": true,
"jsx": "react",
@@ -13,7 +12,9 @@
"sourceMap": true,
"moduleResolution": "node",
"esModuleInterop": true,
"experimentalDecorators": true
"experimentalDecorators": true,
"noImplicitReturns": true,
"noUnusedParameters": true
},
"include": [
"src/**/*.tsx",

View File

@@ -1,52 +1,7 @@
{
"extends": [
"tslint:recommended",
"tslint-react",
"tslint-eslint-rules",
"tslint-consistent-codestyle",
"tslint-config-prettier"
],
"extends": "gts/tslint.json",
"rules": {
"interface-name": false,
"object-literal-sort-keys": false,
"no-empty-interface": false,
"no-submodule-imports": false,
"jsx-no-lambda": false,
"no-console": false,
"no-empty": false,
"radix": false,
"jsx-no-multiline-js": false,
"only-arrow-functions": [true, "allow-declarations"],
"ban-comma-operator": true,
"no-angle-bracket-type-assertion": false,
"no-arg": true,
"no-duplicate-case": true,
"no-empty-character-class": true,
"no-ex-assign": true,
"no-extra-boolean-cast": true,
"no-extra-semi": true,
"no-inner-declarations": true,
"no-invalid-regexp": true,
"no-irregular-whitespace": true,
"no-regex-spaces": true,
"no-sparse-arrays": true,
"no-unexpected-multiline": true,
"valid-jsdoc": true,
"valid-typeof": true,
"early-exit": { "severity": "warning" },
"no-collapsible-if": { "severity": "warning" },
"no-unnecessary-else": { "severity": "warning" },
"no-accessor-recursion": { "severity": "warning" },
"no-else-after-return": {
"severity": "warning",
"options": "allow-else-if"
},
"no-return-undefined": { "severity": "warning" },
"no-static-this": { "severity": "warning" },
"no-var-before-return": {
"severity": "warning",
"options": "allow-destructuring"
},
"prefer-const-enum": { "severity": "warning" }
"deprecation": false,
"no-any": false
}
}

File diff suppressed because it is too large Load Diff

2
src/index.html vendored

File diff suppressed because one or more lines are too long