Parcel 26 ๐ฆ Web Extensions โ @parcel/config-webextension & manifest entries
The @parcel/config-webextension package is a configuration preset, not a plugin. It extends the default Parcel config with the transformers, packagers, and runtimes that a browser extension needs, and it uses the manifest.json file as the entry point instead of an HTML or JavaScript file . The manifest declares the extension’s background script, its content scripts, its popup page, and its icons, and Parcel reads the manifest to discover the files that must be bundled. The build command is parcel build manifest.json --config @parcel/config-webextension, and the .parcelrc file can extend the preset so the --config flag is not needed on every run .
The manifest-entry model is the core difference between a web extension build and a normal web application build. A normal Parcel build starts from an HTML file and follows the script and link tags. A web extension build starts from the manifest and follows the JSON referencesโthe background.service_worker, the content_scripts[].js, the action.default_popup, and the web_accessible_resources . Parcel processes each referenced file according to its extension, so a background.ts is transformed by the TypeScript transformer, a popup.html is processed as an HTML entry, and a content-script.ts is bundled with the runtime that the content script needs .
Key point: @parcel/config-webextension uses manifest.json as the entry point. The manifest’s referencesโbackground, content scripts, popup, options pageโare the files Parcel bundles. Both Manifest V2 and V3 are supported, but the MV3 service worker has a Content Security Policy that disables HMR, so the extension reloads on change instead of hot-updating .
Why the manifest is the entry point
The reference-discovery problem. A web extension has no single HTML entry that references every file. The background script is referenced by the manifest, the content scripts are referenced by the manifest, and the popup is referenced by the manifest. The manifest is the only file that knows about all of them. Parcel’s default build cannot discover these files because they are not linked from an HTML page. The @parcel/config-webextension preset adds a transformer that parses the manifest and treats each reference as a dependency, which is how the files enter the asset graph .
The MV2 vs MV3 difference. Manifest V2 uses background.scripts and a persistent background page. Manifest V3 uses background.service_worker and a non-persistent service worker. Parcel supports both, but the two have different HMR behavior. In MV2, HMR is fully supported by default. In MV3, the service worker’s Content Security Policy restricts the code that can run, and HMR is not supported. A code change causes the extension to reload, which loses the service worker’s state . The --no-hmr flag is the escape hatch for a project that wants the reload behavior without the HMR runtime .
The content script styling problem. A content script’s CSS is injected into the page it runs on, not into the extension’s own page. Parcel collects the CSS imported by a content script and adds it to the css property of the content script entry in the output manifest . The CSS resolves relative URLs against the injected page, not against the extension, so a url(./image.png) in a content script CSS points to the wrong location. The fix is to inline the asset with the data-url: scheme .
The trade-off. The web extension config is a preset that assumes the manifest is the entry. A project that has a build step that generates the manifest, or a project that needs a custom transformer for a file type that the preset does not handle, must extend the preset rather than use it directly. The preset is the starting point, and the .parcelrc file is the customization surface.
a. The setup and the manifest structure
The setup is three steps: install the preset, create the manifest, and add the build script. The .parcelrc file is the optional fourth step that removes the --config flag from the command.
# Install the preset as a dev dependency.
npm install --save-dev @parcel/config-webextension
// .parcelrc โ the preset extension
{
"extends": "@parcel/config-webextension"
}
The manifest is the entry point. The background field points to the service worker (MV3) or the scripts array (MV2). The content_scripts array points to the scripts that run in the context of the matched pages. The action.default_popup points to the popup HTML .
// manifest.json โ the MV3 manifest
{
"manifest_version": 3,
"name": "Sample Web Extension",
"version": "0.0.1",
"background": {
"service_worker": "background.ts",
"type": "module"
},
"content_scripts": [
{
"matches": ["*://github.com/parcel-bundler/*"],
"js": ["parcel-content-script.ts"]
}
],
"action": {
"default_popup": "popup.html"
}
}
The background.ts and parcel-content-script.ts are the source files. Parcel transforms them, bundles them, and rewrites the manifest to point to the output files. The output manifest is the one that the browser loads .
// package.json โ the build scripts
{
"scripts": {
"start": "parcel watch src/manifest.json --host localhost --config @parcel/config-webextension",
"build": "parcel build src/manifest.json --config @parcel/config-webextension"
}
}
The start script runs the watch mode with the --host localhost flag. The flag is sometimes necessary for content script reloading in development. The build script produces the final extension package, which is the contents of the dist directory zipped and uploaded to the browser’s extension store .
b. The MV3 HMR limitation and the content script styling
The MV3 service worker’s Content Security Policy prevents the HMR runtime from running, so Parcel’s HMR does not work in MV3. A code change triggers a full extension reload instead of a hot update . The --no-hmr flag disables the HMR runtime entirely, which is the fix for the background script’s unexpected reload behavior that some projects encounter .
# The watch mode without HMR
parcel watch src/manifest.json --no-hmr --config @parcel/config-webextension
The content script CSS is the other MV3-specific consideration. A content script that imports CSS gets the CSS injected into the page it runs on. The CSS is added to the css array of the content script entry in the output manifest. A relative URL in the CSS resolves against the injected page, not against the extension’s assets. The data-url: scheme inlines the asset, which is the fix for the relative URL problem .
/* content-script.css โ the relative URL resolves against the injected site */
.my-class {
background-image: url(./custom-bg.png); /* points to the site's URL */
}
/* The fix: inline the asset */
.my-other-class {
background-image: url(data-url:./custom-bg.png); /* embeds the asset */
}
The web_accessible_resources field is the other manifest entry that the preset handles. Any resource used in a content script is automatically added to web_accessible_resources, so the developer does not need to list the content script’s dependencies explicitly. A resource that should be accessible from other extensions or websites must be listed explicitly, and Parcel treats the entries as Unix globs rather than Chrome’s recursive globs .
Complete Example Session
// ============================================
// PART 1: THE INSTALLATION
// ============================================
// npm install --save-dev @parcel/config-webextension
// ============================================
// PART 2: THE .parcelrc
// ============================================
// .parcelrc
{
"extends": "@parcel/config-webextension"
}
// The preset is extended, not referenced by the --config flag.
// ============================================
// PART 3: THE MANIFEST
// ============================================
// src/manifest.json
{
"manifest_version": 3,
"name": "Sample Web Extension",
"version": "0.0.1",
"background": {
"service_worker": "background.ts",
"type": "module"
},
"content_scripts": [{
"matches": ["*://github.com/parcel-bundler/*"],
"js": ["parcel-content-script.ts"]
}],
"action": {
"default_popup": "popup.html"
}
}
// ============================================
// PART 4: THE BACKGROUND SCRIPT
// ============================================
// src/background.ts
chrome.runtime.onInstalled.addListener(() => {
console.log('Extension installed');
});
// The TypeScript is transformed by the preset.
// The output is a JavaScript service worker.
// ============================================
// PART 5: THE CONTENT SCRIPT
// ============================================
// src/parcel-content-script.ts
import './content-script.css';
document.body.insertAdjacentHTML('beforeend', '<div class="my-class">Injected</div>');
// The CSS is collected and added to the manifest's css array.
// The relative URL in the CSS resolves against the injected page.
// ============================================
// PART 6: THE CONTENT SCRIPT CSS
// ============================================
// src/content-script.css
.my-class {
background-image: url(data-url:./custom-bg.png);
}
// The data-url: scheme inlines the asset.
// A plain url(./custom-bg.png) would point to the site's URL.
// ============================================
// PART 7: THE POPUP
// ============================================
// src/popup.html
<!DOCTYPE html>
<html>
<body>
<h1>Popup</h1>
<script type="module" src="./popup.ts"></script>
</body>
</html>
// The popup is an HTML entry.
// The script tag references the popup's TypeScript.
// ============================================
// PART 8: THE BUILD SCRIPTS
// ============================================
// package.json
{
"scripts": {
"start": "parcel watch src/manifest.json --host localhost",
"build": "parcel build src/manifest.json"
}
}
// The .parcelrc handles the preset.
// The --config flag is not needed.
// ============================================
// PART 9: THE HMR LIMITATION
// ============================================
// MV3 does not support HMR because of the CSP.
// A code change reloads the extension.
// The --no-hmr flag disables the HMR runtime entirely.
// parcel watch src/manifest.json --no-hmr
// ============================================
// PART 10: THE VERIFICATION
// ============================================
// [ ] The preset is installed.
// [ ] The .parcelrc extends the preset.
// [ ] The manifest is the entry point.
// [ ] The background script is referenced.
// [ ] The content scripts are referenced.
// [ ] The popup is referenced.
// [ ] The content script CSS uses data-url: for local assets.
// [ ] The watch mode uses --host localhost.
// [ ] The MV3 HMR limitation is understood.
The ten parts covered the installation, the .parcelrc, the manifest, the background script, the content script, the content script CSS, the popup, the build scripts, the HMR limitation, and the verification. The through-line is that the manifest is the entry, and the preset handles the references.
Quick Reference
The preset setup
| Step | Command / File |
|---|---|
| Install | npm install -D @parcel/config-webextension |
.parcelrc | { "extends": "@parcel/config-webextension" } |
| Build | parcel build manifest.json |
| Watch | parcel watch manifest.json --host localhost |
The manifest references
| Field | Points to | Transform |
|---|---|---|
background.service_worker | The background script | TypeScript, JavaScript |
content_scripts[].js | The content scripts | TypeScript, JavaScript |
content_scripts[].css | The content script CSS | CSS |
action.default_popup | The popup HTML | HTML |
web_accessible_resources | The accessible resources | Glob patterns |
The MV3 limitations
| Limitation | Cause | Workaround |
|---|---|---|
| No HMR | CSP restriction | Extension reload on change |
| Background reload | HMR runtime | --no-hmr flag |
| CSS relative URLs | Injected into the page | data-url: scheme |
Best Practices
โ Do This:
// Extend the preset in .parcelrc
{ "extends": "@parcel/config-webextension" } // โ
// Use --host localhost for content script reloading
parcel watch manifest.json --host localhost // โ
/* Inline the assets in content script CSS */
background-image: url(data-url:./image.png); // โ
โ Don’t Do This:
// Pass --config on every run
parcel build manifest.json --config @parcel/config-webextension // โ (use .parcelrc)
/* Use a relative URL in content script CSS */
background-image: url(./image.png); // points to the site's URL // โ
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| The background script reloads unexpectedly | MV3 HMR runtime | Use --no-hmr |
| The content script CSS points to the wrong image | Relative URL resolves against the site | Use data-url: |
The build fails with Cannot read properties of undefined | Missing content_scripts in MV3 | Add "content_scripts": [] |
| A devtools panel is not bundled | The HTML is not referenced | Add it to web_accessible_resources |
Real-World Examples
1. The .parcelrc
{ "extends": "@parcel/config-webextension" }
2. The MV3 manifest
{
"manifest_version": 3,
"background": { "service_worker": "background.ts" },
"content_scripts": [{ "matches": ["<all_urls>"], "js": ["content.ts"] }]
}
3. The build command
parcel build manifest.json
4. The watch command
parcel watch manifest.json --host localhost
5. The --no-hmr flag
parcel watch manifest.json --no-hmr
6. The content script CSS
background-image: url(data-url:./bg.png);
7. The popup
<script type="module" src="./popup.ts"></script>
8. The web_accessible_resources
{ "resources": ["panel/index.html"], "matches": ["<all_urls>"] }
9. The devtools panel
{ "devtools_page": "devtools.html" }
10. The verification
ls dist/ # the bundled extension
Visual
The Manifest Entry Model
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE MANIFEST IS THE ENTRY POINT โ
โ โ
โ manifest.json โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ background โโโโถ background.ts โ
โ โ content_scripts โโโโถ content.ts โ
โ โ action.default_popupโโโโถ popup.html โ
โ โโโโโโโโโโโโฌโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Parcel bundles each โ โ
โ โ reference โ โ
โ โโโโโโโโโโโโฌโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ dist/ โ โ
โ โ manifest.json โ The output manifest โ
โ โ background.js โ โ
โ โ content.js โ โ
โ โ popup.html โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The MV3 HMR Limitation
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ MV2 VS MV3 HMR โ
โ โ
โ MV2 MV3 โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ HMR supported โ โ HMR not supported โ โ
โ โ โ โ (CSP restriction) โ โ
โ โ Hot update on save โ โ Extension reload โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ The --no-hmr flag disables the HMR runtime entirely. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Content Script CSS
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE RELATIVE URL RESOLVES AGAINST THE INJECTED PAGE โ
โ โ
โ Content script CSS in the extension โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ background: url( โ โ
โ โ ./image.png) โ โ
โ โโโโโโโโโโโโฌโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ The URL resolves โ โ
โ โ against the site, โ โ
โ โ not the extension โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Fix: url(data-url:./image.png) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
| Preset package | @parcel/config-webextension |
| Entry point | manifest.json |
| Supported manifests | MV2 and MV3 |
| Build command | parcel build manifest.json |
.parcelrc | { "extends": "@parcel/config-webextension" } |
| MV3 HMR | Not supported (CSP) |
| Content script CSS | Use data-url: for local assets |
web_accessible_resources | Auto-populated for content scripts |
Key takeaways:
- The manifest is the entry point, not an HTML file. The
@parcel/config-webextensionpreset parses the manifest and treats every referenceโthe background script, the content scripts, the popupโas a dependency. The build command isparcel build manifest.json, and the.parcelrcfile removes the need for the--configflag . - Both MV2 and MV3 are supported, but the HMR behavior differs. MV2 supports HMR fully. MV3 does not, because the service worker’s Content Security Policy restricts the runtime. A code change in MV3 causes the extension to reload, and the
--no-hmrflag disables the HMR runtime for a project that wants the reload behavior without the runtime . - The content script CSS needs the
data-url:scheme for local assets. A content script’s CSS is injected into the page it runs on, and a relative URL resolves against that page, not against the extension. Thedata-url:scheme inlines the asset and avoids the mismatch . - The
web_accessible_resourcesfield is auto-populated for content scripts. Any resource used in a content script is automatically added toweb_accessible_resources. A resource that should be accessible from other extensions or websites must be listed explicitly, and Parcel treats the entries as Unix globs . - The devtools panel is a special case. A devtools panel’s HTML is not referenced by the manifest’s
devtools_pagedirectly; it is opened by the devtools script. The panel must be listed inweb_accessible_resourcesto be bundled, or Parcel will not include it .
Remember: A Parcel web extension build is a manifest-driven build. The manifest is the entry point, and the preset is the configuration that tells Parcel how to process the manifest’s references. The background script, the content scripts, and the popup are the dependencies, and the output is the bundled extension in the dist directory. The MV3 HMR limitation is the price of the service worker’s security model, and the content script CSS is the case where the injection context changes the meaning of a relative URL. The preset handles the common cases, and the .parcelrc is the customization surface for the rest.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!