extension
Version:
Create cross-browser extensions with no build configuration.
346 lines (314 loc) • 24.5 kB
JavaScript
;
var __webpack_modules__ = {
"extension-develop": function(module) {
module.exports = import("extension-develop").then(function(module) {
return module;
});
}
};
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {
var cachedModule = __webpack_module_cache__[moduleId];
if (void 0 !== cachedModule) return cachedModule.exports;
var module = __webpack_module_cache__[moduleId] = {
exports: {}
};
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
(()=>{
__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_extension_create_namespaceObject = require("extension-create");
const external_pintor_namespaceObject = require("pintor");
var external_pintor_default = /*#__PURE__*/ __webpack_require__.n(external_pintor_namespaceObject);
function getLoggingPrefix(type) {
if ('error' === type) return external_pintor_default().red('ERROR');
if ('warn' === type) return external_pintor_default().brightYellow("\u25BA\u25BA\u25BA");
if ('info' === type) return external_pintor_default().blue("\u25BA\u25BA\u25BA");
return external_pintor_default().green("\u25BA\u25BA\u25BA");
}
const code = (text)=>external_pintor_default().blue(text);
const arg = (text)=>external_pintor_default().gray(text);
function updateFailed(err) {
return `${getLoggingPrefix('error')} Failed to check for updates.\n${external_pintor_default().red(String((null == err ? void 0 : err.message) || err))}`;
}
function checkUpdates(packageJson, update) {
return `${external_pintor_default().blue('Extension.js')} update available.\nYou are currently using version ${external_pintor_default().gray(String(packageJson.version))}. Latest stable is ${external_pintor_default().gray(String(update.latest))}. Please update to enjoy new features and improvements.`;
}
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>'))}
Creates a new extension from a template (React, TypeScript, Vue, Svelte, etc.)
- ${code('extension dev ' + arg('[project-path|remote-url]'))}
Starts a development server with hot reloading
- ${code('extension start ' + arg('[project-path|remote-url]'))}
Builds and starts the extension in production mode
- ${code('extension preview ' + arg('[project-path|remote-url]'))}
Previews the extension in production mode without building
- ${code('extension build ' + arg('[project-path|remote-url]'))}
Builds the extension for packaging/distribution
- ${code('extension cleanup')}
Cleans up orphaned instances and frees unused ports
Common Options
- ${code('--browser')} ${arg('<chrome|edge|firefox>')} Target browser (default: chrome)
- ${code('--profile')} ${arg('<path|boolean>')} Browser profile configuration
- ${code('--polyfill')} ${arg('[boolean]')} Enable/disable cross-browser polyfill
- ${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>')} Open URL and print HTML after content scripts inject
- ${code('--watch-source')} Monitor rebuild events and print HTML on reloads
Browser-Specific Options
- ${code('--chromium-binary')} ${arg('<path>')} Custom Chromium binary path
- ${code('--gecko-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
${code('extension --help')}
This command outputs a help file with key command options.
AI Assistants
- For AI-oriented guidance and deep-dive 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.
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)
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>')} to inspect page HTML after content script injection
- Use ${code('--watch-source')} to monitor real-time changes in stdout
- Automatically enables Chrome remote debugging (port 9222) when source inspection is active
- Extracts Shadow DOM content from ${code('#extension-root')} elements
- Perfect for debugging content script behavior and style injection
- Example: ${code('extension dev --source=' + arg('https://example.com') + ' --watch-source')}
Non-Destructive Testing in CI
- Prefer ${code('EXTENSION_ENV=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_update_check_namespaceObject = require("update-check");
var external_update_check_default = /*#__PURE__*/ __webpack_require__.n(external_update_check_namespaceObject);
function isStableVersion(version) {
return !/[a-zA-Z]/.test(version);
}
async function check_updates_checkUpdates(packageJson) {
let update = null;
try {
update = await external_update_check_default()(packageJson);
} catch (err) {
if ('development' === process.env.EXTENSION_ENV) console.error(updateFailed(err));
}
if (update && isStableVersion(update.latest)) console.log(checkUpdates(packageJson, update));
}
var package_namespaceObject = JSON.parse('{"license":"MIT","repository":{"type":"git","url":"https://github.com/extension-js/extension.js.git","directory":"programs/cli"},"engines":{"node":">=18"},"exports":{".":{"types":"./dist/cli.d.ts","import":"./dist/cli.js","require":"./dist/cli.js"}},"main":"./dist/cli.js","types":"./dist/cli.d.ts","files":["dist","types"],"bin":{"extension":"./dist/cli.js"},"name":"extension","version":"2.0.4","description":"Create cross-browser extensions with no build configuration.","author":{"name":"Cezar Augusto","email":"boss@cezaraugusto.net","url":"https://cezaraugusto.com"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"scripts":{"watch":"rslib build --watch","compile":"rslib build","clean":"rm -rf dist","test":"echo \\"Note: no test specified\\" && exit 0","test:cli":"vitest run"},"keywords":["zero-config","build","develop","browser","extension","chrome extension","edge extension","firefox extension","safari extension","web","react","typescript","webextension","browser-extension","chrome-extension","firefox-addon","edge-extension","safari-web-extension","manifest-v3","mv3","cross-browser","content-script","background-script","devtools","create-extension","scaffold","starter-template","boilerplate","cli"],"dependencies":{"@types/chrome":"^0.0.287","@types/node":"^22.10.1","@types/react":"^19.0.1","@types/react-dom":"^19.0.1","@types/webextension-polyfill":"0.12.3","commander":"^12.1.0","extension-create":"workspace:*","extension-develop":"workspace:*","pintor":"0.3.0","semver":"^7.6.3","update-check":"^1.5.4","webextension-polyfill":"^0.12.0"},"devDependencies":{"@rslib/core":"^0.6.9","@types/mock-fs":"^4.13.4","@types/semver":"^7.5.8","mock-fs":"^5.4.1","tsconfig":"*","typescript":"5.7.2","vitest":"3.2.2"}}');
function parseOptionalBoolean(value) {
if (void 0 === value) return true;
const normalized = String(value).trim().toLowerCase();
return ![
'false',
'0',
'no',
'off'
].includes(normalized);
}
check_updates_checkUpdates(package_namespaceObject);
const extensionJs = external_commander_namespaceObject.program;
const vendors = (browser)=>'all' === browser ? 'chrome,edge,firefox'.split(',') : browser.split(',');
function validateVendorsOrExit(vendorsList) {
const supported = [
'chrome',
'edge',
'firefox'
];
for (const v of vendorsList)if (!supported.includes(v)) {
console.error(unsupportedBrowserFlag(v, supported));
process.exit(1);
}
}
extensionJs.name(package_namespaceObject.name).description(package_namespaceObject.description).version(package_namespaceObject.version).option('--ai-help', 'show AI-assistant oriented help and tips').addHelpText('after', programUserHelp());
extensionJs.command('create').arguments('<project-name|project-path>').usage('create <project-name|project-path> [options]').description('Creates a new extension.').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 (disabled by default)', parseOptionalBoolean, false).action(async function(pathOrRemoteUrl, { template, install }) {
await (0, external_extension_create_namespaceObject.extensionCreate)(pathOrRemoteUrl, {
template,
install,
cliVersion: package_namespaceObject.version
});
});
extensionJs.command('dev').arguments('[project-path|remote-url]').usage('dev [project-path|remote-url] [options]').description('Starts the development server (development mode)').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 | edge | firefox>', 'specify a browser to preview your extension in production mode. Defaults to `chrome`').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 <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('--open [boolean]', 'whether or not to open the browser automatically. Defaults to `true`').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('--source [url]', "opens the provided URL in Chrome and prints the full, live HTML of the page after content scripts are injected").option('--watch-source', 'continuously monitors rebuild events and prints updated HTML whenever the extension reloads and reinjects into the page').action(async function(pathOrRemoteUrl, { browser = 'chrome', ...devOptions }) {
const list = vendors(browser);
validateVendorsOrExit(list);
const { extensionDev } = await Promise.resolve().then(__webpack_require__.bind(__webpack_require__, "extension-develop"));
for (const vendor of list){
var _devOptions_polyfill;
await extensionDev(pathOrRemoteUrl, {
...devOptions,
profile: devOptions.profile,
browser: vendor,
chromiumBinary: devOptions.chromiumBinary,
geckoBinary: devOptions.geckoBinary,
polyfill: (null == (_devOptions_polyfill = devOptions.polyfill) ? void 0 : _devOptions_polyfill.toString()) !== 'false',
open: devOptions.open,
startingUrl: devOptions.startingUrl,
source: devOptions.source,
watchSource: devOptions.watchSource
});
}
});
extensionJs.command('start').arguments('[project-path|remote-url]').usage('start [project-path|remote-url] [options]').description('Starts the development server (production mode)').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 | edge | firefox>', 'specify a browser to preview your extension in production mode. Defaults to `chrome`').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 <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`').action(async function(pathOrRemoteUrl, { browser = 'chrome', ...startOptions }) {
const list = vendors(browser);
validateVendorsOrExit(list);
const { extensionStart } = await Promise.resolve().then(__webpack_require__.bind(__webpack_require__, "extension-develop"));
for (const vendor of list)await extensionStart(pathOrRemoteUrl, {
mode: 'production',
profile: startOptions.profile,
browser: vendor,
chromiumBinary: startOptions.chromiumBinary,
geckoBinary: startOptions.geckoBinary,
startingUrl: startOptions.startingUrl
});
});
extensionJs.command('preview').arguments('[project-name]').usage('preview [path-to-remote-extension] [options]').description('Preview the extension in production mode').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 | edge | firefox>', 'specify a browser to preview your extension in production mode. Defaults to `chrome`').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 <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`').action(async function(pathOrRemoteUrl, { browser = 'chrome', ...previewOptions }) {
const list = vendors(browser);
validateVendorsOrExit(list);
const { extensionPreview } = await Promise.resolve().then(__webpack_require__.bind(__webpack_require__, "extension-develop"));
for (const vendor of list)await extensionPreview(pathOrRemoteUrl, {
mode: 'production',
profile: previewOptions.profile,
browser: vendor,
chromiumBinary: previewOptions.chromiumBinary,
geckoBinary: previewOptions.geckoBinary,
startingUrl: previewOptions.startingUrl
});
});
extensionJs.command('build').arguments('[project-name]').usage('build [path-to-remote-extension] [options]').description('Builds the extension for production').option('--browser <chrome | edge | firefox>', 'specify a browser to preview your extension in production mode. Defaults to `chrome`').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`').action(async function(pathOrRemoteUrl, { browser = 'chrome', ...buildOptions }) {
const list = vendors(browser);
validateVendorsOrExit(list);
const { extensionBuild } = await Promise.resolve().then(__webpack_require__.bind(__webpack_require__, "extension-develop"));
for (const vendor of list)await extensionBuild(pathOrRemoteUrl, {
browser: vendor,
polyfill: buildOptions.polyfill,
zip: buildOptions.zip,
zipSource: buildOptions.zipSource,
zipFilename: buildOptions.zipFilename,
silent: buildOptions.silent
});
});
extensionJs.command('cleanup').description('Clean up orphaned instances and free unused ports').action(async function() {
const { cleanupCommand } = await Promise.resolve().then(__webpack_require__.bind(__webpack_require__, "extension-develop"));
await cleanupCommand();
});
extensionJs.on('option:ai-help', function() {
console.log(programAIHelp());
process.exit(0);
});
extensionJs.parse();
})();
for(var __webpack_i__ in __webpack_exports__)exports[__webpack_i__] = __webpack_exports__[__webpack_i__];
Object.defineProperty(exports, '__esModule', {
value: true
});