This commit is contained in:
Eugene Pankov
2021-07-24 20:25:09 +02:00
parent 3de04221c2
commit 2b4d788c28
25 changed files with 1184 additions and 272 deletions

1
.gitignore vendored
View File

@@ -8,6 +8,7 @@ db.sqlite3
__pycache__
.vscode
build
build-server
*.d.ts
.env
yarn-error.log

0
build-server/.gitkeep Normal file
View File

View File

@@ -18,6 +18,7 @@
"@angular/forms": "^11.0.0",
"@angular/platform-browser": "^11.0.0",
"@angular/platform-browser-dynamic": "^11.0.0",
"@angular/platform-server": "^11.2.14",
"@angular/router": "^11.0.0",
"@fontsource/fira-code": "^4.5.0",
"@fortawesome/angular-fontawesome": "0.8",
@@ -40,6 +41,7 @@
"html-loader": "^2.1.2",
"html-webpack-plugin": "^5.3.2",
"js-yaml": "^4.1.0",
"mini-css-extract-plugin": "^2.1.0",
"ngx-toastr": "^14.0.0",
"node-sass": "^6.0.0",
"pug": "^3.0.2",
@@ -57,8 +59,16 @@
"typescript": "~4.1",
"val-loader": "^4.0.0",
"webpack": "^5.38.1",
"webpack-bundle-analyzer": "^4.4.2",
"webpack-cli": "^4.7.2",
"zone.js": "^0.11.4"
},
"dependencies": {}
"dependencies": {
"@nguniversal/express-engine": "^11.1.0",
"domino": "^2.1.6",
"express": "^4.17.1",
"express-http-proxy": "^1.6.2",
"mock-browser": "^0.92.14",
"source-map-support": "^0.5.19"
}
}

View File

@@ -7,6 +7,7 @@ import { FormsModule } from '@angular/forms'
import { RouterModule } from '@angular/router'
import { HttpClientModule, HttpClientXsrfModule } from '@angular/common/http'
import { ClipboardModule } from '@angular/cdk/clipboard'
import { TransferHttpCacheModule } from '@nguniversal/common'
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'
import { AppComponent } from './components/app.component'
import { MainComponent } from './components/main.component'
@@ -16,6 +17,8 @@ import { HomeComponent } from './components/home.component'
import { LoginComponent } from './components/login.component'
import { InstanceInfoResolver } from './api'
import '@fortawesome/fontawesome-svg-core/styles.css'
const ROUTES = [
{
path: '',
@@ -42,7 +45,10 @@ const ROUTES = [
@NgModule({
imports: [
BrowserModule,
BrowserModule.withServerTransition({
appId: 'tabby'
}),
TransferHttpCacheModule,
BrowserAnimationsModule,
CommonModule,
FormsModule,

27
src/app.server.module.ts Normal file
View File

@@ -0,0 +1,27 @@
import { HTTP_INTERCEPTORS } from '@angular/common/http'
import { NgModule } from '@angular/core'
import { ServerModule, ServerTransferStateModule } from '@angular/platform-server'
import { AppModule } from './app.module'
import { AppComponent } from './components/app.component'
import { UniversalInterceptor } from './ssr-interceptor'
@NgModule({
imports: [
// The AppServerModule should import your AppModule followed
// by the ServerModule from @angular/platform-server.
AppModule,
ServerModule,
ServerTransferStateModule,
],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: UniversalInterceptor,
multi: true
}
],
// Since the bootstrapped component is not inherited from your
// imported AppModule, it needs to be repeated here.
bootstrap: [AppComponent],
})
export class AppServerModule {}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 909 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 536 KiB

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 371 KiB

After

Width:  |  Height:  |  Size: 314 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 666 KiB

After

Width:  |  Height:  |  Size: 383 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 585 KiB

After

Width:  |  Height:  |  Size: 390 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 728 KiB

After

Width:  |  Height:  |  Size: 614 KiB

View File

@@ -49,7 +49,7 @@
.container
.row
.col-12.col-xl-6
img.screenshot([src]='screenshots.window')
img.screenshot([src]='screenshots.window', loading='lazy')
.col-12.col-xl-6
h1 The important stuff
ul
@@ -79,13 +79,13 @@
li Optional #[strong global hotkey] to focus/hide the terminal
li Bracketed paste
.col-12.col-xl-6
img.screenshot([src]='screenshots.tabs')
img.screenshot([src]='screenshots.tabs', loading='lazy')
.section.section-a
.container
.row
.col-12.col-xl-6
img.screenshot([src]='screenshots.ssh')
img.screenshot([src]='screenshots.ssh', loading='lazy')
.col-12.col-xl-6
h1 SSH Client
ul
@@ -110,13 +110,13 @@
li Optional #[strong portable mode]
li Current directory detection that works
.col-12.col-xl-6
img.screenshot([src]='screenshots.win')
img.screenshot([src]='screenshots.win', loading='lazy')
.section.section-a
.container
.row
.col-12.col-xl-6
img.screenshot([src]='screenshots.serial')
img.screenshot([src]='screenshots.serial', loading='lazy')
.col-12.col-xl-6
h1 Serial Terminal
ul

View File

@@ -2,6 +2,7 @@
<html>
<head>
<base href="/">
<meta charset="utf-8" />
<meta name="viewport" content="initial-scale=1, minimal-ui, shrink-to-fit=no">
<link rel="icon" type="image/png">
<link rel="shortcut icon" type="image/png" href="./assets/favicon.png">

2
src/index.server.ts Normal file
View File

@@ -0,0 +1,2 @@
import './styles.scss'
export { AppServerModule } from './app.server.module'

View File

@@ -10,6 +10,5 @@ import './styles.scss'
import { AppModule } from './app.module'
enableProdMode()
platformBrowserDynamic().bootstrapModule(AppModule)

53
src/server.ts Normal file
View File

@@ -0,0 +1,53 @@
import 'zone.js/dist/zone-node';
import { enableProdMode } from '@angular/core';
require('source-map-support').install()
// Express Engine
import { ngExpressEngine } from '@nguniversal/express-engine';
import './ssr-polyfills'
import * as express from 'express'
import { join } from 'path'
// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();
// Express server
const app = express();
const PORT = process.env.PORT || 4000;
const DIST_FOLDER = join(process.cwd(), 'build');
import { AppServerModule } from './app.server.module'
app.engine('html', ngExpressEngine({
bootstrap: AppServerModule,
}));
app.set('view engine', 'html');
app.set('views', DIST_FOLDER);
// Example Express Rest API endpoints
// app.get('/api/**', (req, res) => { });
// Server static files from /browser
app.use('/static', express.static(DIST_FOLDER, {
maxAge: '1y'
}));
var proxy = require('express-http-proxy');
app.get(['/', '/login'], (req, res) => {
res.render('index', { req });
});
app.use('/', proxy('http://tabby.local:8000/api/', {
}))
// Start up the Node server
app.listen(PORT, () => {
console.log(`Node Express server listening on http://localhost:${PORT}`);
});

View File

@@ -8,22 +8,20 @@ import { Config, Gateway, Version } from '../api'
export class SocketProxy {
connect$ = new Subject<void>()
data$ = new Subject<Buffer>()
error$ = new Subject<Buffer>()
close$ = new Subject<Buffer>()
data$ = new Subject<Uint8Array>()
error$ = new Subject<Error>()
close$ = new Subject<void>()
url: string
authToken: string
webSocket: WebSocket|null
initialBuffer: Buffer
initialBuffers: any[] = []
options: {
host: string
port: number
}
constructor (private appConnector: AppConnectorService) {
this.initialBuffer = Buffer.from('')
}
constructor (private appConnector: AppConnectorService) { }
async connect (options) {
this.options = options
@@ -77,8 +75,10 @@ export class SocketProxy {
} else if (msg._ === 'connected') {
this.connect$.next()
this.connect$.complete()
this.webSocket.send(this.initialBuffer)
this.initialBuffer = Buffer.from('')
for (const b of this.initialBuffers) {
this.webSocket.send(b)
}
this.initialBuffers = []
} else if (msg._ === 'error') {
console.error('Connection gateway error', msg)
this.close(new Error(msg.details))
@@ -93,7 +93,7 @@ export class SocketProxy {
write (chunk: Buffer): void {
if (!this.webSocket?.readyState) {
this.initialBuffer = Buffer.concat([this.initialBuffer, chunk])
this.initialBuffers.push(chunk)
} else {
this.webSocket.send(chunk)
}

45
src/ssr-interceptor.ts Normal file
View File

@@ -0,0 +1,45 @@
/**
* This interceptor ensures that the app makes requests
* with relative paths correctly server-side.
* Requests which start with a dot (ex. ./assets/...)
* or relative ones ( ex. /assets/...) will be converted
* to absolute paths
*/
import { Inject, Injectable, Injector, PLATFORM_ID } from '@angular/core';
import { isPlatformServer } from '@angular/common';
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { REQUEST } from '@nguniversal/express-engine/tokens';
import { Request } from 'express';
import { Observable } from 'rxjs';
@Injectable()
export class UniversalInterceptor implements HttpInterceptor {
constructor(
private readonly injector: Injector,
@Inject(PLATFORM_ID) private readonly platformId: any) {
}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const isServer = isPlatformServer(this.platformId);
if (isServer && !request.url.startsWith('//') && (request.url.startsWith('./') || request.url.startsWith('/'))) {
const serverRequest = this.injector.get(REQUEST) as Request;
console.log(serverRequest)
const baseUrl = `${serverRequest.protocol}://${serverRequest.get('Host')}`;
let endpoint = request.url;
/**
* ISSUE https://github.com/angular/angular/issues/19224
* HttpClient doesn't support relative requests server-side
*/
if (endpoint.startsWith('.')) {
endpoint = endpoint.substring(1);
}
// Now the endpoint starts with '/'
request = request.clone({
url: `${baseUrl}${endpoint}`
});
}
return next.handle(request);
}
}

41
src/ssr-polyfills.ts Normal file
View File

@@ -0,0 +1,41 @@
import * as domino from 'domino';
import * as fs from 'fs';
import * as path from 'path';
const template = fs.readFileSync(path.join(process.cwd(), 'build-server', 'index.html')).toString();
const win = domino.createWindow(template);
global['window'] = win;
Object.defineProperty(win.document.body.style, 'transform', {
value: () => {
return {
enumerable: true,
configurable: true
};
},
});
Object.defineProperty(win.document.body.style, 'z-index', {
value: () => {
return {
enumerable: true,
configurable: true
};
},
});
global['document'] = win.document;
global['CSS'] = null;
// global['atob'] = win.atob;
global['atob'] = (base64: string) => {
return Buffer.from(base64, 'base64').toString();
};
function setDomTypes() {
// Make all Domino types available as types in the global env.
Object.assign(global, domino['impl']);
(global as any)['KeyboardEvent'] = domino['impl'].Event;
}
setDomTypes();

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html class="tabby">
<head>
<meta charset="UTF-8">
<meta charset="utf-8" />
<style id="custom-css"></style>
<style>body { transition: 0.5s background; }</style>
</head>

View File

@@ -1,7 +1,7 @@
{
"compilerOptions": {
"baseUrl": "src/",
"module": "esNext",
"module": "es2015",
"target": "es6",
"moduleResolution": "node",
"noImplicitAny": false,
@@ -27,5 +27,5 @@
"*": ["src/*"]
}
},
"exclude": ["app-dist"]
"include": ["src"]
}

View File

@@ -1,31 +0,0 @@
{
"compilerOptions": {
"baseUrl": "src/",
"module": "esNext",
"target": "es6",
"moduleResolution": "node",
"noImplicitAny": false,
"removeComments": false,
"emitDeclarationOnly": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"sourceMap": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"declaration": true,
"lib": [
"dom",
"es5",
"es6",
"es7"
],
"paths": {
"*": ["src/*"]
}
},
"exclude": ["app-dist"]
}

View File

@@ -2,32 +2,24 @@ const path = require('path')
const webpack = require('webpack')
const { AngularWebpackPlugin } = require('@ngtools/webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
const htmlPluginOptions = {
hash: true,
minify: false
}
module.exports = {
target: 'web',
entry: {
index: path.resolve(__dirname, 'src/index.ts'),
terminal: path.resolve(__dirname, 'src/terminal.ts'),
},
const baseConfig = {
mode: process.env.DEV ? 'development' : 'production',
context: __dirname,
devtool: 'source-map',
output: {
path: path.join(__dirname, 'build'),
pathinfo: true,
publicPath: '/static/',
filename: '[name].js',
chunkFilename: '[name].bundle.js',
},
cache: !process.env.DEV ? false : {
type: 'filesystem',
},
resolve: {
mainFields: ['esm2015', 'browser', 'module', 'main'],
modules: [
'src/',
'node_modules/',
@@ -53,14 +45,14 @@ module.exports = {
},
{
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
exclude: /component\.scss/
},
{
test: /\.(ttf|eot|otf|woff|woff2)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
type: 'asset/resource',
},
{ test: /\.css$/, use: ['css-loader', 'sass-loader'] },
{ test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'] },
{
test: /\.(jpeg|png|svg)?$/,
type: 'asset/resource',
@@ -71,12 +63,8 @@ module.exports = {
},
],
},
plugins: [
new AngularWebpackPlugin({
tsconfig: 'tsconfig.main.json',
directTemplateLoading: false,
}),
new MiniCssExtractPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html',
@@ -91,3 +79,62 @@ module.exports = {
}),
],
}
module.exports = [
{
name: 'browser',
target: 'web',
...baseConfig,
entry: {
index: path.resolve(__dirname, 'src/index.ts'),
terminal: path.resolve(__dirname, 'src/terminal.ts'),
},
plugins: [
...baseConfig.plugins,
new AngularWebpackPlugin({
tsconfig: 'tsconfig.json',
directTemplateLoading: false,
skipCodeGeneration: false,
}),
],
output: {
path: path.join(__dirname, 'build'),
pathinfo: true,
publicPath: '/static/',
filename: '[name].js',
chunkFilename: '[name].bundle.js',
},
},
{
name: 'server',
target: 'node',
...baseConfig,
entry: {
'index.server': path.resolve(__dirname, 'src/index.server.ts'),
'server': path.resolve(__dirname, 'src/server.ts'),
},
plugins: [
...baseConfig.plugins,
new AngularWebpackPlugin({
entryModule: path.resolve(__dirname, 'src/app/app.server.module`#AppServerModule'),
mainPath: path.resolve(__dirname, 'src/index.server.ts'),
tsconfig: 'tsconfig.json',
directTemplateLoading: false,
platform: 1,
skipCodeGeneration: false,
}),
],
output: {
libraryTarget: 'commonjs',
path: path.join(__dirname, 'build-server'),
pathinfo: true,
publicPath: '/static/',
filename: '[name].js',
chunkFilename: '[name].bundle.js',
},
},
]
if (process.env.BUNDLE_ANALYZER) {
module.exports[0].plugins.push(new BundleAnalyzerPlugin())
}

1111
yarn.lock

File diff suppressed because it is too large Load Diff