├── CODEOWNERS
├── .npmrc
├── example
├── src
│ ├── utils.ts
│ ├── runtime
│ │ ├── components
│ │ │ ├── JsxComponent.tsx
│ │ │ ├── TestMeSetup.vue
│ │ │ └── TestMe.vue
│ │ ├── composables
│ │ │ └── useWrappedFetch.ts
│ │ └── plugins
│ │ │ └── plugin.ts
│ └── module.ts
├── .nuxtrc
├── playground
│ ├── tsconfig.json
│ ├── app.vue
│ ├── package.json
│ ├── nuxt.config.ts
│ └── types.ts
├── tsconfig.json
└── package.json
├── .attw.json
├── test
├── __snapshots__
│ ├── JsxComponent.d.ts
│ ├── JsxComponent.js
│ ├── TestMeSetup.vue
│ ├── plugin.d.ts
│ ├── TestMe.vue
│ ├── TestMeSetup.d.vue.ts
│ ├── TestMeSetup.vue.d.ts
│ └── TestMe.vue.d.ts
└── build.spec.ts
├── src
├── index.ts
├── commands
│ ├── _shared.ts
│ ├── prepare.ts
│ └── build.ts
└── cli.ts
├── vitest.config.mts
├── pnpm-workspace.yaml
├── .gitignore
├── renovate.json
├── .editorconfig
├── eslint.config.js
├── .github
├── workflows
│ ├── reproduction.yml
│ ├── provenance.yml
│ ├── release-nightly.yml
│ └── ci.yml
└── needs-reproduction.md
├── tsconfig.json
├── knip.json
├── LICENSE
├── package.json
├── README.md
└── CHANGELOG.md
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @danielroe
2 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | shamefully-hoist=true
2 | shell-emulator=true
3 |
--------------------------------------------------------------------------------
/example/src/utils.ts:
--------------------------------------------------------------------------------
1 | export const myModuleUtils = () => {}
2 |
--------------------------------------------------------------------------------
/.attw.json:
--------------------------------------------------------------------------------
1 | {
2 | "ignoreRules": ["cjs-resolves-to-esm"]
3 | }
4 |
--------------------------------------------------------------------------------
/example/.nuxtrc:
--------------------------------------------------------------------------------
1 | imports.autoImport=false
2 | typescript.includeWorkspace=true
3 |
--------------------------------------------------------------------------------
/example/playground/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.nuxt/tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------
/test/__snapshots__/JsxComponent.d.ts:
--------------------------------------------------------------------------------
1 | export default function MyJSXComponent(): import("vue/jsx-runtime").JSX.Element;
2 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { default as build } from './commands/build'
2 | export { default as prepare } from './commands/prepare'
3 |
--------------------------------------------------------------------------------
/example/src/runtime/components/JsxComponent.tsx:
--------------------------------------------------------------------------------
1 | export default function MyJSXComponent() {
2 | return
My JSX Component
3 | }
4 |
--------------------------------------------------------------------------------
/example/playground/app.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Nuxt module playground!
4 |
5 |
6 |
7 |
9 |
--------------------------------------------------------------------------------
/vitest.config.mts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitest/config'
2 |
3 | export default defineConfig({
4 | test: {
5 | coverage: {},
6 | },
7 | })
8 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.nuxt/tsconfig.json",
3 | "exclude": [
4 | "dist",
5 | "node_modules",
6 | "playground",
7 | "../src"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - example
3 | - example/playground
4 |
5 | ignoredBuiltDependencies:
6 | - '@parcel/watcher'
7 | - esbuild
8 | - unrs-resolver
9 |
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode
2 | node_modules
3 | *.log
4 | .DS_Store
5 | coverage
6 | dist
7 | types
8 | package-lock.json
9 | .nuxt
10 | .output
11 | .idea/
12 | example/playground/.nuxtrc
13 | .temp*
14 |
--------------------------------------------------------------------------------
/test/__snapshots__/JsxComponent.js:
--------------------------------------------------------------------------------
1 | import { jsx } from "vue/jsx-runtime";
2 | export default function MyJSXComponent() {
3 | return /* @__PURE__ */ jsx("div", { children: "My JSX Component" });
4 | }
5 |
--------------------------------------------------------------------------------
/example/src/runtime/components/TestMeSetup.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 | {{ count }}
8 |
9 |
10 |
--------------------------------------------------------------------------------
/test/__snapshots__/TestMeSetup.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 | {{ count }}
10 |
11 |
12 |
--------------------------------------------------------------------------------
/example/src/runtime/composables/useWrappedFetch.ts:
--------------------------------------------------------------------------------
1 | // TODO: investigate how to avoid the need for this import
2 | import type {} from 'ofetch'
3 | import { useFetch } from '#app'
4 |
5 | export const useWrappedFetch = () => {
6 | return useFetch('/api/applications')
7 | }
8 |
--------------------------------------------------------------------------------
/test/__snapshots__/plugin.d.ts:
--------------------------------------------------------------------------------
1 | export type SharedTypeFromRuntime = 'shared-type';
2 | declare const _default: import("nuxt/app").Plugin<{
3 | injection: "injected";
4 | }> & import("nuxt/app").ObjectPlugin<{
5 | injection: "injected";
6 | }>;
7 | export default _default;
8 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "github>nuxt/renovate-config-nuxt"
5 | ],
6 | "baseBranches": [
7 | "main"
8 | ],
9 | "lockFileMaintenance": {
10 | "enabled": true
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = true
6 | trim_trailing_whitespace = true
7 | charset = utf-8
8 |
9 | [*.js]
10 | indent_style = space
11 | indent_size = 2
12 |
13 | [{package.json,*.yml,*.cjson}]
14 | indent_style = space
15 | indent_size = 2
16 |
--------------------------------------------------------------------------------
/test/__snapshots__/TestMe.vue:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 | {{ count }}
16 |
17 |
18 |
--------------------------------------------------------------------------------
/example/src/runtime/plugins/plugin.ts:
--------------------------------------------------------------------------------
1 | import { defineNuxtPlugin } from '#app'
2 |
3 | export type SharedTypeFromRuntime = 'shared-type'
4 |
5 | export default defineNuxtPlugin(() => {
6 | console.log('Plugin injected by my-module!')
7 | return {
8 | provide: {
9 | injection: 'injected' as const,
10 | },
11 | }
12 | })
13 |
--------------------------------------------------------------------------------
/example/src/runtime/components/TestMe.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 | {{ count }}
17 |
18 |
19 |
--------------------------------------------------------------------------------
/example/playground/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "my-module-playground",
4 | "scripts": {
5 | "test:types": "nuxt prepare && vue-tsc --noEmit"
6 | },
7 | "dependencies": {
8 | "my-module": "workspace:*",
9 | "nuxt": "latest"
10 | },
11 | "devDependencies": {
12 | "typescript": "^5.9.3",
13 | "vue-tsc": "^3.1.7"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/example/playground/nuxt.config.ts:
--------------------------------------------------------------------------------
1 | export default defineNuxtConfig({
2 | modules: ['my-module'],
3 | hooks: {
4 | 'my-module:init'(sharedType) {
5 | // @ts-expect-error invalid assignment
6 | const _b: number = sharedType
7 | },
8 | },
9 | myModule: {
10 | apiKey: '',
11 | // @ts-expect-error invalid configuration key
12 | api: '',
13 | },
14 | })
15 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import { createConfigForNuxt } from '@nuxt/eslint-config/flat'
3 |
4 | export default createConfigForNuxt({
5 | features: {
6 | tooling: true,
7 | stylistic: true,
8 | },
9 | dirs: {
10 | src: [
11 | './example',
12 | './example/playground',
13 | ],
14 | },
15 | }).append({
16 | ignores: ['test/__snapshots__/*'],
17 | })
18 |
--------------------------------------------------------------------------------
/.github/workflows/reproduction.yml:
--------------------------------------------------------------------------------
1 | name: Reproduire
2 | on:
3 | issues:
4 | types: [labeled]
5 |
6 | permissions:
7 | issues: write
8 |
9 | jobs:
10 | reproduire:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
14 | - uses: Hebilicious/reproduire@4b686ae9cbb72dad60f001d278b6e3b2ce40a9ac # v0.0.9-mp
15 | with:
16 | label: needs reproduction
17 |
--------------------------------------------------------------------------------
/test/__snapshots__/TestMeSetup.d.vue.ts:
--------------------------------------------------------------------------------
1 | type __VLS_Props = {
2 | count: number;
3 | };
4 | declare const __VLS_export: import("@vue/runtime-core").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {}, string, import("@vue/runtime-core").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("@vue/runtime-core").ComponentProvideOptions, false, {}, any>;
5 | declare const _default: typeof __VLS_export;
6 | export default _default;
7 |
--------------------------------------------------------------------------------
/test/__snapshots__/TestMeSetup.vue.d.ts:
--------------------------------------------------------------------------------
1 | type __VLS_Props = {
2 | count: number;
3 | };
4 | declare const __VLS_export: import("@vue/runtime-core").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {}, string, import("@vue/runtime-core").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("@vue/runtime-core").ComponentProvideOptions, false, {}, any>;
5 | declare const _default: typeof __VLS_export;
6 | export default _default;
7 |
--------------------------------------------------------------------------------
/.github/workflows/provenance.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 | permissions:
11 | contents: read
12 | jobs:
13 | check-provenance:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v6
17 | with:
18 | fetch-depth: 0
19 | - name: Check provenance downgrades
20 | uses: danielroe/provenance-action@41bcc969e579d9e29af08ba44fcbfdf95cee6e6c # v0.1.1
21 | with:
22 | fail-on-provenance-change: true
23 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "esModuleInterop": true,
4 | "skipLibCheck": true,
5 | "target": "es2022",
6 | "allowJs": true,
7 | "resolveJsonModule": true,
8 | "moduleDetection": "force",
9 | "isolatedModules": true,
10 | "verbatimModuleSyntax": true,
11 | "strict": true,
12 | "noUncheckedIndexedAccess": true,
13 | "noImplicitOverride": true,
14 | "module": "Preserve",
15 | "noEmit": true,
16 | "moduleResolution": "Bundler",
17 | "declaration": true
18 | },
19 | "include": [
20 | "src",
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/example/playground/types.ts:
--------------------------------------------------------------------------------
1 | import { describe, expectTypeOf, it } from 'vitest'
2 | import { useNuxtApp } from '#app'
3 |
4 | describe('', () => {
5 | it('should have typed injection', () => {
6 | expectTypeOf(useNuxtApp().$injection).toEqualTypeOf<'injected'>()
7 | })
8 | it('should have typed runtime hooks', () => {
9 | useNuxtApp().hook('my-module:runtime-hook', () => {})
10 | })
11 | it('should have typed runtime config', () => {
12 | expectTypeOf(useRuntimeConfig().public.NAME).toEqualTypeOf()
13 | expectTypeOf(useRuntimeConfig().PRIVATE_NAME).toEqualTypeOf()
14 | })
15 | })
16 |
--------------------------------------------------------------------------------
/src/commands/_shared.ts:
--------------------------------------------------------------------------------
1 | import type { ArgDef } from 'citty'
2 | import { resolve } from 'pathe'
3 |
4 | export const sharedArgs = {
5 | // cwd falls back to rootDir's default (indirect default)
6 | cwd: {
7 | type: 'string',
8 | valueHint: 'directory',
9 | description: 'Specify the working directory, this takes precedence over ROOTDIR (default: `.`)',
10 | default: undefined,
11 | },
12 | rootDir: {
13 | type: 'positional',
14 | description: 'Specifies the working directory (default: `.`)',
15 | required: false,
16 | default: '.',
17 | },
18 | } as const satisfies Record
19 |
20 | export const resolveCwdArg = (args: { cwd?: string, rootDir?: string }) => resolve(args.cwd || args.rootDir || '.')
21 |
--------------------------------------------------------------------------------
/test/__snapshots__/TestMe.vue.d.ts:
--------------------------------------------------------------------------------
1 | declare const __VLS_export: import("@vue/runtime-core").DefineComponent, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {}, string, import("@vue/runtime-core").PublicProps, Readonly> & Readonly<{}>, {}, {}, {}, {}, string, import("@vue/runtime-core").ComponentProvideOptions, true, {}, any>;
12 | declare const _default: typeof __VLS_export;
13 | export default _default;
14 |
--------------------------------------------------------------------------------
/knip.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/knip@5/schema.json",
3 | "ignore": [
4 | "test/__snapshots__/**"
5 | ],
6 | "workspaces": {
7 | ".": {
8 | "ignoreDependencies": [
9 | "mkdist",
10 | "vue-sfc-transformer"
11 | ]
12 | },
13 | "example": {
14 | "entry": [
15 | "src/module.ts",
16 | "src/utils.ts",
17 | "src/runtime/**"
18 | ],
19 | "ignoreDependencies": [
20 | "@nuxt/module-builder"
21 | ]
22 | },
23 | "example/playground": {
24 | "entry": [
25 | "providers/custom/index.ts",
26 | "{components,layouts,pages,plugins,server}/**",
27 | "{app,error}.vue",
28 | "layers/**",
29 | "*.ts"
30 | ],
31 | "ignoreDependencies": [
32 | "my-module"
33 | ]
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/.github/workflows/release-nightly.yml:
--------------------------------------------------------------------------------
1 | name: release
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 | push:
8 | branches:
9 | - main
10 |
11 | permissions: {}
12 |
13 | jobs:
14 | release-pkg-pr-new:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
18 | - run: corepack enable
19 | - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
20 | with:
21 | node-version: lts/*
22 | cache: "pnpm"
23 |
24 | - name: Install dependencies
25 | run: pnpm install
26 |
27 | - name: Prepare build environment
28 | run: pnpm dev:prepare
29 |
30 | - run: pnpm build
31 |
32 | - name: publish nightly release
33 | run: pnpm pkg-pr-new publish --compact
34 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "my-module",
3 | "license": "MIT",
4 | "version": "1.0.0",
5 | "type": "module",
6 | "exports": {
7 | ".": {
8 | "types": "./dist/types.d.mts",
9 | "default": "./dist/module.mjs"
10 | },
11 | "./utils": "./dist/utils.mjs"
12 | },
13 | "main": "dist/module.mjs",
14 | "typesVersions": {
15 | "*": {
16 | "utils": [
17 | "./dist/utils.d.mts"
18 | ]
19 | }
20 | },
21 | "files": [
22 | "dist"
23 | ],
24 | "scripts": {
25 | "prepack": "jiti ../src/cli.ts build",
26 | "dev": "nuxi dev playground",
27 | "dev:build": "nuxi build playground",
28 | "dev:prepare": "jiti ../src/cli.ts build --stub && jiti ../src/cli.ts prepare",
29 | "test:types": "vue-tsc --noEmit"
30 | },
31 | "dependencies": {
32 | "@nuxt/kit": "^4.2.2"
33 | },
34 | "devDependencies": {
35 | "@nuxt/module-builder": "latest",
36 | "nuxt": "^4.2.2"
37 | },
38 | "build": {
39 | "entries": [
40 | "./src/utils"
41 | ]
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/cli.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import { defineCommand, runMain } from 'citty'
3 | import type { CommandDef } from 'citty'
4 | import { consola } from 'consola'
5 | import { name, description, version } from '../package.json'
6 |
7 | const _rDefault = (r: unknown) => (r && typeof r === 'object' && 'default' in r ? r.default : r) as Promise
8 |
9 | const main = defineCommand({
10 | meta: {
11 | name,
12 | description,
13 | version,
14 | },
15 | subCommands: {
16 | prepare: () => import('./commands/prepare').then(_rDefault),
17 | build: () => import('./commands/build').then(_rDefault),
18 | },
19 | setup(context) {
20 | // TODO: support 'default command' in citty?
21 | const firstArg = context.rawArgs[0]
22 | if (context.cmd.subCommands && !(firstArg && firstArg in context.cmd.subCommands)) {
23 | consola.warn('Please specify the `build` command explicitly. In a future version of `@nuxt/module-builder`, the implicit default build command will be removed.')
24 | context.rawArgs.unshift('build')
25 | }
26 | },
27 | })
28 |
29 | runMain(main)
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 - Nuxt Project
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/commands/prepare.ts:
--------------------------------------------------------------------------------
1 | import type { NuxtConfig } from '@nuxt/schema'
2 | import { defineCommand } from 'citty'
3 | import { resolve } from 'pathe'
4 | import { resolveCwdArg, sharedArgs } from './_shared'
5 |
6 | export default defineCommand({
7 | meta: {
8 | name: 'prepare',
9 | description: 'Prepare @nuxt/module-builder environment by writing types and stubs',
10 | },
11 | args: {
12 | ...sharedArgs,
13 | },
14 | async run(context) {
15 | const { runCommand } = await import('@nuxt/cli')
16 |
17 | const cwd = resolveCwdArg(context.args)
18 |
19 | return runCommand('prepare', [cwd], {
20 | overrides: {
21 | compatibilityDate: '2024-04-03',
22 | typescript: {
23 | builder: 'shared',
24 | },
25 | imports: {
26 | autoImport: false,
27 | },
28 | modules: [
29 | resolve(cwd, './src/module'),
30 | function (_options, nuxt) {
31 | nuxt.hooks.hook('app:templates', (app) => {
32 | for (const template of app.templates) {
33 | template.write = true
34 | }
35 | })
36 | },
37 | ],
38 | } satisfies NuxtConfig,
39 | })
40 | },
41 | })
42 |
--------------------------------------------------------------------------------
/example/src/module.ts:
--------------------------------------------------------------------------------
1 | import { defineNuxtModule, addPlugin, createResolver } from '@nuxt/kit'
2 |
3 | // https://github.com/nuxt/module-builder/issues/242
4 | import type { SharedTypeFromRuntime } from './runtime/plugins/plugin'
5 |
6 | // Module options TypeScript interface definition
7 | export interface ModuleOptions {
8 | apiKey: string
9 | shared?: SharedTypeFromRuntime
10 | }
11 |
12 | export interface ModuleHooks {
13 | 'my-module:init': (sharedType: SharedTypeFromRuntime) => void
14 | }
15 |
16 | export interface ModuleRuntimeHooks {
17 | 'my-module:runtime-hook': () => void
18 | }
19 |
20 | export interface ModulePublicRuntimeConfig {
21 | NAME: string
22 | }
23 |
24 | export interface ModuleRuntimeConfig {
25 | PRIVATE_NAME: string
26 | }
27 |
28 | export default defineNuxtModule({
29 | meta: {
30 | name: 'my-module',
31 | configKey: 'myModule',
32 | },
33 | // Default configuration options of the Nuxt module
34 | defaults: {
35 | apiKey: '',
36 | },
37 | setup(_options, _nuxt) {
38 | const resolver = createResolver(import.meta.url)
39 |
40 | // @ts-expect-error type should be resolved
41 | _options.shared = 'not-shared-type'
42 |
43 | // Do not add the extension since the `.ts` will be transpiled to `.mjs` after `npm run prepack`
44 | addPlugin(resolver.resolve('./runtime/plugins/plugin'))
45 | },
46 | })
47 |
--------------------------------------------------------------------------------
/.github/needs-reproduction.md:
--------------------------------------------------------------------------------
1 | Would you be able to provide a [reproduction](https://nuxt.com/docs/community/reporting-bugs/#create-a-minimal-reproduction)? 🙏
2 |
3 |
4 | More info
5 |
6 | ### Why do I need to provide a reproduction?
7 |
8 | Reproductions make it possible for us to triage and fix issues quickly with a relatively small team. It helps us discover the source of the problem, and also can reveal assumptions you or we might be making.
9 |
10 | ### What will happen?
11 |
12 | If you've provided a reproduction, we'll remove the label and try to reproduce the issue. If we can, we'll mark it as a bug and prioritise it based on its severity and how many people we think it might affect.
13 |
14 | If `needs reproduction` labeled issues don't receive any substantial activity (e.g., new comments featuring a reproduction link), we'll close them. That's not because we don't care! At any point, feel free to comment with a reproduction and we'll reopen it.
15 |
16 | ### How can I create a reproduction?
17 |
18 | We have a template for starting with a minimal reproduction:
19 |
20 | 👉 https://stackblitz.com/github/nuxt/starter/tree/module
21 |
22 | A public GitHub repository is also perfect. 👌
23 |
24 | Please ensure that the reproduction is as **minimal** as possible. See more details [in our guide](https://nuxt.com/docs/community/reporting-bugs/#create-a-minimal-reproduction).
25 |
26 | You might also find these other articles interesting and/or helpful:
27 |
28 | - [The Importance of Reproductions](https://antfu.me/posts/why-reproductions-are-required)
29 | - [How to Generate a Minimal, Complete, and Verifiable Example](https://stackoverflow.com/help/mcve)
30 |
31 |
32 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | lint:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
16 | - run: corepack enable
17 | - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
18 | with:
19 | node-version: 22
20 | cache: pnpm
21 |
22 | - name: 📦 Install dependencies
23 | run: pnpm install
24 |
25 | - run: pnpm dev:prepare
26 |
27 | - name: 🔠 Lint project
28 | run: pnpm lint
29 |
30 | - name: ✂️ Knip project
31 | run: pnpm test:knip
32 |
33 | - name: ⚙️ Check package engines
34 | run: pnpm test:engines
35 |
36 | - name: 💪 Check published types
37 | run: pnpm test:attw
38 |
39 | - name: 📦 Check `package.json`
40 | run: pnpm test:publint
41 |
42 | ci:
43 | strategy:
44 | matrix:
45 | os: [ubuntu-latest, windows-latest]
46 | runs-on: ${{ matrix.os }}
47 | steps:
48 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
49 | - run: corepack enable
50 | - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
51 | with:
52 | node-version: lts/-1
53 | cache: pnpm
54 |
55 | - name: 📦 Install dependencies
56 | run: pnpm install
57 |
58 | - run: pnpm dev:prepare
59 |
60 | - name: 🧪 Test project
61 | run: pnpm test
62 |
63 | - name: 🛠 Build project
64 | run: pnpm build
65 |
66 | - name: 🛠 Build project (example)
67 | run: pnpm example:build
68 |
69 | - name: 💪 Type check
70 | run: pnpm test:types
71 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@nuxt/module-builder",
3 | "version": "1.0.2",
4 | "repository": "nuxt/module-builder",
5 | "description": "Complete solution for building Nuxt modules",
6 | "license": "MIT",
7 | "type": "module",
8 | "exports": {
9 | ".": "./dist/index.mjs",
10 | "./package.json": "./package.json"
11 | },
12 | "typesVersions": {
13 | "*": {
14 | "*": [
15 | "./dist/*.d.mts"
16 | ],
17 | "package.json": [
18 | "package.json"
19 | ]
20 | }
21 | },
22 | "bin": {
23 | "nuxt-build-module": "./dist/cli.mjs",
24 | "nuxt-module-build": "./dist/cli.mjs"
25 | },
26 | "files": [
27 | "dist"
28 | ],
29 | "scripts": {
30 | "build": "unbuild",
31 | "dev:prepare": "unbuild --stub && pnpm -r dev:prepare",
32 | "example:build": "pnpm nuxt-module-build build ./example",
33 | "example:stub": "pnpm nuxt-module-build build --stub ./example",
34 | "lint": "eslint .",
35 | "nuxt-module-build": "JITI_ESM_RESOLVE=1 jiti ./src/cli.ts",
36 | "prepack": "pnpm build",
37 | "release": "pnpm vitest run && pnpm build && changelogen --release && pnpm publish && git push --follow-tags",
38 | "test": "pnpm vitest --coverage",
39 | "test:attw": "attw --pack example && attw --pack .",
40 | "test:publint": "cd example && publint",
41 | "test:engines": "installed-check -d --no-workspaces",
42 | "test:knip": "knip --exclude unresolved",
43 | "test:types": "vue-tsc --noEmit && pnpm -r test:types"
44 | },
45 | "packageManager": "pnpm@10.25.0",
46 | "dependencies": {
47 | "citty": "^0.1.6",
48 | "consola": "^3.4.2",
49 | "defu": "^6.1.4",
50 | "jiti": "^2.6.1",
51 | "magic-regexp": "^0.10.0",
52 | "mkdist": "^2.4.1",
53 | "mlly": "^1.8.0",
54 | "pathe": "^2.0.3",
55 | "pkg-types": "^2.3.0",
56 | "tsconfck": "^3.1.6",
57 | "unbuild": "^3.6.1",
58 | "vue-sfc-transformer": "^0.1.17"
59 | },
60 | "peerDependencies": {
61 | "@nuxt/cli": "^3.31.1",
62 | "typescript": "^5.9.3"
63 | },
64 | "devDependencies": {
65 | "@arethetypeswrong/cli": "^0.18.2",
66 | "@nuxt/cli": "^3.31.1",
67 | "@nuxt/eslint-config": "^1.11.0",
68 | "@nuxt/schema": "^4.2.2",
69 | "@types/node": "^24.10.1",
70 | "@types/semver": "^7.7.1",
71 | "@vitest/coverage-v8": "^4.0.15",
72 | "changelogen": "^0.6.2",
73 | "eslint": "^9.39.1",
74 | "installed-check": "^9.3.0",
75 | "knip": "^5.72.0",
76 | "nuxt": "^4.2.2",
77 | "ofetch": "^1.5.1",
78 | "pkg-pr-new": "^0.0.62",
79 | "publint": "^0.3.15",
80 | "semver": "^7.7.3",
81 | "tinyexec": "^1.0.2",
82 | "typescript": "~5.9.3",
83 | "vitest": "^4.0.15",
84 | "vue": "^3.5.25",
85 | "vue-tsc": "^3.1.7"
86 | },
87 | "resolutions": {
88 | "@nuxt/kit": "^4.2.2",
89 | "@nuxt/module-builder": "workspace:*",
90 | "@nuxt/schema": "^4.2.2",
91 | "typescript": "~5.9.3",
92 | "vue": "^3.5.25",
93 | "vue-tsc": "^3.1.7"
94 | },
95 | "engines": {
96 | "node": "^18.0.0 || >=20.0.0"
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 📦 Nuxt Module Builder
2 |
3 | [![npm version][npm-version-src]][npm-version-href]
4 | [![License][license-src]][license-href]
5 | [![npm downloads][npm-downloads-src]][npm-downloads-href]
6 |
7 |
8 | > The complete solution to build and ship [Nuxt modules](https://nuxt.com/modules).
9 |
10 | ## Features
11 |
12 | - Compatible with Nuxt 3 and Nuxt Kit
13 | - Unified build with [unjs/unbuild](https://github.com/unjs/unbuild)
14 | - Automated build config using last module spec
15 | - Typescript and ESM support
16 | - Auto generated types and shims for `@nuxt/schema`
17 |
18 | We recommend to checkout the [Nuxt modules author guide](https://nuxt.com/docs/guide/going-further/modules) before starting with the module-builder.
19 |
20 | ## Requirements
21 |
22 | For a user to use a module generated from module-builder, it's recommended they have:
23 | - Node.js >= 18.x. _Latest Node LTS preferred_
24 | - Nuxt 3+.
25 |
26 | ## Quick start
27 |
28 | Get started with our [module starter](https://github.com/nuxt/starter/tree/module):
29 |
30 | ```bash
31 | npm create nuxt -- -t module my-module
32 | ```
33 |
34 | ## Project structure
35 |
36 | The module builder requires a special project structure. You can check out the [module template](https://github.com/nuxt/starter/tree/module).
37 |
38 | ### `src/module.ts`
39 |
40 | The entrypoint for module definition.
41 |
42 | A default export using `defineNuxtModule` and `ModuleOptions` type export is expected.
43 |
44 | You could also optionally export `ModuleHooks` or `ModuleRuntimeHooks` to annotate any custom hooks the module uses.
45 |
46 | ```ts [src/module.ts]
47 | import { defineNuxtModule } from '@nuxt/kit'
48 |
49 | export interface ModuleOptions {
50 | apiKey: string
51 | }
52 |
53 | export interface ModuleHooks {
54 | 'my-module:init': any
55 | }
56 |
57 | export interface ModuleRuntimeHooks {
58 | 'my-module:runtime-hook': any
59 | }
60 |
61 | export interface ModuleRuntimeConfig {
62 | PRIVATE_NAME: string
63 | }
64 |
65 | export interface ModulePublicRuntimeConfig {
66 | NAME: string
67 | }
68 |
69 | export default defineNuxtModule({
70 | meta: {
71 | name: 'my-module',
72 | configKey: 'myModule'
73 | },
74 | defaults: {
75 | apiKey: 'test'
76 | },
77 | async setup (moduleOptions, nuxt) {
78 | // Write module logic in setup function
79 | }
80 | })
81 | ```
82 |
83 | ### `src/runtime/`
84 |
85 | Any runtime file and code that we need to provide by module including plugins, composables and server api, should be in this directory.
86 |
87 | Each file will be transformed individually using [unjs/mkdist](https://github.com/unjs/mkdist) to `dist/runtime/`.
88 |
89 |
90 |
91 | ### `package.json`:
92 |
93 | A minimum `package.json` should look like this:
94 |
95 | ```json [package.json]
96 | {
97 | "name": "my-module",
98 | "license": "MIT",
99 | "version": "1.0.0",
100 | "exports": {
101 | ".": {
102 | "types": "./dist/types.d.mts",
103 | "import": "./dist/module.mjs"
104 | }
105 | },
106 | "main": "./dist/module.mjs",
107 | "typesVersions": {
108 | "*": {
109 | ".": [
110 | "./dist/types.d.mts"
111 | ]
112 | }
113 | },
114 | "files": [
115 | "dist"
116 | ],
117 | "scripts": {
118 | "prepack": "nuxt-module-build build"
119 | },
120 | "dependencies": {
121 | "@nuxt/kit": "latest"
122 | },
123 | "devDependencies": {
124 | "@nuxt/module-builder": "latest"
125 | }
126 | }
127 | ```
128 |
129 | ### `build.config.ts` (optional)
130 |
131 | Module builder is essentially a preset for [unjs/unbuild](https://github.com/unjs/unbuild), check out the [build command](./src/commands/build.ts#L51) for reference.
132 |
133 | To customize/extend the unbuild configuration you can add a `build.config.ts` in the root of your project:
134 |
135 | ```ts
136 | import { defineBuildConfig } from 'unbuild'
137 |
138 | export default defineBuildConfig({
139 | // set additional configuration or customize using hooks
140 | })
141 | ```
142 |
143 | ## Dist files
144 |
145 | Module builder generates dist files in `dist/` directory:
146 |
147 | - `module.mjs`: Module entrypoint build from `src/module`
148 | - `module.json`: Module meta extracted from `module.mjs` + `package.json`
149 | - `types.d.mts`: Exported types in addition to shims for `nuxt.config` auto completion.
150 | - `runtime/*`: Individually transformed files using [unjs/mkdist](https://github.com/unjs/mkdist)
151 | - Javascript and `.ts` files will be transformed to `.js` with extracted types on `.d.ts` file with same name
152 | - `.vue` files will be transformed with extracted `.d.ts` file
153 | - Other files will be copied as is
154 |
155 | ## 💻 Development
156 |
157 | - Clone repository
158 | - Enable [Corepack](https://github.com/nodejs/corepack) using `corepack enable`
159 | - Install dependencies using `pnpm install`
160 | - Try building [example module](./example) using `pnpm example:build`
161 |
162 | ## License
163 |
164 | [MIT](./LICENSE) - Made with 💚
165 |
166 |
167 | [npm-version-src]: https://img.shields.io/npm/v/@nuxt/module-builder/latest.svg?style=flat&colorA=18181B&colorB=28CF8D
168 | [npm-version-href]: https://npmjs.com/package/@nuxt/module-builder
169 |
170 | [npm-downloads-src]: https://img.shields.io/npm/dt/@nuxt/module-builder.svg?style=flat&colorA=18181B&colorB=28CF8D
171 | [npm-downloads-href]: https://npm.chart.dev/@nuxt/module-builder
172 |
173 | [license-src]: https://img.shields.io/github/license/nuxt/module-builder.svg?style=flat&colorA=18181B&colorB=28CF8D
174 | [license-href]: https://github.com/nuxt/module-builder/blob/main/LICENSE
175 |
--------------------------------------------------------------------------------
/test/build.spec.ts:
--------------------------------------------------------------------------------
1 | import { fileURLToPath } from 'node:url'
2 | import { cp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises'
3 | import { beforeAll, describe, it, expect } from 'vitest'
4 | import { exec } from 'tinyexec'
5 | import { readPackageJSON } from 'pkg-types'
6 | import { dirname, join } from 'pathe'
7 | import { findStaticImports } from 'mlly'
8 | import { version as nuxtVersion } from 'nuxt/package.json'
9 | import { satisfies } from 'semver'
10 | import { version } from '../package.json'
11 |
12 | describe('module builder', () => {
13 | const rootDir = fileURLToPath(new URL('../example', import.meta.url))
14 | const secondRootDir = rootDir.replace('example', '.temp-example-without-options')
15 | const distDir = join(rootDir, 'dist')
16 | const secondDistDir = join(secondRootDir, 'dist')
17 | const runtimeDir = join(distDir, 'runtime')
18 |
19 | beforeAll(async () => {
20 | // Prepare second root directory without type export
21 | await mkdir(dirname(secondRootDir), { recursive: true })
22 | await rm(secondRootDir, { force: true, recursive: true })
23 | await cp(rootDir, secondRootDir, { recursive: true })
24 | const moduleSrc = join(secondRootDir, 'src/module.ts')
25 | const contents = await readFile(moduleSrc, 'utf-8').then(r => r.replace('export interface ModuleOptions', 'interface ModuleOptions'))
26 | await writeFile(moduleSrc, contents)
27 |
28 | await Promise.all([
29 | exec('pnpm', ['dev:prepare'], { nodeOptions: { cwd: rootDir } }).then(() => exec('pnpm', ['prepack'], { nodeOptions: { cwd: rootDir } })),
30 | exec('pnpm', ['dev:prepare'], { nodeOptions: { cwd: secondRootDir } }).then(() => exec('pnpm', ['prepack'], { nodeOptions: { cwd: secondRootDir } })),
31 | ])
32 | }, 120 * 1000)
33 |
34 | it('should generate all files', async () => {
35 | const files = await readdir(distDir)
36 | expect(files).toMatchInlineSnapshot(`
37 | [
38 | "module.d.mts",
39 | "module.json",
40 | "module.mjs",
41 | "runtime",
42 | "types.d.mts",
43 | "utils.d.mts",
44 | "utils.mjs",
45 | ]
46 | `)
47 |
48 | const runtime = await readdir(join(runtimeDir, 'plugins'))
49 | expect(runtime).toMatchInlineSnapshot(`
50 | [
51 | "plugin.d.ts",
52 | "plugin.js",
53 | ]
54 | `)
55 | })
56 |
57 | it('should write types to output directory', async () => {
58 | const types = await readFile(join(distDir, 'types.d.mts'), 'utf-8')
59 | expect(types).toMatchInlineSnapshot(`
60 | "import type { ModuleHooks, ModuleRuntimeHooks, ModuleRuntimeConfig, ModulePublicRuntimeConfig } from './module.mjs'
61 |
62 | declare module '#app' {
63 | interface RuntimeNuxtHooks extends ModuleRuntimeHooks {}
64 | }
65 |
66 | declare module '@nuxt/schema' {
67 | interface NuxtHooks extends ModuleHooks {}
68 | interface RuntimeConfig extends ModuleRuntimeConfig {}
69 | interface PublicRuntimeConfig extends ModulePublicRuntimeConfig {}
70 | }
71 |
72 | export { default } from './module.mjs'
73 |
74 | export { type ModuleHooks, type ModuleOptions, type ModulePublicRuntimeConfig, type ModuleRuntimeConfig, type ModuleRuntimeHooks } from './module.mjs'
75 | "
76 | `)
77 | })
78 |
79 | it('should generate types when no ModuleOptions are exported', async () => {
80 | const types = await readFile(join(secondDistDir, 'types.d.mts'), 'utf-8')
81 | expect(types).toMatchInlineSnapshot(`
82 | "import type { NuxtModule } from '@nuxt/schema'
83 |
84 | import type { default as Module, ModuleHooks, ModuleRuntimeHooks, ModuleRuntimeConfig, ModulePublicRuntimeConfig } from './module.mjs'
85 |
86 | declare module '#app' {
87 | interface RuntimeNuxtHooks extends ModuleRuntimeHooks {}
88 | }
89 |
90 | declare module '@nuxt/schema' {
91 | interface NuxtHooks extends ModuleHooks {}
92 | interface RuntimeConfig extends ModuleRuntimeConfig {}
93 | interface PublicRuntimeConfig extends ModulePublicRuntimeConfig {}
94 | }
95 |
96 | export type ModuleOptions = typeof Module extends NuxtModule ? Partial : Record
97 |
98 | export { default } from './module.mjs'
99 |
100 | export { type ModuleHooks, type ModulePublicRuntimeConfig, type ModuleRuntimeConfig, type ModuleRuntimeHooks } from './module.mjs'
101 | "
102 | `)
103 | })
104 |
105 | it('should generate module metadata as separate JSON file', async () => {
106 | const meta = await readFile(join(distDir, 'module.json'), 'utf-8')
107 | const unbuildPkg = await readPackageJSON('unbuild')
108 | expect(JSON.parse(meta)).toMatchObject(
109 | {
110 | builder: {
111 | '@nuxt/module-builder': version,
112 | 'unbuild': unbuildPkg.version,
113 | },
114 | configKey: 'myModule',
115 | name: 'my-module',
116 | version: '1.0.0',
117 | },
118 | )
119 | })
120 |
121 | it('should generate typed plugin', async () => {
122 | const pluginDts = await readFile(join(distDir, 'runtime/plugins/plugin.d.ts'), 'utf-8')
123 | await expect(pluginDts).toMatchFileSnapshot('__snapshots__/plugin.d.ts')
124 | })
125 |
126 | it('should correctly add extensions to imports from runtime/ directory', async () => {
127 | const moduleDts = await readFile(join(distDir, 'module.d.mts'), 'utf-8')
128 | const runtimeImport = findStaticImports(moduleDts).find(i => i.specifier.includes('runtime'))
129 | expect(runtimeImport!.code.trim()).toMatchInlineSnapshot(`"import { SharedTypeFromRuntime } from '../dist/runtime/plugins/plugin.js';"`)
130 | })
131 |
132 | it('should generate components correctly', async () => {
133 | const componentFile = await readFile(join(distDir, 'runtime/components/TestMe.vue'), 'utf-8')
134 | await expect(componentFile.replace(/\r\n/g, '\n')).toMatchFileSnapshot('__snapshots__/TestMe.vue')
135 |
136 | const componentDeclarationFile = await readFile(join(distDir, 'runtime/components/TestMe.vue.d.ts'), 'utf-8')
137 | await expect(componentDeclarationFile.replace(/\r\n/g, '\n')).toMatchFileSnapshot('__snapshots__/TestMe.vue.d.ts')
138 | })
139 |
140 | it('should generate components with