v / cmd / tools / vcreate / vcreate.v
336 lines · 310 sloc · 7.69 KB · 8e35f4d9848f7ad35d857a187dddbfd2eca5e19d
Raw
1// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by an MIT license that can be found in the LICENSE file.
3module main
4
5import os
6import cli { Command, Flag }
7
8// Note: this program follows a similar convention as Rust cargo:
9// `init` creates the structure of project in the current directory,
10// `new` creates the structure of a project in a sub directory.
11
12struct Create {
13mut:
14 name string
15 description string
16 version string
17 license string
18 files []ProjectFiles
19 new_dir bool
20 template Template
21}
22
23struct ProjectFiles {
24 path string
25 content string
26}
27
28enum Template {
29 bin
30 lib
31 web
32}
33
34fn main() {
35 flags := [
36 Flag{
37 flag: .bool
38 name: 'bin'
39 description: 'Use the template for an executable application [default].'
40 },
41 Flag{
42 flag: .bool
43 name: 'lib'
44 description: 'Use the template for a library project.'
45 },
46 Flag{
47 flag: .bool
48 name: 'web'
49 description: 'Use the template for a veb project.'
50 },
51 ]
52 mut cmd := Command{
53 flags: [
54 Flag{
55 flag: .bool
56 name: 'help'
57 description: 'Print help information.'
58 global: true
59 },
60 ]
61 posix_mode: true
62 commands: [
63 Command{
64 name: 'new'
65 usage: '<project_name>'
66 description: [
67 'Creates a new V project in a directory with the specified project name.',
68 '',
69 'A setup prompt is started to create a `v.mod` file with the projects metadata.',
70 'The <project_name> argument can be omitted and entered in the prompts dialog.',
71 'If git is installed, `git init` will be performed during the setup.',
72 ].join_lines()
73 parent: &Command{
74 name: 'v'
75 }
76 posix_mode: true
77 flags: flags
78 pre_execute: validate
79 execute: new_project
80 },
81 Command{
82 name: 'init'
83 description: [
84 'Sets up a V project within the current directory.',
85 '',
86 "If no `v.mod` exists, a setup prompt is started to create one with the project's metadata.",
87 'If no `.v` file exists, a project template is generated. If the current directory is not a',
88 'git project and git is installed, `git init` will be performed during the setup.',
89 ].join_lines()
90 parent: &Command{
91 name: 'v'
92 }
93 posix_mode: true
94 flags: flags
95 pre_execute: validate
96 execute: init_project
97 },
98 ]
99 }
100 cmd.parse(os.args)
101}
102
103fn validate(cmd Command) ! {
104 if cmd.flags.get_bool('help')! {
105 cmd.execute_help()
106 exit(0)
107 }
108 if cmd.args.len > 1 {
109 cerror('too many arguments.\n')
110 cmd.execute_help()
111 exit(2)
112 }
113}
114
115fn new_project(cmd Command) ! {
116 mut c := Create{
117 template: get_template(cmd)
118 new_dir: true
119 }
120 c.prompt(cmd.args)
121 println('Initialising ...')
122 // Generate project files based on `Create.files`.
123 c.create_files_and_directories()
124 c.write_vmod()
125 c.write_gitattributes()
126 c.write_editorconfig()
127 c.create_git_repo(c.name)
128}
129
130fn init_project(cmd Command) ! {
131 mut c := Create{
132 template: get_template(cmd)
133 }
134 dir_name := check_name(os.file_name(os.getwd()))
135 if !os.exists('v.mod') {
136 mod_dir_has_hyphens := dir_name.contains('-')
137 c.name = if mod_dir_has_hyphens { dir_name.replace('-', '_') } else { dir_name }
138 c.prompt(cmd.args)
139 c.write_vmod()
140 if mod_dir_has_hyphens {
141 println('The directory name `${dir_name}` is invalid as a module name. The module name in `v.mod` was set to `${c.name}`')
142 }
143 }
144 println('Initialising ...')
145 c.create_files_and_directories()
146 c.write_gitattributes()
147 c.write_editorconfig()
148 c.create_git_repo('.')
149}
150
151fn (mut c Create) prompt(args []string) {
152 if c.name == '' {
153 c.name = check_name(args[0] or { os.input('Input your project name: ') })
154 if c.name == '' {
155 eprintln('')
156 cerror('project name cannot be empty')
157 exit(1)
158 }
159 if c.name.contains('-') {
160 eprintln('')
161 cerror('`${c.name}` should not contain hyphens')
162 exit(1)
163 }
164 if os.is_dir(c.name) {
165 eprintln('')
166 cerror('`${c.name}` folder already exists')
167 exit(3)
168 }
169 }
170 c.description = os.input('Input your project description: ')
171 default_version := '0.0.0'
172 c.version = os.input('Input your project version: (${default_version}) ')
173 if c.version == '' {
174 c.version = default_version
175 }
176 default_license := 'MIT'
177 c.license = os.input('Input your project license: (${default_license}) ')
178 if c.license == '' {
179 c.license = default_license
180 }
181}
182
183fn get_template(cmd Command) Template {
184 bin := cmd.flags.get_bool('bin') or { false }
185 lib := cmd.flags.get_bool('lib') or { false }
186 web := cmd.flags.get_bool('web') or { false }
187 if (bin && lib) || (bin && web) || (lib && web) {
188 eprintln("error: can't use more then one template")
189 exit(2)
190 }
191 return match true {
192 lib { .lib }
193 web { .web }
194 else { .bin }
195 }
196}
197
198fn cerror(e string) {
199 eprintln('error: ${e}.')
200}
201
202fn check_name(name string) string {
203 if name.trim_space().len == 0 {
204 eprintln('')
205 cerror('project name cannot be empty')
206 exit(1)
207 }
208 if name.is_title() {
209 mut cname := name.to_lower()
210 if cname.contains(' ') {
211 cname = cname.replace(' ', '_')
212 }
213 eprintln('warning: the project name cannot be capitalized, the name will be changed to `${cname}`')
214 return cname
215 }
216 if name.contains(' ') {
217 cname := name.replace(' ', '_')
218 eprintln('warning: the project name cannot contain spaces, the name will be changed to `${cname}`')
219 return cname
220 }
221 return name
222}
223
224fn (c &Create) write_vmod() {
225 path := if c.new_dir { '${c.name}/v.mod' } else { 'v.mod' }
226 content := "Module {
227 name: '${c.name}'
228 description: '${c.description}'
229 version: '${c.version}'
230 license: '${c.license}'
231 dependencies: []
232}
233"
234 os.write_file(path, content) or { panic(err) }
235}
236
237fn (c &Create) write_gitattributes() {
238 path := if c.new_dir { '${c.name}/.gitattributes' } else { '.gitattributes' }
239 if !c.new_dir && os.exists(path) {
240 return
241 }
242 content := '* text=auto eol=lf
243*.bat eol=crlf
244
245*.v linguist-language=V
246*.vv linguist-language=V
247*.vsh linguist-language=V
248v.mod linguist-language=V
249.vdocignore linguist-language=ignore
250'
251 os.write_file(path, content) or { panic(err) }
252}
253
254fn (c &Create) write_editorconfig() {
255 path := if c.new_dir { '${c.name}/.editorconfig' } else { '.editorconfig' }
256 if !c.new_dir && os.exists(path) {
257 return
258 }
259 content := '[*]
260charset = utf-8
261end_of_line = lf
262insert_final_newline = true
263trim_trailing_whitespace = true
264
265[*.v]
266indent_style = tab
267'
268 os.write_file(path, content) or { panic(err) }
269}
270
271fn (c &Create) create_git_repo(dir string) {
272 // Initialize git and add a .gitignore file.
273 if !os.is_dir('${dir}/.git') {
274 res := os.execute('git init ${dir}')
275 if res.exit_code != 0 {
276 eprintln('')
277 cerror('unable to initialize a git repository')
278 exit(4)
279 }
280 }
281 ignore_path := '${dir}/.gitignore'
282 if os.exists(ignore_path) {
283 return
284 }
285 ignore_content := '# Binaries for programs and plugins
286main
287${c.name}
288*.exe
289*.exe~
290*.so
291*.dylib
292*.dll
293
294# Ignore binary output folders
295bin/
296
297# Ignore common editor/system specific metadata
298.DS_Store
299.idea/
300.vscode/
301*.iml
302
303# ENV
304.env
305
306# Web assets and local databases
307*.db
308*.js
309
310# Ignore installed modules through `v install --local`:
311modules/
312'
313 os.write_file(ignore_path, ignore_content) or {}
314}
315
316fn (mut c Create) create_files_and_directories() {
317 // Set project template files for `v new` or when no `.v` files exists during `v init`.
318 if c.new_dir || os.walk_ext('.', '.v').len == 0 {
319 match c.template {
320 .bin { c.set_bin_project_files() }
321 .lib { c.set_lib_project_files() }
322 .web { c.set_web_project_files() }
323 }
324 }
325 for file in c.files {
326 os.mkdir_all(os.dir(file.path)) or { panic(err) }
327 os.write_file(file.path, file.content) or { panic(err) }
328 }
329 kind := match c.template {
330 .bin { 'binary (application)' }
331 .lib { 'library' }
332 .web { 'web' }
333 }
334
335 println('Created ${kind} project `${c.name}`')
336}
337