@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.
- package/CHANGELOG.md +282 -0
- package/LICENSE +91 -0
- package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
- package/README.md +179 -2
- package/dist/aventara-build.bin.js +5 -0
- package/dist/build.error.js +3 -0
- package/dist/cli.js +240 -0
- package/dist/client/client-file.js +449 -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 +56 -0
- package/dist/node-version.guard.js +47 -0
- package/dist/project/generated-file.js +19 -0
- package/dist/project/generated-files.resolver.js +51 -0
- package/dist/project/host.catalog.generated.js +32 -0
- package/dist/project/project-env.loader.js +33 -0
- package/dist/project/project-layout.resolver.js +59 -0
- package/dist/project/project-modules.loader.js +117 -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 +38 -3
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
function literal(computedFields) {
|
|
2
|
+
const ordered = {
|
|
3
|
+
agent: computedFields.agent,
|
|
4
|
+
agentExtends: computedFields.agentExtends,
|
|
5
|
+
application: computedFields.application,
|
|
6
|
+
client: computedFields.client,
|
|
7
|
+
root: computedFields.root,
|
|
8
|
+
};
|
|
9
|
+
return JSON.stringify(ordered, null, "\t");
|
|
10
|
+
}
|
|
11
|
+
export function emitComputedFieldsModule(configFile, reading) {
|
|
12
|
+
const header = `// Generated by aventara-build from the type of ${configFile}. Do not edit: regenerate it (aventara:prepare).`;
|
|
13
|
+
const type = [
|
|
14
|
+
"",
|
|
15
|
+
"// What an adapter's discovery artifact carries on its model's type for the stages that run before compute.",
|
|
16
|
+
"export type AvComputedFields = typeof computedFields;",
|
|
17
|
+
];
|
|
18
|
+
if (reading.kind === "unread") {
|
|
19
|
+
return `${[
|
|
20
|
+
header,
|
|
21
|
+
"",
|
|
22
|
+
`// Not read: ${reading.reason.replaceAll("\n", " ")}. The stages that run before compute may leave any`,
|
|
23
|
+
"// create field out; write the configuration `as const` there to have it read.",
|
|
24
|
+
"export const computedFields = null;",
|
|
25
|
+
...type,
|
|
26
|
+
].join("\n")}\n`;
|
|
27
|
+
}
|
|
28
|
+
return `${[
|
|
29
|
+
header,
|
|
30
|
+
'import type { AvConfigComputedFields } from "@aventara/core";',
|
|
31
|
+
"",
|
|
32
|
+
"// Which fields the configuration's write compute fills (`compute.create`, `compute.update`), per layer and",
|
|
33
|
+
"// Resource. The framework compares it with the configuration at startup and warns when they differ.",
|
|
34
|
+
`export const computedFields = ${literal(reading.computedFields)} as const satisfies AvConfigComputedFields;`,
|
|
35
|
+
...type,
|
|
36
|
+
].join("\n")}\n`;
|
|
37
|
+
}
|
|
@@ -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,56 @@
|
|
|
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
|
+
let api;
|
|
35
|
+
try {
|
|
36
|
+
const framework = await core.createFramework({
|
|
37
|
+
...config,
|
|
38
|
+
adapter: buildTimeAdapter(description),
|
|
39
|
+
});
|
|
40
|
+
contracts = framework.contracts;
|
|
41
|
+
api = framework.api;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (error instanceof core.AvConstructionError) {
|
|
45
|
+
throw new AvBuildError(`${project.configFile} with ${project.adapterFile} does not compile: ${error.message} (${error.stage}).\n${diagnosticLines(error.diagnostics)}`, { cause: error });
|
|
46
|
+
}
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
const prebuilt = await internal.prebuiltContractsOf(description.model, config, contracts);
|
|
50
|
+
return {
|
|
51
|
+
prebuilt,
|
|
52
|
+
source: internal.emitPrebuiltContracts(prebuilt),
|
|
53
|
+
client: contracts.client,
|
|
54
|
+
api,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -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,51 @@
|
|
|
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, defaultDirectory = DEFAULT_DIRECTORY) {
|
|
32
|
+
const fallback = defaultDirectory === DEFAULT_DIRECTORY
|
|
33
|
+
? DEFAULT_GENERATED_FILES
|
|
34
|
+
: filesIn(defaultDirectory);
|
|
35
|
+
const configPath = path.join(projectDirectory, configFile);
|
|
36
|
+
const source = ts.createSourceFile(configPath, readFileSync(configPath, "utf8"), ts.ScriptTarget.Latest, false);
|
|
37
|
+
const specifier = treeSpecifier(source);
|
|
38
|
+
if (specifier === undefined) {
|
|
39
|
+
return fallback;
|
|
40
|
+
}
|
|
41
|
+
const module = path.resolve(path.dirname(configPath), specifier);
|
|
42
|
+
const directory = path.relative(projectDirectory, path.dirname(module));
|
|
43
|
+
const inside = directory !== "" &&
|
|
44
|
+
directory !== ".." &&
|
|
45
|
+
!directory.startsWith(`..${path.sep}`) &&
|
|
46
|
+
!path.isAbsolute(directory);
|
|
47
|
+
return inside &&
|
|
48
|
+
path.basename(module).replace(/\.[cm]?[jt]s$/, "") === TREE_MODULE
|
|
49
|
+
? filesIn(directory.split(path.sep).join("/"))
|
|
50
|
+
: fallback;
|
|
51
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const HOST_CATALOG = [
|
|
2
|
+
{
|
|
3
|
+
"alias": "nest",
|
|
4
|
+
"label": "NestJS",
|
|
5
|
+
"package": "@aventara/nest",
|
|
6
|
+
"framework": {
|
|
7
|
+
"package": "@nestjs/core",
|
|
8
|
+
"range": "^12.0.0"
|
|
9
|
+
},
|
|
10
|
+
"layout": {
|
|
11
|
+
"config": "src/aventara.config",
|
|
12
|
+
"adapter": "src/aventara.adapter",
|
|
13
|
+
"generated": "src/generated/aventara"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"alias": "next",
|
|
18
|
+
"label": "Next.js",
|
|
19
|
+
"package": "@aventara/next",
|
|
20
|
+
"framework": {
|
|
21
|
+
"package": "next",
|
|
22
|
+
"range": ">=16.4 <17"
|
|
23
|
+
},
|
|
24
|
+
"layout": {
|
|
25
|
+
"config": "src/aventara/config",
|
|
26
|
+
"adapter": "src/aventara/adapter",
|
|
27
|
+
"generated": "src/aventara/generated",
|
|
28
|
+
"client": "src/aventara/client.ts",
|
|
29
|
+
"universal": "src/aventara/universal.ts"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { AvEnvFileError, resolveEnvCascade } from "@aventara/client/internal";
|
|
3
|
+
import { AvBuildError } from "../build.error.js";
|
|
4
|
+
export function applyProjectEnv(projectDirectory, env = process.env) {
|
|
5
|
+
let cascade;
|
|
6
|
+
try {
|
|
7
|
+
cascade = resolveEnvCascade({
|
|
8
|
+
directory: projectDirectory,
|
|
9
|
+
processEnv: { ...env },
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (error instanceof AvEnvFileError) {
|
|
14
|
+
throw new AvBuildError(`${error.message.replace(error.filePath, path.relative(projectDirectory, error.filePath))}.`);
|
|
15
|
+
}
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
const added = [];
|
|
19
|
+
for (const [name, value] of Object.entries(cascade.env)) {
|
|
20
|
+
if (env[name] === undefined) {
|
|
21
|
+
env[name] = value;
|
|
22
|
+
added.push(name);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
loaded: cascade.loaded,
|
|
27
|
+
restore: () => {
|
|
28
|
+
for (const name of added) {
|
|
29
|
+
delete env[name];
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { AvBuildError } from "../build.error.js";
|
|
4
|
+
import { HOST_CATALOG } from "./host.catalog.generated.js";
|
|
5
|
+
const FALLBACK_HOST = "nest";
|
|
6
|
+
function clientLayoutOf(entry) {
|
|
7
|
+
const { client, universal } = entry.layout;
|
|
8
|
+
if (client === undefined && universal === undefined) {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
if (client === undefined ||
|
|
12
|
+
universal === undefined ||
|
|
13
|
+
path.posix.dirname(client) !== path.posix.dirname(universal)) {
|
|
14
|
+
throw new AvBuildError(`${entry.package} declares its browser client at ${client ?? "nowhere"} and its universal entry at ${universal ?? "nowhere"}; the build writes both into one folder, so a host declares both, side by side.`);
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
folder: path.posix.dirname(client),
|
|
18
|
+
entry: path.posix.basename(client),
|
|
19
|
+
universal: path.posix.basename(universal),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function layoutOf(entry) {
|
|
23
|
+
const client = clientLayoutOf(entry);
|
|
24
|
+
return {
|
|
25
|
+
host: entry.alias,
|
|
26
|
+
described: entry.alias === FALLBACK_HOST
|
|
27
|
+
? `a ${entry.label} project`
|
|
28
|
+
: `a ${entry.label} project (it depends on ${entry.package})`,
|
|
29
|
+
config: entry.layout.config,
|
|
30
|
+
adapter: entry.layout.adapter,
|
|
31
|
+
generated: entry.layout.generated,
|
|
32
|
+
...(client === undefined ? {} : { client }),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export const PROJECT_LAYOUTS = Object.fromEntries(HOST_CATALOG.map((entry) => [entry.alias, layoutOf(entry)]));
|
|
36
|
+
const DEPENDENCY_LISTS = ["dependencies", "devDependencies"];
|
|
37
|
+
function hostsOf(projectDirectory) {
|
|
38
|
+
let manifest;
|
|
39
|
+
try {
|
|
40
|
+
manifest = JSON.parse(readFileSync(path.join(projectDirectory, "package.json"), "utf8"));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
const lists = DEPENDENCY_LISTS.map((list) => typeof manifest === "object" && manifest !== null
|
|
46
|
+
? manifest[list]
|
|
47
|
+
: undefined);
|
|
48
|
+
return HOST_CATALOG.filter((entry) => lists.some((list) => typeof list === "object" &&
|
|
49
|
+
list !== null &&
|
|
50
|
+
Object.hasOwn(list, entry.package)));
|
|
51
|
+
}
|
|
52
|
+
export function resolveProjectLayout(projectDirectory) {
|
|
53
|
+
const hosts = hostsOf(projectDirectory);
|
|
54
|
+
if (hosts.length > 1) {
|
|
55
|
+
throw new AvBuildError(`${projectDirectory}/package.json depends on both ${hosts.map((host) => host.package).join(" and ")}, so it is not clear where the configuration and the adapter are. A project hosts its framework in one of them; remove the other.`);
|
|
56
|
+
}
|
|
57
|
+
const [host] = hosts;
|
|
58
|
+
return PROJECT_LAYOUTS[host === undefined ? FALLBACK_HOST : host.alias];
|
|
59
|
+
}
|