ssr wip
@@ -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
@@ -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 {}
|
||||
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 909 KiB |
|
Before Width: | Height: | Size: 536 KiB After Width: | Height: | Size: 260 KiB |
|
Before Width: | Height: | Size: 371 KiB After Width: | Height: | Size: 314 KiB |
|
Before Width: | Height: | Size: 666 KiB After Width: | Height: | Size: 383 KiB |
|
Before Width: | Height: | Size: 585 KiB After Width: | Height: | Size: 390 KiB |
|
Before Width: | Height: | Size: 728 KiB After Width: | Height: | Size: 614 KiB |
@@ -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
|
||||
|
||||
@@ -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
@@ -0,0 +1,2 @@
|
||||
import './styles.scss'
|
||||
export { AppServerModule } from './app.server.module'
|
||||
@@ -10,6 +10,5 @@ import './styles.scss'
|
||||
import { AppModule } from './app.module'
|
||||
|
||||
|
||||
|
||||
enableProdMode()
|
||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||
|
||||
53
src/server.ts
Normal 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}`);
|
||||
});
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
@@ -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>
|
||||
|
||||