create-restack-pattern 0.1.8 → 0.2.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/dist/index.js +240 -67
- package/package.json +14 -8
- package/template/{DEVELOPMENT.md → base/DEVELOPMENT.md} +73 -62
- package/template/{backend → base/backend}/package.json +13 -10
- package/template/{backend → base/backend}/src/config/env.ts +2 -2
- package/template/{backend → base/backend}/src/domains/auth/auth.model.ts +5 -3
- package/template/{backend → base/backend}/src/domains/auth/auth.repo.ts +23 -5
- package/template/{backend → base/backend}/src/domains/auth/auth.routes.ts +77 -22
- package/template/{backend → base/backend}/src/domains/auth/auth.service.test.ts +51 -19
- package/template/{backend → base/backend}/src/domains/auth/auth.service.ts +15 -10
- package/template/{backend → base/backend}/src/domains/auth/auth.types.ts +1 -1
- package/template/{backend → base/backend}/src/domains/user/user.model.ts +2 -1
- package/template/{backend → base/backend}/src/domains/user/user.repo.ts +3 -2
- package/template/base/backend/src/domains/user/user.routes.test.ts +127 -0
- package/template/{backend → base/backend}/src/domains/user/user.routes.ts +19 -9
- package/template/{backend → base/backend}/src/domains/user/user.service.ts +8 -2
- package/template/{backend → base/backend}/src/hono-app.ts +6 -11
- package/template/base/backend/src/infra/middlewares/require-auth.test.ts +91 -0
- package/template/base/backend/src/infra/middlewares/require-auth.ts +63 -0
- package/template/base/backend/src/infra/middlewares/require-role.test.ts +66 -0
- package/template/base/backend/src/infra/middlewares/require-role.ts +34 -0
- package/template/{backend → base/backend}/src/utils/logger.ts +7 -10
- package/template/base/backend/src/utils/uuid.test.ts +37 -0
- package/template/{backend → base/backend}/tsconfig.json +4 -0
- package/template/base/frontend/AGENTS.md +11 -0
- package/template/{frontend → base/frontend}/app/(auth)/login/page.tsx +1 -1
- package/template/{frontend → base/frontend}/app/(auth)/register/page.tsx +1 -1
- package/template/{frontend → base/frontend}/app/globals.css +1 -1
- package/template/{frontend → base/frontend}/app/layout.tsx +9 -12
- package/template/{frontend → base/frontend}/app/page.tsx +2 -2
- package/template/{frontend → base/frontend}/app/profile/page.tsx +3 -3
- package/template/{frontend → base/frontend}/components/ui/button.tsx +2 -2
- package/template/{frontend → base/frontend}/components/ui/card.tsx +8 -23
- package/template/{frontend → base/frontend}/components/ui/input.tsx +2 -2
- package/template/{frontend → base/frontend}/components/ui/label.tsx +3 -6
- package/template/{frontend → base/frontend}/domains/auth/components/login-form.tsx +10 -3
- package/template/{frontend → base/frontend}/domains/auth/components/logout-button.tsx +2 -2
- package/template/{frontend → base/frontend}/domains/auth/components/register-form.tsx +10 -3
- package/template/{frontend → base/frontend}/domains/auth/hooks/use-login.ts +2 -2
- package/template/{frontend → base/frontend}/domains/auth/hooks/use-register.ts +6 -3
- package/template/{frontend → base/frontend}/domains/auth/services/auth.api.ts +5 -7
- package/template/{frontend → base/frontend}/domains/user/components/profile-card.tsx +2 -4
- package/template/{frontend → base/frontend}/domains/user/services/user.api.ts +2 -3
- package/template/base/frontend/env.ts +12 -0
- package/template/{frontend → base/frontend}/hooks/use-auth-mutation.ts +6 -3
- package/template/{frontend → base/frontend}/lib/api-client.ts +40 -5
- package/template/{frontend → base/frontend}/lib/utils.ts +1 -1
- package/template/base/frontend/next.config.ts +11 -0
- package/template/{frontend → base/frontend}/package.json +15 -14
- package/template/{frontend → base/frontend}/tsconfig.json +4 -0
- package/template/{gitignore → base/gitignore} +15 -0
- package/template/base/package.json +25 -0
- package/template/{packages → base/packages}/shared/package.json +1 -1
- package/template/{packages → base/packages}/shared/src/index.ts +1 -0
- package/template/{packages → base/packages}/shared/src/schemas/auth.schema.ts +8 -0
- package/template/{packages → base/packages}/shared/src/schemas/user.schema.ts +1 -0
- package/template/base/packages/shared/src/utils/uuid.ts +40 -0
- package/template/{packages → base/packages}/shared/tsconfig.json +26 -0
- package/template/base/pnpm-workspace.yaml +12 -0
- package/template/overlays/db-postgres/backend/.env.example +6 -0
- package/template/overlays/db-postgres/backend/drizzle.config.ts +11 -0
- package/template/overlays/db-postgres/backend/scripts/db-check.js +27 -0
- package/template/overlays/db-postgres/backend/src/infra/db/index.ts +6 -0
- package/template/overlays/db-sqlite/backend/.env.example +6 -0
- package/template/overlays/db-sqlite/backend/drizzle.config.ts +11 -0
- package/template/overlays/db-sqlite/backend/scripts/db-check.js +20 -0
- package/template/overlays/db-sqlite/backend/src/infra/db/index.ts +8 -0
- package/template/overlays/id-serial/packages/shared/src/schemas/user.schema.ts +14 -0
- package/template/overlays/id-uuid/packages/shared/src/schemas/user.schema.ts +14 -0
- package/template/overlays/models/postgres-serial/backend/src/domains/auth/auth.model.ts +12 -0
- package/template/overlays/models/postgres-serial/backend/src/domains/user/user.model.ts +10 -0
- package/template/overlays/models/postgres-uuid/backend/src/domains/auth/auth.model.ts +15 -0
- package/template/overlays/models/postgres-uuid/backend/src/domains/user/user.model.ts +13 -0
- package/template/overlays/models/sqlite-serial/backend/src/domains/auth/auth.model.ts +14 -0
- package/template/overlays/models/sqlite-serial/backend/src/domains/user/user.model.ts +12 -0
- package/template/overlays/models/sqlite-uuid/backend/src/domains/auth/auth.model.ts +17 -0
- package/template/overlays/models/sqlite-uuid/backend/src/domains/user/user.model.ts +15 -0
- package/template/backend/src/domains/user/user.routes.test.ts +0 -82
- package/template/backend/src/infra/middlewares/require-auth.test.ts +0 -51
- package/template/backend/src/infra/middlewares/require-auth.ts +0 -36
- package/template/frontend/AGENTS.md +0 -5
- package/template/frontend/next.config.ts +0 -7
- package/template/package.json +0 -17
- package/template/pnpm-workspace.yaml +0 -4
- package/template/{backend → base/backend}/.env.example +0 -0
- package/template/{backend → base/backend}/README.md +0 -0
- package/template/{backend → base/backend}/api/index.ts +0 -0
- package/template/{backend → base/backend}/drizzle/0000_grey_lethal_legion.sql +0 -0
- package/template/{backend → base/backend}/drizzle/meta/0000_snapshot.json +0 -0
- package/template/{backend → base/backend}/drizzle/meta/_journal.json +0 -0
- package/template/{backend → base/backend}/drizzle.config.ts +0 -0
- package/template/{backend → base/backend}/gitignore +0 -0
- package/template/{backend → base/backend}/public/.gitkeep +0 -0
- package/template/{backend → base/backend}/scripts/db-check.js +0 -0
- package/template/{backend → base/backend}/src/config/.gitkeep +0 -0
- package/template/{backend → base/backend}/src/domains/auth/.gitkeep +0 -0
- package/template/{backend → base/backend}/src/domains/user/.gitkeep +0 -0
- package/template/{backend → base/backend}/src/index.ts +1 -1
- package/template/{backend → base/backend}/src/infra/db/.gitkeep +0 -0
- package/template/{backend → base/backend}/src/infra/db/index.ts +0 -0
- package/template/{backend → base/backend}/src/infra/middlewares/.gitkeep +0 -0
- package/template/{backend → base/backend}/vercel.json +0 -0
- package/template/{frontend → base/frontend}/.env.example +0 -0
- package/template/{frontend → base/frontend}/CLAUDE.md +0 -0
- package/template/{frontend → base/frontend}/README.md +0 -0
- package/template/{frontend → base/frontend}/app/dashboard/page.tsx +2 -2
- package/template/{frontend → base/frontend}/app/icon.png +0 -0
- package/template/{frontend → base/frontend}/components/require-auth.tsx +1 -1
- package/template/{frontend → base/frontend}/components.json +0 -0
- package/template/{frontend → base/frontend}/domains/auth/components/.gitkeep +0 -0
- package/template/{frontend → base/frontend}/domains/auth/hooks/.gitkeep +0 -0
- package/template/{frontend → base/frontend}/domains/auth/services/.gitkeep +0 -0
- package/template/{frontend → base/frontend}/domains/user/components/.gitkeep +0 -0
- package/template/{frontend → base/frontend}/domains/user/hooks/use-profile.ts +2 -2
- /package/template/{frontend → base/frontend}/eslint.config.mjs +0 -0
- /package/template/{frontend → base/frontend}/gitignore +0 -0
- /package/template/{frontend → base/frontend}/postcss.config.mjs +0 -0
- /package/template/{frontend → base/frontend}/public/favicon.ico +0 -0
- /package/template/{frontend → base/frontend}/public/logo.png +0 -0
package/dist/index.js
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { cpSync, existsSync, readFileSync, writeFileSync, readdirSync, renameSync } from "node:fs";
|
|
3
2
|
import { execSync } from "node:child_process";
|
|
4
|
-
import {
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { cpSync, existsSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
6
4
|
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import * as p from "@clack/prompts";
|
|
7
|
+
import color from "picocolors";
|
|
7
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
9
|
const templateDir = path.resolve(__dirname, "..", "template");
|
|
9
|
-
async function promptProjectName() {
|
|
10
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
11
|
-
const answer = await rl.question("Project name: ");
|
|
12
|
-
rl.close();
|
|
13
|
-
return answer.trim();
|
|
14
|
-
}
|
|
15
10
|
function isValidProjectName(name) {
|
|
16
11
|
return /^[a-z0-9-_]+$/i.test(name);
|
|
17
12
|
}
|
|
13
|
+
function isCommandAvailable(command) {
|
|
14
|
+
try {
|
|
15
|
+
execSync(`${command} --version`, { stdio: "ignore" });
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
18
22
|
// Undoes sync-template.mjs's .gitignore -> gitignore rename (npm strips
|
|
19
23
|
// files literally named .gitignore from the published tarball).
|
|
20
24
|
function renameGitignoreFilesBack(dir) {
|
|
@@ -26,85 +30,254 @@ function renameGitignoreFilesBack(dir) {
|
|
|
26
30
|
renameSync(full, path.join(dir, ".gitignore"));
|
|
27
31
|
}
|
|
28
32
|
}
|
|
29
|
-
function isPnpmInstalled() {
|
|
30
|
-
try {
|
|
31
|
-
execSync("pnpm --version", { stdio: "ignore" });
|
|
32
|
-
return true;
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
33
|
async function main() {
|
|
39
|
-
if (
|
|
40
|
-
console.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
34
|
+
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
35
|
+
console.log(`
|
|
36
|
+
Usage: create-restack-pattern [project-name]
|
|
37
|
+
|
|
38
|
+
Options:
|
|
39
|
+
-h, --help Show help message
|
|
40
|
+
-v, --version Show CLI version
|
|
41
|
+
|
|
42
|
+
Examples:
|
|
43
|
+
pnpm create restack-pattern my-saas-app
|
|
44
|
+
npm create restack-pattern@latest
|
|
45
|
+
bun create restack-pattern@latest
|
|
46
|
+
`);
|
|
47
|
+
process.exit(0);
|
|
46
48
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
if (process.argv.includes("--version") || process.argv.includes("-v")) {
|
|
50
|
+
const pkg = JSON.parse(readFileSync(path.resolve(__dirname, "..", "package.json"), "utf-8"));
|
|
51
|
+
console.log(pkg.version);
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
console.log();
|
|
55
|
+
p.intro(color.bgCyan(color.black(" create-restack-pattern ")));
|
|
56
|
+
let projectName = process.argv[2];
|
|
57
|
+
if (!projectName || projectName.startsWith("-")) {
|
|
58
|
+
const nameInput = await p.text({
|
|
59
|
+
message: "What is your project named?",
|
|
60
|
+
placeholder: "my-restack-app",
|
|
61
|
+
defaultValue: "my-restack-app",
|
|
62
|
+
validate: (value) => {
|
|
63
|
+
if (!value)
|
|
64
|
+
return "Project name cannot be empty";
|
|
65
|
+
if (!isValidProjectName(value)) {
|
|
66
|
+
return "Project name may only contain letters, numbers, hyphens, and underscores";
|
|
67
|
+
}
|
|
68
|
+
if (existsSync(path.resolve(process.cwd(), value))) {
|
|
69
|
+
return `Directory "${value}" already exists in the current directory`;
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
if (p.isCancel(nameInput)) {
|
|
74
|
+
p.cancel("Operation cancelled.");
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
77
|
+
projectName = nameInput;
|
|
52
78
|
}
|
|
53
79
|
const targetDir = path.resolve(process.cwd(), projectName);
|
|
54
80
|
if (existsSync(targetDir)) {
|
|
55
|
-
|
|
81
|
+
p.cancel(color.red(`Error: Directory "${projectName}" already exists.`));
|
|
56
82
|
process.exit(1);
|
|
57
83
|
}
|
|
58
|
-
|
|
59
|
-
|
|
84
|
+
const database = (await p.select({
|
|
85
|
+
message: "Select your database:",
|
|
86
|
+
options: [
|
|
87
|
+
{
|
|
88
|
+
value: "postgres",
|
|
89
|
+
label: "PostgreSQL",
|
|
90
|
+
hint: "Neon, Supabase, Docker, AWS RDS (Recommended for SaaS)",
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
value: "sqlite",
|
|
94
|
+
label: "SQLite",
|
|
95
|
+
hint: "Zero-config local file, Turso, Cloudflare D1",
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
initialValue: "postgres",
|
|
99
|
+
}));
|
|
100
|
+
if (p.isCancel(database)) {
|
|
101
|
+
p.cancel("Operation cancelled.");
|
|
102
|
+
process.exit(0);
|
|
103
|
+
}
|
|
104
|
+
const idStrategy = (await p.select({
|
|
105
|
+
message: "Select primary key strategy:",
|
|
106
|
+
options: [
|
|
107
|
+
{
|
|
108
|
+
value: "uuid",
|
|
109
|
+
label: "UUIDv7",
|
|
110
|
+
hint: "Time-sortable, unguessable, secure against enumeration (Recommended)",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
value: "serial",
|
|
114
|
+
label: "Serial Integer",
|
|
115
|
+
hint: "Auto-incrementing 1, 2, 3... (Simple relational lookups)",
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
initialValue: "uuid",
|
|
119
|
+
}));
|
|
120
|
+
if (p.isCancel(idStrategy)) {
|
|
121
|
+
p.cancel("Operation cancelled.");
|
|
122
|
+
process.exit(0);
|
|
123
|
+
}
|
|
124
|
+
const packageManager = (await p.select({
|
|
125
|
+
message: "Select package manager:",
|
|
126
|
+
options: [
|
|
127
|
+
{
|
|
128
|
+
value: "pnpm",
|
|
129
|
+
label: "pnpm",
|
|
130
|
+
hint: "Fast, disk-efficient workspace monorepo (Recommended)",
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
value: "npm",
|
|
134
|
+
label: "npm",
|
|
135
|
+
hint: "Default Node.js package manager",
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
value: "bun",
|
|
139
|
+
label: "bun",
|
|
140
|
+
hint: "Fast all-in-one JavaScript runtime & toolkit",
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
initialValue: "pnpm",
|
|
144
|
+
}));
|
|
145
|
+
if (p.isCancel(packageManager)) {
|
|
146
|
+
p.cancel("Operation cancelled.");
|
|
147
|
+
process.exit(0);
|
|
148
|
+
}
|
|
149
|
+
const s = p.spinner();
|
|
150
|
+
// 1. Scaffold files
|
|
151
|
+
s.start(`Scaffolding ${projectName} (${database} + ${idStrategy}) ...`);
|
|
152
|
+
const baseDir = path.join(templateDir, "base");
|
|
153
|
+
const overlaysDir = path.join(templateDir, "overlays");
|
|
154
|
+
// Copy base template
|
|
155
|
+
cpSync(baseDir, targetDir, { recursive: true });
|
|
156
|
+
// Overlay database configuration
|
|
157
|
+
const dbOverlayDir = path.join(overlaysDir, `db-${database}`);
|
|
158
|
+
if (existsSync(dbOverlayDir)) {
|
|
159
|
+
cpSync(dbOverlayDir, targetDir, { recursive: true });
|
|
160
|
+
}
|
|
161
|
+
// Overlay models
|
|
162
|
+
const modelOverlayDir = path.join(overlaysDir, "models", `${database}-${idStrategy}`);
|
|
163
|
+
if (existsSync(modelOverlayDir)) {
|
|
164
|
+
cpSync(modelOverlayDir, targetDir, { recursive: true });
|
|
165
|
+
}
|
|
166
|
+
// Overlay shared schema
|
|
167
|
+
const idOverlayDir = path.join(overlaysDir, `id-${idStrategy}`);
|
|
168
|
+
if (existsSync(idOverlayDir)) {
|
|
169
|
+
cpSync(idOverlayDir, targetDir, { recursive: true });
|
|
170
|
+
}
|
|
60
171
|
renameGitignoreFilesBack(targetDir);
|
|
172
|
+
// 2. Patch package.json files
|
|
61
173
|
const rootPkgPath = path.join(targetDir, "package.json");
|
|
62
174
|
const rootPkg = JSON.parse(readFileSync(rootPkgPath, "utf-8"));
|
|
63
175
|
rootPkg.name = projectName;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
176
|
+
if (packageManager !== "pnpm") {
|
|
177
|
+
rootPkg.workspaces = ["frontend", "backend", "packages/*"];
|
|
178
|
+
}
|
|
179
|
+
writeFileSync(rootPkgPath, `${JSON.stringify(rootPkg, null, 2)}\n`);
|
|
180
|
+
const backendPkgPath = path.join(targetDir, "backend/package.json");
|
|
181
|
+
if (existsSync(backendPkgPath)) {
|
|
182
|
+
const backendPkg = JSON.parse(readFileSync(backendPkgPath, "utf-8"));
|
|
183
|
+
if (database === "sqlite") {
|
|
184
|
+
if (backendPkg.dependencies) {
|
|
185
|
+
delete backendPkg.dependencies.postgres;
|
|
186
|
+
backendPkg.dependencies["better-sqlite3"] = "^13.0.3";
|
|
187
|
+
}
|
|
188
|
+
if (backendPkg.devDependencies) {
|
|
189
|
+
backendPkg.devDependencies["@types/better-sqlite3"] = "^7.6.13";
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
if (backendPkg.dependencies) {
|
|
194
|
+
delete backendPkg.dependencies["better-sqlite3"];
|
|
195
|
+
}
|
|
196
|
+
if (backendPkg.devDependencies) {
|
|
197
|
+
delete backendPkg.devDependencies["@types/better-sqlite3"];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (packageManager !== "pnpm") {
|
|
201
|
+
if (backendPkg.dependencies?.["@restack/shared"] === "workspace:*") {
|
|
202
|
+
backendPkg.dependencies["@restack/shared"] = "*";
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
writeFileSync(backendPkgPath, `${JSON.stringify(backendPkg, null, 2)}\n`);
|
|
206
|
+
}
|
|
207
|
+
const frontendPkgPath = path.join(targetDir, "frontend/package.json");
|
|
208
|
+
if (existsSync(frontendPkgPath) && packageManager !== "pnpm") {
|
|
209
|
+
const frontendPkg = JSON.parse(readFileSync(frontendPkgPath, "utf-8"));
|
|
210
|
+
if (frontendPkg.dependencies?.["@restack/shared"] === "workspace:*") {
|
|
211
|
+
frontendPkg.dependencies["@restack/shared"] = "*";
|
|
212
|
+
writeFileSync(frontendPkgPath, `${JSON.stringify(frontendPkg, null, 2)}\n`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
s.stop(color.green(`Scaffolded ${projectName} files successfully!`));
|
|
216
|
+
// 3. Install dependencies
|
|
217
|
+
if (isCommandAvailable(packageManager)) {
|
|
218
|
+
s.start(`Installing dependencies using ${packageManager} ...`);
|
|
219
|
+
try {
|
|
220
|
+
execSync(`${packageManager} install`, { cwd: targetDir, stdio: "ignore" });
|
|
221
|
+
s.stop(color.green(`Dependencies installed using ${packageManager}!`));
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
s.stop(color.yellow(`Could not automatically run ${packageManager} install. You can run it manually.`));
|
|
225
|
+
}
|
|
226
|
+
// 4. Build @restack/shared
|
|
227
|
+
s.start("Building @restack/shared package ...");
|
|
228
|
+
try {
|
|
229
|
+
if (packageManager === "pnpm") {
|
|
230
|
+
execSync("pnpm --filter @restack/shared build", { cwd: targetDir, stdio: "ignore" });
|
|
231
|
+
}
|
|
232
|
+
else if (packageManager === "bun") {
|
|
233
|
+
execSync("bun --filter @restack/shared build", { cwd: targetDir, stdio: "ignore" });
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
execSync("npm run build --workspace=@restack/shared", { cwd: targetDir, stdio: "ignore" });
|
|
237
|
+
}
|
|
238
|
+
s.stop(color.green("@restack/shared package built!"));
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
s.stop(color.yellow("Could not automatically build @restack/shared. Run the build script manually."));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
p.log.warn(color.yellow(`Note: ${packageManager} was not found on your system. Run '${packageManager} install' manually.`));
|
|
246
|
+
}
|
|
247
|
+
// 5. Initialize git
|
|
72
248
|
try {
|
|
73
249
|
execSync("git init", { cwd: targetDir, stdio: "ignore" });
|
|
74
250
|
}
|
|
75
251
|
catch {
|
|
76
|
-
//
|
|
252
|
+
// Non-fatal
|
|
77
253
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
254
|
+
const dbConfigStep = database === "postgres"
|
|
255
|
+
? `• For Backend:
|
|
256
|
+
${color.cyan("cp backend/.env.example backend/.env")}
|
|
257
|
+
(Set ${color.yellow("DATABASE_URL")} and a min-32-character ${color.yellow("JWT_SECRET")})`
|
|
258
|
+
: `• For Backend:
|
|
259
|
+
${color.cyan("cp backend/.env.example backend/.env")}
|
|
260
|
+
(SQLite database file will be created automatically at ${color.yellow("sqlite.db")})`;
|
|
261
|
+
p.note(`1. Go to your project directory:
|
|
262
|
+
${color.cyan(`cd ${projectName}`)}
|
|
82
263
|
|
|
83
|
-
|
|
84
|
-
|
|
264
|
+
2. Configure environment variables:
|
|
265
|
+
${dbConfigStep}
|
|
85
266
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
\x1b[36mcp backend/.env.example backend/.env\x1b[0m
|
|
89
|
-
(Open backend/.env and set \x1b[33mDATABASE_URL\x1b[0m and a min-32-chars \x1b[33mJWT_SECRET\x1b[0m)
|
|
267
|
+
• For Frontend:
|
|
268
|
+
${color.cyan("cp frontend/.env.example frontend/.env")}
|
|
90
269
|
|
|
91
|
-
|
|
92
|
-
|
|
270
|
+
3. Push database tables:
|
|
271
|
+
${color.cyan(`${packageManager} --filter backend db:push`)}
|
|
93
272
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
• Frontend (running at http://localhost:3000):
|
|
102
|
-
\x1b[36mpnpm --filter frontend dev\x1b[0m
|
|
103
|
-
|
|
104
|
-
Enjoy building your app with the Restack Pattern!
|
|
105
|
-
`);
|
|
273
|
+
4. Start development servers:
|
|
274
|
+
• Backend (API running at http://localhost:3001):
|
|
275
|
+
${color.cyan(`${packageManager} --filter backend dev`)}
|
|
276
|
+
• Frontend (Next.js running at http://localhost:3000):
|
|
277
|
+
${color.cyan(`${packageManager} --filter frontend dev`)}`, "Next steps to get started:");
|
|
278
|
+
p.outro(color.green(`All done! Enjoy building with the Restack Pattern!`));
|
|
106
279
|
}
|
|
107
280
|
main().catch((err) => {
|
|
108
|
-
|
|
281
|
+
p.cancel(color.red(String(err)));
|
|
109
282
|
process.exit(1);
|
|
110
283
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-restack-pattern",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Scaffold a new Next.js + Hono + Drizzle monorepo using the Restack Pattern (Semi-DDD) template.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -12,18 +12,24 @@
|
|
|
12
12
|
"dist",
|
|
13
13
|
"template"
|
|
14
14
|
],
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"dev": "tsx src/index.ts",
|
|
18
|
-
"sync-template": "node scripts/sync-template.mjs",
|
|
19
|
-
"prepublishOnly": "pnpm sync-template && pnpm build"
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
20
17
|
},
|
|
21
18
|
"devDependencies": {
|
|
22
19
|
"@types/node": "^20.11.17",
|
|
23
|
-
"tsx": "^4.
|
|
20
|
+
"tsx": "^4.23.15",
|
|
24
21
|
"typescript": "^5.8.3"
|
|
25
22
|
},
|
|
26
23
|
"engines": {
|
|
27
24
|
"node": ">=20"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@clack/prompts": "^1.8.1",
|
|
28
|
+
"picocolors": "^1.1.1"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc",
|
|
32
|
+
"dev": "tsx src/index.ts",
|
|
33
|
+
"sync-template": "node scripts/sync-template.mjs"
|
|
28
34
|
}
|
|
29
|
-
}
|
|
35
|
+
}
|
|
@@ -1,62 +1,73 @@
|
|
|
1
|
-
# DEVELOPMENT.md
|
|
2
|
-
|
|
3
|
-
This document outlines the architecture, build guidelines, and code conventions for the Restack Pattern monorepo. All developers and automated tools contributing to this project must follow these rules.
|
|
4
|
-
|
|
5
|
-
## Project Stack
|
|
6
|
-
- **Frontend**: Next.js 16 (App Router)
|
|
7
|
-
- **Backend**: Hono
|
|
8
|
-
- **ORM**: Drizzle ORM
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
- `
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
- `pnpm
|
|
24
|
-
- `pnpm --filter
|
|
25
|
-
- `pnpm --filter backend
|
|
26
|
-
- `pnpm --filter
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
- `
|
|
42
|
-
|
|
43
|
-
- `
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
`
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
##
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
-
|
|
60
|
-
-
|
|
61
|
-
|
|
62
|
-
|
|
1
|
+
# DEVELOPMENT.md
|
|
2
|
+
|
|
3
|
+
This document outlines the architecture, build guidelines, and code conventions for the Restack Pattern monorepo. All developers and automated tools contributing to this project must follow these rules.
|
|
4
|
+
|
|
5
|
+
## Project Stack
|
|
6
|
+
- **Frontend**: Next.js 16 (App Router with Turbopack & Typed Routes)
|
|
7
|
+
- **Backend**: Hono
|
|
8
|
+
- **ORM**: Drizzle ORM
|
|
9
|
+
- **Databases**: PostgreSQL (`postgres-js`) & SQLite (`better-sqlite3`)
|
|
10
|
+
- **Primary Keys**: RFC 9562 UUIDv7 or Serial Auto-Increment
|
|
11
|
+
- **Validation**: Zod (shared schemas)
|
|
12
|
+
- **Quality**: Biome v2 (sub-50ms linter & formatter)
|
|
13
|
+
- **Package Manager**: pnpm workspaces (also supports npm & bun)
|
|
14
|
+
|
|
15
|
+
## Monorepo Layout
|
|
16
|
+
- `frontend/` — Next.js 16 App Router
|
|
17
|
+
- `backend/` — Hono API
|
|
18
|
+
- `packages/shared/` — Shared Zod schemas and utilities (single source of truth)
|
|
19
|
+
- `packages/create-restack-pattern/` — Interactive CLI scaffolder published on npm
|
|
20
|
+
|
|
21
|
+
## Build and Dev Commands
|
|
22
|
+
All commands should be run from the repository root:
|
|
23
|
+
- `pnpm install` — Install all dependencies
|
|
24
|
+
- `pnpm --filter @restack/shared build` — Build shared Zod schemas (run first!)
|
|
25
|
+
- `pnpm --filter backend dev` — Start Hono dev server on port 3001
|
|
26
|
+
- `pnpm --filter frontend dev` — Start Next.js dev server on port 3000
|
|
27
|
+
- `pnpm --filter backend db:push` — Push Drizzle schema to database
|
|
28
|
+
- `pnpm --filter backend db:studio` — Open Drizzle Studio database viewer
|
|
29
|
+
- `pnpm --filter backend test` — Run Vitest tests
|
|
30
|
+
- `pnpm lint` — Run Biome linter across all packages (<50ms)
|
|
31
|
+
- `pnpm format` — Auto-format all code with Biome
|
|
32
|
+
|
|
33
|
+
## Architecture Rules: Semi-DDD (Feature-Driven)
|
|
34
|
+
Code is organized by business domain/feature, not by file type.
|
|
35
|
+
|
|
36
|
+
### Backend Layout (`backend/src/domains/[feature]/`)
|
|
37
|
+
Each domain has 4 primary files:
|
|
38
|
+
- `[feature].routes.ts` — HTTP endpoints & input validation (imports Zod schema from `@restack/shared`)
|
|
39
|
+
- `[feature].service.ts` — Pure business logic only (no direct DB access)
|
|
40
|
+
- `[feature].repo.ts` — Drizzle queries only (no business logic)
|
|
41
|
+
- `[feature].model.ts` — Drizzle table definitions
|
|
42
|
+
|
|
43
|
+
**Cross-Domain Dependency Rule**: The `auth` domain may import from `user` (e.g. `usersTable` from `user.model.ts`). The reverse (user importing from auth) is STRICTLY FORBIDDEN.
|
|
44
|
+
|
|
45
|
+
### Frontend Layout (`frontend/domains/[feature]/`)
|
|
46
|
+
- `components/` — UI components (no direct data fetching)
|
|
47
|
+
- `hooks/` — Reactive state & data fetching logic
|
|
48
|
+
- `services/` — Thin HTTP wrappers around `apiClient`
|
|
49
|
+
- `types.ts` — Local TS type declarations
|
|
50
|
+
- `frontend/app/` is strictly for layout, routing config, metadata, or entry gates. Do not implement custom JSX layouts, forms, state, or complex components here. Always delegate rendering to domain feature components.
|
|
51
|
+
|
|
52
|
+
## Critical Gotcha: Shared Package Build Requirement
|
|
53
|
+
`@restack/shared`'s `package.json` resolves to `dist/`, which is gitignored. When editing Zod schemas in `packages/shared/src/schemas/`, you MUST compile them by running:
|
|
54
|
+
`pnpm --filter @restack/shared build`
|
|
55
|
+
(or keep `pnpm --filter @restack/shared dev` watch mode running in a terminal) or the frontend and backend will fail to resolve the updated types or throw compilation errors.
|
|
56
|
+
|
|
57
|
+
## Auth & Cookie Strategy (Hybrid Auth & RBAC)
|
|
58
|
+
- **Hybrid Auth**: Accepts tokens via `Authorization: Bearer <token>` header first (for mobile, scripts, and external clients) or `access_token` HttpOnly cookie (for web browsers).
|
|
59
|
+
- **Dual Token Issuance**: Endpoints `/register`, `/login`, and `/refresh` return `{ user, accessToken, refreshToken }` in JSON body while simultaneously issuing HttpOnly cookies.
|
|
60
|
+
- **RBAC Guard**: Use `requireRole(...roles)` after `requireAuth` to restrict routes by role:
|
|
61
|
+
```ts
|
|
62
|
+
userRoutes.get("/admin", requireAuth, requireRole("admin"), (c) => ...)
|
|
63
|
+
```
|
|
64
|
+
- Short-lived JWT `access_token` (15m) and opaque `refresh_token` (30d) are both set as `httpOnly` cookies at `Path=/`.
|
|
65
|
+
- Access tokens are automatically refreshed by the client-side `apiClient` on a 401 response.
|
|
66
|
+
- Cookie configuration: `SameSite=None` in production (requires `Secure=true`), and `SameSite=Lax` in development (`Secure=false` since local dev runs on HTTP).
|
|
67
|
+
|
|
68
|
+
## Structured & Correlation Logging
|
|
69
|
+
- Every incoming HTTP request is tagged with a unique `requestId` via Hono's `requestId` middleware.
|
|
70
|
+
- A request-scoped child logger (`logger.child({ requestId })`) is bound to Hono's context.
|
|
71
|
+
- In route handlers, always extract the request-specific child logger from Hono's context (`c.get("logger") || logger`) to log events. This ensures all logs generated during a request carry the correct `requestId`, `userId`, and `authMethod`.
|
|
72
|
+
|
|
73
|
+
|
|
@@ -11,23 +11,26 @@
|
|
|
11
11
|
"test": "vitest run"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@hono/node-server": "^2.
|
|
15
|
-
"@hono/zod-validator": "^0.
|
|
14
|
+
"@hono/node-server": "^2.1.5",
|
|
15
|
+
"@hono/zod-validator": "^0.9.1",
|
|
16
16
|
"@restack/shared": "workspace:*",
|
|
17
17
|
"bcryptjs": "^3.0.3",
|
|
18
|
+
"better-sqlite3": "^13.0.3",
|
|
18
19
|
"dotenv": "^17.4.2",
|
|
19
|
-
"drizzle-orm": "^0.45.
|
|
20
|
-
"hono": "^4.
|
|
21
|
-
"jose": "^6.2.
|
|
22
|
-
"pino": "^10.
|
|
20
|
+
"drizzle-orm": "^0.45.4",
|
|
21
|
+
"hono": "^4.13.13",
|
|
22
|
+
"jose": "^6.2.12",
|
|
23
|
+
"pino": "^10.4.0",
|
|
23
24
|
"postgres": "^3.4.9",
|
|
24
|
-
"zod": "^4.
|
|
25
|
+
"zod": "^4.6.7"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
|
28
|
+
"@types/better-sqlite3": "^9.6.0",
|
|
27
29
|
"@types/node": "^20.11.17",
|
|
28
|
-
"drizzle-kit": "^0.31.
|
|
29
|
-
"
|
|
30
|
-
"
|
|
30
|
+
"drizzle-kit": "^0.31.11",
|
|
31
|
+
"fast-check": "^4.10.2",
|
|
32
|
+
"pino-pretty": "^13.2.0",
|
|
33
|
+
"tsx": "^4.23.15",
|
|
31
34
|
"typescript": "^5.8.3",
|
|
32
35
|
"vitest": "^3.0.0"
|
|
33
36
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import "dotenv/config"
|
|
2
|
-
import { z } from "zod"
|
|
2
|
+
import { z } from "zod"
|
|
3
3
|
|
|
4
4
|
const envSchema = z.object({
|
|
5
5
|
// Memastikan DATABASE_URL ada dan berformat URL valid
|
|
@@ -20,4 +20,4 @@ const envSchema = z.object({
|
|
|
20
20
|
LOG_FILE_PATH: z.string().optional(),
|
|
21
21
|
})
|
|
22
22
|
|
|
23
|
-
export const env = envSchema.parse(process.env)
|
|
23
|
+
export const env = envSchema.parse(process.env)
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { pgTable, serial,
|
|
1
|
+
import { integer, pgTable, serial, text, timestamp } from "drizzle-orm/pg-core"
|
|
2
2
|
import { usersTable } from "../user/user.model.js"
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Auth Sessions Table Schema.
|
|
6
6
|
* Tracks active, refreshable user login sessions.
|
|
7
|
-
*
|
|
7
|
+
*
|
|
8
8
|
* Security rules:
|
|
9
9
|
* - We only store the SHA-256 hash of the refresh token (`refreshTokenHash`),
|
|
10
10
|
* protecting the user session from being compromised if the database leaks.
|
|
@@ -12,7 +12,9 @@ import { usersTable } from "../user/user.model.js"
|
|
|
12
12
|
*/
|
|
13
13
|
export const authSessionsTable = pgTable("auth_sessions", {
|
|
14
14
|
id: serial("id").primaryKey(),
|
|
15
|
-
userId: integer("user_id")
|
|
15
|
+
userId: integer("user_id")
|
|
16
|
+
.notNull()
|
|
17
|
+
.references(() => usersTable.id, { onDelete: "cascade" }),
|
|
16
18
|
// sha256 of the opaque refresh token — never store the raw token.
|
|
17
19
|
refreshTokenHash: text("refresh_token_hash").notNull().unique(),
|
|
18
20
|
expiresAt: timestamp("expires_at").notNull(),
|