mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-06-27 12:55:55 +00:00
4.75.0
* Added: Setting to use native styling for subscription notices in chat. (Closes #1551) * Added: Setting to hide the stories UI in the side navigation. (Closes #1531) * Added: Setting to hide specific users from the directory and sidebar. * Changed: Hide the top navigation's search field when using minimal navigation for a cleaner look. (Closes #1556) * Changed: More internal changes for the support to Manifest v3. * Fixed: Uploading logs from the FFZ Control Center not working. * API Changed: Removed support for `no_sanitize` from `setChildren`. I don't think anything was currently using this, but going forward it is unsupported. We want to avoid using `innerHTML` as much as possible to simplify the approval process.
This commit is contained in:
parent
533bf52c9e
commit
1ee737f2ca
17 changed files with 669 additions and 280 deletions
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "frankerfacez",
|
||||
"author": "Dan Salvato LLC",
|
||||
"version": "4.74.1",
|
||||
"version": "4.75.0",
|
||||
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
|
@ -11,7 +11,9 @@
|
|||
"clean": "rimraf dist",
|
||||
"dev": "cross-env NODE_ENV=development webpack serve",
|
||||
"dev:prod": "cross-env NODE_ENV=production webpack serve",
|
||||
"dev:ext": "cross-env NODE_ENV=development FFZ_EXTENSION=true webpack serve",
|
||||
"build": "pnpm build:prod",
|
||||
"build:ext": "cross-env NODE_ENV=production FFZ_EXTENSION=true webpack build",
|
||||
"build:stats": "cross-env NODE_ENV=production webpack build --json > stats.json",
|
||||
"build:prod": "cross-env NODE_ENV=production webpack build",
|
||||
"build:dev": "cross-env NODE_ENV=development webpack build",
|
||||
|
@ -25,6 +27,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@ffz/fontello-cli": "^1.0.4",
|
||||
"@types/chrome": "^0.0.277",
|
||||
"@types/crypto-js": "^4.2.1",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/safe-regex": "^1.1.6",
|
||||
|
|
24
pnpm-lock.yaml
generated
24
pnpm-lock.yaml
generated
|
@ -97,6 +97,9 @@ devDependencies:
|
|||
'@ffz/fontello-cli':
|
||||
specifier: ^1.0.4
|
||||
version: 1.0.4
|
||||
'@types/chrome':
|
||||
specifier: ^0.0.277
|
||||
version: 0.0.277
|
||||
'@types/crypto-js':
|
||||
specifier: ^4.2.1
|
||||
version: 4.2.1
|
||||
|
@ -588,6 +591,13 @@ packages:
|
|||
'@types/node': 20.5.7
|
||||
dev: true
|
||||
|
||||
/@types/chrome@0.0.277:
|
||||
resolution: {integrity: sha512-qoTgBcDWblSsX+jvFnpUlLUE3LAuOhZfBh9MyMWMQHDsQiYVgBvdZWu9COrdB9+aNnInEyXcFgfc2HE16sdSYQ==}
|
||||
dependencies:
|
||||
'@types/filesystem': 0.0.36
|
||||
'@types/har-format': 1.2.16
|
||||
dev: true
|
||||
|
||||
/@types/connect-history-api-fallback@1.5.0:
|
||||
resolution: {integrity: sha512-4x5FkPpLipqwthjPsF7ZRbOv3uoLUFkTA9G9v583qi4pACvq0uTELrB8OLUzPWUI4IJIyvM85vzkV1nyiI2Lig==}
|
||||
dependencies:
|
||||
|
@ -641,6 +651,20 @@ packages:
|
|||
'@types/serve-static': 1.15.2
|
||||
dev: true
|
||||
|
||||
/@types/filesystem@0.0.36:
|
||||
resolution: {integrity: sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==}
|
||||
dependencies:
|
||||
'@types/filewriter': 0.0.33
|
||||
dev: true
|
||||
|
||||
/@types/filewriter@0.0.33:
|
||||
resolution: {integrity: sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==}
|
||||
dev: true
|
||||
|
||||
/@types/har-format@1.2.16:
|
||||
resolution: {integrity: sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==}
|
||||
dev: true
|
||||
|
||||
/@types/http-errors@2.0.1:
|
||||
resolution: {integrity: sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==}
|
||||
dev: true
|
||||
|
|
|
@ -1,16 +1,52 @@
|
|||
/* eslint strict: off */
|
||||
'use strict';
|
||||
(() => {
|
||||
// Don't run on certain sub-domains.
|
||||
if ( /^(?:localhost\.rig|blog|im|chatdepot|tmi|api|brand|dev|gql|passport)\./.test(location.hostname) )
|
||||
const browser = globalThis.browser ?? globalThis.chrome;
|
||||
|
||||
if (
|
||||
// Don't run on certain sub-domains.
|
||||
/^(?:localhost\.rig|blog|im|chatdepot|tmi|api|brand|dev|gql|passport)\./.test(location.hostname)
|
||||
||
|
||||
// Don't run on pages that have disabled FFZ.
|
||||
/disable_frankerfacez/.test(location.search)
|
||||
) {
|
||||
// Tell the service worker we aren't injecting.
|
||||
browser.runtime.sendMessage({
|
||||
type: 'ffz_not_supported'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if ( /disable_frankerfacez/.test(location.search) )
|
||||
return;
|
||||
// Make sure to wake the service worker up early.
|
||||
browser.runtime.sendMessage({
|
||||
type: 'ffz_injecting'
|
||||
});
|
||||
|
||||
const browser = globalThis.browser ?? globalThis.chrome,
|
||||
// Set up the extension message bridge.
|
||||
window.addEventListener('message', evt => {
|
||||
if (evt.source !== window)
|
||||
return;
|
||||
|
||||
HOST = location.hostname,
|
||||
if (evt.data && evt.data.type === 'ffz_to_ext')
|
||||
browser.runtime.sendMessage(evt.data.data, resp => {
|
||||
if (resp)
|
||||
window.postMessage({
|
||||
type: 'ffz_from_ext',
|
||||
data: resp
|
||||
}, '*');
|
||||
});
|
||||
});
|
||||
|
||||
browser.runtime.onMessage.addListener((msg, sender) => {
|
||||
window.postMessage({
|
||||
type: 'ffz_from_ext',
|
||||
data: msg
|
||||
}, '*');
|
||||
return true;
|
||||
});
|
||||
|
||||
// Now, inject our script into the page context.
|
||||
const HOST = location.hostname,
|
||||
SERVER = browser.runtime.getURL("web"),
|
||||
script = document.createElement('script');
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ class FFZESBridge {
|
|||
|
||||
document.addEventListener('readystatechange', event => {
|
||||
if ( document.documentElement )
|
||||
document.documentElement.dataset.ffzEsbridge = true;
|
||||
document.documentElement.dataset.ffzEsbridge = true;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -93,7 +93,7 @@ export default {
|
|||
return;
|
||||
|
||||
this.uploading = true;
|
||||
const response = await fetch('https://putco.de', {
|
||||
const response = await fetch('https://logs.frankerfacez.com', {
|
||||
method: 'PUT',
|
||||
body: this.text
|
||||
});
|
||||
|
@ -120,4 +120,4 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
@ -255,11 +255,11 @@ export default class MainMenu extends Module {
|
|||
// If we're on a page with minimal root, we want to open settings
|
||||
// in a popout as we're almost certainly within Popout Chat.
|
||||
const layout = this.resolve('site.layout'),
|
||||
item = evt.item,
|
||||
event = evt.event;
|
||||
item = evt?.item,
|
||||
event = evt?.event;
|
||||
|
||||
if ( (layout && layout.is_minimal) || (event && (event.ctrlKey || event.shiftKey)) ) {
|
||||
if ( ! this.openPopout(item) )
|
||||
if ( ! this.openPopout(item) && evt )
|
||||
evt.errored = true;
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -245,7 +245,10 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
|||
window.addEventListener('message', event => {
|
||||
const type = event.data?.ffz_type;
|
||||
|
||||
if ( type === 'request-context' && event.source ) {
|
||||
if ( type === 'open-settings' )
|
||||
this.emit('main_menu:open');
|
||||
|
||||
else if ( type === 'request-context' && event.source ) {
|
||||
this._context_proxies.add(event.source);
|
||||
this._updateContextProxies(event.source);
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import {EventEmitter} from 'utilities/events';
|
|||
import {TicketLock, has, once} from 'utilities/object';
|
||||
import type SettingsManager from '.';
|
||||
import type { OptionalArray, OptionalPromise, ProviderTypeMap } from '../utilities/types';
|
||||
import { EXTENSION } from '../utilities/constants';
|
||||
|
||||
const DB_VERSION = 1,
|
||||
NOT_WWW_TWITCH = window.location.host !== 'www.twitch.tv',
|
||||
|
@ -144,6 +145,255 @@ export abstract class AdvancedSettingsProvider extends SettingsProvider {
|
|||
}
|
||||
|
||||
|
||||
export abstract class RemoteSettingsProvider extends AdvancedSettingsProvider {
|
||||
|
||||
// State and Storage
|
||||
private _start_time: number;
|
||||
private _cached: Map<string, any>;
|
||||
|
||||
private _blobs: boolean | null;
|
||||
private _rpc: Map<number, [(input: any) => void, () => void]>;
|
||||
private _last_id: number;
|
||||
|
||||
private resolved_ready: boolean;
|
||||
private _ready_wait_resolve?: (() => void) | null;
|
||||
private _ready_wait_fail?: ((err: any) => void) | null;
|
||||
private _ready_wait?: Promise<void> | null;
|
||||
|
||||
constructor(manager: SettingsManager) {
|
||||
super(manager);
|
||||
|
||||
this._start_time = performance.now();
|
||||
|
||||
this._rpc = new Map;
|
||||
|
||||
this._cached = new Map;
|
||||
this.resolved_ready = false;
|
||||
this.ready = false;
|
||||
this._ready_wait = null;
|
||||
|
||||
this._blobs = null;
|
||||
this._last_id = 0;
|
||||
}
|
||||
|
||||
get supportsBlobs() {
|
||||
return this._blobs ?? false;
|
||||
}
|
||||
|
||||
// Stuff
|
||||
|
||||
broadcastTransfer() {
|
||||
// TODO: Figure out what this would mean for CORS.
|
||||
}
|
||||
|
||||
disableEvents() {
|
||||
// TODO: Figure out what this would mean for CORS.
|
||||
}
|
||||
|
||||
|
||||
// Initialization
|
||||
|
||||
protected resolveReady(success: boolean, data?: any) {
|
||||
if ( this.manager )
|
||||
this.manager.log.info(`${this.constructor.name} ready in ${(performance.now() - this._start_time).toFixed(5)}ms`);
|
||||
|
||||
this.resolved_ready = true;
|
||||
this.ready = success;
|
||||
|
||||
if ( success && this._ready_wait_resolve )
|
||||
this._ready_wait_resolve();
|
||||
else if ( ! success && this._ready_wait_fail )
|
||||
this._ready_wait_fail(data);
|
||||
}
|
||||
|
||||
awaitReady() {
|
||||
if ( this.resolved_ready ) {
|
||||
if ( this.ready )
|
||||
return Promise.resolve();
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
if ( this._ready_wait )
|
||||
return this._ready_wait;
|
||||
|
||||
return this._ready_wait = new Promise<void>((resolve, fail) => {
|
||||
this._ready_wait_resolve = resolve;
|
||||
this._ready_wait_fail = fail;
|
||||
|
||||
}).finally(() => {
|
||||
this._ready_wait = null;
|
||||
this._ready_wait_resolve = null;
|
||||
this._ready_wait_fail = null;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Provider Methods
|
||||
|
||||
get<T>(key: string, default_value?: T): T {
|
||||
return this._cached.has(key)
|
||||
? this._cached.get(key)
|
||||
: default_value;
|
||||
}
|
||||
|
||||
set(key: string, value: any) {
|
||||
if ( value === undefined ) {
|
||||
if ( this.has(key) )
|
||||
this.delete(key);
|
||||
return;
|
||||
}
|
||||
|
||||
this._cached.set(key, value);
|
||||
this.rpc({ffz_type: 'set', key, value})
|
||||
.catch(err => this.manager.log.error('Error setting value', err));
|
||||
this.emit('set', key, value, false);
|
||||
}
|
||||
|
||||
delete(key: string) {
|
||||
this._cached.delete(key);
|
||||
this.rpc({ffz_type: 'delete', key})
|
||||
.catch(err => this.manager.log.error('Error deleting value', err));
|
||||
this.emit('set', key, undefined, true);
|
||||
}
|
||||
|
||||
clear() {
|
||||
const old_cache = this._cached;
|
||||
this._cached = new Map;
|
||||
for(const key of old_cache.keys())
|
||||
this.emit('changed', key, undefined, true);
|
||||
|
||||
this.rpc('clear')
|
||||
.catch(err => this.manager.log.error('Error clearing storage', err));
|
||||
}
|
||||
|
||||
has(key: string) { return this._cached.has(key); }
|
||||
keys() { return this._cached.keys(); }
|
||||
entries() { return this._cached.entries(); }
|
||||
get size() { return this._cached.size; }
|
||||
|
||||
async flush() {
|
||||
await this.rpc('flush');
|
||||
}
|
||||
|
||||
|
||||
// Provider Methods: Blobs
|
||||
|
||||
async getBlob(key: string) {
|
||||
const msg = await this.rpc({ffz_type: 'get-blob', key});
|
||||
return msg ? deserializeBlob(msg) : null;
|
||||
}
|
||||
|
||||
async setBlob(key: string, value: BlobLike) {
|
||||
await this.rpc({
|
||||
ffz_type: 'set-blob',
|
||||
key,
|
||||
value: await serializeBlob(value)
|
||||
});
|
||||
}
|
||||
|
||||
async deleteBlob(key: string) {
|
||||
await this.rpc({
|
||||
ffz_type: 'delete-blob',
|
||||
key
|
||||
});
|
||||
}
|
||||
|
||||
async hasBlob(key: string) {
|
||||
return this.rpc({ffz_type: 'has-blob', key});
|
||||
}
|
||||
|
||||
async clearBlobs() {
|
||||
await this.rpc('clear-blobs');
|
||||
}
|
||||
|
||||
async blobKeys() {
|
||||
return this.rpc('blob-keys');
|
||||
}
|
||||
|
||||
|
||||
// Communication
|
||||
|
||||
abstract send(msg: string | CorsMessage, transfer?: OptionalArray<Transferable>): void;
|
||||
|
||||
rpc<K extends keyof CorsRpcTypes>(
|
||||
msg: K | RPCInputMessage<K>,
|
||||
transfer?: OptionalArray<Transferable>
|
||||
) {
|
||||
const id = ++this._last_id;
|
||||
|
||||
return new Promise<CorsOutput<K>>((resolve,fail) => {
|
||||
this._rpc.set(id, [resolve, fail]);
|
||||
let out: CorsMessage;
|
||||
if ( typeof msg === 'string' )
|
||||
out = {ffz_type: msg} as CorsMessage;
|
||||
else
|
||||
out = msg as unknown as CorsMessage;
|
||||
|
||||
out.id = id;
|
||||
this.send(out, transfer);
|
||||
});
|
||||
}
|
||||
|
||||
handleMessage(msg: CorsMessage) {
|
||||
if ( msg.ffz_type === 'ready' )
|
||||
this.rpc('init-load').then(msg => {
|
||||
this._blobs = msg.blobs;
|
||||
for(const [key, value] of Object.entries(msg.values))
|
||||
this._cached.set(key, value);
|
||||
|
||||
this.resolveReady(true);
|
||||
|
||||
}).catch(err => {
|
||||
this.resolveReady(false, err);
|
||||
});
|
||||
|
||||
else if ( msg.ffz_type === 'change' )
|
||||
this.onChange(msg);
|
||||
|
||||
else if ( msg.ffz_type === 'change-blob' )
|
||||
this.emit('changed-blob', msg.key, msg.deleted);
|
||||
|
||||
else if ( msg.ffz_type === 'clear-blobs' )
|
||||
this.emit('clear-blobs');
|
||||
|
||||
else if ( msg.ffz_type === 'reply' || msg.ffz_type === 'reply-error' )
|
||||
this.onReply(msg);
|
||||
|
||||
else
|
||||
this.manager.log.warn('Unknown Message', msg.ffz_type, msg);
|
||||
}
|
||||
|
||||
onChange(msg: RPCInputMessage<'change'>) {
|
||||
const key = msg.key,
|
||||
value = msg.value,
|
||||
deleted = msg.deleted;
|
||||
|
||||
if ( deleted ) {
|
||||
this._cached.delete(key);
|
||||
this.emit('changed', key, undefined, true);
|
||||
} else {
|
||||
this._cached.set(key, value);
|
||||
this.emit('changed', key, value, false);
|
||||
}
|
||||
}
|
||||
|
||||
onReply(msg: CorsReplyMessage | CorsReplyErrorMessage) {
|
||||
const id = msg.id,
|
||||
success = msg.ffz_type === 'reply',
|
||||
cbs = this._rpc.get(id);
|
||||
if ( ! cbs )
|
||||
return this.manager.log.warn('Received reply for unknown ID', id);
|
||||
|
||||
this._rpc.delete(id);
|
||||
if ( success )
|
||||
cbs[0](msg.reply);
|
||||
else
|
||||
cbs[1]();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ============================================================================
|
||||
|
@ -1177,7 +1427,7 @@ export class IndexedDBProvider extends AdvancedSettingsProvider {
|
|||
// CrossOriginStorageBridge
|
||||
// ============================================================================
|
||||
|
||||
export class CrossOriginStorageBridge extends AdvancedSettingsProvider {
|
||||
export class CrossOriginStorageBridge extends RemoteSettingsProvider {
|
||||
|
||||
// Static Stuff
|
||||
|
||||
|
@ -1194,36 +1444,11 @@ export class CrossOriginStorageBridge extends AdvancedSettingsProvider {
|
|||
static shouldUpdate = false;
|
||||
|
||||
// State and Storage
|
||||
private _start_time: number;
|
||||
private _cached: Map<string, any>;
|
||||
|
||||
private _blobs: boolean | null;
|
||||
private _rpc: Map<number, [(input: any) => void, () => void]>;
|
||||
private _last_id: number;
|
||||
|
||||
private frame: HTMLIFrameElement | null;
|
||||
private _boundHandleMessage?: ((event: MessageEvent) => void) | null;
|
||||
|
||||
private resolved_ready: boolean;
|
||||
private _ready_wait_resolve?: (() => void) | null;
|
||||
private _ready_wait_fail?: ((err: any) => void) | null;
|
||||
private _ready_wait?: Promise<void> | null;
|
||||
|
||||
constructor(manager: SettingsManager) {
|
||||
super(manager);
|
||||
|
||||
this._start_time = performance.now();
|
||||
|
||||
this._rpc = new Map;
|
||||
|
||||
this._cached = new Map;
|
||||
this.resolved_ready = false;
|
||||
this.ready = false;
|
||||
this._ready_wait = null;
|
||||
|
||||
this._blobs = null;
|
||||
this._last_id = 0;
|
||||
|
||||
const frame = this.frame = document.createElement('iframe');
|
||||
frame.src = (this.manager.root as any).host === 'youtube' ?
|
||||
'//www.youtube.com/__ffz_bridge/' :
|
||||
|
@ -1232,13 +1457,10 @@ export class CrossOriginStorageBridge extends AdvancedSettingsProvider {
|
|||
frame.style.width = '0';
|
||||
frame.style.height = '0';
|
||||
|
||||
this._boundHandleMessage = this.onMessage.bind(this);
|
||||
window.addEventListener('message', this._boundHandleMessage);
|
||||
document.body.appendChild(frame);
|
||||
}
|
||||
this.onMessage = this.onMessage.bind(this);
|
||||
|
||||
get supportsBlobs() {
|
||||
return this._blobs ?? false;
|
||||
window.addEventListener('message', this.onMessage);
|
||||
document.body.appendChild(frame);
|
||||
}
|
||||
|
||||
// Stuff
|
||||
|
@ -1252,128 +1474,16 @@ export class CrossOriginStorageBridge extends AdvancedSettingsProvider {
|
|||
}
|
||||
|
||||
|
||||
// Initialization
|
||||
|
||||
private _resolveReady(success: boolean, data?: any) {
|
||||
if ( this.manager )
|
||||
this.manager.log.info(`COSB ready in ${(performance.now() - this._start_time).toFixed(5)}ms`);
|
||||
|
||||
this.resolved_ready = true;
|
||||
this.ready = success;
|
||||
|
||||
if ( success && this._ready_wait_resolve )
|
||||
this._ready_wait_resolve();
|
||||
else if ( ! success && this._ready_wait_fail )
|
||||
this._ready_wait_fail(data);
|
||||
}
|
||||
|
||||
awaitReady() {
|
||||
if ( this.resolved_ready ) {
|
||||
if ( this.ready )
|
||||
return Promise.resolve();
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
if ( this._ready_wait )
|
||||
return this._ready_wait;
|
||||
|
||||
return this._ready_wait = new Promise<void>((resolve, fail) => {
|
||||
this._ready_wait_resolve = resolve;
|
||||
this._ready_wait_fail = fail;
|
||||
|
||||
}).finally(() => {
|
||||
this._ready_wait = null;
|
||||
this._ready_wait_resolve = null;
|
||||
this._ready_wait_fail = null;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Provider Methods
|
||||
|
||||
get<T>(key: string, default_value?: T): T {
|
||||
return this._cached.has(key)
|
||||
? this._cached.get(key)
|
||||
: default_value;
|
||||
}
|
||||
|
||||
set(key: string, value: any) {
|
||||
if ( value === undefined ) {
|
||||
if ( this.has(key) )
|
||||
this.delete(key);
|
||||
return;
|
||||
}
|
||||
|
||||
this._cached.set(key, value);
|
||||
this.rpc({ffz_type: 'set', key, value})
|
||||
.catch(err => this.manager.log.error('Error setting value', err));
|
||||
this.emit('set', key, value, false);
|
||||
}
|
||||
|
||||
delete(key: string) {
|
||||
this._cached.delete(key);
|
||||
this.rpc({ffz_type: 'delete', key})
|
||||
.catch(err => this.manager.log.error('Error deleting value', err));
|
||||
this.emit('set', key, undefined, true);
|
||||
}
|
||||
|
||||
clear() {
|
||||
const old_cache = this._cached;
|
||||
this._cached = new Map;
|
||||
for(const key of old_cache.keys())
|
||||
this.emit('changed', key, undefined, true);
|
||||
|
||||
this.rpc('clear')
|
||||
.catch(err => this.manager.log.error('Error clearing storage', err));
|
||||
}
|
||||
|
||||
has(key: string) { return this._cached.has(key); }
|
||||
keys() { return this._cached.keys(); }
|
||||
entries() { return this._cached.entries(); }
|
||||
get size() { return this._cached.size; }
|
||||
|
||||
async flush() {
|
||||
await this.rpc('flush');
|
||||
}
|
||||
|
||||
|
||||
// Provider Methods: Blobs
|
||||
|
||||
async getBlob(key: string) {
|
||||
const msg = await this.rpc({ffz_type: 'get-blob', key});
|
||||
return msg ? deserializeBlob(msg) : null;
|
||||
}
|
||||
|
||||
async setBlob(key: string, value: BlobLike) {
|
||||
await this.rpc({
|
||||
ffz_type: 'set-blob',
|
||||
key,
|
||||
value: await serializeBlob(value)
|
||||
});
|
||||
}
|
||||
|
||||
async deleteBlob(key: string) {
|
||||
await this.rpc({
|
||||
ffz_type: 'delete-blob',
|
||||
key
|
||||
});
|
||||
}
|
||||
|
||||
async hasBlob(key: string) {
|
||||
return this.rpc({ffz_type: 'has-blob', key});
|
||||
}
|
||||
|
||||
async clearBlobs() {
|
||||
await this.rpc('clear-blobs');
|
||||
}
|
||||
|
||||
async blobKeys() {
|
||||
return this.rpc('blob-keys');
|
||||
}
|
||||
|
||||
|
||||
// CORS Communication
|
||||
|
||||
onMessage(event: MessageEvent) {
|
||||
const msg = event.data;
|
||||
if ( ! msg || ! msg.ffz_type )
|
||||
return;
|
||||
|
||||
this.handleMessage(msg);
|
||||
}
|
||||
|
||||
send(msg: string | CorsMessage, transfer?: OptionalArray<Transferable>) {
|
||||
if ( typeof msg === 'string' )
|
||||
msg = {ffz_type: msg} as any;
|
||||
|
@ -1390,90 +1500,138 @@ export class CrossOriginStorageBridge extends AdvancedSettingsProvider {
|
|||
}
|
||||
}
|
||||
|
||||
rpc<K extends keyof CorsRpcTypes>(
|
||||
msg: K | RPCInputMessage<K>,
|
||||
transfer?: OptionalArray<Transferable>
|
||||
) {
|
||||
const id = ++this._last_id;
|
||||
|
||||
return new Promise<CorsOutput<K>>((resolve,fail) => {
|
||||
this._rpc.set(id, [resolve, fail]);
|
||||
let out: CorsMessage;
|
||||
if ( typeof msg === 'string' )
|
||||
out = {ffz_type: msg} as CorsMessage;
|
||||
else
|
||||
out = msg as unknown as CorsMessage;
|
||||
|
||||
out.id = id;
|
||||
this.send(out, transfer);
|
||||
});
|
||||
}
|
||||
|
||||
onMessage(event: MessageEvent) {
|
||||
const msg = event.data;
|
||||
if ( ! msg || ! msg.ffz_type )
|
||||
return;
|
||||
|
||||
if ( msg.ffz_type === 'ready' )
|
||||
this.rpc('init-load').then(msg => {
|
||||
this._blobs = msg.blobs;
|
||||
for(const [key, value] of Object.entries(msg.values))
|
||||
this._cached.set(key, value);
|
||||
|
||||
this._resolveReady(true);
|
||||
|
||||
}).catch(err => {
|
||||
this._resolveReady(false, err);
|
||||
});
|
||||
|
||||
else if ( msg.ffz_type === 'change' )
|
||||
this.onChange(msg);
|
||||
|
||||
else if ( msg.ffz_type === 'change-blob' )
|
||||
this.emit('changed-blob', msg.key, msg.deleted);
|
||||
|
||||
else if ( msg.ffz_type === 'clear-blobs' )
|
||||
this.emit('clear-blobs');
|
||||
|
||||
else if ( msg.ffz_type === 'reply' || msg.ffz_type === 'reply-error' )
|
||||
this.onReply(msg);
|
||||
|
||||
else
|
||||
this.manager.log.warn('Unknown Message', msg.ffz_type, msg);
|
||||
}
|
||||
|
||||
onChange(msg: RPCInputMessage<'change'>) {
|
||||
const key = msg.key,
|
||||
value = msg.value,
|
||||
deleted = msg.deleted;
|
||||
|
||||
if ( deleted ) {
|
||||
this._cached.delete(key);
|
||||
this.emit('changed', key, undefined, true);
|
||||
} else {
|
||||
this._cached.set(key, value);
|
||||
this.emit('changed', key, value, false);
|
||||
}
|
||||
}
|
||||
|
||||
onReply(msg: CorsReplyMessage | CorsReplyErrorMessage) {
|
||||
const id = msg.id,
|
||||
success = msg.ffz_type === 'reply',
|
||||
cbs = this._rpc.get(id);
|
||||
if ( ! cbs )
|
||||
return this.manager.log.warn('Received reply for unknown ID', id);
|
||||
|
||||
this._rpc.delete(id);
|
||||
if ( success )
|
||||
cbs[0](msg.reply);
|
||||
else
|
||||
cbs[1]();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// ExtensionProvider
|
||||
// ============================================================================
|
||||
|
||||
export class ExtensionProvider extends RemoteSettingsProvider {
|
||||
|
||||
// Static Stuff
|
||||
|
||||
static supported() { return EXTENSION }
|
||||
|
||||
static hasContent() {
|
||||
if ( ! ExtensionProvider.supported() )
|
||||
return false;
|
||||
|
||||
// We need a promise since we need to message the extension and
|
||||
// request to know if it has keys or not.
|
||||
return new Promise<boolean>((resolve) => {
|
||||
let responded = false,
|
||||
timeout: ReturnType<typeof setTimeout> | null = null ;
|
||||
|
||||
const listener = (evt: MessageEvent<any>) => {
|
||||
if (evt.source !== window)
|
||||
return;
|
||||
|
||||
if (evt.data && evt.data.type === 'ffz_from_ext') {
|
||||
const msg = evt.data.data,
|
||||
type = msg?.ffz_type;
|
||||
|
||||
if (type === 'has-keys') {
|
||||
responded = true;
|
||||
resolve(msg.value);
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
if (!responded) {
|
||||
responded = true;
|
||||
resolve(false);
|
||||
}
|
||||
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
}
|
||||
|
||||
window.removeEventListener('message', listener);
|
||||
}
|
||||
|
||||
window.addEventListener('message', listener);
|
||||
|
||||
window.postMessage({
|
||||
type: 'ffz_to_ext',
|
||||
data: {
|
||||
ffz_type: 'check-has-keys'
|
||||
}
|
||||
}, '*');
|
||||
|
||||
timeout = setTimeout(cleanup, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
static priority = 101;
|
||||
static title = 'Browser Extension Storage';
|
||||
static description = 'This provider uses a browser extension service worker to store settings in a location that should not suffer from issues due to storage partitioning or cache clearing.';
|
||||
|
||||
static allowTransfer = true;
|
||||
static shouldUpdate = true;
|
||||
|
||||
// State and Storage
|
||||
|
||||
constructor(manager: SettingsManager) {
|
||||
super(manager);
|
||||
|
||||
this.onExtMessage = this.onExtMessage.bind(this);
|
||||
window.addEventListener('message', this.onExtMessage);
|
||||
}
|
||||
|
||||
// Stuff
|
||||
|
||||
broadcastTransfer() {
|
||||
|
||||
}
|
||||
|
||||
disableEvents() {
|
||||
|
||||
}
|
||||
|
||||
// Communication
|
||||
|
||||
onExtMessage(evt: MessageEvent<any>) {
|
||||
if (evt.source !== window)
|
||||
return;
|
||||
|
||||
if (evt.data?.type === 'ffz_from_ext' && evt.data.data?.ffz_type)
|
||||
this.handleMessage(evt.data.data);
|
||||
}
|
||||
|
||||
send(msg: string | CorsMessage, transfer?: OptionalArray<Transferable>) {
|
||||
if ( typeof msg === 'string' )
|
||||
msg = {ffz_type: msg} as any;
|
||||
|
||||
try {
|
||||
window.postMessage(
|
||||
{
|
||||
type: 'ffz_to_ext',
|
||||
data: msg
|
||||
},
|
||||
'*',
|
||||
transfer ? (Array.isArray(transfer) ? transfer : [transfer]) : undefined
|
||||
);
|
||||
|
||||
} catch(err) {
|
||||
this.manager.log.error('Error sending message to extension.', err, msg, transfer);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
type CorsRpcTypes = {
|
||||
|
||||
'ready': {
|
||||
input: void;
|
||||
output: void;
|
||||
};
|
||||
|
||||
'load': {
|
||||
input: void;
|
||||
output: Record<string, any>;
|
||||
|
@ -1531,6 +1689,14 @@ type CorsRpcTypes = {
|
|||
output: void;
|
||||
};
|
||||
|
||||
'change-blob': {
|
||||
input: {
|
||||
key: string;
|
||||
deleted: boolean;
|
||||
};
|
||||
output: void;
|
||||
}
|
||||
|
||||
'delete-blob': {
|
||||
input: {
|
||||
key: string;
|
||||
|
@ -1595,6 +1761,7 @@ export const Providers: Record<string, typeof SettingsProvider> = {
|
|||
|
||||
local: LocalStorageProvider,
|
||||
idb: IndexedDBProvider,
|
||||
cosb: CrossOriginStorageBridge
|
||||
cosb: CrossOriginStorageBridge,
|
||||
//ext: ExtensionProvider
|
||||
|
||||
};
|
||||
|
|
|
@ -292,6 +292,15 @@ export default class ChatHook extends Module {
|
|||
|
||||
// Settings
|
||||
|
||||
this.settings.add('chat.subs.native', {
|
||||
default: false,
|
||||
ui: {
|
||||
path: 'Chat > Appearance >> Subscriptions',
|
||||
title: 'Display subscription notices using Twitch\'s native UI.',
|
||||
component: 'setting-check-box'
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('chat.filtering.show-reasons', {
|
||||
default: false,
|
||||
ui: {
|
||||
|
@ -1671,7 +1680,7 @@ export default class ChatHook extends Module {
|
|||
return;
|
||||
|
||||
if ( event.prefix === 'pinned-chat-updates-v1' ) {
|
||||
this.log.info('Pinned Chat', event);
|
||||
this.log.debug('Pinned Chat', event);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -2686,7 +2695,7 @@ export default class ChatHook extends Module {
|
|||
if ( t.chat.context.get('chat.filtering.blocked-types').has('Subscription') )
|
||||
return;
|
||||
|
||||
if ( t.disable_handling )
|
||||
if ( t.disable_handling || t.chat.context.get('chat.subs.native') )
|
||||
return old_sub.call(i, e);
|
||||
|
||||
if ( t.chat.context.get('chat.subs.show') < 3 )
|
||||
|
@ -2772,7 +2781,7 @@ export default class ChatHook extends Module {
|
|||
if ( t.chat.context.get('chat.filtering.blocked-types').has('Resubscription') )
|
||||
return;
|
||||
|
||||
if ( t.disable_handling )
|
||||
if ( t.disable_handling || t.chat.context.get('chat.subs.native') )
|
||||
return old_resub.call(i, e);
|
||||
|
||||
if ( t.chat.context.get('chat.subs.show') < 2 && ! e.body )
|
||||
|
@ -2811,7 +2820,7 @@ export default class ChatHook extends Module {
|
|||
if ( t.chat.context.get('chat.filtering.blocked-types').has('SubGift') )
|
||||
return;
|
||||
|
||||
if ( t.disable_handling )
|
||||
if ( t.disable_handling || t.chat.context.get('chat.subs.native') )
|
||||
return old_subgift.call(i, e);
|
||||
|
||||
const key = `${e.channel}:${e.user.userID}`,
|
||||
|
@ -2887,7 +2896,7 @@ export default class ChatHook extends Module {
|
|||
if ( t.chat.context.get('chat.filtering.blocked-types').has('AnonSubGift') )
|
||||
return;
|
||||
|
||||
if ( t.disable_handling )
|
||||
if ( t.disable_handling || t.chat.context.get('chat.subs.native') )
|
||||
return old_anonsubgift.call(i, e);
|
||||
|
||||
const key = `${e.channel}:ANON`,
|
||||
|
@ -2944,7 +2953,7 @@ export default class ChatHook extends Module {
|
|||
if ( t.chat.context.get('chat.filtering.blocked-types').has('SubMysteryGift') )
|
||||
return;
|
||||
|
||||
if ( t.disable_handling )
|
||||
if ( t.disable_handling || t.chat.context.get('chat.subs.native') )
|
||||
return old_submystery.call(i, e);
|
||||
|
||||
let mystery = null;
|
||||
|
@ -2983,7 +2992,7 @@ export default class ChatHook extends Module {
|
|||
if ( t.chat.context.get('chat.filtering.blocked-types').has('AnonSubMysteryGift') )
|
||||
return;
|
||||
|
||||
if ( t.disable_handling )
|
||||
if ( t.disable_handling || t.chat.context.get('chat.subs.native') )
|
||||
return old_anonsubmystery.call(i, e);
|
||||
|
||||
let mystery = null;
|
||||
|
|
|
@ -54,6 +54,8 @@ const CLASSES = {
|
|||
|
||||
'pinned-hype-chat': '.paid-pinned-chat-message-list',
|
||||
|
||||
'side-stories': '.side-nav__title + div[class*=storiesLeftNavSection]',
|
||||
|
||||
'ci-mod-view': '.chat-input__buttons-container a[href*="/moderator"]',
|
||||
'ci-highlight-settings': '.chat-input__buttons-container button[data-highlight-selector="chat-highlights-shortcut"]',
|
||||
'ci-shield-mode': '.chat-input__buttons-container > div:last-child button[class|="ScCoreButton"]:not([data-highlight-selector]):not([data-a-target])'
|
||||
|
@ -140,6 +142,15 @@ export default class CSSTweaks extends Module {
|
|||
}
|
||||
});
|
||||
|
||||
this.settings.add('layout.side-nav.hide-stories', {
|
||||
default: false,
|
||||
ui: {
|
||||
path: 'Appearance > Layout >> Side Navigation',
|
||||
title: 'Hide Stories',
|
||||
component: 'setting-check-box'
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('layout.side-nav.show', {
|
||||
default: 1,
|
||||
requires: ['layout.use-portrait'],
|
||||
|
@ -522,6 +533,7 @@ export default class CSSTweaks extends Module {
|
|||
this.toggleHide('celebration', ! this.settings.get('channel.show-celebrations'));
|
||||
|
||||
this.settings.getChanges('layout.subtember', val => this.toggleHide('subtember', !val));
|
||||
this.settings.getChanges('layout.side-nav.hide-stories', val => this.toggleHide('side-stories', val));
|
||||
|
||||
this.updateFont();
|
||||
this.updateTopNav();
|
||||
|
|
|
@ -10,17 +10,22 @@
|
|||
|
||||
transition: top ease-in-out 75ms, bottom ease-in-out 75ms;
|
||||
|
||||
.top-nav__search-container,
|
||||
.tw-svg__asset--logotwitch {
|
||||
visibility: hidden;
|
||||
transition: opacity ease-in-out 75ms;
|
||||
opacity: 0;
|
||||
//visibility: hidden;
|
||||
}
|
||||
|
||||
&:focus-within,
|
||||
&:hover {
|
||||
top: 0 !important;
|
||||
|
||||
.top-nav__search-container,
|
||||
.tw-svg__asset--logotwitch {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
//visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -211,6 +211,57 @@ export default class Directory extends Module {
|
|||
changed: () => this.DirectoryShelf.forceUpdate()
|
||||
});
|
||||
|
||||
|
||||
this.settings.add('directory.block-users', {
|
||||
default: [],
|
||||
type: 'array_merge',
|
||||
always_inherit: true,
|
||||
ui: {
|
||||
path: 'Directory > Channels >> Block by Username',
|
||||
component: 'basic-terms',
|
||||
words: false
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('__filter:directory.block-users', {
|
||||
requires: ['directory.block-users'],
|
||||
equals: 'requirements',
|
||||
process(ctx) {
|
||||
const val = ctx.get('directory.block-users');
|
||||
if ( ! val || ! val.length )
|
||||
return null;
|
||||
|
||||
const out = [[], []];
|
||||
|
||||
for(const item of val) {
|
||||
const t = item.t;
|
||||
let v = item.v;
|
||||
|
||||
if ( t === 'glob' )
|
||||
v = glob_to_regex(v);
|
||||
|
||||
else if ( t !== 'raw' )
|
||||
v = escape_regex(v);
|
||||
|
||||
if ( ! v || ! v.length )
|
||||
continue;
|
||||
|
||||
out[item.s ? 0 : 1].push(v);
|
||||
}
|
||||
|
||||
return [
|
||||
out[0].length
|
||||
? new RegExp(`^(?:${out[0].join('|')})$`)
|
||||
: null,
|
||||
out[1].length
|
||||
? new RegExp(`^(?:${out[1].join('|')})$`, 'i')
|
||||
: null
|
||||
];
|
||||
},
|
||||
|
||||
changed: () => this.updateCards()
|
||||
});
|
||||
|
||||
this.settings.add('directory.block-titles', {
|
||||
default: [],
|
||||
type: 'array_merge',
|
||||
|
@ -702,6 +753,19 @@ export default class Directory extends Module {
|
|||
}
|
||||
}
|
||||
|
||||
if ( ! should_hide ) {
|
||||
const regexes = this.settings.get('__filter:directory.block-users');
|
||||
if ( regexes ) {
|
||||
if ( regexes[0] )
|
||||
regexes[0].lastIndex = -1;
|
||||
if ( regexes[1] )
|
||||
regexes[1].lastIndex = -1;
|
||||
|
||||
if (( regexes[0] && regexes[0].test(props.channelLogin) ) || ( regexes[1] && regexes[1].test(props.channelLogin) ))
|
||||
should_hide = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! should_hide ) {
|
||||
const regexes = this.settings.get('__filter:directory.block-titles');
|
||||
if ( regexes ) {
|
||||
|
|
|
@ -383,15 +383,30 @@ export default class Layout extends Module {
|
|||
if ( props?.isPromoted && this.settings.get('directory.hide-promoted') )
|
||||
should_hide = true;
|
||||
else {
|
||||
const regexes = this.settings.get('__filter:directory.block-titles');
|
||||
const title = stream?.broadcaster?.broadcastSettings?.title;
|
||||
if ( regexes && title ) {
|
||||
if ( regexes[0] )
|
||||
regexes[0].lastIndex = -1;
|
||||
if ( regexes[1] )
|
||||
regexes[1].lastIndex = -1;
|
||||
if ( (regexes[0] && regexes[0].test(title)) || (regexes[1] && regexes[1].test(title)) )
|
||||
should_hide = true;
|
||||
if ( ! should_hide ) {
|
||||
const regexes = this.settings.get('__filter:directory.block-users');
|
||||
const login = props.userLogin;
|
||||
if ( regexes && login ) {
|
||||
if ( regexes[0] )
|
||||
regexes[0].lastIndex = -1;
|
||||
if ( regexes[1] )
|
||||
regexes[1].lastIndex = -1;
|
||||
if ( (regexes[0] && regexes[0].test(login)) || (regexes[1] && regexes[1].test(login)) )
|
||||
should_hide = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! should_hide) {
|
||||
const regexes = this.settings.get('__filter:directory.block-titles');
|
||||
const title = stream?.broadcaster?.broadcastSettings?.title;
|
||||
if ( regexes && title ) {
|
||||
if ( regexes[0] )
|
||||
regexes[0].lastIndex = -1;
|
||||
if ( regexes[1] )
|
||||
regexes[1].lastIndex = -1;
|
||||
if ( (regexes[0] && regexes[0].test(title)) || (regexes[1] && regexes[1].test(title)) )
|
||||
should_hide = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
|
||||
import {has} from 'utilities/object';
|
||||
import type { DomFragment, OptionalArray } from './types';
|
||||
import type { DomFragment } from './types';
|
||||
import { DEBUG } from './constants';
|
||||
|
||||
const ATTRS = [
|
||||
|
@ -242,6 +242,9 @@ export function setChildren(
|
|||
no_sanitize: boolean = false,
|
||||
no_empty: boolean = false
|
||||
) {
|
||||
if (no_sanitize)
|
||||
window.FrankerFaceZ.get().log.warn('call to setChildren with no_sanitize set to true -- this is no longer supported');
|
||||
|
||||
if (children instanceof Node ) {
|
||||
if (! no_empty )
|
||||
element.innerHTML = '';
|
||||
|
@ -260,15 +263,20 @@ export function setChildren(
|
|||
else if (child) {
|
||||
const val = typeof child === 'string' ? child : String(child);
|
||||
|
||||
element.appendChild(no_sanitize ?
|
||||
range.createContextualFragment(val) : document.createTextNode(val));
|
||||
// We no longer support no_sanitize
|
||||
//element.appendChild(no_sanitize ?
|
||||
// range.createContextualFragment(val) : document.createTextNode(val));
|
||||
|
||||
element.appendChild(document.createTextNode(val));
|
||||
}
|
||||
|
||||
} else if (children) {
|
||||
const val = typeof children === 'string' ? children : String(children);
|
||||
|
||||
element.appendChild(no_sanitize ?
|
||||
range.createContextualFragment(val) : document.createTextNode(val));
|
||||
// We no longer support no_sanitize
|
||||
//element.appendChild(no_sanitize ?
|
||||
// range.createContextualFragment(val) : document.createTextNode(val));
|
||||
element.appendChild(document.createTextNode(val));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -248,7 +248,14 @@ export interface ExperimentTypeMap {
|
|||
};
|
||||
|
||||
export interface ModuleEventMap {
|
||||
'main_menu': MainMenuEvents;
|
||||
};
|
||||
|
||||
export type MainMenuEvents = {
|
||||
':open': [event?: {
|
||||
item?: string;
|
||||
event?: MouseEvent;
|
||||
}]
|
||||
};
|
||||
|
||||
export interface ModuleMap {
|
||||
|
|
31
src/worker.ts
Normal file
31
src/worker.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
const browser = ((globalThis as any).browser ?? globalThis.chrome) as typeof globalThis.chrome;
|
||||
|
||||
browser.runtime.onInstalled.addListener(() => {
|
||||
browser.action.disable();
|
||||
});
|
||||
|
||||
browser.action.onClicked.addListener(tab => {
|
||||
if ( ! tab?.id )
|
||||
return;
|
||||
|
||||
browser.tabs.sendMessage(tab.id, {
|
||||
type: 'ffz_to_page',
|
||||
data: {
|
||||
ffz_type: 'open-settings'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
const type = message?.type;
|
||||
if ( ! type || ! sender?.tab?.id )
|
||||
return;
|
||||
|
||||
if ( type === 'ffz_not_supported' )
|
||||
browser.action.disable(sender.tab.id);
|
||||
|
||||
else if ( type === 'ffz_injecting' )
|
||||
browser.action.enable(sender.tab.id);
|
||||
|
||||
console.log('got message', message, sender);
|
||||
});
|
|
@ -54,6 +54,20 @@ const ENTRY_POINTS = {
|
|||
clips: './src/clips.js'
|
||||
};
|
||||
|
||||
if ( FOR_EXTENSION )
|
||||
ENTRY_POINTS.worker = './src/worker.ts';
|
||||
|
||||
const COPY_PATTERNS = [
|
||||
{
|
||||
from: FOR_EXTENSION
|
||||
? './src/entry_ext.js'
|
||||
: './src/entry.js',
|
||||
to: (DEV_SERVER || DEV_BUILD)
|
||||
? 'script.js'
|
||||
: 'script.min.js'
|
||||
},
|
||||
];
|
||||
|
||||
const TARGET = 'es2020';
|
||||
|
||||
/** @type {import('webpack').Configuration} */
|
||||
|
@ -125,16 +139,7 @@ const config = {
|
|||
|
||||
plugins: [
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: FOR_EXTENSION
|
||||
? './src/entry_ext.js'
|
||||
: './src/entry.js',
|
||||
to: (DEV_SERVER || DEV_BUILD)
|
||||
? 'script.js'
|
||||
: 'script.min.js'
|
||||
}
|
||||
]
|
||||
patterns: COPY_PATTERNS
|
||||
}),
|
||||
new VueLoaderPlugin(),
|
||||
new EsbuildPlugin({
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue