proteum 1.0.3 → 2.0.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 (184) hide show
  1. package/AGENTS.md +92 -0
  2. package/agents/codex/AGENTS.md +95 -0
  3. package/agents/codex/CODING_STYLE.md +71 -0
  4. package/agents/codex/agents.md.zip +0 -0
  5. package/agents/codex/client/AGENTS.md +102 -0
  6. package/agents/codex/client/pages/AGENTS.md +35 -0
  7. package/agents/codex/server/routes/AGENTS.md +12 -0
  8. package/agents/codex/server/services/AGENTS.md +137 -0
  9. package/agents/codex/tests/AGENTS.md +8 -0
  10. package/cli/app/config.ts +12 -17
  11. package/cli/app/index.ts +59 -99
  12. package/cli/bin.js +1 -1
  13. package/cli/commands/build.ts +23 -12
  14. package/cli/commands/check.ts +19 -0
  15. package/cli/commands/deploy/app.ts +4 -8
  16. package/cli/commands/deploy/web.ts +16 -20
  17. package/cli/commands/dev.ts +185 -75
  18. package/cli/commands/devEvents.ts +106 -0
  19. package/cli/commands/init.ts +63 -57
  20. package/cli/commands/lint.ts +21 -0
  21. package/cli/commands/refresh.ts +6 -6
  22. package/cli/commands/typecheck.ts +18 -0
  23. package/cli/compiler/client/identite.ts +79 -49
  24. package/cli/compiler/client/index.ts +132 -214
  25. package/cli/compiler/common/bundleAnalysis.ts +94 -0
  26. package/cli/compiler/common/clientManifest.ts +67 -0
  27. package/cli/compiler/common/controllers.ts +288 -0
  28. package/cli/compiler/common/files/autres.ts +7 -18
  29. package/cli/compiler/common/files/images.ts +40 -37
  30. package/cli/compiler/common/files/style.ts +12 -25
  31. package/cli/compiler/common/generatedRouteModules.ts +368 -0
  32. package/cli/compiler/common/index.ts +29 -68
  33. package/cli/compiler/common/loaders/forbid-ssr-import.js +13 -0
  34. package/cli/compiler/common/rspackAliases.ts +13 -0
  35. package/cli/compiler/common/scripts.ts +37 -0
  36. package/cli/compiler/index.ts +764 -234
  37. package/cli/compiler/server/index.ts +52 -77
  38. package/cli/compiler/writeIfChanged.ts +21 -0
  39. package/cli/index.ts +65 -90
  40. package/cli/paths.ts +51 -57
  41. package/cli/print.ts +17 -11
  42. package/cli/tsconfig.json +5 -4
  43. package/cli/utils/agents.ts +100 -0
  44. package/cli/utils/check.ts +71 -0
  45. package/cli/utils/index.ts +1 -3
  46. package/cli/utils/keyboard.ts +8 -25
  47. package/cli/utils/runProcess.ts +30 -0
  48. package/client/app/component.tsx +29 -29
  49. package/client/app/index.ts +36 -57
  50. package/client/app/service.ts +7 -12
  51. package/client/app.tsconfig.json +2 -2
  52. package/client/components/Dialog/Manager.ssr.tsx +40 -0
  53. package/client/components/Dialog/Manager.tsx +119 -150
  54. package/client/components/Dialog/status.tsx +3 -3
  55. package/client/components/index.ts +1 -1
  56. package/client/components/types.d.ts +1 -3
  57. package/client/dev/hmr.ts +65 -0
  58. package/client/global.d.ts +2 -2
  59. package/client/hooks.ts +6 -9
  60. package/client/index.ts +2 -1
  61. package/client/islands/index.ts +7 -0
  62. package/client/islands/useDeferredModule.ts +199 -0
  63. package/client/pages/_layout/index.tsx +4 -12
  64. package/client/pages/useHeader.tsx +14 -21
  65. package/client/router.ts +27 -0
  66. package/client/services/router/components/Link.tsx +34 -27
  67. package/client/services/router/components/Page.tsx +6 -14
  68. package/client/services/router/components/router.ssr.tsx +36 -0
  69. package/client/services/router/components/router.tsx +63 -83
  70. package/client/services/router/index.tsx +185 -220
  71. package/client/services/router/request/api.ts +97 -119
  72. package/client/services/router/request/history.ts +2 -2
  73. package/client/services/router/request/index.ts +13 -12
  74. package/client/services/router/request/multipart.ts +72 -62
  75. package/client/services/router/response/index.tsx +68 -61
  76. package/client/services/router/response/page.ts +28 -32
  77. package/client/utils/dom.ts +17 -33
  78. package/common/app/index.ts +3 -3
  79. package/common/data/chaines/index.ts +22 -23
  80. package/common/data/dates.ts +35 -70
  81. package/common/data/markdown.ts +42 -39
  82. package/common/dev/serverHotReload.ts +26 -0
  83. package/common/errors/index.tsx +110 -142
  84. package/common/router/contracts.ts +29 -0
  85. package/common/router/index.ts +89 -108
  86. package/common/router/layouts.ts +34 -47
  87. package/common/router/pageSetup.ts +50 -0
  88. package/common/router/register.ts +53 -24
  89. package/common/router/request/api.ts +30 -36
  90. package/common/router/request/index.ts +2 -8
  91. package/common/router/response/index.ts +8 -15
  92. package/common/router/response/page.ts +70 -58
  93. package/common/utils.ts +1 -1
  94. package/eslint.js +62 -0
  95. package/package.json +12 -45
  96. package/prettier.config.cjs +9 -0
  97. package/scripts/cleanup-generated-controllers.ts +62 -0
  98. package/scripts/fix-reference-app-typing.ts +490 -0
  99. package/scripts/refactor-client-app-imports.ts +244 -0
  100. package/scripts/refactor-client-pages.ts +587 -0
  101. package/scripts/refactor-server-controllers.ts +470 -0
  102. package/scripts/refactor-server-runtime-aliases.ts +360 -0
  103. package/scripts/restore-client-app-import-files.ts +41 -0
  104. package/scripts/restore-files-from-git-head.ts +20 -0
  105. package/scripts/update-codex-agents.ts +35 -0
  106. package/server/app/commands.ts +35 -64
  107. package/server/app/container/config.ts +39 -69
  108. package/server/app/container/console/index.ts +202 -248
  109. package/server/app/container/index.ts +33 -71
  110. package/server/app/controller/index.ts +61 -0
  111. package/server/app/index.ts +39 -105
  112. package/server/app/service/container.ts +41 -42
  113. package/server/app/service/index.ts +120 -147
  114. package/server/context.ts +1 -1
  115. package/server/index.ts +25 -1
  116. package/server/services/auth/index.ts +75 -115
  117. package/server/services/auth/router/index.ts +31 -32
  118. package/server/services/auth/router/request.ts +14 -16
  119. package/server/services/cron/CronTask.ts +13 -26
  120. package/server/services/cron/index.ts +14 -36
  121. package/server/services/disks/driver.ts +40 -58
  122. package/server/services/disks/drivers/local/index.ts +79 -90
  123. package/server/services/disks/drivers/s3/index.ts +116 -163
  124. package/server/services/disks/index.ts +23 -38
  125. package/server/services/email/index.ts +45 -104
  126. package/server/services/email/utils.ts +14 -27
  127. package/server/services/fetch/index.ts +53 -85
  128. package/server/services/prisma/Facet.ts +39 -91
  129. package/server/services/prisma/index.ts +74 -110
  130. package/server/services/router/generatedRuntime.ts +29 -0
  131. package/server/services/router/http/index.ts +77 -72
  132. package/server/services/router/http/multipart.ts +19 -42
  133. package/server/services/router/index.ts +378 -365
  134. package/server/services/router/request/api.ts +26 -25
  135. package/server/services/router/request/index.ts +44 -51
  136. package/server/services/router/request/service.ts +7 -11
  137. package/server/services/router/request/validation/zod.ts +111 -148
  138. package/server/services/router/response/index.ts +110 -125
  139. package/server/services/router/response/mask/Filter.ts +31 -72
  140. package/server/services/router/response/mask/index.ts +8 -15
  141. package/server/services/router/response/mask/selecteurs.ts +11 -25
  142. package/server/services/router/response/page/clientManifest.ts +25 -0
  143. package/server/services/router/response/page/document.tsx +199 -127
  144. package/server/services/router/response/page/index.tsx +89 -94
  145. package/server/services/router/service.ts +13 -15
  146. package/server/services/schema/index.ts +17 -26
  147. package/server/services/schema/request.ts +19 -33
  148. package/server/services/schema/router/index.ts +8 -11
  149. package/server/services/security/encrypt/aes/index.ts +15 -35
  150. package/server/utils/slug.ts +29 -32
  151. package/skills/clean-project-code/SKILL.md +63 -0
  152. package/skills/clean-project-code/agents/openai.yaml +4 -0
  153. package/tsconfig.common.json +4 -3
  154. package/tsconfig.json +4 -1
  155. package/types/aliases.d.ts +17 -21
  156. package/types/controller-input.test.ts +48 -0
  157. package/types/express-extra.d.ts +6 -0
  158. package/types/global/constants.d.ts +1 -0
  159. package/types/global/express-extra.d.ts +6 -0
  160. package/types/global/modules.d.ts +13 -16
  161. package/types/global/utils.d.ts +17 -49
  162. package/types/global/vendors.d.ts +62 -0
  163. package/types/icons.d.ts +65 -1
  164. package/types/uuid.d.ts +3 -0
  165. package/types/vendors.d.ts +62 -0
  166. package/cli/compiler/common/babel/index.ts +0 -173
  167. package/cli/compiler/common/babel/plugins/index.ts +0 -0
  168. package/cli/compiler/common/babel/plugins/services.ts +0 -586
  169. package/cli/compiler/common/babel/routes/imports.ts +0 -127
  170. package/cli/compiler/common/babel/routes/routes.ts +0 -1170
  171. package/client/services/captcha/index.ts +0 -67
  172. package/client/services/socket/index.ts +0 -147
  173. package/common/data/rte/nodes.ts +0 -83
  174. package/common/data/stats.ts +0 -90
  175. package/common/utils/rte.ts +0 -183
  176. package/server/services/auth/old.ts +0 -277
  177. package/server/services/cache/commands.ts +0 -41
  178. package/server/services/cache/index.ts +0 -297
  179. package/server/services/cache/service.json +0 -6
  180. package/server/services/socket/index.ts +0 -162
  181. package/server/services/socket/scope.ts +0 -226
  182. package/server/services/socket/service.json +0 -6
  183. package/server/services_old/SocketClient.ts +0 -92
  184. package/server/services_old/Token.old.ts +0 -97
@@ -4,12 +4,10 @@
4
4
 
5
5
  // Npm
6
6
  import path from 'path';
7
- import webpack from 'webpack';
8
7
  import fs from 'fs-extra';
9
8
  import serialize from 'serialize-javascript';
10
-
11
- import SpeedMeasurePlugin from "speed-measure-webpack-v5-plugin";
12
- const smp = new SpeedMeasurePlugin({ disable: true });
9
+ import { rspack, type Compiler as RspackCompiler } from '@rspack/core';
10
+ import ts from 'typescript';
13
11
 
14
12
  // Core
15
13
  import app from '../app';
@@ -17,69 +15,67 @@ import cli from '..';
17
15
  import createServerConfig from './server';
18
16
  import createClientConfig from './client';
19
17
  import { TCompileMode, TCompileOutputTarget } from './common';
20
-
21
- type TCompilerCallback = (compiler: webpack.Compiler) => void
18
+ import {
19
+ indexControllers,
20
+ generateControllerClientTree,
21
+ printControllerTree,
22
+ type TControllerServiceRoot,
23
+ } from './common/controllers';
24
+ import { writeClientManifest } from './common/clientManifest';
25
+ import { getGeneratedRouteModuleFilepath, writeGeneratedRouteModule } from './common/generatedRouteModules';
26
+ import writeIfChanged from './writeIfChanged';
27
+
28
+ type TCompilerCallback = (compiler: RspackCompiler) => void;
22
29
 
23
30
  type TServiceMetas = {
24
- id: string,
25
- name: string,
26
- parent: string,
27
- dependences: string,
28
- importationPath: string,
29
- priority: number
30
- }
31
+ id: string;
32
+ name: string;
33
+ parent: string;
34
+ dependences: string;
35
+ importationPath: string;
36
+ priority: number;
37
+ };
31
38
 
32
39
  type TRegisteredService = {
33
- id?: string,
34
- name: string,
35
- className: string,
36
- instanciation: (parentRef?: string) => string,
37
- priority: number,
38
- }
40
+ id?: string;
41
+ name: string;
42
+ className: string;
43
+ instanciation: (parentRef?: string, appRef?: string) => string;
44
+ priority: number;
45
+ };
46
+
47
+ type TClientRouteLoader = { filepath: string; chunkId: string; preload: boolean };
48
+
49
+ type TRecentCompilationResult = { succeeded: boolean; hash?: string; modifiedFiles?: string[] };
50
+
51
+ const normalizePath = (value: string) => value.replace(/\\/g, '/');
39
52
 
40
53
  /*----------------------------------
41
54
  - FONCTION
42
55
  ----------------------------------*/
43
56
  export default class Compiler {
44
-
45
- public compiling: { [compiler: string]: Promise<void> } = {};
57
+ public compiling: { [compiler: string]: Promise<void> } = {};
58
+ private recentCompilationResults: { [compiler: string]: TRecentCompilationResult } = {};
59
+ private recentModifiedFiles: { [compiler: string]: string[] } = {};
60
+ private refreshingGeneratedArtifacts?: Promise<void>;
46
61
 
47
62
  public constructor(
48
63
  private mode: TCompileMode,
49
- private callbacks: {
50
- before?: TCompilerCallback,
51
- after?: TCompilerCallback,
52
- } = {},
64
+ private callbacks: { before?: TCompilerCallback; after?: TCompilerCallback } = {},
53
65
  private debug: boolean = false,
54
- private outputTarget: TCompileOutputTarget = mode === 'dev' ? 'dev' : 'bin'
55
- ) {
56
-
57
- }
66
+ private outputTarget: TCompileOutputTarget = mode === 'dev' ? 'dev' : 'bin',
67
+ ) {}
58
68
 
59
69
  public cleanup() {
60
70
  const outputPath = app.outputPath(this.outputTarget);
61
71
  const generatedPublicEntries = new Set(['app']);
72
+ const outputPublicPath = path.join(outputPath, 'public');
73
+ const preserveDevOutput = this.mode === 'dev' && this.outputTarget === 'dev';
62
74
 
63
- fs.emptyDirSync( outputPath );
64
- fs.ensureDirSync( path.join(outputPath, 'public') )
65
- const publicFiles = fs.readdirSync(app.paths.public);
66
- for (const publicFile of publicFiles) {
67
- if (generatedPublicEntries.has(publicFile))
68
- continue;
75
+ if (!preserveDevOutput) fs.emptyDirSync(outputPath);
69
76
 
70
- // Dev: faster to use symlink
71
- if (this.mode === 'dev')
72
- fs.symlinkSync(
73
- path.join(app.paths.public, publicFile),
74
- path.join(outputPath, 'public', publicFile)
75
- );
76
- // Prod: Symlink not always supported by CI / Containers solutions
77
- else
78
- fs.copySync(
79
- path.join(app.paths.public, publicFile),
80
- path.join(outputPath, 'public', publicFile)
81
- );
82
- }
77
+ fs.ensureDirSync(outputPublicPath);
78
+ this.syncPublicEntries(outputPublicPath, generatedPublicEntries, this.mode === 'dev');
83
79
  }
84
80
  /* FIX issue with npm link
85
81
  When we install a module with npm link, this module's deps are not installed in the parent project scope
@@ -89,7 +85,7 @@ export default class Compiler {
89
85
  */
90
86
  public fixNpmLinkIssues() {
91
87
  const corePath = path.join(app.paths.root, '/node_modules/proteum');
92
- if (!fs.lstatSync( corePath ).isSymbolicLink())
88
+ if (!fs.lstatSync(corePath).isSymbolicLink())
93
89
  return console.info("Not fixing npm issue because proteum wasn't installed with npm link.");
94
90
 
95
91
  this.debug && console.info(`Fix NPM link issues ...`);
@@ -102,191 +98,718 @@ export default class Compiler {
102
98
  // Modules are installed locally and not glbally as with with the 5htp package from NPM.
103
99
  // So we need to symbilnk the http-core node_modules in one of the parents of server.js.
104
100
  // It avoids errors like: "Error: Cannot find module 'intl'"
105
- fs.symlinkSync( coreModules, path.join(outputPath, 'node_modules') );
101
+ this.ensureSymlinkSync(coreModules, path.join(outputPath, 'node_modules'));
106
102
 
107
- // Same problem: when 5htp-core is installed via npm link,
103
+ // Same problem: when 5htp-core is installed via npm link,
108
104
  // Typescript doesn't detect React and shows mission JSX errors
109
105
  const preactCoreModule = path.join(coreModules, 'preact');
110
106
  const preactAppModule = path.join(appModules, 'preact');
111
107
  const reactAppModule = path.join(appModules, 'react');
112
108
 
113
- if (!fs.existsSync( preactAppModule ))
114
- fs.symlinkSync( preactCoreModule, preactAppModule );
115
- if (!fs.existsSync( reactAppModule ))
116
- fs.symlinkSync( path.join(preactCoreModule, 'compat'), reactAppModule );
109
+ if (!fs.existsSync(preactAppModule)) fs.symlinkSync(preactCoreModule, preactAppModule);
110
+ if (!fs.existsSync(reactAppModule)) fs.symlinkSync(path.join(preactCoreModule, 'compat'), reactAppModule);
111
+ }
112
+
113
+ private syncPublicEntries(outputPublicPath: string, generatedPublicEntries: Set<string>, useSymlinks: boolean) {
114
+ const publicFiles = new Set(
115
+ fs.readdirSync(app.paths.public).filter((publicFile) => !generatedPublicEntries.has(publicFile)),
116
+ );
117
+
118
+ for (const existingPublicFile of fs.readdirSync(outputPublicPath)) {
119
+ if (generatedPublicEntries.has(existingPublicFile) || publicFiles.has(existingPublicFile)) continue;
120
+
121
+ fs.removeSync(path.join(outputPublicPath, existingPublicFile));
122
+ }
123
+
124
+ for (const publicFile of publicFiles) {
125
+ const sourcePath = path.join(app.paths.public, publicFile);
126
+ const outputFilePath = path.join(outputPublicPath, publicFile);
127
+
128
+ if (useSymlinks) {
129
+ this.ensureSymlinkSync(sourcePath, outputFilePath);
130
+ continue;
131
+ }
132
+
133
+ if (fs.existsSync(outputFilePath)) fs.removeSync(outputFilePath);
134
+
135
+ fs.copySync(sourcePath, outputFilePath);
136
+ }
117
137
  }
118
138
 
119
- private findServices( dir: string ) {
139
+ private ensureSymlinkSync(targetPath: string, linkPath: string) {
140
+ fs.ensureDirSync(path.dirname(linkPath));
141
+
142
+ try {
143
+ const linkStats = fs.lstatSync(linkPath);
144
+
145
+ if (linkStats.isSymbolicLink()) {
146
+ const currentTarget = path.resolve(path.dirname(linkPath), fs.readlinkSync(linkPath));
147
+ if (currentTarget === path.resolve(targetPath)) return;
148
+ }
149
+
150
+ fs.removeSync(linkPath);
151
+ } catch (error) {
152
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
153
+ }
154
+
155
+ fs.symlinkSync(targetPath, linkPath);
156
+ }
120
157
 
121
- const blacklist = ['node_modules', 'proteum']
158
+ private findServices(dir: string) {
159
+ const blacklist = ['node_modules', 'proteum'];
122
160
  const files: string[] = [];
123
161
  const dirents = fs.readdirSync(dir, { withFileTypes: true });
124
162
 
125
163
  for (let dirent of dirents) {
126
-
127
164
  let fileName = dirent.name;
128
165
  let filePath = path.resolve(dir, fileName);
129
166
 
130
- if (blacklist.includes( fileName ))
131
- continue;
167
+ if (blacklist.includes(fileName)) continue;
132
168
 
133
169
  // Define is we should recursively find service in the current item
134
170
  let iterate: boolean = false;
135
171
  if (dirent.isSymbolicLink()) {
136
-
137
- const realPath = path.resolve( dir, fs.readlinkSync(filePath) );
138
- const destinationInfos = fs.lstatSync( realPath );
139
- if (destinationInfos.isDirectory())
140
- iterate = true;
141
-
142
- } else if (dirent.isDirectory())
143
- iterate = true;
172
+ const realPath = path.resolve(dir, fs.readlinkSync(filePath));
173
+ const destinationInfos = fs.lstatSync(realPath);
174
+ if (destinationInfos.isDirectory()) iterate = true;
175
+ } else if (dirent.isDirectory()) iterate = true;
144
176
 
145
177
  // Update the list of found services
146
178
  if (iterate) {
147
- files.push( ...this.findServices(filePath) );
179
+ files.push(...this.findServices(filePath));
148
180
  } else if (dirent.name === 'service.json') {
149
- files.push( path.dirname(filePath) );
181
+ files.push(path.dirname(filePath));
150
182
  }
151
183
  }
152
184
  return files;
153
185
  }
154
186
 
155
- private indexServices() {
156
-
187
+ private findClientRouteFiles(dir: string): string[] {
188
+ return this.findRegisteredRouteFiles(dir, { excludeLayoutDirectories: true });
189
+ }
190
+
191
+ private findServerRouteFiles(dir: string): string[] {
192
+ return this.findRegisteredRouteFiles(dir);
193
+ }
194
+
195
+ private findRegisteredRouteFiles(dir: string, options: { excludeLayoutDirectories?: boolean } = {}): string[] {
196
+ if (!fs.existsSync(dir)) return [];
197
+
198
+ const files: string[] = [];
199
+
200
+ for (const dirent of fs.readdirSync(dir, { withFileTypes: true })) {
201
+ const filePath = path.join(dir, dirent.name);
202
+
203
+ if (dirent.isDirectory()) {
204
+ if (options.excludeLayoutDirectories && dirent.name === '_layout') continue;
205
+
206
+ files.push(...this.findRegisteredRouteFiles(filePath, options));
207
+ continue;
208
+ }
209
+
210
+ if (!dirent.isFile()) continue;
211
+
212
+ if (!/\.(ts|tsx)$/.test(dirent.name)) continue;
213
+
214
+ const content = fs.readFileSync(filePath, 'utf8');
215
+ if (!this.hasRegisteredRouteDefinitions(filePath, content)) continue;
216
+
217
+ files.push(filePath);
218
+ }
219
+
220
+ return files;
221
+ }
222
+
223
+ private hasRegisteredRouteDefinitions(filepath: string, content: string) {
224
+ const sourceFile = ts.createSourceFile(
225
+ filepath,
226
+ content,
227
+ ts.ScriptTarget.Latest,
228
+ true,
229
+ filepath.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
230
+ );
231
+
232
+ return sourceFile.statements.some((statement) => {
233
+ if (!ts.isExpressionStatement(statement)) return false;
234
+ if (!ts.isCallExpression(statement.expression)) return false;
235
+ if (!ts.isPropertyAccessExpression(statement.expression.expression)) return false;
236
+
237
+ const callee = statement.expression.expression;
238
+
239
+ return (
240
+ ts.isIdentifier(callee.expression) &&
241
+ callee.expression.text === 'Router' &&
242
+ ['page', 'error', 'get', 'post', 'put', 'delete', 'patch'].includes(callee.name.text)
243
+ );
244
+ });
245
+ }
246
+
247
+ private findLayoutFiles(dir: string): string[] {
248
+ if (!fs.existsSync(dir)) return [];
249
+
250
+ const files: string[] = [];
251
+
252
+ for (const dirent of fs.readdirSync(dir, { withFileTypes: true })) {
253
+ const filePath = path.join(dir, dirent.name);
254
+
255
+ if (dirent.isDirectory()) {
256
+ files.push(...this.findLayoutFiles(filePath));
257
+ continue;
258
+ }
259
+
260
+ if (!dirent.isFile()) continue;
261
+
262
+ if (dirent.name !== 'index.tsx') continue;
263
+
264
+ if (!normalizePath(filePath).includes('/_layout/')) continue;
265
+
266
+ files.push(filePath);
267
+ }
268
+
269
+ return files;
270
+ }
271
+
272
+ private getGeneratedImportPath(fromDir: string, targetFile: string) {
273
+ const relativeImportPath = path.relative(fromDir, targetFile).replace(/\\/g, '/');
274
+ const normalizedImportPath = relativeImportPath.startsWith('.') ? relativeImportPath : './' + relativeImportPath;
275
+
276
+ return normalizedImportPath.replace(/\.(ts|tsx|js|jsx)$/, '');
277
+ }
278
+
279
+ private cleanupObsoleteGeneratedArtifacts() {
280
+ fs.removeSync(path.join(app.paths.client.generated, 'index.ts'));
281
+ }
282
+
283
+ private readPreloadedRouteChunks() {
284
+ const preloadPath = path.join(app.paths.pages, 'preload.json');
285
+
286
+ if (!fs.existsSync(preloadPath)) return new Set<string>();
287
+
288
+ const content = fs.readJsonSync(preloadPath);
289
+
290
+ if (!Array.isArray(content))
291
+ throw new Error(`Invalid client/pages/preload.json format: expected an array of chunk ids.`);
292
+
293
+ return new Set<string>(content.filter((value): value is string => typeof value === 'string'));
294
+ }
295
+
296
+ private getGeneratedClientRouteModuleFilepath(filepath: string) {
297
+ return getGeneratedRouteModuleFilepath(app.paths.client.generated, app.paths.pages, filepath);
298
+ }
299
+
300
+ private getGeneratedServerRouteModuleFilepath(filepath: string) {
301
+ return getGeneratedRouteModuleFilepath(app.paths.server.generated, app.paths.root, filepath);
302
+ }
303
+
304
+ private generateClientRouteWrapperModules() {
305
+ const clientRouteFiles = this.findClientRouteFiles(app.paths.pages).sort((a, b) => a.localeCompare(b));
306
+ const routeSourceFilepaths = new Set(clientRouteFiles.map((filepath) => normalizePath(path.resolve(filepath))));
307
+
308
+ for (const filepath of clientRouteFiles) {
309
+ const pageChunk = cli.paths.getPageChunk(app, filepath);
310
+
311
+ writeGeneratedRouteModule({
312
+ outputFilepath: this.getGeneratedClientRouteModuleFilepath(filepath),
313
+ runtime: 'client',
314
+ side: 'client',
315
+ sourceFilepath: filepath,
316
+ clientRoute: { chunkId: pageChunk.chunkId, filepath: pageChunk.filepath },
317
+ routeSourceFilepaths,
318
+ });
319
+
320
+ writeGeneratedRouteModule({
321
+ outputFilepath: this.getGeneratedServerRouteModuleFilepath(filepath),
322
+ runtime: 'server',
323
+ side: 'client',
324
+ sourceFilepath: filepath,
325
+ clientRoute: { chunkId: pageChunk.chunkId, filepath: pageChunk.filepath },
326
+ routeSourceFilepaths,
327
+ });
328
+ }
329
+ }
330
+
331
+ private generateServerRouteWrapperModules() {
332
+ const serverRouteFiles = this.findServerRouteFiles(path.join(app.paths.root, 'server', 'routes')).sort((a, b) =>
333
+ a.localeCompare(b),
334
+ );
335
+ const routeSourceFilepaths = new Set(serverRouteFiles.map((filepath) => normalizePath(path.resolve(filepath))));
336
+
337
+ for (const filepath of serverRouteFiles) {
338
+ writeGeneratedRouteModule({
339
+ outputFilepath: this.getGeneratedServerRouteModuleFilepath(filepath),
340
+ runtime: 'server',
341
+ side: 'server',
342
+ sourceFilepath: filepath,
343
+ routeSourceFilepaths,
344
+ });
345
+ }
346
+ }
347
+
348
+ private generateClientRoutesModule() {
349
+ const routeLoadersFile = path.join(app.paths.client.generated, 'routes.ts');
350
+ const preloadedChunks = this.readPreloadedRouteChunks();
351
+
352
+ const routes = this.findClientRouteFiles(app.paths.pages)
353
+ .sort((a, b) => a.localeCompare(b))
354
+ .map<TClientRouteLoader>((filepath) => {
355
+ const { chunkId } = cli.paths.getPageChunk(app, filepath);
356
+
357
+ return { filepath, chunkId, preload: preloadedChunks.has(chunkId) };
358
+ });
359
+
360
+ const imports: string[] = [];
361
+ const routeEntries: string[] = [];
362
+
363
+ routes.forEach((route, index) => {
364
+ const normalizedImportPath = this.getGeneratedImportPath(
365
+ app.paths.client.generated,
366
+ this.getGeneratedClientRouteModuleFilepath(route.filepath),
367
+ );
368
+
369
+ if (route.preload) {
370
+ const localIdentifier = `preloadedRoute${index}`;
371
+ imports.push(
372
+ `import { __register as ${localIdentifier} } from ${JSON.stringify(normalizedImportPath)};`,
373
+ );
374
+ routeEntries.push(
375
+ ` ${JSON.stringify(route.chunkId)}: () => Promise.resolve({ __register: ${localIdentifier} }),`,
376
+ );
377
+ return;
378
+ }
379
+
380
+ routeEntries.push(
381
+ ` ${JSON.stringify(route.chunkId)}: () => import(/* webpackChunkName: ${JSON.stringify(route.chunkId)} */ ${JSON.stringify(normalizedImportPath)}),`,
382
+ );
383
+ });
384
+
385
+ const content = `/*----------------------------------
386
+ - GENERATED FILE
387
+ ----------------------------------*/
388
+
389
+ // This file is generated by Proteum to avoid rebuilding the page loader map in Babel.
390
+ // Do not edit it manually.
391
+
392
+ ${imports.join('\n')}
393
+ ${imports.length ? '\n' : ''}const routes = {
394
+ ${routeEntries.join('\n')}
395
+ };
396
+
397
+ export default routes;
398
+ `;
399
+
400
+ writeIfChanged(routeLoadersFile, content);
401
+ }
402
+
403
+ private generateClientLayoutsModule() {
404
+ const layoutsFile = path.join(app.paths.client.generated, 'layouts.ts');
405
+
406
+ const layouts = this.findLayoutFiles(app.paths.pages)
407
+ .map((filepath) => {
408
+ const { chunkId } = cli.paths.getLayoutChunk(app, filepath);
409
+ const importPath = this.getGeneratedImportPath(app.paths.client.generated, filepath);
410
+ const relativePath = normalizePath(path.relative(app.paths.root, filepath));
411
+ const depth = relativePath.split('/').filter(Boolean).length;
412
+
413
+ return { filepath: relativePath, chunkId, depth, importPath };
414
+ })
415
+ .sort((a, b) => {
416
+ if (b.depth !== a.depth) return b.depth - a.depth;
417
+ return a.filepath.localeCompare(b.filepath);
418
+ });
419
+
420
+ const imports = layouts
421
+ .map((layout, index) => `import * as layoutModule${index} from ${JSON.stringify(layout.importPath)};`)
422
+ .join('\n');
423
+
424
+ const layoutEntries = layouts
425
+ .map((layout, index) => ` ${JSON.stringify(layout.chunkId)}: layoutModule${index},`)
426
+ .join('\n');
427
+
428
+ const orderedLayoutIds = layouts.map((layout) => ` ${JSON.stringify(layout.chunkId)},`).join('\n');
429
+
430
+ const content = `/*----------------------------------
431
+ - GENERATED FILE
432
+ ----------------------------------*/
157
433
 
434
+ // This file is generated by Proteum from app layout files.
435
+ // Do not edit it manually.
436
+
437
+ ${imports}
438
+ ${imports ? '\n' : ''}const layouts = {
439
+ ${layoutEntries}
440
+ };
441
+
442
+ export const layoutOrder = [
443
+ ${orderedLayoutIds}
444
+ ];
445
+
446
+ export default layouts;
447
+ `;
448
+
449
+ writeIfChanged(layoutsFile, content);
450
+ }
451
+
452
+ private generateServerRoutesModule() {
453
+ const routeModulesFile = path.join(app.paths.server.generated, 'routes.ts');
454
+ const serverRouteFiles = this.findServerRouteFiles(path.join(app.paths.root, 'server', 'routes'))
455
+ .sort((a, b) => a.localeCompare(b))
456
+ .map((filepath) => ({
457
+ filepath: normalizePath(path.relative(app.paths.root, filepath)),
458
+ importPath: this.getGeneratedImportPath(
459
+ app.paths.server.generated,
460
+ this.getGeneratedServerRouteModuleFilepath(filepath),
461
+ ),
462
+ }));
463
+
464
+ const pageRouteFiles = this.findClientRouteFiles(app.paths.pages)
465
+ .sort((a, b) => a.localeCompare(b))
466
+ .map((filepath) => ({
467
+ filepath: normalizePath(path.relative(app.paths.root, filepath)),
468
+ importPath: this.getGeneratedImportPath(
469
+ app.paths.server.generated,
470
+ this.getGeneratedServerRouteModuleFilepath(filepath),
471
+ ),
472
+ }));
473
+
474
+ const routeModules = [...serverRouteFiles, ...pageRouteFiles];
475
+
476
+ const imports = routeModules
477
+ .map(
478
+ (routeModule, index) =>
479
+ `const routeModule${index} = require(${JSON.stringify(routeModule.importPath)});`,
480
+ )
481
+ .join('\n');
482
+
483
+ const routeEntries = routeModules
484
+ .map(
485
+ (routeModule, index) => ` {
486
+ filepath: ${JSON.stringify(routeModule.filepath)},
487
+ register: routeModule${index}.__register,
488
+ },`,
489
+ )
490
+ .join('\n');
491
+
492
+ const content = `/*----------------------------------
493
+ - GENERATED FILE
494
+ ----------------------------------*/
495
+
496
+ // This file is generated by Proteum from route registration files.
497
+ // Do not edit it manually.
498
+
499
+ import type { TRouteModule } from "@common/router";
500
+ ${imports ? '\n' + imports : ''}
501
+
502
+ export type TGeneratedRouteModule = {
503
+ filepath: string,
504
+ register?: TRouteModule["__register"],
505
+ }
506
+
507
+ const routeModules: TGeneratedRouteModule[] = [
508
+ ${routeEntries}
509
+ ];
510
+
511
+ export default routeModules;
512
+ `;
513
+
514
+ writeIfChanged(routeModulesFile, content);
515
+ }
516
+
517
+ private generateRoutingModules() {
518
+ this.cleanupObsoleteGeneratedArtifacts();
519
+ this.generateClientRouteWrapperModules();
520
+ this.generateServerRouteWrapperModules();
521
+ this.generateServerRoutesModule();
522
+ this.generateClientRoutesModule();
523
+ this.generateClientLayoutsModule();
524
+ }
525
+
526
+ private indexControllers() {
527
+ const registeredServiceNamesById = new Map<string, string>(
528
+ Object.values(app.registered).flatMap((service: { id?: string; name?: string }) =>
529
+ service.id && service.name ? [[service.id, service.name]] : [],
530
+ ),
531
+ );
532
+
533
+ const appControllerServiceRoots = this.findServices(path.join(app.paths.root, 'server', 'services'))
534
+ .map<TControllerServiceRoot | null>((serviceDir) => {
535
+ const metasFile = path.join(serviceDir, 'service.json');
536
+ const serviceMetas = fs.readJsonSync(metasFile) as { id?: string };
537
+ const alias = serviceMetas.id ? registeredServiceNamesById.get(serviceMetas.id) : undefined;
538
+
539
+ if (!alias) return null;
540
+
541
+ return { alias, dir: serviceDir };
542
+ })
543
+ .filter((serviceRoot): serviceRoot is TControllerServiceRoot => !!serviceRoot)
544
+ .sort((a, b) => b.dir.length - a.dir.length);
545
+
546
+ return indexControllers([
547
+ { importPrefix: '@server/services/', root: path.join(cli.paths.core.root, 'server', 'services') },
548
+ {
549
+ importPrefix: '@/server/services/',
550
+ root: path.join(app.paths.root, 'server', 'services'),
551
+ serviceRoots: appControllerServiceRoots,
552
+ },
553
+ ]);
554
+ }
555
+
556
+ private generateControllerModules() {
557
+ const controllers = this.indexControllers();
558
+ const clientTree = generateControllerClientTree(controllers);
559
+
560
+ const getControllerLeafMeta = (leaf: string) => {
561
+ const meta = JSON.parse(leaf) as {
562
+ routePath: string;
563
+ importPath: string;
564
+ className: string;
565
+ methodName: string;
566
+ hasInput: boolean;
567
+ };
568
+ const controllerIndex = controllers.findIndex((controller) => controller.importPath === meta.importPath);
569
+
570
+ if (controllerIndex === -1) {
571
+ throw new Error(`Unable to find controller import ${meta.importPath} while generating controller types.`);
572
+ }
573
+
574
+ return { ...meta, controllerIndex };
575
+ };
576
+
577
+ const runtimeLeaf = (leaf: string) => {
578
+ const meta = getControllerLeafMeta(leaf);
579
+ const resultType = `TControllerResult<Controller${meta.controllerIndex}, ${JSON.stringify(meta.methodName)}>`;
580
+
581
+ return meta.hasInput
582
+ ? `(data) => api.createFetcher<${resultType}>('POST', ${JSON.stringify(meta.routePath)}, data)`
583
+ : `() => api.createFetcher<${resultType}>('POST', ${JSON.stringify(meta.routePath)})`;
584
+ };
585
+
586
+ const typeImports = controllers
587
+ .map((controller, index) => `import type Controller${index} from ${JSON.stringify(controller.importPath)};`)
588
+ .join('\n');
589
+
590
+ const typeLeaf = (leaf: string) => {
591
+ const meta = getControllerLeafMeta(leaf);
592
+ const fetcherType = `TControllerFetcher<Controller${meta.controllerIndex}, ${JSON.stringify(meta.methodName)}>`;
593
+
594
+ return meta.hasInput ? `(data: any) => ${fetcherType}` : `() => ${fetcherType}`;
595
+ };
596
+
597
+ const createControllersContent = `/*----------------------------------
598
+ - GENERATED FILE
599
+ ----------------------------------*/
600
+
601
+ // This file is generated by Proteum from server controller files.
602
+ // Do not edit it manually.
603
+
604
+ import type ApiClient from '@common/router/request/api';
605
+ import type { TFetcher } from '@common/router/request/api';
606
+ ${typeImports ? '\n' + typeImports : ''}
607
+
608
+ type TControllerResult<TController, TMethod extends keyof TController> =
609
+ TController[TMethod] extends (...args: any[]) => infer TResult ? Awaited<TResult> : never;
610
+
611
+ type TControllerFetcher<TController, TMethod extends keyof TController> = TFetcher<TControllerResult<TController, TMethod>>;
612
+
613
+ export type TControllers = ${printControllerTree(clientTree, typeLeaf)};
614
+
615
+ export const createControllers = (
616
+ api: Pick<ApiClient, 'createFetcher'>
617
+ ): TControllers => (
618
+ ${printControllerTree(clientTree, runtimeLeaf)}
619
+ );
620
+
621
+ export default createControllers;
622
+ `;
623
+
624
+ writeIfChanged(path.join(app.paths.common.generated, 'controllers.ts'), createControllersContent);
625
+
626
+ writeIfChanged(
627
+ path.join(app.paths.client.generated, 'controllers.ts'),
628
+ `export { createControllers, default } from '@/common/.generated/controllers';
629
+ export type { TControllers } from '@/common/.generated/controllers';
630
+ `,
631
+ );
632
+
633
+ const controllerImports = controllers
634
+ .map((controller, index) => `import Controller${index} from ${JSON.stringify(controller.importPath)};`)
635
+ .join('\n');
636
+
637
+ const controllerEntries = controllers.flatMap((controller, controllerIndex) =>
638
+ controller.methods.map(
639
+ (method) => ` {
640
+ path: ${JSON.stringify('/api/' + method.routePath)},
641
+ Controller: Controller${controllerIndex},
642
+ method: ${JSON.stringify(method.name)},
643
+ },`,
644
+ ),
645
+ );
646
+
647
+ writeIfChanged(
648
+ path.join(app.paths.server.generated, 'controllers.ts'),
649
+ `/*----------------------------------
650
+ - GENERATED FILE
651
+ ----------------------------------*/
652
+
653
+ // This file is generated by Proteum from server controller files.
654
+ // Do not edit it manually.
655
+
656
+ import type Controller from '@server/app/controller';
657
+ ${controllerImports ? '\n' + controllerImports : ''}
658
+
659
+ export type TGeneratedControllerDefinition = {
660
+ path: string,
661
+ Controller: new (request: any) => Controller,
662
+ method: string,
663
+ }
664
+
665
+ const controllers: TGeneratedControllerDefinition[] = [
666
+ ${controllerEntries.join('\n')}
667
+ ];
668
+
669
+ export default controllers;
670
+ `,
671
+ );
672
+ }
673
+
674
+ private indexServices() {
158
675
  // Index services
159
676
  const searchDirs = [
160
677
  // The less priority is the first
161
- {
162
- path: '@server/services/',
163
- priority: -1,
164
- root: path.join(cli.paths.core.root, 'server', 'services')
165
- },
166
- {
167
- path: '@/server/services/',
168
- priority: 0,
169
- root: path.join(app.paths.root, 'server', 'services')
170
- },
678
+ { path: '@server/services/', priority: -1, root: path.join(cli.paths.core.root, 'server', 'services') },
679
+ { path: '@/server/services/', priority: 0, root: path.join(app.paths.root, 'server', 'services') },
171
680
  // Temp disabled because compile issue on vercel
172
681
  //'': path.join(app.paths.root, 'node_modules'),
173
- ]
682
+ ];
174
683
 
175
684
  // Generate app class file
176
- const servicesAvailable: {[id: string]: TServiceMetas} = {};
685
+ const servicesAvailable: { [id: string]: TServiceMetas } = {};
177
686
  for (const searchDir of searchDirs) {
178
-
179
687
  const services = this.findServices(searchDir.root);
180
688
 
181
689
  for (const serviceDir of services) {
182
- const metasFile = path.join( serviceDir, 'service.json');
690
+ const metasFile = path.join(serviceDir, 'service.json');
183
691
 
184
692
  // The +1 is to remove the slash
185
- const importationPath = searchDir.path + serviceDir.substring( searchDir.root.length + 1 );
693
+ const importationPath = searchDir.path + serviceDir.substring(searchDir.root.length + 1);
186
694
 
187
695
  const serviceMetas = require(metasFile);
188
696
 
189
- servicesAvailable[ serviceMetas.id ] = {
190
- importationPath,
191
- priority: searchDir.priority,
192
- ...serviceMetas,
193
- };
697
+ servicesAvailable[serviceMetas.id] = { importationPath, priority: searchDir.priority, ...serviceMetas };
194
698
  }
195
699
  }
196
700
 
197
701
  // Read app services
198
- const imported: string[] = []
199
- const referencedNames: {[serviceId: string]: string} = {} // ID to Name
702
+ const imported: string[] = [];
703
+ const referencedNames: { [serviceId: string]: string } = {}; // ID to Name
704
+ let serviceImportIndex = 0;
200
705
 
201
706
  const refService = (serviceName: string, serviceConfig: any, level: number = 0): TRegisteredService => {
202
-
203
707
  if (serviceConfig.refTo !== undefined) {
204
708
  const refTo = serviceConfig.refTo;
205
709
  return {
206
710
  name: serviceName,
207
- instanciation: () => `this.${refTo}`,
208
- priority: 0
209
- }
711
+ className: serviceName,
712
+ instanciation: (_parentRef, appRef = 'this') => `${appRef}.${refTo}`,
713
+ priority: 0,
714
+ };
210
715
  }
211
716
 
212
- const serviceMetas = servicesAvailable[ serviceConfig.id ];
717
+ const serviceMetas = servicesAvailable[serviceConfig.id];
213
718
  if (serviceMetas === undefined)
214
- throw new Error(`Service ${serviceConfig.id} not found. Referenced services: ${Object.keys(servicesAvailable).join('\n')}`);
719
+ throw new Error(
720
+ `Service ${serviceConfig.id} not found. Referenced services: ${Object.keys(servicesAvailable).join('\n')}`,
721
+ );
215
722
 
216
723
  const referencedName = referencedNames[serviceConfig.id];
217
724
  if (referencedName !== undefined)
218
725
  throw new Error(`Service ${serviceConfig.id} is already setup as ${referencedName}`);
219
-
220
- // Generate index & typings
221
- imported.push(`import ${serviceMetas.name} from "${serviceMetas.importationPath}";`);
222
726
 
223
- if (serviceConfig.name !== undefined)
224
- referencedNames[serviceConfig.id] = serviceConfig.name;
727
+ // Generate index & typings
728
+ const importIdentifier = `${serviceMetas.name}Class${serviceImportIndex++}`;
729
+ imported.push(`import ${importIdentifier} from "${serviceMetas.importationPath}";`);
225
730
 
226
- const processConfig = (config: any, level: number = 0) => {
731
+ if (serviceConfig.name !== undefined) referencedNames[serviceConfig.id] = serviceConfig.name;
227
732
 
733
+ const processConfig = (config: any, level: number = 0, appRef: string = 'this') => {
228
734
  let propsStr = '';
229
735
  for (const key in config) {
230
736
  const value = config[key];
231
737
 
232
738
  if (!value || typeof value !== 'object')
233
739
  propsStr += `"${key}":${serialize(value, { space: 4 })},\n`;
234
-
235
740
  // Reference to a service
236
- else if (value.type === 'service.setup' || value.type === 'service.ref') // TODO: more reliable way to detect a service reference
237
- propsStr += `${key}:`+ refService(key, value, level + 1).instanciation() + ',\n'
238
-
741
+ else if (value.type === 'service.setup' || value.type === 'service.ref')
742
+ // TODO: more reliable way to detect a service reference
743
+ propsStr += `${key}:` + refService(key, value, level + 1).instanciation(undefined, appRef) + ',\n';
239
744
  // Recursion
240
745
  else if (level <= 4 && !Array.isArray(value))
241
- propsStr += `"${key}":` + processConfig(value, level + 1) + ',\n';
242
-
243
- else
244
- propsStr += `"${key}":${serialize(value, { space: 4 })},\n`;
245
-
746
+ propsStr += `"${key}":` + processConfig(value, level + 1, appRef) + ',\n';
747
+ else propsStr += `"${key}":${serialize(value, { space: 4 })},\n`;
246
748
  }
247
749
 
248
750
  return `{ ${propsStr} }`;
249
- }
250
- const config = processConfig(serviceConfig.config || {});
751
+ };
251
752
 
252
753
  // Generate the service instance
253
- const instanciation = (parentRef?: string) =>
254
- `new ${serviceMetas.name}(
754
+ const instanciation = (parentRef?: string, appRef: string = 'this') => {
755
+ const config = processConfig(serviceConfig.config || {}, 0, appRef);
756
+ const typedRouterConfig =
757
+ serviceMetas.id === 'Core/Router' && parentRef
758
+ ? `defineServiceConfig(${config} satisfies ConstructorParameters<typeof ${importIdentifier}>[1])`
759
+ : `defineServiceConfig(${config})`;
760
+
761
+ return `new ${importIdentifier}(
255
762
  ${parentRef ? `${parentRef},` : ''}
256
- ${config},
257
- this
258
- )`
763
+ ${typedRouterConfig},
764
+ ${appRef}
765
+ )`;
766
+ };
259
767
 
260
768
  return {
261
769
  id: serviceConfig.id,
262
770
  name: serviceName,
263
771
  instanciation,
264
- className: serviceMetas.name,
772
+ className: importIdentifier,
265
773
  priority: serviceConfig.config?.priority || serviceMetas.priority || 0,
266
774
  };
267
- }
775
+ };
268
776
 
269
- const servicesCode = Object.values(app.registered).map( s => refService(s.name, s, 0));
777
+ const servicesCode = Object.values(app.registered).map((s) => refService(s.name, s, 0));
270
778
  const sortedServices = servicesCode.sort((a, b) => a.priority - b.priority);
271
779
 
272
780
  // Define the app class identifier
273
781
  const appClassIdentifier = app.identity.identifier;
274
- const containerServices = app.containerServices.map( s => "'" + s + "'").join('|');
782
+ const containerServices = app.containerServices.map((s) => "'" + s + "'").join('|');
783
+ const generatedFactories = sortedServices
784
+ .map((service) => {
785
+ const factoryIdentifier = `create${service.className}`;
786
+ const instanceIdentifier = `${service.className}Instance`;
787
+
788
+ return `const ${factoryIdentifier} = (app: ${appClassIdentifier}) => ${service.instanciation('app', 'app')};
789
+
790
+ type ${instanceIdentifier} = ReturnType<typeof ${factoryIdentifier}>;`;
791
+ })
792
+ .join('\n\n');
275
793
 
276
794
  // @/client/.generated/services.d.ts
277
- fs.outputFileSync(
278
- path.join( app.paths.client.generated, 'services.d.ts'),
279
- `declare module "@app" {
795
+ writeIfChanged(
796
+ path.join(app.paths.client.generated, 'services.d.ts'),
797
+ `declare type ${appClassIdentifier} = import("@/server/.generated/app").default;
798
+
799
+ declare module "@app" {
280
800
 
281
801
  import { ${appClassIdentifier} as ${appClassIdentifier}Client } from "@/client";
282
802
  import ${appClassIdentifier}Server from "@/server/.generated/app";
283
803
 
284
804
  export const Router: ${appClassIdentifier}Client['Router'];
285
805
 
286
- ${sortedServices.map(service => service.name !== 'Router'
287
- ? `export const ${service.name}: ${appClassIdentifier}Server["${service.name}"];`
288
- : ''
289
- ).join('\n')}
806
+ ${sortedServices
807
+ .map((service) =>
808
+ service.name !== 'Router'
809
+ ? `export const ${service.name}: ${appClassIdentifier}Server["${service.name}"];`
810
+ : '',
811
+ )
812
+ .join('\n')}
290
813
 
291
814
  }
292
815
 
@@ -306,91 +829,96 @@ declare module '@common/errors' {
306
829
  export type UpgradeRequired = import('@common/errors/index').UpgradeRequired<FeatureKeys>;
307
830
  }
308
831
 
309
- declare module '@request' {
310
-
311
- }
312
-
313
832
  declare namespace preact.JSX {
314
833
  interface HTMLAttributes {
315
834
  src?: string;
316
835
  }
317
836
  }
318
- `
837
+ `,
319
838
  );
320
839
 
321
840
  // @/client/.generated/context.ts
322
- fs.outputFileSync(
323
- path.join( app.paths.client.generated, 'context.ts'),
324
- `// TODO: move it into core (but how to make sure usecontext returns ${appClassIdentifier}'s context ?)
841
+ writeIfChanged(
842
+ path.join(app.paths.client.generated, 'context.ts'),
843
+ `// TODO: move it into core (but how to make sure usecontext returns ${appClassIdentifier}'s context ?)
325
844
  import React from 'react';
326
845
 
327
- import type ${appClassIdentifier}Server from '@/server/.generated/app';
328
- import type { TRouterContext as TServerRouterRequestContext } from '@server/services/router/response';
329
- import type { TRouterContext as TClientRouterRequestContext } from '@client/services/router/response';
330
- import type ${appClassIdentifier}Client from '.';
846
+ import type ${appClassIdentifier}Client from '@/client/index';
331
847
 
332
- // TO Fix: TClientRouterRequestContext is unable to get the right type of ${appClassIdentifier}Client["router"]
333
- // (it gets ClientApplication instead of ${appClassIdentifier}Client)
334
- type ClientRequestContext = TClientRouterRequestContext<${appClassIdentifier}Client["Router"], ${appClassIdentifier}Client>;
335
- type ServerRequestContext = TServerRouterRequestContext<${appClassIdentifier}Server["Router"]>
336
- type UniversalServices = ClientRequestContext | ServerRequestContext
337
-
338
- // Non-universla services are flagged as potentially undefined
339
- export type ClientContext = (
340
- UniversalServices
341
- &
342
- Partial<Omit<ClientRequestContext, keyof UniversalServices>>
343
- &
344
- {
345
- Router: ${appClassIdentifier}Client["Router"],
346
- }
347
- )
848
+ export type ClientContext = ${appClassIdentifier}Client["Router"]["context"];
348
849
 
349
850
  export const ReactClientContext = React.createContext<ClientContext>({} as ClientContext);
350
- export default (): ClientContext => React.useContext<ClientContext>(ReactClientContext);`);
851
+ export default (): ClientContext => React.useContext<ClientContext>(ReactClientContext);`,
852
+ );
351
853
 
352
854
  // @/common/.generated/services.d.ts
353
- fs.outputFileSync(
354
- path.join( app.paths.common.generated, 'services.d.ts'),
355
- `declare module '@models/types' {
855
+ writeIfChanged(
856
+ path.join(app.paths.common.generated, 'services.d.ts'),
857
+ `declare type ${appClassIdentifier} = import("@/server/.generated/app").default;
858
+
859
+ declare module '@models/types' {
356
860
  export * from '@/var/prisma/index';
357
- }`
861
+ }`,
862
+ );
863
+
864
+ // @/common/generated.d.ts
865
+ writeIfChanged(
866
+ path.join(app.paths.root, 'common', 'generated.d.ts'),
867
+ `/// <reference path="./.generated/services.d.ts" />
868
+ `,
358
869
  );
359
870
 
360
871
  // @/server/.generated/app.ts
361
- fs.outputFileSync(
362
- path.join( app.paths.server.generated, 'app.ts'),
363
- `
872
+ writeIfChanged(
873
+ path.join(app.paths.server.generated, 'app.ts'),
874
+ `
364
875
  import { Application } from '@server/app/index';
365
876
  import { ServicesContainer } from '@server/app/service/container';
366
877
 
367
878
  ${imported.join('\n')}
368
879
 
880
+ type TLooseServiceConfig<TConfig> =
881
+ TConfig extends (...args: any[]) => any ? TConfig
882
+ : TConfig extends Array<infer TItem> ? Array<TLooseServiceConfig<TItem>>
883
+ : TConfig extends object ? ({ [K in keyof TConfig]?: TLooseServiceConfig<TConfig[K]> } & Record<string, unknown>)
884
+ : TConfig;
885
+
886
+ const defineServiceConfig = <TConfig>(value: TConfig): TConfig => value;
887
+
888
+ ${generatedFactories}
889
+
369
890
  export default class ${appClassIdentifier} extends Application<ServicesContainer, CurrentUser> {
370
891
 
371
892
  // Make sure the services typigs are reflecting the config and referring to the app
372
- ${sortedServices.map(service =>
373
- `public ${service.name}!: ReturnType<${appClassIdentifier}["registered"]["${service.id}"]["start"]>;`
374
- ).join('\n')}
375
-
376
- protected registered = {
377
- ${sortedServices.map(service =>
378
- `"${service.id}": {
893
+ ${sortedServices
894
+ .map(
895
+ (service) =>
896
+ `public ${service.name}!: ${service.className}Instance;`,
897
+ )
898
+ .join('\n')}
899
+
900
+ protected registered: Record<string, { name: string; priority: number; start: () => import('@server/app/service').AnyService }> = {
901
+ ${sortedServices
902
+ .map(
903
+ (service) =>
904
+ `"${service.id}": {
379
905
  name: "${service.name}",
380
906
  priority: ${service.priority},
381
- start: () => ${service.instanciation('this')}
382
- }`
383
- ).join(',\n')}
384
- } as const;
907
+ start: () => create${service.className}(this)
908
+ }`,
909
+ )
910
+ .join(',\n')}
911
+ };
385
912
  }
386
913
 
387
914
 
388
- `);
915
+ `,
916
+ );
389
917
 
390
918
  // @/server/.generated/services.d.ts
391
- fs.outputFileSync(
392
- path.join( app.paths.server.generated, 'services.d.ts'),
393
- `type InstalledServices = import('./services').Services;
919
+ writeIfChanged(
920
+ path.join(app.paths.server.generated, 'services.d.ts'),
921
+ `type InstalledServices = Record<string, import('@server/app/service').AnyService>;
394
922
 
395
923
  declare type ${appClassIdentifier} = import("@/server/.generated/app").default;
396
924
 
@@ -399,9 +927,9 @@ declare module '@cli/app' {
399
927
  type TSetupConfig<TConfig> =
400
928
  TConfig extends (...args: any[]) => any ? TConfig
401
929
  : TConfig extends Array<infer TItem> ? Array<TSetupConfig<TItem>>
402
- : TConfig extends object ? {
403
- [K in keyof TConfig]: TSetupConfig<TConfig[K]> | TServiceSetup | TServiceRef
404
- }
930
+ : TConfig extends object ? ({
931
+ [K in keyof TConfig]?: TSetupConfig<TConfig[K]> | TServiceSetup | TServiceRef
932
+ } & Record<string, unknown>)
405
933
  : TConfig;
406
934
 
407
935
  type App = {
@@ -461,26 +989,6 @@ declare module '@server/app' {
461
989
  export = foo;
462
990
  }
463
991
 
464
- declare module '@request' {
465
- import type { TRouterContext } from '@server/services/router/response';
466
- const routerContext: TRouterContext<${appClassIdentifier}["Router"]>;
467
- export = routerContext;
468
- }
469
-
470
- declare module '@models' {
471
- import { Prisma, PrismaClient } from '@/var/prisma/index';
472
-
473
- type ModelNames = Prisma.ModelName;
474
-
475
- type ModelDelegates = {
476
- [K in ModelNames]: PrismaClient[Uncapitalize<K>];
477
- };
478
-
479
- const models: ModelDelegates;
480
-
481
- export = models;
482
- }
483
-
484
992
  declare module '@common/errors' {
485
993
 
486
994
  export * from '@common/errors/index';
@@ -495,56 +1003,73 @@ declare module '@common/errors' {
495
1003
 
496
1004
  declare module '@models/types' {
497
1005
  export * from '@/var/prisma/index';
498
- }`
1006
+ }`,
499
1007
  );
500
1008
  }
501
1009
 
502
1010
  private async warmupApp() {
503
-
504
1011
  await app.warmup();
1012
+ }
505
1013
 
1014
+ private async refreshGeneratedArtifacts() {
1015
+ if (!this.refreshingGeneratedArtifacts) {
1016
+ this.refreshingGeneratedArtifacts = (async () => {
1017
+ this.indexServices();
1018
+ this.generateControllerModules();
1019
+ this.generateRoutingModules();
1020
+ })().finally(() => {
1021
+ this.refreshingGeneratedArtifacts = undefined;
1022
+ });
1023
+ }
1024
+
1025
+ await this.refreshingGeneratedArtifacts;
506
1026
  }
507
1027
 
508
1028
  public async refreshGeneratedTypings() {
509
-
510
1029
  await this.warmupApp();
1030
+ await this.refreshGeneratedArtifacts();
1031
+ }
511
1032
 
512
- this.indexServices();
513
-
1033
+ public consumeRecentCompilationResults() {
1034
+ const recentCompilationResults = { ...this.recentCompilationResults };
1035
+ this.recentCompilationResults = {};
1036
+ return recentCompilationResults;
514
1037
  }
515
1038
 
516
1039
  public async create() {
517
-
518
1040
  await this.warmupApp();
519
1041
 
520
1042
  this.cleanup();
521
1043
 
522
1044
  this.fixNpmLinkIssues();
523
-
524
- this.indexServices();
1045
+ await this.refreshGeneratedArtifacts();
525
1046
 
526
1047
  // Create compilers
527
- const multiCompiler = webpack([
528
- smp.wrap( createServerConfig(app, this.mode, this.outputTarget) ),
529
- smp.wrap( createClientConfig(app, this.mode, this.outputTarget) )
1048
+ const multiCompiler = rspack([
1049
+ createServerConfig(app, this.mode, this.outputTarget),
1050
+ createClientConfig(app, this.mode, this.outputTarget),
530
1051
  ]);
531
1052
 
532
1053
  for (const compiler of multiCompiler.compilers) {
533
-
534
1054
  const name = compiler.name;
535
- if (name === undefined)
536
- throw new Error(`A name must be specified to each compiler.`);
1055
+ if (name === undefined) throw new Error(`A name must be specified to each compiler.`);
537
1056
 
538
1057
  let timeStart = new Date();
539
1058
 
540
- let finished: (() => void);
541
- this.compiling[name] = new Promise((resolve) => finished = resolve);
1059
+ let finished: () => void;
1060
+ this.compiling[name] = new Promise((resolve) => (finished = resolve));
1061
+
1062
+ compiler.hooks.beforeRun.tapPromise(name, () => this.refreshGeneratedArtifacts());
1063
+ compiler.hooks.watchRun.tapPromise(name, () => this.refreshGeneratedArtifacts());
542
1064
 
543
1065
  compiler.hooks.compile.tap(name, (compilation) => {
544
-
545
- this.callbacks.before && this.callbacks.before( compiler );
1066
+ this.callbacks.before && this.callbacks.before(compiler);
546
1067
 
547
- this.compiling[name] = new Promise((resolve) => finished = resolve);
1068
+ this.recentModifiedFiles[name] = [...(compiler.modifiedFiles ? [...compiler.modifiedFiles] : [])].map(
1069
+ (filepath) => normalizePath(path.resolve(filepath)),
1070
+ );
1071
+
1072
+ this.compiling[name] = new Promise((resolve) => (finished = resolve));
548
1073
 
549
1074
  timeStart = new Date();
550
1075
  console.info(`[${name}] Compiling ...`);
@@ -552,22 +1077,29 @@ declare module '@models/types' {
552
1077
 
553
1078
  /* TODO: Ne pas résoudre la promise tant que la recompilation des données indexées (icones, identité, ...)
554
1079
  n'a pas été achevée */
555
- compiler.hooks.done.tap(name, stats => {
1080
+ compiler.hooks.done.tap(name, (stats) => {
1081
+ const compilationSucceeded = !stats.hasErrors();
1082
+ this.recentCompilationResults[name] = {
1083
+ succeeded: compilationSucceeded,
1084
+ hash: typeof stats.hash === 'string' ? stats.hash : undefined,
1085
+ modifiedFiles: this.recentModifiedFiles[name] || [],
1086
+ };
556
1087
 
557
1088
  // Shiow status
558
1089
  const timeEnd = new Date();
559
1090
  const time = timeEnd.getTime() - timeStart.getTime();
560
- if (stats.hasErrors()) {
561
-
1091
+ if (!compilationSucceeded) {
562
1092
  console.info(stats.toString(compiler.options.stats));
563
1093
  console.error(`[${name}] Failed to compile after ${time} ms`);
564
1094
 
565
1095
  // Exit process with code 0, so the CI container can understand building failed
566
1096
  // Only in prod, because in dev, we want the compiler watcher continue running
567
- if (this.mode === 'prod')
568
- process.exit(0);
569
-
1097
+ if (this.mode === 'prod') process.exit(0);
570
1098
  } else {
1099
+ if (name === 'client') {
1100
+ writeClientManifest(stats, app.outputPath(this.outputTarget));
1101
+ }
1102
+
571
1103
  this.debug && console.info(stats.toString(compiler.options.stats));
572
1104
  console.info(`[${name}] Finished compilation after ${time} ms`);
573
1105
  }
@@ -579,7 +1111,5 @@ declare module '@models/types' {
579
1111
  }
580
1112
 
581
1113
  return multiCompiler;
582
-
583
1114
  }
584
-
585
1115
  }