@strav/create 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +46 -0
- package/package.json +24 -0
- package/src/index.ts +164 -0
- package/src/prompts.ts +135 -0
- package/src/scaffold.ts +54 -0
- package/src/templates/api/config/http.ts +7 -0
- package/src/templates/api/index.ts +30 -0
- package/src/templates/api/start/routes.ts +12 -0
- package/src/templates/api/tests/health.test.ts.tpl +12 -0
- package/src/templates/shared/.env.tpl +13 -0
- package/src/templates/shared/.gitignore.tpl +5 -0
- package/src/templates/shared/app/controllers/.gitkeep +0 -0
- package/src/templates/shared/app/models/.gitkeep +0 -0
- package/src/templates/shared/config/app.ts +7 -0
- package/src/templates/shared/config/database.ts +11 -0
- package/src/templates/shared/config/encryption.ts +6 -0
- package/src/templates/shared/database/migrations/.gitkeep +0 -0
- package/src/templates/shared/database/schemas/user.ts +10 -0
- package/src/templates/shared/package.json +25 -0
- package/src/templates/shared/strav.ts +2 -0
- package/src/templates/shared/tsconfig.json +24 -0
- package/src/templates/web/config/http.ts +8 -0
- package/src/templates/web/config/session.ts +7 -0
- package/src/templates/web/config/view.ts +4 -0
- package/src/templates/web/index.ts +36 -0
- package/src/templates/web/public/styles.css +53 -0
- package/src/templates/web/resources/islands/setup.ts +11 -0
- package/src/templates/web/resources/styles/.gitkeep +0 -0
- package/src/templates/web/resources/views/welcome.strav +21 -0
- package/src/templates/web/start/routes.ts +10 -0
- package/src/templates/web/tests/health.test.ts.tpl +12 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.5.3
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Web template**: `resources/islands/setup.ts` stub for shared Vue app configuration (plugin installation, global provides)
|
|
8
|
+
|
|
9
|
+
## 0.1.1
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Applied consistent code formatting across all source files
|
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# @stravigor/create
|
|
2
|
+
|
|
3
|
+
Scaffold a new [Strav](https://www.npmjs.com/package/@stravigor/core) application.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bunx @stravigor/create@latest my-app --api # headless REST API
|
|
9
|
+
bunx @stravigor/create@latest my-app --web # full-stack with views
|
|
10
|
+
bunx @stravigor/create@latest my-app # interactive prompt
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Templates
|
|
14
|
+
|
|
15
|
+
- **api** — Headless REST API with CORS enabled
|
|
16
|
+
- **web** — Full-stack with `.strav` views, sessions, and static files
|
|
17
|
+
|
|
18
|
+
## Options
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
bunx @stravigor/create <project-name> [options]
|
|
22
|
+
|
|
23
|
+
--api Headless REST API template
|
|
24
|
+
--web Full-stack template with views and static files
|
|
25
|
+
--template, -t api|web Alias for --api / --web
|
|
26
|
+
--db <name> Database name (default: project name)
|
|
27
|
+
-h, --help Show help
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## What's scaffolded
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
my-app/
|
|
34
|
+
├── index.ts Server entry point
|
|
35
|
+
├── strav.ts CLI (migrations, generators)
|
|
36
|
+
├── config/ Configuration files
|
|
37
|
+
├── database/schemas/ Schema definitions
|
|
38
|
+
├── start/routes.ts Route registration
|
|
39
|
+
├── tests/ Test files
|
|
40
|
+
├── .env Environment variables
|
|
41
|
+
└── package.json
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## License
|
|
45
|
+
|
|
46
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@strav/create",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Scaffold a new Strav application",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"strav",
|
|
9
|
+
"stravigor",
|
|
10
|
+
"bun",
|
|
11
|
+
"framework",
|
|
12
|
+
"scaffold",
|
|
13
|
+
"create"
|
|
14
|
+
],
|
|
15
|
+
"bin": {
|
|
16
|
+
"@strav/create": "./src/index.ts"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"src/",
|
|
20
|
+
"package.json",
|
|
21
|
+
"README.md",
|
|
22
|
+
"CHANGELOG.md"
|
|
23
|
+
]
|
|
24
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { join, resolve } from 'node:path'
|
|
4
|
+
import { select, input } from './prompts.ts'
|
|
5
|
+
import { scaffold, type ScaffoldOptions } from './scaffold.ts'
|
|
6
|
+
import pkg from '../package.json'
|
|
7
|
+
|
|
8
|
+
const VERSION = pkg.version
|
|
9
|
+
|
|
10
|
+
// ── Colors ──────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
const bold = (s: string) => `\x1b[1m${s}\x1b[0m`
|
|
13
|
+
const dim = (s: string) => `\x1b[2m${s}\x1b[0m`
|
|
14
|
+
const green = (s: string) => `\x1b[32m${s}\x1b[0m`
|
|
15
|
+
const cyan = (s: string) => `\x1b[36m${s}\x1b[0m`
|
|
16
|
+
const red = (s: string) => `\x1b[31m${s}\x1b[0m`
|
|
17
|
+
|
|
18
|
+
// ── Arg parsing ─────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
interface ParsedArgs {
|
|
21
|
+
projectName?: string
|
|
22
|
+
template?: 'api' | 'web'
|
|
23
|
+
db?: string
|
|
24
|
+
help?: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseArgs(): ParsedArgs {
|
|
28
|
+
const args = process.argv.slice(2)
|
|
29
|
+
const result: ParsedArgs = {}
|
|
30
|
+
|
|
31
|
+
for (let i = 0; i < args.length; i++) {
|
|
32
|
+
const arg = args[i]
|
|
33
|
+
|
|
34
|
+
if (arg === '--help' || arg === '-h') {
|
|
35
|
+
result.help = true
|
|
36
|
+
} else if (arg === '--api') {
|
|
37
|
+
result.template = 'api'
|
|
38
|
+
} else if (arg === '--web') {
|
|
39
|
+
result.template = 'web'
|
|
40
|
+
} else if (arg === '--template' || arg === '-t') {
|
|
41
|
+
const val = args[++i]
|
|
42
|
+
if (val === 'api' || val === 'web') {
|
|
43
|
+
result.template = val
|
|
44
|
+
} else {
|
|
45
|
+
console.error(red(` Invalid template: ${val}. Use "api" or "web".`))
|
|
46
|
+
process.exit(1)
|
|
47
|
+
}
|
|
48
|
+
} else if (arg === '--db') {
|
|
49
|
+
result.db = args[++i]
|
|
50
|
+
} else if (arg && !arg.startsWith('-') && !result.projectName) {
|
|
51
|
+
result.projectName = arg
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return result
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function printUsage(): void {
|
|
59
|
+
console.log(`
|
|
60
|
+
${bold('@stravigor/create')} ${dim(`v${VERSION}`)}
|
|
61
|
+
|
|
62
|
+
${bold('Usage:')}
|
|
63
|
+
bunx @stravigor/create ${cyan('<project-name>')} [options]
|
|
64
|
+
|
|
65
|
+
${bold('Options:')}
|
|
66
|
+
--api Headless REST API template
|
|
67
|
+
--web Full-stack template with views and static files
|
|
68
|
+
--template, -t ${dim('api|web')} Alias for --api / --web
|
|
69
|
+
--db ${dim('<name>')} Database name (default: project name)
|
|
70
|
+
-h, --help Show this help message
|
|
71
|
+
`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function toSnakeCase(name: string): string {
|
|
75
|
+
return name.replace(/-/g, '_')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Main ────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
async function main(): Promise<void> {
|
|
81
|
+
const args = parseArgs()
|
|
82
|
+
|
|
83
|
+
if (args.help) {
|
|
84
|
+
printUsage()
|
|
85
|
+
process.exit(0)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
console.log()
|
|
89
|
+
console.log(` ${bold('@stravigor/create')} ${dim(`v${VERSION}`)}`)
|
|
90
|
+
console.log()
|
|
91
|
+
|
|
92
|
+
// Project name
|
|
93
|
+
if (!args.projectName) {
|
|
94
|
+
printUsage()
|
|
95
|
+
process.exit(1)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const projectName = args.projectName
|
|
99
|
+
const root = resolve(projectName)
|
|
100
|
+
|
|
101
|
+
// Validate
|
|
102
|
+
if (existsSync(root)) {
|
|
103
|
+
console.error(red(` Directory "${projectName}" already exists.`))
|
|
104
|
+
process.exit(1)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(projectName)) {
|
|
108
|
+
console.error(
|
|
109
|
+
red(` Invalid project name. Use only letters, numbers, hyphens, and underscores.`)
|
|
110
|
+
)
|
|
111
|
+
process.exit(1)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Template
|
|
115
|
+
let template = args.template
|
|
116
|
+
if (!template) {
|
|
117
|
+
template = (await select('Which template?', [
|
|
118
|
+
{ label: 'api', value: 'api', description: 'Headless REST API' },
|
|
119
|
+
{ label: 'web', value: 'web', description: 'Full-stack with views and static files' },
|
|
120
|
+
])) as 'api' | 'web'
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Database name
|
|
124
|
+
const defaultDb = toSnakeCase(projectName)
|
|
125
|
+
const dbName = args.db ?? (await input('Database name:', defaultDb))
|
|
126
|
+
|
|
127
|
+
console.log()
|
|
128
|
+
|
|
129
|
+
// Scaffold
|
|
130
|
+
const opts: ScaffoldOptions = { projectName, template, dbName }
|
|
131
|
+
await scaffold(root, opts)
|
|
132
|
+
console.log(` ${green('+')} Scaffolded project files`)
|
|
133
|
+
|
|
134
|
+
// Install dependencies
|
|
135
|
+
console.log(` ${dim('...')} Installing dependencies`)
|
|
136
|
+
const install = Bun.spawn(['bun', 'install'], { cwd: root, stdout: 'ignore', stderr: 'pipe' })
|
|
137
|
+
const exitCode = await install.exited
|
|
138
|
+
|
|
139
|
+
if (exitCode !== 0) {
|
|
140
|
+
const stderr = await new Response(install.stderr).text()
|
|
141
|
+
console.error(red(` Failed to install dependencies:`))
|
|
142
|
+
console.error(dim(` ${stderr}`))
|
|
143
|
+
process.exit(1)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log(` ${green('+')} Installed dependencies`)
|
|
147
|
+
|
|
148
|
+
// Done
|
|
149
|
+
console.log()
|
|
150
|
+
console.log(` ${green('Project created successfully!')}`)
|
|
151
|
+
console.log()
|
|
152
|
+
console.log(` Next steps:`)
|
|
153
|
+
console.log()
|
|
154
|
+
console.log(` ${dim('$')} cd ${projectName}`)
|
|
155
|
+
console.log(` ${dim('$')} bun --hot index.ts`)
|
|
156
|
+
console.log()
|
|
157
|
+
console.log(` ${dim('Then open http://localhost:3000')}`)
|
|
158
|
+
console.log()
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
main().catch(err => {
|
|
162
|
+
console.error(red(` Error: ${err instanceof Error ? err.message : err}`))
|
|
163
|
+
process.exit(1)
|
|
164
|
+
})
|
package/src/prompts.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
const ESC = '\x1b'
|
|
2
|
+
const ARROW_UP = `${ESC}[A`
|
|
3
|
+
const ARROW_DOWN = `${ESC}[B`
|
|
4
|
+
const ENTER = '\r'
|
|
5
|
+
|
|
6
|
+
interface Choice {
|
|
7
|
+
label: string
|
|
8
|
+
value: string
|
|
9
|
+
description: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function select(message: string, choices: Choice[]): Promise<string> {
|
|
13
|
+
let selected = 0
|
|
14
|
+
|
|
15
|
+
const render = () => {
|
|
16
|
+
// Move cursor up to overwrite previous render (except first time)
|
|
17
|
+
process.stdout.write(`\x1b[${choices.length}A`)
|
|
18
|
+
for (let i = 0; i < choices.length; i++) {
|
|
19
|
+
const prefix = i === selected ? '\x1b[36m>\x1b[0m' : ' '
|
|
20
|
+
const choice = choices[i]!
|
|
21
|
+
const label = i === selected ? `\x1b[1m${choice.label}\x1b[0m` : choice.label
|
|
22
|
+
const desc = `\x1b[2m${choice.description}\x1b[0m`
|
|
23
|
+
process.stdout.write(`\x1b[2K ${prefix} ${label} ${desc}\n`)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
process.stdout.write(` \x1b[1m${message}\x1b[0m\n`)
|
|
28
|
+
// Print initial lines so render() can overwrite them
|
|
29
|
+
for (const choice of choices) {
|
|
30
|
+
process.stdout.write('\n')
|
|
31
|
+
}
|
|
32
|
+
render()
|
|
33
|
+
|
|
34
|
+
return new Promise(resolve => {
|
|
35
|
+
const stdin = process.stdin
|
|
36
|
+
stdin.setRawMode(true)
|
|
37
|
+
stdin.resume()
|
|
38
|
+
stdin.setEncoding('utf8')
|
|
39
|
+
|
|
40
|
+
let buffer = ''
|
|
41
|
+
|
|
42
|
+
const onData = (data: string) => {
|
|
43
|
+
buffer += data
|
|
44
|
+
|
|
45
|
+
// Check for Ctrl+C
|
|
46
|
+
if (buffer.includes('\x03')) {
|
|
47
|
+
stdin.setRawMode(false)
|
|
48
|
+
stdin.pause()
|
|
49
|
+
stdin.removeListener('data', onData)
|
|
50
|
+
process.stdout.write('\n')
|
|
51
|
+
process.exit(0)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Process escape sequences
|
|
55
|
+
while (buffer.length > 0) {
|
|
56
|
+
if (buffer.startsWith(ARROW_UP)) {
|
|
57
|
+
selected = (selected - 1 + choices.length) % choices.length
|
|
58
|
+
render()
|
|
59
|
+
buffer = buffer.slice(ARROW_UP.length)
|
|
60
|
+
} else if (buffer.startsWith(ARROW_DOWN)) {
|
|
61
|
+
selected = (selected + 1) % choices.length
|
|
62
|
+
render()
|
|
63
|
+
buffer = buffer.slice(ARROW_DOWN.length)
|
|
64
|
+
} else if (buffer.startsWith(ENTER)) {
|
|
65
|
+
stdin.setRawMode(false)
|
|
66
|
+
stdin.pause()
|
|
67
|
+
stdin.removeListener('data', onData)
|
|
68
|
+
resolve(choices[selected]!.value)
|
|
69
|
+
buffer = ''
|
|
70
|
+
return
|
|
71
|
+
} else if (buffer.startsWith(ESC)) {
|
|
72
|
+
// Incomplete escape sequence, wait for more data
|
|
73
|
+
break
|
|
74
|
+
} else {
|
|
75
|
+
// Discard unrecognized input
|
|
76
|
+
buffer = buffer.slice(1)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
stdin.on('data', onData)
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function input(message: string, defaultValue: string): Promise<string> {
|
|
86
|
+
process.stdout.write(` \x1b[1m${message}\x1b[0m \x1b[2m(${defaultValue})\x1b[0m `)
|
|
87
|
+
|
|
88
|
+
return new Promise(resolve => {
|
|
89
|
+
const stdin = process.stdin
|
|
90
|
+
stdin.setRawMode(true)
|
|
91
|
+
stdin.resume()
|
|
92
|
+
stdin.setEncoding('utf8')
|
|
93
|
+
|
|
94
|
+
let value = ''
|
|
95
|
+
|
|
96
|
+
const onData = (data: string) => {
|
|
97
|
+
for (const char of data) {
|
|
98
|
+
if (char === '\x03') {
|
|
99
|
+
// Ctrl+C
|
|
100
|
+
stdin.setRawMode(false)
|
|
101
|
+
stdin.pause()
|
|
102
|
+
stdin.removeListener('data', onData)
|
|
103
|
+
process.stdout.write('\n')
|
|
104
|
+
process.exit(0)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (char === '\r' || char === '\n') {
|
|
108
|
+
stdin.setRawMode(false)
|
|
109
|
+
stdin.pause()
|
|
110
|
+
stdin.removeListener('data', onData)
|
|
111
|
+
process.stdout.write('\n')
|
|
112
|
+
resolve(value || defaultValue)
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (char === '\x7f' || char === '\b') {
|
|
117
|
+
// Backspace
|
|
118
|
+
if (value.length > 0) {
|
|
119
|
+
value = value.slice(0, -1)
|
|
120
|
+
process.stdout.write('\b \b')
|
|
121
|
+
}
|
|
122
|
+
continue
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Printable characters
|
|
126
|
+
if (char >= ' ') {
|
|
127
|
+
value += char
|
|
128
|
+
process.stdout.write(char)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
stdin.on('data', onData)
|
|
134
|
+
})
|
|
135
|
+
}
|
package/src/scaffold.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { readdirSync, mkdirSync, statSync } from 'node:fs'
|
|
2
|
+
import { join, dirname } from 'node:path'
|
|
3
|
+
import pkg from '../package.json'
|
|
4
|
+
|
|
5
|
+
export interface ScaffoldOptions {
|
|
6
|
+
projectName: string
|
|
7
|
+
template: 'api' | 'web'
|
|
8
|
+
dbName: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function scaffold(root: string, opts: ScaffoldOptions): Promise<void> {
|
|
12
|
+
const templatesDir = join(import.meta.dir, 'templates')
|
|
13
|
+
const appKey = crypto.randomUUID()
|
|
14
|
+
|
|
15
|
+
const replacements: Record<string, string> = {
|
|
16
|
+
__PROJECT_NAME__: opts.projectName,
|
|
17
|
+
__DB_NAME__: opts.dbName,
|
|
18
|
+
__APP_KEY__: appKey,
|
|
19
|
+
__CORE_VERSION__: `^${pkg.version}`,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Copy shared files first, then template-specific (may override shared)
|
|
23
|
+
await copyDir(join(templatesDir, 'shared'), root, replacements)
|
|
24
|
+
await copyDir(join(templatesDir, opts.template), root, replacements)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function copyDir(
|
|
28
|
+
srcDir: string,
|
|
29
|
+
destDir: string,
|
|
30
|
+
replacements: Record<string, string>
|
|
31
|
+
): Promise<void> {
|
|
32
|
+
const entries = readdirSync(srcDir)
|
|
33
|
+
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
const srcPath = join(srcDir, entry)
|
|
36
|
+
const destPath = join(destDir, entry.replace(/\.tpl$/, ''))
|
|
37
|
+
|
|
38
|
+
if (statSync(srcPath).isDirectory()) {
|
|
39
|
+
await copyDir(srcPath, destPath, replacements)
|
|
40
|
+
} else {
|
|
41
|
+
mkdirSync(dirname(destPath), { recursive: true })
|
|
42
|
+
const content = await Bun.file(srcPath).text()
|
|
43
|
+
await Bun.write(destPath, applyReplacements(content, replacements))
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function applyReplacements(content: string, replacements: Record<string, string>): string {
|
|
49
|
+
let result = content
|
|
50
|
+
for (const [placeholder, value] of Object.entries(replacements)) {
|
|
51
|
+
result = result.replaceAll(placeholder, value)
|
|
52
|
+
}
|
|
53
|
+
return result
|
|
54
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import 'reflect-metadata'
|
|
2
|
+
import { app } from '@stravigor/kernel/core'
|
|
3
|
+
import { router } from '@stravigor/http/http'
|
|
4
|
+
import { ConfigProvider, EncryptionProvider } from '@stravigor/kernel/providers'
|
|
5
|
+
import { DatabaseProvider } from '@stravigor/database/providers'
|
|
6
|
+
import BaseModel from '@stravigor/database/orm/base_model'
|
|
7
|
+
import Database from '@stravigor/database/database/database'
|
|
8
|
+
import Server from '@stravigor/http/http/server'
|
|
9
|
+
import { ExceptionHandler } from '@stravigor/kernel/exceptions'
|
|
10
|
+
|
|
11
|
+
// Register service providers
|
|
12
|
+
app.use(new ConfigProvider()).use(new DatabaseProvider()).use(new EncryptionProvider())
|
|
13
|
+
|
|
14
|
+
// Boot services (loads config, connects database, derives encryption keys)
|
|
15
|
+
await app.start()
|
|
16
|
+
|
|
17
|
+
// Initialize ORM
|
|
18
|
+
new BaseModel(app.resolve(Database))
|
|
19
|
+
|
|
20
|
+
// Configure router
|
|
21
|
+
router.useExceptionHandler(new ExceptionHandler(true))
|
|
22
|
+
router.cors()
|
|
23
|
+
|
|
24
|
+
// Load routes
|
|
25
|
+
await import('./start/routes')
|
|
26
|
+
|
|
27
|
+
// Start HTTP server
|
|
28
|
+
app.singleton(Server)
|
|
29
|
+
const server = app.resolve(Server)
|
|
30
|
+
server.start(router)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { test, expect } from 'bun:test'
|
|
2
|
+
import { TestCase } from '@stravigor/testing'
|
|
3
|
+
|
|
4
|
+
const t = await TestCase.boot({
|
|
5
|
+
routes: () => import('../start/routes'),
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
test('health check returns ok', async () => {
|
|
9
|
+
const res = await t.get('/health')
|
|
10
|
+
expect(res.status).toBe(200)
|
|
11
|
+
expect(await res.json()).toEqual({ status: 'ok' })
|
|
12
|
+
})
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { env } from '@stravigor/kernel/helpers/env'
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
host: env('DB_HOST', '127.0.0.1'),
|
|
5
|
+
port: env.int('DB_PORT', 5432),
|
|
6
|
+
username: env('DB_USERNAME', 'postgres'),
|
|
7
|
+
password: env('DB_PASSWORD', ''),
|
|
8
|
+
database: env('DB_DATABASE', '__DB_NAME__'),
|
|
9
|
+
pool: env.int('DB_POOL_MAX', 10),
|
|
10
|
+
idleTimeout: env.int('DB_IDLE_TIMEOUT', 20),
|
|
11
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineSchema, t, Archetype } from '@stravigor/database/schema'
|
|
2
|
+
|
|
3
|
+
export default defineSchema('user', {
|
|
4
|
+
archetype: Archetype.Entity,
|
|
5
|
+
fields: {
|
|
6
|
+
email: t.varchar(255).required().unique().index(),
|
|
7
|
+
name: t.varchar(255).required(),
|
|
8
|
+
password: t.varchar(255).required().sensitive(),
|
|
9
|
+
},
|
|
10
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PROJECT_NAME__",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"private": true,
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "bun --hot index.ts",
|
|
8
|
+
"start": "bun index.ts",
|
|
9
|
+
"test": "bun test tests/"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@stravigor/kernel": "__CORE_VERSION__",
|
|
13
|
+
"@stravigor/http": "__CORE_VERSION__",
|
|
14
|
+
"@stravigor/view": "__CORE_VERSION__",
|
|
15
|
+
"@stravigor/database": "__CORE_VERSION__",
|
|
16
|
+
"@stravigor/cli": "__CORE_VERSION__",
|
|
17
|
+
"luxon": "^3.7.2",
|
|
18
|
+
"reflect-metadata": "^0.2.2"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/bun": "latest",
|
|
22
|
+
"@types/luxon": "^3.7.1",
|
|
23
|
+
"@stravigor/testing": "__CORE_VERSION__"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": [
|
|
4
|
+
"ESNext"
|
|
5
|
+
],
|
|
6
|
+
"target": "ESNext",
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"moduleDetection": "force",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"experimentalDecorators": true,
|
|
14
|
+
"emitDecoratorMetadata": true,
|
|
15
|
+
"strict": true,
|
|
16
|
+
"skipLibCheck": true,
|
|
17
|
+
"noFallthroughCasesInSwitch": true,
|
|
18
|
+
"noUnusedLocals": false,
|
|
19
|
+
"noUnusedParameters": false
|
|
20
|
+
},
|
|
21
|
+
"include": [
|
|
22
|
+
"**/*.ts"
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import 'reflect-metadata'
|
|
2
|
+
import { app } from '@stravigor/kernel/core'
|
|
3
|
+
import { router } from '@stravigor/http/http'
|
|
4
|
+
import { ConfigProvider, EncryptionProvider } from '@stravigor/kernel/providers'
|
|
5
|
+
import { DatabaseProvider } from '@stravigor/database/providers'
|
|
6
|
+
import { SessionProvider } from '@stravigor/http/providers'
|
|
7
|
+
import { ViewProvider } from '@stravigor/view'
|
|
8
|
+
import BaseModel from '@stravigor/database/orm/base_model'
|
|
9
|
+
import Database from '@stravigor/database/database/database'
|
|
10
|
+
import Server from '@stravigor/http/http/server'
|
|
11
|
+
import { ExceptionHandler } from '@stravigor/kernel/exceptions'
|
|
12
|
+
|
|
13
|
+
// Register service providers
|
|
14
|
+
app
|
|
15
|
+
.use(new ConfigProvider())
|
|
16
|
+
.use(new DatabaseProvider())
|
|
17
|
+
.use(new EncryptionProvider())
|
|
18
|
+
.use(new SessionProvider())
|
|
19
|
+
.use(new ViewProvider())
|
|
20
|
+
|
|
21
|
+
// Boot services (loads config, connects database, derives encryption keys, starts sessions)
|
|
22
|
+
await app.start()
|
|
23
|
+
|
|
24
|
+
// Initialize ORM
|
|
25
|
+
new BaseModel(app.resolve(Database))
|
|
26
|
+
|
|
27
|
+
// Configure router
|
|
28
|
+
router.useExceptionHandler(new ExceptionHandler(true))
|
|
29
|
+
|
|
30
|
+
// Load routes
|
|
31
|
+
await import('./start/routes')
|
|
32
|
+
|
|
33
|
+
// Start HTTP server
|
|
34
|
+
app.singleton(Server)
|
|
35
|
+
const server = app.resolve(Server)
|
|
36
|
+
server.start(router)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
* {
|
|
2
|
+
margin: 0;
|
|
3
|
+
padding: 0;
|
|
4
|
+
box-sizing: border-box;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
body {
|
|
8
|
+
font-family: 'Barlow Semi Condensed', sans-serif;
|
|
9
|
+
background: #f8fafc;
|
|
10
|
+
color: #1e293b;
|
|
11
|
+
min-height: 100vh;
|
|
12
|
+
display: flex;
|
|
13
|
+
align-items: center;
|
|
14
|
+
justify-content: center;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.container {
|
|
18
|
+
text-align: center;
|
|
19
|
+
padding: 2rem;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
h1 {
|
|
23
|
+
font-size: 2.5rem;
|
|
24
|
+
font-weight: 700;
|
|
25
|
+
margin-bottom: 1rem;
|
|
26
|
+
color: oklch(57.7% 0.245 27.325);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
p {
|
|
30
|
+
font-size: 1.125rem;
|
|
31
|
+
color: #64748b;
|
|
32
|
+
margin-bottom: 2rem;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
p strong {
|
|
36
|
+
color: #1e293b;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.links a {
|
|
40
|
+
color: #2563eb;
|
|
41
|
+
text-decoration: none;
|
|
42
|
+
font-size: 0.875rem;
|
|
43
|
+
font-weight: 600;
|
|
44
|
+
border: 1px solid #e2e8f0;
|
|
45
|
+
padding: 0.5rem 1rem;
|
|
46
|
+
border-radius: 0.375rem;
|
|
47
|
+
transition: all 0.2s;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.links a:hover {
|
|
51
|
+
background: #eff6ff;
|
|
52
|
+
border-color: #2563eb;
|
|
53
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { App } from 'vue'
|
|
2
|
+
|
|
3
|
+
export default function setup(app: App) {
|
|
4
|
+
// Install plugins, provide globals, or register components here.
|
|
5
|
+
// This runs once before any island is mounted.
|
|
6
|
+
//
|
|
7
|
+
// Examples:
|
|
8
|
+
// app.use(pinia)
|
|
9
|
+
// app.provide('api', apiClient)
|
|
10
|
+
// app.component('Icon', Icon)
|
|
11
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>{{ name }} — Strav</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Barlow+Semi+Condensed:wght@400;600;700&display=swap" rel="stylesheet">
|
|
10
|
+
<link rel="stylesheet" href="/styles.css">
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div class="container">
|
|
14
|
+
<h1>Welcome to Strav</h1>
|
|
15
|
+
<p>Your application <strong>{{ name }}</strong> is up and running.</p>
|
|
16
|
+
<div class="links">
|
|
17
|
+
<a href="https://github.com/nicoyambura/stravigor" target="_blank">Documentation</a>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
</body>
|
|
21
|
+
</html>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { router } from '@stravigor/http/http'
|
|
2
|
+
import { view } from '@stravigor/view'
|
|
3
|
+
|
|
4
|
+
router.get('/', async () => {
|
|
5
|
+
return view('welcome', { name: '__PROJECT_NAME__' })
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
router.get('/api/health', () => {
|
|
9
|
+
return Response.json({ status: 'ok' })
|
|
10
|
+
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { test, expect } from 'bun:test'
|
|
2
|
+
import { TestCase } from '@stravigor/testing'
|
|
3
|
+
|
|
4
|
+
const t = await TestCase.boot({
|
|
5
|
+
routes: () => import('../start/routes'),
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
test('health check returns ok', async () => {
|
|
9
|
+
const res = await t.get('/api/health')
|
|
10
|
+
expect(res.status).toBe(200)
|
|
11
|
+
expect(await res.json()).toEqual({ status: 'ok' })
|
|
12
|
+
})
|