@aventara/build 1.0.0-rc.14 → 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,32 @@
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
+
3
30
  ## 1.0.0-rc.14
4
31
 
5
32
  ### Patch 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
@@ -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
  ? ""
@@ -158,9 +162,9 @@ function entrySource(style, accessVerbs, options) {
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)};
@@ -284,10 +288,9 @@ function universalSource(style, accessVerbs, options) {
284
288
 
285
289
  import { io } from ${JSON.stringify(style.importFileExtension === "" ? "next/cache" : "next/cache.js")};
286
290
  import { cache } from "react";
287
- import { AvClient${options === undefined ? "" : ", type AvClientOptions"} } from ${from(`./${GENERATED_DIRECTORY}/client`)};
288
- import type { AvFetch } from ${from(`./${GENERATED_DIRECTORY}/runtime/transport`)};
289
- ${optionsImport(options)}
291
+ ${optionsImport(options)}import { AvClient${options === undefined ? "" : ", type AvClientOptions"} } from ${from(`./${GENERATED_DIRECTORY}/client`)};
290
292
  import { avLazy as lazyCall } from ${from(`./${GENERATED_DIRECTORY}/runtime/operation-set`)};
293
+ import type { AvFetch } from ${from(`./${GENERATED_DIRECTORY}/runtime/transport`)};
291
294
 
292
295
  /** The key the server's in-process path is registered under, on globalThis, by ready() from @aventara/next. */
293
296
  const SERVER_PATH = Symbol.for(${JSON.stringify(UNIVERSAL_PATH_KEY)});
@@ -474,11 +477,11 @@ export async function emitBrowserClient(projectDirectory, layout, source) {
474
477
  files: [
475
478
  {
476
479
  file: `${layout.folder}/${layout.entry}`,
477
- text: entrySource(style, accessVerbs, options),
480
+ text: withoutFinalLineBreak(entrySource(style, accessVerbs, options)),
478
481
  },
479
482
  {
480
483
  file: `${layout.folder}/${layout.universal}`,
481
- text: universalSource(style, accessVerbs, options),
484
+ text: withoutFinalLineBreak(universalSource(style, accessVerbs, options)),
482
485
  },
483
486
  ...tree,
484
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.14",
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.14",
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.14",
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.14",
30
+ "@aventara/core": "1.0.0-rc.15",
31
31
  "@types/node": "24.10.1",
32
32
  "typescript": "^5.9.2"
33
33
  },