@aventara/build 1.0.0-rc.13 → 1.0.0-rc.15

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,51 @@
1
1
  # @aventara/build
2
2
 
3
+ ## 1.0.0-rc.15
4
+
5
+ ### Patch Changes
6
+
7
+ - aab6d72: The client `avclient generate` writes now passes a default `biome check` as written, on Biome 2.4.0 and later (Biome's default rules; an opt-in rule or assist such as `useSortedKeys` is not covered): every generated file opens with `biome-ignore-all` lines for formatting, lint and import order (`organizeImports`), core's copied declarations have a blank line after their imports, the copied field-check evaluator carries no line suppression that the file-wide one makes unused, and each file ends without a final line break, which Biome 2.4.0 to 2.4.10 reported on every file under `biome-ignore-all format`. `aventara-build`'s own files pass too, in NestJS and Next.js projects: `resources.generated.ts`, `computed-fields.generated.ts` and `contracts.generated.ts` now open with the same suppressions and end without a final line break, and `--client`'s `src/aventara/client.ts` and `src/aventara/universal.ts` are written the same way. The discovery artifact `aventara-prisma7-generate` writes passes too. A file an earlier version wrote is reported stale by `aventara-build --check` and replaced by the next run. A client `avclient generate` wrote before is replaced on its next run. The client skill now says the generated files need no exclusion under Biome's default rules, and how to skip them in another linter or formatter.
8
+ - bf98da7: A pipeline stage's types now know the root's field definitions. `aventara-build` writes `src/generated/aventara/config-facts.generated.ts` after it compiles the Contracts, and the discovery artifact puts it on the model's type:
9
+
10
+ - A Json field the root shapes reads as its shape in a stage's `ctx` calls, in `tx` and in compute's read-only `ctx`: `ctx.Product.find.many({ select: ["name"] })` types `name` as `{ ar: string; en: string }`. A read gives the shape's own type, the same as `framework.application`'s.
11
+ - A `VIRTUAL` field the root adds is in a variant's typed `ctx.$args` (`ctx.$args.data.tenderedMinor`), and a `ctx` create must write a non-null added input it can create.
12
+ - A variant's hooks and interceptors are told resolved arguments: their `ctx.$args` holds no `$ref` (an `AvTransactionReference`), since references are resolved before they run. Guards, pipes and filters still may see one.
13
+ - A write takes a shaped Json value read-only, deep, on every surface (`framework.application`, `ctx`, the generated client too), so a frozen value, such as a stage's `ctx.$args`, fits as well as a mutable one.
14
+ - A compute stage on a shaped Json field returns the shape's type. A compute stage on an added field returns the type its generated fact states, read from the model instead of the configuration. Without the generated file, its return is not typed.
15
+
16
+ The server warns at startup (`AVENTARA_STALE_GENERATED_TYPES`) when the configuration's definitions differ from the ones the generated types were written with, naming each field. It is the same one warning as for stale computed fields: one stale build gives one warning, with a paragraph for each part that is stale, including a facts file it cannot read. `aventara-build --check` also fails on a stale file. Run `aventara:prepare` after you change a `definition`.
17
+
18
+ The discovery artifact's format is now 5: regenerate it with `aventara:prepare` (an artifact of format 4 is refused, as before). The Json Shape type emitter the client generator uses now lives in `@aventara/core`; the generated clients are unchanged. The CLI's agent skill says this in `SKILL.md` and `reference/stages.md`.
19
+
20
+ - `aventara-build` no longer logs `unchanged:` for `computed-fields.generated.ts` when it keeps that file because the configuration does not type-check: it logs `kept (configuration does not type-check): <file>`, after the type errors, since the kept file may be an earlier version's. The exit code is unchanged (0), so the `postinstall` that runs `aventara:prepare` still completes; fix the errors, then run `aventara:prepare` again.
21
+ - Updated dependencies [aab6d72]
22
+ - Updated dependencies [bf98da7]
23
+ - Updated dependencies [f8cd860]
24
+ - Updated dependencies [8a64af2]
25
+ - Updated dependencies [548881c]
26
+ - Updated dependencies [62fa91f]
27
+ - @aventara/client@1.0.0-rc.15
28
+ - @aventara/core@1.0.0-rc.15
29
+
30
+ ## 1.0.0-rc.14
31
+
32
+ ### Patch Changes
33
+
34
+ - 0895a93: `avDehydrate` and `avHydrate` are typed: data handed from a Server Component to a Client Component as props keeps its type, with no cast.
35
+
36
+ - `avDehydrate(data)` (`@aventara/next`) returns an `AvDehydrated<T>` for data of type `T`, and `avHydrate(value)` (`@/aventara/client`) of an `AvDehydrated<T>` returns the `T`, its `Date`, `bigint`, `Uint8Array` and `AvDecimal` values included. Type an SWR `fallback` prop `AvDehydrated<T>` and pass `avHydrate(fallback)` to `SWRConfig` as it is.
37
+ - `AvDehydrated<T>` is new, exported by `@aventara/next` and by the `src/aventara/client.ts` that `aventara-build --client` writes; the two are one type. It is opaque: a plain object, the data itself, or the dehydrated form of another type is a compile error where an `AvDehydrated<T>` is expected.
38
+ - A Resource or enum named `AvDehydrated` or `avHydrate` is now emitted under a renamed identifier (`AvDehydratedModel`, `avHydrateModel`), with a warning, as other names the generated client owns are. Before, `src/aventara/client.ts`'s own declaration hid a Resource type of that name, which could not be imported from it.
39
+ - Read data you dehydrate through `@/aventara/universal`: `@/aventara/server` types a decimal as core's `AvDecimal`, while `avHydrate` revives it as the browser client's.
40
+ - Pass `avHydrate(fallback)` to `SWRConfig`, never the dehydrated value itself: SWR's `fallback` is typed `any`, so the compiler cannot catch it.
41
+ - `avHydrate` still takes `unknown` and returns `unknown`; `avDehydrate` of an `unknown` value returns an `AvDehydrated<unknown>`. `serializeData: avDehydrate` and `deserializeData: avHydrate` in TanStack Query are written as before.
42
+ - The agent skills show the SWR props pattern typed, with no cast. The frontend skill's refresh line no longer names `npx`, and says a file you edited is kept with a warning naming it, and replaced with `--force`.
43
+
44
+ - Updated dependencies [1c5eaa3]
45
+ - Updated dependencies [0895a93]
46
+ - @aventara/core@1.0.0-rc.14
47
+ - @aventara/client@1.0.0-rc.14
48
+
3
49
  ## 1.0.0-rc.13
4
50
 
5
51
  ### Minor Changes
package/README.md CHANGED
@@ -17,7 +17,7 @@ aventara-prisma7-generate … && aventara-build
17
17
 
18
18
  ## What it reads and writes
19
19
 
20
- Run it from the project's root. It writes three files, in this order, each from what the one before wrote, so a fresh
20
+ Run it from the project's root. It writes four files, in this order, each from what the one before wrote, so a fresh
21
21
  install is right on its first `aventara:prepare`. They go to the directory your configuration imports
22
22
  `aventaraResources` from, beside the discovery artifact: `src/generated/aventara/` normally, `src/aventara/` when
23
23
  `aventara init` found Prisma's client generated into `src/generated` (which `prisma generate` would empty). A
@@ -27,6 +27,7 @@ configuration that imports no tree keeps `src/generated/aventara/`.
27
27
  |---|---|
28
28
  | `resources.generated.ts` | `aventaraResources`: every Resource your `src/resources/**/*.resource.ts` files define, one `resources` tree per layer |
29
29
  | `computed-fields.generated.ts` | `computedFields` (typed `AvComputedFields`): which fields the configuration's write compute fills, read off its type |
30
+ | `config-facts.generated.ts` | `configFacts`: the Json shapes and the fields the root's definitions add, read off the compiled Contracts; the discovery artifact puts them on the model's type, so a pipeline stage's `ctx` and `ctx.$args` know them |
30
31
  | `contracts.generated.ts` | `aventaraContracts` (typed `AvPrebuiltContracts`): the compiled Contracts |
31
32
 
32
33
  ### The Resource tree
@@ -135,7 +136,7 @@ so no running server is needed:
135
136
 
136
137
  | File | What it holds |
137
138
  |---|---|
138
- | `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 |
139
+ | `src/aventara/client.ts` | `avClient`, the typed client calling the app's own `api` (`/api`) from the browser, `avHydrate` and its `AvDehydrated<T>` type; and every name `avclient generate`'s `AvClient.ts` exports besides its default: `AvClient`, the Resource types, the enums, `AvDecimal` and the error classes |
139
140
  | `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 |
140
141
  | `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` |
141
142
 
@@ -189,7 +190,7 @@ aventara-build --check
189
190
  aventara-build --check --client
190
191
  ```
191
192
 
192
- 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
193
+ Writes nothing. It exits 0 when the four files on disk are the ones a build would write now, and 1 on the first that is
193
194
  missing or stale, naming it (and, for the Contracts, the Resources whose configuration or model changed). With
194
195
  `--client`, the browser client's files are checked too, after the Contracts: a missing or stale file, or one an earlier
195
196
  ClientContract's client left, exits 1. Run it in CI before a deploy.
package/dist/cli.js CHANGED
@@ -6,6 +6,7 @@ import { readConfigComputedFields } from "./computed/computed-fields.reader.js";
6
6
  import { emitComputedFieldsModule } from "./computed/computed-fields-file.js";
7
7
  import { checkContractsFile } from "./contracts/contracts-file.js";
8
8
  import { compilePrebuiltContracts } from "./contracts/prebuilt-contracts.compiler.js";
9
+ import { emitConfigFactsModule, emitUnreadConfigFactsModule, } from "./facts/config-facts-file.js";
9
10
  import { acquireBuildLock } from "./project/build.lock.js";
10
11
  import { generatedFileBatch, readGeneratedFile, } from "./project/generated-file.js";
11
12
  import { DEFAULT_GENERATED_FILES, resolveGeneratedFiles, } from "./project/generated-files.resolver.js";
@@ -18,7 +19,7 @@ import { emitResourcesModule } from "./resources/resources-file.js";
18
19
  const USAGE = `aventara-build — compile this project's Aventara Contracts ahead of time.
19
20
 
20
21
  Usage:
21
- aventara-build write the three files below
22
+ aventara-build write the four files below
22
23
  aventara-build --client also write a Next.js app's browser client
23
24
  aventara-build --check [--client] write nothing; exit 1 when one is missing or stale
24
25
 
@@ -29,10 +30,13 @@ Run it from the project's root. It writes, in this order:
29
30
  ${RESOURCES_GLOB} export, one \`resources\` tree per layer;
30
31
  ${DEFAULT_GENERATED_FILES.computedFields}
31
32
  which fields the configuration computes, read off its type;
33
+ ${DEFAULT_GENERATED_FILES.configFacts}
34
+ the Json shapes and the fields the configuration's definitions add,
35
+ read off the compiled Contracts, which pipeline stages' types read;
32
36
  ${DEFAULT_GENERATED_FILES.contracts}
33
37
  the compiled Contracts, \`aventaraContracts\`.
34
38
 
35
- All three go to the directory the configuration imports \`aventaraResources\`
39
+ All four go to the directory the configuration imports \`aventaraResources\`
36
40
  from (src/aventara/ when \`aventara init\` found the ORM's client generated into
37
41
  src/generated), else to ${PROJECT_LAYOUTS.nest.generated}/ (${PROJECT_LAYOUTS.next.generated}/
38
42
  in a Next.js project).
@@ -188,6 +192,14 @@ export async function runBuild(argv, projectDirectory, output) {
188
192
  else {
189
193
  noteWrite(wrote(batch.write(generated.resources, tree), generated.resources, counted(found.definitions.length)));
190
194
  }
195
+ const hadFacts = readGeneratedFile(projectDirectory, generated.configFacts) !== undefined;
196
+ if (check && !hadFacts) {
197
+ output.stderr(refusal({ file: generated.configFacts, what: "is missing" }));
198
+ return 1;
199
+ }
200
+ if (!check && !hadFacts) {
201
+ batch.write(generated.configFacts, emitUnreadConfigFactsModule(files.configFile, "the build has not compiled the Contracts yet"));
202
+ }
191
203
  const hadComputed = readGeneratedFile(projectDirectory, generated.computedFields) !==
192
204
  undefined;
193
205
  if (check && !hadComputed) {
@@ -218,7 +230,7 @@ export async function runBuild(argv, projectDirectory, output) {
218
230
  return 1;
219
231
  }
220
232
  if (kept) {
221
- noteWrite(wrote("unchanged", generated.computedFields));
233
+ noteWrite(`aventara-build: kept (configuration does not type-check): ${generated.computedFields}.`);
222
234
  }
223
235
  else if (check) {
224
236
  const stale = checked(projectDirectory, generated.computedFields, computed, "the fields the configuration computes changed since it was written");
@@ -236,6 +248,17 @@ export async function runBuild(argv, projectDirectory, output) {
236
248
  const project = await loadProjectModules(projectDirectory, files);
237
249
  const compiled = await compilePrebuiltContracts(project);
238
250
  const resources = Object.keys(compiled.prebuilt.fingerprints.resources).length;
251
+ const facts = emitConfigFactsModule(files.configFile, project.internal.configFactsOf(compiled.core), project.internal);
252
+ if (check) {
253
+ const stale = checked(projectDirectory, generated.configFacts, facts, "the Json shapes or the fields the configuration's definitions add changed since it was written");
254
+ if (stale !== undefined) {
255
+ output.stderr(refusal(stale));
256
+ return 1;
257
+ }
258
+ }
259
+ else {
260
+ noteWrite(wrote(batch.write(generated.configFacts, facts), generated.configFacts));
261
+ }
239
262
  const browserClient = clientLayout === undefined
240
263
  ? undefined
241
264
  : await emitBrowserClient(projectDirectory, clientLayout, {
@@ -268,8 +291,8 @@ export async function runBuild(argv, projectDirectory, output) {
268
291
  return 1;
269
292
  }
270
293
  note(browserClient === undefined
271
- ? `aventara-build: ${generated.resources}, ${generated.computedFields} and ${generated.contracts} are up to date.`
272
- : `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.`);
294
+ ? `aventara-build: ${generated.resources}, ${generated.computedFields}, ${generated.configFacts} and ${generated.contracts} are up to date.`
295
+ : `aventara-build: ${generated.resources}, ${generated.computedFields}, ${generated.configFacts}, ${generated.contracts} and the browser client (${browserClient.files[0]?.file}, ${browserClient.files[1]?.file} and ${browserClient.tree}/) are up to date.`);
273
296
  return 0;
274
297
  }
275
298
  output.stderr(refusal({
@@ -8,6 +8,7 @@ export const REGENERATE = "`aventara-build --client` (the project's `aventara:pr
8
8
  const ENTRY_BANNER = [
9
9
  "// biome-ignore-all format: generated output",
10
10
  "// biome-ignore-all lint: generated output",
11
+ "// biome-ignore-all assist/source/organizeImports: generated output",
11
12
  "/* !!! Generated by @aventara/build. Do not edit. !!! */",
12
13
  `/* Regenerate with ${REGENERATE}. */`,
13
14
  ].join("\n");
@@ -145,6 +146,9 @@ const HOOKS: Pick<AvClientOptions, "onCall" | "onSuccess" | "onError"> = {
145
146
  onError: (call, error) => clientOptions()?.onError?.(call, error),
146
147
  };
147
148
  `;
149
+ function withoutFinalLineBreak(text) {
150
+ return text.replace(/\n+$/, "");
151
+ }
148
152
  function optionsImport(options) {
149
153
  return options === undefined
150
154
  ? ""
@@ -152,15 +156,15 @@ function optionsImport(options) {
152
156
  }
153
157
  function entrySource(style, accessVerbs, options) {
154
158
  const from = (module) => JSON.stringify(importSpecifier(module, style));
155
- const { tagKey, valueKey, objectTag, scalarTags } = HYDRATION_FORMAT;
159
+ const { tagKey, valueKey, objectTag, scalarTags, typeKey } = HYDRATION_FORMAT;
156
160
  const tagged = [...scalarTags, objectTag]
157
161
  .map((tag) => JSON.stringify(tag))
158
162
  .join(" | ");
159
163
  return `${ENTRY_BANNER}
160
164
 
161
- import { AvClient${options === undefined ? "" : ", type AvClientOptions"} } from ${from(`./${GENERATED_DIRECTORY}/client`)};
165
+ ${optionsImport(options)}import { AvClient${options === undefined ? "" : ", type AvClientOptions"} } from ${from(`./${GENERATED_DIRECTORY}/client`)};
162
166
  import { type AvBuiltInScalar, decodeScalar } from ${from(`./${GENERATED_DIRECTORY}/runtime/codec`)};
163
- ${optionsImport(options)}
167
+
164
168
  ${clientEntryReExports(style, "type", accessVerbs)}
165
169
  /** What a call made on the server is refused with. */
166
170
  const SERVER_CALL = ${JSON.stringify(SERVER_CALL_REFUSAL)};
@@ -221,17 +225,35 @@ function revivedMembers(record: Readonly<Record<string, unknown>>): Record<strin
221
225
  return Object.fromEntries(Object.entries(record).map(([key, member]) => [key, avHydrate(member)]));
222
226
  }
223
227
 
228
+ /**
229
+ * Data of type \`T\` as \`avDehydrate\` (from \`@aventara/next\`) sends it from the
230
+ * server: opaque, read back as the \`T\` by \`avHydrate\`. Type a Client
231
+ * Component's prop with it to take server data with no cast. It is
232
+ * \`@aventara/next\`'s \`AvDehydrated\`, declared here again: its one member is a
233
+ * brand of the type only, never of the value.
234
+ */
235
+ export type AvDehydrated<T> = {
236
+ /** The type the value was dehydrated from: the type's brand, no member of the value. */
237
+ readonly ${JSON.stringify(typeKey)}: T;
238
+ };
239
+
224
240
  /**
225
241
  * Revives data that \`avDehydrate\` (from \`@aventara/next\`) made safe to pass
226
242
  * from the server to the browser: each tagged Date, bigint, Uint8Array and
227
243
  * decimal becomes the value this client reads, a decimal this client's own
228
244
  * AvDecimal.
229
245
  * Arrays and plain objects are copied with their members revived; any other
230
- * value is returned as it is. Give it to TanStack Query as \`deserializeData\`,
231
- * or call it on an SWR \`fallback\`.
246
+ * value is returned as it is. An \`AvDehydrated<T>\` revives as the \`T\`: call it
247
+ * on a prop of that type, such as an SWR \`fallback\`, and pass on what it
248
+ * returns, never the dehydrated value itself (SWR's \`fallback\` is typed
249
+ * \`any\`, so the type cannot catch it). Give it to TanStack Query as
250
+ * \`deserializeData\`.
232
251
  *
233
252
  * @throws TypeError when a tagged value is not one avDehydrate writes.
234
253
  */
254
+ export function avHydrate<T>(dehydrated: AvDehydrated<T>): T;
255
+ /** For a caller that cannot name the data's type: TanStack Query's \`deserializeData\`. */
256
+ export function avHydrate(dehydrated: unknown): unknown;
235
257
  export function avHydrate(dehydrated: unknown): unknown {
236
258
  if (Array.isArray(dehydrated)) {
237
259
  return dehydrated.map((member: unknown) => avHydrate(member));
@@ -266,10 +288,9 @@ function universalSource(style, accessVerbs, options) {
266
288
 
267
289
  import { io } from ${JSON.stringify(style.importFileExtension === "" ? "next/cache" : "next/cache.js")};
268
290
  import { cache } from "react";
269
- import { AvClient${options === undefined ? "" : ", type AvClientOptions"} } from ${from(`./${GENERATED_DIRECTORY}/client`)};
270
- import type { AvFetch } from ${from(`./${GENERATED_DIRECTORY}/runtime/transport`)};
271
- ${optionsImport(options)}
291
+ ${optionsImport(options)}import { AvClient${options === undefined ? "" : ", type AvClientOptions"} } from ${from(`./${GENERATED_DIRECTORY}/client`)};
272
292
  import { avLazy as lazyCall } from ${from(`./${GENERATED_DIRECTORY}/runtime/operation-set`)};
293
+ import type { AvFetch } from ${from(`./${GENERATED_DIRECTORY}/runtime/transport`)};
273
294
 
274
295
  /** The key the server's in-process path is registered under, on globalThis, by ready() from @aventara/next. */
275
296
  const SERVER_PATH = Symbol.for(${JSON.stringify(UNIVERSAL_PATH_KEY)});
@@ -456,11 +477,11 @@ export async function emitBrowserClient(projectDirectory, layout, source) {
456
477
  files: [
457
478
  {
458
479
  file: `${layout.folder}/${layout.entry}`,
459
- text: entrySource(style, accessVerbs, options),
480
+ text: withoutFinalLineBreak(entrySource(style, accessVerbs, options)),
460
481
  },
461
482
  {
462
483
  file: `${layout.folder}/${layout.universal}`,
463
- text: universalSource(style, accessVerbs, options),
484
+ text: withoutFinalLineBreak(universalSource(style, accessVerbs, options)),
464
485
  },
465
486
  ...tree,
466
487
  ],
@@ -9,7 +9,12 @@ function literal(computedFields) {
9
9
  return JSON.stringify(ordered, null, "\t");
10
10
  }
11
11
  export function emitComputedFieldsModule(configFile, reading) {
12
- const header = `// Generated by aventara-build from the type of ${configFile}. Do not edit: regenerate it (aventara:prepare).`;
12
+ const header = [
13
+ "// biome-ignore-all format: generated output",
14
+ "// biome-ignore-all lint: generated output",
15
+ "// biome-ignore-all assist/source/organizeImports: generated output",
16
+ `// Generated by aventara-build from the type of ${configFile}. Do not edit: regenerate it (aventara:prepare).`,
17
+ ].join("\n");
13
18
  const type = [
14
19
  "",
15
20
  "// What an adapter's discovery artifact carries on its model's type for the stages that run before compute.",
@@ -26,7 +31,7 @@ export function emitComputedFieldsModule(configFile, reading) {
26
31
  "// What an adapter's discovery artifact carries on its model's type for the stages that run before compute:",
27
32
  "// nothing, so they may leave any create field out.",
28
33
  "export type AvComputedFields = undefined;",
29
- ].join("\n")}\n`;
34
+ ].join("\n")}`;
30
35
  }
31
36
  return `${[
32
37
  header,
@@ -36,5 +41,5 @@ export function emitComputedFieldsModule(configFile, reading) {
36
41
  "// Resource. The framework compares it with the configuration at startup and warns when they differ.",
37
42
  `export const computedFields = ${literal(reading.computedFields)} as const satisfies AvConfigComputedFields;`,
38
43
  ...type,
39
- ].join("\n")}\n`;
44
+ ].join("\n")}`;
40
45
  }
@@ -51,6 +51,7 @@ export async function compilePrebuiltContracts(project) {
51
51
  prebuilt,
52
52
  source: internal.emitPrebuiltContracts(prebuilt),
53
53
  client: contracts.client,
54
+ core: contracts.core,
54
55
  api,
55
56
  };
56
57
  }
@@ -0,0 +1,50 @@
1
+ const key = (name) => JSON.stringify(name);
2
+ function shapeTypes(json, jsonSchemaType) {
3
+ const resources = Object.keys(json);
4
+ if (resources.length === 0) {
5
+ return "Record<never, never>";
6
+ }
7
+ const blocks = resources.map((resource) => {
8
+ const fields = json[resource] ?? {};
9
+ const members = Object.keys(fields).map((field) => `\t\t\treadonly ${key(field)}: ${jsonSchemaType(fields[field])};`);
10
+ return `\t\treadonly ${key(resource)}: {\n${members.join("\n")}\n\t\t};`;
11
+ });
12
+ return `{\n${blocks.join("\n")}\n\t}`;
13
+ }
14
+ const headerOf = (configFile) => [
15
+ "// biome-ignore-all format: generated output",
16
+ "// biome-ignore-all lint: generated output",
17
+ "// biome-ignore-all assist/source/organizeImports: generated output",
18
+ `// Generated by aventara-build from the Contracts compiled from ${configFile}. Do not edit: regenerate it (aventara:prepare).`,
19
+ ].join("\n");
20
+ export function emitUnreadConfigFactsModule(configFile, reason) {
21
+ return `${[
22
+ headerOf(configFile),
23
+ "",
24
+ `// Not read: ${reason.replaceAll("\n", " ")}. Pipeline stages see the model's own fields:`,
25
+ "// Json fields unshaped, no field the configuration adds.",
26
+ "export const configFacts = null;",
27
+ "",
28
+ "// What an adapter's discovery artifact carries on its model's type for pipeline stages' ctx and arguments:",
29
+ "// nothing.",
30
+ "export type AvConfigFacts = undefined;",
31
+ ].join("\n")}`;
32
+ }
33
+ export function emitConfigFactsModule(configFile, facts, writers) {
34
+ const literal = JSON.stringify(JSON.parse(writers.canonicalJson(facts)), null, "\t");
35
+ return `${[
36
+ headerOf(configFile),
37
+ "",
38
+ "// The Json shapes and added fields the root's definitions state: per Resource, each shaped field's JSON Schema",
39
+ "// and each added field's Contract field. The framework compares it with the configuration at startup and warns",
40
+ "// when they differ.",
41
+ `export const configFacts = ${literal} as const;`,
42
+ "",
43
+ "// What an adapter's discovery artifact carries on its model's type for pipeline stages' ctx and arguments:",
44
+ "// each added field, and each shaped Json field's value type.",
45
+ "export type AvConfigFacts = {",
46
+ '\treadonly added: (typeof configFacts)["added"];',
47
+ `\treadonly json: ${shapeTypes(facts.json, writers.jsonSchemaType)};`,
48
+ "};",
49
+ ].join("\n")}`;
50
+ }
@@ -8,6 +8,7 @@ function filesIn(directory) {
8
8
  return {
9
9
  resources: `${directory}/${TREE_MODULE}.ts`,
10
10
  computedFields: `${directory}/computed-fields.generated.ts`,
11
+ configFacts: `${directory}/config-facts.generated.ts`,
11
12
  contracts: `${directory}/contracts.generated.ts`,
12
13
  };
13
14
  }
@@ -18,13 +18,24 @@ function localNames(definitions) {
18
18
  export function emitResourcesModule(projectDirectory, resourcesFile, definitions, suffix) {
19
19
  const file = path.join(projectDirectory, resourcesFile);
20
20
  const names = localNames(definitions);
21
- const imports = definitions.map((definition, index) => {
21
+ const imports = definitions
22
+ .map((definition, index) => {
22
23
  const local = names[index];
23
24
  const imported = definition.exportName === local
24
25
  ? local
25
26
  : `${definition.exportName} as ${local}`;
26
- return `import { ${imported} } from ${JSON.stringify(relativeSpecifier(file, path.join(projectDirectory, definition.file), suffix))};`;
27
- });
27
+ const specifier = relativeSpecifier(file, path.join(projectDirectory, definition.file), suffix);
28
+ return {
29
+ specifier,
30
+ line: `import { ${imported} } from ${JSON.stringify(specifier)};`,
31
+ };
32
+ })
33
+ .sort((left, right) => left.specifier < right.specifier
34
+ ? -1
35
+ : left.specifier > right.specifier
36
+ ? 1
37
+ : 0)
38
+ .map(({ line }) => line);
28
39
  const layers = DEFINITION_LAYERS.flatMap((layer) => {
29
40
  const spreads = definitions.flatMap((definition, index) => definition.layers.includes(layer)
30
41
  ? [`...${names[index]}.${layer}`]
@@ -36,6 +47,9 @@ export function emitResourcesModule(projectDirectory, resourcesFile, definitions
36
47
  : [];
37
48
  });
38
49
  return `${[
50
+ "// biome-ignore-all format: generated output",
51
+ "// biome-ignore-all lint: generated output",
52
+ "// biome-ignore-all assist/source/organizeImports: generated output",
39
53
  `// Generated by aventara-build from ${RESOURCES_GLOB}. Do not edit: regenerate it (aventara:prepare).`,
40
54
  ...imports,
41
55
  "",
@@ -44,5 +58,5 @@ export function emitResourcesModule(projectDirectory, resourcesFile, definitions
44
58
  "export const aventaraResources = {",
45
59
  ...layers,
46
60
  "} as const;",
47
- ].join("\n")}\n`;
61
+ ].join("\n")}`;
48
62
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aventara/build",
3
- "version": "1.0.0-rc.13",
3
+ "version": "1.0.0-rc.15",
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,18 +16,18 @@
16
16
  "CHANGELOG.md"
17
17
  ],
18
18
  "dependencies": {
19
- "@aventara/client": "1.0.0-rc.13",
19
+ "@aventara/client": "1.0.0-rc.15",
20
20
  "jiti": "2.7.0"
21
21
  },
22
22
  "peerDependencies": {
23
- "@aventara/core": "1.0.0-rc.13",
23
+ "@aventara/core": "1.0.0-rc.15",
24
24
  "typescript": ">=5.7.0 <7"
25
25
  },
26
26
  "publishConfig": {
27
27
  "access": "public"
28
28
  },
29
29
  "devDependencies": {
30
- "@aventara/core": "1.0.0-rc.13",
30
+ "@aventara/core": "1.0.0-rc.15",
31
31
  "@types/node": "24.10.1",
32
32
  "typescript": "^5.9.2"
33
33
  },