Skip to content

Commit

Permalink
Add support for Web Extension manifest v3 (#7050)
Browse files Browse the repository at this point in the history
  • Loading branch information
101arrowz committed Apr 11, 2022
1 parent be0929a commit af7bada
Show file tree
Hide file tree
Showing 29 changed files with 943 additions and 387 deletions.
3 changes: 2 additions & 1 deletion packages/configs/webextension/index.json
Expand Up @@ -4,7 +4,8 @@
"manifest.json": ["@parcel/transformer-webextension"],
"raw:*": ["@parcel/transformer-raw"]
},
"runtimes": ["...", "@parcel/runtime-webextension"],
"packagers": {
"manifest.json": "@parcel/packager-raw-url"
"manifest.json": "@parcel/packager-webextension"
}
}
3 changes: 2 additions & 1 deletion packages/configs/webextension/package.json
Expand Up @@ -16,7 +16,8 @@
"main": "index.json",
"dependencies": {
"@parcel/config-default": "2.4.1",
"@parcel/packager-raw-url": "2.4.1",
"@parcel/packager-webextension": "2.4.1",
"@parcel/runtime-webextension": "2.4.1",
"@parcel/transformer-raw": "2.4.1",
"@parcel/transformer-webextension": "2.4.1"
}
Expand Down
@@ -0,0 +1,3 @@
{
"extends": ["@parcel/config-webextension"]
}
Empty file.
@@ -0,0 +1 @@
alert('File test alert');
@@ -0,0 +1,3 @@
h1 {
font-family: "Comic Sans MS";
}
@@ -0,0 +1,20 @@
{
"name": "MV3 Migration - content script example",
"description": "Source: https://github.com/GoogleChrome/chrome-extensions-samples",
"version": "0.1",
"manifest_version": 3,
"background": {
"service_worker": "background.js"
},
"permissions": [
"scripting",
"activeTab"
],
"content_scripts": [{
"matches": ["https://*.google.com/*"],
"js": ["other-content-script.js"]
}],
"action": {
"default_popup": "popup.html"
}
}
@@ -0,0 +1 @@
import './injected.css';
@@ -0,0 +1,19 @@
* {
box-sizing: border-box;
}
html,
body,
main {
height: 100%;
margin: 0;
padding: 0;
}
body {
min-width: 20em;
min-height: 10em;
}
main {
padding: 1em .5em;
display: grid;
place-items: center;
}
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="popup.css">
<script src="popup.js" type="module"></script>
</head>
<body>
<main>
<div>
<button id="inject-file">Inject file</button>
</div>
<div>
<button id="inject-function">Inject function</button>
</div>
</main>
</body>
</html>
@@ -0,0 +1,33 @@
import contentScript from 'url:./content-script.js';
let injectFile = document.getElementById('inject-file');
let injectFunction = document.getElementById('inject-function');

async function getCurrentTab() {
let queryOptions = { active: true, currentWindow: true };
let [tab] = await chrome.tabs.query(queryOptions);
return tab;
}

injectFile.addEventListener('click', async () => {
let tab = await getCurrentTab();

chrome.scripting.executeScript({
target: {tabId: tab.id},
files: [contentScript]
});
});

function showAlert(givenName) {
alert(`Hello, ${givenName}`);
}

injectFunction.addEventListener('click', async () => {
let tab = await getCurrentTab();

let name = 'World';
chrome.scripting.executeScript({
target: {tabId: tab.id},
func: showAlert,
args: [name]
});
});
Empty file.
@@ -0,0 +1 @@
This is to cover files like .DS_Store
57 changes: 47 additions & 10 deletions packages/core/integration-tests/test/webextension.js
@@ -1,6 +1,6 @@
import assert from 'assert';
import path from 'path';
import {bundle, assertBundles, outputFS} from '@parcel/test-utils';
import {bundle, assertBundles, outputFS, distDir} from '@parcel/test-utils';

describe('webextension', function () {
it('should resolve a full webextension bundle', async function () {
Expand All @@ -25,7 +25,6 @@ describe('webextension', function () {
assets: ['manifest.json'],
},
{
name: 'background.js',
assets: ['background.ts'],
},
{assets: ['a.txt']},
Expand All @@ -37,6 +36,24 @@ describe('webextension', function () {
{assets: ['content.js']},
{assets: ['content.css']},
]);
assert(
await outputFS.exists(
path.join(distDir, '_locales', 'en_US', 'messages.json'),
),
);
const manifest = JSON.parse(
await outputFS.readFile(
b.getBundles().find(b => b.name == 'manifest.json').filePath,
'utf8',
),
);
const scripts = manifest.background.scripts;
assert.equal(scripts.length, 1);
assert(
(
await outputFS.readFile(path.join(distDir, scripts[0]), 'utf-8')
).includes('Hello Parcel!'),
);
});

it('should resolve the web_accessible_resources globs', async function () {
Expand All @@ -52,15 +69,12 @@ describe('webextension', function () {
assets: ['manifest.json'],
},
{
name: 'index.js',
assets: ['index.ts', 'esmodule-helpers.js'],
},
{
name: 'other.js',
assets: ['other.ts', 'esmodule-helpers.js'],
},
{
name: 'index-jsx.js',
assets: [
'esmodule-helpers.js',
'index-jsx.jsx',
Expand All @@ -78,12 +92,35 @@ describe('webextension', function () {
),
);
const war = manifest.web_accessible_resources;
assert.deepEqual(war, [
'/injected/index.js',
'/injected/nested/other.js',
'/injected/index-jsx.js',
'/injected/single.js',
assert.equal(war.length, 4);
});
it('should support web extension manifest v3', async function () {
let b = await bundle(
path.join(__dirname, '/integration/webextension-mv3/manifest.json'),
);
assertBundles(b, [
{
name: 'manifest.json',
assets: ['manifest.json'],
},
{assets: ['background.js']},
{assets: ['popup.html']},
{assets: ['popup.css']},
{assets: ['popup.js', 'esmodule-helpers.js', 'bundle-url.js']},
{assets: ['content-script.js']},
{assets: ['other-content-script.js']},
{assets: ['injected.css']},
]);
const manifest = JSON.parse(
await outputFS.readFile(path.join(distDir, 'manifest.json'), 'utf-8'),
);
const css = manifest.content_scripts[0].css;
assert.equal(css.length, 1);
assert(
(await outputFS.readFile(path.join(distDir, css[0]), 'utf-8')).includes(
'Comic Sans MS',
),
);
});
// TODO: Test error-checking
});
10 changes: 7 additions & 3 deletions packages/core/types/index.js
Expand Up @@ -186,7 +186,7 @@ export type EnvironmentOptions = {|
*/
export type VersionMap = {
[string]: string,
...
...,
};

export type EnvironmentFeature =
Expand Down Expand Up @@ -398,7 +398,9 @@ export interface AssetSymbols // eslint-disable-next-line no-undef
* This is the default state.
*/
+isCleared: boolean;
get(exportSymbol: Symbol): ?{|
get(
exportSymbol: Symbol,
): ?{|
local: Symbol,
loc: ?SourceLocation,
meta?: ?Meta,
Expand Down Expand Up @@ -441,7 +443,9 @@ export interface MutableDependencySymbols // eslint-disable-next-line no-undef
* This is the default state.
*/
+isCleared: boolean;
get(exportSymbol: Symbol): ?{|
get(
exportSymbol: Symbol,
): ?{|
local: Symbol,
loc: ?SourceLocation,
isWeak: boolean,
Expand Down
27 changes: 27 additions & 0 deletions packages/packagers/webextension/package.json
@@ -0,0 +1,27 @@
{
"name": "@parcel/packager-webextension",
"version": "2.4.1",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"repository": {
"type": "git",
"url": "https://github.com/parcel-bundler/parcel.git"
},
"main": "lib/WebExtensionPackager.js",
"source": "src/WebExtensionPackager.js",
"engines": {
"node": ">=12.0.0",
"parcel": "^2.4.0"
},
"dependencies": {
"@parcel/plugin": "2.4.1",
"@parcel/utils": "2.4.1",
"nullthrows": "^1.1.1"
}
}
76 changes: 76 additions & 0 deletions packages/packagers/webextension/src/WebExtensionPackager.js
@@ -0,0 +1,76 @@
// @flow strict-local

import assert from 'assert';
import nullthrows from 'nullthrows';
import {Packager} from '@parcel/plugin';
import {replaceURLReferences, relativeBundlePath} from '@parcel/utils';

export default (new Packager({
async package({bundle, bundleGraph}) {
let assets = [];
bundle.traverseAssets(asset => {
assets.push(asset);
});

assert.equal(
assets.length,
1,
'Web extension manifest bundles must only contain one asset',
);
const asset = assets[0];
assert(asset.meta.webextEntry === true);

const relPath = b =>
relativeBundlePath(bundle, b, {leadingDotSlash: false});

const manifest = JSON.parse(await asset.getCode());
const deps = asset.getDependencies();
const war = [];
for (const contentScript of manifest.content_scripts || []) {
const jsBundles = deps
.filter(d => contentScript.js?.includes(d.id))
.map(d => nullthrows(bundleGraph.getReferencedBundle(d, bundle)));

contentScript.css = [
...new Set(
(contentScript.css || []).concat(
jsBundles
.flatMap(b => bundleGraph.getReferencedBundles(b))
.filter(b => b.type == 'css')
.map(relPath),
),
),
];

war.push({
matches: contentScript.matches,
extension_ids: [],
resources: jsBundles
.flatMap(b => {
const children = [];
const siblings = bundleGraph.getReferencedBundles(b);
bundleGraph.traverseBundles(child => {
if (b !== child && !siblings.includes(child)) {
children.push(child);
}
}, b);
return children;
})
.map(relPath),
});
}
manifest.web_accessible_resources = (
manifest.web_accessible_resources || []
).concat(
manifest.manifest_version == 2
? [...new Set(war.flatMap(entry => entry.resources))]
: war,
);
let {contents} = replaceURLReferences({
bundle,
bundleGraph,
contents: JSON.stringify(manifest),
});
return {contents};
},
}): Packager);

0 comments on commit af7bada

Please sign in to comment.