better-ts-stack 2.0.2 → 2.1.1
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 +114 -113
- package/dist/args.js +27 -20
- package/dist/builder/buildPlanner.js +1 -1
- package/dist/builder/configGenerator.js +4 -6
- package/dist/builder/index.js +15 -3
- package/dist/builder/moduleSelector.js +6 -2
- package/dist/builder/readmeGenerator.js +107 -0
- package/dist/builder/templateContext.js +53 -0
- package/dist/index.js +7 -1
- package/dist/output/nextSteps.js +23 -22
- package/dist/prompts/index.js +5 -2
- package/dist/prompts/stack.js +23 -2
- package/dist/types/index.js +2 -0
- package/dist/ui/messages.js +3 -1
- package/dist/validators/index.js +5 -2
- package/package.json +3 -3
- package/templates/backend/express/config.json +42 -40
- package/templates/backend/express/gitignore +3 -0
- package/templates/backend/express/src/env.ts.hbs +39 -0
- package/templates/backend/express/src/index.ts.hbs +111 -75
- package/templates/backend/nestjs/config.json +43 -41
- package/templates/backend/nestjs/gitignore +3 -0
- package/templates/backend/nestjs/src/app.module.ts.hbs +13 -0
- package/templates/backend/nestjs/src/env.ts.hbs +39 -0
- package/templates/backend/nestjs/src/health/{health.controller.ts → health.controller.ts.hbs} +8 -0
- package/templates/backend/nestjs/src/main.ts +3 -2
- package/templates/frontend/nextjs/app/page.tsx.hbs +98 -97
- package/templates/frontend/nextjs/config.json +12 -14
- package/templates/frontend/nextjs/gitignore +3 -0
- package/templates/frontend/tanstack/config.json +16 -16
- package/templates/frontend/tanstack/gitignore +3 -0
- package/templates/frontend/tanstack/src/routes/index.tsx.hbs +102 -101
- package/templates/frontend/tanstack/vite.config.ts +23 -14
- package/templates/modules/auth/express/config.json +20 -20
- package/templates/modules/auth/express/src/middleware/requireAuth.ts.hbs +1 -2
- package/templates/modules/auth/express/src/routes/auth.ts.hbs +27 -19
- package/templates/modules/auth/nestjs/config.json +20 -20
- package/templates/modules/auth/nestjs/src/auth/{auth.controller.ts → auth.controller.ts.hbs} +15 -0
- package/templates/modules/auth/nextjs/config.json +4 -4
- package/templates/{frontend/nextjs/proxy.ts.hbs → modules/auth/nextjs/proxy.ts} +1 -8
- package/templates/modules/auth/tanstack/config.json +4 -4
- package/templates/modules/auth/tanstack/src/components/auth/sign-in-form.tsx +132 -134
- package/templates/modules/auth/tanstack/src/components/auth/sign-out-button.tsx +46 -48
- package/templates/modules/auth/tanstack/src/components/auth/sign-up-form.tsx +150 -152
- package/templates/modules/ci/.github/workflows/ci.yml.hbs +43 -0
- package/templates/modules/ci/config.json +10 -0
- package/templates/modules/docker/.dockerignore +2 -0
- package/templates/modules/docker/Dockerfile.hbs +147 -136
- package/templates/modules/docker/docker-compose.yml.hbs +97 -2
- package/templates/modules/drizzle/express/config.json +3 -3
- package/templates/modules/drizzle/nestjs/config.json +3 -3
- package/templates/modules/drizzle/nextjs/config.json +3 -3
- package/templates/modules/drizzle/tanstack/config.json +3 -3
- package/templates/modules/mongoose/express/config.json +2 -2
- package/templates/modules/mongoose/nestjs/config.json +1 -1
- package/templates/modules/mongoose/nextjs/config.json +2 -2
- package/templates/modules/mongoose/tanstack/config.json +2 -2
- package/templates/modules/prisma/express/config.json +4 -4
- package/templates/modules/prisma/express/prisma.config.ts +4 -2
- package/templates/modules/prisma/nestjs/config.json +4 -4
- package/templates/modules/prisma/nestjs/prisma.config.ts +4 -2
- package/templates/modules/prisma/nextjs/config.json +4 -4
- package/templates/modules/prisma/nextjs/prisma.config.ts +4 -2
- package/templates/modules/prisma/tanstack/config.json +4 -4
- package/templates/modules/prisma/tanstack/prisma.config.ts +4 -2
- package/templates/modules/rate-limit/express/config.json +12 -0
- package/templates/modules/rate-limit/express/src/middleware/rateLimit.ts +23 -0
- package/templates/modules/rate-limit/nestjs/config.json +12 -0
package/README.md
CHANGED
|
@@ -1,113 +1,114 @@
|
|
|
1
|
-
# better-ts-stack
|
|
2
|
-
|
|
3
|
-
> A CLI that scaffolds TypeScript backend and full-stack projects.
|
|
4
|
-
|
|
5
|
-
`better-ts-stack` creates an Express, NestJS, Next.js, or TanStack Start project with TypeScript, ESLint, and Prettier set up. Database, authentication, Docker, and git are optional.
|
|
6
|
-
|
|
7
|
-
## Quick Start
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
# Run instantly with npx
|
|
11
|
-
npx better-ts-stack
|
|
12
|
-
|
|
13
|
-
# Or install globally
|
|
14
|
-
npm install -g better-ts-stack
|
|
15
|
-
better-ts-stack
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
Run the command and answer the prompts. The CLI creates the project folder.
|
|
19
|
-
|
|
20
|
-
## Flags
|
|
21
|
-
|
|
22
|
-
Every prompt has a flag. Options you leave out are asked interactively, unless you pass `--yes`.
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
# No prompts at all
|
|
26
|
-
npx better-ts-stack my-api --framework nestjs --database prisma --auth --yes
|
|
27
|
-
|
|
28
|
-
# Create the project in the current (empty) directory
|
|
29
|
-
npx better-ts-stack . --framework nextjs --database drizzle --pm pnpm
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
| Flag
|
|
33
|
-
|
|
|
34
|
-
| `--framework <name>`
|
|
35
|
-
| `--database <name>`
|
|
36
|
-
| `--pm <name>`
|
|
37
|
-
| `--auth` / `--no-auth`
|
|
38
|
-
| `--docker` / `--no-docker`
|
|
39
|
-
| `--
|
|
40
|
-
| `--
|
|
41
|
-
|
|
|
42
|
-
| `--
|
|
43
|
-
|
|
|
44
|
-
| `-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
|
56
|
-
|
|
|
57
|
-
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
-
|
|
92
|
-
-
|
|
93
|
-
-
|
|
94
|
-
-
|
|
95
|
-
-
|
|
96
|
-
-
|
|
97
|
-
-
|
|
98
|
-
-
|
|
99
|
-
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
- [
|
|
109
|
-
- [
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
1
|
+
# better-ts-stack
|
|
2
|
+
|
|
3
|
+
> A CLI that scaffolds TypeScript backend and full-stack projects.
|
|
4
|
+
|
|
5
|
+
`better-ts-stack` creates an Express, NestJS, Next.js, or TanStack Start project with TypeScript, ESLint, and Prettier set up. Database, authentication, Docker, and git are optional.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Run instantly with npx
|
|
11
|
+
npx better-ts-stack
|
|
12
|
+
|
|
13
|
+
# Or install globally
|
|
14
|
+
npm install -g better-ts-stack
|
|
15
|
+
better-ts-stack
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Run the command and answer the prompts. The CLI creates the project folder.
|
|
19
|
+
|
|
20
|
+
## Flags
|
|
21
|
+
|
|
22
|
+
Every prompt has a flag. Options you leave out are asked interactively, unless you pass `--yes`.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# No prompts at all
|
|
26
|
+
npx better-ts-stack my-api --framework nestjs --database prisma --auth --yes
|
|
27
|
+
|
|
28
|
+
# Create the project in the current (empty) directory
|
|
29
|
+
npx better-ts-stack . --framework nextjs --database drizzle --pm pnpm
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
| Flag | Values |
|
|
33
|
+
| ---------------------------------- | -------------------------------------------- |
|
|
34
|
+
| `--framework <name>` | `express`, `nestjs`, `nextjs`, `tanstack` |
|
|
35
|
+
| `--database <name>` | `none`, `prisma`, `drizzle`, `mongoose` |
|
|
36
|
+
| `--pm <name>` | `npm`, `pnpm`, `bun` |
|
|
37
|
+
| `--auth` / `--no-auth` | Add authentication |
|
|
38
|
+
| `--docker` / `--no-docker` | Add Docker files |
|
|
39
|
+
| `--rate-limit` / `--no-rate-limit` | Add rate limiting (Express and NestJS) |
|
|
40
|
+
| `--git` / `--no-git` | Initialize a git repository |
|
|
41
|
+
| `--install` / `--no-install` | Install dependencies |
|
|
42
|
+
| `-y`, `--yes` | Use defaults for missing options, no prompts |
|
|
43
|
+
| `--dry-run` | List what would be created, write nothing |
|
|
44
|
+
| `-h`, `--help` | Show help |
|
|
45
|
+
| `-v`, `--version` | Show the version |
|
|
46
|
+
|
|
47
|
+
`--database` names the ORM, which also selects the database: `prisma` and `drizzle` use PostgreSQL, `mongoose` uses MongoDB.
|
|
48
|
+
|
|
49
|
+
With `--yes`, the defaults are Express, no database, no auth, no rate limiting, no Docker, git initialized, and dependencies not installed. The package manager defaults to the one that ran the CLI: `pnpm` for `pnpm dlx`, `bun` for `bunx`, and `npm` otherwise.
|
|
50
|
+
|
|
51
|
+
The project path can be a name (`my-app`), a nested path (`apps/api`), or `.` for the current directory.
|
|
52
|
+
|
|
53
|
+
## What It Can Scaffold
|
|
54
|
+
|
|
55
|
+
| Project type | Stack | Optional setup |
|
|
56
|
+
| -------------- | --------------------------------------- | ------------------------------------------------------------------------ |
|
|
57
|
+
| Backend API | Express 5 or NestJS 12 + TypeScript | PostgreSQL, MongoDB, Prisma, Drizzle, Mongoose, JWT auth, Docker, git |
|
|
58
|
+
| Full-stack app | Next.js 16 or TanStack Start + React 19 | PostgreSQL, MongoDB, Prisma, Drizzle, Mongoose, Better Auth, Docker, git |
|
|
59
|
+
|
|
60
|
+
Notes:
|
|
61
|
+
|
|
62
|
+
- Better Auth is only offered for full-stack projects that use a database.
|
|
63
|
+
|
|
64
|
+
## Interactive Prompt Flow
|
|
65
|
+
|
|
66
|
+
Without flags, the CLI asks, in order:
|
|
67
|
+
|
|
68
|
+
1. Project name or path
|
|
69
|
+
2. Application type: `Backend API` or `Full-stack App`
|
|
70
|
+
3. Framework
|
|
71
|
+
- Backend: `Express` or `NestJS`
|
|
72
|
+
- Full-stack: `Next.js` or `TanStack Start`
|
|
73
|
+
4. Database: `none`, `PostgreSQL`, or `MongoDB`
|
|
74
|
+
5. ORM/ODM when a database is selected
|
|
75
|
+
- PostgreSQL: `Prisma` or `Drizzle`
|
|
76
|
+
- MongoDB: `Mongoose`
|
|
77
|
+
6. Package manager: `npm`, `pnpm`, or `bun`
|
|
78
|
+
7. Docker setup
|
|
79
|
+
8. Authentication
|
|
80
|
+
- Express and NestJS apps: JWT-based auth
|
|
81
|
+
- Next.js and TanStack Start apps: Better Auth, only when a database is selected
|
|
82
|
+
9. Git initialization
|
|
83
|
+
10. Dependency installation
|
|
84
|
+
|
|
85
|
+
The CLI does not ask for a port. Backend projects default to `PORT=3000`.
|
|
86
|
+
|
|
87
|
+
## What You Get
|
|
88
|
+
|
|
89
|
+
Every generated project includes:
|
|
90
|
+
|
|
91
|
+
- TypeScript with strict mode enabled
|
|
92
|
+
- ESLint and Prettier pre-configured
|
|
93
|
+
- `.env.example` and `.env` setup
|
|
94
|
+
- Ready-to-run project scripts
|
|
95
|
+
- Health check endpoint at `/health` for backend projects
|
|
96
|
+
- shadcn-compatible UI components for Next.js and TanStack Start projects
|
|
97
|
+
- Database wiring and schema setup when selected
|
|
98
|
+
- Authentication scaffolding when selected
|
|
99
|
+
- Docker files when selected
|
|
100
|
+
- Optional git repository initialization
|
|
101
|
+
|
|
102
|
+
## Requirements
|
|
103
|
+
|
|
104
|
+
- Node.js 24 or later
|
|
105
|
+
|
|
106
|
+
## Links
|
|
107
|
+
|
|
108
|
+
- [Documentation](https://better-ts-stack.abdullahtech.me/docs)
|
|
109
|
+
- [GitHub Repository](https://github.com/Abdullah-dev0/better-ts-stack)
|
|
110
|
+
- [Issue Tracker](https://github.com/Abdullah-dev0/better-ts-stack/issues)
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
MIT
|
package/dist/args.js
CHANGED
|
@@ -11,26 +11,29 @@ const FRAMEWORKS = [
|
|
|
11
11
|
].map((option) => option.value);
|
|
12
12
|
const DATABASES = ["none", "prisma", "drizzle", "mongoose"];
|
|
13
13
|
const PACKAGE_MANAGERS = types_1.packageManagerOptions.map((option) => option.value);
|
|
14
|
-
exports.HELP_TEXT = `Usage: better-ts-stack [project-path] [options]
|
|
15
|
-
|
|
16
|
-
Options left out are asked interactively, unless --yes is given.
|
|
17
|
-
|
|
18
|
-
--framework <name> ${FRAMEWORKS.join(" | ")}
|
|
19
|
-
--database <name> ${DATABASES.join(" | ")}
|
|
20
|
-
--pm <name> ${PACKAGE_MANAGERS.join(" | ")}
|
|
21
|
-
--auth, --no-auth Add authentication
|
|
22
|
-
--docker, --no-docker Add Docker files
|
|
23
|
-
--
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
--
|
|
27
|
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
14
|
+
exports.HELP_TEXT = `Usage: better-ts-stack [project-path] [options]
|
|
15
|
+
|
|
16
|
+
Options left out are asked interactively, unless --yes is given.
|
|
17
|
+
|
|
18
|
+
--framework <name> ${FRAMEWORKS.join(" | ")}
|
|
19
|
+
--database <name> ${DATABASES.join(" | ")}
|
|
20
|
+
--pm <name> ${PACKAGE_MANAGERS.join(" | ")}
|
|
21
|
+
--auth, --no-auth Add authentication
|
|
22
|
+
--docker, --no-docker Add Docker files
|
|
23
|
+
--rate-limit, --no-rate-limit
|
|
24
|
+
Add rate limiting (Express and NestJS)
|
|
25
|
+
--ci, --no-ci Add a GitHub Actions workflow
|
|
26
|
+
--git, --no-git Initialize a git repository
|
|
27
|
+
--install, --no-install Install dependencies
|
|
28
|
+
-y, --yes Use defaults for missing options and skip confirmation
|
|
29
|
+
--dry-run List the files, dependencies, scripts, environment
|
|
30
|
+
variables, and commands without writing anything
|
|
31
|
+
-h, --help Show this help
|
|
32
|
+
-v, --version Show the version
|
|
33
|
+
|
|
34
|
+
Examples:
|
|
35
|
+
better-ts-stack my-api --framework nestjs --database prisma --auth --yes
|
|
36
|
+
better-ts-stack . --framework nextjs --database drizzle --pm pnpm
|
|
34
37
|
`;
|
|
35
38
|
function parseChoice(flag, value, allowed) {
|
|
36
39
|
if (value === undefined)
|
|
@@ -53,6 +56,8 @@ function parseCliArgs(argv) {
|
|
|
53
56
|
pm: { type: "string" },
|
|
54
57
|
auth: { type: "boolean" },
|
|
55
58
|
docker: { type: "boolean" },
|
|
59
|
+
"rate-limit": { type: "boolean" },
|
|
60
|
+
ci: { type: "boolean" },
|
|
56
61
|
git: { type: "boolean" },
|
|
57
62
|
install: { type: "boolean" },
|
|
58
63
|
yes: { type: "boolean", short: "y" },
|
|
@@ -75,6 +80,8 @@ function parseCliArgs(argv) {
|
|
|
75
80
|
packageManager: parseChoice("pm", values.pm, PACKAGE_MANAGERS),
|
|
76
81
|
useAuth: values.auth,
|
|
77
82
|
useDocker: values.docker,
|
|
83
|
+
useRateLimit: values["rate-limit"],
|
|
84
|
+
useCi: values.ci,
|
|
78
85
|
initGit: values.git,
|
|
79
86
|
installDeps: values.install,
|
|
80
87
|
};
|
|
@@ -15,7 +15,7 @@ const gitInitializer_1 = require("./gitInitializer");
|
|
|
15
15
|
const moduleSelector_1 = require("./moduleSelector");
|
|
16
16
|
const templateContext_1 = require("./templateContext");
|
|
17
17
|
// Files the builder writes itself instead of copying them from a module
|
|
18
|
-
const GENERATED_FILES = ["package.json", ".env", ".env.example"];
|
|
18
|
+
const GENERATED_FILES = ["package.json", ".env", ".env.example", "README.md"];
|
|
19
19
|
// Loads the selected modules and merges their configurations
|
|
20
20
|
async function loadBuildInputs(config) {
|
|
21
21
|
const selection = (0, moduleSelector_1.selectModules)(config);
|
|
@@ -69,11 +69,7 @@ async function generatePackageJson(targetDir, mergedConfig, config) {
|
|
|
69
69
|
name: config.projectName,
|
|
70
70
|
version: "1.0.0",
|
|
71
71
|
private: true,
|
|
72
|
-
|
|
73
|
-
// 24.x reports mismatched file stats there, so every asset 404s)
|
|
74
|
-
engines: {
|
|
75
|
-
node: config.framework === "tanstack" ? ">=24.20.0" : ">=24.0.0",
|
|
76
|
-
},
|
|
72
|
+
engines: { node: ">=24.0.0" },
|
|
77
73
|
description: `Project created with better-ts-stack using ${config.database !== "none" ? config.database : "no database"}`,
|
|
78
74
|
...(ESM_FRAMEWORKS.includes(config.framework) ? { type: "module" } : {}),
|
|
79
75
|
...(isFullstack ? {} : { main: BACKEND_ENTRY_FILES[config.framework] }),
|
|
@@ -140,7 +136,9 @@ async function generateEnvFile(targetDir, envVars) {
|
|
|
140
136
|
let envContent = "# Environment Variables\n";
|
|
141
137
|
envContent +=
|
|
142
138
|
"# Copy this file to .env and update with your actual values\n\n";
|
|
143
|
-
let localEnvContent =
|
|
139
|
+
let localEnvContent = "# Environment Variables\n";
|
|
140
|
+
localEnvContent +=
|
|
141
|
+
"# Local values for this project. Do not commit them\n\n";
|
|
144
142
|
// Add each environment variable
|
|
145
143
|
for (const [key, value] of Object.entries(envVars)) {
|
|
146
144
|
const resolvedValue = exports.GENERATED_SECRETS.includes(key)
|
package/dist/builder/index.js
CHANGED
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.planBuild = void 0;
|
|
7
7
|
exports.build = build;
|
|
8
8
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
10
|
const nextSteps_1 = require("../output/nextSteps");
|
|
10
11
|
const types_1 = require("../types");
|
|
11
12
|
const validators_1 = require("../validators");
|
|
@@ -14,8 +15,17 @@ const configGenerator_1 = require("./configGenerator");
|
|
|
14
15
|
const dependencyInstaller_1 = require("./dependencyInstaller");
|
|
15
16
|
const fileProcessor_1 = require("./fileProcessor");
|
|
16
17
|
const gitInitializer_1 = require("./gitInitializer");
|
|
18
|
+
const readmeGenerator_1 = require("./readmeGenerator");
|
|
17
19
|
var buildPlanner_2 = require("./buildPlanner");
|
|
18
20
|
Object.defineProperty(exports, "planBuild", { enumerable: true, get: function () { return buildPlanner_2.planBuild; } });
|
|
21
|
+
// Empties a directory except for the entries it may have had before the build
|
|
22
|
+
async function removeCreatedEntries(dir) {
|
|
23
|
+
for (const entry of await fs_extra_1.default.readdir(dir)) {
|
|
24
|
+
if (!validators_1.PRESERVED_ENTRIES.includes(entry)) {
|
|
25
|
+
await fs_extra_1.default.remove(node_path_1.default.join(dir, entry));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
19
29
|
// Main function to orchestrate the project construction process
|
|
20
30
|
async function build(config, targetDir) {
|
|
21
31
|
// 1. Validate target directory
|
|
@@ -42,8 +52,9 @@ async function build(config, targetDir) {
|
|
|
42
52
|
if (config.packageManager === "pnpm") {
|
|
43
53
|
await (0, configGenerator_1.generatePnpmWorkspace)(targetDir, mergedConfig.overrides);
|
|
44
54
|
}
|
|
45
|
-
// 7. Generate environment files
|
|
55
|
+
// 7. Generate environment files and the README
|
|
46
56
|
await (0, configGenerator_1.generateEnvFile)(targetDir, mergedConfig.envVars);
|
|
57
|
+
await (0, readmeGenerator_1.generateReadme)(targetDir, config, mergedConfig);
|
|
47
58
|
// 8. Install dependencies and initialize git (both optional); a failure
|
|
48
59
|
// in either is reported as a warning and does not fail the build
|
|
49
60
|
const depsInstalled = config.installDeps &&
|
|
@@ -57,8 +68,9 @@ async function build(config, targetDir) {
|
|
|
57
68
|
}
|
|
58
69
|
catch (error) {
|
|
59
70
|
// Remove the partial project so a retry is not blocked by a non-empty
|
|
60
|
-
// directory
|
|
61
|
-
|
|
71
|
+
// directory. A directory that already existed is emptied, not deleted,
|
|
72
|
+
// and keeps what it had before, such as .git
|
|
73
|
+
await (didDirectoryExist ? removeCreatedEntries(targetDir) : fs_extra_1.default.remove(targetDir)).catch(() => undefined);
|
|
62
74
|
// Errors raised by the build steps already say what failed
|
|
63
75
|
throw (0, types_1.isBuildError)(error)
|
|
64
76
|
? error
|
|
@@ -9,13 +9,17 @@ function selectModules(config) {
|
|
|
9
9
|
? `backend/${framework}`
|
|
10
10
|
: `frontend/${framework}`;
|
|
11
11
|
const modules = [];
|
|
12
|
-
// Database and
|
|
13
|
-
// auth/nextjs, ...); the Docker
|
|
12
|
+
// Database, auth, and rate-limit modules have one variant per framework
|
|
13
|
+
// (prisma/express, auth/nextjs, ...); the Docker and CI modules are shared
|
|
14
14
|
if (config.database !== "none")
|
|
15
15
|
modules.push(`${config.database}/${framework}`);
|
|
16
16
|
if (config.useDocker)
|
|
17
17
|
modules.push("docker");
|
|
18
18
|
if (config.useAuth)
|
|
19
19
|
modules.push(`auth/${framework}`);
|
|
20
|
+
if (config.useRateLimit)
|
|
21
|
+
modules.push(`rate-limit/${framework}`);
|
|
22
|
+
if (config.useCi)
|
|
23
|
+
modules.push("ci");
|
|
20
24
|
return { base, modules };
|
|
21
25
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.renderReadme = renderReadme;
|
|
7
|
+
exports.generateReadme = generateReadme;
|
|
8
|
+
// Writes the README.md of a generated project
|
|
9
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const nextSteps_1 = require("../output/nextSteps");
|
|
12
|
+
const types_1 = require("../types");
|
|
13
|
+
const configGenerator_1 = require("./configGenerator");
|
|
14
|
+
const DOCS_URL = "https://better-ts-stack.abdullahtech.me/docs";
|
|
15
|
+
const FRAMEWORK_LABELS = {
|
|
16
|
+
express: "Express 5",
|
|
17
|
+
nestjs: "NestJS 12",
|
|
18
|
+
nextjs: "Next.js 16 (App Router)",
|
|
19
|
+
tanstack: "TanStack Start",
|
|
20
|
+
};
|
|
21
|
+
const DATABASE_LABELS = {
|
|
22
|
+
prisma: "Prisma with PostgreSQL",
|
|
23
|
+
drizzle: "Drizzle with PostgreSQL",
|
|
24
|
+
mongoose: "Mongoose with MongoDB",
|
|
25
|
+
};
|
|
26
|
+
// Renders rows as a Markdown table; pipes inside cells are escaped
|
|
27
|
+
function table(headers, rows) {
|
|
28
|
+
const cell = (text) => text.replace(/\|/g, "\\|");
|
|
29
|
+
return [
|
|
30
|
+
`| ${headers.join(" | ")} |`,
|
|
31
|
+
`| ${headers.map(() => "---").join(" | ")} |`,
|
|
32
|
+
...rows.map((row) => `| ${row.map(cell).join(" | ")} |`),
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
// Builds the README content from the project's choices and merged config
|
|
36
|
+
function renderReadme(config, mergedConfig, targetDir) {
|
|
37
|
+
const isFullstack = config.applicationType === "fullstack";
|
|
38
|
+
const lines = [
|
|
39
|
+
`# ${config.projectName}`,
|
|
40
|
+
"",
|
|
41
|
+
`Created with [better-ts-stack](${DOCS_URL}).`,
|
|
42
|
+
"",
|
|
43
|
+
"## Stack",
|
|
44
|
+
"",
|
|
45
|
+
`- **Framework:** ${FRAMEWORK_LABELS[config.framework]}`,
|
|
46
|
+
];
|
|
47
|
+
if (config.database !== "none") {
|
|
48
|
+
lines.push(`- **Database:** ${DATABASE_LABELS[config.database]}`);
|
|
49
|
+
}
|
|
50
|
+
if (config.useAuth) {
|
|
51
|
+
lines.push(isFullstack
|
|
52
|
+
? "- **Auth:** Better Auth with email and password"
|
|
53
|
+
: "- **Auth:** JWT with `/auth/register`, `/auth/login`, and `/auth/me`");
|
|
54
|
+
}
|
|
55
|
+
if (config.useRateLimit) {
|
|
56
|
+
lines.push(`- **Rate limiting:** 100 requests per client IP in 15 minutes${config.useAuth ? ", and 10 sign-up or login attempts" : ""}`);
|
|
57
|
+
}
|
|
58
|
+
if (config.useDocker)
|
|
59
|
+
lines.push("- **Docker:** Dockerfile and Compose file");
|
|
60
|
+
if (config.useCi) {
|
|
61
|
+
lines.push("- **CI:** GitHub Actions builds, type-checks, and lints every push to `main` and every pull request");
|
|
62
|
+
}
|
|
63
|
+
lines.push(`- **Package manager:** ${config.packageManager}`, "");
|
|
64
|
+
const steps = (0, nextSteps_1.generateNextSteps)(config, targetDir, false, true);
|
|
65
|
+
lines.push("## Getting started", "", ...steps.map((step, index) => {
|
|
66
|
+
const isCommand = /^(npm|pnpm|bun|docker) /.test(step);
|
|
67
|
+
return `${index + 1}. ${isCommand ? `\`${step}\`` : step}`;
|
|
68
|
+
}), "");
|
|
69
|
+
// "pre" and "post" hooks run on their own, so only the commands to type
|
|
70
|
+
// are listed
|
|
71
|
+
const scripts = Object.entries(mergedConfig.scripts).filter(([name]) => !/^(pre|post)./.test(name));
|
|
72
|
+
if (scripts.length > 0) {
|
|
73
|
+
lines.push("## Scripts", "", ...table(["Command", "Runs"], scripts.map(([name, command]) => [
|
|
74
|
+
`\`${config.packageManager} run ${name}\``,
|
|
75
|
+
`\`${command}\``,
|
|
76
|
+
])), "");
|
|
77
|
+
}
|
|
78
|
+
const envVars = Object.entries(mergedConfig.envVars);
|
|
79
|
+
if (envVars.length > 0) {
|
|
80
|
+
lines.push("## Environment variables", "", "`.env` holds your local values and is not committed. `.env.example` lists the same variables for other developers.", "", ...table(["Variable", "Default"], envVars.map(([key, value]) => [
|
|
81
|
+
`\`${key}\``,
|
|
82
|
+
configGenerator_1.GENERATED_SECRETS.includes(key)
|
|
83
|
+
? "A random value generated in `.env`"
|
|
84
|
+
: `\`${value}\``,
|
|
85
|
+
])), "");
|
|
86
|
+
}
|
|
87
|
+
lines.push("## Learn more", "", `- [${isFullstack ? "Full-stack" : "Backend"} guide](${DOCS_URL}/modules/${isFullstack ? "frontend" : "backend"})`);
|
|
88
|
+
if (config.database !== "none") {
|
|
89
|
+
lines.push(`- [Database guide](${DOCS_URL}/modules/database)`);
|
|
90
|
+
}
|
|
91
|
+
if (config.useAuth)
|
|
92
|
+
lines.push(`- [Auth guide](${DOCS_URL}/modules/auth)`);
|
|
93
|
+
if (config.useDocker) {
|
|
94
|
+
lines.push(`- [Docker guide](${DOCS_URL}/modules/docker)`);
|
|
95
|
+
}
|
|
96
|
+
lines.push(`- [Troubleshooting](${DOCS_URL}/troubleshooting)`, "");
|
|
97
|
+
return lines.join("\n");
|
|
98
|
+
}
|
|
99
|
+
// Writes README.md into the project
|
|
100
|
+
async function generateReadme(targetDir, config, mergedConfig) {
|
|
101
|
+
try {
|
|
102
|
+
await fs_extra_1.default.writeFile(node_path_1.default.join(targetDir, "README.md"), renderReadme(config, mergedConfig, targetDir), "utf-8");
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
throw (0, types_1.buildError)(error, "README_ERROR", "Failed to generate README.md");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -3,6 +3,56 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.buildTemplateContext = buildTemplateContext;
|
|
4
4
|
// Context builder for Handlebars, providing variables and helpers for templates
|
|
5
5
|
const types_1 = require("../types");
|
|
6
|
+
// The lockfile is optional: a project built before its first install has
|
|
7
|
+
// none, so each install falls back to a plain install without it
|
|
8
|
+
const DOCKER_COMMANDS = {
|
|
9
|
+
npm: {
|
|
10
|
+
setup: "",
|
|
11
|
+
manifests: "package.json package-lock.json*",
|
|
12
|
+
install: "if [ -f package-lock.json ]; then npm ci; else npm install; fi",
|
|
13
|
+
installProduction: "if [ -f package-lock.json ]; then npm ci --omit=dev; else npm install --omit=dev; fi",
|
|
14
|
+
cleanCache: "npm cache clean --force",
|
|
15
|
+
run: "npm run",
|
|
16
|
+
},
|
|
17
|
+
pnpm: {
|
|
18
|
+
setup: "RUN npm install -g pnpm@11",
|
|
19
|
+
manifests: "package.json pnpm-lock.yaml* pnpm-workspace.yaml",
|
|
20
|
+
install: "if [ -f pnpm-lock.yaml ]; then pnpm install --frozen-lockfile; else pnpm install; fi",
|
|
21
|
+
installProduction: "if [ -f pnpm-lock.yaml ]; then pnpm install --prod --frozen-lockfile; else pnpm install --prod; fi",
|
|
22
|
+
cleanCache: "pnpm store prune",
|
|
23
|
+
run: "pnpm run",
|
|
24
|
+
},
|
|
25
|
+
bun: {
|
|
26
|
+
setup: "COPY --from=oven/bun:1-alpine /usr/local/bin/bun /usr/local/bin/bun",
|
|
27
|
+
manifests: "package.json bun.lock*",
|
|
28
|
+
install: "if [ -f bun.lock ]; then bun install --frozen-lockfile; else bun install; fi",
|
|
29
|
+
installProduction: "if [ -f bun.lock ]; then bun install --production --frozen-lockfile; else bun install --production; fi",
|
|
30
|
+
cleanCache: "rm -rf /root/.bun/install/cache",
|
|
31
|
+
run: "bun run",
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
// Placeholders for the CI build, the same ones the Dockerfile uses: a build
|
|
35
|
+
// needs these to be set, but never connects to a database
|
|
36
|
+
function getCiBuildEnv(config) {
|
|
37
|
+
const env = [];
|
|
38
|
+
if (config.database === "prisma" || config.database === "drizzle") {
|
|
39
|
+
env.push({
|
|
40
|
+
name: "DATABASE_URL",
|
|
41
|
+
value: "postgresql://postgres:postgres@localhost:5432/postgres",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (config.database === "mongoose") {
|
|
45
|
+
env.push({ name: "MONGODB_URI", value: "mongodb://localhost:27017/build" });
|
|
46
|
+
}
|
|
47
|
+
// next build loads the auth route, which initializes Better Auth
|
|
48
|
+
if (config.framework === "nextjs" && config.useAuth) {
|
|
49
|
+
env.push({
|
|
50
|
+
name: "BETTER_AUTH_SECRET",
|
|
51
|
+
value: "build-only-placeholder-secret-not-used-at-runtime",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return env;
|
|
55
|
+
}
|
|
6
56
|
function buildTemplateContext(config) {
|
|
7
57
|
return {
|
|
8
58
|
projectName: config.projectName,
|
|
@@ -12,6 +62,9 @@ function buildTemplateContext(config) {
|
|
|
12
62
|
port: types_1.DEFAULT_PORT,
|
|
13
63
|
useDocker: config.useDocker,
|
|
14
64
|
useAuth: config.useAuth,
|
|
65
|
+
useRateLimit: config.useRateLimit,
|
|
66
|
+
docker: DOCKER_COMMANDS[config.packageManager],
|
|
67
|
+
ciBuildEnv: getCiBuildEnv(config),
|
|
15
68
|
helpers: {
|
|
16
69
|
eq: (a, b) => a === b,
|
|
17
70
|
runner: () => {
|
package/dist/index.js
CHANGED
|
@@ -32,7 +32,13 @@ async function main() {
|
|
|
32
32
|
const directoryError = await (0, validators_1.validateDirectoryEmpty)(targetDir);
|
|
33
33
|
if (directoryError)
|
|
34
34
|
throw (0, types_1.buildError)(directoryError, "DIRECTORY_NOT_EMPTY");
|
|
35
|
-
const choices = await (0, prompts_1.collectStackChoices)(args.yes
|
|
35
|
+
const choices = await (0, prompts_1.collectStackChoices)(args.yes
|
|
36
|
+
? {
|
|
37
|
+
...types_1.DEFAULT_CHOICES,
|
|
38
|
+
packageManager: (0, prompts_1.detectPackageManager)(),
|
|
39
|
+
...args.choices,
|
|
40
|
+
}
|
|
41
|
+
: args.choices);
|
|
36
42
|
const config = {
|
|
37
43
|
projectName: node_path_1.default.basename(targetDir),
|
|
38
44
|
applicationType: (0, types_1.getApplicationType)(choices.framework),
|