proteum 2.1.9 → 2.2.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 (84) hide show
  1. package/.codex/environments/environment.toml +11 -0
  2. package/AGENTS.md +27 -11
  3. package/README.md +30 -11
  4. package/agents/project/AGENTS.md +172 -123
  5. package/agents/project/CODING_STYLE.md +1 -1
  6. package/agents/project/app-root/AGENTS.md +16 -0
  7. package/agents/project/client/AGENTS.md +5 -5
  8. package/agents/project/client/pages/AGENTS.md +13 -13
  9. package/agents/project/diagnostics.md +19 -10
  10. package/agents/project/optimizations.md +5 -6
  11. package/agents/project/root/AGENTS.md +297 -0
  12. package/agents/project/server/routes/AGENTS.md +2 -2
  13. package/agents/project/server/services/AGENTS.md +4 -2
  14. package/agents/project/tests/AGENTS.md +9 -2
  15. package/cli/app/index.ts +31 -7
  16. package/cli/commands/configure.ts +226 -0
  17. package/cli/commands/dev.ts +0 -2
  18. package/cli/commands/diagnose.ts +33 -1
  19. package/cli/commands/explain.ts +1 -1
  20. package/cli/commands/migrate.ts +51 -0
  21. package/cli/commands/orient.ts +169 -0
  22. package/cli/commands/perf.ts +8 -1
  23. package/cli/commands/verify.ts +1003 -49
  24. package/cli/compiler/artifacts/manifest.ts +4 -4
  25. package/cli/compiler/artifacts/routing.ts +2 -2
  26. package/cli/compiler/artifacts/services.ts +12 -3
  27. package/cli/compiler/client/index.ts +65 -19
  28. package/cli/compiler/common/files/style.ts +47 -2
  29. package/cli/compiler/common/generatedRouteModules.ts +31 -38
  30. package/cli/compiler/common/index.ts +10 -0
  31. package/cli/compiler/common/proteumManifest.ts +1 -0
  32. package/cli/compiler/server/index.ts +34 -9
  33. package/cli/context.ts +6 -1
  34. package/cli/index.ts +7 -8
  35. package/cli/migrate/pageContract.ts +516 -0
  36. package/cli/paths.ts +47 -6
  37. package/cli/presentation/commands.ts +100 -10
  38. package/cli/presentation/devSession.ts +4 -6
  39. package/cli/presentation/help.ts +2 -2
  40. package/cli/presentation/ink.ts +10 -5
  41. package/cli/presentation/welcome.ts +2 -4
  42. package/cli/runtime/commands.ts +94 -1
  43. package/cli/scaffold/index.ts +2 -2
  44. package/cli/scaffold/templates.ts +4 -2
  45. package/cli/utils/agents.ts +273 -58
  46. package/client/dev/profiler/index.tsx +3 -2
  47. package/client/router.ts +10 -2
  48. package/client/services/router/index.tsx +6 -22
  49. package/common/dev/connect.ts +20 -4
  50. package/common/dev/console.ts +7 -0
  51. package/common/dev/contractsDoctor.ts +354 -0
  52. package/common/dev/diagnostics.ts +10 -7
  53. package/common/dev/inspection.ts +830 -38
  54. package/common/dev/performance.ts +19 -5
  55. package/common/dev/profiler.ts +1 -0
  56. package/common/dev/proteumManifest.ts +5 -4
  57. package/common/dev/requestTrace.ts +78 -1
  58. package/common/env/proteumEnv.ts +10 -3
  59. package/common/router/contracts.ts +8 -11
  60. package/common/router/index.ts +2 -2
  61. package/common/router/pageData.ts +72 -0
  62. package/common/router/register.ts +10 -46
  63. package/common/router/response/page.ts +28 -16
  64. package/docs/assets/unique-domains-chip.png +0 -0
  65. package/docs/dev-sessions.md +8 -4
  66. package/docs/diagnostics.md +77 -11
  67. package/docs/migrate-from-2.1.3.md +388 -0
  68. package/docs/request-tracing.md +42 -9
  69. package/package.json +6 -1
  70. package/scripts/update-codex-agents.ts +2 -2
  71. package/server/app/container/console/index.ts +11 -1
  72. package/server/app/container/trace/index.ts +370 -72
  73. package/server/app/devDiagnostics.ts +1 -1
  74. package/server/app/index.ts +5 -1
  75. package/server/services/auth/index.ts +9 -0
  76. package/server/services/prisma/index.ts +15 -12
  77. package/server/services/router/http/index.ts +1 -1
  78. package/server/services/router/index.ts +105 -23
  79. package/server/services/router/request/api.ts +7 -1
  80. package/server/services/router/request/index.ts +2 -1
  81. package/server/services/router/response/index.ts +8 -28
  82. package/types/global/vendors.d.ts +12 -0
  83. package/types/vendors.d.ts +12 -0
  84. package/common/router/pageSetup.ts +0 -51
@@ -1,12 +1,16 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import ts from 'typescript';
3
4
 
4
5
  import type { TDoctorResponse } from './diagnostics';
5
6
  import type {
6
7
  TProteumManifest,
7
8
  TProteumManifestDiagnostic,
9
+ TProteumManifestSourceLocation,
8
10
  } from './proteumManifest';
9
11
 
12
+ const normalizeFilepath = (value: string) => value.replace(/\\/g, '/');
13
+
10
14
  const buildGeneratedArtifactList = (manifest: TProteumManifest) => {
11
15
  const appRoot = manifest.app.root;
12
16
  const clientRouteModulesRoot = path.join(appRoot, '.proteum', 'client', 'route-modules');
@@ -58,21 +62,361 @@ const createContractDiagnostic = ({
58
62
  filepath,
59
63
  level = 'error',
60
64
  message,
65
+ sourceLocation,
66
+ fixHint,
61
67
  relatedFilepaths,
62
68
  }: {
63
69
  code: string;
64
70
  filepath: string;
65
71
  level?: TProteumManifestDiagnostic['level'];
66
72
  message: string;
73
+ sourceLocation?: TProteumManifestSourceLocation;
74
+ fixHint?: string;
67
75
  relatedFilepaths?: string[];
68
76
  }): TProteumManifestDiagnostic => ({
69
77
  code,
70
78
  filepath,
71
79
  level,
72
80
  message,
81
+ ...(sourceLocation ? { sourceLocation } : {}),
82
+ ...(fixHint ? { fixHint } : {}),
73
83
  ...(relatedFilepaths && relatedFilepaths.length > 0 ? { relatedFilepaths } : {}),
74
84
  });
75
85
 
86
+ const parseSourceFile = (filepath: string, code: string) =>
87
+ ts.createSourceFile(
88
+ filepath,
89
+ code,
90
+ ts.ScriptTarget.Latest,
91
+ true,
92
+ filepath.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
93
+ );
94
+
95
+ const getNodeLocation = (sourceFile: ts.SourceFile, node: ts.Node): TProteumManifestSourceLocation => {
96
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
97
+ return { line: line + 1, column: character + 1 };
98
+ };
99
+
100
+ const isFunctionLike = (node: ts.Node): node is ts.FunctionLikeDeclaration =>
101
+ ts.isFunctionDeclaration(node) ||
102
+ ts.isMethodDeclaration(node) ||
103
+ ts.isFunctionExpression(node) ||
104
+ ts.isArrowFunction(node);
105
+
106
+ const unwrapExpression = (node: ts.Node): ts.Node => {
107
+ let current = node;
108
+ while (
109
+ ts.isAsExpression(current.parent) ||
110
+ ts.isTypeAssertionExpression(current.parent) ||
111
+ ts.isParenthesizedExpression(current.parent)
112
+ ) {
113
+ current = current.parent;
114
+ }
115
+
116
+ return current;
117
+ };
118
+
119
+ const getFunctionContainer = (node: ts.Node) => {
120
+ let current = node.parent;
121
+
122
+ while (current) {
123
+ if (isFunctionLike(current)) return current;
124
+ current = current.parent;
125
+ }
126
+
127
+ return undefined;
128
+ };
129
+
130
+ const getFunctionName = (node: ts.FunctionLikeDeclaration) => {
131
+ if ('name' in node && node.name && ts.isIdentifier(node.name)) return node.name.text;
132
+
133
+ if (node.parent && ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) return node.parent.name.text;
134
+ if (node.parent && ts.isPropertyAssignment(node.parent) && ts.isIdentifier(node.parent.name)) return node.parent.name.text;
135
+
136
+ return undefined;
137
+ };
138
+
139
+ const isRouterRenderCallback = (node: ts.FunctionLikeDeclaration) => {
140
+ const unwrappedNode = unwrapExpression(node);
141
+ const parent = unwrappedNode.parent;
142
+ if (!ts.isCallExpression(parent)) return false;
143
+ if (!ts.isPropertyAccessExpression(parent.expression)) return false;
144
+ if (!ts.isIdentifier(parent.expression.expression) || parent.expression.expression.text !== 'Router') return false;
145
+
146
+ const methodName = parent.expression.name.text;
147
+ if (methodName !== 'page' && methodName !== 'error') return false;
148
+
149
+ const functionArguments = parent.arguments.filter((argument) => isFunctionLike(unwrapExpression(argument)));
150
+ const lastFunctionArgument = functionArguments[functionArguments.length - 1];
151
+
152
+ return lastFunctionArgument === unwrappedNode;
153
+ };
154
+
155
+ const isValidHookContainer = (node: ts.FunctionLikeDeclaration) => {
156
+ if (isRouterRenderCallback(node)) return true;
157
+
158
+ const functionName = getFunctionName(node);
159
+ if (!functionName) return false;
160
+ if (functionName.startsWith('use')) return true;
161
+
162
+ const firstCharacter = functionName[0];
163
+ return firstCharacter === firstCharacter.toUpperCase();
164
+ };
165
+
166
+ const resolveClientRelatedFilepath = ({
167
+ appRoot,
168
+ filepath,
169
+ moduleSpecifier,
170
+ }: {
171
+ appRoot: string;
172
+ filepath: string;
173
+ moduleSpecifier: string;
174
+ }) => {
175
+ if (moduleSpecifier === '@/client/context' || moduleSpecifier === '@generated/client/context') {
176
+ return path.join(appRoot, '.proteum', 'client', 'context.ts');
177
+ }
178
+
179
+ if (moduleSpecifier.startsWith('@/client/')) {
180
+ return path.join(appRoot, moduleSpecifier.slice(2));
181
+ }
182
+
183
+ if (moduleSpecifier.startsWith('@generated/client/')) {
184
+ return path.join(appRoot, '.proteum', 'client', moduleSpecifier.slice('@generated/client/'.length));
185
+ }
186
+
187
+ if (moduleSpecifier.startsWith('.') || moduleSpecifier.startsWith('/')) {
188
+ return path.resolve(path.dirname(filepath), moduleSpecifier);
189
+ }
190
+
191
+ return undefined;
192
+ };
193
+
194
+ const isSsrRuntimeFile = (manifest: TProteumManifest, filepath: string) => {
195
+ const relativeFilepath = normalizeFilepath(path.relative(manifest.app.root, filepath));
196
+ return relativeFilepath.startsWith('server/') || relativeFilepath.startsWith('commands/') || relativeFilepath.includes('.ssr.');
197
+ };
198
+
199
+ const addUniqueDiagnostic = (diagnostics: TProteumManifestDiagnostic[], diagnostic: TProteumManifestDiagnostic) => {
200
+ const key = `${diagnostic.code}:${diagnostic.filepath}:${diagnostic.sourceLocation?.line || 0}:${diagnostic.sourceLocation?.column || 0}:${diagnostic.message}`;
201
+ if (
202
+ diagnostics.some(
203
+ (existing) =>
204
+ `${existing.code}:${existing.filepath}:${existing.sourceLocation?.line || 0}:${existing.sourceLocation?.column || 0}:${existing.message}` ===
205
+ key,
206
+ )
207
+ ) {
208
+ return;
209
+ }
210
+
211
+ diagnostics.push(diagnostic);
212
+ };
213
+
214
+ const buildHookContractDiagnostics = (manifest: TProteumManifest, sourceFilepaths: string[]) => {
215
+ const diagnostics: TProteumManifestDiagnostic[] = [];
216
+
217
+ for (const filepath of sourceFilepaths) {
218
+ if (!fs.existsSync(filepath)) continue;
219
+ if (!filepath.endsWith('.ts') && !filepath.endsWith('.tsx')) continue;
220
+
221
+ const code = fs.readFileSync(filepath, 'utf8');
222
+ const sourceFile = parseSourceFile(filepath, code);
223
+ const imports = new Map<
224
+ string,
225
+ { kind: 'client-hook' | 'router-context'; relatedFilepath?: string; moduleSpecifier: string }
226
+ >();
227
+
228
+ for (const statement of sourceFile.statements) {
229
+ if (!ts.isImportDeclaration(statement) || !statement.importClause) continue;
230
+ if (!ts.isStringLiteral(statement.moduleSpecifier)) continue;
231
+
232
+ const moduleSpecifier = statement.moduleSpecifier.text;
233
+ const relatedFilepath = resolveClientRelatedFilepath({
234
+ appRoot: manifest.app.root,
235
+ filepath,
236
+ moduleSpecifier,
237
+ });
238
+ const normalizedRelatedFilepath = relatedFilepath ? normalizeFilepath(relatedFilepath) : undefined;
239
+ const normalizedAppRoot = normalizeFilepath(manifest.app.root);
240
+ const isKnownClientHookModule =
241
+ moduleSpecifier === '@/client/context' ||
242
+ moduleSpecifier === '@generated/client/context' ||
243
+ moduleSpecifier.startsWith('@/client/') ||
244
+ moduleSpecifier.startsWith('@generated/client/') ||
245
+ normalizedRelatedFilepath?.startsWith(`${normalizedAppRoot}/client/`) === true ||
246
+ normalizedRelatedFilepath?.startsWith(`${normalizedAppRoot}/.proteum/client/`) === true;
247
+ if (!isKnownClientHookModule) continue;
248
+
249
+ const isRouterContextImport =
250
+ moduleSpecifier === '@/client/context' ||
251
+ moduleSpecifier === '@generated/client/context' ||
252
+ moduleSpecifier.endsWith('/client/context');
253
+
254
+ if (statement.importClause.name) {
255
+ imports.set(statement.importClause.name.text, {
256
+ kind: isRouterContextImport ? 'router-context' : 'client-hook',
257
+ relatedFilepath,
258
+ moduleSpecifier,
259
+ });
260
+ }
261
+
262
+ if (!statement.importClause.namedBindings || !ts.isNamedImports(statement.importClause.namedBindings)) continue;
263
+ for (const element of statement.importClause.namedBindings.elements) {
264
+ const localName = element.name.text;
265
+ const importedName = element.propertyName?.text || localName;
266
+ if (!importedName.startsWith('use') && !isRouterContextImport) continue;
267
+
268
+ imports.set(localName, {
269
+ kind: isRouterContextImport ? 'router-context' : 'client-hook',
270
+ relatedFilepath,
271
+ moduleSpecifier,
272
+ });
273
+ }
274
+ }
275
+
276
+ if (imports.size === 0) continue;
277
+
278
+ const ssrRuntimeFile = isSsrRuntimeFile(manifest, filepath);
279
+ const visit = (node: ts.Node) => {
280
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
281
+ const importedHook = imports.get(node.expression.text);
282
+ if (importedHook) {
283
+ const sourceLocation = getNodeLocation(sourceFile, node.expression);
284
+ const relatedFilepaths = importedHook.relatedFilepath ? [importedHook.relatedFilepath] : [];
285
+
286
+ if (ssrRuntimeFile) {
287
+ addUniqueDiagnostic(
288
+ diagnostics,
289
+ createContractDiagnostic({
290
+ code: 'runtime/client-only-hook-in-ssr',
291
+ filepath,
292
+ message: `Client hook "${node.expression.text}" is referenced from SSR-only or server-side runtime code.`,
293
+ sourceLocation,
294
+ fixHint:
295
+ 'Move the hook usage to a client-owned component or split the file so SSR code passes plain data instead of calling client hooks.',
296
+ relatedFilepaths,
297
+ }),
298
+ );
299
+ } else {
300
+ const container = getFunctionContainer(node);
301
+ if (!container || !isValidHookContainer(container)) {
302
+ addUniqueDiagnostic(
303
+ diagnostics,
304
+ createContractDiagnostic({
305
+ code:
306
+ importedHook.kind === 'router-context'
307
+ ? 'runtime/router-context-outside-router'
308
+ : 'runtime/provider-hook-outside-provider',
309
+ filepath,
310
+ message:
311
+ importedHook.kind === 'router-context'
312
+ ? `Router context hook "${node.expression.text}" is called outside Router-owned render execution.`
313
+ : `Provider-dependent hook "${node.expression.text}" is called outside a valid provider/render boundary.`,
314
+ sourceLocation,
315
+ fixHint:
316
+ importedHook.kind === 'router-context'
317
+ ? 'Call the hook only inside a Router.page render callback, a component rendered under App, or a custom hook used from that tree.'
318
+ : 'Move the hook back under the provider-managed React tree or pass the required values as explicit props or SSR data.',
319
+ relatedFilepaths,
320
+ }),
321
+ );
322
+ }
323
+ }
324
+ }
325
+ }
326
+
327
+ ts.forEachChild(node, visit);
328
+ };
329
+
330
+ ts.forEachChild(sourceFile, visit);
331
+ }
332
+
333
+ return diagnostics;
334
+ };
335
+
336
+ const findConnectNamespaceLocation = (setupFilepath: string, namespace: string): TProteumManifestSourceLocation | undefined => {
337
+ if (!fs.existsSync(setupFilepath)) return undefined;
338
+
339
+ const sourceFile = parseSourceFile(setupFilepath, fs.readFileSync(setupFilepath, 'utf8'));
340
+ let fallbackLocation: TProteumManifestSourceLocation | undefined;
341
+ let namespaceLocation: TProteumManifestSourceLocation | undefined;
342
+
343
+ const visit = (node: ts.Node) => {
344
+ if (ts.isPropertyAssignment(node)) {
345
+ const name =
346
+ ts.isIdentifier(node.name) || ts.isStringLiteral(node.name) || ts.isNumericLiteral(node.name)
347
+ ? node.name.text
348
+ : undefined;
349
+
350
+ if (name === 'connect' && !fallbackLocation) fallbackLocation = getNodeLocation(sourceFile, node.name);
351
+ if (name === namespace && !namespaceLocation) namespaceLocation = getNodeLocation(sourceFile, node.name);
352
+ }
353
+
354
+ if (!namespaceLocation) ts.forEachChild(node, visit);
355
+ };
356
+
357
+ ts.forEachChild(sourceFile, visit);
358
+ return namespaceLocation || fallbackLocation;
359
+ };
360
+
361
+ const buildConnectedBoundaryDiagnostics = (manifest: TProteumManifest) => {
362
+ const diagnostics: TProteumManifestDiagnostic[] = [];
363
+
364
+ for (const project of manifest.connectedProjects) {
365
+ const issues: string[] = [];
366
+ let hasErrorIssue = false;
367
+
368
+ if (!project.sourceValue) {
369
+ issues.push(`connect.${project.namespace}.source is missing`);
370
+ hasErrorIssue = true;
371
+ }
372
+ if (!project.sourceKind && project.sourceValue) {
373
+ issues.push('the source could not be resolved into a connected contract');
374
+ hasErrorIssue = true;
375
+ }
376
+ if (!project.cachedContractFilepath) {
377
+ issues.push('no cached connected contract filepath was generated');
378
+ hasErrorIssue = true;
379
+ }
380
+ if (project.cachedContractFilepath && !fs.existsSync(project.cachedContractFilepath))
381
+ {
382
+ issues.push('the cached connected contract file is missing on disk');
383
+ hasErrorIssue = true;
384
+ }
385
+ if (!project.urlInternal) {
386
+ issues.push(`connect.${project.namespace}.urlInternal is missing`);
387
+ hasErrorIssue = true;
388
+ }
389
+ if (project.controllerCount === 0) {
390
+ issues.push('zero connected controllers were imported');
391
+ }
392
+
393
+ if (project.sourceKind === 'file' && project.typingMode !== 'local-typed') {
394
+ issues.push(`file-based sources should resolve to local-typed mode, got "${project.typingMode || 'unknown'}"`);
395
+ }
396
+
397
+ if (project.sourceKind && project.sourceKind !== 'file' && project.typingMode === 'local-typed') {
398
+ issues.push(`non-file connected sources should not report local-typed mode`);
399
+ }
400
+
401
+ if (issues.length === 0) continue;
402
+
403
+ addUniqueDiagnostic(
404
+ diagnostics,
405
+ createContractDiagnostic({
406
+ code: 'runtime/connected-boundary-mismatch',
407
+ filepath: manifest.app.setupFilepath,
408
+ level: hasErrorIssue ? 'error' : 'warning',
409
+ message: `Connected namespace "${project.namespace}" has a framework boundary mismatch: ${issues.join('; ')}.`,
410
+ sourceLocation: findConnectNamespaceLocation(manifest.app.setupFilepath, project.namespace) || { line: 1, column: 1 },
411
+ fixHint: `Update connect.${project.namespace} in proteum.config.ts, refresh the connected contract, then re-check both the consumer and producer runtime surfaces.`,
412
+ relatedFilepaths: project.cachedContractFilepath ? [project.cachedContractFilepath] : [],
413
+ }),
414
+ );
415
+ }
416
+
417
+ return diagnostics;
418
+ };
419
+
76
420
  export const buildContractsDoctorResponse = (manifest: TProteumManifest, strict = false): TDoctorResponse => {
77
421
  const diagnostics: TProteumManifestDiagnostic[] = [];
78
422
  const sourceFilepaths = new Set<string>([
@@ -114,6 +458,16 @@ export const buildContractsDoctorResponse = (manifest: TProteumManifest, strict
114
458
  );
115
459
  }
116
460
 
461
+ diagnostics.push(
462
+ ...buildHookContractDiagnostics(
463
+ manifest,
464
+ [...sourceFilepaths].filter((filepath) =>
465
+ normalizeFilepath(path.resolve(filepath)).startsWith(`${normalizeFilepath(path.resolve(manifest.app.root))}/`),
466
+ ),
467
+ ),
468
+ );
469
+ diagnostics.push(...buildConnectedBoundaryDiagnostics(manifest));
470
+
117
471
  const errors = diagnostics.filter((diagnostic) => diagnostic.level === 'error');
118
472
  const warnings = diagnostics.filter((diagnostic) => diagnostic.level === 'warning');
119
473
 
@@ -72,11 +72,11 @@ const formatRouteTarget = (route: TProteumManifestRoute) => {
72
72
 
73
73
  const formatRouteItem = (manifest: TProteumManifest, route: TProteumManifestRoute) => {
74
74
  const chunk = route.chunkId ? ` chunk=${route.chunkId}` : '';
75
- const setup = route.hasSetup ? ' setup=yes' : ' setup=no';
75
+ const data = route.hasData ? ' data=yes' : ' data=no';
76
76
  const options = route.normalizedOptionKeys.length > 0 ? ` options=${route.normalizedOptionKeys.join(',')}` : '';
77
77
  const resolution = route.targetResolution !== 'literal' ? ` resolution=${route.targetResolution}` : '';
78
78
 
79
- return `${route.kind} ${route.methodName} ${formatRouteTarget(route)} [${route.scope}]${chunk}${setup}${options}${resolution} source=${formatManifestFilepath(manifest, route.filepath)}${formatManifestLocation(route.sourceLocation.line, route.sourceLocation.column)}`;
79
+ return `${route.kind} ${route.methodName} ${formatRouteTarget(route)} [${route.scope}]${chunk}${data}${options}${resolution} source=${formatManifestFilepath(manifest, route.filepath)}${formatManifestLocation(route.sourceLocation.line, route.sourceLocation.column)}`;
80
80
  };
81
81
 
82
82
  const formatLayoutItem = (manifest: TProteumManifest, layout: TProteumManifestLayout) =>
@@ -87,8 +87,9 @@ const formatDiagnosticItem = (manifest: TProteumManifest, diagnostic: TProteumMa
87
87
  diagnostic.relatedFilepaths && diagnostic.relatedFilepaths.length > 0
88
88
  ? ` related=${diagnostic.relatedFilepaths.map((filepath) => formatManifestFilepath(manifest, filepath)).join(',')}`
89
89
  : '';
90
+ const fixHint = diagnostic.fixHint ? ` fix=${diagnostic.fixHint}` : '';
90
91
 
91
- return `[${diagnostic.level}] ${diagnostic.code} ${diagnostic.message} source=${formatManifestFilepath(manifest, diagnostic.filepath)}${formatManifestLocation(diagnostic.sourceLocation?.line, diagnostic.sourceLocation?.column)}${related}`;
92
+ return `[${diagnostic.level}] ${diagnostic.code} ${diagnostic.message} source=${formatManifestFilepath(manifest, diagnostic.filepath)}${formatManifestLocation(diagnostic.sourceLocation?.line, diagnostic.sourceLocation?.column)}${related}${fixHint}`;
92
93
  };
93
94
 
94
95
  export const pickExplainManifestSections = (manifest: TProteumManifest, sectionNames: TExplainSectionName[]) => {
@@ -154,8 +155,8 @@ export const buildExplainBlocks = (manifest: TProteumManifest, sectionNames: TEx
154
155
  blocks.push({
155
156
  title: 'Conventions',
156
157
  items: [
157
- `routeSetupOptionKeys=${manifest.conventions.routeSetupOptionKeys.join(', ')}`,
158
- `reservedRouteSetupKeys=${manifest.conventions.reservedRouteSetupKeys.join(', ')}`,
158
+ `routeOptionKeys=${manifest.conventions.routeOptionKeys.join(', ')}`,
159
+ `reservedRouteOptionKeys=${manifest.conventions.reservedRouteOptionKeys.join(', ')}`,
159
160
  ],
160
161
  });
161
162
  continue;
@@ -291,8 +292,9 @@ export const buildDoctorBlocksFromDiagnostics = (
291
292
  diagnostic.relatedFilepaths && diagnostic.relatedFilepaths.length > 0
292
293
  ? ` related=${diagnostic.relatedFilepaths.map((filepath) => formatManifestFilepath(manifest, filepath)).join(',')}`
293
294
  : '';
295
+ const fixHint = diagnostic.fixHint ? ` fix=${diagnostic.fixHint}` : '';
294
296
 
295
- return `${diagnostic.code} ${diagnostic.message} source=${formatManifestFilepath(manifest, diagnostic.filepath)}${formatManifestLocation(diagnostic.sourceLocation?.line, diagnostic.sourceLocation?.column)}${related}`;
297
+ return `${diagnostic.code} ${diagnostic.message} source=${formatManifestFilepath(manifest, diagnostic.filepath)}${formatManifestLocation(diagnostic.sourceLocation?.line, diagnostic.sourceLocation?.column)}${related}${fixHint}`;
296
298
  }),
297
299
  },
298
300
  {
@@ -302,8 +304,9 @@ export const buildDoctorBlocksFromDiagnostics = (
302
304
  diagnostic.relatedFilepaths && diagnostic.relatedFilepaths.length > 0
303
305
  ? ` related=${diagnostic.relatedFilepaths.map((filepath) => formatManifestFilepath(manifest, filepath)).join(',')}`
304
306
  : '';
307
+ const fixHint = diagnostic.fixHint ? ` fix=${diagnostic.fixHint}` : '';
305
308
 
306
- return `${diagnostic.code} ${diagnostic.message} source=${formatManifestFilepath(manifest, diagnostic.filepath)}${formatManifestLocation(diagnostic.sourceLocation?.line, diagnostic.sourceLocation?.column)}${related}`;
309
+ return `${diagnostic.code} ${diagnostic.message} source=${formatManifestFilepath(manifest, diagnostic.filepath)}${formatManifestLocation(diagnostic.sourceLocation?.line, diagnostic.sourceLocation?.column)}${related}${fixHint}`;
307
310
  }),
308
311
  },
309
312
  ];