@avijs/cli 0.1.1 → 0.2.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/README.md +7 -7
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +15 -5
- package/dist/cli.js.map +1 -1
- package/dist/generate.d.ts +1 -1
- package/dist/generate.d.ts.map +1 -1
- package/dist/generate.js +372 -7
- package/dist/generate.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/templates.d.ts +3 -0
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js +258 -8
- package/dist/templates.js.map +1 -1
- package/dist/validators.d.ts +27 -0
- package/dist/validators.d.ts.map +1 -0
- package/dist/validators.js +56 -0
- package/dist/validators.js.map +1 -0
- package/package.json +7 -3
- package/src/cli.ts +16 -5
- package/src/generate.ts +402 -7
- package/src/index.ts +1 -0
- package/src/templates.ts +271 -13
- package/src/validators.ts +85 -0
package/dist/templates.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Validator } from './validators.js';
|
|
1
2
|
export type TemplateName = 'modular' | 'minimal';
|
|
2
3
|
export interface TemplateOptions {
|
|
3
4
|
name: string;
|
|
@@ -10,6 +11,8 @@ export interface TemplateOptions {
|
|
|
10
11
|
* each other resolve locally too.
|
|
11
12
|
*/
|
|
12
13
|
localPackages?: Readonly<Record<string, string>>;
|
|
14
|
+
/** How the users routes validate input. Default: `valibot`. */
|
|
15
|
+
validator?: Validator;
|
|
13
16
|
}
|
|
14
17
|
/** Every file of a new project, keyed by its path relative to the project folder. */
|
|
15
18
|
export declare function templateFiles(options: TemplateOptions): Record<string, string>;
|
package/dist/templates.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,SAAS,CAAC;AAEjD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,YAAY,CAAC;IACvB,6CAA6C;IAC7C,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,aAAa,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiD,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEhG,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,SAAS,CAAC;AAEjD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,YAAY,CAAC;IACvB,6CAA6C;IAC7C,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,aAAa,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACjD,+DAA+D;IAC/D,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB;AAED,qFAAqF;AACrF,wBAAgB,aAAa,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAY9E"}
|
package/dist/templates.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
|
+
import { SYNTAX, VALIDATOR_VERSIONS } from './validators.js';
|
|
1
2
|
/** Every file of a new project, keyed by its path relative to the project folder. */
|
|
2
3
|
export function templateFiles(options) {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
const validator = options.validator ?? 'valibot';
|
|
5
|
+
const syntax = validator === 'none' ? undefined : SYNTAX[validator];
|
|
6
|
+
const files = options.template === 'modular' ? modular() : minimal();
|
|
7
|
+
if (syntax) {
|
|
8
|
+
Object.assign(files, options.template === 'modular' ? modularSchemas(syntax) : minimalSchemas(syntax));
|
|
9
|
+
if (options.template === 'modular')
|
|
10
|
+
delete files['src/modules/users/dto/create-user.dto.ts'];
|
|
11
|
+
}
|
|
12
|
+
return { ...shared(options, validator), ...files };
|
|
7
13
|
}
|
|
8
|
-
function shared({ name, template, aviVersion, localPackages,
|
|
14
|
+
function shared({ name, template, aviVersion, localPackages }, validator) {
|
|
9
15
|
const spec = (pkg) => localPackages?.[pkg] ?? aviVersion;
|
|
10
16
|
const pkg = {
|
|
11
17
|
name,
|
|
@@ -19,7 +25,10 @@ function shared({ name, template, aviVersion, localPackages, }) {
|
|
|
19
25
|
check: 'avi check',
|
|
20
26
|
test: 'tsc && node --test "dist/**/*.test.js"',
|
|
21
27
|
},
|
|
22
|
-
dependencies: {
|
|
28
|
+
dependencies: {
|
|
29
|
+
'@avijs/core': spec('@avijs/core'),
|
|
30
|
+
...(validator === 'none' ? {} : { [validator]: VALIDATOR_VERSIONS[validator] }),
|
|
31
|
+
},
|
|
23
32
|
devDependencies: {
|
|
24
33
|
'@avijs/cli': spec('@avijs/cli'),
|
|
25
34
|
'@avijs/testing': spec('@avijs/testing'),
|
|
@@ -47,7 +56,7 @@ function shared({ name, template, aviVersion, localPackages, }) {
|
|
|
47
56
|
return {
|
|
48
57
|
'package.json': `${JSON.stringify(pkg, null, 2)}\n`,
|
|
49
58
|
'tsconfig.json': `${JSON.stringify(tsconfig, null, 2)}\n`,
|
|
50
|
-
'.gitignore': 'node_modules/\ndist/\n',
|
|
59
|
+
'.gitignore': 'node_modules/\ndist/\n.env\n',
|
|
51
60
|
'README.md': `# ${name}
|
|
52
61
|
|
|
53
62
|
An [Avi.js](https://github.com/chrischase011/avi-js) API (${template} template).
|
|
@@ -63,7 +72,9 @@ Then open http://localhost:3000/health and http://localhost:3000/users.
|
|
|
63
72
|
`,
|
|
64
73
|
};
|
|
65
74
|
}
|
|
66
|
-
const MAIN_LISTEN =
|
|
75
|
+
const MAIN_LISTEN = listenOn("Number(process.env['PORT'] ?? 3000)");
|
|
76
|
+
function listenOn(port) {
|
|
77
|
+
return `const { port } = await app.listen(${port});
|
|
67
78
|
console.log(\`Listening on http://localhost:\${String(port)}\`);
|
|
68
79
|
|
|
69
80
|
const shutdown = () => {
|
|
@@ -72,6 +83,18 @@ const shutdown = () => {
|
|
|
72
83
|
process.once('SIGINT', shutdown);
|
|
73
84
|
process.once('SIGTERM', shutdown);
|
|
74
85
|
`;
|
|
86
|
+
}
|
|
87
|
+
/** `src/config.ts`: environment variables checked at startup. */
|
|
88
|
+
function configFile(syntax) {
|
|
89
|
+
return `import { defineConfig } from '@avijs/core';
|
|
90
|
+
${syntax.import}
|
|
91
|
+
|
|
92
|
+
// Environment variables, checked when the app starts. A .env file is read too, if present.
|
|
93
|
+
const env = ${syntax.object([['PORT', syntax.port]])};
|
|
94
|
+
|
|
95
|
+
export const Config = defineConfig(env, { envFile: '.env' });
|
|
96
|
+
`;
|
|
97
|
+
}
|
|
75
98
|
function modular() {
|
|
76
99
|
return {
|
|
77
100
|
'src/main.ts': `import { AviFactory } from '@avijs/core';
|
|
@@ -307,4 +330,231 @@ test('answers 404 for an unknown user', async () => {
|
|
|
307
330
|
`,
|
|
308
331
|
};
|
|
309
332
|
}
|
|
333
|
+
const REJECTS_NAMELESS = `test('rejects a user without a name', async () => {
|
|
334
|
+
const res = await client.post('/users', {});
|
|
335
|
+
assert.equal(res.status, 400);
|
|
336
|
+
const body = res.body as { details: { issues: { in: string; path: string[] }[] } };
|
|
337
|
+
assert.deepEqual(
|
|
338
|
+
body.details.issues.map((issue) => \`\${issue.in}.\${issue.path.join('.')}\`),
|
|
339
|
+
['body.name'],
|
|
340
|
+
);
|
|
341
|
+
});
|
|
342
|
+
`;
|
|
343
|
+
/** The users feature with route schemas instead of a handwritten parser. */
|
|
344
|
+
function modularSchemas(syntax) {
|
|
345
|
+
return {
|
|
346
|
+
'src/config.ts': configFile(syntax),
|
|
347
|
+
'src/main.ts': `import { AviFactory } from '@avijs/core';
|
|
348
|
+
import { AppModule } from './app.module.js';
|
|
349
|
+
import { Config } from './config.js';
|
|
350
|
+
|
|
351
|
+
const app = await AviFactory.create(AppModule);
|
|
352
|
+
${listenOn('app.get(Config).PORT')}`,
|
|
353
|
+
'src/app.module.ts': `import { Module } from '@avijs/core';
|
|
354
|
+
import { HealthController } from './common/health.controller.js';
|
|
355
|
+
import { Config } from './config.js';
|
|
356
|
+
import { UsersModule } from './modules/users/users.module.js';
|
|
357
|
+
|
|
358
|
+
@Module({ imports: [Config.module, UsersModule], controllers: [HealthController] })
|
|
359
|
+
export class AppModule {}
|
|
360
|
+
`,
|
|
361
|
+
'src/modules/users/dto/user.dto.ts': `${syntax.import}
|
|
362
|
+
|
|
363
|
+
export const UserId = ${syntax.id};
|
|
364
|
+
|
|
365
|
+
export const CreateUserDto = ${syntax.object([['name', syntax.text(100, 'name is required')]])};
|
|
366
|
+
export type CreateUserDto = ${syntax.infer('CreateUserDto')};
|
|
367
|
+
|
|
368
|
+
// Responses are checked against these and lose any field not listed, so internals cannot leak.
|
|
369
|
+
export const User = ${syntax.object([
|
|
370
|
+
['id', syntax.number],
|
|
371
|
+
['name', syntax.string],
|
|
372
|
+
])};
|
|
373
|
+
export type User = ${syntax.infer('User')};
|
|
374
|
+
|
|
375
|
+
export const UserList = ${syntax.array('User')};
|
|
376
|
+
`,
|
|
377
|
+
'src/modules/users/users.service.ts': `import { Injectable } from '@avijs/core';
|
|
378
|
+
import type { CreateUserDto, User } from './dto/user.dto.js';
|
|
379
|
+
|
|
380
|
+
@Injectable()
|
|
381
|
+
export class UsersService {
|
|
382
|
+
readonly #users: User[] = [{ id: 1, name: 'Ada' }];
|
|
383
|
+
|
|
384
|
+
findAll(): User[] {
|
|
385
|
+
return this.#users;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
findOne(id: number): User | undefined {
|
|
389
|
+
return this.#users.find((user) => user.id === id);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
create(dto: CreateUserDto): User {
|
|
393
|
+
const user = { id: this.#users.length + 1, name: dto.name };
|
|
394
|
+
this.#users.push(user);
|
|
395
|
+
return user;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
`,
|
|
399
|
+
'src/modules/users/users.controller.ts': `import {
|
|
400
|
+
Controller,
|
|
401
|
+
Get,
|
|
402
|
+
HttpError,
|
|
403
|
+
Post,
|
|
404
|
+
reply,
|
|
405
|
+
type RouteContext,
|
|
406
|
+
type RouteSchema,
|
|
407
|
+
} from '@avijs/core';
|
|
408
|
+
import { CreateUserDto, User, UserId, UserList } from './dto/user.dto.js';
|
|
409
|
+
import { UsersService } from './users.service.js';
|
|
410
|
+
|
|
411
|
+
const findOne = { params: UserId, response: User } satisfies RouteSchema;
|
|
412
|
+
const create = { body: CreateUserDto, response: User } satisfies RouteSchema;
|
|
413
|
+
|
|
414
|
+
@Controller('/users')
|
|
415
|
+
export class UsersController {
|
|
416
|
+
static inject = [UsersService] as const;
|
|
417
|
+
|
|
418
|
+
constructor(private readonly users: UsersService) {}
|
|
419
|
+
|
|
420
|
+
@Get('/', { schema: { response: UserList } })
|
|
421
|
+
findAll() {
|
|
422
|
+
return this.users.findAll();
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
@Get('/:id', { schema: findOne })
|
|
426
|
+
findOne(ctx: RouteContext<typeof findOne>) {
|
|
427
|
+
const user = this.users.findOne(ctx.input.params.id);
|
|
428
|
+
if (!user) throw new HttpError(404, 'User not found');
|
|
429
|
+
return user;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
@Post('/', { schema: create })
|
|
433
|
+
create(ctx: RouteContext<typeof create>) {
|
|
434
|
+
return reply(this.users.create(ctx.input.body), { status: 201 });
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
`,
|
|
438
|
+
'src/modules/users/users.test.ts': `import assert from 'node:assert/strict';
|
|
439
|
+
import { after, before, test } from 'node:test';
|
|
440
|
+
import { createTestClient, type TestClient } from '@avijs/testing';
|
|
441
|
+
import { AppModule } from '../../app.module.js';
|
|
442
|
+
|
|
443
|
+
let client: TestClient;
|
|
444
|
+
before(async () => {
|
|
445
|
+
client = await createTestClient(AppModule);
|
|
446
|
+
});
|
|
447
|
+
after(() => client.close());
|
|
448
|
+
|
|
449
|
+
test('lists users', async () => {
|
|
450
|
+
const res = await client.get('/users');
|
|
451
|
+
assert.equal(res.status, 200);
|
|
452
|
+
assert.deepEqual(res.body, [{ id: 1, name: 'Ada' }]);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
test('creates a user', async () => {
|
|
456
|
+
const res = await client.post('/users', { name: ' Grace ', role: 'admin' });
|
|
457
|
+
assert.equal(res.status, 201);
|
|
458
|
+
assert.deepEqual(res.body, { id: 2, name: 'Grace' });
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
${REJECTS_NAMELESS}`,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
function minimalSchemas(syntax) {
|
|
465
|
+
return {
|
|
466
|
+
'src/config.ts': configFile(syntax),
|
|
467
|
+
'src/main.ts': `import { createApp } from './app.js';
|
|
468
|
+
import { Config } from './config.js';
|
|
469
|
+
|
|
470
|
+
const app = createApp();
|
|
471
|
+
await app.init();
|
|
472
|
+
${listenOn('app.get(Config).PORT')}`,
|
|
473
|
+
'src/app.ts': `import { createAvi } from '@avijs/core';
|
|
474
|
+
import { Config } from './config.js';
|
|
475
|
+
import { healthRoutes } from './routes/health.js';
|
|
476
|
+
import { usersRoutes } from './routes/users.js';
|
|
477
|
+
import { UsersService } from './services/users.js';
|
|
478
|
+
|
|
479
|
+
export function createApp() {
|
|
480
|
+
return createAvi().register({
|
|
481
|
+
imports: [Config.module],
|
|
482
|
+
providers: [UsersService],
|
|
483
|
+
controllers: [healthRoutes, usersRoutes],
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
`,
|
|
487
|
+
'src/routes/users.ts': `import { HttpError, defineController, reply, route } from '@avijs/core';
|
|
488
|
+
${syntax.import}
|
|
489
|
+
import { UsersService } from '../services/users.js';
|
|
490
|
+
|
|
491
|
+
const UserId = ${syntax.id};
|
|
492
|
+
|
|
493
|
+
const CreateUser = ${syntax.object([['name', syntax.text(100, 'name is required')]])};
|
|
494
|
+
|
|
495
|
+
// Responses are checked against this and lose any field not listed, so internals cannot leak.
|
|
496
|
+
const User = ${syntax.object([
|
|
497
|
+
['id', syntax.number],
|
|
498
|
+
['name', syntax.string],
|
|
499
|
+
])};
|
|
500
|
+
|
|
501
|
+
export const usersRoutes = defineController({
|
|
502
|
+
prefix: '/users',
|
|
503
|
+
inject: [UsersService],
|
|
504
|
+
routes: (users) => [
|
|
505
|
+
route({
|
|
506
|
+
method: 'GET',
|
|
507
|
+
path: '/',
|
|
508
|
+
schema: { response: ${syntax.array('User')} },
|
|
509
|
+
handler: () => users.findAll(),
|
|
510
|
+
}),
|
|
511
|
+
route({
|
|
512
|
+
method: 'GET',
|
|
513
|
+
path: '/:id',
|
|
514
|
+
schema: { params: UserId, response: User },
|
|
515
|
+
handler: (ctx) => {
|
|
516
|
+
const user = users.findOne(ctx.input.params.id);
|
|
517
|
+
if (!user) throw new HttpError(404, 'User not found');
|
|
518
|
+
return user;
|
|
519
|
+
},
|
|
520
|
+
}),
|
|
521
|
+
route({
|
|
522
|
+
method: 'POST',
|
|
523
|
+
path: '/',
|
|
524
|
+
schema: { body: CreateUser, response: User },
|
|
525
|
+
handler: (ctx) => reply(users.create(ctx.input.body.name), { status: 201 }),
|
|
526
|
+
}),
|
|
527
|
+
],
|
|
528
|
+
});
|
|
529
|
+
`,
|
|
530
|
+
'src/app.test.ts': `import assert from 'node:assert/strict';
|
|
531
|
+
import { after, before, test } from 'node:test';
|
|
532
|
+
import { createTestClient, type TestClient } from '@avijs/testing';
|
|
533
|
+
import { createApp } from './app.js';
|
|
534
|
+
|
|
535
|
+
let client: TestClient;
|
|
536
|
+
before(async () => {
|
|
537
|
+
client = await createTestClient(createApp());
|
|
538
|
+
});
|
|
539
|
+
after(() => client.close());
|
|
540
|
+
|
|
541
|
+
test('lists users', async () => {
|
|
542
|
+
const res = await client.get('/users');
|
|
543
|
+
assert.equal(res.status, 200);
|
|
544
|
+
assert.deepEqual(res.body, [{ id: 1, name: 'Ada' }]);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test('creates a user', async () => {
|
|
548
|
+
const res = await client.post('/users', { name: ' Grace ', role: 'admin' });
|
|
549
|
+
assert.equal(res.status, 201);
|
|
550
|
+
assert.deepEqual(res.body, { id: 2, name: 'Grace' });
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
test('answers 404 for an unknown user', async () => {
|
|
554
|
+
assert.equal((await client.get('/users/99')).status, 404);
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
${REJECTS_NAMELESS}`,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
310
560
|
//# sourceMappingURL=templates.js.map
|
package/dist/templates.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"templates.js","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"templates.js","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Validation libraries generated code can use. `none` keeps handwritten checks. */
|
|
2
|
+
export type Validator = 'valibot' | 'zod' | 'none';
|
|
3
|
+
export declare const VALIDATORS: readonly Validator[];
|
|
4
|
+
/** Version ranges added to a new project's dependencies. */
|
|
5
|
+
export declare const VALIDATOR_VERSIONS: Record<Exclude<Validator, 'none'>, string>;
|
|
6
|
+
/** How each library spells the few schemas the templates and generators need. */
|
|
7
|
+
export interface SchemaSyntax {
|
|
8
|
+
readonly import: string;
|
|
9
|
+
/** Params `{ id }`: digits in, a number out. */
|
|
10
|
+
readonly id: string;
|
|
11
|
+
readonly number: string;
|
|
12
|
+
readonly string: string;
|
|
13
|
+
/** An optional `PORT` variable: digits, as a number, default 3000. */
|
|
14
|
+
readonly port: string;
|
|
15
|
+
/** A trimmed, required string of at most `max` characters. */
|
|
16
|
+
text(max: number, required: string): string;
|
|
17
|
+
object(fields: readonly (readonly [string, string])[]): string;
|
|
18
|
+
array(item: string): string;
|
|
19
|
+
/** Every field of `schema` optional, but at least one present. */
|
|
20
|
+
partialNonEmpty(schema: string): string;
|
|
21
|
+
/** The output type of `schema`. */
|
|
22
|
+
infer(schema: string): string;
|
|
23
|
+
}
|
|
24
|
+
export declare const SYNTAX: Record<Exclude<Validator, 'none'>, SchemaSyntax>;
|
|
25
|
+
/** The validator a project already uses, from its package.json; Valibot wins over Zod. */
|
|
26
|
+
export declare function detectValidator(packageJson: unknown): Validator;
|
|
27
|
+
//# sourceMappingURL=validators.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validators.d.ts","sourceRoot":"","sources":["../src/validators.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,KAAK,GAAG,MAAM,CAAC;AAEnD,eAAO,MAAM,UAAU,EAAE,SAAS,SAAS,EAA+B,CAAC;AAE3E,4DAA4D;AAC5D,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC,EAAE,MAAM,CAGzE,CAAC;AAEF,iFAAiF;AACjF,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,gDAAgD;IAChD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,sEAAsE;IACtE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8DAA8D;IAC9D,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5C,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,GAAG,MAAM,CAAC;IAC/D,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,kEAAkE;IAClE,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IACxC,mCAAmC;IACnC,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;CAC/B;AAKD,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC,EAAE,YAAY,CAsCnE,CAAC;AAEF,0FAA0F;AAC1F,wBAAgB,eAAe,CAAC,WAAW,EAAE,OAAO,GAAG,SAAS,CAU/D"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export const VALIDATORS = ['valibot', 'zod', 'none'];
|
|
2
|
+
/** Version ranges added to a new project's dependencies. */
|
|
3
|
+
export const VALIDATOR_VERSIONS = {
|
|
4
|
+
valibot: '^1.5.0',
|
|
5
|
+
zod: '^4.6.0',
|
|
6
|
+
};
|
|
7
|
+
const fields = (entries) => entries.map(([key, value]) => ` ${key}: ${value},\n`).join('');
|
|
8
|
+
export const SYNTAX = {
|
|
9
|
+
valibot: {
|
|
10
|
+
import: "import * as v from 'valibot';",
|
|
11
|
+
id: `v.object({
|
|
12
|
+
id: v.pipe(v.string(), v.regex(/^\\d+$/, 'id must be a number'), v.transform(Number)),
|
|
13
|
+
})`,
|
|
14
|
+
number: 'v.number()',
|
|
15
|
+
string: 'v.string()',
|
|
16
|
+
port: `v.optional(
|
|
17
|
+
v.pipe(v.string(), v.regex(/^\\d+$/, 'PORT must be a number'), v.transform(Number)),
|
|
18
|
+
'3000',
|
|
19
|
+
)`,
|
|
20
|
+
text: (max, required) => `v.pipe(v.string(), v.trim(), v.minLength(1, '${required}'), v.maxLength(${String(max)}))`,
|
|
21
|
+
object: (entries) => `v.object({\n${fields(entries)}})`,
|
|
22
|
+
array: (item) => `v.array(${item})`,
|
|
23
|
+
partialNonEmpty: (schema) => `v.pipe(
|
|
24
|
+
v.partial(${schema}),
|
|
25
|
+
v.check((input) => Object.keys(input).length > 0, 'nothing to update'),
|
|
26
|
+
)`,
|
|
27
|
+
infer: (schema) => `v.InferOutput<typeof ${schema}>`,
|
|
28
|
+
},
|
|
29
|
+
zod: {
|
|
30
|
+
import: "import { z } from 'zod';",
|
|
31
|
+
id: `z.object({
|
|
32
|
+
id: z.string().regex(/^\\d+$/, 'id must be a number').transform(Number),
|
|
33
|
+
})`,
|
|
34
|
+
number: 'z.number()',
|
|
35
|
+
string: 'z.string()',
|
|
36
|
+
port: "z.string().regex(/^\\d+$/, 'PORT must be a number').default('3000').transform(Number)",
|
|
37
|
+
text: (max, required) => `z.string().trim().min(1, '${required}').max(${String(max)})`,
|
|
38
|
+
object: (entries) => `z.object({\n${fields(entries)}})`,
|
|
39
|
+
array: (item) => `z.array(${item})`,
|
|
40
|
+
partialNonEmpty: (schema) => `${schema}
|
|
41
|
+
.partial()
|
|
42
|
+
.refine((input) => Object.keys(input).length > 0, 'nothing to update')`,
|
|
43
|
+
infer: (schema) => `z.infer<typeof ${schema}>`,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
/** The validator a project already uses, from its package.json; Valibot wins over Zod. */
|
|
47
|
+
export function detectValidator(packageJson) {
|
|
48
|
+
const pkg = packageJson;
|
|
49
|
+
const has = (name) => pkg?.dependencies?.[name] !== undefined || pkg?.devDependencies?.[name] !== undefined;
|
|
50
|
+
if (has('valibot'))
|
|
51
|
+
return 'valibot';
|
|
52
|
+
if (has('zod'))
|
|
53
|
+
return 'zod';
|
|
54
|
+
return 'none';
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=validators.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validators.js","sourceRoot":"","sources":["../src/validators.ts"],"names":[],"mappings":"AAGA,MAAM,CAAC,MAAM,UAAU,GAAyB,CAAC,SAAS,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;AAE3E,4DAA4D;AAC5D,MAAM,CAAC,MAAM,kBAAkB,GAA+C;IAC5E,OAAO,EAAE,QAAQ;IACjB,GAAG,EAAE,QAAQ;CACd,CAAC;AAqBF,MAAM,MAAM,GAAG,CAAC,OAA+C,EAAE,EAAE,CACjE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,GAAG,KAAK,KAAK,KAAK,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AAElE,MAAM,CAAC,MAAM,MAAM,GAAqD;IACtE,OAAO,EAAE;QACP,MAAM,EAAE,+BAA+B;QACvC,EAAE,EAAE;;GAEL;QACC,MAAM,EAAE,YAAY;QACpB,MAAM,EAAE,YAAY;QACpB,IAAI,EAAE;;;IAGN;QACA,IAAI,EAAE,CAAC,GAAG,EAAE,QAAQ,EAAE,EAAE,CACtB,gDAAgD,QAAQ,mBAAmB,MAAM,CAAC,GAAG,CAAC,IAAI;QAC5F,MAAM,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,eAAe,MAAM,CAAC,OAAO,CAAC,IAAI;QACvD,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,WAAW,IAAI,GAAG;QACnC,eAAe,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC;cACnB,MAAM;;EAElB;QACE,KAAK,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,wBAAwB,MAAM,GAAG;KACrD;IACD,GAAG,EAAE;QACH,MAAM,EAAE,0BAA0B;QAClC,EAAE,EAAE;;GAEL;QACC,MAAM,EAAE,YAAY;QACpB,MAAM,EAAE,YAAY;QACpB,IAAI,EAAE,uFAAuF;QAC7F,IAAI,EAAE,CAAC,GAAG,EAAE,QAAQ,EAAE,EAAE,CAAC,6BAA6B,QAAQ,UAAU,MAAM,CAAC,GAAG,CAAC,GAAG;QACtF,MAAM,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,eAAe,MAAM,CAAC,OAAO,CAAC,IAAI;QACvD,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,WAAW,IAAI,GAAG;QACnC,eAAe,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,MAAM;;yEAE+B;QACrE,KAAK,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,kBAAkB,MAAM,GAAG;KAC/C;CACF,CAAC;AAEF,0FAA0F;AAC1F,MAAM,UAAU,eAAe,CAAC,WAAoB;IAClD,MAAM,GAAG,GAAG,WAGJ,CAAC;IACT,MAAM,GAAG,GAAG,CAAC,IAAY,EAAE,EAAE,CAC3B,GAAG,EAAE,YAAY,EAAE,CAAC,IAAI,CAAC,KAAK,SAAS,IAAI,GAAG,EAAE,eAAe,EAAE,CAAC,IAAI,CAAC,KAAK,SAAS,CAAC;IACxF,IAAI,GAAG,CAAC,SAAS,CAAC;QAAE,OAAO,SAAS,CAAC;IACrC,IAAI,GAAG,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC7B,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avijs/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "The avi command: project scaffolding, dev mode and build checks.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"avi",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
],
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"author": "Christopher Robin Chase",
|
|
13
|
-
"homepage": "https://github.
|
|
13
|
+
"homepage": "https://chrischase011.github.io/avi-js/",
|
|
14
14
|
"bugs": {
|
|
15
15
|
"url": "https://github.com/chrischase011/avi-js/issues"
|
|
16
16
|
},
|
|
@@ -44,11 +44,15 @@
|
|
|
44
44
|
"access": "public"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@avijs/compiler": "0.
|
|
47
|
+
"@avijs/compiler": "0.2.0"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"typescript": ">=5.8"
|
|
51
51
|
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"valibot": "1.5.0",
|
|
54
|
+
"zod": "4.6.7"
|
|
55
|
+
},
|
|
52
56
|
"scripts": {
|
|
53
57
|
"typecheck": "tsc -p tsconfig.json"
|
|
54
58
|
}
|
package/src/cli.ts
CHANGED
|
@@ -7,15 +7,18 @@ import path from 'node:path';
|
|
|
7
7
|
import { parseArgs } from 'node:util';
|
|
8
8
|
import { GENERATE_USAGE, generate } from './generate.js';
|
|
9
9
|
import { templateFiles } from './templates.js';
|
|
10
|
+
import { VALIDATORS, type Validator } from './validators.js';
|
|
10
11
|
|
|
11
12
|
const { version } = createRequire(import.meta.url)('../package.json') as { version: string };
|
|
12
13
|
|
|
13
14
|
const USAGE = `Usage:
|
|
14
|
-
avi new <name> [--template modular|minimal] [--
|
|
15
|
+
avi new <name> [--template modular|minimal] [--validator valibot|zod|none] [--dir .]
|
|
16
|
+
[--avi-version ^x.y.z] [--from <dir>]
|
|
15
17
|
Create a project. The modular template (default) uses modules and decorators;
|
|
16
|
-
minimal uses plain routes and services. --
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
minimal uses plain routes and services. --validator picks the schema library for
|
|
19
|
+
route validation (default valibot); none keeps handwritten checks. --from points at
|
|
20
|
+
a folder of packed @avijs tarballs (pnpm pack) to install instead of the registry.
|
|
21
|
+
--avi-version defaults to this CLI's version.
|
|
19
22
|
${GENERATE_USAGE}
|
|
20
23
|
avi dev [--project tsconfig.json] [--entry dist/main.js]
|
|
21
24
|
Compile, then recompile and restart on every change.
|
|
@@ -49,6 +52,7 @@ async function create(args: readonly string[]): Promise<number> {
|
|
|
49
52
|
allowPositionals: true,
|
|
50
53
|
options: {
|
|
51
54
|
template: { type: 'string', default: 'modular' },
|
|
55
|
+
validator: { type: 'string', default: 'valibot' },
|
|
52
56
|
dir: { type: 'string', default: '.' },
|
|
53
57
|
'avi-version': { type: 'string', default: `^${version}` },
|
|
54
58
|
from: { type: 'string' },
|
|
@@ -65,6 +69,12 @@ async function create(args: readonly string[]): Promise<number> {
|
|
|
65
69
|
return 2;
|
|
66
70
|
}
|
|
67
71
|
|
|
72
|
+
const validator = values.validator as Validator;
|
|
73
|
+
if (!VALIDATORS.includes(validator)) {
|
|
74
|
+
console.error(`Unknown validator "${values.validator}". Use ${VALIDATORS.join(', ')}.`);
|
|
75
|
+
return 2;
|
|
76
|
+
}
|
|
77
|
+
|
|
68
78
|
const target = path.resolve(values.dir, name);
|
|
69
79
|
if (existsSync(target) && (await readdir(target)).length > 0) {
|
|
70
80
|
console.error(`${target} already exists and is not empty`);
|
|
@@ -74,6 +84,7 @@ async function create(args: readonly string[]): Promise<number> {
|
|
|
74
84
|
const files = templateFiles({
|
|
75
85
|
name,
|
|
76
86
|
template,
|
|
87
|
+
validator,
|
|
77
88
|
aviVersion: values['avi-version'],
|
|
78
89
|
...(values.from ? { localPackages: await localPackages(values.from, target) } : {}),
|
|
79
90
|
});
|
|
@@ -83,7 +94,7 @@ async function create(args: readonly string[]): Promise<number> {
|
|
|
83
94
|
await writeFile(destination, content);
|
|
84
95
|
}
|
|
85
96
|
|
|
86
|
-
console.log(`Created ${name} (${template}) in ${target}
|
|
97
|
+
console.log(`Created ${name} (${template}, ${validator === 'none' ? 'no validator' : validator}) in ${target}
|
|
87
98
|
|
|
88
99
|
Next:
|
|
89
100
|
cd ${path.relative(process.cwd(), target) || '.'}
|