proteum 2.1.0 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/AGENTS.md +44 -98
  2. package/README.md +143 -10
  3. package/agents/framework/AGENTS.md +146 -886
  4. package/agents/project/AGENTS.md +73 -127
  5. package/agents/project/client/AGENTS.md +22 -93
  6. package/agents/project/client/pages/AGENTS.md +24 -26
  7. package/agents/project/server/routes/AGENTS.md +10 -8
  8. package/agents/project/server/services/AGENTS.md +22 -159
  9. package/agents/project/tests/AGENTS.md +11 -8
  10. package/cli/app/config.ts +7 -20
  11. package/cli/bin.js +8 -0
  12. package/cli/commands/command.ts +243 -0
  13. package/cli/commands/commandLocalRunner.js +198 -0
  14. package/cli/commands/create.ts +5 -0
  15. package/cli/commands/deploy/web.ts +1 -2
  16. package/cli/commands/dev.ts +98 -2
  17. package/cli/commands/doctor.ts +8 -74
  18. package/cli/commands/explain.ts +8 -186
  19. package/cli/commands/init.ts +2 -94
  20. package/cli/commands/trace.ts +228 -0
  21. package/cli/compiler/artifacts/commands.ts +217 -0
  22. package/cli/compiler/artifacts/manifest.ts +35 -21
  23. package/cli/compiler/artifacts/services.ts +300 -1
  24. package/cli/compiler/client/index.ts +43 -8
  25. package/cli/compiler/common/commands.ts +175 -0
  26. package/cli/compiler/common/index.ts +1 -1
  27. package/cli/compiler/common/proteumManifest.ts +15 -114
  28. package/cli/compiler/index.ts +25 -2
  29. package/cli/compiler/server/index.ts +31 -6
  30. package/cli/index.ts +1 -4
  31. package/cli/paths.ts +16 -1
  32. package/cli/presentation/commands.ts +104 -14
  33. package/cli/presentation/devSession.ts +22 -3
  34. package/cli/presentation/proteum_logo_400x400_square_icon.txt +400 -0
  35. package/cli/runtime/commands.ts +121 -4
  36. package/cli/scaffold/index.ts +720 -0
  37. package/cli/scaffold/templates.ts +344 -0
  38. package/cli/scaffold/types.ts +26 -0
  39. package/cli/tsconfig.json +4 -1
  40. package/cli/utils/check.ts +1 -1
  41. package/client/app/component.tsx +13 -9
  42. package/client/dev/profiler/index.tsx +2511 -0
  43. package/client/dev/profiler/noop.tsx +5 -0
  44. package/client/dev/profiler/runtime.noop.ts +116 -0
  45. package/client/dev/profiler/runtime.ts +840 -0
  46. package/client/services/router/components/router.tsx +30 -2
  47. package/client/services/router/index.tsx +27 -3
  48. package/client/services/router/request/api.ts +133 -17
  49. package/commands/proteum/diagnostics.ts +11 -0
  50. package/common/dev/commands.ts +50 -0
  51. package/common/dev/diagnostics.ts +298 -0
  52. package/common/dev/profiler.ts +92 -0
  53. package/common/dev/proteumManifest.ts +135 -0
  54. package/common/dev/requestTrace.ts +115 -0
  55. package/common/env/proteumEnv.ts +284 -0
  56. package/common/router/index.ts +4 -22
  57. package/docs/dev-commands.md +93 -0
  58. package/docs/diagnostics.md +88 -0
  59. package/docs/request-tracing.md +132 -0
  60. package/eslint.js +11 -6
  61. package/package.json +3 -3
  62. package/server/app/commands.ts +35 -370
  63. package/server/app/commandsManager.ts +393 -0
  64. package/server/app/container/config.ts +11 -49
  65. package/server/app/container/console/index.ts +2 -3
  66. package/server/app/container/index.ts +5 -2
  67. package/server/app/container/trace/index.ts +364 -0
  68. package/server/app/devCommands.ts +192 -0
  69. package/server/app/devDiagnostics.ts +53 -0
  70. package/server/app/index.ts +29 -6
  71. package/server/index.ts +0 -1
  72. package/server/services/auth/index.ts +525 -61
  73. package/server/services/auth/router/index.ts +106 -7
  74. package/server/services/cron/CronTask.ts +73 -5
  75. package/server/services/cron/index.ts +34 -11
  76. package/server/services/fetch/index.ts +3 -10
  77. package/server/services/prisma/index.ts +66 -4
  78. package/server/services/router/http/index.ts +173 -6
  79. package/server/services/router/index.ts +200 -12
  80. package/server/services/router/request/api.ts +30 -1
  81. package/server/services/router/response/index.ts +83 -10
  82. package/server/services/router/response/page/document.tsx +16 -0
  83. package/server/services/router/response/page/index.tsx +27 -1
  84. package/skills/clean-project-code/SKILL.md +7 -2
  85. package/test-results/.last-run.json +4 -0
  86. package/types/aliases.d.ts +6 -0
  87. package/types/global/utils.d.ts +7 -14
  88. package/Rte.zip +0 -0
  89. package/agents/project/agents.md.zip +0 -0
  90. package/doc/TODO.md +0 -71
  91. package/doc/front/router.md +0 -27
  92. package/doc/workspace/workspace.png +0 -0
  93. package/doc/workspace/workspace2.png +0 -0
  94. package/doc/workspace/workspace_26.01.22.png +0 -0
  95. package/server/services/router/http/session.ts.old +0 -40
@@ -0,0 +1,217 @@
1
+ import path from 'path';
2
+ import fs from 'fs-extra';
3
+ import ts from 'typescript';
4
+
5
+ import app from '../../app';
6
+ import cli from '../..';
7
+ import { indexCommands } from '../common/commands';
8
+ import { TProteumManifestCommand } from '../common/proteumManifest';
9
+ import writeIfChanged from '../writeIfChanged';
10
+ import { normalizeAbsolutePath } from './shared';
11
+
12
+ const readServerTsconfigPaths = () => {
13
+ const serverTsconfigFilepath = path.join(app.paths.root, 'server', 'tsconfig.json');
14
+ const parsed = ts.readConfigFile(serverTsconfigFilepath, ts.sys.readFile);
15
+
16
+ if (parsed.error) {
17
+ throw new Error(`Unable to read ${serverTsconfigFilepath}: ${parsed.error.messageText}`);
18
+ }
19
+
20
+ const compilerOptions = (parsed.config?.compilerOptions || {}) as { paths?: Record<string, string[]> };
21
+
22
+ return compilerOptions.paths || {};
23
+ };
24
+
25
+ const createCommandsTsconfigContent = () =>
26
+ `${JSON.stringify(
27
+ {
28
+ extends: '../server/tsconfig.json',
29
+ compilerOptions: {
30
+ baseUrl: '..',
31
+ rootDir: '..',
32
+ paths: {
33
+ ...readServerTsconfigPaths(),
34
+ '@/server/index': ['./.proteum/server/commands.app.d.ts'],
35
+ '@models/types': ['./.proteum/server/models.ts'],
36
+ },
37
+ },
38
+ include: ['.', '../var/typings', '../node_modules/proteum/types/global', '../.proteum/server/commands.d.ts'],
39
+ },
40
+ null,
41
+ 4,
42
+ )}
43
+ `;
44
+
45
+ const legacyCommandsTsconfigContent = `{
46
+ "extends": "../server/tsconfig.json",
47
+ "include": [
48
+ ".",
49
+ "../var/typings",
50
+ "../node_modules/proteum/types/global",
51
+ "../.proteum/server/services.d.ts",
52
+ "../.proteum/server/commands.ts",
53
+ "../server/index.ts"
54
+ ]
55
+ }
56
+ `;
57
+
58
+ const transitionalCommandsTsconfigContent = `{
59
+ "extends": "../server/tsconfig.json",
60
+ "include": [
61
+ ".",
62
+ "../var/typings",
63
+ "../node_modules/proteum/types/global",
64
+ "../.proteum/server/services.d.ts",
65
+ "../server/index.ts"
66
+ ]
67
+ }
68
+ `;
69
+
70
+ const commandsOnlyTsconfigContent = `{
71
+ "extends": "../server/tsconfig.json",
72
+ "include": [
73
+ ".",
74
+ "../var/typings",
75
+ "../node_modules/proteum/types/global",
76
+ "../.proteum/server/commands.d.ts"
77
+ ]
78
+ }
79
+ `;
80
+
81
+ const commandsAliasesTsconfigContent = `{
82
+ "extends": "../server/tsconfig.json",
83
+ "compilerOptions": {
84
+ "baseUrl": "..",
85
+ "rootDir": "..",
86
+ "paths": {
87
+ "@/server/index": ["./.proteum/server/commands.app.d.ts"]
88
+ }
89
+ },
90
+ "include": [
91
+ ".",
92
+ "../var/typings",
93
+ "../node_modules/proteum/types/global",
94
+ "../.proteum/server/commands.d.ts"
95
+ ]
96
+ }
97
+ `;
98
+
99
+ const isManagedCommandsTsconfig = (content: string) => {
100
+ try {
101
+ const parsed = JSON.parse(content) as {
102
+ extends?: string;
103
+ include?: string[];
104
+ compilerOptions?: { baseUrl?: string; rootDir?: string };
105
+ };
106
+
107
+ if (parsed.extends !== '../server/tsconfig.json') return false;
108
+ if (JSON.stringify(parsed.include || []) !== JSON.stringify(['.', '../var/typings', '../node_modules/proteum/types/global', '../.proteum/server/commands.d.ts']))
109
+ return false;
110
+
111
+ if (parsed.compilerOptions?.baseUrl !== undefined && parsed.compilerOptions.baseUrl !== '..') return false;
112
+ if (parsed.compilerOptions?.rootDir !== undefined && parsed.compilerOptions.rootDir !== '..') return false;
113
+
114
+ return true;
115
+ } catch {
116
+ return false;
117
+ }
118
+ };
119
+
120
+ const ensureCommandsTsconfig = () => {
121
+ const commandsRoot = path.join(app.paths.root, 'commands');
122
+ const commandsTsconfigFilepath = path.join(commandsRoot, 'tsconfig.json');
123
+ const nextContent = createCommandsTsconfigContent();
124
+
125
+ if (!fs.existsSync(commandsRoot)) return;
126
+
127
+ if (!fs.existsSync(commandsTsconfigFilepath)) {
128
+ writeIfChanged(commandsTsconfigFilepath, nextContent);
129
+ return;
130
+ }
131
+
132
+ const currentContent = fs.readFileSync(commandsTsconfigFilepath, 'utf8');
133
+ const generatedContents = new Set([
134
+ createCommandsTsconfigContent(),
135
+ legacyCommandsTsconfigContent,
136
+ transitionalCommandsTsconfigContent,
137
+ commandsOnlyTsconfigContent,
138
+ commandsAliasesTsconfigContent,
139
+ ]);
140
+
141
+ if (!generatedContents.has(currentContent) && !isManagedCommandsTsconfig(currentContent)) return;
142
+
143
+ writeIfChanged(commandsTsconfigFilepath, nextContent);
144
+ };
145
+
146
+ export const generateCommandArtifacts = () => {
147
+ ensureCommandsTsconfig();
148
+
149
+ const frameworkCommandsRoot = normalizeAbsolutePath(path.join(cli.paths.core.root, 'commands'));
150
+ const commands = indexCommands([
151
+ { importPrefix: `${frameworkCommandsRoot}/`, root: path.join(cli.paths.core.root, 'commands') },
152
+ { importPrefix: '@/commands/', root: path.join(app.paths.root, 'commands') },
153
+ ]);
154
+
155
+ const getManifestScopeFromImportPath = (importPath: string) =>
156
+ importPath.startsWith(`${frameworkCommandsRoot}/`) ? 'framework' : 'app';
157
+
158
+ const manifestCommands = commands.flatMap<TProteumManifestCommand>((command) =>
159
+ command.methods.map((method) => ({
160
+ className: command.className,
161
+ importPath: command.importPath,
162
+ filepath: normalizeAbsolutePath(command.filepath),
163
+ sourceLocation: method.sourceLocation,
164
+ commandBasePath: command.commandBasePath,
165
+ methodName: method.name,
166
+ path: method.path,
167
+ scope: getManifestScopeFromImportPath(command.importPath),
168
+ })),
169
+ );
170
+
171
+ const commandImports = commands
172
+ .map((command, index) => `import Command${index} from ${JSON.stringify(command.importPath)};`)
173
+ .join('\n');
174
+
175
+ const commandEntries = commands.flatMap((command, commandIndex) =>
176
+ command.methods.map(
177
+ (method) => ` {
178
+ path: ${JSON.stringify(method.path)},
179
+ className: ${JSON.stringify(command.className)},
180
+ importPath: ${JSON.stringify(command.importPath)},
181
+ filepath: ${JSON.stringify(normalizeAbsolutePath(command.filepath))},
182
+ sourceLocation: { line: ${method.sourceLocation.line}, column: ${method.sourceLocation.column} },
183
+ scope: ${JSON.stringify(getManifestScopeFromImportPath(command.importPath))},
184
+ Command: Command${commandIndex},
185
+ methodName: ${JSON.stringify(method.name)},
186
+ },`,
187
+ ),
188
+ );
189
+
190
+ writeIfChanged(
191
+ path.join(app.paths.server.generated, 'commands.ts'),
192
+ `/*----------------------------------
193
+ - GENERATED FILE
194
+ ----------------------------------*/
195
+
196
+ // This file is generated by Proteum from command files.
197
+ // Do not edit it manually.
198
+
199
+ import type { Commands } from '@server/app/commands';
200
+ import type { TDevCommandDefinition } from '@common/dev/commands';
201
+ ${commandImports ? '\n' + commandImports : ''}
202
+
203
+ export type TGeneratedCommandDefinition = TDevCommandDefinition & {
204
+ Command: new (app: any) => Commands<any>,
205
+ methodName: string,
206
+ }
207
+
208
+ const commands: TGeneratedCommandDefinition[] = [
209
+ ${commandEntries.join('\n')}
210
+ ];
211
+
212
+ export default commands;
213
+ `,
214
+ );
215
+
216
+ return manifestCommands;
217
+ };
@@ -1,12 +1,14 @@
1
1
  import path from 'path';
2
- import fs from 'fs-extra';
3
- import yaml from 'yaml';
4
2
 
5
3
  import app from '../../app';
6
4
  import cli from '../..';
5
+ import {
6
+ inspectProteumEnv,
7
+ } from '../../../common/env/proteumEnv';
7
8
  import { reservedRouteSetupKeys, routeSetupOptionKeys } from '../../../common/router/pageSetup';
8
9
  import {
9
10
  TProteumManifest,
11
+ TProteumManifestCommand,
10
12
  TProteumManifestController,
11
13
  TProteumManifestDiagnostic,
12
14
  TProteumManifestLayout,
@@ -15,24 +17,12 @@ import {
15
17
  import { writeProteumManifest } from '../common/proteumManifest';
16
18
  import { normalizeAbsolutePath, normalizePath } from './shared';
17
19
 
18
- const envRequiredTopLevelKeys = ['name', 'profile', 'router', 'console'];
19
-
20
- const getEnvTopLevelKeys = () => {
21
- const envFilepath = path.join(app.paths.root, 'env.yaml');
22
-
23
- if (!fs.existsSync(envFilepath)) return [];
24
-
25
- const rawEnv = yaml.parse(fs.readFileSync(envFilepath, 'utf8'));
26
-
27
- if (!rawEnv || typeof rawEnv !== 'object' || Array.isArray(rawEnv)) return [];
28
-
29
- return Object.keys(rawEnv).sort((a, b) => a.localeCompare(b));
30
- };
31
-
32
20
  const collectManifestDiagnostics = ({
21
+ commands,
33
22
  controllers,
34
23
  routes,
35
24
  }: {
25
+ commands: TProteumManifestCommand[];
36
26
  controllers: TProteumManifestController[];
37
27
  routes: TProteumManifest['routes'];
38
28
  }) => {
@@ -186,6 +176,15 @@ const collectManifestDiagnostics = ({
186
176
  .join(', ')}.`,
187
177
  });
188
178
 
179
+ trackDuplicates(commands, (command) => command.path, {
180
+ code: 'command.duplicate-path',
181
+ level: 'error',
182
+ message: (command, others) =>
183
+ `Duplicate command path "${command.path}" also registered in ${others
184
+ .map((other) => normalizePath(path.relative(app.paths.root, other.filepath)))
185
+ .join(', ')}.`,
186
+ });
187
+
189
188
  const postServerRoutesByPath = new Map(
190
189
  routes.server
191
190
  .filter((route) => route.methodName === 'post' && !!route.path)
@@ -220,16 +219,20 @@ const collectManifestDiagnostics = ({
220
219
  export const writeCurrentProteumManifest = ({
221
220
  services,
222
221
  controllers,
222
+ commands,
223
223
  routes,
224
224
  layouts,
225
225
  }: {
226
226
  services: TProteumManifest['services'];
227
227
  controllers: TProteumManifestController[];
228
+ commands: TProteumManifestCommand[];
228
229
  routes: TProteumManifest['routes'];
229
230
  layouts: TProteumManifestLayout[];
230
231
  }) => {
232
+ const envInspection = inspectProteumEnv(app.paths.root);
233
+
231
234
  const manifest: TProteumManifest = {
232
- version: 1,
235
+ version: 2,
233
236
  app: {
234
237
  root: normalizeAbsolutePath(app.paths.root),
235
238
  coreRoot: normalizeAbsolutePath(cli.paths.core.root),
@@ -252,15 +255,26 @@ export const writeCurrentProteumManifest = ({
252
255
  reservedRouteSetupKeys: [...reservedRouteSetupKeys],
253
256
  },
254
257
  env: {
255
- sourceFilepath: normalizeAbsolutePath(path.join(app.paths.root, 'env.yaml')),
256
- loadedTopLevelKeys: getEnvTopLevelKeys(),
257
- requiredTopLevelKeys: [...envRequiredTopLevelKeys],
258
+ source: 'process.env',
259
+ loadedVariableKeys: envInspection.loadedVariableKeys,
260
+ requiredVariables: envInspection.requiredVariables.map((variable) => ({
261
+ key: variable.key,
262
+ possibleValues: [...variable.possibleValues],
263
+ provided: variable.provided,
264
+ })),
265
+ resolved: {
266
+ name: app.env.name,
267
+ profile: app.env.profile,
268
+ routerPort: app.env.router.port,
269
+ routerCurrentDomain: app.env.router.currentDomain,
270
+ },
258
271
  },
259
272
  services,
260
273
  controllers,
274
+ commands,
261
275
  routes,
262
276
  layouts,
263
- diagnostics: collectManifestDiagnostics({ controllers, routes }),
277
+ diagnostics: collectManifestDiagnostics({ commands, controllers, routes }),
264
278
  };
265
279
 
266
280
  writeProteumManifest(app.paths.root, manifest);
@@ -33,6 +33,14 @@ type TParsedAppBootstrap = {
33
33
  };
34
34
 
35
35
  type TServicesAvailable = Record<string, TServiceMetas>;
36
+ type TCommandServiceStubSource = {
37
+ aliasImportPath: string;
38
+ filepath: string;
39
+ };
40
+ type TGeneratedCommandServiceStubs = {
41
+ declarations: string;
42
+ typeNamesByAliasImportPath: Map<string, string>;
43
+ };
36
44
 
37
45
  const buildServicesAvailable = (): TServicesAvailable => {
38
46
  const searchDirs = [
@@ -309,6 +317,251 @@ const parseAppBootstrap = (servicesAvailable: TServicesAvailable): TParsedAppBoo
309
317
  return { rootServices, routerPlugins };
310
318
  };
311
319
 
320
+ const commandServiceSearchRoots = [
321
+ { root: normalizeAbsolutePath(path.join(cli.paths.core.root, 'server', 'services')), prefix: '@server/services/' },
322
+ { root: normalizeAbsolutePath(path.join(app.paths.root, 'server', 'services')), prefix: '@/server/services/' },
323
+ ];
324
+
325
+ const resolveExistingModuleFilepath = (importPath: string) => {
326
+ const candidates = [importPath, `${importPath}.ts`, `${importPath}.tsx`, path.join(importPath, 'index.ts'), path.join(importPath, 'index.tsx')];
327
+
328
+ for (const candidate of candidates) {
329
+ if (!fs.existsSync(candidate)) continue;
330
+ if (!fs.statSync(candidate).isFile()) continue;
331
+
332
+ return normalizeAbsolutePath(candidate);
333
+ }
334
+
335
+ return undefined;
336
+ };
337
+
338
+ const getCommandServiceAliasFromFilepath = (filepath: string) => {
339
+ const normalizedFilepath = normalizeAbsolutePath(filepath);
340
+
341
+ for (const searchRoot of commandServiceSearchRoots) {
342
+ if (!normalizedFilepath.startsWith(searchRoot.root + '/')) continue;
343
+
344
+ let relativePath = normalizedFilepath.substring(searchRoot.root.length + 1).replace(/\.(ts|tsx)$/, '');
345
+ if (relativePath.endsWith('/index')) relativePath = relativePath.substring(0, relativePath.length - '/index'.length);
346
+
347
+ return searchRoot.prefix + relativePath;
348
+ }
349
+
350
+ return undefined;
351
+ };
352
+
353
+ const resolveCommandServiceStubSource = (
354
+ importPath: string,
355
+ sourceFilepath?: string,
356
+ ): TCommandServiceStubSource | undefined => {
357
+ if (importPath.startsWith('./') || importPath.startsWith('../')) {
358
+ if (!sourceFilepath) return undefined;
359
+
360
+ const resolvedFilepath = resolveExistingModuleFilepath(path.resolve(path.dirname(sourceFilepath), importPath));
361
+ if (!resolvedFilepath) return undefined;
362
+
363
+ const aliasImportPath = getCommandServiceAliasFromFilepath(resolvedFilepath);
364
+ if (!aliasImportPath) return undefined;
365
+
366
+ return { aliasImportPath, filepath: resolvedFilepath };
367
+ }
368
+
369
+ const searchRoot = commandServiceSearchRoots.find((entry) => importPath.startsWith(entry.prefix));
370
+ if (!searchRoot) return undefined;
371
+
372
+ const relativeImportPath = importPath.substring(searchRoot.prefix.length);
373
+ const resolvedFilepath = resolveExistingModuleFilepath(path.join(searchRoot.root, relativeImportPath));
374
+ if (!resolvedFilepath) return undefined;
375
+
376
+ const aliasImportPath = getCommandServiceAliasFromFilepath(resolvedFilepath);
377
+ if (!aliasImportPath) return undefined;
378
+
379
+ return { aliasImportPath, filepath: resolvedFilepath };
380
+ };
381
+
382
+ const getCommandServiceStubTypeName = (aliasImportPath: string) =>
383
+ `ProteumCommandService_${aliasImportPath.replace(/[^A-Za-z0-9_$]+/g, '_')}`;
384
+
385
+ const isPrivateOrProtectedInstanceMember = (member: ts.ClassElement) =>
386
+ hasModifier(member, ts.SyntaxKind.PrivateKeyword) ||
387
+ hasModifier(member, ts.SyntaxKind.ProtectedKeyword) ||
388
+ hasModifier(member, ts.SyntaxKind.StaticKeyword);
389
+
390
+ const getPropertyDeclarationType = (
391
+ property: ts.PropertyDeclaration,
392
+ imports: Map<string, string>,
393
+ sourceFilepath: string,
394
+ getStubTypeName: (source: TCommandServiceStubSource) => string,
395
+ enqueueStub: (source: TCommandServiceStubSource) => void,
396
+ ) => {
397
+ const initializer = property.initializer ? unwrapExpression(property.initializer) : undefined;
398
+
399
+ if (initializer && ts.isNewExpression(initializer)) {
400
+ if (ts.isIdentifier(initializer.expression)) {
401
+ const nestedImportPath = imports.get(initializer.expression.text);
402
+ if (nestedImportPath) {
403
+ const nestedSource = resolveCommandServiceStubSource(nestedImportPath, sourceFilepath);
404
+
405
+ if (nestedSource) {
406
+ enqueueStub(nestedSource);
407
+ return getStubTypeName(nestedSource);
408
+ }
409
+ }
410
+ }
411
+ }
412
+
413
+ if (!initializer) return 'any';
414
+ if (ts.isArrayLiteralExpression(initializer)) return 'any[]';
415
+ if (ts.isObjectLiteralExpression(initializer)) return 'Record<string, any>';
416
+ if (ts.isStringLiteral(initializer) || ts.isNoSubstitutionTemplateLiteral(initializer)) return 'string';
417
+ if (ts.isNumericLiteral(initializer)) return 'number';
418
+ if (initializer.kind === ts.SyntaxKind.TrueKeyword || initializer.kind === ts.SyntaxKind.FalseKeyword) return 'boolean';
419
+ if (initializer.kind === ts.SyntaxKind.NullKeyword) return 'null';
420
+
421
+ return 'any';
422
+ };
423
+
424
+ const getCommandMethodParameter = (parameter: ts.ParameterDeclaration, index: number) => {
425
+ const parameterName = ts.isIdentifier(parameter.name) ? parameter.name.text : `arg${index}`;
426
+
427
+ if (parameter.dotDotDotToken) return `...${parameterName}: any[]`;
428
+
429
+ return `${parameterName}${parameter.questionToken || parameter.initializer ? '?' : ''}: any`;
430
+ };
431
+
432
+ const isPromiseTypeNode = (typeNode?: ts.TypeNode) =>
433
+ !!typeNode &&
434
+ ts.isTypeReferenceNode(typeNode) &&
435
+ ts.isIdentifier(typeNode.typeName) &&
436
+ typeNode.typeName.text === 'Promise';
437
+
438
+ const isArrayLikeTypeNode = (typeNode?: ts.TypeNode): boolean => {
439
+ if (!typeNode) return false;
440
+ if (ts.isArrayTypeNode(typeNode)) return true;
441
+
442
+ if (ts.isTypeReferenceNode(typeNode) && ts.isIdentifier(typeNode.typeName)) {
443
+ if (typeNode.typeName.text === 'Array' || typeNode.typeName.text === 'ReadonlyArray') return true;
444
+
445
+ if (typeNode.typeName.text === 'Promise' && typeNode.typeArguments?.[0]) {
446
+ return isArrayLikeTypeNode(typeNode.typeArguments[0]);
447
+ }
448
+ }
449
+
450
+ return false;
451
+ };
452
+
453
+ const getCommandMethodReturnType = (method: ts.MethodDeclaration) => {
454
+ const isPromise = hasModifier(method, ts.SyntaxKind.AsyncKeyword) || isPromiseTypeNode(method.type);
455
+ const containsArrayResult = isArrayLikeTypeNode(method.type);
456
+
457
+ if (isPromise && containsArrayResult) return 'Promise<any[]>';
458
+ if (isPromise) return 'Promise<any>';
459
+ if (containsArrayResult) return 'any[]';
460
+
461
+ return 'any';
462
+ };
463
+
464
+ const createCommandServiceStubDeclarations = (rootServices: TParsedService[]): TGeneratedCommandServiceStubs => {
465
+ const stubs = new Map<string, string>();
466
+ const typeNamesByAliasImportPath = new Map<string, string>();
467
+ const pendingSources: TCommandServiceStubSource[] = [];
468
+ const seenSources = new Set<string>();
469
+ const getStubTypeName = (source: TCommandServiceStubSource) => {
470
+ const existingTypeName = typeNamesByAliasImportPath.get(source.aliasImportPath);
471
+ if (existingTypeName) return existingTypeName;
472
+
473
+ const typeName = getCommandServiceStubTypeName(source.aliasImportPath);
474
+ typeNamesByAliasImportPath.set(source.aliasImportPath, typeName);
475
+
476
+ return typeName;
477
+ };
478
+ const enqueueStub = (source: TCommandServiceStubSource) => {
479
+ if (seenSources.has(source.aliasImportPath)) return;
480
+
481
+ seenSources.add(source.aliasImportPath);
482
+ pendingSources.push(source);
483
+ };
484
+
485
+ for (const rootService of rootServices) {
486
+ const source = resolveCommandServiceStubSource(rootService.meta.importationPath);
487
+ if (source) enqueueStub(source);
488
+ }
489
+
490
+ while (pendingSources.length > 0) {
491
+ const source = pendingSources.shift()!;
492
+ const sourceFile = createSourceFile(source.filepath);
493
+ const imports = buildImportIndex(sourceFile);
494
+ let defaultClass: ts.ClassDeclaration | undefined;
495
+
496
+ try {
497
+ defaultClass = getDefaultExportClassDeclaration(sourceFile);
498
+ } catch {
499
+ defaultClass = undefined;
500
+ }
501
+
502
+ if (!defaultClass) {
503
+ stubs.set(
504
+ source.aliasImportPath,
505
+ `declare class ${getStubTypeName(source)} {
506
+ app: import("@/server/index").default;
507
+ [key: string]: any;
508
+ }`,
509
+ );
510
+ continue;
511
+ }
512
+
513
+ const className = getStubTypeName(source);
514
+ const classMembers = [` app: import("@/server/index").default;`];
515
+
516
+ for (const member of defaultClass.members) {
517
+ if (isPrivateOrProtectedInstanceMember(member)) continue;
518
+
519
+ if (ts.isPropertyDeclaration(member)) {
520
+ const propertyName = getPropertyNameText(member.name);
521
+ if (!propertyName) continue;
522
+
523
+ classMembers.push(
524
+ ` ${propertyName}: ${getPropertyDeclarationType(member, imports, source.filepath, getStubTypeName, enqueueStub)};`,
525
+ );
526
+ continue;
527
+ }
528
+
529
+ if (ts.isGetAccessorDeclaration(member)) {
530
+ const propertyName = getPropertyNameText(member.name);
531
+ if (!propertyName) continue;
532
+
533
+ classMembers.push(` ${propertyName}: any;`);
534
+ continue;
535
+ }
536
+
537
+ if (ts.isMethodDeclaration(member)) {
538
+ const methodName = getPropertyNameText(member.name);
539
+ if (!methodName) continue;
540
+
541
+ const parameters = member.parameters.map((parameter, index) => getCommandMethodParameter(parameter, index)).join(', ');
542
+ const returnType = getCommandMethodReturnType(member);
543
+
544
+ classMembers.push(` ${methodName}(${parameters}): ${returnType};`);
545
+ }
546
+ }
547
+
548
+ stubs.set(
549
+ source.aliasImportPath,
550
+ `declare class ${className} {
551
+ ${classMembers.join('\n')}
552
+ }`,
553
+ );
554
+ }
555
+
556
+ return {
557
+ declarations: Array.from(stubs.entries())
558
+ .sort(([left], [right]) => left.localeCompare(right))
559
+ .map(([, declaration]) => declaration)
560
+ .join('\n\n'),
561
+ typeNamesByAliasImportPath,
562
+ };
563
+ };
564
+
312
565
  const resolveManifestService = (service: TParsedService, parent: string): TProteumManifestService => ({
313
566
  kind: 'service',
314
567
  id: service.meta.id,
@@ -329,6 +582,7 @@ export const generateServiceArtifacts = () => {
329
582
  const containerServices = app.containerServices.map((serviceName) => "'" + serviceName + "'").join('|');
330
583
  const appServices = rootServices.map((service) => resolveManifestService(service, 'app'));
331
584
  const routerPluginServices = routerPlugins.map((service) => resolveManifestService(service, 'Router.plugins'));
585
+ const commandServiceStubs = createCommandServiceStubDeclarations(rootServices);
332
586
 
333
587
  writeIfChanged(
334
588
  path.join(app.paths.client.generated, 'services.d.ts'),
@@ -390,7 +644,15 @@ import type ${appClassIdentifier}Client from '@/client/index';
390
644
 
391
645
  export type ClientContext = ${appClassIdentifier}Client["Router"]["context"];
392
646
 
393
- export const ReactClientContext = React.createContext<ClientContext>({} as ClientContext);
647
+ type GlobalClientContextStore = typeof globalThis & {
648
+ __proteumClientContexts?: Record<string, React.Context<ClientContext>>;
649
+ };
650
+
651
+ const globalClientContextStore = globalThis as GlobalClientContextStore;
652
+ const clientContexts = (globalClientContextStore.__proteumClientContexts ??= {});
653
+
654
+ export const ReactClientContext =
655
+ clientContexts['${appClassIdentifier}'] ?? (clientContexts['${appClassIdentifier}'] = React.createContext<ClientContext>({} as ClientContext));
394
656
  export default (): ClientContext => React.useContext<ClientContext>(ReactClientContext);`,
395
657
  );
396
658
 
@@ -411,6 +673,43 @@ declare module '@models/types' {
411
673
 
412
674
  fs.removeSync(path.join(app.paths.server.generated, 'app.ts'));
413
675
 
676
+ writeIfChanged(
677
+ path.join(app.paths.server.generated, 'commands.d.ts'),
678
+ `declare type ${appClassIdentifier} = import("@/server/index").default;
679
+
680
+ declare module "@models/types" {
681
+ const Models: any;
682
+ export = Models;
683
+ }
684
+
685
+ export {};
686
+ `,
687
+ );
688
+
689
+ writeIfChanged(
690
+ path.join(app.paths.server.generated, 'commands.app.d.ts'),
691
+ `${commandServiceStubs.declarations}
692
+
693
+ declare class ${appClassIdentifier} implements import("@server/app/commands").TCommandApplication {
694
+ env: import("@server/app/commands").TCommandApplication["env"];
695
+ identity: import("@server/app/commands").TCommandApplication["identity"];
696
+ getRootServices: import("@server/app/commands").TCommandApplication["getRootServices"];
697
+ findService?: import("@server/app/commands").TCommandApplication["findService"];
698
+ models?: import("@server/app/commands").TCommandApplication["models"];
699
+ Models?: import("@server/app/commands").TCommandApplication["Models"];
700
+ ${rootServices
701
+ .map((service) => {
702
+ const typeName = commandServiceStubs.typeNamesByAliasImportPath.get(service.meta.importationPath) || 'any';
703
+
704
+ return ` ${service.registeredName}: ${typeName};`;
705
+ })
706
+ .join('\n')}
707
+ }
708
+
709
+ export default ${appClassIdentifier};
710
+ `,
711
+ );
712
+
414
713
  writeIfChanged(
415
714
  path.join(app.paths.server.generated, 'models.ts'),
416
715
  `export * from '@/var/prisma/client';