proteum 2.1.3-1 → 2.1.7

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 (95) hide show
  1. package/AGENTS.md +22 -14
  2. package/README.md +109 -17
  3. package/agents/project/AGENTS.md +188 -25
  4. package/agents/project/CODING_STYLE.md +1 -0
  5. package/agents/project/client/AGENTS.md +13 -8
  6. package/agents/project/client/pages/AGENTS.md +17 -9
  7. package/agents/project/diagnostics.md +52 -0
  8. package/agents/project/optimizations.md +48 -0
  9. package/agents/project/server/routes/AGENTS.md +9 -6
  10. package/agents/project/server/services/AGENTS.md +10 -6
  11. package/agents/project/tests/AGENTS.md +11 -5
  12. package/cli/app/config.ts +13 -14
  13. package/cli/app/index.ts +58 -0
  14. package/cli/commands/connect.ts +45 -0
  15. package/cli/commands/dev.ts +37 -13
  16. package/cli/commands/diagnose.ts +286 -0
  17. package/cli/commands/doctor.ts +18 -5
  18. package/cli/commands/explain.ts +25 -0
  19. package/cli/commands/perf.ts +243 -0
  20. package/cli/commands/trace.ts +9 -1
  21. package/cli/commands/verify.ts +281 -0
  22. package/cli/compiler/artifacts/connectedProjects.ts +453 -0
  23. package/cli/compiler/artifacts/controllers.ts +198 -49
  24. package/cli/compiler/artifacts/discovery.ts +0 -34
  25. package/cli/compiler/artifacts/manifest.ts +95 -6
  26. package/cli/compiler/artifacts/routing.ts +2 -2
  27. package/cli/compiler/artifacts/services.ts +277 -130
  28. package/cli/compiler/client/index.ts +3 -0
  29. package/cli/compiler/common/files/style.ts +52 -0
  30. package/cli/compiler/common/generatedRouteModules.ts +34 -5
  31. package/cli/compiler/common/scripts.ts +11 -5
  32. package/cli/compiler/index.ts +2 -1
  33. package/cli/compiler/server/index.ts +3 -0
  34. package/cli/presentation/commands.ts +110 -7
  35. package/cli/presentation/devSession.ts +32 -7
  36. package/cli/runtime/commands.ts +165 -6
  37. package/cli/scaffold/index.ts +18 -27
  38. package/cli/scaffold/templates.ts +48 -28
  39. package/cli/utils/agents.ts +106 -13
  40. package/cli/utils/keyboard.ts +8 -0
  41. package/client/dev/profiler/ApexChart.tsx +66 -0
  42. package/client/dev/profiler/index.tsx +2508 -302
  43. package/client/dev/profiler/runtime.noop.ts +12 -0
  44. package/client/dev/profiler/runtime.ts +195 -4
  45. package/client/services/router/request/api.ts +6 -1
  46. package/common/applicationConfig.ts +173 -0
  47. package/common/applicationConfigLoader.ts +102 -0
  48. package/common/connectedProjects.ts +113 -0
  49. package/common/dev/connect.ts +267 -0
  50. package/common/dev/console.ts +31 -0
  51. package/common/dev/contractsDoctor.ts +128 -0
  52. package/common/dev/diagnostics.ts +59 -15
  53. package/common/dev/inspection.ts +491 -0
  54. package/common/dev/performance.ts +809 -0
  55. package/common/dev/profiler.ts +3 -0
  56. package/common/dev/proteumManifest.ts +31 -6
  57. package/common/dev/requestTrace.ts +52 -1
  58. package/common/env/proteumEnv.ts +176 -50
  59. package/common/router/index.ts +1 -0
  60. package/common/router/request/api.ts +2 -0
  61. package/config.ts +5 -0
  62. package/docs/dev-commands.md +5 -1
  63. package/docs/dev-sessions.md +90 -0
  64. package/docs/diagnostics.md +74 -11
  65. package/docs/request-tracing.md +50 -3
  66. package/package.json +1 -1
  67. package/server/app/container/config.ts +16 -87
  68. package/server/app/container/console/index.ts +42 -8
  69. package/server/app/container/index.ts +10 -2
  70. package/server/app/container/trace/index.ts +105 -0
  71. package/server/app/devDiagnostics.ts +138 -0
  72. package/server/app/index.ts +18 -8
  73. package/server/app/service/container.ts +0 -12
  74. package/server/app/service/index.ts +0 -2
  75. package/server/services/prisma/index.ts +121 -4
  76. package/server/services/router/http/index.ts +305 -11
  77. package/server/services/router/index.ts +116 -57
  78. package/server/services/router/request/api.ts +160 -19
  79. package/server/services/router/request/index.ts +8 -0
  80. package/server/services/router/response/index.ts +23 -1
  81. package/server/services/router/response/page/document.tsx +31 -14
  82. package/server/services/router/response/page/index.tsx +10 -0
  83. package/agents/framework/AGENTS.md +0 -177
  84. package/server/services/auth/router/service.json +0 -6
  85. package/server/services/auth/service.json +0 -6
  86. package/server/services/cron/service.json +0 -6
  87. package/server/services/disks/drivers/local/service.json +0 -6
  88. package/server/services/disks/drivers/s3/service.json +0 -6
  89. package/server/services/disks/service.json +0 -6
  90. package/server/services/fetch/service.json +0 -7
  91. package/server/services/prisma/service.json +0 -6
  92. package/server/services/router/service.json +0 -6
  93. package/server/services/schema/router/service.json +0 -6
  94. package/server/services/schema/service.json +0 -6
  95. package/server/services/security/encrypt/aes/service.json +0 -6
@@ -6,25 +6,28 @@ import app from '../../app';
6
6
  import cli from '../..';
7
7
  import { TProteumManifestScope, TProteumManifestService } from '../common/proteumManifest';
8
8
  import writeIfChanged from '../writeIfChanged';
9
- import { findServiceDirectories } from './discovery';
10
9
  import { normalizeAbsolutePath } from './shared';
11
10
 
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
- };
11
+ type TImportedBinding =
12
+ | {
13
+ kind: 'default' | 'named';
14
+ importPath: string;
15
+ localName: string;
16
+ exportedName: string;
17
+ }
18
+ | {
19
+ kind: 'namespace';
20
+ importPath: string;
21
+ localName: string;
22
+ };
23
23
 
24
24
  type TParsedService = {
25
- registeredName: string;
25
+ className: string;
26
+ importPath: string;
26
27
  priority: number;
27
- meta: TServiceMetas;
28
+ registeredName: string;
29
+ scope: TProteumManifestScope;
30
+ sourceFilepath?: string;
28
31
  };
29
32
 
30
33
  type TParsedAppBootstrap = {
@@ -32,51 +35,28 @@ type TParsedAppBootstrap = {
32
35
  routerPlugins: TParsedService[];
33
36
  };
34
37
 
35
- type TServicesAvailable = Record<string, TServiceMetas>;
36
38
  type TCommandServiceStubSource = {
37
39
  aliasImportPath: string;
38
40
  filepath: string;
39
41
  };
42
+
40
43
  type TGeneratedCommandServiceStubs = {
41
44
  declarations: string;
42
45
  typeNamesByAliasImportPath: Map<string, string>;
43
46
  };
44
47
 
45
- const buildServicesAvailable = (): TServicesAvailable => {
46
- const searchDirs = [
47
- { path: '@server/services/', priority: -1, root: path.join(cli.paths.core.root, 'server', 'services') },
48
- { path: '@/server/services/', priority: 0, root: path.join(app.paths.root, 'server', 'services') },
49
- ];
48
+ type TResolvedImportSource = {
49
+ scope?: TProteumManifestScope;
50
+ sourceFilepath?: string;
51
+ };
50
52
 
51
- const servicesAvailable: TServicesAvailable = {};
52
-
53
- for (const searchDir of searchDirs) {
54
- const services = findServiceDirectories(searchDir.root);
55
-
56
- for (const serviceDir of services) {
57
- const metasFile = path.join(serviceDir, 'service.json');
58
- const importationPath = searchDir.path + serviceDir.substring(searchDir.root.length + 1);
59
- const serviceMetas = fs.readJsonSync(metasFile) as {
60
- id: string;
61
- name: string;
62
- parent: string;
63
- dependences: string[];
64
- priority?: number;
65
- };
66
-
67
- servicesAvailable[serviceMetas.id] = {
68
- importationPath,
69
- priority: searchDir.priority,
70
- sourceDir: normalizeAbsolutePath(serviceDir),
71
- metasFilepath: normalizeAbsolutePath(metasFile),
72
- scope: searchDir.path.startsWith('@server/services/') ? 'framework' : 'app',
73
- ...serviceMetas,
74
- };
75
- }
76
- }
53
+ const coreServicesRoot = normalizeAbsolutePath(path.join(cli.paths.core.root, 'server', 'services'));
54
+ const appServicesRoot = normalizeAbsolutePath(path.join(app.paths.root, 'server', 'services'));
55
+ const coreServerRoot = normalizeAbsolutePath(path.join(cli.paths.core.root, 'server'));
56
+ const appServerRoot = normalizeAbsolutePath(path.join(app.paths.root, 'server'));
77
57
 
78
- return servicesAvailable;
79
- };
58
+ const moduleSourceCache = new Map<string, ts.SourceFile>();
59
+ const exportExpressionCache = new Map<string, ts.Expression | undefined>();
80
60
 
81
61
  const getAppServerEntryFilepath = () => {
82
62
  const filepath = app.paths.server.entry;
@@ -88,15 +68,23 @@ const getAppServerEntryFilepath = () => {
88
68
  return filepath;
89
69
  };
90
70
 
91
- const createSourceFile = (filepath: string) =>
92
- ts.createSourceFile(
93
- filepath,
94
- fs.readFileSync(filepath, 'utf8'),
71
+ const createSourceFile = (filepath: string) => {
72
+ const normalizedFilepath = normalizeAbsolutePath(filepath);
73
+ const existing = moduleSourceCache.get(normalizedFilepath);
74
+ if (existing) return existing;
75
+
76
+ const sourceFile = ts.createSourceFile(
77
+ normalizedFilepath,
78
+ fs.readFileSync(normalizedFilepath, 'utf8'),
95
79
  ts.ScriptTarget.Latest,
96
80
  true,
97
- filepath.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
81
+ normalizedFilepath.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
98
82
  );
99
83
 
84
+ moduleSourceCache.set(normalizedFilepath, sourceFile);
85
+ return sourceFile;
86
+ };
87
+
100
88
  const hasModifier = (node: ts.Node, kind: ts.SyntaxKind) => {
101
89
  const modifiers = (node as ts.Node & { modifiers?: ts.NodeArray<ts.Modifier> }).modifiers;
102
90
 
@@ -141,7 +129,7 @@ const getDefaultExportClassDeclaration = (sourceFile: ts.SourceFile) => {
141
129
  };
142
130
 
143
131
  const buildImportIndex = (sourceFile: ts.SourceFile) => {
144
- const imports = new Map<string, string>();
132
+ const imports = new Map<string, TImportedBinding>();
145
133
 
146
134
  for (const statement of sourceFile.statements) {
147
135
  if (!ts.isImportDeclaration(statement)) continue;
@@ -152,14 +140,35 @@ const buildImportIndex = (sourceFile: ts.SourceFile) => {
152
140
  const { importClause } = statement;
153
141
 
154
142
  if (importClause.name) {
155
- imports.set(importClause.name.text, importPath);
143
+ imports.set(importClause.name.text, {
144
+ kind: 'default',
145
+ importPath,
146
+ localName: importClause.name.text,
147
+ exportedName: 'default',
148
+ });
156
149
  }
157
150
 
158
151
  const namedBindings = importClause.namedBindings;
159
- if (!namedBindings || !ts.isNamedImports(namedBindings)) continue;
152
+ if (!namedBindings) continue;
153
+
154
+ if (ts.isNamespaceImport(namedBindings)) {
155
+ imports.set(namedBindings.name.text, {
156
+ kind: 'namespace',
157
+ importPath,
158
+ localName: namedBindings.name.text,
159
+ });
160
+ continue;
161
+ }
162
+
163
+ if (!ts.isNamedImports(namedBindings)) continue;
160
164
 
161
165
  for (const element of namedBindings.elements) {
162
- imports.set(element.name.text, importPath);
166
+ imports.set(element.name.text, {
167
+ kind: 'named',
168
+ importPath,
169
+ localName: element.name.text,
170
+ exportedName: element.propertyName?.text || element.name.text,
171
+ });
163
172
  }
164
173
  }
165
174
 
@@ -183,26 +192,180 @@ const getPropertyNameText = (propertyName: ts.PropertyName | undefined): string
183
192
  return undefined;
184
193
  };
185
194
 
186
- const extractPriorityFromConfig = (configExpression?: ts.Expression) => {
187
- if (!configExpression) return 0;
195
+ const resolveExistingModuleFilepath = (importPath: string) => {
196
+ const candidates = [
197
+ importPath,
198
+ `${importPath}.ts`,
199
+ `${importPath}.tsx`,
200
+ path.join(importPath, 'index.ts'),
201
+ path.join(importPath, 'index.tsx'),
202
+ ];
188
203
 
189
- const expression = unwrapExpression(configExpression);
190
- if (!ts.isObjectLiteralExpression(expression)) return 0;
204
+ for (const candidate of candidates) {
205
+ if (!fs.existsSync(candidate)) continue;
206
+ if (!fs.statSync(candidate).isFile()) continue;
191
207
 
192
- const priorityProperty = expression.properties.find((property) => {
208
+ return normalizeAbsolutePath(candidate);
209
+ }
210
+
211
+ return undefined;
212
+ };
213
+
214
+ const resolveImportSource = (importPath: string, fromFilepath: string): TResolvedImportSource => {
215
+ if (importPath.startsWith('@server/')) {
216
+ return {
217
+ scope: 'framework',
218
+ sourceFilepath: resolveExistingModuleFilepath(path.join(cli.paths.core.root, importPath.slice(1))),
219
+ };
220
+ }
221
+
222
+ if (importPath.startsWith('@/')) {
223
+ return {
224
+ scope: 'app',
225
+ sourceFilepath: resolveExistingModuleFilepath(path.join(app.paths.root, importPath.slice(2))),
226
+ };
227
+ }
228
+
229
+ if (importPath.startsWith('./') || importPath.startsWith('../')) {
230
+ const sourceFilepath = resolveExistingModuleFilepath(path.resolve(path.dirname(fromFilepath), importPath));
231
+ if (!sourceFilepath) return {};
232
+
233
+ if (sourceFilepath === appServerRoot || sourceFilepath.startsWith(`${appServerRoot}/`)) {
234
+ return { scope: 'app', sourceFilepath };
235
+ }
236
+
237
+ if (sourceFilepath === coreServerRoot || sourceFilepath.startsWith(`${coreServerRoot}/`)) {
238
+ return { scope: 'framework', sourceFilepath };
239
+ }
240
+
241
+ return { sourceFilepath };
242
+ }
243
+
244
+ return {};
245
+ };
246
+
247
+ const getObjectLiteralProperty = (expression: ts.ObjectLiteralExpression, propertyName: string) =>
248
+ expression.properties.find((property) => {
193
249
  if (!ts.isPropertyAssignment(property)) return false;
194
250
 
195
- return getPropertyNameText(property.name) === 'priority';
251
+ return getPropertyNameText(property.name) === propertyName;
196
252
  });
197
253
 
198
- if (!priorityProperty || !ts.isPropertyAssignment(priorityProperty)) return 0;
254
+ const readNumericLiteral = (expression: ts.Expression) => {
255
+ const unwrappedExpression = unwrapExpression(expression);
256
+
257
+ if (ts.isNumericLiteral(unwrappedExpression)) return Number(unwrappedExpression.text);
258
+
259
+ if (
260
+ ts.isPrefixUnaryExpression(unwrappedExpression) &&
261
+ unwrappedExpression.operator === ts.SyntaxKind.MinusToken &&
262
+ ts.isNumericLiteral(unwrapExpression(unwrappedExpression.operand))
263
+ ) {
264
+ return -Number((unwrapExpression(unwrappedExpression.operand) as ts.NumericLiteral).text);
265
+ }
266
+
267
+ return undefined;
268
+ };
269
+
270
+ const getExportedExpression = (moduleFilepath: string, exportName: string): ts.Expression | undefined => {
271
+ const cacheKey = `${normalizeAbsolutePath(moduleFilepath)}::${exportName}`;
272
+ if (exportExpressionCache.has(cacheKey)) return exportExpressionCache.get(cacheKey);
273
+
274
+ const sourceFile = createSourceFile(moduleFilepath);
275
+ const namedExports = new Map<string, string>();
276
+
277
+ for (const statement of sourceFile.statements) {
278
+ if (!ts.isExportDeclaration(statement) || !statement.exportClause || !ts.isNamedExports(statement.exportClause)) continue;
279
+
280
+ for (const element of statement.exportClause.elements) {
281
+ const exportedName = element.name.text;
282
+ const localName = element.propertyName?.text || element.name.text;
283
+ namedExports.set(exportedName, localName);
284
+ }
285
+ }
286
+
287
+ const localName = namedExports.get(exportName) || exportName;
288
+
289
+ for (const statement of sourceFile.statements) {
290
+ if (!ts.isVariableStatement(statement) || !hasModifier(statement, ts.SyntaxKind.ExportKeyword)) continue;
291
+
292
+ for (const declaration of statement.declarationList.declarations) {
293
+ if (!ts.isIdentifier(declaration.name) || declaration.name.text !== localName || !declaration.initializer) continue;
294
+
295
+ exportExpressionCache.set(cacheKey, declaration.initializer);
296
+ return declaration.initializer;
297
+ }
298
+ }
299
+
300
+ exportExpressionCache.set(cacheKey, undefined);
301
+ return undefined;
302
+ };
303
+
304
+ const extractPriorityFromConfigExpression = (
305
+ configExpression: ts.Expression | undefined,
306
+ imports: Map<string, TImportedBinding>,
307
+ sourceFilepath: string,
308
+ seen = new Set<string>(),
309
+ ): number => {
310
+ if (!configExpression) return 0;
311
+
312
+ const expression = unwrapExpression(configExpression);
199
313
 
200
- const priorityExpression = unwrapExpression(priorityProperty.initializer);
201
- if (ts.isNumericLiteral(priorityExpression)) return Number(priorityExpression.text);
314
+ if (ts.isObjectLiteralExpression(expression)) {
315
+ const priorityProperty = getObjectLiteralProperty(expression, 'priority');
316
+ if (!priorityProperty || !ts.isPropertyAssignment(priorityProperty)) return 0;
317
+ return readNumericLiteral(priorityProperty.initializer) || 0;
318
+ }
319
+
320
+ if (ts.isCallExpression(expression) && ts.isPropertyAccessExpression(expression.expression)) {
321
+ const callee = expression.expression;
202
322
 
203
- if (ts.isPrefixUnaryExpression(priorityExpression) && priorityExpression.operator === ts.SyntaxKind.MinusToken) {
204
- const operand = unwrapExpression(priorityExpression.operand);
205
- if (ts.isNumericLiteral(operand)) return -Number(operand.text);
323
+ if (
324
+ ts.isIdentifier(callee.expression) &&
325
+ callee.expression.text === 'Services' &&
326
+ callee.name.text === 'config'
327
+ ) {
328
+ return extractPriorityFromConfigExpression(expression.arguments[1], imports, sourceFilepath, seen);
329
+ }
330
+ }
331
+
332
+ if (ts.isIdentifier(expression)) {
333
+ const imported = imports.get(expression.text);
334
+ if (!imported || imported.kind === 'namespace') return 0;
335
+
336
+ const resolved = resolveImportSource(imported.importPath, sourceFilepath);
337
+ if (!resolved.sourceFilepath) return 0;
338
+
339
+ const cacheKey = `${resolved.sourceFilepath}::${imported.exportedName}`;
340
+ if (seen.has(cacheKey)) return 0;
341
+
342
+ seen.add(cacheKey);
343
+ return extractPriorityFromConfigExpression(
344
+ getExportedExpression(resolved.sourceFilepath, imported.exportedName),
345
+ buildImportIndex(createSourceFile(resolved.sourceFilepath)),
346
+ resolved.sourceFilepath,
347
+ seen,
348
+ );
349
+ }
350
+
351
+ if (ts.isPropertyAccessExpression(expression) && ts.isIdentifier(expression.expression)) {
352
+ const imported = imports.get(expression.expression.text);
353
+ if (!imported || imported.kind !== 'namespace') return 0;
354
+
355
+ const resolved = resolveImportSource(imported.importPath, sourceFilepath);
356
+ if (!resolved.sourceFilepath) return 0;
357
+
358
+ const exportName = expression.name.text;
359
+ const cacheKey = `${resolved.sourceFilepath}::${exportName}`;
360
+ if (seen.has(cacheKey)) return 0;
361
+
362
+ seen.add(cacheKey);
363
+ return extractPriorityFromConfigExpression(
364
+ getExportedExpression(resolved.sourceFilepath, exportName),
365
+ buildImportIndex(createSourceFile(resolved.sourceFilepath)),
366
+ resolved.sourceFilepath,
367
+ seen,
368
+ );
206
369
  }
207
370
 
208
371
  return 0;
@@ -210,50 +373,51 @@ const extractPriorityFromConfig = (configExpression?: ts.Expression) => {
210
373
 
211
374
  const resolveImportedService = (
212
375
  expression: ts.LeftHandSideExpression,
213
- imports: Map<string, string>,
214
- servicesAvailable: TServicesAvailable,
376
+ imports: Map<string, TImportedBinding>,
377
+ sourceFilepath: string,
215
378
  ) => {
216
379
  if (!ts.isIdentifier(expression)) return undefined;
217
380
 
218
- const importPath = imports.get(expression.text);
219
- if (!importPath) return undefined;
381
+ const imported = imports.get(expression.text);
382
+ if (!imported || imported.kind === 'namespace') return undefined;
383
+
384
+ const resolved = resolveImportSource(imported.importPath, sourceFilepath);
385
+ if (!resolved.scope) return undefined;
220
386
 
221
- return Object.values(servicesAvailable).find((serviceMeta) => serviceMeta.importationPath === importPath);
387
+ return {
388
+ className: expression.text,
389
+ importPath: imported.importPath,
390
+ scope: resolved.scope,
391
+ sourceFilepath: resolved.sourceFilepath,
392
+ };
222
393
  };
223
394
 
224
395
  const parseServiceInstantiation = (
225
396
  registeredName: string,
226
397
  expression: ts.Expression,
227
- imports: Map<string, string>,
228
- servicesAvailable: TServicesAvailable,
398
+ imports: Map<string, TImportedBinding>,
399
+ sourceFilepath: string,
229
400
  configArgIndex: number,
230
401
  ): TParsedService | undefined => {
231
402
  const unwrappedExpression = unwrapExpression(expression);
232
403
  if (!ts.isNewExpression(unwrappedExpression)) return undefined;
233
404
 
234
- const meta = resolveImportedService(unwrappedExpression.expression, imports, servicesAvailable);
235
- if (!meta) return undefined;
405
+ const resolvedService = resolveImportedService(unwrappedExpression.expression, imports, sourceFilepath);
406
+ if (!resolvedService) return undefined;
236
407
 
237
408
  const configExpression = unwrappedExpression.arguments?.[configArgIndex];
238
409
 
239
410
  return {
411
+ ...resolvedService,
240
412
  registeredName,
241
- priority: extractPriorityFromConfig(configExpression),
242
- meta,
413
+ priority: extractPriorityFromConfigExpression(configExpression, imports, sourceFilepath),
243
414
  };
244
415
  };
245
416
 
246
- const getObjectLiteralProperty = (expression: ts.ObjectLiteralExpression, propertyName: string) =>
247
- expression.properties.find((property) => {
248
- if (!ts.isPropertyAssignment(property)) return false;
249
-
250
- return getPropertyNameText(property.name) === propertyName;
251
- });
252
-
253
417
  const extractRouterPlugins = (
254
418
  routerConfigExpression: ts.Expression | undefined,
255
- imports: Map<string, string>,
256
- servicesAvailable: TServicesAvailable,
419
+ imports: Map<string, TImportedBinding>,
420
+ sourceFilepath: string,
257
421
  ) => {
258
422
  if (!routerConfigExpression) return [];
259
423
 
@@ -274,7 +438,7 @@ const extractRouterPlugins = (
274
438
  const registeredName = getPropertyNameText(property.name);
275
439
  if (!registeredName) continue;
276
440
 
277
- const routerPlugin = parseServiceInstantiation(registeredName, property.initializer, imports, servicesAvailable, 0);
441
+ const routerPlugin = parseServiceInstantiation(registeredName, property.initializer, imports, sourceFilepath, 0);
278
442
  if (!routerPlugin) continue;
279
443
 
280
444
  routerPlugins.push(routerPlugin);
@@ -283,7 +447,7 @@ const extractRouterPlugins = (
283
447
  return routerPlugins;
284
448
  };
285
449
 
286
- const parseAppBootstrap = (servicesAvailable: TServicesAvailable): TParsedAppBootstrap => {
450
+ const parseAppBootstrap = (): TParsedAppBootstrap => {
287
451
  const sourceFile = createSourceFile(getAppServerEntryFilepath());
288
452
  const imports = buildImportIndex(sourceFile);
289
453
  const appClass = getDefaultExportClassDeclaration(sourceFile);
@@ -297,16 +461,16 @@ const parseAppBootstrap = (servicesAvailable: TServicesAvailable): TParsedAppBoo
297
461
  const registeredName = getPropertyNameText(member.name);
298
462
  if (!registeredName) continue;
299
463
 
300
- const rootService = parseServiceInstantiation(registeredName, member.initializer, imports, servicesAvailable, 1);
464
+ const rootService = parseServiceInstantiation(registeredName, member.initializer, imports, sourceFile.fileName, 1);
301
465
  if (!rootService) continue;
302
466
 
303
467
  rootServices.push(rootService);
304
468
 
305
- if (rootService.meta.id === 'Core/Router') {
469
+ if (rootService.importPath === '@server/services/router') {
306
470
  const initializer = unwrapExpression(member.initializer);
307
471
  if (!ts.isNewExpression(initializer)) continue;
308
472
 
309
- routerPlugins = extractRouterPlugins(initializer.arguments?.[1], imports, servicesAvailable);
473
+ routerPlugins = extractRouterPlugins(initializer.arguments?.[1], imports, sourceFile.fileName);
310
474
  }
311
475
  }
312
476
 
@@ -318,23 +482,10 @@ const parseAppBootstrap = (servicesAvailable: TServicesAvailable): TParsedAppBoo
318
482
  };
319
483
 
320
484
  const commandServiceSearchRoots = [
321
- { root: normalizeAbsolutePath(path.join(cli.paths.core.root, 'server', 'services')), prefix: '@server/services/' },
322
- { root: normalizeAbsolutePath(path.join(app.paths.root, 'server', 'services')), prefix: '@/server/services/' },
485
+ { root: coreServicesRoot, prefix: '@server/services/' },
486
+ { root: appServicesRoot, prefix: '@/server/services/' },
323
487
  ];
324
488
 
325
- const resolveExistingModuleFilepath = (importPath: string) => {
326
- const candidates = [importPath, `${importPath}.ts`, `${importPath}.tsx`, path.join(importPath, 'index.ts'), path.join(importPath, 'index.tsx')];
327
-
328
- for (const candidate of candidates) {
329
- if (!fs.existsSync(candidate)) continue;
330
- if (!fs.statSync(candidate).isFile()) continue;
331
-
332
- return normalizeAbsolutePath(candidate);
333
- }
334
-
335
- return undefined;
336
- };
337
-
338
489
  const getCommandServiceAliasFromFilepath = (filepath: string) => {
339
490
  const normalizedFilepath = normalizeAbsolutePath(filepath);
340
491
 
@@ -389,23 +540,21 @@ const isPrivateOrProtectedInstanceMember = (member: ts.ClassElement) =>
389
540
 
390
541
  const getPropertyDeclarationType = (
391
542
  property: ts.PropertyDeclaration,
392
- imports: Map<string, string>,
543
+ imports: Map<string, TImportedBinding>,
393
544
  sourceFilepath: string,
394
545
  getStubTypeName: (source: TCommandServiceStubSource) => string,
395
546
  enqueueStub: (source: TCommandServiceStubSource) => void,
396
547
  ) => {
397
548
  const initializer = property.initializer ? unwrapExpression(property.initializer) : undefined;
398
549
 
399
- if (initializer && ts.isNewExpression(initializer)) {
400
- if (ts.isIdentifier(initializer.expression)) {
401
- const nestedImportPath = imports.get(initializer.expression.text);
402
- if (nestedImportPath) {
403
- const nestedSource = resolveCommandServiceStubSource(nestedImportPath, sourceFilepath);
550
+ if (initializer && ts.isNewExpression(initializer) && ts.isIdentifier(initializer.expression)) {
551
+ const nestedImport = imports.get(initializer.expression.text);
552
+ if (nestedImport && nestedImport.kind !== 'namespace') {
553
+ const nestedSource = resolveCommandServiceStubSource(nestedImport.importPath, sourceFilepath);
404
554
 
405
- if (nestedSource) {
406
- enqueueStub(nestedSource);
407
- return getStubTypeName(nestedSource);
408
- }
555
+ if (nestedSource) {
556
+ enqueueStub(nestedSource);
557
+ return getStubTypeName(nestedSource);
409
558
  }
410
559
  }
411
560
  }
@@ -483,7 +632,7 @@ const createCommandServiceStubDeclarations = (rootServices: TParsedService[]): T
483
632
  };
484
633
 
485
634
  for (const rootService of rootServices) {
486
- const source = resolveCommandServiceStubSource(rootService.meta.importationPath);
635
+ const source = resolveCommandServiceStubSource(rootService.importPath, rootService.sourceFilepath);
487
636
  if (source) enqueueStub(source);
488
637
  }
489
638
 
@@ -564,20 +713,17 @@ ${classMembers.join('\n')}
564
713
 
565
714
  const resolveManifestService = (service: TParsedService, parent: string): TProteumManifestService => ({
566
715
  kind: 'service',
567
- id: service.meta.id,
568
716
  registeredName: service.registeredName,
569
- metaName: service.meta.name,
717
+ className: service.className,
570
718
  parent,
571
- priority: service.priority || service.meta.priority || 0,
572
- importPath: service.meta.importationPath,
573
- sourceDir: service.meta.sourceDir,
574
- metasFilepath: service.meta.metasFilepath,
575
- scope: service.meta.scope,
719
+ priority: service.priority,
720
+ importPath: service.importPath,
721
+ sourceFilepath: service.sourceFilepath,
722
+ scope: service.scope,
576
723
  });
577
724
 
578
725
  export const generateServiceArtifacts = () => {
579
- const servicesAvailable = buildServicesAvailable();
580
- const { rootServices, routerPlugins } = parseAppBootstrap(servicesAvailable);
726
+ const { rootServices, routerPlugins } = parseAppBootstrap();
581
727
  const appClassIdentifier = app.identity.identifier;
582
728
  const containerServices = app.containerServices.map((serviceName) => "'" + serviceName + "'").join('|');
583
729
  const appServices = rootServices.map((service) => resolveManifestService(service, 'app'));
@@ -699,7 +845,8 @@ declare class ${appClassIdentifier} implements import("@server/app/commands").TC
699
845
  Models?: import("@server/app/commands").TCommandApplication["Models"];
700
846
  ${rootServices
701
847
  .map((service) => {
702
- const typeName = commandServiceStubs.typeNamesByAliasImportPath.get(service.meta.importationPath) || 'any';
848
+ const source = resolveCommandServiceStubSource(service.importPath, service.sourceFilepath);
849
+ const typeName = source ? commandServiceStubs.typeNamesByAliasImportPath.get(source.aliasImportPath) || 'any' : 'any';
703
850
 
704
851
  return ` ${service.registeredName}: ${typeName};`;
705
852
  })
@@ -78,6 +78,7 @@ export default function createCompiler(
78
78
  const outputPath = app.outputPath(outputTarget);
79
79
  const installedCoreRoot = path.join(app.paths.root, 'node_modules', 'proteum');
80
80
  const frameworkRoots = [cli.paths.core.root, installedCoreRoot];
81
+ const transpileModuleDirectories = app.transpileModuleDirectories;
81
82
 
82
83
  const commonConfig = createCommonConfig(app, 'client', mode, outputTarget);
83
84
 
@@ -164,6 +165,7 @@ export default function createCompiler(
164
165
  ...frameworkRoots.map((rootPath) => rootPath + '/client'),
165
166
  ...frameworkRoots.map((rootPath) => rootPath + '/common'),
166
167
  ...frameworkRoots.map((rootPath) => rootPath + '/server'),
168
+ ...transpileModuleDirectories,
167
169
  ],
168
170
  loader: path.join(
169
171
  cli.paths.core.root,
@@ -190,6 +192,7 @@ export default function createCompiler(
190
192
  ...frameworkRoots.map((rootPath) => rootPath + '/client'),
191
193
  ...frameworkRoots.map((rootPath) => rootPath + '/common'),
192
194
  ...frameworkRoots.map((rootPath) => rootPath + '/server'),
195
+ ...transpileModuleDirectories,
193
196
  ],
194
197
  rules: require('../common/scripts')({ app, side: 'client', dev }),
195
198
  },
@@ -1,10 +1,58 @@
1
+ import path from 'path';
2
+
1
3
  // Plugons
2
4
  import { rspack } from '@rspack/core';
5
+ import type { Root } from 'postcss';
3
6
 
4
7
  import type { App } from '../../../app';
5
8
 
9
+ const normalizePath = (value: string) => path.resolve(value).replace(/\\/g, '/');
10
+
11
+ const isPathInsideDirectory = (filepath: string, directory: string) =>
12
+ filepath === directory || filepath.startsWith(directory + '/');
13
+
14
+ const createTailwindTranspileSourcesPlugin = (app: App) => {
15
+ const appRoot = normalizePath(app.paths.root);
16
+ const transpileSourceDirectories = Array.from(
17
+ new Set(app.transpileModuleDirectories.map((directory) => normalizePath(directory))),
18
+ );
19
+
20
+ if (transpileSourceDirectories.length === 0) return null;
21
+
22
+ return {
23
+ postcssPlugin: 'proteum-tailwind-transpile-sources',
24
+ Once(root: Root) {
25
+ const sourceFile = root.source?.input?.file ? normalizePath(root.source.input.file) : '';
26
+ if (!sourceFile || !isPathInsideDirectory(sourceFile, appRoot)) return;
27
+
28
+ const sourceDirectory = path.dirname(sourceFile);
29
+ const existingSourceParams = new Set<string>();
30
+
31
+ root.walkAtRules('source', (rule) => {
32
+ existingSourceParams.add(String(rule.params).trim());
33
+ });
34
+
35
+ for (const transpileDirectory of [...transpileSourceDirectories].reverse()) {
36
+ const relativeSourceDirectory = path.relative(sourceDirectory, transpileDirectory).split(path.sep).join('/');
37
+ const sourceParam = JSON.stringify(
38
+ relativeSourceDirectory.startsWith('.') ? relativeSourceDirectory : `./${relativeSourceDirectory}`,
39
+ );
40
+
41
+ if (existingSourceParams.has(sourceParam)) continue;
42
+
43
+ root.prepend({
44
+ name: 'source',
45
+ params: sourceParam,
46
+ type: 'atrule',
47
+ });
48
+ }
49
+ },
50
+ };
51
+ };
52
+
6
53
  module.exports = (app: App, dev: boolean, _client: boolean) => {
7
54
  const enableSourceMaps = dev;
55
+ const tailwindTranspileSourcesPlugin = createTailwindTranspileSourcesPlugin(app);
8
56
 
9
57
  return [
10
58
  // Keep CSS delivery identical in dev and prod: extract files so SSR links stylesheets in both modes.
@@ -29,6 +77,10 @@ module.exports = (app: App, dev: boolean, _client: boolean) => {
29
77
  options: {
30
78
  postcssOptions: {
31
79
  plugins: [
80
+ // Tailwind v4 only scans files that it knows about. When app code imports
81
+ // transpiled local packages, register those package roots explicitly so
82
+ // shared utility classes survive app splits and workspace extraction.
83
+ ...(tailwindTranspileSourcesPlugin ? [tailwindTranspileSourcesPlugin] : []),
32
84
  /* Tailwind V4 */ require('@tailwindcss/postcss')({
33
85
  // Ensure Tailwind scans the application sources even if the build
34
86
  // process is launched from another working directory (e.g. Docker).