@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 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. A server warns
81
- `AVENTARA_STALE_GENERATED_TYPES` at startup when the configuration computes other fields than the file says.
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, nothing is written, and the command exits 1.
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, writeContractsFile, } from "./contracts/contracts-file.js";
7
+ import { checkContractsFile } from "./contracts/contracts-file.js";
7
8
  import { compilePrebuiltContracts } from "./contracts/prebuilt-contracts.compiler.js";
8
- import { readGeneratedFile, writeGeneratedFile, } from "./project/generated-file.js";
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
- output.stdout(`aventara-build: environment from ${listed(env.loaded)} (a variable already set keeps its value).`);
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
- output.stdout(`aventara-build: ${file} exports no defineResource(…) result; it is not in aventaraResources.`);
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
- output.stdout(wrote(writeGeneratedFile(projectDirectory, generated.resources, tree), generated.resources, counted(found.definitions.length)));
189
+ noteWrite(wrote(batch.write(generated.resources, tree), generated.resources, counted(found.definitions.length)));
140
190
  }
141
- if (check) {
142
- if (readGeneratedFile(projectDirectory, generated.computedFields) ===
143
- undefined) {
144
- output.stderr(refusal({ file: generated.computedFields, what: "is missing" }));
145
- return 1;
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
- else if (readGeneratedFile(projectDirectory, generated.computedFields) ===
149
- undefined) {
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
- const computed = emitComputedFieldsModule(files.configFile, reading);
157
- if (check) {
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
- output.stdout(wrote(writeGeneratedFile(projectDirectory, generated.computedFields, computed), generated.computedFields));
231
+ noteWrite(wrote(batch.write(generated.computedFields, computed), generated.computedFields));
166
232
  if (reading.kind === "unread") {
167
- 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.`);
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
- output.stdout(wrote(writeContractsFile(projectDirectory, generated.contracts, compiled), generated.contracts, counted(resources)));
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
- output.stdout(`aventara-build: ${warning}`);
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
- output.stdout(`aventara-build: removed ${file}, which this ClientContract's browser client no longer has.`);
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
- output.stdout(wrote(written.write, `${entry?.file}, ${universal?.file} and ${browserClient.tree}/`, `the browser client, calling ${browserClient.entrypoint === "" ? "/" : browserClient.entrypoint}`));
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
- output.stdout(browserClient === undefined
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
- if (error instanceof AvBuildError) {
220
- output.stderr(`aventara-build: ${error.message}`);
221
- return 1;
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
- throw error;
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 instanceof Error && error.stack) || String(error)}\n`);
314
+ process.stderr.write(`${defectText(error)}\n`);
238
315
  process.exitCode = 1;
239
316
  }
240
317
  }
@@ -1,8 +1,9 @@
1
- import { readdirSync, rmSync } from "node:fs";
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, writeGeneratedFile, } from "../project/generated-file.js";
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
- function entrySource(style, accessVerbs) {
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 (writeGeneratedFile(projectDirectory, file, text) === "written") {
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
- rmSync(path.join(projectDirectory, file));
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: "is stale: the ClientContract or the project's module settings changed since it was written",
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
- ...type,
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: [file],
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
- return { kind: "unread", reason: error.message };
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, writeGeneratedFile, } from "../project/generated-file.js";
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
- writeFileSync(target, text);
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 projectFile(projectDirectory, module, layout) {
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.11",
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.11",
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.11",
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.11",
30
+ "@aventara/core": "1.0.0-rc.13",
31
31
  "@types/node": "24.10.1",
32
32
  "typescript": "^5.9.2"
33
33
  },