forgestack-os-cli 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/dist/commands/create.d.ts +1 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +78 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/generators/api.d.ts +3 -0
- package/dist/generators/api.d.ts.map +1 -0
- package/dist/generators/api.js +346 -0
- package/dist/generators/api.js.map +1 -0
- package/dist/generators/auth.d.ts +2 -0
- package/dist/generators/auth.d.ts.map +1 -0
- package/dist/generators/auth.js +371 -0
- package/dist/generators/auth.js.map +1 -0
- package/dist/generators/backend.d.ts +2 -0
- package/dist/generators/backend.d.ts.map +1 -0
- package/dist/generators/backend.js +875 -0
- package/dist/generators/backend.js.map +1 -0
- package/dist/generators/common.d.ts +2 -0
- package/dist/generators/common.d.ts.map +1 -0
- package/dist/generators/common.js +354 -0
- package/dist/generators/common.js.map +1 -0
- package/dist/generators/database.d.ts +2 -0
- package/dist/generators/database.d.ts.map +1 -0
- package/dist/generators/database.js +157 -0
- package/dist/generators/database.js.map +1 -0
- package/dist/generators/docker.d.ts +2 -0
- package/dist/generators/docker.d.ts.map +1 -0
- package/dist/generators/docker.js +181 -0
- package/dist/generators/docker.js.map +1 -0
- package/dist/generators/frontend-helpers.d.ts +3 -0
- package/dist/generators/frontend-helpers.d.ts.map +1 -0
- package/dist/generators/frontend-helpers.js +23 -0
- package/dist/generators/frontend-helpers.js.map +1 -0
- package/dist/generators/frontend.d.ts +2 -0
- package/dist/generators/frontend.d.ts.map +1 -0
- package/dist/generators/frontend.js +735 -0
- package/dist/generators/frontend.js.map +1 -0
- package/dist/generators/index.d.ts +2 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/index.js +59 -0
- package/dist/generators/index.js.map +1 -0
- package/dist/generators/nextjs-helpers.d.ts +6 -0
- package/dist/generators/nextjs-helpers.d.ts.map +1 -0
- package/dist/generators/nextjs-helpers.js +216 -0
- package/dist/generators/nextjs-helpers.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/logger.d.ts +9 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +32 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/prompts.d.ts +2 -0
- package/dist/utils/prompts.d.ts.map +1 -0
- package/dist/utils/prompts.js +107 -0
- package/dist/utils/prompts.js.map +1 -0
- package/dist/utils/validators.d.ts +2 -0
- package/dist/utils/validators.d.ts.map +1 -0
- package/dist/utils/validators.js +48 -0
- package/dist/utils/validators.js.map +1 -0
- package/package.json +49 -0
- package/src/commands/create.ts +82 -0
- package/src/generators/api.ts +353 -0
- package/src/generators/auth.ts +406 -0
- package/src/generators/backend.ts +927 -0
- package/src/generators/common.ts +377 -0
- package/src/generators/database.ts +165 -0
- package/src/generators/docker.ts +185 -0
- package/src/generators/frontend.ts +783 -0
- package/src/generators/index.ts +64 -0
- package/src/index.ts +27 -0
- package/src/types.ts +16 -0
- package/src/utils/logger.ts +31 -0
- package/src/utils/prompts.ts +105 -0
- package/src/utils/validators.ts +50 -0
- package/tests/validators.test.ts +69 -0
- package/tsc_output.txt +0 -0
- package/tsconfig.json +21 -0
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "forgestack-os-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ForgeStack OS CLI - Generate production-ready full-stack SaaS applications",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"forgestack": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "tsx watch src/index.ts",
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"test": "vitest",
|
|
13
|
+
"lint": "eslint src --ext .ts"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"cli",
|
|
17
|
+
"generator",
|
|
18
|
+
"saas",
|
|
19
|
+
"full-stack"
|
|
20
|
+
],
|
|
21
|
+
"author": {
|
|
22
|
+
"name": "Sumit Chauhan",
|
|
23
|
+
"url": "https://github.com/halloffame12"
|
|
24
|
+
},
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"chalk": "^4.1.2",
|
|
28
|
+
"commander": "^12.0.0",
|
|
29
|
+
"ejs": "^3.1.9",
|
|
30
|
+
"execa": "^5.1.1",
|
|
31
|
+
"fs-extra": "^11.2.0",
|
|
32
|
+
"inquirer": "^9.2.15",
|
|
33
|
+
"ora": "^5.4.1",
|
|
34
|
+
"validate-npm-package-name": "^5.0.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/ejs": "^3.1.5",
|
|
38
|
+
"@types/fs-extra": "^11.0.4",
|
|
39
|
+
"@types/inquirer": "^9.0.7",
|
|
40
|
+
"@types/node": "^20.19.27",
|
|
41
|
+
"@types/validate-npm-package-name": "^4.0.2",
|
|
42
|
+
"@typescript-eslint/eslint-plugin": "^6.19.0",
|
|
43
|
+
"@typescript-eslint/parser": "^6.19.0",
|
|
44
|
+
"eslint": "^8.56.0",
|
|
45
|
+
"tsx": "^4.7.0",
|
|
46
|
+
"typescript": "^5.3.3",
|
|
47
|
+
"vitest": "^1.6.1"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { logger } from '../utils/logger';
|
|
5
|
+
import { promptForStack } from '../utils/prompts';
|
|
6
|
+
import { validateStackConfig } from '../utils/validators';
|
|
7
|
+
import { generateProject } from '../generators';
|
|
8
|
+
|
|
9
|
+
export async function createCommand(projectName: string, options: any) {
|
|
10
|
+
try {
|
|
11
|
+
// Display welcome banner
|
|
12
|
+
console.log(chalk.bold.cyan('\n╔═══════════════════════════════════════╗'));
|
|
13
|
+
console.log(chalk.bold.cyan('║ ║'));
|
|
14
|
+
console.log(chalk.bold.cyan('║ 🚀 ForgeStack OS v0.1.0 ║'));
|
|
15
|
+
console.log(chalk.bold.cyan('║ ║'));
|
|
16
|
+
console.log(chalk.bold.cyan('║ One platform. Any stack. Production. ║'));
|
|
17
|
+
console.log(chalk.bold.cyan('║ ║'));
|
|
18
|
+
console.log(chalk.bold.cyan('╚═══════════════════════════════════════╝\n'));
|
|
19
|
+
|
|
20
|
+
// Check if directory already exists
|
|
21
|
+
const targetDir = path.resolve(process.cwd(), projectName);
|
|
22
|
+
if (await fs.pathExists(targetDir)) {
|
|
23
|
+
logger.error(`Directory "${projectName}" already exists!`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Prompt for stack configuration
|
|
28
|
+
logger.title('📋 Configure Your Stack');
|
|
29
|
+
const config = await promptForStack(projectName, options);
|
|
30
|
+
|
|
31
|
+
// Validate configuration
|
|
32
|
+
const validation = validateStackConfig(config);
|
|
33
|
+
|
|
34
|
+
if (validation.warnings.length > 0) {
|
|
35
|
+
console.log('');
|
|
36
|
+
validation.warnings.forEach(warning => logger.warning(warning));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!validation.valid) {
|
|
40
|
+
console.log('');
|
|
41
|
+
validation.errors.forEach(error => logger.error(error));
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Display selected stack
|
|
46
|
+
console.log('');
|
|
47
|
+
logger.title('✨ Your Stack Configuration');
|
|
48
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
49
|
+
console.log(`${chalk.bold('Project:')} ${chalk.cyan(config.projectName)}`);
|
|
50
|
+
console.log(`${chalk.bold('Frontend:')} ${chalk.cyan(config.frontend)}`);
|
|
51
|
+
console.log(`${chalk.bold('Backend:')} ${chalk.cyan(config.backend)}`);
|
|
52
|
+
console.log(`${chalk.bold('Auth:')} ${chalk.cyan(config.auth)}`);
|
|
53
|
+
console.log(`${chalk.bold('Database:')} ${chalk.cyan(config.database)}`);
|
|
54
|
+
console.log(`${chalk.bold('API Style:')} ${chalk.cyan(config.apiStyle)}`);
|
|
55
|
+
console.log(`${chalk.bold('Docker:')} ${chalk.cyan(config.docker ? 'Yes' : 'No')}`);
|
|
56
|
+
console.log(`${chalk.bold('Multi-Tenant:')} ${chalk.cyan(config.multiTenant ? 'Yes' : 'No')}`);
|
|
57
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
58
|
+
console.log('');
|
|
59
|
+
|
|
60
|
+
// Generate project
|
|
61
|
+
await generateProject(config, targetDir);
|
|
62
|
+
|
|
63
|
+
// Success message
|
|
64
|
+
console.log('');
|
|
65
|
+
logger.success(chalk.bold('🎉 Project created successfully!\n'));
|
|
66
|
+
|
|
67
|
+
console.log(chalk.bold('Next steps:\n'));
|
|
68
|
+
console.log(chalk.cyan(` cd ${projectName}`));
|
|
69
|
+
console.log(chalk.cyan(' npm install'));
|
|
70
|
+
console.log(chalk.cyan(' npm run dev\n'));
|
|
71
|
+
|
|
72
|
+
console.log(chalk.gray('For more information, check out the README.md in your project.\n'));
|
|
73
|
+
console.log(chalk.bold('Built by Sumit Chauhan'));
|
|
74
|
+
console.log(chalk.gray('GitHub: https://github.com/halloffame12'));
|
|
75
|
+
console.log(chalk.gray('LinkedIn: https://www.linkedin.com/in/sumit-chauhan-a4ba98325/\n'));
|
|
76
|
+
|
|
77
|
+
} catch (error) {
|
|
78
|
+
logger.error('Failed to create project');
|
|
79
|
+
console.error(error);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { StackConfig } from '../types';
|
|
4
|
+
|
|
5
|
+
export async function generateGraphQL(config: StackConfig, backendDir: string) {
|
|
6
|
+
if (config.backend === 'express') {
|
|
7
|
+
await generateExpressGraphQL(config, backendDir);
|
|
8
|
+
} else if (config.backend === 'nestjs') {
|
|
9
|
+
await generateNestJSGraphQL(config, backendDir);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function generateExpressGraphQL(config: StackConfig, backendDir: string) {
|
|
14
|
+
// Update package.json
|
|
15
|
+
const packageJson = await fs.readJSON(path.join(backendDir, 'package.json'));
|
|
16
|
+
packageJson.dependencies['graphql'] = '^16.8.1';
|
|
17
|
+
packageJson.dependencies['@apollo/server'] = '^4.10.0';
|
|
18
|
+
packageJson.dependencies['@graphql-tools/schema'] = '^10.0.2';
|
|
19
|
+
await fs.writeJSON(path.join(backendDir, 'package.json'), packageJson, { spaces: 2 });
|
|
20
|
+
|
|
21
|
+
// Create GraphQL directory
|
|
22
|
+
const graphqlDir = path.join(backendDir, 'src', 'graphql');
|
|
23
|
+
await fs.ensureDir(graphqlDir);
|
|
24
|
+
await fs.ensureDir(path.join(graphqlDir, 'resolvers'));
|
|
25
|
+
await fs.ensureDir(path.join(graphqlDir, 'types'));
|
|
26
|
+
|
|
27
|
+
// Schema
|
|
28
|
+
const schema = `import { gql } from 'graphql-tag';
|
|
29
|
+
|
|
30
|
+
export const typeDefs = gql\`
|
|
31
|
+
type User {
|
|
32
|
+
id: ID!
|
|
33
|
+
email: String!
|
|
34
|
+
name: String
|
|
35
|
+
${config.multiTenant ? 'tenantId: String' : ''}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type AuthPayload {
|
|
39
|
+
token: String!
|
|
40
|
+
user: User!
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type Query {
|
|
44
|
+
me: User
|
|
45
|
+
users: [User!]!
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type Mutation {
|
|
49
|
+
register(email: String!, password: String!, name: String): AuthPayload!
|
|
50
|
+
login(email: String!, password: String!): AuthPayload!
|
|
51
|
+
}
|
|
52
|
+
\`;
|
|
53
|
+
`;
|
|
54
|
+
await fs.writeFile(path.join(graphqlDir, 'schema.ts'), schema);
|
|
55
|
+
|
|
56
|
+
// Resolvers
|
|
57
|
+
const resolvers = `import bcrypt from 'bcrypt';
|
|
58
|
+
import jwt from 'jsonwebtoken';
|
|
59
|
+
${config.database === 'mongodb' ? "import User from '../models/User';" : "import prisma from '../lib/prisma';"}
|
|
60
|
+
|
|
61
|
+
export const resolvers = {
|
|
62
|
+
Query: {
|
|
63
|
+
me: async (_: any, __: any, context: any) => {
|
|
64
|
+
if (!context.user) throw new Error('Not authenticated');
|
|
65
|
+
|
|
66
|
+
${config.database === 'mongodb' ? `
|
|
67
|
+
return await User.findById(context.user.userId);
|
|
68
|
+
` : `
|
|
69
|
+
return await prisma.user.findUnique({
|
|
70
|
+
where: { id: context.user.userId },
|
|
71
|
+
});
|
|
72
|
+
`}
|
|
73
|
+
},
|
|
74
|
+
users: async (_: any, __: any, context: any) => {
|
|
75
|
+
if (!context.user) throw new Error('Not authenticated');
|
|
76
|
+
|
|
77
|
+
${config.database === 'mongodb' ? `
|
|
78
|
+
return await User.find(${config.multiTenant ? '{ tenantId: context.user.tenantId }' : '{}'});
|
|
79
|
+
` : `
|
|
80
|
+
return await prisma.user.findMany(${config.multiTenant ? '{ where: { tenantId: context.user.tenantId } }' : '{}'});
|
|
81
|
+
`}
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
Mutation: {
|
|
85
|
+
register: async (_: any, { email, password, name }: any) => {
|
|
86
|
+
const hashedPassword = await bcrypt.hash(password, 10);
|
|
87
|
+
|
|
88
|
+
${config.database === 'mongodb' ? `
|
|
89
|
+
const user = await User.create({
|
|
90
|
+
email,
|
|
91
|
+
password: hashedPassword,
|
|
92
|
+
name,
|
|
93
|
+
});
|
|
94
|
+
` : `
|
|
95
|
+
const user = await prisma.user.create({
|
|
96
|
+
data: {
|
|
97
|
+
email,
|
|
98
|
+
password: hashedPassword,
|
|
99
|
+
name,
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
`}
|
|
103
|
+
|
|
104
|
+
const token = jwt.sign(
|
|
105
|
+
{ userId: user.id, email: user.email },
|
|
106
|
+
process.env.JWT_SECRET!,
|
|
107
|
+
{ expiresIn: '7d' }
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return { token, user };
|
|
111
|
+
},
|
|
112
|
+
login: async (_: any, { email, password }: any) => {
|
|
113
|
+
${config.database === 'mongodb' ? `
|
|
114
|
+
const user = await User.findOne({ email });
|
|
115
|
+
` : `
|
|
116
|
+
const user = await prisma.user.findUnique({
|
|
117
|
+
where: { email },
|
|
118
|
+
});
|
|
119
|
+
`}
|
|
120
|
+
|
|
121
|
+
if (!user) throw new Error('Invalid credentials');
|
|
122
|
+
|
|
123
|
+
const valid = await bcrypt.compare(password, user.password);
|
|
124
|
+
if (!valid) throw new Error('Invalid credentials');
|
|
125
|
+
|
|
126
|
+
const token = jwt.sign(
|
|
127
|
+
{ userId: user.id, email: user.email },
|
|
128
|
+
process.env.JWT_SECRET!,
|
|
129
|
+
{ expiresIn: '7d' }
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
return { token, user };
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
`;
|
|
137
|
+
await fs.writeFile(path.join(graphqlDir, 'resolvers', 'index.ts'), resolvers);
|
|
138
|
+
|
|
139
|
+
// Apollo Server setup
|
|
140
|
+
const apolloSetup = `import { ApolloServer } from '@apollo/server';
|
|
141
|
+
import { expressMiddleware } from '@apollo/server/express4';
|
|
142
|
+
import { typeDefs } from './schema';
|
|
143
|
+
import { resolvers } from './resolvers';
|
|
144
|
+
import jwt from 'jsonwebtoken';
|
|
145
|
+
|
|
146
|
+
export async function createApolloServer() {
|
|
147
|
+
const server = new ApolloServer({
|
|
148
|
+
typeDefs,
|
|
149
|
+
resolvers,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
await server.start();
|
|
153
|
+
|
|
154
|
+
return expressMiddleware(server, {
|
|
155
|
+
context: async ({ req }) => {
|
|
156
|
+
const token = req.headers.authorization?.replace('Bearer ', '');
|
|
157
|
+
|
|
158
|
+
if (token) {
|
|
159
|
+
try {
|
|
160
|
+
const user = jwt.verify(token, process.env.JWT_SECRET!);
|
|
161
|
+
return { user };
|
|
162
|
+
} catch (err) {
|
|
163
|
+
return {};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {};
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
`;
|
|
172
|
+
await fs.writeFile(path.join(graphqlDir, 'server.ts'), apolloSetup);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function generateNestJSGraphQL(config: StackConfig, backendDir: string) {
|
|
176
|
+
// Update package.json
|
|
177
|
+
const packageJson = await fs.readJSON(path.join(backendDir, 'package.json'));
|
|
178
|
+
packageJson.dependencies['@nestjs/graphql'] = '^12.0.11';
|
|
179
|
+
packageJson.dependencies['@nestjs/apollo'] = '^12.0.11';
|
|
180
|
+
packageJson.dependencies['@apollo/server'] = '^4.10.0';
|
|
181
|
+
packageJson.dependencies['graphql'] = '^16.8.1';
|
|
182
|
+
await fs.writeJSON(path.join(backendDir, 'package.json'), packageJson, { spaces: 2 });
|
|
183
|
+
|
|
184
|
+
// GraphQL module
|
|
185
|
+
const graphqlModule = `import { Module } from '@nestjs/common';
|
|
186
|
+
import { GraphQLModule } from '@nestjs/graphql';
|
|
187
|
+
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
|
|
188
|
+
import { join } from 'path';
|
|
189
|
+
|
|
190
|
+
@Module({
|
|
191
|
+
imports: [
|
|
192
|
+
GraphQLModule.forRoot<ApolloDriverConfig>({
|
|
193
|
+
driver: ApolloDriver,
|
|
194
|
+
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
|
|
195
|
+
sortSchema: true,
|
|
196
|
+
playground: true,
|
|
197
|
+
context: ({ req }) => ({ req }),
|
|
198
|
+
}),
|
|
199
|
+
],
|
|
200
|
+
})
|
|
201
|
+
export class GraphqlModule {}
|
|
202
|
+
`;
|
|
203
|
+
await fs.writeFile(path.join(backendDir, 'src', 'graphql.module.ts'), graphqlModule);
|
|
204
|
+
|
|
205
|
+
// User resolver
|
|
206
|
+
const userResolver = `import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
|
|
207
|
+
import { UseGuards } from '@nestjs/common';
|
|
208
|
+
import { JwtAuthGuard } from './auth/guards/jwt-auth.guard';
|
|
209
|
+
import { UsersService } from './users/users.service';
|
|
210
|
+
import { AuthService } from './auth/auth.service';
|
|
211
|
+
|
|
212
|
+
@Resolver('User')
|
|
213
|
+
export class UserResolver {
|
|
214
|
+
constructor(
|
|
215
|
+
private usersService: UsersService,
|
|
216
|
+
private authService: AuthService,
|
|
217
|
+
) {}
|
|
218
|
+
|
|
219
|
+
@Query()
|
|
220
|
+
@UseGuards(JwtAuthGuard)
|
|
221
|
+
async me(@Args('id') id: string) {
|
|
222
|
+
return this.usersService.findById(id);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
@Mutation()
|
|
226
|
+
async register(
|
|
227
|
+
@Args('email') email: string,
|
|
228
|
+
@Args('password') password: string,
|
|
229
|
+
@Args('name') name?: string,
|
|
230
|
+
) {
|
|
231
|
+
return this.authService.register({ email, password, name });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
@Mutation()
|
|
235
|
+
async login(
|
|
236
|
+
@Args('email') email: string,
|
|
237
|
+
@Args('password') password: string,
|
|
238
|
+
) {
|
|
239
|
+
return this.authService.login({ email, password });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
`;
|
|
243
|
+
await fs.writeFile(path.join(backendDir, 'src', 'users', 'users.resolver.ts'), userResolver);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export async function generateTRPC(config: StackConfig, backendDir: string, frontendDir: string) {
|
|
247
|
+
// Backend setup
|
|
248
|
+
const packageJson = await fs.readJSON(path.join(backendDir, 'package.json'));
|
|
249
|
+
packageJson.dependencies['@trpc/server'] = '^10.45.0';
|
|
250
|
+
packageJson.dependencies['zod'] = '^3.22.4';
|
|
251
|
+
await fs.writeJSON(path.join(backendDir, 'package.json'), packageJson, { spaces: 2 });
|
|
252
|
+
|
|
253
|
+
// tRPC router
|
|
254
|
+
const trpcDir = path.join(backendDir, 'src', 'trpc');
|
|
255
|
+
await fs.ensureDir(trpcDir);
|
|
256
|
+
await fs.ensureDir(path.join(trpcDir, 'routers'));
|
|
257
|
+
|
|
258
|
+
const trpcSetup = `import { initTRPC } from '@trpc/server';
|
|
259
|
+
import { z } from 'zod';
|
|
260
|
+
|
|
261
|
+
const t = initTRPC.create();
|
|
262
|
+
|
|
263
|
+
export const router = t.router;
|
|
264
|
+
export const publicProcedure = t.procedure;
|
|
265
|
+
`;
|
|
266
|
+
await fs.writeFile(path.join(trpcDir, 'trpc.ts'), trpcSetup);
|
|
267
|
+
|
|
268
|
+
const authRouter = `import { router, publicProcedure } from '../trpc';
|
|
269
|
+
import { z } from 'zod';
|
|
270
|
+
import bcrypt from 'bcrypt';
|
|
271
|
+
import jwt from 'jsonwebtoken';
|
|
272
|
+
${config.database === 'mongodb' ? "import User from '../../models/User';" : "import prisma from '../../lib/prisma';"}
|
|
273
|
+
|
|
274
|
+
export const authRouter = router({
|
|
275
|
+
register: publicProcedure
|
|
276
|
+
.input(z.object({
|
|
277
|
+
email: z.string().email(),
|
|
278
|
+
password: z.string().min(8),
|
|
279
|
+
name: z.string().optional(),
|
|
280
|
+
}))
|
|
281
|
+
.mutation(async ({ input }) => {
|
|
282
|
+
const hashedPassword = await bcrypt.hash(input.password, 10);
|
|
283
|
+
|
|
284
|
+
${config.database === 'mongodb' ? `
|
|
285
|
+
const user = await User.create({
|
|
286
|
+
email: input.email,
|
|
287
|
+
password: hashedPassword,
|
|
288
|
+
name: input.name,
|
|
289
|
+
});
|
|
290
|
+
` : `
|
|
291
|
+
const user = await prisma.user.create({
|
|
292
|
+
data: {
|
|
293
|
+
email: input.email,
|
|
294
|
+
password: hashedPassword,
|
|
295
|
+
name: input.name,
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
`}
|
|
299
|
+
|
|
300
|
+
const token = jwt.sign(
|
|
301
|
+
{ userId: user.id, email: user.email },
|
|
302
|
+
process.env.JWT_SECRET!,
|
|
303
|
+
{ expiresIn: '7d' }
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
return { token, user: { id: user.id, email: user.email, name: user.name } };
|
|
307
|
+
}),
|
|
308
|
+
|
|
309
|
+
login: publicProcedure
|
|
310
|
+
.input(z.object({
|
|
311
|
+
email: z.string().email(),
|
|
312
|
+
password: z.string(),
|
|
313
|
+
}))
|
|
314
|
+
.mutation(async ({ input }) => {
|
|
315
|
+
${config.database === 'mongodb' ? `
|
|
316
|
+
const user = await User.findOne({ email: input.email });
|
|
317
|
+
` : `
|
|
318
|
+
const user = await prisma.user.findUnique({
|
|
319
|
+
where: { email: input.email },
|
|
320
|
+
});
|
|
321
|
+
`}
|
|
322
|
+
|
|
323
|
+
if (!user) throw new Error('Invalid credentials');
|
|
324
|
+
|
|
325
|
+
const valid = await bcrypt.compare(input.password, user.password);
|
|
326
|
+
if (!valid) throw new Error('Invalid credentials');
|
|
327
|
+
|
|
328
|
+
const token = jwt.sign(
|
|
329
|
+
{ userId: user.id, email: user.email },
|
|
330
|
+
process.env.JWT_SECRET!,
|
|
331
|
+
{ expiresIn: '7d' }
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
return { token, user: { id: user.id, email: user.email, name: user.name } };
|
|
335
|
+
}),
|
|
336
|
+
});
|
|
337
|
+
`;
|
|
338
|
+
await fs.writeFile(path.join(trpcDir, 'routers', 'auth.ts'), authRouter);
|
|
339
|
+
|
|
340
|
+
// Frontend tRPC client
|
|
341
|
+
const frontendPackageJson = await fs.readJSON(path.join(frontendDir, 'package.json'));
|
|
342
|
+
frontendPackageJson.dependencies['@trpc/client'] = '^10.45.0';
|
|
343
|
+
frontendPackageJson.dependencies['@trpc/react-query'] = '^10.45.0';
|
|
344
|
+
frontendPackageJson.dependencies['@tanstack/react-query'] = '^5.17.19';
|
|
345
|
+
await fs.writeJSON(path.join(frontendDir, 'package.json'), frontendPackageJson, { spaces: 2 });
|
|
346
|
+
|
|
347
|
+
const trpcClient = `import { createTRPCReact } from '@trpc/react-query';
|
|
348
|
+
import type { AppRouter } from '../../../backend/src/trpc/routers';
|
|
349
|
+
|
|
350
|
+
export const trpc = createTRPCReact<AppRouter>();
|
|
351
|
+
`;
|
|
352
|
+
await fs.writeFile(path.join(frontendDir, 'src', 'lib', 'trpc.ts'), trpcClient);
|
|
353
|
+
}
|