@aventara/build 0.0.0-stage → 1.0.0-rc.8
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 +123 -0
- package/LICENSE +91 -0
- package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
- package/README.md +138 -2
- package/dist/aventara-build.bin.js +5 -0
- package/dist/build.error.js +3 -0
- package/dist/cli.js +168 -0
- package/dist/computed/computed-fields-file.js +37 -0
- package/dist/computed/computed-fields.reader.js +184 -0
- package/dist/contracts/contracts-file.js +45 -0
- package/dist/contracts/prebuilt-contracts.compiler.js +49 -0
- package/dist/node-version.guard.js +47 -0
- package/dist/project/generated-file.js +19 -0
- package/dist/project/generated-files.resolver.js +48 -0
- package/dist/project/project-modules.loader.js +119 -0
- package/dist/project/tsconfig.reader.js +73 -0
- package/dist/resources/resource-definitions.reader.js +111 -0
- package/dist/resources/resources-file.js +48 -0
- package/package.json +36 -3
|
@@ -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": "
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
3
|
+
"version": "1.0.0-rc.8",
|
|
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.8",
|
|
23
|
+
"typescript": ">=5.7.0 <7"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@aventara/core": "1.0.0-rc.8",
|
|
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
|
}
|