proteum 2.1.0-3 → 2.1.0-5
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/README.md +35 -15
- package/agents/framework/AGENTS.md +19 -7
- package/agents/project/AGENTS.md +8 -2
- package/cli/app/config.ts +7 -20
- package/cli/bin.js +8 -0
- package/cli/commands/deploy/web.ts +1 -2
- package/cli/commands/explain.ts +12 -4
- package/cli/commands/trace.ts +22 -12
- package/cli/compiler/artifacts/manifest.ts +18 -19
- package/cli/compiler/common/index.ts +1 -1
- package/cli/compiler/common/proteumManifest.ts +13 -3
- package/cli/presentation/commands.ts +1 -1
- package/client/app/component.tsx +2 -9
- package/client/services/router/index.tsx +2 -3
- package/common/dev/requestTrace.ts +2 -1
- package/common/env/proteumEnv.ts +284 -0
- package/common/router/index.ts +4 -22
- package/docs/request-tracing.md +13 -8
- package/package.json +1 -1
- package/server/app/container/config.ts +11 -64
- package/server/app/container/console/index.ts +2 -1
- package/server/app/container/index.ts +2 -2
- package/server/services/router/index.ts +2 -3
- package/server/services/router/response/index.ts +3 -3
- package/types/global/utils.d.ts +7 -14
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ Proteum combines:
|
|
|
36
36
|
```text
|
|
37
37
|
my-app/
|
|
38
38
|
identity.yaml
|
|
39
|
-
env
|
|
39
|
+
.env # optional file for required local env vars
|
|
40
40
|
package.json
|
|
41
41
|
client/
|
|
42
42
|
pages/
|
|
@@ -63,7 +63,7 @@ my-app/
|
|
|
63
63
|
Important files:
|
|
64
64
|
|
|
65
65
|
- `identity.yaml`: app identity, naming, locale, and SEO-facing metadata defaults
|
|
66
|
-
- `env
|
|
66
|
+
- `process.env` / optional `.env`: `PORT`, `ENV_*`, `URL`, and `TRACE_*` environment variables loaded by the app
|
|
67
67
|
- `server/config/*.ts`: plain typed config exports consumed by the explicit app bootstrap
|
|
68
68
|
- `server/index.ts`: default-exported `Application` subclass that instantiates root services and router plugins
|
|
69
69
|
- `client/pages/**`: SSR page entrypoints registered through `Router.page(...)`
|
|
@@ -71,6 +71,25 @@ Important files:
|
|
|
71
71
|
- `server/services/**`: business logic that extends `Service`
|
|
72
72
|
- `.proteum/**`: framework-owned generated contracts and manifests
|
|
73
73
|
|
|
74
|
+
Required Proteum env vars:
|
|
75
|
+
|
|
76
|
+
- `ENV_NAME`: `local` or `server`
|
|
77
|
+
- `ENV_PROFILE`: `dev`, `testing`, or `prod`
|
|
78
|
+
- `PORT`: default router port
|
|
79
|
+
- `URL`: canonical absolute base URL for `Router.url(..., true)`
|
|
80
|
+
|
|
81
|
+
Proteum does not provide defaults for required env vars. They must be defined explicitly in `process.env` or `.env`.
|
|
82
|
+
|
|
83
|
+
Use `proteum explain env` to see the required env vars, their allowed values, and whether each one is currently provided.
|
|
84
|
+
|
|
85
|
+
Optional trace env vars:
|
|
86
|
+
|
|
87
|
+
- `TRACE_ENABLE`
|
|
88
|
+
- `TRACE_REQUESTS_LIMIT`
|
|
89
|
+
- `TRACE_EVENTS_LIMIT`
|
|
90
|
+
- `TRACE_CAPTURE`
|
|
91
|
+
- `TRACE_PERSIST_ON_ERROR`
|
|
92
|
+
|
|
74
93
|
## Example: Server Bootstrap
|
|
75
94
|
|
|
76
95
|
Proteum app services are declared explicitly through typed config exports plus a concrete `Application` subclass.
|
|
@@ -87,7 +106,7 @@ type RouterBaseConfig = Omit<ServiceConfig<typeof Router>, 'plugins'>;
|
|
|
87
106
|
export const usersConfig = Services.config(Users, {});
|
|
88
107
|
|
|
89
108
|
export const routerBaseConfig = {
|
|
90
|
-
|
|
109
|
+
currentDomain: AppContainer.Environment.router.currentDomain,
|
|
91
110
|
http: {
|
|
92
111
|
domain: 'example.com',
|
|
93
112
|
port: AppContainer.Environment.router.port,
|
|
@@ -265,6 +284,8 @@ proteum trace latest
|
|
|
265
284
|
|
|
266
285
|
Proteum includes a dev-only in-memory request trace buffer for routing, controller, context, SSR, and render debugging.
|
|
267
286
|
|
|
287
|
+
When diagnosing or testing against an app, first read the default port from `PORT` or `./.proteum/manifest.json` and check whether a server is already running there. If it is, inspect the existing traces before reproducing the issue so you can collect past errors and their context.
|
|
288
|
+
|
|
268
289
|
- `proteum trace requests`: list the most recent request summaries
|
|
269
290
|
- `proteum trace latest`: show the latest captured request
|
|
270
291
|
- `proteum trace show <requestId>`: inspect one trace in detail
|
|
@@ -275,19 +296,18 @@ Proteum includes a dev-only in-memory request trace buffer for routing, controll
|
|
|
275
296
|
Default behavior:
|
|
276
297
|
|
|
277
298
|
- tracing is enabled only in `profile: dev`
|
|
278
|
-
- traces live in memory and are bounded by `
|
|
299
|
+
- traces live in memory and are bounded by `TRACE_REQUESTS_LIMIT` and `TRACE_EVENTS_LIMIT`
|
|
279
300
|
- payloads are summarized, long strings are truncated, and sensitive fields such as cookies, passwords, and tokens are redacted
|
|
280
|
-
- `
|
|
301
|
+
- `TRACE_PERSIST_ON_ERROR` can export crashing requests under `var/traces/`
|
|
281
302
|
|
|
282
|
-
|
|
303
|
+
Trace env example:
|
|
283
304
|
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
persistOnError: true
|
|
305
|
+
```bash
|
|
306
|
+
export TRACE_ENABLE=true
|
|
307
|
+
export TRACE_REQUESTS_LIMIT=200
|
|
308
|
+
export TRACE_EVENTS_LIMIT=800
|
|
309
|
+
export TRACE_CAPTURE=resolve
|
|
310
|
+
export TRACE_PERSIST_ON_ERROR=true
|
|
291
311
|
```
|
|
292
312
|
|
|
293
313
|
Capture modes:
|
|
@@ -312,7 +332,7 @@ Proteum is built so an agent can answer these questions quickly and reliably:
|
|
|
312
332
|
Proteum answers those questions with explicit artifacts:
|
|
313
333
|
|
|
314
334
|
- `identity.yaml` for app identity
|
|
315
|
-
- `
|
|
335
|
+
- `PORT`, `ENV_*`, `URL`, and `TRACE_*` env vars for the environment surface
|
|
316
336
|
- `server/index.ts` for the explicit root service graph
|
|
317
337
|
- `.proteum/manifest.json` for machine-readable app structure
|
|
318
338
|
- `proteum explain --json` for structured framework introspection
|
|
@@ -321,7 +341,7 @@ Proteum answers those questions with explicit artifacts:
|
|
|
321
341
|
If you are an LLM or automation agent, start here:
|
|
322
342
|
|
|
323
343
|
1. Read `identity.yaml`.
|
|
324
|
-
2. Read `env
|
|
344
|
+
2. Read `PORT`, the relevant `ENV_*`, `URL`, and `TRACE_*` env vars, or run `proteum explain env`.
|
|
325
345
|
3. Inspect `server/index.ts` and `server/config/*.ts` for the explicit app bootstrap.
|
|
326
346
|
4. Read `.proteum/manifest.json` or run `proteum explain --json`.
|
|
327
347
|
5. Inspect `server/controllers/**` for request entrypoints.
|
|
@@ -12,7 +12,8 @@ When you enter a Proteum app, inspect it in this order:
|
|
|
12
12
|
2. Inspect `./server/index.ts` and `./server/config/*.ts`.
|
|
13
13
|
3. Inspect the touched `./server/controllers/**/*.ts`, `./server/services/**`, `./server/routes/**`, and `./client/pages/**` files.
|
|
14
14
|
4. Run `npx proteum doctor` if routing or generation looks suspicious.
|
|
15
|
-
5.
|
|
15
|
+
5. If you need to diagnose or test against a running app, check the default port in `PORT` or `./.proteum/manifest.json` first.
|
|
16
|
+
6. If a server is already running on that port, use `npx proteum trace` to inspect past requests, errors, and their context before reproducing the issue or adding temporary logs.
|
|
16
17
|
|
|
17
18
|
## Non-Negotiable Rules
|
|
18
19
|
|
|
@@ -37,7 +38,7 @@ Proteum reads these source files directly:
|
|
|
37
38
|
|
|
38
39
|
- `package.json`
|
|
39
40
|
- `identity.yaml`
|
|
40
|
-
- `env
|
|
41
|
+
- `process.env` via the `PORT`, `ENV_*`, `URL`, and `TRACE_*` env contract
|
|
41
42
|
- `server/config/*.ts`
|
|
42
43
|
- `server/index.ts`
|
|
43
44
|
- `server/services/**/service.json`
|
|
@@ -238,12 +239,17 @@ Relevant aliases:
|
|
|
238
239
|
|
|
239
240
|
1. Run `npx proteum explain --json`.
|
|
240
241
|
2. Run `npx proteum doctor`.
|
|
241
|
-
3.
|
|
242
|
-
|
|
242
|
+
3. Read the default port from `PORT` or `./.proteum/manifest.json` and check whether a server is already running there.
|
|
243
|
+
4. If a server is already running on that default port, inspect existing traces first:
|
|
244
|
+
- `npx proteum trace requests --port <envPort>`
|
|
245
|
+
- `npx proteum trace latest --port <envPort>`
|
|
246
|
+
- `npx proteum trace show <requestId> --port <envPort>` when you need the full context for a past error
|
|
247
|
+
5. If the issue is request-time behavior in dev and the existing traces are not enough, run:
|
|
248
|
+
- `npx proteum trace arm --capture deep --port <envPort>`
|
|
243
249
|
- reproduce the failing request once
|
|
244
|
-
- `npx proteum trace latest
|
|
245
|
-
|
|
246
|
-
|
|
250
|
+
- `npx proteum trace latest --port <envPort>` or `npx proteum trace show <requestId> --port <envPort>`
|
|
251
|
+
6. Inspect the touched controller, service, route, or page source.
|
|
252
|
+
7. Only add temporary logging if the trace is insufficient.
|
|
247
253
|
|
|
248
254
|
For the full trace reference, see `node_modules/proteum/docs/request-tracing.md` in installed apps or `docs/request-tracing.md` in the framework repository.
|
|
249
255
|
|
|
@@ -274,6 +280,12 @@ Verify at the correct layer:
|
|
|
274
280
|
- SSR changes: load the real page and inspect rendered HTML plus browser console
|
|
275
281
|
- router/plugin changes: verify request context, auth, redirects, metrics, and validation on a running app
|
|
276
282
|
|
|
283
|
+
When you need to diagnose or test against an app that may already be running:
|
|
284
|
+
|
|
285
|
+
- read the default port from `PORT` or `./.proteum/manifest.json`
|
|
286
|
+
- check whether a server is already running on that port
|
|
287
|
+
- if it is, inspect `proteum trace requests`, `proteum trace latest`, and `proteum trace show <requestId>` before reproducing the issue
|
|
288
|
+
|
|
277
289
|
Useful app commands:
|
|
278
290
|
|
|
279
291
|
- `proteum dev`
|
package/agents/project/AGENTS.md
CHANGED
|
@@ -25,6 +25,12 @@ For request-time issues in dev, inspect traces before adding temporary logs:
|
|
|
25
25
|
- `npx proteum trace latest`
|
|
26
26
|
- `npx proteum trace arm --capture deep`
|
|
27
27
|
|
|
28
|
+
If you need to diagnose or test against a running app:
|
|
29
|
+
|
|
30
|
+
- read the default port from `PORT` or `./.proteum/manifest.json`
|
|
31
|
+
- check whether a server is already running on that port
|
|
32
|
+
- if it is, inspect existing traces first to collect past errors and their context before reproducing the issue
|
|
33
|
+
|
|
28
34
|
## Project Structure
|
|
29
35
|
|
|
30
36
|
This is a full-stack monolith project using TypeScript, Node.js, Preact, and Proteum.
|
|
@@ -77,13 +83,13 @@ When a feature depends on a curated list, keep one canonical catalog or registry
|
|
|
77
83
|
1. evaluate or quantify the probability
|
|
78
84
|
2. explain why
|
|
79
85
|
3. suggest how to fix it
|
|
80
|
-
- When the issue is request-time behavior in dev, prefer `npx proteum trace`
|
|
86
|
+
- When the issue is request-time behavior in dev, first check whether a server is already running on the default port from `PORT` or `./.proteum/manifest.json`. If it is, prefer `npx proteum trace` to inspect past errors and their context before reproducing the issue or adding logs.
|
|
81
87
|
- When you have finished your work, summarize in one top-level short sentence the changes you made since the beginning of the conversation. Output as `Commit message`.
|
|
82
88
|
|
|
83
89
|
## High-Impact Files
|
|
84
90
|
|
|
85
91
|
- `tsconfig*.json`
|
|
86
|
-
- `env
|
|
92
|
+
- `PORT`, `ENV_*`, `URL`, and `TRACE_*` env setup
|
|
87
93
|
- Prisma-generated files
|
|
88
94
|
- symbolic links
|
|
89
95
|
|
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');
|
|
@@ -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/explain.ts
CHANGED
|
@@ -115,10 +115,11 @@ const printSection = (title: string, lines: string[]) => {
|
|
|
115
115
|
const renderSummary = (manifest: TProteumManifest) => {
|
|
116
116
|
const errorsCount = manifest.diagnostics.filter((diagnostic) => diagnostic.level === 'error').length;
|
|
117
117
|
const warningsCount = manifest.diagnostics.filter((diagnostic) => diagnostic.level === 'warning').length;
|
|
118
|
+
const providedRequiredEnvVariables = manifest.env.requiredVariables.filter((variable) => variable.provided).length;
|
|
118
119
|
const lines = [
|
|
119
120
|
`Proteum manifest: ${formatFilepath(manifest, path.join(manifest.app.root, '.proteum', 'manifest.json'))}`,
|
|
120
121
|
`App: ${manifest.app.identity.name} (${manifest.app.identity.identifier})`,
|
|
121
|
-
`Env
|
|
122
|
+
`Env vars: ${providedRequiredEnvVariables}/${manifest.env.requiredVariables.length} required provided`,
|
|
122
123
|
`Services: ${manifest.services.app.length} app, ${manifest.services.routerPlugins.length} router plugins`,
|
|
123
124
|
`Controllers: ${manifest.controllers.length}`,
|
|
124
125
|
`Routes: ${manifest.routes.client.length} client, ${manifest.routes.server.length} server`,
|
|
@@ -163,9 +164,16 @@ const renderHuman = (manifest: TProteumManifest, sectionNames: TExplainSectionNa
|
|
|
163
164
|
if (sectionName === 'env') {
|
|
164
165
|
sections.push(
|
|
165
166
|
printSection('Env', [
|
|
166
|
-
`- source=${
|
|
167
|
-
`-
|
|
168
|
-
|
|
167
|
+
`- source=${manifest.env.source}`,
|
|
168
|
+
`- loadedVariableKeys=${manifest.env.loadedVariableKeys.join(', ') || 'none'}`,
|
|
169
|
+
...manifest.env.requiredVariables.map(
|
|
170
|
+
(variable) =>
|
|
171
|
+
`- ${variable.key} possibleValues=${variable.possibleValues.join(' | ')} provided=${variable.provided ? 'yes' : 'no'}`,
|
|
172
|
+
),
|
|
173
|
+
`- resolved.name=${manifest.env.resolved.name}`,
|
|
174
|
+
`- resolved.profile=${manifest.env.resolved.profile}`,
|
|
175
|
+
`- resolved.routerPort=${manifest.env.resolved.routerPort}`,
|
|
176
|
+
`- resolved.routerCurrentDomain=${manifest.env.resolved.routerCurrentDomain}`,
|
|
169
177
|
]),
|
|
170
178
|
);
|
|
171
179
|
continue;
|
package/cli/commands/trace.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import got from 'got';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import yaml from 'yaml';
|
|
5
4
|
import { UsageError } from 'clipanion';
|
|
6
5
|
|
|
7
6
|
import cli from '..';
|
|
@@ -12,7 +11,7 @@ import type {
|
|
|
12
11
|
TRequestTraceListItem,
|
|
13
12
|
TRequestTraceListResponse,
|
|
14
13
|
TRequestTraceResponse,
|
|
15
|
-
} from '
|
|
14
|
+
} from '../../common/dev/requestTrace';
|
|
16
15
|
|
|
17
16
|
type TTraceAction = 'latest' | 'show' | 'requests' | 'arm' | 'export';
|
|
18
17
|
|
|
@@ -31,22 +30,33 @@ const getAction = () => {
|
|
|
31
30
|
|
|
32
31
|
const normalizeBaseUrl = (value: string) => value.replace(/\/+$/, '');
|
|
33
32
|
|
|
33
|
+
const getRouterPortFromManifest = () => {
|
|
34
|
+
const manifestFilepath = path.join(cli.args.workdir as string, '.proteum', 'manifest.json');
|
|
35
|
+
if (!fs.existsSync(manifestFilepath)) return undefined;
|
|
36
|
+
|
|
37
|
+
const manifest = fs.readJsonSync(manifestFilepath, { throws: false }) as
|
|
38
|
+
| { env?: { resolved?: { routerPort?: number } } }
|
|
39
|
+
| undefined;
|
|
40
|
+
const port = manifest?.env?.resolved?.routerPort;
|
|
41
|
+
|
|
42
|
+
if (typeof port !== 'number' || port <= 0) return undefined;
|
|
43
|
+
|
|
44
|
+
return String(port);
|
|
45
|
+
};
|
|
46
|
+
|
|
34
47
|
const getRouterPort = () => {
|
|
35
48
|
const overridePort = typeof cli.args.port === 'string' && cli.args.port ? cli.args.port : '';
|
|
36
49
|
if (overridePort) return overridePort;
|
|
37
50
|
|
|
38
|
-
const
|
|
39
|
-
if (
|
|
40
|
-
throw new UsageError(`Could not find env.yaml in ${cli.args.workdir as string}. Pass --port or --url explicitly.`);
|
|
41
|
-
}
|
|
51
|
+
const envPort = process.env.PORT?.trim();
|
|
52
|
+
if (envPort) return envPort;
|
|
42
53
|
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
if (!port) {
|
|
46
|
-
throw new UsageError(`Could not determine the router port from ${envFilepath}. Pass --port or --url explicitly.`);
|
|
47
|
-
}
|
|
54
|
+
const manifestPort = getRouterPortFromManifest();
|
|
55
|
+
if (manifestPort) return manifestPort;
|
|
48
56
|
|
|
49
|
-
|
|
57
|
+
throw new UsageError(
|
|
58
|
+
`Could not determine the router port from PORT or .proteum/manifest.json in ${cli.args.workdir as string}. Pass --port or --url explicitly.`,
|
|
59
|
+
);
|
|
50
60
|
};
|
|
51
61
|
|
|
52
62
|
const getRouterBaseUrls = () => {
|
|
@@ -1,9 +1,10 @@
|
|
|
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,
|
|
@@ -15,20 +16,6 @@ import {
|
|
|
15
16
|
import { writeProteumManifest } from '../common/proteumManifest';
|
|
16
17
|
import { normalizeAbsolutePath, normalizePath } from './shared';
|
|
17
18
|
|
|
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
19
|
const collectManifestDiagnostics = ({
|
|
33
20
|
controllers,
|
|
34
21
|
routes,
|
|
@@ -228,6 +215,8 @@ export const writeCurrentProteumManifest = ({
|
|
|
228
215
|
routes: TProteumManifest['routes'];
|
|
229
216
|
layouts: TProteumManifestLayout[];
|
|
230
217
|
}) => {
|
|
218
|
+
const envInspection = inspectProteumEnv(app.paths.root);
|
|
219
|
+
|
|
231
220
|
const manifest: TProteumManifest = {
|
|
232
221
|
version: 1,
|
|
233
222
|
app: {
|
|
@@ -252,9 +241,19 @@ export const writeCurrentProteumManifest = ({
|
|
|
252
241
|
reservedRouteSetupKeys: [...reservedRouteSetupKeys],
|
|
253
242
|
},
|
|
254
243
|
env: {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
244
|
+
source: 'process.env',
|
|
245
|
+
loadedVariableKeys: envInspection.loadedVariableKeys,
|
|
246
|
+
requiredVariables: envInspection.requiredVariables.map((variable) => ({
|
|
247
|
+
key: variable.key,
|
|
248
|
+
possibleValues: [...variable.possibleValues],
|
|
249
|
+
provided: variable.provided,
|
|
250
|
+
})),
|
|
251
|
+
resolved: {
|
|
252
|
+
name: app.env.name,
|
|
253
|
+
profile: app.env.profile,
|
|
254
|
+
routerPort: app.env.router.port,
|
|
255
|
+
routerCurrentDomain: app.env.router.currentDomain,
|
|
256
|
+
},
|
|
258
257
|
},
|
|
259
258
|
services,
|
|
260
259
|
controllers,
|
|
@@ -80,7 +80,7 @@ export default function createCommonConfig(
|
|
|
80
80
|
APP_NAME: JSON.stringify(app.identity.web.title),
|
|
81
81
|
APP_OUTPUT_DIR: JSON.stringify(path.basename(app.outputPath(outputTarget))),
|
|
82
82
|
PROTEUM_DEV_EVENT_PORT: JSON.stringify(dev ? (app.devEventPort ?? null) : null),
|
|
83
|
-
|
|
83
|
+
PROTEUM_PORT_OVERRIDE: JSON.stringify(app.routerPortOverride ?? null),
|
|
84
84
|
}),
|
|
85
85
|
|
|
86
86
|
...(dev ? [] : []),
|
|
@@ -100,9 +100,19 @@ export type TProteumManifest = {
|
|
|
100
100
|
reservedRouteSetupKeys: string[];
|
|
101
101
|
};
|
|
102
102
|
env: {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
+
};
|
|
106
116
|
};
|
|
107
117
|
services: {
|
|
108
118
|
app: TProteumManifestService[];
|
|
@@ -202,7 +202,7 @@ export const proteumCommands: Record<TProteumCommandName, TProteumCommandDoc> =
|
|
|
202
202
|
notes: [
|
|
203
203
|
'This command talks to the running app over the dev-only `__proteum/trace` HTTP endpoints.',
|
|
204
204
|
'Traces are stored in a bounded in-memory buffer with payload summarization and sensitive-field redaction.',
|
|
205
|
-
'Use `--port` when the app is not running on the router port declared in `
|
|
205
|
+
'Use `--port` when the app is not running on the router port declared in `PORT`, or `--url` when the host itself is non-standard.',
|
|
206
206
|
],
|
|
207
207
|
status: 'experimental',
|
|
208
208
|
},
|
package/client/app/component.tsx
CHANGED
|
@@ -38,16 +38,9 @@ export default function App({ context }: { context: ClientContext }) {
|
|
|
38
38
|
<DialogManager />
|
|
39
39
|
|
|
40
40
|
{!layout ? (
|
|
41
|
-
|
|
42
|
-
{/* TODO: move to app, because here, we're not aware that the router service has been defined */}
|
|
43
|
-
<RouterComponent service={context.Router} />
|
|
44
|
-
</>
|
|
41
|
+
<RouterComponent service={context.Router} />
|
|
45
42
|
) : (
|
|
46
|
-
|
|
47
|
-
{' '}
|
|
48
|
-
{/* Same as router/components/Page.tsx */}
|
|
49
|
-
<layout.Component {...layoutProps} />
|
|
50
|
-
</>
|
|
43
|
+
<layout.Component {...layoutProps} />
|
|
51
44
|
)}
|
|
52
45
|
</ReactClientContext.Provider>
|
|
53
46
|
);
|
|
@@ -21,7 +21,6 @@ import BaseRouter, {
|
|
|
21
21
|
TErrorRoute,
|
|
22
22
|
TRouteOptions,
|
|
23
23
|
TRouteModule,
|
|
24
|
-
TDomainsList,
|
|
25
24
|
matchRoute,
|
|
26
25
|
buildUrl,
|
|
27
26
|
} from '@common/router';
|
|
@@ -136,7 +135,7 @@ export default class ClientRouter<
|
|
|
136
135
|
// Context data
|
|
137
136
|
public ssrRoutes = browserWindow.routes || [];
|
|
138
137
|
public ssrContext = browserWindow.ssr;
|
|
139
|
-
public
|
|
138
|
+
public currentDomain = browserWindow.ssr?.currentDomain || window.location.origin;
|
|
140
139
|
public context!: TRouterContext<this, this['app']>;
|
|
141
140
|
|
|
142
141
|
public setLoading!: React.Dispatch<React.SetStateAction<boolean>>;
|
|
@@ -153,7 +152,7 @@ export default class ClientRouter<
|
|
|
153
152
|
}
|
|
154
153
|
|
|
155
154
|
public url = (path: string, params: {} = {}, absolute: boolean = true) =>
|
|
156
|
-
buildUrl(path, params, this.
|
|
155
|
+
buildUrl(path, params, this.currentDomain, absolute);
|
|
157
156
|
|
|
158
157
|
public go(url: string | number, data: {} = {}, opt: { newTab?: boolean } = {}) {
|
|
159
158
|
// Error code
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export const traceCaptureModes = ['summary', 'resolve', 'deep'] as const;
|
|
2
2
|
|
|
3
3
|
export type TTraceCaptureMode = (typeof traceCaptureModes)[number];
|
|
4
|
+
type TTracePrimitive = string | number | boolean;
|
|
4
5
|
|
|
5
6
|
export const traceEventTypes = [
|
|
6
7
|
'request.start',
|
|
@@ -27,7 +28,7 @@ export const traceEventTypes = [
|
|
|
27
28
|
export type TTraceEventType = (typeof traceEventTypes)[number];
|
|
28
29
|
|
|
29
30
|
export type TTraceSummaryValue =
|
|
30
|
-
|
|
|
31
|
+
| TTracePrimitive
|
|
31
32
|
| null
|
|
32
33
|
| { kind: 'undefined' }
|
|
33
34
|
| { kind: 'redacted'; reason: string }
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import dotenv from 'dotenv';
|
|
4
|
+
|
|
5
|
+
export type TProteumEnvName = 'local' | 'server';
|
|
6
|
+
export type TProteumEnvProfile = 'dev' | 'testing' | 'prod';
|
|
7
|
+
export type TProteumTraceCapture = 'summary' | 'resolve' | 'deep';
|
|
8
|
+
export type TProteumRequiredEnvVariable = {
|
|
9
|
+
key: TProteumRequiredEnvVariableKey;
|
|
10
|
+
possibleValues: string[];
|
|
11
|
+
provided: boolean;
|
|
12
|
+
};
|
|
13
|
+
export type TProteumEnvInspection = {
|
|
14
|
+
loadedVariableKeys: string[];
|
|
15
|
+
requiredVariables: TProteumRequiredEnvVariable[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type TProteumEnvConfig = {
|
|
19
|
+
name: TProteumEnvName;
|
|
20
|
+
profile: TProteumEnvProfile;
|
|
21
|
+
router: {
|
|
22
|
+
port: number;
|
|
23
|
+
currentDomain: string;
|
|
24
|
+
};
|
|
25
|
+
trace: {
|
|
26
|
+
enable: boolean;
|
|
27
|
+
requestsLimit: number;
|
|
28
|
+
eventsLimit: number;
|
|
29
|
+
capture: TProteumTraceCapture;
|
|
30
|
+
persistOnError: boolean;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type TProteumLoadedEnvConfig = TProteumEnvConfig & { version: string };
|
|
35
|
+
|
|
36
|
+
const dotenvFileNames = ['.env'];
|
|
37
|
+
const requiredProteumEnvVariableKeys = ['ENV_NAME', 'ENV_PROFILE', 'PORT', 'URL'] as const;
|
|
38
|
+
const optionalProteumEnvVariablePrefixes = ['TRACE_'] as const;
|
|
39
|
+
|
|
40
|
+
export type TProteumRequiredEnvVariableKey = (typeof requiredProteumEnvVariableKeys)[number];
|
|
41
|
+
|
|
42
|
+
const requiredProteumEnvVariablePossibleValues: Record<TProteumRequiredEnvVariableKey, string[]> = {
|
|
43
|
+
ENV_NAME: ['local', 'server'],
|
|
44
|
+
ENV_PROFILE: ['dev', 'testing', 'prod'],
|
|
45
|
+
PORT: ['integer between 1 and 65535'],
|
|
46
|
+
URL: ['absolute URL'],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const envDefinitionHint = (appDir: string) => `Define it in process.env or ${appDir}/.env.`;
|
|
50
|
+
const isProvidedEnvValue = (value: string | undefined) => typeof value === 'string' && value.trim() !== '';
|
|
51
|
+
|
|
52
|
+
const formatRequiredEnvVariableStatus = (variable: TProteumRequiredEnvVariable) =>
|
|
53
|
+
`- ${variable.key} possibleValues=${variable.possibleValues.join(' | ')} provided=${variable.provided ? 'yes' : 'no'}`;
|
|
54
|
+
|
|
55
|
+
const createProteumEnvError = ({
|
|
56
|
+
appDir,
|
|
57
|
+
message,
|
|
58
|
+
}: {
|
|
59
|
+
appDir: string;
|
|
60
|
+
message: string;
|
|
61
|
+
}) => {
|
|
62
|
+
const inspection = inspectProteumEnv(appDir);
|
|
63
|
+
|
|
64
|
+
return new Error(
|
|
65
|
+
[message, envDefinitionHint(appDir), '', 'Required env variables:', ...inspection.requiredVariables.map(formatRequiredEnvVariableStatus)].join(
|
|
66
|
+
'\n',
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const parseBooleanEnvValue = ({
|
|
72
|
+
key,
|
|
73
|
+
value,
|
|
74
|
+
appDir,
|
|
75
|
+
}: {
|
|
76
|
+
key: string;
|
|
77
|
+
value: string | undefined;
|
|
78
|
+
appDir: string;
|
|
79
|
+
}) => {
|
|
80
|
+
if (value === undefined || value === '') return undefined;
|
|
81
|
+
|
|
82
|
+
const normalized = value.trim().toLowerCase();
|
|
83
|
+
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
|
|
84
|
+
if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
|
|
85
|
+
|
|
86
|
+
throw createProteumEnvError({
|
|
87
|
+
appDir,
|
|
88
|
+
message: `Invalid boolean value for ${key}: "${value}". Expected one of: 1, 0, true, false, yes, no, on, off.`,
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const parseIntegerEnvValue = ({
|
|
93
|
+
key,
|
|
94
|
+
value,
|
|
95
|
+
appDir,
|
|
96
|
+
min = 1,
|
|
97
|
+
}: {
|
|
98
|
+
key: string;
|
|
99
|
+
value: string;
|
|
100
|
+
appDir: string;
|
|
101
|
+
min?: number;
|
|
102
|
+
}) => {
|
|
103
|
+
const parsed = Number.parseInt(value, 10);
|
|
104
|
+
if (Number.isNaN(parsed) || parsed < min) {
|
|
105
|
+
throw createProteumEnvError({
|
|
106
|
+
appDir,
|
|
107
|
+
message: `Invalid integer value for ${key}: "${value}". Expected an integer greater than or equal to ${min}.`,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return parsed;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const getRequiredEnvValue = ({ key, appDir }: { key: TProteumRequiredEnvVariableKey; appDir: string }) => {
|
|
115
|
+
const value = process.env[key]?.trim();
|
|
116
|
+
if (value) return value;
|
|
117
|
+
|
|
118
|
+
throw createProteumEnvError({
|
|
119
|
+
appDir,
|
|
120
|
+
message: `Missing required Proteum env variable "${key}".`,
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const parseEnvName = (value: string, appDir: string): TProteumEnvName => {
|
|
125
|
+
if (value === 'local' || value === 'server') return value;
|
|
126
|
+
throw createProteumEnvError({
|
|
127
|
+
appDir,
|
|
128
|
+
message: `Invalid ENV_NAME "${value}". Expected "local" or "server".`,
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const parseEnvProfile = (value: string, appDir: string): TProteumEnvProfile => {
|
|
133
|
+
if (value === 'dev' || value === 'testing' || value === 'prod') return value;
|
|
134
|
+
throw createProteumEnvError({
|
|
135
|
+
appDir,
|
|
136
|
+
message: `Invalid ENV_PROFILE "${value}". Expected "dev", "testing", or "prod".`,
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const parseTraceCapture = ({
|
|
141
|
+
value,
|
|
142
|
+
appDir,
|
|
143
|
+
}: {
|
|
144
|
+
value: string | undefined;
|
|
145
|
+
appDir: string;
|
|
146
|
+
}): TProteumTraceCapture | undefined => {
|
|
147
|
+
if (value === undefined || value === '') return undefined;
|
|
148
|
+
if (value === 'summary' || value === 'resolve' || value === 'deep') return value;
|
|
149
|
+
|
|
150
|
+
throw createProteumEnvError({
|
|
151
|
+
appDir,
|
|
152
|
+
message: `Invalid TRACE_CAPTURE "${value}". Expected "summary", "resolve", or "deep".`,
|
|
153
|
+
});
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const parseAbsoluteUrl = ({
|
|
157
|
+
key,
|
|
158
|
+
value,
|
|
159
|
+
appDir,
|
|
160
|
+
}: {
|
|
161
|
+
key: string;
|
|
162
|
+
value: string;
|
|
163
|
+
appDir: string;
|
|
164
|
+
}) => {
|
|
165
|
+
let url: URL;
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
url = new URL(value);
|
|
169
|
+
} catch {
|
|
170
|
+
throw createProteumEnvError({
|
|
171
|
+
appDir,
|
|
172
|
+
message: `Invalid absolute URL for ${key}: "${value}". Expected an absolute http:// or https:// URL.`,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
177
|
+
throw createProteumEnvError({
|
|
178
|
+
appDir,
|
|
179
|
+
message: `Invalid absolute URL for ${key}: "${value}". Expected an absolute http:// or https:// URL.`,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return value;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export const loadOptionalProteumDotenv = (appDir: string) => {
|
|
187
|
+
for (const filename of dotenvFileNames) {
|
|
188
|
+
const filepath = path.join(appDir, filename);
|
|
189
|
+
if (!fs.existsSync(filepath)) continue;
|
|
190
|
+
dotenv.config({ path: filepath, quiet: true });
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export const getLoadedProteumEnvVariableKeys = () =>
|
|
195
|
+
Object.keys(process.env)
|
|
196
|
+
.filter(
|
|
197
|
+
(key) =>
|
|
198
|
+
requiredProteumEnvVariableKeys.includes(key as TProteumRequiredEnvVariableKey) ||
|
|
199
|
+
optionalProteumEnvVariablePrefixes.some((prefix) => key.startsWith(prefix)),
|
|
200
|
+
)
|
|
201
|
+
.sort((a, b) => a.localeCompare(b));
|
|
202
|
+
|
|
203
|
+
export const inspectProteumEnv = (appDir: string): TProteumEnvInspection => {
|
|
204
|
+
loadOptionalProteumDotenv(appDir);
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
loadedVariableKeys: getLoadedProteumEnvVariableKeys(),
|
|
208
|
+
requiredVariables: requiredProteumEnvVariableKeys.map((key) => ({
|
|
209
|
+
key,
|
|
210
|
+
possibleValues: [...requiredProteumEnvVariablePossibleValues[key]],
|
|
211
|
+
provided: isProvidedEnvValue(process.env[key]),
|
|
212
|
+
})),
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
export const parseProteumEnvConfig = ({
|
|
217
|
+
appDir,
|
|
218
|
+
routerPortOverride,
|
|
219
|
+
}: {
|
|
220
|
+
appDir: string;
|
|
221
|
+
routerPortOverride?: number;
|
|
222
|
+
}): TProteumEnvConfig => {
|
|
223
|
+
loadOptionalProteumDotenv(appDir);
|
|
224
|
+
|
|
225
|
+
const name = parseEnvName(getRequiredEnvValue({ key: 'ENV_NAME', appDir }), appDir);
|
|
226
|
+
const profile = parseEnvProfile(getRequiredEnvValue({ key: 'ENV_PROFILE', appDir }), appDir);
|
|
227
|
+
const configuredRouterPort = parseIntegerEnvValue({
|
|
228
|
+
key: 'PORT',
|
|
229
|
+
value: getRequiredEnvValue({ key: 'PORT', appDir }),
|
|
230
|
+
appDir,
|
|
231
|
+
});
|
|
232
|
+
const currentDomain = parseAbsoluteUrl({
|
|
233
|
+
key: 'URL',
|
|
234
|
+
value: getRequiredEnvValue({ key: 'URL', appDir }),
|
|
235
|
+
appDir,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const traceEnable = parseBooleanEnvValue({
|
|
239
|
+
key: 'TRACE_ENABLE',
|
|
240
|
+
value: process.env.TRACE_ENABLE,
|
|
241
|
+
appDir,
|
|
242
|
+
});
|
|
243
|
+
const tracePersistOnError = parseBooleanEnvValue({
|
|
244
|
+
key: 'TRACE_PERSIST_ON_ERROR',
|
|
245
|
+
value: process.env.TRACE_PERSIST_ON_ERROR,
|
|
246
|
+
appDir,
|
|
247
|
+
});
|
|
248
|
+
const traceRequestsLimit = process.env.TRACE_REQUESTS_LIMIT?.trim();
|
|
249
|
+
const traceEventsLimit = process.env.TRACE_EVENTS_LIMIT?.trim();
|
|
250
|
+
const traceCapture = parseTraceCapture({
|
|
251
|
+
value: process.env.TRACE_CAPTURE?.trim(),
|
|
252
|
+
appDir,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
name,
|
|
257
|
+
profile,
|
|
258
|
+
router: {
|
|
259
|
+
port: routerPortOverride === undefined ? configuredRouterPort : routerPortOverride,
|
|
260
|
+
currentDomain,
|
|
261
|
+
},
|
|
262
|
+
trace: {
|
|
263
|
+
enable: traceEnable ?? profile === 'dev',
|
|
264
|
+
requestsLimit:
|
|
265
|
+
traceRequestsLimit === undefined || traceRequestsLimit === ''
|
|
266
|
+
? 200
|
|
267
|
+
: parseIntegerEnvValue({
|
|
268
|
+
key: 'TRACE_REQUESTS_LIMIT',
|
|
269
|
+
value: traceRequestsLimit,
|
|
270
|
+
appDir,
|
|
271
|
+
}),
|
|
272
|
+
eventsLimit:
|
|
273
|
+
traceEventsLimit === undefined || traceEventsLimit === ''
|
|
274
|
+
? 800
|
|
275
|
+
: parseIntegerEnvValue({
|
|
276
|
+
key: 'TRACE_EVENTS_LIMIT',
|
|
277
|
+
value: traceEventsLimit,
|
|
278
|
+
appDir,
|
|
279
|
+
}),
|
|
280
|
+
capture: traceCapture ?? 'resolve',
|
|
281
|
+
persistOnError: tracePersistOnError ?? profile === 'dev',
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
};
|
package/common/router/index.ts
CHANGED
|
@@ -106,8 +106,6 @@ export type TRouteModule<TRegisteredRoute = any> = {
|
|
|
106
106
|
__register: TAppArrowFunction<TRegisteredRoute>;
|
|
107
107
|
};
|
|
108
108
|
|
|
109
|
-
export type TDomainsList = { [endpointId: string]: string } & { current: string };
|
|
110
|
-
|
|
111
109
|
export const defaultOptions: Pick<TRouteOptions, 'priority'> = { priority: 0 };
|
|
112
110
|
|
|
113
111
|
/*----------------------------------
|
|
@@ -116,31 +114,15 @@ export const defaultOptions: Pick<TRouteOptions, 'priority'> = { priority: 0 };
|
|
|
116
114
|
export const buildUrl = (
|
|
117
115
|
path: string,
|
|
118
116
|
params: { [key: string]: any },
|
|
119
|
-
|
|
117
|
+
currentDomain: string,
|
|
120
118
|
absolute: boolean,
|
|
121
119
|
) => {
|
|
122
120
|
let prefix: string = '';
|
|
123
121
|
|
|
124
122
|
// Relative to domain
|
|
125
|
-
if (path[0] === '/' && absolute) prefix =
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
// Extract domain ID from path
|
|
129
|
-
let domainId: string;
|
|
130
|
-
let slackPos = path.indexOf('/');
|
|
131
|
-
if (slackPos === -1) slackPos = path.length;
|
|
132
|
-
domainId = path.substring(1, slackPos);
|
|
133
|
-
path = path.substring(slackPos);
|
|
134
|
-
|
|
135
|
-
// Get domain
|
|
136
|
-
const domain = domains[domainId];
|
|
137
|
-
if (domain === undefined) throw new Error('Unknown API endpoint ID: ' + domainId);
|
|
138
|
-
|
|
139
|
-
// Return full url
|
|
140
|
-
prefix = domain;
|
|
141
|
-
|
|
142
|
-
// Absolute URL
|
|
143
|
-
}
|
|
123
|
+
if (path[0] === '/' && absolute) prefix = currentDomain;
|
|
124
|
+
else if (path[0] === '@')
|
|
125
|
+
throw new Error(`Proteum no longer supports Router.url() domain aliases. Use a root-relative path or absolute URL instead: "${path}".`);
|
|
144
126
|
|
|
145
127
|
// Path parapeters
|
|
146
128
|
const searchParams = new URLSearchParams();
|
package/docs/request-tracing.md
CHANGED
|
@@ -19,6 +19,12 @@ proteum trace export <requestId>
|
|
|
19
19
|
proteum trace latest --url http://127.0.0.1:3010
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
+
Before reproducing a bug or starting a new test pass:
|
|
23
|
+
|
|
24
|
+
- read the default port from `PORT` or `./.proteum/manifest.json`
|
|
25
|
+
- check whether a dev server is already running on that port
|
|
26
|
+
- if it is, inspect `proteum trace requests`, `proteum trace latest`, and `proteum trace show <requestId>` first so you can capture past errors and their context
|
|
27
|
+
|
|
22
28
|
Typical debugging flow:
|
|
23
29
|
|
|
24
30
|
```bash
|
|
@@ -54,15 +60,14 @@ Use `deep` selectively. It is for one-off investigation, not continuous capture.
|
|
|
54
60
|
|
|
55
61
|
## Configuration
|
|
56
62
|
|
|
57
|
-
|
|
63
|
+
Set trace behavior with env vars:
|
|
58
64
|
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
persistOnError: true
|
|
65
|
+
```bash
|
|
66
|
+
export TRACE_ENABLE=true
|
|
67
|
+
export TRACE_REQUESTS_LIMIT=200
|
|
68
|
+
export TRACE_EVENTS_LIMIT=800
|
|
69
|
+
export TRACE_CAPTURE=resolve
|
|
70
|
+
export TRACE_PERSIST_ON_ERROR=true
|
|
66
71
|
```
|
|
67
72
|
|
|
68
73
|
Notes:
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "proteum",
|
|
3
3
|
"description": "LLM-first Opinionated Typescript Framework for web applications.",
|
|
4
|
-
"version": "2.1.0-
|
|
4
|
+
"version": "2.1.0-5",
|
|
5
5
|
"author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
|
|
6
6
|
"repository": "git://github.com/gaetanlegac/proteum.git",
|
|
7
7
|
"license": "MIT",
|
|
@@ -12,10 +12,10 @@ import fs from 'fs-extra';
|
|
|
12
12
|
import yaml from 'yaml';
|
|
13
13
|
|
|
14
14
|
// Types
|
|
15
|
-
import type
|
|
16
|
-
import type { TLogProfile } from './console';
|
|
15
|
+
import { parseProteumEnvConfig, type TProteumLoadedEnvConfig } from '../../../common/env/proteumEnv';
|
|
17
16
|
|
|
18
|
-
declare const
|
|
17
|
+
declare const PROTEUM_PORT_OVERRIDE: number | null;
|
|
18
|
+
declare const BUILD_DATE: string;
|
|
19
19
|
|
|
20
20
|
/*----------------------------------
|
|
21
21
|
- TYPES
|
|
@@ -31,50 +31,8 @@ declare global {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
/*
|
|
35
|
-
name: server
|
|
36
|
-
profile: prod
|
|
37
|
-
|
|
38
|
-
router:
|
|
39
|
-
port: 80
|
|
40
|
-
domains:
|
|
41
|
-
current: 'https://recruiters.becrosspath.com'
|
|
42
|
-
recruiters: 'https://recruiters.becrosspath.com'
|
|
43
|
-
landing: 'https://becrosspath.com'
|
|
44
|
-
employers: 'https://employers.becrosspath.com'
|
|
45
|
-
candidates: 'https://candidates.becrosspath.com'
|
|
46
|
-
csm: 'https://csm.becrosspath.com'
|
|
47
|
-
|
|
48
|
-
database:
|
|
49
|
-
name: 'aws'
|
|
50
|
-
databases: [railway]
|
|
51
|
-
host: 'mysql-z7vp.railway.internal'
|
|
52
|
-
port: 3306
|
|
53
|
-
login: root
|
|
54
|
-
password: "GMnVsczoyYkyzwvVqDkMUOAIjVsumEev"
|
|
55
|
-
|
|
56
|
-
console:
|
|
57
|
-
enable: false
|
|
58
|
-
debug: false
|
|
59
|
-
bufferLimit: 10000
|
|
60
|
-
level: 'log'
|
|
61
|
-
*/
|
|
62
|
-
|
|
63
34
|
export type TEnvName = TEnvConfig['name'];
|
|
64
|
-
export type TEnvConfig =
|
|
65
|
-
name: 'local' | 'server';
|
|
66
|
-
profile: 'dev' | 'testing' | 'prod';
|
|
67
|
-
|
|
68
|
-
router: { port: number; domains: TDomainsList };
|
|
69
|
-
console: { enable: boolean; debug: boolean; bufferLimit: number; level: TLogProfile };
|
|
70
|
-
trace: {
|
|
71
|
-
enable: boolean;
|
|
72
|
-
requestsLimit: number;
|
|
73
|
-
eventsLimit: number;
|
|
74
|
-
capture: 'summary' | 'resolve' | 'deep';
|
|
75
|
-
persistOnError: boolean;
|
|
76
|
-
};
|
|
77
|
-
};
|
|
35
|
+
export type TEnvConfig = TProteumLoadedEnvConfig;
|
|
78
36
|
|
|
79
37
|
type AppIdentityConfig = {
|
|
80
38
|
name: string;
|
|
@@ -105,8 +63,7 @@ export type AppConfig = { env: Config.Env; identity: Config.Identity };
|
|
|
105
63
|
const debug = false;
|
|
106
64
|
|
|
107
65
|
const getRouterPortOverride = () => {
|
|
108
|
-
if (typeof
|
|
109
|
-
return PROTEUM_ROUTER_PORT_OVERRIDE;
|
|
66
|
+
if (typeof PROTEUM_PORT_OVERRIDE !== 'undefined' && PROTEUM_PORT_OVERRIDE !== null) return PROTEUM_PORT_OVERRIDE;
|
|
110
67
|
|
|
111
68
|
return undefined;
|
|
112
69
|
};
|
|
@@ -127,23 +84,13 @@ export default class ConfigParser {
|
|
|
127
84
|
}
|
|
128
85
|
|
|
129
86
|
public env(): TEnvConfig {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
debug && console.info('[app] Using environment:', process.env.NODE_ENV);
|
|
133
|
-
const envFileName = this.appDir + '/env.yaml';
|
|
134
|
-
const envFile = this.loadYaml(envFileName);
|
|
135
|
-
const routerPortOverride = getRouterPortOverride();
|
|
136
|
-
const traceConfig = envFile.trace || {};
|
|
87
|
+
debug && console.info('[app] Loading Proteum env vars from process.env');
|
|
88
|
+
|
|
137
89
|
return {
|
|
138
|
-
...
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
requestsLimit: traceConfig.requestsLimit ?? 200,
|
|
143
|
-
eventsLimit: traceConfig.eventsLimit ?? 800,
|
|
144
|
-
capture: traceConfig.capture ?? 'resolve',
|
|
145
|
-
persistOnError: traceConfig.persistOnError ?? envFile.profile === 'dev',
|
|
146
|
-
},
|
|
90
|
+
...parseProteumEnvConfig({
|
|
91
|
+
appDir: this.appDir,
|
|
92
|
+
routerPortOverride: getRouterPortOverride(),
|
|
93
|
+
}),
|
|
147
94
|
version: BUILD_DATE,
|
|
148
95
|
};
|
|
149
96
|
}
|
|
@@ -23,9 +23,10 @@ import type ServerRequest from '@server/services/router/request';
|
|
|
23
23
|
- SERVICE CONFIG
|
|
24
24
|
----------------------------------*/
|
|
25
25
|
|
|
26
|
-
export type TLogProfile = 'silly' | 'info' | 'warn' | 'error';
|
|
26
|
+
export type TLogProfile = 'silly' | 'log' | 'info' | 'warn' | 'error';
|
|
27
27
|
|
|
28
28
|
export type Config = { debug?: boolean; enable: boolean; bufferLimit: number; level: TLogProfile };
|
|
29
|
+
export const defaultConsoleConfig: Config = { enable: false, debug: false, bufferLimit: 10000, level: 'log' };
|
|
29
30
|
|
|
30
31
|
export type Hooks = {};
|
|
31
32
|
|
|
@@ -14,7 +14,7 @@ import type Application from '..';
|
|
|
14
14
|
import type { StartedServicesIndex } from '../service';
|
|
15
15
|
import Services, { ServicesContainer } from '../service/container';
|
|
16
16
|
import ConfigParser, { TEnvConfig } from './config';
|
|
17
|
-
import Console from './console';
|
|
17
|
+
import Console, { defaultConsoleConfig } from './console';
|
|
18
18
|
import Trace from './trace';
|
|
19
19
|
import type ServerRequest from '@server/services/router/request';
|
|
20
20
|
|
|
@@ -53,7 +53,7 @@ export class ApplicationContainer<TServicesIndex extends StartedServicesIndex =
|
|
|
53
53
|
const configParser = new ConfigParser(this.path.root);
|
|
54
54
|
this.Environment = configParser.env();
|
|
55
55
|
this.Identity = configParser.identity();
|
|
56
|
-
this.Console = new Console(this,
|
|
56
|
+
this.Console = new Console(this, defaultConsoleConfig);
|
|
57
57
|
this.Trace = new Trace(this, this.Environment.trace);
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -32,7 +32,6 @@ import BaseRouter, {
|
|
|
32
32
|
defaultOptions,
|
|
33
33
|
matchRoute,
|
|
34
34
|
buildUrl,
|
|
35
|
-
TDomainsList,
|
|
36
35
|
} from '@common/router';
|
|
37
36
|
import type { TSsrUnresolvedRoute, TRegisterPageArgs } from '@common/router/contracts';
|
|
38
37
|
import { buildRegex, getRegisterPageArgs } from '@common/router/register';
|
|
@@ -108,7 +107,7 @@ export type Config<
|
|
|
108
107
|
|
|
109
108
|
disk?: string; // Disk driver ID
|
|
110
109
|
|
|
111
|
-
|
|
110
|
+
currentDomain: string;
|
|
112
111
|
|
|
113
112
|
http: HttpServiceConfig;
|
|
114
113
|
|
|
@@ -356,7 +355,7 @@ export default class ServerRouter<
|
|
|
356
355
|
}
|
|
357
356
|
|
|
358
357
|
public url = (path: string, params: {} = {}, absolute: boolean = true) =>
|
|
359
|
-
buildUrl(path, params, this.config.
|
|
358
|
+
buildUrl(path, params, this.config.currentDomain, absolute);
|
|
360
359
|
|
|
361
360
|
/*----------------------------------
|
|
362
361
|
- REGISTER
|
|
@@ -14,7 +14,7 @@ import express from 'express';
|
|
|
14
14
|
import context from '@server/context';
|
|
15
15
|
import type { AnyRouterService, default as ServerRouter, TServerRouter, TAnyRouter } from '@server/services/router';
|
|
16
16
|
import ServerRequest from '@server/services/router/request';
|
|
17
|
-
import { TMatchedRoute, TRoute, TAnyRoute
|
|
17
|
+
import { TMatchedRoute, TRoute, TAnyRoute } from '@common/router';
|
|
18
18
|
import { NotFound, Forbidden, Anomaly } from '@common/errors';
|
|
19
19
|
import BaseResponse, { TResponseData } from '@common/router/response';
|
|
20
20
|
import { splitRouteSetupResult } from '@common/router/pageSetup';
|
|
@@ -38,7 +38,7 @@ export type TBasicSSrData = {
|
|
|
38
38
|
request: { data: TObjetDonnees; id: string };
|
|
39
39
|
page: { chunkId: string; data?: TObjetDonnees };
|
|
40
40
|
user: TBasicUser | null;
|
|
41
|
-
|
|
41
|
+
currentDomain: string;
|
|
42
42
|
};
|
|
43
43
|
|
|
44
44
|
type TServerRouterApplication<TRouter extends TServerRouter> =
|
|
@@ -277,7 +277,7 @@ export default class ServerResponse<
|
|
|
277
277
|
request: { id: this.request.id, data: this.request.data },
|
|
278
278
|
page: { chunkId: page.chunkId || '', data: page.data },
|
|
279
279
|
user: this.request.user,
|
|
280
|
-
|
|
280
|
+
currentDomain: this.router.config.currentDomain,
|
|
281
281
|
...customSsrData,
|
|
282
282
|
};
|
|
283
283
|
}
|
package/types/global/utils.d.ts
CHANGED
|
@@ -43,24 +43,17 @@ declare type PrimitiveValue = string | number | boolean;
|
|
|
43
43
|
/*type TEnvConfig = {
|
|
44
44
|
name: 'local' | 'server',
|
|
45
45
|
profile: 'dev' | 'testing' | 'prod',
|
|
46
|
-
|
|
46
|
+
|
|
47
47
|
router: {
|
|
48
48
|
port: number,
|
|
49
|
-
|
|
50
|
-
},
|
|
51
|
-
database: {
|
|
52
|
-
name: string,
|
|
53
|
-
databases: string[],
|
|
54
|
-
host: string,
|
|
55
|
-
port: number,
|
|
56
|
-
login: string,
|
|
57
|
-
password: string,
|
|
49
|
+
currentDomain: string
|
|
58
50
|
},
|
|
59
|
-
|
|
51
|
+
trace: {
|
|
60
52
|
enable: boolean,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
53
|
+
requestsLimit: number,
|
|
54
|
+
eventsLimit: number,
|
|
55
|
+
capture: 'summary' | 'resolve' | 'deep',
|
|
56
|
+
persistOnError: boolean,
|
|
64
57
|
},
|
|
65
58
|
}*/
|
|
66
59
|
|