@aventara/build 1.0.0-rc.4 → 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 CHANGED
@@ -1,5 +1,126 @@
1
1
  # @aventara/build
2
2
 
3
+ ## 1.0.0-rc.9
4
+
5
+ ### Patch Changes
6
+
7
+ - 99278e4: `aventara-build` reads a Next.js project's files from `src/aventara/` and, with `--client`, writes its browser client.
8
+
9
+ - A project whose `package.json` depends on `@aventara/next` keeps its configuration and adapter in
10
+ `src/aventara/config.ts` and `src/aventara/adapter.ts`; the build runs them there, and a configuration that imports no
11
+ Resource tree has its generated files in `src/aventara/generated/`. Every other project keeps
12
+ `src/aventara.config.ts` and `src/aventara.adapter.ts`, unchanged. A missing file is named with where the project's
13
+ host keeps it, and a project depending on both `@aventara/nest` and `@aventara/next` is refused in one sentence.
14
+ - `aventara-build --client` also writes the browser client from the ClientContract it has just compiled, with no
15
+ running server: `src/aventara/client.ts` and the client's modules under `src/aventara/generated/`, the same modules
16
+ `avclient generate` writes for that ClientContract except that the entrypoint is the app's own `api` on the page's
17
+ origin (`/api`) and the regenerate instructions. Its imports are spelled as the project's `tsconfig.json` says, and nothing it writes imports
18
+ `@aventara/client`.
19
+ - `src/aventara/client.ts` exports `avClient` and `avHydrate`, and re-exports every name `avclient generate`'s
20
+ `AvClient.ts` exports besides its default (`AvClient`, the Resource types, the enums, `AvDecimal`, the error
21
+ classes), so `import { AvDecimal } from "@/aventara/client"` works. The generated files and the contract-mismatch
22
+ error say to regenerate with `aventara-build --client` (the project's `aventara:prepare` script). `avClient` calls the API from the browser; a call made
23
+ on the server (a Client Component rendering there) rejects with one sentence pointing to `@/aventara/server` and
24
+ `@/aventara/universal`, and sends nothing. `avHydrate` revives data `avDehydrate` sends from the server: tagged
25
+ dates, `bigint`s, byte arrays and decimals become `Date`, `bigint`, `Uint8Array` and the client's own `AvDecimal`,
26
+ for TanStack Query's `deserializeData` or an SWR `fallback`.
27
+ - `aventara-build --check --client` checks those files too: a missing or stale one, or one an earlier ClientContract's
28
+ client left, exits 1 naming it. `--client` is refused in a NestJS project, whose frontends keep using
29
+ `avclient generate`, which is unchanged.
30
+ - `@aventara/client` gains an `@aventara/client/internal` entry, which `@aventara/build` reads the generator through.
31
+ It is not public API.
32
+
33
+ - 8c40b81: `aventara new` and `aventara init` now scaffold Next.js apps.
34
+
35
+ - `aventara new <name> --host next` runs `create-next-app@latest` (TypeScript, the App Router under `src/`, the `@/*`
36
+ alias, no install of its own), then the same `init` steps, then the install. create-next-app asks its own questions
37
+ on your terminal; with `--yes` it takes its defaults. Its own flags go after `--`, as they are:
38
+ `aventara new my-app --host next --yes -- --tailwind`. `--skip-git` becomes its `--disable-git`. Without a terminal,
39
+ `--yes` is required. `--host` defaults to `nest`: a NestJS project is scaffolded exactly as before.
40
+ - `aventara init` tells the host by `package.json`: `@nestjs/core` is NestJS 12, `next` is Next.js 16, and a project
41
+ that depends on both or neither is refused in one sentence, with nothing written. A Next.js app needs `src/app/`
42
+ and the `@/*` → `./src/*` path in `tsconfig.json`, and no ORM yet.
43
+ - In a Next.js app, init writes `src/aventara/config.ts`, `src/aventara/adapter.ts`, `src/aventara/server.ts`
44
+ (`Aventara()`, exporting `handlers`, `avClient`, `avApp` and `ready`), `src/app/api/[...aventara]/route.ts`,
45
+ `src/instrumentation.ts` (awaiting `ready()` on the Node.js runtime) and `src/prisma.ts`, with Prisma's schema and
46
+ config. `aventara:prepare` ends with `aventara-build --client` and runs on `postinstall` and `prebuild`; the files it
47
+ generates (`src/aventara/generated/`, `src/aventara/client.ts`, `src/aventara/universal.ts`) are added to
48
+ `.gitignore`. An instrumentation file of your own is never replaced: the lines to add are printed.
49
+ - Each host package now declares its project layout in its manifest (`"aventara": { "host": … }`). The CLI scaffolds
50
+ from it and `aventara-build` reads its paths from it, so the two always agree.
51
+
52
+ - 33493e4: `@/aventara/universal` and `avDehydrate`: data prefetched on the server keeps its values in the browser.
53
+
54
+ - `aventara-build --client` also writes `src/aventara/universal.ts`. Its `avClient` has the same types as
55
+ `@/aventara/client`'s and runs on both sides: in the browser it calls the app's `/api`; on the server it answers the
56
+ same request in process, with the request's `state`, so a `Date`, a `bigint` or a decimal comes back the same on
57
+ either side. It needs `src/instrumentation.ts` to await `ready()`; on a server where nothing ran it, a call is
58
+ refused with one sentence naming `instrumentation.ts` and `ready()`. Each call first awaits Next's `io()`, as
59
+ `@aventara/next`'s `avClient` does, so under Cache Components a page's call runs at request time, never while
60
+ `next build` prerenders it (the page needs `<Suspense>`, `"use cache"` or `export const instant = false`). Like
61
+ the other generated files, it imports nothing from `@aventara/client` or `@aventara/next` (only `io` from
62
+ `next/cache`), and `aventara-build --check --client` checks it.
63
+ - `ready()` now also registers the in-process path the universal `avClient` takes on the server.
64
+ - `@aventara/next` exports `avDehydrate`. It turns each `Date`, `bigint`, `Uint8Array` and decimal in a result into a
65
+ tagged plain object that React can pass from a Server Component to a Client Component, where `avHydrate` from
66
+ `@/aventara/client` turns it back. Use it as TanStack Query's `dehydrate(queryClient, { serializeData: avDehydrate })`
67
+ with `deserializeData: avHydrate` on the browser's `QueryClient`, or on an SWR `fallback`.
68
+
69
+ - Updated dependencies [99278e4]
70
+ - Updated dependencies [205a379]
71
+ - Updated dependencies [a0bb05e]
72
+ - Updated dependencies [86ab319]
73
+ - Updated dependencies [fdf52e6]
74
+ - @aventara/client@1.0.0-rc.9
75
+ - @aventara/core@1.0.0-rc.9
76
+
77
+ ## 1.0.0-rc.8
78
+
79
+ 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.
80
+
81
+ ### Patch Changes
82
+
83
+ - 1c8e4ee: `aventara-build --check` says what changed when the prebuilt Contracts file is stale, in the same words as the
84
+ `AVENTARA_PREBUILT_STALE` warning at startup. Before, anything outside a single Resource read "an input that belongs
85
+ to no single Resource, the core version or the file format changed". Now it names the versions or the inputs:
86
+
87
+ ```text
88
+ aventara-build: src/generated/aventara/contracts.generated.ts is stale: it was built by @aventara/core 1.0.0-rc.7, and this is 1.0.0-rc.8. Run aventara-build (npm run aventara:prepare) and deploy the file it writes.
89
+ aventara-build: src/generated/aventara/contracts.generated.ts is stale: Resource User changed and the configuration or model outside the Resources changed since it was written. Run aventara-build (npm run aventara:prepare) and deploy the file it writes.
90
+ ```
91
+
92
+ It also names a file format or protocol version that differs, the Resources that are new or gone, and a file that
93
+ no longer reads as a prebuilt Contracts file.
94
+
95
+ - Updated dependencies [1c8e4ee]
96
+ - @aventara/core@1.0.0-rc.8
97
+
98
+ ## 1.0.0-rc.7
99
+
100
+ 1.0.0-rc.5 and 1.0.0-rc.6 were tagged but never published; 1.0.0-rc.7 contains everything listed under them and the changes below.
101
+
102
+ ### Patch Changes
103
+
104
+ - Updated dependencies [f917c1c]
105
+ - @aventara/core@1.0.0-rc.7
106
+
107
+ ## 1.0.0-rc.6
108
+
109
+ 1.0.0-rc.5 was tagged but never published. 1.0.0-rc.6 contains everything listed under 1.0.0-rc.5 and the fix below.
110
+
111
+ ### Patch Changes
112
+
113
+ - Updated dependencies [1cf72e0]
114
+ - @aventara/core@1.0.0-rc.6
115
+
116
+ ## 1.0.0-rc.5
117
+
118
+ ### Patch Changes
119
+
120
+ - Updated dependencies [4795ff1]
121
+ - Updated dependencies [9e178ca]
122
+ - @aventara/core@1.0.0-rc.5
123
+
3
124
  ## 1.0.0-rc.4
4
125
 
5
126
  ### Patch Changes
package/README.md CHANGED
@@ -100,15 +100,50 @@ diagnostic is printed with its path and documentation link, nothing is written,
100
100
  Because the build runs `src/aventara.config.ts`, everything it imports runs at build time too. Keep its top level free of
101
101
  side effects (no connection, no request). A value read from `process.env` is read when the build runs.
102
102
 
103
+ ## Next.js projects
104
+
105
+ A project whose `package.json` depends on `@aventara/next` keeps everything Aventara in one folder, `src/aventara/`. The
106
+ build runs `src/aventara/config.ts` and `src/aventara/adapter.ts` there instead of `src/aventara.config.ts` and
107
+ `src/aventara.adapter.ts`, and a configuration that imports no Resource tree has its files in `src/aventara/generated/`.
108
+ Every other project keeps the paths above.
109
+
110
+ ### `--client`
111
+
112
+ ```bash
113
+ aventara-build --client
114
+ ```
115
+
116
+ In a Next.js project, `--client` also writes the browser client, from the ClientContract the build has just compiled,
117
+ so no running server is needed:
118
+
119
+ | File | What it holds |
120
+ |---|---|
121
+ | `src/aventara/client.ts` | `avClient`, the typed client calling the app's own `api` (`/api`) from the browser, and `avHydrate`; and every name `avclient generate`'s `AvClient.ts` exports besides its default: `AvClient`, the Resource types, the enums, `AvDecimal` and the error classes |
122
+ | `src/aventara/universal.ts` | `avClient` for both sides: in the browser it calls the app's own `api`, on the server it calls the app in process, through what `ready()` from `@aventara/next` registers in `src/instrumentation.ts`, with that request's state |
123
+ | `src/aventara/generated/` (the client's modules) | the same modules `avclient generate` writes for the same ClientContract, but with the app's own `api` as the entrypoint, and saying to regenerate them with `aventara-build --client` |
124
+
125
+ Import it as `@/aventara/client` in Client Components, and `@/aventara/universal` in code that runs on both sides (a
126
+ TanStack Query `queryFn` prefetched on the server, for example). The files import nothing from `@aventara/client` or
127
+ `@aventara/next`, so the app needs no generator at run time. `avClient` calls the API from the browser only: a call made on the server, while a
128
+ Client Component renders there, is refused with a sentence pointing to `@/aventara/server` and `@/aventara/universal`.
129
+ The universal `avClient` answers the same values on both sides; on a server where `ready()` has not run, a call is
130
+ refused with a sentence naming `src/instrumentation.ts` and `ready()`. `avHydrate` revives data `avDehydrate` sent from the server (dates, `bigint`s, byte arrays and decimals, a decimal as
131
+ the client's own `AvDecimal`), for TanStack Query's `deserializeData` or an SWR `fallback`.
132
+
133
+ `avclient generate` stays the way to generate a client for a separate frontend. In a NestJS project `--client` is
134
+ refused.
135
+
103
136
  ## `--check`
104
137
 
105
138
  ```bash
106
139
  aventara-build --check
140
+ aventara-build --check --client
107
141
  ```
108
142
 
109
143
  Writes nothing. It exits 0 when the three files on disk are the ones a build would write now, and 1 on the first that is
110
- missing or stale, naming it (and, for the Contracts, the Resources whose configuration or model changed). Run it in CI
111
- before a deploy.
144
+ missing or stale, naming it (and, for the Contracts, the Resources whose configuration or model changed). With
145
+ `--client`, the browser client's files are checked too, after the Contracts: a missing or stale file, or one an earlier
146
+ ClientContract's client left, exits 1. Run it in CI before a deploy.
112
147
 
113
148
  ## Using the file
114
149
 
package/dist/cli.js CHANGED
@@ -1,20 +1,23 @@
1
1
  import path from "node:path";
2
2
  import { AvBuildError } from "./build.error.js";
3
+ import { checkBrowserClient, emitBrowserClient, writeBrowserClient, } from "./client/client-file.js";
3
4
  import { readConfigComputedFields } from "./computed/computed-fields.reader.js";
4
5
  import { emitComputedFieldsModule } from "./computed/computed-fields-file.js";
5
6
  import { checkContractsFile, writeContractsFile, } from "./contracts/contracts-file.js";
6
7
  import { compilePrebuiltContracts } from "./contracts/prebuilt-contracts.compiler.js";
7
8
  import { readGeneratedFile, writeGeneratedFile, } from "./project/generated-file.js";
8
9
  import { DEFAULT_GENERATED_FILES, resolveGeneratedFiles, } from "./project/generated-files.resolver.js";
9
- import { findProjectModules, loadProjectModules, PROJECT_MODULES, } from "./project/project-modules.loader.js";
10
+ import { PROJECT_LAYOUTS, resolveProjectLayout, } from "./project/project-layout.resolver.js";
11
+ import { findProjectModules, loadProjectModules, } from "./project/project-modules.loader.js";
10
12
  import { importSuffixOf, projectCompilerOptions, } from "./project/tsconfig.reader.js";
11
13
  import { RESOURCES_GLOB, readResourceDefinitions, } from "./resources/resource-definitions.reader.js";
12
14
  import { emitResourcesModule } from "./resources/resources-file.js";
13
15
  const USAGE = `aventara-build — compile this project's Aventara Contracts ahead of time.
14
16
 
15
17
  Usage:
16
- aventara-build write the three files below
17
- aventara-build --check write nothing; exit 1 when one is missing or stale
18
+ aventara-build write the three files below
19
+ aventara-build --client also write a Next.js app's browser client
20
+ aventara-build --check [--client] write nothing; exit 1 when one is missing or stale
18
21
 
19
22
  Run it from the project's root. It writes, in this order:
20
23
 
@@ -28,36 +31,50 @@ Run it from the project's root. It writes, in this order:
28
31
 
29
32
  All three go to the directory the configuration imports \`aventaraResources\`
30
33
  from (src/aventara/ when \`aventara init\` found the ORM's client generated into
31
- src/generated), else to src/generated/aventara/.
34
+ src/generated), else to ${PROJECT_LAYOUTS.nest.generated}/ (${PROJECT_LAYOUTS.next.generated}/
35
+ in a Next.js project).
32
36
 
33
- It runs ${PROJECT_MODULES.config}.ts (the configuration, its
34
- \`aventaraConfig\` export or its default, without \`adapter\`) and
35
- ${PROJECT_MODULES.adapter}.ts (its default export: the adapter's
36
- description, such as \`describePrismaAdapter({ discovery, provider, driver })\`),
37
- in TypeScript or JavaScript, and compiles the Contracts with the project's own
38
- @aventara/core. It never constructs a database client.
37
+ It runs the configuration (its \`aventaraConfig\` export or its default,
38
+ without \`adapter\`) and the adapter's description (its default export, such
39
+ as \`describePrismaAdapter({ discovery, provider, driver })\`), in TypeScript
40
+ or JavaScript, and compiles the Contracts with the project's own
41
+ @aventara/core. It never constructs a database client. Where they are depends
42
+ on the project's host:
43
+
44
+ a Next.js project (its package.json depends on @aventara/next)
45
+ ${PROJECT_LAYOUTS.next.config}.ts and ${PROJECT_LAYOUTS.next.adapter}.ts
46
+ any other project (NestJS)
47
+ ${PROJECT_LAYOUTS.nest.config}.ts and ${PROJECT_LAYOUTS.nest.adapter}.ts
48
+
49
+ With --client, a Next.js project also gets its browser client, written from
50
+ the ClientContract just compiled: ${PROJECT_LAYOUTS.next.client.folder}/${PROJECT_LAYOUTS.next.client.entry}
51
+ (\`avClient\`, calling the app's own \`api\` from the browser, and \`avHydrate\`),
52
+ ${PROJECT_LAYOUTS.next.client.folder}/${PROJECT_LAYOUTS.next.client.universal} (\`avClient\` for both sides: the app's \`api\`
53
+ from the browser, the app in process on the server) and the client's modules
54
+ under ${PROJECT_LAYOUTS.next.client.folder}/generated/.
39
55
 
40
56
  A serverless or edge host passes the file's \`aventaraContracts\` to
41
57
  createFramework as \`prebuilt\`; a long-running server needs none.
42
58
 
43
59
  Options:
60
+ --client Also write the browser client (Next.js projects).
44
61
  --check Exit 1, writing nothing, when a file is missing or stale.
45
62
  -h, --help Print this and exit 0.`;
46
63
  function parse(argv) {
47
64
  let check = false;
65
+ let client = false;
48
66
  for (const argument of argv) {
49
67
  if (argument === "--check") {
50
68
  check = true;
51
69
  continue;
52
70
  }
71
+ if (argument === "--client") {
72
+ client = true;
73
+ continue;
74
+ }
53
75
  throw new AvBuildError(`unknown argument \`${argument}\`.\n\n${USAGE}`);
54
76
  }
55
- return { check };
56
- }
57
- function listed(names) {
58
- return names.length < 2
59
- ? names.join("")
60
- : `${names.slice(0, -1).join(", ")} and ${names.at(-1)}`;
77
+ return { check, client };
61
78
  }
62
79
  function counted(count) {
63
80
  return `${count} Resource${count === 1 ? "" : "s"}`;
@@ -82,9 +99,14 @@ export async function runBuild(argv, projectDirectory, output) {
82
99
  return 0;
83
100
  }
84
101
  try {
85
- const { check } = parse(argv);
86
- const files = findProjectModules(projectDirectory);
87
- const generated = resolveGeneratedFiles(projectDirectory, files.configFile);
102
+ const { check, client } = parse(argv);
103
+ const layout = resolveProjectLayout(projectDirectory);
104
+ const clientLayout = client ? layout.client : undefined;
105
+ if (client && clientLayout === undefined) {
106
+ throw new AvBuildError(`--client writes the browser client of a Next.js project, and ${projectDirectory} is ${layout.described}. A separate frontend generates its client from the running server with \`avclient generate\`.`);
107
+ }
108
+ const files = findProjectModules(projectDirectory, layout);
109
+ const generated = resolveGeneratedFiles(projectDirectory, files.configFile, layout.generated);
88
110
  const options = projectCompilerOptions(projectDirectory);
89
111
  const found = readResourceDefinitions(projectDirectory, options);
90
112
  const tree = emitResourcesModule(projectDirectory, generated.resources, found.definitions, importSuffixOf(options, path.dirname(path.join(projectDirectory, generated.resources))));
@@ -133,13 +155,40 @@ export async function runBuild(argv, projectDirectory, output) {
133
155
  const project = await loadProjectModules(projectDirectory, files);
134
156
  const compiled = await compilePrebuiltContracts(project);
135
157
  const resources = Object.keys(compiled.prebuilt.fingerprints.resources).length;
158
+ const browserClient = clientLayout === undefined
159
+ ? undefined
160
+ : await emitBrowserClient(projectDirectory, clientLayout, {
161
+ client: compiled.client,
162
+ api: compiled.api,
163
+ canonicalText: (contract) => project.internal.canonicalizeContract(contract),
164
+ });
136
165
  if (!check) {
137
166
  output.stdout(wrote(writeContractsFile(projectDirectory, generated.contracts, compiled), generated.contracts, counted(resources)));
167
+ if (browserClient !== undefined) {
168
+ for (const warning of browserClient.warnings) {
169
+ output.stdout(`aventara-build: ${warning}`);
170
+ }
171
+ const written = writeBrowserClient(projectDirectory, browserClient);
172
+ for (const file of written.removed) {
173
+ output.stdout(`aventara-build: removed ${file}, which this ClientContract's browser client no longer has.`);
174
+ }
175
+ const [entry, universal] = browserClient.files;
176
+ output.stdout(wrote(written.write, `${entry?.file}, ${universal?.file} and ${browserClient.tree}/`, `the browser client, calling ${browserClient.entrypoint === "" ? "/" : browserClient.entrypoint}`));
177
+ }
138
178
  return 0;
139
179
  }
140
- const contracts = await checkContractsFile(projectDirectory, generated.contracts, compiled);
180
+ const contracts = await checkContractsFile(projectDirectory, generated.contracts, compiled, project.internal.prebuiltFileChanges);
141
181
  if (contracts.kind === "fresh") {
142
- output.stdout(`aventara-build: ${generated.resources}, ${generated.computedFields} and ${generated.contracts} are up to date.`);
182
+ const stale = browserClient === undefined
183
+ ? undefined
184
+ : checkBrowserClient(projectDirectory, browserClient);
185
+ if (stale !== undefined) {
186
+ output.stderr(refusal(stale));
187
+ return 1;
188
+ }
189
+ output.stdout(browserClient === undefined
190
+ ? `aventara-build: ${generated.resources}, ${generated.computedFields} and ${generated.contracts} are up to date.`
191
+ : `aventara-build: ${generated.resources}, ${generated.computedFields}, ${generated.contracts} and the browser client (${browserClient.files[0]?.file}, ${browserClient.files[1]?.file} and ${browserClient.tree}/) are up to date.`);
143
192
  return 0;
144
193
  }
145
194
  output.stderr(refusal({
@@ -147,18 +196,7 @@ export async function runBuild(argv, projectDirectory, output) {
147
196
  deploy: true,
148
197
  what: contracts.kind === "missing"
149
198
  ? "is missing"
150
- : `is stale: ${[
151
- ...(contracts.resources.length === 0
152
- ? []
153
- : [
154
- `the inputs of ${listed(contracts.resources)} changed since it was written`,
155
- ]),
156
- ...(contracts.shared
157
- ? [
158
- "an input that belongs to no single Resource, the core version or the file format changed",
159
- ]
160
- : []),
161
- ].join("; ")}`,
199
+ : `is stale: ${contracts.changes.join("; ")}`,
162
200
  }));
163
201
  return 1;
164
202
  }
@@ -0,0 +1,328 @@
1
+ import { readdirSync, rmSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { AvClientConfigError, AvGeneratedNameError, acceptClientContract, carriesGeneratedOwnership, clientEntryReExports, emitClientTree, GENERATED_DIRECTORY, HYDRATION_FORMAT, importSpecifier, resolveModuleStyle, sameOriginEntrypoint, } from "@aventara/client/internal";
4
+ import { AvBuildError } from "../build.error.js";
5
+ import { readGeneratedFile, writeGeneratedFile, } from "../project/generated-file.js";
6
+ export const REGENERATE = "`aventara-build --client` (the project's `aventara:prepare` script)";
7
+ const ENTRY_BANNER = [
8
+ "// biome-ignore-all format: generated output",
9
+ "// biome-ignore-all lint: generated output",
10
+ "/* !!! Generated by @aventara/build. Do not edit. !!! */",
11
+ `/* Regenerate with ${REGENERATE}. */`,
12
+ ].join("\n");
13
+ export const SERVER_CALL_REFUSAL = "avClient from @/aventara/client runs in the browser only, and this call ran on the server, where a Client Component renders first: on the server, call avClient from @/aventara/server, or from @/aventara/universal for data the browser then hydrates.";
14
+ const BROWSER_GUARD_SOURCE = `/** \`call\`, rejected instead of run when \`refusal()\` names why. */
15
+ function guarded(call: (...args: unknown[]) => unknown): (...args: unknown[]) => unknown {
16
+ return (...args) => {
17
+ const refused = refusal();
18
+ return refused === undefined ? call(...args) : Promise.reject(new Error(refused));
19
+ };
20
+ }
21
+ `;
22
+ const UNIVERSAL_GUARD_SOURCE = `/**
23
+ * Next.js's \`io()\`: while \`next build\` prerenders a page it waits, so a call
24
+ * there runs at request time, as any database read does; it resolves at once
25
+ * everywhere else. Next.js refuses it in an \`after()\` callback a page
26
+ * scheduled, where the render is over, and the call then runs at once.
27
+ */
28
+ function ioFirst(): Promise<void> {
29
+ try {
30
+ return io();
31
+ } catch {
32
+ return Promise.resolve();
33
+ }
34
+ }
35
+
36
+ /** \`call\`, once \`io()\` lets it run, rejected instead of run when \`refusal()\` names why. */
37
+ function guarded(call: (...args: unknown[]) => unknown): (...args: unknown[]) => unknown {
38
+ return (...args) =>
39
+ ioFirst().then(() => {
40
+ const refused = refusal();
41
+ return refused === undefined ? call(...args) : Promise.reject(new Error(refused));
42
+ });
43
+ }
44
+ `;
45
+ const GUARD_SOURCE = `/** A frozen copy of one Resource's families, or one family's variants, each call guarded. */
46
+ function guardedTree(tree: object): object {
47
+ const copy: Record<string, unknown> = Object.create(Object.getPrototypeOf(tree) as object | null);
48
+ for (const [key, value] of Object.entries(tree)) {
49
+ copy[key] = typeof value === "function" ? guarded(value as (...args: unknown[]) => unknown) : guardedTree(value as object);
50
+ }
51
+ return Object.freeze(copy);
52
+ }
53
+
54
+ /**
55
+ * The client with every call that sends a request guarded; \`tx\` builds
56
+ * deferred operations and sends nothing, so it is kept as it is.
57
+ */
58
+ function guardedClient(client: AvClient): AvClient {
59
+ const copy: AvClient = Object.create(AvClient.prototype) as AvClient;
60
+ for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(client))) {
61
+ const value: unknown = descriptor.value;
62
+ Object.defineProperty(copy, key, {
63
+ ...descriptor,
64
+ value:
65
+ key === "tx"
66
+ ? value
67
+ : typeof value === "function"
68
+ ? guarded(value as (...args: unknown[]) => unknown)
69
+ : guardedTree(value as object),
70
+ });
71
+ }
72
+ return copy;
73
+ }
74
+ `;
75
+ function entrySource(style) {
76
+ const from = (module) => JSON.stringify(importSpecifier(module, style));
77
+ const { tagKey, valueKey, objectTag, scalarTags } = HYDRATION_FORMAT;
78
+ const tagged = [...scalarTags, objectTag]
79
+ .map((tag) => JSON.stringify(tag))
80
+ .join(" | ");
81
+ return `${ENTRY_BANNER}
82
+
83
+ import { AvClient } from ${from(`./${GENERATED_DIRECTORY}/client`)};
84
+ import { type AvBuiltInScalar, decodeScalar } from ${from(`./${GENERATED_DIRECTORY}/runtime/codec`)};
85
+
86
+ ${clientEntryReExports(style, "type")}
87
+ /** What a call made on the server is refused with. */
88
+ const SERVER_CALL = ${JSON.stringify(SERVER_CALL_REFUSAL)};
89
+
90
+ /**
91
+ * Whether this code runs where the client's entrypoint, a path on the page's own
92
+ * origin, can be resolved: a page or a worker has a location, a server has none.
93
+ */
94
+ function onServer(): boolean {
95
+ return (globalThis as { readonly location?: unknown }).location === undefined;
96
+ }
97
+
98
+ /** Why a call made here and now is refused; \`undefined\` when it may run. */
99
+ function refusal(): string | undefined {
100
+ return onServer() ? SERVER_CALL : undefined;
101
+ }
102
+
103
+ ${BROWSER_GUARD_SOURCE}
104
+ ${GUARD_SOURCE}
105
+ /**
106
+ * This app's client, calling its own origin's API from the browser. A call made
107
+ * on the server (while a Client Component renders there) rejects instead: on the
108
+ * server, import \`avClient\` from \`@/aventara/server\` or \`@/aventara/universal\`.
109
+ */
110
+ export const avClient: AvClient = guardedClient(new AvClient());
111
+
112
+ /** The key a dehydrated value's tag is under: \`{ ${JSON.stringify(tagKey)}: <scalar>, ${JSON.stringify(valueKey)}: <its wire form> }\`. */
113
+ const TAG = ${JSON.stringify(tagKey)};
114
+
115
+ /** The key a tagged value's wire form is under. */
116
+ const VALUE = ${JSON.stringify(valueKey)};
117
+
118
+ /** The tag of a plain object holding a ${JSON.stringify(tagKey)} key of its own, kept as data. */
119
+ const OBJECT_TAG = ${JSON.stringify(objectTag)};
120
+
121
+ /** The scalars a value may be tagged as. */
122
+ const TAGGED_SCALARS: ReadonlySet<string> = new Set<AvBuiltInScalar>([${scalarTags.map((tag) => JSON.stringify(tag)).join(", ")}]);
123
+
124
+ function isPlainObject(value: unknown): value is Readonly<Record<string, unknown>> {
125
+ if (typeof value !== "object" || value === null) {
126
+ return false;
127
+ }
128
+ const prototype: unknown = Object.getPrototypeOf(value);
129
+ return prototype === Object.prototype || prototype === null;
130
+ }
131
+
132
+ function revivedMembers(record: Readonly<Record<string, unknown>>): Record<string, unknown> {
133
+ return Object.fromEntries(Object.entries(record).map(([key, member]) => [key, avHydrate(member)]));
134
+ }
135
+
136
+ /**
137
+ * Revives data that \`avDehydrate\` (from \`@aventara/next\`) made safe to pass
138
+ * from the server to the browser: each tagged Date, bigint, Uint8Array and
139
+ * decimal becomes the value this client reads, a decimal this client's own
140
+ * AvDecimal.
141
+ * Arrays and plain objects are copied with their members revived; any other
142
+ * value is returned as it is. Give it to TanStack Query as \`deserializeData\`,
143
+ * or call it on an SWR \`fallback\`.
144
+ *
145
+ * @throws TypeError when a tagged value is not one avDehydrate writes.
146
+ */
147
+ export function avHydrate(dehydrated: unknown): unknown {
148
+ if (Array.isArray(dehydrated)) {
149
+ return dehydrated.map((member: unknown) => avHydrate(member));
150
+ }
151
+ if (!isPlainObject(dehydrated)) {
152
+ return dehydrated;
153
+ }
154
+ if (!Object.hasOwn(dehydrated, TAG)) {
155
+ return revivedMembers(dehydrated);
156
+ }
157
+ const tag = dehydrated[TAG];
158
+ const wire = dehydrated[VALUE];
159
+ if (Object.keys(dehydrated).length === 2 && Object.hasOwn(dehydrated, VALUE)) {
160
+ if (tag === OBJECT_TAG && isPlainObject(wire)) {
161
+ return revivedMembers(wire);
162
+ }
163
+ if (typeof tag === "string" && TAGGED_SCALARS.has(tag)) {
164
+ return decodeScalar(tag as AvBuiltInScalar, wire);
165
+ }
166
+ }
167
+ throw new TypeError(
168
+ ${JSON.stringify(`A value tagged ${JSON.stringify(tagKey)} must be { ${JSON.stringify(tagKey)}: ${tagged}, ${JSON.stringify(valueKey)}: its value }, as avDehydrate writes it.`)},
169
+ );
170
+ }
171
+ `;
172
+ }
173
+ export const UNIVERSAL_PATH_KEY = "aventara.next.universal";
174
+ export const UNREGISTERED_REFUSAL = 'avClient from @/aventara/universal ran on the server, where it calls this app in process through what ready() registers, and nothing is registered: export register() from src/instrumentation.ts and, when process.env.NEXT_RUNTIME is "nodejs", await ready() from @/aventara/server there.';
175
+ function universalSource(style) {
176
+ const from = (module) => JSON.stringify(importSpecifier(module, style));
177
+ return `${ENTRY_BANNER}
178
+
179
+ import { io } from ${JSON.stringify(style.importFileExtension === "" ? "next/cache" : "next/cache.js")};
180
+ import { AvClient } from ${from(`./${GENERATED_DIRECTORY}/client`)};
181
+ import type { AvFetch } from ${from(`./${GENERATED_DIRECTORY}/runtime/transport`)};
182
+
183
+ /** The key the server's in-process path is registered under, on globalThis, by ready() from @aventara/next. */
184
+ const SERVER_PATH = Symbol.for(${JSON.stringify(UNIVERSAL_PATH_KEY)});
185
+
186
+ /** What a call made on a server where nothing is registered is refused with. */
187
+ const UNREGISTERED = ${JSON.stringify(UNREGISTERED_REFUSAL)};
188
+
189
+ /** Whether this code runs on the server: a page or a worker has a location, a server has none. */
190
+ function onServer(): boolean {
191
+ return (globalThis as { readonly location?: unknown }).location === undefined;
192
+ }
193
+
194
+ /** The in-process path ready() registered on this server, or undefined. */
195
+ function serverPath(): AvFetch | undefined {
196
+ const registered = (globalThis as { readonly [key: symbol]: unknown })[SERVER_PATH];
197
+ return typeof registered === "function" ? (registered as AvFetch) : undefined;
198
+ }
199
+
200
+ /** Why a call made here and now is refused; \`undefined\` when it may run. */
201
+ function refusal(): string | undefined {
202
+ return onServer() && serverPath() === undefined ? UNREGISTERED : undefined;
203
+ }
204
+
205
+ /** In the browser, the platform's fetch; on the server, the in-process path. */
206
+ function universalFetch(...request: unknown[]): unknown {
207
+ const reach = onServer() ? serverPath() : (globalThis as { readonly fetch?: AvFetch }).fetch;
208
+ if (reach === undefined) {
209
+ throw new TypeError(onServer() ? UNREGISTERED : "No fetch is available here.");
210
+ }
211
+ return (reach as (...request: unknown[]) => unknown)(...request);
212
+ }
213
+
214
+ ${UNIVERSAL_GUARD_SOURCE}
215
+ ${GUARD_SOURCE}
216
+ /**
217
+ * This app's client, on the server and in the browser alike: in the browser it
218
+ * calls the app's own origin's API; on the server it calls the app in process,
219
+ * with the request's state, through what \`ready()\` registered at startup
220
+ * (\`src/instrumentation.ts\`). Its values are the same on both sides, so data
221
+ * prefetched on the server hydrates in the browser: pass it through
222
+ * \`avDehydrate\` (\`@aventara/next\`) and \`avHydrate\` (\`@/aventara/client\`).
223
+ */
224
+ export const avClient: AvClient = guardedClient(new AvClient({ fetch: universalFetch as AvFetch }));
225
+ `;
226
+ }
227
+ function refusedAsBuild(step) {
228
+ try {
229
+ return step();
230
+ }
231
+ catch (error) {
232
+ if (error instanceof AvClientConfigError ||
233
+ error instanceof AvGeneratedNameError) {
234
+ throw new AvBuildError(`the browser client cannot be written: ${error.message}`, { cause: error });
235
+ }
236
+ throw error;
237
+ }
238
+ }
239
+ export async function emitBrowserClient(projectDirectory, layout, source) {
240
+ const acceptance = await acceptClientContract(JSON.parse(source.canonicalText(source.client)));
241
+ if (!acceptance.accepted) {
242
+ throw new AvBuildError(`the ClientContract this build compiled was refused by the client generator: ${acceptance.message}`);
243
+ }
244
+ const folder = path.join(projectDirectory, layout.folder);
245
+ const style = refusedAsBuild(() => resolveModuleStyle({
246
+ generateAt: folder,
247
+ entryFile: path.join(folder, layout.entry),
248
+ }));
249
+ const entrypoint = refusedAsBuild(() => sameOriginEntrypoint(source.api));
250
+ const emission = refusedAsBuild(() => emitClientTree(acceptance.contract, entrypoint, {
251
+ importFileExtension: style.importFileExtension,
252
+ moduleFormat: style.moduleFormat,
253
+ }, REGENERATE));
254
+ const decoder = new TextDecoder();
255
+ const tree = emission.tree
256
+ .filter((file) => file.path.startsWith(`${GENERATED_DIRECTORY}/`))
257
+ .map((file) => ({
258
+ file: `${layout.folder}/${file.path}`,
259
+ text: decoder.decode(file.bytes),
260
+ }));
261
+ return {
262
+ files: [
263
+ { file: `${layout.folder}/${layout.entry}`, text: entrySource(style) },
264
+ {
265
+ file: `${layout.folder}/${layout.universal}`,
266
+ text: universalSource(style),
267
+ },
268
+ ...tree,
269
+ ],
270
+ tree: `${layout.folder}/${GENERATED_DIRECTORY}`,
271
+ entrypoint: entrypoint.path,
272
+ warnings: emission.warnings,
273
+ };
274
+ }
275
+ function filesUnder(projectDirectory, directory) {
276
+ const root = path.join(projectDirectory, directory);
277
+ try {
278
+ return readdirSync(root, { recursive: true, withFileTypes: true })
279
+ .filter((entry) => entry.isFile())
280
+ .map((entry) => `${directory}/${path
281
+ .relative(root, path.join(entry.parentPath, entry.name))
282
+ .split(path.sep)
283
+ .join("/")}`)
284
+ .sort();
285
+ }
286
+ catch {
287
+ return [];
288
+ }
289
+ }
290
+ function leftovers(projectDirectory, client) {
291
+ const written = new Set(client.files.map(({ file }) => file));
292
+ return filesUnder(projectDirectory, client.tree).filter((file) => !written.has(file) &&
293
+ carriesGeneratedOwnership(readGeneratedFile(projectDirectory, file) ?? ""));
294
+ }
295
+ export function writeBrowserClient(projectDirectory, client) {
296
+ let write = "unchanged";
297
+ for (const { file, text } of client.files) {
298
+ if (writeGeneratedFile(projectDirectory, file, text) === "written") {
299
+ write = "written";
300
+ }
301
+ }
302
+ const removed = leftovers(projectDirectory, client);
303
+ for (const file of removed) {
304
+ rmSync(path.join(projectDirectory, file));
305
+ }
306
+ return { write: removed.length > 0 ? "written" : write, removed };
307
+ }
308
+ export function checkBrowserClient(projectDirectory, client) {
309
+ for (const { file, text } of client.files) {
310
+ const current = readGeneratedFile(projectDirectory, file);
311
+ if (current === undefined) {
312
+ return { file, what: "is missing" };
313
+ }
314
+ if (current !== text) {
315
+ return {
316
+ file,
317
+ what: "is stale: the ClientContract or the project's module settings changed since it was written",
318
+ };
319
+ }
320
+ }
321
+ const [left] = leftovers(projectDirectory, client);
322
+ return left === undefined
323
+ ? undefined
324
+ : {
325
+ file: left,
326
+ what: "is left from an earlier ClientContract's browser client",
327
+ };
328
+ }
@@ -5,7 +5,7 @@ import { readGeneratedFile, writeGeneratedFile, } from "../project/generated-fil
5
5
  export function writeContractsFile(projectDirectory, contractsFile, compiled) {
6
6
  return writeGeneratedFile(projectDirectory, contractsFile, compiled.source);
7
7
  }
8
- export async function checkContractsFile(projectDirectory, contractsFile, compiled) {
8
+ export async function checkContractsFile(projectDirectory, contractsFile, compiled, changesOf) {
9
9
  const file = path.join(projectDirectory, contractsFile);
10
10
  const text = readGeneratedFile(projectDirectory, contractsFile);
11
11
  if (text === undefined) {
@@ -15,9 +15,21 @@ export async function checkContractsFile(projectDirectory, contractsFile, compil
15
15
  return { kind: "fresh" };
16
16
  }
17
17
  const previous = await previousContracts(projectDirectory, file);
18
- return previous === undefined
19
- ? { kind: "stale", resources: [], shared: true }
20
- : difference(previous, compiled.prebuilt);
18
+ if (previous === undefined) {
19
+ return {
20
+ kind: "stale",
21
+ changes: ["it could not be read as a prebuilt Contracts file"],
22
+ };
23
+ }
24
+ const changes = changesOf(previous, compiled.prebuilt);
25
+ return {
26
+ kind: "stale",
27
+ changes: changes.length > 0
28
+ ? changes
29
+ : [
30
+ "its text is not what this build writes, though it records the same core version, file format and inputs",
31
+ ],
32
+ };
21
33
  }
22
34
  async function previousContracts(projectDirectory, file) {
23
35
  const jiti = createJiti(pathToFileURL(path.join(projectDirectory, "package.json")).href, { moduleCache: false, fsCache: false, interopDefault: false });
@@ -31,21 +43,3 @@ async function previousContracts(projectDirectory, file) {
31
43
  return undefined;
32
44
  }
33
45
  }
34
- function difference(previous, current) {
35
- const before = previous.fingerprints.resources;
36
- const after = current.fingerprints.resources;
37
- const resources = [
38
- ...new Set([...Object.keys(before), ...Object.keys(after)]),
39
- ]
40
- .filter((name) => before[name] !== after[name])
41
- .sort();
42
- const shared = previous.fingerprints.shared !== current.fingerprints.shared ||
43
- previous.core !== current.core ||
44
- previous.protocol !== current.protocol ||
45
- previous.format !== current.format;
46
- return {
47
- kind: "stale",
48
- resources,
49
- shared: shared || resources.length === 0,
50
- };
51
- }
@@ -31,12 +31,14 @@ function diagnosticLines(diagnostics) {
31
31
  export async function compilePrebuiltContracts(project) {
32
32
  const { core, internal, config, description } = project;
33
33
  let contracts;
34
+ let api;
34
35
  try {
35
36
  const framework = await core.createFramework({
36
37
  ...config,
37
38
  adapter: buildTimeAdapter(description),
38
39
  });
39
40
  contracts = framework.contracts;
41
+ api = framework.api;
40
42
  }
41
43
  catch (error) {
42
44
  if (error instanceof core.AvConstructionError) {
@@ -45,5 +47,10 @@ export async function compilePrebuiltContracts(project) {
45
47
  throw error;
46
48
  }
47
49
  const prebuilt = await internal.prebuiltContractsOf(description.model, config, contracts);
48
- return { prebuilt, source: internal.emitPrebuiltContracts(prebuilt) };
50
+ return {
51
+ prebuilt,
52
+ source: internal.emitPrebuiltContracts(prebuilt),
53
+ client: contracts.client,
54
+ api,
55
+ };
49
56
  }
@@ -28,12 +28,15 @@ function treeSpecifier(source) {
28
28
  }
29
29
  return undefined;
30
30
  }
31
- export function resolveGeneratedFiles(projectDirectory, configFile) {
31
+ export function resolveGeneratedFiles(projectDirectory, configFile, defaultDirectory = DEFAULT_DIRECTORY) {
32
+ const fallback = defaultDirectory === DEFAULT_DIRECTORY
33
+ ? DEFAULT_GENERATED_FILES
34
+ : filesIn(defaultDirectory);
32
35
  const configPath = path.join(projectDirectory, configFile);
33
36
  const source = ts.createSourceFile(configPath, readFileSync(configPath, "utf8"), ts.ScriptTarget.Latest, false);
34
37
  const specifier = treeSpecifier(source);
35
38
  if (specifier === undefined) {
36
- return DEFAULT_GENERATED_FILES;
39
+ return fallback;
37
40
  }
38
41
  const module = path.resolve(path.dirname(configPath), specifier);
39
42
  const directory = path.relative(projectDirectory, path.dirname(module));
@@ -44,5 +47,5 @@ export function resolveGeneratedFiles(projectDirectory, configFile) {
44
47
  return inside &&
45
48
  path.basename(module).replace(/\.[cm]?[jt]s$/, "") === TREE_MODULE
46
49
  ? filesIn(directory.split(path.sep).join("/"))
47
- : DEFAULT_GENERATED_FILES;
50
+ : fallback;
48
51
  }
@@ -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
+ ];
@@ -0,0 +1,59 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { AvBuildError } from "../build.error.js";
4
+ import { HOST_CATALOG } from "./host.catalog.generated.js";
5
+ const FALLBACK_HOST = "nest";
6
+ function clientLayoutOf(entry) {
7
+ const { client, universal } = entry.layout;
8
+ if (client === undefined && universal === undefined) {
9
+ return undefined;
10
+ }
11
+ if (client === undefined ||
12
+ universal === undefined ||
13
+ path.posix.dirname(client) !== path.posix.dirname(universal)) {
14
+ throw new AvBuildError(`${entry.package} declares its browser client at ${client ?? "nowhere"} and its universal entry at ${universal ?? "nowhere"}; the build writes both into one folder, so a host declares both, side by side.`);
15
+ }
16
+ return {
17
+ folder: path.posix.dirname(client),
18
+ entry: path.posix.basename(client),
19
+ universal: path.posix.basename(universal),
20
+ };
21
+ }
22
+ function layoutOf(entry) {
23
+ const client = clientLayoutOf(entry);
24
+ return {
25
+ host: entry.alias,
26
+ described: entry.alias === FALLBACK_HOST
27
+ ? `a ${entry.label} project`
28
+ : `a ${entry.label} project (it depends on ${entry.package})`,
29
+ config: entry.layout.config,
30
+ adapter: entry.layout.adapter,
31
+ generated: entry.layout.generated,
32
+ ...(client === undefined ? {} : { client }),
33
+ };
34
+ }
35
+ export const PROJECT_LAYOUTS = Object.fromEntries(HOST_CATALOG.map((entry) => [entry.alias, layoutOf(entry)]));
36
+ const DEPENDENCY_LISTS = ["dependencies", "devDependencies"];
37
+ function hostsOf(projectDirectory) {
38
+ let manifest;
39
+ try {
40
+ manifest = JSON.parse(readFileSync(path.join(projectDirectory, "package.json"), "utf8"));
41
+ }
42
+ catch {
43
+ return [];
44
+ }
45
+ const lists = DEPENDENCY_LISTS.map((list) => typeof manifest === "object" && manifest !== null
46
+ ? manifest[list]
47
+ : undefined);
48
+ return HOST_CATALOG.filter((entry) => lists.some((list) => typeof list === "object" &&
49
+ list !== null &&
50
+ Object.hasOwn(list, entry.package)));
51
+ }
52
+ export function resolveProjectLayout(projectDirectory) {
53
+ const hosts = hostsOf(projectDirectory);
54
+ if (hosts.length > 1) {
55
+ throw new AvBuildError(`${projectDirectory}/package.json depends on both ${hosts.map((host) => host.package).join(" and ")}, so it is not clear where the configuration and the adapter are. A project hosts its framework in one of them; remove the other.`);
56
+ }
57
+ const [host] = hosts;
58
+ return PROJECT_LAYOUTS[host === undefined ? FALLBACK_HOST : host.alias];
59
+ }
@@ -5,18 +5,14 @@ import { createJiti } from "jiti";
5
5
  import { AvBuildError } from "../build.error.js";
6
6
  const EXTENSIONS = [".js", ".ts", ".mjs", ".cjs", ".mts", ".cts"];
7
7
  const EXTENSIONS_AS_LISTED = [".ts", ".mts", ".cts", ".js", ".mjs", ".cjs"];
8
- export const PROJECT_MODULES = {
9
- config: "src/aventara.config",
10
- adapter: "src/aventara.adapter",
11
- };
12
8
  const CONFIG_EXPORTS = ["aventaraConfig", "default"];
13
- function projectFile(projectDirectory, module) {
9
+ function projectFile(projectDirectory, module, layout) {
14
10
  const present = EXTENSIONS.map((extension) => `${module}${extension}`).filter((file) => statSync(path.join(projectDirectory, file), {
15
11
  throwIfNoEntry: false,
16
12
  })?.isFile() === true);
17
13
  if (present.length === 0) {
18
14
  const [first, ...others] = EXTENSIONS_AS_LISTED;
19
- throw new AvBuildError(`no ${module}${first}, ${others.slice(0, -1).join(", ")} or ${others.at(-1)} in ${projectDirectory}. Run aventara-build from the project's root (the aventara:prepare script does).`);
15
+ throw new AvBuildError(`no ${module}${first}, ${others.slice(0, -1).join(", ")} or ${others.at(-1)} in ${projectDirectory}, where ${layout.described} keeps it. Run aventara-build from the project's root (the aventara:prepare script does).`);
20
16
  }
21
17
  if (present.length > 1) {
22
18
  throw new AvBuildError(`${present.slice(0, -1).join(", ")} and ${present.at(-1)} are all ${module}; keep one and delete the other${present.length > 2 ? "s" : ""}.`);
@@ -99,10 +95,10 @@ async function projectCore(jiti) {
99
95
  throw new AvBuildError(`@aventara/core could not be loaded from the project (${error instanceof Error ? error.message.split("\n", 1)[0] : String(error)}); install it beside @aventara/build.`, { cause: error });
100
96
  }
101
97
  }
102
- export function findProjectModules(projectDirectory) {
98
+ export function findProjectModules(projectDirectory, layout) {
103
99
  return {
104
- configFile: projectFile(projectDirectory, PROJECT_MODULES.config),
105
- adapterFile: projectFile(projectDirectory, PROJECT_MODULES.adapter),
100
+ configFile: projectFile(projectDirectory, layout.config, layout),
101
+ adapterFile: projectFile(projectDirectory, layout.adapter, layout),
106
102
  };
107
103
  }
108
104
  export async function loadProjectModules(projectDirectory, { configFile, adapterFile }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aventara/build",
3
- "version": "1.0.0-rc.4",
3
+ "version": "1.0.0-rc.9",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "Compiles an Aventara application's Contracts at build time, for serverless and edge cold starts.",
6
6
  "type": "module",
@@ -16,21 +16,23 @@
16
16
  "CHANGELOG.md"
17
17
  ],
18
18
  "dependencies": {
19
+ "@aventara/client": "1.0.0-rc.9",
19
20
  "jiti": "2.7.0"
20
21
  },
21
22
  "peerDependencies": {
22
- "@aventara/core": "1.0.0-rc.4",
23
+ "@aventara/core": "1.0.0-rc.9",
23
24
  "typescript": ">=5.7.0 <7"
24
25
  },
25
26
  "publishConfig": {
26
27
  "access": "public"
27
28
  },
28
29
  "devDependencies": {
29
- "@aventara/core": "1.0.0-rc.4",
30
+ "@aventara/core": "1.0.0-rc.9",
30
31
  "@types/node": "24.10.1",
31
32
  "typescript": "^5.9.2"
32
33
  },
33
34
  "scripts": {
35
+ "prebuild": "node ../../scripts/build/host-catalog.generator.ts --out src/project/host.catalog.generated.ts",
34
36
  "build": "node ../../scripts/build/build-output.cleaner.ts dist && tsc -p tsconfig.json --removeComments",
35
37
  "typecheck": "tsc -p tsconfig.typecheck.json --noEmit",
36
38
  "test": "vitest run --typecheck --config ../../vitest.config.mts --root .",