proteum 2.0.0 → 2.1.0-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/AGENTS.md +13 -1
  2. package/README.md +375 -0
  3. package/agents/framework/AGENTS.md +917 -0
  4. package/agents/project/AGENTS.md +138 -0
  5. package/agents/{codex → project}/CODING_STYLE.md +3 -2
  6. package/agents/project/client/AGENTS.md +108 -0
  7. package/agents/{codex → project}/client/pages/AGENTS.md +8 -8
  8. package/agents/{codex → project}/server/routes/AGENTS.md +2 -1
  9. package/agents/project/server/services/AGENTS.md +170 -0
  10. package/agents/{codex → project}/tests/AGENTS.md +1 -0
  11. package/cli/app/config.ts +3 -2
  12. package/cli/app/index.ts +6 -66
  13. package/cli/bin.js +7 -2
  14. package/cli/commands/build.ts +94 -27
  15. package/cli/commands/check.ts +15 -1
  16. package/cli/commands/dev.ts +288 -132
  17. package/cli/commands/doctor.ts +108 -0
  18. package/cli/commands/explain.ts +226 -0
  19. package/cli/commands/init.ts +76 -70
  20. package/cli/commands/lint.ts +18 -1
  21. package/cli/commands/refresh.ts +16 -6
  22. package/cli/commands/typecheck.ts +14 -1
  23. package/cli/compiler/artifacts/controllers.ts +150 -0
  24. package/cli/compiler/artifacts/discovery.ts +132 -0
  25. package/cli/compiler/artifacts/manifest.ts +267 -0
  26. package/cli/compiler/artifacts/routing.ts +315 -0
  27. package/cli/compiler/artifacts/services.ts +480 -0
  28. package/cli/compiler/artifacts/shared.ts +12 -0
  29. package/cli/compiler/client/identite.ts +2 -1
  30. package/cli/compiler/client/index.ts +13 -3
  31. package/cli/compiler/common/controllers.ts +23 -28
  32. package/cli/compiler/common/files/style.ts +3 -4
  33. package/cli/compiler/common/generatedRouteModules.ts +333 -19
  34. package/cli/compiler/common/proteumManifest.ts +133 -0
  35. package/cli/compiler/index.ts +33 -896
  36. package/cli/compiler/server/index.ts +21 -4
  37. package/cli/context.ts +71 -0
  38. package/cli/index.ts +39 -181
  39. package/cli/presentation/commands.ts +208 -0
  40. package/cli/presentation/compileReporter.ts +65 -0
  41. package/cli/presentation/devSession.ts +70 -0
  42. package/cli/presentation/help.ts +193 -0
  43. package/cli/presentation/ink.ts +69 -0
  44. package/cli/presentation/layout.ts +83 -0
  45. package/cli/runtime/argv.ts +49 -0
  46. package/cli/runtime/command.ts +25 -0
  47. package/cli/runtime/commands.ts +221 -0
  48. package/cli/runtime/importEsm.ts +7 -0
  49. package/cli/runtime/verbose.ts +15 -0
  50. package/cli/utils/agents.ts +5 -4
  51. package/cli/utils/keyboard.ts +12 -6
  52. package/client/app/index.ts +0 -6
  53. package/client/services/router/index.tsx +1 -1
  54. package/client/services/router/response/index.tsx +2 -2
  55. package/common/dev/serverHotReload.ts +12 -0
  56. package/common/router/index.ts +3 -2
  57. package/common/router/layouts.ts +1 -1
  58. package/common/router/pageSetup.ts +1 -0
  59. package/package.json +10 -8
  60. package/prettier/router-registration-plugin.cjs +52 -0
  61. package/prettier.config.cjs +1 -0
  62. package/scripts/cleanup-generated-controllers.ts +2 -2
  63. package/scripts/fix-reference-app-typing.ts +2 -2
  64. package/scripts/format-router-registrations.ts +119 -0
  65. package/scripts/migrate-explicit-controllers-and-request.ts +423 -0
  66. package/scripts/refactor-server-controllers.ts +19 -18
  67. package/scripts/refactor-server-runtime-aliases.ts +1 -1
  68. package/server/app/commands.ts +309 -25
  69. package/server/app/container/config.ts +1 -1
  70. package/server/app/container/index.ts +2 -2
  71. package/server/app/controller/index.ts +13 -4
  72. package/server/app/index.ts +53 -37
  73. package/server/app/service/container.ts +26 -28
  74. package/server/app/service/index.ts +10 -20
  75. package/server/app.tsconfig.json +9 -2
  76. package/server/index.ts +32 -1
  77. package/server/services/auth/index.ts +234 -15
  78. package/server/services/auth/router/index.ts +39 -7
  79. package/server/services/auth/router/request.ts +40 -8
  80. package/server/services/disks/index.ts +1 -1
  81. package/server/services/prisma/Facet.ts +2 -2
  82. package/server/services/prisma/index.ts +28 -5
  83. package/server/services/prisma/mariadb.ts +47 -0
  84. package/server/services/router/http/index.ts +9 -1
  85. package/server/services/router/index.ts +10 -4
  86. package/server/services/router/response/index.ts +26 -6
  87. package/types/auth-check-rules.test.ts +51 -0
  88. package/types/controller-request-context.test.ts +55 -0
  89. package/types/service-config.test.ts +39 -0
  90. package/agents/codex/AGENTS.md +0 -95
  91. package/agents/codex/client/AGENTS.md +0 -102
  92. package/agents/codex/server/services/AGENTS.md +0 -137
  93. package/server/services/models.7z +0 -0
  94. /package/agents/{codex → project}/agents.md.zip +0 -0
@@ -5,51 +5,26 @@
5
5
  // Npm
6
6
  import path from 'path';
7
7
  import fs from 'fs-extra';
8
- import serialize from 'serialize-javascript';
9
8
  import { rspack, type Compiler as RspackCompiler } from '@rspack/core';
10
- import ts from 'typescript';
11
9
 
12
10
  // Core
13
11
  import app from '../app';
14
- import cli from '..';
15
12
  import createServerConfig from './server';
16
13
  import createClientConfig from './client';
17
14
  import { TCompileMode, TCompileOutputTarget } from './common';
18
- import {
19
- indexControllers,
20
- generateControllerClientTree,
21
- printControllerTree,
22
- type TControllerServiceRoot,
23
- } from './common/controllers';
24
15
  import { writeClientManifest } from './common/clientManifest';
25
- import { getGeneratedRouteModuleFilepath, writeGeneratedRouteModule } from './common/generatedRouteModules';
26
- import writeIfChanged from './writeIfChanged';
16
+ import { logVerbose } from '../runtime/verbose';
17
+ import { createCompileReporter, type TCompileReporter } from '../presentation/compileReporter';
18
+ import { generateRoutingArtifacts } from './artifacts/routing';
19
+ import { generateControllerArtifacts } from './artifacts/controllers';
20
+ import { generateServiceArtifacts } from './artifacts/services';
21
+ import { writeCurrentProteumManifest } from './artifacts/manifest';
22
+ import { normalizePath } from './artifacts/shared';
27
23
 
28
24
  type TCompilerCallback = (compiler: RspackCompiler) => void;
29
25
 
30
- type TServiceMetas = {
31
- id: string;
32
- name: string;
33
- parent: string;
34
- dependences: string;
35
- importationPath: string;
36
- priority: number;
37
- };
38
-
39
- type TRegisteredService = {
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
26
  type TRecentCompilationResult = { succeeded: boolean; hash?: string; modifiedFiles?: string[] };
50
27
 
51
- const normalizePath = (value: string) => value.replace(/\\/g, '/');
52
-
53
28
  /*----------------------------------
54
29
  - FONCTION
55
30
  ----------------------------------*/
@@ -58,6 +33,7 @@ export default class Compiler {
58
33
  private recentCompilationResults: { [compiler: string]: TRecentCompilationResult } = {};
59
34
  private recentModifiedFiles: { [compiler: string]: string[] } = {};
60
35
  private refreshingGeneratedArtifacts?: Promise<void>;
36
+ private compileReporter?: TCompileReporter;
61
37
 
62
38
  public constructor(
63
39
  private mode: TCompileMode,
@@ -86,9 +62,9 @@ export default class Compiler {
86
62
  public fixNpmLinkIssues() {
87
63
  const corePath = path.join(app.paths.root, '/node_modules/proteum');
88
64
  if (!fs.lstatSync(corePath).isSymbolicLink())
89
- return console.info("Not fixing npm issue because proteum wasn't installed with npm link.");
65
+ return logVerbose("Not fixing npm issue because proteum wasn't installed with npm link.");
90
66
 
91
- this.debug && console.info(`Fix NPM link issues ...`);
67
+ this.debug && logVerbose(`Fix NPM link issues ...`);
92
68
  const outputPath = app.outputPath(this.outputTarget);
93
69
 
94
70
  const appModules = path.join(app.paths.root, 'node_modules');
@@ -155,858 +131,6 @@ export default class Compiler {
155
131
  fs.symlinkSync(targetPath, linkPath);
156
132
  }
157
133
 
158
- private findServices(dir: string) {
159
- const blacklist = ['node_modules', 'proteum'];
160
- const files: string[] = [];
161
- const dirents = fs.readdirSync(dir, { withFileTypes: true });
162
-
163
- for (let dirent of dirents) {
164
- let fileName = dirent.name;
165
- let filePath = path.resolve(dir, fileName);
166
-
167
- if (blacklist.includes(fileName)) continue;
168
-
169
- // Define is we should recursively find service in the current item
170
- let iterate: boolean = false;
171
- if (dirent.isSymbolicLink()) {
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;
176
-
177
- // Update the list of found services
178
- if (iterate) {
179
- files.push(...this.findServices(filePath));
180
- } else if (dirent.name === 'service.json') {
181
- files.push(path.dirname(filePath));
182
- }
183
- }
184
- return files;
185
- }
186
-
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
- };
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() {
675
- // Index services
676
- const searchDirs = [
677
- // The less priority is the first
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') },
680
- // Temp disabled because compile issue on vercel
681
- //'': path.join(app.paths.root, 'node_modules'),
682
- ];
683
-
684
- // Generate app class file
685
- const servicesAvailable: { [id: string]: TServiceMetas } = {};
686
- for (const searchDir of searchDirs) {
687
- const services = this.findServices(searchDir.root);
688
-
689
- for (const serviceDir of services) {
690
- const metasFile = path.join(serviceDir, 'service.json');
691
-
692
- // The +1 is to remove the slash
693
- const importationPath = searchDir.path + serviceDir.substring(searchDir.root.length + 1);
694
-
695
- const serviceMetas = require(metasFile);
696
-
697
- servicesAvailable[serviceMetas.id] = { importationPath, priority: searchDir.priority, ...serviceMetas };
698
- }
699
- }
700
-
701
- // Read app services
702
- const imported: string[] = [];
703
- const referencedNames: { [serviceId: string]: string } = {}; // ID to Name
704
- let serviceImportIndex = 0;
705
-
706
- const refService = (serviceName: string, serviceConfig: any, level: number = 0): TRegisteredService => {
707
- if (serviceConfig.refTo !== undefined) {
708
- const refTo = serviceConfig.refTo;
709
- return {
710
- name: serviceName,
711
- className: serviceName,
712
- instanciation: (_parentRef, appRef = 'this') => `${appRef}.${refTo}`,
713
- priority: 0,
714
- };
715
- }
716
-
717
- const serviceMetas = servicesAvailable[serviceConfig.id];
718
- if (serviceMetas === undefined)
719
- throw new Error(
720
- `Service ${serviceConfig.id} not found. Referenced services: ${Object.keys(servicesAvailable).join('\n')}`,
721
- );
722
-
723
- const referencedName = referencedNames[serviceConfig.id];
724
- if (referencedName !== undefined)
725
- throw new Error(`Service ${serviceConfig.id} is already setup as ${referencedName}`);
726
-
727
- // Generate index & typings
728
- const importIdentifier = `${serviceMetas.name}Class${serviceImportIndex++}`;
729
- imported.push(`import ${importIdentifier} from "${serviceMetas.importationPath}";`);
730
-
731
- if (serviceConfig.name !== undefined) referencedNames[serviceConfig.id] = serviceConfig.name;
732
-
733
- const processConfig = (config: any, level: number = 0, appRef: string = 'this') => {
734
- let propsStr = '';
735
- for (const key in config) {
736
- const value = config[key];
737
-
738
- if (!value || typeof value !== 'object')
739
- propsStr += `"${key}":${serialize(value, { space: 4 })},\n`;
740
- // Reference to a service
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';
744
- // Recursion
745
- else if (level <= 4 && !Array.isArray(value))
746
- propsStr += `"${key}":` + processConfig(value, level + 1, appRef) + ',\n';
747
- else propsStr += `"${key}":${serialize(value, { space: 4 })},\n`;
748
- }
749
-
750
- return `{ ${propsStr} }`;
751
- };
752
-
753
- // Generate the service instance
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}(
762
- ${parentRef ? `${parentRef},` : ''}
763
- ${typedRouterConfig},
764
- ${appRef}
765
- )`;
766
- };
767
-
768
- return {
769
- id: serviceConfig.id,
770
- name: serviceName,
771
- instanciation,
772
- className: importIdentifier,
773
- priority: serviceConfig.config?.priority || serviceMetas.priority || 0,
774
- };
775
- };
776
-
777
- const servicesCode = Object.values(app.registered).map((s) => refService(s.name, s, 0));
778
- const sortedServices = servicesCode.sort((a, b) => a.priority - b.priority);
779
-
780
- // Define the app class identifier
781
- const appClassIdentifier = app.identity.identifier;
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');
793
-
794
- // @/client/.generated/services.d.ts
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" {
800
-
801
- import { ${appClassIdentifier} as ${appClassIdentifier}Client } from "@/client";
802
- import ${appClassIdentifier}Server from "@/server/.generated/app";
803
-
804
- export const Router: ${appClassIdentifier}Client['Router'];
805
-
806
- ${sortedServices
807
- .map((service) =>
808
- service.name !== 'Router'
809
- ? `export const ${service.name}: ${appClassIdentifier}Server["${service.name}"];`
810
- : '',
811
- )
812
- .join('\n')}
813
-
814
- }
815
-
816
- declare module '@models/types' {
817
- export * from '@/var/prisma/index';
818
- }
819
-
820
- declare module '@common/errors' {
821
-
822
- export * from '@common/errors/index';
823
- export { default } from '@common/errors/index';
824
-
825
- export const AuthRequired: typeof import('@common/errors/index').AuthRequired<FeatureKeys>;
826
- export type AuthRequired = import('@common/errors/index').AuthRequired<FeatureKeys>;
827
-
828
- export const UpgradeRequired: typeof import('@common/errors/index').UpgradeRequired<FeatureKeys>;
829
- export type UpgradeRequired = import('@common/errors/index').UpgradeRequired<FeatureKeys>;
830
- }
831
-
832
- declare namespace preact.JSX {
833
- interface HTMLAttributes {
834
- src?: string;
835
- }
836
- }
837
- `,
838
- );
839
-
840
- // @/client/.generated/context.ts
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 ?)
844
- import React from 'react';
845
-
846
- import type ${appClassIdentifier}Client from '@/client/index';
847
-
848
- export type ClientContext = ${appClassIdentifier}Client["Router"]["context"];
849
-
850
- export const ReactClientContext = React.createContext<ClientContext>({} as ClientContext);
851
- export default (): ClientContext => React.useContext<ClientContext>(ReactClientContext);`,
852
- );
853
-
854
- // @/common/.generated/services.d.ts
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' {
860
- export * from '@/var/prisma/index';
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
- `,
869
- );
870
-
871
- // @/server/.generated/app.ts
872
- writeIfChanged(
873
- path.join(app.paths.server.generated, 'app.ts'),
874
- `
875
- import { Application } from '@server/app/index';
876
- import { ServicesContainer } from '@server/app/service/container';
877
-
878
- ${imported.join('\n')}
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
-
890
- export default class ${appClassIdentifier} extends Application<ServicesContainer, CurrentUser> {
891
-
892
- // Make sure the services typigs are reflecting the config and referring to the app
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}": {
905
- name: "${service.name}",
906
- priority: ${service.priority},
907
- start: () => create${service.className}(this)
908
- }`,
909
- )
910
- .join(',\n')}
911
- };
912
- }
913
-
914
-
915
- `,
916
- );
917
-
918
- // @/server/.generated/services.d.ts
919
- writeIfChanged(
920
- path.join(app.paths.server.generated, 'services.d.ts'),
921
- `type InstalledServices = Record<string, import('@server/app/service').AnyService>;
922
-
923
- declare type ${appClassIdentifier} = import("@/server/.generated/app").default;
924
-
925
- declare module '@cli/app' {
926
-
927
- type TSetupConfig<TConfig> =
928
- TConfig extends (...args: any[]) => any ? TConfig
929
- : TConfig extends Array<infer TItem> ? Array<TSetupConfig<TItem>>
930
- : TConfig extends object ? ({
931
- [K in keyof TConfig]?: TSetupConfig<TConfig[K]> | TServiceSetup | TServiceRef
932
- } & Record<string, unknown>)
933
- : TConfig;
934
-
935
- type App = {
936
-
937
- env: TEnvConfig;
938
-
939
- use: (referenceName: string) => TServiceRef;
940
-
941
- setup: <TServiceName extends keyof ${appClassIdentifier}>(...args: [
942
- // { user: app.setup('Core/User') }
943
- servicePath: string,
944
- serviceConfig?: {}
945
- ] | [
946
- // app.setup('User', 'Core/User')
947
- serviceName: TServiceName,
948
- servicePath: string,
949
- serviceConfig?: TSetupConfig<${appClassIdentifier}[TServiceName]["config"]>
950
- ]) => TServiceSetup;
951
- }
952
- const app: App;
953
- export = app;
954
- }
955
-
956
- declare module "@app" {
957
-
958
- import { ApplicationContainer } from '@server/app/container';
959
-
960
- const ServerServices: (
961
- Pick<
962
- ApplicationContainer<InstalledServices>,
963
- ${containerServices}
964
- >
965
- &
966
- ${appClassIdentifier}
967
- )
968
-
969
- export = ServerServices
970
- }
971
-
972
- declare module '@server/app' {
973
-
974
- import { Application } from "@server/app";
975
- import { Environment } from "@server/app";
976
- import { ServicesContainer } from "@server/app/service/container";
977
-
978
- abstract class ApplicationWithServices extends Application<
979
- ServicesContainer<InstalledServices>
980
- > {}
981
-
982
- export interface Exported {
983
- Application: typeof ApplicationWithServices,
984
- Environment: Environment,
985
- }
986
-
987
- const foo: Exported;
988
-
989
- export = foo;
990
- }
991
-
992
- declare module '@common/errors' {
993
-
994
- export * from '@common/errors/index';
995
- export { default } from '@common/errors/index';
996
-
997
- export const AuthRequired: typeof import('@common/errors/index').AuthRequired<FeatureKeys>;
998
- export type AuthRequired = import('@common/errors/index').AuthRequired<FeatureKeys>;
999
-
1000
- export const UpgradeRequired: typeof import('@common/errors/index').UpgradeRequired<FeatureKeys>;
1001
- export type UpgradeRequired = import('@common/errors/index').UpgradeRequired<FeatureKeys>;
1002
- }
1003
-
1004
- declare module '@models/types' {
1005
- export * from '@/var/prisma/index';
1006
- }`,
1007
- );
1008
- }
1009
-
1010
134
  private async warmupApp() {
1011
135
  await app.warmup();
1012
136
  }
@@ -1014,9 +138,16 @@ declare module '@models/types' {
1014
138
  private async refreshGeneratedArtifacts() {
1015
139
  if (!this.refreshingGeneratedArtifacts) {
1016
140
  this.refreshingGeneratedArtifacts = (async () => {
1017
- this.indexServices();
1018
- this.generateControllerModules();
1019
- this.generateRoutingModules();
141
+ const services = generateServiceArtifacts();
142
+ const controllers = generateControllerArtifacts();
143
+ const { clientRoutes, serverRoutes, layouts } = generateRoutingArtifacts();
144
+
145
+ writeCurrentProteumManifest({
146
+ services,
147
+ controllers,
148
+ routes: { client: clientRoutes, server: serverRoutes },
149
+ layouts,
150
+ });
1020
151
  })().finally(() => {
1021
152
  this.refreshingGeneratedArtifacts = undefined;
1022
153
  });
@@ -1030,6 +161,11 @@ declare module '@models/types' {
1030
161
  await this.refreshGeneratedArtifacts();
1031
162
  }
1032
163
 
164
+ public dispose() {
165
+ this.compileReporter?.stop();
166
+ this.compileReporter = undefined;
167
+ }
168
+
1033
169
  public consumeRecentCompilationResults() {
1034
170
  const recentCompilationResults = { ...this.recentCompilationResults };
1035
171
  this.recentCompilationResults = {};
@@ -1049,6 +185,9 @@ declare module '@models/types' {
1049
185
  createServerConfig(app, this.mode, this.outputTarget),
1050
186
  createClientConfig(app, this.mode, this.outputTarget),
1051
187
  ]);
188
+ this.compileReporter = createCompileReporter({
189
+ enabled: this.mode === 'dev' && this.outputTarget === 'dev',
190
+ });
1052
191
 
1053
192
  for (const compiler of multiCompiler.compilers) {
1054
193
  const name = compiler.name;
@@ -1072,7 +211,8 @@ declare module '@models/types' {
1072
211
  this.compiling[name] = new Promise((resolve) => (finished = resolve));
1073
212
 
1074
213
  timeStart = new Date();
1075
- console.info(`[${name}] Compiling ...`);
214
+ this.compileReporter?.start(name, this.recentModifiedFiles[name] || []);
215
+ logVerbose(`[${name}] Compiling ...`);
1076
216
  });
1077
217
 
1078
218
  /* TODO: Ne pas résoudre la promise tant que la recompilation des données indexées (icones, identité, ...)
@@ -1088,20 +228,17 @@ declare module '@models/types' {
1088
228
  // Shiow status
1089
229
  const timeEnd = new Date();
1090
230
  const time = timeEnd.getTime() - timeStart.getTime();
231
+ this.compileReporter?.finish(name, { succeeded: compilationSucceeded, durationMs: time });
1091
232
  if (!compilationSucceeded) {
1092
233
  console.info(stats.toString(compiler.options.stats));
1093
234
  console.error(`[${name}] Failed to compile after ${time} ms`);
1094
-
1095
- // Exit process with code 0, so the CI container can understand building failed
1096
- // Only in prod, because in dev, we want the compiler watcher continue running
1097
- if (this.mode === 'prod') process.exit(0);
1098
235
  } else {
1099
236
  if (name === 'client') {
1100
237
  writeClientManifest(stats, app.outputPath(this.outputTarget));
1101
238
  }
1102
239
 
1103
- this.debug && console.info(stats.toString(compiler.options.stats));
1104
- console.info(`[${name}] Finished compilation after ${time} ms`);
240
+ this.debug && logVerbose(stats.toString(compiler.options.stats));
241
+ logVerbose(`[${name}] Finished compilation after ${time} ms`);
1105
242
  }
1106
243
 
1107
244
  // Mark as finished