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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,39 @@
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.4",
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.18.0 || >=24.2.0"
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
+ "jiti": "2.7.0"
20
+ },
21
+ "peerDependencies": {
22
+ "@aventara/core": "1.0.0-rc.4",
23
+ "typescript": ">=5.7.0 <7"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "devDependencies": {
29
+ "@aventara/core": "1.0.0-rc.4",
30
+ "@types/node": "24.10.1",
31
+ "typescript": "^5.9.2"
32
+ },
33
+ "scripts": {
34
+ "build": "node ../../scripts/build/build-output.cleaner.ts dist && tsc -p tsconfig.json --removeComments",
35
+ "typecheck": "tsc -p tsconfig.typecheck.json --noEmit",
36
+ "test": "vitest run --typecheck --config ../../vitest.config.mts --root .",
37
+ "lint": "biome lint src"
38
+ }
6
39
  }