@mpgd/cli 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -120,8 +120,6 @@ const entryCommand = defineI18n({
120
120
  ko: 'mpgd-kit 스타터와 타깃 워크플로우를 관리합니다.',
121
121
  }),
122
122
  run: () => {
123
- console.log('Use a sub-command: game, target, kit.');
124
- console.log('Run "pnpm mpgd --help" for available commands.');
125
123
  },
126
124
  });
127
125
  const gameCommand = defineI18n({
@@ -420,12 +418,10 @@ const gameCommand = defineI18n({
420
418
  }),
421
419
  ])),
422
420
  run: () => {
423
- console.log('Use "mpgd game icons generate <game>", verify, or inspect.');
424
421
  },
425
422
  }),
426
423
  },
427
424
  run: () => {
428
- console.log('Use "mpgd game create <directory>", "mpgd game accept <game>", or "mpgd game icons generate <game>".');
429
425
  },
430
426
  });
431
427
  const defaultLegalDir = 'legal';
@@ -553,12 +549,9 @@ function acceptGame(input) {
553
549
  MPGD_RELEASE_MANIFEST_FILE: releaseManifestFile,
554
550
  },
555
551
  });
556
- console.log(`[mpgd:accept] JSON report: ${result.jsonFile}`);
557
- console.log(`[mpgd:accept] Markdown report: ${result.markdownFile}`);
558
552
  if (result.report.status === 'failed') {
559
553
  throw new Error(`Game acceptance failed. Report: ${result.markdownFile}`);
560
554
  }
561
- console.log('[mpgd:accept] passed');
562
555
  }
563
556
  function assertGameAcceptanceRoot(gameRoot) {
564
557
  const resolved = path.resolve(gameRoot);
@@ -642,7 +635,6 @@ const legalCommand = defineI18n({
642
635
  ...resolveLegalCommandOptions(ctx.values),
643
636
  check: false,
644
637
  });
645
- console.log(`Built legal site: ${result.outDir}`);
646
638
  },
647
639
  }),
648
640
  check: defineI18n({
@@ -658,12 +650,10 @@ const legalCommand = defineI18n({
658
650
  ...resolveLegalCommandOptions(ctx.values),
659
651
  check: true,
660
652
  });
661
- console.log(`Legal site checked: ${result.outDir}`);
662
653
  },
663
654
  }),
664
655
  },
665
656
  run: () => {
666
- console.log('Use "mpgd legal build" or "mpgd legal check".');
667
657
  },
668
658
  });
669
659
  const targetCommand = defineI18n({
@@ -872,7 +862,6 @@ const targetCommand = defineI18n({
872
862
  }),
873
863
  },
874
864
  run: () => {
875
- console.log('Use "mpgd target build <target>" or "mpgd target smoke <target>".');
876
865
  },
877
866
  });
878
867
  const kitCommand = defineI18n({
@@ -891,17 +880,10 @@ const kitCommand = defineI18n({
891
880
  ko: 'CLI, kit, template, target wrapper 상태를 출력합니다.',
892
881
  }),
893
882
  run: () => {
894
- console.log(`cli package: ${packageRoot}`);
895
- console.log(`mpgd-kit: ${detectedKitRoot ?? 'not detected'}`);
896
- console.log(`cli template: ${existsSync(gameTemplateDir) ? 'ok' : 'missing'}`);
897
- console.log(`mobile wrapper: ${kitFileStatus('apps/mobile-capacitor/package.json')}`);
898
- console.log(`ait wrapper: ${kitFileStatus('apps/target-ait/package.json')}`);
899
- console.log(`devvit wrapper: ${kitFileStatus('apps/target-devvit/package.json')}`);
900
883
  },
901
884
  }),
902
885
  },
903
886
  run: () => {
904
- console.log('Use "mpgd kit doctor".');
905
887
  },
906
888
  });
907
889
  function commandResource(description, args = {}) {
@@ -1116,9 +1098,7 @@ function createGameApp(input) {
1116
1098
  });
1117
1099
  const files = collectTemplateFiles(gameTemplateDir);
1118
1100
  if (input.dryRun) {
1119
- console.log(`Would create ${path.relative(process.cwd(), appDir) || appDir}:`);
1120
1101
  for (const file of files) {
1121
- console.log(`- ${path.join(path.relative(process.cwd(), appDir), file.relativePath)}`);
1122
1102
  }
1123
1103
  return;
1124
1104
  }
@@ -1127,20 +1107,6 @@ function createGameApp(input) {
1127
1107
  mkdirSync(path.dirname(outputFile), { recursive: true });
1128
1108
  writeFileSync(outputFile, renderTemplate(file.content, context));
1129
1109
  }
1130
- console.log(`Created ${appDir}`);
1131
- console.log('Next steps:');
1132
- console.log(` cd ${appDir}`);
1133
- console.log(' pnpm install -w');
1134
- console.log(' pnpm dev');
1135
- console.log('Target builds require an mpgd-kit checkout:');
1136
- console.log([
1137
- ' mpgd target build-all',
1138
- `--targets-file ${path.join(appDir, 'mpgd.targets.json')}`,
1139
- `--targets ${recommendedMatrixTargets}`,
1140
- '--profile staging',
1141
- '--ait-variant wrapper',
1142
- '--kit-path <path-to-mpgd-kit>',
1143
- ].join(' '));
1144
1110
  }
1145
1111
  function assertValidGameName(name) {
1146
1112
  if (!/^[a-z][a-z0-9-]*$/.test(name)) {
@@ -1492,7 +1458,6 @@ function runTargetCommand(input) {
1492
1458
  const args = input.action === 'build'
1493
1459
  ? ['build:target', target, profile]
1494
1460
  : ['smoke:target', target];
1495
- console.log(`[mpgd] ${input.action} ${target}`);
1496
1461
  runPnpm(args, env);
1497
1462
  }
1498
1463
  function runTargetMatrix(input) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mpgd/cli",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Gunshi CLI for mpgd-kit starter generation and target workflow orchestration.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -47,20 +47,20 @@
47
47
  },
48
48
  "devDependencies": {
49
49
  "@types/node": "^24.0.0",
50
- "ttsc": "0.16.9",
51
- "typescript": "7.0.1-rc",
52
- "@mpgd/adapter-ait": "0.4.1",
53
- "@mpgd/adapter-devvit": "0.5.0",
54
- "@mpgd/analytics": "0.3.4",
55
- "@mpgd/adapter-browser": "0.4.1",
56
- "@mpgd/adapter-capacitor": "0.4.1",
57
- "@mpgd/bridge": "0.5.0",
58
- "@mpgd/catalog": "0.3.4",
59
- "@mpgd/game-services": "0.5.0",
60
- "@mpgd/i18n": "0.5.0",
61
- "@mpgd/platform": "0.5.0",
62
- "@mpgd/phaser-assets": "0.4.0",
63
- "@mpgd/target-config": "0.6.0"
50
+ "ttsc": "0.18.4",
51
+ "typescript": "7.0.2",
52
+ "@mpgd/adapter-ait": "0.4.2",
53
+ "@mpgd/adapter-browser": "0.4.2",
54
+ "@mpgd/analytics": "0.3.5",
55
+ "@mpgd/adapter-capacitor": "0.4.2",
56
+ "@mpgd/adapter-devvit": "0.7.0",
57
+ "@mpgd/bridge": "0.6.0",
58
+ "@mpgd/catalog": "0.3.5",
59
+ "@mpgd/game-services": "0.7.0",
60
+ "@mpgd/phaser-assets": "0.4.1",
61
+ "@mpgd/i18n": "0.5.1",
62
+ "@mpgd/platform": "0.5.1",
63
+ "@mpgd/target-config": "0.6.1"
64
64
  },
65
65
  "main": "./dist/index.js",
66
66
  "types": "./dist/index.d.ts",
@@ -19,9 +19,9 @@
19
19
  },
20
20
  "devDependencies": {
21
21
  "@cloudflare/workers-types": "^5.20260707.1",
22
- "@ttsc/unplugin": "0.16.9",
23
- "ttsc": "0.16.9",
24
- "typescript": "7.0.1-rc",
22
+ "@ttsc/unplugin": "0.18.4",
23
+ "ttsc": "0.18.4",
24
+ "typescript": "7.0.2",
25
25
  "vite": "^8.1.3",
26
26
  "wrangler": "^4.107.0"
27
27
  }
@@ -25,16 +25,30 @@ available and the CLI asks for a manual code. After that, use
25
25
  `pnpm devvit:playtest`, `pnpm devvit:upload`, and `pnpm devvit:publish` from the
26
26
  game root.
27
27
 
28
- The client bundle is copied into `dist/client` by the mpgd target build. The server bridge is
29
- compiled to `dist/server/index.cjs` and keeps Devvit SDK imports out of Phaser scenes.
30
- The game root pins `@mpgd/cli`, so these commands use the same CLI version as the generated
31
- starter's other `@mpgd/*` dependencies.
28
+ The official `@devvit/start/vite` plugin builds the game client into
29
+ `dist/client` and the CommonJS server bridge into `dist/server/index.cjs` in one
30
+ pass. The mpgd target build wraps that unified build with release provenance and
31
+ effective-target evidence while keeping Devvit SDK imports out of Phaser scenes.
32
+ The game root pins `@mpgd/cli`, so these commands use the same CLI version as the
33
+ generated starter's other `@mpgd/*` dependencies.
34
+
35
+ The bridge endpoint at `/api/mpgd/rpc` uses the direct oRPC Node HTTP adapter
36
+ from `@mpgd/bridge/orpc/node`; Express, Hono, and Fetch request conversion are
37
+ not required. Devvit-owned menu, scheduler, trigger, and form callbacks remain
38
+ thin `/internal/...` routes and delegate to shared service functions. oRPC
39
+ Publisher helpers can broadcast results after a task completes, but they do not
40
+ replace the `devvit.json` scheduler endpoint and should not use process-local
41
+ memory when delivery must cross instances. The generated target exposes only
42
+ the oRPC bridge route; the former JSON compatibility route is not generated.
32
43
 
33
44
  The default post entry uses `index.html` for a lightweight inline preview. Its
34
45
  Play button requests the `game` entrypoint, which loads the separate
35
46
  `game.html` Phaser document. Keep inline UI free of game runtime imports so the
36
47
  card remains lightweight before expansion.
37
48
 
49
+ `devvit playtest` runs the official unified Vite build in watch mode, so a
50
+ separate client/server watcher or staging prebuild is not required.
51
+
38
52
  ## Durable Post Operations
39
53
 
40
54
  Use `src/server/postOperationStore.ts` with `@mpgd/adapter-devvit/server` for a
@@ -59,3 +73,24 @@ first success, duplicate calls, interruption,
59
73
  response loss, lease expiry with concurrent reconciliation, malformed durable
60
74
  state, invalid launch metadata, and cross-scope key isolation before enabling a
61
75
  scheduled or user-triggered publication flow.
76
+
77
+ ## AI-assisted Devvit development
78
+
79
+ Devvit provides an optional MCP server for targeted documentation search and
80
+ deployed-app log inspection. See the official
81
+ [Devvit AI Tools guide](https://developers.reddit.com/docs/guides/ai), then
82
+ register it once in the developer's global Codex configuration:
83
+
84
+ ```sh
85
+ codex mcp add devvit -- npx -y @devvit/mcp
86
+ ```
87
+
88
+ Restart Codex or begin a new task after registering it. Use `devvit_search`
89
+ before broad Devvit documentation exploration. Use the experimental
90
+ `devvit_logs` tool for a deployed app and subreddit when diagnosing production
91
+ or playtest behavior, and confirm its findings with local tests and
92
+ `pnpm devvit:playtest`.
93
+
94
+ The MCP server is an agent-side aid only. It is not required by the generated
95
+ game build, must not be bundled into target artifacts, and does not replace
96
+ Devvit CLI authentication or release verification.
@@ -41,7 +41,7 @@
41
41
  ]
42
42
  },
43
43
  "scripts": {
44
- "dev": "pnpm dev:prepare && pnpm dev:watch",
44
+ "dev": "vite build --mode staging --watch",
45
45
  "build": "pnpm build"
46
46
  }
47
47
  }
@@ -9,12 +9,7 @@
9
9
  "init": "pnpm run prepare:icon:production && devvit upload --config devvit.json",
10
10
  "init:copy-paste": "pnpm run prepare:icon:production && devvit upload --copy-paste --config devvit.json",
11
11
  "dev": "pnpm run prepare:icon && devvit playtest --config devvit.json",
12
- "dev:prepare": "pnpm --dir ../.. build:devvit:staging",
13
- "dev:client": "APP_TARGET=reddit APP_VERSION=0.0.0-dev BUILD_ID=devvit-watch pnpm --dir ../.. exec vite build --mode staging --watch --outDir apps/target-devvit/dist/client",
14
- "dev:server": "vite build --config vite.server.config.ts --watch",
15
- "dev:watch": "concurrently --kill-others --success first \"pnpm run dev:client\" \"pnpm run dev:server\"",
16
12
  "build": "pnpm --dir ../.. build:devvit",
17
- "build:server": "vite build --config vite.server.config.ts",
18
13
  "whoami": "devvit whoami",
19
14
  "upload": "pnpm run prepare:icon:production && devvit upload --config devvit.json",
20
15
  "publish": "pnpm run prepare:icon:production && devvit publish --config devvit.json",
@@ -24,20 +19,17 @@
24
19
  "fix": "ttsc fix"
25
20
  },
26
21
  "dependencies": {
27
- "@devvit/web": "0.13.6",
22
+ "@devvit/web": "0.13.8",
28
23
  "@mpgd/adapter-devvit": "__MPGD_DEPENDENCY_VERSION_ADAPTER_DEVVIT__",
29
- "@mpgd/bridge": "__MPGD_DEPENDENCY_VERSION_BRIDGE__",
30
- "express": "^5.2.1",
31
- "helmet": "^8.1.0"
24
+ "@mpgd/bridge": "__MPGD_DEPENDENCY_VERSION_BRIDGE__"
32
25
  },
33
26
  "devDependencies": {
34
- "@ttsc/unplugin": "0.16.9",
35
- "@types/express": "^5.0.6",
27
+ "@devvit/start": "0.13.8",
28
+ "@ttsc/unplugin": "0.18.4",
36
29
  "@types/node": "^24.0.0",
37
- "concurrently": "10.0.3",
38
- "devvit": "0.13.6",
39
- "ttsc": "0.16.9",
40
- "typescript": "7.0.1-rc",
30
+ "devvit": "0.13.8",
31
+ "ttsc": "0.18.4",
32
+ "typescript": "7.0.2",
41
33
  "vite": "^8.1.3"
42
34
  }
43
35
  }
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="theme-color" content="#0f172a" />
7
+ <title>__GAME_TITLE__</title>
8
+ </head>
9
+ <body>
10
+ <div id="game"></div>
11
+ <script type="module" src="./game.ts"></script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,3 @@
1
+ await import('../../../../src/gameEntry');
2
+
3
+ export {};
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="theme-color" content="#0f172a" />
7
+ <title>__GAME_TITLE__</title>
8
+ </head>
9
+ <body>
10
+ <div id="game"></div>
11
+ <script type="module" src="./preview.ts"></script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,3 @@
1
+ await import('../../../../src/entry');
2
+
3
+ export {};
@@ -1,23 +1,22 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+
1
3
  import { createServer, context, getServerPort, reddit, redis } from '@devvit/web/server';
2
4
  import type { UiResponse } from '@devvit/web/shared';
3
5
  import {
4
- assertBridgeRequest,
5
6
  createBridgeError,
6
7
  type BridgeRequest,
7
8
  type BridgeResponse,
8
9
  } from '@mpgd/bridge';
9
10
  import {
10
- createBridgeRpcFetchHandler,
11
11
  createBridgeRpcRouter,
12
12
  defaultBridgeRpcEndpoint,
13
13
  } from '@mpgd/bridge/orpc';
14
- import express, { type Request as ExpressRequest, type Response as ExpressResponse } from 'express';
15
- import helmet from 'helmet';
14
+ import { createBridgeRpcNodeHandler } from '@mpgd/bridge/orpc/node';
16
15
 
17
- const app = express();
18
16
  const redisKeyComponentPattern = /^[A-Za-z0-9:_-]{1,128}$/;
19
17
  const maxStorageKeyLength = 128;
20
18
  const maxEncodedStorageKeyLength = 384;
19
+ const maxRequestBodySize = 1_048_576;
21
20
  const leaderboardUpdateMaxAttempts = 3;
22
21
  const leaderboardBackoffBaseMs = 25;
23
22
  const leaderboardLockTtlMs = 2_000;
@@ -25,51 +24,53 @@ const leaderboardLockTtlSeconds = Math.ceil(leaderboardLockTtlMs / 1_000);
25
24
  const leaderboardLockRetryBudgetMs = leaderboardLockTtlSeconds * 1_000;
26
25
  const gameName = '__GAME_NAME__';
27
26
  const gameTitle = __GAME_TITLE_TS_LITERAL__;
28
- const expressManagedResponseHeaders = new Set([
29
- 'connection',
30
- 'content-encoding',
31
- 'content-length',
32
- 'transfer-encoding',
33
- ]);
34
-
35
- app.disable('x-powered-by');
36
- app.use(helmet());
37
-
38
- const bridgeRpcFetchHandler = createBridgeRpcFetchHandler(
27
+ const bridgeRpcHandler = createBridgeRpcNodeHandler(
39
28
  createBridgeRpcRouter(handleBridgeRequest),
29
+ {
30
+ maxBodySize: maxRequestBodySize,
31
+ prefix: defaultBridgeRpcEndpoint,
32
+ },
40
33
  );
41
34
 
42
- app.use(defaultBridgeRpcEndpoint, express.raw({ type: '*/*', limit: '1mb' }), async (
43
- request: ExpressRequest,
44
- response: ExpressResponse,
45
- ): Promise<void> => {
46
- try {
47
- const fetchResponse = await bridgeRpcFetchHandler(expressRequestToFetchRequest(request));
48
- await sendFetchResponse(response, fetchResponse);
49
- } catch (error) {
50
- const message = error instanceof Error ? error.message : 'Devvit oRPC request failed.';
51
- console.error(`devvit oRPC internal error: ${message}`, error);
52
- response.status(500).json(
53
- createBridgeError(
54
- requestIdFromBody(request.body),
55
- 'DEVVIT_BRIDGE_INTERNAL_ERROR',
56
- 'Devvit bridge request failed.',
57
- true,
58
- ),
59
- );
35
+ async function handleHttpRequest(
36
+ request: IncomingMessage,
37
+ response: ServerResponse,
38
+ ): Promise<void> {
39
+ setResponseSecurityHeaders(response);
40
+
41
+ if (request.method === 'POST' && requestPathname(request) === '/internal/menu/create-post') {
42
+ try {
43
+ await drainRequestBody(request, maxRequestBodySize);
44
+ } catch (error) {
45
+ if (!(error instanceof RequestBodyTooLargeError)) {
46
+ throw error;
47
+ }
48
+
49
+ discardOversizedRequestBody(request, response);
50
+ sendJson(response, 413, {
51
+ error: 'REQUEST_BODY_TOO_LARGE',
52
+ });
53
+ return;
54
+ }
55
+
56
+ await handleCreatePostMenu(response);
57
+ return;
60
58
  }
61
- });
62
59
 
63
- app.use(express.json({ limit: '1mb' }));
60
+ if (await bridgeRpcHandler(request, response)) {
61
+ return;
62
+ }
63
+
64
+ sendJson(response, 404, {
65
+ error: 'NOT_FOUND',
66
+ });
67
+ }
64
68
 
65
- app.post('/internal/menu/create-post', async (
66
- _request: ExpressRequest,
67
- response: ExpressResponse,
68
- ): Promise<void> => {
69
+ async function handleCreatePostMenu(response: ServerResponse): Promise<void> {
69
70
  const subredditName = currentSubredditName();
70
71
 
71
72
  if (subredditName === undefined) {
72
- response.status(200).json({
73
+ sendJson(response, 200, {
73
74
  showToast: {
74
75
  text: 'Could not resolve the target subreddit for this menu action.',
75
76
  appearance: 'neutral',
@@ -92,7 +93,7 @@ app.post('/internal/menu/create-post', async (
92
93
  },
93
94
  });
94
95
 
95
- response.status(200).json({
96
+ sendJson(response, 200, {
96
97
  showToast: {
97
98
  text: `Created ${gameTitle} post ${post.id}.`,
98
99
  appearance: 'success',
@@ -100,53 +101,30 @@ app.post('/internal/menu/create-post', async (
100
101
  } satisfies UiResponse);
101
102
  } catch (error) {
102
103
  console.error(`devvit custom post creation failed: ${errorMessage(error)}`, error);
103
- response.status(200).json({
104
+ sendJson(response, 200, {
104
105
  showToast: {
105
106
  text: `Could not create the ${gameTitle} post.`,
106
107
  appearance: 'neutral',
107
108
  },
108
109
  } satisfies UiResponse);
109
110
  }
110
- });
111
+ }
111
112
 
112
- app.post('/api/mpgd/bridge', async (
113
- request: ExpressRequest,
114
- response: ExpressResponse,
115
- ): Promise<void> => {
116
- let bridgeRequest: BridgeRequest;
113
+ const server = createServer((request, response) => {
114
+ void handleHttpRequest(request, response).catch((error: unknown) => {
115
+ console.error(`devvit server request failed: ${errorMessage(error)}`, error);
117
116
 
118
- try {
119
- bridgeRequest = assertBridgeRequest(request.body);
120
- } catch (error) {
121
- response.status(400).json(
122
- createBridgeError(
123
- requestIdFromBody(request.body),
124
- 'INVALID_BRIDGE_REQUEST',
125
- error instanceof Error ? error.message : 'Invalid bridge request.',
126
- ),
127
- );
128
- return;
129
- }
117
+ if (response.headersSent) {
118
+ response.end();
119
+ return;
120
+ }
130
121
 
131
- try {
132
- const bridgeResponse = await handleBridgeRequest(bridgeRequest);
133
- response.status(200).json(bridgeResponse);
134
- } catch (error) {
135
- const message = error instanceof Error ? error.message : 'Devvit bridge request failed.';
136
- console.error(`devvit bridge internal error: ${message}`, error);
137
-
138
- response.status(500).json(
139
- createBridgeError(
140
- bridgeRequest.id,
141
- 'DEVVIT_BRIDGE_INTERNAL_ERROR',
142
- 'Devvit bridge request failed.',
143
- true,
144
- ),
145
- );
146
- }
122
+ sendJson(response, 500, {
123
+ error: 'DEVVIT_SERVER_INTERNAL_ERROR',
124
+ });
125
+ });
147
126
  });
148
127
 
149
- const server = createServer(app);
150
128
  const port = getServerPort();
151
129
 
152
130
  server.on('error', (error) => {
@@ -387,15 +365,6 @@ async function saveStorage(input: BridgeRequest): Promise<BridgeResponse> {
387
365
  });
388
366
  }
389
367
 
390
- function requestIdFromBody(input: unknown): string {
391
- if (typeof input !== 'object' || input === null) {
392
- return 'unknown';
393
- }
394
-
395
- const candidate = input as { readonly id?: unknown };
396
- return typeof candidate.id === 'string' ? candidate.id : 'unknown';
397
- }
398
-
399
368
  function ok(input: BridgeRequest, data: unknown): BridgeResponse {
400
369
  return {
401
370
  id: input.id,
@@ -641,61 +610,76 @@ function optionalObjectPayload(payload: unknown): Record<string, unknown> {
641
610
  return payload as Record<string, unknown>;
642
611
  }
643
612
 
644
- function expressRequestToFetchRequest(request: ExpressRequest): Request {
645
- const headers = new Headers();
613
+ function requestPathname(request: IncomingMessage): string {
614
+ return new URL(request.url ?? '/', 'http://localhost').pathname;
615
+ }
616
+
617
+ class RequestBodyTooLargeError extends Error {}
646
618
 
647
- for (const [key, value] of Object.entries(request.headers)) {
648
- if (typeof value === 'string') {
649
- headers.set(key, value);
650
- } else if (Array.isArray(value)) {
651
- headers.set(key, value.join(', '));
652
- }
653
- }
619
+ async function* readRequestBodyChunks(
620
+ request: IncomingMessage,
621
+ maxBodySize: number,
622
+ ): AsyncGenerator<Buffer, void, undefined> {
623
+ assertContentLengthWithinLimit(request, maxBodySize);
654
624
 
655
- const init: RequestInit = {
656
- method: request.method,
657
- headers,
658
- };
625
+ let bodySize = 0;
659
626
 
660
- if (request.method !== 'GET' && request.method !== 'HEAD') {
661
- init.body = requestBodyToBodyInit(request.body);
627
+ for await (const chunk of request) {
628
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
629
+ bodySize += buffer.byteLength;
662
630
 
663
- if (!headers.has('content-type') && !(request.body instanceof Uint8Array)) {
664
- headers.set('content-type', 'application/json');
631
+ if (bodySize > maxBodySize) {
632
+ throw new RequestBodyTooLargeError();
665
633
  }
634
+
635
+ yield buffer;
666
636
  }
637
+ }
667
638
 
668
- return new Request(expressRequestUrl(request), init);
639
+ async function drainRequestBody(
640
+ request: IncomingMessage,
641
+ maxBodySize: number,
642
+ ): Promise<void> {
643
+ for await (const _chunk of readRequestBodyChunks(request, maxBodySize)) {
644
+ // Drain the bounded request stream without retaining its contents.
645
+ }
669
646
  }
670
647
 
671
- function requestBodyToBodyInit(input: unknown): BodyInit {
672
- if (input instanceof Uint8Array) {
673
- const body = new Uint8Array(input.byteLength);
674
- body.set(input);
648
+ function assertContentLengthWithinLimit(
649
+ request: IncomingMessage,
650
+ maxBodySize: number,
651
+ ): void {
652
+ const contentLength = request.headers['content-length'];
675
653
 
676
- return body.buffer as ArrayBuffer;
654
+ if (
655
+ contentLength !== undefined
656
+ && Number.isFinite(Number(contentLength))
657
+ && Number(contentLength) > maxBodySize
658
+ ) {
659
+ throw new RequestBodyTooLargeError();
677
660
  }
678
-
679
- return JSON.stringify(input ?? null);
680
661
  }
681
662
 
682
- function expressRequestUrl(request: ExpressRequest): string {
683
- const host = request.get('host') ?? 'localhost';
684
- const protocol = request.protocol || 'https';
685
-
686
- return `${protocol}://${host}${request.originalUrl}`;
663
+ function discardOversizedRequestBody(
664
+ request: IncomingMessage,
665
+ response: ServerResponse,
666
+ ): void {
667
+ response.setHeader('connection', 'close');
668
+ request.resume();
687
669
  }
688
670
 
689
- async function sendFetchResponse(
690
- response: ExpressResponse,
691
- fetchResponse: Response,
692
- ): Promise<void> {
693
- fetchResponse.headers.forEach((value: string, key: string) => {
694
- if (!expressManagedResponseHeaders.has(key.toLowerCase())) {
695
- response.setHeader(key, value);
696
- }
697
- });
671
+ function setResponseSecurityHeaders(response: ServerResponse): void {
672
+ response.setHeader('cache-control', 'no-store');
673
+ response.setHeader('content-security-policy', "default-src 'none'; frame-ancestors 'none'");
674
+ response.setHeader('referrer-policy', 'no-referrer');
675
+ response.setHeader('x-content-type-options', 'nosniff');
676
+ response.setHeader('x-frame-options', 'DENY');
677
+ }
698
678
 
699
- response.status(fetchResponse.status);
700
- response.send(await fetchResponse.text());
679
+ function sendJson(response: ServerResponse, status: number, body: unknown): void {
680
+ const payload = JSON.stringify(body);
681
+ response.statusCode = status;
682
+ response.setHeader('content-length', Buffer.byteLength(payload));
683
+ response.setHeader('content-type', 'application/json; charset=utf-8');
684
+ response.end(payload);
701
685
  }
@@ -2,8 +2,13 @@
2
2
  "extends": "../../tsconfig.json",
3
3
  "compilerOptions": {
4
4
  "allowSyntheticDefaultImports": true,
5
- "types": ["node"],
5
+ "types": ["node", "vite/client"],
6
6
  "noEmit": true
7
7
  },
8
- "include": ["src/**/*.ts", "vite.server.config.ts"]
8
+ "include": [
9
+ "src/**/*.ts",
10
+ "vite.config.ts",
11
+ "../../src/**/*.ts",
12
+ "../../vite.shared.ts"
13
+ ]
9
14
  }
@@ -0,0 +1,41 @@
1
+ import { resolve } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+
4
+ import { devvit } from '@devvit/start/vite';
5
+ import { defineConfig } from 'vite';
6
+
7
+ import { createGameViteSharedConfig } from '../../vite.shared';
8
+
9
+ const wrapperRoot = fileURLToPath(new URL('.', import.meta.url));
10
+ const gameRoot = resolve(wrapperRoot, '../..');
11
+
12
+ export default defineConfig(({ mode }) => {
13
+ const shared = createGameViteSharedConfig({
14
+ appTarget: 'reddit',
15
+ configTarget: 'reddit',
16
+ gameRoot,
17
+ mode,
18
+ platformTargetsFile: resolve(gameRoot, 'mpgd.targets.json'),
19
+ project: resolve(wrapperRoot, 'tsconfig.json'),
20
+ });
21
+
22
+ return {
23
+ ...shared,
24
+ publicDir: resolve(gameRoot, 'public'),
25
+ plugins: [
26
+ ...(shared.plugins ?? []),
27
+ devvit({
28
+ client: {
29
+ build: {
30
+ sourcemap: mode !== 'production',
31
+ },
32
+ },
33
+ server: {
34
+ build: {
35
+ sourcemap: mode !== 'production',
36
+ },
37
+ },
38
+ }),
39
+ ],
40
+ };
41
+ });
@@ -73,10 +73,10 @@
73
73
  "@ait-co/console-cli": "^0.1.45",
74
74
  "@cloudflare/workers-types": "^5.20260707.1",
75
75
  "@mpgd/cli": "__MPGD_DEPENDENCY_VERSION_CLI__",
76
- "@ttsc/unplugin": "0.16.9",
76
+ "@ttsc/unplugin": "0.18.4",
77
77
  "@types/node": "^24.0.0",
78
- "ttsc": "0.16.9",
79
- "typescript": "7.0.1-rc",
78
+ "ttsc": "0.18.4",
79
+ "typescript": "7.0.2",
80
80
  "vite": "^8.1.3",
81
81
  "wrangler": "^4.107.0"
82
82
  }
@@ -1,53 +1,27 @@
1
- import { existsSync, readFileSync } from 'node:fs';
2
1
  import { resolve } from 'node:path';
3
2
 
4
- import ttsc from '@ttsc/unplugin/vite';
5
3
  import { defineConfig } from 'vite';
6
4
 
7
- interface RuntimePlatformTargetMetadata {
8
- readonly kind: string;
9
- readonly adapter: string;
10
- readonly integrations?: Record<string, unknown>;
11
- }
5
+ import { createGameViteSharedConfig } from './vite.shared';
6
+
7
+ export { resolveBuildGatewayModule } from './vite.shared';
8
+
9
+ const gameRoot = process.cwd();
12
10
 
13
11
  export default defineConfig(({ mode }) => {
14
- const isProduction = mode === 'production';
15
- const platformTarget = readRuntimePlatformTarget();
16
12
  const appTarget = process.env.APP_TARGET ?? 'browser';
17
13
  const isDevvitBuild = appTarget === 'reddit';
18
- const buildGatewayModule = resolveBuildGatewayModule({
19
- target: appTarget,
20
- debug: !isProduction,
21
- buildId: process.env.BUILD_ID ?? 'local',
22
- });
23
14
 
24
15
  return {
25
- base: './',
26
- plugins: [
27
- ttsc({
28
- project: 'tsconfig.json',
29
- plugins: false,
30
- }),
31
- ],
32
- resolve: {
33
- alias: {
34
- ...createCatalogAliases(),
35
- '#mpgd-platform-gateway': resolve(buildGatewayModule),
36
- },
37
- },
38
- define: {
39
- __APP_TARGET__: JSON.stringify(appTarget),
40
- __MPGD_CONFIG_TARGET__: JSON.stringify(process.env.MPGD_CONFIG_TARGET ?? ''),
41
- __MPGD_PLATFORM_TARGET__:
42
- platformTarget === undefined ? 'undefined' : JSON.stringify(platformTarget),
43
- __APP_VERSION__: JSON.stringify(process.env.APP_VERSION ?? '0.0.0-dev'),
44
- __BUILD_ID__: JSON.stringify(process.env.BUILD_ID ?? 'local'),
45
- __SOURCE_GIT_SHA__: JSON.stringify(process.env.MPGD_SOURCE_GIT_SHA ?? 'uncommitted'),
46
- __DEBUG_BUILD__: JSON.stringify(!isProduction),
47
- },
16
+ ...createGameViteSharedConfig({
17
+ appTarget,
18
+ gameRoot,
19
+ mode,
20
+ project: resolve(gameRoot, 'tsconfig.json'),
21
+ }),
48
22
  build: {
49
23
  target: 'es2022',
50
- sourcemap: !isProduction,
24
+ sourcemap: mode !== 'production',
51
25
  outDir: 'dist',
52
26
  assetsDir: 'assets',
53
27
  emptyOutDir: true,
@@ -55,8 +29,8 @@ export default defineConfig(({ mode }) => {
55
29
  ...(isDevvitBuild
56
30
  ? {
57
31
  input: {
58
- preview: resolve('index.html'),
59
- game: resolve('game.html'),
32
+ preview: resolve(gameRoot, 'index.html'),
33
+ game: resolve(gameRoot, 'game.html'),
60
34
  },
61
35
  }
62
36
  : {}),
@@ -69,128 +43,3 @@ export default defineConfig(({ mode }) => {
69
43
  },
70
44
  };
71
45
  });
72
-
73
- export function resolveBuildGatewayModule(input: {
74
- readonly target: string;
75
- readonly debug: boolean;
76
- readonly buildId: string;
77
- }): string {
78
- switch (input.target) {
79
- case 'android':
80
- return 'src/platform/buildGateways/capacitorAndroid.ts';
81
- case 'ios':
82
- return 'src/platform/buildGateways/capacitorIos.ts';
83
- case 'ait':
84
- return input.debug
85
- ? 'src/platform/buildGateways/aitSandbox.ts'
86
- : 'src/platform/buildGateways/ait.ts';
87
- case 'reddit':
88
- return input.debug && input.buildId === 'devvit-sandbox'
89
- ? 'src/platform/buildGateways/redditSandbox.ts'
90
- : 'src/platform/buildGateways/reddit.ts';
91
- default:
92
- return 'src/platform/buildGateways/browser.ts';
93
- }
94
- }
95
-
96
- function createCatalogAliases(): Record<string, string> {
97
- const productCatalogFile = readConfiguredPath(process.env.MPGD_PRODUCT_CATALOG_FILE);
98
- const adPlacementsFile = readConfiguredPath(process.env.MPGD_AD_PLACEMENTS_FILE);
99
-
100
- if ((productCatalogFile === undefined) !== (adPlacementsFile === undefined)) {
101
- throw new Error(
102
- 'MPGD_PRODUCT_CATALOG_FILE and MPGD_AD_PLACEMENTS_FILE must be configured together.',
103
- );
104
- }
105
-
106
- if (productCatalogFile === undefined || adPlacementsFile === undefined) {
107
- return {};
108
- }
109
-
110
- return {
111
- '@mpgd/catalog/catalog.json': resolveCatalogPath(productCatalogFile, adPlacementsFile),
112
- '@mpgd/catalog/placements.json': resolveCatalogPath(adPlacementsFile, productCatalogFile),
113
- };
114
- }
115
-
116
- function readConfiguredPath(value: string | undefined): string | undefined {
117
- const normalized = value?.trim();
118
- return normalized === undefined || normalized.length === 0 ? undefined : normalized;
119
- }
120
-
121
- function readRuntimePlatformTarget(): RuntimePlatformTargetMetadata | undefined {
122
- const targetsFile = readConfiguredPath(process.env.MPGD_PLATFORM_TARGETS_FILE);
123
- const configTarget = readConfiguredPath(process.env.MPGD_CONFIG_TARGET);
124
-
125
- if (targetsFile === undefined || configTarget === undefined) {
126
- return undefined;
127
- }
128
-
129
- const resolvedTargetsFile = resolve(targetsFile);
130
- let parsed: unknown;
131
-
132
- try {
133
- parsed = JSON.parse(readFileSync(resolvedTargetsFile, 'utf8'));
134
- } catch (error) {
135
- throw new Error(
136
- `Failed to read or parse MPGD_PLATFORM_TARGETS_FILE at ${resolvedTargetsFile}: ${formatError(error)}`,
137
- );
138
- }
139
-
140
- if (!isRecord(parsed) || !isRecord(parsed.targets)) {
141
- throw new Error('MPGD_PLATFORM_TARGETS_FILE must contain a targets object.');
142
- }
143
-
144
- const target = parsed.targets[configTarget];
145
-
146
- if (!isRecord(target)) {
147
- throw new Error(`Missing platform target metadata for ${configTarget}.`);
148
- }
149
-
150
- if (typeof target.kind !== 'string' || typeof target.adapter !== 'string') {
151
- throw new Error(`Platform target ${configTarget} must define kind and adapter.`);
152
- }
153
-
154
- if (target.integrations !== undefined && !isRecord(target.integrations)) {
155
- throw new Error(`Platform target ${configTarget} integrations must be an object.`);
156
- }
157
-
158
- return {
159
- kind: target.kind,
160
- adapter: target.adapter,
161
- ...(target.integrations === undefined ? {} : { integrations: target.integrations }),
162
- };
163
- }
164
-
165
- function isRecord(input: unknown): input is Record<string, unknown> {
166
- return typeof input === 'object' && input !== null && !Array.isArray(input);
167
- }
168
-
169
- function formatError(error: unknown): string {
170
- return error instanceof Error ? error.message : String(error);
171
- }
172
-
173
- function resolveCatalogPath(path: string, pairedPath: string): string {
174
- return resolve(resolveCatalogBaseDir(path, pairedPath), path);
175
- }
176
-
177
- function resolveCatalogBaseDir(path: string, pairedPath: string): string {
178
- const fallbackBaseDir = process.cwd();
179
- const candidates = [
180
- fallbackBaseDir,
181
- readConfiguredPath(process.env.INIT_CWD),
182
- readConfiguredPath(process.env.PWD),
183
- ];
184
-
185
- for (const candidate of candidates) {
186
- if (
187
- candidate !== undefined
188
- && existsSync(resolve(candidate, path))
189
- && existsSync(resolve(candidate, pairedPath))
190
- ) {
191
- return candidate;
192
- }
193
- }
194
-
195
- return fallbackBaseDir;
196
- }
@@ -0,0 +1,212 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+
4
+ import ttsc from '@ttsc/unplugin/vite';
5
+ import type { UserConfig } from 'vite';
6
+
7
+ interface RuntimePlatformTargetMetadata {
8
+ readonly kind: string;
9
+ readonly adapter: string;
10
+ readonly integrations?: Record<string, unknown>;
11
+ }
12
+
13
+ const devvitSandboxBuildId = 'devvit-sandbox';
14
+
15
+ export interface CreateGameViteSharedConfigInput {
16
+ readonly appTarget?: string;
17
+ readonly configTarget?: string;
18
+ readonly gameRoot: string;
19
+ readonly mode: string;
20
+ readonly platformTargetsFile?: string;
21
+ readonly project: string;
22
+ readonly productCatalogFile?: string;
23
+ readonly adPlacementsFile?: string;
24
+ }
25
+
26
+ export function createGameViteSharedConfig(
27
+ input: CreateGameViteSharedConfigInput,
28
+ ): UserConfig {
29
+ const isProduction = input.mode === 'production';
30
+ const appTarget = input.appTarget ?? process.env.APP_TARGET ?? 'browser';
31
+ const configTarget = input.configTarget ?? process.env.MPGD_CONFIG_TARGET ?? '';
32
+ const platformTarget = readRuntimePlatformTarget(
33
+ input.platformTargetsFile ?? process.env.MPGD_PLATFORM_TARGETS_FILE,
34
+ configTarget,
35
+ );
36
+ const buildGatewayModule = resolveBuildGatewayModule({
37
+ target: appTarget,
38
+ debug: !isProduction,
39
+ buildId: process.env.BUILD_ID ?? 'local',
40
+ });
41
+
42
+ return {
43
+ base: './',
44
+ plugins: [
45
+ ttsc({
46
+ project: input.project,
47
+ plugins: false,
48
+ }),
49
+ ],
50
+ resolve: {
51
+ alias: {
52
+ ...createCatalogAliases({
53
+ gameRoot: input.gameRoot,
54
+ productCatalogFile: input.productCatalogFile,
55
+ adPlacementsFile: input.adPlacementsFile,
56
+ }),
57
+ '#mpgd-platform-gateway': resolve(input.gameRoot, buildGatewayModule),
58
+ },
59
+ },
60
+ define: {
61
+ __APP_TARGET__: JSON.stringify(appTarget),
62
+ __MPGD_CONFIG_TARGET__: JSON.stringify(configTarget),
63
+ __MPGD_PLATFORM_TARGET__:
64
+ platformTarget === undefined ? 'undefined' : JSON.stringify(platformTarget),
65
+ __APP_VERSION__: JSON.stringify(process.env.APP_VERSION ?? '0.0.0-dev'),
66
+ __BUILD_ID__: JSON.stringify(process.env.BUILD_ID ?? 'local'),
67
+ __SOURCE_GIT_SHA__: JSON.stringify(process.env.MPGD_SOURCE_GIT_SHA ?? 'uncommitted'),
68
+ __DEBUG_BUILD__: JSON.stringify(!isProduction),
69
+ },
70
+ };
71
+ }
72
+
73
+ export function resolveBuildGatewayModule(input: {
74
+ readonly target: string;
75
+ readonly debug: boolean;
76
+ readonly buildId: string;
77
+ }): string {
78
+ switch (input.target) {
79
+ case 'android':
80
+ return 'src/platform/buildGateways/capacitorAndroid.ts';
81
+ case 'ios':
82
+ return 'src/platform/buildGateways/capacitorIos.ts';
83
+ case 'ait':
84
+ return input.debug
85
+ ? 'src/platform/buildGateways/aitSandbox.ts'
86
+ : 'src/platform/buildGateways/ait.ts';
87
+ case 'reddit':
88
+ return input.debug && input.buildId === devvitSandboxBuildId
89
+ ? 'src/platform/buildGateways/redditSandbox.ts'
90
+ : 'src/platform/buildGateways/reddit.ts';
91
+ default:
92
+ return 'src/platform/buildGateways/browser.ts';
93
+ }
94
+ }
95
+
96
+ function createCatalogAliases(input: {
97
+ readonly gameRoot: string;
98
+ readonly productCatalogFile: string | undefined;
99
+ readonly adPlacementsFile: string | undefined;
100
+ }): Record<string, string> {
101
+ const productCatalogFile = readConfiguredPath(
102
+ input.productCatalogFile ?? process.env.MPGD_PRODUCT_CATALOG_FILE,
103
+ );
104
+ const adPlacementsFile = readConfiguredPath(
105
+ input.adPlacementsFile ?? process.env.MPGD_AD_PLACEMENTS_FILE,
106
+ );
107
+
108
+ if ((productCatalogFile === undefined) !== (adPlacementsFile === undefined)) {
109
+ throw new Error(
110
+ 'productCatalogFile and adPlacementsFile '
111
+ + '(MPGD_PRODUCT_CATALOG_FILE / MPGD_AD_PLACEMENTS_FILE) must be configured together.',
112
+ );
113
+ }
114
+
115
+ if (productCatalogFile === undefined || adPlacementsFile === undefined) {
116
+ return {};
117
+ }
118
+
119
+ const catalogBaseDir = resolveCatalogBaseDir(
120
+ productCatalogFile,
121
+ adPlacementsFile,
122
+ input.gameRoot,
123
+ );
124
+
125
+ return {
126
+ '@mpgd/catalog/catalog.json': resolve(catalogBaseDir, productCatalogFile),
127
+ '@mpgd/catalog/placements.json': resolve(catalogBaseDir, adPlacementsFile),
128
+ };
129
+ }
130
+
131
+ function readConfiguredPath(value: string | undefined): string | undefined {
132
+ const normalized = value?.trim();
133
+ return normalized === undefined || normalized.length === 0 ? undefined : normalized;
134
+ }
135
+
136
+ function readRuntimePlatformTarget(
137
+ targetsFileInput: string | undefined,
138
+ configTarget: string,
139
+ ): RuntimePlatformTargetMetadata | undefined {
140
+ const targetsFile = readConfiguredPath(targetsFileInput);
141
+
142
+ if (targetsFile === undefined || configTarget.length === 0) {
143
+ return undefined;
144
+ }
145
+
146
+ const resolvedTargetsFile = resolve(targetsFile);
147
+ let parsed: unknown;
148
+
149
+ try {
150
+ parsed = JSON.parse(readFileSync(resolvedTargetsFile, 'utf8'));
151
+ } catch (error) {
152
+ throw new Error(
153
+ `Failed to read or parse MPGD_PLATFORM_TARGETS_FILE at ${resolvedTargetsFile}: ${formatError(error)}`,
154
+ );
155
+ }
156
+
157
+ if (!isRecord(parsed) || !isRecord(parsed.targets)) {
158
+ throw new Error('MPGD_PLATFORM_TARGETS_FILE must contain a targets object.');
159
+ }
160
+
161
+ const target = parsed.targets[configTarget];
162
+
163
+ if (!isRecord(target)) {
164
+ throw new Error(`Missing platform target metadata for ${configTarget}.`);
165
+ }
166
+
167
+ if (typeof target.kind !== 'string' || typeof target.adapter !== 'string') {
168
+ throw new Error(`Platform target ${configTarget} must define kind and adapter.`);
169
+ }
170
+
171
+ if (target.integrations !== undefined && !isRecord(target.integrations)) {
172
+ throw new Error(`Platform target ${configTarget} integrations must be an object.`);
173
+ }
174
+
175
+ return {
176
+ kind: target.kind,
177
+ adapter: target.adapter,
178
+ ...(target.integrations === undefined ? {} : { integrations: target.integrations }),
179
+ };
180
+ }
181
+
182
+ function isRecord(input: unknown): input is Record<string, unknown> {
183
+ return typeof input === 'object' && input !== null && !Array.isArray(input);
184
+ }
185
+
186
+ function formatError(error: unknown): string {
187
+ return error instanceof Error ? error.message : String(error);
188
+ }
189
+
190
+ function resolveCatalogBaseDir(path: string, pairedPath: string, gameRoot: string): string {
191
+ const candidates = [
192
+ gameRoot,
193
+ process.cwd(),
194
+ readConfiguredPath(process.env.INIT_CWD),
195
+ readConfiguredPath(process.env.PWD),
196
+ ];
197
+
198
+ for (const candidate of candidates) {
199
+ if (
200
+ candidate !== undefined
201
+ && existsSync(resolve(candidate, path))
202
+ && existsSync(resolve(candidate, pairedPath))
203
+ ) {
204
+ return candidate;
205
+ }
206
+ }
207
+
208
+ throw new Error(
209
+ `Could not locate catalog files (${path}, ${pairedPath}) in any expected directory; `
210
+ + `checked: ${candidates.filter((candidate) => candidate !== undefined).join(', ')}.`,
211
+ );
212
+ }
@@ -1,37 +0,0 @@
1
- import { builtinModules } from 'node:module';
2
-
3
- import ttsc from '@ttsc/unplugin/vite';
4
- import { defineConfig } from 'vite';
5
-
6
- const externalBuiltins = [
7
- ...builtinModules,
8
- ...builtinModules.map((moduleName) => `node:${moduleName}`),
9
- ];
10
-
11
- export default defineConfig({
12
- plugins: [
13
- ttsc({
14
- project: 'tsconfig.json',
15
- plugins: false,
16
- }),
17
- ],
18
- ssr: {
19
- noExternal: true,
20
- },
21
- build: {
22
- ssr: 'src/server/index.ts',
23
- outDir: 'dist/server',
24
- target: 'node22',
25
- minify: 'esbuild',
26
- sourcemap: false,
27
- emptyOutDir: true,
28
- rollupOptions: {
29
- external: externalBuiltins,
30
- output: {
31
- format: 'cjs',
32
- entryFileNames: 'index.cjs',
33
- codeSplitting: false,
34
- },
35
- },
36
- },
37
- });