proteum 1.0.2 → 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 (185) hide show
  1. package/AGENTS.md +101 -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 +13 -11
  11. package/cli/app/index.ts +74 -82
  12. package/cli/bin.js +1 -1
  13. package/cli/commands/build.ts +51 -14
  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 +189 -64
  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 +18 -0
  22. package/cli/commands/typecheck.ts +18 -0
  23. package/cli/compiler/client/identite.ts +80 -53
  24. package/cli/compiler/client/index.ts +139 -213
  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 +11 -22
  31. package/cli/compiler/common/generatedRouteModules.ts +368 -0
  32. package/cli/compiler/common/index.ts +31 -65
  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 +781 -230
  37. package/cli/compiler/server/index.ts +59 -75
  38. package/cli/compiler/writeIfChanged.ts +21 -0
  39. package/cli/index.ts +71 -72
  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/doc/TODO.md +1 -1
  95. package/eslint.js +62 -0
  96. package/package.json +12 -47
  97. package/prettier.config.cjs +9 -0
  98. package/scripts/cleanup-generated-controllers.ts +62 -0
  99. package/scripts/fix-reference-app-typing.ts +490 -0
  100. package/scripts/refactor-client-app-imports.ts +244 -0
  101. package/scripts/refactor-client-pages.ts +587 -0
  102. package/scripts/refactor-server-controllers.ts +470 -0
  103. package/scripts/refactor-server-runtime-aliases.ts +360 -0
  104. package/scripts/restore-client-app-import-files.ts +41 -0
  105. package/scripts/restore-files-from-git-head.ts +20 -0
  106. package/scripts/update-codex-agents.ts +35 -0
  107. package/server/app/commands.ts +35 -64
  108. package/server/app/container/config.ts +48 -59
  109. package/server/app/container/console/index.ts +202 -248
  110. package/server/app/container/index.ts +33 -71
  111. package/server/app/controller/index.ts +61 -0
  112. package/server/app/index.ts +39 -105
  113. package/server/app/service/container.ts +41 -42
  114. package/server/app/service/index.ts +120 -147
  115. package/server/context.ts +1 -1
  116. package/server/index.ts +25 -1
  117. package/server/services/auth/index.ts +75 -115
  118. package/server/services/auth/router/index.ts +31 -32
  119. package/server/services/auth/router/request.ts +14 -16
  120. package/server/services/cron/CronTask.ts +13 -26
  121. package/server/services/cron/index.ts +14 -36
  122. package/server/services/disks/driver.ts +40 -58
  123. package/server/services/disks/drivers/local/index.ts +79 -90
  124. package/server/services/disks/drivers/s3/index.ts +116 -163
  125. package/server/services/disks/index.ts +23 -38
  126. package/server/services/email/index.ts +45 -104
  127. package/server/services/email/utils.ts +14 -27
  128. package/server/services/fetch/index.ts +53 -85
  129. package/server/services/prisma/Facet.ts +39 -91
  130. package/server/services/prisma/index.ts +74 -110
  131. package/server/services/router/generatedRuntime.ts +29 -0
  132. package/server/services/router/http/index.ts +78 -73
  133. package/server/services/router/http/multipart.ts +19 -42
  134. package/server/services/router/index.ts +378 -365
  135. package/server/services/router/request/api.ts +26 -25
  136. package/server/services/router/request/index.ts +44 -51
  137. package/server/services/router/request/service.ts +7 -11
  138. package/server/services/router/request/validation/zod.ts +111 -148
  139. package/server/services/router/response/index.ts +110 -125
  140. package/server/services/router/response/mask/Filter.ts +31 -72
  141. package/server/services/router/response/mask/index.ts +8 -15
  142. package/server/services/router/response/mask/selecteurs.ts +11 -25
  143. package/server/services/router/response/page/clientManifest.ts +25 -0
  144. package/server/services/router/response/page/document.tsx +199 -127
  145. package/server/services/router/response/page/index.tsx +89 -94
  146. package/server/services/router/service.ts +13 -15
  147. package/server/services/schema/index.ts +17 -26
  148. package/server/services/schema/request.ts +19 -33
  149. package/server/services/schema/router/index.ts +8 -11
  150. package/server/services/security/encrypt/aes/index.ts +15 -35
  151. package/server/utils/slug.ts +29 -35
  152. package/skills/clean-project-code/SKILL.md +63 -0
  153. package/skills/clean-project-code/agents/openai.yaml +4 -0
  154. package/tsconfig.common.json +4 -3
  155. package/tsconfig.json +4 -1
  156. package/types/aliases.d.ts +17 -21
  157. package/types/controller-input.test.ts +48 -0
  158. package/types/express-extra.d.ts +6 -0
  159. package/types/global/constants.d.ts +13 -0
  160. package/types/global/express-extra.d.ts +6 -0
  161. package/types/global/modules.d.ts +13 -16
  162. package/types/global/utils.d.ts +17 -49
  163. package/types/global/vendors.d.ts +62 -0
  164. package/types/icons.d.ts +65 -1
  165. package/types/uuid.d.ts +3 -0
  166. package/types/vendors.d.ts +62 -0
  167. package/cli/compiler/common/babel/index.ts +0 -170
  168. package/cli/compiler/common/babel/plugins/index.ts +0 -0
  169. package/cli/compiler/common/babel/plugins/services.ts +0 -586
  170. package/cli/compiler/common/babel/routes/imports.ts +0 -127
  171. package/cli/compiler/common/babel/routes/routes.ts +0 -1130
  172. package/client/services/captcha/index.ts +0 -67
  173. package/client/services/socket/index.ts +0 -147
  174. package/common/data/rte/nodes.ts +0 -83
  175. package/common/data/stats.ts +0 -90
  176. package/common/utils/rte.ts +0 -183
  177. package/server/services/auth/old.ts +0 -277
  178. package/server/services/cache/commands.ts +0 -41
  179. package/server/services/cache/index.ts +0 -297
  180. package/server/services/cache/service.json +0 -6
  181. package/server/services/socket/index.ts +0 -162
  182. package/server/services/socket/scope.ts +0 -226
  183. package/server/services/socket/service.json +0 -6
  184. package/server/services_old/SocketClient.ts +0 -92
  185. package/server/services_old/Token.old.ts +0 -97
@@ -4,76 +4,78 @@
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';
16
14
  import cli from '..';
17
15
  import createServerConfig from './server';
18
16
  import createClientConfig from './client';
19
- import { TCompileMode } from './common';
20
-
21
- type TCompilerCallback = (compiler: webpack.Compiler) => void
17
+ import { TCompileMode, TCompileOutputTarget } from './common';
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
- } = {},
53
- private debug: boolean = false
54
- ) {
55
-
56
- }
64
+ private callbacks: { before?: TCompilerCallback; after?: TCompilerCallback } = {},
65
+ private debug: boolean = false,
66
+ private outputTarget: TCompileOutputTarget = mode === 'dev' ? 'dev' : 'bin',
67
+ ) {}
57
68
 
58
69
  public cleanup() {
70
+ const outputPath = app.outputPath(this.outputTarget);
71
+ const generatedPublicEntries = new Set(['app']);
72
+ const outputPublicPath = path.join(outputPath, 'public');
73
+ const preserveDevOutput = this.mode === 'dev' && this.outputTarget === 'dev';
59
74
 
60
- fs.emptyDirSync( app.paths.bin );
61
- fs.ensureDirSync( path.join(app.paths.bin, 'public') )
62
- const publicFiles = fs.readdirSync(app.paths.public);
63
- for (const publicFile of publicFiles) {
64
- // Dev: faster to use symlink
65
- if (this.mode === 'dev')
66
- fs.symlinkSync(
67
- path.join(app.paths.public, publicFile),
68
- path.join(app.paths.bin, 'public', publicFile)
69
- );
70
- // Prod: Symlink not always supported by CI / Containers solutions
71
- else
72
- fs.copySync(
73
- path.join(app.paths.public, publicFile),
74
- path.join(app.paths.bin, 'public', publicFile)
75
- );
76
- }
75
+ if (!preserveDevOutput) fs.emptyDirSync(outputPath);
76
+
77
+ fs.ensureDirSync(outputPublicPath);
78
+ this.syncPublicEntries(outputPublicPath, generatedPublicEntries, this.mode === 'dev');
77
79
  }
78
80
  /* FIX issue with npm link
79
81
  When we install a module with npm link, this module's deps are not installed in the parent project scope
@@ -83,10 +85,11 @@ export default class Compiler {
83
85
  */
84
86
  public fixNpmLinkIssues() {
85
87
  const corePath = path.join(app.paths.root, '/node_modules/proteum');
86
- if (!fs.lstatSync( corePath ).isSymbolicLink())
88
+ if (!fs.lstatSync(corePath).isSymbolicLink())
87
89
  return console.info("Not fixing npm issue because proteum wasn't installed with npm link.");
88
90
 
89
91
  this.debug && console.info(`Fix NPM link issues ...`);
92
+ const outputPath = app.outputPath(this.outputTarget);
90
93
 
91
94
  const appModules = path.join(app.paths.root, 'node_modules');
92
95
  const coreModules = path.join(corePath, 'node_modules');
@@ -95,191 +98,718 @@ export default class Compiler {
95
98
  // Modules are installed locally and not glbally as with with the 5htp package from NPM.
96
99
  // So we need to symbilnk the http-core node_modules in one of the parents of server.js.
97
100
  // It avoids errors like: "Error: Cannot find module 'intl'"
98
- fs.symlinkSync( coreModules, path.join(app.paths.bin, 'node_modules') );
101
+ this.ensureSymlinkSync(coreModules, path.join(outputPath, 'node_modules'));
99
102
 
100
- // Same problem: when 5htp-core is installed via npm link,
103
+ // Same problem: when 5htp-core is installed via npm link,
101
104
  // Typescript doesn't detect React and shows mission JSX errors
102
105
  const preactCoreModule = path.join(coreModules, 'preact');
103
106
  const preactAppModule = path.join(appModules, 'preact');
104
107
  const reactAppModule = path.join(appModules, 'react');
105
108
 
106
- if (!fs.existsSync( preactAppModule ))
107
- fs.symlinkSync( preactCoreModule, preactAppModule );
108
- if (!fs.existsSync( reactAppModule ))
109
- 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);
110
111
  }
111
112
 
112
- private findServices( dir: string ) {
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;
113
120
 
114
- const blacklist = ['node_modules', 'proteum']
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
+ }
137
+ }
138
+
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
+ }
157
+
158
+ private findServices(dir: string) {
159
+ const blacklist = ['node_modules', 'proteum'];
115
160
  const files: string[] = [];
116
161
  const dirents = fs.readdirSync(dir, { withFileTypes: true });
117
162
 
118
163
  for (let dirent of dirents) {
119
-
120
164
  let fileName = dirent.name;
121
165
  let filePath = path.resolve(dir, fileName);
122
166
 
123
- if (blacklist.includes( fileName ))
124
- continue;
167
+ if (blacklist.includes(fileName)) continue;
125
168
 
126
169
  // Define is we should recursively find service in the current item
127
170
  let iterate: boolean = false;
128
171
  if (dirent.isSymbolicLink()) {
129
-
130
- const realPath = path.resolve( dir, fs.readlinkSync(filePath) );
131
- const destinationInfos = fs.lstatSync( realPath );
132
- if (destinationInfos.isDirectory())
133
- iterate = true;
134
-
135
- } else if (dirent.isDirectory())
136
- 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;
137
176
 
138
177
  // Update the list of found services
139
178
  if (iterate) {
140
- files.push( ...this.findServices(filePath) );
179
+ files.push(...this.findServices(filePath));
141
180
  } else if (dirent.name === 'service.json') {
142
- files.push( path.dirname(filePath) );
181
+ files.push(path.dirname(filePath));
143
182
  }
144
183
  }
145
184
  return files;
146
185
  }
147
186
 
148
- private indexServices() {
149
-
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
+ ----------------------------------*/
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
+ };
150
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() {
151
675
  // Index services
152
676
  const searchDirs = [
153
677
  // The less priority is the first
154
- {
155
- path: '@server/services/',
156
- priority: -1,
157
- root: path.join(cli.paths.core.root, 'server', 'services')
158
- },
159
- {
160
- path: '@/server/services/',
161
- priority: 0,
162
- root: path.join(app.paths.root, 'server', 'services')
163
- },
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') },
164
680
  // Temp disabled because compile issue on vercel
165
681
  //'': path.join(app.paths.root, 'node_modules'),
166
- ]
682
+ ];
167
683
 
168
684
  // Generate app class file
169
- const servicesAvailable: {[id: string]: TServiceMetas} = {};
685
+ const servicesAvailable: { [id: string]: TServiceMetas } = {};
170
686
  for (const searchDir of searchDirs) {
171
-
172
687
  const services = this.findServices(searchDir.root);
173
688
 
174
689
  for (const serviceDir of services) {
175
- const metasFile = path.join( serviceDir, 'service.json');
690
+ const metasFile = path.join(serviceDir, 'service.json');
176
691
 
177
692
  // The +1 is to remove the slash
178
- const importationPath = searchDir.path + serviceDir.substring( searchDir.root.length + 1 );
693
+ const importationPath = searchDir.path + serviceDir.substring(searchDir.root.length + 1);
179
694
 
180
695
  const serviceMetas = require(metasFile);
181
696
 
182
- servicesAvailable[ serviceMetas.id ] = {
183
- importationPath,
184
- priority: searchDir.priority,
185
- ...serviceMetas,
186
- };
697
+ servicesAvailable[serviceMetas.id] = { importationPath, priority: searchDir.priority, ...serviceMetas };
187
698
  }
188
699
  }
189
700
 
190
701
  // Read app services
191
- const imported: string[] = []
192
- 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;
193
705
 
194
706
  const refService = (serviceName: string, serviceConfig: any, level: number = 0): TRegisteredService => {
195
-
196
707
  if (serviceConfig.refTo !== undefined) {
197
708
  const refTo = serviceConfig.refTo;
198
709
  return {
199
710
  name: serviceName,
200
- instanciation: () => `this.${refTo}`,
201
- priority: 0
202
- }
711
+ className: serviceName,
712
+ instanciation: (_parentRef, appRef = 'this') => `${appRef}.${refTo}`,
713
+ priority: 0,
714
+ };
203
715
  }
204
716
 
205
- const serviceMetas = servicesAvailable[ serviceConfig.id ];
717
+ const serviceMetas = servicesAvailable[serviceConfig.id];
206
718
  if (serviceMetas === undefined)
207
- 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
+ );
208
722
 
209
723
  const referencedName = referencedNames[serviceConfig.id];
210
724
  if (referencedName !== undefined)
211
725
  throw new Error(`Service ${serviceConfig.id} is already setup as ${referencedName}`);
212
-
213
- // Generate index & typings
214
- imported.push(`import ${serviceMetas.name} from "${serviceMetas.importationPath}";`);
215
726
 
216
- if (serviceConfig.name !== undefined)
217
- referencedNames[serviceConfig.id] = serviceConfig.name;
727
+ // Generate index & typings
728
+ const importIdentifier = `${serviceMetas.name}Class${serviceImportIndex++}`;
729
+ imported.push(`import ${importIdentifier} from "${serviceMetas.importationPath}";`);
218
730
 
219
- const processConfig = (config: any, level: number = 0) => {
731
+ if (serviceConfig.name !== undefined) referencedNames[serviceConfig.id] = serviceConfig.name;
220
732
 
733
+ const processConfig = (config: any, level: number = 0, appRef: string = 'this') => {
221
734
  let propsStr = '';
222
735
  for (const key in config) {
223
736
  const value = config[key];
224
737
 
225
738
  if (!value || typeof value !== 'object')
226
739
  propsStr += `"${key}":${serialize(value, { space: 4 })},\n`;
227
-
228
740
  // Reference to a service
229
- else if (value.type === 'service.setup' || value.type === 'service.ref') // TODO: more reliable way to detect a service reference
230
- propsStr += `${key}:`+ refService(key, value, level + 1).instanciation() + ',\n'
231
-
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';
232
744
  // Recursion
233
745
  else if (level <= 4 && !Array.isArray(value))
234
- propsStr += `"${key}":` + processConfig(value, level + 1) + ',\n';
235
-
236
- else
237
- propsStr += `"${key}":${serialize(value, { space: 4 })},\n`;
238
-
746
+ propsStr += `"${key}":` + processConfig(value, level + 1, appRef) + ',\n';
747
+ else propsStr += `"${key}":${serialize(value, { space: 4 })},\n`;
239
748
  }
240
749
 
241
750
  return `{ ${propsStr} }`;
242
- }
243
- const config = processConfig(serviceConfig.config || {});
751
+ };
244
752
 
245
753
  // Generate the service instance
246
- const instanciation = (parentRef?: string) =>
247
- `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}(
248
762
  ${parentRef ? `${parentRef},` : ''}
249
- ${config},
250
- this
251
- )`
763
+ ${typedRouterConfig},
764
+ ${appRef}
765
+ )`;
766
+ };
252
767
 
253
768
  return {
254
769
  id: serviceConfig.id,
255
770
  name: serviceName,
256
771
  instanciation,
257
- className: serviceMetas.name,
772
+ className: importIdentifier,
258
773
  priority: serviceConfig.config?.priority || serviceMetas.priority || 0,
259
774
  };
260
- }
775
+ };
261
776
 
262
- 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));
263
778
  const sortedServices = servicesCode.sort((a, b) => a.priority - b.priority);
264
779
 
265
780
  // Define the app class identifier
266
781
  const appClassIdentifier = app.identity.identifier;
267
- 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');
268
793
 
269
794
  // @/client/.generated/services.d.ts
270
- fs.outputFileSync(
271
- path.join( app.paths.client.generated, 'services.d.ts'),
272
- `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" {
273
800
 
274
801
  import { ${appClassIdentifier} as ${appClassIdentifier}Client } from "@/client";
275
802
  import ${appClassIdentifier}Server from "@/server/.generated/app";
276
803
 
277
804
  export const Router: ${appClassIdentifier}Client['Router'];
278
805
 
279
- ${sortedServices.map(service => service.name !== 'Router'
280
- ? `export const ${service.name}: ${appClassIdentifier}Server["${service.name}"];`
281
- : ''
282
- ).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')}
283
813
 
284
814
  }
285
815
 
@@ -299,91 +829,96 @@ declare module '@common/errors' {
299
829
  export type UpgradeRequired = import('@common/errors/index').UpgradeRequired<FeatureKeys>;
300
830
  }
301
831
 
302
- declare module '@request' {
303
-
304
- }
305
-
306
832
  declare namespace preact.JSX {
307
833
  interface HTMLAttributes {
308
834
  src?: string;
309
835
  }
310
836
  }
311
- `
837
+ `,
312
838
  );
313
839
 
314
840
  // @/client/.generated/context.ts
315
- fs.outputFileSync(
316
- path.join( app.paths.client.generated, 'context.ts'),
317
- `// 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 ?)
318
844
  import React from 'react';
319
845
 
320
- import type ${appClassIdentifier}Server from '@/server/.generated/app';
321
- import type { TRouterContext as TServerRouterRequestContext } from '@server/services/router/response';
322
- import type { TRouterContext as TClientRouterRequestContext } from '@client/services/router/response';
323
- import type ${appClassIdentifier}Client from '.';
846
+ import type ${appClassIdentifier}Client from '@/client/index';
324
847
 
325
- // TO Fix: TClientRouterRequestContext is unable to get the right type of ${appClassIdentifier}Client["router"]
326
- // (it gets ClientApplication instead of ${appClassIdentifier}Client)
327
- type ClientRequestContext = TClientRouterRequestContext<${appClassIdentifier}Client["Router"], ${appClassIdentifier}Client>;
328
- type ServerRequestContext = TServerRouterRequestContext<${appClassIdentifier}Server["Router"]>
329
- type UniversalServices = ClientRequestContext | ServerRequestContext
330
-
331
- // Non-universla services are flagged as potentially undefined
332
- export type ClientContext = (
333
- UniversalServices
334
- &
335
- Partial<Omit<ClientRequestContext, keyof UniversalServices>>
336
- &
337
- {
338
- Router: ${appClassIdentifier}Client["Router"],
339
- }
340
- )
848
+ export type ClientContext = ${appClassIdentifier}Client["Router"]["context"];
341
849
 
342
850
  export const ReactClientContext = React.createContext<ClientContext>({} as ClientContext);
343
- export default (): ClientContext => React.useContext<ClientContext>(ReactClientContext);`);
851
+ export default (): ClientContext => React.useContext<ClientContext>(ReactClientContext);`,
852
+ );
344
853
 
345
854
  // @/common/.generated/services.d.ts
346
- fs.outputFileSync(
347
- path.join( app.paths.common.generated, 'services.d.ts'),
348
- `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' {
349
860
  export * from '@/var/prisma/index';
350
- }`
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
+ `,
351
869
  );
352
870
 
353
871
  // @/server/.generated/app.ts
354
- fs.outputFileSync(
355
- path.join( app.paths.server.generated, 'app.ts'),
356
- `
872
+ writeIfChanged(
873
+ path.join(app.paths.server.generated, 'app.ts'),
874
+ `
357
875
  import { Application } from '@server/app/index';
358
876
  import { ServicesContainer } from '@server/app/service/container';
359
877
 
360
878
  ${imported.join('\n')}
361
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
+
362
890
  export default class ${appClassIdentifier} extends Application<ServicesContainer, CurrentUser> {
363
891
 
364
892
  // Make sure the services typigs are reflecting the config and referring to the app
365
- ${sortedServices.map(service =>
366
- `public ${service.name}!: ReturnType<${appClassIdentifier}["registered"]["${service.id}"]["start"]>;`
367
- ).join('\n')}
368
-
369
- protected registered = {
370
- ${sortedServices.map(service =>
371
- `"${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}": {
372
905
  name: "${service.name}",
373
906
  priority: ${service.priority},
374
- start: () => ${service.instanciation('this')}
375
- }`
376
- ).join(',\n')}
377
- } as const;
907
+ start: () => create${service.className}(this)
908
+ }`,
909
+ )
910
+ .join(',\n')}
911
+ };
378
912
  }
379
913
 
380
914
 
381
- `);
915
+ `,
916
+ );
382
917
 
383
918
  // @/server/.generated/services.d.ts
384
- fs.outputFileSync(
385
- path.join( app.paths.server.generated, 'services.d.ts'),
386
- `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>;
387
922
 
388
923
  declare type ${appClassIdentifier} = import("@/server/.generated/app").default;
389
924
 
@@ -392,9 +927,9 @@ declare module '@cli/app' {
392
927
  type TSetupConfig<TConfig> =
393
928
  TConfig extends (...args: any[]) => any ? TConfig
394
929
  : TConfig extends Array<infer TItem> ? Array<TSetupConfig<TItem>>
395
- : TConfig extends object ? {
396
- [K in keyof TConfig]: TSetupConfig<TConfig[K]> | TServiceSetup | TServiceRef
397
- }
930
+ : TConfig extends object ? ({
931
+ [K in keyof TConfig]?: TSetupConfig<TConfig[K]> | TServiceSetup | TServiceRef
932
+ } & Record<string, unknown>)
398
933
  : TConfig;
399
934
 
400
935
  type App = {
@@ -454,26 +989,6 @@ declare module '@server/app' {
454
989
  export = foo;
455
990
  }
456
991
 
457
- declare module '@request' {
458
- import type { TRouterContext } from '@server/services/router/response';
459
- const routerContext: TRouterContext<${appClassIdentifier}["Router"]>;
460
- export = routerContext;
461
- }
462
-
463
- declare module '@models' {
464
- import { Prisma, PrismaClient } from '@/var/prisma/index';
465
-
466
- type ModelNames = Prisma.ModelName;
467
-
468
- type ModelDelegates = {
469
- [K in ModelNames]: PrismaClient[Uncapitalize<K>];
470
- };
471
-
472
- const models: ModelDelegates;
473
-
474
- export = models;
475
- }
476
-
477
992
  declare module '@common/errors' {
478
993
 
479
994
  export * from '@common/errors/index';
@@ -488,42 +1003,73 @@ declare module '@common/errors' {
488
1003
 
489
1004
  declare module '@models/types' {
490
1005
  export * from '@/var/prisma/index';
491
- }`
1006
+ }`,
492
1007
  );
493
1008
  }
494
1009
 
495
- public async create() {
496
-
1010
+ private async warmupApp() {
497
1011
  await app.warmup();
1012
+ }
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;
1026
+ }
1027
+
1028
+ public async refreshGeneratedTypings() {
1029
+ await this.warmupApp();
1030
+ await this.refreshGeneratedArtifacts();
1031
+ }
1032
+
1033
+ public consumeRecentCompilationResults() {
1034
+ const recentCompilationResults = { ...this.recentCompilationResults };
1035
+ this.recentCompilationResults = {};
1036
+ return recentCompilationResults;
1037
+ }
1038
+
1039
+ public async create() {
1040
+ await this.warmupApp();
498
1041
 
499
1042
  this.cleanup();
500
1043
 
501
1044
  this.fixNpmLinkIssues();
502
-
503
- this.indexServices();
1045
+ await this.refreshGeneratedArtifacts();
504
1046
 
505
1047
  // Create compilers
506
- const multiCompiler = webpack([
507
- smp.wrap( createServerConfig(app, this.mode) ),
508
- smp.wrap( createClientConfig(app, this.mode) )
1048
+ const multiCompiler = rspack([
1049
+ createServerConfig(app, this.mode, this.outputTarget),
1050
+ createClientConfig(app, this.mode, this.outputTarget),
509
1051
  ]);
510
1052
 
511
1053
  for (const compiler of multiCompiler.compilers) {
512
-
513
1054
  const name = compiler.name;
514
- if (name === undefined)
515
- 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.`);
516
1056
 
517
1057
  let timeStart = new Date();
518
1058
 
519
- let finished: (() => void);
520
- 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());
521
1064
 
522
1065
  compiler.hooks.compile.tap(name, (compilation) => {
523
-
524
- this.callbacks.before && this.callbacks.before( compiler );
1066
+ this.callbacks.before && this.callbacks.before(compiler);
525
1067
 
526
- 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));
527
1073
 
528
1074
  timeStart = new Date();
529
1075
  console.info(`[${name}] Compiling ...`);
@@ -531,22 +1077,29 @@ declare module '@models/types' {
531
1077
 
532
1078
  /* TODO: Ne pas résoudre la promise tant que la recompilation des données indexées (icones, identité, ...)
533
1079
  n'a pas été achevée */
534
- 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
+ };
535
1087
 
536
1088
  // Shiow status
537
1089
  const timeEnd = new Date();
538
1090
  const time = timeEnd.getTime() - timeStart.getTime();
539
- if (stats.hasErrors()) {
540
-
1091
+ if (!compilationSucceeded) {
541
1092
  console.info(stats.toString(compiler.options.stats));
542
1093
  console.error(`[${name}] Failed to compile after ${time} ms`);
543
1094
 
544
1095
  // Exit process with code 0, so the CI container can understand building failed
545
1096
  // Only in prod, because in dev, we want the compiler watcher continue running
546
- if (this.mode === 'prod')
547
- process.exit(0);
548
-
1097
+ if (this.mode === 'prod') process.exit(0);
549
1098
  } else {
1099
+ if (name === 'client') {
1100
+ writeClientManifest(stats, app.outputPath(this.outputTarget));
1101
+ }
1102
+
550
1103
  this.debug && console.info(stats.toString(compiler.options.stats));
551
1104
  console.info(`[${name}] Finished compilation after ${time} ms`);
552
1105
  }
@@ -558,7 +1111,5 @@ declare module '@models/types' {
558
1111
  }
559
1112
 
560
1113
  return multiCompiler;
561
-
562
1114
  }
563
-
564
1115
  }