@aventara/cli 1.0.0-rc.8 → 1.0.0-rc.9
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/CHANGELOG.md +39 -0
- package/README.md +41 -3
- package/dist/catalog/host-entry.interface.js +10 -0
- package/dist/catalog/host.catalog.generated.js +32 -0
- package/dist/command/command.parser.js +55 -3
- package/dist/plan/host-plan.interface.js +1 -0
- package/dist/plan/nest-host.planner.js +131 -0
- package/dist/plan/next-host.planner.js +133 -0
- package/dist/plan/project.planner.js +81 -166
- package/dist/project/project.inspector.js +52 -13
- package/dist/run/command.runner.js +37 -19
- package/dist/run/init.orchestrator.js +46 -29
- package/dist/run/new.orchestrator.js +100 -25
- package/dist/skill/agent-skill.generated.js +2 -2
- package/dist/templates/next-server.entry.js +5 -0
- package/dist/templates/prisma7/prisma7-nest.templates.js +242 -0
- package/dist/templates/prisma7/prisma7-next.templates.js +182 -0
- package/dist/templates/prisma7/prisma7-orm.templates.js +180 -0
- package/dist/templates/prisma7/prisma7.templates.js +7 -411
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# @aventara/cli
|
|
2
2
|
|
|
3
|
+
## 1.0.0-rc.9
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- a0bb05e: The agent skill that `aventara new`, `aventara init` and `aventara skill` write has a new section, "Calling the API over
|
|
8
|
+
HTTP", with a working curl example: the operation path uses the model's name as written in the Prisma schema
|
|
9
|
+
(`/api/_resources/User/create/one`), every request sends the `Aventara-Protocol-Version: 1` and
|
|
10
|
+
`Aventara-Contract-Hash` headers (the hash comes from `GET /api/_contract`), and a create's body is `{ "data": … }`. It
|
|
11
|
+
lists the answers each mistake gets. The printed next steps point to it. To refresh the skill in an existing
|
|
12
|
+
project, run `aventara skill`.
|
|
13
|
+
- 8c40b81: `aventara new` and `aventara init` now scaffold Next.js apps.
|
|
14
|
+
|
|
15
|
+
- `aventara new <name> --host next` runs `create-next-app@latest` (TypeScript, the App Router under `src/`, the `@/*`
|
|
16
|
+
alias, no install of its own), then the same `init` steps, then the install. create-next-app asks its own questions
|
|
17
|
+
on your terminal; with `--yes` it takes its defaults. Its own flags go after `--`, as they are:
|
|
18
|
+
`aventara new my-app --host next --yes -- --tailwind`. `--skip-git` becomes its `--disable-git`. Without a terminal,
|
|
19
|
+
`--yes` is required. `--host` defaults to `nest`: a NestJS project is scaffolded exactly as before.
|
|
20
|
+
- `aventara init` tells the host by `package.json`: `@nestjs/core` is NestJS 12, `next` is Next.js 16, and a project
|
|
21
|
+
that depends on both or neither is refused in one sentence, with nothing written. A Next.js app needs `src/app/`
|
|
22
|
+
and the `@/*` → `./src/*` path in `tsconfig.json`, and no ORM yet.
|
|
23
|
+
- In a Next.js app, init writes `src/aventara/config.ts`, `src/aventara/adapter.ts`, `src/aventara/server.ts`
|
|
24
|
+
(`Aventara()`, exporting `handlers`, `avClient`, `avApp` and `ready`), `src/app/api/[...aventara]/route.ts`,
|
|
25
|
+
`src/instrumentation.ts` (awaiting `ready()` on the Node.js runtime) and `src/prisma.ts`, with Prisma's schema and
|
|
26
|
+
config. `aventara:prepare` ends with `aventara-build --client` and runs on `postinstall` and `prebuild`; the files it
|
|
27
|
+
generates (`src/aventara/generated/`, `src/aventara/client.ts`, `src/aventara/universal.ts`) are added to
|
|
28
|
+
`.gitignore`. An instrumentation file of your own is never replaced: the lines to add are printed.
|
|
29
|
+
- Each host package now declares its project layout in its manifest (`"aventara": { "host": … }`). The CLI scaffolds
|
|
30
|
+
from it and `aventara-build` reads its paths from it, so the two always agree.
|
|
31
|
+
|
|
32
|
+
- 501b832: `aventara new --host next` and `aventara init` in a Next.js app add `serverExternalPackages: ["@aventara/core",
|
|
33
|
+
"@aventara/prisma7-adapter"]` to `next.config.ts`, so Node.js loads them once per server process: every route and
|
|
34
|
+
page shares one core, one Framework and one Prisma client. The line goes into create-next-app's config, or any config
|
|
35
|
+
whose `const nextConfig = {` object has no `serverExternalPackages`; an app with no config gets a `next.config.ts`. A
|
|
36
|
+
config that already names `serverExternalPackages`, or is written another way, is never changed, even with `--yes`:
|
|
37
|
+
the line to add is printed, with a warning. The agent skill's Next.js section says to keep it.
|
|
38
|
+
- The agent skill's Next.js section says what a page that calls `avClient` or `avApp` needs under Cache Components
|
|
39
|
+
(create-next-app's default): the calling component inside `<Suspense>` (or a `loading.tsx`), `"use cache"` on the
|
|
40
|
+
function that makes the call, or `export const instant = false`.
|
|
41
|
+
|
|
3
42
|
## 1.0.0-rc.8
|
|
4
43
|
|
|
5
44
|
1.0.0-rc.5, rc.6 and rc.7 were tagged but never published; 1.0.0-rc.8 contains everything listed under them and the changes below.
|
package/README.md
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# `@aventara/cli`
|
|
2
2
|
|
|
3
|
-
The `aventara` command: it scaffolds an Aventara server on NestJS 12.
|
|
3
|
+
The `aventara` command: it scaffolds an Aventara server on NestJS 12 or Next.js 16.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm i -g @aventara/cli@rc # or run it once: npx @aventara/cli@rc <command>
|
|
7
7
|
aventara new my-api # a new NestJS project with Aventara
|
|
8
|
-
aventara
|
|
8
|
+
aventara new my-app --host next # a new Next.js app with Aventara (create-next-app, then init)
|
|
9
|
+
aventara init # Aventara added to the NestJS 12 or Next.js 16 project in this directory
|
|
9
10
|
aventara skill # refresh the agent skill for the installed @aventara/core
|
|
10
11
|
```
|
|
11
12
|
|
|
@@ -23,9 +24,22 @@ without `.env` (it is in `.gitignore`). No commit is made, and one line says why
|
|
|
23
24
|
`user.name`/`user.email`, or when the directory is already inside a git repository (a monorepo: commit it there).
|
|
24
25
|
`--skip-git` makes no repository and no commit.
|
|
25
26
|
|
|
27
|
+
### `aventara new <name> --host next`
|
|
28
|
+
|
|
29
|
+
Runs `create-next-app@latest <name> --ts --app --src-dir --import-alias "@/*" --use-<npm|pnpm> --skip-install`, then
|
|
30
|
+
`aventara init`'s pipeline over the new app, then the install. create-next-app asks its own questions on your terminal;
|
|
31
|
+
with `--yes` it takes its defaults instead (and is given `--yes` too), which is also what a run with no terminal needs.
|
|
32
|
+
Its own flags go after `--`, as they are: `aventara new my-app --host next --yes -- --tailwind --eslint`. `--skip-git`
|
|
33
|
+
becomes its `--disable-git`. `--host` defaults to `nest`.
|
|
34
|
+
|
|
26
35
|
## `aventara init`
|
|
27
36
|
|
|
28
|
-
Runs inside an existing NestJS 12
|
|
37
|
+
Runs inside an existing NestJS 12 or Next.js 16 project, telling them apart by `package.json`: `@nestjs/core` is
|
|
38
|
+
NestJS, `next` is Next.js, and a project depending on both or neither is refused in one sentence. A Next.js app must use
|
|
39
|
+
the App Router under `src/app/` and map `@/*` to `./src/*` in `tsconfig.json` (as create-next-app writes it), and have
|
|
40
|
+
no ORM yet. The rest of this section is NestJS's; a Next.js app gets [its own files](#what-it-writes-in-a-nextjs-app).
|
|
41
|
+
|
|
42
|
+
In a NestJS project, it refuses another Nest major,
|
|
29
43
|
TypeScript 7, a yarn-only lockfile, or a project already initialized.
|
|
30
44
|
|
|
31
45
|
In a project with no ORM yet, the ORM and the database are asked (or taken from the flags), and everything below is
|
|
@@ -46,6 +60,7 @@ possible answer is shown, not asked.
|
|
|
46
60
|
| Flag | Question | Default |
|
|
47
61
|
|---|---|---|
|
|
48
62
|
| `<name>` (`new` only) | The project directory. | required |
|
|
63
|
+
| `--host <nest\|next>` (`new` only) | The framework to build on; `init` reads it from `package.json`. Not asked. | `nest` |
|
|
49
64
|
| `--orm <alias>` | The ORM, as an adapter's alias: `prisma7` (Prisma 7, `@aventara/prisma7-adapter`). `--help` lists the aliases that exist. | the only one there is |
|
|
50
65
|
| `--db <provider>` | The database: the adapter's providers (`sqlite`, `postgresql`). | `sqlite` |
|
|
51
66
|
| `--package-manager <npm\|pnpm>` | The package manager. | the lockfile's, else the one that launched the CLI, else npm |
|
|
@@ -55,6 +70,7 @@ possible answer is shown, not asked.
|
|
|
55
70
|
| `--skip-git` (`new`) | Passed to `nest new`. | |
|
|
56
71
|
| `-y`, `--yes` | Accept every unanswered default, and replace existing content that differs. | |
|
|
57
72
|
| `--verbose` | Show the output of `nest new` and the install as they run. | |
|
|
73
|
+
| `-- <args>` (`new --host next`) | Passed to create-next-app as they are, after its own. | |
|
|
58
74
|
|
|
59
75
|
While it works, each step is shown on stderr, such as `[3/4] Installing dependencies (npm install)`. On a terminal it
|
|
60
76
|
has a spinner and the seconds so far; in a pipe, in CI (`CI`) or with `NO_COLOR` it is one plain line per step
|
|
@@ -81,6 +97,28 @@ when you confirm, or with `--yes`; where nobody can be asked, the run stops and
|
|
|
81
97
|
| `.claude/skills/aventara/` (`SKILL.md`, `reference/*.md`, `.aventara-skill.json`): the agent skill your AI coding agent reads | Aventara's: `aventara skill` refreshes it, and keeps a file you changed unless `--force` |
|
|
82
98
|
| `AGENTS.md`: an Aventara section between `<!-- aventara:start -->` and `<!-- aventara:end -->`, appended to an existing file | you, except the marked section |
|
|
83
99
|
|
|
100
|
+
### What it writes in a Next.js app
|
|
101
|
+
|
|
102
|
+
Everything Aventara is in one folder, `src/aventara/`, at the paths `@aventara/next` declares and `aventara-build`
|
|
103
|
+
reads.
|
|
104
|
+
|
|
105
|
+
| Path | Owner afterwards |
|
|
106
|
+
|---|---|
|
|
107
|
+
| `prisma/schema.prisma`, `prisma.config.ts` | you (as in a NestJS project) |
|
|
108
|
+
| `src/prisma.ts`: `prisma()`, the app's one Prisma client with the driver adapter, made on first use and kept across `next dev` reloads | you |
|
|
109
|
+
| `src/aventara/config.ts`: `aventaraConfig`, as in a NestJS project, logging through `console.error` | you |
|
|
110
|
+
| `src/aventara/adapter.ts`: the adapter's description | you |
|
|
111
|
+
| `src/aventara/server.ts`: `Aventara({ ...aventaraConfig, adapter, prebuilt, state })` exporting `handlers`, `avClient`, `avApp` and `ready`; `@/aventara/server` | you |
|
|
112
|
+
| `src/app/api/[...aventara]/route.ts`: every method of `handlers` | you |
|
|
113
|
+
| `src/instrumentation.ts`: awaits `ready()` on the Node.js runtime, so a bad configuration stops `next start`. An instrumentation file of yours is never replaced, even with `--yes`: the lines to add are printed | you |
|
|
114
|
+
| `next.config.ts`: `serverExternalPackages: ["@aventara/core", "@aventara/prisma7-adapter"]` added to its configuration object, so Node.js loads them once per server process (one core and one Prisma client for every route and page). Added to create-next-app's config, or to any whose `const nextConfig = {` object has no `serverExternalPackages`; written as a new `next.config.ts` when the app has none. A config that already names `serverExternalPackages`, or is written another way, is never changed, even with `--yes`: the line to add is printed | you |
|
|
115
|
+
| `package.json`: `@aventara/core`, `@aventara/next`, the adapter and `@aventara/build` at this CLI's version, `server-only`, Prisma at `7.10.0`; `aventara:prepare` (ending in `aventara-build --client`), run by `postinstall` and `prebuild` | you |
|
|
116
|
+
| `.env` (`DATABASE_URL`), `.gitignore` (`/src/generated/`, `/dev.db*`, `/src/aventara/generated/`, `/src/aventara/client.ts`, `/src/aventara/universal.ts`, `.env`) | you |
|
|
117
|
+
| `src/aventara/client.ts`, `src/aventara/universal.ts`, `src/aventara/generated/` | generated by `aventara:prepare`; never edit |
|
|
118
|
+
|
|
119
|
+
No CORS (the browser calls the same origin), no Pages Router, no proxy, and no MCP route: an `mcp` endpoint you add on
|
|
120
|
+
Next.js needs `authorization`. Next steps: `npx prisma db push`, then `npm run dev`.
|
|
121
|
+
|
|
84
122
|
### Next steps it prints
|
|
85
123
|
|
|
86
124
|
```bash
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { HOST_CATALOG } from "./host.catalog.generated.js";
|
|
2
|
+
import { lowestMajorOf } from "./range.reader.js";
|
|
3
|
+
export const HOSTS = HOST_CATALOG;
|
|
4
|
+
export const DEFAULT_HOST = "nest";
|
|
5
|
+
export function hostOf(alias) {
|
|
6
|
+
return HOST_CATALOG.find((entry) => entry.alias === alias);
|
|
7
|
+
}
|
|
8
|
+
export function frameworkMajorOf(host) {
|
|
9
|
+
return lowestMajorOf(host.framework.range);
|
|
10
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const HOST_CATALOG = [
|
|
2
|
+
{
|
|
3
|
+
"alias": "nest",
|
|
4
|
+
"label": "NestJS",
|
|
5
|
+
"package": "@aventara/nest",
|
|
6
|
+
"framework": {
|
|
7
|
+
"package": "@nestjs/core",
|
|
8
|
+
"range": "^12.0.0"
|
|
9
|
+
},
|
|
10
|
+
"layout": {
|
|
11
|
+
"config": "src/aventara.config",
|
|
12
|
+
"adapter": "src/aventara.adapter",
|
|
13
|
+
"generated": "src/generated/aventara"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"alias": "next",
|
|
18
|
+
"label": "Next.js",
|
|
19
|
+
"package": "@aventara/next",
|
|
20
|
+
"framework": {
|
|
21
|
+
"package": "next",
|
|
22
|
+
"range": ">=16.4 <17"
|
|
23
|
+
},
|
|
24
|
+
"layout": {
|
|
25
|
+
"config": "src/aventara/config",
|
|
26
|
+
"adapter": "src/aventara/adapter",
|
|
27
|
+
"generated": "src/aventara/generated",
|
|
28
|
+
"client": "src/aventara/client.ts",
|
|
29
|
+
"universal": "src/aventara/universal.ts"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DEFAULT_HOST, HOSTS } from "../catalog/host-entry.interface.js";
|
|
1
2
|
import { INIT_FLAG_QUESTIONS, NEW_QUESTIONS, ormAliasesSentence, } from "../wizard/wizard.questions.js";
|
|
2
3
|
export class AvCliCommandError extends Error {
|
|
3
4
|
name = "AvCliCommandError";
|
|
@@ -34,6 +35,23 @@ const QUESTIONS = {
|
|
|
34
35
|
init: INIT_FLAG_QUESTIONS,
|
|
35
36
|
};
|
|
36
37
|
const HELP = "run `aventara --help` for usage";
|
|
38
|
+
const HOST_FLAG = "--host";
|
|
39
|
+
export function hostAliasesSentence() {
|
|
40
|
+
const hosts = HOSTS.map((host) => `${host.alias} (${host.label}, ${host.package})`);
|
|
41
|
+
return `--host takes ${hosts.length <= 1
|
|
42
|
+
? hosts.join("")
|
|
43
|
+
: `${hosts.slice(0, -1).join(", ")} or ${hosts[hosts.length - 1]}`}`;
|
|
44
|
+
}
|
|
45
|
+
const NEW_ONLY_ROWS = [
|
|
46
|
+
[
|
|
47
|
+
`${HOST_FLAG} <${HOSTS.map((host) => host.alias).join("|")}>`,
|
|
48
|
+
`the framework to build on; ${DEFAULT_HOST} when omitted`,
|
|
49
|
+
],
|
|
50
|
+
[
|
|
51
|
+
"-- <args>",
|
|
52
|
+
"passed as they are to the project generator (create-next-app, with --host next)",
|
|
53
|
+
],
|
|
54
|
+
];
|
|
37
55
|
function flagUsage(question, catalog) {
|
|
38
56
|
const accepted = question.accepts?.(catalog);
|
|
39
57
|
return `${question.flag} ${accepted === undefined
|
|
@@ -52,6 +70,7 @@ function usageLines(command, catalog) {
|
|
|
52
70
|
`${option.alias === undefined ? "" : `${option.alias}, `}${option.flag}`,
|
|
53
71
|
option.summary,
|
|
54
72
|
]),
|
|
73
|
+
...(command === "new" ? NEW_ONLY_ROWS : []),
|
|
55
74
|
];
|
|
56
75
|
const width = Math.max(...rows.map(([left]) => left.length));
|
|
57
76
|
return rows
|
|
@@ -59,8 +78,8 @@ function usageLines(command, catalog) {
|
|
|
59
78
|
.join("\n");
|
|
60
79
|
}
|
|
61
80
|
const SUMMARY = {
|
|
62
|
-
new: "Create a
|
|
63
|
-
init:
|
|
81
|
+
new: "Create a project with Aventara (nest new or create-next-app, then init).",
|
|
82
|
+
init: `Add Aventara to the ${HOSTS.map((host) => host.label).join(" or ")} project in this directory.`,
|
|
64
83
|
};
|
|
65
84
|
const SYNOPSIS = {
|
|
66
85
|
new: "aventara new <name> [options]",
|
|
@@ -94,7 +113,7 @@ ${usageLines(command, catalog)}
|
|
|
94
113
|
${asked(catalog)}`;
|
|
95
114
|
}
|
|
96
115
|
export function usage(catalog) {
|
|
97
|
-
return `aventara — scaffold an Aventara server on
|
|
116
|
+
return `aventara — scaffold an Aventara server on ${HOSTS.map((host) => host.label).join(" or ")}.
|
|
98
117
|
|
|
99
118
|
Usage:
|
|
100
119
|
${SYNOPSIS.new} ${SUMMARY.new}
|
|
@@ -134,10 +153,19 @@ export function parseCliCommand(argv) {
|
|
|
134
153
|
const questions = QUESTIONS[command];
|
|
135
154
|
const given = {};
|
|
136
155
|
const options = {};
|
|
156
|
+
let host;
|
|
157
|
+
let passthrough;
|
|
137
158
|
const seen = new Set();
|
|
138
159
|
const positional = questions.find((question) => !question.flag.startsWith("--"));
|
|
139
160
|
for (let index = 0; index < rest.length; index += 1) {
|
|
140
161
|
const argument = rest[index];
|
|
162
|
+
if (argument === "--") {
|
|
163
|
+
if (command !== "new") {
|
|
164
|
+
throw new AvCliCommandError(`aventara ${command} passes nothing on: arguments after -- go to aventara new's project generator; ${HELP}`);
|
|
165
|
+
}
|
|
166
|
+
passthrough = rest.slice(index + 1);
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
141
169
|
if (!argument.startsWith("-")) {
|
|
142
170
|
if (positional === undefined || given[positional.id] !== undefined) {
|
|
143
171
|
throw new AvCliCommandError(`unexpected argument ${JSON.stringify(argument)}: aventara ${command} takes ${positional === undefined ? "no" : "one"} positional argument; ${HELP}`);
|
|
@@ -148,6 +176,28 @@ export function parseCliCommand(argv) {
|
|
|
148
176
|
const equals = argument.indexOf("=");
|
|
149
177
|
const flag = equals === -1 ? argument : argument.slice(0, equals);
|
|
150
178
|
const inline = equals === -1 ? undefined : argument.slice(equals + 1);
|
|
179
|
+
if (flag === HOST_FLAG) {
|
|
180
|
+
if (command !== "new") {
|
|
181
|
+
throw new AvCliCommandError(`${HOST_FLAG} is a flag of aventara new, not aventara ${command}: init reads the host from package.json; ${HELP}`);
|
|
182
|
+
}
|
|
183
|
+
if (host !== undefined) {
|
|
184
|
+
throw new AvCliCommandError(`${HOST_FLAG} was given twice; ${HELP}`);
|
|
185
|
+
}
|
|
186
|
+
const value = inline ?? rest[index + 1];
|
|
187
|
+
if (value === undefined ||
|
|
188
|
+
(inline === undefined && value.startsWith("-"))) {
|
|
189
|
+
throw new AvCliCommandError(`${HOST_FLAG} takes a value; ${HELP}`);
|
|
190
|
+
}
|
|
191
|
+
if (inline === undefined) {
|
|
192
|
+
index += 1;
|
|
193
|
+
}
|
|
194
|
+
const alias = value.trim().toLowerCase();
|
|
195
|
+
if (!HOSTS.some((candidate) => candidate.alias === alias)) {
|
|
196
|
+
throw new AvCliCommandError(`there is no Aventara host for ${JSON.stringify(value)}; ${hostAliasesSentence()}; ${HELP}`);
|
|
197
|
+
}
|
|
198
|
+
host = alias;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
151
201
|
const question = questions.find((candidate) => candidate.flag === flag);
|
|
152
202
|
const option = OPTIONS.find((candidate) => candidate.flag === flag || candidate.alias === flag);
|
|
153
203
|
const canonical = question?.flag ?? option?.flag ?? flag;
|
|
@@ -188,6 +238,8 @@ export function parseCliCommand(argv) {
|
|
|
188
238
|
skipGit: options.skipGit === true,
|
|
189
239
|
yes: options.yes === true,
|
|
190
240
|
...(options.verbose === true ? { verbose: true } : {}),
|
|
241
|
+
...(host === undefined ? {} : { host }),
|
|
242
|
+
...(passthrough === undefined ? {} : { passthrough }),
|
|
191
243
|
};
|
|
192
244
|
}
|
|
193
245
|
function parseSkillCommand(rest) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const PREPARE_SCRIPT = "aventara:prepare";
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { spliceAppModule, wiringInstructions, } from "../apply/app-module.anchor.js";
|
|
2
|
+
import { E2E_SPEC, spliceContractTest } from "../apply/e2e-spec.anchor.js";
|
|
3
|
+
import { CORS_ORIGINS_VARIABLE, MAIN_FILE, mainCorsInstructions, SCAFFOLD_CORS_ORIGINS, spliceMainCors, } from "../apply/main-cors.anchor.js";
|
|
4
|
+
import { hostOf } from "../catalog/host-entry.interface.js";
|
|
5
|
+
import { APP_MODULE, } from "../project/project.inspector.js";
|
|
6
|
+
import { SCAFFOLD_API, } from "../templates/template.registry.js";
|
|
7
|
+
import { PREPARE_SCRIPT, } from "./host-plan.interface.js";
|
|
8
|
+
const PREPARED_SCRIPTS = [
|
|
9
|
+
"build",
|
|
10
|
+
"start",
|
|
11
|
+
"start:dev",
|
|
12
|
+
"start:debug",
|
|
13
|
+
];
|
|
14
|
+
const START_SCRIPTS = {
|
|
15
|
+
start: { from: "nest start", to: "nest start --env-file .env" },
|
|
16
|
+
"start:dev": {
|
|
17
|
+
from: "nest start --watch",
|
|
18
|
+
to: "nest start --watch --env-file .env",
|
|
19
|
+
},
|
|
20
|
+
"start:debug": {
|
|
21
|
+
from: "nest start --debug --watch",
|
|
22
|
+
to: "nest start --debug --watch --env-file .env",
|
|
23
|
+
},
|
|
24
|
+
"start:prod": {
|
|
25
|
+
from: "node dist/main",
|
|
26
|
+
to: "node --env-file-if-exists=.env dist/main",
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
const E2E_SCRIPTS = {
|
|
30
|
+
esm: {
|
|
31
|
+
from: "vitest run --config ./vitest.config.e2e.ts",
|
|
32
|
+
to: "node --env-file-if-exists=.env ./node_modules/vitest/vitest.mjs run --config ./vitest.config.e2e.ts",
|
|
33
|
+
},
|
|
34
|
+
cjs: {
|
|
35
|
+
from: "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --config ./test/jest-e2e.json",
|
|
36
|
+
to: "node --env-file-if-exists=.env --experimental-vm-modules ./node_modules/jest/bin/jest.js --config ./test/jest-e2e.json",
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
const NEST = hostOf("nest");
|
|
40
|
+
export function frontendInitCommand(packageManager, cliVersion) {
|
|
41
|
+
const tag = /^\d+\.\d+\.\d+-([0-9A-Za-z-]+)/.exec(cliVersion)?.[1];
|
|
42
|
+
const runner = packageManager === "pnpm" ? "pnpm dlx" : "npx";
|
|
43
|
+
return `${runner} @aventara/client${tag === undefined ? "" : `@${tag}`} init`;
|
|
44
|
+
}
|
|
45
|
+
export function planNestHost(builder, input) {
|
|
46
|
+
const { inspection, scaffold, packageManager } = input;
|
|
47
|
+
const wiring = {
|
|
48
|
+
imports: [
|
|
49
|
+
`import { AventaraModule } from '${NEST.package}';`,
|
|
50
|
+
...scaffold.appModule.imports,
|
|
51
|
+
],
|
|
52
|
+
entries: scaffold.appModule.entries,
|
|
53
|
+
};
|
|
54
|
+
const appModule = builder.read(APP_MODULE);
|
|
55
|
+
const edit = input.wiringByHand !== undefined
|
|
56
|
+
? { kind: "not-anchored", reason: input.wiringByHand }
|
|
57
|
+
: appModule === undefined
|
|
58
|
+
? {
|
|
59
|
+
kind: "not-anchored",
|
|
60
|
+
reason: `there is no ${APP_MODULE}`,
|
|
61
|
+
}
|
|
62
|
+
: spliceAppModule(appModule, wiring);
|
|
63
|
+
if (edit.kind === "edited") {
|
|
64
|
+
builder.write(APP_MODULE, appModule, edit.text);
|
|
65
|
+
const spec = builder.read(E2E_SPEC);
|
|
66
|
+
const tested = spec === undefined ? undefined : spliceContractTest(spec, SCAFFOLD_API);
|
|
67
|
+
if (tested !== undefined) {
|
|
68
|
+
builder.write(E2E_SPEC, spec, tested);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
builder.instruct(wiringInstructions(wiring));
|
|
73
|
+
builder.warn(`${APP_MODULE} was not edited: ${edit.reason}; add AventaraModule by hand, as printed below`);
|
|
74
|
+
}
|
|
75
|
+
const main = builder.read(MAIN_FILE);
|
|
76
|
+
const cors = main === undefined
|
|
77
|
+
? { kind: "not-anchored", reason: `there is no ${MAIN_FILE}` }
|
|
78
|
+
: spliceMainCors(main);
|
|
79
|
+
if (cors.kind === "edited") {
|
|
80
|
+
builder.write(MAIN_FILE, main, cors.text);
|
|
81
|
+
}
|
|
82
|
+
else if (cors.kind === "configured") {
|
|
83
|
+
builder.warn(`${MAIN_FILE} already configures CORS, so it was left as it is; a browser frontend on another origin is answered only if that configuration allows it`);
|
|
84
|
+
}
|
|
85
|
+
else if (cors.kind === "not-anchored") {
|
|
86
|
+
builder.instruct(mainCorsInstructions());
|
|
87
|
+
builder.warn(`${MAIN_FILE} was not edited: ${cors.reason}; add CORS by hand, as printed below`);
|
|
88
|
+
}
|
|
89
|
+
const corsWanted = cors.kind !== "configured";
|
|
90
|
+
const scaffoldWritesEnv = Object.keys(scaffold.env).length > 0;
|
|
91
|
+
const env = builder.read(".env");
|
|
92
|
+
const corsStep = corsWanted && !scaffoldWritesEnv && env === undefined
|
|
93
|
+
? [
|
|
94
|
+
`Let a browser frontend call the server: add ${CORS_ORIGINS_VARIABLE}="${SCAFFOLD_CORS_ORIGINS}" to .env (unset, CORS stays off)`,
|
|
95
|
+
]
|
|
96
|
+
: [];
|
|
97
|
+
const run = packageManager === "pnpm" ? "pnpm" : "npm";
|
|
98
|
+
return {
|
|
99
|
+
dependencies: { [NEST.package]: input.aventaraVersion },
|
|
100
|
+
build: "aventara-build",
|
|
101
|
+
scripts: Object.fromEntries(PREPARED_SCRIPTS.map((script) => [
|
|
102
|
+
`pre${script}`,
|
|
103
|
+
`${packageManager} run ${PREPARE_SCRIPT}`,
|
|
104
|
+
])),
|
|
105
|
+
rewrites: inspection.usesNestConfig
|
|
106
|
+
? {}
|
|
107
|
+
: {
|
|
108
|
+
...START_SCRIPTS,
|
|
109
|
+
"test:e2e": E2E_SCRIPTS[inspection.moduleKind],
|
|
110
|
+
},
|
|
111
|
+
...(corsWanted
|
|
112
|
+
? {
|
|
113
|
+
envEntry: {
|
|
114
|
+
key: CORS_ORIGINS_VARIABLE,
|
|
115
|
+
value: SCAFFOLD_CORS_ORIGINS,
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
: {}),
|
|
119
|
+
gitignore: [],
|
|
120
|
+
nextSteps: [
|
|
121
|
+
...(inspection.usesNestConfig
|
|
122
|
+
? [
|
|
123
|
+
"The start scripts were left as they are: @nestjs/config loads .env, so make sure it does before Prisma connects.",
|
|
124
|
+
]
|
|
125
|
+
: []),
|
|
126
|
+
...corsStep,
|
|
127
|
+
`Start the server: ${run} run start:dev`,
|
|
128
|
+
`In your frontend: ${frontendInitCommand(packageManager, input.aventaraVersion)}`,
|
|
129
|
+
],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { hostOf } from "../catalog/host-entry.interface.js";
|
|
2
|
+
import { SERVER_IMPORT } from "../templates/next-server.entry.js";
|
|
3
|
+
import { SCAFFOLD_API } from "../templates/template.registry.js";
|
|
4
|
+
import { PREPARE_SCRIPT, } from "./host-plan.interface.js";
|
|
5
|
+
const NEXT = hostOf("next");
|
|
6
|
+
export const ROUTE_FILE = `src/app${SCAFFOLD_API}/[...aventara]/route.ts`;
|
|
7
|
+
export const INSTRUMENTATION_FILE = "src/instrumentation.ts";
|
|
8
|
+
const INSTRUMENTATION_FILES = [INSTRUMENTATION_FILE, "src/instrumentation.js"];
|
|
9
|
+
const SERVER_ONLY = { "server-only": "0.0.1" };
|
|
10
|
+
function routeFile() {
|
|
11
|
+
return [
|
|
12
|
+
`import { handlers } from "${SERVER_IMPORT}";`,
|
|
13
|
+
"",
|
|
14
|
+
"// Aventara's API under the configuration's `api` path. Every method is exported,",
|
|
15
|
+
"// so one an operation does not take is answered by Aventara (405 with Allow).",
|
|
16
|
+
"export const { GET, POST, PUT, PATCH, DELETE } = handlers;",
|
|
17
|
+
"",
|
|
18
|
+
].join("\n");
|
|
19
|
+
}
|
|
20
|
+
const REGISTER_BODY = [
|
|
21
|
+
' if (process.env.NEXT_RUNTIME === "nodejs") {',
|
|
22
|
+
` const { ready } = await import("${SERVER_IMPORT}");`,
|
|
23
|
+
" await ready();",
|
|
24
|
+
" }",
|
|
25
|
+
];
|
|
26
|
+
function instrumentationFile() {
|
|
27
|
+
return [
|
|
28
|
+
"/**",
|
|
29
|
+
" * Next.js calls register() once, when the server starts. On the Node.js runtime",
|
|
30
|
+
" * it builds Aventara's framework, so a bad configuration stops `next start` and",
|
|
31
|
+
" * the first request finds it ready.",
|
|
32
|
+
" */",
|
|
33
|
+
"export async function register() {",
|
|
34
|
+
...REGISTER_BODY,
|
|
35
|
+
"}",
|
|
36
|
+
"",
|
|
37
|
+
].join("\n");
|
|
38
|
+
}
|
|
39
|
+
const NEXT_CONFIG_FILES = [
|
|
40
|
+
"next.config.ts",
|
|
41
|
+
"next.config.mts",
|
|
42
|
+
"next.config.mjs",
|
|
43
|
+
"next.config.js",
|
|
44
|
+
"next.config.cjs",
|
|
45
|
+
];
|
|
46
|
+
const CONFIG_OPENING = /^const nextConfig(?:: NextConfig)? = \{\n/m;
|
|
47
|
+
function externalLines(packages, indent) {
|
|
48
|
+
return [
|
|
49
|
+
`${indent}// Loaded by Node.js once per server process, not bundled into each layer:`,
|
|
50
|
+
`${indent}// one Aventara core and one Prisma client, shared by every route and page.`,
|
|
51
|
+
`${indent}serverExternalPackages: [${packages.map((name) => JSON.stringify(name)).join(", ")}],`,
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
function newNextConfig(packages) {
|
|
55
|
+
return [
|
|
56
|
+
'import type { NextConfig } from "next";',
|
|
57
|
+
"",
|
|
58
|
+
"const nextConfig: NextConfig = {",
|
|
59
|
+
...externalLines(packages, " "),
|
|
60
|
+
"};",
|
|
61
|
+
"",
|
|
62
|
+
"export default nextConfig;",
|
|
63
|
+
"",
|
|
64
|
+
].join("\n");
|
|
65
|
+
}
|
|
66
|
+
export function withServerExternals(text, packages) {
|
|
67
|
+
if (text.includes("serverExternalPackages")) {
|
|
68
|
+
return packages.every((name) => text.includes(JSON.stringify(name)))
|
|
69
|
+
? text
|
|
70
|
+
: undefined;
|
|
71
|
+
}
|
|
72
|
+
const opening = CONFIG_OPENING.exec(text);
|
|
73
|
+
if (opening === null) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
const at = opening.index + opening[0].length;
|
|
77
|
+
const indent = /^[ \t]+/.exec(text.slice(at))?.[0] ?? " ";
|
|
78
|
+
return `${text.slice(0, at)}${externalLines(packages, indent).join("\n")}\n${text.slice(at)}`;
|
|
79
|
+
}
|
|
80
|
+
export function planNextHost(builder, input) {
|
|
81
|
+
builder.file(ROUTE_FILE, routeFile());
|
|
82
|
+
const existing = INSTRUMENTATION_FILES.find((file) => builder.read(file) !== undefined);
|
|
83
|
+
const wanted = instrumentationFile();
|
|
84
|
+
if (existing === undefined) {
|
|
85
|
+
builder.file(INSTRUMENTATION_FILE, wanted);
|
|
86
|
+
}
|
|
87
|
+
else if (builder.read(existing) !== wanted) {
|
|
88
|
+
builder.instruct([
|
|
89
|
+
`Add Aventara's startup to register() in ${existing}:`,
|
|
90
|
+
"",
|
|
91
|
+
...REGISTER_BODY,
|
|
92
|
+
"",
|
|
93
|
+
].join("\n"));
|
|
94
|
+
builder.warn(`${existing} is yours, so it was left as it is; add the lines printed below to its register()`);
|
|
95
|
+
}
|
|
96
|
+
const configFile = NEXT_CONFIG_FILES.find((file) => builder.read(file) !== undefined);
|
|
97
|
+
if (configFile === undefined) {
|
|
98
|
+
builder.file(NEXT_CONFIG_FILES[0], newNextConfig(input.externalPackages));
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
const before = builder.read(configFile);
|
|
102
|
+
const after = withServerExternals(before, input.externalPackages);
|
|
103
|
+
if (after === undefined) {
|
|
104
|
+
builder.instruct([
|
|
105
|
+
`Add Aventara's server packages to the configuration object in ${configFile} (merge them into serverExternalPackages if it has one):`,
|
|
106
|
+
"",
|
|
107
|
+
...externalLines(input.externalPackages, " "),
|
|
108
|
+
"",
|
|
109
|
+
].join("\n"));
|
|
110
|
+
builder.warn(`${configFile} is yours, so it was left as it is; add the serverExternalPackages line printed below to it`);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
builder.write(configFile, before, after);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const { layout } = NEXT;
|
|
117
|
+
const run = input.packageManager === "pnpm" ? "pnpm" : "npm";
|
|
118
|
+
return {
|
|
119
|
+
dependencies: {
|
|
120
|
+
[NEXT.package]: input.aventaraVersion,
|
|
121
|
+
...SERVER_ONLY,
|
|
122
|
+
},
|
|
123
|
+
build: "aventara-build --client",
|
|
124
|
+
scripts: { prebuild: `${input.packageManager} run ${PREPARE_SCRIPT}` },
|
|
125
|
+
rewrites: {},
|
|
126
|
+
gitignore: [
|
|
127
|
+
`/${layout.generated}/`,
|
|
128
|
+
...(layout.client === undefined ? [] : [`/${layout.client}`]),
|
|
129
|
+
...(layout.universal === undefined ? [] : [`/${layout.universal}`]),
|
|
130
|
+
],
|
|
131
|
+
nextSteps: [`Start the app: ${run} run dev`],
|
|
132
|
+
};
|
|
133
|
+
}
|