proteum 2.0.0-1 → 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.
- package/AGENTS.md +13 -1
- package/README.md +375 -0
- package/agents/framework/AGENTS.md +917 -0
- package/agents/project/AGENTS.md +138 -0
- package/agents/{codex → project}/CODING_STYLE.md +3 -2
- package/agents/project/client/AGENTS.md +108 -0
- package/agents/{codex → project}/client/pages/AGENTS.md +8 -8
- package/agents/{codex → project}/server/routes/AGENTS.md +2 -1
- package/agents/project/server/services/AGENTS.md +170 -0
- package/agents/{codex → project}/tests/AGENTS.md +1 -0
- package/cli/app/config.ts +3 -2
- package/cli/app/index.ts +6 -66
- package/cli/bin.js +7 -2
- package/cli/commands/build.ts +94 -27
- package/cli/commands/check.ts +15 -1
- package/cli/commands/dev.ts +288 -132
- package/cli/commands/doctor.ts +108 -0
- package/cli/commands/explain.ts +226 -0
- package/cli/commands/init.ts +76 -70
- package/cli/commands/lint.ts +18 -1
- package/cli/commands/refresh.ts +16 -6
- package/cli/commands/typecheck.ts +14 -1
- package/cli/compiler/artifacts/controllers.ts +150 -0
- package/cli/compiler/artifacts/discovery.ts +132 -0
- package/cli/compiler/artifacts/manifest.ts +267 -0
- package/cli/compiler/artifacts/routing.ts +315 -0
- package/cli/compiler/artifacts/services.ts +480 -0
- package/cli/compiler/artifacts/shared.ts +12 -0
- package/cli/compiler/client/identite.ts +2 -1
- package/cli/compiler/client/index.ts +13 -3
- package/cli/compiler/common/controllers.ts +23 -28
- package/cli/compiler/common/files/style.ts +3 -4
- package/cli/compiler/common/generatedRouteModules.ts +333 -19
- package/cli/compiler/common/proteumManifest.ts +133 -0
- package/cli/compiler/index.ts +33 -896
- package/cli/compiler/server/index.ts +21 -4
- package/cli/context.ts +71 -0
- package/cli/index.ts +39 -181
- package/cli/presentation/commands.ts +208 -0
- package/cli/presentation/compileReporter.ts +65 -0
- package/cli/presentation/devSession.ts +70 -0
- package/cli/presentation/help.ts +193 -0
- package/cli/presentation/ink.ts +69 -0
- package/cli/presentation/layout.ts +83 -0
- package/cli/runtime/argv.ts +49 -0
- package/cli/runtime/command.ts +25 -0
- package/cli/runtime/commands.ts +221 -0
- package/cli/runtime/importEsm.ts +7 -0
- package/cli/runtime/verbose.ts +15 -0
- package/cli/utils/agents.ts +5 -4
- package/cli/utils/keyboard.ts +12 -6
- package/client/app/index.ts +0 -6
- package/client/services/router/index.tsx +1 -1
- package/client/services/router/response/index.tsx +2 -2
- package/common/dev/serverHotReload.ts +12 -0
- package/common/router/index.ts +3 -2
- package/common/router/layouts.ts +1 -1
- package/common/router/pageSetup.ts +1 -0
- package/package.json +10 -8
- package/prettier/router-registration-plugin.cjs +52 -0
- package/prettier.config.cjs +1 -0
- package/scripts/cleanup-generated-controllers.ts +2 -2
- package/scripts/fix-reference-app-typing.ts +2 -2
- package/scripts/format-router-registrations.ts +119 -0
- package/scripts/migrate-explicit-controllers-and-request.ts +423 -0
- package/scripts/refactor-server-controllers.ts +19 -18
- package/scripts/refactor-server-runtime-aliases.ts +1 -1
- package/server/app/commands.ts +309 -25
- package/server/app/container/config.ts +1 -1
- package/server/app/container/index.ts +2 -2
- package/server/app/controller/index.ts +13 -4
- package/server/app/index.ts +53 -37
- package/server/app/service/container.ts +26 -28
- package/server/app/service/index.ts +10 -20
- package/server/app.tsconfig.json +9 -2
- package/server/index.ts +32 -1
- package/server/services/auth/index.ts +234 -15
- package/server/services/auth/router/index.ts +39 -7
- package/server/services/auth/router/request.ts +40 -8
- package/server/services/disks/index.ts +1 -1
- package/server/services/prisma/Facet.ts +2 -2
- package/server/services/prisma/index.ts +22 -5
- package/server/services/prisma/mariadb.ts +47 -0
- package/server/services/router/http/index.ts +9 -1
- package/server/services/router/index.ts +10 -4
- package/server/services/router/response/index.ts +26 -6
- package/types/auth-check-rules.test.ts +51 -0
- package/types/controller-request-context.test.ts +55 -0
- package/types/service-config.test.ts +39 -0
- package/agents/codex/AGENTS.md +0 -95
- package/agents/codex/client/AGENTS.md +0 -102
- package/agents/codex/server/services/AGENTS.md +0 -137
- package/server/services/models.7z +0 -0
- /package/agents/{codex → project}/agents.md.zip +0 -0
package/cli/commands/build.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
};
|
package/cli/commands/check.ts
CHANGED
|
@@ -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(
|
|
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
|
};
|
package/cli/commands/dev.ts
CHANGED
|
@@ -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
|
-
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
/*----------------------------------
|
|
55
|
+
- HELPERS
|
|
56
|
+
----------------------------------*/
|
|
55
57
|
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
124
|
-
console.log('Watch callback. Server compilation failed; keeping current app instance.');
|
|
125
|
-
}
|
|
83
|
+
let settled = false;
|
|
126
84
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
94
|
+
const onExit = () => finish(true);
|
|
95
|
+
const onClose = () => finish(true);
|
|
96
|
+
const timeout = setTimeout(() => finish(false), timeoutMs);
|
|
137
97
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
98
|
+
child.once('exit', onExit);
|
|
99
|
+
child.once('close', onClose);
|
|
100
|
+
});
|
|
141
101
|
|
|
142
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
120
|
+
child.kill(signal);
|
|
121
|
+
return true;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
const errno = error as NodeJS.ErrnoException;
|
|
149
124
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
163
|
-
|
|
177
|
+
async function stopAppInternal(reason: string) {
|
|
178
|
+
const currentApp = cp;
|
|
179
|
+
if (currentApp === undefined) return;
|
|
164
180
|
|
|
165
|
-
|
|
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
|
-
|
|
173
|
-
stopApp('Restart asked');
|
|
185
|
+
if (!signalAppProcess(currentApp, 'SIGTERM')) return;
|
|
174
186
|
|
|
175
|
-
|
|
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
|
-
|
|
182
|
-
if (!isServerHotReloadResult(message)) return;
|
|
189
|
+
logVerbose(`Server instance ${currentApp.pid} did not stop after SIGTERM. Escalating to SIGKILL.`);
|
|
183
190
|
|
|
184
|
-
|
|
185
|
-
console.log('Server hot reload applied without restarting app.');
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
191
|
+
if (!signalAppProcess(currentApp, 'SIGKILL')) return;
|
|
188
192
|
|
|
189
|
-
|
|
190
|
-
startApp(app);
|
|
191
|
-
});
|
|
193
|
+
await waitForChildExit(currentApp, 2000);
|
|
192
194
|
}
|
|
193
195
|
|
|
194
|
-
function stopApp(reason: string) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
208
|
-
|
|
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
|
+
};
|