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.
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs-extra';
2
2
  import path from 'path';
3
3
  import type { RspackPluginInstance } from '@rspack/core';
4
+ import { UsageError } from 'clipanion';
4
5
 
5
6
  import cli from '../..';
6
7
  import type { App } from '../../app';
@@ -8,8 +9,56 @@ import type { App } from '../../app';
8
9
  const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
9
10
 
10
11
  export type TBundleAnalysisReportPaths = { reportPath: string; statsPath: string };
12
+ export type TBundleAnalysisMode = 'server' | 'static';
13
+ type TBundleAnalysisServerUrlArgs = {
14
+ listenHost: string;
15
+ listenPort: number | 'auto';
16
+ boundAddress?: string | { address?: string; port?: number } | null;
17
+ };
18
+
19
+ const defaultAnalyzerHost = '127.0.0.1';
20
+ const defaultAnalyzerPort = 8888;
21
+ let latestClientBundleAnalysisServerUrl: string | undefined;
11
22
 
12
23
  export const isBundleAnalysisEnabled = () => cli.args.analyze === true;
24
+ export const isBundleAnalysisServerEnabled = () => cli.args.analyzeServe === true;
25
+ export const getBundleAnalysisMode = (): TBundleAnalysisMode => (isBundleAnalysisServerEnabled() ? 'server' : 'static');
26
+
27
+ const hasCliStringArg = (name: string) => typeof cli.args[name] === 'string' && (cli.args[name] as string).trim().length > 0;
28
+
29
+ export const hasBundleAnalysisServerOverrides = () => hasCliStringArg('analyzeHost') || hasCliStringArg('analyzePort');
30
+
31
+ export const getBundleAnalysisServerHost = () =>
32
+ hasCliStringArg('analyzeHost') ? String(cli.args.analyzeHost).trim() : defaultAnalyzerHost;
33
+
34
+ export const getBundleAnalysisServerPort = (): number | 'auto' => {
35
+ const rawPort = hasCliStringArg('analyzePort') ? String(cli.args.analyzePort).trim() : '';
36
+ if (!rawPort) return defaultAnalyzerPort;
37
+ if (rawPort === 'auto') return 'auto';
38
+
39
+ const parsedPort = Number.parseInt(rawPort, 10);
40
+ if (!Number.isInteger(parsedPort) || parsedPort < 1 || parsedPort > 65535) {
41
+ throw new UsageError(`Invalid analyzer port "${rawPort}". Use a number between 1 and 65535, or \`auto\`.`);
42
+ }
43
+
44
+ return parsedPort;
45
+ };
46
+
47
+ const createBundleAnalysisServerUrl = ({ listenHost, listenPort, boundAddress }: TBundleAnalysisServerUrlArgs) => {
48
+ const port =
49
+ typeof boundAddress === 'object' && boundAddress !== null && typeof boundAddress.port === 'number'
50
+ ? boundAddress.port
51
+ : listenPort;
52
+ const url = `http://${listenHost}:${port}`;
53
+ latestClientBundleAnalysisServerUrl = url;
54
+ return url;
55
+ };
56
+
57
+ export const consumeClientBundleAnalysisServerUrl = () => {
58
+ const url = latestClientBundleAnalysisServerUrl;
59
+ latestClientBundleAnalysisServerUrl = undefined;
60
+ return url;
61
+ };
13
62
 
14
63
  export const getClientBundleAnalysisReportPaths = (
15
64
  app: App,
@@ -23,19 +72,25 @@ export const getClientBundleAnalysisReportPaths = (
23
72
  export const createClientBundleAnalysisPlugins = (app: App, outputTarget: 'dev' | 'bin'): RspackPluginInstance[] => {
24
73
  if (!isBundleAnalysisEnabled()) return [];
25
74
 
75
+ latestClientBundleAnalysisServerUrl = undefined;
76
+
26
77
  const { reportPath, statsPath } = getClientBundleAnalysisReportPaths(app, outputTarget);
78
+ const analyzerMode = getBundleAnalysisMode();
27
79
 
28
80
  fs.ensureDirSync(path.dirname(reportPath));
29
81
 
30
82
  return [
31
83
  new BundleAnalyzerPlugin({
32
- analyzerMode: 'static',
84
+ analyzerMode,
85
+ analyzerHost: getBundleAnalysisServerHost(),
86
+ analyzerPort: getBundleAnalysisServerPort(),
33
87
  openAnalyzer: false,
34
88
  defaultSizes: 'parsed',
35
89
  reportFilename: reportPath,
36
90
  generateStatsFile: true,
37
91
  statsFilename: statsPath,
38
92
  logLevel: 'info',
93
+ analyzerUrl: createBundleAnalysisServerUrl,
39
94
  }),
40
95
  ];
41
96
  };
package/cli/index.ts CHANGED
@@ -5,19 +5,54 @@ import { Cli } from 'clipanion';
5
5
  import cli from './context';
6
6
  import { proteumCommandNames } from './presentation/commands';
7
7
  import { renderCliOverview, renderCommandHelp, resolveCustomHelpRequest } from './presentation/help';
8
+ import { renderCliWelcomeBanner } from './presentation/welcome';
8
9
  import { normalizeHelpArgv, normalizeLegacyArgv } from './runtime/argv';
9
10
  import { createCli, registeredCommands } from './runtime/commands';
10
11
 
12
+ const formatInvocation = (argv: string[]) => ['proteum', ...argv].join(' ').trim();
13
+
14
+ const shouldRenderSharedWelcomeBanner = ({
15
+ argv,
16
+ helpRequestKind,
17
+ }: {
18
+ argv: string[];
19
+ helpRequestKind: 'none' | 'overview' | 'command';
20
+ }) => {
21
+ if (helpRequestKind !== 'none') return true;
22
+ if (argv[0] !== 'dev') return true;
23
+
24
+ if (argv.length === 1) return false;
25
+
26
+ const action = argv[1];
27
+ if (action.startsWith('-')) return false;
28
+
29
+ return action === 'list' || action === 'stop';
30
+ };
31
+
11
32
  export const runCli = async (argv: string[] = process.argv.slice(2)) => {
12
33
  const normalizedArgv = normalizeHelpArgv(normalizeLegacyArgv(argv), proteumCommandNames);
13
- const clipanion = createCli(String(cli.packageJson.version || ''));
34
+ const version = String(cli.packageJson.version || '');
35
+ const clipanion = createCli(version);
14
36
  const initAvailable = true;
15
37
  const helpRequest = resolveCustomHelpRequest(normalizedArgv);
38
+ const shouldRenderWelcomeBanner = shouldRenderSharedWelcomeBanner({
39
+ argv: normalizedArgv,
40
+ helpRequestKind: helpRequest.kind,
41
+ });
42
+
43
+ if (shouldRenderWelcomeBanner) {
44
+ process.stderr.write(
45
+ `${await renderCliWelcomeBanner({
46
+ command: formatInvocation(normalizedArgv),
47
+ version,
48
+ })}\n\n`,
49
+ );
50
+ }
16
51
 
17
52
  if (helpRequest.kind === 'overview') {
18
53
  process.stdout.write(
19
54
  await renderCliOverview({
20
- version: String(cli.packageJson.version || ''),
55
+ version,
21
56
  workdir: process.cwd(),
22
57
  initAvailable,
23
58
  }),
@@ -110,18 +110,37 @@ export const proteumCommands: Record<TProteumCommandName, TProteumCommandDoc> =
110
110
  name: 'dev',
111
111
  category: 'Daily workflow',
112
112
  summary: 'Start the local compiler, SSR server, and hot reload loop.',
113
- usage: 'proteum dev [--port <port>] [--cache|--no-cache]',
113
+ usage: 'proteum dev [list|stop] [--port <port>] [--session-file <path>] [--replace-existing] [--all] [--stale] [--json] [--cache|--no-cache]',
114
114
  bestFor:
115
115
  'Day-to-day app work. This is the main entrypoint used by the current reference apps during local development.',
116
116
  examples: [
117
117
  { description: 'Start the app on its configured router port', command: 'proteum dev' },
118
- { description: 'Run a second Proteum app on another port', command: 'proteum dev --port 3101' },
118
+ { description: 'Replace the tracked dev session on another port', command: 'proteum dev --port 3101 --replace-existing' },
119
+ {
120
+ description: 'Start a tracked dev session with an explicit session file for an agent task',
121
+ command: 'proteum dev --port 3101 --session-file var/run/proteum/dev/agents/task.json --replace-existing',
122
+ },
123
+ {
124
+ description: 'List tracked Proteum dev sessions as JSON',
125
+ command: 'proteum dev list --json',
126
+ },
127
+ {
128
+ description: 'Stop every stale tracked dev session for the current app',
129
+ command: 'proteum dev stop --all --stale',
130
+ },
119
131
  {
120
132
  description: 'Disable the filesystem cache while debugging compiler state',
121
133
  command: 'proteum dev --no-cache',
122
134
  },
123
135
  ],
124
- notes: ['Legacy single-dash long options remain supported, for example `proteum dev -port 3001`.'],
136
+ notes: [
137
+ 'Proteum writes a machine-readable dev session file under `var/run/proteum/dev/<port>.json` by default; override it with `--session-file` when an agent needs a stable path.',
138
+ 'Use `--replace-existing` when retries should stop the previously tracked matching session before starting a new one.',
139
+ '`proteum dev list` inspects tracked sessions for the current app root. Add `--stale` to show only orphaned or dead sessions.',
140
+ '`proteum dev stop` targets the current session file by default. Add `--all` to stop every tracked session for the current app root.',
141
+ '`proteum dev` clears the interactive terminal once at startup, then shows `CTRL+R` reload and `CTRL+C` shutdown hotkeys in the session banner.',
142
+ 'Legacy single-dash long options remain supported, for example `proteum dev -port 3001`.',
143
+ ],
125
144
  status: 'stable',
126
145
  },
127
146
  refresh: {
@@ -141,7 +160,7 @@ export const proteumCommands: Record<TProteumCommandName, TProteumCommandDoc> =
141
160
  name: 'build',
142
161
  category: 'Daily workflow',
143
162
  summary: 'Build the application.',
144
- usage: 'proteum build [--prod] [--strict] [--cache] [--analyze] [--port <port>]',
163
+ usage: 'proteum build [--prod] [--strict] [--cache] [--analyze] [--analyze-serve] [--analyze-host <host>] [--analyze-port <port|auto>] [--port <port>]',
145
164
  bestFor: 'CI, release builds, and local verification of the production server and client output.',
146
165
  examples: [
147
166
  { description: 'Run the normal production build', command: 'proteum build --prod' },
@@ -150,10 +169,17 @@ export const proteumCommands: Record<TProteumCommandName, TProteumCommandDoc> =
150
169
  command: 'proteum build --prod --strict',
151
170
  },
152
171
  { description: 'Generate bundle analysis artifacts', command: 'proteum build --prod --analyze' },
172
+ {
173
+ description: 'Serve the bundle analysis at a local URL and let the OS choose the port',
174
+ command: 'proteum build --prod --analyze --analyze-serve --analyze-port auto',
175
+ },
153
176
  { description: 'Reuse the filesystem cache during builds', command: 'proteum build --prod --cache' },
154
177
  ],
155
178
  notes: [
156
179
  'Legacy positional booleans remain supported, for example `proteum build prod strict analyze`.',
180
+ '`--analyze` alone emits `bin/bundle-analysis/client.html` and `client-stats.json`.',
181
+ '`--analyze-serve` switches the analyzer to HTTP server mode and keeps the process open until you stop it.',
182
+ '`--analyze-host` and `--analyze-port` require `--analyze-serve`; use `auto` to let the OS assign a free port.',
157
183
  'Use `--strict` when the build must refresh generated typings and fail on any TypeScript error before compilation starts.',
158
184
  'The production output is emitted under `bin/`.',
159
185
  ],
@@ -2,16 +2,7 @@ const React = require('react') as typeof import('react');
2
2
 
3
3
  import { renderRows } from './layout';
4
4
  import { renderInk } from './ink';
5
-
6
- const ProteumWordmark = [
7
- String.raw` ____ ____ ___ _____ _____ _ _ __ __`,
8
- String.raw`| _ \| _ \ / _ \_ _| ____| | | | \/ |`,
9
- String.raw`| |_) | |_) | | | || | | _| | | | | |\/| |`,
10
- String.raw`| __/| _ <| |_| || | | |___| |_| | | | |`,
11
- String.raw`|_| |_| \_\\___/ |_| |_____|\___/|_| |_|`,
12
- ];
13
-
14
- const ProteumTagline = 'Agent-first SSR compiler and server loop.';
5
+ import { renderWelcomePanel } from './welcome';
15
6
 
16
7
  export const renderDevSession = async ({
17
8
  appName,
@@ -29,21 +20,9 @@ export const renderDevSession = async ({
29
20
  proteumVersion: string;
30
21
  }) =>
31
22
  [
32
- await renderInk(({ Box, Text }) => {
33
- const createElement = React.createElement;
34
- const wordmark = ProteumWordmark.map((line) =>
35
- createElement(Text, { key: line, bold: true, color: 'blue' }, line),
36
- );
37
- const versionLabel = proteumVersion ? `v${proteumVersion}` : '';
38
-
39
- return createElement(
40
- Box,
41
- { borderStyle: 'round', borderColor: 'blue', paddingX: 2, paddingY: 0, flexDirection: 'column' },
42
- createElement(Text, { bold: true, backgroundColor: 'blue', color: 'white' }, ' WELCOME TO '),
43
- createElement(Box, { flexDirection: 'column' }, ...wordmark),
44
- versionLabel ? createElement(Text, { bold: true, color: 'blue' }, versionLabel) : null,
45
- createElement(Text, { dimColor: true }, ProteumTagline),
46
- );
23
+ await renderWelcomePanel({
24
+ version: proteumVersion,
25
+ tagline: 'Agent-first SSR compiler and server loop.',
47
26
  }),
48
27
  renderRows(
49
28
  [
@@ -61,7 +40,8 @@ export const renderDevSession = async ({
61
40
  { label: 'perf', value: `proteum perf top --port ${routerPort}` },
62
41
  { label: 'trace', value: `proteum trace latest --port ${routerPort}` },
63
42
  { label: 'trace deep', value: `proteum trace arm --capture deep --port ${routerPort}` },
64
- { label: 'hotkeys', value: 'Ctrl+R reload, Ctrl+C stop' },
43
+ { label: 'reload', value: 'CTRL+R' },
44
+ { label: 'shutdown', value: 'CTRL+C' },
65
45
  ],
66
46
  { minLabelWidth: 12, maxLabelWidth: 12 },
67
47
  ),
@@ -135,6 +135,10 @@ export const renderCliOverview = async ({
135
135
  indent: ' ',
136
136
  nextIndent: ' ',
137
137
  }),
138
+ wrapText('Every Proteum CLI invocation prints the welcome banner. `proteum dev` is the only command that clears the interactive terminal before rendering its session UI.', {
139
+ indent: ' ',
140
+ nextIndent: ' ',
141
+ }),
138
142
  wrapText('Legacy single-dash flags and positional booleans remain accepted for older app scripts, but new docs should prefer modern long flags.', {
139
143
  indent: ' ',
140
144
  nextIndent: ' ',
@@ -0,0 +1,63 @@
1
+ const React = require('react') as typeof import('react');
2
+
3
+ import { renderRows } from './layout';
4
+ import { renderInk } from './ink';
5
+
6
+ const ProteumWordmark = [
7
+ String.raw` ____ ____ ___ _____ _____ _ _ __ __`,
8
+ String.raw`| _ \| _ \ / _ \_ _| ____| | | | \/ |`,
9
+ String.raw`| |_) | |_) | | | || | | _| | | | | |\/| |`,
10
+ String.raw`| __/| _ <| |_| || | | |___| |_| | | | |`,
11
+ String.raw`|_| |_| \_\\___/ |_| |_____|\___/|_| |_|`,
12
+ ];
13
+
14
+ export const clearInteractiveConsole = () => {
15
+ if (process.stdout.isTTY !== true || process.env.TERM === 'dumb') return;
16
+
17
+ process.stdout.write('\x1B[2J\x1B[3J\x1B[H');
18
+ };
19
+
20
+ export const renderWelcomePanel = async ({
21
+ version,
22
+ tagline,
23
+ }: {
24
+ version: string;
25
+ tagline: string;
26
+ }) =>
27
+ renderInk(({ Box, Text }) => {
28
+ const createElement = React.createElement;
29
+ const wordmark = ProteumWordmark.map((line) =>
30
+ createElement(Text, { key: line, bold: true, color: 'blue' }, line),
31
+ );
32
+ const versionLabel = version ? `v${version}` : '';
33
+
34
+ return createElement(
35
+ Box,
36
+ { borderStyle: 'round', borderColor: 'blue', paddingX: 2, paddingY: 0, flexDirection: 'column' },
37
+ createElement(Text, { bold: true, backgroundColor: 'blue', color: 'white' }, ' WELCOME TO '),
38
+ createElement(Box, { flexDirection: 'column' }, ...wordmark),
39
+ versionLabel ? createElement(Text, { bold: true, color: 'blue' }, versionLabel) : null,
40
+ createElement(Text, { dimColor: true }, tagline),
41
+ );
42
+ });
43
+
44
+ export const renderCliWelcomeBanner = async ({
45
+ command,
46
+ version,
47
+ }: {
48
+ command: string;
49
+ version: string;
50
+ }) =>
51
+ [
52
+ await renderWelcomePanel({
53
+ version,
54
+ tagline: 'Explicit SSR / SEO / TypeScript framework for agent-friendly apps.',
55
+ }),
56
+ renderRows(
57
+ [
58
+ { label: 'command', value: command },
59
+ { label: 'shutdown', value: 'CTRL+C' },
60
+ ],
61
+ { minLabelWidth: 10, maxLabelWidth: 10 },
62
+ ),
63
+ ].join('\n\n');
@@ -79,13 +79,38 @@ class DevCommand extends ProteumCommand {
79
79
 
80
80
  public static usage = buildUsage('dev');
81
81
 
82
+ public json = Option.Boolean('--json', false, { description: 'Print machine-readable dev session output.' });
82
83
  public port = Option.String('--port', { description: 'Override the router port.' });
83
84
  public cache = Option.Boolean('--cache', true, { description: 'Enable filesystem caching.' });
84
- public legacyArgs = Option.Rest();
85
+ public sessionFile = Option.String('--session-file', {
86
+ description: 'Override the dev session file path used for list, stop, or the active dev server.',
87
+ });
88
+ public replaceExisting = Option.Boolean('--replace-existing', false, {
89
+ description: 'Stop the existing matching dev session before starting a new one.',
90
+ });
91
+ public all = Option.Boolean('--all', false, {
92
+ description: 'When used with `dev stop`, stop every tracked dev session for the current app root.',
93
+ });
94
+ public stale = Option.Boolean('--stale', false, {
95
+ description: 'Filter `dev list` or `dev stop --all` to stale tracked sessions only.',
96
+ });
97
+ public args = Option.Rest();
85
98
 
86
99
  public async execute() {
87
- assertNoLegacyArgs('dev', this.legacyArgs);
88
- this.setCliArgs({ port: this.port ?? '', cache: this.cache });
100
+ const [maybeAction = '', ...restArgs] = this.args;
101
+ const action = maybeAction === 'list' || maybeAction === 'stop' ? maybeAction : '';
102
+
103
+ assertNoLegacyArgs('dev', action ? restArgs : this.args);
104
+ this.setCliArgs({
105
+ action: action || 'start',
106
+ port: this.port ?? '',
107
+ cache: this.cache,
108
+ json: this.json,
109
+ sessionFile: this.sessionFile ?? '',
110
+ replaceExisting: this.replaceExisting,
111
+ all: this.all,
112
+ stale: this.stale,
113
+ });
89
114
  await runCommandModule(() => import('../commands/dev'));
90
115
  }
91
116
  }
@@ -113,6 +138,15 @@ class BuildCommand extends ProteumCommand {
113
138
  public prod = Option.Boolean('--prod', false, { description: 'Build in production mode.' });
114
139
  public cache = Option.Boolean('--cache', false, { description: 'Enable filesystem caching during the build.' });
115
140
  public analyze = Option.Boolean('--analyze', false, { description: 'Emit the client bundle analysis report.' });
141
+ public analyzeServe = Option.Boolean('--analyze-serve', false, {
142
+ description: 'Serve the bundle analysis over HTTP instead of only writing a static report.',
143
+ });
144
+ public analyzeHost = Option.String('--analyze-host', {
145
+ description: 'Host used by the analyzer HTTP server when `--analyze-serve` is enabled.',
146
+ });
147
+ public analyzePort = Option.String('--analyze-port', {
148
+ description: 'Port used by the analyzer HTTP server when `--analyze-serve` is enabled. Use `auto` for an ephemeral port.',
149
+ });
116
150
  public strict = Option.Boolean('--strict', false, {
117
151
  description: 'Refresh generated typings and fail the build if TypeScript reports any error.',
118
152
  });
@@ -125,6 +159,9 @@ class BuildCommand extends ProteumCommand {
125
159
  prod: this.prod,
126
160
  cache: this.cache,
127
161
  analyze: this.analyze,
162
+ analyzeServe: this.analyzeServe,
163
+ analyzeHost: this.analyzeHost ?? '',
164
+ analyzePort: this.analyzePort ?? '',
128
165
  strict: this.strict,
129
166
  } satisfies TArgsObject;
130
167