| | |

Parcel 26 ๐Ÿ“ฆ Web Extensions โ€” @parcel/config-webextension & manifest entries

โฑ๏ธ Estimated read: 12 min | |

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

StepCommand / File
Installnpm install -D @parcel/config-webextension
.parcelrc{ "extends": "@parcel/config-webextension" }
Buildparcel build manifest.json
Watchparcel watch manifest.json --host localhost

The manifest references

FieldPoints toTransform
background.service_workerThe background scriptTypeScript, JavaScript
content_scripts[].jsThe content scriptsTypeScript, JavaScript
content_scripts[].cssThe content script CSSCSS
action.default_popupThe popup HTMLHTML
web_accessible_resourcesThe accessible resourcesGlob patterns

The MV3 limitations

LimitationCauseWorkaround
No HMRCSP restrictionExtension reload on change
Background reloadHMR runtime--no-hmr flag
CSS relative URLsInjected into the pagedata-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

PitfallWhy It HappensFix
The background script reloads unexpectedlyMV3 HMR runtimeUse --no-hmr
The content script CSS points to the wrong imageRelative URL resolves against the siteUse data-url:
The build fails with Cannot read properties of undefinedMissing content_scripts in MV3Add "content_scripts": []
A devtools panel is not bundledThe HTML is not referencedAdd 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

ItemValue
Preset package@parcel/config-webextension
Entry pointmanifest.json
Supported manifestsMV2 and MV3
Build commandparcel build manifest.json
.parcelrc{ "extends": "@parcel/config-webextension" }
MV3 HMRNot supported (CSP)
Content script CSSUse data-url: for local assets
web_accessible_resourcesAuto-populated for content scripts

Key takeaways:

  • The manifest is the entry point, not an HTML file. The @parcel/config-webextension preset parses the manifest and treats every referenceโ€”the background script, the content scripts, the popupโ€”as a dependency. The build command is parcel build manifest.json, and the .parcelrc file removes the need for the --config flag .
  • 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-hmr flag 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. The data-url: scheme inlines the asset and avoids the mismatch .
  • The web_accessible_resources field is auto-populated for content scripts. Any resource used in a content script is automatically added to web_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_page directly; it is opened by the devtools script. The panel must be listed in web_accessible_resources to 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.

Was this helpful?


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!