proteum 2.1.0-5 → 2.1.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/AGENTS.md +37 -49
- package/README.md +52 -1
- package/agents/framework/AGENTS.md +104 -236
- package/agents/project/AGENTS.md +36 -70
- package/cli/commands/command.ts +243 -0
- package/cli/commands/commandLocalRunner.js +198 -0
- package/cli/commands/dev.ts +95 -1
- package/cli/commands/doctor.ts +8 -74
- package/cli/commands/explain.ts +8 -194
- package/cli/commands/trace.ts +8 -0
- package/cli/compiler/artifacts/commands.ts +217 -0
- package/cli/compiler/artifacts/manifest.ts +17 -2
- package/cli/compiler/artifacts/services.ts +291 -0
- package/cli/compiler/client/index.ts +13 -0
- package/cli/compiler/common/commands.ts +175 -0
- package/cli/compiler/common/proteumManifest.ts +15 -124
- package/cli/compiler/index.ts +25 -2
- package/cli/compiler/server/index.ts +3 -0
- package/cli/presentation/commands.ts +37 -5
- package/cli/runtime/commands.ts +29 -1
- package/cli/tsconfig.json +4 -1
- package/cli/utils/check.ts +1 -1
- package/client/app/component.tsx +11 -0
- package/client/dev/profiler/index.tsx +1511 -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 +25 -0
- 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 +91 -0
- package/common/dev/proteumManifest.ts +135 -0
- package/common/dev/requestTrace.ts +28 -1
- package/docs/dev-commands.md +86 -0
- package/docs/request-tracing.md +2 -0
- package/package.json +1 -2
- package/server/app/commands.ts +35 -370
- package/server/app/commandsManager.ts +393 -0
- package/server/app/container/console/index.ts +0 -2
- package/server/app/container/trace/index.ts +88 -8
- package/server/app/devCommands.ts +192 -0
- package/server/app/devDiagnostics.ts +53 -0
- package/server/app/index.ts +27 -4
- 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 +1 -1
- package/server/services/router/http/index.ts +132 -21
- package/server/services/router/index.ts +40 -4
- package/server/services/router/request/api.ts +30 -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
|
@@ -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
|
+
};
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import { reservedRouteSetupKeys, routeSetupOptionKeys } from '../../../common/router/pageSetup';
|
|
9
9
|
import {
|
|
10
10
|
TProteumManifest,
|
|
11
|
+
TProteumManifestCommand,
|
|
11
12
|
TProteumManifestController,
|
|
12
13
|
TProteumManifestDiagnostic,
|
|
13
14
|
TProteumManifestLayout,
|
|
@@ -17,9 +18,11 @@ import { writeProteumManifest } from '../common/proteumManifest';
|
|
|
17
18
|
import { normalizeAbsolutePath, normalizePath } from './shared';
|
|
18
19
|
|
|
19
20
|
const collectManifestDiagnostics = ({
|
|
21
|
+
commands,
|
|
20
22
|
controllers,
|
|
21
23
|
routes,
|
|
22
24
|
}: {
|
|
25
|
+
commands: TProteumManifestCommand[];
|
|
23
26
|
controllers: TProteumManifestController[];
|
|
24
27
|
routes: TProteumManifest['routes'];
|
|
25
28
|
}) => {
|
|
@@ -173,6 +176,15 @@ const collectManifestDiagnostics = ({
|
|
|
173
176
|
.join(', ')}.`,
|
|
174
177
|
});
|
|
175
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
|
+
|
|
176
188
|
const postServerRoutesByPath = new Map(
|
|
177
189
|
routes.server
|
|
178
190
|
.filter((route) => route.methodName === 'post' && !!route.path)
|
|
@@ -207,18 +219,20 @@ const collectManifestDiagnostics = ({
|
|
|
207
219
|
export const writeCurrentProteumManifest = ({
|
|
208
220
|
services,
|
|
209
221
|
controllers,
|
|
222
|
+
commands,
|
|
210
223
|
routes,
|
|
211
224
|
layouts,
|
|
212
225
|
}: {
|
|
213
226
|
services: TProteumManifest['services'];
|
|
214
227
|
controllers: TProteumManifestController[];
|
|
228
|
+
commands: TProteumManifestCommand[];
|
|
215
229
|
routes: TProteumManifest['routes'];
|
|
216
230
|
layouts: TProteumManifestLayout[];
|
|
217
231
|
}) => {
|
|
218
232
|
const envInspection = inspectProteumEnv(app.paths.root);
|
|
219
233
|
|
|
220
234
|
const manifest: TProteumManifest = {
|
|
221
|
-
version:
|
|
235
|
+
version: 2,
|
|
222
236
|
app: {
|
|
223
237
|
root: normalizeAbsolutePath(app.paths.root),
|
|
224
238
|
coreRoot: normalizeAbsolutePath(cli.paths.core.root),
|
|
@@ -257,9 +271,10 @@ export const writeCurrentProteumManifest = ({
|
|
|
257
271
|
},
|
|
258
272
|
services,
|
|
259
273
|
controllers,
|
|
274
|
+
commands,
|
|
260
275
|
routes,
|
|
261
276
|
layouts,
|
|
262
|
-
diagnostics: collectManifestDiagnostics({ controllers, routes }),
|
|
277
|
+
diagnostics: collectManifestDiagnostics({ commands, controllers, routes }),
|
|
263
278
|
};
|
|
264
279
|
|
|
265
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'),
|
|
@@ -419,6 +673,43 @@ declare module '@models/types' {
|
|
|
419
673
|
|
|
420
674
|
fs.removeSync(path.join(app.paths.server.generated, 'app.ts'));
|
|
421
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
|
+
|
|
422
713
|
writeIfChanged(
|
|
423
714
|
path.join(app.paths.server.generated, 'models.ts'),
|
|
424
715
|
`export * from '@/var/prisma/client';
|
|
@@ -221,6 +221,19 @@ export default function createCompiler(
|
|
|
221
221
|
plugins: [
|
|
222
222
|
...(commonConfig.plugins || []),
|
|
223
223
|
|
|
224
|
+
...(dev
|
|
225
|
+
? []
|
|
226
|
+
: [
|
|
227
|
+
new rspack.NormalModuleReplacementPlugin(
|
|
228
|
+
/^@client\/dev\/profiler$/,
|
|
229
|
+
cli.paths.core.root + '/client/dev/profiler/noop.tsx',
|
|
230
|
+
),
|
|
231
|
+
new rspack.NormalModuleReplacementPlugin(
|
|
232
|
+
/^@client\/dev\/profiler\/runtime$/,
|
|
233
|
+
cli.paths.core.root + '/client/dev/profiler/runtime.noop.ts',
|
|
234
|
+
),
|
|
235
|
+
]),
|
|
236
|
+
|
|
224
237
|
// Extract CSS in dev too so SSR emits the same stylesheet links as production.
|
|
225
238
|
new rspack.CssExtractRspackPlugin({}),
|
|
226
239
|
|