From aa31805d78ffcc957d09e1786670444b20e89a1e Mon Sep 17 00:00:00 2001 From: oritwoen <18102267+oritwoen@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:37:57 +0200 Subject: [PATCH 1/2] feat(add-template): support project stubs --- README.md | 4 ++++ packages/nuxt-cli/src/commands/add-template.ts | 11 ++++++++++- .../nuxt-cli/test/unit/commands/add-template.spec.ts | 11 ++++++++++- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 12f4d4916..588a92dba 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,10 @@ nuxt add nuxt-shiki nuxt add nuxt-seo-kit ``` +## Custom templates + +To override a built-in `add-template` template, add a matching file to the project `stubs` directory. For example, `stubs/component.vue` is used instead of the default component contents when running `nuxt add-template component `. + ## Update Notifications Once a day, `nuxt/cli` checks in the background whether a newer version of Nuxt has been released and, if so, prints a short nudge to run `nuxt upgrade` after the command finishes. The `latest` dist-tag is read from the registry configured in your `.npmrc` and cached in your user-level `.nuxtrc`. diff --git a/packages/nuxt-cli/src/commands/add-template.ts b/packages/nuxt-cli/src/commands/add-template.ts index bdb3c19db..690e02efe 100644 --- a/packages/nuxt-cli/src/commands/add-template.ts +++ b/packages/nuxt-cli/src/commands/add-template.ts @@ -6,7 +6,7 @@ import { styleText } from 'node:util' import { intro, outro } from '@clack/prompts' import { defineCommand } from 'citty' -import { dirname, resolve } from 'pathe' +import { dirname, extname, resolve } from 'pathe' import { loadKit } from '../utils/kit' import { logger } from '../utils/logger' @@ -99,6 +99,15 @@ export default defineCommand({ const kit = await loadKit(cwd) const config = await kit.loadNuxtConfig({ cwd }) const res = templates[templateName]({ name, args: ctx.args, nuxtOptions: config }) + const stubPath = resolve(config.rootDir, 'stubs', `${templateName}${extname(res.path)}`) + try { + res.contents = await fsp.readFile(stubPath, 'utf8') + } + catch (error) { + if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) { + throw error + } + } const parentDir = dirname(res.path) const createdDir = await fsp.mkdir(parentDir, { recursive: true }) if (createdDir) { diff --git a/packages/nuxt-cli/test/unit/commands/add-template.spec.ts b/packages/nuxt-cli/test/unit/commands/add-template.spec.ts index 2ccd20421..fe5e5a655 100644 --- a/packages/nuxt-cli/test/unit/commands/add-template.spec.ts +++ b/packages/nuxt-cli/test/unit/commands/add-template.spec.ts @@ -1,4 +1,4 @@ -import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -33,6 +33,15 @@ describe('add-template command', () => { expect(await readFile(path, 'utf8')).toMatch(/\n$/) }) + it('uses a project stub when one exists for the template', async () => { + await mkdir(join(cwd, 'stubs')) + await writeFile(join(cwd, 'stubs/component.vue'), '\n') + + await run('component', 'user-card') + + await expect(readFile(join(cwd, 'components/user-card.vue'), 'utf8')).resolves.toBe('\n') + }) + it('exposes template-specific options', async () => { await run('api', 'users', '--method', 'get') await run('component', 'island', '--mode', 'client') From ac137bd68731d3be6413695c12ce83d85febff48 Mon Sep 17 00:00:00 2001 From: oritwoen <18102267+oritwoen@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:57:14 +0200 Subject: [PATCH 2/2] fix(add-template): preserve stub contents --- packages/nuxt-cli/src/commands/add-template.ts | 5 +++-- .../test/unit/commands/add-template.spec.ts | 15 ++++++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/nuxt-cli/src/commands/add-template.ts b/packages/nuxt-cli/src/commands/add-template.ts index 690e02efe..077c5f731 100644 --- a/packages/nuxt-cli/src/commands/add-template.ts +++ b/packages/nuxt-cli/src/commands/add-template.ts @@ -100,8 +100,9 @@ export default defineCommand({ const config = await kit.loadNuxtConfig({ cwd }) const res = templates[templateName]({ name, args: ctx.args, nuxtOptions: config }) const stubPath = resolve(config.rootDir, 'stubs', `${templateName}${extname(res.path)}`) + let contents = `${res.contents.trim()}\n` try { - res.contents = await fsp.readFile(stubPath, 'utf8') + contents = await fsp.readFile(stubPath, 'utf8') } catch (error) { if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) { @@ -118,7 +119,7 @@ export default defineCommand({ } try { - await fsp.writeFile(res.path, `${res.contents.trim()}\n`, { flag: ctx.args.force ? 'w' : 'wx' }) + await fsp.writeFile(res.path, contents, { flag: ctx.args.force ? 'w' : 'wx' }) } catch (error) { if (!ctx.args.force && (error as NodeJS.ErrnoException).code === 'EEXIST') { diff --git a/packages/nuxt-cli/test/unit/commands/add-template.spec.ts b/packages/nuxt-cli/test/unit/commands/add-template.spec.ts index fe5e5a655..03b77e5c2 100644 --- a/packages/nuxt-cli/test/unit/commands/add-template.spec.ts +++ b/packages/nuxt-cli/test/unit/commands/add-template.spec.ts @@ -33,13 +33,22 @@ describe('add-template command', () => { expect(await readFile(path, 'utf8')).toMatch(/\n$/) }) - it('uses a project stub when one exists for the template', async () => { + it('preserves project stub contents exactly', async () => { await mkdir(join(cwd, 'stubs')) - await writeFile(join(cwd, 'stubs/component.vue'), '\n') + await writeFile(join(cwd, 'stubs/component.vue'), ' \n\n') await run('component', 'user-card') - await expect(readFile(join(cwd, 'components/user-card.vue'), 'utf8')).resolves.toBe('\n') + await expect(readFile(join(cwd, 'components/user-card.vue'), 'utf8')).resolves.toBe(' \n\n') + }) + + it('preserves an empty project stub', async () => { + await mkdir(join(cwd, 'stubs')) + await writeFile(join(cwd, 'stubs/component.vue'), '') + + await run('component', 'empty-card') + + await expect(readFile(join(cwd, 'components/empty-card.vue'), 'utf8')).resolves.toBe('') }) it('exposes template-specific options', async () => {