Skip to content

Commit

Permalink
feat: firefox experimental manifest V3 support (#63)
Browse files Browse the repository at this point in the history
* feat: support manifest V3 in firefox

adds a useDynamicUrlContentScripts option to toggle using use_dynamic_url in content scripts
refactors the way plugin options are passed around manifest parsers

* refactor: move web accessible script filter creation to util function

* docs: Firefox experimental manifest V3 usage instructions
  • Loading branch information
samrum authored Nov 22, 2022
1 parent a662329 commit fd62c7d
Show file tree
Hide file tree
Showing 11 changed files with 137 additions and 80 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,22 @@ The plugin will automatically default vite's `build.target` config option to the

## Usage Specific Examples

<details>
<summary>Manifest V3 usage with Firefox</summary>
For Firefox experimental manifest V3 support, there are two configurations an extension needs to make:

1. Background service workers are not supported, so you are required to use a background script.
2. The `use_dynamic_url` property is not supported for web accessible resources. In the plugin options, set the `useDynamicUrlContentScripts` option to false:

```js
webExtension({
...
useDynamicUrlContentScripts: false,
}),
```

</details>

<details>
<summary>Devtools</summary>
To add content to the browser dev tools, add `devtools_page` to your manifest
Expand Down
21 changes: 16 additions & 5 deletions src/devBuilder/devBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,36 @@
import { copy, emptyDir, ensureDir, readFile, writeFile } from "fs-extra";
import path from "path";
import { ResolvedConfig, ViteDevServer, normalizePath } from "vite";
import {
ResolvedConfig,
ViteDevServer,
normalizePath,
createFilter,
} from "vite";
import { getScriptLoaderFile } from "../utils/loader";
import { getInputFileName, getOutputFileName } from "../utils/file";
import { getVirtualModule } from "../utils/virtualModule";
import { PluginExtras } from "..";
import { addHmrSupportToCsp } from "../utils/addHmrSupportToCsp";
import { ViteWebExtensionOptions } from "../../types";
import { createWebAccessibleScriptsFilter } from "../utils/filter";

export default abstract class DevBuilder<
Manifest extends chrome.runtime.Manifest
> {
protected hmrServerOrigin = "";
protected inlineScriptHashes = new Set<string>();
protected outDir: string;
protected webAccessibleScriptsFilter: ReturnType<typeof createFilter>;

constructor(
private viteConfig: ResolvedConfig,
private pluginExtras: PluginExtras,
private pluginOptions: ViteWebExtensionOptions,
private viteDevServer?: ViteDevServer
) {
this.outDir = this.viteConfig.build.outDir;

this.webAccessibleScriptsFilter = createWebAccessibleScriptsFilter(
this.pluginOptions.webAccessibleScripts
);
}

async writeBuild({
Expand All @@ -40,7 +51,7 @@ export default abstract class DevBuilder<
await this.writeManifestContentScriptFiles(manifest);
await this.writeManifestWebAccessibleScriptFiles(
manifest,
this.pluginExtras.webAccessibleScriptsFilter
this.webAccessibleScriptsFilter
);

await this.writeBuildFiles(manifest, manifestHtmlFiles);
Expand Down Expand Up @@ -162,7 +173,7 @@ export default abstract class DevBuilder<

protected abstract writeManifestWebAccessibleScriptFiles(
manifest: Manifest,
webAccessibleScriptsFilter: PluginExtras["webAccessibleScriptsFilter"]
webAccessibleScriptsFilter: ReturnType<typeof createFilter>
): Promise<void>;

private getHmrServerOrigin(devServerPort: number): string {
Expand Down
4 changes: 2 additions & 2 deletions src/devBuilder/devBuilderManifestV2.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import crypto from "crypto";
import { ensureDir, writeFile } from "fs-extra";
import path from "path";
import { PluginExtras } from "..";
import { createFilter } from "vite";
import { getOutputFileName } from "../utils/file";
import { getScriptLoaderFile } from "../utils/loader";
import DevBuilder from "./devBuilder";
Expand Down Expand Up @@ -30,7 +30,7 @@ export default class DevBuilderManifestV2 extends DevBuilder<chrome.runtime.Mani

protected async writeManifestWebAccessibleScriptFiles(
manifest: chrome.runtime.ManifestV2,
webAccessibleScriptsFilter: PluginExtras["webAccessibleScriptsFilter"]
webAccessibleScriptsFilter: ReturnType<typeof createFilter>
) {
if (!manifest.web_accessible_resources) {
return;
Expand Down
4 changes: 2 additions & 2 deletions src/devBuilder/devBuilderManifestV3.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ensureDir, writeFile } from "fs-extra";
import path from "path";
import { PluginExtras } from "..";
import { createFilter } from "vite";
import { getOutputFileName } from "../utils/file";
import {
getScriptLoaderFile,
Expand Down Expand Up @@ -52,7 +52,7 @@ export default class DevBuilderManifestV3 extends DevBuilder<chrome.runtime.Mani

protected async writeManifestWebAccessibleScriptFiles(
manifest: chrome.runtime.ManifestV3,
webAccessibleScriptsFilter: PluginExtras["webAccessibleScriptsFilter"]
webAccessibleScriptsFilter: ReturnType<typeof createFilter>
) {
if (!manifest.web_accessible_resources) {
return;
Expand Down
15 changes: 1 addition & 14 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { createFilter } from "vite";
import type { EmittedFile, OutputBundle } from "rollup";
import type { Plugin, ResolvedConfig } from "vite";
import type { ViteWebExtensionOptions } from "../types";
Expand All @@ -12,10 +11,6 @@ import {
updateConfigForExtensionSupport,
} from "./utils/vite";

export interface PluginExtras {
webAccessibleScriptsFilter: ReturnType<typeof createFilter>;
}

export default function webExtension(
pluginOptions: ViteWebExtensionOptions
): Plugin {
Expand All @@ -29,13 +24,6 @@ export default function webExtension(
| ManifestParser<chrome.runtime.ManifestV2>
| ManifestParser<chrome.runtime.ManifestV3>;

const webConfig = pluginOptions.webAccessibleScripts;
let webAccessibleScriptsFilter = createFilter(
webConfig?.include || /\.([cem]?js|ts)$/,
webConfig?.exclude || "",
webConfig?.options
);

return {
name: "webExtension",
enforce: "post", // required to revert vite asset self.location transform to import.meta.url
Expand All @@ -59,8 +47,7 @@ export default function webExtension(

async options(options) {
manifestParser = ManifestParserFactory.getParser(
JSON.parse(JSON.stringify(pluginOptions.manifest)),
{ webAccessibleScriptsFilter },
pluginOptions,
viteConfig
);

Expand Down
60 changes: 56 additions & 4 deletions src/manifestParser/manifestParser.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { createFilter } from "vite";
import { readFileSync } from "fs-extra";
import { ResolvedConfig, ViteDevServer } from "vite";
import DevBuilder from "../devBuilder/devBuilder";
Expand All @@ -8,7 +9,10 @@ import {
getWebAccessibleScriptLoaderForOutputChunk,
} from "../utils/loader";
import { getChunkInfoFromBundle } from "../utils/rollup";
import { PluginExtras } from "..";
import type { ViteWebExtensionOptions } from "../../types";
import { getScriptHtmlLoaderFile } from "../utils/loader";
import { setVirtualModule } from "../utils/virtualModule";
import { createWebAccessibleScriptsFilter } from "../utils/filter";

export interface ParseResult<Manifest extends chrome.runtime.Manifest> {
inputScripts: [string, string][];
Expand All @@ -19,13 +23,22 @@ export interface ParseResult<Manifest extends chrome.runtime.Manifest> {
export default abstract class ManifestParser<
Manifest extends chrome.runtime.Manifest
> {
protected inputManifest: Manifest;
protected webAccessibleScriptsFilter: ReturnType<typeof createFilter>;
protected viteDevServer: ViteDevServer | undefined;

constructor(
protected inputManifest: Manifest,
protected pluginExtras: PluginExtras,
protected pluginOptions: ViteWebExtensionOptions,
protected viteConfig: ResolvedConfig
) {}
) {
this.inputManifest = JSON.parse(
JSON.stringify(this.pluginOptions.manifest)
);

this.webAccessibleScriptsFilter = createWebAccessibleScriptsFilter(
this.pluginOptions.webAccessibleScripts
);
}

async parseInput(): Promise<ParseResult<Manifest>> {
const parseResult: ParseResult<Manifest> = {
Expand All @@ -39,6 +52,7 @@ export default abstract class ManifestParser<
this.parseInputHtmlFiles,
this.parseInputContentScripts,
this.parseInputWebAccessibleScripts,
this.parseInputBackgroundScripts,
...this.getParseInputMethods()
);
}
Expand Down Expand Up @@ -281,4 +295,42 @@ export default abstract class ManifestParser<

return metadata;
}

private parseInputBackgroundScripts(
result: ParseResult<Manifest>
): ParseResult<Manifest> {
// @ts-expect-error - Force support of event pages in manifest V3
if (!result.manifest.background?.scripts) {
return result;
}

const htmlLoaderFile = getScriptHtmlLoaderFile(
"background",
// @ts-expect-error - Force support of event pages in manifest V3
result.manifest.background.scripts.map((script) => {
if (/^[\.\/]/.test(script)) {
return script;
}

return `/${script}`;
})
);

const inputFile = getInputFileName(
htmlLoaderFile.fileName,
this.viteConfig.root
);
const outputFile = getOutputFileName(htmlLoaderFile.fileName);

result.inputScripts.push([outputFile, inputFile]);

setVirtualModule(inputFile, htmlLoaderFile.source);

// @ts-expect-error - Force support of event pages in manifest V3
delete result.manifest.background.scripts;
// @ts-expect-error - Force support of event pages in manifest V3
result.manifest.background.page = htmlLoaderFile.fileName;

return result;
}
}
11 changes: 5 additions & 6 deletions src/manifestParser/manifestParserFactory.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
import { ResolvedConfig } from "vite";
import { PluginExtras } from "..";
import { ViteWebExtensionOptions } from "../../types";
import ManifestParser from "./manifestParser";
import ManifestV2 from "./manifestV2";
import ManifestV3 from "./manifestV3";

export default class ManifestParserFactory {
static getParser(
manifest: chrome.runtime.Manifest,
pluginExtras: PluginExtras,
pluginOptions: ViteWebExtensionOptions,
viteConfig: ResolvedConfig
):
| ManifestParser<chrome.runtime.ManifestV2>
| ManifestParser<chrome.runtime.ManifestV3> {
switch (manifest.manifest_version) {
switch (pluginOptions.manifest.manifest_version) {
case 2:
return new ManifestV2(manifest, pluginExtras, viteConfig);
return new ManifestV2(pluginOptions, viteConfig);
case 3:
return new ManifestV3(manifest, pluginExtras, viteConfig);
return new ManifestV3(pluginOptions, viteConfig);
default:
throw new Error(
`No parser available for manifest_version ${
Expand Down
44 changes: 4 additions & 40 deletions src/manifestParser/manifestV2.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { getScriptHtmlLoaderFile } from "../utils/loader";
import { setVirtualModule } from "../utils/virtualModule";
import { ParseResult } from "./manifestParser";
import {
isSingleHtmlFilename,
Expand All @@ -18,7 +16,7 @@ export default class ManifestV2 extends ManifestParser<Manifest> {
protected createDevBuilder(): DevBuilder<Manifest> {
return new DevBuilderManifestV2(
this.viteConfig,
this.pluginExtras,
this.pluginOptions,
this.viteDevServer
);
}
Expand All @@ -39,7 +37,7 @@ export default class ManifestV2 extends ManifestParser<Manifest> {
protected getParseInputMethods(): ((
result: ManifestParseResult
) => ManifestParseResult)[] {
return [this.parseInputBackgroundScripts];
return [];
}

protected getParseOutputMethods(): ((
Expand All @@ -48,40 +46,6 @@ export default class ManifestV2 extends ManifestParser<Manifest> {
return [this.parseWatchModeSupport.bind(this)];
}

private parseInputBackgroundScripts(
result: ManifestParseResult
): ManifestParseResult {
if (!result.manifest.background?.scripts) {
return result;
}

const htmlLoaderFile = getScriptHtmlLoaderFile(
"background",
result.manifest.background.scripts.map((script) => {
if (/^[\.\/]/.test(script)) {
return script;
}

return `/${script}`;
})
);

const inputFile = getInputFileName(
htmlLoaderFile.fileName,
this.viteConfig.root
);
const outputFile = getOutputFileName(htmlLoaderFile.fileName);

result.inputScripts.push([outputFile, inputFile]);

setVirtualModule(inputFile, htmlLoaderFile.source);

delete result.manifest.background.scripts;
result.manifest.background.page = htmlLoaderFile.fileName;

return result;
}

protected parseInputWebAccessibleScripts(
result: ParseResult<Manifest>
): ParseResult<Manifest> {
Expand All @@ -91,7 +55,7 @@ export default class ManifestV2 extends ManifestParser<Manifest> {
const inputFile = getInputFileName(resource, this.viteConfig.root);
const outputFile = getOutputFileName(resource);

if (this.pluginExtras.webAccessibleScriptsFilter(inputFile)) {
if (this.webAccessibleScriptsFilter(inputFile)) {
result.inputScripts.push([outputFile, inputFile]);
}
});
Expand Down Expand Up @@ -144,7 +108,7 @@ export default class ManifestV2 extends ManifestParser<Manifest> {
for (const resource of result.manifest.web_accessible_resources) {
if (
resource.includes("*") ||
!this.pluginExtras.webAccessibleScriptsFilter(resource)
!this.webAccessibleScriptsFilter(resource)
) {
continue;
}
Expand Down
Loading

0 comments on commit fd62c7d

Please sign in to comment.