@playcademy/vite-plugin 0.1.26-alpha.5 → 0.1.26-alpha.7

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/README.md CHANGED
@@ -78,13 +78,15 @@ export default defineConfig({
78
78
  plugins: [
79
79
  playcademy({
80
80
  export: {
81
- platform: 'web', // Platform identifier (auto-detected)
82
81
  autoZip: true, // Create deployment zip (enabled by default)
83
82
  },
84
83
  sandbox: {
85
84
  autoStart: true, // Start sandbox automatically
86
- url: 'http://localhost:4321', // Sandbox URL
87
85
  verbose: false, // Enable debug logging
86
+ logLevel: 'info', // Log level (debug, info, warn, error)
87
+ seed: true, // Seed database with demo data
88
+ recreateDb: false, // Recreate database on each start
89
+ memoryOnly: false, // Use in-memory database
88
90
  realtime: {
89
91
  enabled: false, // Disabled by default, enable for multiplayer
90
92
  port: 4322, // Defaults to API port + 1
@@ -143,14 +145,24 @@ Configuration for manifest generation and build output:
143
145
 
144
146
  Configuration for the development sandbox server:
145
147
 
146
- | Option | Type | Default | Description |
147
- | ------------ | ---------------------------------------- | ----------------------------- | -------------------------------- |
148
- | `autoStart` | `boolean` | `true` | Start sandbox during development |
149
- | `url` | `string` | `'http://localhost:4321/api'` | Sandbox server URL |
150
- | `verbose` | `boolean` | `false` | Enable verbose logging |
151
- | `logLevel` | `'debug' \| 'info' \| 'warn' \| 'error'` | `'info'` | Log level for sandbox server |
152
- | `recreateDb` | `boolean` | `false` | Recreate database on each start |
153
- | `realtime` | `object` | `{...}` | Real-time server configuration |
148
+ | Option | Type | Default | Description |
149
+ | -------------- | ---------------------------------------- | ----------------------------- | --------------------------------------- |
150
+ | `autoStart` | `boolean` | `true` | Start sandbox during development |
151
+ | `url` | `string` | `'http://localhost:4321/api'` | Sandbox server URL |
152
+ | `verbose` | `boolean` | `false` | Enable verbose logging |
153
+ | `logLevel` | `'debug' \| 'info' \| 'warn' \| 'error'` | `'info'` | Log level for sandbox server |
154
+ | `recreateDb` | `boolean` | `false` | Recreate database on each start |
155
+ | `seed` | `boolean` | `true` | Seed database with demo data |
156
+ | `memoryOnly` | `boolean` | `false` | Use in-memory database (non-persistent) |
157
+ | `databasePath` | `string` | `undefined` | Custom path for database file |
158
+ | `realtime` | `object` | `{ enabled: false }` | Real-time server configuration |
159
+
160
+ #### Realtime Options (`sandbox.realtime`)
161
+
162
+ | Option | Type | Default | Description |
163
+ | --------- | --------- | ------------ | -------------------------------- |
164
+ | `enabled` | `boolean` | `false` | Enable WebSocket server |
165
+ | `port` | `number` | API port + 1 | Custom port for WebSocket server |
154
166
 
155
167
  ## Build Output
156
168
 
@@ -317,6 +329,7 @@ export default defineConfig({
317
329
  sandbox: {
318
330
  realtime: {
319
331
  enabled: true,
332
+ port: 4322, // Optional: custom WebSocket port
320
333
  },
321
334
  },
322
335
  }),
@@ -324,6 +337,36 @@ export default defineConfig({
324
337
  })
325
338
  ```
326
339
 
340
+ ### Testing with Clean Database
341
+
342
+ ```typescript
343
+ // Start with a fresh database on each dev server restart
344
+ export default defineConfig({
345
+ plugins: [
346
+ playcademy({
347
+ sandbox: {
348
+ recreateDb: true, // Recreate database on each start
349
+ },
350
+ }),
351
+ ],
352
+ })
353
+ ```
354
+
355
+ ### In-Memory Database for CI
356
+
357
+ ```typescript
358
+ // Use in-memory database for fast, ephemeral testing
359
+ export default defineConfig({
360
+ plugins: [
361
+ playcademy({
362
+ sandbox: {
363
+ memoryOnly: true, // Database in RAM only
364
+ },
365
+ }),
366
+ ],
367
+ })
368
+ ```
369
+
327
370
  ### Godot Export
328
371
 
329
372
  ```typescript
package/dist/index.js CHANGED
@@ -41271,7 +41271,11 @@ function printBanner(viteConfig, servers, projectInfo, pluginVersion) {
41271
41271
  viteConfig.logger.info(`${INDENT}${import_picocolors3.green("➜")} ${import_picocolors3.bold("Game:")} ${import_picocolors3.cyan(projectInfo.slug)}`);
41272
41272
  viteConfig.logger.info(`${INDENT}${import_picocolors3.green("➜")} ${import_picocolors3.bold("Sandbox:")} ${import_picocolors3.cyan(`http://localhost:${import_picocolors3.bold(servers.sandbox.toString())}/api`)}`);
41273
41273
  if (servers.backend) {
41274
- viteConfig.logger.info(`${INDENT}${import_picocolors3.green("➜")} ${import_picocolors3.bold("Backend:")} ${import_picocolors3.cyan(`http://localhost:${import_picocolors3.bold(servers.backend.toString())}/api`)}`);
41274
+ const backendUrl = servers.vite ? `http://localhost:${import_picocolors3.bold(servers.vite.toString())}/api ${import_picocolors3.dim(`(via ${servers.backend})`)}` : `http://localhost:${import_picocolors3.bold(servers.backend.toString())}/api`;
41275
+ viteConfig.logger.info(`${INDENT}${import_picocolors3.green("➜")} ${import_picocolors3.bold("Backend:")} ${import_picocolors3.cyan(backendUrl)}`);
41276
+ }
41277
+ if (servers.realtime) {
41278
+ viteConfig.logger.info(`${INDENT}${import_picocolors3.green("➜")} ${import_picocolors3.bold("Realtime:")} ${import_picocolors3.cyan(`ws://localhost:${import_picocolors3.bold(servers.realtime.toString())}`)}`);
41275
41279
  }
41276
41280
  viteConfig.logger.info("");
41277
41281
  }
@@ -41359,7 +41363,7 @@ async function setupCliDevServer(options) {
41359
41363
  var import_picocolors5 = __toESM(require_picocolors(), 1);
41360
41364
  import { DEFAULT_PORTS as DEFAULT_PORTS2 } from "playcademy/constants";
41361
41365
 
41362
- // ../sandbox/dist/server/index.js
41366
+ // ../sandbox/dist/server.js
41363
41367
  import { createRequire as createRequire2 } from "node:module";
41364
41368
  import * as s3 from "fs";
41365
41369
  import * as o3 from "path";
@@ -153167,30 +153171,30 @@ var package_default2 = {
153167
153171
  type: "module",
153168
153172
  exports: {
153169
153173
  ".": {
153170
- import: "./dist/server/index.js",
153171
- types: "./dist/server/index.d.ts"
153174
+ import: "./dist/server.js",
153175
+ types: "./dist/server.d.ts"
153172
153176
  },
153173
153177
  "./cli": {
153174
- import: "./dist/cli/index.js",
153175
- types: "./dist/cli/index.d.ts"
153178
+ import: "./dist/cli.js",
153179
+ types: "./dist/cli.d.ts"
153176
153180
  },
153177
153181
  "./config": {
153178
- import: "./dist/config/index.js",
153179
- types: "./dist/config/index.d.ts"
153182
+ import: "./dist/config.js",
153183
+ types: "./dist/config.d.ts"
153180
153184
  }
153181
153185
  },
153182
153186
  bin: {
153183
- "playcademy-sandbox": "./dist/cli/index.js"
153187
+ "playcademy-sandbox": "./dist/cli.js"
153184
153188
  },
153185
153189
  files: [
153186
153190
  "dist"
153187
153191
  ],
153188
153192
  scripts: {
153189
153193
  build: "bun run build.ts",
153190
- dev: "bun --watch src/cli.ts",
153194
+ dev: "bun --watch src/cli",
153195
+ docs: "typedoc",
153191
153196
  pub: "bun publish.ts",
153192
- start: "bun src/cli.ts",
153193
- docs: "typedoc"
153197
+ start: "bun src/cli.ts"
153194
153198
  },
153195
153199
  dependencies: {
153196
153200
  "@electric-sql/pglite": "^0.3.2",
@@ -194511,21 +194515,14 @@ async function getTodayXpFromEvents(db, userId, date4, timezone2) {
194511
194515
  };
194512
194516
  }
194513
194517
  init_src();
194514
- async function checkGameAccess(gameId, user) {
194518
+ async function verifyGameAccessById(gameId, user) {
194515
194519
  if (user.role !== "admin") {
194516
194520
  const existingGame = await getDatabase().query.games.findFirst({
194517
194521
  where: and(eq(games.id, gameId), eq(games.developerId, user.id)),
194518
194522
  columns: { id: true }
194519
194523
  });
194520
194524
  if (!existingGame) {
194521
- const gameExists = await getDatabase().query.games.findFirst({
194522
- where: eq(games.id, gameId),
194523
- columns: { id: true }
194524
- });
194525
- if (!gameExists) {
194526
- throw ApiError.notFound("Game not found");
194527
- }
194528
- throw ApiError.forbidden("You do not own this game");
194525
+ throw ApiError.notFound("Game not found");
194529
194526
  }
194530
194527
  } else {
194531
194528
  const gameExists = await getDatabase().query.games.findFirst({
@@ -195006,6 +195003,10 @@ async function applyForDeveloperStatus(ctx) {
195006
195003
  }
195007
195004
  try {
195008
195005
  await db.update(users).set({ developerStatus: "pending" }).where(eq(users.id, user.id));
195006
+ log2.info("[API] Developer status application submitted", {
195007
+ userId: user.id,
195008
+ newStatus: "pending"
195009
+ });
195009
195010
  } catch (error2) {
195010
195011
  log2.error(`Error updating developer status for user ${user.id}:`, {
195011
195012
  error: error2
@@ -200375,6 +200376,7 @@ async function initiateUpload(ctx) {
200375
200376
  throw ApiError.badRequest("Invalid JSON body");
200376
200377
  }
200377
200378
  const { fileName, gameId } = inputData;
200379
+ await verifyGameAccessById(gameId, user);
200378
200380
  if (!uploadDeps) {
200379
200381
  throw ApiError.internal("Upload dependencies not configured");
200380
200382
  }
@@ -200511,7 +200513,7 @@ async function deleteGame(ctx) {
200511
200513
  if (!gameId) {
200512
200514
  throw ApiError.badRequest("Missing game ID");
200513
200515
  }
200514
- await checkGameAccess(gameId, user);
200516
+ await verifyGameAccessById(gameId, user);
200515
200517
  try {
200516
200518
  const db = getDatabase();
200517
200519
  const gameToDelete = await db.query.games.findFirst({
@@ -200526,6 +200528,11 @@ async function deleteGame(ctx) {
200526
200528
  if (result.length === 0) {
200527
200529
  throw ApiError.notFound("Game not found for deletion");
200528
200530
  }
200531
+ log2.info("[API] Game deleted", {
200532
+ gameId: result[0].id,
200533
+ slug: gameToDelete?.slug,
200534
+ hadActiveDeployment: !!activeDeployment
200535
+ });
200529
200536
  if (activeDeployment?.provider === "cloudflare") {
200530
200537
  try {
200531
200538
  const cloudflare2 = getCloudflareProvider();
@@ -200541,7 +200548,7 @@ async function deleteGame(ctx) {
200541
200548
  });
200542
200549
  }
200543
200550
  } catch (cleanupError) {
200544
- log2.info("Non-fatal: Failed to cleanup Cloudflare Worker for deleted game", {
200551
+ log2.warn("Non-fatal: Failed to cleanup Cloudflare Worker for deleted game", {
200545
200552
  gameId,
200546
200553
  deploymentId: activeDeployment.deploymentId,
200547
200554
  cleanupError
@@ -200700,11 +200707,17 @@ async function upsertGameBySlug(ctx) {
200700
200707
  gameId: gameResponse.id
200701
200708
  }).where(eq(mapElements.id, metadataInput.mapElementId));
200702
200709
  } catch (mapError) {
200703
- log2.info(`Failed to update map element ${metadataInput.mapElementId}:`, {
200710
+ log2.warn(`Failed to update map element ${metadataInput.mapElementId}:`, {
200704
200711
  mapError
200705
200712
  });
200706
200713
  }
200707
200714
  }
200715
+ log2.info("[API] Game upserted", {
200716
+ gameId: gameResponse.id,
200717
+ slug: gameResponse.slug,
200718
+ operation: isUpdate ? "update" : "create",
200719
+ displayName: gameResponse.displayName
200720
+ });
200708
200721
  return gameResponse;
200709
200722
  } catch (error2) {
200710
200723
  if (error2 instanceof ApiError) {
@@ -200925,6 +200938,12 @@ async function createItem(ctx) {
200925
200938
  if (!newItem) {
200926
200939
  throw ApiError.internal("Failed to create item in database");
200927
200940
  }
200941
+ log2.info("[API] Item created", {
200942
+ itemId: newItem.id,
200943
+ displayName: newItem.displayName,
200944
+ type: newItem.type,
200945
+ gameId: newItem.gameId
200946
+ });
200928
200947
  return newItem;
200929
200948
  } catch (error2) {
200930
200949
  if (error2 instanceof Error) {
@@ -200977,6 +200996,11 @@ async function updateItem(ctx) {
200977
200996
  if (!updatedItem) {
200978
200997
  throw ApiError.notFound("Item not found for update");
200979
200998
  }
200999
+ log2.info("[API] Item updated", {
201000
+ itemId: updatedItem.id,
201001
+ displayName: updatedItem.displayName,
201002
+ updatedFields: Object.keys(inputData)
201003
+ });
200980
201004
  return updatedItem;
200981
201005
  } catch (error2) {
200982
201006
  if (error2 instanceof ApiError)
@@ -201012,6 +201036,9 @@ async function deleteItem(ctx) {
201012
201036
  if (result.length === 0) {
201013
201037
  throw ApiError.notFound("Item not found for deletion");
201014
201038
  }
201039
+ log2.info("[API] Item deleted", {
201040
+ itemId: result[0].id
201041
+ });
201015
201042
  } catch (error2) {
201016
201043
  if (error2 instanceof ApiError) {
201017
201044
  throw error2;
@@ -201136,6 +201163,12 @@ async function createGameItem(ctx) {
201136
201163
  if (!newItem) {
201137
201164
  throw ApiError.internal("Failed to create item in database");
201138
201165
  }
201166
+ log2.info("[API] Game item created", {
201167
+ itemId: newItem.id,
201168
+ gameId: newItem.gameId,
201169
+ displayName: newItem.displayName,
201170
+ type: newItem.type
201171
+ });
201139
201172
  return newItem;
201140
201173
  } catch (error2) {
201141
201174
  if (error2 instanceof Error) {
@@ -201199,6 +201232,12 @@ async function updateGameItem(ctx) {
201199
201232
  if (!updatedItem) {
201200
201233
  throw ApiError.notFound("Item not found for update");
201201
201234
  }
201235
+ log2.info("[API] Game item updated", {
201236
+ itemId: updatedItem.id,
201237
+ gameId: updatedItem.gameId,
201238
+ displayName: updatedItem.displayName,
201239
+ updatedFields: Object.keys(inputData)
201240
+ });
201202
201241
  return updatedItem;
201203
201242
  } catch (error2) {
201204
201243
  if (error2 instanceof ApiError) {
@@ -201241,6 +201280,10 @@ async function deleteGameItem(ctx) {
201241
201280
  if (result.length === 0) {
201242
201281
  throw ApiError.notFound("Item not found for this game");
201243
201282
  }
201283
+ log2.info("[API] Game item deleted", {
201284
+ itemId: result[0].id,
201285
+ gameId
201286
+ });
201244
201287
  } catch (error2) {
201245
201288
  if (error2 instanceof ApiError) {
201246
201289
  throw error2;
@@ -201304,6 +201347,13 @@ async function createGameItemShopListing(ctx) {
201304
201347
  if (!newListing) {
201305
201348
  throw ApiError.internal("Failed to create shop listing in database");
201306
201349
  }
201350
+ log2.info("[API] Game item shop listing created", {
201351
+ listingId: newListing.id,
201352
+ gameId,
201353
+ itemId,
201354
+ currencyId: newListing.currencyId,
201355
+ price: newListing.price
201356
+ });
201307
201357
  return newListing;
201308
201358
  } catch (error2) {
201309
201359
  if (error2 instanceof ApiError)
@@ -201412,6 +201462,12 @@ async function updateGameItemShopListing(ctx) {
201412
201462
  if (!updatedListing) {
201413
201463
  throw ApiError.notFound("Shop listing not found for this item");
201414
201464
  }
201465
+ log2.info("[API] Game item shop listing updated", {
201466
+ listingId: updatedListing.id,
201467
+ gameId,
201468
+ itemId,
201469
+ updatedFields: Object.keys(inputData)
201470
+ });
201415
201471
  return updatedListing;
201416
201472
  } catch (error2) {
201417
201473
  if (error2 instanceof ApiError)
@@ -201458,7 +201514,11 @@ async function deleteGameItemShopListing(ctx) {
201458
201514
  if (result.length === 0) {
201459
201515
  throw ApiError.notFound("Shop listing not found for this item");
201460
201516
  }
201461
- log2.info(`Shop listing deleted for item ${itemId} by user ${user.id}`);
201517
+ log2.info("[API] Game item shop listing deleted", {
201518
+ listingId: result[0].id,
201519
+ gameId,
201520
+ itemId
201521
+ });
201462
201522
  } catch (error2) {
201463
201523
  if (error2 instanceof ApiError) {
201464
201524
  throw error2;
@@ -202300,6 +202360,12 @@ async function addInventoryItem(ctx) {
202300
202360
  }
202301
202361
  return updatedQuantity;
202302
202362
  });
202363
+ log2.info("[API] Inventory item added", {
202364
+ userId: user.id,
202365
+ itemId,
202366
+ quantityAdded: qty,
202367
+ newTotal
202368
+ });
202303
202369
  return { newTotal };
202304
202370
  } catch (error2) {
202305
202371
  if (error2 instanceof ApiError) {
@@ -202350,6 +202416,12 @@ async function removeInventoryItem(ctx) {
202350
202416
  }
202351
202417
  return updatedItemRecord.quantity;
202352
202418
  });
202419
+ log2.info("[API] Inventory item removed", {
202420
+ userId: user.id,
202421
+ itemId,
202422
+ quantityRemoved: qty,
202423
+ newTotal
202424
+ });
202353
202425
  return { newTotal };
202354
202426
  } catch (error2) {
202355
202427
  if (error2 instanceof ApiError) {
@@ -202703,6 +202775,12 @@ async function createCurrency(ctx) {
202703
202775
  if (!newCurrency) {
202704
202776
  throw ApiError.internal("Failed to create currency in database");
202705
202777
  }
202778
+ log2.info("[API] Currency created", {
202779
+ currencyId: newCurrency.id,
202780
+ itemId: newCurrency.itemId,
202781
+ symbol: newCurrency.symbol,
202782
+ isPrimary: newCurrency.isPrimary
202783
+ });
202706
202784
  return newCurrency;
202707
202785
  } catch (error2) {
202708
202786
  if (error2 instanceof ApiError)
@@ -202756,6 +202834,11 @@ async function updateCurrency(ctx) {
202756
202834
  if (!updatedCurrency) {
202757
202835
  throw ApiError.notFound("Currency not found for update");
202758
202836
  }
202837
+ log2.info("[API] Currency updated", {
202838
+ currencyId: updatedCurrency.id,
202839
+ itemId: updatedCurrency.itemId,
202840
+ updatedFields: Object.keys(inputData)
202841
+ });
202759
202842
  return updatedCurrency;
202760
202843
  } catch (error2) {
202761
202844
  if (error2 instanceof ApiError)
@@ -202794,6 +202877,9 @@ async function deleteCurrency(ctx) {
202794
202877
  if (result.length === 0) {
202795
202878
  throw ApiError.notFound("Currency not found for deletion");
202796
202879
  }
202880
+ log2.info("[API] Currency deleted", {
202881
+ currencyId: result[0].id
202882
+ });
202797
202883
  } catch (error2) {
202798
202884
  if (error2 instanceof ApiError)
202799
202885
  throw error2;
@@ -203287,6 +203373,12 @@ async function createShopListing(ctx) {
203287
203373
  if (!newListing) {
203288
203374
  throw ApiError.internal("Failed to create shop listing in database");
203289
203375
  }
203376
+ log2.info("[API] Shop listing created", {
203377
+ listingId: newListing.id,
203378
+ itemId: newListing.itemId,
203379
+ currencyId: newListing.currencyId,
203380
+ price: newListing.price
203381
+ });
203290
203382
  return newListing;
203291
203383
  } catch (error2) {
203292
203384
  if (error2 instanceof ApiError)
@@ -203404,6 +203496,11 @@ async function updateShopListing(ctx) {
203404
203496
  if (!updatedListing) {
203405
203497
  throw ApiError.notFound("Shop listing not found for update");
203406
203498
  }
203499
+ log2.info("[API] Shop listing updated", {
203500
+ listingId: updatedListing.id,
203501
+ itemId: updatedListing.itemId,
203502
+ updatedFields: Object.keys(inputData)
203503
+ });
203407
203504
  return updatedListing;
203408
203505
  } catch (error2) {
203409
203506
  if (error2 instanceof ApiError)
@@ -203447,6 +203544,9 @@ async function deleteShopListing(ctx) {
203447
203544
  if (result.length === 0) {
203448
203545
  throw ApiError.notFound("Shop listing not found for deletion");
203449
203546
  }
203547
+ log2.info("[API] Shop listing deleted", {
203548
+ listingId: result[0].id
203549
+ });
203450
203550
  } catch (error2) {
203451
203551
  if (error2 instanceof ApiError)
203452
203552
  throw error2;
@@ -203715,6 +203815,10 @@ async function createPlayerCharacter(ctx) {
203715
203815
  await tx.update(users).set({ characterCreated: true }).where(eq(users.id, user.id));
203716
203816
  return characterRow;
203717
203817
  });
203818
+ log2.info("[API] Player character created", {
203819
+ userId: user.id,
203820
+ characterId: result.id
203821
+ });
203718
203822
  return result;
203719
203823
  } catch (error2) {
203720
203824
  log2.error("Error creating player character", { error: error2 });
@@ -203749,6 +203853,11 @@ async function updatePlayerCharacter(ctx) {
203749
203853
  const [row] = await db.update(playerCharacters).set({ ...payload, updatedAt: new Date }).where(eq(playerCharacters.userId, user.id)).returning();
203750
203854
  if (!row)
203751
203855
  throw ApiError.notFound("Player character not found");
203856
+ log2.info("[API] Player character updated", {
203857
+ userId: user.id,
203858
+ characterId: row.id,
203859
+ updatedFields: Object.keys(payload)
203860
+ });
203752
203861
  return row;
203753
203862
  } catch (error2) {
203754
203863
  log2.error("Error updating player character", { error: error2 });
@@ -205397,17 +205506,30 @@ function detectTimebackOptions() {
205397
205506
  }
205398
205507
 
205399
205508
  // src/lib/sandbox/server.ts
205400
- function printSandboxInfo(viteConfig, apiPort, realtimePort, projectInfo) {
205509
+ function printSandboxInfo(viteConfig, apiPort, realtimePort, projectInfo, realtimeEnabled) {
205401
205510
  viteConfig.logger.info("");
205402
205511
  viteConfig.logger.info(` ${import_picocolors5.default.green(import_picocolors5.default.bold("PLAYCADEMY"))} ${import_picocolors5.default.green(`v${version3}`)}`);
205403
205512
  viteConfig.logger.info("");
205404
205513
  viteConfig.logger.info(` ${import_picocolors5.default.green("➜")} ${import_picocolors5.default.bold("Game:")} ${import_picocolors5.default.cyan(projectInfo.slug)}`);
205405
205514
  viteConfig.logger.info(` ${import_picocolors5.default.green("➜")} ${import_picocolors5.default.bold("Sandbox:")} ${import_picocolors5.default.cyan(`http://localhost:${import_picocolors5.default.bold(apiPort.toString())}/api`)}`);
205406
- viteConfig.logger.info(` ${import_picocolors5.default.green("➜")} ${import_picocolors5.default.bold("Realtime:")} ${import_picocolors5.default.cyan(`ws://localhost:${import_picocolors5.default.bold(realtimePort.toString())}`)}`);
205515
+ if (realtimeEnabled) {
205516
+ viteConfig.logger.info(` ${import_picocolors5.default.green("➜")} ${import_picocolors5.default.bold("Realtime:")} ${import_picocolors5.default.cyan(`ws://localhost:${import_picocolors5.default.bold(realtimePort.toString())}`)}`);
205517
+ }
205407
205518
  viteConfig.logger.info("");
205408
205519
  }
205409
205520
  async function startSandbox(viteConfig, autoStart = true, options = {}) {
205410
- const { verbose = false, customUrl, quiet = false, recreateDb = false, logLevel = "info" } = options;
205521
+ const {
205522
+ verbose = false,
205523
+ customUrl,
205524
+ quiet = false,
205525
+ recreateDb = false,
205526
+ seed = true,
205527
+ memoryOnly = false,
205528
+ databasePath,
205529
+ realtimeEnabled = false,
205530
+ realtimePort,
205531
+ logLevel = "info"
205532
+ } = options;
205411
205533
  if (!autoStart || viteConfig.command !== "serve") {
205412
205534
  const baseUrl = customUrl ?? `http://localhost:${DEFAULT_PORTS2.SANDBOX}`;
205413
205535
  const deriveRealtimeUrl = (url) => {
@@ -205424,6 +205546,7 @@ async function startSandbox(viteConfig, autoStart = true, options = {}) {
205424
205546
  baseUrl,
205425
205547
  realtimeUrl: deriveRealtimeUrl(baseUrl),
205426
205548
  port: DEFAULT_PORTS2.SANDBOX,
205549
+ realtimePort: undefined,
205427
205550
  project: null,
205428
205551
  cleanup: () => {}
205429
205552
  };
@@ -205431,17 +205554,19 @@ async function startSandbox(viteConfig, autoStart = true, options = {}) {
205431
205554
  try {
205432
205555
  const sandboxPort = await findAvailablePort(DEFAULT_PORTS2.SANDBOX);
205433
205556
  const baseUrl = `http://localhost:${sandboxPort}`;
205434
- const realtimePort = await findAvailablePort(sandboxPort + 1);
205435
- const realtimeUrl = `ws://localhost:${realtimePort}`;
205436
205557
  const projectInfo = await extractProjectInfo(viteConfig);
205437
205558
  const timebackOptions = detectTimebackOptions();
205559
+ const finalRealtimePort = realtimePort ?? await findAvailablePort(sandboxPort + 1);
205560
+ const realtimeUrl = `ws://localhost:${finalRealtimePort}`;
205438
205561
  const server = await startServer2(sandboxPort, projectInfo, {
205439
205562
  verbose,
205440
205563
  quiet,
205441
- seed: true,
205564
+ seed,
205565
+ memoryOnly,
205566
+ databasePath,
205442
205567
  recreateDb,
205443
205568
  logLevel,
205444
- realtime: { enabled: false, port: realtimePort },
205569
+ realtime: { enabled: realtimeEnabled, port: finalRealtimePort },
205445
205570
  timeback: timebackOptions,
205446
205571
  logger: createLoggerAdapter("sandbox")
205447
205572
  });
@@ -205454,13 +205579,14 @@ async function startSandbox(viteConfig, autoStart = true, options = {}) {
205454
205579
  });
205455
205580
  if (!quiet) {
205456
205581
  setTimeout(() => {
205457
- printSandboxInfo(viteConfig, sandboxPort, realtimePort, projectInfo);
205582
+ printSandboxInfo(viteConfig, sandboxPort, finalRealtimePort, projectInfo, realtimeEnabled);
205458
205583
  }, 100);
205459
205584
  }
205460
205585
  return {
205461
205586
  baseUrl,
205462
205587
  realtimeUrl,
205463
205588
  port: sandboxPort,
205589
+ realtimePort: realtimeEnabled ? finalRealtimePort : undefined,
205464
205590
  project: projectInfo,
205465
205591
  cleanup: () => {
205466
205592
  cleanupServerInfo("sandbox", viteConfig.root, process.pid);
@@ -205475,6 +205601,7 @@ async function startSandbox(viteConfig, autoStart = true, options = {}) {
205475
205601
  baseUrl: `http://localhost:${DEFAULT_PORTS2.SANDBOX}`,
205476
205602
  realtimeUrl: "ws://localhost:4322",
205477
205603
  port: DEFAULT_PORTS2.SANDBOX,
205604
+ realtimePort: undefined,
205478
205605
  project: null,
205479
205606
  cleanup: () => {}
205480
205607
  };
@@ -205956,7 +206083,12 @@ async function configurePlatformMode(server, viteConfig, options) {
205956
206083
  logLevel: options.logLevel,
205957
206084
  customUrl: options.sandboxUrl,
205958
206085
  quiet: true,
205959
- recreateDb: options.recreateDb
206086
+ recreateDb: options.recreateDb,
206087
+ seed: options.seed,
206088
+ memoryOnly: options.memoryOnly,
206089
+ databasePath: options.databasePath,
206090
+ realtimeEnabled: options.realtimeEnabled,
206091
+ realtimePort: options.realtimePort
205960
206092
  });
205961
206093
  serverState.sandbox = sandbox;
205962
206094
  const backend = await setupCliDevServer({
@@ -205972,7 +206104,13 @@ async function configurePlatformMode(server, viteConfig, options) {
205972
206104
  server.httpServer?.once("listening", () => {
205973
206105
  setTimeout(async () => {
205974
206106
  const projectInfo = await extractProjectInfo(viteConfig);
205975
- printBanner(viteConfig, { sandbox: sandbox.port, backend: backend?.port }, projectInfo, package_default.version);
206107
+ const vitePort = server.config.server.port;
206108
+ printBanner(viteConfig, {
206109
+ sandbox: sandbox.port,
206110
+ backend: backend?.port,
206111
+ realtime: sandbox.realtimePort,
206112
+ vite: vitePort
206113
+ }, projectInfo, package_default.version);
205976
206114
  }, 100);
205977
206115
  });
205978
206116
  }
@@ -206011,9 +206149,6 @@ async function toggleMode(options) {
206011
206149
  options.viteConfig.logger.error("[Playcademy] Cannot toggle mode: no Vite server reference");
206012
206150
  return;
206013
206151
  }
206014
- options.viteConfig.logger.info("");
206015
- options.viteConfig.logger.info(import_picocolors7.default.yellow(`Switching from ${import_picocolors7.default.bold(currentMode)} to ${import_picocolors7.default.bold(newMode)} mode...`));
206016
- options.viteConfig.logger.info("");
206017
206152
  await cleanupServers();
206018
206153
  await new Promise((resolve2) => setTimeout(resolve2, 100));
206019
206154
  setCurrentMode(newMode);
@@ -206022,9 +206157,9 @@ async function toggleMode(options) {
206022
206157
  } else {
206023
206158
  await configurePlatformMode(viteServer, options.viteConfig, options.platformModeOptions);
206024
206159
  }
206025
- options.viteConfig.logger.info("");
206026
- options.viteConfig.logger.info(import_picocolors7.default.green(`✓ Switched to ${import_picocolors7.default.bold(newMode)} mode`));
206027
- options.viteConfig.logger.info("");
206160
+ options.viteConfig.logger.info(`
206161
+ ${import_picocolors7.default.green(`✓ Switched to ${import_picocolors7.default.bold(newMode)} mode`)}
206162
+ `);
206028
206163
  }
206029
206164
 
206030
206165
  // src/hooks/configure-server.ts
@@ -206046,6 +206181,11 @@ async function configureServerHook(server, context) {
206046
206181
  logLevel: context.options.logLevel,
206047
206182
  sandboxUrl: context.options.sandboxUrl,
206048
206183
  recreateDb: context.options.recreateDb,
206184
+ seed: context.options.seed,
206185
+ memoryOnly: context.options.memoryOnly,
206186
+ databasePath: context.options.databasePath,
206187
+ realtimeEnabled: context.options.realtimeEnabled,
206188
+ realtimePort: context.options.realtimePort,
206049
206189
  showBadge: context.options.showBadge,
206050
206190
  preferredBackendPort: preferredPort
206051
206191
  };
@@ -206103,6 +206243,7 @@ function resolveOptions(options) {
206103
206243
  const exportOptions = options.export ?? {};
206104
206244
  const sandboxOptions = options.sandbox ?? {};
206105
206245
  const shellOptions = options.shell ?? {};
206246
+ const realtimeOptions = sandboxOptions.realtime ?? {};
206106
206247
  return {
206107
206248
  mode: options.mode ?? "platform",
206108
206249
  autoZip: exportOptions.autoZip ?? true,
@@ -206111,6 +206252,11 @@ function resolveOptions(options) {
206111
206252
  verbose: sandboxOptions.verbose ?? false,
206112
206253
  logLevel: sandboxOptions.logLevel ?? "info",
206113
206254
  recreateDb: sandboxOptions.recreateDb ?? false,
206255
+ seed: sandboxOptions.seed ?? true,
206256
+ memoryOnly: sandboxOptions.memoryOnly ?? false,
206257
+ databasePath: sandboxOptions.databasePath,
206258
+ realtimeEnabled: realtimeOptions.enabled ?? false,
206259
+ realtimePort: realtimeOptions.port,
206114
206260
  showBadge: shellOptions.showBadge ?? true
206115
206261
  };
206116
206262
  }
@@ -20,4 +20,6 @@ export declare const log: {
20
20
  export declare function printBanner(viteConfig: ResolvedConfig, servers: {
21
21
  sandbox: number;
22
22
  backend?: number;
23
+ realtime?: number;
24
+ vite?: number;
23
25
  }, projectInfo: ProjectInfo, pluginVersion: string): void;
@@ -5,5 +5,10 @@ export declare function startSandbox(viteConfig: ResolvedConfig, autoStart?: boo
5
5
  customUrl?: string;
6
6
  quiet?: boolean;
7
7
  recreateDb?: boolean;
8
+ seed?: boolean;
9
+ memoryOnly?: boolean;
10
+ databasePath?: string;
11
+ realtimeEnabled?: boolean;
12
+ realtimePort?: number;
8
13
  logLevel?: 'debug' | 'info' | 'warn' | 'error';
9
14
  }): Promise<SandboxManager>;
@@ -9,6 +9,11 @@ export interface PlatformModeOptions {
9
9
  logLevel: 'debug' | 'info' | 'warn' | 'error';
10
10
  sandboxUrl: string;
11
11
  recreateDb: boolean;
12
+ seed: boolean;
13
+ memoryOnly: boolean;
14
+ databasePath?: string;
15
+ realtimeEnabled: boolean;
16
+ realtimePort?: number;
12
17
  showBadge: boolean;
13
18
  preferredBackendPort: number;
14
19
  }
@@ -14,6 +14,11 @@ export interface ResolvedPluginOptions {
14
14
  verbose: boolean;
15
15
  logLevel: 'debug' | 'info' | 'warn' | 'error';
16
16
  recreateDb: boolean;
17
+ seed: boolean;
18
+ memoryOnly: boolean;
19
+ databasePath?: string;
20
+ realtimeEnabled: boolean;
21
+ realtimePort?: number;
17
22
  showBadge: boolean;
18
23
  }
19
24
  /**
@@ -50,6 +55,7 @@ export interface SandboxManager {
50
55
  baseUrl: string;
51
56
  realtimeUrl: string;
52
57
  port: number;
58
+ realtimePort?: number;
53
59
  project: ProjectInfo | null;
54
60
  cleanup: () => void;
55
61
  }
@@ -3,58 +3,319 @@
3
3
  */
4
4
  /**
5
5
  * Plugin operation mode
6
- * - 'platform': Full Playcademy platform with sandbox + backend + shell wrapper (default)
7
- * - 'standalone': Backend only, no sandbox or shell (for testing integrations)
6
+ *
7
+ * Controls how the Vite plugin operates during development:
8
+ * - `'platform'`: Full Playcademy platform experience with sandbox server, backend bundling, and shell wrapper (default)
9
+ * - `'standalone'`: Backend only, no sandbox or shell
10
+ *
11
+ * @default 'platform'
8
12
  */
9
13
  export type PlaycademyMode = 'platform' | 'standalone';
10
14
  /**
11
- * Configuration options for exporting Playcademy games
15
+ * Configuration options for exporting/building Playcademy games
16
+ *
17
+ * Controls how your game is packaged for deployment.
12
18
  */
13
19
  export interface PlaycademyExportOptions {
14
20
  /**
15
- * Automatically create a deployment zip archive at .playcademy/{project-name}.zip
21
+ * Automatically create a deployment zip archive after build.
22
+ *
23
+ * The zip file is created at `.playcademy/{project-name}.zip` and contains
24
+ * all files needed for deployment (frontend assets + backend bundle).
25
+ *
16
26
  * @default true
27
+ * @example
28
+ * ```ts
29
+ * export: {
30
+ * autoZip: false // Disable auto-zipping
31
+ * }
32
+ * ```
17
33
  */
18
34
  autoZip?: boolean;
19
35
  }
20
36
  /**
21
37
  * Configuration options for the Playcademy sandbox server
38
+ *
39
+ * The sandbox server provides a local development environment that simulates
40
+ * the Playcademy platform, including API endpoints, authentication, and database.
22
41
  */
23
42
  export interface PlaycademySandboxOptions {
43
+ /**
44
+ * Automatically start the sandbox server when Vite starts.
45
+ *
46
+ * Set to `false` if you want to start the sandbox server manually.
47
+ *
48
+ * @default true
49
+ * @example
50
+ * ```ts
51
+ * sandbox: {
52
+ * autoStart: false // Start sandbox manually
53
+ * }
54
+ * ```
55
+ */
24
56
  autoStart?: boolean;
57
+ /**
58
+ * Custom URL for the sandbox server.
59
+ *
60
+ * Useful if you need to run the sandbox on a specific host or port.
61
+ * By default, the sandbox uses `http://localhost:{port}` where port
62
+ * is auto-assigned.
63
+ *
64
+ * @example
65
+ * ```ts
66
+ * sandbox: {
67
+ * url: 'http://localhost:8788'
68
+ * }
69
+ * ```
70
+ */
25
71
  url?: string;
72
+ /**
73
+ * Enable verbose logging for the sandbox server.
74
+ *
75
+ * Shows detailed information about requests, database operations, and more.
76
+ * Equivalent to setting `logLevel: 'debug'`.
77
+ *
78
+ * @default false
79
+ * @example
80
+ * ```ts
81
+ * sandbox: {
82
+ * verbose: true
83
+ * }
84
+ * ```
85
+ */
26
86
  verbose?: boolean;
27
87
  /**
28
- * Log level for the sandbox server
88
+ * Log level for the sandbox server.
89
+ *
90
+ * Controls the verbosity of sandbox server logs:
91
+ * - `'debug'`: Very detailed logs (all operations)
92
+ * - `'info'`: Standard operational logs
93
+ * - `'warn'`: Warnings only
94
+ * - `'error'`: Errors only
95
+ *
29
96
  * @default 'info'
97
+ * @example
98
+ * ```ts
99
+ * sandbox: {
100
+ * logLevel: 'debug' // Show all debug info
101
+ * }
102
+ * ```
30
103
  */
31
104
  logLevel?: 'debug' | 'info' | 'warn' | 'error';
32
105
  /**
33
- * Recreate the sandbox database on each start
106
+ * Recreate the sandbox database on each server start.
107
+ *
108
+ * When `true`, the database is dropped and recreated with seed data
109
+ * every time the dev server starts. Useful for ensuring a clean state
110
+ * during development.
111
+ *
112
+ * **Warning**: All existing data will be lost on each restart.
113
+ *
34
114
  * @default false
115
+ * @example
116
+ * ```ts
117
+ * sandbox: {
118
+ * recreateDb: true // Fresh database on every restart
119
+ * }
120
+ * ```
35
121
  */
36
122
  recreateDb?: boolean;
123
+ /**
124
+ * Seed the database with demo data on startup.
125
+ *
126
+ * Creates demo users, games, achievements, and other platform data
127
+ * for testing. Disable if you want to start with an empty database.
128
+ *
129
+ * @default true
130
+ * @example
131
+ * ```ts
132
+ * sandbox: {
133
+ * seed: false // Start with empty database
134
+ * }
135
+ * ```
136
+ */
137
+ seed?: boolean;
138
+ /**
139
+ * Use an in-memory database instead of a file.
140
+ *
141
+ * The database only exists in RAM and is lost when the server stops.
142
+ * Faster but non-persistent. Useful for testing and CI environments.
143
+ *
144
+ * @default false
145
+ * @example
146
+ * ```ts
147
+ * sandbox: {
148
+ * memoryOnly: true // Database in RAM only
149
+ * }
150
+ * ```
151
+ */
152
+ memoryOnly?: boolean;
153
+ /**
154
+ * Custom path for the database file.
155
+ *
156
+ * Specifies where the SQLite database file should be stored.
157
+ * If not provided, defaults to a path based on node_modules location.
158
+ *
159
+ * Special value `':memory:'` creates an in-memory database
160
+ * (equivalent to `memoryOnly: true`).
161
+ *
162
+ * @example
163
+ * ```ts
164
+ * sandbox: {
165
+ * databasePath: './my-game-sandbox.db'
166
+ * }
167
+ * ```
168
+ *
169
+ * @example
170
+ * ```ts
171
+ * sandbox: {
172
+ * databasePath: ':memory:' // In-memory database
173
+ * }
174
+ * ```
175
+ */
176
+ databasePath?: string;
177
+ /**
178
+ * Real-time/WebSocket server configuration.
179
+ *
180
+ * The real-time server provides WebSocket support for live updates,
181
+ * multiplayer features, and other real-time functionality.
182
+ *
183
+ * @default { enabled: false }
184
+ * @example
185
+ * ```ts
186
+ * sandbox: {
187
+ * realtime: {
188
+ * enabled: true,
189
+ * port: 4322 // Custom WebSocket port
190
+ * }
191
+ * }
192
+ * ```
193
+ */
194
+ realtime?: {
195
+ /**
196
+ * Enable the real-time/WebSocket server.
197
+ *
198
+ * @default false
199
+ */
200
+ enabled?: boolean;
201
+ /**
202
+ * Port for the WebSocket server.
203
+ *
204
+ * By default, uses the HTTP port + 1 (e.g., if HTTP is 4321,
205
+ * WebSocket will be 4322).
206
+ */
207
+ port?: number;
208
+ };
37
209
  }
38
210
  /**
39
211
  * Configuration options for the development shell wrapper
212
+ *
213
+ * The shell provides the platform UI during development, including the
214
+ * Playcademy badge, game selection, and other platform features.
40
215
  */
41
216
  export interface PlaycademyShellOptions {
42
217
  /**
43
- * Show the Playcademy badge in the corner during development
218
+ * Show the Playcademy badge in the corner during development.
219
+ *
44
220
  * @default true
221
+ * @example
222
+ * ```ts
223
+ * shell: {
224
+ * showBadge: false // Hide the badge
225
+ * }
226
+ * ```
45
227
  */
46
228
  showBadge?: boolean;
47
229
  }
48
230
  /**
49
- * Main plugin configuration options
231
+ * Main configuration options for the Playcademy Vite plugin
232
+ *
233
+ * Configure how your game integrates with the Playcademy platform during
234
+ * development and build.
235
+ *
236
+ * @example
237
+ * ```ts
238
+ * // vite.config.ts
239
+ * import { playcademy } from '@playcademy/vite-plugin'
240
+ *
241
+ * export default defineConfig({
242
+ * plugins: [
243
+ * playcademy({
244
+ * mode: 'platform',
245
+ * sandbox: {
246
+ * logLevel: 'debug',
247
+ * recreateDb: true
248
+ * },
249
+ * shell: {
250
+ * showBadge: true
251
+ * }
252
+ * })
253
+ * ]
254
+ * })
255
+ * ```
50
256
  */
51
257
  export interface PlaycademyPluginOptions {
52
258
  /**
53
- * Plugin operation mode
259
+ * Plugin operation mode.
260
+ *
261
+ * - `'platform'`: Full development experience with sandbox server and shell (recommended)
262
+ * - `'standalone'`: Backend bundling only, no platform features
263
+ *
264
+ * Most games should use `'platform'` mode.
265
+ *
54
266
  * @default 'platform'
267
+ * @example
268
+ * ```ts
269
+ * {
270
+ * mode: 'standalone' // For testing backend in isolation
271
+ * }
272
+ * ```
55
273
  */
56
274
  mode?: PlaycademyMode;
275
+ /**
276
+ * Export/build configuration options.
277
+ *
278
+ * Controls how your game is packaged for deployment.
279
+ *
280
+ * @example
281
+ * ```ts
282
+ * {
283
+ * export: {
284
+ * autoZip: true // Create deployment zip automatically
285
+ * }
286
+ * }
287
+ * ```
288
+ */
57
289
  export?: PlaycademyExportOptions;
290
+ /**
291
+ * Sandbox server configuration options.
292
+ *
293
+ * The sandbox provides a local Playcademy platform environment for development.
294
+ *
295
+ * @example
296
+ * ```ts
297
+ * {
298
+ * sandbox: {
299
+ * logLevel: 'debug',
300
+ * recreateDb: true
301
+ * }
302
+ * }
303
+ * ```
304
+ */
58
305
  sandbox?: PlaycademySandboxOptions;
306
+ /**
307
+ * Development shell configuration options.
308
+ *
309
+ * The shell wraps your game with platform UI during development.
310
+ *
311
+ * @example
312
+ * ```ts
313
+ * {
314
+ * shell: {
315
+ * showBadge: false
316
+ * }
317
+ * }
318
+ * ```
319
+ */
59
320
  shell?: PlaycademyShellOptions;
60
321
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcademy/vite-plugin",
3
- "version": "0.1.26-alpha.5",
3
+ "version": "0.1.26-alpha.7",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {