@aventara/cli 0.0.0-stage → 0.1.0-pilot.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 +91 -0
- package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
- package/README.md +88 -2
- package/dist/apply/app-module.anchor.d.ts +30 -0
- package/dist/apply/app-module.anchor.js +185 -0
- package/dist/apply/conflict.confirmer.d.ts +24 -0
- package/dist/apply/conflict.confirmer.js +39 -0
- package/dist/apply/manifest.merger.d.ts +47 -0
- package/dist/apply/manifest.merger.js +139 -0
- package/dist/aventara.bin.d.ts +2 -0
- package/dist/aventara.bin.js +15 -0
- package/dist/catalog/adapter.catalog.generated.d.ts +19 -0
- package/dist/catalog/adapter.catalog.generated.js +35 -0
- package/dist/catalog/catalog-entry.interface.d.ts +46 -0
- package/dist/catalog/catalog-entry.interface.js +1 -0
- package/dist/catalog/catalog.matcher.d.ts +46 -0
- package/dist/catalog/catalog.matcher.js +83 -0
- package/dist/catalog/range.reader.d.ts +17 -0
- package/dist/catalog/range.reader.js +77 -0
- package/dist/cli.d.ts +30 -0
- package/dist/cli.js +96 -0
- package/dist/command/command.parser.d.ts +31 -0
- package/dist/command/command.parser.js +138 -0
- package/dist/node-version.guard.d.ts +8 -0
- package/dist/node-version.guard.js +59 -0
- package/dist/plan/project.planner.d.ts +52 -0
- package/dist/plan/project.planner.js +178 -0
- package/dist/project/package-manager.detector.d.ts +12 -0
- package/dist/project/package-manager.detector.js +14 -0
- package/dist/project/project.inspector.d.ts +38 -0
- package/dist/project/project.inspector.js +84 -0
- package/dist/project/service.detector.d.ts +45 -0
- package/dist/project/service.detector.js +106 -0
- package/dist/project/source.scanner.d.ts +16 -0
- package/dist/project/source.scanner.js +134 -0
- package/dist/run/command.runner.d.ts +15 -0
- package/dist/run/command.runner.js +20 -0
- package/dist/run/init.orchestrator.d.ts +29 -0
- package/dist/run/init.orchestrator.js +222 -0
- package/dist/run/new.orchestrator.d.ts +21 -0
- package/dist/run/new.orchestrator.js +88 -0
- package/dist/templates/prisma7/prisma7.templates.d.ts +34 -0
- package/dist/templates/prisma7/prisma7.templates.js +396 -0
- package/dist/templates/template.registry.d.ts +151 -0
- package/dist/templates/template.registry.js +12 -0
- package/dist/wizard/answer.resolver.d.ts +49 -0
- package/dist/wizard/answer.resolver.js +106 -0
- package/dist/wizard/readline.prompter.d.ts +24 -0
- package/dist/wizard/readline.prompter.js +76 -0
- package/dist/wizard/wizard.questions.d.ts +114 -0
- package/dist/wizard/wizard.questions.js +261 -0
- package/package.json +28 -3
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { lowestVersionOf } from "../../catalog/range.reader.js";
|
|
4
|
+
/**
|
|
5
|
+
* The `prisma7` entry's templates (D7): Prisma 7 knowledge as text the CLI
|
|
6
|
+
* writes and reads, keyed by the providers `@aventara/prisma7-adapter`
|
|
7
|
+
* declares. Every value that is an adapter fact — the ORM package, the driver
|
|
8
|
+
* per provider, the generator, the generate bin, the version floor — is read
|
|
9
|
+
* from the catalog entry handed in, not restated here.
|
|
10
|
+
*
|
|
11
|
+
* The texts follow the Nest scaffold's formatting (prettier, `singleQuote`,
|
|
12
|
+
* `trailingComma: "all"`, B1), so the project's own `format` script leaves them
|
|
13
|
+
* as they are.
|
|
14
|
+
*/
|
|
15
|
+
/** The generated client goes under `src`, where `nest build` compiles it (B6). */
|
|
16
|
+
const CLIENT_OUTPUT = "src/generated/prisma";
|
|
17
|
+
const SCHEMA = "prisma/schema.prisma";
|
|
18
|
+
/** B5: the Prisma CLI loads either name, and `prisma7.config.ts` wins when both exist. */
|
|
19
|
+
const CONFIG_FILES = ["prisma7.config.ts", "prisma.config.ts"];
|
|
20
|
+
const DRIVER_PREFIX = "@prisma/adapter-";
|
|
21
|
+
const NEW_SERVICE = {
|
|
22
|
+
file: "src/prisma.service.ts",
|
|
23
|
+
exportName: "PrismaService",
|
|
24
|
+
};
|
|
25
|
+
const NEW_MODULE = {
|
|
26
|
+
file: "src/prisma.service.ts",
|
|
27
|
+
exportName: "PrismaModule",
|
|
28
|
+
};
|
|
29
|
+
const PROVIDERS = {
|
|
30
|
+
sqlite: {
|
|
31
|
+
driverClass: "PrismaBetterSqlite3",
|
|
32
|
+
driverOptions: "{ url }",
|
|
33
|
+
databaseUrl: "file:./dev.db",
|
|
34
|
+
gitignore: ["/dev.db*"],
|
|
35
|
+
// B9: pnpm 12 refuses these install scripts until they are allowed.
|
|
36
|
+
nativeBuilds: ["@prisma/engines", "better-sqlite3", "prisma"],
|
|
37
|
+
},
|
|
38
|
+
postgresql: {
|
|
39
|
+
driverClass: "PrismaPg",
|
|
40
|
+
driverOptions: "{ connectionString: url }",
|
|
41
|
+
// `prisma init`'s own placeholder (B5); printed in the next steps, never asked (Q16-8).
|
|
42
|
+
databaseUrl: "postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public",
|
|
43
|
+
gitignore: [],
|
|
44
|
+
nativeBuilds: ["@prisma/engines", "prisma"],
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
/** A relative import specifier from `from` (a project file) to `to` (a project path, no extension). */
|
|
48
|
+
function specifier(from, to, js) {
|
|
49
|
+
const relative = path.posix.relative(path.posix.dirname(from), to);
|
|
50
|
+
return `${relative.startsWith(".") ? relative : `./${relative}`}${js}`;
|
|
51
|
+
}
|
|
52
|
+
function schema(input) {
|
|
53
|
+
return [
|
|
54
|
+
"generator client {",
|
|
55
|
+
` provider = "${input.entry.generators[0]}"`,
|
|
56
|
+
` output = "../${CLIENT_OUTPUT}"`,
|
|
57
|
+
...(input.moduleKind === "cjs" ? [' moduleFormat = "cjs"'] : []),
|
|
58
|
+
"}",
|
|
59
|
+
"",
|
|
60
|
+
"datasource db {",
|
|
61
|
+
` provider = "${input.provider}"`,
|
|
62
|
+
"}",
|
|
63
|
+
"",
|
|
64
|
+
// R12: Prisma's `--with-model` starter (B5).
|
|
65
|
+
"model User {",
|
|
66
|
+
" id Int @id @default(autoincrement())",
|
|
67
|
+
" email String @unique",
|
|
68
|
+
" name String?",
|
|
69
|
+
"}",
|
|
70
|
+
"",
|
|
71
|
+
].join("\n");
|
|
72
|
+
}
|
|
73
|
+
/** `prisma.config.ts` (Q16-19). Prisma does not load `.env` itself (B5). */
|
|
74
|
+
function prismaConfig() {
|
|
75
|
+
return [
|
|
76
|
+
"import { existsSync } from 'node:fs';",
|
|
77
|
+
"import { defineConfig } from 'prisma/config';",
|
|
78
|
+
"",
|
|
79
|
+
"// Prisma reads DATABASE_URL from the environment and does not load .env itself.",
|
|
80
|
+
"if (existsSync('.env')) {",
|
|
81
|
+
" process.loadEnvFile('.env');",
|
|
82
|
+
"}",
|
|
83
|
+
"",
|
|
84
|
+
"export default defineConfig({",
|
|
85
|
+
` schema: '${SCHEMA}',`,
|
|
86
|
+
" migrations: {",
|
|
87
|
+
" path: 'prisma/migrations',",
|
|
88
|
+
" },",
|
|
89
|
+
" datasource: {",
|
|
90
|
+
" url: process.env.DATABASE_URL,",
|
|
91
|
+
" },",
|
|
92
|
+
"});",
|
|
93
|
+
"",
|
|
94
|
+
].join("\n");
|
|
95
|
+
}
|
|
96
|
+
/** The developer-owned service (P2: `extends PrismaClient`, so `client: prisma`). */
|
|
97
|
+
function prismaService(input) {
|
|
98
|
+
const provider = PROVIDERS[input.provider];
|
|
99
|
+
const driver = input.entry.drivers[input.provider];
|
|
100
|
+
return [
|
|
101
|
+
"import { Injectable, Module, type OnModuleDestroy } from '@nestjs/common';",
|
|
102
|
+
`import { ${provider.driverClass} } from '${driver}';`,
|
|
103
|
+
`import { PrismaClient } from '${specifier(NEW_SERVICE.file, `${input.clientOutput}/client`, input.js)}';`,
|
|
104
|
+
"",
|
|
105
|
+
"@Injectable()",
|
|
106
|
+
"export class PrismaService extends PrismaClient implements OnModuleDestroy {",
|
|
107
|
+
" constructor() {",
|
|
108
|
+
" const url = process.env.DATABASE_URL;",
|
|
109
|
+
" if (!url) {",
|
|
110
|
+
" // Thrown while Nest builds the application, before it listens.",
|
|
111
|
+
" throw new Error(",
|
|
112
|
+
" 'DATABASE_URL is not set: put it in .env (the start scripts load it) or in the environment.',",
|
|
113
|
+
" );",
|
|
114
|
+
" }",
|
|
115
|
+
` super({ adapter: new ${provider.driverClass}(${provider.driverOptions}) });`,
|
|
116
|
+
" }",
|
|
117
|
+
"",
|
|
118
|
+
" async onModuleDestroy(): Promise<void> {",
|
|
119
|
+
" await this.$disconnect();",
|
|
120
|
+
" }",
|
|
121
|
+
"}",
|
|
122
|
+
"",
|
|
123
|
+
"@Module({ providers: [PrismaService], exports: [PrismaService] })",
|
|
124
|
+
"export class PrismaModule {}",
|
|
125
|
+
"",
|
|
126
|
+
].join("\n");
|
|
127
|
+
}
|
|
128
|
+
/** `src/aventara.config.ts` (R8, Q16-17): the framework's configuration. */
|
|
129
|
+
function aventaraConfig(input) {
|
|
130
|
+
const file = "src/aventara.config.ts";
|
|
131
|
+
const service = input.service.exportName;
|
|
132
|
+
return [
|
|
133
|
+
`import { createPrismaAdapter } from '${input.entry.adapterPackage}';`,
|
|
134
|
+
`import { discovery } from '${specifier(file, input.artifact.replace(/\.ts$/, ""), input.js)}';`,
|
|
135
|
+
`import type { ${service} } from '${specifier(file, input.service.file.replace(/\.ts$/, ""), input.js)}';`,
|
|
136
|
+
"",
|
|
137
|
+
"/**",
|
|
138
|
+
" * Aventara's configuration: the entrypoint, and the restrictions and",
|
|
139
|
+
" * pipelines you add. AventaraModule (app.module.ts) builds the framework from",
|
|
140
|
+
" * it; `discovery` is regenerated by the aventara:prepare script.",
|
|
141
|
+
" */",
|
|
142
|
+
`export async function aventaraConfig(prisma: ${service}) {`,
|
|
143
|
+
" return {",
|
|
144
|
+
" entrypoint: '/api',",
|
|
145
|
+
" adapter: await createPrismaAdapter({",
|
|
146
|
+
input.service.clientMember === undefined
|
|
147
|
+
? " client: prisma,"
|
|
148
|
+
: ` client: prisma.${input.service.clientMember},`,
|
|
149
|
+
" discovery,",
|
|
150
|
+
` provider: '${input.provider}',`,
|
|
151
|
+
` driver: '${input.entry.drivers[input.provider]}',`,
|
|
152
|
+
" }),",
|
|
153
|
+
" } as const;",
|
|
154
|
+
"}",
|
|
155
|
+
"",
|
|
156
|
+
].join("\n");
|
|
157
|
+
}
|
|
158
|
+
function prepare(input) {
|
|
159
|
+
return [
|
|
160
|
+
input.entry.bin,
|
|
161
|
+
`--schema ${input.schema}`,
|
|
162
|
+
`--client ${input.clientOutput}`,
|
|
163
|
+
`--provider ${input.provider}`,
|
|
164
|
+
`--driver ${input.entry.drivers[input.provider]}`,
|
|
165
|
+
`--out ${input.artifact}`,
|
|
166
|
+
].join(" ");
|
|
167
|
+
}
|
|
168
|
+
function appModule(service, module, js) {
|
|
169
|
+
const from = "src/app.module.ts";
|
|
170
|
+
const sameFile = service.file === module.file;
|
|
171
|
+
return {
|
|
172
|
+
imports: [
|
|
173
|
+
`import { aventaraConfig } from '${specifier(from, "src/aventara.config", js)}';`,
|
|
174
|
+
...(sameFile
|
|
175
|
+
? [
|
|
176
|
+
`import { ${module.exportName}, ${service.exportName} } from '${specifier(from, service.file.replace(/\.ts$/, ""), js)}';`,
|
|
177
|
+
]
|
|
178
|
+
: [
|
|
179
|
+
`import { ${module.exportName} } from '${specifier(from, module.file.replace(/\.ts$/, ""), js)}';`,
|
|
180
|
+
`import { ${service.exportName} } from '${specifier(from, service.file.replace(/\.ts$/, ""), js)}';`,
|
|
181
|
+
]),
|
|
182
|
+
],
|
|
183
|
+
entries: [
|
|
184
|
+
`${module.exportName},`,
|
|
185
|
+
[
|
|
186
|
+
"AventaraModule.forRootAsync({",
|
|
187
|
+
` imports: [${module.exportName}],`,
|
|
188
|
+
` inject: [${service.exportName}],`,
|
|
189
|
+
` useFactory: async (prisma: ${service.exportName}) => ({`,
|
|
190
|
+
" config: await aventaraConfig(prisma),",
|
|
191
|
+
" }),",
|
|
192
|
+
"}),",
|
|
193
|
+
].join("\n"),
|
|
194
|
+
],
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function scaffold(input) {
|
|
198
|
+
const js = input.moduleKind === "esm" ? ".js" : "";
|
|
199
|
+
const floor = lowestVersionOf(input.entry.range);
|
|
200
|
+
const provider = PROVIDERS[input.provider];
|
|
201
|
+
const driver = input.entry.drivers[input.provider];
|
|
202
|
+
const run = input.packageManager === "pnpm" ? "pnpm exec" : "npx";
|
|
203
|
+
const artifact = "src/generated/aventara/discovery.artifact.ts";
|
|
204
|
+
return {
|
|
205
|
+
files: [
|
|
206
|
+
{ path: SCHEMA, content: schema(input) },
|
|
207
|
+
{ path: "prisma.config.ts", content: prismaConfig() },
|
|
208
|
+
{
|
|
209
|
+
path: NEW_SERVICE.file,
|
|
210
|
+
content: prismaService({ ...input, clientOutput: CLIENT_OUTPUT, js }),
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
path: "src/aventara.config.ts",
|
|
214
|
+
content: aventaraConfig({
|
|
215
|
+
...input,
|
|
216
|
+
service: NEW_SERVICE,
|
|
217
|
+
artifact,
|
|
218
|
+
js,
|
|
219
|
+
}),
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
env: { DATABASE_URL: provider.databaseUrl },
|
|
223
|
+
gitignore: ["/src/generated/", ...provider.gitignore],
|
|
224
|
+
// D1, D9: exactly the version floor the adapter declares; never bare `prisma`.
|
|
225
|
+
dependencies: { [input.entry.orm.package]: floor, [driver]: floor },
|
|
226
|
+
devDependencies: { prisma: floor },
|
|
227
|
+
prepare: prepare({
|
|
228
|
+
...input,
|
|
229
|
+
schema: SCHEMA,
|
|
230
|
+
clientOutput: CLIENT_OUTPUT,
|
|
231
|
+
artifact,
|
|
232
|
+
}),
|
|
233
|
+
appModule: appModule(NEW_SERVICE, NEW_MODULE, js),
|
|
234
|
+
nativeBuilds: provider.nativeBuilds,
|
|
235
|
+
nextSteps: [
|
|
236
|
+
...(input.provider === "postgresql"
|
|
237
|
+
? ["Set DATABASE_URL in .env to your PostgreSQL database."]
|
|
238
|
+
: []),
|
|
239
|
+
`Create the database tables: ${run} prisma db push`,
|
|
240
|
+
],
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function reuse(input) {
|
|
244
|
+
const js = input.moduleKind === "esm" ? ".js" : "";
|
|
245
|
+
const { facts } = input;
|
|
246
|
+
return {
|
|
247
|
+
files: [
|
|
248
|
+
...(input.writeService
|
|
249
|
+
? [
|
|
250
|
+
{
|
|
251
|
+
path: NEW_SERVICE.file,
|
|
252
|
+
content: prismaService({
|
|
253
|
+
entry: input.entry,
|
|
254
|
+
provider: facts.provider,
|
|
255
|
+
clientOutput: facts.clientOutput,
|
|
256
|
+
js,
|
|
257
|
+
}),
|
|
258
|
+
},
|
|
259
|
+
]
|
|
260
|
+
: []),
|
|
261
|
+
{
|
|
262
|
+
path: "src/aventara.config.ts",
|
|
263
|
+
content: aventaraConfig({
|
|
264
|
+
entry: input.entry,
|
|
265
|
+
provider: facts.provider,
|
|
266
|
+
service: input.service,
|
|
267
|
+
artifact: input.artifact,
|
|
268
|
+
js,
|
|
269
|
+
}),
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
// R6: the developer's Prisma files, `.env` key and versions are theirs.
|
|
273
|
+
env: {},
|
|
274
|
+
gitignore: [`/${path.posix.dirname(input.artifact)}/`],
|
|
275
|
+
dependencies: {},
|
|
276
|
+
devDependencies: {},
|
|
277
|
+
prepare: prepare({
|
|
278
|
+
entry: input.entry,
|
|
279
|
+
schema: facts.schema,
|
|
280
|
+
clientOutput: facts.clientOutput,
|
|
281
|
+
provider: facts.provider,
|
|
282
|
+
artifact: input.artifact,
|
|
283
|
+
}),
|
|
284
|
+
appModule: appModule(input.service, input.module, js),
|
|
285
|
+
nativeBuilds: [],
|
|
286
|
+
nextSteps: [],
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
/* ------------------------------------------------------------------ *
|
|
290
|
+
* Reading an existing Prisma 7 setup (R6) *
|
|
291
|
+
* ------------------------------------------------------------------ */
|
|
292
|
+
function schemaLocation(directory, manifest) {
|
|
293
|
+
for (const config of CONFIG_FILES) {
|
|
294
|
+
const file = path.join(directory, config);
|
|
295
|
+
if (existsSync(file)) {
|
|
296
|
+
const declared = /\bschema\s*:\s*['"]([^'"]+)['"]/.exec(readFileSync(file, "utf8"))?.[1];
|
|
297
|
+
if (declared !== undefined) {
|
|
298
|
+
return path.posix.normalize(declared);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const prisma = manifest.prisma;
|
|
303
|
+
return typeof prisma?.schema === "string"
|
|
304
|
+
? path.posix.normalize(prisma.schema)
|
|
305
|
+
: SCHEMA;
|
|
306
|
+
}
|
|
307
|
+
/** The `.prisma` files of a schema path: the file, or every file in a schema folder. */
|
|
308
|
+
function schemaFiles(directory, schemaPath) {
|
|
309
|
+
const full = path.join(directory, schemaPath);
|
|
310
|
+
if (!existsSync(full)) {
|
|
311
|
+
return [];
|
|
312
|
+
}
|
|
313
|
+
if (!statSync(full).isDirectory()) {
|
|
314
|
+
return [schemaPath];
|
|
315
|
+
}
|
|
316
|
+
return readdirSync(full, { recursive: true, withFileTypes: true })
|
|
317
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".prisma"))
|
|
318
|
+
.map((entry) => path
|
|
319
|
+
.relative(directory, path.join(entry.parentPath, entry.name))
|
|
320
|
+
.split(path.sep)
|
|
321
|
+
.join("/"))
|
|
322
|
+
.sort();
|
|
323
|
+
}
|
|
324
|
+
function blocks(text, kind) {
|
|
325
|
+
return [
|
|
326
|
+
...text.matchAll(new RegExp(`^\\s*${kind}\\s+\\w+\\s*\\{([^}]*)\\}`, "gm")),
|
|
327
|
+
].map((match) => match[1]);
|
|
328
|
+
}
|
|
329
|
+
function setting(block, name) {
|
|
330
|
+
return new RegExp(`^\\s*${name}\\s*=\\s*"([^"]*)"`, "m").exec(block)?.[1];
|
|
331
|
+
}
|
|
332
|
+
function readProject(directory, manifest) {
|
|
333
|
+
const schemaPath = schemaLocation(directory, manifest);
|
|
334
|
+
let provider;
|
|
335
|
+
let generator;
|
|
336
|
+
let clientOutput;
|
|
337
|
+
for (const file of schemaFiles(directory, schemaPath)) {
|
|
338
|
+
const text = readFileSync(path.join(directory, file), "utf8").replace(/\/\/.*$/gm, "");
|
|
339
|
+
provider ??= blocks(text, "datasource")
|
|
340
|
+
.map((block) => setting(block, "provider"))
|
|
341
|
+
.find((value) => value !== undefined);
|
|
342
|
+
for (const block of blocks(text, "generator")) {
|
|
343
|
+
const declared = setting(block, "provider");
|
|
344
|
+
const output = setting(block, "output");
|
|
345
|
+
// The client generator, not a second generator (a zod or docs one) beside it.
|
|
346
|
+
if (generator === undefined || /^prisma-client/.test(declared ?? "")) {
|
|
347
|
+
generator = declared;
|
|
348
|
+
clientOutput =
|
|
349
|
+
output === undefined
|
|
350
|
+
? undefined
|
|
351
|
+
: path.posix.normalize(path.posix.join(path.posix.dirname(file), output));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
const dependencies = [
|
|
356
|
+
...Object.keys(manifest.dependencies ?? {}),
|
|
357
|
+
...Object.keys(manifest.devDependencies ?? {}),
|
|
358
|
+
];
|
|
359
|
+
return {
|
|
360
|
+
schema: schemaPath,
|
|
361
|
+
provider,
|
|
362
|
+
generator,
|
|
363
|
+
clientOutput,
|
|
364
|
+
drivers: dependencies.filter((name) => name.startsWith(DRIVER_PREFIX)),
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
function clientShape(facts) {
|
|
368
|
+
return {
|
|
369
|
+
clientClass: "PrismaClient",
|
|
370
|
+
importsClient: (imported, file) => {
|
|
371
|
+
if (!imported.startsWith(".")) {
|
|
372
|
+
return imported === "@prisma/client";
|
|
373
|
+
}
|
|
374
|
+
if (facts.clientOutput === undefined) {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
const resolved = path.posix
|
|
378
|
+
.join(path.posix.dirname(file), imported)
|
|
379
|
+
.replace(/\.(js|ts)$/, "");
|
|
380
|
+
return (resolved === facts.clientOutput ||
|
|
381
|
+
resolved === `${facts.clientOutput}/client` ||
|
|
382
|
+
resolved === `${facts.clientOutput}/index`);
|
|
383
|
+
},
|
|
384
|
+
skip: facts.clientOutput === undefined
|
|
385
|
+
? ["src/generated"]
|
|
386
|
+
: ["src/generated", facts.clientOutput],
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
export const PRISMA7_TEMPLATES = {
|
|
390
|
+
providers: PROVIDERS,
|
|
391
|
+
scaffold,
|
|
392
|
+
readProject,
|
|
393
|
+
clientShape,
|
|
394
|
+
newService: { service: NEW_SERVICE, module: NEW_MODULE },
|
|
395
|
+
reuse,
|
|
396
|
+
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type { PackageManifest } from "../apply/manifest.merger.js";
|
|
2
|
+
import type { CatalogEntry, EntryId, ProviderOf } from "../catalog/catalog-entry.interface.js";
|
|
3
|
+
import type { ClientShape, ExportReference } from "../project/service.detector.js";
|
|
4
|
+
import type { PackageManager } from "../wizard/wizard.questions.js";
|
|
5
|
+
/**
|
|
6
|
+
* D7 — what the CLI writes for an ORM lives here as text, keyed by catalog id;
|
|
7
|
+
* the adapter itself runs only through its bin.
|
|
8
|
+
*
|
|
9
|
+
* The registry is **total** in both directions, and both are checked before a
|
|
10
|
+
* project is ever scaffolded:
|
|
11
|
+
* - at compile time, over the generated catalog's literal type: an entry with
|
|
12
|
+
* no template set, or a template set missing a provider its adapter declares,
|
|
13
|
+
* does not type-check;
|
|
14
|
+
* - at build time, by the catalog generator: a templates directory with no
|
|
15
|
+
* adapter, or an adapter with no templates directory, fails the build.
|
|
16
|
+
*/
|
|
17
|
+
/** What changes with the database provider an entry offers. */
|
|
18
|
+
export type ProviderTemplate = {
|
|
19
|
+
/** The driver adapter's class, constructed in the written ORM service. */
|
|
20
|
+
readonly driverClass: string;
|
|
21
|
+
/** The driver class's constructor argument, given the connection string as `url`. */
|
|
22
|
+
readonly driverOptions: string;
|
|
23
|
+
/** The `DATABASE_URL` line's value; a placeholder where it cannot be known (Q16-8). */
|
|
24
|
+
readonly databaseUrl: string;
|
|
25
|
+
/** `.gitignore` lines this provider's files need (a SQLite database file). */
|
|
26
|
+
readonly gitignore: readonly string[];
|
|
27
|
+
/** Packages whose install scripts pnpm must be allowed to run (B9). */
|
|
28
|
+
readonly nativeBuilds: readonly string[];
|
|
29
|
+
};
|
|
30
|
+
export type ModuleKind = "esm" | "cjs";
|
|
31
|
+
/** What a template set is asked to scaffold. */
|
|
32
|
+
export type ScaffoldInput = {
|
|
33
|
+
readonly entry: CatalogEntry;
|
|
34
|
+
/** One of `entry.providers`. */
|
|
35
|
+
readonly provider: string;
|
|
36
|
+
readonly moduleKind: ModuleKind;
|
|
37
|
+
readonly packageManager: PackageManager;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* What an ORM's templates contribute to a project. Everything here is the
|
|
41
|
+
* ORM's; what is Nest's (the start scripts, the anchored `app.module.ts` edit,
|
|
42
|
+
* `AventaraModule`'s import) and what is Aventara's (its packages, the
|
|
43
|
+
* `aventara:prepare` and `postinstall` script names) is the planner's.
|
|
44
|
+
*/
|
|
45
|
+
export type OrmScaffold = {
|
|
46
|
+
/** New, developer-owned files (§4.3). */
|
|
47
|
+
readonly files: readonly {
|
|
48
|
+
readonly path: string;
|
|
49
|
+
readonly content: string;
|
|
50
|
+
}[];
|
|
51
|
+
/** `.env` entries. */
|
|
52
|
+
readonly env: Readonly<Record<string, string>>;
|
|
53
|
+
readonly gitignore: readonly string[];
|
|
54
|
+
readonly dependencies: Readonly<Record<string, string>>;
|
|
55
|
+
readonly devDependencies: Readonly<Record<string, string>>;
|
|
56
|
+
/** The command `aventara:prepare` runs (D7: the adapter only through its bin). */
|
|
57
|
+
readonly prepare: string;
|
|
58
|
+
/** What the anchored `app.module.ts` edit adds: import lines, and `imports:` entries. */
|
|
59
|
+
readonly appModule: {
|
|
60
|
+
readonly imports: readonly string[];
|
|
61
|
+
readonly entries: readonly string[];
|
|
62
|
+
};
|
|
63
|
+
readonly nativeBuilds: readonly string[];
|
|
64
|
+
/** The ORM's next steps, after the install and before the server starts (R11). */
|
|
65
|
+
readonly nextSteps: readonly string[];
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* What a project's existing ORM setup states, read by the entry's templates
|
|
69
|
+
* (R6) — the ORM's own file layout is template knowledge (D7); judging it
|
|
70
|
+
* against the catalog is the matcher's (`catalog.matcher.ts`).
|
|
71
|
+
*/
|
|
72
|
+
export type OrmProjectFacts = {
|
|
73
|
+
/** The schema the generate step reads, relative to the project root. */
|
|
74
|
+
readonly schema: string;
|
|
75
|
+
readonly provider: string | undefined;
|
|
76
|
+
readonly generator: string | undefined;
|
|
77
|
+
/** The generated client's directory, relative to the project root. */
|
|
78
|
+
readonly clientOutput: string | undefined;
|
|
79
|
+
/** The driver-adapter packages the project depends on. */
|
|
80
|
+
readonly drivers: readonly string[];
|
|
81
|
+
};
|
|
82
|
+
/** Reusing a project's ORM setup (R6): its schema, its output, its service. */
|
|
83
|
+
export type ReuseInput = {
|
|
84
|
+
readonly entry: CatalogEntry;
|
|
85
|
+
readonly facts: OrmProjectFacts & {
|
|
86
|
+
readonly provider: string;
|
|
87
|
+
readonly clientOutput: string;
|
|
88
|
+
};
|
|
89
|
+
readonly moduleKind: ModuleKind;
|
|
90
|
+
readonly packageManager: PackageManager;
|
|
91
|
+
/** The developer's service, or the template's new one when it is to be written. */
|
|
92
|
+
readonly service: ExportReference & {
|
|
93
|
+
readonly clientMember?: string;
|
|
94
|
+
};
|
|
95
|
+
readonly module: ExportReference;
|
|
96
|
+
/** Whether `service` and `module` are the template's new file, to be written. */
|
|
97
|
+
readonly writeService: boolean;
|
|
98
|
+
/** Where the discovery artifact goes, relative to the project root. */
|
|
99
|
+
readonly artifact: string;
|
|
100
|
+
};
|
|
101
|
+
export type TemplateSet<I extends EntryId> = {
|
|
102
|
+
readonly providers: {
|
|
103
|
+
readonly [P in ProviderOf<I>]: ProviderTemplate;
|
|
104
|
+
};
|
|
105
|
+
/** A project with no ORM: everything §4.3 lists. */
|
|
106
|
+
readonly scaffold: (input: ScaffoldInput) => OrmScaffold;
|
|
107
|
+
/** Reads an existing setup's facts; `undefined` when there is none to read. */
|
|
108
|
+
readonly readProject: (directory: string, manifest: PackageManifest) => OrmProjectFacts;
|
|
109
|
+
/** How the developer's service is recognised in their source. */
|
|
110
|
+
readonly clientShape: (facts: OrmProjectFacts) => ClientShape;
|
|
111
|
+
/** The service init writes when the project has none: its file and names. */
|
|
112
|
+
readonly newService: {
|
|
113
|
+
readonly service: ExportReference;
|
|
114
|
+
readonly module: ExportReference;
|
|
115
|
+
};
|
|
116
|
+
/** A project with the ORM already set up: no ORM file written or edited (R6). */
|
|
117
|
+
readonly reuse: (input: ReuseInput) => OrmScaffold;
|
|
118
|
+
};
|
|
119
|
+
export type TemplateRegistry = {
|
|
120
|
+
readonly [I in EntryId]: TemplateSet<I>;
|
|
121
|
+
};
|
|
122
|
+
export declare const TEMPLATE_REGISTRY: {
|
|
123
|
+
readonly prisma7: {
|
|
124
|
+
readonly providers: {
|
|
125
|
+
readonly sqlite: {
|
|
126
|
+
readonly driverClass: "PrismaBetterSqlite3";
|
|
127
|
+
readonly driverOptions: "{ url }";
|
|
128
|
+
readonly databaseUrl: "file:./dev.db";
|
|
129
|
+
readonly gitignore: readonly ["/dev.db*"];
|
|
130
|
+
readonly nativeBuilds: readonly ["@prisma/engines", "better-sqlite3", "prisma"];
|
|
131
|
+
};
|
|
132
|
+
readonly postgresql: {
|
|
133
|
+
readonly driverClass: "PrismaPg";
|
|
134
|
+
readonly driverOptions: "{ connectionString: url }";
|
|
135
|
+
readonly databaseUrl: "postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public";
|
|
136
|
+
readonly gitignore: readonly [];
|
|
137
|
+
readonly nativeBuilds: readonly ["@prisma/engines", "prisma"];
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
readonly scaffold: (input: ScaffoldInput) => OrmScaffold;
|
|
141
|
+
readonly readProject: (directory: string, manifest: PackageManifest) => OrmProjectFacts;
|
|
142
|
+
readonly clientShape: (facts: OrmProjectFacts) => ClientShape;
|
|
143
|
+
readonly newService: {
|
|
144
|
+
readonly service: ExportReference;
|
|
145
|
+
readonly module: ExportReference;
|
|
146
|
+
};
|
|
147
|
+
readonly reuse: (input: ReuseInput) => OrmScaffold;
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
/** The template set for `entry` — every catalog entry has one (checked at build). */
|
|
151
|
+
export declare function templatesFor(entry: CatalogEntry): TemplateSet<EntryId>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { PRISMA7_TEMPLATES } from "./prisma7/prisma7.templates.js";
|
|
2
|
+
export const TEMPLATE_REGISTRY = {
|
|
3
|
+
prisma7: PRISMA7_TEMPLATES,
|
|
4
|
+
};
|
|
5
|
+
/** The template set for `entry` — every catalog entry has one (checked at build). */
|
|
6
|
+
export function templatesFor(entry) {
|
|
7
|
+
const set = TEMPLATE_REGISTRY[entry.id];
|
|
8
|
+
if (set === undefined) {
|
|
9
|
+
throw new Error(`the catalog entry ${entry.id} has no template set; the build should have refused it`);
|
|
10
|
+
}
|
|
11
|
+
return set;
|
|
12
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { AnyWizardQuestion, QuestionId, WizardAnswers, WizardContext } from "./wizard.questions.js";
|
|
2
|
+
/**
|
|
3
|
+
* R5 — how each question gets its answer, in this order:
|
|
4
|
+
*
|
|
5
|
+
* 1. **its flag**, validated by the question (a bad value is refused, never
|
|
6
|
+
* re-asked);
|
|
7
|
+
* 2. **the only choice there is** (N3): shown on a terminal with no keypress;
|
|
8
|
+
* 3. **`--yes`**: the question's default, when it has one;
|
|
9
|
+
* 4. **the prompt**, when someone can answer (stdin is a terminal);
|
|
10
|
+
* 5. otherwise the question is **unanswered**, and once every question has been
|
|
11
|
+
* tried the run refuses in one sentence naming exactly those flags.
|
|
12
|
+
*
|
|
13
|
+
* `--yes` comes before the prompt because it means "accept each unanswered
|
|
14
|
+
* question's default" (R5 + R2), on a terminal as much as anywhere; a question
|
|
15
|
+
* with no default (`new`'s `<name>`) is still asked on a terminal.
|
|
16
|
+
*/
|
|
17
|
+
/** The seam to a person (B26): `node:readline/promises` on a terminal, a script in specs. */
|
|
18
|
+
export type Prompter = {
|
|
19
|
+
/** Whether someone can answer: stdin is a terminal. */
|
|
20
|
+
readonly interactive: boolean;
|
|
21
|
+
/** Asks once; resolves with what was typed, `""` for Enter alone. */
|
|
22
|
+
readonly ask: (request: PromptRequest) => Promise<string>;
|
|
23
|
+
/** Says one line without asking (N3, and why an answer was not accepted). */
|
|
24
|
+
readonly say: (line: string) => void;
|
|
25
|
+
};
|
|
26
|
+
export type PromptRequest = {
|
|
27
|
+
readonly label: string;
|
|
28
|
+
/** The accepted values, shown with the label; absent for free text. */
|
|
29
|
+
readonly choices?: readonly string[];
|
|
30
|
+
readonly defaultValue?: string;
|
|
31
|
+
};
|
|
32
|
+
/** Nobody can answer and some questions are unanswered. A refusal: one sentence. */
|
|
33
|
+
export declare class UnansweredQuestionsError extends Error {
|
|
34
|
+
readonly name = "UnansweredQuestionsError";
|
|
35
|
+
}
|
|
36
|
+
export type AnswerSources = {
|
|
37
|
+
/** Raw answers from the command line, by question. */
|
|
38
|
+
readonly given: Readonly<Partial<Record<QuestionId, string>>>;
|
|
39
|
+
readonly yes: boolean;
|
|
40
|
+
readonly prompter: Prompter;
|
|
41
|
+
readonly context: Omit<WizardContext, "answered">;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Settles `questions` in order, each later question seeing the earlier answers.
|
|
45
|
+
*
|
|
46
|
+
* @throws WizardAnswerError when a flag's value is not an answer.
|
|
47
|
+
* @throws UnansweredQuestionsError when nobody can answer and something is unanswered.
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolveAnswers(questions: readonly AnyWizardQuestion[], sources: AnswerSources): Promise<Partial<WizardAnswers>>;
|