Mostly using direct assets instead of stored files somewhere within the project.

master
WolverinDEV 2021-03-17 13:28:27 +01:00
parent 66021b125b
commit 61da22895f
36 changed files with 3709 additions and 2755 deletions

52
file.ts
View File

@ -30,49 +30,26 @@ type ProjectResource = {
}
const APP_FILE_LIST_SHARED_SOURCE: ProjectResource[] = [
{ /* javascript files as manifest.json */
"type": "js",
"search-pattern": /.*\.(js|json|svg|png|css|html)$/,
"build-target": "dev|rel",
"path": "js/",
"local-path": "./dist/"
},
{ /* shared html files */
"type": "html",
"search-pattern": /^.*([a-zA-Z]+)\.(html|json)$/,
"build-target": "dev|rel",
"path": "./",
"local-path": "./shared/html/"
},
{ /* javascript files as manifest.json */
"type": "js",
"search-pattern": /.*\.(js|json|svg|png)$/,
"build-target": "dev|rel",
"path": "js/",
"local-path": "./dist/"
},
{ /* javascript files as manifest.json */
"type": "html",
"search-pattern": /.*\.html$/,
"build-target": "dev|rel",
"path": "./",
"local-path": "./dist/"
},
{ /* Loader css file (only required in dev mode. In release it gets inlined) */
"type": "css",
"search-pattern": /.*\.css$/,
"build-target": "dev",
"path": "css/",
"local-path": "./loader/css/"
},
{ /* shared sound files */
"type": "wav",
"search-pattern": /.*\.wav$/,
"build-target": "dev|rel",
"path": "audio/",
"local-path": "./shared/audio/"
},
{ /* shared data sound files */
"type": "json",
"search-pattern": /.*\.json/,
"search-pattern": /.*\.(wav|json)$/,
"build-target": "dev|rel",
"path": "audio/",
@ -87,15 +64,6 @@ const APP_FILE_LIST_SHARED_SOURCE: ProjectResource[] = [
"path": "img/",
"local-path": "./shared/img/"
},
{ /* assembly files */
"web-only": true,
"type": "wasm",
"search-pattern": /.*\.(wasm)/,
"build-target": "dev|rel",
"path": "js/",
"local-path": "./dist/"
}
];
const APP_FILE_LIST_SHARED_VENDORS: ProjectResource[] = [];

View File

@ -1,31 +0,0 @@
import * as path from "path";
import EJSGenerator = require("../webpack/EJSGenerator");
class IndexGenerator extends EJSGenerator {
constructor(options: {
buildTarget: string;
output: string,
isDevelopment: boolean
}) {
super({
variables: {
build_target: options.buildTarget
},
output: options.output,
initialJSEntryChunk: "loader",
input: path.join(__dirname, "html/index.html.ejs"),
minify: !options.isDevelopment,
embedInitialJSEntryChunk: !options.isDevelopment,
embedInitialCSSFile: !options.isDevelopment,
initialCSSFile: {
localFile: path.join(__dirname, "css/index.css"),
publicFile: "css/index.css"
}
});
}
}
export = IndexGenerator;

View File

@ -8,7 +8,4 @@ body {
*, :before, :after {
box-sizing: border-box;
outline: none;
}
@import "loader";
@import "overlay";
}

3
loader/app/css/index.ts Normal file
View File

@ -0,0 +1,3 @@
import "!style-loader!css-loader?url=false!sass-loader?sourceMap=true!./index.scss";
import "!style-loader!css-loader?url=false!sass-loader?sourceMap=true!./loader.scss";
import "!style-loader!css-loader?url=false!sass-loader?sourceMap=true!./overlay.css";

View File

@ -80,7 +80,7 @@ $loop-time-halloween: 25s / 24;
width: 249px;
height: 125px;
background: url("img/loader/steam.png") 0 0, url("../img/loader/steam.png") 0 0;
background: url("../images/steam.png") 0 0;
animation: sprite-steam 2.5s steps(50) forwards infinite;
}

View File

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 188 KiB

View File

@ -1,10 +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 */

View File

@ -1,5 +1,5 @@
import * as script_loader from "./script_loader";
import * as template_loader from "./template_loader";
import * as style_loader from "./style_loader";
import * as Animation from "../animation";
import {getUrlParameter} from "./utils";
@ -111,8 +111,6 @@ export type ModuleMapping = {
const module_mapping_: ModuleMapping[] = [];
export function module_mapping() : ModuleMapping[] { return module_mapping_; }
export function get_cache_version() { return cache_tag; }
export function finished() {
return currentStage == Stage.DONE;
}
@ -360,7 +358,7 @@ export type DependSource = {
export type SourcePath = string | DependSource | string[];
export const scripts = script_loader;
export const templates = template_loader;
export const style = style_loader;
/* Hello World message */
{

View File

@ -0,0 +1,142 @@
import {config, critical_error, SourcePath} from "./loader";
import {load_parallel, LoadCallback, LoadSyntaxError, ParallelOptions, script_name} from "./utils";
let _style_promises: {[key: string]: Promise<void>} = {};
function load_style_url(url: string) : Promise<void> {
if(typeof _style_promises[url] === "object")
return _style_promises[url];
return (_style_promises[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(event.filename == tag.href) { //FIXME!
window.removeEventListener('error', error_handler as any);
reject(new SyntaxError(event.error));
event.preventDefault();
error = true;
}
};
window.addEventListener('error', error_handler as any);
tag.type = "text/css";
tag.rel = "stylesheet";
const cleanup = () => {
tag.onerror = undefined;
tag.onload = undefined;
clearTimeout(timeout_handle);
window.removeEventListener('error', error_handler as any);
};
const timeout_handle = setTimeout(() => {
cleanup();
reject("timeout");
}, 5000);
tag.onerror = error => {
cleanup();
tag.remove();
if(config.error)
console.error("File load error for file %s: %o", url, error);
reject("failed to load file " + url);
};
tag.onload = () => {
cleanup();
{
const css: CSSStyleSheet = tag.sheet as CSSStyleSheet;
const rules = css.cssRules;
const rules_remove: number[] = [];
const rules_add: string[] = [];
for(let index = 0; index < rules.length; index++) {
const rule = rules.item(index);
let rule_text = rule.cssText;
if(rule.cssText.indexOf("%%base_path%%") != -1) {
rules_remove.push(index);
rules_add.push(rule_text.replace("%%base_path%%", document.location.origin + document.location.pathname));
}
}
for(const index of rules_remove.sort((a, b) => b > a ? 1 : 0)) {
if(css.removeRule)
css.removeRule(index);
else
css.deleteRule(index);
}
for(const rule of rules_add)
css.insertRule(rule, rules_remove[0]);
}
if(config.verbose) console.debug("Style sheet %o loaded", url);
setTimeout(resolve, 100);
};
document.getElementById("style").appendChild(tag);
tag.href = config.baseUrl + url;
})).then(result => {
/* cleanup memory */
_style_promises[url] = Promise.resolve(); /* this promise does not holds the whole script tag and other memory */
return _style_promises[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];
});
}
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 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);
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);
if(style.error instanceof LoadSyntaxError) {
console.log(" - %s: %o", sname, style.error.source);
} else {
console.log(" - %s: %o", sname, style.error);
}
}
}
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);
}
}

View File

@ -1,90 +0,0 @@
import {config, critical_error, SourcePath} from "./loader";
import {load_parallel, LoadCallback, LoadSyntaxError, ParallelOptions, script_name} from "./utils";
let _template_promises: {[key: string]: Promise<void>} = {};
function load_template_url(url: string) : Promise<void> {
if(typeof _template_promises[url] === "object")
return _template_promises[url];
return (_template_promises[url] = (async () => {
const response = await (await fetch(config.baseUrl + url)).text();
let node = document.createElement("html");
node.innerHTML = response;
let tags: HTMLCollection;
if(node.getElementsByTagName("body").length > 0)
tags = node.getElementsByTagName("body")[0].children;
else
tags = node.children;
let root = document.getElementById("templates");
if(!root) {
critical_error("Failed to find template tag!");
throw "Failed to find template tag";
}
while(tags.length > 0){
let tag = tags.item(0);
root.appendChild(tag);
}
})()).then(result => {
/* cleanup memory */
_template_promises[url] = Promise.resolve(); /* this promise does not holds the whole script tag and other memory */
return _template_promises[url];
}).catch(error => {
/* cleanup memory */
_template_promises[url] = Promise.reject(error); /* this promise does not holds the whole script tag and other memory */
return _template_promises[url];
});
}
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(!_template_promises[depend])
throw "Missing dependency " + depend;
await _template_promises[depend];
}
await load_template_url(source.url + (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);
if(result.failed.length > 0) {
if(config.error) {
console.error("Failed to load the following template files:");
for(const style of result.failed) {
const sname = script_name(style.request, false);
if(style.error instanceof LoadSyntaxError) {
console.log(" - %s: %o", sname, style.error.source);
} else {
console.log(" - %s: %o", sname, style.error);
}
}
}
critical_error("Failed to load template file " + script_name(result.failed[0].request, true) + " <br>" + "View the browser console for more information!");
throw "failed to load template file " + script_name(result.failed[0].request, false);
}
}

View File

@ -11,6 +11,10 @@ export interface TeaManifest {
hash: string,
file: string
}[],
css_files: {
hash: string,
file: string
}[],
modules: {
id: string,
context: string,
@ -52,12 +56,24 @@ export async function loadManifestTarget(chunkName: string, taskId: number) {
modules: manifest.chunks[chunkName].modules
});
loader.style.load_multiple(manifest.chunks[chunkName].css_files.map(e => "js/" + e.file), {
cache_tag: undefined,
max_parallel_requests: 4
}, (entry, state) => {
if(state !== "loading") {
return;
}
loader.setCurrentTaskName(taskId, script_name(entry, false));
});
await loader.scripts.load_multiple(manifest.chunks[chunkName].files.map(e => "js/" + e.file), {
cache_tag: undefined,
max_parallel_requests: 4
}, (script, state) => {
if(state !== "loading")
if(state !== "loading") {
return;
}
loader.setCurrentTaskName(taskId, script_name(script, false));
});

View File

@ -1,5 +1,5 @@
/* IE11 and safari */
if(Element.prototype.remove === undefined)
if(Element.prototype.remove === undefined) {
Object.defineProperty(Element.prototype, "remove", {
enumerable: false,
configurable: false,
@ -8,6 +8,7 @@ if(Element.prototype.remove === undefined)
this.parentElement.removeChild(this);
}
});
}
/* IE11 */
function ReplaceWithPolyfill() {
@ -28,14 +29,17 @@ function ReplaceWithPolyfill() {
parent.insertBefore(currentNode, this.nextSibling);
}
}
if (!Element.prototype.replaceWith)
if (!Element.prototype.replaceWith) {
Element.prototype.replaceWith = ReplaceWithPolyfill;
}
if (!CharacterData.prototype.replaceWith)
if (!CharacterData.prototype.replaceWith) {
CharacterData.prototype.replaceWith = ReplaceWithPolyfill;
}
if (!DocumentType.prototype.replaceWith)
if (!DocumentType.prototype.replaceWith) {
DocumentType.prototype.replaceWith = ReplaceWithPolyfill;
}
// Source: https://github.com/jserz/js_piece/blob/master/DOM/ParentNode/append()/append().md
(function (arr) {

View File

@ -1,26 +1,8 @@
import "./shared";
import * as loader from "../loader/loader";
import {ApplicationLoader, SourcePath} from "../loader/loader";
import {script_name} from "../loader/utils";
import {ApplicationLoader} from "../loader/loader";
import {loadManifest, loadManifestTarget} from "../maifest";
declare global {
interface Window {
native_client: boolean;
}
}
function getCacheTag() {
return "?_ts=" + (__build.mode === "debug" ? Date.now() : __build.timestamp);
}
const LoaderTaskCallback = taskId => (script: SourcePath, state) => {
if(state !== "loading")
return;
loader.setCurrentTaskName(taskId, script_name(script, false));
};
/* all javascript loaders */
const loader_javascript = {
load_scripts: async taskId => {
@ -30,33 +12,12 @@ const loader_javascript = {
}
};
const loader_webassembly = {
test_webassembly: async () => {
/* We dont required WebAssembly anymore for fundamental functions, only for auto decoding
if(typeof (WebAssembly) === "undefined" || typeof (WebAssembly.compile) === "undefined") {
console.log(navigator.browserSpecs);
if (navigator.browserSpecs.name == 'Safari') {
if (parseInt(navigator.browserSpecs.version) < 11) {
displayCriticalError("You require Safari 11 or higher to use the web client!<br>Safari " + navigator.browserSpecs.version + " does not support WebAssambly!");
return;
}
}
else {
// Do something for all other browsers.
}
displayCriticalError("You require WebAssembly for TeaSpeak-Web!");
throw "Missing web assembly";
}
*/
}
};
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")
(<any>window)["isSecureContext"] = location.protocol !== 'https:' || location.hostname === 'localhost';
(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!");
@ -66,33 +27,12 @@ loader.register_task(loader.Stage.INITIALIZING, {
priority: 20
});
loader.register_task(loader.Stage.INITIALIZING, {
name: "webassembly tester",
function: loader_webassembly.test_webassembly,
priority: 20
});
loader.register_task(loader.Stage.JAVASCRIPT, {
name: "scripts",
function: loader_javascript.load_scripts,
priority: 10
});
loader.register_task(loader.Stage.TEMPLATES, {
name: "templates",
function: async taskId => {
await loader.templates.load_multiple([
"templates.html",
"templates/modal/musicmanage.html",
"templates/modal/newcomer.html",
], {
cache_tag: getCacheTag(),
max_parallel_requests: -1
}, LoaderTaskCallback(taskId));
},
priority: 10
});
loader.register_task(loader.Stage.SETUP, {
name: "page setup",
function: async () => {

View File

@ -45,19 +45,6 @@ export default class implements ApplicationLoader {
priority: 10
});
loader.register_task(loader.Stage.TEMPLATES, {
name: "templates",
function: async () => {
await loader.templates.load_multiple([
"templates.html"
], {
cache_tag: "?22",
max_parallel_requests: -1
});
},
priority: 10
});
loader.execute_managed(false);
}
}

View File

@ -13,14 +13,11 @@ loader.register_task(Stage.SETUP, {
}
window.__native_client_init_hook();
window.native_client = true;
} else {
if(__build.target !== "web") {
loader.critical_error("App seems not to be compiled for the web.", "This app has been compiled for " + __build.target);
return;
}
window.native_client = false;
}
},
priority: 1000
@ -50,7 +47,6 @@ loader.register_task(Stage.SETUP, {
case "ie":
loader.critical_error("Browser not supported", "We're sorry, but your browser isn't supported.");
throw "unsupported browser";
}
},
priority: 50

View File

@ -1,2 +0,0 @@
**/*.css
**/*.css.map

View File

@ -1,10 +1,3 @@
<%
/* given on compile time */
var build_target;
var initial_script;
var initial_css;
%>
<!DOCTYPE html>
<html lang="en">
<head>
@ -16,20 +9,20 @@ var initial_css;
<meta name="og:description" content="The TeaSpeak Web client is a in the browser running client for the VoIP communication software TeaSpeak." />
<meta name="og:url" content="https://web.teaspeak.de/">
<%# TODO: Put in an appropirate image <meta name="og:image" content="https://www.whatsapp.com/img/whatsapp-promo.png"> %>
<% /* TODO: Put in an appropriate image <meta name="og:image" content="https://www.whatsapp.com/img/whatsapp-promo.png"> */ %>
<%# Using an absolute path here since the manifest.json works only with such. %>
<% /* Using an absolute path here since the manifest.json works only with such. */ %>
<link rel="manifest" href="/manifest.json">
<% if(build_target === "client") { %>
<% if(buildTarget === "client") { %>
<title>TeaClient</title>
<meta name='og:title' content='TeaClient'>
<% } else { %>
<% } else { %>
<title>TeaSpeak-Web</title>
<meta name='og:title' content='TeaSpeak-Web'>
<link rel='shortcut icon' href='img/favicon/teacup.png' type='image/x-icon' id="favicon">
<%# <link rel="apple-touch-icon" sizes="194x194" href="/apple-touch-icon.png" type="image/png"> %>
<% } %>
<% /* <link rel="apple-touch-icon" sizes="194x194" href="/apple-touch-icon.png" type="image/png"> */ %>
<% } %>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="format-detection" content="telephone=no">
@ -49,10 +42,10 @@ var initial_css;
<link rel="preload" as="image" href="img/loader/initial-sequence.gif">
<link rel="preload" as="image" href="img/loader/bowl.png">
<%# We don't preload the bowl since it's only a div background %>
<% /* We don't preload the bowl since it's only a div background */ %>
<link rel="preload" as="image" href="img/loader/text.png">
<%- initial_css %>
<%= htmlWebpackPlugin.tags.headTags %>
</head>
<body>
<!-- No javascript error -->
@ -101,6 +94,6 @@ var initial_css;
</div>
</div>
<%- initial_script %>
<%= htmlWebpackPlugin.tags.bodyTags %>
</body>
</html>

5475
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,8 @@
"build-client": "webpack --config webpack-client.config.js",
"webpack-web": "webpack --config webpack-web.config.js",
"webpack-client": "webpack --config webpack-client.config.js",
"generate-i18n-gtranslate": "node shared/generate_i18n_gtranslate.js"
"generate-i18n-gtranslate": "node shared/generate_i18n_gtranslate.js",
"dev-server": "webpack serve --config webpack-web.config.js"
},
"author": "TeaSpeak (WolverinDEV)",
"license": "ISC",
@ -29,12 +30,13 @@
"@types/html-minifier": "^3.5.3",
"@types/jquery": "^3.3.34",
"@types/jsrender": "^1.0.5",
"@types/loader-utils": "^1.1.3",
"@types/lodash": "^4.14.149",
"@types/moment": "^2.13.0",
"@types/node": "^12.7.2",
"@types/react-color": "^3.0.4",
"@types/react-dom": "^16.9.5",
"@types/react-grid-layout": "^1.1.1",
"@types/react-transition-group": "^4.4.0",
"@types/remarkable": "^1.7.4",
"@types/sdp-transform": "^2.4.4",
"@types/sha256": "^0.2.0",
@ -43,13 +45,11 @@
"@types/xml-parser": "^1.2.29",
"@wasm-tool/wasm-pack-plugin": "^1.3.1",
"babel-loader": "^8.1.0",
"chunk-manifest-webpack-plugin": "^1.1.2",
"circular-dependency-plugin": "^5.2.0",
"clean-css": "^4.2.1",
"clean-webpack-plugin": "^3.0.0",
"css-loader": "^3.6.0",
"csso-cli": "^3.0.0",
"ejs": "^3.0.2",
"css-minimizer-webpack-plugin": "^1.3.0",
"exports-loader": "^0.7.0",
"fast-xml-parser": "^3.17.4",
"file-loader": "^6.0.0",
@ -57,13 +57,16 @@
"gulp": "^4.0.2",
"html-loader": "^1.0.0",
"html-minifier": "^4.0.0",
"html-webpack-plugin": "^4.0.3",
"html-webpack-inline-source-plugin": "0.0.10",
"html-webpack-plugin": "^5.3.1",
"inline-chunks-html-webpack-plugin": "^1.3.1",
"mime-types": "^2.1.24",
"mini-css-extract-plugin": "^0.9.0",
"mini-css-extract-plugin": "^1.3.9",
"mkdirp": "^0.5.1",
"node-sass": "^4.14.1",
"potpack": "^1.0.1",
"raw-loader": "^4.0.0",
"react-dev-utils": "^11.0.4",
"sass": "1.22.10",
"sass-loader": "^8.0.2",
"sha256": "^0.2.0",
@ -76,12 +79,11 @@
"typescript": "^3.7.0",
"url-loader": "^4.1.1",
"wabt": "^1.0.13",
"webpack": "^4.42.1",
"webpack": "^5.26.1",
"webpack-bundle-analyzer": "^3.6.1",
"webpack-cli": "^3.3.11",
"webpack-svg-sprite-generator": "^1.0.17",
"worker-plugin": "^4.0.3",
"xml-parser": "^1.2.1"
"webpack-cli": "^4.5.0",
"webpack-dev-server": "^3.11.2",
"webpack-svg-sprite-generator": "^5.0.2"
},
"repository": {
"type": "git",
@ -92,8 +94,6 @@
},
"homepage": "https://www.teaspeak.de",
"dependencies": {
"@types/react-grid-layout": "^1.1.1",
"@types/react-transition-group": "^4.4.0",
"broadcastchannel-polyfill": "^1.0.1",
"detect-browser": "^5.2.0",
"dompurify": "^2.0.8",

View File

@ -43,6 +43,8 @@ if [[ "$1" == "full" ]]; then
echo "Full cleanup. Deleting generated javascript and css files"
cleanup_files "shared/js" "*.js" "JavaScript"
cleanup_files "shared/js" "*.js.map" "JavaScript-Mapping"
cleanup_files "shared/js" "*.css" "JavaScript - CSS"
cleanup_files "shared/js" "*.css.map" "JavaScript - CSS - Mapping"
cleanup_files "shared/css/static/" "*.css" "CSS" # We only use SCSS, not CSS
cleanup_files "shared/css/static/" "*.css.map" "CSS-Mapping"

View File

@ -1,74 +0,0 @@
#!/usr/bin/env bash
cd "$(dirname "$0")" || exit 1
#find css/static/ -name '*.css' -exec cat {} \; | npm run csso -- --output `pwd`/generated/static/base.css
#File order
files=(
"css/static/properties.css"
"css/static/main-layout.css"
"css/static/general.css"
"css/static/channel-tree.css"
"css/static/connection_handlers.css"
"css/static/context_menu.css"
"css/static/frame-chat.css"
"css/static/server-log.css"
"css/static/scroll.css"
"css/static/hostbanner.css"
"css/static/htmltags.css"
"css/static/menu-bar.css"
"css/static/mixin.css"
"css/static/modal.css"
"css/static/modals.css"
"css/static/modal-about.css"
"css/static/modal-avatar.css"
"css/static/modal-banclient.css"
"css/static/modal-banlist.css"
"css/static/modal-bookmarks.css"
"css/static/modal-channel.css"
"css/static/modal-channelinfo.css"
"css/static/modal-clientinfo.css"
"css/static/modal-connect.css"
"css/static/modal-group-assignment.css"
"css/static/modal-icons.css"
"css/static/modal-identity.css"
"css/static/modal-newcomer.css"
"css/static/modal-invite.css"
"css/static/modal-keyselect.css"
"css/static/modal-poke.css"
"css/static/modal-query.css"
"css/static/modal-server.css"
"css/static/modal-musicmanage.css"
"css/static/modal-serverinfobandwidth.css"
"css/static/modal-serverinfo.css"
"css/static/modal-settings.css"
"css/static/overlay-image-preview.css"
"css/static/ts/tab.css"
"css/static/ts/chat.css"
"css/static/ts/icons.css"
"css/static/ts/icons_em.css"
"css/static/ts/country.css"
)
target_file=`pwd`/../generated/static/base.css
if [[ ! -d $(dirname ${target_file}) ]]; then
echo "Creating target path ($(dirname "${target_file}"))"
mkdir -p $(dirname "${target_file}")
if [[ $? -ne 0 ]]; then
echo "Failed to create target path!"
exit 1
fi
fi
echo "/* Auto generated merged CSS file */" > "${target_file}"
for file in "${files[@]}"; do
if [[ ${file} =~ css/* ]]; then
file="./${file:4}"
fi
cat "${file}" >> "${target_file}"
done
cat "${target_file}" | npm run csso -- --output "$(pwd)/../generated/static/base.css"

View File

@ -1,4 +1,9 @@
declare module "*.png" {
const value: any;
export = value;
}
declare module "*.html" {
const value: any;
export = value;
}

View File

@ -583,4 +583,4 @@ loader.register_task(Stage.JAVASCRIPT_INITIALIZING,{
}
},
priority: 100
});
});

View File

@ -294,7 +294,8 @@ class IdentityPOWWorker {
private _initialized = false;
async initialize(key: string) {
this._worker = new Worker("tc-shared/workers/pow", { type: "module" });
// @ts-ignore
this._worker = new Worker(new URL("tc-shared/workers/pow", import.meta.url));
/* initialize */
await new Promise<void>((resolve, reject) => {

View File

@ -228,21 +228,17 @@ if(typeof ($) !== "undefined") {
if(!$.fn.renderTag) {
$.fn.renderTag = function (this: JQuery, values?: any) : JQuery {
let result;
if(this.render) {
result = $(this.render(values));
} else {
const template = jsrender.render[this.attr("id")];
if(!template) {
console.error("Tried to render template %o, but template is not available!", this.attr("id"));
throw "missing template " + this.attr("id");
}
/*
result = window.jsrender.templates("tmpl_permission_entry", $("#tmpl_permission_entry").html());
result = window.jsrender.templates("xxx", this.html());
*/
result = template(values);
result = $(result);
const template = $.views.templates[this.attr("id")];
if(!template) {
console.error("Tried to render template %o, but template is not available!", this.attr("id"));
throw "missing template " + this.attr("id");
}
/*
result = window.jsrender.templates("tmpl_permission_entry", $("#tmpl_permission_entry").html());
result = window.jsrender.templates("xxx", this.html());
*/
result = template(values);
result = $(result);
result.find("node").each((index, element) => {
$(element).replaceWith(values[$(element).attr("key")] || (values[0] || [])[$(element).attr("key")]);
});

View File

@ -20,7 +20,6 @@ import {ChannelTreeRenderer} from "tc-shared/ui/tree/Renderer";
import {ChannelTreeUIEvents} from "tc-shared/ui/tree/Definitions";
const cssStyle = require("./AppRenderer.scss");
const VideoFrame = React.memo((props: { events: Registry<AppUiEvents> }) => {
const refElement = React.useRef<HTMLDivElement>();
const [ container, setContainer ] = useState<HTMLDivElement | undefined>(() => {

View File

@ -2,6 +2,26 @@ import * as loader from "tc-loader";
import moment from "moment";
import {LogCategory, logError, logTrace} from "../log";
import {tr} from "tc-shared/i18n/localize";
import TemplateFile from "../../html/templates.html";
import TemplateMusicManage from "../../html/templates/modal/musicmanage.html";
import TemplateNewComer from "../../html/templates/modal/newcomer.html";
function initializeHtml(html: string) {
const hangingPoint = document.getElementById("templates");
const node = document.createElement("html");
node.innerHTML = html;
for(const element of node.getElementsByClassName("jsrender-template")) {
if(!$.templates(element.id, element.innerHTML)) {
logError(LogCategory.GENERAL, tr("Failed to setup cache for js renderer template %s!"), element.id);
} else {
logTrace(LogCategory.GENERAL, tr("Successfully loaded jsrender template %s"), element.id);
const elem = document.createElement("div");
elem.id = element.id;
hangingPoint?.appendChild(elem); }
}
}
export function setupJSRender() : boolean {
if(!$.views) {
@ -25,11 +45,8 @@ export function setupJSRender() : boolean {
return /* @tr-ignore */ tr(args[0]);
});
$(".jsrender-template").each((idx, _entry) => {
if(!$.templates(_entry.id, _entry.innerHTML)) {
logError(LogCategory.GENERAL, tr("Failed to setup cache for js renderer template %s!"), _entry.id);
} else
logTrace(LogCategory.GENERAL, tr("Successfully loaded jsrender template %s"), _entry.id);
});
initializeHtml(TemplateFile);
initializeHtml(TemplateMusicManage);
initializeHtml(TemplateNewComer);
return true;
}

View File

@ -14,12 +14,9 @@
"webpack-web.config.ts",
"webpack/build-definitions.d.ts",
"webpack/ManifestPlugin.ts",
"webpack/EJSGenerator.ts",
"webpack/HtmlWebpackInlineSource.ts",
"webpack/WatLoader.ts",
"webpack/DevelBlocks.ts",
"loader/IndexGenerator.ts",
"webpack/ManifestPlugin.ts",
"babel.config.ts",
"file.ts"

View File

@ -25,6 +25,7 @@ async function initializeFaviconRenderer() {
iconImage = new Image();
iconImage.src = kClientSpriteUrl;
await new Promise((resolve, reject) => {
iconImage.onload = resolve;
iconImage.onerror = () => reject("failed to load client icon sprite");

View File

@ -3,7 +3,7 @@ import * as config_base from "./webpack.config";
export = () => config_base.config("client").then(config => {
Object.assign(config.entry, {
"client-app": "./client/app/index.ts"
"client-app": ["./client/app/index.ts"]
});
Object.assign(config.resolve.alias, {
@ -15,7 +15,7 @@ export = () => config_base.config("client").then(config => {
if(!Array.isArray(config.externals))
throw "invalid config";
config.externals.push((context, request, callback) => {
config.externals.push(({ context, request }, callback) => {
if (request.startsWith("tc-backend/")) {
return callback(null, `window["backend-loader"].require("${request}")`);
}

View File

@ -3,8 +3,8 @@ import * as config_base from "./webpack.config";
export = () => config_base.config("web").then(config => {
Object.assign(config.entry, {
"shared-app": "./web/app/index.ts",
"modal-external": "./web/app/index-external.ts"
"shared-app": ["./web/app/index.ts"],
"modal-external": ["./web/app/index-external.ts"]
});
Object.assign(config.resolve.alias, {
@ -13,7 +13,5 @@ export = () => config_base.config("web").then(config => {
"tc-backend": path.resolve(__dirname, "web/app"),
});
config.node = config.node || {};
config.node["fs"] = "empty";
return Promise.resolve(config);
});

View File

@ -3,20 +3,23 @@ import * as fs from "fs";
import trtransformer from "./tools/trgen/ts_transformer";
import {exec} from "child_process";
import * as util from "util";
import { Plugin as SvgSpriteGenerator } from "webpack-svg-sprite-generator";
import LoaderIndexGenerator from "./loader/IndexGenerator";
import {Configuration} from "webpack";
const path = require('path');
const webpack = require("webpack");
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
import { Plugin as SvgSpriteGenerator } from "webpack-svg-sprite-generator";
const ManifestGenerator = require("./webpack/ManifestPlugin");
const WorkerPlugin = require('worker-plugin');
const InlineChunkHtmlPlugin = require("react-dev-utils/InlineChunkHtmlPlugin");
import HtmlWebpackPlugin from "html-webpack-plugin";
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const { WebpackManifestPlugin } = require('webpack-manifest-plugin');
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
export let isDevelopment = process.env.NODE_ENV === 'development';
console.log("Webpacking for %s (%s)", isDevelopment ? "development" : "production", process.env.NODE_ENV || "NODE_ENV not specified");
@ -63,34 +66,60 @@ const isLoaderFile = (file: string) => {
return false;
};
export const config = async (target: "web" | "client"): Promise<Configuration> => ({
const generateIndexPlugin = (target: "web" | "client"): HtmlWebpackPlugin => {
const options: HtmlWebpackPlugin.Options & { inlineSource?: RegExp | string } = {};
options.cache = true;
options.chunks = ["loader"];
options.inject = false;
options.template = path.join(__dirname, "loader", "index.ejs");
options.templateParameters = { buildTarget: target };
options.scriptLoading = "defer";
if(!isDevelopment) {
options.minify = {
html5: true,
collapseWhitespace: true,
removeComments: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeTagWhitespace: true,
minifyCSS: true,
minifyJS: true,
minifyURLs: true,
};
options.inlineSource = /\.(js|css)$/;
}
return new HtmlWebpackPlugin(options);
}
export const config = async (target: "web" | "client"): Promise<Configuration & { devServer: any }> => ({
entry: {
"loader": "./loader/app/index.ts",
"modal-external": "./shared/js/ui/react-elements/external-modal/PopoutEntrypoint.ts",
"loader": ["./loader/app/index.ts"],
"modal-external": ["./shared/js/ui/react-elements/external-modal/PopoutEntrypoint.ts"],
//"devel-main": "./shared/js/devel_main.ts"
},
devtool: isDevelopment ? "inline-source-map" : undefined,
mode: isDevelopment ? "development" : "production",
plugins: [
//new CleanWebpackPlugin(),
new CleanWebpackPlugin(),
new MiniCssExtractPlugin({
filename: isDevelopment ? '[name].css' : '[name].[hash].css',
chunkFilename: isDevelopment ? '[id].css' : '[id].[hash].css'
filename: isDevelopment ? '[name].css' : '[name].[contenthash].css',
chunkFilename: isDevelopment ? '[id].css' : '[id].[contenthash].css',
ignoreOrder: true
}),
new ManifestGenerator({
file: path.join(__dirname, "dist/manifest.json"),
base: __dirname
outputFileName: "manifest.json",
context: __dirname
}),
new WorkerPlugin(),
//new BundleAnalyzerPlugin(),
isDevelopment ? undefined : new webpack.optimize.AggressiveSplittingPlugin({
minSize: 1024 * 8,
maxSize: 1024 * 128
}),
new webpack.DefinePlugin(await generateDefinitions(target)),
new SvgSpriteGenerator({
dtsOutputFolder: path.join(__dirname, "shared", "svg-sprites"),
publicPath: "js/",
configurations: {
"client-icons": {
folder: path.join(__dirname, "shared", "img", "client-icons"),
@ -125,20 +154,22 @@ export const config = async (target: "web" | "client"): Promise<Configuration> =
}
}
}),
new LoaderIndexGenerator({
buildTarget: target,
output: path.join(__dirname, "dist/index.html"),
isDevelopment: isDevelopment
})
generateIndexPlugin(target),
new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/.*/]),
].filter(e => !!e),
module: {
rules: [
{
test: /\.(s[ac]|c)ss$/,
loader: [
'style-loader',
//MiniCssExtractPlugin.loader,
use: [
//'style-loader',
{
loader: MiniCssExtractPlugin.loader,
options: {
esModule: false
}
},
{
loader: 'css-loader',
options: {
@ -161,7 +192,7 @@ export const config = async (target: "web" | "client"): Promise<Configuration> =
test: (module: string) => module.match(/\.tsx?$/) && !isLoaderFile(module),
exclude: /node_modules/,
loader: [
use: [
{
loader: 'ts-loader',
options: {
@ -185,7 +216,7 @@ export const config = async (target: "web" | "client"): Promise<Configuration> =
test: (module: string) => module.match(/\.tsx?$/) && isLoaderFile(module),
exclude: /(node_modules|bower_components)/,
loader: [
use: [
{
loader: "babel-loader",
options: {
@ -202,24 +233,26 @@ export const config = async (target: "web" | "client"): Promise<Configuration> =
},
{
test: /\.was?t$/,
loader: [
use: [
"./webpack/WatLoader.js"
]
},
{
test: /\.svg$/,
loader: 'svg-inline-loader'
use: 'svg-inline-loader'
},
{
test: /ChangeLog\.md$/i,
loader: "raw-loader",
options: {
esModule: false
}
test: /ChangeLog\.md$|\.html$/i,
use: {
loader: "raw-loader",
options: {
esModule: false
}
},
},
{
test: /\.(png|jpg|jpeg|gif)?$/,
loader: 'file-loader',
use: 'file-loader',
},
]
} as any,
@ -245,9 +278,19 @@ export const config = async (target: "web" | "client"): Promise<Configuration> =
},
optimization: {
splitChunks: {
chunks: "all"
chunks: "all",
maxSize: 512 * 1024
},
minimize: !isDevelopment,
minimizer: [new TerserPlugin()]
}
minimizer: [
new TerserPlugin(),
new CssMinimizerPlugin()
]
},
devServer: {
publicPath: "/",
contentBase: path.join(__dirname, 'dist'),
writeToDisk: true,
compress: true
},
});

View File

@ -1,107 +0,0 @@
import * as webpack from "webpack";
import * as ejs from "ejs";
import * as fs from "fs";
import * as util from "util";
import * as minifier from "html-minifier";
import * as path from "path";
import Compilation = webpack.compilation.Compilation;
interface Options {
input: string,
output: string,
minify?: boolean,
initialJSEntryChunk: string,
embedInitialJSEntryChunk?: boolean,
initialCSSFile: {
localFile: string,
publicFile: string
},
embedInitialCSSFile?: boolean,
variables?: {[name: string]: any};
}
class EJSGenerator {
readonly options: Options;
constructor(options: Options) {
this.options = options;
}
private async generateEntryJsTag(compilation: Compilation) {
const entry_group = compilation.chunkGroups.find(e => e.options.name === this.options.initialJSEntryChunk);
if(!entry_group) return; /* not the correct compilation */
const tags = entry_group.chunks.map(chunk => {
if(chunk.files.length !== 1)
throw "invalid chunk file count";
const file = chunk.files[0];
if(path.extname(file) !== ".js")
throw "Entry chunk file has unknown extension";
if(!this.options.embedInitialJSEntryChunk) {
return '<script type="application/javascript" src=' + compilation.compiler.options.output.publicPath + file + ' async defer></script>';
} else {
const script = fs.readFileSync(path.join(compilation.compiler.outputPath, file));
return `<script type="application/javascript">${script}</script>`;
}
});
return tags.join("\n");
}
private async generateEntryCssTag() {
if(this.options.embedInitialCSSFile) {
const style = await util.promisify(fs.readFile)(this.options.initialCSSFile.localFile);
return `<style>${style}</style>`
} else {
return `<link rel="stylesheet" href="${this.options.initialCSSFile.publicFile}">`
}
}
apply(compiler: webpack.Compiler) {
compiler.hooks.afterEmit.tapPromise(this.constructor.name, async compilation => {
const input = await util.promisify(fs.readFile)(this.options.input);
const variables = Object.assign({}, this.options.variables);
variables["initial_script"] = await this.generateEntryJsTag(compilation);
variables["initial_css"] = await this.generateEntryCssTag();
let generated = await ejs.render(input.toString(), variables, {
beautify: false, /* uglify is a bit dump and does not understands ES6 */
context: this
});
if(this.options.minify) {
generated = minifier.minify(generated, {
html5: true,
collapseWhitespace: true,
removeComments: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeTagWhitespace: true,
minifyCSS: true,
minifyJS: true,
minifyURLs: true,
});
}
await util.promisify(fs.writeFile)(this.options.output, generated);
});
compiler.hooks.afterCompile.tapPromise(this.constructor.name, async compilation => {
const file = path.resolve(this.options.input);
if(compilation.fileDependencies.has(file))
return;
console.log("Adding additional watch to %s", file);
compilation.fileDependencies.add(file);
});
}
}
export = EJSGenerator;

View File

@ -1,65 +1,64 @@
import * as webpack from "webpack";
import * as fs from "fs-extra";
import * as path from "path";
interface Options {
file?: string;
base: string;
outputFileName?: string;
context: string;
}
class ManifestGenerator {
private manifest_content;
private readonly options: Options;
readonly options: Options;
constructor(options: Options) {
this.options = options || { base: __dirname };
this.options = options || { context: __dirname };
}
apply(compiler: webpack.Compiler) {
compiler.hooks.afterCompile.tap(this.constructor.name, compilation => {
const chunks_data = {};
compiler.hooks.emit.tap(this.constructor.name, compilation => {
const chunkData = {};
for(const chunkGroup of compilation.chunkGroups) {
const fileJs = [];
const filesCss = [];
const modules = [];
for(const chunk of chunkGroup.chunks) {
if(!chunk.files.length)
if(!chunk.files.size) {
continue;
/*
if(chunk.files.length !== 1) {
console.error("Expected only one file per chunk but got " + chunk.files.length);
chunk.files.forEach(e => console.log(" - %s", e));
throw "expected only one file per chunk";
}
*/
for(const file of chunk.files) {
const extension = path.extname(file);
if(extension === ".js") {
fileJs.push({
hash: chunk.hash,
file: file
});
} else if(extension === ".css") {
filesCss.push({
hash: chunk.hash,
file: file
});
} else if(extension === ".wasm") {
/* do nothing */
} else {
throw "Unknown chunk file with extension " + extension;
switch (extension) {
case ".js":
fileJs.push({
hash: chunk.hash,
file: file
});
break;
case ".css":
filesCss.push({
hash: chunk.hash,
file: file
});
break;
case ".wasm":
break;
default:
throw "Unknown chunk file with extension " + extension;
}
}
for(const module of chunk.getModules()) {
if(!module.type.startsWith("javascript/"))
for(const module of chunk.getModules() as any[]) {
if(!module.type.startsWith("javascript/")) {
continue;
}
if(!module.context)
if(!module.context) {
continue;
}
if(module.context.startsWith("svg-sprites/")) {
/* custom svg sprite handler */
@ -71,37 +70,39 @@ class ManifestGenerator {
continue;
}
if(!module.resource)
if(!module.resource) {
continue;
}
if(module.context !== path.dirname(module.resource))
if(module.context !== path.dirname(module.resource)) {
throw "invalid context/resource relation";
}
modules.push({
id: module.id,
context: path.relative(this.options.base, module.context).replace(/\\/g, "/"),
context: path.relative(this.options.context, module.context).replace(/\\/g, "/"),
resource: path.basename(module.resource)
});
}
}
chunks_data[chunkGroup.options.name] = {
chunkData[chunkGroup.options.name] = {
files: fileJs,
css_files: filesCss,
modules: modules
};
}
this.manifest_content = {
const payload = JSON.stringify({
version: 2,
chunks: chunks_data
};
});
chunks: chunkData
});
compiler.hooks.done.tap(this.constructor.name, () => {
const file = this.options.file || "manifest.json";
fs.mkdirpSync(path.dirname(file));
fs.writeFileSync(this.options.file || "manifest.json", JSON.stringify(this.manifest_content));
const fileName = this.options.outputFileName || "manifest.json";
compilation.assets[fileName] = {
size() { return payload.length; },
source() { return payload; }
} as any;
});
}
}

View File

@ -1,12 +1,7 @@
import * as webpack from "webpack";
import {RawSourceMap} from "source-map";
import LoaderContext = webpack.loader.LoaderContext;
const wabt = require("wabt")();
const filename = "module.wast";
export default function loader(this: LoaderContext, source: string | Buffer, sourceMap?: RawSourceMap): string | Buffer | void | undefined {
export default function loader(source: string | Buffer): string | Buffer | void | undefined {
this.cacheable();
const module = wabt.parseWat(filename, source);