diff --git a/.gitignore b/.gitignore index dd6b5dcd..e8d198e4 100644 --- a/.gitignore +++ b/.gitignore @@ -25,9 +25,10 @@ node_modules/ /todo.txt /tmp/ -# All out config files are .ts files +# All our config files are .ts files /*.js /*.js.map +!babel.config.js /webpack/*.js /webpack/*.js.map diff --git a/ChangeLog.md b/ChangeLog.md index 52dd7b9f..5bb20d24 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -6,6 +6,10 @@ - Improved chat box behaviour - Automatically crawling all channels on server join for new messages (requires TeaSpeak 1.4.16-b2 or higher) +* **12.07.20** + - Made the loader compatible with ES5 to support older browsers + - Updated the loader animation + * **15.06.20** - Recoded the permission editor with react - Fixed sever permission editor display bugs diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 00000000..15e4dde8 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,27 @@ +module.exports = function (api) { + api.cache(false); + const presets = [ + [ + "@babel/preset-env", + { + "corejs": { "version":3 }, + "useBuiltIns": "usage", + "targets": { + "edge": "17", + "firefox": "60", + "chrome": "67", + "safari": "11.1", + "ie": "11" + } + } + ] + ]; + const plugins = [ + ["@babel/transform-runtime"], + ["@babel/plugin-transform-modules-commonjs"] + ]; + return { + presets, + plugins + }; +}; \ No newline at end of file diff --git a/file.ts b/file.ts index 3675dbdc..d0cf2e10 100644 --- a/file.ts +++ b/file.ts @@ -121,7 +121,7 @@ const APP_FILE_LIST_SHARED_SOURCE: ProjectResource[] = [ }, { /* shared image files */ "type": "img", - "search-pattern": /.*\.(svg|png)/, + "search-pattern": /.*\.(svg|png|gif)/, "build-target": "dev|rel", "path": "img/", diff --git a/loader/IndexGenerator.ts b/loader/IndexGenerator.ts new file mode 100644 index 00000000..6b049ca8 --- /dev/null +++ b/loader/IndexGenerator.ts @@ -0,0 +1,31 @@ +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; \ No newline at end of file diff --git a/loader/app/animation.ts b/loader/app/animation.ts new file mode 100644 index 00000000..d14f616e --- /dev/null +++ b/loader/app/animation.ts @@ -0,0 +1,128 @@ +import * as loader from "./loader/loader"; +import {Stage} from "./loader/loader"; + +let overlay: HTMLDivElement; +let setupContainer: HTMLDivElement; +let idleContainer: HTMLDivElement; +let idleSteamContainer: HTMLDivElement; +let loaderStageContainer: HTMLDivElement; + +let finalizing = false; +let initializeTimestamp; + +let verbose = false; +let apngSupport = undefined; + +async function detectAPNGSupport() { + const image = new Image(); + const ctx = document.createElement("canvas").getContext("2d"); + + // frame 1 (skipped on apng-supporting browsers): [0, 0, 0, 255] + // frame 2: [0, 0, 0, 0] + image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACGFjVEwAAAABAAAAAcMq2TYAAAANSURBVAiZY2BgYPgPAAEEAQB9ssjfAAAAGmZjVEwAAAAAAAAAAQAAAAEAAAAAAAAAAAD6A+gBAbNU+2sAAAARZmRBVAAAAAEImWNgYGBgAAAABQAB6MzFdgAAAABJRU5ErkJggg=="; + await new Promise(resolve => image.onload = resolve); + + ctx.drawImage(image, 0, 0); + apngSupport = ctx.getImageData(0, 0, 1, 1).data[3] === 0; + console.log("Browser APNG support: %o", apngSupport); +} + +function initializeElements() { + overlay = document.getElementById("loader-overlay") as HTMLDivElement; + if(!overlay) + throw "missing loader overlay"; + + for(const lazyImage of [...overlay.getElementsByTagName("lazy-img")]) { + const image = document.createElement("img"); + image.alt = lazyImage.getAttribute("alt"); + image.src = lazyImage.getAttribute(apngSupport ? "src-apng" : "src-gif") || lazyImage.getAttribute("src"); + image.className = lazyImage.className; + lazyImage.replaceWith(image); + } + + setupContainer = overlay.getElementsByClassName("setup")[0] as HTMLDivElement; + if(!setupContainer) + throw "missing setup container"; + + idleContainer = overlay.getElementsByClassName("idle")[0] as HTMLDivElement; + if(!idleContainer) + throw "missing idle container"; + + idleSteamContainer = idleContainer.getElementsByClassName("steam")[0] as HTMLDivElement; + if(!idleSteamContainer) + throw "missing idle steam container"; + + loaderStageContainer = overlay.getElementsByClassName("loader-stage")[0] as HTMLDivElement; + if(!loaderStageContainer) + throw "missing loader stage container"; + + setupContainer.onanimationend = setupAnimationFinished; + idleSteamContainer.onanimationiteration = idleSteamAnimationLooped; + overlay.onanimationend = overlayAnimationFinished; +} + +export async function initialize() { + await detectAPNGSupport(); + try { + initializeElements(); + } catch (error) { + console.error("Failed to setup animations: %o", error); + loader.critical_error("Animation setup failed", error); + return false; + } + + StageNames[Stage.SETUP] = "starting app"; + StageNames[Stage.TEMPLATES] = "loading templates"; + StageNames[Stage.STYLE] = "loading styles"; + StageNames[Stage.JAVASCRIPT] = "loading app"; + StageNames[Stage.JAVASCRIPT_INITIALIZING] = "initializing"; + StageNames[Stage.FINALIZING] = "rounding up"; + StageNames[Stage.LOADED] = "starting app"; + + overlay.classList.add("initialized"); + setupContainer.classList.add("visible"); + + initializeTimestamp = Date.now(); + return true; +} + +export function abort() { + overlay?.remove(); +} + +export function finalize() { + finalizing = true; + + if(loaderStageContainer) + loaderStageContainer.innerText = "app loaded successfully (" + (Date.now() - initializeTimestamp) + "ms)"; +} + +const StageNames = {}; +export function updateState(state: Stage, tasks: string[]) { + if(loaderStageContainer) + loaderStageContainer.innerText = StageNames[state] + (tasks.length === 1 ? " (task: " + tasks[0] + ")" : " (tasks: " + tasks.join(",") + ")"); +} + +function setupAnimationFinished() { + verbose && console.log("Entering idle animation"); + + setupContainer.classList.remove("visible"); + idleContainer.classList.add("visible"); +} + +function idleSteamAnimationLooped() { + verbose && console.log("Idle animation looped. Should finalize: %o", finalizing); + if(!finalizing) + return; + + overlay.classList.add("finishing"); +} + +function overlayAnimationFinished(event: AnimationEvent) { + /* the text animation is the last one */ + if(event.animationName !== "swipe-out-text") + return; + + verbose && console.log("Animation finished"); + overlay.remove(); +} \ No newline at end of file diff --git a/loader/app/index.ts b/loader/app/index.ts index 0f9091a9..8276a1f4 100644 --- a/loader/app/index.ts +++ b/loader/app/index.ts @@ -1,8 +1,15 @@ -import * as loader from "./targets/app"; -import * as loader_base from "./loader/loader"; -window["loader"] = loader_base; - /* let the loader register himself at the window first */ -setTimeout(loader.run, 0); +import "core-js/stable"; +import "./polifill"; + +import * as loader from "./loader/loader"; +window["loader"] = loader; +/* let the loader register himself at the window first */ + +import * as AppLoader from "./targets/app"; +setTimeout(AppLoader.run, 0); + +import * as EmptyLoader from "./targets/empty"; +//setTimeout(EmptyLoader.run, 0); export {}; diff --git a/loader/app/loader/loader.ts b/loader/app/loader/loader.ts index cf725c82..865064fa 100644 --- a/loader/app/loader/loader.ts +++ b/loader/app/loader/loader.ts @@ -1,6 +1,7 @@ import * as script_loader from "./script_loader"; import * as style_loader from "./style_loader"; import * as template_loader from "./template_loader"; +import * as Animation from "../animation"; declare global { interface Window { @@ -31,7 +32,7 @@ export let config: Config = { export type Task = { name: string, priority: number, /* tasks with the same priority will be executed in sync */ - function: () => Promise + function: (taskId?: number) => Promise }; export enum Stage { @@ -39,30 +40,37 @@ export enum Stage { loading loader required files (incl this) */ INITIALIZING, + /* setting up the loading process */ SETUP, + /* loading all style sheet files */ STYLE, + /* loading all javascript files */ JAVASCRIPT, + /* loading all template files */ TEMPLATES, + /* initializing static/global stuff */ JAVASCRIPT_INITIALIZING, + /* finalizing load process */ FINALIZING, + /* invoking main task */ @@ -72,7 +80,7 @@ export enum Stage { } let cache_tag: string | undefined; -let current_stage: Stage = undefined; +let currentStage: Stage = undefined; const tasks: {[key:number]:Task[]} = {}; /* test if all files shall be load from cache or fetch again */ @@ -109,12 +117,12 @@ export function module_mapping() : ModuleMapping[] { return module_mapping_; } export function get_cache_version() { return cache_tag; } export function finished() { - return current_stage == Stage.DONE; + return currentStage == Stage.DONE; } -export function running() { return typeof(current_stage) !== "undefined"; } +export function running() { return typeof(currentStage) !== "undefined"; } export function register_task(stage: Stage, task: Task) { - if(current_stage > stage) { + if(currentStage > stage) { if(config.error) console.warn("Register loading task, but it had already been finished. Executing task anyways!"); @@ -139,20 +147,41 @@ export function register_task(stage: Stage, task: Task) { tasks[stage] = task_array.sort((a, b) => a.priority - b.priority); } +type RunningTask = { + taskId: number, + name: string, + promise: Promise | undefined +}; +let runningTasks: RunningTask[] = []; +let runningTaskIdIndex = 1; + +export function setCurrentTaskName(taskId: number, name: string) { + const task = runningTasks.find(e => e.taskId === taskId); + if(!task) { + console.warn("Tried to set task name of unknown task %d", taskId); + return; + } + + task.name = name; + Animation.updateState(currentStage, runningTasks.map(e => e.name)); +} + export async function execute() { - document.getElementById("loader-overlay").classList.add("started"); + if(!await Animation.initialize()) + return; + loader_cache_tag(); const load_begin = Date.now(); let begin: number = 0; let end: number = Date.now(); - while(current_stage <= Stage.LOADED || typeof(current_stage) === "undefined") { + while(currentStage <= Stage.LOADED || typeof(currentStage) === "undefined") { - let current_tasks: Task[] = []; - while((tasks[current_stage] || []).length > 0) { - if(current_tasks.length == 0 || current_tasks[0].priority == tasks[current_stage][0].priority) { - current_tasks.push(tasks[current_stage].pop()); + let pendingTasks: Task[] = []; + while((tasks[currentStage] || []).length > 0) { + if(pendingTasks.length == 0 || pendingTasks[0].priority == tasks[currentStage][0].priority) { + pendingTasks.push(tasks[currentStage].pop()); } else break; } @@ -161,23 +190,45 @@ export async function execute() { task: Task }[] = []; - const promises: Promise[] = []; - for(const task of current_tasks) { + for(const task of pendingTasks) { + const rTask = { + taskId: ++runningTaskIdIndex, + name: task.name, + promise: undefined + } as RunningTask; + try { - if(config.verbose) console.debug("Executing loader %s (%d)", task.name, task.priority); - const promise = task.function(); + if(config.verbose) + console.debug("Executing loader %s (%d)", task.name, task.priority); + + runningTasks.push(rTask); + const promise = task.function(rTask.taskId); if(!promise) { + runningTasks.splice(runningTasks.indexOf(rTask), 1); console.error("Loading task %s hasn't returned a promise!", task.name); continue; } - promises.push(promise.catch(error => { + + rTask.promise = promise.catch(error => { errors.push({ task: task, error: error }); + return Promise.resolve(); - })); + }).then(() => { + const index = runningTasks.indexOf(rTask); + if(index === -1) { + console.warn("Running task (%s) finished, but it has been unregistered already!", task.name); + return; + } + runningTasks.splice(index, 1); + }); } catch(error) { + const index = runningTasks.indexOf(rTask); + if(index !== -1) + runningTasks.splice(index, 1); + errors.push({ task: task, error: error @@ -185,41 +236,47 @@ export async function execute() { } } - if(promises.length > 0) { - await Promise.all([...promises]); + if(runningTasks.length > 0) { + Animation.updateState(currentStage, runningTasks.map(e => e.name)); + await Promise.all(runningTasks.map(e => e.promise)); } if(errors.length > 0) { - if(config.loader_groups) console.groupEnd(); + if(config.loader_groups) + console.groupEnd(); console.error("Failed to execute loader. The following tasks failed (%d):", errors.length); for(const error of errors) console.error(" - %s: %o", error.task.name, error.error); - throw "failed to process step " + Stage[current_stage]; + throw "failed to process step " + Stage[currentStage]; } - if(current_tasks.length == 0) { - if(typeof(current_stage) === "undefined") { - current_stage = -1; + if(pendingTasks.length == 0) { + if(typeof(currentStage) === "undefined") { + currentStage = -1; if(config.verbose) console.debug("[loader] Booting app"); - } else if(current_stage < Stage.INITIALIZING) { + } else if(currentStage < Stage.INITIALIZING) { if(config.loader_groups) console.groupEnd(); - if(config.verbose) console.debug("[loader] Entering next state (%s). Last state took %dms", Stage[current_stage + 1], (end = Date.now()) - begin); + if(config.verbose) console.debug("[loader] Entering next state (%s). Last state took %dms", Stage[currentStage + 1], (end = Date.now()) - begin); } else { if(config.loader_groups) console.groupEnd(); if(config.verbose) console.debug("[loader] Finish invoke took %dms", (end = Date.now()) - begin); } begin = end; - current_stage += 1; + currentStage += 1; - if(current_stage != Stage.DONE && config.loader_groups) - console.groupCollapsed("Executing loading stage %s", Stage[current_stage]); + if(currentStage != Stage.DONE && config.loader_groups) + console.groupCollapsed("Executing loading stage %s", Stage[currentStage]); } } - if(config.verbose) console.debug("[loader] finished loader. (Total time: %dms)", Date.now() - load_begin); + if(config.verbose) + console.debug("[loader] finished loader. (Total time: %dms)", Date.now() - load_begin); + + Animation.finalize(); } + export function execute_managed() { execute().then(() => { if(config.verbose) { @@ -242,28 +299,12 @@ export function execute_managed() { }); } -let _fadeout_warned; -export function hide_overlay() { - if(typeof($) === "undefined") { - if(!_fadeout_warned) - console.warn("Could not fadeout loader screen. Missing jquery functions."); - _fadeout_warned = true; - return; - } - const animation_duration = 750; - - $(".loader .bookshelf_wrapper").animate({top: 0, opacity: 0}, animation_duration); - $(".loader .half").animate({width: 0}, animation_duration, () => { - $(".loader").detach(); - }); -} - /* critical error handler */ export type ErrorHandler = (message: string, detail: string) => void; let _callback_critical_error: ErrorHandler; let _callback_critical_called: boolean = false; export function critical_error(message: string, detail?: string) { - document.getElementById("loader-overlay").classList.add("started"); + Animation.abort(); if(_callback_critical_called) { console.warn("[CRITICAL] %s", message); @@ -312,7 +353,6 @@ export const templates = template_loader; /* Hello World message */ { - const clog = console.log; const print_security = () => { { diff --git a/loader/app/loader/script_loader.ts b/loader/app/loader/script_loader.ts index 80a3622f..8a7a10a9 100644 --- a/loader/app/loader/script_loader.ts +++ b/loader/app/loader/script_loader.ts @@ -1,5 +1,5 @@ import {config, critical_error, SourcePath} from "./loader"; -import {load_parallel, LoadSyntaxError, ParallelOptions, script_name} from "./utils"; +import {load_parallel, LoadCallback, LoadSyntaxError, ParallelOptions, script_name} from "./utils"; let _script_promises: {[key: string]: Promise} = {}; @@ -88,13 +88,14 @@ export async function load(path: SourcePath, options: Options) : Promise { throw "Missing dependency " + depend; await _script_promises[depend]; } + await load_script_url(source.url + (options.cache_tag || "")); } } type MultipleOptions = Options | ParallelOptions; -export async function load_multiple(paths: SourcePath[], options: MultipleOptions) : Promise { - const result = await load_parallel(paths, e => load(e, options), e => script_name(e, false), options); +export async function load_multiple(paths: SourcePath[], options: MultipleOptions, callback?: LoadCallback) : Promise { + const result = await load_parallel(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 scripts:"); diff --git a/loader/app/loader/style_loader.ts b/loader/app/loader/style_loader.ts index f23ec3e2..b3ea670e 100644 --- a/loader/app/loader/style_loader.ts +++ b/loader/app/loader/style_loader.ts @@ -1,5 +1,5 @@ import {config, critical_error, SourcePath} from "./loader"; -import {load_parallel, LoadSyntaxError, ParallelOptions, script_name} from "./utils"; +import {load_parallel, LoadCallback, LoadSyntaxError, ParallelOptions, script_name} from "./utils"; let _style_promises: {[key: string]: Promise} = {}; @@ -121,8 +121,8 @@ export async function load(path: SourcePath, options: Options) : Promise { } export type MultipleOptions = Options | ParallelOptions; -export async function load_multiple(paths: SourcePath[], options: MultipleOptions) : Promise { - const result = await load_parallel(paths, e => load(e, options), e => script_name(e, false), options); +export async function load_multiple(paths: SourcePath[], options: MultipleOptions, callback?: LoadCallback) : Promise { + const result = await load_parallel(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:"); diff --git a/loader/app/loader/template_loader.ts b/loader/app/loader/template_loader.ts index cfe51660..6bf25e93 100644 --- a/loader/app/loader/template_loader.ts +++ b/loader/app/loader/template_loader.ts @@ -1,5 +1,5 @@ import {config, critical_error, SourcePath} from "./loader"; -import {load_parallel, LoadSyntaxError, ParallelOptions, script_name} from "./utils"; +import {load_parallel, LoadCallback, LoadSyntaxError, ParallelOptions, script_name} from "./utils"; let _template_promises: {[key: string]: Promise} = {}; @@ -69,8 +69,8 @@ export async function load(path: SourcePath, options: Options) : Promise { } export type MultipleOptions = Options | ParallelOptions; -export async function load_multiple(paths: SourcePath[], options: MultipleOptions) : Promise { - const result = await load_parallel(paths, e => load(e, options), e => script_name(e, false), options); +export async function load_multiple(paths: SourcePath[], options: MultipleOptions, callback?: LoadCallback) : Promise { + const result = await load_parallel(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:"); diff --git a/loader/app/loader/utils.ts b/loader/app/loader/utils.ts index fdab50f2..8dfc717d 100644 --- a/loader/app/loader/utils.ts +++ b/loader/app/loader/utils.ts @@ -10,11 +10,7 @@ export class LoadSyntaxError { export function script_name(path: SourcePath, html: boolean) { if(Array.isArray(path)) { - let buffer = ""; - let _or = " or "; - for(let entry of path) - buffer += _or + script_name(entry, html); - return buffer.slice(_or.length); + return path.filter(e => !!e).map(e => script_name(e, html)).join(" or "); } else if(typeof(path) === "string") return html ? "" + path + "" : path; else @@ -35,31 +31,40 @@ export interface ParallelResult { skipped: T[]; } -export async function load_parallel(requests: T[], executor: (_: T) => Promise, stringify: (_: T) => string, options: ParallelOptions) : Promise> { +export type LoadCallback = (entry: T, state: "loading" | "loaded") => void; + +export async function load_parallel(requests: T[], executor: (_: T) => Promise, stringify: (_: T) => string, options: ParallelOptions, callback?: LoadCallback) : Promise> { const result: ParallelResult = { failed: [], succeeded: [], skipped: [] }; - const pending_requests = requests.slice(0).reverse(); /* we're only able to pop from the back */ - const current_requests = {}; + const pendingRequests = requests.slice(0).reverse(); /* we're only able to pop from the back */ + const currentRequests = {}; - while (pending_requests.length > 0) { - while(typeof options.max_parallel_requests !== "number" || options.max_parallel_requests <= 0 || Object.keys(current_requests).length < options.max_parallel_requests) { - const script = pending_requests.pop(); - const name = stringify(script); + if(typeof callback === "undefined") + callback = () => {}; - current_requests[name] = executor(script).catch(e => result.failed.push({ request: script, error: e })).then(() => { - delete current_requests[name]; + options.max_parallel_requests = 1; + while (pendingRequests.length > 0) { + while(typeof options.max_parallel_requests !== "number" || options.max_parallel_requests <= 0 || Object.keys(currentRequests).length < options.max_parallel_requests) { + const element = pendingRequests.pop(); + const name = stringify(element); + + callback(element, "loading"); + currentRequests[name] = executor(element).catch(e => result.failed.push({ request: element, error: e })).then(() => { + delete currentRequests[name]; + callback(element, "loaded"); }); - if(pending_requests.length == 0) break; + if(pendingRequests.length == 0) + break; } /* * Wait 'till a new "slot" for downloading is free. * This should also not throw because any errors will be caught before. */ - await Promise.race(Object.keys(current_requests).map(e => current_requests[e])); + await Promise.race(Object.keys(currentRequests).map(e => currentRequests[e])); if(result.failed.length > 0) break; /* finish loading the other requests and than show the error */ } - await Promise.all(Object.keys(current_requests).map(e => current_requests[e])); - result.skipped.push(...pending_requests); + await Promise.all(Object.keys(currentRequests).map(e => currentRequests[e])); + result.skipped.push(...pendingRequests); return result; } \ No newline at end of file diff --git a/loader/app/polifill.ts b/loader/app/polifill.ts new file mode 100644 index 00000000..3acd59a0 --- /dev/null +++ b/loader/app/polifill.ts @@ -0,0 +1,57 @@ +/* IE11 */ +if(!Element.prototype.remove) + Element.prototype.remove = function() { + this.parentElement?.removeChild(this); + }; + +function ReplaceWithPolyfill() { + let parent = this.parentNode, i = arguments.length, currentNode; + if (!parent) return; + if (!i) { parent.removeChild(this); } + while (i--) { // i-- decrements i and returns the value of i before the decrement + currentNode = arguments[i]; + if (typeof currentNode !== 'object'){ + currentNode = this.ownerDocument.createTextNode(currentNode); + } else if (currentNode.parentNode){ + currentNode.parentNode.removeChild(currentNode); + } + // the value of "i" below is after the decrement + if (!i) // if currentNode is the first argument (currentNode === arguments[0]) + parent.replaceChild(currentNode, this); + else // if currentNode isn't the first + parent.insertBefore(currentNode, this.nextSibling); + } +} +if (!Element.prototype.replaceWith) + Element.prototype.replaceWith = ReplaceWithPolyfill; + +if (!CharacterData.prototype.replaceWith) + CharacterData.prototype.replaceWith = ReplaceWithPolyfill; + +if (!DocumentType.prototype.replaceWith) + DocumentType.prototype.replaceWith = ReplaceWithPolyfill; + +// Source: https://github.com/jserz/js_piece/blob/master/DOM/ParentNode/append()/append().md +(function (arr) { + arr.forEach(function (item) { + if (item.hasOwnProperty('append')) { + return; + } + Object.defineProperty(item, 'append', { + configurable: true, + enumerable: true, + writable: true, + value: function append() { + var argArr = Array.prototype.slice.call(arguments), + docFrag = document.createDocumentFragment(); + + argArr.forEach(function (argItem) { + var isNode = argItem instanceof Node; + docFrag.appendChild(isNode ? argItem : document.createTextNode(String(argItem))); + }); + + this.appendChild(docFrag); + } + }); + }); +})([Element.prototype, Document.prototype, DocumentFragment.prototype]); \ No newline at end of file diff --git a/loader/app/targets/app.ts b/loader/app/targets/app.ts index d50cbee8..118c119b 100644 --- a/loader/app/targets/app.ts +++ b/loader/app/targets/app.ts @@ -1,5 +1,8 @@ +import "./shared"; import * as loader from "../loader/loader"; -import {config} from "../loader/loader"; +import {config, SourcePath} from "../loader/loader"; +import {script_name} from "../loader/utils"; +import { detect as detectBrowser } from "detect-browser"; declare global { interface Window { @@ -44,21 +47,18 @@ interface Manifest { }}; } +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 () => { + load_scripts: async taskId => { if(!window.require) { - await loader.scripts.load(["vendor/jquery/jquery.min.js"], { cache_tag: cache_tag() }); - } else { - /* - loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { - name: "forum sync", - priority: 10, - function: async () => { - forum.sync_main(); - } - }); - */ + await loader.scripts.load_multiple(["vendor/jquery/jquery.min.js"], { cache_tag: cache_tag() }, LoaderTaskCallback(taskId)); } await loader.scripts.load_multiple([ @@ -71,8 +71,9 @@ const loader_javascript = { ], { cache_tag: cache_tag(), max_parallel_requests: -1 - }); + }, LoaderTaskCallback(taskId)); + loader.setCurrentTaskName(taskId, "manifest"); let manifest: Manifest; try { const response = await fetch(config.baseUrl + "js/manifest.json"); @@ -99,7 +100,7 @@ const loader_javascript = { await loader.scripts.load_multiple(manifest.chunks[chunk_name].files.map(e => "js/" + e.file), { cache_tag: undefined, max_parallel_requests: -1 - }); + }, LoaderTaskCallback(taskId)); } }; @@ -125,7 +126,7 @@ const loader_webassembly = { }; const loader_style = { - load_style: async () => { + load_style: async taskId => { const options = { cache_tag: cache_tag(), max_parallel_requests: -1 @@ -133,22 +134,24 @@ const loader_style = { await loader.style.load_multiple([ "vendor/xbbcode/src/xbbcode.css" - ], options); + ], options, LoaderTaskCallback(taskId)); + await loader.style.load_multiple([ "vendor/emoji-picker/src/jquery.lsxemojipicker.css" - ], options); + ], options, LoaderTaskCallback(taskId)); + await loader.style.load_multiple([ ["vendor/highlight/styles/darcula.css", ""], /* empty string means not required */ - ], options); + ], options, LoaderTaskCallback(taskId)); if(__build.mode === "debug") { - await loader_style.load_style_debug(); + await loader_style.load_style_debug(taskId); } else { - await loader_style.load_style_release(); + await loader_style.load_style_release(taskId); } }, - load_style_debug: async () => { + load_style_debug: async taskId => { await loader.style.load_multiple([ "css/static/main.css", "css/static/main-layout.css", @@ -196,17 +199,17 @@ const loader_style = { ], { cache_tag: cache_tag(), max_parallel_requests: -1 - }); + }, LoaderTaskCallback(taskId)); }, - load_style_release: async () => { + load_style_release: async taskId => { await loader.style.load_multiple([ "css/static/base.css", "css/static/main.css", ], { cache_tag: cache_tag(), max_parallel_requests: -1 - }); + }, LoaderTaskCallback(taskId)); } }; @@ -228,36 +231,12 @@ loader.register_task(loader.Stage.INITIALIZING, { priority: 50 }); -loader.register_task(loader.Stage.INITIALIZING, { - name: "Browser detection", - function: async () => { - navigator.browserSpecs = (function(){ - let ua = navigator.userAgent, tem, M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || []; - if(/trident/i.test(M[1])){ - tem = /\brv[ :]+(\d+)/g.exec(ua) || []; - return {name:'IE',version:(tem[1] || '')}; - } - if(M[1]=== 'Chrome'){ - tem = ua.match(/\b(OPR|Edge)\/(\d+)/); - if(tem != null) return {name:tem[1].replace('OPR', 'Opera'),version:tem[2]}; - } - M = M[2]? [M[1], M[2]]: [navigator.appName, navigator.appVersion, '-?']; - if((tem = ua.match(/version\/(\d+)/i))!= null) - M.splice(1, 1, tem[1]); - return {name:M[0], version:M[1]}; - })(); - - console.log("Resolved browser manufacturer to \"%s\" version \"%s\"", navigator.browserSpecs.name, navigator.browserSpecs.version); - }, - priority: 30 -}); - 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") - (window)["isSecureContext"] = location.protocol !== 'https:' && location.hostname !== 'localhost'; + (window)["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!"); @@ -274,7 +253,7 @@ loader.register_task(loader.Stage.INITIALIZING, { }); loader.register_task(loader.Stage.JAVASCRIPT, { - name: "javascript", + name: "scripts", function: loader_javascript.load_scripts, priority: 10 }); @@ -287,7 +266,7 @@ loader.register_task(loader.Stage.STYLE, { loader.register_task(loader.Stage.TEMPLATES, { name: "templates", - function: async () => { + function: async taskId => { await loader.templates.load_multiple([ "templates.html", "templates/modal/musicmanage.html", @@ -295,17 +274,11 @@ loader.register_task(loader.Stage.TEMPLATES, { ], { cache_tag: cache_tag(), max_parallel_requests: -1 - }); + }, LoaderTaskCallback(taskId)); }, priority: 10 }); -loader.register_task(loader.Stage.LOADED, { - name: "loaded handler", - function: async () => loader.hide_overlay(), - priority: 5 -}); - loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { name: "lsx emoji picker setup", function: async () => await (window as any).setup_lsx_emoji_picker({twemoji: typeof(window.twemoji) !== "undefined"}), @@ -368,8 +341,6 @@ loader.register_task(loader.Stage.SETUP, { }); export function run() { - window["Module"] = (window["Module"] || {}) as any; /* Why? */ - /* TeaClient */ if(node_require) { if(__build.target !== "client") { diff --git a/loader/app/targets/certaccept.ts b/loader/app/targets/certaccept.ts index 794240c6..21a7e20d 100644 --- a/loader/app/targets/certaccept.ts +++ b/loader/app/targets/certaccept.ts @@ -71,12 +71,6 @@ loader.register_task(loader.Stage.STYLE, { priority: 10 }); -loader.register_task(loader.Stage.LOADED, { - name: "loaded handler", - function: async () => loader.hide_overlay(), - priority: 0 -}); - /* register tasks */ loader.register_task(loader.Stage.INITIALIZING, { name: "safari fix", diff --git a/loader/app/targets/empty.ts b/loader/app/targets/empty.ts new file mode 100644 index 00000000..45172a62 --- /dev/null +++ b/loader/app/targets/empty.ts @@ -0,0 +1,27 @@ +import "./shared"; +import * as loader from "../loader/loader"; +import {Stage} from "../loader/loader"; + +export function run() { + loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { + name: "doing nothing", + priority: 1, + function: async taskId => { + console.log("Doing nothing"); + + for(let index of [1, 2, 3]) { + await new Promise(resolve => { + const callback = () => { + document.removeEventListener("click", resolve); + resolve(); + }; + + document.addEventListener("click", callback); + }); + loader.setCurrentTaskName(taskId, "try again (" + index + ")"); + } + } + }); + + loader.execute_managed(); +} \ No newline at end of file diff --git a/loader/app/targets/shared.ts b/loader/app/targets/shared.ts new file mode 100644 index 00000000..cb5f1ddb --- /dev/null +++ b/loader/app/targets/shared.ts @@ -0,0 +1,38 @@ +import * as loader from "../loader/loader"; +import {Stage} from "../loader/loader"; +import {detect as detectBrowser} from "detect-browser"; + +if(__build.target === "web") { + loader.register_task(Stage.SETUP, { + name: "outdated browser checker", + function: async () => { + const browser = detectBrowser(); + navigator.browserSpecs = browser; + + if(!browser) + return; + + console.log("Resolved browser manufacturer to \"%s\" version \"%s\" on %s", browser.name, browser.version, browser.os); + if(browser.type && browser.type !== "browser") { + loader.critical_error("Your device isn't supported.", "User agent type " + browser.type + " isn't supported."); + throw "unsupported user type"; + } + + switch (browser?.name) { + case "aol": + case "bot": + case "crios": + case "ie": + loader.critical_error("Browser not supported", "We're sorry, but your browser isn't supported."); + throw "unsupported browser"; + + } + }, + priority: 50 + }); +} + +/* directly disable all context menus */ //disableGlobalContextMenu +if(!location.search.match(/(.*[?&]|^)disableGlobalContextMenu=1($|&.*)/)) { + document.addEventListener("contextmenu", event => event.preventDefault()); +} \ No newline at end of file diff --git a/loader/css/index.scss b/loader/css/index.scss new file mode 100644 index 00000000..20a076b6 --- /dev/null +++ b/loader/css/index.scss @@ -0,0 +1,7 @@ +body { + padding: 0; + margin: 0; +} + +@import "loader"; +@import "overlay"; diff --git a/loader/css/loader.scss b/loader/css/loader.scss index f9ec5b67..76d5780a 100644 --- a/loader/css/loader.scss +++ b/loader/css/loader.scss @@ -1,331 +1,186 @@ -$thickness: 5px; -$duration: 2500; -$delay: $duration/6; -$background: #222222; +$setup-time: 80s / 24; /* 24 frames / sec; the initial sequence is 80 seconds */ -@mixin polka($size, $dot, $base, $accent) { - background: $base; - background-image: radial-gradient($accent $dot, transparent 0); - background-size: $size $size; - background-position: 0 -2.5px; -} - -.loader { - margin: 0; - - display: block; - position: fixed; - top: 0; - bottom: 0; - left: 0; - right: 0; - - z-index: 900; - text-align: center; -} - -.loader .half { - position: fixed; - background: #222222; - top: 0; - bottom: 0; - width: 50%; - height: 100%; -} - -.loader .half.right { - right: 0; -} - -.loader .half.left { - left: 0; -} - -.bookshelf_wrapper { - position: relative; - top: 40%; - left: 50%; - transform: translate(-50%, -50%); -} - -.books_list { - margin: 0 auto; - width: 300px; - padding: 0; -} - -.book_item { +#loader-overlay { position: absolute; - top: -120px; - box-sizing: border-box; - list-style: none; - width: 40px; - height: 120px; - opacity: 0; - background-color: #1e6cc7; - border: $thickness solid white; - transform-origin: bottom left; - transform: translateX(300px); - animation: travel #{$duration}ms linear infinite; + overflow: hidden; - &.first { - top: -140px; - height: 140px; + top: 0; + left: 0; + right: 0; + bottom: 0; - &:before, - &:after { - content: ''; - position: absolute; - top: 10px; - left: 0; - width: 100%; - height: $thickness; - background-color: white; - } + background: #1e1e1e; - &:after { - top: initial; - bottom: 10px; - } + user-select: none; + + z-index: 10000; + + display: flex; + flex-direction: column; + justify-content: center; + + .container { + flex-shrink: 0; + + display: block; + position: relative; + + width: 1000px; + height: 1000px; + + align-self: center; + margin-bottom: 10vh; + + transition-duration: .5s; } - &.second, - &.fifth { - &:before, - &:after { - box-sizing: border-box; - content: ''; - position: absolute; - top: 10px; - left: 0; - width: 100%; - height: $thickness*3.5; - border-top: $thickness solid white; - border-bottom: $thickness solid white; - } - - &:after { - top: initial; - bottom: 10px; - } - } - - &.third { - &:before, - &:after { - box-sizing: border-box; - content: ''; - position: absolute; - top: 10px; - left: 9px; - width: 12px; - height: 12px; - border-radius: 50%; - border: $thickness solid white; - } - - &:after { - top: initial; - bottom: 10px; - } - } - - &.fourth { - top: -130px; - height: 130px; - - &:before { - box-sizing: border-box; - content: ''; - position: absolute; - top: 46px; - left: 0; - width: 100%; - height: $thickness*3.5; - border-top: $thickness solid white; - border-bottom: $thickness solid white; - } - } - - &.fifth { - top: -100px; - height: 100px; - } - - &.sixth { - top: -140px; - height: 140px; - - &:before { - box-sizing: border-box; - content: ''; - position: absolute; - bottom: 31px; - left: 0px; - width: 100%; - height: $thickness; - background-color: white; - } - - &:after { - box-sizing: border-box; - content: ''; - position: absolute; - bottom: 10px; - left: 9px; - width: 12px; - height: 12px; - border-radius: 50%; - border: $thickness solid white; - } - } - - &:nth-child(2) { - animation-delay: #{$delay*1}ms; - } - - &:nth-child(3) { - animation-delay: #{$delay*2}ms; - } - - &:nth-child(4) { - animation-delay: #{$delay*3}ms; - } - - &:nth-child(5) { - animation-delay: #{$delay*4}ms; - } - - &:nth-child(6) { - animation-delay: #{$delay*5}ms; - } - -} - -.shelf { - width: 300px; - height: $thickness; - margin: 0 auto; - background-color: white; - position: relative; - - &:before, - &:after { - content: ''; + .setup, .idle { position: absolute; - width: 100%; - height: 100%; - @include polka(10px, 30%, $background, rgba(255, 255, 255, 0.5)); - top: 200%; - left: 5%; - animation: move #{$duration/10}ms linear infinite; + + top: 0; + left: 0; + right: 0; + bottom: 0; + + display: none; + + &.visible { + display: block; + } } - &:after { - top: 400%; - left: 7.5%; + .setup.visible { + animation: loader-initial-sequence 0s cubic-bezier(.81,.01,.65,1.16) $setup-time forwards; } + .idle { + img { + position: absolute; + } + + .steam { + position: absolute; + + top: 282px; + left: 380px; + + width: 249px; + height: 125px; + background: url("../img/loader/steam.png") 0 0; + + animation: sprite-steam 2.5s steps(50) forwards infinite; + } + } + + &.finishing { + .idle { + .steam { + display: none; + } + + .bowl { + animation: swipe-out-bowl .5s both; + } + + .text { + animation: swipe-out-text .5s .12s both; + } + } + + pointer-events: none; + animation: overlay-fade .3s .2s both; + } + + .loader-stage { + position: absolute; + + left: 5px; + bottom: 5px; + + font-size: 12px; + font-family: monospace; + + color: #999; + } } -@keyframes move { +@media all and (max-width: 850px) { + #loader-overlay .container { + transform: scale(.5); + } +} +@media all and (max-height: 700px) { + #loader-overlay .container { + transform: scale(.5); + } +} + +@media all and (max-width: 400px) { + #loader-overlay .container { + transform: scale(.3); + } +} + +@keyframes loader-initial-sequence { + to { + display: none; + } +} + +@keyframes sprite-steam { + to { + background-position: 0 -6250px; + } +} + +@keyframes swipe-out-bowl { from { - background-position-x: 0; + transform: translate3d(0, 0, 0); + } + + 40% { + opacity: 1; + transform: translate3d(-60px, 0, 0) skew(-5deg, 0) rotateY(-6deg); } to { - background-position-x: 10px; + opacity: 0; + transform: translate3d(700px, 0, 0) skew(30deg, 0) rotateZ(-6deg); } - } -@keyframes travel { - - 0% { - opacity: 0; - transform: translateX(300px) rotateZ(0deg) scaleY(1); +@keyframes swipe-out-text { + from { + transform: translate3d(0, 0, 0); } - 6.5% { - transform: translateX(279.5px) rotateZ(0deg) scaleY(1.1); - } - - 8.8% { - transform: translateX(273.6px) rotateZ(0deg) scaleY(1); - } - - 10% { + 40% { opacity: 1; - transform: translateX(270px) rotateZ(0deg); + transform: translate3d(-30px, 20px, 0) skew(-5deg, 0); } - 17.6% { - transform: translateX(247.2px) rotateZ(-30deg); + to { + opacity: 0; + transform: translate3d(550px, 0, 0) skew(30deg, 0) scale(.96, 1.25) rotateZ(6deg); } +} - 45% { - transform: translateX(165px) rotateZ(-30deg); - } - - 49.5% { - transform: translateX(151.5px) rotateZ(-45deg); - } - - 61.5% { - transform: translateX(115.5px) rotateZ(-45deg); - } - - 67% { - transform: translateX(99px) rotateZ(-60deg); - } - - 76% { - transform: translateX(72px) rotateZ(-60deg); - } - - 83.5% { - opacity: 1; - transform: translateX(49.5px) rotateZ(-90deg); - } - - 90% { +@keyframes overlay-fade { + to { opacity: 0; } - - 100% { - opacity: 0; - transform: translateX(0px) rotateZ(-90deg); - } - } /* Automated loader timeout */ -$loader_timeout: 2.5s; -.loader:not(.started) { - &, & > .half, & > .bookshelf_wrapper { - -moz-animation: _loader_hide 0s ease-in $loader_timeout forwards; - -webkit-animation: _loader_hide 0s ease-in $loader_timeout forwards; - -o-animation: _loader_hide 0s ease-in $loader_timeout forwards; - animation: _loader_hide 0s ease-in $loader_timeout forwards; - -webkit-animation-fill-mode: forwards; - animation-fill-mode: forwards; - } -} - -.loader:not(.started) + #critical-load { +#loader-overlay:not(.initialized) + #critical-load { display: block !important; opacity: 0; - -moz-animation: _loader_show 0s ease-in $loader_timeout forwards; - -webkit-animation: _loader_show 0s ease-in $loader_timeout forwards; - -o-animation: _loader_show 0s ease-in $loader_timeout forwards; - animation: _loader_show 0s ease-in $loader_timeout forwards; - -webkit-animation-fill-mode: forwards; - animation-fill-mode: forwards; + animation: loader-setup-timeout 0s ease-in $setup-time forwards; .error::before { - content: 'Failed to startup app loader!'; + content: 'Failed to startup loader!'; } .detail::before { @@ -333,29 +188,8 @@ $loader_timeout: 2.5s; } } -@keyframes _loader_hide { - to { - width: 0; - height: 0; - overflow: hidden; - } -} -@-webkit-keyframes _loader_hide { - to { - width: 0; - height: 0; - visibility: hidden; - } -} - -@keyframes _loader_show { - to { - opacity: 1; - } -} - -@-webkit-keyframes _loader_show { +@keyframes loader-setup-timeout { to { opacity: 1; } diff --git a/loader/css/overlay.scss b/loader/css/overlay.scss new file mode 100644 index 00000000..f348ba1a --- /dev/null +++ b/loader/css/overlay.scss @@ -0,0 +1,58 @@ +#overlay-no-js, #critical-load { + z-index: 10000; + display: none; + position: fixed; + + top: 0; + bottom: 0; + left: 0; + right: 0; + + background: #1e1e1e; + text-align: center; + + .container { + position: relative; + display: inline-block; + + top: 20%; + } + + &.shown { + display: block; + } +} + +#critical-load { + img { + height: 12em + } + + .error { + color: #bd1515; + margin-bottom: 0 + } + + .detail { + color: #696363; + margin-top: .5em + } +} + + +@media (max-height: 750px) { + #critical-load .container { + top: unset; + } + + #critical-load { + font-size: .8rem; + + flex-direction: column; + justify-content: center; + } + + #critical-load.shown { + display: flex; + } +} \ No newline at end of file diff --git a/loader/html/index.html.ejs b/loader/html/index.html.ejs new file mode 100644 index 00000000..81b33761 --- /dev/null +++ b/loader/html/index.html.ejs @@ -0,0 +1,111 @@ +<% +/* given on compile time */ +var build_target; +var initial_script; +var initial_css; +%> + + + + + + + + + + + + + <%# TODO: Put in an appropirate image %> + + <%# Using an absolute path here since the manifest.json works only with such. %> + + + <% if(build_target === "client") { %> + TeaClient + + <% } else { %> + TeaSpeak-Web + + + <%# %> + <% } %> + + + + + + + + + + + <%# We're preloading the PNG file by default since most browser have APNG support %> + + + <%# We don't preload the bowl since it's only a div background %> + + + <%- initial_css %> + + + + + + +
+
+ + +
+
+
+ +
+
+ + +
+
+
+ +
+
+ + +
+
+ load error + +

+

+
+
+ + <%- initial_script %> + + \ No newline at end of file diff --git a/package.json b/package.json index 0fc30d0e..8f109fec 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,9 @@ "author": "TeaSpeak (WolverinDEV)", "license": "ISC", "devDependencies": { + "@babel/core": "^7.10.4", + "@babel/plugin-transform-runtime": "^7.10.4", + "@babel/preset-env": "^7.10.4", "@google-cloud/translate": "^5.3.0", "@types/dompurify": "^2.0.1", "@types/ejs": "^3.0.2", @@ -38,6 +41,7 @@ "@types/react-dom": "^16.9.5", "@types/sha256": "^0.2.0", "@types/websocket": "0.0.40", + "babel-loader": "^8.1.0", "chunk-manifest-webpack-plugin": "^1.1.2", "circular-dependency-plugin": "^5.2.0", "clean-css": "^4.2.1", @@ -81,6 +85,7 @@ }, "homepage": "https://www.teaspeak.de", "dependencies": { + "detect-browser": "^5.1.1", "@types/emoji-mart": "^3.0.2", "dompurify": "^2.0.8", "emoji-mart": "^3.0.0", diff --git a/shared/html/index.html.ejs b/shared/html/index.html.ejs deleted file mode 100644 index 4095f9f2..00000000 --- a/shared/html/index.html.ejs +++ /dev/null @@ -1,220 +0,0 @@ -<% -/* given build compile */ -var build_target; -var initial_script; -var initial_css; -%> - - - - - - - - - - - - - - - - - - - <% if(build_target === "client") { %> - TeaClient - - <% } else { %> - TeaSpeak-Web - - - - <% } %> - - - - - - - - - - - - - - <%- initial_css %> - - - - - - -
-
- - -
-
-
-
-
    -
  • -
  • -
  • -
  • -
  • -
  • -
-
-
-
- - -
-
- - -

-

-
-
- -<%# - -
- - - - - - - - - - - - - - - - - - - - - - - - -
- - -%> - <%- initial_script %> - - \ No newline at end of file diff --git a/shared/js/main.tsx b/shared/js/main.tsx index 504003a9..f62ef964 100644 --- a/shared/js/main.tsx +++ b/shared/js/main.tsx @@ -34,8 +34,6 @@ import {spawnFileTransferModal} from "tc-shared/ui/modal/transfer/ModalFileTrans import {MenuEntryType, spawn_context_menu} from "tc-shared/ui/elements/ContextMenu"; import {copy_to_clipboard} from "tc-shared/utils/helpers"; import ContextMenuEvent = JQuery.ContextMenuEvent; -import {spawnPermissionEditorModal} from "tc-shared/ui/modal/permission/ModalPermissionEditor"; -import {spawnGroupCreate} from "tc-shared/ui/modal/ModalGroups"; /* required import for init */ require("./proto").initialize(); diff --git a/tsbaseconfig.json b/tsbaseconfig.json index 2d9d3f12..17fc34fd 100644 --- a/tsbaseconfig.json +++ b/tsbaseconfig.json @@ -18,9 +18,11 @@ "webpack/WatLoader.ts", "webpack/DevelBlocks.ts", + "loader/IndexGenerator.ts", + "file.ts" ], "exclude": [ - "node_modules", + "node_modules" ] } \ No newline at end of file diff --git a/webpack.config.ts b/webpack.config.ts index 9112edea..d3e0e6e9 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -3,7 +3,7 @@ import * as fs from "fs"; import trtransformer from "./tools/trgen/ts_transformer"; import {exec} from "child_process"; import * as util from "util"; -import EJSGenerator = require("./webpack/EJSGenerator"); +import LoaderIndexGenerator = require("./loader/IndexGenerator"); const path = require('path'); const webpack = require("webpack"); @@ -44,6 +44,16 @@ const generate_definitions = async (target: string) => { } as any; }; +const isLoaderFile = (file: string) => { + if(file.startsWith(__dirname)) { + const path = file.substr(__dirname.length).replace(/\\/g, "/"); + if(path.startsWith("/loader/")) { + return true; + } + } + return false; +}; + export const config = async (target: "web" | "client") => { return { entry: { "loader": "./loader/app/index.ts" @@ -77,21 +87,10 @@ export const config = async (target: "web" | "client") => { return { }), new webpack.DefinePlugin(await generate_definitions(target)), - new EJSGenerator({ - variables: { - build_target: target - }, - input: path.join(__dirname, "shared/html/index.html.ejs"), + new LoaderIndexGenerator({ + buildTarget: target, output: path.join(__dirname, "dist/index.html"), - initialJSEntryChunk: "loader", - minify: !isDevelopment, - embedInitialJSEntryChunk: !isDevelopment, - - embedInitialCSSFile: !isDevelopment, - initialCSSFile: { - localFile: path.join(__dirname, "loader/css/loader.css"), - publicFile: "css/loader.css" - } + isDevelopment: isDevelopment }) ].filter(e => !!e), module: { @@ -127,7 +126,7 @@ export const config = async (target: "web" | "client") => { return { ] }, { - test: /\.tsx?$/, + test: (module: string) => module.match(/\.tsx?$/) && !isLoaderFile(module), exclude: /node_modules/, loader: [ @@ -154,6 +153,25 @@ export const config = async (target: "web" | "client") => { return { } ] }, + { + test: (module: string) => module.match(/\.tsx?$/) && isLoaderFile(module), + exclude: /(node_modules|bower_components)/, + + loader: [ + { + loader: "babel-loader", + options: { + presets: ["@babel/preset-env"] //Preset used for env setup + } + }, + { + loader: "ts-loader", + options: { + transpileOnly: true + } + } + ] + }, { test: /\.was?t$/, loader: [ diff --git a/webpack/EJSGenerator.ts b/webpack/EJSGenerator.ts index 4d20b907..8ebeb8dd 100644 --- a/webpack/EJSGenerator.ts +++ b/webpack/EJSGenerator.ts @@ -31,31 +31,34 @@ class EJSGenerator { this.options = options; } - private async generate_entry_js_tag(compilation: Compilation) { + 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 */ - if(entry_group.chunks.length !== 1) throw "Unsupported entry chunk size. We only support one at the moment."; - if(entry_group.chunks[0].files.length !== 1) - throw "Entry chunk has too many files. We only support to inline one!"; - const file = entry_group.chunks[0].files[0]; - if(path.extname(file) !== ".js") - throw "Entry chunk file has unknown extension"; - if(!this.options.embedInitialJSEntryChunk) { - return ''; - } else { - const script = await util.promisify(fs.readFile)(path.join(compilation.compiler.outputPath, file)); - return ``; - } + 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 ''; + } else { + const script = fs.readFileSync(path.join(compilation.compiler.outputPath, file)); + return ``; + } + }); + return tags.join("\n"); } - private async generate_entry_css_tag() { + private async generateEntryCssTag() { if(this.options.embedInitialCSSFile) { const style = await util.promisify(fs.readFile)(this.options.initialCSSFile.localFile); return `` } else { - // - return `` + return `` } } @@ -64,11 +67,12 @@ class EJSGenerator { const input = await util.promisify(fs.readFile)(this.options.input); const variables = Object.assign({}, this.options.variables); - variables["initial_script"] = await this.generate_entry_js_tag(compilation); - variables["initial_css"] = await this.generate_entry_css_tag(); + 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 */ + beautify: false, /* uglify is a bit dump and does not understands ES6 */ + context: this }); if(this.options.minify) { @@ -82,12 +86,21 @@ class EJSGenerator { removeTagWhitespace: true, minifyCSS: true, minifyJS: true, - minifyURLs: 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); + }); } }