Skip to content

Commit

Permalink
feat: add compatibility mode for Firefox (#776)
Browse files Browse the repository at this point in the history
* do not generate `use_dynamic_url` for Firefox

* fix `web_accessible_resources` for Firefox

* add background page support for FF

* fix: ignore `FirefoxManifestBackground`

* fix: allow `serviceWorker` to fallback to an empty string

* Added backgroundscripts support for Firefox

* Create happy-carpets-boil.md

---------

Co-authored-by: Ibiyemi Abiodun <ibiyemi@intulon.com>
Co-authored-by: Janne Holm <janne.holm@meltlake.com>
Co-authored-by: Jack Steam <jacksteamdev@gmail.com>
  • Loading branch information
4 people committed Sep 24, 2023
1 parent 3d2f4d5 commit 34980de
Show file tree
Hide file tree
Showing 9 changed files with 149 additions and 45 deletions.
5 changes: 5 additions & 0 deletions .changeset/happy-carpets-boil.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@crxjs/vite-plugin": patch
---

feat: add compatibility mode for Firefox
48 changes: 35 additions & 13 deletions packages/vite-plugin/src/node/archive/plugin-contentScripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@ export const dynamicResourcesName = '<dynamic_resource>' as const
* to all urls. This is secure enough for our purposes b/c the CRX origin is
* changed randomly each runtime reload.
*/
export const pluginContentScripts: CrxPluginFn = ({ contentScripts = {} }) => {
export const pluginContentScripts: CrxPluginFn = ({
contentScripts = {},
browser = 'chrome',
}) => {
const { hmrTimeout = 5000, injectCss = true } = contentScripts
const dynamicScriptsById = new Map<string, DynamicScriptData>()
const dynamicScriptsByLoaderRefId = new Map<string, DynamicScriptData>()
Expand Down Expand Up @@ -410,14 +413,20 @@ export const pluginContentScripts: CrxPluginFn = ({ contentScripts = {} }) => {
.filter(({ resources }) => resources.length)

// during development don't specific resources
manifest.web_accessible_resources.push({
// change the extension origin on every reload
use_dynamic_url: true,
const war: WebAccessibleResourceByMatch = {
// all web origins can access
matches: ['<all_urls>'],
// all resources are web accessible
resources: ['**/*', '*'],
})
}

if (browser !== 'firefox') {
// change the extension origin on every reload
// not allowed in FF b/c FF does this by default
war.use_dynamic_url = true
}

manifest.web_accessible_resources.push(war)
} else {
const vmAsset = bundle['manifest.json'] as OutputAsset
if (!vmAsset) throw new Error('vite manifest is missing')
Expand Down Expand Up @@ -564,33 +573,46 @@ export const pluginContentScripts: CrxPluginFn = ({ contentScripts = {} }) => {

// clean up web_accessible_resources
if (manifest.web_accessible_resources?.length) {
const war = manifest.web_accessible_resources
const wars = manifest.web_accessible_resources
manifest.web_accessible_resources = []
const map = new Map<string, Set<string>>()
for (const r of war)
if (isResourceByMatch(r)) {
for (const war of wars)
if (isResourceByMatch(war)) {
// combine resources that share match patterns
const { matches, resources, use_dynamic_url = false } = r
const { matches, resources, use_dynamic_url = false } = war
const key = [use_dynamic_url, matches.sort()]
.map((x) => JSON.stringify(x))
.join('::')
const set = map.get(key) ?? new Set()
resources.forEach((r) => set.add(r))
map.set(key, set)
} else {
// FF does not allow the use_dynamic_url key b/c the urls are always
// dynamic in FF
if (browser === 'firefox' && 'use_dynamic_url' in war)
delete war.use_dynamic_url

// don't touch resources by CRX_id
manifest.web_accessible_resources.push(r)
manifest.web_accessible_resources.push(war)
}
// rebuild combined resources
for (const [key, set] of map) {
const [use_dynamic_url, matches] = key
.split('::')
.map((x) => JSON.parse(x)) as [boolean, string[]]
manifest.web_accessible_resources.push({

const war:
| WebAccessibleResourceById
| WebAccessibleResourceByMatch = {
matches,
resources: [...set],
use_dynamic_url,
})
}

// FF does not allow the use_dynamic_url key b/c the urls are always
// dynamic in FF
if (browser !== 'firefox') war.use_dynamic_url = use_dynamic_url

manifest.web_accessible_resources.push(war)
}
} else {
// array is empty or undefined
Expand Down
8 changes: 6 additions & 2 deletions packages/vite-plugin/src/node/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ export async function manifestFiles(

const contentScripts = manifest.content_scripts?.flatMap(({ js }) => js) ?? []
const contentStyles = manifest.content_scripts?.flatMap(({ css }) => css)
const serviceWorker = manifest.background?.service_worker

const serviceWorker = manifest.background && "service_worker" in manifest.background ? manifest.background.service_worker : undefined;
const backgroundScripts = manifest.background && "scripts" in manifest.background ? manifest.background.scripts : undefined;
const background = serviceWorker ? [serviceWorker].filter(isString) : backgroundScripts ? backgroundScripts.filter(isString) : [];

const htmlPages = htmlFiles(manifest)

const icons = [
Expand Down Expand Up @@ -55,7 +59,7 @@ export async function manifestFiles(
icons: [...new Set(icons)].filter(isString),
locales: [...new Set(locales)].filter(isString),
rulesets: [...new Set(rulesets)].filter(isString),
background: [serviceWorker].filter(isString),
background: background,
webAccessibleResources,
}
}
Expand Down
16 changes: 12 additions & 4 deletions packages/vite-plugin/src/node/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ export interface WebAccessibleResourceById {
use_dynamic_url?: boolean
}

export interface ChromeManifestBackground {
service_worker: string
type?: 'module' // If the service worker uses ES modules
}

export interface FirefoxManifestBackground {
scripts: string[]
persistent?: false
}

export interface ManifestV3 {
// Required
manifest_version: number
Expand All @@ -32,10 +42,8 @@ export interface ManifestV3 {
action?: chrome.runtime.ManifestAction | undefined
author?: string | undefined
background?:
| {
service_worker: string
type?: 'module' // If the service worker uses ES modules
}
| ChromeManifestBackground
| FirefoxManifestBackground
| undefined
chrome_settings_overrides?:
| {
Expand Down
52 changes: 41 additions & 11 deletions packages/vite-plugin/src/node/plugin-background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import workerHmrClient from 'client/es/hmr-client-worker.ts'
import { ResolvedConfig } from 'vite'
import { defineClientValues } from './defineClientValues'
import { getFileName } from './fileWriter-utilities'
import type { CrxPluginFn } from './types'
import { ChromeManifestBackground, FirefoxManifestBackground } from './manifest'
import { getOptions } from './plugin-optionsProvider'
import type { Browser, CrxPluginFn } from './types'
import { workerClientId } from './virtualFileIds'

/**
Expand All @@ -21,6 +23,7 @@ import { workerClientId } from './virtualFileIds'
*/
export const pluginBackground: CrxPluginFn = () => {
let config: ResolvedConfig
let browser: Browser

return [
{
Expand All @@ -43,24 +46,45 @@ export const pluginBackground: CrxPluginFn = () => {
name: 'crx:background-loader-file',
// this should happen after other plugins; the loader file is an implementation detail
enforce: 'post',
async config(config) {
const opts = await getOptions(config)
browser = opts.browser || 'chrome'
},
configResolved(_config) {
config = _config
},
renderCrxManifest(manifest) {
const worker = manifest.background?.service_worker
const worker =
browser === 'firefox'
? (manifest.background as FirefoxManifestBackground)?.scripts[0]
: (manifest.background as ChromeManifestBackground)?.service_worker

let loader: string
if (config.command === 'serve') {
const port = config.server.port?.toString()
if (typeof port === 'undefined')
throw new Error('server port is undefined in watch mode')

// development, required to define env vars
loader = `import 'http://localhost:${port}/@vite/env';\n`
// development, required hmr client
loader += `import 'http://localhost:${port}${workerClientId}';\n`
// development, optional service worker
if (worker) loader += `import 'http://localhost:${port}/${worker}';\n`
if (browser === 'firefox') {
// in FF, our "service worker" is actually a background page so we
// can't use import statements

// development, required to define env vars
loader = `import('http://localhost:${port}/@vite/env');\n`
// development, required hmr client
loader += `import('http://localhost:${port}${workerClientId}');\n`
// development, optional service worker
if (worker)
loader += `import('http://localhost:${port}/${worker}');\n`
} else {
// development, required to define env vars
loader = `import 'http://localhost:${port}/@vite/env';\n`
// development, required hmr client
loader += `import 'http://localhost:${port}${workerClientId}';\n`
// development, optional service worker
if (worker)
loader += `import 'http://localhost:${port}/${worker}';\n`
}
} else if (worker) {
// production w/ service worker loader at root, see comment at top of file.
loader = `import './${worker}';\n`
Expand All @@ -76,9 +100,15 @@ export const pluginBackground: CrxPluginFn = () => {
source: loader,
})

manifest.background = {
service_worker: this.getFileName(refId),
type: 'module',
if (browser !== 'firefox') {
manifest.background = {
service_worker: this.getFileName(refId),
type: 'module',
}
} else {
manifest.background = {
scripts: [this.getFileName(refId)],
}
}

return manifest
Expand Down
6 changes: 3 additions & 3 deletions packages/vite-plugin/src/node/plugin-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import colors from 'picocolors'
import { OutputAsset, OutputChunk } from 'rollup'
import { ResolvedConfig } from 'vite'
import { contentScripts, hashScriptId } from './contentScripts'
import { htmlFiles, manifestFiles } from './files'
import { formatFileData, getFileName, prefix } from './fileWriter-utilities'
import { htmlFiles, manifestFiles } from './files'
import {
decodeManifest,
encodeManifest,
Expand Down Expand Up @@ -228,7 +228,7 @@ export const pluginManifest: CrxPluginFn = () => {
)
}

if (manifest.background?.service_worker) {
if (manifest.background && 'service_worker' in manifest.background) {
const file = manifest.background.service_worker
const id = join(config.root, file)
const refId = this.emitFile({
Expand Down Expand Up @@ -272,7 +272,7 @@ export const pluginManifest: CrxPluginFn = () => {
// transform hook emits files and replaces in manifest with ref ids
// update background service worker filename from ref
// service worker not emitted during development, so don't update file name
if (manifest.background?.service_worker) {
if (manifest.background && 'service_worker' in manifest.background) {
const ref = manifest.background.service_worker
const name = this.getFileName(ref)
manifest.background.service_worker = name
Expand Down
4 changes: 3 additions & 1 deletion packages/vite-plugin/src/node/plugin-optionsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { PluginOption, UserConfig } from 'vite'
import { ManifestV3Export } from './defineManifest'
import { CrxOptions, CrxPlugin } from './types'

export type CrxInputOptions = { manifest: ManifestV3Export } & CrxOptions
export interface CrxInputOptions extends CrxOptions {
manifest: ManifestV3Export
}

const pluginName = 'crx:optionsProvider'
export const pluginOptionsProvider = (options: CrxInputOptions | null) => {
Expand Down
35 changes: 28 additions & 7 deletions packages/vite-plugin/src/node/plugin-webAccessibleResources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,24 @@ import {
WebAccessibleResourceByMatch,
} from './manifest'
import { getOptions } from './plugin-optionsProvider'
import { CrxPluginFn } from './types'
import type { CrxPluginFn, Browser } from './types'

const debug = _debug('web-acc-res')

export const pluginWebAccessibleResources: CrxPluginFn = () => {
let config: ResolvedConfig
let injectCss: boolean
let browser: Browser

return [
{
name: 'crx:web-accessible-resources',
apply: 'serve',
enforce: 'post',
async config(config) {
const opts = await getOptions(config)
browser = opts.browser || 'chrome'
},
renderCrxManifest(manifest) {
// set default value for web_accessible_resources
manifest.web_accessible_resources =
Expand All @@ -40,15 +45,22 @@ export const pluginWebAccessibleResources: CrxPluginFn = () => {
}))
.filter(({ resources }) => resources.length)

// during development don't do specific resources
manifest.web_accessible_resources.push({
// change the extension origin on every reload
use_dynamic_url: true,
// during development don't specific resources
const war: WebAccessibleResourceByMatch = {
// all web origins can access
matches: ['<all_urls>'],
// all resources are web accessible
resources: ['**/*', '*'],
})
// change the extension origin on every reload
use_dynamic_url: true,
}

if (browser === 'firefox') {
// not allowed in FF b/c FF does this by default
delete war.use_dynamic_url
}

manifest.web_accessible_resources.push(war)

return manifest
},
Expand All @@ -58,7 +70,9 @@ export const pluginWebAccessibleResources: CrxPluginFn = () => {
apply: 'build',
enforce: 'post',
async config({ build, ...config }, { command }) {
const { contentScripts = {} } = await getOptions(config)
const opts = await getOptions(config)
const contentScripts = opts.contentScripts || {}
browser = opts.browser || 'chrome'
injectCss = contentScripts.injectCss ?? true

return { ...config, build: { ...build, manifest: command === 'build' } }
Expand Down Expand Up @@ -191,6 +205,13 @@ export const pluginWebAccessibleResources: CrxPluginFn = () => {
})
}

/* ------------- BROWSER COMPATIBILITY ------------- */
if (browser === 'firefox') {
for (const war of combinedResources) {
delete war.use_dynamic_url
}
}

/* --------------- CLEAN UP MANIFEST --------------- */

if (combinedResources.length === 0)
Expand Down
Loading

0 comments on commit 34980de

Please sign in to comment.