1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-27 21:05:53 +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",
"author": "Dan Salvato LLC",
"version": "4.20.69",
"version": "4.20.70",
"description": "FrankerFaceZ is a Twitch enhancement suite.",
"license": "Apache-2.0",
"scripts": {

View file

@ -70,7 +70,7 @@ export const Links = {
i18n: this.i18n,
allow_media: show_images,
allow_unsafe: show_unsafe,
onload: tip.update
onload: () => requestAnimationFrame(() => tip.update())
};
let content;
@ -214,7 +214,7 @@ Links.tooltip.interactive = 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 64;

View file

@ -112,7 +112,7 @@ export default class TooltipProvider extends Module {
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);
@ -125,12 +125,17 @@ export default class TooltipProvider extends Module {
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, {
html: true,
i18n: this.i18n,
live: true,
check_modifiers: true,
container: tip_container || container,
delayHide: this.checkDelayHide.bind(this, default_type),
delayShow: this.checkDelayShow.bind(this, default_type),

View file

@ -174,43 +174,111 @@ export default class Twilight extends BaseSite {
}
getCore() {
if ( this._core )
return this._core;
if ( ! this._core )
this._core = this.web_munch.getModule('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;
return this._core;
}
}
const CALCULATE_BITS = '_calculateChangedBits';
Twilight.KNOWN_MODULES = {
simplebar: n => n.globalObserver && n.initDOMLoadedElements,
react: n => n.Component && n.createElement,
'core-1': n => n.o && n.o.experiments,
'core-2': n => n.p && n.p.experiments,
'core-3': n => n.q && n.q.experiments,
core: n => {
if ( n['$6']?.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,
'extension-service': n => n.extensionService,
'chat-types': n => n.b && has(n.b, 'Message') && has(n.b, 'RoomMods'),
'gql-printer': n => n !== window && n.print,
'chat-types': n => {
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,
'algolia-search': n => n.a && n.a.prototype && n.a.prototype.queryTopResults && n.a.prototype.queryForType,
highlightstack: n => n.b && has(n.b, '_calculateChangedBits') && n.c && has(n.c, '_calculateChangedBits')
'algolia-search': n => {
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 = [
'embed-chat',
'popout'
'popout',
'dash-popout-chat',
'mod-popout-chat'
];
@ -233,7 +301,9 @@ Twilight.CHAT_ROUTES = [
'squad',
'command-center',
'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',
'dash': 'Dashboard',
'popout': 'Popout Chat',
'dash-chat': 'Dashboard Popout Chat',
'dash-popout-chat': 'Dashboard Popout Chat',
'user-video': 'Channel Video',
'popout-player': 'Popout/Embed Player'
};
@ -293,6 +363,7 @@ Twilight.DASH_ROUTES = {
'dash-settings-revenue': '/u/:userName/settings/revenue',
'dash-extensions': '/u/:userName/extensions',
'dash-streaming-tools': '/u/:userName/broadcast',
'dash-popout-chat': '/popout/u/:userName/stream-manager/chat',
};
Twilight.ROUTES = {
@ -313,7 +384,7 @@ Twilight.ROUTES = {
//'dash-automod': '/:userName/dashboard/settings/automod',
'event': '/event/:eventName',
'popout': '/popout/:userName/chat',
'dash-chat': '/popout/:userName/dashboard/live/chat',
//'dash-chat': '/popout/:userName/dashboard/live/chat',
'video': '/videos/:videoID',
'user-video': '/:userName/video/:videoID',
'user-videos': '/:userName/videos/:filter?',
@ -331,7 +402,8 @@ Twilight.ROUTES = {
'squad': '/:userName/squad',
'command-center': '/:userName/commandcenter',
'embed-chat': '/embed/:userName/chat',
'mod-view': '/moderator/:userName'
'mod-view': '/moderator/:userName',
'mod-popout-chat': '/popout/moderator/:userName/chat'
};

View file

@ -193,8 +193,11 @@ export default class Channel extends Module {
this.removePanelTips(inst);
if ( ! inst._ffz_tips ) {
inst._ffz_tips = this.resolve('tooltips')._createInstance(el, 'tw-link', 'link');
inst._ffz_tip_el = el;
const tt = this.resolve('tooltips');
if ( tt ) {
inst._ffz_tips = tt._createInstance(el, 'tw-link', 'link', tt.getRoot());
inst._ffz_tip_el = el;
}
}
}

View file

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

View file

@ -5,4 +5,8 @@
.top-nav__menu > div:empty {
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.fine');
this.inject('site.router');
this.tried = new Set;
}
@ -64,29 +66,49 @@ export default class Switchboard extends Module {
const router = await this.awaitRouter();
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.loadRoute(da_switch, location, true);
this.loadOne();
}
loadRoute(da_switch, location, with_params) {
for(const route of da_switch.props.children) {
loadOne() {
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 )
continue;
if ( with_params !== null && with_params !== route.props.path.includes(':') )
continue;
if ( this.tried.has(route.props.path) )
continue;
try {
const reg = pathToRegexp(route.props.path);
if ( ! reg.exec || reg.exec(location) )
if ( ! reg.exec || reg.exec(this.location) )
continue;
} catch(err) {
continue;
}
this.tried.add(route.props.path);
this.log.info('Found Non-Matching Route', route.props.path);
const component_class = route.props.component;
@ -107,6 +129,7 @@ export default class Switchboard extends Module {
try {
component.props.loader().then(() => {
this.log.info('Successfully forced a chunk to load using route', route.props.path)
this.waitAndSee();
});
} catch(err) {
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 {
component.props.children.props.loader().then(() => {
this.log.info('Successfully forced a chunk to load using route', route.props.path)
this.waitAndSee();
});
} catch(err) {
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 {get} from 'utilities/object';
import merge from 'utilities/graphql';
import { FFZEvent } from 'utilities/events';
/*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 {
constructor(err) {
super(`${err.message}; Location: ${err.locations}`);
@ -49,11 +64,20 @@ export default class Apollo extends Module {
this.modifiers = {};
this.post_modifiers = {};
this.inject('..web_munch');
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.
let client = this.client;
@ -69,9 +93,6 @@ export default class Apollo extends Module {
if ( ! client )
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.
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.');
@ -216,19 +237,25 @@ export default class Apollo extends Module {
onDisable() {
// 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) {
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,
query_map = qm && qm.queries,
raw_id = id_map && id_map[operation],
id = Array.isArray(raw_id) ? raw_id[0] : raw_id,
query = query_map && query_map.get(id),
modifiers = this.modifiers[operation];
query = query_map && query_map.get(id);
if ( modifiers ) {
for(const mod of modifiers) {
@ -239,30 +266,43 @@ export default class Apollo extends Module {
}
}
this.emit(`:request.${operation}`, request.query, request.variables);
let modified = !! modifiers;
// Wipe the old query data. This is obviously not optimal, but Apollo will
// raise an exception otherwise because the query string doesn't match.
if ( has_listeners ) {
const e = new ApolloEvent({
operation,
request
});
const q = this.client.queryManager.queryStore.store[id],
qs = this.gql_print && this.gql_print(request.query);
this.emit(event, e);
if ( e._changed )
modified = true;
}
if ( q )
if ( qs ) {
q.queryString = qs;
request.query.loc.source.body = qs;
request.query.loc.end = qs.length;
if ( modified ) {
// Wipe the old query data. This is obviously not optimal, but Apollo will
// raise an exception otherwise because the query string doesn't match.
if ( query ) {
query.document = request.query;
if ( query.observableQuery && query.observableQuery.options )
query.observableQuery.options.query = request.query;
const q = this.client.queryManager.queryStore.store[id],
qs = this.gqlPrint && this.gqlPrint(request.query);
if ( q )
if ( qs ) {
q.queryString = qs;
request.query.loc.source.body = qs;
request.query.loc.end = qs.length;
if ( query ) {
query.document = request.query;
if ( query.observableQuery && query.observableQuery.options )
query.observableQuery.options.query = request.query;
}
} else {
this.log.info('Unable to find GQL Print. Clearing store for query:', operation);
this.client.queryManager.queryStore.store[id] = null;
}
} else {
this.log.info('Unable to find GQL Print. Clearing store for query:', operation);
this.client.queryManager.queryStore.store[id] = null;
}
}
}
apolloPostFlight(response) {

View file

@ -7,7 +7,17 @@
import Module from 'utilities/module';
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;
@ -20,9 +30,11 @@ export default class WebMunch extends Module {
this._original_loader = null;
this._known_rules = {};
this._require = null;
this._module_names = {};
this._chunk_names = {};
this._mod_cache = {};
this._known_ids = new Set;
this.v4 = null;
this.hookLoader();
@ -38,37 +50,47 @@ export default class WebMunch extends Module {
if ( this._original_loader )
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 )
return this.log.error("Unable to find webpack's loader after two minutes.");
return setTimeout(this.hookLoader.bind(this, attempts + 1), 250);
}
if ( typeof window.webpackJsonp === 'function' ) {
const thing = window[name];
if ( typeof thing === 'function' ) {
// v3
this.v4 = false;
this._original_loader = window.webpackJsonp;
this._original_loader = thing;
try {
window.webpackJsonp = this.webpackJsonpv3.bind(this);
window[name] = this.webpackJsonpv3.bind(this);
} catch(err) {
this.log.warn('Unable to wrap webpackJsonp due to write protection.');
return;
}
} else if ( Array.isArray(window.webpackJsonp) ) {
} else if ( Array.isArray(thing) ) {
// v4
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.
for(const chunk of window.webpackJsonp)
for(const chunk of thing)
if ( chunk && chunk[1] )
this.processModulesV4(chunk[1]);
try {
window.webpackJsonp.push = this.webpackJsonpv4.bind(this);
thing.push = this.webpackJsonpv4.bind(this);
} catch(err) {
this.log.warn('Unable to wrap webpackJsonp (v4) due to write protection.');
return;
@ -84,9 +106,9 @@ export default class WebMunch extends Module {
webpackJsonpv3(chunk_ids, modules) {
const names = chunk_ids.map(x => this._module_names[x] || x).join(', ');
this.log.debug(`Twitch Chunk Loaded: ${chunk_ids} (${names})`);
this.log.debug(`Modules: ${Object.keys(modules)}`);
const names = chunk_ids.map(x => this._chunk_names[x] || x).join(', ');
this.log.verbose(`Twitch Chunk Loaded: ${chunk_ids} (${names})`);
this.log.verbose(`Modules: ${Object.keys(modules)}`);
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)
if ( has(modules, mod_id) ) {
this._known_ids.add(mod_id);
const original_module = modules[mod_id];
modules[mod_id] = function(module, exports, require, ...args) {
if ( ! t._require && typeof require === 'function' ) {
@ -127,15 +150,15 @@ export default class WebMunch extends Module {
webpackJsonpv4(data) {
const chunk_ids = data[0],
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.debug(`Modules: ${Object.keys(modules)}`);
this.log.verbose(`Twitch Chunk Loaded: ${chunk_ids} (${names})`);
this.log.verbose(`Modules: ${Object.keys(modules)}`);
if ( 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);
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) {
if ( typeof key === 'function' ) {
predicate = key;
@ -176,7 +247,7 @@ export default class WebMunch extends Module {
return this._mod_cache[key];
const require = this._require;
if ( ! require || ! require.c )
if ( ! require )
return null;
if ( ! predicate )
@ -185,17 +256,147 @@ export default class WebMunch extends Module {
if ( ! predicate )
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) ) {
checked++;
const module = require.c[k],
mod = module && module.exports;
if ( mod && predicate(mod) ) {
if ( key )
this._mod_cache[key] = mod;
return 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 )
this._mod_cache[key] = out;
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;
}
@ -256,10 +457,38 @@ export default class WebMunch extends Module {
try {
modules = JSON.parse(data[1].replace(/(\d+):/g, '"$1":'))
} 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 ) {
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().`)
} else
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) ) {
if ( prop && prop !== 'false' )
el.setAttribute(key, prop);
console.log('bool-attr', key, prop);
} else if ( lk.startsWith('aria-') || ATTRS.includes(lk) )
el.setAttribute(key, prop);

View file

@ -124,6 +124,10 @@ export class EventEmitter {
return list ? Array.from(list) : [];
}
hasListeners(event) {
return !! this.__listeners[event]
}
emitUnsafe(event, ...args) {
let list = this.__listeners[event];
if ( ! list )
@ -446,6 +450,7 @@ export class HierarchicalEventEmitter extends EventEmitter {
waitFor(event) { return super.waitFor(this.abs_path(event)) }
off(event, fn, ctx) { return super.off(this.abs_path(event), fn, ctx) }
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) }
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 {
constructor(parent, name, level, raven) {
this.root = parent ? parent.root : this;
@ -21,7 +37,7 @@ export class Logger {
this.init = false;
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.children = {};
@ -34,6 +50,10 @@ export class Logger {
return this.children[name];
}
verbose(...args) {
return this.invoke(Logger.VERBOSE, args);
}
debug(...args) {
return this.invoke(Logger.DEBUG, args);
}
@ -79,26 +99,28 @@ export class Logger {
const message = Array.prototype.slice.call(args);
if ( this.root.init )
this.root.captured_init.push({
time: Date.now(),
category: this.name,
if ( level !== Logger.VERBOSE ) {
if ( this.root.init )
this.root.captured_init.push({
time: Date.now(),
category: this.name,
message: message.join(' '),
level: RAVEN_LEVELS[level] || level
});
this.crumb({
message: message.join(' '),
category: this.name,
level: RAVEN_LEVELS[level] || level
});
this.crumb({
message: message.join(' '),
category: this.name,
level: RAVEN_LEVELS[level] || level
});
}
if ( this.name )
message.unshift(`%c${this.root.label} [%c${this.name}%c]:%c`, 'color:#755000; font-weight:bold', '', 'color:#755000; font-weight:bold', '');
else
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);
else if ( level === Logger.INFO )
@ -115,9 +137,7 @@ export class Logger {
}
}
Logger.DEFAULT_LEVEL = 2;
Logger.VERBOSE = 0;
Logger.DEBUG = 1;
Logger.INFO = 2;
Logger.WARN = 4;
@ -125,4 +145,6 @@ Logger.WARNING = 4;
Logger.ERROR = 8;
Logger.OFF = 99;
Logger.DEFAULT_LEVEL = Logger.INFO;
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.parent = parent;
this.container = this.options.container || this.parent;
this.cls = cls;
if ( this.check_modifiers )
@ -134,6 +135,7 @@ export class Tooltip {
this.elements = null;
this._onMouseOut = this._onMouseOver = null;
this.container = null;
this.parent = null;
}
@ -367,8 +369,9 @@ export class Tooltip {
tip._update = () => {
if ( tip.popper ) {
tip.popper.destroy();
tip.popper = new Popper(popper_target, el, pop_opts);
tip.popper.update();
/*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.
tip.popper = new Popper(popper_target, el, pop_opts);
this.parent.appendChild(el);
this.container.appendChild(el);
tip.visible = true;
if ( opts.onShow )

View file

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