diff --git a/packages/packagers/webextension/src/WebExtensionPackager.js b/packages/packagers/webextension/src/WebExtensionPackager.js index 694964cc7e3..1473f2464ee 100644 --- a/packages/packagers/webextension/src/WebExtensionPackager.js +++ b/packages/packagers/webextension/src/WebExtensionPackager.js @@ -6,7 +6,7 @@ import {Packager} from '@parcel/plugin'; import {replaceURLReferences, relativeBundlePath} from '@parcel/utils'; export default (new Packager({ - async package({bundle, bundleGraph, options}) { + async package({bundle, bundleGraph}) { let assets = []; bundle.traverseAssets(asset => { assets.push(asset); @@ -43,14 +43,24 @@ export default (new Packager({ contentScript.css = [ ...new Set( - (contentScript.css || []).concat( - srcBundles - .flatMap(b => bundleGraph.getReferencedBundles(b)) - .filter(b => b.type == 'css') - .map(relPath), - ), + srcBundles + .flatMap(b => bundleGraph.getReferencedBundles(b)) + .filter(b => b.type == 'css') + .map(relPath) + .concat(contentScript.css || []), ), ]; + + contentScript.js = [ + ...new Set( + srcBundles + .flatMap(b => bundleGraph.getReferencedBundles(b)) + .filter(b => b.type == 'js') + .map(relPath) + .concat(contentScript.js || []), + ), + ]; + const resources = srcBundles .flatMap(b => { const children = []; @@ -80,10 +90,6 @@ export default (new Packager({ } } - if (manifest.manifest_version == 3 && options.hmrOptions) { - war.push({matches: [''], resources: ['__parcel_hmr_proxy__']}); - } - const warResult = (manifest.web_accessible_resources || []).concat( manifest.manifest_version == 2 ? [...new Set(war.flatMap(entry => entry.resources))] diff --git a/packages/runtimes/hmr/src/loaders/hmr-runtime.js b/packages/runtimes/hmr/src/loaders/hmr-runtime.js index 7514eedb3f3..9b060ea5b8a 100644 --- a/packages/runtimes/hmr/src/loaders/hmr-runtime.js +++ b/packages/runtimes/hmr/src/loaders/hmr-runtime.js @@ -96,17 +96,26 @@ if ((!parent || !parent.isParcelRequire) && typeof WebSocket !== 'undefined') { !/localhost|127.0.0.1|0.0.0.0/.test(hostname)) ? 'wss' : 'ws'; - var ws = new WebSocket( - protocol + '://' + hostname + (port ? ':' + port : '') + '/', - ); + + var ws; + try { + ws = new WebSocket( + protocol + '://' + hostname + (port ? ':' + port : '') + '/', + ); + } catch (err) { + if (err.message) { + console.error(err.message); + } + ws = {}; + } // Web extension context var extCtx = - typeof chrome === 'undefined' - ? typeof browser === 'undefined' + typeof browser === 'undefined' + ? typeof chrome === 'undefined' ? null - : browser - : chrome; + : chrome + : browser; // Safari doesn't support sourceURL in error stacks. // eval may also be disabled via CSP, so do a quick check. @@ -206,7 +215,9 @@ if ((!parent || !parent.isParcelRequire) && typeof WebSocket !== 'undefined') { } }; ws.onerror = function (e) { - console.error(e.message); + if (e.message) { + console.error(e.message); + } }; ws.onclose = function (e) { if (process.env.PARCEL_BUILD_ENV !== 'test') { @@ -400,25 +411,16 @@ async function hmrApplyUpdates(assets) { if (!supportsSourceURL) { let promises = assets.map(asset => hmrDownload(asset)?.catch(err => { - // Web extension bugfix for Chromium - // https://bugs.chromium.org/p/chromium/issues/detail?id=1255412#c12 + // Web extension fix if ( extCtx && extCtx.runtime && - extCtx.runtime.getManifest().manifest_version == 3 + extCtx.runtime.getManifest().manifest_version == 3 && + typeof ServiceWorkerGlobalScope != 'undefined' && + global instanceof ServiceWorkerGlobalScope ) { - if ( - typeof ServiceWorkerGlobalScope != 'undefined' && - global instanceof ServiceWorkerGlobalScope - ) { - extCtx.runtime.reload(); - return; - } - asset.url = extCtx.runtime.getURL( - '/__parcel_hmr_proxy__?url=' + - encodeURIComponent(asset.url + '?t=' + Date.now()), - ); - return hmrDownload(asset); + extCtx.runtime.reload(); + return; } throw err; }), diff --git a/packages/runtimes/webextension/src/WebExtensionRuntime.js b/packages/runtimes/webextension/src/WebExtensionRuntime.js index 4f3a5f25921..073b46bcfb8 100644 --- a/packages/runtimes/webextension/src/WebExtensionRuntime.js +++ b/packages/runtimes/webextension/src/WebExtensionRuntime.js @@ -1,6 +1,7 @@ // @flow strict-local import {Runtime} from '@parcel/plugin'; +import {replaceURLReferences} from '@parcel/utils'; import nullthrows from 'nullthrows'; import fs from 'fs'; import path from 'path'; @@ -11,14 +12,15 @@ const AUTORELOAD_BG = fs.readFileSync( ); export default (new Runtime({ - apply({bundle, bundleGraph, options}) { + loadConfig({config}) { + config.invalidateOnBuild(); + }, + async apply({bundle, bundleGraph, options}) { if (!bundle.env.isBrowser() || bundle.env.isWorklet()) { return; } - if (bundle.name == 'manifest.json') { - const asset = bundle.getMainEntry(); - if (asset?.meta.webextEntry !== true) return; + if (bundle.getMainEntry()?.meta.webextEntry === true) { // Hack to bust packager cache when any descendants update const descendants = []; bundleGraph.traverseBundles(b => { @@ -35,7 +37,7 @@ export default (new Runtime({ .find(b => b.getMainEntry()?.meta.webextEntry === true); const entry = manifest?.getMainEntry(); const insertDep = entry?.meta.webextBGInsert; - if (insertDep == null) return; + if (!manifest || !entry || insertDep == null) return; const insertBundle = bundleGraph.getReferencedBundle( nullthrows(entry?.getDependencies().find(dep => dep.id === insertDep)), nullthrows(manifest), @@ -50,16 +52,30 @@ export default (new Runtime({ // Add autoreload if (bundle === firstInsertableBundle) { - return { - filePath: __filename, - code: - `var HMR_HOST = ${JSON.stringify( - options.hmrOptions?.host ?? 'localhost', - )};` + - `var HMR_PORT = '${options.hmrOptions?.port ?? ''}';` + - AUTORELOAD_BG, - isEntry: true, - }; + return [ + { + filePath: __filename, + code: AUTORELOAD_BG, + isEntry: true, + }, + { + filePath: __filename, + // cache bust on non-asset manifest.json changes + code: `JSON.parse(${JSON.stringify( + JSON.stringify( + JSON.parse( + replaceURLReferences({ + bundle: manifest, + bundleGraph, + contents: await entry.getCode(), + getReplacement: () => '', + }).contents, + ), + ), + )})`, + isEntry: true, + }, + ]; } } }, diff --git a/packages/runtimes/webextension/src/autoreload-bg.js b/packages/runtimes/webextension/src/autoreload-bg.js index 37bf39338a1..f53ad2e96af 100644 --- a/packages/runtimes/webextension/src/autoreload-bg.js +++ b/packages/runtimes/webextension/src/autoreload-bg.js @@ -1,28 +1,44 @@ -/* global chrome, browser, addEventListener, HMR_HOST, HMR_PORT */ -var env = typeof chrome == 'undefined' ? browser : chrome; -env.runtime.onMessage.addListener(function (msg) { +/* global chrome, browser */ +let env = typeof browser === 'undefined' ? chrome : browser; +let origReload = env.runtime.reload; +let avoidID = -1; + +let promisify = + (obj, fn) => + (...args) => { + if (typeof browser === 'undefined') { + return new Promise((resolve, reject) => + obj[fn](...args, res => + env.runtime.lastError ? reject(env.runtime.lastError) : resolve(res), + ), + ); + } + return obj[fn](...args); + }; + +let queryTabs = promisify(env.tabs, 'query'); +let messageTab = promisify(env.tabs, 'sendMessage'); + +env.runtime.reload = () => { + queryTabs({}) + .then(tabs => { + return Promise.all( + tabs.map(tab => { + if (tab.id === avoidID) return; + return messageTab(tab.id, { + __parcel_hmr_reload__: true, + }).catch(() => {}); + }), + ); + }) + .then(() => { + origReload.call(env.runtime); + }); +}; + +env.runtime.onMessage.addListener((msg, sender) => { if (msg.__parcel_hmr_reload__) { + avoidID = sender.tab.id; env.runtime.reload(); } }); - -if (env.runtime.getManifest().manifest_version == 3) { - var proxyLoc = env.runtime.getURL('/__parcel_hmr_proxy__?url='); - addEventListener('fetch', function (evt) { - var url = evt.request.url; - if (url.startsWith(proxyLoc)) { - url = new URL(decodeURIComponent(url.slice(proxyLoc.length))); - if (url.hostname == HMR_HOST && url.port == HMR_PORT) { - evt.respondWith( - fetch(url).then(function (res) { - return new Response(res.body, { - headers: { - 'Content-Type': res.headers.get('Content-Type'), - }, - }); - }), - ); - } - } - }); -} diff --git a/packages/transformers/webextension/src/WebExtensionTransformer.js b/packages/transformers/webextension/src/WebExtensionTransformer.js index 724f6a7f643..ac12fbb8669 100644 --- a/packages/transformers/webextension/src/WebExtensionTransformer.js +++ b/packages/transformers/webextension/src/WebExtensionTransformer.js @@ -94,7 +94,6 @@ async function collectDependencies( } } } - let needRuntimeBG = false; if (program.content_scripts) { for (let i = 0; i < program.content_scripts.length; ++i) { const sc = program.content_scripts[i]; @@ -114,7 +113,6 @@ async function collectDependencies( } } if (hot && sc.js && sc.js.length) { - needRuntimeBG = true; sc.js.push( asset.addURLDependency('./runtime/autoreload.js', { resolveFrom: __filename, @@ -267,91 +265,90 @@ async function collectDependencies( } } } - if (isMV2) { - if (program.background?.page) { - program.background.page = asset.addURLDependency( - program.background.page, - { - bundleBehavior: 'isolated', - loc: { - filePath, - ...getJSONSourceLocation(ptrs['/background/page'], 'value'), - }, + if (program.background?.page) { + program.background.page = asset.addURLDependency(program.background.page, { + bundleBehavior: 'isolated', + loc: { + filePath, + ...getJSONSourceLocation(ptrs['/background/page'], 'value'), + }, + }); + } else if (program.background?.service_worker) { + program.background.service_worker = asset.addURLDependency( + program.background.service_worker, + { + bundleBehavior: 'isolated', + loc: { + filePath, + ...getJSONSourceLocation(ptrs['/background/service_worker'], 'value'), }, - ); - if (needRuntimeBG) { - asset.meta.webextBGInsert = program.background.page; - } - } - if (hot) { + env: { + context: 'service-worker', + sourceType: program.background.type == 'module' ? 'module' : 'script', + }, + }, + ); + } + if (hot) { + if (isMV2) { // To enable HMR, we must override the CSP to allow 'unsafe-eval' program.content_security_policy = cspPatchHMR( program.content_security_policy, ); - - if (needRuntimeBG && !program.background?.page) { - if (!program.background) { - program.background = {}; - } - if (!program.background.scripts) { - program.background.scripts = []; - } - if (program.background.scripts.length == 0) { - program.background.scripts.push( - asset.addURLDependency('./runtime/default-bg.js', { - resolveFrom: __filename, - }), - ); - } - asset.meta.webextBGInsert = program.background.scripts[0]; - } - } - } else { - if (program.background?.service_worker) { - program.background.service_worker = asset.addURLDependency( - program.background.service_worker, - { - bundleBehavior: 'isolated', - loc: { - filePath, - ...getJSONSourceLocation( - ptrs['/background/service_worker'], - 'value', - ), - }, - env: { - context: 'service-worker', - sourceType: - program.background.type == 'module' ? 'module' : 'script', - }, - }, - ); - } - if (hot) { - // Enable eval HMR for sandbox, + } else { + // Enable HMR for fetched localhost chunks const csp = program.content_security_policy || {}; csp.extension_pages = cspPatchHMR( csp.extension_pages, - `http://${hmrOptions?.host || 'localhost'}`, + `http://${hmrOptions?.host || 'localhost'}:*`, ); // Sandbox allows eval by default if (csp.sandbox) csp.sandbox = cspPatchHMR(csp.sandbox); program.content_security_policy = csp; - if (needRuntimeBG) { - if (!program.background) { - program.background = {}; - } - if (!program.background.service_worker) { - program.background.service_worker = asset.addURLDependency( - './runtime/default-bg.js', - { - resolveFrom: __filename, - env: {context: 'service-worker'}, - }, - ); - } - asset.meta.webextBGInsert = program.background.service_worker; + } + + if (!program.background) { + program.background = {}; + } + + if (program.background.page) { + asset.meta.webextBGInsert = program.background.page; + } else if (isMV2 || program.background.scripts) { + if (!program.background.scripts) { + program.background.scripts = []; } + if (program.background.scripts.length == 0) { + program.background.scripts.push( + asset.addURLDependency('./runtime/default-bg.js', { + resolveFrom: __filename, + }), + ); + } + asset.meta.webextBGInsert = program.background.scripts[0]; + } else { + if (!program.background.service_worker) { + program.background.service_worker = asset.addURLDependency( + './runtime/default-bg.js', + { + resolveFrom: __filename, + env: {context: 'service-worker'}, + }, + ); + } + asset.meta.webextBGInsert = program.background.service_worker; + } + + if (!program.permissions) program.permissions = []; + if (!isMV2 && !program.permissions.includes('scripting')) { + program.permissions.push('scripting'); + } + const hostPerms = [ + ...new Set(program.content_scripts.flatMap(sc => sc.matches)), + ]; + if (isMV2) program.permissions = program.permissions.concat(hostPerms); + else { + if (!program.host_permissions) program.host_permissions = []; + program.host_permissions = program.host_permissions.concat(hostPerms); } } } diff --git a/packages/transformers/webextension/src/runtime/autoreload.js b/packages/transformers/webextension/src/runtime/autoreload.js index df9da7aa0b1..d569458dd5f 100644 --- a/packages/transformers/webextension/src/runtime/autoreload.js +++ b/packages/transformers/webextension/src/runtime/autoreload.js @@ -1,11 +1,26 @@ -/* global chrome, browser, addEventListener */ -var env = typeof chrome == 'undefined' ? browser : chrome; +/* global chrome, browser, addEventListener, location */ +var env = typeof browser == 'undefined' ? chrome : browser; +var blockReload = true; + addEventListener('beforeunload', function () { + if (!blockReload) return; try { env.runtime.sendMessage({ __parcel_hmr_reload__: true, }); + // spinlock for 500ms to let background reload + let end = Date.now() + 500; + while (Date.now() < end); } catch (err) { // ignore throwing if extension context invalidated } }); + +env.runtime.onMessage.addListener(function (msg) { + if (msg.__parcel_hmr_reload__) { + blockReload = false; + setTimeout(function () { + location.reload(); + }, 400); + } +}); diff --git a/packages/transformers/webextension/src/schema.js b/packages/transformers/webextension/src/schema.js index 40146f17dfb..89534b91954 100644 --- a/packages/transformers/webextension/src/schema.js +++ b/packages/transformers/webextension/src/schema.js @@ -76,6 +76,16 @@ const warBase = { additionalProperties: false, }; +const mv2Background = { + type: 'object', + properties: { + scripts: arrStr, + page: string, + persistent: boolean, + }, + additionalProperties: false, +}; + const commonProps = { $schema: string, name: string, @@ -450,16 +460,21 @@ export const MV3Schema = ({ }, action: browserAction, background: { - type: 'object', - properties: { - service_worker: string, - type: { - type: 'string', - enum: ['classic', 'module'], + oneOf: [ + { + type: 'object', + properties: { + service_worker: string, + type: { + type: 'string', + enum: ['classic', 'module'], + }, + }, + additionalProperties: false, + required: ['service_worker'], }, - }, - additionalProperties: false, - required: ['service_worker'], + mv2Background, + ], // for Firefox }, content_security_policy: { type: 'object', @@ -511,15 +526,7 @@ export const MV2Schema = ({ type: 'number', enum: [2], }, - background: { - type: 'object', - properties: { - scripts: arrStr, - page: string, - persistent: boolean, - }, - additionalProperties: false, - }, + background: mv2Background, browser_action: browserAction, content_security_policy: string, page_action: {