@colyseus/playground 0.17.12 → 0.18.1

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src-backend/index.ts"],
4
- "sourcesContent": ["import path from 'path';\nimport express, { Router } from 'express';\nimport { auth, JWT } from '@colyseus/auth';\nimport { matchMaker, IRoomCache, __globalEndpoints } from '@colyseus/core';\nimport type { Endpoint } from \"@colyseus/better-call\";\nimport { applyMonkeyPatch } from './colyseus.ext.js';\n\nimport { fileURLToPath } from 'url'; // required for ESM build (see build.mjs)\n\nexport type AuthConfig = {\n oauth: string[],\n register: boolean,\n anonymous: boolean,\n};\n\nexport function playground(): Router {\n applyMonkeyPatch();\n\n const router = express.Router();\n\n // serve static frontend\n router.use(\"/\", express.static(path.resolve(__dirname, \"..\", \"build\")));\n\n // expose matchmaking stats\n router.get(\"/rooms\", async (req, res) => {\n const rooms = await matchMaker.driver.query({});\n\n const roomsByType: { [roomName: string]: number } = {};\n const roomsById: { [roomName: string]: IRoomCache } = {};\n\n rooms.forEach((room) => {\n if (!roomsByType[room.name]) { roomsByType[room.name] = 0; }\n roomsByType[room.name]++;\n roomsById[room.roomId] = room;\n });\n\n res.json({\n rooms: Object.keys(matchMaker.getAllHandlers()),\n\n roomsByType,\n roomsById,\n\n auth: {\n // list of OAuth providers\n oauth: Object.keys(auth.oauth.providers),\n register: typeof(auth.settings.onRegisterWithEmailAndPassword) === \"function\",\n anonymous: typeof(JWT.settings.secret) === \"string\",\n } as AuthConfig\n });\n });\n\n // serve API docs for playground\n // (workaround to use better-call route inside express.Router)\n router.get(\"/__apidocs\", async (_, res) => {\n /**\n * Optional: if zod is available, we can use toJSONSchema() for body and query types\n */\n let z: any = undefined;\n try { z = await import(\"zod\"); } catch (e: any) { /* zod not installed */ }\n\n res.json(Object.values(__globalEndpoints).map((endpoint: Endpoint) => {\n return {\n method: endpoint.options.method,\n path: endpoint.path,\n body: z && endpoint.options.body && z.toJSONSchema(endpoint.options.body),\n query: z && endpoint.options.query && z.toJSONSchema(endpoint.options.query),\n metadata: endpoint.options.metadata,\n description: endpoint.options.metadata?.openapi?.description,\n };\n }));\n });\n\n return router;\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAAiB;AACjB,qBAAgC;AAChC,kBAA0B;AAC1B,kBAA0D;AAE1D,0BAAiC;AAU1B,SAAS,aAAqB;AACnC,4CAAiB;AAEjB,QAAM,SAAS,eAAAA,QAAQ,OAAO;AAG9B,SAAO,IAAI,KAAK,eAAAA,QAAQ,OAAO,YAAAC,QAAK,QAAQ,WAAW,MAAM,OAAO,CAAC,CAAC;AAGtE,SAAO,IAAI,UAAU,OAAO,KAAK,QAAQ;AACvC,UAAM,QAAQ,MAAM,uBAAW,OAAO,MAAM,CAAC,CAAC;AAE9C,UAAM,cAA8C,CAAC;AACrD,UAAM,YAAgD,CAAC;AAEvD,UAAM,QAAQ,CAAC,SAAS;AACtB,UAAI,CAAC,YAAY,KAAK,IAAI,GAAG;AAAE,oBAAY,KAAK,IAAI,IAAI;AAAA,MAAG;AAC3D,kBAAY,KAAK,IAAI;AACrB,gBAAU,KAAK,MAAM,IAAI;AAAA,IAC3B,CAAC;AAED,QAAI,KAAK;AAAA,MACP,OAAO,OAAO,KAAK,uBAAW,eAAe,CAAC;AAAA,MAE9C;AAAA,MACA;AAAA,MAEA,MAAM;AAAA;AAAA,QAEJ,OAAO,OAAO,KAAK,iBAAK,MAAM,SAAS;AAAA,QACvC,UAAU,OAAO,iBAAK,SAAS,mCAAoC;AAAA,QACnE,WAAW,OAAO,gBAAI,SAAS,WAAY;AAAA,MAC7C;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AAID,SAAO,IAAI,cAAc,OAAO,GAAG,QAAQ;AAIzC,QAAI,IAAS;AACb,QAAI;AAAE,UAAI,MAAM,OAAO,KAAK;AAAA,IAAG,SAAS,GAAP;AAAA,IAA0C;AAE3E,QAAI,KAAK,OAAO,OAAO,6BAAiB,EAAE,IAAI,CAAC,aAAuB;AA5D1E;AA6DM,aAAO;AAAA,QACL,QAAQ,SAAS,QAAQ;AAAA,QACzB,MAAM,SAAS;AAAA,QACf,MAAM,KAAK,SAAS,QAAQ,QAAQ,EAAE,aAAa,SAAS,QAAQ,IAAI;AAAA,QACxE,OAAO,KAAK,SAAS,QAAQ,SAAS,EAAE,aAAa,SAAS,QAAQ,KAAK;AAAA,QAC3E,UAAU,SAAS,QAAQ;AAAA,QAC3B,cAAa,oBAAS,QAAQ,aAAjB,mBAA2B,YAA3B,mBAAoC;AAAA,MACnD;AAAA,IACF,CAAC,CAAC;AAAA,EACJ,CAAC;AAED,SAAO;AACT;",
6
- "names": ["express", "path"]
4
+ "sourcesContent": ["import fs from 'fs/promises';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport inspector from 'node:inspector/promises';\nimport { spawn } from 'node:child_process';\nimport os from 'node:os';\nimport { createRequire } from 'node:module';\nimport type { IncomingMessage, ServerResponse } from 'http';\nimport { createEndpoint, dualModeEndpoints, isDevMode, logger, matchMaker, Server, type IRoomCache, type Endpoint } from '@colyseus/core';\nimport { auth, JWT } from '@colyseus/auth';\nimport { applyMonkeyPatch } from './colyseus.ext.js';\nimport { serveStatic } from './serve-static.js';\n\nexport type AuthConfig = {\n oauth: string[],\n register: boolean,\n anonymous: boolean,\n};\n\nexport interface PlaygroundOptions {\n /** Mount prefix used when spread into `createRouter`. Ignored in express-middleware mode (express strips its mount path). Defaults to `''`. */\n prefix?: string;\n /** Better-call middleware applied to every playground endpoint \u2014 use for auth gating. */\n use?: any[];\n}\n\nconst SPA_DIST = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'build');\n\n// CPU profiling state \u2014 single process, single active session. The captured\n// `.cpuprofile` and the cpupro-generated standalone HTML report are kept in\n// memory (latest only); a process restart clears them, which is fine for a\n// dev tool. See the `/profiling/*` endpoints below.\nlet profilingSession: inspector.Session | null = null;\nlet profilingStartedAt = 0;\nlet lastProfile: string | null = null;\nlet lastReportHtml: string | null = null;\nlet lastProfilingError: string | null = null;\n\n// cpupro is an OPTIONAL peer dependency \u2014 it pulls in a native addon\n// (v8-profiler-next) we don't use, so it's opt-in. Resolve its CLI bin if\n// installed, else null. cpupro's `exports` map hides `bin/cpupro`, so resolve\n// the package dir via its (exported) package.json and join the bin path.\nfunction resolveCpuproBin(): string | null {\n try {\n const require = createRequire(import.meta.url);\n return path.join(path.dirname(require.resolve('cpupro/package.json')), 'bin', 'cpupro');\n } catch {\n return null;\n }\n}\n\n// Render an existing `.cpuprofile` (JSON string) into a standalone,\n// self-contained cpupro HTML report by shelling out to the cpupro CLI. The\n// CLI is the documented, format-stable path and is agnostic to our cjs/esm\n// dual build.\nasync function generateCpuproReport(profileJson: string): Promise<string> {\n const bin = resolveCpuproBin();\n if (!bin) {\n throw new Error(\"cpupro is not installed \u2014 run `npm install cpupro` to render reports in the playground. The .cpuprofile is still available to download.\");\n }\n const stamp = Date.now();\n const profilePath = path.join(os.tmpdir(), `colyseus-profile-${stamp}.cpuprofile`);\n const reportName = `colyseus-report-${stamp}.html`;\n const reportPath = path.join(os.tmpdir(), reportName);\n await fs.writeFile(profilePath, profileJson);\n try {\n await new Promise<void>((resolve, reject) => {\n const child = spawn(process.execPath, [bin, profilePath, '-n', '-o', os.tmpdir(), '-f', reportName], { stdio: 'ignore' });\n child.on('error', reject);\n child.on('exit', (code) => code === 0 ? resolve() : reject(new Error(`cpupro exited with code ${code}`)));\n });\n return patchCpuproReport(await fs.readFile(reportPath, 'utf8'));\n } finally {\n await fs.rm(profilePath, { force: true }).catch(() => {});\n await fs.rm(reportPath, { force: true }).catch(() => {});\n }\n}\n\n// cpupro@0.7.0 ships a `build/report.html` template configured to fetch its\n// data from a sibling `model.data` URL (`dataSource:\"url\"`), yet `report.js`\n// appends the actual profile inline as `discoveryLoader.push()` chunks \u2014 a\n// mechanism only wired up when `dataSource===\"push\"`. As shipped the file is\n// therefore not self-contained: it requests `model.data` (404s through our\n// SPA) and throws `discoveryLoader is not defined`. Flip the data source to\n// `push` so the embedded chunks are consumed and the report stands alone.\nfunction patchCpuproReport(html: string): string {\n return html.replace(/dataSource:\"url\",data:[A-Za-z_$][\\w$]*\\.model\\.data/, 'dataSource:\"push\",data:1');\n}\n\nexport function playground(opts: PlaygroundOptions = {}) {\n applyMonkeyPatch();\n\n const prefix = opts.prefix ?? '';\n const use = opts.use ?? [];\n\n // Data endpoints leak internals (room listing + auth config, API schemas,\n // CPU profiles/stack frames). Open during local dev (devMode or a\n // non-production NODE_ENV); on production mounts require a `use:` guard \u2014\n // the guard is the opt-in. 404 hides the routes' existence.\n let warnedGuardedProd = false;\n const guardedDenied = () => {\n if (isDevMode || process.env.NODE_ENV !== 'production') { return null; }\n if (use.length === 0) { return new Response('Not found', { status: 404 }); }\n if (!warnedGuardedProd) {\n warnedGuardedProd = true;\n logger.warn('@colyseus/playground: serving in production behind `use:` middleware \u2014 make sure at least one enforces authentication.');\n }\n return null;\n };\n\n const endpoints: Record<string, Endpoint> = {\n 'playground-rooms': createEndpoint(`${prefix}/rooms`, { method: 'GET', use }, async () => {\n const denied = guardedDenied(); if (denied) { return denied; }\n const rooms = await matchMaker.driver.query({});\n const roomsByType: Record<string, number> = {};\n const roomsById: Record<string, IRoomCache> = {};\n rooms.forEach((room) => {\n roomsByType[room.name] = (roomsByType[room.name] ?? 0) + 1;\n roomsById[room.roomId] = room;\n });\n return {\n rooms: Object.keys(matchMaker.getAllHandlers()),\n roomsByType,\n roomsById,\n auth: {\n oauth: Object.keys(auth.oauth.providers),\n register: typeof auth.settings.onRegisterWithEmailAndPassword === 'function',\n anonymous: typeof JWT.settings.secret === 'string',\n } as AuthConfig,\n };\n }),\n\n 'playground-apidocs': createEndpoint(`${prefix}/__apidocs`, { method: 'GET', use }, async () => {\n const denied = guardedDenied(); if (denied) { return denied; }\n\n let z: any;\n try { z = await import('zod'); } catch { /* zod is an optional peer */ }\n const routerEndpoints: Record<string, any> = (Server.current?.router as any)?.endpoints ?? {};\n return Object.values(routerEndpoints)\n // SERVER_ONLY endpoints aren't routable \u2014 don't list them either\n .filter((endpoint: any) => !endpoint.options.metadata?.SERVER_ONLY)\n .map((endpoint: any) => ({\n method: endpoint.options.method,\n path: endpoint.path,\n body: z && endpoint.options.body && z.toJSONSchema(endpoint.options.body),\n query: z && endpoint.options.query && z.toJSONSchema(endpoint.options.query),\n metadata: endpoint.options.metadata,\n description: endpoint.options.metadata?.openapi?.description,\n }));\n }),\n\n 'profiling-start': createEndpoint(`${prefix}/profiling/start`, { method: 'POST', use }, async (ctx) => {\n const denied = guardedDenied(); if (denied) { return denied; }\n if (profilingSession) {\n return { status: 'running', startedAt: profilingStartedAt, error: 'already running' };\n }\n const interval = Number((ctx.query as any)?.interval) || 100;\n const session = new inspector.Session();\n session.connect();\n await session.post('Profiler.enable');\n await session.post('Profiler.setSamplingInterval', { interval });\n await session.post('Profiler.start');\n profilingSession = session;\n profilingStartedAt = Date.now();\n lastProfile = null;\n lastReportHtml = null;\n lastProfilingError = null;\n return { status: 'started', startedAt: profilingStartedAt, interval };\n }),\n\n 'profiling-stop': createEndpoint(`${prefix}/profiling/stop`, { method: 'POST', use }, async () => {\n const denied = guardedDenied(); if (denied) { return denied; }\n if (!profilingSession) {\n return { status: 'idle', error: 'not running' };\n }\n const session = profilingSession;\n profilingSession = null;\n const durationMs = Date.now() - profilingStartedAt;\n const { profile } = await session.post('Profiler.stop');\n session.disconnect();\n lastProfile = JSON.stringify(profile);\n lastReportHtml = null;\n lastProfilingError = null;\n try {\n lastReportHtml = await generateCpuproReport(lastProfile);\n } catch (e: any) {\n lastProfilingError = e?.message ?? String(e);\n }\n return {\n status: 'stopped',\n durationMs,\n profileBytes: lastProfile.length,\n reportReady: !!lastReportHtml,\n error: lastProfilingError,\n };\n }),\n\n 'profiling-status': createEndpoint(`${prefix}/profiling/status`, { method: 'GET', use }, async () => {\n const denied = guardedDenied(); if (denied) { return denied; }\n return {\n running: !!profilingSession,\n startedAt: profilingSession ? profilingStartedAt : 0,\n hasProfile: !!lastProfile,\n hasReport: !!lastReportHtml,\n // false when the optional `cpupro` peer dep isn't installed \u2014 the UI\n // uses this to warn up front that captures are download-only.\n reportSupported: !!resolveCpuproBin(),\n error: lastProfilingError,\n };\n }),\n\n 'profiling-profile': createEndpoint(`${prefix}/profiling/profile.cpuprofile`, { method: 'GET', use }, async () => {\n const denied = guardedDenied(); if (denied) { return denied; }\n if (!lastProfile) { return new Response('No profile captured', { status: 404 }); }\n const buf = Buffer.from(lastProfile, 'utf8');\n return new Response(new Uint8Array(buf) as any, {\n status: 200,\n headers: {\n 'content-type': 'application/json',\n 'content-length': String(buf.length),\n 'content-disposition': 'attachment; filename=\"profile.cpuprofile\"',\n 'cache-control': 'no-cache',\n // serve-static.ts sets this for the same reason: better-call's node\n // adapter doesn't call res.end() on a backpressured single-chunk\n // body, so a keep-alive response is left unterminated and browsers\n // spin. Closing the connection terminates it cleanly.\n 'connection': 'close',\n },\n });\n }),\n\n 'profiling-report': createEndpoint(`${prefix}/profiling/report.html`, { method: 'GET', use }, async () => {\n const denied = guardedDenied(); if (denied) { return denied; }\n if (!lastReportHtml) { return new Response('No report available', { status: 404 }); }\n const buf = Buffer.from(lastReportHtml, 'utf8');\n return new Response(new Uint8Array(buf) as any, {\n status: 200,\n headers: {\n 'content-type': 'text/html; charset=utf-8',\n 'content-length': String(buf.length),\n 'cache-control': 'no-cache',\n // see profiling-profile above \u2014 `connection: close` so the browser\n // doesn't spin on better-call's unterminated keep-alive response.\n 'connection': 'close',\n },\n });\n }),\n\n 'playground-index': createEndpoint(`${prefix}/`, { method: 'GET', use }, async () => {\n return serveStatic(SPA_DIST, '');\n }),\n\n 'playground-static': createEndpoint(`${prefix}/**:splat`, { method: 'GET', use }, async (ctx) => {\n return serveStatic(SPA_DIST, (ctx.params as any).splat);\n }),\n };\n\n const SPA_DIST_RESOLVED = path.resolve(SPA_DIST);\n\n return dualModeEndpoints(endpoints, {\n catchAllKey: 'playground-static',\n buildMiddleware: ({ fullHandler }) => {\n // Strip express's `baseUrl` so the inner router sees the request\n // relative to its mount point \u2014 playground's endpoint paths assume\n // a `''` prefix, so a sub-mount like `app.use(\"/foo\", playground())`\n // needs the leading `/foo` stripped before dispatch.\n const dispatch = (req: IncomingMessage, res: ServerResponse, next: (e?: any) => void) => {\n const stripped = Object.create(req, {\n baseUrl: { value: '', enumerable: true, configurable: true },\n originalUrl: { value: req.url, enumerable: true, configurable: true },\n });\n fullHandler(stripped as any, res as any).catch(next);\n };\n\n return (req, res, next) => {\n const url = (req.url ?? '').split('?')[0]!;\n const isProfiling = url.startsWith('/profiling');\n\n // Profiling start/stop are POSTs; everything else here is GET-only.\n if (req.method === 'POST') {\n return isProfiling ? dispatch(req, res, next) : next();\n }\n if (req.method !== 'GET') { return next(); }\n\n if (url === '/' || url === '/rooms' || url === '/__apidocs' || isProfiling) {\n return dispatch(req, res, next);\n }\n\n // Asset request \u2014 only delegate if the file actually exists on\n // disk. Avoids the catch-all's SPA fallback (which would serve\n // index.html for unknown paths, masking sibling express routes).\n const rel = url.replace(/^\\/+/, '');\n if (!rel || rel.includes('..')) { return next(); }\n const filePath = path.resolve(SPA_DIST_RESOLVED, rel);\n if (!filePath.startsWith(SPA_DIST_RESOLVED + path.sep)) { return next(); }\n\n fs.stat(filePath).then((stat) => {\n if (stat.isFile()) {\n dispatch(req, res, next);\n } else {\n next();\n }\n }).catch(() => next());\n };\n },\n });\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["import_promises", "path", "require", "os", "fs", "_a", "_b", "inspector"]
7
7
  }
package/build/index.d.ts CHANGED
@@ -1,7 +1,13 @@
1
- import { Router } from 'express';
1
+ import { type Endpoint } from '@colyseus/core';
2
2
  export type AuthConfig = {
3
3
  oauth: string[];
4
4
  register: boolean;
5
5
  anonymous: boolean;
6
6
  };
7
- export declare function playground(): Router;
7
+ export interface PlaygroundOptions {
8
+ /** Mount prefix used when spread into `createRouter`. Ignored in express-middleware mode (express strips its mount path). Defaults to `''`. */
9
+ prefix?: string;
10
+ /** Better-call middleware applied to every playground endpoint — use for auth gating. */
11
+ use?: any[];
12
+ }
13
+ export declare function playground(opts?: PlaygroundOptions): import("@colyseus/core").ExpressMiddleware & Record<string, Endpoint>;
package/build/index.html CHANGED
@@ -12,8 +12,8 @@
12
12
  <link rel="apple-touch-icon" href="./logo192.png" />
13
13
  <link rel="manifest" href="./manifest.json" crossorigin="use-credentials" />
14
14
  <title>[Colyseus Playground]</title>
15
- <script type="module" crossorigin src="./assets/index-D6rlB4zv.js"></script>
16
- <link rel="stylesheet" crossorigin href="./assets/index-BS3RJoKX.css">
15
+ <script type="module" crossorigin src="./assets/index-B7uMF8vk.js"></script>
16
+ <link rel="stylesheet" crossorigin href="./assets/index-BvbHcjwZ.css">
17
17
  </head>
18
18
  <body>
19
19
  <noscript>You need to enable JavaScript to run this app.</noscript>
package/build/index.mjs CHANGED
@@ -1,55 +1,277 @@
1
1
  // src-backend/index.ts
2
+ import fs from "fs/promises";
2
3
  import path from "path";
3
- import express from "express";
4
+ import { fileURLToPath } from "url";
5
+ import inspector from "node:inspector/promises";
6
+ import { spawn } from "node:child_process";
7
+ import os from "node:os";
8
+ import { createRequire } from "node:module";
9
+ import { createEndpoint, dualModeEndpoints, isDevMode, logger, matchMaker, Server } from "@colyseus/core";
4
10
  import { auth, JWT } from "@colyseus/auth";
5
- import { matchMaker, __globalEndpoints } from "@colyseus/core";
6
11
  import { applyMonkeyPatch } from "./colyseus.ext.mjs";
7
- import { fileURLToPath } from "url";
8
- function playground() {
9
- applyMonkeyPatch();
10
- const router = express.Router();
11
- router.use("/", express.static(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "build")));
12
- router.get("/rooms", async (req, res) => {
13
- const rooms = await matchMaker.driver.query({});
14
- const roomsByType = {};
15
- const roomsById = {};
16
- rooms.forEach((room) => {
17
- if (!roomsByType[room.name]) {
18
- roomsByType[room.name] = 0;
19
- }
20
- roomsByType[room.name]++;
21
- roomsById[room.roomId] = room;
12
+ import { serveStatic } from "./serve-static.mjs";
13
+ var SPA_DIST = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "build");
14
+ var profilingSession = null;
15
+ var profilingStartedAt = 0;
16
+ var lastProfile = null;
17
+ var lastReportHtml = null;
18
+ var lastProfilingError = null;
19
+ function resolveCpuproBin() {
20
+ try {
21
+ const require2 = createRequire(import.meta.url);
22
+ return path.join(path.dirname(require2.resolve("cpupro/package.json")), "bin", "cpupro");
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+ async function generateCpuproReport(profileJson) {
28
+ const bin = resolveCpuproBin();
29
+ if (!bin) {
30
+ throw new Error("cpupro is not installed \u2014 run `npm install cpupro` to render reports in the playground. The .cpuprofile is still available to download.");
31
+ }
32
+ const stamp = Date.now();
33
+ const profilePath = path.join(os.tmpdir(), `colyseus-profile-${stamp}.cpuprofile`);
34
+ const reportName = `colyseus-report-${stamp}.html`;
35
+ const reportPath = path.join(os.tmpdir(), reportName);
36
+ await fs.writeFile(profilePath, profileJson);
37
+ try {
38
+ await new Promise((resolve, reject) => {
39
+ const child = spawn(process.execPath, [bin, profilePath, "-n", "-o", os.tmpdir(), "-f", reportName], { stdio: "ignore" });
40
+ child.on("error", reject);
41
+ child.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`cpupro exited with code ${code}`)));
22
42
  });
23
- res.json({
24
- rooms: Object.keys(matchMaker.getAllHandlers()),
25
- roomsByType,
26
- roomsById,
27
- auth: {
28
- // list of OAuth providers
29
- oauth: Object.keys(auth.oauth.providers),
30
- register: typeof auth.settings.onRegisterWithEmailAndPassword === "function",
31
- anonymous: typeof JWT.settings.secret === "string"
32
- }
43
+ return patchCpuproReport(await fs.readFile(reportPath, "utf8"));
44
+ } finally {
45
+ await fs.rm(profilePath, { force: true }).catch(() => {
33
46
  });
34
- });
35
- router.get("/__apidocs", async (_, res) => {
36
- let z = void 0;
37
- try {
38
- z = await import("zod");
39
- } catch (e) {
47
+ await fs.rm(reportPath, { force: true }).catch(() => {
48
+ });
49
+ }
50
+ }
51
+ function patchCpuproReport(html) {
52
+ return html.replace(/dataSource:"url",data:[A-Za-z_$][\w$]*\.model\.data/, 'dataSource:"push",data:1');
53
+ }
54
+ function playground(opts = {}) {
55
+ applyMonkeyPatch();
56
+ const prefix = opts.prefix ?? "";
57
+ const use = opts.use ?? [];
58
+ let warnedGuardedProd = false;
59
+ const guardedDenied = () => {
60
+ if (isDevMode || process.env.NODE_ENV !== "production") {
61
+ return null;
62
+ }
63
+ if (use.length === 0) {
64
+ return new Response("Not found", { status: 404 });
65
+ }
66
+ if (!warnedGuardedProd) {
67
+ warnedGuardedProd = true;
68
+ logger.warn("@colyseus/playground: serving in production behind `use:` middleware \u2014 make sure at least one enforces authentication.");
40
69
  }
41
- res.json(Object.values(__globalEndpoints).map((endpoint) => {
70
+ return null;
71
+ };
72
+ const endpoints = {
73
+ "playground-rooms": createEndpoint(`${prefix}/rooms`, { method: "GET", use }, async () => {
74
+ const denied = guardedDenied();
75
+ if (denied) {
76
+ return denied;
77
+ }
78
+ const rooms = await matchMaker.driver.query({});
79
+ const roomsByType = {};
80
+ const roomsById = {};
81
+ rooms.forEach((room) => {
82
+ roomsByType[room.name] = (roomsByType[room.name] ?? 0) + 1;
83
+ roomsById[room.roomId] = room;
84
+ });
42
85
  return {
86
+ rooms: Object.keys(matchMaker.getAllHandlers()),
87
+ roomsByType,
88
+ roomsById,
89
+ auth: {
90
+ oauth: Object.keys(auth.oauth.providers),
91
+ register: typeof auth.settings.onRegisterWithEmailAndPassword === "function",
92
+ anonymous: typeof JWT.settings.secret === "string"
93
+ }
94
+ };
95
+ }),
96
+ "playground-apidocs": createEndpoint(`${prefix}/__apidocs`, { method: "GET", use }, async () => {
97
+ const denied = guardedDenied();
98
+ if (denied) {
99
+ return denied;
100
+ }
101
+ let z;
102
+ try {
103
+ z = await import("zod");
104
+ } catch {
105
+ }
106
+ const routerEndpoints = Server.current?.router?.endpoints ?? {};
107
+ return Object.values(routerEndpoints).filter((endpoint) => !endpoint.options.metadata?.SERVER_ONLY).map((endpoint) => ({
43
108
  method: endpoint.options.method,
44
109
  path: endpoint.path,
45
110
  body: z && endpoint.options.body && z.toJSONSchema(endpoint.options.body),
46
111
  query: z && endpoint.options.query && z.toJSONSchema(endpoint.options.query),
47
112
  metadata: endpoint.options.metadata,
48
113
  description: endpoint.options.metadata?.openapi?.description
114
+ }));
115
+ }),
116
+ "profiling-start": createEndpoint(`${prefix}/profiling/start`, { method: "POST", use }, async (ctx) => {
117
+ const denied = guardedDenied();
118
+ if (denied) {
119
+ return denied;
120
+ }
121
+ if (profilingSession) {
122
+ return { status: "running", startedAt: profilingStartedAt, error: "already running" };
123
+ }
124
+ const interval = Number(ctx.query?.interval) || 100;
125
+ const session = new inspector.Session();
126
+ session.connect();
127
+ await session.post("Profiler.enable");
128
+ await session.post("Profiler.setSamplingInterval", { interval });
129
+ await session.post("Profiler.start");
130
+ profilingSession = session;
131
+ profilingStartedAt = Date.now();
132
+ lastProfile = null;
133
+ lastReportHtml = null;
134
+ lastProfilingError = null;
135
+ return { status: "started", startedAt: profilingStartedAt, interval };
136
+ }),
137
+ "profiling-stop": createEndpoint(`${prefix}/profiling/stop`, { method: "POST", use }, async () => {
138
+ const denied = guardedDenied();
139
+ if (denied) {
140
+ return denied;
141
+ }
142
+ if (!profilingSession) {
143
+ return { status: "idle", error: "not running" };
144
+ }
145
+ const session = profilingSession;
146
+ profilingSession = null;
147
+ const durationMs = Date.now() - profilingStartedAt;
148
+ const { profile } = await session.post("Profiler.stop");
149
+ session.disconnect();
150
+ lastProfile = JSON.stringify(profile);
151
+ lastReportHtml = null;
152
+ lastProfilingError = null;
153
+ try {
154
+ lastReportHtml = await generateCpuproReport(lastProfile);
155
+ } catch (e) {
156
+ lastProfilingError = e?.message ?? String(e);
157
+ }
158
+ return {
159
+ status: "stopped",
160
+ durationMs,
161
+ profileBytes: lastProfile.length,
162
+ reportReady: !!lastReportHtml,
163
+ error: lastProfilingError
164
+ };
165
+ }),
166
+ "profiling-status": createEndpoint(`${prefix}/profiling/status`, { method: "GET", use }, async () => {
167
+ const denied = guardedDenied();
168
+ if (denied) {
169
+ return denied;
170
+ }
171
+ return {
172
+ running: !!profilingSession,
173
+ startedAt: profilingSession ? profilingStartedAt : 0,
174
+ hasProfile: !!lastProfile,
175
+ hasReport: !!lastReportHtml,
176
+ // false when the optional `cpupro` peer dep isn't installed — the UI
177
+ // uses this to warn up front that captures are download-only.
178
+ reportSupported: !!resolveCpuproBin(),
179
+ error: lastProfilingError
180
+ };
181
+ }),
182
+ "profiling-profile": createEndpoint(`${prefix}/profiling/profile.cpuprofile`, { method: "GET", use }, async () => {
183
+ const denied = guardedDenied();
184
+ if (denied) {
185
+ return denied;
186
+ }
187
+ if (!lastProfile) {
188
+ return new Response("No profile captured", { status: 404 });
189
+ }
190
+ const buf = Buffer.from(lastProfile, "utf8");
191
+ return new Response(new Uint8Array(buf), {
192
+ status: 200,
193
+ headers: {
194
+ "content-type": "application/json",
195
+ "content-length": String(buf.length),
196
+ "content-disposition": 'attachment; filename="profile.cpuprofile"',
197
+ "cache-control": "no-cache",
198
+ // serve-static.ts sets this for the same reason: better-call's node
199
+ // adapter doesn't call res.end() on a backpressured single-chunk
200
+ // body, so a keep-alive response is left unterminated and browsers
201
+ // spin. Closing the connection terminates it cleanly.
202
+ "connection": "close"
203
+ }
204
+ });
205
+ }),
206
+ "profiling-report": createEndpoint(`${prefix}/profiling/report.html`, { method: "GET", use }, async () => {
207
+ const denied = guardedDenied();
208
+ if (denied) {
209
+ return denied;
210
+ }
211
+ if (!lastReportHtml) {
212
+ return new Response("No report available", { status: 404 });
213
+ }
214
+ const buf = Buffer.from(lastReportHtml, "utf8");
215
+ return new Response(new Uint8Array(buf), {
216
+ status: 200,
217
+ headers: {
218
+ "content-type": "text/html; charset=utf-8",
219
+ "content-length": String(buf.length),
220
+ "cache-control": "no-cache",
221
+ // see profiling-profile above — `connection: close` so the browser
222
+ // doesn't spin on better-call's unterminated keep-alive response.
223
+ "connection": "close"
224
+ }
225
+ });
226
+ }),
227
+ "playground-index": createEndpoint(`${prefix}/`, { method: "GET", use }, async () => {
228
+ return serveStatic(SPA_DIST, "");
229
+ }),
230
+ "playground-static": createEndpoint(`${prefix}/**:splat`, { method: "GET", use }, async (ctx) => {
231
+ return serveStatic(SPA_DIST, ctx.params.splat);
232
+ })
233
+ };
234
+ const SPA_DIST_RESOLVED = path.resolve(SPA_DIST);
235
+ return dualModeEndpoints(endpoints, {
236
+ catchAllKey: "playground-static",
237
+ buildMiddleware: ({ fullHandler }) => {
238
+ const dispatch = (req, res, next) => {
239
+ const stripped = Object.create(req, {
240
+ baseUrl: { value: "", enumerable: true, configurable: true },
241
+ originalUrl: { value: req.url, enumerable: true, configurable: true }
242
+ });
243
+ fullHandler(stripped, res).catch(next);
49
244
  };
50
- }));
245
+ return (req, res, next) => {
246
+ const url = (req.url ?? "").split("?")[0];
247
+ const isProfiling = url.startsWith("/profiling");
248
+ if (req.method === "POST") {
249
+ return isProfiling ? dispatch(req, res, next) : next();
250
+ }
251
+ if (req.method !== "GET") {
252
+ return next();
253
+ }
254
+ if (url === "/" || url === "/rooms" || url === "/__apidocs" || isProfiling) {
255
+ return dispatch(req, res, next);
256
+ }
257
+ const rel = url.replace(/^\/+/, "");
258
+ if (!rel || rel.includes("..")) {
259
+ return next();
260
+ }
261
+ const filePath = path.resolve(SPA_DIST_RESOLVED, rel);
262
+ if (!filePath.startsWith(SPA_DIST_RESOLVED + path.sep)) {
263
+ return next();
264
+ }
265
+ fs.stat(filePath).then((stat) => {
266
+ if (stat.isFile()) {
267
+ dispatch(req, res, next);
268
+ } else {
269
+ next();
270
+ }
271
+ }).catch(() => next());
272
+ };
273
+ }
51
274
  });
52
- return router;
53
275
  }
54
276
  export {
55
277
  playground
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src-backend/index.ts"],
4
- "sourcesContent": ["import path from 'path';\nimport express, { Router } from 'express';\nimport { auth, JWT } from '@colyseus/auth';\nimport { matchMaker, IRoomCache, __globalEndpoints } from '@colyseus/core';\nimport type { Endpoint } from \"@colyseus/better-call\";\nimport { applyMonkeyPatch } from './colyseus.ext.js';\n\nimport { fileURLToPath } from 'url'; // required for ESM build (see build.mjs)\n\nexport type AuthConfig = {\n oauth: string[],\n register: boolean,\n anonymous: boolean,\n};\n\nexport function playground(): Router {\n applyMonkeyPatch();\n\n const router = express.Router();\n\n // serve static frontend\n router.use(\"/\", express.static(path.resolve(path.dirname(fileURLToPath(import.meta.url)), \"..\", \"build\")));\n\n // expose matchmaking stats\n router.get(\"/rooms\", async (req, res) => {\n const rooms = await matchMaker.driver.query({});\n\n const roomsByType: { [roomName: string]: number } = {};\n const roomsById: { [roomName: string]: IRoomCache } = {};\n\n rooms.forEach((room) => {\n if (!roomsByType[room.name]) { roomsByType[room.name] = 0; }\n roomsByType[room.name]++;\n roomsById[room.roomId] = room;\n });\n\n res.json({\n rooms: Object.keys(matchMaker.getAllHandlers()),\n\n roomsByType,\n roomsById,\n\n auth: {\n // list of OAuth providers\n oauth: Object.keys(auth.oauth.providers),\n register: typeof(auth.settings.onRegisterWithEmailAndPassword) === \"function\",\n anonymous: typeof(JWT.settings.secret) === \"string\",\n } as AuthConfig\n });\n });\n\n // serve API docs for playground\n // (workaround to use better-call route inside express.Router)\n router.get(\"/__apidocs\", async (_, res) => {\n /**\n * Optional: if zod is available, we can use toJSONSchema() for body and query types\n */\n let z: any = undefined;\n try { z = await import(\"zod\"); } catch (e: any) { /* zod not installed */ }\n\n res.json(Object.values(__globalEndpoints).map((endpoint: Endpoint) => {\n return {\n method: endpoint.options.method,\n path: endpoint.path,\n body: z && endpoint.options.body && z.toJSONSchema(endpoint.options.body),\n query: z && endpoint.options.query && z.toJSONSchema(endpoint.options.query),\n metadata: endpoint.options.metadata,\n description: endpoint.options.metadata?.openapi?.description,\n };\n }));\n });\n\n return router;\n}\n"],
5
- "mappings": ";AAAA,OAAO,UAAU;AACjB,OAAO,aAAyB;AAChC,SAAS,MAAM,WAAW;AAC1B,SAAS,YAAwB,yBAAyB;AAE1D,SAAS,wBAAwB;AAEjC,SAAS,qBAAqB;AAQvB,SAAS,aAAqB;AACnC,mBAAiB;AAEjB,QAAM,SAAS,QAAQ,OAAO;AAG9B,SAAO,IAAI,KAAK,QAAQ,OAAO,KAAK,QAAQ,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC,GAAG,MAAM,OAAO,CAAC,CAAC;AAGzG,SAAO,IAAI,UAAU,OAAO,KAAK,QAAQ;AACvC,UAAM,QAAQ,MAAM,WAAW,OAAO,MAAM,CAAC,CAAC;AAE9C,UAAM,cAA8C,CAAC;AACrD,UAAM,YAAgD,CAAC;AAEvD,UAAM,QAAQ,CAAC,SAAS;AACtB,UAAI,CAAC,YAAY,KAAK,IAAI,GAAG;AAAE,oBAAY,KAAK,IAAI,IAAI;AAAA,MAAG;AAC3D,kBAAY,KAAK,IAAI;AACrB,gBAAU,KAAK,MAAM,IAAI;AAAA,IAC3B,CAAC;AAED,QAAI,KAAK;AAAA,MACP,OAAO,OAAO,KAAK,WAAW,eAAe,CAAC;AAAA,MAE9C;AAAA,MACA;AAAA,MAEA,MAAM;AAAA;AAAA,QAEJ,OAAO,OAAO,KAAK,KAAK,MAAM,SAAS;AAAA,QACvC,UAAU,OAAO,KAAK,SAAS,mCAAoC;AAAA,QACnE,WAAW,OAAO,IAAI,SAAS,WAAY;AAAA,MAC7C;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AAID,SAAO,IAAI,cAAc,OAAO,GAAG,QAAQ;AAIzC,QAAI,IAAS;AACb,QAAI;AAAE,UAAI,MAAM,OAAO,KAAK;AAAA,IAAG,SAAS,GAAP;AAAA,IAA0C;AAE3E,QAAI,KAAK,OAAO,OAAO,iBAAiB,EAAE,IAAI,CAAC,aAAuB;AACpE,aAAO;AAAA,QACL,QAAQ,SAAS,QAAQ;AAAA,QACzB,MAAM,SAAS;AAAA,QACf,MAAM,KAAK,SAAS,QAAQ,QAAQ,EAAE,aAAa,SAAS,QAAQ,IAAI;AAAA,QACxE,OAAO,KAAK,SAAS,QAAQ,SAAS,EAAE,aAAa,SAAS,QAAQ,KAAK;AAAA,QAC3E,UAAU,SAAS,QAAQ;AAAA,QAC3B,aAAa,SAAS,QAAQ,UAAU,SAAS;AAAA,MACnD;AAAA,IACF,CAAC,CAAC;AAAA,EACJ,CAAC;AAED,SAAO;AACT;",
6
- "names": []
4
+ "sourcesContent": ["import fs from 'fs/promises';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport inspector from 'node:inspector/promises';\nimport { spawn } from 'node:child_process';\nimport os from 'node:os';\nimport { createRequire } from 'node:module';\nimport type { IncomingMessage, ServerResponse } from 'http';\nimport { createEndpoint, dualModeEndpoints, isDevMode, logger, matchMaker, Server, type IRoomCache, type Endpoint } from '@colyseus/core';\nimport { auth, JWT } from '@colyseus/auth';\nimport { applyMonkeyPatch } from './colyseus.ext.js';\nimport { serveStatic } from './serve-static.js';\n\nexport type AuthConfig = {\n oauth: string[],\n register: boolean,\n anonymous: boolean,\n};\n\nexport interface PlaygroundOptions {\n /** Mount prefix used when spread into `createRouter`. Ignored in express-middleware mode (express strips its mount path). Defaults to `''`. */\n prefix?: string;\n /** Better-call middleware applied to every playground endpoint \u2014 use for auth gating. */\n use?: any[];\n}\n\nconst SPA_DIST = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'build');\n\n// CPU profiling state \u2014 single process, single active session. The captured\n// `.cpuprofile` and the cpupro-generated standalone HTML report are kept in\n// memory (latest only); a process restart clears them, which is fine for a\n// dev tool. See the `/profiling/*` endpoints below.\nlet profilingSession: inspector.Session | null = null;\nlet profilingStartedAt = 0;\nlet lastProfile: string | null = null;\nlet lastReportHtml: string | null = null;\nlet lastProfilingError: string | null = null;\n\n// cpupro is an OPTIONAL peer dependency \u2014 it pulls in a native addon\n// (v8-profiler-next) we don't use, so it's opt-in. Resolve its CLI bin if\n// installed, else null. cpupro's `exports` map hides `bin/cpupro`, so resolve\n// the package dir via its (exported) package.json and join the bin path.\nfunction resolveCpuproBin(): string | null {\n try {\n const require = createRequire(import.meta.url);\n return path.join(path.dirname(require.resolve('cpupro/package.json')), 'bin', 'cpupro');\n } catch {\n return null;\n }\n}\n\n// Render an existing `.cpuprofile` (JSON string) into a standalone,\n// self-contained cpupro HTML report by shelling out to the cpupro CLI. The\n// CLI is the documented, format-stable path and is agnostic to our cjs/esm\n// dual build.\nasync function generateCpuproReport(profileJson: string): Promise<string> {\n const bin = resolveCpuproBin();\n if (!bin) {\n throw new Error(\"cpupro is not installed \u2014 run `npm install cpupro` to render reports in the playground. The .cpuprofile is still available to download.\");\n }\n const stamp = Date.now();\n const profilePath = path.join(os.tmpdir(), `colyseus-profile-${stamp}.cpuprofile`);\n const reportName = `colyseus-report-${stamp}.html`;\n const reportPath = path.join(os.tmpdir(), reportName);\n await fs.writeFile(profilePath, profileJson);\n try {\n await new Promise<void>((resolve, reject) => {\n const child = spawn(process.execPath, [bin, profilePath, '-n', '-o', os.tmpdir(), '-f', reportName], { stdio: 'ignore' });\n child.on('error', reject);\n child.on('exit', (code) => code === 0 ? resolve() : reject(new Error(`cpupro exited with code ${code}`)));\n });\n return patchCpuproReport(await fs.readFile(reportPath, 'utf8'));\n } finally {\n await fs.rm(profilePath, { force: true }).catch(() => {});\n await fs.rm(reportPath, { force: true }).catch(() => {});\n }\n}\n\n// cpupro@0.7.0 ships a `build/report.html` template configured to fetch its\n// data from a sibling `model.data` URL (`dataSource:\"url\"`), yet `report.js`\n// appends the actual profile inline as `discoveryLoader.push()` chunks \u2014 a\n// mechanism only wired up when `dataSource===\"push\"`. As shipped the file is\n// therefore not self-contained: it requests `model.data` (404s through our\n// SPA) and throws `discoveryLoader is not defined`. Flip the data source to\n// `push` so the embedded chunks are consumed and the report stands alone.\nfunction patchCpuproReport(html: string): string {\n return html.replace(/dataSource:\"url\",data:[A-Za-z_$][\\w$]*\\.model\\.data/, 'dataSource:\"push\",data:1');\n}\n\nexport function playground(opts: PlaygroundOptions = {}) {\n applyMonkeyPatch();\n\n const prefix = opts.prefix ?? '';\n const use = opts.use ?? [];\n\n // Data endpoints leak internals (room listing + auth config, API schemas,\n // CPU profiles/stack frames). Open during local dev (devMode or a\n // non-production NODE_ENV); on production mounts require a `use:` guard \u2014\n // the guard is the opt-in. 404 hides the routes' existence.\n let warnedGuardedProd = false;\n const guardedDenied = () => {\n if (isDevMode || process.env.NODE_ENV !== 'production') { return null; }\n if (use.length === 0) { return new Response('Not found', { status: 404 }); }\n if (!warnedGuardedProd) {\n warnedGuardedProd = true;\n logger.warn('@colyseus/playground: serving in production behind `use:` middleware \u2014 make sure at least one enforces authentication.');\n }\n return null;\n };\n\n const endpoints: Record<string, Endpoint> = {\n 'playground-rooms': createEndpoint(`${prefix}/rooms`, { method: 'GET', use }, async () => {\n const denied = guardedDenied(); if (denied) { return denied; }\n const rooms = await matchMaker.driver.query({});\n const roomsByType: Record<string, number> = {};\n const roomsById: Record<string, IRoomCache> = {};\n rooms.forEach((room) => {\n roomsByType[room.name] = (roomsByType[room.name] ?? 0) + 1;\n roomsById[room.roomId] = room;\n });\n return {\n rooms: Object.keys(matchMaker.getAllHandlers()),\n roomsByType,\n roomsById,\n auth: {\n oauth: Object.keys(auth.oauth.providers),\n register: typeof auth.settings.onRegisterWithEmailAndPassword === 'function',\n anonymous: typeof JWT.settings.secret === 'string',\n } as AuthConfig,\n };\n }),\n\n 'playground-apidocs': createEndpoint(`${prefix}/__apidocs`, { method: 'GET', use }, async () => {\n const denied = guardedDenied(); if (denied) { return denied; }\n\n let z: any;\n try { z = await import('zod'); } catch { /* zod is an optional peer */ }\n const routerEndpoints: Record<string, any> = (Server.current?.router as any)?.endpoints ?? {};\n return Object.values(routerEndpoints)\n // SERVER_ONLY endpoints aren't routable \u2014 don't list them either\n .filter((endpoint: any) => !endpoint.options.metadata?.SERVER_ONLY)\n .map((endpoint: any) => ({\n method: endpoint.options.method,\n path: endpoint.path,\n body: z && endpoint.options.body && z.toJSONSchema(endpoint.options.body),\n query: z && endpoint.options.query && z.toJSONSchema(endpoint.options.query),\n metadata: endpoint.options.metadata,\n description: endpoint.options.metadata?.openapi?.description,\n }));\n }),\n\n 'profiling-start': createEndpoint(`${prefix}/profiling/start`, { method: 'POST', use }, async (ctx) => {\n const denied = guardedDenied(); if (denied) { return denied; }\n if (profilingSession) {\n return { status: 'running', startedAt: profilingStartedAt, error: 'already running' };\n }\n const interval = Number((ctx.query as any)?.interval) || 100;\n const session = new inspector.Session();\n session.connect();\n await session.post('Profiler.enable');\n await session.post('Profiler.setSamplingInterval', { interval });\n await session.post('Profiler.start');\n profilingSession = session;\n profilingStartedAt = Date.now();\n lastProfile = null;\n lastReportHtml = null;\n lastProfilingError = null;\n return { status: 'started', startedAt: profilingStartedAt, interval };\n }),\n\n 'profiling-stop': createEndpoint(`${prefix}/profiling/stop`, { method: 'POST', use }, async () => {\n const denied = guardedDenied(); if (denied) { return denied; }\n if (!profilingSession) {\n return { status: 'idle', error: 'not running' };\n }\n const session = profilingSession;\n profilingSession = null;\n const durationMs = Date.now() - profilingStartedAt;\n const { profile } = await session.post('Profiler.stop');\n session.disconnect();\n lastProfile = JSON.stringify(profile);\n lastReportHtml = null;\n lastProfilingError = null;\n try {\n lastReportHtml = await generateCpuproReport(lastProfile);\n } catch (e: any) {\n lastProfilingError = e?.message ?? String(e);\n }\n return {\n status: 'stopped',\n durationMs,\n profileBytes: lastProfile.length,\n reportReady: !!lastReportHtml,\n error: lastProfilingError,\n };\n }),\n\n 'profiling-status': createEndpoint(`${prefix}/profiling/status`, { method: 'GET', use }, async () => {\n const denied = guardedDenied(); if (denied) { return denied; }\n return {\n running: !!profilingSession,\n startedAt: profilingSession ? profilingStartedAt : 0,\n hasProfile: !!lastProfile,\n hasReport: !!lastReportHtml,\n // false when the optional `cpupro` peer dep isn't installed \u2014 the UI\n // uses this to warn up front that captures are download-only.\n reportSupported: !!resolveCpuproBin(),\n error: lastProfilingError,\n };\n }),\n\n 'profiling-profile': createEndpoint(`${prefix}/profiling/profile.cpuprofile`, { method: 'GET', use }, async () => {\n const denied = guardedDenied(); if (denied) { return denied; }\n if (!lastProfile) { return new Response('No profile captured', { status: 404 }); }\n const buf = Buffer.from(lastProfile, 'utf8');\n return new Response(new Uint8Array(buf) as any, {\n status: 200,\n headers: {\n 'content-type': 'application/json',\n 'content-length': String(buf.length),\n 'content-disposition': 'attachment; filename=\"profile.cpuprofile\"',\n 'cache-control': 'no-cache',\n // serve-static.ts sets this for the same reason: better-call's node\n // adapter doesn't call res.end() on a backpressured single-chunk\n // body, so a keep-alive response is left unterminated and browsers\n // spin. Closing the connection terminates it cleanly.\n 'connection': 'close',\n },\n });\n }),\n\n 'profiling-report': createEndpoint(`${prefix}/profiling/report.html`, { method: 'GET', use }, async () => {\n const denied = guardedDenied(); if (denied) { return denied; }\n if (!lastReportHtml) { return new Response('No report available', { status: 404 }); }\n const buf = Buffer.from(lastReportHtml, 'utf8');\n return new Response(new Uint8Array(buf) as any, {\n status: 200,\n headers: {\n 'content-type': 'text/html; charset=utf-8',\n 'content-length': String(buf.length),\n 'cache-control': 'no-cache',\n // see profiling-profile above \u2014 `connection: close` so the browser\n // doesn't spin on better-call's unterminated keep-alive response.\n 'connection': 'close',\n },\n });\n }),\n\n 'playground-index': createEndpoint(`${prefix}/`, { method: 'GET', use }, async () => {\n return serveStatic(SPA_DIST, '');\n }),\n\n 'playground-static': createEndpoint(`${prefix}/**:splat`, { method: 'GET', use }, async (ctx) => {\n return serveStatic(SPA_DIST, (ctx.params as any).splat);\n }),\n };\n\n const SPA_DIST_RESOLVED = path.resolve(SPA_DIST);\n\n return dualModeEndpoints(endpoints, {\n catchAllKey: 'playground-static',\n buildMiddleware: ({ fullHandler }) => {\n // Strip express's `baseUrl` so the inner router sees the request\n // relative to its mount point \u2014 playground's endpoint paths assume\n // a `''` prefix, so a sub-mount like `app.use(\"/foo\", playground())`\n // needs the leading `/foo` stripped before dispatch.\n const dispatch = (req: IncomingMessage, res: ServerResponse, next: (e?: any) => void) => {\n const stripped = Object.create(req, {\n baseUrl: { value: '', enumerable: true, configurable: true },\n originalUrl: { value: req.url, enumerable: true, configurable: true },\n });\n fullHandler(stripped as any, res as any).catch(next);\n };\n\n return (req, res, next) => {\n const url = (req.url ?? '').split('?')[0]!;\n const isProfiling = url.startsWith('/profiling');\n\n // Profiling start/stop are POSTs; everything else here is GET-only.\n if (req.method === 'POST') {\n return isProfiling ? dispatch(req, res, next) : next();\n }\n if (req.method !== 'GET') { return next(); }\n\n if (url === '/' || url === '/rooms' || url === '/__apidocs' || isProfiling) {\n return dispatch(req, res, next);\n }\n\n // Asset request \u2014 only delegate if the file actually exists on\n // disk. Avoids the catch-all's SPA fallback (which would serve\n // index.html for unknown paths, masking sibling express routes).\n const rel = url.replace(/^\\/+/, '');\n if (!rel || rel.includes('..')) { return next(); }\n const filePath = path.resolve(SPA_DIST_RESOLVED, rel);\n if (!filePath.startsWith(SPA_DIST_RESOLVED + path.sep)) { return next(); }\n\n fs.stat(filePath).then((stat) => {\n if (stat.isFile()) {\n dispatch(req, res, next);\n } else {\n next();\n }\n }).catch(() => next());\n };\n },\n });\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["require"]
7
7
  }
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src-backend/serve-static.ts
31
+ var serve_static_exports = {};
32
+ __export(serve_static_exports, {
33
+ serveStatic: () => serveStatic
34
+ });
35
+ module.exports = __toCommonJS(serve_static_exports);
36
+ var import_promises = __toESM(require("fs/promises"));
37
+ var import_path = __toESM(require("path"));
38
+ var MIME = {
39
+ ".html": "text/html; charset=utf-8",
40
+ ".js": "application/javascript; charset=utf-8",
41
+ ".mjs": "application/javascript; charset=utf-8",
42
+ ".css": "text/css; charset=utf-8",
43
+ ".json": "application/json; charset=utf-8",
44
+ ".svg": "image/svg+xml",
45
+ ".png": "image/png",
46
+ ".jpg": "image/jpeg",
47
+ ".jpeg": "image/jpeg",
48
+ ".gif": "image/gif",
49
+ ".ico": "image/x-icon",
50
+ ".woff": "font/woff",
51
+ ".woff2": "font/woff2",
52
+ ".ttf": "font/ttf",
53
+ ".map": "application/json; charset=utf-8",
54
+ ".txt": "text/plain; charset=utf-8"
55
+ };
56
+ async function serveStatic(root, relPath) {
57
+ const safe = sanitize(relPath != null ? relPath : "");
58
+ const filePath = safe ? import_path.default.join(root, safe) : import_path.default.join(root, "index.html");
59
+ const resolved = import_path.default.resolve(filePath);
60
+ const resolvedRoot = import_path.default.resolve(root);
61
+ if (!resolved.startsWith(resolvedRoot + import_path.default.sep) && resolved !== resolvedRoot) {
62
+ return new Response("forbidden", { status: 403 });
63
+ }
64
+ const data = await tryRead(resolved);
65
+ if (data) {
66
+ return fileResponse(data, mimeOf(resolved));
67
+ }
68
+ if (!import_path.default.extname(safe)) {
69
+ const index = await tryRead(import_path.default.join(resolvedRoot, "index.html"));
70
+ if (index) {
71
+ return fileResponse(index, "text/html; charset=utf-8");
72
+ }
73
+ }
74
+ return new Response("not found", { status: 404 });
75
+ }
76
+ function sanitize(p) {
77
+ return p.replace(/^\/+/, "").split("/").filter((seg) => seg && seg !== ".." && seg !== ".").join("/");
78
+ }
79
+ function mimeOf(filePath) {
80
+ var _a;
81
+ return (_a = MIME[import_path.default.extname(filePath).toLowerCase()]) != null ? _a : "application/octet-stream";
82
+ }
83
+ async function tryRead(filePath) {
84
+ try {
85
+ const stat = await import_promises.default.stat(filePath);
86
+ if (!stat.isFile()) {
87
+ return null;
88
+ }
89
+ return await import_promises.default.readFile(filePath);
90
+ } catch (e) {
91
+ return null;
92
+ }
93
+ }
94
+ function fileResponse(buf, contentType) {
95
+ return new Response(new Uint8Array(buf), {
96
+ status: 200,
97
+ headers: {
98
+ "content-type": contentType,
99
+ "content-length": String(buf.length),
100
+ "cache-control": "no-cache",
101
+ "connection": "close"
102
+ }
103
+ });
104
+ }
105
+ // Annotate the CommonJS export names for ESM import in node:
106
+ 0 && (module.exports = {
107
+ serveStatic
108
+ });
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src-backend/serve-static.ts"],
4
+ "sourcesContent": ["// Static-file helper for the built playground SPA. Mirrors the helper used by\n// @colyseus/admin (kept duplicated for now \u2014 both consumers are framework-owned\n// and the helper is small + stable).\nimport fs from 'fs/promises';\nimport path from 'path';\n\nconst MIME: Record<string, string> = {\n '.html': 'text/html; charset=utf-8',\n '.js': 'application/javascript; charset=utf-8',\n '.mjs': 'application/javascript; charset=utf-8',\n '.css': 'text/css; charset=utf-8',\n '.json': 'application/json; charset=utf-8',\n '.svg': 'image/svg+xml',\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.gif': 'image/gif',\n '.ico': 'image/x-icon',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n '.ttf': 'font/ttf',\n '.map': 'application/json; charset=utf-8',\n '.txt': 'text/plain; charset=utf-8',\n};\n\nexport async function serveStatic(root: string, relPath: string | undefined): Promise<Response> {\n const safe = sanitize(relPath ?? '');\n const filePath = safe ? path.join(root, safe) : path.join(root, 'index.html');\n\n const resolved = path.resolve(filePath);\n const resolvedRoot = path.resolve(root);\n if (!resolved.startsWith(resolvedRoot + path.sep) && resolved !== resolvedRoot) {\n return new Response('forbidden', { status: 403 });\n }\n\n const data = await tryRead(resolved);\n if (data) { return fileResponse(data, mimeOf(resolved)); }\n\n // SPA fallback \u2014 serve index.html for any non-asset request\n if (!path.extname(safe)) {\n const index = await tryRead(path.join(resolvedRoot, 'index.html'));\n if (index) { return fileResponse(index, 'text/html; charset=utf-8'); }\n }\n\n return new Response('not found', { status: 404 });\n}\n\nfunction sanitize(p: string): string {\n return p.replace(/^\\/+/, '').split('/').filter((seg) => seg && seg !== '..' && seg !== '.').join('/');\n}\n\nfunction mimeOf(filePath: string): string {\n return MIME[path.extname(filePath).toLowerCase()] ?? 'application/octet-stream';\n}\n\nasync function tryRead(filePath: string): Promise<Buffer | null> {\n try {\n const stat = await fs.stat(filePath);\n if (!stat.isFile()) { return null; }\n return await fs.readFile(filePath);\n } catch {\n return null;\n }\n}\n\nfunction fileResponse(buf: Buffer, contentType: string): Response {\n return new Response(new Uint8Array(buf) as any, {\n status: 200,\n headers: {\n 'content-type': contentType,\n 'content-length': String(buf.length),\n 'cache-control': 'no-cache',\n 'connection': 'close',\n },\n });\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,sBAAe;AACf,kBAAiB;AAEjB,IAAM,OAA+B;AAAA,EACnC,SAAS;AAAA,EACT,OAAS;AAAA,EACT,QAAS;AAAA,EACT,QAAS;AAAA,EACT,SAAS;AAAA,EACT,QAAS;AAAA,EACT,QAAS;AAAA,EACT,QAAS;AAAA,EACT,SAAS;AAAA,EACT,QAAS;AAAA,EACT,QAAS;AAAA,EACT,SAAS;AAAA,EACT,UAAU;AAAA,EACV,QAAS;AAAA,EACT,QAAS;AAAA,EACT,QAAS;AACX;AAEA,eAAsB,YAAY,MAAc,SAAgD;AAC9F,QAAM,OAAO,SAAS,4BAAW,EAAE;AACnC,QAAM,WAAW,OAAO,YAAAA,QAAK,KAAK,MAAM,IAAI,IAAI,YAAAA,QAAK,KAAK,MAAM,YAAY;AAE5E,QAAM,WAAW,YAAAA,QAAK,QAAQ,QAAQ;AACtC,QAAM,eAAe,YAAAA,QAAK,QAAQ,IAAI;AACtC,MAAI,CAAC,SAAS,WAAW,eAAe,YAAAA,QAAK,GAAG,KAAK,aAAa,cAAc;AAC9E,WAAO,IAAI,SAAS,aAAa,EAAE,QAAQ,IAAI,CAAC;AAAA,EAClD;AAEA,QAAM,OAAO,MAAM,QAAQ,QAAQ;AACnC,MAAI,MAAM;AAAE,WAAO,aAAa,MAAM,OAAO,QAAQ,CAAC;AAAA,EAAG;AAGzD,MAAI,CAAC,YAAAA,QAAK,QAAQ,IAAI,GAAG;AACvB,UAAM,QAAQ,MAAM,QAAQ,YAAAA,QAAK,KAAK,cAAc,YAAY,CAAC;AACjE,QAAI,OAAO;AAAE,aAAO,aAAa,OAAO,0BAA0B;AAAA,IAAG;AAAA,EACvE;AAEA,SAAO,IAAI,SAAS,aAAa,EAAE,QAAQ,IAAI,CAAC;AAClD;AAEA,SAAS,SAAS,GAAmB;AACnC,SAAO,EAAE,QAAQ,QAAQ,EAAE,EAAE,MAAM,GAAG,EAAE,OAAO,CAAC,QAAQ,OAAO,QAAQ,QAAQ,QAAQ,GAAG,EAAE,KAAK,GAAG;AACtG;AAEA,SAAS,OAAO,UAA0B;AAnD1C;AAoDE,UAAO,UAAK,YAAAA,QAAK,QAAQ,QAAQ,EAAE,YAAY,CAAC,MAAzC,YAA8C;AACvD;AAEA,eAAe,QAAQ,UAA0C;AAC/D,MAAI;AACF,UAAM,OAAO,MAAM,gBAAAC,QAAG,KAAK,QAAQ;AACnC,QAAI,CAAC,KAAK,OAAO,GAAG;AAAE,aAAO;AAAA,IAAM;AACnC,WAAO,MAAM,gBAAAA,QAAG,SAAS,QAAQ;AAAA,EACnC,SAAQ,GAAN;AACA,WAAO;AAAA,EACT;AACF;AAEA,SAAS,aAAa,KAAa,aAA+B;AAChE,SAAO,IAAI,SAAS,IAAI,WAAW,GAAG,GAAU;AAAA,IAC9C,QAAQ;AAAA,IACR,SAAS;AAAA,MACP,gBAAgB;AAAA,MAChB,kBAAkB,OAAO,IAAI,MAAM;AAAA,MACnC,iBAAiB;AAAA,MACjB,cAAc;AAAA,IAChB;AAAA,EACF,CAAC;AACH;",
6
+ "names": ["path", "fs"]
7
+ }
@@ -0,0 +1 @@
1
+ export declare function serveStatic(root: string, relPath: string | undefined): Promise<Response>;