diff --git a/packages/core/utils/src/http-server.js b/packages/core/utils/src/http-server.js index 1500a57e0e6..205dabb1bac 100644 --- a/packages/core/utils/src/http-server.js +++ b/packages/core/utils/src/http-server.js @@ -1,7 +1,15 @@ // @flow strict-local -import type {Server as HTTPOnlyServer} from 'http'; -import type {Server as HTTPSServer} from 'https'; +import type { + Server as HTTPOnlyServer, + IncomingMessage as HTTPRequest, + ServerResponse as HTTPResponse, +} from 'http'; +import type { + Server as HTTPSServer, + IncomingMessage as HTTPSRequest, + ServerResponse as HTTPSResponse, +} from 'https'; import type {Socket} from 'net'; import type {FilePath, HTTPSOptions} from '@parcel/types'; import type {FileSystem} from '@parcel/fs'; @@ -12,12 +20,16 @@ import nullthrows from 'nullthrows'; import {getCertificate, generateCertificate} from './'; type CreateHTTPServerOpts = {| - https: ?(HTTPSOptions | boolean), - inputFS: FileSystem, - outputFS: FileSystem, - cacheDir: FilePath, - listener?: (mixed, mixed) => void, + listener?: (HTTPRequest | HTTPSRequest, HTTPResponse | HTTPSResponse) => void, host?: string, + ... + | {| + https: ?(HTTPSOptions | boolean), + inputFS: FileSystem, + outputFS: FileSystem, + cacheDir: FilePath, + |} + | {||}, |}; export type HTTPServer = HTTPOnlyServer | HTTPSServer; diff --git a/packages/packagers/webextension/src/WebExtensionPackager.js b/packages/packagers/webextension/src/WebExtensionPackager.js index f1670adbed7..1f02d0eca47 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}) { + async package({bundle, bundleGraph, options}) { let assets = []; bundle.traverseAssets(asset => { assets.push(asset); @@ -73,14 +73,17 @@ 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))] : war, ); - if (warResult.length > 0) { - manifest.web_accessible_resources = warResult; - } + + if (warResult.length > 0) manifest.web_accessible_resources = warResult; let {contents} = replaceURLReferences({ bundle, diff --git a/packages/reporters/dev-server/src/HMRServer.js b/packages/reporters/dev-server/src/HMRServer.js index 44f0195e716..94c296aee7e 100644 --- a/packages/reporters/dev-server/src/HMRServer.js +++ b/packages/reporters/dev-server/src/HMRServer.js @@ -10,12 +10,25 @@ import type { } from '@parcel/types'; import type {Diagnostic} from '@parcel/diagnostic'; import type {AnsiDiagnosticResult} from '@parcel/utils'; -import type {ServerError, HMRServerOptions} from './types.js.flow'; - +import type { + ServerError, + HMRServerOptions, + Request, + Response, +} from './types.js.flow'; +import {setHeaders, SOURCES_ENDPOINT} from './Server'; + +import nullthrows from 'nullthrows'; +import url from 'url'; +import mime from 'mime-types'; import WebSocket from 'ws'; import invariant from 'assert'; -import {ansiHtml, prettyDiagnostic, PromiseQueue} from '@parcel/utils'; -import {HMR_ENDPOINT} from './Server'; +import { + ansiHtml, + createHTTPServer, + prettyDiagnostic, + PromiseQueue, +} from '@parcel/utils'; export type HMRAsset = {| id: string, @@ -23,6 +36,7 @@ export type HMRAsset = {| type: string, output: string, envHash: string, + outputFormat: string, depsByBundle: {[string]: {[string]: string, ...}, ...}, |}; @@ -40,22 +54,38 @@ export type HMRMessage = |}; const FS_CONCURRENCY = 64; +const HMR_ENDPOINT = '/__parcel_hmr'; export default class HMRServer { wss: WebSocket.Server; unresolvedError: HMRMessage | null = null; options: HMRServerOptions; + bundleGraph: BundleGraph | null = null; + stopServer: ?() => Promise; constructor(options: HMRServerOptions) { this.options = options; } - start(): any { - this.wss = new WebSocket.Server( - this.options.devServer - ? {server: this.options.devServer} - : {port: this.options.port}, - ); + async start() { + let server = this.options.devServer; + if (!server) { + let result = await createHTTPServer({ + listener: (req, res) => { + setHeaders(res); + if (!this.handle(req, res)) { + res.statusCode = 404; + res.end(); + } + }, + }); + server = result.server; + server.listen(this.options.port, this.options.host); + this.stopServer = result.stop; + } else { + this.options.addMiddleware?.((req, res) => this.handle(req, res)); + } + this.wss = new WebSocket.Server({server}); this.wss.on('connection', ws => { if (this.unresolvedError) { @@ -65,13 +95,28 @@ export default class HMRServer { // $FlowFixMe[incompatible-exact] this.wss.on('error', err => this.handleSocketError(err)); + } - let address = this.wss.address(); - invariant(typeof address === 'object' && address != null); - return address.port; + handle(req: Request, res: Response): boolean { + let {pathname} = url.parse(req.originalUrl || req.url); + if (pathname != null && pathname.startsWith(HMR_ENDPOINT)) { + let id = pathname.slice(HMR_ENDPOINT.length + 1); + let bundleGraph = nullthrows(this.bundleGraph); + let asset = bundleGraph.getAssetById(id); + this.getHotAssetContents(asset).then(output => { + res.setHeader('Content-Type', mime.contentType(asset.type)); + res.end(output); + }); + return true; + } + return false; } - stop() { + async stop() { + if (this.stopServer != null) { + await this.stopServer(); + this.stopServer = null; + } this.wss.close(); } @@ -106,6 +151,7 @@ export default class HMRServer { async emitUpdate(event: BuildSuccessEvent) { this.unresolvedError = null; + this.bundleGraph = event.bundleGraph; let changedAssets = new Set(event.changedAssets.values()); if (changedAssets.size === 0) return; @@ -153,14 +199,13 @@ export default class HMRServer { return { id: event.bundleGraph.getAssetPublicId(asset), - url: getSourceURL(event.bundleGraph, asset), + url: this.getSourceURL(asset), type: asset.type, // No need to send the contents of non-JS assets to the client. output: - asset.type === 'js' - ? await getHotAssetContents(event.bundleGraph, asset) - : '', + asset.type === 'js' ? await this.getHotAssetContents(asset) : '', envHash: asset.env.id, + outputFormat: asset.env.outputFormat, depsByBundle, }; }); @@ -173,6 +218,41 @@ export default class HMRServer { }); } + async getHotAssetContents(asset: Asset): Promise { + let output = await asset.getCode(); + let bundleGraph = nullthrows(this.bundleGraph); + if (asset.type === 'js') { + let publicId = bundleGraph.getAssetPublicId(asset); + output = `parcelHotUpdate['${publicId}'] = function (require, module, exports) {${output}}`; + } + + let sourcemap = await asset.getMap(); + if (sourcemap) { + let sourcemapStringified = await sourcemap.stringify({ + format: 'inline', + sourceRoot: SOURCES_ENDPOINT + '/', + // $FlowFixMe + fs: asset.fs, + }); + + invariant(typeof sourcemapStringified === 'string'); + output += `\n//# sourceMappingURL=${sourcemapStringified}`; + output += `\n//# sourceURL=${encodeURI(this.getSourceURL(asset))}\n`; + } + + return output; + } + + getSourceURL(asset: Asset): string { + let origin = ''; + if (!this.options.devServer) { + origin = `http://${this.options.host || 'localhost'}:${ + this.options.port + }`; + } + return origin + HMR_ENDPOINT + '/' + asset.id; + } + handleSocketError(err: ServerError) { if (err.code === 'ECONNRESET') { // This gets triggered on page refresh, ignore this @@ -201,34 +281,3 @@ function getSpecifier(dep: Dependency): string { return dep.specifier; } - -export async function getHotAssetContents( - bundleGraph: BundleGraph, - asset: Asset, -): Promise { - let output = await asset.getCode(); - if (asset.type === 'js') { - let publicId = bundleGraph.getAssetPublicId(asset); - output = `parcelHotUpdate['${publicId}'] = function (require, module, exports) {${output}}`; - } - - let sourcemap = await asset.getMap(); - if (sourcemap) { - let sourcemapStringified = await sourcemap.stringify({ - format: 'inline', - sourceRoot: '/__parcel_source_root/', - // $FlowFixMe - fs: asset.fs, - }); - - invariant(typeof sourcemapStringified === 'string'); - output += `\n//# sourceMappingURL=${sourcemapStringified}`; - output += `\n//# sourceURL=${getSourceURL(bundleGraph, asset)}\n`; - } - - return output; -} - -function getSourceURL(bundleGraph, asset) { - return HMR_ENDPOINT + asset.id; -} diff --git a/packages/reporters/dev-server/src/Server.js b/packages/reporters/dev-server/src/Server.js index 655cc86e8a6..8789b84604c 100644 --- a/packages/reporters/dev-server/src/Server.js +++ b/packages/reporters/dev-server/src/Server.js @@ -29,13 +29,10 @@ import connect from 'connect'; import serveHandler from 'serve-handler'; import {createProxyMiddleware} from 'http-proxy-middleware'; import {URL, URLSearchParams} from 'url'; -import {getHotAssetContents} from './HMRServer'; -import nullthrows from 'nullthrows'; -import mime from 'mime-types'; import launchEditor from 'launch-editor'; import fresh from 'fresh'; -function setHeaders(res: Response) { +export function setHeaders(res: Response) { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader( 'Access-Control-Allow-Methods', @@ -48,8 +45,7 @@ function setHeaders(res: Response) { res.setHeader('Cache-Control', 'max-age=0, must-revalidate'); } -const SOURCES_ENDPOINT = '/__parcel_source_root'; -export const HMR_ENDPOINT = '/__parcel_hmr/'; +export const SOURCES_ENDPOINT = '/__parcel_source_root'; const EDITOR_ENDPOINT = '/__parcel_launch_editor'; const TEMPLATE_404 = fs.readFileSync( path.join(__dirname, 'templates/404.html'), @@ -65,6 +61,7 @@ type NextFunction = (req: Request, res: Response, next?: (any) => any) => any; export default class Server { pending: boolean; pendingRequests: Array<[Request, Response]>; + middleware: Array<(req: Request, res: Response) => boolean>; options: DevServerOptions; rootPath: string; bundleGraph: BundleGraph | null; @@ -87,6 +84,7 @@ export default class Server { } this.pending = true; this.pendingRequests = []; + this.middleware = []; this.bundleGraph = null; this.requestBundle = null; this.errors = null; @@ -135,8 +133,8 @@ export default class Server { } respond(req: Request, res: Response): mixed { + if (this.middleware.some(handler => handler(req, res))) return; let {pathname, search} = url.parse(req.originalUrl || req.url); - if (pathname == null) { pathname = '/'; } @@ -151,13 +149,9 @@ export default class Server { } launchEditor(file); } - setHeaders(res); res.end(); } else if (this.errors) { return this.send500(req, res); - } else if (pathname.startsWith(HMR_ENDPOINT)) { - let id = pathname.slice(HMR_ENDPOINT.length); - return this.sendAsset(id, res); } else if (path.extname(pathname) === '') { // If the URL doesn't start with the public path, or the URL doesn't // have a file extension, send the main HTML bundle. @@ -266,16 +260,6 @@ export default class Server { } } - async sendAsset(id: string, res: Response) { - let bundleGraph = nullthrows(this.bundleGraph); - let asset = bundleGraph.getAssetById(id); - let output = await getHotAssetContents(bundleGraph, asset); - - setHeaders(res); - res.setHeader('Content-Type', mime.contentType(asset.type)); - res.end(output); - } - serveDist( req: Request, res: Response, @@ -369,19 +353,15 @@ export default class Server { sendError(res: Response, statusCode: number) { res.statusCode = statusCode; - setHeaders(res); res.end(); } send404(req: Request, res: Response) { res.statusCode = 404; - setHeaders(res); res.end(TEMPLATE_404); } send500(req: Request, res: Response): void | Response { - setHeaders(res); - res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.writeHead(500); diff --git a/packages/reporters/dev-server/src/ServerReporter.js b/packages/reporters/dev-server/src/ServerReporter.js index 5e85824d2bb..1d436f084c3 100644 --- a/packages/reporters/dev-server/src/ServerReporter.js +++ b/packages/reporters/dev-server/src/ServerReporter.js @@ -44,22 +44,26 @@ export default (new Reporter({ if (hmrOptions && hmrOptions.port === serveOptions.port) { let hmrServerOptions = { port: serveOptions.port, + host: hmrOptions.host, devServer, + addMiddleware: handler => { + server?.middleware.push(handler); + }, logger, }; hmrServer = new HMRServer(hmrServerOptions); hmrServers.set(serveOptions.port, hmrServer); - hmrServer.start(); + await hmrServer.start(); return; } } let port = hmrOptions?.port; if (typeof port === 'number') { - let hmrServerOptions = {port, logger}; + let hmrServerOptions = {port, host: hmrOptions?.host, logger}; hmrServer = new HMRServer(hmrServerOptions); hmrServers.set(port, hmrServer); - hmrServer.start(); + await hmrServer.start(); } break; } @@ -75,7 +79,7 @@ export default (new Reporter({ servers.delete(server.options.port); } if (hmrOptions && hmrServer) { - hmrServer.stop(); + await hmrServer.stop(); // $FlowFixMe[prop-missing] hmrServers.delete(hmrServer.wss.options.port); } diff --git a/packages/reporters/dev-server/src/types.js.flow b/packages/reporters/dev-server/src/types.js.flow index afcac1274f9..b6e85d00832 100644 --- a/packages/reporters/dev-server/src/types.js.flow +++ b/packages/reporters/dev-server/src/types.js.flow @@ -37,6 +37,10 @@ export type ServerError = Error & {| export type HMRServerOptions = {| devServer?: HTTPServer, + addMiddleware?: ( + handler: (req: Request, res: Response) => boolean + ) => void, port: number, + host: ?string, logger: PluginLogger, |}; diff --git a/packages/runtimes/hmr/src/loaders/hmr-runtime.js b/packages/runtimes/hmr/src/loaders/hmr-runtime.js index 933b3d31a2a..513f45a0859 100644 --- a/packages/runtimes/hmr/src/loaders/hmr-runtime.js +++ b/packages/runtimes/hmr/src/loaders/hmr-runtime.js @@ -1,5 +1,5 @@ // @flow -/* global HMR_HOST, HMR_PORT, HMR_ENV_HASH, HMR_SECURE, chrome, browser, importScripts */ +/* global HMR_HOST, HMR_PORT, HMR_ENV_HASH, HMR_SECURE, chrome, browser, globalThis, __parcel__import__, __parcel__importScripts__, ServiceWorkerGlobalScope */ /*:: import type { @@ -31,6 +31,8 @@ interface ParcelModule { interface ExtensionContext { runtime: {| reload(): void, + getURL(url: string): string; + getManifest(): {manifest_version: number, ...}; |}; } declare var module: {bundle: ParcelRequire, ...}; @@ -40,6 +42,10 @@ declare var HMR_ENV_HASH: string; declare var HMR_SECURE: boolean; declare var chrome: ExtensionContext; declare var browser: ExtensionContext; +declare var __parcel__import__: (string) => Promise; +declare var __parcel__importScripts__: (string) => Promise; +declare var globalThis: typeof self; +declare var ServiceWorkerGlobalScope: Object; */ var OVERLAY_ID = '__parcel__error__overlay__'; @@ -93,6 +99,14 @@ if ((!parent || !parent.isParcelRequire) && typeof WebSocket !== 'undefined') { protocol + '://' + hostname + (port ? ':' + port : '') + '/', ); + // Web extension context + var extCtx = + typeof chrome === 'undefined' + ? typeof browser === 'undefined' + ? null + : browser + : chrome; + // Safari doesn't support sourceURL in error stacks. // eval may also be disabled via CSP, so do a quick check. var supportsSourceURL = false; @@ -146,20 +160,7 @@ if ((!parent || !parent.isParcelRequire) && typeof WebSocket !== 'undefined') { hmrAcceptRun(assetsToAccept[i][0], id); } } - } else if ('reload' in location) { - location.reload(); - } else { - // Web extension context - var ext = - typeof chrome === 'undefined' - ? typeof browser === 'undefined' - ? null - : browser - : chrome; - if (ext && ext.runtime && ext.runtime.reload) { - ext.runtime.reload(); - } - } + } else fullReload(); } if (data.type === 'error') { @@ -251,6 +252,14 @@ ${frame.code}`; return overlay; } +function fullReload() { + if ('reload' in location) { + location.reload(); + } else if (extCtx && extCtx.runtime && extCtx.runtime.reload) { + extCtx.runtime.reload(); + } +} + function getParents(bundle, id) /*: Array<[ParcelRequire, string]> */ { var modules = bundle.modules; if (!modules) { @@ -325,6 +334,37 @@ function reloadCSS() { }, 50); } +function hmrDownload(asset) { + if (asset.type === 'js') { + if (typeof document !== 'undefined') { + let script = document.createElement('script'); + script.src = asset.url + '?t=' + Date.now(); + if (asset.outputFormat === 'esmodule') { + script.type = 'module'; + } + return new Promise((resolve, reject) => { + script.onload = () => resolve(script); + script.onerror = reject; + document.head?.appendChild(script); + }); + } else if (typeof importScripts === 'function') { + // Worker scripts + if (asset.outputFormat === 'esmodule') { + return __parcel__import__(asset.url + '?t=' + Date.now()); + } else { + return new Promise((resolve, reject) => { + try { + __parcel__importScripts__(asset.url + '?t=' + Date.now()); + resolve(); + } catch (err) { + reject(err); + } + }); + } + } + } +} + async function hmrApplyUpdates(assets) { global.parcelHotUpdate = Object.create(null); @@ -337,27 +377,31 @@ async function hmrApplyUpdates(assets) { // https://bugs.webkit.org/show_bug.cgi?id=137297 // This path is also taken if a CSP disallows eval. if (!supportsSourceURL) { - let promises = assets.map(asset => { - if (asset.type === 'js') { - if (typeof document !== 'undefined') { - let script = document.createElement('script'); - script.src = asset.url; - return new Promise((resolve, reject) => { - script.onload = () => resolve(script); - script.onerror = reject; - document.head?.appendChild(script); - }); - } else if (typeof importScripts === 'function') { - return new Promise((resolve, reject) => { - try { - importScripts(asset.url); - } catch (err) { - reject(err); - } - }); + 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 + if ( + extCtx && + extCtx.runtime && + extCtx.runtime.getManifest().manifest_version == 3 + ) { + 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); } - } - }); + throw err; + }), + ); scriptsToRemove = await Promise.all(promises); } @@ -410,6 +454,7 @@ function hmrApply(bundle /*: ParcelRequire */, asset /*: HMRAsset */) { (0, eval)(asset.output); } + // $FlowFixMe let fn = global.parcelHotUpdate[asset.id]; modules[asset.id] = [fn, deps]; } else if (bundle.parent) { diff --git a/packages/runtimes/webextension/src/WebExtensionRuntime.js b/packages/runtimes/webextension/src/WebExtensionRuntime.js index 46efb9c6d53..4f3a5f25921 100644 --- a/packages/runtimes/webextension/src/WebExtensionRuntime.js +++ b/packages/runtimes/webextension/src/WebExtensionRuntime.js @@ -52,7 +52,12 @@ export default (new Runtime({ if (bundle === firstInsertableBundle) { return { filePath: __filename, - code: AUTORELOAD_BG, + code: + `var HMR_HOST = ${JSON.stringify( + options.hmrOptions?.host ?? 'localhost', + )};` + + `var HMR_PORT = '${options.hmrOptions?.port ?? ''}';` + + AUTORELOAD_BG, isEntry: true, }; } diff --git a/packages/runtimes/webextension/src/autoreload-bg.js b/packages/runtimes/webextension/src/autoreload-bg.js index 07f6b59622a..470d7898703 100644 --- a/packages/runtimes/webextension/src/autoreload-bg.js +++ b/packages/runtimes/webextension/src/autoreload-bg.js @@ -1,7 +1,28 @@ -/* global chrome, browser */ +/* global chrome, browser, addEventListener, fetch, Response, HMR_HOST, HMR_PORT */ var env = typeof chrome == 'undefined' ? browser : chrome; env.runtime.onMessage.addListener(function (msg) { if (msg.__parcel_hmr_reload__) { 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 9a690bb2fc2..b3088765ec7 100644 --- a/packages/transformers/webextension/src/WebExtensionTransformer.js +++ b/packages/transformers/webextension/src/WebExtensionTransformer.js @@ -1,5 +1,5 @@ // @flow -import type {MutableAsset} from '@parcel/types'; +import type {MutableAsset, HMROptions} from '@parcel/types'; import {Transformer} from '@parcel/plugin'; import path from 'path'; @@ -25,6 +25,7 @@ const DEP_LOCS = [ ['chrome_url_overrides'], ['devtools_page'], ['options_ui', 'page'], + ['sandbox', 'pages'], ['sidebar_action', 'default_icon'], ['sidebar_action', 'default_panel'], ['storage', 'managed_schema'], @@ -37,8 +38,9 @@ async function collectDependencies( asset: MutableAsset, program: any, ptrs: {[key: string]: any, ...}, - hot: boolean, + hmrOptions: ?HMROptions, ) { + const hot = Boolean(hmrOptions); const fs = asset.fs; const filePath = asset.filePath; const assetDir = path.dirname(filePath); @@ -307,43 +309,59 @@ async function collectDependencies( }, ); } - 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'}, - }, - ); + if (hot) { + // Enable eval HMR for sandbox, + const csp = program.content_security_policy || {}; + csp.extension_pages = cspPatchHMR( + csp.extension_pages, + `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; } - asset.meta.webextBGInsert = program.background.service_worker; } } } -function cspPatchHMR(policy: ?string) { +function cspPatchHMR(policy: ?string, insert?: string) { + let defaultSrc = "'self'"; + if (insert == null) { + insert = "'unsafe-eval'"; + defaultSrc = "'self' blob: filesystem:"; + } if (policy) { const csp = parseCSP(policy); policy = ''; if (!csp['script-src']) { - csp['script-src'] = ["'self' 'unsafe-eval' blob: filesystem:"]; + csp['script-src'] = [defaultSrc]; + } + if (!csp['script-src'].includes(insert)) { + csp['script-src'].push(insert); } - if (!csp['script-src'].includes("'unsafe-eval'")) { - csp['script-src'].push("'unsafe-eval'"); + if (csp.sandbox && !csp.sandbox.includes('allow-scripts')) { + csp.sandbox.push('allow-scripts'); } for (const k in csp) { policy += `${k} ${csp[k].join(' ')};`; } return policy; } else { - return ( - "script-src 'self' 'unsafe-eval' blob: filesystem:;" + - "object-src 'self' blob: filesystem:;" - ); + return `script-src ${defaultSrc} ${insert};` + `object-src ${defaultSrc};`; } } @@ -353,6 +371,10 @@ export default (new Transformer({ // browsers, and because it avoids delegating extra config to the user asset.setEnvironment({ context: 'browser', + outputFormat: + asset.env.outputFormat == 'commonjs' + ? 'global' + : asset.env.outputFormat, engines: { browsers: asset.env.engines.browsers, }, @@ -362,7 +384,6 @@ export default (new Transformer({ inlineSources: true, }, includeNodeModules: asset.env.includeNodeModules, - outputFormat: asset.env.outputFormat, sourceType: asset.env.sourceType, isLibrary: asset.env.isLibrary, shouldOptimize: asset.env.shouldOptimize, @@ -390,12 +411,7 @@ export default (new Transformer({ '@parcel/transformer-webextension', 'Invalid Web Extension manifest', ); - await collectDependencies( - asset, - data, - parsed.pointers, - Boolean(options.hmrOptions), - ); + await collectDependencies(asset, data, parsed.pointers, options.hmrOptions); asset.setCode(JSON.stringify(data, null, 2)); asset.meta.webextEntry = true; return [asset]; diff --git a/packages/transformers/webextension/src/schema.js b/packages/transformers/webextension/src/schema.js index 22fbf5a0cd4..98e40e1a2e8 100644 --- a/packages/transformers/webextension/src/schema.js +++ b/packages/transformers/webextension/src/schema.js @@ -290,7 +290,6 @@ const commonProps = { }, }, }, - // sandbox is deprecated short_name: string, // FF only, but has some use sidebar_action: { @@ -449,6 +448,13 @@ export const MV3Schema = ({ additionalProperties: false, }, host_permissions: arrStr, + sandbox: { + type: 'object', + properties: { + pages: arrStr, + }, + additionalProperties: false, + }, web_accessible_resources: { type: 'array', items: { @@ -486,6 +492,7 @@ export const MV2Schema = ({ additionalProperties: false, }, browser_action: browserAction, + content_security_policy: string, page_action: { type: 'object', properties: { @@ -497,7 +504,14 @@ export const MV2Schema = ({ }, additionalProperties: false, }, - content_security_policy: string, + sandbox: { + type: 'object', + properties: { + pages: arrStr, + content_security_policy: string, + }, + additionalProperties: false, + }, web_accessible_resources: arrStr, }, required: ['manifest_version', 'name', 'version'],