proteum 2.0.0 → 2.1.0

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.
Files changed (94) hide show
  1. package/AGENTS.md +13 -1
  2. package/README.md +375 -0
  3. package/agents/framework/AGENTS.md +917 -0
  4. package/agents/project/AGENTS.md +138 -0
  5. package/agents/{codex → project}/CODING_STYLE.md +3 -2
  6. package/agents/project/client/AGENTS.md +108 -0
  7. package/agents/{codex → project}/client/pages/AGENTS.md +8 -8
  8. package/agents/{codex → project}/server/routes/AGENTS.md +2 -1
  9. package/agents/project/server/services/AGENTS.md +170 -0
  10. package/agents/{codex → project}/tests/AGENTS.md +1 -0
  11. package/cli/app/config.ts +3 -2
  12. package/cli/app/index.ts +6 -66
  13. package/cli/bin.js +7 -2
  14. package/cli/commands/build.ts +94 -27
  15. package/cli/commands/check.ts +15 -1
  16. package/cli/commands/dev.ts +288 -132
  17. package/cli/commands/doctor.ts +108 -0
  18. package/cli/commands/explain.ts +226 -0
  19. package/cli/commands/init.ts +76 -70
  20. package/cli/commands/lint.ts +18 -1
  21. package/cli/commands/refresh.ts +16 -6
  22. package/cli/commands/typecheck.ts +14 -1
  23. package/cli/compiler/artifacts/controllers.ts +150 -0
  24. package/cli/compiler/artifacts/discovery.ts +132 -0
  25. package/cli/compiler/artifacts/manifest.ts +267 -0
  26. package/cli/compiler/artifacts/routing.ts +315 -0
  27. package/cli/compiler/artifacts/services.ts +480 -0
  28. package/cli/compiler/artifacts/shared.ts +12 -0
  29. package/cli/compiler/client/identite.ts +2 -1
  30. package/cli/compiler/client/index.ts +13 -3
  31. package/cli/compiler/common/controllers.ts +23 -28
  32. package/cli/compiler/common/files/style.ts +3 -4
  33. package/cli/compiler/common/generatedRouteModules.ts +333 -19
  34. package/cli/compiler/common/proteumManifest.ts +133 -0
  35. package/cli/compiler/index.ts +33 -896
  36. package/cli/compiler/server/index.ts +21 -4
  37. package/cli/context.ts +71 -0
  38. package/cli/index.ts +39 -181
  39. package/cli/presentation/commands.ts +208 -0
  40. package/cli/presentation/compileReporter.ts +65 -0
  41. package/cli/presentation/devSession.ts +70 -0
  42. package/cli/presentation/help.ts +193 -0
  43. package/cli/presentation/ink.ts +69 -0
  44. package/cli/presentation/layout.ts +83 -0
  45. package/cli/runtime/argv.ts +49 -0
  46. package/cli/runtime/command.ts +25 -0
  47. package/cli/runtime/commands.ts +221 -0
  48. package/cli/runtime/importEsm.ts +7 -0
  49. package/cli/runtime/verbose.ts +15 -0
  50. package/cli/utils/agents.ts +5 -4
  51. package/cli/utils/keyboard.ts +12 -6
  52. package/client/app/index.ts +0 -6
  53. package/client/services/router/index.tsx +1 -1
  54. package/client/services/router/response/index.tsx +2 -2
  55. package/common/dev/serverHotReload.ts +12 -0
  56. package/common/router/index.ts +3 -2
  57. package/common/router/layouts.ts +1 -1
  58. package/common/router/pageSetup.ts +1 -0
  59. package/package.json +10 -8
  60. package/prettier/router-registration-plugin.cjs +52 -0
  61. package/prettier.config.cjs +1 -0
  62. package/scripts/cleanup-generated-controllers.ts +2 -2
  63. package/scripts/fix-reference-app-typing.ts +2 -2
  64. package/scripts/format-router-registrations.ts +119 -0
  65. package/scripts/migrate-explicit-controllers-and-request.ts +423 -0
  66. package/scripts/refactor-server-controllers.ts +19 -18
  67. package/scripts/refactor-server-runtime-aliases.ts +1 -1
  68. package/server/app/commands.ts +309 -25
  69. package/server/app/container/config.ts +1 -1
  70. package/server/app/container/index.ts +2 -2
  71. package/server/app/controller/index.ts +13 -4
  72. package/server/app/index.ts +53 -37
  73. package/server/app/service/container.ts +26 -28
  74. package/server/app/service/index.ts +10 -20
  75. package/server/app.tsconfig.json +9 -2
  76. package/server/index.ts +32 -1
  77. package/server/services/auth/index.ts +234 -15
  78. package/server/services/auth/router/index.ts +39 -7
  79. package/server/services/auth/router/request.ts +40 -8
  80. package/server/services/disks/index.ts +1 -1
  81. package/server/services/prisma/Facet.ts +2 -2
  82. package/server/services/prisma/index.ts +22 -5
  83. package/server/services/prisma/mariadb.ts +47 -0
  84. package/server/services/router/http/index.ts +9 -1
  85. package/server/services/router/index.ts +10 -4
  86. package/server/services/router/response/index.ts +26 -6
  87. package/types/auth-check-rules.test.ts +51 -0
  88. package/types/controller-request-context.test.ts +55 -0
  89. package/types/service-config.test.ts +39 -0
  90. package/agents/codex/AGENTS.md +0 -95
  91. package/agents/codex/client/AGENTS.md +0 -102
  92. package/agents/codex/server/services/AGENTS.md +0 -137
  93. package/server/services/models.7z +0 -0
  94. /package/agents/{codex → project}/agents.md.zip +0 -0
@@ -13,20 +13,24 @@ import {
13
13
  getClientBundleAnalysisReportPaths,
14
14
  waitForClientBundleAnalysisArtifacts,
15
15
  } from '../compiler/common/bundleAnalysis';
16
+ import { refreshGeneratedTypings, runAppTypecheck } from '../utils/check';
17
+ import { renderRows } from '../presentation/layout';
18
+ import { renderStep, renderSuccess, renderTitle } from '../presentation/ink';
16
19
 
17
- const allowedBuildArgs = new Set(['prod', 'cache', 'analyze']);
20
+ const allowedBuildArgs = new Set(['prod', 'cache', 'analyze', 'strict']);
21
+ type TBuildMultiCompiler = Awaited<ReturnType<Compiler['create']>>;
18
22
 
19
23
  /*----------------------------------
20
24
  - COMMAND
21
25
  ----------------------------------*/
22
26
  function resolveBuildMode(): TCompileMode {
23
27
  const enabledArgs = Object.entries(cli.args)
24
- .filter(([name, value]) => name !== 'workdir' && value === true)
28
+ .filter(([name, value]) => name !== 'workdir' && name !== 'verbose' && value === true)
25
29
  .map(([name]) => name);
26
30
 
27
31
  const invalidArgs = enabledArgs.filter((arg) => !allowedBuildArgs.has(arg));
28
32
  if (invalidArgs.length > 0)
29
- throw new Error(`Unknown build argument(s): ${invalidArgs.join(', ')}. Allowed values: prod, cache, analyze.`);
33
+ throw new Error(`Unknown build argument(s): ${invalidArgs.join(', ')}. Allowed values: prod, cache, analyze, strict.`);
30
34
 
31
35
  const requestedModes = enabledArgs.filter((arg): arg is TCompileMode => arg === 'prod');
32
36
  if (requestedModes.length > 1)
@@ -35,37 +39,100 @@ function resolveBuildMode(): TCompileMode {
35
39
  return requestedModes[0] ?? 'prod';
36
40
  }
37
41
 
38
- export const run = async (): Promise<void> => {
39
- const mode = resolveBuildMode();
40
- const compiler = new Compiler(mode, {}, false, 'bin');
41
- const multiCompiler = await compiler.create();
42
-
42
+ const closeMultiCompiler = async (multiCompiler: TBuildMultiCompiler) =>
43
43
  await new Promise<void>((resolve, reject) => {
44
- multiCompiler.run((error, stats) => {
44
+ multiCompiler.close((error) => {
45
45
  if (error) {
46
- console.error('An error occurred during the compilation:', error);
47
46
  reject(error);
48
47
  return;
49
48
  }
50
49
 
51
- if (stats?.hasErrors()) {
52
- reject(new Error(`Compilation failed for build mode "${mode}".`));
53
- return;
54
- }
55
-
56
- if (cli.args.analyze === true) {
57
- waitForClientBundleAnalysisArtifacts(app, 'bin')
58
- .then(() => {
59
- const { reportPath, statsPath } = getClientBundleAnalysisReportPaths(app, 'bin');
60
- console.info(`Client bundle analysis report: ${reportPath}`);
61
- console.info(`Client bundle analysis stats: ${statsPath}`);
62
- resolve();
63
- })
64
- .catch(reject);
65
- return;
66
- }
67
-
68
50
  resolve();
69
51
  });
70
52
  });
53
+
54
+ export const run = async (): Promise<void> => {
55
+ const mode = resolveBuildMode();
56
+ const strict = cli.args.strict === true;
57
+ let analysisArtifacts: { reportPath: string; statsPath: string } | undefined;
58
+
59
+ console.info(
60
+ [
61
+ await renderTitle(
62
+ 'PROTEUM BUILD',
63
+ strict
64
+ ? 'Refreshing contracts, running TypeScript, then producing the bundles.'
65
+ : 'Producing the server and client bundles.',
66
+ ),
67
+ renderRows([
68
+ { label: 'app', value: cli.paths.appRoot === process.cwd() ? '.' : cli.paths.appRoot },
69
+ { label: 'mode', value: mode },
70
+ { label: 'strict', value: strict ? 'enabled' : 'disabled' },
71
+ { label: 'cache', value: cli.args.cache === true ? 'enabled' : 'disabled' },
72
+ { label: 'analyze', value: cli.args.analyze === true ? 'enabled' : 'disabled' },
73
+ { label: 'output', value: 'bin/' },
74
+ ]),
75
+ ].join('\n\n'),
76
+ );
77
+
78
+ if (strict) {
79
+ console.info(await renderStep('[1/3]', 'Refreshing generated typings.'));
80
+ await refreshGeneratedTypings();
81
+ console.info(await renderStep('[2/3]', 'Running TypeScript typechecking.'));
82
+ await runAppTypecheck();
83
+ console.info(await renderStep('[3/3]', 'Running the production compiler.'));
84
+ } else {
85
+ console.info(await renderStep('[1/1]', 'Running the production compiler.'));
86
+ }
87
+
88
+ const compiler = new Compiler(mode, {}, false, 'bin');
89
+ const multiCompiler = await compiler.create();
90
+ let buildError: Error | undefined;
91
+
92
+ try {
93
+ await new Promise<void>((resolve, reject) => {
94
+ multiCompiler.run((error, stats) => {
95
+ if (error) {
96
+ console.error('An error occurred during the compilation:', error);
97
+ reject(error);
98
+ return;
99
+ }
100
+
101
+ if (stats?.hasErrors()) {
102
+ reject(new Error(`Compilation failed for build mode "${mode}".`));
103
+ return;
104
+ }
105
+
106
+ if (cli.args.analyze === true) {
107
+ waitForClientBundleAnalysisArtifacts(app, 'bin')
108
+ .then(() => {
109
+ analysisArtifacts = getClientBundleAnalysisReportPaths(app, 'bin');
110
+ resolve();
111
+ })
112
+ .catch(reject);
113
+ return;
114
+ }
115
+
116
+ resolve();
117
+ });
118
+ });
119
+ } catch (error) {
120
+ buildError = error instanceof Error ? error : new Error(String(error));
121
+ } finally {
122
+ compiler.dispose();
123
+ await closeMultiCompiler(multiCompiler);
124
+ }
125
+
126
+ if (buildError) throw buildError;
127
+
128
+ if (analysisArtifacts !== undefined) {
129
+ console.info(
130
+ renderRows([
131
+ { label: 'report', value: analysisArtifacts.reportPath },
132
+ { label: 'stats', value: analysisArtifacts.statsPath },
133
+ ]),
134
+ );
135
+ }
136
+
137
+ console.info(await renderSuccess(`Build completed in ${mode} mode.`));
71
138
  };
@@ -1,8 +1,12 @@
1
1
  import cli from '..';
2
2
  import { refreshGeneratedTypings, runAppLint, runAppTypecheck } from '../utils/check';
3
+ import { renderRows } from '../presentation/layout';
4
+ import { renderStep, renderSuccess, renderTitle } from '../presentation/ink';
3
5
 
4
6
  const validateCheckArgs = () => {
5
- const enabledArgs = Object.entries(cli.args).filter(([name, value]) => name !== 'workdir' && value === true);
7
+ const enabledArgs = Object.entries(cli.args).filter(
8
+ ([name, value]) => name !== 'workdir' && name !== 'verbose' && value === true,
9
+ );
6
10
 
7
11
  if (enabledArgs.length > 0)
8
12
  throw new Error(
@@ -13,7 +17,17 @@ const validateCheckArgs = () => {
13
17
  export const run = async (): Promise<void> => {
14
18
  validateCheckArgs();
15
19
 
20
+ console.info(
21
+ [
22
+ await renderTitle('PROTEUM CHECK', 'Refreshing contracts, running TypeScript, then running ESLint.'),
23
+ renderRows([{ label: 'app', value: cli.paths.appRoot === process.cwd() ? '.' : cli.paths.appRoot }]),
24
+ ].join('\n\n'),
25
+ );
26
+ console.info(await renderStep('[1/3]', 'Refreshing generated typings.'));
16
27
  await refreshGeneratedTypings();
28
+ console.info(await renderStep('[2/3]', 'Running TypeScript typechecking.'));
17
29
  await runAppTypecheck();
30
+ console.info(await renderStep('[3/3]', 'Running ESLint.'));
18
31
  await runAppLint();
32
+ console.info(await renderSuccess('All checks passed.'));
19
33
  };
@@ -10,6 +10,7 @@ import { spawn, ChildProcess } from 'child_process';
10
10
  import cli from '..';
11
11
  import Keyboard from '../utils/keyboard';
12
12
  import {
13
+ isServerReadyMessage,
13
14
  isServerHotReloadResult,
14
15
  serverHotReloadMessageType,
15
16
  TServerHotReloadRequest,
@@ -19,6 +20,8 @@ import {
19
20
  import Compiler from '../compiler';
20
21
  import { createDevEventServer } from './devEvents';
21
22
  import { ensureProjectAgentSymlinks } from '../utils/agents';
23
+ import { renderDevSession, renderServerReadyBanner } from '../presentation/devSession';
24
+ import { logVerbose } from '../runtime/verbose';
22
25
 
23
26
  // Core
24
27
  import { app, App } from '../app';
@@ -28,7 +31,7 @@ import { app, App } from '../app';
28
31
  ----------------------------------*/
29
32
 
30
33
  // Watch rules shared by the dev compiler and hot reload gate.
31
- const ignoredWatchPathPatterns = /(node_modules\/(?!proteum\/))|(\.generated\/)|(\.cache\/)/;
34
+ const ignoredWatchPathPatterns = /(node_modules\/(?!proteum\/))|(\.generated\/)|(\.cache\/)|(\.proteum\/)/;
32
35
  const hotReloadableServerPathPatterns = [
33
36
  /^client\/pages\//,
34
37
  /^client\/components\//,
@@ -39,164 +42,161 @@ const hotReloadableServerPathPatterns = [
39
42
  const hotReloadableRoots = [() => app.paths.root, () => cli.paths.core.root];
40
43
 
41
44
  /*----------------------------------
42
- - MAIN PROCESS
45
+ - STATE
43
46
  ----------------------------------*/
44
- export const run = () =>
45
- new Promise<void>(async () => {
46
- ensureProjectAgentSymlinks({ appRoot: app.paths.root, coreRoot: cli.paths.core.root });
47
47
 
48
- const devEventServer = await createDevEventServer(app.env.router.port + 1);
49
- app.devEventPort = devEventServer.port;
50
- console.info(`Proteum dev event server ready on http://localhost:${devEventServer.port}/__proteum_hmr`);
48
+ // Current server child process used by the dev loop.
49
+ let cp: ChildProcess | undefined = undefined;
50
+ let devSessionStopping = false;
51
+ let appProcessOperation: Promise<void> = Promise.resolve();
52
+ type TDevWatching = ReturnType<Awaited<ReturnType<Compiler['create']>>['watch']>;
51
53
 
52
- const compiler = new Compiler('dev', {
53
- before: (compiler) => {
54
- if (compiler.name !== 'server') return;
54
+ /*----------------------------------
55
+ - HELPERS
56
+ ----------------------------------*/
55
57
 
56
- const changedFilesList = compiler.modifiedFiles ? [...compiler.modifiedFiles] : [];
58
+ const closeWatching = async (watching: TDevWatching) =>
59
+ await new Promise<void>((resolve, reject) => {
60
+ watching.close((error?: Error | null) => {
61
+ if (error) {
62
+ reject(error);
63
+ return;
64
+ }
57
65
 
58
- if (changedFilesList.length === 0) {
59
- console.info('Server compilation started. App restart will wait for a successful server build.');
60
- } else {
61
- console.info('Need to recompile server because files changed:\n' + changedFilesList.join('\n'));
62
- }
63
- },
64
- after: () => {},
66
+ resolve();
65
67
  });
68
+ });
66
69
 
67
- const multiCompiler = await compiler.create();
68
- const ignoredOutputPaths = [app.paths.bin, app.paths.dev].map(normalizeWatchPath);
69
-
70
- multiCompiler.watch(
71
- {
72
- // Watching may not work with NFS and machines in VirtualBox
73
- // Uncomment next line if it is your case (use true or interval in milliseconds)
74
- //poll: 1000,
75
-
76
- // Decrease CPU or memory usage in some file systems
77
- // Ignore updated from:
78
- // - Node modules except 5HTP core (framework dev mode)
79
- // - Generated files during runtime (cause infinite loop. Ex: models.d.ts)
80
- // - Webpack output folders (`./dev`, legacy `./bin`)
81
- ignored: (watchPath: string) => {
82
- const normalizedPath = normalizeWatchPath(watchPath);
83
- return (
84
- ignoredWatchPathPatterns.test(normalizedPath) ||
85
- ignoredOutputPaths.some(
86
- (outputPath) =>
87
- normalizedPath === outputPath || normalizedPath.startsWith(outputPath + '/'),
88
- )
89
- );
90
- },
91
-
92
- //aggregateTimeout: 1000,
93
- },
94
- async (error, stats) => {
95
- if (error) {
96
- compiler.consumeRecentCompilationResults();
97
- console.error('Error in milticompiler.watch', error, stats?.toString());
98
- return;
99
- }
70
+ const runSerializedAppProcessOperation = async <T>(operation: () => Promise<T>) => {
71
+ const resultPromise = appProcessOperation.catch(() => undefined).then(() => operation());
72
+ appProcessOperation = resultPromise.then(() => undefined, () => undefined);
73
+ return resultPromise;
74
+ };
100
75
 
101
- const recentCompilationResults = compiler.consumeRecentCompilationResults();
102
- const serverResult = recentCompilationResults.server;
103
- const clientResult = recentCompilationResults.client;
104
-
105
- let restartedServer = false;
106
-
107
- if (serverResult?.succeeded === true) {
108
- const changedFilesList = serverResult.modifiedFiles || [];
109
- const canHotReloadServer = isServerHotReloadEligible(changedFilesList);
110
-
111
- if (canHotReloadServer && requestServerHotReload(changedFilesList)) {
112
- console.log(
113
- 'Watch callback. Server route bundle changed; hot-swapping generated routes without restarting app.',
114
- );
115
- } else {
116
- console.log('Watch callback. Reloading app because server bundle changed ...');
117
- startApp(app);
118
- restartedServer = true;
119
- devEventServer.broadcast({ type: 'reload', reason: 'server' });
120
- }
121
- }
76
+ const waitForChildExit = async (child: ChildProcess, timeoutMs: number) =>
77
+ await new Promise<boolean>((resolve) => {
78
+ if (child.exitCode !== null || child.signalCode !== null) {
79
+ resolve(true);
80
+ return;
81
+ }
122
82
 
123
- if (serverResult?.succeeded === false) {
124
- console.log('Watch callback. Server compilation failed; keeping current app instance.');
125
- }
83
+ let settled = false;
126
84
 
127
- if (!restartedServer && clientResult?.succeeded === true) {
128
- console.log('Watch callback. Client assets updated; server restart skipped.');
129
- devEventServer.broadcast({ type: 'reload', reason: 'client' });
130
- return;
131
- }
85
+ const finish = (result: boolean) => {
86
+ if (settled) return;
87
+ settled = true;
88
+ clearTimeout(timeout);
89
+ child.off('exit', onExit);
90
+ child.off('close', onClose);
91
+ resolve(result);
92
+ };
132
93
 
133
- if (!restartedServer && clientResult?.succeeded === false) {
134
- console.log('Watch callback. Client compilation failed; server restart skipped.');
135
- return;
136
- }
94
+ const onExit = () => finish(true);
95
+ const onClose = () => finish(true);
96
+ const timeout = setTimeout(() => finish(false), timeoutMs);
137
97
 
138
- if (restartedServer || serverResult?.succeeded === true || serverResult?.succeeded === false) {
139
- return;
140
- }
98
+ child.once('exit', onExit);
99
+ child.once('close', onClose);
100
+ });
141
101
 
142
- console.log('Watch callback. No compiler changes were tracked.');
143
- },
144
- );
102
+ const escapeForRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
103
+ const createIgnoredWatchPattern = (outputPaths: string[]) =>
104
+ new RegExp(
105
+ [
106
+ ignoredWatchPathPatterns.source,
107
+ ...outputPaths.map((outputPath) => `(?:^${escapeForRegExp(outputPath)}(?:/|$))`),
108
+ ].join('|'),
109
+ );
110
+ const getDevAppName = (app: App) =>
111
+ app.identity.web?.fullTitle || app.identity.web?.title || app.identity.name || app.packageJson.name || app.paths.root;
112
+
113
+ const signalAppProcess = (child: ChildProcess, signal: NodeJS.Signals) => {
114
+ try {
115
+ if (process.platform !== 'win32' && child.pid !== undefined) {
116
+ process.kill(-child.pid, signal);
117
+ return true;
118
+ }
145
119
 
146
- Keyboard.input('ctrl+r', async () => {
147
- console.log('Waiting for compilers to be ready ...', Object.keys(compiler.compiling));
148
- await Promise.all(Object.values(compiler.compiling));
120
+ child.kill(signal);
121
+ return true;
122
+ } catch (error) {
123
+ const errno = error as NodeJS.ErrnoException;
149
124
 
150
- console.log('Reloading app ...');
151
- startApp(app);
152
- devEventServer.broadcast({ type: 'reload', reason: 'manual' });
125
+ if (errno.code === 'ESRCH') return false;
126
+
127
+ throw error;
128
+ }
129
+ };
130
+
131
+ async function startApp(app: App) {
132
+ await runSerializedAppProcessOperation(async () => {
133
+ if (devSessionStopping) return;
134
+
135
+ await stopAppInternal('Restart asked');
136
+ if (devSessionStopping) return;
137
+
138
+ logVerbose('Launching new server ...');
139
+ cp = spawn('node', ['--preserve-symlinks', app.outputPath('dev') + '/server.js'], {
140
+ // stdin, stdout, stderr
141
+ stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
142
+ detached: true,
153
143
  });
154
144
 
155
- Keyboard.input('ctrl+c', () => {
156
- stopApp('CTRL+C Pressed');
157
- void devEventServer.close();
145
+ const child = cp;
146
+
147
+ child.on('exit', () => {
148
+ if (cp === child) cp = undefined;
149
+ });
150
+
151
+ child.on('message', (message: unknown) => {
152
+ if (isServerReadyMessage(message)) {
153
+ void (async () => {
154
+ console.info(
155
+ await renderServerReadyBanner({
156
+ appName: getDevAppName(app),
157
+ publicUrl: message.publicUrl,
158
+ }),
159
+ );
160
+ })();
161
+ return;
162
+ }
163
+
164
+ if (!isServerHotReloadResult(message)) return;
165
+
166
+ if (message.type === serverHotReloadMessageType.succeeded) {
167
+ logVerbose('Server hot reload applied without restarting app.');
168
+ return;
169
+ }
170
+
171
+ console.error('Server hot reload failed. Restarting app with a fresh process.', message.error || '');
172
+ void startApp(app);
158
173
  });
159
174
  });
175
+ }
160
176
 
161
- /*----------------------------------
162
- - STATE
163
- ----------------------------------*/
177
+ async function stopAppInternal(reason: string) {
178
+ const currentApp = cp;
179
+ if (currentApp === undefined) return;
164
180
 
165
- // Current server child process used by the dev loop.
166
- let cp: ChildProcess | undefined = undefined;
181
+ cp = undefined;
167
182
 
168
- /*----------------------------------
169
- - HELPERS
170
- ----------------------------------*/
183
+ logVerbose(`Killing current server instance (ID: ${currentApp.pid}) for the following reason:`, reason);
171
184
 
172
- async function startApp(app: App) {
173
- stopApp('Restart asked');
185
+ if (!signalAppProcess(currentApp, 'SIGTERM')) return;
174
186
 
175
- console.info('Launching new server ...');
176
- cp = spawn('node', ['--preserve-symlinks', app.outputPath('dev') + '/server.js'], {
177
- // sdin, sdout, sderr
178
- stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
179
- });
187
+ if (await waitForChildExit(currentApp, 5000)) return;
180
188
 
181
- cp.on('message', (message: unknown) => {
182
- if (!isServerHotReloadResult(message)) return;
189
+ logVerbose(`Server instance ${currentApp.pid} did not stop after SIGTERM. Escalating to SIGKILL.`);
183
190
 
184
- if (message.type === serverHotReloadMessageType.succeeded) {
185
- console.log('Server hot reload applied without restarting app.');
186
- return;
187
- }
191
+ if (!signalAppProcess(currentApp, 'SIGKILL')) return;
188
192
 
189
- console.error('Server hot reload failed. Restarting app with a fresh process.', message.error || '');
190
- startApp(app);
191
- });
193
+ await waitForChildExit(currentApp, 2000);
192
194
  }
193
195
 
194
- function stopApp(reason: string) {
195
- if (cp !== undefined) {
196
- console.info(`Killing current server instance (ID: ${cp.pid}) for the following reason:`, reason);
197
- cp.kill();
198
- cp = undefined;
199
- }
196
+ async function stopApp(reason: string) {
197
+ await runSerializedAppProcessOperation(async () => {
198
+ await stopAppInternal(reason);
199
+ });
200
200
  }
201
201
 
202
202
  function requestServerHotReload(changedFiles: string[]) {
@@ -204,8 +204,12 @@ function requestServerHotReload(changedFiles: string[]) {
204
204
 
205
205
  const message: TServerHotReloadRequest = { type: serverHotReloadMessageType.request, changedFiles };
206
206
 
207
- cp.send(message);
208
- return true;
207
+ try {
208
+ cp.send(message);
209
+ return true;
210
+ } catch {
211
+ return false;
212
+ }
209
213
  }
210
214
 
211
215
  function isServerHotReloadEligible(changedFiles: string[]) {
@@ -232,3 +236,155 @@ function isServerHotReloadEligible(changedFiles: string[]) {
232
236
  function normalizeWatchPath(watchPath: string) {
233
237
  return path.resolve(watchPath).replace(/\\/g, '/').replace(/\/$/, '');
234
238
  }
239
+
240
+ /*----------------------------------
241
+ - MAIN PROCESS
242
+ ----------------------------------*/
243
+ export const run = async () => {
244
+ devSessionStopping = false;
245
+ ensureProjectAgentSymlinks({ appRoot: app.paths.root, coreRoot: cli.paths.core.root });
246
+
247
+ const devEventServer = await createDevEventServer(app.env.router.port + 1);
248
+ app.devEventPort = devEventServer.port;
249
+ console.info(
250
+ await renderDevSession({
251
+ appName: getDevAppName(app),
252
+ appRoot: app.paths.root === process.cwd() ? '.' : app.paths.root,
253
+ routerPort: app.env.router.port,
254
+ devEventPort: devEventServer.port,
255
+ }),
256
+ );
257
+
258
+ const compiler = new Compiler('dev', {
259
+ before: (compiler) => {
260
+ if (compiler.name !== 'server') return;
261
+
262
+ const changedFilesList = compiler.modifiedFiles ? [...compiler.modifiedFiles] : [];
263
+
264
+ if (changedFilesList.length === 0) {
265
+ logVerbose('Server compilation started. App restart will wait for a successful server build.');
266
+ } else {
267
+ logVerbose('Need to recompile server because files changed:\n' + changedFilesList.join('\n'));
268
+ }
269
+ },
270
+ after: () => {},
271
+ });
272
+
273
+ const multiCompiler = await compiler.create();
274
+ const ignoredOutputPaths = [app.paths.bin, app.paths.dev].map(normalizeWatchPath);
275
+ const ignoredWatchPattern = createIgnoredWatchPattern(ignoredOutputPaths);
276
+
277
+ const watching = multiCompiler.watch(
278
+ {
279
+ // Watching may not work with NFS and machines in VirtualBox
280
+ // Uncomment next line if it is your case (use true or interval in milliseconds)
281
+ //poll: 1000,
282
+
283
+ // Decrease CPU or memory usage in some file systems
284
+ // Ignore updated from:
285
+ // - Node modules except 5HTP core (framework dev mode)
286
+ // - Generated files during runtime (cause infinite loop. Ex: models.d.ts)
287
+ // - Webpack output folders (`./dev`, legacy `./bin`)
288
+ ignored: ignoredWatchPattern,
289
+
290
+ //aggregateTimeout: 1000,
291
+ },
292
+ async (error, stats) => {
293
+ if (error) {
294
+ compiler.consumeRecentCompilationResults();
295
+ console.error('Error in milticompiler.watch', error, stats ? stats.toString('errors-warnings') : '');
296
+ return;
297
+ }
298
+
299
+ const recentCompilationResults = compiler.consumeRecentCompilationResults();
300
+ const serverResult = recentCompilationResults.server;
301
+ const clientResult = recentCompilationResults.client;
302
+
303
+ let restartedServer = false;
304
+
305
+ if (serverResult?.succeeded === true) {
306
+ const changedFilesList = serverResult.modifiedFiles || [];
307
+ const canHotReloadServer = isServerHotReloadEligible(changedFilesList);
308
+
309
+ if (canHotReloadServer && requestServerHotReload(changedFilesList)) {
310
+ logVerbose(
311
+ 'Watch callback. Server route bundle changed; hot-swapping generated routes without restarting app.',
312
+ );
313
+ } else {
314
+ logVerbose('Watch callback. Reloading app because server bundle changed ...');
315
+ await startApp(app);
316
+ restartedServer = true;
317
+ devEventServer.broadcast({ type: 'reload', reason: 'server' });
318
+ }
319
+ }
320
+
321
+ if (serverResult?.succeeded === false) {
322
+ logVerbose('Watch callback. Server compilation failed; keeping current app instance.');
323
+ }
324
+
325
+ if (!restartedServer && clientResult?.succeeded === true) {
326
+ logVerbose('Watch callback. Client assets updated; server restart skipped.');
327
+ devEventServer.broadcast({ type: 'reload', reason: 'client' });
328
+ return;
329
+ }
330
+
331
+ if (!restartedServer && clientResult?.succeeded === false) {
332
+ logVerbose('Watch callback. Client compilation failed; server restart skipped.');
333
+ return;
334
+ }
335
+
336
+ if (restartedServer || serverResult?.succeeded === true || serverResult?.succeeded === false) {
337
+ return;
338
+ }
339
+
340
+ logVerbose('Watch callback. No compiler changes were tracked.');
341
+ },
342
+ );
343
+
344
+ let shuttingDownPromise: Promise<void> | undefined;
345
+
346
+ const shutdown = async (reason: string) => {
347
+ if (shuttingDownPromise) return shuttingDownPromise;
348
+
349
+ devSessionStopping = true;
350
+ shuttingDownPromise = (async () => {
351
+ logVerbose('Stopping the Proteum dev session ...', reason);
352
+ await closeWatching(watching);
353
+ compiler.dispose();
354
+ await stopApp(reason);
355
+ await devEventServer.close();
356
+ })();
357
+
358
+ return shuttingDownPromise;
359
+ };
360
+
361
+ const exitAfterShutdown = (reason: string, exitCode: number) => {
362
+ void (async () => {
363
+ try {
364
+ await shutdown(reason);
365
+ process.exit(exitCode);
366
+ } catch (error) {
367
+ console.error(error);
368
+ process.exit(1);
369
+ }
370
+ })();
371
+ };
372
+
373
+ Keyboard.input('ctrl+r', async () => {
374
+ logVerbose('Waiting for compilers to be ready ...', Object.keys(compiler.compiling));
375
+ await Promise.all(Object.values(compiler.compiling));
376
+
377
+ logVerbose('Reloading app ...');
378
+ await startApp(app);
379
+ devEventServer.broadcast({ type: 'reload', reason: 'manual' });
380
+ });
381
+
382
+ Keyboard.input('ctrl+c', async () => {
383
+ await shutdown('CTRL+C Pressed');
384
+ process.exit(0);
385
+ });
386
+
387
+ process.once('SIGINT', () => exitAfterShutdown('SIGINT', 0));
388
+ process.once('SIGTERM', () => exitAfterShutdown('SIGTERM', 0));
389
+ process.once('SIGHUP', () => exitAfterShutdown('SIGHUP', 0));
390
+ };