Nuxt-like layers functionality for WXT browser extensions
WXT Layers brings Nuxt's layers architecture to browser extensions, enabling you to organise your extension into isolated, reusable slices:
src/ # core logic
composables/
entrypoints/
public/
utils/
layers/
some-feature/ # related files
entrypoints/
public/
some-service/ # related files
composables/
index.tsLayers are self-contained folders that mirror WXT's project structure. Each layer can have its own entry points, auto-imports, public files, and manifest modifications, with some additional layer-specific ✨ sprinkled on top.
Main use cases:
- Large projects: a flexible level of abstraction once your project gets large
- Cleaner organisation: use core WXT structure for shared code, layers for specific features
- Feature isolation: keep related code together (e.g.,
layers/analytics/,layers/auth/)
Jump to the usage section for full details.
Check the demo folder for a working Hacker News enhancement extension — HN Enhanced — that shows WXT Layers in action with two isolated feature layers:
- Theme — replaces the favicon, highlights high-scoring stories, opens links in new tabs
- Tracker — marks read stories with a checkmark and orange highlight, logs your reading history to a sidepanel
It's a practical reference for layer discovery, per-layer background scripts, auto-imports, barrel file exports, public file copying, CSS assets, and manifest merging — all in one runnable project.
Quickly jump to:
- Setup
Get started quickly with install, config and files - Usage
Explore how the module works - Patterns
Best practices for setting up larger projects - Options
Detailed configuration information - Debugging
What to do when things don't work
Install via your preferred package manager:
npm install wxt-module-layersConfigure wxt.config.ts:
import { defineConfig } from 'wxt'
export default defineConfig({
// enable the module
modules: ['wxt-module-layers'],
// options
layers: {
logLevel: 'debug', // set this to see what gets discovered and registered
}
})WXT Layers has sensible defaults to get started quickly. It looks for sub-folders under <root>/layers/ and expects each folder to mirror the structure of a regular WXT extension.
Let's add some files to get started:
layers/ # default source (note: you can configure multiple sources)
example/ # example layer
entrypoints/ # standard entrypoints folder
options.html # global options pageRight click your extension's icon, click "Options", and view the options page – loaded from a layer 😎
Tip
Your extension will rebuild as you add or update new files or configuration options during development. If you're feeling adventurous, try moving your popup entrypoint and related code and see what happens!
You can stop here if you're happy with the defaults, otherwise, read on to deep dive usage, patterns and options.
The main areas of interest in a Layers project are:
- Folder structure
A mirror of WXT's standard structure - Entrypoints
Automatic, manual, and background scripts - Imports and exports
Aliases, auto-imports, and index files - Public files and assets
Layer-specific public files and assets - Options (separate section)
Module, source and layer-level configuration
Layers mirror the folder structure of a regular WXT extension:
src/
composables/ # Global composables
entrypoints/ # Global entrypoints
...
layers/ # Layers folder
analytics/ # Layer name Aliased to #<name>
entrypoints/ # Auto-discovered By folder or manual config
composables/ # Optional auto-import Off by default
public/ # Auto-copied To .output/<name>/*
...
layer.config.ts # Optional layer config
index.ts # Optional layer exportsAll defaults are configurable (see options).
There are several ways to configure and utilise entry points in layers:
- Entrypoints folders
Automatic discovery - Custom entrypoint locations
Manual configuration - Per-layer background scripts
Automatic or manual background script integration
File-based entrypoints are discovered in exactly the same way as a regular WXT project:
layers/my-layer/
entrypoints/
background.ts # background script
content.ts # content script
popup/ # popup
index.html
popup.ts
sidepanel.html # sidepanel
... # etcThe module also supports custom entry points:
layers/some-feature/
background/
services/
index.ts # non-standard location
panel/
services/
index.html # non-standard locationTo do this, configure using an entrypoints object in your layer config:
// layers/some-feature/layer.config.ts
export default defineLayer({
entrypoints: {
background: 'background/index.ts', // output to background.js
sidepanel: 'panel/index.html', // output to sidepanel.html
}
})Tip
To configure multiple layers at once, use source options
Every layer can have its own background script; they are compiled at build time and either:
- automatically added to a virtual background endpoint if no existing
srcbackground endpoint - manually imported and run in your existing
srcbackground endpoint
Let's say you have two layers which need to add their own listeners, alarms, etc:
layers/
some-feature/entrypoints/background.ts
another-feature/entrypoints/background.tsIn each layer, define a background entrypoint as you normally would:
// layers/some-feature/entrypoints/background.ts
export default function () {
// set up listeners, alarms, etc.
}Tip
Create an async function if you want layers to complete their work in sequence before the next one runs
If you have an existing src background entrypoint, manually import the layer backgrounds initializer and run it:
// src/entrypoints/background.ts
import { main } from 'wxt-module-layers:background'
export default defineBackground(() => {
console.log('Main background ready')
main()
})If you don't have, or don't need, an existing src background entrypoint, the module will automatically create and add a new background entrypoint that runs all layer backgrounds in order. No additional coding required!
If you need background scripts to run in a specific order, set the order property in layer config:
// layers/some-feature/layer.config.ts
export default defineLayer({
order: 0, // Lower values run earlier (default is 50)
})Note
During development, each background layer's console.log()s will:
- output within a layer-named
console.group()entry - add execution timings
There are three main ways of orchestrating dependencies in layers:
- Layer aliases
Global layer aliases that work across sources - Auto-imports
Per-sources or per-layer auto-imports - Index files
Barrel files to expose layer dependencies
By default, all sources and layers are aliased using the layerAlias default template #{name}:
layers/ --> #layers
foo-feature/ --> #foo-feature
bar-feature/ --> #bar-featureThis allows for clearer imports between layers:
// src/entrypoints/background.ts
import { foo } from '#foo-feature' // imports from layers/foo-feature/index.tsTo reconfigure, see module, source and layer options.
By default, auto-import is off for layers; the rationale being:
- a project's global concerns will generally be contained in
src/*folders - in larger projects, it's more manageable control layers' access explicitly
However, feel free to re-configure auto-imports per module, source or layer.
Important
Restart your TypeScript server if your IDE doesn't pick up imports:
- VS Code:
Cmd/Ctrl + Shift + P→ "TypeScript: Restart TS Server" - WebStorm:
Shift + Shift→ "Restart TypeScript Service"
Your IDE should update; if it doesn't, you might need to give it a minute!
You can use index or "barrel" files to expose a layer's dependencies:
// layers/some-feature/index.ts
export * from './components'
export * from './utils'
...Other layers then import explicitly:
import { foo, bar } from '#some-feature'Public files (i.e. those accessed via URL at runtime) from each layer will be copied to the main project's public folder:
layers/some-feature/
public/
icons/my-icon.png # some-feature/icons/my-icon.pngBy default, public paths are prefixed with the layer name; reconfigure this at the module, source or layer level:
{
publicPrefix: '', // no prefix; ensure layer public files are unique!
publicPrefix: '{name}/', // default: prefix with layer name
}By default, public files are not web-accessible; reconfigure this at the module, source or layer level:
{
publicAccess: ['*://wxt.dev'], // limit access to one or more domains
publicAccess: false, // prevent access to files
}Asset files (i.e. those bundled by rollup at build time) should be imported into source code:
// layers/some-feature/entrypoints/popup.ts
import '../assets/styles.scss' // styles will be compiled in the final bundle
import icon from '../assets/icon.svg?raw' // assets should be referenced directly in codeThis section contains best-practice information regarding:
Set up multiple sources for different purposes:
export default defineConfig({
srcDir: 'src', // core extension logic
layers: {
sources: [
'layers/*', // feature-specific layers
'packages/*', // reusable packages
]
}
})Use source options to configure layers en-masse:
export default defineConfig({
...
layers: {
sources: [
{
source: 'features/*',
entrypoints: { background: 'background.ts' } // load custom background scripts
},
{
source: 'packages/*',
autoImports: ['composables'], // auto-import all composables
},
]
}
})Use WXT's base structure for shared functionality:
src/
entrypoints/ # Main extension entrypoints
background.ts
composables/ # Shared composables
useStorage.ts
useSettings.ts
utils/ # Shared utilities
logger.ts
api.ts
public/ # Public assets
icon.pngIsolate specific features in their own layers:
layers/
analytics/
entrypoints/
content.ts # Track page views
services/
tracker.ts
public/
icons/
auth/
entrypoints/
background.ts # Handle auth state
popup.html # Login UI
composables/
useAuth.ts
index.ts # optional barrel file exportImport between layers using aliases (if auto-imports are off):
// src/entrypoints/content.ts
import { useAuth } from '#auth'Create reusable layers for common functionality across projects:
packages/
error-reporting/ # Reusable Sentry integration
feature-flags/ # Reusable feature flag system
telemetry/ # Reusable analyticsNote that layers can be installed from any relative or absolute path:
{
sources: [
// multiple layers
'src/layers/*',
'packages/*',
// single layers
'~/Projects/.../some-package',
'/Volumes/Projects/.../some-package',
'node_modules/some-package/',
]
}Note
WXT Layers does not currently support installing directly from Github, but you can install and share layers from NPM by referencing the node_modules/<package_name> folder directly.
Different ways to import code in your extension.
Core utilities, types, and shared code in your base WXT structure
Import from your main src/ directory:
import { logger } from '~/utils/logger'
import type { User } from '~/types'Use-it-from-anywhere, low-friction, less typing...
If you set autoImports in module, source, or layer options...
// layers/some-feature/layer.config.ts
export default defineLayer({
autoImports: ['composables'],
})...exports in that folder...
// layers/some-feature/composables/someThing.ts
export function someThing () { /* ... */ }...will be globally available:
// Anywhere:
const track = someThing() // Just worksCross-layer dependencies, services, and types
Optionally, create a barrel file in layer root folder:
// layers/analytics/index.ts
export * from './services/tracker'
export * from './composables/useTracking'Then, import from other layers:
import { AuthService } from '#auth'
import { trackEvent } from '#analytics'Internal layer code that shouldn't be exposed outside
Import from the same layer:
// Within layers/analytics/
import { TrackerService } from '../services/tracker'
import type { Event } from '../types'Other plugins or your own code can be informed when layers are resolved, by hooking into the 'layers:resolved' event:
wxt.hook('layers:resolved' as any, async (layerDirs: string[]) => {
// do something with layer dirs
})The WXT Pages module does just this to add file based routes from individual layers.
Options are hierarchical, configurable at module > source > layer levels:
| Name | Module | Source | Layer | Description |
|---|---|---|---|---|
logLevel |
✅ | Logger output level | ||
sources |
✅ | Paths/globs to layer sources | ||
source |
✅ | Path/glob to layer sources | ||
layerAlias |
✅ | ✅ | ✅ | Layer alias configuration |
autoImports |
✅ | ✅ | ✅ | Auto-import folder paths |
entrypoints |
✅ | ✅ | ✅ | Manual entry point configuration |
publicPrefix |
✅ | ✅ | ✅ | Copied public files location |
publicAccess |
✅ | ✅ | ✅ | Copied public files accessibility |
order |
✅ | Background script execution order | ||
manifest |
✅ | Raw manifest modification |
Module options are configured in wxt.config.ts under the layers key:
// Kitchen sink example of all configuration options (not representative of a typical project)
export default defineConfig({
modules: ['wxt-module-layers'],
layers: {
// Where to find layers (default: 'layers/*')
sources: [
'layers/*', // All folders under /layers/
'src/packages/*', // All folders under /src/packages/
'vendor/analytics', // Single specific layer
{
source: 'features/*', // Options per source (see Source Options below)
sourceAlias: ...,
layerAlias: ...,
entrypoints: ...,
autoImports: ...,
publicPrefix: ...,
publicAccess: ...,
}
],
// Default source alias template (default: '#{name}')
sourceAlias: '@{name}', // i.e. @features, @packages, etc.
// Default layer alias template (default: '#{name}')
layerAlias: '@{name}', // i.e. @analytics, @auth, etc.
// Default auto-import folders (default: [], no auto-imports)
autoImports: [
'composables',
'utils',
...
],
// Default manual entrypoint files (default: null, scans entrypoint folders)
entrypoints: {
background: 'bg.ts', // add custom background endpoint location
...
},
// Default public file prefix (default: '{name}', copy into subfolder)
publicPrefix: '/', // Copied to '/' rather than '/auth/'
// Default public file accessibility (default: false, nothing added to `web_accessible_resources`)
publicAccess: '*://wxt.dev', // make all files in `public` folders accessible on wxt.dev
// Logging level (default: 'info')
logLevel: 'debug', // Exposes useful debugging information
}
})Note
In all options, configuration such as #{name} will be replaced with the relevant source or layer name
You can mass-assign layer options to sources layers by supplying objects rather than paths:
export default defineConfig({
...
layers: {
sources: [
// configure path only
'packages/*',
// configure options for all layers in source
{
source: 'packages/*', // all folders under features
sourceAlias: '@packages', // add an alias to the source folder
layerAlias: '', // don't add layer aliases
autoImports: ['utils'], // auto-import everything from utils folder
entrypoints: {
background: 'bg.ts' // add custom background endpoint location
},
publicPrefix: '/', // place all public files in root
publicAccess: '*://wxt.dev', // make public files only accessible from wxt.dev
},
],
},
})Note that source options fall back to module options if not set.
Most layers work just-fine with defaults. Configure when you need to:
- control background script execution order
- use non-standard entry point locations
- modify the extension manifest
- modify aliases (not-recommended at layer level for consistency)
- modify auto-imports
- modify
publicfiles location - modify
publicfiles access
// layers/analytics/layer.config.ts
import { defineLayer } from 'wxt-module-layers'
export default defineLayer({
// Control background script order (default: 50, lower = earlier)
order: 0,
// Manually specify entry points (bypasses auto-discovery)
entrypoints: {
'background': 'background/index.ts', // --> background.ts
'linkedin.content': 'content/linkedin.ts', // --> content-scripts/linkedin.ts
'twitter.content': 'content/twitter.ts', // --> content-scripts/twitter.ts
},
// Modify extension manifest
manifest: (wxt, manifest) => {
manifest.permissions?.push('storage', 'cookies')
manifest.host_permissions?.push('*://*.example.com/*')
},
// Override module defaults (rarely needed)
layerAlias: '@tracking',
// Auto-import specific folders
autoImports: ['composables', 'services'],
// Customise public files location
publicPrefix: 'tracking',
// Customise public files access
publicAccess: false,
})Note that layer options fall back to source and module options if not set.
Manual entry point options should be configured using a key => path format.
Note
The key string mirrors WXT's filename naming conventions:
// layers/some-feature/layer.config.ts
export default defineLayer({
entrypoints: {
// virtual-background
'background': '<path>.ts',
// single entrypoint, per context
// popup, options, bookmarks, history, new tab, devtools
'<context>': '<path>.html',
// named entrypoints, per context (names must be unique!)
// sidepanel, sandbox, content script, unlisted page, script or css
'<name>.<context>': '<path>.<ext>',
}
})Important
Custom entry point names MUST be unique! See the debugging section for troubleshooting output errors.
The following example demonstrates naming for separate content scripts:
export default defineLayer({
entrypoints: {
'linkedin.content': 'content/linkedin.ts', // content-scripts/linkedin.js
'twitter.content': 'content/twitter.ts', // content-scripts/twitter.js
'github.content': 'content/github.ts', // content-scripts/github.js
}
})export default defineConfig({
layers: {
logLevel: 'debug',
}
})Terminal output shows what's discovered:
[layers] [source]: layers/*
[layers] - alias: #layers
[layers] [layer]: layers/complex
[layers] - alias: #complex
[layers] - entrypoint: entrypoints/background.ts (layer-background)
[layers] - auto-imports: composables, services
...
Layer background scripts will:
- wrap their layers'
console.log()s in named groups - output execution times
Aliases not resolving:
Auto-imports not working:
- Check
autoImportsare added, i.e.['composables', 'utils', ...] - Verify WXT's own imports aren't disabled
Entrypoints not found:
- Use module debug logging to see what's scanned (
logLevel: 'debug') - Check file naming matches WXT conventions
- Check no duplicate entrypoint names
- Consider manual
entrypointsconfig
Use this table to understand how WXT converts entry point identifiers to filenames:
| Usage | Key | Source | Target |
|---|---|---|---|
| Virtual | |||
background |
*.[jt]s |
background.js |
|
| Single | |||
bookmarks |
*.html |
bookmarks.html |
|
devtools |
*.html |
devtools.html |
|
history |
*.html |
history.html |
|
newtab |
*.html |
newtab.html |
|
options |
*.html |
options.html |
|
popup |
*.html |
popup.html |
|
sandbox |
*.html |
sandbox.html |
|
sidepanel |
*.html |
sidepanel.html |
|
content |
*.[jt]sx? |
content-scripts/content.js |
|
content |
*.css,scss,... |
content-scripts/content.css |
|
| Multiple | |||
{name}.sidepanel |
*.html |
{name}.html |
|
{name}.sandbox |
*.html |
{name}.html |
|
| Content | |||
{name}.content |
*.[jt]sx? |
content-scripts/{name}.js |
|
{name}.content |
*.css,scss,... |
content-scripts/{name}.css |
|
| Unlisted | |||
{name} |
*.html |
{name}.html |
|
{name} |
*.[jt]sx? |
{name}.js |
|
{name} |
*.css,scss,... |
{name}.css |
WXT will error if duplicate entrypoint names are found:
ERROR Multiple entrypoints with the same name detected, only one entrypoint for each name is allowed.
- foo
- entrypoints/foo.content.ts
- entrypoints/foo.html