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.
- package/AGENTS.md +44 -98
- package/README.md +143 -10
- package/agents/framework/AGENTS.md +146 -886
- package/agents/project/AGENTS.md +73 -127
- package/agents/project/client/AGENTS.md +22 -93
- package/agents/project/client/pages/AGENTS.md +24 -26
- package/agents/project/server/routes/AGENTS.md +10 -8
- package/agents/project/server/services/AGENTS.md +22 -159
- package/agents/project/tests/AGENTS.md +11 -8
- package/cli/app/config.ts +7 -20
- package/cli/bin.js +8 -0
- package/cli/commands/command.ts +243 -0
- package/cli/commands/commandLocalRunner.js +198 -0
- package/cli/commands/create.ts +5 -0
- package/cli/commands/deploy/web.ts +1 -2
- package/cli/commands/dev.ts +98 -2
- package/cli/commands/doctor.ts +8 -74
- package/cli/commands/explain.ts +8 -186
- package/cli/commands/init.ts +2 -94
- package/cli/commands/trace.ts +228 -0
- package/cli/compiler/artifacts/commands.ts +217 -0
- package/cli/compiler/artifacts/manifest.ts +35 -21
- package/cli/compiler/artifacts/services.ts +300 -1
- package/cli/compiler/client/index.ts +43 -8
- package/cli/compiler/common/commands.ts +175 -0
- package/cli/compiler/common/index.ts +1 -1
- package/cli/compiler/common/proteumManifest.ts +15 -114
- package/cli/compiler/index.ts +25 -2
- package/cli/compiler/server/index.ts +31 -6
- package/cli/index.ts +1 -4
- package/cli/paths.ts +16 -1
- package/cli/presentation/commands.ts +104 -14
- package/cli/presentation/devSession.ts +22 -3
- package/cli/presentation/proteum_logo_400x400_square_icon.txt +400 -0
- package/cli/runtime/commands.ts +121 -4
- package/cli/scaffold/index.ts +720 -0
- package/cli/scaffold/templates.ts +344 -0
- package/cli/scaffold/types.ts +26 -0
- package/cli/tsconfig.json +4 -1
- package/cli/utils/check.ts +1 -1
- package/client/app/component.tsx +13 -9
- package/client/dev/profiler/index.tsx +2511 -0
- package/client/dev/profiler/noop.tsx +5 -0
- package/client/dev/profiler/runtime.noop.ts +116 -0
- package/client/dev/profiler/runtime.ts +840 -0
- package/client/services/router/components/router.tsx +30 -2
- package/client/services/router/index.tsx +27 -3
- package/client/services/router/request/api.ts +133 -17
- package/commands/proteum/diagnostics.ts +11 -0
- package/common/dev/commands.ts +50 -0
- package/common/dev/diagnostics.ts +298 -0
- package/common/dev/profiler.ts +92 -0
- package/common/dev/proteumManifest.ts +135 -0
- package/common/dev/requestTrace.ts +115 -0
- package/common/env/proteumEnv.ts +284 -0
- package/common/router/index.ts +4 -22
- package/docs/dev-commands.md +93 -0
- package/docs/diagnostics.md +88 -0
- package/docs/request-tracing.md +132 -0
- package/eslint.js +11 -6
- package/package.json +3 -3
- package/server/app/commands.ts +35 -370
- package/server/app/commandsManager.ts +393 -0
- package/server/app/container/config.ts +11 -49
- package/server/app/container/console/index.ts +2 -3
- package/server/app/container/index.ts +5 -2
- package/server/app/container/trace/index.ts +364 -0
- package/server/app/devCommands.ts +192 -0
- package/server/app/devDiagnostics.ts +53 -0
- package/server/app/index.ts +29 -6
- package/server/index.ts +0 -1
- package/server/services/auth/index.ts +525 -61
- package/server/services/auth/router/index.ts +106 -7
- package/server/services/cron/CronTask.ts +73 -5
- package/server/services/cron/index.ts +34 -11
- package/server/services/fetch/index.ts +3 -10
- package/server/services/prisma/index.ts +66 -4
- package/server/services/router/http/index.ts +173 -6
- package/server/services/router/index.ts +200 -12
- package/server/services/router/request/api.ts +30 -1
- package/server/services/router/response/index.ts +83 -10
- package/server/services/router/response/page/document.tsx +16 -0
- package/server/services/router/response/page/index.tsx +27 -1
- package/skills/clean-project-code/SKILL.md +7 -2
- package/test-results/.last-run.json +4 -0
- package/types/aliases.d.ts +6 -0
- package/types/global/utils.d.ts +7 -14
- package/Rte.zip +0 -0
- package/agents/project/agents.md.zip +0 -0
- package/doc/TODO.md +0 -71
- package/doc/front/router.md +0 -27
- package/doc/workspace/workspace.png +0 -0
- package/doc/workspace/workspace2.png +0 -0
- package/doc/workspace/workspace_26.01.22.png +0 -0
- 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:
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
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';
|