@avijs/cli 0.0.0-stage → 0.1.1
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 +20 -2
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +4 -0
- package/dist/bin.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +154 -0
- package/dist/cli.js.map +1 -0
- package/dist/generate.d.ts +3 -0
- package/dist/generate.d.ts.map +1 -0
- package/dist/generate.js +308 -0
- package/dist/generate.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/templates.d.ts +16 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +310 -0
- package/dist/templates.js.map +1 -0
- package/package.json +52 -3
- package/src/bin.ts +4 -0
- package/src/cli.ts +160 -0
- package/src/generate.ts +394 -0
- package/src/index.ts +3 -0
- package/src/templates.ts +333 -0
package/src/templates.ts
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
export type TemplateName = 'modular' | 'minimal';
|
|
2
|
+
|
|
3
|
+
export interface TemplateOptions {
|
|
4
|
+
name: string;
|
|
5
|
+
template: TemplateName;
|
|
6
|
+
/** Version range for the @avijs packages. */
|
|
7
|
+
aviVersion: string;
|
|
8
|
+
/**
|
|
9
|
+
* Local package files to use instead of the registry, keyed by package name, such as
|
|
10
|
+
* 'file:../artifacts/avijs-core-0.1.0.tgz'. Also added as npm overrides so packages that depend on
|
|
11
|
+
* each other resolve locally too.
|
|
12
|
+
*/
|
|
13
|
+
localPackages?: Readonly<Record<string, string>>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Every file of a new project, keyed by its path relative to the project folder. */
|
|
17
|
+
export function templateFiles(options: TemplateOptions): Record<string, string> {
|
|
18
|
+
return {
|
|
19
|
+
...shared(options),
|
|
20
|
+
...(options.template === 'modular' ? modular() : minimal()),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function shared({
|
|
25
|
+
name,
|
|
26
|
+
template,
|
|
27
|
+
aviVersion,
|
|
28
|
+
localPackages,
|
|
29
|
+
}: TemplateOptions): Record<string, string> {
|
|
30
|
+
const spec = (pkg: string) => localPackages?.[pkg] ?? aviVersion;
|
|
31
|
+
const pkg = {
|
|
32
|
+
name,
|
|
33
|
+
version: '0.1.0',
|
|
34
|
+
private: true,
|
|
35
|
+
type: 'module',
|
|
36
|
+
scripts: {
|
|
37
|
+
build: 'tsc',
|
|
38
|
+
start: 'node dist/main.js',
|
|
39
|
+
dev: 'avi dev',
|
|
40
|
+
check: 'avi check',
|
|
41
|
+
test: 'tsc && node --test "dist/**/*.test.js"',
|
|
42
|
+
},
|
|
43
|
+
dependencies: { '@avijs/core': spec('@avijs/core') },
|
|
44
|
+
devDependencies: {
|
|
45
|
+
'@avijs/cli': spec('@avijs/cli'),
|
|
46
|
+
'@avijs/testing': spec('@avijs/testing'),
|
|
47
|
+
'@types/node': '^22.0.0',
|
|
48
|
+
typescript: '~6.0.3',
|
|
49
|
+
},
|
|
50
|
+
engines: { node: '>=22.12' },
|
|
51
|
+
...(localPackages ? { overrides: localPackages } : {}),
|
|
52
|
+
};
|
|
53
|
+
const tsconfig = {
|
|
54
|
+
compilerOptions: {
|
|
55
|
+
target: 'ES2023',
|
|
56
|
+
lib: ['ES2023', 'ESNext.Disposable'],
|
|
57
|
+
module: 'NodeNext',
|
|
58
|
+
moduleResolution: 'NodeNext',
|
|
59
|
+
types: ['node'],
|
|
60
|
+
strict: true,
|
|
61
|
+
skipLibCheck: true,
|
|
62
|
+
outDir: 'dist',
|
|
63
|
+
rootDir: 'src',
|
|
64
|
+
sourceMap: true,
|
|
65
|
+
},
|
|
66
|
+
include: ['src'],
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
'package.json': `${JSON.stringify(pkg, null, 2)}\n`,
|
|
70
|
+
'tsconfig.json': `${JSON.stringify(tsconfig, null, 2)}\n`,
|
|
71
|
+
'.gitignore': 'node_modules/\ndist/\n',
|
|
72
|
+
'README.md': `# ${name}
|
|
73
|
+
|
|
74
|
+
An [Avi.js](https://github.com/chrischase011/avi-js) API (${template} template).
|
|
75
|
+
|
|
76
|
+
\`\`\`sh
|
|
77
|
+
npm install
|
|
78
|
+
npm run dev # compile and restart on change
|
|
79
|
+
npm test # compile and run the tests
|
|
80
|
+
npm start # run the compiled app
|
|
81
|
+
\`\`\`
|
|
82
|
+
|
|
83
|
+
Then open http://localhost:3000/health and http://localhost:3000/users.
|
|
84
|
+
`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const MAIN_LISTEN = `const { port } = await app.listen(Number(process.env['PORT'] ?? 3000));
|
|
89
|
+
console.log(\`Listening on http://localhost:\${String(port)}\`);
|
|
90
|
+
|
|
91
|
+
const shutdown = () => {
|
|
92
|
+
void app.close().then(() => process.exit(0));
|
|
93
|
+
};
|
|
94
|
+
process.once('SIGINT', shutdown);
|
|
95
|
+
process.once('SIGTERM', shutdown);
|
|
96
|
+
`;
|
|
97
|
+
|
|
98
|
+
function modular(): Record<string, string> {
|
|
99
|
+
return {
|
|
100
|
+
'src/main.ts': `import { AviFactory } from '@avijs/core';
|
|
101
|
+
import { AppModule } from './app.module.js';
|
|
102
|
+
|
|
103
|
+
const app = await AviFactory.create(AppModule);
|
|
104
|
+
${MAIN_LISTEN}`,
|
|
105
|
+
'src/app.module.ts': `import { Module } from '@avijs/core';
|
|
106
|
+
import { HealthController } from './common/health.controller.js';
|
|
107
|
+
import { UsersModule } from './modules/users/users.module.js';
|
|
108
|
+
|
|
109
|
+
@Module({ imports: [UsersModule], controllers: [HealthController] })
|
|
110
|
+
export class AppModule {}
|
|
111
|
+
`,
|
|
112
|
+
'src/common/health.controller.ts': `import { Controller, Get } from '@avijs/core';
|
|
113
|
+
|
|
114
|
+
@Controller()
|
|
115
|
+
export class HealthController {
|
|
116
|
+
@Get('/health')
|
|
117
|
+
check() {
|
|
118
|
+
return 'ok';
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
`,
|
|
122
|
+
'src/modules/users/users.module.ts': `import { Module } from '@avijs/core';
|
|
123
|
+
import { UsersController } from './users.controller.js';
|
|
124
|
+
import { UsersService } from './users.service.js';
|
|
125
|
+
|
|
126
|
+
@Module({ controllers: [UsersController], providers: [UsersService], exports: [UsersService] })
|
|
127
|
+
export class UsersModule {}
|
|
128
|
+
`,
|
|
129
|
+
'src/modules/users/users.service.ts': `import { Injectable } from '@avijs/core';
|
|
130
|
+
import type { CreateUserDto } from './dto/create-user.dto.js';
|
|
131
|
+
|
|
132
|
+
export interface User {
|
|
133
|
+
id: number;
|
|
134
|
+
name: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@Injectable()
|
|
138
|
+
export class UsersService {
|
|
139
|
+
readonly #users: User[] = [{ id: 1, name: 'Ada' }];
|
|
140
|
+
|
|
141
|
+
findAll(): User[] {
|
|
142
|
+
return this.#users;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
findOne(id: number): User | undefined {
|
|
146
|
+
return this.#users.find((user) => user.id === id);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
create(dto: CreateUserDto): User {
|
|
150
|
+
const user = { id: this.#users.length + 1, name: dto.name };
|
|
151
|
+
this.#users.push(user);
|
|
152
|
+
return user;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
`,
|
|
156
|
+
'src/modules/users/users.controller.ts': `import { Controller, Get, HttpError, Post, reply, type Context } from '@avijs/core';
|
|
157
|
+
import { parseCreateUser } from './dto/create-user.dto.js';
|
|
158
|
+
import { UsersService } from './users.service.js';
|
|
159
|
+
|
|
160
|
+
@Controller('/users')
|
|
161
|
+
export class UsersController {
|
|
162
|
+
static inject = [UsersService] as const;
|
|
163
|
+
|
|
164
|
+
constructor(private readonly users: UsersService) {}
|
|
165
|
+
|
|
166
|
+
@Get()
|
|
167
|
+
findAll() {
|
|
168
|
+
return this.users.findAll();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@Get('/:id')
|
|
172
|
+
findOne(ctx: Context) {
|
|
173
|
+
const user = this.users.findOne(Number(ctx.params['id']));
|
|
174
|
+
if (!user) throw new HttpError(404, 'User not found');
|
|
175
|
+
return user;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
@Post()
|
|
179
|
+
async create(ctx: Context) {
|
|
180
|
+
const dto = parseCreateUser(await ctx.json());
|
|
181
|
+
if (!dto) throw new HttpError(400, 'Body must be an object with a name');
|
|
182
|
+
return reply(this.users.create(dto), { status: 201 });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
`,
|
|
186
|
+
'src/modules/users/dto/create-user.dto.ts': `export interface CreateUserDto {
|
|
187
|
+
name: string;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Swap this for a schema library (Zod, Valibot, ...) and route schemas when the API grows.
|
|
191
|
+
export function parseCreateUser(input: unknown): CreateUserDto | undefined {
|
|
192
|
+
if (typeof input !== 'object' || input === null) return undefined;
|
|
193
|
+
const { name } = input as { name?: unknown };
|
|
194
|
+
return typeof name === 'string' && name.trim() !== '' ? { name: name.trim() } : undefined;
|
|
195
|
+
}
|
|
196
|
+
`,
|
|
197
|
+
'src/modules/users/users.test.ts': `import assert from 'node:assert/strict';
|
|
198
|
+
import { after, before, test } from 'node:test';
|
|
199
|
+
import { createTestClient, type TestClient } from '@avijs/testing';
|
|
200
|
+
import { AppModule } from '../../app.module.js';
|
|
201
|
+
|
|
202
|
+
let client: TestClient;
|
|
203
|
+
before(async () => {
|
|
204
|
+
client = await createTestClient(AppModule);
|
|
205
|
+
});
|
|
206
|
+
after(() => client.close());
|
|
207
|
+
|
|
208
|
+
test('lists users', async () => {
|
|
209
|
+
const res = await client.get('/users');
|
|
210
|
+
assert.equal(res.status, 200);
|
|
211
|
+
assert.deepEqual(res.body, [{ id: 1, name: 'Ada' }]);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('creates a user', async () => {
|
|
215
|
+
const res = await client.post('/users', { name: 'Grace' });
|
|
216
|
+
assert.equal(res.status, 201);
|
|
217
|
+
assert.deepEqual(res.body, { id: 2, name: 'Grace' });
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('rejects a user without a name', async () => {
|
|
221
|
+
assert.equal((await client.post('/users', {})).status, 400);
|
|
222
|
+
});
|
|
223
|
+
`,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function minimal(): Record<string, string> {
|
|
228
|
+
return {
|
|
229
|
+
'src/main.ts': `import { createApp } from './app.js';
|
|
230
|
+
|
|
231
|
+
const app = createApp();
|
|
232
|
+
${MAIN_LISTEN}`,
|
|
233
|
+
'src/app.ts': `import { createAvi } from '@avijs/core';
|
|
234
|
+
import { healthRoutes } from './routes/health.js';
|
|
235
|
+
import { usersRoutes } from './routes/users.js';
|
|
236
|
+
import { UsersService } from './services/users.js';
|
|
237
|
+
|
|
238
|
+
export function createApp() {
|
|
239
|
+
return createAvi().register({
|
|
240
|
+
providers: [UsersService],
|
|
241
|
+
controllers: [healthRoutes, usersRoutes],
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
`,
|
|
245
|
+
'src/routes/health.ts': `import { defineController, route } from '@avijs/core';
|
|
246
|
+
|
|
247
|
+
export const healthRoutes = defineController({
|
|
248
|
+
routes: [route({ method: 'GET', path: '/health', handler: () => 'ok' })],
|
|
249
|
+
});
|
|
250
|
+
`,
|
|
251
|
+
'src/routes/users.ts': `import { HttpError, defineController, reply, route } from '@avijs/core';
|
|
252
|
+
import { UsersService } from '../services/users.js';
|
|
253
|
+
|
|
254
|
+
export const usersRoutes = defineController({
|
|
255
|
+
prefix: '/users',
|
|
256
|
+
inject: [UsersService],
|
|
257
|
+
routes: (users) => [
|
|
258
|
+
route({ method: 'GET', path: '/', handler: () => users.findAll() }),
|
|
259
|
+
route({
|
|
260
|
+
method: 'GET',
|
|
261
|
+
path: '/:id',
|
|
262
|
+
handler: (ctx) => {
|
|
263
|
+
const user = users.findOne(Number(ctx.params.id));
|
|
264
|
+
if (!user) throw new HttpError(404, 'User not found');
|
|
265
|
+
return user;
|
|
266
|
+
},
|
|
267
|
+
}),
|
|
268
|
+
route({
|
|
269
|
+
method: 'POST',
|
|
270
|
+
path: '/',
|
|
271
|
+
handler: async (ctx) => {
|
|
272
|
+
const { name } = (await ctx.json()) as { name?: unknown };
|
|
273
|
+
if (typeof name !== 'string' || name.trim() === '') {
|
|
274
|
+
throw new HttpError(400, 'Body must be an object with a name');
|
|
275
|
+
}
|
|
276
|
+
return reply(users.create(name.trim()), { status: 201 });
|
|
277
|
+
},
|
|
278
|
+
}),
|
|
279
|
+
],
|
|
280
|
+
});
|
|
281
|
+
`,
|
|
282
|
+
'src/services/users.ts': `export interface User {
|
|
283
|
+
id: number;
|
|
284
|
+
name: string;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export class UsersService {
|
|
288
|
+
readonly #users: User[] = [{ id: 1, name: 'Ada' }];
|
|
289
|
+
|
|
290
|
+
findAll(): User[] {
|
|
291
|
+
return this.#users;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
findOne(id: number): User | undefined {
|
|
295
|
+
return this.#users.find((user) => user.id === id);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
create(name: string): User {
|
|
299
|
+
const user = { id: this.#users.length + 1, name };
|
|
300
|
+
this.#users.push(user);
|
|
301
|
+
return user;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
`,
|
|
305
|
+
'src/app.test.ts': `import assert from 'node:assert/strict';
|
|
306
|
+
import { after, before, test } from 'node:test';
|
|
307
|
+
import { createTestClient, type TestClient } from '@avijs/testing';
|
|
308
|
+
import { createApp } from './app.js';
|
|
309
|
+
|
|
310
|
+
let client: TestClient;
|
|
311
|
+
before(async () => {
|
|
312
|
+
client = await createTestClient(createApp());
|
|
313
|
+
});
|
|
314
|
+
after(() => client.close());
|
|
315
|
+
|
|
316
|
+
test('lists users', async () => {
|
|
317
|
+
const res = await client.get('/users');
|
|
318
|
+
assert.equal(res.status, 200);
|
|
319
|
+
assert.deepEqual(res.body, [{ id: 1, name: 'Ada' }]);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test('creates a user', async () => {
|
|
323
|
+
const res = await client.post('/users', { name: 'Grace' });
|
|
324
|
+
assert.equal(res.status, 201);
|
|
325
|
+
assert.deepEqual(res.body, { id: 2, name: 'Grace' });
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test('answers 404 for an unknown user', async () => {
|
|
329
|
+
assert.equal((await client.get('/users/99')).status, 404);
|
|
330
|
+
});
|
|
331
|
+
`,
|
|
332
|
+
};
|
|
333
|
+
}
|