From 3681bddfd5051196f57c8b9b8a26fb87d8c5fb66 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 3 Mar 2023 17:52:55 +0000 Subject: [PATCH] feat(nuxt): auto-register modules in `~/modules` (#19394) --- .../2.directory-structure/1.modules.md | 48 +++++++++++++++++++ packages/nuxt/src/core/nuxt.ts | 40 +++++++++++++--- packages/schema/src/config/common.ts | 5 ++ test/basic.test.ts | 7 +++ .../basic/modules/auto-registered/index.ts | 15 ++++++ .../auto-registered/runtime/handler.ts | 1 + test/fixtures/basic/modules/example.ts | 9 ++-- 7 files changed, 115 insertions(+), 10 deletions(-) create mode 100644 docs/2.guide/2.directory-structure/1.modules.md create mode 100644 test/fixtures/basic/modules/auto-registered/index.ts create mode 100644 test/fixtures/basic/modules/auto-registered/runtime/handler.ts diff --git a/docs/2.guide/2.directory-structure/1.modules.md b/docs/2.guide/2.directory-structure/1.modules.md new file mode 100644 index 0000000000..a4f6734fc1 --- /dev/null +++ b/docs/2.guide/2.directory-structure/1.modules.md @@ -0,0 +1,48 @@ +--- +navigation.icon: IconDirectory +title: 'modules' +head.title: 'modules/' +description: Use the modules/ directory to automatically register local modules within your application. +--- + +# Modules Directory + +Nuxt scans the `modules/` directory and loads them before starting. It is a good place to place any local modules you develop while building your application. + +The auto-registered files patterns are: +- `modules/*/index.ts` +- `modules/*.ts` + +You don't need to add those local modules to your [`nuxt.config.ts`](/docs/guide/directory-structure/nuxt.config) separately. + +::code-group +```ts [modules/hello/index.ts] +// `nuxt/kit` is a helper subpath import you can use when defining local modules +// that means you do not need to add `@nuxt/kit` to your project's dependencies +import { createResolver, defineNuxtModule, addServerHandler } from 'nuxt/kit' + +export default defineNuxtModule({ + meta: { + name: 'hello' + }, + setup () { + const { resolve } = createResolver(import.meta.url) + + // Add an API route + addServerHandler({ + route: '/api/hello', + handler: resolve('./runtime/api-route') + }) + } +}) +``` +```ts [modules/hello/runtime/api-route.ts] +export default defineEventHandler(() => { + return { hello: 'world' } +} +``` +:: + +When starting Nuxt, the `hello` module will be registered and the `/api/hello` route will be available. + +:ReadMore{link="/docs/guide/going-further/modules"} diff --git a/packages/nuxt/src/core/nuxt.ts b/packages/nuxt/src/core/nuxt.ts index 3fd4365507..1b86ea03bb 100644 --- a/packages/nuxt/src/core/nuxt.ts +++ b/packages/nuxt/src/core/nuxt.ts @@ -1,7 +1,7 @@ -import { join, normalize, resolve } from 'pathe' +import { join, normalize, relative, resolve } from 'pathe' import { createHooks, createDebugger } from 'hookable' import type { LoadNuxtOptions } from '@nuxt/kit' -import { loadNuxtConfig, nuxtCtx, installModule, addComponent, addVitePlugin, addWebpackPlugin, tryResolveModule, addPlugin } from '@nuxt/kit' +import { resolvePath, resolveAlias, resolveFiles, loadNuxtConfig, nuxtCtx, installModule, addComponent, addVitePlugin, addWebpackPlugin, tryResolveModule, addPlugin } from '@nuxt/kit' import escapeRE from 'escape-string-regexp' import fse from 'fs-extra' @@ -122,10 +122,38 @@ async function initNuxt (nuxt: Nuxt) { // Init user modules await nuxt.callHook('modules:before') - const modulesToInstall = [ - ...nuxt.options.modules, - ...nuxt.options._modules - ] + const modulesToInstall = [] + + const watchedPaths = new Set() + const specifiedModules = new Set() + + for (const _mod of nuxt.options.modules) { + const mod = Array.isArray(_mod) ? _mod[0] : _mod + if (typeof mod !== 'string') { continue } + const modPath = await resolvePath(resolveAlias(mod)) + specifiedModules.add(modPath) + } + + // Automatically register user modules + for (const config of nuxt.options._layers.map(layer => layer.config).reverse()) { + const layerModules = await resolveFiles(config.srcDir, [ + `${config.dir?.modules || 'modules'}/*{${nuxt.options.extensions.join(',')}}`, + `${config.dir?.modules || 'modules'}/*/index{${nuxt.options.extensions.join(',')}}` + ]) + for (const mod of layerModules) { + watchedPaths.add(relative(config.srcDir, mod)) + if (specifiedModules.has(mod)) { continue } + specifiedModules.add(mod) + modulesToInstall.push(mod) + } + } + + // Register user and then ad-hoc modules + modulesToInstall.push(...nuxt.options.modules, ...nuxt.options._modules) + + nuxt.hooks.hookOnce('builder:watch', (event, path) => { + if (watchedPaths.has(path)) { nuxt.callHook('restart', { hard: true }) } + }) // Add addComponent({ diff --git a/packages/schema/src/config/common.ts b/packages/schema/src/config/common.ts index 97ebd5520e..cda2f8b736 100644 --- a/packages/schema/src/config/common.ts +++ b/packages/schema/src/config/common.ts @@ -219,6 +219,11 @@ export default defineUntypedSchema({ */ middleware: 'middleware', + /** + * The modules directory, each file in which will be auto-registered as a Nuxt module. + */ + modules: 'modules', + /** * The directory which will be processed to auto-generate your application page routes. */ diff --git a/test/basic.test.ts b/test/basic.test.ts index 6c7410def1..10a04b7b7e 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -46,6 +46,13 @@ describe('route rules', () => { }) }) +describe('modules', () => { + it('should auto-register modules in ~/modules', async () => { + const result = await $fetch('/auto-registered-module') + expect(result).toEqual('handler added by auto-registered module') + }) +}) + describe('pages', () => { it('render index', async () => { const html = await $fetch('/') diff --git a/test/fixtures/basic/modules/auto-registered/index.ts b/test/fixtures/basic/modules/auto-registered/index.ts new file mode 100644 index 0000000000..98e5ad9cbb --- /dev/null +++ b/test/fixtures/basic/modules/auto-registered/index.ts @@ -0,0 +1,15 @@ +import { createResolver, defineNuxtModule, addServerHandler } from 'nuxt/kit' + +export default defineNuxtModule({ + meta: { + name: 'auto-registered-module' + }, + setup () { + const resolver = createResolver(import.meta.url) + + addServerHandler({ + handler: resolver.resolve('./runtime/handler'), + route: '/auto-registered-module' + }) + } +}) diff --git a/test/fixtures/basic/modules/auto-registered/runtime/handler.ts b/test/fixtures/basic/modules/auto-registered/runtime/handler.ts new file mode 100644 index 0000000000..563d40e0b9 --- /dev/null +++ b/test/fixtures/basic/modules/auto-registered/runtime/handler.ts @@ -0,0 +1 @@ +export default defineEventHandler(() => 'handler added by auto-registered module') diff --git a/test/fixtures/basic/modules/example.ts b/test/fixtures/basic/modules/example.ts index d55c2c4253..d4eca6c235 100644 --- a/test/fixtures/basic/modules/example.ts +++ b/test/fixtures/basic/modules/example.ts @@ -1,5 +1,4 @@ -import { fileURLToPath } from 'node:url' -import { defineNuxtModule, addPlugin, useNuxt } from '@nuxt/kit' +import { defineNuxtModule, createResolver, addPlugin, useNuxt } from 'nuxt/kit' export default defineNuxtModule({ defaults: { @@ -11,11 +10,13 @@ export default defineNuxtModule({ configKey: 'sampleModule' }, setup () { - addPlugin(fileURLToPath(new URL('./runtime/plugin', import.meta.url))) + const resolver = createResolver(import.meta.url) + + addPlugin(resolver.resolve('./runtime/plugin')) useNuxt().hook('app:resolve', (app) => { app.middleware.push({ name: 'unctx-test', - path: fileURLToPath(new URL('./runtime/middleware', import.meta.url)), + path: resolver.resolve('./runtime/middleware'), global: true }) })