@aventara/build 1.0.0-rc.11 → 1.0.0-rc.13
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 +51 -0
- package/README.md +46 -3
- package/dist/cli.js +110 -33
- package/dist/client/client-file.js +95 -19
- package/dist/computed/computed-fields-file.js +4 -1
- package/dist/computed/computed-fields.reader.js +30 -3
- package/dist/contracts/contracts-file.js +1 -4
- package/dist/project/build.lock.js +80 -0
- package/dist/project/generated-file.js +86 -2
- package/dist/project/project-modules.loader.js +9 -5
- package/dist/project/tsconfig.reader.js +12 -0
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
# @aventara/build
|
|
2
2
|
|
|
3
|
+
## 1.0.0-rc.13
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- a4569dd: In a Next.js project, `aventara-build --client` gives the browser client hooks from `src/aventara/client-options.ts`
|
|
8
|
+
(or `.js`; another extension is refused by name): its default export, `{ onCall, onSuccess, onError }`, typed with
|
|
9
|
+
`satisfies Pick<AvClientOptions, "onCall" | "onSuccess" | "onError">` (`AvClientOptions` from `@/aventara/client`).
|
|
10
|
+
`@/aventara/client` and `@/aventara/universal` import it and give its hooks to every client they make, the browser's
|
|
11
|
+
one and each call's own on the server; without the file both are written as before. The entries keep their own
|
|
12
|
+
`fetch`, `grouping` and entrypoint: in the file those are type errors naming the entry, and are ignored at run time.
|
|
13
|
+
The file is bundled into the browser too, so it holds no secret and imports nothing server-only. On the server
|
|
14
|
+
`onCall`'s headers reach the in-process request, while its state still comes from the request being served.
|
|
15
|
+
The hooks read the default export when they run, never when the entries load, so the file may import from
|
|
16
|
+
`@/aventara/client` (an error class); a file with no default export leaves the client working without hooks, with one
|
|
17
|
+
warning (`AVENTARA_CLIENT_OPTIONS_NO_DEFAULT`). `@/aventara/server` is not an `AvClient` and takes no hooks. Adding or removing the file makes `--check` find both
|
|
18
|
+
entries stale.
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- 5c65195: `aventara new` and `aventara init` now add a `typecheck` script, so a monorepo's `turbo typecheck` reaches the new project: `tsc --noEmit` in a NestJS project, `next typegen && tsc --noEmit` in a Next.js one. A `typecheck` script the project already has is kept as it is. The agent skill's "Before you finish" runs it.
|
|
23
|
+
|
|
24
|
+
`aventara-build` no longer leaves a project whose `tsc` reports `Type 'AvModel' does not satisfy the constraint 'AvAdapterModel'` in every Resource file, which hid the real error. A run that fails (or is interrupted) now changes no generated file, the Next.js browser client included: each keeps what the last successful run wrote. The failure prints before the run's other lines. Each file is replaced in one step, and a second run started while one is writing the same project is refused. When the configuration's computed fields cannot be read and the project does not type-check (read as `tsc -p .` reads it), the build prints those type errors first and keeps `computed-fields.generated.ts` as it was, instead of rewriting it to state nothing. A file that does state nothing (a configuration that is not a literal) now types its model correctly. If you see that cascade from an older build, run `aventara:prepare` again and fix the first error it prints.
|
|
25
|
+
|
|
26
|
+
- Updated dependencies [6c3e25c]
|
|
27
|
+
- Updated dependencies [33419de]
|
|
28
|
+
- Updated dependencies [2aaa27d]
|
|
29
|
+
- Updated dependencies [f3b291f]
|
|
30
|
+
- Updated dependencies [2aaa27d]
|
|
31
|
+
- Updated dependencies [e4e8abd]
|
|
32
|
+
- Updated dependencies [e662048]
|
|
33
|
+
- Updated dependencies [f3b291f]
|
|
34
|
+
- Updated dependencies [f3b291f]
|
|
35
|
+
- Updated dependencies [6c3e25c]
|
|
36
|
+
- Updated dependencies [6c3e25c]
|
|
37
|
+
- Updated dependencies [73aa797]
|
|
38
|
+
- Updated dependencies [a6cebba]
|
|
39
|
+
- Updated dependencies [e6495a0]
|
|
40
|
+
- Updated dependencies [a708188]
|
|
41
|
+
- Updated dependencies [a9260d2]
|
|
42
|
+
- @aventara/core@1.0.0-rc.13
|
|
43
|
+
- @aventara/client@1.0.0-rc.13
|
|
44
|
+
|
|
45
|
+
## 1.0.0-rc.12
|
|
46
|
+
|
|
47
|
+
### Patch Changes
|
|
48
|
+
|
|
49
|
+
- Updated dependencies [afd5eda]
|
|
50
|
+
- Updated dependencies [fdeee8a]
|
|
51
|
+
- @aventara/client@1.0.0-rc.12
|
|
52
|
+
- @aventara/core@1.0.0-rc.12
|
|
53
|
+
|
|
3
54
|
## 1.0.0-rc.11
|
|
4
55
|
|
|
5
56
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -77,8 +77,11 @@ configuration's TypeScript type, after it wrote the tree, and writes them to `co
|
|
|
77
77
|
adapter's discovery artifact imports that file as its sibling (its generate step is given `--config src/aventara.config.ts`), so a
|
|
78
78
|
variant's guards, pipes and filters, which run before compute, may leave exactly those fields out of a create. Where the
|
|
79
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.
|
|
81
|
-
|
|
80
|
+
file states nothing (`null`): those stages may then leave any create field out. When the configuration is not read and
|
|
81
|
+
the configuration or a Resource file it imports does not type-check either, the build prints those type errors first
|
|
82
|
+
(the project read as `tsc -p .` reads it) and leaves the file as it was: fix them, or write the configuration `as const`,
|
|
83
|
+
whichever kept it from being read. `--check` then exits 1: the file cannot be judged until they are fixed. A server warns `AVENTARA_STALE_GENERATED_TYPES` at startup when the
|
|
84
|
+
configuration computes other fields than the file says.
|
|
82
85
|
|
|
83
86
|
### The Contracts
|
|
84
87
|
|
|
@@ -95,7 +98,15 @@ description holds, and writes `src/generated/aventara/contracts.generated.ts`, w
|
|
|
95
98
|
keep it out of git, as the scaffold's `.gitignore` does, and never edit it.
|
|
96
99
|
|
|
97
100
|
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,
|
|
101
|
+
diagnostic is printed with its path and documentation link, and the command exits 1. A run that fails, for this or any
|
|
102
|
+
other reason (Ctrl-C included), prints why before anything else and changes no generated file, the browser client's
|
|
103
|
+
included: each keeps what the last run that succeeded wrote (a fresh project gets none). Each file is replaced in one
|
|
104
|
+
step, never left half-written, and one run at a time writes a project's files: a second, started while the first
|
|
105
|
+
writes, is refused (the lock is `node_modules/.cache/aventara-build/build.lock`; a dead run's is taken over).
|
|
106
|
+
|
|
107
|
+
`Type 'AvModel' does not satisfy the constraint 'AvAdapterModel'` in every Resource file means the generated files and
|
|
108
|
+
your configuration are out of step: `aventara:prepare` failed or has not run since a change. Run it, and fix the first
|
|
109
|
+
error it prints.
|
|
99
110
|
|
|
100
111
|
Because the build runs `src/aventara.config.ts`, everything it imports runs at build time too. Keep its top level free of
|
|
101
112
|
side effects (no connection, no request). A value read from `process.env` is read when the build runs.
|
|
@@ -136,6 +147,38 @@ The universal `avClient` answers the same values on both sides; on a server wher
|
|
|
136
147
|
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
|
|
137
148
|
the client's own `AvDecimal`), for TanStack Query's `deserializeData` or an SWR `fallback`.
|
|
138
149
|
|
|
150
|
+
### The client's hooks: `src/aventara/client-options.ts`
|
|
151
|
+
|
|
152
|
+
To give the browser client hooks (`onCall`, `onSuccess`, `onError`, see `@aventara/client`'s README), export them from
|
|
153
|
+
`src/aventara/client-options.ts` (or `.js`; any other extension, `.tsx` and `.mts` included, is refused, since the
|
|
154
|
+
entries must import it under webpack and Turbopack alike):
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
import type { AvClientOptions } from "@/aventara/client";
|
|
158
|
+
|
|
159
|
+
export default {
|
|
160
|
+
onCall: () => ({ headers: { "X-Tenant": "acme" } }),
|
|
161
|
+
onError: (call, error) => console.warn(call.label, error),
|
|
162
|
+
} satisfies Pick<AvClientOptions, "onCall" | "onSuccess" | "onError">;
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
- When the file is there, `client.ts` and `universal.ts` import it and give its default export's hooks to every
|
|
166
|
+
`AvClient` they make: the browser's one, and on the server the one each `@/aventara/universal` call gets. Without
|
|
167
|
+
the file they are written as before.
|
|
168
|
+
- The hooks read the file's default export when they run, never when the entries load, so the file may import from
|
|
169
|
+
`@/aventara/client` (an error class for `instanceof`). A file with no default export leaves the client working
|
|
170
|
+
without hooks, with one warning (`AVENTARA_CLIENT_OPTIONS_NO_DEFAULT`).
|
|
171
|
+
- The entries keep their own `fetch` and `grouping`, and the default entrypoint: a `fetch`, `grouping` or
|
|
172
|
+
`entrypoint` in the file is a type error naming the entry that sets it, and is ignored at run time.
|
|
173
|
+
- The file is bundled into the browser too, since `client.ts` imports it. Import nothing server-only into it and keep
|
|
174
|
+
no secret there; when a hook must act differently on the server, branch on `typeof window`.
|
|
175
|
+
- On the server, `@/aventara/universal` calls the app in process: `onCall`'s headers reach that request, but its
|
|
176
|
+
state is seeded from the request being served, as every server call's is, so a header added there signs no one in.
|
|
177
|
+
In the browser they are on the HTTP request, where your `state` reads them like any other header.
|
|
178
|
+
- `@/aventara/server`'s `avClient` is the Framework's own in-process client, not an `AvClient`: the file does not reach
|
|
179
|
+
it.
|
|
180
|
+
- Adding or removing the file changes both entries: run the build again (`--check` finds them stale).
|
|
181
|
+
|
|
139
182
|
`avclient generate` stays the way to generate a client for a separate frontend. In a NestJS project `--client` is
|
|
140
183
|
refused.
|
|
141
184
|
|
package/dist/cli.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import ts from "typescript";
|
|
2
3
|
import { AvBuildError } from "./build.error.js";
|
|
3
4
|
import { checkBrowserClient, emitBrowserClient, writeBrowserClient, } from "./client/client-file.js";
|
|
4
5
|
import { readConfigComputedFields } from "./computed/computed-fields.reader.js";
|
|
5
6
|
import { emitComputedFieldsModule } from "./computed/computed-fields-file.js";
|
|
6
|
-
import { checkContractsFile
|
|
7
|
+
import { checkContractsFile } from "./contracts/contracts-file.js";
|
|
7
8
|
import { compilePrebuiltContracts } from "./contracts/prebuilt-contracts.compiler.js";
|
|
8
|
-
import {
|
|
9
|
+
import { acquireBuildLock } from "./project/build.lock.js";
|
|
10
|
+
import { generatedFileBatch, readGeneratedFile, } from "./project/generated-file.js";
|
|
9
11
|
import { DEFAULT_GENERATED_FILES, resolveGeneratedFiles, } from "./project/generated-files.resolver.js";
|
|
10
12
|
import { applyProjectEnv } from "./project/project-env.loader.js";
|
|
11
13
|
import { PROJECT_LAYOUTS, resolveProjectLayout, } from "./project/project-layout.resolver.js";
|
|
12
14
|
import { findProjectModules, loadProjectModules, } from "./project/project-modules.loader.js";
|
|
13
|
-
import { importSuffixOf, projectCompilerOptions, } from "./project/tsconfig.reader.js";
|
|
15
|
+
import { importSuffixOf, projectCompilerOptions, projectSourceFiles, } from "./project/tsconfig.reader.js";
|
|
14
16
|
import { RESOURCES_GLOB, readResourceDefinitions, } from "./resources/resource-definitions.reader.js";
|
|
15
17
|
import { emitResourcesModule } from "./resources/resources-file.js";
|
|
16
18
|
const USAGE = `aventara-build — compile this project's Aventara Contracts ahead of time.
|
|
@@ -91,6 +93,36 @@ function counted(count) {
|
|
|
91
93
|
function wrote(written, file, detail) {
|
|
92
94
|
return `aventara-build: ${written === "written" ? "wrote" : "unchanged:"} ${file}${detail === undefined ? "" : ` (${detail})`}.`;
|
|
93
95
|
}
|
|
96
|
+
function defectText(error) {
|
|
97
|
+
return (error instanceof Error && error.stack) || String(error);
|
|
98
|
+
}
|
|
99
|
+
const SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
100
|
+
function restoreOnSignal(undo) {
|
|
101
|
+
const stop = () => {
|
|
102
|
+
for (const signal of SIGNALS) {
|
|
103
|
+
process.removeListener(signal, onSignal);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
const onSignal = (signal) => {
|
|
107
|
+
stop();
|
|
108
|
+
undo();
|
|
109
|
+
process.kill(process.pid, signal);
|
|
110
|
+
};
|
|
111
|
+
for (const signal of SIGNALS) {
|
|
112
|
+
process.once(signal, onSignal);
|
|
113
|
+
}
|
|
114
|
+
return stop;
|
|
115
|
+
}
|
|
116
|
+
const LISTED_TYPE_ERRORS = 10;
|
|
117
|
+
function typeErrors(projectDirectory, errors) {
|
|
118
|
+
const listed = ts.formatDiagnostics(errors.slice(0, LISTED_TYPE_ERRORS), {
|
|
119
|
+
getCanonicalFileName: (file) => file,
|
|
120
|
+
getCurrentDirectory: () => projectDirectory,
|
|
121
|
+
getNewLine: () => "\n",
|
|
122
|
+
});
|
|
123
|
+
const more = errors.length - LISTED_TYPE_ERRORS;
|
|
124
|
+
return `${listed.trimEnd()}${more > 0 ? `\n(+${more} more)` : ""}`;
|
|
125
|
+
}
|
|
94
126
|
function checked(projectDirectory, file, text, stale) {
|
|
95
127
|
const current = readGeneratedFile(projectDirectory, file);
|
|
96
128
|
return current === undefined
|
|
@@ -99,8 +131,8 @@ function checked(projectDirectory, file, text, stale) {
|
|
|
99
131
|
? undefined
|
|
100
132
|
: { file, what: `is stale: ${stale}` };
|
|
101
133
|
}
|
|
102
|
-
function refusal({ file, what, deploy }) {
|
|
103
|
-
return `aventara-build: ${file} ${what}. Run aventara-build (npm run aventara:prepare)${deploy === true ? " and deploy the file it writes" : ""}
|
|
134
|
+
function refusal({ file, what, deploy, remedy }) {
|
|
135
|
+
return `aventara-build: ${file} ${what}. ${remedy ?? `Run aventara-build (npm run aventara:prepare)${deploy === true ? " and deploy the file it writes" : ""}.`}`;
|
|
104
136
|
}
|
|
105
137
|
export async function runBuild(argv, projectDirectory, output) {
|
|
106
138
|
if (argv.includes("-h") || argv.includes("--help")) {
|
|
@@ -108,12 +140,30 @@ export async function runBuild(argv, projectDirectory, output) {
|
|
|
108
140
|
return 0;
|
|
109
141
|
}
|
|
110
142
|
let restoreEnv;
|
|
143
|
+
const lines = [];
|
|
144
|
+
const note = (text) => {
|
|
145
|
+
lines.push({ text });
|
|
146
|
+
};
|
|
147
|
+
const noteWrite = (text) => {
|
|
148
|
+
lines.push({ text, write: true });
|
|
149
|
+
};
|
|
150
|
+
const batch = generatedFileBatch(projectDirectory);
|
|
151
|
+
let failed = false;
|
|
152
|
+
let releaseLock;
|
|
153
|
+
let stopSignals;
|
|
111
154
|
try {
|
|
112
155
|
const { check, client } = parse(argv);
|
|
156
|
+
if (!check) {
|
|
157
|
+
releaseLock = acquireBuildLock(projectDirectory);
|
|
158
|
+
stopSignals = restoreOnSignal(() => {
|
|
159
|
+
batch.restore();
|
|
160
|
+
releaseLock?.();
|
|
161
|
+
});
|
|
162
|
+
}
|
|
113
163
|
const env = applyProjectEnv(projectDirectory);
|
|
114
164
|
restoreEnv = env.restore;
|
|
115
165
|
if (env.loaded.length > 0) {
|
|
116
|
-
|
|
166
|
+
note(`aventara-build: environment from ${listed(env.loaded)} (a variable already set keeps its value).`);
|
|
117
167
|
}
|
|
118
168
|
const layout = resolveProjectLayout(projectDirectory);
|
|
119
169
|
const clientLayout = client ? layout.client : undefined;
|
|
@@ -126,7 +176,7 @@ export async function runBuild(argv, projectDirectory, output) {
|
|
|
126
176
|
const found = readResourceDefinitions(projectDirectory, options);
|
|
127
177
|
const tree = emitResourcesModule(projectDirectory, generated.resources, found.definitions, importSuffixOf(options, path.dirname(path.join(projectDirectory, generated.resources))));
|
|
128
178
|
for (const file of found.withoutDefinition) {
|
|
129
|
-
|
|
179
|
+
note(`aventara-build: ${file} exports no defineResource(…) result; it is not in aventaraResources.`);
|
|
130
180
|
}
|
|
131
181
|
if (check) {
|
|
132
182
|
const stale = checked(projectDirectory, generated.resources, tree, "the Resource files changed since it was written");
|
|
@@ -136,25 +186,41 @@ export async function runBuild(argv, projectDirectory, output) {
|
|
|
136
186
|
}
|
|
137
187
|
}
|
|
138
188
|
else {
|
|
139
|
-
|
|
189
|
+
noteWrite(wrote(batch.write(generated.resources, tree), generated.resources, counted(found.definitions.length)));
|
|
140
190
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
191
|
+
const hadComputed = readGeneratedFile(projectDirectory, generated.computedFields) !==
|
|
192
|
+
undefined;
|
|
193
|
+
if (check && !hadComputed) {
|
|
194
|
+
output.stderr(refusal({ file: generated.computedFields, what: "is missing" }));
|
|
195
|
+
return 1;
|
|
147
196
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
writeGeneratedFile(projectDirectory, generated.computedFields, emitComputedFieldsModule(files.configFile, {
|
|
197
|
+
if (!check && !hadComputed) {
|
|
198
|
+
batch.write(generated.computedFields, emitComputedFieldsModule(files.configFile, {
|
|
151
199
|
kind: "unread",
|
|
152
200
|
reason: "the build has not read the configuration yet",
|
|
153
201
|
}));
|
|
154
202
|
}
|
|
155
|
-
const reading = readConfigComputedFields(path.join(projectDirectory, files.configFile), options, found.program);
|
|
156
|
-
|
|
157
|
-
|
|
203
|
+
const reading = readConfigComputedFields(path.join(projectDirectory, files.configFile), options, found.program, projectSourceFiles(projectDirectory));
|
|
204
|
+
if (reading.kind === "ill-typed") {
|
|
205
|
+
output.stderr(`aventara-build: computed fields not read from ${files.configFile}: ${reading.reason}. It does not type-check either: if these errors are what kept it from being read, fix them; if not, write the configuration \`as const\` there.${hadComputed ? ` ${generated.computedFields} keeps what it said.` : ""}\n${typeErrors(projectDirectory, reading.errors)}`);
|
|
206
|
+
}
|
|
207
|
+
const kept = reading.kind === "ill-typed" && hadComputed;
|
|
208
|
+
const statement = reading.kind === "ill-typed"
|
|
209
|
+
? { kind: "unread", reason: reading.reason }
|
|
210
|
+
: reading;
|
|
211
|
+
const computed = emitComputedFieldsModule(files.configFile, statement);
|
|
212
|
+
if (kept && check) {
|
|
213
|
+
output.stderr(refusal({
|
|
214
|
+
file: generated.computedFields,
|
|
215
|
+
what: "cannot be checked while the configuration does not type-check",
|
|
216
|
+
remedy: "Fix the type errors listed above; running aventara-build changes nothing until then.",
|
|
217
|
+
}));
|
|
218
|
+
return 1;
|
|
219
|
+
}
|
|
220
|
+
if (kept) {
|
|
221
|
+
noteWrite(wrote("unchanged", generated.computedFields));
|
|
222
|
+
}
|
|
223
|
+
else if (check) {
|
|
158
224
|
const stale = checked(projectDirectory, generated.computedFields, computed, "the fields the configuration computes changed since it was written");
|
|
159
225
|
if (stale !== undefined) {
|
|
160
226
|
output.stderr(refusal(stale));
|
|
@@ -162,9 +228,9 @@ export async function runBuild(argv, projectDirectory, output) {
|
|
|
162
228
|
}
|
|
163
229
|
}
|
|
164
230
|
else {
|
|
165
|
-
|
|
231
|
+
noteWrite(wrote(batch.write(generated.computedFields, computed), generated.computedFields));
|
|
166
232
|
if (reading.kind === "unread") {
|
|
167
|
-
|
|
233
|
+
note(`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.`);
|
|
168
234
|
}
|
|
169
235
|
}
|
|
170
236
|
const project = await loadProjectModules(projectDirectory, files);
|
|
@@ -178,17 +244,17 @@ export async function runBuild(argv, projectDirectory, output) {
|
|
|
178
244
|
canonicalText: (contract) => project.internal.canonicalizeContract(contract),
|
|
179
245
|
});
|
|
180
246
|
if (!check) {
|
|
181
|
-
|
|
247
|
+
noteWrite(wrote(batch.write(generated.contracts, compiled.source), generated.contracts, counted(resources)));
|
|
182
248
|
if (browserClient !== undefined) {
|
|
183
249
|
for (const warning of browserClient.warnings) {
|
|
184
|
-
|
|
250
|
+
note(`aventara-build: ${warning}`);
|
|
185
251
|
}
|
|
186
|
-
const written = writeBrowserClient(projectDirectory, browserClient);
|
|
252
|
+
const written = writeBrowserClient(projectDirectory, browserClient, batch);
|
|
187
253
|
for (const file of written.removed) {
|
|
188
|
-
|
|
254
|
+
note(`aventara-build: removed ${file}, which this ClientContract's browser client no longer has.`);
|
|
189
255
|
}
|
|
190
256
|
const [entry, universal] = browserClient.files;
|
|
191
|
-
|
|
257
|
+
noteWrite(wrote(written.write, `${entry?.file}, ${universal?.file} and ${browserClient.tree}/`, `the browser client, calling ${browserClient.entrypoint === "" ? "/" : browserClient.entrypoint}`));
|
|
192
258
|
}
|
|
193
259
|
return 0;
|
|
194
260
|
}
|
|
@@ -201,7 +267,7 @@ export async function runBuild(argv, projectDirectory, output) {
|
|
|
201
267
|
output.stderr(refusal(stale));
|
|
202
268
|
return 1;
|
|
203
269
|
}
|
|
204
|
-
|
|
270
|
+
note(browserClient === undefined
|
|
205
271
|
? `aventara-build: ${generated.resources}, ${generated.computedFields} and ${generated.contracts} are up to date.`
|
|
206
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.`);
|
|
207
273
|
return 0;
|
|
@@ -216,14 +282,25 @@ export async function runBuild(argv, projectDirectory, output) {
|
|
|
216
282
|
return 1;
|
|
217
283
|
}
|
|
218
284
|
catch (error) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
285
|
+
failed = true;
|
|
286
|
+
const unrestored = batch.restore();
|
|
287
|
+
output.stderr(error instanceof AvBuildError
|
|
288
|
+
? `aventara-build: ${error.message}`
|
|
289
|
+
: defectText(error));
|
|
290
|
+
for (const { file, error: cause } of unrestored) {
|
|
291
|
+
output.stderr(`aventara-build: ${file} could not be put back as it was (${cause instanceof Error ? cause.message : String(cause)}); run aventara-build again.`);
|
|
222
292
|
}
|
|
223
|
-
|
|
293
|
+
return 1;
|
|
224
294
|
}
|
|
225
295
|
finally {
|
|
296
|
+
stopSignals?.();
|
|
297
|
+
releaseLock?.();
|
|
226
298
|
restoreEnv?.();
|
|
299
|
+
for (const line of lines) {
|
|
300
|
+
if (!(failed && line.write === true)) {
|
|
301
|
+
output.stdout(line.text);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
227
304
|
}
|
|
228
305
|
}
|
|
229
306
|
export async function runFromProcess() {
|
|
@@ -234,7 +311,7 @@ export async function runFromProcess() {
|
|
|
234
311
|
});
|
|
235
312
|
}
|
|
236
313
|
catch (error) {
|
|
237
|
-
process.stderr.write(`${(error
|
|
314
|
+
process.stderr.write(`${defectText(error)}\n`);
|
|
238
315
|
process.exitCode = 1;
|
|
239
316
|
}
|
|
240
317
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { readdirSync,
|
|
1
|
+
import { readdirSync, statSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { AvClientConfigError, AvGeneratedNameError, acceptClientContract, accessVerbsOf, carriesGeneratedOwnership, clientEntryReExports, emitClientTree, GENERATED_DIRECTORY, HYDRATION_FORMAT, importSpecifier, resolveModuleStyle, sameOriginEntrypoint, } from "@aventara/client/internal";
|
|
4
4
|
import { AvBuildError } from "../build.error.js";
|
|
5
|
-
import { readGeneratedFile,
|
|
5
|
+
import { readGeneratedFile, } from "../project/generated-file.js";
|
|
6
|
+
import { optionalProjectFile } from "../project/project-modules.loader.js";
|
|
6
7
|
export const REGENERATE = "`aventara-build --client` (the project's `aventara:prepare` script)";
|
|
7
8
|
const ENTRY_BANNER = [
|
|
8
9
|
"// biome-ignore-all format: generated output",
|
|
@@ -87,7 +88,69 @@ function guardedClient(template: AvClient): AvClient {
|
|
|
87
88
|
return copy;
|
|
88
89
|
}
|
|
89
90
|
`;
|
|
90
|
-
|
|
91
|
+
export const CLIENT_OPTIONS_MODULE = "client-options";
|
|
92
|
+
const NO_DEFAULT_KEY = "aventara.build.client-options.no-default";
|
|
93
|
+
const OPTIONS_NO_DEFAULT_WARNING = "AVENTARA_CLIENT_OPTIONS_NO_DEFAULT";
|
|
94
|
+
export const NO_DEFAULT_WARNING = "src/aventara/client-options has no default export: the client runs without hooks. Export them as its default: export default { onCall, onSuccess, onError }.";
|
|
95
|
+
const CLIENT_OPTIONS_EXTENSIONS = [".ts", ".js"];
|
|
96
|
+
function optionsSpecifier(file, style) {
|
|
97
|
+
const base = `./${CLIENT_OPTIONS_MODULE}`;
|
|
98
|
+
if (path.extname(file) === ".ts") {
|
|
99
|
+
return importSpecifier(base, style);
|
|
100
|
+
}
|
|
101
|
+
return style.importFileExtension === "" ? base : `${base}.js`;
|
|
102
|
+
}
|
|
103
|
+
const ownedOption = (name) => JSON.stringify(`Error: src/aventara/client-options sets onCall, onSuccess and onError only; @/aventara/client sets ${name}`);
|
|
104
|
+
const HOOKS_SOURCE = `/** What src/aventara/client-options exports by default: the client's hooks. This module sets the rest. */
|
|
105
|
+
type ClientOptions = Pick<AvClientOptions, "onCall" | "onSuccess" | "onError"> & {
|
|
106
|
+
readonly fetch?: ${ownedOption("fetch")};
|
|
107
|
+
readonly grouping?: ${ownedOption("grouping")};
|
|
108
|
+
readonly entrypoint?: ${ownedOption("entrypoint")};
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/** The options module's default export, checked: the hooks, and nothing this module sets; none at all is fine. */
|
|
112
|
+
type CheckedDefault<D extends ClientOptions | undefined> = D;
|
|
113
|
+
type OptionsDefault = CheckedDefault<typeof optionsModule extends { readonly default: infer D } ? D : undefined>;
|
|
114
|
+
|
|
115
|
+
/** The process-wide mark that the missing default export was reported. */
|
|
116
|
+
const NO_DEFAULT_REPORTED = Symbol.for(${JSON.stringify(NO_DEFAULT_KEY)});
|
|
117
|
+
|
|
118
|
+
/** The options module's default export, read when a hook runs; a missing one is reported once. */
|
|
119
|
+
function clientOptions(): ClientOptions | undefined {
|
|
120
|
+
const options: unknown = (optionsModule as unknown as { readonly default?: unknown }).default;
|
|
121
|
+
if (typeof options === "object" && options !== null) {
|
|
122
|
+
return options as ClientOptions & OptionsDefault;
|
|
123
|
+
}
|
|
124
|
+
const holder = globalThis as { [key: symbol]: unknown };
|
|
125
|
+
if (holder[NO_DEFAULT_REPORTED] !== true) {
|
|
126
|
+
holder[NO_DEFAULT_REPORTED] = true;
|
|
127
|
+
const platform = globalThis as {
|
|
128
|
+
readonly process?: { readonly emitWarning?: (message: string, options: { readonly type: string; readonly code: string }) => void };
|
|
129
|
+
readonly console?: { warn(message: string): void };
|
|
130
|
+
};
|
|
131
|
+
const message = ${JSON.stringify(NO_DEFAULT_WARNING)};
|
|
132
|
+
if (typeof platform.process?.emitWarning === "function") {
|
|
133
|
+
platform.process.emitWarning(message, { type: "AventaraWarning", code: ${JSON.stringify(OPTIONS_NO_DEFAULT_WARNING)} });
|
|
134
|
+
} else {
|
|
135
|
+
platform.console?.warn("AventaraWarning: " + message);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The hooks every client this module makes runs, on either side: the options module's, read when each runs. */
|
|
142
|
+
const HOOKS: Pick<AvClientOptions, "onCall" | "onSuccess" | "onError"> = {
|
|
143
|
+
onCall: (call) => clientOptions()?.onCall?.(call),
|
|
144
|
+
onSuccess: (call, result) => clientOptions()?.onSuccess?.(call, result),
|
|
145
|
+
onError: (call, error) => clientOptions()?.onError?.(call, error),
|
|
146
|
+
};
|
|
147
|
+
`;
|
|
148
|
+
function optionsImport(options) {
|
|
149
|
+
return options === undefined
|
|
150
|
+
? ""
|
|
151
|
+
: `import * as optionsModule from ${JSON.stringify(options)};\n`;
|
|
152
|
+
}
|
|
153
|
+
function entrySource(style, accessVerbs, options) {
|
|
91
154
|
const from = (module) => JSON.stringify(importSpecifier(module, style));
|
|
92
155
|
const { tagKey, valueKey, objectTag, scalarTags } = HYDRATION_FORMAT;
|
|
93
156
|
const tagged = [...scalarTags, objectTag]
|
|
@@ -95,9 +158,9 @@ function entrySource(style, accessVerbs) {
|
|
|
95
158
|
.join(" | ");
|
|
96
159
|
return `${ENTRY_BANNER}
|
|
97
160
|
|
|
98
|
-
import { AvClient } from ${from(`./${GENERATED_DIRECTORY}/client`)};
|
|
161
|
+
import { AvClient${options === undefined ? "" : ", type AvClientOptions"} } from ${from(`./${GENERATED_DIRECTORY}/client`)};
|
|
99
162
|
import { type AvBuiltInScalar, decodeScalar } from ${from(`./${GENERATED_DIRECTORY}/runtime/codec`)};
|
|
100
|
-
|
|
163
|
+
${optionsImport(options)}
|
|
101
164
|
${clientEntryReExports(style, "type", accessVerbs)}
|
|
102
165
|
/** What a call made on the server is refused with. */
|
|
103
166
|
const SERVER_CALL = ${JSON.stringify(SERVER_CALL_REFUSAL)};
|
|
@@ -117,8 +180,8 @@ function refusal(): string | undefined {
|
|
|
117
180
|
|
|
118
181
|
type Call = (...args: unknown[]) => unknown;
|
|
119
182
|
|
|
120
|
-
/** The one client every call is made on. */
|
|
121
|
-
const CLIENT = new AvClient();
|
|
183
|
+
${options === undefined ? "" : HOOKS_SOURCE + "\n"}/** The one client every call is made on. */
|
|
184
|
+
const CLIENT = new AvClient(${options === undefined ? "" : "{ ...HOOKS }"});
|
|
122
185
|
|
|
123
186
|
/** The client a call made here and now is made on: the one client. */
|
|
124
187
|
function current(): AvClient {
|
|
@@ -197,15 +260,15 @@ export function avHydrate(dehydrated: unknown): unknown {
|
|
|
197
260
|
}
|
|
198
261
|
export const UNIVERSAL_PATH_KEY = "aventara.next.universal";
|
|
199
262
|
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.';
|
|
200
|
-
function universalSource(style, accessVerbs) {
|
|
263
|
+
function universalSource(style, accessVerbs, options) {
|
|
201
264
|
const from = (module) => JSON.stringify(importSpecifier(module, style));
|
|
202
265
|
return `${ENTRY_BANNER}
|
|
203
266
|
|
|
204
267
|
import { io } from ${JSON.stringify(style.importFileExtension === "" ? "next/cache" : "next/cache.js")};
|
|
205
268
|
import { cache } from "react";
|
|
206
|
-
import { AvClient } from ${from(`./${GENERATED_DIRECTORY}/client`)};
|
|
269
|
+
import { AvClient${options === undefined ? "" : ", type AvClientOptions"} } from ${from(`./${GENERATED_DIRECTORY}/client`)};
|
|
207
270
|
import type { AvFetch } from ${from(`./${GENERATED_DIRECTORY}/runtime/transport`)};
|
|
208
|
-
|
|
271
|
+
${optionsImport(options)}
|
|
209
272
|
import { avLazy as lazyCall } from ${from(`./${GENERATED_DIRECTORY}/runtime/operation-set`)};
|
|
210
273
|
|
|
211
274
|
/** The key the server's in-process path is registered under, on globalThis, by ready() from @aventara/next. */
|
|
@@ -242,8 +305,8 @@ async function universalFetch(...request: unknown[]): Promise<unknown> {
|
|
|
242
305
|
|
|
243
306
|
type Call = (...args: unknown[]) => unknown;
|
|
244
307
|
|
|
245
|
-
/** The browser's one client, whose calls are grouped, and the shape \`avClient\` copies. */
|
|
246
|
-
const BROWSER = new AvClient({ fetch: universalFetch as AvFetch });
|
|
308
|
+
${options === undefined ? "" : HOOKS_SOURCE + "\n"}/** The browser's one client, whose calls are grouped, and the shape \`avClient\` copies. */
|
|
309
|
+
const BROWSER = new AvClient({ ${options === undefined ? "" : "...HOOKS, "}fetch: universalFetch as AvFetch });
|
|
247
310
|
|
|
248
311
|
/**
|
|
249
312
|
* The client a call made here and now is made on: the browser's one; on the
|
|
@@ -252,7 +315,7 @@ const BROWSER = new AvClient({ fetch: universalFetch as AvFetch });
|
|
|
252
315
|
* nested in it), and shares no queue with any other call.
|
|
253
316
|
*/
|
|
254
317
|
function current(): AvClient {
|
|
255
|
-
return onServer() ? new AvClient({ fetch: universalFetch as AvFetch, grouping: false }) : BROWSER;
|
|
318
|
+
return onServer() ? new AvClient({ ${options === undefined ? "" : "...HOOKS, "}fetch: universalFetch as AvFetch, grouping: false }) : BROWSER;
|
|
256
319
|
}
|
|
257
320
|
|
|
258
321
|
/** On the server, the calls each avLazy made in the request being rendered (React's \`cache\`). */
|
|
@@ -369,6 +432,19 @@ export async function emitBrowserClient(projectDirectory, layout, source) {
|
|
|
369
432
|
moduleFormat: style.moduleFormat,
|
|
370
433
|
}, REGENERATE));
|
|
371
434
|
const accessVerbs = accessVerbsOf(acceptance.contract).length > 0;
|
|
435
|
+
const optionsModule = `${layout.folder}/${CLIENT_OPTIONS_MODULE}`;
|
|
436
|
+
const optionsFile = [".tsx", ".jsx"]
|
|
437
|
+
.map((extension) => `${optionsModule}${extension}`)
|
|
438
|
+
.find((file) => statSync(path.join(projectDirectory, file), {
|
|
439
|
+
throwIfNoEntry: false,
|
|
440
|
+
})?.isFile() === true) ?? optionalProjectFile(projectDirectory, optionsModule);
|
|
441
|
+
if (optionsFile !== undefined &&
|
|
442
|
+
!CLIENT_OPTIONS_EXTENSIONS.includes(path.extname(optionsFile))) {
|
|
443
|
+
throw new AvBuildError(`${optionsFile} is not read: the client's options module is ${layout.folder}/${CLIENT_OPTIONS_MODULE}.ts or .js, the two the entries import under every Next.js bundler; rename it.`);
|
|
444
|
+
}
|
|
445
|
+
const options = optionsFile === undefined
|
|
446
|
+
? undefined
|
|
447
|
+
: optionsSpecifier(optionsFile, style);
|
|
372
448
|
const decoder = new TextDecoder();
|
|
373
449
|
const tree = emission.tree
|
|
374
450
|
.filter((file) => file.path.startsWith(`${GENERATED_DIRECTORY}/`))
|
|
@@ -380,11 +456,11 @@ export async function emitBrowserClient(projectDirectory, layout, source) {
|
|
|
380
456
|
files: [
|
|
381
457
|
{
|
|
382
458
|
file: `${layout.folder}/${layout.entry}`,
|
|
383
|
-
text: entrySource(style, accessVerbs),
|
|
459
|
+
text: entrySource(style, accessVerbs, options),
|
|
384
460
|
},
|
|
385
461
|
{
|
|
386
462
|
file: `${layout.folder}/${layout.universal}`,
|
|
387
|
-
text: universalSource(style, accessVerbs),
|
|
463
|
+
text: universalSource(style, accessVerbs, options),
|
|
388
464
|
},
|
|
389
465
|
...tree,
|
|
390
466
|
],
|
|
@@ -413,16 +489,16 @@ function leftovers(projectDirectory, client) {
|
|
|
413
489
|
return filesUnder(projectDirectory, client.tree).filter((file) => !written.has(file) &&
|
|
414
490
|
carriesGeneratedOwnership(readGeneratedFile(projectDirectory, file) ?? ""));
|
|
415
491
|
}
|
|
416
|
-
export function writeBrowserClient(projectDirectory, client) {
|
|
492
|
+
export function writeBrowserClient(projectDirectory, client, writer) {
|
|
417
493
|
let write = "unchanged";
|
|
418
494
|
for (const { file, text } of client.files) {
|
|
419
|
-
if (
|
|
495
|
+
if (writer.write(file, text) === "written") {
|
|
420
496
|
write = "written";
|
|
421
497
|
}
|
|
422
498
|
}
|
|
423
499
|
const removed = leftovers(projectDirectory, client);
|
|
424
500
|
for (const file of removed) {
|
|
425
|
-
|
|
501
|
+
writer.remove(file);
|
|
426
502
|
}
|
|
427
503
|
return { write: removed.length > 0 ? "written" : write, removed };
|
|
428
504
|
}
|
|
@@ -435,7 +511,7 @@ export function checkBrowserClient(projectDirectory, client) {
|
|
|
435
511
|
if (current !== text) {
|
|
436
512
|
return {
|
|
437
513
|
file,
|
|
438
|
-
what:
|
|
514
|
+
what: `is stale: the ClientContract, the project's module settings or its ${CLIENT_OPTIONS_MODULE} module (added or removed) changed since it was written`,
|
|
439
515
|
};
|
|
440
516
|
}
|
|
441
517
|
}
|
|
@@ -22,7 +22,10 @@ export function emitComputedFieldsModule(configFile, reading) {
|
|
|
22
22
|
`// Not read: ${reading.reason.replaceAll("\n", " ")}. The stages that run before compute may leave any`,
|
|
23
23
|
"// create field out; write the configuration `as const` there to have it read.",
|
|
24
24
|
"export const computedFields = null;",
|
|
25
|
-
|
|
25
|
+
"",
|
|
26
|
+
"// What an adapter's discovery artifact carries on its model's type for the stages that run before compute:",
|
|
27
|
+
"// nothing, so they may leave any create field out.",
|
|
28
|
+
"export type AvComputedFields = undefined;",
|
|
26
29
|
].join("\n")}\n`;
|
|
27
30
|
}
|
|
28
31
|
return `${[
|
|
@@ -6,10 +6,13 @@ const AGENT_PARENTS = ["root", "application", "client"];
|
|
|
6
6
|
const PHASES = ["create", "update"];
|
|
7
7
|
class NotExact extends Error {
|
|
8
8
|
}
|
|
9
|
-
export function readConfigComputedFields(configFile, options, oldProgram) {
|
|
9
|
+
export function readConfigComputedFields(configFile, options, oldProgram, projectFiles = []) {
|
|
10
10
|
const file = path.resolve(configFile);
|
|
11
11
|
const program = ts.createProgram({
|
|
12
|
-
rootNames: [
|
|
12
|
+
rootNames: [
|
|
13
|
+
file,
|
|
14
|
+
...projectFiles.filter((name) => path.resolve(name) !== file),
|
|
15
|
+
],
|
|
13
16
|
options: { ...options, noEmit: true, skipLibCheck: true },
|
|
14
17
|
...(oldProgram === undefined ? {} : { oldProgram }),
|
|
15
18
|
});
|
|
@@ -44,11 +47,35 @@ export function readConfigComputedFields(configFile, options, oldProgram) {
|
|
|
44
47
|
}
|
|
45
48
|
catch (error) {
|
|
46
49
|
if (error instanceof NotExact) {
|
|
47
|
-
|
|
50
|
+
const errors = configurationErrors(program, file, options);
|
|
51
|
+
return errors.length > 0
|
|
52
|
+
? { kind: "ill-typed", reason: error.message, errors }
|
|
53
|
+
: { kind: "unread", reason: error.message };
|
|
48
54
|
}
|
|
49
55
|
throw error;
|
|
50
56
|
}
|
|
51
57
|
}
|
|
58
|
+
function configurationErrors(program, file, options) {
|
|
59
|
+
const reached = new Set(ts
|
|
60
|
+
.createProgram({
|
|
61
|
+
rootNames: [file],
|
|
62
|
+
options: { ...options, noEmit: true, skipLibCheck: true },
|
|
63
|
+
oldProgram: program,
|
|
64
|
+
})
|
|
65
|
+
.getSourceFiles()
|
|
66
|
+
.map((source) => path.resolve(source.fileName)));
|
|
67
|
+
return program
|
|
68
|
+
.getSourceFiles()
|
|
69
|
+
.filter((source) => reached.has(path.resolve(source.fileName)) &&
|
|
70
|
+
!source.isDeclarationFile &&
|
|
71
|
+
!program.isSourceFileFromExternalLibrary(source) &&
|
|
72
|
+
!source.fileName.includes("/node_modules/"))
|
|
73
|
+
.flatMap((source) => [
|
|
74
|
+
...program.getSyntacticDiagnostics(source),
|
|
75
|
+
...program.getSemanticDiagnostics(source),
|
|
76
|
+
])
|
|
77
|
+
.filter((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error);
|
|
78
|
+
}
|
|
52
79
|
function computedFieldsOfType(checker, config) {
|
|
53
80
|
assertExact(checker, config, "the configuration");
|
|
54
81
|
const overlays = Object.fromEntries(LAYERS.map((layer) => {
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { pathToFileURL } from "node:url";
|
|
3
3
|
import { createJiti } from "jiti";
|
|
4
|
-
import { readGeneratedFile
|
|
5
|
-
export function writeContractsFile(projectDirectory, contractsFile, compiled) {
|
|
6
|
-
return writeGeneratedFile(projectDirectory, contractsFile, compiled.source);
|
|
7
|
-
}
|
|
4
|
+
import { readGeneratedFile } from "../project/generated-file.js";
|
|
8
5
|
export async function checkContractsFile(projectDirectory, contractsFile, compiled, changesOf) {
|
|
9
6
|
const file = path.join(projectDirectory, contractsFile);
|
|
10
7
|
const text = readGeneratedFile(projectDirectory, contractsFile);
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdirSync, readFileSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { AvBuildError } from "../build.error.js";
|
|
5
|
+
const STALE_AFTER_MS = 10 * 60 * 1000;
|
|
6
|
+
export function buildLockFileOf(projectDirectory) {
|
|
7
|
+
return path.join(projectDirectory, "node_modules", ".cache", "aventara-build", "build.lock");
|
|
8
|
+
}
|
|
9
|
+
function running(pid) {
|
|
10
|
+
try {
|
|
11
|
+
process.kill(pid, 0);
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
return error.code === "EPERM";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function liveHolder(lock) {
|
|
19
|
+
try {
|
|
20
|
+
const holder = Number.parseInt(readFileSync(lock, "utf8"), 10);
|
|
21
|
+
const age = Date.now() - statSync(lock).mtimeMs;
|
|
22
|
+
return Number.isInteger(holder) &&
|
|
23
|
+
holder !== process.pid &&
|
|
24
|
+
age < STALE_AFTER_MS &&
|
|
25
|
+
running(holder)
|
|
26
|
+
? holder
|
|
27
|
+
: undefined;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function acquireBuildLock(projectDirectory) {
|
|
34
|
+
const lock = buildLockFileOf(projectDirectory);
|
|
35
|
+
const shown = path.relative(projectDirectory, lock);
|
|
36
|
+
const unwritable = (error) => {
|
|
37
|
+
const { code, path: at } = error;
|
|
38
|
+
const where = at === undefined
|
|
39
|
+
? ""
|
|
40
|
+
: ` at ${path.relative(projectDirectory, at) || "."}`;
|
|
41
|
+
return new AvBuildError(`this project's lock file ${shown} cannot be written (${code ?? String(error)}${where}). Make node_modules a writable folder (the package manager's install makes one) and run it again.`);
|
|
42
|
+
};
|
|
43
|
+
try {
|
|
44
|
+
mkdirSync(path.dirname(lock), { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
throw unwritable(error);
|
|
48
|
+
}
|
|
49
|
+
const held = `${process.pid} ${randomUUID()}`;
|
|
50
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
51
|
+
try {
|
|
52
|
+
writeFileSync(lock, held, { flag: "wx" });
|
|
53
|
+
return () => {
|
|
54
|
+
try {
|
|
55
|
+
if (readFileSync(lock, "utf8") === held) {
|
|
56
|
+
rmSync(lock, { force: true });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (error.code !== "EEXIST") {
|
|
65
|
+
throw unwritable(error);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const holder = liveHolder(lock);
|
|
69
|
+
if (holder !== undefined) {
|
|
70
|
+
throw new AvBuildError(`another aventara-build (process ${holder}) is writing this project's generated files. Run it again when that one has finished, or delete ${shown} if no build is running.`);
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
rmSync(lock, { force: true });
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
throw unwritable(error);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
throw new AvBuildError("another aventara-build took this project's lock at the same moment. Run it again.");
|
|
80
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmdirSync, rmSync, writeFileSync, } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
export function readGeneratedFile(projectDirectory, file) {
|
|
4
4
|
try {
|
|
@@ -8,12 +8,96 @@ export function readGeneratedFile(projectDirectory, file) {
|
|
|
8
8
|
return undefined;
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
|
+
export function replaceFile(target, text, rename = renameSync) {
|
|
12
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
13
|
+
try {
|
|
14
|
+
writeFileSync(temporary, text);
|
|
15
|
+
for (let attempt = 1;; attempt += 1) {
|
|
16
|
+
try {
|
|
17
|
+
rename(temporary, target);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
const code = error.code;
|
|
22
|
+
if (code !== "EPERM" && code !== "EBUSY" && code !== "EACCES") {
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
if (attempt === RENAME_ATTEMPTS) {
|
|
26
|
+
writeFileSync(target, text);
|
|
27
|
+
rmSync(temporary, { force: true });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
Atomics.wait(PAUSE, 0, 0, RENAME_PAUSE_MS);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
rmSync(temporary, { force: true });
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const RENAME_ATTEMPTS = 5;
|
|
40
|
+
const RENAME_PAUSE_MS = 50;
|
|
41
|
+
const PAUSE = new Int32Array(new SharedArrayBuffer(4));
|
|
11
42
|
export function writeGeneratedFile(projectDirectory, file, text) {
|
|
12
43
|
if (readGeneratedFile(projectDirectory, file) === text) {
|
|
13
44
|
return "unchanged";
|
|
14
45
|
}
|
|
15
46
|
const target = path.join(projectDirectory, file);
|
|
16
47
|
mkdirSync(path.dirname(target), { recursive: true });
|
|
17
|
-
|
|
48
|
+
replaceFile(target, text);
|
|
18
49
|
return "written";
|
|
19
50
|
}
|
|
51
|
+
export function generatedFileBatch(projectDirectory) {
|
|
52
|
+
const before = new Map();
|
|
53
|
+
const made = [];
|
|
54
|
+
const remember = (file) => {
|
|
55
|
+
if (!before.has(file)) {
|
|
56
|
+
before.set(file, readGeneratedFile(projectDirectory, file));
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
write: (file, text) => {
|
|
61
|
+
if (readGeneratedFile(projectDirectory, file) === text) {
|
|
62
|
+
return "unchanged";
|
|
63
|
+
}
|
|
64
|
+
remember(file);
|
|
65
|
+
const missing = [];
|
|
66
|
+
for (let folder = path.dirname(path.join(projectDirectory, file)); !existsSync(folder) && folder !== path.dirname(folder); folder = path.dirname(folder)) {
|
|
67
|
+
missing.unshift(folder);
|
|
68
|
+
}
|
|
69
|
+
made.push(...missing);
|
|
70
|
+
return writeGeneratedFile(projectDirectory, file, text);
|
|
71
|
+
},
|
|
72
|
+
remove: (file) => {
|
|
73
|
+
remember(file);
|
|
74
|
+
rmSync(path.join(projectDirectory, file), { force: true });
|
|
75
|
+
},
|
|
76
|
+
restore: () => {
|
|
77
|
+
const failures = [];
|
|
78
|
+
for (const [file, text] of before) {
|
|
79
|
+
const target = path.join(projectDirectory, file);
|
|
80
|
+
try {
|
|
81
|
+
if (text === undefined) {
|
|
82
|
+
rmSync(target, { force: true });
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
86
|
+
replaceFile(target, text);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
failures.push({ file, error });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
for (const folder of [...made].reverse()) {
|
|
94
|
+
try {
|
|
95
|
+
rmdirSync(folder);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return failures;
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -7,19 +7,23 @@ const EXTENSIONS = [".js", ".ts", ".mjs", ".cjs", ".mts", ".cts"];
|
|
|
7
7
|
const EXTENSIONS_AS_LISTED = [".ts", ".mts", ".cts", ".js", ".mjs", ".cjs"];
|
|
8
8
|
const SERVER_MARKERS = { "server-only": {} };
|
|
9
9
|
const CONFIG_EXPORTS = ["aventaraConfig", "default"];
|
|
10
|
-
function
|
|
10
|
+
export function optionalProjectFile(projectDirectory, module) {
|
|
11
11
|
const present = EXTENSIONS.map((extension) => `${module}${extension}`).filter((file) => statSync(path.join(projectDirectory, file), {
|
|
12
12
|
throwIfNoEntry: false,
|
|
13
13
|
})?.isFile() === true);
|
|
14
|
-
if (present.length === 0) {
|
|
15
|
-
const [first, ...others] = EXTENSIONS_AS_LISTED;
|
|
16
|
-
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).`);
|
|
17
|
-
}
|
|
18
14
|
if (present.length > 1) {
|
|
19
15
|
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" : ""}.`);
|
|
20
16
|
}
|
|
21
17
|
return present[0];
|
|
22
18
|
}
|
|
19
|
+
function projectFile(projectDirectory, module, layout) {
|
|
20
|
+
const file = optionalProjectFile(projectDirectory, module);
|
|
21
|
+
if (file === undefined) {
|
|
22
|
+
const [first, ...others] = EXTENSIONS_AS_LISTED;
|
|
23
|
+
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).`);
|
|
24
|
+
}
|
|
25
|
+
return file;
|
|
26
|
+
}
|
|
23
27
|
async function evaluate(jiti, projectDirectory, file) {
|
|
24
28
|
try {
|
|
25
29
|
return await jiti.import(path.join(projectDirectory, file));
|
|
@@ -22,6 +22,18 @@ export function projectCompilerOptions(projectDirectory) {
|
|
|
22
22
|
}
|
|
23
23
|
return ts.parseJsonConfigFileContent(read.config, ts.sys, path.dirname(tsconfig)).options;
|
|
24
24
|
}
|
|
25
|
+
export function projectSourceFiles(projectDirectory) {
|
|
26
|
+
assertClassicCompilerApi();
|
|
27
|
+
const tsconfig = ts.findConfigFile(projectDirectory, ts.sys.fileExists, "tsconfig.json");
|
|
28
|
+
if (tsconfig === undefined) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
const read = ts.readConfigFile(tsconfig, ts.sys.readFile);
|
|
32
|
+
return read.error === undefined
|
|
33
|
+
? ts.parseJsonConfigFileContent(read.config, ts.sys, path.dirname(tsconfig))
|
|
34
|
+
.fileNames
|
|
35
|
+
: [];
|
|
36
|
+
}
|
|
25
37
|
export function importSuffixOf(options, directory) {
|
|
26
38
|
if (options.allowImportingTsExtensions === true ||
|
|
27
39
|
options
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aventara/build",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.13",
|
|
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.
|
|
19
|
+
"@aventara/client": "1.0.0-rc.13",
|
|
20
20
|
"jiti": "2.7.0"
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
23
|
-
"@aventara/core": "1.0.0-rc.
|
|
23
|
+
"@aventara/core": "1.0.0-rc.13",
|
|
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.
|
|
30
|
+
"@aventara/core": "1.0.0-rc.13",
|
|
31
31
|
"@types/node": "24.10.1",
|
|
32
32
|
"typescript": "^5.9.2"
|
|
33
33
|
},
|