{"id":"eb878ca3-e570-4c4a-a45d-4096c24275f3","shortId":"2LcX8m","kind":"skill","title":"crxjs","tagline":"CRXJS Chrome extension development — true HMR for popup, options, content scripts, side panels, manifest-driven builds, dynamic content script imports (`?script`, `?script&module`), and `defineManifest` for type-safe manifests. Uses Vite as its build tool. Use when the user menti","description":"# CRXJS\n\nCRXJS is a Chrome extension development tool that provides true HMR for popup, options, content scripts, and side panels. It reads your manifest to auto-generate the extension output, handles content script injection, and manages the service worker build. Under the hood it is a Vite plugin (`@crxjs/vite-plugin`).\n\n## Current status\n\n- **Package**: `@crxjs/vite-plugin` (v2.x stable, latest v2.4.0 as of March 2026)\n- **Scaffolding**: `npm create crxjs@latest` (always use `@latest`)\n- **Maintained by**: @Toumash and @FliPPeDround (since mid-2025)\n- **GitHub**: github.com/crxjs/chrome-extension-tools (~4k stars)\n- **Vite compatibility**: v3 through v8-beta\n\n## Quick start\n\n```bash\n# Scaffold new project (picks framework interactively)\nnpm create crxjs@latest\n\n# Or add to existing Vite project\nnpm install @crxjs/vite-plugin -D\n```\n\n## Vite config by framework\n\nCRXJS is added as a Vite plugin. The setup varies slightly per framework.\n\n### React\n\n```typescript\n// vite.config.ts\nimport { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\nimport { crx } from \"@crxjs/vite-plugin\";\nimport manifest from \"./manifest.json\";\n\nexport default defineConfig({\n  plugins: [react(), crx({ manifest })],\n});\n```\n\nUse `@vitejs/plugin-react` (not `plugin-react-swc`) for best HMR compatibility. If you must use SWC, cast the manifest:\n\n```typescript\nimport { ManifestV3Export } from \"@crxjs/vite-plugin\";\nconst manifest = manifestJson as ManifestV3Export;\n```\n\n### Vue\n\n```typescript\nimport vue from \"@vitejs/plugin-vue\";\nimport { crx } from \"@crxjs/vite-plugin\";\nimport manifest from \"./manifest.json\";\n\nexport default defineConfig({\n  plugins: [vue(), crx({ manifest })],\n});\n```\n\n### Svelte\n\n```typescript\nimport { svelte } from \"@sveltejs/vite-plugin-svelte\";\nimport { crx } from \"@crxjs/vite-plugin\";\nimport manifest from \"./manifest.json\";\n\nexport default defineConfig({\n  plugins: [svelte(), crx({ manifest })],\n});\n```\n\n### Vanilla TypeScript\n\n```typescript\nimport { crx } from \"@crxjs/vite-plugin\";\nimport manifest from \"./manifest.json\";\n\nexport default defineConfig({\n  plugins: [crx({ manifest })],\n});\n```\n\n## defineManifest — type-safe dynamic manifest\n\nInstead of a static JSON file, use CRXJS's `defineManifest` for dynamic values and full TypeScript autocompletion:\n\n```typescript\n// manifest.ts\nimport { defineManifest } from \"@crxjs/vite-plugin\";\nimport pkg from \"./package.json\";\n\nexport default defineManifest((config) => ({\n  manifest_version: 3,\n  name: config.command === \"serve\" ? `[DEV] ${pkg.name}` : pkg.name,\n  version: pkg.version,\n  description: pkg.description,\n  permissions: [\"storage\", \"activeTab\", \"scripting\"],\n  action: {\n    default_popup: \"src/popup/index.html\",\n    default_icon: {\n      \"16\": \"public/icons/icon16.png\",\n      \"48\": \"public/icons/icon48.png\",\n    },\n  },\n  background: {\n    service_worker: \"src/background/index.ts\",\n    type: \"module\",\n  },\n  content_scripts: [\n    {\n      matches: [\"https://*/*\"],\n      js: [\"src/content/index.ts\"],\n      css: [\"src/content/styles.css\"],\n    },\n  ],\n  options_page: \"src/options/index.html\",\n  side_panel: { default_path: \"src/sidepanel/index.html\" },\n  icons: {\n    \"16\": \"public/icons/icon16.png\",\n    \"48\": \"public/icons/icon48.png\",\n    \"128\": \"public/icons/icon128.png\",\n  },\n}));\n```\n\nImport in vite.config.ts:\n\n```typescript\nimport manifest from \"./manifest\";\n// ... crx({ manifest })\n```\n\n## Type declarations\n\nAdd to a `src/vite-env.d.ts` or `src/crxjs.d.ts`:\n\n```typescript\n/// <reference types=\"@crxjs/vite-plugin/client\" />\n```\n\nThis enables types for `?script` and `?script&module` imports.\n\n## HMR behavior by context\n\n| Context | HMR | How it works |\n| --- | --- | --- |\n| Popup | Full HMR | WebSocket-based, state preserved |\n| Options page | Full HMR | Same as popup |\n| Side panel | Full HMR | Same as popup |\n| Content script (manifest) | True HMR | CRXJS injects loader + HMR client |\n| Content script (dynamic) | True HMR | Via `?script` import |\n| Service worker | Auto-reload | Changes trigger full extension reload |\n| Main world scripts | No HMR | Skipped by CRXJS loader |\n\nContent script HMR works because CRXJS generates a loader script that imports an HMR preamble, the HMR client, and your actual script — enabling real module-level HMR without full page reload. This is CRXJS's main differentiator.\n\n## Dynamic content script imports\n\nFor content scripts injected programmatically (not in manifest), CRXJS provides special import suffixes:\n\n```typescript\n// background.ts — ?script gives you a resolved path for executeScript\nimport contentScript from \"./content?script\";\n\nchrome.action.onClicked.addListener(async (tab) => {\n  await chrome.scripting.executeScript({\n    target: { tabId: tab.id! },\n    files: [contentScript],\n  });\n});\n```\n\nFor main world injection (no HMR):\n\n```typescript\nimport mainWorldScript from \"./inject?script&module\";\n\nawait chrome.scripting.executeScript({\n  target: { tabId },\n  world: \"MAIN\",\n  files: [mainWorldScript],\n});\n```\n\n## CRXJS plugin options\n\n```typescript\ncrx({\n  manifest,\n  browser: \"chrome\", // 'chrome' | 'firefox'\n  contentScripts: {\n    injectCss: true, // auto-inject CSS for content scripts\n    hmrTimeout: 5000, // HMR connection timeout (ms)\n  },\n});\n```\n\n## Development workflow\n\n```bash\n# Start dev server (outputs to dist/ with HMR)\nnpm run dev\n\n# 1. Open chrome://extensions\n# 2. Enable \"Developer mode\"\n# 3. Click \"Load unpacked\"\n# 4. Select the dist/ directory\n# 5. Edit code — popup/content scripts update instantly via HMR\n# 6. Service worker changes trigger automatic extension reload\n```\n\nAfter loading once, subsequent `npm run dev` sessions reconnect automatically. No need to re-load the extension unless manifest.json changes.\n\n## Production build\n\n```bash\nnpm run build    # outputs to dist/\n```\n\nThe dist/ directory is ready to zip and upload to Chrome Web Store:\n\n```bash\ncd dist && zip -r ../extension.zip .\n```\n\nDisable Vite's module preload to avoid CWS rejection of inline scripts:\n\n```typescript\nbuild: {\n  modulePreload: false;\n}\n```\n\n## Known issues and workarounds\n\n### Tailwind CSS HMR in content scripts\n\nNew Tailwind classes may not trigger CSS updates in content scripts. **Workaround**: restart dev server after adding new utility classes. Improved in v2.4.0 but not fully resolved. Ensure `injectCss: true` in config.\n\n### WebSocket connection errors (`ws://localhost:undefined/`)\n\n**Cause**: port mismatch between dev server and HMR config. **Fix**: explicitly set both to the same value:\n\n```typescript\nserver: {\n  port: 5173,\n  strictPort: true,\n  hmr: { port: 5173 },\n}\n```\n\n### \"Manifest version 2 is deprecated\" warning\n\nIf you see this, your manifest is being interpreted as MV2. **Fix**: ensure `\"manifest_version\": 3` is set.\n\n### Content scripts not injecting on file:// URLs\n\nChrome requires the user to enable \"Allow access to file URLs\" in the extension settings at chrome://extensions. CRXJS cannot change this.\n\n### HMR stops working after Chrome update\n\nCRXJS's HMR relies on injecting a content script that connects to the dev server's WebSocket. Chrome security updates occasionally break this. **Fix**: update to the latest CRXJS version, which tracks Chrome changes.\n\n## CRXJS vs alternatives\n\n| Feature | CRXJS | WXT | Plasmo |\n| --- | --- | --- | --- |\n| Content script HMR | True HMR | File-based reload | Partial |\n| Framework support | Any Vite framework | Any | React-focused |\n| Abstraction level | Thin (Vite plugin) | Full framework | Full framework |\n| Messaging helpers | None (use chrome.\\* directly) | Built-in | Built-in |\n| Storage wrappers | None | Built-in | Built-in |\n| Cross-browser | Chrome + Firefox | Chrome + Firefox + Safari | Chrome + Firefox |\n| File-based routing | No | Yes | Yes |\n| Learning curve | Low (know Vite, know CRXJS) | Medium | Medium |\n\n**Choose CRXJS when**: you want minimal abstraction over raw Chrome APIs and value content script HMR above all. CRXJS stays out of the way — no magic routing, no wrapper APIs, just your code with HMR.\n\n**Choose WXT when**: you want conventions, built-in utilities, and cross-browser support.\n\n**Choose Plasmo when**: you're React-focused and want the highest-level abstraction.\n\n## Project structure (recommended)\n\n```\nmy-extension/\n├── src/\n│   ├── background/\n│   │   └── index.ts\n│   ├── content/\n│   │   ├── index.ts\n│   │   └── styles.css\n│   ├── popup/\n│   │   ├── index.html        <- CRXJS resolves HTML entry points\n│   │   ├── App.tsx\n│   │   └── main.tsx\n│   ├── options/\n│   │   ├── index.html\n│   │   └── main.tsx\n│   ├── sidepanel/\n│   │   ├── index.html\n│   │   └── main.tsx\n│   └── shared/\n│       ├── messages.ts\n│       └── storage.ts\n├── public/\n│   └── icons/\n├── manifest.ts               <- or manifest.json\n├── vite.config.ts\n├── tsconfig.json\n└── package.json\n```\n\nCRXJS resolves HTML files referenced in the manifest automatically. Your popup.html can use standard `<script type=\"module\" src=\"./main.tsx\">` and it works.\n\nIf you encounter a bug or unexpected behavior in CRXJS, open an issue at github.com/crxjs/chrome-extension-tools/issues.","tags":["crxjs","skills","samber","agent","agent-skills","antigravity","claude","claude-code","code","codex","coding","copilot"],"capabilities":["skill","source-samber","skill-crxjs","topic-agent","topic-agent-skills","topic-antigravity","topic-claude","topic-claude-code","topic-code","topic-codex","topic-coding","topic-copilot","topic-cursor","topic-gemini","topic-gemini-cli-extension"],"categories":["cc-skills"],"synonyms":[],"warnings":[],"endpointUrl":"https://skills.sh/samber/cc-skills/crxjs","protocol":"skill","transport":"skills-sh","auth":{"type":"none","details":{"cli":"npx skills add samber/cc-skills","source_repo":"https://github.com/samber/cc-skills","install_from":"skills.sh"}},"qualityScore":"0.489","qualityRationale":"deterministic score 0.49 from registry signals: · indexed on github topic:agent-skills · 79 github stars · SKILL.md body (9,102 chars)","verified":false,"liveness":"unknown","lastLivenessCheck":null,"agentReviews":{"count":0,"score_avg":null,"cost_usd_avg":null,"success_rate":null,"latency_p50_ms":null,"narrative_summary":null,"summary_updated_at":null},"enrichmentModel":"deterministic:skill-github:v1","enrichmentVersion":1,"enrichedAt":"2026-05-02T06:55:37.326Z","embedding":null,"createdAt":"2026-04-18T22:13:31.477Z","updatedAt":"2026-05-02T06:55:37.326Z","lastSeenAt":"2026-05-02T06:55:37.326Z","tsv":"'-2025':121 '/content':545 '/crxjs/chrome-extension-tools':125 '/extension.zip':699 '/inject':567 '/manifest':388 '/manifest.json':193,243,264,282 '/package.json':321 '1':618 '128':379 '16':349,375 '2':621,791 '2026':105 '3':328,625,810 '4':629 '48':351,377 '4k':126 '5':634 '5000':599 '5173':783,788 '6':643 'abstract':906,968,1026 'access':826 'action':343 'activetab':341 'actual':497 'ad':164,742 'add':149,393 'allow':825 'altern':882 'alway':111 'api':972,991 'app.tsx':1046 'async':548 'auto':70,461,592 'auto-gener':69 'auto-inject':591 'auto-reload':460 'autocomplet':311 'automat':648,660,1073 'avoid':706 'await':550,570 'background':353,1034 'background.ts':533 'base':423,894,948 'bash':137,606,674,694 'behavior':410 'best':209 'beta':134 'break':867 'browser':584,938,1010 'build':18,37,84,673,677,713 'built':922,925,931,934,1004 'built-in':921,924,930,933,1003 'cannot':837 'cast':217 'caus':763 'cd':695 'chang':463,646,671,838,879 'choos':962,997,1012 'chrome':3,48,585,586,691,819,844,863,878,919,939,941,944,971 'chrome.action.onclicked.addlistener':547 'chrome.scripting.executescript':551,571 'class':728,745 'click':626 'client':449,494 'code':636,994 'compat':129,211 'config':159,325,757,771 'config.command':330 'connect':601,759,856 'const':225 'content':11,20,59,76,359,440,450,477,516,520,596,724,735,813,853,887,975,1036 'contentscript':543,556,588 'context':412,413 'convent':1002 'creat':108,145 'cross':937,1009 'cross-brows':936,1008 'crx':187,199,237,249,258,270,276,287,389,582 'crxjs':1,2,44,45,109,146,162,302,445,475,482,511,527,578,836,846,874,880,884,959,963,980,1041,1065 'crxjs/vite-plugin':93,97,156,189,224,239,260,278,317 'css':364,594,721,732 'current':94 'curv':954 'cws':707 'd':157 'declar':392 'default':195,245,266,284,323,344,347,371 'defineconfig':179,196,246,267,285 'definemanifest':27,289,304,315,324 'deprec':793 'descript':337 'dev':332,608,617,657,739,767,859 'develop':5,50,604,623 'differenti':514 'direct':920 'directori':633,683 'disabl':700 'dist':612,632,680,682,696 'driven':17 'dynam':19,293,306,452,515 'edit':635 'enabl':401,499,622,824 'ensur':753,807 'entri':1044 'error':760 'executescript':541 'exist':151 'explicit':773 'export':194,244,265,283,322 'extens':4,49,73,466,620,649,668,832,835,1032 'fals':715 'featur':883 'file':300,555,576,828,893,947,1068 'file-bas':892,946 'firefox':587,940,942,945 'fix':772,806,869 'flippedround':118 'focus':905,1019 'framework':142,161,174,897,901,912,914 'full':309,419,428,435,465,506,911,913 'fulli':751 'generat':71,483 'github':122 'github.com':124 'github.com/crxjs/chrome-extension-tools':123 'give':535 'handl':75 'helper':916 'highest':1024 'highest-level':1023 'hmr':7,55,210,409,414,420,429,436,444,448,454,472,479,490,493,504,562,600,614,642,722,770,786,840,848,889,891,977,996 'hmrtimeout':598 'hood':87 'html':1043,1067 'icon':348,374,1058 'import':22,178,182,186,190,221,232,236,240,253,257,261,275,279,314,318,381,385,408,457,488,518,530,542,564 'improv':746 'index.html':1040,1049,1052 'index.ts':1035,1037 'inject':78,446,522,560,593,816,851 'injectcss':589,754 'inlin':710 'instal':155 'instant':640 'instead':295 'interact':143 'interpret':803 'issu':717 'js':362 'json':299 'know':956,958 'known':716 'latest':100,110,113,147,873 'learn':953 'level':503,907,1025 'load':627,652,666 'loader':447,476,485 'localhost':761 'low':955 'magic':987 'main':468,513,558,575 'main.tsx':1047,1050,1053 'maintain':114 'mainworldscript':565,577 'manag':80 'manifest':16,32,67,191,200,219,226,241,250,262,271,280,288,294,326,386,390,442,526,583,789,800,808,1072 'manifest-driven':15 'manifest.json':670,1061 'manifest.ts':313,1059 'manifestjson':227 'manifestv3export':222,229 'march':104 'match':361 'may':729 'medium':960,961 'menti':43 'messag':915 'messages.ts':1055 'mid':120 'minim':967 'mismatch':765 'mode':624 'modul':25,358,407,502,569,703 'module-level':501 'modulepreload':714 'ms':603 'must':214 'mv2':805 'my-extens':1030 'name':329 'need':662 'new':139,726,743 'none':917,929 'npm':107,144,154,615,655,675 'occasion':866 'open':619 'option':10,58,366,426,580,1048 'output':74,610,678 'packag':96 'package.json':1064 'page':367,427,507 'panel':14,63,370,434 'partial':896 'path':372,539 'per':173 'permiss':339 'pick':141 'pkg':319 'pkg.description':338 'pkg.name':333,334 'pkg.version':336 'plasmo':886,1013 'plugin':92,168,197,205,247,268,286,579,910 'plugin-react-swc':204 'point':1045 'popup':9,57,345,418,432,439,1039 'popup.html':1075 'popup/content':637 'port':764,782,787 'preambl':491 'preload':704 'preserv':425 'product':672 'programmat':523 'project':140,153,1027 'provid':53,528 'public':1057 'public/icons/icon128.png':380 'public/icons/icon16.png':350,376 'public/icons/icon48.png':352,378 'quick':135 'r':698 'raw':970 're':665,1016 're-load':664 'react':175,183,198,206,904,1018 'react-focus':903,1017 'read':65 'readi':685 'real':500 'recommend':1029 'reconnect':659 'referenc':1069 'reject':708 'reli':849 'reload':462,467,508,650,895 'requir':820 'resolv':538,752,1042,1066 'restart':738 'rout':949,988 'run':616,656,676 'safari':943 'safe':31,292 'scaffold':106,138 'script':12,21,23,24,60,77,342,360,404,406,441,451,456,470,478,486,498,517,521,534,546,568,597,638,711,725,736,814,854,888,976 'secur':864 'see':797 'select':630 'serv':331 'server':609,740,768,781,860 'servic':82,354,458,644 'session':658 'set':774,812,833 'setup':170 'share':1054 'side':13,62,369,433 'sidepanel':1051 'sinc':119 'skill' 'skill-crxjs' 'skip':473 'slight':172 'source-samber' 'special':529 'src':1033 'src/background/index.ts':356 'src/content/index.ts':363 'src/content/styles.css':365 'src/crxjs.d.ts':398 'src/options/index.html':368 'src/popup/index.html':346 'src/sidepanel/index.html':373 'src/vite-env.d.ts':396 'stabl':99 'standard':1078 'star':127 'start':136,607 'state':424 'static':298 'status':95 'stay':981 'stop':841 'storag':340,927 'storage.ts':1056 'store':693 'strictport':784 'structur':1028 'styles.css':1038 'subsequ':654 'suffix':531 'support':898,1011 'svelt':251,254,269 'sveltejs/vite-plugin-svelte':256 'swc':207,216 'tab':549 'tab.id':554 'tabid':553,573 'tailwind':720,727 'target':552,572 'thin':908 'timeout':602 'tool':38,51 'topic-agent' 'topic-agent-skills' 'topic-antigravity' 'topic-claude' 'topic-claude-code' 'topic-code' 'topic-codex' 'topic-coding' 'topic-copilot' 'topic-cursor' 'topic-gemini' 'topic-gemini-cli-extension' 'toumash':116 'track':877 'trigger':464,647,731 'true':6,54,443,453,590,755,785,890 'tsconfig.json':1063 'type':30,291,357,391,402 'type-saf':29,290 'typescript':176,220,231,252,273,274,310,312,384,399,532,563,581,712,780 'undefin':762 'unless':669 'unpack':628 'updat':639,733,845,865,870 'upload':689 'url':818,829 'use':33,39,112,201,215,301,918,1077 'user':42,822 'util':744,1006 'v2.4.0':101,748 'v2.x':98 'v3':130 'v8':133 'v8-beta':132 'valu':307,779,974 'vanilla':272 'vari':171 'version':327,335,790,809,875 'via':455,641 'vite':34,91,128,152,158,167,181,701,900,909,957 'vite.config.ts':177,383,1062 'vitejs/plugin-react':185,202 'vitejs/plugin-vue':235 'vs':881 'vue':230,233,248 'want':966,1001,1021 'warn':794 'way':985 'web':692 'websocket':422,758,862 'websocket-bas':421 'without':505 'work':417,480,842 'workaround':719,737 'worker':83,355,459,645 'workflow':605 'world':469,559,574 'wrapper':928,990 'wxt':885,998 'yes':951,952 'zip':687,697","prices":[{"id":"17382442-a040-429a-8628-9ed388b62a71","listingId":"eb878ca3-e570-4c4a-a45d-4096c24275f3","amountUsd":"0","unit":"free","nativeCurrency":null,"nativeAmount":null,"chain":null,"payTo":null,"paymentMethod":"skill-free","isPrimary":true,"details":{"org":"samber","category":"cc-skills","install_from":"skills.sh"},"createdAt":"2026-04-18T22:13:31.477Z"}],"sources":[{"listingId":"eb878ca3-e570-4c4a-a45d-4096c24275f3","source":"github","sourceId":"samber/cc-skills/crxjs","sourceUrl":"https://github.com/samber/cc-skills/tree/main/skills/crxjs","isPrimary":false,"firstSeenAt":"2026-04-18T22:13:31.477Z","lastSeenAt":"2026-05-02T06:55:37.326Z"}],"details":{"listingId":"eb878ca3-e570-4c4a-a45d-4096c24275f3","quickStartSnippet":null,"exampleRequest":null,"exampleResponse":null,"schema":null,"openapiUrl":null,"agentsTxtUrl":null,"citations":[],"useCases":[],"bestFor":[],"notFor":[],"kindDetails":{"org":"samber","slug":"crxjs","github":{"repo":"samber/cc-skills","stars":79,"topics":["agent","agent-skills","ai","antigravity","claude","claude-code","code","codex","coding","copilot","cursor","gemini","gemini-cli-extension","openclaw","opencode","plugin","skills","skillsmp","vibe-coding"],"license":"mit","html_url":"https://github.com/samber/cc-skills","pushed_at":"2026-05-01T17:07:53Z","description":"🧑‍🎨 A collection of agentic skills that works","skill_md_sha":"1f37d7dbc248d6eb70e6210ceeca4c613f6ff397","skill_md_path":"skills/crxjs/SKILL.md","default_branch":"main","skill_tree_url":"https://github.com/samber/cc-skills/tree/main/skills/crxjs"},"layout":"multi","source":"github","category":"cc-skills","frontmatter":{"name":"crxjs","license":"MIT","description":"CRXJS Chrome extension development — true HMR for popup, options, content scripts, side panels, manifest-driven builds, dynamic content script imports (`?script`, `?script&module`), and `defineManifest` for type-safe manifests. Uses Vite as its build tool. Use when the user mentions CRXJS, crxjs, @crxjs/vite-plugin, 'extension with hot reload', 'HMR for chrome extension', or wants to set up a CRXJS-based Chrome extension project with any framework (React, Vue, Svelte, Solid, Vanilla). Also trigger when the user has an existing CRXJS project and wants to add features, fix HMR issues, or configure content scripts with CRXJS. For general Chrome extension architecture (messaging, CSP, storage, permissions) -> See `samber/cc-skills@chrome-extension` skill.","compatibility":"Designed for Claude Code or similar AI coding agents. Requires git, node."},"skills_sh_url":"https://skills.sh/samber/cc-skills/crxjs"},"updatedAt":"2026-05-02T06:55:37.326Z"}}