proteum 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/AGENTS.md +13 -1
  2. package/README.md +375 -0
  3. package/agents/framework/AGENTS.md +917 -0
  4. package/agents/project/AGENTS.md +138 -0
  5. package/agents/{codex → project}/CODING_STYLE.md +3 -2
  6. package/agents/project/client/AGENTS.md +108 -0
  7. package/agents/{codex → project}/client/pages/AGENTS.md +8 -8
  8. package/agents/{codex → project}/server/routes/AGENTS.md +2 -1
  9. package/agents/project/server/services/AGENTS.md +170 -0
  10. package/agents/{codex → project}/tests/AGENTS.md +1 -0
  11. package/cli/app/config.ts +3 -2
  12. package/cli/app/index.ts +6 -66
  13. package/cli/bin.js +7 -2
  14. package/cli/commands/build.ts +94 -27
  15. package/cli/commands/check.ts +15 -1
  16. package/cli/commands/dev.ts +288 -132
  17. package/cli/commands/doctor.ts +108 -0
  18. package/cli/commands/explain.ts +226 -0
  19. package/cli/commands/init.ts +76 -70
  20. package/cli/commands/lint.ts +18 -1
  21. package/cli/commands/refresh.ts +16 -6
  22. package/cli/commands/typecheck.ts +14 -1
  23. package/cli/compiler/artifacts/controllers.ts +150 -0
  24. package/cli/compiler/artifacts/discovery.ts +132 -0
  25. package/cli/compiler/artifacts/manifest.ts +267 -0
  26. package/cli/compiler/artifacts/routing.ts +315 -0
  27. package/cli/compiler/artifacts/services.ts +480 -0
  28. package/cli/compiler/artifacts/shared.ts +12 -0
  29. package/cli/compiler/client/identite.ts +2 -1
  30. package/cli/compiler/client/index.ts +13 -3
  31. package/cli/compiler/common/controllers.ts +23 -28
  32. package/cli/compiler/common/files/style.ts +3 -4
  33. package/cli/compiler/common/generatedRouteModules.ts +333 -19
  34. package/cli/compiler/common/proteumManifest.ts +133 -0
  35. package/cli/compiler/index.ts +33 -896
  36. package/cli/compiler/server/index.ts +21 -4
  37. package/cli/context.ts +71 -0
  38. package/cli/index.ts +39 -181
  39. package/cli/presentation/commands.ts +208 -0
  40. package/cli/presentation/compileReporter.ts +65 -0
  41. package/cli/presentation/devSession.ts +70 -0
  42. package/cli/presentation/help.ts +193 -0
  43. package/cli/presentation/ink.ts +69 -0
  44. package/cli/presentation/layout.ts +83 -0
  45. package/cli/runtime/argv.ts +49 -0
  46. package/cli/runtime/command.ts +25 -0
  47. package/cli/runtime/commands.ts +221 -0
  48. package/cli/runtime/importEsm.ts +7 -0
  49. package/cli/runtime/verbose.ts +15 -0
  50. package/cli/utils/agents.ts +5 -4
  51. package/cli/utils/keyboard.ts +12 -6
  52. package/client/app/index.ts +0 -6
  53. package/client/services/router/index.tsx +1 -1
  54. package/client/services/router/response/index.tsx +2 -2
  55. package/common/dev/serverHotReload.ts +12 -0
  56. package/common/router/index.ts +3 -2
  57. package/common/router/layouts.ts +1 -1
  58. package/common/router/pageSetup.ts +1 -0
  59. package/package.json +10 -8
  60. package/prettier/router-registration-plugin.cjs +52 -0
  61. package/prettier.config.cjs +1 -0
  62. package/scripts/cleanup-generated-controllers.ts +2 -2
  63. package/scripts/fix-reference-app-typing.ts +2 -2
  64. package/scripts/format-router-registrations.ts +119 -0
  65. package/scripts/migrate-explicit-controllers-and-request.ts +423 -0
  66. package/scripts/refactor-server-controllers.ts +19 -18
  67. package/scripts/refactor-server-runtime-aliases.ts +1 -1
  68. package/server/app/commands.ts +309 -25
  69. package/server/app/container/config.ts +1 -1
  70. package/server/app/container/index.ts +2 -2
  71. package/server/app/controller/index.ts +13 -4
  72. package/server/app/index.ts +53 -37
  73. package/server/app/service/container.ts +26 -28
  74. package/server/app/service/index.ts +10 -20
  75. package/server/app.tsconfig.json +9 -2
  76. package/server/index.ts +32 -1
  77. package/server/services/auth/index.ts +234 -15
  78. package/server/services/auth/router/index.ts +39 -7
  79. package/server/services/auth/router/request.ts +40 -8
  80. package/server/services/disks/index.ts +1 -1
  81. package/server/services/prisma/Facet.ts +2 -2
  82. package/server/services/prisma/index.ts +22 -5
  83. package/server/services/prisma/mariadb.ts +47 -0
  84. package/server/services/router/http/index.ts +9 -1
  85. package/server/services/router/index.ts +10 -4
  86. package/server/services/router/response/index.ts +26 -6
  87. package/types/auth-check-rules.test.ts +51 -0
  88. package/types/controller-request-context.test.ts +55 -0
  89. package/types/service-config.test.ts +39 -0
  90. package/agents/codex/AGENTS.md +0 -95
  91. package/agents/codex/client/AGENTS.md +0 -102
  92. package/agents/codex/server/services/AGENTS.md +0 -137
  93. package/server/services/models.7z +0 -0
  94. /package/agents/{codex → project}/agents.md.zip +0 -0
@@ -0,0 +1,480 @@
1
+ import path from 'path';
2
+ import fs from 'fs-extra';
3
+ import ts from 'typescript';
4
+
5
+ import app from '../../app';
6
+ import cli from '../..';
7
+ import { TProteumManifestScope, TProteumManifestService } from '../common/proteumManifest';
8
+ import writeIfChanged from '../writeIfChanged';
9
+ import { findServiceDirectories } from './discovery';
10
+ import { normalizeAbsolutePath } from './shared';
11
+
12
+ type TServiceMetas = {
13
+ id: string;
14
+ name: string;
15
+ parent: string;
16
+ dependences: string[];
17
+ importationPath: string;
18
+ priority: number;
19
+ sourceDir: string;
20
+ metasFilepath: string;
21
+ scope: TProteumManifestScope;
22
+ };
23
+
24
+ type TParsedService = {
25
+ registeredName: string;
26
+ priority: number;
27
+ meta: TServiceMetas;
28
+ };
29
+
30
+ type TParsedAppBootstrap = {
31
+ rootServices: TParsedService[];
32
+ routerPlugins: TParsedService[];
33
+ };
34
+
35
+ type TServicesAvailable = Record<string, TServiceMetas>;
36
+
37
+ const buildServicesAvailable = (): TServicesAvailable => {
38
+ const searchDirs = [
39
+ { path: '@server/services/', priority: -1, root: path.join(cli.paths.core.root, 'server', 'services') },
40
+ { path: '@/server/services/', priority: 0, root: path.join(app.paths.root, 'server', 'services') },
41
+ ];
42
+
43
+ const servicesAvailable: TServicesAvailable = {};
44
+
45
+ for (const searchDir of searchDirs) {
46
+ const services = findServiceDirectories(searchDir.root);
47
+
48
+ for (const serviceDir of services) {
49
+ const metasFile = path.join(serviceDir, 'service.json');
50
+ const importationPath = searchDir.path + serviceDir.substring(searchDir.root.length + 1);
51
+ const serviceMetas = fs.readJsonSync(metasFile) as {
52
+ id: string;
53
+ name: string;
54
+ parent: string;
55
+ dependences: string[];
56
+ priority?: number;
57
+ };
58
+
59
+ servicesAvailable[serviceMetas.id] = {
60
+ importationPath,
61
+ priority: searchDir.priority,
62
+ sourceDir: normalizeAbsolutePath(serviceDir),
63
+ metasFilepath: normalizeAbsolutePath(metasFile),
64
+ scope: searchDir.path.startsWith('@server/services/') ? 'framework' : 'app',
65
+ ...serviceMetas,
66
+ };
67
+ }
68
+ }
69
+
70
+ return servicesAvailable;
71
+ };
72
+
73
+ const getAppServerEntryFilepath = () => {
74
+ const filepath = app.paths.server.entry;
75
+
76
+ if (!fs.existsSync(filepath)) {
77
+ throw new Error(`Expected an explicit server application entrypoint at ${filepath}.`);
78
+ }
79
+
80
+ return filepath;
81
+ };
82
+
83
+ const createSourceFile = (filepath: string) =>
84
+ ts.createSourceFile(
85
+ filepath,
86
+ fs.readFileSync(filepath, 'utf8'),
87
+ ts.ScriptTarget.Latest,
88
+ true,
89
+ filepath.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
90
+ );
91
+
92
+ const hasModifier = (node: ts.Node, kind: ts.SyntaxKind) => {
93
+ const modifiers = (node as ts.Node & { modifiers?: ts.NodeArray<ts.Modifier> }).modifiers;
94
+
95
+ return modifiers?.some((modifier) => modifier.kind === kind) ?? false;
96
+ };
97
+
98
+ const getDefaultExportClassDeclaration = (sourceFile: ts.SourceFile) => {
99
+ for (const statement of sourceFile.statements) {
100
+ if (!ts.isClassDeclaration(statement)) continue;
101
+ if (!hasModifier(statement, ts.SyntaxKind.ExportKeyword)) continue;
102
+ if (!hasModifier(statement, ts.SyntaxKind.DefaultKeyword)) continue;
103
+
104
+ return statement;
105
+ }
106
+
107
+ let defaultExportIdentifier: string | undefined;
108
+
109
+ for (const statement of sourceFile.statements) {
110
+ if (!ts.isExportAssignment(statement)) continue;
111
+ if (!ts.isIdentifier(statement.expression)) continue;
112
+
113
+ defaultExportIdentifier = statement.expression.text;
114
+ break;
115
+ }
116
+
117
+ if (!defaultExportIdentifier) {
118
+ throw new Error(`Expected ${sourceFile.fileName} to default-export an Application subclass.`);
119
+ }
120
+
121
+ const declaration = sourceFile.statements.find(
122
+ (statement): statement is ts.ClassDeclaration =>
123
+ ts.isClassDeclaration(statement) && statement.name?.text === defaultExportIdentifier,
124
+ );
125
+
126
+ if (!declaration) {
127
+ throw new Error(
128
+ `Unable to resolve the default-exported Application class "${defaultExportIdentifier}" in ${sourceFile.fileName}.`,
129
+ );
130
+ }
131
+
132
+ return declaration;
133
+ };
134
+
135
+ const buildImportIndex = (sourceFile: ts.SourceFile) => {
136
+ const imports = new Map<string, string>();
137
+
138
+ for (const statement of sourceFile.statements) {
139
+ if (!ts.isImportDeclaration(statement)) continue;
140
+ if (!statement.importClause) continue;
141
+ if (!ts.isStringLiteral(statement.moduleSpecifier)) continue;
142
+
143
+ const importPath = statement.moduleSpecifier.text;
144
+ const { importClause } = statement;
145
+
146
+ if (importClause.name) {
147
+ imports.set(importClause.name.text, importPath);
148
+ }
149
+
150
+ const namedBindings = importClause.namedBindings;
151
+ if (!namedBindings || !ts.isNamedImports(namedBindings)) continue;
152
+
153
+ for (const element of namedBindings.elements) {
154
+ imports.set(element.name.text, importPath);
155
+ }
156
+ }
157
+
158
+ return imports;
159
+ };
160
+
161
+ const unwrapExpression = (expression: ts.Expression): ts.Expression => {
162
+ if (ts.isParenthesizedExpression(expression)) return unwrapExpression(expression.expression);
163
+ if (ts.isAsExpression(expression)) return unwrapExpression(expression.expression);
164
+ if (ts.isSatisfiesExpression(expression)) return unwrapExpression(expression.expression);
165
+ if (ts.isNonNullExpression(expression)) return unwrapExpression(expression.expression);
166
+
167
+ return expression;
168
+ };
169
+
170
+ const getPropertyNameText = (propertyName: ts.PropertyName | undefined): string | undefined => {
171
+ if (!propertyName) return undefined;
172
+ if (ts.isIdentifier(propertyName) || ts.isStringLiteral(propertyName) || ts.isNumericLiteral(propertyName))
173
+ return propertyName.text;
174
+
175
+ return undefined;
176
+ };
177
+
178
+ const extractPriorityFromConfig = (configExpression?: ts.Expression) => {
179
+ if (!configExpression) return 0;
180
+
181
+ const expression = unwrapExpression(configExpression);
182
+ if (!ts.isObjectLiteralExpression(expression)) return 0;
183
+
184
+ const priorityProperty = expression.properties.find((property) => {
185
+ if (!ts.isPropertyAssignment(property)) return false;
186
+
187
+ return getPropertyNameText(property.name) === 'priority';
188
+ });
189
+
190
+ if (!priorityProperty || !ts.isPropertyAssignment(priorityProperty)) return 0;
191
+
192
+ const priorityExpression = unwrapExpression(priorityProperty.initializer);
193
+ if (ts.isNumericLiteral(priorityExpression)) return Number(priorityExpression.text);
194
+
195
+ if (ts.isPrefixUnaryExpression(priorityExpression) && priorityExpression.operator === ts.SyntaxKind.MinusToken) {
196
+ const operand = unwrapExpression(priorityExpression.operand);
197
+ if (ts.isNumericLiteral(operand)) return -Number(operand.text);
198
+ }
199
+
200
+ return 0;
201
+ };
202
+
203
+ const resolveImportedService = (
204
+ expression: ts.LeftHandSideExpression,
205
+ imports: Map<string, string>,
206
+ servicesAvailable: TServicesAvailable,
207
+ ) => {
208
+ if (!ts.isIdentifier(expression)) return undefined;
209
+
210
+ const importPath = imports.get(expression.text);
211
+ if (!importPath) return undefined;
212
+
213
+ return Object.values(servicesAvailable).find((serviceMeta) => serviceMeta.importationPath === importPath);
214
+ };
215
+
216
+ const parseServiceInstantiation = (
217
+ registeredName: string,
218
+ expression: ts.Expression,
219
+ imports: Map<string, string>,
220
+ servicesAvailable: TServicesAvailable,
221
+ configArgIndex: number,
222
+ ): TParsedService | undefined => {
223
+ const unwrappedExpression = unwrapExpression(expression);
224
+ if (!ts.isNewExpression(unwrappedExpression)) return undefined;
225
+
226
+ const meta = resolveImportedService(unwrappedExpression.expression, imports, servicesAvailable);
227
+ if (!meta) return undefined;
228
+
229
+ const configExpression = unwrappedExpression.arguments?.[configArgIndex];
230
+
231
+ return {
232
+ registeredName,
233
+ priority: extractPriorityFromConfig(configExpression),
234
+ meta,
235
+ };
236
+ };
237
+
238
+ const getObjectLiteralProperty = (expression: ts.ObjectLiteralExpression, propertyName: string) =>
239
+ expression.properties.find((property) => {
240
+ if (!ts.isPropertyAssignment(property)) return false;
241
+
242
+ return getPropertyNameText(property.name) === propertyName;
243
+ });
244
+
245
+ const extractRouterPlugins = (
246
+ routerConfigExpression: ts.Expression | undefined,
247
+ imports: Map<string, string>,
248
+ servicesAvailable: TServicesAvailable,
249
+ ) => {
250
+ if (!routerConfigExpression) return [];
251
+
252
+ const configExpression = unwrapExpression(routerConfigExpression);
253
+ if (!ts.isObjectLiteralExpression(configExpression)) return [];
254
+
255
+ const pluginsProperty = getObjectLiteralProperty(configExpression, 'plugins');
256
+ if (!pluginsProperty || !ts.isPropertyAssignment(pluginsProperty)) return [];
257
+
258
+ const pluginsExpression = unwrapExpression(pluginsProperty.initializer);
259
+ if (!ts.isObjectLiteralExpression(pluginsExpression)) return [];
260
+
261
+ const routerPlugins: TParsedService[] = [];
262
+
263
+ for (const property of pluginsExpression.properties) {
264
+ if (!ts.isPropertyAssignment(property)) continue;
265
+
266
+ const registeredName = getPropertyNameText(property.name);
267
+ if (!registeredName) continue;
268
+
269
+ const routerPlugin = parseServiceInstantiation(registeredName, property.initializer, imports, servicesAvailable, 0);
270
+ if (!routerPlugin) continue;
271
+
272
+ routerPlugins.push(routerPlugin);
273
+ }
274
+
275
+ return routerPlugins;
276
+ };
277
+
278
+ const parseAppBootstrap = (servicesAvailable: TServicesAvailable): TParsedAppBootstrap => {
279
+ const sourceFile = createSourceFile(getAppServerEntryFilepath());
280
+ const imports = buildImportIndex(sourceFile);
281
+ const appClass = getDefaultExportClassDeclaration(sourceFile);
282
+
283
+ const rootServices: TParsedService[] = [];
284
+ let routerPlugins: TParsedService[] = [];
285
+
286
+ for (const member of appClass.members) {
287
+ if (!ts.isPropertyDeclaration(member) || !member.initializer) continue;
288
+
289
+ const registeredName = getPropertyNameText(member.name);
290
+ if (!registeredName) continue;
291
+
292
+ const rootService = parseServiceInstantiation(registeredName, member.initializer, imports, servicesAvailable, 1);
293
+ if (!rootService) continue;
294
+
295
+ rootServices.push(rootService);
296
+
297
+ if (rootService.meta.id === 'Core/Router') {
298
+ const initializer = unwrapExpression(member.initializer);
299
+ if (!ts.isNewExpression(initializer)) continue;
300
+
301
+ routerPlugins = extractRouterPlugins(initializer.arguments?.[1], imports, servicesAvailable);
302
+ }
303
+ }
304
+
305
+ if (rootServices.length === 0) {
306
+ throw new Error(`No root services were found in ${sourceFile.fileName}.`);
307
+ }
308
+
309
+ return { rootServices, routerPlugins };
310
+ };
311
+
312
+ const resolveManifestService = (service: TParsedService, parent: string): TProteumManifestService => ({
313
+ kind: 'service',
314
+ id: service.meta.id,
315
+ registeredName: service.registeredName,
316
+ metaName: service.meta.name,
317
+ parent,
318
+ priority: service.priority || service.meta.priority || 0,
319
+ importPath: service.meta.importationPath,
320
+ sourceDir: service.meta.sourceDir,
321
+ metasFilepath: service.meta.metasFilepath,
322
+ scope: service.meta.scope,
323
+ });
324
+
325
+ export const generateServiceArtifacts = () => {
326
+ const servicesAvailable = buildServicesAvailable();
327
+ const { rootServices, routerPlugins } = parseAppBootstrap(servicesAvailable);
328
+ const appClassIdentifier = app.identity.identifier;
329
+ const containerServices = app.containerServices.map((serviceName) => "'" + serviceName + "'").join('|');
330
+ const appServices = rootServices.map((service) => resolveManifestService(service, 'app'));
331
+ const routerPluginServices = routerPlugins.map((service) => resolveManifestService(service, 'Router.plugins'));
332
+
333
+ writeIfChanged(
334
+ path.join(app.paths.client.generated, 'services.d.ts'),
335
+ `declare type ${appClassIdentifier} = import("@/server/index").default;
336
+
337
+ declare module "@app" {
338
+
339
+ import { ${appClassIdentifier} as ${appClassIdentifier}Client } from "@/client";
340
+ import ${appClassIdentifier}Server from "@/server/index";
341
+
342
+ export const Router: ${appClassIdentifier}Client['Router'];
343
+
344
+ ${rootServices
345
+ .map((service) =>
346
+ service.registeredName !== 'Router'
347
+ ? `export const ${service.registeredName}: ${appClassIdentifier}Server["${service.registeredName}"];`
348
+ : '',
349
+ )
350
+ .join('\n')}
351
+
352
+ }
353
+
354
+ declare module '@models/types' {
355
+ export * from '@generated/client/models';
356
+ }
357
+
358
+ declare module '@common/errors' {
359
+
360
+ export * from '@common/errors/index';
361
+ export { default } from '@common/errors/index';
362
+
363
+ export const AuthRequired: typeof import('@common/errors/index').AuthRequired<FeatureKeys>;
364
+ export type AuthRequired = import('@common/errors/index').AuthRequired<FeatureKeys>;
365
+
366
+ export const UpgradeRequired: typeof import('@common/errors/index').UpgradeRequired<FeatureKeys>;
367
+ export type UpgradeRequired = import('@common/errors/index').UpgradeRequired<FeatureKeys>;
368
+ }
369
+
370
+ declare namespace preact.JSX {
371
+ interface HTMLAttributes {
372
+ src?: string;
373
+ }
374
+ }
375
+ `,
376
+ );
377
+
378
+ writeIfChanged(
379
+ path.join(app.paths.client.generated, 'models.ts'),
380
+ `export * from '@/var/prisma/browser';
381
+ `,
382
+ );
383
+
384
+ writeIfChanged(
385
+ path.join(app.paths.client.generated, 'context.ts'),
386
+ `// TODO: move it into core (but how to make sure usecontext returns ${appClassIdentifier}'s context ?)
387
+ import React from 'react';
388
+
389
+ import type ${appClassIdentifier}Client from '@/client/index';
390
+
391
+ export type ClientContext = ${appClassIdentifier}Client["Router"]["context"];
392
+
393
+ export const ReactClientContext = React.createContext<ClientContext>({} as ClientContext);
394
+ export default (): ClientContext => React.useContext<ClientContext>(ReactClientContext);`,
395
+ );
396
+
397
+ writeIfChanged(
398
+ path.join(app.paths.common.generated, 'services.d.ts'),
399
+ `declare type ${appClassIdentifier} = import("@/server/index").default;
400
+
401
+ declare module '@models/types' {
402
+ export * from '@generated/common/models';
403
+ }`,
404
+ );
405
+
406
+ writeIfChanged(
407
+ path.join(app.paths.common.generated, 'models.ts'),
408
+ `export * from '@/var/prisma/browser';
409
+ `,
410
+ );
411
+
412
+ fs.removeSync(path.join(app.paths.server.generated, 'app.ts'));
413
+
414
+ writeIfChanged(
415
+ path.join(app.paths.server.generated, 'models.ts'),
416
+ `export * from '@/var/prisma/client';
417
+ `,
418
+ );
419
+
420
+ writeIfChanged(
421
+ path.join(app.paths.server.generated, 'services.d.ts'),
422
+ `type InstalledServices = import("@server/app").RootServicesOf<import("@/server/index").default>;
423
+
424
+ declare type ${appClassIdentifier} = import("@/server/index").default;
425
+
426
+ declare module "@app" {
427
+
428
+ import { ApplicationContainer } from '@server/app/container';
429
+
430
+ const ServerServices: (
431
+ Pick<
432
+ ApplicationContainer<InstalledServices>,
433
+ ${containerServices}
434
+ >
435
+ &
436
+ ${appClassIdentifier}
437
+ )
438
+
439
+ export = ServerServices
440
+ }
441
+
442
+ declare module '@server/app' {
443
+
444
+ import { Application } from "@server/app";
445
+ import { Environment } from "@server/app";
446
+ import { ServicesContainer } from "@server/app/service/container";
447
+
448
+ abstract class ApplicationWithServices extends Application<
449
+ ServicesContainer<InstalledServices>
450
+ > {}
451
+
452
+ export interface Exported {
453
+ Application: typeof ApplicationWithServices,
454
+ Environment: Environment,
455
+ }
456
+
457
+ const foo: Exported;
458
+
459
+ export = foo;
460
+ }
461
+
462
+ declare module '@common/errors' {
463
+
464
+ export * from '@common/errors/index';
465
+ export { default } from '@common/errors/index';
466
+
467
+ export const AuthRequired: typeof import('@common/errors/index').AuthRequired<FeatureKeys>;
468
+ export type AuthRequired = import('@common/errors/index').AuthRequired<FeatureKeys>;
469
+
470
+ export const UpgradeRequired: typeof import('@common/errors/index').UpgradeRequired<FeatureKeys>;
471
+ export type UpgradeRequired = import('@common/errors/index').UpgradeRequired<FeatureKeys>;
472
+ }
473
+
474
+ declare module '@models/types' {
475
+ export * from '@generated/server/models';
476
+ }`,
477
+ );
478
+
479
+ return { app: appServices, routerPlugins: routerPluginServices };
480
+ };
@@ -0,0 +1,12 @@
1
+ import path from 'path';
2
+
3
+ export const normalizePath = (value: string) => value.replace(/\\/g, '/');
4
+
5
+ export const normalizeAbsolutePath = (value: string) => normalizePath(path.resolve(value));
6
+
7
+ export const getGeneratedImportPath = (fromDir: string, targetFile: string) => {
8
+ const relativeImportPath = path.relative(fromDir, targetFile).replace(/\\/g, '/');
9
+ const normalizedImportPath = relativeImportPath.startsWith('.') ? relativeImportPath : './' + relativeImportPath;
10
+
11
+ return normalizedImportPath.replace(/\.(ts|tsx|js|jsx)$/, '');
12
+ };
@@ -6,6 +6,7 @@ import fs from 'fs-extra';
6
6
 
7
7
  // Type
8
8
  import type { App } from '../../app';
9
+ import { logVerbose } from '../../runtime/verbose';
9
10
 
10
11
  export default async (app: App, outputDir: string, enabled: boolean = true) => {
11
12
  if (!enabled) return;
@@ -23,7 +24,7 @@ export default async (app: App, outputDir: string, enabled: boolean = true) => {
23
24
  )
24
25
  return;
25
26
 
26
- console.info(`Generating identity assets ...`);
27
+ logVerbose(`Generating identity assets ...`);
27
28
  fs.emptyDirSync(outputDir);
28
29
 
29
30
  const response = await favicons(logoPath, options);
@@ -14,6 +14,7 @@ import { createClientBundleAnalysisPlugins } from '../common/bundleAnalysis';
14
14
  import { toRspackAliases } from '../common/rspackAliases';
15
15
  import identityAssets from './identite';
16
16
  import cli from '../..';
17
+ import { logVerbose } from '../../runtime/verbose';
17
18
 
18
19
  // Type
19
20
  import type { App } from '../../app';
@@ -54,7 +55,7 @@ export default function createCompiler(
54
55
  mode: TCompileMode,
55
56
  outputTarget: TCompileOutputTarget = mode === 'dev' ? 'dev' : 'bin',
56
57
  ): Configuration {
57
- console.info(`Creating compiler for client (${mode}).`);
58
+ logVerbose(`Creating compiler for client (${mode}).`);
58
59
  const dev = mode === 'dev';
59
60
  const outputPath = app.outputPath(outputTarget);
60
61
 
@@ -133,12 +134,15 @@ export default function createCompiler(
133
134
  include: [
134
135
  app.paths.root + '/client',
135
136
  cli.paths.core.root + '/client',
137
+ app.paths.client.generated,
136
138
 
137
139
  app.paths.root + '/common',
138
140
  cli.paths.core.root + '/common',
141
+ app.paths.common.generated,
139
142
 
140
143
  app.paths.root + '/server',
141
144
  cli.paths.core.root + '/server',
145
+ app.paths.server.generated,
142
146
  ],
143
147
  loader: path.join(
144
148
  cli.paths.core.root,
@@ -154,11 +158,16 @@ export default function createCompiler(
154
158
  include: [
155
159
  app.paths.root + '/client',
156
160
  cli.paths.core.root + '/client',
161
+ app.paths.client.generated,
157
162
 
158
163
  app.paths.root + '/common',
159
164
  cli.paths.core.root + '/common',
165
+ app.paths.common.generated,
160
166
 
161
- app.paths.root + '/server/.generated/models.ts',
167
+ // Prisma 7 generates browser-safe TypeScript entrypoints under var/prisma.
168
+ app.paths.root + '/var/prisma',
169
+
170
+ app.paths.server.generated + '/models.ts',
162
171
  ],
163
172
  rules: require('../common/scripts')({ app, side: 'client', dev }),
164
173
  },
@@ -190,7 +199,8 @@ export default function createCompiler(
190
199
  plugins: [
191
200
  ...(commonConfig.plugins || []),
192
201
 
193
- ...(dev ? [] : [new rspack.CssExtractRspackPlugin({})]),
202
+ // Extract CSS in dev too so SSR emits the same stylesheet links as production.
203
+ new rspack.CssExtractRspackPlugin({}),
194
204
 
195
205
  ...createClientBundleAnalysisPlugins(app, outputTarget),
196
206
  ],
@@ -11,7 +11,14 @@ import ts from 'typescript';
11
11
  - TYPES
12
12
  ----------------------------------*/
13
13
 
14
- export type TControllerMethodMeta = { name: string; inputCallsCount: number; routePath: string };
14
+ export type TControllerSourceLocation = { line: number; column: number };
15
+
16
+ export type TControllerMethodMeta = {
17
+ name: string;
18
+ inputCallsCount: number;
19
+ routePath: string;
20
+ sourceLocation: TControllerSourceLocation;
21
+ };
15
22
 
16
23
  export type TControllerFileMeta = {
17
24
  importPath: string;
@@ -21,9 +28,7 @@ export type TControllerFileMeta = {
21
28
  methods: TControllerMethodMeta[];
22
29
  };
23
30
 
24
- export type TControllerServiceRoot = { alias: string; dir: string };
25
-
26
- type TControllerSearchDir = { importPrefix: string; root: string; serviceRoots?: TControllerServiceRoot[] };
31
+ type TControllerSearchDir = { importPrefix: string; root: string };
27
32
 
28
33
  /*----------------------------------
29
34
  - HELPERS
@@ -31,35 +36,19 @@ type TControllerSearchDir = { importPrefix: string; root: string; serviceRoots?:
31
36
 
32
37
  const getControllerSegments = (relativePath: string) => {
33
38
  const segments = relativePath
34
- .replace(/\.controller\.ts$/, '')
39
+ .replace(/\.ts$/, '')
35
40
  .split('/')
36
41
  .filter(Boolean);
37
42
 
38
- if (segments.length > 1 && segments[segments.length - 1] === segments[segments.length - 2]) {
43
+ if (segments[segments.length - 1] === 'index') {
39
44
  segments.pop();
40
45
  }
41
46
 
42
47
  return segments;
43
48
  };
44
49
 
45
- const getControllerBasePathFromFilepath = (filepath: string, root: string, serviceRoots: TControllerServiceRoot[] = []) => {
46
- const normalizedFilepath = filepath.replace(/\\/g, '/');
47
- const serviceRoot = serviceRoots
48
- .filter((candidate) => normalizedFilepath.startsWith(candidate.dir.replace(/\\/g, '/') + '/'))
49
- .sort((a, b) => b.dir.length - a.dir.length)[0];
50
-
51
- if (!serviceRoot) {
52
- return getControllerSegments(path.relative(root, filepath).replace(/\\/g, '/')).join('/');
53
- }
54
-
55
- const segments = getControllerSegments(path.relative(serviceRoot.dir, filepath).replace(/\\/g, '/'));
56
-
57
- if (segments[0]?.toLowerCase() === serviceRoot.alias.toLowerCase()) {
58
- segments.shift();
59
- }
60
-
61
- return [serviceRoot.alias, ...segments].filter(Boolean).join('/');
62
- };
50
+ const getControllerBasePathFromFilepath = (filepath: string, root: string) =>
51
+ getControllerSegments(path.relative(root, filepath).replace(/\\/g, '/')).join('/');
63
52
 
64
53
  const getGeneratedClassName = (filepath: string) => {
65
54
  const filename = path.basename(filepath, '.ts').replace(/[^A-Za-z0-9_$]+/g, '_');
@@ -85,7 +74,8 @@ const findControllerFiles = (dir: string): string[] => {
85
74
  }
86
75
 
87
76
  if (!dirent.isFile()) continue;
88
- if (!dirent.name.endsWith('.controller.ts')) continue;
77
+ if (!dirent.name.endsWith('.ts')) continue;
78
+ if (dirent.name.endsWith('.d.ts')) continue;
89
79
 
90
80
  files.push(filepath);
91
81
  }
@@ -102,6 +92,12 @@ const parseSourceFile = (filepath: string, code: string) =>
102
92
  filepath.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
103
93
  );
104
94
 
95
+ const getNodeLocation = (sourceFile: ts.SourceFile, node: ts.Node): TControllerSourceLocation => {
96
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
97
+
98
+ return { line: line + 1, column: character + 1 };
99
+ };
100
+
105
101
  const hasModifier = (node: ts.Node, kind: ts.SyntaxKind) =>
106
102
  !!node.modifiers?.some((modifier) => modifier.kind === kind);
107
103
 
@@ -187,9 +183,7 @@ export const indexControllers = (searchDirs: TControllerSearchDir[]) => {
187
183
  if (!defaultClass) continue;
188
184
 
189
185
  const className = defaultClass.name?.text || getGeneratedClassName(filepath);
190
- const routeBasePath =
191
- controllerPathOverride ||
192
- getControllerBasePathFromFilepath(filepath, searchDir.root, searchDir.serviceRoots || []);
186
+ const routeBasePath = controllerPathOverride || getControllerBasePathFromFilepath(filepath, searchDir.root);
193
187
  const methods: TControllerMethodMeta[] = [];
194
188
 
195
189
  for (const member of defaultClass.members) {
@@ -208,6 +202,7 @@ export const indexControllers = (searchDirs: TControllerSearchDir[]) => {
208
202
  name: methodName,
209
203
  inputCallsCount,
210
204
  routePath: [routeBasePath, methodName].filter(Boolean).join('/'),
205
+ sourceLocation: getNodeLocation(sourceFile, member.name),
211
206
  });
212
207
  }
213
208
 
@@ -3,13 +3,12 @@ import { rspack } from '@rspack/core';
3
3
 
4
4
  import type { App } from '../../../app';
5
5
 
6
- module.exports = (app: App, dev: boolean, client: boolean) => {
6
+ module.exports = (app: App, dev: boolean, _client: boolean) => {
7
7
  const enableSourceMaps = dev;
8
- const useStyleLoader = client && dev;
9
8
 
10
9
  return [
11
- // Apply PostCSS plugins including autoprefixer
12
- useStyleLoader ? { loader: 'style-loader' } : { loader: rspack.CssExtractRspackPlugin.loader },
10
+ // Keep CSS delivery identical in dev and prod: extract files so SSR links stylesheets in both modes.
11
+ { loader: rspack.CssExtractRspackPlugin.loader },
13
12
 
14
13
  // Process external/third-party styles
15
14
  { exclude: [app.paths.root], loader: 'css-loader', options: { sourceMap: enableSourceMaps } },