Some more small rework and cleanup

master
WolverinDEV 2021-03-18 18:59:06 +01:00
parent e2be859266
commit 85acd6dd56
11 changed files with 202 additions and 255 deletions

View File

@ -1,6 +1,6 @@
import * as loader from "./loader/loader";
import {Stage} from "./loader/loader";
import {getUrlParameter} from "./loader/utils";
import {getUrlParameter} from "./loader/Utils";
let overlay: HTMLDivElement;
let setupContainer: HTMLDivElement;

99
loader/app/bootstrap.ts Normal file
View File

@ -0,0 +1,99 @@
import "core-js/stable";
import "./polifill";
import "./css";
import {ApplicationLoader} from "./loader/loader";
import {getUrlParameter} from "./loader/Utils";
/* let the loader register himself at the window first */
const target = getUrlParameter("loader-target") || "app";
console.info("Loading app with loader \"%s\"", target);
let appLoader: ApplicationLoader;
if(target === "empty") {
appLoader = new (require("./targets/empty").default);
} else if(target === "manifest") {
appLoader = new (require("./targets/maifest-target").default);
} else {
appLoader = new (require("./targets/app").default);
}
setTimeout(() => appLoader.execute(), 0);
export {};
if(__build.target === "client") {
/* do this so we don't get a react dev tools warning within the client */
if(!('__REACT_DEVTOOLS_GLOBAL_HOOK__' in window)) {
window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {};
}
window.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = function () {};
}
/* Hello World message */
{
const clog = console.log;
const print_security = () => {
{
const css = [
"display: block",
"text-align: center",
"font-size: 42px",
"font-weight: bold",
"-webkit-text-stroke: 2px black",
"color: red"
].join(";");
clog("%c ", "font-size: 100px;");
clog("%cSecurity warning:", css);
}
{
const css = [
"display: block",
"text-align: center",
"font-size: 18px",
"font-weight: bold"
].join(";");
clog("%cPasting anything in here could give attackers access to your data.", css);
clog("%cUnless you understand exactly what you are doing, close this window and stay safe.", css);
clog("%c ", "font-size: 100px;");
}
};
/* print the hello world */
{
const css = [
"display: block",
"text-align: center",
"font-size: 72px",
"font-weight: bold",
"-webkit-text-stroke: 2px black",
"color: #18BC9C"
].join(";");
clog("%cHey, hold on!", css);
}
{
const css = [
"display: block",
"text-align: center",
"font-size: 26px",
"font-weight: bold"
].join(";");
const css_2 = [
"display: block",
"text-align: center",
"font-size: 26px",
"font-weight: bold",
"color: blue"
].join(";");
const display_detect = /./;
display_detect.toString = function() { print_security(); return ""; };
clog("%cLovely to see you using and debugging the TeaSpeak-Web client.", css);
clog("%cIf you have some good ideas or already done some incredible changes,", css);
clog("%cyou'll be may interested to share them here: %chttps://github.com/TeaSpeak/TeaWeb", css, css_2);
clog("%c ", display_detect);
}
}

View File

@ -1,35 +1,14 @@
import "core-js/stable";
import "./polifill";
import "./css";
import * as loader from "./loader/loader";
import {ApplicationLoader} from "./loader/loader";
import {getUrlParameter} from "./loader/utils";
if(window["loader"]) {
throw "an loader instance has already been defined";
}
window["loader"] = loader;
/* let the loader register himself at the window first */
const target = getUrlParameter("loader-target") || "app";
console.info("Loading app with loader \"%s\"", target);
export * from "./loader/loader";
export * as loaderAnimation from "./animation";
let appLoader: ApplicationLoader;
if(target === "empty") {
appLoader = new (require("./targets/empty").default);
} else if(target === "manifest") {
appLoader = new (require("./targets/maifest-target").default);
} else {
appLoader = new (require("./targets/app").default);
import "./bootstrap";
/* FIXME: This is glue! */
if(window["loader"]) {
throw "an loader instance has already been defined";
}
setTimeout(() => appLoader.execute(), 0);
export {};
if(__build.target === "client") {
/* do this so we don't get a react dev tools warning within the client */
if(!('__REACT_DEVTOOLS_GLOBAL_HOOK__' in window))
window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {};
window.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = function () {};
}
window["loader"] = module.exports;

View File

@ -1,11 +1,12 @@
import {config, critical_error, SourcePath} from "./loader";
import {load_parallel, LoadCallback, LoadSyntaxError, ParallelOptions, script_name} from "./utils";
import {executeParallelLoad, LoadCallback, LoadSyntaxError, ParallelOptions} from "./Utils";
let _script_promises: {[key: string]: Promise<void>} = {};
function load_script_url(url: string) : Promise<void> {
if(typeof _script_promises[url] === "object")
if(typeof _script_promises[url] === "object") {
return _script_promises[url];
}
return (_script_promises[url] = new Promise((resolve, reject) => {
const script_tag: HTMLScriptElement = document.createElement("script");
@ -67,40 +68,18 @@ export interface Options {
cache_tag?: string;
}
export async function load(path: SourcePath, options: Options) : Promise<void> {
if(Array.isArray(path)) { //We have fallback scripts
return load(path[0], options).catch(error => {
if(error instanceof LoadSyntaxError)
return Promise.reject(error);
if(path.length > 1)
return load(path.slice(1), options);
return Promise.reject(error);
});
} else {
const source = typeof(path) === "string" ? {url: path, depends: []} : path;
if(source.url.length == 0) return Promise.resolve();
/* await depends */
for(const depend of source.depends) {
if(!_script_promises[depend])
throw "Missing dependency " + depend;
await _script_promises[depend];
}
await load_script_url(source.url + (options.cache_tag || ""));
}
export async function loadScript(path: SourcePath, options: Options) : Promise<void> {
await load_script_url(path + (options.cache_tag || ""));
}
type MultipleOptions = Options | ParallelOptions;
export async function load_multiple(paths: SourcePath[], options: MultipleOptions, callback?: LoadCallback<SourcePath>) : Promise<void> {
const result = await load_parallel<SourcePath>(paths, e => load(e, options), e => script_name(e, false), options, callback);
export async function loadScripts(paths: SourcePath[], options: MultipleOptions, callback?: LoadCallback<SourcePath>) : Promise<void> {
const result = await executeParallelLoad<SourcePath>(paths, e => loadScript(e, options), e => e, options, callback);
if(result.failed.length > 0) {
if(config.error) {
console.error("Failed to load the following scripts:");
for(const script of result.failed) {
const sname = script_name(script.request, false);
const sname = script.request;
if(script.error instanceof LoadSyntaxError) {
const source = script.error.source as Error;
if(source.name === "TypeError") {
@ -126,11 +105,11 @@ export async function load_multiple(paths: SourcePath[], options: MultipleOption
} else if(typeof error === "string") {
errorMessage = error;
} else {
console.error("Script %s loading error: %o", script_name(result.failed[0].request, false), error);
console.error("Script %s loading error: %o", result.failed[0].request, error);
errorMessage = "View the browser console for more information!";
}
critical_error("Failed to load script " + script_name(result.failed[0].request, true), errorMessage);
critical_error("Failed to load script " + result.failed[0].request, errorMessage);
}
throw "failed to load script " + script_name(result.failed[0].request, false) + " (" + errorMessage + ")";
throw "failed to load script " + result.failed[0].request + " (" + errorMessage + ")";
}
}

View File

@ -1,18 +1,22 @@
import {config, critical_error, SourcePath} from "./loader";
import {load_parallel, LoadCallback, LoadSyntaxError, ParallelOptions, script_name} from "./utils";
import {executeParallelLoad, LoadCallback, LoadSyntaxError, ParallelOptions} from "./Utils";
let _style_promises: {[key: string]: Promise<void>} = {};
let loadedStyles: {[key: string]: Promise<void>} = {};
function load_style_url(url: string) : Promise<void> {
if(typeof _style_promises[url] === "object")
return _style_promises[url];
if(typeof loadedStyles[url] === "object") {
return loadedStyles[url];
}
return (_style_promises[url] = new Promise((resolve, reject) => {
return (loadedStyles[url] = new Promise((resolve, reject) => {
const tag: HTMLLinkElement = document.createElement("link");
let error = false;
const error_handler = (event: ErrorEvent) => {
if(config.verbose) console.log("msg: %o, url: %o, line: %o, col: %o, error: %o", event.message, event.filename, event.lineno, event.colno, event.error);
if(config.verbose) {
console.log("msg: %o, url: %o, line: %o, col: %o, error: %o", event.message, event.filename, event.lineno, event.colno, event.error);
}
if(event.filename == tag.href) { //FIXME!
window.removeEventListener('error', error_handler as any);
@ -42,8 +46,9 @@ function load_style_url(url: string) : Promise<void> {
tag.onerror = error => {
cleanup();
tag.remove();
if(config.error)
if(config.error) {
console.error("File load error for file %s: %o", url, error);
}
reject("failed to load file " + url);
};
tag.onload = () => {
@ -80,14 +85,14 @@ function load_style_url(url: string) : Promise<void> {
document.getElementById("style").appendChild(tag);
tag.href = config.baseUrl + url;
})).then(result => {
})).then(() => {
/* cleanup memory */
_style_promises[url] = Promise.resolve(); /* this promise does not holds the whole script tag and other memory */
return _style_promises[url];
loadedStyles[url] = Promise.resolve(); /* this promise does not holds the whole script tag and other memory */
return loadedStyles[url];
}).catch(error => {
/* cleanup memory */
_style_promises[url] = Promise.reject(error); /* this promise does not holds the whole script tag and other memory */
return _style_promises[url];
loadedStyles[url] = Promise.reject(error); /* this promise does not holds the whole script tag and other memory */
return loadedStyles[url];
});
}
@ -95,39 +100,18 @@ export interface Options {
cache_tag?: string;
}
export async function load(path: SourcePath, options: Options) : Promise<void> {
if(Array.isArray(path)) { //We have fallback scripts
return load(path[0], options).catch(error => {
if(error instanceof LoadSyntaxError)
return Promise.reject(error);
if(path.length > 1)
return load(path.slice(1), options);
return Promise.reject(error);
});
} else {
const source = typeof(path) === "string" ? {url: path, depends: []} : path;
if(source.url.length == 0) return Promise.resolve();
/* await depends */
for(const depend of source.depends) {
if(!_style_promises[depend])
throw "Missing dependency " + depend;
await _style_promises[depend];
}
await load_style_url(source.url + (options.cache_tag || ""));
}
export async function loadStyle(path: SourcePath, options: Options) : Promise<void> {
await load_style_url(path + (options.cache_tag || ""));
}
export type MultipleOptions = Options | ParallelOptions;
export async function load_multiple(paths: SourcePath[], options: MultipleOptions, callback?: LoadCallback<SourcePath>) : Promise<void> {
const result = await load_parallel<SourcePath>(paths, e => load(e, options), e => script_name(e, false), options, callback);
export async function loadStyles(paths: SourcePath[], options: MultipleOptions, callback?: LoadCallback<SourcePath>) : Promise<void> {
const result = await executeParallelLoad<SourcePath>(paths, e => loadStyle(e, options), e => e, options, callback);
if(result.failed.length > 0) {
if(config.error) {
console.error("Failed to load the following style sheets:");
for(const style of result.failed) {
const sname = script_name(style.request, false);
const sname = style.request;
if(style.error instanceof LoadSyntaxError) {
console.log(" - %s: %o", sname, style.error.source);
} else {
@ -136,7 +120,7 @@ export async function load_multiple(paths: SourcePath[], options: MultipleOption
}
}
critical_error("Failed to load style " + script_name(result.failed[0].request, true) + " <br>" + "View the browser console for more information!");
throw "failed to load style " + script_name(result.failed[0].request, false);
critical_error("Failed to load style <code>" + result.failed[0].request + "</code><br>" + "View the browser console for more information!");
throw "failed to load style " + result.failed[0].request;
}
}

View File

@ -1,10 +1,10 @@
import {SourcePath} from "./loader";
import {Options} from "./script_loader";
import {Options} from "./ScriptLoader";
export const getUrlParameter = key => {
const match = location.search.match(new RegExp("(.*[?&]|^)" + key + "=([^&]+)($|&.*)"));
if(!match)
if(!match) {
return undefined;
}
return match[2];
};
@ -16,24 +16,15 @@ export class LoadSyntaxError {
}
}
export function script_name(path: SourcePath, html: boolean) {
if(Array.isArray(path)) {
return path.filter(e => !!e).map(e => script_name(e, html)).join(" or ");
} else if(typeof(path) === "string")
return html ? "<code>" + path + "</code>" : path;
else
return html ? "<code>" + path.url + "</code>" : path.url;
}
export interface ParallelOptions extends Options {
max_parallel_requests?: number
maxParallelRequests?: number
}
export interface ParallelResult<T> {
succeeded: T[];
failed: {
request: T,
error: T
error: any
}[],
skipped: T[];
@ -41,15 +32,22 @@ export interface ParallelResult<T> {
export type LoadCallback<T> = (entry: T, state: "loading" | "loaded") => void;
export async function load_parallel<T>(requests: T[], executor: (_: T) => Promise<void>, stringify: (_: T) => string, options: ParallelOptions, callback?: LoadCallback<T>) : Promise<ParallelResult<T>> {
export async function executeParallelLoad<T>(
requests: T[],
executor: (_: T) => Promise<void>,
stringify: (_: T) => string,
options: ParallelOptions,
callback?: LoadCallback<T>
) : Promise<ParallelResult<T>> {
const result: ParallelResult<T> = { failed: [], succeeded: [], skipped: [] };
const pendingRequests = requests.slice(0).reverse(); /* we're only able to pop from the back */
const currentRequests = {};
if(typeof callback === "undefined")
if(typeof callback === "undefined") {
callback = () => {};
}
const maxParallelRequests = typeof options.max_parallel_requests === "number" && options.max_parallel_requests > 0 ? options.max_parallel_requests : Number.MAX_SAFE_INTEGER;
const maxParallelRequests = typeof options.maxParallelRequests === "number" && options.maxParallelRequests > 0 ? options.maxParallelRequests : Number.MAX_SAFE_INTEGER;
while (pendingRequests.length > 0) {
while(Object.keys(currentRequests).length < maxParallelRequests) {
const element = pendingRequests.pop();
@ -60,8 +58,10 @@ export async function load_parallel<T>(requests: T[], executor: (_: T) => Promis
delete currentRequests[name];
callback(element, "loaded");
});
if(pendingRequests.length == 0)
if(pendingRequests.length == 0) {
break;
}
}
/*
@ -69,8 +69,9 @@ export async function load_parallel<T>(requests: T[], executor: (_: T) => Promis
* This should also not throw because any errors will be caught before.
*/
await Promise.race(Object.keys(currentRequests).map(e => currentRequests[e]));
if(result.failed.length > 0)
if(result.failed.length > 0) {
break; /* finish loading the other requests and than show the error */
}
}
await Promise.all(Object.keys(currentRequests).map(e => currentRequests[e]));
result.skipped.push(...pendingRequests);

View File

@ -1,7 +1,5 @@
import * as script_loader from "./script_loader";
import * as style_loader from "./style_loader";
import * as Animation from "../animation";
import {getUrlParameter} from "./utils";
import {getUrlParameter} from "./Utils";
export interface ApplicationLoader {
execute();
@ -76,30 +74,9 @@ export enum Stage {
DONE
}
let cache_tag: string | undefined;
let currentStage: Stage = undefined;
const tasks: {[key:number]: InternalTask[]} = {};
/* test if all files shall be load from cache or fetch again */
function loader_cache_tag() {
if(__build.mode === "debug") {
cache_tag = "?_ts=" + Date.now();
return;
}
const cached_version = localStorage.getItem("cached_version");
if(!cached_version || cached_version !== __build.version) {
register_task(Stage.LOADED, {
priority: 0,
name: "cached version updater",
function: async () => {
localStorage.setItem("cached_version", __build.version);
}
});
}
cache_tag = "?_version=" + __build.version;
}
export type ModuleMapping = {
application: string,
modules: {
@ -174,22 +151,23 @@ export function setCurrentTaskName(taskId: number, name: string) {
}
export async function execute(customLoadingAnimations: boolean) {
if(!await Animation.initialize(customLoadingAnimations))
if(!await Animation.initialize(customLoadingAnimations)) {
return;
}
loader_cache_tag();
const load_begin = Date.now();
const timestampBegin = Date.now();
let begin: number = 0;
let end: number = Date.now();
while(currentStage <= Stage.LOADED || typeof(currentStage) === "undefined") {
while(currentStage <= Stage.LOADED || typeof currentStage === "undefined") {
let pendingTasks: InternalTask[] = [];
while((tasks[currentStage] || []).length > 0) {
if(pendingTasks.length == 0 || pendingTasks[0].priority == tasks[currentStage][0].priority) {
pendingTasks.push(tasks[currentStage].pop());
} else break;
} else {
break;
}
}
const errors: {
@ -282,7 +260,7 @@ export async function execute(customLoadingAnimations: boolean) {
}
if(config.verbose)
console.debug("[loader] finished loader. (Total time: %dms)", Date.now() - load_begin);
console.debug("[loader] finished loader. (Total time: %dms)", Date.now() - timestampBegin);
Animation.finalize(config.abortAnimationOnFinish);
}
@ -351,79 +329,4 @@ export function critical_error_handler(handler?: ErrorHandler, override?: boolea
}
/* loaders */
export type DependSource = {
url: string;
depends: string[];
}
export type SourcePath = string | DependSource | string[];
export const scripts = script_loader;
export const style = style_loader;
/* Hello World message */
{
const clog = console.log;
const print_security = () => {
{
const css = [
"display: block",
"text-align: center",
"font-size: 42px",
"font-weight: bold",
"-webkit-text-stroke: 2px black",
"color: red"
].join(";");
clog("%c ", "font-size: 100px;");
clog("%cSecurity warning:", css);
}
{
const css = [
"display: block",
"text-align: center",
"font-size: 18px",
"font-weight: bold"
].join(";");
clog("%cPasting anything in here could give attackers access to your data.", css);
clog("%cUnless you understand exactly what you are doing, close this window and stay safe.", css);
clog("%c ", "font-size: 100px;");
}
};
/* print the hello world */
{
const css = [
"display: block",
"text-align: center",
"font-size: 72px",
"font-weight: bold",
"-webkit-text-stroke: 2px black",
"color: #18BC9C"
].join(";");
clog("%cHey, hold on!", css);
}
{
const css = [
"display: block",
"text-align: center",
"font-size: 26px",
"font-weight: bold"
].join(";");
const css_2 = [
"display: block",
"text-align: center",
"font-size: 26px",
"font-weight: bold",
"color: blue"
].join(";");
const display_detect = /./;
display_detect.toString = function() { print_security(); return ""; };
clog("%cLovely to see you using and debugging the TeaSpeak-Web client.", css);
clog("%cIf you have some good ideas or already done some incredible changes,", css);
clog("%cyou'll be may interested to share them here: %chttps://github.com/TeaSpeak/TeaWeb", css, css_2);
clog("%c ", display_detect);
}
}
export type SourcePath = string;

View File

@ -1,8 +1,9 @@
import * as loader from "./loader/loader";
import {config} from "./loader/loader";
import {script_name} from "./loader/utils";
import {loadStyles} from "./loader/StyleLoader";
import {loadScripts} from "./loader/ScriptLoader";
export interface TeaManifest {
export interface ApplicationManifest {
version: number;
chunks: {
@ -24,15 +25,17 @@ export interface TeaManifest {
};
}
let manifest: TeaManifest;
export async function loadManifest() : Promise<TeaManifest> {
let manifest: ApplicationManifest;
export async function loadManifest() : Promise<ApplicationManifest> {
if(manifest) {
return manifest;
}
try {
const response = await fetch(config.baseUrl + "/manifest.json?_date=" + Date.now());
if(!response.ok) throw response.status + " " + response.statusText;
if(!response.ok) {
throw response.status + " " + response.statusText;
}
manifest = await response.json();
} catch(error) {
@ -40,8 +43,10 @@ export async function loadManifest() : Promise<TeaManifest> {
loader.critical_error("Failed to load manifest.json", error);
throw "failed to load manifest.json";
}
if(manifest.version !== 2)
if(manifest.version !== 2) {
throw "invalid manifest version";
}
return manifest;
}
@ -51,30 +56,31 @@ export async function loadManifestTarget(chunkName: string, taskId: number) {
loader.critical_error("Missing entry chunk in manifest.json", "Chunk " + chunkName + " is missing.");
throw "missing entry chunk";
}
loader.module_mapping().push({
application: chunkName,
modules: manifest.chunks[chunkName].modules
});
loader.style.load_multiple(manifest.chunks[chunkName].css_files.map(e => e.file), {
await loadStyles(manifest.chunks[chunkName].css_files.map(e => e.file), {
cache_tag: undefined,
max_parallel_requests: 4
maxParallelRequests: 4
}, (entry, state) => {
if(state !== "loading") {
if (state !== "loading") {
return;
}
loader.setCurrentTaskName(taskId, script_name(entry, false));
loader.setCurrentTaskName(taskId, entry);
});
await loader.scripts.load_multiple(manifest.chunks[chunkName].files.map(e => e.file), {
await loadScripts(manifest.chunks[chunkName].files.map(e => e.file), {
cache_tag: undefined,
max_parallel_requests: 4
maxParallelRequests: 4
}, (script, state) => {
if(state !== "loading") {
return;
}
loader.setCurrentTaskName(taskId, script_name(script, false));
loader.setCurrentTaskName(taskId, script);
});
}

View File

@ -3,21 +3,13 @@ import * as loader from "../loader/loader";
import {ApplicationLoader} from "../loader/loader";
import {loadManifest, loadManifestTarget} from "../maifest";
/* all javascript loaders */
const loader_javascript = {
load_scripts: async taskId => {
loader.setCurrentTaskName(taskId, "manifest");
await loadManifest();
await loadManifestTarget(__build.entry_chunk_name, taskId);
}
};
loader.register_task(loader.Stage.INITIALIZING, {
name: "secure tester",
function: async () => {
/* we need https or localhost to use some things like the storage API */
if(typeof isSecureContext === "undefined")
if(typeof isSecureContext === "undefined") {
(window as any)["isSecureContext"] = location.protocol !== 'https:' || location.hostname === 'localhost';
}
if(!isSecureContext) {
loader.critical_error("TeaWeb cant run on unsecured sides.", "App requires to be loaded via HTTPS!");
@ -28,8 +20,12 @@ loader.register_task(loader.Stage.INITIALIZING, {
});
loader.register_task(loader.Stage.JAVASCRIPT, {
name: "scripts",
function: loader_javascript.load_scripts,
name: "manifest",
function: async taskId => {
loader.setCurrentTaskName(taskId, "manifest");
await loadManifest();
await loadManifestTarget(__build.entry_chunk_name, taskId);
},
priority: 10
});

View File

@ -2,7 +2,7 @@ import "./shared";
import * as loader from "../loader/loader";
import {ApplicationLoader, Stage} from "../loader/loader";
import {loadManifest, loadManifestTarget} from "../maifest";
import {getUrlParameter} from "../loader/utils";
import {getUrlParameter} from "../loader/Utils";
export default class implements ApplicationLoader {
execute() {

View File

@ -1,6 +1,6 @@
import * as loader from "../loader/loader";
import {Stage} from "../loader/loader";
import {BrowserInfo, detect as detectBrowser,} from "detect-browser";
import {detect as detectBrowser} from "detect-browser";
loader.register_task(Stage.SETUP, {
name: "app init",