@devvit/server 0.11.19-next-2025-07-02-17-32-32-070c5de78.0 → 0.11.19-next-2025-07-02-19-47-19-53796ec90.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/define-config.d.ts +8 -1
- package/define-config.d.ts.map +1 -1
- package/define-config.js +76 -16
- package/define-config.test.d.ts.map +1 -0
- package/package.json +8 -8
package/define-config.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Devvit } from '@devvit/public-api';
|
|
1
|
+
import { Devvit, type FormKey } from '@devvit/public-api';
|
|
2
|
+
import type { UiResponse } from '@devvit/shared/types/ui-response.js';
|
|
2
3
|
import type { AppConfig, AppFormsConfig, AppMenuActionConfig, AppPermissionConfig, AppPostCreateConfig, AppSchedulerConfig } from '@devvit/shared-types/schemas/config-file.v1.js';
|
|
3
4
|
/**
|
|
4
5
|
* A subset of AppConfig (see config-file.v1.json) available in V8-flavored
|
|
@@ -53,6 +54,12 @@ export type DynamicAppPostCreateConfig = AppPostCreateConfig & {
|
|
|
53
54
|
title?(): string | Promise<string> | undefined;
|
|
54
55
|
} | undefined;
|
|
55
56
|
};
|
|
57
|
+
/** @internal [state] Map of devvit.json form keys to Devvit-singleton form keys. */
|
|
58
|
+
export declare const formKeyMap: {
|
|
59
|
+
[formKey: string]: FormKey;
|
|
60
|
+
};
|
|
56
61
|
/** @experimental */
|
|
57
62
|
export declare function defineConfig(config: Readonly<DynamicAppConfig | AppConfig>): typeof Devvit;
|
|
63
|
+
/** @internal */
|
|
64
|
+
export declare function validateUiResponse(uiResponse: UiResponse): void;
|
|
58
65
|
//# sourceMappingURL=define-config.d.ts.map
|
package/define-config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"define-config.d.ts","sourceRoot":"","sources":["../src/define-config.tsx"],"names":[],"mappings":"AACA,OAAO,EAEL,MAAM,
|
|
1
|
+
{"version":3,"file":"define-config.d.ts","sourceRoot":"","sources":["../src/define-config.tsx"],"names":[],"mappings":"AACA,OAAO,EAEL,MAAM,EACN,KAAK,OAAO,EAMb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qCAAqC,CAAC;AAEtE,OAAO,KAAK,EACV,SAAS,EACT,cAAc,EACd,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,kBAAkB,EACnB,MAAM,gDAAgD,CAAC;AAIxD;;;;;;;;GAQG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,WAAW,EAAE,mBAAmB,CAAC;IACjC,IAAI,CAAC,EAAE,oBAAoB,GAAG,SAAS,CAAC;IACxC,WAAW,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACpC,SAAS,CAAC,EAAE,kBAAkB,GAAG,SAAS,CAAC;IAC3C,KAAK,CAAC,EAAE,cAAc,CAAC;CACxB,CAAC;AAEF,oBAAoB;AACpB,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE;QACN,GAAG,EAAE,MAAM,CAAC;QACZ,KAAK,EAAE,MAAM,CAAC;QACd,kBAAkB;QAClB,MAAM,CAAC,EAAE;YACP,iDAAiD;YACjD,MAAM,CAAC,EACH,QAAQ,GACR;gBACE,IAAI,EAAE,OAAO,CAAC;gBACd;;;mBAGG;gBACH,KAAK,CAAC,EAAE,CAAC,MAAM,MAAM,CAAC,GAAG,SAAS,CAAC;aACpC,GACD,MAAM,CAAC,mBAAmB,GAC1B,SAAS,CAAC;SACf,CAAC;KACH,CAAC;IACF,MAAM,EAAE,0BAA0B,CAAC;CACpC,CAAC;AAEF,oBAAoB;AACpB,MAAM,MAAM,0BAA0B,GAAG,mBAAmB,GAAG;IAC7D,kBAAkB;IAClB,MAAM,CAAC,EACH;QACE;;;;WAIG;QACH,MAAM,CAAC,IAAI,GAAG,CAAC,OAAO,GAAG,SAAS,CAAC;QACnC;;;WAGG;QACH,KAAK,CAAC,IAAI,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC;KAChD,GACD,SAAS,CAAC;CACf,CAAC;AAEF,oFAAoF;AACpF,eAAO,MAAM,UAAU,EAAE;IAAE,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAA;CAAO,CAAC;AAE7D,oBAAoB;AACpB,wBAAgB,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC,gBAAgB,GAAG,SAAS,CAAC,GAAG,OAAO,MAAM,CAO1F;AAyGD,gBAAgB;AAChB,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,UAAU,GAAG,IAAI,CA4D/D"}
|
package/define-config.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Devvit, useWebView, } from '@devvit/public-api';
|
|
2
2
|
import { getServerPort } from './get-server-port.js';
|
|
3
|
-
/** [state] Map of devvit.json form keys to Devvit-singleton form keys. */
|
|
4
|
-
const formKeyMap = {};
|
|
3
|
+
/** @internal [state] Map of devvit.json form keys to Devvit-singleton form keys. */
|
|
4
|
+
export const formKeyMap = {};
|
|
5
5
|
/** @experimental */
|
|
6
6
|
export function defineConfig(config) {
|
|
7
7
|
configurePermissions(config.permissions);
|
|
@@ -77,9 +77,9 @@ function configureMenuActions(menuActions) {
|
|
|
77
77
|
location: action.location,
|
|
78
78
|
onPress: async (ev, ctx) => {
|
|
79
79
|
const responseJson = await callWebbitEndpoint(action.endpoint, ev, ctx.debug.metadata);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
await handleUiResponse(ctx,
|
|
80
|
+
const uiResponse = responseJson;
|
|
81
|
+
validateUiResponse(uiResponse);
|
|
82
|
+
await handleUiResponse(ctx, uiResponse);
|
|
83
83
|
},
|
|
84
84
|
};
|
|
85
85
|
// "user" type is blank in Devvit classic. So if it's present in
|
|
@@ -102,9 +102,58 @@ function configureForms(forms) {
|
|
|
102
102
|
}
|
|
103
103
|
async function handleFormResponse(endpoint, event, ctx) {
|
|
104
104
|
const responseJson = await callWebbitEndpoint(endpoint, event.values, ctx.debug.metadata);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
await handleUiResponse(ctx,
|
|
105
|
+
const uiResponse = responseJson;
|
|
106
|
+
validateUiResponse(uiResponse);
|
|
107
|
+
await handleUiResponse(ctx, uiResponse);
|
|
108
|
+
}
|
|
109
|
+
// TODO: expand this to fully validate the UiResponse format, including Form definitions,
|
|
110
|
+
// and convert signature to validateUiResponse(uiResponse: JSONValue): uiResponse is UiResponse
|
|
111
|
+
/** @internal */
|
|
112
|
+
export function validateUiResponse(uiResponse) {
|
|
113
|
+
// Validations:
|
|
114
|
+
// (1) The only valid fields on uiResponse are showToast, navigateTo, and showForm.
|
|
115
|
+
const validKeys = ['showToast', 'navigateTo', 'showForm'];
|
|
116
|
+
const invalidKeys = Object.keys(uiResponse).filter((key) => !validKeys.includes(key));
|
|
117
|
+
if (invalidKeys.length > 0) {
|
|
118
|
+
throw new Error(`Invalid fields found in UiResponse: ${invalidKeys.join(', ')}. Valid fields are: ${validKeys.join(', ')}`);
|
|
119
|
+
}
|
|
120
|
+
// (2) showForm must have a name that exists in the formKeyMap.
|
|
121
|
+
if (uiResponse.showForm) {
|
|
122
|
+
if (!uiResponse.showForm.name) {
|
|
123
|
+
throw new Error('showForm must have a name');
|
|
124
|
+
}
|
|
125
|
+
if (!formKeyMap[uiResponse.showForm.name]) {
|
|
126
|
+
throw new Error(`Form with name ${uiResponse.showForm.name} not found in devvit.json. Consider adding '"forms"."${uiResponse.showForm.name}"="/internal/your/endpoint"'.`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// (3) showToast must be a string or an object with only fields [text, appearance]
|
|
130
|
+
// and text is a mandatory string.
|
|
131
|
+
if (uiResponse.showToast !== undefined) {
|
|
132
|
+
if (typeof uiResponse.showToast === 'string') {
|
|
133
|
+
// Valid case: showToast is a string
|
|
134
|
+
}
|
|
135
|
+
else if (typeof uiResponse.showToast === 'object' && uiResponse.showToast !== null) {
|
|
136
|
+
// Check if it's an object with valid fields
|
|
137
|
+
const toastKeys = Object.keys(uiResponse.showToast);
|
|
138
|
+
const validToastKeys = ['text', 'appearance'];
|
|
139
|
+
const invalidToastKeys = toastKeys.filter((key) => !validToastKeys.includes(key));
|
|
140
|
+
if (invalidToastKeys.length > 0) {
|
|
141
|
+
throw new Error(`Invalid fields found in showToast: ${invalidToastKeys.join(', ')}. Valid fields are: ${validToastKeys.join(', ')}`);
|
|
142
|
+
}
|
|
143
|
+
if (typeof uiResponse.showToast.text !== 'string') {
|
|
144
|
+
throw new Error('showToast.text is required and must be a string');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
throw new Error('showToast must be a string or an object with text and optional appearance fields');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// (4) navigateTo must be a valid URL or a Post object. (this is validated client-side, so
|
|
152
|
+
// we don't need to validate it here).
|
|
153
|
+
// (5) navigateTo and showForm are mutually exclusive.
|
|
154
|
+
if (uiResponse.navigateTo && uiResponse.showForm) {
|
|
155
|
+
throw new Error('navigateTo and showForm cannot be used together in UiResponse');
|
|
156
|
+
}
|
|
108
157
|
}
|
|
109
158
|
/**
|
|
110
159
|
* Handle a UiResponse from a Webbit handler (menu action or form handler).
|
|
@@ -121,12 +170,7 @@ async function handleUiResponse(ctx, uiResponse) {
|
|
|
121
170
|
ctx.ui.navigateTo(uiResponse.navigateTo);
|
|
122
171
|
}
|
|
123
172
|
if (uiResponse.showForm) {
|
|
124
|
-
|
|
125
|
-
if (!formKey) {
|
|
126
|
-
const name = uiResponse.showForm.name;
|
|
127
|
-
throw new Error(`Form with name ${name} not found in devvit.json. Consider adding '"forms"."${name}"="/internal/your/endpoint"'.`);
|
|
128
|
-
}
|
|
129
|
-
ctx.ui.showFormInternal(formKey, uiResponse.showForm.data, uiResponse.showForm.form);
|
|
173
|
+
ctx.ui.showFormInternal(formKeyMap[uiResponse.showForm.name], uiResponse.showForm.data, uiResponse.showForm.form);
|
|
130
174
|
}
|
|
131
175
|
}
|
|
132
176
|
async function callWebbitEndpoint(endpoint, body, metadata) {
|
|
@@ -144,9 +188,25 @@ async function callWebbitEndpoint(endpoint, body, metadata) {
|
|
|
144
188
|
});
|
|
145
189
|
if (!response.ok) {
|
|
146
190
|
const body = await response.text();
|
|
147
|
-
|
|
191
|
+
let errorMessage = `Failed to POST ${endpoint}: ${response.statusText}, `;
|
|
192
|
+
if (response.status === 404) {
|
|
193
|
+
errorMessage += `ensure that you're handling this endpoint in your server code.`;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
errorMessage += `body: ${body.substring(0, 100)}`;
|
|
197
|
+
}
|
|
198
|
+
throw new Error(errorMessage);
|
|
199
|
+
}
|
|
200
|
+
const contentType = response.headers.get('Content-Type');
|
|
201
|
+
if (!contentType || !contentType.includes('application/json')) {
|
|
202
|
+
throw new Error(`Failed to POST ${endpoint}: expected response type 'application/json', received '${contentType}'`);
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
return await response.json();
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
throw new Error(`Failed to POST ${endpoint}: ${error}`);
|
|
148
209
|
}
|
|
149
|
-
return await response.json();
|
|
150
210
|
}
|
|
151
211
|
function configureScheduler(schedulerConfig) {
|
|
152
212
|
const cronTasks = {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define-config.test.d.ts","sourceRoot":"","sources":["../src/define-config.test.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devvit/server",
|
|
3
|
-
"version": "0.11.19-next-2025-07-02-
|
|
3
|
+
"version": "0.11.19-next-2025-07-02-19-47-19-53796ec90.0",
|
|
4
4
|
"license": "BSD-3-Clause",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -23,14 +23,14 @@
|
|
|
23
23
|
},
|
|
24
24
|
"types": "./index.d.ts",
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@devvit/protos": "0.11.19-next-2025-07-02-
|
|
27
|
-
"@devvit/public-api": "0.11.19-next-2025-07-02-
|
|
28
|
-
"@devvit/shared": "0.11.19-next-2025-07-02-
|
|
29
|
-
"@devvit/shared-types": "0.11.19-next-2025-07-02-
|
|
26
|
+
"@devvit/protos": "0.11.19-next-2025-07-02-19-47-19-53796ec90.0",
|
|
27
|
+
"@devvit/public-api": "0.11.19-next-2025-07-02-19-47-19-53796ec90.0",
|
|
28
|
+
"@devvit/shared": "0.11.19-next-2025-07-02-19-47-19-53796ec90.0",
|
|
29
|
+
"@devvit/shared-types": "0.11.19-next-2025-07-02-19-47-19-53796ec90.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@devvit/repo-tools": "0.11.19-next-2025-07-02-
|
|
33
|
-
"@devvit/tsconfig": "0.11.19-next-2025-07-02-
|
|
32
|
+
"@devvit/repo-tools": "0.11.19-next-2025-07-02-19-47-19-53796ec90.0",
|
|
33
|
+
"@devvit/tsconfig": "0.11.19-next-2025-07-02-19-47-19-53796ec90.0",
|
|
34
34
|
"eslint": "9.11.1",
|
|
35
35
|
"typescript": "5.8.3",
|
|
36
36
|
"vitest": "1.6.1"
|
|
@@ -39,5 +39,5 @@
|
|
|
39
39
|
"directory": "dist"
|
|
40
40
|
},
|
|
41
41
|
"source": "./src/index.ts",
|
|
42
|
-
"gitHead": "
|
|
42
|
+
"gitHead": "3f254687c8acc69f3c5670529ab4790fd910780e"
|
|
43
43
|
}
|