@lunora/codegen 0.0.0 → 1.0.0-alpha.2
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/LICENSE.md +105 -0
- package/README.md +117 -9
- package/__assets__/package-og.svg +14 -0
- package/dist/index.d.mts +1473 -0
- package/dist/index.d.ts +1473 -0
- package/dist/index.mjs +28 -0
- package/dist/packem_shared/CONTAINERS_FILENAME-0K-pjNb8.mjs +224 -0
- package/dist/packem_shared/CodegenDiagnosticError-54jWDxA9.mjs +22 -0
- package/dist/packem_shared/LUNORA_ERROR_CODES-CySpQPD3.mjs +61 -0
- package/dist/packem_shared/buildOpenApiDocument-yHVN66Xd.mjs +183 -0
- package/dist/packem_shared/buildOpenRpcDocument-BZGY1-jT.mjs +60 -0
- package/dist/packem_shared/buildSchemaSnapshot-DzLDbWk3.mjs +233 -0
- package/dist/packem_shared/createCodegenProject-DGJm0_Pk.mjs +895 -0
- package/dist/packem_shared/discover-ast-CT6BgBr4.mjs +13 -0
- package/dist/packem_shared/discoverAuthApiCalls-C35R6z0T.mjs +62 -0
- package/dist/packem_shared/discoverCrons-BL6iGuJ3.mjs +254 -0
- package/dist/packem_shared/discoverFunctions-DEgAcRuD.mjs +460 -0
- package/dist/packem_shared/discoverHttpRoutes-C978pBiG.mjs +131 -0
- package/dist/packem_shared/discoverInserts-CRQdXvHO.mjs +39 -0
- package/dist/packem_shared/discoverMaskProcedures-B64zA740.mjs +217 -0
- package/dist/packem_shared/discoverMigrations-Doj_-BAA.mjs +91 -0
- package/dist/packem_shared/discoverNondeterministicCalls-4KiPQxQU.mjs +122 -0
- package/dist/packem_shared/discoverQueries-BkIi0dBD.mjs +62 -0
- package/dist/packem_shared/discoverRlsMetadata-DpRB1HMe.mjs +280 -0
- package/dist/packem_shared/discoverSchema-BBulgGbH.mjs +542 -0
- package/dist/packem_shared/discoverStorageRulesMetadata-DAqJUxUv.mjs +97 -0
- package/dist/packem_shared/discoverWorkflows-DRDQdhfq.mjs +84 -0
- package/dist/packem_shared/emitApi-hRVC-kE7.mjs +2426 -0
- package/dist/packem_shared/emitApp-CzZ6GbrD.mjs +593 -0
- package/dist/packem_shared/lintSchema-DicbOHvH.mjs +68 -0
- package/dist/packem_shared/parse-validator-tuQtHrsr.mjs +132 -0
- package/dist/packem_shared/paths-BRd6JHuF.mjs +11 -0
- package/dist/packem_shared/schemaFromIr-DTYsLBaA.mjs +57 -0
- package/package.json +45 -17
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Node } from 'ts-morph';
|
|
2
|
+
|
|
3
|
+
const TS_EXTENSION_RE = /\.ts$/u;
|
|
4
|
+
const enclosingExportName = (call) => {
|
|
5
|
+
for (const ancestor of call.getAncestors()) {
|
|
6
|
+
if (Node.isVariableDeclaration(ancestor) && ancestor.getVariableStatement()?.hasExportKeyword() === true) {
|
|
7
|
+
return ancestor.getName();
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
return "";
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export { TS_EXTENSION_RE as T, enclosingExportName as e };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { relative, sep } from 'node:path';
|
|
2
|
+
import { SyntaxKind, Node } from 'ts-morph';
|
|
3
|
+
import { T as TS_EXTENSION_RE, e as enclosingExportName } from './discover-ast-CT6BgBr4.mjs';
|
|
4
|
+
import { listLunoraSourceFiles } from './discoverFunctions-DEgAcRuD.mjs';
|
|
5
|
+
|
|
6
|
+
const isAuthApiCall = (call) => {
|
|
7
|
+
const callee = call.getExpression();
|
|
8
|
+
if (!Node.isPropertyAccessExpression(callee)) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
const receiver = callee.getExpression();
|
|
12
|
+
if (Node.isPropertyAccessExpression(receiver)) {
|
|
13
|
+
return receiver.getName() === "authApi";
|
|
14
|
+
}
|
|
15
|
+
return Node.isIdentifier(receiver) && receiver.getText() === "authApi";
|
|
16
|
+
};
|
|
17
|
+
const hasHeadersProp = (call) => {
|
|
18
|
+
const argument = call.getArguments()[0];
|
|
19
|
+
if (argument === void 0) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
if (!Node.isObjectLiteralExpression(argument)) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
for (const property of argument.getProperties()) {
|
|
26
|
+
if ((Node.isPropertyAssignment(property) || Node.isShorthandPropertyAssignment(property)) && property.getName() === "headers") {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
if (Node.isSpreadAssignment(property)) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
};
|
|
35
|
+
const discoverAuthApiCalls = (project, lunoraDirectory) => {
|
|
36
|
+
const calls = [];
|
|
37
|
+
for (const filePath of listLunoraSourceFiles(lunoraDirectory)) {
|
|
38
|
+
const sourceFile = project.getSourceFile(filePath) ?? project.addSourceFileAtPath(filePath);
|
|
39
|
+
const relativePath = relative(lunoraDirectory, filePath).replace(TS_EXTENSION_RE, "").split(sep).join("/");
|
|
40
|
+
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
41
|
+
if (!isAuthApiCall(call)) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const exportName = enclosingExportName(call);
|
|
45
|
+
if (exportName === "") {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const callee = call.getExpression();
|
|
49
|
+
const method = Node.isPropertyAccessExpression(callee) ? callee.getName() : "";
|
|
50
|
+
calls.push({
|
|
51
|
+
exportName,
|
|
52
|
+
file: relativePath,
|
|
53
|
+
hasHeaders: hasHeadersProp(call),
|
|
54
|
+
line: call.getStartLineNumber(),
|
|
55
|
+
method
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return calls;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export { discoverAuthApiCalls as default };
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { isValidCronExpression, compileCronSchedule, CRON_SCHEDULE_KINDS } from '@lunora/scheduler';
|
|
2
|
+
import { SyntaxKind, Node } from 'ts-morph';
|
|
3
|
+
import { listLunoraSourceFiles } from './discoverFunctions-DEgAcRuD.mjs';
|
|
4
|
+
import { s as sanitizeNamespace } from './paths-BRd6JHuF.mjs';
|
|
5
|
+
|
|
6
|
+
const CRON_METHODS = /* @__PURE__ */ new Set([...CRON_SCHEDULE_KINDS, "cron"]);
|
|
7
|
+
const CRON_JOBS_SOURCES = /* @__PURE__ */ new Set(["@lunora/scheduler", "@lunora/server"]);
|
|
8
|
+
const isCronJobsFactory = (identifier) => {
|
|
9
|
+
const symbol = identifier.getSymbol();
|
|
10
|
+
if (!symbol) {
|
|
11
|
+
return identifier.getText() === "cronJobs";
|
|
12
|
+
}
|
|
13
|
+
for (const declaration of symbol.getDeclarations()) {
|
|
14
|
+
if (!Node.isImportSpecifier(declaration)) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (!CRON_JOBS_SOURCES.has(declaration.getImportDeclaration().getModuleSpecifierValue())) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
return declaration.getNameNode().getText() === "cronJobs";
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
};
|
|
24
|
+
const collectCronBuilderNames = (source) => {
|
|
25
|
+
const names = /* @__PURE__ */ new Set();
|
|
26
|
+
for (const declaration of source.getVariableDeclarations()) {
|
|
27
|
+
const initializer = declaration.getInitializer();
|
|
28
|
+
if (initializer?.getKind() !== SyntaxKind.CallExpression) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const callee = initializer.getExpression();
|
|
32
|
+
if (Node.isIdentifier(callee) && isCronJobsFactory(callee)) {
|
|
33
|
+
names.add(declaration.getName());
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return names;
|
|
37
|
+
};
|
|
38
|
+
const rootIdentifierOf = (node) => {
|
|
39
|
+
let current = node;
|
|
40
|
+
while (Node.isCallExpression(current) || Node.isPropertyAccessExpression(current)) {
|
|
41
|
+
current = current.getExpression();
|
|
42
|
+
}
|
|
43
|
+
return Node.isIdentifier(current) ? current.getText() : void 0;
|
|
44
|
+
};
|
|
45
|
+
const stringArgument = (call, index) => {
|
|
46
|
+
const argument = call.getArguments()[index];
|
|
47
|
+
return argument && Node.isStringLiteral(argument) ? argument.getLiteralValue() : void 0;
|
|
48
|
+
};
|
|
49
|
+
const literalValue = (node, jobName) => {
|
|
50
|
+
if (Node.isStringLiteral(node) || Node.isNoSubstitutionTemplateLiteral(node)) {
|
|
51
|
+
return node.getLiteralValue();
|
|
52
|
+
}
|
|
53
|
+
if (Node.isNumericLiteral(node)) {
|
|
54
|
+
return node.getLiteralValue();
|
|
55
|
+
}
|
|
56
|
+
if (Node.isTrueLiteral(node)) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
if (Node.isFalseLiteral(node)) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
if (Node.isPrefixUnaryExpression(node) && node.getOperatorToken() === SyntaxKind.MinusToken) {
|
|
63
|
+
const operand = node.getOperand();
|
|
64
|
+
if (Node.isNumericLiteral(operand)) {
|
|
65
|
+
return -operand.getLiteralValue();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (Node.isObjectLiteralExpression(node)) {
|
|
69
|
+
return objectLiteralValue(node, jobName);
|
|
70
|
+
}
|
|
71
|
+
if (Node.isArrayLiteralExpression(node)) {
|
|
72
|
+
return node.getElements().map((element) => literalValue(element, jobName));
|
|
73
|
+
}
|
|
74
|
+
throw Object.assign(new Error(`Cron job "${jobName}" passes a non-static value where a literal is required; codegen can only read literals.`), {
|
|
75
|
+
code: "CRON_NON_STATIC_VALUE",
|
|
76
|
+
name: "LunoraError",
|
|
77
|
+
status: 500
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
const objectLiteralValue = (object, jobName) => {
|
|
81
|
+
const result = {};
|
|
82
|
+
for (const property of object.getProperties()) {
|
|
83
|
+
if (!Node.isPropertyAssignment(property)) {
|
|
84
|
+
throw Object.assign(
|
|
85
|
+
new Error(`Cron job "${jobName}" uses an unsupported object property (shorthand/spread/method) where a static literal is required.`),
|
|
86
|
+
{
|
|
87
|
+
code: "CRON_NON_STATIC_VALUE",
|
|
88
|
+
name: "LunoraError",
|
|
89
|
+
status: 500
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const initializer = property.getInitializer();
|
|
94
|
+
if (initializer) {
|
|
95
|
+
result[property.getName()] = literalValue(initializer, jobName);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
};
|
|
100
|
+
const functionPathFromArgument = (call, index, jobName) => {
|
|
101
|
+
const argument = call.getArguments()[index];
|
|
102
|
+
if (!argument || !Node.isPropertyAccessExpression(argument)) {
|
|
103
|
+
throw Object.assign(
|
|
104
|
+
new Error(`Cron job "${jobName}" must reference a function statically (e.g. internal.email.digest); codegen cannot resolve a dynamic reference.`),
|
|
105
|
+
{
|
|
106
|
+
code: "CRON_NON_STATIC_FN",
|
|
107
|
+
name: "LunoraError",
|
|
108
|
+
status: 500
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
const functionName = argument.getName();
|
|
113
|
+
const receiver = argument.getExpression();
|
|
114
|
+
if (!Node.isPropertyAccessExpression(receiver)) {
|
|
115
|
+
throw Object.assign(new Error(`Cron job "${jobName}" function reference must be of the form internal.file.fn (two property accesses).`), {
|
|
116
|
+
code: "CRON_NON_STATIC_FN",
|
|
117
|
+
name: "LunoraError",
|
|
118
|
+
status: 500
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
const namespace = receiver.getName();
|
|
122
|
+
return `${sanitizeNamespace(namespace)}:${functionName}`;
|
|
123
|
+
};
|
|
124
|
+
const workflowTarget = (workflow) => {
|
|
125
|
+
return { workflow: { binding: workflow.bindingName, exportName: workflow.exportName } };
|
|
126
|
+
};
|
|
127
|
+
const resolveTarget = (call, index, jobName, workflowsByName) => {
|
|
128
|
+
const argument = call.getArguments()[index];
|
|
129
|
+
if (argument && Node.isPropertyAccessExpression(argument)) {
|
|
130
|
+
const receiver = argument.getExpression();
|
|
131
|
+
if (Node.isIdentifier(receiver) && receiver.getText() === "workflows") {
|
|
132
|
+
const workflow = workflowsByName.get(argument.getName());
|
|
133
|
+
if (workflow) {
|
|
134
|
+
return workflowTarget(workflow);
|
|
135
|
+
}
|
|
136
|
+
throw Object.assign(
|
|
137
|
+
new Error(`Cron job "${jobName}" targets workflows.${argument.getName()}, but no such workflow is declared in lunora/workflows.ts.`),
|
|
138
|
+
{
|
|
139
|
+
code: "CRON_NON_STATIC_FN",
|
|
140
|
+
name: "LunoraError",
|
|
141
|
+
status: 500
|
|
142
|
+
}
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return { functionPath: functionPathFromArgument(call, index, jobName) };
|
|
146
|
+
}
|
|
147
|
+
if (argument && Node.isIdentifier(argument)) {
|
|
148
|
+
const workflow = workflowsByName.get(argument.getText());
|
|
149
|
+
if (workflow) {
|
|
150
|
+
return workflowTarget(workflow);
|
|
151
|
+
}
|
|
152
|
+
throw Object.assign(
|
|
153
|
+
new Error(
|
|
154
|
+
`Cron job "${jobName}" references "${argument.getText()}", which is neither a function (internal.file.fn / api.file.fn) nor a declared workflow in lunora/workflows.ts.`
|
|
155
|
+
),
|
|
156
|
+
{
|
|
157
|
+
code: "CRON_NON_STATIC_FN",
|
|
158
|
+
name: "LunoraError",
|
|
159
|
+
status: 500
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
return { functionPath: functionPathFromArgument(call, index, jobName) };
|
|
164
|
+
};
|
|
165
|
+
const cronFromCall = (call, callee, builderNames, workflowsByName) => {
|
|
166
|
+
const method = callee.getName();
|
|
167
|
+
if (!CRON_METHODS.has(method)) {
|
|
168
|
+
return void 0;
|
|
169
|
+
}
|
|
170
|
+
if (!builderNames.has(rootIdentifierOf(callee.getExpression()) ?? "")) {
|
|
171
|
+
return void 0;
|
|
172
|
+
}
|
|
173
|
+
const name = stringArgument(call, 0);
|
|
174
|
+
if (name === void 0 || name.trim() === "") {
|
|
175
|
+
throw Object.assign(new Error(`A cron ".${method}(...)" registration must pass a non-empty string-literal name as its first argument.`), {
|
|
176
|
+
code: "CRON_NAME_NOT_STATIC",
|
|
177
|
+
name: "LunoraError",
|
|
178
|
+
status: 500
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
let cron;
|
|
182
|
+
if (method === "cron") {
|
|
183
|
+
const expression = stringArgument(call, 1);
|
|
184
|
+
if (expression === void 0) {
|
|
185
|
+
throw Object.assign(new Error(`Cron job "${name}" must pass a string-literal cron expression to ".cron(...)".`), {
|
|
186
|
+
code: "CRON_EXPR_NOT_STATIC",
|
|
187
|
+
name: "LunoraError",
|
|
188
|
+
status: 500
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
if (!isValidCronExpression(expression)) {
|
|
192
|
+
throw Object.assign(new Error(`Cron job "${name}" has an invalid cron expression "${expression}" — expected 5 or 6 space-separated fields.`), {
|
|
193
|
+
code: "CRON_EXPR_INVALID",
|
|
194
|
+
name: "LunoraError",
|
|
195
|
+
status: 500
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
cron = expression;
|
|
199
|
+
} else {
|
|
200
|
+
const scheduleArgument = call.getArguments()[1];
|
|
201
|
+
if (!scheduleArgument || !Node.isObjectLiteralExpression(scheduleArgument)) {
|
|
202
|
+
throw Object.assign(new Error(`Cron job "${name}" must pass an object-literal schedule to ".${method}(...)".`), {
|
|
203
|
+
code: "CRON_SCHEDULE_NOT_STATIC",
|
|
204
|
+
name: "LunoraError",
|
|
205
|
+
status: 500
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
cron = compileCronSchedule(method, objectLiteralValue(scheduleArgument, name));
|
|
209
|
+
}
|
|
210
|
+
const target = resolveTarget(call, 2, name, workflowsByName);
|
|
211
|
+
const argumentsNode = call.getArguments()[3];
|
|
212
|
+
const args = argumentsNode && Node.isObjectLiteralExpression(argumentsNode) ? objectLiteralValue(argumentsNode, name) : {};
|
|
213
|
+
return { args, cron, name, ...target };
|
|
214
|
+
};
|
|
215
|
+
const assertUniqueNames = (crons) => {
|
|
216
|
+
const seen = /* @__PURE__ */ new Set();
|
|
217
|
+
for (const cron of crons) {
|
|
218
|
+
if (seen.has(cron.name)) {
|
|
219
|
+
throw Object.assign(new Error(`Duplicate cron job name "${cron.name}": cron names must be unique across the project.`), {
|
|
220
|
+
code: "DUPLICATE_CRON_NAME",
|
|
221
|
+
name: "LunoraError",
|
|
222
|
+
status: 500
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
seen.add(cron.name);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
const discoverCrons = (project, lunoraDirectory, workflows = []) => {
|
|
229
|
+
const filePaths = listLunoraSourceFiles(lunoraDirectory);
|
|
230
|
+
const crons = [];
|
|
231
|
+
const workflowsByName = new Map(workflows.map((workflow) => [workflow.exportName, workflow]));
|
|
232
|
+
for (const filePath of filePaths) {
|
|
233
|
+
const source = project.getSourceFile(filePath) ?? project.addSourceFileAtPath(filePath);
|
|
234
|
+
const builderNames = collectCronBuilderNames(source);
|
|
235
|
+
if (builderNames.size === 0) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
for (const call of source.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
239
|
+
const callee = call.getExpression();
|
|
240
|
+
if (!Node.isPropertyAccessExpression(callee)) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const cron = cronFromCall(call, callee, builderNames, workflowsByName);
|
|
244
|
+
if (cron) {
|
|
245
|
+
crons.push(cron);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
crons.sort((a, b) => a.name.localeCompare(b.name));
|
|
250
|
+
assertUniqueNames(crons);
|
|
251
|
+
return crons;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
export { discoverCrons as default };
|