@vampgg/cli 1.0.0-beta.1 → 1.0.0-beta.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/README.md CHANGED
@@ -2,15 +2,147 @@
2
2
 
3
3
  ECS code generator CLI for @vampgg. Parses Bebop (`.bop`) schemas and emits the
4
4
  TypeScript ECS components, factories, deltas, and mutation schemas used by a
5
- `@vampgg` game.
5
+ `@vampgg` game. The binary is `vamp`.
6
6
 
7
- ## Usage
7
+ ```bash
8
+ pnpm add -D @vampgg/cli bebop-tools
9
+ ```
10
+
11
+ > **Prerequisite:** `generate` shells out to `bebopc` (from `bebop-tools`). It must
12
+ > be resolvable via `npx bebopc`, or `vamp generate` exits with a clear error. Use
13
+ > `--skip-bebopc` if you regenerate `src/bebop.ts` separately.
14
+
15
+ ## Commands
16
+
17
+ ### `vamp init [--cwd <dir>]`
18
+
19
+ Scaffolds a new game's schema: creates `schema/` with template `.bop` files
20
+ (`entity.bop`, `actions.bop`, `state.bop`, `tags.bop`) plus `bebop.json` (the
21
+ `bebopc` config) and `vamp.json` (the codegen config). Existing files are left
22
+ untouched, so it's safe to re-run.
23
+
24
+ ### `vamp generate [--cwd <dir>] [--skip-bebopc] [--watch]`
25
+
26
+ The codegen pipeline: emit `mutation.bop` (the `EntityDelta` / `MutationScope` wire
27
+ types) from your `Entity` message → run `bebopc build` (→ `src/bebop.ts`, skip with
28
+ `--skip-bebopc`) → emit the generated TypeScript.
8
29
 
9
30
  ```bash
10
- vamp init # scaffold schema/ and a vamp.json config
11
- vamp generate # parse the .bop schemas and (re)generate TypeScript output
31
+ vamp generate # one-shot regenerate
32
+ vamp generate --watch # re-run on schema changes (debounced; never drops a change)
33
+ ```
34
+
35
+ ## Generated output
36
+
37
+ `vamp generate` runs `bebopc` (which writes `src/bebop.ts`) and then emits **three**
38
+ TypeScript files from a single `vamp.json` `outFile` (default `./src/game.generated.ts`):
39
+
40
+ | File | Depends on | Import it from |
41
+ | -------------------------- | ----------------------------------------------- | --------------------------- |
42
+ | `game.core.generated.ts` | `@vampgg/ecs` only | anywhere (node, web, tests) |
43
+ | `game.worker.generated.ts` | `@vampgg/worker` (imports `cloudflare:workers`) | a Cloudflare Worker only |
44
+ | `game.generated.ts` | re-exports both (barrel) | Worker code / convenience |
45
+
46
+ The `game.core.generated.ts` file (component map, `EntityDelta`, `materialize/merge/accumulateDelta`,
47
+ `createECSOptions`, the `createGame*` system factories) carries **no** Worker
48
+ dependency — import it directly from non-Worker packages to keep `cloudflare:workers`
49
+ out of your dependency graph. `game.worker.generated.ts` holds the `GameECS` durable
50
+ object, runtime, and interest broadcast. The barrel keeps existing `./game.generated`
51
+ imports working. Override the two part-file paths with `coreOutFile` / `workerOutFile`
52
+ in `vamp.json`.
53
+
54
+ ### `env` (durable-object bindings type)
55
+
56
+ The generated `GameECS` exposes an overridable `Env` generic that defaults to
57
+ `Cloudflare.Env` (the `wrangler types` bindings interface). For a non-Worker package
58
+ that has no wrangler types, set `env` in `vamp.json`:
59
+
60
+ ```jsonc
61
+ {
62
+ "outFile": "./src/game.generated.ts",
63
+ "env": "unknown", // or "{}", or a locally-declared bindings type
64
+ }
12
65
  ```
13
66
 
67
+ ### tsconfig / tooling
68
+
69
+ - **`moduleResolution`**: `nodenext` **or** `bundler` both work — the generated
70
+ relative imports carry explicit `.js` extensions.
71
+ - **`verbatimModuleSyntax`**: leave it **off**. The `bebopc`-generated `src/bebop.ts`
72
+ emits a value-style import of the `BebopRecord` type, which `verbatimModuleSyntax`
73
+ rejects. (The `@vampgg/cli`-generated files are themselves compatible; the constraint
74
+ is bebopc's output.)
75
+ - **Formatter / linter**: the generated files are overwritten on every run, so add them
76
+ to your ignore globs, e.g. `src/bebop.ts` and `src/*.generated.ts`.
77
+
78
+ ## Configuration
79
+
80
+ `vamp.json` (`FrameworkConfig`) points the generator at your schema files and output:
81
+
82
+ ```jsonc
83
+ {
84
+ "schemas": {
85
+ "entity": "schema/entity.bop",
86
+ "actions": "schema/actions.bop",
87
+ "state": "schema/state.bop",
88
+ "tags": "schema/tags.bop",
89
+ // "mutation": "schema/mutation.bop" // optional; auto-emitted otherwise
90
+ },
91
+ "outFile": "./src/game.generated.ts",
92
+ // optional: "coreOutFile", "workerOutFile", "bebopConfig", "env"
93
+ }
94
+ ```
95
+
96
+ `bebop.json` — the `bebopc` config `init` writes alongside it:
97
+
98
+ ```jsonc
99
+ {
100
+ "include": ["schema/**/*.bop"],
101
+ "generators": { "ts": { "outFile": "./src/bebop.ts" } },
102
+ }
103
+ ```
104
+
105
+ See `examples/basic/vamp.json` for a complete config.
106
+
107
+ ## End-to-end workflow
108
+
109
+ ```bash
110
+ vamp init # scaffold schema/ + configs
111
+ # edit schema/*.bop to define your entity, actions, state, tags
112
+ vamp generate # emit src/bebop.ts + the generated TypeScript
113
+ ```
114
+
115
+ Then import the generated symbols to wire the world — see
116
+ [`@vampgg/worker`](../../packages/worker) for the Durable Object entry and
117
+ [`examples/basic/`](../../examples/basic) for the full stack (schema → generated
118
+ options → systems → worker → Solid client).
119
+
120
+ ## Programmatic API
121
+
122
+ The generator is usable as a library (`@vampgg/cli`):
123
+
124
+ ```ts
125
+ import { loadVampConfig, loadBebopConfig, generate, generateMutationSchema } from "@vampgg/cli";
126
+
127
+ const vampConfig = loadVampConfig(cwd);
128
+ const bebopConfig = loadBebopConfig(cwd, vampConfig.bebopConfig);
129
+ generateMutationSchema(cwd, vampConfig); // emit mutation.bop
130
+ const { core, worker, barrel } = generate(cwd, bebopConfig, vampConfig); // emit the TS files
131
+ ```
132
+
133
+ Also exported: `parseSchema` / `loadSchemaFromFile` / `loadAndParseSchema`,
134
+ `emitMutationSchema`, `resolveMutationPath`, and the `FrameworkConfig` /
135
+ `BebopConfig` / `ParsedSchema` types.
136
+
137
+ ## Performance
138
+
139
+ `vamp generate` emits the framework's fast path — the archetype-graph ECS, additive
140
+ CRDT deltas, and interest-routed broadcast that the full-stack benchmark measures
141
+ (server frames over 1,024 entities, ~100 µs single-`act` round-trips, 0%-loss
142
+ fan-out to dozens of observers, ~18k entities per Durable Object). See
143
+ [`@vampgg/worker`](../../packages/worker#performance) for the numbers and how to
144
+ reproduce them (`cd examples/basic && pnpm bench`).
145
+
14
146
  ## Development
15
147
 
16
148
  ```bash
@@ -1,6 +1,6 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
- import { dirname, isAbsolute, relative, resolve } from "node:path";
3
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
4
4
  import { parse, printParseErrorCode } from "jsonc-parser";
5
5
  import { BinarySchema } from "bebop";
6
6
  //#region src/config/loader.ts
@@ -350,16 +350,18 @@ export function createECSOptions(createId: () => string): ECSOptions<Entity, Ent
350
350
  }
351
351
  //#endregion
352
352
  //#region src/generators/emit-classes.ts
353
- function emitClasses(tagsType = "number") {
353
+ function emitClasses(tagsType = "number", env = "Cloudflare.Env") {
354
354
  return `/**
355
355
  * App-typed {@link ECSDurableObject}: this schema's \`Actions\`/\`Tags\`/\`Entity\`/
356
- * \`EntityDelta\` are baked in, leaving \`UserSession\`/\`Context\`/\`UpdateArguments\`
357
- * open. Subclass this as your game's durable object.
356
+ * \`EntityDelta\` are baked in, leaving \`UserSession\`/\`Context\`/\`UpdateArguments\`/\`Env\`
357
+ * open (\`Env\` defaults to \`${env}\`, configurable via the \`env\` field in vamp.json).
358
+ * Subclass this as your game's durable object.
358
359
  */
359
360
  export class GameECS<
360
361
  UserSession extends {} = {},
361
362
  Context extends {} = {},
362
363
  UpdateArguments extends Array<unknown> = [],
364
+ Env = ${env},
363
365
  > extends ECSDurableObject<
364
366
  UserSession,
365
367
  Context,
@@ -368,7 +370,7 @@ export class GameECS<
368
370
  ${tagsType},
369
371
  Entity,
370
372
  EntityDelta,
371
- Cloudflare.Env
373
+ Env
372
374
  > {}
373
375
 
374
376
  /** App-typed {@link ECSStorage} over this schema's {@link Entity}. */
@@ -598,6 +600,30 @@ export function createGameBehavior<
598
600
  }
599
601
  //#endregion
600
602
  //#region src/generators/codegen.ts
603
+ const AUTOGEN_HEADER = `// This file is auto-generated by @vampgg/cli. Do not edit manually.`;
604
+ /**
605
+ * Derive a `.core`/`.worker` sibling path from the barrel `outFile`
606
+ * (`./src/game.generated.ts` → `./src/game.core.generated.ts`). Falls back to
607
+ * inserting the part before a plain `.ts`, or appending it, for atypical names.
608
+ */
609
+ function derivePartPath(outFile, part) {
610
+ if (outFile.endsWith(".generated.ts")) return `${outFile.slice(0, -13)}.${part}.generated.ts`;
611
+ if (outFile.endsWith(".ts")) return `${outFile.slice(0, -3)}.${part}.ts`;
612
+ return `${outFile}.${part}.ts`;
613
+ }
614
+ /**
615
+ * Build the relative ESM specifier that imports `toFile` from `fromFile`, with a
616
+ * `.js` extension (nodenext-compatible; maps back to the `.ts` source).
617
+ */
618
+ function relativeImport(fromFile, toFile) {
619
+ let rel = relative(dirname(fromFile), toFile).split("\\").join("/");
620
+ rel = rel.replace(/\.ts$/, ".js");
621
+ return rel.startsWith(".") ? rel : `./${rel}`;
622
+ }
623
+ function writeGenerated(path, sections) {
624
+ mkdirSync(dirname(path), { recursive: true });
625
+ writeFileSync(path, sections.join("\n"), "utf-8");
626
+ }
601
627
  function generate(cwd, config, vampConfig) {
602
628
  const schema = loadAndParseSchema(resolve(cwd, config.generators?.ts?.outFile ?? "./src/bebop.ts"));
603
629
  const entityDef = schema.definitions.get("Entity");
@@ -606,17 +632,17 @@ function generate(cwd, config, vampConfig) {
606
632
  if (!schema.definitions.get("State")) throw new Error("No 'State' definition found in schema");
607
633
  if (!schema.definitions.get("Tags")) throw new Error("No 'Tags' definition found in schema");
608
634
  const bebopImportTypes = collectBebopImportTypes(entityDef, schema);
609
- const bebopImport = "./bebop";
635
+ const bebopImport = "./bebop.js";
610
636
  const helperImports = emitHelperImports(entityDef, schema);
611
- const output = [
612
- `// This file is auto-generated by @vampgg/cli. Do not edit manually.`,
613
- `import type { ECSOptions, MutationBatch, EntitySystem, ArchetypeSystem, Behavior, System, Query, QueryBuilder } from "@vampgg/ecs";`,
637
+ const barrelPath = resolve(cwd, vampConfig.outFile);
638
+ const corePath = resolve(cwd, vampConfig.coreOutFile ?? derivePartPath(vampConfig.outFile, "core"));
639
+ const workerPath = resolve(cwd, vampConfig.workerOutFile ?? derivePartPath(vampConfig.outFile, "worker"));
640
+ const coreSections = [
641
+ AUTOGEN_HEADER,
642
+ `import type { ECSOptions, EntitySystem, ArchetypeSystem, Behavior, System, Query, QueryBuilder } from "@vampgg/ecs";`,
614
643
  `import { createEntitySystem, createArchetypeSystem, createBehavior } from "@vampgg/ecs";`,
615
644
  ...helperImports ? [helperImports] : [],
616
- `import { defineECSRuntime, ECSDurableObject, ECSStorage, type ECSRuntimeConfiguration, type RPCContext } from "@vampgg/worker";`,
617
- `import { createInterestBroadcast, type InterestBroadcastConfig } from "@vampgg/worker/interest";`,
618
645
  `import type { Entity, Actions, Tags${bebopImportTypes} } from "${bebopImport}";`,
619
- `import { MutationScope, type MutationRecord } from "${bebopImport}";`,
620
646
  "",
621
647
  emitComponents(entityDef),
622
648
  "",
@@ -626,21 +652,47 @@ function generate(cwd, config, vampConfig) {
626
652
  "",
627
653
  emitFactory(),
628
654
  "",
629
- emitClasses("Tags"),
655
+ emitSystems(),
656
+ ""
657
+ ];
658
+ const coreFromWorker = relativeImport(workerPath, corePath);
659
+ const workerSections = [
660
+ AUTOGEN_HEADER,
661
+ `import type { MutationBatch } from "@vampgg/ecs";`,
662
+ `import { defineECSRuntime, ECSDurableObject, ECSStorage, type ECSRuntimeConfiguration, type RPCContext } from "@vampgg/worker";`,
663
+ `import { createInterestBroadcast, type InterestBroadcastConfig } from "@vampgg/worker/interest";`,
664
+ `import type { Entity, Actions, Tags } from "${bebopImport}";`,
665
+ `import { MutationScope, type MutationRecord } from "${bebopImport}";`,
666
+ `import type { EntityDelta } from "${coreFromWorker}";`,
667
+ "",
668
+ emitClasses("Tags", vampConfig.env),
630
669
  "",
631
670
  emitGameContext(),
632
671
  "",
633
672
  emitRuntime(),
634
673
  "",
635
- emitSystems(),
636
- "",
637
674
  emitInterest(),
638
675
  ""
639
- ].join("\n");
640
- const outPath = resolve(cwd, vampConfig.outFile);
641
- mkdirSync(dirname(outPath), { recursive: true });
642
- writeFileSync(outPath, output, "utf-8");
643
- return outPath;
676
+ ];
677
+ const coreFromBarrel = relativeImport(barrelPath, corePath);
678
+ const workerFromBarrel = relativeImport(barrelPath, workerPath);
679
+ const barrelSections = [
680
+ AUTOGEN_HEADER,
681
+ `// Barrel over the generated ECS surface. Import the pure core file directly`,
682
+ `// (\`${coreFromBarrel}\`) from non-Worker code to avoid pulling in @vampgg/worker`,
683
+ `// and its \`cloudflare:workers\` import.`,
684
+ `export * from "${coreFromBarrel}";`,
685
+ `export * from "${workerFromBarrel}";`,
686
+ ""
687
+ ];
688
+ writeGenerated(corePath, coreSections);
689
+ writeGenerated(workerPath, workerSections);
690
+ writeGenerated(barrelPath, barrelSections);
691
+ return {
692
+ barrel: barrelPath,
693
+ core: corePath,
694
+ worker: workerPath
695
+ };
644
696
  }
645
697
  /** Candidate custom type names a field references (base, array member, or map value). */
646
698
  function candidateTypeNames(field) {
@@ -953,6 +1005,22 @@ function emitMutationSchema(entity, entityImportPath, userDeltas = /* @__PURE__
953
1005
  //#endregion
954
1006
  //#region src/generators/resolve-imports.ts
955
1007
  /**
1008
+ * Resolve a package specifier to its *symlinked* `node_modules` path rather than
1009
+ * the realpath. `require.resolve` runs `fs.realpathSync` on its result, which under
1010
+ * pnpm points into the version-pinned `.pnpm/<pkg>@<version>/…` store — a path that
1011
+ * breaks the moment the version bumps. Walking the module search dirs and joining
1012
+ * the specifier keeps the stable `node_modules/<pkg>/<subpath>` symlink instead.
1013
+ */
1014
+ function resolveStablePath(req, specifier) {
1015
+ const searchDirs = req.resolve.paths(specifier);
1016
+ if (!searchDirs) return null;
1017
+ for (const dir of searchDirs) {
1018
+ const candidate = join(dir, specifier);
1019
+ if (existsSync(candidate)) return candidate;
1020
+ }
1021
+ return null;
1022
+ }
1023
+ /**
956
1024
  * Extract the import paths from a bebop source file. Bebop imports look like
957
1025
  * `import "../relative/path.bop"` or `import "@scope/pkg/schema/foo.bop"`.
958
1026
  */
@@ -979,6 +1047,8 @@ function resolveBebopImport(specifier, fromDir) {
979
1047
  return existsSync(abs) ? abs : null;
980
1048
  }
981
1049
  const req = createRequire(resolve(fromDir, "noop.js"));
1050
+ const stable = resolveStablePath(req, specifier);
1051
+ if (stable) return stable;
982
1052
  try {
983
1053
  const direct = req.resolve(specifier);
984
1054
  if (existsSync(direct)) return direct;
package/dist/index.d.mts CHANGED
@@ -14,6 +14,25 @@ interface FrameworkConfig {
14
14
  mutation?: string;
15
15
  };
16
16
  outFile: string;
17
+ /**
18
+ * Override path for the generated **pure** ECS file (components, deltas, helpers,
19
+ * factory, systems — depends only on `@vampgg/ecs`). Defaults to a sibling of
20
+ * `outFile` with `.core.generated.ts` in place of `.generated.ts`.
21
+ */
22
+ coreOutFile?: string;
23
+ /**
24
+ * Override path for the generated **worker** file (the `GameECS` durable object,
25
+ * runtime, and interest broadcast — depends on `@vampgg/worker`). Defaults to a
26
+ * sibling of `outFile` with `.worker.generated.ts` in place of `.generated.ts`.
27
+ */
28
+ workerOutFile?: string;
29
+ /**
30
+ * TypeScript type used as the default `Env` generic of the generated `GameECS`
31
+ * durable object. Defaults to `"Cloudflare.Env"` (the wrangler-generated bindings
32
+ * type). Set to `"unknown"`, `"{}"`, or a locally-declared type for non-Worker
33
+ * packages that lack wrangler types.
34
+ */
35
+ env?: string;
17
36
  }
18
37
  interface BebopConfig {
19
38
  include?: string[];
@@ -67,7 +86,16 @@ declare function loadSchemaFromFile(bebopTsPath: string): Uint8Array;
67
86
  declare function loadAndParseSchema(bebopTsPath: string): ParsedSchema;
68
87
  //#endregion
69
88
  //#region src/generators/codegen.d.ts
70
- declare function generate(cwd: string, config: BebopConfig, vampConfig: FrameworkConfig): string;
89
+ /** Absolute paths of the three files `generate` writes. */
90
+ interface GeneratedPaths {
91
+ /** Backward-compatible barrel re-exporting `core` + `worker`. */
92
+ barrel: string;
93
+ /** Pure ECS file (`@vampgg/ecs` only) — safe to import from non-Worker code. */
94
+ core: string;
95
+ /** Worker runtime file (`@vampgg/worker`, imports `cloudflare:workers`). */
96
+ worker: string;
97
+ }
98
+ declare function generate(cwd: string, config: BebopConfig, vampConfig: FrameworkConfig): GeneratedPaths;
71
99
  //#endregion
72
100
  //#region src/generators/parse-bop-source.d.ts
73
101
  /** Recursive classification of a type token (base/array/map). */
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { a as extractMessageBody, c as generate, d as parseSchema, f as loadBebopConfig, i as emitMutationSchema, l as loadAndParseSchema, n as resolveMutationPath, o as parseEntityMessage, p as loadVampConfig, s as parseMessage, t as generateMutationSchema, u as loadSchemaFromFile } from "./generate-mutation-schema-DPVvPX4h.mjs";
1
+ import { a as extractMessageBody, c as generate, d as parseSchema, f as loadBebopConfig, i as emitMutationSchema, l as loadAndParseSchema, n as resolveMutationPath, o as parseEntityMessage, p as loadVampConfig, s as parseMessage, t as generateMutationSchema, u as loadSchemaFromFile } from "./generate-mutation-schema-g3hIkmuv.mjs";
2
2
  export { emitMutationSchema, extractMessageBody, generate, generateMutationSchema, loadAndParseSchema, loadBebopConfig, loadSchemaFromFile, loadVampConfig, parseEntityMessage, parseMessage, parseSchema, resolveMutationPath };
package/dist/vamp.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { c as generate, f as loadBebopConfig, p as loadVampConfig, r as resolveBebopImport, t as generateMutationSchema } from "./generate-mutation-schema-DPVvPX4h.mjs";
2
+ import { c as generate, f as loadBebopConfig, p as loadVampConfig, r as resolveBebopImport, t as generateMutationSchema } from "./generate-mutation-schema-g3hIkmuv.mjs";
3
3
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
4
4
  import { relative, resolve } from "node:path";
5
5
  import { defineCommand, runMain } from "citty";
@@ -94,8 +94,10 @@ const generateCommand = defineCommand({
94
94
  }
95
95
  }
96
96
  console.log("Generating ECS types...");
97
- const outPath = generate(cwd, bebopConfig, vampConfig);
98
- console.log(`Generated ${outPath}`);
97
+ const { core, worker, barrel } = generate(cwd, bebopConfig, vampConfig);
98
+ console.log(`Generated ${core}`);
99
+ console.log(`Generated ${worker}`);
100
+ console.log(`Generated ${barrel}`);
99
101
  return true;
100
102
  };
101
103
  if (!args.watch) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vampgg/cli",
3
- "version": "1.0.0-beta.1",
3
+ "version": "1.0.0-beta.3",
4
4
  "description": "ECS code generator CLI for @vampgg",
5
5
  "homepage": "https://github.com/sammccord/vamp/tree/main/tools/cli#readme",
6
6
  "bugs": {
@@ -40,9 +40,9 @@
40
40
  "typescript": "^5",
41
41
  "vite-plus": "^0.1.24",
42
42
  "vitest": "npm:@voidzero-dev/vite-plus-test@latest",
43
- "@vampgg/ecs": "1.0.0-beta.1",
44
- "@vampgg/utils": "1.0.0-beta.1",
45
- "@vampgg/worker": "1.0.0-beta.1"
43
+ "@vampgg/utils": "1.0.0-beta.2",
44
+ "@vampgg/ecs": "1.0.0-beta.2",
45
+ "@vampgg/worker": "1.0.0-beta.2"
46
46
  },
47
47
  "scripts": {
48
48
  "build": "vp pack",