@objectifthunes/create-sandstone 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/generators.d.ts +7 -0
- package/dist/generators.js +328 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +70 -0
- package/dist/prompts.d.ts +13 -0
- package/dist/prompts.js +113 -0
- package/package.json +23 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ProjectConfig } from './prompts.js';
|
|
2
|
+
export declare function generatePackageJson(config: ProjectConfig): string;
|
|
3
|
+
export declare function generateTsConfig(config: ProjectConfig): string;
|
|
4
|
+
export declare function generateEnvExample(config: ProjectConfig): string;
|
|
5
|
+
export declare function generateGitignore(): string;
|
|
6
|
+
export declare function generateDockerCompose(): string;
|
|
7
|
+
export declare function generateMainTs(config: ProjectConfig): string;
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
export function generatePackageJson(config) {
|
|
2
|
+
const deps = {
|
|
3
|
+
'@objectifthunes/sandstone-sdk': '^0.5.0',
|
|
4
|
+
graphql: '^16.9.0',
|
|
5
|
+
};
|
|
6
|
+
const devDeps = {
|
|
7
|
+
typescript: '^5.7.0',
|
|
8
|
+
'@types/node': '^22.0.0',
|
|
9
|
+
vitest: '^3.0.0',
|
|
10
|
+
};
|
|
11
|
+
// Database
|
|
12
|
+
deps.pg = '^8.13.0';
|
|
13
|
+
// JWT
|
|
14
|
+
deps.jose = '^5.9.0';
|
|
15
|
+
// Logger
|
|
16
|
+
deps.pino = '^9.6.0';
|
|
17
|
+
// Framework
|
|
18
|
+
switch (config.framework) {
|
|
19
|
+
case 'express':
|
|
20
|
+
deps.express = '^5.0.0';
|
|
21
|
+
devDeps['@types/express'] = '^5.0.0';
|
|
22
|
+
break;
|
|
23
|
+
case 'fastify':
|
|
24
|
+
deps.fastify = '^5.2.0';
|
|
25
|
+
break;
|
|
26
|
+
case 'hono':
|
|
27
|
+
deps.hono = '^4.6.0';
|
|
28
|
+
break;
|
|
29
|
+
case 'nestjs':
|
|
30
|
+
deps['@nestjs/common'] = '^11.0.0';
|
|
31
|
+
deps['@nestjs/core'] = '^11.0.0';
|
|
32
|
+
deps['@nestjs/platform-express'] = '^11.0.0';
|
|
33
|
+
deps['@nestjs/graphql'] = '^13.0.0';
|
|
34
|
+
deps['@nestjs/apollo'] = '^13.0.0';
|
|
35
|
+
deps['@apollo/server'] = '^5.0.0';
|
|
36
|
+
deps['reflect-metadata'] = '^0.2.0';
|
|
37
|
+
deps.rxjs = '^7.8.0';
|
|
38
|
+
devDeps['@nestjs/cli'] = '^11.0.0';
|
|
39
|
+
devDeps['@types/express'] = '^5.0.0';
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
// Email
|
|
43
|
+
if (config.email === 'resend')
|
|
44
|
+
deps.resend = '^4.1.0';
|
|
45
|
+
if (config.email === 'nodemailer') {
|
|
46
|
+
deps.nodemailer = '^6.9.0';
|
|
47
|
+
devDeps['@types/nodemailer'] = '^6.0.0';
|
|
48
|
+
}
|
|
49
|
+
// Password
|
|
50
|
+
if (config.authStrategies.includes('password'))
|
|
51
|
+
deps.argon2 = '^0.41.0';
|
|
52
|
+
// Payments
|
|
53
|
+
if (config.payments === 'stripe')
|
|
54
|
+
deps.stripe = '^17.5.0';
|
|
55
|
+
// Storage
|
|
56
|
+
if (config.storage === 'r2' || config.storage === 's3') {
|
|
57
|
+
deps['@aws-sdk/client-s3'] = '^3.700.0';
|
|
58
|
+
deps['@aws-sdk/s3-request-presigner'] = '^3.700.0';
|
|
59
|
+
}
|
|
60
|
+
// Cache
|
|
61
|
+
if (config.cache === 'upstash')
|
|
62
|
+
deps['@upstash/redis'] = '^1.34.0';
|
|
63
|
+
const scripts = {
|
|
64
|
+
build: 'tsc',
|
|
65
|
+
start: 'node dist/main.js',
|
|
66
|
+
test: 'vitest run',
|
|
67
|
+
'test:watch': 'vitest',
|
|
68
|
+
};
|
|
69
|
+
if (config.framework === 'nestjs') {
|
|
70
|
+
scripts.build = 'nest build';
|
|
71
|
+
scripts['start:dev'] = 'nest start --watch';
|
|
72
|
+
}
|
|
73
|
+
return JSON.stringify({
|
|
74
|
+
name: config.name,
|
|
75
|
+
version: '0.0.1',
|
|
76
|
+
private: true,
|
|
77
|
+
type: 'module',
|
|
78
|
+
scripts,
|
|
79
|
+
dependencies: deps,
|
|
80
|
+
devDependencies: devDeps,
|
|
81
|
+
}, null, 2);
|
|
82
|
+
}
|
|
83
|
+
export function generateTsConfig(config) {
|
|
84
|
+
const isNest = config.framework === 'nestjs';
|
|
85
|
+
return JSON.stringify({
|
|
86
|
+
compilerOptions: {
|
|
87
|
+
target: 'ES2022',
|
|
88
|
+
module: isNest ? 'NodeNext' : 'NodeNext',
|
|
89
|
+
moduleResolution: 'NodeNext',
|
|
90
|
+
lib: ['ES2022'],
|
|
91
|
+
outDir: 'dist',
|
|
92
|
+
rootDir: 'src',
|
|
93
|
+
declaration: true,
|
|
94
|
+
strict: true,
|
|
95
|
+
esModuleInterop: true,
|
|
96
|
+
skipLibCheck: true,
|
|
97
|
+
forceConsistentCasingInFileNames: true,
|
|
98
|
+
...(isNest
|
|
99
|
+
? { emitDecoratorMetadata: true, experimentalDecorators: true }
|
|
100
|
+
: {}),
|
|
101
|
+
},
|
|
102
|
+
include: ['src'],
|
|
103
|
+
exclude: ['node_modules', 'dist'],
|
|
104
|
+
}, null, 2);
|
|
105
|
+
}
|
|
106
|
+
export function generateEnvExample(config) {
|
|
107
|
+
const lines = ['# Database'];
|
|
108
|
+
lines.push('DATABASE_URL=postgresql://user:password@localhost:5432/mydb');
|
|
109
|
+
lines.push('');
|
|
110
|
+
lines.push('# JWT');
|
|
111
|
+
lines.push('JWT_SECRET=your-secret-at-least-32-characters-long');
|
|
112
|
+
lines.push('');
|
|
113
|
+
if (config.email === 'resend') {
|
|
114
|
+
lines.push('# Resend');
|
|
115
|
+
lines.push('RESEND_API_KEY=re_xxxxxxxxxxxx');
|
|
116
|
+
lines.push('EMAIL_FROM=noreply@yourdomain.com');
|
|
117
|
+
lines.push('');
|
|
118
|
+
}
|
|
119
|
+
else if (config.email === 'nodemailer') {
|
|
120
|
+
lines.push('# SMTP');
|
|
121
|
+
lines.push('SMTP_HOST=smtp.example.com');
|
|
122
|
+
lines.push('SMTP_PORT=587');
|
|
123
|
+
lines.push('SMTP_USER=user');
|
|
124
|
+
lines.push('SMTP_PASS=pass');
|
|
125
|
+
lines.push('EMAIL_FROM=noreply@yourdomain.com');
|
|
126
|
+
lines.push('');
|
|
127
|
+
}
|
|
128
|
+
if (config.authStrategies.includes('oauth')) {
|
|
129
|
+
for (const p of config.oauthProviders) {
|
|
130
|
+
lines.push(`# OAuth: ${p}`);
|
|
131
|
+
lines.push(`${p.toUpperCase()}_CLIENT_ID=`);
|
|
132
|
+
lines.push(`${p.toUpperCase()}_CLIENT_SECRET=`);
|
|
133
|
+
lines.push('');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (config.payments === 'stripe') {
|
|
137
|
+
lines.push('# Stripe');
|
|
138
|
+
lines.push('STRIPE_SECRET_KEY=sk_test_xxx');
|
|
139
|
+
lines.push('STRIPE_WEBHOOK_SECRET=whsec_xxx');
|
|
140
|
+
lines.push('');
|
|
141
|
+
}
|
|
142
|
+
else if (config.payments === 'revenuecat') {
|
|
143
|
+
lines.push('# RevenueCat');
|
|
144
|
+
lines.push('REVENUECAT_API_KEY=');
|
|
145
|
+
lines.push('REVENUECAT_WEBHOOK_SECRET=');
|
|
146
|
+
lines.push('');
|
|
147
|
+
}
|
|
148
|
+
if (config.storage === 'r2') {
|
|
149
|
+
lines.push('# Cloudflare R2');
|
|
150
|
+
lines.push('R2_ACCOUNT_ID=');
|
|
151
|
+
lines.push('R2_ACCESS_KEY_ID=');
|
|
152
|
+
lines.push('R2_SECRET_ACCESS_KEY=');
|
|
153
|
+
lines.push('R2_BUCKET=');
|
|
154
|
+
lines.push('R2_PUBLIC_URL=');
|
|
155
|
+
lines.push('');
|
|
156
|
+
}
|
|
157
|
+
else if (config.storage === 's3') {
|
|
158
|
+
lines.push('# AWS S3');
|
|
159
|
+
lines.push('S3_BUCKET=');
|
|
160
|
+
lines.push('S3_REGION=');
|
|
161
|
+
lines.push('S3_ACCESS_KEY_ID=');
|
|
162
|
+
lines.push('S3_SECRET_ACCESS_KEY=');
|
|
163
|
+
lines.push('');
|
|
164
|
+
}
|
|
165
|
+
if (config.cache === 'upstash') {
|
|
166
|
+
lines.push('# Upstash Redis');
|
|
167
|
+
lines.push('UPSTASH_REDIS_URL=');
|
|
168
|
+
lines.push('UPSTASH_REDIS_TOKEN=');
|
|
169
|
+
lines.push('');
|
|
170
|
+
}
|
|
171
|
+
lines.push('# Server');
|
|
172
|
+
lines.push('PORT=3000');
|
|
173
|
+
return lines.join('\n');
|
|
174
|
+
}
|
|
175
|
+
export function generateGitignore() {
|
|
176
|
+
return `node_modules/
|
|
177
|
+
dist/
|
|
178
|
+
*.tsbuildinfo
|
|
179
|
+
.env
|
|
180
|
+
.env.local
|
|
181
|
+
coverage/
|
|
182
|
+
.DS_Store
|
|
183
|
+
`;
|
|
184
|
+
}
|
|
185
|
+
export function generateDockerCompose() {
|
|
186
|
+
return `services:
|
|
187
|
+
postgres:
|
|
188
|
+
image: postgres:17-alpine
|
|
189
|
+
ports:
|
|
190
|
+
- "5488:5432"
|
|
191
|
+
environment:
|
|
192
|
+
POSTGRES_USER: dev
|
|
193
|
+
POSTGRES_PASSWORD: dev
|
|
194
|
+
POSTGRES_DB: dev
|
|
195
|
+
tmpfs:
|
|
196
|
+
- /var/lib/postgresql/data
|
|
197
|
+
healthcheck:
|
|
198
|
+
test: ["CMD-SHELL", "pg_isready -U dev"]
|
|
199
|
+
interval: 2s
|
|
200
|
+
timeout: 5s
|
|
201
|
+
retries: 10
|
|
202
|
+
|
|
203
|
+
mailhog:
|
|
204
|
+
image: mailhog/mailhog:latest
|
|
205
|
+
ports:
|
|
206
|
+
- "1025:1025"
|
|
207
|
+
- "8025:8025"
|
|
208
|
+
`;
|
|
209
|
+
}
|
|
210
|
+
export function generateMainTs(config) {
|
|
211
|
+
const pkg = '@objectifthunes/sandstone-sdk';
|
|
212
|
+
const dbImport = config.database === 'supabase'
|
|
213
|
+
? `import { createSupabaseClient } from '${pkg}/supabase';`
|
|
214
|
+
: `import { createPgClient } from '${pkg}/pg';`;
|
|
215
|
+
const dbCreate = config.database === 'supabase'
|
|
216
|
+
? `createSupabaseClient({ connectionString: process.env.DATABASE_URL! })`
|
|
217
|
+
: `createPgClient({ connectionString: process.env.DATABASE_URL! })`;
|
|
218
|
+
let emailImport = '';
|
|
219
|
+
let emailCreate = '';
|
|
220
|
+
if (config.email === 'resend') {
|
|
221
|
+
emailImport = `import { createResendTransport } from '${pkg}/resend';`;
|
|
222
|
+
emailCreate = `createResendTransport({ apiKey: process.env.RESEND_API_KEY! })`;
|
|
223
|
+
}
|
|
224
|
+
else if (config.email === 'nodemailer') {
|
|
225
|
+
emailImport = `import { createNodemailerTransport } from '${pkg}/nodemailer';`;
|
|
226
|
+
emailCreate = `createNodemailerTransport({ host: process.env.SMTP_HOST!, port: Number(process.env.SMTP_PORT ?? 587), auth: { user: process.env.SMTP_USER!, pass: process.env.SMTP_PASS! } })`;
|
|
227
|
+
}
|
|
228
|
+
let passwordImport = '';
|
|
229
|
+
let passwordConfig = '';
|
|
230
|
+
if (config.authStrategies.includes('password')) {
|
|
231
|
+
passwordImport = `import { createArgon2Hasher } from '${pkg}/argon2';`;
|
|
232
|
+
passwordConfig = ` password: { hasher: createArgon2Hasher() },`;
|
|
233
|
+
}
|
|
234
|
+
let oauthImports = '';
|
|
235
|
+
let oauthConfig = '';
|
|
236
|
+
if (config.authStrategies.includes('oauth') && config.oauthProviders.length > 0) {
|
|
237
|
+
const oauthLines = [];
|
|
238
|
+
const configLines = [];
|
|
239
|
+
for (const p of config.oauthProviders) {
|
|
240
|
+
const upper = p.charAt(0).toUpperCase() + p.slice(1);
|
|
241
|
+
oauthLines.push(`import { create${upper}OAuth } from '${pkg}/oauth-${p}';`);
|
|
242
|
+
configLines.push(` ${p}: create${upper}OAuth({ clientId: process.env.${p.toUpperCase()}_CLIENT_ID!, clientSecret: process.env.${p.toUpperCase()}_CLIENT_SECRET! }),`);
|
|
243
|
+
}
|
|
244
|
+
oauthImports = oauthLines.join('\n');
|
|
245
|
+
oauthConfig = ` oauth: {\n${configLines.join('\n')}\n },`;
|
|
246
|
+
}
|
|
247
|
+
let paymentImport = '';
|
|
248
|
+
let paymentConfig = '';
|
|
249
|
+
if (config.payments === 'stripe') {
|
|
250
|
+
paymentImport = `import { createStripeProvider } from '${pkg}/stripe';`;
|
|
251
|
+
paymentConfig = ` payments: { provider: createStripeProvider({ secretKey: process.env.STRIPE_SECRET_KEY!, webhookSecret: process.env.STRIPE_WEBHOOK_SECRET! }) },`;
|
|
252
|
+
}
|
|
253
|
+
else if (config.payments === 'revenuecat') {
|
|
254
|
+
paymentImport = `import { createRevenueCatProvider } from '${pkg}/revenuecat';`;
|
|
255
|
+
paymentConfig = ` payments: { provider: createRevenueCatProvider({ apiKey: process.env.REVENUECAT_API_KEY!, webhookSecret: process.env.REVENUECAT_WEBHOOK_SECRET! }) },`;
|
|
256
|
+
}
|
|
257
|
+
let storageImport = '';
|
|
258
|
+
let storageConfig = '';
|
|
259
|
+
if (config.storage === 'r2') {
|
|
260
|
+
storageImport = `import { createR2Storage } from '${pkg}/r2';`;
|
|
261
|
+
storageConfig = ` storage: createR2Storage({ accountId: process.env.R2_ACCOUNT_ID!, accessKeyId: process.env.R2_ACCESS_KEY_ID!, secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!, bucket: process.env.R2_BUCKET! }),`;
|
|
262
|
+
}
|
|
263
|
+
else if (config.storage === 's3') {
|
|
264
|
+
storageImport = `import { createS3Storage } from '${pkg}/s3';`;
|
|
265
|
+
storageConfig = ` storage: createS3Storage({ bucket: process.env.S3_BUCKET!, region: process.env.S3_REGION!, accessKeyId: process.env.S3_ACCESS_KEY_ID!, secretAccessKey: process.env.S3_SECRET_ACCESS_KEY! }),`;
|
|
266
|
+
}
|
|
267
|
+
else if (config.storage === 'local') {
|
|
268
|
+
storageImport = `import { createLocalStorage } from '${pkg}/local-storage';`;
|
|
269
|
+
storageConfig = ` storage: createLocalStorage({ directory: './uploads' }),`;
|
|
270
|
+
}
|
|
271
|
+
let cacheImport = '';
|
|
272
|
+
let cacheConfig = '';
|
|
273
|
+
if (config.cache === 'upstash') {
|
|
274
|
+
cacheImport = `import { createUpstashCache } from '${pkg}/upstash';`;
|
|
275
|
+
cacheConfig = ` cache: createUpstashCache({ url: process.env.UPSTASH_REDIS_URL!, token: process.env.UPSTASH_REDIS_TOKEN! }),`;
|
|
276
|
+
}
|
|
277
|
+
const imports = [
|
|
278
|
+
`import { createAuth, createGraphQLHandler, createContextFactory, runMigrations } from '${pkg}';`,
|
|
279
|
+
`import { createJoseSigner } from '${pkg}/jose';`,
|
|
280
|
+
`import { createPinoLogger } from '${pkg}/pino';`,
|
|
281
|
+
dbImport,
|
|
282
|
+
emailImport,
|
|
283
|
+
passwordImport,
|
|
284
|
+
oauthImports,
|
|
285
|
+
paymentImport,
|
|
286
|
+
storageImport,
|
|
287
|
+
cacheImport,
|
|
288
|
+
]
|
|
289
|
+
.filter(Boolean)
|
|
290
|
+
.join('\n');
|
|
291
|
+
const authConfig = [
|
|
292
|
+
` otp: { length: 6, expiresIn: '10m', maxAttempts: 3, cooldown: '60s' },`,
|
|
293
|
+
passwordConfig,
|
|
294
|
+
oauthConfig,
|
|
295
|
+
` session: { enabled: true },`,
|
|
296
|
+
]
|
|
297
|
+
.filter(Boolean)
|
|
298
|
+
.join('\n');
|
|
299
|
+
return `${imports}
|
|
300
|
+
|
|
301
|
+
async function main() {
|
|
302
|
+
const db = ${dbCreate};
|
|
303
|
+
const logger = createPinoLogger({ level: 'info', pretty: true });
|
|
304
|
+
const tokens = createJoseSigner({ secret: process.env.JWT_SECRET! });
|
|
305
|
+
${config.email !== 'none' ? ` const emailTransport = ${emailCreate};` : ''}
|
|
306
|
+
|
|
307
|
+
await runMigrations(db, { includeBuiltIn: true });
|
|
308
|
+
|
|
309
|
+
const auth = createAuth({
|
|
310
|
+
db,
|
|
311
|
+
${config.email !== 'none' ? ' email: emailTransport,' : ''}
|
|
312
|
+
tokens,
|
|
313
|
+
logger,
|
|
314
|
+
${authConfig}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const createContext = createContextFactory({ db, tokens, logger });
|
|
318
|
+
// TODO: add your GraphQL schema here
|
|
319
|
+
// const graphqlHandler = createGraphQLHandler({ schema, createContext });
|
|
320
|
+
|
|
321
|
+
const port = process.env.PORT ?? 3000;
|
|
322
|
+
logger.info(\`Server ready to start on port \${port}\`);
|
|
323
|
+
logger.info('Add your HTTP framework and GraphQL schema to complete setup.');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
main().catch(console.error);
|
|
327
|
+
`;
|
|
328
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { runPrompts } from './prompts.js';
|
|
5
|
+
import { generatePackageJson, generateTsConfig, generateEnvExample, generateGitignore, generateDockerCompose, generateMainTs, } from './generators.js';
|
|
6
|
+
async function write(dir, name, content) {
|
|
7
|
+
const path = join(dir, name);
|
|
8
|
+
await writeFile(path, content, 'utf-8');
|
|
9
|
+
console.log(` created ${name}`);
|
|
10
|
+
}
|
|
11
|
+
async function main() {
|
|
12
|
+
console.log('\n @objectifthunes/create-sandstone\n');
|
|
13
|
+
const config = await runPrompts();
|
|
14
|
+
const dir = join(process.cwd(), config.name);
|
|
15
|
+
console.log(`\n Scaffolding ${config.name}...\n`);
|
|
16
|
+
await mkdir(dir, { recursive: true });
|
|
17
|
+
await mkdir(join(dir, 'src'), { recursive: true });
|
|
18
|
+
await mkdir(join(dir, 'migrations'), { recursive: true });
|
|
19
|
+
await mkdir(join(dir, 'tests', 'unit'), { recursive: true });
|
|
20
|
+
await write(dir, 'package.json', generatePackageJson(config));
|
|
21
|
+
await write(dir, 'tsconfig.json', generateTsConfig(config));
|
|
22
|
+
await write(dir, '.env.example', generateEnvExample(config));
|
|
23
|
+
await write(dir, '.gitignore', generateGitignore());
|
|
24
|
+
await write(dir, 'src/main.ts', generateMainTs(config));
|
|
25
|
+
if (config.docker) {
|
|
26
|
+
await write(dir, 'docker-compose.yml', generateDockerCompose());
|
|
27
|
+
}
|
|
28
|
+
// Sample migration
|
|
29
|
+
await write(dir, 'migrations/001_sample.sql', `CREATE TABLE IF NOT EXISTS items (
|
|
30
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
31
|
+
user_id UUID NOT NULL REFERENCES auth_users(id) ON DELETE CASCADE,
|
|
32
|
+
title TEXT NOT NULL,
|
|
33
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
34
|
+
);
|
|
35
|
+
`);
|
|
36
|
+
// Sample test
|
|
37
|
+
await write(dir, 'tests/unit/sample.test.ts', `import { describe, it, expect } from 'vitest';
|
|
38
|
+
import { createInMemoryDatabase } from '@objectifthunes/sandstone-sdk/testing';
|
|
39
|
+
|
|
40
|
+
describe('sample', () => {
|
|
41
|
+
it('in-memory database works', async () => {
|
|
42
|
+
const db = createInMemoryDatabase();
|
|
43
|
+
await db.query('CREATE TABLE test (id TEXT)');
|
|
44
|
+
await db.query('INSERT INTO "test" ("id") VALUES ($1) RETURNING *', ['1']);
|
|
45
|
+
const result = await db.query('SELECT * FROM "test"');
|
|
46
|
+
expect(result.rows.length).toBe(1);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
`);
|
|
50
|
+
// Vitest config
|
|
51
|
+
await write(dir, 'vitest.config.ts', `import { defineConfig } from 'vitest/config';
|
|
52
|
+
|
|
53
|
+
export default defineConfig({
|
|
54
|
+
test: {
|
|
55
|
+
globals: true,
|
|
56
|
+
include: ['tests/unit/**/*.test.ts'],
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
`);
|
|
60
|
+
console.log('\n Done! Next steps:\n');
|
|
61
|
+
console.log(` cd ${config.name}`);
|
|
62
|
+
console.log(' cp .env.example .env');
|
|
63
|
+
console.log(' pnpm install');
|
|
64
|
+
console.log(' pnpm test');
|
|
65
|
+
console.log('');
|
|
66
|
+
}
|
|
67
|
+
main().catch((err) => {
|
|
68
|
+
console.error(err);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface ProjectConfig {
|
|
2
|
+
name: string;
|
|
3
|
+
framework: 'express' | 'fastify' | 'hono' | 'nestjs' | 'none';
|
|
4
|
+
database: 'supabase' | 'neon' | 'pg';
|
|
5
|
+
authStrategies: ('otp' | 'password' | 'oauth')[];
|
|
6
|
+
oauthProviders: ('google' | 'github' | 'apple' | 'discord')[];
|
|
7
|
+
email: 'resend' | 'nodemailer' | 'none';
|
|
8
|
+
payments: 'stripe' | 'revenuecat' | 'none';
|
|
9
|
+
storage: 'r2' | 's3' | 'local' | 'none';
|
|
10
|
+
cache: 'upstash' | 'none';
|
|
11
|
+
docker: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function runPrompts(): Promise<ProjectConfig>;
|
package/dist/prompts.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import prompts from 'prompts';
|
|
2
|
+
export async function runPrompts() {
|
|
3
|
+
const response = await prompts([
|
|
4
|
+
{
|
|
5
|
+
type: 'text',
|
|
6
|
+
name: 'name',
|
|
7
|
+
message: 'Project name',
|
|
8
|
+
initial: 'my-backend',
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
type: 'select',
|
|
12
|
+
name: 'framework',
|
|
13
|
+
message: 'HTTP framework',
|
|
14
|
+
choices: [
|
|
15
|
+
{ title: 'Express', value: 'express' },
|
|
16
|
+
{ title: 'Fastify', value: 'fastify' },
|
|
17
|
+
{ title: 'Hono', value: 'hono' },
|
|
18
|
+
{ title: 'NestJS', value: 'nestjs' },
|
|
19
|
+
{ title: 'None (raw Node.js)', value: 'none' },
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
type: 'select',
|
|
24
|
+
name: 'database',
|
|
25
|
+
message: 'Database',
|
|
26
|
+
choices: [
|
|
27
|
+
{ title: 'Supabase PostgreSQL', value: 'supabase' },
|
|
28
|
+
{ title: 'Raw PostgreSQL (pg)', value: 'pg' },
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
type: 'multiselect',
|
|
33
|
+
name: 'authStrategies',
|
|
34
|
+
message: 'Auth strategies',
|
|
35
|
+
choices: [
|
|
36
|
+
{ title: 'Email OTP (6-digit code)', value: 'otp', selected: true },
|
|
37
|
+
{ title: 'Password (argon2)', value: 'password' },
|
|
38
|
+
{ title: 'OAuth', value: 'oauth' },
|
|
39
|
+
],
|
|
40
|
+
min: 1,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
type: (prev) => prev.includes('oauth') ? 'multiselect' : null,
|
|
44
|
+
name: 'oauthProviders',
|
|
45
|
+
message: 'OAuth providers',
|
|
46
|
+
choices: [
|
|
47
|
+
{ title: 'Google', value: 'google', selected: true },
|
|
48
|
+
{ title: 'GitHub', value: 'github' },
|
|
49
|
+
{ title: 'Apple', value: 'apple' },
|
|
50
|
+
{ title: 'Discord', value: 'discord' },
|
|
51
|
+
],
|
|
52
|
+
min: 1,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
type: 'select',
|
|
56
|
+
name: 'email',
|
|
57
|
+
message: 'Email provider',
|
|
58
|
+
choices: [
|
|
59
|
+
{ title: 'Resend', value: 'resend' },
|
|
60
|
+
{ title: 'Nodemailer (SMTP)', value: 'nodemailer' },
|
|
61
|
+
{ title: 'None', value: 'none' },
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
type: 'select',
|
|
66
|
+
name: 'payments',
|
|
67
|
+
message: 'Payments',
|
|
68
|
+
choices: [
|
|
69
|
+
{ title: 'Stripe', value: 'stripe' },
|
|
70
|
+
{ title: 'RevenueCat', value: 'revenuecat' },
|
|
71
|
+
{ title: 'None', value: 'none' },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
type: 'select',
|
|
76
|
+
name: 'storage',
|
|
77
|
+
message: 'Storage',
|
|
78
|
+
choices: [
|
|
79
|
+
{ title: 'Cloudflare R2', value: 'r2' },
|
|
80
|
+
{ title: 'AWS S3', value: 's3' },
|
|
81
|
+
{ title: 'Local filesystem', value: 'local' },
|
|
82
|
+
{ title: 'None', value: 'none' },
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
type: 'select',
|
|
87
|
+
name: 'cache',
|
|
88
|
+
message: 'Cache',
|
|
89
|
+
choices: [
|
|
90
|
+
{ title: 'Upstash Redis', value: 'upstash' },
|
|
91
|
+
{ title: 'None', value: 'none' },
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
type: 'confirm',
|
|
96
|
+
name: 'docker',
|
|
97
|
+
message: 'Include docker-compose for local dev/testing?',
|
|
98
|
+
initial: true,
|
|
99
|
+
},
|
|
100
|
+
], { onCancel: () => process.exit(0) });
|
|
101
|
+
return {
|
|
102
|
+
name: response.name,
|
|
103
|
+
framework: response.framework,
|
|
104
|
+
database: response.database,
|
|
105
|
+
authStrategies: response.authStrategies ?? ['otp'],
|
|
106
|
+
oauthProviders: response.oauthProviders ?? [],
|
|
107
|
+
email: response.email,
|
|
108
|
+
payments: response.payments,
|
|
109
|
+
storage: response.storage,
|
|
110
|
+
cache: response.cache,
|
|
111
|
+
docker: response.docker,
|
|
112
|
+
};
|
|
113
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@objectifthunes/create-sandstone",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scaffold a backend project using @objectifthunes/sandstone-sdk",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-sandstone": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"prompts": "^2.4.0"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"typescript": "^5.7.0",
|
|
17
|
+
"@types/node": "^22.0.0",
|
|
18
|
+
"@types/prompts": "^2.4.0"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc"
|
|
22
|
+
}
|
|
23
|
+
}
|