Skip to content

codegen

Maarten Vroegindeweij edited this page Feb 18, 2026 · 1 revision

@ifc-factory/codegen

Layer 1 — Depends on express-parser. Generates TypeScript code from an EXPRESS AST. Used internally to generate the @ifc-factory/schema package.

Installation

pnpm add @ifc-factory/codegen

CLI Usage

Generate TypeScript from an EXPRESS schema:

npx tsx packages/codegen/src/cli.ts <schema.exp> <output-dir>

Via pnpm workspace:

pnpm build:schema
# Equivalent to: tsx packages/codegen/src/cli.ts schemas/IFC4X3_ADD2.exp packages/schema/src/generated/

What Gets Generated

From the IFC4X3 ADD2 EXPRESS schema, the codegen produces:

Output Count Directory Description
Entity interfaces 876 entities/ One TypeScript interface per IFC entity
Enumerations 243 enums/ String enums matching STEP .VALUES.
Type aliases 132 types/ Primitive type wrappers (IfcLabel = string)
Select types 61 selects/ Union types for EXPRESS SELECTs
Schema metadata 1 metadata/ Runtime attribute info for STEP parsing
Entity registry 1 metadata/ UPPERCASE → PascalCase name mapping
Barrel index 1 . Re-exports everything

Generated Output Structure

packages/schema/src/generated/
├── entities/
│   ├── IfcActionRequest.ts
│   ├── IfcActor.ts
│   ├── IfcActorRole.ts
│   ├── ...
│   └── IfcZone.ts                    (876 files)
├── enums/
│   ├── IfcActionRequestTypeEnum.ts
│   ├── ...
│   └── IfcWorkScheduleTypeEnum.ts    (243 files)
├── types/
│   ├── IfcAbsorbedDoseMeasure.ts
│   ├── ...
│   └── IfcVolumetricFlowRateMeasure.ts (132 files)
├── selects/
│   ├── IfcActorSelect.ts
│   ├── ...
│   └── IfcValue.ts                    (61 files)
├── metadata/
│   ├── schema-metadata.ts             (~12,000 lines)
│   └── entity-registry.ts             (~900 lines)
└── index.ts                           (barrel export)

Emitters

The codegen consists of specialized emitters, each responsible for one category:

Enum Emitter

Converts EXPRESS ENUMERATION to TypeScript string enums:

TYPE IfcWallTypeEnum = ENUMERATION OF
  (MOVABLE, PARAPET, PARTITIONING, SOLIDWALL, STANDARD, USERDEFINED, NOTDEFINED);
END_TYPE;

export enum IfcWallTypeEnum {
  MOVABLE = 'MOVABLE',
  PARAPET = 'PARAPET',
  PARTITIONING = 'PARTITIONING',
  SOLIDWALL = 'SOLIDWALL',
  STANDARD = 'STANDARD',
  USERDEFINED = 'USERDEFINED',
  NOTDEFINED = 'NOTDEFINED',
}

Type Alias Emitter

Converts EXPRESS TYPE aliases to TypeScript types:

TYPE IfcLabel = STRING(255);
END_TYPE;

TYPE IfcLengthMeasure = REAL;
END_TYPE;

export type IfcLabel = string;
export type IfcLengthMeasure = number;

Select Emitter

Converts EXPRESS SELECT to TypeScript union types:

TYPE IfcValue = SELECT
  (IfcMeasureValue, IfcSimpleValue, IfcDerivedMeasureValue);
END_TYPE;

export type IfcValue = IfcMeasureValue | IfcSimpleValue | IfcDerivedMeasureValue;

Entity Emitter

Converts EXPRESS ENTITY to TypeScript interfaces:

ENTITY IfcWall
 SUBTYPE OF (IfcBuildingElement);
  PredefinedType : OPTIONAL IfcWallTypeEnum;
END_ENTITY;

import type { IfcBuildingElement } from './IfcBuildingElement.js';

export interface IfcWall extends IfcBuildingElement {
  PredefinedType: string | null;
}

Design decisions:

  • Root entities (like IfcRoot) get readonly type: string
  • Child entities inherit type without redeclaring it
  • OPTIONAL attributes become T | null
  • Entity references become number (expressID)
  • import type used for cross-references (no runtime cycles)

Metadata Emitter

Generates runtime schema metadata needed for STEP parsing:

export const SCHEMA_METADATA: Record<string, EntityMetadata> = {
  IfcWall: {
    parent: 'IfcBuildingElement',
    abstract: false,
    allAttributes: [
      { name: 'GlobalId', type: 'string', optional: false },
      { name: 'OwnerHistory', type: 'ref', optional: true },
      { name: 'Name', type: 'string', optional: true },
      { name: 'Description', type: 'string', optional: true },
      { name: 'ObjectType', type: 'string', optional: true },
      { name: 'ObjectPlacement', type: 'ref', optional: true },
      { name: 'Representation', type: 'ref', optional: true },
      { name: 'Tag', type: 'string', optional: true },
      { name: 'PredefinedType', type: 'enum', optional: true },
    ],
  },
};

allAttributes includes all inherited attributes in STEP order — essential for correct positional parsing.

Registry Emitter

Generates the UPPERCASE → PascalCase mapping:

export const ENTITY_REGISTRY: Record<string, string> = {
  'IFCWALL': 'IfcWall',
  'IFCDOOR': 'IfcDoor',
  // ...
};

Index Emitter

Generates barrel export files. Uses export type for interfaces to avoid runtime overhead:

export type { IfcWall } from './entities/IfcWall.js';
export { IfcWallTypeEnum } from './enums/IfcWallTypeEnum.js';
export { SCHEMA_METADATA } from './metadata/schema-metadata.js';

Type Mapping

EXPRESS Type TypeScript Type Notes
INTEGER number
REAL number
NUMBER number
BOOLEAN boolean
LOGICAL boolean | null Three-valued: TRUE, FALSE, UNKNOWN
STRING string Width/fixed constraints ignored
STRING(n) string
BINARY Uint8Array
SET OF T T[] Bounds ignored in TypeScript
LIST OF T T[]
BAG OF T T[]
ARRAY OF T T[]
OPTIONAL T T | null
ENUMERATION enum (string values)
SELECT union type
Entity reference number expressID

Programmatic API

import { generateFromSchema } from '@ifc-factory/codegen';
import { parseExpress } from '@ifc-factory/express-parser';
import { readFileSync } from 'node:fs';

const source = readFileSync('schema.exp', 'utf-8');
const schema = parseExpress(source);

await generateFromSchema(schema, './output/');

See Also

Clone this wiki locally