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

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>