Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 34 additions & 16 deletions packages/nuxi/src/commands/add-template.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import type { TemplateName } from '../utils/templates/names'

import { existsSync, promises as fsp } from 'node:fs'
import process from 'node:process'

Expand All @@ -15,6 +13,28 @@ import { templates } from '../utils/templates/index'
import { templateNames } from '../utils/templates/names'
import { cwdArgs, logLevelArgs } from './_shared'

async function loadNuxtConfigWithModules(cwd: string) {
const kit = await loadKit(cwd)
const nuxt = await kit.loadNuxt({ cwd, ready: false }).catch(() => null)

if (!nuxt) {
return { config: await kit.loadNuxtConfig({ cwd }) }
}

try {
await nuxt.ready()
await nuxt.hooks.callHook('templates:extend', templates)
}
catch {
// module setup may fail; fall through with built-in templates only
}
finally {
await nuxt.close()
}

return { config: nuxt.options }
}

export default defineCommand({
meta: {
name: 'add-template',
Expand Down Expand Up @@ -45,16 +65,7 @@ export default defineCommand({

intro(colors.cyan('Adding template...'))

const templateName = ctx.args.template as TemplateName

// Validate template name
if (!templateNames.includes(templateName)) {
const templateNames = Object.keys(templates).map(name => colors.cyan(name))
const lastTemplateName = templateNames.pop()
logger.error(`Template ${colors.cyan(templateName)} is not supported.`)
logger.info(`Possible values are ${templateNames.join(', ')} or ${lastTemplateName}.`)
process.exit(1)
}
const templateName = ctx.args.template

// Validate options
const ext = extname(ctx.args.name)
Expand All @@ -69,12 +80,19 @@ export default defineCommand({
}

// Load config in order to respect srcDir
const kit = await loadKit(cwd)
const config = await kit.loadNuxtConfig({ cwd })
const { config } = await loadNuxtConfigWithModules(cwd)

// Resolve template
const template = templates[templateName as keyof typeof templates]
// Validate template name
const template = templates[templateName]
if (!template) {
const templateNames = Object.keys(templates).map(name => colors.cyan(name))
const lastTemplateName = templateNames.pop()
logger.error(`Template ${colors.cyan(templateName)} is not supported.`)
logger.info(`Possible values are ${templateNames.join(', ')} or ${lastTemplateName}.`)
process.exit(1)
}

// Resolve template
const res = template({ name, args: ctx.args, nuxtOptions: config })

// Ensure not overriding user code
Expand Down
5 changes: 1 addition & 4 deletions packages/nuxi/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { CommandDef } from 'citty'
import type { TemplateName } from './utils/templates/names'

import nodeCrypto from 'node:crypto'
import { builtinModules, createRequire } from 'node:module'
Expand All @@ -17,8 +16,6 @@ import { runCommand } from './run'
import { setupGlobalConsole } from './utils/console'
import { checkEngines } from './utils/engines'
import { debug, logger } from './utils/logger'
import { templateNames } from './utils/templates/names'

// globalThis.crypto support for Node.js 18
if (!globalThis.crypto) {
globalThis.crypto = nodeCrypto.webcrypto as unknown as Crypto
Expand Down Expand Up @@ -67,7 +64,7 @@ const _main = defineCommand({
await backgroundTasks
}

if (command === 'add' && ctx.rawArgs[1] && templateNames.includes(ctx.rawArgs[1] as TemplateName)) {
if (command === 'add' && ctx.rawArgs[1]) {
logger.warn(`${colors.yellow('Deprecated:')} Using ${colors.cyan('nuxt add <template> <name>')} is deprecated.`)
logger.info(`Please use ${colors.cyan('nuxt add-template <template> <name>')} instead.`)
const addTemplate = await import('./commands/add-template').then(m => m.default || m)
Expand Down
21 changes: 21 additions & 0 deletions packages/nuxi/src/types/nuxt-hooks.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// TODO: Remove this file once `templates:extend` is published in `@nuxt/schema`.

import type { HookResult, NuxtOptions } from '@nuxt/schema'

declare module '@nuxt/schema' {
interface NuxtHooks {
/**
* Allows extending nuxi `add-template` code generation templates.
*
* The `templates` object maps template names to generator functions. Modules
* can add new entries or override existing ones by mutating the object.
*/
'templates:extend': (templates: Record<string, (options: {
name: string
args: Record<string, unknown>
nuxtOptions: NuxtOptions
}) => { path: string, contents: string }>) => HookResult
}
}

export {}
10 changes: 5 additions & 5 deletions packages/nuxi/src/utils/templates/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@ import { serverPlugin } from './server-plugin'
import { serverRoute } from './server-route'
import { serverUtil } from './server-util'

interface TemplateOptions {
export interface TemplateOptions {
name: string
args: Record<string, any>
nuxtOptions: NuxtOptions
}

interface Template {
export interface Template {
(options: TemplateOptions): { path: string, contents: string }
}

const templates = {
const templates: Record<string, Template> = {
'api': api,
'app': app,
'app-config': appConfig,
Expand All @@ -43,7 +43,7 @@ const templates = {
'server-plugin': serverPlugin,
'server-route': serverRoute,
'server-util': serverUtil,
} satisfies Record<string, Template>
}

// -- internal utils --

Expand All @@ -69,4 +69,4 @@ function applySuffix(
return suffix
}

export { applySuffix, Template, templates }
export { applySuffix, templates }
60 changes: 57 additions & 3 deletions packages/nuxi/test/unit/templates.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,66 @@
import type { NuxtOptions } from '@nuxt/schema'
import { describe, expect, it } from 'vitest'

import { templates } from '../../src/utils/templates/index'
import { applySuffix, templates } from '../../src/utils/templates/index'

describe('templates', () => {
it('composables', () => {
for (const name of ['useSomeComposable', 'someComposable', 'use-some-composable', 'use-someComposable', 'some-composable']) {
expect(templates.composable({ name, args: {}, nuxtOptions: { srcDir: '/src' } as NuxtOptions }).contents.trim().split('\n')[0]).toBe('export const useSomeComposable = () => {')
expect(templates.composable!({ name, args: {}, nuxtOptions: { srcDir: '/src' } as NuxtOptions }).contents.trim().split('\n')[0]).toBe('export const useSomeComposable = () => {')
}
})
})

describe('templates registry — extensibility', () => {
it('can be extended with new template types', () => {
const key = '__test_new__'
templates[key] = ({ name, nuxtOptions }) => ({
path: `${nuxtOptions.srcDir}/custom/${name}.ts`,
contents: `export const ${name} = 'extended'`,
})
const result = templates[key]!({ name: 'test', args: {}, nuxtOptions: { srcDir: '/src' } as NuxtOptions })
expect(result.path).toBe('/src/custom/test.ts')
expect(result.contents).toContain('extended')
delete templates[key]
})

it('can override existing template types', () => {
const original = templates.composable!
templates.composable = ({ name }) => ({
path: `/custom/${name}.ts`,
contents: 'overridden',
})
expect(templates.composable!({ name: 'x', args: {}, nuxtOptions: { srcDir: '/' } as NuxtOptions }).contents).toBe('overridden')
templates.composable = original
})

it('built-in templates are present and produce expected output', () => {
const names = Object.keys(templates)
expect(names).toContain('api')
expect(names).toContain('app')
expect(names).toContain('component')
expect(names).toContain('page')
expect(names).toContain('plugin')
})
})

describe('applySuffix', () => {
it('appends suffix for truthy args', () => {
const result = applySuffix({ client: true, server: false }, ['client', 'server'])
expect(result).toBe('.client')
})

it('appends multiple suffixes', () => {
const result = applySuffix({ client: true, server: true }, ['client', 'server'])
expect(result).toBe('.client.server')
})

it('appends mode-based suffix with unwrapFrom', () => {
const result = applySuffix({ mode: 'server' }, ['client', 'server'], 'mode')
expect(result).toBe('.server')
})

it('returns empty string when no args match', () => {
const result = applySuffix({ other: true }, ['client', 'server'])
expect(result).toBe('')
})
})
Loading