diff --git a/.gitignore b/.gitignore index 235067b..201facc 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ yarn-debug.log* yarn-error.log* .DS_Store webroot -dist \ No newline at end of file +dist +bin \ No newline at end of file diff --git a/build-server.sh b/build-server.sh new file mode 100755 index 0000000..b77b2e3 --- /dev/null +++ b/build-server.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SERVER_JS_OUTPUT=bin/index.cjs +OUTPUT_BUNDLE=dist/main.bundle.json + +# This should spit out a bundle using the classic bundler approach +npx devvit bundle actor main + +# Build the server bundle using esbuild +./esbuild.mjs + +# Create a temporary directory +TEMP_DIR=$(mktemp -d) +trap 'rm -rf "$TEMP_DIR"' EXIT + +jq --rawfile code "$SERVER_JS_OUTPUT" \ + --rawfile sourcemap "${SERVER_JS_OUTPUT}.map" \ + '.standaloneServerCode = $code | .standaloneServerSourceMap = $sourcemap | .actor.name = "main" | .actor.version = "0.0.0"' \ + ${OUTPUT_BUNDLE} > bin/bundle.combined.json + +echo "Bundle created at bin/bundle.combined.json" + diff --git a/bundle.server.template.json b/bundle.server.template.json new file mode 100644 index 0000000..1f12505 --- /dev/null +++ b/bundle.server.template.json @@ -0,0 +1,99 @@ +{ + "actor": { + "name": "main", + "owner": "snoo", + "version": "0.0.0" + }, + "code": "", + "sourceMap": "", + "devvitJson": { "name": "abc", "server": {} }, + "hostname": "main.local", + "provides": [ + { + "fullName": "devvit.actor.hello.Hello", + "methods": [ + { + "fullName": "/devvit.actor.hello.Hello/Ping", + "name": "Ping", + "requestType": "devvit.actor.hello.PingMessage", + "responseType": "devvit.actor.hello.PingMessage" + } + ], + "name": "Hello" + } + ], + "uses": [ + { + "actor": { + "name": "default", + "owner": "devvit", + "version": "1.0.0" + }, + "hostname": "wrappertypes.plugins.local", + "provides": [ + { + "fullName": "devvit.actor.test.WrapperTypes", + "methods": [ + { + "fullName": "/devvit.actor.test.WrapperTypes/StringRequest", + "name": "StringRequest", + "requestType": "google.protobuf.StringValue", + "responseType": "google.protobuf.StringValue" + }, + { + "fullName": "/devvit.actor.test.WrapperTypes/BoolRequest", + "name": "BoolRequest", + "requestType": "google.protobuf.BoolValue", + "responseType": "google.protobuf.BoolValue" + }, + { + "fullName": "/devvit.actor.test.WrapperTypes/Int32Request", + "name": "Int32Request", + "requestType": "google.protobuf.Int32Value", + "responseType": "google.protobuf.Int32Value" + }, + { + "fullName": "/devvit.actor.test.WrapperTypes/UInt32Request", + "name": "UInt32Request", + "requestType": "google.protobuf.UInt32Value", + "responseType": "google.protobuf.UInt32Value" + }, + { + "fullName": "/devvit.actor.test.WrapperTypes/Int64Request", + "name": "Int64Request", + "requestType": "google.protobuf.Int64Value", + "responseType": "google.protobuf.Int64Value" + }, + { + "fullName": "/devvit.actor.test.WrapperTypes/UInt64Request", + "name": "UInt64Request", + "requestType": "google.protobuf.UInt64Value", + "responseType": "google.protobuf.UInt64Value" + }, + { + "fullName": "/devvit.actor.test.WrapperTypes/FloatRequest", + "name": "FloatRequest", + "requestType": "google.protobuf.FloatValue", + "responseType": "google.protobuf.FloatValue" + }, + { + "fullName": "/devvit.actor.test.WrapperTypes/DoubleRequest", + "name": "DoubleRequest", + "requestType": "google.protobuf.DoubleValue", + "responseType": "google.protobuf.DoubleValue" + } + ], + "name": "WrapperTypes" + } + ] + } + ], + "buildInfo": { + "created": "2025-03-17T17:25:08.874Z", + "dependencies": { + "node": "22.6.0", + "@devvit/protos": "0.11.11-dev", + "@devvit/public-api": "0.11.11-dev" + } + } +} diff --git a/devvit.yaml b/devvit.yaml index c285ec0..fdc0d54 100644 --- a/devvit.yaml +++ b/devvit.yaml @@ -1,2 +1,2 @@ -name: YOUR_APP_NAME -version: 0.0.0.0 +name: ss-webbit-tests +version: 0.0.1.5 diff --git a/esbuild.mjs b/esbuild.mjs new file mode 100755 index 0000000..3200be4 --- /dev/null +++ b/esbuild.mjs @@ -0,0 +1,53 @@ +#!/usr/bin/env node + +/* eslint-disable no-console */ +import * as esbuild from "esbuild"; +import { nodeExternalsPlugin } from "esbuild-node-externals"; + +import path from "path"; + +/** + * @param {boolean} watch + * @returns {Promise> | undefined} + */ +async function build(watch) { + /** @type {esbuild.BuildOptions} */ + const opts = { + entryPoints: ["src/server/index.ts"], + outdir: "bin", + format: "cjs", + entryNames: "[name]", + outExtension: { ".js": ".cjs" }, + plugins: [ + nodeExternalsPlugin({ + packagePath: path.resolve("./package.json"), + allowList: [ + 'express', + /^@devvit\// + ], + }), + ], + sourcemap: true, + bundle: true, + platform: "node", + }; + + if (!watch) { + await esbuild.build(opts); + console.log( + `[esbuild/server] Build finished. Output is in ${path.resolve("bin")}` + ); + return; + } + + console.log("[esbuild/server] Starting watch mode..."); + const ctx = await esbuild.context(opts); + await ctx.watch(); +} + +async function main() { + const enableWatchMode = process.argv.includes("--watch"); + await build(enableWatchMode); +} + +await main(); diff --git a/package.json b/package.json index 58d0719..fc41cf9 100644 --- a/package.json +++ b/package.json @@ -1,29 +1,34 @@ { "private": true, - "name": "devvit-hello-world-experimental", + "name": "ss-webbit-testing", "version": "0.0.0", "license": "BSD-3-Clause", "type": "module", "scripts": { "build:client": "cd src/client && vite build", + "build:server": "./build-server.sh", "deploy": "npm run build:client && devvit upload", "dev": "concurrently -p \"[{name}]\" -n \"VITE,DEVVIT,GAME\" -c \"blue,green,magenta\" \"npm run dev:vite\" \"npm run dev:devvit\" \"npm run dev:client\" --restart-tries 2", "dev:client": "cd src/client && vite build --watch", - "dev:devvit": "devvit playtest YOUR_SUBREDDIT_NAME", + "dev:devvit": "MY_PORTAL=0 mydevvit playtest SnooSnekTest", "dev:vite": "cd src/client && vite --port 7474", "login": "devvit login", "type-check": "tsc --build" }, "dependencies": { - "@devvit/client": "next", - "@devvit/public-api": "next", - "@devvit/server": "next", - "devvit": "next", + "@devvit/client": "../../devvit/packages/client", + "@devvit/public-api": "../../devvit/packages/public-api", + "@devvit/server": "../../devvit/packages/server", + "@devvit/redis": "../../devvit/packages/redis", + "@devvit/reddit": "../../devvit/packages/reddit", + "devvit": "../../devvit/packages/devvit", "express": "5.1.0" }, "devDependencies": { "@types/express": "5.0.1", "concurrently": "9.1.2", + "esbuild": "0.23.0", + "esbuild-node-externals": "1.15.0", "typescript": "5.8.2", "vite": "6.2.4" } diff --git a/src/devvit/main.tsx b/src/devvit/main.tsx index 72a7c32..0e5cf76 100644 --- a/src/devvit/main.tsx +++ b/src/devvit/main.tsx @@ -1,19 +1,36 @@ import { Devvit } from "@devvit/public-api"; // Side effect import to bundle the server. The /index is required for server splitting. -import "../server/index"; -import { defineConfig } from "@devvit/server"; +// import "../server/index"; +// import { defineConfig } from "@devvit/server"; -defineConfig({ +// defineConfig({ +// name: "Hello World", +// description: "Hello World", +// entry: "index.html", +// height: "tall", +// inline: true, +// menu: { +// enable: true, +// label: "[Webbit] New Hello World Post", +// postTitle: "Hello World", +// }, +// }); + +Devvit.addCustomPostType({ name: "Hello World", - description: "Hello World", - entry: "index.html", - height: "tall", - inline: true, - menu: { - enable: true, - label: "New Hello World Post", - postTitle: "Hello World", + description: "A simple hello world post type", + render: (context) => { + if (context.postId === "t3_THROW_ERROR") { + throw new Error("Test error."); + } + + console.log("Post ID!!:", context.postId); + console.log("App Name!!:", context.appName); + console.log("User ID!!:", context.userId); + console.log("Subreddit ID!!:", context.subredditId); + console.log("App version!!:", context.appVersion); + return ; }, }); diff --git a/src/server/index.ts b/src/server/index.ts index 9516885..956b8ca 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,106 +1,160 @@ import express from "express"; -import { createServer } from "http"; -import { devvitMiddleware } from "./middleware"; +import type { Request, Response } from "express"; +import { getReddit } from "@devvit/reddit"; +import { getRedis } from "@devvit/redis"; +import { + createServer, + getContext, + getServerPort, + webbitEnable, +} from "@devvit/server"; + import { InitResponse, IncrementResponse, DecrementResponse, } from "../shared/types/api"; -const app = express(); +const router = express.Router(); -// Middleware for JSON body parsing -app.use(express.json()); -// Middleware for URL-encoded body parsing -app.use(express.urlencoded({ extended: true })); -// Middleware for plain text body parsing -app.use(express.text()); +router.get( + "/api/init", + async (_req, res: Response): Promise => { + const context = getContext(); + const redis = getRedis(); + const postId = context.postId; -// Apply Devvit middleware -app.use(devvitMiddleware); + if (!postId) { + console.error("API Init Error: postId not found in devvit context"); + res.status(400).json({ + status: "error", + message: "postId is required but missing from context", + }); + return; + } -const router = express.Router(); + const user = await getReddit().getCurrentUser(); + console.log("API Init for user: ", user?.username); -router.get< - { postId: string }, - InitResponse | { status: string; message: string } ->("/api/init", async (req, res): Promise => { - const { postId } = req.devvit; - - if (!postId) { - console.error("API Init Error: postId not found in devvit context"); - res.status(400).json({ - status: "error", - message: "postId is required but missing from context", - }); - return; + try { + const count = await redis.get("count"); + res.json({ + type: "init", + postId: postId, + count: count ? parseInt(count) : 0, + }); + } catch (error) { + console.error(`API Init Error for post ${postId}:`, error); + let errorMessage = "Unknown error during initialization"; + if (error instanceof Error) { + errorMessage = `Initialization failed: ${error.message}`; + } + res.status(400).json({ status: "error", message: errorMessage }); + } } +); + +router.post( + "/api/increment", + async (_req, res: Response): Promise => { + const context = getContext(); + const redis = getRedis(); + const postId = context.postId; + + if (!postId) { + res.status(400).json({ + status: "error", + message: "postId is required", + }); + return; + } - try { - const count = await req.devvit.redis.get("count"); res.json({ - type: "init", - postId: postId, - count: count ? parseInt(count) : 0, + count: await redis.incrby("count", 1), + postId, + type: "increment", }); - } catch (error) { - console.error(`API Init Error for post ${postId}:`, error); - let errorMessage = "Unknown error during initialization"; - if (error instanceof Error) { - errorMessage = `Initialization failed: ${error.message}`; - } - res.status(400).json({ status: "error", message: errorMessage }); } -}); +); + +router.post( + "/api/decrement", + async (_req, res: Response): Promise => { + const context = getContext(); + const redis = getRedis(); + const postId = context.postId; + if (!postId) { + res.status(400).json({ + status: "error", + message: "postId is required", + }); + return; + } -router.post< - { postId: string }, - IncrementResponse | { status: string; message: string }, - unknown ->("/api/increment", async (req, res): Promise => { - const { postId } = req.devvit; - if (!postId) { - res.status(400).json({ - status: "error", - message: "postId is required", + res.json({ + count: await redis.incrby("count", -1), + postId, + type: "decrement", }); - return; } +); - res.json({ - count: await req.devvit.redis.incrBy("count", 1), - postId, - type: "increment", - }); -}); +router.get( + "/api/hello", + async ( + req: Request, + res: Response< + { message: string; status: string } | { postId: string; message: string } + > + ): Promise => { + const context = getContext(); + const postId = context.postId; + if (!postId) { + res.status(400).json({ + status: "error", + message: "postId is required", + }); + return; + } -router.post< - { postId: string }, - DecrementResponse | { status: string; message: string }, - unknown ->("/api/decrement", async (req, res): Promise => { - const { postId } = req.devvit; - if (!postId) { - res.status(400).json({ - status: "error", - message: "postId is required", - }); - return; + const message = + typeof req.query["message"] === "string" ? req.query["message"] : ""; + res.json({ message, postId }); } +); - res.json({ - count: await req.devvit.redis.incrBy("count", -1), - postId, - type: "decrement", - }); -}); +router.get( + "/api/error", + async ( + _req: Request, + _res: Response< + { message: string; status: string } | { postId: string; message: string } + > + ): Promise => { + const context = getContext(); + const postId = context.postId; + throw new Error("This is a test error from the API: " + postId); + } +); -// Use router middleware +const app = express(); app.use(router); -// Get port from environment variable with fallback -const port = process.env.WEBBIT_PORT || 3000; - const server = createServer(app); server.on("error", (err) => console.error(`server error; ${err.stack}`)); -server.listen(port, () => console.log(`http://localhost:${port}`)); +server.listen(getServerPort(), () => { + const addr = server.address(); + if (addr === null) { + console.log("Server address is null, but I'm listening!"); + return; + } + if (typeof addr === "string") { + console.log(`Server is listening on ${addr}`); + return; + } + if (typeof addr === "object") { + console.log(`Server is listening on ${addr.address}:${addr.port}`); + return; + } + console.error("...the hell is it doing?"); +}); diff --git a/src/server/middleware.ts b/src/server/middleware.ts deleted file mode 100644 index 9c66e8b..0000000 --- a/src/server/middleware.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { RequestContext } from '@devvit/server'; -import { Request, Response, NextFunction } from 'express'; -import { Devvit } from '@devvit/public-api'; - -// Extend Express Request type to include devvit context -declare global { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace Express { - export interface Request { - devvit: Devvit.Context; // Make it non-optional, middleware should add it - } - } -} - -// Create the middleware function -export function devvitMiddleware(req: Request, _res: Response, next: NextFunction): void { - // Add the devvit property to the request - req.devvit = RequestContext(req.headers) as Devvit.Context; - next(); -} diff --git a/src/shared/types/api.ts b/src/shared/types/api.ts index 61a0355..b1087aa 100644 --- a/src/shared/types/api.ts +++ b/src/shared/types/api.ts @@ -1,17 +1,22 @@ -export type InitResponse = { +export type InitResponse = ErrorResponse | { type: "init"; postId: string; count: number; }; -export type IncrementResponse = { +export type IncrementResponse = ErrorResponse | { type: "increment"; postId: string; count: number; }; -export type DecrementResponse = { +export type DecrementResponse = ErrorResponse | { type: "decrement"; postId: string; count: number; }; + +export type ErrorResponse = { + status: "error"; + message: string; +};