@avijs/cli 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +12 -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 +149 -0
- package/dist/cli.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 +155 -0
- package/src/index.ts +3 -0
- package/src/templates.ts +333 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Christopher Robin Chase
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @avijs/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The avi command: avi new, avi dev, avi check and avi manifest.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install -D @avijs/cli
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Part of [Avi.js](https://github.com/chrischase011/avi-js), a TypeScript framework for Node.js with
|
|
10
|
+
modules, controllers and dependency injection, without `reflect-metadata`. See the
|
|
11
|
+
[tutorial](https://github.com/chrischase011/avi-js/blob/main/docs/tutorial/README.md) to get started.
|
|
12
|
+
|
|
13
|
+
Requires Node.js 22.12 or newer. MIT licensed.
|
package/dist/bin.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":""}
|
package/dist/bin.js
ADDED
package/dist/bin.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAEhC,OAAO,CAAC,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAwBA,wBAAsB,IAAI,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAcnE"}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { main as compile } from '@avijs/compiler';
|
|
2
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { mkdir, readdir, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { parseArgs } from 'node:util';
|
|
8
|
+
import { templateFiles } from './templates.js';
|
|
9
|
+
const { version } = createRequire(import.meta.url)('../package.json');
|
|
10
|
+
const USAGE = `Usage:
|
|
11
|
+
avi new <name> [--template modular|minimal] [--dir .] [--avi-version ^x.y.z] [--from <dir>]
|
|
12
|
+
Create a project. The modular template (default) uses modules and decorators;
|
|
13
|
+
minimal uses plain routes and services. --from points at a folder of packed
|
|
14
|
+
@avijs tarballs (pnpm pack) to install instead of the registry. --avi-version
|
|
15
|
+
defaults to this CLI's version.
|
|
16
|
+
avi dev [--project tsconfig.json] [--entry dist/main.js]
|
|
17
|
+
Compile, then recompile and restart on every change.
|
|
18
|
+
avi check [--project tsconfig.json]
|
|
19
|
+
Type-check every static inject list.
|
|
20
|
+
avi manifest <entry.js> [--export AppModule] [--out avi.manifest.json]
|
|
21
|
+
Describe the app's modules, providers and routes.`;
|
|
22
|
+
export async function main(argv) {
|
|
23
|
+
const [command, ...rest] = argv;
|
|
24
|
+
switch (command) {
|
|
25
|
+
case 'new':
|
|
26
|
+
return create(rest);
|
|
27
|
+
case 'dev':
|
|
28
|
+
return dev(rest);
|
|
29
|
+
case 'check':
|
|
30
|
+
case 'manifest':
|
|
31
|
+
return compile(argv);
|
|
32
|
+
default:
|
|
33
|
+
console.error(USAGE);
|
|
34
|
+
return command === undefined || command === 'help' || command === '--help' ? 0 : 2;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function create(args) {
|
|
38
|
+
const { values, positionals } = parseArgs({
|
|
39
|
+
args: [...args],
|
|
40
|
+
allowPositionals: true,
|
|
41
|
+
options: {
|
|
42
|
+
template: { type: 'string', default: 'modular' },
|
|
43
|
+
dir: { type: 'string', default: '.' },
|
|
44
|
+
'avi-version': { type: 'string', default: `^${version}` },
|
|
45
|
+
from: { type: 'string' },
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
const name = positionals[0];
|
|
49
|
+
if (!name || !/^[a-z0-9][a-z0-9._-]*$/.test(name)) {
|
|
50
|
+
console.error('avi new needs a project name in lowercase letters, digits, ".", "_" or "-"');
|
|
51
|
+
return 2;
|
|
52
|
+
}
|
|
53
|
+
const template = values.template;
|
|
54
|
+
if (template !== 'modular' && template !== 'minimal') {
|
|
55
|
+
console.error(`Unknown template "${template}". Use modular or minimal.`);
|
|
56
|
+
return 2;
|
|
57
|
+
}
|
|
58
|
+
const target = path.resolve(values.dir, name);
|
|
59
|
+
if (existsSync(target) && (await readdir(target)).length > 0) {
|
|
60
|
+
console.error(`${target} already exists and is not empty`);
|
|
61
|
+
return 1;
|
|
62
|
+
}
|
|
63
|
+
const files = templateFiles({
|
|
64
|
+
name,
|
|
65
|
+
template,
|
|
66
|
+
aviVersion: values['avi-version'],
|
|
67
|
+
...(values.from ? { localPackages: await localPackages(values.from, target) } : {}),
|
|
68
|
+
});
|
|
69
|
+
for (const [file, content] of Object.entries(files)) {
|
|
70
|
+
const destination = path.join(target, file);
|
|
71
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
72
|
+
await writeFile(destination, content);
|
|
73
|
+
}
|
|
74
|
+
console.log(`Created ${name} (${template}) in ${target}
|
|
75
|
+
|
|
76
|
+
Next:
|
|
77
|
+
cd ${path.relative(process.cwd(), target) || '.'}
|
|
78
|
+
npm install
|
|
79
|
+
npm run dev`);
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
async function dev(args) {
|
|
83
|
+
const { values } = parseArgs({
|
|
84
|
+
args: [...args],
|
|
85
|
+
options: {
|
|
86
|
+
project: { type: 'string', default: 'tsconfig.json' },
|
|
87
|
+
entry: { type: 'string', default: 'dist/main.js' },
|
|
88
|
+
'dry-run': { type: 'boolean', default: false },
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
let tsc;
|
|
92
|
+
try {
|
|
93
|
+
tsc = createRequire(path.resolve('package.json')).resolve('typescript/bin/tsc');
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
console.error('avi dev needs TypeScript installed in this project: npm install -D typescript');
|
|
97
|
+
return 1;
|
|
98
|
+
}
|
|
99
|
+
const build = [tsc, '-p', values.project];
|
|
100
|
+
const watchBuild = [tsc, '-p', values.project, '--watch', '--preserveWatchOutput'];
|
|
101
|
+
const watchRun = ['--watch', '--watch-preserve-output', values.entry];
|
|
102
|
+
if (values['dry-run']) {
|
|
103
|
+
for (const command of [build, watchBuild, watchRun]) {
|
|
104
|
+
console.log(['node', ...command].join(' '));
|
|
105
|
+
}
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
const first = spawnSync(process.execPath, build, { stdio: 'inherit' });
|
|
109
|
+
if (first.status !== 0)
|
|
110
|
+
return first.status ?? 1;
|
|
111
|
+
const children = [
|
|
112
|
+
spawn(process.execPath, watchBuild, { stdio: 'inherit' }),
|
|
113
|
+
spawn(process.execPath, watchRun, { stdio: 'inherit' }),
|
|
114
|
+
];
|
|
115
|
+
return new Promise((resolve) => {
|
|
116
|
+
const stop = () => {
|
|
117
|
+
for (const child of children)
|
|
118
|
+
child.kill();
|
|
119
|
+
resolve(0);
|
|
120
|
+
};
|
|
121
|
+
process.once('SIGINT', stop);
|
|
122
|
+
process.once('SIGTERM', stop);
|
|
123
|
+
for (const child of children) {
|
|
124
|
+
child.once('exit', (code) => {
|
|
125
|
+
if (code !== null && code !== 0) {
|
|
126
|
+
for (const other of children)
|
|
127
|
+
other.kill();
|
|
128
|
+
resolve(code);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
/** Maps each packed @avi tarball in `dir` to a `file:` spec relative to the new project. */
|
|
135
|
+
async function localPackages(dir, project) {
|
|
136
|
+
const folder = path.resolve(dir);
|
|
137
|
+
const specs = {};
|
|
138
|
+
for (const file of await readdir(folder)) {
|
|
139
|
+
const match = /^avijs-([a-z-]+?)-\d+\.\d+\.\d+.*\.tgz$/.exec(file);
|
|
140
|
+
if (!match?.[1])
|
|
141
|
+
continue;
|
|
142
|
+
const relative = path.relative(project, path.join(folder, file)).replaceAll(path.sep, '/');
|
|
143
|
+
specs[`@avijs/${match[1]}`] = `file:${relative}`;
|
|
144
|
+
}
|
|
145
|
+
if (!specs['@avijs/core'])
|
|
146
|
+
throw new Error(`No @avijs package tarballs found in ${folder}`);
|
|
147
|
+
return specs;
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,IAAI,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,KAAK,EAAE,SAAS,EAAqB,MAAM,oBAAoB,CAAC;AACzE,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAE/C,MAAM,EAAE,OAAO,EAAE,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,iBAAiB,CAAwB,CAAC;AAE7F,MAAM,KAAK,GAAG;;;;;;;;;;;wDAW0C,CAAC;AAEzD,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,IAAuB;IAChD,MAAM,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;IAChC,QAAQ,OAAO,EAAE,CAAC;QAChB,KAAK,KAAK;YACR,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC;QACtB,KAAK,KAAK;YACR,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC;QACnB,KAAK,OAAO,CAAC;QACb,KAAK,UAAU;YACb,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC;QACvB;YACE,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACrB,OAAO,OAAO,KAAK,SAAS,IAAI,OAAO,KAAK,MAAM,IAAI,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACvF,CAAC;AACH,CAAC;AAED,KAAK,UAAU,MAAM,CAAC,IAAuB;IAC3C,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,SAAS,CAAC;QACxC,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC;QACf,gBAAgB,EAAE,IAAI;QACtB,OAAO,EAAE;YACP,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE;YAChD,GAAG,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE;YACrC,aAAa,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,OAAO,EAAE,EAAE;YACzD,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;SACzB;KACF,CAAC,CAAC;IACH,MAAM,IAAI,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC;IAC5B,IAAI,CAAC,IAAI,IAAI,CAAC,wBAAwB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAClD,OAAO,CAAC,KAAK,CAAC,4EAA4E,CAAC,CAAC;QAC5F,OAAO,CAAC,CAAC;IACX,CAAC;IACD,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;IACjC,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QACrD,OAAO,CAAC,KAAK,CAAC,qBAAqB,QAAQ,4BAA4B,CAAC,CAAC;QACzE,OAAO,CAAC,CAAC;IACX,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAC9C,IAAI,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC7D,OAAO,CAAC,KAAK,CAAC,GAAG,MAAM,kCAAkC,CAAC,CAAC;QAC3D,OAAO,CAAC,CAAC;IACX,CAAC;IAED,MAAM,KAAK,GAAG,aAAa,CAAC;QAC1B,IAAI;QACJ,QAAQ;QACR,UAAU,EAAE,MAAM,CAAC,aAAa,CAAC;QACjC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACpF,CAAC,CAAC;IACH,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACpD,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAC5C,MAAM,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC5D,MAAM,SAAS,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IACxC,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,KAAK,QAAQ,QAAQ,MAAM;;;OAGjD,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,MAAM,CAAC,IAAI,GAAG;;cAEpC,CAAC,CAAC;IACd,OAAO,CAAC,CAAC;AACX,CAAC;AAED,KAAK,UAAU,GAAG,CAAC,IAAuB;IACxC,MAAM,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;QAC3B,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC;QACf,OAAO,EAAE;YACP,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,eAAe,EAAE;YACrD,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,cAAc,EAAE;YAClD,SAAS,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE;SAC/C;KACF,CAAC,CAAC;IAEH,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAClF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,KAAK,CAAC,+EAA+E,CAAC,CAAC;QAC/F,OAAO,CAAC,CAAC;IACX,CAAC;IAED,MAAM,KAAK,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IAC1C,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,OAAO,EAAE,SAAS,EAAE,uBAAuB,CAAC,CAAC;IACnF,MAAM,QAAQ,GAAG,CAAC,SAAS,EAAE,yBAAyB,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;IACtE,IAAI,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC;QACtB,KAAK,MAAM,OAAO,IAAI,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,CAAC,EAAE,CAAC;YACpD,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QAC9C,CAAC;QACD,OAAO,CAAC,CAAC;IACX,CAAC;IAED,MAAM,KAAK,GAAG,SAAS,CAAC,OAAO,CAAC,QAAQ,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;IACvE,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;IAEjD,MAAM,QAAQ,GAAmB;QAC/B,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,UAAU,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;QACzD,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;KACxD,CAAC;IACF,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,IAAI,GAAG,GAAG,EAAE;YAChB,KAAK,MAAM,KAAK,IAAI,QAAQ;gBAAE,KAAK,CAAC,IAAI,EAAE,CAAC;YAC3C,OAAO,CAAC,CAAC,CAAC,CAAC;QACb,CAAC,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;QAC7B,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;QAC9B,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;YAC7B,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;gBAC1B,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;oBAChC,KAAK,MAAM,KAAK,IAAI,QAAQ;wBAAE,KAAK,CAAC,IAAI,EAAE,CAAC;oBAC3C,OAAO,CAAC,IAAI,CAAC,CAAC;gBAChB,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC;AAED,4FAA4F;AAC5F,KAAK,UAAU,aAAa,CAAC,GAAW,EAAE,OAAe;IACvD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACjC,MAAM,KAAK,GAA2B,EAAE,CAAC;IACzC,KAAK,MAAM,IAAI,IAAI,MAAM,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACzC,MAAM,KAAK,GAAG,yCAAyC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;YAAE,SAAS;QAC1B,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QAC3F,KAAK,CAAC,UAAU,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,QAAQ,QAAQ,EAAE,CAAC;IACnD,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,uCAAuC,MAAM,EAAE,CAAC,CAAC;IAC5F,OAAO,KAAK,CAAC;AACf,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAChC,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAChC,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type TemplateName = 'modular' | 'minimal';
|
|
2
|
+
export interface TemplateOptions {
|
|
3
|
+
name: string;
|
|
4
|
+
template: TemplateName;
|
|
5
|
+
/** Version range for the @avijs packages. */
|
|
6
|
+
aviVersion: string;
|
|
7
|
+
/**
|
|
8
|
+
* Local package files to use instead of the registry, keyed by package name, such as
|
|
9
|
+
* 'file:../artifacts/avijs-core-0.1.0.tgz'. Also added as npm overrides so packages that depend on
|
|
10
|
+
* each other resolve locally too.
|
|
11
|
+
*/
|
|
12
|
+
localPackages?: Readonly<Record<string, string>>;
|
|
13
|
+
}
|
|
14
|
+
/** Every file of a new project, keyed by its path relative to the project folder. */
|
|
15
|
+
export declare function templateFiles(options: TemplateOptions): Record<string, string>;
|
|
16
|
+
//# sourceMappingURL=templates.d.ts.map
|
|
@@ -0,0 +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;CAClD;AAED,qFAAqF;AACrF,wBAAgB,aAAa,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAK9E"}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/** Every file of a new project, keyed by its path relative to the project folder. */
|
|
2
|
+
export function templateFiles(options) {
|
|
3
|
+
return {
|
|
4
|
+
...shared(options),
|
|
5
|
+
...(options.template === 'modular' ? modular() : minimal()),
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
function shared({ name, template, aviVersion, localPackages, }) {
|
|
9
|
+
const spec = (pkg) => localPackages?.[pkg] ?? aviVersion;
|
|
10
|
+
const pkg = {
|
|
11
|
+
name,
|
|
12
|
+
version: '0.1.0',
|
|
13
|
+
private: true,
|
|
14
|
+
type: 'module',
|
|
15
|
+
scripts: {
|
|
16
|
+
build: 'tsc',
|
|
17
|
+
start: 'node dist/main.js',
|
|
18
|
+
dev: 'avi dev',
|
|
19
|
+
check: 'avi check',
|
|
20
|
+
test: 'tsc && node --test "dist/**/*.test.js"',
|
|
21
|
+
},
|
|
22
|
+
dependencies: { '@avijs/core': spec('@avijs/core') },
|
|
23
|
+
devDependencies: {
|
|
24
|
+
'@avijs/cli': spec('@avijs/cli'),
|
|
25
|
+
'@avijs/testing': spec('@avijs/testing'),
|
|
26
|
+
'@types/node': '^22.0.0',
|
|
27
|
+
typescript: '~6.0.3',
|
|
28
|
+
},
|
|
29
|
+
engines: { node: '>=22.12' },
|
|
30
|
+
...(localPackages ? { overrides: localPackages } : {}),
|
|
31
|
+
};
|
|
32
|
+
const tsconfig = {
|
|
33
|
+
compilerOptions: {
|
|
34
|
+
target: 'ES2023',
|
|
35
|
+
lib: ['ES2023', 'ESNext.Disposable'],
|
|
36
|
+
module: 'NodeNext',
|
|
37
|
+
moduleResolution: 'NodeNext',
|
|
38
|
+
types: ['node'],
|
|
39
|
+
strict: true,
|
|
40
|
+
skipLibCheck: true,
|
|
41
|
+
outDir: 'dist',
|
|
42
|
+
rootDir: 'src',
|
|
43
|
+
sourceMap: true,
|
|
44
|
+
},
|
|
45
|
+
include: ['src'],
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
'package.json': `${JSON.stringify(pkg, null, 2)}\n`,
|
|
49
|
+
'tsconfig.json': `${JSON.stringify(tsconfig, null, 2)}\n`,
|
|
50
|
+
'.gitignore': 'node_modules/\ndist/\n',
|
|
51
|
+
'README.md': `# ${name}
|
|
52
|
+
|
|
53
|
+
An [Avi.js](https://github.com/chrischase011/avi-js) API (${template} template).
|
|
54
|
+
|
|
55
|
+
\`\`\`sh
|
|
56
|
+
npm install
|
|
57
|
+
npm run dev # compile and restart on change
|
|
58
|
+
npm test # compile and run the tests
|
|
59
|
+
npm start # run the compiled app
|
|
60
|
+
\`\`\`
|
|
61
|
+
|
|
62
|
+
Then open http://localhost:3000/health and http://localhost:3000/users.
|
|
63
|
+
`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const MAIN_LISTEN = `const { port } = await app.listen(Number(process.env['PORT'] ?? 3000));
|
|
67
|
+
console.log(\`Listening on http://localhost:\${String(port)}\`);
|
|
68
|
+
|
|
69
|
+
const shutdown = () => {
|
|
70
|
+
void app.close().then(() => process.exit(0));
|
|
71
|
+
};
|
|
72
|
+
process.once('SIGINT', shutdown);
|
|
73
|
+
process.once('SIGTERM', shutdown);
|
|
74
|
+
`;
|
|
75
|
+
function modular() {
|
|
76
|
+
return {
|
|
77
|
+
'src/main.ts': `import { AviFactory } from '@avijs/core';
|
|
78
|
+
import { AppModule } from './app.module.js';
|
|
79
|
+
|
|
80
|
+
const app = await AviFactory.create(AppModule);
|
|
81
|
+
${MAIN_LISTEN}`,
|
|
82
|
+
'src/app.module.ts': `import { Module } from '@avijs/core';
|
|
83
|
+
import { HealthController } from './common/health.controller.js';
|
|
84
|
+
import { UsersModule } from './modules/users/users.module.js';
|
|
85
|
+
|
|
86
|
+
@Module({ imports: [UsersModule], controllers: [HealthController] })
|
|
87
|
+
export class AppModule {}
|
|
88
|
+
`,
|
|
89
|
+
'src/common/health.controller.ts': `import { Controller, Get } from '@avijs/core';
|
|
90
|
+
|
|
91
|
+
@Controller()
|
|
92
|
+
export class HealthController {
|
|
93
|
+
@Get('/health')
|
|
94
|
+
check() {
|
|
95
|
+
return 'ok';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
`,
|
|
99
|
+
'src/modules/users/users.module.ts': `import { Module } from '@avijs/core';
|
|
100
|
+
import { UsersController } from './users.controller.js';
|
|
101
|
+
import { UsersService } from './users.service.js';
|
|
102
|
+
|
|
103
|
+
@Module({ controllers: [UsersController], providers: [UsersService], exports: [UsersService] })
|
|
104
|
+
export class UsersModule {}
|
|
105
|
+
`,
|
|
106
|
+
'src/modules/users/users.service.ts': `import { Injectable } from '@avijs/core';
|
|
107
|
+
import type { CreateUserDto } from './dto/create-user.dto.js';
|
|
108
|
+
|
|
109
|
+
export interface User {
|
|
110
|
+
id: number;
|
|
111
|
+
name: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@Injectable()
|
|
115
|
+
export class UsersService {
|
|
116
|
+
readonly #users: User[] = [{ id: 1, name: 'Ada' }];
|
|
117
|
+
|
|
118
|
+
findAll(): User[] {
|
|
119
|
+
return this.#users;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
findOne(id: number): User | undefined {
|
|
123
|
+
return this.#users.find((user) => user.id === id);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
create(dto: CreateUserDto): User {
|
|
127
|
+
const user = { id: this.#users.length + 1, name: dto.name };
|
|
128
|
+
this.#users.push(user);
|
|
129
|
+
return user;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
`,
|
|
133
|
+
'src/modules/users/users.controller.ts': `import { Controller, Get, HttpError, Post, reply, type Context } from '@avijs/core';
|
|
134
|
+
import { parseCreateUser } from './dto/create-user.dto.js';
|
|
135
|
+
import { UsersService } from './users.service.js';
|
|
136
|
+
|
|
137
|
+
@Controller('/users')
|
|
138
|
+
export class UsersController {
|
|
139
|
+
static inject = [UsersService] as const;
|
|
140
|
+
|
|
141
|
+
constructor(private readonly users: UsersService) {}
|
|
142
|
+
|
|
143
|
+
@Get()
|
|
144
|
+
findAll() {
|
|
145
|
+
return this.users.findAll();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@Get('/:id')
|
|
149
|
+
findOne(ctx: Context) {
|
|
150
|
+
const user = this.users.findOne(Number(ctx.params['id']));
|
|
151
|
+
if (!user) throw new HttpError(404, 'User not found');
|
|
152
|
+
return user;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@Post()
|
|
156
|
+
async create(ctx: Context) {
|
|
157
|
+
const dto = parseCreateUser(await ctx.json());
|
|
158
|
+
if (!dto) throw new HttpError(400, 'Body must be an object with a name');
|
|
159
|
+
return reply(this.users.create(dto), { status: 201 });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
`,
|
|
163
|
+
'src/modules/users/dto/create-user.dto.ts': `export interface CreateUserDto {
|
|
164
|
+
name: string;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Swap this for a schema library (Zod, Valibot, ...) and route schemas when the API grows.
|
|
168
|
+
export function parseCreateUser(input: unknown): CreateUserDto | undefined {
|
|
169
|
+
if (typeof input !== 'object' || input === null) return undefined;
|
|
170
|
+
const { name } = input as { name?: unknown };
|
|
171
|
+
return typeof name === 'string' && name.trim() !== '' ? { name: name.trim() } : undefined;
|
|
172
|
+
}
|
|
173
|
+
`,
|
|
174
|
+
'src/modules/users/users.test.ts': `import assert from 'node:assert/strict';
|
|
175
|
+
import { after, before, test } from 'node:test';
|
|
176
|
+
import { createTestClient, type TestClient } from '@avijs/testing';
|
|
177
|
+
import { AppModule } from '../../app.module.js';
|
|
178
|
+
|
|
179
|
+
let client: TestClient;
|
|
180
|
+
before(async () => {
|
|
181
|
+
client = await createTestClient(AppModule);
|
|
182
|
+
});
|
|
183
|
+
after(() => client.close());
|
|
184
|
+
|
|
185
|
+
test('lists users', async () => {
|
|
186
|
+
const res = await client.get('/users');
|
|
187
|
+
assert.equal(res.status, 200);
|
|
188
|
+
assert.deepEqual(res.body, [{ id: 1, name: 'Ada' }]);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('creates a user', async () => {
|
|
192
|
+
const res = await client.post('/users', { name: 'Grace' });
|
|
193
|
+
assert.equal(res.status, 201);
|
|
194
|
+
assert.deepEqual(res.body, { id: 2, name: 'Grace' });
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('rejects a user without a name', async () => {
|
|
198
|
+
assert.equal((await client.post('/users', {})).status, 400);
|
|
199
|
+
});
|
|
200
|
+
`,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function minimal() {
|
|
204
|
+
return {
|
|
205
|
+
'src/main.ts': `import { createApp } from './app.js';
|
|
206
|
+
|
|
207
|
+
const app = createApp();
|
|
208
|
+
${MAIN_LISTEN}`,
|
|
209
|
+
'src/app.ts': `import { createAvi } from '@avijs/core';
|
|
210
|
+
import { healthRoutes } from './routes/health.js';
|
|
211
|
+
import { usersRoutes } from './routes/users.js';
|
|
212
|
+
import { UsersService } from './services/users.js';
|
|
213
|
+
|
|
214
|
+
export function createApp() {
|
|
215
|
+
return createAvi().register({
|
|
216
|
+
providers: [UsersService],
|
|
217
|
+
controllers: [healthRoutes, usersRoutes],
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
`,
|
|
221
|
+
'src/routes/health.ts': `import { defineController, route } from '@avijs/core';
|
|
222
|
+
|
|
223
|
+
export const healthRoutes = defineController({
|
|
224
|
+
routes: [route({ method: 'GET', path: '/health', handler: () => 'ok' })],
|
|
225
|
+
});
|
|
226
|
+
`,
|
|
227
|
+
'src/routes/users.ts': `import { HttpError, defineController, reply, route } from '@avijs/core';
|
|
228
|
+
import { UsersService } from '../services/users.js';
|
|
229
|
+
|
|
230
|
+
export const usersRoutes = defineController({
|
|
231
|
+
prefix: '/users',
|
|
232
|
+
inject: [UsersService],
|
|
233
|
+
routes: (users) => [
|
|
234
|
+
route({ method: 'GET', path: '/', handler: () => users.findAll() }),
|
|
235
|
+
route({
|
|
236
|
+
method: 'GET',
|
|
237
|
+
path: '/:id',
|
|
238
|
+
handler: (ctx) => {
|
|
239
|
+
const user = users.findOne(Number(ctx.params.id));
|
|
240
|
+
if (!user) throw new HttpError(404, 'User not found');
|
|
241
|
+
return user;
|
|
242
|
+
},
|
|
243
|
+
}),
|
|
244
|
+
route({
|
|
245
|
+
method: 'POST',
|
|
246
|
+
path: '/',
|
|
247
|
+
handler: async (ctx) => {
|
|
248
|
+
const { name } = (await ctx.json()) as { name?: unknown };
|
|
249
|
+
if (typeof name !== 'string' || name.trim() === '') {
|
|
250
|
+
throw new HttpError(400, 'Body must be an object with a name');
|
|
251
|
+
}
|
|
252
|
+
return reply(users.create(name.trim()), { status: 201 });
|
|
253
|
+
},
|
|
254
|
+
}),
|
|
255
|
+
],
|
|
256
|
+
});
|
|
257
|
+
`,
|
|
258
|
+
'src/services/users.ts': `export interface User {
|
|
259
|
+
id: number;
|
|
260
|
+
name: string;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export class UsersService {
|
|
264
|
+
readonly #users: User[] = [{ id: 1, name: 'Ada' }];
|
|
265
|
+
|
|
266
|
+
findAll(): User[] {
|
|
267
|
+
return this.#users;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
findOne(id: number): User | undefined {
|
|
271
|
+
return this.#users.find((user) => user.id === id);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
create(name: string): User {
|
|
275
|
+
const user = { id: this.#users.length + 1, name };
|
|
276
|
+
this.#users.push(user);
|
|
277
|
+
return user;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
`,
|
|
281
|
+
'src/app.test.ts': `import assert from 'node:assert/strict';
|
|
282
|
+
import { after, before, test } from 'node:test';
|
|
283
|
+
import { createTestClient, type TestClient } from '@avijs/testing';
|
|
284
|
+
import { createApp } from './app.js';
|
|
285
|
+
|
|
286
|
+
let client: TestClient;
|
|
287
|
+
before(async () => {
|
|
288
|
+
client = await createTestClient(createApp());
|
|
289
|
+
});
|
|
290
|
+
after(() => client.close());
|
|
291
|
+
|
|
292
|
+
test('lists users', async () => {
|
|
293
|
+
const res = await client.get('/users');
|
|
294
|
+
assert.equal(res.status, 200);
|
|
295
|
+
assert.deepEqual(res.body, [{ id: 1, name: 'Ada' }]);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test('creates a user', async () => {
|
|
299
|
+
const res = await client.post('/users', { name: 'Grace' });
|
|
300
|
+
assert.equal(res.status, 201);
|
|
301
|
+
assert.deepEqual(res.body, { id: 2, name: 'Grace' });
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test('answers 404 for an unknown user', async () => {
|
|
305
|
+
assert.equal((await client.get('/users/99')).status, 404);
|
|
306
|
+
});
|
|
307
|
+
`,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
//# sourceMappingURL=templates.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"templates.js","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avijs/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The avi command: project scaffolding, dev mode and build checks.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"avi",
|
|
7
|
+
"avi.js",
|
|
8
|
+
"cli",
|
|
9
|
+
"scaffolding"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"author": "Christopher Robin Chase",
|
|
13
|
+
"homepage": "https://github.com/chrischase011/avi-js#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/chrischase011/avi-js/issues"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/chrischase011/avi-js.git",
|
|
20
|
+
"directory": "packages/cli"
|
|
21
|
+
},
|
|
22
|
+
"type": "module",
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=22.12"
|
|
25
|
+
},
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"bin": {
|
|
28
|
+
"avi": "./dist/bin.js"
|
|
29
|
+
},
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"@avijs/source": "./src/index.ts",
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"default": "./dist/index.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist",
|
|
39
|
+
"src",
|
|
40
|
+
"README.md",
|
|
41
|
+
"LICENSE"
|
|
42
|
+
],
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@avijs/compiler": "0.1.0"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"typescript": ">=5.8"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"typecheck": "tsc -p tsconfig.json"
|
|
54
|
+
}
|
|
6
55
|
}
|
package/src/bin.ts
ADDED
package/src/cli.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { main as compile } from '@avijs/compiler';
|
|
2
|
+
import { spawn, spawnSync, type ChildProcess } from 'node:child_process';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { mkdir, readdir, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { parseArgs } from 'node:util';
|
|
8
|
+
import { templateFiles } from './templates.js';
|
|
9
|
+
|
|
10
|
+
const { version } = createRequire(import.meta.url)('../package.json') as { version: string };
|
|
11
|
+
|
|
12
|
+
const USAGE = `Usage:
|
|
13
|
+
avi new <name> [--template modular|minimal] [--dir .] [--avi-version ^x.y.z] [--from <dir>]
|
|
14
|
+
Create a project. The modular template (default) uses modules and decorators;
|
|
15
|
+
minimal uses plain routes and services. --from points at a folder of packed
|
|
16
|
+
@avijs tarballs (pnpm pack) to install instead of the registry. --avi-version
|
|
17
|
+
defaults to this CLI's version.
|
|
18
|
+
avi dev [--project tsconfig.json] [--entry dist/main.js]
|
|
19
|
+
Compile, then recompile and restart on every change.
|
|
20
|
+
avi check [--project tsconfig.json]
|
|
21
|
+
Type-check every static inject list.
|
|
22
|
+
avi manifest <entry.js> [--export AppModule] [--out avi.manifest.json]
|
|
23
|
+
Describe the app's modules, providers and routes.`;
|
|
24
|
+
|
|
25
|
+
export async function main(argv: readonly string[]): Promise<number> {
|
|
26
|
+
const [command, ...rest] = argv;
|
|
27
|
+
switch (command) {
|
|
28
|
+
case 'new':
|
|
29
|
+
return create(rest);
|
|
30
|
+
case 'dev':
|
|
31
|
+
return dev(rest);
|
|
32
|
+
case 'check':
|
|
33
|
+
case 'manifest':
|
|
34
|
+
return compile(argv);
|
|
35
|
+
default:
|
|
36
|
+
console.error(USAGE);
|
|
37
|
+
return command === undefined || command === 'help' || command === '--help' ? 0 : 2;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function create(args: readonly string[]): Promise<number> {
|
|
42
|
+
const { values, positionals } = parseArgs({
|
|
43
|
+
args: [...args],
|
|
44
|
+
allowPositionals: true,
|
|
45
|
+
options: {
|
|
46
|
+
template: { type: 'string', default: 'modular' },
|
|
47
|
+
dir: { type: 'string', default: '.' },
|
|
48
|
+
'avi-version': { type: 'string', default: `^${version}` },
|
|
49
|
+
from: { type: 'string' },
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
const name = positionals[0];
|
|
53
|
+
if (!name || !/^[a-z0-9][a-z0-9._-]*$/.test(name)) {
|
|
54
|
+
console.error('avi new needs a project name in lowercase letters, digits, ".", "_" or "-"');
|
|
55
|
+
return 2;
|
|
56
|
+
}
|
|
57
|
+
const template = values.template;
|
|
58
|
+
if (template !== 'modular' && template !== 'minimal') {
|
|
59
|
+
console.error(`Unknown template "${template}". Use modular or minimal.`);
|
|
60
|
+
return 2;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const target = path.resolve(values.dir, name);
|
|
64
|
+
if (existsSync(target) && (await readdir(target)).length > 0) {
|
|
65
|
+
console.error(`${target} already exists and is not empty`);
|
|
66
|
+
return 1;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const files = templateFiles({
|
|
70
|
+
name,
|
|
71
|
+
template,
|
|
72
|
+
aviVersion: values['avi-version'],
|
|
73
|
+
...(values.from ? { localPackages: await localPackages(values.from, target) } : {}),
|
|
74
|
+
});
|
|
75
|
+
for (const [file, content] of Object.entries(files)) {
|
|
76
|
+
const destination = path.join(target, file);
|
|
77
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
78
|
+
await writeFile(destination, content);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log(`Created ${name} (${template}) in ${target}
|
|
82
|
+
|
|
83
|
+
Next:
|
|
84
|
+
cd ${path.relative(process.cwd(), target) || '.'}
|
|
85
|
+
npm install
|
|
86
|
+
npm run dev`);
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function dev(args: readonly string[]): Promise<number> {
|
|
91
|
+
const { values } = parseArgs({
|
|
92
|
+
args: [...args],
|
|
93
|
+
options: {
|
|
94
|
+
project: { type: 'string', default: 'tsconfig.json' },
|
|
95
|
+
entry: { type: 'string', default: 'dist/main.js' },
|
|
96
|
+
'dry-run': { type: 'boolean', default: false },
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
let tsc: string;
|
|
101
|
+
try {
|
|
102
|
+
tsc = createRequire(path.resolve('package.json')).resolve('typescript/bin/tsc');
|
|
103
|
+
} catch {
|
|
104
|
+
console.error('avi dev needs TypeScript installed in this project: npm install -D typescript');
|
|
105
|
+
return 1;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const build = [tsc, '-p', values.project];
|
|
109
|
+
const watchBuild = [tsc, '-p', values.project, '--watch', '--preserveWatchOutput'];
|
|
110
|
+
const watchRun = ['--watch', '--watch-preserve-output', values.entry];
|
|
111
|
+
if (values['dry-run']) {
|
|
112
|
+
for (const command of [build, watchBuild, watchRun]) {
|
|
113
|
+
console.log(['node', ...command].join(' '));
|
|
114
|
+
}
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const first = spawnSync(process.execPath, build, { stdio: 'inherit' });
|
|
119
|
+
if (first.status !== 0) return first.status ?? 1;
|
|
120
|
+
|
|
121
|
+
const children: ChildProcess[] = [
|
|
122
|
+
spawn(process.execPath, watchBuild, { stdio: 'inherit' }),
|
|
123
|
+
spawn(process.execPath, watchRun, { stdio: 'inherit' }),
|
|
124
|
+
];
|
|
125
|
+
return new Promise((resolve) => {
|
|
126
|
+
const stop = () => {
|
|
127
|
+
for (const child of children) child.kill();
|
|
128
|
+
resolve(0);
|
|
129
|
+
};
|
|
130
|
+
process.once('SIGINT', stop);
|
|
131
|
+
process.once('SIGTERM', stop);
|
|
132
|
+
for (const child of children) {
|
|
133
|
+
child.once('exit', (code) => {
|
|
134
|
+
if (code !== null && code !== 0) {
|
|
135
|
+
for (const other of children) other.kill();
|
|
136
|
+
resolve(code);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Maps each packed @avi tarball in `dir` to a `file:` spec relative to the new project. */
|
|
144
|
+
async function localPackages(dir: string, project: string): Promise<Record<string, string>> {
|
|
145
|
+
const folder = path.resolve(dir);
|
|
146
|
+
const specs: Record<string, string> = {};
|
|
147
|
+
for (const file of await readdir(folder)) {
|
|
148
|
+
const match = /^avijs-([a-z-]+?)-\d+\.\d+\.\d+.*\.tgz$/.exec(file);
|
|
149
|
+
if (!match?.[1]) continue;
|
|
150
|
+
const relative = path.relative(project, path.join(folder, file)).replaceAll(path.sep, '/');
|
|
151
|
+
specs[`@avijs/${match[1]}`] = `file:${relative}`;
|
|
152
|
+
}
|
|
153
|
+
if (!specs['@avijs/core']) throw new Error(`No @avijs package tarballs found in ${folder}`);
|
|
154
|
+
return specs;
|
|
155
|
+
}
|
package/src/index.ts
ADDED
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
|
+
}
|