create-frootjs 0.0.0-stage → 1.0.2
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/LICENSE +21 -0
- package/README.md +68 -2
- package/dist/generator.d.ts +21 -0
- package/dist/generator.d.ts.map +1 -0
- package/dist/generator.js +62 -0
- package/dist/generator.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +215 -0
- package/dist/index.js.map +1 -0
- package/dist/templates/fullstack.d.ts +7 -0
- package/dist/templates/fullstack.d.ts.map +1 -0
- package/dist/templates/fullstack.js +224 -0
- package/dist/templates/fullstack.js.map +1 -0
- package/dist/templates/index.d.ts +10 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +26 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/templates/minimal.d.ts +7 -0
- package/dist/templates/minimal.d.ts.map +1 -0
- package/dist/templates/minimal.js +89 -0
- package/dist/templates/minimal.js.map +1 -0
- package/dist/templates/modular.d.ts +7 -0
- package/dist/templates/modular.d.ts.map +1 -0
- package/dist/templates/modular.js +300 -0
- package/dist/templates/modular.js.map +1 -0
- package/dist/templates/mvc.d.ts +7 -0
- package/dist/templates/mvc.d.ts.map +1 -0
- package/dist/templates/mvc.js +239 -0
- package/dist/templates/mvc.js.map +1 -0
- package/dist/templates/openapi.d.ts +7 -0
- package/dist/templates/openapi.d.ts.map +1 -0
- package/dist/templates/openapi.js +206 -0
- package/dist/templates/openapi.js.map +1 -0
- package/dist/templates/streaming.d.ts +7 -0
- package/dist/templates/streaming.d.ts.map +1 -0
- package/dist/templates/streaming.js +211 -0
- package/dist/templates/streaming.js.map +1 -0
- package/dist/templates/types.d.ts +17 -0
- package/dist/templates/types.d.ts.map +1 -0
- package/dist/templates/types.js +47 -0
- package/dist/templates/types.js.map +1 -0
- package/dist/templates/websocket.d.ts +7 -0
- package/dist/templates/websocket.d.ts.map +1 -0
- package/dist/templates/websocket.js +198 -0
- package/dist/templates/websocket.js.map +1 -0
- package/dist/ui.d.ts +73 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +236 -0
- package/dist/ui.js.map +1 -0
- package/package.json +45 -4
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MVC / Layered Architecture Template
|
|
3
|
+
* Organizes code by layer: Routes -> Controllers -> Services -> Models.
|
|
4
|
+
*/
|
|
5
|
+
import { COMMON_GITIGNORE, COMMON_TSCONFIG } from './types.js';
|
|
6
|
+
export const mvcTemplate = {
|
|
7
|
+
id: 'mvc',
|
|
8
|
+
name: 'MVC / Layered Architecture',
|
|
9
|
+
description: 'Structured enterprise layout: routes, controllers, services, and models',
|
|
10
|
+
generate: (projectName) => {
|
|
11
|
+
return {
|
|
12
|
+
'package.json': JSON.stringify({
|
|
13
|
+
name: projectName,
|
|
14
|
+
version: '1.0.0',
|
|
15
|
+
type: 'module',
|
|
16
|
+
scripts: {
|
|
17
|
+
dev: 'node --experimental-strip-types src/server.ts',
|
|
18
|
+
build: 'tsc',
|
|
19
|
+
start: 'node dist/server.js',
|
|
20
|
+
test: 'node --test tests/*.test.ts',
|
|
21
|
+
},
|
|
22
|
+
dependencies: {
|
|
23
|
+
frootjs: '^1.0.1',
|
|
24
|
+
},
|
|
25
|
+
devDependencies: {
|
|
26
|
+
'@types/node': '^22.10.0',
|
|
27
|
+
typescript: '^5.8.0',
|
|
28
|
+
},
|
|
29
|
+
}, null, 2) + '\n',
|
|
30
|
+
'tsconfig.json': COMMON_TSCONFIG,
|
|
31
|
+
'.gitignore': COMMON_GITIGNORE,
|
|
32
|
+
'README.md': `# ${projectName}
|
|
33
|
+
|
|
34
|
+
Froot application built with the **MVC / Layered Architecture** pattern.
|
|
35
|
+
|
|
36
|
+
## Directory Structure
|
|
37
|
+
|
|
38
|
+
\`\`\`
|
|
39
|
+
src/
|
|
40
|
+
├── controllers/ # Request handlers & HTTP response formatting
|
|
41
|
+
│ ├── health.controller.ts
|
|
42
|
+
│ └── user.controller.ts
|
|
43
|
+
├── services/ # Core business logic & database interaction
|
|
44
|
+
│ └── user.service.ts
|
|
45
|
+
├── models/ # TypeScript interfaces & domain types
|
|
46
|
+
│ └── user.model.ts
|
|
47
|
+
├── routes/ # Froot route definitions & sub-app mounting
|
|
48
|
+
│ ├── health.routes.ts
|
|
49
|
+
│ ├── user.routes.ts
|
|
50
|
+
│ └── index.ts
|
|
51
|
+
├── middleware/ # Custom onion middleware
|
|
52
|
+
│ └── request-logger.ts
|
|
53
|
+
├── app.ts # Application assembly
|
|
54
|
+
└── server.ts # Server entry point
|
|
55
|
+
\`\`\`
|
|
56
|
+
|
|
57
|
+
## Getting Started
|
|
58
|
+
|
|
59
|
+
\`\`\`bash
|
|
60
|
+
# 1. Install dependencies
|
|
61
|
+
pnpm install
|
|
62
|
+
|
|
63
|
+
# 2. Run in development mode
|
|
64
|
+
pnpm run dev
|
|
65
|
+
|
|
66
|
+
# 3. Build for production
|
|
67
|
+
pnpm run build
|
|
68
|
+
pnpm start
|
|
69
|
+
\`\`\`
|
|
70
|
+
`,
|
|
71
|
+
'src/models/user.model.ts': `export interface User {
|
|
72
|
+
id: string;
|
|
73
|
+
name: string;
|
|
74
|
+
email: string;
|
|
75
|
+
role: 'admin' | 'user' | 'guest';
|
|
76
|
+
createdAt: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface CreateUserInput {
|
|
80
|
+
name: string;
|
|
81
|
+
email: string;
|
|
82
|
+
role?: 'admin' | 'user' | 'guest';
|
|
83
|
+
}
|
|
84
|
+
`,
|
|
85
|
+
'src/services/user.service.ts': `import type { User, CreateUserInput } from '../models/user.model.js';
|
|
86
|
+
|
|
87
|
+
// In-memory mock database
|
|
88
|
+
const usersTable = new Map<string, User>([
|
|
89
|
+
['usr_1', { id: 'usr_1', name: 'Alice Smith', email: 'alice@example.com', role: 'admin', createdAt: new Date().toISOString() }],
|
|
90
|
+
['usr_2', { id: 'usr_2', name: 'Bob Jones', email: 'bob@example.com', role: 'user', createdAt: new Date().toISOString() }],
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
export class UserService {
|
|
94
|
+
static async findAll(): Promise<User[]> {
|
|
95
|
+
return Array.from(usersTable.values());
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
static async findById(id: string): Promise<User | null> {
|
|
99
|
+
return usersTable.get(id) ?? null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
static async create(input: CreateUserInput): Promise<User> {
|
|
103
|
+
const id = 'usr_' + Math.random().toString(36).substring(2, 9);
|
|
104
|
+
const user: User = {
|
|
105
|
+
id,
|
|
106
|
+
name: input.name,
|
|
107
|
+
email: input.email,
|
|
108
|
+
role: input.role ?? 'user',
|
|
109
|
+
createdAt: new Date().toISOString(),
|
|
110
|
+
};
|
|
111
|
+
usersTable.set(id, user);
|
|
112
|
+
return user;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
static async delete(id: string): Promise<boolean> {
|
|
116
|
+
return usersTable.delete(id);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
`,
|
|
120
|
+
'src/controllers/health.controller.ts': `import type { Context } from 'frootjs';
|
|
121
|
+
|
|
122
|
+
export class HealthController {
|
|
123
|
+
static getHealth(c: Context) {
|
|
124
|
+
return c.json({
|
|
125
|
+
status: 'ok',
|
|
126
|
+
uptime: process.uptime(),
|
|
127
|
+
timestamp: new Date().toISOString(),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
`,
|
|
132
|
+
'src/controllers/user.controller.ts': `import type { Context } from 'frootjs';
|
|
133
|
+
import { UserService } from '../services/user.service.js';
|
|
134
|
+
import type { CreateUserInput } from '../models/user.model.js';
|
|
135
|
+
|
|
136
|
+
export class UserController {
|
|
137
|
+
static async list(c: Context) {
|
|
138
|
+
const users = await UserService.findAll();
|
|
139
|
+
return c.json(users);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
static async getById(c: Context) {
|
|
143
|
+
const id = c.req.param('id');
|
|
144
|
+
const user = await UserService.findById(id);
|
|
145
|
+
if (!user) {
|
|
146
|
+
return c.json({ error: 'User not found' }, 404);
|
|
147
|
+
}
|
|
148
|
+
return c.json(user);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
static async create(c: Context) {
|
|
152
|
+
const body = await c.req.json<CreateUserInput>();
|
|
153
|
+
if (!body.name || !body.email) {
|
|
154
|
+
return c.json({ error: 'Name and email are required' }, 400);
|
|
155
|
+
}
|
|
156
|
+
const user = await UserService.create(body);
|
|
157
|
+
return c.json(user, 201);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
static async remove(c: Context) {
|
|
161
|
+
const id = c.req.param('id');
|
|
162
|
+
const ok = await UserService.delete(id);
|
|
163
|
+
if (!ok) {
|
|
164
|
+
return c.json({ error: 'User not found' }, 404);
|
|
165
|
+
}
|
|
166
|
+
return c.json({ success: true, message: 'User deleted' });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
`,
|
|
170
|
+
'src/routes/health.routes.ts': `import { Froot } from 'frootjs';
|
|
171
|
+
import { HealthController } from '../controllers/health.controller.js';
|
|
172
|
+
|
|
173
|
+
export const healthRoutes = new Froot();
|
|
174
|
+
|
|
175
|
+
healthRoutes.get('/health', HealthController.getHealth);
|
|
176
|
+
`,
|
|
177
|
+
'src/routes/user.routes.ts': `import { Froot } from 'frootjs';
|
|
178
|
+
import { UserController } from '../controllers/user.controller.js';
|
|
179
|
+
|
|
180
|
+
export const userRoutes = new Froot();
|
|
181
|
+
|
|
182
|
+
userRoutes.get('/users', UserController.list);
|
|
183
|
+
userRoutes.get('/users/:id', UserController.getById);
|
|
184
|
+
userRoutes.post('/users', UserController.create);
|
|
185
|
+
userRoutes.delete('/users/:id', UserController.remove);
|
|
186
|
+
`,
|
|
187
|
+
'src/routes/index.ts': `import { Froot } from 'frootjs';
|
|
188
|
+
import { healthRoutes } from './health.routes.js';
|
|
189
|
+
import { userRoutes } from './user.routes.js';
|
|
190
|
+
|
|
191
|
+
export const apiRoutes = new Froot();
|
|
192
|
+
|
|
193
|
+
// Mount sub-apps
|
|
194
|
+
apiRoutes.route('/api', healthRoutes);
|
|
195
|
+
apiRoutes.route('/api', userRoutes);
|
|
196
|
+
`,
|
|
197
|
+
'src/middleware/request-logger.ts': `import type { MiddlewareHandler } from 'frootjs';
|
|
198
|
+
|
|
199
|
+
export const requestLogger: MiddlewareHandler = async (c, next) => {
|
|
200
|
+
const start = performance.now();
|
|
201
|
+
await next();
|
|
202
|
+
const duration = (performance.now() - start).toFixed(2);
|
|
203
|
+
console.log(\`[\${c.req.method}] \${c.req.path} - \${c.statusCode} (\${duration}ms)\`);
|
|
204
|
+
};
|
|
205
|
+
`,
|
|
206
|
+
'src/app.ts': `import { Froot } from 'frootjs';
|
|
207
|
+
import { cors } from 'frootjs/cors';
|
|
208
|
+
import { requestLogger } from './middleware/request-logger.js';
|
|
209
|
+
import { apiRoutes } from './routes/index.js';
|
|
210
|
+
|
|
211
|
+
export const app = new Froot();
|
|
212
|
+
|
|
213
|
+
// Global middleware
|
|
214
|
+
app.use(cors());
|
|
215
|
+
app.use(requestLogger);
|
|
216
|
+
|
|
217
|
+
// Mount API routes
|
|
218
|
+
app.route('', apiRoutes);
|
|
219
|
+
|
|
220
|
+
// Root route
|
|
221
|
+
app.get('/', (c) => {
|
|
222
|
+
return c.json({
|
|
223
|
+
message: 'Welcome to Froot MVC API',
|
|
224
|
+
docs: '/api/health',
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
`,
|
|
228
|
+
'src/server.ts': `import { app } from './app.js';
|
|
229
|
+
|
|
230
|
+
const PORT = Number(process.env.PORT) || 3000;
|
|
231
|
+
|
|
232
|
+
app.listen(PORT, () => {
|
|
233
|
+
console.log(\`Server is running on http://localhost:\${PORT}\`);
|
|
234
|
+
});
|
|
235
|
+
`,
|
|
236
|
+
};
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
//# sourceMappingURL=mvc.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mvc.js","sourceRoot":"","sources":["../../src/templates/mvc.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAuC,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAEpG,MAAM,CAAC,MAAM,WAAW,GAAuB;IAC7C,EAAE,EAAE,KAAK;IACT,IAAI,EAAE,4BAA4B;IAClC,WAAW,EAAE,yEAAyE;IACtF,QAAQ,EAAE,CAAC,WAAmB,EAAmB,EAAE;QACjD,OAAO;YACL,cAAc,EAAE,IAAI,CAAC,SAAS,CAC5B;gBACE,IAAI,EAAE,WAAW;gBACjB,OAAO,EAAE,OAAO;gBAChB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE;oBACP,GAAG,EAAE,+CAA+C;oBACpD,KAAK,EAAE,KAAK;oBACZ,KAAK,EAAE,qBAAqB;oBAC5B,IAAI,EAAE,6BAA6B;iBACpC;gBACD,YAAY,EAAE;oBACZ,OAAO,EAAE,QAAQ;iBAClB;gBACD,eAAe,EAAE;oBACf,aAAa,EAAE,UAAU;oBACzB,UAAU,EAAE,QAAQ;iBACrB;aACF,EACD,IAAI,EACJ,CAAC,CACF,GAAG,IAAI;YAER,eAAe,EAAE,eAAe;YAChC,YAAY,EAAE,gBAAgB;YAE9B,WAAW,EAAE,KAAK,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsClC;YAEK,0BAA0B,EAAE;;;;;;;;;;;;;CAajC;YAEK,8BAA8B,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkCrC;YAEK,sCAAsC,EAAE;;;;;;;;;;;CAW7C;YAEK,oCAAoC,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqC3C;YAEK,6BAA6B,EAAE;;;;;;CAMpC;YAEK,2BAA2B,EAAE;;;;;;;;;CASlC;YAEK,qBAAqB,EAAE;;;;;;;;;CAS5B;YAEK,kCAAkC,EAAE;;;;;;;;CAQzC;YAEK,YAAY,EAAE;;;;;;;;;;;;;;;;;;;;;CAqBnB;YAEK,eAAe,EAAE;;;;;;;CAOtB;SACI,CAAC;IACJ,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI 3.1 & Scalar Docs Template
|
|
3
|
+
* Self-documenting REST API with schema validation, OpenAPI 3.1 JSON, and Scalar interactive UI.
|
|
4
|
+
*/
|
|
5
|
+
import { TemplateDefinition } from './types.js';
|
|
6
|
+
export declare const openapiTemplate: TemplateDefinition;
|
|
7
|
+
//# sourceMappingURL=openapi.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../../src/templates/openapi.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,kBAAkB,EAAsD,MAAM,YAAY,CAAC;AAEpG,eAAO,MAAM,eAAe,EAAE,kBAgN7B,CAAC"}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI 3.1 & Scalar Docs Template
|
|
3
|
+
* Self-documenting REST API with schema validation, OpenAPI 3.1 JSON, and Scalar interactive UI.
|
|
4
|
+
*/
|
|
5
|
+
import { COMMON_GITIGNORE, COMMON_TSCONFIG } from './types.js';
|
|
6
|
+
export const openapiTemplate = {
|
|
7
|
+
id: 'openapi',
|
|
8
|
+
name: 'OpenAPI 3.1 & Interactive Docs',
|
|
9
|
+
description: 'Self-documenting REST API with auto-generated OpenAPI 3.1 spec & Scalar reference UI',
|
|
10
|
+
generate: (projectName) => {
|
|
11
|
+
return {
|
|
12
|
+
'package.json': JSON.stringify({
|
|
13
|
+
name: projectName,
|
|
14
|
+
version: '1.0.0',
|
|
15
|
+
type: 'module',
|
|
16
|
+
scripts: {
|
|
17
|
+
dev: 'node --experimental-strip-types src/server.ts',
|
|
18
|
+
build: 'tsc',
|
|
19
|
+
start: 'node dist/server.js',
|
|
20
|
+
test: 'node --test tests/*.test.ts',
|
|
21
|
+
},
|
|
22
|
+
dependencies: {
|
|
23
|
+
frootjs: '^1.0.1',
|
|
24
|
+
},
|
|
25
|
+
devDependencies: {
|
|
26
|
+
'@types/node': '^22.10.0',
|
|
27
|
+
typescript: '^5.8.0',
|
|
28
|
+
},
|
|
29
|
+
}, null, 2) + '\n',
|
|
30
|
+
'tsconfig.json': COMMON_TSCONFIG,
|
|
31
|
+
'.gitignore': COMMON_GITIGNORE,
|
|
32
|
+
'README.md': `# ${projectName}
|
|
33
|
+
|
|
34
|
+
Froot application built with the **OpenAPI 3.1 & Interactive Docs** pattern.
|
|
35
|
+
|
|
36
|
+
## Features
|
|
37
|
+
|
|
38
|
+
- **Automatic OpenAPI 3.1 Spec**: Machine-readable spec available at \`/openapi.json\`
|
|
39
|
+
- **Scalar Interactive UI**: Modern, high-performance API reference documentation at \`/docs\`
|
|
40
|
+
- **Typed Schema Contracts**: Schemas defined inline with routes, supporting JSON Schema, Standard Schema v1, and Zod
|
|
41
|
+
|
|
42
|
+
## Getting Started
|
|
43
|
+
|
|
44
|
+
\`\`\`bash
|
|
45
|
+
# 1. Install dependencies
|
|
46
|
+
pnpm install
|
|
47
|
+
|
|
48
|
+
# 2. Run in development mode
|
|
49
|
+
pnpm run dev
|
|
50
|
+
|
|
51
|
+
# 3. Explore Interactive Docs
|
|
52
|
+
http://localhost:3000/docs
|
|
53
|
+
\`\`\`
|
|
54
|
+
`,
|
|
55
|
+
'src/schemas.ts': `export const TaskSchema = {
|
|
56
|
+
type: 'object',
|
|
57
|
+
properties: {
|
|
58
|
+
id: { type: 'string', description: 'Unique task identifier' },
|
|
59
|
+
title: { type: 'string', description: 'Task title' },
|
|
60
|
+
completed: { type: 'boolean', description: 'Completion status' },
|
|
61
|
+
createdAt: { type: 'string', format: 'date-time' },
|
|
62
|
+
},
|
|
63
|
+
required: ['id', 'title', 'completed', 'createdAt'],
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const CreateTaskInputSchema = {
|
|
67
|
+
type: 'object',
|
|
68
|
+
properties: {
|
|
69
|
+
title: { type: 'string', description: 'Title of the new task' },
|
|
70
|
+
},
|
|
71
|
+
required: ['title'],
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const ErrorResponseSchema = {
|
|
75
|
+
type: 'object',
|
|
76
|
+
properties: {
|
|
77
|
+
error: { type: 'string', description: 'Error message' },
|
|
78
|
+
},
|
|
79
|
+
required: ['error'],
|
|
80
|
+
};
|
|
81
|
+
`,
|
|
82
|
+
'src/routes/tasks.ts': `import { Froot } from 'frootjs';
|
|
83
|
+
import { TaskSchema, CreateTaskInputSchema, ErrorResponseSchema } from '../schemas.js';
|
|
84
|
+
|
|
85
|
+
export interface Task {
|
|
86
|
+
id: string;
|
|
87
|
+
title: string;
|
|
88
|
+
completed: boolean;
|
|
89
|
+
createdAt: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const tasks: Task[] = [
|
|
93
|
+
{ id: 'task_1', title: 'Spin up Froot project', completed: true, createdAt: new Date().toISOString() },
|
|
94
|
+
{ id: 'task_2', title: 'Explore Scalar documentation at /docs', completed: false, createdAt: new Date().toISOString() },
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
export const taskRoutes = new Froot();
|
|
98
|
+
|
|
99
|
+
// List tasks
|
|
100
|
+
taskRoutes.get(
|
|
101
|
+
'/tasks',
|
|
102
|
+
{
|
|
103
|
+
summary: 'List all tasks',
|
|
104
|
+
description: 'Retrieves the complete list of scheduled tasks.',
|
|
105
|
+
tags: ['Tasks'],
|
|
106
|
+
operationId: 'listTasks',
|
|
107
|
+
response: {
|
|
108
|
+
type: 'array',
|
|
109
|
+
items: TaskSchema,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
(c) => c.json(tasks)
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// Get task by ID
|
|
116
|
+
taskRoutes.get(
|
|
117
|
+
'/tasks/:id',
|
|
118
|
+
{
|
|
119
|
+
summary: 'Get task by ID',
|
|
120
|
+
tags: ['Tasks'],
|
|
121
|
+
operationId: 'getTaskById',
|
|
122
|
+
params: {
|
|
123
|
+
type: 'object',
|
|
124
|
+
properties: {
|
|
125
|
+
id: { type: 'string', description: 'Task ID' },
|
|
126
|
+
},
|
|
127
|
+
required: ['id'],
|
|
128
|
+
},
|
|
129
|
+
response: TaskSchema,
|
|
130
|
+
errors: {
|
|
131
|
+
404: ErrorResponseSchema,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
(c) => {
|
|
135
|
+
const id = c.req.param('id');
|
|
136
|
+
const task = tasks.find((t) => t.id === id);
|
|
137
|
+
if (!task) {
|
|
138
|
+
return c.json({ error: 'Task not found' }, 404);
|
|
139
|
+
}
|
|
140
|
+
return c.json(task);
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// Create task
|
|
145
|
+
taskRoutes.post(
|
|
146
|
+
'/tasks',
|
|
147
|
+
{
|
|
148
|
+
summary: 'Create a new task',
|
|
149
|
+
tags: ['Tasks'],
|
|
150
|
+
operationId: 'createTask',
|
|
151
|
+
body: CreateTaskInputSchema,
|
|
152
|
+
response: TaskSchema,
|
|
153
|
+
errors: {
|
|
154
|
+
400: ErrorResponseSchema,
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
async (c) => {
|
|
158
|
+
const body = await c.req.json<{ title?: string }>();
|
|
159
|
+
if (!body.title) {
|
|
160
|
+
return c.json({ error: 'Title is required' }, 400);
|
|
161
|
+
}
|
|
162
|
+
const newTask: Task = {
|
|
163
|
+
id: 'task_' + Math.random().toString(36).substring(2, 9),
|
|
164
|
+
title: body.title,
|
|
165
|
+
completed: false,
|
|
166
|
+
createdAt: new Date().toISOString(),
|
|
167
|
+
};
|
|
168
|
+
tasks.push(newTask);
|
|
169
|
+
return c.json(newTask, 201);
|
|
170
|
+
}
|
|
171
|
+
);
|
|
172
|
+
`,
|
|
173
|
+
'src/server.ts': `import { Froot } from 'frootjs';
|
|
174
|
+
import { cors } from 'frootjs/cors';
|
|
175
|
+
import { taskRoutes } from './routes/tasks.js';
|
|
176
|
+
|
|
177
|
+
export const app = new Froot();
|
|
178
|
+
|
|
179
|
+
app.use(cors());
|
|
180
|
+
|
|
181
|
+
// Mount sub-routes
|
|
182
|
+
app.route('/api', taskRoutes);
|
|
183
|
+
|
|
184
|
+
// Root redirect to /docs
|
|
185
|
+
app.get('/', (c) => {
|
|
186
|
+
return c.redirect('/docs');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Auto-mount OpenAPI 3.1 specification (/openapi.json) and Scalar UI (/docs)
|
|
190
|
+
app.openapi({
|
|
191
|
+
title: '${projectName} API',
|
|
192
|
+
version: '1.0.0',
|
|
193
|
+
description: 'Self-documenting API powered by Froot OpenAPI 3.1 & Scalar Reference UI.',
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const PORT = Number(process.env.PORT) || 3000;
|
|
197
|
+
app.listen(PORT, () => {
|
|
198
|
+
console.log(\`Server running on http://localhost:\${PORT}\`);
|
|
199
|
+
console.log(\`Interactive Docs available at http://localhost:\${PORT}/docs\`);
|
|
200
|
+
console.log(\`OpenAPI JSON available at http://localhost:\${PORT}/openapi.json\`);
|
|
201
|
+
});
|
|
202
|
+
`,
|
|
203
|
+
};
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
//# sourceMappingURL=openapi.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi.js","sourceRoot":"","sources":["../../src/templates/openapi.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAuC,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAEpG,MAAM,CAAC,MAAM,eAAe,GAAuB;IACjD,EAAE,EAAE,SAAS;IACb,IAAI,EAAE,gCAAgC;IACtC,WAAW,EAAE,sFAAsF;IACnG,QAAQ,EAAE,CAAC,WAAmB,EAAmB,EAAE;QACjD,OAAO;YACL,cAAc,EAAE,IAAI,CAAC,SAAS,CAC5B;gBACE,IAAI,EAAE,WAAW;gBACjB,OAAO,EAAE,OAAO;gBAChB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE;oBACP,GAAG,EAAE,+CAA+C;oBACpD,KAAK,EAAE,KAAK;oBACZ,KAAK,EAAE,qBAAqB;oBAC5B,IAAI,EAAE,6BAA6B;iBACpC;gBACD,YAAY,EAAE;oBACZ,OAAO,EAAE,QAAQ;iBAClB;gBACD,eAAe,EAAE;oBACf,aAAa,EAAE,UAAU;oBACzB,UAAU,EAAE,QAAQ;iBACrB;aACF,EACD,IAAI,EACJ,CAAC,CACF,GAAG,IAAI;YAER,eAAe,EAAE,eAAe;YAChC,YAAY,EAAE,gBAAgB;YAE9B,WAAW,EAAE,KAAK,WAAW;;;;;;;;;;;;;;;;;;;;;;CAsBlC;YAEK,gBAAgB,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BvB;YAEK,qBAAqB,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0F5B;YAEK,eAAe,EAAE;;;;;;;;;;;;;;;;;;YAkBX,WAAW;;;;;;;;;;;CAWtB;SACI,CAAC;IACJ,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming & Server-Sent Events (SSE) Template
|
|
3
|
+
* Demonstrates real-time SSE streaming, LLM-style token streaming, and an interactive HTML live client.
|
|
4
|
+
*/
|
|
5
|
+
import { TemplateDefinition } from './types.js';
|
|
6
|
+
export declare const streamingTemplate: TemplateDefinition;
|
|
7
|
+
//# sourceMappingURL=streaming.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"streaming.d.ts","sourceRoot":"","sources":["../../src/templates/streaming.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,kBAAkB,EAAsD,MAAM,YAAY,CAAC;AAEpG,eAAO,MAAM,iBAAiB,EAAE,kBAmN/B,CAAC"}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming & Server-Sent Events (SSE) Template
|
|
3
|
+
* Demonstrates real-time SSE streaming, LLM-style token streaming, and an interactive HTML live client.
|
|
4
|
+
*/
|
|
5
|
+
import { COMMON_GITIGNORE, COMMON_TSCONFIG } from './types.js';
|
|
6
|
+
export const streamingTemplate = {
|
|
7
|
+
id: 'streaming',
|
|
8
|
+
name: 'Real-Time Streaming & SSE',
|
|
9
|
+
description: 'Server-Sent Events, AI token streaming simulation, and interactive web dashboard',
|
|
10
|
+
generate: (projectName) => {
|
|
11
|
+
return {
|
|
12
|
+
'package.json': JSON.stringify({
|
|
13
|
+
name: projectName,
|
|
14
|
+
version: '1.0.0',
|
|
15
|
+
type: 'module',
|
|
16
|
+
scripts: {
|
|
17
|
+
dev: 'node --experimental-strip-types src/server.ts',
|
|
18
|
+
build: 'tsc',
|
|
19
|
+
start: 'node dist/server.js',
|
|
20
|
+
},
|
|
21
|
+
dependencies: {
|
|
22
|
+
frootjs: '^1.0.1',
|
|
23
|
+
},
|
|
24
|
+
devDependencies: {
|
|
25
|
+
'@types/node': '^22.10.0',
|
|
26
|
+
typescript: '^5.8.0',
|
|
27
|
+
},
|
|
28
|
+
}, null, 2) + '\n',
|
|
29
|
+
'tsconfig.json': COMMON_TSCONFIG,
|
|
30
|
+
'.gitignore': COMMON_GITIGNORE,
|
|
31
|
+
'README.md': `# ${projectName}
|
|
32
|
+
|
|
33
|
+
Froot application built with the **Real-Time Streaming & Server-Sent Events (SSE)** pattern.
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
- **SSE Telemetry Channel**: Streams real-time metrics (CPU memory, active requests) using \`streamSSE\`
|
|
38
|
+
- **AI Token Streamer**: Demonstrates streaming LLM-style markdown tokens chunk-by-chunk using \`streamText\`
|
|
39
|
+
- **Live Interactive Dashboard**: Embedded web client displaying real-time graphs and live chat stream
|
|
40
|
+
|
|
41
|
+
## Getting Started
|
|
42
|
+
|
|
43
|
+
\`\`\`bash
|
|
44
|
+
# 1. Install dependencies
|
|
45
|
+
pnpm install
|
|
46
|
+
|
|
47
|
+
# 2. Run in development mode
|
|
48
|
+
pnpm run dev
|
|
49
|
+
|
|
50
|
+
# 3. Open in browser
|
|
51
|
+
http://localhost:3000
|
|
52
|
+
\`\`\`
|
|
53
|
+
`,
|
|
54
|
+
'src/server.ts': `import { Froot } from 'frootjs';
|
|
55
|
+
import { cors } from 'frootjs/cors';
|
|
56
|
+
import { streamSSE, streamText, sleep } from 'frootjs/streaming';
|
|
57
|
+
|
|
58
|
+
export const app = new Froot();
|
|
59
|
+
|
|
60
|
+
app.use(cors());
|
|
61
|
+
|
|
62
|
+
// Embedded Dashboard HTML
|
|
63
|
+
app.get('/', (c) => {
|
|
64
|
+
return c.html(\`<!DOCTYPE html>
|
|
65
|
+
<html lang="en">
|
|
66
|
+
<head>
|
|
67
|
+
<meta charset="UTF-8">
|
|
68
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
69
|
+
<title>Froot Streaming Demo</title>
|
|
70
|
+
<style>
|
|
71
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
72
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #0f172a; color: #f8fafc; padding: 2rem; }
|
|
73
|
+
.container { max-width: 800px; margin: 0 auto; display: flex; flex-direction: column; gap: 1.5rem; }
|
|
74
|
+
.card { background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 1.5rem; }
|
|
75
|
+
h1 { color: #10b981; font-size: 1.75rem; margin-bottom: 0.5rem; }
|
|
76
|
+
h2 { font-size: 1.25rem; margin-bottom: 1rem; color: #38bdf8; }
|
|
77
|
+
pre { background: #0b0f19; padding: 1rem; border-radius: 6px; overflow-x: auto; font-size: 0.9rem; line-height: 1.4; }
|
|
78
|
+
.metrics { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-bottom: 1rem; }
|
|
79
|
+
.metric-box { background: #0b0f19; padding: 1rem; border-radius: 6px; text-align: center; }
|
|
80
|
+
.metric-value { font-size: 1.5rem; font-weight: bold; color: #10b981; }
|
|
81
|
+
.metric-label { font-size: 0.8rem; color: #94a3b8; text-transform: uppercase; margin-top: 0.25rem; }
|
|
82
|
+
button { background: #10b981; color: #0f172a; border: none; padding: 0.6rem 1.2rem; border-radius: 6px; font-weight: bold; cursor: pointer; }
|
|
83
|
+
button:hover { background: #059669; }
|
|
84
|
+
</style>
|
|
85
|
+
</head>
|
|
86
|
+
<body>
|
|
87
|
+
<div class="container">
|
|
88
|
+
<div>
|
|
89
|
+
<h1>Froot Streaming Engine</h1>
|
|
90
|
+
<p style="color: #94a3b8;">High-throughput Server-Sent Events & chunked HTTP streaming</p>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<!-- SSE Metrics Card -->
|
|
94
|
+
<div class="card">
|
|
95
|
+
<h2>Live Telemetry Stream (SSE)</h2>
|
|
96
|
+
<div class="metrics">
|
|
97
|
+
<div class="metric-box">
|
|
98
|
+
<div id="m-memory" class="metric-value">--</div>
|
|
99
|
+
<div class="metric-label">Memory (MB)</div>
|
|
100
|
+
</div>
|
|
101
|
+
<div class="metric-box">
|
|
102
|
+
<div id="m-uptime" class="metric-value">--</div>
|
|
103
|
+
<div class="metric-label">Uptime (s)</div>
|
|
104
|
+
</div>
|
|
105
|
+
<div class="metric-box">
|
|
106
|
+
<div id="m-count" class="metric-value">0</div>
|
|
107
|
+
<div class="metric-label">Events Received</div>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
<pre id="sse-log">Connecting to /sse/metrics...</pre>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<!-- LLM Stream Card -->
|
|
114
|
+
<div class="card">
|
|
115
|
+
<h2>AI Token Stream Simulation</h2>
|
|
116
|
+
<div style="margin-bottom: 1rem;">
|
|
117
|
+
<button id="btn-stream">Generate Streaming Text</button>
|
|
118
|
+
</div>
|
|
119
|
+
<pre id="stream-output">Click button to trigger token-by-token stream...</pre>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<script>
|
|
124
|
+
// SSE Stream
|
|
125
|
+
let count = 0;
|
|
126
|
+
const es = new EventSource('/sse/metrics');
|
|
127
|
+
const log = document.getElementById('sse-log');
|
|
128
|
+
es.addEventListener('telemetry', (e) => {
|
|
129
|
+
const data = JSON.parse(e.data);
|
|
130
|
+
count++;
|
|
131
|
+
document.getElementById('m-memory').textContent = data.memoryUsageMB;
|
|
132
|
+
document.getElementById('m-uptime').textContent = data.uptimeSec;
|
|
133
|
+
document.getElementById('m-count').textContent = count;
|
|
134
|
+
log.textContent = 'Last event: ' + JSON.stringify(data, null, 2);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Token Stream
|
|
138
|
+
const btn = document.getElementById('btn-stream');
|
|
139
|
+
const output = document.getElementById('stream-output');
|
|
140
|
+
btn.addEventListener('click', async () => {
|
|
141
|
+
btn.disabled = true;
|
|
142
|
+
output.textContent = '';
|
|
143
|
+
const response = await fetch('/api/chat/stream');
|
|
144
|
+
const reader = response.body.getReader();
|
|
145
|
+
const decoder = new TextDecoder();
|
|
146
|
+
while (true) {
|
|
147
|
+
const { value, done } = await reader.read();
|
|
148
|
+
if (done) break;
|
|
149
|
+
output.textContent += decoder.decode(value);
|
|
150
|
+
}
|
|
151
|
+
btn.disabled = false;
|
|
152
|
+
});
|
|
153
|
+
</script>
|
|
154
|
+
</body>
|
|
155
|
+
</html>\`);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Real-time SSE Telemetry endpoint
|
|
159
|
+
app.get('/sse/metrics', (c) => {
|
|
160
|
+
return streamSSE(
|
|
161
|
+
c,
|
|
162
|
+
async (stream) => {
|
|
163
|
+
let eventId = 1;
|
|
164
|
+
while (!stream.signal.aborted) {
|
|
165
|
+
const mem = process.memoryUsage();
|
|
166
|
+
const payload = {
|
|
167
|
+
memoryUsageMB: Math.round(mem.rss / 1024 / 1024),
|
|
168
|
+
uptimeSec: Math.floor(process.uptime()),
|
|
169
|
+
timestamp: new Date().toISOString(),
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
await stream.writeSSE({
|
|
173
|
+
event: 'telemetry',
|
|
174
|
+
id: String(eventId++),
|
|
175
|
+
data: JSON.stringify(payload),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
await sleep(1000, stream.signal);
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
{ heartbeatMs: 15_000 }
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// AI Token Streaming Simulation endpoint
|
|
186
|
+
app.get('/api/chat/stream', (c) => {
|
|
187
|
+
const sampleResponse = [
|
|
188
|
+
'Hello! ', 'I ', 'am ', 'streaming ', 'from ', 'a ', 'Froot ', 'server. ',
|
|
189
|
+
'Each ', 'token ', 'is ', 'dispatched ', 'with ', 'zero-copy ', 'efficiency.\\n\\n',
|
|
190
|
+
'Froot ', 'supports ', 'standards-compliant ', 'WHATWG ', 'ReadableStream ',
|
|
191
|
+
'pipelines ', 'compatible ', 'with ', 'all ', 'modern ', 'edge ', 'runtimes.',
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
return streamText(c, async (stream) => {
|
|
195
|
+
for (const chunk of sampleResponse) {
|
|
196
|
+
if (stream.signal.aborted) break;
|
|
197
|
+
await stream.write(chunk);
|
|
198
|
+
await sleep(60, stream.signal);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const PORT = Number(process.env.PORT) || 3000;
|
|
204
|
+
app.listen(PORT, () => {
|
|
205
|
+
console.log(\`Streaming server running on http://localhost:\${PORT}\`);
|
|
206
|
+
});
|
|
207
|
+
`,
|
|
208
|
+
};
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
//# sourceMappingURL=streaming.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"streaming.js","sourceRoot":"","sources":["../../src/templates/streaming.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAuC,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAEpG,MAAM,CAAC,MAAM,iBAAiB,GAAuB;IACnD,EAAE,EAAE,WAAW;IACf,IAAI,EAAE,2BAA2B;IACjC,WAAW,EAAE,kFAAkF;IAC/F,QAAQ,EAAE,CAAC,WAAmB,EAAmB,EAAE;QACjD,OAAO;YACL,cAAc,EAAE,IAAI,CAAC,SAAS,CAC5B;gBACE,IAAI,EAAE,WAAW;gBACjB,OAAO,EAAE,OAAO;gBAChB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE;oBACP,GAAG,EAAE,+CAA+C;oBACpD,KAAK,EAAE,KAAK;oBACZ,KAAK,EAAE,qBAAqB;iBAC7B;gBACD,YAAY,EAAE;oBACZ,OAAO,EAAE,QAAQ;iBAClB;gBACD,eAAe,EAAE;oBACf,aAAa,EAAE,UAAU;oBACzB,UAAU,EAAE,QAAQ;iBACrB;aACF,EACD,IAAI,EACJ,CAAC,CACF,GAAG,IAAI;YAER,eAAe,EAAE,eAAe;YAChC,YAAY,EAAE,gBAAgB;YAE9B,WAAW,EAAE,KAAK,WAAW;;;;;;;;;;;;;;;;;;;;;;CAsBlC;YAEK,eAAe,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyJtB;SACI,CAAC;IACJ,CAAC;CACF,CAAC"}
|