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,175 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
|
|
5
|
+
export type TCommandSourceLocation = { line: number; column: number };
|
|
6
|
+
|
|
7
|
+
export type TCommandMethodMeta = {
|
|
8
|
+
name: string;
|
|
9
|
+
path: string;
|
|
10
|
+
sourceLocation: TCommandSourceLocation;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type TCommandFileMeta = {
|
|
14
|
+
importPath: string;
|
|
15
|
+
filepath: string;
|
|
16
|
+
className: string;
|
|
17
|
+
commandBasePath: string;
|
|
18
|
+
methods: TCommandMethodMeta[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type TCommandSearchDir = { importPrefix: string; root: string };
|
|
22
|
+
|
|
23
|
+
const getCommandSegments = (relativePath: string) => {
|
|
24
|
+
const segments = relativePath
|
|
25
|
+
.replace(/\.ts$/, '')
|
|
26
|
+
.split('/')
|
|
27
|
+
.filter(Boolean);
|
|
28
|
+
|
|
29
|
+
if (segments[segments.length - 1] === 'index') {
|
|
30
|
+
segments.pop();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return segments;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const getCommandBasePathFromFilepath = (filepath: string, root: string) =>
|
|
37
|
+
getCommandSegments(path.relative(root, filepath).replace(/\\/g, '/')).join('/');
|
|
38
|
+
|
|
39
|
+
const getGeneratedClassName = (filepath: string) => {
|
|
40
|
+
const filename = path.basename(filepath, '.ts').replace(/[^A-Za-z0-9_$]+/g, '_');
|
|
41
|
+
const normalized = filename.length ? filename : 'Commands';
|
|
42
|
+
|
|
43
|
+
return normalized[0].toUpperCase() + normalized.substring(1);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const buildImportPath = (searchDir: TCommandSearchDir, filepath: string) =>
|
|
47
|
+
searchDir.importPrefix + path.relative(searchDir.root, filepath).replace(/\\/g, '/').replace(/\.ts$/, '');
|
|
48
|
+
|
|
49
|
+
const findCommandFiles = (dir: string): string[] => {
|
|
50
|
+
if (!fs.existsSync(dir)) return [];
|
|
51
|
+
|
|
52
|
+
const files: string[] = [];
|
|
53
|
+
|
|
54
|
+
for (const dirent of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
55
|
+
const filepath = path.join(dir, dirent.name);
|
|
56
|
+
|
|
57
|
+
if (dirent.isDirectory()) {
|
|
58
|
+
files.push(...findCommandFiles(filepath));
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!dirent.isFile()) continue;
|
|
63
|
+
if (!dirent.name.endsWith('.ts')) continue;
|
|
64
|
+
if (dirent.name.endsWith('.d.ts')) continue;
|
|
65
|
+
|
|
66
|
+
files.push(filepath);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return files;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const parseSourceFile = (filepath: string, code: string) =>
|
|
73
|
+
ts.createSourceFile(
|
|
74
|
+
filepath,
|
|
75
|
+
code,
|
|
76
|
+
ts.ScriptTarget.Latest,
|
|
77
|
+
true,
|
|
78
|
+
filepath.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const getNodeLocation = (sourceFile: ts.SourceFile, node: ts.Node): TCommandSourceLocation => {
|
|
82
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
83
|
+
|
|
84
|
+
return { line: line + 1, column: character + 1 };
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const hasModifier = (node: ts.Node, kind: ts.SyntaxKind) =>
|
|
88
|
+
!!node.modifiers?.some((modifier) => modifier.kind === kind);
|
|
89
|
+
|
|
90
|
+
const getDefaultExportClass = (sourceFile: ts.SourceFile) => {
|
|
91
|
+
const classes = new Map<string, ts.ClassDeclaration>();
|
|
92
|
+
|
|
93
|
+
for (const statement of sourceFile.statements) {
|
|
94
|
+
if (ts.isClassDeclaration(statement) && statement.name) {
|
|
95
|
+
classes.set(statement.name.text, statement);
|
|
96
|
+
|
|
97
|
+
if (hasModifier(statement, ts.SyntaxKind.DefaultKeyword)) {
|
|
98
|
+
return statement;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
for (const statement of sourceFile.statements) {
|
|
104
|
+
if (!ts.isExportAssignment(statement) || statement.isExportEquals) continue;
|
|
105
|
+
|
|
106
|
+
if (ts.isIdentifier(statement.expression)) {
|
|
107
|
+
return classes.get(statement.expression.text);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return undefined;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const getExportedString = (sourceFile: ts.SourceFile, exportName: string) => {
|
|
115
|
+
for (const statement of sourceFile.statements) {
|
|
116
|
+
if (!ts.isVariableStatement(statement)) continue;
|
|
117
|
+
if (!hasModifier(statement, ts.SyntaxKind.ExportKeyword)) continue;
|
|
118
|
+
|
|
119
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
120
|
+
if (!ts.isIdentifier(declaration.name)) continue;
|
|
121
|
+
if (declaration.name.text !== exportName) continue;
|
|
122
|
+
if (!declaration.initializer || !ts.isStringLiteral(declaration.initializer)) continue;
|
|
123
|
+
|
|
124
|
+
return declaration.initializer.text;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return undefined;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export const indexCommands = (searchDirs: TCommandSearchDir[]) => {
|
|
132
|
+
const commands: TCommandFileMeta[] = [];
|
|
133
|
+
|
|
134
|
+
for (const searchDir of searchDirs) {
|
|
135
|
+
const commandFiles = findCommandFiles(searchDir.root);
|
|
136
|
+
|
|
137
|
+
for (const filepath of commandFiles.sort((a, b) => a.localeCompare(b))) {
|
|
138
|
+
const code = fs.readFileSync(filepath, 'utf8');
|
|
139
|
+
const sourceFile = parseSourceFile(filepath, code);
|
|
140
|
+
|
|
141
|
+
const commandPathOverride = getExportedString(sourceFile, 'commandPath');
|
|
142
|
+
const defaultClass = getDefaultExportClass(sourceFile);
|
|
143
|
+
|
|
144
|
+
if (!defaultClass) continue;
|
|
145
|
+
|
|
146
|
+
const className = defaultClass.name?.text || getGeneratedClassName(filepath);
|
|
147
|
+
const commandBasePath = commandPathOverride || getCommandBasePathFromFilepath(filepath, searchDir.root);
|
|
148
|
+
const methods: TCommandMethodMeta[] = [];
|
|
149
|
+
|
|
150
|
+
for (const member of defaultClass.members) {
|
|
151
|
+
if (!ts.isMethodDeclaration(member)) continue;
|
|
152
|
+
if (!member.body) continue;
|
|
153
|
+
if (!member.name || !ts.isIdentifier(member.name)) continue;
|
|
154
|
+
|
|
155
|
+
methods.push({
|
|
156
|
+
name: member.name.text,
|
|
157
|
+
path: [commandBasePath, member.name.text].filter(Boolean).join('/'),
|
|
158
|
+
sourceLocation: getNodeLocation(sourceFile, member.name),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!methods.length) continue;
|
|
163
|
+
|
|
164
|
+
commands.push({
|
|
165
|
+
filepath,
|
|
166
|
+
importPath: buildImportPath(searchDir, filepath),
|
|
167
|
+
className,
|
|
168
|
+
commandBasePath,
|
|
169
|
+
methods,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return commands.sort((a, b) => a.filepath.localeCompare(b.filepath));
|
|
175
|
+
};
|
|
@@ -2,130 +2,21 @@ import path from 'path';
|
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
|
|
4
4
|
import writeIfChanged from '../writeIfChanged';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export type
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
export type TProteumManifestService = {
|
|
21
|
-
kind: 'service' | 'ref';
|
|
22
|
-
id?: string;
|
|
23
|
-
registeredName: string;
|
|
24
|
-
metaName?: string;
|
|
25
|
-
parent: string;
|
|
26
|
-
priority: number;
|
|
27
|
-
importPath?: string;
|
|
28
|
-
sourceDir?: string;
|
|
29
|
-
metasFilepath?: string;
|
|
30
|
-
refTo?: string;
|
|
31
|
-
scope: TProteumManifestScope;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export type TProteumManifestController = {
|
|
35
|
-
className: string;
|
|
36
|
-
importPath: string;
|
|
37
|
-
filepath: string;
|
|
38
|
-
sourceLocation: TProteumManifestSourceLocation;
|
|
39
|
-
routeBasePath: string;
|
|
40
|
-
methodName: string;
|
|
41
|
-
inputCallsCount: number;
|
|
42
|
-
hasInput: boolean;
|
|
43
|
-
routePath: string;
|
|
44
|
-
httpPath: string;
|
|
45
|
-
clientAccessor: string;
|
|
46
|
-
scope: TProteumManifestScope;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
export type TProteumManifestRoute = {
|
|
50
|
-
kind: 'client-page' | 'client-error' | 'server-route';
|
|
51
|
-
methodName: string;
|
|
52
|
-
serviceLocalName: string;
|
|
53
|
-
filepath: string;
|
|
54
|
-
sourceLocation: TProteumManifestSourceLocation;
|
|
55
|
-
targetResolution: TProteumManifestRouteTargetResolution;
|
|
56
|
-
path?: string;
|
|
57
|
-
pathRaw?: string;
|
|
58
|
-
code?: number;
|
|
59
|
-
codeRaw?: string;
|
|
60
|
-
optionKeys: string[];
|
|
61
|
-
normalizedOptionKeys: string[];
|
|
62
|
-
invalidOptionKeys: string[];
|
|
63
|
-
reservedOptionKeys: string[];
|
|
64
|
-
optionsRaw?: string;
|
|
65
|
-
hasSetup: boolean;
|
|
66
|
-
chunkId?: string;
|
|
67
|
-
chunkFilepath?: string;
|
|
68
|
-
scope: TProteumManifestScope;
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
export type TProteumManifestLayout = {
|
|
72
|
-
chunkId: string;
|
|
73
|
-
filepath: string;
|
|
74
|
-
importPath: string;
|
|
75
|
-
depth: number;
|
|
76
|
-
scope: TProteumManifestScope;
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
export type TProteumManifest = {
|
|
80
|
-
version: 1;
|
|
81
|
-
app: {
|
|
82
|
-
root: string;
|
|
83
|
-
coreRoot: string;
|
|
84
|
-
identityFilepath: string;
|
|
85
|
-
identity: {
|
|
86
|
-
name: string;
|
|
87
|
-
identifier: string;
|
|
88
|
-
description: string;
|
|
89
|
-
language?: string;
|
|
90
|
-
locale?: string;
|
|
91
|
-
title?: string;
|
|
92
|
-
titleSuffix?: string;
|
|
93
|
-
fullTitle?: string;
|
|
94
|
-
webDescription?: string;
|
|
95
|
-
version?: string;
|
|
96
|
-
};
|
|
97
|
-
};
|
|
98
|
-
conventions: {
|
|
99
|
-
routeSetupOptionKeys: string[];
|
|
100
|
-
reservedRouteSetupKeys: string[];
|
|
101
|
-
};
|
|
102
|
-
env: {
|
|
103
|
-
source: string;
|
|
104
|
-
loadedVariableKeys: string[];
|
|
105
|
-
requiredVariables: {
|
|
106
|
-
key: string;
|
|
107
|
-
possibleValues: string[];
|
|
108
|
-
provided: boolean;
|
|
109
|
-
}[];
|
|
110
|
-
resolved: {
|
|
111
|
-
name: string;
|
|
112
|
-
profile: string;
|
|
113
|
-
routerPort: number;
|
|
114
|
-
routerCurrentDomain: string;
|
|
115
|
-
};
|
|
116
|
-
};
|
|
117
|
-
services: {
|
|
118
|
-
app: TProteumManifestService[];
|
|
119
|
-
routerPlugins: TProteumManifestService[];
|
|
120
|
-
};
|
|
121
|
-
controllers: TProteumManifestController[];
|
|
122
|
-
routes: {
|
|
123
|
-
client: TProteumManifestRoute[];
|
|
124
|
-
server: TProteumManifestRoute[];
|
|
125
|
-
};
|
|
126
|
-
layouts: TProteumManifestLayout[];
|
|
127
|
-
diagnostics: TProteumManifestDiagnostic[];
|
|
128
|
-
};
|
|
5
|
+
import type { TProteumManifest } from '@common/dev/proteumManifest';
|
|
6
|
+
|
|
7
|
+
export type {
|
|
8
|
+
TProteumManifest,
|
|
9
|
+
TProteumManifestCommand,
|
|
10
|
+
TProteumManifestController,
|
|
11
|
+
TProteumManifestDiagnostic,
|
|
12
|
+
TProteumManifestDiagnosticLevel,
|
|
13
|
+
TProteumManifestLayout,
|
|
14
|
+
TProteumManifestRoute,
|
|
15
|
+
TProteumManifestRouteTargetResolution,
|
|
16
|
+
TProteumManifestScope,
|
|
17
|
+
TProteumManifestService,
|
|
18
|
+
TProteumManifestSourceLocation,
|
|
19
|
+
} from '@common/dev/proteumManifest';
|
|
129
20
|
|
|
130
21
|
export const getProteumManifestPath = (appRoot: string) => path.join(appRoot, '.proteum', 'manifest.json');
|
|
131
22
|
|
package/cli/compiler/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { writeClientManifest } from './common/clientManifest';
|
|
|
16
16
|
import { logVerbose } from '../runtime/verbose';
|
|
17
17
|
import { createCompileReporter, type TCompileReporter } from '../presentation/compileReporter';
|
|
18
18
|
import { generateRoutingArtifacts } from './artifacts/routing';
|
|
19
|
+
import { generateCommandArtifacts } from './artifacts/commands';
|
|
19
20
|
import { generateControllerArtifacts } from './artifacts/controllers';
|
|
20
21
|
import { generateServiceArtifacts } from './artifacts/services';
|
|
21
22
|
import { writeCurrentProteumManifest } from './artifacts/manifest';
|
|
@@ -32,6 +33,7 @@ export default class Compiler {
|
|
|
32
33
|
public compiling: { [compiler: string]: Promise<void> } = {};
|
|
33
34
|
private recentCompilationResults: { [compiler: string]: TRecentCompilationResult } = {};
|
|
34
35
|
private recentModifiedFiles: { [compiler: string]: string[] } = {};
|
|
36
|
+
private manualModifiedFiles: { [compiler: string]: Set<string> } = {};
|
|
35
37
|
private refreshingGeneratedArtifacts?: Promise<void>;
|
|
36
38
|
private compileReporter?: TCompileReporter;
|
|
37
39
|
|
|
@@ -140,11 +142,13 @@ export default class Compiler {
|
|
|
140
142
|
this.refreshingGeneratedArtifacts = (async () => {
|
|
141
143
|
const services = generateServiceArtifacts();
|
|
142
144
|
const controllers = generateControllerArtifacts();
|
|
145
|
+
const commands = generateCommandArtifacts();
|
|
143
146
|
const { clientRoutes, serverRoutes, layouts } = generateRoutingArtifacts();
|
|
144
147
|
|
|
145
148
|
writeCurrentProteumManifest({
|
|
146
149
|
services,
|
|
147
150
|
controllers,
|
|
151
|
+
commands,
|
|
148
152
|
routes: { client: clientRoutes, server: serverRoutes },
|
|
149
153
|
layouts,
|
|
150
154
|
});
|
|
@@ -172,6 +176,21 @@ export default class Compiler {
|
|
|
172
176
|
return recentCompilationResults;
|
|
173
177
|
}
|
|
174
178
|
|
|
179
|
+
public noteManualModifiedFiles(compilerNames: string[] | string, filepaths: string[]) {
|
|
180
|
+
const normalizedCompilerNames = Array.isArray(compilerNames) ? compilerNames : [compilerNames];
|
|
181
|
+
const normalizedFilepaths = filepaths.map((filepath) => normalizePath(path.resolve(filepath)));
|
|
182
|
+
|
|
183
|
+
for (const compilerName of normalizedCompilerNames) {
|
|
184
|
+
const pendingFiles = this.manualModifiedFiles[compilerName] || new Set<string>();
|
|
185
|
+
|
|
186
|
+
for (const filepath of normalizedFilepaths) {
|
|
187
|
+
pendingFiles.add(filepath);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
this.manualModifiedFiles[compilerName] = pendingFiles;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
175
194
|
public async create() {
|
|
176
195
|
await this.warmupApp();
|
|
177
196
|
|
|
@@ -204,9 +223,13 @@ export default class Compiler {
|
|
|
204
223
|
compiler.hooks.compile.tap(name, (compilation) => {
|
|
205
224
|
this.callbacks.before && this.callbacks.before(compiler);
|
|
206
225
|
|
|
207
|
-
|
|
208
|
-
|
|
226
|
+
const compilerModifiedFiles = [...(compiler.modifiedFiles ? [...compiler.modifiedFiles] : [])].map((filepath) =>
|
|
227
|
+
normalizePath(path.resolve(filepath)),
|
|
209
228
|
);
|
|
229
|
+
const manualModifiedFiles = [...(this.manualModifiedFiles[name] || [])];
|
|
230
|
+
delete this.manualModifiedFiles[name];
|
|
231
|
+
|
|
232
|
+
this.recentModifiedFiles[name] = [...new Set([...compilerModifiedFiles, ...manualModifiedFiles])];
|
|
210
233
|
|
|
211
234
|
this.compiling[name] = new Promise((resolve) => (finished = resolve));
|
|
212
235
|
|
|
@@ -184,9 +184,12 @@ export default function createCompiler(
|
|
|
184
184
|
// Prisma 7 generates TypeScript entrypoints under var/prisma.
|
|
185
185
|
app.paths.root + '/var/prisma',
|
|
186
186
|
|
|
187
|
+
app.paths.root + '/commands',
|
|
188
|
+
|
|
187
189
|
// Dossiers server uniquement pour le bundle server
|
|
188
190
|
app.paths.root + '/server',
|
|
189
191
|
app.paths.server.generated,
|
|
192
|
+
...frameworkRoots.map((rootPath) => rootPath + '/commands'),
|
|
190
193
|
...frameworkRoots.map((rootPath) => rootPath + '/client'),
|
|
191
194
|
...frameworkRoots.map((rootPath) => rootPath + '/common'),
|
|
192
195
|
...frameworkRoots.map((rootPath) => rootPath + '/server'),
|
|
@@ -14,6 +14,7 @@ export const proteumCommandNames = [
|
|
|
14
14
|
'doctor',
|
|
15
15
|
'explain',
|
|
16
16
|
'trace',
|
|
17
|
+
'command',
|
|
17
18
|
] as const;
|
|
18
19
|
|
|
19
20
|
export type TProteumCommandName = (typeof proteumCommandNames)[number];
|
|
@@ -46,7 +47,7 @@ export const proteumRecommendedFlow: TRow[] = [
|
|
|
46
47
|
export const proteumCommandGroups: Array<{ title: string; names: TProteumCommandName[] }> = [
|
|
47
48
|
{ title: 'Daily workflow', names: ['dev', 'refresh', 'build'] },
|
|
48
49
|
{ title: 'Quality gates', names: ['typecheck', 'lint', 'check'] },
|
|
49
|
-
{ title: 'Manifest and contracts', names: ['doctor', 'explain', 'trace'] },
|
|
50
|
+
{ title: 'Manifest and contracts', names: ['doctor', 'explain', 'trace', 'command'] },
|
|
50
51
|
{ title: 'Project scaffolding', names: ['init'] },
|
|
51
52
|
];
|
|
52
53
|
|
|
@@ -171,14 +172,14 @@ export const proteumCommands: Record<TProteumCommandName, TProteumCommandDoc> =
|
|
|
171
172
|
name: 'explain',
|
|
172
173
|
category: 'Manifest and contracts',
|
|
173
174
|
summary: 'Explain the generated Proteum manifest.',
|
|
174
|
-
usage: 'proteum explain [--all|--app|--conventions|--env|--services|--controllers|--routes|--layouts|--diagnostics] [--json]',
|
|
175
|
+
usage: 'proteum explain [--all|--app|--conventions|--env|--services|--controllers|--commands|--routes|--layouts|--diagnostics] [--json]',
|
|
175
176
|
bestFor:
|
|
176
|
-
'Inspecting how source files became generated routes, controllers, layouts, services, and diagnostics without reading compiler internals.',
|
|
177
|
+
'Inspecting how source files became generated routes, controllers, commands, layouts, services, and diagnostics without reading compiler internals.',
|
|
177
178
|
examples: [
|
|
178
179
|
{ description: 'Show the default human summary', command: 'proteum explain' },
|
|
179
180
|
{
|
|
180
|
-
description: 'Inspect generated routes and
|
|
181
|
-
command: 'proteum explain --routes --controllers',
|
|
181
|
+
description: 'Inspect generated routes, controllers, and commands together',
|
|
182
|
+
command: 'proteum explain --routes --controllers --commands',
|
|
182
183
|
},
|
|
183
184
|
{ description: 'Emit the selected manifest sections as JSON', command: 'proteum explain --routes --json' },
|
|
184
185
|
],
|
|
@@ -206,6 +207,37 @@ export const proteumCommands: Record<TProteumCommandName, TProteumCommandDoc> =
|
|
|
206
207
|
],
|
|
207
208
|
status: 'experimental',
|
|
208
209
|
},
|
|
210
|
+
command: {
|
|
211
|
+
name: 'command',
|
|
212
|
+
category: 'Manifest and contracts',
|
|
213
|
+
summary: 'Run a dev-only app command from /commands or against an existing dev instance.',
|
|
214
|
+
usage: 'proteum command <path> [--port <port>|--url <baseUrl>] [--json]',
|
|
215
|
+
bestFor:
|
|
216
|
+
'Internal testing, debugging, and one-off service execution that should not be exposed as a normal controller or route.',
|
|
217
|
+
examples: [
|
|
218
|
+
{
|
|
219
|
+
description: 'Run a local command through a temporary bundled dev server',
|
|
220
|
+
command: 'proteum command proteum/diagnostics/ping',
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
description: 'Run a command against an existing dev server',
|
|
224
|
+
command: 'proteum command proteum/diagnostics/ping --port 3101',
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
description: 'Emit the command execution as JSON',
|
|
228
|
+
command: 'proteum command proteum/diagnostics/ping --json',
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
notes: [
|
|
232
|
+
'Commands live under `./commands/**/*.ts` and default-export a class that extends `{ Commands }` from `@server/app/commands`.',
|
|
233
|
+
'Methods are addressed by file path plus method name, mirroring controller path generation.',
|
|
234
|
+
'Proteum creates `./commands/tsconfig.json` and `.proteum/server/commands.d.ts` so `/commands` gets a command-specific alias/type project.',
|
|
235
|
+
'Prefer `extends Commands` directly inside `/commands`; importing the app class is still supported through a generated command-only `@/server/index` type alias.',
|
|
236
|
+
'Without `--port` or `--url`, Proteum refreshes generated artifacts, builds the dev output, starts a temporary local dev server, runs the command, and exits.',
|
|
237
|
+
'With `--port` or `--url`, Proteum talks to the running app over the dev-only `__proteum/commands` HTTP endpoints.',
|
|
238
|
+
],
|
|
239
|
+
status: 'experimental',
|
|
240
|
+
},
|
|
209
241
|
};
|
|
210
242
|
|
|
211
243
|
export const isLikelyProteumAppRoot = (workdir: string) =>
|
package/cli/runtime/commands.ts
CHANGED
|
@@ -153,6 +153,7 @@ class ExplainCommand extends ProteumCommand {
|
|
|
153
153
|
public env = Option.Boolean('--env', false, { description: 'Include the env section.' });
|
|
154
154
|
public services = Option.Boolean('--services', false, { description: 'Include the services section.' });
|
|
155
155
|
public controllers = Option.Boolean('--controllers', false, { description: 'Include the controllers section.' });
|
|
156
|
+
public commands = Option.Boolean('--commands', false, { description: 'Include the commands section.' });
|
|
156
157
|
public routes = Option.Boolean('--routes', false, { description: 'Include the routes section.' });
|
|
157
158
|
public layouts = Option.Boolean('--layouts', false, { description: 'Include the layouts section.' });
|
|
158
159
|
public diagnostics = Option.Boolean('--diagnostics', false, {
|
|
@@ -169,6 +170,7 @@ class ExplainCommand extends ProteumCommand {
|
|
|
169
170
|
env: this.env,
|
|
170
171
|
services: this.services,
|
|
171
172
|
controllers: this.controllers,
|
|
173
|
+
commands: this.commands,
|
|
172
174
|
routes: this.routes,
|
|
173
175
|
layouts: this.layouts,
|
|
174
176
|
diagnostics: this.diagnostics,
|
|
@@ -177,7 +179,7 @@ class ExplainCommand extends ProteumCommand {
|
|
|
177
179
|
applyLegacyBooleanArgs(
|
|
178
180
|
'explain',
|
|
179
181
|
this.legacyArgs,
|
|
180
|
-
['json', 'all', 'app', 'conventions', 'env', 'services', 'controllers', 'routes', 'layouts', 'diagnostics'],
|
|
182
|
+
['json', 'all', 'app', 'conventions', 'env', 'services', 'controllers', 'commands', 'routes', 'layouts', 'diagnostics'],
|
|
181
183
|
args,
|
|
182
184
|
);
|
|
183
185
|
this.setCliArgs(args);
|
|
@@ -214,6 +216,30 @@ class TraceCommand extends ProteumCommand {
|
|
|
214
216
|
}
|
|
215
217
|
}
|
|
216
218
|
|
|
219
|
+
class CommandCommand extends ProteumCommand {
|
|
220
|
+
public static paths = [['command']];
|
|
221
|
+
|
|
222
|
+
public static usage = buildUsage('command');
|
|
223
|
+
|
|
224
|
+
public port = Option.String('--port', { description: 'Target an existing dev server on the given port.' });
|
|
225
|
+
public url = Option.String('--url', { description: 'Target an existing dev server at the given base URL.' });
|
|
226
|
+
public json = Option.Boolean('--json', false, { description: 'Print JSON output.' });
|
|
227
|
+
public args = Option.Rest();
|
|
228
|
+
|
|
229
|
+
public async execute() {
|
|
230
|
+
const [path = ''] = this.args;
|
|
231
|
+
|
|
232
|
+
this.setCliArgs({
|
|
233
|
+
path,
|
|
234
|
+
port: this.port ?? '',
|
|
235
|
+
url: this.url ?? '',
|
|
236
|
+
json: this.json,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
await runCommandModule(() => import('../commands/command'));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
217
243
|
export const registeredCommands = {
|
|
218
244
|
init: InitCommand,
|
|
219
245
|
dev: DevCommand,
|
|
@@ -225,6 +251,7 @@ export const registeredCommands = {
|
|
|
225
251
|
doctor: DoctorCommand,
|
|
226
252
|
explain: ExplainCommand,
|
|
227
253
|
trace: TraceCommand,
|
|
254
|
+
command: CommandCommand,
|
|
228
255
|
} as const;
|
|
229
256
|
|
|
230
257
|
export const createCli = (version: string) => {
|
|
@@ -247,6 +274,7 @@ export const createCli = (version: string) => {
|
|
|
247
274
|
clipanion.register(DoctorCommand);
|
|
248
275
|
clipanion.register(ExplainCommand);
|
|
249
276
|
clipanion.register(TraceCommand);
|
|
277
|
+
clipanion.register(CommandCommand);
|
|
250
278
|
|
|
251
279
|
return clipanion;
|
|
252
280
|
};
|
package/cli/tsconfig.json
CHANGED
|
@@ -29,9 +29,12 @@
|
|
|
29
29
|
"outDir": "./bin",
|
|
30
30
|
|
|
31
31
|
"paths": {
|
|
32
|
+
"@client/*": [ "../client/*" ],
|
|
32
33
|
"@cli/*": [ "./*" ],
|
|
33
34
|
"@cli/app": [ "./app" ],
|
|
34
|
-
"@cli": [ "./" ]
|
|
35
|
+
"@cli": [ "./" ],
|
|
36
|
+
"@common/*": [ "../common/*" ],
|
|
37
|
+
"@server/*": [ "../server/*" ]
|
|
35
38
|
},
|
|
36
39
|
},
|
|
37
40
|
|
package/cli/utils/check.ts
CHANGED
|
@@ -5,7 +5,7 @@ import cli from '..';
|
|
|
5
5
|
import Compiler from '../compiler';
|
|
6
6
|
import { runProcess } from './runProcess';
|
|
7
7
|
|
|
8
|
-
const tsconfigPaths = ['client/tsconfig.json', 'server/tsconfig.json'];
|
|
8
|
+
const tsconfigPaths = ['client/tsconfig.json', 'server/tsconfig.json', 'commands/tsconfig.json'];
|
|
9
9
|
const eslintConfigPaths = ['eslint.config.mjs', 'eslint.config.js', 'eslint.config.cjs'];
|
|
10
10
|
|
|
11
11
|
const resolveInstalledBinary = (name: string) => {
|
package/client/app/component.tsx
CHANGED
|
@@ -21,10 +21,17 @@ export default function App({ context }: { context: ClientContext }) {
|
|
|
21
21
|
const curLayout = context.page?.layout;
|
|
22
22
|
const [layout, setLayout] = React.useState<Layout | false | undefined>(curLayout);
|
|
23
23
|
const [apiData, setApiData] = React.useState<{ [k: string]: any } | null>(context.page?.data || {});
|
|
24
|
+
const shouldEnableDevProfiler = __DEV__ && typeof window !== 'undefined' && window.dev;
|
|
25
|
+
const [isDevProfilerMounted, setDevProfilerMounted] = React.useState(false);
|
|
24
26
|
|
|
25
27
|
// TODO: context.page is always provided in the context on the client side
|
|
26
28
|
if (context.app.side === 'client') context.app.setLayout = setLayout;
|
|
27
29
|
|
|
30
|
+
React.useEffect(() => {
|
|
31
|
+
if (!shouldEnableDevProfiler) return;
|
|
32
|
+
setDevProfilerMounted(true);
|
|
33
|
+
}, [shouldEnableDevProfiler]);
|
|
34
|
+
|
|
28
35
|
const layoutProps: LayoutProps = {
|
|
29
36
|
...context,
|
|
30
37
|
context,
|
|
@@ -32,10 +39,14 @@ export default function App({ context }: { context: ClientContext }) {
|
|
|
32
39
|
menu: undefined,
|
|
33
40
|
children: undefined,
|
|
34
41
|
};
|
|
42
|
+
const DevProfiler = shouldEnableDevProfiler
|
|
43
|
+
? ((require('@client/dev/profiler') as typeof import('@client/dev/profiler')).default as React.ComponentType)
|
|
44
|
+
: null;
|
|
35
45
|
|
|
36
46
|
return (
|
|
37
47
|
<ReactClientContext.Provider value={context}>
|
|
38
48
|
<DialogManager />
|
|
49
|
+
{DevProfiler && isDevProfilerMounted ? <DevProfiler /> : null}
|
|
39
50
|
|
|
40
51
|
{!layout ? (
|
|
41
52
|
<RouterComponent service={context.Router} />
|