@thunderbirdops/webext-compat-data
Version:
Web-extension Compat Data provides a way to generate and reference web-extension compatibility data in your apps or web-extensions.
1,178 lines (1,075 loc) • 36.5 kB
JavaScript
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* Author: John Bieling
* Version: 1.0 (28.06.2024)
*
* Features:
* - clone the existing browser-compat-data as a starting point
* - read and parse all schema files
* - handle $imports and $refs (recursive refs are ignored)
* - update compat data based on the schema files, trust re-used firefox
* schemas as fully supported
* - use an override file to manually change/enrich compat data
* - auto-remove sub-entries if they all have the same compat data as the parent
* - handle unsupported
* - handle different notations and log all unexpected entries
*
* TODO:
* [ ]: Instead of excluding files, add the APIs to the override file, to be
* able to add comments.
* [ ] Programmatically extract the correct version_added value
* [ ] Do not add parents of non-nested properties
* [ ] handle deprecated
* [ ] handle manifests
*
* Note: There are 4 different notation for parameter properties:
* - flatting (tabs.create(), tabs.executeScript(), https://github.com/mdn/browser-compat-data/blob/7afd5da3bfe0e0f4434585ee75f277d784662caf/webextensions/api/tabs.json#L2002)
* - paramName_propName_parameter (browserAction.getBadgeBackgroundColor(), https://github.com/mdn/browser-compat-data/blob/7afd5da3bfe0e0f4434585ee75f277d784662caf/webextensions/api/browserAction.json#L138)
* - propName_value (tabs.update, https://github.com/mdn/browser-compat-data/blob/7afd5da3bfe0e0f4434585ee75f277d784662caf/webextensions/api/tabs.json#L3966)
* - nesting (windows.create(), https://github.com/mdn/browser-compat-data/blob/7afd5da3bfe0e0f4434585ee75f277d784662caf/webextensions/api/windows.json#L699)
*
* Issues filed:
* - (browser)action.functions.setIcon.parameters (https://github.com/mdn/browser-compat-data/pull/23543)
* - tabs.update has tabId as first parameter and updateProperties is using propName_value notation (https://github.com/mdn/browser-compat-data/pull/23544)
*/
const path = require("path");
const fs = require("fs-extra");
const yargs = require("yargs");
const jsonUtils = require("comment-json");
const bcd = require("@mdn/browser-compat-data");
// Note: Using a positive-confirm list and a known-false-positive list to have
// control over the parameter entries which are accepted in flat property notation.
// It should be a goal to remove all usages of the flat property notation in BCD.
const CONFIRMED_FLAT_PROPS = [
"action.functions.setIcon.parameters.details.properties.imageData",
"browserAction.functions.setIcon.parameters.details.properties.imageData",
"commands.functions.update.parameters.detail.properties.",
"menus.functions.create.parameters.createProperties.properties.",
"tabs.functions.create.parameters.createProperties.properties.",
"tabs.functions.executeScript.parameters.details.properties.",
"tabs.functions.insertCSS.parameters.details.properties.",
"windows.functions.update.parameters.updateInfo.properties.",
"windows.functions.getAll.parameters.getInfo.properties.",
];
const KNOWN_FALSE_POSITIVE_FLAT_PROPS = [
"bookmarks.functions.search.parameters.query.properties.query",
"topSites.types.MostVisitedURL.properties.type.enum.url",
"bookmarks.events.onCreated.parameters.bookmark.properties.id",
"bookmarks.events.onRemoved.parameters.removeInfo.properties.node.properties.",
"menus.types.OnShowData.properties.selectedAccount.properties.rootFolder.properties.",
"menus.types.OnClickData.properties.selectedAccount.properties.rootFolder.properties.",
"menus.events.onShown.parameters.info.properties.selectedAccount.properties.rootFolder.properties.",
"menus.events.onClicked.parameters.info.properties.selectedAccount.properties.rootFolder.properties.",
"menus.functions.update.parameters.updateProperties.properties.onclick.parameters.info.properties.selectedAccount.properties.rootFolder.properties.",
"menus.functions.create.parameters.createProperties.properties.onclick.parameters.info.properties.selectedAccount.properties.rootFolder.properties.",
];
const SKIP_BROWSER_SCHEMAS = ["normandyAddonStudy.json"];
const SKIP_TOOLKIT_SCHEMAS = [
// Privileged extensions only.
"activity_log.json",
"geckoProfiler.json",
"network_status.json",
"telemetry.json",
// Not usable by extensions.
"test.json",
];
const SUPPORTED_BROWSER_NAMESPACES = ["pkcs11"];
const REIMPLEMENTED_BROWSER_NAMESPACES = [
"commands",
"menus",
"sessions",
"tabs",
"windows",
];
const UNSUPPORTED_TOOLKIT_NAMESPACES = [
"pageAction",
"captivePortal",
"proxy", // Bug 1903727.
];
const REIMPLEMENTED_TOOLKIT_NAMESPACES = ["action", "browserAction", "theme"];
const HELP_SCREEN = `
Usage:
node generate_compat_data.js <options>
Required options:
--source=path - Path to a local checkout of a mozilla source
repository with a matching /comm directory.
Optional options:
--no-mailextensions - Do not add (Thunderbird-only) MailExtensions APIs.
--no-minimize - Do not minimize compat data by excluding properties
which have the same compat data as their parent
parameter.
--override=path - Path to a JSON file with compat data, which should
be enforced (for example to mark a toolkit API only
partially compatible). The applicable entries of
that data are printed to the console.
--verbosity=level - Integer representing a selection (sum) of the
requested log entries:
1: namespace definitions after $imports
2: list all entries found in the schema files
4: be verbose while updating the cloned BCD data
`;
const args = yargs.argv;
const VERBOSITY = args.verbosity ? parseInt(args.verbosity, 10) : 0;
const MINIMIZE = args.minimize ?? true;
const INCLUDE_MAILEXTENSIONS = args.mailextensions ?? true;
if (!args.source) {
console.log(HELP_SCREEN);
} else {
main();
}
// -----------------------------------------------------------------------------
async function main() {
const tcd = { webextensions: { api: {} } };
// Read override compat data.
let override;
if (args.override && fs.existsSync(args.override)) {
override = jsonUtils.parse(fs.readFileSync(args.override, "utf-8"));
}
// Read the relevant toolkit schema files, excluding internal API's.
const toolkit_namespaces = readSchemaFiles(
getJsonFiles(
path.join(args.source, "toolkit", "components", "extensions", "schemas")
).filter(e => !SKIP_TOOLKIT_SCHEMAS.includes(e.name))
);
// Read the browser schema files.
const browser_namespaces = readSchemaFiles(
getJsonFiles(
path.join(args.source, "browser", "components", "extensions", "schemas")
).filter(e => !SKIP_BROWSER_SCHEMAS.includes(e.name))
);
// Read Thunderbird's own schema files.
const mail_namespaces = readSchemaFiles(
getJsonFiles(
path.join(
args.source,
"comm",
"mail",
"components",
"extensions",
"schemas"
)
)
);
processImports(toolkit_namespaces);
processImports(browser_namespaces);
processImports(mail_namespaces);
// Clone browser-compat-data.
cloneBrowserCompatData(bcd.webextensions, tcd.webextensions);
const updateCompatDataLogEntries = new Set();
if (VERBOSITY & 2) {
console.log("");
console.log("Scanning schema files in /comm");
console.log("==============================");
}
const mail_entries = new Map();
for (const namespaceObj of mail_namespaces) {
const entries = mail_entries.get(namespaceObj.namespace) ?? new Map();
collectNamespaceEntriesAndResolveRefs(namespaceObj, entries, [
...mail_namespaces,
...toolkit_namespaces.filter(
e => !REIMPLEMENTED_TOOLKIT_NAMESPACES.includes(e.namespace)
),
]);
mail_entries.set(namespaceObj.namespace, entries);
}
if (VERBOSITY & 2) {
console.log("");
console.log("Scanning schema files in /toolkit");
console.log("=================================");
}
for (const namespaceObj of toolkit_namespaces) {
const entries = new Map();
collectNamespaceEntriesAndResolveRefs(namespaceObj, entries, [
...browser_namespaces,
...toolkit_namespaces,
]);
const mail_entry = mail_entries.get(namespaceObj.namespace);
for (const [namespace_entry, value] of entries) {
let expected;
if (UNSUPPORTED_TOOLKIT_NAMESPACES.includes(namespaceObj.namespace)) {
expected = false;
} else if (
REIMPLEMENTED_TOOLKIT_NAMESPACES.includes(namespaceObj.namespace)
) {
// Check how Thunderbird reimplemented the namespace.
expected =
mail_entry.has(namespace_entry) &&
!mail_entry.get(namespace_entry).unsupported;
} else {
// The data copied from BCD should be fine for APIs which Thunderbird
// re-uses from mozilla-central. Unexpected differences are provided by
// the overlay file.
continue;
}
updateCompatData(
tcd,
namespace_entry,
{ version_added: expected },
updateCompatDataLogEntries
);
}
}
if (VERBOSITY & 2) {
console.log("");
console.log("Scanning schema files in /browser");
console.log("=================================");
}
for (const namespaceObj of browser_namespaces) {
const entries = new Map();
collectNamespaceEntriesAndResolveRefs(namespaceObj, entries, [
...browser_namespaces,
...toolkit_namespaces,
]);
const mail_entry = mail_entries.get(namespaceObj.namespace);
for (const [namespace_entry, value] of entries) {
let expected;
if (SUPPORTED_BROWSER_NAMESPACES.includes(namespaceObj.namespace)) {
// The data copied from BCD should be fine for APIs which Thunderbird
// cloned from mozilla-central. Unexpected differences are provided by
// the overlay file.
continue;
} else if (
REIMPLEMENTED_BROWSER_NAMESPACES.includes(namespaceObj.namespace)
) {
// Check how Thunderbird reimplemented the namespace.
expected =
mail_entry.has(namespace_entry) &&
!mail_entry.get(namespace_entry).unsupported;
} else {
// All other browser APIs are not supported.
expected = false;
}
updateCompatData(
tcd,
namespace_entry,
{ version_added: expected },
updateCompatDataLogEntries
);
}
}
// Check Thunderbird's own data, redo the re-implemented namespaces as well,
// to check for added elements.
if (INCLUDE_MAILEXTENSIONS) {
for (const namespaceObj of mail_namespaces) {
const entries = mail_entries.get(namespaceObj.namespace);
const isReimplemented =
REIMPLEMENTED_TOOLKIT_NAMESPACES.includes(namespaceObj.namespace) ||
REIMPLEMENTED_BROWSER_NAMESPACES.includes(namespaceObj.namespace);
for (const [namespace_entry, value] of entries) {
const expected = !value.unsupported;
updateCompatData(
tcd,
namespace_entry,
{ version_added: expected },
updateCompatDataLogEntries,
!isReimplemented
);
}
}
}
// Log entries collected in updateCompatData.
updateCompatDataLogEntries.forEach(e => console.log(e));
if (override) {
// Output the actually overridden values, to help minimize the override file.
console.log(
JSON.stringify(
{
webextensions: overrideBrowserCompatData(
override.webextensions,
tcd.webextensions
),
},
null,
2
)
);
}
const browser_compat_data = sortKeys(mergeObjects(bcd, tcd));
if (MINIMIZE) {
reduceBrowserCompatData(browser_compat_data.webextensions);
}
// Final check, this should not report errors.
checkForMissingThunderbirdEntries(
browser_compat_data.webextensions,
"webextensions"
);
// Write modified webextension BCD.
await writePrettyJSONFile(
INCLUDE_MAILEXTENSIONS
? "thunderbird_mailextensions.json"
: "thunderbird_webextensions.json",
{ webextensions: browser_compat_data.webextensions }
);
// Write modified webextension BCD (single file per namespace).
const apiDirectory = INCLUDE_MAILEXTENSIONS
? path.join("thunderbird_mailextensions", "api")
: path.join("thunderbird_webextensions", "api");
if (!fs.existsSync(apiDirectory)) {
fs.mkdirSync(apiDirectory, { recursive: true });
}
for (const file of await fs.readdir(apiDirectory)) {
await fs.unlink(path.join(apiDirectory, file));
}
for (const namespaceName of Object.keys(
browser_compat_data.webextensions.api
)) {
await writePrettyJSONFile(
path.join(apiDirectory, `${namespaceName}.json`),
browser_compat_data.webextensions.api[namespaceName]
);
}
}
// -----------------------------------------------------------------------------
// Recursive merge, to is modified.
function mergeObjects(to, from) {
for (const n in from) {
if (typeof to[n] != "object") {
to[n] = from[n];
} else if (typeof from[n] == "object") {
to[n] = mergeObjects(to[n], from[n]);
}
}
return to;
}
function sortKeys(x) {
if (typeof x !== "object" || !x) {
return x;
}
if (Array.isArray(x)) {
return x.map(sortKeys);
}
return Object.keys(x)
.sort()
.reduce((o, k) => ({ ...o, [k]: sortKeys(x[k]) }), {});
}
function getJsonFiles(folderPath) {
return fs
.readdirSync(folderPath, { withFileTypes: true })
.filter(
item =>
!item.isDirectory() && path.extname(item.name).toLowerCase() === ".json"
);
}
function readSchemaFiles(files) {
const schemas = [];
const namespaces = [{ namespace: "manifest" }];
for (const file of files) {
const json = jsonUtils.parse(
fs.readFileSync(path.join(file.path, file.name), "utf-8")
);
schemas.push({
file,
json,
});
}
for (const schema of schemas) {
for (const namespaceObj of schema.json) {
// Merge manifest namespaces.
const namespaceName = namespaceObj.namespace;
if (namespaceName == "manifest") {
const manifestObj = namespaces[0];
for (const key of Object.keys(namespaceObj)) {
if (key == "namespace") {
continue;
}
if (Array.isArray(namespaceObj[key])) {
if (manifestObj[key]) {
manifestObj[key].push(...namespaceObj[key]);
} else {
manifestObj[key] = [...namespaceObj[key]];
}
} else {
console.error(`Error: ${namespaceName}.${key} cannot be merged`);
}
}
namespaces[0] = manifestObj;
} else {
namespaces.push(namespaceObj);
}
}
}
return namespaces;
}
/**
* Helper function to produce pretty JSON files.
*
* @param {string} path - The path to write the JSON to.
* @param {obj} json - The obj to write into the file.
*/
async function writePrettyJSONFile(path, json) {
try {
return await fs.outputFile(path, JSON.stringify(json, null, 4));
} catch (err) {
console.log("Error in writePrettyJSONFile()", path, err);
throw err;
}
}
// -----------------------------------------------------------------------------
// Copy "firefox" entries from the BCD data as "thunderbird" entries into the
// TCD data.
function cloneBrowserCompatData(bcdEntry, tcdEntry) {
if (typeof bcdEntry !== "object" || !bcdEntry) {
console.error(
`Error: Should not find an non-object entry in BCD data: ${bcdEntry}`
);
return;
}
if (Array.isArray(bcdEntry)) {
console.error(
`Error: Should not find an array entry in BCD data: ${bcdEntry}`
);
return;
}
Object.keys(bcdEntry).forEach(k => {
if (!tcdEntry[k]) {
tcdEntry[k] = {};
}
if (k == "__compat") {
const version_added =
bcdEntry[k].support?.firefox?.version_added || false;
tcdEntry[k] = { support: { thunderbird: { version_added } } };
} else {
cloneBrowserCompatData(bcdEntry[k], tcdEntry[k]);
}
});
}
// Copy/update "thunderbird" entries from the override data into the TCD data.
function overrideBrowserCompatData(
overrideEntry,
tcdEntry,
parent = "webextensions"
) {
if (typeof overrideEntry !== "object" || !overrideEntry) {
console.error(
`Error: Should not find an non-object entry in BCD data: ${overrideEntry}`
);
return undefined;
}
if (Array.isArray(overrideEntry)) {
console.error(
`Error: Should not find an array entry in BCD data: ${overrideEntry}`
);
return undefined;
}
let modifiedEntry;
Object.keys(overrideEntry).forEach(k => {
if (k == "__compat") {
const tcd = tcdEntry[k]?.support?.thunderbird;
const override = overrideEntry[k]?.support?.thunderbird;
// When not to replace?
// - override is empty
// - override.version_added is empty
// - tcd.version_added is a string and bdc.version_added is true
// - no change
if (
!(
!override ||
typeof override.version_added === "undefined" ||
tcd?.version_added === override.version_added ||
(typeof tcd?.version_added === "string" &&
override.version_added === true)
)
) {
if (!modifiedEntry) {
modifiedEntry = {};
}
modifiedEntry[k] = overrideEntry[k];
tcdEntry[k] = { support: { thunderbird: override } };
}
} else if (tcdEntry[k]) {
const childMod = overrideBrowserCompatData(
overrideEntry[k],
tcdEntry[k],
`${parent}.${k}`
);
if (childMod) {
if (!modifiedEntry) {
modifiedEntry = {};
}
modifiedEntry[k] = childMod;
}
} else {
tcdEntry[k] = overrideEntry[k];
if (!modifiedEntry) {
modifiedEntry = {};
}
modifiedEntry[k] = overrideEntry[k];
}
});
return modifiedEntry;
}
// Remove properties, if all properties have the same version_added data as the
// parent. Do not remove entries with additional __compat data.
function reduceBrowserCompatData(tcdEntry, parentKey, path = "") {
/*
TODO:
"details": {
"__compat": {
"support": {
"thunderbird": {
"version_added": true
}
}
}
},
"details_tabId_parameter": {
"__compat": {
"support": {
"thunderbird": {
"version_added": true
}
}
}
},
*/
if (typeof tcdEntry !== "object" || !tcdEntry) {
console.error(
`Error: Should not find an non-object entry in BCD data: ${tcdEntry}`
);
return undefined;
}
if (Array.isArray(tcdEntry)) {
console.error(
`Error: Should not find an array entry in BCD data: ${tcdEntry}`
);
return undefined;
}
const parentCompatStrings = new Set();
const parentCompatEntries = sortKeys(tcdEntry?.__compat?.support);
let parentCompatVendors = [];
let parentCompatString = "";
if (parentCompatEntries) {
parentCompatVendors = Object.keys(parentCompatEntries);
parentCompatString = JSON.stringify(parentCompatEntries);
parentCompatStrings.add(parentCompatString);
}
// Check all children and remove children which have the same version_added
// entries as the parent. Make sure to only remove thunderbird entries and only
// if no other vendors are listed.
Object.keys(tcdEntry).forEach(k => {
if (k != "__compat") {
const childCompatStrings = [
...reduceBrowserCompatData(tcdEntry[k], k, `${path}.${k}`),
];
// Add the childs compat data to the global parent compat data.
childCompatStrings.forEach(e => parentCompatStrings.add(e));
// Compare this child with parent directly.
if (
path.split(".").length > 3 &&
parentCompatVendors.length == 1 &&
parentCompatVendors[0] == "thunderbird" &&
childCompatStrings.length == 1 &&
childCompatStrings[0] == parentCompatString
) {
delete tcdEntry[k];
}
}
});
return parentCompatStrings;
}
function checkForMissingThunderbirdEntries(tcdEntry, path = "") {
if (typeof tcdEntry !== "object" || !tcdEntry) {
console.error(
`Error: Should not find an non-object entry in BCD data: ${tcdEntry}`
);
return;
}
if (Array.isArray(tcdEntry)) {
console.error(
`Error: Should not find an array entry in BCD data: ${tcdEntry}`
);
return;
}
// If this entity is unsupported, do not dive into its properties.
const isSupported = tcdEntry?.__compat?.support?.thunderbird?.version_added;
if (isSupported === false) {
return;
}
Object.keys(tcdEntry).forEach(k => {
if (k == "__compat") {
const support = tcdEntry[k]?.support;
if (support) {
const vendors = Object.keys(support);
if (!vendors.includes("thunderbird")) {
console.error(
`Error: Missing thunderbird entry in: ${path} (${vendors})`
);
}
}
} else {
checkForMissingThunderbirdEntries(tcdEntry[k], `${path}.${k}`);
}
});
}
// -----------------------------------------------------------------------------
function processImports(namespaces) {
for (let i = 0; i < namespaces.length; i++) {
const namespaceObj = namespaces[i];
namespaces[i] = _processImports(namespaceObj, namespaces);
}
if (VERBOSITY & 1) {
console.log("");
console.log("Found namespace definitions after $import");
console.log("=========================================");
for (const namespaceObj of namespaces) {
console.log(JSON.stringify(namespaceObj, null, 2));
}
}
}
/**
* Helper function to find an element or namespace in the provided schema obj.
*
* @param {any} value - The value to process. Usually the global schema data, but
* the function recursively calls itself on nested elements.
* @param {string} searchString - The id or namespace name to look for.
*
* @returns {any} The processed value.
*/
function getNestedIdOrNamespace(value, searchString) {
if (typeof value !== "object") {
return undefined;
}
if (Array.isArray(value)) {
for (const element of value) {
const rv = getNestedIdOrNamespace(element, searchString);
if (rv !== undefined) {
return rv;
}
}
return undefined;
}
// An object.
if (value.namespace == searchString) {
return value;
}
if (value.id == searchString) {
return value;
}
for (const element of Object.values(value)) {
const rv = getNestedIdOrNamespace(element, searchString);
if (rv !== undefined) {
return rv;
}
}
return undefined;
}
function collectNamespaceEntriesAndResolveRefs(
value,
entries,
all_namespaces,
parentKey,
handledRefs = [],
fullPath = ""
) {
if (typeof value !== "object") {
if (parentKey == "enum") {
entries.set(`${fullPath}.${value}`, value);
}
return;
}
if (Array.isArray(value)) {
value.forEach(e => {
collectNamespaceEntriesAndResolveRefs(
e,
entries,
all_namespaces,
parentKey,
[...handledRefs],
fullPath
);
});
return;
}
// Looks like value is an object. Find out where we are and build the path.
if (value.namespace) {
// Reset.
fullPath = value.namespace;
}
if (value.name && typeof value.name !== "object") {
fullPath = `${fullPath}.${value.name}`;
} else if (value.id && typeof value.id !== "object") {
fullPath = `${fullPath}.${value.id}`;
}
if (fullPath) {
entries.set(fullPath, value);
if (VERBOSITY & 2) {
console.log(fullPath);
}
}
const handleRef = value => {
const refId = value.$ref;
const parts = refId.split(".");
const id = parts.pop();
const requested_namespace = parts[0];
const current_namespace = fullPath.split(".")[0];
// Search the specified namespace, the local namespace, the manifest namespace
// and then all other namespaces.
const searchNamespaces = [
requested_namespace,
current_namespace,
"manifest",
...all_namespaces.map(n => n.namespace),
];
for (const searchNamespace of searchNamespaces) {
const searchSchemas = all_namespaces.filter(
n => n.namespace == searchNamespace
);
for (const searchSchema of searchSchemas) {
const ref = getNestedIdOrNamespace(searchSchema, id);
if (ref) {
// Deep-clone the found ref to prevent circular dependencies.
return JSON.parse(JSON.stringify(ref));
}
}
}
console.log(`Warning: Missing requested $ref: ${id} (${refId})`);
return null;
};
// Replace $refs, but ignore recursive usages.
if (value.$ref && !handledRefs.includes(value.$ref)) {
const ref = handleRef(value);
if (ref) {
handledRefs.push(value.$ref);
Object.assign(value, ref);
delete value.$ref;
delete value.id;
}
}
Object.keys(value).forEach(key => {
switch (key) {
case "choices":
collectNamespaceEntriesAndResolveRefs(
value[key],
entries,
all_namespaces,
key,
[...handledRefs],
fullPath
);
break;
default:
collectNamespaceEntriesAndResolveRefs(
value[key],
entries,
all_namespaces,
key,
[...handledRefs],
`${fullPath}.${key}`
);
}
});
}
/**
* Replace $import statements by the actual referenced element/namespace.
*
* @param {any} obj - The value to process. Usually a schema JSON, but the
* function recursively calls itself on nested elements.
*
* @returns {any} The processed value.
*/
function _processImports(obj, all_namespaces) {
if (typeof obj !== "object") {
return obj;
}
if (Array.isArray(obj)) {
return obj.map(v => _processImports(v, all_namespaces));
}
if (obj.hasOwnProperty("$import")) {
// Assume imports are unique, ignore prepended namespace.
const id = obj.$import;
// TODO: We skip ManifestBase for now.
if (id == "manifest.ManifestBase") {
return obj;
}
for (const searchSchema of all_namespaces) {
let imported = getNestedIdOrNamespace(searchSchema, id);
if (imported) {
// Do not import top level manifest limits.
imported = JSON.parse(JSON.stringify(imported));
delete imported.min_manifest_version;
delete imported.max_manifest_version;
// Do not import namespace name and id.
delete imported.namespace;
delete imported.id;
delete obj.$import;
return mergeObjects(obj, imported);
}
}
console.error(`Warning: Missing requested $import: ${id}`);
}
// Default.
return Object.keys(obj).reduce((o, key) => {
o[key] = _processImports(obj[key], all_namespaces);
return o;
}, {});
}
// -----------------------------------------------------------------------------
function updateCompatData(
tcd,
namespace_entry,
compatEntry,
logEntries,
skipFlatPropsCheck = false
) {
const expected = compatEntry.version_added;
const parts = namespace_entry.split(".");
const [namespace, entryType, entryName] = parts;
const curr_namespace_parts = [];
if (VERBOSITY & 4) {
console.log(`Processing ${namespace_entry} : ${expected}`);
}
const newEntry = () => {
return {
__compat: {
support: {
thunderbird: compatEntry,
},
},
};
};
const detectNotation = (
entry,
itemName,
parent,
parentItemName,
curr_namespace_entry
) => {
// The notation used in BCD is very very inconsistent. They even use different
// notations within the same parameter. See action.setIcon
// https://github.com/mdn/browser-compat-data/blob/7afd5da3bfe0e0f4434585ee75f277d784662caf/webextensions/api/action.json#L605
// We therefore first check if the _parameter or _value notation is used, and
// then check on the specific element and issue a warning if mixed data has
// been found.
let rv;
if (parent) {
// Detect <paramName>_<propertyName>_parameter notation used in this parameter.
const dashedParameterNotation = Object.keys(parent).filter(
e => e.startsWith(`${parentItemName}_`) && e.endsWith(`_parameter`)
);
if (dashedParameterNotation.length) {
if (VERBOSITY & 4) {
console.log(`Notation (_parameter): ${dashedParameterNotation}`);
}
rv = {
notation: "_parameter",
processEntry: parent,
processKey: `${parentItemName}_${itemName}_parameter`,
};
}
// Detect <propertyName>_value notation used in this parameter.
const dashedValueNotation = Object.keys(parent).filter(e =>
e.endsWith(`_value`)
);
if (dashedValueNotation.length) {
if (VERBOSITY & 4) {
console.log(`Notation (_value): ${dashedValueNotation}`);
}
if (rv) {
logEntries.add(
`Warning: Found MIXED notations for ${curr_namespace_entry}`
);
}
rv = {
notation: "_value",
processEntry: parent,
processKey: `${itemName}_value`,
};
}
// Detect flat notation for the specified element.
if (
CONFIRMED_FLAT_PROPS.some(
e =>
curr_namespace_entry.split(".").length == e.split(".").length &&
curr_namespace_entry.startsWith(e)
)
) {
logEntries.add(
`Info: Accepted confirmed flat notation usage: ${curr_namespace_entry}`
);
rv = {
notation: "flat",
processEntry: parent,
processKey: itemName,
};
} else if (!skipFlatPropsCheck) {
const flatNotation = Object.keys(parent).filter(e => e == itemName);
if (flatNotation.length) {
if (VERBOSITY & 4) {
console.log(`Notation (flat): ${flatNotation}`);
}
if (rv) {
logEntries.add(
`Warning: Found MIXED notations: ${curr_namespace_entry}`
);
}
if (
!KNOWN_FALSE_POSITIVE_FLAT_PROPS.some(e =>
curr_namespace_entry.startsWith(e)
)
) {
logEntries.add(
`Info: Ignored unconfirmed but potential flat notation usage: ${curr_namespace_entry}`
);
}
}
}
}
return (
rv || {
notation: "nested",
processEntry: entry,
processKey: itemName,
}
);
};
const handleEntry = (entry, itemName) => {
const curr_namespace_entry = curr_namespace_parts.join(".");
if (VERBOSITY & 4) {
console.log(` handling: ${curr_namespace_entry} / ${namespace_entry}`);
}
if (!expected) {
if (curr_namespace_entry == namespace_entry) {
// If expected to be unsupported, replace the entire entity with a single
// compat value.
if (VERBOSITY & 4) {
console.log(` replacing: ${namespace_entry}`);
}
entry[itemName] = newEntry();
return false;
}
if (entry[itemName]) {
// The current level element exists, but we have not reached the final
// depth. Do not touch it and process the next level.
return true;
}
// The current level element does exists, create it and process the next
// level! The final cleanup will reduce such hierarchies to a minimum.
if (VERBOSITY & 4) {
console.log(` adding intermediate: ${namespace_entry}`);
}
entry[itemName] = newEntry();
return true;
}
// Add the current level entry, if missing.
if (!entry[itemName]) {
if (VERBOSITY & 4) {
console.log(` adding: ${expected} : ${curr_namespace_entry}`);
}
entry[itemName] = newEntry();
}
// Add or update __compat of current level.
if (!entry[itemName].__compat?.support?.thunderbird?.version_added) {
if (VERBOSITY & 4) {
console.log(` setting as true: ${expected} : ${curr_namespace_entry}`);
}
entry[itemName].__compat = newEntry().__compat;
} else if (
entry[itemName].__compat.support.thunderbird.version_added === false
) {
if (VERBOSITY & 4) {
console.log(` forcing to true: ${expected} : ${curr_namespace_entry}`);
}
entry[itemName].__compat.support.thunderbird.version_added = expected;
}
if (curr_namespace_entry == namespace_entry) {
if (VERBOSITY & 4) {
console.log(` finished: ${namespace_entry}`);
}
return false;
}
return true;
};
if (namespace) {
curr_namespace_parts.push(namespace);
if (VERBOSITY & 4) {
console.log(` next ${curr_namespace_parts.join(" ")}`);
}
if (namespace == "manifest") {
// TODO: Handle manifest entries.
return;
}
if (!handleEntry(tcd.webextensions.api, namespace)) {
return;
}
}
if (
namespace &&
entryType &&
entryName &&
["functions", "events", "properties", "types"].includes(entryType)
) {
curr_namespace_parts.push(entryType);
curr_namespace_parts.push(entryName);
if (VERBOSITY & 4) {
console.log(` next ${curr_namespace_parts.join(" ")}`);
}
if (!handleEntry(tcd.webextensions.api[namespace], entryName)) {
return;
}
if (`${namespace}.${entryType}.${entryName}` == namespace_entry) {
// No further processing, if the entire schema entry has been processed.
return;
}
} else if (VERBOSITY & 4) {
console.log(` skipped ${namespace} ${entryType} ${entryName}`);
}
if (
namespace &&
entryType &&
entryName &&
["functions", "events", "types"].includes(entryType) &&
parts.length > 3
) {
let parentEntryName = namespace;
let parent = tcd.webextensions.api[parentEntryName];
let entry = parent[entryName];
for (let i = 3; i < parts.length; i += 2) {
// Process a sub-entry of the current entry.
const subEntryType = parts[i];
const subEntryName = parts[i + 1];
curr_namespace_parts.push(subEntryType);
curr_namespace_parts.push(subEntryName);
if (VERBOSITY & 4) {
console.log(` next ${curr_namespace_parts.join(" ")}`);
}
if (!subEntryType || !subEntryName || subEntryName == "callback") {
// No further processing.
if (VERBOSITY & 4) {
console.log(` finished (not enough data or callback)`);
}
return;
}
if (!["properties", "parameters", "enum"].includes(subEntryType)) {
// No further processing, but break to reach the final log statement.
if (VERBOSITY & 4) {
console.log(` finished (ignore group: ${subEntryType})`);
}
break;
}
const { processEntry, processKey } = detectNotation(
entry,
subEntryName,
parent,
parentEntryName,
curr_namespace_parts.join(".")
);
if (!handleEntry(processEntry, processKey)) {
return;
}
parentEntryName = processKey;
parent = processEntry;
entry = parent[processKey];
}
}
// Suppress logging for top level entries, for which entries are expected to
// not be generated.
if (parts.length > 2) {
if (VERBOSITY & 4) {
console.log(`IGNORED for expected ${expected} : ${namespace_entry}`);
}
}
}