create-sprint 0.0.2 → 0.0.5

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/src/cli.ts ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runCLI } from "./index";
4
+
5
+ const args = process.argv.slice(2);
6
+
7
+ if (args.includes("--help") || args.includes("-h")) {
8
+ console.log("\nšŸš€ Sprint - Quickly API Framework\n");
9
+ console.log("Usage: npx create-sprint [options]");
10
+ console.log("\nOptions:");
11
+ console.log(" --ts, --typescript Create TypeScript project");
12
+ console.log(" --js, --javascript Create JavaScript project");
13
+ console.log(" --name <name> Project name (use '.' for current directory)");
14
+ console.log(" --no-install Skip automatic dependency installation");
15
+ console.log(" --telemetry <type> Telemetry: none, sentry, glitchtip, discord");
16
+ console.log(" --docker Add Docker support");
17
+ console.log(" --yes, -y Skip all prompts (use defaults)");
18
+ console.log(" --help, -h Show this help message");
19
+ console.log("\nExamples:");
20
+ console.log(" npx create-sprint Interactive mode");
21
+ console.log(" npx create-sprint -y Create TypeScript project with defaults");
22
+ console.log(" npx create-sprint --ts --name my-api");
23
+ console.log(" npx create-sprint --js --name . --telemetry sentry --docker\n");
24
+ process.exit(0);
25
+ }
26
+
27
+ runCLI(args).catch(console.error);
@@ -0,0 +1,396 @@
1
+ export function getTypeScriptPackageJson(name: string, telemetry: string) {
2
+ const deps: Record<string, string> = {
3
+ "sprint-es": "^0.0.24",
4
+ dotenv: "^17.0.0",
5
+ };
6
+
7
+ const devDeps: Record<string, string> = {
8
+ "@types/node": "^22.0.0",
9
+ "tsx": "^4.19.0",
10
+ typescript: "^5.6.0",
11
+ vite: "^6.0.0",
12
+ };
13
+
14
+ if (telemetry === "sentry" || telemetry === "glitchtip") {
15
+ deps["@sentry/node"] = "^8.0.0";
16
+ } else if (telemetry === "discord") {
17
+ deps["axios"] = "^1.6.0";
18
+ }
19
+
20
+ return {
21
+ name: name === "." ? "sprint-app" : name,
22
+ version: "0.0.1",
23
+ description: "Sprint API",
24
+ main: "dist/index.js",
25
+ scripts: {
26
+ build: "vite build",
27
+ start: "NODE_ENV=production node dist/index.js",
28
+ dev: "NODE_ENV=development tsx src/index.ts",
29
+ },
30
+ dependencies: deps,
31
+ devDependencies: devDeps,
32
+ };
33
+ }
34
+
35
+ export function getJavaScriptPackageJson(name: string, telemetry: string) {
36
+ const deps: Record<string, string> = {
37
+ "sprint-es": "^0.0.24",
38
+ dotenv: "^17.0.0",
39
+ };
40
+
41
+ if (telemetry === "sentry" || telemetry === "glitchtip") {
42
+ deps["@sentry/node"] = "^8.0.0";
43
+ } else if (telemetry === "discord") {
44
+ deps["axios"] = "^1.6.0";
45
+ }
46
+
47
+ return {
48
+ name: name === "." ? "sprint-app" : name,
49
+ version: "0.0.1",
50
+ description: "Sprint API",
51
+ main: "src/index.js",
52
+ type: "module",
53
+ scripts: {
54
+ start: "NODE_ENV=production node src/index.js",
55
+ dev: "NODE_ENV=development node --watch src/index.js",
56
+ },
57
+ dependencies: deps,
58
+ };
59
+ }
60
+
61
+ export function getTsConfig() {
62
+ return JSON.stringify({
63
+ compilerOptions: {
64
+ target: "ES2020",
65
+ module: "ESNext",
66
+ moduleResolution: "bundler",
67
+ lib: ["ES2020"],
68
+ outDir: "./dist",
69
+ rootDir: "./src",
70
+ strict: true,
71
+ esModuleInterop: true,
72
+ skipLibCheck: true,
73
+ forceConsistentCasingInFileNames: true,
74
+ resolveJsonModule: true,
75
+ declaration: true,
76
+ declarationMap: true,
77
+ sourceMap: true,
78
+ tabWidth: 4,
79
+ },
80
+ include: ["src/**/*"],
81
+ exclude: ["node_modules", "dist"],
82
+ }, null, 2);
83
+ }
84
+
85
+ export function getViteConfig() {
86
+ return `import { defineConfig } from "vite";
87
+ import { resolve } from "path";
88
+
89
+ export default defineConfig({
90
+ build: {
91
+ lib: {
92
+ entry: resolve(__dirname, "src/index.ts"),
93
+ formats: ["es"],
94
+ fileName: "index",
95
+ },
96
+ outDir: "dist",
97
+ rollupOptions: {
98
+ external: ["sprint-es", "express", "cors", "morgan", "serve-favicon", "dotenv"],
99
+ },
100
+ target: "ES2020",
101
+ },
102
+ resolve: {
103
+ alias: {
104
+ "@": resolve(__dirname, "src"),
105
+ },
106
+ },
107
+ });
108
+ `;
109
+ }
110
+
111
+ export function getMainFile(language: string) {
112
+ if (language === "typescript") {
113
+ return `import Sprint from "sprint-es";
114
+ import { config } from "./sprint.config";
115
+ import homeRouter from "./routes/home";
116
+
117
+ const app = new Sprint(config);
118
+
119
+ app.use(homeRouter);
120
+ `;
121
+ }
122
+
123
+ return `import Sprint from "sprint-es";
124
+ import { config } from "./sprint.config.js";
125
+ import homeRouter from "./routes/home.js";
126
+
127
+ const app = new Sprint(config);
128
+
129
+ app.use(homeRouter);
130
+ `;
131
+ }
132
+
133
+ export function getHomeRoute(language: string) {
134
+ if (language === "typescript") {
135
+ return `import { Router } from "sprint-es";
136
+
137
+ const router = Router();
138
+
139
+ router.get("/", (req, res) => {
140
+ res.json({
141
+ message: "Hello World",
142
+ status: "ok"
143
+ });
144
+ });
145
+
146
+ export default router;
147
+ `;
148
+ }
149
+ return `import { Router } from "sprint-es";
150
+
151
+ const router = Router();
152
+
153
+ router.get("/", (req, res) => {
154
+ res.json({
155
+ message: "Hello World",
156
+ status: "ok"
157
+ });
158
+ });
159
+
160
+ export default router;
161
+ `;
162
+ }
163
+
164
+ export function getDockerfile(language: string) {
165
+ if (language === "typescript") {
166
+ return `FROM node:20-alpine
167
+
168
+ WORKDIR /app
169
+
170
+ COPY package*.json ./
171
+
172
+ RUN npm ci
173
+
174
+ COPY . .
175
+
176
+ RUN npm run build
177
+
178
+ EXPOSE 3000
179
+
180
+ CMD ["npm", "start"]
181
+ `;
182
+ }
183
+ return `FROM node:20-alpine
184
+
185
+ WORKDIR /app
186
+
187
+ COPY package*.json ./
188
+
189
+ RUN npm ci
190
+
191
+ COPY . .
192
+
193
+ EXPOSE 3000
194
+
195
+ CMD ["npm", "start"]
196
+ `;
197
+ }
198
+
199
+ export function getDockerCompose(language: string) {
200
+ return `version: "3.8"
201
+
202
+ services:
203
+ app:
204
+ build: .
205
+ ports:
206
+ - "3000:3000"
207
+ environment:
208
+ - NODE_ENV=production
209
+ - PORT=3000
210
+ restart: unless-stopped
211
+ `;
212
+ }
213
+
214
+ export function getGitignore() {
215
+ return `# Dependencies
216
+ node_modules/
217
+ npm-debug.log*
218
+ yarn-debug.log*
219
+ yarn-error.log*
220
+
221
+ # Build
222
+ dist/
223
+ build/
224
+ *.tsbuildinfo
225
+
226
+ # Environment
227
+ .env.development
228
+ .env.production
229
+ .env.local
230
+ .env.*.local
231
+
232
+ # IDE
233
+ .vscode/
234
+ .idea/
235
+ *.swp
236
+ *.swo
237
+ *~
238
+
239
+ # OS
240
+ .DS_Store
241
+ Thumbs.db
242
+
243
+ # Logs
244
+ logs/
245
+ *.log
246
+
247
+ # Test
248
+ coverage/
249
+
250
+ # Temporary
251
+ tmp/
252
+ temp/
253
+ `;
254
+ }
255
+
256
+ export function getDockerIgnore() {
257
+ return `node_modules
258
+ npm-debug.log
259
+ .env
260
+ .env.*
261
+ .git
262
+ .gitignore
263
+ README.md
264
+ dist
265
+ build
266
+ coverage
267
+ .vscode
268
+ .idea
269
+ *.log
270
+ tmp
271
+ temp
272
+ `;
273
+ }
274
+
275
+ export function getSprintConfigFile(language: string, telemetry: string) {
276
+ if (language === "typescript") {
277
+ let config = `import type { SprintOptions } from "sprint-es";
278
+
279
+ export const config: SprintOptions = {
280
+ port: process.env.PORT ? parseInt(process.env.PORT) : 3000
281
+ };
282
+
283
+ `;
284
+
285
+ if (telemetry === "sentry" || telemetry === "glitchtip") {
286
+ config += `import { initTelemetry } from "sprint-es/telemetry";
287
+
288
+ initTelemetry({
289
+ provider: "${telemetry}",
290
+ dsn: process.env.SENTRY_DSN || "",
291
+ environment: process.env.NODE_ENV || "development"
292
+ });
293
+ `;
294
+ } else if (telemetry === "discord") {
295
+ config += `import { initTelemetry } from "sprint-es/telemetry";
296
+
297
+ initTelemetry({
298
+ provider: "discord",
299
+ webhookUrl: process.env.DISCORD_WEBHOOK_URL || ""
300
+ });
301
+ `;
302
+ }
303
+
304
+ return config;
305
+ }
306
+
307
+ let config = `export const config = {
308
+ port: process.env.PORT ? parseInt(process.env.PORT) : 3000
309
+ };
310
+ `;
311
+
312
+ if (telemetry === "sentry" || telemetry === "glitchtip") {
313
+ config += `
314
+ import { initTelemetry } from "sprint-es/telemetry";
315
+
316
+ initTelemetry({
317
+ provider: "${telemetry}",
318
+ dsn: process.env.SENTRY_DSN || "",
319
+ environment: process.env.NODE_ENV || "development"
320
+ });
321
+ `;
322
+ } else if (telemetry === "discord") {
323
+ config += `
324
+ import { initTelemetry } from "sprint-es/telemetry";
325
+
326
+ initTelemetry({
327
+ provider: "discord",
328
+ webhookUrl: process.env.DISCORD_WEBHOOK_URL || ""
329
+ });
330
+ `;
331
+ }
332
+
333
+ return config;
334
+ }
335
+
336
+ export function getEnvExample(telemetry: string) {
337
+ let env = `PORT=3000
338
+
339
+ # Development: npm run dev (NODE_ENV=development)
340
+ # Production: npm start (NODE_ENV=production)
341
+ `;
342
+
343
+ if (telemetry === "sentry" || telemetry === "glitchtip") {
344
+ env += `
345
+ # Sentry / GlitchTip (use GlitchTip DSN for self-hosted)
346
+ SENTRY_DSN=
347
+ `;
348
+ } else if (telemetry === "discord") {
349
+ env += `
350
+ # Discord Webhook URL for error notifications
351
+ DISCORD_WEBHOOK_URL=
352
+ `;
353
+ }
354
+
355
+ return env;
356
+ }
357
+
358
+ export function getEnvDevelopment(telemetry: string) {
359
+ let env = `NODE_ENV=development
360
+ PORT=3000
361
+ `;
362
+
363
+ if (telemetry === "sentry" || telemetry === "glitchtip") {
364
+ env += `
365
+ # Sentry / GlitchTip
366
+ SENTRY_DSN=
367
+ `;
368
+ } else if (telemetry === "discord") {
369
+ env += `
370
+ # Discord Webhook URL
371
+ DISCORD_WEBHOOK_URL=
372
+ `;
373
+ }
374
+
375
+ return env;
376
+ }
377
+
378
+ export function getEnvProduction(telemetry: string) {
379
+ let env = `NODE_ENV=production
380
+ PORT=3000
381
+ `;
382
+
383
+ if (telemetry === "sentry" || telemetry === "glitchtip") {
384
+ env += `
385
+ # Sentry / GlitchTip
386
+ SENTRY_DSN=
387
+ `;
388
+ } else if (telemetry === "discord") {
389
+ env += `
390
+ # Discord Webhook URL
391
+ DISCORD_WEBHOOK_URL=
392
+ `;
393
+ }
394
+
395
+ return env;
396
+ }
package/src/index.ts ADDED
@@ -0,0 +1,252 @@
1
+ import { execSync } from "child_process";
2
+ import { existsSync } from "fs";
3
+ import { mkdir, writeFile } from "fs/promises";
4
+ import { join } from "path";
5
+ import { input, select, confirm } from "@inquirer/prompts";
6
+ import { validateProjectName } from "./validators";
7
+ import { getTypeScriptPackageJson, getJavaScriptPackageJson, getTsConfig, getViteConfig, getMainFile, getHomeRoute, getDockerfile, getDockerCompose, getGitignore, getDockerIgnore, getSprintConfigFile, getEnvExample, getEnvDevelopment, getEnvProduction } from "./generators";
8
+
9
+ export interface CLIOptions {
10
+ projectName?: string;
11
+ language?: "typescript" | "javascript";
12
+ telemetry?: "none" | "sentry" | "glitchtip" | "discord";
13
+ docker?: boolean;
14
+ skipInstall?: boolean;
15
+ skipPrompts?: boolean;
16
+ }
17
+
18
+ export async function runCLI(args: string[]) {
19
+ const options = parseArgs(args);
20
+
21
+ console.log("\nšŸš€ Welcome to Sprint - Quickly API Framework\n");
22
+
23
+ let projectName = options.projectName;
24
+ let language = options.language;
25
+ const telemetry = options.telemetry;
26
+ const useDocker = options.docker;
27
+
28
+ if (!projectName) {
29
+ projectName = await getProjectName();
30
+ }
31
+
32
+ const error = validateProjectName(projectName);
33
+ if (error) {
34
+ console.error(`\nāŒ Error: ${error}\n`);
35
+ process.exit(1);
36
+ }
37
+
38
+ if (!language) {
39
+ language = await selectLanguage();
40
+ }
41
+
42
+ console.log(`\nāœ… Creating Sprint project: ${projectName === "." ? "current directory" : projectName} with ${language === "typescript" ? "TypeScript" : "JavaScript"}\n`);
43
+
44
+ await createProject(projectName, language, telemetry, useDocker);
45
+
46
+ console.log("\nāœ… Project created successfully!");
47
+
48
+ let installDeps = true;
49
+ if (options.skipInstall) {
50
+ installDeps = false;
51
+ } else if (!options.skipPrompts) {
52
+ installDeps = await confirm({
53
+ message: "Do you want to install dependencies now?",
54
+ default: true,
55
+ });
56
+ }
57
+
58
+ if (installDeps) {
59
+ console.log("\nšŸ“¦ Installing dependencies...\n");
60
+ const targetDir = projectName === "." ? process.cwd() : join(process.cwd(), projectName);
61
+ try {
62
+ execSync("npm install", { cwd: targetDir, stdio: "inherit" });
63
+ console.log("\nāœ… Dependencies installed successfully!");
64
+ } catch {
65
+ console.error("\nāŒ Error installing dependencies. Please run 'npm install' manually.");
66
+ }
67
+ }
68
+
69
+ console.log("\nšŸ“¦ Next steps:");
70
+ const cdCmd = projectName === "." ? "" : `cd ${projectName} && `;
71
+ if (!installDeps) {
72
+ console.log(` ${cdCmd}npm install`);
73
+ }
74
+ console.log(` ${cdCmd}npm run dev`);
75
+ console.log("\n");
76
+ }
77
+
78
+ function parseArgs(args: string[]): CLIOptions {
79
+ const options: CLIOptions = {};
80
+
81
+ const hasTs = args.includes("--ts") || args.includes("--typescript");
82
+ const hasJs = args.includes("--js") || args.includes("--javascript");
83
+ const hasName = args.indexOf("--name");
84
+ const telemetryArg = args.includes("--telemetry") ? args[args.indexOf("--telemetry") + 1] : null;
85
+
86
+ if (args.includes("--yes") || args.includes("-y")) {
87
+ options.skipPrompts = true;
88
+ options.language = "typescript";
89
+ } else if (hasTs) {
90
+ options.language = "typescript";
91
+ } else if (hasJs) {
92
+ options.language = "javascript";
93
+ }
94
+
95
+ if (hasName !== -1 && args[hasName + 1]) {
96
+ options.projectName = args[hasName + 1];
97
+ }
98
+
99
+ if (args.includes("--current")) {
100
+ options.projectName = ".";
101
+ }
102
+
103
+ if (args.includes("--docker")) {
104
+ options.docker = true;
105
+ }
106
+
107
+ if (args.includes("--no-install")) {
108
+ options.skipInstall = true;
109
+ }
110
+
111
+ if (telemetryArg && ["sentry", "glitchtip", "discord", "none"].includes(telemetryArg)) {
112
+ options.telemetry = telemetryArg as CLIOptions["telemetry"];
113
+ }
114
+
115
+ return options;
116
+ }
117
+
118
+ async function getProjectName(): Promise<string> {
119
+ const name = await input({
120
+ message: "Enter project name:",
121
+ validate: (value) => {
122
+ return validateProjectName(value) || true;
123
+ },
124
+ });
125
+
126
+ return name;
127
+ }
128
+
129
+ async function selectLanguage(): Promise<"typescript" | "javascript"> {
130
+ const language = await select({
131
+ message: "Select your preferred language:",
132
+ choices: [
133
+ {
134
+ name: "TypeScript",
135
+ value: "typescript",
136
+ description: "Recommended - Type safety and better developer experience",
137
+ },
138
+ {
139
+ name: "JavaScript",
140
+ value: "javascript",
141
+ description: "Vanilla JavaScript for simpler projects",
142
+ },
143
+ ],
144
+ });
145
+
146
+ return language as "typescript" | "javascript";
147
+ }
148
+
149
+ async function selectTelemetry(): Promise<"none" | "sentry" | "glitchtip" | "discord"> {
150
+ const telemetry = await select({
151
+ message: "Select error tracking/telemetry solution:",
152
+ choices: [
153
+ {
154
+ name: "None",
155
+ value: "none",
156
+ description: "No error tracking integration",
157
+ },
158
+ {
159
+ name: "Sentry",
160
+ value: "sentry",
161
+ description: "Full-featured error tracking (free tier available)",
162
+ },
163
+ {
164
+ name: "GlitchTip",
165
+ value: "glitchtip",
166
+ description: "Simple error tracking, can be self-hosted",
167
+ },
168
+ {
169
+ name: "Discord Webhook",
170
+ value: "discord",
171
+ description: "Send error notifications to Discord channel",
172
+ },
173
+ ],
174
+ });
175
+
176
+ return telemetry as "none" | "sentry" | "glitchtip" | "discord";
177
+ }
178
+
179
+ async function createProject(
180
+ projectName: string,
181
+ language: "typescript" | "javascript",
182
+ telemetryArg?: string,
183
+ useDockerArg?: boolean
184
+ ) {
185
+ const isCurrentDir = projectName === ".";
186
+ const targetDir = isCurrentDir ? process.cwd() : join(process.cwd(), projectName);
187
+
188
+ if (!isCurrentDir && existsSync(targetDir)) {
189
+ console.error(`Error: Directory ${projectName} already exists`);
190
+ process.exit(1);
191
+ }
192
+
193
+ if (!isCurrentDir) {
194
+ await mkdir(targetDir, { recursive: true });
195
+ }
196
+
197
+ let telemetry = telemetryArg || "none";
198
+ if (!telemetryArg) {
199
+ telemetry = await selectTelemetry();
200
+ }
201
+
202
+ let useDocker = useDockerArg || false;
203
+ if (!useDockerArg) {
204
+ useDocker = await confirm({
205
+ message: "Do you want to add Docker support?",
206
+ default: false,
207
+ });
208
+ }
209
+
210
+ let pkgJson;
211
+ if (language === "typescript") {
212
+ pkgJson = getTypeScriptPackageJson(projectName, telemetry);
213
+ } else {
214
+ pkgJson = getJavaScriptPackageJson(projectName, telemetry);
215
+ }
216
+
217
+ await writeFile(join(targetDir, "package.json"), JSON.stringify(pkgJson, null, 2));
218
+
219
+ if (language === "typescript") {
220
+ await writeFile(join(targetDir, "tsconfig.json"), getTsConfig());
221
+ await writeFile(join(targetDir, "vite.config.ts"), getViteConfig());
222
+ await writeFile(join(targetDir, "sprint.config.ts"), getSprintConfigFile(language, telemetry));
223
+ } else {
224
+ await writeFile(join(targetDir, "sprint.config.js"), getSprintConfigFile(language, telemetry));
225
+ }
226
+
227
+ const srcDir = join(targetDir, "src");
228
+ await mkdir(srcDir, { recursive: true });
229
+
230
+ await mkdir(join(srcDir, "middlewares"), { recursive: true });
231
+ await mkdir(join(srcDir, "routes"), { recursive: true });
232
+ await mkdir(join(srcDir, "controllers"), { recursive: true });
233
+
234
+ await writeFile(join(srcDir, "middlewares", ".gitkeep"), "");
235
+ await writeFile(join(srcDir, "controllers", ".gitkeep"), "");
236
+
237
+ await writeFile(join(srcDir, "app." + (language === "typescript" ? "ts" : "js")), getMainFile(language));
238
+
239
+ await writeFile(join(srcDir, "routes", "home." + (language === "typescript" ? "ts" : "js")), getHomeRoute(language));
240
+
241
+ await writeFile(join(targetDir, ".env.example"), getEnvExample(telemetry));
242
+ await writeFile(join(targetDir, ".env.development"), getEnvDevelopment(telemetry));
243
+ await writeFile(join(targetDir, ".env.production"), getEnvProduction(telemetry));
244
+
245
+ await writeFile(join(targetDir, ".gitignore"), getGitignore());
246
+
247
+ if (useDocker) {
248
+ await writeFile(join(targetDir, "Dockerfile"), getDockerfile(language));
249
+ await writeFile(join(targetDir, "docker-compose.yml"), getDockerCompose(language));
250
+ await writeFile(join(targetDir, ".dockerignore"), getDockerIgnore());
251
+ }
252
+ }
@@ -0,0 +1,32 @@
1
+ export function validateProjectName(name: string): string | null {
2
+ if (!name.trim()) return "Please enter a project name";
3
+
4
+ const n = name.trim();
5
+
6
+ if (n !== n.toLowerCase()) {
7
+ return "Project name must be lowercase";
8
+ }
9
+
10
+ if (n.length > 214) {
11
+ return "Project name must be less than 214 characters";
12
+ }
13
+
14
+ if (n.startsWith("-") || n.startsWith(".")) {
15
+ return "Project name cannot start with - or .";
16
+ }
17
+
18
+ if (/[~!@#$%^&*(){}[\]<>?:]/.test(n)) {
19
+ return "Project name cannot contain special characters (only letters, numbers, - and _)";
20
+ }
21
+
22
+ if (n !== encodeURIComponent(n)) {
23
+ return "Project name must be URL-safe";
24
+ }
25
+
26
+ const reserved = ["node_modules", "favicon.ico"];
27
+ if (reserved.includes(n.toLowerCase())) {
28
+ return `Cannot use "${n}" as project name`;
29
+ }
30
+
31
+ return null;
32
+ }