Initial GameCult Bifrost Reddit bridge

This commit is contained in:
Meta Vens 2026-06-09 01:58:22 +01:00
commit 9a89410d90
26 changed files with 6765 additions and 0 deletions

18
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,18 @@
# Dependabot configuration to update npm packages weekly
version: 2
updates:
- package-ecosystem: "npm"
directory: "/" # Location of your package.json and package-lock.json
schedule:
interval: "cron"
cronjob: "0 17 * * 1,2" # At 5pm on Monday and Tuesday - https://crontab.guru/
timezone: "America/New_York"
open-pull-requests-limit: 10 # Optional: prevents PR spam by limiting active updates
ignore:
- dependency-name: "@types/node"
update-types: ["version-update:semver-major"]
groups:
devvit:
patterns:
- "devvit"
- "@devvit/*"

7
.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
node_modules
.DS_Store
dist
.env
public/*.js
public/*.js.map
devvit-playtest*.log

1
.npmrc Normal file
View file

@ -0,0 +1 @@
engine-strict=true

1
.nvmrc Normal file
View file

@ -0,0 +1 @@
22.6.0

26
LICENSE Normal file
View file

@ -0,0 +1,26 @@
# License (BSD-3-Clause)
Copyright (c) 2025 Reddit Inc.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

75
README.md Normal file
View file

@ -0,0 +1,75 @@
# GameCult Bifrost Reddit Bridge
Devvit app for the Reddit-side actuator of Bifrost public transport.
Bifrost owns governed GameCult crossings. This app gives Bifrost a Reddit-native
body that can create subreddit posts and comments through Devvit, then return
receipt-shaped JSON for the canonical Bifrost topic/comment store.
## Authority
- Owner: Bifrost.
- Provider body: Reddit Devvit app `GameCult-Bifrost`.
- Current app directory: `E:\Projects\gamecult-bifrost`.
- Canonical discussion/approval: Bifrost governance topics.
- Public action: Reddit posts and comments executed as the Devvit app account.
- Receipt: `bifrost.reddit.receipt` JSON returned by the bridge endpoint.
Personas should not call Reddit directly. They create Bifrost-governed intents;
Bifrost routes authorized Reddit crossings through this app and records the
permalink receipt back on the canonical topic.
## Endpoints
- `GET /api/health`
- `POST /internal/bifrost/reddit-post`
- `POST /internal/bifrost/reddit-comment`
- `POST /internal/menu/post-create`
- `POST /internal/on-app-install`
Set `BIFROST_REDDIT_BRIDGE_SECRET` to require callers to send
`x-bifrost-bridge-secret`. The secret check is deliberately small because
Heimdall should own long-term credential custody and capability claims.
### Reddit Post Request
```json
{
"actor": "nibu",
"source": "bifrost-topic:topic_...",
"subredditName": "GameCult",
"title": "Thread title",
"text": "Markdown body",
"idempotencyKey": "optional stable key"
}
```
### Reddit Comment Request
```json
{
"actor": "nibu",
"source": "bifrost-topic-comment:comment_...",
"parentId": "t3_...",
"text": "Markdown body",
"idempotencyKey": "optional stable key"
}
```
## Commands
```powershell
npm run type-check
npm run build
npm run dev
npm run deploy
```
`npm run dev` starts a Reddit playtest session. `npm run deploy` builds and
uploads a new app version.
## Next Cut
Wire the main `E:\Projects\Bifrost` bridge tooling to enqueue or invoke these
Reddit actions from canonical governance topic comments, then store the returned
`bifrost.reddit.receipt` on the topic as the public proof.

41
devvit.json Normal file
View file

@ -0,0 +1,41 @@
{
"$schema": "https://developers.reddit.com/schema/config-file.v1.json",
"name": "gamecult-bifrost",
"post": {
"dir": "public",
"entrypoints": {
"default": {
"inline": true,
"entry": "splash.html"
},
"game": {
"entry": "game.html"
}
}
},
"server": {
"dir": "dist/server",
"entry": "index.js"
},
"menu": {
"items": [
{
"label": "Create Bifrost bridge hub",
"description": "Create a Bifrost-owned Reddit bridge status post",
"location": "subreddit",
"forUserType": "moderator",
"endpoint": "/internal/menu/post-create"
}
]
},
"triggers": {
"onAppInstall": "/internal/on-app-install"
},
"scripts": {
"dev": "node --experimental-strip-types ./tools/build.ts --watch",
"build": "node --experimental-strip-types ./tools/build.ts --minify"
},
"dev": {
"subreddit": "gamecult_bifrost_dev"
}
}

5851
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

27
package.json Normal file
View file

@ -0,0 +1,27 @@
{
"private": true,
"name": "gamecult-bifrost",
"version": "0.0.0",
"license": "BSD-3-Clause",
"type": "module",
"scripts": {
"build": "node --experimental-strip-types ./tools/build.ts --minify",
"deploy": "npm run build && devvit upload",
"dev": "devvit playtest",
"login": "devvit login",
"launch": "npm run build && npm run deploy && devvit publish",
"type-check": "tsc --build"
},
"engines": {
"node": ">=22.6.0"
},
"dependencies": {
"@devvit/web": "0.13.3",
"devvit": "0.13.3"
},
"devDependencies": {
"@types/node": "22.19.19",
"typescript": "6.0.3",
"esbuild": "0.28.0"
}
}

69
public/game.css Normal file
View file

@ -0,0 +1,69 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
Arial, sans-serif;
}
.app {
align-items: center;
background: #f7f3ea;
color: #171717;
display: flex;
min-height: 100vh;
justify-content: center;
padding: 24px;
}
.content {
display: grid;
gap: 14px;
max-width: 560px;
text-align: left;
width: min(100%, 560px);
}
.eyebrow {
color: #8a2f1b;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0;
text-transform: uppercase;
}
.title {
font-size: 2rem;
line-height: 1.1;
}
.description {
color: #24513a;
font-size: 1rem;
font-weight: 650;
line-height: 1.45;
}
.ledger {
border-left: 3px solid #1c3f5f;
color: #3d3a35;
display: grid;
font-size: 0.92rem;
gap: 8px;
line-height: 1.35;
padding-left: 14px;
}
.footer {
bottom: 16px;
position: fixed;
}
.text-link {
background: transparent;
border: 0;
color: #1c3f5f;
cursor: pointer;
font-size: 0.82rem;
text-decoration: underline;
}

28
public/game.html Normal file
View file

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" type="text/css" href="game.css" />
<meta charset="UTF-8" />
<meta
name="viewport"
content="initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, width=device-width, height=device-height, user-scalable=0"
/>
<title>Bifrost Reddit Bridge</title>
</head>
<body class="app">
<main class="content">
<p class="eyebrow">GameCult Bifrost</p>
<h1 class="title">Reddit bridge status</h1>
<p class="description" id="bridge-status">Checking bridge actuator...</p>
<section class="ledger">
<p>Owner: Bifrost</p>
<p>Actions: reddit-post, reddit-comment</p>
<p>Receipts: returned to canonical Bifrost topics</p>
</section>
</main>
<footer class="footer">
<button class="text-link" id="docs-link">Devvit docs</button>
</footer>
<script type="module" src="game.js" defer></script>
</body>
</html>

BIN
public/snoo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

69
public/splash.css Normal file
View file

@ -0,0 +1,69 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
Arial, sans-serif;
}
.app {
align-items: center;
background: #f7f3ea;
color: #171717;
display: flex;
min-height: 100vh;
justify-content: center;
padding: 24px;
}
.content {
display: grid;
gap: 14px;
max-width: 520px;
text-align: center;
}
.eyebrow {
color: #8a2f1b;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0;
text-transform: uppercase;
}
.title {
font-size: 2rem;
line-height: 1.1;
}
.description {
color: #4c4a45;
font-size: 1rem;
line-height: 1.45;
}
.open-button {
background: #1c3f5f;
border: 0;
border-radius: 6px;
color: #ffffff;
cursor: pointer;
font-size: 0.95rem;
justify-self: center;
min-height: 36px;
padding: 8px 16px;
}
.footer {
bottom: 16px;
position: fixed;
}
.text-link {
background: transparent;
border: 0;
color: #1c3f5f;
cursor: pointer;
font-size: 0.82rem;
text-decoration: underline;
}

27
public/splash.html Normal file
View file

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" type="text/css" href="splash.css" />
<meta charset="UTF-8" />
<meta
name="viewport"
content="initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, width=device-width, height=device-height, user-scalable=0"
/>
<title>Bifrost Reddit Bridge</title>
</head>
<body class="app">
<main class="content">
<p class="eyebrow">GameCult Bifrost</p>
<h1 class="title">Reddit bridge actuator</h1>
<p class="description">
Governed posts and comments originate in Bifrost topics, then land here
with receipts.
</p>
<button class="open-button" id="open-button">Open status</button>
</main>
<footer class="footer">
<button class="text-link" id="docs-link">Devvit docs</button>
</footer>
<script type="module" src="splash.js" defer></script>
</body>
</html>

26
src/client/game.ts Normal file
View file

@ -0,0 +1,26 @@
import { navigateTo } from "@devvit/web/client";
import { ApiEndpoint, type HealthResponse } from "../shared/api.ts";
const statusElement = document.getElementById("bridge-status");
document.getElementById("docs-link")?.addEventListener("click", () => {
navigateTo("https://developers.reddit.com/docs");
});
async function refreshStatus(): Promise<void> {
if (!statusElement) {
return;
}
try {
const response = await fetch(ApiEndpoint.Health);
const data = (await response.json()) as HealthResponse;
statusElement.textContent =
data.status === "ok" ? "Bridge actuator online" : "Bridge status unknown";
} catch (error) {
console.error("Unable to read bridge status", error);
statusElement.textContent = "Bridge status unavailable";
}
}
void refreshStatus();

9
src/client/splash.ts Normal file
View file

@ -0,0 +1,9 @@
import { navigateTo, requestExpandedMode } from "@devvit/web/client";
document.getElementById("open-button")?.addEventListener("click", (event) => {
requestExpandedMode(event, "game");
});
document.getElementById("docs-link")?.addEventListener("click", () => {
navigateTo("https://developers.reddit.com/docs");
});

8
src/server/index.ts Normal file
View file

@ -0,0 +1,8 @@
import { createServer, getServerPort } from "@devvit/web/server";
import { serverOnRequest } from "./server.ts";
const server = createServer(serverOnRequest);
const port: number = getServerPort();
server.on("error", (err) => console.error(`server error; ${err.stack}`));
server.listen(port);

267
src/server/server.ts Normal file
View file

@ -0,0 +1,267 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import { context, reddit } from "@devvit/web/server";
import type {
PartialJsonValue,
TriggerResponse,
UiResponse,
} from "@devvit/web/shared";
import {
ApiEndpoint,
type BridgeCommentRequest,
type BridgePostRequest,
type BridgeReceipt,
type HealthResponse,
} from "../shared/api.ts";
import { once } from "node:events";
export async function serverOnRequest(
req: IncomingMessage,
rsp: ServerResponse,
): Promise<void> {
try {
await onRequest(req, rsp);
} catch (err) {
if (err instanceof BridgeError) {
writeJSON<ErrorResponse>(
err.status,
{ error: err.message, status: err.status },
rsp,
);
return;
}
const msg = `server error; ${err instanceof Error ? err.stack : err}`;
console.error(msg);
writeJSON<ErrorResponse>(500, { error: msg, status: 500 }, rsp);
}
}
async function onRequest(
req: IncomingMessage,
rsp: ServerResponse,
): Promise<void> {
const endpoint = getEndpoint(req.url);
if (!endpoint) {
writeJSON<ErrorResponse>(404, { error: "not found", status: 404 }, rsp);
return;
}
let body: BridgeReceipt | HealthResponse | UiResponse | TriggerResponse;
switch (endpoint) {
case ApiEndpoint.Health:
body = { type: "health", app: "gamecult-bifrost", status: "ok" };
break;
case ApiEndpoint.BridgePost:
assertBridgeSecret(req);
body = await onBridgePost(req);
break;
case ApiEndpoint.BridgeComment:
assertBridgeSecret(req);
body = await onBridgeComment(req);
break;
case ApiEndpoint.OnPostCreate:
body = await onMenuNewPost();
break;
case ApiEndpoint.OnAppInstall:
body = await onAppInstall();
break;
default:
endpoint satisfies never;
writeJSON<ErrorResponse>(404, { error: "not found", status: 404 }, rsp);
return;
}
writeJSON<PartialJsonValue>(200, body, rsp);
}
type ErrorResponse = {
error: string;
status: number;
};
async function onBridgePost(req: IncomingMessage): Promise<BridgeReceipt> {
const request = validatePostRequest(await readJSON<BridgePostRequest>(req));
const post = await reddit.submitPost({
subredditName: request.subredditName,
title: request.title,
text: renderGovernedBody(request.text, request),
sendreplies: false,
runAs: "APP",
});
return {
type: "bifrost.reddit.receipt",
action: "reddit-post",
actor: request.actor,
source: request.source,
target: `r/${request.subredditName}`,
redditId: post.id,
url: post.url || redditPermalink(post.permalink),
createdAt: new Date().toISOString(),
};
}
async function onBridgeComment(req: IncomingMessage): Promise<BridgeReceipt> {
const request = validateCommentRequest(
await readJSON<BridgeCommentRequest>(req),
);
const comment = await reddit.submitComment({
id: normalizeThingId(request.parentId),
text: renderGovernedBody(request.text, request),
runAs: "APP",
});
return {
type: "bifrost.reddit.receipt",
action: "reddit-comment",
actor: request.actor,
source: request.source,
target: request.parentId,
redditId: comment.id,
url: comment.url || redditPermalink(comment.permalink),
createdAt: new Date().toISOString(),
};
}
async function onMenuNewPost(): Promise<UiResponse> {
const subreddit = await reddit.getCurrentSubreddit();
const post = await reddit.submitCustomPost({
subredditName: subreddit.name,
title: "Bifrost bridge intake",
textFallback: {
text:
"Bifrost owns governed GameCult crossings into Reddit. " +
"Canonical proposals, approvals, and receipts remain in Bifrost.",
},
postData: {
kind: "gamecult-bifrost-bridge-hub",
createdBy: context.username ?? "unknown",
},
});
return {
showToast: { text: `Bridge hub ${post.id} created.`, appearance: "success" },
navigateTo: post.url || redditPermalink(post.permalink),
};
}
async function onAppInstall(): Promise<TriggerResponse> {
const subreddit = await reddit.getCurrentSubreddit();
await reddit.submitCustomPost({
subredditName: subreddit.name,
title: "Bifrost bridge online",
textFallback: {
text:
"The GameCult Bifrost Reddit bridge is installed. " +
"Bridge actions must still originate from governed Bifrost topics.",
},
postData: {
kind: "gamecult-bifrost-install-receipt",
},
});
return {};
}
function getEndpoint(url: string | undefined): ApiEndpoint | undefined {
if (!url || url === "/") {
return undefined;
}
const pathname = new URL(url, "https://devvit.local").pathname;
return Object.values(ApiEndpoint).find((endpoint) => endpoint === pathname);
}
function assertBridgeSecret(req: IncomingMessage): void {
const expectedSecret = process.env.BIFROST_REDDIT_BRIDGE_SECRET;
if (!expectedSecret) {
return;
}
const actualSecret = req.headers["x-bifrost-bridge-secret"];
if (actualSecret !== expectedSecret) {
throw new BridgeError(401, "invalid bridge secret");
}
}
function validatePostRequest(request: BridgePostRequest): BridgePostRequest {
assertNonEmpty(request.actor, "actor");
assertNonEmpty(request.source, "source");
assertNonEmpty(request.subredditName, "subredditName");
assertNonEmpty(request.title, "title");
assertNonEmpty(request.text, "text");
return request;
}
function validateCommentRequest(
request: BridgeCommentRequest,
): BridgeCommentRequest {
assertNonEmpty(request.actor, "actor");
assertNonEmpty(request.source, "source");
assertNonEmpty(request.parentId, "parentId");
assertNonEmpty(request.text, "text");
return request;
}
function assertNonEmpty(value: unknown, field: string): asserts value is string {
if (typeof value !== "string" || value.trim().length === 0) {
throw new BridgeError(400, `missing ${field}`);
}
}
function normalizeThingId(id: string): `t1_${string}` | `t3_${string}` {
if (id.startsWith("t1_") || id.startsWith("t3_")) {
return id as `t1_${string}` | `t3_${string}`;
}
throw new BridgeError(400, "parentId must be a t1_ comment or t3_ post id");
}
function renderGovernedBody(
body: string,
request: BridgePostRequest | BridgeCommentRequest,
): string {
const idempotency = request.idempotencyKey
? `\n\nBifrost idempotency: ${request.idempotencyKey}`
: "";
return `${body.trim()}\n\n---\nBifrost source: ${request.source}\nActor: ${request.actor}${idempotency}`;
}
function redditPermalink(permalink: string): string {
if (permalink.startsWith("http")) {
return permalink;
}
return `https://www.reddit.com${permalink}`;
}
function writeJSON<T extends PartialJsonValue>(
status: number,
json: Readonly<T>,
rsp: ServerResponse,
): void {
const body = JSON.stringify(json);
const len = Buffer.byteLength(body);
rsp.writeHead(status, {
"Content-Length": len,
"Content-Type": "application/json",
});
rsp.end(body);
}
async function readJSON<T>(req: IncomingMessage): Promise<T> {
const chunks: Uint8Array[] = [];
req.on("data", (chunk) => chunks.push(chunk));
await once(req, "end");
return JSON.parse(`${Buffer.concat(chunks)}`);
}
class BridgeError extends Error {
readonly status: number;
constructor(status: number, message: string) {
super(message);
this.status = status;
}
}

43
src/shared/api.ts Normal file
View file

@ -0,0 +1,43 @@
export const ApiEndpoint = {
Health: "/api/health",
BridgePost: "/internal/bifrost/reddit-post",
BridgeComment: "/internal/bifrost/reddit-comment",
OnPostCreate: "/internal/menu/post-create",
OnAppInstall: "/internal/on-app-install",
} as const;
export type ApiEndpoint = (typeof ApiEndpoint)[keyof typeof ApiEndpoint];
export type HealthResponse = {
type: "health";
app: "gamecult-bifrost";
status: "ok";
};
export type BridgePostRequest = {
actor: string;
source: string;
subredditName: string;
title: string;
text: string;
idempotencyKey?: string;
};
export type BridgeCommentRequest = {
actor: string;
source: string;
parentId: string;
text: string;
idempotencyKey?: string;
};
export type BridgeReceipt = {
type: "bifrost.reddit.receipt";
action: "reddit-post" | "reddit-comment";
actor: string;
source: string;
target: string;
redditId: string;
url: string;
createdAt: string;
};

56
tools/build.ts Normal file
View file

@ -0,0 +1,56 @@
#!/usr/bin/env -S node --experimental-strip-types
// Bundles sources to dist/ and public/.
//
// build.ts [--minify] [--watch]
// --local Run development server. Serve on http://localhost:1234 and reload on
// code change.
// --minify Minify output.
// --watch Automatically rebuild whenever an input changes.
import fs from "node:fs";
import type { BuildOptions } from "esbuild";
import esbuild from "esbuild";
const watch = process.argv.includes("--watch");
const opts: BuildOptions = {
bundle: true,
logLevel: "info", // Print the port and build demarcations.
metafile: true,
sourcemap: "linked",
target: "es2023", // https://esbuild.github.io/content-types/#tsconfig-json
};
const clientOpts: BuildOptions = {
...opts,
entryPoints: ["src/client/splash.ts", "src/client/game.ts"],
format: "esm",
outdir: "public",
platform: "browser",
};
const serverOpts: BuildOptions = {
...opts,
entryPoints: ["src/server/index.ts"],
format: "cjs",
outdir: "dist/server",
platform: "node",
};
if (watch) {
const clientCtx = await esbuild.context(clientOpts);
const serverCtx = await esbuild.context(serverOpts);
await Promise.all([
watch ? clientCtx.watch() : undefined,
watch ? serverCtx.watch() : undefined,
]);
} else {
const [client, server] = await Promise.all([
esbuild.build(clientOpts),
esbuild.build(serverOpts),
]);
if (client.metafile)
fs.writeFileSync("dist/client.meta.json", JSON.stringify(client.metafile));
if (server.metafile)
fs.writeFileSync("dist/server.meta.json", JSON.stringify(server.metafile));
}

57
tools/tsconfig.base.json Normal file
View file

@ -0,0 +1,57 @@
{
"$schema": "https://json.schemastore.org/tsconfig.json",
"compilerOptions": {
// Enable incremental builds.
"composite": true,
// Enable type-stripping.
"allowImportingTsExtensions": true,
// Only allow type-strippable syntax.
"erasableSyntaxOnly": true,
// Maximize type checking.
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"forceConsistentCasingInFileNames": true,
"noImplicitOverride": true,
"noUncheckedIndexedAccess": true,
"noUncheckedSideEffectImports": true,
"noUnusedLocals": true,
"resolveJsonModule": true,
"strict": true,
// Projects add types needed.
"lib": ["ES2023"],
"types": [],
"isolatedDeclarations": true, // Require explicit types.
// Improve compatibility with compilers that aren't type system aware.
"isolatedModules": true,
"esModuleInterop": true, // Use ESM.
// Allow JSON type-checking and imports.
"module": "ESNext",
"moduleResolution": "Bundler",
"rewriteRelativeImportExtensions": true, // Rewrite .ts imports to .js.
// All subprojects output to dist/ relative the project root.
"outDir": "../dist",
"rootDir": "..",
// Assume library types are checked and compatible.
"skipLibCheck": true,
"skipDefaultLibCheck": true,
"sourceMap": true,
"declarationMap": true, // Map back to original sources, not .d.ts.
"target": "ES2023",
// Don't transform type-only imports.
"verbatimModuleSyntax": true
}
}

View file

@ -0,0 +1,17 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"outDir": "../dist/types/client",
"lib": ["DOM", "ES2023"],
"tsBuildInfoFile": "../dist/types/client/tsconfig.tsbuildinfo",
"customConditions": ["browser"],
"rootDir": "../src/client"
},
"include": ["../src/client/**/*"],
"exclude": ["../src/server/**/*"],
"references": [
{
"path": "./tsconfig.shared.json"
}
]
}

8
tools/tsconfig.json Normal file
View file

@ -0,0 +1,8 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"outDir": "../dist/types/server",
"tsBuildInfoFile": "../dist/types/tools/tsconfig.tsbuildinfo",
"types": ["node"]
}
}

View file

@ -0,0 +1,16 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"outDir": "../dist/types/server",
"tsBuildInfoFile": "../dist/types/server/tsconfig.tsbuildinfo",
"customConditions": ["server"],
"types": ["node"],
"rootDir": "../src/server"
},
"include": ["../src/server/**/*"],
"references": [
{
"path": "./tsconfig.shared.json"
}
]
}

View file

@ -0,0 +1,10 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"outDir": "../dist/types/shared",
"tsBuildInfoFile": "../dist/types/shared/tsconfig.tsbuildinfo",
"rootDir": "../src/shared"
},
"include": ["../src/shared/**/*"],
"exclude": []
}

8
tsconfig.json Normal file
View file

@ -0,0 +1,8 @@
{
"files": [],
"references": [
{ "path": "./tools/tsconfig.client.json" },
{ "path": "./tools/tsconfig.server.json" },
{ "path": "./tools/tsconfig.shared.json" }
]
}