@devvit/ui-renderer 0.8.5
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 +26 -0
- package/client/devvit-ui-interactive.d.ts +27 -0
- package/client/devvit-ui-interactive.d.ts.map +1 -0
- package/client/devvit-ui-interactive.js +193 -0
- package/client/renderer.d.ts +3 -0
- package/client/renderer.d.ts.map +1 -0
- package/client/renderer.js +2 -0
- package/index.d.ts +2 -0
- package/index.d.ts.map +1 -0
- package/index.js +1 -0
- package/package.json +59 -0
- package/render-core.d.ts +20 -0
- package/render-core.d.ts.map +1 -0
- package/render-core.js +271 -0
- package/server/renderer.d.ts +3 -0
- package/server/renderer.d.ts.map +1 -0
- package/server/renderer.js +4 -0
- package/styles.css +13 -0
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.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Component that renders wraps a rendering of devvit ui and makes it interactive.
|
|
4
|
+
*
|
|
5
|
+
* Needs to handle
|
|
6
|
+
* 1 - not getting slotted content and IT triggers the first render
|
|
7
|
+
* 2 - getting slotted content from SSR and then replacing it when interactivity happens
|
|
8
|
+
*/
|
|
9
|
+
import { LitElement } from 'lit';
|
|
10
|
+
import { RenderResponse } from '@devvit/protos';
|
|
11
|
+
import type { CustomPost, Metadata } from '@devvit/protos';
|
|
12
|
+
import type { CommonRuntimeLike } from '@devvit/runtimes/common/runtime/CommonRuntime.js';
|
|
13
|
+
type RenderStateChange = (state: RenderResponse) => void;
|
|
14
|
+
export declare class DevvitInteractiveUI extends LitElement {
|
|
15
|
+
#private;
|
|
16
|
+
actor?: CustomPost;
|
|
17
|
+
metadata?: Metadata;
|
|
18
|
+
onRender?: RenderStateChange;
|
|
19
|
+
runtime?: CommonRuntimeLike;
|
|
20
|
+
renderResponse?: RenderResponse;
|
|
21
|
+
static get styles(): import("lit").CSSResult[];
|
|
22
|
+
connectedCallback(): void;
|
|
23
|
+
disconnectedCallback(): void;
|
|
24
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
25
|
+
}
|
|
26
|
+
export {};
|
|
27
|
+
//# sourceMappingURL=devvit-ui-interactive.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"devvit-ui-interactive.d.ts","sourceRoot":"","sources":["../../src/client/devvit-ui-interactive.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;AAEH,OAAO,EAAa,UAAU,EAAE,MAAM,KAAK,CAAC;AAG5C,OAAO,EAA4F,cAAc,EAAc,MAAM,gBAAgB,CAAC;AACtJ,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kDAAkD,CAAC;AAS1F,KAAK,iBAAiB,GAAG,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;AAKzD,qBACa,mBAAoB,SAAQ,UAAU;;IACjB,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,OAAO,CAAC,EAAE,iBAAiB,CAAC;IAI5D,cAAc,CAAC,EAAE,cAAc,CAAC;IAMhC,WAAoB,MAAM,8BAMzB;IAIQ,iBAAiB;IAQjB,oBAAoB;IAkIpB,MAAM;CAiBhB"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
var _DevvitInteractiveUI_instances, _DevvitInteractiveUI_context, _DevvitInteractiveUI_realtime, _DevvitInteractiveUI_realtimeHandle, _DevvitInteractiveUI_rerenderHandle, _DevvitInteractiveUI_onMessage, _DevvitInteractiveUI_triggerFirstRender, _DevvitInteractiveUI_requestRender, _DevvitInteractiveUI_updateSubscriptions, _DevvitInteractiveUI_handleRenderAgainFromResponse, _DevvitInteractiveUI_onClick;
|
|
2
|
+
import { __classPrivateFieldGet, __classPrivateFieldSet, __decorate, __metadata } from "tslib";
|
|
3
|
+
/**
|
|
4
|
+
* Component that renders wraps a rendering of devvit ui and makes it interactive.
|
|
5
|
+
*
|
|
6
|
+
* Needs to handle
|
|
7
|
+
* 1 - not getting slotted content and IT triggers the first render
|
|
8
|
+
* 2 - getting slotted content from SSR and then replacing it when interactivity happens
|
|
9
|
+
*/
|
|
10
|
+
/// <reference lib="dom" />
|
|
11
|
+
import { html, css, LitElement } from 'lit';
|
|
12
|
+
import { customElement, property, state } from 'lit/decorators.js';
|
|
13
|
+
import { BlocksEvent, BlocksEventType, RealtimeDefinition, RenderResponse } from '@devvit/protos';
|
|
14
|
+
import { renderRoot } from './renderer.js';
|
|
15
|
+
import tailwind from '@reddit/shreddit.styles';
|
|
16
|
+
let DevvitInteractiveUI = class DevvitInteractiveUI extends LitElement {
|
|
17
|
+
constructor() {
|
|
18
|
+
super(...arguments);
|
|
19
|
+
_DevvitInteractiveUI_instances.add(this);
|
|
20
|
+
_DevvitInteractiveUI_context.set(this, {});
|
|
21
|
+
_DevvitInteractiveUI_realtime.set(this, undefined);
|
|
22
|
+
_DevvitInteractiveUI_realtimeHandle.set(this, void 0);
|
|
23
|
+
_DevvitInteractiveUI_rerenderHandle.set(this, void 0);
|
|
24
|
+
_DevvitInteractiveUI_onMessage.set(this, (event) => {
|
|
25
|
+
console.log('got message', event);
|
|
26
|
+
const evt = BlocksEvent.fromJSON(event.data);
|
|
27
|
+
if (evt.type === BlocksEventType.USER_ACTION) {
|
|
28
|
+
const req = {
|
|
29
|
+
event: evt,
|
|
30
|
+
context: __classPrivateFieldGet(this, _DevvitInteractiveUI_context, "f"),
|
|
31
|
+
};
|
|
32
|
+
void __classPrivateFieldGet(this, _DevvitInteractiveUI_instances, "m", _DevvitInteractiveUI_requestRender).call(this, req);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
static get styles() {
|
|
37
|
+
return [
|
|
38
|
+
tailwind,
|
|
39
|
+
// dynamic mode styles only. 99% of things should come statically from tailwind
|
|
40
|
+
css ``,
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
connectedCallback() {
|
|
44
|
+
super.connectedCallback();
|
|
45
|
+
void __classPrivateFieldGet(this, _DevvitInteractiveUI_instances, "m", _DevvitInteractiveUI_triggerFirstRender).call(this);
|
|
46
|
+
window.addEventListener('message', __classPrivateFieldGet(this, _DevvitInteractiveUI_onMessage, "f"));
|
|
47
|
+
}
|
|
48
|
+
disconnectedCallback() {
|
|
49
|
+
super.disconnectedCallback();
|
|
50
|
+
console.log('disconnected from custom post preview');
|
|
51
|
+
window.removeEventListener('message', __classPrivateFieldGet(this, _DevvitInteractiveUI_onMessage, "f"));
|
|
52
|
+
}
|
|
53
|
+
render() {
|
|
54
|
+
// TODO make this handle slotted content from SSR
|
|
55
|
+
if (!this.renderResponse) {
|
|
56
|
+
// eslint-disable-next-line @reddit/i18n-shreddit/no-unwrapped-strings
|
|
57
|
+
return html `<div>Loading...</div>`;
|
|
58
|
+
}
|
|
59
|
+
const uiRoot = this.renderResponse.ui;
|
|
60
|
+
// TODO better handle case where there is no UI to render
|
|
61
|
+
if (!uiRoot) {
|
|
62
|
+
// eslint-disable-next-line @reddit/i18n-shreddit/no-unwrapped-strings
|
|
63
|
+
return html `<div>No UI to render</div>`;
|
|
64
|
+
}
|
|
65
|
+
return html `<div @click=${__classPrivateFieldGet(this, _DevvitInteractiveUI_instances, "m", _DevvitInteractiveUI_onClick)}>${renderRoot(uiRoot, this.renderResponse)}</div>`;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
_DevvitInteractiveUI_context = new WeakMap(), _DevvitInteractiveUI_realtime = new WeakMap(), _DevvitInteractiveUI_realtimeHandle = new WeakMap(), _DevvitInteractiveUI_rerenderHandle = new WeakMap(), _DevvitInteractiveUI_onMessage = new WeakMap(), _DevvitInteractiveUI_instances = new WeakSet(), _DevvitInteractiveUI_triggerFirstRender = async function _DevvitInteractiveUI_triggerFirstRender() {
|
|
69
|
+
const req = {
|
|
70
|
+
event: {
|
|
71
|
+
type: BlocksEventType.INITIAL_RENDER,
|
|
72
|
+
key: "initial-render",
|
|
73
|
+
data: {},
|
|
74
|
+
},
|
|
75
|
+
context: {},
|
|
76
|
+
};
|
|
77
|
+
await __classPrivateFieldGet(this, _DevvitInteractiveUI_instances, "m", _DevvitInteractiveUI_requestRender).call(this, req);
|
|
78
|
+
}, _DevvitInteractiveUI_requestRender =
|
|
79
|
+
// TODO block on only one render request going through at a time
|
|
80
|
+
async function _DevvitInteractiveUI_requestRender(req) {
|
|
81
|
+
try {
|
|
82
|
+
const response = await this.actor?.Render(req, this.metadata);
|
|
83
|
+
if (response) {
|
|
84
|
+
__classPrivateFieldSet(this, _DevvitInteractiveUI_context, response.context, "f");
|
|
85
|
+
this.renderResponse = response;
|
|
86
|
+
__classPrivateFieldGet(this, _DevvitInteractiveUI_instances, "m", _DevvitInteractiveUI_handleRenderAgainFromResponse).call(this, response);
|
|
87
|
+
__classPrivateFieldGet(this, _DevvitInteractiveUI_instances, "m", _DevvitInteractiveUI_updateSubscriptions).call(this, response?.subscriptions || []);
|
|
88
|
+
this.onRender?.(response);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
console.warn('No response from custom post');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
console.error('Error while running custom post', err);
|
|
96
|
+
}
|
|
97
|
+
}, _DevvitInteractiveUI_updateSubscriptions = function _DevvitInteractiveUI_updateSubscriptions(subscriptions) {
|
|
98
|
+
if (!__classPrivateFieldGet(this, _DevvitInteractiveUI_realtime, "f")) {
|
|
99
|
+
if (this.runtime) {
|
|
100
|
+
__classPrivateFieldSet(this, _DevvitInteractiveUI_realtime, this.runtime.getPlugin(RealtimeDefinition), "f");
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
console.error('No runtime found');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (__classPrivateFieldGet(this, _DevvitInteractiveUI_realtimeHandle, "f")) {
|
|
108
|
+
__classPrivateFieldGet(this, _DevvitInteractiveUI_realtimeHandle, "f").unsubscribe();
|
|
109
|
+
}
|
|
110
|
+
if (!__classPrivateFieldGet(this, _DevvitInteractiveUI_realtime, "f")) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const obs = __classPrivateFieldGet(this, _DevvitInteractiveUI_realtime, "f").Subscribe({ channels: subscriptions });
|
|
114
|
+
__classPrivateFieldSet(this, _DevvitInteractiveUI_realtimeHandle, obs.subscribe((event) => {
|
|
115
|
+
console.log('got realtime event', event);
|
|
116
|
+
const req = {
|
|
117
|
+
event: {
|
|
118
|
+
type: BlocksEventType.SUBSCRIPTION_EVENT,
|
|
119
|
+
key: event.channel,
|
|
120
|
+
data: event.data,
|
|
121
|
+
}, context: __classPrivateFieldGet(this, _DevvitInteractiveUI_context, "f"),
|
|
122
|
+
};
|
|
123
|
+
void __classPrivateFieldGet(this, _DevvitInteractiveUI_instances, "m", _DevvitInteractiveUI_requestRender).call(this, req);
|
|
124
|
+
}), "f");
|
|
125
|
+
}, _DevvitInteractiveUI_handleRenderAgainFromResponse = function _DevvitInteractiveUI_handleRenderAgainFromResponse(response) {
|
|
126
|
+
// TODO: This could keep the app running indefinitely when you switch tabs and aren't
|
|
127
|
+
// focused on it (bad for battery / cpu).
|
|
128
|
+
if (__classPrivateFieldGet(this, _DevvitInteractiveUI_rerenderHandle, "f")) {
|
|
129
|
+
window.clearTimeout(__classPrivateFieldGet(this, _DevvitInteractiveUI_rerenderHandle, "f"));
|
|
130
|
+
__classPrivateFieldSet(this, _DevvitInteractiveUI_rerenderHandle, undefined, "f");
|
|
131
|
+
}
|
|
132
|
+
let tick = response.renderAgainInSeconds;
|
|
133
|
+
if (tick && tick > 0) {
|
|
134
|
+
tick = Math.round(tick * 1000);
|
|
135
|
+
const req = {
|
|
136
|
+
event: {
|
|
137
|
+
type: BlocksEventType.SCHEDULED_EVENT,
|
|
138
|
+
key: "scheduled-event",
|
|
139
|
+
data: {},
|
|
140
|
+
}, context: __classPrivateFieldGet(this, _DevvitInteractiveUI_context, "f"),
|
|
141
|
+
};
|
|
142
|
+
__classPrivateFieldSet(this, _DevvitInteractiveUI_rerenderHandle, window.setTimeout(() => {
|
|
143
|
+
void __classPrivateFieldGet(this, _DevvitInteractiveUI_instances, "m", _DevvitInteractiveUI_requestRender).call(this, req);
|
|
144
|
+
}, tick), "f");
|
|
145
|
+
}
|
|
146
|
+
}, _DevvitInteractiveUI_onClick = function _DevvitInteractiveUI_onClick(event) {
|
|
147
|
+
// walk the tree and find the nearest data-action-id
|
|
148
|
+
// and use that to send a new render request into the app
|
|
149
|
+
let target = event.target;
|
|
150
|
+
let actionId;
|
|
151
|
+
while (target) {
|
|
152
|
+
actionId = target.dataset.actionId;
|
|
153
|
+
if (actionId) {
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
target = target.parentElement || undefined;
|
|
157
|
+
}
|
|
158
|
+
if (actionId) {
|
|
159
|
+
const req = {
|
|
160
|
+
event: {
|
|
161
|
+
type: BlocksEventType.USER_ACTION,
|
|
162
|
+
key: actionId,
|
|
163
|
+
data: {},
|
|
164
|
+
}, context: __classPrivateFieldGet(this, _DevvitInteractiveUI_context, "f"),
|
|
165
|
+
};
|
|
166
|
+
void __classPrivateFieldGet(this, _DevvitInteractiveUI_instances, "m", _DevvitInteractiveUI_requestRender).call(this, req);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
__decorate([
|
|
170
|
+
property({ attribute: false }),
|
|
171
|
+
__metadata("design:type", Object)
|
|
172
|
+
], DevvitInteractiveUI.prototype, "actor", void 0);
|
|
173
|
+
__decorate([
|
|
174
|
+
property({ attribute: false }),
|
|
175
|
+
__metadata("design:type", Object)
|
|
176
|
+
], DevvitInteractiveUI.prototype, "metadata", void 0);
|
|
177
|
+
__decorate([
|
|
178
|
+
property({ attribute: false }),
|
|
179
|
+
__metadata("design:type", Function)
|
|
180
|
+
], DevvitInteractiveUI.prototype, "onRender", void 0);
|
|
181
|
+
__decorate([
|
|
182
|
+
property({ attribute: false }),
|
|
183
|
+
__metadata("design:type", Object)
|
|
184
|
+
], DevvitInteractiveUI.prototype, "runtime", void 0);
|
|
185
|
+
__decorate([
|
|
186
|
+
state(),
|
|
187
|
+
property({ attribute: false, reflect: true }),
|
|
188
|
+
__metadata("design:type", Object)
|
|
189
|
+
], DevvitInteractiveUI.prototype, "renderResponse", void 0);
|
|
190
|
+
DevvitInteractiveUI = __decorate([
|
|
191
|
+
customElement('devvit-ui-interactive')
|
|
192
|
+
], DevvitInteractiveUI);
|
|
193
|
+
export { DevvitInteractiveUI };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../../src/client/renderer.ts"],"names":[],"mappings":"AAAA,OAAO,kEAAkE,CAAC;AAC1E,cAAc,mBAAmB,CAAC"}
|
package/index.d.ts
ADDED
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"}
|
package/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@devvit/ui-renderer",
|
|
3
|
+
"version": "0.8.5",
|
|
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 && yarn styles:copy",
|
|
13
|
+
"clean": "rm -rf .turbo coverage dist",
|
|
14
|
+
"clobber": "yarn clean && rm -rf node_modules",
|
|
15
|
+
"dev": "yarn styles:copy && tsc -w",
|
|
16
|
+
"lint": "redlint .",
|
|
17
|
+
"lint:fix": "yarn lint --fix",
|
|
18
|
+
"prepublishOnly": "publish-package-json",
|
|
19
|
+
"styles:clean": "rm -f dist/styles.css",
|
|
20
|
+
"styles:copy": "yarn styles:clean && cp styles.css dist/styles.css",
|
|
21
|
+
"styles:dev": "echo 'todo make a filewatcher for styles.css or build it in less and use the preprocessor watch mode'",
|
|
22
|
+
"test": "yarn test:unit && yarn test:types && yarn lint",
|
|
23
|
+
"test:types": "tsc --noEmit",
|
|
24
|
+
"test:unit": "vitest run",
|
|
25
|
+
"test:unit-with-coverage": "vitest run --coverage"
|
|
26
|
+
},
|
|
27
|
+
"types": "./index.d.ts",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@devvit/protos": "0.8.5",
|
|
30
|
+
"@devvit/runtimes": "0.8.5",
|
|
31
|
+
"rxjs": "7.8.0"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@reddit/faceplate-ui": "1.0.5-6",
|
|
35
|
+
"@reddit/shreddit.styles": "^1.0.8"
|
|
36
|
+
},
|
|
37
|
+
"optionalDependencies": {
|
|
38
|
+
"@reddit/baseplate": "^0.12.1",
|
|
39
|
+
"lit": "^2.0.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@devvit/eslint-config": "0.8.5",
|
|
43
|
+
"@devvit/public-api": "0.8.5",
|
|
44
|
+
"@devvit/repo-tools": "0.8.5",
|
|
45
|
+
"@devvit/tsconfig": "0.8.5",
|
|
46
|
+
"@reddit/baseplate": "^0.12.1",
|
|
47
|
+
"@reddit/eslint-plugin-i18n-shreddit": "0.1.0",
|
|
48
|
+
"@reddit/faceplate-ui": "1.0.5-6",
|
|
49
|
+
"eslint": "8.9.0",
|
|
50
|
+
"lit": "^2.0.0",
|
|
51
|
+
"typescript": "4.9.3",
|
|
52
|
+
"vitest": "0.8.2"
|
|
53
|
+
},
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"directory": "dist"
|
|
56
|
+
},
|
|
57
|
+
"source": "./src/index.ts",
|
|
58
|
+
"gitHead": "4f506a30c6af7ad2caeb7b0192129c63d4ebbd84"
|
|
59
|
+
}
|
package/render-core.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core rendering functions for devvit UI elements.
|
|
3
|
+
*
|
|
4
|
+
* This uses @reddit/faceplate-ui's renderStrategies to render the UI. This is necessary
|
|
5
|
+
* because we need to render the UI on the server-side shreddit style, and also client-side.
|
|
6
|
+
* Examples of where this will be used:
|
|
7
|
+
* - dev-server's preview components
|
|
8
|
+
* - shreddit
|
|
9
|
+
* - d2x
|
|
10
|
+
* - other reddit clients
|
|
11
|
+
*
|
|
12
|
+
* All styling will be done with tailwind classes whenever possible.
|
|
13
|
+
*
|
|
14
|
+
* Styling not feasible with tailwind classes will be done with inline styles or css classes
|
|
15
|
+
*/
|
|
16
|
+
import { Element, RenderResponse } from '@devvit/protos';
|
|
17
|
+
import type { TemplateLike } from '@reddit/baseplate/html.js';
|
|
18
|
+
export declare const renderRoot: (element: Element, rsp: RenderResponse) => TemplateLike;
|
|
19
|
+
export declare const renderElement: (element: Element, rsp: RenderResponse) => TemplateLike;
|
|
20
|
+
//# sourceMappingURL=render-core.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render-core.d.ts","sourceRoot":"","sources":["../src/render-core.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EACL,OAAO,EAIP,cAAc,EAKf,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAI9D,eAAO,MAAM,UAAU,YAAa,OAAO,OAAO,cAAc,KAAG,YAKlE,CAAC;AAEF,eAAO,MAAM,aAAa,YAAa,OAAO,OAAO,cAAc,KAAG,YAoBrE,CAAC"}
|
package/render-core.js
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/* eslint-disable @reddit/i18n-shreddit/no-unwrapped-strings */
|
|
2
|
+
/**
|
|
3
|
+
* Core rendering functions for devvit UI elements.
|
|
4
|
+
*
|
|
5
|
+
* This uses @reddit/faceplate-ui's renderStrategies to render the UI. This is necessary
|
|
6
|
+
* because we need to render the UI on the server-side shreddit style, and also client-side.
|
|
7
|
+
* Examples of where this will be used:
|
|
8
|
+
* - dev-server's preview components
|
|
9
|
+
* - shreddit
|
|
10
|
+
* - d2x
|
|
11
|
+
* - other reddit clients
|
|
12
|
+
*
|
|
13
|
+
* All styling will be done with tailwind classes whenever possible.
|
|
14
|
+
*
|
|
15
|
+
* Styling not feasible with tailwind classes will be done with inline styles or css classes
|
|
16
|
+
*/
|
|
17
|
+
import { ElementType, ObjectFit, Padding, StackAlign, StackDirection, StackRoundedCornerSize, TextAlign, } from '@devvit/protos';
|
|
18
|
+
import { getTemplateRenderingStrategy } from '@reddit/faceplate-ui/faceplateUIConfig.js';
|
|
19
|
+
export const renderRoot = (element, rsp) => {
|
|
20
|
+
const { html } = getTemplateRenderingStrategy();
|
|
21
|
+
return html `<div class="font-semibold p-sm text-12 font-sans tracking-tight leading-5">
|
|
22
|
+
${renderElement(element, rsp)}
|
|
23
|
+
</div>`;
|
|
24
|
+
};
|
|
25
|
+
export const renderElement = (element, rsp) => {
|
|
26
|
+
switch (element.type) {
|
|
27
|
+
case ElementType.STACK:
|
|
28
|
+
return renderStack(element, rsp);
|
|
29
|
+
case ElementType.IMAGE:
|
|
30
|
+
return renderImage(element);
|
|
31
|
+
case ElementType.BUTTON:
|
|
32
|
+
return renderButton(element);
|
|
33
|
+
case ElementType.SPACER:
|
|
34
|
+
return renderSpacer(element);
|
|
35
|
+
case ElementType.FRAGMENT:
|
|
36
|
+
return renderFragment(element, rsp);
|
|
37
|
+
case ElementType.TEXT:
|
|
38
|
+
return renderText(element);
|
|
39
|
+
case ElementType.WEB_VIEW:
|
|
40
|
+
return renderWebView(element, rsp);
|
|
41
|
+
}
|
|
42
|
+
const { html } = getTemplateRenderingStrategy();
|
|
43
|
+
return html `DEBUG ME, unknown element type: ${element.type}`;
|
|
44
|
+
};
|
|
45
|
+
const refBySrc = {};
|
|
46
|
+
const renderWebView = (element, rsp) => {
|
|
47
|
+
const { html, styleMap } = getTemplateRenderingStrategy();
|
|
48
|
+
console.log('rendering webview', element, rsp);
|
|
49
|
+
postMessageToIframe(element.src, rsp);
|
|
50
|
+
return html `<iframe
|
|
51
|
+
class="border-box ${grow(element)}"
|
|
52
|
+
@load="${(evt) => { refBySrc[element.src] = evt.target; postMessageToIframe(element.src, rsp); }}"
|
|
53
|
+
ref=${(el) => (refBySrc[element.src] = el)}
|
|
54
|
+
sandbox="allow-scripts"
|
|
55
|
+
src=${element.src}
|
|
56
|
+
style=${styleMap({
|
|
57
|
+
backgroundColor: element.backgroundColor,
|
|
58
|
+
borderColor: element.borderColor,
|
|
59
|
+
borderWidth: element.borderColor ? '1px' : "0",
|
|
60
|
+
borderRadius: element.roundedCornerSize
|
|
61
|
+
? `${element.roundedCornerSize}px`
|
|
62
|
+
: undefined,
|
|
63
|
+
})}
|
|
64
|
+
></iframe>`;
|
|
65
|
+
};
|
|
66
|
+
const postMessageToIframe = (src, rsp) => {
|
|
67
|
+
const iframe = refBySrc[src];
|
|
68
|
+
console.log('posting message to iframe', iframe, rsp);
|
|
69
|
+
iframe?.contentWindow?.postMessage(rsp, '*');
|
|
70
|
+
};
|
|
71
|
+
const renderStack = (element, rsp) => {
|
|
72
|
+
const layoutClass = getStackLayoutClass(element);
|
|
73
|
+
const flexDirection = getFlexDirectionClass(element);
|
|
74
|
+
const justifyContent = justifyContentClass(element);
|
|
75
|
+
const alignContent = alignContentClass(element.crossAlign);
|
|
76
|
+
const borderRadiusClass = getBorderRadiusClass(element.roundedCornerSize);
|
|
77
|
+
const paddingClass = getPaddingClass(element);
|
|
78
|
+
const style = {
|
|
79
|
+
backgroundColor: element.backgroundColor,
|
|
80
|
+
border: element.borderColor && `1px ${element.borderColor} solid`,
|
|
81
|
+
};
|
|
82
|
+
if (element.sizePercent) {
|
|
83
|
+
if (element.stackDirection === StackDirection.VERTICAL) {
|
|
84
|
+
style.height = `${element.sizePercent}%`;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
style.width = `${element.sizePercent}%`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const { html, repeat, styleMap } = getTemplateRenderingStrategy();
|
|
91
|
+
return html `<div
|
|
92
|
+
class="border-box ${layoutClass} ${grow(element)} ${flexDirection} ${alignContent} ${justifyContent} ${borderRadiusClass} ${paddingClass}"
|
|
93
|
+
style=${styleMap(style)}
|
|
94
|
+
data-action-id=${element.actionId}
|
|
95
|
+
>
|
|
96
|
+
${repeat(element.children, (e) => renderElement(e, rsp))}
|
|
97
|
+
</div>`;
|
|
98
|
+
};
|
|
99
|
+
const grow = (element) => {
|
|
100
|
+
return element.grow ? 'grow' : '';
|
|
101
|
+
};
|
|
102
|
+
const getPaddingClass = (element) => {
|
|
103
|
+
if (element.stackDirection === StackDirection.DEPTH) {
|
|
104
|
+
return '';
|
|
105
|
+
}
|
|
106
|
+
if (element.padding === Padding.PADDING_LARGE) {
|
|
107
|
+
return 'p-lg';
|
|
108
|
+
}
|
|
109
|
+
else if (element.padding === Padding.PADDING_MEDIUM) {
|
|
110
|
+
return 'p-md';
|
|
111
|
+
}
|
|
112
|
+
else if (element.padding === Padding.PADDING_SMALL) {
|
|
113
|
+
return 'p-sm';
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
return '';
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
const getFlexDirectionClass = (element) => {
|
|
120
|
+
let flexDirection = 'flex-row';
|
|
121
|
+
if (element.stackDirection === StackDirection.VERTICAL) {
|
|
122
|
+
if (element.stackReverse) {
|
|
123
|
+
flexDirection = 'flex-col-reverse';
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
flexDirection = 'flex-col';
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else if (element.stackDirection === StackDirection.DEPTH) {
|
|
130
|
+
flexDirection = 'devvit-depth';
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
if (element.stackReverse) {
|
|
134
|
+
flexDirection = 'flex-row-reverse';
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return flexDirection;
|
|
138
|
+
};
|
|
139
|
+
const getStackLayoutClass = (element) => {
|
|
140
|
+
if (element.stackDirection === StackDirection.DEPTH) {
|
|
141
|
+
return 'devvit-depth';
|
|
142
|
+
}
|
|
143
|
+
return 'flex';
|
|
144
|
+
};
|
|
145
|
+
const justifyContentClass = (element) => {
|
|
146
|
+
const zStack = element.stackDirection === StackDirection.DEPTH;
|
|
147
|
+
switch (element.stackAlign) {
|
|
148
|
+
case StackAlign.STACK_START:
|
|
149
|
+
return zStack ? 'justify-items-start' : 'justify-start';
|
|
150
|
+
case StackAlign.STACK_END:
|
|
151
|
+
return zStack ? 'justify-items-end' : `justify-end`;
|
|
152
|
+
case StackAlign.STACK_CENTER:
|
|
153
|
+
return zStack ? 'justify-items-center' : `justify-center`;
|
|
154
|
+
case undefined:
|
|
155
|
+
default:
|
|
156
|
+
return zStack ? '' : 'justify-center';
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
const alignContentClass = (align) => {
|
|
160
|
+
switch (align) {
|
|
161
|
+
case StackAlign.STACK_START:
|
|
162
|
+
return 'items-start';
|
|
163
|
+
case StackAlign.STACK_END:
|
|
164
|
+
return 'items-end';
|
|
165
|
+
case StackAlign.STACK_CENTER:
|
|
166
|
+
return 'items-center';
|
|
167
|
+
case undefined:
|
|
168
|
+
default:
|
|
169
|
+
return 'items-stretch';
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const getBorderRadiusClass = (rounding) => {
|
|
173
|
+
switch (rounding) {
|
|
174
|
+
case StackRoundedCornerSize.CORNER_NONE:
|
|
175
|
+
return 'rounded-none';
|
|
176
|
+
case StackRoundedCornerSize.CORNER_SMALL:
|
|
177
|
+
return 'rounded-sm';
|
|
178
|
+
case StackRoundedCornerSize.CORNER_MEDIUM:
|
|
179
|
+
// TODO this might not work in faceplate's tailwind config
|
|
180
|
+
// see https://faceplate-ui.snooguts.net/tailwind/#Border%20Radius
|
|
181
|
+
return 'rounded-md';
|
|
182
|
+
case StackRoundedCornerSize.CORNER_LARGE:
|
|
183
|
+
return 'rounded-lg';
|
|
184
|
+
case StackRoundedCornerSize.CORNER_FULL:
|
|
185
|
+
return 'rounded-full';
|
|
186
|
+
default:
|
|
187
|
+
return 'rounded-none';
|
|
188
|
+
}
|
|
189
|
+
return '';
|
|
190
|
+
};
|
|
191
|
+
const renderImage = (element) => {
|
|
192
|
+
const { html } = getTemplateRenderingStrategy();
|
|
193
|
+
return html `<img
|
|
194
|
+
referrerpolicy="no-referrer"
|
|
195
|
+
crossorigin="anonymous"
|
|
196
|
+
loading="lazy"
|
|
197
|
+
class="${getObjectFitClass(element.objectFit)} ${grow(element)}"
|
|
198
|
+
src=${element.src}
|
|
199
|
+
data-action-id=${element.actionId}
|
|
200
|
+
/>`;
|
|
201
|
+
};
|
|
202
|
+
const getObjectFitClass = (fit) => {
|
|
203
|
+
switch (fit) {
|
|
204
|
+
case ObjectFit.CONTAIN:
|
|
205
|
+
return 'object-contain';
|
|
206
|
+
case ObjectFit.COVER:
|
|
207
|
+
return 'object-cover';
|
|
208
|
+
case ObjectFit.FILL:
|
|
209
|
+
return 'object-fill';
|
|
210
|
+
case ObjectFit.SCALE_DOWN:
|
|
211
|
+
return 'object-scale-down';
|
|
212
|
+
case ObjectFit.NONE:
|
|
213
|
+
return 'object-none';
|
|
214
|
+
}
|
|
215
|
+
return '';
|
|
216
|
+
};
|
|
217
|
+
const renderButton = (element) => {
|
|
218
|
+
// TODO improve button rendering
|
|
219
|
+
// TODO figure out how to dynamically pick the client vs server faceplate template
|
|
220
|
+
const { html } = getTemplateRenderingStrategy();
|
|
221
|
+
return html `<button class="${grow(element)}" data-action-id=${element.actionId}>
|
|
222
|
+
${element.text}
|
|
223
|
+
</button>`;
|
|
224
|
+
};
|
|
225
|
+
const getSpacerPaddingClass = (element) => {
|
|
226
|
+
if (element.padding === Padding.PADDING_LARGE) {
|
|
227
|
+
return 'pr-lg pb-lg';
|
|
228
|
+
}
|
|
229
|
+
else if (element.padding === Padding.PADDING_MEDIUM) {
|
|
230
|
+
return 'pr-md pb-md';
|
|
231
|
+
}
|
|
232
|
+
else if (element.padding === Padding.PADDING_SMALL) {
|
|
233
|
+
return 'pr-sm pb-sm';
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
return '';
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
const renderSpacer = (element) => {
|
|
240
|
+
const paddingClass = getSpacerPaddingClass(element);
|
|
241
|
+
const { html } = getTemplateRenderingStrategy();
|
|
242
|
+
return html `<div class="${grow(element)} ${paddingClass}"></div>`;
|
|
243
|
+
};
|
|
244
|
+
const renderFragment = (element, rsp) => {
|
|
245
|
+
const { html, repeat } = getTemplateRenderingStrategy();
|
|
246
|
+
return html `${repeat(element.children, (e) => renderElement(e, rsp))})}`;
|
|
247
|
+
};
|
|
248
|
+
const renderText = (element) => {
|
|
249
|
+
const style = {
|
|
250
|
+
fontSize: element.fontSize && element.fontSize !== 0 ? `${element.fontSize}em` : undefined,
|
|
251
|
+
color: element.color,
|
|
252
|
+
width: element.sizePercent ? `${element.sizePercent}%` : undefined,
|
|
253
|
+
};
|
|
254
|
+
const textAlignClass = getTextAlignClass(element.textAlign);
|
|
255
|
+
const { html, styleMap } = getTemplateRenderingStrategy();
|
|
256
|
+
return html `<div class="${textAlignClass} ${grow(element)}" style=${styleMap(style)}>
|
|
257
|
+
${element.text}
|
|
258
|
+
</div>`;
|
|
259
|
+
};
|
|
260
|
+
const getTextAlignClass = (align) => {
|
|
261
|
+
switch (align) {
|
|
262
|
+
case TextAlign.TEXT_ALIGN_CENTER:
|
|
263
|
+
return 'text-center';
|
|
264
|
+
case TextAlign.TEXT_ALIGN_END:
|
|
265
|
+
return 'text-end';
|
|
266
|
+
case TextAlign.TEXT_ALIGN_START:
|
|
267
|
+
case undefined:
|
|
268
|
+
default:
|
|
269
|
+
return 'text-start';
|
|
270
|
+
}
|
|
271
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../../src/server/renderer.ts"],"names":[],"mappings":"AAAA,OAAO,kEAAkE,CAAC;AAC1E,cAAc,mBAAmB,CAAC"}
|
package/styles.css
ADDED