create-onion-lasagna-app 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/README.md +319 -0
- package/dist/index.js +662 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# create-onion-lasagna-app
|
|
2
|
+
|
|
3
|
+
Scaffold new onion-lasagna projects with a single command.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
bunx create-onion-lasagna-app my-app
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Interactive mode (recommended)
|
|
13
|
+
bunx create-onion-lasagna-app
|
|
14
|
+
|
|
15
|
+
# With project name
|
|
16
|
+
bunx create-onion-lasagna-app my-app
|
|
17
|
+
|
|
18
|
+
# Skip prompts with defaults
|
|
19
|
+
bunx create-onion-lasagna-app my-app --yes
|
|
20
|
+
|
|
21
|
+
# Full customization
|
|
22
|
+
bunx create-onion-lasagna-app my-app --structure simple -s simple-clean -v zod -f hono --use-pnpm
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## How It Works
|
|
26
|
+
|
|
27
|
+
```mermaid
|
|
28
|
+
flowchart LR
|
|
29
|
+
A[Run CLI] --> B{Interactive?}
|
|
30
|
+
B -->|Yes| C[Prompts]
|
|
31
|
+
B -->|No| D[Use Flags]
|
|
32
|
+
C --> E[Clone Starter]
|
|
33
|
+
D --> E
|
|
34
|
+
E --> F[Inject Dependencies]
|
|
35
|
+
F --> G[Create Config]
|
|
36
|
+
G --> H{Install?}
|
|
37
|
+
H -->|Yes| I[Install with PM]
|
|
38
|
+
H -->|No| J{Git?}
|
|
39
|
+
I --> J
|
|
40
|
+
J -->|Yes| K[git init + commit]
|
|
41
|
+
J -->|No| L[Done]
|
|
42
|
+
K --> L
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Options
|
|
46
|
+
|
|
47
|
+
| Flag | Alias | Description | Default |
|
|
48
|
+
| -------------- | ----- | -------------------------------------------------- | ----------- |
|
|
49
|
+
| `--structure` | - | Project structure: `simple`, `modules` | `simple` |
|
|
50
|
+
| `--starter` | `-s` | Starter template (filtered by structure) | Auto |
|
|
51
|
+
| `--validator` | `-v` | Validation: `zod`, `valibot`, `arktype`, `typebox` | `zod` |
|
|
52
|
+
| `--framework` | `-f` | Framework: `hono`, `elysia`, `fastify` | `hono` |
|
|
53
|
+
| `--use-bun` | - | Use bun package manager | Auto-detect |
|
|
54
|
+
| `--use-npm` | - | Use npm package manager | - |
|
|
55
|
+
| `--use-yarn` | - | Use yarn package manager | - |
|
|
56
|
+
| `--use-pnpm` | - | Use pnpm package manager | - |
|
|
57
|
+
| `--skip-git` | `-g` | Skip git initialization | `false` |
|
|
58
|
+
| `--no-install` | - | Skip dependency installation | `false` |
|
|
59
|
+
| `--dry-run` | `-d` | Preview what would be created | - |
|
|
60
|
+
| `--yes` | `-y` | Skip prompts, use defaults | - |
|
|
61
|
+
| `--version` | `-V` | Show version number | - |
|
|
62
|
+
| `--help` | `-h` | Show help | - |
|
|
63
|
+
|
|
64
|
+
## Package Manager
|
|
65
|
+
|
|
66
|
+
The CLI auto-detects your package manager based on how you invoke it:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
bunx create-onion-lasagna-app my-app # Uses bun
|
|
70
|
+
npx create-onion-lasagna-app my-app # Uses npm
|
|
71
|
+
pnpm create onion-lasagna-app my-app # Uses pnpm
|
|
72
|
+
yarn create onion-lasagna-app my-app # Uses yarn
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Override with explicit flags:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
bunx create-onion-lasagna-app my-app --use-pnpm
|
|
79
|
+
npx create-onion-lasagna-app my-app --use-bun
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Git Initialization
|
|
83
|
+
|
|
84
|
+
By default, the CLI initializes a git repository with an initial commit:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# Default: git init + initial commit
|
|
88
|
+
bunx create-onion-lasagna-app my-app
|
|
89
|
+
|
|
90
|
+
# Skip git initialization
|
|
91
|
+
bunx create-onion-lasagna-app my-app --skip-git
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Dry Run Mode
|
|
95
|
+
|
|
96
|
+
Preview what would be created without making any changes:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
bunx create-onion-lasagna-app my-app --dry-run
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Output includes:
|
|
103
|
+
|
|
104
|
+
- Project configuration summary
|
|
105
|
+
- Files that would be created
|
|
106
|
+
- Actions that would be performed
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
DRY RUN No changes will be made.
|
|
110
|
+
|
|
111
|
+
Project Configuration:
|
|
112
|
+
──────────────────────────────────────────────────
|
|
113
|
+
Name: my-app
|
|
114
|
+
Directory: /path/to/my-app
|
|
115
|
+
Structure: simple
|
|
116
|
+
Starter: simple-clean
|
|
117
|
+
...
|
|
118
|
+
|
|
119
|
+
Files that would be created:
|
|
120
|
+
──────────────────────────────────────────────────
|
|
121
|
+
+ my-app/
|
|
122
|
+
+ my-app/package.json
|
|
123
|
+
+ my-app/.onion-lasagna.json
|
|
124
|
+
...
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Project Name Validation
|
|
128
|
+
|
|
129
|
+
Project names follow npm package naming conventions:
|
|
130
|
+
|
|
131
|
+
| Rule | Invalid | Suggestion |
|
|
132
|
+
| --------------------------- | -------------- | ----------------- |
|
|
133
|
+
| Lowercase only | `MyApp` | `myapp` |
|
|
134
|
+
| No spaces | `my app` | `my-app` |
|
|
135
|
+
| No leading numbers | `123-app` | `app-123-app` |
|
|
136
|
+
| No leading dots/underscores | `_myapp` | `myapp` |
|
|
137
|
+
| No reserved names | `node_modules` | `my-node_modules` |
|
|
138
|
+
| Max 214 characters | (too long) | (truncated) |
|
|
139
|
+
|
|
140
|
+
Invalid names are caught early with helpful suggestions.
|
|
141
|
+
|
|
142
|
+
## Directory Conflict Handling
|
|
143
|
+
|
|
144
|
+
If the target directory exists and isn't empty, interactive mode offers:
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
? Directory "my-app" already exists and is not empty.
|
|
148
|
+
How would you like to proceed?
|
|
149
|
+
○ Overwrite - Remove existing files and continue
|
|
150
|
+
○ Choose a different name
|
|
151
|
+
○ Cancel
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
In non-interactive mode (`--yes`), existing non-empty directories cause an error.
|
|
155
|
+
|
|
156
|
+
## Structures & Starters
|
|
157
|
+
|
|
158
|
+
```mermaid
|
|
159
|
+
graph TD
|
|
160
|
+
subgraph Structures
|
|
161
|
+
S[simple] --> SC[simple-clean]
|
|
162
|
+
M[modules] --> MC[modules-clean]
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Simple Structure
|
|
167
|
+
|
|
168
|
+
Flat structure for small to medium projects.
|
|
169
|
+
|
|
170
|
+
| Starter | Description |
|
|
171
|
+
| -------------- | ----------------------------- |
|
|
172
|
+
| `simple-clean` | Minimal setup, ready to build |
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
my-app/
|
|
176
|
+
├── packages/
|
|
177
|
+
│ └── backend/
|
|
178
|
+
│ ├── bounded-contexts/
|
|
179
|
+
│ │ └── example/
|
|
180
|
+
│ ├── orchestrations/
|
|
181
|
+
│ └── shared/
|
|
182
|
+
├── .onion-lasagna.json
|
|
183
|
+
└── package.json
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Modules Structure
|
|
187
|
+
|
|
188
|
+
Module-based structure for large enterprise projects.
|
|
189
|
+
|
|
190
|
+
| Starter | Description |
|
|
191
|
+
| --------------- | ----------------------------- |
|
|
192
|
+
| `modules-clean` | Minimal setup, ready to build |
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
my-app/
|
|
196
|
+
├── packages/
|
|
197
|
+
│ ├── backend-modules/
|
|
198
|
+
│ │ ├── user-management/
|
|
199
|
+
│ │ ├── billing/
|
|
200
|
+
│ │ └── notifications/
|
|
201
|
+
│ └── backend-orchestrations/
|
|
202
|
+
├── .onion-lasagna.json
|
|
203
|
+
└── package.json
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Smart Starter Filtering
|
|
207
|
+
|
|
208
|
+
The CLI automatically filters starters based on your selected structure:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
# Only shows simple-* starters
|
|
212
|
+
bunx create-onion-lasagna-app my-app --structure simple
|
|
213
|
+
|
|
214
|
+
# Only shows modules-* starters
|
|
215
|
+
bunx create-onion-lasagna-app my-app --structure modules
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
If an incompatible starter is specified, the CLI will error:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
# Error: Starter "modules-clean" is not compatible with structure "simple"
|
|
222
|
+
bunx create-onion-lasagna-app my-app --structure simple -s modules-clean
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Validators
|
|
226
|
+
|
|
227
|
+
```mermaid
|
|
228
|
+
graph TD
|
|
229
|
+
subgraph Validators
|
|
230
|
+
Z[Zod] -->|Most Popular| V[Validation]
|
|
231
|
+
VB[Valibot] -->|Smallest Bundle| V
|
|
232
|
+
A[ArkType] -->|Fastest Runtime| V
|
|
233
|
+
T[TypeBox] -->|JSON Schema| V
|
|
234
|
+
end
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
| Library | Best For |
|
|
238
|
+
| ----------- | -------------------------------------------------- |
|
|
239
|
+
| **Zod** | TypeScript-first, great inference, large ecosystem |
|
|
240
|
+
| **Valibot** | Bundle size critical apps, tree-shakeable |
|
|
241
|
+
| **ArkType** | Performance critical, complex schemas |
|
|
242
|
+
| **TypeBox** | JSON Schema compatibility, OpenAPI |
|
|
243
|
+
|
|
244
|
+
## Frameworks
|
|
245
|
+
|
|
246
|
+
| Framework | Runtime | Best For |
|
|
247
|
+
| ----------- | --------------------------- | ------------------------------------------- |
|
|
248
|
+
| **Hono** | Any (Node, Bun, Deno, Edge) | Universal deployment |
|
|
249
|
+
| **Elysia** | Bun | Maximum performance, end-to-end type safety |
|
|
250
|
+
| **Fastify** | Node | Enterprise, large plugin ecosystem |
|
|
251
|
+
|
|
252
|
+
## Generated Files
|
|
253
|
+
|
|
254
|
+
After scaffolding, you'll find:
|
|
255
|
+
|
|
256
|
+
| File | Purpose |
|
|
257
|
+
| ------------------------------- | ------------------------------------------------------------------------- |
|
|
258
|
+
| `.onion-lasagna.json` | Project config (structure, starter, validator, framework, packageManager) |
|
|
259
|
+
| `.git/` | Initialized git repository with initial commit |
|
|
260
|
+
| `packages/backend/.env` | Environment variables |
|
|
261
|
+
| `packages/backend/.env.example` | Environment template |
|
|
262
|
+
|
|
263
|
+
## Examples
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
# Simple API with Hono + Zod (defaults)
|
|
267
|
+
bunx create-onion-lasagna-app api --yes
|
|
268
|
+
|
|
269
|
+
# Enterprise monolith with Fastify + Valibot + pnpm
|
|
270
|
+
bunx create-onion-lasagna-app platform --structure modules -v valibot -f fastify --use-pnpm
|
|
271
|
+
|
|
272
|
+
# High-performance Bun app with Elysia + ArkType, no git
|
|
273
|
+
bunx create-onion-lasagna-app service -v arktype -f elysia --skip-git
|
|
274
|
+
|
|
275
|
+
# CI/CD: npm, no install, no git
|
|
276
|
+
npx create-onion-lasagna-app test-app --yes --no-install --skip-git
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## After Scaffolding
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
cd my-app
|
|
283
|
+
bun run dev # Start development server
|
|
284
|
+
bun run build # Build for production
|
|
285
|
+
bun run test # Run tests
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Post-install instructions adapt to your selected package manager:
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
# If you used --use-pnpm
|
|
292
|
+
cd my-app
|
|
293
|
+
pnpm dev
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Configuration
|
|
297
|
+
|
|
298
|
+
The `.onion-lasagna.json` file stores your project settings:
|
|
299
|
+
|
|
300
|
+
```json
|
|
301
|
+
{
|
|
302
|
+
"structure": "simple",
|
|
303
|
+
"starter": "simple-clean",
|
|
304
|
+
"validator": "zod",
|
|
305
|
+
"framework": "hono",
|
|
306
|
+
"packageManager": "bun",
|
|
307
|
+
"createdAt": "2024-01-15T10:30:00.000Z"
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
This config is used by `onion-lasagna-cli` for code generation.
|
|
312
|
+
|
|
313
|
+
## Adding New Starters
|
|
314
|
+
|
|
315
|
+
New starters can be added to either structure. The naming convention is:
|
|
316
|
+
|
|
317
|
+
- `{structure}-{name}` (e.g., `simple-clean`, `modules-clean`)
|
|
318
|
+
|
|
319
|
+
The CLI will automatically pick them up and show them when the matching structure is selected.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import fs2 from "fs";
|
|
5
|
+
import path2 from "path";
|
|
6
|
+
import * as p from "@clack/prompts";
|
|
7
|
+
import pc from "picocolors";
|
|
8
|
+
|
|
9
|
+
// src/scaffold.ts
|
|
10
|
+
import { execSync } from "child_process";
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import path from "path";
|
|
13
|
+
import degit from "degit";
|
|
14
|
+
var RESERVED_NAMES = /* @__PURE__ */ new Set([
|
|
15
|
+
"node_modules",
|
|
16
|
+
"favicon.ico",
|
|
17
|
+
"package.json",
|
|
18
|
+
"package-lock.json",
|
|
19
|
+
"yarn.lock",
|
|
20
|
+
"pnpm-lock.yaml",
|
|
21
|
+
"bun.lockb",
|
|
22
|
+
".git",
|
|
23
|
+
".gitignore",
|
|
24
|
+
".env",
|
|
25
|
+
"src",
|
|
26
|
+
"dist",
|
|
27
|
+
"build",
|
|
28
|
+
"test",
|
|
29
|
+
"tests",
|
|
30
|
+
"lib",
|
|
31
|
+
"bin",
|
|
32
|
+
"npm",
|
|
33
|
+
"npx",
|
|
34
|
+
"node"
|
|
35
|
+
]);
|
|
36
|
+
function validateProjectName(name) {
|
|
37
|
+
if (!name || name.trim().length === 0) {
|
|
38
|
+
return { valid: false, error: "Project name cannot be empty" };
|
|
39
|
+
}
|
|
40
|
+
if (name.length > 214) {
|
|
41
|
+
return {
|
|
42
|
+
valid: false,
|
|
43
|
+
error: "Project name must be 214 characters or fewer",
|
|
44
|
+
suggestion: name.slice(0, 50)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (RESERVED_NAMES.has(name.toLowerCase())) {
|
|
48
|
+
return {
|
|
49
|
+
valid: false,
|
|
50
|
+
error: `"${name}" is a reserved name`,
|
|
51
|
+
suggestion: `my-${name}`
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (name.startsWith(".") || name.startsWith("_")) {
|
|
55
|
+
return {
|
|
56
|
+
valid: false,
|
|
57
|
+
error: "Project name cannot start with a dot or underscore",
|
|
58
|
+
suggestion: name.replace(/^[._]+/, "")
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (/^[0-9]/.test(name)) {
|
|
62
|
+
return {
|
|
63
|
+
valid: false,
|
|
64
|
+
error: "Project name cannot start with a number",
|
|
65
|
+
suggestion: `app-${name}`
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (/[A-Z]/.test(name)) {
|
|
69
|
+
return {
|
|
70
|
+
valid: false,
|
|
71
|
+
error: "Project name must be lowercase",
|
|
72
|
+
suggestion: name.toLowerCase()
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (/\s/.test(name)) {
|
|
76
|
+
return {
|
|
77
|
+
valid: false,
|
|
78
|
+
error: "Project name cannot contain spaces",
|
|
79
|
+
suggestion: name.replace(/\s+/g, "-")
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/.test(name)) {
|
|
83
|
+
const sanitized = name.toLowerCase().replace(/[^a-z0-9._-]/g, "-").replace(/^[._-]+/, "").replace(/-+/g, "-");
|
|
84
|
+
return {
|
|
85
|
+
valid: false,
|
|
86
|
+
error: "Project name can only contain lowercase letters, numbers, hyphens, and underscores",
|
|
87
|
+
suggestion: sanitized || "my-app"
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (/[._-]$/.test(name)) {
|
|
91
|
+
return {
|
|
92
|
+
valid: false,
|
|
93
|
+
error: "Project name cannot end with a dot, underscore, or hyphen",
|
|
94
|
+
suggestion: name.replace(/[._-]+$/, "")
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return { valid: true };
|
|
98
|
+
}
|
|
99
|
+
var STARTERS = {
|
|
100
|
+
"simple-clean": {
|
|
101
|
+
structure: "simple",
|
|
102
|
+
label: "Clean",
|
|
103
|
+
hint: "Minimal setup, ready to build",
|
|
104
|
+
repoPath: "simple-clean-starter"
|
|
105
|
+
},
|
|
106
|
+
"modules-clean": {
|
|
107
|
+
structure: "modules",
|
|
108
|
+
label: "Clean",
|
|
109
|
+
hint: "Minimal setup, ready to build",
|
|
110
|
+
repoPath: "modules-clean-starter"
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
var REPO = "Cosmneo/onion-lasagna";
|
|
114
|
+
var VALIDATOR_PACKAGES = {
|
|
115
|
+
zod: "zod",
|
|
116
|
+
valibot: "valibot",
|
|
117
|
+
arktype: "arktype",
|
|
118
|
+
typebox: "@sinclair/typebox"
|
|
119
|
+
};
|
|
120
|
+
var FRAMEWORK_PACKAGES = {
|
|
121
|
+
hono: ["hono"],
|
|
122
|
+
elysia: ["elysia"],
|
|
123
|
+
fastify: ["fastify"]
|
|
124
|
+
};
|
|
125
|
+
var INSTALL_COMMANDS = {
|
|
126
|
+
npm: "npm install",
|
|
127
|
+
yarn: "yarn",
|
|
128
|
+
pnpm: "pnpm install",
|
|
129
|
+
bun: "bun install"
|
|
130
|
+
};
|
|
131
|
+
function initGitRepository(targetDir) {
|
|
132
|
+
try {
|
|
133
|
+
execSync("git init", { cwd: targetDir, stdio: "ignore" });
|
|
134
|
+
execSync("git add -A", { cwd: targetDir, stdio: "ignore" });
|
|
135
|
+
execSync('git commit -m "Initial commit from create-onion-lasagna-app"', {
|
|
136
|
+
cwd: targetDir,
|
|
137
|
+
stdio: "ignore"
|
|
138
|
+
});
|
|
139
|
+
return true;
|
|
140
|
+
} catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async function scaffold(options) {
|
|
145
|
+
const { name, structure, starter, validator, framework, packageManager, install, skipGit } = options;
|
|
146
|
+
const targetDir = path.resolve(process.cwd(), name);
|
|
147
|
+
if (fs.existsSync(targetDir)) {
|
|
148
|
+
const files = fs.readdirSync(targetDir);
|
|
149
|
+
if (files.length > 0) {
|
|
150
|
+
throw new Error(`Directory "${name}" is not empty`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const starterConfig = STARTERS[starter];
|
|
154
|
+
if (!starterConfig) {
|
|
155
|
+
throw new Error(`Unknown starter: ${starter}`);
|
|
156
|
+
}
|
|
157
|
+
if (starterConfig.structure !== structure) {
|
|
158
|
+
throw new Error(`Starter "${starter}" is not compatible with structure "${structure}"`);
|
|
159
|
+
}
|
|
160
|
+
const starterPath = `${REPO}/starters/${starterConfig.repoPath}`;
|
|
161
|
+
const emitter = degit(starterPath, {
|
|
162
|
+
cache: false,
|
|
163
|
+
force: true,
|
|
164
|
+
verbose: false
|
|
165
|
+
});
|
|
166
|
+
await emitter.clone(targetDir);
|
|
167
|
+
const rootPackageJsonPath = path.join(targetDir, "package.json");
|
|
168
|
+
const rootPackageJson = JSON.parse(fs.readFileSync(rootPackageJsonPath, "utf-8"));
|
|
169
|
+
rootPackageJson.name = name;
|
|
170
|
+
fs.writeFileSync(rootPackageJsonPath, JSON.stringify(rootPackageJson, null, 2) + "\n");
|
|
171
|
+
const backendPackageJsonPath = path.join(targetDir, "packages", "backend", "package.json");
|
|
172
|
+
if (fs.existsSync(backendPackageJsonPath)) {
|
|
173
|
+
const backendPackageJson = JSON.parse(fs.readFileSync(backendPackageJsonPath, "utf-8"));
|
|
174
|
+
backendPackageJson.dependencies = backendPackageJson.dependencies || {};
|
|
175
|
+
backendPackageJson.dependencies[VALIDATOR_PACKAGES[validator]] = "latest";
|
|
176
|
+
for (const pkg of FRAMEWORK_PACKAGES[framework]) {
|
|
177
|
+
backendPackageJson.dependencies[pkg] = "latest";
|
|
178
|
+
}
|
|
179
|
+
fs.writeFileSync(backendPackageJsonPath, JSON.stringify(backendPackageJson, null, 2) + "\n");
|
|
180
|
+
}
|
|
181
|
+
const envExamplePath = path.join(targetDir, "packages", "backend", ".env.example");
|
|
182
|
+
const envContent = `# Environment variables
|
|
183
|
+
NODE_ENV=development
|
|
184
|
+
PORT=3000
|
|
185
|
+
`;
|
|
186
|
+
fs.writeFileSync(envExamplePath, envContent);
|
|
187
|
+
const envPath = path.join(targetDir, "packages", "backend", ".env");
|
|
188
|
+
fs.writeFileSync(envPath, envContent);
|
|
189
|
+
const configPath = path.join(targetDir, ".onion-lasagna.json");
|
|
190
|
+
const config = {
|
|
191
|
+
structure,
|
|
192
|
+
starter,
|
|
193
|
+
validator,
|
|
194
|
+
framework,
|
|
195
|
+
packageManager,
|
|
196
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
197
|
+
};
|
|
198
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
199
|
+
if (install) {
|
|
200
|
+
const installCmd = INSTALL_COMMANDS[packageManager];
|
|
201
|
+
execSync(installCmd, {
|
|
202
|
+
cwd: targetDir,
|
|
203
|
+
stdio: "ignore"
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
if (!skipGit) {
|
|
207
|
+
initGitRepository(targetDir);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/index.ts
|
|
212
|
+
var VERSION = "0.1.0";
|
|
213
|
+
function setupSignalHandlers() {
|
|
214
|
+
const cleanup = () => {
|
|
215
|
+
process.stdout.write("\x1B[?25h");
|
|
216
|
+
console.log("\n");
|
|
217
|
+
p.cancel("Operation cancelled.");
|
|
218
|
+
process.exit(0);
|
|
219
|
+
};
|
|
220
|
+
process.on("SIGINT", cleanup);
|
|
221
|
+
process.on("SIGTERM", cleanup);
|
|
222
|
+
}
|
|
223
|
+
function detectPackageManager() {
|
|
224
|
+
const userAgent = process.env.npm_config_user_agent || "";
|
|
225
|
+
if (userAgent.startsWith("yarn")) return "yarn";
|
|
226
|
+
if (userAgent.startsWith("pnpm")) return "pnpm";
|
|
227
|
+
if (userAgent.startsWith("bun")) return "bun";
|
|
228
|
+
if (userAgent.startsWith("npm")) return "npm";
|
|
229
|
+
return "bun";
|
|
230
|
+
}
|
|
231
|
+
function parseArgs(args) {
|
|
232
|
+
const result = {};
|
|
233
|
+
for (let i = 0; i < args.length; i++) {
|
|
234
|
+
const arg = args[i];
|
|
235
|
+
if (arg === "--help" || arg === "-h") {
|
|
236
|
+
result.help = true;
|
|
237
|
+
} else if (arg === "--version" || arg === "-V") {
|
|
238
|
+
result.version = true;
|
|
239
|
+
} else if (arg === "--yes" || arg === "-y") {
|
|
240
|
+
result.yes = true;
|
|
241
|
+
} else if (arg === "--no-install") {
|
|
242
|
+
result.install = false;
|
|
243
|
+
} else if (arg === "--skip-git" || arg === "-g") {
|
|
244
|
+
result.skipGit = true;
|
|
245
|
+
} else if (arg === "--dry-run" || arg === "-d") {
|
|
246
|
+
result.dryRun = true;
|
|
247
|
+
} else if (arg === "--use-npm") {
|
|
248
|
+
result.packageManager = "npm";
|
|
249
|
+
} else if (arg === "--use-yarn") {
|
|
250
|
+
result.packageManager = "yarn";
|
|
251
|
+
} else if (arg === "--use-pnpm") {
|
|
252
|
+
result.packageManager = "pnpm";
|
|
253
|
+
} else if (arg === "--use-bun") {
|
|
254
|
+
result.packageManager = "bun";
|
|
255
|
+
} else if (arg === "--structure") {
|
|
256
|
+
result.structure = args[++i];
|
|
257
|
+
} else if (arg === "--starter" || arg === "-s") {
|
|
258
|
+
result.starter = args[++i];
|
|
259
|
+
} else if (arg === "--validator" || arg === "-v") {
|
|
260
|
+
result.validator = args[++i];
|
|
261
|
+
} else if (arg === "--framework" || arg === "-f") {
|
|
262
|
+
result.framework = args[++i];
|
|
263
|
+
} else if (!arg?.startsWith("-") && !result.name) {
|
|
264
|
+
result.name = arg;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return result;
|
|
268
|
+
}
|
|
269
|
+
function showHelp() {
|
|
270
|
+
const simpleStarters = Object.entries(STARTERS).filter(([, s]) => s.structure === "simple").map(([key]) => key);
|
|
271
|
+
const modulesStarters = Object.entries(STARTERS).filter(([, s]) => s.structure === "modules").map(([key]) => key);
|
|
272
|
+
console.log(`
|
|
273
|
+
${pc.bold("create-onion-lasagna-app")} ${pc.dim(`v${VERSION}`)} - Scaffold a new onion-lasagna project
|
|
274
|
+
|
|
275
|
+
${pc.bold("Usage:")}
|
|
276
|
+
create-onion-lasagna-app [project-name] [options]
|
|
277
|
+
|
|
278
|
+
${pc.bold("Options:")}
|
|
279
|
+
--structure <type> Project structure: simple, modules (default: simple)
|
|
280
|
+
-s, --starter <name> Starter template (filtered by structure)
|
|
281
|
+
-v, --validator <lib> Validation library: zod, valibot, arktype, typebox (default: zod)
|
|
282
|
+
-f, --framework <fw> Web framework: hono, elysia, fastify (default: hono)
|
|
283
|
+
|
|
284
|
+
--use-bun Use bun package manager (default)
|
|
285
|
+
--use-npm Use npm package manager
|
|
286
|
+
--use-yarn Use yarn package manager
|
|
287
|
+
--use-pnpm Use pnpm package manager
|
|
288
|
+
|
|
289
|
+
-g, --skip-git Skip git initialization
|
|
290
|
+
--no-install Skip dependency installation
|
|
291
|
+
-d, --dry-run Show what would be created without making changes
|
|
292
|
+
-y, --yes Skip prompts and use defaults
|
|
293
|
+
|
|
294
|
+
-V, --version Show version number
|
|
295
|
+
-h, --help Show this help message
|
|
296
|
+
|
|
297
|
+
${pc.bold("Starters:")}
|
|
298
|
+
${pc.dim("simple structure:")} ${simpleStarters.join(", ")}
|
|
299
|
+
${pc.dim("modules structure:")} ${modulesStarters.join(", ")}
|
|
300
|
+
|
|
301
|
+
${pc.bold("Examples:")}
|
|
302
|
+
create-onion-lasagna-app my-app
|
|
303
|
+
create-onion-lasagna-app my-app --use-pnpm --skip-git
|
|
304
|
+
create-onion-lasagna-app my-app --structure modules -s modules-clean
|
|
305
|
+
create-onion-lasagna-app my-app --dry-run
|
|
306
|
+
create-onion-lasagna-app my-app --yes
|
|
307
|
+
`);
|
|
308
|
+
}
|
|
309
|
+
function getStartersForStructure(structure) {
|
|
310
|
+
return Object.entries(STARTERS).filter(([, s]) => s.structure === structure).map(([key, s]) => ({
|
|
311
|
+
value: key,
|
|
312
|
+
label: s.label,
|
|
313
|
+
hint: s.hint
|
|
314
|
+
}));
|
|
315
|
+
}
|
|
316
|
+
function getDefaultStarterForStructure(structure) {
|
|
317
|
+
const starters = Object.entries(STARTERS).filter(([, s]) => s.structure === structure);
|
|
318
|
+
return starters[0]?.[0];
|
|
319
|
+
}
|
|
320
|
+
function getRunCommand(pm) {
|
|
321
|
+
return pm === "npm" ? "npm run" : pm;
|
|
322
|
+
}
|
|
323
|
+
function getInstallCommand(pm) {
|
|
324
|
+
return pm === "yarn" ? "yarn" : `${pm} install`;
|
|
325
|
+
}
|
|
326
|
+
function showDryRunOutput(options) {
|
|
327
|
+
const targetDir = path2.resolve(process.cwd(), options.name);
|
|
328
|
+
console.log(`
|
|
329
|
+
${pc.bgYellow(pc.black(" DRY RUN "))} No changes will be made.
|
|
330
|
+
`);
|
|
331
|
+
console.log(`${pc.bold("Project Configuration:")}`);
|
|
332
|
+
console.log(pc.dim("\u2500".repeat(50)));
|
|
333
|
+
console.log(` ${pc.cyan("Name:")} ${options.name}`);
|
|
334
|
+
console.log(` ${pc.cyan("Directory:")} ${targetDir}`);
|
|
335
|
+
console.log(` ${pc.cyan("Structure:")} ${options.structure}`);
|
|
336
|
+
console.log(` ${pc.cyan("Starter:")} ${options.starter}`);
|
|
337
|
+
console.log(` ${pc.cyan("Validator:")} ${options.validator}`);
|
|
338
|
+
console.log(` ${pc.cyan("Framework:")} ${options.framework}`);
|
|
339
|
+
console.log(` ${pc.cyan("Package Manager:")} ${options.packageManager}`);
|
|
340
|
+
console.log(` ${pc.cyan("Install deps:")} ${options.install ? "yes" : "no"}`);
|
|
341
|
+
console.log(` ${pc.cyan("Git init:")} ${options.skipGit ? "no" : "yes"}`);
|
|
342
|
+
console.log(`
|
|
343
|
+
${pc.bold("Files that would be created:")}`);
|
|
344
|
+
console.log(pc.dim("\u2500".repeat(50)));
|
|
345
|
+
console.log(` ${pc.green("+")} ${options.name}/`);
|
|
346
|
+
console.log(` ${pc.green("+")} ${options.name}/package.json`);
|
|
347
|
+
console.log(` ${pc.green("+")} ${options.name}/.onion-lasagna.json`);
|
|
348
|
+
console.log(` ${pc.green("+")} ${options.name}/packages/backend/`);
|
|
349
|
+
console.log(` ${pc.green("+")} ${options.name}/packages/backend/package.json`);
|
|
350
|
+
console.log(` ${pc.green("+")} ${options.name}/packages/backend/.env`);
|
|
351
|
+
console.log(` ${pc.green("+")} ${options.name}/packages/backend/.env.example`);
|
|
352
|
+
if (!options.skipGit) {
|
|
353
|
+
console.log(` ${pc.green("+")} ${options.name}/.git/`);
|
|
354
|
+
}
|
|
355
|
+
console.log(`
|
|
356
|
+
${pc.bold("Actions that would be performed:")}`);
|
|
357
|
+
console.log(pc.dim("\u2500".repeat(50)));
|
|
358
|
+
console.log(` ${pc.blue("1.")} Clone starter template from GitHub`);
|
|
359
|
+
console.log(` ${pc.blue("2.")} Update package.json with project name`);
|
|
360
|
+
console.log(` ${pc.blue("3.")} Add ${options.validator} and ${options.framework} dependencies`);
|
|
361
|
+
console.log(` ${pc.blue("4.")} Create environment files`);
|
|
362
|
+
console.log(` ${pc.blue("5.")} Create .onion-lasagna.json config`);
|
|
363
|
+
if (options.install) {
|
|
364
|
+
console.log(` ${pc.blue("6.")} Run ${getInstallCommand(options.packageManager)}`);
|
|
365
|
+
}
|
|
366
|
+
if (!options.skipGit) {
|
|
367
|
+
console.log(` ${pc.blue(options.install ? "7." : "6.")} Initialize git repository`);
|
|
368
|
+
}
|
|
369
|
+
console.log(`
|
|
370
|
+
${pc.dim("Run without --dry-run to create the project.")}
|
|
371
|
+
`);
|
|
372
|
+
}
|
|
373
|
+
function showRichPostInstall(options) {
|
|
374
|
+
const { name, packageManager, install, skipGit } = options;
|
|
375
|
+
const run = getRunCommand(packageManager);
|
|
376
|
+
console.log(
|
|
377
|
+
`
|
|
378
|
+
${pc.green("Success!")} Created ${pc.bold(name)} at ${pc.dim(path2.resolve(process.cwd(), name))}`
|
|
379
|
+
);
|
|
380
|
+
console.log(`
|
|
381
|
+
${pc.bold("Inside that directory, you can run:")}`);
|
|
382
|
+
console.log(pc.dim("\u2500".repeat(50)));
|
|
383
|
+
console.log(`
|
|
384
|
+
${pc.cyan(`${run} dev`)}`);
|
|
385
|
+
console.log(` ${pc.dim("Start the development server")}`);
|
|
386
|
+
console.log(`
|
|
387
|
+
${pc.cyan(`${run} build`)}`);
|
|
388
|
+
console.log(` ${pc.dim("Build for production")}`);
|
|
389
|
+
console.log(`
|
|
390
|
+
${pc.cyan(`${run} test`)}`);
|
|
391
|
+
console.log(` ${pc.dim("Run tests")}`);
|
|
392
|
+
console.log(`
|
|
393
|
+
${pc.cyan(`${run} lint`)}`);
|
|
394
|
+
console.log(` ${pc.dim("Check for linting errors")}`);
|
|
395
|
+
console.log(`
|
|
396
|
+
${pc.bold("We suggest you begin by typing:")}`);
|
|
397
|
+
console.log(pc.dim("\u2500".repeat(50)));
|
|
398
|
+
console.log(`
|
|
399
|
+
${pc.cyan(`cd ${name}`)}`);
|
|
400
|
+
if (!install) {
|
|
401
|
+
console.log(` ${pc.cyan(getInstallCommand(packageManager))}`);
|
|
402
|
+
}
|
|
403
|
+
console.log(` ${pc.cyan(`${run} dev`)}`);
|
|
404
|
+
if (!skipGit) {
|
|
405
|
+
console.log(`
|
|
406
|
+
${pc.dim("A git repository has been initialized with an initial commit.")}`);
|
|
407
|
+
}
|
|
408
|
+
console.log("");
|
|
409
|
+
}
|
|
410
|
+
async function checkDirectoryConflict(name) {
|
|
411
|
+
const targetDir = path2.resolve(process.cwd(), name);
|
|
412
|
+
if (!fs2.existsSync(targetDir)) {
|
|
413
|
+
return "overwrite";
|
|
414
|
+
}
|
|
415
|
+
const files = fs2.readdirSync(targetDir);
|
|
416
|
+
if (files.length === 0) {
|
|
417
|
+
return "overwrite";
|
|
418
|
+
}
|
|
419
|
+
p.log.warn(`Directory ${pc.cyan(name)} already exists and is not empty.`);
|
|
420
|
+
const action = await p.select({
|
|
421
|
+
message: "How would you like to proceed?",
|
|
422
|
+
options: [
|
|
423
|
+
{ value: "overwrite", label: "Overwrite", hint: "Remove existing files and continue" },
|
|
424
|
+
{ value: "new-name", label: "Choose a different name", hint: "Enter a new project name" },
|
|
425
|
+
{ value: "cancel", label: "Cancel", hint: "Abort the operation" }
|
|
426
|
+
]
|
|
427
|
+
});
|
|
428
|
+
if (p.isCancel(action)) {
|
|
429
|
+
return "cancel";
|
|
430
|
+
}
|
|
431
|
+
return action;
|
|
432
|
+
}
|
|
433
|
+
function removeDirectory(dir) {
|
|
434
|
+
if (fs2.existsSync(dir)) {
|
|
435
|
+
fs2.rmSync(dir, { recursive: true, force: true });
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
async function main() {
|
|
439
|
+
setupSignalHandlers();
|
|
440
|
+
const args = parseArgs(process.argv.slice(2));
|
|
441
|
+
if (args.version) {
|
|
442
|
+
console.log(`create-onion-lasagna-app v${VERSION}`);
|
|
443
|
+
process.exit(0);
|
|
444
|
+
}
|
|
445
|
+
if (args.help) {
|
|
446
|
+
showHelp();
|
|
447
|
+
process.exit(0);
|
|
448
|
+
}
|
|
449
|
+
const detectedPm = detectPackageManager();
|
|
450
|
+
if (args.yes || args.name && args.structure && args.starter && args.validator && args.framework) {
|
|
451
|
+
const structure = args.structure || "simple";
|
|
452
|
+
const starter = args.starter || getDefaultStarterForStructure(structure);
|
|
453
|
+
const packageManager2 = args.packageManager || detectedPm;
|
|
454
|
+
const nameValidation = validateProjectName(args.name || "my-onion-app");
|
|
455
|
+
if (!nameValidation.valid) {
|
|
456
|
+
console.error(pc.red(`Invalid project name: ${nameValidation.error}`));
|
|
457
|
+
if (nameValidation.suggestion) {
|
|
458
|
+
console.error(pc.dim(`Suggestion: ${nameValidation.suggestion}`));
|
|
459
|
+
}
|
|
460
|
+
process.exit(1);
|
|
461
|
+
}
|
|
462
|
+
const starterConfig = STARTERS[starter];
|
|
463
|
+
if (starterConfig && starterConfig.structure !== structure) {
|
|
464
|
+
console.error(pc.red(`Starter "${starter}" is not compatible with structure "${structure}"`));
|
|
465
|
+
console.error(
|
|
466
|
+
pc.dim(
|
|
467
|
+
`Available starters for ${structure}: ${getStartersForStructure(structure).map((s2) => s2.value).join(", ")}`
|
|
468
|
+
)
|
|
469
|
+
);
|
|
470
|
+
process.exit(1);
|
|
471
|
+
}
|
|
472
|
+
const options = {
|
|
473
|
+
name: args.name || "my-onion-app",
|
|
474
|
+
structure,
|
|
475
|
+
starter,
|
|
476
|
+
validator: args.validator || "zod",
|
|
477
|
+
framework: args.framework || "hono",
|
|
478
|
+
packageManager: packageManager2,
|
|
479
|
+
install: args.install !== false,
|
|
480
|
+
skipGit: args.skipGit ?? false
|
|
481
|
+
};
|
|
482
|
+
if (args.dryRun) {
|
|
483
|
+
showDryRunOutput(options);
|
|
484
|
+
process.exit(0);
|
|
485
|
+
}
|
|
486
|
+
const targetDir = path2.resolve(process.cwd(), options.name);
|
|
487
|
+
if (fs2.existsSync(targetDir)) {
|
|
488
|
+
const files = fs2.readdirSync(targetDir);
|
|
489
|
+
if (files.length > 0) {
|
|
490
|
+
console.error(pc.red(`Directory "${options.name}" already exists and is not empty.`));
|
|
491
|
+
console.error(pc.dim("Use interactive mode or choose a different name."));
|
|
492
|
+
process.exit(1);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
console.log(pc.cyan(`
|
|
496
|
+
Creating ${options.name}...
|
|
497
|
+
`));
|
|
498
|
+
try {
|
|
499
|
+
await scaffold(options);
|
|
500
|
+
showRichPostInstall(options);
|
|
501
|
+
} catch (error) {
|
|
502
|
+
console.error(pc.red(error instanceof Error ? error.message : String(error)));
|
|
503
|
+
process.exit(1);
|
|
504
|
+
}
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
console.clear();
|
|
508
|
+
p.intro(pc.bgCyan(pc.black(` create-onion-lasagna-app v${VERSION} `)));
|
|
509
|
+
let projectName = args.name || "my-onion-app";
|
|
510
|
+
let needsNewName = false;
|
|
511
|
+
if (args.name) {
|
|
512
|
+
const nameValidation = validateProjectName(args.name);
|
|
513
|
+
if (!nameValidation.valid) {
|
|
514
|
+
p.log.warn(`Invalid project name: ${nameValidation.error}`);
|
|
515
|
+
if (nameValidation.suggestion) {
|
|
516
|
+
p.log.info(`Suggestion: ${nameValidation.suggestion}`);
|
|
517
|
+
}
|
|
518
|
+
needsNewName = true;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
if (!needsNewName && projectName) {
|
|
522
|
+
const conflictAction = await checkDirectoryConflict(projectName);
|
|
523
|
+
if (conflictAction === "cancel") {
|
|
524
|
+
p.cancel("Operation cancelled.");
|
|
525
|
+
process.exit(0);
|
|
526
|
+
} else if (conflictAction === "overwrite") {
|
|
527
|
+
const targetDir = path2.resolve(process.cwd(), projectName);
|
|
528
|
+
if (fs2.existsSync(targetDir) && fs2.readdirSync(targetDir).length > 0) {
|
|
529
|
+
removeDirectory(targetDir);
|
|
530
|
+
p.log.info(`Removed existing directory: ${projectName}`);
|
|
531
|
+
}
|
|
532
|
+
} else if (conflictAction === "new-name") {
|
|
533
|
+
needsNewName = true;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
const project = await p.group(
|
|
537
|
+
{
|
|
538
|
+
name: () => {
|
|
539
|
+
if (!needsNewName && args.name) {
|
|
540
|
+
return Promise.resolve(args.name);
|
|
541
|
+
}
|
|
542
|
+
return p.text({
|
|
543
|
+
message: "Project name",
|
|
544
|
+
placeholder: "my-onion-app",
|
|
545
|
+
defaultValue: needsNewName ? "" : projectName,
|
|
546
|
+
validate: (value) => {
|
|
547
|
+
if (!value) return "Project name is required";
|
|
548
|
+
const validation = validateProjectName(value);
|
|
549
|
+
if (!validation.valid) {
|
|
550
|
+
return validation.error + (validation.suggestion ? ` (try: ${validation.suggestion})` : "");
|
|
551
|
+
}
|
|
552
|
+
const targetDir = path2.resolve(process.cwd(), value);
|
|
553
|
+
if (fs2.existsSync(targetDir) && fs2.readdirSync(targetDir).length > 0) {
|
|
554
|
+
return `Directory "${value}" already exists and is not empty`;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
},
|
|
559
|
+
structure: () => p.select({
|
|
560
|
+
message: "Select project structure",
|
|
561
|
+
initialValue: args.structure,
|
|
562
|
+
options: [
|
|
563
|
+
{
|
|
564
|
+
value: "simple",
|
|
565
|
+
label: "Simple",
|
|
566
|
+
hint: "Flat structure, great for small to medium projects"
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
value: "modules",
|
|
570
|
+
label: "Modules",
|
|
571
|
+
hint: "Module-based structure for large enterprise projects"
|
|
572
|
+
}
|
|
573
|
+
]
|
|
574
|
+
}),
|
|
575
|
+
starter: ({ results }) => {
|
|
576
|
+
const structure = results.structure;
|
|
577
|
+
const starters = getStartersForStructure(structure);
|
|
578
|
+
if (starters.length === 1) {
|
|
579
|
+
return Promise.resolve(starters[0].value);
|
|
580
|
+
}
|
|
581
|
+
return p.select({
|
|
582
|
+
message: "Select a starter template",
|
|
583
|
+
initialValue: args.starter,
|
|
584
|
+
options: starters
|
|
585
|
+
});
|
|
586
|
+
},
|
|
587
|
+
validator: () => p.select({
|
|
588
|
+
message: "Select a validation library",
|
|
589
|
+
initialValue: args.validator,
|
|
590
|
+
options: [
|
|
591
|
+
{ value: "zod", label: "Zod", hint: "Most popular, great TypeScript inference" },
|
|
592
|
+
{ value: "valibot", label: "Valibot", hint: "Smallest bundle size" },
|
|
593
|
+
{ value: "arktype", label: "ArkType", hint: "Fastest runtime validation" },
|
|
594
|
+
{ value: "typebox", label: "TypeBox", hint: "JSON Schema compatible" }
|
|
595
|
+
]
|
|
596
|
+
}),
|
|
597
|
+
framework: () => p.select({
|
|
598
|
+
message: "Select a web framework",
|
|
599
|
+
initialValue: args.framework,
|
|
600
|
+
options: [
|
|
601
|
+
{ value: "hono", label: "Hono", hint: "Fast, lightweight, works everywhere" },
|
|
602
|
+
{ value: "elysia", label: "Elysia", hint: "Bun-optimized, end-to-end type safety" },
|
|
603
|
+
{ value: "fastify", label: "Fastify", hint: "Mature, plugin ecosystem" }
|
|
604
|
+
]
|
|
605
|
+
}),
|
|
606
|
+
packageManager: () => p.select({
|
|
607
|
+
message: "Select a package manager",
|
|
608
|
+
initialValue: args.packageManager || detectedPm,
|
|
609
|
+
options: [
|
|
610
|
+
{ value: "bun", label: "bun", hint: "Fast, all-in-one toolkit" },
|
|
611
|
+
{ value: "pnpm", label: "pnpm", hint: "Fast, disk space efficient" },
|
|
612
|
+
{ value: "npm", label: "npm", hint: "Node.js default package manager" },
|
|
613
|
+
{ value: "yarn", label: "yarn", hint: "Classic alternative to npm" }
|
|
614
|
+
]
|
|
615
|
+
}),
|
|
616
|
+
install: () => p.confirm({
|
|
617
|
+
message: "Install dependencies?",
|
|
618
|
+
initialValue: args.install !== false
|
|
619
|
+
})
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
onCancel: () => {
|
|
623
|
+
p.cancel("Operation cancelled.");
|
|
624
|
+
process.exit(0);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
);
|
|
628
|
+
const packageManager = project.packageManager;
|
|
629
|
+
const scaffoldOptions = {
|
|
630
|
+
name: project.name,
|
|
631
|
+
structure: project.structure,
|
|
632
|
+
starter: project.starter,
|
|
633
|
+
validator: project.validator,
|
|
634
|
+
framework: project.framework,
|
|
635
|
+
packageManager,
|
|
636
|
+
install: project.install,
|
|
637
|
+
skipGit: args.skipGit ?? false
|
|
638
|
+
};
|
|
639
|
+
if (args.dryRun) {
|
|
640
|
+
showDryRunOutput(scaffoldOptions);
|
|
641
|
+
p.outro(pc.dim("Dry run complete. No changes were made."));
|
|
642
|
+
process.exit(0);
|
|
643
|
+
}
|
|
644
|
+
const s = p.spinner();
|
|
645
|
+
try {
|
|
646
|
+
s.start("Scaffolding project...");
|
|
647
|
+
await scaffold(scaffoldOptions);
|
|
648
|
+
s.stop("Project scaffolded!");
|
|
649
|
+
showRichPostInstall({
|
|
650
|
+
name: project.name,
|
|
651
|
+
packageManager,
|
|
652
|
+
install: project.install,
|
|
653
|
+
skipGit: args.skipGit ?? false
|
|
654
|
+
});
|
|
655
|
+
p.outro(pc.green("Happy coding!"));
|
|
656
|
+
} catch (error) {
|
|
657
|
+
s.stop("Failed to scaffold project");
|
|
658
|
+
p.log.error(error instanceof Error ? error.message : String(error));
|
|
659
|
+
process.exit(1);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
main().catch(console.error);
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-onion-lasagna-app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI to scaffold new onion-lasagna projects",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Cosmneo",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Cosmneo/onion-lasagna.git",
|
|
11
|
+
"directory": "packages/create-onion-lasagna-app"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/Cosmneo/onion-lasagna/tree/main/packages/create-onion-lasagna-app#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/Cosmneo/onion-lasagna/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"cli",
|
|
19
|
+
"scaffold",
|
|
20
|
+
"onion-architecture",
|
|
21
|
+
"hexagonal-architecture",
|
|
22
|
+
"ddd",
|
|
23
|
+
"create-app",
|
|
24
|
+
"boilerplate",
|
|
25
|
+
"starter",
|
|
26
|
+
"typescript"
|
|
27
|
+
],
|
|
28
|
+
"bin": {
|
|
29
|
+
"create-onion-lasagna-app": "./dist/index.js"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsup",
|
|
36
|
+
"dev": "tsup --watch",
|
|
37
|
+
"start": "bun run src/index.ts"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@clack/prompts": "^0.9.1",
|
|
41
|
+
"degit": "^2.8.4",
|
|
42
|
+
"picocolors": "^1.1.1"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/degit": "^2.8.6",
|
|
46
|
+
"@types/node": "^22.10.2",
|
|
47
|
+
"tsup": "^8.5.1",
|
|
48
|
+
"typescript": "^5.8.3"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
}
|
|
53
|
+
}
|