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.ts
Layers 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.
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 new 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 defineBackground(async () => {
// 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 initializer and run it:
// src/entrypoints/background.ts
import layers from 'wxt-module-layers:background'
export default async defineBackground(() => {
console.log('Main background ready')
layers()
})If you don't have 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 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:
{
publicPath: '', // no prefix; ensure layer public files are unique!
publicPath: '{name}', // default: prefix with layer name
}Note that layer-specific assets should be imported into source code as usual:
// layers/some-feature/entrypoints/popup.ts
import '../assets/styles.scss'This 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 template or literal string |
autoImports |
✅ | ✅ | ✅ | Auto-import folder paths |
entrypoints |
✅ | ✅ | ✅ | Manual entry point configuration |
publicPrefix |
✅ | ✅ | ✅ | Layer public path template or literal string prefix |
order |
✅ | Background script load order | ||
manifest |
✅ | Manifest access |
Module options are configured in wxt.config.ts under the layers key:
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: ...,
publicPath: ...,
}
],
// 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/'
// 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
entrypoints: {
background: 'bg.ts' // add custom background endpoint location
}
}
],
},
})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
publicpath prefix
// 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',
})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