Skip to content

Commit

Permalink
MV3 WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
tophf committed Oct 16, 2024
1 parent abce7cd commit 40cb2b1
Show file tree
Hide file tree
Showing 37 changed files with 501 additions and 90 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@
"update-locales": "tx pull --all && node tools/fix-transifex.js && git commit -m \"update locales\" _locales",
"update-transifex": "tx push -s",
"build": "webpack-cli",
"watch": "webpack-cli watch --node-env DEV",
"watch": "webpack-cli watch --define-process-env-node-env DEV",
"watch-mv3": "webpack-cli watch --define-process-env-node-env chrome-mv3",
"build-icons": "node tools/build-icons",
"zip": "npm test && node tools/build-zip.js",
"postinstall": "node tools/postinstall.js",
Expand Down
34 changes: 34 additions & 0 deletions src/background-sw/bg-offscreen.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import createPort from '/js/port';

const offscreen = /** @type {OffscreenAPI} */ new Proxy({
get exec() {
const url = new URL('offscreen.html', location).href;
const res = createPort(async () => {
let client;
for (let retry = 0; retry < 2; retry++) {
client = (await self.clients.matchAll({includeUncontrolled: true}))
.find(c => c.url === url);
if (client || retry) {
return client;
}
try {
await chrome.offscreen.createDocument({
url,
reasons: ['BLOBS', 'DOM_PARSER', 'MATCH_MEDIA', 'WORKERS'],
justification: 'ManifestV3 requirement',
});
} catch (err) {
if (!err.message.startsWith('Only a single offscreen')) throw err;
}
}
});
Object.defineProperty(this, 'exec', {value: res});
return res;
},
}, {
get: (me, cmd) => function (...args) {
return me.exec.call(this, cmd, args);
},
});

export default offscreen;
File renamed without changes.
50 changes: 50 additions & 0 deletions src/background-sw/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import './intro';
import * as msg from '/js/msg';
import './keep-alive';
import './bg-offscreen';
import '../background';
import {URLS} from '/js/toolbox';

/** @param {ExtendableEvent} evt */
self.oninstall = evt => {
evt.addRoutes({
condition: {not: {urlPattern: `${URLS.ownOrigin}*.user.css`}},
source: 'network',
});
};

/** @param {FetchEvent} evt */
self.onfetch = evt => {
let url = evt.request.url;
if (url.startsWith(URLS.ownOrigin)
&& +(url = url.split('#'))[1]
&& url[0].endsWith('.user.css')
&& !url[0].includes('?') /* skipping installer */) {
evt.respondWith(Response.redirect('edit.html?id=' + url[1]));
}
};

self.onmessage = evt => {
if (evt.data[0] === 'port') {
chrome.runtime.connect({name: evt.data[1]});
evt.ports[0].onmessage = onClientPortMessage;
evt.ports[0].postMessage({id: 0});
}
};

/**
* @this {MessagePort}
* @param {MessageEvent} evt
*/
async function onClientPortMessage(evt) {
const {args, id} = evt.data;
let res, err;
try {
res = msg._execute('extension', ...args, {});
if (res instanceof Promise) res = await res;
} catch (e) {
err = e;
res = undefined;
}
this.postMessage({id, res, err});
}
2 changes: 2 additions & 0 deletions src/background-sw/intro.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
self.browser = chrome;
self.window = self;
36 changes: 36 additions & 0 deletions src/background-sw/keep-alive.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {subscribe} from '/src/js/prefs';

/** @type {?Promise[]} */
let busy;
let lastBusyTime;
let pulse;

subscribe('keepAlive', checkPref, true);

export function keepAliveWhileBusy(...promises) {
if (!busy) checkBusyWhenSettled(promises);
else busy.push(...promises);
}

function checkBusyWhenSettled(promises) {
Promise.allSettled(busy = promises).then(checkBusy);
}

function checkBusy({length}) {
if (length < busy.length) {
checkBusyWhenSettled(busy.slice(length));
} else {
busy = null;
lastBusyTime = performance.now();
}
}

async function checkPref(key, val) {
if (busy || val < 0 || val && (performance.now() - lastBusyTime < val * 60e3)) {
chrome.runtime.getPlatformInfo();
if (!pulse) pulse = setInterval(checkPref, 25e3, key, val);
} else if (pulse) {
clearInterval(pulse);
pulse = 0;
}
}
4 changes: 3 additions & 1 deletion src/background/color-scheme.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ const mode = {
};
let isDarkNow = false;
// matchMedia's onchange doesn't work in bg context, so we use it in our content script
update('system', matchMedia('(prefers-color-scheme:dark)').matches);
if (!process.env.MV3) {
update('system', matchMedia('(prefers-color-scheme:dark)').matches);
}
prefs.subscribe(kSTATE, (_, val) => {
if (val === 'time') {
prefs.subscribe([kSTART, kEND], onNightChanged, true);
Expand Down
27 changes: 19 additions & 8 deletions src/background/content-scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,25 @@ export default function reinjectContentScripts() {
if (!cs[ALL_URLS] && !cs.matches.some(url.match, url)) {
continue;
}
const options = {
runAt: cs.run_at,
allFrames: cs.all_frames,
matchAboutBlank: cs.match_about_blank,
};
for (const file of cs.js) {
options.file = file;
jobs.push(browser.tabs.executeScript(tabId, options).catch(ignoreChromeError));
if (process.env.MV3) {
jobs.push(chrome.scripting.executeScript({
injectImmediately: cs.run_at === 'document_start',
target: {
allFrames: cs.all_frames,
tabId,
},
files: cs.js,
}).catch(ignoreChromeError));
} else {
const options = {
runAt: cs.run_at,
allFrames: cs.all_frames,
matchAboutBlank: cs.match_about_blank,
};
for (const file of cs.js) {
options.file = file;
jobs.push(browser.tabs.executeScript(tabId, options).catch(ignoreChromeError));
}
}
}
await Promise.all(jobs);
Expand Down
4 changes: 2 additions & 2 deletions src/background/icon-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export function overrideBadge({text = '', color = '', title = ''} = {}) {
refreshIconBadgeText(tabId);
}
}
chrome.browserAction.setTitle({
safeCall('setTitle', {
title: title && chrome.i18n.getMessage(title) || title || '',
});
}
Expand Down Expand Up @@ -197,7 +197,7 @@ function refreshStaleBadges() {
}

function safeCall(method, data) {
const {browserAction = {}} = chrome;
const {action = {}, browserAction = action} = chrome;
const fn = browserAction[method];
if (fn) {
try {
Expand Down
3 changes: 3 additions & 0 deletions src/background/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<%= htmlWebpackPlugin.tags.headTags %>
<%= htmlWebpackPlugin.tags.bodyTags %>

4 changes: 4 additions & 0 deletions src/background/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {updateIconBadge} from './icon-manager';
import prefsApi from './prefs-api';
import * as styleMan from './style-manager';
import initStyleViaApi from './style-via-api';
import './style-via-webrequest';
import * as syncMan from './sync-manager';
import {openEditor, openManage, openURL, waitForTabUrl} from './tab-util';
import * as updateMan from './update-manager';
Expand Down Expand Up @@ -103,6 +104,9 @@ chrome.runtime.onInstalled.addListener(({reason, previousVersion}) => {
if (previousVersion === '1.5.30') {
API.prefsDb.delete('badFavs'); // old Stylus marked all icons as bad when network was offline
}
if (process.env.MV3 && previousVersion.startsWith('1.5.')) {
prefs.set('keepAlive', -1);
}
});

onMessage((m, sender) => {
Expand Down
30 changes: 20 additions & 10 deletions src/background/style-via-webrequest.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import popupGetStyles from '/js/popup-get-styles';
import * as prefs from '/js/prefs';
import {CHROME, FIREFOX, ignoreChromeError, MF, URLS} from '/js/toolbox';
import {CHROME, FIREFOX, ignoreChromeError, MF_ACTION_HTML, URLS} from '/js/toolbox';
import {API} from './common';
import {getSectionsByUrl} from './style-manager';
import tabMan from './tab-manager';
Expand All @@ -14,11 +14,12 @@ const blobUrlPrefix = 'blob:' + chrome.runtime.getURL('/');
/** @type {Object<string,StylesToPass>} */
const stylesToPass = {};
const state = {};
const injectedCode = `${data => {
const INJECTED_FUNC = data => {
if (self.INJECTED !== 1) { // storing data only if apply.js hasn't run yet
window[Symbol.for('styles')] = data;
}
}}`;
};
const INJECTED_CODE = `${INJECTED_FUNC}`;

toggle();
prefs.subscribe([idXHR, idOFF, idCSP], toggle);
Expand All @@ -34,7 +35,7 @@ function toggle() {
urls: [
'*://*/*',
CHROME &&
chrome.runtime.getURL(MF.browser_action.default_popup),
chrome.runtime.getURL(MF_ACTION_HTML),
].filter(Boolean),
types: ['main_frame', 'sub_frame'],
};
Expand All @@ -55,7 +56,7 @@ function toggle() {
if (CHROME && !off) {
chrome.webNavigation.onCommitted.addListener(injectData, {url: [{urlPrefix: 'http'}]});
}
if (CHROME) {
if (CHROME && !process.env.MV3) {
chrome.webRequest.onBeforeRequest.addListener(openNamedStyle, {
urls: [URLS.ownOrigin + '*.user.css'],
types: ['main_frame'],
Expand Down Expand Up @@ -83,11 +84,20 @@ function injectData(req) {
const data = stylesToPass[req2key(req)];
if (data && !data.injected) {
data.injected = true;
chrome.tabs.executeScript(req.tabId, {
frameId: req.frameId,
runAt: 'document_start',
code: `(${injectedCode})(${JSON.stringify(data.payload)})`,
}, ignoreChromeError);
if (process.env.MV3) {
chrome.scripting.executeScript({
target: {tabId: req.tabId, frameIds: [req.frameId]},
args: [data.payload],
func: INJECTED_FUNC,
injectImmediately: true,
}, ignoreChromeError);
} else {
chrome.tabs.executeScript(req.tabId, {
frameId: req.frameId,
runAt: 'document_start',
code: `(${INJECTED_CODE})(${JSON.stringify(data.payload)})`,
}, ignoreChromeError);
}
if (!state.xhr) cleanUp(req);
}
}
Expand Down
7 changes: 4 additions & 3 deletions src/background/sync-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import db from './db';
import {overrideBadge} from './icon-manager';
import * as styleMan from './style-manager';
import {getToken, revokeToken} from './token-manager';
import * as dbToCloud from 'db-to-cloud';

//#region Init

Expand Down Expand Up @@ -176,7 +177,7 @@ export async function syncNow() {
//#region Utils

async function initController() {
ctrl = (await (ctrl = import('db-to-cloud'))).dbToCloud({
ctrl = await (ctrl = dbToCloud.dbToCloud({
onGet: _id => styleMan.uuid2style(_id) || uuidIndex.custom[_id],
async onPut(doc) {
if (!doc) return; // TODO: delete it?
Expand Down Expand Up @@ -229,7 +230,7 @@ async function initController() {
retryMaxAttempts: 10,
retryExp: 1.2,
retryDelay: 6,
});
}));
}

function emitStatusChange() {
Expand Down Expand Up @@ -272,7 +273,7 @@ async function getDrive(name) {
const options = await getDriveOptions(name);
options.getAccessToken = () => getToken(name);
options.fetch = name === 'webdav' ? fetchWebDAV.bind(options) : fetch;
return (await import('db-to-cloud')).drive[name].default(options);
return dbToCloud.drive[name].default(options);
}
throw new Error(`unknown cloud name: ${name}`);
}
Expand Down
3 changes: 2 additions & 1 deletion src/background/token-manager.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import launchWebAuthFlow from 'webext-launch-web-auth-flow';
import {browserWindows, clamp, FIREFOX, URLS} from '/js/toolbox';
import {chromeLocal} from '/js/storage-util';
import {isVivaldi} from './common';
Expand Down Expand Up @@ -158,7 +159,7 @@ async function authUser(keys, name, interactive = false, hooks = null) {
const width = clamp(screen.availWidth - 100, 400, 800);
const height = clamp(screen.availHeight - 100, 200, 800);
const wnd = !alwaysUseTab && await browserWindows.getLastFocused();
const finalUrl = await (await import('webext-launch-web-auth-flow')).default({
const finalUrl = await launchWebAuthFlow({
url,
alwaysUseTab,
interactive,
Expand Down
40 changes: 35 additions & 5 deletions src/background/usercss-install-helper.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import browser from '/js/browser';
import {DNR_ID_INSTALLER} from '/js/dnr';
import * as prefs from '/js/prefs';
import {FIREFOX, RX_META, URLS} from '/js/toolbox';
import {bgReady} from './common';
Expand All @@ -21,7 +22,7 @@ export function getInstallCode(url) {
}

function toggle(key, val) {
chrome.webRequest.onHeadersReceived.removeListener(maybeInstallByMime);
if (!process.env.MV3) chrome.webRequest.onHeadersReceived.removeListener(maybeInstallByMime);
tabMan.onOff(maybeInstall, val);
const urls = val ? [''] : [
/* Known distribution sites where we ignore urlInstaller option, because
Expand All @@ -31,10 +32,39 @@ function toggle(key, val) {
...URLS.usoaRaw,
...['greasy', 'sleazy'].map(h => `https://update.${h}fork.org/`),
];
chrome.webRequest.onHeadersReceived.addListener(maybeInstallByMime, {
urls: urls.reduce(reduceUsercssGlobs, []),
types: ['main_frame'],
}, ['responseHeaders', 'blocking']);
if (process.env.MV3) {
const header = 'content-type';
/** @type {chrome.declarativeNetRequest.Rule[]} */
const rules = [{
id: DNR_ID_INSTALLER,
condition: {
regexFilter: val
? /^.*\.user\.(?:css|less|styl)(?:\?.*)?$/.source
: /^.*\.user\.css$/.source,
requestDomains: val
? undefined
: [...new Set(urls.map(u => u.split('/')[2]))],
resourceTypes: ['main_frame'],
responseHeaders: [{header, values: ['text/*']}],
excludedResponseHeaders: [{header, values: ['text/html']}],
},
action: {
type: 'redirect',
redirect: {
regexSubstitution: chrome.runtime.getURL('install-usercss.html#\\0'),
},
},
}];
chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: rules.map(r => r.id),
addRules: rules,
});
} else {
chrome.webRequest.onHeadersReceived.addListener(maybeInstallByMime, {
urls: urls.reduce(reduceUsercssGlobs, []),
types: ['main_frame'],
}, ['responseHeaders', 'blocking']);
}
}

function clearInstallCode(url) {
Expand Down
Loading

0 comments on commit 40cb2b1

Please sign in to comment.