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
package/cli/app/config.ts
CHANGED
|
@@ -2,20 +2,12 @@
|
|
|
2
2
|
- DEPENDANCES
|
|
3
3
|
----------------------------------*/
|
|
4
4
|
|
|
5
|
-
/*
|
|
6
|
-
NOTE: This is a copy of core/sever/app/config
|
|
7
|
-
We can't import core deps here because it will cause the following error:
|
|
8
|
-
"Can't use import when not a module"
|
|
9
|
-
It will be possible to import core files when the CLI will be compiled as one output file with tsc
|
|
10
|
-
And for that, we need to fix the TS errors for the CLI
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
5
|
// Npm
|
|
14
6
|
import fs from 'fs-extra';
|
|
15
7
|
import yaml from 'yaml';
|
|
16
8
|
|
|
17
9
|
// Types
|
|
18
|
-
import type
|
|
10
|
+
import { parseProteumEnvConfig, type TProteumLoadedEnvConfig } from '../../common/env/proteumEnv';
|
|
19
11
|
import { logVerbose } from '../runtime/verbose';
|
|
20
12
|
|
|
21
13
|
/*----------------------------------
|
|
@@ -34,18 +26,13 @@ export default class ConfigParser {
|
|
|
34
26
|
return yaml.parse(rawConfig);
|
|
35
27
|
}
|
|
36
28
|
|
|
37
|
-
public env():
|
|
38
|
-
|
|
39
|
-
// Otherwise, we're in production environment (docker)
|
|
40
|
-
logVerbose('[app] Using environment:', process.env.NODE_ENV);
|
|
41
|
-
const envFileName = this.appDir + '/env.yaml';
|
|
42
|
-
const envFile = this.loadYaml(envFileName);
|
|
29
|
+
public env(): TProteumLoadedEnvConfig {
|
|
30
|
+
logVerbose('[app] Loading Proteum env vars from process.env');
|
|
43
31
|
return {
|
|
44
|
-
...
|
|
45
|
-
|
|
46
|
-
this.routerPortOverride
|
|
47
|
-
|
|
48
|
-
: { ...envFile.router, port: this.routerPortOverride },
|
|
32
|
+
...parseProteumEnvConfig({
|
|
33
|
+
appDir: this.appDir,
|
|
34
|
+
routerPortOverride: this.routerPortOverride,
|
|
35
|
+
}),
|
|
49
36
|
version: 'CLI',
|
|
50
37
|
};
|
|
51
38
|
}
|
package/cli/bin.js
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
|
|
5
|
+
const clearInteractiveConsole = () => {
|
|
6
|
+
if (process.stdout.isTTY !== true || process.env.TERM === 'dumb') return;
|
|
7
|
+
|
|
8
|
+
process.stdout.write('\x1B[2J\x1B[3J\x1B[H');
|
|
9
|
+
};
|
|
10
|
+
|
|
5
11
|
/*
|
|
6
12
|
Why this exists (npm i vs npm link difference)
|
|
7
13
|
|
|
@@ -30,6 +36,8 @@ if (!process.env.TS_NODE_IGNORE) {
|
|
|
30
36
|
process.env.TS_NODE_PROJECT = path.join(__dirname, 'tsconfig.json');
|
|
31
37
|
process.env.TS_NODE_TRANSPILE_ONLY = '1';
|
|
32
38
|
|
|
39
|
+
clearInteractiveConsole();
|
|
40
|
+
|
|
33
41
|
require('ts-node/register/transpile-only');
|
|
34
42
|
|
|
35
43
|
const { runCli } = require('./index.ts');
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import got from 'got';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import { UsageError } from 'clipanion';
|
|
5
|
+
|
|
6
|
+
import cli from '..';
|
|
7
|
+
import app from '../app';
|
|
8
|
+
import {
|
|
9
|
+
normalizeDevCommandPath,
|
|
10
|
+
type TDevCommandErrorResponse,
|
|
11
|
+
type TDevCommandExecution,
|
|
12
|
+
type TDevCommandRunResponse,
|
|
13
|
+
} from '../../common/dev/commands';
|
|
14
|
+
|
|
15
|
+
const localCommandResultMarker = '__PROTEUM_COMMAND_RESULT__';
|
|
16
|
+
|
|
17
|
+
const normalizeBaseUrl = (value: string) => value.replace(/\/+$/, '');
|
|
18
|
+
|
|
19
|
+
const getRouterPortFromManifest = () => {
|
|
20
|
+
const manifestFilepath = path.join(cli.args.workdir as string, '.proteum', 'manifest.json');
|
|
21
|
+
if (!require('fs-extra').existsSync(manifestFilepath)) return undefined;
|
|
22
|
+
|
|
23
|
+
const manifest = require('fs-extra').readJsonSync(manifestFilepath, { throws: false }) as
|
|
24
|
+
| { env?: { resolved?: { routerPort?: number } } }
|
|
25
|
+
| undefined;
|
|
26
|
+
const port = manifest?.env?.resolved?.routerPort;
|
|
27
|
+
|
|
28
|
+
if (typeof port !== 'number' || port <= 0) return undefined;
|
|
29
|
+
|
|
30
|
+
return String(port);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const getRouterPort = () => {
|
|
34
|
+
const overridePort = typeof cli.args.port === 'string' && cli.args.port ? cli.args.port : '';
|
|
35
|
+
if (overridePort) return overridePort;
|
|
36
|
+
|
|
37
|
+
const envPort = process.env.PORT?.trim();
|
|
38
|
+
if (envPort) return envPort;
|
|
39
|
+
|
|
40
|
+
const manifestPort = getRouterPortFromManifest();
|
|
41
|
+
if (manifestPort) return manifestPort;
|
|
42
|
+
|
|
43
|
+
throw new UsageError(
|
|
44
|
+
`Could not determine the router port from PORT or .proteum/manifest.json in ${cli.args.workdir as string}. Pass --port or --url explicitly.`,
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const getRouterBaseUrls = () => {
|
|
49
|
+
const explicitUrl = typeof cli.args.url === 'string' && cli.args.url ? cli.args.url.trim() : '';
|
|
50
|
+
if (explicitUrl) return [normalizeBaseUrl(explicitUrl)];
|
|
51
|
+
|
|
52
|
+
const port = getRouterPort();
|
|
53
|
+
return [...new Set([`http://127.0.0.1:${port}`, `http://localhost:${port}`, `http://[::1]:${port}`])];
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const getCommandErrorMessage = (body: TDevCommandErrorResponse | object | string | undefined, statusCode: number) => {
|
|
57
|
+
if (typeof body === 'object' && body !== null && 'error' in body && typeof body.error === 'string') {
|
|
58
|
+
return body.error;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return `Command request failed with status ${statusCode}.`;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const requestJson = async <TResponse>(pathname: string, options?: { method?: 'GET' | 'POST'; json?: object }) => {
|
|
65
|
+
const attempts: string[] = [];
|
|
66
|
+
|
|
67
|
+
for (const baseUrl of getRouterBaseUrls()) {
|
|
68
|
+
try {
|
|
69
|
+
const response = await got(`${baseUrl}${pathname}`, {
|
|
70
|
+
method: options?.method || 'GET',
|
|
71
|
+
json: options?.json,
|
|
72
|
+
responseType: 'json',
|
|
73
|
+
throwHttpErrors: false,
|
|
74
|
+
retry: { limit: 0 },
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (response.statusCode >= 400) {
|
|
78
|
+
throw new UsageError(
|
|
79
|
+
getCommandErrorMessage(response.body as TDevCommandErrorResponse | object | string | undefined, response.statusCode),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return response.body as TResponse;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (error instanceof UsageError) throw error;
|
|
86
|
+
|
|
87
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
88
|
+
attempts.push(`${baseUrl}${pathname}: ${message}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
throw new UsageError(
|
|
93
|
+
[
|
|
94
|
+
'Could not reach the Proteum command server.',
|
|
95
|
+
...attempts.map((attempt) => `- ${attempt}`),
|
|
96
|
+
'Make sure the app is running with `proteum dev`, or omit --port/--url to run the command locally.',
|
|
97
|
+
].join('\n'),
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const printJson = (value: object) => {
|
|
102
|
+
console.log(JSON.stringify(value, null, 2));
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const renderExecution = (execution: TDevCommandExecution) => {
|
|
106
|
+
const lines = [
|
|
107
|
+
`Command ${execution.command.path}`,
|
|
108
|
+
`- status=${execution.status} durationMs=${execution.durationMs}`,
|
|
109
|
+
`- source=${execution.command.filepath}:${execution.command.sourceLocation.line}:${execution.command.sourceLocation.column}`,
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
if (execution.errorMessage) {
|
|
113
|
+
lines.push(`- error=${execution.errorMessage}`);
|
|
114
|
+
} else if (execution.result?.json !== undefined) {
|
|
115
|
+
lines.push('Result');
|
|
116
|
+
lines.push(JSON.stringify(execution.result.json, null, 2));
|
|
117
|
+
} else if (execution.result?.summary !== undefined) {
|
|
118
|
+
lines.push('Result');
|
|
119
|
+
lines.push(JSON.stringify(execution.result.summary, null, 2));
|
|
120
|
+
} else {
|
|
121
|
+
lines.push('- result=undefined');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return lines.join('\n');
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const runLocalCommand = async (commandPath: string) => {
|
|
128
|
+
if (app.env.profile !== 'dev') {
|
|
129
|
+
throw new UsageError(`Proteum commands are only available when ENV_PROFILE=dev. Current profile: ${app.env.profile}.`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const runnerFilepath = path.join(cli.paths.core.root, 'cli', 'commands', 'commandLocalRunner.js');
|
|
133
|
+
|
|
134
|
+
return await new Promise<TDevCommandExecution>((resolve, reject) => {
|
|
135
|
+
const stdoutChunks: Buffer[] = [];
|
|
136
|
+
const stderrChunks: Buffer[] = [];
|
|
137
|
+
const child = spawn(process.execPath, [runnerFilepath, app.paths.root, commandPath], {
|
|
138
|
+
cwd: app.paths.root,
|
|
139
|
+
env: { ...process.env },
|
|
140
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
child.stdout.on('data', (chunk: Buffer | string) => {
|
|
144
|
+
stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
child.stderr.on('data', (chunk: Buffer | string) => {
|
|
148
|
+
stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
child.on('error', (error) => reject(error));
|
|
152
|
+
child.on('close', (exitCode) => {
|
|
153
|
+
const stdout = Buffer.concat(stdoutChunks).toString('utf8');
|
|
154
|
+
const stderr = Buffer.concat(stderrChunks).toString('utf8');
|
|
155
|
+
const markerLine = stdout
|
|
156
|
+
.split(/\r?\n/)
|
|
157
|
+
.find((line) => line.startsWith(localCommandResultMarker));
|
|
158
|
+
|
|
159
|
+
if (stderr.trim()) {
|
|
160
|
+
process.stderr.write(stderr.endsWith('\n') ? stderr : `${stderr}\n`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!markerLine) {
|
|
164
|
+
reject(
|
|
165
|
+
new Error(
|
|
166
|
+
[
|
|
167
|
+
`Local command runner exited without returning a structured result (exit code ${exitCode ?? 'unknown'}).`,
|
|
168
|
+
stdout.trim() ? `stdout:\n${stdout.trim()}` : undefined,
|
|
169
|
+
]
|
|
170
|
+
.filter(Boolean)
|
|
171
|
+
.join('\n\n'),
|
|
172
|
+
),
|
|
173
|
+
);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const payload = JSON.parse(markerLine.slice(localCommandResultMarker.length)) as
|
|
178
|
+
| { execution: TDevCommandExecution }
|
|
179
|
+
| { error: string };
|
|
180
|
+
|
|
181
|
+
if ('execution' in payload) {
|
|
182
|
+
if (exitCode && exitCode !== 0) {
|
|
183
|
+
const error = new Error(payload.execution.errorMessage || `Command "${commandPath}" failed.`) as Error & {
|
|
184
|
+
execution?: TDevCommandExecution;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
error.execution = payload.execution;
|
|
188
|
+
reject(error);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
resolve(payload.execution);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
reject(new Error(payload.error || `Command "${commandPath}" failed.`));
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
export const run = async () => {
|
|
202
|
+
const rawPath = typeof cli.args.path === 'string' ? cli.args.path : '';
|
|
203
|
+
const commandPath = normalizeDevCommandPath(rawPath);
|
|
204
|
+
const shouldPrintJson = cli.args.json === true;
|
|
205
|
+
const shouldUseRemoteServer =
|
|
206
|
+
(typeof cli.args.port === 'string' && cli.args.port.length > 0) ||
|
|
207
|
+
(typeof cli.args.url === 'string' && cli.args.url.length > 0);
|
|
208
|
+
|
|
209
|
+
if (!commandPath) {
|
|
210
|
+
throw new UsageError('A command path is required. Example: proteum command diagnostics/ping');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const execution = shouldUseRemoteServer
|
|
215
|
+
? (await requestJson<TDevCommandRunResponse>('/__proteum/commands/run', {
|
|
216
|
+
method: 'POST',
|
|
217
|
+
json: { path: commandPath },
|
|
218
|
+
})).execution
|
|
219
|
+
: await runLocalCommand(commandPath);
|
|
220
|
+
|
|
221
|
+
if (shouldPrintJson) {
|
|
222
|
+
printJson({ execution });
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
console.log(renderExecution(execution));
|
|
227
|
+
} catch (error) {
|
|
228
|
+
if (error instanceof Error && 'execution' in error && typeof error.execution === 'object' && error.execution) {
|
|
229
|
+
const execution = error.execution as TDevCommandExecution;
|
|
230
|
+
|
|
231
|
+
if (shouldPrintJson) {
|
|
232
|
+
printJson({ execution });
|
|
233
|
+
} else {
|
|
234
|
+
console.log(renderExecution(execution));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
process.exitCode = 1;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const net = require('net');
|
|
3
|
+
const { spawn } = require('child_process');
|
|
4
|
+
const tsNode = require('ts-node');
|
|
5
|
+
|
|
6
|
+
const resultMarker = '__PROTEUM_COMMAND_RESULT__';
|
|
7
|
+
|
|
8
|
+
const printPayload = (payload) => {
|
|
9
|
+
process.stdout.write(`${resultMarker}${JSON.stringify(payload)}\n`);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const fail = (error, execution) => {
|
|
13
|
+
if (execution) {
|
|
14
|
+
printPayload({ execution });
|
|
15
|
+
process.exitCode = 1;
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
printPayload({ error: error instanceof Error ? error.message : String(error) });
|
|
20
|
+
process.exitCode = 1;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const getAvailablePort = async () =>
|
|
24
|
+
await new Promise((resolve, reject) => {
|
|
25
|
+
const server = net.createServer();
|
|
26
|
+
|
|
27
|
+
server.once('error', reject);
|
|
28
|
+
server.listen(0, '127.0.0.1', () => {
|
|
29
|
+
const address = server.address();
|
|
30
|
+
|
|
31
|
+
if (!address || typeof address === 'string') {
|
|
32
|
+
server.close(() => reject(new Error('Could not determine a local port for the command runner.')));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
server.close((error) => {
|
|
37
|
+
if (error) {
|
|
38
|
+
reject(error);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
resolve(address.port);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const closeMultiCompiler = async (multiCompiler) =>
|
|
48
|
+
await new Promise((resolve, reject) => {
|
|
49
|
+
multiCompiler.close((error) => {
|
|
50
|
+
if (error) {
|
|
51
|
+
reject(error);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
resolve();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const runCompiler = async (compiler) => {
|
|
60
|
+
const multiCompiler = await compiler.create();
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
await new Promise((resolve, reject) => {
|
|
64
|
+
multiCompiler.run((error, stats) => {
|
|
65
|
+
if (error) {
|
|
66
|
+
reject(error);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (stats && stats.hasErrors()) {
|
|
71
|
+
reject(new Error('Compilation failed for the local dev command runner.'));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
resolve();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
} finally {
|
|
79
|
+
compiler.dispose();
|
|
80
|
+
await closeMultiCompiler(multiCompiler);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const waitForServerReady = async (child) =>
|
|
85
|
+
await new Promise((resolve, reject) => {
|
|
86
|
+
let settled = false;
|
|
87
|
+
|
|
88
|
+
const finish = (callback, value) => {
|
|
89
|
+
if (settled) return;
|
|
90
|
+
settled = true;
|
|
91
|
+
clearTimeout(timeout);
|
|
92
|
+
child.off('message', onMessage);
|
|
93
|
+
child.off('error', onError);
|
|
94
|
+
child.off('exit', onExit);
|
|
95
|
+
callback(value);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const onMessage = (message) => {
|
|
99
|
+
if (!message || message.type !== 'proteum:server-ready' || typeof message.publicUrl !== 'string') return;
|
|
100
|
+
finish(resolve, message.publicUrl);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const onError = (error) => finish(reject, error);
|
|
104
|
+
const onExit = (code, signal) =>
|
|
105
|
+
finish(reject, new Error(`Local command server exited before becoming ready (code=${code}, signal=${signal}).`));
|
|
106
|
+
const timeout = setTimeout(
|
|
107
|
+
() => finish(reject, new Error('Timed out while waiting for the local command server to become ready.')),
|
|
108
|
+
30000,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
child.on('message', onMessage);
|
|
112
|
+
child.once('error', onError);
|
|
113
|
+
child.once('exit', onExit);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const stopServerProcess = async (child) => {
|
|
117
|
+
if (!child || child.exitCode !== null || child.signalCode !== null) return;
|
|
118
|
+
|
|
119
|
+
child.kill('SIGTERM');
|
|
120
|
+
|
|
121
|
+
await new Promise((resolve) => {
|
|
122
|
+
const timeout = setTimeout(() => {
|
|
123
|
+
if (child.exitCode === null && child.signalCode === null) child.kill('SIGKILL');
|
|
124
|
+
}, 5000);
|
|
125
|
+
|
|
126
|
+
child.once('exit', () => {
|
|
127
|
+
clearTimeout(timeout);
|
|
128
|
+
resolve();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const requestCommand = async (baseUrl, commandPath) => {
|
|
134
|
+
const response = await fetch(`${baseUrl}/__proteum/commands/run`, {
|
|
135
|
+
method: 'POST',
|
|
136
|
+
headers: { 'content-type': 'application/json' },
|
|
137
|
+
body: JSON.stringify({ path: commandPath }),
|
|
138
|
+
});
|
|
139
|
+
const body = await response.json();
|
|
140
|
+
|
|
141
|
+
if (body && typeof body === 'object' && body.execution) {
|
|
142
|
+
return { statusCode: response.status, execution: body.execution };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
throw new Error((body && body.error) || `Command request failed with status ${response.status}.`);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
(async () => {
|
|
149
|
+
const [, , appRootArg = '', commandPath = ''] = process.argv;
|
|
150
|
+
const appRoot = path.resolve(appRootArg);
|
|
151
|
+
|
|
152
|
+
if (!appRootArg || !commandPath) {
|
|
153
|
+
fail(new Error('commandLocalRunner requires <appRoot> and <commandPath>.'));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
process.chdir(appRoot);
|
|
158
|
+
|
|
159
|
+
tsNode.register({
|
|
160
|
+
transpileOnly: true,
|
|
161
|
+
project: path.join(__dirname, '..', 'tsconfig.json'),
|
|
162
|
+
files: true,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const port = await getAvailablePort();
|
|
166
|
+
const cli = require('../context.ts').default;
|
|
167
|
+
cli.setArgs({ workdir: appRoot, path: commandPath, port: String(port), url: '', json: true });
|
|
168
|
+
|
|
169
|
+
const app = require('../app/index.ts').default;
|
|
170
|
+
const Compiler = require('../compiler/index.ts').default;
|
|
171
|
+
|
|
172
|
+
if (app.env.profile !== 'dev') {
|
|
173
|
+
fail(new Error(`Proteum commands are only available when ENV_PROFILE=dev. Current profile: ${app.env.profile}.`));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const compiler = new Compiler('dev');
|
|
178
|
+
await runCompiler(compiler);
|
|
179
|
+
|
|
180
|
+
const serverProcess = spawn(process.execPath, ['--preserve-symlinks', path.join(app.outputPath('dev'), 'server.js')], {
|
|
181
|
+
cwd: app.paths.root,
|
|
182
|
+
stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const publicUrl = await waitForServerReady(serverProcess);
|
|
187
|
+
const { statusCode, execution } = await requestCommand(publicUrl, commandPath);
|
|
188
|
+
|
|
189
|
+
printPayload({ execution });
|
|
190
|
+
if (statusCode >= 400 || execution.status === 'error') {
|
|
191
|
+
process.exitCode = 1;
|
|
192
|
+
}
|
|
193
|
+
} finally {
|
|
194
|
+
await stopServerProcess(serverProcess);
|
|
195
|
+
}
|
|
196
|
+
})().catch((error) => {
|
|
197
|
+
fail(error);
|
|
198
|
+
});
|
|
@@ -45,8 +45,7 @@ export async function run() {
|
|
|
45
45
|
{ spaces: 4 },
|
|
46
46
|
);
|
|
47
47
|
|
|
48
|
-
//
|
|
49
|
-
fs.copyFileSync(app.paths.root + (simulate ? '/env.yaml' : '/env.server.yaml'), temp + '/env.yaml');
|
|
48
|
+
// Deployment now relies on exported ENV_*, URL, TRACE_*, and PORT variables instead of copied env config files.
|
|
50
49
|
|
|
51
50
|
// Compile & Run Docker
|
|
52
51
|
await cli.shell(`docker compose up --build`);
|
package/cli/commands/dev.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// Npm
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import { spawn, ChildProcess } from 'child_process';
|
|
8
|
+
import fs from 'fs-extra';
|
|
9
|
+
import type { FSWatcher } from 'fs';
|
|
8
10
|
|
|
9
11
|
// Cor elibs
|
|
10
12
|
import cli from '..';
|
|
@@ -20,7 +22,7 @@ import {
|
|
|
20
22
|
import Compiler from '../compiler';
|
|
21
23
|
import { createDevEventServer } from './devEvents';
|
|
22
24
|
import { ensureProjectAgentSymlinks } from '../utils/agents';
|
|
23
|
-
import { renderDevSession, renderServerReadyBanner } from '../presentation/devSession';
|
|
25
|
+
import { renderDevSession, renderServerReadyBanner, renderDevShutdownBanner } from '../presentation/devSession';
|
|
24
26
|
import { logVerbose } from '../runtime/verbose';
|
|
25
27
|
|
|
26
28
|
// Core
|
|
@@ -31,7 +33,7 @@ import { app, App } from '../app';
|
|
|
31
33
|
----------------------------------*/
|
|
32
34
|
|
|
33
35
|
// Watch rules shared by the dev compiler and hot reload gate.
|
|
34
|
-
const ignoredWatchPathPatterns = /(node_modules\/(?!proteum\/))|(\.generated\/)|(\.cache\/)|(\.proteum\/)/;
|
|
36
|
+
const ignoredWatchPathPatterns = /(node_modules\/(?!proteum\/))|(\.generated\/)|(\.cache\/)|(\.proteum\/)|(\/var\/traces\/)/;
|
|
35
37
|
const hotReloadableServerPathPatterns = [
|
|
36
38
|
/^client\/pages\//,
|
|
37
39
|
/^client\/components\//,
|
|
@@ -50,6 +52,7 @@ let cp: ChildProcess | undefined = undefined;
|
|
|
50
52
|
let devSessionStopping = false;
|
|
51
53
|
let appProcessOperation: Promise<void> = Promise.resolve();
|
|
52
54
|
type TDevWatching = ReturnType<Awaited<ReturnType<Compiler['create']>>['watch']>;
|
|
55
|
+
type TIndexedSourceWatching = { close: () => Promise<void> };
|
|
53
56
|
|
|
54
57
|
/*----------------------------------
|
|
55
58
|
- HELPERS
|
|
@@ -110,6 +113,22 @@ const createIgnoredWatchPattern = (outputPaths: string[]) =>
|
|
|
110
113
|
const getDevAppName = (app: App) =>
|
|
111
114
|
app.identity.web?.fullTitle || app.identity.web?.title || app.identity.name || app.packageJson.name || app.paths.root;
|
|
112
115
|
|
|
116
|
+
const cleanupPersistedDevTraces = async (app: App) => {
|
|
117
|
+
const tracesRoot = path.join(app.paths.root, 'var', 'traces');
|
|
118
|
+
if (!(await fs.pathExists(tracesRoot))) return;
|
|
119
|
+
|
|
120
|
+
const entries = await fs.readdir(tracesRoot);
|
|
121
|
+
const removableEntries = entries.filter((entry) => entry !== 'exports');
|
|
122
|
+
if (removableEntries.length === 0) return;
|
|
123
|
+
|
|
124
|
+
await Promise.all(removableEntries.map((entry) => fs.remove(path.join(tracesRoot, entry))));
|
|
125
|
+
|
|
126
|
+
const remainingEntries = await fs.readdir(tracesRoot).catch(() => []);
|
|
127
|
+
if (remainingEntries.length === 0) {
|
|
128
|
+
await fs.remove(tracesRoot);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
113
132
|
const signalAppProcess = (child: ChildProcess, signal: NodeJS.Signals) => {
|
|
114
133
|
try {
|
|
115
134
|
if (process.platform !== 'win32' && child.pid !== undefined) {
|
|
@@ -155,6 +174,7 @@ async function startApp(app: App) {
|
|
|
155
174
|
await renderServerReadyBanner({
|
|
156
175
|
appName: getDevAppName(app),
|
|
157
176
|
publicUrl: message.publicUrl,
|
|
177
|
+
routerPort: app.env.router.port,
|
|
158
178
|
}),
|
|
159
179
|
);
|
|
160
180
|
})();
|
|
@@ -237,6 +257,78 @@ function normalizeWatchPath(watchPath: string) {
|
|
|
237
257
|
return path.resolve(watchPath).replace(/\\/g, '/').replace(/\/$/, '');
|
|
238
258
|
}
|
|
239
259
|
|
|
260
|
+
const indexedSourceWatchRules: { compilerName: 'server'; root: () => string; relativePathPattern: RegExp }[] = [
|
|
261
|
+
{ compilerName: 'server', root: () => app.paths.root, relativePathPattern: /^commands(?:\/|$)/ },
|
|
262
|
+
{ compilerName: 'server', root: () => cli.paths.core.root, relativePathPattern: /^commands(?:\/|$)/ },
|
|
263
|
+
];
|
|
264
|
+
|
|
265
|
+
const closeFsWatcher = async (watcher: FSWatcher) => {
|
|
266
|
+
await new Promise<void>((resolve) => {
|
|
267
|
+
watcher.once('close', () => resolve());
|
|
268
|
+
watcher.close();
|
|
269
|
+
});
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const createIndexedSourceWatching = ({
|
|
273
|
+
compiler,
|
|
274
|
+
watching,
|
|
275
|
+
}: {
|
|
276
|
+
compiler: Compiler;
|
|
277
|
+
watching: TDevWatching;
|
|
278
|
+
}): TIndexedSourceWatching => {
|
|
279
|
+
const watchers: FSWatcher[] = [];
|
|
280
|
+
const pendingChanges = new Map<'server', Set<string>>();
|
|
281
|
+
let invalidateTimer: NodeJS.Timeout | undefined;
|
|
282
|
+
|
|
283
|
+
const flushInvalidate = () => {
|
|
284
|
+
invalidateTimer = undefined;
|
|
285
|
+
|
|
286
|
+
for (const [compilerName, changedFiles] of pendingChanges) {
|
|
287
|
+
compiler.noteManualModifiedFiles(compilerName, [...changedFiles]);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
pendingChanges.clear();
|
|
291
|
+
logVerbose('Indexed source files changed. Invalidating the dev compiler to refresh generated artifacts.');
|
|
292
|
+
watching.invalidate();
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const queueInvalidate = (compilerName: 'server', filepath: string) => {
|
|
296
|
+
const normalizedFilepath = normalizeWatchPath(filepath);
|
|
297
|
+
const changedFiles = pendingChanges.get(compilerName) || new Set<string>();
|
|
298
|
+
|
|
299
|
+
changedFiles.add(normalizedFilepath);
|
|
300
|
+
pendingChanges.set(compilerName, changedFiles);
|
|
301
|
+
|
|
302
|
+
if (invalidateTimer) return;
|
|
303
|
+
invalidateTimer = setTimeout(flushInvalidate, 40);
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
for (const watchRule of indexedSourceWatchRules) {
|
|
307
|
+
const rootPath = watchRule.root();
|
|
308
|
+
|
|
309
|
+
watchers.push(
|
|
310
|
+
fs.watch(rootPath, { recursive: true }, (eventType, filename) => {
|
|
311
|
+
const relativePath = typeof filename === 'string' ? filename.replace(/\\/g, '/').replace(/^\.\//, '') : '';
|
|
312
|
+
if (relativePath && !watchRule.relativePathPattern.test(relativePath)) return;
|
|
313
|
+
if (eventType !== 'rename' && relativePath) return;
|
|
314
|
+
|
|
315
|
+
queueInvalidate(watchRule.compilerName, relativePath ? path.join(rootPath, relativePath) : rootPath);
|
|
316
|
+
}),
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
close: async () => {
|
|
322
|
+
if (invalidateTimer) {
|
|
323
|
+
clearTimeout(invalidateTimer);
|
|
324
|
+
invalidateTimer = undefined;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
await Promise.all(watchers.map((watcher) => closeFsWatcher(watcher)));
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
};
|
|
331
|
+
|
|
240
332
|
/*----------------------------------
|
|
241
333
|
- MAIN PROCESS
|
|
242
334
|
----------------------------------*/
|
|
@@ -340,6 +432,7 @@ export const run = async () => {
|
|
|
340
432
|
logVerbose('Watch callback. No compiler changes were tracked.');
|
|
341
433
|
},
|
|
342
434
|
);
|
|
435
|
+
const indexedSourceWatching = createIndexedSourceWatching({ compiler, watching });
|
|
343
436
|
|
|
344
437
|
let shuttingDownPromise: Promise<void> | undefined;
|
|
345
438
|
|
|
@@ -349,10 +442,13 @@ export const run = async () => {
|
|
|
349
442
|
devSessionStopping = true;
|
|
350
443
|
shuttingDownPromise = (async () => {
|
|
351
444
|
logVerbose('Stopping the Proteum dev session ...', reason);
|
|
445
|
+
await indexedSourceWatching.close();
|
|
352
446
|
await closeWatching(watching);
|
|
353
447
|
compiler.dispose();
|
|
354
448
|
await stopApp(reason);
|
|
449
|
+
await cleanupPersistedDevTraces(app);
|
|
355
450
|
await devEventServer.close();
|
|
451
|
+
console.info(await renderDevShutdownBanner());
|
|
356
452
|
})();
|
|
357
453
|
|
|
358
454
|
return shuttingDownPromise;
|