UNPKG

web-ext-run

Version:

A tool to open and run web extensions

208 lines (200 loc) 6.96 kB
import path from 'path'; import { createWriteStream } from 'fs'; import fs from 'fs/promises'; import parseJSON from 'parse-json'; import stripBom from 'strip-bom'; import defaultFromEvent from 'promise-toolbox/fromEvent'; import zipDir from 'zip-dir'; import defaultSourceWatcher from '../watcher.js'; import getValidatedManifest, { getManifestId } from '../util/manifest.js'; import { prepareArtifactsDir } from '../util/artifacts.js'; import { createLogger } from '../util/logger.js'; import { UsageError, isErrorWithCode } from '../errors.js'; import { createFileFilter as defaultFileFilterCreator } from '../util/file-filter.js'; const log = createLogger(import.meta.url); const DEFAULT_FILENAME_TEMPLATE = '{name}-{version}.zip'; export function safeFileName(name) { return name.toLowerCase().replace(/[^a-z0-9.-]+/g, '_'); } // defaultPackageCreator types and implementation. // This defines the _locales/messages.json type. See: // https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Internationalization#Providing_localized_strings_in__locales export async function getDefaultLocalizedName({ messageFile, manifestData }) { let messageData; let messageContents; let extensionName = manifestData.name; try { messageContents = await fs.readFile(messageFile, { encoding: 'utf-8' }); } catch (error) { throw new UsageError(`Error reading messages.json file at ${messageFile}: ${error}`); } messageContents = stripBom(messageContents); const { default: stripJsonComments } = await import('strip-json-comments'); try { messageData = parseJSON(stripJsonComments(messageContents)); } catch (error) { throw new UsageError(`Error parsing messages.json file at ${messageFile}: ${error}`); } extensionName = manifestData.name.replace(/__MSG_([A-Za-z0-9@_]+?)__/g, (match, messageName) => { if (!(messageData[messageName] && messageData[messageName].message)) { const error = new UsageError(`The locale file ${messageFile} ` + `is missing key: ${messageName}`); throw error; } else { return messageData[messageName].message; } }); return Promise.resolve(extensionName); } // https://stackoverflow.com/a/22129960 export function getStringPropertyValue(prop, obj) { const properties = prop.split('.'); const value = properties.reduce((prev, curr) => prev && prev[curr], obj); if (!['string', 'number'].includes(typeof value)) { throw new UsageError(`Manifest key "${prop}" is missing or has an invalid type: ${value}`); } const stringValue = `${value}`; if (!stringValue.length) { throw new UsageError(`Manifest key "${prop}" value is an empty string`); } return stringValue; } function getPackageNameFromTemplate(filenameTemplate, manifestData) { const packageName = filenameTemplate.replace(/{([A-Za-z0-9._]+?)}/g, (match, manifestProperty) => { return safeFileName(getStringPropertyValue(manifestProperty, manifestData)); }); // Validate the resulting packageName string, after interpolating the manifest property // specified in the template string. const parsed = path.parse(packageName); if (parsed.dir) { throw new UsageError(`Invalid filename template "${filenameTemplate}". ` + `Filename "${packageName}" should not contain a path`); } if (!['.zip', '.xpi'].includes(parsed.ext)) { throw new UsageError(`Invalid filename template "${filenameTemplate}". ` + `Filename "${packageName}" should have a zip or xpi extension`); } return packageName; } export async function defaultPackageCreator({ manifestData, sourceDir, fileFilter, artifactsDir, overwriteDest, showReadyMessage, filename = DEFAULT_FILENAME_TEMPLATE }, { fromEvent = defaultFromEvent } = {}) { let id; if (manifestData) { id = getManifestId(manifestData); log.debug(`Using manifest id=${id || '[not specified]'}`); } else { manifestData = await getValidatedManifest(sourceDir); } const buffer = await zipDir(sourceDir, { filter: (...args) => fileFilter.wantFile(...args) }); let filenameTemplate = filename; let { default_locale } = manifestData; if (default_locale) { default_locale = default_locale.replace(/-/g, '_'); const messageFile = path.join(sourceDir, '_locales', default_locale, 'messages.json'); log.debug('Manifest declared default_locale, localizing extension name'); const extensionName = await getDefaultLocalizedName({ messageFile, manifestData }); // allow for a localized `{name}`, without mutating `manifestData` filenameTemplate = filenameTemplate.replace(/{name}/g, extensionName); } const packageName = safeFileName(getPackageNameFromTemplate(filenameTemplate, manifestData)); const extensionPath = path.join(artifactsDir, packageName); // Added 'wx' flags to avoid overwriting of existing package. const stream = createWriteStream(extensionPath, { flags: 'wx' }); stream.write(buffer, () => { stream.end(); }); try { await fromEvent(stream, 'close'); } catch (error) { if (!isErrorWithCode('EEXIST', error)) { throw error; } if (!overwriteDest) { throw new UsageError(`Extension exists at the destination path: ${extensionPath}\n` + 'Use --overwrite-dest to enable overwriting.'); } log.info(`Destination exists, overwriting: ${extensionPath}`); const overwriteStream = createWriteStream(extensionPath); overwriteStream.write(buffer, () => { overwriteStream.end(); }); await fromEvent(overwriteStream, 'close'); } if (showReadyMessage) { log.info(`Your web extension is ready: ${extensionPath}`); } return { extensionPath }; } // Build command types and implementation. export default async function build({ sourceDir, artifactsDir, asNeeded = false, overwriteDest = false, ignoreFiles = [], filename = DEFAULT_FILENAME_TEMPLATE }, { manifestData, createFileFilter = defaultFileFilterCreator, fileFilter = createFileFilter({ sourceDir, artifactsDir, ignoreFiles }), onSourceChange = defaultSourceWatcher, packageCreator = defaultPackageCreator, showReadyMessage = true } = {}) { const rebuildAsNeeded = asNeeded; // alias for `build --as-needed` log.info(`Building web extension from ${sourceDir}`); const createPackage = () => packageCreator({ manifestData, sourceDir, fileFilter, artifactsDir, overwriteDest, showReadyMessage, filename }); await prepareArtifactsDir(artifactsDir); const result = await createPackage(); if (rebuildAsNeeded) { log.info('Rebuilding when files change...'); onSourceChange({ sourceDir, artifactsDir, onChange: () => { return createPackage().catch(error => { log.error(error.stack); throw error; }); }, shouldWatchFile: (...args) => fileFilter.wantFile(...args) }); } return result; } //# sourceMappingURL=build.js.map