@lagless/codegen 0.0.33

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 ADDED
@@ -0,0 +1,403 @@
1
+ # @lagless/codegen
2
+
3
+ ## 1. Responsibility & Context
4
+
5
+ Code generator that transforms YAML schema definitions into TypeScript ECS (Entity Component System) classes for Lagless simulations. Reads declarative schema files describing components, singletons, filters, inputs, and player resources, then generates type-safe TypeScript classes with deterministic memory layouts, input registries, core modules, and ECSRunner subclasses. Eliminates boilerplate and ensures consistency across ECS projects.
6
+
7
+ ## 2. Architecture Role
8
+
9
+ **Upstream dependencies:** `@lagless/binary`, `@lagless/core`
10
+ **Downstream consumers:** `circle-sumo-simulation` (uses generated code from `ecs.yaml`)
11
+
12
+ This tool sits outside the runtime dependency graph — it's a build-time/development-time code generator. Generated code depends on `@lagless/core` and `@lagless/binary`, but the generator itself is not imported by applications. Integrates with Nx workspace generators for monorepo workflows.
13
+
14
+ ## 3. Public API
15
+
16
+ ### Parser
17
+
18
+ - **`parseYamlConfig(configContent: string, configPath?: string): { schema: ECSSchema, projectName: string }`** — Parse YAML config string into `ECSSchema` object. `projectName` extracted from YAML `projectName:` field or derived from `configPath` (e.g., `circle-sumo/...` → `CircleSumo`). Throws on invalid schema.
19
+
20
+ - **`parseFieldType(typeStr: string): FieldDefinition`** — Parse field type string (e.g., `"float32"`, `"uint8[16]"`) into `FieldDefinition` with `{ type, isArray, arrayLength? }`. Used internally by schema parser.
21
+
22
+ - **`parseInputFieldType(fieldName: string, fieldType: string): InputFieldDefinition`** — Parse input field with byte length calculation. Returns `{ name, type, isArray, arrayLength?, byteLength }`.
23
+
24
+ - **`getProjectNameFromConfigPath(configPath: string): string`** — Extract project name from file path. Converts kebab-case to PascalCase (e.g., `circle-sumo/ecs.yaml` → `CircleSumo`).
25
+
26
+ ### Generator
27
+
28
+ - **`generateCode(options: GenerateCodeOptions): Promise<void>`** — Main code generation entry point. Generates all TypeScript classes (components, singletons, filters, inputs, player resources, input registry, core module, runner class, barrel export file) from schema.
29
+ - Options: `{ schema, projectName, outputDir, templateDir, fileOperations }`
30
+
31
+ - **`generateBarrelFileContent(schema: ECSSchema, projectName: string): string`** — Generate barrel export file (`index.ts`) content that re-exports all generated classes.
32
+
33
+ ### Template Engine
34
+
35
+ - **`generateFromTemplate(options: TemplateOptions): Promise<void>`** — Render EJS templates from `templateDir` to `outputDir` with provided `data`. Processes `.template` files and `__variable__` filename patterns.
36
+ - Options: `{ templateDir, outputDir, data, fileOperations }`
37
+
38
+ - **`TemplateEngine`** — Class-based template engine. Use `generateFromTemplate()` function for simpler usage.
39
+
40
+ ### Types
41
+
42
+ - **`ECSConfig`** — YAML schema interface with optional fields: `projectName`, `components`, `singletons`, `playerResources`, `filters`, `inputs`
43
+
44
+ - **`GenerateCodeOptions`** — Generator function options: `schema`, `projectName`, `outputDir`, `templateDir`, `fileOperations`
45
+
46
+ - **`FileOperations`** — Abstraction for file I/O operations. Allows generator to work with Node.js `fs` module (CLI) or Nx `Tree` API (workspace generators).
47
+ - Methods: `readFile`, `writeFile`, `joinPath`, `exists`, `readDir?`, `isDirectory?`
48
+
49
+ ## 4. Preconditions
50
+
51
+ - **YAML schema file exists** at the specified config path (e.g., `src/lib/schema/ecs.yaml`)
52
+ - **Schema must contain at least one of:** `components` or `singletons` (cannot be empty)
53
+ - **Template files exist** in the specified template directory (default: `tools/codegen/files/`)
54
+ - **Output directory is writable** (generator creates it if missing)
55
+ - **For Nx generator:** Nx workspace initialized with `@lagless/codegen` in dependencies
56
+
57
+ ## 5. Postconditions
58
+
59
+ - **Generated TypeScript files** in the output directory (default: `<config_dir>/../code-gen/`)
60
+ - **Component classes** with SoA memory layout for each component in schema
61
+ - **Singleton classes** with typed array accessors for singleton fields
62
+ - **Filter classes** with bitmask matching logic for entity iteration
63
+ - **Input classes** with binary serialization/deserialization methods
64
+ - **PlayerResource classes** with per-player state management
65
+ - **Input registry** mapping input IDs to input classes
66
+ - **Core module** (`<ProjectName>.core.ts`) with `getECSSchema()` function
67
+ - **Runner class** (`<ProjectName>.runner.ts`) extending `ECSRunner` with schema integration
68
+ - **Barrel export** (`index.ts`) re-exporting all generated classes
69
+ - All files formatted and ready for use in ECS simulation
70
+
71
+ ## 6. Invariants & Constraints
72
+
73
+ - **Component IDs are powers of 2** — `id = 2^n` where `n` is the component's index in the YAML file. Required for bitmask filtering (e.g., first component = 1, second = 2, third = 4, etc.).
74
+ - **Input IDs start at 1** — Sequential integers (not powers of 2). First input = 1, second = 2, etc.
75
+ - **Field type strings** must match `@lagless/binary` supported types: `uint8`, `uint16`, `uint32`, `int8`, `int16`, `int32`, `float32`, `float64`
76
+ - **Array syntax:** `type[length]` where `length > 0` (e.g., `uint8[16]` for 16-byte UUID)
77
+ - **Project name** must be valid TypeScript identifier (PascalCase recommended)
78
+ - **Filter include/exclude** component names must exist in the `components:` section
79
+ - **Template files** use EJS syntax (`<%= variable %>`, `<% if (condition) { %>...`)
80
+ - **Output is deterministic** — Same YAML input always produces identical TypeScript output (important for version control)
81
+
82
+ ## 7. Safety Notes (AI Agent)
83
+
84
+ ### DO NOT
85
+
86
+ - **DO NOT** modify component ID calculation logic — changing from powers of 2 breaks bitmask filtering in `@lagless/core`
87
+ - **DO NOT** add unsupported field types — only types in `@lagless/binary` `typeToArrayConstructor` are valid
88
+ - **DO NOT** change template file structure without updating `generateCode()` logic — generator assumes specific template directory layout (`component/`, `singleton/`, `filter/`, etc.)
89
+ - **DO NOT** generate code directly into `src/` — always use a separate output directory (e.g., `code-gen/`) to distinguish generated from hand-written code
90
+ - **DO NOT** edit generated files manually — changes will be overwritten on next generation. Modify the YAML schema or templates instead.
91
+
92
+ ### Common Mistakes
93
+
94
+ - **Forgetting to regenerate** after YAML changes — run `nx g @lagless/codegen:ecs --configPath <path>` after editing schema
95
+ - **Invalid component references in filters** — filter `include`/`exclude` must reference components defined in `components:` section
96
+ - **Zero or negative array lengths** — `uint8[0]` and `uint8[-1]` are invalid (parser throws error)
97
+ - **Missing projectName** — Either set `projectName:` in YAML or ensure config path contains project directory for auto-detection
98
+ - **Template syntax errors** — EJS templates use `<%= %>` for output and `<% %>` for logic; JavaScript syntax must be valid
99
+
100
+ ## 8. Usage Examples
101
+
102
+ ### CLI Usage
103
+
104
+ ```bash
105
+ # Generate code from YAML schema
106
+ npx lagless-codegen -c path/to/ecs.yaml
107
+
108
+ # With custom output directory
109
+ npx lagless-codegen -c path/to/ecs.yaml -o src/generated
110
+
111
+ # With custom templates
112
+ npx lagless-codegen -c path/to/ecs.yaml -t my-templates/
113
+ ```
114
+
115
+ ### Nx Generator Usage
116
+
117
+ ```bash
118
+ # In Nx monorepo
119
+ nx g @lagless/codegen:ecs --configPath circle-sumo/circle-sumo-simulation/src/lib/schema/ecs.yaml
120
+ ```
121
+
122
+ ### Example YAML Schema
123
+
124
+ ```yaml
125
+ # src/lib/schema/ecs.yaml
126
+ projectName: MyGame
127
+ components:
128
+ Transform2d:
129
+ positionX: float32
130
+ positionY: float32
131
+ rotation: float32
132
+ Health:
133
+ current: float32
134
+ max: float32
135
+ singletons:
136
+ GameState:
137
+ startedAtTick: uint32
138
+ finishedAtTick: uint32
139
+ playerResources:
140
+ PlayerResource:
141
+ id: uint8[16]
142
+ score: uint32
143
+ inputs:
144
+ Move:
145
+ direction: float32
146
+ speed: float32
147
+ Shoot:
148
+ angle: float32
149
+ filters:
150
+ AliveFilter:
151
+ include:
152
+ - Transform2d
153
+ - Health
154
+ ```
155
+
156
+ ### Programmatic Usage
157
+
158
+ ```typescript
159
+ import { parseYamlConfig, generateCode } from '@lagless/codegen';
160
+ import * as fs from 'fs';
161
+ import * as path from 'path';
162
+
163
+ const yamlContent = fs.readFileSync('ecs.yaml', 'utf-8');
164
+ const { schema, projectName } = parseYamlConfig(yamlContent, 'ecs.yaml');
165
+
166
+ await generateCode({
167
+ schema,
168
+ projectName,
169
+ outputDir: './generated',
170
+ templateDir: './node_modules/@lagless/codegen/files',
171
+ fileOperations: {
172
+ readFile: (p) => fs.readFileSync(p, 'utf-8'),
173
+ writeFile: (p, content) => {
174
+ fs.mkdirSync(path.dirname(p), { recursive: true });
175
+ fs.writeFileSync(p, content, 'utf-8');
176
+ },
177
+ joinPath: (...segments) => path.join(...segments),
178
+ exists: (p) => fs.existsSync(p),
179
+ readDir: (p) => fs.readdirSync(p),
180
+ isDirectory: (p) => fs.statSync(p).isDirectory(),
181
+ },
182
+ });
183
+
184
+ console.log('Generated ECS code!');
185
+ ```
186
+
187
+ ## 9. Testing Guidance
188
+
189
+ No test suite currently exists for this module. When adding tests, consider:
190
+
191
+ - **Parser tests:** Validate YAML parsing with valid/invalid schemas, component ID generation, field type parsing, project name extraction
192
+ - **Generator tests:** Verify generated code compiles and matches expected output (snapshot testing)
193
+ - **Template tests:** Check EJS templates render correctly with sample data
194
+ - **Integration tests:** Generate code from real schema, compile with TypeScript, instantiate classes
195
+ - **Error handling:** Test invalid YAML syntax, missing required fields, unsupported types, circular dependencies in filters
196
+ - Use `@lagless/core` test utilities to verify generated classes integrate correctly with ECS runtime
197
+
198
+ ## 10. Change Checklist
199
+
200
+ When modifying this module:
201
+
202
+ 1. **Changing component ID generation:** Update `parseYamlConfig()` logic AND document migration path for existing projects (component ID changes break save files)
203
+ 2. **Adding new field types:** Update `parseFieldType()` AND ensure type is supported in `@lagless/binary` `typeToArrayConstructor`
204
+ 3. **Modifying templates:** Test generated code compiles with TypeScript AND passes `@lagless/core` runtime checks
205
+ 4. **Changing template directory structure:** Update `generateCode()` function to match new layout
206
+ 5. **Adding new YAML sections:** Update `ECSConfig` interface, parser logic, generator function, AND add corresponding template directory
207
+ 6. **Modifying CLI flags:** Update `commander` options in `cli.ts` AND update documentation
208
+ 7. **Changing output file names:** Update `generateBarrelFileContent()` to match new naming scheme
209
+ 8. **Breaking changes:** Bump version and document migration guide for users
210
+
211
+ ## 11. Integration Notes
212
+
213
+ ### With Nx Workspace
214
+
215
+ 1. Add `@lagless/codegen` to workspace dependencies
216
+ 2. Create `generators.json` in project root:
217
+ ```json
218
+ {
219
+ "generators": {
220
+ "ecs": {
221
+ "factory": "./src/nx-generator",
222
+ "schema": "./schema.json",
223
+ "description": "Generate ECS code from YAML"
224
+ }
225
+ }
226
+ }
227
+ ```
228
+ 3. Run generator: `nx g @lagless/codegen:ecs --configPath <path>`
229
+
230
+ ### With ECS Projects
231
+
232
+ 1. Create YAML schema file (e.g., `src/lib/schema/ecs.yaml`)
233
+ 2. Run codegen to generate classes in `src/lib/schema/code-gen/`
234
+ 3. Import generated classes:
235
+ ```typescript
236
+ import { Transform2d, Health, AliveFilter, MyGameRunner } from './schema/code-gen';
237
+ ```
238
+ 4. Extend runner class:
239
+ ```typescript
240
+ export class GameRunner extends MyGameRunner {
241
+ constructor() {
242
+ super({ /* ECSConfig */ });
243
+ }
244
+ }
245
+ ```
246
+ 5. Regenerate after schema changes: `nx g @lagless/codegen:ecs --configPath ...`
247
+
248
+ ### Generated File Structure
249
+
250
+ ```
251
+ code-gen/
252
+ ├── index.ts # Barrel export (re-exports all classes)
253
+ ├── Transform2d.ts # Component class
254
+ ├── Health.ts # Component class
255
+ ├── GameState.ts # Singleton class
256
+ ├── PlayerResource.ts # PlayerResource class
257
+ ├── Move.ts # Input class
258
+ ├── Shoot.ts # Input class
259
+ ├── AliveFilter.ts # Filter class
260
+ ├── MyGameInputRegistry.ts # Input registry (maps input IDs to classes)
261
+ ├── MyGame.core.ts # Core module (getECSSchema() function)
262
+ └── MyGame.runner.ts # Runner class (extends ECSRunner)
263
+ ```
264
+
265
+ ## 12. Appendix
266
+
267
+ ### YAML Schema Format Reference
268
+
269
+ ```yaml
270
+ # Optional: Project name (PascalCase). If omitted, derived from config path.
271
+ projectName: MyGame
272
+
273
+ # Components: Define entity data structures with typed fields
274
+ components:
275
+ ComponentName:
276
+ fieldName: fieldType
277
+ # Supported types: uint8, uint16, uint32, int8, int16, int32, float32, float64
278
+ # Arrays: type[length] (e.g., uint8[16] for 16-byte array)
279
+
280
+ # Singletons: Global state (one instance per simulation)
281
+ singletons:
282
+ SingletonName:
283
+ fieldName: fieldType
284
+
285
+ # Player Resources: Per-player state (one instance per player)
286
+ playerResources:
287
+ ResourceName:
288
+ fieldName: fieldType
289
+
290
+ # Inputs: Player commands (serialized over network)
291
+ inputs:
292
+ InputName:
293
+ fieldName: fieldType
294
+
295
+ # Filters: Entity iterators based on component membership
296
+ filters:
297
+ FilterName:
298
+ include:
299
+ - ComponentName1
300
+ - ComponentName2
301
+ exclude:
302
+ - ComponentName3
303
+ ```
304
+
305
+ ### Component ID Assignment
306
+
307
+ Components receive IDs as powers of 2 based on their order in the YAML file:
308
+
309
+ | Index | Component | ID (decimal) | ID (binary) |
310
+ |-------|-----------|--------------|-------------|
311
+ | 0 | First | 1 | 0b00001 |
312
+ | 1 | Second | 2 | 0b00010 |
313
+ | 2 | Third | 4 | 0b00100 |
314
+ | 3 | Fourth | 8 | 0b01000 |
315
+ | 4 | Fifth | 16 | 0b10000 |
316
+
317
+ **Why powers of 2:** Entity bitmasks store component membership as a single integer. Filter matching uses bitwise AND operations:
318
+ ```typescript
319
+ entity.bitmask & filter.includeMask === filter.includeMask // Has all required components
320
+ entity.bitmask & filter.excludeMask === 0 // Has no excluded components
321
+ ```
322
+
323
+ ### Supported Field Types
324
+
325
+ | Type | Bytes | Range/Precision |
326
+ |-----------|-------|-------------------------------------|
327
+ | `uint8` | 1 | 0 to 255 |
328
+ | `uint16` | 2 | 0 to 65,535 |
329
+ | `uint32` | 4 | 0 to 4,294,967,295 |
330
+ | `int8` | 1 | -128 to 127 |
331
+ | `int16` | 2 | -32,768 to 32,767 |
332
+ | `int32` | 4 | -2,147,483,648 to 2,147,483,647 |
333
+ | `float32` | 4 | IEEE 754 single precision |
334
+ | `float64` | 8 | IEEE 754 double precision |
335
+
336
+ **Array syntax:** Append `[length]` for fixed-size arrays (e.g., `uint8[16]` = 16-byte array).
337
+
338
+ ### Template Directory Structure
339
+
340
+ ```
341
+ files/
342
+ ├── component/
343
+ │ └── __name__.ts.template # Component class template
344
+ ├── singleton/
345
+ │ └── __name__.ts.template # Singleton class template
346
+ ├── playerResource/
347
+ │ └── __name__.ts.template # PlayerResource class template
348
+ ├── filter/
349
+ │ └── __name__.ts.template # Filter class template
350
+ ├── input/
351
+ │ └── __name__.ts.template # Input class template
352
+ ├── input-registry/
353
+ │ └── __projectName__InputRegistry.ts.template
354
+ ├── core/
355
+ │ └── __projectName__.core.ts.template
356
+ └── runner/
357
+ └── __projectName__.runner.ts.template
358
+ ```
359
+
360
+ Templates use EJS syntax:
361
+ - `<%= variable %>` — Output value
362
+ - `<% if (condition) { %>...<% } %>` — Control flow
363
+ - `__name__`, `__projectName__` — Filename placeholders replaced during generation
364
+
365
+ ### CLI Options
366
+
367
+ | Flag | Alias | Required | Description |
368
+ |------|-------|----------|-------------|
369
+ | `--config <path>` | `-c` | Yes | Path to YAML configuration file |
370
+ | `--output <path>` | `-o` | No | Output directory (default: `<config_dir>/../code-gen`) |
371
+ | `--templates <path>` | `-t` | No | Templates directory (default: built-in templates) |
372
+
373
+ ### Example Generated Component Class
374
+
375
+ Input YAML:
376
+ ```yaml
377
+ components:
378
+ Health:
379
+ current: float32
380
+ max: float32
381
+ ```
382
+
383
+ Generated TypeScript (simplified):
384
+ ```typescript
385
+ import { ComponentBase } from '@lagless/core';
386
+
387
+ export class Health extends ComponentBase {
388
+ static readonly id = 4; // Power of 2 based on index
389
+ static readonly fields = {
390
+ current: { type: 'float32', isArray: false },
391
+ max: { type: 'float32', isArray: false },
392
+ };
393
+
394
+ unsafe = {
395
+ current: new Float32Array(this.mem.arrayBuffer, this.byteOffset, this.maxEntities),
396
+ max: new Float32Array(this.mem.arrayBuffer, this.byteOffset + 4 * this.maxEntities, this.maxEntities),
397
+ };
398
+
399
+ // ...additional methods
400
+ }
401
+ ```
402
+
403
+ All field accessors use SoA (Struct of Arrays) layout backed by the simulation's single ArrayBuffer for deterministic snapshots and rollback.
package/dist/cli.js ADDED
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ import { program } from 'commander';
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+ import { parseYamlConfig } from './parser.js';
6
+ import { generateCode } from './generator.js';
7
+ import { DIRNAME } from './dirname.js';
8
+ program.name('lagless-codegen').description('Generate ECS code from YAML configuration').version('1.0.0').requiredOption('-c, --config <path>', 'Path to YAML configuration file').option('-o, --output <path>', 'Output directory (default: config_dir/../code-gen)').option('-t, --templates <path>', 'Templates directory').action(async (options)=>{
9
+ try {
10
+ await generateFromConfig(options);
11
+ } catch (error) {
12
+ console.error('Error:', error instanceof Error ? error.message : error);
13
+ process.exit(1);
14
+ }
15
+ });
16
+ async function generateFromConfig(options) {
17
+ const { config: configPath, output: outputPath, templates: templatesPath } = options;
18
+ // Validate config file exists
19
+ if (!fs.existsSync(configPath)) {
20
+ throw new Error(`Config file not found: ${configPath}`);
21
+ }
22
+ // Read and parse config
23
+ const configContent = fs.readFileSync(configPath, 'utf-8');
24
+ if (!configContent.trim()) {
25
+ throw new Error(`Config file is empty: ${configPath}`);
26
+ }
27
+ const { schema, projectName } = parseYamlConfig(configContent, configPath);
28
+ // Determine output directory
29
+ const outputDir = outputPath || path.join(path.dirname(configPath), '..', 'code-gen');
30
+ // Determine templates directory
31
+ const templateDir = templatesPath || path.join(DIRNAME, '..', 'files');
32
+ console.log(`Generating ECS code...`);
33
+ console.log(`Config: ${configPath}`);
34
+ console.log(`Output: ${outputDir}`);
35
+ console.log(`Templates: ${templateDir}`);
36
+ console.log(`Project: ${projectName}`);
37
+ // Create file operations for Node.js filesystem
38
+ const fileOperations = {
39
+ readFile: (filePath)=>{
40
+ try {
41
+ return fs.readFileSync(filePath, 'utf-8');
42
+ } catch (e) {
43
+ return '';
44
+ }
45
+ },
46
+ writeFile: (filePath, content)=>{
47
+ const dir = path.dirname(filePath);
48
+ if (!fs.existsSync(dir)) {
49
+ fs.mkdirSync(dir, {
50
+ recursive: true
51
+ });
52
+ }
53
+ fs.writeFileSync(filePath, content, 'utf-8');
54
+ },
55
+ joinPath: (...segments)=>path.join(...segments),
56
+ exists: (filePath)=>fs.existsSync(filePath),
57
+ readDir: (dirPath)=>{
58
+ try {
59
+ return fs.readdirSync(dirPath);
60
+ } catch (e) {
61
+ return [];
62
+ }
63
+ },
64
+ isDirectory: (filePath)=>{
65
+ try {
66
+ return fs.statSync(filePath).isDirectory();
67
+ } catch (e) {
68
+ return false;
69
+ }
70
+ }
71
+ };
72
+ // Generate code
73
+ await generateCode({
74
+ schema,
75
+ projectName,
76
+ outputDir,
77
+ templateDir,
78
+ fileOperations
79
+ });
80
+ console.log('ECS code generation complete!');
81
+ console.log(`Generated ${schema.components.length} components and ${schema.singletons.length} singletons.`);
82
+ console.log(`Project name: ${projectName}`);
83
+ console.log(`Output written to: ${outputDir}`);
84
+ }
85
+ // Handle unhandled promise rejections
86
+ process.on('unhandledRejection', (reason, promise)=>{
87
+ console.error('Unhandled Rejection at:', promise, 'reason:', reason);
88
+ process.exit(1);
89
+ });
90
+ program.parse();
91
+
92
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { program } from 'commander';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { parseYamlConfig } from './parser.js';\nimport { generateCode } from './generator.js';\nimport { FileOperations } from './template-engine.js';\nimport { DIRNAME } from './dirname.js';\n\ninterface CliOptions {\n config: string;\n output?: string;\n templates?: string;\n}\n\nprogram\n .name('lagless-codegen')\n .description('Generate ECS code from YAML configuration')\n .version('1.0.0')\n .requiredOption('-c, --config <path>', 'Path to YAML configuration file')\n .option('-o, --output <path>', 'Output directory (default: config_dir/../code-gen)')\n .option('-t, --templates <path>', 'Templates directory')\n .action(async (options: CliOptions) => {\n try {\n await generateFromConfig(options);\n } catch (error) {\n console.error('Error:', error instanceof Error ? error.message : error);\n process.exit(1);\n }\n });\n\nasync function generateFromConfig(options: CliOptions): Promise<void> {\n const { config: configPath, output: outputPath, templates: templatesPath } = options;\n\n // Validate config file exists\n if (!fs.existsSync(configPath)) {\n throw new Error(`Config file not found: ${configPath}`);\n }\n\n // Read and parse config\n const configContent = fs.readFileSync(configPath, 'utf-8');\n if (!configContent.trim()) {\n throw new Error(`Config file is empty: ${configPath}`);\n }\n\n const { schema, projectName } = parseYamlConfig(configContent, configPath);\n\n // Determine output directory\n const outputDir = outputPath || path.join(path.dirname(configPath), '..', 'code-gen');\n\n // Determine templates directory\n const templateDir = templatesPath || path.join(DIRNAME, '..', 'files');\n\n console.log(`Generating ECS code...`);\n console.log(`Config: ${configPath}`);\n console.log(`Output: ${outputDir}`);\n console.log(`Templates: ${templateDir}`);\n console.log(`Project: ${projectName}`);\n\n // Create file operations for Node.js filesystem\n const fileOperations: FileOperations = {\n readFile: (filePath: string) => {\n try {\n return fs.readFileSync(filePath, 'utf-8');\n } catch {\n return '';\n }\n },\n writeFile: (filePath: string, content: string) => {\n const dir = path.dirname(filePath);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n fs.writeFileSync(filePath, content, 'utf-8');\n },\n joinPath: (...segments: string[]) => path.join(...segments),\n exists: (filePath: string) => fs.existsSync(filePath),\n readDir: (dirPath: string) => {\n try {\n return fs.readdirSync(dirPath);\n } catch {\n return [];\n }\n },\n isDirectory: (filePath: string) => {\n try {\n return fs.statSync(filePath).isDirectory();\n } catch {\n return false;\n }\n },\n };\n\n // Generate code\n await generateCode({\n schema,\n projectName,\n outputDir,\n templateDir,\n fileOperations,\n });\n\n console.log('ECS code generation complete!');\n console.log(`Generated ${schema.components.length} components and ${schema.singletons.length} singletons.`);\n console.log(`Project name: ${projectName}`);\n console.log(`Output written to: ${outputDir}`);\n}\n\n// Handle unhandled promise rejections\nprocess.on('unhandledRejection', (reason, promise) => {\n console.error('Unhandled Rejection at:', promise, 'reason:', reason);\n process.exit(1);\n});\n\nprogram.parse();\n"],"names":["program","fs","path","parseYamlConfig","generateCode","DIRNAME","name","description","version","requiredOption","option","action","options","generateFromConfig","error","console","Error","message","process","exit","config","configPath","output","outputPath","templates","templatesPath","existsSync","configContent","readFileSync","trim","schema","projectName","outputDir","join","dirname","templateDir","log","fileOperations","readFile","filePath","writeFile","content","dir","mkdirSync","recursive","writeFileSync","joinPath","segments","exists","readDir","dirPath","readdirSync","isDirectory","statSync","components","length","singletons","on","reason","promise","parse"],"rangeMappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import path from 'path';
2
+ import { fileURLToPath } from 'url';
3
+ export const DIRNAME = // In CJS __dirname exists; in ESM we compute it from import.meta.url
4
+ typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
5
+
6
+ //# sourceMappingURL=dirname.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/dirname.ts"],"sourcesContent":["import path from 'path';\nimport { fileURLToPath } from 'url';\n\nexport const DIRNAME =\n // In CJS __dirname exists; in ESM we compute it from import.meta.url\n typeof __dirname !== 'undefined'\n ? __dirname\n : path.dirname(fileURLToPath(import.meta.url));\n"],"names":["path","fileURLToPath","DIRNAME","__dirname","dirname","url"],"rangeMappings":";;;","mappings":"AAAA,OAAOA,UAAU,OAAO;AACxB,SAASC,aAAa,QAAQ,MAAM;AAEpC,OAAO,MAAMC,UACX,qEAAqE;AACrE,OAAOC,cAAc,cACjBA,YACAH,KAAKI,OAAO,CAACH,cAAc,YAAYI,GAAG,GAAG"}
@@ -0,0 +1,156 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ import { generateFromTemplate as processTemplate } from './template-engine.js';
3
+ import { FieldTypeReverse, getTypeSizeBytes, typedArrayConstructors, typeToArrayConstructor } from '@lagless/binary';
4
+ export function generateBarrelFileContent(schema, projectName) {
5
+ let content = '// Generated by @lagless/codegen. Do not edit manually.\n\n';
6
+ // Export components
7
+ schema.components.forEach((component)=>{
8
+ content += `export * from './${component.name}.js';\n`;
9
+ });
10
+ // Export singletons
11
+ schema.singletons.forEach((singleton)=>{
12
+ content += `export * from './${singleton.name}.js';\n`;
13
+ });
14
+ // Export player resources
15
+ schema.playerResources.forEach((playerResource)=>{
16
+ content += `export * from './${playerResource.name}.js';\n`;
17
+ });
18
+ // Export filters
19
+ schema.filters.forEach((filter)=>{
20
+ content += `export * from './${filter.name}.js';\n`;
21
+ });
22
+ // Export inputs
23
+ schema.inputs.forEach((input)=>{
24
+ content += `export * from './${input.name}.js';\n`;
25
+ });
26
+ // Export input registry
27
+ content += `export * from './${projectName}InputRegistry.js';\n`;
28
+ // Export core
29
+ content += `export * from './${projectName}.core.js';\n`;
30
+ // Export runner
31
+ content += `export * from './${projectName}.runner.js';\n`;
32
+ return content;
33
+ }
34
+ export async function generateCode(options) {
35
+ const { schema, projectName, outputDir, templateDir, fileOperations } = options;
36
+ const { writeFile, joinPath, exists } = fileOperations;
37
+ // Ensure output directory exists
38
+ if (!exists(outputDir)) {
39
+ writeFile(joinPath(outputDir, '.gitkeep'), '');
40
+ }
41
+ // Generate component classes
42
+ for (const component of schema.components){
43
+ await processTemplate({
44
+ templateDir: joinPath(templateDir, 'component'),
45
+ outputDir,
46
+ data: _extends({}, component, {
47
+ component,
48
+ projectName,
49
+ typeToArrayConstructor,
50
+ getFieldByteSize: (field)=>{
51
+ const baseSize = getTypeSizeBytes(field.type);
52
+ return field.isArray ? baseSize * field.arrayLength : baseSize;
53
+ }
54
+ }),
55
+ fileOperations
56
+ });
57
+ }
58
+ // Generate singleton classes
59
+ for (const singleton of schema.singletons){
60
+ await processTemplate({
61
+ templateDir: joinPath(templateDir, 'singleton'),
62
+ outputDir,
63
+ data: _extends({}, singleton, {
64
+ singleton,
65
+ projectName,
66
+ typeToArrayConstructor
67
+ }),
68
+ fileOperations
69
+ });
70
+ }
71
+ // Generate playerResource classes
72
+ for (const playerResource of schema.playerResources){
73
+ await processTemplate({
74
+ templateDir: joinPath(templateDir, 'playerResource'),
75
+ outputDir,
76
+ data: _extends({}, playerResource, {
77
+ playerResource,
78
+ projectName,
79
+ typeToArrayConstructor
80
+ }),
81
+ fileOperations
82
+ });
83
+ }
84
+ // Generate Filter classes
85
+ for (const filter of schema.filters){
86
+ const componentsImports = [
87
+ ...filter.include,
88
+ ...filter.exclude
89
+ ].map((c)=>c.name);
90
+ const includeMask = filter.include.reduce((acc, component)=>acc | component.id, 0);
91
+ const excludeMask = filter.exclude.reduce((acc, component)=>acc | component.id, 0);
92
+ await processTemplate({
93
+ templateDir: joinPath(templateDir, 'filter'),
94
+ outputDir,
95
+ data: {
96
+ filter,
97
+ includeMask,
98
+ excludeMask,
99
+ componentsImports,
100
+ name: filter.name,
101
+ projectName
102
+ },
103
+ fileOperations
104
+ });
105
+ }
106
+ // Generate Input classes
107
+ for (const input of schema.inputs){
108
+ await processTemplate({
109
+ templateDir: joinPath(templateDir, 'input'),
110
+ outputDir,
111
+ data: _extends({}, input, {
112
+ input,
113
+ projectName,
114
+ FieldTypeReverse,
115
+ typedArrayConstructors
116
+ }),
117
+ fileOperations
118
+ });
119
+ }
120
+ // Generate input registry
121
+ await processTemplate({
122
+ templateDir: joinPath(templateDir, 'input-registry'),
123
+ outputDir,
124
+ data: {
125
+ projectName,
126
+ inputs: schema.inputs,
127
+ schema
128
+ },
129
+ fileOperations
130
+ });
131
+ // Generate ECSCore class
132
+ await processTemplate({
133
+ templateDir: joinPath(templateDir, 'core'),
134
+ outputDir,
135
+ data: {
136
+ projectName,
137
+ schema
138
+ },
139
+ fileOperations
140
+ });
141
+ // Generate Runner class
142
+ await processTemplate({
143
+ templateDir: joinPath(templateDir, 'runner'),
144
+ outputDir,
145
+ data: {
146
+ projectName,
147
+ schema
148
+ },
149
+ fileOperations
150
+ });
151
+ // Generate barrel file
152
+ const barrelContent = generateBarrelFileContent(schema, projectName);
153
+ writeFile(joinPath(outputDir, 'index.ts'), barrelContent);
154
+ }
155
+
156
+ //# sourceMappingURL=generator.js.map