Skip to content

Commit dff7bf0

Browse files
masonwyatt23claude
andcommitted
fix: filter icon/mockup/skeleton components, fix Prisma type leak
Real-world testing against TourVault (164-file Next.js app) found: - 20 SVG icon components (ArrowRightIcon, etc.) treated as screens → filtered - Mockup/Skeleton/Placeholder components treated as screens → filtered - Prisma enum imports (Import("...").$Enums.Status) leaked into Swift → sanitized - SwiftData model filenames and class names collided with @observable stores → SwiftData classes renamed from {Entity}Store to {Entity}Record → SwiftData files renamed to {Entity}DataStore.swift Result: TourVault output 129→107 files, 0 syntax errors, 0 junk views Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 856407e commit dff7bf0

4 files changed

Lines changed: 29 additions & 16 deletions

File tree

src/generator/model-generator.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ function typeDefinitionToSwiftType(td: TypeDefinition): string {
4040
if (/[<>|{}]|=>/.test(result)) {
4141
result = 'String';
4242
}
43+
// Catch leaked import paths (e.g., Prisma enums: Import("...").$Enums.Status)
44+
if (/Import\(|import\(|node_modules|\.prisma|\.\/|\.\.\//.test(result)) {
45+
result = 'String';
46+
}
4347
return result;
4448
}
4549

@@ -632,7 +636,7 @@ function swiftDataFieldType(swiftType: string): string {
632636

633637
function generateSwiftDataModel(entity: Entity, model: SemanticAppModel): string {
634638
const structName = pascalCase(entity.name);
635-
const storeName = `${structName}Store`;
639+
const storeName = `${structName}Record`;
636640
const fields = filterSwiftFields(entity.fields ?? []).map(analyseField);
637641
const hasId = fields.some((f) => f.isId);
638642
const relationships = entity.relationships ?? [];
@@ -648,7 +652,7 @@ function generateSwiftDataModel(entity: Entity, model: SemanticAppModel): string
648652
if (relationshipFieldNames.has(field.name)) {
649653
const rel = relationships.find(r => camelCase(r.fieldName) === field.name);
650654
if (rel) {
651-
const targetStore = `${pascalCase(rel.targetEntity)}Store`;
655+
const targetStore = `${pascalCase(rel.targetEntity)}Record`;
652656
if (rel.type === 'one-to-many' || rel.type === 'many-to-many') {
653657
lines.push(` @Relationship var ${field.name}: [${targetStore}]`);
654658
} else {
@@ -716,8 +720,8 @@ export function generateSwiftDataModels(model: SemanticAppModel): GeneratedFile[
716720
for (const [groupKey, groupEntities] of groups) {
717721
const lines: string[] = [];
718722
const groupFileName = groupEntities.length === 1
719-
? `${pascalCase(groupEntities[0].name)}Store.swift`
720-
: `${pascalCase(groupKey)}Stores.swift`;
723+
? `${pascalCase(groupEntities[0].name)}DataStore.swift`
724+
: `${pascalCase(groupKey)}DataStores.swift`;
721725
const sourceRef = groupEntities[0]?.sourceFile ? relativeSourcePath(groupEntities[0].sourceFile) : groupKey;
722726
lines.push(`// Generated by Morphkit`);
723727
lines.push(`// SwiftData persistence models from: ${sourceRef}`);
@@ -753,7 +757,7 @@ export function generateDataManager(model: SemanticAppModel): GeneratedFile | nu
753757

754758
for (const entity of eligible) {
755759
const structName = pascalCase(entity.name);
756-
const storeName = `${structName}Store`;
760+
const storeName = `${structName}Record`;
757761
const varName = camelCase(entity.name);
758762
const pluralVar = varName.endsWith('s') ? varName : `${varName}s`;
759763
lines.push('');

src/generator/project-generator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ function generateAppEntry(model: SemanticAppModel): GeneratedFile {
6161
lines.push(' ContentView()');
6262
lines.push(' }');
6363
if (hasSwiftData) {
64-
const storeTypes = eligible.map(e => `${pascalCase(e.name)}Store.self`);
64+
const storeTypes = eligible.map(e => `${pascalCase(e.name)}Record.self`);
6565
lines.push(` .modelContainer(for: [${storeTypes.join(', ')}])`);
6666
}
6767
lines.push(' }');

src/generator/swiftui-generator.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,8 @@ function sanitizeSwiftTypeName(name: string | undefined): string | undefined {
257257
if (JS_DOM_TYPES.has(name)) return undefined;
258258
// Reject names containing TS syntax (generics, unions, object literals, arrow fns)
259259
if (/[<>|{}]|=>/.test(name)) return undefined;
260+
// Reject leaked import paths (Prisma enums, relative paths, node_modules)
261+
if (/Import\(|import\(|node_modules|\.prisma|\.\/|\.\.\/|\$Enums/.test(name)) return undefined;
260262
return name;
261263
}
262264

@@ -4047,6 +4049,13 @@ export function isMarketingScreen(screen: Screen): boolean {
40474049

40484050
if (matchesMarketingName) return true;
40494051

4052+
// Filter icon components — SVG/icon wrappers are not screens
4053+
if (/Icon$/i.test(screen.name) || /^(Arrow|Check|Play|Chart|Users|Mic|Folder|Message|Sparkles|Trophy|Map|Upload|Search|File|Status|Trend|Star|Heart|Bell|Settings|Lock|Eye|Pen|Trash|Plus|Minus|Close|Menu|Calendar|Clock|Download|Share|Link|Filter|Sort|Grid|List|Home|Mail|Phone|Camera|Video|Globe|Flag|Tag|Pin|Bookmark|Shield|Zap|Sun|Moon|Cloud|Wifi|Battery|Volume|Pause|Stop|Skip|Refresh|Rotate|Crop|Layers|Copy|Clipboard|Info|Help|Alert|Warning)Icon$/i.test(screen.name)) return true;
4054+
4055+
// Filter mockup/demo/skeleton/placeholder components — these are UI decorations, not screens
4056+
if (/Mockup$|Skeleton$|Placeholder$|Shimmer$|Loader$/i.test(screen.name)) return true;
4057+
if (/^(Animated|Loading|Skeleton|Shimmer)/i.test(screen.name) && screen.dataRequirements.length === 0 && screen.actions.length === 0) return true;
4058+
40504059
// Blog-prefixed screens that are purely static (no data, state, or actions)
40514060
const isStatic =
40524061
screen.dataRequirements.length === 0 &&

test/generator/swiftdata-persistence.test.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ describe('SwiftData Persistence Layer', () => {
166166
const files = generateSwiftDataModels(model);
167167

168168
expect(files.length).toBeGreaterThan(0);
169-
const storeFile = files.find(f => f.path.includes('ProductStore'));
169+
const storeFile = files.find(f => f.path.includes('DataStore'));
170170
expect(storeFile).toBeDefined();
171171
});
172172

@@ -176,7 +176,7 @@ describe('SwiftData Persistence Layer', () => {
176176
const content = files[0].content;
177177

178178
expect(content).toContain('@Model');
179-
expect(content).toContain('final class ProductStore');
179+
expect(content).toContain('final class ProductRecord');
180180
});
181181

182182
test('SwiftData model imports SwiftData', () => {
@@ -211,12 +211,12 @@ describe('SwiftData Persistence Layer', () => {
211211
expect(content).toContain('@Attribute(.unique)');
212212
});
213213

214-
test('SwiftData model uses Store suffix naming', () => {
214+
test('SwiftData model uses Record suffix naming', () => {
215215
const model = createMinimalModel([productEntity, userEntity]);
216216
const files = generateSwiftDataModels(model);
217217

218-
const productStore = files.find(f => f.content.includes('ProductStore'));
219-
const userStore = files.find(f => f.content.includes('UserStore'));
218+
const productStore = files.find(f => f.content.includes('ProductRecord'));
219+
const userStore = files.find(f => f.content.includes('UserRecord'));
220220

221221
expect(productStore).toBeDefined();
222222
expect(userStore).toBeDefined();
@@ -275,7 +275,7 @@ describe('SwiftData Persistence Layer', () => {
275275
const content = files[0].content;
276276

277277
expect(content).toContain('@Relationship');
278-
expect(content).toContain('[ProductStore]');
278+
expect(content).toContain('[ProductRecord]');
279279
});
280280
});
281281

@@ -327,7 +327,7 @@ describe('SwiftData Persistence Layer', () => {
327327
const file = generateDataManager(model);
328328
const content = file!.content;
329329

330-
expect(content).toContain('FetchDescriptor<ProductStore>');
330+
expect(content).toContain('FetchDescriptor<ProductRecord>');
331331
});
332332

333333
test('DataManager imports SwiftData', () => {
@@ -371,11 +371,11 @@ describe('SwiftData Persistence Layer', () => {
371371
// Verify the naming convention that project-generator uses
372372
const storeTypeNames = eligible.map(e => {
373373
const name = e.name.charAt(0).toUpperCase() + e.name.slice(1);
374-
return `${name}Store.self`;
374+
return `${name}Record.self`;
375375
});
376376

377-
expect(storeTypeNames).toContain('ProductStore.self');
378-
expect(storeTypeNames).toContain('UserStore.self');
377+
expect(storeTypeNames).toContain('ProductRecord.self');
378+
expect(storeTypeNames).toContain('UserRecord.self');
379379
});
380380
});
381381
});

0 commit comments

Comments
 (0)