Skip to content

Commit 50e71e5

Browse files
committed
fix(add-template): expose template options
1 parent 634eeb7 commit 50e71e5

3 files changed

Lines changed: 138 additions & 39 deletions

File tree

‎packages/nuxt-cli/src/commands/add-template.ts‎

Lines changed: 59 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
importtype{TemplateName}from'../utils/templates/names'
22

3-
import{existsSync,promisesasfsp}from'node:fs'
3+
import{promisesasfsp}from'node:fs'
44
importprocessfrom'node:process'
5-
65
import{styleText}from'node:util'
7-
import{cancel,intro,outro}from'@clack/prompts'
6+
7+
import{intro,outro}from'@clack/prompts'
88
import{defineCommand}from'citty'
9-
import{dirname,extname,resolve}from'pathe'
9+
import{dirname,resolve}from'pathe'
1010

1111
import{loadKit}from'../utils/kit'
1212
import{logger}from'../utils/logger'
@@ -25,9 +25,31 @@ export default defineCommand({
2525
...logLevelArgs,
2626
force: {
2727
type: 'boolean',
28-
description: 'Force override file if it already exists',
28+
description: 'Overwrite the file if it already exists',
2929
default: false,
3030
},
31+
mode: {
32+
type: 'string',
33+
valueHint: 'client|server',
34+
description: 'Add a client or server suffix to a component or plugin',
35+
},
36+
method: {
37+
type: 'string',
38+
valueHint: 'connect|delete|get|head|options|patch|post|put|trace',
39+
description: 'Add an HTTP method suffix to an API route',
40+
},
41+
global: {
42+
type: 'boolean',
43+
description: 'Create global route middleware',
44+
},
45+
api: {
46+
type: 'boolean',
47+
description: 'Create a server route in the API directory',
48+
},
49+
pages: {
50+
type: 'boolean',
51+
description: 'Include NuxtPage and NuxtLayout in the app template',
52+
},
3153
template: {
3254
type: 'positional',
3355
required: true,
@@ -47,55 +69,56 @@ export default defineCommand({
4769

4870
consttemplateName=ctx.args.templateasTemplateName
4971

50-
// Validate template name
5172
if(!templateNames.includes(templateName)){
52-
consttemplateNames=Object.keys(templates).map(name=>styleText('cyan',name))
53-
constlastTemplateName=templateNames.pop()
73+
constsupported=templateNames.map(name=>styleText('cyan',name))
74+
constlast=supported.pop()
5475
logger.error(`Template ${styleText('cyan',templateName)} is not supported.`)
55-
logger.info(`Possible values are ${templateNames.join(', ')} or ${lastTemplateName}.`)
76+
logger.info(`Possible values are ${supported.join(', ')} or ${last}.`)
5677
process.exit(1)
5778
}
5879

59-
// Validate options
60-
constext=extname(ctx.args.name)
61-
constname
62-
=ext==='.vue'||ext==='.ts'
63-
? ctx.args.name.replace(ext,'')
64-
: ctx.args.name
65-
66-
if(!name){
67-
cancel('name argument is missing!')
80+
if(ctx.args.mode&&ctx.args.mode!=='client'&&ctx.args.mode!=='server'){
81+
logger.error(`Mode must be ${styleText('cyan','client')} or ${styleText('cyan','server')}.`)
82+
process.exit(1)
83+
}
84+
if(ctx.args.method&&!['connect','delete','get','head','options','patch','post','put','trace'].includes(ctx.args.method)){
85+
logger.error(`HTTP method ${styleText('cyan',ctx.args.method)} is not supported.`)
6886
process.exit(1)
6987
}
7088

71-
// Load config in order to respect srcDir
72-
constkit=awaitloadKit(cwd)
73-
constconfig=awaitkit.loadNuxtConfig({ cwd })
74-
75-
// Resolve template
76-
consttemplate=templates[templateNameaskeyoftypeoftemplates]
77-
78-
constres=template({ name,args: ctx.args,nuxtOptions: config})
89+
constext=['.vue','.ts'].find(ext=>ctx.args.name.endsWith(ext))
90+
constname=ext
91+
? ctx.args.name.slice(0,-ext.length)
92+
: ctx.args.name
7993

80-
// Ensure not overriding user code
81-
if(!ctx.args.force&&existsSync(res.path)){
82-
logger.error(`File exists at ${styleText('cyan',relativeToProcess(res.path))}.`)
83-
logger.info(`Use ${styleText('cyan','--force')} to override or use a different name.`)
94+
if(!name){
95+
logger.error('Template name must not be empty.')
8496
process.exit(1)
8597
}
8698

87-
// Ensure parent directory exists
99+
constkit=awaitloadKit(cwd)
100+
constconfig=awaitkit.loadNuxtConfig({ cwd })
101+
constres=templates[templateName]({ name,args: ctx.args,nuxtOptions: config})
88102
constparentDir=dirname(res.path)
89-
if(!existsSync(parentDir)){
90-
logger.step(`Creating directory ${styleText('cyan',relativeToProcess(parentDir))}.`)
103+
constcreatedDir=awaitfsp.mkdir(parentDir,{recursive: true})
104+
if(createdDir){
105+
logger.step(`Created directory ${styleText('cyan',relativeToProcess(parentDir))}.`)
91106
if(templateName==='page'){
92107
logger.info('This enables vue-router functionality!')
93108
}
94-
awaitfsp.mkdir(parentDir,{recursive: true})
95109
}
96110

97-
// Write file
98-
awaitfsp.writeFile(res.path,`${res.contents.trim()}\n`)
111+
try{
112+
awaitfsp.writeFile(res.path,`${res.contents.trim()}\n`,{flag: ctx.args.force ? 'w' : 'wx'})
113+
}
114+
catch(error){
115+
if(!ctx.args.force&&(errorasNodeJS.ErrnoException).code==='EEXIST'){
116+
logger.error(`File exists at ${styleText('cyan',relativeToProcess(res.path))}.`)
117+
logger.info(`Use ${styleText('cyan','--force')} to overwrite it or use a different name.`)
118+
process.exit(1)
119+
}
120+
throwerror
121+
}
99122
logger.success(`Created ${styleText('cyan',relativeToProcess(res.path))}.`)
100123
outro(`Generated a new ${styleText('cyan',templateName)}!`)
101124
},
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import{mkdtemp,readFile,rm,writeFile}from'node:fs/promises'
2+
import{tmpdir}from'node:os'
3+
import{join}from'node:path'
4+
import{afterEach,beforeEach,describe,expect,it,vi}from'vitest'
5+
6+
importcommandfrom'../../../src/commands/add-template'
7+
import{runCommandDef}from'../../../src/run-command'
8+
9+
letcwd: string
10+
11+
beforeEach(async()=>{
12+
cwd=awaitmkdtemp(join(tmpdir(),'nuxt-add-template-'))
13+
vi.spyOn(process,'exit').mockImplementation((code)=>{
14+
thrownewError(`process exited with code ${code}`)
15+
})
16+
})
17+
18+
afterEach(async()=>{
19+
vi.restoreAllMocks()
20+
awaitrm(cwd,{recursive: true,force: true})
21+
})
22+
23+
asyncfunctionrun(...args: string[]){
24+
returnrunCommandDef(command,[...args,'--cwd',cwd])
25+
}
26+
27+
describe('add-template command',()=>{
28+
it('generates nested templates and strips only the final supported extension',async()=>{
29+
awaitrun('component','admin/user-card.vue')
30+
31+
constpath=join(cwd,'components/admin/user-card.vue')
32+
expect(awaitreadFile(path,'utf8')).toContain('Component: admin/user-card')
33+
expect(awaitreadFile(path,'utf8')).toMatch(/\n$/)
34+
})
35+
36+
it('exposes template-specific options',async()=>{
37+
awaitrun('api','users','--method','get')
38+
awaitrun('component','island','--mode','client')
39+
awaitrun('middleware','auth','--global')
40+
awaitrun('server-route','health','--api')
41+
awaitrun('app','ignored','--pages')
42+
43+
awaitexpect(readFile(join(cwd,'server/api/users.get.ts'),'utf8')).resolves.toContain('return \'Hello users\'')
44+
awaitexpect(readFile(join(cwd,'components/island.client.vue'),'utf8')).resolves.toContain('Component: island')
45+
awaitexpect(readFile(join(cwd,'middleware/auth.global.ts'),'utf8')).resolves.toContain('defineNuxtRouteMiddleware')
46+
awaitexpect(readFile(join(cwd,'server/api/health.ts'),'utf8')).resolves.toContain('defineEventHandler')
47+
awaitexpect(readFile(join(cwd,'app.vue'),'utf8')).resolves.toContain('<NuxtPage/>')
48+
})
49+
50+
it('rejects unsupported suffix options',async()=>{
51+
awaitexpect(run('component','island','--mode','worker')).rejects.toThrow('process exited with code 1')
52+
awaitexpect(run('api','users','--method','fetch')).rejects.toThrow('process exited with code 1')
53+
})
54+
55+
it('rejects names containing only an extension',async()=>{
56+
awaitexpect(run('component','.vue')).rejects.toThrow('process exited with code 1')
57+
awaitexpect(readFile(join(cwd,'components/.vue'),'utf8')).rejects.toMatchObject({code: 'ENOENT'})
58+
})
59+
60+
it('does not overwrite an existing file without force',async()=>{
61+
awaitrun('composable','counter')
62+
constpath=join(cwd,'composables/counter.ts')
63+
awaitwriteFile(path,'existing\n')
64+
65+
awaitexpect(run('composable','counter')).rejects.toThrow()
66+
awaitexpect(readFile(path,'utf8')).resolves.toBe('existing\n')
67+
68+
awaitrun('composable','counter','--force')
69+
awaitexpect(readFile(path,'utf8')).resolves.toContain('export const useCounter')
70+
})
71+
})

‎packages/nuxt-cli/test/unit/help.spec.ts‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,14 @@ describe('help', () => {
9393
9494
OPTIONS
9595
96-
--cwd=<directory> Specify the root directory of your Nuxt project (Default: .)
97-
--logLevel=<silent|info|verbose> Specify build-time log level
98-
--force Force override file if it already exists (Default: false)
96+
--cwd=<directory> Specify the root directory of your Nuxt project (Default: .)
97+
--logLevel=<silent|info|verbose> Specify build-time log level
98+
--force Overwrite the file if it already exists (Default: false)
99+
--mode=<client|server> Add a client or server suffix to a component or plugin
100+
--method=<connect|delete|get|head|options|patch|post|put|trace> Add an HTTP method suffix to an API route
101+
--global Create global route middleware
102+
--api Create a server route in the API directory
103+
--pages Include NuxtPage and NuxtLayout in the app template
99104
"
100105
`)
101106
})

0 commit comments

Comments
 (0)