@eide/foir-cli 0.1.3 → 0.1.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/dist/cli.js CHANGED
File without changes
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Generates the shared FieldTypes.swift file with platform value type structs.
3
+ */
4
+ export declare function generateSwiftFieldTypesFile(): string;
5
+ //# sourceMappingURL=swift-field-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"swift-field-types.d.ts","sourceRoot":"","sources":["../../../src/codegen/generators/swift-field-types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,wBAAgB,2BAA2B,IAAI,MAAM,CA6IpD"}
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Generates the shared FieldTypes.swift file with platform value type structs.
3
+ */
4
+ export function generateSwiftFieldTypesFile() {
5
+ return `//
6
+ // FieldTypes.swift
7
+ //
8
+ // Shared value types for platform sync data.
9
+ //
10
+ // @generated by foir \u2014 DO NOT EDIT MANUALLY
11
+ //
12
+
13
+ import Foundation
14
+
15
+ // MARK: - Image
16
+
17
+ struct ImageValue {
18
+ let id: String
19
+ let url: String
20
+ var alt: String?
21
+ var width: Int?
22
+ var height: Int?
23
+
24
+ func toSyncData() -> [String: Any] {
25
+ var data: [String: Any] = ["id": id, "url": url]
26
+ if let alt { data["alt"] = alt }
27
+ if let width { data["width"] = width }
28
+ if let height { data["height"] = height }
29
+ return data
30
+ }
31
+
32
+ static func fromSyncData(_ data: [String: Any]) -> ImageValue {
33
+ ImageValue(
34
+ id: data["id"] as? String ?? "",
35
+ url: data["url"] as? String ?? "",
36
+ alt: data["alt"] as? String,
37
+ width: data["width"] as? Int,
38
+ height: data["height"] as? Int
39
+ )
40
+ }
41
+ }
42
+
43
+ // MARK: - Video
44
+
45
+ struct VideoValue {
46
+ let id: String
47
+ let url: String
48
+ var thumbnail: String?
49
+ var duration: Double?
50
+
51
+ func toSyncData() -> [String: Any] {
52
+ var data: [String: Any] = ["id": id, "url": url]
53
+ if let thumbnail { data["thumbnail"] = thumbnail }
54
+ if let duration { data["duration"] = duration }
55
+ return data
56
+ }
57
+
58
+ static func fromSyncData(_ data: [String: Any]) -> VideoValue {
59
+ VideoValue(
60
+ id: data["id"] as? String ?? "",
61
+ url: data["url"] as? String ?? "",
62
+ thumbnail: data["thumbnail"] as? String,
63
+ duration: data["duration"] as? Double
64
+ )
65
+ }
66
+ }
67
+
68
+ // MARK: - File
69
+
70
+ struct FileValue {
71
+ let id: String
72
+ let url: String
73
+ let name: String
74
+ let size: Int
75
+ let mimeType: String
76
+
77
+ func toSyncData() -> [String: Any] {
78
+ [
79
+ "id": id,
80
+ "url": url,
81
+ "name": name,
82
+ "size": size,
83
+ "mimeType": mimeType,
84
+ ]
85
+ }
86
+
87
+ static func fromSyncData(_ data: [String: Any]) -> FileValue {
88
+ FileValue(
89
+ id: data["id"] as? String ?? "",
90
+ url: data["url"] as? String ?? "",
91
+ name: data["name"] as? String ?? "",
92
+ size: data["size"] as? Int ?? 0,
93
+ mimeType: data["mimeType"] as? String ?? ""
94
+ )
95
+ }
96
+ }
97
+
98
+ // MARK: - Currency
99
+
100
+ struct CurrencyValue {
101
+ let amount: Double
102
+ let currency: String
103
+
104
+ func toSyncData() -> [String: Any] {
105
+ ["amount": amount, "currency": currency]
106
+ }
107
+
108
+ static func fromSyncData(_ data: [String: Any]) -> CurrencyValue {
109
+ CurrencyValue(
110
+ amount: data["amount"] as? Double ?? 0,
111
+ currency: data["currency"] as? String ?? ""
112
+ )
113
+ }
114
+ }
115
+
116
+ // MARK: - Link
117
+
118
+ struct LinkValue {
119
+ let type: String
120
+ var entityModelKey: String?
121
+ var entityNaturalKey: String?
122
+ var url: String?
123
+ var target: String?
124
+
125
+ func toSyncData() -> [String: Any] {
126
+ var data: [String: Any] = ["type": type]
127
+ if let entityModelKey { data["entityModelKey"] = entityModelKey }
128
+ if let entityNaturalKey { data["entityNaturalKey"] = entityNaturalKey }
129
+ if let url { data["url"] = url }
130
+ if let target { data["target"] = target }
131
+ return data
132
+ }
133
+
134
+ static func fromSyncData(_ data: [String: Any]) -> LinkValue {
135
+ LinkValue(
136
+ type: data["type"] as? String ?? "",
137
+ entityModelKey: data["entityModelKey"] as? String,
138
+ entityNaturalKey: data["entityNaturalKey"] as? String,
139
+ url: data["url"] as? String,
140
+ target: data["target"] as? String
141
+ )
142
+ }
143
+ }
144
+ `;
145
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Generates ModelKeys.swift with all model key constants in one enum.
3
+ */
4
+ import type { CodegenModel } from '../fetch-models.js';
5
+ export declare function generateSwiftModelKeys(models: CodegenModel[]): string;
6
+ //# sourceMappingURL=swift-model-keys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"swift-model-keys.d.ts","sourceRoot":"","sources":["../../../src/codegen/generators/swift-model-keys.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGvD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,CAwBrE"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Generates ModelKeys.swift with all model key constants in one enum.
3
+ */
4
+ import { toCamelCase } from '../field-mapping.js';
5
+ export function generateSwiftModelKeys(models) {
6
+ const lines = [];
7
+ lines.push('//');
8
+ lines.push('// ModelKeys.swift');
9
+ lines.push('//');
10
+ lines.push('// All model key constants.');
11
+ lines.push('//');
12
+ lines.push('// @generated by foir \u2014 DO NOT EDIT MANUALLY');
13
+ lines.push('//');
14
+ lines.push('');
15
+ lines.push('import Foundation');
16
+ lines.push('');
17
+ lines.push('enum ModelKeys {');
18
+ for (const model of models) {
19
+ const propName = toCamelCase(model.key);
20
+ lines.push(` static let ${propName} = "${model.key}"`);
21
+ }
22
+ lines.push('}');
23
+ lines.push('');
24
+ return lines.join('\n');
25
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Generates per-model Swift files with:
3
+ * - Field key enum (compile-time safe field keys)
4
+ * - Data struct (typed representation)
5
+ * - Serialization helpers (toSyncData / fromSyncData)
6
+ * - Model config constant
7
+ */
8
+ import type { CodegenModel } from '../fetch-models.js';
9
+ /**
10
+ * Generate a complete Swift file for a single model.
11
+ */
12
+ export declare function generateSwiftModelFile(model: CodegenModel): string;
13
+ //# sourceMappingURL=swift-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"swift-types.d.ts","sourceRoot":"","sources":["../../../src/codegen/generators/swift-types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAKvD;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAkClE"}
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Generates per-model Swift files with:
3
+ * - Field key enum (compile-time safe field keys)
4
+ * - Data struct (typed representation)
5
+ * - Serialization helpers (toSyncData / fromSyncData)
6
+ * - Model config constant
7
+ */
8
+ import { toPascalCase } from '../field-mapping.js';
9
+ import { getSwiftFieldType, SWIFT_FIELD_TYPE_MAPPING } from '../swift-field-mapping.js';
10
+ /**
11
+ * Generate a complete Swift file for a single model.
12
+ */
13
+ export function generateSwiftModelFile(model) {
14
+ const typeName = toPascalCase(model.key);
15
+ const fields = model.fields ?? [];
16
+ const lines = [];
17
+ // Header
18
+ lines.push('//');
19
+ lines.push(`// ${typeName}.swift`);
20
+ lines.push('//');
21
+ lines.push(`// Generated from model '${model.key}'`);
22
+ lines.push('//');
23
+ lines.push('// @generated by foir \u2014 DO NOT EDIT MANUALLY');
24
+ lines.push('//');
25
+ lines.push('');
26
+ lines.push('import Foundation');
27
+ lines.push('');
28
+ // 1. Field key enum
29
+ lines.push(generateFieldsEnum(typeName, fields));
30
+ lines.push('');
31
+ // 2. Data struct
32
+ lines.push(generateDataStruct(typeName, fields));
33
+ lines.push('');
34
+ // 3. Serialization extension
35
+ lines.push(generateSerializationExtension(typeName, fields));
36
+ lines.push('');
37
+ // 4. Config enum
38
+ lines.push(generateConfigEnum(typeName, model));
39
+ return lines.join('\n');
40
+ }
41
+ function generateFieldsEnum(typeName, fields) {
42
+ const lines = [];
43
+ lines.push(`// MARK: - ${typeName} Field Keys`);
44
+ lines.push('');
45
+ lines.push(`enum ${typeName}Fields {`);
46
+ for (const field of fields) {
47
+ lines.push(` static let ${field.key} = "${field.key}"`);
48
+ }
49
+ lines.push('}');
50
+ return lines.join('\n');
51
+ }
52
+ function generateDataStruct(typeName, fields) {
53
+ const lines = [];
54
+ lines.push(`// MARK: - ${typeName} Data`);
55
+ lines.push('');
56
+ lines.push(`struct ${typeName}Data {`);
57
+ for (const field of fields) {
58
+ const { type, isOptional } = getSwiftFieldType(field);
59
+ const optionalSuffix = isOptional ? '?' : '';
60
+ lines.push(` var ${field.key}: ${type}${optionalSuffix}`);
61
+ }
62
+ lines.push('}');
63
+ return lines.join('\n');
64
+ }
65
+ function generateSerializationExtension(typeName, fields) {
66
+ const lines = [];
67
+ lines.push(`// MARK: - ${typeName} Serialization`);
68
+ lines.push('');
69
+ lines.push(`extension ${typeName}Data {`);
70
+ // toSyncData()
71
+ lines.push(' func toSyncData() -> [String: Any] {');
72
+ // Find the first required (non-optional) field to seed the dictionary
73
+ const requiredFields = fields.filter((f) => {
74
+ const { isOptional } = getSwiftFieldType(f);
75
+ return !isOptional;
76
+ });
77
+ const optionalFields = fields.filter((f) => {
78
+ const { isOptional } = getSwiftFieldType(f);
79
+ return isOptional;
80
+ });
81
+ if (requiredFields.length > 0) {
82
+ if (optionalFields.length === 0) {
83
+ // All fields are required — return inline
84
+ lines.push(` return [`);
85
+ requiredFields.forEach((f, i) => {
86
+ const comma = i < requiredFields.length - 1 ? ',' : '';
87
+ lines.push(` ${typeName}Fields.${f.key}: ${toSyncValueExpr(f)}${comma}`);
88
+ });
89
+ lines.push(' ]');
90
+ }
91
+ else {
92
+ lines.push(` var data: [String: Any] = [`);
93
+ requiredFields.forEach((f, i) => {
94
+ const comma = i < requiredFields.length - 1 ? ',' : '';
95
+ lines.push(` ${typeName}Fields.${f.key}: ${toSyncValueExpr(f)}${comma}`);
96
+ });
97
+ lines.push(' ]');
98
+ for (const f of optionalFields) {
99
+ lines.push(` if let ${f.key} { data[${typeName}Fields.${f.key}] = ${toSyncValueExprForOptional(f)} }`);
100
+ }
101
+ lines.push(' return data');
102
+ }
103
+ }
104
+ else {
105
+ // All fields are optional
106
+ lines.push(' var data: [String: Any] = [:]');
107
+ for (const f of optionalFields) {
108
+ lines.push(` if let ${f.key} { data[${typeName}Fields.${f.key}] = ${toSyncValueExprForOptional(f)} }`);
109
+ }
110
+ lines.push(' return data');
111
+ }
112
+ lines.push(' }');
113
+ lines.push('');
114
+ // fromSyncData()
115
+ lines.push(' static func fromSyncData(_ data: [String: Any]) -> ' + typeName + 'Data {');
116
+ lines.push(` ${typeName}Data(`);
117
+ fields.forEach((field, i) => {
118
+ const comma = i < fields.length - 1 ? ',' : '';
119
+ const { isOptional, mapping } = getSwiftFieldType(field);
120
+ lines.push(` ${field.key}: ${fromSyncDataExpr(field, typeName, isOptional, mapping)}${comma}`);
121
+ });
122
+ lines.push(' )');
123
+ lines.push(' }');
124
+ lines.push('}');
125
+ return lines.join('\n');
126
+ }
127
+ /** Expression for a required field value in toSyncData */
128
+ function toSyncValueExpr(field) {
129
+ const mapping = SWIFT_FIELD_TYPE_MAPPING[field.type];
130
+ if (mapping?.needsSharedType) {
131
+ return `${field.key}.toSyncData()`;
132
+ }
133
+ return field.key;
134
+ }
135
+ /** Expression for an optional field value in toSyncData (inside `if let`) */
136
+ function toSyncValueExprForOptional(field) {
137
+ const mapping = SWIFT_FIELD_TYPE_MAPPING[field.type];
138
+ if (mapping?.needsSharedType) {
139
+ return `${field.key}.toSyncData()`;
140
+ }
141
+ return field.key;
142
+ }
143
+ /** Expression for reading a field from sync data dictionary */
144
+ function fromSyncDataExpr(field, typeName, isOptional, mapping) {
145
+ const accessor = `data[${typeName}Fields.${field.key}]`;
146
+ if (!mapping) {
147
+ // Unknown type
148
+ return isOptional ? `${accessor}` : `${accessor} ?? nil`;
149
+ }
150
+ // Types with shared structs need .fromSyncData conversion
151
+ if (mapping.needsSharedType) {
152
+ const dictCast = `${accessor} as? [String: Any]`;
153
+ if (isOptional) {
154
+ return `(${dictCast}).map { ${mapping.type}.fromSyncData($0) }`;
155
+ }
156
+ return `${mapping.type}.fromSyncData(${dictCast} ?? [:])`;
157
+ }
158
+ // json → Any
159
+ if (field.type === 'json') {
160
+ return isOptional ? accessor : `${accessor}`;
161
+ }
162
+ // Simple cast types
163
+ if (isOptional) {
164
+ return `${accessor} ${mapping.castExpression}`;
165
+ }
166
+ return `${accessor} ${mapping.castExpression} ?? ${mapping.defaultValue}`;
167
+ }
168
+ function generateConfigEnum(typeName, model) {
169
+ const lines = [];
170
+ lines.push(`// MARK: - ${typeName} Config`);
171
+ lines.push('');
172
+ lines.push(`enum ${typeName}Config {`);
173
+ const escapedName = (model.name ?? model.key).replace(/"/g, '\\"');
174
+ lines.push(` static let key = "${model.key}"`);
175
+ lines.push(` static let name = "${escapedName}"`);
176
+ lines.push(` static let customerScoped = ${model.config.customerScoped}`);
177
+ lines.push(` static let publicApi = ${model.config.publicApi}`);
178
+ lines.push(` static let versioning = ${model.config.versioning}`);
179
+ lines.push(` static let publishing = ${model.config.publishing}`);
180
+ lines.push(` static let variants = ${model.config.variants}`);
181
+ lines.push('}');
182
+ lines.push('');
183
+ return lines.join('\n');
184
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Maps EIDE field types to Swift types for sync data serialization.
3
+ *
4
+ * Reference fields map to String (not a struct) because sync data
5
+ * payloads carry plain string IDs — the platform resolves references separately.
6
+ */
7
+ import type { FieldSchemaForGen } from './field-mapping.js';
8
+ export interface SwiftTypeMapping {
9
+ /** Swift type name */
10
+ type: string;
11
+ /** Whether the field is always optional (regardless of required flag) */
12
+ alwaysOptional: boolean;
13
+ /** Default value expression for non-optional required fields in fromSyncData */
14
+ defaultValue: string;
15
+ /** The `as?` cast expression for fromSyncData */
16
+ castExpression: string;
17
+ /** Whether this type needs a shared struct in FieldTypes.swift */
18
+ needsSharedType?: boolean;
19
+ }
20
+ export declare const SWIFT_FIELD_TYPE_MAPPING: Record<string, SwiftTypeMapping>;
21
+ /**
22
+ * Get the Swift type for a field.
23
+ * Returns { type, isOptional } so callers can decide on `?` suffix.
24
+ */
25
+ export declare function getSwiftFieldType(field: FieldSchemaForGen): {
26
+ type: string;
27
+ isOptional: boolean;
28
+ mapping: SwiftTypeMapping | undefined;
29
+ };
30
+ //# sourceMappingURL=swift-field-mapping.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"swift-field-mapping.d.ts","sourceRoot":"","sources":["../../src/codegen/swift-field-mapping.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAE5D,MAAM,WAAW,gBAAgB;IAC/B,sBAAsB;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,cAAc,EAAE,OAAO,CAAC;IACxB,gFAAgF;IAChF,YAAY,EAAE,MAAM,CAAC;IACrB,iDAAiD;IACjD,cAAc,EAAE,MAAM,CAAC;IACvB,kEAAkE;IAClE,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,eAAO,MAAM,wBAAwB,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAoIrE,CAAC;AAEF;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,iBAAiB,GAAG;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,OAAO,CAAC;IACpB,OAAO,EAAE,gBAAgB,GAAG,SAAS,CAAC;CACvC,CAeA"}
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Maps EIDE field types to Swift types for sync data serialization.
3
+ *
4
+ * Reference fields map to String (not a struct) because sync data
5
+ * payloads carry plain string IDs — the platform resolves references separately.
6
+ */
7
+ export const SWIFT_FIELD_TYPE_MAPPING = {
8
+ text: {
9
+ type: 'String',
10
+ alwaysOptional: false,
11
+ defaultValue: '""',
12
+ castExpression: 'as? String',
13
+ },
14
+ richtext: {
15
+ type: 'String',
16
+ alwaysOptional: true,
17
+ defaultValue: '""',
18
+ castExpression: 'as? String',
19
+ },
20
+ number: {
21
+ type: 'Double',
22
+ alwaysOptional: true,
23
+ defaultValue: '0',
24
+ castExpression: 'as? Double',
25
+ },
26
+ boolean: {
27
+ type: 'Bool',
28
+ alwaysOptional: true,
29
+ defaultValue: 'false',
30
+ castExpression: 'as? Bool',
31
+ },
32
+ email: {
33
+ type: 'String',
34
+ alwaysOptional: true,
35
+ defaultValue: '""',
36
+ castExpression: 'as? String',
37
+ },
38
+ phone: {
39
+ type: 'String',
40
+ alwaysOptional: true,
41
+ defaultValue: '""',
42
+ castExpression: 'as? String',
43
+ },
44
+ url: {
45
+ type: 'String',
46
+ alwaysOptional: true,
47
+ defaultValue: '""',
48
+ castExpression: 'as? String',
49
+ },
50
+ date: {
51
+ type: 'String',
52
+ alwaysOptional: true,
53
+ defaultValue: '""',
54
+ castExpression: 'as? String',
55
+ },
56
+ image: {
57
+ type: 'ImageValue',
58
+ alwaysOptional: true,
59
+ defaultValue: 'ImageValue(id: "", url: "")',
60
+ castExpression: 'as? [String: Any]',
61
+ needsSharedType: true,
62
+ },
63
+ video: {
64
+ type: 'VideoValue',
65
+ alwaysOptional: true,
66
+ defaultValue: 'VideoValue(id: "", url: "")',
67
+ castExpression: 'as? [String: Any]',
68
+ needsSharedType: true,
69
+ },
70
+ file: {
71
+ type: 'FileValue',
72
+ alwaysOptional: true,
73
+ defaultValue: 'FileValue(id: "", url: "", name: "", size: 0, mimeType: "")',
74
+ castExpression: 'as? [String: Any]',
75
+ needsSharedType: true,
76
+ },
77
+ currency: {
78
+ type: 'CurrencyValue',
79
+ alwaysOptional: true,
80
+ defaultValue: 'CurrencyValue(amount: 0, currency: "")',
81
+ castExpression: 'as? [String: Any]',
82
+ needsSharedType: true,
83
+ },
84
+ select: {
85
+ type: 'String',
86
+ alwaysOptional: true,
87
+ defaultValue: '""',
88
+ castExpression: 'as? String',
89
+ },
90
+ multiselect: {
91
+ type: '[String]',
92
+ alwaysOptional: true,
93
+ defaultValue: '[]',
94
+ castExpression: 'as? [String]',
95
+ },
96
+ json: {
97
+ type: 'Any',
98
+ alwaysOptional: true,
99
+ defaultValue: 'nil',
100
+ castExpression: '',
101
+ },
102
+ list: {
103
+ type: '[Any]',
104
+ alwaysOptional: true,
105
+ defaultValue: '[]',
106
+ castExpression: 'as? [Any]',
107
+ },
108
+ tree: {
109
+ type: '[Any]',
110
+ alwaysOptional: true,
111
+ defaultValue: '[]',
112
+ castExpression: 'as? [Any]',
113
+ },
114
+ flexible: {
115
+ type: '[[String: Any]]',
116
+ alwaysOptional: true,
117
+ defaultValue: '[]',
118
+ castExpression: 'as? [[String: Any]]',
119
+ },
120
+ 'entity-reference': {
121
+ type: 'String',
122
+ alwaysOptional: true,
123
+ defaultValue: '""',
124
+ castExpression: 'as? String',
125
+ },
126
+ reference: {
127
+ type: 'String',
128
+ alwaysOptional: true,
129
+ defaultValue: '""',
130
+ castExpression: 'as? String',
131
+ },
132
+ link: {
133
+ type: 'LinkValue',
134
+ alwaysOptional: true,
135
+ defaultValue: 'LinkValue(type: "")',
136
+ castExpression: 'as? [String: Any]',
137
+ needsSharedType: true,
138
+ },
139
+ };
140
+ /**
141
+ * Get the Swift type for a field.
142
+ * Returns { type, isOptional } so callers can decide on `?` suffix.
143
+ */
144
+ export function getSwiftFieldType(field) {
145
+ const mapping = SWIFT_FIELD_TYPE_MAPPING[field.type];
146
+ if (!mapping) {
147
+ // Unknown type — treat as optional Any
148
+ return {
149
+ type: 'Any',
150
+ isOptional: true,
151
+ mapping: undefined,
152
+ };
153
+ }
154
+ const isOptional = mapping.alwaysOptional || !field.required;
155
+ return { type: mapping.type, isOptional, mapping };
156
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"write-files.d.ts","sourceRoot":"","sources":["../../src/codegen/write-files.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,WAAW,GAAE,OAAc,GAC1B,OAAO,CAAC,IAAI,CAAC,CAqBf;AAED;;GAEG;AACH,wBAAsB,UAAU,CAC9B,KAAK,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,EAC/C,WAAW,GAAE,OAAc,GAC1B,OAAO,CAAC,IAAI,CAAC,CAIf"}
1
+ {"version":3,"file":"write-files.d.ts","sourceRoot":"","sources":["../../src/codegen/write-files.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,WAAW,GAAE,OAAc,GAC1B,OAAO,CAAC,IAAI,CAAC,CAsBf;AAED;;GAEG;AACH,wBAAsB,UAAU,CAC9B,KAAK,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,EAC/C,WAAW,GAAE,OAAc,GAC1B,OAAO,CAAC,IAAI,CAAC,CAIf"}
@@ -9,7 +9,8 @@ import { dirname } from 'path';
9
9
  export async function writeGeneratedFile(filePath, content, usePrettier = true) {
10
10
  await mkdir(dirname(filePath), { recursive: true });
11
11
  let formattedContent = content;
12
- if (usePrettier) {
12
+ const isSwift = filePath.endsWith('.swift');
13
+ if (usePrettier && !isSwift) {
13
14
  try {
14
15
  const prettier = await import('prettier');
15
16
  const parser = filePath.endsWith('.graphql') ? 'graphql' : 'typescript';
@@ -1 +1 @@
1
- {"version":3,"file":"pull.d.ts","sourceRoot":"","sources":["../../src/commands/pull.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAYtD,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,aAAa,GAC9B,IAAI,CAmHN"}
1
+ {"version":3,"file":"pull.d.ts","sourceRoot":"","sources":["../../src/commands/pull.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAgBtD,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,aAAa,GAC9B,IAAI,CAmJN"}
@@ -4,21 +4,26 @@ import { withErrorHandler } from '../lib/errors.js';
4
4
  import { createClient } from '../lib/client.js';
5
5
  import { loadPullConfig } from '../config/pull-config.js';
6
6
  import { fetchModelsForCodegen, filterModels } from '../codegen/fetch-models.js';
7
+ import { toPascalCase } from '../codegen/field-mapping.js';
7
8
  import { generateFieldTypesFile } from '../codegen/generators/field-types.js';
8
9
  import { generateConfigFile } from '../codegen/generators/config.js';
9
10
  import { generateModelTypes } from '../codegen/generators/model-types.js';
10
11
  import { generateModelIndex } from '../codegen/generators/model-index.js';
11
12
  import { generateModelDocuments } from '../codegen/generators/documents.js';
13
+ import { generateSwiftModelFile } from '../codegen/generators/swift-types.js';
14
+ import { generateSwiftFieldTypesFile } from '../codegen/generators/swift-field-types.js';
15
+ import { generateSwiftModelKeys } from '../codegen/generators/swift-model-keys.js';
12
16
  import { writeFiles } from '../codegen/write-files.js';
13
17
  export function registerPullCommand(program, globalOpts) {
14
18
  program
15
19
  .command('pull')
16
- .description('Generate TypeScript types and GraphQL documents from platform models')
20
+ .description('Generate TypeScript types, GraphQL documents, and Swift types from platform models')
17
21
  .option('--config <path>', 'Path to config file')
18
22
  .option('--only <models>', 'Comma-separated model keys to generate')
19
23
  .option('--no-prettier', 'Skip Prettier formatting')
20
24
  .option('--dry-run', 'Show what would be generated without writing')
21
25
  .option('--out <dir>', 'Override output directory for types')
26
+ .option('--swift <dir>', 'Generate Swift files to directory')
22
27
  .action(withErrorHandler(globalOpts, async (cmdOpts) => {
23
28
  const opts = globalOpts();
24
29
  const flags = {
@@ -27,6 +32,7 @@ export function registerPullCommand(program, globalOpts) {
27
32
  noPrettier: cmdOpts.prettier === false,
28
33
  dryRun: !!cmdOpts.dryRun,
29
34
  out: cmdOpts.out,
35
+ swift: cmdOpts.swift,
30
36
  };
31
37
  const config = await loadPullConfig(flags);
32
38
  // Fetch models
@@ -81,6 +87,28 @@ export function registerPullCommand(program, globalOpts) {
81
87
  path: resolve(typesDir, 'index.ts'),
82
88
  content: generateMainIndex(),
83
89
  });
90
+ // 6. Swift output (when configured)
91
+ if (config.output.swift) {
92
+ const swiftDir = resolve(cwd, config.output.swift);
93
+ // Shared types
94
+ files.push({
95
+ path: resolve(swiftDir, 'FieldTypes.swift'),
96
+ content: generateSwiftFieldTypesFile(),
97
+ });
98
+ // Model keys
99
+ files.push({
100
+ path: resolve(swiftDir, 'ModelKeys.swift'),
101
+ content: generateSwiftModelKeys(models),
102
+ });
103
+ // Per-model Swift files
104
+ for (const model of models) {
105
+ const swiftTypeName = toPascalCase(model.key);
106
+ files.push({
107
+ path: resolve(swiftDir, `${swiftTypeName}.swift`),
108
+ content: generateSwiftModelFile(model),
109
+ });
110
+ }
111
+ }
84
112
  // Dry run: list files
85
113
  if (config.dryRun) {
86
114
  console.log(chalk.bold('\nDry run — files that would be generated:\n'));
@@ -96,12 +124,16 @@ export function registerPullCommand(program, globalOpts) {
96
124
  // Summary
97
125
  const modelCount = models.length;
98
126
  const docCount = publicModels.length;
127
+ const swiftCount = config.output.swift ? models.length + 2 : 0; // +2 for FieldTypes + ModelKeys
99
128
  console.log(chalk.green(`\nGenerated ${files.length} file(s)`) +
100
- chalk.dim(` (${modelCount} model type(s), ${docCount} document(s))`));
129
+ chalk.dim(` (${modelCount} model type(s), ${docCount} document(s)${swiftCount > 0 ? `, ${swiftCount} Swift file(s)` : ''})`));
101
130
  console.log(chalk.dim(` Types: ${typesDir}`));
102
131
  if (docCount > 0) {
103
132
  console.log(chalk.dim(` Documents: ${docsDir}`));
104
133
  }
134
+ if (config.output.swift) {
135
+ console.log(chalk.dim(` Swift: ${resolve(cwd, config.output.swift)}`));
136
+ }
105
137
  }));
106
138
  }
107
139
  function generateMainIndex() {
@@ -7,11 +7,13 @@ export interface PullCliFlags {
7
7
  noPrettier?: boolean;
8
8
  dryRun?: boolean;
9
9
  out?: string;
10
+ swift?: string;
10
11
  }
11
12
  export interface ResolvedPullConfig {
12
13
  output: {
13
14
  types: string;
14
15
  documents: string;
16
+ swift?: string;
15
17
  };
16
18
  only: string[];
17
19
  includeInline: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"pull-config.d.ts","sourceRoot":"","sources":["../../src/config/pull-config.ts"],"names":[],"mappings":"AAAA;;GAEG;AA+CH,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7C,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,YAAY,GAClB,OAAO,CAAC,kBAAkB,CAAC,CAuB7B"}
1
+ {"version":3,"file":"pull-config.d.ts","sourceRoot":"","sources":["../../src/config/pull-config.ts"],"names":[],"mappings":"AAAA;;GAEG;AA+CH,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7D,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,YAAY,GAClB,OAAO,CAAC,kBAAkB,CAAC,CAwB7B"}
@@ -54,7 +54,8 @@ export async function loadPullConfig(flags) {
54
54
  // Merge: defaults ← file config ← CLI flags
55
55
  const types = flags.out ?? fileConfig.output?.types ?? DEFAULT_TYPES_DIR;
56
56
  const documents = fileConfig.output?.documents ?? DEFAULT_DOCS_DIR;
57
- const output = { types, documents };
57
+ const swift = flags.swift ?? fileConfig.output?.swift;
58
+ const output = { types, documents, ...(swift ? { swift } : {}) };
58
59
  const only = flags.only
59
60
  ? flags.only.split(',').map((s) => s.trim())
60
61
  : fileConfig.only ?? [];
@@ -7,6 +7,8 @@ export interface FoirPullConfig {
7
7
  types?: string;
8
8
  /** Directory for generated GraphQL documents (default: './src/generated/documents') */
9
9
  documents?: string;
10
+ /** Directory for generated Swift files (e.g., './generated/swift') */
11
+ swift?: string;
10
12
  };
11
13
  /** Filter to specific model keys */
12
14
  only?: string[];
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/config/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE;QACP,kFAAkF;QAClF,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,uFAAuF;QACvF,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,oCAAoC;IACpC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,qEAAqE;IACrE,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,EAAE,cAAc,CAAC;CACvB;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,UAAU,CAE3D"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/config/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE;QACP,kFAAkF;QAClF,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,uFAAuF;QACvF,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,sEAAsE;QACtE,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,oCAAoC;IACpC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,qEAAqE;IACrE,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,EAAE,cAAc,CAAC;CACvB;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,UAAU,CAE3D"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eide/foir-cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Universal platform CLI for EIDE — scriptable, composable resource management",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -23,6 +23,13 @@
23
23
  "dist",
24
24
  "README.md"
25
25
  ],
26
+ "scripts": {
27
+ "build": "tsc",
28
+ "dev:cli": "tsx src/cli.ts",
29
+ "check-types": "tsc --noEmit",
30
+ "test": "vitest run",
31
+ "test:watch": "vitest watch"
32
+ },
26
33
  "keywords": [
27
34
  "foir",
28
35
  "eide",
@@ -45,21 +52,14 @@
45
52
  "prettier": "^3.4.2"
46
53
  },
47
54
  "devDependencies": {
55
+ "@foir/config": "workspace:*",
48
56
  "@types/inquirer": "^9.0.7",
49
57
  "@types/node": "^22.5.0",
50
58
  "tsx": "^4.20.0",
51
59
  "typescript": "5.9.2",
52
- "vitest": "^3.2.4",
53
- "@foir/config": "0.0.1"
60
+ "vitest": "^3.2.4"
54
61
  },
55
62
  "engines": {
56
63
  "node": ">=18.0.0"
57
- },
58
- "scripts": {
59
- "build": "tsc",
60
- "dev:cli": "tsx src/cli.ts",
61
- "check-types": "tsc --noEmit",
62
- "test": "vitest run",
63
- "test:watch": "vitest watch"
64
64
  }
65
- }
65
+ }