@aventara/build 0.0.0-stage → 1.0.0-rc.11

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.
@@ -0,0 +1,117 @@
1
+ import { statSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { createJiti } from "jiti";
5
+ import { AvBuildError } from "../build.error.js";
6
+ const EXTENSIONS = [".js", ".ts", ".mjs", ".cjs", ".mts", ".cts"];
7
+ const EXTENSIONS_AS_LISTED = [".ts", ".mts", ".cts", ".js", ".mjs", ".cjs"];
8
+ const SERVER_MARKERS = { "server-only": {} };
9
+ const CONFIG_EXPORTS = ["aventaraConfig", "default"];
10
+ function projectFile(projectDirectory, module, layout) {
11
+ const present = EXTENSIONS.map((extension) => `${module}${extension}`).filter((file) => statSync(path.join(projectDirectory, file), {
12
+ throwIfNoEntry: false,
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
+ if (present.length > 1) {
19
+ 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
+ }
21
+ return present[0];
22
+ }
23
+ async function evaluate(jiti, projectDirectory, file) {
24
+ try {
25
+ return await jiti.import(path.join(projectDirectory, file));
26
+ }
27
+ catch (error) {
28
+ const message = error instanceof Error ? error.message : String(error);
29
+ throw new AvBuildError(`${file} could not be run: ${message.split("\n", 1)[0]}. aventara-build runs it at build time, so its top-level code must run without a server, a database client or a request.`, { cause: error });
30
+ }
31
+ }
32
+ function isRecord(value) {
33
+ return typeof value === "object" && value !== null && !Array.isArray(value);
34
+ }
35
+ function configOf(file, namespace) {
36
+ const name = CONFIG_EXPORTS.find((candidate) => Object.hasOwn(namespace, candidate));
37
+ if (name === undefined) {
38
+ throw new AvBuildError(`${file} exports neither \`aventaraConfig\` nor a default: export the configuration as \`export const aventaraConfig = { … } as const\`.`);
39
+ }
40
+ const shown = name === "default" ? "its default export" : `\`${name}\``;
41
+ const value = namespace[name];
42
+ if (typeof value === "function") {
43
+ throw new AvBuildError(`${shown} in ${file} is a function. aventara-build runs the configuration without a database client, so export the configuration itself, as a value without \`adapter\`, and join the adapter where the framework is created (\`{ ...aventaraConfig, adapter }\`).`);
44
+ }
45
+ if (!isRecord(value)) {
46
+ throw new AvBuildError(`${shown} in ${file} is not an object: it must be the configuration, \`{ api, … } as const\`.`);
47
+ }
48
+ if (Object.hasOwn(value, "adapter")) {
49
+ throw new AvBuildError(`${shown} in ${file} builds its own adapter (\`adapter\`). Export the configuration without it and join the adapter where the framework is created: \`{ ...aventaraConfig, adapter: await createPrismaAdapter({ ...adapter, client }) }\`.`);
50
+ }
51
+ if (Object.hasOwn(value, "prebuilt")) {
52
+ throw new AvBuildError(`${shown} in ${file} sets \`prebuilt\`, which is this build's output: pass it where the framework is created.`);
53
+ }
54
+ return value;
55
+ }
56
+ const TRANSACTIONS = [
57
+ "interactive",
58
+ "none",
59
+ ];
60
+ function descriptionOf(file, namespace) {
61
+ const shape = "`export default describePrismaAdapter({ discovery, provider, driver })` (or your adapter's describe function)";
62
+ if (!Object.hasOwn(namespace, "default")) {
63
+ throw new AvBuildError(`${file} has no default export; it must be the adapter's description: ${shape}.`);
64
+ }
65
+ const value = namespace.default;
66
+ if (!isRecord(value)) {
67
+ throw new AvBuildError(`${file}'s default export is not an object; it must be the adapter's description: ${shape}.`);
68
+ }
69
+ const wrong = (member, expected) => new AvBuildError(`${file}'s default export is not an adapter description: its \`${member}\` is not ${expected}. It must be ${shape}.`);
70
+ if (!isRecord(value.model)) {
71
+ throw wrong("model", "an object");
72
+ }
73
+ if (!TRANSACTIONS.includes(value.transactions)) {
74
+ throw wrong("transactions", '"interactive" or "none"');
75
+ }
76
+ for (const member of ["referentialActions", "computedFields"]) {
77
+ if (value[member] !== undefined && !isRecord(value[member])) {
78
+ throw wrong(member, "an object");
79
+ }
80
+ }
81
+ if (value.sqlStatements !== undefined &&
82
+ typeof value.sqlStatements !== "boolean") {
83
+ throw wrong("sqlStatements", "a boolean");
84
+ }
85
+ return value;
86
+ }
87
+ async function projectCore(jiti) {
88
+ try {
89
+ const [core, internal] = await Promise.all([
90
+ jiti.import("@aventara/core"),
91
+ jiti.import("@aventara/core/internal"),
92
+ ]);
93
+ return { core, internal };
94
+ }
95
+ catch (error) {
96
+ throw new AvBuildError(`@aventara/core could not be loaded from the project (${error instanceof Error ? error.message.split("\n", 1)[0] : String(error)}); install it beside @aventara/build.`, { cause: error });
97
+ }
98
+ }
99
+ export function findProjectModules(projectDirectory, layout) {
100
+ return {
101
+ configFile: projectFile(projectDirectory, layout.config, layout),
102
+ adapterFile: projectFile(projectDirectory, layout.adapter, layout),
103
+ };
104
+ }
105
+ export async function loadProjectModules(projectDirectory, { configFile, adapterFile }) {
106
+ const jiti = createJiti(pathToFileURL(path.join(projectDirectory, "package.json")).href, {
107
+ moduleCache: false,
108
+ fsCache: false,
109
+ interopDefault: false,
110
+ nativeModules: ["@aventara/core"],
111
+ virtualModules: SERVER_MARKERS,
112
+ });
113
+ const project = await projectCore(jiti);
114
+ const config = configOf(configFile, await evaluate(jiti, projectDirectory, configFile));
115
+ const description = descriptionOf(adapterFile, await evaluate(jiti, projectDirectory, adapterFile));
116
+ return { ...project, configFile, config, adapterFile, description };
117
+ }
@@ -0,0 +1,73 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import ts from "typescript";
4
+ import { AvBuildError } from "../build.error.js";
5
+ function defaultOptions() {
6
+ return {
7
+ target: ts.ScriptTarget.ES2022,
8
+ module: ts.ModuleKind.NodeNext,
9
+ moduleResolution: ts.ModuleResolutionKind.NodeNext,
10
+ strict: true,
11
+ };
12
+ }
13
+ export function projectCompilerOptions(projectDirectory) {
14
+ assertClassicCompilerApi();
15
+ const tsconfig = ts.findConfigFile(projectDirectory, ts.sys.fileExists, "tsconfig.json");
16
+ if (tsconfig === undefined) {
17
+ return defaultOptions();
18
+ }
19
+ const read = ts.readConfigFile(tsconfig, ts.sys.readFile);
20
+ if (read.error !== undefined) {
21
+ return defaultOptions();
22
+ }
23
+ return ts.parseJsonConfigFileContent(read.config, ts.sys, path.dirname(tsconfig)).options;
24
+ }
25
+ export function importSuffixOf(options, directory) {
26
+ if (options.allowImportingTsExtensions === true ||
27
+ options
28
+ .rewriteRelativeImportExtensions === true) {
29
+ return ".ts";
30
+ }
31
+ if (options.module === ts.ModuleKind.CommonJS ||
32
+ options.moduleResolution === ts.ModuleResolutionKind.Bundler) {
33
+ return "";
34
+ }
35
+ if (options.module === ts.ModuleKind.Node16 ||
36
+ options.module === ts.ModuleKind.NodeNext) {
37
+ return packageType(directory) === "module" ? ".js" : "";
38
+ }
39
+ return ".js";
40
+ }
41
+ function packageType(directory) {
42
+ for (let current = path.resolve(directory);;) {
43
+ const manifest = path.join(current, "package.json");
44
+ if (existsSync(manifest)) {
45
+ try {
46
+ return JSON.parse(readFileSync(manifest, "utf8")).type;
47
+ }
48
+ catch {
49
+ return undefined;
50
+ }
51
+ }
52
+ const parent = path.dirname(current);
53
+ if (parent === current) {
54
+ return undefined;
55
+ }
56
+ current = parent;
57
+ }
58
+ }
59
+ export function relativeSpecifier(from, to, suffix) {
60
+ const relative = path
61
+ .relative(path.dirname(from), to.replace(/\.ts$/, ""))
62
+ .split(path.sep)
63
+ .join("/");
64
+ return `${relative.startsWith(".") ? relative : `./${relative}`}${suffix}`;
65
+ }
66
+ function assertClassicCompilerApi() {
67
+ const compiler = ts;
68
+ if (typeof compiler?.createProgram === "function" &&
69
+ typeof compiler.findConfigFile === "function") {
70
+ return;
71
+ }
72
+ throw new AvBuildError(`the installed \`typescript\` ${compiler?.version ?? "(no version)"} has no classic compiler API (\`createProgram\`), which aventara-build reads the project's Resources and configuration with; install a \`typescript\` below 7.`);
73
+ }
@@ -0,0 +1,111 @@
1
+ import { globSync } from "node:fs";
2
+ import path from "node:path";
3
+ import ts from "typescript";
4
+ import { AvBuildError } from "../build.error.js";
5
+ export const RESOURCES_GLOB = "src/resources/**/*.resource.ts";
6
+ export const DEFINITION_LAYERS = [
7
+ "root",
8
+ "application",
9
+ "client",
10
+ "agent",
11
+ ];
12
+ const DEFINED_RESOURCE = "AvDefinedResource";
13
+ function matchedFiles(projectDirectory) {
14
+ return globSync(RESOURCES_GLOB, { cwd: projectDirectory })
15
+ .map((file) => file.split(path.sep).join("/"))
16
+ .sort();
17
+ }
18
+ function listed(items) {
19
+ return items.length < 2
20
+ ? items.join("")
21
+ : `${items.slice(0, -1).join(", ")} and ${items.at(-1)}`;
22
+ }
23
+ export function readResourceDefinitions(projectDirectory, options) {
24
+ const files = matchedFiles(projectDirectory);
25
+ const program = ts.createProgram({
26
+ rootNames: files.map((file) => path.join(projectDirectory, file)),
27
+ options: { ...options, noEmit: true, skipLibCheck: true },
28
+ });
29
+ const checker = program.getTypeChecker();
30
+ const found = new Map();
31
+ const places = new Map();
32
+ const withoutDefinition = [];
33
+ for (const file of files) {
34
+ const source = program.getSourceFile(path.join(projectDirectory, file));
35
+ const module = source === undefined ? undefined : checker.getSymbolAtLocation(source);
36
+ let defines = false;
37
+ for (const exported of module === undefined
38
+ ? []
39
+ : checker.getExportsOfModule(module)) {
40
+ const target = exported.flags & ts.SymbolFlags.Alias
41
+ ? checker.getAliasedSymbol(exported)
42
+ : exported;
43
+ if ((target.flags & ts.SymbolFlags.Value) === 0) {
44
+ continue;
45
+ }
46
+ const type = checker.getTypeOfSymbol(target);
47
+ if (type.aliasSymbol?.escapedName !== DEFINED_RESOURCE) {
48
+ continue;
49
+ }
50
+ defines = true;
51
+ const exportName = exported.escapedName;
52
+ if (!found.has(target)) {
53
+ const [named] = type.aliasTypeArguments ?? [];
54
+ if (named === undefined || !named.isStringLiteral()) {
55
+ throw new AvBuildError(`\`${exportName}\` in ${file} defines no single Resource (\`${named === undefined ? "?" : checker.typeToString(named)}\`): name the Resource as a string, \`defineResource<Model>()("Post", …)\`.`);
56
+ }
57
+ const properties = new Set(type.getProperties().map((property) => property.getName()));
58
+ found.set(target, {
59
+ resource: named.value,
60
+ layers: DEFINITION_LAYERS.filter((layer) => properties.has(layer)),
61
+ });
62
+ }
63
+ places.set(target, [...(places.get(target) ?? []), { file, exportName }]);
64
+ }
65
+ if (!defines) {
66
+ withoutDefinition.push(file);
67
+ }
68
+ }
69
+ const byResource = new Map();
70
+ for (const [symbol, { resource, layers }] of found) {
71
+ const all = places.get(symbol) ?? [];
72
+ const declaredIn = symbol.declarations?.[0]?.getSourceFile().fileName;
73
+ const place = all.find((candidate) => declaredIn !== undefined &&
74
+ path.resolve(projectDirectory, candidate.file) ===
75
+ path.resolve(declaredIn)) ?? all[0];
76
+ if (place === undefined) {
77
+ continue;
78
+ }
79
+ byResource.set(resource, [
80
+ ...(byResource.get(resource) ?? []),
81
+ { ...place, resource, layers },
82
+ ]);
83
+ }
84
+ const doubled = [...byResource.entries()]
85
+ .filter(([, definitions]) => definitions.length > 1)
86
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
87
+ if (doubled.length > 0) {
88
+ throw new AvBuildError(doubled
89
+ .map(([resource, definitions]) => `${resource} is defined ${definitions.length === 2 ? "twice" : `${definitions.length} times`}: by ${listed([...definitions]
90
+ .sort((left, right) => left.file === right.file
91
+ ? left.exportName < right.exportName
92
+ ? -1
93
+ : 1
94
+ : left.file < right.file
95
+ ? -1
96
+ : 1)
97
+ .map((definition) => `\`${definition.exportName}\` in ${definition.file}`))}. Keep one defineResource(${JSON.stringify(resource)}, …) and move the other's blocks into it.`)
98
+ .join("\n"));
99
+ }
100
+ return {
101
+ definitions: [...byResource.values()]
102
+ .map((definitions) => definitions[0])
103
+ .sort((left, right) => left.resource < right.resource
104
+ ? -1
105
+ : left.resource > right.resource
106
+ ? 1
107
+ : 0),
108
+ withoutDefinition,
109
+ program,
110
+ };
111
+ }
@@ -0,0 +1,48 @@
1
+ import path from "node:path";
2
+ import { relativeSpecifier, } from "../project/tsconfig.reader.js";
3
+ import { DEFINITION_LAYERS, RESOURCES_GLOB, } from "./resource-definitions.reader.js";
4
+ function localNames(definitions) {
5
+ const taken = new Set(["aventaraResources"]);
6
+ return definitions.map((definition) => {
7
+ const preferred = definition.exportName === "default"
8
+ ? `${definition.resource.charAt(0).toLowerCase()}${definition.resource.slice(1)}Resource`
9
+ : definition.exportName;
10
+ let name = preferred;
11
+ for (let index = 2; taken.has(name); index += 1) {
12
+ name = `${preferred}${index}`;
13
+ }
14
+ taken.add(name);
15
+ return name;
16
+ });
17
+ }
18
+ export function emitResourcesModule(projectDirectory, resourcesFile, definitions, suffix) {
19
+ const file = path.join(projectDirectory, resourcesFile);
20
+ const names = localNames(definitions);
21
+ const imports = definitions.map((definition, index) => {
22
+ const local = names[index];
23
+ const imported = definition.exportName === local
24
+ ? local
25
+ : `${definition.exportName} as ${local}`;
26
+ return `import { ${imported} } from ${JSON.stringify(relativeSpecifier(file, path.join(projectDirectory, definition.file), suffix))};`;
27
+ });
28
+ const layers = DEFINITION_LAYERS.flatMap((layer) => {
29
+ const spreads = definitions.flatMap((definition, index) => definition.layers.includes(layer)
30
+ ? [`...${names[index]}.${layer}`]
31
+ : []);
32
+ return layer === "root" || spreads.length > 0
33
+ ? [
34
+ `\t${layer}: {${spreads.length === 0 ? "" : ` ${spreads.join(", ")} `}},`,
35
+ ]
36
+ : [];
37
+ });
38
+ return `${[
39
+ `// Generated by aventara-build from ${RESOURCES_GLOB}. Do not edit: regenerate it (aventara:prepare).`,
40
+ ...imports,
41
+ "",
42
+ "// Each layer's `resources` tree, every `defineResource(…)` result spread into it:",
43
+ "// `resources: aventaraResources.root`, `client: { resources: aventaraResources.client }`.",
44
+ "export const aventaraResources = {",
45
+ ...layers,
46
+ "} as const;",
47
+ ].join("\n")}\n`;
48
+ }
package/package.json CHANGED
@@ -1,6 +1,41 @@
1
1
  {
2
2
  "name": "@aventara/build",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.0.0-rc.11",
4
+ "license": "SEE LICENSE IN LICENSE",
5
+ "description": "Compiles an Aventara application's Contracts at build time, for serverless and edge cold starts.",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": "^22.23.4 || >=24.22.1"
9
+ },
10
+ "bin": {
11
+ "aventara-build": "./dist/aventara-build.bin.js"
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "LICENSE-ADDITIONAL-PERMISSION.md",
16
+ "CHANGELOG.md"
17
+ ],
18
+ "dependencies": {
19
+ "@aventara/client": "1.0.0-rc.11",
20
+ "jiti": "2.7.0"
21
+ },
22
+ "peerDependencies": {
23
+ "@aventara/core": "1.0.0-rc.11",
24
+ "typescript": ">=5.7.0 <7"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "devDependencies": {
30
+ "@aventara/core": "1.0.0-rc.11",
31
+ "@types/node": "24.10.1",
32
+ "typescript": "^5.9.2"
33
+ },
34
+ "scripts": {
35
+ "prebuild": "node ../../scripts/build/host-catalog.generator.ts --out src/project/host.catalog.generated.ts",
36
+ "build": "node ../../scripts/build/build-output.cleaner.ts dist && tsc -p tsconfig.json --removeComments",
37
+ "typecheck": "tsc -p tsconfig.typecheck.json --noEmit",
38
+ "test": "vitest run --typecheck --config ../../vitest.config.mts --root .",
39
+ "lint": "biome lint src"
40
+ }
6
41
  }