Skip to content

Nuxt-like layers functionality for WXT browser extensions

License

Notifications You must be signed in to change notification settings

davestewart/wxt-module-layers

Repository files navigation

WXT Layers

Nuxt-like layers functionality for WXT browser extensions

Abstract

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.

Contents

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

Setup

Installation

Install via your preferred package manager:

npm install wxt-module-layers

Configuration

Configure 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
  }
})

Quick start

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 page

Right 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.

Usage

The main areas of interest in a Layers project are:

Folder Structure

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 exports

All defaults are configurable (see options).

Entrypoints

There are several ways to configure and utilise entry points in layers:

Entrypoints Folders

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
    ...                 # etc

Manual Entrypoint locations

The module also supports custom entry points:

layers/some-feature/
  background/
    services/
    index.ts            # non-standard location
  panel/
    services/
    index.html          # non-standard location

To 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

Per-layer background scripts

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 src background endpoint
  • manually imported and run in your existing src background 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.ts

In 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

Imports and Exports

There are three main ways of orchestrating dependencies in layers:

Layer Aliases

By default, all sources and layers are aliased using the layerAlias default template #{name}:

layers/           --> #layers 
  foo-feature/    --> #foo-feature
  bar-feature/    --> #bar-feature

This allows for clearer imports between layers:

// src/entrypoints/background.ts
import { foo } from '#foo-feature' // imports from layers/foo-feature/index.ts

To reconfigure, see module, source and layer options.

Auto-imports

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!

Index Files

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 and Assets

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.png

By 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'

Patterns

This section contains best-practice information regarding:

Sources

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
      },
    ]
  }
})

Core Extension Logic

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.png

Feature Layers

Isolate 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 export

Import between layers using aliases (if auto-imports are off):

// src/entrypoints/content.ts
import { useAuth } from '#auth'

Package Layers

Create reusable layers for common functionality across projects:

packages/
  error-reporting/      # Reusable Sentry integration
  feature-flags/        # Reusable feature flag system
  telemetry/            # Reusable analytics

Note 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.

Import Strategies

Different ways to import code in your extension.

Source Imports (~/)

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'

Auto-imports (Global)

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 works

Layer Global (#some-layer)

Cross-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'

Layer Local (../)

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'!

Extensibility

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

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

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

Source Options

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.

Layer Options

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 public path 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.

Entrypoints Options

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
  }
})

Debugging

Enable Logging

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

Common Issues

Aliases not resolving:

  • Restart TypeScript server
  • Verify layerAlias isn't falseat module, source or layer level

Auto-imports not working:

  • Check autoImports are 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 entrypoints config

Entrypoint naming

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

About

Nuxt-like layers functionality for WXT browser extensions

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published