proteum 2.1.7 → 2.1.9-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,6 +23,20 @@ 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';
39
+ import { resolveFrameworkInstallInfo } from '../paths';
26
40
  import { logVerbose } from '../runtime/verbose';
27
41
 
28
42
  // Core
@@ -51,6 +65,8 @@ const hotReloadableRoots = [() => app.paths.root, () => cli.paths.core.root];
51
65
  let cp: ChildProcess | undefined = undefined;
52
66
  let devSessionStopping = false;
53
67
  let appProcessOperation: Promise<void> = Promise.resolve();
68
+ let currentDevSessionFilePath: string | undefined = undefined;
69
+ let devSessionExitCleanupRegistered = false;
54
70
  type TDevWatching = ReturnType<Awaited<ReturnType<Compiler['create']>>['watch']>;
55
71
  type TIndexedSourceWatching = { close: () => Promise<void> };
56
72
 
@@ -122,6 +138,7 @@ const createIgnoredWatchMatcher = (outputPaths: string[]) => (watchPath: string)
122
138
 
123
139
  return ignoredWatchPathPatterns.test(normalizedWatchPath);
124
140
  };
141
+
125
142
  const getDevAppName = (app: App) =>
126
143
  app.identity.web?.fullTitle || app.identity.web?.title || app.identity.name || app.packageJson.name || app.paths.root;
127
144
 
@@ -159,6 +176,190 @@ const signalAppProcess = (child: ChildProcess, signal: NodeJS.Signals) => {
159
176
  }
160
177
  };
161
178
 
179
+ const getRequestedSessionFilePath = () =>
180
+ typeof cli.args.sessionFile === 'string' && cli.args.sessionFile.trim() ? cli.args.sessionFile : undefined;
181
+
182
+ const getResolvedDevSessionFilePath = () =>
183
+ resolveDevSessionFilePath({
184
+ appRoot: app.paths.root,
185
+ port: app.env.router.port,
186
+ sessionFilePath: getRequestedSessionFilePath(),
187
+ });
188
+
189
+ const registerDevSessionExitCleanup = () => {
190
+ if (devSessionExitCleanupRegistered) return;
191
+
192
+ devSessionExitCleanupRegistered = true;
193
+ process.once('exit', () => {
194
+ if (!currentDevSessionFilePath) return;
195
+ removeDevSessionRecordSync(currentDevSessionFilePath);
196
+ });
197
+ };
198
+
199
+ const updateCurrentDevSession = async (patch: { publicUrl?: string; state?: 'starting' | 'ready' }) => {
200
+ if (!currentDevSessionFilePath) return;
201
+
202
+ await updateDevSessionRecord({
203
+ sessionFilePath: currentDevSessionFilePath,
204
+ patch,
205
+ });
206
+ };
207
+
208
+ const cleanupCurrentDevSession = async () => {
209
+ if (!currentDevSessionFilePath) return;
210
+
211
+ const sessionFilePath = currentDevSessionFilePath;
212
+ currentDevSessionFilePath = undefined;
213
+ await removeDevSessionRecord(sessionFilePath);
214
+ };
215
+
216
+ const describeInspection = (inspection: TDevSessionInspection) => {
217
+ if (!inspection.record) {
218
+ return [
219
+ 'stale invalid',
220
+ inspection.sessionFilePath,
221
+ inspection.parseError || 'Unreadable session file.',
222
+ ].join(' | ');
223
+ }
224
+
225
+ const parts = [
226
+ inspection.live ? 'live' : 'stale',
227
+ inspection.record.state,
228
+ `pid ${inspection.record.pid}`,
229
+ `port ${inspection.record.routerPort}`,
230
+ ];
231
+
232
+ if (inspection.record.publicUrl) parts.push(inspection.record.publicUrl);
233
+ parts.push(inspection.sessionFilePath);
234
+
235
+ return parts.join(' | ');
236
+ };
237
+
238
+ const describeStopResult = (result: TStopDevSessionResult) => {
239
+ if (!result.matched) return `missing | ${result.sessionFilePath}`;
240
+ if (result.invalid)
241
+ return `removed stale invalid | ${result.sessionFilePath} | ${result.parseError || 'Unreadable session file.'}`;
242
+ if (result.removed && result.stopped && !result.live) {
243
+ return [
244
+ result.pid !== null ? `stopped pid ${result.pid}` : 'stopped',
245
+ result.routerPort !== null ? `port ${result.routerPort}` : '',
246
+ result.publicUrl,
247
+ result.sessionFilePath,
248
+ ]
249
+ .filter(Boolean)
250
+ .join(' | ');
251
+ }
252
+
253
+ return [
254
+ 'failed',
255
+ result.pid !== null ? `pid ${result.pid}` : '',
256
+ result.routerPort !== null ? `port ${result.routerPort}` : '',
257
+ result.publicUrl,
258
+ result.sessionFilePath,
259
+ ]
260
+ .filter(Boolean)
261
+ .join(' | ');
262
+ };
263
+
264
+ const printJson = (payload: unknown) => {
265
+ process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
266
+ };
267
+
268
+ const runListCommand = async () => {
269
+ const inspections = await listDevSessionInspections({
270
+ appRoot: app.paths.root,
271
+ sessionFilePath: getRequestedSessionFilePath(),
272
+ });
273
+ const filteredInspections = cli.args.stale === true ? inspections.filter((inspection) => inspection.stale) : inspections;
274
+
275
+ if (cli.args.json === true) {
276
+ printJson({
277
+ appRoot: app.paths.root,
278
+ sessions: filteredInspections.map((inspection) => ({
279
+ sessionFilePath: inspection.sessionFilePath,
280
+ live: inspection.live,
281
+ stale: inspection.stale,
282
+ invalid: inspection.invalid,
283
+ parseError: inspection.parseError,
284
+ record: inspection.record,
285
+ })),
286
+ });
287
+ return;
288
+ }
289
+
290
+ if (filteredInspections.length === 0) {
291
+ console.info(`No Proteum dev sessions found for ${app.paths.root}.`);
292
+ return;
293
+ }
294
+
295
+ console.info(filteredInspections.map(describeInspection).join('\n'));
296
+ };
297
+
298
+ const runStopCommand = async () => {
299
+ const stopAll = cli.args.all === true;
300
+ const filterStale = cli.args.stale === true;
301
+
302
+ const targetSessionFilePaths = stopAll
303
+ ? (await listDevSessionInspections({
304
+ appRoot: app.paths.root,
305
+ sessionFilePath: getRequestedSessionFilePath(),
306
+ }))
307
+ .filter((inspection) => !filterStale || inspection.stale)
308
+ .map((inspection) => inspection.sessionFilePath)
309
+ : [getResolvedDevSessionFilePath()];
310
+
311
+ const results = await Promise.all(targetSessionFilePaths.map((sessionFilePath) => stopDevSessionFile(sessionFilePath)));
312
+ const failedResults = results.filter((result) => result.matched && !result.stopped);
313
+
314
+ if (cli.args.json === true) {
315
+ printJson({ appRoot: app.paths.root, results });
316
+ } else if (results.length === 0) {
317
+ console.info(`No Proteum dev sessions matched for ${app.paths.root}.`);
318
+ } else {
319
+ console.info(results.map(describeStopResult).join('\n'));
320
+ }
321
+
322
+ if (failedResults.length > 0) {
323
+ process.exitCode = 1;
324
+ }
325
+ };
326
+
327
+ const ensureDevSessionSlot = async () => {
328
+ const sessionFilePath = getResolvedDevSessionFilePath();
329
+ const existingInspection = await inspectDevSessionFile(sessionFilePath);
330
+
331
+ if (existingInspection?.record && existingInspection.live && existingInspection.record.pid !== process.pid) {
332
+ if (cli.args.replaceExisting !== true) {
333
+ throw new Error(
334
+ `A Proteum dev session is already registered at ${sessionFilePath} (pid ${existingInspection.record.pid}, port ${existingInspection.record.routerPort}). ` +
335
+ 'Use `proteum dev stop` or restart with `proteum dev --replace-existing`.',
336
+ );
337
+ }
338
+
339
+ const stopResult = await stopDevSessionFile(sessionFilePath);
340
+ if (!stopResult.stopped) {
341
+ throw new Error(`Could not stop the existing Proteum dev session registered at ${sessionFilePath}.`);
342
+ }
343
+ } else if (existingInspection) {
344
+ await stopDevSessionFile(sessionFilePath);
345
+ }
346
+
347
+ currentDevSessionFilePath = sessionFilePath;
348
+ registerDevSessionExitCleanup();
349
+ await fs.ensureDir(path.dirname(sessionFilePath));
350
+ await fs.writeJson(
351
+ sessionFilePath,
352
+ createDevSessionRecord({
353
+ appRoot: app.paths.root,
354
+ port: app.env.router.port,
355
+ sessionFilePath,
356
+ }),
357
+ { spaces: 2 },
358
+ );
359
+
360
+ logVerbose(`Registered Proteum dev session at ${sessionFilePath}.`);
361
+ };
362
+
162
363
  async function startApp(app: App) {
163
364
  await runSerializedAppProcessOperation(async () => {
164
365
  if (devSessionStopping) return;
@@ -166,6 +367,7 @@ async function startApp(app: App) {
166
367
  await stopAppInternal('Restart asked');
167
368
  if (devSessionStopping) return;
168
369
 
370
+ await updateCurrentDevSession({ state: 'starting', publicUrl: '' });
169
371
  logVerbose('Launching new server ...');
170
372
  cp = spawn('node', ['--preserve-symlinks', app.outputPath('dev') + '/server.js'], {
171
373
  // stdin, stdout, stderr
@@ -191,10 +393,11 @@ async function startApp(app: App) {
191
393
  if (isServerReadyMessage(message)) {
192
394
  childReady = true;
193
395
  void (async () => {
396
+ await updateCurrentDevSession({ publicUrl: message.publicUrl, state: 'ready' });
194
397
  console.info(
195
398
  await renderServerReadyBanner({
196
399
  appName: getDevAppName(app),
197
- connectedProjectsCount: Object.keys(app.env.connectedProjects).length,
400
+ connectedProjects: message.connectedProjects,
198
401
  publicUrl: message.publicUrl,
199
402
  routerPort: app.env.router.port,
200
403
  }),
@@ -351,12 +554,15 @@ const createIndexedSourceWatching = ({
351
554
  };
352
555
  };
353
556
 
354
- /*----------------------------------
355
- - MAIN PROCESS
356
- ----------------------------------*/
357
- export const run = async () => {
557
+ const runDevLoop = async () => {
358
558
  devSessionStopping = false;
559
+ clearInteractiveConsole();
359
560
  ensureProjectAgentSymlinks({ appRoot: app.paths.root, coreRoot: cli.paths.core.root });
561
+ await ensureDevSessionSlot();
562
+ const proteumInstall = resolveFrameworkInstallInfo({
563
+ appRoot: app.paths.root,
564
+ framework: cli.paths.framework,
565
+ });
360
566
 
361
567
  const devEventServer = await createDevEventServer(app.env.router.port + 1);
362
568
  app.devEventPort = devEventServer.port;
@@ -367,6 +573,7 @@ export const run = async () => {
367
573
  connectedProjects: Object.values(app.env.connectedProjects),
368
574
  routerPort: app.env.router.port,
369
575
  devEventPort: devEventServer.port,
576
+ proteumInstallSummary: proteumInstall.summary,
370
577
  proteumVersion: String(cli.packageJson.version || ''),
371
578
  }),
372
579
  );
@@ -472,6 +679,7 @@ export const run = async () => {
472
679
  await stopApp(reason);
473
680
  await cleanupPersistedDevTraces(app);
474
681
  await devEventServer.close();
682
+ await cleanupCurrentDevSession();
475
683
  console.info(await renderDevShutdownBanner());
476
684
  })();
477
685
 
@@ -508,3 +716,22 @@ export const run = async () => {
508
716
  process.once('SIGTERM', () => exitAfterShutdown('SIGTERM', 0));
509
717
  process.once('SIGHUP', () => exitAfterShutdown('SIGHUP', 0));
510
718
  };
719
+
720
+ /*----------------------------------
721
+ - MAIN PROCESS
722
+ ----------------------------------*/
723
+ export const run = async () => {
724
+ const action = typeof cli.args.action === 'string' ? cli.args.action : 'start';
725
+
726
+ if (action === 'list') {
727
+ await runListCommand();
728
+ return;
729
+ }
730
+
731
+ if (action === 'stop') {
732
+ await runStopCommand();
733
+ return;
734
+ }
735
+
736
+ await runDevLoop();
737
+ };
@@ -22,8 +22,15 @@ const readServerTsconfigPaths = () => {
22
22
  return compilerOptions.paths || {};
23
23
  };
24
24
 
25
- const createCommandsTsconfigContent = () =>
26
- `${JSON.stringify(
25
+ const getCommandsTsconfigFilepath = () => path.join(app.paths.root, 'commands', 'tsconfig.json');
26
+
27
+ const getCommandsGlobalTypesPath = (commandsTsconfigFilepath: string) =>
28
+ cli.paths.relativeFrameworkPathFrom(commandsTsconfigFilepath, 'types', 'global');
29
+
30
+ const createCommandsTsconfigContent = () => {
31
+ const commandsTsconfigFilepath = getCommandsTsconfigFilepath();
32
+
33
+ return `${JSON.stringify(
27
34
  {
28
35
  extends: '../server/tsconfig.json',
29
36
  compilerOptions: {
@@ -35,12 +42,13 @@ const createCommandsTsconfigContent = () =>
35
42
  '@models/types': ['./.proteum/server/models.ts'],
36
43
  },
37
44
  },
38
- include: ['.', '../var/typings', '../node_modules/proteum/types/global', '../.proteum/server/commands.d.ts'],
45
+ include: ['.', '../var/typings', getCommandsGlobalTypesPath(commandsTsconfigFilepath), '../.proteum/server/commands.d.ts'],
39
46
  },
40
47
  null,
41
48
  4,
42
49
  )}
43
50
  `;
51
+ };
44
52
 
45
53
  const legacyCommandsTsconfigContent = `{
46
54
  "extends": "../server/tsconfig.json",
@@ -105,8 +113,15 @@ const isManagedCommandsTsconfig = (content: string) => {
105
113
  };
106
114
 
107
115
  if (parsed.extends !== '../server/tsconfig.json') return false;
108
- if (JSON.stringify(parsed.include || []) !== JSON.stringify(['.', '../var/typings', '../node_modules/proteum/types/global', '../.proteum/server/commands.d.ts']))
116
+ if (!Array.isArray(parsed.include) || parsed.include.length !== 4) return false;
117
+ if (parsed.include[0] !== '.' || parsed.include[1] !== '../var/typings') return false;
118
+ if (parsed.include[3] !== '../.proteum/server/commands.d.ts') return false;
119
+ if (
120
+ parsed.include[2] !== getCommandsGlobalTypesPath(getCommandsTsconfigFilepath()) &&
121
+ !parsed.include[2].includes('node_modules/proteum/types/global')
122
+ ) {
109
123
  return false;
124
+ }
110
125
 
111
126
  if (parsed.compilerOptions?.baseUrl !== undefined && parsed.compilerOptions.baseUrl !== '..') return false;
112
127
  if (parsed.compilerOptions?.rootDir !== undefined && parsed.compilerOptions.rootDir !== '..') return false;
@@ -119,7 +134,7 @@ const isManagedCommandsTsconfig = (content: string) => {
119
134
 
120
135
  const ensureCommandsTsconfig = () => {
121
136
  const commandsRoot = path.join(app.paths.root, 'commands');
122
- const commandsTsconfigFilepath = path.join(commandsRoot, 'tsconfig.json');
137
+ const commandsTsconfigFilepath = getCommandsTsconfigFilepath();
123
138
  const nextContent = createCommandsTsconfigContent();
124
139
 
125
140
  if (!fs.existsSync(commandsRoot)) return;
@@ -21,22 +21,40 @@ import type { App } from '../../app';
21
21
 
22
22
  const debug = false;
23
23
  const ssrScriptPattern = /\.ssr\.(ts|tsx)$/;
24
- const normalizedCoreRoot = cli.paths.core.root.replace(/\\/g, '/');
25
- const hmrClientEntry = path.join(cli.paths.core.root, 'client', 'dev', 'hmr.ts');
26
-
27
24
  const normalizeModulePath = (value?: string) => (value || '').replace(/\\/g, '/');
28
- const resolveFromAppOrCore = (app: App, request: string) =>
29
- require.resolve(request, { paths: [app.paths.root, cli.paths.core.root] });
30
- const rewriteFrameworkAliasTargets = (app: App, aliases: Record<string, string | string[]>) => {
31
- const installedCoreRoot = normalizeModulePath(path.join(app.paths.root, 'node_modules', 'proteum'));
32
- const activeCoreRoot = normalizeModulePath(cli.paths.core.root);
25
+ const getFrameworkSourceRoot = () => {
26
+ const installedCoreRoot = cli.paths.framework.installedRoot
27
+ ? normalizeModulePath(cli.paths.framework.installedRoot)
28
+ : undefined;
29
+ const activeCoreRoot = normalizeModulePath(cli.paths.framework.activeRoot);
30
+
31
+ if (installedCoreRoot && activeCoreRoot.includes('/node_modules/')) {
32
+ return installedCoreRoot;
33
+ }
34
+
35
+ return activeCoreRoot;
36
+ };
33
37
 
34
- if (installedCoreRoot === activeCoreRoot) return aliases;
38
+ const resolveFromAppOrCore = (_app: App, request: string) => cli.paths.resolveRequest(request);
39
+ const rewriteFrameworkAliasTargets = (aliases: Record<string, string | string[]>) => {
40
+ const visibleFrameworkRoots = [
41
+ ...cli.paths.getVisiblePackageInstallRoots('proteum'),
42
+ cli.paths.framework.installedRoot,
43
+ cli.paths.framework.activeRoot,
44
+ ]
45
+ .filter((rootPath): rootPath is string => typeof rootPath === 'string' && rootPath !== '')
46
+ .map((rootPath) => normalizeModulePath(rootPath))
47
+ .filter((rootPath, index, list) => list.indexOf(rootPath) === index);
48
+ const frameworkSourceRoot = getFrameworkSourceRoot();
35
49
 
36
50
  const rewriteCandidate = (candidate: string) =>
37
- normalizeModulePath(candidate).startsWith(installedCoreRoot + '/')
38
- ? activeCoreRoot + normalizeModulePath(candidate).substring(installedCoreRoot.length)
39
- : candidate;
51
+ visibleFrameworkRoots.reduce((nextCandidate, rootPath) => {
52
+ const normalizedCandidate = normalizeModulePath(nextCandidate);
53
+
54
+ return normalizedCandidate.startsWith(rootPath + '/')
55
+ ? frameworkSourceRoot + normalizedCandidate.substring(rootPath.length)
56
+ : nextCandidate;
57
+ }, candidate);
40
58
 
41
59
  return Object.fromEntries(
42
60
  Object.entries(aliases).map(([alias, value]) => [
@@ -61,8 +79,9 @@ const isExternalVendorModule = (module: Module) => {
61
79
 
62
80
  const isCoreSourceModule = (module: Module) => {
63
81
  const modulePath = getModulePath(module);
82
+ const frameworkSourceRoot = getFrameworkSourceRoot();
64
83
 
65
- return modulePath.startsWith(normalizedCoreRoot + '/') || modulePath.includes('/node_modules/proteum/');
84
+ return modulePath.startsWith(frameworkSourceRoot + '/') || modulePath.includes('/node_modules/proteum/');
66
85
  };
67
86
 
68
87
  /*----------------------------------
@@ -76,9 +95,12 @@ export default function createCompiler(
76
95
  logVerbose(`Creating compiler for client (${mode}).`);
77
96
  const dev = mode === 'dev';
78
97
  const outputPath = app.outputPath(outputTarget);
79
- const installedCoreRoot = path.join(app.paths.root, 'node_modules', 'proteum');
80
- const frameworkRoots = [cli.paths.core.root, installedCoreRoot];
98
+ const frameworkSourceRoot = getFrameworkSourceRoot();
99
+ const frameworkRoots = [frameworkSourceRoot, ...cli.paths.getFrameworkRoots()].filter(
100
+ (rootPath, index, list) => list.indexOf(rootPath) === index,
101
+ );
81
102
  const transpileModuleDirectories = app.transpileModuleDirectories;
103
+ const hmrClientEntry = path.join(frameworkSourceRoot, 'client', 'dev', 'hmr.ts');
82
104
 
83
105
  const commonConfig = createCommonConfig(app, 'client', mode, outputTarget);
84
106
 
@@ -93,14 +115,15 @@ export default function createCompiler(
93
115
  );*/
94
116
 
95
117
  // Convert tsconfig paths into bundler aliases.
96
- const { aliases } = app.aliases.client.forWebpack({ modulesPath: app.paths.root + '/node_modules' });
97
- const resolvedAliases = rewriteFrameworkAliasTargets(app, aliases);
118
+ const { aliases } = app.aliases.client.forWebpack({ modulesPath: cli.paths.framework.appNodeModulesRoot });
119
+ const resolvedAliases = rewriteFrameworkAliasTargets(aliases);
98
120
 
99
121
  // We're not supposed in any case to import server libs from client
100
122
  delete resolvedAliases['@server'];
101
123
  delete resolvedAliases['@/server'];
102
124
  const rspackAliases = toRspackAliases(resolvedAliases);
103
- rspackAliases['@/client/router$'] = cli.paths.core.root + '/client/router.ts';
125
+ rspackAliases['proteum'] = frameworkSourceRoot;
126
+ rspackAliases['@/client/router$'] = frameworkSourceRoot + '/client/router.ts';
104
127
  rspackAliases['preact/jsx-runtime$'] = resolveFromAppOrCore(app, 'preact/jsx-runtime');
105
128
  rspackAliases['react/jsx-runtime$'] = resolveFromAppOrCore(app, 'preact/jsx-runtime');
106
129
  rspackAliases['react/jsx-dev-runtime$'] = resolveFromAppOrCore(app, 'preact/jsx-dev-runtime');
@@ -113,8 +136,8 @@ export default function createCompiler(
113
136
  target: 'web',
114
137
  entry: {
115
138
  client: dev
116
- ? [hmrClientEntry, cli.paths.core.root + '/client/index.ts']
117
- : [cli.paths.core.root + '/client/index.ts'],
139
+ ? [hmrClientEntry, frameworkSourceRoot + '/client/index.ts']
140
+ : [frameworkSourceRoot + '/client/index.ts'],
118
141
  },
119
142
 
120
143
  output: {
@@ -168,7 +191,7 @@ export default function createCompiler(
168
191
  ...transpileModuleDirectories,
169
192
  ],
170
193
  loader: path.join(
171
- cli.paths.core.root,
194
+ frameworkSourceRoot,
172
195
  'cli',
173
196
  'compiler',
174
197
  'common',
@@ -229,11 +252,11 @@ export default function createCompiler(
229
252
  : [
230
253
  new rspack.NormalModuleReplacementPlugin(
231
254
  /^@client\/dev\/profiler$/,
232
- cli.paths.core.root + '/client/dev/profiler/noop.tsx',
255
+ frameworkSourceRoot + '/client/dev/profiler/noop.tsx',
233
256
  ),
234
257
  new rspack.NormalModuleReplacementPlugin(
235
258
  /^@client\/dev\/profiler\/runtime$/,
236
- cli.paths.core.root + '/client/dev/profiler/runtime.noop.ts',
259
+ frameworkSourceRoot + '/client/dev/profiler/runtime.noop.ts',
237
260
  ),
238
261
  ]),
239
262
 
@@ -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
  };
@@ -45,6 +45,21 @@ export default function createCommonConfig(
45
45
  ): Configuration {
46
46
  const dev = mode === 'dev';
47
47
  const enableFilesystemCache = dev ? cli.args.cache !== false : cli.args.cache === true;
48
+ const frameworkPackageRoots = [cli.paths.framework.installedRoot, cli.paths.framework.activeRoot].filter(
49
+ (rootPath, index, list): rootPath is string => typeof rootPath === 'string' && list.indexOf(rootPath) === index,
50
+ );
51
+ const visibleNodeModulesRoots = [
52
+ ...cli.paths.getVisibleNodeModulesRootsForPath(app.paths.root),
53
+ ...frameworkPackageRoots.flatMap((rootPath) => cli.paths.getVisibleNodeModulesRootsForPath(rootPath)),
54
+ ...cli.paths.getVisibleNodeModulesRootsForPath(cli.paths.core.cli),
55
+ ].filter((moduleRoot, index, list) => list.indexOf(moduleRoot) === index);
56
+ const loaderModuleRoots = [
57
+ ...visibleNodeModulesRoots,
58
+ ...frameworkPackageRoots.map((rootPath) => path.join(rootPath, 'node_modules')),
59
+ cli.paths.framework.appNodeModulesRoot,
60
+ cli.paths.framework.frameworkNodeModulesRoot,
61
+ path.join(cli.paths.core.cli, 'node_modules'),
62
+ ].filter((moduleRoot, index, list) => list.indexOf(moduleRoot) === index);
48
63
  const config: Configuration = {
49
64
  // Project root
50
65
  context: app.paths.root,
@@ -55,11 +70,7 @@ export default function createCommonConfig(
55
70
  // Support both install modes:
56
71
  // - npm i: loaders are often hoisted in app/node_modules
57
72
  // - npm link: loaders often live in framework/node_modules
58
- modules: [
59
- app.paths.root + '/node_modules',
60
- cli.paths.core.root + '/node_modules',
61
- cli.paths.core.cli + '/node_modules',
62
- ],
73
+ modules: loaderModuleRoots,
63
74
  mainFields: ['loader', 'main'],
64
75
  },
65
76
 
@@ -9,6 +9,7 @@ import { rspack, type Compiler as RspackCompiler } from '@rspack/core';
9
9
 
10
10
  // Core
11
11
  import app from '../app';
12
+ import cli from '..';
12
13
  import createServerConfig from './server';
13
14
  import createClientConfig from './client';
14
15
  import { TCompileMode, TCompileOutputTarget } from './common';
@@ -62,15 +63,21 @@ export default class Compiler {
62
63
  - Including React, so VSCode shows that JSX is missing
63
64
  */
64
65
  public fixNpmLinkIssues() {
65
- const corePath = path.join(app.paths.root, '/node_modules/proteum');
66
- if (!fs.lstatSync(corePath).isSymbolicLink())
67
- return logVerbose("Not fixing npm issue because proteum wasn't installed with npm link.");
66
+ const installedFrameworkRoot = cli.paths.framework.installedRoot;
67
+
68
+ if (!installedFrameworkRoot || !fs.existsSync(installedFrameworkRoot)) {
69
+ return logVerbose("Not fixing npm link issues because the app can't see an installed Proteum package.");
70
+ }
71
+
72
+ if (!fs.lstatSync(installedFrameworkRoot).isSymbolicLink()) {
73
+ return logVerbose("Not fixing npm link issues because Proteum wasn't installed with npm link.");
74
+ }
68
75
 
69
76
  this.debug && logVerbose(`Fix NPM link issues ...`);
70
77
  const outputPath = app.outputPath(this.outputTarget);
71
78
 
72
- const appModules = path.join(app.paths.root, 'node_modules');
73
- const coreModules = path.join(corePath, 'node_modules');
79
+ const appModules = cli.paths.framework.appNodeModulesRoot;
80
+ const coreModules = cli.paths.framework.frameworkNodeModulesRoot;
74
81
 
75
82
  // When the 5htp package is installed from npm link,
76
83
  // Modules are installed locally and not glbally as with with the 5htp package from NPM.