1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-08-10 16:10:55 +00:00
* Fixed: Popout Chat from the dashboard and mod view not working correctly. Please note there is still a race condition on the dashboard popout chat. It may require several refreshes or not work at all depending on your Internet connection.
* Fixed: Only load the chat types from Twitch once. Ignore any future module loads.
* Fixed: Hide the empty bar at the bottom of Twitch pages due to incorrect styles being applied to the new snackbar container element.
* Fixed: Apollo should only clear the query cache if it makes changes to a query. Likewise, Apollo should only fetch the `gql-printer` module upon demand.
* Fixed: Remove debug logging from `utilities/dom::createElement`
* Changed: Slightly delay tool-tip repositioning when rich content is loaded, hopefully reducing flicker events.
* Changed: Refactor WebMunch, adding compatibility for a future webpack update and reducing the number of modules checked when scanning for modules.
* Changed: Allow Switchboard to keep trying to load routes if the one it tries fails to actually populate `require()`.
* API Added: `EventEmitter::hasListeners(event)` method for determining if there are any listeners for a specific event.
* API Added: `localStorage.ffzLogLevel` can be set to override the global log level.
* API Added: `log.verbose(...)` as an even weaker logging level than `debug(...)`
* API Changed: Allow Tooltip instances to add tool-tips to the DOM under a different element than the parent element used for events.
This commit is contained in:
SirStendec 2021-02-24 14:38:25 -05:00
parent 77d6cf56d2
commit ab4f72c345
16 changed files with 666 additions and 127 deletions

View file

@ -1,7 +1,7 @@
{ {
"name": "frankerfacez", "name": "frankerfacez",
"author": "Dan Salvato LLC", "author": "Dan Salvato LLC",
"version": "4.20.69", "version": "4.20.70",
"description": "FrankerFaceZ is a Twitch enhancement suite.", "description": "FrankerFaceZ is a Twitch enhancement suite.",
"license": "Apache-2.0", "license": "Apache-2.0",
"scripts": { "scripts": {

View file

@ -70,7 +70,7 @@ export const Links = {
i18n: this.i18n, i18n: this.i18n,
allow_media: show_images, allow_media: show_images,
allow_unsafe: show_unsafe, allow_unsafe: show_unsafe,
onload: tip.update onload: () => requestAnimationFrame(() => tip.update())
}; };
let content; let content;
@ -214,7 +214,7 @@ Links.tooltip.interactive = function(target) {
}; };
Links.tooltip.delayHide = function(target) { Links.tooltip.delayHide = function(target) {
if ( ! this.context.get('tooltip.rich-links') || ! this.context.get('tooltip.link-interaction') || target.dataset.isMail === 'true' ) if ( ! this.context.get('tooltip.rich-links') || target.dataset.isMail === 'true' )
return 0; return 0;
return 64; return 64;

View file

@ -112,7 +112,7 @@ export default class TooltipProvider extends Module {
onEnable() { onEnable() {
const container = document.querySelector('.sunlight-root') || document.querySelector('#root>div') || document.querySelector('#root') || document.querySelector('.clips-root') || document.body; const container = this.getRoot();
window.addEventListener('fullscreenchange', this.onFSChange); window.addEventListener('fullscreenchange', this.onFSChange);
@ -125,12 +125,17 @@ export default class TooltipProvider extends Module {
this.on(':cleanup', this.cleanup); this.on(':cleanup', this.cleanup);
} }
_createInstance(container, klass = 'ffz-tooltip', default_type = 'text') { getRoot() { // eslint-disable-line class-methods-use-this
return document.querySelector('.sunlight-root') || document.querySelector('#root>div') || document.querySelector('#root') || document.querySelector('.clips-root') || document.body;
}
_createInstance(container, klass = 'ffz-tooltip', default_type = 'text', tip_container) {
return new Tooltip(container, klass, { return new Tooltip(container, klass, {
html: true, html: true,
i18n: this.i18n, i18n: this.i18n,
live: true, live: true,
check_modifiers: true, check_modifiers: true,
container: tip_container || container,
delayHide: this.checkDelayHide.bind(this, default_type), delayHide: this.checkDelayHide.bind(this, default_type),
delayShow: this.checkDelayShow.bind(this, default_type), delayShow: this.checkDelayShow.bind(this, default_type),

View file

@ -174,43 +174,111 @@ export default class Twilight extends BaseSite {
} }
getCore() { getCore() {
if ( this._core ) if ( ! this._core )
this._core = this.web_munch.getModule('core');
return this._core; return this._core;
let core = this.web_munch.getModule('core-1');
if ( core )
return this._core = core.o;
core = this.web_munch.getModule('core-2');
if ( core )
return this._core = core.p;
core = this.web_munch.getModule('core-3');
if ( core )
return this._core = core.q;
} }
} }
const CALCULATE_BITS = '_calculateChangedBits';
Twilight.KNOWN_MODULES = { Twilight.KNOWN_MODULES = {
simplebar: n => n.globalObserver && n.initDOMLoadedElements, simplebar: n => n.globalObserver && n.initDOMLoadedElements,
react: n => n.Component && n.createElement, react: n => n.Component && n.createElement,
'core-1': n => n.o && n.o.experiments, core: n => {
'core-2': n => n.p && n.p.experiments, if ( n['$6']?.experiments )
'core-3': n => n.q && n.q.experiments, return n['$6'];
if ( n.p?.experiments )
return n.p;
if ( n.o?.experiments )
return n.o;
if ( n.q?.experiments )
return n.q;
},
cookie: n => n && n.set && n.get && n.getJSON && n.withConverter, cookie: n => n && n.set && n.get && n.getJSON && n.withConverter,
'extension-service': n => n.extensionService, 'extension-service': n => n.extensionService,
'chat-types': n => n.b && has(n.b, 'Message') && has(n.b, 'RoomMods'), 'chat-types': n => {
'gql-printer': n => n !== window && n.print, if ( has(n.b, 'Message') && has(n.b, 'RoomMods') )
return {
automod: n.a,
chat: n.b,
message: n.c,
mod: n.e
};
if ( has(n.SJ, 'Message') && has(n.SJ, 'RoomMods') )
return {
automod: n.mT,
chat: n.SJ,
message: n.Ay,
mod: n.Aw
};
},
'gql-printer': n => {
if ( n === window )
return;
if ( n.print && n.print.toString().includes('.visit') )
return n.print;
if ( n.S && n.S.toString().includes('.visit') )
return n.S;
},
mousetrap: n => n.bindGlobal && n.unbind && n.handleKey, mousetrap: n => n.bindGlobal && n.unbind && n.handleKey,
'algolia-search': n => n.a && n.a.prototype && n.a.prototype.queryTopResults && n.a.prototype.queryForType, 'algolia-search': n => {
highlightstack: n => n.b && has(n.b, '_calculateChangedBits') && n.c && has(n.c, '_calculateChangedBits') if ( n.a?.prototype?.queryTopResults && n.a.prototype.queryForType )
return n.a;
if ( n.w9?.prototype?.queryTopResults && n.w9.prototype.queryForType )
return n.w9;
},
highlightstack: n => {
if ( has(n.b, CALCULATE_BITS) && has(n.c, CALCULATE_BITS) )
return {
stack: n.b,
dispatch: n.c
};
if ( has(n.fQ, CALCULATE_BITS) && has(n.vJ, CALCULATE_BITS) )
return {
stack: n.fQ,
dispatch: n.vJ
};
}
} }
const VEND_CHUNK = n => n && n.includes('vendor');
Twilight.KNOWN_MODULES.core.use_result = true;
//Twilight.KNOWN_MODULES.core.chunks = 'core';
Twilight.KNOWN_MODULES.simplebar.chunks = VEND_CHUNK;
Twilight.KNOWN_MODULES.react.chunks = VEND_CHUNK;
Twilight.KNOWN_MODULES.cookie.chunks = VEND_CHUNK;
Twilight.KNOWN_MODULES['gql-printer'].use_result = true;
Twilight.KNOWN_MODULES['gql-printer'].chunks = VEND_CHUNK;
Twilight.KNOWN_MODULES.mousetrap.chunks = VEND_CHUNK;
const CHAT_CHUNK = n => n && n.includes('chat');
Twilight.KNOWN_MODULES['chat-types'].use_result = true;
Twilight.KNOWN_MODULES['chat-types'].chunks = CHAT_CHUNK;
Twilight.KNOWN_MODULES['highlightstack'].use_result = true;
Twilight.KNOWN_MODULES['highlightstack'].chunks = CHAT_CHUNK;
Twilight.KNOWN_MODULES['algolia-search'].use_result = true;
Twilight.KNOWN_MODULES['algolia-search'].chunks = 'core';
Twilight.POPOUT_ROUTES = [ Twilight.POPOUT_ROUTES = [
'embed-chat', 'embed-chat',
'popout' 'popout',
'dash-popout-chat',
'mod-popout-chat'
]; ];
@ -233,7 +301,9 @@ Twilight.CHAT_ROUTES = [
'squad', 'squad',
'command-center', 'command-center',
'dash-stream-manager', 'dash-stream-manager',
'mod-view' 'dash-popout-chat',
'mod-view',
'mod-popout-chat'
]; ];
@ -243,7 +313,7 @@ Twilight.ROUTE_NAMES = {
'dir-all': 'Browse Live Channels', 'dir-all': 'Browse Live Channels',
'dash': 'Dashboard', 'dash': 'Dashboard',
'popout': 'Popout Chat', 'popout': 'Popout Chat',
'dash-chat': 'Dashboard Popout Chat', 'dash-popout-chat': 'Dashboard Popout Chat',
'user-video': 'Channel Video', 'user-video': 'Channel Video',
'popout-player': 'Popout/Embed Player' 'popout-player': 'Popout/Embed Player'
}; };
@ -293,6 +363,7 @@ Twilight.DASH_ROUTES = {
'dash-settings-revenue': '/u/:userName/settings/revenue', 'dash-settings-revenue': '/u/:userName/settings/revenue',
'dash-extensions': '/u/:userName/extensions', 'dash-extensions': '/u/:userName/extensions',
'dash-streaming-tools': '/u/:userName/broadcast', 'dash-streaming-tools': '/u/:userName/broadcast',
'dash-popout-chat': '/popout/u/:userName/stream-manager/chat',
}; };
Twilight.ROUTES = { Twilight.ROUTES = {
@ -313,7 +384,7 @@ Twilight.ROUTES = {
//'dash-automod': '/:userName/dashboard/settings/automod', //'dash-automod': '/:userName/dashboard/settings/automod',
'event': '/event/:eventName', 'event': '/event/:eventName',
'popout': '/popout/:userName/chat', 'popout': '/popout/:userName/chat',
'dash-chat': '/popout/:userName/dashboard/live/chat', //'dash-chat': '/popout/:userName/dashboard/live/chat',
'video': '/videos/:videoID', 'video': '/videos/:videoID',
'user-video': '/:userName/video/:videoID', 'user-video': '/:userName/video/:videoID',
'user-videos': '/:userName/videos/:filter?', 'user-videos': '/:userName/videos/:filter?',
@ -331,7 +402,8 @@ Twilight.ROUTES = {
'squad': '/:userName/squad', 'squad': '/:userName/squad',
'command-center': '/:userName/commandcenter', 'command-center': '/:userName/commandcenter',
'embed-chat': '/embed/:userName/chat', 'embed-chat': '/embed/:userName/chat',
'mod-view': '/moderator/:userName' 'mod-view': '/moderator/:userName',
'mod-popout-chat': '/popout/moderator/:userName/chat'
}; };

View file

@ -193,10 +193,13 @@ export default class Channel extends Module {
this.removePanelTips(inst); this.removePanelTips(inst);
if ( ! inst._ffz_tips ) { if ( ! inst._ffz_tips ) {
inst._ffz_tips = this.resolve('tooltips')._createInstance(el, 'tw-link', 'link'); const tt = this.resolve('tooltips');
if ( tt ) {
inst._ffz_tips = tt._createInstance(el, 'tw-link', 'link', tt.getRoot());
inst._ffz_tip_el = el; inst._ffz_tip_el = el;
} }
} }
}
removePanelTips(inst) { // eslint-disable-line class-methods-use-this removePanelTips(inst) { // eslint-disable-line class-methods-use-this
if ( inst?._ffz_tips ) { if ( inst?._ffz_tips ) {

View file

@ -735,27 +735,32 @@ export default class ChatHook extends Module {
async grabTypes() { async grabTypes() {
const ct = await this.web_munch.findModule('chat-types'), if ( this.types_loaded )
changes = []; return;
this.automod_types = ct && ct.a || AUTOMOD_TYPES; const ct = await this.web_munch.findModule('chat-types');
this.chat_types = ct && ct.b || CHAT_TYPES;
this.message_types = ct && ct.c || MESSAGE_TYPES; this.automod_types = ct?.automod || AUTOMOD_TYPES;
this.mod_types = ct && ct.e || MOD_TYPES; this.chat_types = ct?.chat || CHAT_TYPES;
this.message_types = ct?.message || MESSAGE_TYPES;
this.mod_types = ct?.mod || MOD_TYPES;
if ( ! ct ) if ( ! ct )
return; return;
if ( ct.a && ! shallow_object_equals(ct.a, AUTOMOD_TYPES) ) this.types_loaded = true;
const changes = [];
if ( ! shallow_object_equals(this.automod_types, AUTOMOD_TYPES) )
changes.push('AUTOMOD_TYPES'); changes.push('AUTOMOD_TYPES');
if ( ct.b && ! shallow_object_equals(ct.b, CHAT_TYPES) ) if ( ! shallow_object_equals(this.chat_types, CHAT_TYPES) )
changes.push('CHAT_TYPES'); changes.push('CHAT_TYPES');
if ( ct.c && ! shallow_object_equals(ct.c, MESSAGE_TYPES) ) if ( ! shallow_object_equals(this.message_types, MESSAGE_TYPES) )
changes.push('MESSAGE_TYPES'); changes.push('MESSAGE_TYPES');
if ( ct.e && ! shallow_object_equals(ct.e, MOD_TYPES) ) if ( ! shallow_object_equals(this.mod_types, MOD_TYPES) )
changes.push('MOD_TYPES'); changes.push('MOD_TYPES');
if ( changes.length ) if ( changes.length )
@ -1296,15 +1301,15 @@ export default class ChatHook extends Module {
const t = this, const t = this,
React = this.web_munch.getModule('react'), React = this.web_munch.getModule('react'),
Stack = this.web_munch.getModule('highlightstack'), createElement = React && React.createElement,
createElement = React && React.createElement; StackMod = this.web_munch.getModule('highlightstack');
if ( ! createElement || ! Stack || ! Stack.b ) if ( ! createElement || ! StackMod )
return false; return false;
this.CommunityStackHandler = function() { this.CommunityStackHandler = function() {
const stack = React.useContext(Stack.b), const stack = React.useContext(StackMod.stack),
dispatch = React.useContext(Stack.c); dispatch = React.useContext(StackMod.dispatch);
t.community_stack = stack; t.community_stack = stack;
t.community_dispatch = dispatch; t.community_dispatch = dispatch;

View file

@ -6,3 +6,7 @@
.top-nav__menu > div:empty { .top-nav__menu > div:empty {
display: none display: none
} }
.twilight-main > .tw-relative.tw-z-above.tw-bottom-0 {
position: absolute !important;
}

View file

@ -16,6 +16,8 @@ export default class Switchboard extends Module {
this.inject('site.web_munch'); this.inject('site.web_munch');
this.inject('site.fine'); this.inject('site.fine');
this.inject('site.router'); this.inject('site.router');
this.tried = new Set;
} }
@ -64,29 +66,49 @@ export default class Switchboard extends Module {
const router = await this.awaitRouter(); const router = await this.awaitRouter();
this.log.info(`Found Route and Switch with ${da_switch.props.children.length} routes.`); this.log.info(`Found Route and Switch with ${da_switch.props.children.length} routes.`);
const location = router.props.location.pathname; this.da_switch = da_switch;
this.location = router.props.location.pathname;
//const location = router.props.location.pathname;
if ( ! this.loadRoute(da_switch, location, false) ) this.loadOne();
this.loadRoute(da_switch, location, true);
} }
loadRoute(da_switch, location, with_params) { loadOne() {
for(const route of da_switch.props.children) { if ( ! this.loadRoute(false) )
this.loadRoute(true);
}
waitAndSee() {
requestAnimationFrame(() => {
if ( this.web_munch._require )
return;
this.log.info('We still need require(). Trying again.');
this.loadOne();
});
}
loadRoute(with_params) {
for(const route of this.da_switch.props.children) {
if ( ! route.props || ! route.props.component ) if ( ! route.props || ! route.props.component )
continue; continue;
if ( with_params !== null && with_params !== route.props.path.includes(':') ) if ( with_params !== null && with_params !== route.props.path.includes(':') )
continue; continue;
if ( this.tried.has(route.props.path) )
continue;
try { try {
const reg = pathToRegexp(route.props.path); const reg = pathToRegexp(route.props.path);
if ( ! reg.exec || reg.exec(location) ) if ( ! reg.exec || reg.exec(this.location) )
continue; continue;
} catch(err) { } catch(err) {
continue; continue;
} }
this.tried.add(route.props.path);
this.log.info('Found Non-Matching Route', route.props.path); this.log.info('Found Non-Matching Route', route.props.path);
const component_class = route.props.component; const component_class = route.props.component;
@ -107,6 +129,7 @@ export default class Switchboard extends Module {
try { try {
component.props.loader().then(() => { component.props.loader().then(() => {
this.log.info('Successfully forced a chunk to load using route', route.props.path) this.log.info('Successfully forced a chunk to load using route', route.props.path)
this.waitAndSee();
}); });
} catch(err) { } catch(err) {
this.log.warn('Unexpected result trying to use component pre-loader to force loading of another chunk.'); this.log.warn('Unexpected result trying to use component pre-loader to force loading of another chunk.');
@ -126,6 +149,7 @@ export default class Switchboard extends Module {
try { try {
component.props.children.props.loader().then(() => { component.props.children.props.loader().then(() => {
this.log.info('Successfully forced a chunk to load using route', route.props.path) this.log.info('Successfully forced a chunk to load using route', route.props.path)
this.waitAndSee();
}); });
} catch(err) { } catch(err) {
this.log.warn('Unexpected result trying to use component loader to force loading of another chunk.'); this.log.warn('Unexpected result trying to use component loader to force loading of another chunk.');

View file

@ -8,6 +8,7 @@
import Module from 'utilities/module'; import Module from 'utilities/module';
import {get} from 'utilities/object'; import {get} from 'utilities/object';
import merge from 'utilities/graphql'; import merge from 'utilities/graphql';
import { FFZEvent } from 'utilities/events';
/*const BAD_ERRORS = [ /*const BAD_ERRORS = [
@ -35,6 +36,20 @@ function skip_error(err) {
}*/ }*/
export class ApolloEvent extends FFZEvent {
constructor(data) {
super(data);
this._changed = false;
}
markChanged() {
this._changed = true;
return this;
}
}
export class GQLError extends Error { export class GQLError extends Error {
constructor(err) { constructor(err) {
super(`${err.message}; Location: ${err.locations}`); super(`${err.message}; Location: ${err.locations}`);
@ -49,11 +64,20 @@ export default class Apollo extends Module {
this.modifiers = {}; this.modifiers = {};
this.post_modifiers = {}; this.post_modifiers = {};
this.inject('..web_munch');
this.inject('..fine'); this.inject('..fine');
} }
async onEnable() { get gqlPrint() {
if ( this._gql_print )
return this._gql_print;
const web_munch = this.resolve('site.web_munch'),
printer = this._gql_print = web_munch?.getModule?.('gql-printer');
return printer;
}
onEnable() {
// TODO: Come up with a better way to await something existing. // TODO: Come up with a better way to await something existing.
let client = this.client; let client = this.client;
@ -69,9 +93,6 @@ export default class Apollo extends Module {
if ( ! client ) if ( ! client )
return new Promise(() => this.onEnable(), 50); return new Promise(() => this.onEnable(), 50);
this.printer = await this.web_munch.findModule('gql-printer');
this.gql_print = this.printer && this.printer.print;
// Register middleware so that we can intercept requests. // Register middleware so that we can intercept requests.
if ( ! this.client.link || ! this.client.queryManager || ! this.client.queryManager.link ) { if ( ! this.client.link || ! this.client.queryManager || ! this.client.queryManager.link ) {
this.log.error('Apollo does not have a Link. We are unable to manipulate queries.'); this.log.error('Apollo does not have a Link. We are unable to manipulate queries.');
@ -216,19 +237,25 @@ export default class Apollo extends Module {
onDisable() { onDisable() {
// Remove our references to things. // Remove our references to things.
this.client = this.printer = this.gql_print = this.old_link = this.old_qm_dedup = this.old_qm_link = null; this.client = this.printer = this._gql_print = this.old_link = this.old_qm_dedup = this.old_qm_link = null;
} }
apolloPreFlight(request) { apolloPreFlight(request) {
const operation = request.operationName, const operation = request.operationName,
qm = this.client.queryManager, modifiers = this.modifiers[operation],
event = `:request.${operation}`,
has_listeners = this.hasListeners(event);
if ( ! modifiers && ! has_listeners )
return;
const qm = this.client.queryManager,
id_map = qm && qm.queryIdsByName, id_map = qm && qm.queryIdsByName,
query_map = qm && qm.queries, query_map = qm && qm.queries,
raw_id = id_map && id_map[operation], raw_id = id_map && id_map[operation],
id = Array.isArray(raw_id) ? raw_id[0] : raw_id, id = Array.isArray(raw_id) ? raw_id[0] : raw_id,
query = query_map && query_map.get(id), query = query_map && query_map.get(id);
modifiers = this.modifiers[operation];
if ( modifiers ) { if ( modifiers ) {
for(const mod of modifiers) { for(const mod of modifiers) {
@ -239,13 +266,25 @@ export default class Apollo extends Module {
} }
} }
this.emit(`:request.${operation}`, request.query, request.variables); let modified = !! modifiers;
if ( has_listeners ) {
const e = new ApolloEvent({
operation,
request
});
this.emit(event, e);
if ( e._changed )
modified = true;
}
if ( modified ) {
// Wipe the old query data. This is obviously not optimal, but Apollo will // Wipe the old query data. This is obviously not optimal, but Apollo will
// raise an exception otherwise because the query string doesn't match. // raise an exception otherwise because the query string doesn't match.
const q = this.client.queryManager.queryStore.store[id], const q = this.client.queryManager.queryStore.store[id],
qs = this.gql_print && this.gql_print(request.query); qs = this.gqlPrint && this.gqlPrint(request.query);
if ( q ) if ( q )
if ( qs ) { if ( qs ) {
@ -264,6 +303,7 @@ export default class Apollo extends Module {
this.client.queryManager.queryStore.store[id] = null; this.client.queryManager.queryStore.store[id] = null;
} }
} }
}
apolloPostFlight(response) { apolloPostFlight(response) {
if ( ! response.extensions ) if ( ! response.extensions )

View file

@ -7,7 +7,17 @@
import Module from 'utilities/module'; import Module from 'utilities/module';
import {has} from 'utilities/object'; import {has} from 'utilities/object';
import { DEBUG } from '../constants';
const NAMES = [
'webpackJsonp',
'webpackChunktwitch_twilight'
];
const HARD_MODULES = [
[0, 'vendor'],
[1, 'core']
];
let last_muncher = 0; let last_muncher = 0;
@ -20,9 +30,11 @@ export default class WebMunch extends Module {
this._original_loader = null; this._original_loader = null;
this._known_rules = {}; this._known_rules = {};
this._require = null; this._require = null;
this._module_names = {}; this._chunk_names = {};
this._mod_cache = {}; this._mod_cache = {};
this._known_ids = new Set;
this.v4 = null; this.v4 = null;
this.hookLoader(); this.hookLoader();
@ -38,37 +50,47 @@ export default class WebMunch extends Module {
if ( this._original_loader ) if ( this._original_loader )
return this.log.warn('Attempted to call hookLoader twice.'); return this.log.warn('Attempted to call hookLoader twice.');
if ( ! window.webpackJsonp ) { let name;
for(const n of NAMES)
if ( window[n] ) {
name = n;
break;
}
if ( ! name ) {
if ( attempts > 500 ) if ( attempts > 500 )
return this.log.error("Unable to find webpack's loader after two minutes."); return this.log.error("Unable to find webpack's loader after two minutes.");
return setTimeout(this.hookLoader.bind(this, attempts + 1), 250); return setTimeout(this.hookLoader.bind(this, attempts + 1), 250);
} }
if ( typeof window.webpackJsonp === 'function' ) { const thing = window[name];
if ( typeof thing === 'function' ) {
// v3 // v3
this.v4 = false; this.v4 = false;
this._original_loader = window.webpackJsonp; this._original_loader = thing;
try { try {
window.webpackJsonp = this.webpackJsonpv3.bind(this); window[name] = this.webpackJsonpv3.bind(this);
} catch(err) { } catch(err) {
this.log.warn('Unable to wrap webpackJsonp due to write protection.'); this.log.warn('Unable to wrap webpackJsonp due to write protection.');
return; return;
} }
} else if ( Array.isArray(window.webpackJsonp) ) { } else if ( Array.isArray(thing) ) {
// v4 // v4
this.v4 = true; this.v4 = true;
this._original_loader = window.webpackJsonp.push; this._original_store = thing;
this._original_loader = thing.push;
// Wrap all existing modules in case any of them haven't been required yet. // Wrap all existing modules in case any of them haven't been required yet.
for(const chunk of window.webpackJsonp) for(const chunk of thing)
if ( chunk && chunk[1] ) if ( chunk && chunk[1] )
this.processModulesV4(chunk[1]); this.processModulesV4(chunk[1]);
try { try {
window.webpackJsonp.push = this.webpackJsonpv4.bind(this); thing.push = this.webpackJsonpv4.bind(this);
} catch(err) { } catch(err) {
this.log.warn('Unable to wrap webpackJsonp (v4) due to write protection.'); this.log.warn('Unable to wrap webpackJsonp (v4) due to write protection.');
return; return;
@ -84,9 +106,9 @@ export default class WebMunch extends Module {
webpackJsonpv3(chunk_ids, modules) { webpackJsonpv3(chunk_ids, modules) {
const names = chunk_ids.map(x => this._module_names[x] || x).join(', '); const names = chunk_ids.map(x => this._chunk_names[x] || x).join(', ');
this.log.debug(`Twitch Chunk Loaded: ${chunk_ids} (${names})`); this.log.verbose(`Twitch Chunk Loaded: ${chunk_ids} (${names})`);
this.log.debug(`Modules: ${Object.keys(modules)}`); this.log.verbose(`Modules: ${Object.keys(modules)}`);
const res = this._original_loader.apply(window, arguments); // eslint-disable-line prefer-rest-params const res = this._original_loader.apply(window, arguments); // eslint-disable-line prefer-rest-params
@ -101,6 +123,7 @@ export default class WebMunch extends Module {
for(const mod_id in modules) for(const mod_id in modules)
if ( has(modules, mod_id) ) { if ( has(modules, mod_id) ) {
this._known_ids.add(mod_id);
const original_module = modules[mod_id]; const original_module = modules[mod_id];
modules[mod_id] = function(module, exports, require, ...args) { modules[mod_id] = function(module, exports, require, ...args) {
if ( ! t._require && typeof require === 'function' ) { if ( ! t._require && typeof require === 'function' ) {
@ -127,15 +150,15 @@ export default class WebMunch extends Module {
webpackJsonpv4(data) { webpackJsonpv4(data) {
const chunk_ids = data[0], const chunk_ids = data[0],
modules = data[1], modules = data[1],
names = Array.isArray(chunk_ids) && chunk_ids.map(x => this._module_names[x] || x).join(', '); names = Array.isArray(chunk_ids) && chunk_ids.map(x => this._chunk_names[x] || x).join(', ');
this.log.debug(`Twitch Chunk Loaded: ${chunk_ids} (${names})`); this.log.verbose(`Twitch Chunk Loaded: ${chunk_ids} (${names})`);
this.log.debug(`Modules: ${Object.keys(modules)}`); this.log.verbose(`Modules: ${Object.keys(modules)}`);
if ( modules ) if ( modules )
this.processModulesV4(modules); this.processModulesV4(modules);
const res = this._original_loader.apply(window.webpackJsonp, arguments); // eslint-disable-line prefer-rest-params const res = this._original_loader.apply(this._original_store, arguments); // eslint-disable-line prefer-rest-params
this.emit(':loaded', chunk_ids, names, modules); this.emit(':loaded', chunk_ids, names, modules);
return res; return res;
} }
@ -166,6 +189,54 @@ export default class WebMunch extends Module {
} }
findDeep(chunks, predicate, multi = true) {
if ( chunks && ! Array.isArray(chunks) )
chunks = [chunks];
if ( ! this._require || ! this.v4 || ! this._original_store )
return new Error('We do not have webpack');
const out = [],
names = this._chunk_names;
for(const [cs, modules] of this._original_store) {
if ( chunks ) {
let matched = false;
for(const c of cs) {
if ( chunks.includes(c) || chunks.includes(`${c}`) || (names[c] && chunks.includes(names[c])) ) {
matched = true;
break;
}
}
if ( ! matched )
continue;
}
for(const id of Object.keys(modules)) {
try {
const mod = this._require(id);
for(const key in mod)
if ( mod[key] && predicate(mod[key]) ) {
this.log.info(`Found in key "${key}" of module "${id}" (${this.chunkNameForModule(id)})`);
if ( ! multi )
return mod;
out.push(mod);
break;
}
} catch(err) {
this.log.warn('Exception while deep scanning webpack.', err);
}
}
}
if ( out.length )
return out;
this.log.info('Unable to find deep scan target.');
return null;
}
getModule(key, predicate) { getModule(key, predicate) {
if ( typeof key === 'function' ) { if ( typeof key === 'function' ) {
predicate = key; predicate = key;
@ -176,7 +247,7 @@ export default class WebMunch extends Module {
return this._mod_cache[key]; return this._mod_cache[key];
const require = this._require; const require = this._require;
if ( ! require || ! require.c ) if ( ! require )
return null; return null;
if ( ! predicate ) if ( ! predicate )
@ -185,19 +256,149 @@ export default class WebMunch extends Module {
if ( ! predicate ) if ( ! predicate )
throw new Error(`no known predicate for locating ${key}`); throw new Error(`no known predicate for locating ${key}`);
for(const k in require.c) if ( require.c )
return this._oldGetModule(key, predicate, require);
if ( require.m )
return this._newGetModule(key, predicate, require);
}
_chunksForModule(id) {
if ( ! this.v4 )
return null;
if ( ! this._original_store )
return null;
for(const [chunks, modules] of this._original_store) {
if ( modules[id] )
return chunks;
}
}
chunkNameForModule(id) {
const chunks = this._chunksForModule(id);
if ( ! chunks )
return null;
for(const chunk of chunks) {
const name = this._chunk_names[chunk];
if ( name )
return name;
}
return null;
}
_oldGetModule(key, predicate, require) {
if ( ! require || ! require.c )
return null;
let ids;
if ( this._original_store && predicate.chunks ) {
const chunk_pred = typeof predicate.chunks === 'function';
if ( ! chunk_pred && ! Array.isArray(predicate.chunks) )
predicate.chunks = [predicate.chunks];
const chunks = predicate.chunks,
names = this._chunk_names;
ids = [];
for(const [cs, modules] of this._original_store) {
let matched = false;
for(const c of cs) {
if ( chunk_pred ? chunks(names[c], c) : (chunks.includes(c) || chunks.includes(String(c)) || (names[c] && chunks.includes(names[c]))) ) {
matched = true;
break;
}
}
if ( matched )
ids = [...ids, ...Object.keys(modules)];
}
ids = new Set(ids);
} else
ids = Object.keys(require.c);
let checked = 0;
for(const k of ids)
if ( has(require.c, k) ) { if ( has(require.c, k) ) {
checked++;
const module = require.c[k], const module = require.c[k],
mod = module && module.exports; mod = module && module.exports;
if ( mod && predicate(mod) ) { if ( mod ) {
const ret = predicate(mod);
if ( ret ) {
this.log.debug(`Located module "${key}" in module ${k}${DEBUG ? ` (${this.chunkNameForModule(k)})` : ''} after ${checked} tries`);
const out = predicate.use_result ? ret : mod;
if ( key ) if ( key )
this._mod_cache[key] = mod; this._mod_cache[key] = out;
return mod; return out;
} }
} }
} }
this.log.debug(`Unable to locate module "${key}"`);
return null;
}
_newGetModule(key, predicate, require) {
if ( ! require )
return null;
let ids = this._known_ids;
if ( this._original_store && predicate.chunks ) {
const chunk_pred = typeof predicate.chunks === 'function';
if ( ! chunk_pred && ! Array.isArray(predicate.chunks) )
predicate.chunks = [predicate.chunks];
const chunks = predicate.chunks,
names = this._chunk_names;
ids = [];
for(const [cs, modules] of this._original_store) {
let matched = false;
for(const c of cs) {
if ( chunk_pred ? chunks(names[c], c) : (chunks.includes(c) || chunks.includes(String(c)) || (names[c] && chunks.includes(names[c]))) ) {
matched = true;
break;
}
}
if ( matched )
ids = [...ids, ...Object.keys(modules)];
}
ids = new Set(ids);
}
let checked = 0;
for(const id of ids) {
try {
checked++;
const mod = require(id);
if ( mod ) {
const ret = predicate(mod);
if ( ret ) {
this.log.debug(`Located module "${key}" in module ${id}${DEBUG ? ` (${this.chunkNameForModule(id)})` : ''} after ${checked} tries`);
const out = predicate.use_result ? ret : mod;
if ( key )
this._mod_cache[key] = out;
return out;
}
}
} catch(err) {
this.log.warn('Unexpected error trying to find module', err);
}
}
this.log.debug(`Unable to locate module "${key}"`);
return null;
}
// ======================================================================== // ========================================================================
// Grabbing Require // Grabbing Require
@ -256,10 +457,38 @@ export default class WebMunch extends Module {
try { try {
modules = JSON.parse(data[1].replace(/(\d+):/g, '"$1":')) modules = JSON.parse(data[1].replace(/(\d+):/g, '"$1":'))
} catch(err) { } // eslint-disable-line no-empty } catch(err) { } // eslint-disable-line no-empty
} else if ( require.u ) {
const builder = require.u.toString(),
match = /assets\/"\+({\d+:.*?})/.exec(builder),
data = match ? match[1].replace(/([\de]+):/g, (_, m) => {
if ( /^\d+e\d+$/.test(m) ) {
const bits = m.split('e');
m = parseInt(bits[0], 10) * (10 ** parseInt(bits[1], 10));
}
return `"${m}":`;
}) : null;
if ( data )
try {
modules = JSON.parse(data);
} catch(err) { console.log(data); console.log(err) /* no-op */ }
} }
if ( modules ) { if ( modules ) {
this._module_names = modules; // Ensure that vendor and core have names.
if ( this._original_store ) {
for(const [pos, name] of HARD_MODULES) {
const mods = this._original_store[pos]?.[0];
if ( Array.isArray(mods) )
for(const id of mods)
if ( typeof id !== 'object' && ! modules[id] )
modules[id] = name;
}
}
this._chunk_names = modules;
this.log.info(`Loaded names for ${Object.keys(modules).length} chunks from require().`) this.log.info(`Loaded names for ${Object.keys(modules).length} chunks from require().`)
} else } else
this.log.warn(`Unable to find chunk names in require().`); this.log.warn(`Unable to find chunk names in require().`);

View file

@ -127,7 +127,6 @@ export function createElement(tag, props, ...children) {
else if ( BOOLEAN_ATTRS.includes(lk) ) { else if ( BOOLEAN_ATTRS.includes(lk) ) {
if ( prop && prop !== 'false' ) if ( prop && prop !== 'false' )
el.setAttribute(key, prop); el.setAttribute(key, prop);
console.log('bool-attr', key, prop);
} else if ( lk.startsWith('aria-') || ATTRS.includes(lk) ) } else if ( lk.startsWith('aria-') || ATTRS.includes(lk) )
el.setAttribute(key, prop); el.setAttribute(key, prop);

View file

@ -124,6 +124,10 @@ export class EventEmitter {
return list ? Array.from(list) : []; return list ? Array.from(list) : [];
} }
hasListeners(event) {
return !! this.__listeners[event]
}
emitUnsafe(event, ...args) { emitUnsafe(event, ...args) {
let list = this.__listeners[event]; let list = this.__listeners[event];
if ( ! list ) if ( ! list )
@ -446,6 +450,7 @@ export class HierarchicalEventEmitter extends EventEmitter {
waitFor(event) { return super.waitFor(this.abs_path(event)) } waitFor(event) { return super.waitFor(this.abs_path(event)) }
off(event, fn, ctx) { return super.off(this.abs_path(event), fn, ctx) } off(event, fn, ctx) { return super.off(this.abs_path(event), fn, ctx) }
listeners(event) { return super.listeners(this.abs_path(event)) } listeners(event) { return super.listeners(this.abs_path(event)) }
hasListeners(event) { return super.hasListeners(this.abs_path(event)) }
emit(event, ...args) { return super.emit(this.abs_path(event), ...args) } emit(event, ...args) { return super.emit(this.abs_path(event), ...args) }
emitUnsafe(event, ...args) { return super.emitUnsafe(this.abs_path(event), ...args) } emitUnsafe(event, ...args) { return super.emitUnsafe(this.abs_path(event), ...args) }

View file

@ -8,6 +8,22 @@ const RAVEN_LEVELS = {
}; };
function readLSLevel() {
const level = localStorage.ffzLogLevel;
if ( ! level )
return null;
const upper = level.toUpperCase();
if ( Logger[upper] )
return Logger[upper];
if ( /^\d+$/.test(level) )
return parseInt(level, 10);
return null;
}
export class Logger { export class Logger {
constructor(parent, name, level, raven) { constructor(parent, name, level, raven) {
this.root = parent ? parent.root : this; this.root = parent ? parent.root : this;
@ -21,7 +37,7 @@ export class Logger {
this.init = false; this.init = false;
this.enabled = true; this.enabled = true;
this.level = level || (parent && parent.level) || Logger.DEFAULT_LEVEL; this.level = level ?? (parent && parent.level) ?? readLSLevel() ?? Logger.DEFAULT_LEVEL;
this.raven = raven || (parent && parent.raven); this.raven = raven || (parent && parent.raven);
this.children = {}; this.children = {};
@ -34,6 +50,10 @@ export class Logger {
return this.children[name]; return this.children[name];
} }
verbose(...args) {
return this.invoke(Logger.VERBOSE, args);
}
debug(...args) { debug(...args) {
return this.invoke(Logger.DEBUG, args); return this.invoke(Logger.DEBUG, args);
} }
@ -79,6 +99,7 @@ export class Logger {
const message = Array.prototype.slice.call(args); const message = Array.prototype.slice.call(args);
if ( level !== Logger.VERBOSE ) {
if ( this.root.init ) if ( this.root.init )
this.root.captured_init.push({ this.root.captured_init.push({
time: Date.now(), time: Date.now(),
@ -92,13 +113,14 @@ export class Logger {
category: this.name, category: this.name,
level: RAVEN_LEVELS[level] || level level: RAVEN_LEVELS[level] || level
}); });
}
if ( this.name ) if ( this.name )
message.unshift(`%c${this.root.label} [%c${this.name}%c]:%c`, 'color:#755000; font-weight:bold', '', 'color:#755000; font-weight:bold', ''); message.unshift(`%c${this.root.label} [%c${this.name}%c]:%c`, 'color:#755000; font-weight:bold', '', 'color:#755000; font-weight:bold', '');
else else
message.unshift(`%c${this.root.label}:%c`, 'color:#755000; font-weight:bold', ''); message.unshift(`%c${this.root.label}:%c`, 'color:#755000; font-weight:bold', '');
if ( level === Logger.DEBUG ) if ( level === Logger.DEBUG || level === Logger.VERBOSE )
console.debug(...message); console.debug(...message);
else if ( level === Logger.INFO ) else if ( level === Logger.INFO )
@ -115,9 +137,7 @@ export class Logger {
} }
} }
Logger.VERBOSE = 0;
Logger.DEFAULT_LEVEL = 2;
Logger.DEBUG = 1; Logger.DEBUG = 1;
Logger.INFO = 2; Logger.INFO = 2;
Logger.WARN = 4; Logger.WARN = 4;
@ -125,4 +145,6 @@ Logger.WARNING = 4;
Logger.ERROR = 8; Logger.ERROR = 8;
Logger.OFF = 99; Logger.OFF = 99;
Logger.DEFAULT_LEVEL = Logger.INFO;
export default Logger; export default Logger;

View file

@ -0,0 +1,130 @@
'use strict';
export function parse(path) {
return parseAST({
path,
i: 0
});
}
function parseAST(ctx) {
const path = ctx.path,
length = path.length,
out = [];
let token, raw;
let old_tab = false,
old_page = false;
while ( ctx.i < length ) {
const start = ctx.i,
char = path[start],
next = path[start + 1];
if ( ! token ) {
raw = [];
token = {};
}
// JSON
if ( char === '@' && next === '{') {
ctx.i++;
const tag = parseJSON(ctx);
if ( tag )
Object.assign(token, tag);
continue;
}
// Segment End?
const tab = char === `~` && next === '>',
page = char === '>' && next === '>',
segment = ! page && char === '>';
if ( ! segment && ! page && ! tab ) {
raw.push(char);
ctx.i++;
continue;
}
// We're at the end of a segment, so push
// the token out.
if ( tab || page )
ctx.i++;
token.title = raw.join('').trim();
token.key = token.title.toSnakeCase();
token.page = old_page;
token.tab = old_tab;
old_page = page;
old_tab = tab;
out.push(token);
token = raw = null;
ctx.i++;
}
if ( token ) {
token.title = raw.join('').trim();
token.key = token.title.toSnakeCase();
token.page = old_page;
token.tab = old_tab;
out.push(token);
}
return out;
}
function parseJSON(ctx) {
const path = ctx.path,
length = path.length,
start = ctx.i;
ctx.i++;
const stack = ['{'];
let string = false;
while ( ctx.i < length && stack.length ) {
const start = ctx.i,
char = path[start];
if ( string ) {
if ( char === '\\' ) {
ctx.i++;
continue;
}
if ( (char === '"' || char === "'") && char === string ) {
stack.pop();
string = false;
}
} else {
if ( char === '"' || char === "'" ) {
string = char;
stack.push(char);
}
if ( char === '{' || char === '[' )
stack.push(char);
if ( char === ']' ) {
if ( stack.pop() !== '[' )
throw new SyntaxError('Invalid JSON');
}
if ( char === '}' ) {
if ( stack.pop() !== '{' )
throw new SyntaxError('Invalid JSON');
}
}
ctx.i++;
}
return JSON.parse(path.slice(start, ctx.i));
}

View file

@ -45,6 +45,7 @@ export class Tooltip {
this.check_modifiers = this.options.check_modifiers; this.check_modifiers = this.options.check_modifiers;
this.parent = parent; this.parent = parent;
this.container = this.options.container || this.parent;
this.cls = cls; this.cls = cls;
if ( this.check_modifiers ) if ( this.check_modifiers )
@ -134,6 +135,7 @@ export class Tooltip {
this.elements = null; this.elements = null;
this._onMouseOut = this._onMouseOver = null; this._onMouseOut = this._onMouseOver = null;
this.container = null;
this.parent = null; this.parent = null;
} }
@ -367,8 +369,9 @@ export class Tooltip {
tip._update = () => { tip._update = () => {
if ( tip.popper ) { if ( tip.popper ) {
tip.popper.destroy(); tip.popper.update();
tip.popper = new Popper(popper_target, el, pop_opts); /*tip.popper.destroy();
tip.popper = new Popper(popper_target, el, pop_opts);*/
} }
} }
@ -412,7 +415,7 @@ export class Tooltip {
// Add everything to the DOM and create the Popper instance. // Add everything to the DOM and create the Popper instance.
tip.popper = new Popper(popper_target, el, pop_opts); tip.popper = new Popper(popper_target, el, pop_opts);
this.parent.appendChild(el); this.container.appendChild(el);
tip.visible = true; tip.visible = true;
if ( opts.onShow ) if ( opts.onShow )

View file

@ -138,11 +138,9 @@ export default class TwitchData extends Module {
return this._search; return this._search;
const apollo = this.apollo.client, const apollo = this.apollo.client,
core = this.site.getCore(), core = this.site.getCore();
search_module = this.web_munch.getModule('algolia-search'),
SearchClient = search_module && search_module.a;
const SearchClient = this.web_munch.getModule('algolia-search');
if ( ! SearchClient || ! apollo || ! core ) if ( ! SearchClient || ! apollo || ! core )
return null; return null;