963 markdown files reformatted with the repository's pinned prettier so pnpm format:check covers the folded tree like every other repo file. The formatter's embedded-language pass also normalized code fences (TS semicolons, closed HTML tags in examples, lowercased CSS hex colors, one renumbered list that skipped an index). Alphanumeric token deltas vs the fold commit were audited file-by-file; all are formatter-equivalent markup normalizations plus the four sanitized skills.
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