@ifc-lite/codegen 1.15.2 → 1.15.4

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,8 +1,13 @@
1
1
  {
2
2
  "name": "@ifc-lite/codegen",
3
- "version": "1.15.2",
3
+ "version": "1.15.4",
4
4
  "description": "TypeScript code generator from IFC EXPRESS schemas",
5
5
  "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/LTplus-AG/ifc-lite.git",
9
+ "directory": "packages/codegen"
10
+ },
6
11
  "main": "dist/index.js",
7
12
  "types": "dist/index.d.ts",
8
13
  "keywords": [
@@ -14,19 +19,20 @@
14
19
  "author": "",
15
20
  "license": "MIT",
16
21
  "dependencies": {
17
- "commander": "^11.1.0"
22
+ "commander": "^15.0.0",
23
+ "@ifc-lite/data": "^2.1.0"
18
24
  },
19
25
  "devDependencies": {
20
- "@types/node": "^20.10.0",
21
- "typescript": "^5.3.3",
22
- "vitest": "^1.0.4"
26
+ "@types/node": "^25.9.3",
27
+ "typescript": "^6.0.3",
28
+ "vitest": "^4.1.0"
23
29
  },
24
30
  "scripts": {
25
31
  "build": "pnpm exec tsc",
26
32
  "dev": "pnpm exec tsc --watch",
27
33
  "test": "vitest run",
28
34
  "generate": "node dist/cli.js",
29
- "generate:ifc4": "node dist/cli.js schemas/IFC4.exp -o generated/ifc4",
35
+ "generate:ifc4": "node dist/cli.js schemas/IFC4_ADD2_TC1.exp -o generated/ifc4",
30
36
  "generate:ifc4x3": "node dist/cli.js schemas/IFC4X3.exp -o generated/ifc4x3"
31
37
  }
32
38
  }
@@ -403,8 +403,12 @@ export function getAllAttributes(
403
403
  // then reverse to get STEP order (parent-first / root → leaf)
404
404
  const levels: AttributeDefinition[][] = [];
405
405
 
406
+ // Guard against cyclic SUBTYPE OF chains in malformed schemas
407
+ const seen = new Set<string>();
406
408
  let current: EntityDefinition | undefined = entity;
407
- while (current) {
409
+ while (current && !seen.has(current.name)) {
410
+ seen.add(current.name);
411
+
408
412
  if (current.attributes.length > 0) {
409
413
  levels.push(current.attributes);
410
414
  }
@@ -435,8 +439,11 @@ export function getInheritanceChain(
435
439
  ): string[] {
436
440
  const chain: string[] = [];
437
441
 
442
+ // Guard against cyclic SUBTYPE OF chains in malformed schemas
443
+ const seen = new Set<string>();
438
444
  let current: EntityDefinition | undefined = entity;
439
- while (current) {
445
+ while (current && !seen.has(current.name)) {
446
+ seen.add(current.name);
440
447
  chain.unshift(current.name); // Add to front
441
448
 
442
449
  if (current.supertype) {
@@ -5,17 +5,21 @@
5
5
  /**
6
6
  * Serialization Generator
7
7
  *
8
- * Generates TypeScript code for serializing IFC entities to STEP format.
8
+ * Emits the per-schema STEP serializer bundle file. The serialization LOGIC is
9
+ * schema-agnostic and lives once in `@ifc-lite/data`; this generator emits only
10
+ * a thin binding that re-exports those primitives and binds the bundle's
11
+ * `SCHEMA_REGISTRY` to the registry-coupled helpers. Keeping the logic in a
12
+ * single place means the generated copies can never drift from the runtime.
9
13
  */
10
14
 
11
- import type { ExpressSchema, EntityDefinition, AttributeDefinition } from './express-parser.js';
12
- import { getAllAttributes } from './express-parser.js';
15
+ import type { ExpressSchema } from './express-parser.js';
13
16
 
14
17
  /**
15
- * Generate serialization support code
18
+ * Generate serialization support code — a thin re-export of the shared
19
+ * `@ifc-lite/data` STEP serializer with this schema's registry bound in.
16
20
  */
17
21
  export function generateSerializers(schema: ExpressSchema): string {
18
- let code = `/* This Source Code Form is subject to the terms of the Mozilla Public
22
+ return `/* This Source Code Form is subject to the terms of the Mozilla Public
19
23
  * License, v. 2.0. If a copy of the MPL was not distributed with this
20
24
  * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
21
25
 
@@ -23,321 +27,48 @@ export function generateSerializers(schema: ExpressSchema): string {
23
27
  * IFC Serialization Support
24
28
  * Generated from EXPRESS schema: ${schema.name}
25
29
  *
26
- * Utilities for serializing IFC entities to STEP format.
30
+ * Thin binding over the shared, schema-agnostic STEP serializer in
31
+ * \`@ifc-lite/data\`. All serialization logic lives there (single source of
32
+ * truth); only this bundle's \`SCHEMA_REGISTRY\` is bound to the registry-coupled
33
+ * helpers below.
27
34
  *
28
35
  * DO NOT EDIT - This file is auto-generated
29
36
  */
30
37
 
31
38
  import { SCHEMA_REGISTRY } from './schema-registry.js';
32
-
33
- /**
34
- * STEP value types
35
- */
36
- export type StepValue =
37
- | string
38
- | number
39
- | boolean
40
- | null
41
- | undefined
42
- | StepValue[]
43
- | EntityRef
44
- | EnumValue;
45
-
46
- /**
47
- * Entity reference (#123)
48
- */
49
- export interface EntityRef {
50
- ref: number;
51
- }
52
-
53
- /**
54
- * Enum value (.VALUE.)
55
- */
56
- export interface EnumValue {
57
- enum: string;
58
- }
59
-
60
- /**
61
- * Check if value is an entity reference
62
- */
63
- export function isEntityRef(value: unknown): value is EntityRef {
64
- return typeof value === 'object' && value !== null && 'ref' in value;
65
- }
66
-
67
- /**
68
- * Check if value is an enum value
69
- */
70
- export function isEnumValue(value: unknown): value is EnumValue {
71
- return typeof value === 'object' && value !== null && 'enum' in value;
72
- }
73
-
74
- /**
75
- * Create an entity reference
76
- */
77
- export function ref(id: number): EntityRef {
78
- return { ref: id };
79
- }
80
-
81
- /**
82
- * Create an enum value
83
- */
84
- export function enumVal(value: string): EnumValue {
85
- return { enum: value };
86
- }
87
-
88
- /**
89
- * Base interface for serializable entities
90
- */
91
- export interface StepEntity {
92
- /** Express ID (#123) */
93
- expressId: number;
94
- /** IFC type name */
95
- type: string;
96
- /** Attribute values */
97
- [key: string]: unknown;
98
- }
99
-
100
- /**
101
- * Serialize a single value to STEP format
102
- */
103
- export function serializeValue(value: StepValue): string {
104
- // Null/undefined -> $
105
- if (value === null || value === undefined) {
106
- return '$';
107
- }
108
-
109
- // Derived value -> *
110
- if (value === '*') {
111
- return '*';
112
- }
113
-
114
- // Boolean
115
- if (typeof value === 'boolean') {
116
- return value ? '.T.' : '.F.';
117
- }
118
-
119
- // Number
120
- if (typeof value === 'number') {
121
- if (!Number.isFinite(value)) {
122
- return '$';
123
- }
124
- // Use exponential notation for large/small numbers
125
- if (Math.abs(value) > 1e10 || (Math.abs(value) < 1e-10 && value !== 0)) {
126
- return value.toExponential(10).toUpperCase().replace('E+', 'E');
127
- }
128
- // Otherwise use fixed notation
129
- const str = value.toString();
130
- // Ensure there's a decimal point for REAL values
131
- return str.includes('.') ? str : str + '.';
132
- }
133
-
134
- // String
135
- if (typeof value === 'string') {
136
- return "'" + escapeStepString(value) + "'";
137
- }
138
-
139
- // Entity reference
140
- if (isEntityRef(value)) {
141
- return '#' + value.ref;
142
- }
143
-
144
- // Enum value
145
- if (isEnumValue(value)) {
146
- return '.' + value.enum + '.';
147
- }
148
-
149
- // Array/List
150
- if (Array.isArray(value)) {
151
- if (value.length === 0) {
152
- return '()';
153
- }
154
- return '(' + value.map(v => serializeValue(v as StepValue)).join(',') + ')';
155
- }
156
-
157
- // Object (shouldn't happen for valid STEP values)
158
- return '$';
159
- }
160
-
161
- /**
162
- * Escape a string for STEP format
163
- */
164
- function escapeStepString(str: string): string {
165
- return str
166
- .replace(/\\\\/g, '\\\\\\\\') // Backslash
167
- .replace(/'/g, "''"); // Single quote
168
- }
169
-
170
- /**
171
- * Serialize an entity to a STEP line
39
+ import {
40
+ generateHeader,
41
+ generateStepFileWithRegistry,
42
+ toStepLineWithRegistry,
43
+ type StepEntity,
44
+ } from '@ifc-lite/data';
45
+
46
+ export {
47
+ isEntityRef,
48
+ isEnumValue,
49
+ ref,
50
+ enumVal,
51
+ serializeValue,
52
+ generateHeader,
53
+ parseStepValue,
54
+ } from '@ifc-lite/data';
55
+ export type { StepValue, EntityRef, EnumValue, StepEntity } from '@ifc-lite/data';
56
+
57
+ /**
58
+ * Serialize an entity to a STEP line (bound to this bundle's schema).
172
59
  */
173
60
  export function toStepLine(entity: StepEntity): string {
174
- const schema = SCHEMA_REGISTRY.entities[entity.type];
175
- if (!schema) {
176
- throw new Error(\`Unknown entity type: \${entity.type}\`);
177
- }
178
-
179
- // Get all attributes in order
180
- const values: string[] = [];
181
- for (const attr of schema.allAttributes) {
182
- const value = entity[attr.name];
183
- values.push(serializeValue(value as StepValue));
184
- }
185
-
186
- return \`#\${entity.expressId}=\${entity.type.toUpperCase()}(\${values.join(',')});\`;
187
- }
188
-
189
- /**
190
- * Generate STEP file header
191
- */
192
- export function generateHeader(options: {
193
- description?: string;
194
- author?: string;
195
- organization?: string;
196
- application?: string;
197
- schema: 'IFC2X3' | 'IFC4' | 'IFC4X3' | 'IFC5';
198
- filename?: string;
199
- }): string {
200
- const now = new Date().toISOString().replace(/[-:]/g, '').split('.')[0];
201
- const desc = options.description || 'ViewDefinition [CoordinationView]';
202
- const author = options.author || '';
203
- const org = options.organization || '';
204
- const app = options.application || 'ifc-lite';
205
- const filename = options.filename || 'output.ifc';
206
-
207
- return \`ISO-10303-21;
208
- HEADER;
209
- FILE_DESCRIPTION('\${desc}','2;1');
210
- FILE_NAME('\${filename}','\${now}',('\${author}'),('\${org}'),'\${app}','\${app}','');
211
- FILE_SCHEMA(('\${options.schema}'));
212
- ENDSEC;
213
- \`;
61
+ return toStepLineWithRegistry(SCHEMA_REGISTRY, entity);
214
62
  }
215
63
 
216
64
  /**
217
- * Generate complete STEP file content
65
+ * Generate complete STEP file content (bound to this bundle's schema).
218
66
  */
219
67
  export function generateStepFile(
220
68
  entities: StepEntity[],
221
69
  options: Parameters<typeof generateHeader>[0]
222
70
  ): string {
223
- const header = generateHeader(options);
224
-
225
- // Sort entities by ID for deterministic output
226
- const sorted = [...entities].sort((a, b) => a.expressId - b.expressId);
227
-
228
- const data = sorted.map(e => toStepLine(e)).join('\\n');
229
-
230
- return \`\${header}DATA;
231
- \${data}
232
- ENDSEC;
233
- END-ISO-10303-21;
234
- \`;
235
- }
236
-
237
- /**
238
- * Parse a STEP value from string
239
- */
240
- export function parseStepValue(str: string): StepValue {
241
- str = str.trim();
242
-
243
- // Null
244
- if (str === '$') {
245
- return null;
246
- }
247
-
248
- // Derived
249
- if (str === '*') {
250
- return '*' as unknown as StepValue;
251
- }
252
-
253
- // Boolean
254
- if (str === '.T.') {
255
- return true;
256
- }
257
- if (str === '.F.') {
258
- return false;
259
- }
260
- if (str === '.U.') {
261
- return null; // Unknown/indeterminate
262
- }
263
-
264
- // Entity reference
265
- if (str.startsWith('#')) {
266
- return { ref: parseInt(str.substring(1), 10) };
267
- }
268
-
269
- // Enum
270
- if (str.startsWith('.') && str.endsWith('.')) {
271
- return { enum: str.substring(1, str.length - 1) };
272
- }
273
-
274
- // String
275
- if (str.startsWith("'") && str.endsWith("'")) {
276
- return unescapeStepString(str.substring(1, str.length - 1));
277
- }
278
-
279
- // List
280
- if (str.startsWith('(') && str.endsWith(')')) {
281
- return parseStepList(str);
282
- }
283
-
284
- // Number
285
- const num = parseFloat(str);
286
- if (!isNaN(num)) {
287
- return num;
288
- }
289
-
290
- // Unknown
291
- return str;
292
- }
293
-
294
- /**
295
- * Parse a STEP list
296
- */
297
- function parseStepList(str: string): StepValue[] {
298
- // Remove outer parentheses
299
- const inner = str.substring(1, str.length - 1).trim();
300
- if (inner === '') {
301
- return [];
302
- }
303
-
304
- const values: StepValue[] = [];
305
- let depth = 0;
306
- let current = '';
307
-
308
- for (let i = 0; i < inner.length; i++) {
309
- const char = inner[i];
310
-
311
- if (char === '(' || char === '[') {
312
- depth++;
313
- current += char;
314
- } else if (char === ')' || char === ']') {
315
- depth--;
316
- current += char;
317
- } else if (char === ',' && depth === 0) {
318
- values.push(parseStepValue(current));
319
- current = '';
320
- } else {
321
- current += char;
322
- }
323
- }
324
-
325
- if (current.trim()) {
326
- values.push(parseStepValue(current));
327
- }
328
-
329
- return values;
330
- }
331
-
332
- /**
333
- * Unescape a STEP string
334
- */
335
- function unescapeStepString(str: string): string {
336
- return str
337
- .replace(/''/g, "'")
338
- .replace(/\\\\\\\\/g, '\\\\');
71
+ return generateStepFileWithRegistry(SCHEMA_REGISTRY, entities, options);
339
72
  }
340
73
  `;
341
-
342
- return code;
343
74
  }
@@ -145,7 +145,9 @@ function generateAttribute(attr: AttributeDefinition): string {
145
145
  // Wrap in array if needed
146
146
  // Note: attr.type may already contain [] for nested collections from the parser
147
147
  if (attr.isArray || attr.isList || attr.isSet) {
148
- tsType = `${tsType}[]`;
148
+ // Parenthesize union element types so `boolean | null` becomes
149
+ // `(boolean | null)[]` rather than the precedence-misparsed `boolean | null[]`.
150
+ tsType = /\|/.test(tsType) ? `(${tsType})[]` : `${tsType}[]`;
149
151
  }
150
152
 
151
153
  code += tsType;
@@ -183,7 +185,8 @@ function mapExpressTypeToTypeScript(expressType: string): string {
183
185
  const collectionMatch = expressType.match(/^(LIST|SET|ARRAY)\s*\[.*?\]\s*OF\s+(.+)$/i);
184
186
  if (collectionMatch) {
185
187
  const innerType = mapExpressTypeToTypeScript(collectionMatch[2].trim());
186
- return `${innerType}[]`;
188
+ // Parenthesize union element types to preserve `(union)[]` precedence.
189
+ return /\|/.test(innerType) ? `(${innerType})[]` : `${innerType}[]`;
187
190
  }
188
191
 
189
192
  // Handle ENUMERATION OF (...) - these should be handled by enum generation
@@ -0,0 +1,65 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+
5
+ /**
6
+ * The STEP serializer LOGIC lives once in `@ifc-lite/data`. The per-schema
7
+ * bundle that codegen emits must be a thin re-export + registry binding, never
8
+ * an inlined copy — otherwise the copies can silently drift (which is exactly
9
+ * the bug this refactor removed). These tests fail if anyone re-inlines logic.
10
+ */
11
+
12
+ import { describe, it, expect } from 'vitest';
13
+ import type { ExpressSchema } from '../src/express-parser.js';
14
+ import { generateSerializers } from '../src/serialization-generator.js';
15
+
16
+ const STUB_SCHEMA: ExpressSchema = {
17
+ name: 'IFC_TEST_SCHEMA',
18
+ entities: [],
19
+ types: [],
20
+ enums: [],
21
+ selects: [],
22
+ };
23
+
24
+ describe('generateSerializers', () => {
25
+ const out = generateSerializers(STUB_SCHEMA);
26
+
27
+ it('re-exports the shared serializer from @ifc-lite/data', () => {
28
+ expect(out).toContain("from '@ifc-lite/data'");
29
+ expect(out).toContain('toStepLineWithRegistry');
30
+ expect(out).toContain('generateStepFileWithRegistry');
31
+ });
32
+
33
+ it('binds the bundle SCHEMA_REGISTRY to the registry-coupled helpers', () => {
34
+ expect(out).toContain("import { SCHEMA_REGISTRY } from './schema-registry.js'");
35
+ expect(out).toContain('toStepLineWithRegistry(SCHEMA_REGISTRY, entity)');
36
+ });
37
+
38
+ it('still surfaces the full public serializer API', () => {
39
+ for (const sym of [
40
+ 'serializeValue',
41
+ 'generateHeader',
42
+ 'parseStepValue',
43
+ 'ref',
44
+ 'enumVal',
45
+ 'isEntityRef',
46
+ 'isEnumValue',
47
+ 'toStepLine',
48
+ 'generateStepFile',
49
+ ]) {
50
+ expect(out).toContain(sym);
51
+ }
52
+ });
53
+
54
+ it('does NOT inline serializer logic (must stay a thin re-export)', () => {
55
+ // The header/escape/parse implementations belong to @ifc-lite/data only.
56
+ expect(out).not.toContain('function escapeStepString');
57
+ expect(out).not.toContain('FILE_DESCRIPTION');
58
+ expect(out).not.toContain("'.T.'"); // serializeValue boolean branch
59
+ expect(out).not.toContain('function parseStepList');
60
+ });
61
+
62
+ it('stamps the schema name in the banner', () => {
63
+ expect(out).toContain('Generated from EXPRESS schema: IFC_TEST_SCHEMA');
64
+ });
65
+ });
package/tsconfig.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "target": "ES2022",
4
- "module": "ES2022",
4
+ "module": "nodenext",
5
5
  "lib": ["ES2022"],
6
6
  "types": ["node"],
7
- "moduleResolution": "node",
7
+ "moduleResolution": "nodenext",
8
8
  "outDir": "./dist",
9
9
  "rootDir": "./src",
10
10
  "declaration": true,