From 008097824561c886eae0451d886ea7a969ff2653 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Wed, 25 Mar 2026 16:42:22 +0300 Subject: [PATCH] tools: fix v new --web creating vweb projects instead of veb (fixes #24270) --- cmd/tools/vcreate/project_model_web.v | 598 +++--------------- cmd/tools/vcreate/vcreate.v | 19 +- cmd/tools/vcreate/vcreate_init_test.v | 2 +- cmd/tools/vcreate/vcreate_web_test.v | 39 ++ .../vcreate_windows_sqlite_note_test.v | 9 - doc/docs.md | 2 +- 6 files changed, 118 insertions(+), 551 deletions(-) create mode 100644 cmd/tools/vcreate/vcreate_web_test.v delete mode 100644 cmd/tools/vcreate/vcreate_windows_sqlite_note_test.v diff --git a/cmd/tools/vcreate/project_model_web.v b/cmd/tools/vcreate/project_model_web.v index cf122cce4..3ffcaa626 100644 --- a/cmd/tools/vcreate/project_model_web.v +++ b/cmd/tools/vcreate/project_model_web.v @@ -4,565 +4,117 @@ import os fn (mut c Create) set_web_project_files() { base := if c.new_dir { c.name } else { '' } - - // v source code - c.files << ProjectFiles{ - path: os.join_path(base, 'main.v') - content: "module main - -import veb -import db.sqlite -import os - -struct Context { - veb.Context -} - -struct App { - veb.StaticHandler - db sqlite.DB -} - -pub fn (app &App) index(mut ctx Context) veb.Result { - title := 'veb app' - return \$veb.html() -} - -fn main() { - mut db := sqlite.connect(os.resource_abs_path('app.db'))! - sql db { - create table User - create table Product - }! - defer { db.close() or { panic(err) } } - - mut app := &App{ - db: db - } - app.handle_static(os.resource_abs_path('static'), true)! - veb.run[App, Context](mut app, 8082) + path: os.join_path(base, 'assets', 'main.css') + content: "html, body { + font-family: Arial, Helvetica, sans-serif; + background: #f4efe6; + color: #1f2933; + height: 100%; + margin: 0; } -" - } - c.files << ProjectFiles{ - path: os.join_path(base, 'user_model.v') - content: "module main -@[table: 'users'] -pub struct User { -mut: - id int @[primary; sql: serial] - username string @[unique] - password string - active bool - products []Product @[fkey: 'user_id'] +body { + display: grid; + place-items: center; + padding: 24px; } -" - } - c.files << ProjectFiles{ - path: os.join_path(base, 'user_view.v') - content: "module main - -import veb -@['/api/users'; get] -pub fn (app &App) api_get_users(mut ctx Context) veb.Result { - token := ctx.req.header.get_custom('token') or { '' } - if !app.verify_auth(token) { - ctx.res.set_status(.unauthorized) - return ctx.text('Not valid token') - } - response := app.get_users() or { - ctx.res.set_status(.bad_request) - return ctx.text('\${err}') - } - return ctx.json(response) +.card { + max-width: 560px; + padding: 32px; + border: 1px solid #d8ccb8; + border-radius: 16px; + background: #fffaf1; + box-shadow: 0 18px 50px rgba(69, 52, 35, 0.08); } -@['/api/user'; get] -pub fn (app &App) api_get_user(mut ctx Context) veb.Result { - token := ctx.req.header.get_custom('token') or { '' } - user_id := app.user_auth(token) or { - ctx.res.set_status(.unauthorized) - return ctx.text('\${err}') - } - response := app.get_user(user_id) or { - ctx.res.set_status(.bad_request) - return ctx.text('\${err}') - } - return ctx.json(response) +.eyebrow { + margin: 0 0 12px; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; + color: #8b5e34; } -@['/api/user/create'; post] -pub fn (app &App) controller_create_user(mut ctx Context) veb.Result { - username := ctx.form['username'] or { '' } - password := ctx.form['password'] or { '' } - if username == '' { - ctx.res.set_status(.bad_request) - return ctx.text('username cannot be empty') - } - if password == '' { - ctx.res.set_status(.bad_request) - return ctx.text('password cannot be empty') - } - app.add_user(username, password) or { - ctx.res.set_status(.bad_request) - return ctx.text('\${err}') - } - ctx.res.set_status(.created) - return ctx.text('User created successfully') +h1 { + margin: 0 0 12px; + font-size: 40px; + line-height: 1.1; } -" - } - c.files << ProjectFiles{ - path: os.join_path(base, 'user_controller.v') - content: "module main -import crypto.bcrypt - -fn (app &App) add_user(username string, password string) ! { - hashed_password := bcrypt.generate_from_password(password.bytes(), bcrypt.min_cost)! - user_model := User{ - username: username - password: hashed_password - active: true - } - sql app.db { - insert user_model into User - }! +p { + margin: 0; + font-size: 18px; + line-height: 1.6; } -fn (app &App) get_users() ![]User { - return sql app.db { - select from User - }! +.hint { + margin-top: 16px; + font-size: 14px; + color: #52606d; } -fn (app &App) get_user(id int) !User { - results := sql app.db { - select from User where id == id - }! - if results.len == 0 { - return error('no results') - } - return results[0] +code { + font-family: 'Courier New', monospace; } " } c.files << ProjectFiles{ - path: os.join_path(base, 'auth_model.v') - content: "module main - -import time -import crypto.hmac -import encoding.base64 -import crypto.sha256 -import json - -struct JwtHeader { - alg string - typ string -} - -struct JwtPayload { - sub string // (subject) = Entity to whom the token belongs, usually the user ID; - iss string // (issuer) = Token issuer; - exp string // (expiration) = Timestamp of when the token will expire; - iat time.Time // (issued at) = Timestamp of when the token was created; - aud string // (audience) = Token recipient, represents the application that will use it. - name string - roles string - permissions string -} - -fn make_token(user User) string { - secret := 'SECRET_KEY' // os.getenv('SECRET_KEY') - jwt_header := JwtHeader{'HS256', 'JWT'} - jwt_payload := JwtPayload{ - sub: '\${user.id}' - name: '\${user.username}' - iat: time.now() - } - header := base64.url_encode(json.encode(jwt_header).bytes()) - payload := base64.url_encode(json.encode(jwt_payload).bytes()) - signature := base64.url_encode(hmac.new(secret.bytes(), '\${header}.\${payload}'.bytes(), - sha256.sum, sha256.block_size).bytestr().bytes()) - jwt := '\${header}.\${payload}.\${signature}' - return jwt -} + path: os.join_path(base, 'templates', 'index.html') + content: " + + + + + @title + @css '/assets/main.css' + + +
+

veb starter

+

@title

+

@message

+

Try GET /health for a plain-text response.

+
+ + " } c.files << ProjectFiles{ - path: os.join_path(base, 'auth_view.v') + path: os.join_path(base, 'main.v') content: "module main +import os import veb -@['/api/auth'; post] -pub fn (app &App) api_auth(mut ctx Context) veb.Result { - username := ctx.form['username'] or { '' } - password := ctx.form['password'] or { '' } - response := app.do_auth(username, password) or { - ctx.res.set_status(.bad_request) - return ctx.text('error: \${err}') - } - return ctx.json(response) -} -" - } - c.files << ProjectFiles{ - path: os.join_path(base, 'auth_controller.v') - content: "module main - -import crypto.bcrypt -import crypto.hmac -import encoding.base64 -import crypto.sha256 -import json - -fn (app &App) do_auth(username string, password string) !string { - users := sql app.db { - select from User where username == username - }! - if users.len == 0 { - return error('user not found') - } - user := users.first() - if !user.active { - return error('user is not active') - } - bcrypt.compare_hash_and_password(password.bytes(), user.password.bytes()) or { - return error('Failed to auth user, \${err}') - } - return make_token(user) -} - -fn (app &App) verify_auth(token string) bool { - if token == '' { - return false - } - secret := 'SECRET_KEY' // os.getenv('SECRET_KEY') - token_split := token.split('.') - signature_mirror := hmac.new(secret.bytes(), '\${token_split[0]}.\${token_split[1]}'.bytes(), - sha256.sum, sha256.block_size).bytestr().bytes() - signature_from_token := base64.url_decode(token_split[2]) - return hmac.equal(signature_from_token, signature_mirror) +pub struct Context { + veb.Context } -fn (app &App) user_auth(token string) !int { - if !app.verify_auth(token) { - return error('Invalid token') - } - jwt_payload_stringify := base64.url_decode_str(token.split('.')[1]) - jwt_payload := json.decode(JwtPayload, jwt_payload_stringify) or { return err } - return jwt_payload.sub.int() +pub struct App { + veb.StaticHandler } -" - } - c.files << ProjectFiles{ - path: os.join_path(base, 'product_model.v') - content: "module main -@[table: 'products'] -struct Product { - id int @[primary; sql: serial] - user_id int - name string @[sql_type: 'TEXT'] - created_at string @[default: 'CURRENT_TIMESTAMP'] -} -" - } - c.files << ProjectFiles{ - path: os.join_path(base, 'product_view.v') - content: "module main - -import veb - -@['/products'; get] -pub fn (app &App) products(mut ctx Context) veb.Result { - token := ctx.get_cookie('token') or { - ctx.res.set_status(.bad_request) - return ctx.text('Cookie not found') - } - user_id := app.user_auth(token) or { - ctx.res.set_status(.unauthorized) - return ctx.text('\${err}') - } - user := app.get_user(user_id) or { - ctx.res.set_status(.bad_request) - return ctx.text('\${err}') - } +pub fn (app &App) index() veb.Result { + title := '${c.name}' + message := 'Your new V web app is powered by veb.' return \$veb.html() } -@['/api/products'; get] -pub fn (app &App) api_get_products(mut ctx Context) veb.Result { - token := ctx.req.header.get_custom('token') or { '' } - user_id := app.user_auth(token) or { - ctx.res.set_status(.unauthorized) - return ctx.text('\${err}') - } - response := app.get_user_products(user_id) or { - ctx.res.set_status(.bad_request) - return ctx.text('\${err}') - } - return ctx.json(response) -} - -@['/api/product/create'; post] -pub fn (app &App) api_create_product(mut ctx Context) veb.Result { - product_name := ctx.form['product_name'] or { '' } - if product_name == '' { - ctx.res.set_status(.bad_request) - return ctx.text('product name cannot be empty') - } - token := ctx.req.header.get_custom('token') or { '' } - user_id := app.user_auth(token) or { - ctx.res.set_status(.unauthorized) - return ctx.text('\${err}') - } - app.add_product(product_name, user_id) or { - ctx.res.set_status(.bad_request) - return ctx.text('\${err}') - } - ctx.res.set_status(.created) - return ctx.text('product created successfully') -} -" - } - c.files << ProjectFiles{ - path: os.join_path(base, 'product_controller.v') - content: 'module main - -fn (app &App) add_product(product_name string, user_id int) ! { - product_model := Product{ - name: product_name - user_id: user_id - } - sql app.db { - insert product_model into Product - }! +@['/health'; get] +pub fn (app &App) health(mut ctx Context) veb.Result { + return ctx.text('ok') } -fn (app &App) get_user_products(user_id int) ![]Product { - return sql app.db { - select from Product where user_id == user_id - }! +fn main() { + // Keep asset and template paths stable for `v run .`. + os.chdir(os.dir(@FILE))! + mut app := &App{} + app.handle_static('assets', false)! + veb.run[App, Context](mut app, 8080) } -' - } - - // html content - - c.files << ProjectFiles{ - path: os.join_path(base, 'templates', 'index.html') - content: " - - - - - - - - - - - \${title} - - -
@include 'layout/header.html'
-
-
-
- - -
-
- - -
-
-
- - - " } - c.files << ProjectFiles{ - path: os.join_path(base, 'templates', 'products.html') - content: " - - - - - - - - - - - - - - - @css '/css/products.css' - - Login - - -
@include 'layout/header.html'
- -
-
-
- - -
-
- -
-
- -
- -
- - - - - - - - - - @for product in user.products - - - - - - @end - -
IDNameCreated date
\${product.id}\${product.name}\${product.created_at}
-
- - -" - } - c.files << ProjectFiles{ - path: os.join_path(base, 'templates', 'layout', 'header.html') - content: " -" - } - c.files << ProjectFiles{ - path: os.join_path(base, 'static', 'css', 'products.css') - content: 'h1.title { - font-family: Arial, Helvetica, sans-serif; - color: #3b7bbf; -} - -div.products-table { - border: 1px solid; - max-width: 720px; - padding: 10px; - margin: 10px; -} -' - } } diff --git a/cmd/tools/vcreate/vcreate.v b/cmd/tools/vcreate/vcreate.v index 6f1ec7be3..d27421073 100644 --- a/cmd/tools/vcreate/vcreate.v +++ b/cmd/tools/vcreate/vcreate.v @@ -46,7 +46,7 @@ fn main() { Flag{ flag: .bool name: 'web' - description: 'Use the template for a vweb project.' + description: 'Use the template for a veb project.' }, ] mut cmd := Command{ @@ -303,7 +303,7 @@ bin/ # ENV .env -# vweb and database +# Web assets and local databases *.db *.js @@ -332,19 +332,4 @@ fn (mut c Create) create_files_and_directories() { .web { 'web' } } println('Created ${kind} project `${c.name}`') - c.print_web_template_notes() -} - -fn (c &Create) print_web_template_notes() { - sqlite_header_path := os.join_path(@VEXEROOT, 'thirdparty', 'sqlite', 'sqlite3.h') - if !should_print_windows_web_sqlite_note(c.template, os.user_os(), os.exists(sqlite_header_path)) { - return - } - println('Note: this web template uses `db.sqlite`.') - println('On Windows, run `${os.quoted_path(@VEXE)} vlib/db/sqlite/install_thirdparty_sqlite.vsh`') - println('once, then run your app again.') -} - -fn should_print_windows_web_sqlite_note(template Template, user_os string, has_sqlite_header bool) bool { - return template == .web && user_os == 'windows' && !has_sqlite_header } diff --git a/cmd/tools/vcreate/vcreate_init_test.v b/cmd/tools/vcreate/vcreate_init_test.v index a1276cc12..663a19fcf 100644 --- a/cmd/tools/vcreate/vcreate_init_test.v +++ b/cmd/tools/vcreate/vcreate_init_test.v @@ -78,7 +78,7 @@ fn init_and_check() ! { '# ENV', '.env', '', - '# vweb and database', + '# Web assets and local databases', '*.db', '*.js', '', diff --git a/cmd/tools/vcreate/vcreate_web_test.v b/cmd/tools/vcreate/vcreate_web_test.v new file mode 100644 index 000000000..7c40e9266 --- /dev/null +++ b/cmd/tools/vcreate/vcreate_web_test.v @@ -0,0 +1,39 @@ +module main + +import os + +const web_test_path = os.join_path(os.vtmp_dir(), 'test_vcreate_web') + +fn test_web_template_uses_veb() { + os.rmdir_all(web_test_path) or {} + defer { + os.rmdir_all(web_test_path) or {} + } + os.mkdir_all(web_test_path)! + old_wd := os.getwd() + defer { + os.chdir(old_wd) or {} + } + os.chdir(web_test_path)! + project_name := 'my_web_project' + project_path := os.join_path(web_test_path, project_name) + mut c := Create{ + name: project_name + description: 'My Awesome V Web Project.' + version: '0.1.0' + license: 'MIT' + new_dir: true + template: .web + } + c.create_files_and_directories() + c.write_vmod() + main_v := os.read_file(os.join_path(project_path, 'main.v'))! + assert main_v.contains('import veb') + assert !main_v.contains('import vweb') + assert main_v.contains('\$veb.html()') + template_html := os.read_file(os.join_path(project_path, 'templates', 'index.html'))! + assert template_html.contains('veb starter') + os.chdir(project_path)! + res := os.execute('${os.quoted_path(@VEXE)} .') + assert res.exit_code == 0, res.output +} diff --git a/cmd/tools/vcreate/vcreate_windows_sqlite_note_test.v b/cmd/tools/vcreate/vcreate_windows_sqlite_note_test.v deleted file mode 100644 index 642fecc23..000000000 --- a/cmd/tools/vcreate/vcreate_windows_sqlite_note_test.v +++ /dev/null @@ -1,9 +0,0 @@ -module main - -fn test_should_print_windows_web_sqlite_note() { - assert should_print_windows_web_sqlite_note(.web, 'windows', false) - assert !should_print_windows_web_sqlite_note(.web, 'windows', true) - assert !should_print_windows_web_sqlite_note(.bin, 'windows', false) - assert !should_print_windows_web_sqlite_note(.lib, 'windows', false) - assert !should_print_windows_web_sqlite_note(.web, 'linux', false) -} diff --git a/doc/docs.md b/doc/docs.md index 55f87ad35..d840d74fe 100644 --- a/doc/docs.md +++ b/doc/docs.md @@ -82,7 +82,7 @@ by using any of the following commands in a terminal: * `v init` → adds necessary files to the current folder to make it a V project * `v new abc` → creates a new project in the new folder `abc`, by default a "hello world" project. -* `v new --web abcd` → creates a new project in the new folder `abcd`, using the vweb template. +* `v new --web abcd` → creates a new project in the new folder `abcd`, using the veb template. ## Table of Contents -- 2.39.5