UNPKG

extension

Version:

Create cross-browser extensions with no build configuration.

875 lines (838 loc) 52.7 kB
#!/usr/bin/env node "use strict"; var __webpack_require__ = {}; (()=>{ __webpack_require__.n = (module)=>{ var getter = module && module.__esModule ? ()=>module['default'] : ()=>module; __webpack_require__.d(getter, { a: getter }); return getter; }; })(); (()=>{ __webpack_require__.d = (exports1, definition)=>{ for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, { enumerable: true, get: definition[key] }); }; })(); (()=>{ __webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop); })(); var __webpack_exports__ = {}; const external_commander_namespaceObject = require("commander"); const external_update_check_namespaceObject = require("update-check"); var external_update_check_default = /*#__PURE__*/ __webpack_require__.n(external_update_check_namespaceObject); const external_pintor_namespaceObject = require("pintor"); var external_pintor_default = /*#__PURE__*/ __webpack_require__.n(external_pintor_namespaceObject); function getLoggingPrefix(type) { const isAuthor = 'true' === process.env.EXTENSION_AUTHOR_MODE; if (isAuthor) { const base = 'error' === type ? 'ERROR Author says' : '►►► Author says'; return external_pintor_default().brightMagenta(base); } if ('error' === type) return external_pintor_default().red('ERROR'); if ('warn' === type) return external_pintor_default().brightYellow('►►►'); if ('info' === type) return external_pintor_default().gray('►►►'); return external_pintor_default().green('►►►'); } const code = (text)=>external_pintor_default().blue(text); const arg = (text)=>external_pintor_default().gray(text); const fmt = { heading: (title)=>external_pintor_default().underline(external_pintor_default().blue(title)), label: (k)=>external_pintor_default().gray(k.toUpperCase()), val: (v)=>external_pintor_default().underline(v), code: (v)=>external_pintor_default().blue(v), bullet: (s)=>`- ${s}`, block (title, rows) { const head = fmt.heading(title); const body = rows.map(([k, v])=>`${fmt.label(k)} ${v}`).join('\n'); return `${head}\n${body}`; }, truncate (input, max = 800) { const s = (()=>{ try { return 'string' == typeof input ? input : JSON.stringify(input); } catch { return String(input); } })(); return s.length > max ? s.slice(0, max) + '…' : s; } }; const commandDescriptions = { create: 'Creates a new extension from a template (React, TypeScript, Vue, Svelte, etc.)', dev: 'Starts the development server with hot reloading', start: 'Builds and starts the extension in production mode', preview: 'Previews the extension in production mode without building', build: 'Builds the extension for packaging/distribution', cleanup: 'Cleans up orphaned instances and frees unused ports' }; function unhandledError(err) { const message = err instanceof Error ? err.stack || err.message : 'string' == typeof err ? err : fmt.truncate(err); return `${getLoggingPrefix('error')} ${external_pintor_default().red(String(message || 'Unknown error'))}`; } function updateFailed(err) { return `${getLoggingPrefix('error')} Failed to check for updates.\n${external_pintor_default().red(String(err?.message || err))}`; } function checkUpdates(packageJson, update) { return `${getLoggingPrefix('info')} 🧩 ${external_pintor_default().blue('Extension.js')} update available.\n\nYou are currently using version ${external_pintor_default().red(String(packageJson.version))}. Latest stable is ${external_pintor_default().green(String(update.latest))}.\nUpdate to the latest stable to get fixes and new features.`; } function programUserHelp() { return `\n${getLoggingPrefix('info')} ${external_pintor_default().underline('Help center for the Extension.js program')} Usage: extension [command] [options] Notes - All high-level commands offer their own \`--help\` with usage and flag lists. Example - ${code('extension create --help')} outputs information about the "create" command. Available Commands - ${code('extension create ' + arg('<project-name|project-path>'))} ${commandDescriptions.create} - ${code('extension dev ' + arg('[project-path|remote-url]'))} ${commandDescriptions.dev} - ${code('extension start ' + arg('[project-path|remote-url]'))} ${commandDescriptions.start} - ${code('extension preview ' + arg('[project-path|remote-url]'))} ${commandDescriptions.preview} - ${code('extension build ' + arg('[project-path|remote-url]'))} ${commandDescriptions.build} - ${code('extension cleanup')} ${commandDescriptions.cleanup} Common Options - ${code('--browser')} ${arg('<chrome|edge|firefox|chromium|chromium-based|gecko-based|firefox-based>')} Target browser/engine (default: chrome) - ${code('--profile')} ${arg('<path|boolean>')} Browser profile configuration - ${code('--polyfill')} ${arg('[boolean]')} Enable/disable cross-browser polyfill - ${code('--no-telemetry')} Disable anonymous telemetry for this run - ${code('--port')} ${arg('<number>')} Development server port (default: 8080) - ${code('--starting-url')} ${arg('<url>')} Initial URL to load in browser - ${code('--silent')} ${arg('[boolean]')} Suppress console output during build Source Inspection - ${code('--source')} ${arg('<url|boolean>')} Open URL and print HTML after content scripts inject - When provided without a URL, falls back to ${arg('--starting-url')} or ${arg('https://example.com')} - Watch mode is enabled by default when ${code('--source')} is present Browser-Specific Options - ${code('--chromium-binary')} ${arg('<path>')} Custom Chromium binary path - ${code('--gecko-binary')}/${code('--firefox-binary')} ${arg('<path>')} Custom Firefox/Gecko binary path Build Options - ${code('--zip')} ${arg('[boolean]')} Create ZIP archive of built extension - ${code('--zip-source')} ${arg('[boolean]')} Include source files in ZIP - ${code('--zip-filename')} ${arg('<name>')} Custom ZIP filename ${external_pintor_default().underline('Centralized Logger (terminal output)')} - The manager extension embeds a centralized logger that streams events to the CLI. - Enable and filter logs directly via ${code('extension dev')} flags: - ${code('--logs')} ${arg('<off|error|warn|info|debug|trace>')} Minimum level (default: info) - ${code('--log-context')} ${arg('<list|all>')} Contexts: background,content,page,sidebar,popup,options,devtools - ${code('--log-format')} ${arg('<pretty|json>')} Output format (default: pretty) - ${code('--no-log-timestamps')} Hide ISO timestamps in pretty output - ${code('--no-log-color')} Disable color in pretty output - ${code('--log-url')} ${arg('<substring|/regex/>')} Filter by event.url - ${code('--log-tab')} ${arg('<id>')} Filter by tabId - Example: ${code('extension dev ./my-ext --logs=debug --log-context=all --log-format=pretty')} ${code('extension --help')} This command outputs a help file with key command options. ${external_pintor_default().underline('Path Resolution (important)')} - Leading ${code('/')} in manifest/HTML means extension root (the directory containing ${code('manifest.json')}). - Relative paths resolve from the ${code('manifest.json')} directory. - Absolute OS paths are used as-is. AI Assistants - For AI-oriented guidance and deeper tips, run ${code('extension --ai-help')} Report issues - ${external_pintor_default().underline('https://github.com/cezaraugusto/extension/issues/new')}`; } function unsupportedBrowserFlag(value, supported) { return `${getLoggingPrefix('error')} Unsupported --browser value: ${value}. Supported: ${supported.join(', ')}.`; } function programAIHelp() { return `\n${getLoggingPrefix('info')} ${external_pintor_default().gray('Development tips for extension developers and AI assistants')} Browser-Specific Configuration - Use browser prefixes in manifest.json for browser-specific fields: ${code('{"firefox:manifest": 2, "chrome:manifest": 3}')} This applies manifest v2 to Firefox only, v3 to Chrome/Edge. Centralized Logger (for AI & CI) - Logs from all contexts are centralized by the manager extension and streamed to the CLI. - Prefer these flags to control terminal logs during ${code('extension dev')}: - ${code('--logs')} ${arg('<off|error|warn|info|debug|trace>')} Minimum level - ${code('--log-context')} ${arg('<list|all>')} Contexts to include - ${code('--log-format')} ${arg('<pretty|json>')} Pretty for humans; JSON for machines/NDJSON pipelines - ${code('--no-log-timestamps')} ${arg(' ')} Disable timestamps (pretty) - ${code('--no-log-color')} ${arg(' ')} Disable ANSI colors (pretty) - ${code('--log-url')} ${arg('<substring|/regex/>')} Filter by URL - ${code('--log-tab')} ${arg('<id>')} Filter by tabId - Good CI pattern: ${code('EXTENSION_AUTHOR_MODE=development EXTENSION_AUTO_EXIT_MS=6000 extension dev ./ext --logs=info --log-format=json')} Special Folders for Entrypoints - Use special folders to handle entrypoints and assets not declared in manifest.json: - ${external_pintor_default().underline(code('public/'))} - Static assets automatically copied to build (resolves to output root) - ${external_pintor_default().underline(code('pages/'))} - HTML files not declared in manifest (e.g., welcome pages) - ${external_pintor_default().underline(code("scripts/"))} - JavaScript files not declared in manifest (e.g., executable scripts) Predictable Output Paths - Core HTML destinations are standardized across browsers so you can reference them safely in code/tests: - ${code('devtools_page')} → ${code('devtools/index.html')} - ${code('sidebar_action.default_panel')} (MV2) and ${code('side_panel.default_path')} (MV3) → ${code('sidebar/index.html')} - ${code('options_ui.page')} and ${code('options_page')} → ${code('options/index.html')} - ${code('background.page')} → ${code('background/index.html')} - ${code('action.default_popup')}, ${code('browser_action.default_popup')}, ${code('page_action.default_popup')} → ${code('action/index.html')} - Other predictable outputs: - ${code('chrome_url_overrides.*')} → ${code('chrome_url_overrides/<key>.html')} - ${code("content_scripts[n].js/css")} → ${code("content_scripts/content-<n>.{js,css}")} - ${code('sandbox.pages[]')} → ${code('sandbox/page-<n>.html')} - ${code("user_scripts.api_script")} → ${code("user_scripts/api_script.js")} - ${code('icons/*')} → ${code('icons/')} (feature-specific icon folders preserved where applicable) Public & Special Folders (Output Behavior) - ${external_pintor_default().underline(code('public/'))} is the web root in output. Authors can use ${code('/foo')}, ${code('/public/foo')}, ${code('public/foo')}, or ${code('./public/foo')} and they all emit as ${code('dist/<browser>/foo')}. - ${external_pintor_default().underline(code('pages/'))} files emit as ${code('pages/<name>.html')}. Relative assets referenced inside page HTML are emitted under ${code('assets/')} preserving relative structure; public-root URLs are preserved. - ${external_pintor_default().underline(code("scripts/"))} files emit as ${code("scripts/<name>.js")} with extracted CSS when applicable. Shadow DOM for Content Scripts - Add ${code('use shadow-dom')} directive to content scripts for style isolation - Automatically creates ${code('#extension-root')} element with shadow DOM - All CSS imports are automatically injected into shadow DOM - Prevents style conflicts with host page Environment Variables - Use ${code(arg('EXTENSION_PUBLIC_*'))} prefix for variables accessible in extension code - Supported in both ${code('process.env')} and ${code('import.meta.env')} - Environment file priority: ${external_pintor_default().underline(code(arg('.env.{browser}.{mode}')))} > ${external_pintor_default().underline(code(arg('.env.{browser}')))} > ${external_pintor_default().underline(code(arg('.env.{mode}')))} > ${external_pintor_default().underline(code(arg('.env')))} - Example: ${code(arg('EXTENSION_PUBLIC_API_KEY=your_key'))} Available Templates - ${external_pintor_default().green('Frameworks')}: ${code(arg('react'))}, ${code(arg('preact'))}, ${code(arg('vue'))}, ${code(arg('svelte'))} - ${external_pintor_default().green('Languages')}: ${code(arg("javascript"))}, ${code(arg("typescript"))} - ${external_pintor_default().green('Contexts')}: ${code(arg('content'))} (content scripts), ${code(arg('new'))} (new tab), ${code(arg('action'))} (popup) - ${external_pintor_default().green('Styling')}: ${code(arg('tailwind'))}, ${code(arg('sass'))}, ${code(arg('less'))} - ${external_pintor_default().green('Configs')}: ${code(arg('eslint'))}, ${code(arg('prettier'))}, ${code(arg('stylelint'))} Webpack/Rspack Configuration - Create ${external_pintor_default().underline(code(arg('extension.config.js')))} for custom webpack configuration - Function receives base config, return modified config - Supports all webpack/rspack loaders and plugins - Example: ${code('export default {')} ${code(' config: (config) => {')} ${code(" config.module.rules.push({ test: /\\.svg$/, use: ['@svgr/webpack'] })")} ${code(' return config')} ${code(' }')} ${code('}')} Managed Dependencies (Important) - ${external_pintor_default().green('Do not add')} packages that ${external_pintor_default().blue('Extension.js')} already ships in its own toolchain. - The guard only triggers when a managed package is declared in your ${code('package.json')} ${external_pintor_default().gray('and')} is referenced in your ${external_pintor_default().underline(code('extension.config.js'))}. - In that case, the program will ${external_pintor_default().red('print an error and abort')} to avoid version conflicts. - Remove the duplicate from your project ${code('package.json')} or avoid referencing it in ${external_pintor_default().underline(code('extension.config.js'))} and rely on the built-in version instead. - If you truly need a different version, open an issue so we can evaluate a safe upgrade. Framework-Specific Configuration - Create ${external_pintor_default().underline(code(arg('vue.loader.js')))} for Vue-specific loader configuration - Create ${external_pintor_default().underline(code(arg('svelte.loader.js')))} for Svelte-specific loader configuration - Automatically detected and used by Extension.js - Example svelte.loader.js: ${code('module.exports = {')} ${code(' preprocess: require("svelte-preprocess")({')} ${code(" typescript: true")} ${code(' })')} ${code('}')} Hot Module Replacement (HMR) - Automatically enabled in development mode - CSS changes trigger automatic style updates - React/Preact/Vue/Svelte components hot reload - Content scripts automatically re-inject on changes - Service workers, _locales and manifest changes reload the extension Source Inspection & Real-Time Monitoring - Use ${code('--source')} ${arg('<url|boolean>')} to inspect page HTML after content script injection - When no URL is provided, falls back to ${arg('--starting-url')} or ${arg('https://example.com')} - Watch mode is enabled by default when ${code('--source')} is present - Automatically enables Chrome remote debugging (port 9222) when source inspection is active - Extracts Shadow DOM content from ${code('#extension-root')} or ${code('[data-extension-root=\"true\"]')} elements - Useful for debugging content script behavior and style injection - Example: ${code('extension dev --source=' + arg('https://example.com'))} Non-Destructive Testing in CI - Prefer ${code('EXTENSION_AUTHOR_MODE=development')} to copy local templates and avoid network. - Reuse Playwright's Chromium via ${code('--chromium-binary')} path when available. - Set ${code(arg('EXTENSION_AUTO_EXIT_MS'))} and ${code(arg('EXTENSION_FORCE_KILL_MS'))} for non-interactive dev sessions. File Watching & HMR Examples - Content script JS/TS changes trigger reinjection; CSS changes update styles live. - For watch-source HTML prints, update a visible string in ${code("content/scripts.*")} and assert it appears in stdout. Troubleshooting - If HTML is not printed, ensure ${code('--source')} is provided and browser launched with debugging port. - Use ${code('--silent true')} during builds to reduce noise; logs still surface errors. - When ports conflict, pass ${code('--port 0')} to auto-select an available port. Non-Interactive / Auto Mode (CI) - Set ${code(arg('EXTENSION_AUTO_EXIT_MS'))} to enable self-termination after N milliseconds. Useful when ${code('pnpm extension dev')} would otherwise hang under Rspack watch. Example: ${code(arg('EXTENSION_AUTO_EXIT_MS=6000'))} pnpm extension dev ./templates/react --browser chrome --source ${arg('https://example.com')} - Optional: ${code(arg('EXTENSION_FORCE_KILL_MS'))} to hard-exit after N ms as a fallback (defaults to auto-exit + 4000). Cross-Browser Compatibility - Use ${code('--polyfill')} flag to enable webextension-polyfill - Automatically handles browser API differences - Supports Chrome, Edge, Firefox with single codebase`; } const external_semver_namespaceObject = require("semver"); const external_node_fs_namespaceObject = require("node:fs"); var external_node_fs_default = /*#__PURE__*/ __webpack_require__.n(external_node_fs_namespaceObject); const external_node_path_namespaceObject = require("node:path"); var external_node_path_default = /*#__PURE__*/ __webpack_require__.n(external_node_path_namespaceObject); let cachedPackageJson = null; function getCliPackageJson() { if (cachedPackageJson) return cachedPackageJson; const candidates = [ external_node_path_default().resolve(__dirname, 'package.json'), external_node_path_default().resolve(__dirname, '..', 'package.json') ]; for (const candidate of candidates)if (external_node_fs_default().existsSync(candidate)) { const content = external_node_fs_default().readFileSync(candidate, 'utf8'); const parsed = JSON.parse(content); cachedPackageJson = parsed; return parsed; } throw new Error('Extension.js CLI package.json not found.'); } function isStableVersion(version) { const v = external_semver_namespaceObject.parse(version); return Boolean(v && 0 === v.prerelease.length); } async function check_updates_checkUpdates() { const packageJson = getCliPackageJson(); let update = null; try { update = await external_update_check_default()(packageJson); } catch (err) { if ('true' === process.env.EXTENSION_AUTHOR_MODE) console.error(updateFailed(err)); } if (update && isStableVersion(update.latest)) return checkUpdates(packageJson, update); return null; } const external_fs_namespaceObject = require("fs"); var external_fs_default = /*#__PURE__*/ __webpack_require__.n(external_fs_namespaceObject); const external_path_namespaceObject = require("path"); var external_path_default = /*#__PURE__*/ __webpack_require__.n(external_path_namespaceObject); const external_node_os_namespaceObject = require("node:os"); var external_node_os_default = /*#__PURE__*/ __webpack_require__.n(external_node_os_namespaceObject); const external_node_crypto_namespaceObject = require("node:crypto"); var external_node_crypto_default = /*#__PURE__*/ __webpack_require__.n(external_node_crypto_namespaceObject); function _define_property(obj, key, value) { if (key in obj) Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); else obj[key] = value; return obj; } function isCI() { const v = process.env; return Boolean(v.CI || v.GITHUB_ACTIONS || v.GITLAB_CI || v.BUILDKITE || v.CIRCLECI || v.TRAVIS); } function configDir() { const xdg = process.env.XDG_CONFIG_HOME; if (xdg) return external_node_path_default().join(xdg, 'extensionjs'); if ('win32' === process.platform && process.env.APPDATA) return external_node_path_default().join(process.env.APPDATA, 'extensionjs'); return external_node_path_default().join(external_node_os_default().homedir(), '.config', 'extensionjs'); } function ensureDir(p) { if (external_node_fs_default().existsSync(p)) return; external_node_fs_default().mkdirSync(p, { recursive: true }); } function loadOrCreateId(file) { if (external_node_fs_default().existsSync(file)) return external_node_fs_default().readFileSync(file, 'utf8').trim(); const id = external_node_crypto_default().randomUUID(); ensureDir(external_node_path_default().dirname(file)); external_node_fs_default().writeFileSync(file, id, 'utf8'); return id; } function auditFilePath() { const dir = external_node_path_default().join(configDir(), 'telemetry'); ensureDir(dir); return external_node_path_default().join(dir, 'events.jsonl'); } const DEFAULT_FLUSH_AT = Number(process.env.EXTENSION_TELEMETRY_FLUSH_AT || 10); const DEFAULT_FLUSH_INTERVAL = Number(process.env.EXTENSION_TELEMETRY_FLUSH_INTERVAL || 2000); const DEFAULT_TIMEOUT_MS = Number(process.env.EXTENSION_TELEMETRY_TIMEOUT_MS || 200); class Telemetry { track(event, props = {}) { if (this.disabled) return; const payload = { event, properties: { ...this.common, ...props, $ip: null }, distinct_id: this.anonId }; external_node_fs_default().appendFileSync(auditFilePath(), JSON.stringify(payload) + '\n'); if (this.debug) console.error('[telemetry]', JSON.stringify(payload)); if (!this.apiKey || !this.host) return; this.buffer.push(payload); if (this.buffer.length >= DEFAULT_FLUSH_AT) return void this.flush(); if (!this.timer) this.timer = setTimeout(()=>{ this.timer = null; this.flush(); }, DEFAULT_FLUSH_INTERVAL); } async flush() { if (this.disabled || !this.apiKey || !this.host) return; if (0 === this.buffer.length) return; const batch = this.buffer.splice(0, this.buffer.length); try { const ac = new AbortController(); const t = setTimeout(()=>ac.abort(), DEFAULT_TIMEOUT_MS); const url = new URL('/capture/', this.host); await fetch(url.toString(), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ api_key: this.apiKey, batch: batch.map((e)=>({ event: e.event, properties: e.properties, distinct_id: e.distinct_id })) }), signal: ac.signal, keepalive: true }).catch(()=>{}); clearTimeout(t); } catch {} } shutdown() {} constructor(init){ _define_property(this, "anonId", void 0); _define_property(this, "common", void 0); _define_property(this, "debug", void 0); _define_property(this, "disabled", void 0); _define_property(this, "apiKey", void 0); _define_property(this, "host", void 0); _define_property(this, "buffer", []); _define_property(this, "timer", null); this.debug = '1' === process.env.EXTENSION_TELEMETRY_DEBUG; this.disabled = Boolean(init.disabled); this.anonId = 'disabled'; if (!this.disabled) { const idFile = external_node_path_default().join(configDir(), 'telemetry', 'anonymous-id'); this.anonId = loadOrCreateId(idFile); } this.common = { app: init.app, version: init.version, os: process.platform, arch: process.arch, node: process.versions.node, is_ci: isCI(), schema_version: 1 }; this.apiKey = init.apiKey || process.env.EXTENSION_PUBLIC_POSTHOG_KEY; this.host = init.host || process.env.EXTENSION_PUBLIC_POSTHOG_HOST; if (!this.disabled) { const consentPath = external_node_path_default().join(configDir(), 'telemetry', 'consent'); if (!external_node_fs_default().existsSync(consentPath)) { external_node_fs_default().writeFileSync(consentPath, 'ok', 'utf8'); this.track('cli_telemetry_consent', { value: 'implicit_opt_in' }); console.log(`${external_pintor_default().gray('►►►')} Telemetry is enabled for Extension.js. To opt out, run with --no-telemetry. Learn more in TELEMETRY.md.`); } } } } function summarizeManifest(manifest) { const mv = manifest?.manifest_version === 2 ? 2 : 3; const permissions = Array.isArray(manifest?.permissions) ? manifest.permissions : []; const optionalPermissions = Array.isArray(manifest?.optional_permissions) ? manifest.optional_permissions : []; const hostPermissions = Array.isArray(manifest?.host_permissions) ? manifest.host_permissions : []; const usesAllUrls = [ ...permissions, ...hostPermissions ].includes('<all_urls>'); const usesDeclarativeNetRequest = permissions.includes('declarativeNetRequest') || permissions.includes('declarativeNetRequestWithHostAccess'); const background = manifest?.background; let backgroundType = 'none'; if (3 === mv && background?.service_worker) backgroundType = 'service_worker'; else if (2 === mv && (Array.isArray(background?.scripts) && background.scripts.length > 0 || background?.page)) backgroundType = 'event_page'; const contentScriptsCount = Array.isArray(manifest?.content_scripts) ? manifest.content_scripts.length : 0; const hasDevtoolsPage = Boolean(manifest?.devtools_page); const hasActionPopup = Boolean(manifest?.action?.default_popup); return { mv, permissions_count: permissions.length, optional_permissions_count: optionalPermissions.length, host_permissions_count: hostPermissions.length, uses_all_urls: usesAllUrls, uses_declarative_net_request: usesDeclarativeNetRequest, background_type: backgroundType, content_scripts_count: contentScriptsCount, has_devtools_page: hasDevtoolsPage, has_action_popup: hasActionPopup }; } function isTelemetryDisabledFromArgs(argv) { return argv.includes('--no-telemetry'); } const telemetryDisabled = isTelemetryDisabledFromArgs(process.argv); function findManifestJson(projectRoot) { const stack = [ projectRoot ]; while(stack.length > 0){ const dir = stack.pop(); if (!dir) continue; let entries; try { entries = external_fs_default().readdirSync(dir, { withFileTypes: true }); } catch { continue; } for (const entry of entries){ if (entry.isFile() && 'manifest.json' === entry.name) return external_path_default().join(dir, entry.name); if (entry.isDirectory() && !entry.name.startsWith('.') && 'node_modules' !== entry.name && 'dist' !== entry.name) stack.push(external_path_default().join(dir, entry.name)); } } return null; } const telemetry_cli_telemetry = new Telemetry({ app: 'extension', version: getCliPackageJson().version, disabled: telemetryDisabled }); if (!telemetryDisabled) { const startedAt = Date.now(); const known = new Set([ 'create', 'dev', 'start', 'preview', 'build', 'cleanup' ]); const invoked = process.argv.slice(2).find((a)=>known.has(a)) || 'unknown'; telemetry_cli_telemetry.track('cli_boot', { command_guess: invoked }); const manifestPath = findManifestJson(process.cwd()); if (manifestPath) { const raw = external_fs_default().readFileSync(manifestPath, 'utf8'); const json = JSON.parse(raw); const summary = summarizeManifest(json); telemetry_cli_telemetry.track('manifest_summary', summary); } process.on('beforeExit', async function() { telemetry_cli_telemetry.track('cli_shutdown', { command_guess: invoked, duration_ms: Date.now() - startedAt, exit_code: process.exitCode ?? 0 }); await telemetry_cli_telemetry.flush(); }); process.on('uncaughtException', function(err) { telemetry_cli_telemetry.track('cli_error', { command_guess: invoked, error_name: String(err?.name || 'Error').slice(0, 64) }); }); process.on('unhandledRejection', function(reason) { telemetry_cli_telemetry.track('cli_error', { command_guess: invoked, error_name: String(reason?.name || 'PromiseRejection').slice(0, 64) }); }); } require("node:url"); function parseOptionalBoolean(value) { if (void 0 === value) return true; const normalized = String(value).trim().toLowerCase(); return ![ 'false', '0', 'no', 'off' ].includes(normalized); } const vendors = (browser)=>{ const value = browser ?? 'chromium'; return 'all' === value ? [ 'chrome', 'edge', 'firefox' ] : String(value).split(','); }; function validateVendorsOrExit(vendorsList, onInvalid) { const supported = [ 'chrome', 'edge', 'firefox', 'chromium', 'chromium-based', 'gecko-based', 'firefox-based' ]; for (const v of vendorsList)if (!supported.includes(v)) { onInvalid(v, supported); process.exit(1); } } function registerCreateCommand(program, telemetry) { program.command('create').arguments('<project-name|project-path>').usage('create <project-name|project-path> [options]').description(commandDescriptions.create).option('-t, --template <template-name>', 'specify a template for the created project').option('--install [boolean]', 'whether or not to install the dependencies after creating the project (enabled by default)', parseOptionalBoolean, true).action(async function(pathOrRemoteUrl, { template, install }) { const startedAt = Date.now(); telemetry.track('cli_command_start', { command: 'create', template: template || 'default', install: Boolean(install) }); try { const { extensionCreate } = await import("extension-create"); const { ensureDependencies } = await import("extension-develop"); const projectPath = external_path_namespaceObject.isAbsolute(pathOrRemoteUrl) ? pathOrRemoteUrl : external_path_namespaceObject.join(process.cwd(), pathOrRemoteUrl); await extensionCreate(pathOrRemoteUrl, { template, install, cliVersion: getCliPackageJson().version }); if (install) await ensureDependencies(projectPath, { skipProjectInstall: true, showRunAgainMessage: false }); telemetry.track('cli_command_finish', { command: 'create', duration_ms: Date.now() - startedAt, success: true, exit_code: 0 }); } catch (err) { telemetry.track('cli_command_finish', { command: 'create', duration_ms: Date.now() - startedAt, success: false, exit_code: 1 }); throw err; } }); } function normalizeSourceOption(source, startingUrl) { if (!source) return; const hasExplicitSourceString = 'string' == typeof source && 'true' !== String(source).trim().toLowerCase(); const hasStartingUrl = 'string' == typeof startingUrl && String(startingUrl).trim().length > 0; if (!hasExplicitSourceString) return hasStartingUrl ? String(startingUrl) : 'https://example.com'; return String(source); } function parseLogContexts(raw) { if (!raw || 0 === String(raw).trim().length) return; if ('all' === String(raw).trim().toLowerCase()) return; const allowed = [ 'background', 'content', 'page', 'sidebar', 'popup', 'options', 'devtools' ]; const values = String(raw).split(',').map((s)=>s.trim()).filter((s)=>s.length > 0).filter((c)=>allowed.includes(c)); return values.length > 0 ? values : void 0; } function registerDevCommand(program, telemetry) { program.command('dev').arguments('[project-path|remote-url]').usage('dev [project-path|remote-url] [options]').description(commandDescriptions.dev).option('--profile <path-to-file | boolean>', 'what path to use for the browser profile. A boolean value of false sets the profile to the default user profile. Defaults to a fresh profile').option('--browser <chrome | chromium | edge | firefox | chromium-based | gecko-based | firefox-based>', 'specify a browser/engine to run. Defaults to `chromium`').option('--chromium-binary <path-to-binary>', 'specify a path to the Chromium binary. This option overrides the --browser setting. Defaults to the system default').option('--gecko-binary, --firefox-binary <path-to-binary>', 'specify a path to the Gecko binary. This option overrides the --browser setting. Defaults to the system default').option('--polyfill [boolean]', 'whether or not to apply the cross-browser polyfill. Defaults to `false`').option('--no-open', 'do not open the browser automatically (default: open)').option('--starting-url <url>', 'specify the starting URL for the browser. Defaults to `undefined`').option('--port <port>', 'specify the port to use for the development server. Defaults to `8080`').option('--log-context <list>', '[experimental] comma-separated contexts to include (background,content,page,sidebar,popup,options,devtools). Use `all` to include all contexts (default)').option('--logs <off|error|warn|info|debug|trace|all>', '[experimental] minimum centralized logger level to display in terminal (default: off)').option('--log-format <pretty|json>', '[experimental] output format for logger events. Defaults to `pretty`').option('--no-log-timestamps', 'disable ISO timestamps in pretty output').option('--no-log-color', 'disable color in pretty output').option('--log-url <pattern>', '[experimental] only show logs where event.url matches this substring or regex (/re/i)').option('--log-tab <id>', 'only show logs for a specific tabId (number)').option('--source [url]', "[experimental] opens the provided URL in Chrome and prints the full, live HTML of the page after content scripts are injected").option('--author, --author-mode', '[internal] enable maintainer diagnostics (does not affect user runtime logs)').action(async function(pathOrRemoteUrl, { browser = 'chromium', ...devOptions }) { if (devOptions.author || devOptions['authorMode']) { process.env.EXTENSION_AUTHOR_MODE = 'true'; if (!process.env.EXTENSION_VERBOSE) process.env.EXTENSION_VERBOSE = '1'; } const cmdStart = Date.now(); telemetry.track('cli_command_start', { command: 'dev', vendors: vendors(browser), polyfill_used: devOptions.polyfill?.toString() !== 'false', log_level: devOptions.logLevel || 'off', log_format: devOptions.logFormat || 'pretty', custom_binary_used: Boolean(devOptions.chromiumBinary || devOptions.geckoBinary) }); const list = vendors(browser); validateVendorsOrExit(list, (invalid, supported)=>{ console.error(unsupportedBrowserFlag(invalid, supported)); }); const normalizedSource = normalizeSourceOption(devOptions.source, devOptions.startingUrl); if (normalizedSource) { devOptions.source = normalizedSource; devOptions.watchSource = true; } const { extensionDev } = await import("extension-develop"); for (const vendor of list){ const vendorStart = Date.now(); telemetry.track('cli_vendor_start', { command: 'dev', vendor }); const logsOption = devOptions.logs; const logContextOption = devOptions.logContext; const devArgs = { ...devOptions, profile: devOptions.profile, browser: vendor, chromiumBinary: devOptions.chromiumBinary, geckoBinary: devOptions.geckoBinary, polyfill: devOptions.polyfill?.toString() !== 'false', open: devOptions.open, startingUrl: devOptions.startingUrl, source: devOptions.source, watchSource: devOptions.watchSource, logLevel: logsOption || devOptions.logLevel || 'off', logContexts: parseLogContexts(logContextOption), logFormat: devOptions.logFormat || 'pretty', logTimestamps: false !== devOptions.logTimestamps, logColor: false !== devOptions.logColor, logUrl: devOptions.logUrl, logTab: devOptions.logTab }; await extensionDev(pathOrRemoteUrl, devArgs); telemetry.track('cli_vendor_finish', { command: 'dev', vendor, duration_ms: Date.now() - vendorStart }); } telemetry.track('cli_command_finish', { command: 'dev', duration_ms: Date.now() - cmdStart, success: 0 === process.exitCode || null == process.exitCode, exit_code: process.exitCode ?? 0 }); }); } function registerStartCommand(program, telemetry) { program.command('start').arguments('[project-path|remote-url]').usage('start [project-path|remote-url] [options]').description(commandDescriptions.start).option('--profile <path-to-file | boolean>', 'what path to use for the browser profile. A boolean value of false sets the profile to the default user profile. Defaults to a fresh profile').option('--browser <chrome | chromium | edge | firefox | chromium-based | gecko-based | firefox-based>', 'specify a browser/engine to run. Defaults to `chromium`').option('--polyfill [boolean]', 'whether or not to apply the cross-browser polyfill. Defaults to `true`').option('--chromium-binary <path-to-binary>', 'specify a path to the Chromium binary. This option overrides the --browser setting. Defaults to the system default').option('--gecko-binary, --firefox-binary <path-to-binary>', 'specify a path to the Gecko binary. This option overrides the --browser setting. Defaults to the system default').option('--starting-url <url>', 'specify the starting URL for the browser. Defaults to `undefined`').option('--port <port>', 'specify the port to use for the development server. Defaults to `8080`').option('--log-context <list>', '[experimental] comma-separated contexts to include (background,content,page,sidebar,popup,options,devtools). Use `all` to include all contexts (default)').option('--logs <off|error|warn|info|debug|trace|all>', '[experimental] minimum centralized logger level to display in terminal (default: off)').option('--log-format <pretty|json>', '[experimental] output format for logger events. Defaults to `pretty`').option('--no-log-timestamps', 'disable ISO timestamps in pretty output').option('--no-log-color', 'disable color in pretty output').option('--log-url <pattern>', '[experimental] only show logs where event.url matches this substring or regex (/re/i)').option('--log-tab <id>', 'only show logs for a specific tabId (number)').option('--source [url]', "[experimental] opens the provided URL in Chrome and prints the full, live HTML of the page after content scripts are injected").option('--author, --author-mode', '[internal] enable maintainer diagnostics (does not affect user runtime logs)').action(async function(pathOrRemoteUrl, { browser = 'chromium', ...startOptions }) { if (startOptions.author || startOptions.authorMode) { process.env.EXTENSION_AUTHOR_MODE = 'true'; if (!process.env.EXTENSION_VERBOSE) process.env.EXTENSION_VERBOSE = '1'; } const cmdStart = Date.now(); telemetry.track('cli_command_start', { command: 'start', vendors: vendors(browser), polyfill_used: startOptions.polyfill?.toString() !== 'false' }); const list = vendors(browser); validateVendorsOrExit(list, (invalid, supported)=>{ console.error(unsupportedBrowserFlag(invalid, supported)); }); const { extensionStart } = await import("extension-develop"); for (const vendor of list){ const vendorStart = Date.now(); telemetry.track('cli_vendor_start', { command: 'start', vendor }); const logsOption = startOptions.logs; const logContextOption = startOptions.logContext; const logContexts = parseLogContexts(logContextOption); await extensionStart(pathOrRemoteUrl, { mode: 'production', profile: startOptions.profile, browser: vendor, chromiumBinary: startOptions.chromiumBinary, geckoBinary: startOptions.geckoBinary, startingUrl: startOptions.startingUrl, port: startOptions.port, source: 'string' == typeof startOptions.source ? startOptions.source : startOptions.source, watchSource: startOptions.watchSource, logLevel: logsOption || startOptions.logLevel || 'off', logContexts, logFormat: startOptions.logFormat || 'pretty', logTimestamps: false !== startOptions.logTimestamps, logColor: false !== startOptions.logColor, logUrl: startOptions.logUrl, logTab: startOptions.logTab }); telemetry.track('cli_vendor_finish', { command: 'start', vendor, duration_ms: Date.now() - vendorStart }); } telemetry.track('cli_command_finish', { command: 'start', duration_ms: Date.now() - cmdStart, success: 0 === process.exitCode || null == process.exitCode, exit_code: process.exitCode ?? 0 }); }); } function registerPreviewCommand(program, telemetry) { program.command('preview').arguments('[project-name]').usage('preview [path-to-remote-extension] [options]').description(commandDescriptions.preview).option('--profile <path-to-file | boolean>', 'what path to use for the browser profile. A boolean value of false sets the profile to the default user profile. Defaults to a fresh profile').option('--browser <chrome | chromium | edge | firefox | chromium-based | gecko-based | firefox-based>', 'specify a browser/engine to run. Defaults to `chromium`').option('--chromium-binary <path-to-binary>', 'specify a path to the Chromium binary. This option overrides the --browser setting. Defaults to the system default').option('--gecko-binary, --firefox-binary <path-to-binary>', 'specify a path to the Gecko binary. This option overrides the --browser setting. Defaults to the system default').option('--starting-url <url>', 'specify the starting URL for the browser. Defaults to `undefined`').option('--port <port>', 'specify the port to use for the development server. Defaults to `8080`').option('--log-context <list>', '[experimental] comma-separated contexts to include (background,content,page,sidebar,popup,options,devtools). Use `all` to include all contexts (default)').option('--logs <off|error|warn|info|debug|trace|all>', '[experimental] minimum centralized logger level to display in terminal (default: off)').option('--log-format <pretty|json>', '[experimental] output format for logger events. Defaults to `pretty`').option('--no-log-timestamps', 'disable ISO timestamps in pretty output').option('--no-log-color', 'disable color in pretty output').option('--log-url <pattern>', '[experimental] only show logs where event.url matches this substring or regex (/re/i)').option('--log-tab <id>', 'only show logs for a specific tabId (number)').option('--source [url]', "[experimental] opens the provided URL in Chrome and prints the full, live HTML of the page after content scripts are injected").option('--author, --author-mode', '[internal] enable maintainer diagnostics (does not affect user runtime logs)').action(async function(pathOrRemoteUrl, { browser = 'chromium', ...previewOptions }) { if (previewOptions.author || previewOptions['authorMode']) { process.env.EXTENSION_AUTHOR_MODE = 'true'; if (!process.env.EXTENSION_VERBOSE) process.env.EXTENSION_VERBOSE = '1'; } const cmdStart = Date.now(); telemetry.track('cli_command_start', { command: 'preview', vendors: vendors(browser) }); const list = vendors(browser); validateVendorsOrExit(list, (invalid, supported)=>{ console.error(unsupportedBrowserFlag(invalid, supported)); }); if (!process.env.EXTJS_LIGHT) { const isRemote = 'string' == typeof pathOrRemoteUrl && /^https?:/i.test(pathOrRemoteUrl); if (isRemote) process.env.EXTJS_LIGHT = '1'; } const { extensionPreview } = await import("extension-develop"); for (const vendor of list){ const vendorStart = Date.now(); telemetry.track('cli_vendor_start', { command: 'preview', vendor }); const logsOption = previewOptions.logs; const logContextOption = previewOptions.logContext; const logContexts = parseLogContexts(logContextOption); await extensionPreview(pathOrRemoteUrl, { mode: 'production', profile: previewOptions.profile, browser: vendor, chromiumBinary: previewOptions.chromiumBinary, geckoBinary: previewOptions.geckoBinary, startingUrl: previewOptions.startingUrl, port: previewOptions.port, source: 'string' == typeof previewOptions.source ? previewOptions.source : previewOptions.source, watchSource: previewOptions.watchSource, logLevel: logsOption || previewOptions.logLevel || 'off', logContexts, logFormat: previewOptions.logFormat || 'pretty', logTimestamps: false !== previewOptions.logTimestamps, logColor: false !== previewOptions.logColor, logUrl: previewOptions.logUrl, logTab: previewOptions.logTab }); telemetry.track('cli_vendor_finish', { command: 'preview', vendor, duration_ms: Date.now() - vendorStart }); } telemetry.track('cli_command_finish', { command: 'preview', duration_ms: Date.now() - cmdStart, success: 0 === process.exitCode || null == process.exitCode, exit_code: process.exitCode ?? 0 }); }); } function registerBuildCommand(program, telemetry) { program.command('build').arguments('[project-name]').usage('build [path-to-remote-extension] [options]').description(commandDescriptions.build).option('--browser <chrome | chromium | edge | firefox | chromium-based | gecko-based | firefox-based>', 'specify a browser/engine to run. Defaults to `chromium`').option('--polyfill [boolean]', 'whether or not to apply the cross-browser polyfill. Defaults to `false`').option('--zip [boolean]', 'whether or not to compress the extension into a ZIP file. Defaults to `false`').option('--zip-source [boolean]', 'whether or not to include the source files in the ZIP file. Defaults to `false`').option('--zip-filename <string>', 'specify the name of the ZIP file. Defaults to the extension name and version').option('--silent [boolean]', 'whether or not to open the browser automatically. Defaults to `false`').option('--author, --author-mode', '[internal] enable maintainer diagnostics (does not affect user runtime logs)').action(async function(pathOrRemoteUrl, { browser = 'chromium', ...buildOptions }) { if (buildOptions.author || buildOptions['authorMode']) { process.env.EXTENSION_AUTHOR_MODE = 'true'; if (!process.env.EXTENSION_VERBOSE) process.env.EXTENSION_VERBOSE = '1'; } const cmdStart = Date.now(); telemetry.track('cli_command_start', { command: 'build', vendors: vendors(browser), polyfill_used: buildOptions.polyfill || false, zip: buildOptions.zip || false, zip_source: buildOptions.zipSource || false }); const list = vendors(browser); validateVendorsOrExit(list, (invalid, supported)=>{ console.error(unsupportedBrowserFlag(invalid, supported)); }); const { extensionBuild } = await import("extension-develo