@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,184 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
const CONFIG_EXPORTS = ["aventaraConfig", "default"];
|
|
4
|
+
const LAYERS = ["application", "client", "agent"];
|
|
5
|
+
const AGENT_PARENTS = ["root", "application", "client"];
|
|
6
|
+
const PHASES = ["create", "update"];
|
|
7
|
+
class NotExact extends Error {
|
|
8
|
+
}
|
|
9
|
+
export function readConfigComputedFields(configFile, options, oldProgram) {
|
|
10
|
+
const file = path.resolve(configFile);
|
|
11
|
+
const program = ts.createProgram({
|
|
12
|
+
rootNames: [file],
|
|
13
|
+
options: { ...options, noEmit: true, skipLibCheck: true },
|
|
14
|
+
...(oldProgram === undefined ? {} : { oldProgram }),
|
|
15
|
+
});
|
|
16
|
+
const checker = program.getTypeChecker();
|
|
17
|
+
const source = program.getSourceFile(file);
|
|
18
|
+
const module = source === undefined ? undefined : checker.getSymbolAtLocation(source);
|
|
19
|
+
const exported = module === undefined
|
|
20
|
+
? undefined
|
|
21
|
+
: CONFIG_EXPORTS.map((name) => checker
|
|
22
|
+
.getExportsOfModule(module)
|
|
23
|
+
.find((symbol) => symbol.escapedName === name)).find((symbol) => symbol !== undefined);
|
|
24
|
+
if (exported === undefined) {
|
|
25
|
+
return {
|
|
26
|
+
kind: "unread",
|
|
27
|
+
reason: "it exports neither `aventaraConfig` nor a default",
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const target = exported.flags & ts.SymbolFlags.Alias
|
|
31
|
+
? checker.getAliasedSymbol(exported)
|
|
32
|
+
: exported;
|
|
33
|
+
const declared = checker.getTypeOfSymbol(target);
|
|
34
|
+
const [signature] = declared.getCallSignatures();
|
|
35
|
+
const config = signature === undefined
|
|
36
|
+
? declared
|
|
37
|
+
: (checker.getAwaitedType(signature.getReturnType()) ??
|
|
38
|
+
signature.getReturnType());
|
|
39
|
+
try {
|
|
40
|
+
return {
|
|
41
|
+
kind: "read",
|
|
42
|
+
computedFields: computedFieldsOfType(checker, config),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (error instanceof NotExact) {
|
|
47
|
+
return { kind: "unread", reason: error.message };
|
|
48
|
+
}
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function computedFieldsOfType(checker, config) {
|
|
53
|
+
assertExact(checker, config, "the configuration");
|
|
54
|
+
const overlays = Object.fromEntries(LAYERS.map((layer) => {
|
|
55
|
+
const overlay = stated(checker, config, layer, "the configuration");
|
|
56
|
+
return [
|
|
57
|
+
layer,
|
|
58
|
+
overlay === undefined
|
|
59
|
+
? {}
|
|
60
|
+
: layerComputedFields(checker, overlay, layer),
|
|
61
|
+
];
|
|
62
|
+
}));
|
|
63
|
+
return {
|
|
64
|
+
agent: overlays.agent,
|
|
65
|
+
agentExtends: agentParent(checker, config),
|
|
66
|
+
application: overlays.application,
|
|
67
|
+
client: overlays.client,
|
|
68
|
+
root: layerComputedFields(checker, config, "the root"),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function agentParent(checker, config) {
|
|
72
|
+
const agent = stated(checker, config, "agent", "the configuration");
|
|
73
|
+
const property = agent?.getProperty("extends");
|
|
74
|
+
if (property !== undefined && property.flags & ts.SymbolFlags.Optional) {
|
|
75
|
+
throw new NotExact("`extends` in agent may be absent");
|
|
76
|
+
}
|
|
77
|
+
const parent = property === undefined ? undefined : checker.getTypeOfSymbol(property);
|
|
78
|
+
if (parent === undefined) {
|
|
79
|
+
return "client";
|
|
80
|
+
}
|
|
81
|
+
const named = AGENT_PARENTS.find((candidate) => parent.isStringLiteral() && parent.value === candidate);
|
|
82
|
+
if (named === undefined) {
|
|
83
|
+
throw new NotExact(`\`agent.extends\` is \`${checker.typeToString(parent)}\`, not one layer`);
|
|
84
|
+
}
|
|
85
|
+
return named;
|
|
86
|
+
}
|
|
87
|
+
function layerComputedFields(checker, layer, where) {
|
|
88
|
+
const resources = stated(checker, layer, "resources", where);
|
|
89
|
+
if (resources === undefined) {
|
|
90
|
+
return {};
|
|
91
|
+
}
|
|
92
|
+
const result = {};
|
|
93
|
+
for (const [resource, block] of members(checker, resources, `${where}'s resources`)) {
|
|
94
|
+
const fields = stated(checker, block, "fields", `resources.${resource}`);
|
|
95
|
+
if (fields === undefined) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const phases = {
|
|
99
|
+
create: [],
|
|
100
|
+
update: [],
|
|
101
|
+
};
|
|
102
|
+
for (const [field, entry] of members(checker, fields, `resources.${resource}.fields`)) {
|
|
103
|
+
const at = `resources.${resource}.fields.${field}`;
|
|
104
|
+
const pipelines = stated(checker, entry, "pipelines", at);
|
|
105
|
+
const compute = pipelines === undefined
|
|
106
|
+
? undefined
|
|
107
|
+
: stated(checker, pipelines, "compute", `${at}.pipelines`);
|
|
108
|
+
if (compute === undefined) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
for (const phase of PHASES) {
|
|
112
|
+
if (stagePresent(checker, compute, phase, `${at}.pipelines.compute`)) {
|
|
113
|
+
phases[phase].push(field);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const create = phases.create.sort();
|
|
118
|
+
const update = phases.update.sort();
|
|
119
|
+
if (create.length > 0 || update.length > 0) {
|
|
120
|
+
result[resource] = {
|
|
121
|
+
...(create.length === 0 ? {} : { create }),
|
|
122
|
+
...(update.length === 0 ? {} : { update }),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return Object.fromEntries(Object.entries(result).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0));
|
|
127
|
+
}
|
|
128
|
+
function assertExact(checker, type, where) {
|
|
129
|
+
if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) {
|
|
130
|
+
throw new NotExact(`${where} has no type (\`${checker.typeToString(type)}\`)`);
|
|
131
|
+
}
|
|
132
|
+
if (type.isUnion()) {
|
|
133
|
+
throw new NotExact(`${where} is one of several shapes (\`${checker.typeToString(type)}\`)`);
|
|
134
|
+
}
|
|
135
|
+
if (checker.getIndexInfosOfType(type).length > 0) {
|
|
136
|
+
throw new NotExact(`${where} is not a literal: it has an index signature`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function stated(checker, type, name, where) {
|
|
140
|
+
const property = type.getProperty(name);
|
|
141
|
+
if (property === undefined) {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
if (property.flags & ts.SymbolFlags.Optional) {
|
|
145
|
+
throw new NotExact(`\`${name}\` in ${where} may be absent`);
|
|
146
|
+
}
|
|
147
|
+
const value = checker.getTypeOfSymbol(property);
|
|
148
|
+
if (value.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Never)) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
assertExact(checker, value, `\`${name}\` in ${where}`);
|
|
152
|
+
return value;
|
|
153
|
+
}
|
|
154
|
+
function stagePresent(checker, compute, phase, where) {
|
|
155
|
+
const property = compute.getProperty(phase);
|
|
156
|
+
if (property === undefined) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
if (property.flags & ts.SymbolFlags.Optional) {
|
|
160
|
+
throw new NotExact(`\`${phase}\` in ${where} may be absent`);
|
|
161
|
+
}
|
|
162
|
+
const stage = checker.getTypeOfSymbol(property);
|
|
163
|
+
if (stage.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Never)) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
const undefinedMember = (candidate) => (candidate.flags & ts.TypeFlags.Undefined) !== 0;
|
|
167
|
+
if (stage.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown) ||
|
|
168
|
+
(stage.isUnion() && stage.types.some(undefinedMember))) {
|
|
169
|
+
throw new NotExact(`\`${phase}\` in ${where} may be undefined (\`${checker.typeToString(stage)}\`)`);
|
|
170
|
+
}
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
function members(checker, type, where) {
|
|
174
|
+
return type
|
|
175
|
+
.getProperties()
|
|
176
|
+
.map((property) => {
|
|
177
|
+
const name = property.getName();
|
|
178
|
+
const value = stated(checker, type, name, where);
|
|
179
|
+
return value === undefined
|
|
180
|
+
? undefined
|
|
181
|
+
: [name, value];
|
|
182
|
+
})
|
|
183
|
+
.filter((entry) => entry !== undefined);
|
|
184
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
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
|
+
}
|
|
8
|
+
export async function checkContractsFile(projectDirectory, contractsFile, compiled, changesOf) {
|
|
9
|
+
const file = path.join(projectDirectory, contractsFile);
|
|
10
|
+
const text = readGeneratedFile(projectDirectory, contractsFile);
|
|
11
|
+
if (text === undefined) {
|
|
12
|
+
return { kind: "missing" };
|
|
13
|
+
}
|
|
14
|
+
if (text === compiled.source) {
|
|
15
|
+
return { kind: "fresh" };
|
|
16
|
+
}
|
|
17
|
+
const previous = await previousContracts(projectDirectory, file);
|
|
18
|
+
if (previous === undefined) {
|
|
19
|
+
return {
|
|
20
|
+
kind: "stale",
|
|
21
|
+
changes: ["it could not be read as a prebuilt Contracts file"],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const changes = changesOf(previous, compiled.prebuilt);
|
|
25
|
+
return {
|
|
26
|
+
kind: "stale",
|
|
27
|
+
changes: changes.length > 0
|
|
28
|
+
? changes
|
|
29
|
+
: [
|
|
30
|
+
"its text is not what this build writes, though it records the same core version, file format and inputs",
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
async function previousContracts(projectDirectory, file) {
|
|
35
|
+
const jiti = createJiti(pathToFileURL(path.join(projectDirectory, "package.json")).href, { moduleCache: false, fsCache: false, interopDefault: false });
|
|
36
|
+
try {
|
|
37
|
+
const module = await jiti.import(file);
|
|
38
|
+
return module.aventaraContracts?.fingerprints?.resources === undefined
|
|
39
|
+
? undefined
|
|
40
|
+
: module.aventaraContracts;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { AvBuildError } from "../build.error.js";
|
|
2
|
+
function notAtBuildTime() {
|
|
3
|
+
throw new AvBuildError("the adapter was called during aventara-build; a build compiles the Contracts and runs no operation.");
|
|
4
|
+
}
|
|
5
|
+
function buildTimeAdapter(description) {
|
|
6
|
+
return {
|
|
7
|
+
model: description.model,
|
|
8
|
+
...(description.referentialActions === undefined
|
|
9
|
+
? {}
|
|
10
|
+
: { referentialActions: description.referentialActions }),
|
|
11
|
+
...(description.computedFields === undefined
|
|
12
|
+
? {}
|
|
13
|
+
: { computedFields: description.computedFields }),
|
|
14
|
+
...(description.sqlStatements === undefined
|
|
15
|
+
? {}
|
|
16
|
+
: { sqlStatements: description.sqlStatements }),
|
|
17
|
+
execute: notAtBuildTime,
|
|
18
|
+
...(description.sqlStatements === true
|
|
19
|
+
? { executeSql: notAtBuildTime }
|
|
20
|
+
: {}),
|
|
21
|
+
...(description.transactions === "interactive"
|
|
22
|
+
? { transaction: notAtBuildTime }
|
|
23
|
+
: {}),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function diagnosticLines(diagnostics) {
|
|
27
|
+
return diagnostics
|
|
28
|
+
.map((diagnostic) => ` - ${diagnostic.code} at ${diagnostic.path}: ${diagnostic.message} (${diagnostic.docs})`)
|
|
29
|
+
.join("\n");
|
|
30
|
+
}
|
|
31
|
+
export async function compilePrebuiltContracts(project) {
|
|
32
|
+
const { core, internal, config, description } = project;
|
|
33
|
+
let contracts;
|
|
34
|
+
try {
|
|
35
|
+
const framework = await core.createFramework({
|
|
36
|
+
...config,
|
|
37
|
+
adapter: buildTimeAdapter(description),
|
|
38
|
+
});
|
|
39
|
+
contracts = framework.contracts;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (error instanceof core.AvConstructionError) {
|
|
43
|
+
throw new AvBuildError(`${project.configFile} with ${project.adapterFile} does not compile: ${error.message} (${error.stage}).\n${diagnosticLines(error.diagnostics)}`, { cause: error });
|
|
44
|
+
}
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
const prebuilt = await internal.prebuiltContractsOf(description.model, config, contracts);
|
|
48
|
+
return { prebuilt, source: internal.emitPrebuiltContracts(prebuilt) };
|
|
49
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
function versionOf(text) {
|
|
3
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(text);
|
|
4
|
+
if (match === null) {
|
|
5
|
+
throw new Error(`"${text}" is not a Node version`);
|
|
6
|
+
}
|
|
7
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
8
|
+
}
|
|
9
|
+
function compareVersions(left, right) {
|
|
10
|
+
for (let index = 0; index < 3; index += 1) {
|
|
11
|
+
const difference = left[index] - right[index];
|
|
12
|
+
if (difference !== 0) {
|
|
13
|
+
return difference;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
function admits(range, version) {
|
|
19
|
+
return range.split("||").some((part) => {
|
|
20
|
+
const comparator = /^\s*(\^|>=)(\d+\.\d+\.\d+)\s*$/.exec(part);
|
|
21
|
+
if (comparator === null) {
|
|
22
|
+
throw new Error(`engines.node "${range}": the bin's Node guard reads only "^x.y.z" and ">=x.y.z" joined by "||"`);
|
|
23
|
+
}
|
|
24
|
+
const floor = versionOf(comparator[2]);
|
|
25
|
+
return (compareVersions(version, floor) >= 0 &&
|
|
26
|
+
(comparator[1] === ">=" || version[0] === floor[0]));
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export function nodeVersionRefusal(bin, packageName, range, version) {
|
|
30
|
+
return admits(range, versionOf(version))
|
|
31
|
+
? undefined
|
|
32
|
+
: `${bin}: Node ${version} is not supported; ${packageName} needs Node ${range}.`;
|
|
33
|
+
}
|
|
34
|
+
export function refuseUnsupportedNode(bin, manifestUrl) {
|
|
35
|
+
const manifest = JSON.parse(readFileSync(manifestUrl, "utf8"));
|
|
36
|
+
const range = manifest.engines === undefined ? undefined : manifest.engines.node;
|
|
37
|
+
if (range === undefined) {
|
|
38
|
+
throw new Error(`${manifest.name} declares no engines.node for its bin's Node guard`);
|
|
39
|
+
}
|
|
40
|
+
const refusal = nodeVersionRefusal(bin, manifest.name, range, process.version);
|
|
41
|
+
if (refusal === undefined) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
process.stderr.write(`${refusal}\n`);
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export function readGeneratedFile(projectDirectory, file) {
|
|
4
|
+
try {
|
|
5
|
+
return readFileSync(path.join(projectDirectory, file), "utf8");
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function writeGeneratedFile(projectDirectory, file, text) {
|
|
12
|
+
if (readGeneratedFile(projectDirectory, file) === text) {
|
|
13
|
+
return "unchanged";
|
|
14
|
+
}
|
|
15
|
+
const target = path.join(projectDirectory, file);
|
|
16
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
17
|
+
writeFileSync(target, text);
|
|
18
|
+
return "written";
|
|
19
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import ts from "typescript";
|
|
4
|
+
const DEFAULT_DIRECTORY = "src/generated/aventara";
|
|
5
|
+
const TREE_EXPORT = "aventaraResources";
|
|
6
|
+
const TREE_MODULE = "resources.generated";
|
|
7
|
+
function filesIn(directory) {
|
|
8
|
+
return {
|
|
9
|
+
resources: `${directory}/${TREE_MODULE}.ts`,
|
|
10
|
+
computedFields: `${directory}/computed-fields.generated.ts`,
|
|
11
|
+
contracts: `${directory}/contracts.generated.ts`,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export const DEFAULT_GENERATED_FILES = filesIn(DEFAULT_DIRECTORY);
|
|
15
|
+
function treeSpecifier(source) {
|
|
16
|
+
for (const statement of source.statements) {
|
|
17
|
+
if (!ts.isImportDeclaration(statement) ||
|
|
18
|
+
!ts.isStringLiteral(statement.moduleSpecifier) ||
|
|
19
|
+
statement.importClause?.namedBindings === undefined ||
|
|
20
|
+
!ts.isNamedImports(statement.importClause.namedBindings)) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const imported = statement.importClause.namedBindings.elements.some((element) => (element.propertyName ?? element.name).text === TREE_EXPORT);
|
|
24
|
+
const specifier = statement.moduleSpecifier.text;
|
|
25
|
+
if (imported && specifier.startsWith(".")) {
|
|
26
|
+
return specifier;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
export function resolveGeneratedFiles(projectDirectory, configFile) {
|
|
32
|
+
const configPath = path.join(projectDirectory, configFile);
|
|
33
|
+
const source = ts.createSourceFile(configPath, readFileSync(configPath, "utf8"), ts.ScriptTarget.Latest, false);
|
|
34
|
+
const specifier = treeSpecifier(source);
|
|
35
|
+
if (specifier === undefined) {
|
|
36
|
+
return DEFAULT_GENERATED_FILES;
|
|
37
|
+
}
|
|
38
|
+
const module = path.resolve(path.dirname(configPath), specifier);
|
|
39
|
+
const directory = path.relative(projectDirectory, path.dirname(module));
|
|
40
|
+
const inside = directory !== "" &&
|
|
41
|
+
directory !== ".." &&
|
|
42
|
+
!directory.startsWith(`..${path.sep}`) &&
|
|
43
|
+
!path.isAbsolute(directory);
|
|
44
|
+
return inside &&
|
|
45
|
+
path.basename(module).replace(/\.[cm]?[jt]s$/, "") === TREE_MODULE
|
|
46
|
+
? filesIn(directory.split(path.sep).join("/"))
|
|
47
|
+
: DEFAULT_GENERATED_FILES;
|
|
48
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
export const PROJECT_MODULES = {
|
|
9
|
+
config: "src/aventara.config",
|
|
10
|
+
adapter: "src/aventara.adapter",
|
|
11
|
+
};
|
|
12
|
+
const CONFIG_EXPORTS = ["aventaraConfig", "default"];
|
|
13
|
+
function projectFile(projectDirectory, module) {
|
|
14
|
+
const present = EXTENSIONS.map((extension) => `${module}${extension}`).filter((file) => statSync(path.join(projectDirectory, file), {
|
|
15
|
+
throwIfNoEntry: false,
|
|
16
|
+
})?.isFile() === true);
|
|
17
|
+
if (present.length === 0) {
|
|
18
|
+
const [first, ...others] = EXTENSIONS_AS_LISTED;
|
|
19
|
+
throw new AvBuildError(`no ${module}${first}, ${others.slice(0, -1).join(", ")} or ${others.at(-1)} in ${projectDirectory}. Run aventara-build from the project's root (the aventara:prepare script does).`);
|
|
20
|
+
}
|
|
21
|
+
if (present.length > 1) {
|
|
22
|
+
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" : ""}.`);
|
|
23
|
+
}
|
|
24
|
+
return present[0];
|
|
25
|
+
}
|
|
26
|
+
async function evaluate(jiti, projectDirectory, file) {
|
|
27
|
+
try {
|
|
28
|
+
return await jiti.import(path.join(projectDirectory, file));
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
32
|
+
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 });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function isRecord(value) {
|
|
36
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
37
|
+
}
|
|
38
|
+
function configOf(file, namespace) {
|
|
39
|
+
const name = CONFIG_EXPORTS.find((candidate) => Object.hasOwn(namespace, candidate));
|
|
40
|
+
if (name === undefined) {
|
|
41
|
+
throw new AvBuildError(`${file} exports neither \`aventaraConfig\` nor a default: export the configuration as \`export const aventaraConfig = { … } as const\`.`);
|
|
42
|
+
}
|
|
43
|
+
const shown = name === "default" ? "its default export" : `\`${name}\``;
|
|
44
|
+
const value = namespace[name];
|
|
45
|
+
if (typeof value === "function") {
|
|
46
|
+
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 }\`).`);
|
|
47
|
+
}
|
|
48
|
+
if (!isRecord(value)) {
|
|
49
|
+
throw new AvBuildError(`${shown} in ${file} is not an object: it must be the configuration, \`{ api, … } as const\`.`);
|
|
50
|
+
}
|
|
51
|
+
if (Object.hasOwn(value, "adapter")) {
|
|
52
|
+
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 }) }\`.`);
|
|
53
|
+
}
|
|
54
|
+
if (Object.hasOwn(value, "prebuilt")) {
|
|
55
|
+
throw new AvBuildError(`${shown} in ${file} sets \`prebuilt\`, which is this build's output: pass it where the framework is created.`);
|
|
56
|
+
}
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
const TRANSACTIONS = [
|
|
60
|
+
"interactive",
|
|
61
|
+
"none",
|
|
62
|
+
];
|
|
63
|
+
function descriptionOf(file, namespace) {
|
|
64
|
+
const shape = "`export default describePrismaAdapter({ discovery, provider, driver })` (or your adapter's describe function)";
|
|
65
|
+
if (!Object.hasOwn(namespace, "default")) {
|
|
66
|
+
throw new AvBuildError(`${file} has no default export; it must be the adapter's description: ${shape}.`);
|
|
67
|
+
}
|
|
68
|
+
const value = namespace.default;
|
|
69
|
+
if (!isRecord(value)) {
|
|
70
|
+
throw new AvBuildError(`${file}'s default export is not an object; it must be the adapter's description: ${shape}.`);
|
|
71
|
+
}
|
|
72
|
+
const wrong = (member, expected) => new AvBuildError(`${file}'s default export is not an adapter description: its \`${member}\` is not ${expected}. It must be ${shape}.`);
|
|
73
|
+
if (!isRecord(value.model)) {
|
|
74
|
+
throw wrong("model", "an object");
|
|
75
|
+
}
|
|
76
|
+
if (!TRANSACTIONS.includes(value.transactions)) {
|
|
77
|
+
throw wrong("transactions", '"interactive" or "none"');
|
|
78
|
+
}
|
|
79
|
+
for (const member of ["referentialActions", "computedFields"]) {
|
|
80
|
+
if (value[member] !== undefined && !isRecord(value[member])) {
|
|
81
|
+
throw wrong(member, "an object");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (value.sqlStatements !== undefined &&
|
|
85
|
+
typeof value.sqlStatements !== "boolean") {
|
|
86
|
+
throw wrong("sqlStatements", "a boolean");
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
async function projectCore(jiti) {
|
|
91
|
+
try {
|
|
92
|
+
const [core, internal] = await Promise.all([
|
|
93
|
+
jiti.import("@aventara/core"),
|
|
94
|
+
jiti.import("@aventara/core/internal"),
|
|
95
|
+
]);
|
|
96
|
+
return { core, internal };
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
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 });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export function findProjectModules(projectDirectory) {
|
|
103
|
+
return {
|
|
104
|
+
configFile: projectFile(projectDirectory, PROJECT_MODULES.config),
|
|
105
|
+
adapterFile: projectFile(projectDirectory, PROJECT_MODULES.adapter),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export async function loadProjectModules(projectDirectory, { configFile, adapterFile }) {
|
|
109
|
+
const jiti = createJiti(pathToFileURL(path.join(projectDirectory, "package.json")).href, {
|
|
110
|
+
moduleCache: false,
|
|
111
|
+
fsCache: false,
|
|
112
|
+
interopDefault: false,
|
|
113
|
+
nativeModules: ["@aventara/core"],
|
|
114
|
+
});
|
|
115
|
+
const project = await projectCore(jiti);
|
|
116
|
+
const config = configOf(configFile, await evaluate(jiti, projectDirectory, configFile));
|
|
117
|
+
const description = descriptionOf(adapterFile, await evaluate(jiti, projectDirectory, adapterFile));
|
|
118
|
+
return { ...project, configFile, config, adapterFile, description };
|
|
119
|
+
}
|
|
@@ -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
|
+
}
|