extension
Version:
Create cross-browser extensions with no build configuration.
875 lines (838 loc) • 52.7 kB
JavaScript
#!/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