tools.vcreate: rework cli arg handling to extend scaffolding and fix issues (#19889)

This commit is contained in:
Turiiya 2023-11-16 14:40:23 +01:00 committed by GitHub
parent 9308bcd48a
commit e9258c2a08
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 237 additions and 139 deletions

View File

@ -3,9 +3,9 @@ module main
import os
fn (mut c Create) set_bin_project_files() {
main_path := os.join_path('src', 'main.v')
base := if c.new_dir { c.name } else { '' }
c.files << ProjectFiles{
path: if c.new_dir { os.join_path(c.name, main_path) } else { main_path }
path: os.join_path(base, 'src', 'main.v')
content: "module main
fn main() {

View File

@ -3,8 +3,9 @@ module main
import os
fn (mut c Create) set_lib_project_files() {
base := if c.new_dir { c.name } else { '' }
c.files << ProjectFiles{
path: os.join_path(c.name, 'src', c.name + '.v')
path: os.join_path(base, 'src', c.name + '.v')
content: 'module ${c.name}
// square calculates the second power of `x`
@ -14,7 +15,7 @@ pub fn square(x int) int {
'
}
c.files << ProjectFiles{
path: os.join_path(c.name, 'tests', 'square_test.v')
path: os.join_path(base, 'tests', 'square_test.v')
content: 'import ${c.name}
fn test_square() {

View File

@ -3,8 +3,9 @@ module main
import os { join_path }
fn (mut c Create) set_web_project_files() {
base := if c.new_dir { c.name } else { '' }
c.files << ProjectFiles{
path: join_path(c.name, 'src', 'databases', 'config_databases_sqlite.v')
path: join_path(base, 'src', 'databases', 'config_databases_sqlite.v')
content: "module databases
import db.sqlite // can change to 'db.mysql', 'db.pg'
@ -16,7 +17,7 @@ pub fn create_db_connection() !sqlite.DB {
"
}
c.files << ProjectFiles{
path: join_path(c.name, 'src', 'templates', 'header_component.html')
path: join_path(base, 'src', 'templates', 'header_component.html')
content: "<nav>
<div class='nav-wrapper'>
<a href='javascript:window.history.back();' class='left'>
@ -35,7 +36,7 @@ pub fn create_db_connection() !sqlite.DB {
"
}
c.files << ProjectFiles{
path: join_path(c.name, 'src', 'templates', 'products.css')
path: join_path(base, 'src', 'templates', 'products.css')
content: 'h1.title {
font-family: Arial, Helvetica, sans-serif;
color: #3b7bbf;
@ -49,7 +50,7 @@ div.products-table {
}'
}
c.files << ProjectFiles{
path: join_path(c.name, 'src', 'templates', 'products.html')
path: join_path(base, 'src', 'templates', 'products.html')
content: "<!DOCTYPE html>
<html>
<head>
@ -146,7 +147,7 @@ div.products-table {
</html>"
}
c.files << ProjectFiles{
path: join_path(c.name, 'src', 'auth_controllers.v')
path: join_path(base, 'src', 'auth_controllers.v')
content: "module main
import vweb
@ -163,7 +164,7 @@ pub fn (mut app App) controller_auth(username string, password string) vweb.Resu
"
}
c.files << ProjectFiles{
path: join_path(c.name, 'src', 'auth_dto.v')
path: join_path(base, 'src', 'auth_dto.v')
content: 'module main
struct AuthRequestDto {
@ -173,7 +174,7 @@ struct AuthRequestDto {
'
}
c.files << ProjectFiles{
path: join_path(c.name, 'src', 'auth_services.v')
path: join_path(base, 'src', 'auth_services.v')
content: "module main
import crypto.hmac
@ -268,7 +269,7 @@ fn auth_verify(token string) bool {
"
}
c.files << ProjectFiles{
path: join_path(c.name, 'src', 'index.html')
path: join_path(base, 'src', 'index.html')
content: "<!DOCTYPE html>
<html>
<head>
@ -347,7 +348,7 @@ fn auth_verify(token string) bool {
"
}
c.files << ProjectFiles{
path: join_path(c.name, 'src', 'main.v')
path: join_path(base, 'src', 'main.v')
content: "module main
import vweb
@ -392,7 +393,7 @@ pub fn (mut app App) index() vweb.Result {
"
}
c.files << ProjectFiles{
path: join_path(c.name, 'src', 'product_controller.v')
path: join_path(base, 'src', 'product_controller.v')
content: "module main
import vweb
@ -458,7 +459,7 @@ pub fn (mut app App) controller_create_product(product_name string) vweb.Result
"
}
c.files << ProjectFiles{
path: join_path(c.name, 'src', 'product_entities.v')
path: join_path(base, 'src', 'product_entities.v')
content: "module main
@[table: 'products']
@ -471,7 +472,7 @@ struct Product {
"
}
c.files << ProjectFiles{
path: join_path(c.name, 'src', 'product_service.v')
path: join_path(base, 'src', 'product_service.v')
content: "module main
import databases
@ -518,7 +519,7 @@ fn (mut app App) service_get_all_products_from(user_id int) ![]Product {
"
}
c.files << ProjectFiles{
path: join_path(c.name, 'src', 'product_view_api.v')
path: join_path(base, 'src', 'product_view_api.v')
content: "module main
import json
@ -557,7 +558,7 @@ pub fn get_product(token string) ![]User {
"
}
c.files << ProjectFiles{
path: join_path(c.name, 'src', 'product_view.v')
path: join_path(base, 'src', 'product_view.v')
content: "module main
import vweb
@ -579,7 +580,7 @@ pub fn (mut app App) products() !vweb.Result {
"
}
c.files << ProjectFiles{
path: join_path(c.name, 'src', 'user_controllers.v')
path: join_path(base, 'src', 'user_controllers.v')
content: "module main
import vweb
@ -649,7 +650,7 @@ pub fn (mut app App) controller_create_user(username string, password string) vw
"
}
c.files << ProjectFiles{
path: join_path(c.name, 'src', 'user_entities.v')
path: join_path(base, 'src', 'user_entities.v')
content: "module main
@[table: 'users']
@ -664,7 +665,7 @@ mut:
"
}
c.files << ProjectFiles{
path: join_path(c.name, 'src', 'user_services.v')
path: join_path(base, 'src', 'user_services.v')
content: "module main
import crypto.bcrypt
@ -733,7 +734,7 @@ fn (mut app App) service_get_user(id int) !User {
"
}
c.files << ProjectFiles{
path: join_path(c.name, 'src', 'user_view_api.v')
path: join_path(base, 'src', 'user_view_api.v')
content: "module main
import json

View File

@ -10,6 +10,6 @@ spawn $v_root/v init
expect "Input your project description: " { send "\r" } timeout { exit 1 }
expect "Input your project version: (0.0.0) " { send "\r" } timeout { exit 1 }
expect "Input your project license: (MIT) " { send "\r" } timeout { exit 1 }
expect "Complete!" {} timeout {} timeout { exit 1 }
# The completion message is verified in `vcreate_init_test.v`.
expect eof

View File

@ -13,6 +13,6 @@ expect "Input your project description: " { send "\r" } timeout { exit 1 }
expect "Input your project version: (0.0.0) " { send "\r" } timeout { exit 1 }
expect "Input your project license: (MIT) " { send "\r" } timeout { exit 1 }
expect "The directory name `$project_dir_name` is invalid as a module name. The module name in `v.mod` was set to `$corrected_mod_name`" {} timeout { exit 1 }
expect "Complete!" {} timeout {} timeout { exit 1 }
expect "Created binary (application) project `$corrected_mod_name`" {} timeout {} timeout { exit 1 }
expect eof

View File

@ -0,0 +1,17 @@
#!/usr/bin/env expect
set timeout 3
# Pass v_root as arg, since we chdir into a temp directory during testing and create a project there.
set v_root [lindex $argv 0]
set model [lindex $argv 1]
spawn $v_root/v init $model
expect "Input your project description: " { send "My Awesome V Application.\r" } timeout { exit 1 }
expect "Input your project version: (0.0.0) " { send "0.0.1\r" } timeout { exit 1 }
expect "Input your project license: (MIT) " { send "\r" } timeout { exit 1 }
expect "Initialising ..." {} timeout { exit 1 }
# The completion message is verified in `vcreate_init_test.v`.
expect eof

View File

@ -4,15 +4,15 @@ set timeout 3
# Pass v_root as arg, since we chdir into a temp directory during testing and create a project there.
set v_root [lindex $argv 0]
set project_name [lindex $argv 1]
set model [lindex $argv 2]
set model [lindex $argv 1]
set project_name [lindex $argv 2]
spawn $v_root/v new $project_name $model
spawn $v_root/v new $model $project_name
expect "Input your project description: " { send "My Awesome V Project.\r" } timeout { exit 1 }
expect "Input your project version: (0.0.0) " { send "0.0.1\r" } timeout { exit 1 }
expect "Input your project license: (MIT) " { send "\r" } timeout { exit 1 }
expect "Initialising ..." {} timeout { exit 1 }
expect "Complete!" {} timeout { exit 1 }
expect "Created library project `$project_name`" {} timeout { exit 1 }
expect eof

View File

@ -12,6 +12,6 @@ expect "Input your project description: " { send "\r" } timeout { exit 1 }
expect "Input your project version: (0.0.0) " { send "\r" } timeout { exit 1 }
expect "Input your project license: (MIT) " { send "\r" } timeout { exit 1 }
expect "Initialising ..." {} timeout { exit 1 }
expect "Complete!" {} timeout { exit 1 }
expect "Created binary (application) project `$project_name`" {} timeout { exit 1 }
expect eof

View File

@ -13,6 +13,6 @@ expect "Input your project description: " { send "My Awesome V Project.\r" } tim
expect "Input your project version: (0.0.0) " { send "0.1.0\r" } timeout { exit 1 }
expect "Input your project license: (MIT) " { send "GPL\r" } timeout { exit 1 }
expect "Initialising ..." {} timeout { exit 1 }
expect "Complete!" {} timeout { exit 1 }
expect "Created binary (application) project `$project_name`" {} timeout { exit 1 }
expect eof

View File

@ -3,18 +3,11 @@
module main
import os
import cli { Command, Flag }
// Note: this program follows a similar convention to Rust: `init` makes the
// structure of the program in the _current_ directory, while `new`
// makes the program structure in a _sub_ directory. Besides that, the
// functionality is essentially the same.
// Note: here are the currently supported invocations so far:
// - `v init` -> initialize a new project in the current folder
// - `v new` -> create a new project in the directory specified during setup, using the "bin" template by default.
// - `v new my_bin_project bin` -> create a new project directory `my_bin_project`, using the bin template.
// - `v new my_lib_project lib` -> create a new project directory `my_lib_project`, using the lib template.
// - `v new my_web_project web` -> create a new project directory `my_web_project`, using the vweb template.
// Note: this program follows a similar convention as Rust cargo:
// `init` creates the structure of project in the current directory,
// `new` creates the structure of a project in a sub directory.
struct Create {
mut:
@ -24,6 +17,7 @@ mut:
license string
files []ProjectFiles
new_dir bool
template Template
}
struct ProjectFiles {
@ -31,89 +25,125 @@ struct ProjectFiles {
content string
}
fn main() {
cmd := os.args[1]
match cmd {
'new' {
// list of models allowed
project_models := ['bin', 'lib', 'web']
if os.args.len == 4 {
// validation
if os.args.last() !in project_models {
mut error_str := 'It is not possible create a "${os.args[os.args.len - 2]}" project.\n'
error_str += 'See the list of allowed projects:\n'
for model in project_models {
error_str += 'v new ${os.args[os.args.len - 2]} ${model}\n'
}
eprintln(error_str)
exit(1)
}
}
new_project(os.args[2..])
}
'init' {
init_project(os.args[2..])
}
else {
cerror('unknown command: ${cmd}')
exit(1)
}
}
println('Complete!')
enum Template {
bin
lib
web
}
fn new_project(args []string) {
mut c := Create{}
c.new_dir = true
c.prompt(args)
println('Initialising ...')
if args.len == 2 {
// E.g.: `v new my_project lib`
match os.args.last() {
'bin' {
c.set_bin_project_files()
}
'lib' {
c.set_lib_project_files()
}
'web' {
c.set_web_project_files()
}
else {
eprintln('${os.args.last()} model not exist')
exit(1)
}
}
} else {
// E.g.: `v new my_project`
c.set_bin_project_files()
fn main() {
flags := [
Flag{
flag: .bool
name: 'bin'
description: 'Use the template for an executable application [default].'
},
Flag{
flag: .bool
name: 'lib'
description: 'Use the template for a library project.'
},
Flag{
flag: .bool
name: 'web'
description: 'Use the template for a vweb project.'
},
]
mut cmd := Command{
flags: [
Flag{
flag: .bool
name: 'help'
description: 'Print help information.'
global: true
},
]
posix_mode: true
commands: [
Command{
name: 'new'
usage: '<project_name>'
description: [
'Creates a new V project in a directory with the specified project name.',
'',
'A setup prompt is started to create a `v.mod` file with the projects metadata.',
'The <project_name> argument can be omitted and entered in the prompts dialog.',
'If git is installed, `git init` will be performed during the setup.',
].join_lines()
parent: &Command{
name: 'v'
}
posix_mode: true
disable_man: true
flags: flags
pre_execute: validate
execute: new_project
},
Command{
name: 'init'
description: [
'Sets up a V project within the current directory.',
'',
"If no `v.mod` exists, a setup prompt is started to create one with the project's metadata.",
'If no `.v` file exists, a project template is generated. If the current directory is not a',
'git project and git is installed, `git init` will be performed during the setup.',
].join_lines()
parent: &Command{
name: 'v'
}
posix_mode: true
disable_man: true
flags: flags
pre_execute: validate
execute: init_project
},
]
}
cmd.parse(os.args)
}
// gen project based in the `Create.files` info
fn validate(cmd Command) ! {
if cmd.flags.get_bool('help')! {
cmd.execute_help()
exit(0)
}
if cmd.args.len > 1 {
cerror('too many arguments.\n')
cmd.execute_help()
exit(2)
}
}
fn new_project(cmd Command) ! {
mut c := Create{
template: get_template(cmd)
new_dir: true
}
c.prompt(cmd.args)
println('Initialising ...')
// Generate project files based on `Create.files`.
c.create_files_and_directories()
c.write_vmod()
c.write_gitattributes()
c.write_editorconfig()
c.create_git_repo(c.name)
}
fn init_project(args []string) {
mut c := Create{}
fn init_project(cmd Command) ! {
mut c := Create{
template: get_template(cmd)
}
dir_name := check_name(os.file_name(os.getwd()))
if !os.exists('v.mod') {
mod_dir_has_hyphens := dir_name.contains('-')
c.name = if mod_dir_has_hyphens { dir_name.replace('-', '_') } else { dir_name }
c.prompt(args)
c.prompt(cmd.args)
c.write_vmod()
if mod_dir_has_hyphens {
println('The directory name `${dir_name}` is invalid as a module name. The module name in `v.mod` was set to `${c.name}`')
}
}
if !os.exists('src/main.v') {
c.set_bin_project_files()
}
println('Initialising ...')
c.create_files_and_directories()
c.write_gitattributes()
c.write_editorconfig()
@ -124,15 +154,18 @@ fn (mut c Create) prompt(args []string) {
if c.name == '' {
c.name = check_name(args[0] or { os.input('Input your project name: ') })
if c.name == '' {
eprintln('')
cerror('project name cannot be empty')
exit(1)
}
if c.name.contains('-') {
cerror('"${c.name}" should not contain hyphens')
eprintln('')
cerror('`${c.name}` should not contain hyphens')
exit(1)
}
if os.is_dir(c.name) {
cerror('${c.name} folder already exists')
eprintln('')
cerror('`${c.name}` folder already exists')
exit(3)
}
}
@ -149,12 +182,28 @@ fn (mut c Create) prompt(args []string) {
}
}
fn get_template(cmd Command) Template {
bin := cmd.flags.get_bool('bin') or { false }
lib := cmd.flags.get_bool('lib') or { false }
web := cmd.flags.get_bool('web') or { false }
if (bin && lib) || (bin && web) || (lib && web) {
eprintln("error: can't use more then one template")
exit(2)
}
return match true {
lib { .lib }
web { .web }
else { .bin }
}
}
fn cerror(e string) {
eprintln('\nerror: ${e}')
eprintln('error: ${e}.')
}
fn check_name(name string) string {
if name.trim_space().len == 0 {
eprintln('')
cerror('project name cannot be empty')
exit(1)
}
@ -221,11 +270,12 @@ indent_style = tab
}
fn (c &Create) create_git_repo(dir string) {
// Create Git Repo and .gitignore file
// Initialize git and add a .gitignore file.
if !os.is_dir('${dir}/.git') {
res := os.execute('git init ${dir}')
if res.exit_code != 0 {
cerror('Unable to create git repo')
eprintln('')
cerror('unable to initialize a git repository')
exit(4)
}
}
@ -262,8 +312,22 @@ bin/
}
fn (mut c Create) create_files_and_directories() {
// Set project template files for `v new` or when no `.v` files exists during `v init`.
if c.new_dir || os.walk_ext('.', '.v').len == 0 {
match c.template {
.bin { c.set_bin_project_files() }
.lib { c.set_lib_project_files() }
.web { c.set_web_project_files() }
}
}
for file in c.files {
os.mkdir_all(os.dir(file.path)) or { panic(err) }
os.write_file(file.path, file.content) or { panic(err) }
}
kind := match c.template {
.bin { 'binary (application)' }
.lib { 'library' }
.web { 'web' }
}
println('Created ${kind} project `${c.name}`')
}

View File

@ -24,11 +24,11 @@ fn testsuite_end() {
fn init_and_check() ! {
os.chdir(test_path)!
// Keep track of the last modified time of the main file to ensure it is not modifed if it already exists.
// Keep track of the last modified time of the main file to ensure it is not modified if it already exists.
main_exists := os.exists('src/main.v')
main_last_modified := if main_exists { os.file_last_mod_unix('src/main.v') } else { 0 }
// Initilize project.
// Initialize project.
os.execute_or_exit('${expect_exe} ${os.join_path(expect_tests_path, 'init.expect')} ${vroot}')
x := os.execute_or_exit('${vexe} run .')
@ -147,13 +147,13 @@ indent_style = tab
prepare_test_path()!
os.write_file('.gitattributes', git_attributes_content)!
os.write_file('.editorconfig', editor_config_content)!
os.execute_or_exit('${expect_exe} ${os.join_path(expect_tests_path, 'init.expect')} ${vroot}')
res := os.execute_or_exit('${expect_exe} ${os.join_path(expect_tests_path, 'init.expect')} ${vroot}')
assert res.output.contains('Created binary (application) project `${test_project_dir_name}`')
assert os.read_file('.gitattributes')! == git_attributes_content
assert os.read_file('.editorconfig')! == editor_config_content
}
fn test_v_init_in_dir_with_invalid_mod_name() {
fn test_v_init_in_dir_with_invalid_mod_name_input() {
// A project with a directory name with hyphens, which is invalid for a module name.
dir_name_with_invalid_mod_name := 'my-proj'
corrected_mod_name := 'my_proj'
@ -168,3 +168,21 @@ fn test_v_init_in_dir_with_invalid_mod_name() {
}
assert mod.name == corrected_mod_name
}
fn test_v_init_with_model_arg_input() {
prepare_test_path()!
model := '--lib'
res := os.execute_or_exit('${expect_exe} ${os.join_path(expect_tests_path, 'init_with_model_arg.expect')} ${vroot} ${model}')
assert res.output.contains('Created library project `${test_project_dir_name}`'), res.output
project_path := os.join_path(test_path)
mod := vmod.from_file(os.join_path(project_path, 'v.mod')) or {
assert false, err.str()
return
}
assert mod.name == test_project_dir_name
assert mod.description == 'My Awesome V Application.'
assert mod.version == '0.0.1'
assert mod.license == 'MIT'
// Assert existence of a model-specific file.
assert os.exists(os.join_path(project_path, 'tests', 'square_test.v'))
}

View File

@ -68,12 +68,13 @@ fn test_new_with_name_arg_input() {
fn test_new_with_model_arg_input() {
prepare_test_path()!
project_name := 'my_lib'
model := 'lib'
os.execute_opt('${expect_exe} ${os.join_path(expect_tests_path, 'new_with_model_arg.expect')} ${vroot} ${project_name} ${model}') or {
model := '--lib'
os.execute_opt('${expect_exe} ${os.join_path(expect_tests_path, 'new_with_model_arg.expect')} ${vroot} ${model} ${project_name}') or {
assert false, err.msg()
}
project_path := os.join_path(test_module_path, project_name)
// Assert mod data set in `new_with_model_arg.expect`.
mod := vmod.from_file(os.join_path(test_module_path, project_name, 'v.mod')) or {
mod := vmod.from_file(os.join_path(project_path, 'v.mod')) or {
assert false, err.str()
return
}
@ -81,4 +82,6 @@ fn test_new_with_model_arg_input() {
assert mod.description == 'My Awesome V Project.'
assert mod.version == '0.0.1'
assert mod.license == 'MIT'
// Assert existence of a model-specific file.
assert os.exists(os.join_path(project_path, 'tests', 'square_test.v'))
}

View File

@ -2,6 +2,9 @@ module help
import os
// Topics whose module uses the cli module.
const cli_topics = ['new', 'init']
fn hdir(base string) string {
return os.join_path(base, 'vlib', 'v', 'help')
}
@ -38,6 +41,10 @@ pub fn print_and_exit(topic string, opts ExitOptions) {
exit(fail_code)
}
}
if topic in help.cli_topics {
os.system('${@VEXE} ${topic} --help')
exit(opts.exit_code)
}
mut topic_path := ''
for path in os.walk_ext(help_dir(), '.txt') {
if topic == os.file_name(path).all_before('.txt') {

View File

@ -49,3 +49,12 @@ fn test_topic_sub_help() {
assert res.exit_code == 0, res.output
assert res.output != ''
}
fn test_help_topic_with_cli_mod() {
res := os.execute_or_exit(vexe + ' help init')
assert res.output.contains('Usage: v init [flags]')
assert res.output.contains('Sets up a V project within the current directory.')
assert res.output.contains('Flags:')
assert res.output.contains('--bin Use the template for an executable application [default]')
assert res.output.contains('--lib Use the template for a library project.')
}

View File

@ -1,9 +0,0 @@
Sets up a V project within the current directory.
Usage:
v init
If no '.v' file exists, then will create a 'main.v' file.
If no 'v.mod' file exists, one will be created.
If the current directory is not already controlled with 'git', will perform
'git init' (if git is installed on the system).

View File

@ -1,13 +0,0 @@
Sets up a new V project
Usage:
v new [NAME] [DESCRIPTION]
Sets up a new V project with a 'v.mod' file, and a 'main.v' "Hello World"
file, and performs 'git init' (if git is installed on the system).
If NAME is given, the project will be setup in a new directory with that
name, and that name will be added to the 'v.mod' file. If no name is given,
the user will be prompted for a name.
If DESCRIPTION is given, the 'v.mod' file is updated with said description.