Auto reload and bug fixes

This commit is contained in:
Sebastian Seedorf
2020-11-14 19:38:16 +01:00
parent 5a56fc26d2
commit 6189b95b6e
22 changed files with 265 additions and 47 deletions

View File

@@ -2,5 +2,16 @@
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="HtmlUnknownAttribute" enabled="true" level="WARNING" enabled_by_default="true">
<option name="myValues">
<value>
<list size="2">
<item index="0" class="java.lang.String" itemvalue="src" />
<item index="1" class="java.lang.String" itemvalue="async" />
</list>
</value>
</option>
<option name="myCustomValuesEnabled" value="true" />
</inspection_tool>
</profile>
</component>

131
package-lock.json generated
View File

@@ -528,6 +528,15 @@
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
},
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"requires": {
"color-convert": "^1.9.0"
}
},
"aproba": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
@@ -715,6 +724,28 @@
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
},
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"dependencies": {
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
}
}
}
},
"character-parser": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz",
@@ -826,6 +857,52 @@
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"concurrently": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-5.3.0.tgz",
"integrity": "sha512-8MhqOB6PWlBfA2vJ8a0bSFKATOdWlHiQlk11IfmQBPaHVP8oP2gsh2MObE6UR3hqDHqvaIvLTyceNW6obVuFHQ==",
"dev": true,
"requires": {
"chalk": "^2.4.2",
"date-fns": "^2.0.1",
"lodash": "^4.17.15",
"read-pkg": "^4.0.1",
"rxjs": "^6.5.2",
"spawn-command": "^0.0.2-1",
"supports-color": "^6.1.0",
"tree-kill": "^1.2.2",
"yargs": "^13.3.0"
},
"dependencies": {
"parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
"integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
"dev": true,
"requires": {
"error-ex": "^1.3.1",
"json-parse-better-errors": "^1.0.1"
}
},
"pify": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
"dev": true
},
"read-pkg": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-4.0.1.tgz",
"integrity": "sha1-ljYlN48+HE1IyFhytabsfV0JMjc=",
"dev": true,
"requires": {
"normalize-package-data": "^2.3.2",
"parse-json": "^4.0.0",
"pify": "^3.0.0"
}
}
}
},
"connect-redis": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-5.0.0.tgz",
@@ -904,6 +981,12 @@
"assert-plus": "^1.0.0"
}
},
"date-fns": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.16.1.tgz",
"integrity": "sha512-sAJVKx/FqrLYHAQeN7VpJrPhagZc9R4ImZIWYRFZaaohR3KzmuK88touwsSwSVT8Qcbd4zoDsnGfX4GFB4imyQ==",
"dev": true
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -1980,6 +2063,12 @@
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM="
},
"json-parse-better-errors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
"integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
"dev": true
},
"json-prune": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/json-prune/-/json-prune-1.1.0.tgz",
@@ -2324,6 +2413,12 @@
"node-sass": "^4.3.0"
}
},
"node-watch": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/node-watch/-/node-watch-0.7.0.tgz",
"integrity": "sha512-OOBiglke5SlRQT5WYfwXTmYqTfXjcTNBHpalyHLtLxDpQYVpVRkJqabcch1kmwJsjV/J4OZuzEafeb4soqtFZA==",
"dev": true
},
"nopt": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
@@ -2929,6 +3024,15 @@
"integrity": "sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw==",
"dev": true
},
"rxjs": {
"version": "6.6.3",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz",
"integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==",
"dev": true,
"requires": {
"tslib": "^1.9.0"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@@ -3082,6 +3186,12 @@
"amdefine": ">=0.0.4"
}
},
"spawn-command": {
"version": "0.0.2-1",
"resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz",
"integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=",
"dev": true
},
"spdx-correct": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz",
@@ -3232,6 +3342,15 @@
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
"dev": true
},
"supports-color": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
"integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
}
},
"table": {
"version": "5.4.6",
"resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz",
@@ -3333,6 +3452,12 @@
"punycode": "^2.1.1"
}
},
"tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
"dev": true
},
"trim-newlines": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz",
@@ -3667,6 +3792,12 @@
"mkdirp": "^0.5.1"
}
},
"ws": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.0.tgz",
"integrity": "sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ==",
"dev": true
},
"y18n": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",

View File

@@ -5,7 +5,10 @@
"scripts": {
"lint": "eslint . --ext .ts",
"lint-fix": "eslint . --ext .ts --fix",
"debug": "tsc-watch --onSuccess \"node --enable-source-maps --use-openssl-ca --unhandled-rejections=strict ./out/index\"",
"update-client-hash": "node -e \"require('fs').writeFileSync('public/misc/hash.txt', require('randomstring').generate())\"",
"debug-client": "tsc-watch --project ./public/js-source",
"debug-server": "tsc-watch --onSuccess \"node --enable-source-maps --use-openssl-ca --unhandled-rejections=strict ./out/index\"",
"debug": "concurrently \"npm run debug-client\" \"npm run debug-server\"",
"build": "tsc",
"production": "node --use-openssl-ca --unhandled-rejections=strict ./out/index",
"install-debug": "npm install && npm run build",
@@ -41,10 +44,13 @@
"@types/uuid": "^8.3.0",
"@typescript-eslint/eslint-plugin": "^4.7.0",
"@typescript-eslint/parser": "^4.7.0",
"concurrently": "^5.3.0",
"eslint": "^7.13.0",
"eslint-plugin-no-null": "^1.0.2",
"eslint-plugin-promise": "^4.2.1",
"node-watch": "^0.7.0",
"tsc-watch": "^4.2.9",
"typescript": "^4.0.5"
"typescript": "^4.0.5",
"ws": "^7.4.0"
}
}

View File

@@ -29,6 +29,8 @@ module.exports = {
"no-void": "error",
"comma-spacing": "error",
"comma-dangle": ["error", "always-multiline"],
"comma-style": "error",
"semi": "error",
"no-restricted-imports": ["error",
"assert", "buffer", "child_process", "cluster", "crypto", "dgram", "dns", "domain", "events", "freelist",

View File

@@ -1,4 +1,4 @@
import {getUserInfo} from './utils';
import {getUserInfo} from './utils/utils';
export async function getUserName(): Promise<string> {
const info = await getUserInfo();

View File

@@ -1,7 +1,5 @@
// Export setConfig so that the external url can set set automatically
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export {setConfig} from './utils';
// Export setConfig so that the external url can set set automatically by the server
export {setConfig} from './utils/utils';
import {getUserName} from './SomeModule';

View File

@@ -17,7 +17,7 @@ export function getConfig(): Promise<ClientConfig> {
}
export async function getUserInfo(): Promise<UserInfo|undefined> {
const getBaseUrl = await getConfig();
const res = await fetch(getBaseUrl.EXTERNAL_BASE_URL + "/api/user");
const config = await getConfig();
const res = await fetch(config.EXTERNAL_BASE_URL + "/api/user");
return res.json();
}

View File

@@ -11,6 +11,7 @@ module.exports = {
'eslint:recommended',
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:promise/recommended",
],
rules: {
"no-console": "error",
@@ -30,16 +31,8 @@ module.exports = {
"no-void": "error",
"comma-spacing": "error",
"comma-dangle": ["error", "always-multiline"],
"promise/no-return-wrap": "error",
"promise/param-names": "error",
"promise/catch-or-return": "error",
"promise/no-native": "off",
"promise/no-nesting": "warn",
"promise/no-promise-in-callback": "warn",
"promise/no-callback-in-promise": "warn",
"promise/avoid-new": "warn",
"promise/no-new-statics": "error",
"promise/no-return-in-finally": "warn",
"promise/valid-params": "warn",
"comma-style": "error",
"semi": "error",
"promise/always-return": "off",
},
};

View File

@@ -6,15 +6,18 @@ import * as redisStore from 'connect-redis';
import * as session from 'express-session';
import * as sassMiddleware from 'node-sass-middleware';
import indexRouter from './routes';
import {HttpLogger, Redis, Config, setupAuthProxy} from './utils';
import {HttpLogger, Redis, Config, setupAuthProxy, getReloadRouter} from './utils';
import {Store} from 'express-session';
export const app = express();
// view engine setup
app.set('views', path.join(__dirname, '../views'));
app.set('view engine', 'pug');
app.use((req, res, next) => {
res.locals.Config = Config;
next();
});
const router = express.Router();
@@ -24,7 +27,9 @@ app.use(express.json());
app.use(express.urlencoded({extended: false}));
// auth proxy middleware
app.use(setupAuthProxy);
router.use(setupAuthProxy);
// auto reloader (when running in debug mode)
router.use(getReloadRouter());
// session
let sessionStore: Store|undefined = undefined;
@@ -32,7 +37,7 @@ if (Redis.client) {
const RedisStore = redisStore(session);
sessionStore = new RedisStore({client: Redis.client});
}
app.use(session({
router.use(session({
store: sessionStore,
secret: Config.SESSION_SECRET,
resave: false,

View File

@@ -9,7 +9,7 @@ import {Config, Logger} from './utils';
app.set('port', Config.PORT);
const server = http.createServer(app);
server.listen(Config.PORT);
app.listen(Config.PORT);
server.on('error', onError);
server.on('listening', onListening);

View File

@@ -12,7 +12,7 @@ router.use("/health", healthRouter);
/* GET home page. */
router.get('/', async (req, res) => {
const email = (await req.getUserInfo())?.email ?? "No email found!";
res.render('index', {title: 'Express', email, externalUrl: Config.EXTERNAL_BASE_URL});
res.render('index', {title: 'Express', email});
});
router.get('/logout', (req, res) => {

View File

@@ -2,11 +2,10 @@ import {Request, RequestHandler} from 'express';
import {Config, Logger} from '.';
import {Resolvable} from './helpers/resolvable';
import fetch from 'node-fetch';
import * as properUrlJoin from 'proper-url-join';
const urlJoin = properUrlJoin as unknown as properUrlJoin.default;
import {urlJoin} from './helpers/urlJoin';
export const setupAuthProxy: RequestHandler = (req: Request, res, next) => {
const resolvable = new Resolvable<UserInfo|undefined>(async () => {
const resolvable = new Resolvable(async () => {
if (!Config.USERINFO_HEADER) {
return undefined;
}
@@ -17,7 +16,7 @@ export const setupAuthProxy: RequestHandler = (req: Request, res, next) => {
}
try {
const res = await fetch(url, {headers: [[Config.USERINFO_HEADER, token]]});
return await res.json();
return await res.json() as UserInfo;
} catch (e) {
Logger.warn(e);
return undefined;
@@ -32,6 +31,6 @@ export const setupAuthProxy: RequestHandler = (req: Request, res, next) => {
}
this.redirect(307, url);
return true;
}
};
next();
}
};

58
src/utils/auto-reload.ts Normal file
View File

@@ -0,0 +1,58 @@
import {Router} from 'express';
import {Config} from './config';
import {Logger} from './logging';
import {urlJoin} from './helpers/urlJoin';
import {v4} from 'uuid';
import Timeout = NodeJS.Timeout;
export function getReloadRouter(): Router {
const reloadRouter = Router();
if (!Config.isProduction) {
let uuid = v4();
let updateTimeout: Timeout|undefined = undefined;
import("node-watch").then((watch) => {
watch.default('public', {recursive: true}, (evt, name) => {
if (updateTimeout !== undefined) clearTimeout(updateTimeout);
updateTimeout = setTimeout(() => {
uuid = v4();
}, 200);
});
}).catch((err) => { Logger.error(err); });
reloadRouter.get("/auto-reload/client.js", (req, res) => {
Logger.debug(req.url, req.originalUrl, req.baseUrl);
res.setHeader('Content-Type', "application/javascript");
// language=JavaScript
res.send(`
const loc = window.location;
const url = loc.protocol+'//'+loc.host+'${urlJoin(req.baseUrl, "/auto-reload")}';
const parse = async res => (await res.json()).uuid;
let hash = undefined;
let hadError = false;
setInterval(async () => {
try {
const data = await parse(await fetch(url));
if (data) {
hash = hash === undefined ? data : hash;
if (hash !== data) {
window.location.reload();
}
}
} catch (e) {
if (hadError === false) {
console.log(e);
hadError = true;
}
}
}, 3000);
`.replace(/\t/g, ""));
});
reloadRouter.get("/auto-reload", (req, res) => {
req.noLogging = true;
res.json({uuid});
});
}
return reloadRouter;
}

View File

@@ -36,5 +36,7 @@ const envs = {
export const Config = {
...envs,
isProduction,
EXTERNAL_BASE_URL: envs.EXTERNAL_BASE_URL || urlJoin(`http://${envs.HOSTNAME}${envs.PORT !== 80 ? `:${envs.PORT}` : ""}`),
EXTERNAL_BASE_URL:
envs.EXTERNAL_BASE_URL ||
urlJoin(`http://${envs.HOSTNAME}${envs.PORT !== 80 ? `:${envs.PORT}` : ""}`, envs.BASE_PATH),
}

View File

@@ -5,22 +5,22 @@ enum ResolvableState {
DONE
}
class FetchOnce<T> {
class FetchOnce<T, U extends Array<unknown>> {
protected data: T|undefined;
protected error: unknown|undefined;
protected state: ResolvableState = ResolvableState.WAITING;
protected pendings: [(res: Promise<T>|T) => void, (reason: unknown) => void][] = [];
constructor(protected fetchMethod?: () => Promise<T>) { }
constructor(protected fetchMethod?: (...args: U) => Promise<T>) { }
public resolve(): Promise<T> {
public resolve(...args: U): Promise<T> {
// eslint-disable-next-line promise/avoid-new
return new Promise((resolve, reject) => {
switch (this.state) {
case ResolvableState.WAITING:
this.state = ResolvableState.PENDING;
this.pendings.push([resolve, reject]);
if (this.fetchMethod) this.parsePromise(this.fetchMethod());
if (this.fetchMethod) this.parsePromise(this.fetchMethod(...args));
break;
case ResolvableState.PENDING:
this.pendings.push([resolve, reject]);
@@ -52,13 +52,13 @@ class FetchOnce<T> {
}
}
export class Resolvable<T> extends FetchOnce<T> {
constructor(fetchMethod: () => Promise<T>) {
export class Resolvable<T, U extends Array<unknown>> extends FetchOnce<T, U> {
constructor(fetchMethod: (...args: U) => Promise<T>) {
super(fetchMethod);
}
}
export class WaitForSync<T> extends FetchOnce<T> {
export class WaitForSync<T, U extends Array<unknown>> extends FetchOnce<T, U> {
protected state: ResolvableState = ResolvableState.PENDING;
constructor() {

View File

@@ -0,0 +1,2 @@
import * as properUrlJoin from 'proper-url-join';
export const urlJoin = properUrlJoin as unknown as properUrlJoin.default;

View File

@@ -2,3 +2,5 @@ export {Config} from './config';
export {Redis} from './redis';
export {Logger, HttpLogger} from './logging';
export {setupAuthProxy} from './auth-proxy';
export {Resolvable, WaitForSync} from './helpers/resolvable';
export {getReloadRouter} from './auto-reload';

View File

@@ -33,6 +33,7 @@ for (const level of levels) {
export const HttpLogger: RequestHandler = (req, res, next) => {
const start = Date.now();
const path = req.path;
type Callback = (() => void)|undefined;
const end = res.end;
res.end = function(...args: [Callback] & [unknown, Callback] & [unknown, BufferEncoding, Callback]) {
@@ -45,8 +46,9 @@ export const HttpLogger: RequestHandler = (req, res, next) => {
const status = colorFunction(res.statusCode.toString(10));
const method = req.method.toUpperCase().padEnd(6, " ");
const responseTime = (Date.now()-start).toString(10).padStart(3, " ");
Logger.http(`${status} ${method} ${responseTime}ms ${req.path}`);
if (!req.noLogging)
Logger.http(`${status} ${method} ${responseTime}ms ${path}`);
end.apply(res, args);
};
next();
}
};

5
src/utils/types/logging.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare namespace Express {
interface Request {
noLogging: boolean|undefined;
}
}

View File

@@ -1,14 +1,16 @@
- const baseUrl = Config.EXTERNAL_BASE_URL;
doctype html
html
head
title= title
link(rel='stylesheet', href='/styles/style.css')
link(rel='stylesheet', href=baseUrl+'/styles/style.css')
if !Config.isProduction
script(type='text/javascript', async, src=baseUrl+'/auto-reload/client.js')
body
block content
script(type='text/javascript', src='/js/require-2.3.6.min.js')
script(type='text/javascript', src='/js/bundle.js')
script(type='text/javascript', src=baseUrl+'/js/require-2.3.6.min.js')
script(type='text/javascript', src=baseUrl+'/js/bundle.js')
script.
require(['src/index'], function (index) {
console.log(index);
index.setConfig({EXTERNAL_BASE_URL: '#{externalUrl}'});
index.setConfig({EXTERNAL_BASE_URL: '#{baseUrl}'});
});