UNPKG

extension

Version:

Create cross-browser extensions with no build configuration.

346 lines (314 loc) 24.5 kB
#!/usr/bin/env node "use strict"; 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 });