@devvit/server 0.11.14-next-2025-05-05-765f3688c.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/LICENSE ADDED
@@ -0,0 +1,26 @@
1
+ Copyright (c) 2023 Reddit Inc.
2
+
3
+ Redistribution and use in source and binary forms, with or without
4
+ modification, are permitted provided that the following conditions
5
+ are met:
6
+
7
+ 1. Redistributions of source code must retain the above copyright
8
+ notice, this list of conditions and the following disclaimer.
9
+ 2. Redistributions in binary form must reproduce the above copyright
10
+ notice, this list of conditions and the following disclaimer in the
11
+ documentation and/or other materials provided with the distribution.
12
+ 3. Neither the name of the copyright holder nor the names of its
13
+ contributors may be used to endorse or promote products derived from
14
+ this software without specific prior written permission.
15
+
16
+ THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
17
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19
+ ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
20
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
22
+ OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
23
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
24
+ LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
25
+ OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
26
+ SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # @devvit/server
2
+
3
+ Node.js library for Reddit integration.
package/index.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { RequestContext, setRequestContext } from './request-context.js';
2
+ export { webbitEnablePost } from './webbit-post.js';
3
+ export { webbitEnable } from './webbit-server.js';
4
+ //# sourceMappingURL=index.d.ts.map
package/index.d.ts.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC"}
package/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { RequestContext, setRequestContext } from './request-context.js';
2
+ export { webbitEnablePost } from './webbit-post.js';
3
+ export { webbitEnable } from './webbit-server.js';
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@devvit/server",
3
+ "version": "0.11.14-next-2025-05-05-765f3688c.0",
4
+ "license": "BSD-3-Clause",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://developers.reddit.com/"
8
+ },
9
+ "type": "module",
10
+ "main": "./index.js",
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "clean": "rm -rf .turbo coverage dist",
14
+ "clobber": "yarn clean && rm -rf node_modules",
15
+ "dev": "tsc -w",
16
+ "lint": "redlint .",
17
+ "lint:fix": "yarn lint --fix",
18
+ "prepublishOnly": "publish-package-json",
19
+ "test": "yarn test:unit && yarn test:types && yarn lint",
20
+ "test:types": "tsc --noEmit",
21
+ "test:unit": "vitest run",
22
+ "test:unit-with-coverage": "vitest run --coverage"
23
+ },
24
+ "types": "./index.d.ts",
25
+ "dependencies": {
26
+ "@devvit/protos": "0.11.14-next-2025-05-05-765f3688c.0",
27
+ "@devvit/public-api": "0.11.14-next-2025-05-05-765f3688c.0",
28
+ "@devvit/shared-types": "0.11.14-next-2025-05-05-765f3688c.0"
29
+ },
30
+ "devDependencies": {
31
+ "@devvit/repo-tools": "0.11.14-next-2025-05-05-765f3688c.0",
32
+ "@devvit/tsconfig": "0.11.14-next-2025-05-05-765f3688c.0",
33
+ "eslint": "9.11.1",
34
+ "typescript": "5.3.2",
35
+ "vitest": "1.6.1"
36
+ },
37
+ "publishConfig": {
38
+ "directory": "dist"
39
+ },
40
+ "source": "./src/index.ts",
41
+ "gitHead": "f74f2c6405eac9d70b8ac67cdd13b24370bddb4a"
42
+ }
@@ -0,0 +1,19 @@
1
+ import type { Metadata } from '@devvit/protos';
2
+ import type { Context } from '@devvit/public-api';
3
+ /** Devvit server context for the lifetime of a request. */
4
+ export type RequestContext = Omit<Context, 'dimensions' | 'kvStore' | 'modLog' | 'ui' | 'uiEnvironment'>;
5
+ /** Designed to be compatible with IncomingHttpHeaders and any KV. */
6
+ type Headers = {
7
+ [header: string]: string | string[] | undefined;
8
+ };
9
+ /** Constructs a new RequestContext. */
10
+ export declare let RequestContext: (headers: Readonly<Headers>) => RequestContext;
11
+ /**
12
+ * Overwrite the context provider for test.
13
+ * @experimental
14
+ */
15
+ export declare function setRequestContext(fn: typeof RequestContext): void;
16
+ /** @internal */
17
+ export declare function metaFromIncomingMessage(headers: Readonly<Headers>): Metadata;
18
+ export {};
19
+ //# sourceMappingURL=request-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-context.d.ts","sourceRoot":"","sources":["../src/request-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAKlD,2DAA2D;AAC3D,MAAM,MAAM,cAAc,GAAG,IAAI,CAC/B,OAAO,EACP,YAAY,GAAG,SAAS,GAAG,QAAQ,GAAG,IAAI,GAAG,eAAe,CAC7D,CAAC;AAEF,qEAAqE;AACrE,KAAK,OAAO,GAAG;IAAE,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAA;CAAE,CAAC;AAEnE,uCAAuC;AACvC,eAAO,IAAI,cAAc,YAAa,SAAS,OAAO,CAAC,KAAG,cAMzD,CAAC;AAEF;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,OAAO,cAAc,GAAG,IAAI,CAEjE;AAED,gBAAgB;AAChB,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,QAAQ,CAM5E"}
@@ -0,0 +1,23 @@
1
+ import { makeAPIClients } from '@devvit/public-api/apis/makeAPIClients.js';
2
+ import { getContextFromMetadata } from '@devvit/public-api/devvit/internals/context.js';
3
+ import { Header, headerPrefix } from '@devvit/shared-types/Header.js';
4
+ /** Constructs a new RequestContext. */
5
+ export let RequestContext = (headers) => {
6
+ const meta = metaFromIncomingMessage(headers);
7
+ return Object.assign(makeAPIClients({ metadata: meta }), getContextFromMetadata(meta, meta[Header.Post]?.values[0]));
8
+ };
9
+ /**
10
+ * Overwrite the context provider for test.
11
+ * @experimental
12
+ */
13
+ export function setRequestContext(fn) {
14
+ RequestContext = fn;
15
+ }
16
+ /** @internal */
17
+ export function metaFromIncomingMessage(headers) {
18
+ const meta = {};
19
+ for (const [key, val] of Object.entries(headers))
20
+ if (key.startsWith(headerPrefix))
21
+ meta[key] = { values: typeof val === 'object' ? val : val == null ? [] : [val] };
22
+ return meta;
23
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-context.test.d.ts","sourceRoot":"","sources":["../src/request-context.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,45 @@
1
+ import { type Context, type CustomPostType, type SubmitCustomPostOptions } from '@devvit/public-api';
2
+ /** Configuration parameters for creating a webbit post type. */
3
+ type WebbitPostParams = Omit<CustomPostType, 'render'> & Partial<Pick<CustomPostType, 'render'>> & {
4
+ /** The path to the HTML entrypoint for the frontend */
5
+ entry: string;
6
+ menu?: WebbitMenuParams;
7
+ /** If true, the webbit post will be rendered inline in the post body instead of a focus mode webview with a launch button. */
8
+ inline?: boolean;
9
+ /**
10
+ * The text for the button that launches the webbit app in focus mode
11
+ * @defaultValue 'Launch App'
12
+ */
13
+ launchButtonText?: string;
14
+ };
15
+ /** Optional parameters for the automatically created subreddit menu item for creating posts. */
16
+ type WebbitMenuParams = Partial<Pick<SubmitCustomPostOptions, 'preview'>> & {
17
+ /**
18
+ * Whether the menu item is enabled
19
+ * @defaultValue true
20
+ */
21
+ enable?: boolean;
22
+ /**
23
+ * The label for the menu item
24
+ * @defaultValue '[${app name}] New Post'
25
+ */
26
+ label?: string;
27
+ /** The title of the post to be created. Can be a static string or a function that returns a string. */
28
+ postTitle?: string | ((context: Context) => Promise<string> | string);
29
+ };
30
+ /**
31
+ *
32
+ * @param {WebbitPostParams }params Parameters for the webbit post type.
33
+ * @example
34
+ * ```ts
35
+ * webbitEnablePost({
36
+ * name: 'Hello Webbit',
37
+ * description: 'Custom webbit post',
38
+ * height: 'tall',
39
+ * entry: 'page.html',
40
+ * });
41
+ * ```
42
+ */
43
+ export declare function webbitEnablePost(params: WebbitPostParams): void;
44
+ export {};
45
+ //# sourceMappingURL=webbit-post.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webbit-post.d.ts","sourceRoot":"","sources":["../src/webbit-post.tsx"],"names":[],"mappings":"AAKA,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,cAAc,EAKnB,KAAK,uBAAuB,EAI7B,MAAM,oBAAoB,CAAC;AAM5B,gEAAgE;AAChE,KAAK,gBAAgB,GAAG,IAAI,CAAC,cAAc,EAAE,QAAQ,CAAC,GACpD,OAAO,CAAC,IAAI,CAAC,cAAc,EAAE,QAAQ,CAAC,CAAC,GAAG;IACxC,uDAAuD;IACvD,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,8HAA8H;IAC9H,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEJ,gGAAgG;AAChG,KAAK,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,uBAAuB,EAAE,SAAS,CAAC,CAAC,GAAG;IAC1E;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uGAAuG;IACvG,SAAS,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC;CACvE,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI,CA6B/D"}
package/webbit-post.js ADDED
@@ -0,0 +1,123 @@
1
+ import { EffectType } from '@devvit/protos';
2
+ import { WebbitServerDefinition, } from '@devvit/protos/types/devvit/actor/webbit/webbit.js';
3
+ import { Devvit, useChannel, useState, useWebView, } from '@devvit/public-api';
4
+ import { useEffectEmitter } from '@devvit/public-api/devvit/internals/blocks/handler/BlocksHandler.js';
5
+ import { extendDevvitPrototype } from '@devvit/public-api/devvit/internals/helpers/extendDevvitPrototype.js';
6
+ import { newWebbitServer } from './webbit-server.js';
7
+ /**
8
+ *
9
+ * @param {WebbitPostParams }params Parameters for the webbit post type.
10
+ * @example
11
+ * ```ts
12
+ * webbitEnablePost({
13
+ * name: 'Hello Webbit',
14
+ * description: 'Custom webbit post',
15
+ * height: 'tall',
16
+ * entry: 'page.html',
17
+ * });
18
+ * ```
19
+ */
20
+ export function webbitEnablePost(params) {
21
+ // add the webbit server to the Devvit prototype
22
+ const server = newWebbitServer();
23
+ extendDevvitPrototype('Request', server.Request);
24
+ // enable Devvit features
25
+ Devvit.configure({
26
+ realtime: true,
27
+ redditAPI: true,
28
+ });
29
+ Devvit.provide(WebbitServerDefinition);
30
+ let render = (context) => params.inline ? defaultInlineRender(context, params) : defaultRender(params);
31
+ if (params.render) {
32
+ render = params.render;
33
+ }
34
+ // setup the custom post type
35
+ Devvit.addCustomPostType({ ...params, render });
36
+ // setup a menu item to create a webbit post
37
+ if (params.menu?.enable === false) {
38
+ // If the menu is disabled, we don't add a menu item.
39
+ return;
40
+ }
41
+ Devvit.addMenuItem(defaultMenuItem(params));
42
+ }
43
+ function defaultRender(params) {
44
+ const launchButtonText = params.launchButtonText ?? 'Launch App';
45
+ const [subscriptions, setSubscriptions] = useState([]);
46
+ const onMessage = messageHandler(setSubscriptions);
47
+ const { mount, postMessage } = useWebView({
48
+ // URL of your web view content
49
+ url: params.entry,
50
+ // Handle messages from web view
51
+ onMessage,
52
+ });
53
+ messageRelay(subscriptions, postMessage);
54
+ return (Devvit.createElement("vstack", { alignment: "center middle", height: "100%" },
55
+ Devvit.createElement("button", { onPress: mount }, launchButtonText),
56
+ ";"));
57
+ }
58
+ function defaultInlineRender(context, params) {
59
+ const webViewId = 'webView';
60
+ const [subscriptions, setSubscriptions] = useState([]);
61
+ messageRelay(subscriptions, (msg) => {
62
+ context.ui.webView.postMessage(webViewId, msg);
63
+ });
64
+ const onMessage = messageHandler(setSubscriptions);
65
+ return (Devvit.createElement("webview", { id: webViewId, url: params.entry, width: "100%", height: "100%", onMessage: onMessage }));
66
+ }
67
+ function messageHandler(setSubscriptions) {
68
+ const emitter = useEffectEmitter();
69
+ return (message) => {
70
+ // if is object, check type
71
+ if (typeof message === 'object' &&
72
+ message !== null &&
73
+ 'type' in message &&
74
+ message.type === 'effect') {
75
+ const effect = message.data;
76
+ const dedupeKey = message.key;
77
+ // Needs to be handled specially because its a dynamic subscription set.
78
+ if (effect.type === EffectType.EFFECT_REALTIME_SUB) {
79
+ setSubscriptions(effect.realtimeSubscriptions?.subscriptionIds ?? []);
80
+ }
81
+ emitter.emitEffect(dedupeKey, effect);
82
+ }
83
+ };
84
+ }
85
+ function messageRelay(subscriptions, sender) {
86
+ for (const sub of subscriptions) {
87
+ const channel = useChannel({
88
+ name: sub,
89
+ onMessage: (msg) => {
90
+ sender({
91
+ type: 'realtime',
92
+ channel: sub,
93
+ msg,
94
+ });
95
+ },
96
+ });
97
+ channel.subscribe();
98
+ }
99
+ }
100
+ function defaultMenuItem({ name, menu }) {
101
+ return {
102
+ forUserType: ['moderator'],
103
+ label: menu?.label || `[${name}] New Post`,
104
+ location: 'subreddit',
105
+ async onPress(_ev, ctx) {
106
+ const title = typeof menu?.postTitle === 'function'
107
+ ? await menu?.postTitle(ctx)
108
+ : menu?.postTitle || name;
109
+ if (!ctx.subredditName)
110
+ throw Error('invalid subreddit name');
111
+ const post = await ctx.reddit.submitPost({
112
+ preview: menu?.preview || Devvit.createElement("text", null, "Loading\u2026"),
113
+ subredditName: ctx.subredditName,
114
+ title,
115
+ });
116
+ await ctx.ui.showToast({
117
+ text: `Created post "${title}" in ${ctx.subredditName}. Navigating to it now.`,
118
+ appearance: 'success',
119
+ });
120
+ ctx.ui.navigateTo(post);
121
+ },
122
+ };
123
+ }
@@ -0,0 +1,4 @@
1
+ import { type WebbitServer } from '@devvit/protos/types/devvit/actor/webbit/webbit.js';
2
+ export declare function webbitEnable(): void;
3
+ export declare function newWebbitServer(): WebbitServer;
4
+ //# sourceMappingURL=webbit-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webbit-server.d.ts","sourceRoot":"","sources":["../src/webbit-server.ts"],"names":[],"mappings":"AACA,OAAO,EAIL,KAAK,YAAY,EAElB,MAAM,oDAAoD,CAAC;AAI5D,wBAAgB,YAAY,IAAI,IAAI,CAInC;AAED,wBAAgB,eAAe,IAAI,YAAY,CAsB9C"}
@@ -0,0 +1,29 @@
1
+ import { HttpMethod, WebbitServerDefinition, } from '@devvit/protos/types/devvit/actor/webbit/webbit.js';
2
+ import { Devvit } from '@devvit/public-api';
3
+ import { extendDevvitPrototype } from '@devvit/public-api/devvit/internals/helpers/extendDevvitPrototype.js';
4
+ export function webbitEnable() {
5
+ const server = newWebbitServer();
6
+ extendDevvitPrototype('Request', server.Request);
7
+ Devvit.provide(WebbitServerDefinition);
8
+ }
9
+ export function newWebbitServer() {
10
+ return {
11
+ async Request(req, meta) {
12
+ const port = process.env.WEBBIT_PORT || '3000';
13
+ // only set the body if the method is not GET or HEAD
14
+ const body = [HttpMethod.GET, HttpMethod.HEAD].includes(req.method) ? null : req.body;
15
+ const rsp = await fetch(new URL(req.path, `http://webbit.local:${port}`), {
16
+ body,
17
+ headers: [
18
+ ...Object.entries(req.headers),
19
+ // devvit- headers alway have priority.
20
+ ...Object.entries(meta ?? {}).map(([k, v]) => [k, v.values.join()]),
21
+ ],
22
+ method: HttpMethod[req.method],
23
+ });
24
+ const headers = {};
25
+ rsp.headers.forEach((v, k) => (headers[k] = v));
26
+ return { body: new Uint8Array(await rsp.arrayBuffer()), headers, statusCode: rsp.status };
27
+ },
28
+ };
29
+ }