proteum 2.1.6 → 2.1.8
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 +6 -1
- package/README.md +5 -1
- package/agents/project/AGENTS.md +7 -1
- package/agents/project/diagnostics.md +4 -0
- package/agents/project/optimizations.md +1 -0
- package/cli/bin.js +0 -8
- package/cli/commands/build.ts +60 -9
- package/cli/commands/dev.ts +236 -6
- package/cli/compiler/artifacts/manifest.ts +8 -3
- package/cli/compiler/common/bundleAnalysis.ts +56 -1
- package/cli/index.ts +37 -2
- package/cli/presentation/commands.ts +30 -4
- package/cli/presentation/devSession.ts +6 -26
- package/cli/presentation/help.ts +4 -0
- package/cli/presentation/welcome.ts +63 -0
- package/cli/runtime/commands.ts +40 -3
- package/cli/runtime/devSessions.ts +337 -0
- package/cli/scaffold/index.ts +4 -2
- package/cli/scaffold/templates.ts +7 -1
- package/cli/utils/agents.ts +102 -11
- package/package.json +1 -1
- package/server/app/container/index.ts +7 -1
- package/server/services/router/http/index.ts +52 -24
- package/server/services/router/index.ts +66 -10
- package/server/services/router/response/page/document.tsx +26 -14
package/AGENTS.md
CHANGED
|
@@ -26,6 +26,9 @@ After those optimization concerns, preserve explicit, typed, machine-readable co
|
|
|
26
26
|
|
|
27
27
|
- If the user pastes raw errors without asking for a fix, do not implement changes. List likely causes and, for each one, give probability, why, and how to fix it.
|
|
28
28
|
- After implementing a framework feature or change, do not stop at code edits. Boot both reference apps, exercise the affected flow with Playwright or the smallest real Proteum surface, run the relevant `proteum` diagnostics or perf commands, and confirm there is no meaningful regression in runtime behavior, performance, load size, SEO output, or coding-style expectations before finishing.
|
|
29
|
+
- When starting a long-lived reference app dev server for framework work, prefer `npx proteum dev --session-file <path> --replace-existing --port <port>` so the session can be listed and stopped deterministically later.
|
|
30
|
+
- Before retrying a boot on the same app, changing ports, or finishing the task, stop every framework-started dev session with `npx proteum dev stop --session-file <path>` or `npx proteum dev stop --all --stale`.
|
|
31
|
+
- If the task changed the dev workflow itself, verify the final cleanup path with `npx proteum dev list --json` before finishing.
|
|
29
32
|
- When you have finished your work, summarize in one top-level short (up to 100 characters) sentence the changes you made since the beginning of the conversation. Output as "Commit message".
|
|
30
33
|
|
|
31
34
|
## Core Changes
|
|
@@ -36,6 +39,7 @@ After those optimization concerns, preserve explicit, typed, machine-readable co
|
|
|
36
39
|
- `/Users/gaetan/Desktop/Projets/unique.domains/website`
|
|
37
40
|
- Inspect how both apps currently use the touched feature, runtime, API, compiler behavior, or generated output before proposing or implementing changes.
|
|
38
41
|
- Keep the developer-facing contract synchronized when framework work changes CLI commands, profiler capabilities, or the `proteum dev` banner. Update the live surfaces together in the same pass: CLI command/help definitions, profiler panels and dev-only endpoints, banner text/examples, and the most relevant agent docs that describe them, especially `AGENTS.md`, `agents/project/AGENTS.md`, `agents/project/diagnostics.md`, and any narrower `agents/project/**/AGENTS.md` file that mentions the changed workflow.
|
|
42
|
+
- Current CLI banner contract: every human-facing Proteum CLI run prints the welcome banner, while only `proteum dev` clears the interactive terminal before rendering and exposes `CTRL+R` reload plus `CTRL+C` shutdown hotkeys in its session UI.
|
|
39
43
|
- Keep core changes aligned with the explicit controller/page architecture in `agents/project/AGENTS.md`.
|
|
40
44
|
- Prefer removing framework magic when the same result can be expressed with explicit contracts, generated code, or typed context.
|
|
41
45
|
- Apply the pruning rules from `agents/project/optimizations.md`, especially for webpack plugins, Babel plugins, aliases, helpers, runtime services, and npm packages that are not meaningfully used by both apps.
|
|
@@ -54,9 +58,10 @@ After those optimization concerns, preserve explicit, typed, machine-readable co
|
|
|
54
58
|
|
|
55
59
|
Do not stop at static analysis for routing, controllers, generated code, SSR, client runtime, services, webpack, Babel, or emitted assets.
|
|
56
60
|
|
|
57
|
-
- Run `npx proteum dev --no-cache --port 3xxx` in both reference apps on explicit ports.
|
|
61
|
+
- Run `npx proteum dev --no-cache --replace-existing --session-file var/run/proteum/dev/framework-<app>.json --port 3xxx` in both reference apps on explicit ports.
|
|
58
62
|
- When validating a concrete route, controller path, or failing page on a running dev server, prefer `proteum diagnose <path> --port <port>` first. Use raw `proteum trace ...` output when you need lower-level event detail beyond the diagnose summary.
|
|
59
63
|
- When the issue is latency, CPU, SQL cost, render cost, or memory drift, inspect `proteum perf top`, `proteum perf request`, `proteum perf compare`, or `proteum perf memory` against the running dev server before adding custom instrumentation.
|
|
64
|
+
- When a framework change can affect shipped client code size, run `proteum build --prod --analyze` for static bundle artifacts or `proteum build --prod --analyze --analyze-serve --analyze-port auto` when you need a local analyzer URL.
|
|
60
65
|
- For protected browser or API flows in dev, prefer `npx proteum session <email> --role <role>` to mint a dev auth cookie instead of automating the login UI. Use the login UI only when login itself is the feature under test.
|
|
61
66
|
- For request-time behavior, arm traces with `proteum trace arm --capture deep`, reproduce once, then inspect `proteum trace latest` or `proteum trace show <requestId>`.
|
|
62
67
|
- When the framework-facing workflow itself changed, verify the CLI surface too with `proteum verify framework-change --crosspath-port <port> --product-port <port> --website-port <port>`.
|
package/README.md
CHANGED
|
@@ -323,7 +323,7 @@ Proteum ships with a compact CLI focused on the real app lifecycle:
|
|
|
323
323
|
| `proteum typecheck` | Refresh generated typings, then run TypeScript |
|
|
324
324
|
| `proteum lint` | Run ESLint for the current app |
|
|
325
325
|
| `proteum check` | Refresh, typecheck, and lint in one command |
|
|
326
|
-
| `proteum build --prod` | Produce the production server and client bundles into `bin
|
|
326
|
+
| `proteum build --prod` | Produce the production server and client bundles into `bin/`, with optional static or served bundle analysis |
|
|
327
327
|
| `proteum connect` | Inspect connected-project sources, env, cached contracts, and imported controllers |
|
|
328
328
|
| `proteum doctor` | Inspect manifest diagnostics |
|
|
329
329
|
| `proteum explain` | Explain routes, controllers, services, layouts, conventions, env, and connected projects |
|
|
@@ -343,8 +343,12 @@ proteum dev
|
|
|
343
343
|
proteum refresh
|
|
344
344
|
proteum check
|
|
345
345
|
proteum build --prod
|
|
346
|
+
proteum build --prod --analyze
|
|
347
|
+
proteum build --prod --analyze --analyze-serve --analyze-port auto
|
|
346
348
|
```
|
|
347
349
|
|
|
350
|
+
Every human-facing Proteum CLI run prints the welcome banner. `proteum dev` is the only command that clears the interactive terminal before rendering its live session UI, and its banner exposes `CTRL+R` reload plus `CTRL+C` shutdown hotkeys.
|
|
351
|
+
|
|
348
352
|
Useful inspection commands:
|
|
349
353
|
|
|
350
354
|
```bash
|
package/agents/project/AGENTS.md
CHANGED
|
@@ -16,9 +16,13 @@ Coding style source of truth: project-root `CODING_STYLE.md`.
|
|
|
16
16
|
- Follow project-root `diagnostics.md` for diagnosis, runtime reproduction, temporary instrumentation, error-solving workflow, and verification method selection.
|
|
17
17
|
- For new app or artifact boilerplate, prefer `npx proteum init ...` and `npx proteum create ...` before creating files by hand. Use `--dry-run --json` when an agent needs a machine-readable plan before writing files.
|
|
18
18
|
- After running `npx proteum create ...`, adapt the generated code to the real feature instead of leaving placeholder logic in place.
|
|
19
|
+
- When starting a long-lived dev server for an agent task, prefer `npx proteum dev --session-file <path> --replace-existing --port <port>` so the session can be listed and stopped deterministically later.
|
|
20
|
+
- Do not start a second `proteum dev` server for the same app and port until the earlier tracked session has been stopped or replaced.
|
|
19
21
|
- When framework work changes Proteum CLI commands, profiler panels/features, or the `proteum dev` banners, keep this file, project-root `diagnostics.md`, and any narrower area `AGENTS.md` that mentions the same workflow aligned with the live framework behavior in the same pass.
|
|
22
|
+
- Current CLI banner contract: every human-facing Proteum CLI run prints the welcome banner, while only `proteum dev` clears the interactive terminal before rendering and exposes `CTRL+R` reload plus `CTRL+C` shutdown hotkeys in its session UI.
|
|
20
23
|
- Before finishing, double-check the touched files and generated output against the applicable optimization, diagnostics, and coding-style sources: project-root `optimizations.md`, project-root `diagnostics.md`, project-root `CODING_STYLE.md`, and any narrower area `AGENTS.md`.
|
|
21
24
|
- After implementing any feature or behavior change, always verify it on a running app before finishing: start the server, exercise the affected flow with Playwright or the smallest real runtime or `npx proteum` surface, run the relevant diagnostics or perf commands, and confirm there is no meaningful regression in behavior, performance, bundle/load size, SEO output, or coding style.
|
|
25
|
+
- Before finishing a task, stop every `proteum dev` session started during the task and confirm cleanup with `npx proteum dev list --json` or an explicit `npx proteum dev stop --session-file <path>`.
|
|
22
26
|
- When you have finished your work, summarize in one top-level short (up to 100 characters) sentence ALL the changes you made since the beginning of the WHOLE conversation. Output as "Commit message".
|
|
23
27
|
|
|
24
28
|
## Project Shape
|
|
@@ -98,6 +102,8 @@ Prefer structured CLI surfaces over re-deriving framework facts from source:
|
|
|
98
102
|
- `npx proteum command ...`
|
|
99
103
|
- `npx proteum session ...`
|
|
100
104
|
- `npx proteum create ... --dry-run --json`
|
|
105
|
+
- `npx proteum dev list --json`
|
|
106
|
+
- `npx proteum dev stop --session-file <path>`
|
|
101
107
|
|
|
102
108
|
Prefer scaffold commands before hand-writing boilerplate:
|
|
103
109
|
|
|
@@ -224,7 +230,7 @@ Verify at the correct layer:
|
|
|
224
230
|
- router or plugin changes: verify request context, auth, redirects, metrics, and validation on a running app
|
|
225
231
|
- For trace-first reproduction, session-based auth setup, temporary logs, and post-fix surface checks, follow project-root `diagnostics.md`.
|
|
226
232
|
|
|
227
|
-
Useful commands: `npx proteum init <dir> --name <name>`, `npx proteum create <kind> <target>`, `proteum dev`, `npx proteum refresh`, `npx proteum typecheck`, `npx proteum lint`, `npx proteum check`, `npx proteum build prod`, `npx proteum perf top`, `npx proteum perf request <requestId|path>`, `npx proteum perf compare --baseline yesterday --target today`, `npx proteum command <path>`, `npx proteum session <email> --role <role>`.
|
|
233
|
+
Useful commands: `npx proteum init <dir> --name <name>`, `npx proteum create <kind> <target>`, `proteum dev`, `proteum dev list --json`, `proteum dev stop --session-file <path>`, `npx proteum refresh`, `npx proteum typecheck`, `npx proteum lint`, `npx proteum check`, `npx proteum build prod`, `npx proteum build --prod --analyze`, `npx proteum build --prod --analyze --analyze-serve --analyze-port auto`, `npx proteum perf top`, `npx proteum perf request <requestId|path>`, `npx proteum perf compare --baseline yesterday --target today`, `npx proteum command <path>`, `npx proteum session <email> --role <role>`.
|
|
228
234
|
|
|
229
235
|
## High-Impact Files
|
|
230
236
|
|
|
@@ -12,10 +12,13 @@ This file is the canonical source of truth for diagnostics, temporary instrument
|
|
|
12
12
|
|
|
13
13
|
## Runtime Diagnostics
|
|
14
14
|
|
|
15
|
+
- For long-lived dev reproductions, start the app with `npx proteum dev --session-file <path> --replace-existing --port <port>` so the session can be listed and stopped deterministically after the repro.
|
|
16
|
+
- Human-facing Proteum CLI runs now print the welcome banner, but only `npx proteum dev` clears the interactive terminal before rendering; keep that in mind when capturing or comparing command logs during diagnosis.
|
|
15
17
|
- For request-time issues in dev, start with `npx proteum diagnose <path> --port <port>` when you have a concrete failing route, page, controller path, or request target. It combines owner lookup, manifest diagnostics, contract diagnostics, matching trace data, and buffered server logs in one pass.
|
|
16
18
|
- For connected-project failures, confirm the consumer app resolves the expected `connect.<Namespace>.source` and `connect.<Namespace>.urlInternal` values, the producer app exposes `GET /api/__proteum/connected/ping`, and the imported controller entries show `scope=connected` in `proteum explain`.
|
|
17
19
|
- Use `npx proteum explain owner <query>` when you need a fast ownership graph for a route, controller path, source file, or generated artifact before reading code.
|
|
18
20
|
- For performance issues or regressions in dev, use `npx proteum perf top --since <window>` to rank hot paths, `npx proteum perf request <requestId|path>` for one request waterfall, `npx proteum perf compare --baseline <window> --target <window>` for regressions, and `npx proteum perf memory --since <window>` for heap or RSS drift.
|
|
21
|
+
- For bundle-size inspection, use `npx proteum build --prod --analyze` to emit `bin/bundle-analysis/client.html` and `client-stats.json`, or add `--analyze-serve --analyze-port auto` when you want a local analyzer URL instead of a static HTML file.
|
|
19
22
|
- For request-time issues in dev, inspect traces before adding logs when the diagnose surface is still too coarse.
|
|
20
23
|
- If a server is already running on the default port from `PORT` or `./.proteum/manifest.json`, inspect existing traces before reproducing the issue.
|
|
21
24
|
- If existing traces are insufficient, arm `npx proteum trace arm --capture deep`, reproduce once, then inspect the new request with `npx proteum trace latest` or `npx proteum trace show <requestId>`.
|
|
@@ -47,6 +50,7 @@ This file is the canonical source of truth for diagnostics, temporary instrument
|
|
|
47
50
|
- For browser regressions, prefer targeted Playwright coverage and inspect failure artifacts such as screenshots, videos, `error-context.md`, and Playwright traces.
|
|
48
51
|
- Treat server startup failures, runtime errors, browser console errors or warnings, and Playwright failures as blocking unless they are clearly unrelated to the change.
|
|
49
52
|
- When the touched surface can affect coding-style enforcement, run the smallest relevant project check such as `npx proteum lint` or `npx proteum check` before finishing.
|
|
53
|
+
- If the task started any long-lived `proteum dev` server, stop it explicitly with `npx proteum dev stop --session-file <path>` or `npx proteum dev stop --all --stale`, then confirm the remaining tracked sessions with `npx proteum dev list --json`.
|
|
50
54
|
- Add `data-testid` when stable selectors are missing instead of relying on brittle text or DOM-shape selectors.
|
|
51
55
|
- If an isolated test misses prerequisite state, run the smallest broader scope that reproduces the real setup.
|
|
52
56
|
- After a fix, re-check traces, rendered HTML, browser console, and server output when those surfaces were part of the original failure.
|
|
@@ -13,6 +13,7 @@ When tradeoffs exist inside optimization work, optimize in this order:
|
|
|
13
13
|
## Bundle Size And Runtime Cost
|
|
14
14
|
|
|
15
15
|
- Reduce shipped client bundle size and unnecessary runtime code.
|
|
16
|
+
- When you need evidence for a bundle-size regression, run `npx proteum build --prod --analyze` for static artifacts or `npx proteum build --prod --analyze --analyze-serve --analyze-port auto` for a local analyzer URL.
|
|
16
17
|
- Before inventing a new helper, runtime, plugin, abstraction, primitive, parser, formatter, SDK wrapper, or build-time tool, first check whether the repo already depends on a suitable package.
|
|
17
18
|
- If the repo does not already depend on one, search npm before writing a custom implementation.
|
|
18
19
|
- Prefer established, flexible, well-typed, widely adopted, actively maintained packages.
|
package/cli/bin.js
CHANGED
|
@@ -2,12 +2,6 @@
|
|
|
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
|
-
|
|
11
5
|
/*
|
|
12
6
|
Why this exists (npm i vs npm link difference)
|
|
13
7
|
|
|
@@ -36,8 +30,6 @@ if (!process.env.TS_NODE_IGNORE) {
|
|
|
36
30
|
process.env.TS_NODE_PROJECT = path.join(__dirname, 'tsconfig.json');
|
|
37
31
|
process.env.TS_NODE_TRANSPILE_ONLY = '1';
|
|
38
32
|
|
|
39
|
-
clearInteractiveConsole();
|
|
40
|
-
|
|
41
33
|
require('ts-node/register/transpile-only');
|
|
42
34
|
|
|
43
35
|
const { runCli } = require('./index.ts');
|
package/cli/commands/build.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
- DEPENDANCES
|
|
3
3
|
----------------------------------*/
|
|
4
4
|
|
|
5
|
+
import { UsageError } from 'clipanion';
|
|
6
|
+
|
|
5
7
|
// Core
|
|
6
8
|
import cli from '..';
|
|
7
9
|
import { app } from '../app';
|
|
@@ -10,15 +12,23 @@ import { app } from '../app';
|
|
|
10
12
|
import Compiler from '../compiler';
|
|
11
13
|
import type { TCompileMode } from '../compiler/common';
|
|
12
14
|
import {
|
|
15
|
+
consumeClientBundleAnalysisServerUrl,
|
|
16
|
+
getBundleAnalysisMode,
|
|
17
|
+
getBundleAnalysisServerHost,
|
|
18
|
+
getBundleAnalysisServerPort,
|
|
13
19
|
getClientBundleAnalysisReportPaths,
|
|
20
|
+
hasBundleAnalysisServerOverrides,
|
|
14
21
|
waitForClientBundleAnalysisArtifacts,
|
|
15
22
|
} from '../compiler/common/bundleAnalysis';
|
|
16
23
|
import { refreshGeneratedTypings, runAppTypecheck } from '../utils/check';
|
|
17
24
|
import { renderRows } from '../presentation/layout';
|
|
18
25
|
import { renderStep, renderSuccess, renderTitle } from '../presentation/ink';
|
|
19
26
|
|
|
20
|
-
const allowedBuildArgs = new Set(['prod', 'cache', 'analyze', 'strict']);
|
|
27
|
+
const allowedBuildArgs = new Set(['prod', 'cache', 'analyze', 'analyzeServe', 'strict']);
|
|
21
28
|
type TBuildMultiCompiler = Awaited<ReturnType<Compiler['create']>>;
|
|
29
|
+
type TBuildAnalysisResult =
|
|
30
|
+
| { mode: 'static'; reportPath: string; statsPath: string }
|
|
31
|
+
| { mode: 'server'; statsPath: string; url?: string };
|
|
22
32
|
|
|
23
33
|
/*----------------------------------
|
|
24
34
|
- COMMAND
|
|
@@ -30,7 +40,9 @@ function resolveBuildMode(): TCompileMode {
|
|
|
30
40
|
|
|
31
41
|
const invalidArgs = enabledArgs.filter((arg) => !allowedBuildArgs.has(arg));
|
|
32
42
|
if (invalidArgs.length > 0)
|
|
33
|
-
throw new Error(
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Unknown build argument(s): ${invalidArgs.join(', ')}. Allowed values: prod, cache, analyze, analyzeServe, strict.`,
|
|
45
|
+
);
|
|
34
46
|
|
|
35
47
|
const requestedModes = enabledArgs.filter((arg): arg is TCompileMode => arg === 'prod');
|
|
36
48
|
if (requestedModes.length > 1)
|
|
@@ -39,6 +51,19 @@ function resolveBuildMode(): TCompileMode {
|
|
|
39
51
|
return requestedModes[0] ?? 'prod';
|
|
40
52
|
}
|
|
41
53
|
|
|
54
|
+
function assertValidBuildAnalyzerArgs() {
|
|
55
|
+
const analyzeEnabled = cli.args.analyze === true;
|
|
56
|
+
const analyzeServeEnabled = cli.args.analyzeServe === true;
|
|
57
|
+
|
|
58
|
+
if (!analyzeEnabled && (analyzeServeEnabled || hasBundleAnalysisServerOverrides())) {
|
|
59
|
+
throw new UsageError('Analyzer server flags require `--analyze`.');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!analyzeServeEnabled && hasBundleAnalysisServerOverrides()) {
|
|
63
|
+
throw new UsageError('`--analyze-host` and `--analyze-port` require `--analyze-serve`.');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
42
67
|
const closeMultiCompiler = async (multiCompiler: TBuildMultiCompiler) =>
|
|
43
68
|
await new Promise<void>((resolve, reject) => {
|
|
44
69
|
multiCompiler.close((error) => {
|
|
@@ -54,7 +79,11 @@ const closeMultiCompiler = async (multiCompiler: TBuildMultiCompiler) =>
|
|
|
54
79
|
export const run = async (): Promise<void> => {
|
|
55
80
|
const mode = resolveBuildMode();
|
|
56
81
|
const strict = cli.args.strict === true;
|
|
57
|
-
|
|
82
|
+
assertValidBuildAnalyzerArgs();
|
|
83
|
+
|
|
84
|
+
const analyze = cli.args.analyze === true;
|
|
85
|
+
const analysisMode = analyze ? getBundleAnalysisMode() : undefined;
|
|
86
|
+
let analysisResult: TBuildAnalysisResult | undefined;
|
|
58
87
|
|
|
59
88
|
console.info(
|
|
60
89
|
[
|
|
@@ -69,7 +98,14 @@ export const run = async (): Promise<void> => {
|
|
|
69
98
|
{ label: 'mode', value: mode },
|
|
70
99
|
{ label: 'strict', value: strict ? 'enabled' : 'disabled' },
|
|
71
100
|
{ label: 'cache', value: cli.args.cache === true ? 'enabled' : 'disabled' },
|
|
72
|
-
{ label: 'analyze', value:
|
|
101
|
+
{ label: 'analyze', value: analyze ? 'enabled' : 'disabled' },
|
|
102
|
+
...(analyze ? [{ label: 'analyze mode', value: analysisMode || 'static' }] : []),
|
|
103
|
+
...(analysisMode === 'server'
|
|
104
|
+
? [
|
|
105
|
+
{ label: 'analyze host', value: getBundleAnalysisServerHost() },
|
|
106
|
+
{ label: 'analyze port', value: String(getBundleAnalysisServerPort()) },
|
|
107
|
+
]
|
|
108
|
+
: []),
|
|
73
109
|
{ label: 'output', value: 'bin/' },
|
|
74
110
|
]),
|
|
75
111
|
].join('\n\n'),
|
|
@@ -103,10 +139,11 @@ export const run = async (): Promise<void> => {
|
|
|
103
139
|
return;
|
|
104
140
|
}
|
|
105
141
|
|
|
106
|
-
if (
|
|
142
|
+
if (analysisMode === 'static') {
|
|
107
143
|
waitForClientBundleAnalysisArtifacts(app, 'bin')
|
|
108
144
|
.then(() => {
|
|
109
|
-
|
|
145
|
+
const { reportPath, statsPath } = getClientBundleAnalysisReportPaths(app, 'bin');
|
|
146
|
+
analysisResult = { mode: 'static', reportPath, statsPath };
|
|
110
147
|
resolve();
|
|
111
148
|
})
|
|
112
149
|
.catch(reject);
|
|
@@ -125,11 +162,25 @@ export const run = async (): Promise<void> => {
|
|
|
125
162
|
|
|
126
163
|
if (buildError) throw buildError;
|
|
127
164
|
|
|
128
|
-
if (
|
|
165
|
+
if (analysisMode === 'server') {
|
|
166
|
+
const { statsPath } = getClientBundleAnalysisReportPaths(app, 'bin');
|
|
167
|
+
analysisResult = {
|
|
168
|
+
mode: 'server',
|
|
169
|
+
statsPath,
|
|
170
|
+
url: consumeClientBundleAnalysisServerUrl(),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (analysisResult !== undefined) {
|
|
129
175
|
console.info(
|
|
130
176
|
renderRows([
|
|
131
|
-
{ label: 'report', value:
|
|
132
|
-
|
|
177
|
+
...(analysisResult.mode === 'static' ? [{ label: 'report', value: analysisResult.reportPath }] : []),
|
|
178
|
+
...(analysisResult.mode === 'server'
|
|
179
|
+
? [
|
|
180
|
+
{ label: 'server', value: analysisResult.url || 'Analyzer server started. See the analyzer log for the URL.' },
|
|
181
|
+
]
|
|
182
|
+
: []),
|
|
183
|
+
{ label: 'stats', value: analysisResult.statsPath },
|
|
133
184
|
]),
|
|
134
185
|
);
|
|
135
186
|
}
|
package/cli/commands/dev.ts
CHANGED
|
@@ -23,6 +23,19 @@ import Compiler from '../compiler';
|
|
|
23
23
|
import { createDevEventServer } from './devEvents';
|
|
24
24
|
import { ensureProjectAgentSymlinks } from '../utils/agents';
|
|
25
25
|
import { renderDevSession, renderServerReadyBanner, renderDevShutdownBanner } from '../presentation/devSession';
|
|
26
|
+
import { clearInteractiveConsole } from '../presentation/welcome';
|
|
27
|
+
import {
|
|
28
|
+
createDevSessionRecord,
|
|
29
|
+
inspectDevSessionFile,
|
|
30
|
+
listDevSessionInspections,
|
|
31
|
+
removeDevSessionRecord,
|
|
32
|
+
removeDevSessionRecordSync,
|
|
33
|
+
resolveDevSessionFilePath,
|
|
34
|
+
stopDevSessionFile,
|
|
35
|
+
updateDevSessionRecord,
|
|
36
|
+
type TDevSessionInspection,
|
|
37
|
+
type TStopDevSessionResult,
|
|
38
|
+
} from '../runtime/devSessions';
|
|
26
39
|
import { logVerbose } from '../runtime/verbose';
|
|
27
40
|
|
|
28
41
|
// Core
|
|
@@ -51,6 +64,8 @@ const hotReloadableRoots = [() => app.paths.root, () => cli.paths.core.root];
|
|
|
51
64
|
let cp: ChildProcess | undefined = undefined;
|
|
52
65
|
let devSessionStopping = false;
|
|
53
66
|
let appProcessOperation: Promise<void> = Promise.resolve();
|
|
67
|
+
let currentDevSessionFilePath: string | undefined = undefined;
|
|
68
|
+
let devSessionExitCleanupRegistered = false;
|
|
54
69
|
type TDevWatching = ReturnType<Awaited<ReturnType<Compiler['create']>>['watch']>;
|
|
55
70
|
type TIndexedSourceWatching = { close: () => Promise<void> };
|
|
56
71
|
|
|
@@ -122,6 +137,7 @@ const createIgnoredWatchMatcher = (outputPaths: string[]) => (watchPath: string)
|
|
|
122
137
|
|
|
123
138
|
return ignoredWatchPathPatterns.test(normalizedWatchPath);
|
|
124
139
|
};
|
|
140
|
+
|
|
125
141
|
const getDevAppName = (app: App) =>
|
|
126
142
|
app.identity.web?.fullTitle || app.identity.web?.title || app.identity.name || app.packageJson.name || app.paths.root;
|
|
127
143
|
|
|
@@ -159,6 +175,190 @@ const signalAppProcess = (child: ChildProcess, signal: NodeJS.Signals) => {
|
|
|
159
175
|
}
|
|
160
176
|
};
|
|
161
177
|
|
|
178
|
+
const getRequestedSessionFilePath = () =>
|
|
179
|
+
typeof cli.args.sessionFile === 'string' && cli.args.sessionFile.trim() ? cli.args.sessionFile : undefined;
|
|
180
|
+
|
|
181
|
+
const getResolvedDevSessionFilePath = () =>
|
|
182
|
+
resolveDevSessionFilePath({
|
|
183
|
+
appRoot: app.paths.root,
|
|
184
|
+
port: app.env.router.port,
|
|
185
|
+
sessionFilePath: getRequestedSessionFilePath(),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const registerDevSessionExitCleanup = () => {
|
|
189
|
+
if (devSessionExitCleanupRegistered) return;
|
|
190
|
+
|
|
191
|
+
devSessionExitCleanupRegistered = true;
|
|
192
|
+
process.once('exit', () => {
|
|
193
|
+
if (!currentDevSessionFilePath) return;
|
|
194
|
+
removeDevSessionRecordSync(currentDevSessionFilePath);
|
|
195
|
+
});
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const updateCurrentDevSession = async (patch: { publicUrl?: string; state?: 'starting' | 'ready' }) => {
|
|
199
|
+
if (!currentDevSessionFilePath) return;
|
|
200
|
+
|
|
201
|
+
await updateDevSessionRecord({
|
|
202
|
+
sessionFilePath: currentDevSessionFilePath,
|
|
203
|
+
patch,
|
|
204
|
+
});
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const cleanupCurrentDevSession = async () => {
|
|
208
|
+
if (!currentDevSessionFilePath) return;
|
|
209
|
+
|
|
210
|
+
const sessionFilePath = currentDevSessionFilePath;
|
|
211
|
+
currentDevSessionFilePath = undefined;
|
|
212
|
+
await removeDevSessionRecord(sessionFilePath);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const describeInspection = (inspection: TDevSessionInspection) => {
|
|
216
|
+
if (!inspection.record) {
|
|
217
|
+
return [
|
|
218
|
+
'stale invalid',
|
|
219
|
+
inspection.sessionFilePath,
|
|
220
|
+
inspection.parseError || 'Unreadable session file.',
|
|
221
|
+
].join(' | ');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const parts = [
|
|
225
|
+
inspection.live ? 'live' : 'stale',
|
|
226
|
+
inspection.record.state,
|
|
227
|
+
`pid ${inspection.record.pid}`,
|
|
228
|
+
`port ${inspection.record.routerPort}`,
|
|
229
|
+
];
|
|
230
|
+
|
|
231
|
+
if (inspection.record.publicUrl) parts.push(inspection.record.publicUrl);
|
|
232
|
+
parts.push(inspection.sessionFilePath);
|
|
233
|
+
|
|
234
|
+
return parts.join(' | ');
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const describeStopResult = (result: TStopDevSessionResult) => {
|
|
238
|
+
if (!result.matched) return `missing | ${result.sessionFilePath}`;
|
|
239
|
+
if (result.invalid)
|
|
240
|
+
return `removed stale invalid | ${result.sessionFilePath} | ${result.parseError || 'Unreadable session file.'}`;
|
|
241
|
+
if (result.removed && result.stopped && !result.live) {
|
|
242
|
+
return [
|
|
243
|
+
result.pid !== null ? `stopped pid ${result.pid}` : 'stopped',
|
|
244
|
+
result.routerPort !== null ? `port ${result.routerPort}` : '',
|
|
245
|
+
result.publicUrl,
|
|
246
|
+
result.sessionFilePath,
|
|
247
|
+
]
|
|
248
|
+
.filter(Boolean)
|
|
249
|
+
.join(' | ');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return [
|
|
253
|
+
'failed',
|
|
254
|
+
result.pid !== null ? `pid ${result.pid}` : '',
|
|
255
|
+
result.routerPort !== null ? `port ${result.routerPort}` : '',
|
|
256
|
+
result.publicUrl,
|
|
257
|
+
result.sessionFilePath,
|
|
258
|
+
]
|
|
259
|
+
.filter(Boolean)
|
|
260
|
+
.join(' | ');
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const printJson = (payload: unknown) => {
|
|
264
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const runListCommand = async () => {
|
|
268
|
+
const inspections = await listDevSessionInspections({
|
|
269
|
+
appRoot: app.paths.root,
|
|
270
|
+
sessionFilePath: getRequestedSessionFilePath(),
|
|
271
|
+
});
|
|
272
|
+
const filteredInspections = cli.args.stale === true ? inspections.filter((inspection) => inspection.stale) : inspections;
|
|
273
|
+
|
|
274
|
+
if (cli.args.json === true) {
|
|
275
|
+
printJson({
|
|
276
|
+
appRoot: app.paths.root,
|
|
277
|
+
sessions: filteredInspections.map((inspection) => ({
|
|
278
|
+
sessionFilePath: inspection.sessionFilePath,
|
|
279
|
+
live: inspection.live,
|
|
280
|
+
stale: inspection.stale,
|
|
281
|
+
invalid: inspection.invalid,
|
|
282
|
+
parseError: inspection.parseError,
|
|
283
|
+
record: inspection.record,
|
|
284
|
+
})),
|
|
285
|
+
});
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (filteredInspections.length === 0) {
|
|
290
|
+
console.info(`No Proteum dev sessions found for ${app.paths.root}.`);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
console.info(filteredInspections.map(describeInspection).join('\n'));
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const runStopCommand = async () => {
|
|
298
|
+
const stopAll = cli.args.all === true;
|
|
299
|
+
const filterStale = cli.args.stale === true;
|
|
300
|
+
|
|
301
|
+
const targetSessionFilePaths = stopAll
|
|
302
|
+
? (await listDevSessionInspections({
|
|
303
|
+
appRoot: app.paths.root,
|
|
304
|
+
sessionFilePath: getRequestedSessionFilePath(),
|
|
305
|
+
}))
|
|
306
|
+
.filter((inspection) => !filterStale || inspection.stale)
|
|
307
|
+
.map((inspection) => inspection.sessionFilePath)
|
|
308
|
+
: [getResolvedDevSessionFilePath()];
|
|
309
|
+
|
|
310
|
+
const results = await Promise.all(targetSessionFilePaths.map((sessionFilePath) => stopDevSessionFile(sessionFilePath)));
|
|
311
|
+
const failedResults = results.filter((result) => result.matched && !result.stopped);
|
|
312
|
+
|
|
313
|
+
if (cli.args.json === true) {
|
|
314
|
+
printJson({ appRoot: app.paths.root, results });
|
|
315
|
+
} else if (results.length === 0) {
|
|
316
|
+
console.info(`No Proteum dev sessions matched for ${app.paths.root}.`);
|
|
317
|
+
} else {
|
|
318
|
+
console.info(results.map(describeStopResult).join('\n'));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (failedResults.length > 0) {
|
|
322
|
+
process.exitCode = 1;
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const ensureDevSessionSlot = async () => {
|
|
327
|
+
const sessionFilePath = getResolvedDevSessionFilePath();
|
|
328
|
+
const existingInspection = await inspectDevSessionFile(sessionFilePath);
|
|
329
|
+
|
|
330
|
+
if (existingInspection?.record && existingInspection.live && existingInspection.record.pid !== process.pid) {
|
|
331
|
+
if (cli.args.replaceExisting !== true) {
|
|
332
|
+
throw new Error(
|
|
333
|
+
`A Proteum dev session is already registered at ${sessionFilePath} (pid ${existingInspection.record.pid}, port ${existingInspection.record.routerPort}). ` +
|
|
334
|
+
'Use `proteum dev stop` or restart with `proteum dev --replace-existing`.',
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const stopResult = await stopDevSessionFile(sessionFilePath);
|
|
339
|
+
if (!stopResult.stopped) {
|
|
340
|
+
throw new Error(`Could not stop the existing Proteum dev session registered at ${sessionFilePath}.`);
|
|
341
|
+
}
|
|
342
|
+
} else if (existingInspection) {
|
|
343
|
+
await stopDevSessionFile(sessionFilePath);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
currentDevSessionFilePath = sessionFilePath;
|
|
347
|
+
registerDevSessionExitCleanup();
|
|
348
|
+
await fs.ensureDir(path.dirname(sessionFilePath));
|
|
349
|
+
await fs.writeJson(
|
|
350
|
+
sessionFilePath,
|
|
351
|
+
createDevSessionRecord({
|
|
352
|
+
appRoot: app.paths.root,
|
|
353
|
+
port: app.env.router.port,
|
|
354
|
+
sessionFilePath,
|
|
355
|
+
}),
|
|
356
|
+
{ spaces: 2 },
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
logVerbose(`Registered Proteum dev session at ${sessionFilePath}.`);
|
|
360
|
+
};
|
|
361
|
+
|
|
162
362
|
async function startApp(app: App) {
|
|
163
363
|
await runSerializedAppProcessOperation(async () => {
|
|
164
364
|
if (devSessionStopping) return;
|
|
@@ -166,6 +366,7 @@ async function startApp(app: App) {
|
|
|
166
366
|
await stopAppInternal('Restart asked');
|
|
167
367
|
if (devSessionStopping) return;
|
|
168
368
|
|
|
369
|
+
await updateCurrentDevSession({ state: 'starting', publicUrl: '' });
|
|
169
370
|
logVerbose('Launching new server ...');
|
|
170
371
|
cp = spawn('node', ['--preserve-symlinks', app.outputPath('dev') + '/server.js'], {
|
|
171
372
|
// stdin, stdout, stderr
|
|
@@ -174,14 +375,24 @@ async function startApp(app: App) {
|
|
|
174
375
|
});
|
|
175
376
|
|
|
176
377
|
const child = cp;
|
|
378
|
+
let childReady = false;
|
|
379
|
+
|
|
380
|
+
child.on('exit', (code, signal) => {
|
|
381
|
+
const isCurrentChild = cp === child;
|
|
382
|
+
if (isCurrentChild) cp = undefined;
|
|
383
|
+
if (!isCurrentChild || devSessionStopping || childReady) return;
|
|
177
384
|
|
|
178
|
-
|
|
179
|
-
|
|
385
|
+
console.error(
|
|
386
|
+
`Proteum dev server exited before reporting ready.${code !== null ? ` Exit code: ${code}.` : ''}${signal ? ` Signal: ${signal}.` : ''}`,
|
|
387
|
+
);
|
|
388
|
+
process.exit(code && code !== 0 ? code : 1);
|
|
180
389
|
});
|
|
181
390
|
|
|
182
391
|
child.on('message', (message: unknown) => {
|
|
183
392
|
if (isServerReadyMessage(message)) {
|
|
393
|
+
childReady = true;
|
|
184
394
|
void (async () => {
|
|
395
|
+
await updateCurrentDevSession({ publicUrl: message.publicUrl, state: 'ready' });
|
|
185
396
|
console.info(
|
|
186
397
|
await renderServerReadyBanner({
|
|
187
398
|
appName: getDevAppName(app),
|
|
@@ -342,12 +553,11 @@ const createIndexedSourceWatching = ({
|
|
|
342
553
|
};
|
|
343
554
|
};
|
|
344
555
|
|
|
345
|
-
|
|
346
|
-
- MAIN PROCESS
|
|
347
|
-
----------------------------------*/
|
|
348
|
-
export const run = async () => {
|
|
556
|
+
const runDevLoop = async () => {
|
|
349
557
|
devSessionStopping = false;
|
|
558
|
+
clearInteractiveConsole();
|
|
350
559
|
ensureProjectAgentSymlinks({ appRoot: app.paths.root, coreRoot: cli.paths.core.root });
|
|
560
|
+
await ensureDevSessionSlot();
|
|
351
561
|
|
|
352
562
|
const devEventServer = await createDevEventServer(app.env.router.port + 1);
|
|
353
563
|
app.devEventPort = devEventServer.port;
|
|
@@ -463,6 +673,7 @@ export const run = async () => {
|
|
|
463
673
|
await stopApp(reason);
|
|
464
674
|
await cleanupPersistedDevTraces(app);
|
|
465
675
|
await devEventServer.close();
|
|
676
|
+
await cleanupCurrentDevSession();
|
|
466
677
|
console.info(await renderDevShutdownBanner());
|
|
467
678
|
})();
|
|
468
679
|
|
|
@@ -499,3 +710,22 @@ export const run = async () => {
|
|
|
499
710
|
process.once('SIGTERM', () => exitAfterShutdown('SIGTERM', 0));
|
|
500
711
|
process.once('SIGHUP', () => exitAfterShutdown('SIGHUP', 0));
|
|
501
712
|
};
|
|
713
|
+
|
|
714
|
+
/*----------------------------------
|
|
715
|
+
- MAIN PROCESS
|
|
716
|
+
----------------------------------*/
|
|
717
|
+
export const run = async () => {
|
|
718
|
+
const action = typeof cli.args.action === 'string' ? cli.args.action : 'start';
|
|
719
|
+
|
|
720
|
+
if (action === 'list') {
|
|
721
|
+
await runListCommand();
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (action === 'stop') {
|
|
726
|
+
await runStopCommand();
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
await runDevLoop();
|
|
731
|
+
};
|
|
@@ -5,6 +5,7 @@ import app from '../../app';
|
|
|
5
5
|
import cli from '../..';
|
|
6
6
|
import { inspectProteumEnv } from '../../../common/env/proteumEnv';
|
|
7
7
|
import { reservedRouteSetupKeys, routeSetupOptionKeys } from '../../../common/router/pageSetup';
|
|
8
|
+
import { getProjectInstructionGitignoreEntries } from '../../utils/agents';
|
|
8
9
|
import {
|
|
9
10
|
TProteumManifest,
|
|
10
11
|
TProteumManifestCommand,
|
|
@@ -39,6 +40,10 @@ const collectManifestDiagnostics = ({
|
|
|
39
40
|
routes: TProteumManifest['routes'];
|
|
40
41
|
}) => {
|
|
41
42
|
const diagnostics: TProteumManifestDiagnostic[] = [];
|
|
43
|
+
const expectedGitignoreEntries = [
|
|
44
|
+
...requiredGitignoreEntries,
|
|
45
|
+
...getProjectInstructionGitignoreEntries({ coreRoot: cli.paths.core.root }),
|
|
46
|
+
];
|
|
42
47
|
|
|
43
48
|
const pushDiagnostic = (diagnostic: TProteumManifestDiagnostic) => {
|
|
44
49
|
diagnostics.push(diagnostic);
|
|
@@ -224,7 +229,7 @@ const collectManifestDiagnostics = ({
|
|
|
224
229
|
pushDiagnostic({
|
|
225
230
|
level: 'warning',
|
|
226
231
|
code: 'app.gitignore-missing',
|
|
227
|
-
message: `Missing .gitignore. Proteum
|
|
232
|
+
message: `Missing .gitignore. Proteum-managed paths should ignore ${expectedGitignoreEntries.join(', ')}.`,
|
|
228
233
|
filepath: gitignoreFilepath,
|
|
229
234
|
});
|
|
230
235
|
} else {
|
|
@@ -236,14 +241,14 @@ const collectManifestDiagnostics = ({
|
|
|
236
241
|
.map(normalizeGitignoreEntry),
|
|
237
242
|
);
|
|
238
243
|
|
|
239
|
-
for (const requiredEntry of
|
|
244
|
+
for (const requiredEntry of expectedGitignoreEntries) {
|
|
240
245
|
const normalizedRequiredEntry = normalizeGitignoreEntry(requiredEntry);
|
|
241
246
|
if (entries.has(normalizedRequiredEntry)) continue;
|
|
242
247
|
|
|
243
248
|
pushDiagnostic({
|
|
244
249
|
level: 'warning',
|
|
245
250
|
code: 'app.gitignore-generated-entry-missing',
|
|
246
|
-
message: `Add "${requiredEntry}" to .gitignore so Proteum
|
|
251
|
+
message: `Add "${requiredEntry}" to .gitignore so Proteum-managed paths stay untracked.`,
|
|
247
252
|
filepath: gitignoreFilepath,
|
|
248
253
|
});
|
|
249
254
|
}
|