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.
- package/AGENTS.md +2 -2
- package/README.md +46 -19
- package/agents/project/AGENTS.md +9 -7
- package/agents/project/CODING_STYLE.md +1 -1
- package/agents/project/client/AGENTS.md +5 -1
- package/agents/project/diagnostics.md +1 -1
- package/agents/project/root/AGENTS.md +9 -7
- package/agents/project/server/services/AGENTS.md +4 -0
- package/agents/project/tests/AGENTS.md +1 -1
- package/cli/commands/verify.ts +117 -4
- package/cli/compiler/artifacts/controllerHelper.ts +66 -0
- package/cli/compiler/artifacts/controllers.ts +3 -0
- package/cli/compiler/artifacts/services.ts +14 -8
- package/cli/compiler/common/generatedRouteModules.ts +270 -53
- package/cli/presentation/commands.ts +11 -1
- package/cli/runtime/commands.ts +6 -0
- package/cli/scaffold/templates.ts +14 -6
- package/cli/utils/agents.ts +1 -1
- package/cli/verification/changed.ts +460 -0
- package/client/app/index.ts +22 -5
- package/client/services/router/index.tsx +1 -1
- package/client/services/router/request/api.ts +2 -2
- package/common/applicationConfig.ts +177 -0
- package/common/applicationConfigLoader.ts +33 -1
- package/common/dev/contractsDoctor.ts +16 -0
- package/config.ts +5 -1
- package/docs/migration-2.5.md +269 -0
- package/eslint.js +96 -50
- package/package.json +1 -1
- package/server/app/index.ts +28 -2
- package/server/services/router/index.ts +3 -3
- package/tests/cli-mcp-command.test.cjs +14 -0
- package/tests/client-app-error-handling.test.cjs +100 -0
- package/tests/contracts-doctor.test.cjs +98 -0
- package/tests/definition-contracts.test.cjs +129 -0
- package/tests/dev-transpile-watch.test.cjs +3 -6
- package/tests/eslint-rules.test.cjs +246 -7
- package/tests/scaffold-templates.test.cjs +43 -0
- package/tests/server-app-report-error.test.cjs +135 -0
- 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
|
|
125
|
-
'
|
|
126
|
-
'
|
|
127
|
-
'
|
|
128
|
-
|
|
129
|
-
'
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
'
|
|
134
|
-
'
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
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 (
|
|
157
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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.
|
|
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",
|
package/server/app/index.ts
CHANGED
|
@@ -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:
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
+
});
|