npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

wxt-module-pages

v0.5.0

Published

File-system based routing for WXT browser extensions

Downloads

10

Readme

wxt-module-pages

File-system based routing for WXT browser extensions with multi-framework support.

File-system based routing for WXT browser extensions with layouts, catch-all routes, route groups, and multi-framework support

Automatically discover all pages/ directories in your project and generate routes - just like Nuxt, but for browser extensions and with support for Vue, React, Preact, Svelte, Solid.js, Lit, and Angular.

Features

  • 🔍 Auto-discovery - Automatically finds all pages/ directories
  • 🎨 Multi-framework - Built-in drivers for 7+ frameworks
  • 📁 File-based routing - Convention over configuration
  • 🔄 Dynamic routes - Support for [id] parameters
  • 🌐 Catch-all routes - Match any depth with [...slug]
  • 📂 Route groups - Organize with (folder) without affecting URLs
  • 🎭 Layouts - Shared UI with automatic nesting
  • 🏗️ Layers support - Override routes using directory precedence
  • HMR - Hot module replacement in development
  • 🎯 Type-safe - Full TypeScript support
  • 🪶 Zero config - Works out of the box

Installation

npm install wxt-module-pages
# or
pnpm add wxt-module-pages
# or
yarn add wxt-module-pages

Quick Start

1. Add the module to your wxt.config.ts

import { defineConfig } from 'wxt'
import pages from 'wxt-module-pages'

export default defineConfig({
  modules: [
    pages()
  ]
})

2. Create your pages directory

entrypoints/popup/pages/
  index.vue          → /
  about.vue          → /about
  users/
    index.vue        → /users
    [id].vue         → /users/:id

3. Import and use the routes

// entrypoints/popup/main.ts
import { createApp } from 'vue'
import { createRouter, createWebHashHistory } from 'vue-router'
import routes from 'virtual:routes'
import App from './App.vue'

const router = createRouter({
  history: createWebHashHistory(),
  routes
})

createApp(App).use(router).mount('#app')

Framework Usage

Vue / Nuxt

// wxt.config.ts
import pages from 'wxt-module-pages'

export default defineConfig({
  modules: [pages()] // vue is default
})
// entrypoints/popup/main.ts
import { createApp } from 'vue'
import { createRouter, createWebHashHistory } from 'vue-router'
import routes from 'virtual:routes'
import App from './App.vue'

const router = createRouter({
  history: createWebHashHistory(),
  routes
})

createApp(App).use(router).mount('#app')

React

// wxt.config.ts
import pages, { reactDriver } from 'wxt-module-pages'

export default defineConfig({
  modules: [pages({ driver: reactDriver() })]
})
// entrypoints/popup/main.tsx
import { createRoot } from 'react-dom/client'
import { HashRouter, Routes, Route } from 'react-router-dom'
import routes from 'virtual:routes'

createRoot(document.getElementById('root')!).render(
  <HashRouter>
    <Routes>
      {routes.map(route => (
        <Route key={route.path} {...route} />
      ))}
    </Routes>
  </HashRouter>
)

Preact

// wxt.config.ts
import pages, { preactDriver } from 'wxt-module-pages'

export default defineConfig({
  modules: [pages({ driver: preactDriver() })]
})
// entrypoints/popup/main.tsx
import { render } from 'preact'
import { Router, Route } from 'preact-router'
import routes from 'virtual:routes'

render(
  <Router>
    {routes.map(route => (
      <Route 
        key={route.path} 
        path={route.path} 
        component={route.component} 
      />
    ))}
  </Router>,
  document.getElementById('root')!
)

Svelte

// wxt.config.ts
import pages, { svelteDriver } from 'wxt-module-pages'

export default defineConfig({
  modules: [pages({ driver: svelteDriver() })]
})
// entrypoints/popup/main.ts
import { mount } from 'svelte'
import Router from 'svelte-spa-router'
import routes from 'virtual:routes'
import App from './App.svelte'

// convert to svelte-spa-router format
const routeMap = {}
routes.forEach(route => {
  routeMap[route.path] = route.component
})

mount(App, {
  target: document.getElementById('app')!,
  props: { routes: routeMap }
})

Solid.js

// wxt.config.ts
import pages, { solidDriver } from 'wxt-module-pages'

export default defineConfig({
  modules: [pages({ driver: solidDriver() })]
})
// entrypoints/popup/main.tsx
import { render } from 'solid-js/web'
import { Router, Route } from '@solidjs/router'
import routes from 'virtual:routes'

render(
  () => (
    <Router>
      {routes.map(route => (
        <Route path={route.path} component={route.component} />
      ))}
    </Router>
  ),
  document.getElementById('root')!
)

Lit (Web Components)

// wxt.config.ts
import pages, { litDriver } from 'wxt-module-pages'

export default defineConfig({
  modules: [pages({ driver: litDriver() })]
})
// entrypoints/popup/main.ts
import { Router } from '@vaadin/router'
import routes from 'virtual:routes'

const outlet = document.getElementById('outlet')
const router = new Router(outlet)

// load all components
Promise.all(routes.map(route => route.load())).then(() => {
  router.setRoutes(routes)
})

Angular

// wxt.config.ts
import pages, { angularDriver } from 'wxt-module-pages'

export default defineConfig({
  modules: [pages({ driver: angularDriver() })]
})
// entrypoints/popup/main.ts
import { bootstrapApplication } from '@angular/platform-browser'
import { provideRouter } from '@angular/router'
import { AppComponent } from './app.component'
import routes from 'virtual:routes'

bootstrapApplication(AppComponent, {
  providers: [provideRouter(routes)]
})

Advanced Usage

Multiple Pages Directories

The module automatically discovers all pages/ directories in your project:

entrypoints/popup/pages/    → found
entrypoints/options/pages/  → found
layers/base/pages/          → found
layers/custom/pages/        → found

Importing Specific Routes

You can import routes from specific directories:

// merged routes from all pages directories
import routes from 'virtual:routes'

// specific directory
import popupRoutes from 'virtual:entrypoints/popup/pages'
import optionsRoutes from 'virtual:entrypoints/options/pages'

// layer-specific
import baseRoutes from 'virtual:layers/base/pages'
import customRoutes from 'virtual:layers/custom/pages'

Layer Override System

When using virtual:routes (merged routes), later directories override earlier ones:

layers/base/pages/index.vue       → base route
layers/custom/pages/index.vue     → overrides base

This is perfect for extension variants or themed versions.

File Naming Conventions

index.vue                  → /
about.vue                  → /about
users/index.vue            → /users
users/[id].vue             → /users/:id
posts/[slug]/edit.vue      → /posts/:slug/edit
docs/[...slug].vue         → /docs/:slug(.*)* (catch-all)
(admin)/settings.vue       → /settings (route group)
dashboard/layout.vue       → Wraps all dashboard/* pages

Route Patterns

Dynamic Routes:

pages/users/[id].vue       → /users/:id

Catch-all Routes:

pages/docs/[...slug].vue   → /docs/:slug(.*)*

Matches /docs/anything/at/all/depths

Route Groups (Organizational):

pages/(marketing)/blog.vue    → /blog (not /marketing/blog)
pages/(admin)/settings.vue    → /settings (not /admin/settings)

Parentheses folders are ignored in URLs - use them to organize your files!

Layouts:

pages/dashboard/
  layout.vue               → Parent wrapper component
  overview.vue             → /dashboard/overview
  settings.vue             → /dashboard/settings

The layout automatically wraps all sibling pages. Layouts can nest!

Route Names

Routes are automatically named based on their file path:

index.vue                  → 'index'
about.vue                  → 'about'
users/index.vue            → 'users'
users/[id].vue             → 'users-id'
posts/[slug]/edit.vue      → 'posts-slug-edit'
docs/[...slug].vue         → 'docs-slug-all'
(admin)/settings.vue       → 'settings'

Working with Layouts

Layouts let you wrap multiple pages with shared UI (navigation, sidebar, etc.).

Create a layout:

<!-- pages/dashboard/layout.vue -->
<template>
  <div class="dashboard">
    <Sidebar />
    <main>
      <RouterView /> <!-- Child pages render here -->
    </main>
  </div>
</template>

Add child pages:

pages/dashboard/
  layout.vue           → Wraps all children
  overview.vue         → /dashboard/overview
  analytics.vue        → /dashboard/analytics
  settings.vue         → /dashboard/settings

All pages in the dashboard/ folder are automatically wrapped by layout.vue!

Nested layouts:

pages/dashboard/
  layout.vue           → Parent layout
  overview.vue
  users/
    layout.vue         → Nested layout
    index.vue          → /dashboard/users
    [id].vue           → /dashboard/users/:id

Layouts can nest infinitely - each subfolder's layout wraps its children.

Generated routes structure:

{
  path: '/dashboard',
  component: () => import('./dashboard/layout.vue'),
  children: [
    {
      path: 'overview',
      component: () => import('./dashboard/overview.vue')
    },
    {
      path: 'users',
      component: () => import('./dashboard/users/layout.vue'),
      children: [
        {
          path: '',
          component: () => import('./dashboard/users/index.vue')
        },
        {
          path: ':id',
          component: () => import('./dashboard/users/[id].vue')
        }
      ]
    }
  ]
}

Custom Drivers

Create your own driver for custom frameworks or routing setups:

import type { PagesDriver } from 'wxt-module-pages'

function myCustomDriver(): PagesDriver {
  return {
    extensions: ['.custom'],
    
    routeToCode(route) {
      return `  {
    path: '${route.path}',
    name: '${route.name}',
    component: () => import('${route.file}')
  }`
    },
    
    wrapRoutes(routeStrings) {
      return `export default [
${routeStrings.join(',\n')}
]`
    }
  }
}

export default defineConfig({
  modules: [
    pages({ driver: myCustomDriver() })
  ]
})

How It Works

  1. Build-time scanning - The module scans your project for pages/ directories
  2. File detection - Only includes directories containing framework-specific files (.vue, .jsx, etc.)
  3. Virtual modules - Exposes routes via Vite virtual modules
  4. Route generation - Generates framework-specific route configurations
  5. HMR support - Watches for changes and triggers hot reloads

TypeScript Support

Copy the appropriate type declaration file to your project:

For Vue (default):

cp node_modules/wxt-module-pages/types/virtual-modules.d.ts ./types/

For React:

cp node_modules/wxt-module-pages/types/virtual-modules-react.d.ts ./types/virtual-modules.d.ts

For Solid.js:

cp node_modules/wxt-module-pages/types/virtual-modules-solid.d.ts ./types/virtual-modules.d.ts

For Angular:

cp node_modules/wxt-module-pages/types/virtual-modules-angular.d.ts ./types/virtual-modules.d.ts

This provides full type safety for:

import routes from 'virtual:routes'
import specificRoutes from 'virtual:entrypoints/popup/pages'

See types/README.md for more details.

Configuration

PagesOptions

interface PagesOptions {
  /** Driver to use (defaults to vue) */
  driver?: PagesDriver
}

PagesDriver Interface

interface PagesDriver {
  /** File extensions to scan for */
  extensions: string[]
  
  /** Convert a route definition to code string */
  routeToCode(route: RouteDefinition): string
  
  /** Wrap all routes in framework-specific export */
  wrapRoutes(routeStrings: string[]): string
}

interface RouteDefinition {
  path: string
  name: string
  file: string
}

Excluded Directories

The following directories are automatically excluded from scanning:

  • node_modules
  • .wxt
  • dist
  • .nuxt
  • .output
  • .next
  • build

Examples

See the /examples directory for complete working examples:

Why?

Browser extensions often need multiple routed interfaces (popup, options page, side panel). Managing these routes manually is tedious. This module brings the beloved file-system routing from Nuxt to WXT, making extension development faster and more enjoyable.

Contributing

Contributions welcome! Please open an issue or PR.

License

MIT © Dave Stewart