Browse Source

place all compiled assets in build folder

pull/270/head
butlerx 5 years ago
parent
commit
12b2432c21
No known key found for this signature in database GPG Key ID: B37CA765BAA89170
  1. 2
      .eslintignore
  2. 20
      .eslintrc.json
  3. 4
      containers/wetty/Dockerfile
  4. 22
      package.json
  5. 0
      src/assets/favicon.ico
  6. 0
      src/assets/scss/options.scss
  7. 0
      src/assets/scss/overlay.scss
  8. 0
      src/assets/scss/styles.scss
  9. 0
      src/assets/scss/terminal.scss
  10. 0
      src/assets/scss/variables.scss
  11. 6
      src/buffer.ts
  12. 12
      src/client/client/options.ts
  13. 5
      src/client/dev.ts
  14. 161
      src/client/index.ts
  15. 3
      src/client/shared/elements.ts
  16. 74
      src/client/wetty.ts
  17. 13
      src/client/wetty/clipboard.ts
  18. 6
      src/client/wetty/disconnect.ts
  19. 0
      src/client/wetty/download.spec.ts
  20. 75
      src/client/wetty/download.ts
  21. 4
      src/client/wetty/mobile.ts
  22. 51
      src/client/wetty/options.ts
  23. 2
      src/client/wetty/socket.ts
  24. 1
      src/server.ts
  25. 27
      src/server/socketServer.ts
  26. 24
      src/server/socketServer/html.ts
  27. 1
      src/shared/env.ts
  28. 6
      src/shared/logger.ts
  29. 2
      tsconfig.json

2
.eslintignore

@ -3,4 +3,4 @@ node_modules/
lib
public/
*hterm*
web_modules
build

20
.eslintrc.json

@ -1,6 +1,9 @@
{
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint", "prettier"],
"plugins": [
"@typescript-eslint",
"prettier"
],
"env": {
"es6": true,
"node": true,
@ -14,8 +17,14 @@
"prettier/@typescript-eslint"
],
"rules": {
"linebreak-style": ["error", "unix"],
"arrow-parens": ["error", "as-needed"],
"linebreak-style": [
"error",
"unix"
],
"arrow-parens": [
"error",
"as-needed"
],
"no-param-reassign": [
"error",
{
@ -61,7 +70,10 @@
"settings": {
"import/resolver": {
"node": {
"extensions": [".ts", ".js"]
"extensions": [
".ts",
".js"
]
}
}
}

4
containers/wetty/Dockerfile

@ -1,4 +1,4 @@
FROM node:dubnium-alpine as builder
FROM node:current-alpine as builder
RUN apk add -U build-base python
WORKDIR /usr/src/app
COPY . /usr/src/app
@ -6,7 +6,7 @@ RUN yarn && \
yarn build && \
yarn install --production --ignore-scripts --prefer-offline
FROM node:dubnium-alpine
FROM node:current-alpine
LABEL maintainer="butlerx@notthe.cloud"
WORKDIR /usr/src/app
ENV NODE_ENV=production

22
package.json

@ -5,22 +5,20 @@
"homepage": "https://github.com/butlerx/wetty",
"license": "MIT",
"type": "module",
"main": "./lib/main.js",
"module": "./lib/server.js",
"main": "./build/main.js",
"module": "./build/server.js",
"files": [
"lib/",
"assets/css"
"build/"
],
"scripts": {
"build": "snowpack build && tsc -p tsconfig.json",
"contributor": "all-contributors",
"dev": "NODE_ENV=development yarn build && concurrently --kill-others --success first \"tsc -p tsconfig.json \" \"snowpack dev\" \"nodemon .\"",
"lint": "eslint --ext .ts,.js .",
"prepublishOnly": "NODE_ENV=production yarn build",
"build": "snowpack build",
"dev": "snowpack dev",
"prepublishOnly": "snowpack build",
"start": "NODE_ENV=production node .",
"lint": "eslint --ext .ts,.js .",
"contributor": "all-contributors",
"test": "mocha -r babel-register-ts src/**/*.spec.ts",
"postinstall": "snowpack install",
"clean": "rm -rf assets/css build lib web_modules node_modules"
"clean": "bash build.sh --clean"
},
"repository": {
"type": "git",
@ -75,7 +73,7 @@
[
"@snowpack/plugin-run-script",
{
"cmd": "sass assets/scss:assets/css --load-path=node_modules -s compressed --no-source-map",
"cmd": "bash build.sh",
"watch": "$1 --watch"
}
]

0
assets/favicon.ico → src/assets/favicon.ico

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 165 KiB

0
assets/scss/options.scss → src/assets/scss/options.scss

0
assets/scss/overlay.scss → src/assets/scss/overlay.scss

0
assets/scss/styles.scss → src/assets/scss/styles.scss

0
assets/scss/terminal.scss → src/assets/scss/terminal.scss

0
assets/scss/variables.scss → src/assets/scss/variables.scss

6
src/buffer.ts

@ -3,13 +3,13 @@ import { createInterface } from 'readline';
ask('Enter your username');
function ask(question: string): Promise<string> {
const r = createInterface({
const rlp = createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise(resolve => {
r.question(`${question}: `, answer => {
r.close();
rlp.question(`${question}: `, answer => {
rlp.close();
resolve(answer);
});
});

12
src/client/client/options.ts

@ -1,12 +0,0 @@
import { isUndefined } from '../../web_modules/lodash.js';
export function loadOptions(): object {
const defaultOptions = { fontSize: 14 };
try {
return isUndefined(localStorage.options)
? defaultOptions
: JSON.parse(localStorage.options);
} catch {
return defaultOptions;
}
}

5
src/client/dev.ts

@ -0,0 +1,5 @@
caches.keys().then(cacheNames => {
cacheNames.forEach(cacheName => {
caches.delete(cacheName);
});
});

161
src/client/index.ts

@ -1,161 +0,0 @@
import { Terminal } from '../web_modules/xterm.js';
import { isNull } from '../web_modules/lodash.js';
import { FitAddon } from '../web_modules/xterm-addon-fit.js';
import {
dom,
library,
} from '../web_modules/@fortawesome/fontawesome-svg-core.js';
import { faCogs } from '../web_modules/@fortawesome/free-solid-svg-icons.js';
import Toastify from '../web_modules/toastify-js.js';
import fileType from '../web_modules/file-type.js';
import { FileDownloader } from './client/download.js';
import { copySelected, copyShortcut } from './client/copyToClipboard.js';
import { disconnect } from './client/disconnect.js';
import { loadOptions } from './client/options.js';
import { mobileKeyboard } from './client/mobile.js';
import { overlay, terminal } from './shared/elements.js';
import { socket } from './client/socket.js';
import { verifyPrompt } from './shared/verify.js';
// Setup for fontawesome
library.add(faCogs);
dom.watch();
socket.on('connect', () => {
const term = new Terminal();
if (isNull(terminal)) return;
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
term.open(terminal);
const resize = (): void => {
fitAddon.fit();
socket.emit('resize', { cols: term.cols, rows: term.rows });
};
const options = loadOptions();
Object.entries(options).forEach(([key, value]) => {
term.setOption(key, value);
});
const code = JSON.stringify(options, null, 2);
const editor = document.querySelector('#options .editor');
if (!isNull(editor)) {
editor.value = code;
editor.addEventListener('keyup', () => {
try {
const updated = JSON.parse(editor.value);
const updatedCode = JSON.stringify(updated, null, 2);
editor.value = updatedCode;
editor.classList.remove('error');
localStorage.options = updatedCode;
Object.keys(updated).forEach(key => {
const value = updated[key];
term.setOption(key, value);
});
resize();
} catch {
// skip
editor.classList.add('error');
}
});
const toggle = document.querySelector('#options .toggler');
const optionsElem = document.getElementById('options');
if (!isNull(toggle) && !isNull(optionsElem)) {
toggle.addEventListener('click', e => {
optionsElem.classList.toggle('opened');
e.preventDefault();
});
}
}
if (!isNull(overlay)) overlay.style.display = 'none';
window.addEventListener('beforeunload', verifyPrompt, false);
term.attachCustomKeyEventHandler(copyShortcut);
document.addEventListener(
'mouseup',
() => {
if (term.hasSelection()) copySelected(term.getSelection());
},
false,
);
window.onresize = resize;
resize();
term.focus();
mobileKeyboard();
const fileDownloader = new FileDownloader((bufferCharacters: string) => {
let fileCharacters = bufferCharacters;
// Try to decode it as base64, if it fails we assume it's not base64
try {
fileCharacters = window.atob(fileCharacters);
} catch (err) {
// Assuming it's not base64...
}
const bytes = new Uint8Array(fileCharacters.length);
for (let i = 0; i < fileCharacters.length; i += 1) {
bytes[i] = fileCharacters.charCodeAt(i);
}
let mimeType = 'application/octet-stream';
let fileExt = '';
const typeData = fileType(bytes);
if (typeData) {
mimeType = typeData.mime;
fileExt = typeData.ext;
}
// Check if the buffer is ASCII
// Ref: https://stackoverflow.com/a/14313213
// eslint-disable-next-line no-control-regex
else if (/^[\x00-\xFF]*$/.test(fileCharacters)) {
mimeType = 'text/plain';
fileExt = 'txt';
}
const fileName = `file-${new Date()
.toISOString()
.split('.')[0]
.replace(/-/g, '')
.replace('T', '')
.replace(/:/g, '')}${fileExt ? `.${fileExt}` : ''}`;
const blob = new Blob([new Uint8Array(bytes.buffer)], {
type: mimeType,
});
const blobUrl = URL.createObjectURL(blob);
Toastify({
text: `Download ready: <a href="${blobUrl}" target="_blank" download="${fileName}">${fileName}</a>`,
duration: 10000,
newWindow: true,
gravity: 'bottom',
position: 'right',
backgroundColor: '#fff',
stopOnFocus: true,
}).showToast();
});
term.onData((data: string) => {
socket.emit('input', data);
});
term.onResize((size: string) => {
socket.emit('resize', size);
});
socket
.on('data', (data: string) => {
const remainingData = fileDownloader.buffer(data);
if (remainingData) {
term.write(remainingData);
}
})
.on('login', () => {
term.writeln('');
resize();
})
.on('logout', disconnect)
.on('disconnect', disconnect)
.on('error', (err: string | null) => {
if (err) disconnect(err);
});
});

3
src/client/shared/elements.ts

@ -1,2 +1,5 @@
export const overlay = document.getElementById('overlay');
export const terminal = document.getElementById('terminal');
export const editor = document.querySelector(
'#options .editor',
) as HTMLInputElement;

74
src/client/wetty.ts

@ -0,0 +1,74 @@
import _ from 'lodash';
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { dom, library } from '@fortawesome/fontawesome-svg-core';
import { faCogs } from '@fortawesome/free-solid-svg-icons';
import { FileDownloader } from './wetty/download.js';
import { copySelected, copyShortcut } from './wetty/clipboard.js';
import { disconnect } from './wetty/disconnect.js';
import { configureTerm } from './wetty/options.js';
import { mobileKeyboard } from './wetty/mobile.js';
import { overlay, terminal } from './shared/elements.js';
import { socket } from './wetty/socket.js';
import { verifyPrompt } from './shared/verify.js';
// Setup for fontawesome
library.add(faCogs);
dom.watch();
socket.on('connect', () => {
const term = new Terminal();
if (_.isNull(terminal)) return;
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
term.open(terminal);
const resize = (): void => {
fitAddon.fit();
socket.emit('resize', { cols: term.cols, rows: term.rows });
};
configureTerm(term, resize);
if (!_.isNull(overlay)) overlay.style.display = 'none';
window.addEventListener('beforeunload', verifyPrompt, false);
term.attachCustomKeyEventHandler(copyShortcut);
document.addEventListener(
'mouseup',
event => {
if (term.hasSelection()) copySelected(event, term.getSelection());
},
false,
);
window.onresize = resize;
resize();
term.focus();
mobileKeyboard();
const fileDownloader = new FileDownloader();
term.onData((data: string) => {
socket.emit('input', data);
});
term.onResize((size: { cols: number; rows: number }) => {
socket.emit('resize', size);
});
socket
.on('data', (data: string) => {
const remainingData = fileDownloader.buffer(data);
if (remainingData) {
term.write(remainingData);
}
})
.on('login', () => {
term.writeln('');
resize();
})
.on('logout', disconnect)
.on('disconnect', disconnect)
.on('error', (err: string | null) => {
if (err) disconnect(err);
});
});

13
src/client/client/copyToClipboard.ts → src/client/wetty/clipboard.ts

@ -1,7 +1,12 @@
// NOTE text selection on double click or select
export function copySelected(text: string): boolean {
if (window.clipboardData?.setData) {
window.clipboardData.setData('Text', text);
/**
Copy text selection to clipboard on double click or select
@param event - the event this function is bound to eg mouseup
@param text - the selected text to copy
@returns boolean to indicate success or failure
*/
export function copySelected(event: Event, text: string): boolean {
if (event.clipboardData?.setData) {
event.clipboardData.setData('Text', text);
return true;
}
if (

6
src/client/client/disconnect.ts → src/client/wetty/disconnect.ts

@ -1,11 +1,11 @@
import { isNull, isUndefined } from '../../web_modules/lodash.js';
import _ from 'lodash';
import { verifyPrompt } from '../shared/verify.js';
import { overlay } from '../shared/elements.js';
export function disconnect(reason: string): void {
if (isNull(overlay)) return;
if (_.isNull(overlay)) return;
overlay.style.display = 'block';
const msg = document.getElementById('msg');
if (!isUndefined(reason) && !isNull(msg)) msg.innerHTML = reason;
if (!_.isUndefined(reason) && !_.isNull(msg)) msg.innerHTML = reason;
window.removeEventListener('beforeunload', verifyPrompt, false);
}

0
src/client/client/download.spec.ts → src/client/wetty/download.spec.ts

75
src/client/client/download.ts → src/client/wetty/download.ts

@ -1,6 +1,60 @@
import Toastify from 'toastify-js';
import fileType from 'file-type';
const DEFAULT_FILE_BEGIN = '\u001b[5i';
const DEFAULT_FILE_END = '\u001b[4i';
function onCompleteFile(bufferCharacters: string): void {
let fileCharacters = bufferCharacters;
// Try to decode it as base64, if it fails we assume it's not base64
try {
fileCharacters = window.atob(fileCharacters);
} catch (err) {
// Assuming it's not base64...
}
const bytes = new Uint8Array(fileCharacters.length);
for (let i = 0; i < fileCharacters.length; i += 1) {
bytes[i] = fileCharacters.charCodeAt(i);
}
let mimeType = 'application/octet-stream';
let fileExt = '';
const typeData = fileType(bytes);
if (typeData) {
mimeType = typeData.mime;
fileExt = typeData.ext;
}
// Check if the buffer is ASCII
// Ref: https://stackoverflow.com/a/14313213
// eslint-disable-next-line no-control-regex
else if (/^[\x00-\xFF]*$/.test(fileCharacters)) {
mimeType = 'text/plain';
fileExt = 'txt';
}
const fileName = `file-${new Date()
.toISOString()
.split('.')[0]
.replace(/-/g, '')
.replace('T', '')
.replace(/:/g, '')}${fileExt ? `.${fileExt}` : ''}`;
const blob = new Blob([new Uint8Array(bytes.buffer)], {
type: mimeType,
});
const blobUrl = URL.createObjectURL(blob);
Toastify({
text: `Download ready: <a href="${blobUrl}" target="_blank" download="${fileName}">${fileName}</a>`,
duration: 10000,
newWindow: true,
gravity: 'bottom',
position: 'right',
backgroundColor: '#fff',
stopOnFocus: true,
}).showToast();
}
export class FileDownloader {
fileBuffer: string[];
fileBegin: string;
@ -9,15 +63,15 @@ export class FileDownloader {
onCompleteFileCallback: Function;
constructor(
onCompleteFileCallback: (file: string) => void,
onCompleteFileCallback: Function = onCompleteFile,
fileBegin: string = DEFAULT_FILE_BEGIN,
fileEnd: string = DEFAULT_FILE_END
fileEnd: string = DEFAULT_FILE_END,
) {
this.fileBuffer = [];
this.onCompleteFileCallback = onCompleteFileCallback;
this.fileBegin = fileBegin;
this.fileEnd = fileEnd;
this.partialFileBegin = '';
this.onCompleteFileCallback = onCompleteFileCallback;
}
bufferCharacter(character: string): string {
@ -69,13 +123,13 @@ export class FileDownloader {
this.fileBuffer.length >= this.fileBegin.length + this.fileEnd.length &&
this.fileBuffer.slice(-this.fileEnd.length).join('') === this.fileEnd
) {
this.onCompleteFile(
this.onCompleteFileCallback(
this.fileBuffer
.slice(
this.fileBegin.length,
this.fileBuffer.length - this.fileEnd.length
this.fileBuffer.length - this.fileEnd.length,
)
.join('')
.join(''),
);
this.fileBuffer = [];
}
@ -93,13 +147,6 @@ export class FileDownloader {
) {
return data;
}
return data
.split('')
.map(this.bufferCharacter.bind(this))
.join('');
}
onCompleteFile(bufferCharacters: string): void {
this.onCompleteFileCallback(bufferCharacters);
return data.split('').map(this.bufferCharacter.bind(this)).join('');
}
}

4
src/client/client/mobile.ts → src/client/wetty/mobile.ts

@ -1,8 +1,8 @@
import { isNull } from '../../web_modules/lodash.js';
import _ from 'lodash';
export function mobileKeyboard(): void {
const [screen] = document.getElementsByClassName('xterm-screen');
if (isNull(screen)) return;
if (_.isNull(screen)) return;
screen.setAttribute('contenteditable', 'true');
screen.setAttribute('spellcheck', 'false');
screen.setAttribute('autocorrect', 'false');

51
src/client/wetty/options.ts

@ -0,0 +1,51 @@
import _ from 'lodash';
import type { Terminal } from 'xterm';
import { editor } from '../shared/elements.js';
function loadOptions(): object {
const defaultOptions = { fontSize: 14 };
try {
return _.isUndefined(localStorage.options)
? defaultOptions
: JSON.parse(localStorage.options);
} catch {
return defaultOptions;
}
}
export function configureTerm(term: Terminal, resize: Function): void {
const options = loadOptions();
Object.entries(options).forEach(([key, value]) => {
term.setOption(key, value);
});
const config = JSON.stringify(options, null, 2);
if (!_.isNull(editor)) {
editor.value = config;
editor.addEventListener('keyup', () => {
try {
const updated = JSON.parse(editor.value);
const updatedConf = JSON.stringify(updated, null, 2);
editor.value = updatedConf;
editor.classList.remove('error');
localStorage.options = updatedConf;
Object.keys(updated).forEach(key => {
const value = updated[key];
term.setOption(key, value);
});
resize();
} catch {
// skip
editor.classList.add('error');
}
});
const toggle = document.querySelector('#options .toggler');
const optionsElem = document.getElementById('options');
if (!_.isNull(toggle) && !_.isNull(optionsElem)) {
toggle.addEventListener('click', e => {
optionsElem.classList.toggle('opened');
e.preventDefault();
});
}
}
}

2
src/client/client/socket.ts → src/client/wetty/socket.ts

@ -1,4 +1,4 @@
import io from '../../web_modules/socket.io-client.js';
import io from 'socket.io-client';
const userRegex = new RegExp('ssh/[^/]+$');
export const trim = (str: string): string => str.replace(/\/*$/, '');

1
src/server.ts

@ -19,6 +19,7 @@ import {
/**
* Starts WeTTy Server
* @name startServer
* @returns Promise that resolves SocketIO server
*/
export async function startServer(
ssh: SSH = sshDefault,

27
src/server/socketServer.ts

@ -5,7 +5,6 @@ import helmet from 'helmet';
import http from 'http';
import https from 'https';
import isUndefined from 'lodash/isUndefined.js';
import sassMiddleware from 'node-sass-middleware';
import socket from 'socket.io';
import winston from 'express-winston';
import { join, resolve } from 'path';
@ -15,6 +14,8 @@ import { html } from './socketServer/html.js';
import { logger } from '../shared/logger.js';
const trim = (str: string): string => str.replace(/\/*$/, '');
const serveStatic = (path: string) =>
express.static(resolve(process.cwd(), 'build', path));
export function server(
{ base, port, host, title, bypassHelmet }: Server,
@ -32,28 +33,12 @@ export function server(
const app = express();
app
.use(
`${basePath}/web_modules`,
express.static(resolve(process.cwd(), 'web_modules')),
)
.use(
sassMiddleware({
src: resolve(process.cwd(), 'lib', 'client'),
dest: resolve(process.cwd(), 'assets'),
outputStyle: 'compressed',
log(severity: string, key: string, value: string) {
logger.log(severity, 'node-sass-middleware %s : %s', key, value);
},
}),
)
.use(`${basePath}/assets`, express.static(resolve(process.cwd(), 'assets')))
.use(
`${basePath}/client`,
express.static(resolve(process.cwd(), 'lib', 'client')),
)
.use(`${basePath}/web_modules`, serveStatic('web_modules'))
.use(`${basePath}/assets`, serveStatic('assets'))
.use(`${basePath}/client`, serveStatic('client'))
.use(winston.logger(logger))
.use(compression())
.use(favicon(join('assets', 'favicon.ico')));
.use(favicon(join('build', 'assets', 'favicon.ico')));
/* .use((req, res, next) => {
if (req.path.substr(-1) === '/' && req.path.length > 1)
res.redirect(

24
src/server/socketServer/html.ts

@ -1,9 +1,13 @@
import express from 'express';
import type express from 'express';
import { isDev } from '../../shared/env.js';
const jsFiles = isDev ? ['dev', 'wetty'] : ['wetty'];
const cssFiles = ['styles', 'options', 'overlay', 'terminal'];
const render = (
title: string,
css: string,
js: string,
css: string[],
js: string[],
): string => `<!doctype html>
<html lang="en">
<head>
@ -11,7 +15,7 @@ const render = (
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>${title}</title>
<link rel="stylesheet" href="${css}" />
${css.map(file => `<link rel="stylesheet" href="${file}" />`).join('\n')}
</head>
<body>
<div id="overlay">
@ -24,11 +28,13 @@ const render = (
<a class="toggler"
href="#"
alt="Toggle options"
><i class="fas fa-cogs" /></a>
><i class="fas fa-cogs"></i></a>
<textarea class="editor"></textarea>
</div>
<div id="terminal"></div>
<script type="module" src="${js}" />
${js
.map(file => `<script type="module" src="${file}"></script>`)
.join('\n')}
</body>
</html>`;
@ -37,5 +43,9 @@ export const html = (base: string, title: string) => (
res: express.Response,
) =>
res.send(
render(title, `${base}/assets/styles.css`, `${base}/client/index.js`),
render(
title,
cssFiles.map(css => `${base}/assets/css/${css}.css`),
jsFiles.map(js => `${base}/client/${js}.js`),
),
);

1
src/shared/env.ts

@ -0,0 +1 @@
export const isDev = process.env.NODE_ENV === 'development';

6
src/shared/logger.ts

@ -1,5 +1,7 @@
import winston from 'winston';
import { isDev } from './env.js';
const { combine, timestamp, label, simple, json, colorize } = winston.format;
const dev = combine(
@ -12,10 +14,10 @@ const dev = combine(
const prod = combine(label({ label: 'Wetty' }), timestamp(), json());
export const logger = winston.createLogger({
format: process.env.NODE_ENV === 'development' ? dev : prod,
format: isDev ? dev : prod,
transports: [
new winston.transports.Console({
level: process.env.NODE_ENV === 'development' ? 'debug' : 'info',
level: isDev ? 'debug' : 'info',
handleExceptions: true,
}),
],

2
tsconfig.json

@ -15,7 +15,7 @@
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"outDir": "./lib",
"outDir": "./build",
"removeComments": true,
"skipLibCheck": true,
"sourceMap": true,

Loading…
Cancel
Save