@loutrejs/loutre 0.1.0 → 0.3.0
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/README.md +69 -0
- package/dist/adapters/aws-lambda.js +2 -0
- package/dist/adapters/bun.d.ts +3 -1
- package/dist/adapters/bun.js +90 -33
- package/dist/adapters/cloudflare-workers.js +2 -0
- package/dist/adapters/deno.d.ts +3 -1
- package/dist/adapters/deno.js +99 -33
- package/dist/adapters/electron.js +2 -0
- package/dist/application/openapi.d.ts +6 -0
- package/dist/application/openapi.js +14 -19
- package/dist/core/contract.d.ts +47 -7
- package/dist/core/contract.js +59 -10
- package/dist/generated/version.d.ts +1 -0
- package/dist/generated/version.js +2 -0
- package/dist/graph/compiler.d.ts +4 -0
- package/dist/graph/compiler.js +228 -0
- package/dist/graph/graph.js +65 -40
- package/dist/graph/index.d.ts +2 -1
- package/dist/graph/index.js +2 -1
- package/dist/graph/ir.d.ts +24 -8
- package/dist/http/application.js +5 -5
- package/dist/http/basic-auth.js +1 -1
- package/dist/http/client.d.ts +72 -0
- package/dist/http/client.js +258 -0
- package/dist/http/cors.js +4 -4
- package/dist/http/definitions.d.ts +13 -5
- package/dist/http/definitions.js +6 -2
- package/dist/http/index.d.ts +1 -0
- package/dist/http/index.js +1 -0
- package/dist/message-port/index.d.ts +12 -4
- package/dist/message-port/index.js +13 -6
- package/dist/presentation.d.ts +33 -0
- package/dist/presentation.js +233 -0
- package/dist/runtime/capability.js +1 -1
- package/dist/runtime/di.js +2 -2
- package/dist/runtime/engine.d.ts +3 -0
- package/dist/runtime/engine.js +38 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/pipeline.js +13 -13
- package/dist/runtime/server-url.d.ts +1 -0
- package/dist/runtime/server-url.js +5 -0
- package/package.json +5 -1
package/dist/core/contract.js
CHANGED
|
@@ -1,15 +1,64 @@
|
|
|
1
|
-
export function
|
|
2
|
-
return {
|
|
1
|
+
export function protocolGroup(protocol, procedures) {
|
|
2
|
+
return Object.freeze({
|
|
3
|
+
kind: 'protocol-group',
|
|
4
|
+
protocol,
|
|
5
|
+
procedures: Object.freeze({ ...procedures }),
|
|
6
|
+
});
|
|
3
7
|
}
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
8
|
+
function defineContract(groups) {
|
|
9
|
+
if (groups.length === 0) {
|
|
10
|
+
throw new Error('contract([]) requires at least one protocol group');
|
|
11
|
+
}
|
|
12
|
+
const procedures = {};
|
|
13
|
+
for (const group of groups) {
|
|
14
|
+
for (const [procedureName, descriptor] of Object.entries(group.procedures)) {
|
|
15
|
+
const current = procedures[procedureName];
|
|
16
|
+
if (Object.hasOwn(current?.protocols ?? {}, group.protocol)) {
|
|
17
|
+
throw new Error(`Duplicate contract procedure protocol: ${procedureName}.${group.protocol}`);
|
|
18
|
+
}
|
|
19
|
+
procedures[procedureName] = {
|
|
20
|
+
kind: 'procedure',
|
|
21
|
+
protocols: {
|
|
22
|
+
...current?.protocols,
|
|
23
|
+
[group.protocol]: descriptor,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
assertUniqueDispatchKeys(procedures);
|
|
29
|
+
return Object.freeze({
|
|
30
|
+
kind: 'contract',
|
|
31
|
+
procedures: Object.freeze(procedures),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function mergeContracts(contracts) {
|
|
35
|
+
if (contracts.length === 0) {
|
|
36
|
+
throw new Error('contract.merge([]) requires at least one Contract');
|
|
37
|
+
}
|
|
38
|
+
const procedures = {};
|
|
39
|
+
for (const source of contracts) {
|
|
40
|
+
for (const [procedureName, procedureDefinition] of Object.entries(source.procedures)) {
|
|
41
|
+
const current = procedures[procedureName];
|
|
42
|
+
const protocols = { ...current?.protocols };
|
|
43
|
+
for (const [protocolName, descriptor] of Object.entries(procedureDefinition.protocols)) {
|
|
44
|
+
if (Object.hasOwn(protocols, protocolName)) {
|
|
45
|
+
throw new Error(`Duplicate contract procedure protocol: ${procedureName}.${protocolName}`);
|
|
46
|
+
}
|
|
47
|
+
protocols[protocolName] = descriptor;
|
|
48
|
+
}
|
|
49
|
+
procedures[procedureName] = { kind: 'procedure', protocols };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
assertUniqueDispatchKeys(procedures);
|
|
53
|
+
return Object.freeze({
|
|
7
54
|
kind: 'contract',
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
};
|
|
55
|
+
procedures: Object.freeze(procedures),
|
|
56
|
+
});
|
|
11
57
|
}
|
|
12
|
-
|
|
58
|
+
export const contract = Object.assign(defineContract, {
|
|
59
|
+
merge: mergeContracts,
|
|
60
|
+
});
|
|
61
|
+
function assertUniqueDispatchKeys(procedures) {
|
|
13
62
|
const paths = new Map();
|
|
14
63
|
for (const [procedureName, procedureDefinition] of Object.entries(procedures)) {
|
|
15
64
|
for (const [protocolName, protocol] of Object.entries(procedureDefinition.protocols)) {
|
|
@@ -19,7 +68,7 @@ function assertUniqueDispatchKeys(procedures, contractName) {
|
|
|
19
68
|
if (typeof dispatchKey !== 'string') {
|
|
20
69
|
throw new Error(`Protocol dispatchKey must be a string or null: ${procedureName}.${protocolName}`);
|
|
21
70
|
}
|
|
22
|
-
const path = `${
|
|
71
|
+
const path = `${procedureName}.${protocolName}`;
|
|
23
72
|
const existing = paths.get(dispatchKey);
|
|
24
73
|
if (existing) {
|
|
25
74
|
throw new Error(`Duplicate protocol dispatch key "${dispatchKey}" between ${existing} and ${path}`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const LOUTRE_VERSION = "0.3.0";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type ApplicationCompilationInput } from './graph.js';
|
|
2
|
+
import type { CompilationResult } from './ir.js';
|
|
3
|
+
export declare function compileApplication(input: ApplicationCompilationInput): CompilationResult;
|
|
4
|
+
export declare const buildApplicationGraph: typeof compileApplication;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { asModuleInstance, normalizeProvider, tokenName, } from '../core/index.js';
|
|
2
|
+
import { compileApplication as compileBaseApplication, } from './graph.js';
|
|
3
|
+
export function compileApplication(input) {
|
|
4
|
+
const base = compileBaseApplication(input);
|
|
5
|
+
const modules = collectModules(input.modules);
|
|
6
|
+
const moduleIds = new Map(modules.map((module, index) => [module, `module:${index + 1}`]));
|
|
7
|
+
const implementationModules = collectImplementationModules(modules, moduleIds);
|
|
8
|
+
const implementationNames = new Map(base.graph.implementations.map((implementation) => [
|
|
9
|
+
implementation.id,
|
|
10
|
+
implementation.name,
|
|
11
|
+
]));
|
|
12
|
+
const graphModules = new Map(base.graph.modules.map((module) => [module.id, module]));
|
|
13
|
+
const executionModules = new Map();
|
|
14
|
+
for (const execution of base.graph.executions) {
|
|
15
|
+
if (execution.kind !== 'protocol')
|
|
16
|
+
continue;
|
|
17
|
+
const implementationName = implementationNames.get(execution.implementation);
|
|
18
|
+
const module = implementationName
|
|
19
|
+
? implementationModules.get(implementationName)
|
|
20
|
+
: undefined;
|
|
21
|
+
if (!module)
|
|
22
|
+
continue;
|
|
23
|
+
executionModules.set(`${execution.contract}:${execution.procedure}:${execution.protocol}`, module);
|
|
24
|
+
}
|
|
25
|
+
const providerModules = collectProviderModules(modules, moduleIds);
|
|
26
|
+
const nodes = base.graph.nodes.map((node) => {
|
|
27
|
+
const module = node.module ??
|
|
28
|
+
providerModules.get(node.label) ??
|
|
29
|
+
moduleForImplementation(node, implementationModules) ??
|
|
30
|
+
moduleForLifecycle(node) ??
|
|
31
|
+
moduleForLayer(node, executionModules);
|
|
32
|
+
if (!module)
|
|
33
|
+
return node;
|
|
34
|
+
const owner = graphModules.get(module);
|
|
35
|
+
if (!owner)
|
|
36
|
+
return node;
|
|
37
|
+
return {
|
|
38
|
+
...node,
|
|
39
|
+
module,
|
|
40
|
+
visibility: owner.exports.includes(node.label) ? 'exported' : 'private',
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
const diagnostics = [
|
|
44
|
+
...base.diagnostics,
|
|
45
|
+
...validateModuleExports(base.graph.modules),
|
|
46
|
+
...validateModuleVisibility(base.graph, nodes),
|
|
47
|
+
];
|
|
48
|
+
const graph = {
|
|
49
|
+
...base.graph,
|
|
50
|
+
nodes,
|
|
51
|
+
diagnostics,
|
|
52
|
+
};
|
|
53
|
+
return { graph, diagnostics };
|
|
54
|
+
}
|
|
55
|
+
export const buildApplicationGraph = compileApplication;
|
|
56
|
+
function collectModules(roots) {
|
|
57
|
+
const modules = [];
|
|
58
|
+
const seen = new Set();
|
|
59
|
+
const visit = (candidate) => {
|
|
60
|
+
const module = asModuleInstance(candidate);
|
|
61
|
+
if (seen.has(module))
|
|
62
|
+
return;
|
|
63
|
+
seen.add(module);
|
|
64
|
+
for (const imported of module.definition.imports ?? [])
|
|
65
|
+
visit(imported);
|
|
66
|
+
modules.push(module);
|
|
67
|
+
};
|
|
68
|
+
for (const root of roots)
|
|
69
|
+
visit(root);
|
|
70
|
+
return modules;
|
|
71
|
+
}
|
|
72
|
+
function collectProviderModules(modules, moduleIds) {
|
|
73
|
+
const result = new Map();
|
|
74
|
+
const ambiguous = new Set();
|
|
75
|
+
for (const module of modules) {
|
|
76
|
+
const moduleId = moduleIds.get(module);
|
|
77
|
+
if (!moduleId)
|
|
78
|
+
continue;
|
|
79
|
+
for (const declaration of module.definition.providers ?? []) {
|
|
80
|
+
const provider = normalizeProvider(declaration);
|
|
81
|
+
const labels = [
|
|
82
|
+
tokenName(provider.provide),
|
|
83
|
+
...(provider.kind === 'class' ? [provider.useClass.name] : []),
|
|
84
|
+
...(provider.kind === 'conditional'
|
|
85
|
+
? Reflect.ownKeys(provider.mapping).flatMap((key) => {
|
|
86
|
+
const candidate = provider.mapping[key];
|
|
87
|
+
return candidate ? [candidate.name] : [];
|
|
88
|
+
})
|
|
89
|
+
: []),
|
|
90
|
+
];
|
|
91
|
+
for (const label of labels) {
|
|
92
|
+
const current = result.get(label);
|
|
93
|
+
if (current && current !== moduleId)
|
|
94
|
+
ambiguous.add(label);
|
|
95
|
+
else
|
|
96
|
+
result.set(label, moduleId);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
for (const label of ambiguous)
|
|
101
|
+
result.delete(label);
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
function collectImplementationModules(modules, moduleIds) {
|
|
105
|
+
const result = new Map();
|
|
106
|
+
const ambiguous = new Set();
|
|
107
|
+
for (const module of modules) {
|
|
108
|
+
const moduleId = moduleIds.get(module);
|
|
109
|
+
if (!moduleId)
|
|
110
|
+
continue;
|
|
111
|
+
for (const implementation of module.definition.implementations ?? []) {
|
|
112
|
+
const current = result.get(implementation.name);
|
|
113
|
+
if (current && current !== moduleId)
|
|
114
|
+
ambiguous.add(implementation.name);
|
|
115
|
+
else
|
|
116
|
+
result.set(implementation.name, moduleId);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for (const name of ambiguous)
|
|
120
|
+
result.delete(name);
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
function moduleForImplementation(node, modules) {
|
|
124
|
+
return node.kind === 'implementation' ? modules.get(node.label) : undefined;
|
|
125
|
+
}
|
|
126
|
+
function moduleForLifecycle(node) {
|
|
127
|
+
if (node.kind !== 'framework' || !node.id.startsWith('lifecycle:module:')) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
const match = /^lifecycle:(module:\d+):/.exec(node.id);
|
|
131
|
+
return match?.[1];
|
|
132
|
+
}
|
|
133
|
+
function moduleForLayer(node, executionModules) {
|
|
134
|
+
if (node.kind !== 'layer')
|
|
135
|
+
return undefined;
|
|
136
|
+
const match = /^layer:(contract:\d+)\/([^/]+)\/([^/]+)\//.exec(node.id);
|
|
137
|
+
if (!match)
|
|
138
|
+
return undefined;
|
|
139
|
+
const [, contract, procedure, protocol] = match;
|
|
140
|
+
return executionModules.get(`${contract}:${procedure}:${protocol}`);
|
|
141
|
+
}
|
|
142
|
+
function validateModuleExports(modules) {
|
|
143
|
+
const byId = new Map(modules.map((module) => [module.id, module]));
|
|
144
|
+
return modules.flatMap((module) => module.exports.flatMap((token) => declaresToken(module, token) ||
|
|
145
|
+
canResolveExport(module, token, byId, new Set())
|
|
146
|
+
? []
|
|
147
|
+
: [
|
|
148
|
+
{
|
|
149
|
+
code: 'LUTRE_MODULE_EXPORT_UNRESOLVED',
|
|
150
|
+
message: `${describeModule(module)} exports ${token}, but the token is neither declared nor re-exported from an imported Module.`,
|
|
151
|
+
path: `${module.id}.exports.${token}`,
|
|
152
|
+
},
|
|
153
|
+
]));
|
|
154
|
+
}
|
|
155
|
+
function declaresToken(module, token) {
|
|
156
|
+
return module.providers.includes(token) || module.environment.includes(token);
|
|
157
|
+
}
|
|
158
|
+
function canResolveExport(source, token, modules, visited) {
|
|
159
|
+
if (visited.has(source.id))
|
|
160
|
+
return false;
|
|
161
|
+
visited.add(source.id);
|
|
162
|
+
for (const importedId of source.imports) {
|
|
163
|
+
const imported = modules.get(importedId);
|
|
164
|
+
if (!imported || !imported.exports.includes(token))
|
|
165
|
+
continue;
|
|
166
|
+
if (declaresToken(imported, token))
|
|
167
|
+
return true;
|
|
168
|
+
if (canResolveExport(imported, token, modules, new Set(visited)))
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
function validateModuleVisibility(graph, nodes) {
|
|
174
|
+
const modules = new Map(graph.modules.map((module) => [module.id, module]));
|
|
175
|
+
const nodesById = new Map(nodes.map((node) => [node.id, node]));
|
|
176
|
+
const seen = new Set();
|
|
177
|
+
const diagnostics = [];
|
|
178
|
+
for (const edge of graph.edges) {
|
|
179
|
+
const consumer = nodesById.get(edge.from);
|
|
180
|
+
const dependency = nodesById.get(edge.to);
|
|
181
|
+
if (!consumer?.module ||
|
|
182
|
+
!dependency?.module ||
|
|
183
|
+
consumer.module === dependency.module ||
|
|
184
|
+
!modules.has(consumer.module) ||
|
|
185
|
+
!modules.has(dependency.module)) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const key = `${consumer.id}->${dependency.id}`;
|
|
189
|
+
if (seen.has(key))
|
|
190
|
+
continue;
|
|
191
|
+
seen.add(key);
|
|
192
|
+
if (isTokenVisibleFrom(consumer.module, dependency.module, dependency.label, modules)) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
diagnostics.push({
|
|
196
|
+
code: 'LUTRE_MODULE_VISIBILITY',
|
|
197
|
+
message: `${consumer.label} in ${describeModuleId(consumer.module, modules)} depends on private ${dependency.label} from ${describeModuleId(dependency.module, modules)}. Export the dependency and import its Module explicitly.`,
|
|
198
|
+
path: consumer.id,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return diagnostics;
|
|
202
|
+
}
|
|
203
|
+
function isTokenVisibleFrom(sourceId, targetId, token, modules, visited = new Set()) {
|
|
204
|
+
if (visited.has(sourceId))
|
|
205
|
+
return false;
|
|
206
|
+
visited.add(sourceId);
|
|
207
|
+
const source = modules.get(sourceId);
|
|
208
|
+
if (!source)
|
|
209
|
+
return false;
|
|
210
|
+
for (const importedId of source.imports) {
|
|
211
|
+
const imported = modules.get(importedId);
|
|
212
|
+
if (!imported || !imported.exports.includes(token))
|
|
213
|
+
continue;
|
|
214
|
+
if (importedId === targetId)
|
|
215
|
+
return true;
|
|
216
|
+
if (isTokenVisibleFrom(importedId, targetId, token, modules, new Set(visited))) {
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
function describeModule(module) {
|
|
223
|
+
return module.name ?? module.description ?? module.id;
|
|
224
|
+
}
|
|
225
|
+
function describeModuleId(id, modules) {
|
|
226
|
+
const module = modules.get(id);
|
|
227
|
+
return module ? describeModule(module) : id;
|
|
228
|
+
}
|
package/dist/graph/graph.js
CHANGED
|
@@ -38,17 +38,29 @@ export function compileApplication(input) {
|
|
|
38
38
|
validateNamedDescriptors(queues, 'LUTRE_QUEUE_DUPLICATE', diagnostics);
|
|
39
39
|
validateCronTriggers(triggers, diagnostics);
|
|
40
40
|
diagnostics.push(...validateQueueDrivers(input.modules, queueTriggers));
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const current = contractNames.get(contract);
|
|
41
|
+
const contractIds = new Map();
|
|
42
|
+
const idContract = (contract) => {
|
|
43
|
+
const current = contractIds.get(contract);
|
|
45
44
|
if (current)
|
|
46
45
|
return current;
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
return
|
|
46
|
+
const id = `contract:${contractIds.size + 1}`;
|
|
47
|
+
contractIds.set(contract, id);
|
|
48
|
+
return id;
|
|
49
|
+
};
|
|
50
|
+
const implementationIds = new Map();
|
|
51
|
+
const idImplementation = (implementation) => {
|
|
52
|
+
const current = implementationIds.get(implementation);
|
|
53
|
+
if (current)
|
|
54
|
+
return current;
|
|
55
|
+
const id = `implementation:${implementationIds.size + 1}`;
|
|
56
|
+
implementationIds.set(implementation, id);
|
|
57
|
+
return id;
|
|
50
58
|
};
|
|
51
59
|
const descriptors = modules.flatMap((module) => module.definition.implementations ?? []);
|
|
60
|
+
for (const implementation of new Set(descriptors)) {
|
|
61
|
+
idContract(implementation.contract);
|
|
62
|
+
idImplementation(implementation);
|
|
63
|
+
}
|
|
52
64
|
const targets = [];
|
|
53
65
|
for (const implementation of descriptors) {
|
|
54
66
|
for (const procedureName of implementation.procedures) {
|
|
@@ -58,13 +70,14 @@ export function compileApplication(input) {
|
|
|
58
70
|
diagnostics.push({
|
|
59
71
|
code: 'LUTRE_IMPL_003',
|
|
60
72
|
message: `${procedureName} is not declared for protocol ${implementation.protocol}`,
|
|
61
|
-
path: `${
|
|
73
|
+
path: `${idContract(implementation.contract)}.${procedureName}.${implementation.protocol}`,
|
|
62
74
|
});
|
|
63
75
|
continue;
|
|
64
76
|
}
|
|
65
77
|
targets.push({
|
|
66
78
|
implementation,
|
|
67
|
-
|
|
79
|
+
contractId: idContract(implementation.contract),
|
|
80
|
+
implementationId: idImplementation(implementation),
|
|
68
81
|
procedure: procedureName,
|
|
69
82
|
protocol: implementation.protocol,
|
|
70
83
|
dispatchKey: protocol.dispatchKey,
|
|
@@ -77,28 +90,27 @@ export function compileApplication(input) {
|
|
|
77
90
|
}
|
|
78
91
|
}
|
|
79
92
|
validateDispatchKeys(targets, diagnostics);
|
|
80
|
-
validateCoverage(descriptors,
|
|
93
|
+
validateCoverage(descriptors, contractIds, diagnostics);
|
|
81
94
|
validateDuplicateProviders(modules, diagnostics);
|
|
82
95
|
const tokensById = collectCustomTokens(providers, targets, diagnostics);
|
|
83
96
|
const contextKeysByName = collectContextKeys(targets, diagnostics);
|
|
84
97
|
const pipelines = [];
|
|
85
|
-
const implementations = [];
|
|
86
98
|
for (const target of targets) {
|
|
87
99
|
validatePipeline(target, diagnostics);
|
|
88
100
|
pipelines.push({
|
|
89
|
-
contract: target.
|
|
101
|
+
contract: target.contractId,
|
|
90
102
|
procedure: target.procedure,
|
|
91
103
|
protocol: target.protocol,
|
|
92
104
|
layers: target.pipeline.map(toLayerIR),
|
|
93
105
|
});
|
|
94
|
-
implementations.push({
|
|
95
|
-
contract: target.contractName,
|
|
96
|
-
procedure: target.procedure,
|
|
97
|
-
protocol: target.protocol,
|
|
98
|
-
implementation: target.implementation.name,
|
|
99
|
-
method: target.procedure,
|
|
100
|
-
});
|
|
101
106
|
}
|
|
107
|
+
const implementations = [...new Set(descriptors)].map((implementation) => ({
|
|
108
|
+
id: idImplementation(implementation),
|
|
109
|
+
name: implementation.name,
|
|
110
|
+
contract: idContract(implementation.contract),
|
|
111
|
+
protocol: implementation.protocol,
|
|
112
|
+
procedures: implementation.procedures,
|
|
113
|
+
}));
|
|
102
114
|
const dependencyGraph = buildDependencyGraph(modules, descriptors, targets, tasks, diagnostics);
|
|
103
115
|
const probedTokenIds = dependencyGraph.nodes
|
|
104
116
|
.filter((node) => node.kind === 'token')
|
|
@@ -150,7 +162,7 @@ export function compileApplication(input) {
|
|
|
150
162
|
})),
|
|
151
163
|
tokens: [...new Set([...tokensById.keys(), ...probedTokenIds])].map((id) => ({ id })),
|
|
152
164
|
contextKeys: [...contextKeysByName.keys()].map((name) => ({ name })),
|
|
153
|
-
contracts: [...
|
|
165
|
+
contracts: [...contractIds.entries()].map(([contract, id]) => toContractIR(contract, id)),
|
|
154
166
|
pipelines,
|
|
155
167
|
implementations,
|
|
156
168
|
tasks: tasks.map((task) => ({
|
|
@@ -161,12 +173,12 @@ export function compileApplication(input) {
|
|
|
161
173
|
queues: queues.map(toQueueIR),
|
|
162
174
|
executions: [
|
|
163
175
|
...targets.map((target) => ({
|
|
164
|
-
id: `protocol:${target.
|
|
176
|
+
id: `protocol:${target.contractId}:${target.procedure}:${target.protocol}`,
|
|
165
177
|
kind: 'protocol',
|
|
166
178
|
protocol: target.protocol,
|
|
167
|
-
contract: target.
|
|
179
|
+
contract: target.contractId,
|
|
168
180
|
procedure: target.procedure,
|
|
169
|
-
implementation: target.
|
|
181
|
+
implementation: target.implementationId,
|
|
170
182
|
})),
|
|
171
183
|
...publicTasks.map((task) => ({
|
|
172
184
|
id: `task:${task.name}`,
|
|
@@ -177,7 +189,7 @@ export function compileApplication(input) {
|
|
|
177
189
|
],
|
|
178
190
|
capabilities: [
|
|
179
191
|
...targets.flatMap((target) => {
|
|
180
|
-
const requiredBy = `${target.
|
|
192
|
+
const requiredBy = `${target.contractId}.${target.procedure}`;
|
|
181
193
|
return [
|
|
182
194
|
{ name: 'crypto.random', scope: 'execution', requiredBy },
|
|
183
195
|
...(target.protocol === 'http'
|
|
@@ -257,7 +269,7 @@ function validateDispatchKeys(targets, diagnostics) {
|
|
|
257
269
|
for (const target of targets) {
|
|
258
270
|
if (target.dispatchKey === null)
|
|
259
271
|
continue;
|
|
260
|
-
const path = `${target.
|
|
272
|
+
const path = `${target.contractId}.${target.procedure}.${target.protocol}`;
|
|
261
273
|
const existing = targetsByKey.get(target.dispatchKey);
|
|
262
274
|
if (existing) {
|
|
263
275
|
if (existing.implementation.contract === target.implementation.contract &&
|
|
@@ -265,7 +277,7 @@ function validateDispatchKeys(targets, diagnostics) {
|
|
|
265
277
|
existing.protocol === target.protocol) {
|
|
266
278
|
continue;
|
|
267
279
|
}
|
|
268
|
-
const existingPath = `${existing.
|
|
280
|
+
const existingPath = `${existing.contractId}.${existing.procedure}.${existing.protocol}`;
|
|
269
281
|
diagnostics.push({
|
|
270
282
|
code: 'LUTRE_PROTOCOL_001',
|
|
271
283
|
message: `Duplicate protocol dispatch key "${target.dispatchKey}": ${existingPath}, ${path}`,
|
|
@@ -465,7 +477,7 @@ function buildDependencyGraph(modules, descriptors, targets, tasks, diagnostics)
|
|
|
465
477
|
diagnostic.message.includes(token.id))) {
|
|
466
478
|
diagnostics.push({
|
|
467
479
|
code: 'LUTRE_TOKEN_001',
|
|
468
|
-
message: `Token ID ${token.id}
|
|
480
|
+
message: `Token ID ${token.id} is duplicated across different token declarations`,
|
|
469
481
|
path: `dependency:${token.id}`,
|
|
470
482
|
});
|
|
471
483
|
}
|
|
@@ -736,7 +748,7 @@ function buildDependencyGraph(modules, descriptors, targets, tasks, diagnostics)
|
|
|
736
748
|
const definition = layerDefinitionOf(item);
|
|
737
749
|
const consumer = {
|
|
738
750
|
kind: 'layer-consumer',
|
|
739
|
-
id: `layer:${target.
|
|
751
|
+
id: `layer:${target.contractId}/${target.procedure}/${target.protocol}/${indexPath.join('.')}`,
|
|
740
752
|
name: definition.name,
|
|
741
753
|
};
|
|
742
754
|
if (!nodes.some(({ id }) => id === consumer.id)) {
|
|
@@ -803,7 +815,7 @@ function validateDuplicateProviders(modules, diagnostics) {
|
|
|
803
815
|
}
|
|
804
816
|
diagnostics.push({
|
|
805
817
|
code: 'LUTRE_DI_003',
|
|
806
|
-
message: `Provider ${tokenName(provider.provide)}
|
|
818
|
+
message: `Provider ${tokenName(provider.provide)} is duplicated in ${existing.module} and ${moduleName}`,
|
|
807
819
|
path: `${moduleName}.providers.${tokenName(provider.provide)}`,
|
|
808
820
|
});
|
|
809
821
|
continue;
|
|
@@ -827,7 +839,7 @@ function collectCustomTokens(providers, targets, diagnostics) {
|
|
|
827
839
|
if (existing && existing !== candidate) {
|
|
828
840
|
diagnostics.push({
|
|
829
841
|
code: 'LUTRE_TOKEN_001',
|
|
830
|
-
message: `Token ID ${candidate.id}
|
|
842
|
+
message: `Token ID ${candidate.id} is duplicated across different token declarations`,
|
|
831
843
|
path,
|
|
832
844
|
});
|
|
833
845
|
return;
|
|
@@ -847,7 +859,7 @@ function collectCustomTokens(providers, targets, diagnostics) {
|
|
|
847
859
|
function collectContextKeys(targets, diagnostics) {
|
|
848
860
|
const keys = new Map();
|
|
849
861
|
for (const target of targets) {
|
|
850
|
-
const path = `${target.
|
|
862
|
+
const path = `${target.contractId}.${target.procedure}.${target.protocol}`;
|
|
851
863
|
visitPipelineItems(target.pipeline, (item) => {
|
|
852
864
|
if (item.kind !== 'layer')
|
|
853
865
|
return;
|
|
@@ -856,7 +868,7 @@ function collectContextKeys(targets, diagnostics) {
|
|
|
856
868
|
if (existing && existing !== key) {
|
|
857
869
|
diagnostics.push({
|
|
858
870
|
code: 'LUTRE_CONTEXT_002',
|
|
859
|
-
message: `Context Key ${key.name}
|
|
871
|
+
message: `Context Key ${key.name} is duplicated across different declarations`,
|
|
860
872
|
path,
|
|
861
873
|
});
|
|
862
874
|
continue;
|
|
@@ -867,6 +879,19 @@ function collectContextKeys(targets, diagnostics) {
|
|
|
867
879
|
}
|
|
868
880
|
return keys;
|
|
869
881
|
}
|
|
882
|
+
function toContractIR(contract, id) {
|
|
883
|
+
return {
|
|
884
|
+
id,
|
|
885
|
+
procedures: Object.entries(contract.procedures).map(([procedureName, procedure]) => ({
|
|
886
|
+
name: procedureName,
|
|
887
|
+
protocols: Object.entries(procedure.protocols).map(([protocolName, protocol]) => ({
|
|
888
|
+
name: protocolName,
|
|
889
|
+
dispatchKey: protocol.dispatchKey,
|
|
890
|
+
interaction: protocol.interaction ?? 'unary',
|
|
891
|
+
})),
|
|
892
|
+
})),
|
|
893
|
+
};
|
|
894
|
+
}
|
|
870
895
|
export function assertValidCompilation(result) {
|
|
871
896
|
if (result.diagnostics.length > 0) {
|
|
872
897
|
throw new StaticValidationError(result.diagnostics, result.graph);
|
|
@@ -889,7 +914,7 @@ function collectModules(roots) {
|
|
|
889
914
|
visit(root);
|
|
890
915
|
return modules;
|
|
891
916
|
}
|
|
892
|
-
function validateCoverage(implementations,
|
|
917
|
+
function validateCoverage(implementations, contractIds, diagnostics) {
|
|
893
918
|
const contracts = new Set(implementations.map((implementation) => implementation.contract));
|
|
894
919
|
for (const contract of contracts) {
|
|
895
920
|
const protocols = new Set(Object.values(contract.procedures).flatMap((procedure) => Object.keys(procedure.protocols)));
|
|
@@ -900,7 +925,7 @@ function validateCoverage(implementations, contractNames, diagnostics) {
|
|
|
900
925
|
const covering = implementations.filter((implementation) => implementation.contract === contract &&
|
|
901
926
|
implementation.protocol === protocol &&
|
|
902
927
|
implementation.procedures.includes(procedureName));
|
|
903
|
-
const path = `${
|
|
928
|
+
const path = `${contractIds.get(contract) ?? 'contract:unknown'}.${procedureName}.${protocol}`;
|
|
904
929
|
if (covering.length === 0) {
|
|
905
930
|
diagnostics.push({
|
|
906
931
|
code: 'LUTRE_IMPL_001',
|
|
@@ -920,7 +945,7 @@ function validateCoverage(implementations, contractNames, diagnostics) {
|
|
|
920
945
|
}
|
|
921
946
|
}
|
|
922
947
|
function validatePipeline(target, diagnostics) {
|
|
923
|
-
const path = `${target.
|
|
948
|
+
const path = `${target.contractId}.${target.procedure}.${target.protocol}`;
|
|
924
949
|
const flattened = [];
|
|
925
950
|
visitPipelineItems(target.pipeline, (item, indexPath) => {
|
|
926
951
|
flattened.push({ item, indexPath });
|
|
@@ -964,7 +989,7 @@ function validatePipeline(target, diagnostics) {
|
|
|
964
989
|
if (!validated.has(required)) {
|
|
965
990
|
diagnostics.push({
|
|
966
991
|
code: 'LUTRE_VALIDATION_001',
|
|
967
|
-
message: `${item.name}
|
|
992
|
+
message: `${item.name} requires validated ${required} but appears before validate.${required}`,
|
|
968
993
|
path,
|
|
969
994
|
});
|
|
970
995
|
}
|
|
@@ -973,7 +998,7 @@ function validatePipeline(target, diagnostics) {
|
|
|
973
998
|
if (!available.has(required)) {
|
|
974
999
|
diagnostics.push({
|
|
975
1000
|
code: 'LUTRE_PIPELINE_004',
|
|
976
|
-
message:
|
|
1001
|
+
message: `Context Key ${contextKeyName(required)} required by ${item.name} is unavailable`,
|
|
977
1002
|
path,
|
|
978
1003
|
});
|
|
979
1004
|
}
|
|
@@ -982,7 +1007,7 @@ function validatePipeline(target, diagnostics) {
|
|
|
982
1007
|
if (available.has(provided)) {
|
|
983
1008
|
diagnostics.push({
|
|
984
1009
|
code: 'LUTRE_CONTEXT_003',
|
|
985
|
-
message: `${item.name}
|
|
1010
|
+
message: `${item.name} cannot implicitly overwrite existing Context Key ${contextKeyName(provided)}`,
|
|
986
1011
|
path,
|
|
987
1012
|
});
|
|
988
1013
|
}
|
|
@@ -995,7 +1020,7 @@ function validatePipeline(target, diagnostics) {
|
|
|
995
1020
|
if (!response) {
|
|
996
1021
|
diagnostics.push({
|
|
997
1022
|
code: 'LUTRE_SHORT_CIRCUIT_001',
|
|
998
|
-
message:
|
|
1023
|
+
message: `Short-circuit variant ${shortCircuit.variant} from ${item.name} is not declared in the response`,
|
|
999
1024
|
path,
|
|
1000
1025
|
});
|
|
1001
1026
|
continue;
|
|
@@ -1005,7 +1030,7 @@ function validatePipeline(target, diagnostics) {
|
|
|
1005
1030
|
response.status !== expectedStatus) {
|
|
1006
1031
|
diagnostics.push({
|
|
1007
1032
|
code: 'LUTRE_SHORT_CIRCUIT_002',
|
|
1008
|
-
message:
|
|
1033
|
+
message: `Short-circuit variant ${shortCircuit.variant} from ${item.name} must use HTTP ${expectedStatus}`,
|
|
1009
1034
|
path,
|
|
1010
1035
|
});
|
|
1011
1036
|
}
|
package/dist/graph/index.d.ts
CHANGED
package/dist/graph/index.js
CHANGED