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[] = [ 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 */ { /* shared html files */
"type": "html", "type": "html",
"search-pattern": /^.*([a-zA-Z]+)\.(html|json)$/, "search-pattern": /^.*([a-zA-Z]+)\.(html|json)$/,
"build-target": "dev|rel", "build-target": "dev|rel",
"path": "./", "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/" "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 */ { /* shared sound files */
"type": "wav", "type": "wav",
"search-pattern": /.*\.wav$/, "search-pattern": /.*\.(wav|json)$/,
"build-target": "dev|rel",
"path": "audio/",
"local-path": "./shared/audio/"
},
{ /* shared data sound files */
"type": "json",
"search-pattern": /.*\.json/,
"build-target": "dev|rel", "build-target": "dev|rel",
"path": "audio/", "path": "audio/",
@ -87,15 +64,6 @@ const APP_FILE_LIST_SHARED_SOURCE: ProjectResource[] = [
"path": "img/", "path": "img/",
"local-path": "./shared/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[] = []; 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 { *, :before, :after {
box-sizing: border-box; box-sizing: border-box;
outline: none; 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; width: 249px;
height: 125px; 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; 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 "core-js/stable";
import "./polifill"; import "./polifill";
import "./css";
import * as loader from "./loader/loader"; import * as loader from "./loader/loader";
import {ApplicationLoader} from "./loader/loader"; import {ApplicationLoader} from "./loader/loader";
import {getUrlParameter} from "./loader/utils"; import {getUrlParameter} from "./loader/utils";
if(window["loader"]) {
throw "an loader instance has already been defined";
}
window["loader"] = loader; window["loader"] = loader;
/* let the loader register himself at the window first */ /* let the loader register himself at the window first */

View File

@ -1,5 +1,5 @@
import * as script_loader from "./script_loader"; 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 * as Animation from "../animation";
import {getUrlParameter} from "./utils"; import {getUrlParameter} from "./utils";
@ -111,8 +111,6 @@ export type ModuleMapping = {
const module_mapping_: ModuleMapping[] = []; const module_mapping_: ModuleMapping[] = [];
export function module_mapping() : ModuleMapping[] { return module_mapping_; } export function module_mapping() : ModuleMapping[] { return module_mapping_; }
export function get_cache_version() { return cache_tag; }
export function finished() { export function finished() {
return currentStage == Stage.DONE; return currentStage == Stage.DONE;
} }
@ -360,7 +358,7 @@ export type DependSource = {
export type SourcePath = string | DependSource | string[]; export type SourcePath = string | DependSource | string[];
export const scripts = script_loader; export const scripts = script_loader;
export const templates = template_loader; export const style = style_loader;
/* Hello World message */ /* 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, hash: string,
file: string file: string
}[], }[],
css_files: {
hash: string,
file: string
}[],
modules: { modules: {
id: string, id: string,
context: string, context: string,
@ -52,12 +56,24 @@ export async function loadManifestTarget(chunkName: string, taskId: number) {
modules: manifest.chunks[chunkName].modules 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), { await loader.scripts.load_multiple(manifest.chunks[chunkName].files.map(e => "js/" + e.file), {
cache_tag: undefined, cache_tag: undefined,
max_parallel_requests: 4 max_parallel_requests: 4
}, (script, state) => { }, (script, state) => {
if(state !== "loading") if(state !== "loading") {
return; return;
}
loader.setCurrentTaskName(taskId, script_name(script, false)); loader.setCurrentTaskName(taskId, script_name(script, false));
}); });

View File

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

View File

@ -1,26 +1,8 @@
import "./shared"; import "./shared";
import * as loader from "../loader/loader"; import * as loader from "../loader/loader";
import {ApplicationLoader, SourcePath} from "../loader/loader"; import {ApplicationLoader} from "../loader/loader";
import {script_name} from "../loader/utils";
import {loadManifest, loadManifestTarget} from "../maifest"; 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 */ /* all javascript loaders */
const loader_javascript = { const loader_javascript = {
load_scripts: async taskId => { 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, { loader.register_task(loader.Stage.INITIALIZING, {
name: "secure tester", name: "secure tester",
function: async () => { function: async () => {
/* we need https or localhost to use some things like the storage API */ /* we need https or localhost to use some things like the storage API */
if(typeof isSecureContext === "undefined") 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) { if(!isSecureContext) {
loader.critical_error("TeaWeb cant run on unsecured sides.", "App requires to be loaded via HTTPS!"); 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 priority: 20
}); });
loader.register_task(loader.Stage.INITIALIZING, {
name: "webassembly tester",
function: loader_webassembly.test_webassembly,
priority: 20
});
loader.register_task(loader.Stage.JAVASCRIPT, { loader.register_task(loader.Stage.JAVASCRIPT, {
name: "scripts", name: "scripts",
function: loader_javascript.load_scripts, function: loader_javascript.load_scripts,
priority: 10 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, { loader.register_task(loader.Stage.SETUP, {
name: "page setup", name: "page setup",
function: async () => { function: async () => {

View File

@ -45,19 +45,6 @@ export default class implements ApplicationLoader {
priority: 10 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); loader.execute_managed(false);
} }
} }

View File

@ -13,14 +13,11 @@ loader.register_task(Stage.SETUP, {
} }
window.__native_client_init_hook(); window.__native_client_init_hook();
window.native_client = true;
} else { } else {
if(__build.target !== "web") { if(__build.target !== "web") {
loader.critical_error("App seems not to be compiled for the web.", "This app has been compiled for " + __build.target); loader.critical_error("App seems not to be compiled for the web.", "This app has been compiled for " + __build.target);
return; return;
} }
window.native_client = false;
} }
}, },
priority: 1000 priority: 1000
@ -50,7 +47,6 @@ loader.register_task(Stage.SETUP, {
case "ie": case "ie":
loader.critical_error("Browser not supported", "We're sorry, but your browser isn't supported."); loader.critical_error("Browser not supported", "We're sorry, but your browser isn't supported.");
throw "unsupported browser"; throw "unsupported browser";
} }
}, },
priority: 50 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> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <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: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/"> <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"> <link rel="manifest" href="/manifest.json">
<% if(build_target === "client") { %> <% if(buildTarget === "client") { %>
<title>TeaClient</title> <title>TeaClient</title>
<meta name='og:title' content='TeaClient'> <meta name='og:title' content='TeaClient'>
<% } else { %> <% } else { %>
<title>TeaSpeak-Web</title> <title>TeaSpeak-Web</title>
<meta name='og:title' content='TeaSpeak-Web'> <meta name='og:title' content='TeaSpeak-Web'>
<link rel='shortcut icon' href='img/favicon/teacup.png' type='image/x-icon' id="favicon"> <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 http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="format-detection" content="telephone=no"> <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/initial-sequence.gif">
<link rel="preload" as="image" href="img/loader/bowl.png"> <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"> <link rel="preload" as="image" href="img/loader/text.png">
<%- initial_css %> <%= htmlWebpackPlugin.tags.headTags %>
</head> </head>
<body> <body>
<!-- No javascript error --> <!-- No javascript error -->
@ -101,6 +94,6 @@ var initial_css;
</div> </div>
</div> </div>
<%- initial_script %> <%= htmlWebpackPlugin.tags.bodyTags %>
</body> </body>
</html> </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", "build-client": "webpack --config webpack-client.config.js",
"webpack-web": "webpack --config webpack-web.config.js", "webpack-web": "webpack --config webpack-web.config.js",
"webpack-client": "webpack --config webpack-client.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)", "author": "TeaSpeak (WolverinDEV)",
"license": "ISC", "license": "ISC",
@ -29,12 +30,13 @@
"@types/html-minifier": "^3.5.3", "@types/html-minifier": "^3.5.3",
"@types/jquery": "^3.3.34", "@types/jquery": "^3.3.34",
"@types/jsrender": "^1.0.5", "@types/jsrender": "^1.0.5",
"@types/loader-utils": "^1.1.3",
"@types/lodash": "^4.14.149", "@types/lodash": "^4.14.149",
"@types/moment": "^2.13.0", "@types/moment": "^2.13.0",
"@types/node": "^12.7.2", "@types/node": "^12.7.2",
"@types/react-color": "^3.0.4", "@types/react-color": "^3.0.4",
"@types/react-dom": "^16.9.5", "@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/remarkable": "^1.7.4",
"@types/sdp-transform": "^2.4.4", "@types/sdp-transform": "^2.4.4",
"@types/sha256": "^0.2.0", "@types/sha256": "^0.2.0",
@ -43,13 +45,11 @@
"@types/xml-parser": "^1.2.29", "@types/xml-parser": "^1.2.29",
"@wasm-tool/wasm-pack-plugin": "^1.3.1", "@wasm-tool/wasm-pack-plugin": "^1.3.1",
"babel-loader": "^8.1.0", "babel-loader": "^8.1.0",
"chunk-manifest-webpack-plugin": "^1.1.2",
"circular-dependency-plugin": "^5.2.0", "circular-dependency-plugin": "^5.2.0",
"clean-css": "^4.2.1", "clean-css": "^4.2.1",
"clean-webpack-plugin": "^3.0.0", "clean-webpack-plugin": "^3.0.0",
"css-loader": "^3.6.0", "css-loader": "^3.6.0",
"csso-cli": "^3.0.0", "css-minimizer-webpack-plugin": "^1.3.0",
"ejs": "^3.0.2",
"exports-loader": "^0.7.0", "exports-loader": "^0.7.0",
"fast-xml-parser": "^3.17.4", "fast-xml-parser": "^3.17.4",
"file-loader": "^6.0.0", "file-loader": "^6.0.0",
@ -57,13 +57,16 @@
"gulp": "^4.0.2", "gulp": "^4.0.2",
"html-loader": "^1.0.0", "html-loader": "^1.0.0",
"html-minifier": "^4.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", "mime-types": "^2.1.24",
"mini-css-extract-plugin": "^0.9.0", "mini-css-extract-plugin": "^1.3.9",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"node-sass": "^4.14.1", "node-sass": "^4.14.1",
"potpack": "^1.0.1", "potpack": "^1.0.1",
"raw-loader": "^4.0.0", "raw-loader": "^4.0.0",
"react-dev-utils": "^11.0.4",
"sass": "1.22.10", "sass": "1.22.10",
"sass-loader": "^8.0.2", "sass-loader": "^8.0.2",
"sha256": "^0.2.0", "sha256": "^0.2.0",
@ -76,12 +79,11 @@
"typescript": "^3.7.0", "typescript": "^3.7.0",
"url-loader": "^4.1.1", "url-loader": "^4.1.1",
"wabt": "^1.0.13", "wabt": "^1.0.13",
"webpack": "^4.42.1", "webpack": "^5.26.1",
"webpack-bundle-analyzer": "^3.6.1", "webpack-bundle-analyzer": "^3.6.1",
"webpack-cli": "^3.3.11", "webpack-cli": "^4.5.0",
"webpack-svg-sprite-generator": "^1.0.17", "webpack-dev-server": "^3.11.2",
"worker-plugin": "^4.0.3", "webpack-svg-sprite-generator": "^5.0.2"
"xml-parser": "^1.2.1"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -92,8 +94,6 @@
}, },
"homepage": "https://www.teaspeak.de", "homepage": "https://www.teaspeak.de",
"dependencies": { "dependencies": {
"@types/react-grid-layout": "^1.1.1",
"@types/react-transition-group": "^4.4.0",
"broadcastchannel-polyfill": "^1.0.1", "broadcastchannel-polyfill": "^1.0.1",
"detect-browser": "^5.2.0", "detect-browser": "^5.2.0",
"dompurify": "^2.0.8", "dompurify": "^2.0.8",

View File

@ -43,6 +43,8 @@ if [[ "$1" == "full" ]]; then
echo "Full cleanup. Deleting generated javascript and css files" echo "Full cleanup. Deleting generated javascript and css files"
cleanup_files "shared/js" "*.js" "JavaScript" cleanup_files "shared/js" "*.js" "JavaScript"
cleanup_files "shared/js" "*.js.map" "JavaScript-Mapping" 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" "CSS" # We only use SCSS, not CSS
cleanup_files "shared/css/static/" "*.css.map" "CSS-Mapping" 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" { declare module "*.png" {
const value: any; const value: any;
export = value; export = value;
}
declare module "*.html" {
const value: any;
export = value;
} }

View File

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

View File

@ -294,7 +294,8 @@ class IdentityPOWWorker {
private _initialized = false; private _initialized = false;
async initialize(key: string) { 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 */ /* initialize */
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {

View File

@ -228,21 +228,17 @@ if(typeof ($) !== "undefined") {
if(!$.fn.renderTag) { if(!$.fn.renderTag) {
$.fn.renderTag = function (this: JQuery, values?: any) : JQuery { $.fn.renderTag = function (this: JQuery, values?: any) : JQuery {
let result; let result;
if(this.render) { const template = $.views.templates[this.attr("id")];
result = $(this.render(values)); if(!template) {
} else { console.error("Tried to render template %o, but template is not available!", this.attr("id"));
const template = jsrender.render[this.attr("id")]; throw "missing template " + 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 = 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) => { result.find("node").each((index, element) => {
$(element).replaceWith(values[$(element).attr("key")] || (values[0] || [])[$(element).attr("key")]); $(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"; import {ChannelTreeUIEvents} from "tc-shared/ui/tree/Definitions";
const cssStyle = require("./AppRenderer.scss"); const cssStyle = require("./AppRenderer.scss");
const VideoFrame = React.memo((props: { events: Registry<AppUiEvents> }) => { const VideoFrame = React.memo((props: { events: Registry<AppUiEvents> }) => {
const refElement = React.useRef<HTMLDivElement>(); const refElement = React.useRef<HTMLDivElement>();
const [ container, setContainer ] = useState<HTMLDivElement | undefined>(() => { const [ container, setContainer ] = useState<HTMLDivElement | undefined>(() => {

View File

@ -2,6 +2,26 @@ import * as loader from "tc-loader";
import moment from "moment"; import moment from "moment";
import {LogCategory, logError, logTrace} from "../log"; import {LogCategory, logError, logTrace} from "../log";
import {tr} from "tc-shared/i18n/localize"; 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 { export function setupJSRender() : boolean {
if(!$.views) { if(!$.views) {
@ -25,11 +45,8 @@ export function setupJSRender() : boolean {
return /* @tr-ignore */ tr(args[0]); return /* @tr-ignore */ tr(args[0]);
}); });
$(".jsrender-template").each((idx, _entry) => { initializeHtml(TemplateFile);
if(!$.templates(_entry.id, _entry.innerHTML)) { initializeHtml(TemplateMusicManage);
logError(LogCategory.GENERAL, tr("Failed to setup cache for js renderer template %s!"), _entry.id); initializeHtml(TemplateNewComer);
} else
logTrace(LogCategory.GENERAL, tr("Successfully loaded jsrender template %s"), _entry.id);
});
return true; return true;
} }

View File

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

View File

@ -25,6 +25,7 @@ async function initializeFaviconRenderer() {
iconImage = new Image(); iconImage = new Image();
iconImage.src = kClientSpriteUrl; iconImage.src = kClientSpriteUrl;
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
iconImage.onload = resolve; iconImage.onload = resolve;
iconImage.onerror = () => reject("failed to load client icon sprite"); 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 => { export = () => config_base.config("client").then(config => {
Object.assign(config.entry, { Object.assign(config.entry, {
"client-app": "./client/app/index.ts" "client-app": ["./client/app/index.ts"]
}); });
Object.assign(config.resolve.alias, { Object.assign(config.resolve.alias, {
@ -15,7 +15,7 @@ export = () => config_base.config("client").then(config => {
if(!Array.isArray(config.externals)) if(!Array.isArray(config.externals))
throw "invalid config"; throw "invalid config";
config.externals.push((context, request, callback) => { config.externals.push(({ context, request }, callback) => {
if (request.startsWith("tc-backend/")) { if (request.startsWith("tc-backend/")) {
return callback(null, `window["backend-loader"].require("${request}")`); 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 => { export = () => config_base.config("web").then(config => {
Object.assign(config.entry, { Object.assign(config.entry, {
"shared-app": "./web/app/index.ts", "shared-app": ["./web/app/index.ts"],
"modal-external": "./web/app/index-external.ts" "modal-external": ["./web/app/index-external.ts"]
}); });
Object.assign(config.resolve.alias, { Object.assign(config.resolve.alias, {
@ -13,7 +13,5 @@ export = () => config_base.config("web").then(config => {
"tc-backend": path.resolve(__dirname, "web/app"), "tc-backend": path.resolve(__dirname, "web/app"),
}); });
config.node = config.node || {};
config.node["fs"] = "empty";
return Promise.resolve(config); return Promise.resolve(config);
}); });

View File

@ -3,20 +3,23 @@ import * as fs from "fs";
import trtransformer from "./tools/trgen/ts_transformer"; import trtransformer from "./tools/trgen/ts_transformer";
import {exec} from "child_process"; import {exec} from "child_process";
import * as util from "util"; import * as util from "util";
import { Plugin as SvgSpriteGenerator } from "webpack-svg-sprite-generator";
import LoaderIndexGenerator from "./loader/IndexGenerator";
import {Configuration} from "webpack"; import {Configuration} from "webpack";
const path = require('path'); const path = require('path');
const webpack = require("webpack"); 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 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 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'; 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"); 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; 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: { entry: {
"loader": "./loader/app/index.ts", "loader": ["./loader/app/index.ts"],
"modal-external": "./shared/js/ui/react-elements/external-modal/PopoutEntrypoint.ts", "modal-external": ["./shared/js/ui/react-elements/external-modal/PopoutEntrypoint.ts"],
//"devel-main": "./shared/js/devel_main.ts" //"devel-main": "./shared/js/devel_main.ts"
}, },
devtool: isDevelopment ? "inline-source-map" : undefined, devtool: isDevelopment ? "inline-source-map" : undefined,
mode: isDevelopment ? "development" : "production", mode: isDevelopment ? "development" : "production",
plugins: [ plugins: [
//new CleanWebpackPlugin(), new CleanWebpackPlugin(),
new MiniCssExtractPlugin({ new MiniCssExtractPlugin({
filename: isDevelopment ? '[name].css' : '[name].[hash].css', filename: isDevelopment ? '[name].css' : '[name].[contenthash].css',
chunkFilename: isDevelopment ? '[id].css' : '[id].[hash].css' chunkFilename: isDevelopment ? '[id].css' : '[id].[contenthash].css',
ignoreOrder: true
}), }),
new ManifestGenerator({ new ManifestGenerator({
file: path.join(__dirname, "dist/manifest.json"), outputFileName: "manifest.json",
base: __dirname context: __dirname
}), }),
new WorkerPlugin(),
//new BundleAnalyzerPlugin(), //new BundleAnalyzerPlugin(),
isDevelopment ? undefined : new webpack.optimize.AggressiveSplittingPlugin({
minSize: 1024 * 8,
maxSize: 1024 * 128
}),
new webpack.DefinePlugin(await generateDefinitions(target)), new webpack.DefinePlugin(await generateDefinitions(target)),
new SvgSpriteGenerator({ new SvgSpriteGenerator({
dtsOutputFolder: path.join(__dirname, "shared", "svg-sprites"), dtsOutputFolder: path.join(__dirname, "shared", "svg-sprites"),
publicPath: "js/",
configurations: { configurations: {
"client-icons": { "client-icons": {
folder: path.join(__dirname, "shared", "img", "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({ generateIndexPlugin(target),
buildTarget: target, new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/.*/]),
output: path.join(__dirname, "dist/index.html"),
isDevelopment: isDevelopment
})
].filter(e => !!e), ].filter(e => !!e),
module: { module: {
rules: [ rules: [
{ {
test: /\.(s[ac]|c)ss$/, test: /\.(s[ac]|c)ss$/,
loader: [ use: [
'style-loader', //'style-loader',
//MiniCssExtractPlugin.loader, {
loader: MiniCssExtractPlugin.loader,
options: {
esModule: false
}
},
{ {
loader: 'css-loader', loader: 'css-loader',
options: { options: {
@ -161,7 +192,7 @@ export const config = async (target: "web" | "client"): Promise<Configuration> =
test: (module: string) => module.match(/\.tsx?$/) && !isLoaderFile(module), test: (module: string) => module.match(/\.tsx?$/) && !isLoaderFile(module),
exclude: /node_modules/, exclude: /node_modules/,
loader: [ use: [
{ {
loader: 'ts-loader', loader: 'ts-loader',
options: { options: {
@ -185,7 +216,7 @@ export const config = async (target: "web" | "client"): Promise<Configuration> =
test: (module: string) => module.match(/\.tsx?$/) && isLoaderFile(module), test: (module: string) => module.match(/\.tsx?$/) && isLoaderFile(module),
exclude: /(node_modules|bower_components)/, exclude: /(node_modules|bower_components)/,
loader: [ use: [
{ {
loader: "babel-loader", loader: "babel-loader",
options: { options: {
@ -202,24 +233,26 @@ export const config = async (target: "web" | "client"): Promise<Configuration> =
}, },
{ {
test: /\.was?t$/, test: /\.was?t$/,
loader: [ use: [
"./webpack/WatLoader.js" "./webpack/WatLoader.js"
] ]
}, },
{ {
test: /\.svg$/, test: /\.svg$/,
loader: 'svg-inline-loader' use: 'svg-inline-loader'
}, },
{ {
test: /ChangeLog\.md$/i, test: /ChangeLog\.md$|\.html$/i,
loader: "raw-loader", use: {
options: { loader: "raw-loader",
esModule: false options: {
} esModule: false
}
},
}, },
{ {
test: /\.(png|jpg|jpeg|gif)?$/, test: /\.(png|jpg|jpeg|gif)?$/,
loader: 'file-loader', use: 'file-loader',
}, },
] ]
} as any, } as any,
@ -245,9 +278,19 @@ export const config = async (target: "web" | "client"): Promise<Configuration> =
}, },
optimization: { optimization: {
splitChunks: { splitChunks: {
chunks: "all" chunks: "all",
maxSize: 512 * 1024
}, },
minimize: !isDevelopment, 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 webpack from "webpack";
import * as fs from "fs-extra";
import * as path from "path"; import * as path from "path";
interface Options { interface Options {
file?: string; outputFileName?: string;
base: string; context: string;
} }
class ManifestGenerator { class ManifestGenerator {
private manifest_content; private readonly options: Options;
readonly options: Options;
constructor(options: Options) { constructor(options: Options) {
this.options = options || { base: __dirname }; this.options = options || { context: __dirname };
} }
apply(compiler: webpack.Compiler) { apply(compiler: webpack.Compiler) {
compiler.hooks.afterCompile.tap(this.constructor.name, compilation => { compiler.hooks.emit.tap(this.constructor.name, compilation => {
const chunks_data = {}; const chunkData = {};
for(const chunkGroup of compilation.chunkGroups) { for(const chunkGroup of compilation.chunkGroups) {
const fileJs = []; const fileJs = [];
const filesCss = []; const filesCss = [];
const modules = []; const modules = [];
for(const chunk of chunkGroup.chunks) { for(const chunk of chunkGroup.chunks) {
if(!chunk.files.length) if(!chunk.files.size) {
continue; 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) { for(const file of chunk.files) {
const extension = path.extname(file); const extension = path.extname(file);
if(extension === ".js") { switch (extension) {
fileJs.push({ case ".js":
hash: chunk.hash, fileJs.push({
file: file hash: chunk.hash,
}); file: file
} else if(extension === ".css") { });
filesCss.push({ break;
hash: chunk.hash,
file: file case ".css":
}); filesCss.push({
} else if(extension === ".wasm") { hash: chunk.hash,
/* do nothing */ file: file
} else { });
throw "Unknown chunk file with extension " + extension; break;
case ".wasm":
break;
default:
throw "Unknown chunk file with extension " + extension;
} }
} }
for(const module of chunk.getModules()) { for(const module of chunk.getModules() as any[]) {
if(!module.type.startsWith("javascript/")) if(!module.type.startsWith("javascript/")) {
continue; continue;
}
if(!module.context) if(!module.context) {
continue; continue;
}
if(module.context.startsWith("svg-sprites/")) { if(module.context.startsWith("svg-sprites/")) {
/* custom svg sprite handler */ /* custom svg sprite handler */
@ -71,37 +70,39 @@ class ManifestGenerator {
continue; continue;
} }
if(!module.resource) if(!module.resource) {
continue; continue;
}
if(module.context !== path.dirname(module.resource)) if(module.context !== path.dirname(module.resource)) {
throw "invalid context/resource relation"; throw "invalid context/resource relation";
}
modules.push({ modules.push({
id: module.id, 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) resource: path.basename(module.resource)
}); });
} }
} }
chunks_data[chunkGroup.options.name] = { chunkData[chunkGroup.options.name] = {
files: fileJs, files: fileJs,
css_files: filesCss, css_files: filesCss,
modules: modules modules: modules
}; };
} }
this.manifest_content = { const payload = JSON.stringify({
version: 2, version: 2,
chunks: chunks_data chunks: chunkData
}; });
});
compiler.hooks.done.tap(this.constructor.name, () => { const fileName = this.options.outputFileName || "manifest.json";
const file = this.options.file || "manifest.json"; compilation.assets[fileName] = {
fs.mkdirpSync(path.dirname(file)); size() { return payload.length; },
fs.writeFileSync(this.options.file || "manifest.json", JSON.stringify(this.manifest_content)); 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 wabt = require("wabt")();
const filename = "module.wast"; 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(); this.cacheable();
const module = wabt.parseWat(filename, source); const module = wabt.parseWat(filename, source);