proteum 2.0.0 → 2.1.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.
Files changed (94) hide show
  1. package/AGENTS.md +13 -1
  2. package/README.md +375 -0
  3. package/agents/framework/AGENTS.md +917 -0
  4. package/agents/project/AGENTS.md +138 -0
  5. package/agents/{codex → project}/CODING_STYLE.md +3 -2
  6. package/agents/project/client/AGENTS.md +108 -0
  7. package/agents/{codex → project}/client/pages/AGENTS.md +8 -8
  8. package/agents/{codex → project}/server/routes/AGENTS.md +2 -1
  9. package/agents/project/server/services/AGENTS.md +170 -0
  10. package/agents/{codex → project}/tests/AGENTS.md +1 -0
  11. package/cli/app/config.ts +3 -2
  12. package/cli/app/index.ts +6 -66
  13. package/cli/bin.js +7 -2
  14. package/cli/commands/build.ts +94 -27
  15. package/cli/commands/check.ts +15 -1
  16. package/cli/commands/dev.ts +288 -132
  17. package/cli/commands/doctor.ts +108 -0
  18. package/cli/commands/explain.ts +226 -0
  19. package/cli/commands/init.ts +76 -70
  20. package/cli/commands/lint.ts +18 -1
  21. package/cli/commands/refresh.ts +16 -6
  22. package/cli/commands/typecheck.ts +14 -1
  23. package/cli/compiler/artifacts/controllers.ts +150 -0
  24. package/cli/compiler/artifacts/discovery.ts +132 -0
  25. package/cli/compiler/artifacts/manifest.ts +267 -0
  26. package/cli/compiler/artifacts/routing.ts +315 -0
  27. package/cli/compiler/artifacts/services.ts +480 -0
  28. package/cli/compiler/artifacts/shared.ts +12 -0
  29. package/cli/compiler/client/identite.ts +2 -1
  30. package/cli/compiler/client/index.ts +13 -3
  31. package/cli/compiler/common/controllers.ts +23 -28
  32. package/cli/compiler/common/files/style.ts +3 -4
  33. package/cli/compiler/common/generatedRouteModules.ts +333 -19
  34. package/cli/compiler/common/proteumManifest.ts +133 -0
  35. package/cli/compiler/index.ts +33 -896
  36. package/cli/compiler/server/index.ts +21 -4
  37. package/cli/context.ts +71 -0
  38. package/cli/index.ts +39 -181
  39. package/cli/presentation/commands.ts +208 -0
  40. package/cli/presentation/compileReporter.ts +65 -0
  41. package/cli/presentation/devSession.ts +70 -0
  42. package/cli/presentation/help.ts +193 -0
  43. package/cli/presentation/ink.ts +69 -0
  44. package/cli/presentation/layout.ts +83 -0
  45. package/cli/runtime/argv.ts +49 -0
  46. package/cli/runtime/command.ts +25 -0
  47. package/cli/runtime/commands.ts +221 -0
  48. package/cli/runtime/importEsm.ts +7 -0
  49. package/cli/runtime/verbose.ts +15 -0
  50. package/cli/utils/agents.ts +5 -4
  51. package/cli/utils/keyboard.ts +12 -6
  52. package/client/app/index.ts +0 -6
  53. package/client/services/router/index.tsx +1 -1
  54. package/client/services/router/response/index.tsx +2 -2
  55. package/common/dev/serverHotReload.ts +12 -0
  56. package/common/router/index.ts +3 -2
  57. package/common/router/layouts.ts +1 -1
  58. package/common/router/pageSetup.ts +1 -0
  59. package/package.json +10 -8
  60. package/prettier/router-registration-plugin.cjs +52 -0
  61. package/prettier.config.cjs +1 -0
  62. package/scripts/cleanup-generated-controllers.ts +2 -2
  63. package/scripts/fix-reference-app-typing.ts +2 -2
  64. package/scripts/format-router-registrations.ts +119 -0
  65. package/scripts/migrate-explicit-controllers-and-request.ts +423 -0
  66. package/scripts/refactor-server-controllers.ts +19 -18
  67. package/scripts/refactor-server-runtime-aliases.ts +1 -1
  68. package/server/app/commands.ts +309 -25
  69. package/server/app/container/config.ts +1 -1
  70. package/server/app/container/index.ts +2 -2
  71. package/server/app/controller/index.ts +13 -4
  72. package/server/app/index.ts +53 -37
  73. package/server/app/service/container.ts +26 -28
  74. package/server/app/service/index.ts +10 -20
  75. package/server/app.tsconfig.json +9 -2
  76. package/server/index.ts +32 -1
  77. package/server/services/auth/index.ts +234 -15
  78. package/server/services/auth/router/index.ts +39 -7
  79. package/server/services/auth/router/request.ts +40 -8
  80. package/server/services/disks/index.ts +1 -1
  81. package/server/services/prisma/Facet.ts +2 -2
  82. package/server/services/prisma/index.ts +22 -5
  83. package/server/services/prisma/mariadb.ts +47 -0
  84. package/server/services/router/http/index.ts +9 -1
  85. package/server/services/router/index.ts +10 -4
  86. package/server/services/router/response/index.ts +26 -6
  87. package/types/auth-check-rules.test.ts +51 -0
  88. package/types/controller-request-context.test.ts +55 -0
  89. package/types/service-config.test.ts +39 -0
  90. package/agents/codex/AGENTS.md +0 -95
  91. package/agents/codex/client/AGENTS.md +0 -102
  92. package/agents/codex/server/services/AGENTS.md +0 -137
  93. package/server/services/models.7z +0 -0
  94. /package/agents/{codex → project}/agents.md.zip +0 -0
@@ -3,12 +3,12 @@
3
3
  ----------------------------------*/
4
4
 
5
5
  // Npm
6
- import yargsParser from 'yargs-parser';
6
+ import { Cli, Command as ClipanionCommand, Option, UsageError } from 'clipanion';
7
7
 
8
8
  // Core
9
9
  import type { Application } from './index';
10
10
  import Service from '@server/app/service';
11
- import { NotFound } from '@common/errors';
11
+ import { InputError, NotFound } from '@common/errors';
12
12
 
13
13
  /*----------------------------------
14
14
  - TYPES
@@ -16,9 +16,9 @@ import { NotFound } from '@common/errors';
16
16
 
17
17
  type CommandCallback<TArgs extends any[]> = (...args: TArgs) => Promise<any>;
18
18
 
19
- export type CommandsList = { [commandName: string]: Command };
19
+ export type CommandsList = { [commandName: string]: RuntimeCommand };
20
20
 
21
- export type Command<TArgs extends any[] = any[]> = {
21
+ export type RuntimeCommand<TArgs extends any[] = any[]> = {
22
22
  name: string;
23
23
  description: string;
24
24
  run?: CommandCallback<TArgs>;
@@ -37,6 +37,166 @@ export type Hooks = {};
37
37
 
38
38
  export type Services = {};
39
39
 
40
+ type TCommandArgumentValue = string | number | boolean;
41
+ type TParsedCommandArgs = { [key: string]: TCommandArgumentValue | TCommandArgumentValue[] };
42
+
43
+ const commandValuePattern = /^-?(?:\d+|\d*\.\d+)$/;
44
+
45
+ const tokenizeCommandString = (commandString: string) => {
46
+ const tokens: string[] = [];
47
+ let currentToken = '';
48
+ let quote: '"' | "'" | undefined;
49
+ let escaping = false;
50
+
51
+ for (const character of commandString) {
52
+ if (escaping) {
53
+ currentToken += character;
54
+ escaping = false;
55
+ continue;
56
+ }
57
+
58
+ if (character === '\\') {
59
+ escaping = true;
60
+ continue;
61
+ }
62
+
63
+ if (quote) {
64
+ if (character === quote) {
65
+ quote = undefined;
66
+ continue;
67
+ }
68
+
69
+ currentToken += character;
70
+ continue;
71
+ }
72
+
73
+ if (character === '"' || character === "'") {
74
+ quote = character;
75
+ continue;
76
+ }
77
+
78
+ if (/\s/.test(character)) {
79
+ if (!currentToken) continue;
80
+
81
+ tokens.push(currentToken);
82
+ currentToken = '';
83
+ continue;
84
+ }
85
+
86
+ currentToken += character;
87
+ }
88
+
89
+ if (escaping) currentToken += '\\';
90
+ if (currentToken) tokens.push(currentToken);
91
+
92
+ return tokens;
93
+ };
94
+
95
+ const isOptionToken = (token: string) => /^-(?!\d+(\.\d+)?$).+/.test(token);
96
+
97
+ const normalizeCommandValue = (value: string): TCommandArgumentValue => {
98
+ if (value === 'true') return true;
99
+ if (value === 'false') return false;
100
+ if (commandValuePattern.test(value)) return Number(value);
101
+
102
+ return value;
103
+ };
104
+
105
+ const addParsedArgValue = (args: TParsedCommandArgs, key: string, value: TCommandArgumentValue) => {
106
+ const existingValue = args[key];
107
+
108
+ if (existingValue === undefined) {
109
+ args[key] = value;
110
+ return;
111
+ }
112
+
113
+ args[key] = Array.isArray(existingValue) ? [...existingValue, value] : [existingValue, value];
114
+ };
115
+
116
+ const parseCommandOptionTokens = (tokens: string[]) => {
117
+ const namedArguments: TParsedCommandArgs = {};
118
+ const positionalArguments: string[] = [];
119
+
120
+ let usePositionalOnly = false;
121
+
122
+ for (let index = 0; index < tokens.length; index++) {
123
+ const token = tokens[index];
124
+
125
+ if (usePositionalOnly) {
126
+ positionalArguments.push(token);
127
+ continue;
128
+ }
129
+
130
+ if (token === '--') {
131
+ usePositionalOnly = true;
132
+ continue;
133
+ }
134
+
135
+ if (token === '--help' || token === '-h') return { help: true as const, args: namedArguments, positionalArguments };
136
+
137
+ if (token.startsWith('--') && token.length > 2) {
138
+ const body = token.slice(2);
139
+
140
+ if (body.startsWith('no-') && body.length > 3) {
141
+ addParsedArgValue(namedArguments, body.slice(3), false);
142
+ continue;
143
+ }
144
+
145
+ const equalsIndex = body.indexOf('=');
146
+
147
+ if (equalsIndex >= 0) {
148
+ addParsedArgValue(
149
+ namedArguments,
150
+ body.slice(0, equalsIndex),
151
+ normalizeCommandValue(body.slice(equalsIndex + 1)),
152
+ );
153
+ continue;
154
+ }
155
+
156
+ const nextToken = tokens[index + 1];
157
+
158
+ if (nextToken !== undefined && !isOptionToken(nextToken)) {
159
+ addParsedArgValue(namedArguments, body, normalizeCommandValue(nextToken));
160
+ index++;
161
+ continue;
162
+ }
163
+
164
+ addParsedArgValue(namedArguments, body, true);
165
+ continue;
166
+ }
167
+
168
+ if (token.startsWith('-') && token.length > 1 && !commandValuePattern.test(token)) {
169
+ const body = token.slice(1);
170
+ const equalsIndex = body.indexOf('=');
171
+
172
+ if (equalsIndex >= 0) {
173
+ addParsedArgValue(
174
+ namedArguments,
175
+ body.slice(0, equalsIndex),
176
+ normalizeCommandValue(body.slice(equalsIndex + 1)),
177
+ );
178
+ continue;
179
+ }
180
+
181
+ const nextToken = tokens[index + 1];
182
+
183
+ if (body.length === 1 && nextToken !== undefined && !isOptionToken(nextToken)) {
184
+ addParsedArgValue(namedArguments, body, normalizeCommandValue(nextToken));
185
+ index++;
186
+ continue;
187
+ }
188
+
189
+ for (const shortFlag of body) addParsedArgValue(namedArguments, shortFlag, true);
190
+
191
+ continue;
192
+ }
193
+
194
+ positionalArguments.push(token);
195
+ }
196
+
197
+ return { help: false as const, args: namedArguments, positionalArguments };
198
+ };
199
+
40
200
  /*----------------------------------
41
201
  - SERVICE
42
202
  ----------------------------------*/
@@ -45,68 +205,192 @@ export default class CommandsManager extends Service<Config, Hooks, Application>
45
205
 
46
206
  public commandsIndex: CommandsList = {};
47
207
 
208
+ private runtimeCli?: Cli;
209
+
48
210
  public command<TArgs extends any[]>(
49
211
  ...args:
50
- | [name: string, description: string, childrens: Command[]]
51
- | [name: string, description: string, run: CommandCallback<TArgs>, childrens?: Command[]]
52
- ): Command {
212
+ | [name: string, description: string, childrens: RuntimeCommand[]]
213
+ | [name: string, description: string, run: CommandCallback<TArgs>, childrens?: RuntimeCommand[]]
214
+ ): RuntimeCommand {
53
215
  let name: string, description: string;
54
- let childrens: Command[] | undefined;
216
+ let childrens: RuntimeCommand[] | undefined;
55
217
  let run: CommandCallback<TArgs> | undefined;
56
218
 
57
219
  if (typeof args[2] === 'object') [name, description, childrens] = args;
58
220
  else [name, description, run, childrens] = args;
59
221
 
60
- const command: Command = { name, description, run, childrens: childrens ? this.indexFromList(childrens) : {} };
222
+ const command: RuntimeCommand = { name, description, run, childrens: childrens ? this.indexFromList(childrens) : {} };
61
223
 
62
224
  return command;
63
225
  }
64
226
 
65
- private indexFromList(list: Command[]): CommandsList {
227
+ private indexFromList(list: RuntimeCommand[]): CommandsList {
66
228
  const index: CommandsList = {};
67
229
  for (const command of list) index[command.name] = command;
68
230
 
69
231
  return index;
70
232
  }
71
233
 
234
+ private invalidateRuntimeCli() {
235
+ this.runtimeCli = undefined;
236
+ }
237
+
238
+ private createRuntimeCommandClass(command: RuntimeCommand, path: string[]) {
239
+ const manager = this;
240
+ const usage = ClipanionCommand.Usage({ description: command.description });
241
+
242
+ if (command.run === undefined) {
243
+ class RuntimeNamespaceCommand extends ClipanionCommand {
244
+ public static override paths = [path];
245
+ public static override usage = usage;
246
+
247
+ public async execute() {
248
+ throw new NotFound(`This command isn't runnable.`);
249
+ }
250
+ }
251
+
252
+ Object.defineProperty(RuntimeNamespaceCommand, 'name', {
253
+ value: `${path.map((segment) => segment.replace(/[^A-Za-z0-9]/g, '_')).join('_') || 'Root'}NamespaceCommand`,
254
+ });
255
+
256
+ return RuntimeNamespaceCommand;
257
+ }
258
+
259
+ class RuntimeRunnableCommand extends ClipanionCommand {
260
+ public static override paths = [path];
261
+ public static override usage = usage;
262
+
263
+ public proxy = Option.Proxy({ name: 'args' });
264
+
265
+ public async execute() {
266
+ return manager.executeRegisteredCommand(command, path, this.proxy);
267
+ }
268
+ }
269
+
270
+ Object.defineProperty(RuntimeRunnableCommand, 'name', {
271
+ value: `${path.map((segment) => segment.replace(/[^A-Za-z0-9]/g, '_')).join('_') || 'Root'}RunnableCommand`,
272
+ });
273
+
274
+ return RuntimeRunnableCommand;
275
+ }
276
+
277
+ private createRuntimeCli() {
278
+ const cli = new Cli({
279
+ binaryName: this.app.identity.identifier || 'app',
280
+ enableCapture: false,
281
+ });
282
+
283
+ const registerCommands = (commands: CommandsList, parentPath: string[] = []) => {
284
+ for (const command of Object.values(commands)) {
285
+ const path = [...parentPath, command.name];
286
+
287
+ cli.register(this.createRuntimeCommandClass(command, path));
288
+
289
+ if (Object.keys(command.childrens).length > 0) registerCommands(command.childrens, path);
290
+ }
291
+ };
292
+
293
+ registerCommands(this.commandsIndex);
294
+
295
+ return cli;
296
+ }
297
+
298
+ private getRuntimeCli() {
299
+ this.runtimeCli ??= this.createRuntimeCli();
300
+
301
+ return this.runtimeCli;
302
+ }
303
+
304
+ private async executeRegisteredCommand(command: RuntimeCommand, path: string[], proxyTokens: string[]) {
305
+ if (command.run === undefined) throw new NotFound(`This command isn't runnable.`);
306
+
307
+ const { help, args, positionalArguments } = parseCommandOptionTokens(proxyTokens);
308
+
309
+ this.config.debug &&
310
+ console.log(LogPrefix, `Run command path: ${path.join(' ')} | Parsed proxy tokens:`, {
311
+ proxyTokens,
312
+ args,
313
+ positionalArguments,
314
+ });
315
+
316
+ if (help) {
317
+ const cli = this.getRuntimeCli();
318
+ const runtimeCommand = cli.process(path, Cli.defaultContext);
319
+
320
+ return cli.usage(runtimeCommand, { detailed: true });
321
+ }
322
+
323
+ if (positionalArguments.length > 0) {
324
+ throw new UsageError(
325
+ `Unexpected positional arguments for "${path.join(' ')}": ${positionalArguments.join(', ')}.`,
326
+ );
327
+ }
328
+
329
+ const argsList = Object.values(args);
330
+
331
+ return command.run(...(argsList as Parameters<NonNullable<typeof command.run>>));
332
+ }
333
+
334
+ private createRuntimeCliApi(cli: Cli) {
335
+ return {
336
+ binaryLabel: cli.binaryLabel,
337
+ binaryName: cli.binaryName,
338
+ binaryVersion: cli.binaryVersion,
339
+ enableCapture: cli.enableCapture,
340
+ enableColors: cli.enableColors,
341
+ definitions: () => cli.definitions(),
342
+ definition: (commandClass: any) => cli.definition(commandClass),
343
+ error: (error: Error, opts?: any) => cli.error(error, opts),
344
+ format: (colored?: boolean) => cli.format(colored),
345
+ process: (input: string[]) => cli.process(input, Cli.defaultContext),
346
+ run: (input: string[]) => cli.run(input, Cli.defaultContext),
347
+ usage: (command?: any, opts?: any) => cli.usage(command, opts),
348
+ };
349
+ }
350
+
72
351
  /*----------------------------------
73
352
  - REGISTER
74
353
  ----------------------------------*/
75
- public fromList(list: Command[]) {
354
+ public fromList(list: RuntimeCommand[]) {
76
355
  for (const command of list) {
77
356
  if (this.commandsIndex[command.name] !== undefined)
78
357
  throw new Error(`Tried to register command "${command.name}", but it already has been defined.`);
79
358
 
80
359
  this.commandsIndex[command.name] = command;
81
360
  }
361
+
362
+ this.invalidateRuntimeCli();
82
363
  }
83
364
 
84
365
  /*----------------------------------
85
366
  - RUN
86
367
  ----------------------------------*/
87
368
  public async run(commandString: string) {
88
- const { _, ...args } = yargsParser(commandString);
369
+ const tokens = tokenizeCommandString(commandString);
89
370
 
90
- this.config.debug && console.log(LogPrefix, `Run command: ${commandString} | Parsed:`, { _, ...args });
371
+ this.config.debug && console.log(LogPrefix, `Run command: ${commandString} | Tokens:`, tokens);
91
372
 
92
- let command: Command | undefined;
93
- for (const commandName of _) {
94
- const commandsList: CommandsList = command === undefined ? this.commandsIndex : command.childrens;
373
+ if (tokens.length === 0) throw new NotFound(`Command not found.`);
95
374
 
96
- command = commandsList[commandName];
375
+ const cli = this.getRuntimeCli();
97
376
 
98
- if (command === undefined) break;
99
- }
377
+ try {
378
+ const command = cli.process(tokens, Cli.defaultContext);
100
379
 
101
- if (command === undefined) throw new NotFound(`Command not found.`);
380
+ if (command.help) return cli.usage(command, { detailed: true });
102
381
 
103
- if (command.run === undefined) throw new NotFound(`This command isn't runnable.`);
382
+ command.context = Cli.defaultContext;
383
+ command.cli = this.createRuntimeCliApi(cli);
104
384
 
105
- // TODO: order correctly & validate type according to injected typescript typedefs (command.run.params)
106
- const argsList = Object.values(args);
385
+ return await command.validateAndExecute();
386
+ } catch (error) {
387
+ if (error instanceof UsageError) throw new InputError(error.message);
107
388
 
108
- const result = await command.run(...(argsList as Parameters<NonNullable<typeof command.run>>));
389
+ if (error instanceof Error && ['UnknownSyntaxError', 'AmbiguousSyntaxError'].includes(error.name)) {
390
+ throw new NotFound(error.message);
391
+ }
109
392
 
110
- return result;
393
+ throw error;
394
+ }
111
395
  }
112
396
  }
@@ -122,7 +122,7 @@ export default class ConfigParser {
122
122
  public env(): TEnvConfig {
123
123
  // We assume that when we run 5htp dev, we're in local
124
124
  // Otherwise, we're in production environment (docker)
125
- console.log('[app] Using environment:', process.env.NODE_ENV);
125
+ debug && console.info('[app] Using environment:', process.env.NODE_ENV);
126
126
  const envFileName = this.appDir + '/env.yaml';
127
127
  const envFile = this.loadYaml(envFileName);
128
128
  const routerPortOverride = getRouterPortOverride();
@@ -38,8 +38,8 @@ export class ApplicationContainer<TServicesIndex extends StartedServicesIndex =
38
38
  public: path.join(process.cwd(), '/public'),
39
39
  var: path.join(process.cwd(), '/var'),
40
40
 
41
- client: { generated: path.join(process.cwd(), 'src', 'client', '.generated') },
42
- server: { generated: path.join(process.cwd(), 'src', 'server', '.generated') },
41
+ client: { generated: path.join(process.cwd(), '.proteum', 'client') },
42
+ server: { generated: path.join(process.cwd(), '.proteum', 'server') },
43
43
  };
44
44
 
45
45
  /*----------------------------------
@@ -8,6 +8,7 @@ import zod from 'zod';
8
8
  // Core
9
9
  import context from '@server/context';
10
10
  import type { Application } from '../index';
11
+ import type { TServiceModelsClient } from '../service';
11
12
  import type { TRouterContext, TAnyRouter } from '@server/services/router';
12
13
  import {
13
14
  toValidationSchema,
@@ -22,7 +23,15 @@ export type { z } from '@server/services/router/request/validation/zod';
22
23
  - TYPES
23
24
  ----------------------------------*/
24
25
 
25
- type TControllerContext = TRouterContext<TAnyRouter>;
26
+ type TControllerRouter<TApplication extends Application = Application> = TApplication extends { Router: infer TRouter }
27
+ ? TRouter extends TAnyRouter
28
+ ? TRouter
29
+ : TAnyRouter
30
+ : TAnyRouter;
31
+
32
+ export type TControllerRequestContext<TApplication extends Application = Application> = TRouterContext<
33
+ TControllerRouter<TApplication>
34
+ >;
26
35
 
27
36
  /*----------------------------------
28
37
  - CLASS
@@ -30,7 +39,7 @@ type TControllerContext = TRouterContext<TAnyRouter>;
30
39
 
31
40
  export default abstract class Controller<
32
41
  TApplication extends Application = Application,
33
- TContext extends TControllerContext = TControllerContext,
42
+ TContext extends TControllerRequestContext<TApplication> = TControllerRequestContext<TApplication>,
34
43
  > {
35
44
  public constructor(public request: TContext) {}
36
45
 
@@ -42,9 +51,9 @@ export default abstract class Controller<
42
51
  return this.app;
43
52
  }
44
53
 
45
- public get models() {
54
+ public get models(): TServiceModelsClient<TApplication> {
46
55
  const app = this.app as { models?: { client?: unknown }; Models?: { client?: unknown } };
47
- return app.models?.client ?? app.Models?.client;
56
+ return (app.models?.client ?? app.Models?.client) as TServiceModelsClient<TApplication>;
48
57
  }
49
58
 
50
59
  public input<TSchema extends TValidationSchema>(schema: TSchema): zod.output<TSchema>;
@@ -14,6 +14,7 @@ import { Anomaly } from '@common/errors';
14
14
  import { TBasicUser } from '@server/services/auth';
15
15
 
16
16
  export { default as Services } from './service/container';
17
+ export type { ServiceConfig } from './service/container';
17
18
  export type { TEnvConfig as Environment } from './container/config';
18
19
 
19
20
  /*----------------------------------
@@ -34,6 +35,17 @@ export const Service = ServicesContainer;
34
35
  type Prettify<T> = { [K in keyof T]: T[K] } & {};
35
36
 
36
37
  export type ApplicationProperties = Prettify<keyof Application>;
38
+ export type RootServicesOf<TApplication extends Application = Application> = Prettify<{
39
+ [TKey in Exclude<keyof TApplication, ApplicationProperties> as TApplication[TKey] extends AnyService ? TKey : never]: TApplication[TKey];
40
+ }>;
41
+
42
+ const isServiceInstance = (value: unknown): value is AnyService => {
43
+ if (!value || typeof value !== 'object') return false;
44
+
45
+ const service = value as Partial<AnyService>;
46
+
47
+ return typeof service.runHook === 'function' && typeof service.getServiceInstance === 'function' && service.status !== undefined;
48
+ };
37
49
 
38
50
  /*----------------------------------
39
51
  - FUNCTIONS
@@ -68,8 +80,6 @@ export abstract class Application<
68
80
  public debug: boolean = false;
69
81
  public launched: boolean = false;
70
82
 
71
- protected abstract registered: { [serviceId: string]: { name: string; start: () => AnyService } };
72
-
73
83
  /*----------------------------------
74
84
  - INIT
75
85
  ----------------------------------*/
@@ -120,14 +130,8 @@ export abstract class Application<
120
130
  console.log('Core version', CORE_VERSION);
121
131
  const startTime = Date.now();
122
132
 
123
- this.startServices();
124
-
125
- console.log('----------------------------------');
126
- console.log('- SERVICES');
127
- console.log('----------------------------------');
128
133
  const startingServices = await this.ready();
129
134
  await Promise.all(startingServices);
130
- console.log('All services are ready');
131
135
  await this.runHook('ready');
132
136
 
133
137
  const startedTime = (Date.now() - startTime) / 1000;
@@ -139,65 +143,77 @@ export abstract class Application<
139
143
  - ERROR HANDLING
140
144
  ----------------------------------*/
141
145
 
142
- private startServices() {
143
- // Satrt services
144
- for (const serviceId in this.registered) {
145
- try {
146
- const service = this.registered[serviceId];
147
- const instance = service.start();
148
- (this as Record<string, unknown>)[service.name] = instance.getServiceInstance();
149
- } catch (error) {
150
- console.error('Error while starting service', serviceId, error);
151
- throw error;
152
- }
146
+ private listRootServices(): Array<[string, AnyService]> {
147
+ return Object.keys(this)
148
+ .map((serviceName) => [serviceName, (this as Record<string, unknown>)[serviceName]] as const)
149
+ .filter(
150
+ ([, service]) =>
151
+ isServiceInstance(service) &&
152
+ service !== this &&
153
+ service.parent === this &&
154
+ service.app === this,
155
+ )
156
+ .map(([serviceName, service]) => [serviceName, service as AnyService]);
157
+ }
158
+
159
+ public getRootServices(): RootServicesOf<this> {
160
+ const services: Record<string, AnyService> = {};
161
+
162
+ for (const [serviceName, service] of this.listRootServices()) {
163
+ services[serviceName] = service;
153
164
  }
165
+
166
+ return services as RootServicesOf<this>;
167
+ }
168
+
169
+ public findService(serviceId: string): AnyService | undefined {
170
+ const rootServices = this.getRootServices() as Record<string, AnyService>;
171
+ const directService = rootServices[serviceId];
172
+ if (directService) return directService;
173
+
174
+ const serviceName = serviceId.split('/').pop();
175
+ if (!serviceName) return undefined;
176
+
177
+ return rootServices[serviceName];
154
178
  }
155
179
 
156
180
  public register(service: AnyService) {
157
- return service.ready();
181
+ return (service as AnyService & { ready: () => Promise<any> }).ready();
158
182
  }
159
183
 
160
184
  public async ready() {
161
185
  const startingServices: Promise<any>[] = [];
162
186
 
163
- // Print services
164
- const processService = async (propKey: string, service: AnyService, level: number = 0) => {
187
+ const processService = async (_propKey: string, service: AnyService) => {
165
188
  if (service.status !== 'starting') return;
166
189
 
167
190
  // Services start shouldn't block app boot
168
191
  // use await ServiceName.started to make services depends on each other
169
- service.starting = service.ready();
192
+ service.starting = (service as AnyService & { ready: () => Promise<any> }).ready();
170
193
  startingServices.push(service.starting);
171
194
  service.status = 'running';
172
- console.log('-' + '-'.repeat(level * 1), propKey + ': ' + service.constructor.name);
173
195
 
174
196
  // Subservices
175
197
  for (const propKey in service) {
176
- if (propKey === 'app') continue;
177
- const propValue = (service as Record<string, any>)[propKey];
198
+ if (propKey === 'app' || propKey === 'parent') continue;
199
+ const propValue = (service as Record<string, any>)[propKey];
178
200
 
179
201
  // Check if service
180
- const isService =
181
- typeof propValue === 'object' &&
182
- !(propValue instanceof Application) &&
183
- propValue !== null &&
184
- propValue.status !== undefined;
185
- if (!isService) continue;
202
+ if (!isServiceInstance(propValue) || propValue instanceof Application) continue;
186
203
 
187
204
  // Services start shouldn't block app boot
188
- processService(propKey, propValue, level + 1);
205
+ processService(propKey, propValue);
189
206
  }
190
207
  };
191
208
 
192
- for (const serviceId in this.registered) {
193
- const registeredService = this.registered[serviceId];
194
- const service = (this as Record<string, any>)[registeredService.name];
209
+ for (const [serviceName, service] of this.listRootServices()) {
210
+ const rootService = service as AnyService;
195
211
 
196
212
  // TODO: move to router
197
213
  // Application.on('service.ready')
198
214
 
199
215
  // Services start shouldn't block app boot
200
- processService(serviceId, service);
216
+ processService(serviceName, rootService);
201
217
  }
202
218
 
203
219
  return startingServices;