UNPKG

rollup-plugin-chrome-extension

Version:

Build Chrome Extensions with this Rollup plugin.

1,506 lines (1,362 loc) 422 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); require('array-flat-polyfill'); var fs = require('fs-extra'); var lodash = require('lodash'); var path = require('path'); var cheerio = require('cheerio'); var cosmiconfig = require('cosmiconfig'); var jsonpathPlus = require('jsonpath-plus'); var memoize = require('mem'); var slash = require('slash'); var fs$1 = require('fs'); var glob = require('glob'); var Ajv = require('ajv'); var jsonPtr = require('json-ptr'); var rollup = require('rollup'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs); var path__default = /*#__PURE__*/_interopDefaultLegacy(path); var cheerio__default = /*#__PURE__*/_interopDefaultLegacy(cheerio); var memoize__default = /*#__PURE__*/_interopDefaultLegacy(memoize); var slash__default = /*#__PURE__*/_interopDefaultLegacy(slash); var glob__default = /*#__PURE__*/_interopDefaultLegacy(glob); var Ajv__default = /*#__PURE__*/_interopDefaultLegacy(Ajv); const not = (fn) => (x) => !fn(x); function isChunk(x) { return x && x.type === 'chunk' } function isAsset(x) { return x.type === 'asset' } function isString(x) { return typeof x === 'string' } function isUndefined(x) { return typeof x === 'undefined' } function isNull(x) { return x === null } function isPresent(x) { return !isUndefined(x) && !isNull(x) } const normalizeFilename = (p) => p.replace(/\.[tj]sx?$/, '.js'); /** Update the manifest source in the output bundle */ const updateManifest = ( updater, bundle, handleError, ) => { try { const manifestKey = 'manifest.json'; const manifestAsset = bundle[manifestKey]; if (!manifestAsset) { throw new Error('No manifest.json in the rollup output bundle.') } const manifest = JSON.parse(manifestAsset.source ); const result = updater(manifest); manifestAsset.source = JSON.stringify(result, undefined, 2); } catch (error) { if (handleError && error instanceof Error) { handleError(error.message); } else { throw error } } return bundle }; function reduceToRecord(srcDir) { if (srcDir === null || typeof srcDir === 'undefined') { // This would be a config error, so should throw throw new TypeError('srcDir is null or undefined') } return (inputRecord, filename) => { const name = path.relative(srcDir, filename).split('.').slice(0, -1).join('.'); if (name in inputRecord) { throw new Error( `Script files with different extensions should not share names:\n\n"${filename}"\nwill overwrite\n"${inputRecord[name]}"`, ) } return { ...inputRecord, [name]: filename } } } const loadHtml = (rootPath) => (filePath) => { const htmlCode = fs__default["default"].readFileSync(filePath, 'utf8'); const $ = cheerio__default["default"].load(htmlCode); return Object.assign($, { filePath, rootPath }) }; const getRelativePath = ({ filePath, rootPath }) => (p) => { const htmlFileDir = path__default["default"].dirname(filePath); let relDir; if (p.startsWith('/')) { relDir = path__default["default"].relative(process.cwd(), rootPath); } else { relDir = path__default["default"].relative(process.cwd(), htmlFileDir); } return path__default["default"].join(relDir, p) }; /* -------------------- SCRIPTS -------------------- */ const getScriptElems = ($) => $('script') .not('[data-rollup-asset]') .not('[src^="http:"]') .not('[src^="https:"]') .not('[src^="data:"]') .not('[src^="/"]'); // Mutative action const mutateScriptElems = ({ browserPolyfill }) => ($) => { getScriptElems($) .attr('type', 'module') .attr('src', (i, value) => { // FIXME: @types/cheerio is wrong for AttrFunction: index.d.ts, line 16 // declare type AttrFunction = (i: number, currentValue: string) => any; // eslint-disable-next-line // @ts-ignore const replaced = value.replace(/\.[jt]sx?/g, '.js'); return replaced }); if (browserPolyfill) { const head = $('head'); if ( browserPolyfill === true || (typeof browserPolyfill === 'object' && browserPolyfill.executeScript) ) { head.prepend( '<script src="/assets/browser-polyfill-executeScript.js"></script>', ); } head.prepend('<script src="/assets/browser-polyfill.js"></script>'); } return $ }; const getScripts = ($) => getScriptElems($).toArray(); const getScriptSrc = ($) => getScripts($) .map((elem) => $(elem).attr('src')) .filter(isString) .map(getRelativePath($)); /* ----------------- ASSET SCRIPTS ----------------- */ const getAssets = ($) => $('script') .filter('[data-rollup-asset="true"]') .not('[src^="http:"]') .not('[src^="https:"]') .not('[src^="data:"]') .not('[src^="/"]') .toArray(); const getJsAssets = ($) => getAssets($) .map((elem) => $(elem).attr('src')) .filter(isString) .map(getRelativePath($)); /* -------------------- css ------------------- */ const getCss = ($) => $('link') .filter('[rel="stylesheet"]') .not('[href^="http:"]') .not('[href^="https:"]') .not('[href^="data:"]') .not('[href^="/"]') .toArray(); const getCssHrefs = ($) => getCss($) .map((elem) => $(elem).attr('href')) .filter(isString) .map(getRelativePath($)); /* -------------------- img ------------------- */ const getImgs = ($) => $('img') .not('[src^="http://"]') .not('[src^="https://"]') .not('[src^="data:"]') .toArray(); const getFavicons = ($) => $('link[rel="icon"]') .not('[href^="http:"]') .not('[href^="https:"]') .not('[href^="data:"]') .toArray(); const getImgSrcs = ($) => { return [ ...getImgs($).map((elem) => $(elem).attr('src')), ...getFavicons($).map((elem) => $(elem).attr('href')), ] .filter(isString) .map(getRelativePath($)) }; const isHtml = (path) => /\.html?$/.test(path); const name$1 = 'html-inputs'; /* ============================================ */ /* HTML-INPUTS */ /* ============================================ */ function htmlInputs( htmlInputsOptions, /** Used for testing */ cache = { scripts: [], html: [], html$: [], js: [], css: [], img: [], input: [], } , ) { return { name: name$1, cache, /* ============================================ */ /* OPTIONS HOOK */ /* ============================================ */ options(options) { // srcDir may be initialized by another plugin const { srcDir } = htmlInputsOptions; if (srcDir) { cache.srcDir = srcDir; } else { throw new TypeError('options.srcDir not initialized') } // Skip if cache.input exists // cache is dumped in watchChange hook // Parse options.input to array let input; if (typeof options.input === 'string') { input = [options.input]; } else if (Array.isArray(options.input)) { input = [...options.input]; } else if (typeof options.input === 'object') { input = Object.values(options.input); } else { throw new TypeError(`options.input cannot be ${typeof options.input}`) } /* ------------------------------------------------- */ /* HANDLE HTML FILES */ /* ------------------------------------------------- */ // Filter htm and html files cache.html = input.filter(isHtml); // If no html files, do nothing if (cache.html.length === 0) return options // If the cache has been dumped, reload from files if (cache.html$.length === 0) { // This is all done once cache.html$ = cache.html.map(loadHtml(srcDir)); cache.js = lodash.flatten(cache.html$.map(getScriptSrc)); cache.css = lodash.flatten(cache.html$.map(getCssHrefs)); cache.img = lodash.flatten(cache.html$.map(getImgSrcs)); cache.scripts = lodash.flatten(cache.html$.map(getJsAssets)); // Cache jsEntries with existing options.input cache.input = input.filter(not(isHtml)).concat(cache.js); // Prepare cache.html$ for asset emission cache.html$.forEach(mutateScriptElems(htmlInputsOptions)); if (cache.input.length === 0) { throw new Error( 'At least one HTML file must have at least one script.', ) } } // TODO: simply remove HTML files from options.input // - Parse HTML and emit chunks and assets in buildStart return { ...options, input: cache.input.reduce(reduceToRecord(htmlInputsOptions.srcDir), {}), } }, /* ============================================ */ /* HANDLE FILE CHANGES */ /* ============================================ */ async buildStart() { const { srcDir } = htmlInputsOptions; if (srcDir) { cache.srcDir = srcDir; } else { throw new TypeError('options.srcDir not initialized') } const assets = [...cache.css, ...cache.img, ...cache.scripts]; assets.concat(cache.html).forEach((asset) => { this.addWatchFile(asset); }); const emitting = assets.map(async (asset) => { // Read these files as Buffers const source = await fs.readFile(asset); const fileName = path.relative(srcDir, asset); this.emitFile({ type: 'asset', source, // Buffer fileName, }); }); cache.html$.map(($) => { const source = $.html(); const fileName = path.relative(srcDir, $.filePath); this.emitFile({ type: 'asset', source, // String fileName, }); }); await Promise.all(emitting); }, watchChange(id) { if (id.endsWith('.html') || id.endsWith('manifest.json')) { // Dump cache if html file or manifest changes cache.html$ = []; } }, } } const code$5 = "(function () {\n\t'use strict';\n\n\tconst importPath = /*@__PURE__*/ JSON.parse('%PATH%');\n\n\timport(chrome.runtime.getURL(importPath));\n\n})();\n"; function isMV2( m, ) { if (!isPresent(m)) throw new TypeError('manifest is undefined') return m.manifest_version === 2 } function isMV3( m, ) { if (!isPresent(m)) throw new TypeError('manifest is undefined') return m.manifest_version === 3 } const cloneObject = (obj) => JSON.parse(JSON.stringify(obj)); const code$4 = "(function () {\n 'use strict';\n\n function delay(ms) {\n return new Promise((resolve) => {\n setTimeout(resolve, ms);\n })\n }\n\n function captureEvents(events) {\n const captured = events.map(captureEvent);\n\n return () => captured.forEach((t) => t())\n\n function captureEvent(event) {\n let isCapturePhase = true;\n\n // eslint-disable-next-line @typescript-eslint/ban-types\n const callbacks = new Map();\n const eventArgs = new Set();\n\n // This is the only listener for the native event\n event.addListener(handleEvent);\n\n function handleEvent(...args) {\n if (isCapturePhase) {\n // This is before dynamic import completes\n eventArgs.add(args);\n\n if (typeof args[2] === 'function') {\n // During capture phase all messages are async\n return true\n } else {\n // Sync messages or some other event\n return false\n }\n } else {\n // The callbacks determine the listener return value\n return callListeners(...args)\n }\n }\n\n // Called when dynamic import is complete\n // and when subsequent events fire\n function callListeners(...args) {\n let isAsyncCallback = false;\n callbacks.forEach((options, cb) => {\n // A callback error should not affect the other callbacks\n try {\n isAsyncCallback = cb(...args) || isAsyncCallback;\n } catch (error) {\n console.error(error);\n }\n });\n\n if (!isAsyncCallback && typeof args[2] === 'function') {\n // We made this an async message callback during capture phase\n // when the function handleEvent returned true\n // so we are responsible to call sendResponse\n // If the callbacks are sync message callbacks\n // the sendMessage callback on the other side\n // resolves with no arguments (this is the same behavior)\n args[2]();\n }\n\n // Support events after import is complete\n return isAsyncCallback\n }\n\n // This function will trigger this Event with our stored args\n function triggerEvents() {\n // Fire each event for this Event\n eventArgs.forEach((args) => {\n callListeners(...args);\n });\n\n // Dynamic import is complete\n isCapturePhase = false;\n // Don't need these anymore\n eventArgs.clear();\n }\n\n // All future listeners are handled by our code\n event.addListener = function addListener(cb, ...options) {\n callbacks.set(cb, options);\n };\n\n event.hasListeners = function hasListeners() {\n return callbacks.size > 0\n };\n\n event.hasListener = function hasListener(cb) {\n return callbacks.has(cb)\n };\n\n event.removeListener = function removeListener(cb) {\n callbacks.delete(cb);\n };\n\n event.__isCapturedEvent = true;\n\n return triggerEvents\n }\n }\n\n function resolvePath(object, path, defaultValue) {\n return path.split('.').reduce((o, p) => (o ? o[p] : defaultValue), object) \n }\n\n const eventPaths = /*@__PURE__*/ JSON.parse('%EVENTS%'); \n const importPath = /*@__PURE__*/ JSON.parse('%PATH%'); \n const delayLength = /*@__PURE__*/ JSON.parse('%DELAY%');\n\n const events = eventPaths.map((eventPath) => resolvePath(chrome, eventPath));\n const triggerEvents = captureEvents(events);\n\n import(importPath).then(async () => {\n if (delayLength) await delay(delayLength);\n\n triggerEvents();\n });\n\n})();\n"; const code$3 = "(function () {\n 'use strict';\n\n function captureEvents(events) {\n const captured = events.map(captureEvent);\n\n return () => captured.forEach((t) => t())\n\n function captureEvent(event) {\n let isCapturePhase = true;\n\n // eslint-disable-next-line @typescript-eslint/ban-types\n const callbacks = new Map();\n const eventArgs = new Set();\n\n // This is the only listener for the native event\n event.addListener(handleEvent);\n\n function handleEvent(...args) {\n if (isCapturePhase) {\n // This is before dynamic import completes\n eventArgs.add(args);\n\n if (typeof args[2] === 'function') {\n // During capture phase all messages are async\n return true\n } else {\n // Sync messages or some other event\n return false\n }\n } else {\n // The callbacks determine the listener return value\n return callListeners(...args)\n }\n }\n\n // Called when dynamic import is complete\n // and when subsequent events fire\n function callListeners(...args) {\n let isAsyncCallback = false;\n callbacks.forEach((options, cb) => {\n // A callback error should not affect the other callbacks\n try {\n isAsyncCallback = cb(...args) || isAsyncCallback;\n } catch (error) {\n console.error(error);\n }\n });\n\n if (!isAsyncCallback && typeof args[2] === 'function') {\n // We made this an async message callback during capture phase\n // when the function handleEvent returned true\n // so we are responsible to call sendResponse\n // If the callbacks are sync message callbacks\n // the sendMessage callback on the other side\n // resolves with no arguments (this is the same behavior)\n args[2]();\n }\n\n // Support events after import is complete\n return isAsyncCallback\n }\n\n // This function will trigger this Event with our stored args\n function triggerEvents() {\n // Fire each event for this Event\n eventArgs.forEach((args) => {\n callListeners(...args);\n });\n\n // Dynamic import is complete\n isCapturePhase = false;\n // Don't need these anymore\n eventArgs.clear();\n }\n\n // All future listeners are handled by our code\n event.addListener = function addListener(cb, ...options) {\n callbacks.set(cb, options);\n };\n\n event.hasListeners = function hasListeners() {\n return callbacks.size > 0\n };\n\n event.hasListener = function hasListener(cb) {\n return callbacks.has(cb)\n };\n\n event.removeListener = function removeListener(cb) {\n callbacks.delete(cb);\n };\n\n event.__isCapturedEvent = true;\n\n return triggerEvents\n }\n }\n\n function delay(ms) {\n return new Promise((resolve) => {\n setTimeout(resolve, ms);\n })\n }\n\n /**\n * Get matches from an object of nested objects\n *\n * @export\n * @template T Type of matches\n * @param {*} object Parent object to search\n * @param {(x: any) => boolean} pred A predicate function that will receive each property value of an object\n * @param {string[]} excludeKeys Exclude a property if the key exactly matches\n * @returns {T[]} The matched values from the parent object\n */\n function getDeepMatches(object, pred, excludeKeys) {\n const keys = typeof object === 'object' && object ? Object.keys(object) : [];\n\n return keys.length\n ? keys\n .filter((key) => !excludeKeys.includes(key))\n .reduce((r, key) => {\n const target = object[key];\n\n if (target && pred(target)) {\n return [...r, target]\n } else {\n return [...r, ...getDeepMatches(target, pred, excludeKeys)]\n }\n }, [] )\n : []\n }\n\n const importPath = /*@__PURE__*/ JSON.parse('%PATH%'); \n const delayLength = /*@__PURE__*/ JSON.parse('%DELAY%'); \n const excludedPaths = /*@__PURE__*/ JSON.parse('%EXCLUDE%');\n\n const events = getDeepMatches(\n chrome,\n (x) => typeof x === 'object' && 'addListener' in x,\n // The webRequest API is not compatible with event pages\n // TODO: this can be removed\n // if we stop using this wrapper with \"webRequest\" permission\n excludedPaths.concat(['webRequest']),\n );\n const triggerEvents = captureEvents(events);\n\n import(importPath).then(async () => {\n if (delayLength) await delay(delayLength);\n\n triggerEvents();\n });\n\n})();\n"; /** * This options object allows fine-tuning of the dynamic import wrapper. * * @export * @interface DynamicImportWrapper */ // FEATURE: add static code analysis for wake events // - This will be slower... function prepImportWrapperScript({ eventDelay = 0, wakeEvents = [], excludeNames = ['extension'], }) { const delay = JSON.stringify(eventDelay); const events = wakeEvents.length ? JSON.stringify(wakeEvents.map((ev) => ev.replace(/^chrome\./, ''))) : false; const exclude = JSON.stringify(excludeNames); const script = ( events ? code$4.replace('%EVENTS%', events) : code$3.replace('%EXCLUDE%', exclude) ).replace('%DELAY%', delay); return script } const isManifestFileName = (filename) => path.basename(filename).startsWith('manifest'); const validateFileName = (filename, { input }) => { if (isUndefined(filename)) throw new Error( `Could not find manifest in Rollup options.input: ${JSON.stringify( input, )}`, ) if (!fs$1.existsSync(filename)) throw new Error(`Could not load manifest: ${filename} does not exist`) return filename }; function getInputManifestPath(options) { if (Array.isArray(options.input)) { const manifestIndex = options.input.findIndex(isManifestFileName); const inputAry = [ ...options.input.slice(0, manifestIndex), ...options.input.slice(manifestIndex + 1), ]; const inputManifestPath = validateFileName( options.input[manifestIndex], options, ); return { inputManifestPath, inputAry } } else if (typeof options.input === 'object') { const inputManifestPath = validateFileName(options.input.manifest, options); const inputObj = cloneObject(options.input); delete inputObj['manifest']; return { inputManifestPath, inputObj } } else if (isString(options.input)) { const inputManifestPath = validateFileName(options.input, options); return { inputManifestPath } } throw new TypeError( `Rollup options.input cannot be type "${typeof options.input}"`, ) } const combinePerms = ( ...permissions ) => { const { perms, xperms } = (permissions.flat(Infinity) ) .filter((perm) => typeof perm !== 'undefined') .reduce( ({ perms, xperms }, perm) => { if (perm.startsWith('!')) { xperms.add(perm.slice(1)); } else { perms.add(perm); } return { perms, xperms } }, { perms: new Set(), xperms: new Set() }, ); return [...perms].filter((p) => !xperms.has(p)) }; /* ============================================ */ /* CHECK PERMISSIONS */ /* ============================================ */ // export const debugger = s => /((chromep?)|(browser))[\s\n]*\.[\s\n]*debugger/.test(s) // export const enterprise.deviceAttributes = s => /((chromep?)|(browser))[\s\n]*\.[\s\n]*enterprise\.deviceAttributes/.test(s) // export const enterprise.hardwarePlatform = s => /((chromep?)|(browser))[\s\n]*\.[\s\n]*enterprise\.hardwarePlatform/.test(s) // export const enterprise.platformKeys = s => /((chromep?)|(browser))[\s\n]*\.[\s\n]*enterprise\.platformKeys/.test(s) // export const networking.config = s => /((chromep?)|(browser))[\s\n]*\.[\s\n]*networking\.config/.test(s) // export const system.cpu = s => /((chromep?)|(browser))[\s\n]*\.[\s\n]*system\.cpu/.test(s) // export const system.display = s => /((chromep?)|(browser))[\s\n]*\.[\s\n]*system\.display/.test(s) // export const system.memory = s => /((chromep?)|(browser))[\s\n]*\.[\s\n]*system\.memory/.test(s) // export const system.storage = s => /((chromep?)|(browser))[\s\n]*\.[\s\n]*system\.storage/.test(s) const alarms = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*alarms/.test(s); const bookmarks = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*bookmarks/.test(s); const contentSettings = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*contentSettings/.test(s); const contextMenus = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*contextMenus/.test(s); const cookies = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*cookies/.test(s); const declarativeContent = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*declarativeContent/.test(s); const declarativeNetRequest = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*declarativeNetRequest/.test(s); const declarativeWebRequest = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*declarativeWebRequest/.test(s); const desktopCapture = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*desktopCapture/.test(s); const displaySource = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*displaySource/.test(s); const dns = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*dns/.test(s); const documentScan = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*documentScan/.test(s); const downloads = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*downloads/.test(s); const experimental = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*experimental/.test(s); const fileBrowserHandler = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*fileBrowserHandler/.test(s); const fileSystemProvider = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*fileSystemProvider/.test(s); const fontSettings = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*fontSettings/.test(s); const gcm = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*gcm/.test(s); const geolocation = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*geolocation/.test(s); const history = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*history/.test(s); const identity = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*identity/.test(s); const idle = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*idle/.test(s); const idltest = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*idltest/.test(s); const management = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*management/.test(s); const nativeMessaging = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*nativeMessaging/.test(s); const notifications = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*notifications/.test(s); const pageCapture = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*pageCapture/.test(s); const platformKeys = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*platformKeys/.test(s); const power = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*power/.test(s); const printerProvider = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*printerProvider/.test(s); const privacy = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*privacy/.test(s); const processes = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*processes/.test(s); const proxy = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*proxy/.test(s); const sessions = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*sessions/.test(s); const signedInDevices = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*signedInDevices/.test(s); const storage = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*storage/.test(s); const tabCapture = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*tabCapture/.test(s); // export const tabs = s => /((chromep?)|(browser))[\s\n]*\.[\s\n]*tabs/.test(s) const topSites = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*topSites/.test(s); const tts = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*tts/.test(s); const ttsEngine = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*ttsEngine/.test(s); const unlimitedStorage = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*unlimitedStorage/.test(s); const vpnProvider = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*vpnProvider/.test(s); const wallpaper = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*wallpaper/.test(s); const webNavigation = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*webNavigation/.test(s); const webRequest = (s) => /((chromep?)|(browser))[\s\n]*\.[\s\n]*webRequest/.test(s); const webRequestBlocking = (s) => webRequest(s) && s.includes("'blocking'"); // TODO: add readClipboard // TODO: add writeClipboard var permissions = /*#__PURE__*/Object.freeze({ __proto__: null, alarms: alarms, bookmarks: bookmarks, contentSettings: contentSettings, contextMenus: contextMenus, cookies: cookies, declarativeContent: declarativeContent, declarativeNetRequest: declarativeNetRequest, declarativeWebRequest: declarativeWebRequest, desktopCapture: desktopCapture, displaySource: displaySource, dns: dns, documentScan: documentScan, downloads: downloads, experimental: experimental, fileBrowserHandler: fileBrowserHandler, fileSystemProvider: fileSystemProvider, fontSettings: fontSettings, gcm: gcm, geolocation: geolocation, history: history, identity: identity, idle: idle, idltest: idltest, management: management, nativeMessaging: nativeMessaging, notifications: notifications, pageCapture: pageCapture, platformKeys: platformKeys, power: power, printerProvider: printerProvider, privacy: privacy, processes: processes, proxy: proxy, sessions: sessions, signedInDevices: signedInDevices, storage: storage, tabCapture: tabCapture, topSites: topSites, tts: tts, ttsEngine: ttsEngine, unlimitedStorage: unlimitedStorage, vpnProvider: vpnProvider, wallpaper: wallpaper, webNavigation: webNavigation, webRequest: webRequest, webRequestBlocking: webRequestBlocking }); /* ============================================ */ /* DERIVE PERMISSIONS */ /* ============================================ */ const derivePermissions = (set, { code }) => Object.entries(permissions) .filter(([key]) => key !== 'default') .filter(([, fn]) => fn(code)) .map(([key]) => key) .reduce((s, p) => s.add(p), set); /* -------------------------------------------- */ /* DERIVE FILES */ /* -------------------------------------------- */ function deriveFiles( manifest, srcDir, options, ) { if (manifest.manifest_version === 3) { return deriveFilesMV3(manifest, srcDir, options) } else { return deriveFilesMV2(manifest, srcDir, options) } } function deriveFilesMV3( manifest, srcDir, options, ) { const locales = isString(manifest.default_locale) ? ['_locales/**/messages.json'] : []; const files = lodash.get( manifest, 'web_accessible_resources', [] , ) .flatMap(({ resources }) => resources) .concat(locales) .reduce((r, x) => { if (glob__default["default"].hasMagic(x)) { const files = glob__default["default"].sync(x, { cwd: srcDir }); return [...r, ...files.map((f) => f.replace(srcDir, ''))] } else { return [...r, x] } }, [] ); const contentScripts = lodash.get( manifest, 'content_scripts', [] , ).reduce((r, { js = [] }) => [...r, ...js], [] ); const js = [ ...files.filter((f) => /\.[jt]sx?$/.test(f)), lodash.get(manifest, 'background.service_worker'), ...(options.contentScripts ? contentScripts : []), ]; const html = [ ...files.filter((f) => /\.html?$/.test(f)), lodash.get(manifest, 'options_page'), lodash.get(manifest, 'options_ui.page'), lodash.get(manifest, 'devtools_page'), lodash.get(manifest, 'action.default_popup'), ...Object.values(lodash.get(manifest, 'chrome_url_overrides', {})), ]; const css = [ ...files.filter((f) => f.endsWith('.css')), ...lodash.get(manifest, 'content_scripts', [] ).reduce( (r, { css = [] }) => [...r, ...css], [] , ), ]; const img = [ ...files.filter((f) => /\.(jpe?g|png|svg|tiff?|gif|webp|bmp|ico)$/i.test(f), ), ...(Object.values(lodash.get(manifest, 'icons', {})) ), ...(Object.values(lodash.get(manifest, 'action.default_icon', {})) ), ]; // Files like fonts, things that are not expected const others = lodash.difference(files, css, contentScripts, js, html, img); return { css: validate(css), contentScripts: validate(contentScripts), js: validate(js), html: validate(html), img: validate(img), others: validate(others), } function validate(ary) { return [...new Set(ary.filter(isString))].map((x) => path.join(srcDir, x)) } } function deriveFilesMV2( manifest, srcDir, options, ) { const locales = isString(manifest.default_locale) ? ['_locales/**/messages.json'] : []; const files = lodash.get( manifest, 'web_accessible_resources', [] , ) .concat(locales) .reduce((r, x) => { if (glob__default["default"].hasMagic(x)) { const files = glob__default["default"].sync(x, { cwd: srcDir }); return [...r, ...files.map((f) => f.replace(srcDir, ''))] } else { return [...r, x] } }, [] ); const contentScripts = lodash.get( manifest, 'content_scripts', [] , ).reduce((r, { js = [] }) => [...r, ...js], [] ); const js = [ ...files.filter((f) => /\.[jt]sx?$/.test(f)), ...lodash.get(manifest, 'background.scripts', [] ), ...(options.contentScripts ? contentScripts : []), ]; const html = [ ...files.filter((f) => /\.html?$/.test(f)), lodash.get(manifest, 'background.page'), lodash.get(manifest, 'options_page'), lodash.get(manifest, 'options_ui.page'), lodash.get(manifest, 'devtools_page'), lodash.get(manifest, 'browser_action.default_popup'), lodash.get(manifest, 'page_action.default_popup'), ...Object.values(lodash.get(manifest, 'chrome_url_overrides', {})), ]; const css = [ ...files.filter((f) => f.endsWith('.css')), ...lodash.get(manifest, 'content_scripts', [] ).reduce( (r, { css = [] }) => [...r, ...css], [] , ), ]; const actionIconSet = [ 'browser_action.default_icon', 'page_action.default_icon', ].reduce((set, query) => { const result = lodash.get(manifest, query, {}); if (typeof result === 'string') { set.add(result); } else { Object.values(result).forEach((x) => set.add(x)); } return set }, new Set()); const img = [ ...actionIconSet, ...files.filter((f) => /\.(jpe?g|png|svg|tiff?|gif|webp|bmp|ico)$/i.test(f), ), ...Object.values(lodash.get(manifest, 'icons', {})), ]; // Files like fonts, things that are not expected const others = lodash.difference(files, css, contentScripts, js, html, img); return { css: validate(css), contentScripts: validate(contentScripts), js: validate(js), html: validate(html), img: validate(img), others: validate(others), } function validate(ary) { return [...new Set(ary.filter(isString))].map((x) => path.join(srcDir, x)) } } var $id$2 = "https://extend-chrome.dev/schema/manifest-strict.schema.json"; var $schema$2 = "http://json-schema.org/draft-07/schema#"; var required = [ "manifest_version", "name", "version" ]; var then = { $ref: "./manifest-v3.schema.json" }; var schema = { $id: $id$2, $schema: $schema$2, required: required, "if": { properties: { manifest_version: { type: "number", "enum": [ 3 ] } } }, then: then, "else": { $ref: "./manifest-v2.schema.json" } }; var $id$1 = "https://extend-chrome.dev/schema/manifest-v2.schema.json"; var $schema$1 = "http://json-schema.org/draft-07/schema#"; var additionalProperties$1 = true; var definitions$1 = { action: { dependencies: { icons: { not: { required: [ "icons" ] } }, name: { not: { required: [ "name" ] } }, popup: { not: { required: [ "popup" ] } } }, properties: { default_icon: { anyOf: [ { description: "FIXME: String form is deprecated.", type: "string" }, { description: "Icon for the main toolbar.", properties: { "19": { $ref: "#/definitions/icon" }, "38": { $ref: "#/definitions/icon" } }, type: "object" } ] }, default_popup: { $ref: "#/definitions/uri", description: "The popup appears when the user clicks the icon." }, default_title: { description: "Tooltip for the main toolbar icon.", type: "string" } }, type: "object" }, command: { additionalProperties: false, properties: { description: { type: "string" }, suggested_key: { additionalProperties: false, patternProperties: { "^(default|mac|windows|linux|chromeos)$": { pattern: "^(Ctrl|Command|MacCtrl|Alt|Option)\\+(Shift\\+)?[A-Z]", type: "string" } }, type: "object" } }, type: "object" }, content_security_policy: { "default": "script-src 'self'; object-src 'self'", description: "This introduces some fairly strict policies that will make extensions more secure by default, and provides you with the ability to create and enforce rules governing the types of content that can be loaded and executed by your extensions and applications.", format: "content-security-policy", type: "string" }, glob_pattern: { format: "glob-pattern", type: "string" }, icon: { $ref: "#/definitions/uri" }, match_pattern: { format: "match-pattern", pattern: "^((\\*|http|https|file|ftp|chrome-extension):\\/\\/(\\*|(([^/*:]+:(\\d{1,5}|\\*)))|(\\*.[^\\/*:]+)|[^\\/*:]+)?(\\/.*))|<all_urls>$", type: "string" }, mime_type: { format: "mime-type", pattern: "^(?:application|audio|image|message|model|multipart|text|video)\\/[-+.\\w]+$", type: "string" }, page: { $ref: "#/definitions/uri" }, permissions: { items: { format: "permission", type: "string" }, type: "array", uniqueItems: true }, scripts: { items: { $ref: "#/definitions/uri" }, minItems: 1, type: "array", uniqueItems: true }, uri: { type: "string" }, version_string: { pattern: "^(?:\\d{1,5}\\.){0,3}\\d{1,5}$", type: "string" } }; var dependencies$1 = { browser_action: { not: { required: [ "page_action" ] } }, content_scripts: { not: { required: [ "script_badge" ] } }, page_action: { not: { required: [ "browser_action" ] } }, script_badge: { not: { required: [ "content_scripts" ] } } }; var properties$1 = { action: { not: { } }, background: { dependencies: { page: { not: { required: [ "scripts" ] } }, scripts: { not: { required: [ "page" ] } } }, description: "The background page is an HTML page that runs in the extension process. It exists for the lifetime of your extension, and only one instance of it at a time is active.", properties: { page: { $ref: "#/definitions/page", "default": "background.html", description: "Specify the HTML of the background page." }, persistent: { "default": true, description: "When false, makes the background page an event page (loaded only when needed).", type: "boolean" }, scripts: { $ref: "#/definitions/scripts", "default": [ "background.js" ], description: "A background page will be generated by the extension system that includes each of the files listed in the scripts property." }, service_worker: { not: { } }, type: { not: { } } }, type: "object" }, browser_action: { $ref: "#/definitions/action", description: "Use browser actions to put icons in the main Google Chrome toolbar, to the right of the address bar. In addition to its icon, a browser action can also have a tooltip, a badge, and a popup." }, chrome_settings_overrides: { }, chrome_url_overrides: { additionalProperties: false, description: "Override pages are a way to substitute an HTML file from your extension for a page that Google Chrome normally provides.", maxProperties: 1, properties: { bookmarks: { $ref: "#/definitions/page", "default": "bookmarks.html", description: "The page that appears when the user chooses the Bookmark Manager menu item from the Chrome menu or, on Mac, the Bookmark Manager item from the Bookmarks menu. You can also get to this page by entering the URL chrome://bookmarks." }, history: { $ref: "#/definitions/page", "default": "history.html", description: "The page that appears when the user chooses the History menu item from the Chrome menu or, on Mac, the Show Full History item from the History menu. You can also get to this page by entering the URL chrome://history." }, newtab: { $ref: "#/definitions/page", "default": "newtab.html", description: "The page that appears when the user creates a new tab or window. You can also get to this page by entering the URL chrome://newtab." } }, type: "object" }, commands: { description: "Use the commands API to add keyboard shortcuts that trigger actions in your extension, for example, an action to open the browser action or send a command to the extension.", patternProperties: { ".*": { $ref: "#/definitions/command" }, "^_execute_browser_action$": { $ref: "#/definitions/command" }, "^_execute_page_action$": { $ref: "#/definitions/command" } }, type: "object" }, content_pack: { }, content_scripts: { description: "Content scripts are JavaScript files that run in the context of web pages.", items: { additionalProperties: false, properties: { all_frames: { "default": false, description: "Controls whether the content script runs in all frames of the matching page, or only the top frame.", type: "boolean" }, css: { description: "The list of CSS files to be injected into matching pages. These are injected in the order they appear in this array, before any DOM is constructed or displayed for the page.", items: { $ref: "#/definitions/uri" }, type: "array", uniqueItems: true }, exclude_globs: { description: "Applied after matches to exclude URLs that match this glob. Intended to emulate the @exclude Greasemonkey keyword.", items: { $ref: "#/definitions/glob_pattern" }, type: "array", uniqueItems: true }, exclude_matches: { description: "Excludes pages that this content script would otherwise be injected into.", items: { $ref: "#/definitions/match_pattern" }, type: "array", uniqueItems: true }, include_globs: { description: "Applied after matches to include only those URLs that also match this glob. Intended to emulate the @include Greasemonkey keyword.", items: { $ref: "#/definitions/glob_pattern" }, type: "array", uniqueItems: true }, js: { $ref: "#/definitions/scripts", description: "The list of JavaScript files to be injected into matching pages. These are injected in the order they appear in this array." }, match_about_blank: { "default": false, description: "Whether to insert the content script on about:blank and about:srcdoc.", type: "boolean" }, matches: { description: "Specifies which pages this content script will be injected into.", items: { $ref: "#/definitions/match_pattern" }, minItems: 1, type: "array", uniqueItems: true }, run_at: { "default": "document_idle", description: "Controls when the files in js are injected.", "enum": [ "document_start", "document_end", "document_idle" ], type: "string" } }, required: [ "matches" ], type: "object" }, minItems: 1, type: "array", uniqueItems: true }, content_security_policy: { $ref: "#/definitions/content_security_policy" }, current_locale: { }, default_locale: { "default": "en", description: "Specifies the subdirectory of _locales that contains the default strings for this extension.", type: "string" }, description: { description: "A plain text description of the extension", maxLength: 132, type: "string" }, devtools_page: { $ref: "#/definitions/page", description: "A DevTools extension adds functionality to the Chrome DevTools. It can add new UI panels and sidebars, interact with the inspected page, get information about network requests, and more." }, externally_connectable: { description: "Declares which extensions, apps, and web pages can connect to your extension via runtime.connect and runtime.sendMessage.", items: { additionalProperties: false, properties: { accepts_tls_channel_id: { "default": false, description: "Indicates that the extension would like to make use of the TLS channel ID of the web page connecting to it. The web page must also opt to send the TLS channel ID to the extension via setting includeTlsChannelId to true in runtime.connect's connectInfo or runtime.sendMessage's options.", type: "boolean" }, ids: { items: { description: "The IDs of extensions or apps that are allowed to connect. If left empty or unspecified, no extensions or apps can connect.", type: "string" }, type: "array" }, matches: { items: { description: "The URL patterns for web pages that are allowed to connect. This does not affect content scripts. If left empty or unspecified, no web pages can connect.", type: "string" }, type: "array" } }, type: "object" }, type: "object" }, file_browser_handlers: { description: "You can use this API to enable users to upload files to your website.", items: { additionalProperties: false, properties: { default_title: { description: "What the button will display.", type: "string" }, file_filters: { description: "Filetypes to match.", items: { type: "string" }, minItems: 1, type: "array" }, id: { description: "Used by event handling code to differentiate between multiple file handlers", type: "string" } }, required: [ "id", "default_title", "file_filters" ], type: "object" }, minItems: 1, type: "array" }, homepage_url: { $ref: "#/definitions/uri", description: "The URL of the homepage for this extension." }, icons: { description: "One or more icons that represent the extension, app, or theme. Recommended format: PNG; also BMP, GIF, ICO, JPEG.", minProperties: 1, properties: { "16": { $ref: "#/definitions/icon", description: "Used as the favicon for an extension's pages and infobar." }, "48": { $ref: "#/definitions/icon", description: "Used on the extension management page (chrome://extensions)." }, "128": { $ref: "#/definitions/icon", description: "Used during installation and in the Chrome Web Store." }, "256": { $ref: "#/definitions/icon", description: "Used during installation and in the Chrome Web Store." } }, type: "object" }, "import": { }, incognito: { "default": "spanning", description: "Specify how this extension will behave if allowed to run in incognito mode.", "enum": [ "spanning", "split", "not_allowed" ], type: "string" }, input_components: { description: "Allows your extension to handle keystrokes, set the composition, and manage the candidate window.", items: { additionalProperties: false, properties: { description: { type: "string" }, id: { type: "string" }, language: { type: "string" }, layouts: { type: "array" }, name: { type: "string" }, type: { type: "string" } }, required: [ "name", "type", "id", "description", "language", "layouts" ], type: "object" }, type: "array" }, key: { description: "This value can be used to control the unique ID of an extension, app, or theme when it is loaded during development.", type: "string" }, manifest_version: { description: "One integer specifying the version of the manifest file format your package requires.", "enum": [ 2 ], maximum: 2, minimum: 2, type: "number" }, minimum_chrome_version: { $ref: "#/definitions/version_string", description: "The version of Chrome that your extension, app, or theme requires, if any." }, nacl_modules: { description: "One or more mappings from MIME types to the Native Client module that handles each type.", items: { additionalProperties: false, properties: { mime_type: { $ref: "#/definitions/mime_type", description: "The MIME type for which the Native Client module will be registered as content handler." }, path: { $ref: "#/definitions/uri", description: "The location of a Native Client manifest (a .nmf file) within the extension directory." } }, required: [ "path", "mime_type" ], type: "object" }, minItems: 1, type: "array", uniqueItems: true }, name: { description: "The name of the extension", maxLength: 45, type: "string" }, oauth2: { additionalProperties: false, description: "Use the Chrome Identity API to authenticate users: the getAuthToken for users logged into their Google Account and the launchWebAuthFlow for users logged into a non-Google account.", properties: { client_id: { description: "You need to register your app in the Google APIs Console to get the client ID.", type: "string" }, scopes: { items: { type: "string" }, minItems: 1, type: "array" } }, required: [ "client_id", "scopes" ], type: "object" }, offline_enabled: { description: "Whether the app or extension is expected to work offline. When Chrome detects that it is offline, apps with this field set to true will be highlighted on the New Tab page.", type: "boolean" }, omnibox: { additionalProperties: false, description: "The omnibox API allows you to register a keyword with Google Chrome's address bar, which is also known as the omnibox.", properties: { keyword: { description: "The keyward that will trigger your extension.", type: "string" } }, required: [ "keyword" ], type: "object" }, optional_permissions: { $ref: "#/definitions/permissions", description: "Use the chrome.permissions API to request declared optional permissions at run time rather than install time, so users understand why the permissions are needed and grant only those that are necessary." }, options_page: { $ref: "#/definitions/page", "default": "options.html", description: "To allow users to customize the behavior of your extension, you may wish to provide an options page. If you do, a link to it will be provided from the extensions management page at chrome://extensions. Clicking the Options link opens a new tab pointing at your options page." }, options_ui: { descript