@strav/spring 0.4.30 → 1.0.0-alpha.28
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/README.md +11 -52
- package/package.json +19 -20
- package/src/args.ts +119 -0
- package/src/cli.ts +134 -0
- package/src/index.ts +10 -176
- package/src/prompts.ts +49 -127
- package/src/scaffold.ts +115 -36
- package/src/spring_error.ts +11 -0
- package/src/templates/shared/README.md.tt +37 -0
- package/src/templates/shared/_dot_env.example.tt +12 -0
- package/src/templates/shared/_dot_env.tt +12 -0
- package/src/templates/shared/_dot_gitignore +12 -0
- package/src/templates/shared/app/console/_dot_gitkeep +0 -0
- package/src/templates/shared/app/exceptions/_dot_gitkeep +0 -0
- package/src/templates/shared/app/http/controllers/_dot_gitkeep +0 -0
- package/src/templates/shared/app/http/middleware/_dot_gitkeep +0 -0
- package/src/templates/shared/app/http/requests/_dot_gitkeep +0 -0
- package/src/templates/shared/app/jobs/_dot_gitkeep +0 -0
- package/src/templates/shared/app/mail/_dot_gitkeep +0 -0
- package/src/templates/shared/app/models/_dot_gitkeep +0 -0
- package/src/templates/shared/app/notifications/_dot_gitkeep +0 -0
- package/src/templates/shared/app/policies/_dot_gitkeep +0 -0
- package/src/templates/shared/app/providers/app_provider.ts +18 -0
- package/src/templates/shared/app/repositories/_dot_gitkeep +0 -0
- package/src/templates/shared/bin/strav.ts +21 -0
- package/src/templates/shared/bootstrap/app.ts +13 -0
- package/src/templates/shared/bootstrap/providers.ts +29 -0
- package/src/templates/shared/config/app.ts.tt +13 -0
- package/src/templates/shared/config/http.ts +9 -0
- package/src/templates/shared/config/logger.ts +9 -0
- package/src/templates/shared/database/factories/_dot_gitkeep +0 -0
- package/src/templates/shared/database/migrations/_dot_gitkeep +0 -0
- package/src/templates/shared/database/schemas/_dot_gitkeep +0 -0
- package/src/templates/shared/database/seeders/_dot_gitkeep +0 -0
- package/src/templates/shared/package.json.tt +22 -0
- package/src/templates/shared/routes/api.ts +11 -0
- package/src/templates/shared/routes/console.ts +10 -0
- package/src/templates/shared/storage/cache/_dot_gitkeep +0 -0
- package/src/templates/shared/storage/logs/_dot_gitkeep +0 -0
- package/src/templates/shared/storage/uploads/_dot_gitkeep +0 -0
- package/src/templates/shared/tests/feature/healthz.test.ts.tt +19 -0
- package/src/templates/shared/tests/unit/_dot_gitkeep +0 -0
- package/src/templates/shared/tsconfig.json +21 -11
- package/src/templates/web/README.md.tt +42 -0
- package/src/templates/web/_dot_gitignore +13 -0
- package/src/templates/web/app/providers/app_provider.ts +21 -0
- package/src/templates/web/bootstrap/providers.ts +34 -0
- package/src/templates/web/config/http.ts +17 -8
- package/src/templates/web/config/view.ts +7 -7
- package/src/templates/web/package.json.tt +26 -0
- package/src/templates/web/public/assets/_dot_gitkeep +0 -0
- package/src/templates/web/resources/css/app.css +41 -0
- package/src/templates/web/resources/ts/islands/counter.vue +11 -0
- package/src/templates/web/resources/ts/islands/setup.ts +13 -0
- package/src/templates/web/resources/views/components/_dot_gitkeep +0 -0
- package/src/templates/web/resources/views/errors/404.strav +8 -0
- package/src/templates/web/resources/views/errors/500.strav +8 -0
- package/src/templates/web/resources/views/layouts/app.strav.tt +15 -0
- package/src/templates/web/resources/views/pages/index.strav.tt +12 -0
- package/src/templates/web/routes/broadcast.ts +9 -0
- package/src/templates/web/routes/web.ts +19 -0
- package/src/templates/web/tests/browser/_dot_gitkeep +0 -0
- package/src/version.ts +9 -0
- package/src/templates/api/app/controllers/controller.ts +0 -15
- package/src/templates/api/app/controllers/user_controller.ts +0 -69
- package/src/templates/api/config/database.ts +0 -9
- package/src/templates/api/config/http.ts +0 -17
- package/src/templates/api/database/factories/user_factory.ts +0 -11
- package/src/templates/api/database/schemas/user.ts +0 -13
- package/src/templates/api/database/seeders/database_seeder.ts +0 -8
- package/src/templates/api/database/seeders/user_seeder.ts +0 -15
- package/src/templates/api/index.ts +0 -11
- package/src/templates/api/package.json +0 -24
- package/src/templates/api/start/providers.ts +0 -10
- package/src/templates/api/start/routes.ts +0 -22
- package/src/templates/shared/config/app.ts +0 -7
- package/src/templates/shared/config/encryption.ts +0 -5
- package/src/templates/shared/package.json +0 -24
- package/src/templates/shared/storage/uploads/.gitkeep +0 -1
- package/src/templates/shared/strav.ts +0 -2
- package/src/templates/shared/tests/example.test.ts +0 -11
- package/src/templates/web/index.ts +0 -28
- package/src/templates/web/package.json +0 -26
- package/src/templates/web/public/builds/.gitkeep +0 -1
- package/src/templates/web/public/css/.gitkeep +0 -1
- package/src/templates/web/resources/css/app.scss +0 -77
- package/src/templates/web/resources/islands/counter.vue +0 -31
- package/src/templates/web/resources/views/layouts/app.strav +0 -18
- package/src/templates/web/resources/views/pages/index.strav +0 -32
- package/src/templates/web/start/providers.ts +0 -11
package/README.md
CHANGED
|
@@ -1,61 +1,20 @@
|
|
|
1
1
|
# @strav/spring
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## Usage
|
|
3
|
+
Project scaffolder for Strav 1.0. Writes a working app skeleton you can boot in a single command.
|
|
6
4
|
|
|
7
5
|
```bash
|
|
8
|
-
bunx @strav/spring my-app --
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
## Templates
|
|
14
|
-
|
|
15
|
-
- **api** — Headless REST API with CORS enabled
|
|
16
|
-
- **web** — Full-stack with .strav views, Vue islands, and sessions
|
|
17
|
-
|
|
18
|
-
## Options
|
|
19
|
-
|
|
20
|
-
```
|
|
21
|
-
bunx @strav/spring <project-name> [options]
|
|
22
|
-
|
|
23
|
-
--api Headless REST API template
|
|
24
|
-
--web Full-stack template with Vue islands
|
|
25
|
-
--template, -t api|web Alias for --api / --web
|
|
26
|
-
--db <name> Database name (default: project name)
|
|
27
|
-
-h, --help Show help
|
|
6
|
+
bunx @strav/spring my-app --api
|
|
7
|
+
cd my-app
|
|
8
|
+
bun install
|
|
9
|
+
bun strav serve
|
|
10
|
+
# → listening on http://localhost:3000
|
|
28
11
|
```
|
|
29
12
|
|
|
30
|
-
|
|
13
|
+
See [`docs/spring/`](../../docs/spring/) for the full documentation and the [template-strategy ADR](../../docs/decisions/spring-template-strategy.md) for design notes.
|
|
31
14
|
|
|
32
|
-
|
|
33
|
-
my-app/
|
|
34
|
-
├── app/
|
|
35
|
-
│ ├── controllers/ # HTTP controllers
|
|
36
|
-
│ ├── models/ # Database models (generated from schemas)
|
|
37
|
-
│ ├── middleware/ # Custom middleware
|
|
38
|
-
│ ├── providers/ # Service providers
|
|
39
|
-
│ ├── policies/ # Authorization policies
|
|
40
|
-
│ ├── jobs/ # Queue jobs
|
|
41
|
-
│ └── services/ # Business logic services
|
|
42
|
-
├── config/ # Configuration files
|
|
43
|
-
├── database/
|
|
44
|
-
│ ├── schemas/ # Schema definitions
|
|
45
|
-
│ ├── migrations/ # Generated migrations
|
|
46
|
-
│ ├── seeders/ # Database seeders
|
|
47
|
-
│ └── factories/ # Model factories
|
|
48
|
-
├── resources/
|
|
49
|
-
│ ├── views/ # .strav templates
|
|
50
|
-
│ ├── css/ # Stylesheets
|
|
51
|
-
│ └── ts/islands/ # Vue.js islands
|
|
52
|
-
├── routes/ # Route definitions
|
|
53
|
-
├── tests/ # Test files
|
|
54
|
-
├── index.ts # Application entry point
|
|
55
|
-
├── strav.ts # CLI tool
|
|
56
|
-
└── .env # Environment variables
|
|
57
|
-
```
|
|
15
|
+
## Status
|
|
58
16
|
|
|
59
|
-
|
|
17
|
+
- Slice A — `--api` template + CLI shell. **Shipped.**
|
|
18
|
+
- Slice B — `--web` template (`@strav/view` + Vue islands + plain CSS). **Shipped.**
|
|
60
19
|
|
|
61
|
-
|
|
20
|
+
The scaffolder is feature-complete for 1.0. Slice 5.17 (`port` codemod) is cancelled — no 0.x backward-compat commitment.
|
package/package.json
CHANGED
|
@@ -1,30 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@strav/spring",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-alpha.28",
|
|
4
|
+
"description": "Strav project scaffolder — `bunx @strav/spring my-app` writes a working app skeleton (bin/, bootstrap/, config/, routes/, …) per spec/directory-structure.md. Two templates: --api (headless REST) and --web (Vue islands + .strav views). Runtime-independent of the framework — no @strav/* runtime deps.",
|
|
4
5
|
"type": "module",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
"framework",
|
|
11
|
-
"scaffold",
|
|
12
|
-
"create",
|
|
13
|
-
"typescript",
|
|
14
|
-
"vue"
|
|
15
|
-
],
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts"
|
|
10
|
+
},
|
|
16
11
|
"bin": {
|
|
17
|
-
"@strav/spring": "./src/
|
|
12
|
+
"@strav/spring": "./src/cli.ts"
|
|
18
13
|
},
|
|
19
14
|
"files": [
|
|
20
|
-
"src
|
|
21
|
-
"package.json",
|
|
15
|
+
"src",
|
|
22
16
|
"README.md"
|
|
23
17
|
],
|
|
24
|
-
"
|
|
25
|
-
"
|
|
18
|
+
"engines": {
|
|
19
|
+
"bun": ">=1.3.14"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"@types/bun": ">=1.3.14"
|
|
26
26
|
},
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
}
|
|
27
|
+
"dependencies": null,
|
|
28
|
+
"devDependencies": null
|
|
30
29
|
}
|
package/src/args.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure CLI argument parser for `bunx @strav/spring`. Side-effect free so
|
|
3
|
+
* `tests/unit/args.test.ts` can hit every branch without spawning a process.
|
|
4
|
+
*
|
|
5
|
+
* Surface:
|
|
6
|
+
* bunx @strav/spring <project-name> [--api|--web|-t api|web] [--db <name>]
|
|
7
|
+
* [--no-install] [-h|--help] [-v|--version]
|
|
8
|
+
*
|
|
9
|
+
* Validation rules:
|
|
10
|
+
* - Project name is required for a real run. Help / version short-circuit it.
|
|
11
|
+
* - Project name matches `/^[a-z0-9][a-z0-9_-]*$/` (no uppercase, no leading
|
|
12
|
+
* dot, no spaces). This is what npm packageName allows minus the scope.
|
|
13
|
+
* - `--template` / `-t` only accepts `api` or `web`. Conflicts between
|
|
14
|
+
* `--api` / `--web` / `--template` → error.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { SpringError } from './spring_error.ts'
|
|
18
|
+
|
|
19
|
+
export type Template = 'api' | 'web'
|
|
20
|
+
|
|
21
|
+
export interface ParsedArgs {
|
|
22
|
+
/** Empty when the user just asked for help or version. */
|
|
23
|
+
projectName?: string
|
|
24
|
+
template?: Template
|
|
25
|
+
dbName?: string
|
|
26
|
+
noInstall: boolean
|
|
27
|
+
help: boolean
|
|
28
|
+
version: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const NAME_RE = /^[a-z0-9][a-z0-9_-]*$/
|
|
32
|
+
|
|
33
|
+
export function parseArgs(argv: readonly string[]): ParsedArgs {
|
|
34
|
+
const out: ParsedArgs = { noInstall: false, help: false, version: false }
|
|
35
|
+
let templateSeenAs: string | undefined
|
|
36
|
+
|
|
37
|
+
const setTemplate = (value: string, flag: string): void => {
|
|
38
|
+
if (value !== 'api' && value !== 'web') {
|
|
39
|
+
throw new SpringError(`${flag}: expected "api" or "web", got "${value}"`)
|
|
40
|
+
}
|
|
41
|
+
if (out.template !== undefined && out.template !== value) {
|
|
42
|
+
throw new SpringError(
|
|
43
|
+
`${flag} conflicts with earlier ${templateSeenAs} (resolved to "${out.template}")`,
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
out.template = value as Template
|
|
47
|
+
templateSeenAs = flag
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < argv.length; i++) {
|
|
51
|
+
const arg = argv[i] as string
|
|
52
|
+
switch (arg) {
|
|
53
|
+
case '-h':
|
|
54
|
+
case '--help':
|
|
55
|
+
out.help = true
|
|
56
|
+
break
|
|
57
|
+
case '-v':
|
|
58
|
+
case '--version':
|
|
59
|
+
out.version = true
|
|
60
|
+
break
|
|
61
|
+
case '--api':
|
|
62
|
+
setTemplate('api', '--api')
|
|
63
|
+
break
|
|
64
|
+
case '--web':
|
|
65
|
+
setTemplate('web', '--web')
|
|
66
|
+
break
|
|
67
|
+
case '--no-install':
|
|
68
|
+
out.noInstall = true
|
|
69
|
+
break
|
|
70
|
+
case '-t':
|
|
71
|
+
case '--template': {
|
|
72
|
+
const next = argv[++i]
|
|
73
|
+
if (next === undefined) {
|
|
74
|
+
throw new SpringError(`${arg}: missing value (expected "api" or "web")`)
|
|
75
|
+
}
|
|
76
|
+
setTemplate(next, arg)
|
|
77
|
+
break
|
|
78
|
+
}
|
|
79
|
+
case '--db': {
|
|
80
|
+
const next = argv[++i]
|
|
81
|
+
if (next === undefined) {
|
|
82
|
+
throw new SpringError(`--db: missing value (expected a database name)`)
|
|
83
|
+
}
|
|
84
|
+
out.dbName = next
|
|
85
|
+
break
|
|
86
|
+
}
|
|
87
|
+
default: {
|
|
88
|
+
if (arg.startsWith('-')) {
|
|
89
|
+
throw new SpringError(`unknown option: ${arg}`)
|
|
90
|
+
}
|
|
91
|
+
if (out.projectName !== undefined) {
|
|
92
|
+
throw new SpringError(
|
|
93
|
+
`unexpected positional argument "${arg}" (project name "${out.projectName}" already set)`,
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
if (!NAME_RE.test(arg)) {
|
|
97
|
+
throw new SpringError(
|
|
98
|
+
`invalid project name "${arg}" — must match /^[a-z0-9][a-z0-9_-]*$/ (lowercase letters, digits, hyphen, underscore)`,
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
out.projectName = arg
|
|
102
|
+
break
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return out
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Convert a project name to a snake_case database default. Splits on `-`
|
|
111
|
+
* and runs of non-alphanumerics. Mirrors what 0.x spring did so the
|
|
112
|
+
* default `DB_DATABASE` value reads naturally for `my-blog` → `my_blog`.
|
|
113
|
+
*/
|
|
114
|
+
export function toSnakeCase(name: string): string {
|
|
115
|
+
return name
|
|
116
|
+
.toLowerCase()
|
|
117
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
118
|
+
.replace(/^_+|_+$/g, '')
|
|
119
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* `bunx @strav/spring <project>` entry. Wires argv parsing → optional
|
|
4
|
+
* interactive prompts → scaffold → `bun install`. Errors print to stderr
|
|
5
|
+
* and exit non-zero; expected (`SpringError`) errors print without the
|
|
6
|
+
* stack trace, unexpected ones include it.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync } from 'node:fs'
|
|
10
|
+
import { resolve } from 'node:path'
|
|
11
|
+
import { parseArgs, type Template, toSnakeCase } from './args.ts'
|
|
12
|
+
import { input, select } from './prompts.ts'
|
|
13
|
+
import { scaffold } from './scaffold.ts'
|
|
14
|
+
import { SpringError } from './spring_error.ts'
|
|
15
|
+
import { STRAV_VERSION } from './version.ts'
|
|
16
|
+
|
|
17
|
+
const bold = (s: string): string => `\x1b[1m${s}\x1b[0m`
|
|
18
|
+
const dim = (s: string): string => `\x1b[2m${s}\x1b[0m`
|
|
19
|
+
const green = (s: string): string => `\x1b[32m${s}\x1b[0m`
|
|
20
|
+
const red = (s: string): string => `\x1b[31m${s}\x1b[0m`
|
|
21
|
+
const cyan = (s: string): string => `\x1b[36m${s}\x1b[0m`
|
|
22
|
+
|
|
23
|
+
const SPRING_VERSION = '1.0.0-alpha.28'
|
|
24
|
+
|
|
25
|
+
function printUsage(): void {
|
|
26
|
+
process.stdout.write(`
|
|
27
|
+
${bold('@strav/spring')} ${dim(`v${SPRING_VERSION}`)}
|
|
28
|
+
${dim('Strav project scaffolder')}
|
|
29
|
+
|
|
30
|
+
${bold('Usage:')}
|
|
31
|
+
bunx @strav/spring ${cyan('<project-name>')} [options]
|
|
32
|
+
|
|
33
|
+
${bold('Options:')}
|
|
34
|
+
--api Headless REST template
|
|
35
|
+
--web Full-stack template ${dim('(pages auto-router + Vue islands)')}
|
|
36
|
+
--template, -t ${dim('api|web')} Alias for --api / --web
|
|
37
|
+
--db ${dim('<name>')} Database name ${dim('(default: snake_case(project-name))')}
|
|
38
|
+
--no-install Skip ${dim('bun install')} after scaffolding
|
|
39
|
+
-v, --version Print spring version and exit
|
|
40
|
+
-h, --help Show this help and exit
|
|
41
|
+
|
|
42
|
+
${bold('Examples:')}
|
|
43
|
+
bunx @strav/spring my-api --api
|
|
44
|
+
bunx @strav/spring my-app ${dim('# interactive prompt')}
|
|
45
|
+
bunx @strav/spring my-app --api --no-install
|
|
46
|
+
`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function main(): Promise<number> {
|
|
50
|
+
const args = parseArgs(process.argv.slice(2))
|
|
51
|
+
|
|
52
|
+
if (args.help) {
|
|
53
|
+
printUsage()
|
|
54
|
+
return 0
|
|
55
|
+
}
|
|
56
|
+
if (args.version) {
|
|
57
|
+
process.stdout.write(`${SPRING_VERSION}\n`)
|
|
58
|
+
return 0
|
|
59
|
+
}
|
|
60
|
+
if (args.projectName === undefined) {
|
|
61
|
+
printUsage()
|
|
62
|
+
return 1
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const dest = resolve(args.projectName)
|
|
66
|
+
if (existsSync(dest)) {
|
|
67
|
+
throw new SpringError(`directory already exists: ${dest}`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
process.stdout.write(`\n ${bold('@strav/spring')} ${dim(`v${SPRING_VERSION}`)}\n`)
|
|
71
|
+
process.stdout.write(` ${dim('Scaffolding a Strav app')}\n`)
|
|
72
|
+
|
|
73
|
+
let template: Template
|
|
74
|
+
if (args.template !== undefined) {
|
|
75
|
+
template = args.template
|
|
76
|
+
} else {
|
|
77
|
+
template = await select<Template>('Which template?', [
|
|
78
|
+
{ value: 'api', label: 'api', description: 'Headless REST template' },
|
|
79
|
+
{
|
|
80
|
+
value: 'web',
|
|
81
|
+
label: 'web',
|
|
82
|
+
description: 'Full-stack — pages auto-router + Vue islands + plain CSS',
|
|
83
|
+
},
|
|
84
|
+
])
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const dbName = args.dbName ?? (await input('Database name', toSnakeCase(args.projectName)))
|
|
88
|
+
|
|
89
|
+
process.stdout.write('\n')
|
|
90
|
+
const result = await scaffold({
|
|
91
|
+
projectName: args.projectName,
|
|
92
|
+
template,
|
|
93
|
+
dbName,
|
|
94
|
+
dest,
|
|
95
|
+
stravVersion: STRAV_VERSION,
|
|
96
|
+
})
|
|
97
|
+
process.stdout.write(` ${green('+')} wrote ${result.files.length} files into ${dest}\n`)
|
|
98
|
+
|
|
99
|
+
if (!args.noInstall) {
|
|
100
|
+
process.stdout.write(` ${dim('…')} installing dependencies\n`)
|
|
101
|
+
const proc = Bun.spawn(['bun', 'install'], {
|
|
102
|
+
cwd: dest,
|
|
103
|
+
stdout: 'ignore',
|
|
104
|
+
stderr: 'pipe',
|
|
105
|
+
})
|
|
106
|
+
const code = await proc.exited
|
|
107
|
+
if (code !== 0) {
|
|
108
|
+
const stderr = await new Response(proc.stderr).text()
|
|
109
|
+
throw new SpringError(`bun install failed (exit ${code}):\n${stderr}`)
|
|
110
|
+
}
|
|
111
|
+
process.stdout.write(` ${green('+')} installed dependencies\n`)
|
|
112
|
+
} else {
|
|
113
|
+
process.stdout.write(` ${dim('-')} skipped install (--no-install)\n`)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
process.stdout.write(`\n ${green('Done!')} Next steps:\n\n`)
|
|
117
|
+
process.stdout.write(` ${dim('$')} cd ${args.projectName}\n`)
|
|
118
|
+
if (args.noInstall) process.stdout.write(` ${dim('$')} bun install\n`)
|
|
119
|
+
process.stdout.write(` ${dim('$')} bun strav serve\n\n`)
|
|
120
|
+
process.stdout.write(` ${dim('Then open http://localhost:3000')}\n\n`)
|
|
121
|
+
return 0
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
process.exit(await main())
|
|
126
|
+
} catch (err) {
|
|
127
|
+
if (err instanceof SpringError) {
|
|
128
|
+
process.stderr.write(`\n ${red('✗')} ${err.message}\n\n`)
|
|
129
|
+
process.exit(1)
|
|
130
|
+
}
|
|
131
|
+
process.stderr.write(`\n ${red('✗ internal error')}\n`)
|
|
132
|
+
process.stderr.write(` ${err instanceof Error ? (err.stack ?? err.message) : String(err)}\n\n`)
|
|
133
|
+
process.exit(1)
|
|
134
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,176 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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('@strav/spring')} ${dim(`v${VERSION}`)}
|
|
61
|
-
${dim('The Rite of the Bun ecosystem')}
|
|
62
|
-
|
|
63
|
-
${bold('Usage:')}
|
|
64
|
-
bunx @strav/spring ${cyan('<project-name>')} [options]
|
|
65
|
-
|
|
66
|
-
${bold('Options:')}
|
|
67
|
-
--api Headless REST API template
|
|
68
|
-
--web Full-stack template with Vue islands and views
|
|
69
|
-
--template, -t ${dim('api|web')} Alias for --api / --web
|
|
70
|
-
--db ${dim('<name>')} Database name (default: project name)
|
|
71
|
-
-h, --help Show this help message
|
|
72
|
-
|
|
73
|
-
${bold('Examples:')}
|
|
74
|
-
bunx @strav/spring my-blog --web
|
|
75
|
-
bunx @strav/spring my-api --api
|
|
76
|
-
bunx @strav/spring my-app ${dim('# interactive prompt')}
|
|
77
|
-
`)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function toSnakeCase(name: string): string {
|
|
81
|
-
return name
|
|
82
|
-
.replace(/([A-Z])/g, '_$1')
|
|
83
|
-
.toLowerCase()
|
|
84
|
-
.replace(/[-\s]+/g, '_')
|
|
85
|
-
.replace(/^_+|_+$/g, '')
|
|
86
|
-
.replace(/_+/g, '_')
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// ── Main ────────────────────────────────────────────────────────────
|
|
90
|
-
|
|
91
|
-
async function main(): Promise<void> {
|
|
92
|
-
const args = parseArgs()
|
|
93
|
-
|
|
94
|
-
if (args.help) {
|
|
95
|
-
printUsage()
|
|
96
|
-
process.exit(0)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
console.log()
|
|
100
|
-
console.log(` ${bold('@strav/spring')} ${dim(`v${VERSION}`)}`)
|
|
101
|
-
console.log(` ${dim('The rite of the Bun ecosystem')}`)
|
|
102
|
-
console.log()
|
|
103
|
-
|
|
104
|
-
// Project name
|
|
105
|
-
if (!args.projectName) {
|
|
106
|
-
printUsage()
|
|
107
|
-
process.exit(1)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const projectName = args.projectName
|
|
111
|
-
const root = resolve(projectName)
|
|
112
|
-
|
|
113
|
-
// Validate
|
|
114
|
-
if (existsSync(root)) {
|
|
115
|
-
console.error(red(` Directory "${projectName}" already exists.`))
|
|
116
|
-
process.exit(1)
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (!/^[a-zA-Z0-9_-]+$/.test(projectName)) {
|
|
120
|
-
console.error(
|
|
121
|
-
red(` Invalid project name. Use only letters, numbers, hyphens, and underscores.`)
|
|
122
|
-
)
|
|
123
|
-
process.exit(1)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Template
|
|
127
|
-
let template = args.template
|
|
128
|
-
if (!template) {
|
|
129
|
-
template = (await select('Which template?', [
|
|
130
|
-
{ label: 'web', value: 'web', description: 'Full-stack with Vue islands, views, and sessions' },
|
|
131
|
-
{ label: 'api', value: 'api', description: 'Headless REST API with CORS enabled' },
|
|
132
|
-
])) as 'api' | 'web'
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Database name
|
|
136
|
-
const defaultDb = toSnakeCase(projectName)
|
|
137
|
-
const dbName = args.db ?? defaultDb
|
|
138
|
-
|
|
139
|
-
console.log()
|
|
140
|
-
|
|
141
|
-
// Scaffold
|
|
142
|
-
const opts: ScaffoldOptions = { projectName, template, dbName }
|
|
143
|
-
await scaffold(root, opts)
|
|
144
|
-
console.log(` ${green('+')} Scaffolded project files`)
|
|
145
|
-
|
|
146
|
-
// Install dependencies
|
|
147
|
-
console.log(` ${dim('...')} Installing dependencies`)
|
|
148
|
-
const install = Bun.spawn(['bun', 'install'], { cwd: root, stdout: 'ignore', stderr: 'pipe' })
|
|
149
|
-
const exitCode = await install.exited
|
|
150
|
-
|
|
151
|
-
if (exitCode !== 0) {
|
|
152
|
-
const stderr = await new Response(install.stderr).text()
|
|
153
|
-
console.error(red(` Failed to install dependencies:`))
|
|
154
|
-
console.error(dim(` ${stderr}`))
|
|
155
|
-
process.exit(1)
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
console.log(` ${green('+')} Installed dependencies`)
|
|
159
|
-
|
|
160
|
-
// Done
|
|
161
|
-
console.log()
|
|
162
|
-
console.log(` ${green('Project created successfully!')}`)
|
|
163
|
-
console.log()
|
|
164
|
-
console.log(` Next steps:`)
|
|
165
|
-
console.log()
|
|
166
|
-
console.log(` ${dim('$')} cd ${projectName}`)
|
|
167
|
-
console.log(` ${dim('$')} bun --hot index.ts`)
|
|
168
|
-
console.log()
|
|
169
|
-
console.log(` ${dim('Then open http://localhost:3000')}`)
|
|
170
|
-
console.log()
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
main().catch(err => {
|
|
174
|
-
console.error(red(` Error: ${err instanceof Error ? err.message : err}`))
|
|
175
|
-
process.exit(1)
|
|
176
|
-
})
|
|
1
|
+
/**
|
|
2
|
+
* Public barrel for `@strav/spring`. The package's primary surface is the
|
|
3
|
+
* CLI (`bin: @strav/spring`); the exports here are the programmatic API
|
|
4
|
+
* used by tests and any tool that wants to embed scaffolding.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { type ParsedArgs, parseArgs, type Template, toSnakeCase } from './args.ts'
|
|
8
|
+
export { type ScaffoldOptions, type ScaffoldResult, scaffold } from './scaffold.ts'
|
|
9
|
+
export { SpringError } from './spring_error.ts'
|
|
10
|
+
export { STRAV_VERSION } from './version.ts'
|