proteum 2.5.0 → 2.5.2

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 (40) hide show
  1. package/AGENTS.md +2 -2
  2. package/README.md +46 -19
  3. package/agents/project/AGENTS.md +9 -7
  4. package/agents/project/CODING_STYLE.md +1 -1
  5. package/agents/project/client/AGENTS.md +5 -1
  6. package/agents/project/diagnostics.md +1 -1
  7. package/agents/project/root/AGENTS.md +9 -7
  8. package/agents/project/server/services/AGENTS.md +4 -0
  9. package/agents/project/tests/AGENTS.md +1 -1
  10. package/cli/commands/verify.ts +117 -4
  11. package/cli/compiler/artifacts/controllerHelper.ts +66 -0
  12. package/cli/compiler/artifacts/controllers.ts +3 -0
  13. package/cli/compiler/artifacts/services.ts +14 -8
  14. package/cli/compiler/common/generatedRouteModules.ts +270 -53
  15. package/cli/presentation/commands.ts +11 -1
  16. package/cli/runtime/commands.ts +6 -0
  17. package/cli/scaffold/templates.ts +14 -6
  18. package/cli/utils/agents.ts +1 -1
  19. package/cli/verification/changed.ts +460 -0
  20. package/client/app/index.ts +22 -5
  21. package/client/services/router/index.tsx +1 -1
  22. package/client/services/router/request/api.ts +2 -2
  23. package/common/applicationConfig.ts +177 -0
  24. package/common/applicationConfigLoader.ts +33 -1
  25. package/common/dev/contractsDoctor.ts +16 -0
  26. package/config.ts +5 -1
  27. package/docs/migration-2.5.md +269 -0
  28. package/eslint.js +96 -50
  29. package/package.json +1 -1
  30. package/server/app/index.ts +28 -2
  31. package/server/services/router/index.ts +3 -3
  32. package/tests/cli-mcp-command.test.cjs +14 -0
  33. package/tests/client-app-error-handling.test.cjs +100 -0
  34. package/tests/contracts-doctor.test.cjs +98 -0
  35. package/tests/definition-contracts.test.cjs +129 -0
  36. package/tests/dev-transpile-watch.test.cjs +3 -6
  37. package/tests/eslint-rules.test.cjs +246 -7
  38. package/tests/scaffold-templates.test.cjs +43 -0
  39. package/tests/server-app-report-error.test.cjs +135 -0
  40. package/tests/verify-changed.test.cjs +200 -0
package/eslint.js CHANGED
@@ -16,23 +16,24 @@ const createZodTypeFactorySelector = (factoryName) =>
16
16
 
17
17
  const skippedTraversalKeys = new Set(['parent', 'loc', 'range', 'tokens', 'comments']);
18
18
 
19
- const traverseNode = (node, visit, parent = null, parentKey = null) => {
19
+ const traverseNode = (node, visit, parent = null, parentKey = null, ancestors = []) => {
20
20
  if (!node || typeof node !== 'object') return;
21
21
 
22
- visit(node, parent, parentKey);
22
+ visit(node, parent, parentKey, ancestors);
23
23
 
24
24
  for (const key of Object.keys(node)) {
25
25
  if (skippedTraversalKeys.has(key)) continue;
26
26
 
27
27
  const value = node[key];
28
+ const nextAncestors = [...ancestors, { node, childKey: key }];
28
29
  if (Array.isArray(value)) {
29
30
  value.forEach((child) => {
30
- if (child && typeof child.type === 'string') traverseNode(child, visit, node, key);
31
+ if (child && typeof child.type === 'string') traverseNode(child, visit, node, key, nextAncestors);
31
32
  });
32
33
  continue;
33
34
  }
34
35
 
35
- if (value && typeof value.type === 'string') traverseNode(value, visit, node, key);
36
+ if (value && typeof value.type === 'string') traverseNode(value, visit, node, key, nextAncestors);
36
37
  }
37
38
  };
38
39
 
@@ -121,77 +122,122 @@ const getCalleePropertyName = (callee) => {
121
122
  return null;
122
123
  };
123
124
 
124
- const preservingCallNames = new Set([
125
- 'captureError',
126
- 'captureException',
127
- 'consoleError',
128
- 'handleError',
129
- 'logError',
130
- 'onError',
131
- 'reject',
132
- 'reportError',
133
- 'setError',
134
- 'setErrorMessage',
135
- ]);
136
-
137
- const preservingMemberNames = new Set(['captureException', 'error', 'handleError', 'reject', 'warn']);
138
-
139
- const isPreservingCall = (callExpression, names) => {
140
- const propertyName = getCalleePropertyName(callExpression.callee);
141
- if (!propertyName) return false;
142
- if (callExpression.callee.type === 'MemberExpression' && callExpression.callee.object?.name === 'console')
143
- return false;
144
-
145
- const isKnownPreserver =
146
- preservingCallNames.has(propertyName) ||
147
- (callExpression.callee.type === 'MemberExpression' && preservingMemberNames.has(propertyName));
148
-
149
- return isKnownPreserver && nodeReferencesName(callExpression, names);
125
+ const getErrorHandlingSide = (filename) => {
126
+ const normalized = filename.replace(/\\/g, '/');
127
+ if (/(^|\/)client\//.test(normalized)) return 'client';
128
+ if (/(^|\/)(server|commands)\//.test(normalized)) return 'server';
129
+
130
+ return 'shared';
131
+ };
132
+
133
+ const getMemberPropertyName = (node) => {
134
+ if (node?.type !== 'MemberExpression') return null;
135
+ if (node.property.type === 'Identifier') return node.property.name;
136
+ if (node.property.type === 'Literal') return String(node.property.value);
137
+
138
+ return null;
139
+ };
140
+
141
+ const isConsoleMember = (node) =>
142
+ node?.type === 'MemberExpression' && node.object?.type === 'Identifier' && node.object.name === 'console';
143
+
144
+ const isAppReceiver = (node) => {
145
+ if (!node) return false;
146
+ if (node.type === 'Identifier' && node.name === 'app') return true;
147
+ if (node.type === 'MemberExpression' && getMemberPropertyName(node) === 'app') return true;
148
+
149
+ return false;
150
+ };
151
+
152
+ const isClientErrorHandlerCall = (callExpression) =>
153
+ callExpression.callee.type === 'MemberExpression' &&
154
+ getMemberPropertyName(callExpression.callee) === 'handleError' &&
155
+ isAppReceiver(callExpression.callee.object);
156
+
157
+ const isServerErrorReporterCall = (callExpression) =>
158
+ callExpression.callee.type === 'MemberExpression' &&
159
+ getMemberPropertyName(callExpression.callee) === 'reportError' &&
160
+ isAppReceiver(callExpression.callee.object);
161
+
162
+ const isPromiseRejectCall = (callExpression) => getCalleePropertyName(callExpression.callee) === 'reject';
163
+
164
+ const hasOptionalCallBoundary = (callExpression, ancestors = []) => {
165
+ if (callExpression.optional === true) return true;
166
+
167
+ let hasOptional = false;
168
+ traverseNode(callExpression.callee, (child) => {
169
+ if (child.type === 'ChainExpression' || child.optional === true) hasOptional = true;
170
+ });
171
+
172
+ return hasOptional || ancestors.some(({ node }) => node.type === 'ChainExpression');
150
173
  };
151
174
 
152
- const handlerPreservesCaughtError = (node, names) => {
175
+ const isUnderConditionalControlFlow = (ancestors = []) =>
176
+ ancestors.some(({ node, childKey }) => {
177
+ if (node.type === 'IfStatement') return childKey === 'consequent' || childKey === 'alternate';
178
+ if (node.type === 'ConditionalExpression') return childKey === 'consequent' || childKey === 'alternate';
179
+ if (node.type === 'LogicalExpression') return childKey === 'right';
180
+ if (node.type === 'SwitchCase') return childKey === 'consequent';
181
+
182
+ return (
183
+ ['ForInStatement', 'ForOfStatement', 'ForStatement', 'WhileStatement', 'DoWhileStatement'].includes(
184
+ node.type,
185
+ ) && childKey === 'body'
186
+ );
187
+ });
188
+
189
+ const isPreservingCall = (callExpression, names, side, ancestors = []) => {
190
+ if (!nodeReferencesName(callExpression, names)) return false;
191
+ if (hasOptionalCallBoundary(callExpression, ancestors)) return false;
192
+ if (isUnderConditionalControlFlow(ancestors)) return false;
193
+ if (isConsoleMember(callExpression.callee)) return false;
194
+ if (isPromiseRejectCall(callExpression)) return true;
195
+ if (side === 'client') return isClientErrorHandlerCall(callExpression);
196
+ if (side === 'server') return isServerErrorReporterCall(callExpression);
197
+
198
+ return isClientErrorHandlerCall(callExpression) || isServerErrorReporterCall(callExpression);
199
+ };
200
+
201
+ const handlerPreservesCaughtError = (node, names, side) => {
153
202
  let preserves = false;
154
203
 
155
- traverseNode(node, (child) => {
156
- if (child.type === 'ThrowStatement' && nodeReferencesName(child.argument, names)) preserves = true;
157
- if (child.type === 'CallExpression' && isPreservingCall(child, names)) preserves = true;
204
+ traverseNode(node, (child, _parent, _parentKey, ancestors) => {
205
+ if (
206
+ child.type === 'ThrowStatement' &&
207
+ nodeReferencesName(child.argument, names) &&
208
+ !isUnderConditionalControlFlow(ancestors)
209
+ ) {
210
+ preserves = true;
211
+ }
212
+ if (child.type === 'CallExpression' && isPreservingCall(child, names, side, ancestors)) preserves = true;
158
213
  });
159
214
 
160
215
  return preserves;
161
216
  };
162
217
 
163
- const directPromiseCatchHandlers = new Set([
164
- 'captureError',
165
- 'captureException',
166
- 'consoleError',
167
- 'handleError',
168
- 'logError',
169
- 'reportError',
170
- ]);
171
-
172
218
  const isDirectPromiseCatchHandler = (node) => {
173
219
  const name = getCalleePropertyName(node);
174
- if (name && directPromiseCatchHandlers.has(name)) return true;
175
- return false;
220
+ return name === 'reject';
176
221
  };
177
222
 
178
223
  const createSwallowedErrorRule = () => ({
179
224
  meta: {
180
225
  type: 'problem',
181
226
  docs: {
182
- description: 'Require caught errors to be preserved, reported, rethrown, or surfaced with original detail.',
227
+ description: 'Require caught errors to reach the standard app error path or be rethrown.',
183
228
  },
184
229
  messages: {
185
230
  missingParam:
186
- 'Caught errors must be bound and preserved. Use `catch (error)` and rethrow, report, route, or surface original details.',
231
+ 'Caught errors must be bound and routed through the standard error path. Use `catch (error)` and rethrow, call app.reportError on the server, or call app.handleError on the client.',
187
232
  unusedParam:
188
- 'Caught error `{{name}}` is discarded. Rethrow it, report it, route it to app error handling, or surface its original details.',
233
+ 'Caught error `{{name}}` is discarded. Rethrow it, call app.reportError on the server, or call app.handleError on the client.',
189
234
  unpreserved:
190
- 'Caught error `{{name}}` is used but not preserved. Rethrow it, report it, route it, or surface original error details.',
235
+ 'Caught error `{{name}}` is used but not routed through the standard error path. Rethrow it, call app.reportError on the server, or call app.handleError on the client.',
191
236
  },
192
237
  schema: [],
193
238
  },
194
239
  create(context) {
240
+ const side = getErrorHandlingSide(context.filename || context.getFilename?.() || '');
195
241
  const reportHandler = (node, params, body) => {
196
242
  const names = params.flatMap((param) => collectPatternNames(param));
197
243
  if (names.length === 0) {
@@ -205,7 +251,7 @@ const createSwallowedErrorRule = () => ({
205
251
  return;
206
252
  }
207
253
 
208
- if (!handlerPreservesCaughtError(body, collectDerivedErrorNames(body, names))) {
254
+ if (!handlerPreservesCaughtError(body, collectDerivedErrorNames(body, names), side)) {
209
255
  context.report({ node, messageId: 'unpreserved', data: { name: referencedName } });
210
256
  }
211
257
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "proteum",
3
3
  "description": "LLM-first Opinionated Typescript Framework for web applications.",
4
- "version": "2.5.0",
4
+ "version": "2.5.2",
5
5
  "author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
6
6
  "repository": "git://github.com/gaetanlegac/proteum.git",
7
7
  "license": "MIT",
@@ -92,6 +92,24 @@ const createCommandsManager = (app: Application) => new CommandsManager(app, { d
92
92
  const createDevCommandsRegistry = (app: Application) => new DevCommandsRegistry(app);
93
93
  const createDevDiagnosticsRegistry = (app: Application) => new DevDiagnosticsRegistry(app);
94
94
 
95
+ const normalizeReportedError = (rejection: unknown, fallbackMessage: string): Error => {
96
+ if (rejection instanceof Error) return rejection;
97
+ if (typeof rejection === 'string') return new Error(rejection);
98
+
99
+ return new Error(fallbackMessage);
100
+ };
101
+
102
+ const getNumericErrorProperty = (error: Error, property: 'http' | 'status' | 'statusCode') => {
103
+ const value = (error as Error & Partial<Record<typeof property, unknown>>)[property];
104
+ return typeof value === 'number' ? value : undefined;
105
+ };
106
+
107
+ const getErrorHttpCode = (error: Error) =>
108
+ getNumericErrorProperty(error, 'http') ??
109
+ getNumericErrorProperty(error, 'status') ??
110
+ getNumericErrorProperty(error, 'statusCode') ??
111
+ 500;
112
+
95
113
  /*----------------------------------
96
114
  - FUNCTIONS
97
115
  ----------------------------------*/
@@ -138,12 +156,12 @@ export abstract class Application<
138
156
  // Handle unhandled crash
139
157
  this.on('error', (e, request) => this.container.handleBug(e, 'An error occured in the application', request));
140
158
 
141
- process.on('unhandledRejection', (error: any, _promise: any) => {
159
+ process.on('unhandledRejection', (error: unknown) => {
142
160
  // Log so we know it's coming from unhandledRejection
143
161
  console.error('unhandledRejection', error);
144
162
 
145
163
  // We don't log the error here because it's the role of the app to decidehiw to log errors
146
- this.runHook('error', error);
164
+ void this.reportError(error);
147
165
  });
148
166
 
149
167
  // We can't pass this in super so we assign here
@@ -155,6 +173,14 @@ export abstract class Application<
155
173
  return this.container.Console.createBugReport(new Anomaly(...anomalyArgs));
156
174
  }
157
175
 
176
+ public async reportError(rejection: unknown, request?: ServerRequest<TServerRouter>) {
177
+ const error = normalizeReportedError(rejection, 'Unknown application error');
178
+ const code = getErrorHttpCode(error);
179
+
180
+ if (code === 500) await this.runHook('error', error, request);
181
+ else await this.runHook('error.' + code, error, request);
182
+ }
183
+
158
184
  /*----------------------------------
159
185
  - COMMANDS
160
186
  ----------------------------------*/
@@ -637,7 +637,7 @@ export default class ServerRouter<
637
637
  error instanceof Error ? error : new Error(typeof error === 'string' ? error : 'Unknown request.finished hook error');
638
638
 
639
639
  try {
640
- await this.app.runHook('error', typedError, request);
640
+ await this.app.reportError(typedError, request);
641
641
  } catch (hookError) {
642
642
  console.error('request.finished hook error', typedError, 'Error hook failure', hookError);
643
643
  }
@@ -1111,7 +1111,7 @@ export default class ServerRouter<
1111
1111
  console.log(LogPrefix, 'Error catched from the router:', error);
1112
1112
 
1113
1113
  // Report error
1114
- await this.app.runHook('error', error, request);
1114
+ await this.app.reportError(error, request);
1115
1115
 
1116
1116
  // Don't exose technical errors to users
1117
1117
  if (this.app.env.profile === 'prod')
@@ -1123,7 +1123,7 @@ export default class ServerRouter<
1123
1123
  /*if (this.app.env.profile === "dev")
1124
1124
  console.warn(e);*/
1125
1125
 
1126
- await this.app.runHook('error.' + code, error, request);
1126
+ await this.app.reportError(error, request);
1127
1127
  }
1128
1128
 
1129
1129
  // Return error based on the request format
@@ -205,6 +205,20 @@ test('mcp help describes projectId routing', () => {
205
205
  assert.match(output, /--stdio/);
206
206
  });
207
207
 
208
+ test('verify help documents changed-file verification flags', () => {
209
+ const result = spawnSync(process.execPath, [cliBin, 'verify', '--help'], {
210
+ cwd: coreRoot,
211
+ encoding: 'utf8',
212
+ });
213
+ const output = `${result.stdout}\n${result.stderr}`;
214
+
215
+ assert.equal(result.status, 0);
216
+ assert.match(output, /changed/);
217
+ assert.match(output, /--staged/);
218
+ assert.match(output, /--base/);
219
+ assert.match(output, /--dry-run/);
220
+ });
221
+
208
222
  test('mcp daemon launch prints a central MCP connection banner', async () => {
209
223
  const registryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-mcp-daemon-launch-'));
210
224
  const reserveServer = http.createServer((req, res) => res.end('reserved'));
@@ -0,0 +1,100 @@
1
+ const assert = require('node:assert/strict');
2
+ const Module = require('node:module');
3
+ const path = require('node:path');
4
+
5
+ const coreRoot = path.join(__dirname, '..');
6
+ require('module-alias').addAliases({
7
+ '@client': path.join(coreRoot, 'client'),
8
+ '@common': path.join(coreRoot, 'common'),
9
+ '@server': path.join(coreRoot, 'server'),
10
+ });
11
+ process.env.TS_NODE_PROJECT = path.join(coreRoot, 'cli', 'tsconfig.json');
12
+ process.env.TS_NODE_TRANSPILE_ONLY = '1';
13
+ require('ts-node/register/transpile-only');
14
+ const previousLessExtension = require.extensions['.less'];
15
+ require.extensions['.less'] = () => {};
16
+
17
+ const clientContextStub = () => ({ side: 'client' });
18
+ clientContextStub.default = clientContextStub;
19
+ const originalLoad = Module._load;
20
+ Module._load = function load(request, parent, isMain) {
21
+ if (request === '@/client/context') return clientContextStub;
22
+
23
+ return originalLoad.call(this, request, parent, isMain);
24
+ };
25
+
26
+ const previousWindow = global.window;
27
+ const previousDocument = global.document;
28
+ global.window = {
29
+ dev: false,
30
+ addEventListener: () => {},
31
+ removeEventListener: () => {},
32
+ history: {
33
+ state: {},
34
+ pushState: () => {},
35
+ replaceState: () => {},
36
+ },
37
+ location: {
38
+ hash: '',
39
+ pathname: '/',
40
+ search: '',
41
+ assign: () => {},
42
+ },
43
+ };
44
+ global.document = { defaultView: global.window };
45
+
46
+ const {
47
+ default: Application,
48
+ getClientErrorMessage,
49
+ normalizeClientError,
50
+ } = require('../client/app/index.ts');
51
+
52
+ class TestApplication extends Application {
53
+ boot() {}
54
+ handleUpdate() {}
55
+ }
56
+
57
+ test('client error message normalization accepts unknown caught values', () => {
58
+ assert.equal(getClientErrorMessage(new Error('boom'), 'fallback'), 'boom');
59
+ assert.equal(getClientErrorMessage('string failure', 'fallback'), 'string failure');
60
+ assert.equal(getClientErrorMessage({ message: 'object failure' }, 'fallback'), 'object failure');
61
+ assert.equal(getClientErrorMessage({ code: 'UNKNOWN' }, 'fallback'), 'fallback');
62
+ });
63
+
64
+ test('client error normalization returns an Error instance', () => {
65
+ const existing = new Error('existing');
66
+
67
+ assert.equal(normalizeClientError(existing), existing);
68
+ assert.equal(normalizeClientError('string failure').message, 'string failure');
69
+ assert.equal(normalizeClientError({ code: 'UNKNOWN' }, 'fallback').message, 'fallback');
70
+ });
71
+
72
+ test('client app handleError logs and returns displayable message', () => {
73
+ const app = new TestApplication();
74
+ const logged = [];
75
+ const originalConsoleError = console.error;
76
+ console.error = (error) => {
77
+ logged.push(error);
78
+ };
79
+
80
+ try {
81
+ const message = app.handleError({ code: 'UNKNOWN' }, 'Unable to finish action.');
82
+
83
+ assert.equal(message, 'Unable to finish action.');
84
+ assert.equal(logged.length, 1);
85
+ assert.equal(logged[0] instanceof Error, true);
86
+ assert.equal(logged[0].message, 'Unable to finish action.');
87
+ } finally {
88
+ console.error = originalConsoleError;
89
+ }
90
+ });
91
+
92
+ afterAll(() => {
93
+ Module._load = originalLoad;
94
+ if (previousLessExtension === undefined) delete require.extensions['.less'];
95
+ else require.extensions['.less'] = previousLessExtension;
96
+ if (previousWindow === undefined) delete global.window;
97
+ else global.window = previousWindow;
98
+ if (previousDocument === undefined) delete global.document;
99
+ else global.document = previousDocument;
100
+ });
@@ -0,0 +1,98 @@
1
+ const assert = require('node:assert/strict');
2
+ const fs = require('node:fs');
3
+ const os = require('node:os');
4
+ const path = require('node:path');
5
+
6
+ const coreRoot = path.resolve(__dirname, '..');
7
+ process.env.TS_NODE_PROJECT = path.join(coreRoot, 'tsconfig.json');
8
+ process.env.TS_NODE_TRANSPILE_ONLY = '1';
9
+ require('ts-node/register/transpile-only');
10
+
11
+ const { buildContractsDoctorResponse } = require('../common/dev/contractsDoctor.ts');
12
+
13
+ const touch = (filepath, content = '') => {
14
+ fs.mkdirSync(path.dirname(filepath), { recursive: true });
15
+ fs.writeFileSync(filepath, content);
16
+ };
17
+
18
+ const createManifest = (appRoot, pageFilepath) => ({
19
+ app: {
20
+ identityFilepath: path.join(appRoot, 'identity.config.ts'),
21
+ root: appRoot,
22
+ setupFilepath: path.join(appRoot, 'proteum.config.ts'),
23
+ },
24
+ connectedProjects: [],
25
+ commands: [],
26
+ controllers: [],
27
+ layouts: [],
28
+ routes: {
29
+ client: [
30
+ {
31
+ chunkFilepath: 'landing/index',
32
+ filepath: pageFilepath,
33
+ },
34
+ ],
35
+ server: [],
36
+ },
37
+ services: {
38
+ app: [],
39
+ routerPlugins: [],
40
+ },
41
+ });
42
+
43
+ test('contracts doctor allows hooks inside definePageRoute render callbacks', () => {
44
+ const appRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-contracts-doctor-'));
45
+ const pageFilepath = path.join(appRoot, 'client/pages/landing/index.tsx');
46
+
47
+ touch(path.join(appRoot, 'proteum.config.ts'), 'export default {};');
48
+ touch(path.join(appRoot, 'identity.config.ts'), 'export default {};');
49
+ touch(path.join(appRoot, 'client/hooks/useToast.ts'), 'export default function useToast() { return () => undefined; }');
50
+ touch(
51
+ pageFilepath,
52
+ `import { definePageRoute } from '@common/router/definitions';
53
+ import useToast from '@/client/hooks/useToast';
54
+
55
+ export default definePageRoute({
56
+ path: '/',
57
+ options: {},
58
+ data: null,
59
+ render: () => {
60
+ const toast = useToast();
61
+ return toast ? null : null;
62
+ },
63
+ });
64
+ `,
65
+ );
66
+
67
+ for (const filepath of [
68
+ 'proteum.connected.json',
69
+ '.proteum/manifest.json',
70
+ '.proteum/proteum.connected.d.ts',
71
+ '.proteum/client/context.ts',
72
+ '.proteum/client/controllers.ts',
73
+ '.proteum/client/layouts.ts',
74
+ '.proteum/client/models.ts',
75
+ '.proteum/client/routes.ts',
76
+ '.proteum/client/services.d.ts',
77
+ '.proteum/common/controllers.ts',
78
+ '.proteum/common/models.ts',
79
+ '.proteum/common/services.d.ts',
80
+ '.proteum/server/commands.app.d.ts',
81
+ '.proteum/server/commands.d.ts',
82
+ '.proteum/server/commands.ts',
83
+ '.proteum/server/controllers.ts',
84
+ '.proteum/server/models.ts',
85
+ '.proteum/server/routes.ts',
86
+ '.proteum/server/services.d.ts',
87
+ '.proteum/client/route-modules/landing/index.tsx',
88
+ '.proteum/server/route-modules/client/pages/landing/index.tsx',
89
+ ]) {
90
+ touch(path.join(appRoot, filepath));
91
+ }
92
+
93
+ const response = buildContractsDoctorResponse(createManifest(appRoot, pageFilepath));
94
+ assert.equal(
95
+ response.diagnostics.some((diagnostic) => diagnostic.code === 'runtime/provider-hook-outside-provider'),
96
+ false,
97
+ );
98
+ });