├── 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 | 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 | 10 | -------------------------------------------------------------------------------- /test/__snapshots__/TestMeSetup.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 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 | 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 | 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 | Volta board 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