Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Web Extension manifest V3 #7050

Merged
merged 43 commits into from Apr 11, 2022
Merged
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
1871c28
initial work
101arrowz Oct 10, 2021
663bce3
Fix MV3
101arrowz Oct 11, 2021
967ad13
Merge branch 'v2' into v2
101arrowz Oct 12, 2021
d06a776
Merge branch 'v2' into v2
101arrowz Oct 13, 2021
cd58baa
Merge branch 'v2' into v2
101arrowz Oct 14, 2021
bf15cd3
Merge branch 'v2' into v2
101arrowz Oct 21, 2021
8c40207
working on hmr
101arrowz Oct 21, 2021
2f03e50
Merge branch 'v2' into v2
101arrowz Oct 21, 2021
5434a10
Merge branch 'v2' into v2
101arrowz Oct 25, 2021
9b5698b
Improved error messages
101arrowz Oct 25, 2021
a71db1d
fix sourceType for script background service workers
101arrowz Oct 25, 2021
fa12c2f
new tests
101arrowz Oct 25, 2021
93927b4
Merge branch 'v2' into v2
101arrowz Oct 26, 2021
d638758
Merge branch 'v2' into v2
101arrowz Oct 26, 2021
e2dc5c1
Fix lint
101arrowz Oct 26, 2021
da0a335
Merge branch 'v2' into v2
101arrowz Oct 28, 2021
6847f22
Merge branch 'v2' into v2
101arrowz Oct 29, 2021
6ec72ba
Merge branch 'v2' into v2
101arrowz Nov 13, 2021
0cb5cd7
Merge branch 'v2' into v2
101arrowz Nov 19, 2021
754f308
Merge branch 'v2' into v2
101arrowz Dec 8, 2021
b71d986
Merge branch 'v2' into v2
101arrowz Feb 10, 2022
9d4f6df
revamp
101arrowz Mar 19, 2022
8145bb7
merge
101arrowz Mar 19, 2022
d4e2227
patch versions
101arrowz Mar 19, 2022
69e355e
bugfixes
101arrowz Mar 22, 2022
49f08de
fix tests
101arrowz Mar 22, 2022
c651b44
fix lint
101arrowz Mar 22, 2022
c8522ed
Merge branch 'v2' into v2
101arrowz Mar 22, 2022
e8b4279
automatic web_accessible_resources installation, fixed url imports
101arrowz Mar 22, 2022
a9b7766
Merge branch 'v2' of https://github.com/101arrowz/parcel into v2
101arrowz Mar 22, 2022
206e863
bugfixes
101arrowz Mar 22, 2022
8040212
more bugfixes
101arrowz Mar 24, 2022
22567b0
Merge branch 'v2' into v2
101arrowz Mar 24, 2022
1b1c326
fix lint
101arrowz Mar 24, 2022
f109735
fix asset invalidation
101arrowz Mar 24, 2022
01c23d2
fix versions
101arrowz Mar 24, 2022
4bb01ec
Merge branch 'v2' into v2
101arrowz Mar 28, 2022
7ed00e4
Merge branch 'v2' into v2
101arrowz Mar 30, 2022
b40cf67
minor patches
101arrowz Apr 4, 2022
8afd736
review fixes
101arrowz Apr 5, 2022
cf429a2
improvements
101arrowz Apr 6, 2022
fa91a63
Merge branch 'v2' into v2
devongovett Apr 11, 2022
669f618
Fix dependencies
devongovett Apr 11, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.0",
"@parcel/packager-raw-url": "2.4.0",
"@parcel/packager-webextension": "2.4.0",
"@parcel/runtime-webextension": "2.4.0",
"@parcel/transformer-raw": "2.4.0",
"@parcel/transformer-webextension": "2.4.0"
}
Expand Down
@@ -0,0 +1,3 @@
{
"extends": ["@parcel/config-webextension"]
}
@@ -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]
});
});
@@ -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']},
devongovett marked this conversation as resolved.
Show resolved Hide resolved
{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.0",
"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.0",
"@parcel/utils": "2.4.0",
"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);