@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
|
@@ -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.1",
|
|
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.1"
|
|
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,160 @@
|
|
|
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 { GENERATE_USAGE, generate } from './generate.js';
|
|
9
|
+
import { templateFiles } from './templates.js';
|
|
10
|
+
|
|
11
|
+
const { version } = createRequire(import.meta.url)('../package.json') as { version: string };
|
|
12
|
+
|
|
13
|
+
const USAGE = `Usage:
|
|
14
|
+
avi new <name> [--template modular|minimal] [--dir .] [--avi-version ^x.y.z] [--from <dir>]
|
|
15
|
+
Create a project. The modular template (default) uses modules and decorators;
|
|
16
|
+
minimal uses plain routes and services. --from points at a folder of packed
|
|
17
|
+
@avijs tarballs (pnpm pack) to install instead of the registry. --avi-version
|
|
18
|
+
defaults to this CLI's version.
|
|
19
|
+
${GENERATE_USAGE}
|
|
20
|
+
avi dev [--project tsconfig.json] [--entry dist/main.js]
|
|
21
|
+
Compile, then recompile and restart on every change.
|
|
22
|
+
avi check [--project tsconfig.json]
|
|
23
|
+
Type-check every static inject list.
|
|
24
|
+
avi manifest <entry.js> [--export AppModule] [--out avi.manifest.json]
|
|
25
|
+
Describe the app's modules, providers and routes.`;
|
|
26
|
+
|
|
27
|
+
export async function main(argv: readonly string[]): Promise<number> {
|
|
28
|
+
const [command, ...rest] = argv;
|
|
29
|
+
switch (command) {
|
|
30
|
+
case 'new':
|
|
31
|
+
return create(rest);
|
|
32
|
+
case 'generate':
|
|
33
|
+
case 'g':
|
|
34
|
+
return generate(rest);
|
|
35
|
+
case 'dev':
|
|
36
|
+
return dev(rest);
|
|
37
|
+
case 'check':
|
|
38
|
+
case 'manifest':
|
|
39
|
+
return compile(argv);
|
|
40
|
+
default:
|
|
41
|
+
console.error(USAGE);
|
|
42
|
+
return command === undefined || command === 'help' || command === '--help' ? 0 : 2;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function create(args: readonly string[]): Promise<number> {
|
|
47
|
+
const { values, positionals } = parseArgs({
|
|
48
|
+
args: [...args],
|
|
49
|
+
allowPositionals: true,
|
|
50
|
+
options: {
|
|
51
|
+
template: { type: 'string', default: 'modular' },
|
|
52
|
+
dir: { type: 'string', default: '.' },
|
|
53
|
+
'avi-version': { type: 'string', default: `^${version}` },
|
|
54
|
+
from: { type: 'string' },
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
const name = positionals[0];
|
|
58
|
+
if (!name || !/^[a-z0-9][a-z0-9._-]*$/.test(name)) {
|
|
59
|
+
console.error('avi new needs a project name in lowercase letters, digits, ".", "_" or "-"');
|
|
60
|
+
return 2;
|
|
61
|
+
}
|
|
62
|
+
const template = values.template;
|
|
63
|
+
if (template !== 'modular' && template !== 'minimal') {
|
|
64
|
+
console.error(`Unknown template "${template}". Use modular or minimal.`);
|
|
65
|
+
return 2;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const target = path.resolve(values.dir, name);
|
|
69
|
+
if (existsSync(target) && (await readdir(target)).length > 0) {
|
|
70
|
+
console.error(`${target} already exists and is not empty`);
|
|
71
|
+
return 1;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const files = templateFiles({
|
|
75
|
+
name,
|
|
76
|
+
template,
|
|
77
|
+
aviVersion: values['avi-version'],
|
|
78
|
+
...(values.from ? { localPackages: await localPackages(values.from, target) } : {}),
|
|
79
|
+
});
|
|
80
|
+
for (const [file, content] of Object.entries(files)) {
|
|
81
|
+
const destination = path.join(target, file);
|
|
82
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
83
|
+
await writeFile(destination, content);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.log(`Created ${name} (${template}) in ${target}
|
|
87
|
+
|
|
88
|
+
Next:
|
|
89
|
+
cd ${path.relative(process.cwd(), target) || '.'}
|
|
90
|
+
npm install
|
|
91
|
+
npm run dev`);
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function dev(args: readonly string[]): Promise<number> {
|
|
96
|
+
const { values } = parseArgs({
|
|
97
|
+
args: [...args],
|
|
98
|
+
options: {
|
|
99
|
+
project: { type: 'string', default: 'tsconfig.json' },
|
|
100
|
+
entry: { type: 'string', default: 'dist/main.js' },
|
|
101
|
+
'dry-run': { type: 'boolean', default: false },
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
let tsc: string;
|
|
106
|
+
try {
|
|
107
|
+
tsc = createRequire(path.resolve('package.json')).resolve('typescript/bin/tsc');
|
|
108
|
+
} catch {
|
|
109
|
+
console.error('avi dev needs TypeScript installed in this project: npm install -D typescript');
|
|
110
|
+
return 1;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const build = [tsc, '-p', values.project];
|
|
114
|
+
const watchBuild = [tsc, '-p', values.project, '--watch', '--preserveWatchOutput'];
|
|
115
|
+
const watchRun = ['--watch', '--watch-preserve-output', values.entry];
|
|
116
|
+
if (values['dry-run']) {
|
|
117
|
+
for (const command of [build, watchBuild, watchRun]) {
|
|
118
|
+
console.log(['node', ...command].join(' '));
|
|
119
|
+
}
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const first = spawnSync(process.execPath, build, { stdio: 'inherit' });
|
|
124
|
+
if (first.status !== 0) return first.status ?? 1;
|
|
125
|
+
|
|
126
|
+
const children: ChildProcess[] = [
|
|
127
|
+
spawn(process.execPath, watchBuild, { stdio: 'inherit' }),
|
|
128
|
+
spawn(process.execPath, watchRun, { stdio: 'inherit' }),
|
|
129
|
+
];
|
|
130
|
+
return new Promise((resolve) => {
|
|
131
|
+
const stop = () => {
|
|
132
|
+
for (const child of children) child.kill();
|
|
133
|
+
resolve(0);
|
|
134
|
+
};
|
|
135
|
+
process.once('SIGINT', stop);
|
|
136
|
+
process.once('SIGTERM', stop);
|
|
137
|
+
for (const child of children) {
|
|
138
|
+
child.once('exit', (code) => {
|
|
139
|
+
if (code !== null && code !== 0) {
|
|
140
|
+
for (const other of children) other.kill();
|
|
141
|
+
resolve(code);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Maps each packed @avi tarball in `dir` to a `file:` spec relative to the new project. */
|
|
149
|
+
async function localPackages(dir: string, project: string): Promise<Record<string, string>> {
|
|
150
|
+
const folder = path.resolve(dir);
|
|
151
|
+
const specs: Record<string, string> = {};
|
|
152
|
+
for (const file of await readdir(folder)) {
|
|
153
|
+
const match = /^avijs-([a-z-]+?)-\d+\.\d+\.\d+.*\.tgz$/.exec(file);
|
|
154
|
+
if (!match?.[1]) continue;
|
|
155
|
+
const relative = path.relative(project, path.join(folder, file)).replaceAll(path.sep, '/');
|
|
156
|
+
specs[`@avijs/${match[1]}`] = `file:${relative}`;
|
|
157
|
+
}
|
|
158
|
+
if (!specs['@avijs/core']) throw new Error(`No @avijs package tarballs found in ${folder}`);
|
|
159
|
+
return specs;
|
|
160
|
+
}
|