mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 14:41:28 -07:00
remove hard coded routes
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": ["mcp__plugin_cloudflare_cloudflare-docs__search_cloudflare_documentation"]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -100,13 +100,6 @@ new-package *args:
|
|||||||
update *args:
|
update *args:
|
||||||
bun runx update "$@"
|
bun runx update "$@"
|
||||||
|
|
||||||
# Sync generated config (wrangler routes, etc.) from env.json
|
|
||||||
[group('4. utility')]
|
|
||||||
[positional-arguments]
|
|
||||||
[no-cd]
|
|
||||||
sync *args:
|
|
||||||
bun runx sync "$@"
|
|
||||||
|
|
||||||
# CLI in packages/tools for running commands in the repo.
|
# CLI in packages/tools for running commands in the repo.
|
||||||
[group('4. utility')]
|
[group('4. utility')]
|
||||||
[positional-arguments]
|
[positional-arguments]
|
||||||
|
|||||||
@@ -47,12 +47,15 @@ own copy.
|
|||||||
```bash
|
```bash
|
||||||
cp env.example.json env.json
|
cp env.example.json env.json
|
||||||
# edit env.json and set "domain" to your domain
|
# edit env.json and set "domain" to your domain
|
||||||
just sync
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`just sync` regenerates the derived config (wrangler route patterns, the `ns`
|
`just deploy` reads `env.json` at deploy time and passes the base domain to
|
||||||
service-discovery document, and the api share-link base URL) from `env.json`.
|
wrangler — each worker is attached to its custom domain via `--domain
|
||||||
Re-run it whenever you change `env.json`.
|
<subdomain>.<domain>`, and the base domain is injected as the `DOMAIN` var so
|
||||||
|
the `ns` service-discovery document and the api share-link base URL are built at
|
||||||
|
runtime. Nothing in version control is rewritten; committed `wrangler.jsonc`
|
||||||
|
files have no routes, and per-app subdomain overrides live under `subdomains` in
|
||||||
|
`env.json`.
|
||||||
|
|
||||||
**Run Development Server:**
|
**Run Development Server:**
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,6 @@
|
|||||||
"main": "src/accounts.app.ts",
|
"main": "src/accounts.app.ts",
|
||||||
"compatibility_date": "2025-09-20",
|
"compatibility_date": "2025-09-20",
|
||||||
"compatibility_flags": ["nodejs_compat"],
|
"compatibility_flags": ["nodejs_compat"],
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"pattern": "accounts.rec.example.com",
|
|
||||||
"custom_domain": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
// Shared D1 database. The `accounts` table schema/migrations are owned by the
|
// Shared D1 database. The `accounts` table schema/migrations are owned by the
|
||||||
// `auth` worker; this worker binds it read/write to look up and create accounts.
|
// `auth` worker; this worker binds it read/write to look up and create accounts.
|
||||||
"d1_databases": [
|
"d1_databases": [
|
||||||
|
|||||||
@@ -174,7 +174,11 @@ const app = new Hono<App>({ strict: false })
|
|||||||
VersionRegex: '.*',
|
VersionRegex: '.*',
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.get('/api/config/v2', (c) => c.json(apiConfigV2))
|
// ShareBaseUrl is derived from the deploy-time base domain; the rest of the
|
||||||
|
// config is static.
|
||||||
|
.get('/api/config/v2', (c) =>
|
||||||
|
c.json({ ...apiConfigV2, ShareBaseUrl: `https://www.${c.env.DOMAIN}/{0}` })
|
||||||
|
)
|
||||||
.get('/api/versioncheck/v4', (c) =>
|
.get('/api/versioncheck/v4', (c) =>
|
||||||
c.json({
|
c.json({
|
||||||
VersionStatus: 0,
|
VersionStatus: 0,
|
||||||
|
|||||||
@@ -2,6 +2,12 @@ import type { HonoApp } from '@repo/hono-helpers'
|
|||||||
import type { SharedHonoEnv, SharedHonoVariables } from '@repo/hono-helpers/src/types'
|
import type { SharedHonoEnv, SharedHonoVariables } from '@repo/hono-helpers/src/types'
|
||||||
|
|
||||||
export type Env = SharedHonoEnv & {
|
export type Env = SharedHonoEnv & {
|
||||||
|
/**
|
||||||
|
* Base domain the share-link URL is derived from, e.g. `rec.example.com`.
|
||||||
|
* Injected at deploy time via `--var DOMAIN`; defaults in `wrangler.jsonc`
|
||||||
|
* for local dev and tests.
|
||||||
|
*/
|
||||||
|
DOMAIN: string
|
||||||
// Shared rooms database (schema/migrations owned by the `rooms` worker). Used
|
// Shared rooms database (schema/migrations owned by the `rooms` worker). Used
|
||||||
// read-only here for the /roomserver/rooms/* endpoints.
|
// read-only here for the /roomserver/rooms/* endpoints.
|
||||||
DB: D1Database
|
DB: D1Database
|
||||||
|
|||||||
@@ -4,12 +4,6 @@
|
|||||||
"main": "src/api.app.ts",
|
"main": "src/api.app.ts",
|
||||||
"compatibility_date": "2025-09-20",
|
"compatibility_date": "2025-09-20",
|
||||||
"compatibility_flags": ["nodejs_compat"],
|
"compatibility_flags": ["nodejs_compat"],
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"pattern": "api.rec.example.com",
|
|
||||||
"custom_domain": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
// Shared rooms DB (created + migrated by the `rooms` worker; bound read-only here).
|
// Shared rooms DB (created + migrated by the `rooms` worker; bound read-only here).
|
||||||
"d1_databases": [
|
"d1_databases": [
|
||||||
{
|
{
|
||||||
@@ -36,6 +30,7 @@
|
|||||||
},
|
},
|
||||||
"vars": {
|
"vars": {
|
||||||
"ENVIRONMENT": "development", // overridden during deployment
|
"ENVIRONMENT": "development", // overridden during deployment
|
||||||
"SENTRY_RELEASE": "unknown" // overridden during deployment
|
"SENTRY_RELEASE": "unknown", // overridden during deployment
|
||||||
|
"DOMAIN": "rec.example.com" // base domain; overridden during deployment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,6 @@
|
|||||||
"main": "src/auth.app.ts",
|
"main": "src/auth.app.ts",
|
||||||
"compatibility_date": "2025-09-20",
|
"compatibility_date": "2025-09-20",
|
||||||
"compatibility_flags": ["nodejs_compat"],
|
"compatibility_flags": ["nodejs_compat"],
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"pattern": "auth.rec.example.com",
|
|
||||||
"custom_domain": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
// Shared D1 database (also used by the rooms worker). The `auth` worker owns
|
// Shared D1 database (also used by the rooms worker). The `auth` worker owns
|
||||||
// the `accounts` table; a dedicated migrations_table keeps its migration
|
// the `accounts` table; a dedicated migrations_table keeps its migration
|
||||||
// history separate from the rooms worker's migrations on the same database.
|
// history separate from the rooms worker's migrations on the same database.
|
||||||
|
|||||||
@@ -6,12 +6,6 @@
|
|||||||
"compatibility_flags": [
|
"compatibility_flags": [
|
||||||
"nodejs_compat"
|
"nodejs_compat"
|
||||||
],
|
],
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"pattern": "cdn.rec.example.com",
|
|
||||||
"custom_domain": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
// CDN binaries (signature blobs + room build data) are stored as R2 objects
|
// CDN binaries (signature blobs + room build data) are stored as R2 objects
|
||||||
// and streamed back by key. Keys are prefixed `sigs/` and `room/`.
|
// and streamed back by key. Keys are prefixed `sigs/` and `room/`.
|
||||||
"r2_buckets": [
|
"r2_buckets": [
|
||||||
|
|||||||
@@ -4,12 +4,6 @@
|
|||||||
"main": "src/chat.app.ts",
|
"main": "src/chat.app.ts",
|
||||||
"compatibility_date": "2025-09-20",
|
"compatibility_date": "2025-09-20",
|
||||||
"compatibility_flags": ["nodejs_compat"],
|
"compatibility_flags": ["nodejs_compat"],
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"pattern": "chat.rec.example.com",
|
|
||||||
"custom_domain": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"upload_source_maps": true,
|
"upload_source_maps": true,
|
||||||
"observability": {
|
"observability": {
|
||||||
"logs": {
|
"logs": {
|
||||||
|
|||||||
@@ -4,12 +4,6 @@
|
|||||||
"main": "src/clubs.app.ts",
|
"main": "src/clubs.app.ts",
|
||||||
"compatibility_date": "2025-09-20",
|
"compatibility_date": "2025-09-20",
|
||||||
"compatibility_flags": ["nodejs_compat"],
|
"compatibility_flags": ["nodejs_compat"],
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"pattern": "clubs.rec.example.com",
|
|
||||||
"custom_domain": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"logpush": false,
|
"logpush": false,
|
||||||
"upload_source_maps": true,
|
"upload_source_maps": true,
|
||||||
"observability": {
|
"observability": {
|
||||||
|
|||||||
@@ -4,12 +4,6 @@
|
|||||||
"main": "src/commerce.app.ts",
|
"main": "src/commerce.app.ts",
|
||||||
"compatibility_date": "2025-09-20",
|
"compatibility_date": "2025-09-20",
|
||||||
"compatibility_flags": ["nodejs_compat"],
|
"compatibility_flags": ["nodejs_compat"],
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"pattern": "commerce.rec.example.com",
|
|
||||||
"custom_domain": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"upload_source_maps": true,
|
"upload_source_maps": true,
|
||||||
"observability": {
|
"observability": {
|
||||||
"logs": {
|
"logs": {
|
||||||
|
|||||||
@@ -4,12 +4,6 @@
|
|||||||
"main": "src/datacollection.app.ts",
|
"main": "src/datacollection.app.ts",
|
||||||
"compatibility_date": "2025-09-20",
|
"compatibility_date": "2025-09-20",
|
||||||
"compatibility_flags": ["nodejs_compat"],
|
"compatibility_flags": ["nodejs_compat"],
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"pattern": "datacollection.rec.example.com",
|
|
||||||
"custom_domain": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"upload_source_maps": true,
|
"upload_source_maps": true,
|
||||||
"observability": {
|
"observability": {
|
||||||
"logs": {
|
"logs": {
|
||||||
|
|||||||
@@ -4,12 +4,6 @@
|
|||||||
"main": "src/econ.app.ts",
|
"main": "src/econ.app.ts",
|
||||||
"compatibility_date": "2025-09-20",
|
"compatibility_date": "2025-09-20",
|
||||||
"compatibility_flags": ["nodejs_compat"],
|
"compatibility_flags": ["nodejs_compat"],
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"pattern": "econ.rec.example.com",
|
|
||||||
"custom_domain": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"logpush": false,
|
"logpush": false,
|
||||||
"upload_source_maps": true,
|
"upload_source_maps": true,
|
||||||
"observability": {
|
"observability": {
|
||||||
|
|||||||
@@ -4,12 +4,6 @@
|
|||||||
"main": "src/img.app.ts",
|
"main": "src/img.app.ts",
|
||||||
"compatibility_date": "2025-09-20",
|
"compatibility_date": "2025-09-20",
|
||||||
"compatibility_flags": ["nodejs_compat"],
|
"compatibility_flags": ["nodejs_compat"],
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"pattern": "img.rec.example.com",
|
|
||||||
"custom_domain": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
// Images are stored as objects in an R2 bucket and streamed back by key.
|
// Images are stored as objects in an R2 bucket and streamed back by key.
|
||||||
"r2_buckets": [
|
"r2_buckets": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,12 +4,6 @@
|
|||||||
"main": "src/match.app.ts",
|
"main": "src/match.app.ts",
|
||||||
"compatibility_date": "2025-09-20",
|
"compatibility_date": "2025-09-20",
|
||||||
"compatibility_flags": ["nodejs_compat"],
|
"compatibility_flags": ["nodejs_compat"],
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"pattern": "match.rec.example.com",
|
|
||||||
"custom_domain": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
// Per-player presence store (room instance the player is currently in).
|
// Per-player presence store (room instance the player is currently in).
|
||||||
// Create with `wrangler kv namespace create MATCH_PRESENCE` and set the id.
|
// Create with `wrangler kv namespace create MATCH_PRESENCE` and set the id.
|
||||||
"kv_namespaces": [
|
"kv_namespaces": [
|
||||||
|
|||||||
@@ -4,12 +4,6 @@
|
|||||||
"main": "src/notify.app.ts",
|
"main": "src/notify.app.ts",
|
||||||
"compatibility_date": "2025-09-20",
|
"compatibility_date": "2025-09-20",
|
||||||
"compatibility_flags": ["nodejs_compat"],
|
"compatibility_flags": ["nodejs_compat"],
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"pattern": "notify.rec.example.com",
|
|
||||||
"custom_domain": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"durable_objects": {
|
"durable_objects": {
|
||||||
"bindings": [{ "name": "NOTIFICATIONS_HUB", "class_name": "NotificationsHub" }]
|
"bindings": [{ "name": "NOTIFICATIONS_HUB", "class_name": "NotificationsHub" }]
|
||||||
},
|
},
|
||||||
|
|||||||
+6
-11
@@ -6,17 +6,12 @@ Name-server / service-discovery worker served on the `ns` subdomain.
|
|||||||
discover every service host (Accounts, API, Auth, Econ, Matchmaking,
|
discover every service host (Accounts, API, Auth, Econ, Matchmaking,
|
||||||
Notifications, …).
|
Notifications, …).
|
||||||
|
|
||||||
The document is served from `static/endpoints.json`, whose hosts are generated
|
Each host is built at runtime from the `DOMAIN` var (the base domain) plus the
|
||||||
from the repo-root `env.json` by `runx sync` — every entry is derived from the
|
service → subdomain map in `src/endpoints.ts`. `DOMAIN` is injected at deploy
|
||||||
configured base `domain`.
|
time from the repo-root `env.json` (see `run-wrangler-deploy`) and defaults to
|
||||||
|
`rec.example.com` in `wrangler.jsonc` for local dev.
|
||||||
|
|
||||||
## Updating endpoints
|
## Updating endpoints
|
||||||
|
|
||||||
Change `domain` in `env.json` (or edit the host map in `static/endpoints.json`),
|
- To change the base domain, edit `domain` in `env.json` and redeploy.
|
||||||
then regenerate:
|
- To add or rename a service host, edit the map in `src/endpoints.ts`.
|
||||||
|
|
||||||
```sh
|
|
||||||
just sync
|
|
||||||
```
|
|
||||||
|
|
||||||
(Bundled at build time, so a redeploy is required for changes to take effect.)
|
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ import type { HonoApp } from '@repo/hono-helpers'
|
|||||||
import type { SharedHonoEnv, SharedHonoVariables } from '@repo/hono-helpers/src/types'
|
import type { SharedHonoEnv, SharedHonoVariables } from '@repo/hono-helpers/src/types'
|
||||||
|
|
||||||
export type Env = SharedHonoEnv & {
|
export type Env = SharedHonoEnv & {
|
||||||
// add additional Bindings here
|
/**
|
||||||
|
* Base domain the service hosts are derived from, e.g. `rec.example.com`.
|
||||||
|
* Injected at deploy time via `--var DOMAIN`; defaults in `wrangler.jsonc`
|
||||||
|
* for local dev and tests.
|
||||||
|
*/
|
||||||
|
DOMAIN: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Variables can be extended */
|
/** Variables can be extended */
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* Service-discovery map: service label → subdomain. The game client fetches the
|
||||||
|
* generated `{ label: "https://<subdomain>.<domain>" }` document from `/`.
|
||||||
|
*
|
||||||
|
* The base domain is injected at deploy time via the `DOMAIN` var (see
|
||||||
|
* `run-wrangler-deploy`), so the real domain never lives in a versioned file.
|
||||||
|
*/
|
||||||
|
const SERVICE_SUBDOMAINS = {
|
||||||
|
Accounts: 'accounts',
|
||||||
|
AI: 'ai',
|
||||||
|
API: 'api',
|
||||||
|
Auth: 'auth',
|
||||||
|
BugReporting: 'bugreporting',
|
||||||
|
Cards: 'cards',
|
||||||
|
CDN: 'cdn',
|
||||||
|
Chat: 'chat',
|
||||||
|
Clubs: 'clubs',
|
||||||
|
CMS: 'cms',
|
||||||
|
Commerce: 'commerce',
|
||||||
|
Data: 'data',
|
||||||
|
DataCollection: 'datacollection',
|
||||||
|
Discovery: 'discovery',
|
||||||
|
Econ: 'econ',
|
||||||
|
GameLogs: 'gamelogs',
|
||||||
|
Geo: 'geo',
|
||||||
|
Images: 'img',
|
||||||
|
Leaderboard: 'leaderboard',
|
||||||
|
Link: 'link',
|
||||||
|
Lists: 'lists',
|
||||||
|
Matchmaking: 'match',
|
||||||
|
Moderation: 'api',
|
||||||
|
Notifications: 'notify',
|
||||||
|
PlatformNotifications: 'platformnotifications',
|
||||||
|
PlayerSettings: 'playersettings',
|
||||||
|
RoomComments: 'roomcomments',
|
||||||
|
RoomieIntegrations: 'roomieintegrations',
|
||||||
|
Rooms: 'rooms',
|
||||||
|
Storage: 'storage',
|
||||||
|
Strings: 'strings',
|
||||||
|
StringsCDN: 'strings-cdn',
|
||||||
|
Studio: 'studio',
|
||||||
|
Thorn: 'thorn',
|
||||||
|
Videos: 'videos',
|
||||||
|
WWW: 'www',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
/** Builds the endpoints document for `domain`, e.g. `rec.example.com`. */
|
||||||
|
export function buildEndpoints(domain: string): Record<string, string> {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(SERVICE_SUBDOMAINS).map(([label, sub]) => [label, `https://${sub}.${domain}`])
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -3,15 +3,16 @@ import { useWorkersLogger } from 'workers-tagged-logger'
|
|||||||
|
|
||||||
import { withNotFound, withOnError } from '@repo/hono-helpers'
|
import { withNotFound, withOnError } from '@repo/hono-helpers'
|
||||||
|
|
||||||
import endpoints from '../static/endpoints.json'
|
import { buildEndpoints } from './endpoints'
|
||||||
|
|
||||||
import type { App } from './context'
|
import type { App } from './context'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name-server / service-discovery worker served at the apex domain.
|
* Name-server / service-discovery worker served at the apex domain.
|
||||||
* Returns the endpoints document the game client fetches to discover every
|
* Returns the endpoints document the game client fetches to discover every
|
||||||
* service host. Generated from `env.json` by `runx sync` — run it after
|
* service host. Hosts are derived from the `DOMAIN` var, which is injected at
|
||||||
* changing the domain (see `apps/ns/static/endpoints.json`).
|
* deploy time (see `run-wrangler-deploy`) and defaults in `wrangler.jsonc` for
|
||||||
|
* local dev.
|
||||||
*/
|
*/
|
||||||
const app = new Hono<App>()
|
const app = new Hono<App>()
|
||||||
.use(
|
.use(
|
||||||
@@ -27,7 +28,7 @@ const app = new Hono<App>()
|
|||||||
.onError(withOnError())
|
.onError(withOnError())
|
||||||
.notFound(withNotFound())
|
.notFound(withNotFound())
|
||||||
|
|
||||||
// Endpoints document. TODO: generate this dynamically per environment.
|
// Endpoints document, derived from the deploy-time base domain.
|
||||||
.get('/', (c) => c.json(endpoints))
|
.get('/', (c) => c.json(buildEndpoints(c.env.DOMAIN)))
|
||||||
|
|
||||||
export default app
|
export default app
|
||||||
|
|||||||
@@ -2,16 +2,19 @@ import { exports } from 'cloudflare:workers'
|
|||||||
import { describe, expect, test } from 'vitest'
|
import { describe, expect, test } from 'vitest'
|
||||||
|
|
||||||
import '../../ns.app'
|
import '../../ns.app'
|
||||||
import endpoints from '../../../static/endpoints.json'
|
import { buildEndpoints } from '../../endpoints'
|
||||||
|
|
||||||
const ORIGIN = 'https://example.com'
|
const ORIGIN = 'https://example.com'
|
||||||
|
|
||||||
|
// Must match the DOMAIN var default in apps/ns/wrangler.jsonc.
|
||||||
|
const TEST_DOMAIN = 'rec.example.com'
|
||||||
|
|
||||||
describe('ns endpoints', () => {
|
describe('ns endpoints', () => {
|
||||||
test('GET / returns the endpoints document', async () => {
|
test('GET / returns the endpoints document', async () => {
|
||||||
const res = await exports.default.fetch(`${ORIGIN}/`)
|
const res = await exports.default.fetch(`${ORIGIN}/`)
|
||||||
expect(res.status).toBe(200)
|
expect(res.status).toBe(200)
|
||||||
const body = await res.json()
|
const body = await res.json()
|
||||||
expect(body).toEqual(endpoints)
|
expect(body).toEqual(buildEndpoints(TEST_DOMAIN))
|
||||||
})
|
})
|
||||||
|
|
||||||
test('unknown path returns 404', async () => {
|
test('unknown path returns 404', async () => {
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"Accounts": "https://accounts.rec.example.com",
|
|
||||||
"AI": "https://ai.rec.example.com",
|
|
||||||
"API": "https://api.rec.example.com",
|
|
||||||
"Auth": "https://auth.rec.example.com",
|
|
||||||
"BugReporting": "https://bugreporting.rec.example.com",
|
|
||||||
"Cards": "https://cards.rec.example.com",
|
|
||||||
"CDN": "https://cdn.rec.example.com",
|
|
||||||
"Chat": "https://chat.rec.example.com",
|
|
||||||
"Clubs": "https://clubs.rec.example.com",
|
|
||||||
"CMS": "https://cms.rec.example.com",
|
|
||||||
"Commerce": "https://commerce.rec.example.com",
|
|
||||||
"Data": "https://data.rec.example.com",
|
|
||||||
"DataCollection": "https://datacollection.rec.example.com",
|
|
||||||
"Discovery": "https://discovery.rec.example.com",
|
|
||||||
"Econ": "https://econ.rec.example.com",
|
|
||||||
"GameLogs": "https://gamelogs.rec.example.com",
|
|
||||||
"Geo": "https://geo.rec.example.com",
|
|
||||||
"Images": "https://img.rec.example.com",
|
|
||||||
"Leaderboard": "https://leaderboard.rec.example.com",
|
|
||||||
"Link": "https://link.rec.example.com",
|
|
||||||
"Lists": "https://lists.rec.example.com",
|
|
||||||
"Matchmaking": "https://match.rec.example.com",
|
|
||||||
"Moderation": "https://api.rec.example.com",
|
|
||||||
"Notifications": "https://notify.rec.example.com",
|
|
||||||
"PlatformNotifications": "https://platformnotifications.rec.example.com",
|
|
||||||
"PlayerSettings": "https://playersettings.rec.example.com",
|
|
||||||
"RoomComments": "https://roomcomments.rec.example.com",
|
|
||||||
"RoomieIntegrations": "https://roomieintegrations.rec.example.com",
|
|
||||||
"Rooms": "https://rooms.rec.example.com",
|
|
||||||
"Storage": "https://storage.rec.example.com",
|
|
||||||
"Strings": "https://strings.rec.example.com",
|
|
||||||
"StringsCDN": "https://strings-cdn.rec.example.com",
|
|
||||||
"Studio": "https://studio.rec.example.com",
|
|
||||||
"Thorn": "https://thorn.rec.example.com",
|
|
||||||
"Videos": "https://videos.rec.example.com",
|
|
||||||
"WWW": "https://www.rec.example.com"
|
|
||||||
}
|
|
||||||
@@ -4,12 +4,6 @@
|
|||||||
"main": "src/ns.app.ts",
|
"main": "src/ns.app.ts",
|
||||||
"compatibility_date": "2025-09-20",
|
"compatibility_date": "2025-09-20",
|
||||||
"compatibility_flags": ["nodejs_compat"],
|
"compatibility_flags": ["nodejs_compat"],
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"pattern": "ns.rec.example.com",
|
|
||||||
"custom_domain": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"logpush": false,
|
"logpush": false,
|
||||||
"upload_source_maps": true,
|
"upload_source_maps": true,
|
||||||
"observability": {
|
"observability": {
|
||||||
@@ -20,6 +14,7 @@
|
|||||||
},
|
},
|
||||||
"vars": {
|
"vars": {
|
||||||
"ENVIRONMENT": "development", // overridden during deployment
|
"ENVIRONMENT": "development", // overridden during deployment
|
||||||
"SENTRY_RELEASE": "unknown" // overridden during deployment
|
"SENTRY_RELEASE": "unknown", // overridden during deployment
|
||||||
|
"DOMAIN": "rec.example.com" // base domain; overridden during deployment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,6 @@
|
|||||||
"main": "src/playersettings.app.ts",
|
"main": "src/playersettings.app.ts",
|
||||||
"compatibility_date": "2025-09-20",
|
"compatibility_date": "2025-09-20",
|
||||||
"compatibility_flags": ["nodejs_compat"],
|
"compatibility_flags": ["nodejs_compat"],
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"pattern": "playersettings.rec.example.com",
|
|
||||||
"custom_domain": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
// Per-player settings persistence.
|
// Per-player settings persistence.
|
||||||
"kv_namespaces": [
|
"kv_namespaces": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,12 +4,6 @@
|
|||||||
"main": "src/rooms.app.ts",
|
"main": "src/rooms.app.ts",
|
||||||
"compatibility_date": "2025-09-20",
|
"compatibility_date": "2025-09-20",
|
||||||
"compatibility_flags": ["nodejs_compat"],
|
"compatibility_flags": ["nodejs_compat"],
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"pattern": "rooms.rec.example.com",
|
|
||||||
"custom_domain": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
// Room storage. Create with `wrangler d1 create rec-rooms`, set the id, then
|
// Room storage. Create with `wrangler d1 create rec-rooms`, set the id, then
|
||||||
// `wrangler d1 migrations apply rec-rooms --remote`.
|
// `wrangler d1 migrations apply rec-rooms --remote`.
|
||||||
"d1_databases": [
|
"d1_databases": [
|
||||||
|
|||||||
@@ -5,10 +5,29 @@ set -eu
|
|||||||
NAME=$(jq -r '.name' package.json)
|
NAME=$(jq -r '.name' package.json)
|
||||||
VERSION=$(get-version)
|
VERSION=$(get-version)
|
||||||
|
|
||||||
|
# Resolve the base domain (and this worker's subdomain) from the gitignored
|
||||||
|
# env.json at the repo root, then deploy onto the custom domain via `--domain`.
|
||||||
|
# This keeps the real domain out of versioned files — committed wrangler.jsonc
|
||||||
|
# has no routes, and the base domain is passed as the DOMAIN var at runtime.
|
||||||
|
ROOT=$(git rev-parse --show-toplevel)
|
||||||
|
ENV_JSON="$ROOT/env.json"
|
||||||
|
if [ ! -f "$ENV_JSON" ]; then
|
||||||
|
echo "error: $ENV_JSON not found — copy env.example.json and set your domain" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
DIR=$(basename "$PWD")
|
||||||
|
DOMAIN=$(jq -r '.domain' "$ENV_JSON")
|
||||||
|
# Per-app subdomain override, falling back to the worker's directory name.
|
||||||
|
SUBDOMAIN=$(jq -r --arg d "$DIR" '.subdomains[$d] // $d' "$ENV_JSON")
|
||||||
|
HOST="$SUBDOMAIN.$DOMAIN"
|
||||||
|
|
||||||
# Deploy with wrangler using the extracted values as binding variables
|
# Deploy with wrangler using the extracted values as binding variables
|
||||||
echo "Deploying worker $NAME version $VERSION"
|
echo "Deploying worker $NAME version $VERSION to $HOST"
|
||||||
exec wrangler deploy \
|
exec wrangler deploy \
|
||||||
--var NAME:"$NAME" \
|
--var NAME:"$NAME" \
|
||||||
--var SENTRY_RELEASE:"$VERSION" \
|
--var SENTRY_RELEASE:"$VERSION" \
|
||||||
|
--var DOMAIN:"$DOMAIN" \
|
||||||
|
--domain "$HOST" \
|
||||||
--minify \
|
--minify \
|
||||||
"$@"
|
"$@"
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { ciCmd } from '../cmd/ci.cmd'
|
|||||||
import { devCmd } from '../cmd/dev.cmd'
|
import { devCmd } from '../cmd/dev.cmd'
|
||||||
import { fixCmd } from '../cmd/fix.cmd'
|
import { fixCmd } from '../cmd/fix.cmd'
|
||||||
import { shfmtCmd } from '../cmd/shfmt.cmd'
|
import { shfmtCmd } from '../cmd/shfmt.cmd'
|
||||||
import { syncCmd } from '../cmd/sync.cmd'
|
|
||||||
import { updateCmd } from '../cmd/update.cmd'
|
import { updateCmd } from '../cmd/update.cmd'
|
||||||
|
|
||||||
program
|
program
|
||||||
@@ -26,7 +25,6 @@ program
|
|||||||
.addCommand(ciCmd)
|
.addCommand(ciCmd)
|
||||||
.addCommand(updateCmd)
|
.addCommand(updateCmd)
|
||||||
.addCommand(shfmtCmd)
|
.addCommand(shfmtCmd)
|
||||||
.addCommand(syncCmd)
|
|
||||||
|
|
||||||
// Don't hang for unresolved promises
|
// Don't hang for unresolved promises
|
||||||
.hook('postAction', () => process.exit(0))
|
.hook('postAction', () => process.exit(0))
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
import { Command } from '@commander-js/extra-typings'
|
|
||||||
import { z } from 'zod'
|
|
||||||
|
|
||||||
import { getRepoRoot } from '../path'
|
|
||||||
|
|
||||||
const Env = z.object({
|
|
||||||
/** Base domain that all service hosts are derived from, e.g. `rec.example.com`. */
|
|
||||||
domain: z.string().min(1),
|
|
||||||
/**
|
|
||||||
* Optional per-app subdomain overrides, keyed by the app's directory name.
|
|
||||||
* Defaults to the directory name when not set.
|
|
||||||
*/
|
|
||||||
subdomains: z.record(z.string(), z.string()).optional(),
|
|
||||||
})
|
|
||||||
|
|
||||||
type Edit = {
|
|
||||||
label: string
|
|
||||||
file: string
|
|
||||||
/** Rewrites the file's derived values; a no-op when nothing needs changing. */
|
|
||||||
transform: (text: string) => string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const syncCmd = new Command('sync')
|
|
||||||
.description('Sync generated config (wrangler routes, ns endpoints, etc.) from env.json')
|
|
||||||
.option('--check', `Exit non-zero if any file is out of sync (don't write changes)`, false)
|
|
||||||
.action(async ({ check }) => {
|
|
||||||
const repoRoot = getRepoRoot()
|
|
||||||
const env = Env.parse(await fs.readJson(path.join(repoRoot, 'env.json')))
|
|
||||||
|
|
||||||
const edits: Edit[] = []
|
|
||||||
|
|
||||||
// Worker custom-domain routes: `<subdomain>.<domain>`, derived from the app dir name.
|
|
||||||
const wranglerConfigs = await glob('apps/*/wrangler.jsonc', { cwd: repoRoot, absolute: true })
|
|
||||||
for (const file of wranglerConfigs.sort()) {
|
|
||||||
const dir = path.basename(path.dirname(file))
|
|
||||||
const subdomain = env.subdomains?.[dir] ?? dir
|
|
||||||
const pattern = `${subdomain}.${env.domain}`
|
|
||||||
edits.push({
|
|
||||||
label: `apps/${dir}/wrangler.jsonc → ${pattern}`,
|
|
||||||
file,
|
|
||||||
// Only matches when the file has a route; otherwise leaves the file untouched.
|
|
||||||
transform: (t) => t.replace(/("pattern":\s*")[^"]*(")/, `$1${pattern}$2`),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ns service-discovery document — every host is one of our own subdomains, so swap
|
|
||||||
// the base domain while preserving each entry's subdomain label.
|
|
||||||
const endpointsFile = path.join(repoRoot, 'apps/ns/static/endpoints.json')
|
|
||||||
if (await fs.pathExists(endpointsFile)) {
|
|
||||||
edits.push({
|
|
||||||
label: 'apps/ns/static/endpoints.json',
|
|
||||||
file: endpointsFile,
|
|
||||||
transform: (t) =>
|
|
||||||
t.replace(/("https:\/\/[a-z0-9-]+\.)[a-z0-9.-]+(")/g, `$1${env.domain}$2`),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Share-link base URL in the api worker's static config (only this one field is a
|
|
||||||
// host of ours; other URLs in the file are third-party and must be left alone).
|
|
||||||
const apiConfig = path.join(repoRoot, 'apps/api/static/api-config-v2.json')
|
|
||||||
if (await fs.pathExists(apiConfig)) {
|
|
||||||
edits.push({
|
|
||||||
label: 'apps/api/static/api-config-v2.json (ShareBaseUrl)',
|
|
||||||
file: apiConfig,
|
|
||||||
transform: (t) =>
|
|
||||||
t.replace(/("ShareBaseUrl":\s*"https:\/\/[a-z0-9-]+\.)[a-z0-9.-]+(\/)/, `$1${env.domain}$2`),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const outOfSync: string[] = []
|
|
||||||
for (const { label, file, transform } of edits) {
|
|
||||||
const text = await fs.readFile(file, 'utf8')
|
|
||||||
const next = transform(text)
|
|
||||||
if (next === text) continue
|
|
||||||
outOfSync.push(label)
|
|
||||||
if (!check) await fs.writeFile(file, next)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (outOfSync.length === 0) {
|
|
||||||
echo(chalk.green('✓ generated config in sync'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (check) {
|
|
||||||
echo(chalk.red('✗ generated config out of sync. Run `just sync` to fix:'))
|
|
||||||
for (const line of outOfSync) echo(` ${line}`)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
echo(chalk.green(`✓ synced ${outOfSync.length} file(s):`))
|
|
||||||
for (const line of outOfSync) echo(` ${line}`)
|
|
||||||
})
|
|
||||||
Reference in New Issue
Block a user