@aventara/build 0.0.0-stage → 1.0.0-rc.4
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 +76 -0
- package/LICENSE +91 -0
- package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
- package/README.md +138 -2
- package/dist/aventara-build.bin.js +5 -0
- package/dist/build.error.js +3 -0
- package/dist/cli.js +184 -0
- package/dist/computed/computed-fields-file.js +37 -0
- package/dist/computed/computed-fields.reader.js +184 -0
- package/dist/contracts/contracts-file.js +51 -0
- package/dist/contracts/prebuilt-contracts.compiler.js +49 -0
- package/dist/node-version.guard.js +47 -0
- package/dist/project/generated-file.js +19 -0
- package/dist/project/generated-files.resolver.js +48 -0
- package/dist/project/project-modules.loader.js +119 -0
- package/dist/project/tsconfig.reader.js +73 -0
- package/dist/resources/resource-definitions.reader.js +111 -0
- package/dist/resources/resources-file.js +48 -0
- package/package.json +36 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# @aventara/build
|
|
2
|
+
|
|
3
|
+
## 1.0.0-rc.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 7c3b65f: Build-time Contracts for serverless and edge cold starts.
|
|
8
|
+
|
|
9
|
+
- New package `@aventara/build`, bin `aventara-build`, a development dependency. It runs `src/aventara.config.ts` and
|
|
10
|
+
`src/aventara.adapter.ts` without a database client, compiles the Contracts with the project's `@aventara/core` and
|
|
11
|
+
writes `src/generated/aventara/contracts.generated.ts` (`aventaraContracts`), which a serverless or edge entry passes
|
|
12
|
+
to `createFramework` as `prebuilt`. A configuration that startup would refuse fails the build with the same
|
|
13
|
+
`CONFIG_…`/`COMPILER_…` diagnostics. `aventara-build --check` writes nothing and exits 1 when the file is missing or
|
|
14
|
+
stale, naming the Resources that changed.
|
|
15
|
+
- `@aventara/prisma7-adapter`: `describePrismaAdapter({ discovery, provider, driver })` states the adapter's passive
|
|
16
|
+
facts (the model, the driver's transaction capability, the referential actions, the computed fields) with no client.
|
|
17
|
+
`createPrismaAdapter({ ...description, client })` builds the adapter from the same facts, and still checks the client
|
|
18
|
+
against the discovery artifact.
|
|
19
|
+
- `@aventara/cli`: the scaffold's `src/aventara.config.ts` exports the configuration as a plain value
|
|
20
|
+
(`export const aventaraConfig = { … } as const`) with no `adapter`. A new `src/aventara.adapter.ts` default-exports
|
|
21
|
+
`describePrismaAdapter(…)`, and `app.module.ts` joins the two:
|
|
22
|
+
`config: { ...aventaraConfig, adapter: await createPrismaAdapter({ ...adapter, client: prisma }) }`. The Nest server
|
|
23
|
+
still compiles its Contracts at startup and passes no `prebuilt`. `aventara:prepare` ends with `&& aventara-build`,
|
|
24
|
+
and `@aventara/build` is an exact development dependency.
|
|
25
|
+
|
|
26
|
+
- 2b1b17e: `aventara-build`'s files no longer land inside a Prisma client generated into `src/generated`.
|
|
27
|
+
|
|
28
|
+
When `aventara init` reuses a project whose Prisma client is generated into `src/generated`, it already puts the
|
|
29
|
+
discovery artifact in `src/aventara/`, because `prisma generate` deletes every file in its output directory that it did
|
|
30
|
+
not write. `aventara-build` still wrote its three files to `src/generated/aventara/`, so each `prisma generate` deleted
|
|
31
|
+
them and the project stopped compiling until the next `aventara:prepare`. Now all of Aventara's generated files are
|
|
32
|
+
in one place.
|
|
33
|
+
|
|
34
|
+
- `@aventara/build`: `aventara-build` writes `resources.generated.ts`, `computed-fields.generated.ts` and
|
|
35
|
+
`contracts.generated.ts` to the directory the configuration imports `aventaraResources` from, so the tree is always
|
|
36
|
+
written where the configuration reads it. A configuration that imports no tree keeps `src/generated/aventara/`.
|
|
37
|
+
The files of a project whose client is generated anywhere else are the same bytes, in the same place.
|
|
38
|
+
- `@aventara/prisma7-adapter`: with `--config`, the artifact imports the computed fields from its sibling,
|
|
39
|
+
`./computed-fields.generated`, wherever `--out` puts it. `--config` is still required for that import and the step
|
|
40
|
+
still checks that the file exists, but the step no longer uses its location. A scaffold's artifact is unchanged.
|
|
41
|
+
- `@aventara/cli`: in that project, `src/aventara.config.ts` imports the tree from `./aventara/resources.generated`,
|
|
42
|
+
and `.gitignore` gets the one `/src/aventara/` line that covers all four files (no separate
|
|
43
|
+
`/src/generated/aventara/contracts.generated.ts` line). The agent skill says where the generated files are in
|
|
44
|
+
both cases.
|
|
45
|
+
|
|
46
|
+
- 3813207: The Resource tree and the computed fields come from `aventara-build`.
|
|
47
|
+
|
|
48
|
+
- `@aventara/build`: before it compiles the Contracts, `aventara-build` writes two more files under
|
|
49
|
+
`src/generated/aventara/`. `resources.generated.ts` exports `aventaraResources`: every `defineResource(…)` result
|
|
50
|
+
that the files `src/resources/**/*.resource.ts` export, found by its type (never run), one `resources` tree per
|
|
51
|
+
layer, written as the plain spreads you wrote by hand (`root` always, `application`, `client` and `agent` when some
|
|
52
|
+
Resource has a block for them). Two definitions of one Resource stop the build, naming both. Imports are spelled as
|
|
53
|
+
the project spells its own. `computed-fields.generated.ts` exports `computedFields` and its type `AvComputedFields`:
|
|
54
|
+
which fields the configuration's write compute fills, read off its type after the tree exists, so a fresh install is
|
|
55
|
+
typed right on its first `aventara:prepare`. `--check` covers all three files. `typescript` (`>=5.7.0 <7`) is now a
|
|
56
|
+
peer dependency.
|
|
57
|
+
- `@aventara/prisma7-adapter`: the generate step no longer reads the configuration. With `--config`, the discovery
|
|
58
|
+
artifact imports `computedFields` and `AvComputedFields` from the file `aventara-build` writes beside the
|
|
59
|
+
configuration (`generated/aventara/computed-fields.generated.ts`); a `--config` file that does not exist is still
|
|
60
|
+
refused. `readConfigComputedFields`, `AvPrismaConfigReaderError` and `ConfigComputedFieldsReading` are removed from
|
|
61
|
+
`@aventara/prisma7-adapter/generate`, as are `PrismaGenerateToolchain.readComputedFields` and
|
|
62
|
+
`PrismaGenerateResult.computedFields`.
|
|
63
|
+
- `@aventara/cli`: the scaffold's `src/aventara.config.ts` takes its Resources from the tree,
|
|
64
|
+
`resources: aventaraResources.root`, so a new Resource file needs no configuration edit; the agent skill says so.
|
|
65
|
+
|
|
66
|
+
- Updated dependencies [e3d03a7]
|
|
67
|
+
- Updated dependencies [d10639a]
|
|
68
|
+
- Updated dependencies [f67e68c]
|
|
69
|
+
- Updated dependencies [708556c]
|
|
70
|
+
- Updated dependencies [3a0dfca]
|
|
71
|
+
- Updated dependencies [99169d2]
|
|
72
|
+
- Updated dependencies [0fd2608]
|
|
73
|
+
- Updated dependencies [154e440]
|
|
74
|
+
- Updated dependencies [2fcffdb]
|
|
75
|
+
- @aventara/core@1.0.0-rc.4
|
|
76
|
+
|
package/LICENSE
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Required Notice: Copyright 2026 Mohamed Ragheb
|
|
2
|
+
|
|
3
|
+
# PolyForm Shield License 1.0.0
|
|
4
|
+
|
|
5
|
+
<https://polyformproject.org/licenses/shield/1.0.0>
|
|
6
|
+
|
|
7
|
+
## Acceptance
|
|
8
|
+
|
|
9
|
+
In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
|
|
10
|
+
|
|
11
|
+
## Copyright License
|
|
12
|
+
|
|
13
|
+
The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license).
|
|
14
|
+
|
|
15
|
+
## Distribution License
|
|
16
|
+
|
|
17
|
+
The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license).
|
|
18
|
+
|
|
19
|
+
## Notices
|
|
20
|
+
|
|
21
|
+
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example:
|
|
22
|
+
|
|
23
|
+
> Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
|
|
24
|
+
|
|
25
|
+
## Changes and New Works License
|
|
26
|
+
|
|
27
|
+
The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose.
|
|
28
|
+
|
|
29
|
+
## Patent License
|
|
30
|
+
|
|
31
|
+
The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
|
|
32
|
+
|
|
33
|
+
## Noncompete
|
|
34
|
+
|
|
35
|
+
Any purpose is a permitted purpose, except for providing any product that competes with the software or any product the licensor or any of its affiliates provides using the software.
|
|
36
|
+
|
|
37
|
+
## Competition
|
|
38
|
+
|
|
39
|
+
Goods and services compete even when they provide functionality through different kinds of interfaces or for different technical platforms. Applications can compete with services, libraries with plugins, frameworks with development tools, and so on, even if they're written in different programming languages or for different computer architectures. Goods and services compete even when provided free of charge. If you market a product as a practical substitute for the software or another product, it definitely competes.
|
|
40
|
+
|
|
41
|
+
## New Products
|
|
42
|
+
|
|
43
|
+
If you are using the software to provide a product that does not compete, but the licensor or any of its affiliates brings your product into competition by providing a new version of the software or another product using the software, you may continue using versions of the software available under these terms beforehand to provide your competing product, but not any later versions.
|
|
44
|
+
|
|
45
|
+
## Discontinued Products
|
|
46
|
+
|
|
47
|
+
You may begin using the software to compete with a product or service that the licensor or any of its affiliates has stopped providing, unless the licensor includes a plain-text line beginning with `Licensor Line of Business:` with the software that mentions that line of business. For example:
|
|
48
|
+
|
|
49
|
+
> Licensor Line of Business: YoyodyneCMS Content Management System (http://example.com/cms)
|
|
50
|
+
|
|
51
|
+
## Sales of Business
|
|
52
|
+
|
|
53
|
+
If the licensor or any of its affiliates sells a line of business developing the software or using the software to provide a product, the buyer can also enforce [Noncompete](#noncompete) for that product.
|
|
54
|
+
|
|
55
|
+
## Fair Use
|
|
56
|
+
|
|
57
|
+
You may have "fair use" rights for the software under the law. These terms do not limit them.
|
|
58
|
+
|
|
59
|
+
## No Other Rights
|
|
60
|
+
|
|
61
|
+
These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses.
|
|
62
|
+
|
|
63
|
+
## Patent Defense
|
|
64
|
+
|
|
65
|
+
If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
|
|
66
|
+
|
|
67
|
+
## Violations
|
|
68
|
+
|
|
69
|
+
The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately.
|
|
70
|
+
|
|
71
|
+
## No Liability
|
|
72
|
+
|
|
73
|
+
***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
|
|
74
|
+
|
|
75
|
+
## Definitions
|
|
76
|
+
|
|
77
|
+
The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
|
|
78
|
+
|
|
79
|
+
A **product** can be a good or service, or a combination of them.
|
|
80
|
+
|
|
81
|
+
**You** refers to the individual or entity agreeing to these terms.
|
|
82
|
+
|
|
83
|
+
**Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all its affiliates.
|
|
84
|
+
|
|
85
|
+
**Affiliates** means the other organizations than an organization has control over, is under the control of, or is under common control with.
|
|
86
|
+
|
|
87
|
+
**Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
|
|
88
|
+
|
|
89
|
+
**Your licenses** are all the licenses granted to you for the software under these terms.
|
|
90
|
+
|
|
91
|
+
**Use** means anything you do with the software requiring one of your licenses.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Additional Permission — Aventara
|
|
2
|
+
|
|
3
|
+
Copyright 2026 Mohamed Ragheb
|
|
4
|
+
|
|
5
|
+
In addition to the permissions of the PolyForm Shield License 1.0.0 in `LICENSE`, the licensor grants the following additional permission:
|
|
6
|
+
|
|
7
|
+
The Noncompete section applies only to providing a product that competes with the software itself — the Aventara framework, its packages and its tooling. Providing a product that competes with any other product the licensor or its affiliates provide using the software is a permitted purpose.
|
|
8
|
+
|
|
9
|
+
This additional permission only adds to your permissions; it does not restrict or replace any term of `LICENSE`.
|
package/README.md
CHANGED
|
@@ -1,3 +1,139 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@aventara/build`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The `aventara-build` command compiles an Aventara application's Contracts when the project is built, so a serverless
|
|
4
|
+
or edge function loads them at a cold start instead of compiling them. It is a development dependency: nothing in your
|
|
5
|
+
deployed application imports it.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install --save-dev --save-exact @aventara/build@<your @aventara/core version>
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`aventara new` and `aventara init` add it and run it from the project's `aventara:prepare` script, after the adapter's
|
|
12
|
+
generate step:
|
|
13
|
+
|
|
14
|
+
```text
|
|
15
|
+
aventara-prisma7-generate … && aventara-build
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## What it reads and writes
|
|
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
|
|
21
|
+
install is right on its first `aventara:prepare`. They go to the directory your configuration imports
|
|
22
|
+
`aventaraResources` from, beside the discovery artifact: `src/generated/aventara/` normally, `src/aventara/` when
|
|
23
|
+
`aventara init` found Prisma's client generated into `src/generated` (which `prisma generate` would empty). A
|
|
24
|
+
configuration that imports no tree keeps `src/generated/aventara/`.
|
|
25
|
+
|
|
26
|
+
| File | What it holds |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `resources.generated.ts` | `aventaraResources`: every Resource your `src/resources/**/*.resource.ts` files define, one `resources` tree per layer |
|
|
29
|
+
| `computed-fields.generated.ts` | `computedFields` (typed `AvComputedFields`): which fields the configuration's write compute fills, read off its type |
|
|
30
|
+
| `contracts.generated.ts` | `aventaraContracts` (typed `AvPrebuiltContracts`): the compiled Contracts |
|
|
31
|
+
|
|
32
|
+
### The Resource tree
|
|
33
|
+
|
|
34
|
+
Each Resource's configuration lives in its own file under `src/resources/`, named `<name>.resource.ts`, as an exported
|
|
35
|
+
`defineResource(...)` result. The build reads those files through TypeScript (it never runs them), takes every export
|
|
36
|
+
whose type is a `defineResource` result, and writes the spreads you would otherwise write by hand:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
// src/generated/aventara/resources.generated.ts (generated)
|
|
40
|
+
import { post } from "../../resources/post.resource.js";
|
|
41
|
+
import { session } from "../../resources/session.resource.js";
|
|
42
|
+
|
|
43
|
+
export const aventaraResources = {
|
|
44
|
+
root: { ...post.root, ...session.root },
|
|
45
|
+
client: { ...post.client },
|
|
46
|
+
} as const;
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`root` is always there; `application`, `client` and `agent` only when some Resource gives that layer a block. The
|
|
50
|
+
configuration takes the trees once, and a new Resource file needs no edit to it:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
// src/aventara.config.ts
|
|
54
|
+
import { aventaraResources } from "./generated/aventara/resources.generated.js";
|
|
55
|
+
|
|
56
|
+
export const aventaraConfig = {
|
|
57
|
+
api: "/api",
|
|
58
|
+
exposed: true,
|
|
59
|
+
resources: aventaraResources.root,
|
|
60
|
+
client: { exposed: true, resources: aventaraResources.client },
|
|
61
|
+
} as const;
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Two files defining the same Resource stop the build, naming both: spread by hand, the later spread silently replaced
|
|
65
|
+
the earlier block. The imports are spelled as your project spells its own, by the rule `avclient` follows for the client it writes:
|
|
66
|
+
`.js` in an ES module under `nodenext`, none with `module: "commonjs"` or `moduleResolution: "bundler"`, `.ts` with
|
|
67
|
+
`allowImportingTsExtensions`. One difference: a CommonJS package under `nodenext` (Nest's CommonJS scaffold) gets no
|
|
68
|
+
suffix either, as the scaffold writes its own imports there, because these files import values and the jest such a
|
|
69
|
+
project tests with resolves no `.js` specifier to a `.ts` file. The tree is the same expression you would write, so it
|
|
70
|
+
costs the same type checking. The files matched are `src/resources/**/*.resource.ts`; the pattern is not configurable
|
|
71
|
+
yet.
|
|
72
|
+
|
|
73
|
+
### The computed fields
|
|
74
|
+
|
|
75
|
+
The build reads which fields the configuration's `compute.create` and `compute.update` stages fill off the
|
|
76
|
+
configuration's TypeScript type, after it wrote the tree, and writes them to `computed-fields.generated.ts`. The Prisma
|
|
77
|
+
adapter's discovery artifact imports that file as its sibling (its generate step is given `--config src/aventara.config.ts`), so a
|
|
78
|
+
variant's guards, pipes and filters, which run before compute, may leave exactly those fields out of a create. Where the
|
|
79
|
+
configuration is not a literal (a wide `AvConfig` annotation, a conditional spread), the build prints where and the
|
|
80
|
+
file states nothing (`null`): those stages may then leave any create field out. A server warns
|
|
81
|
+
`AVENTARA_STALE_GENERATED_TYPES` at startup when the configuration computes other fields than the file says.
|
|
82
|
+
|
|
83
|
+
### The Contracts
|
|
84
|
+
|
|
85
|
+
To compile the Contracts it runs two of your modules, in TypeScript or JavaScript, ES module or CommonJS:
|
|
86
|
+
|
|
87
|
+
| File | What it must export |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `src/aventara.config.ts` | `aventaraConfig` (or a default export): the configuration as a plain value, without `adapter` |
|
|
90
|
+
| `src/aventara.adapter.ts` | a default export: the adapter's description, `describePrismaAdapter({ discovery, provider, driver })` for Prisma 7 |
|
|
91
|
+
|
|
92
|
+
It compiles the four Contracts with your project's own `@aventara/core`, from the configuration and the model the
|
|
93
|
+
description holds, and writes `src/generated/aventara/contracts.generated.ts`, whose export `aventaraContracts` is typed
|
|
94
|
+
`AvPrebuiltContracts`. It never constructs a database client and never connects to a database. The file is generated:
|
|
95
|
+
keep it out of git, as the scaffold's `.gitignore` does, and never edit it.
|
|
96
|
+
|
|
97
|
+
A configuration that a server would refuse at startup fails the build instead: every `CONFIG_…` and `COMPILER_…`
|
|
98
|
+
diagnostic is printed with its path and documentation link, nothing is written, and the command exits 1.
|
|
99
|
+
|
|
100
|
+
Because the build runs `src/aventara.config.ts`, everything it imports runs at build time too. Keep its top level free of
|
|
101
|
+
side effects (no connection, no request). A value read from `process.env` is read when the build runs.
|
|
102
|
+
|
|
103
|
+
## `--check`
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
aventara-build --check
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
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.
|
|
112
|
+
|
|
113
|
+
## Using the file
|
|
114
|
+
|
|
115
|
+
Pass `aventaraContracts` as `prebuilt` where the framework is created, beside the adapter:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
// a serverless or edge entry
|
|
119
|
+
import { createFramework } from "@aventara/core";
|
|
120
|
+
import { createPrismaAdapter } from "@aventara/prisma7-adapter";
|
|
121
|
+
import adapter from "./aventara.adapter.js";
|
|
122
|
+
import { aventaraConfig } from "./aventara.config.js";
|
|
123
|
+
import { aventaraContracts } from "./generated/aventara/contracts.generated.js";
|
|
124
|
+
import { prisma } from "./prisma.client.js";
|
|
125
|
+
|
|
126
|
+
export const framework = await createFramework({
|
|
127
|
+
...aventaraConfig,
|
|
128
|
+
adapter: await createPrismaAdapter({ ...adapter, client: prisma }),
|
|
129
|
+
prebuilt: aventaraContracts,
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The framework uses the file only when it was compiled by the same `@aventara/core`, for the same protocol, from the same
|
|
134
|
+
configuration and model. Otherwise it ignores the whole file, compiles at startup as it would without one, and warns
|
|
135
|
+
once with `AVENTARA_PREBUILT_STALE`, naming what changed. A stale file never serves a wrong Contract; it only costs the
|
|
136
|
+
startup time the file was meant to save.
|
|
137
|
+
|
|
138
|
+
A long-running server does not need the file. The Nest project `aventara new` writes compiles its Contracts at startup
|
|
139
|
+
and passes no `prebuilt`.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { AvBuildError } from "./build.error.js";
|
|
3
|
+
import { readConfigComputedFields } from "./computed/computed-fields.reader.js";
|
|
4
|
+
import { emitComputedFieldsModule } from "./computed/computed-fields-file.js";
|
|
5
|
+
import { checkContractsFile, writeContractsFile, } from "./contracts/contracts-file.js";
|
|
6
|
+
import { compilePrebuiltContracts } from "./contracts/prebuilt-contracts.compiler.js";
|
|
7
|
+
import { readGeneratedFile, writeGeneratedFile, } from "./project/generated-file.js";
|
|
8
|
+
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 { importSuffixOf, projectCompilerOptions, } from "./project/tsconfig.reader.js";
|
|
11
|
+
import { RESOURCES_GLOB, readResourceDefinitions, } from "./resources/resource-definitions.reader.js";
|
|
12
|
+
import { emitResourcesModule } from "./resources/resources-file.js";
|
|
13
|
+
const USAGE = `aventara-build — compile this project's Aventara Contracts ahead of time.
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
aventara-build write the three files below
|
|
17
|
+
aventara-build --check write nothing; exit 1 when one is missing or stale
|
|
18
|
+
|
|
19
|
+
Run it from the project's root. It writes, in this order:
|
|
20
|
+
|
|
21
|
+
${DEFAULT_GENERATED_FILES.resources}
|
|
22
|
+
\`aventaraResources\`: every \`defineResource(…)\` result the files
|
|
23
|
+
${RESOURCES_GLOB} export, one \`resources\` tree per layer;
|
|
24
|
+
${DEFAULT_GENERATED_FILES.computedFields}
|
|
25
|
+
which fields the configuration computes, read off its type;
|
|
26
|
+
${DEFAULT_GENERATED_FILES.contracts}
|
|
27
|
+
the compiled Contracts, \`aventaraContracts\`.
|
|
28
|
+
|
|
29
|
+
All three go to the directory the configuration imports \`aventaraResources\`
|
|
30
|
+
from (src/aventara/ when \`aventara init\` found the ORM's client generated into
|
|
31
|
+
src/generated), else to src/generated/aventara/.
|
|
32
|
+
|
|
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.
|
|
39
|
+
|
|
40
|
+
A serverless or edge host passes the file's \`aventaraContracts\` to
|
|
41
|
+
createFramework as \`prebuilt\`; a long-running server needs none.
|
|
42
|
+
|
|
43
|
+
Options:
|
|
44
|
+
--check Exit 1, writing nothing, when a file is missing or stale.
|
|
45
|
+
-h, --help Print this and exit 0.`;
|
|
46
|
+
function parse(argv) {
|
|
47
|
+
let check = false;
|
|
48
|
+
for (const argument of argv) {
|
|
49
|
+
if (argument === "--check") {
|
|
50
|
+
check = true;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
throw new AvBuildError(`unknown argument \`${argument}\`.\n\n${USAGE}`);
|
|
54
|
+
}
|
|
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)}`;
|
|
61
|
+
}
|
|
62
|
+
function counted(count) {
|
|
63
|
+
return `${count} Resource${count === 1 ? "" : "s"}`;
|
|
64
|
+
}
|
|
65
|
+
function wrote(written, file, detail) {
|
|
66
|
+
return `aventara-build: ${written === "written" ? "wrote" : "unchanged:"} ${file}${detail === undefined ? "" : ` (${detail})`}.`;
|
|
67
|
+
}
|
|
68
|
+
function checked(projectDirectory, file, text, stale) {
|
|
69
|
+
const current = readGeneratedFile(projectDirectory, file);
|
|
70
|
+
return current === undefined
|
|
71
|
+
? { file, what: "is missing" }
|
|
72
|
+
: current === text
|
|
73
|
+
? undefined
|
|
74
|
+
: { file, what: `is stale: ${stale}` };
|
|
75
|
+
}
|
|
76
|
+
function refusal({ file, what, deploy }) {
|
|
77
|
+
return `aventara-build: ${file} ${what}. Run aventara-build (npm run aventara:prepare)${deploy === true ? " and deploy the file it writes" : ""}.`;
|
|
78
|
+
}
|
|
79
|
+
export async function runBuild(argv, projectDirectory, output) {
|
|
80
|
+
if (argv.includes("-h") || argv.includes("--help")) {
|
|
81
|
+
output.stdout(USAGE);
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const { check } = parse(argv);
|
|
86
|
+
const files = findProjectModules(projectDirectory);
|
|
87
|
+
const generated = resolveGeneratedFiles(projectDirectory, files.configFile);
|
|
88
|
+
const options = projectCompilerOptions(projectDirectory);
|
|
89
|
+
const found = readResourceDefinitions(projectDirectory, options);
|
|
90
|
+
const tree = emitResourcesModule(projectDirectory, generated.resources, found.definitions, importSuffixOf(options, path.dirname(path.join(projectDirectory, generated.resources))));
|
|
91
|
+
for (const file of found.withoutDefinition) {
|
|
92
|
+
output.stdout(`aventara-build: ${file} exports no defineResource(…) result; it is not in aventaraResources.`);
|
|
93
|
+
}
|
|
94
|
+
if (check) {
|
|
95
|
+
const stale = checked(projectDirectory, generated.resources, tree, "the Resource files changed since it was written");
|
|
96
|
+
if (stale !== undefined) {
|
|
97
|
+
output.stderr(refusal(stale));
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
output.stdout(wrote(writeGeneratedFile(projectDirectory, generated.resources, tree), generated.resources, counted(found.definitions.length)));
|
|
103
|
+
}
|
|
104
|
+
if (check) {
|
|
105
|
+
if (readGeneratedFile(projectDirectory, generated.computedFields) ===
|
|
106
|
+
undefined) {
|
|
107
|
+
output.stderr(refusal({ file: generated.computedFields, what: "is missing" }));
|
|
108
|
+
return 1;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else if (readGeneratedFile(projectDirectory, generated.computedFields) ===
|
|
112
|
+
undefined) {
|
|
113
|
+
writeGeneratedFile(projectDirectory, generated.computedFields, emitComputedFieldsModule(files.configFile, {
|
|
114
|
+
kind: "unread",
|
|
115
|
+
reason: "the build has not read the configuration yet",
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
const reading = readConfigComputedFields(path.join(projectDirectory, files.configFile), options, found.program);
|
|
119
|
+
const computed = emitComputedFieldsModule(files.configFile, reading);
|
|
120
|
+
if (check) {
|
|
121
|
+
const stale = checked(projectDirectory, generated.computedFields, computed, "the fields the configuration computes changed since it was written");
|
|
122
|
+
if (stale !== undefined) {
|
|
123
|
+
output.stderr(refusal(stale));
|
|
124
|
+
return 1;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
output.stdout(wrote(writeGeneratedFile(projectDirectory, generated.computedFields, computed), generated.computedFields));
|
|
129
|
+
if (reading.kind === "unread") {
|
|
130
|
+
output.stdout(`aventara-build: computed fields not read from ${files.configFile}: ${reading.reason}. The stages before compute may leave any create field out; write the configuration \`as const\` there.`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const project = await loadProjectModules(projectDirectory, files);
|
|
134
|
+
const compiled = await compilePrebuiltContracts(project);
|
|
135
|
+
const resources = Object.keys(compiled.prebuilt.fingerprints.resources).length;
|
|
136
|
+
if (!check) {
|
|
137
|
+
output.stdout(wrote(writeContractsFile(projectDirectory, generated.contracts, compiled), generated.contracts, counted(resources)));
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
const contracts = await checkContractsFile(projectDirectory, generated.contracts, compiled);
|
|
141
|
+
if (contracts.kind === "fresh") {
|
|
142
|
+
output.stdout(`aventara-build: ${generated.resources}, ${generated.computedFields} and ${generated.contracts} are up to date.`);
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
output.stderr(refusal({
|
|
146
|
+
file: generated.contracts,
|
|
147
|
+
deploy: true,
|
|
148
|
+
what: contracts.kind === "missing"
|
|
149
|
+
? "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("; ")}`,
|
|
162
|
+
}));
|
|
163
|
+
return 1;
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
if (error instanceof AvBuildError) {
|
|
167
|
+
output.stderr(`aventara-build: ${error.message}`);
|
|
168
|
+
return 1;
|
|
169
|
+
}
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
export async function runFromProcess() {
|
|
174
|
+
try {
|
|
175
|
+
process.exitCode = await runBuild(process.argv.slice(2), process.cwd(), {
|
|
176
|
+
stdout: (line) => process.stdout.write(`${line}\n`),
|
|
177
|
+
stderr: (line) => process.stderr.write(`${line}\n`),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
process.stderr.write(`${String(error instanceof Error ? error.stack : error)}\n`);
|
|
182
|
+
process.exitCode = 1;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
function literal(computedFields) {
|
|
2
|
+
const ordered = {
|
|
3
|
+
agent: computedFields.agent,
|
|
4
|
+
agentExtends: computedFields.agentExtends,
|
|
5
|
+
application: computedFields.application,
|
|
6
|
+
client: computedFields.client,
|
|
7
|
+
root: computedFields.root,
|
|
8
|
+
};
|
|
9
|
+
return JSON.stringify(ordered, null, "\t");
|
|
10
|
+
}
|
|
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).`;
|
|
13
|
+
const type = [
|
|
14
|
+
"",
|
|
15
|
+
"// What an adapter's discovery artifact carries on its model's type for the stages that run before compute.",
|
|
16
|
+
"export type AvComputedFields = typeof computedFields;",
|
|
17
|
+
];
|
|
18
|
+
if (reading.kind === "unread") {
|
|
19
|
+
return `${[
|
|
20
|
+
header,
|
|
21
|
+
"",
|
|
22
|
+
`// Not read: ${reading.reason.replaceAll("\n", " ")}. The stages that run before compute may leave any`,
|
|
23
|
+
"// create field out; write the configuration `as const` there to have it read.",
|
|
24
|
+
"export const computedFields = null;",
|
|
25
|
+
...type,
|
|
26
|
+
].join("\n")}\n`;
|
|
27
|
+
}
|
|
28
|
+
return `${[
|
|
29
|
+
header,
|
|
30
|
+
'import type { AvConfigComputedFields } from "@aventara/core";',
|
|
31
|
+
"",
|
|
32
|
+
"// Which fields the configuration's write compute fills (`compute.create`, `compute.update`), per layer and",
|
|
33
|
+
"// Resource. The framework compares it with the configuration at startup and warns when they differ.",
|
|
34
|
+
`export const computedFields = ${literal(reading.computedFields)} as const satisfies AvConfigComputedFields;`,
|
|
35
|
+
...type,
|
|
36
|
+
].join("\n")}\n`;
|
|
37
|
+
}
|