Bug 1811443 - Support optional background.type "module" for WebExtensions Event Page scripts. r=willdurand,robwu

Differential Revision: https://phabricator.services.mozilla.com/D169922
This commit is contained in:
Luca Greco 2023-02-16 13:56:18 +00:00
parent 58c4c9c0ff
commit 94cb12435e
8 changed files with 169 additions and 3 deletions

View file

@ -316,5 +316,8 @@ dictionary WebExtensionInit {
sequence<DOMString>? backgroundScripts = null;
DOMString? backgroundWorkerScript = null;
// Whether the background scripts should be loaded as ES modules.
boolean backgroundTypeModule = false;
Promise<WebExtensionPolicy?> readyPromise;
};

View file

@ -2760,6 +2760,10 @@ class Extension extends ExtensionData {
return this.manifest.background?.scripts;
}
get backgroundTypeModule() {
return this.manifest.background?.type === "module";
}
get backgroundWorkerScript() {
return this.manifest.background?.service_worker;
}
@ -2828,6 +2832,7 @@ class Extension extends ExtensionData {
return {
backgroundScripts: this.backgroundScripts,
backgroundWorkerScript: this.backgroundWorkerScript,
backgroundTypeModule: this.backgroundTypeModule,
childModules: this.modules && this.modules.child,
dependencies: this.dependencies,
persistentBackground: this.persistentBackground,

View file

@ -142,6 +142,14 @@ ExtensionManager = {
({ backgroundWorkerScript } = getData(extension, "extendedData") || {});
}
let { backgroundTypeModule } = extension;
if (
backgroundTypeModule == null &&
WebExtensionPolicy.isExtensionProcess
) {
({ backgroundTypeModule } = getData(extension, "extendedData") || {});
}
policy = new WebExtensionPolicy({
id: extension.id,
mozExtensionHostname: extension.uuid,
@ -162,6 +170,7 @@ ExtensionManager = {
backgroundScripts,
backgroundWorkerScript,
backgroundTypeModule,
contentScripts: extension.contentScripts,
});

View file

@ -30,6 +30,10 @@ using namespace dom;
static const char kProto[] = "moz-extension";
static const char kBackgroundScriptTypeDefault[] = "text/javascript";
static const char kBackgroundScriptTypeModule[] = "module";
static const char kBackgroundPageHTMLStart[] =
"<!DOCTYPE html>\n\
<html>\n\
@ -38,7 +42,7 @@ static const char kBackgroundPageHTMLStart[] =
static const char kBackgroundPageHTMLScript[] =
"\n\
<script type=\"text/javascript\" src=\"%s\"></script>";
<script type=\"%s\" src=\"%s\"></script>";
static const char kBackgroundPageHTMLEnd[] =
"\n\
@ -297,6 +301,8 @@ WebExtensionPolicy::WebExtensionPolicy(GlobalObject& aGlobal,
aInit.mBackgroundScripts.Value());
}
mBackgroundTypeModule = aInit.mBackgroundTypeModule;
mContentScripts.SetCapacity(aInit.mContentScripts.Length());
for (const auto& scriptInit : aInit.mContentScripts) {
// The activeTab permission is only for dynamically injected scripts,
@ -525,11 +531,13 @@ nsCString WebExtensionPolicy::BackgroundPageHTML() const {
result.AppendLiteral(kBackgroundPageHTMLStart);
const char* scriptType = mBackgroundTypeModule ? kBackgroundScriptTypeModule
: kBackgroundScriptTypeDefault;
for (auto& script : mBackgroundScripts.Value()) {
nsCString escaped;
nsAppendEscapedHTML(NS_ConvertUTF16toUTF8(script), escaped);
result.AppendPrintf(kBackgroundPageHTMLScript, escaped.get());
result.AppendPrintf(kBackgroundPageHTMLScript, scriptType, escaped.get());
}
result.AppendLiteral(kBackgroundPageHTMLEnd);

View file

@ -368,6 +368,8 @@ class WebExtensionPolicy final : public nsISupports, public nsWrapperCache {
dom::Nullable<nsTArray<nsString>> mBackgroundScripts;
bool mBackgroundTypeModule = false;
nsTArray<RefPtr<WebExtensionContentScript>> mContentScripts;
RefPtr<dom::Promise> mReadyPromise;

View file

@ -148,6 +148,11 @@
"type": "array",
"items": { "$ref": "ExtensionURL" }
},
"type": {
"optional": true,
"type": "string",
"enum": ["module", "classic"]
},
"persistent": {
"optional": true,
"type": "boolean",

View file

@ -0,0 +1,133 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
async function assertBackgroundScriptTypes(
extensionTestWrapper,
expectedScriptTypesMap
) {
const { baseURI } = extensionTestWrapper.extension;
let expectedMapWithResolvedURLs = Object.keys(expectedScriptTypesMap).reduce(
(result, scriptPath) => {
result[baseURI.resolve(scriptPath)] = expectedScriptTypesMap[scriptPath];
return result;
},
{}
);
const page = await ExtensionTestUtils.loadContentPage(
baseURI.resolve("_generated_background_page.html")
);
const scriptTypesMap = await page.spawn([], () => {
const scripts = Array.from(
this.content.document.querySelectorAll("script")
);
return scripts.reduce((result, script) => {
result[script.getAttribute("src")] = script.getAttribute("type");
return result;
}, {});
});
await page.close();
Assert.deepEqual(
scriptTypesMap,
expectedMapWithResolvedURLs,
"Got the expected script type from the generated background page"
);
}
async function testBackgroundScriptClassic({ manifestTypeClassicSet }) {
const extension = ExtensionTestUtils.loadExtension({
manifest: {
background: {
scripts: ["anotherScript.js", "main.js"],
type: manifestTypeClassicSet ? "classic" : undefined,
},
},
files: {
"main.js": ``,
"anotherScript.js": ``,
},
});
await extension.startup();
await assertBackgroundScriptTypes(extension, {
"main.js": "text/javascript",
"anotherScript.js": "text/javascript",
});
await extension.unload();
}
add_task(async function test_background_scripts_type_default() {
await testBackgroundScriptClassic({ manifestTypeClassicSet: false });
});
add_task(async function test_background_scripts_type_classic() {
await testBackgroundScriptClassic({ manifestTypeClassicSet: true });
});
add_task(async function test_background_scripts_type_module() {
const extension = ExtensionTestUtils.loadExtension({
manifest: {
background: {
scripts: ["anotherModule.js", "mainModule.js"],
type: "module",
},
},
files: {
"mainModule.js": `
import { initBackground } from "/importedModule.js";
browser.test.log("mainModule.js - ESM module executing");
initBackground();
`,
"importedModule.js": `
export function initBackground() {
browser.test.onMessage.addListener((msg) => {
browser.test.log("importedModule.js - test message received");
browser.test.sendMessage("esm-module-reply", msg);
});
browser.test.log("importedModule.js - initBackground executed");
}
browser.test.log("importedModule.js - ESM module loaded");
`,
"anotherModule.js": `
browser.test.log("anotherModule.js - ESM module loaded");
`,
},
});
await extension.startup();
await extension.sendMessage("test-event-value");
equal(
await extension.awaitMessage("esm-module-reply"),
"test-event-value",
"Got the expected event from the ESM module loaded from the background script"
);
await assertBackgroundScriptTypes(extension, {
"mainModule.js": "module",
"anotherModule.js": "module",
});
await extension.unload();
});
add_task(async function test_background_scripts_type_invalid() {
const extension = ExtensionTestUtils.loadExtension({
manifest: {
background: {
scripts: ["anotherScript.js", "main.js"],
type: "invalid",
},
},
files: {
"main.js": ``,
"anotherScript.js": ``,
},
});
ExtensionTestUtils.failOnSchemaWarnings(false);
await Assert.rejects(
extension.startup(),
/Error processing background: .* \.type must be one of/,
"Expected install to fail"
);
ExtensionTestUtils.failOnSchemaWarnings(true);
});

View file

@ -28,6 +28,7 @@ skip-if = os == "android" # Android does not use Places for history.
[test_ext_background_sub_windows.js]
[test_ext_background_teardown.js]
[test_ext_background_telemetry.js]
[test_ext_background_type_module.js]
[test_ext_background_window_properties.js]
skip-if = os == "android"
[test_ext_browserSettings.js]