import fs from "fs"; /** * Helper class for processing and using manifest for packing the extension. */ class ManifestProcessor { /** * Current state of the manifest object. * @type {Manifest} */ #manifestObject; /** * @param {Manifest} parsedManifest Original manifest contents parsed as JSON object. */ constructor(parsedManifest) { this.#manifestObject = parsedManifest; } /** * Collect all the content scripts & stylesheets for single build action. * * @returns {Set} */ collectContentScripts() { const contentScripts = this.#manifestObject.content_scripts; if (!contentScripts) { console.info('No content scripts to collect.'); return new Set(); } const entryPoints = new Set(); for (let entry of contentScripts) { if (entry.js) { for (let jsPath of entry.js) { entryPoints.add(jsPath); } } if (entry.css) { for (let cssPath of entry.css) { entryPoints.add(cssPath); } } } return entryPoints; } /** * Map over every content script defined in the manifest. If no content scripts defined, no calls will be made to the * callback. * * @param {function(ContentScriptsEntry): Promise} mapCallback Processing function to call on * every entry. Entries should be modified and returned. Function should be asynchronous. * * @return {Promise} */ async mapContentScripts(mapCallback) { const contentScripts = this.#manifestObject.content_scripts; if (!contentScripts) { console.info('No content scripts to map over.'); return; } for (let entryIndex = 0; entryIndex < contentScripts.length; entryIndex++) { contentScripts[entryIndex] = await mapCallback(contentScripts[entryIndex]); } } /** * Pass the version of the plugin from following package.json file. * * @param {string} packageFilePath Path to the JSON file to parse and extract the version from. If version is not * found, original version will be kept. */ passVersionFromPackage(packageFilePath) { /** @type {PackageObject} */ const packageObject = JSON.parse(fs.readFileSync(packageFilePath, 'utf8')); if (packageObject.version) { this.#manifestObject.version = packageObject.version; } } /** * Find all patterns in content scripts and host permissions and replace the hostname to the different one. * * @param {string|string[]} singleOrMultipleHostnames One or multiple hostnames to replace the original hostname with. */ replaceHostTo(singleOrMultipleHostnames) { if (typeof singleOrMultipleHostnames === 'string') { singleOrMultipleHostnames = [singleOrMultipleHostnames]; } this.#manifestObject.host_permissions = singleOrMultipleHostnames.map(hostname => `*://*.${hostname}/`); this.#manifestObject.content_scripts?.forEach(entry => { entry.matches = entry.matches.reduce((resultMatches, originalMatchPattern) => { for (const updatedHostname of singleOrMultipleHostnames) { resultMatches.push( originalMatchPattern.replace( /\*:\/\/\*\.[a-z]+\.[a-z]+\//, `*://*.${updatedHostname}/` ), ); } return resultMatches; }, []); }) } /** * Set different identifier for Gecko-based browsers (Firefox). * * @param {string} id ID of the extension to use. */ setGeckoIdentifier(id) { this.#manifestObject.browser_specific_settings.gecko.id = id; } /** * Set the different extension name. * * @param {string} booruName */ replaceBooruNameWith(booruName) { this.#manifestObject.name = this.#manifestObject.name.replaceAll('Furbooru', booruName); this.#manifestObject.description = this.#manifestObject.description.replaceAll('Furbooru', booruName); } /** * Save the current state of the manifest into the selected file. * * @param {string} manifestFilePath File to write the resulting manifest to. Should be called after all the * modifications. */ saveTo(manifestFilePath) { fs.writeFileSync( manifestFilePath, JSON.stringify(this.#manifestObject, null, 2), { encoding: 'utf8' } ); } } /** * Load the manifest and create a processor object. * * @param {string} filePath Path to the original manifest file. * * @return {ManifestProcessor} Object for manipulating manifest file. */ export function loadManifest(filePath) { const manifest = JSON.parse(fs.readFileSync(filePath, 'utf-8')); return new ManifestProcessor(manifest); } /** * @typedef {Object} Manifest * @property {string} name * @property {string} description * @property {string} version * @property {BrowserSpecificSettings} browser_specific_settings * @property {string[]} host_permissions * @property {ContentScriptsEntry[]|undefined} content_scripts */ /** * @typedef {Object} BrowserSpecificSettings * @property {GeckoSettings} gecko */ /** * @typedef {Object} GeckoSettings * @property {string} id */ /** * @typedef {Object} ContentScriptsEntry * @property {string[]} matches * @property {string[]|undefined} js * @property {string[]|undefined} css */ /** * @typedef {Object} PackageObject * @property {string|undefined} version */