@dreamboard-games/workspace-codegen 0.1.1 → 0.1.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dreamboard-games/workspace-codegen",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Internal Dreamboard workspace source generator",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -20,8 +20,8 @@
20
20
  "test:unit": "bun test --timeout 30000"
21
21
  },
22
22
  "dependencies": {
23
- "@dreamboard/sdk-types": "npm:@dreamboard-games/sdk-types@0.1.1",
24
- "@dreamboard/ui-sdk": "npm:@dreamboard-games/ui-sdk@0.0.42"
23
+ "@dreamboard/sdk-types": "npm:@dreamboard-games/sdk-types@0.1.2",
24
+ "@dreamboard/ui-sdk": "npm:@dreamboard-games/ui-sdk@0.0.45"
25
25
  },
26
26
  "license": "SEE LICENSE IN LICENSE",
27
27
  "publishConfig": {
@@ -1,4 +1,12 @@
1
- import { lstat, mkdir, mkdtemp, readFile, rm, symlink } from "node:fs/promises";
1
+ import {
2
+ lstat,
3
+ mkdir,
4
+ mkdtemp,
5
+ readFile,
6
+ readlink,
7
+ rm,
8
+ symlink,
9
+ } from "node:fs/promises";
2
10
  import os from "node:os";
3
11
  import path from "node:path";
4
12
  import process from "node:process";
@@ -34,7 +42,8 @@ const EXPECTED_UI_FRAMEWORK_TSCONFIG = `{
34
42
  "jsx": "react-jsx",
35
43
  "strict": true,
36
44
  "esModuleInterop": true,
37
- "skipLibCheck": true,
45
+ "skipLibCheck": false,
46
+ "types": [],
38
47
  "paths": {
39
48
  "@dreamboard/manifest-contract": [
40
49
  "../shared/manifest-contract.ts"
@@ -59,6 +68,44 @@ const EXPECTED_UI_FRAMEWORK_TSCONFIG = `{
59
68
  }
60
69
  `;
61
70
 
71
+ const EXPECTED_APP_FRAMEWORK_TSCONFIG = `{
72
+ "compilerOptions": {
73
+ "target": "ES2020",
74
+ "module": "ESNext",
75
+ "moduleResolution": "bundler",
76
+ "strict": true,
77
+ "esModuleInterop": true,
78
+ "skipLibCheck": false,
79
+ "types": [],
80
+ "declaration": false,
81
+ "rootDir": "..",
82
+ "outDir": "./dist",
83
+ "paths": {
84
+ "@dreamboard/manifest-contract": [
85
+ "../shared/manifest-contract.ts"
86
+ ],
87
+ "@dreamboard/ui-contract": [
88
+ "../shared/generated/ui-contract.ts"
89
+ ],
90
+ "@shared/*": [
91
+ "../shared/*"
92
+ ]
93
+ }
94
+ },
95
+ "include": [
96
+ "./**/*.ts",
97
+ "./**/*.d.ts",
98
+ "../shared/manifest-*.ts",
99
+ "../shared/manifest-*.d.ts"
100
+ ],
101
+ "exclude": [
102
+ "node_modules",
103
+ "dist",
104
+ "../shared/generated"
105
+ ]
106
+ }
107
+ `;
108
+
62
109
  async function loadBenchmarkManifest(): Promise<GameTopologyManifest> {
63
110
  return materializeManifest(benchmarkRoot) as Promise<GameTopologyManifest>;
64
111
  }
@@ -79,6 +126,32 @@ async function ensureSymlink(targetPath: string, linkPath: string) {
79
126
  );
80
127
  }
81
128
 
129
+ async function ensurePackageSymlink(targetPath: string, linkPath: string) {
130
+ try {
131
+ const stats = await lstat(linkPath);
132
+ if (stats.isSymbolicLink()) {
133
+ const existingTarget = await readlink(linkPath);
134
+ const resolvedExistingTarget = path.resolve(
135
+ path.dirname(linkPath),
136
+ existingTarget,
137
+ );
138
+ if (resolvedExistingTarget === targetPath) {
139
+ return;
140
+ }
141
+ }
142
+ await rm(linkPath, { recursive: true, force: true });
143
+ } catch {
144
+ // Fall through and create the symlink.
145
+ }
146
+
147
+ await mkdir(path.dirname(linkPath), { recursive: true });
148
+ await symlink(
149
+ targetPath,
150
+ linkPath,
151
+ process.platform === "win32" ? "junction" : "dir",
152
+ );
153
+ }
154
+
82
155
  function collectComponentHomeTypes(
83
156
  manifest: GameTopologyManifest,
84
157
  ): Set<string> {
@@ -258,12 +331,15 @@ test("authoritative ui framework tsconfig uses the scaffold format", async () =>
258
331
  await loadBenchmarkManifest(),
259
332
  );
260
333
 
334
+ expect(authoritativeFiles["app/tsconfig.framework.json"]).toBe(
335
+ EXPECTED_APP_FRAMEWORK_TSCONFIG,
336
+ );
261
337
  expect(authoritativeFiles["ui/tsconfig.framework.json"]).toBe(
262
338
  EXPECTED_UI_FRAMEWORK_TSCONFIG,
263
339
  );
264
340
  });
265
341
 
266
- test("generated ui-contract.ts exposes a typed useHexBoardView wrapper", async () => {
342
+ test("generated ui-contract.ts exposes typed hex board view primitives", async () => {
267
343
  const manifest = await loadBenchmarkManifest();
268
344
  const authoritativeFiles = generateAuthoritativeFiles(manifest);
269
345
 
@@ -274,20 +350,31 @@ test("generated ui-contract.ts exposes a typed useHexBoardView wrapper", async (
274
350
  );
275
351
  }
276
352
 
277
- // Imports static topology and exports the workspace-typed helper.
353
+ // Imports static topology and exports the workspace-typed component.
278
354
  expect(uiContract).toContain("staticBoards");
279
- expect(uiContract).toContain("createHexBoardView");
280
- expect(uiContract).toContain("export function useHexBoardView");
281
-
282
- // Constrains the boardId argument to the manifest's hex-board ids and
283
- // narrows TSpaceView.id to that board's space-id type.
355
+ expect(uiContract).toContain(
356
+ "type BoardHexViewProps as BoardHexViewPropsGeneric",
357
+ );
358
+ expect(uiContract).toContain(
359
+ "type BoardHexGridProps as BoardHexGridPropsGeneric",
360
+ );
361
+ expect(uiContract).toContain("export type HexBoardViewProps");
362
+ expect(uiContract).toContain("export type HexBoardGridProps");
363
+ expect(uiContract).toContain("type WorkspaceBoard");
364
+ expect(uiContract).toContain('export const Board: WorkspaceUI["Board"]');
365
+ expect(uiContract).toContain("baseUI.Board.HexView");
366
+ expect(uiContract).toContain("baseUI.Board.HexGrid");
367
+ expect(uiContract).not.toContain("export function useHexBoardView");
368
+
369
+ // Constrains the board prop to the manifest's hex-board ids and narrows
370
+ // TSpaceView.id to that board's space-id type.
284
371
  expect(uiContract).toContain("export type HexBoardId");
285
372
  expect(uiContract).toContain("keyof typeof hexStaticBoards");
286
373
  expect(uiContract).toContain("Id extends HexBoardId");
287
374
  expect(uiContract).toContain("BoardSpaceIdOf");
288
375
  });
289
376
 
290
- test("generated ui-contract.ts exposes typed interaction form wrappers", async () => {
377
+ test("generated ui-contract.ts exposes Interaction.Field without standalone field wrappers", async () => {
291
378
  const manifest = await loadBenchmarkManifest();
292
379
  const authoritativeFiles = generateAuthoritativeFiles(manifest);
293
380
 
@@ -298,13 +385,19 @@ test("generated ui-contract.ts exposes typed interaction form wrappers", async (
298
385
  );
299
386
  }
300
387
 
301
- expect(uiContract).toContain("InteractionForm as InteractionFormGeneric");
302
- expect(uiContract).toContain("export type InteractionFieldRenderers");
303
- expect(uiContract).toContain(
388
+ expect(uiContract).not.toContain("InteractionForm as InteractionFormGeneric");
389
+ expect(uiContract).not.toContain("export type InteractionFieldRenderers");
390
+ expect(uiContract).not.toContain("export type InteractionFieldProps");
391
+ expect(uiContract).not.toContain(
304
392
  "export type InteractionFormProps<Key extends InteractionKey>",
305
393
  );
306
- expect(uiContract).toContain("export function InteractionForm");
307
- expect(uiContract).toContain("export function InteractionField");
394
+ expect(uiContract).not.toContain("export function InteractionForm");
395
+ expect(uiContract).not.toContain("export function InteractionField");
396
+ expect(uiContract).toContain("readonly Interaction: Omit<");
397
+ expect(uiContract).toContain(
398
+ 'DreamboardUI<typeof uiContract>["Interaction"]',
399
+ );
400
+ expect(uiContract).toContain("export const Interaction = UI.Interaction;");
308
401
  });
309
402
 
310
403
  test("generated ui-contract.ts exposes typed Radix-style primitives without GameShell wrappers", async () => {
@@ -318,11 +411,20 @@ test("generated ui-contract.ts exposes typed Radix-style primitives without Game
318
411
  );
319
412
  }
320
413
 
321
- expect(uiContract).toContain("export const UI = createDreamboardUI");
414
+ expect(uiContract).toContain("const baseUI = createDreamboardUI(uiContract)");
415
+ expect(uiContract).toContain("export const UI: WorkspaceUI = {");
416
+ expect(uiContract).toContain("type TypedGame");
417
+ expect(uiContract).toContain("export const Game");
322
418
  expect(uiContract).toContain("export const Interaction = UI.Interaction");
323
419
  expect(uiContract).toContain("export const Prompt = UI.Prompt");
420
+ expect(uiContract).toContain("type UIPlayerRegistry");
421
+ expect(uiContract).toContain("players: {} as UIPlayerRegistry");
422
+ expect(uiContract).toContain("export const PlayerRoster");
423
+ expect(uiContract).toContain('export const Dice: WorkspaceUI["Dice"]');
324
424
  expect(uiContract).toContain("export const Zone = UI.Zone");
325
- expect(uiContract).toContain("export const Board = UI.Board");
425
+ expect(uiContract).toContain("createResourceCounter");
426
+ expect(uiContract).toContain("export const ResourceCounter");
427
+ expect(uiContract).toContain('export const Board: WorkspaceUI["Board"]');
326
428
  expect(uiContract).not.toContain("WorkspaceGameShell");
327
429
  expect(uiContract).not.toContain("GameShell as GameShellGeneric");
328
430
  expect(uiContract).not.toContain("defineMarketSurface");
@@ -330,18 +432,7 @@ test("generated ui-contract.ts exposes typed Radix-style primitives without Game
330
432
  });
331
433
 
332
434
  test("board-contract-lab manifest authoring typechecks", async () => {
333
- await ensureSymlink(
334
- WORKSPACE_NODE_MODULES,
335
- path.join(benchmarkRoot, "node_modules"),
336
- );
337
- await ensureSymlink(
338
- SDK_TYPES_PACKAGE_ROOT,
339
- path.join(benchmarkRoot, "node_modules", "@dreamboard", "sdk-types"),
340
- );
341
- await ensureSymlink(
342
- UI_SDK_NODE_MODULES,
343
- path.join(benchmarkRoot, "ui", "node_modules"),
344
- );
435
+ await prepareExampleTypecheckDependencies(benchmarkRoot);
345
436
 
346
437
  const result = runNode20ExampleScript(benchmarkRoot, "typecheck:manifest");
347
438
 
@@ -354,9 +445,35 @@ test("generated app contracts typecheck under Node 20 without a larger heap", as
354
445
  const examples = [thingsInRingsRoot, benchmarkRoot];
355
446
 
356
447
  for (const exampleRoot of examples) {
448
+ await prepareExampleTypecheckDependencies(exampleRoot);
357
449
  const result = runNode20ExampleScript(exampleRoot, "typecheck:app");
358
450
  if (result.exitCode !== 0) {
359
451
  throwTypecheckError(`${exampleRoot} app`, result);
360
452
  }
361
453
  }
362
454
  }, 120_000);
455
+
456
+ async function prepareExampleTypecheckDependencies(exampleRoot: string) {
457
+ const nodeModulesPath = path.join(exampleRoot, "node_modules");
458
+ await ensureSymlink(WORKSPACE_NODE_MODULES, nodeModulesPath);
459
+ await ensureSymlink(
460
+ SDK_TYPES_PACKAGE_ROOT,
461
+ path.join(exampleRoot, "node_modules", "@dreamboard", "sdk-types"),
462
+ );
463
+ await ensureSymlink(
464
+ UI_SDK_NODE_MODULES,
465
+ path.join(exampleRoot, "ui", "node_modules"),
466
+ );
467
+
468
+ const nodeModulesStats = await lstat(nodeModulesPath);
469
+ if (!nodeModulesStats.isSymbolicLink()) {
470
+ await ensurePackageSymlink(
471
+ path.join(UI_SDK_NODE_MODULES, "@types", "react"),
472
+ path.join(nodeModulesPath, "@types", "react"),
473
+ );
474
+ await ensurePackageSymlink(
475
+ path.join(UI_SDK_NODE_MODULES, "@types", "react-dom"),
476
+ path.join(nodeModulesPath, "@types", "react-dom"),
477
+ );
478
+ }
479
+ }
@@ -724,7 +724,7 @@ test("generateManifestContractSource emits typed topology fields and helper lite
724
724
  expect(source).not.toContain("cardName: z.string().optional()");
725
725
  expect(source).not.toContain("description: z.string().optional()");
726
726
  expect(source).toContain(
727
- "const cardPropertiesSchemaByCardSetId: Record<string, z.ZodTypeAny> = {",
727
+ "const cardPropertiesSchemaByCardSetId: Record<string, z.ZodType<unknown>> = {",
728
728
  );
729
729
  expect(source).toContain(
730
730
  "function createCardStateSchema<CardIdValue extends CardId>(",
@@ -3383,7 +3383,7 @@ function renderCardPropertiesSchemaByCardSetId(
3383
3383
  ` ${quote(cardSet.id)}: ${toPascalCase(cardSet.id)}CardPropertiesSchema,`,
3384
3384
  ];
3385
3385
  });
3386
- return `const cardPropertiesSchemaByCardSetId: Record<string, z.ZodTypeAny> = {\n${entries.join("\n")}\n};`;
3386
+ return `const cardPropertiesSchemaByCardSetId: Record<string, z.ZodType<unknown>> = {\n${entries.join("\n")}\n};`;
3387
3387
  }
3388
3388
 
3389
3389
  function renderObjectSchemaSection(
@@ -3666,7 +3666,7 @@ function renderCardStateSchemaById(analysis: ManifestAnalysis): string {
3666
3666
  return `z.object(
3667
3667
  Object.fromEntries(
3668
3668
  literals.cardIds.map((cardId) => [cardId, createCardStateSchema(cardId)]),
3669
- ) as Record<CardId, z.ZodTypeAny>,
3669
+ ) as Record<CardId, z.ZodType<unknown>>,
3670
3670
  )`;
3671
3671
  }
3672
3672
 
@@ -3773,8 +3773,17 @@ function renderGenericBoardStateSchema(
3773
3773
  },
3774
3774
  templateId: z.string().nullable().optional(),
3775
3775
  fields: ${`${boardFieldsTypeName(board.board.id)}Schema`},
3776
+ // T220: per-board state.spaces is loose-keyed by string. The
3777
+ // canonical narrow id (\`ids.spaceId\`) is a Zod enum of EVERY
3778
+ // space id across every board, which makes a strict
3779
+ // \`z.record(enum, …)\` reject any board whose spaces are a
3780
+ // proper subset of that enum. Loose keying matches the JSON wire
3781
+ // shape (\`additionalProperties\`); the inner \`id: ids.spaceId\`
3782
+ // narrows the value's spaceId at parse time. A future per-board
3783
+ // branded-id refactor (option B) can re-tighten this without a
3784
+ // wire change.
3776
3785
  spaces: z.record(
3777
- ids.spaceId,
3786
+ z.string(),
3778
3787
  z.object({
3779
3788
  id: ids.spaceId,
3780
3789
  name: z.string().nullable().optional(),
@@ -3835,8 +3844,9 @@ function renderHexBoardStateSchema(
3835
3844
  },
3836
3845
  templateId: z.string().nullable().optional(),
3837
3846
  fields: ${`${boardFieldsTypeName(board.board.id)}Schema`},
3847
+ // T220: see generic-board comment on the loose-keying choice.
3838
3848
  spaces: z.record(
3839
- ids.spaceId,
3849
+ z.string(),
3840
3850
  z.object({
3841
3851
  id: ids.spaceId,
3842
3852
  name: z.string().nullable().optional(),
@@ -3907,8 +3917,9 @@ function renderSquareBoardStateSchema(
3907
3917
  },
3908
3918
  templateId: z.string().nullable().optional(),
3909
3919
  fields: ${`${boardFieldsTypeName(board.board.id)}Schema`},
3920
+ // T220: see generic-board comment on the loose-keying choice.
3910
3921
  spaces: z.record(
3911
- ids.spaceId,
3922
+ z.string(),
3912
3923
  z.object({
3913
3924
  id: ids.spaceId,
3914
3925
  name: z.string().nullable().optional(),
@@ -5718,7 +5729,11 @@ const runtimeGenericBoardStateSchema = z.object({
5718
5729
  playerId: ids.playerId.nullable().optional(),
5719
5730
  templateId: z.string().nullable().optional(),
5720
5731
  fields: unknownRecordSchema,
5721
- spaces: z.record(ids.spaceId, boardSpaceStateSchema),
5732
+ // T220: per-board state.spaces is loose-keyed by string. See the
5733
+ // codegen-template comment in renderGenericBoardStateSchema for
5734
+ // the rationale; the wire shape is unchanged (additionalProperties
5735
+ // JSON), and the inner id field narrows at parse time.
5736
+ spaces: z.record(z.string(), boardSpaceStateSchema),
5722
5737
  relations: z.array(boardRelationStateSchema),
5723
5738
  containers: z.record(ids.boardContainerId, boardContainerStateSchema),
5724
5739
  });
@@ -5748,7 +5763,8 @@ const squareVertexStateSchema = z.object({
5748
5763
  });
5749
5764
  const runtimeHexBoardStateSchema = runtimeGenericBoardStateSchema.extend({
5750
5765
  layout: z.literal("hex"),
5751
- spaces: z.record(ids.spaceId, hexSpaceStateSchema),
5766
+ // T220: loose-keyed by string — see comment above.
5767
+ spaces: z.record(z.string(), hexSpaceStateSchema),
5752
5768
  relations: z.array(boardRelationStateSchema),
5753
5769
  containers: z.object({}),
5754
5770
  orientation: z.enum(["pointy-top", "flat-top"]),
@@ -5757,7 +5773,8 @@ const runtimeHexBoardStateSchema = runtimeGenericBoardStateSchema.extend({
5757
5773
  });
5758
5774
  const runtimeSquareBoardStateSchema = runtimeGenericBoardStateSchema.extend({
5759
5775
  layout: z.literal("square"),
5760
- spaces: z.record(ids.spaceId, squareSpaceStateSchema),
5776
+ // T220: loose-keyed by string — see comment above.
5777
+ spaces: z.record(z.string(), squareSpaceStateSchema),
5761
5778
  relations: z.array(boardRelationStateSchema),
5762
5779
  containers: z.record(ids.boardContainerId, boardContainerStateSchema),
5763
5780
  edges: z.array(hexEdgeStateSchema),
@@ -5973,11 +5990,11 @@ export const schemas = {
5973
5990
  } as const;
5974
5991
 
5975
5992
  export function createGameStateSchema<
5976
- PhaseNameSchema extends z.ZodTypeAny,
5977
- PublicSchema extends z.ZodTypeAny,
5978
- PrivateSchema extends z.ZodTypeAny,
5979
- HiddenSchema extends z.ZodTypeAny,
5980
- PhasesSchema extends z.ZodTypeAny,
5993
+ PhaseNameSchema extends z.ZodType<unknown>,
5994
+ PublicSchema extends z.ZodType<unknown>,
5995
+ PrivateSchema extends z.ZodType<unknown>,
5996
+ HiddenSchema extends z.ZodType<unknown>,
5997
+ PhasesSchema extends z.ZodType<unknown>,
5981
5998
  >({
5982
5999
  phaseNameSchema,
5983
6000
  publicSchema,
@@ -6178,6 +6195,34 @@ function renderManifestRuntimeSource(legacySource: string): string {
6178
6195
  `} from "@dreamboard/app-sdk/reducer";\n\nconst unknownRecordSchema`,
6179
6196
  `} from "@dreamboard/app-sdk/reducer";\nimport { literals } from "./manifest-literals";\nimport type { PlayerId as PublicPlayerId, TableState as PublicTableState } from "./manifest-types";\n\nconst unknownRecordSchema`,
6180
6197
  )
6198
+ .replaceAll(
6199
+ "literals.playerIds as unknown as readonly PlayerId[]",
6200
+ "literals.playerIds",
6201
+ )
6202
+ .replaceAll(
6203
+ "cardId: cardIdSchema as unknown as z.ZodType<CardId>,",
6204
+ "cardId: assumeManifestSchema<CardId>(cardIdSchema),",
6205
+ )
6206
+ .replaceAll(
6207
+ "deckId: deckIdSchema as unknown as z.ZodType<DeckId>,",
6208
+ "deckId: assumeManifestSchema<DeckId>(deckIdSchema),",
6209
+ )
6210
+ .replaceAll(
6211
+ "handId: handIdSchema as unknown as z.ZodType<HandId>,",
6212
+ "handId: assumeManifestSchema<HandId>(handIdSchema),",
6213
+ )
6214
+ .replace(
6215
+ /export const staticBoards = ([\s\S]*?) as const;\n\nconst baseInitialTable = /,
6216
+ "export const staticBoards = $1 as const satisfies StaticBoards<PublicTableState>;\n\nconst baseInitialTable = ",
6217
+ )
6218
+ .replace(
6219
+ /const baseInitialTable = ([\s\S]*?) as const as unknown as TableState;\nconst baseDeckCardsByZoneId:/,
6220
+ "const baseInitialTable = cloneManifestDefault<PublicTableState>($1);\nconst baseDeckCardsByZoneId:",
6221
+ )
6222
+ .replace(
6223
+ "staticBoards: staticBoards as unknown as StaticBoards<TableState>,",
6224
+ "staticBoards,",
6225
+ )
6181
6226
  .replace(
6182
6227
  `export const manifestContract: ReducerManifestContract<\n RuntimeTableRecord,`,
6183
6228
  `export const manifestContract: ReducerManifestContract<\n PublicTableState,`,
@@ -6188,7 +6233,7 @@ function renderManifestRuntimeSource(legacySource: string): string {
6188
6233
  )
6189
6234
  .replace(
6190
6235
  `const playerIdSchema = markManifestScopedSchema(\n z\n .string()\n .min(1)\n .transform((value) => asPlayerId(value)),\n);`,
6191
- `const playerIdSchema = markManifestScopedSchema(\n z\n .string()\n .min(1)\n .transform((value) => asPlayerId(value)),\n) as unknown as z.ZodType<PublicPlayerId>;`,
6236
+ `const playerIdSchema = assumeManifestSchema<PublicPlayerId>(\n markManifestScopedSchema(\n z\n .string()\n .min(1)\n .transform((value) => asPlayerId(value)),\n ),\n);`,
6192
6237
  );
6193
6238
  }
6194
6239