Pulled ALL skills from 15 source repositories: - anthropics/skills: 16 (docs, design, MCP, testing) - obra/superpowers: 14 (TDD, debugging, agents, planning) - coreyhaines31/marketingskills: 25 (marketing, CRO, SEO, growth) - better-auth/skills: 5 (auth patterns) - vercel-labs/agent-skills: 5 (React, design, Vercel) - antfu/skills: 16 (Vue, Vite, Vitest, pnpm, Turborepo) - Plus 13 individual skills from various repos Mosaic Stack is not limited to coding — the Orchestrator and subagents serve coding, business, design, marketing, writing, logistics, analysis, and more. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
12 KiB
12 KiB
name, description
| name | description |
|---|---|
| module-authoring | Complete guide to creating publishable Nuxt modules with best practices |
Module Authoring
This guide covers creating publishable Nuxt modules with proper structure, type safety, and best practices.
Module Structure
Recommended structure for a publishable module:
my-nuxt-module/
├── src/
│ ├── module.ts # Module entry
│ └── runtime/
│ ├── components/ # Vue components
│ ├── composables/ # Composables
│ ├── plugins/ # Nuxt plugins
│ └── server/ # Server handlers
├── playground/ # Development app
├── package.json
└── tsconfig.json
Module Definition
Basic Module with Type-safe Options
// src/module.ts
import { defineNuxtModule, createResolver, addPlugin, addComponent, addImports } from '@nuxt/kit'
export interface ModuleOptions {
prefix?: string
apiKey: string
enabled?: boolean
}
export default defineNuxtModule<ModuleOptions>({
meta: {
name: 'my-module',
configKey: 'myModule',
compatibility: {
nuxt: '>=3.0.0',
},
},
defaults: {
prefix: 'My',
enabled: true,
},
setup(options, nuxt) {
if (!options.enabled) return
const { resolve } = createResolver(import.meta.url)
// Module setup logic here
},
})
Using .with() for Strict Type Inference
When you need TypeScript to infer that default values are always present:
import { defineNuxtModule } from '@nuxt/kit'
interface ModuleOptions {
apiKey: string
baseURL: string
timeout?: number
}
export default defineNuxtModule<ModuleOptions>().with({
meta: {
name: '@nuxtjs/my-api',
configKey: 'myApi',
},
defaults: {
baseURL: 'https://api.example.com',
timeout: 5000,
},
setup(resolvedOptions, nuxt) {
// resolvedOptions.baseURL is guaranteed to be string (not undefined)
// resolvedOptions.timeout is guaranteed to be number (not undefined)
},
})
Adding Runtime Assets
Components
import { addComponent, addComponentsDir, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
setup() {
const { resolve } = createResolver(import.meta.url)
// Single component
addComponent({
name: 'MyButton',
filePath: resolve('./runtime/components/MyButton.vue'),
})
// Component directory with prefix
addComponentsDir({
path: resolve('./runtime/components'),
prefix: 'My',
pathPrefix: false,
})
},
})
Composables and Auto-imports
import { addImports, addImportsDir, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
setup() {
const { resolve } = createResolver(import.meta.url)
// Single import
addImports({
name: 'useMyUtil',
from: resolve('./runtime/composables/useMyUtil'),
})
// Directory of composables
addImportsDir(resolve('./runtime/composables'))
},
})
Plugins
import { addPlugin, addPluginTemplate, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
setup(options) {
const { resolve } = createResolver(import.meta.url)
// Static plugin file
addPlugin({
src: resolve('./runtime/plugins/myPlugin'),
mode: 'client', // 'client', 'server', or 'all'
})
// Dynamic plugin with generated code
addPluginTemplate({
filename: 'my-module-plugin.mjs',
getContents: () => `
import { defineNuxtPlugin } from '#app/nuxt'
export default defineNuxtPlugin({
name: 'my-module',
setup() {
const config = ${JSON.stringify(options)}
// Plugin logic
}
})`,
})
},
})
Server Extensions
Server Handlers
import { addServerHandler, addServerScanDir, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
setup() {
const { resolve } = createResolver(import.meta.url)
// Single handler
addServerHandler({
route: '/api/my-endpoint',
handler: resolve('./runtime/server/api/my-endpoint'),
})
// Scan entire server directory (api/, routes/, middleware/, utils/)
addServerScanDir(resolve('./runtime/server'))
},
})
Server Composables
import { addServerImports, addServerImportsDir, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
setup() {
const { resolve } = createResolver(import.meta.url)
// Single server import
addServerImports({
name: 'useServerUtil',
from: resolve('./runtime/server/utils/useServerUtil'),
})
// Server composables directory
addServerImportsDir(resolve('./runtime/server/composables'))
},
})
Nitro Plugin
import { addServerPlugin, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
setup() {
const { resolve } = createResolver(import.meta.url)
addServerPlugin(resolve('./runtime/server/plugin'))
},
})
// runtime/server/plugin.ts
import { defineNitroPlugin } from 'nitropack/runtime'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('request', (event) => {
console.log('Request:', event.path)
})
})
Templates and Virtual Files
Generate Virtual Files
import { addTemplate, addTypeTemplate, addServerTemplate, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
setup(options, nuxt) {
const { resolve } = createResolver(import.meta.url)
// Client/build virtual file (accessible via #build/my-config.mjs)
addTemplate({
filename: 'my-config.mjs',
getContents: () => `export default ${JSON.stringify(options)}`,
})
// Type declarations
addTypeTemplate({
filename: 'types/my-module.d.ts',
getContents: () => `
declare module '#my-module' {
export interface Config {
apiKey: string
}
}`,
})
// Nitro virtual file (accessible in server routes)
addServerTemplate({
filename: '#my-module/config.mjs',
getContents: () => `export const config = ${JSON.stringify(options)}`,
})
},
})
Access Virtual Files
// In runtime plugin
// @ts-expect-error - virtual file
import config from '#build/my-config.mjs'
// In server routes
import { config } from '#my-module/config.js'
Extending Pages and Routes
import { extendPages, extendRouteRules, addRouteMiddleware, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
setup() {
const { resolve } = createResolver(import.meta.url)
// Add pages
extendPages((pages) => {
pages.push({
name: 'my-page',
path: '/my-route',
file: resolve('./runtime/pages/MyPage.vue'),
})
})
// Add route rules (caching, redirects, etc.)
extendRouteRules('/api/**', {
cache: { maxAge: 60 },
})
// Add middleware
addRouteMiddleware({
name: 'my-middleware',
path: resolve('./runtime/middleware/myMiddleware'),
global: true,
})
},
})
Module Dependencies
Declare dependencies on other modules with version constraints:
export default defineNuxtModule({
meta: {
name: 'my-module',
},
moduleDependencies: {
'@nuxtjs/tailwindcss': {
version: '>=6.0.0',
// Set defaults (user can override)
defaults: {
exposeConfig: true,
},
// Force specific options
overrides: {
viewer: false,
},
},
'@nuxtjs/i18n': {
optional: true, // Won't fail if not installed
defaults: {
defaultLocale: 'en',
},
},
},
setup() {
// Dependencies are guaranteed to be set up before this runs
},
})
Dynamic Dependencies
moduleDependencies(nuxt) {
const deps: Record<string, any> = {
'@nuxtjs/tailwindcss': { version: '>=6.0.0' },
}
if (nuxt.options.ssr) {
deps['@nuxtjs/html-validator'] = { optional: true }
}
return deps
}
Lifecycle Hooks
Requires meta.name and meta.version:
export default defineNuxtModule({
meta: {
name: 'my-module',
version: '1.2.0',
},
onInstall(nuxt) {
// First-time setup
console.log('Module installed for the first time')
},
onUpgrade(nuxt, options, previousVersion) {
// Version upgrade migrations
console.log(`Upgrading from ${previousVersion}`)
},
setup(options, nuxt) {
// Regular setup runs every build
},
})
Extending Configuration
export default defineNuxtModule({
setup(options, nuxt) {
// Add CSS
nuxt.options.css.push('my-module/styles.css')
// Add runtime config
nuxt.options.runtimeConfig.public.myModule = {
apiUrl: options.apiUrl,
}
// Extend Vite config
nuxt.options.vite.optimizeDeps ||= {}
nuxt.options.vite.optimizeDeps.include ||= []
nuxt.options.vite.optimizeDeps.include.push('some-package')
// Add build transpile
nuxt.options.build.transpile.push('my-package')
},
})
Using Hooks
export default defineNuxtModule({
// Declarative hooks
hooks: {
'components:dirs': (dirs) => {
dirs.push({ path: '~/extra' })
},
},
setup(options, nuxt) {
// Programmatic hooks
nuxt.hook('pages:extend', (pages) => {
// Modify pages
})
nuxt.hook('imports:extend', (imports) => {
imports.push({ name: 'myHelper', from: 'my-package' })
})
nuxt.hook('nitro:config', (config) => {
// Modify Nitro config
})
nuxt.hook('vite:extendConfig', (config) => {
// Modify Vite config
})
},
})
Path Resolution
import { createResolver, resolvePath, findPath } from '@nuxt/kit'
export default defineNuxtModule({
async setup(options, nuxt) {
// Resolver relative to module
const { resolve } = createResolver(import.meta.url)
const pluginPath = resolve('./runtime/plugin')
// Resolve with extensions and aliases
const entrypoint = await resolvePath('@some/package')
// Find first existing file
const configPath = await findPath([
resolve('./config.ts'),
resolve('./config.js'),
])
},
})
Module Package.json
{
"name": "my-nuxt-module",
"version": "1.0.0",
"type": "module",
"exports": {
".": {
"import": "./dist/module.mjs",
"require": "./dist/module.cjs"
}
},
"main": "./dist/module.cjs",
"module": "./dist/module.mjs",
"types": "./dist/types.d.ts",
"files": ["dist"],
"scripts": {
"dev": "nuxi dev playground",
"build": "nuxt-module-build build",
"prepare": "nuxt-module-build build --stub"
},
"dependencies": {
"@nuxt/kit": "^3.0.0"
},
"devDependencies": {
"@nuxt/module-builder": "latest",
"nuxt": "^3.0.0"
}
}
Disabling Modules
Users can disable a module via config key:
// nuxt.config.ts
export default defineNuxtConfig({
// Disable entirely
myModule: false,
// Or with options
myModule: {
enabled: false,
},
})
Development Workflow
- Create module:
npx nuxi init -t module my-module - Develop:
npm run dev(runs playground) - Build:
npm run build - Test:
npm run test
Best Practices
- Use
createResolver(import.meta.url)for all path resolution - Prefix components to avoid naming conflicts
- Make options type-safe with
ModuleOptionsinterface - Use
moduleDependenciesinstead ofinstallModule - Provide sensible defaults for all options
- Add compatibility requirements in
meta.compatibility - Use virtual files for dynamic configuration
- Separate client/server plugins appropriately