@doxajs/compiler 0.1.0-alpha.1
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 +201 -0
- package/NOTICE +4 -0
- package/README.md +8 -0
- package/dist/compiler 2.js +1573 -0
- package/dist/compiler.d 2.ts +17 -0
- package/dist/compiler.d.ts +17 -0
- package/dist/compiler.d.ts 2.map +1 -0
- package/dist/compiler.d.ts.map +1 -0
- package/dist/compiler.js +1594 -0
- package/dist/compiler.js 2.map +1 -0
- package/dist/compiler.js.map +1 -0
- package/dist/errors 2.js +4 -0
- package/dist/errors.d 2.ts +4 -0
- package/dist/errors.d.ts +4 -0
- package/dist/errors.d.ts 2.map +1 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +4 -0
- package/dist/errors.js 2.map +1 -0
- package/dist/errors.js.map +1 -0
- package/dist/index 2.js +2 -0
- package/dist/index.d 2.ts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts 2.map +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js 2.map +1 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest-validation 2.js +59 -0
- package/dist/manifest-validation.d 2.ts +5 -0
- package/dist/manifest-validation.d.ts +5 -0
- package/dist/manifest-validation.d.ts 2.map +1 -0
- package/dist/manifest-validation.d.ts.map +1 -0
- package/dist/manifest-validation.js +59 -0
- package/dist/manifest-validation.js 2.map +1 -0
- package/dist/manifest-validation.js.map +1 -0
- package/package.json +44 -0
package/dist/compiler.js
ADDED
|
@@ -0,0 +1,1594 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import ts from 'typescript';
|
|
5
|
+
import { MANIFEST_FORMAT_VERSION, canonicalJson, } from '@doxajs/manifest';
|
|
6
|
+
import { DoxaCompilationError } from './errors.js';
|
|
7
|
+
import { assertAcyclicProviderGraph, assertScopeSafety, assertUnique, } from './manifest-validation.js';
|
|
8
|
+
export { DoxaCompilationError } from './errors.js';
|
|
9
|
+
const FRAMEWORK_VERSION = '0.1.0';
|
|
10
|
+
const COMPILER_VERSION = '0.1.0';
|
|
11
|
+
const DECLARATION_FIELDS = new Set([
|
|
12
|
+
'id',
|
|
13
|
+
'features',
|
|
14
|
+
'configs',
|
|
15
|
+
'providers',
|
|
16
|
+
'actions',
|
|
17
|
+
'queries',
|
|
18
|
+
'models',
|
|
19
|
+
'observers',
|
|
20
|
+
'routes',
|
|
21
|
+
'events',
|
|
22
|
+
'listeners',
|
|
23
|
+
'jobs',
|
|
24
|
+
'schedules',
|
|
25
|
+
'policies',
|
|
26
|
+
'signals',
|
|
27
|
+
'signalHandlers',
|
|
28
|
+
'commands',
|
|
29
|
+
]);
|
|
30
|
+
export async function compileApplication(options) {
|
|
31
|
+
const normalized = normalizeOptions(options);
|
|
32
|
+
const program = createProgram(normalized.tsconfigPath);
|
|
33
|
+
const checker = program.getTypeChecker();
|
|
34
|
+
assertValidProgram(program);
|
|
35
|
+
const applicationSource = program.getSourceFile(normalized.applicationFile);
|
|
36
|
+
if (!applicationSource) {
|
|
37
|
+
throw new DoxaCompilationError(`Application source is not part of the TypeScript program: ${normalized.applicationFile}`);
|
|
38
|
+
}
|
|
39
|
+
const applicationDeclaration = findExportedClass(applicationSource, normalized.applicationExport);
|
|
40
|
+
assertDeclarationOnly(applicationDeclaration, 'Application');
|
|
41
|
+
const applicationId = readRequiredInstanceString(applicationDeclaration, 'id');
|
|
42
|
+
const applicationName = requiredClassName(applicationDeclaration);
|
|
43
|
+
const featureDeclarations = readClassArray(applicationDeclaration, 'features', checker);
|
|
44
|
+
const features = featureDeclarations.map((declaration) => {
|
|
45
|
+
assertDeclarationOnly(declaration, 'Feature');
|
|
46
|
+
return {
|
|
47
|
+
id: readRequiredInstanceString(declaration, 'id'),
|
|
48
|
+
name: requiredClassName(declaration),
|
|
49
|
+
source: sourceOf(declaration, normalized.projectRoot),
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
assertUnique(features, (feature) => feature.id, 'Feature ID');
|
|
53
|
+
const configurations = [];
|
|
54
|
+
const configurationByDeclaration = new Map();
|
|
55
|
+
registerConfigurations(readClassArray(applicationDeclaration, 'configs', checker), `application:${applicationId}`);
|
|
56
|
+
for (let index = 0; index < featureDeclarations.length; index += 1) {
|
|
57
|
+
const declaration = featureDeclarations[index];
|
|
58
|
+
const feature = features[index];
|
|
59
|
+
if (!declaration || !feature)
|
|
60
|
+
continue;
|
|
61
|
+
registerConfigurations(readClassArray(declaration, 'configs', checker), feature.id);
|
|
62
|
+
}
|
|
63
|
+
const providers = [];
|
|
64
|
+
const providerByDeclaration = new Map();
|
|
65
|
+
const providerRoots = new Map();
|
|
66
|
+
const actions = [];
|
|
67
|
+
const queries = [];
|
|
68
|
+
const operationByDeclaration = new Map();
|
|
69
|
+
const operationRoots = new Map();
|
|
70
|
+
const models = [];
|
|
71
|
+
const modelByDeclaration = new Map();
|
|
72
|
+
const modelRoots = new Map();
|
|
73
|
+
const observers = [];
|
|
74
|
+
const observerByDeclaration = new Map();
|
|
75
|
+
const observerRoots = new Map();
|
|
76
|
+
const routes = [];
|
|
77
|
+
const routeByDeclaration = new Map();
|
|
78
|
+
const routeRoots = new Map();
|
|
79
|
+
const events = [];
|
|
80
|
+
const eventByDeclaration = new Map();
|
|
81
|
+
const eventRoots = new Map();
|
|
82
|
+
const listeners = [];
|
|
83
|
+
const listenerByDeclaration = new Map();
|
|
84
|
+
const listenerRoots = new Map();
|
|
85
|
+
const jobs = [];
|
|
86
|
+
const jobByDeclaration = new Map();
|
|
87
|
+
const jobRoots = new Map();
|
|
88
|
+
const schedules = [];
|
|
89
|
+
const scheduleByDeclaration = new Map();
|
|
90
|
+
const scheduleRoots = new Map();
|
|
91
|
+
const policies = [];
|
|
92
|
+
const policyByDeclaration = new Map();
|
|
93
|
+
const policyRoots = new Map();
|
|
94
|
+
const signals = [];
|
|
95
|
+
const signalByDeclaration = new Map();
|
|
96
|
+
const signalRoots = new Map();
|
|
97
|
+
const signalHandlers = [];
|
|
98
|
+
const signalHandlerByDeclaration = new Map();
|
|
99
|
+
const signalHandlerRoots = new Map();
|
|
100
|
+
const commands = [];
|
|
101
|
+
const commandByDeclaration = new Map();
|
|
102
|
+
const commandRoots = new Map();
|
|
103
|
+
for (let index = 0; index < featureDeclarations.length; index += 1) {
|
|
104
|
+
const featureDeclaration = featureDeclarations[index];
|
|
105
|
+
const feature = features[index];
|
|
106
|
+
if (!featureDeclaration || !feature)
|
|
107
|
+
continue;
|
|
108
|
+
for (const providerDeclaration of readClassArray(featureDeclaration, 'providers', checker)) {
|
|
109
|
+
const existing = providerRoots.get(providerDeclaration);
|
|
110
|
+
if (existing && existing.ownerId !== feature.id) {
|
|
111
|
+
fail(providerDeclaration, `${requiredClassName(providerDeclaration)} is declared as a provider by multiple Features.`);
|
|
112
|
+
}
|
|
113
|
+
providerRoots.set(providerDeclaration, { ownerId: feature.id });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
for (let index = 0; index < featureDeclarations.length; index += 1) {
|
|
117
|
+
const featureDeclaration = featureDeclarations[index];
|
|
118
|
+
const feature = features[index];
|
|
119
|
+
if (!featureDeclaration || !feature)
|
|
120
|
+
continue;
|
|
121
|
+
for (const action of readClassArray(featureDeclaration, 'actions', checker)) {
|
|
122
|
+
registerOperationRoot(action, feature.id, 'action');
|
|
123
|
+
}
|
|
124
|
+
for (const query of readClassArray(featureDeclaration, 'queries', checker)) {
|
|
125
|
+
registerOperationRoot(query, feature.id, 'query');
|
|
126
|
+
}
|
|
127
|
+
for (const model of readClassArray(featureDeclaration, 'models', checker)) {
|
|
128
|
+
const existing = modelRoots.get(model);
|
|
129
|
+
if (existing) {
|
|
130
|
+
fail(model, `${requiredClassName(model)} is already declared as a model by ${existing.ownerId}.`);
|
|
131
|
+
}
|
|
132
|
+
modelRoots.set(model, { ownerId: feature.id });
|
|
133
|
+
}
|
|
134
|
+
registerOwnedRoots(featureDeclaration, 'observers', feature.id, observerRoots, 'observer');
|
|
135
|
+
registerOwnedRoots(featureDeclaration, 'routes', feature.id, routeRoots, 'route');
|
|
136
|
+
registerOwnedRoots(featureDeclaration, 'events', feature.id, eventRoots, 'event');
|
|
137
|
+
registerOwnedRoots(featureDeclaration, 'listeners', feature.id, listenerRoots, 'listener');
|
|
138
|
+
registerOwnedRoots(featureDeclaration, 'jobs', feature.id, jobRoots, 'job');
|
|
139
|
+
registerOwnedRoots(featureDeclaration, 'schedules', feature.id, scheduleRoots, 'schedule');
|
|
140
|
+
registerOwnedRoots(featureDeclaration, 'policies', feature.id, policyRoots, 'policy');
|
|
141
|
+
registerOwnedRoots(featureDeclaration, 'signals', feature.id, signalRoots, 'signal');
|
|
142
|
+
registerOwnedRoots(featureDeclaration, 'signalHandlers', feature.id, signalHandlerRoots, 'signal handler');
|
|
143
|
+
registerOwnedRoots(featureDeclaration, 'commands', feature.id, commandRoots, 'command');
|
|
144
|
+
}
|
|
145
|
+
for (const [providerDeclaration, root] of providerRoots) {
|
|
146
|
+
registerProvider(providerDeclaration, root.ownerId, 'provider');
|
|
147
|
+
}
|
|
148
|
+
for (const [operation, root] of operationRoots) {
|
|
149
|
+
registerOperation(operation, root.ownerId, root.role);
|
|
150
|
+
}
|
|
151
|
+
for (const [model, root] of modelRoots) {
|
|
152
|
+
registerModel(model, root.ownerId);
|
|
153
|
+
}
|
|
154
|
+
for (const [observer, root] of observerRoots)
|
|
155
|
+
registerObserver(observer, root.ownerId);
|
|
156
|
+
for (const [event, root] of eventRoots) {
|
|
157
|
+
registerEvent(event, root.ownerId);
|
|
158
|
+
}
|
|
159
|
+
for (const [listener, root] of listenerRoots) {
|
|
160
|
+
registerListener(listener, root.ownerId);
|
|
161
|
+
}
|
|
162
|
+
for (const [route, root] of routeRoots) {
|
|
163
|
+
registerRoute(route, root.ownerId);
|
|
164
|
+
}
|
|
165
|
+
for (const [job, root] of jobRoots) {
|
|
166
|
+
registerJob(job, root.ownerId);
|
|
167
|
+
}
|
|
168
|
+
for (const [schedule, root] of scheduleRoots) {
|
|
169
|
+
registerSchedule(schedule, root.ownerId);
|
|
170
|
+
}
|
|
171
|
+
for (const [policy, root] of policyRoots) {
|
|
172
|
+
registerPolicy(policy, root.ownerId);
|
|
173
|
+
}
|
|
174
|
+
for (const [signal, root] of signalRoots)
|
|
175
|
+
registerSignal(signal, root.ownerId);
|
|
176
|
+
for (const [handler, root] of signalHandlerRoots)
|
|
177
|
+
registerSignalHandler(handler, root.ownerId);
|
|
178
|
+
for (const [command, root] of commandRoots)
|
|
179
|
+
registerCommand(command, root.ownerId);
|
|
180
|
+
assertUnique(providers, (provider) => provider.id, 'provider ID');
|
|
181
|
+
assertUnique(actions, (operation) => operation.id, 'action ID');
|
|
182
|
+
assertUnique(queries, (operation) => operation.id, 'query ID');
|
|
183
|
+
assertUnique(models, (model) => model.id, 'model ID');
|
|
184
|
+
assertUnique(observers, (observer) => observer.id, 'observer ID');
|
|
185
|
+
assertUnique(routes, (route) => route.id, 'route ID');
|
|
186
|
+
assertUnique(routes, (route) => `${route.method} ${route.path}`, 'HTTP route');
|
|
187
|
+
assertUnique(events, (event) => event.id, 'event ID');
|
|
188
|
+
assertUnique(listeners, (listener) => listener.id, 'listener ID');
|
|
189
|
+
assertUnique(jobs, (job) => job.id, 'job ID');
|
|
190
|
+
assertUnique(schedules, (schedule) => schedule.id, 'schedule ID');
|
|
191
|
+
assertUnique(policies, (policy) => policy.id, 'policy ID');
|
|
192
|
+
assertUnique(signals, (signal) => signal.id, 'signal ID');
|
|
193
|
+
assertUnique(signalHandlers, (handler) => handler.id, 'signal handler ID');
|
|
194
|
+
assertUnique(commands, (command) => command.id, 'command ID');
|
|
195
|
+
assertUnique(commands, (command) => command.command, 'command name');
|
|
196
|
+
assertUnique(policies.flatMap((policy) => policy.abilities.map((ability) => ({ ability, policy }))), (entry) => entry.ability, 'policy ability');
|
|
197
|
+
const policyAbilities = new Set(policies.flatMap((policy) => policy.abilities));
|
|
198
|
+
for (const entry of [
|
|
199
|
+
...routes,
|
|
200
|
+
...actions,
|
|
201
|
+
...queries,
|
|
202
|
+
...listeners,
|
|
203
|
+
...jobs,
|
|
204
|
+
...schedules,
|
|
205
|
+
...signalHandlers,
|
|
206
|
+
...commands,
|
|
207
|
+
]) {
|
|
208
|
+
if (entry.access !== 'public' && !policyAbilities.has(entry.access)) {
|
|
209
|
+
throw new DoxaCompilationError(`${entry.id} requires ability ${entry.access}, but no selected Policy declares it.`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
assertAcyclicProviderGraph(providers);
|
|
213
|
+
assertScopeSafety(providers);
|
|
214
|
+
const transactionProviders = providers.filter((provider) => provider.capabilities.includes('transactions'));
|
|
215
|
+
if (actions.length > 0 && transactionProviders.length !== 1) {
|
|
216
|
+
throw new DoxaCompilationError(`Applications with actions require exactly one transaction provider; found ${transactionProviders.length}.`);
|
|
217
|
+
}
|
|
218
|
+
const queueProviders = providers.filter((provider) => provider.capabilities.includes('queues'));
|
|
219
|
+
const authenticationProviders = providers.filter((provider) => provider.capabilities.includes('authentication'));
|
|
220
|
+
if (authenticationProviders.length > 1) {
|
|
221
|
+
throw new DoxaCompilationError(`Applications may declare at most one authentication provider; found ${authenticationProviders.length}.`);
|
|
222
|
+
}
|
|
223
|
+
const cacheProviders = providers.filter((provider) => provider.capabilities.includes('cache'));
|
|
224
|
+
if (cacheProviders.length > 1) {
|
|
225
|
+
throw new DoxaCompilationError(`Applications may declare at most one cache provider; found ${cacheProviders.length}.`);
|
|
226
|
+
}
|
|
227
|
+
for (const capability of ['mail', 'sms', 'broadcasting', 'telemetry']) {
|
|
228
|
+
const selected = providers.filter((provider) => provider.capabilities.includes(capability));
|
|
229
|
+
if (selected.length > 1)
|
|
230
|
+
throw new DoxaCompilationError(`Applications may declare at most one ${capability} provider; found ${selected.length}.`);
|
|
231
|
+
}
|
|
232
|
+
const queuedListeners = listeners.filter((listener) => listener.delivery === 'queued' || listener.delivery === 'queued-after-commit');
|
|
233
|
+
const queuedBroadcasts = events.filter((event) => event.broadcast === 'queued');
|
|
234
|
+
const broadcastingProviders = providers.filter((provider) => provider.capabilities.includes('broadcasting'));
|
|
235
|
+
if (events.some((event) => event.broadcast !== false) && broadcastingProviders.length !== 1) {
|
|
236
|
+
throw new DoxaCompilationError(`Applications with broadcast events require exactly one broadcasting provider; found ${broadcastingProviders.length}.`);
|
|
237
|
+
}
|
|
238
|
+
if (events.some((event) => event.broadcast !== false) &&
|
|
239
|
+
!policyAbilities.has('broadcast.subscribe')) {
|
|
240
|
+
throw new DoxaCompilationError('Applications with broadcast events must declare a Policy for the broadcast.subscribe ability.');
|
|
241
|
+
}
|
|
242
|
+
const communicationProviders = providers.filter((provider) => provider.capabilities.includes('mail') || provider.capabilities.includes('sms'));
|
|
243
|
+
if ((jobs.length > 0 ||
|
|
244
|
+
queuedListeners.length > 0 ||
|
|
245
|
+
queuedBroadcasts.length > 0 ||
|
|
246
|
+
schedules.length > 0 ||
|
|
247
|
+
communicationProviders.length > 0) &&
|
|
248
|
+
queueProviders.length !== 1) {
|
|
249
|
+
throw new DoxaCompilationError(`Applications with jobs, schedules, queued listeners, or queued broadcasts require exactly one queue provider; found ${queueProviders.length}.`);
|
|
250
|
+
}
|
|
251
|
+
const application = {
|
|
252
|
+
id: applicationId,
|
|
253
|
+
name: applicationName,
|
|
254
|
+
source: sourceOf(applicationDeclaration, normalized.projectRoot),
|
|
255
|
+
};
|
|
256
|
+
const semanticManifest = {
|
|
257
|
+
formatVersion: MANIFEST_FORMAT_VERSION,
|
|
258
|
+
applicationId,
|
|
259
|
+
frameworkVersion: FRAMEWORK_VERSION,
|
|
260
|
+
compilerVersion: COMPILER_VERSION,
|
|
261
|
+
application,
|
|
262
|
+
features: [...features].sort(byId),
|
|
263
|
+
configurations: [...configurations].sort(byId),
|
|
264
|
+
providers: [...providers].sort(byId),
|
|
265
|
+
actions: [...actions].sort(byId),
|
|
266
|
+
queries: [...queries].sort(byId),
|
|
267
|
+
models: [...models].sort(byId),
|
|
268
|
+
observers: [...observers].sort(byId),
|
|
269
|
+
routes: [...routes].sort(byId),
|
|
270
|
+
events: [...events].sort(byId),
|
|
271
|
+
listeners: [...listeners].sort(byId),
|
|
272
|
+
jobs: [...jobs].sort(byId),
|
|
273
|
+
schedules: [...schedules].sort(byId),
|
|
274
|
+
policies: [...policies].sort(byId),
|
|
275
|
+
signals: [...signals].sort(byId),
|
|
276
|
+
signalHandlers: [...signalHandlers].sort(byId),
|
|
277
|
+
commands: [...commands].sort(byId),
|
|
278
|
+
};
|
|
279
|
+
const buildHash = createHash('sha256').update(canonicalJson(semanticManifest)).digest('hex');
|
|
280
|
+
const manifest = { ...semanticManifest, buildHash };
|
|
281
|
+
await mkdir(normalized.artifactsDirectory, { recursive: true });
|
|
282
|
+
const manifestPath = path.join(normalized.artifactsDirectory, 'manifest.json');
|
|
283
|
+
const registryPath = path.join(normalized.artifactsDirectory, 'registry.mjs');
|
|
284
|
+
await writeFile(manifestPath, `${canonicalJson(manifest)}\n`, 'utf8');
|
|
285
|
+
await writeFile(registryPath, renderRegistry({
|
|
286
|
+
id: `application:${applicationId}`,
|
|
287
|
+
declaration: applicationDeclaration,
|
|
288
|
+
}, [...configurationByDeclaration.entries()].map(([declaration, entry]) => ({
|
|
289
|
+
id: entry.id,
|
|
290
|
+
declaration,
|
|
291
|
+
})), [...providerByDeclaration.entries()].map(([declaration, entry]) => ({
|
|
292
|
+
id: entry.id,
|
|
293
|
+
declaration,
|
|
294
|
+
})), [...operationByDeclaration.entries()].map(([declaration, entry]) => ({
|
|
295
|
+
id: entry.id,
|
|
296
|
+
declaration,
|
|
297
|
+
})), [...modelByDeclaration.entries()].map(([declaration, entry]) => ({
|
|
298
|
+
id: entry.id,
|
|
299
|
+
declaration,
|
|
300
|
+
})), [...observerByDeclaration.entries()].map(([declaration, entry]) => ({
|
|
301
|
+
id: entry.id,
|
|
302
|
+
declaration,
|
|
303
|
+
})), [...routeByDeclaration.entries()].map(([declaration, entry]) => ({
|
|
304
|
+
id: entry.id,
|
|
305
|
+
declaration,
|
|
306
|
+
})), [...eventByDeclaration.entries()].map(([declaration, entry]) => ({
|
|
307
|
+
id: entry.id,
|
|
308
|
+
declaration,
|
|
309
|
+
})), [...listenerByDeclaration.entries()].map(([declaration, entry]) => ({
|
|
310
|
+
id: entry.id,
|
|
311
|
+
declaration,
|
|
312
|
+
})), [...jobByDeclaration.entries()].map(([declaration, entry]) => ({
|
|
313
|
+
id: entry.id,
|
|
314
|
+
declaration,
|
|
315
|
+
})), [...scheduleByDeclaration.entries()].map(([declaration, entry]) => ({
|
|
316
|
+
id: entry.id,
|
|
317
|
+
declaration,
|
|
318
|
+
})), [...policyByDeclaration.entries()].map(([declaration, entry]) => ({
|
|
319
|
+
id: entry.id,
|
|
320
|
+
declaration,
|
|
321
|
+
})), [...signalByDeclaration.entries()].map(([declaration, entry]) => ({
|
|
322
|
+
id: entry.id,
|
|
323
|
+
declaration,
|
|
324
|
+
})), [...signalHandlerByDeclaration.entries()].map(([declaration, entry]) => ({
|
|
325
|
+
id: entry.id,
|
|
326
|
+
declaration,
|
|
327
|
+
})), [...commandByDeclaration.entries()].map(([declaration, entry]) => ({
|
|
328
|
+
id: entry.id,
|
|
329
|
+
declaration,
|
|
330
|
+
})), buildHash, normalized), 'utf8');
|
|
331
|
+
return { manifest, manifestPath, registryPath };
|
|
332
|
+
function registerConfigurations(declarations, ownerId) {
|
|
333
|
+
for (const declaration of declarations) {
|
|
334
|
+
const existing = configurationByDeclaration.get(declaration);
|
|
335
|
+
if (existing) {
|
|
336
|
+
if (existing.ownerId !== ownerId) {
|
|
337
|
+
fail(declaration, `Configuration ${existing.name} is declared by multiple owners.`);
|
|
338
|
+
}
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
assertConfigurationDeclaration(declaration);
|
|
342
|
+
const name = requiredClassName(declaration);
|
|
343
|
+
const localId = toKebabCase(name.replace(/Config$/, ''));
|
|
344
|
+
const id = `config:${ownerId}/${localId}`;
|
|
345
|
+
const properties = declaration.members
|
|
346
|
+
.filter(ts.isPropertyDeclaration)
|
|
347
|
+
.filter((property) => !hasModifier(property, ts.SyntaxKind.StaticKeyword))
|
|
348
|
+
.map((property) => compileConfigurationProperty(name, property, checker, normalized.projectRoot));
|
|
349
|
+
const entry = {
|
|
350
|
+
id,
|
|
351
|
+
ownerId,
|
|
352
|
+
name,
|
|
353
|
+
exportName: name,
|
|
354
|
+
source: sourceOf(declaration, normalized.projectRoot),
|
|
355
|
+
properties,
|
|
356
|
+
};
|
|
357
|
+
configurations.push(entry);
|
|
358
|
+
configurationByDeclaration.set(declaration, entry);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function registerProvider(declaration, ownerId, role) {
|
|
362
|
+
assertConcreteClass(declaration);
|
|
363
|
+
const existing = providerByDeclaration.get(declaration);
|
|
364
|
+
if (existing) {
|
|
365
|
+
if (existing.ownerId !== ownerId) {
|
|
366
|
+
fail(declaration, `Concrete service ${existing.name} is reachable across Feature boundaries without being provided explicitly.`);
|
|
367
|
+
}
|
|
368
|
+
return existing;
|
|
369
|
+
}
|
|
370
|
+
const name = requiredClassName(declaration);
|
|
371
|
+
const localId = role === 'provider' ? readRequiredStaticString(declaration, 'id') : toKebabCase(name);
|
|
372
|
+
const id = `${role}:${ownerId}/${localId}`;
|
|
373
|
+
const placeholder = {
|
|
374
|
+
id,
|
|
375
|
+
ownerId,
|
|
376
|
+
name,
|
|
377
|
+
exportName: name,
|
|
378
|
+
role,
|
|
379
|
+
scope: role === 'provider'
|
|
380
|
+
? 'singleton'
|
|
381
|
+
: implementsNamedInterface(declaration, 'ExecutionScoped', checker)
|
|
382
|
+
? 'execution'
|
|
383
|
+
: 'transient',
|
|
384
|
+
durableIdentity: role === 'provider',
|
|
385
|
+
capabilities: [
|
|
386
|
+
...(extendsNamedClass(declaration, 'Auth', checker) ? ['authentication'] : []),
|
|
387
|
+
...(extendsNamedClass(declaration, 'TransactionManager', checker)
|
|
388
|
+
? ['transactions']
|
|
389
|
+
: []),
|
|
390
|
+
...(extendsNamedClass(declaration, 'QueueManager', checker) ? ['queues'] : []),
|
|
391
|
+
...(extendsNamedClass(declaration, 'Cache', checker) ? ['cache'] : []),
|
|
392
|
+
...(extendsNamedClass(declaration, 'MailTransport', checker) ? ['mail'] : []),
|
|
393
|
+
...(extendsNamedClass(declaration, 'SmsTransport', checker) ? ['sms'] : []),
|
|
394
|
+
...(extendsNamedClass(declaration, 'BroadcastTransport', checker)
|
|
395
|
+
? ['broadcasting']
|
|
396
|
+
: []),
|
|
397
|
+
...(extendsNamedClass(declaration, 'Telemetry', checker) ? ['telemetry'] : []),
|
|
398
|
+
...(extendsNamedClass(declaration, 'ObservationRecorder', checker)
|
|
399
|
+
? ['observations']
|
|
400
|
+
: []),
|
|
401
|
+
],
|
|
402
|
+
source: sourceOf(declaration, normalized.projectRoot),
|
|
403
|
+
dependencies: [],
|
|
404
|
+
lifecycle: lifecycleOf(declaration, checker),
|
|
405
|
+
};
|
|
406
|
+
providerByDeclaration.set(declaration, placeholder);
|
|
407
|
+
providers.push(placeholder);
|
|
408
|
+
const complete = { ...placeholder, dependencies: dependenciesFor(declaration, ownerId) };
|
|
409
|
+
providerByDeclaration.set(declaration, complete);
|
|
410
|
+
providers[providers.indexOf(placeholder)] = complete;
|
|
411
|
+
return complete;
|
|
412
|
+
}
|
|
413
|
+
function dependenciesFor(declaration, ownerId) {
|
|
414
|
+
const constructor = declaration.members.find(ts.isConstructorDeclaration);
|
|
415
|
+
const frameworkRole = [
|
|
416
|
+
'Action',
|
|
417
|
+
'Query',
|
|
418
|
+
'Route',
|
|
419
|
+
'Listener',
|
|
420
|
+
'Job',
|
|
421
|
+
'Policy',
|
|
422
|
+
'SignalHandler',
|
|
423
|
+
'Observer',
|
|
424
|
+
'Command',
|
|
425
|
+
].some((role) => extendsNamedClass(declaration, role, checker));
|
|
426
|
+
if (frameworkRole && constructor && constructor.parameters.length > 0) {
|
|
427
|
+
fail(constructor, `${requiredClassName(declaration)} is a framework role; declare scoped dependencies with this.inject() instead of constructor parameters.`);
|
|
428
|
+
}
|
|
429
|
+
const constructorDependencies = constructor?.parameters.map((parameter) => dependencyFor(parameter, parameter.name.getText(), Boolean(parameter.questionToken || parameter.initializer) ||
|
|
430
|
+
includesUndefined(checker.getTypeAtLocation(parameter)), 'constructor', classDeclarationForType(parameter, checker))) ?? [];
|
|
431
|
+
const injectionCalls = [];
|
|
432
|
+
const visit = (node) => {
|
|
433
|
+
if (ts.isCallExpression(node) && roleInjectionKind(node)) {
|
|
434
|
+
injectionCalls.push(node);
|
|
435
|
+
}
|
|
436
|
+
ts.forEachChild(node, visit);
|
|
437
|
+
};
|
|
438
|
+
declaration.members.forEach(visit);
|
|
439
|
+
const roleDependencies = injectionCalls.map((call) => {
|
|
440
|
+
const member = call.parent;
|
|
441
|
+
if (!ts.isPropertyDeclaration(member) ||
|
|
442
|
+
member.initializer !== call ||
|
|
443
|
+
member.parent !== declaration) {
|
|
444
|
+
fail(call, 'this.inject() must be the direct initializer of a role class property.');
|
|
445
|
+
}
|
|
446
|
+
const injectionKind = roleInjectionKind(call);
|
|
447
|
+
if (!injectionKind || call.arguments.length !== 1) {
|
|
448
|
+
fail(call, 'this.inject() requires exactly one statically identifiable dependency token.');
|
|
449
|
+
}
|
|
450
|
+
const name = propertyName(member.name);
|
|
451
|
+
if (!name)
|
|
452
|
+
fail(member, 'Injected role properties must use an identifier or string literal name.');
|
|
453
|
+
const dependencyDeclaration = resolveClassReference(call.arguments[0], checker);
|
|
454
|
+
return dependencyFor(call, name, injectionKind === 'optional', 'role', dependencyDeclaration);
|
|
455
|
+
});
|
|
456
|
+
return [...constructorDependencies, ...roleDependencies];
|
|
457
|
+
function dependencyFor(source, parameter, optional, kind, dependencyDeclaration) {
|
|
458
|
+
let targetId;
|
|
459
|
+
if (dependencyDeclaration) {
|
|
460
|
+
const builtinId = builtinIdForDeclaration(dependencyDeclaration);
|
|
461
|
+
const capability = providerCapabilityForDeclaration(dependencyDeclaration);
|
|
462
|
+
const capabilityProvider = capability
|
|
463
|
+
? providers.find((provider) => provider.capabilities.includes(capability))
|
|
464
|
+
: undefined;
|
|
465
|
+
if (capability && !capabilityProvider && !optional) {
|
|
466
|
+
fail(source, `${requiredClassName(dependencyDeclaration)} requires one selected ${capability} provider.`);
|
|
467
|
+
}
|
|
468
|
+
const configuration = configurationByDeclaration.get(dependencyDeclaration);
|
|
469
|
+
const providerRoot = providerRoots.get(dependencyDeclaration);
|
|
470
|
+
const operationRoot = operationRoots.get(dependencyDeclaration);
|
|
471
|
+
const modelRoot = modelRoots.get(dependencyDeclaration);
|
|
472
|
+
const observerRoot = observerRoots.get(dependencyDeclaration);
|
|
473
|
+
const routeRoot = routeRoots.get(dependencyDeclaration);
|
|
474
|
+
const eventRoot = eventRoots.get(dependencyDeclaration);
|
|
475
|
+
const listenerRoot = listenerRoots.get(dependencyDeclaration);
|
|
476
|
+
const jobRoot = jobRoots.get(dependencyDeclaration);
|
|
477
|
+
const scheduleRoot = scheduleRoots.get(dependencyDeclaration);
|
|
478
|
+
const policyRoot = policyRoots.get(dependencyDeclaration);
|
|
479
|
+
const signalRoot = signalRoots.get(dependencyDeclaration);
|
|
480
|
+
const signalHandlerRoot = signalHandlerRoots.get(dependencyDeclaration);
|
|
481
|
+
const commandRoot = commandRoots.get(dependencyDeclaration);
|
|
482
|
+
if (operationRoot) {
|
|
483
|
+
fail(source, `${requiredClassName(dependencyDeclaration)} is an operation class; inject ActionBus or QueryBus instead.`);
|
|
484
|
+
}
|
|
485
|
+
if (modelRoot) {
|
|
486
|
+
fail(source, `${requiredClassName(dependencyDeclaration)} is a model class and is not a dependency; use its static retrieval API.`);
|
|
487
|
+
}
|
|
488
|
+
if (routeRoot ||
|
|
489
|
+
eventRoot ||
|
|
490
|
+
listenerRoot ||
|
|
491
|
+
jobRoot ||
|
|
492
|
+
scheduleRoot ||
|
|
493
|
+
policyRoot ||
|
|
494
|
+
observerRoot ||
|
|
495
|
+
signalRoot ||
|
|
496
|
+
signalHandlerRoot ||
|
|
497
|
+
commandRoot) {
|
|
498
|
+
fail(source, `${requiredClassName(dependencyDeclaration)} is a framework role class and cannot be injected directly.`);
|
|
499
|
+
}
|
|
500
|
+
if (providerRoot && providerRoot.ownerId !== ownerId) {
|
|
501
|
+
fail(source, `${requiredClassName(dependencyDeclaration)} is private to Feature ${providerRoot.ownerId}.`);
|
|
502
|
+
}
|
|
503
|
+
const abstractDependency = hasModifier(dependencyDeclaration, ts.SyntaxKind.AbstractKeyword);
|
|
504
|
+
targetId =
|
|
505
|
+
builtinId ??
|
|
506
|
+
capabilityProvider?.id ??
|
|
507
|
+
configuration?.id ??
|
|
508
|
+
(optional && abstractDependency
|
|
509
|
+
? undefined
|
|
510
|
+
: registerProvider(dependencyDeclaration, providerRoot?.ownerId ?? ownerId, providerRoot ? 'provider' : 'service').id);
|
|
511
|
+
}
|
|
512
|
+
if (!targetId && !optional) {
|
|
513
|
+
fail(source, `Required ${kind === 'role' ? 'role' : 'constructor'} dependency ${parameter} cannot be resolved to a declared configuration or concrete class.`);
|
|
514
|
+
}
|
|
515
|
+
return {
|
|
516
|
+
kind,
|
|
517
|
+
parameter,
|
|
518
|
+
token: dependencyDeclaration ? requiredClassName(dependencyDeclaration) : parameter,
|
|
519
|
+
...(targetId ? { targetId } : {}),
|
|
520
|
+
optional,
|
|
521
|
+
source: sourceOf(source, normalized.projectRoot),
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
function registerOperation(declaration, ownerId, role) {
|
|
526
|
+
assertConcreteClass(declaration);
|
|
527
|
+
const existing = operationByDeclaration.get(declaration);
|
|
528
|
+
if (existing) {
|
|
529
|
+
fail(declaration, `${existing.name} is declared more than once as an application operation.`);
|
|
530
|
+
}
|
|
531
|
+
if (!extendsNamedClass(declaration, role === 'action' ? 'Action' : 'Query', checker)) {
|
|
532
|
+
fail(declaration, `${requiredClassName(declaration)} must extend ${role === 'action' ? 'Action' : 'Query'}.`);
|
|
533
|
+
}
|
|
534
|
+
if (!checker.getTypeAtLocation(declaration).getProperty('handle')) {
|
|
535
|
+
fail(declaration, `${requiredClassName(declaration)} must define handle(input).`);
|
|
536
|
+
}
|
|
537
|
+
const name = requiredClassName(declaration);
|
|
538
|
+
const lifecycle = lifecycleOf(declaration, checker);
|
|
539
|
+
if (lifecycle.start || lifecycle.drain || lifecycle.stop) {
|
|
540
|
+
fail(declaration, `${name} may define dispose(), but operation handlers cannot own application lifecycle phases.`);
|
|
541
|
+
}
|
|
542
|
+
const entry = {
|
|
543
|
+
id: `${role}:${ownerId}/${readRequiredStaticString(declaration, 'id')}`,
|
|
544
|
+
ownerId,
|
|
545
|
+
name,
|
|
546
|
+
exportName: name,
|
|
547
|
+
role,
|
|
548
|
+
scope: 'transient',
|
|
549
|
+
transactional: role === 'action',
|
|
550
|
+
access: readAccess(declaration),
|
|
551
|
+
source: sourceOf(declaration, normalized.projectRoot),
|
|
552
|
+
dependencies: [],
|
|
553
|
+
lifecycle,
|
|
554
|
+
};
|
|
555
|
+
operationByDeclaration.set(declaration, entry);
|
|
556
|
+
const complete = { ...entry, dependencies: dependenciesFor(declaration, ownerId) };
|
|
557
|
+
operationByDeclaration.set(declaration, complete);
|
|
558
|
+
const operations = role === 'action' ? actions : queries;
|
|
559
|
+
operations.push(complete);
|
|
560
|
+
return complete;
|
|
561
|
+
}
|
|
562
|
+
function registerOperationRoot(declaration, ownerId, role) {
|
|
563
|
+
const existing = operationRoots.get(declaration);
|
|
564
|
+
if (existing) {
|
|
565
|
+
fail(declaration, `${requiredClassName(declaration)} is already declared as ${existing.role} by ${existing.ownerId}.`);
|
|
566
|
+
}
|
|
567
|
+
operationRoots.set(declaration, { ownerId, role });
|
|
568
|
+
}
|
|
569
|
+
function registerOwnedRoots(feature, field, ownerId, roots, role) {
|
|
570
|
+
for (const declaration of readClassArray(feature, field, checker)) {
|
|
571
|
+
const existing = roots.get(declaration);
|
|
572
|
+
if (existing) {
|
|
573
|
+
fail(declaration, `${requiredClassName(declaration)} is already declared as a ${role} by ${existing.ownerId}.`);
|
|
574
|
+
}
|
|
575
|
+
roots.set(declaration, { ownerId });
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
function registerModel(declaration, ownerId) {
|
|
579
|
+
assertConcreteClass(declaration);
|
|
580
|
+
if (!extendsNamedClass(declaration, 'Model', checker)) {
|
|
581
|
+
fail(declaration, `${requiredClassName(declaration)} must extend Model.`);
|
|
582
|
+
}
|
|
583
|
+
const name = requiredClassName(declaration);
|
|
584
|
+
const localId = readRequiredStaticString(declaration, 'id');
|
|
585
|
+
const entry = {
|
|
586
|
+
id: `model:${ownerId}/${localId}`,
|
|
587
|
+
ownerId,
|
|
588
|
+
name,
|
|
589
|
+
exportName: name,
|
|
590
|
+
entityType: `model:${ownerId}/${localId}`,
|
|
591
|
+
storage: compileModelStorage(declaration),
|
|
592
|
+
source: sourceOf(declaration, normalized.projectRoot),
|
|
593
|
+
};
|
|
594
|
+
modelByDeclaration.set(declaration, entry);
|
|
595
|
+
models.push(entry);
|
|
596
|
+
return entry;
|
|
597
|
+
}
|
|
598
|
+
function compileModelStorage(declaration) {
|
|
599
|
+
const tableValue = readOptionalStaticJson(declaration, 'table');
|
|
600
|
+
if (tableValue === undefined)
|
|
601
|
+
return { kind: 'entity-state' };
|
|
602
|
+
if (typeof tableValue !== 'string' || !validQualifiedIdentifier(tableValue)) {
|
|
603
|
+
fail(declaration, `${requiredClassName(declaration)}.table must be a literal PostgreSQL table name.`);
|
|
604
|
+
}
|
|
605
|
+
const columnsValue = readOptionalStaticJson(declaration, 'columns') ?? {};
|
|
606
|
+
if (!isStringRecord(columnsValue)) {
|
|
607
|
+
fail(declaration, `${requiredClassName(declaration)}.columns must be a literal attribute-to-column string object.`);
|
|
608
|
+
}
|
|
609
|
+
for (const [attribute, column] of Object.entries(columnsValue)) {
|
|
610
|
+
if (!validIdentifier(attribute) || !validIdentifier(column)) {
|
|
611
|
+
fail(declaration, `${requiredClassName(declaration)}.columns contains an invalid attribute or PostgreSQL column name.`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
const primaryKeyValue = readOptionalStaticJson(declaration, 'primaryKey') ?? columnsValue.id ?? 'id';
|
|
615
|
+
if (typeof primaryKeyValue !== 'string' || !validIdentifier(primaryKeyValue)) {
|
|
616
|
+
fail(declaration, `${requiredClassName(declaration)}.primaryKey must be a literal PostgreSQL column name.`);
|
|
617
|
+
}
|
|
618
|
+
const versionValue = readOptionalStaticJson(declaration, 'versionColumn');
|
|
619
|
+
if (versionValue !== undefined &&
|
|
620
|
+
(typeof versionValue !== 'string' || !validIdentifier(versionValue))) {
|
|
621
|
+
fail(declaration, `${requiredClassName(declaration)}.versionColumn must be a literal PostgreSQL column name.`);
|
|
622
|
+
}
|
|
623
|
+
const timestampsValue = readOptionalStaticJson(declaration, 'timestamps') ?? false;
|
|
624
|
+
let timestamps;
|
|
625
|
+
if (timestampsValue === false)
|
|
626
|
+
timestamps = false;
|
|
627
|
+
else if (timestampsValue === true)
|
|
628
|
+
timestamps = { createdAt: 'created_at', updatedAt: 'updated_at' };
|
|
629
|
+
else if (isStringRecord(timestampsValue) &&
|
|
630
|
+
typeof timestampsValue.createdAt === 'string' &&
|
|
631
|
+
validIdentifier(timestampsValue.createdAt) &&
|
|
632
|
+
typeof timestampsValue.updatedAt === 'string' &&
|
|
633
|
+
validIdentifier(timestampsValue.updatedAt)) {
|
|
634
|
+
timestamps = { createdAt: timestampsValue.createdAt, updatedAt: timestampsValue.updatedAt };
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
fail(declaration, `${requiredClassName(declaration)}.timestamps must be false, true, or { createdAt, updatedAt } column names.`);
|
|
638
|
+
}
|
|
639
|
+
return {
|
|
640
|
+
kind: 'table',
|
|
641
|
+
table: tableValue,
|
|
642
|
+
primaryKey: primaryKeyValue,
|
|
643
|
+
columns: { ...columnsValue, id: primaryKeyValue },
|
|
644
|
+
...(typeof versionValue === 'string' ? { versionColumn: versionValue } : {}),
|
|
645
|
+
timestamps,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
function registerObserver(declaration, ownerId) {
|
|
649
|
+
assertConcreteClass(declaration);
|
|
650
|
+
if (!extendsNamedClass(declaration, 'Observer', checker)) {
|
|
651
|
+
fail(declaration, `${requiredClassName(declaration)} must extend Observer.`);
|
|
652
|
+
}
|
|
653
|
+
const phaseNames = [
|
|
654
|
+
'retrieved',
|
|
655
|
+
'saving',
|
|
656
|
+
'creating',
|
|
657
|
+
'updating',
|
|
658
|
+
'created',
|
|
659
|
+
'updated',
|
|
660
|
+
'saved',
|
|
661
|
+
'committed',
|
|
662
|
+
];
|
|
663
|
+
const methods = declaration.members.filter((member) => ts.isMethodDeclaration(member) &&
|
|
664
|
+
phaseNames.includes(propertyName(member.name)));
|
|
665
|
+
if (methods.length === 0) {
|
|
666
|
+
fail(declaration, `${requiredClassName(declaration)} must define at least one model lifecycle method.`);
|
|
667
|
+
}
|
|
668
|
+
let model;
|
|
669
|
+
for (const method of methods) {
|
|
670
|
+
if (method.parameters.length !== 1) {
|
|
671
|
+
fail(method, `Observer method ${propertyName(method.name)} must accept one typed model parameter.`);
|
|
672
|
+
}
|
|
673
|
+
const modelDeclaration = classDeclarationForType(method.parameters[0], checker);
|
|
674
|
+
const candidate = modelDeclaration ? modelByDeclaration.get(modelDeclaration) : undefined;
|
|
675
|
+
if (!candidate) {
|
|
676
|
+
fail(method.parameters[0], 'Observer lifecycle methods must name a Model declared by a selected Feature.');
|
|
677
|
+
}
|
|
678
|
+
if (model && model.id !== candidate.id) {
|
|
679
|
+
fail(method, `${requiredClassName(declaration)} cannot observe more than one Model.`);
|
|
680
|
+
}
|
|
681
|
+
model = candidate;
|
|
682
|
+
}
|
|
683
|
+
const lifecycle = lifecycleOf(declaration, checker);
|
|
684
|
+
if (lifecycle.start || lifecycle.drain || lifecycle.stop || lifecycle.dispose) {
|
|
685
|
+
fail(declaration, `${requiredClassName(declaration)} cannot own container lifecycle phases.`);
|
|
686
|
+
}
|
|
687
|
+
const name = requiredClassName(declaration);
|
|
688
|
+
const entry = {
|
|
689
|
+
id: `observer:${ownerId}/${readRequiredStaticString(declaration, 'id')}`,
|
|
690
|
+
ownerId,
|
|
691
|
+
name,
|
|
692
|
+
exportName: name,
|
|
693
|
+
modelId: model.id,
|
|
694
|
+
phases: methods.map((method) => propertyName(method.name)),
|
|
695
|
+
scope: 'transient',
|
|
696
|
+
source: sourceOf(declaration, normalized.projectRoot),
|
|
697
|
+
dependencies: [],
|
|
698
|
+
lifecycle,
|
|
699
|
+
};
|
|
700
|
+
observerByDeclaration.set(declaration, entry);
|
|
701
|
+
const complete = { ...entry, dependencies: dependenciesFor(declaration, ownerId) };
|
|
702
|
+
observerByDeclaration.set(declaration, complete);
|
|
703
|
+
observers.push(complete);
|
|
704
|
+
return complete;
|
|
705
|
+
}
|
|
706
|
+
function registerEvent(declaration, ownerId) {
|
|
707
|
+
assertConcreteClass(declaration);
|
|
708
|
+
if (!extendsNamedClass(declaration, 'Event', checker)) {
|
|
709
|
+
fail(declaration, `${requiredClassName(declaration)} must extend Event.`);
|
|
710
|
+
}
|
|
711
|
+
const name = requiredClassName(declaration);
|
|
712
|
+
const entry = {
|
|
713
|
+
id: `event:${ownerId}/${readRequiredStaticString(declaration, 'id')}`,
|
|
714
|
+
ownerId,
|
|
715
|
+
name,
|
|
716
|
+
exportName: name,
|
|
717
|
+
dispatch: implementsNamedInterface(declaration, 'ShouldDispatchAfterCommit', checker)
|
|
718
|
+
? 'after-commit'
|
|
719
|
+
: 'immediate',
|
|
720
|
+
broadcast: implementsNamedInterface(declaration, 'ShouldBroadcastNow', checker)
|
|
721
|
+
? 'now'
|
|
722
|
+
: implementsNamedInterface(declaration, 'ShouldBroadcast', checker)
|
|
723
|
+
? 'queued'
|
|
724
|
+
: false,
|
|
725
|
+
source: sourceOf(declaration, normalized.projectRoot),
|
|
726
|
+
};
|
|
727
|
+
eventByDeclaration.set(declaration, entry);
|
|
728
|
+
events.push(entry);
|
|
729
|
+
return entry;
|
|
730
|
+
}
|
|
731
|
+
function registerListener(declaration, ownerId) {
|
|
732
|
+
assertConcreteClass(declaration);
|
|
733
|
+
if (!extendsNamedClass(declaration, 'Listener', checker)) {
|
|
734
|
+
fail(declaration, `${requiredClassName(declaration)} must extend Listener.`);
|
|
735
|
+
}
|
|
736
|
+
const handle = declaration.members.find((member) => ts.isMethodDeclaration(member) && propertyName(member.name) === 'handle');
|
|
737
|
+
if (!handle || handle.parameters.length !== 1) {
|
|
738
|
+
fail(declaration, `${requiredClassName(declaration)} must define handle(event) with one typed event parameter.`);
|
|
739
|
+
}
|
|
740
|
+
const eventDeclaration = classDeclarationForType(handle.parameters[0], checker);
|
|
741
|
+
const event = eventDeclaration ? eventByDeclaration.get(eventDeclaration) : undefined;
|
|
742
|
+
if (!event) {
|
|
743
|
+
fail(handle.parameters[0], 'Listener handle(event) must name an Event declared by a selected Feature.');
|
|
744
|
+
}
|
|
745
|
+
const queuedAfterCommit = implementsNamedInterface(declaration, 'ShouldQueueAfterCommit', checker);
|
|
746
|
+
const queued = queuedAfterCommit || implementsNamedInterface(declaration, 'ShouldQueue', checker);
|
|
747
|
+
const afterCommit = implementsNamedInterface(declaration, 'ShouldHandleEventsAfterCommit', checker);
|
|
748
|
+
if (queued && afterCommit) {
|
|
749
|
+
fail(declaration, `${requiredClassName(declaration)} cannot combine queued and local after-commit capabilities.`);
|
|
750
|
+
}
|
|
751
|
+
const lifecycle = lifecycleOf(declaration, checker);
|
|
752
|
+
if (lifecycle.start || lifecycle.drain || lifecycle.stop) {
|
|
753
|
+
fail(declaration, `${requiredClassName(declaration)} may define dispose(), but listeners cannot own application lifecycle phases.`);
|
|
754
|
+
}
|
|
755
|
+
const name = requiredClassName(declaration);
|
|
756
|
+
const entry = {
|
|
757
|
+
id: `listener:${ownerId}/${readRequiredStaticString(declaration, 'id')}`,
|
|
758
|
+
ownerId,
|
|
759
|
+
name,
|
|
760
|
+
exportName: name,
|
|
761
|
+
eventId: event.id,
|
|
762
|
+
delivery: queuedAfterCommit
|
|
763
|
+
? 'queued-after-commit'
|
|
764
|
+
: queued
|
|
765
|
+
? 'queued'
|
|
766
|
+
: afterCommit
|
|
767
|
+
? 'after-commit'
|
|
768
|
+
: 'local',
|
|
769
|
+
access: readAccess(declaration),
|
|
770
|
+
scope: 'transient',
|
|
771
|
+
source: sourceOf(declaration, normalized.projectRoot),
|
|
772
|
+
dependencies: [],
|
|
773
|
+
lifecycle,
|
|
774
|
+
};
|
|
775
|
+
listenerByDeclaration.set(declaration, entry);
|
|
776
|
+
const complete = { ...entry, dependencies: dependenciesFor(declaration, ownerId) };
|
|
777
|
+
listenerByDeclaration.set(declaration, complete);
|
|
778
|
+
listeners.push(complete);
|
|
779
|
+
return complete;
|
|
780
|
+
}
|
|
781
|
+
function registerRoute(declaration, ownerId) {
|
|
782
|
+
assertConcreteClass(declaration);
|
|
783
|
+
if (!extendsNamedClass(declaration, 'Route', checker)) {
|
|
784
|
+
fail(declaration, `${requiredClassName(declaration)} must extend Route.`);
|
|
785
|
+
}
|
|
786
|
+
const method = readRequiredInstanceString(declaration, 'method').toUpperCase();
|
|
787
|
+
if (!['DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT'].includes(method)) {
|
|
788
|
+
fail(declaration, `${requiredClassName(declaration)}.method is not a supported HTTP method.`);
|
|
789
|
+
}
|
|
790
|
+
const routePath = readRequiredInstanceString(declaration, 'path');
|
|
791
|
+
if (!routePath.startsWith('/')) {
|
|
792
|
+
fail(declaration, `${requiredClassName(declaration)}.path must begin with /.`);
|
|
793
|
+
}
|
|
794
|
+
const handle = declaration.members.find((member) => ts.isMethodDeclaration(member) && propertyName(member.name) === 'handle');
|
|
795
|
+
if (!handle || handle.parameters.length !== 1) {
|
|
796
|
+
fail(declaration, `${requiredClassName(declaration)} must define handle(request).`);
|
|
797
|
+
}
|
|
798
|
+
const lifecycle = lifecycleOf(declaration, checker);
|
|
799
|
+
if (lifecycle.start || lifecycle.drain || lifecycle.stop) {
|
|
800
|
+
fail(declaration, `${requiredClassName(declaration)} may define dispose(), but routes cannot own application lifecycle phases.`);
|
|
801
|
+
}
|
|
802
|
+
const name = requiredClassName(declaration);
|
|
803
|
+
const access = readRequiredStaticString(declaration, 'access');
|
|
804
|
+
if (access !== 'public' && !/^[a-z][a-z0-9._:-]{1,127}$/.test(access)) {
|
|
805
|
+
fail(declaration, `${name}.access must be "public" or a stable ability name.`);
|
|
806
|
+
}
|
|
807
|
+
const entry = {
|
|
808
|
+
id: `route:${ownerId}/${readRequiredStaticString(declaration, 'id')}`,
|
|
809
|
+
ownerId,
|
|
810
|
+
name,
|
|
811
|
+
exportName: name,
|
|
812
|
+
method: method,
|
|
813
|
+
path: routePath,
|
|
814
|
+
access,
|
|
815
|
+
scope: 'transient',
|
|
816
|
+
source: sourceOf(declaration, normalized.projectRoot),
|
|
817
|
+
dependencies: [],
|
|
818
|
+
lifecycle,
|
|
819
|
+
};
|
|
820
|
+
routeByDeclaration.set(declaration, entry);
|
|
821
|
+
const complete = { ...entry, dependencies: dependenciesFor(declaration, ownerId) };
|
|
822
|
+
routeByDeclaration.set(declaration, complete);
|
|
823
|
+
routes.push(complete);
|
|
824
|
+
return complete;
|
|
825
|
+
}
|
|
826
|
+
function registerJob(declaration, ownerId) {
|
|
827
|
+
assertConcreteClass(declaration);
|
|
828
|
+
if (!extendsNamedClass(declaration, 'Job', checker)) {
|
|
829
|
+
fail(declaration, `${requiredClassName(declaration)} must extend Job.`);
|
|
830
|
+
}
|
|
831
|
+
const handle = declaration.members.find((member) => ts.isMethodDeclaration(member) && propertyName(member.name) === 'handle');
|
|
832
|
+
if (!handle || handle.parameters.length !== 1) {
|
|
833
|
+
fail(declaration, `${requiredClassName(declaration)} must define handle(input).`);
|
|
834
|
+
}
|
|
835
|
+
const lifecycle = lifecycleOf(declaration, checker);
|
|
836
|
+
if (lifecycle.start || lifecycle.drain || lifecycle.stop) {
|
|
837
|
+
fail(declaration, `${requiredClassName(declaration)} may define dispose(), but jobs cannot own application lifecycle phases.`);
|
|
838
|
+
}
|
|
839
|
+
const name = requiredClassName(declaration);
|
|
840
|
+
const retries = readOptionalStaticNumber(declaration, 'retries', 3);
|
|
841
|
+
const retryDelay = readOptionalStaticNumber(declaration, 'retryDelay', 1);
|
|
842
|
+
const timeout = readOptionalStaticNumber(declaration, 'timeout', 30);
|
|
843
|
+
if (!Number.isInteger(retries) || retries < 0) {
|
|
844
|
+
fail(declaration, `${name}.retries must be a non-negative integer.`);
|
|
845
|
+
}
|
|
846
|
+
if (!Number.isFinite(retryDelay) || retryDelay < 0) {
|
|
847
|
+
fail(declaration, `${name}.retryDelay must be a non-negative number.`);
|
|
848
|
+
}
|
|
849
|
+
if (!Number.isFinite(timeout) || timeout < 1) {
|
|
850
|
+
fail(declaration, `${name}.timeout must be at least one second.`);
|
|
851
|
+
}
|
|
852
|
+
const entry = {
|
|
853
|
+
id: `job:${ownerId}/${readRequiredStaticString(declaration, 'id')}`,
|
|
854
|
+
ownerId,
|
|
855
|
+
name,
|
|
856
|
+
exportName: name,
|
|
857
|
+
scope: 'transient',
|
|
858
|
+
retries,
|
|
859
|
+
retryDelay,
|
|
860
|
+
backoff: readOptionalStaticBoolean(declaration, 'backoff', true),
|
|
861
|
+
timeout,
|
|
862
|
+
access: readAccess(declaration),
|
|
863
|
+
source: sourceOf(declaration, normalized.projectRoot),
|
|
864
|
+
dependencies: [],
|
|
865
|
+
lifecycle,
|
|
866
|
+
};
|
|
867
|
+
jobByDeclaration.set(declaration, entry);
|
|
868
|
+
const complete = { ...entry, dependencies: dependenciesFor(declaration, ownerId) };
|
|
869
|
+
jobByDeclaration.set(declaration, complete);
|
|
870
|
+
jobs.push(complete);
|
|
871
|
+
return complete;
|
|
872
|
+
}
|
|
873
|
+
function registerSchedule(declaration, ownerId) {
|
|
874
|
+
assertConcreteClass(declaration);
|
|
875
|
+
if (!extendsNamedClass(declaration, 'Schedule', checker)) {
|
|
876
|
+
fail(declaration, `${requiredClassName(declaration)} must extend Schedule.`);
|
|
877
|
+
}
|
|
878
|
+
const jobDeclaration = readRequiredStaticClass(declaration, 'job', checker);
|
|
879
|
+
const job = jobByDeclaration.get(jobDeclaration);
|
|
880
|
+
if (!job)
|
|
881
|
+
fail(declaration, `${requiredClassName(declaration)}.job must name a Job declared by a selected Feature.`);
|
|
882
|
+
const cron = readOptionalStaticString(declaration, 'cron');
|
|
883
|
+
const everySeconds = readOptionalStaticNumberValue(declaration, 'everySeconds');
|
|
884
|
+
if ((cron === undefined) === (everySeconds === undefined)) {
|
|
885
|
+
fail(declaration, `${requiredClassName(declaration)} must declare exactly one of static cron or static everySeconds.`);
|
|
886
|
+
}
|
|
887
|
+
if (cron !== undefined && cron.trim().split(/\s+/).length !== 5) {
|
|
888
|
+
fail(declaration, `${requiredClassName(declaration)}.cron must use a five-field cron expression.`);
|
|
889
|
+
}
|
|
890
|
+
if (everySeconds !== undefined && (!Number.isInteger(everySeconds) || everySeconds < 1)) {
|
|
891
|
+
fail(declaration, `${requiredClassName(declaration)}.everySeconds must be a positive integer.`);
|
|
892
|
+
}
|
|
893
|
+
const timeZone = readOptionalStaticString(declaration, 'timeZone') ?? 'UTC';
|
|
894
|
+
try {
|
|
895
|
+
new Intl.DateTimeFormat('en-US', { timeZone }).format();
|
|
896
|
+
}
|
|
897
|
+
catch {
|
|
898
|
+
fail(declaration, `${requiredClassName(declaration)}.timeZone must be a valid IANA time-zone name.`);
|
|
899
|
+
}
|
|
900
|
+
const overlap = readOptionalStaticString(declaration, 'overlap') ?? 'serialize';
|
|
901
|
+
if (overlap !== 'allow' && overlap !== 'serialize') {
|
|
902
|
+
fail(declaration, `${requiredClassName(declaration)}.overlap must be "allow" or "serialize".`);
|
|
903
|
+
}
|
|
904
|
+
const misfire = readOptionalStaticString(declaration, 'misfire') ?? 'skip';
|
|
905
|
+
if (misfire !== 'skip') {
|
|
906
|
+
fail(declaration, `${requiredClassName(declaration)}.misfire currently supports only "skip".`);
|
|
907
|
+
}
|
|
908
|
+
const entry = {
|
|
909
|
+
id: `schedule:${ownerId}/${readRequiredStaticString(declaration, 'id')}`,
|
|
910
|
+
ownerId,
|
|
911
|
+
name: requiredClassName(declaration),
|
|
912
|
+
exportName: requiredClassName(declaration),
|
|
913
|
+
jobId: job.id,
|
|
914
|
+
cadence: cron !== undefined
|
|
915
|
+
? { kind: 'cron', expression: cron }
|
|
916
|
+
: { kind: 'interval', seconds: everySeconds },
|
|
917
|
+
timeZone,
|
|
918
|
+
overlap,
|
|
919
|
+
misfire,
|
|
920
|
+
input: readOptionalStaticJson(declaration, 'input') ?? null,
|
|
921
|
+
access: readAccess(declaration),
|
|
922
|
+
source: sourceOf(declaration, normalized.projectRoot),
|
|
923
|
+
};
|
|
924
|
+
scheduleByDeclaration.set(declaration, entry);
|
|
925
|
+
schedules.push(entry);
|
|
926
|
+
return entry;
|
|
927
|
+
}
|
|
928
|
+
function registerPolicy(declaration, ownerId) {
|
|
929
|
+
assertConcreteClass(declaration);
|
|
930
|
+
if (!extendsNamedClass(declaration, 'Policy', checker)) {
|
|
931
|
+
fail(declaration, `${requiredClassName(declaration)} must extend Policy.`);
|
|
932
|
+
}
|
|
933
|
+
const decide = declaration.members.find((member) => ts.isMethodDeclaration(member) && propertyName(member.name) === 'decide');
|
|
934
|
+
if (!decide || decide.parameters.length !== 1) {
|
|
935
|
+
fail(declaration, `${requiredClassName(declaration)} must define decide(request).`);
|
|
936
|
+
}
|
|
937
|
+
const lifecycle = lifecycleOf(declaration, checker);
|
|
938
|
+
if (lifecycle.start || lifecycle.drain || lifecycle.stop) {
|
|
939
|
+
fail(declaration, `${requiredClassName(declaration)} may define dispose(), but policies cannot own application lifecycle phases.`);
|
|
940
|
+
}
|
|
941
|
+
const name = requiredClassName(declaration);
|
|
942
|
+
const abilities = readRequiredStaticStringArray(declaration, 'abilities');
|
|
943
|
+
if (abilities.length === 0)
|
|
944
|
+
fail(declaration, `${name}.abilities must contain at least one ability.`);
|
|
945
|
+
const entry = {
|
|
946
|
+
id: `policy:${ownerId}/${readRequiredStaticString(declaration, 'id')}`,
|
|
947
|
+
ownerId,
|
|
948
|
+
name,
|
|
949
|
+
exportName: name,
|
|
950
|
+
scope: 'transient',
|
|
951
|
+
abilities: [...new Set(abilities)].sort(),
|
|
952
|
+
source: sourceOf(declaration, normalized.projectRoot),
|
|
953
|
+
dependencies: [],
|
|
954
|
+
lifecycle,
|
|
955
|
+
};
|
|
956
|
+
policyByDeclaration.set(declaration, entry);
|
|
957
|
+
const complete = { ...entry, dependencies: dependenciesFor(declaration, ownerId) };
|
|
958
|
+
policyByDeclaration.set(declaration, complete);
|
|
959
|
+
policies.push(complete);
|
|
960
|
+
return complete;
|
|
961
|
+
}
|
|
962
|
+
function registerSignal(declaration, ownerId) {
|
|
963
|
+
assertConcreteClass(declaration);
|
|
964
|
+
if (!extendsNamedClass(declaration, 'Signal', checker)) {
|
|
965
|
+
fail(declaration, `${requiredClassName(declaration)} must extend Signal.`);
|
|
966
|
+
}
|
|
967
|
+
const name = requiredClassName(declaration);
|
|
968
|
+
const entry = {
|
|
969
|
+
id: `signal:${ownerId}/${readRequiredStaticString(declaration, 'id')}`,
|
|
970
|
+
ownerId,
|
|
971
|
+
name,
|
|
972
|
+
exportName: name,
|
|
973
|
+
source: sourceOf(declaration, normalized.projectRoot),
|
|
974
|
+
};
|
|
975
|
+
signalByDeclaration.set(declaration, entry);
|
|
976
|
+
signals.push(entry);
|
|
977
|
+
return entry;
|
|
978
|
+
}
|
|
979
|
+
function registerSignalHandler(declaration, ownerId) {
|
|
980
|
+
assertConcreteClass(declaration);
|
|
981
|
+
if (!extendsNamedClass(declaration, 'SignalHandler', checker)) {
|
|
982
|
+
fail(declaration, `${requiredClassName(declaration)} must extend SignalHandler.`);
|
|
983
|
+
}
|
|
984
|
+
const handle = declaration.members.find((member) => ts.isMethodDeclaration(member) && propertyName(member.name) === 'handle');
|
|
985
|
+
if (!handle || handle.parameters.length !== 1) {
|
|
986
|
+
fail(declaration, `${requiredClassName(declaration)} must define handle(signal).`);
|
|
987
|
+
}
|
|
988
|
+
const signalDeclaration = classDeclarationForType(handle.parameters[0], checker);
|
|
989
|
+
const signal = signalDeclaration ? signalByDeclaration.get(signalDeclaration) : undefined;
|
|
990
|
+
if (!signal)
|
|
991
|
+
fail(handle.parameters[0], 'SignalHandler handle(signal) must name a Signal declared by a selected Feature.');
|
|
992
|
+
const lifecycle = lifecycleOf(declaration, checker);
|
|
993
|
+
if (lifecycle.start || lifecycle.drain || lifecycle.stop) {
|
|
994
|
+
fail(declaration, `${requiredClassName(declaration)} may define dispose(), but signal handlers cannot own lifecycle phases.`);
|
|
995
|
+
}
|
|
996
|
+
const name = requiredClassName(declaration);
|
|
997
|
+
const entry = {
|
|
998
|
+
id: `signal-handler:${ownerId}/${readRequiredStaticString(declaration, 'id')}`,
|
|
999
|
+
ownerId,
|
|
1000
|
+
name,
|
|
1001
|
+
exportName: name,
|
|
1002
|
+
signalId: signal.id,
|
|
1003
|
+
access: readAccess(declaration),
|
|
1004
|
+
scope: 'transient',
|
|
1005
|
+
source: sourceOf(declaration, normalized.projectRoot),
|
|
1006
|
+
dependencies: [],
|
|
1007
|
+
lifecycle,
|
|
1008
|
+
};
|
|
1009
|
+
signalHandlerByDeclaration.set(declaration, entry);
|
|
1010
|
+
const complete = { ...entry, dependencies: dependenciesFor(declaration, ownerId) };
|
|
1011
|
+
signalHandlerByDeclaration.set(declaration, complete);
|
|
1012
|
+
signalHandlers.push(complete);
|
|
1013
|
+
return complete;
|
|
1014
|
+
}
|
|
1015
|
+
function registerCommand(declaration, ownerId) {
|
|
1016
|
+
assertConcreteClass(declaration);
|
|
1017
|
+
if (!extendsNamedClass(declaration, 'Command', checker))
|
|
1018
|
+
fail(declaration, `${requiredClassName(declaration)} must extend Command.`);
|
|
1019
|
+
const handle = declaration.members.find((member) => ts.isMethodDeclaration(member) && propertyName(member.name) === 'handle');
|
|
1020
|
+
if (!handle || handle.parameters.length !== 1)
|
|
1021
|
+
fail(declaration, `${requiredClassName(declaration)} must define handle(arguments_).`);
|
|
1022
|
+
const lifecycle = lifecycleOf(declaration, checker);
|
|
1023
|
+
if (lifecycle.start || lifecycle.drain || lifecycle.stop)
|
|
1024
|
+
fail(declaration, `${requiredClassName(declaration)} may define dispose(), but commands cannot own application lifecycle phases.`);
|
|
1025
|
+
const name = requiredClassName(declaration);
|
|
1026
|
+
const command = readRequiredStaticString(declaration, 'name');
|
|
1027
|
+
if (!/^[a-z][a-z0-9]*(?::[a-z][a-z0-9-]*)*$/.test(command))
|
|
1028
|
+
fail(declaration, `${name}.name must be a stable colon-delimited command name.`);
|
|
1029
|
+
const entry = {
|
|
1030
|
+
id: `command:${ownerId}/${readRequiredStaticString(declaration, 'id')}`,
|
|
1031
|
+
ownerId,
|
|
1032
|
+
name,
|
|
1033
|
+
exportName: name,
|
|
1034
|
+
command,
|
|
1035
|
+
description: readOptionalStaticString(declaration, 'description') ?? '',
|
|
1036
|
+
access: readAccess(declaration),
|
|
1037
|
+
scope: 'transient',
|
|
1038
|
+
source: sourceOf(declaration, normalized.projectRoot),
|
|
1039
|
+
dependencies: [],
|
|
1040
|
+
lifecycle,
|
|
1041
|
+
};
|
|
1042
|
+
commandByDeclaration.set(declaration, entry);
|
|
1043
|
+
const complete = { ...entry, dependencies: dependenciesFor(declaration, ownerId) };
|
|
1044
|
+
commandByDeclaration.set(declaration, complete);
|
|
1045
|
+
commands.push(complete);
|
|
1046
|
+
return complete;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
function normalizeOptions(options) {
|
|
1050
|
+
const tsconfigPath = path.resolve(options.tsconfigPath);
|
|
1051
|
+
return {
|
|
1052
|
+
tsconfigPath,
|
|
1053
|
+
applicationFile: path.resolve(options.applicationFile),
|
|
1054
|
+
sourceRoot: path.resolve(options.sourceRoot),
|
|
1055
|
+
outputRoot: path.resolve(options.outputRoot),
|
|
1056
|
+
artifactsDirectory: path.resolve(options.artifactsDirectory),
|
|
1057
|
+
applicationExport: options.applicationExport ?? 'Application',
|
|
1058
|
+
projectRoot: path.dirname(tsconfigPath),
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
function createProgram(tsconfigPath) {
|
|
1062
|
+
const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
|
|
1063
|
+
if (configFile.error) {
|
|
1064
|
+
throw new DoxaCompilationError(formatDiagnostics([configFile.error]));
|
|
1065
|
+
}
|
|
1066
|
+
const parsed = ts.parseJsonConfigFileContent(configFile.config, ts.sys, path.dirname(tsconfigPath));
|
|
1067
|
+
if (parsed.errors.length > 0) {
|
|
1068
|
+
throw new DoxaCompilationError(formatDiagnostics(parsed.errors));
|
|
1069
|
+
}
|
|
1070
|
+
return ts.createProgram({ rootNames: parsed.fileNames, options: parsed.options });
|
|
1071
|
+
}
|
|
1072
|
+
function assertValidProgram(program) {
|
|
1073
|
+
const diagnostics = ts.getPreEmitDiagnostics(program);
|
|
1074
|
+
if (diagnostics.length > 0) {
|
|
1075
|
+
throw new DoxaCompilationError(formatDiagnostics(diagnostics));
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
function formatDiagnostics(diagnostics) {
|
|
1079
|
+
return ts.formatDiagnosticsWithColorAndContext(diagnostics, {
|
|
1080
|
+
getCanonicalFileName: (fileName) => fileName,
|
|
1081
|
+
getCurrentDirectory: ts.sys.getCurrentDirectory,
|
|
1082
|
+
getNewLine: () => ts.sys.newLine,
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
function findExportedClass(source, exportName) {
|
|
1086
|
+
const declaration = source.statements.find((statement) => ts.isClassDeclaration(statement) &&
|
|
1087
|
+
statement.name?.text === exportName &&
|
|
1088
|
+
hasModifier(statement, ts.SyntaxKind.ExportKeyword));
|
|
1089
|
+
if (!declaration) {
|
|
1090
|
+
throw new DoxaCompilationError(`Expected exported Application class ${exportName} in ${source.fileName}.`);
|
|
1091
|
+
}
|
|
1092
|
+
return declaration;
|
|
1093
|
+
}
|
|
1094
|
+
function assertDeclarationOnly(declaration, kind) {
|
|
1095
|
+
for (const member of declaration.members) {
|
|
1096
|
+
if (!ts.isPropertyDeclaration(member)) {
|
|
1097
|
+
fail(member, `${kind} declarations may contain declarative fields only.`);
|
|
1098
|
+
}
|
|
1099
|
+
const name = propertyName(member.name);
|
|
1100
|
+
if (!name ||
|
|
1101
|
+
!DECLARATION_FIELDS.has(name) ||
|
|
1102
|
+
hasModifier(member, ts.SyntaxKind.StaticKeyword)) {
|
|
1103
|
+
fail(member, `${kind} field ${name ?? member.name.getText()} is not a supported declaration field.`);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
function assertConfigurationDeclaration(declaration) {
|
|
1108
|
+
for (const member of declaration.members) {
|
|
1109
|
+
if (!ts.isPropertyDeclaration(member) || hasModifier(member, ts.SyntaxKind.StaticKeyword)) {
|
|
1110
|
+
fail(member, 'Configuration declarations may contain instance properties only.');
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
function readClassArray(declaration, name, checker) {
|
|
1115
|
+
const property = findInstanceProperty(declaration, name);
|
|
1116
|
+
if (!property)
|
|
1117
|
+
return [];
|
|
1118
|
+
if (!property.initializer || !ts.isArrayLiteralExpression(property.initializer)) {
|
|
1119
|
+
fail(property, `${name} must be a literal array of direct class references.`);
|
|
1120
|
+
}
|
|
1121
|
+
return property.initializer.elements.map((element) => {
|
|
1122
|
+
if (ts.isSpreadElement(element)) {
|
|
1123
|
+
fail(element, `${name} may not contain spread elements.`);
|
|
1124
|
+
}
|
|
1125
|
+
const resolved = resolveClassReference(element, checker);
|
|
1126
|
+
if (!resolved)
|
|
1127
|
+
fail(element, `${element.getText()} does not resolve to a class declaration.`);
|
|
1128
|
+
return resolved;
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
function resolveClassReference(node, checker) {
|
|
1132
|
+
let symbol = checker.getSymbolAtLocation(node);
|
|
1133
|
+
if (symbol && (symbol.flags & ts.SymbolFlags.Alias) !== 0) {
|
|
1134
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
1135
|
+
}
|
|
1136
|
+
const declaration = symbol?.valueDeclaration ?? symbol?.declarations?.[0];
|
|
1137
|
+
return declaration && ts.isClassDeclaration(declaration) ? declaration : undefined;
|
|
1138
|
+
}
|
|
1139
|
+
function classDeclarationForType(parameter, checker) {
|
|
1140
|
+
const type = checker.getNonNullableType(checker.getTypeAtLocation(parameter));
|
|
1141
|
+
const symbol = type.getSymbol();
|
|
1142
|
+
const declaration = symbol?.valueDeclaration ?? symbol?.declarations?.[0];
|
|
1143
|
+
return declaration && ts.isClassDeclaration(declaration) ? declaration : undefined;
|
|
1144
|
+
}
|
|
1145
|
+
function roleInjectionKind(call) {
|
|
1146
|
+
if (!ts.isPropertyAccessExpression(call.expression))
|
|
1147
|
+
return undefined;
|
|
1148
|
+
if (call.expression.name.text === 'inject' &&
|
|
1149
|
+
call.expression.expression.kind === ts.SyntaxKind.ThisKeyword)
|
|
1150
|
+
return 'required';
|
|
1151
|
+
const receiver = call.expression.expression;
|
|
1152
|
+
return call.expression.name.text === 'optional' &&
|
|
1153
|
+
ts.isPropertyAccessExpression(receiver) &&
|
|
1154
|
+
receiver.name.text === 'inject' &&
|
|
1155
|
+
receiver.expression.kind === ts.SyntaxKind.ThisKeyword
|
|
1156
|
+
? 'optional'
|
|
1157
|
+
: undefined;
|
|
1158
|
+
}
|
|
1159
|
+
function compileConfigurationProperty(configurationName, property, checker, projectRoot) {
|
|
1160
|
+
const name = propertyName(property.name);
|
|
1161
|
+
if (!name)
|
|
1162
|
+
fail(property, 'Configuration property names must be identifiers or string literals.');
|
|
1163
|
+
const type = checker.getTypeAtLocation(property);
|
|
1164
|
+
const nonNullable = checker.getNonNullableType(type);
|
|
1165
|
+
const secret = isNamedCoreType(nonNullable, 'SecretString');
|
|
1166
|
+
const literalValues = literalUnionValues(nonNullable);
|
|
1167
|
+
const kind = secret
|
|
1168
|
+
? 'secret-string'
|
|
1169
|
+
: literalValues
|
|
1170
|
+
? 'literal-union'
|
|
1171
|
+
: (nonNullable.flags & ts.TypeFlags.StringLike) !== 0
|
|
1172
|
+
? 'string'
|
|
1173
|
+
: (nonNullable.flags & ts.TypeFlags.NumberLike) !== 0
|
|
1174
|
+
? 'number'
|
|
1175
|
+
: (nonNullable.flags & ts.TypeFlags.BooleanLike) !== 0
|
|
1176
|
+
? 'boolean'
|
|
1177
|
+
: undefined;
|
|
1178
|
+
if (!kind) {
|
|
1179
|
+
fail(property, `Configuration property ${name} must use a supported scalar or literal union type.`);
|
|
1180
|
+
}
|
|
1181
|
+
const defaultValue = property.initializer
|
|
1182
|
+
? readScalarInitializer(property.initializer)
|
|
1183
|
+
: undefined;
|
|
1184
|
+
const group = toScreamingSnake(configurationName.replace(/Config$/, ''));
|
|
1185
|
+
const result = {
|
|
1186
|
+
name,
|
|
1187
|
+
environmentKey: `${group}_${toScreamingSnake(name)}`,
|
|
1188
|
+
kind,
|
|
1189
|
+
...(literalValues ? { allowedValues: literalValues } : {}),
|
|
1190
|
+
optional: Boolean(property.questionToken) || includesUndefined(type),
|
|
1191
|
+
sensitive: secret,
|
|
1192
|
+
...(defaultValue !== undefined ? { defaultValue } : {}),
|
|
1193
|
+
source: sourceOf(property, projectRoot),
|
|
1194
|
+
};
|
|
1195
|
+
return result;
|
|
1196
|
+
}
|
|
1197
|
+
function literalUnionValues(type) {
|
|
1198
|
+
if (!type.isUnion())
|
|
1199
|
+
return undefined;
|
|
1200
|
+
const values = [];
|
|
1201
|
+
for (const member of type.types) {
|
|
1202
|
+
if (member.isStringLiteral() || member.isNumberLiteral()) {
|
|
1203
|
+
values.push(member.value);
|
|
1204
|
+
}
|
|
1205
|
+
else if ((member.flags & ts.TypeFlags.BooleanLiteral) !== 0) {
|
|
1206
|
+
values.push(member.intrinsicName === 'true');
|
|
1207
|
+
}
|
|
1208
|
+
else {
|
|
1209
|
+
return undefined;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
return values.length > 0 ? values : undefined;
|
|
1213
|
+
}
|
|
1214
|
+
function isNamedCoreType(type, name) {
|
|
1215
|
+
const declaration = type.getSymbol()?.declarations?.[0];
|
|
1216
|
+
return Boolean(declaration &&
|
|
1217
|
+
ts.isClassDeclaration(declaration) &&
|
|
1218
|
+
declaration.name?.text === name &&
|
|
1219
|
+
isCoreDeclaration(declaration));
|
|
1220
|
+
}
|
|
1221
|
+
function readScalarInitializer(initializer) {
|
|
1222
|
+
if (ts.isStringLiteral(initializer) || ts.isNumericLiteral(initializer)) {
|
|
1223
|
+
return ts.isNumericLiteral(initializer) ? Number(initializer.text) : initializer.text;
|
|
1224
|
+
}
|
|
1225
|
+
if (initializer.kind === ts.SyntaxKind.TrueKeyword)
|
|
1226
|
+
return true;
|
|
1227
|
+
if (initializer.kind === ts.SyntaxKind.FalseKeyword)
|
|
1228
|
+
return false;
|
|
1229
|
+
fail(initializer, 'Configuration defaults must be literal strings, numbers, or booleans.');
|
|
1230
|
+
}
|
|
1231
|
+
function lifecycleOf(declaration, checker) {
|
|
1232
|
+
const type = checker.getTypeAtLocation(declaration);
|
|
1233
|
+
return {
|
|
1234
|
+
start: Boolean(type.getProperty('start')),
|
|
1235
|
+
drain: Boolean(type.getProperty('drain')),
|
|
1236
|
+
stop: Boolean(type.getProperty('stop')),
|
|
1237
|
+
dispose: Boolean(type.getProperty('dispose')),
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
function implementsNamedInterface(declaration, name, checker) {
|
|
1241
|
+
return (declaration.heritageClauses
|
|
1242
|
+
?.filter((clause) => clause.token === ts.SyntaxKind.ImplementsKeyword)
|
|
1243
|
+
.some((clause) => clause.types.some((type) => {
|
|
1244
|
+
const referenced = resolveNamedDeclaration(type.expression, checker);
|
|
1245
|
+
return referenced?.name?.text === name && isCoreDeclaration(referenced);
|
|
1246
|
+
})) ?? false);
|
|
1247
|
+
}
|
|
1248
|
+
function extendsNamedClass(declaration, name, checker) {
|
|
1249
|
+
for (const clause of declaration.heritageClauses ?? []) {
|
|
1250
|
+
if (clause.token !== ts.SyntaxKind.ExtendsKeyword)
|
|
1251
|
+
continue;
|
|
1252
|
+
for (const type of clause.types) {
|
|
1253
|
+
const base = resolveClassReference(type.expression, checker);
|
|
1254
|
+
if (base?.name?.text === name && isCoreDeclaration(base))
|
|
1255
|
+
return true;
|
|
1256
|
+
if (base && extendsNamedClass(base, name, checker))
|
|
1257
|
+
return true;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
return false;
|
|
1261
|
+
}
|
|
1262
|
+
function resolveNamedDeclaration(node, checker) {
|
|
1263
|
+
let symbol = checker.getSymbolAtLocation(node);
|
|
1264
|
+
if (symbol && (symbol.flags & ts.SymbolFlags.Alias) !== 0) {
|
|
1265
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
1266
|
+
}
|
|
1267
|
+
return symbol?.declarations?.find((declaration) => 'name' in declaration);
|
|
1268
|
+
}
|
|
1269
|
+
function isCoreDeclaration(declaration) {
|
|
1270
|
+
const source = declaration.getSourceFile().fileName.split(path.sep).join('/');
|
|
1271
|
+
return source.includes('/packages/core/') || source.includes('/@doxajs/core/');
|
|
1272
|
+
}
|
|
1273
|
+
function builtinIdForDeclaration(declaration) {
|
|
1274
|
+
const name = requiredClassName(declaration);
|
|
1275
|
+
if (name !== 'ActionBus' &&
|
|
1276
|
+
name !== 'QueryBus' &&
|
|
1277
|
+
name !== 'CurrentExecution' &&
|
|
1278
|
+
name !== 'CurrentJob' &&
|
|
1279
|
+
name !== 'UnitOfWork' &&
|
|
1280
|
+
name !== 'Authorization' &&
|
|
1281
|
+
name !== 'Mailer' &&
|
|
1282
|
+
name !== 'Sms' &&
|
|
1283
|
+
name !== 'DeliveryLedger' &&
|
|
1284
|
+
name !== 'Logger')
|
|
1285
|
+
return undefined;
|
|
1286
|
+
const source = declaration.getSourceFile().fileName.split(path.sep).join('/');
|
|
1287
|
+
if (!source.includes('/packages/core/') && !source.includes('/@doxajs/core/'))
|
|
1288
|
+
return undefined;
|
|
1289
|
+
if (name === 'ActionBus')
|
|
1290
|
+
return 'doxa:action-bus';
|
|
1291
|
+
if (name === 'QueryBus')
|
|
1292
|
+
return 'doxa:query-bus';
|
|
1293
|
+
if (name === 'CurrentExecution')
|
|
1294
|
+
return 'doxa:current-execution';
|
|
1295
|
+
if (name === 'CurrentJob')
|
|
1296
|
+
return 'doxa:current-job';
|
|
1297
|
+
if (name === 'Authorization')
|
|
1298
|
+
return 'doxa:authorization';
|
|
1299
|
+
if (name === 'Mailer')
|
|
1300
|
+
return 'doxa:mailer';
|
|
1301
|
+
if (name === 'Sms')
|
|
1302
|
+
return 'doxa:sms';
|
|
1303
|
+
if (name === 'DeliveryLedger')
|
|
1304
|
+
return 'doxa:delivery-ledger';
|
|
1305
|
+
if (name === 'Logger')
|
|
1306
|
+
return 'doxa:logger';
|
|
1307
|
+
return 'doxa:unit-of-work';
|
|
1308
|
+
}
|
|
1309
|
+
function providerCapabilityForDeclaration(declaration) {
|
|
1310
|
+
const name = requiredClassName(declaration);
|
|
1311
|
+
if (name !== 'Auth' &&
|
|
1312
|
+
name !== 'TransactionManager' &&
|
|
1313
|
+
name !== 'QueueManager' &&
|
|
1314
|
+
name !== 'Cache' &&
|
|
1315
|
+
name !== 'MailTransport' &&
|
|
1316
|
+
name !== 'SmsTransport' &&
|
|
1317
|
+
name !== 'BroadcastTransport' &&
|
|
1318
|
+
name !== 'Telemetry' &&
|
|
1319
|
+
name !== 'ObservationRecorder')
|
|
1320
|
+
return undefined;
|
|
1321
|
+
const source = declaration.getSourceFile().fileName.split(path.sep).join('/');
|
|
1322
|
+
return source.includes('/packages/core/') || source.includes('/@doxajs/core/')
|
|
1323
|
+
? name === 'Auth'
|
|
1324
|
+
? 'authentication'
|
|
1325
|
+
: name === 'TransactionManager'
|
|
1326
|
+
? 'transactions'
|
|
1327
|
+
: name === 'QueueManager'
|
|
1328
|
+
? 'queues'
|
|
1329
|
+
: name === 'Cache'
|
|
1330
|
+
? 'cache'
|
|
1331
|
+
: name === 'MailTransport'
|
|
1332
|
+
? 'mail'
|
|
1333
|
+
: name === 'SmsTransport'
|
|
1334
|
+
? 'sms'
|
|
1335
|
+
: name === 'BroadcastTransport'
|
|
1336
|
+
? 'broadcasting'
|
|
1337
|
+
: name === 'Telemetry'
|
|
1338
|
+
? 'telemetry'
|
|
1339
|
+
: 'observations'
|
|
1340
|
+
: undefined;
|
|
1341
|
+
}
|
|
1342
|
+
function assertConcreteClass(declaration) {
|
|
1343
|
+
if (hasModifier(declaration, ts.SyntaxKind.AbstractKeyword)) {
|
|
1344
|
+
fail(declaration, `${requiredClassName(declaration)} must be concrete before it can be registered.`);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
function renderRegistry(application, configurations, providers, operations, models, observers, routes, events, listeners, jobs, schedules, policies, signals, signalHandlers, commands, buildHash, options) {
|
|
1348
|
+
const registrations = [
|
|
1349
|
+
application,
|
|
1350
|
+
...configurations,
|
|
1351
|
+
...providers,
|
|
1352
|
+
...operations,
|
|
1353
|
+
...models,
|
|
1354
|
+
...observers,
|
|
1355
|
+
...routes,
|
|
1356
|
+
...events,
|
|
1357
|
+
...listeners,
|
|
1358
|
+
...jobs,
|
|
1359
|
+
...schedules,
|
|
1360
|
+
...policies,
|
|
1361
|
+
...signals,
|
|
1362
|
+
...signalHandlers,
|
|
1363
|
+
...commands,
|
|
1364
|
+
].sort((left, right) => left.id.localeCompare(right.id));
|
|
1365
|
+
const imports = registrations.map((registration, index) => {
|
|
1366
|
+
const sourceFile = registration.declaration.getSourceFile().fileName;
|
|
1367
|
+
const relativeSource = path.relative(options.sourceRoot, sourceFile);
|
|
1368
|
+
if (relativeSource.startsWith('..') || path.isAbsolute(relativeSource)) {
|
|
1369
|
+
fail(registration.declaration, 'Registry classes must be emitted from within the configured source root.');
|
|
1370
|
+
}
|
|
1371
|
+
const outputFile = path
|
|
1372
|
+
.join(options.outputRoot, relativeSource)
|
|
1373
|
+
.replace(/\.(?:mts|cts|tsx|ts)$/, '.js');
|
|
1374
|
+
let specifier = path.relative(options.artifactsDirectory, outputFile).split(path.sep).join('/');
|
|
1375
|
+
if (!specifier.startsWith('.'))
|
|
1376
|
+
specifier = `./${specifier}`;
|
|
1377
|
+
return `import { ${requiredClassName(registration.declaration)} as C${index} } from ${JSON.stringify(specifier)}`;
|
|
1378
|
+
});
|
|
1379
|
+
const entries = registrations.map((registration, index) => ` ${JSON.stringify(registration.id)}: C${index},`);
|
|
1380
|
+
return [
|
|
1381
|
+
'// Generated by @doxajs/compiler. Do not edit.',
|
|
1382
|
+
...imports,
|
|
1383
|
+
'',
|
|
1384
|
+
`export const formatVersion = ${MANIFEST_FORMAT_VERSION}`,
|
|
1385
|
+
`export const buildHash = ${JSON.stringify(buildHash)}`,
|
|
1386
|
+
'export const constructors = Object.freeze({',
|
|
1387
|
+
...entries,
|
|
1388
|
+
'})',
|
|
1389
|
+
'',
|
|
1390
|
+
].join('\n');
|
|
1391
|
+
}
|
|
1392
|
+
function readRequiredInstanceString(declaration, name) {
|
|
1393
|
+
const property = findInstanceProperty(declaration, name);
|
|
1394
|
+
if (!property?.initializer ||
|
|
1395
|
+
!ts.isStringLiteral(property.initializer) ||
|
|
1396
|
+
property.initializer.text.length === 0) {
|
|
1397
|
+
fail(declaration, `${requiredClassName(declaration)}.${name} must be a non-empty string literal.`);
|
|
1398
|
+
}
|
|
1399
|
+
return property.initializer.text;
|
|
1400
|
+
}
|
|
1401
|
+
function readRequiredStaticString(declaration, name) {
|
|
1402
|
+
const property = declaration.members.find((member) => ts.isPropertyDeclaration(member) &&
|
|
1403
|
+
hasModifier(member, ts.SyntaxKind.StaticKeyword) &&
|
|
1404
|
+
propertyName(member.name) === name);
|
|
1405
|
+
if (!property?.initializer ||
|
|
1406
|
+
!ts.isStringLiteral(property.initializer) ||
|
|
1407
|
+
property.initializer.text.length === 0) {
|
|
1408
|
+
fail(declaration, `${requiredClassName(declaration)} must declare static ${name} as a non-empty string literal.`);
|
|
1409
|
+
}
|
|
1410
|
+
return property.initializer.text;
|
|
1411
|
+
}
|
|
1412
|
+
function readAccess(declaration) {
|
|
1413
|
+
const access = readRequiredStaticString(declaration, 'access');
|
|
1414
|
+
if (access !== 'public' && !/^[a-z][a-z0-9._:-]{1,127}$/.test(access)) {
|
|
1415
|
+
fail(declaration, `${requiredClassName(declaration)}.access must be "public" or a stable ability name.`);
|
|
1416
|
+
}
|
|
1417
|
+
return access;
|
|
1418
|
+
}
|
|
1419
|
+
function staticProperty(declaration, name) {
|
|
1420
|
+
return declaration.members.find((member) => ts.isPropertyDeclaration(member) &&
|
|
1421
|
+
hasModifier(member, ts.SyntaxKind.StaticKeyword) &&
|
|
1422
|
+
propertyName(member.name) === name);
|
|
1423
|
+
}
|
|
1424
|
+
function readOptionalStaticString(declaration, name) {
|
|
1425
|
+
const property = staticProperty(declaration, name);
|
|
1426
|
+
if (!property)
|
|
1427
|
+
return undefined;
|
|
1428
|
+
if (!property.initializer ||
|
|
1429
|
+
!ts.isStringLiteral(property.initializer) ||
|
|
1430
|
+
property.initializer.text.length === 0) {
|
|
1431
|
+
fail(property, `${requiredClassName(declaration)}.${name} must be a non-empty string literal.`);
|
|
1432
|
+
}
|
|
1433
|
+
return property.initializer.text;
|
|
1434
|
+
}
|
|
1435
|
+
function readRequiredStaticStringArray(declaration, name) {
|
|
1436
|
+
const property = staticProperty(declaration, name);
|
|
1437
|
+
if (!property?.initializer || !ts.isArrayLiteralExpression(property.initializer)) {
|
|
1438
|
+
fail(declaration, `${requiredClassName(declaration)} must declare static ${name} as a literal string array.`);
|
|
1439
|
+
}
|
|
1440
|
+
return property.initializer.elements.map((element) => {
|
|
1441
|
+
if (!ts.isStringLiteral(element) || element.text.length === 0) {
|
|
1442
|
+
fail(element, `${requiredClassName(declaration)}.${name} may contain only non-empty string literals.`);
|
|
1443
|
+
}
|
|
1444
|
+
return element.text;
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
function readOptionalStaticNumberValue(declaration, name) {
|
|
1448
|
+
const property = staticProperty(declaration, name);
|
|
1449
|
+
if (!property)
|
|
1450
|
+
return undefined;
|
|
1451
|
+
if (!property.initializer || !ts.isNumericLiteral(property.initializer)) {
|
|
1452
|
+
fail(property, `${requiredClassName(declaration)}.${name} must be a numeric literal.`);
|
|
1453
|
+
}
|
|
1454
|
+
return Number(property.initializer.text);
|
|
1455
|
+
}
|
|
1456
|
+
function readRequiredStaticClass(declaration, name, checker) {
|
|
1457
|
+
const property = staticProperty(declaration, name);
|
|
1458
|
+
if (!property?.initializer) {
|
|
1459
|
+
fail(declaration, `${requiredClassName(declaration)} must declare static ${name} as a direct class reference.`);
|
|
1460
|
+
}
|
|
1461
|
+
const resolved = resolveClassReference(property.initializer, checker);
|
|
1462
|
+
if (!resolved)
|
|
1463
|
+
fail(property, `${requiredClassName(declaration)}.${name} must be a direct class reference.`);
|
|
1464
|
+
return resolved;
|
|
1465
|
+
}
|
|
1466
|
+
function readOptionalStaticJson(declaration, name) {
|
|
1467
|
+
const property = staticProperty(declaration, name);
|
|
1468
|
+
return property?.initializer ? readJsonLiteral(property.initializer) : undefined;
|
|
1469
|
+
}
|
|
1470
|
+
function readJsonLiteral(node) {
|
|
1471
|
+
if (ts.isParenthesizedExpression(node) ||
|
|
1472
|
+
ts.isAsExpression(node) ||
|
|
1473
|
+
ts.isTypeAssertionExpression(node) ||
|
|
1474
|
+
ts.isSatisfiesExpression(node)) {
|
|
1475
|
+
return readJsonLiteral(node.expression);
|
|
1476
|
+
}
|
|
1477
|
+
if (ts.isStringLiteral(node) || ts.isNumericLiteral(node)) {
|
|
1478
|
+
return ts.isNumericLiteral(node) ? Number(node.text) : node.text;
|
|
1479
|
+
}
|
|
1480
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword)
|
|
1481
|
+
return true;
|
|
1482
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword)
|
|
1483
|
+
return false;
|
|
1484
|
+
if (node.kind === ts.SyntaxKind.NullKeyword)
|
|
1485
|
+
return null;
|
|
1486
|
+
if (ts.isPrefixUnaryExpression(node) &&
|
|
1487
|
+
node.operator === ts.SyntaxKind.MinusToken &&
|
|
1488
|
+
ts.isNumericLiteral(node.operand))
|
|
1489
|
+
return -Number(node.operand.text);
|
|
1490
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
1491
|
+
return node.elements.map((element) => {
|
|
1492
|
+
if (ts.isSpreadElement(element))
|
|
1493
|
+
fail(element, 'Schedule input may not contain spreads.');
|
|
1494
|
+
return readJsonLiteral(element);
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
1498
|
+
return Object.fromEntries(node.properties.map((property) => {
|
|
1499
|
+
if (!ts.isPropertyAssignment(property)) {
|
|
1500
|
+
fail(property, 'Schedule input must use explicit JSON property assignments.');
|
|
1501
|
+
}
|
|
1502
|
+
const key = propertyName(property.name);
|
|
1503
|
+
if (!key)
|
|
1504
|
+
fail(property, 'Schedule input property names must be literal.');
|
|
1505
|
+
return [key, readJsonLiteral(property.initializer)];
|
|
1506
|
+
}));
|
|
1507
|
+
}
|
|
1508
|
+
fail(node, 'Schedule input must be a JSON literal so Gnosis and the manifest can inspect it.');
|
|
1509
|
+
}
|
|
1510
|
+
function validIdentifier(value) {
|
|
1511
|
+
return /^[A-Za-z_][A-Za-z0-9_$]*$/.test(value);
|
|
1512
|
+
}
|
|
1513
|
+
function validQualifiedIdentifier(value) {
|
|
1514
|
+
const parts = value.split('.');
|
|
1515
|
+
return parts.length > 0 && parts.length <= 2 && parts.every(validIdentifier);
|
|
1516
|
+
}
|
|
1517
|
+
function isStringRecord(value) {
|
|
1518
|
+
return (typeof value === 'object' &&
|
|
1519
|
+
value !== null &&
|
|
1520
|
+
!Array.isArray(value) &&
|
|
1521
|
+
Object.values(value).every((entry) => typeof entry === 'string'));
|
|
1522
|
+
}
|
|
1523
|
+
function readOptionalStaticNumber(declaration, name, fallback) {
|
|
1524
|
+
const property = declaration.members.find((member) => ts.isPropertyDeclaration(member) &&
|
|
1525
|
+
hasModifier(member, ts.SyntaxKind.StaticKeyword) &&
|
|
1526
|
+
propertyName(member.name) === name);
|
|
1527
|
+
if (!property)
|
|
1528
|
+
return fallback;
|
|
1529
|
+
if (!property.initializer || !ts.isNumericLiteral(property.initializer)) {
|
|
1530
|
+
fail(property, `${requiredClassName(declaration)}.${name} must be a numeric literal.`);
|
|
1531
|
+
}
|
|
1532
|
+
return Number(property.initializer.text);
|
|
1533
|
+
}
|
|
1534
|
+
function readOptionalStaticBoolean(declaration, name, fallback) {
|
|
1535
|
+
const property = declaration.members.find((member) => ts.isPropertyDeclaration(member) &&
|
|
1536
|
+
hasModifier(member, ts.SyntaxKind.StaticKeyword) &&
|
|
1537
|
+
propertyName(member.name) === name);
|
|
1538
|
+
if (!property)
|
|
1539
|
+
return fallback;
|
|
1540
|
+
if (property.initializer?.kind === ts.SyntaxKind.TrueKeyword)
|
|
1541
|
+
return true;
|
|
1542
|
+
if (property.initializer?.kind === ts.SyntaxKind.FalseKeyword)
|
|
1543
|
+
return false;
|
|
1544
|
+
fail(property, `${requiredClassName(declaration)}.${name} must be a boolean literal.`);
|
|
1545
|
+
}
|
|
1546
|
+
function findInstanceProperty(declaration, name) {
|
|
1547
|
+
return declaration.members.find((member) => ts.isPropertyDeclaration(member) &&
|
|
1548
|
+
!hasModifier(member, ts.SyntaxKind.StaticKeyword) &&
|
|
1549
|
+
propertyName(member.name) === name);
|
|
1550
|
+
}
|
|
1551
|
+
function propertyName(name) {
|
|
1552
|
+
return ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : undefined;
|
|
1553
|
+
}
|
|
1554
|
+
function requiredClassName(declaration) {
|
|
1555
|
+
if (!declaration.name)
|
|
1556
|
+
fail(declaration, 'Doxa declarations must use named classes.');
|
|
1557
|
+
return declaration.name.text;
|
|
1558
|
+
}
|
|
1559
|
+
function sourceOf(node, projectRoot) {
|
|
1560
|
+
const source = node.getSourceFile();
|
|
1561
|
+
const position = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
1562
|
+
return {
|
|
1563
|
+
file: path.relative(projectRoot, source.fileName).split(path.sep).join('/'),
|
|
1564
|
+
line: position.line + 1,
|
|
1565
|
+
column: position.character + 1,
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
function includesUndefined(type) {
|
|
1569
|
+
return (type.isUnion() && type.types.some((member) => (member.flags & ts.TypeFlags.Undefined) !== 0));
|
|
1570
|
+
}
|
|
1571
|
+
function hasModifier(node, kind) {
|
|
1572
|
+
return Boolean(ts.canHaveModifiers(node) && ts.getModifiers(node)?.some((modifier) => modifier.kind === kind));
|
|
1573
|
+
}
|
|
1574
|
+
function byId(left, right) {
|
|
1575
|
+
return left.id.localeCompare(right.id);
|
|
1576
|
+
}
|
|
1577
|
+
function toKebabCase(value) {
|
|
1578
|
+
return value
|
|
1579
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
1580
|
+
.replace(/_/g, '-')
|
|
1581
|
+
.toLowerCase();
|
|
1582
|
+
}
|
|
1583
|
+
function toScreamingSnake(value) {
|
|
1584
|
+
return value
|
|
1585
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
1586
|
+
.replace(/-/g, '_')
|
|
1587
|
+
.toUpperCase();
|
|
1588
|
+
}
|
|
1589
|
+
function fail(node, message) {
|
|
1590
|
+
const source = node.getSourceFile();
|
|
1591
|
+
const position = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
1592
|
+
throw new DoxaCompilationError(`${source.fileName}:${position.line + 1}:${position.character + 1} ${message}`);
|
|
1593
|
+
}
|
|
1594
|
+
//# sourceMappingURL=compiler.js.map
|